分类: C/C++
C++ Memory Leak Finder
C++内存泄露检测器
leakfinder.zip
作者:Fredrik Bornander(高级攻城狮)
Introduction 引言In this article I will discuss a way to build a memory leak detection program for C and C++ applications.This is not an attempt at writing a fully fledged leak detector but rather an introduction to one way (of many) ways of finding leaks.
本文将要探讨的是一种用于检查C与C++应用程序的内存泄露的方法,鉴于授人以鱼不如授人以渔的理念,本文主旨并非在于提供一个完整可用的泄露检测工具,而是重点介绍一种通用的方法。
The approach I've gone for is library injection and even if the C++ source code provided is for Linux (tested on Ubuntu 11.10) the method should work for any platform that allows library injection.
我准备要阐述的就是库注入的方法,文中采用的C++测试代码尽管是在Linux平台运行的(Ubuntu11.10),但是此方法并不限于Linux平台,只要库注入可用,任何平台都可引入此方法。
What is a leak 什么是泄露There are many ways in which an application can leak resources, and there are many different resources that can be leaked. In this article I'll focus on and only look for memory leaks (not file handles or sockets or any other resource that might be allocated), but even when constraining the scope to just memory there are still many different kinds of leaks. When talking about memory leaks in C++, most people think of scenarios like this (very simplified one);
应用程序会因各种问题会产生资源泄露,同时也有各种不同的资源会有泄露隐患。本文将要探讨的只是内存泄露(不是文件句柄、sockets或者其它可能存在泄露的资源),当然即便是将范围缩小至“内存”泄露的问题,也是有各种不同种类的泄露。当讨论到C++中的内存泄露时,很多人脑海中立马浮现的是如下代码所示的问题(或者类似的):
点击(此处)折叠或打开
Memory is allocated but is not deleted/unallocated before the reference to the allocated memory goes out of scope. This means of course that the memory will be unavailable for use as well as for further allocations until the program terminates.
上述内存分配之后在超出该内存的引用范围后并没有释放,这就意味着这块内存在整个进程的周期范围内将不可被再次使用,因而产生了泄漏。
But memory leaks can appear in other more subtle ways as well, sometimes the memory is still referenced but just not used, such as when items are regularly added to a std::vector without ever being released. If such astd::vector is never cleared and still never again looked at by the application it can be considered leak, even though the memory is still referenced.
但往往内存泄漏会以各种其它形态出现,比如有时内存仍然是被引用的但是没有使用,比如有时将item加入到std::vector中之后没有释放,如果此vector没有被清空并且没有再被应用程序操作调用,则也被认作是产生了泄漏,尽管内存仍然可以引用。
In this article, for simplicity, I'll only look at the first scenario; allocated memory that is not deallocated. In short, I'll show a way of tracking each call to malloc and free and record information so that they can be married up.
本文主要阐述的是一种方法,因而为简单起见,主要讲述第一种情形的泄露,即分配的内存没有被释放。简而言之就是我会去跟踪malloc与free的调用,以此来保证二者的次数时匹配的。
You might argue that you're using C++ and are therefore allocating and deallocating your memory using new anddelete but in most cases the implementation of those will still call the C versions malloc and free.
或许你要强调使用的是C++,因而使用的是new和delete来做内存的分配与释放,但是究其根源,使用的还是底层c调用的malloc与free函数。
Library Injection 库注入Library Injection is when a user tells the OS to first look in LibraryA for system wide methods before looking in the "standard" libraries, where LibraryA is a library containing an overload of a system function. On some Linux and Unix distributions this can be achieved using the LD_PRELOAD environment variable to set the library that is to be injected.
所谓库注入,是当用户告诉系统在涉及到系统范围内的函数调用时,当“库A”中包含了系统函数的重载时,首先调用“库A”而非直接调用标准库中的此类函数,在一些Linux或者Unix发行版本中,可以通过LD_PRELOAD环境变量来设置要注入的库。
To track memory allocations and deallocations a shared library containing overloads for malloc and free must be created and then injected so that whenever an allocation of memory is requested the custom version is hit first.
为了跟踪内存的分配和释放,一个包含malloc和free函数重载的动态库必须先创建再注入进去,这样一旦分配内存的函数被调用,那么用户预设的重载的版本将会被先调用。
First step is to create a C++ file, leakfinder.cpp, that will contain the two methods;
首先创建一个C++文件,leakfinder.cpp将包含如下两个函数:
点击(此处)折叠或打开
The above example can be compiled into a shared library using g++ (which is the compiler I'll use for the C++ code through out this article) by running:
上述函数可以使用g++来编译成动态链接库(本文中Demo将全部使用C++代码来实现)。
Using an example C program that is built to leak we can test this, the example C program is called c_example and it looks like this:
使用一段C程序可以测试泄露的问题,C程序如下c_example如下所示:
点击(此处)折叠或打开
It is important to test the injection in a terminal other than the one used to compile the source code or the injection might (will) interfere with the compiler and any other commands that use malloc or free.
要强调的是,测试这段注入代码时需要在特定的终端上运行,以避免编译源码或者其它可能调用到malloc或free函数的应用被此库函数拦截下来而导致操作失败。
Therefore, when working with this example make sure you have one terminal open for compiling and building (both the leak finder and the example applictions), and one where the LD_PRELOAD environment variable is set where you can run the test applications.
因而,测试这些代码时,你需要有两个终端来操作,一个用于编译(包括“泄露检测工具”以及应用测试代码),一个用来设置LD_PRELOAD环境变量后来运行测试代码。
Another way of making sure the pre-load only is applicable for the c_example application is by running it like this;LD_PRELOAD=./leakfinder.so c_example.
另外一个确保pre_load只为C_example使用的方法是,如下这样运行:LD_PRELOAD=./leadfinder.so c_example
Set the LD_PRELOAD in your terminal to point to the leakfinder.so and run the c_example test application using;
在终端上将LD_PRELOAD设置为leakfinder.so再运行c_example程序可以这样设置:
When first run it turns out that no allocations or deallocations are detected, this is because of the linkage. As g++(a C++ and not a C compiled) was used to compile the leakfinder.cpp file it has applied name mangling and that means he function names do not match the intended system functions malloc and free. To resolve this issue the functions has to be declared with C linkage:
首次运行会发现没有任何分配或释放操作被探测到,这是因为链接问题,因为g++编译器是c++而非c编译器,编译leadfinder.cpp时考虑到C++中存在的重载问题,函数名并没有匹配系统调用的malloc与free函数,解决此问题的办法是函数定义之前增加如下修饰部分:
点击(此处)折叠或打开
When the leak finder with the correct linkage is preloaded and the example is run it will generate an output that looks something like this;
当此链接问题解决后,泄露检查工具会被预加载,测试代码运行的结果如下所示:
Loads and loads of allocations and deallocations intercepted! This is because even though the test application only explicitly allocates a few things other parts also allocates and deallocates stuff, such as the printf and even the cout calls in the injected code.
我们看到不仅仅是我们认为调用malloc与free的地方被拦截了。
这是因为即便我们在测试demo中调用内分配的地方有限,但是其它一些现有的函数如printf甚至cout调用内部都调用了分配和释放内存函数。
The print out ends with a Segmentation fault which is of course because you cannot hope to replace an actual memory allocation with a print statement and expect stuff to still work , but the method is proved to work.
最后输出的段错误提示,是因为没有办法使用简单的输出操作来替代真正的内存分配并且还能让其继续保持工作。但是这至少证明了我所阐述的方法是可行的。
Actual implementation 真正的实现As it is clearly very important for the overloaded methods malloc and free to still do the work they are intended to do (as in allocating and deallocating memory on and from the heap) the injected code must look up pointers to the actual implementation and delegate the calls to these before tracking them.
显而易见的是我们重载的malloc与free函数是需要真正意义上实现它们原有的功能,即能实现在堆上动态的分配和释放内存,因而拦截的代码部分除了跟踪,最终还是需要访问这些函数真正实现的指针。
Finding it 找到函数实现To find the actual implementation is done by calling dlsym (defined in dlfcn.h), this function returns function pointers to functions made available using dlopen. Using the constant RTLD_NEXT the previous entry can be retrieved, and that pointer can be stored in a static variable to cache it so the lookup doesn't have to take place every time;
找到函数真正的实现是通过调用dlsym(定义在dlfcn.h中)来实现的,通过调用dlopen可以实现利用此函数返回指定函数的指针。通过RTLD_NEXT常量,之前的入口便可获得,这个入口指针可以保存在一个静态变量中,从而无需每次替换时都来重新查询这个指针。
点击(此处)折叠或打开
点击(此处)折叠或打开
In the code above the system version (or standard version) of malloc and free are looked up using dlsym(RTLD_NEXT, [name of function]) and then stored in sys_malloc and sys_free respectively. After the system version of the functions are cached (if not already cached) they're called to perform the allocation or deallocation and after that the leak finder will intercept the calls to track the information required to compile a list of leaks.
上述代码中,系统自带(或标准库自带)的malloc和free函数使用dlsym(RTLD_NEXT, [函数名])可以获取到入口地址并且分别保存为sys_malloc和 sys_free,在系统自带函数入口保存好了后,他们会用来真正实现内存的分配和释放,在此之后,“内存泄露检测器”会拦截应用程序中的所有对这两个函数的调用,从而能跟踪到用户所需要的信息。
Allocation Info 分配信息To allow the user to fix leaks, each leak needs to be associated with additional information to make it easy to track down the leak and correct it, in leakfinder I track four different things;
为了允许用户解决泄露问题,每个泄露需关联可以用于跟踪和纠正这些泄露的其它信息,在“泄露探测器”中我跟踪4种不同信息。
1) References
2) Stacktrace
3) Size
4) Thread
1) 引用
2) 堆栈轨迹
3) 内存大小
4) 线程ID
References 引用In order to marry up a deallocation to a previous allocation there need to be something that is unique and consistent across malloc and free calls, and one thing that can be used is the address of the allocated memory. It has to be unique as no two allocations are allowed to get the same piece of memory and it's consistent across calls as it's the return value of the allocation and the parameter to the deallocation.
为了使得释放操作能与之前的分配操作匹配,需要有针对malloc与free调用的持续的唯一性记录。可以用来实现此记录的便是分配的内存地址。因为这个地址肯定是唯一的,并且作为分配内存时的返回值和释放时的传入值,在整个调用期间它也是持续存在的。
Stacktrace 堆栈轨迹In order for a memory leak detector to be useful it needs to be able to tell the user where the leak was allocated. One way to get a stacktrace in Linux is to use the backtrace and backtrace_symbols functions in execinfo.h.
为了使得“内存泄露检测器”并非浪得虚名,我们不但需要检测到泄露的存在,而且应能准确定位到是何处分配的内存存在泄露问题。在Linux上获取堆栈轨迹的方法是使用execinfo.h头文件中的 backtrace 和backtrace_symbols方法。
Function backtrace gets all the return addresses for all the functions that are currently active on the stack in a particular thread, which is essentially thestacktrace.
In order to get a more readable version of the stacktrace the return addresses from backtrace can be fed into backtrace_symbols to get the names of the functions on the stack.
backtrace 函数可以用来获取特定线程上当前栈内所有函数地址作为返回值供分析使用,本质上来讲就是堆栈轨迹。
为获得堆栈轨迹的更多的可用分析信息,可以通过backtrace返回的地址传入backtrace_symbols 函数后获取栈上的函数名。两个函数体分别如下所示:
点击(此处)折叠或打开
Less important but still sometimes useful is to record the size of the allocation and which thread it was allocated on.
尽管不太重要,但是记录下分配的内存大小以及分配该内存的线程有时还是有一定价值的。
The size is passed in as a size_t argument to malloc so that is easy enough to grab and record.
内存大小是malloc函数的类型为size_t的传入参数,因而易于获得并记录下来。
Recording the current thread id might be slightly harder depending on the thread library used, in this example I've used the pthread library so I get the thread id as a pthread_t by calling pthread_self().
记录下当前线程可能稍显复杂,因为这会依赖于所使用的线程库。本文中的例子我使用的是pthread库,因而可以通过调用pthread_self()函数,其类型为pthread_t的返回值便是该线程的ID。
allocation_info allocation_info类The information listed above, reference, stacktrace, size and thread are in leakfinder stored in a class called.
上述列出的引用、堆栈轨迹、大小以及线程ID在本“泄漏检测器”中封装在allocation_info的类中。其定义与实现如下所示:
allocation_info.h点击(此处)折叠或打开
点击(此处)折叠或打开
Armed with a way of intercepting allocations (library injection) and a way to store allocation information (allocation_info) we're now ready to implement our basic memory leak detector.
拥有了拦截分配内存函数(库注入)的方法和存储分配信息的方法(allocation_info类),我们接下来先实现一个简单的“内存泄露检测器”。
Allocations 分配The first problem with tracking allocations inside the allocation method is that to track it memory needs to be allocated to store the allocation_info, and that obviously means that for every allocation another allocation is required. Since the additional allocation uses malloc as well the approach leads to a stack overflow.
首先来的问题就是,为了讲内存分配的信息存储在allocation_info中,就另外需要调用内存分配函数malloc来分配这个存储信息区域,这样就陷入了死循环直至栈溢出。
The solution is to only track allocations that are originating from outside of leakfinder. By declaring a static boolean called i***ternalSource that is set to false before the allocation is recorded and back to true when done it is possible to exclude allocation that arise from recording the source allocation. The overloaded mallocmethod then looks something like this;
解决的方法是,只跟踪“内存泄露检测器”代码外部的一些内存分配函数,通过定义一个名为i***ternalSource 的静态变量来区分,在进行对外部分配内存信息记录之前将其设置为false,完成记录之后设置为true,这可以有效的将记录内存分配信息时调用的malloc函数与被监控的malloc函数区分开来。冲在的malloc函数如下所示:
点击(此处)折叠或打开
This takes care of the exclusion of internal allocation but suffers from threading issues as the statici***ternalSource might be read/written by two threads at the same time causing undefined behaviour.
上述代码的确做到了将检测器内部调用malloc的情况区别开来,但是存在的问题是,如果有同时有多个线程对静态变量i***ternalSource 进行读写操作,那么就有可能导致一些不确定的结果。
By guarding the inside of the if-statement with a lock (using pthread threads) the malloc method changes to this;
为了对此进行防护,采用加锁的办法(使用pthread线程),上述重载的malloc修改如下:
点击(此处)折叠或打开
Now the malloc implementation is thread safe (or thread-safeish, it still suffers from some issues but for sake of simplicity I'm going to keep it this way for this article).
现在实现的malloc函数就能确保是线程安全的。(或者说相对安全的,因为还是存在一些特殊情形会导致问题存在,但是为了简单起见,本文中我们认为此已经足以确保该部分是线程安全的)。
The rest of the implementation is the matter of grabbing the stacktrace and storing it along with the reference, size and thread id. The size is already passed in and the thread id has been grabbed using pthread_self, the remaining this to store is simply the reference which is the address of ptr which is returned by the actual implementation ofmalloc.
剩下的实现即抓取堆栈轨迹并且将其引用、大小和线程ID记录下来。大小已经记录,线程ID也通过pthread_self函数获得,剩下的需要存储的就是真正的malloc函数返回的内存指针ptr。
All of the above yields a malloc function that looks like this;
综上述所,最终实现的malloc函数版本如下所示:
点击(此处)折叠或打开
To marry up a deallocation to a previous allocation is simple. The free uses the same method of preventing it to be run for an internal free and thread safety and in addition to this it's just a matter of finding the allocation_info in the allocations vector. The unique key is the reference or address;
与之前的分配内存的操作匹配的释放甚是简单,我们需要重载的free函数也需要考虑到之前提到的“内存泄露检测器”内部的free调用和线程安全问题,除此之外还需要做的就是从allocation容器中获取allocation_info (分配的内存信息),其唯一性标识便是内存的引用或者说地址。
点击(此处)折叠或打开
After all the allocations and deallocations have been tracked and matched off the ones that were never deallocated must be reported on, so when is a suitable time to do that?
所有的分配操作和释放操作均已记录下,那么对于那些分配之后没有释放的落单的地方必须要上报出来,何时做才合适呢?
Since it's pretty much technically impossible to tell if a leak has happened before the program terminates leakfinderuses program exit to sum up the leaks. One might argue that the leak happens when the pointer referencing the memory goes out of scope of the pointer has not been deallocated but since it is possible to store the pointer value in just about any other data structure that can be hard to rely on.
鉴于技术上不太可能实现在程序结束之前就统计出是否存在内存泄露的问题,我们的“内存泄露检测器”使用程序退出机制来实现泄露的统计。可能会有人说,指针在引用超出该指针作用域范围的内存后没有释放,这会导致内存泄露的产生,但是由于指针值可以存储在任何其它数据结构中,因而这个假设是很难成立的。
C style destructor C风格的析构 There are different ways to hook into the termination of a program, which one to pick depends on platform and personal taste.
One approach is to use a pragma directive;
很多方法可以实现hook到程序的终结,选择哪一个往往会由平台和个人喜好确定。
一种方法是使用pragma命令,如下所示:
点击(此处)折叠或打开
but for leakfinder I've gone for a C style destructor;
但是对我们的“泄露检测器”,我准备使用的是c风格的析构函数。
点击(此处)折叠或打开
Using this approach, the method compile_allocation is executed when the shared library is unloaded, this is typically at program exit.
使用这一方法,compile_allocation函数在共享库卸载时会被调用,这是程序退出时的典型特征。
Since all the not unallocated allocations are held in the vector allocations at program exit, the work of thecompile_allocation method is just to iterate through the leaks and somehow output the leak information.
由于程序退出时,所有的没有被释放的内存仍然保留在allocations这个容器中,compile_allocation这个函数的作用就是利用迭代器便利所有这些没有被释放的内存,并输出这些泄露的相关信息。
Where the best place to output the leak information to I am not entirely sure of. In certain scenarios a file would be convinient but for simplicity I've decided to let leakfinder just dump the summary to standard out.
我并不确定哪儿来输出这些泄露信息是最好的,输出到一个文件当然是灰常方便的,但是在此为了简单,我就直接往标准输出上来显示了。
To avoid extra work i***ternalSource needs to be set to false at the beginning of compile_allocaion as printing the summary requires allocations to take place.
为了避免不必要的错误,i***ternalSource 在准备打印compile_allocaion信息之前就需要设为false。
To print addresses and pointers in hex variables hex and dec from iomanip are used.
为了将地址和指针输出为十六进制形式,通过iomanip中的hex和dec来实现打印。
点击(此处)折叠或打开
To try it out first build the leakfinder shared library by typing (there's also a makefile included for those who cares not for compiling by hand);
首先来将“leakfinder”编译成共享库,可以通过g++命令来编译或者直接用现成的makefile编译。
Then build the c_example shared library by typing;
再编译c_example共享库:
Then, open up a different terminal and set the LD_PRELOAD;
再来开启一个终端,并设置LD_PRELOAD变量。
Lastly, run the c_example in the terminal where LD_PRELOAD was set;
最后,在设置过LD_PRELOAD环境变量的终端上启动c_example:
Running that application should produce an output similar to this;
输出的结果大致如下所示:
点击(此处)折叠或打开
While that has indeed first printed the output of c_example followed by the summary of the four leaks, the stactrace isn't very easy to dechiper. That is because the program is lacking symbols but by compiling it again (in the first terminal) with option -rdynamic this can be corrected;
从输出的结果中,的确可以看到c_example中出现的4次泄露的记录,但是堆栈轨迹并不太易读。这是由于编译程序时,少了-rdynamic设置,而缺少符号文件。重新编译即可解决此问题。
A more useful stacktrace is provided;
更完整的堆栈轨迹如下所示:
点击(此处)折叠或打开
Now the method name is included and that makes it a whole lot easier to read, and while this works for C++programs as well their stacktraces are still a bit messy because of name mangling, but still very much readable when compiled with -rdynamic, as seen below in the output from cpp_example (also included in the download);
这样函数名已经显示出来,就更易读了,但是在c++程序中,这个堆栈轨迹还是因为命名空间的问题有些混乱,但是使用 –rdynamic后还是有很多改进的,下面的输出时cpp_example程序的输出结果。
点击(此处)折叠或打开
Like I stated at the beginning of the article, this is not an attempt at a finished leak detection product but an explanation of how such a product can be created. The observant reader will have realised that the implementation included with this article is lacking in many areas, it does not explicitly cater for (for example) calloc and that using a std::vector to store the allocation_info objects results in a linear performance penalty on the lookups.
如我刚开始介绍的,本文的目的并非试图实现一个功能十足的“内存泄露检测器”的产品,只是阐述了如何去按照我给出的方法来去实现这个产品。细心的读者会发现这个demo中的实现还是有很多欠缺的,比如demo中没有明确的提供calloc函数的重载,并且使用std::vector存储allocation_info对象的结果是导致在大量数据查找时的性能的线性下降。
Regardless of this, I hope the article has provided a certain amount of insight into how leaks can be spotted in a non-intrusive way (in the sense that the application code does not need to be instrumented or otherwise augmented).
抛开这些,我希望本文能够为你提供一种解决非入侵式的内存泄露探测方法(就这个层面来讲,应用程序代码无需做自身的检测或其它方式来增强监测)
And weirdly enough, the Code Project article submission wizard does not allow me to upload .tar files, so if you want one of those instead; you can get it from here (the allocation_info.cpp file is broken but I'll fix that at a later time, the .zip file is correct); https://sites.google.com/site/fredrikbornander/Home/leakfinder.tar?attredirects=0&d=1
文中的demo可以在以下地址下载。(附件中已有)
Any comments are most welcome.
欢迎拍砖。(译者:sorry,为了保证原汁原味,作者的思考过程都译出,显得有些啰嗦)
本文及代码按CPOL授权发布