了解一些汇编知识有助于我们分析 crash,逆向等。
下面看一下高级语言中的函数调用,汇编是如何实现的。
看一个最简单的 C 语言的函数调用。
C 代码:
int main(int argc, char * argv[]) {
int64_t a = 1;
int64_t b = 2;
test(a);
}
对应的汇编:
coobjcBaseExample`main:
0x102ed7b04 <+0>: sub sp, sp, #0x40 ; =0x40
0x102ed7b08 <+4>: stp x29, x30, [sp, #0x30]
0x102ed7b0c <+8>: add x29, sp, #0x30 ; =0x30
0x102ed7b10 <+12>: orr x8, xzr, #0x2
0x102ed7b14 <+16>: orr x9, xzr, #0x1
0x102ed7b18 <+20>: stur w0, [x29, #-0x4]
0x102ed7b1c <+24>: stur x1, [x29, #-0x10]
0x102ed7b20 <+28>: str x9, [sp, #0x18]
0x102ed7b24 <+32>: str x8, [sp, #0x10]
-> 0x102ed7b28 <+36>: ldr x0, [sp, #0x18]
0x102ed7b2c <+40>: bl 0x102ed7ae0 ; test at main.m:12
0x102ed7b30 <+44>: mov w10, #0x0
0x102ed7b34 <+48>: str x0, [sp, #0x8]
0x102ed7b38 <+52>: mov x0, x10
0x102ed7b3c <+56>: ldp x29, x30, [sp, #0x30]
0x102ed7b40 <+60>: add sp, sp, #0x40 ; =0x40
0x102ed7b44 <+64>: ret
所涉及的寄存器
- 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