Linux下malloc/free调用栈无法直接获取,因glibc不记录调用位置;需通过LD_PRELOAD劫持函数并用__builtin_return_address(0)或backtrace()采集栈帧,配合延迟符号解析实现轻量级内存分配追踪。

为什么 malloc/free 的调用栈无法直接拿到
在 Linux 下,malloc 和 free 本身不记录调用位置,除非你拦截它们并手动采集栈帧。glibc 提供了 __malloc_hook / __free_hook,但这些接口在较新版本(2.34+)已被移除,且多线程下不安全。所以不能依赖 glibc 内部钩子,得自己接管内存分配入口。
实际做法是:用宏或链接时替换(LD_PRELOAD)劫持 malloc、calloc、realloc、free,并在每次调用中用 backtrace() 捕获栈地址,再用 backtrace_symbols() 或 addr2line 解析为文件行号。
- 必须在程序启动早期就安装钩子,否则初始化阶段的分配会漏掉
-
backtrace()在 signal handler 中调用可能不安全,避免在free钩子里做符号解析,只存 raw 地址 - 多线程下需用
pthread_mutex_t保护全局记录表,但锁本身会拖慢分配路径 —— 轻量级检测器应默认单线程,或用 per-thread buffer 减少争用
如何用 LD_PRELOAD 替换标准库内存函数
写一个 malloc_interceptor.cpp,定义自己的 malloc、free 等函数,并加 __attribute__((visibility("default"))) 导出。编译成共享库后,运行时用 LD_PRELOAD=./libintercept.so ./your_program 加载。
关键点不是“重写 malloc”,而是“转发 + 记录”。例如:
立即学习“C++免费学习笔记(深入)”;
void* malloc(size_t size) {
static void* (*real_malloc)(size_t) = nullptr;
if (!real_malloc) real_malloc = (void*(*)(size_t))dlsym(RTLD_NEXT, "malloc");
void* ptr = real_malloc(size);
if (ptr) record_allocation(ptr, size, __builtin_return_address(0));
return ptr;
}-
dlsym(RTLD_NEXT, "malloc")是获取真实 libc 实现的标准方式,不能硬编码调用libc符号 -
__builtin_return_address(0)比backtrace()更快,只取当前调用点,适合轻量场景;若需完整栈,才用backtrace()+ 缓存 - 不要在钩子函数里调用
printf或std::string—— 可能触发递归 malloc,导致死锁或崩溃
检测器退出时如何输出未释放的内存块
注册 atexit() 回调,在程序正常退出前遍历所有未匹配的 malloc 记录,打印地址、大小、分配时的源码位置(如果已解析)。注意:异常退出(如 abort()、信号终止)不会触发 atexit,所以也得捕获 SIGSEGV、SIGABRT 等信号做兜底输出。
- 记录结构建议用
std::unordered_map<void allocationinfo></void>,key 是分配地址,value 存 size 和原始栈地址数组 - 符号解析尽量延迟到报告阶段,避免污染分配路径;可用
addr2line -e your_binary -fC -p批量处理地址 - 如果程序 fork 了子进程,LD_PRELOAD 会继承,但子进程 exit 不该重复报告父进程的 leak —— 可通过
getpid()过滤,只在主进程 report
为什么 new/delete 也得单独处理
new 和 delete 默认调用 malloc/free,但如果你用了自定义 operator new(比如全局重载或类内重载),或者链接了 -fno-builtin-new,那它们就绕过你的 malloc 钩子。必须显式拦截 operator new、operator delete 及其数组版本。
- 声明时加
[[gnu::visibility("default")]]并确保链接时不被优化掉(加-fno-omit-frame-pointer) -
operator new可能被内联,尤其小对象;编译时加-fno-builtin强制走函数调用路径 - 注意 placement new 不分配内存,无需记录;但要识别并跳过,否则误报
真正麻烦的不是技术实现,而是符号解析的可靠性 —— strip 过的二进制、inlined 函数、优化后的栈帧都可能导致地址对不上行号。轻量级检测器能告诉你“哪次 malloc 没 free”,但不一定能精确到 foo.cpp:42,得接受这个边界。


















