网站建设类电话销售,啥都有是什么购物平台,logo123设计网,视频网站直播怎么做C进阶专栏#xff1a;http://t.csdnimg.cn/aTncz 相关系列文章 C技术要点总结, 面试必备, 收藏起来慢慢看 C惯用法之RAII思想: 资源管理 C智能指针的自定义销毁器(销毁策略) 目录
1.内存泄漏概述
1.1.内存泄漏产生原因
1.2 内存泄漏导致的后果
1.3 内存泄漏解决思路
2.宏… C进阶专栏http://t.csdnimg.cn/aTncz 相关系列文章 C技术要点总结, 面试必备, 收藏起来慢慢看 C惯用法之RAII思想: 资源管理 C智能指针的自定义销毁器(销毁策略) 目录
1.内存泄漏概述
1.1.内存泄漏产生原因
1.2 内存泄漏导致的后果
1.3 内存泄漏解决思路
2.宏定义方法
2.1.宏定义
2.2.检测位置
2.3.结果分析
3.hook方法
3.1.hook
3.2.检测位置
3.3 递归调用
3.4.结果分析
3.5.addr2line
4.__libc_malloc 和 __libc_free 1.内存泄漏概述
1.1.内存泄漏产生原因
内存泄漏是在没有自动 gc 的编程语言里面经常发生的一个问题。
自动垃圾回收Automatic Garbage Collection简称 GC是一种内存管理技术在程序运行时自动检测和回收不再使用的内存对象以避免内存泄漏和释放已分配内存的负担。
因为没有 gc所以分配的内存需要程序员自己调用释放。其核心原因是调用分配与释放没有符合开闭原则没有配对形成了有分配没有释放的指针从而产生了内存泄漏。
void myTest(size_t s1)
{void a1malloc(s1);void a2malloc(s1);free(a1);
}
以上代码段分配了两个s1大小的内存块由 a1 与 a2 指向。而代码块执行完以后释放了 a1而 a2 没有释放。形成了有分配没有释放的指针产生了内存泄漏。
1.2 内存泄漏导致的后果
随着工程代码量越来越多有分配没有释放自然会使得进程堆的内存会越来越少直到耗尽。从而导致后面的运行时代码不能成功分配内存使程序崩溃。
1.3 内存泄漏解决思路
最好的办法肯定是引入自动垃圾回收gc。但是这不适合C/C语言。
解决内存泄漏我们需要解决两点
1能够检测出来是否发送内存泄漏
2如果发生内存泄漏能够检测出来具体是哪一行代码所引起的。
内存泄漏是由于内存分配与内存释放不匹配所引起的。因此对内存分配函数malloc/calloc/realloc以及内存释放函数free进行“劫持”hook就能能够统计出内存分配的位置内存释放的位置从而判断是否匹配。
2.宏定义方法
2.1.宏定义
使用宏定义替换系统的内存分配接口。并利用__FILE__、__LINE__分别获取当前编译文件的文件名、行号进行追踪位置信息。
#define malloc(size) _malloc(size, __FILE__, __LINE__)
#define free(ptr) _free(ptr, __FILE__, __LINE__)
需要注意的是宏定义一定要放在内存分配之前这样预编译阶段才会替换为我们自己实现的_malloc和_free。
2.2.检测位置
为了方便观察我们可以在内存分配_malloc的时候创建一个文件。文件名为指向新分配内存的指针值文件内容为指针值、调用_malloc时的文件名、行号。
在该内存释放_free的时候删除该指针对应的文件。
最后程序运行结束如果没有文件说明没有内存泄漏否则说明存在内存泄漏。
2.3.结果分析
#include stdio.h
#include stdlib.h
#include unistd.hvoid *_malloc(size_t size, const char *filename, int line){void *ptr malloc(size);char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);FILE *fp fopen(buffer, w);fprintf(fp, []addr: %p, filename: %s, line: %d\n, ptr, filename, line);fflush(fp);fclose(fp);return ptr;
}void _free(void *ptr, const char *filename, int line){char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);if (unlink(buffer) 0){printf(double free: %p\n, ptr);return;}return free(ptr);
}#define malloc(size) _malloc(size, __FILE__, __LINE__)
#define free(ptr) _free(ptr, __FILE__, __LINE__)int main() {void *p1 malloc(5);void *p2 malloc(18);void *p3 malloc(15);free(p1);free(p3);
}
最后在memory文件夹里可以看到存在一个文件说明有一个地方出现内存泄漏 []addr: 0x559e55b6e8b0, filename: fun1.c, line: 39 从结果上看内存泄漏发生第39行。
3.hook方法
利用 hook 机制改写系统的内存分配函数。
3.1.hook
hook方法的实现分三个步骤
1定义函数指针。
typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f NULL;typedef void (*free_t)(void *ptr);
free_t free_f NULL;
2函数实现函数名与目标函数名一致。
void *malloc(size_t size)
{//改写的功能
}void free(void *ptr)
{//改写的功能
}
3初始化hook调用dlsym()。
void init_hook(){if (!malloc_f){malloc_f dlsym(RTLD_NEXT, malloc);}if (!free_f){free_f dlsym(RTLD_NEXT, free);}
}
3.2.检测位置
宏定义的方法在检测调用所在行号的时候使用了系统定义的__LINE__因为是宏定义的malloc预编译时候直接嵌入。因此__LINE__返回的就是调用malloc的位置。
但是hook方法不一样系统定义的__LINE__在函数内部调用无法确定在主函数中的调用位置。比如 fprintf(fp, []addr: %p, filename: %s, line: %d\n, ptr, filename, line); 返回的就是fprintf所在的行号。
因此使用gcc 提供的__builtin_return_address该函数返回当前函数或其调用者之一的返回地址。参数level 表示向上扫描调用堆栈的帧数。比如对于 main -- f1() -- f2() -- f3() f3()函数里面调用 __builtin_return_address (0)返回f3的地址调用 __builtin_return_address (1)返回f2的地址
3.3 递归调用
hook的时候要考虑其他函数也用到所hook住的函数比如在printf()函数里面也调用了malloc那么就需要防止内部递归进入死循环。 通过gdb调试在第23行打断点发现每次运行都回到了23行。
这是因为sprintf隐含调用了malloc这样就陷入一个循环
23行的sprintf — 自定义的malloc — 23行的sprintf — 自定义的malloc -- 23行的sprintf — 自定义的malloc -- ……
解决办法是限制调用次数。当进入 malloc 函数内部后根据自己的需要设置 hook 的开关。在关闭的区域内调用 malloc 后进入到 else 部分执行原来的 hook 函数避免了无限递归的发生。
int enable_malloc_hook 1;
void *malloc(size_t size) { // 执行改写的 malloc 函数if (enable_malloc_hook) {enable_malloc_hook 0;// 关闭 hook, printf 内部的 malloc 执行 else 的部分// 其他代码enable_malloc_hook 1;}// 执行原来的 malloc 函数else {p malloc_f(size);}
}
3.4.结果分析
// gcc -o fun2 fun2.c -ldl -g#define _GNU_SOURCE
#include dlfcn.h#include stdio.h
#include stdlib.h
#include unistd.h
#include link.htypedef void *(*malloc_t)(size_t size);
malloc_t malloc_f NULL;typedef void (*free_t)(void *ptr);
free_t free_f NULL;int enable_malloc_hook 1;
int enable_free_hook 1;void *malloc(size_t size){void *ptr NULL;if (enable_malloc_hook ){enable_malloc_hook 0; enable_free_hook 0;ptr malloc_f(size);void *caller __builtin_return_address(0);char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);FILE *fp fopen(buffer, w);fprintf(fp, [] caller: %p, addr: %p, size: %ld\n, caller, ptr, size);fflush(fp);fclose(fp);enable_malloc_hook 1;enable_free_hook 1;}else {ptr malloc_f(size);}return ptr;
}void free(void *ptr){if (enable_free_hook ){enable_free_hook 0;enable_malloc_hook 0;char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);if (unlink(buffer) 0){printf(double free: %p\n, ptr);return;}free_f(ptr);enable_malloc_hook 1;enable_free_hook 1;}else {free_f(ptr);}
}void init_hook(){if (!malloc_f){malloc_f dlsym(RTLD_NEXT, malloc);}if (!free_f){free_f dlsym(RTLD_NEXT, free);}
}
int main(){init_hook();void *p1 malloc(5);void *p2 malloc(18);void *p3 malloc(15);free(p1);free(p3);
} 从结果看存在一个内存泄漏但是 caller0x16bb 是地址不是具体行号。使用addr2line可以将地址转换为文件名和行号。
3.5.addr2line
利用addr2line工具将地址转换为文件名和行号得到源文件的行数根据机器码地址定位到源码所在行数) addr2line -f -e fun2 -a 0x16bb 参数-f显示函数名信息。-e filename指定需要转换地址的可执行文件名。-a address显示指定地址十六进制。
但是高版本 gcc 下使用 addr2line 命令会出现乱码问题。 ?? ??:0 addr2line 作用于 ELF 可执行文件而高版本的 gcc 调用 __builtin_return_address返回的地址 caller 位于内存映像上所以会产生乱码。 解决办法是利用动态链接库的dladdr函数 作用于共享目标可以获取某个地址的符号信息。使用该函数可以解析符号地址。如下
// gcc -o fun2 fun2.c -ldl -g#define _GNU_SOURCE
#include dlfcn.h#include stdio.h
#include stdlib.h
#include unistd.h
#include link.h// 解析地址
void* converToELF(void *addr) {Dl_info info;struct link_map *link;dladdr1(addr, info, (void **)link, RTLD_DL_LINKMAP);// printf(%p\n, (void *)(size_t)addr - link-l_addr);return (void *)((size_t)addr - link-l_addr);
}typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f NULL;typedef void (*free_t)(void *ptr);
free_t free_f NULL;int enable_malloc_hook 1;
int enable_free_hook 1;void *malloc(size_t size){void *ptr NULL;if (enable_malloc_hook ){enable_malloc_hook 0; ptr malloc_f(size);void *caller __builtin_return_address(0);char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);FILE *fp fopen(buffer, w);// converToELF(caller)fprintf(fp, [] caller: %p, addr: %p, size: %ld\n, converToELF(caller), ptr, size);fflush(fp);fclose(fp);enable_malloc_hook 1;}else {ptr malloc_f(size);}return ptr;
}void free(void *ptr){if (enable_free_hook ){enable_free_hook 0;char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);if (unlink(buffer) 0){printf(double free: %p\n, ptr);return;}free_f(ptr);enable_free_hook 1;}else {free_f(ptr);}
}void init_hook(){if (!malloc_f){malloc_f dlsym(RTLD_NEXT, malloc);}if (!free_f){free_f dlsym(RTLD_NEXT, free);}
}
int main(){init_hook();void *p1 malloc(5);void *p2 malloc(18);void *p3 malloc(15);free(p1);free(p3);
}
4.__libc_malloc 和 __libc_free
思路和hook的一样因为malloc和free底层调用的也是__libc_malloc和__libc_free。
// gcc -o fun3 fun3.c
#define _GNU_SOURCE
#include dlfcn.h
#include stdio.h
#include stdlib.h
#include unistd.h
#include link.hvoid* converToELF(void *addr) {Dl_info info;struct link_map *link;dladdr1(addr, info, (void **)link, RTLD_DL_LINKMAP);// printf(%p\n, (void *)(size_t)addr - link-l_addr);return (void *)((size_t)addr - link-l_addr);
}extern void *__libc_malloc(size_t size);
extern void *__libc_free(void *ptr);int enable_malloc_hook 1;
int enable_free_hook 1;void *malloc(size_t size){void *ptr NULL;if (enable_malloc_hook ){enable_malloc_hook 0; enable_free_hook 0;ptr __libc_malloc(size);void *caller __builtin_return_address(0);char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);FILE *fp fopen(buffer, w);fprintf(fp, [] caller: %p, addr: %p, size: %ld\n, converToELF(caller), ptr, size);fflush(fp);fclose(fp);enable_malloc_hook 1;enable_free_hook 1;}else {ptr __libc_malloc(size);}return ptr;
}void free(void *ptr){if (enable_free_hook ){enable_free_hook 0;enable_malloc_hook 0;char buffer[128] {0};sprintf(buffer, ./memory/%p.memory, ptr);if (unlink(buffer) 0){printf(double free: %p\n, ptr);return;}__libc_free(ptr);enable_malloc_hook 1;enable_free_hook 1;}else {__libc_free(ptr);}
}int main(){void *p1 malloc(5);void *p2 malloc(18);void *p3 malloc(15);free(p1);free(p3);
}