在嵌入式Linux開發中,特別是復雜軟件,多人協作開發時,當某人無意間寫了一個代碼bug導致程序崩潰,但又不知道崩潰的具體位置時,單純靠走讀代碼,很難快速的定位問題。
本篇就來介紹一種方法,使用backtrace工具,來輔助定位程序崩潰的位置信息。
backtrace是 C/C++ 中用于獲取程序調用棧信息的函數,借助backtrace可以排查崩潰并定位代碼行號。
1 backtrace分析程序崩潰的原理
在linux系統中,運行程序若發生崩潰,會產生相應的信號,例如訪問空指針會觸發SIGSEGV(signum:11)。
這時可以使用signal函數來捕獲這個信息,捕獲信號后,支持自定義的handler函數進行一些處理。
在自定義的handler函數中,可以使用backtrace函數,來打印程序調用棧信息。
最后使用addr2line函數,將地址轉換為可讀的函數名和行號。
使用backtrace分析程序崩潰,需要在編譯時使用 -g
選項生成的調試信息。
使用addr2line工具,將地址轉換為可讀的函數名和行號,實例如下:
addr2line -e 程序名 -f -C 0x400526
# 輸出:
main
/path/to/main.c:42
2 一些要用到的函數
2.1 signal
2.1.1 函數原型
在 C 和 C++ 中,signal
函數用于設置信號處理方式。
其原型定義在 <signal.h>
頭文件中:
typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
參數說明:
- int signum:信號編號(整數),如:
SIGINT
(2):中斷信號(Ctrl+C)SIGSEGV
(11):段錯誤SIGILL
(4):非法指令SIGTERM
(15):終止信號SIGFPE
(8):浮點異常
- sighandler_t handler:信號處理函數指針,有三種取值:
- 用戶定義函數:
void handler(int signum)
類型的函數 SIG_DFL
:默認處理(如終止程序)SIG_IGN
:忽略該信號
- 用戶定義函數:
返回值:
- 成功:返回之前的信號處理函數指針
- 失敗:返回
SIG_ERR
,并設置errno
(如EINVAL
表示無效信號)
2.1.2 常見信號列表
signum | 信號名稱 | 默認行為 | 觸發場景 |
---|---|---|---|
1 | SIGHUP | 終止程序 | 終端連接斷開(如 SSH 會話結束),或用戶登出時通知進程重新加載配置 |
2 | SIGINT | 終止程序(Ctrl+C) | 用戶在終端按下 Ctrl+C,請求中斷當前進程 |
3 | SIGQUIT | 終止程序并生成 Core 文件 | 用戶按下 Ctrl+\,通常用于強制退出并生成調試用的 Core 文件 |
4 | SIGILL | 終止程序并生成 Core 文件 | 進程執行非法指令(如無效的機器碼),通常由程序編譯錯誤或硬件異常導致 |
5 | SIGTRAP | 終止程序并生成 Core 文件 | 觸發斷點陷阱(如調試器設置的斷點),用于程序調試時的中斷 |
6 | SIGABRT | 終止程序并生成 Core 文件 | 通常是由進程自身調用 C標準函數庫 的 abort() 函數來觸發 |
7 | SIGBUS | 終止程序并生成 Core 文件 | 硬件總線錯誤(如訪問未對齊的內存地址,或內存映射文件錯誤) |
8 | SIGFPE | 終止程序并生成 Core 文件 | 發生算術錯誤(如除零、溢出、精度錯誤),例如1/0 運算 |
9 | SIGKILL | 強制終止程序(不可捕獲) | 系統或用戶發送kill -9 命令,用于強制終止無響應的進程,無法被忽略或處理 |
10 | SIGUSR1 | 終止程序 | 用戶自定義信號 1,可由程序自定義處理邏輯(如日志刷新、狀態通知) |
11 | SIGSEGV | 終止程序并生成 Core 文件 | 訪問無效內存地址(如空指針解引用、越界訪問),是最常見的程序崩潰原因之一 |
12 | SIGUSR2 | 終止程序 | 用戶自定義信號 2,用途與SIGUSR1 類似,供程序開發者自由定義功能 |
13 | SIGPIPE | 終止程序 | 向已關閉的管道或套接字寫入數據(如 TCP 連接斷開后繼續發送數據) |
14 | SIGALRM | 終止程序 | 定時器超時(由alarm() 或setitimer() 函數觸發),用于超時控制 |
15 | SIGTERM | 終止程序(可捕獲) | 系統或用戶發送kill 命令(默認),請求進程正常退出,程序可自定義處理邏輯 |
16 | SIGSTKFLT | 終止程序 | 棧溢出錯誤(僅在某些架構上存在,如 x86),通常與硬件相關的棧異常有關 |
17 | SIGCHLD | 忽略信號 | 子進程狀態改變(如終止或暫停),父進程可通過wait() 系列函數獲取子進程信息 |
18 | SIGCONT | 繼續運行暫停的進程 | 當進程被暫停(如SIGSTOP )后,用于恢復其執行,默認行為為繼續運行 |
19 | SIGSTOP | 暫停進程(不可捕獲) | 系統或用戶發送kill -STOP 命令,用于暫停進程執行,無法被忽略或處理 |
信號分類:
- 不可捕獲信號:無法通過
signal
或sigaction
修改處理方式,只能由系統強制控制。SIGKILL
(9)SIGSTOP
(19)
- 用戶自定義信號:可由程序自由定義處理邏輯,常用于進程間通信或調試。
SIGUSR1
(10)SIGUSR2
(12)
- 異常信號:通常由程序錯誤(如內存操作異常)觸發,默認會生成 Core 文件用于調試。
SIGBUS
(7)SIGSEGV
(11)- …
默認行為的差異:
- 多數信號的默認行為是終止程序,但部分信號(如
SIGCHLD
)默認會被忽略,而SIGCONT
則用于恢復進程運行。
2.2 backtrace
在 C 和 C++ 中,backtrace
函數用于獲取當前程序的調用堆棧信息,常用于調試和錯誤處理。
其原型定義在 <execinfo.h>
頭文件中:
/* 獲取當前調用堆棧中的函數地址 */
int backtrace(void **buffer, int size);
- 參數
- void **buffer:指向存儲函數地址的數組的指針。
- int size:數組的最大元素數(即最多獲取的堆棧幀數)。
- 返回值
- 成功:返回實際獲取的堆棧幀數(不超過
size
)。 - 失敗:返回 0(極罕見,通常僅在內存不足時發生)。
- 成功:返回實際獲取的堆棧幀數(不超過
2.3 backtrace_symbols
/* 將函數地址轉換為可讀的字符串(如函數名、偏移量) */
char **backtrace_symbols(void *const *buffer, int size);
- 參數
- void *const *buffer:backtrace返回的函數地址數組
- int size:backtrace返回的實際幀數
- 返回值
- 成功:返回指向字符串數組的指針,每個元素對應一個堆棧幀(需用
free()
釋放) - 失敗:返回
NULL
,并設置errno
- 成功:返回指向字符串數組的指針,每個元素對應一個堆棧幀(需用
2.4 backtrace_symbols_fd
/* 將函數地址直接輸出到文件 */
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
- 參數
- void *const *buffer:同
backtrace_symbols
- int size:同
backtrace_symbols
- int fd:文件描述符(如
STDERR_FILENO
),用于輸出結果
- void *const *buffer:同
- 返回值:無(直接輸出到文件)
3 實例代碼
3.1 主函數
//g++ -g test.cpp -o test
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <csignal>
#include <string.h>
#include <fcntl.h>
#include <vector>//<---信號處理函數添加到這里void TestFun()
{printf("[%s] in\n", __func__);std::vector<int> a;printf("[%s] a[1]=%d\n", __func__, a[1]);
}int main()
{std::vector<int> vSignalType = {SIGILL, SIGSEGV, SIGABRT}; for (int &signalType : vSignalType){if (SIG_ERR == signal(signalType, SignalHandler)){printf("[%s] signal for signalType:%d err\n", __func__, signalType);}}TestFun();return 0;
}
3.2 信號處理函數
#define MAX_STACK_FRAMES 100void SignalHandler(int signum)
{printf("[%s] signum:%d(%s)\n", __func__, signum, strsignal(signum));signal(signum, SIG_DFL); //恢復默認行為// [backtrace] 獲取當前調用堆棧中的函數地址void *buffer[MAX_STACK_FRAMES];size_t size = backtrace(buffer, MAX_STACK_FRAMES);printf("[%s] backtrace() return %zu address. Stack trace:\n", __func__, size);// [backtrace_symbols] 將函數地址轉換為可讀的字符串char **symbols = (char **) backtrace_symbols(buffer, size);if (symbols == NULL) {printf("[%s] backtrace_symbols() null\n", __func__);return;}for (size_t i = 0; i < size; ++i){printf("#%d %s\n", (int)i, symbols[i]); //打印每一個函數地址}free(symbols);// [backtrace_symbols_fd] 將函數地址直接輸出到文件int fd = open("backtrace.txt", O_CREAT | O_WRONLY, S_IRWXU | S_IRWXG | S_IRWXO);if (fd >= 0){backtrace_symbols_fd(buffer, size, fd);close(fd);}
}
3.3 addr2line解析backtrace信息
#!/bin/shif [ $# -lt 2 ]; thenecho "example: myaddr2line.sh test backtrace.log"exit 1
fiBIN_FILE=$1
BACK_TRACE_FILE=$2lines=$(cat $BACK_TRACE_FILE | grep ${BIN_FILE})
for line in ${lines}; doaddr=$(echo $line | awk -F '(' '{print $2}' | awk -F ')' '{print $1}')addr2line -e ${BIN_FILE} -C -f $addr
done
addr2line 是一個用于將程序地址(如內存地址)轉換為源代碼位置(文件名和行號)的工具。以下是其常用參數的詳細含義:
參數 | 含義 | 說明 |
---|---|---|
-e | --exe=FILE | 指定要分析的可執行文件或共享庫(必選參數)。 |
-p | --pretty-print | 以更易讀的格式輸出信息(如添加換行和縮進)。 |
-C | --demangle[=style] | 還原 C++ 符號名(如將 _Z3foov 轉換為 foo() )。 |
-i | --inlines | 顯示內聯函數的調用信息(包括原始函數和內聯位置)。 |
-f | --functions | 顯示函數名(默認僅顯示地址對應的行號)。 |
3.4 測試結果
可以看到,定位到了test.cpp的50行為崩潰的位置,代碼中的vector a沒有賦值,直接訪問vector[1]將會崩潰。
具體的調用棧關系為:
- main函數,test.cpp的65行:調用的
TestFun
函數 - TestFun函數,test.cpp的50行:執行的
printf("[%s] a[1]=%d\n", __func__, a[1]);
- SignalHandler函數,test.cpp的20行:崩潰觸發的SIGSEGV信號被捕獲后,在SignalHandler函數中的backtrace被處理
SignalHandler函數中,通過backtrace_symbols打印的信息,與通過backtrace_symbols_fd保存在backtrace.txt文件中的信息,其實是一樣的:
使用myaddr2line.sh腳本,可以方便打印所有的行號信息。
當然也可以手動使用addr2line來打印行號信息,只是效率較低。
另外,注意backtrace的地址,圓括號 ()
和 方括號 []
中的地址具有不同含義,分別對應 符號表中的函數地址 和 實際執行地址。
-
圓括號
(...)
中的地址- 含義:函數內部的 相對偏移量(相對于函數起始地址)
- 格式:
函數名+0x偏移量
- 作用:指示崩潰發生在該函數的具體位置。
-
方括號
[...]
中的地址- 含義:指令在 內存中的實際地址(絕對地址)
- 格式:
0xXXXXXXXX
- 作用:可直接用于
addr2line
等工具定位源代碼
但在本示例程序測試中,卻要使用圓括號中的地址,addr2line才能顯示行號,這里有待再研究。
4 總結
本篇介紹了如何使用backtrace工具來定位Linux應用程序崩潰的位置信息,首先通過signal捕獲崩潰信息,然后通過backtrace記錄崩潰時的堆棧調用信息,最后使用addr2line來顯示對應的崩潰時的代碼行號。