malloc_hook在现代glibc中不可用,因2.34+已移除__malloc_hook变量,且2.33下ld_preload易触发double free;推荐用ld_preload拦截malloc/free并结合backtrace_symbols_fd采样。

为什么 malloc_hook 在现代 glibc 上基本不可用
因为从 glibc 2.34 开始,__malloc_hook 等全局 hook 变量被彻底移除,不是 deprecated,是直接删了。你写代码去赋值 __malloc_hook,链接会报 undefined symbol;即使降级到 2.33,启用 LD_PRELOAD 后也极大概率触发 double free or corruption —— 因为 hook 函数本身可能再次调用 malloc(比如格式化栈帧),形成递归分配。
所以别试 __malloc_hook + backtrace 的老方案,它现在既不安全也不可靠。
用 LD_PRELOAD 替换 malloc/free 更可行
这是目前最稳定、兼容性最好的用户态采样方式:把标准库的内存函数用自定义版本拦截,在入口记录调用栈和大小,再转发给真正的 malloc(通过 dlsym(RTLD_NEXT, "malloc") 获取)。
实操要点:
立即学习“C++免费学习笔记(深入)”;
- 必须在共享库中实现,并用
extern "C"导出符号,避免 C++ name mangling - 所有 hook 函数里禁止调用任何可能间接 malloc 的东西:
std::string、std::cout、printf(它可能 malloc 缓冲区)、甚至backtrace_symbols(它 malloc) - 栈采集用
backtrace+backtrace_symbols_fd最稳妥,后者不 malloc,直接写 fd - 采样频率要控制,比如只对 >1KB 的分配记录,否则小对象爆炸式打点会拖垮程序
示例片段(关键逻辑):
extern "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");
if (size > 1024) {
void* bt[64];
int nptrs = backtrace(bt, 64);
backtrace_symbols_fd(bt, nptrs, STDERR_FILENO); // 或写入 mmap'd buffer
}
return real_malloc(size);
}
}如何把栈帧数据喂给 flamegraph.pl
火焰图工具不认原始 backtrace 输出,需要转成它要求的折叠格式(folded stack trace),每行形如 a;b;c;d;main 123(函数名分号分隔,末尾是样本数)。
常见坑:
-
backtrace_symbols返回的字符串含地址(如./a.out(+0x1234)),flamegraph.pl默认忽略带括号的,得用--color或预处理清洗 - 不同编译器生成符号风格不同:GCC 带 offset,Clang 可能带 `
`,建议用 addr2line -e ./binary -f -C -i做后处理 - 别实时 pipe 给
flamegraph.pl—— 高频分配下 I/O 成瓶颈,先存文本,采样结束再批量转换
真正难的是线程安全与性能干扰
所有 hook 函数都运行在应用线程上下文中,而 backtrace 和文件写入都不是轻量操作。多线程下若共用一个 buffer 或 fd,必须加锁,但锁本身又引入竞争和延迟,导致采样失真。
更现实的做法:
- 每个线程用
thread_local缓冲区暂存栈帧(固定大小 ring buffer),满后再批量刷出 - 避免锁:用无锁队列把栈帧指针发给单独的 writer 线程(需原子操作或 hazard pointer)
- 采样开关做成运行时可调(比如通过
atomic<bool></bool>控制),方便线上灰度 - 注意:
backtrace在某些优化级别(-O2以上)可能无法正确展开内联函数或 tail-call,建议编译时加-fno-omit-frame-pointer
越想准确实时看内存热点,越得接受它本身会轻微改变内存行为——这是绕不开的观测代价。










