Linux C/C++ 学习日记(58):手写检测内存泄露的组件

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

一、内存泄漏是什么?

调用malloc,没调用free

二、如何实现检测

  1. malloc的时候,创建文件
  2. free的时候,删除文件
  3. 程序运行结束,查看是否多出文件,文件数等于泄露数
    (malloc的时候,可以往文件里面写入malloc调用的位置和大小,精准定位导致内存泄露的代码)

定位有两种方法:

1. 采用宏定义封装malloc / free

直接使用__FILE__、funcLINE

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方式:

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

相关推荐
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意4 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码4 天前
嵌入式学习路线
学习
毛小茛4 天前
计算机系统概论——校验码
学习
babe小鑫4 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms4 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下4 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。4 天前
2026.2.25监控学习
学习
im_AMBER4 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J4 天前
从“Hello World“ 开始 C++
c语言·c++·学习