如何使用backtrace定位Linux程序的崩溃位置

发布于:2025-07-06 ⋅ 阅读:(14) ⋅ 点赞:(0)

在嵌入式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命令,用于暂停进程执行,无法被忽略或处理

信号分类

  • 不可捕获信号:无法通过signalsigaction修改处理方式,只能由系统强制控制。
    • 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] 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 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() 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/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 是一个用于将程序地址(如内存地址)转换为源代码位置(文件名和行号)的工具。以下是其常用参数的详细含义:

参数 含义 说明
-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来显示对应的崩溃时的代码行号。