介绍
arm平台的调用栈与x86平台的调用栈大致相同,稍微有些区别,主要在于栈帧的压栈内容和传参方式不同。在arm平台的不同程序,采用的编译选项不同,程序运行期间的栈帧也会不同。有些工具在对arm的调用栈回溯时,可能会遇到无法回溯的情况。例如gdb在使用bt查看core dump文件调用栈时,有时会出现Backtrace stoped
的情况,有可能就是栈空间的压栈顺序导致的。当工具无法回溯时,就需要人工结合汇编代码对栈进行回溯,或者使用unwind进行回溯。
arm栈帧结构
-
通常情况下,arm的调用栈大致结构与x86相同,都是从高地址向低地址扩张。
-
pc, lr, sp, fp是处理器的寄存器,其含义如下:
- pc, program counter,程序计数器。程序当前运行的指令会放入到pc寄存器中
- lr是该函数的返回地址
- 调用栈从高地址向低地址增长,当函数调用时,分别将分别将pc, lr, ip和 fp寄存器压入栈中,然后移动sp指针,为当前程序开辟栈空间。
- sp是前一个函数的栈顶,fp是前一个函数的栈底
arm官方手册描述如下:
一个arm程序,在任一时刻都存在十五个通用寄存器,这取决于当前的处理器模式。 它们分别是 r0-r12、sp、lr。
sp(或 r13)是堆栈指针。 C 和 C++ 编译器始终将 sp 用作堆栈指针。 在 Thumb-2 中,sp 被严格定义为堆栈指针,因此许多对堆栈操作无用而又使用了 sp 的指令会产生不可预测的结果。 建议您不要将 sp 用作通用寄存器。
在用户模式下,lr(或 r14)用作链接寄存器 (lr),用于存储调用子例程时的返回地址。 如果返回地址存储在堆栈上,则也可将 r14 用作通用寄存器。
在异常处理模式下,lr 存放异常的返回地址;如果在一个异常内执行了子例程调用,则 lr 存放子例程的返回地址。如果返回地址存储在堆栈上,则可将 lr 用作通用寄存器。
除了官方手册中描述的sp,lr寄存器,通常r12还会作为fp寄存器。fp寄存器对于程序的运行没有帮助,主要用于对栈帧的回溯。因为sp时刻指向的栈顶,通过fp得知上一个栈帧的起始位置。
栈回溯原理
要获取栈回溯信息,通常需要以下几个步骤:
- 获取栈指针:栈指针(Stack Pointer, SP)指向当前栈的顶部,即当前栈帧的起始位置。栈回溯需要从栈指针开始读取栈帧的信息。
- 遍历栈帧:从栈顶开始,逐个解析栈帧中的返回地址。每个栈帧的返回地址指向调用当前函数的地方。
- 符号解析:返回地址通常是内存地址,需要通过符号表将这些地址转换为函数名称和源代码行号等信息。符号表是在编译时生成的,它包含了函数和变量的映射信息。