C 语言内存布局与 ABI
更新时间:2026-08-25。本文是
languages/c/主题专家层第 2 篇。入门层讲"局部变量在栈上",本文深入"栈到底怎么工作"——函数调用时栈帧如何建立、参数怎么传、返回值怎么给。这是理解perf反汇编、看懂调用栈、排查栈溢出问题的底层功。
本文要回答的问题
- 函数调用时,栈上发生了什么?"栈帧"里存了什么?
- 参数是通过寄存器传还是通过栈传?返回值放哪?
- 什么是 ABI?为什么不同编译器编出来的
.o能互相链接?
一、进程虚拟内存布局回顾
先建立全局坐标(详细见 指针与内存管理):
| 区域 | 地址方向 | 内容 |
|---|---|---|
| 代码段(text) | 低地址 | 机器指令,只读 |
| 数据段(data/bss) | 中间 | 全局/静态变量 |
| 堆(heap) | 向上增长 | malloc 动态分配 |
| 栈(stack) | 向下增长 | 局部变量、调用帧 |
栈向下增长意味着:每调用一次函数,栈指针(rsp)减小(向低地址移动),为新栈帧腾出空间。
二、栈帧(stack frame)
2.1 栈帧里有什么
每次函数调用,都会在栈上开辟一个"栈帧",存放:
| 内容 | 作用 |
|---|---|
| 返回地址 | 函数结束后回到哪 |
| 保存的寄存器 | 调用者的寄存器上下文 |
| 局部变量 | 函数内的局部变量 |
| 参数(部分) | 超过寄存器数量的参数 |

2.2 函数调用的完整过程(以 x86-64 为例)
int add(int a, int b) {
int sum = a + b;
return sum;
}
int main(void) {
int r = add(3, 4);
return r;
}编译成汇编(gcc -S)后,add 函数大致是:
add:
push %rbp ; ① 保存调用者的 rbp
mov %rsp, %rbp ; ② rbp 指向当前栈帧底部
mov %edi, -4(%rbp) ; 参数 a(edi)存到局部
mov %esi, -8(%rbp) ; 参数 b(esi)存到局部
mov -4(%rbp), %eax ; 计算 a + b
add -8(%rbp), %eax
mov %eax, -12(%rbp) ; sum 存到局部
mov -12(%rbp), %eax ; 返回值放 eax
pop %rbp ; ③ 恢复 rbp
ret ; ④ 弹出返回地址并跳回三、System V AMD64 调用约定
调用约定(calling convention)规定"参数怎么传、返回值怎么给、谁负责保存哪些寄存器"。64 位 Linux 用 System V AMD64 ABI:
3.1 参数传递
前 6 个整型/指针参数用寄存器传:
| 参数顺序 | 寄存器 |
|---|---|
| 第 1 个 | rdi |
| 第 2 个 | rsi |
| 第 3 个 | rdx |
| 第 4 个 | rcx |
| 第 5 个 | r8 |
| 第 6 个 | r9 |
超过 6 个的参数压栈传递。浮点参数用 xmm0~xmm7。
3.2 返回值
整型/指针返回值放 rax,浮点返回值放 xmm0。
3.3 寄存器分类
| 类型 | 寄存器 | 谁负责保存 |
|---|---|---|
| 被调用者保存(callee-saved) | rbx rbp r12~r15 | 被调函数用前要保存,返回前恢复 |
| 调用者保存(caller-saved) | rax rcx rdx rsi rdi r8~r11 | 调用者需自己保存 |
四、什么是 ABI
ABI(Application Binary Interface,应用二进制接口) 规定二进制层面的兼容规则:
| ABI 规定的内容 | 说明 |
|---|---|
| 数据类型大小 | int 几字节、对齐方式 |
| 调用约定 | 参数/返回值怎么传 |
| 系统调用方式 | 怎么发起 syscall |
| 可执行文件格式 | ELF |
为什么重要:只要遵守同一 ABI,不同编译器(gcc/clang)、不同语言(C/Rust)编译出的 .o 就能互相链接。这就是为什么 Rust 能调用 C 库、C 能调用汇编。
ABI 与 API 的区别:API 是源码层面的接口约定(函数签名),ABI 是二进制层面的约定(寄存器/内存布局)。API 变了要改代码,ABI 变了要重新编译。
五、栈溢出(stack overflow)
栈大小是有限的(默认约 8MB,ulimit -s 查看)。以下情况会栈溢出:
- 无限递归:每层递归占一个栈帧,深到一定程度耗尽栈。
- 超大局部数组:
int huge[10000000];在栈上分配几十 MB。 - 越界写破坏返回地址:缓冲区溢出攻击的原理。
void recurse(int n) {
recurse(n + 1); // 无终止条件,无限递归 → 栈溢出
}栈溢出会导致 SIGSEGV(段错误),见 crash 排查线。
六、与本站主线衔接
- perf 反汇编:理解栈帧和寄存器,才能看懂
perf annotate的汇编视图,见 perf 使用指南。 - 火焰图:火焰图每个"层"就是一个栈帧,见 L5 ELF。
- 缓冲区溢出:栈帧布局是理解溢出攻击与防护(栈保护、ASLR)的基础,见 crash 排查线。
- 内存重排:ABI 与内存模型、屏障的关系,见 L4 内存屏障。
一句话总结
内存布局与 ABI = 栈向下增长、每次调用开一个栈帧(返回地址+rbp+局部变量)、System V AMD64 用 rdi/rsi/rdx 等寄存器传前 6 个参数、rax 存返回值;看懂栈帧和调用约定,就真正看懂了 perf 反汇编和函数调用的底层机制。
上一篇:编译、链接与 ELF 下一篇:与汇编、性能剖析衔接