当前位置: 首页 > 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.dtcms.com/a/267664.html

相关文章:

  • 【STM32】定时器中断 + 含常用寄存器和库函数配置(提供完整实例代码)
  • 洛谷 P11967 [GESP202503 八级] 割裂-普及+/提高
  • 百度文心 4.5 大模型详解:ERNIE 4.5 Technical Report
  • 水下航行器外形之变体式与回转体的区别
  • 线程锁和线程同步
  • 从“电话催维修“到“手机看进度“——售后服务系统开发如何重构客户体验
  • Linux网络配置与故障排除完全指南
  • 12 nacos配置中心
  • 使用Kahn算法处理节点依赖关系
  • ABB焊接机器人智能节气仪
  • 汽车制造车间检测机器人与PLC无线以太网实时控制方案
  • 数据库学习笔记(十七)--触发器的使用
  • Java SE--数组
  • 前端相关性能优化笔记
  • TEXT Complete Search
  • 【RK3568 编译rtl8723DU驱动】
  • Write-up:hacker_dns
  • 安达发|告别低效排产:APS高级排程如何助力电池企业智造升级?
  • Java 大视界 -- 基于 Java 的大数据实时流处理在工业物联网设备能耗实时监测与节能优化中的应用(332)
  • 09_云原生架构:拥抱不确定性
  • 【力扣 简单 C】746. 使用最小花费爬楼梯
  • AI小智项目全解析:软硬件架构与开发环境配置
  • 自动化Prompt生成平台的研发体系设计
  • [HDLBits] Cs450/history shift
  • vue router 里push方法重写为什么要重绑定this
  • Xmind功能特点
  • LucidShape 2024.09 最新
  • 2025年3月青少年电子学会等级考试 中小学生python编程等级考试三级真题答案解析(判断题)
  • Docker文件操作、数据卷、挂载
  • Servlet学习