乐于分享
好东西不私藏

C 语言零修改源码!自动给所有函数加进入 / 退出日志,支持动态库、线程安全、毫秒级耗时统计

C 语言零修改源码!自动给所有函数加进入 / 退出日志,支持动态库、线程安全、毫秒级耗时统计

接手老旧C语言项目不敢改源码?动态库无源码想排查调用链路?手动埋点工作量大、侵入性强还容易漏?多线程日志混乱、耗时统计不准?
今天带来两套零侵入日志方案,完全不改动业务源码,自动打印函数名、执行耗时、线程信息,支持可执行程序、动态库,甚至main函数之前的初始化逻辑也能监控,线程安全无冲突,调试、排查、性能统计一站式搞定!

一、方案总览

本文提供两种无侵入实现方式,适配不同场景,按需选择即可:
  • GCC编译器插桩:终极无侵入,全自动覆盖所有函数,支持动态库、main前函数
  • 宏包装函数:轻量跨平台,无需特殊编译参数,适合小规模业务函数监控

二、方案一:GCC编译器插桩(生产推荐)

2.1核心原理

借助GCC内置编译选项 -finstrument-functions,编译器会在每个函数入口、出口自动插入固定钩子调用,无需修改源码,即可接管所有函数执行流程,属于真正的无侵入AOP编程。

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核心原理

利用C语言宏替换特性,重定义函数名,在不修改业务函数、不改动调用处的前提下,自动包裹日志逻辑,跨编译器兼容(GCC/Clang/MSVC),无特殊编译依赖。

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性能影响

    两种方案均为轻量实现,耗时统计仅做时间戳差值计算,无复杂逻辑:
      宏包装:性能损耗可忽略,高频调用无压力编译器插桩:仅新增函数跳转、时间获取、打印操作,普通业务场景无感知,可过滤高频函数优化

      六、总结要点

      C语言无侵入日志监控,核心是不破坏业务代码、快速定位问题,两套方案覆盖绝大多数场景:

      GCC插桩:追求全自动化、监控动态库、排查main前初始化问题,生产环境首选,真正做到零修改、全覆盖。

      选宏包装:需要跨平台、仅监控部分业务函数、无编译参数限制,轻量调试首选。

      这两种技巧不仅适用于日志打印,还可拓展到性能统计、调用链追踪、异常监控等场景,是C语言后端、嵌入式、底层开发必备的硬核技能,大幅提升调试效率和问题定位速度。