1. 引言
内存泄漏的概念及其重要性
内存泄漏是软件开发中常见的问题,指已分配的内存未能正确释放回操作系统或未能有效地重新利用,从而导致应用程序逐渐消耗更多的内存资源。这种现象在长时间运行的应用中尤为危险,可能导致程序变慢甚至崩溃,严重影响用户体验和系统稳定性。
为何需要自定义工具监测和解决内存泄漏
虽然市场上存在多种内存泄漏检测工具,如 Valgrind、LeakSanitizer 等,但它们往往需要额外的运行时开销或不支持特定的定制需求。通过开发自定义的内存管理工具,开发者可以精确控制监控的粒度,优化性能,同时集成更多针对特定项目的功能,如自动报告、特定场景的检测等。
本节内容将介绍两种方法快速定位内存泄漏
2. 自定义内存管理函数的介绍
nMalloc 和 nFree 函数的设计与实现
nMalloc
和 nFree
是两个用于替代标准 malloc
和 free
的自定义函数,它们通过创建和删除与内存地址相关联的文件来追踪内存的分配和释放。这种方法允许开发者获得关于内存使用的详细日志信息,助力于识别和修复内存泄漏问题。
nMalloc函数:
void *nMalloc(size_t size, const char *filename, int line) {
void *p = malloc(size);
if (flag) {
char buff[128] = {0};
snprintf(buff, 128, "./mem/%p.mem", p);
// 在./mem/路径下在创建.mem的文件
FILE *fp = fopen(buff, "w");
if (!fp) {
free(p);
return NULL;
}
fprintf(fp, "[+]%s:%d, addr: %p, size: %ld\n", filename, line, p, size);
fflush(fp);
fclose(fp);
}
return p;
}
nFree函数:
void nFree(void *ptr) {
if (flag) {
char buff[128] = {0};
snprintf(buff, 128, "./mem/%p.mem", ptr);
// 在./mem/路径下在删除对应的.mem文件
if (unlink(buff) < 0) {
printf("double free: %p\n", ptr);
return;
}
}
free(ptr);
}
通过宏定义重定义 malloc
和 free
,所有的内存分配和释放调用都会被捕获:
#define malloc(size) nMalloc(size, __FILE__, __LINE__)
#define free(ptr) nFree(ptr)
在C和C++编程中,__FILE__
和 __LINE__
是两个预处理宏,它们在编译时被自动定义。这两个宏为程序员提供了代码在源文件中的具体位置信息,非常有用于调试、日志记录、错误报告等场景。
__FILE__
__FILE__
宏在预处理期间被展开为一个字符串常量,表示当前源文件的名称。这个名称通常是完整的文件路径,具体取决于编译器的实现和编译时的文件指定方式。在多文件项目中,__FILE__
可以帮助确定日志消息或错误报告的来源。
使用示例:
printf("Error occurred in file: %s\n", __FILE__);
这行代码将输出发生错误时所在的文件名,帮助定位问题发生的源代码文件。
__LINE__
__LINE__
宏在预处理期间被展开为一个整型常量,表示宏在源文件中的行号。这对于定位程序中的具体位置非常有用,尤其是在输出错误信息时。
使用示例:
printf("Error occurred at line: %d\n", __LINE__);
这行代码将输出错误发生的行号,使得调试过程中能够迅速定位到代码中的具体位置。
局限性
不可访问源代码的情况: 在很多实际应用场景中,特别是处理第三方库或遗留系统时,可能无法访问或修改源代码。在这种情况下,使用 LD_PRELOAD 来劫持内存管理函数成为了一种可行的方法,它允许你在不改动原有二进制文件的情况下注入自定义逻辑。
需要非侵入式修改时: 即使在可以修改源代码的情况下,引入 nMalloc 和 nFree 还是需要重新定义全局的 malloc 和 free,这可能会影响到代码库中的每一个使用这些函数的地方。而使用钩子技术可以在不触碰这些代码的情况下,实现对内存操作的监控和控制。
3. 动态链接库劫持的方法
劫持概念的介绍
在复杂的应用环境中,直接修改源代码以引入自定义内存管理可能不可行。此时,可以通过动态链接库劫持技术在运行时重定义内存管理函数,无需修改现有代码。
使用 dlsym 和 LD_PRELOAD 劫持内存函数
使用 dlsym
函数和 LD_PRELOAD
环境变量,开发者可以劫持任何动态链接库中的函数,包括 malloc
和 free
。LD_PRELOAD
指定的库会在程序启动前加载,允许其中的符号优先于其他库解析,实现对原始函数的劫持。
示例实现:
typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;
typedef void (*free_t)(void *ptr);
free_t free_f = NULL;
void init_hook(void) {
malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
free_f = (free_t)dlsym(RTLD_NEXT, "free");
}
void *malloc(size_t size) {
if (enable_malloc) {
enable_malloc = 0;
void *p = malloc_f(size);
enable_malloc = 1;
return p;
}
return malloc_f(size);
}
void free(void *ptr) {
if (enable_free) {
enable_free = 0;
free_f(ptr);
enable_free = 1;
} else {
free_f(ptr);
}
}
在这种设置中,自定义的 malloc
和 free
函数通过 malloc_f
和 free_f
调用原始的内存分配和释放函数,允许在不影响应用程序正常功能的情况下插入额外的逻辑,如内存使用监控。
4. 获取调用堆栈信息
ConvertToELF 函数的作用
ConvertToELF
函数的主要作用是将程序中的内存地址转换为从可执行文件的基地址开始的偏移量。这一转换对于使用工具如 addr2line
来解析地址至具体的源代码行是必需的,因为 addr2line
需要 ELF 文件中的相对地址,而不是程序运行时的绝对内存地址。
详解:
在动态链接的应用程序中,代码和数据的实际内存地址在每次运行时都可能不同,这是由于操作系统的地址空间布局随机化(ASLR)技术。ConvertToELF
通过从实际地址中减去模块的加载基址(l_addr
),计算出一个与运行时无关的固定偏移量。这样,即使在不同的运行实例中,相同的代码位置都会有相同的偏移量。
void *ConvertToELF(void *addr) {
Dl_info info;
struct link_map *link;
dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);
return (void *)((size_t)addr - link->l_addr);
}
利用 addr2line 定位源代码行
通过 ConvertToELF
获取的偏移量,可以使用 addr2line
工具来将这个偏移量映射到具体的源代码文件和行号。这对于调试和修复内存泄漏等问题非常有用,因为它允许开发者直接看到问题发生的源代码位置。
5. 使用 addr2line 定位内存泄漏
步骤详解
假设已经通过调用 ConvertToELF
获取了一个函数调用地址的相对偏移。下面是如何使用 addr2line
来定位这个地址对应的源代码行的步骤:
- 确保编译时加入了
-g
选项,以包含调试信息。 - 运行
addr2line
命令,指定相对偏移和目标可执行文件:
这里的addr2line -e your_program_executable -f -i -a 0xRelativeOffset
-e
选项指定可执行文件,-f
显示函数名,-i
在有内联函数时展示所有内联框架,-a
显示偏移量。
示例
假设从 ConvertToELF(caller)
得到的地址是 0x3a2d4
,您可以使用以下命令来找出对应的源码位置:
addr2line -f -e ./memleak -a 0x3a2d4
这条命令将输出类似下面的信息,指示错误发生的函数和行号:
0x3a2d4
main
/path/to/source/file.c:42
如何利用 addr2line 输出信息
输出的信息将帮助开发者快速定位问题发生的源文件和具体行号,大大简化调试过程。在内存泄漏的情况下,能够准确知道哪一行代码未正确释放内存,从而直接跳转到该位置进行修复。
6. 结论
内存泄漏检测组件的重要性和实用性
开发自定义的内存泄漏检测组件不仅能帮助开发者提高代码质量,还能在软件开发过程中节省大量的调试和维护时间。这种组件能够提供即时的反馈和详细的内存使用情况,是优化软件性能和稳定性的关键工具。
定制工具的贡献
通过实现和使用这类定制工具,开发者可以更深入地理解和掌握内存管理,避免常见的编程错误,提升整个软件项目的可靠性和效率。此外,这种工具还可以作为团队内部代码审查和质量保证过程的一部分,进一步增强软件开发的专业性和系统性。
完整代码
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define HOOK 1
// kvstore -c kv.conf
#if !HOOK
int flag = 1;
void *nMalloc(size_t size, const char *filename, int line) {
void *p = malloc(size);
if (flag) {
char buff[128] = {0};
snprintf(buff, 128, "./mem/%p.mem", p);
FILE *fp = fopen(buff, "w");
if (!fp) {
free(p);
return NULL;
}
fprintf(fp, "[+]%s:%d, addr: %p, size: %ld\n", filename, line, p, size);
fflush(fp);
fclose(fp);
}
//printf("nMalloc: %p, size: %ld\n", p, size);
return p;
}
void nFree(void *ptr) {
//printf("nFree: %p, \n", ptr);
if (flag) {
char buff[128] = {0};
snprintf(buff, 128, "./mem/%p.mem", ptr);
if (unlink(buff) < 0) {
printf("double free: %p", ptr);
return ;
}
}
return free(ptr);
}
#define malloc(size) nMalloc(size, __FILE__, __LINE__)
#define free(ptr) nFree(ptr)
#else
// hook
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 = 1;
int enable_free = 1;
void *ConvertToELF(void *addr) {
Dl_info info;
struct link_map *link;
dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);
return (void *)((size_t)addr - link->l_addr);
}
// main --> f1 --> f2 --> malloc
void *malloc(size_t size) {
void *p = NULL;
if (enable_malloc) {
enable_malloc = 0;
p = malloc_f(size);
void *caller = __builtin_return_address(0);
char buff[128] = {0};
sprintf(buff, "./mem/%p.mem", p);
FILE *fp = fopen(buff, "w");
if (!fp) {
free(p);
return NULL;
}
//fprintf(fp, "[+]%p, addr: %p, size: %ld\n", caller, p, size);
fprintf(fp, "[+]%p, addr: %p, size: %ld\n", ConvertToELF(caller), p, size);
fflush(fp);
enable_malloc = 1;
} else {
p = malloc_f(size);
}
return p;
}
// addr2line
void free(void *ptr) {
if (enable_free) {
enable_free = 0;
char buff[128] = {0};
snprintf(buff, 128, "./mem/%p.mem", ptr);
if (unlink(buff) < 0) {
printf("double free: %p", ptr);
return ;
}
free_f(ptr);
enable_free = 1;
} else {
free_f(ptr);
}
return ;
}
void init_hook(void) {
if (!malloc_f) {
malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
}
if (!free_f) {
free_f = (free_t)dlsym(RTLD_NEXT, "free");
}
}
#endif
int main() {
#if HOOK
init_hook();
#endif
void *p1 = malloc(5);
void *p2 = malloc(10);
void *p3 = malloc(35);
void *p4 = malloc(10);
free(p1);
free(p3);
free(p4);
getchar();
}