如何使用backtrace定位Linux程序的崩溃位置
在嵌入式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来显示对应的崩溃时的代码行号。