1.内存泄漏(memory leak)本质
内存泄露的本质其实就是malloc/new 和 free/delete 不匹配,或 重复释放。
2.预防内存泄漏
-
尽量从逻辑上保证每一个malloc/new 和 free/delete 相匹配;
-
对指针赋予地址之前,保证指针的指向为空;
-
对于结构化指针元素,释放这个指针元素之前,如果它的内部有指针,保证其内部的指针先被释放,再释放这个元素;
-
对于指针函数,保证有变量接收它的返回值。
3.怎么排查是否有内存泄漏
-
对于简单程序,可以直接在代码中排查,但是对于大型、复杂的程序,仅靠肉眼观察非常吃力;
-
用dlsym做函数拦截,将malloc/free替换
// 编译命令:gcc -shared -fPIC -ldl -o hook.so hook.c
// _GUN_SOURCE 必须放在最顶部,否则会报错 RTLD_NEXT 未定义
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
// 定义函数指针类型
typedef void*(*malloc_func_t)(size_t);
static malloc_func_t malloc_f = NULL;
// free 同理
typedef void(*free_func_t)(void* p);
static free_func_t free_f = NULL;
// 通过符号抢占,先定义同名函数,将malloc hook住
void* malloc(size_t size) {
printf("exec malloc.\n");
if (!malloc_f) {
// 获取下一个出现的 malloc 地址
malloc_f = (malloc_func_t)dlsym(RTLD_NEXT, "malloc");
}
void *ptr = malloc_f(size);
return ptr;
}
void free(void* p){
printf("exec free.\n");
if(!free_f){
free_f = (free_func_t)dlsym(RTLD_NEXT,"free");
}
free_f(p);
}
int main(){
void *ptr1 = malloc(10);
void *ptr2 = malloc(20);
void *ptr3 = malloc(30);
free(ptr1);
free(ptr3);
return 0;
}
如上文件编译运行之后会出现Segmentation fault报错,原因是printf内部会调用malloc函数,而malloc函数被我们使用钩子hook了,会导致调用重载的malloc,再次进入printf,导致了一个很难以发现的递归。
// 编译命令:gcc -shared -fPIC -ldl -o hook.so hook.c
// _GUN_SOURCE 必须放在最顶部,否则会报错 RTLD_NEXT 未定义
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义函数指针类型
typedef void*(*malloc_func_t)(size_t);
static malloc_func_t malloc_f = NULL;
// free 同理
typedef void(*free_func_t)(void* p);
static free_func_t free_f = NULL;
bool malloc_enable = true;
bool free_f_enable = true;
int malloc_times,free_times;
// 通过符号抢占,先定义同名函数,将malloc hook住
void* malloc(size_t size) {
if(malloc_enable){
// 通过一个变量屏蔽
malloc_enable = false;
if (!malloc_f)
{
// 获取下一个出现的 malloc 地址,此处就将库函数中的 malloc 函数地址给到了malloc_f
// 所以下文调用malloc_f就可以正确分配内存
malloc_f = (malloc_func_t)dlsym(RTLD_NEXT, "malloc");
}
// 如果申请/释放次数很多,不方便观察,所以不使用打印,而是用数字计数。
// printf("exec malloc.\n");
malloc_enable = true;
}
void *p = malloc_f(size);
malloc_times++;
return p;
}
void free(void* p){
if(free_f_enable){
// 与上文同理
free_f_enable = false;
if (!free_f){
// 与上文同理
free_f = (free_func_t)dlsym(RTLD_NEXT, "free");
}
// printf("exec free.\n");
free_f_enable = true;
}
free_times++;
free_f(p);
}
int main(){
void *ptr1 = malloc(10);
void *ptr2 = malloc(20);
void *ptr3 = malloc(30);
free(ptr1);
free(ptr3);
printf("malloc times: %d\n", malloc_times);
printf("free times: %d\n", free_times);
return 0;
}
输出如下:
使用printf每次都打印的结果:
exec malloc.
exec malloc.
exec malloc.
exec free.
exec free.
只打印计数的结果:
malloc times: 3
free times: 2
可以发现,与我们的预期是符合的,即有三次申请内存空间,但只有两次释放,这样就可以知道是否出现了内存泄漏。
4.如何定位内存泄漏精确位置
核心思想依旧是重载malloc/free(hook)
1.通过__builtin_return_adress()获取地址
即通过 __builtin_return_address 获取上级调用的函数的地址
1.获取分配内存的语句的地址
// __builtin_return_address 获取上级调用的退出的地址,可以设置为1级,也可以设置为2级...
// 打印调用malloc和free位置的信息。 这里打印地址 通过 addr2line 进行地址和行号转换
#define _GNU_SOURCE
#include <dlfcn.h> //对应的头文件
#include <stdio.h>
#include <stdlib.h>
typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;
typedef void (*free_t)(void* p);
free_t free_f = NULL;
int enable_malloc_hook = 1;
int enable_free_hook = 1;
void * malloc(size_t size){
if(enable_malloc_hook) //对第三方调用导致的递归进行规避
{
enable_malloc_hook = 0;
//打印上层调用的地址,即什么地方调用了 malloc 函数
void *carrer = __builtin_return_address(0);
printf("exec malloc [%p ]\n", carrer );
enable_malloc_hook = 1;
}
return malloc_f(size);
}
void free(void * p){
if(enable_free_hook){
enable_free_hook = 0;
void *carrer = __builtin_return_address(0);
printf("exec free [%p]\n", carrer);
enable_free_hook = 1;
}
free_f(p);
}
//通过dlsym 对malloc和free使用前进行hook
static void init_malloc_free_hook(){
//只需要执行一次
if(malloc_f == NULL){
malloc_f = dlsym(RTLD_NEXT, "malloc"); //除了RTLD_NEXT 还有一个参数RTLD_DEFAULT
}
if(free_f == NULL)
{
free_f = dlsym(RTLD_NEXT, "free");
}
}
int main()
{
init_malloc_free_hook(); //执行一次即可
void * ptr1 = malloc(10);
void * ptr2 = malloc(20);
void *ptr3 = malloc(30);
free(ptr1);
free(ptr3);
return 0;
}
运行结果如下:
exec malloc [0x55ad9abe7822 ]
exec malloc [0x55ad9abe7830 ]
exec malloc [0x55ad9abe783e ]
exec free [0x55ad9abe784e]
exec free [0x55ad9abe785a]
再使用addr2line 获取到地址对应的文件、函数以及行数
-f function 表示获取函数
-e executable 后指定可执行文件
-a address 指定地址
现代 Linux 系统默认编译时会生成 PIE (Position Independent Executable 位置独立可执行文件)可执行文件(gcc默认启用选项 -pie),这意味着每次程序运行时会以随机的不同的基地址被加载,程序中所有输出的地址都是相对这个基地址的偏移量。
addr2line -f -e demo -a 0x55ad9abe7830
此处提供 0x55ad9abe7830 无法正确获取结果
输出如下:
0x000055ad9abe7830
??
??:0
要想获得正确结果需要执行下面步骤(此处我的基地址为0x5ee0f8586000,基址获取比较麻烦,后文有讲解):
offset=$(printf "0x%x" $((0x5ee0f8586830 - 0x5ee0f8586000)))
addr2line -e demo -a $offset
(或者自己手动计算:addr2line -e demo -a 0x830)
这样可以得到正确结果:
0x0000000000000830
/home/he/path/demo.c:56
此时虽然能获得地址对应的文件、函数、行数,但是还是不能知道是哪一行出现了内存泄漏,接下来就解决这个问题
2.只保留未释放的指针的分配语句地址
// malloc和free 之间的关联是申请内存的地址,以该地址作为基准就可以一一对应
// malloc时写入一个文件,打印行数等必要信息 free时删除这个文件 通过有剩余文件判断内存泄露
#define _GNU_SOURCE
#include <dlfcn.h> //对应的头文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;
typedef void (*free_t)(void* p);
free_t free_f = NULL;
int enable_malloc_hook = 1;
int enable_free_hook = 1;
#define MEM_FILE_LENGTH 40
void * malloc(size_t size){
if(enable_malloc_hook) //对第三方调用导致的递归进行规避
{
enable_malloc_hook = 0;
//实际的内存申请,根据该地址写文件和free 相互关联
void *ptr =malloc_f(size);
//打印上层调用的地址
void *carrer = __builtin_return_address(0);
printf("exec malloc [%p ]\n", carrer );
//通过写入文件的方式 对malloc和free进行关联 malloc时写入文件
char file_buff[MEM_FILE_LENGTH] = {0};
// 文件名以指针地址命名:0x????????.mem,在释放时传入参数为指针,顺便通过指针删除,无需额外参数
sprintf(file_buff, "./mem/%p.mem", ptr);
//打开文件写入必要信息 使用前创建目录级别(此处文件名存放在file_buff中)
FILE *fp = fopen(file_buff, "w");
fprintf(fp, "[malloc addr : +%p ] ---->mem:%p size:%lu \n",carrer, ptr, size);
fflush(fp); //刷新写入文件
enable_malloc_hook = 1;
return ptr;
}else
{
return malloc_f(size);
}
}
void free(void * p){
if(enable_free_hook){
enable_free_hook = 0;
void *carrer = __builtin_return_address(0);
//free时删除文件 根据剩余文件判断内存泄露
char file_buff[MEM_FILE_LENGTH] = {0};
sprintf(file_buff, "./mem/%p.mem", p);
//删除文件 根据malloc对应的指针
if(unlink(file_buff) <0)
{
printf("double free: %p, %p \n", p, carrer);
}
//这里的打印实际就没意义了
printf("exec free [%p]\n", carrer);
free_f(p);
enable_free_hook = 1;
}else
{
free_f(p);
}
}
//通过dlsym 对malloc和free使用前进行hook
static void init_malloc_free_hook(){
//只需要执行一次
if(malloc_f == NULL){
malloc_f = dlsym(RTLD_NEXT, "malloc"); //除了RTLD_NEXT 还有一个参数RTLD_DEFAULT
}
if(free_f == NULL)
{
free_f = dlsym(RTLD_NEXT, "free");
}
return ;
}
int main()
{
init_malloc_free_hook(); //执行一次
void * ptr1 = malloc(10);
void * ptr2 = malloc(20);
free(ptr1);
void * ptr3 = malloc(30);
free(ptr3);
return 0;
}
这样,上面的程序运行之后如果在目录下的mem文件夹中有文件,就说明出现了内存泄露,打开文件,文件内容形如这样,指明了分配内存的语句在可执行文件中的地址,分配的地址值以及大小:
[malloc addr : +0x56040ab28b89 ] ---->mem:0x56040b6488d0 size:20
此时如果你的可执行文件没有使用-pie选项,在 addr2line 中使用分配此内存的调用位置在文件中的地址(第一个地址)即可得到语句在程序中的精确位置:
he@ubuntu:~/path/memoryLeak$ addr2line -fe demo -a 0x56040ab28b89
main
/home/he/path/memoryLeak/demo.c:88
3.获取基地址以计算精确位置
如果你的可执行文件使用的 -pie 选项并且不想修改配置文件,按照下面的方案可以解决
1.修改编译选项
编译时使用 -no-pie 就可以直接使用addr2line获取具体文件、函数、行数:
gcc -o a.out a.c -g -no-pie
2.程序运行时间足够长
可以通过查看 /proc/<pid>/maps 获取基地址:
# 替换 <pid> 为实际进程 ID ,用你的可执行程序替换<your_program>
cat /proc/<pid>/maps | grep '<your_program>'
3.程序运行时间较短
通过GDB获取基地址:
用你的可执行程序替换第一行的<your_program>
he@ubuntu:~/path/memoryLeak$ gdb ./<your_program> -ex "starti" -ex "info proc mappings" -ex "q"
... 省略一长段此处不需要的输出...
---Type <return> to continue, or q <return> to quit---
Program stopped.
0x00007ffff7dd4090 in _start () from /lib64/ld-linux-x86-64.so.2
process 13227
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x555555554000 0x555555555000 0x1000 0x0 /home/he/path/memoryLeak/demo
0x555555755000 0x555555757000 0x2000 0x1000 /home/he/path/memoryLeak/demo
0x7ffff7dd3000 0x7ffff7dfc000 0x29000 0x0 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ff7000 0x7ffff7ffa000 0x3000 0x0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 0x2000 0x0 [vdso]
0x7ffff7ffc000 0x7ffff7ffe000 0x2000 0x29000 /lib/x86_64-linux-gnu/ld-2---Type <return> to continue, or q <return> to quit---
.27.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
A debugging session is active.
Inferior 1 [process 13227] will be killed.
Quit anyway? (y or n)
Please answer y or n.
A debugging session is active.
Inferior 1 [process 13227] will be killed.
Quit anyway? (y or n) y
上面输出的Mapped address spaces字段中有很多,我们只需要Start Addr的第一个,我这里是0x555555554000
使用计算器(手算也可以:( )用mem目录下的文件中第一个地址(绝对地址:0x56040ab28b89 )减去基地址(0x555555554000)得到相对地址(0x17F7050B89),再将这个相对地址作为addr2line的参数即可得到内存泄漏的位置(这里有一个很奇怪的问题,如果我用0x17F7050B89作为参数依然得不到正常结果,但是写成加上空格的形式:0x17 F705 0B89才能得到结果,虽然输出依旧有点问题,有懂为什么的大佬可以指点一下):
he@ubuntu:~/path/memoryLeak$ addr2line -fe demo -a 0x17 F705 0B89
0x0000000000000017
??
??:0
0x000000000000f705
??
??:0
0x0000000000000b89
main
/home/he/path/memoryLeak/demo.c:88
2.使用宏定义替换malloc/free,__LINE__获取位置
先实现我们自定义的malloc_hook/free_hook函数,在宏里面预先填好两个参数__FILE__和__LINE__,用自定义的替换原来的malloc和free,即可看见文件和行数
#include <stdio.h>
#include <stdlib.h>
// 这两个宏不能放在这里 会对malloc_hook 和 free_hook 内部实际调用的也替换,
// 形成递归调用 并且无法规避 会报错Segmentation fault
// #define malloc(size) malloc_hook(size, __FILE__, __LINE__)
// #define free(p) free_hook(p, __FILE__, __LINE__)
#define MEM_FILE_LENGTH 40
//实现目标函数
void *malloc_hook(size_t size, const char* file, int line)
{
//这里还是通过文件的方式进行识别
void *ptr =malloc(size);
char file_name_buff[MEM_FILE_LENGTH] = {0};
sprintf(file_name_buff, "./mem/%p.mem", ptr);
//打开文件写入必要信息 使用前创建目录级别
FILE *fp = fopen(file_name_buff, "w");
fprintf(fp, "[file:%s line:%d ] ---->mem:%p size:%lu \n",file, line, ptr, size);
fflush(fp); //刷新写入文件
return ptr;
}
void free_hook(void *p, const char* file, int line)
{
char file_name_buff[MEM_FILE_LENGTH] = {0};
sprintf(file_name_buff, "./mem/%p.mem", p);
if(unlink(file_name_buff) <0)
{
printf("double free: %p, file: %s. line :%d \n", p, file, line);
}
free(p);
}
//宏定义实现代码中调用malloc/free时调用我们目标函数
#define malloc(size) malloc_hook(size, __FILE__, __LINE__)
#define free(p) free_hook(p, __FILE__, __LINE__)
int main()
{
void * ptr1 = malloc(10);
void * ptr2 = malloc(20);
free(ptr1);
void * ptr3 = malloc(30);
free(ptr3);
return 0;
}
这样的方式很简单即可得到最后的结果,依然是在mem目录下查看文件,如果有就发生了内存泄漏,反之就没有发生。
[file:macro_hook1.c line:43 ] ---->mem:0x5571bf8ac4c0 size:20
可以很直观的看到内存泄漏发生在哪个文件和行数。
3.劫持malloc,用_libc_malloc申请内存
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//实际内存申请的函数
extern void *__libc_malloc(size_t size);
int enable_malloc_hook = 1;
extern void __libc_free(void* p);
int enable_free_hook = 1;
// func --> malloc() { __builtin_return_address(0)}
// callback --> func --> malloc() { __builtin_return_address(1)}
// main --> callback --> func --> malloc() { __builtin_return_address(2)}
//calloc, realloc
void *malloc(size_t size) {
if (enable_malloc_hook) {
enable_malloc_hook = 0;
void *p = __libc_malloc(size); //重载达到劫持后 实际内存申请
void *caller = __builtin_return_address(0); // 0
char buff[128] = {0};
sprintf(buff, "./mem/%p.mem", p);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+%p] --> addr:%p, size:%ld\n", caller, p, size);
fflush(fp);
//fclose(fp); //free
enable_malloc_hook = 1;
return p;
} else {
return __libc_malloc(size);
}
return NULL;
}
void free(void *p) {
if (enable_free_hook) {
enable_free_hook = 0;
char buff[128] = {0};
sprintf(buff, "./mem/%p.mem", p);
if (unlink(buff) < 0) { // no exist
printf("double free: %p\n", p);
}
__libc_free(p);
// rm -rf p.mem
enable_free_hook = 1;
} else {
__libc_free(p);
}
}
int main()
{
void * ptr1 = malloc(10);
void * ptr2 = malloc(20);
free(ptr1);
void * ptr3 = malloc(30);
free(ptr3);
return 0;
}
获取具体内存泄漏位置同上,不再赘述,下文同样省略。
4.mem_trace劫持malloc/free
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <malloc.h>
/* #include <malloc.h>
void *(*__malloc_hook)(size_t size, const void *caller);
void *(*__realloc_hook)(void *ptr, size_t size, const void *caller);
void *(*__memalign_hook)(size_t alignment, size_t size,
const void *caller);
void (*__free_hook)(void *ptr, const void *caller);
void (*__malloc_initialize_hook)(void);
void (*__after_morecore_hook)(void);*/
// #include <mcheck.h>
typedef void *(*malloc_hook_t)(size_t size, const void *caller);
typedef void (*free_hook_t)(void *p, const void *caller);
malloc_hook_t old_malloc_f = NULL;
free_hook_t old_free_f = NULL;
int replaced = 0;
void mem_trace(void);
void mem_untrace(void);
void *malloc_hook_f(size_t size, const void *caller) {
mem_untrace();
void *ptr = malloc(size);
//printf("+%p: addr[%p]\n", caller, ptr);
char buff[128] = {0};
sprintf(buff, "./mem/%p.mem", ptr);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+%p] --> addr:%p, size:%ld\n", caller, ptr, size);
fflush(fp);
fclose(fp); //free
mem_trace();
return ptr;
}
void *free_hook_f(void *p, const void *caller) {
mem_untrace();
//printf("-%p: addr[%p]\n", caller, p);
char buff[128] = {0};
sprintf(buff, "./mem/%p.mem", p);
if (unlink(buff) < 0) { // no exist
printf("double free: %p\n", p);
return NULL;
}
free(p);
mem_trace();
}
//对__malloc_hook 和__free_hook 重赋值
void mem_trace(void) { //mtrace
replaced = 1;
old_malloc_f = __malloc_hook; //malloc -->
old_free_f = __free_hook;
__malloc_hook = malloc_hook_f;
__free_hook = free_hook_f;
}
//还原 __malloc_hook 和__free_hook
void mem_untrace(void) {
__malloc_hook = old_malloc_f;
__free_hook = old_free_f;
replaced = 0;
}
int main()
{
mem_trace(); //mtrace(); //进行hook劫持
void * ptr1 = malloc(10);
void * ptr2 = malloc(20);
free(ptr1);
void * ptr3 = malloc(30);
free(ptr3);
mem_untrace(); //muntrace(); //取消劫持
return 0;
}
工作原理:
-
通过 __malloc_hook和__free_hook这两个Glibc提供的钩子函数,拦截所有malloc和free调用
-
每次malloc时,会在mem目录下创建一个以地址命名的文件,记录分配大小和调用位置
-
每次free时,会删除对应的内存记录文件
-
如果检测到重复释放(double free),会打印警告
代码结构:
-
mem_trace():启用hook,保存原始hook函数指针;
-
mem_untrace():恢复原始hook函数指针;
-
malloc_hook_f():malloc的hook实现;
-
free_hook_f(): free的hook实现。
5.使用valgrind检测内存泄漏
valgrind --leak-check=full --show-leak-kinds=all --log-file=<filename> ./your_program
1.常用参数及释义
1.--leak-check=<mode>
-
控制内存泄漏检测级别:
-
no:不检测 -
summary(默认):仅显示泄漏数量 -
yes/full:显示每个泄漏的详细栈回溯
-
一般来说使用full或者yes选项
2.--show-leak-kinds=<kinds>
-
指定要显示的泄漏类型(使用逗号分隔):
-
definite:明确泄漏(指针永久丢失) -
possible:可能泄漏(如指针位于内存块中间) -
indirect:间接泄漏(通过其他泄漏内存访问) -
reachable:程序结束前仍可达(未释放但指针未丢失)
-
一般使用all选项
3.--track-origins=yes
-
跟踪未初始化值的来源(如使用未初始化的变量)
直接启用
4.--log-file=<file>
-
输出重定向到文件(默认输出到 stderr)
生成的结果较长可以开启
2.结果的分析
如果使用了 --show-leak-kinds=all ,那么结果中就会出现下面所有的字段(没有泄露的即为0),对各字段的释义:
-
definitely lost:程序一定存在内存泄露; -
indirectly lost:间接泄漏(通常由直接泄漏引起),泄露情况和指针结构相关; -
possibly lost:指针指向内存块中间(可能会误报) -
still reachable:程序结束前未释放(需评估是否合理),没有释放掉一些本可以释放的内存; -
suppressed:意味着有些泄露信息被压抑文件压抑了,在默认的 suppression 文件中可以看到相关设置; -
可能会出现
Invalid read/write:内存越界访问(严重错误)。
6.声明
文中如有错误欢迎指出。本文仅供个人学习记录、不具有任何其他用途,若有侵权联系删除。
部分参考帖子:
https://zhuanlan.zhihu.com/p/458541056
https://zhuanlan.zhihu.com/p/494468532
https://zhuanlan.zhihu.com/p/15101814919
https://blog.youkuaiyun.com/qq_38393271/article/details/121537669
4万+

被折叠的 条评论
为什么被折叠?



