ARM64堆栈回溯

基于AAPCS64栈帧的组织方式

先看一个实例代码程序:

#include 

int callee_func2(int a)
{
    int b = 2;
    return a + b;
}

int callee_func1(int a)
{
    int b = 1, c;

    c = callee_func2(a);
    return b + c;
}

int main(void)
{
    int ret;
    ret = callee_func1(0);
    return 0;
}                               

对该程序进行编译以及反汇编操作:

aarch64-linux-gnu-gcc test.c -o test
aarch64-linux-gnu-objdump -d test > disassemble.txt

打开disassemble.txt查看汇编代码:

 0000000000400578 
: 400578: a9be7bfd stp x29, x30, [sp,#-32]! 40057c: 910003fd mov x29, sp 400580: 52800000 mov w0, #0x0 // #0 400584: 97fffff0 bl 400544 400588: b9001fa0 str w0, [x29,#28] 40058c: 52800000 mov w0, #0x0 // #0 400590: a8c27bfd ldp x29, x30, [sp],#32 400594: d65f03c0 ret

主要查看main函数的入口位置,函数的入口最早做的就是对函数跳转的现场进行保存:

400578:   a9be7bfd    stp x29, x30, [sp,#-32]!

这一行表示把上一个函数的FP和LR寄存器push保存到sp-32的位置上,并且对sp地址-32操作,也就是说对于 main 函数预留了32 bytes的堆栈空间进行使用。

40057c:   910003fd    mov x29, sp

第二行,表示更新main函数使用的堆栈帧地址到FP中。这样通过FP寄存器我们可以在后续调用中对main函数的栈帧再进行保存。参考后面调用callee_func1函数的操作。

400584:   97fffff0    bl  400544 

这一步会执行跳转操作,同时会把返回地址更新到LR寄存器。接下来分析callee_func1函数:

0000000000400544 :
  400544:   a9bd7bfd    stp x29, x30, [sp,#-48]!
  400548:   910003fd    mov x29, sp
  40054c:   b9001fa0    str w0, [x29,#28]
  400550:   52800020    mov w0, #0x1                    // #1
  400554:   b9002fa0    str w0, [x29,#44]
  400558:   b9401fa0    ldr w0, [x29,#28]
  40055c:   97fffff1    bl  400520 
  400560:   b9002ba0    str w0, [x29,#40]
  400564:   b9402fa1    ldr w1, [x29,#44]
  400568:   b9402ba0    ldr w0, [x29,#40]
  40056c:   0b000020    add w0, w1, w0
  400570:   a8c37bfd    ldp x29, x30, [sp],#48
  400574:   d65f03c0    ret

在该子函数中,我们看到依然是同样的套路,第一步会先把FP和LR寄存器保存到堆栈中:

  400544:   a9bd7bfd    stp x29, x30, [sp,#-48]!

这一行就把main函数使用的FP和LR寄存器保存到堆栈中了,并且对SP寄存器地址-48,含义就是预留了48 bytes的堆栈空间给callee_func1使用。再接着看该函数的最后返回:

400570:   a8c37bfd    ldp x29, x30, [sp],#48

这里把上一级main函数使用的FP和LR从堆栈中恢复出来了。同时对sp寄存器执行+48操作,从而恢复上一级函数的堆栈指针现场,然后调用ret操作:

400574:   d65f03c0    ret

这一行会自动把LR寄存器保存的地址赋值给PC,也就因此跳转回main函数继续运行。

如何根据栈信息恢复函数调用关系

假如遇到机器panic的情况,首先从SP堆栈指针我可以看到,该SP指针对应的地址处,保存的应该就是上一级函数的LR寄存器和FP寄存器现场。根据(LR-4)可以找到上一级函数所在的地址(为什么-4?因为LR中保存的是返回后要执行的下一条指令,所以-4后的位置就是jmp的label)。上一级FP寄存器实际上就等于上一级函数使用的堆栈栈顶,当然这里栈顶依然会保存更上一级的LR和FP寄存器现场,按照这种链式保存的格式,可以回溯整个函数的调用流程。这样我们可以利用gdb来查看对应地址所对应的函数了。
比如本例中:

callee_func1 SP/FP --> callee_func1 stack top -- > saved main LR/FP
main LR-4 -- > main function label
main FP   ---> main stack top --> (save caller's FP and LR)

最后的函数执行

需要特别留意的是,当一个函数为最底层函数,并且不再调用其他函数时,FP在该函数中不会继续更新了,比如上例中的 callee_func2 函数,它不会调用其他函数,从它的汇编代码中查看,该函数在入口处并没有更新FP寄存器,因此也就不需要对FP做入栈保存的动作。仅仅对SP做了更新

0000000000400520 :
  400520:   d10083ff    sub sp, sp, #0x20
  400524:   b9000fe0    str w0, [sp,#12]
  400528:   52800040    mov w0, #0x2                    // #2
  40052c:   b9001fe0    str w0, [sp,#28]
  400530:   b9400fe1    ldr w1, [sp,#12]
  400534:   b9401fe0    ldr w0, [sp,#28]
  400538:   0b000020    add w0, w1, w0
  40053c:   910083ff    add sp, sp, #0x20
  400540:   d65f03c0    ret

你可能感兴趣的:(故障分析)