由于语言在自动内存管理上的欠缺,C/C++在内存管理上从来都是需要程序员小心处理的一个方面,当项目代码上了一定规模,内存消耗和泄漏就会成为程序稳定运行的第一大敌。如果不在项目之初就建立内存管理和泄漏检测机制,后面蛋疼的问题就会接踵而来。这篇文章着重讨论内存泄漏检测,而内存管理与具体项目类型关系密切,后面有时间我会着重游戏项目来讨论。
内存泄漏检测的基本步骤是:1)包装(重载)内存分配/释放API;2)进行内存分配时记下相关信息:地址、大小、调用栈;3)释放时清除之前记录的对应信息;4)程序退出时(确保在所有内存释放操作完成之后),输出剩下的记录。其中,对进行分配操作是的调用栈回溯是个重点信息,它能够帮助我们找出内存泄漏代码。
Windows中的Dbghelp库提供了丰富的调试API。StackWalk应该是进行栈回溯最直接的一种接口了,但是它不够快。如果能先记录下调用栈上的CALL指令地址,然后在输出日志时解析出符号,将会大大降低检测机制对程序本身性能的影响。Dbghelp库中提供了Sym*FromAddr系列API,可以通过指令地址获取函数符号,那么剩下的就是如何记录指令地址的问题了。从网上借了一张x86调用栈示意图,如下:
从图中可以看出,Callee的EBP始终指向Caller的EBP,EBP下面是指向Caller下一条指令(注意x86体系下栈的增长方向是小地址),因此通过EBP就可以回溯整个调用栈了。通过下面代码可以实现此功能:
宏参数frame是个void*指针数组,数组的大小取决于想要回溯的栈深度。内存分配和回收的包装代码如下:
我们看到,内存管理系统内部终究还是要使用语言提供的内存分配/释放API,只要配对实现了分配与释放管理,系统内部的无泄漏是很容易保证的。在这里着重讲解原理,就不重载new/delete operator了。最后看一下调用栈函数符号的回溯代码:
我们用下面代码做测试用例:
泄漏检测结果:
参考:
[1] Using DbgHelp
[2] Intel x86 Function-call Conventions - Assembly View