C 语言零修改源码!自动给所有函数加进入 / 退出日志,支持动态库、线程安全、毫秒级耗时统计
一、方案总览
-
GCC编译器插桩:终极无侵入,全自动覆盖所有函数,支持动态库、main前函数 -
宏包装函数:轻量跨平台,无需特殊编译参数,适合小规模业务函数监控
二、方案一:GCC编译器插桩(生产推荐)
2.1核心原理
2.2支持范围
-
✅ 可执行程序全函数监控 -
✅ 动态库(.so)无源码监控 -
✅ main函数之前的构造/初始化函数 -
✅ 多线程安全,日志不混乱、无竞争 -
✅ 零源码修改,不侵入业务逻辑
2.3完整代码(线程安全+耗时+函数名)
|
#define _GNU_SOURCE#include <stdio.h>#include <sys/time.h>#include <dlfcn.h>#include <pthread.h>#include <unistd.h>#include <time.h>#include <string.h>// 线程局部存储,保证多线程安全,避免全局变量冲突static __thread int call_depth = 0;static __thread long long tv_start;// 获取微秒级时间戳,用于耗时统计static long long now_us(void) {struct timeval tv;gettimeofday(&tv, NULL);return (long long)tv.tv_sec * 1000000 + tv.tv_usec;}// 函数地址转函数名,依赖rdynamic导出符号表static const char* get_func_name(void *addr) {Dl_info info;if (dladdr(addr, &info) && info.dli_sname)return info.dli_sname;return “unknown_func”;}// 格式化时间戳,日志可读性更强static void get_timestamp(char *buf, size_t len) {struct timeval tv;gettimeofday(&tv, NULL);struct tm *tm = localtime(&tv.tv_sec);snprintf(buf, len, “%04d-%02d-%02d %02d:%02d:%02d.%03ld”,tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,tm->tm_hour, tm->tm_min, tm->tm_sec, tv.tv_usec / 1000);}// 函数进入钩子:编译器自动调用void __cyg_profile_func_enter(void *func, void *caller) {(void)caller;char ts[64];get_timestamp(ts, sizeof(ts));tv_start = now_us();// 调用栈层级缩进,清晰展示调用关系for (int i = 0; i < call_depth; i++)printf(“| “);// 打印:时间戳+线程ID+进入函数名printf(“[%s][TID:%lu] [ENTER] %s\n”,ts, (unsigned long)pthread_self(), get_func_name(func));call_depth++;}// 函数退出钩子:编译器自动调用void __cyg_profile_func_exit(void *func, void *caller) {(void)caller;call_depth–;long long cost = now_us() – tv_start;char ts[64];get_timestamp(ts, sizeof(ts));for (int i = 0; i < call_depth; i++)printf(“| “);// 打印:时间戳+线程ID+退出函数名+执行耗时printf(“[%s][TID:%lu] [ LEAVE] %s 耗时:%lld us\n”,ts, (unsigned long)pthread_self(), get_func_name(func), cost);}// ==================== 业务代码(完全无需修改)====================int add(int a, int b) {return a + b;}int mul(int a, int b) {return a * b;}int main() {add(10, 20);mul(5, 6);return 0;} |
2.4编译命令(必加参数)
|
-finstrument-functions:开启插桩# -rdynamic:导出函数名,让dladdr解析# -ldl:链接动态链接库,支持dladdr# -lpthread:支持多线程gcc -finstrument-functions -rdynamic test.c -o func_log -ldl -lpthread |
2.5运行效果
|
[2026-03-20 22:40:15.123][TID:12345] [ENTER] main[2026-03-20 22:40:15.123][TID:12345] | [ENTER] add[2026-03-20 22:40:15.123][TID:12345] | [ LEAVE] add 耗时:2 us[2026-03-20 22:40:15.123][TID:12345] | [ENTER] mul[2026-03-20 22:40:15.123][TID:12345] | [ LEAVE] mul 耗时:1 us[2026-03-20 22:40:15.123][TID:12345] [ LEAVE] main 耗时:8 us |
三、方案二:宏包装函数(轻量跨平台)
3.1核心原理
3.2完整代码
|
#include <stdio.h>#include <sys/time.h>// 获取微秒级时间#define GET_TIME_US() ({ \struct timeval tv; \gettimeofday(&tv, NULL); \(long long)tv.tv_sec * 1000000 + tv.tv_usec; \})// 通用函数包装宏:返回值+函数名+参数列表#define WRAP_FUNC(ret, func_name, …) \ret __##func_name(__VA_ARGS__); \ret func_name(__VA_ARGS__) { \long long start = GET_TIME_US(); \printf(“[ENTER] %s\n”, #func_name); \ret res = __##func_name(__VA_ARGS__); \long long cost = GET_TIME_US() – start; \printf(“[ LEAVE] %s 耗时:%lld us\n”, #func_name, cost); \return res; \} \ret __##func_name(__VA_ARGS__)// 包装业务函数(只需新增这一行,无需改原函数)WRAP_FUNC(int, add, int a, int b);WRAP_FUNC(int, mul, int a, int b);// ==================== 业务代码(原样不动)====================int add(int a, int b) {return a + b;}int mul(int a, int b) {return a * b;}int main() {add(10, 20);mul(5, 6);return 0;} |
3.3编译命令
|
无需特殊参数,直接编译gcc test.c -o func_log_wrap |
四、两种方案优劣对比
|
对比维度 |
GCC编译器插桩 |
宏包装函数 |
|
源码侵入性 |
✅ 零侵入,完全不改代码 |
✅ 零侵入,仅加宏定义 |
|
覆盖范围 |
✅ 全自动覆盖所有函数 |
❌ 需逐个函数包装 |
|
动态库支持 |
✅ 支持,无源码也可监控 |
❌ 不支持 |
|
main前函数 |
✅ 支持 |
❌ 不支持 |
|
跨平台性 |
❌ GCC专属,依赖编译器 |
✅ 全编译器、全平台通用 |
|
线程安全 |
✅ 线程局部变量保障 |
✅ 栈变量天然安全 |
|
性能开销 |
低,业务场景<1%损耗 |
极低,几乎无损耗 |
|
适用场景 |
大规模项目、动态库、全链路监控 |
小规模业务、跨平台、轻量调试 |
五、性能与线程安全说明
5.1线程安全保障
插桩方案:采用__thread线程局部存储,每个线程独立维护调用栈、计时变量,无锁不冲突宏包装方案:耗时统计基于栈变量,线程间互不干扰,日志输出稳定有序
5.2性能影响
宏包装:性能损耗可忽略,高频调用无压力编译器插桩:仅新增函数跳转、时间获取、打印操作,普通业务场景无感知,可过滤高频函数优化
六、总结要点
|
选GCC插桩:追求全自动化、监控动态库、排查main前初始化问题,生产环境首选,真正做到零修改、全覆盖。 选宏包装:需要跨平台、仅监控部分业务函数、无编译参数限制,轻量调试首选。 |
夜雨聆风