先看一个实例代码程序:
#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