首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

Linux C 程序崩潰自查利器:backtrace 實戰指南

     編輯:關於C語言

為什麼需要 backtrace

程序異常退出(Segmentation Fault、Abort、FPE 等)時,核心轉儲雖完整但分析成本高。在信號處理函數中調用 backtrace() 系列函數,可即時輸出當前調用棧,快速定位崩潰現場,是生產環境無 Core 文件權限、嵌入式設備存儲受限場景的首選方案。

核心 API

#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);
}

架構相關:手寫 backtrace(ARM 示例)

若平台不支援 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 或終止)
}

符號解析增強:dladdr + cxa_demangle

#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。將其集成至項目啟動初始化,配合自動化日誌收集分析系統,可將線上崩潰定位時間從小時級壓縮至分鐘級。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved