ARM64堆栈回溯

基于AAPCS64栈帧的组织方式

先看一个实例代码程序:

#include <stdio.h>

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 <main>:
   400578:   a9be7bfd    stp x29, x30, [sp,#-32]!
   40057c:   910003fd    mov x29, sp
   400580:   52800000    mov w0, #0x0                    // #0
   400584:   97fffff0    bl  400544 <callee_func1>
   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 <callee_func1>

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

0000000000400544 <callee_func1>:
  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 <callee_func2>
  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 <callee_func2>:
  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

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值