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