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

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

相关推荐
我命由我123457 分钟前
Photoshop - Photoshop 工具栏(58)锐化工具
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
前端小菜袅15 分钟前
AI时代,新的技术学习方式
学习·ai编程
victory043123 分钟前
大模型学习阶段总结和下一阶段展望
深度学习·学习·大模型
程序猿零零漆27 分钟前
Spring之旅 - 记录学习 Spring 框架的过程和经验(十三)SpringMVC快速入门、请求处理
java·学习·spring
曾浩轩36 分钟前
跟着江协科技学STM32之4-5OLED模块教程OLED显示原理
科技·stm32·单片机·嵌入式硬件·学习
CCPC不拿奖不改名1 小时前
网络与API:从HTTP协议视角理解网络分层原理+面试习题
开发语言·网络·python·网络协议·学习·http·面试
却道天凉_好个秋1 小时前
音视频学习(八十四):视频压缩:MPEG 1、MPEG 2和MPEG 4
学习·音视频
●VON1 小时前
AI 保险机制:为智能时代的不确定性兜底
人工智能·学习·安全·制造·von
代码游侠1 小时前
学习笔记——HC-SR04 超声波测距传感器
开发语言·笔记·嵌入式硬件·学习
军军君011 小时前
Three.js基础功能学习七:加载器与管理器
开发语言·前端·javascript·学习·3d·threejs·三维