内存泄漏检测组件

简介: 内存泄漏检测组件

一、内存泄漏概述


1.1 什么是内存泄漏

内存泄漏是在没有自动 gc 的编程语言里面,经常发生的一个问题。


自动垃圾回收(Automatic Garbage Collection,简称 GC)是一种内存管理技术,在程序运行时自动检测和回收不再使用的内存对象,以避免内存泄漏和释放已分配内存的负担。


因为没有 gc,所以分配的内存需要程序员自己调用释放。其核心原因是调用分配与释放没有符合开闭原则,没有配对,形成了有分配,没有释放的指针,从而产生了内存泄漏。


void func(size_t s1)
{
  void p1=malloc(s1);
  void p2=malloc(s1);
  free(p1);
}

以上代码段,分配了两个s1大小的内存块,由 p1 与 p2 指向。而代码块执行完以后,释放了 p1,而 p2 没有释放。形成了有分配没有释放的指针,产生了内存泄漏。


1.2 内存泄漏导致的后果

随着工程代码量越来越多,有分配没有释放,自然会使得进程堆的内存会越来越少,直到耗尽。从而导致后面的运行时代码不能成功分配内存,使程序奔溃。


1.3 内存泄漏解决思路

最好的办法肯定是引入自动垃圾回收gc。但是这不适合C/C++语言。


解决内存泄漏,我们需要解决两点:

1)能够检测出来是否发送内存泄漏

2)如果发生内存泄漏,能够检测出来具体是哪一行代码所引起的。


内存泄漏是由于内存分配与内存释放,不匹配所引起的。因此对内存分配函数malloc/calloc/realloc,以及内存释放函数free进行“劫持”hook,就能能够统计出内存分配的位

置,内存释放的位置,从而判断是否匹配。


二、宏定义方法

2.1 宏定义

使用宏定义,替换系统的内存分配接口。并利用__FILE__、__LINE__分别获取当前编译文件的文件名、行号,进行追踪位置信息。

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

需要注意的是,宏定义一定要放在内存分配之前,这样预编译阶段才会替换为我们自己实现的_malloc和_free。


2.2 检测位置

为了方便观察,我们可以在内存分配_malloc的时候,创建一个文件。文件名为指向新分配内存的指针值,文件内容为指针值、调用_malloc时的文件名、行号。

在该内存释放_free的时候,删除该指针对应的文件。

最后,程序运行结束,如果没有文件说明没有内存泄漏,否则说明存在内存泄漏。


2.3 结果分析

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

最后在memory文件夹里,可以看到存在一个文件,说明有一个地方出现内存泄漏


b09b10efd5139183e3296b06c8d33948_36172e11f6224d9dba0cdd81f375377f.png

[+]addr: 0x559e55b6e8b0, filename: fun1.c, line: 39

从结果上看,内存泄漏发生第39行。


三、hook方法

利用 hook 机制改写系统的内存分配函数。


3.1 hook

hook方法的实现分三个步骤

1)定义函数指针。

typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;
typedef void (*free_t)(void *ptr);
free_t free_f = NULL;

2)函数实现,函数名与目标函数名一致。


void *malloc(size_t size)
{
  //改写的功能
}
void free(void *ptr)
{
  //改写的功能
}

3)初始化hook,调用dlsym()。

void init_hook(){
    if (!malloc_f){
        malloc_f = dlsym(RTLD_NEXT, "malloc");
    }
    if (!free_f){
        free_f = dlsym(RTLD_NEXT, "free");
    }
}

3.2 检测位置

宏定义的方法在检测调用所在行号的时候使用了系统定义的__LINE__,因为是宏定义的malloc,预编译时候直接嵌入。因此__LINE__返回的就是调用malloc的位置。


但是hook方法不一样,系统定义的__LINE__在函数内部调用,无法确定在主函数中的调用位置。比如


fprintf(fp, "[+]addr: %p, filename: %s, line: %d\n", ptr, filename, line);

返回的就是fprintf所在的行号。


因此使用gcc 提供的__builtin_return_address,该函数返回当前函数或其调用者之一的返回地址。 参数level 表示向上扫描调用堆栈的帧数。比如对于 main --> f1() --> f2() --> f3() ,f3()函数里面调用 __builtin_return_address (0),返回f3的地址;调用 __builtin_return_address (1),返回f2的地址;


3.3 递归调用

hook的时候,要考虑其他函数也用到所hook住的函数,比如在printf()函数里面也调用了malloc,那么就需要防止内部递归进入死循环。

f27dc9c2c32ed9bb9eab7cf540ec6e76_44425a3dd2114ab39fcda0399b0c9920.png

通过gdb调试,在第23行打断点,发现每次运行都回到了23行。

这是因为sprintf隐含调用了malloc,这样就陷入一个循环:

23行的sprintf —> 自定义的malloc —> 23行的sprintf —> 自定义的malloc --> 23行的sprintf —> 自定义的malloc --> ……

解决办法是,限制调用次数。当进入 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 的部分
       // 其他代码
        enable_malloc_hook = 1;
    }
    // 执行原来的 malloc 函数
    else {
        p = malloc_f(size);
    }
}

3.4 结果分析

// gcc -o fun2 fun2.c -ldl -g
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <link.h>
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_hook = 1;
int enable_free_hook = 1;
void *malloc(size_t size){
    void *ptr = NULL;
    if (enable_malloc_hook ){
        enable_malloc_hook = 0; 
        enable_free_hook = 0;
        ptr = malloc_f(size);
        void *caller = __builtin_return_address(0);
        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);
        FILE *fp = fopen(buffer, "w");
        fprintf(fp, "[+] caller: %p, addr: %p, size: %ld\n", caller, ptr, size);
        fflush(fp);
        fclose(fp);
        enable_malloc_hook = 1;
        enable_free_hook = 1;
    }
    else {
        ptr = malloc_f(size);
    }
    return ptr;
}
void free(void *ptr){
    if (enable_free_hook ){
        enable_free_hook = 0;
        enable_malloc_hook = 0;
        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);
        if (unlink(buffer) < 0){
            printf("double free: %p\n", ptr);
            return;
        }
        free_f(ptr);
        enable_malloc_hook = 1;
        enable_free_hook = 1;
    }
    else {
        free_f(ptr);
    }
}
void init_hook(){
    if (!malloc_f){
        malloc_f = dlsym(RTLD_NEXT, "malloc");
    }
    if (!free_f){
        free_f = dlsym(RTLD_NEXT, "free");
    }
}
int main(){
    init_hook();
    void *p1 = malloc(5);
    void *p2 = malloc(18);
    void *p3 = malloc(15);
    free(p1);
    free(p3);
}

305031bd546a201823f346326dc91978_fb3a87783f264f37948dce4538ccc60e.png

从结果看存在一个内存泄漏,但是 caller:0x16bb 是地址,不是具体行号。使用addr2line可以将地址转换为文件名和行号。


3.5 addr2line

利用addr2line工具,将地址转换为文件名和行号,得到源文件的行数(根据机器码地址定位到源码所在行数)


addr2line -f -e fun2 -a 0x16bb

参数:

-f:显示函数名信息。

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

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


但是,高版本 gcc 下使用 addr2line 命令会出现乱码问题。


??
??:0

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

03b98054a6c81ead40f9df3e5d16eb6a_353737628a934da7a097baa8c69accd4.png

解决办法是利用动态链接库的dladdr函数 ,作用于共享目标,可以获取某个地址的符号信息。使用该函数可以解析符号地址


// gcc -o fun2 fun2.c -ldl -g
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <link.h>
// 解析地址
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);
}
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_hook = 1;
int enable_free_hook = 1;
void *malloc(size_t size){
    void *ptr = NULL;
    if (enable_malloc_hook ){
        enable_malloc_hook = 0; 
        ptr = malloc_f(size);
        void *caller = __builtin_return_address(0);
        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);
        FILE *fp = fopen(buffer, "w");
        // converToELF(caller)
        fprintf(fp, "[+] caller: %p, addr: %p, size: %ld\n", converToELF(caller), ptr, size);
        fflush(fp);
        fclose(fp);
        enable_malloc_hook = 1;
    }
    else {
        ptr = malloc_f(size);
    }
    return ptr;
}
void free(void *ptr){
    if (enable_free_hook ){
        enable_free_hook = 0;
        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);
        if (unlink(buffer) < 0){
            printf("double free: %p\n", ptr);
            return;
        }
        free_f(ptr);
        enable_free_hook = 1;
    }
    else {
        free_f(ptr);
    }
}
void init_hook(){
    if (!malloc_f){
        malloc_f = dlsym(RTLD_NEXT, "malloc");
    }
    if (!free_f){
        free_f = dlsym(RTLD_NEXT, "free");
    }
}
int main(){
    init_hook();
    void *p1 = malloc(5);
    void *p2 = malloc(18);
    void *p3 = malloc(15);
    free(p1);
    free(p3);
}

四、__libc_malloc 和 __libc_free

思路和hook的一样,因为malloc和free底层调用的也是__libc_malloc和__libc_free。

// gcc -o fun3 fun3.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <link.h>
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);
}
extern void *__libc_malloc(size_t size);
extern void *__libc_free(void *ptr);
int enable_malloc_hook = 1;
int enable_free_hook = 1;
void *malloc(size_t size){
    void *ptr = NULL;
    if (enable_malloc_hook ){
        enable_malloc_hook = 0; 
        enable_free_hook = 0;
        ptr = __libc_malloc(size);
        void *caller = __builtin_return_address(0);
        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);
        FILE *fp = fopen(buffer, "w");
        fprintf(fp, "[+] caller: %p, addr: %p, size: %ld\n", converToELF(caller), ptr, size);
        fflush(fp);
        fclose(fp);
        enable_malloc_hook = 1;
        enable_free_hook = 1;
    }
    else {
        ptr = __libc_malloc(size);
    }
    return ptr;
}
void free(void *ptr){
    if (enable_free_hook ){
        enable_free_hook = 0;
        enable_malloc_hook = 0;
        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);
        if (unlink(buffer) < 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(5);
    void *p2 = malloc(18);
    void *p3 = malloc(15);
    free(p1);
    free(p3);
}
目录
相关文章
|
18天前
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
114 52
|
29天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
46 6
|
1月前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
188 9
|
1月前
|
监控 JavaScript 前端开发
如何检测和解决 JavaScript 中内存泄漏问题
【10月更文挑战第25天】解决内存泄漏问题需要对代码有深入的理解和细致的排查。同时,不断优化和改进代码的结构和逻辑也是预防内存泄漏的重要措施。
46 6
|
1月前
|
Web App开发 缓存 JavaScript
如何检测和解决闭包引起的内存泄露
闭包引起的内存泄露是JavaScript开发中常见的问题。本文介绍了闭包导致内存泄露的原因,以及如何通过工具检测和代码优化来解决这些问题。
|
2月前
|
Web App开发 开发者
|
2月前
|
缓存 监控 Java
内存泄漏:深入理解、检测与解决
【10月更文挑战第19天】内存泄漏:深入理解、检测与解决
73 0
|
2月前
|
设计模式 Java Android开发
安卓应用开发中的内存泄漏检测与修复
【9月更文挑战第30天】在安卓应用开发过程中,内存泄漏是一个常见而又棘手的问题。它不仅会导致应用运行缓慢,还可能引发应用崩溃,严重影响用户体验。本文将深入探讨如何检测和修复内存泄漏,以提升应用性能和稳定性。我们将通过一个具体的代码示例,展示如何使用Android Studio的Memory Profiler工具来定位内存泄漏,并介绍几种常见的内存泄漏场景及其解决方案。无论你是初学者还是有经验的开发者,这篇文章都将为你提供实用的技巧和方法,帮助你打造更优质的安卓应用。
|
2月前
|
数据处理 Python
Python读取大文件的“坑“与内存占用检测
Python读取大文件的“坑“与内存占用检测
58 0
|
2月前
|
存储 算法 C语言
MacOS环境-手写操作系统-15-内核管理 检测可用内存
MacOS环境-手写操作系统-15-内核管理 检测可用内存
45 0