x86-64 汇编 —— 栈与调用约定


栈(Stack)

栈是一块从高地址向低地址增长的内存区域,由 rsp(栈顶指针)管理。

graph TD
    subgraph "栈 (高地址 → 低地址)"
        A["argv, envp<br/>← 程序启动时的栈"]
        B["函数参数 n<br/>← 第7+个参数(超出入寄存器范围的)"]
        C["返回地址<br/>← call 指令压入"]
        D["旧的 rbp<br/>← 被调用者保存"]
        E["局部变量<br/>← 函数内部分配"]
        F["..."]
        G["(空闲)"]
    end
    A --> B --> C --> D --> E --> F --> G
    H["rsp(栈顶,低地址)"] -.-> G

基本栈操作

push rax ; rsp = rsp - 8; [rsp] = rax(入栈)
pop rax ; rax = [rsp]; rsp = rsp + 8(出栈)
sub rsp, 32 ; 分配 32 字节栈空间
add rsp, 32 ; 释放 32 字节栈空间

函数调用全过程

int add(int a, int b) 为例(Linux System V ABI):

; 调用者:
mov edi, 3 ; 第1参数 → rdi
mov esi, 5 ; 第2参数 → rsi
call add ; 压入返回地址,跳转到 add
; rsp 指向返回地址
 
; 被调用者 (add):
push rbp ; 保存调用者的 rbp
mov rbp, rsp ; 建立自己的栈帧
mov eax, edi ; eax = a
add eax, esi ; eax = a + b(返回值在 eax)
pop rbp ; 恢复调用者的 rbp
ret ; 弹出返回地址,返回调用者

调用约定(Calling Convention)

调用约定定义了函数参数的传递顺序和寄存器使用规则。C++ 教程中常出现两套 ABI:

System V AMD64 ABI(Linux / macOS / BSD)

参数位置寄存器
第1参数(整数/指针)rdi
第2参数rsi
第3参数rdx
第4参数rcx
第5参数r8
第6参数r9
第7+ 参数(从右向左压入)
返回值rax

C++ 非静态成员函数的隐式第一参数是 this 指针,因此在 Linux 上 this 通过 rdi 传递。这是 C++ 教程 05_面向对象(一)类与对象基础 中讲解 this 指针底层机制的基础。

Microsoft x64 ABI(Windows)

参数位置寄存器
第1参数rcx
第2参数rdx
第3参数r8
第4参数r9
第5+ 参数
返回值rax

Windows 上 this 通过 rcx 传递(因为它是第一个参数)。

对比示例

int add(int a, int b) { return a + b; }
 
struct Foo {
 int val;
 int addTo(int x) { return val + x; }
};

Linux 上的汇编:

; int add(int a, int b)
; a→edi, b→esi
add:
 lea eax, [rdi+rsi] ; eax = a + b
 ret
 
; int Foo::addTo(int x)
; this→rdi, x→esi
Foo::addTo:
 mov eax, [rdi] ; eax = this->val(val 在偏移 0)
 add eax, esi ; eax = this->val + x
 ret

Windows 上的汇编:

; int add(int a, int b)
; a→ecx, b→edx
add:
 lea eax, [rcx+rdx] ; eax = a + b
 ret
 
; int Foo::addTo(int x)
; this→rcx, x→edx
Foo::addTo:
 mov eax, [rcx] ; eax = this->val
 add eax, edx ; eax = this->val + x
 ret

同一份 C++ 代码,Linux 和 Windows 生成不同的汇编,但语义等价。理解这一差异后就不会在看到不同平台的汇编时困惑。

被调用者保存 vs 调用者保存

分类寄存器规则
被调用者保存rbx, rbp, r12~r15函数使用前必须保存原值,返回前恢复
调用者保存rax, rcx, rdx, rsi, rdi, r8~r11函数可自由修改,调用者需自行保存

编译器会处理这些细节。手动写汇编或阅读编译输出时需了解。

栈对齐

x86-64 ABI 要求 call 指令执行前 rsp 必须 16 字节对齐。push/pop 操作 8 字节,编译器自动插入 sub rsp, 8 等调整。在 C++ 教程 05_面向对象(一)类与对象基础 的内存对齐章节中,对象大小必须是最大对齐值的整数倍,与此要求一脉相承。

练习

以下题目从汇编/底层视角训练相关能力:

题号题目链接涉及知识点
20有效的括号https://leetcode.cn/problems/valid-parentheses/栈结构(push/pop对应)
155最小栈https://leetcode.cn/problems/min-stack/栈帧设计
232用栈实现队列https://leetcode.cn/problems/implement-queue-using-stacks/双栈、调用约定思想
94二叉树的中序遍历https://leetcode.cn/problems/binary-tree-inorder-traversal/递归调用的栈帧展开