x86-64函数调用及栈帧原理

本文介绍了x86-64架构下函数调用的原理,包括函数调用时的栈帧结构、参数传递、返回地址保存以及函数返回时的栈帧恢复过程。详细解析了call指令、ret指令以及leave指令在函数调用和返回中的作用,阐述了编译器如何自动处理寄存器和栈帧的保存与恢复。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

x86-64函数调用及栈帧原理

函数的调用

子函数调用时,调用者与被调用者的栈帧结构如下图所示:

image

在子函数调用时,执行的操作有:

  1. 父函数将调用参数从后向前压栈 ->
  2. 将返回地址压栈保存 ->
  3. 跳转到子函数起始地址执行 ->
  4. 子函数将父函数栈帧起始地址(%rpb) 压栈 ->
  5. 将 %rbp 的值设置为当前 %rsp 的值,即将 %rbp 指向子函数栈帧的起始地址。

上述过程中,保存返回地址和跳转到子函数处执行由 call 一条指令完成,在call 指令执行完成时,已经进入了子程序中,因而将上一栈帧%rbp 压栈的操作,需要由子程序来完成。函数调用时在汇编层面的指令序列如下:

...   # 参数压栈
call FUNC  # 将返回地址压栈,并跳转到子函数 FUNC 处执行
...  # 函数调用的返回位置

FUNC:  # 子函数入口
pushq %rbp  # 保存旧的帧指针,相当于创建新的栈帧
movq  %rsp, %rbp  # 让 %rbp 指向新栈帧的起始位置
subq  $N, %rsp  # 在新栈帧中预留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位

保存返回地址和保存上一栈帧的%rbp 都是为了函数返回时,恢复父函数的栈帧结构。在使用高级语言进行函数调用时,由编译器自动完成上述整个流程。对于”Caller Save” 和 “Callee Save” 寄存器的保存和恢复,也都是由编译器自动完成的。

需要注意的是,父函数中进行参数压栈时,顺序是从后向前进行的。但是,这一行为并不是固定的,是依赖于编译器的具体实现的,在gcc 中,使用的是从后向前的压栈方式,这种方式便于支持类似于 printf(“%d, %d”, i, j) 这样的使用变长参数的函数调用。

通过移动 %rsp 指针来改变帧的大小。%rbp 和 %rsp 之间的空间就是当前栈帧。而过程调用和退出过程,分别使用 call 指令和 ret 指令。“callq _fun1”是调用 _fun1 过程,这个指令相当于下面两句代码,它用到了栈来保存返回地址:

pushq %rip  # 保存下一条指令的地址,用于函数返回继续执行
jmp _fun1   # 跳转到函数 _fun1

_fun1 函数用 ret 指令返回,它相当于:

popq %rip   # 恢复指令指针寄存器
jmp %rip

函数的返回

函数返回时,我们只需要得到函数的返回值(保存在 %rax 中),之后就需要将栈的结构恢复到函数调用之前的状态,并跳转到父函数的返回地址处继续执行。由于函数调用时已经保存了返回地址和父函数栈帧的起始地址,要恢复到子函数调用之前的父栈帧,我们只需要执行以下两条指令:

movq %rbp, %rsp    # 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处
popq %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处

为了便于栈帧恢复,x86-64 架构中提供了 leave 指令来实现上述两条命令的功能。执行 leave 后,前面图中函数调用的栈帧结构如下:
image

可以看出,调用 leave 后,%rsp 指向的正好是返回地址,x86-64 提供的 ret 指令,其作用就是从当前 %rsp 指向的位置(即栈顶)弹出数据,并跳转到此数据代表的地址处,在leave 执行后,%rsp 指向的正好是返回地址,因而 ret 的作用就是把 %rsp 上移一个位置,并跳转到返回地址执行。可以看出,leave 指令用于恢复父函数的栈帧,ret 用于跳转到返回地址处,leave 和ret 配合共同完成了子函数的返回。当执行完成 ret 后,%rsp 指向的是父栈帧的结尾处,父栈帧尾部存储的调用参数由编译器自动释放。

#include <stdio.h>

int fun1(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8){
    int c = 10; 
    return x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + c;
};

int main(int argc, char* argv[]){
    printf("fun1: %d\n", fun1(1,2,3,4,5,6,7,8));
    return 0;
}

使用MinGW gcc 11.3.0 编译后的汇编如下:

fun1(int, int, int, int, int, int, int, int):
       # 函数调用的序曲, 设置栈指针
        pushq   %rbp                      # 把调用者的栈帧底部地址保存起来
        movq    %rsp, %rbp                # 把调用者的栈帧顶部地址, 设置为本栈帧的底部
        subq    $16, %rsp
        movl    %ecx, 16(%rbp)
        movl    %edx, 24(%rbp)
        movl    %r8d, 32(%rbp)
        movl    %r9d, 40(%rbp)
        movl    $10, -4(%rbp)            # 变量 c 赋值为 10, 也可以写成 movl $10, (%rsp)
        movl    16(%rbp), %edx
        movl    24(%rbp), %eax
        addl    %eax, %edx
        movl    32(%rbp), %eax
        addl    %eax, %edx
        movl    40(%rbp), %eax
        addl    %eax, %edx
        movl    48(%rbp), %eax
        addl    %eax, %edx
        movl    56(%rbp), %eax
        addl    %eax, %edx
        movl    64(%rbp), %eax
        addl    %eax, %edx
        movl    72(%rbp), %eax
        addl    %eax, %edx
        movl    -4(%rbp), %eax          # 加上 c 的值
        addl    %edx, %eax
        addq    $16, %rsp
        # 函数调用的尾声, 恢复栈指针为原来的值
        popq    %rbp                    # 恢复调用者栈帧的底部数值
        ret                             # 返回
.LC0:
        .ascii "fun1: %d\12\0"
main:
        # 函数调用的序曲, 设置栈指针
        pushq   %rbp                  # 把调用者的栈帧底部地址保存起来 
        movq    %rsp, %rbp            # 把调用者的栈帧顶部地址, 设置为本栈帧的底部
        subq    $64, %rsp             # 这里是为了让栈帧 16 字节对齐,实际使用可以更少
        movl    %ecx, 16(%rbp)
        movq    %rdx, 24(%rbp)
        call    __main
        movl    $8, 56(%rsp)     # 参数 8
        movl    $7, 48(%rsp)     # 参数 7
        movl    $6, 40(%rsp)     # 参数 6
        movl    $5, 32(%rsp)     # 参数 5
        movl    $4, %r9d         # 参数 4
        movl    $3, %r8d         # 参数 3
        movl    $2, %edx         # 参数 2
        movl    $1, %ecx         # 参数 1
        call    fun1(int, int, int, int, int, int, int, int)   # 调用函数
        movl    %eax, %edx
        leaq    .LC0(%rip), %rax    # 第一个参数是字符串的地址
        movq    %rax, %rcx          # 第二个参数是前一个参数的返回值
        call    printf              # 调用函数
        movl    $0, %eax            # 设置返回值。这句也常用 xorl %esi, %esi 这样的指令, 都是置为零
        addq    $64, %rsp           # 缩小栈
        # 函数调用的尾声, 恢复栈指针为原来的值
        popq    %rbp            # 恢复调用者栈帧的底部数值
        ret                     # 返回
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值