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