目录
[🧭 调用约定完全指南:从零到精通(x86 & x64 架构)](#🧭 调用约定完全指南:从零到精通(x86 & x64 架构))
[🛠️ 前言:为什么必须掌握调用约定?](#🛠️ 前言:为什么必须掌握调用约定?)
[🎯 第一章 什么是调用约定?四大核心问题](#🎯 第一章 什么是调用约定?四大核心问题)
[⚙️ 第二章 x86 32位时代三大经典调用约定深度详解](#⚙️ 第二章 x86 32位时代三大经典调用约定深度详解)
[🔴 cdecl ------ C语言的默认王者(最常用、最灵活)](#🔴 cdecl —— C语言的默认王者(最常用、最灵活))
[🟢 stdcall ------ Windows API 的灵魂(系统调用标准)](#🟢 stdcall —— Windows API 的灵魂(系统调用标准))
[🟡 fastcall ------ 微软早期性能优化版](#🟡 fastcall —— 微软早期性能优化版)
[🔵 第三章 现代 x64 时代的两大主流调用约定](#🔵 第三章 现代 x64 时代的两大主流调用约定)
[🏆 Microsoft x64 调用约定(Windows 64位默认)](#🏆 Microsoft x64 调用约定(Windows 64位默认))
[🏆 System V AMD64 ABI(Linux/macOS 64位默认)](#🏆 System V AMD64 ABI(Linux/macOS 64位默认))
[🛠️ 第四章 寄存器保存规则(所有约定通用的核心)](#🛠️ 第四章 寄存器保存规则(所有约定通用的核心))
[📊 第五章 堆栈结构与实际案例全景图](#📊 第五章 堆栈结构与实际案例全景图)
[案例1:cdecl 调用 add(5, 8) 完整堆栈变化](#案例1:cdecl 调用 add(5, 8) 完整堆栈变化)
[案例2:使用 EBP vs 直接用 ESP 的致命错误(经典坑)](#案例2:使用 EBP vs 直接用 ESP 的致命错误(经典坑))
[🌟 第六章 总结与选型建议](#🌟 第六章 总结与选型建议)

🧭 调用约定完全指南:从零到精通(x86 & x64 架构)
🛠️ 前言:为什么必须掌握调用约定?
调用约定是 C 语言与汇编之间最核心的"契约"。没有它,参数会找不到、返回值会丢失、寄存器会被随意破坏、堆栈会崩溃。掌握调用约定 = 真正看得懂编译器生成的每一行汇编 = 写出稳定、能与系统库无缝互操作的底层代码。
🎯 第一章 什么是调用约定?四大核心问题
一个完整的调用约定必须明确回答以下四个问题:
-
参数如何传递?(寄存器还是栈?顺序如何?)
-
谁负责清理堆栈?(调用者还是被调用者?)
-
哪些寄存器可以随意破坏?哪些必须由谁保存?
-
返回值放在哪里?
| 要素 | 常见答案示例 |
|---|---|
| 参数传递 | 从右到左压栈 / 前几个参数走寄存器 |
| 栈清理责任 | 调用者清理(cdecl)或被调用者清理(stdcall) |
| 被调用者必须保存 | EBX、ESI、EDI、EBP(x86) |
| 返回值寄存器 | EAX(32位) / RAX(64位) |
⚙️ 第二章 x86 32位时代三大经典调用约定深度详解
🔴 cdecl ------ C语言的默认王者(最常用、最灵活)
-
核心特性一览
-
参数从右到左压栈
-
栈由调用者清理
-
支持可变参数函数(printf、scanf 必须使用)
-
返回值放在 EAX
-
-
标准函数序言与结尾(强烈推荐写法)
cpp
myfunc:
push ebp ; 1. 保存旧 ebp
mov ebp, esp ; 2. 建立新栈帧
push ebx ; 3. 保存被调用者保存寄存器
push esi
push edi
; 参数访问(绝对稳定!)
; [ebp + 8] = 第1个参数
; [ebp + 12] = 第2个参数
; [ebp + 16] = 第3个参数
; [ebp + 4] = 返回地址
; [ebp] = 旧 ebp
; 函数主体...
mov eax, 999 ; 返回值
pop edi ; 恢复寄存器(逆序)
pop esi
pop ebx
leave ; 等价于 mov esp, ebp ; pop ebp
ret ; 注意:ret 不清理参数,由调用者清理
- 调用者完整示例(C 与汇编双版本)
cpp
int result = add(10, 20, 30);
cpp
push 30 ; 第3个参数最先压
push 20
push 10 ; 第1个参数最后压,离栈顶最近
call add
add esp, 12 ; 调用者自己清理 3×4 字节
mov [result], eax
- 为什么变参函数只能用 cdecl? 参数个数不固定,只有调用者知道到底压了多少,只有调用者能正确执行 add esp, n。
🟢 stdcall ------ Windows API 的灵魂(系统调用标准)
-
核心特性一览
-
参数从右到左压栈(和 cdecl 完全一样)
-
栈由被调用者清理(ret n)
-
不支持可变参数
-
返回值 EAX
-
-
最大优势:调用者代码极简洁
; 被调用者结尾
cpp
leave
ret 12 ; 自动清理 3 个参数!这就是 stdcall 的精髓
- 调用者代码(超级干净)
cpp
push 30
push 20
push 10
call MessageBoxA ; 调用完直接继续,无需 add esp
-
使用场景与注意事项
-
所有 Win32 API(Kernel32、User32、GDI32)
-
DLL 导出函数强烈推荐使用,避免不同模块栈清理不一致导致崩溃
-
绝不能用于变参函数
-
🟡 fastcall ------ 微软早期性能优化版
-
核心特性
-
前两个整数/指针参数通过 ECX 和 EDX 传递
-
其余参数从右到左压栈
-
栈由调用者清理(类似 cdecl)
-
返回值仍为 EAX
-
-
调用者示例
cpp
int __fastcall fast_demo(int a, int b, int c, int d);
cpp
mov ecx, 100 ; 第1个参数 → ecx
mov edx, 200 ; 第2个参数 → edx
push 400 ; 第4个参数
push 300 ; 第3个参数
call fast_demo
add esp, 8 ; 只清理栈上的两个参数
- 被调用者访问方式
cpp
; 参数位置:
; ecx = 第1个参数
; edx = 第2个参数
; [ebp+8] = 第3个参数
; [ebp+12] = 第4个参数
🔵 第三章 现代 x64 时代的两大主流调用约定
🏆 Microsoft x64 调用约定(Windows 64位默认)
-
参数传递规则(前4个整数/指针) RCX → 第1个 RDX → 第2个 R8 → 第3个 R9 → 第4个 第5个及以后从右到左压栈
-
必须预留 32 字节"影子空间"(Shadow Space)
sub rsp, 32 ; 必须分配,即使不使用
call SomeFunction
add rsp, 32 ; 调用后回收
- 完整调用示例
cpp
mov rcx, hwnd
mov rdx, text_ptr
mov r8, title_ptr
mov r9, MB_OK
sub rsp, 32 ; 影子空间
call MessageBoxW
add rsp, 32
🏆 System V AMD64 ABI(Linux/macOS 64位默认)
-
整数/指针前6个参数寄存器 RDI, RSI, RDX, RCX, R8, R9 浮点参数走 XMM0~XMM7
-
调用者负责栈清理,无影子空间要求,但栈必须 16 字节对齐
🛠️ 第四章 寄存器保存规则(所有约定通用的核心)
-
被调用者必须保存(Callee-saved) x86:EBX, ESI, EDI, EBP x64:RBX, RBP, R12-R15
-
调用者必须保存(Caller-saved) x86:EAX, ECX, EDX x64:RAX, RCX, RDX, R8-R11, XMM0-XMM5
-
经典保存/恢复模板
cpp
push rbx
push r12
push r13
push r14
push r15
; ... 函数主体
pop r15
pop r14
pop r13
pop r12
pop rbx
📊 第五章 堆栈结构与实际案例全景图
案例1:cdecl 调用 add(5, 8) 完整堆栈变化
cpp
调用前:
[高地址]
...
[低地址] ← ESP
压参后:
+12 8 ← 参数2
+8 5 ← 参数1
+4 ← CALL 将要压入的返回地址
0 ← ESP
CALL 执行后:
+16 8
+12 5
+8 返回地址
+4 旧 EBP ← 被调用者 push ebp 后
0 局部变量等 ← ESP
案例2:使用 EBP vs 直接用 ESP 的致命错误(经典坑)
cpp
; 错误写法
mov eax, [esp+4] ; 取第1个参数
push ebx ; 保存寄存器
mov ebx, [esp+8] ; 想取第2个参数?错!现在偏移变了!
; 正确写法:建立栈帧
push ebp
mov ebp, esp
mov eax, [ebp+8] ; 第1个参数,永远固定
mov ebx, [ebp+12] ; 第2个参数,永远固定
🌟 第六章 总结与选型建议
| 场景 | 推荐调用约定 | 原因 |
|---|---|---|
| 编写普通 C 函数 | cdecl | 最通用,支持变参 |
| 编写 DLL 导出函数 | stdcall (x86) | 调用者代码最简洁 |
| 编写高性能库函数 | fastcall (老项目) / x64 寄存器约定 | 参数走寄存器更快 |
| 与 Win32 API 交互 | stdcall (32位) / Microsoft x64 | 必须保持一致 |
| Linux 系统编程 | cdecl (32位) / System V AMD64 | 系统默认 |
| 编写变参函数(printf) | 只能用 cdecl | 只有调用者知道参数个数 |
掌握这些调用约定,你就真正拥有了"看懂编译器、写出稳定底层代码"的能力。无论逆向、驱动、性能优化还是系统编程,这些知识都是绕不过去的基石。