由使用LeakDialog时遇到的问题而引出的一些分析
由使用LeakDialog时遇到的问题而引出的一些分析
前段时间在使用leakDialog检测调用malloc和new所分配的内存泄露时,发现其根本不起作用!这让我百思不得其解!周末有时间研究了一下终于弄清了原因所在。本着分享的精神,将其写成博文,希望对大家有用。
LeakDialog是用于内存泄露检测的常用工具。使用LeakDialog不需要添加任何代码,就可以捕获各种形式的内存泄露。同时它还能显示执行内存分配的栈回溯以及内存分配的统计信息。
其使用方法很简单,在此不再介绍它的使用方法,有不懂的可以查找其他资料。
LeakDialog支持6种不同的分配器:
虚拟内存分配器
堆分配器
MPHeap分配器
COM的AllocatorCoTaskmem分配器
COM的私有分配器
C运行时分配器
我们在使用拦截new和malloc分配的内存泄露时就是使用了C运行时(CRT C Run Time)分配器。
LeakDialog原理
LeakDialog通过微软的Detours库来拦截对内存分配和释放操作的调用,如malloc或free函数。Microsoft Detours库是在二进制级别对现有代码进行修改。其主要原理为:在程序执行过程中将所要拦截的函数的起始位置的若干指令替换为一条无条件跳转指令。该修改是动态修改的,不会修改二进制文件。
LeakDialog通过上述技术拦截每一次的内存分配和释放操作。点击log按钮时LeakDialog会找出已经被分配但还未被释放的内存操作。
今天我们仅仅讨论LeakDialog的C运行时分配器对调用malloc和new分配的内存泄露的检测。将通过以下几个测试用例一步步深入挖掘。
实验一:测试Leakdialog对内存泄露的检测。
使用vc6.0编写的控制台程序。使用默认配置,主要代码如下:
#include <iostream> void func2() { int *p = new int[200]; if(p) { std::cout<<"func2 sucecss!"<<std::endl; } } void func3() { char *p = (char*)malloc(sizeof(char) * 100); if(p) { std::cout<<"func3 success!"<<std::endl; } } void func1() { func2(); func3(); } int main() { std::cout<<"by ithzhang---------blog.csdn.net/ithzhang"<<std::endl; getchar(); func1(); getchar(); return 0; }
编译运行后,使用LeakDialog进行监视,在执行完func1后点击log按钮,发现没有产生日志文件。这说明LeakDialog没有检测到内存泄露。通过代码我们可以发现程序明明出现了两处内存泄露。这说明LeakDialog没有起作用。相信很多童鞋在刚接触LeakDialog时都会进行类似的测试。但原因究竟为何?
这里不卖关子了。由于控制台程序默认配置使用静态链接CRT(C运行时)库,而LeakDialog的C运行时分配器仅仅拦截位于msvcrt.dll中的指定的内存分配操作。这是导致该问题的罪魁祸首!
选择工程->设置->C++ ->Code generation 我们看到了工程的默认配置: Userun-time library选项的值为Debugsingle-Thread表示程序在链接时使用c运行库的静态版本。如下图:
要使程序在运行时动态的加载CRT库,只需将Use run-time library修改为Multithreaded DLL或Debug Multithreaded DLL。Debug表示用于调试的dll。如下图所示:
修改配置后再次执行刚才的操作,这次我们看到log文件产生了,使用ie打开后,如下图:
上图显示出了两次内存泄露操作,分别位于func2和func3中。由此我们很容易的找出内存泄露的位置。
注意在LeakDialog中配置调试符号所在目录,否则function和filename将会显示为空,同时不能选中Use DebugHelp Stack walk API to Walk stacks。
实验二:验证LeakDialog原理
1.唤出windbg,选择open Executable 选择实验一产生的控制台程序。由于在本地生成,因此不要配置调试符号和源代码路径。
2.程序暂停在了在了ntdll.dll中的初始断点处。输入bp msvcrt!malloc命令并回车,在位于msvcrt.dll中的malloc函数的入口处设置软件断点。
3.按F5继续执行,并让程序执行func1.
4.可以看到程序停在了msvcrt.dll的malloc函数入口处。
如下图,我们看到了malloc的汇编代码。
再次执行上述1-4,只是在2和3之间打开LeakDialog开启CRT allocator监视该进程。
这一次程序仍然执行到malloc入口处,如下图:
比较上述两图,我们可以发现第二张截图malloc的第一条指令被替换成了jmp指令。
上述被替换的指令就是LeakDialog为了监视位于msvcrt.dll中的malloc函数而插入的。感兴趣的童鞋可以继续分析jmp指令后的一些操作。
通过同样的方法我们同样可以得到对new的调用:
开启LeakDialog后,operator new函数入口的指令被替换,如下图:
实验三:vc2005编译上述代码,重复做实验二(由于vc2005默认配置为动态链接CRT库,因此不需要修改配置)。
发现并没有生成log,说明leakdialog并没有检测到内存泄露。实验继续进行。
使用windbg打开生成的exe。在调用func2之前打开leakdialog对该进程进行监视,并在msvcr80D.dll的malloc处设置断点。
执行func2,发现程序并未停到断点处,输入u msvcrt80D!malloc 对位于msvcrt80D.dll中的malloc进行反汇编,发现其第一条指令并未被替换,如下图:
而接下来的情况更让人难以置信:输入u msvcrt!malloc 对位于msvcrt.dll中的malloc进行反汇编,如下图:
可以发现位于msvcrt.dll中的malloc的第一条指令被Leakdialog替换了无条件跳转指令。
查看operator new也发现了类似的情况:
msvcrt.dll中的operator new的替换,如下图:
这究竟是为何?
参考帮助文档,有下面一句话:
The C Runtime Allocator tracks the following calls from MSVCRT.DLL:
- malloc,
- calloc,
- realloc,
- free,
- new,
- new[],
- delete and
- delete[]
这次终于真相大白,原来Leakdialog仅仅拦截位于msvcrt.dll中的内存分配函数。
由于msvcrt.dll是vc6.0使用的CRT库,因此leakdialog仅仅对使用vc6.0编写的程序有效。因此对于由vc2005编译的使用msvcr80.dll的程序的内存泄露,leakdialog无法检测。
原因或许就是leakdialog版本太老所致,那时候vc2005还未出现。
本次实验结束,你看懂了吗?
2014.2.11于浙江杭州