当前位置: 首页 > news >正文

如何使用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信号名称默认行为触发场景
1SIGHUP终止程序终端连接断开(如 SSH 会话结束),或用户登出时通知进程重新加载配置
2SIGINT终止程序(Ctrl+C)用户在终端按下 Ctrl+C,请求中断当前进程
3SIGQUIT终止程序并生成 Core 文件用户按下 Ctrl+\,通常用于强制退出并生成调试用的 Core 文件
4SIGILL终止程序并生成 Core 文件进程执行非法指令(如无效的机器码),通常由程序编译错误或硬件异常导致
5SIGTRAP终止程序并生成 Core 文件触发断点陷阱(如调试器设置的断点),用于程序调试时的中断
6SIGABRT终止程序并生成 Core 文件通常是由进程自身调用 C标准函数库 的 abort() 函数来触发
7SIGBUS终止程序并生成 Core 文件硬件总线错误(如访问未对齐的内存地址,或内存映射文件错误)
8SIGFPE终止程序并生成 Core 文件发生算术错误(如除零、溢出、精度错误),例如1/0运算
9SIGKILL强制终止程序(不可捕获)系统或用户发送kill -9命令,用于强制终止无响应的进程,无法被忽略或处理
10SIGUSR1终止程序用户自定义信号 1,可由程序自定义处理逻辑(如日志刷新、状态通知)
11SIGSEGV终止程序并生成 Core 文件访问无效内存地址(如空指针解引用、越界访问),是最常见的程序崩溃原因之一
12SIGUSR2终止程序用户自定义信号 2,用途与SIGUSR1类似,供程序开发者自由定义功能
13SIGPIPE终止程序向已关闭的管道或套接字写入数据(如 TCP 连接断开后继续发送数据)
14SIGALRM终止程序定时器超时(由alarm()setitimer()函数触发),用于超时控制
15SIGTERM终止程序(可捕获)系统或用户发送kill命令(默认),请求进程正常退出,程序可自定义处理逻辑
16SIGSTKFLT终止程序栈溢出错误(仅在某些架构上存在,如 x86),通常与硬件相关的栈异常有关
17SIGCHLD忽略信号子进程状态改变(如终止或暂停),父进程可通过wait()系列函数获取子进程信息
18SIGCONT继续运行暂停的进程当进程被暂停(如SIGSTOP)后,用于恢复其执行,默认行为为继续运行
19SIGSTOP暂停进程(不可捕获)系统或用户发送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 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来显示对应的崩溃时的代码行号。

http://www.lryc.cn/news/581403.html

相关文章:

  • Python练习Day1
  • 【C语言刷题】第十一天:加量加餐继续,代码题训练,融会贯通IO模式
  • 双倍硬件=双倍性能?TDengine线性扩展能力深度实测验证!
  • 类(JavaBean类)和对象
  • BM6 判断链表中是否有环(牛客)
  • Linux安装java后没法运行
  • 西门子PLC博图软件学习(一)
  • 手写 Vue 中虚拟 DOM 到真实 DOM 的完整过程
  • .NET9 实现排序算法(MergeSortTest 和 QuickSortTest)性能测试
  • LinkedList 链表数据结构实现 (OPENPPP2)
  • 前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
  • AI智能体革命:从对话机器到自主决策的进化之路 **——当AI长出“手和脑”,一场人机协作范式转移正在发生
  • AI小智项目全解析:软硬件架构与开发环境配置
  • 图灵完备之路(数电学习三分钟)----解码器
  • Pytest 测试发现机制详解:自动识别测试函数与模块
  • 理想汽车6月交付36279辆 第二季度共交付111074辆
  • 比较两个csv文件的内容是否一致
  • Python 机器学习核心入门与实战进阶 Day 3 - 决策树 随机森林模型实战
  • HTML初学者第三天
  • centos 7.6安装mysql8
  • 基于大模型的肾积水全周期预测与诊疗方案研究报告
  • 03每日简报20250705
  • Qt开发:QListWidget的介绍和使用
  • java整合itext pdf实现自定义PDF文件格式导出
  • 画笔的进化:生成式AI与艺术创造力的范式革命
  • 完成ssl不安全警告
  • 数据结构:数组:二分查找(Binary Search)
  • 用 Turbo Vision 2 为 Qt 6 控制台应用创建 TUI 字符 MainFrame
  • Java-继承
  • 隐马尔可夫模型:语音识别系统的时序解码引擎