在嵌入式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 常见信号列表
信号分类:
- 不可捕获信号:无法通过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),用于输出结果 - 返回值:无(直接输出到文件)
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] inn", __func__);
std::vector<int> a;
printf("[%s] a[1]=%dn", __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 errn", __func__, signalType);
}
}
TestFun();
return0;
}
3.2 信号处理函数
#define MAX_STACK_FRAMES 100
void 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() nulln", __func__);
return;
}
for (size_t i = 0; i < size; ++i)
{
printf("#%d %sn", (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/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 是一个用于将程序地址(如内存地址)转换为源代码位置(文件名和行号)的工具。以下是其常用参数的详细含义:
3.4 测试结果
可以看到,定位到了test.cpp的50行为崩溃的位置,代码中的vector a没有赋值,直接访问vector[1]将会崩溃。
具体的调用栈关系为:
- main函数,test.cpp的65行:调用的TestFun函数
- TestFun函数,test.cpp的50行:执行的printf("[%s] a[1]=%dn", __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来显示对应的崩溃时的代码行号。