程序異常退出(Segmentation Fault、Abort、FPE 等)時,核心轉儲雖完整但分析成本高。在信號處理函數中調用 backtrace() 系列函數,可即時輸出當前調用棧,快速定位崩潰現場,是生產環境無 Core 文件權限、嵌入式設備存儲受限場景的首選方案。
#include <execinfo.h>
int backtrace(void **buffer, int size); // 獲取調用棧地址
char **backtrace_symbols(void *const *buffer, int size); // 解析為符號字串
void backtrace_symbols_fd(void *const *buffer, int size, int fd); // 直接寫文件描述符
buffer:存放返回地址的數組size:最大幀數gcc -g -rdynamic -o program program.c
-g:生成調試符號-rdynamic:將所有符號加入動態符號表(.dynsym),使 backtrace_symbols 能解析函數名。靜態鏈接時需 -Wl,--export-dynamic#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#include <unistd.h>
static void sig_handler(int sig, siginfo_t *info, void *ucontext) {
void *array[20];
size_t size = backtrace(array, 20);
char **strings = backtrace_symbols(array, size);
fprintf(stderr, "Signal %d received, stack trace:\n", sig);
for (size_t i = 0; i < size; i++)
fprintf(stderr, "%s\n", strings[i]);
free(strings);
_exit(1); // 避免重入信號處理器
}
int main() {
struct sigaction sa = {0};
sa.sa_sigaction = sig_handler;
sa.sa_flags = SA_SIGINFO | SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
// 觸發崩潰
char *p = NULL;
*p = 0;
}
Signal 11 received, stack trace:
./program(main+0x1a) [0x4005aa]
./program(__libc_start_main+0xf5) [0x7f8b3c5a7b45]
/lib/x86_64-linux-gnu/libc.so.6(_start+0x2a) [0x7f8b3c5a7b45]
配合 addr2line -e program 0x4005aa 可精確定位到源碼行號。
backtrace 返回的是調用者返回地址,非故障指令地址。結合 ucontext_t 可獲取精確 RIP/EIP:
static void sig_handler(int sig, siginfo_t *info, void *ucontext) {
ucontext_t *uc = (ucontext_t*)ucontext;
void *fault_ip = 0;
#if defined(__x86_64__)
fault_ip = (void*)uc->uc_mcontext.gregs[REG_RIP];
#elif defined(__i386__)
fault_ip = (void*)uc->uc_mcontext.gregs[REG_EIP];
#elif defined(__aarch64__)
fault_ip = (void*)uc->uc_mcontext.pc;
#endif
void *array[20];
size_t size = backtrace(array, 20);
array[1] = fault_ip; // 替換第 2 幀(信號處理器返回地址)為故障地址
backtrace_symbols_fd(array, size, STDERR_FILENO);
_exit(1);
}
若平台不支援 execinfo.h,需手動解析幀指針鏈:
// ARM (EABI) 幀結構:[fp, lr, ...] fp 指向上一幀 fp
int backtrace_arm(void **buffer, int size) {
register void *fp __asm__("fp");
void *lr = __builtin_return_address(0);
int n = 0;
buffer[n++] = lr; // 當前函數返回地址
while (fp && n < size) {
void **frame = (void**)fp;
fp = frame[0]; // 上一幀 fp
lr = frame[1]; // 上一幀 lr (返回地址)
buffer[n++] = lr;
}
return n;
}
x86/x64 類似,通過 RBP/EBP 鏈遍歷。注意幀指針優化(-fomit-frame-pointer)會破壞鏈完整性,調試構建建議 -fno-omit-frame-pointer。
void setup_crash_logger() {
struct sigaction sa = {0};
sa.sa_sigaction = crash_handler;
sa.sa_flags = SA_SIGINFO | SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
sigaction(SIGSYS, &sa, NULL);
}
void crash_handler(int sig, siginfo_t *info, void *uctx) {
if (atomic_exchange(&crashing, 1)) return; // 防重入
char filename[256];
time_t t = time(NULL);
strftime(filename, sizeof(filename), "crash_%Y%m%d_%H%M%S.log", localtime(&t));
int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0644);
if (fd < 0) { _exit(1); }
void *bt[32];
int n = backtrace(bt, 32);
// 替換故障指令指針
ucontext_t *uc = (ucontext_t*)uctx;
#if defined(__x86_64__)
bt[1] = (void*)uc->uc_mcontext.gregs[REG_RIP];
#elif defined(__i386__)
bt[1] = (void*)uc->uc_mcontext.gregs[REG_EIP];
#endif
backtrace_symbols_fd(bt, n, fd);
dprintf(fd, "Signal: %d, Fault addr: %p\n", sig, info->si_addr);
close(fd);
// 可選:睡眠便於附加調試器
sleep(5);
signal(sig, SIG_DFL);
raise(sig); // 觸發默認處理(生成 core 或終止)
}
#include <dlfcn.h>
#include <cxxabi.h>
void print_enhanced_backtrace(void **buffer, int size) {
for (int i = 0; i < size; i++) {
Dl_info info;
if (dladdr(buffer[i], &info)) {
const char *sym = info.dli_sname;
char *demangled = NULL;
int status = 0;
if (sym) demangled = abi::__cxa_demangle(sym, NULL, NULL, &status);
printf("#%-2d %p in %s (%s)\n", i, buffer[i],
(status == 0 && demangled) ? demangled : (sym ? sym : "??"),
info.dli_fname ? info.dli_fname : "??");
free(demangled);
} else {
printf("#%-2d %p\n", i, buffer[i]);
}
}
}
| 方面 | 說明 |
|---|---|
| 性能開銷 | 僅在崩潰時觸發,正常運行零開銷 |
| 最大深度 | 受 buffer 大小限制,通常 32-64 足夠 |
| 內聯函數 | 不會出現在棧跡中 |
| 尾調用優化 | 可能導致幀丟失 |
| 異步信號安全 | backtrace 非 異步信號安全,但崩潰處理器中通常可接受風險 |
| C++ 異常 | 不捕獲 C++ 異常堆疊,需配合 __cxa_throw 鉤子 |
backtrace 系列函數是 Linux C/C++ 開發者「零成本」獲取崩潰現場的利器。關鍵在於:編啟用 -rdynamic、信號處理器中替換故障 IP、落盤日誌含時間戳、符號解析配合 dladdr/cxa_demangle。將其集成至項目啟動初始化,配合自動化日誌收集分析系統,可將線上崩潰定位時間從小時級壓縮至分鐘級。