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

栈回溯方案

注:栈回溯无法很好的定位到未调优化的函数,需要编译前使用 -fno-optimize-sibling-calls 选项禁止尾调优化。

基于unwind的栈回溯

  在 arm 架构下,不少32位系统用的是 unwind 形式的栈回溯,这种栈回溯要复杂很多。首先需要程序有一个特殊的段 .ARM.unwind_idx 或者.ARM.unwind_tab,在连接文件中增加 __start_unwind_idx,这就是 ARM.unwind_idx 段的起始地址。这个unwind段中存储着跟函数入栈相关的关键数据。当函数执行入栈指令后,在 unwind 段会保存跟入栈指令一一对应的编码数据,根据这些编码数据,就能计算出当前函数栈大小和 cpu 的哪些寄存器入栈了,已经在栈中什么位置。
  当栈回溯时,首先根据当前函数中的指令地址,就可以计算出函数 unwind 段的地址,然后从 unwind 段取出跟入栈有关的编码数据,根据这些编码数据就能计算出当前函数栈的大小以及入栈时 lr 寄存器数据在栈中的存储地址。这样就可以找到 lr 寄存器数据,就是当前函数返回地址,也就是上一级函数的指令地址。此时sp一般指向的函数栈顶,SP+函数栈大小 就是上一级函数的栈顶。这样就完成了一次栈回溯,并且知道了上一级函数的指令地址和栈顶地址,按照同样的方法就能对上一级函数栈回溯,类推就能实现整个栈回溯流程。
  编译时需要加入 -funwind-tables 选项,此选项在编译时会依赖 标准glibc 库,要求不能使用 -nostdlib 选项,否则会报某些函数缺失错误。

.ARM.exidx : {__exidx_start = .;*(.ARM.exidx* .gnu.linkonce.armexidx.*)__exidx_end = .;}

优缺点分析

  • 缺点
    需要在代码的连接脚本中增加新的 unwind 专用段,对资源要求较高,且修改连接脚本容易引发未知问题。

  • 优点
    暂未详细分析。

基于FP寄存器的栈回溯

  APCS 规范在ARM架构上定义了程序函数调用和栈帧定义以及寄存器的使用的规范,中定义了 FP 和 IP 寄存器的作用,目前这个规范已经被 AAPCS 规范所取代,此种方式基本已经不在使用。
在这里插入图片描述

核心思想

  通过当前获取异常时的 FP 寄存器,找到调用者的栈帧开始地址,再通过栈帧下的相对偏移找到调用栈下的LR,从而确定子函数跳转地址 PC = LR - 跳转指令大小。

回溯过程

  1. 异常处理函数中获取线程异常时的堆栈指针 SP(获取MSP) 和 FP;

  2. 通过FP寄存器找到当前栈的栈帧开始位置,并通过偏移和LR的指令特征找到 fun_B 栈帧下存放的 LR

  3. 通过 (PC = *LR-跳转指令大小) 确定父函数 fun_A 调用异常函数 fun_B 的地址

  4. 通过获取 PC 地址下的指令,通过BL/BLX解码 找到 fun_B 函数的地址

  5. 通过相对偏移找到函数 fun_B 栈帧下的FP,从而找到函数 fun_A 的栈帧开始地址,执行LR搜索和指令解码

  6. 最后会找到 main 函数栈帧的开始地址,通过main栈帧偏移找到LR地址,发现 *LR 的地址是线程 main 函数退出收尾函数时停止栈回溯。

优缺点分析

  • 优点
  1. 实现方案简单,栈回溯效率高,能够直接确定调用者的栈帧开始地址,快速定位到调用者栈帧下的 LR;

  2. 栈的遍历中无需检查每个 LR 的特征,通用语Thumb 和 ARM 指令集;

  • 缺点
  1. 目前基本不在使用APCS规范,高于gcc 5.0 版本的编译器不在支持 FP寄存器的压栈;

  2. 需要修改链接脚本,在编译选项中加入-fomit-frame-pointer -mapcs-frame;

  3. 在每个函数下都增加了 FP 等寄存器的压栈指令,调试时与实际运行程序有差别;

基于SP遍历 LR 的栈回溯

在这里插入图片描述

核心思想

  获取异常发生时线程函数的 SP(MSP),然后逐个从栈上取出内容进行判断,在 Thumb 指令集下栈上保存的 LR 是父函数进入子函数位置的下一条指令位置 +1,这里的+1表明了栈上 LR 位置存放的一定是一个奇数,再判断这个奇数-1 是否在 .text 段范围内,筛选出奇数后判断 *LR - 4 和 *LR - 2 位置是否满足 BL/BLX 指令特征,对于 ARM 指令集下栈上保存的 LR 是父函数进入子函数位置的下一条指令位置,这个值是4字节对齐的,再判断这个奇数-1 是否在 .text 段范围内,筛选出 4 字节对齐的内容后判断 *LR - 4 和 *LR - 2 位置是否满足 BL/BLX 指令特征,然后计算出跳转指令大小 PC=(*LR-指令大小) 就是 子函数调用位置,再根据获取 PC 下的指令,对指令进行地址解码就可以找到函数开始地址了。

回溯过程

  1. 异常处理函数中获取线程异常时的堆栈指针 SP(获取MSP),中断/内核栈下的PC(指向异常发生时的执行指令);
  2. 从异常时 SP 向上遍历栈帧找到 Thumb 指令集下 LR1 位置下的内容为 0x60256b93 这是一个奇数,然后取出 0x60256b93 - 1 - 4 = 0x60256b8e ,判断这个地址是否在 .text 段范围内,再判断指令是否为 bl/blx中的一种,如果是则说明找到了父函数 fun_A 调用 fun_B 的位置,记录下来;
  3. 继续向上遍历栈,找到 Thumb 指令集下 LR2 位置下的内容为 0x602561e9 这是一个奇数,取出0x602561e9 - 1 -4 = 0x602561e4 地址下的内容,判断是否是 b/bl/blx中的一种,如果是则说明找到了父函数 main 调用fun_A 的位置,记录下来;
  4. 继续向上遍历栈,找到 Thumb 指令集下 LR3 位置,*LR3-1-4 等于线程退出收尾函数时停止栈回溯;

优缺点分析

  • 优点
  1. 栈回溯效率相对较高,只需遍历栈找特征LR值即可;
  2. 无需修改连接脚本,对原始SDK侵入性较小;
  • 缺点
  1. 严重依赖 LR 特征值,可能出现错误解析;
  2. 不同架构,以及Thumb 和 ARM 指令集中BL/BLX 指令格式不同,兼容较为繁琐;

基于SP 代码遍历的栈回溯

在这里插入图片描述

核心思想

  获取异常发生时线程函数的SP 和 PC ,通过 PC 位置在 .text 上寻找函数压栈操作指令 push/stmdb 和栈内存申请指令 sub/sub.w (SUB SP minus immediate) ,计算出栈帧大小,然后确定 LR 位置,确定调用者栈底位置 SP+framesize,然后在确定调用者调用子函数的位置 PC = LR - 跳转指令大小,之后根据 PC 位置继续从调用者函数的 .text 代码段遍历栈帧操作指令。

回溯过程

  1. 异常中断中获取线程异常时的堆栈指针 SP(获取MSP),中断/内核栈下的PC(指向异常发生时的执行指令);
  2. 向上遍历异常函数 fun_B 的 .text 段内容寻找 push 指令,解析 6026d17a 处压栈的寄存器个数4个寄存器包括 lr 寄存器,以此处 push 指令特征值为 0xb500,继续遍历 6026d178 处压栈寄存器个数为 4,则相对于 lr 寄存器的偏移 offsetsize = 4,此时栈帧大小为 8;
  3. 从 push 指令向下搜索栈扩展指令 sub/sub.w,6026d180 处在栈上申请了 386 *4 个空间;所以栈帧总大小为 framesize = 386+8;
  4. 确定 LR 位置 LR = SP + framesize - offsetsize;
  5. 确定调用者 fun_A 函数栈帧(调用者栈)的栈帧底部位置 SP = SP + framesize;
  6. 再通过 (*LR - 跳转指令大小) = PC 确定 fun_A 中调用 fun_B 的位置;
  7. 之后继续从 fun_A 下的 .text 段的 PC 位置向上遍历,如此循环,直到找到的 *LR 是线程退出收尾函数为止;

优缺点分析

  • 优点
  1. 对栈上内容的依赖性较小,完全通过 .text 代码节进行遍历;
  2. 栈的遍历中无需检查每个 LR 的特征,适用于 Thumb 和 RAM 指令集;
  • 缺点
  1. 效率较低,需要对从函数开始到子函数跳转位置进行遍历,如果函数很长则影响效率;
  2. 复杂性高,需要解析栈操作指令来获取压栈和栈扩展的大小,从而确定栈帧大小;

内存泄露定位

打印栈帧还有一个应用,就是检查谁引起内存泄露:

// s_array 为全局数组
static int alloc_en(void *addr, unsigned int size);void* malloc_wrapper(unsigned int size)
{void *ptr = (void*)malloc(size);alloc_en(ptr, size);return ptr;
}static int alloc_en(void *addr, unsigned int size)
{for(i = 0; i < MAX_CALL; ++i){if(NULL == s_array[i].addr)break;}if(i >= MAX_CALL){printf("no free slot");return -1;}s_array[i].addr = (U32)addr;s_array[i].size = size;s_array[i].caller = backtrace(3);// 三级调用者是谁?
}

🌀路西法 的个人博客拥有更多美文等你来读。


http://www.mrgr.cn/news/91467.html

相关文章:

  • who knows the answer
  • 解锁机器学习核心算法 | 支持向量机:机器学习中的分类利刃
  • 下载安装运行测试开源vision-language-action(VLA)模型OpenVLA
  • C语言——深入理解指针(2)(数组与指针)
  • Arm64架构CentOS7服务器搭建Fabric环境
  • Django 5实用指南(二)项目结构与管理
  • 【MySQL安装】
  • TMS320F28335二次bootloader在线IAP升级
  • 云计算架构学习之Ansible-playbook实战、Ansible-流程控制、Ansible-字典循环-roles角色
  • Docker安装Minio对象存储
  • 天翼云910B部署DeepSeek蒸馏70B LLaMA模型实践总结
  • 如何使用 vxe-table grid 全配置式给单元格字段格式化内容,格式化下拉选项内容
  • 小米电视维修记录 2025/2/18
  • Ubuntu学习备忘
  • 【TOT】Tree-of-Thought Prompting
  • python进阶篇-面向对象
  • 23种设计模式 - 模板方法
  • cesium视频投影
  • 前端VUE+后端uwsgi 环境搭建
  • Breakout Tool