概念

函数提高了一种封装代码的方式,它可以被多次调用,调用完成后会返回调用点,可选的它可以接收多个参数,返回一个值。

运行时栈

栈是后进先出的数据结构,系统在程序运行时会分配一块内存区域以栈的方式管理,这块内存区域就叫运行时栈或栈内存空间。

函数调用,参数传递,局部变量所需要的内存都使用栈空间。

在内存中,栈底为高地址,栈顶为底地址,即栈向低地址增长,压栈时栈顶地址减小,出栈时栈顶地址增大。

x86 使用 RSP 寄存器保存栈顶,arm64 使用 sp 寄存器保存栈顶

函数的调用与返回

1
2
3
4
5
6
7
8
void f(void)
{
}

void c(void)
{
    f();
}

函数调用只需要将程序计数器(PC)执行被调用函数代码的起始位置,函数返回时需要跳转到调用点下一条执行以继续执行当前函数。

因此函数调用分为两步:

  1. 保存当前位置到栈中
  2. 跳转到新函数执行

函数返回也是两步:

  1. 从栈中获取之前保存的调用点位置
  2. 跳转到调用点位置继续执行

下面汇编均使用 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 放到寄存器中

参数数量123456
寄存器rdirsirdxrcxr8r9

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 寄存器传递

1
2
3
f:
        mov     w0, 100
        ret