Linux 栈回溯

目录

  • 前言
  • 一、什么是栈回溯?
  • 二、栈回溯的实现原理
  • 三、参考阅读

前言

  日常工作中,我们在开发软件程序时,经常会遇到程序奔溃的问题,导致程序奔溃的原因有很多,我们一般都是定位到相关代码,再去查询具体原因。而定位到bug相关代码往往需要依赖栈回溯这个功能,知道程序是在哪里挂掉的。

一、什么是栈回溯?

  在Linux系统中,栈回溯(stack trace)是用于跟踪程序执行期间函数调用的一种技术,记录了程序在出现错误或异常时,从当前位置开始追溯回到程序的执行起点,包括每个函数的调用关系和相应的返回地址。栈回溯可以帮助开发人员快速定位问题所在。Linux系统提供了几种获取栈回溯的方法:

  1. backtrace函数:该函数定义在execinfo.h头文件中,可以获取当前线程的栈回溯信息。使用该函数需要在编译时添加-g选项以启用调试符号。使用方法:
#include 
#include 
#include 

void printStackTrace() {
  void *callstack[128];
  int i, frames;
  char **strs;

  frames = backtrace(callstack, 128);
  strs = backtrace_symbols(callstack, frames);
  
  printf("Stack Trace:\n");
  for (i = 0; i < frames; i++) {
    printf("%s\n", strs[i]);
  }
  
  free(strs);
}

void func1() {
  printf("Entering func1...\n");
  printStackTrace();
  printf("Exiting func1...\n");
}

void func2() {
  printf("Entering func2...\n");
  func1();
  printf("Exiting func2...\n");
}

void func3() {
  printf("Entering func3...\n");
  func2();
  printf("Exiting func3...\n");
}

int main() {
  printf("Entering main...\n");
  func3();
  printf("Exiting main...\n");

  return 0;
}

编译指令如下:

gcc -rdynamic stacktrace.c -o stacktrace
./stacktrace

通过添加 -rdynamic 参数,您将为可执行文件生成完整的符号表,从而使 backtrace_symbols 函数能够正确解析堆栈帧的信息。

  1. pstack命令:该命令可以打印指定进程或线程的栈回溯信息。为了使用 pstack 工具,您的系统必须安装有 gdb(GNU Debugger)。在大多数 Linux 发行版中,gdb 已经预装或可以通过包管理器轻松安装。pstack使用方法:
pstack <pid>
pstack -T <pid>
  1. gdb调试器:该调试器可以在程序崩溃时获取栈回溯信息。使用方法:
gdb <executable>
(gdb) run
<program crashes>
(gdb) bt

 无论使用哪种方法,栈回溯都可以提供关于程序崩溃或异常的有价值的信息,帮助开发人员快速定位问题所在。

二、栈回溯的实现原理

  在 Linux ARM64 系统上,实现栈回溯的原理与其他架构类似,主要涉及到寄存器、栈帧和符号表等概念。

  1. 寄存器:ARM64 架构有一组通用寄存器,用于存储函数调用的参数、局部变量和返回值等。在栈回溯过程中,关键的寄存器是程序计数器(Program Counter,PC),它保存着当前指令的地址。栈回溯需要从 PC 寄存器中获取每个函数调用的返回地址。
	ARM64 架构中的 CPU 寄存器是用于存储和处理数据的关键组件。寄存器在计算过程中用于存储
操作数、中间结果和控制信息。以下是 ARM64 架构中常见的 CPU 寄存器:

1. 通用寄存器(General-Purpose Registers):
   -3164 位的通用寄存器,用来存储整数类型数据。
   - 这些寄存器被用于算术运算、数据传输、函数参数传递和临时存储等。
   - 寄存器命名为 x0-x30,其中 x30(栈帧指针)一般作为函数的帧指针使用。

2. 程序计数器(Program Counter):
   - 存储当前执行的指令的地址。
   - 通常使用 `pc` 表示,是一个 64 位的寄存器。

3. 标志寄存器(Flags Register):
   - 存储运算结果的条件信息,例如是否溢出、是否相等等。
   - 在 ARM64 架构中,标志寄存器叫做 Condition Flags Register(CPSR)。

4. 浮点寄存器(Floating-Point Registers):
   -32128 位的浮点寄存器,用于存储浮点数和进行浮点运算。
   - 寄存器命名为 v0-v31,每个寄存器可以存储一个 128 位的浮点数或者多个较小精度的浮点数。

5. SIMD 寄存器(Single Instruction, Multiple Data Registers):
   -32128 位的 SIMD 寄存器,用于存储数据并执行 SIMD(单指令多数据)运算。
   - 在 ARM64 架构中,SIMD 寄存器被称为向量寄存器(Vector Registers)。
   - 寄存器命名为 v0-v31,每个寄存器可以存储一个 128 位的向量或者多个较小精度的元素。

	除了上述常见的寄存器之外,ARM64 架构还有一些特殊用途的寄存器,如堆栈指针寄存器
(Stack Pointer Register,SP)等。

	这些寄存器在程序的执行过程中起着重要的作用,用于处理数据、控制程序流程、传递参数等。
编程时,需要根据需求合理使用寄存器来优化性能和实现所需的功能。

Linux 栈回溯_第1张图片

	x0-x30 是 ARM64 架构中的通用寄存器,共有 31 个寄存器,用于存储整数类型数据以及执行
各种操作。下面对这些寄存器进行详细说明:

1. x0-x30 (x0~x30)- 这些寄存器是通用寄存器,每个寄存器的大小为 64 位。
   - 在函数调用中,寄存器 x0-x7 用于函数参数的传递,后续的参数(这几个寄存器存满了)存储在栈上。
   - 寄存器 x8 保留给系统调用使用。
   - 寄存器 x9-x15 可用作临时寄存器。
   - 寄存器 x16-x17 用作特殊用途寄存器。
   - 寄存器 x18 保留给全局数据指针使用(例如 TLS 模型)。
   - 寄存器 x19-x28 用作通用寄存器,可以用于存储数据和进行算术运算。
   - 寄存器 x29(FP,Frame Pointer)用作栈帧指针,指向当前函数的栈帧的起始位置。
   - 寄存器 x30(LR,Link Register)用于存储函数调用时的返回地址。

总体而言,x0-x30 寄存器在 ARM64 架构中用于存储数据、进行算术运算、传递函数参数、控制程序流程等。在函数调用过程中,这些寄存器的使用通过约定规则来进行管理,有助于提高运行效率和优化编译代码。开发者需要根据编程需求合理使用这些寄存器,并遵循相关规则来保证程序的正确性和性能。
  1. 栈帧:在函数调用过程中,通过栈帧(Stack Frame)来保存函数的局部变量、返回地址和其他调用相关信息。每个栈帧由保护区域(Saved Registers)和局部变量区域(Local Variables)组成。栈帧中包含了被调用函数的返回地址,在函数返回时会恢复到该地址继续执行。

  2. 栈回溯算法:栈回溯的实现通常使用递归算法或迭代算法。下面是一种迭代的栈回溯算法:

    • 在当前堆栈帧中获取当前函数的返回地址。
    • 根据地址和可执行文件的调试符号表信息,匹配到对应的函数名称和行号。
    • 打印或记录匹配到的函数名称和行号。
    • 对于从地址中解析的下一个返回地址,重复上述步骤,直到回溯到最顶层的函数或者到达设定的回溯层数。
  3. 符号表:符号表是一个映射关系,将函数名与对应的地址关联起来。在栈回溯过程中,通过符号表可以将地址解析为函数名和行号等调试信息。在 Linux 系统中,可以使用调试符号表文件(例如 ELF 文件)来获取符号表信息。

  需要注意的是,栈回溯的准确性和可读性受到符号表的影响。如果可执行文件没有包含调试符号信息,或者没有正确设置符号文件的路径,栈回溯可能只能提供地址而无法解析为函数名和行号。因此,在构建可执行文件时,建议开启调试符号信息的生成并妥善保存符号表文件。

三、参考阅读

ARM体系结构:https://zhuanlan.zhihu.com/p/577979125?utm_id=0
内核中dump_stack:https://www.cnblogs.com/pengdonglin137/p/11109427.html
dump_stack:https://blog.csdn.net/weixin_52849254/article/details/130559085

  dump_stack 函数是 Linux 内核中的一个调试函数,用于在内核代码中打印当前的函数调用堆栈信息。它用于诊断和调试内核中发生的问题,如内核崩溃或死锁等。

  dump_stack 函数的原型定义在 kernel/lib/dump_stack.c 头文件中:

void dump_stack(void);
	__dump_stack();
		dump_stack_print_info(KERN_DEFAULT);
		show_stack(NULL, NULL) //arch\arm64\kernel\traps.c

  通过调用 dump_stack 函数,可以在内核日志中输出当前的函数调用堆栈信息。这对于定位问题非常有用,特别是在内核崩溃时,您可以通过查看内核日志中的堆栈跟踪信息来确定哪些函数导致了问题。

  要在内核代码中使用 dump_stack 函数,只需在适当的位置调用它即可。例如,在驱动程序中遇到错误或异常情况时,可以在相应的错误处理路径中调用 dump_stack 函数,以便在内核日志中获取有关问题的更多信息。

注:推荐一本讲调试技巧的书《Debug Hacks中文版—深入调试的技术和工具》,非常nice!!!

你可能感兴趣的:(Linux,API编程,从C高级到征服C++,linux,运维,服务器)