使用HandleSpy定位托管代码句柄泄漏

来源:互联网 发布:会计电算化是什么软件 编辑:程序博客网 时间:2024/05/12 14:48

引子

 我们知道句柄泄漏原因多种多样,一般泄漏的对象主要是内核句柄 、 文件句柄、互操作句柄等。
由于Framework的GC帮我们干了很多事情,所以很多C#程序员养成了吃饭后不洗碗的习惯,new出来的对象基本不考虑如何清理。
一般的对象当然不用考虑,但是遇到需要释放的对象(如IO操作、内核对象创建等),忘了释放,那问题便产生了。

环境说明

本文所有演示均在win7 32位。
需要安装软件:
  • VS2010
  • dotNetFramework4.0
  • WinDbg 
  • HandleSpy

 方案

如果项目业务流程复杂,无法通过简单的二分法定位。那就只有依赖工具了。
之前找了不少的工具来查找,都没有达到预期的目标。

直到那次在知乎上看到了tishion的文章 内核对象&句柄&泄漏&检测 ,才知道HandleSpy这个工具。
这个工具能通过HOOK API的方式判断创建的句柄在一段时间内是否释放,并能显示各种句柄创建的调用堆栈地址信息。
但用这个工具不能直接分析出托管模块的堆栈。与作者沟通后也没有找到合适的解决方案。

想法一:
曾想通过注入进程的DLL调用托管代码提供的API返回堆栈信息,这在理论上当然是可行的。
可实际应用的时候却发现了一大堆问题,比如注入的DLL已经HOOK 了一大堆API,在HOOK的方法里去调用托管代码,又会调用各种已经被hook的方法。
总之,实践说明要将这种方案完美实施可能会耗费大量的时间,最后托管与本地代码之间这样频繁调用会不会出问题还不好说。
所以最后我还是选择放弃了该方案。
想法二:
就在我决定放弃该方案不久,发现WinDbg原来也能调试托管代码。所以决定试试联合Windbg通过堆栈地址定位到具体代码。理论上该方案也可行,而且比前一个方案要优雅那么一点,只不过 需要多动动手。

实施方案

为了实施该方案,新建一个C#控制台应用程序,写一小段模拟句柄泄漏的代码,定时创建一个不会终止的线程。
 class Program    {        static void Main(string[] args)        {            Thread thd;            while (true)            {                thd = new Thread(new ThreadStart(TestMethod));                thd.Start();                Thread.Sleep(2000);            }         }        static void TestMethod()        {            while (true)            {                Thread.Sleep(2000);            }        }    }


然后编译执行程序,我这里叫ConsoleApplication1
打开HandleSpy(注意需要管理员权限),点击选择进程图标。附加到ConsoleApplication1.exe


进程附件后会出现统计句柄窗口,实时显示当前句柄的数量与统计图
 


等待程序运行一段时间,应该会有大量的句柄泄漏了。点击停止会出现时间线上的句柄个数统计图。  


鼠标框选点击右键-》查看选区内函数调用,就可查看泄漏项的堆栈调用信息,如下图所示。CreateEventW是由UnknowModule模块调用托管库完成的。此处有可能是一个泄漏项。 调用的地址就是0x002900e2,仅通过HandleSpy是无法得知该模块的信息的。



接下来重点来了,我要通过WinDBG查找0x002900e2这个地址相关的托管堆栈信息。

打开WinDbg->附加到进程 ConsoleApplication1.exe  ,附加之后程序会中断。
输入命令 .load 加载sos扩展
.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll 
然后!u 查看托管堆栈信息 
!u  2900e2
输出如下,可以找到这一行
>>> 0x2900e2 90 nop
往上看即是调用Thread.Start()的方法。由该段输出可知,Program.cs 41行(本地环境该文件中还有不少测试代码)创建的句柄没有在框选范围内释放、 回到源码文件可知,正是 调用 thd.Start(); 这一行 

结语

后来发现VS也能加载sos.dll扩展,所以没有winDBG也能完成上述的操作。 
 在托管代码中,Framework内部的调用或是一些资源回收策略可能导致误判。
也许还有更加简单方便的方法解决此类问题,欢迎各位指点。



原创粉丝点击