注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
一、内存泄漏是什么?
调用malloc,没调用free
二、如何实现检测
- malloc的时候,创建文件
- free的时候,删除文件
- 程序运行结束,查看是否多出文件,文件数等于泄露数
(malloc的时候,可以往文件里面写入malloc调用的位置和大小,精准定位导致内存泄露的代码)
定位有两种方法:
1. 采用宏定义封装malloc / free
直接使用__FILE__、func、LINE
cpp
void *nMalloc(size_t size, const char *filename, const char *funcname, int line)
{
添加文件...
}
void nFree(void *ptr, const char *filename, const char *funcname, int line)
{
删除文件...
}
#define malloc(size) nMalloc(size, __FILE__, __func__, __LINE__)
#define free(ptr) nFree(ptr, __FILE__, __func__, __LINE__)
main
{
malloc();
free();
}
2. 采用hook封装malloc
cpp
typedef void *(*malloc_t)(size_t size);
typedef void (*free_t)(void *ptr);
malloc_t malloc_f = NULL;
free_t free_f = NULL;
void *TranslateToSymbol(void *addr)
{
Dl_info info;
struct link_map *link;
dladdr1(addr, &info, (void *)&link, RTLD_DL_LINKMAP);
return (void *)(addr - link->l_addr);
}
void *malloc(size_t size)
{
...
void *caller = __builtin_return_address(0); // 返回上一级调用处所在的地址, 1 则是上上级
snprintf(buff, 128, "./block/%p.mem", ptr);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+][%p] %p: %ld malloc\n", TranslateToSymbol(caller), ptr, size); // addr2line -e ./[exe] -f -a [addr] 查找对应的行数
fflush(fp);
...
}
void free(void *ptr)
{
...
}
int main()
{
malloc();
free();
}
知识点:
2.1 __builtin_return_address(n):
获取上(n+1)级调用该函数的地址
2.2 TranslateToSymbol():
计算出相对偏移地址,兼容新旧两系统关于地址的获取。
| 系统状态(核心变量) | __builtin_return_address (0) 返回值(运行时绝对地址) | 未用 TranslateToSymbol(直接用绝对地址查 addr2line) | 用 TranslateToSymbol(绝对地址 - 加载基址 = 相对偏移) | addr2line 解析结果(加 -g 编译) |
|---|---|---|---|---|
| ASLR 关闭(旧系统如 Linux 2.4、XP 未开 ASLR) | 固定值(无随机性)例:0x08048abc(加载基址固定为 0x08048000) |
直接用 0x08048abc 查询 |
转换后仍为 0x08048abc - 0x08048000 = 0x8abc(基址为 0 / 固定) |
均可准确解析 |
| ASLR 开启(Linux 2.6.12+、RHEL5、新版系统) | 随机值(每次运行不同) 例 1:0x7f1234567890 例 2:0x7f9876547890 (加载基址分别为 0x7f1234560000、 0x7f9876540000) |
直接用 0x7f1234567890 查询 |
转换后为固定值: 例1:0x7f1234567890 - 0x7f1234560000 = 0x7890 例2:0x7f9876547890 - 0x7f9876540000 = 0x7890 |
未转换:解析失败(???) 转换后:准确解析(main.c:45) |
可以调用:cat /proc/sys/kernel/randomize_va_space, 为0则ASLR 关闭、为1或2则ASLR 开启
2.3 根据文件定位malloc的位置。
注意:编译时一定要加-g,否则addr2line输出的第3行信息会是乱码。
查找方式:

三、完整代码
1. 使用方式:
需要创建block文件夹专门存储生成的文件:
- 采用宏:直接正常编译 gcc -o memleak memleak.c -ldl,然后查看文件的内容即可。
- 采用hook: 需要加-g,即gcc -o memleak memleak.c -ldl -g, 查看文件内容,
然后执行 addr2line -e ./[exe] -f -a [caller_addr] 。- 统计 ./block 下所有文件(含子目录、不含软链接):find ./block -type f | wc -l
2. 知识点
2.1 宏调用跟函数调用区别
- 宏调用是预处理阶段的文本替换,无函数调用开销、无类型检查,仅作用于代码文本层面;函数接口调用是运行时的指令执行,有调用栈开销、有严格类型检查,作用于内存中函数实体层面。
- 宏方法可以直接获取行数,且printf等其它内置函数,隐式调用的仍然是malloc的原生接口。
- 而hook方法需要获取调用者的地址,根据地址推导出行数。且要考虑内置函数隐式调用malloc的问题。
2.2 热更新(可选择是否开启):
创建文件:mem_monitor.conf。里面写入enable=1。程序运行时可以随时更改enable的值控制内存监测是否开启。
3. hook的注意事项:
主要问题:
printf、fopen、snprintf、fprintf、fflush 等标准库函数分配内存时,会调用你重定义的
malloc接口(非 libc 原生);其内存释放时则调用 libc 内部封装的原生free接口(不走你重定义的free)。问题1:
重定义的malloc内部调用了fprintf等函数,它们会调用你自定义的malloc,不做处理的话会直接陷入无限循环。
解决:
enable_malloc, 为1时才能走写入文件这路线。
在调用fprintf之前将其置为0, 调用完之后置为1。这样保证了malloc内部的fprintf不会生成文件
问题2:
在外部调用printf等函数,它们分配内存走的是重定义的malloc(会生成文件),而释放内存走的是系统内置的。这就导致其生成的文件无法释放,造成有内存泄露的假象。
解决:
is_business_code:
判断caller_addr 是我们用户态程序的地址,还是三方库(libc)的地址,只有是我们用户态程序的地址才创建文件。
因此使用hook要考虑的问题有很多,代码更加繁杂,使用的时候建议采用宏这种方式
4. memleak.c
cpp
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
// 统计 ./block 下所有文件(含子目录、不含软链接):find ./block -type f | wc -l
#define CONFIGURE_FILE_NAME "./mem_monitor.conf"
// 是否开启内存监测
int monitor_enable = 1;
// 读取配置文件函数
void load_config(const char *config_path)
{
FILE *fp = fopen(config_path, "r");
if (!fp)
{
// 配置文件不存在则保持当前状态,不影响程序运行
return;
}
char line[64] = {0};
int new_enable = 1; // 默认保持开启
while (fgets(line, sizeof(line), fp) != NULL)
{
// 解析配置项:enable=1 或 enable=0
if (strstr(line, "enable=") == line)
{
new_enable = atoi(line + strlen("enable="));
break;
}
}
fclose(fp);
// 仅当配置值变化时更新开关并打印提示(不影响核心逻辑)
if (new_enable != monitor_enable)
{
monitor_enable = new_enable;
printf("[热更新] 内存监测状态切换:%s\n", monitor_enable ? "开启" : "关闭");
}
}
#if 1
void *nMalloc(size_t size, const char *filename, const char *funcname, int line)
{
void *ptr = malloc(size);
if (!monitor_enable)
{
return ptr;
}
char buff[128] = {0};
snprintf(buff, 128, "./block/%p.mem", ptr);
FILE *fp = fopen(buff, "w");
if (!fp)
{
free(ptr);
return NULL;
}
#if 0
// 系统时间
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now); // 线程安全的时间转换
char time_buf[32] = {0};
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_now);
#else
// 北京时间
time_t now = time(NULL);
// 北京时间 = UTC时间 + 8小时(3600*8=28800秒)
time_t beijing_time = now + 28800;
struct tm tm_now;
gmtime_r(&beijing_time, &tm_now); // 用gmtime_r避免本地时区干扰
char time_buf[32] = {0};
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_now);
#endif
fprintf(fp, "malloc: [%s] [%s:%s:%d] [ptr: %p]: [size: %ld] \n", time_buf, filename, funcname, line, ptr, size);
fflush(fp);
fclose(fp);
return ptr;
}
void nFree(void *ptr, const char *filename, const char *funcname, int line)
{
if (!monitor_enable)
{
return free(ptr);
}
char buff[128] = {0};
snprintf(buff, 128, "./block/%p.mem", ptr);
unlink(buff);
return free(ptr);
}
#define malloc(size) nMalloc(size, __FILE__, __func__, __LINE__)
#define free(ptr) nFree(ptr, __FILE__, __func__, __LINE__)
#else
// hook
typedef void *(*malloc_t)(size_t size);
typedef void (*free_t)(void *ptr);
malloc_t malloc_f = NULL;
free_t free_f = NULL;
int enable_malloc = 1;
int enable_free = 1;
void *TranslateToSymbol(void *addr)
{
Dl_info info;
struct link_map *link;
dladdr1(addr, &info, (void *)&link, RTLD_DL_LINKMAP);
return (void *)(addr - link->l_addr);
}
// 判断调用者是否是业务代码(非标准库)
int is_business_code(void *caller)
{
Dl_info info;
if (dladdr(caller, &info) == 0)
return 0;
// 核心:只要不是libc的调用,就是业务代码
return (info.dli_fname && strstr(info.dli_fname, "libc") == NULL);
}
void *malloc(size_t size)
{
if (!malloc_f)
{
malloc_f = dlsym(RTLD_NEXT, "malloc");
}
void *ptr = NULL;
if (monitor_enable && enable_malloc)
{
enable_malloc = 0;
ptr = malloc_f(size);
void *caller = __builtin_return_address(0); // 返回上一级调用处所在的地址, 1 则是上上级
if (!is_business_code(caller))
{
enable_malloc = 1;
return ptr;
}
char buff[128] = {0};
snprintf(buff, 128, "./block/%p.mem", ptr); // 文件名采用ptr
FILE *fp = fopen(buff, "w");
if (!fp)
{
enable_malloc = 1;
free(ptr);
return NULL;
}
// 获取并格式化当前时间
#if 0
// 系统时间
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now); // 线程安全的时间转换
char time_buf[32] = {0};
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_now);
#else
// 北京时间
time_t now = time(NULL);
// 北京时间 = UTC时间 + 8小时(3600*8=28800秒)
time_t beijing_time = now + 28800;
struct tm tm_now;
gmtime_r(&beijing_time, &tm_now); // 用gmtime_r避免本地时区干扰
char time_buf[32] = {0};
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_now);
#endif
// 仅在原有输出基础上添加时间字段,其余内容完全不变
fprintf(fp, "malloc: [%s] [caller_addr: %p] [ptr: %p]: [size: %ld]\n", time_buf, TranslateToSymbol(caller), ptr, size); // addr2line -e ./[exe] -f -a [caller_addr] 查找对应的行数
fflush(fp);
fclose(fp);
enable_malloc = 1;
}
else
{
ptr = malloc_f(size);
}
return ptr;
}
void free(void *ptr)
{
if (!free_f)
{
free_f = dlsym(RTLD_NEXT, "free");
}
if (monitor_enable)
{
char buff[128] = {0};
snprintf(buff, 128, "./block/%p.mem", ptr);
unlink(buff);
}
return free_f(ptr);
}
#endif
void *monitor_thread(void *)
{
while (1)
{
load_config(CONFIGURE_FILE_NAME); // 每秒读取一次配置文件
sleep(1);
}
}
#if 1
int main()
{
// 不开启热更新
printf("start\n");
size_t size = 5;
void *p1 = malloc(size);
void *p2 = malloc(size * 2);
void *p3 = malloc(size * 3);
free(p1);
free(p3);
return 0;
}
#else
// 热更新开启
int main()
{
load_config(CONFIGURE_FILE_NAME);
pthread_t t1;
pthread_create(&t1, NULL, monitor_thread, NULL);
size_t size = 5;
printf("begin\n");
for (int i = 0; i < 1000; i++)
{
void *p1 = malloc(size);
void *p2 = malloc(size * 2);
void *p3 = malloc(size * 3);
free(p1);
free(p3);
// usleep(10000); // 期间开启/关闭监测
}
printf("complete\n");
pthread_join(t1, NULL);
return 0;
}
#endif
5. mem_monitor.conf
bash
# 开启内存监测(默认)
enable=1
6. 测试结果:
1. 宏方式:

2. hook方式:

(热更新你们感兴趣自己测试吧)