GCC 的 -finstrument-functions 通过自动在函数入口/出口插入 __cyg_profile_func_enter/exit 回调实现轻量级插桩,需用 extern "C" 定义二者,禁用 inline 和异常,配合 -O0/-O1 与 -fno-omit-frame-pointer;函数名用 dladdr()(注意多线程安全)、调用栈可用 __builtin_frame_address(0) 存入线程局部缓冲区;所有全局状态须无锁(如 std::atomic),避免 malloc/printf 等非异步信号安全调用;数据导出推荐 mmap 环形缓冲区+二进制格式(含时间戳、类型、地址),离线解析以生成火焰图。

如何用 GCC 的 -finstrument-functions 自动插桩
GCC 提供的 -finstrument-functions 是最轻量、侵入性最小的函数级插桩方式,不需要改源码,编译时自动在每个函数入口和出口调用你定义的两个回调函数。
它不适用于 inline 函数、构造/析构函数(除非加 __attribute__((noinline)))、或被优化掉的函数(建议用 -O0 或 -O1 配合 -fno-omit-frame-pointer)。
- 必须定义且仅定义两个 C 风格函数:
__cyg_profile_func_enter和__cyg_profile_func_exit,签名严格为void __cyg_profile_func_enter(void *this_fn, void *call_site)和同签名的 exit 版本 - 这两个函数不能是 inline、不能是 C++ 成员函数、不能抛异常;建议用
extern "C"包裹 -
this_fn是当前函数地址,可用dladdr()查符号名,但要注意:多线程下dladdr不可重入,需配合std::mutex或用dladdr1+RTLD_DL_SYMENT - 记录开销大,建议只对关键模块启用,或用原子计数器 + 环形缓冲区避免锁
怎么拿到函数名和调用栈(不用第三方库)
仅靠 dladdr() 只能解析当前函数符号,无法还原调用栈。若要轻量级栈追踪,可结合 backtrace() + backtrace_symbols(),但后者分配堆内存且不可重入。
更稳妥的做法是:在 __cyg_profile_func_enter 中用 __builtin_frame_address(0) 获取当前帧地址,存入线程局部缓冲区;exit 时弹出。这样只做指针操作,无内存分配。
- 用
thread_local std::vector存储帧地址(注意:vector 的 push/pop 在信号安全上下文中仍不安全,生产环境建议预分配固定大小数组)call_stack; - 符号解析应延迟到采样结束后再批量调用
dladdr,避免在 hot path 上阻塞 - 若需精确行号,编译时必须加
-g,且dladdr对内联函数返回的是外层函数地址
如何避免插桩导致程序行为改变(尤其是多线程和信号)
插桩代码一旦进入信号处理上下文(如 SIGPROF 触发的采样),或与主线程竞争同一资源,极易引发死锁或崩溃。这是自研 Profiler 最常翻车的地方。
- 所有全局状态(如计数器、缓冲区)必须用无锁结构:优先选
std::atomic,避免std::mutex;环形缓冲区可用std::atomic管理读写索引 - 禁止在插桩函数中调用 malloc/new、printf、std::cout、std::string 构造等可能触发锁或信号的函数
- 如果程序本身用
setitimer()做周期采样,确保插桩逻辑不会被该 timer 中断——可临时屏蔽信号(sighold(SIGPROF)),但需极小心 - 构造函数/析构函数默认不被插桩,若需覆盖,显式添加
__attribute__((noinline, used))
怎样导出和解析插桩数据(文本 vs 二进制)
直接打印到 stdout/stderr 效率低、易乱序、难解析。推荐写入内存映射文件(mmap())或预分配的环形 buffer,由独立线程定期 dump。
二进制格式比文本节省 3–5 倍空间,也规避了字符串编码问题。一个最小可行格式只需包含:uint64_t timestamp、uint8_t type(enter=0/exit=1)、uintptr_t func_addr、uintptr_t parent_addr(可选)。
- 时间戳用
clock_gettime(CLOCK_MONOTONIC, &ts),避免gettimeofday()的系统调用开销和时钟跳变 - dump 线程不应直接解析符号,而是把 raw buffer 写入文件,后续用离线工具(如 Python +
pyelftools)解析 - 若需火焰图,输出需转成
stackcollapse-perf.pl兼容格式:每行一个调用栈,函数名用;分隔,末尾跟采样次数
真正难的不是插桩,而是让插桩逻辑在各种优化等级、各种调用场景(虚函数、模板实例化、异常栈展开)下保持稳定——这些细节往往要靠大量 objdump -d 和 gdb 单步验证。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











