了解一些汇编知识有助于我们分析 crash,逆向等。
下面看一下高级语言中的函数调用,汇编是如何实现的。
看一个最简单的 C 语言的函数调用。
C 代码:
1 | int main(int argc, char * argv[]) { |
对应的汇编:
1 | coobjcBaseExample`main: |
所涉及的寄存器
- fp 寄存器 (frame pointer) 指向栈底
- lr 寄存器 (link register) 保存子程序执行完返回的地址
- sp 寄存器 (stack pointer) 栈顶
进入 mian 函数
- 分配栈空间
0x102ed7b04 <+0>: sub sp, sp, #0x40
如图,arm64 架构中,堆在低地址,从低地址向高地址增长。栈在高地址,从高地址向低地址增长,因此栈底在高地址,栈顶在低地址。
进入函数需要做的第一步就是开辟栈空间,供函数体内变量使用。这里通过 sub sp, sp, #0x40
就是栈顶向低地址方向移动 64(0x40) 个字节,开辟了64个字节大小的栈空间。
- 保存 fp 和 lr (保存现场)
stp x29, x30, [sp, #0x30]
把 main 函数调用者的 fp 和 main 函数执行完之后需要返回的地址保存 sp + 0x30 - 0x40 这段内存上。arm 64 一般寄存器的宽度是 64 位也就是 8个字节,0x30 - 0x40 有 16 个字节,可以存储两个寄存器的内容。
- 移动栈底到 sp + 0x30 位置上
add x29, sp, #0x30
也就是 main 的栈空间是 64 个字节大小,最底下的16个字节用来保存现场,真实可以用的栈地址是 sp + 0x30 - sp 的这 48 个字节。
- 为 test 传入参数
0x102ed7b28 <+36>: ldr x0, [sp, #0x18]
把 a 的值放入 x0 寄存器。
arm 64 会把前8个参数放入 x0 - x7 寄存器中,8个以外的参数通过栈传递
- 跳转到 test 函数执行
0x102ed7b2c <+40>: bl 0x102ed7ae0
- test 执行完之后 从 X0 获取返回值
0x102ed7b34 <+48>: str x0, [sp, #0x8]
- 从栈里恢复 fp 寄存器 lr寄存器,恢复现场
0x102ed7b3c <+56>: ldp x29, x30, [sp, #0x30]
- 释放栈空间
0x102ed7b40 <+60>: add sp, sp, #0x40
sp 加 0x40,释放掉函数开头分配的 64 个字节的栈空间。
- 根据 lr 中的值,返回 main 函数调用的地方执行
0x102ed7b44 <+64>: ret