使用BoundsChecker检测内存泄漏

来源:互联网 发布:荣耀9安兔兔跑分数据 编辑:程序博客网 时间:2024/05/20 02:29

使用BoundsChecker检测内存泄漏:

  
BoundsChecker采用一种被称为 Code Injection的技术,来截获对分配内存和释放内存的函数的调用。简单地说,当你的程序开始运行时,BoundsChecker的DLL被自动载入进程的地址空间(这可以通过system-level的Hook实现),然后它会修改进程中对内存分配和释放的函数调用,让这些调用首先转入它的代码,然后再执行原来的代码。BoundsChecker在做这些动作的时,无须修改被调试程序的源代码或工程配置文件,这使得使用它非常的简便、直接。

   这里我们以malloc函数为例,截获
其他的函数方法与此类似。

   需要被截获的函数可能在DLL中,也可能在程序的代码里。比如,如果静态连结C-Runtime Library,那么malloc函数的代码会被连结到程序里。为了截获住对这类函数的调用,BoundsChecker会动态修改这些函数的指令。

   以下两段汇编代码,一段没有BoundsChecker介入,另一段则有BoundsChecker的介入:

   126: _CRTIMP void * __cdecl malloc (

   127: size_t nSize

   128: )

   129: {

   00403C10 push ebp

   00403C11 mov ebp,esp

   130: return _nh_malloc_dbg(nSize, _newmode, _NORMAL_BLOCK, NULL, 0);

   00403C13 push 0

00403C15 push 0

   00403C17 push 1

   00403C19 mov eax,[__newmode (0042376c)]

   00403C1E push eax

   00403C1F mov ecx,dword ptr [nSize]

   00403C22 push ecx

   00403C23 call _nh_malloc_dbg (00403c80)

   00403C28 add esp,14h

   131: }

   以下这一段代码有BoundsChecker介入:

   126: _CRTIMP void * __cdecl malloc (

   127: size_t nSize

   128: )

   129: {

   00403C10 jmp 01F41EC8

   00403C15 push 0

   00403C17 push 1

   00403C19 mov eax,[__newmode (0042376c)]
  
   00403C1E push eax

00403C1F mov ecx,dword ptr [nSize]

   00403C22 push ecx

   00403C23 call _nh_malloc_dbg (00403c80)

   00403C28 add esp,14h

   131: }

  当BoundsChecker介入后,函数malloc的前三条汇编指令被替换成一条jmp指令,原来的三条指令被搬到地址01F41EC8处了。当程序进入malloc后先jmp到01F41EC8,执行原来的三条指令,然后就是BoundsChecker的天下了。大致上它会先记录函数的返回地址(函数的返回地址在stack上,所以很容易修改),然后把返回地址指向属于BoundsChecker的代码,接着跳到malloc函数原来的指令,也就是在00403c15的地方。当malloc函数结束的时候,由于返回地址被修改,它会返回到BoundsChecker的代码中,此时BoundsChecker会记录由malloc分配的内存的指针,然后再跳转到到原来的返回地址去。

   如果内存分配/释放函数在DLL中,BoundsChecker则采用另一种方法来截获对这些函数的调用。BoundsChecker通过修改程序的DLL Import Table让table中的函数地址指向自己的地址,以达到截获的目的。关于如何拦截
Windows的系统函数,《程序员》杂志2002年8期,《API钩子揭密(下)》,对修改导入地址表做了概要的描述。我就不再赘述。

  截获住这些分配和释放函数,BoundsChecker就能记录被分配的内存或资源的生命周期。接下来的问题是如何与源代码相关,也就是说当BoundsChecker检测到内存泄漏,它如何报告这块内存块是哪段代码分配的。答案是调试信息(DebugInformation)。当我们编译一个Debug版的程序时,编译器会把源代码和二进制代码之间的对应关系记录下来,放到一个单独的文件里(.pdb)或者直接连结进目标程序中。有了这些信息,调试器才能完成断点设置,单步执行,查看变量等功能。BoundsChecker支持多种调试信息格式,它通过直接读取调试信息就能得到分配某块内存的源代码在哪个文件,哪一行上。使用Code Injection和DebugInformation,使BoundsChecker不但能记录呼叫分配函数的源代码的位置,而且还能记录分配时的CallStack,以及Call Stack上的函数的源代码位置。这在使用像MFC这样的类库时非常有用,以下我用一个例子来说明:

void ShowXItemMenu()

   {

   …

   CMenu menu;

   menu.CreatePopupMenu();

   //add menu items.

   menu.TrackPropupMenu();

   …

   }

   void ShowYItemMenu( )

   {

   …

   CMenu menu;

   menu.CreatePopupMenu();

   //add menu items.

   menu.TrackPropupMenu();

   menu.Detach();//this will cause HMENU leak

   …

   }

   BOOL CMenu::CreatePopupMenu()

   {


   hMenu = CreatePopupMenu();

   …

   }

  当调用ShowYItemMenu()时,我们故意造成HMENU的泄漏。但是,对于BoundsChecker来说被泄漏的HMENU是在classCMenu::CreatePopupMenu()中分配的。假设的你的程序有许多地方使用了CMenu的CreatePopupMenu()函数,如果只是告诉你泄漏是由CMenu::CreatePopupMenu()造成的,你依然无法确认问题的根结到底在哪里,在ShowXItemMenu()中还是在ShowYItemMenu()中,或者还有其它的地方也使用了CreatePopupMenu()?有了CallStack的信息,问题就容易了。BoundsChecker会如下报告泄漏的HMENU的信息:

Function

File

Line

CMenu::CreatePopupMenu

E:/8168/vc98/mfc/mfc/include/afxwin1.inl

1009

ShowYItemMenu

E:/testmemleak/mytest.cpp

100

这里省略了其他的函数调用


   如此,我们很容易找到发生问题的函数是ShowYItemMenu()。当使用MFC之类的类库编程时,大部分的API调用都被封装在类库的class里,有了Call Stack信息,我们就可以非常容易的追踪到真正发生泄漏的代码。

   记录Call Stack信息会使程序的运行变得非常慢,因此默认情况下BoundsChecker不会记录Call Stack信息。可以按照以下的步骤打开记录Call Stack信息的选项开关:

   1. 打开菜单:BoundsChecker|Setting…

   2. 在Error Detection页中,在Error Detection Scheme的List中选择Custom

   3. 在Category的Combox中选择 Pointer and leak error check

   4. 钩上Report Call Stack复选框

   5. 点击Ok

基于Code Injection,BoundsChecker还提供了API Parameter的校验功能,memory over run等功能。这些功能对于程序的开发都非常有益。由于这些内容不属于本文的主题,所以不在此详述了。

   尽管BoundsChecker的功能如此强大,但是面对隐式内存泄漏仍然显得苍白无力。所以接下来我们看看如何用Performance Monitor检测内存泄漏。(下一篇)