原理
内存泄漏概念
内存泄漏(Memory leak):在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存被称之为内存泄漏。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在导致程序未被释放,从而造成了内存的浪费。
检测原理
要想实现内存泄漏检测,最容易想到的就是在程序对内存进行操作(eg: 申请内存
,释放内存
等)时记录下来,程序退出时对照一下申请和释放是否对应得上,即可实现内存泄漏检测。说起来并不难,但是我们如何监听程序对内存的操作呢?这个会在下面的具体方式里讲到。
那么延伸一下,如何检测 野指针
?
根据我浅薄的认知,目前仅知道可使用 内存涂鸦
的方式,即每次申请内存,先对其内容进行未初始化涂鸦。若使用该指针时发现其指向为未初始化涂鸦值,则证明该指针为 野指针
。
同理,我们释放内存也需对其进行内存涂鸦,这样再次调用该指针时,发现其指向内存被涂鸦成 未初始化
,即可知其使用的是野指针。
据我所知有些编译器就是这么做的,例如 VC++。
实现方式
上面的原理听起来不难,但是具体怎么记录某程序的内存操作呢?
较为简便的是以下三种方法:
重载
当我们在程序中写下 new
和 delete
时,我们实际上调用的是 C++
语言内置的 new operator
和 delete operator
。所以我们可以重载其 new operator
和 delete operator
以实现内存分配及释放情况的记录与分析。
这种方法,在之前的博文:C++ -重载运算符 实现数组越界检测&被除数检测 里讲过,这里仅告知需要重写以下函数:
void* operator new( size_t nSize, char* pszFileName, int nLineNum )
void* operator new[]( size_t nSize, char* pszFileName, int nLineNum )
void operator delete( void *ptr )
void operator delete[]( void *ptr )
该方法的缺点也是很明显的,它只是相当于给库函数再封装了一层。在这一层内实现了对内存的记录分析。
对于原有项目(直接使用 new operator
& delete operator
等),需要将 new( )
、delete( )
等 一.一 修改,方可实现内存检测。
gcc_wrap
山重水尽疑无路 柳暗花明又一村
GCC 为我们预留了后门,给我们留下了一个有趣的选项 –wrap=symbol
我们可称之为 包装函数
;它可以使链接器在链接 symbol符号
时优先查找__wrap_symbol
,查找不到时才会链接原始的 symbol符号
。
下面是一个 demo
:
/* test.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
extern "C"
{
int malloc_count = 0;
int free_count = 0;
void *__real_malloc(size_t size);
void __real_free(void *ptr);
void print_trace ();
void *__wrap_malloc(size_t size){
malloc_count++;
print_trace();
return __real_malloc(size);
}
void __wrap_free(void *ptr){
free_count++;
print_trace();
__real_free(ptr);
}
void *operator new(size_t size){
return __wrap_malloc(size);
}
void operator delete(void *ptr){
__wrap_free(ptr);
}
void print_trace ()
{
printf ("========== call stack =========\n");
void *array[10];
int size;
char **strings;
int i;
size = backtrace (array, 10);
strings = backtrace_symbols (array, size);
if (NULL != strings)
{
printf ("Obtained %zd stack frames.\n", size);
for (i = 0; i < size; i++)
{
printf ("%s\n", strings[i]);
}
__real_free(strings);
strings = NULL;
}
}
}
int main(){
int *p1 = (int*)malloc((sizeof(int)));
int *ptr1 = new int[5];
int *ptr2 = new int[5];
delete ptr1;
printf ("========== memory leak detection =========\n");
printf("malloc_count = %d\n",malloc_count);
printf("free_count = %d\n",free_count);
if(malloc_count != free_count){
printf("memony leak!\n");
}
return 0;
}
print_trace( )
打印栈,不用亦可
编译时记得带上那个有趣的选项
gcc test.cpp -Wl,--wrap=malloc --wrap=free
LD_PRELOAD偷梁换柱
总觉得利用 gcc-wrap
的方法还不够方便,能不能打包为 so库
,对任意需要内存检测的函数进行链接编译呢?
可达鸭表示:可
在此之前,我们先来看下 Linux系统
加载 so库
的流程:
以下内容转自 郑瀚Andrew_Hann - Linux System Calls Hooking Method Summary
包括 Linux
系统在内的很多开源系统都是基于 Glibc
的,动态链接的 ELF可执行文件
在启动时同时会启动 动态链接器(/lib/ld-linux.so.X)
,程序所依赖的共享对象全部由动态链接器负责 装载和初始化,所以这里所谓的共享库的查找过程,本质上就是动态链接器(/lib/ld-linux.so.X)对共享库路径的搜索过程,搜索过程如下:
- /etc/ld.so.cache:
Linux
为了加速LD_PRELOAD
的搜索过程,在系统中建立了一个ldconfig程序
,这个程序负责:- 将共享库下的各个共享库维护一个
SO-NAME
(一一对应的符号链接),这样每个共享库的SO-NAME就能够指向正确的共享库文件 - 将全部SO-NAME收集起来,集中放到
/etc/ld.so.cache
文件里面,并建立一个SO-NAME的缓存 - 当动态链接器要查找共享库时,它可以直接从/etc/ld.so.cache里面查找。所以,如果我们在系统指定的共享库目录下添加、删除或更新任何一个共享库,或者我们更改了
/etc/ld.so.conf
、/etc/ld.preload
的配置,都应该运行一次ldconfig
这个程序,以便更新SO-NAME和/etc/ld.so.cache
- 将共享库下的各个共享库维护一个
- 根据/etc/ld.so.preload中的配置进行搜索(LD_PRELOAD):这个配置文件中保存了需要搜索的共享库路径,Linux动态共享库加载器根据顺序进行 逐行广度搜索
- 根据环境变量LD_LIBRARY_PATH指定的动态库搜索路径
- 根据ELF文件中的配置信息:任何一个动态链接的模块所依赖的模块路径保存在
".dynamic"段
中,由DT_NEED
类型的项表示,动态链接器会按照这个路径去查找DT_RPATH
所指定的路径,编译目标代码时,可以对gcc
加入链接参数-Wl,-rpath
指定动态库搜索路径。- DT_NEED段中保存的是绝对路径,则动态链接器直接按照这个路径进行直接加载
- DT_NEED段中保存的是相对路径,动态链接器会在按照一个约定的顺序进行库文件查找下列路径
- /lib
- /usr/lib
- /etc/ld.so.conf中配置指定的搜索路径
小结:可以看到,LD_PRELOAD
是 Linux系统
中启动新进程首先要 加载so
的搜索路径,所以它可以影响程序的运行时的链接(Runtime linker
),它允许你定义在程序运行前"优先加载"的动态链接库。
我们只要在通过 LD_PRELOAD
加载的 .so
中编写我们需要 hook
的同名函数,根据Linux对外部动态共享库的符号引入全局符号表的处理,后引入的符号会被省略,即系统原始的 .so(/lib64/libc.so.6)
中的符号会被省略。
通过 strace program
也可以看到,Linux
是优先加载 LD_PRELOAD
指明的 .so
,然后再加载系统默认的 .so
的。
/* testmallic.cpp*/
#include <stdio.h>
#include <stdlib.h>
int malloc_count = 0;
int free_count = 0;
void printf_end();
void printf_end(){
fprintf(stderr,"=========Result=========\n");
fprintf(stderr,"malloc_count = %d\n",malloc_count);
fprintf(stderr,"free_count = %d\n",free_count);
if(malloc_count != free_count){
fprintf(stderr,"Memlook\n");
}
}
void* malloc(size_t size){
if(0 == malloc_count){
atexit(printf_end);
}
malloc_count ++;
fprintf(stderr,"malloc_count = %d\n",malloc_count);
return realloc(NULL,size);
}
void free(void *p){
free_count ++;
fprintf(stderr,"free_count = %d\n",free_count);
realloc(p,0);
}
编译为 so库
g++ -shared -fpic -o testmallox.so testmallic.cpp
再写个测试用例
/*main.cpp*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char *c1 = (char*)malloc(sizeof(char));
int *c2 = (int*)malloc(sizeof(int));
free(c1);
return 0;
}
编译一下
g++ -o main main.cpp
运行
LD_PRELOAD=./testmalloc.so ./main