概念
函数提高了一种封装代码的方式,它可以被多次调用,调用完成后会返回调用点,可选的它可以接收多个参数,返回一个值。
运行时栈
栈是后进先出的数据结构,系统在程序运行时会分配一块内存区域以栈的方式管理,这块内存区域就叫运行时栈或栈内存空间。
函数调用,参数传递,局部变量所需要的内存都使用栈空间。
在内存中,栈底为高地址,栈顶为底地址,即栈向低地址增长,压栈时栈顶地址减小,出栈时栈顶地址增大。
x86 使用 RSP 寄存器保存栈顶,arm64 使用 sp 寄存器保存栈顶
函数的调用与返回
1
2
3
4
5
6
7
8
| void f(void)
{
}
void c(void)
{
f();
}
|
函数调用只需要将程序计数器(PC)执行被调用函数代码的起始位置,函数返回时需要跳转到调用点下一条执行以继续执行当前函数。
因此函数调用分为两步:
- 保存当前位置到栈中
- 跳转到新函数执行
函数返回也是两步:
- 从栈中获取之前保存的调用点位置
- 跳转到调用点位置继续执行
下面汇编均使用 gcc 编译器,其他编译器可能不一样
x86 平台:
call 指令用来调用函数, 它会将调用点地址即 call 指令下一条指令地址压入栈中,然后将 PC 指向调用函数的地址。
ret 指令从函数返回,它从栈中弹出地址,并将 PC 指向该地址
上面代码生成的 x86 汇编
1
2
3
4
5
6
7
8
9
10
11
12
13
| f:
pushq %rbp
movq %rsp, %rbp
nop
popq %rbp
ret
c:
pushq %rbp
movq %rsp, %rbp
call f
nop
popq %rbp
ret
|
arm64 平台:
bl 指令相当于 call 指令,它将下一条指令保存到 x30 寄存器 ,然后跳转
ret 指令从函数返回,地址为 x30 寄存器地址,如果嵌套调用,使用栈保存和恢复 x30 寄存器
生成的 arm64 汇编:
1
2
3
4
5
6
7
8
9
10
| f:
nop
ret
c:
stp x29, x30, [sp, -16]!
mov x29, sp
bl f
nop
ldp x29, x30, [sp], 16
ret
|
局部变量
函数内部局部变量内存使用栈空间分配,函数返回时释放用到的栈空间
1
2
3
4
5
6
| void f(void)
{
long a = 10;
char b = 20;
long c = 30;
}
|
对应的 x86 汇编
1
2
3
4
5
6
7
8
9
| f:
pushq %rbp // 将 rbp 寄存器原来内容放入栈中 ,rsp 增长 8
movq %rsp, %rbp // 将栈顶 rsp 的值赋值给 rbp ,现在 rbp 指向栈顶
movq $10, -8(%rbp) // 栈增长 8 ,将 10 放入
movb $20, -9(%rbp) // 虽然是 char ,但是栈增长依旧是 8
movq $30, -24(%rbp)
nop
popq %rbp // 将 rbp 还原 ,rsp 减少
ret
|
变长栈
当用 alloca 或者声明变长数组时,栈的大小要在运行时才能确定
1
2
3
4
5
6
| void f(int n)
{
int a = 10;
int array[n];
array[a] = 100;
}
|
这种情况,编译器会使用 rbp 作为栈的基地址,函数调用过程中保持不变,栈中局部变量利用 rbp 寻址
对应的汇编及解释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| f:
// 前两条指令存储 rbp, 并将 rsp 保存到 rbp
pushq %rbp
movq %rsp, %rbp
// 分配 48 字节的栈空间
subq $48, %rsp
// 变量 n 放到栈上
movl %edi, -36(%rbp)
movq %rsp, %rax
movq %rax, %rcx
// int a = 10
movl $10, -4(%rbp)
movl -36(%rbp), %eax
// n 放到 rdx ,rdx 符号扩展
movslq %eax, %rdx
// n - 1 放到栈上
subq $1, %rdx
movq %rdx, -16(%rbp)
cltq
// ((n * 4 + 15) / 16) * 16) , 向上对齐到 16 字节
leaq 0(,%rax,4), %rdx
movl $16, %eax
subq $1, %rax
addq %rdx, %rax
movl $16, %esi
movl $0, %edx
divq %rsi
imulq $16, %rax, %rax
// 分配栈空间
subq %rax, %rsp
movq %rsp, %rax
// 栈地址对齐 4 字节 (n + 3) / 4 * 4
addq $3, %rax
shrq $2, %rax
salq $2, %rax
// 保存数组首地址到栈中
movq %rax, -24(%rbp)
// array[a] = 100
movq -24(%rbp), %rax
movl -4(%rbp), %edx
movslq %edx, %rdx
movl $100, (%rax,%rdx,4)
// 恢复 rsp ,释放动态数组
movq %rcx, %rsp
nop
// 等价于 movq %rbp ,%rsp; popq %rbp
leave
ret
|
参数传递
x86 小于 6 个参数,使用寄存器传递,大于 6 个参数的部分使用栈传递
1
2
3
4
5
6
| void f(long a, long b, long c, long d, long e, long f, long g, long h);
void c(void)
{
f(1, 2, 3, 4, 5, 6, 7, 8);
}
|
f 调用时第 7, 8 参数放到栈中,1, 2, 3, 4, 5, 6 放到寄存器中
| 参数数量 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|
| 寄存器 | rdi | rsi | rdx | rcx | r8 | r9 |
7,8 两个参数 8 先入栈,7 后入栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| c:
pushq %rbp
movq %rsp, %rbp
pushq $8
pushq $7
movl $6, %r9d
movl $5, %r8d
movl $4, %ecx
movl $3, %edx
movl $2, %esi
movl $1, %edi
call f
addq $16, %rsp
nop
leave
ret
|
结构体作为参数时 ,按照成员顺序分配给不同的寄存器传递
1
2
3
4
5
6
| struct A {
long a;
long b;
};
void f(struct A a);
|
其中寄存器 rdi 传递 A.a ,寄存器 rsi 传递 A.b
返回值传递
1
2
3
4
| int f(void)
{
return 100;
}
|
x86 使用 rax 寄存器传递返回值
1
2
3
4
5
6
| f:
pushq %rbp
movq %rsp, %rbp
movl $100, %eax
popq %rbp
ret
|
asm 使用 w0 寄存器传递