C语言包装malloc/free检测内存泄漏时fopen触发段错误求助
内存泄漏检测工具的段错误问题
我尝试包装C标准库的malloc和free函数来检测内存泄漏,通过在函数里加fprintf把内存分配/释放的地址和大小写入日志文件。但用gcc -o mainapp main.c -Wall -Wextra编译运行时,fopen行会触发段错误;把fopen放到malloc和free函数里也一样会崩。换成printf就能正常运行,环境是Ubuntu 22.04、GCC 11.3.0,完整代码如下:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #define __USE_GNU #include <dlfcn.h> #define TEST_MEM_LEAK 1 // a value of 1 means to join the memory leak detection, and a value of 0 means not to join #if TEST_MEM_LEAK typedef void *(*malloc_t)(size_t size); malloc_t malloc_f = NULL; typedef void (*free_t)(void *p); free_t free_f = NULL; int malloc_flag = 1; // It is used to prevent repeated recursion and cannot exit because the printf function will call malloc for memory allocation int free_flag = 1; const char* logFileName = "/home/hammamiw/Documents/HeapMonitor/allocs.log"; FILE* fp = NULL; void initCheck() { fp = fopen("/home/hammamiw/Documents/HeapMonitor/allocs.log", "w"); } void *malloc(size_t size) { if(malloc_flag) { initCheck(); malloc_flag = 0; // Used to prevent printf from causing an error when calling malloc recursively void *p = malloc_f(size); fprintf(fp, "malloc, %lx, %lu\n", (uintptr_t)p, size); //printf("m\n"); malloc_flag = 1; // It is used to ensure that the initial value of flag flag is consistent when malloc in this file is called again return p; } else { return malloc_f(size); // Here, the malloc function in the system library obtained by dlsym is called } } void free(void *p) { initCheck(); if(free_flag) { //initCheck(); free_flag = 0; fprintf(fp, "F, %lx\n", (uintptr_t)p); //printf("f\n"); free_f(p); free_flag = 1; } else { free_f(p); } } #endif int main() { #if TEST_MEM_LEAK // the part from if to endif can be divided into function calls malloc_f = dlsym(RTLD_NEXT, "malloc"); if(!malloc_f) { printf("load malloc failed: %s\n", dlerror()); return 1; } free_f = dlsym(RTLD_NEXT, "free"); if(!free_f) { printf("load free failed: %s\n", dlerror()); return 1; } #endif void *p1 = malloc(10); //The malloc function in this article will be called first void *p2 = malloc(20); //Here, p2 is not released and there is a memory leak. Judge by checking whether the number of malloc and free times printed is the same free(p2); free(p1); return 0; }
问题根源
- 递归调用+空指针访问:
fopen内部会调用malloc,而你重写的malloc又会调用initCheck执行fopen,形成无限递归。更致命的是,main函数里通过dlsym获取系统malloc指针的代码是在第一次malloc调用之后才执行的,此时malloc_f还是NULL,递归调用时访问空指针直接触发段错误。 - 初始化时机不对:程序启动阶段,某些库函数可能会提前调用
malloc,这时候malloc_f还没被赋值,同样会触发空指针访问。
修复方案
方案1:用系统调用替代标准库IO,提前初始化函数指针
直接用open、write这类系统调用写日志,它们不会依赖C标准库的malloc,同时用__attribute__((constructor))让函数指针初始化在main之前完成,彻底避免空指针问题:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #define __USE_GNU #include <dlfcn.h> #define TEST_MEM_LEAK 1 #if TEST_MEM_LEAK typedef void *(*malloc_t)(size_t size); malloc_t malloc_f = NULL; typedef void (*free_t)(void *p); free_t free_f = NULL; int malloc_flag = 1; int free_flag = 1; const char* logFileName = "/home/hammamiw/Documents/HeapMonitor/allocs.log"; int log_fd = -1; // 用系统调用打开日志,不依赖malloc void initLog() { if (log_fd == -1) { log_fd = open(logFileName, O_WRONLY | O_CREAT | O_TRUNC, 0644); } } void *malloc(size_t size) { if(malloc_flag) { initLog(); malloc_flag = 0; void *p = malloc_f(size); // 手动格式化字符串,用write输出 char buf[64]; int len = snprintf(buf, sizeof(buf), "malloc, %lx, %lu\n", (uintptr_t)p, size); write(log_fd, buf, len); malloc_flag = 1; return p; } else { return malloc_f(size); } } void free(void *p) { initLog(); if(free_flag) { free_flag = 0; char buf[64]; int len = snprintf(buf, sizeof(buf), "F, %lx\n", (uintptr_t)p); write(log_fd, buf, len); free_f(p); free_flag = 1; } else { free_f(p); } } // 在main执行前就获取系统malloc/free指针 __attribute__((constructor)) void initMallocFree() { malloc_f = dlsym(RTLD_NEXT, "malloc"); if(!malloc_f) { char err[128]; snprintf(err, sizeof(err), "load malloc failed: %s\n", dlerror()); write(STDERR_FILENO, err, strlen(err)); _exit(1); } free_f = dlsym(RTLD_NEXT, "free"); if(!free_f) { char err[128]; snprintf(err, sizeof(err), "load free failed: %s\n", dlerror()); write(STDERR_FILENO, err, strlen(err)); _exit(1); } } #endif int main() { void *p1 = malloc(10); void *p2 = malloc(20); free(p2); free(p1); return 0; }
编译命令
编译时必须链接dl库,否则dlsym无法正常工作:
gcc -o mainapp main.c -Wall -Wextra -ldl
方案2:保留fprintf但严格控制递归和初始化时机
如果一定要用fprintf,需要确保fopen调用时不会触发自己的malloc,并且用pthread_once保证日志只初始化一次,同时在文件操作时暂时关闭递归保护:
不过方案1更可靠,因为系统调用完全绕过了C标准库的内存分配,不会有递归问题。
内容的提问来源于stack exchange,提问作者Wassim Hammami
相关产品推荐
相关产品推荐

