注:栈回溯无法很好的定位到未调优化的函数,需要编译前使用 -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 规范所取代,此种方式基本已经不在使用。
栈回溯1.png

核心思想

  通过当前获取异常时的 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 的栈回溯

栈回溯2.png

核心思想

  获取异常发生时线程函数的 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 代码遍历的栈回溯

栈回溯3.png

核心思想

  获取异常发生时线程函数的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);// 三级调用者是谁?
}

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

文章作者: 路西法
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 以梦为马
栈回溯 栈回溯
喜欢就支持一下吧