C++如何实现简易的内存分配火焰图采样?(malloc hook记录)

6次阅读

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

C++如何实现简易的内存分配火焰图采样?(malloc hook记录)

为什么 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::Stringstd::coutprintf(它可能 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

越想准确实时看内存热点,越得接受它本身会轻微改变内存行为——这是绕不开的观测代价。

text=ZqhQzanResources