函数调用与栈帧

函数调用与栈帧

引言

cpp 复制代码
int add(int a, int b) {
    int c = a + b;
    return c;
}

int main() {
    int x = add(1, 2);
}

执行main时,CPU如何执行add

  1. 跳到add的地址去执行
  2. 需要传递给add的参数
  3. add执行完毕后结果返回值需要传递给main
  4. 需要返回到main中调用的地方,且main依旧保持原样

栈与栈帧

栈是一个后进先出的内存区域。同时在linux中,栈向低地址增长(这是 ABI/实现选择)。

为什么选用栈进行函数调用?

栈本身便是后进先出,天然满足保持add执行后,main保持原样的要求。

栈帧是对函数调用时产生的栈空间的定义。

plain 复制代码
┌─────────────────────┐  ← 高地址
│ 调用者传入的参数。     │  (可能有)
├─────────────────────┤
│ 返回地址             │  (必须有)
├─────────────────────┤
│ 保存的帧指针          │  (可选)
├─────────────────────┤
│ 保存的被调者寄存器     │  (按需)
├─────────────────────┤
│ 局部变量, 临时空间    │  (按需)
├─────────────────────┤
│ 对齐填充             │  (可能有)
└─────────────────────┘  ← 低地址

汇编代码

让我们看看计算机底层汇编是如何实现上述操作的。

cpp 复制代码
add(int, int):
        push    rbp  // 压栈
        mov     rbp, rsp // rbp = rsp
        sub     rsp, 32  // 给局部变量/对齐留空间
        mov     DWORD PTR [rbp-20], edi // 将 rbp - 20 的空间存放参数
        mov     DWORD PTR [rbp-24], esi
        mov     edx, DWORD PTR [rbp-20] // edx = edi
        mov     eax, DWORD PTR [rbp-24] // eax = esi
        add     eax, edx // eax = eax + edx
        mov     DWORD PTR [rbp-4], eax  // rbp - 4 的空间存放 eax
        mov     eax, DWORD PTR [rbp-4]  // eax 等于 rbp - 4 中的值
        pop     rbp // 弹出 rbp
        ret
main:
        push    rbp      // 将 rbp 压入栈中
        mov     rbp, rsp // rbp = rsp
        sub     rsp, 16  // 向下开辟 16 字节空间,用于对齐
        mov     esi, 2   // 传递参数
        mov     edi, 1   // 传递参数
        call    add(int, int) // 调用函数
        mov     DWORD PTR [rbp-4], eax // rbp - 4 的空间存放 eax
        mov     eax, 0   // eax = 0
        leave  
        ret

调用者传入参数

前 6 个整数/指针参数:走寄存器

Linux x86-64(SysV ABI)约定:

  1. 第 1 个参数 → rdi
  2. 第 2 个参数 → rsi
  3. 第 3 个参数 → rdx
  4. 第 4 个参数 → rcx
  5. 第 5 个参数 → r8
  6. 第 6 个参数 → r9
    超过 6 个的,才会继续压入到栈上。

所以才有

cpp 复制代码
        mov     esi, 2   // 传递参数
        mov     edi, 1   // 传递参数

寄存器也分32与64位。esi是32位的,rsi是64位的。edi,rdi同理

下面是寄存器用法:

返回地址

call add 做两件事

  1. 把"下一条指令的地址"压入栈 ------ 这就是 返回地址
  2. 跳转到 add 的入口地址执行
    也就是说:call = push 返回地址 + 跳转

ret 做一件事

  1. 从栈顶弹出一个地址,跳转到那里继续执行
    也就是说:ret = pop 返回地址 + 跳回去

所以返回地址由被调用者保存。

保存的帧指针

cpp 复制代码
        push    rbp  // 压栈
        mov     rbp, rsp // rbp = rsp
        // ...
        pop     rbp
为什么要保存 rbp
  1. 调用约定要求rbp 通常属于 callee-saved(被调用者若使用必须恢复)
  2. 要形成栈帧链:调试/回溯时可以沿着"保存的 rbp"一级一级找到上层函数

push rbp 是一个"压栈"指令,本质等价于两步:

  1. rsp = rsp - 8
  2. [rsp] = rbp(把当前 rbp 的 8 字节值写到栈顶空间)

pop rbp 是"出栈"指令,本质等价于两步:

  1. rbp = [rsp](把栈顶 8 字节读到 rbp)
  2. rsp = rsp + 8

通过pushpop可以

  • rbp 恢复为进入函数前的旧值
  • 栈顶回到原来的位置(向高地址移动 8 字节)

局部变量 / 临时空间

cpp 复制代码
        mov     DWORD PTR [rbp-20], edi // 将 rbp - 20 的空间存放参数
        mov     DWORD PTR [rbp-24], esi
		// ...
        mov     DWORD PTR [rbp-4], eax  // rbp - 4 的空间存放 eax

本质上就是通过rbp的偏移,去进行存储。

要求偏移空间是合法的。

  • 局部变量区:服务于"源代码语义",写了这个变量,它应该在函数期间存在
  • 临时空间:服务于"编译器实现",没写,但机器需要。

对齐填充

cpp 复制代码
        sub     rsp, 16  // 向下开辟 16 字节空间,用于对齐

对齐,一个地址如果能被某个数整除,就说它按那个数对齐。例如:

  • 地址 0x...10 能被 16 整除 → 16 字节对齐
  • 地址 0x...18 不能被 16 整除(余 8) → 非 16 字节对齐

对齐填充(padding),当你需要某个对齐,但当前位置不满足时,就在中间"多留出一些没用的字节",把后面的内容挪到满足对齐的位置。在栈帧里,这种"没用的字节"就是对齐填充

在 x86-64 linux(ABI) 里,调用约定要求:

在执行 call 指令之前,栈指针 rsp 需要满足特定对齐(常见要求是 16 字节对齐)。

原因主要有三类:

  1. ABI 要求
  2. 性能提升,对齐的数据更利于 CPU 以更少的内存事务读写
  3. 可变参数、调用链一致性,保证跨编译器、跨语言(C/C++/Rust 等)互调时,栈布局一致且可预期。

返回值

这里有几种情况:

4字节:当函数返回值是4个字节会通过%eax寄存器作为通道,函数将返回值存储在%eax中,返回后函数的调用方再读取%eax。

5-8个字节:通过rax寄存器作为通道。

大于8个字节:以如下代码举例:

cpp 复制代码
struct A {// ...大于8字节  };
A func() {
    A b;
    return b;
}
A x = func();
  1. 调用者分配返回对象空间(temp);
  2. 把这块空间的地址作为隐藏的第一个参数传给被调用者;
  3. 被调用函数把结果写入该地址指向的内存。
  4. 调用者得到这块内存中的结果。

返回值传递方式如图:

参考文章:

https://chengxumiaodaren.com/docs/build-debug/linux-func-call/

相关推荐
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc8 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
小白同学_C8 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖8 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
赶路人儿9 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar1239 小时前
C++使用format
开发语言·c++·算法
码说AI10 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS10 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
lanhuazui1010 小时前
C++ 中什么时候用::(作用域解析运算符)
c++