汇编之函数调用

了解一些汇编知识有助于我们分析 crash,逆向等。
下面看一下高级语言中的函数调用,汇编是如何实现的。

看一个最简单的 C 语言的函数调用。

C 代码:

1
2
3
4
5
int main(int argc, char * argv[]) {
int64_t a = 1;
int64_t b = 2;
test(a);
}

对应的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

所涉及的寄存器

  1. fp 寄存器 (frame pointer) 指向栈底
  2. lr 寄存器 (link register) 保存子程序执行完返回的地址
  3. sp 寄存器 (stack pointer) 栈顶

进入 mian 函数

  1. 分配栈空间

0x102ed7b04 <+0>: sub sp, sp, #0x40

如图,arm64 架构中,堆在低地址,从低地址向高地址增长。栈在高地址,从高地址向低地址增长,因此栈底在高地址,栈顶在低地址。
进入函数需要做的第一步就是开辟栈空间,供函数体内变量使用。这里通过 sub sp, sp, #0x40 就是栈顶向低地址方向移动 64(0x40) 个字节,开辟了64个字节大小的栈空间。

  1. 保存 fp 和 lr (保存现场)

stp x29, x30, [sp, #0x30]

把 main 函数调用者的 fp 和 main 函数执行完之后需要返回的地址保存 sp + 0x30 - 0x40 这段内存上。arm 64 一般寄存器的宽度是 64 位也就是 8个字节,0x30 - 0x40 有 16 个字节,可以存储两个寄存器的内容。

  1. 移动栈底到 sp + 0x30 位置上

add x29, sp, #0x30

也就是 main 的栈空间是 64 个字节大小,最底下的16个字节用来保存现场,真实可以用的栈地址是 sp + 0x30 - sp 的这 48 个字节。

  1. 为 test 传入参数

0x102ed7b28 <+36>: ldr x0, [sp, #0x18]

把 a 的值放入 x0 寄存器。

arm 64 会把前8个参数放入 x0 - x7 寄存器中,8个以外的参数通过栈传递

  1. 跳转到 test 函数执行

0x102ed7b2c <+40>: bl 0x102ed7ae0

  1. test 执行完之后 从 X0 获取返回值

0x102ed7b34 <+48>: str x0, [sp, #0x8]

  1. 从栈里恢复 fp 寄存器 lr寄存器,恢复现场

0x102ed7b3c <+56>: ldp x29, x30, [sp, #0x30]

  1. 释放栈空间

0x102ed7b40 <+60>: add sp, sp, #0x40

sp 加 0x40,释放掉函数开头分配的 64 个字节的栈空间。

  1. 根据 lr 中的值,返回 main 函数调用的地方执行

0x102ed7b44 <+64>: ret