C/C++程序员必备技能 ---- 内存泄漏检测

简介: C/C++程序员必备技能 ---- 内存泄漏检测

1、mtrace


mtrace(memory trace),是 GNU Glibc 自带的内存问题检测工具,追踪内存分配相关函数的调用,检测内存是否泄漏,定位内存泄漏的位置。


1.1、mtrace 函数


通过 hook 机制实现。

#include <mcheck.h>
// 开启内存分配跟踪 
void mtrace(void);
// 取消内存分配跟踪 
void muntrace(void);
1.2、设置日志生成路径


外部(线上系统):export`命令导出环境变量,需要重启


改进:使用进程热更新,则无需重启。


export MALLOC_TRACE=./mem.log


内部:setenv 函数增加或修改环境变量(只在本进程本次执行中生效)


setenv("MALLOC_TRACE", "./mem.log");


1.3、编译源码


调试程序,加上-g调试选项,创建符号表,关闭优化机制,这样可以定位到源码的具体位置,而不是可执行文件的地址信息。


gcc -o memleak memleak.c -g


1.4、运行分析


例:源码第 16 行 malloc 发生了内存泄漏。

3.png

运行结果如下:+ 表示申请内存, - 表示释放内存。对比后发现机器码 [0x400671] 处调用 malloc 申请的内存未释放。

4.png

1.5、定位源码位置addr2line


通过使用 addr2line 命令工具,得到源文件的行数(根据机器码地址定位到源码所在行数)


参数:


-f:显示函数名信息。


-e filename:指定需要转换地址的可执行文件名。


-a address:显示指定地址(十六进制)。


使用例子如下:


addr2line -f -e memleak -a 0x4011ab      

5.png  

2、宏定义


2.1、检测位置


使用宏定义,替换系统的内存分配函数,并在内部添加位置信息,实现监控    

#define malloc(size) _malloc(size, __FILE__, __LINE__)
#define free(p) _free(p, __FILE__, __LINE__)

这里,使用了系统宏定义,来追踪位置信息。


__FILE__,正在编译文件的文件名


__LINE__,正在编译文件的行号


2.2、改进方案


每次观察内存分配与释放是否一一对应的时候,令人恼火,有没有什么更好的方法?


我们可以在内存分配的同时,创建一个文件,文件名为指向新分配内存的指针,文件里记录源码调用内存分配函数时对应的位置;该内存释放时,删除该指针对应的文件。这样一来,程序结束后,如果没有文件则说明内存没有泄露;若存在文件,则说明内存发生泄漏。

 

2.3、运行分析


例:源码第 36 行 malloc 发生了内存泄漏。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void *_malloc(size_t size, const char *filename, int line) {  
    void *p = malloc(size);
    char buff[128] = {0};
    sprintf(buff, "./mem/%p.mem", p);
    FILE *fp = fopen(buff, "w");
    fprintf(fp, "[+]%s:%d, addr: %p, size: %ld\n", filename, line, p, size);
    // printf("[+]%s:%d, %p\n", filename, line, p);
    fflush(fp);
    fclose(fp);
    return p;
}
void _free(void *p, const char *filename, int line) {    
    char buff[128] = {0};
    sprintf(buff, "./mem/%p.mem", p);
    if (unlink(buff) < 0) {
        printf("double free: %p\n", p);
        return;
    }
    return free(p);
}
#define malloc(size) _malloc(size, __FILE__, __LINE__)
#define free(p) _free(p, __FILE__, __LINE__)
int main() { 
    void *p1 = malloc(10);
    void *p2 = malloc(15);
    void *p3 = malloc(20);
    free(p2);
    free(p3);
    return 0;
}

运行结束后,可以看到,存在一个文件,说明发生了内存泄漏。查看文件信息后,结果显示在源码的第 36 行,正好对应指针 p1 的 malloc。


[+]memleak.c:36, addr: 0x1a88010, size: 10


3、hook


利用 hook 机制改写系统的内存分配函数,同样关键在于如何检测并确定内存泄漏的位置。


3.1、检测位置


系统宏定义__LINE__在函数内部调用,无法确定在主函数中的调用位置。


gcc 提供了 __builtin_return_address ,该函数返回当前函数或其调用者之一的返回地址。 参数level 表示向上扫描调用堆栈的帧数

void * __builtin_return_address(unsigned int level)

这样,我们可以获得主函数中调用系统内存分配函数的所在位置。


3.2、递归调用的问题


由于我们要改写系统内存的分配函数,不可避免的,对于某些系统函数,若其内部也调用了内存分配函数,则同样被改写了,会产生功能问题。


例:我们改写了系统内存分配函数 malloc 函数,调用 malloc 函数时执行printf 函数,而 printf 函数内部会调用系统的 malloc 函数。

void *malloc(size_t size) { 
  printf("%s:%d\n", __FILE__, __LINE__);
}

从 gdb 调试结果可以看出,调用 malloc 执行 printf 函数,调用 printf 执行 malloc 函数,因此陷入无尽的递归调用中,直至栈溢出。

Breakpoint 1, malloc (size=1024) at memleak.c:48
48    printf("%s:%d\n", __FILE__, __LINE__);
(gdb) c
Continuing

为了解决这一问题,我们设置变量 enable_malloc_hook,当进入 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 的部分
        printf("%s:%d\n", __FILE__, __LINE__);
        enable_malloc_hook = 1;
    }
    // 执行原来的 malloc 函数
    else {
        p = malloc_f(size);
    }
}
3.3、运行分析


例:源码第 82 行 malloc 发生了内存泄漏。

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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_hook = 1;
int enable_free_hook = 1; 
void *malloc(size_t size) { 
    void *p = NULL;
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;
        enable_free_hook = 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");
        fprintf(fp, "%s:[%p] + addr: %p, size: %ld\n", __FILE__, caller, p, size);
        fflush(fp);
        fclose(fp);
        enable_free_hook = 1;
        enable_malloc_hook = 1;
    } 
    else {
        p = malloc_f(size);
    }
    return p;
}
void free(void *ptr) {    
    if (enable_free_hook) {
        enable_free_hook = 0;
        enable_malloc_hook = 0;
        char buff[128] = {0};
        sprintf(buff, "./mem/%p.mem", ptr);
        if (unlink(buff) < 0) {
            printf("double free: %p\n", ptr);
            return;
        }
        free_f(ptr);
        enable_malloc_hook = 1;
        enable_free_hook = 1;
    } 
    else {
        free_f(ptr);
    }
}
static void init_hook(void) {
    if (malloc_f == NULL) {
        malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
    }
    if (free_f == NULL) {
        free_f = (free_t)dlsym(RTLD_NEXT, "free");
    }
}
int main() { 
    init_hook();
    void *p1 = malloc(10);
    void *p2 = malloc(15);
    void *p3 = malloc(20);
    free(p2);
    free(p3);
    return 0;
}

运行结束后,可以看到,存在一个文件,说明发生了内存泄漏。查看文件信息如下:


memleak.c:[0x400b62] + addr: 0x117d010, size: 10


使用 addr2line 定位源码位置


addr2line -f -e memleak -a 0x400b62


内存泄漏的位置在源码的第 82 行,正好对应指针 p1 的 malloc。


0x0000000000400b62


main


/home/jtz/code/memleak.c:82


4、__libc_malloc


gcc 提供__libc_malloc, libc_free接口用来代替系统的 malloc, free,实现 hook 机制。

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern void *__libc_malloc(size_t size);
extern void __libc_free(void *p);
int enable_malloc_hook = 1;
int enable_free_hook = 1; 
void *malloc(size_t size) { 
    void *p = NULL;
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;
        enable_free_hook = 0;
        p = __libc_malloc(size);
        // 返回函数堆栈
        void *caller = __builtin_return_address(0);
        char buff[128] = {0};
        sprintf(buff, "./mem/%p.mem", p);
        FILE *fp = fopen(buff, "w");
        fprintf(fp, "%s:[%p] + addr: %p, size: %ld\n", __FILE__, caller, p, size);
        fflush(fp);
        fclose(fp);
        enable_free_hook = 1;
        enable_malloc_hook = 1;
    } 
    else {
        p = __libc_malloc(size);
    }
    return p;
}
void free(void *ptr) {    
    if (enable_free_hook) {
        enable_free_hook = 0;
        enable_malloc_hook = 0;
        char buff[128] = {0};
        sprintf(buff, "./mem/%p.mem", ptr);
        if (unlink(buff) < 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(10);
    void *p2 = malloc(15);
    void *p3 = malloc(20);
    free(p2);
    free(p3);
    return 0;
}

5、addr2line 的乱码问题


在高版本 gcc 下使用 addr2line 命令会出现乱码问题,以 hook 方法实现的内存泄漏检测代码为例,换用 gcc 11.2.0 执行相同的代码,查看内存泄漏文件的信息。


memleak.c:[0x5649280a4625] + addr: 0x564929ae02a0, size: 10


使用 addr2line 定位所在的源码位置


addr2line -f -e memleak -a 0x5649280a4625


此时出现了乱码


0x00005649280a4625


??


??:0


这是由于 addr2line 作用于 ELF 可执行文件,而高版本的 gcc 调用 __builtin_return_address返回的地址 caller 位于内存映像上,所以会产生乱码。

6.png

动态链接库的dladdr函数 ,作用于共享目标,可以获取某个地址的符号信息,函数原型如下:

#define _GNU_SOURCE
#include <dlfcn.h>
int dladdr(void *addr, Dl_info *info);
int dladdr1(void *addr, Dl_info *info, void **extra_info, int flags);
Link with -ldl.

使用该函数可以解析符号地址

void* converToELF(void *addr) {
    Dl_info info;
    struct link_map *link;
    dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);
    // elf地址 = 用户程序的地址 - 基地址
    return (void *)((size_t)addr - link->l_addr);
}

我们对 hook 方法实现的内存检测方法的代码进行修改,源码第 93 行 malloc 发生了内存泄漏。

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <link.h>
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_hook = 1;
int enable_free_hook = 1; 
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);
}
void *malloc(size_t size) { 
    void *p = NULL;
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;
        enable_free_hook = 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");
        // 对 converToELF(caller) 返回的地址进行转换
        fprintf(fp, "%s:[%p] + addr: %p, size: %ld\n", __FILE__, converToELF(caller), p, size);
        fflush(fp);
        fclose(fp);
        enable_free_hook = 1;
        enable_malloc_hook = 1;
    } 
    else {
        p = malloc_f(size);
    }
    return p;
}
void free(void *ptr) {    
    if (enable_free_hook) {
        enable_free_hook = 0;
        enable_malloc_hook = 0;
        char buff[128] = {0};
        sprintf(buff, "./mem/%p.mem", ptr);
        if (unlink(buff) < 0) {
            printf("double free: %p\n", ptr);
            return;
        }
        free_f(ptr);
        enable_malloc_hook = 1;
        enable_free_hook = 1;
    } 
    else {
        free_f(ptr);
    }
}
static void init_hook(void) {
    if (malloc_f == NULL) {
        malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
    }
    if (free_f == NULL) {
        free_f = (free_t)dlsym(RTLD_NEXT, "free");
    }
}
int main() { 
    init_hook();
    void *p1 = malloc(10);
    void *p2 = malloc(15);
    void *p3 = malloc(20);
    free(p2);
    free(p3);
    return 0;
}

修改后执行程序,内存泄漏文件信息后,可以看到,ELF 地址 0x16af明显变小了。


memleak.c:[0x16af] + addr: 0x55a0d06b82a0, size: 10


使用 addr2line 定位所在的源码位置,验证成功。


0x00000000000016af


main


/home/jtz/memleak.c:93

相关文章
|
3月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
128 26
|
8月前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
3月前
|
存储 监控 算法
基于跳表数据结构的企业局域网监控异常连接实时检测 C++ 算法研究
跳表(Skip List)是一种基于概率的数据结构,适用于企业局域网监控中海量连接记录的高效处理。其通过多层索引机制实现快速查找、插入和删除操作,时间复杂度为 $O(\log n)$,优于链表和平衡树。跳表在异常连接识别、黑名单管理和历史记录溯源等场景中表现出色,具备实现简单、支持范围查询等优势,是企业网络监控中动态数据管理的理想选择。
84 0
|
4月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
61 1
|
10月前
|
存储 缓存 编译器
【硬核】C++11并发:内存模型和原子类型
本文从C++11并发编程中的关键概念——内存模型与原子类型入手,结合详尽的代码示例,抽丝剥茧地介绍了如何实现无锁化并发的性能优化。
404 68
|
7月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
8月前
|
监控 Java 计算机视觉
Python图像处理中的内存泄漏问题:原因、检测与解决方案
在Python图像处理中,内存泄漏是常见问题,尤其在处理大图像时。本文探讨了内存泄漏的原因(如大图像数据、循环引用、外部库使用等),并介绍了检测工具(如memory_profiler、objgraph、tracemalloc)和解决方法(如显式释放资源、避免循环引用、选择良好内存管理的库)。通过具体代码示例,帮助开发者有效应对内存泄漏挑战。
356 1
|
8月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
345 0
|
9月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
338 0
|
3月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
829 0

热门文章

最新文章