Linux C 函数调用栈获取三种方案总结
本文总结了三种在 Linux 上用 C 获取并打印调用栈的方案:backtrace、addr2line 源码行解析、libunwind 展开。包括原理、调用流程、编译要点与最小示例代码。结合本项目文件与开关说明 src/stacktrace.c、examples/examples.c、Makefile。
三种方案
backtrace/backtrace_symbols
原理
-
execinfo.h的backtrace()采集返回地址数组,backtrace_symbols()将地址转为符号字符串。 -
简单易用、零额外依赖;但在高优化或省略帧指针时不够稳健,且不直接给
“文件:行”。
调用流程
backtrace(void **buffer, int size)抓取返回地址。backtrace_symbols(void *const *buffer, int size)获取符号字符串。- 打印并
free(symbols)。
编译要点
- 建议:
-g -O0 -fno-omit-frame-pointer -rdynamic
最小示例
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#define N 64
void print_bt(void) {
void *buf[N];
int n = backtrace(buf, N);
if (n <= 0) return;
char **syms = backtrace_symbols(buf, n);
if (!syms) return;
for (int i = 0; i < n; ++i) {
fprintf(stderr, "[%02d] %s\n", i, syms[i]);
}
free(syms);
}
addr2line
原理
- ELF 中的 DWARF 调试信息(由
-g生成)记录“地址 <-> 文件:行”。 addr2line -e <elf> 0x<offset>将地址转为“函数 + 文件:行”。- 由于 PIE/共享库 + ASLR,运行时绝对地址需换算为“相对模块基址的偏移”。
dladdr(addr, &info)得到模块路径info.dli_fname与基址info.dli_fbase。off = (uintptr_t)addr - (uintptr_t)info.dli_fbase。
调用流程
- 先通过
backtrace()或libunwind得到每帧 IP。 dladdr()查模块路径与基址。- 计算偏移并
addr2line -e <obj> 0x<off>。
编译要点
- 必须:
-g - 建议:
-rdynamic - 链接:
-ldl
最小示例(单帧)
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
static void addr2line_one(void *addr) {
Dl_info info; const char *path = NULL; uintptr_t base = 0;
if (dladdr(addr, &info) && info.dli_fname && info.dli_fbase) {
path = info.dli_fname; base = (uintptr_t)info.dli_fbase;
} else {
path = "/proc/self/exe"; base = 0;
}
uintptr_t off = (uintptr_t)addr - base;
char cmd[1024];
snprintf(cmd, sizeof(cmd), "addr2line -e '%s' -C -f -p 0x%lx 2>/dev/null",
path, (unsigned long)off);
FILE *fp = popen(cmd, "r"); if (!fp) return;
char line[1024]; if (fgets(line, sizeof(line), fp)) fputs(line, stderr);
pclose(fp);
}
libunwind
原理
- 使用 CFI(
.eh_frame等)与寄存器上下文逐帧展开;相对backtrace()更稳健,适合优化构建或省略帧指针时。 - 得到 IP/函数名/偏移后,可叠加
addr2line打印源码行。
调用流程
unw_getcontext(&ctx)获取当前上下文。unw_init_local(&cursor, &ctx)初始化游标。- 循环
unw_step(&cursor)上溯每一帧。 - 每帧用
unw_get_reg(...UNW_REG_IP...)获取 IP,unw_get_proc_name()获取函数名。 - 如需源码行,配合
addr2line。
编译要点
- 依赖:安装
libunwind(Debian/Ubuntu:sudo apt-get install -y libunwind-dev) - 链接:
-lunwind -lunwind-x86_64(不同架构名称可能不同)
最小示例(核心循环)
#include <libunwind.h>
#include <stdio.h>
void unwind_demo(void) {
unw_cursor_t cur; unw_context_t ctx;
if (unw_getcontext(&ctx) != 0) return;
if (unw_init_local(&cur, &ctx) != 0) return;
for (int i = 0; i < 64 && unw_step(&cur) > 0; ++i) {
unw_word_t ip = 0, off = 0; char name[256];
if (unw_get_reg(&cur, UNW_REG_IP, &ip) != 0) break;
if (unw_get_proc_name(&cur, name, sizeof(name), &off) == 0) {
fprintf(stderr, "[%02d] %s+0x%lx (%p)\n",
i, name, (unsigned long)off, (void*)(uintptr_t)ip);
} else {
fprintf(stderr, "[%02d] %p\n", i, (void*)(uintptr_t)ip);
}
}
}
信号崩溃打印
原理:捕捉 SIGSEGV、SIGABRT,在信号处理器里打印调用栈。
注意:信号处理器中应尽量使用异步信号安全的 API;为简洁可以使用 fprintf,适合开发调试。
如何选择
快速集成/开发调试:execinfo + addr2line(推荐编译:-g -O0 -fno-omit-frame-pointer -rdynamic)。
优化构建/无帧指针:启用 libunwind 展开,必要时叠加 addr2line。
最少依赖:仅 execinfo,但信息粗糙(无具体行号)。
常见问题
- 输出
??:0:可能没用-g、未做 PIE/DSO 偏移换算、或库本身无调试信息。 - 高优化或省略帧指针:栈可能不完整;开发期建议
-O0 -fno-omit-frame-pointer。 - musl/Alpine:
execinfo.h可能需要libexecinfo且链接-lexecinfo。