C&&汇编中的调用约定

目录

[🧭 调用约定完全指南:从零到精通(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 语言与汇编之间最核心的"契约"。没有它,参数会找不到、返回值会丢失、寄存器会被随意破坏、堆栈会崩溃。掌握调用约定 = 真正看得懂编译器生成的每一行汇编 = 写出稳定、能与系统库无缝互操作的底层代码。

🎯 第一章 什么是调用约定?四大核心问题

一个完整的调用约定必须明确回答以下四个问题:

  1. 参数如何传递?(寄存器还是栈?顺序如何?)

  2. 谁负责清理堆栈?(调用者还是被调用者?)

  3. 哪些寄存器可以随意破坏?哪些必须由谁保存?

  4. 返回值放在哪里?

要素 常见答案示例
参数传递 从右到左压栈 / 前几个参数走寄存器
栈清理责任 调用者清理(cdecl)或被调用者清理(stdcall)
被调用者必须保存 EBX、ESI、EDI、EBP(x86)
返回值寄存器 EAX(32位) / RAX(64位)

⚙️ 第二章 x86 32位时代三大经典调用约定深度详解

🔴 cdecl ------ C语言的默认王者(最常用、最灵活)
  1. 核心特性一览

    • 参数从右到左压栈

    • 栈由调用者清理

    • 支持可变参数函数(printf、scanf 必须使用)

    • 返回值放在 EAX

  2. 标准函数序言与结尾(强烈推荐写法)

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 不清理参数,由调用者清理
  1. 调用者完整示例(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
  1. 为什么变参函数只能用 cdecl? 参数个数不固定,只有调用者知道到底压了多少,只有调用者能正确执行 add esp, n。
🟢 stdcall ------ Windows API 的灵魂(系统调用标准)
  1. 核心特性一览

    • 参数从右到左压栈(和 cdecl 完全一样)

    • 栈由被调用者清理(ret n)

    • 不支持可变参数

    • 返回值 EAX

  2. 最大优势:调用者代码极简洁

复制代码
; 被调用者结尾
cpp 复制代码
leave
ret 12                     ; 自动清理 3 个参数!这就是 stdcall 的精髓
  1. 调用者代码(超级干净)
cpp 复制代码
push 30
push 20
push 10
call MessageBoxA           ; 调用完直接继续,无需 add esp
  1. 使用场景与注意事项

    • 所有 Win32 API(Kernel32、User32、GDI32)

    • DLL 导出函数强烈推荐使用,避免不同模块栈清理不一致导致崩溃

    • 绝不能用于变参函数

🟡 fastcall ------ 微软早期性能优化版
  1. 核心特性

    • 前两个整数/指针参数通过 ECX 和 EDX 传递

    • 其余参数从右到左压栈

    • 栈由调用者清理(类似 cdecl)

    • 返回值仍为 EAX

  2. 调用者示例

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                 ; 只清理栈上的两个参数
  1. 被调用者访问方式
cpp 复制代码
; 参数位置:
; ecx = 第1个参数
; edx = 第2个参数
; [ebp+8]  = 第3个参数
; [ebp+12] = 第4个参数

🔵 第三章 现代 x64 时代的两大主流调用约定

🏆 Microsoft x64 调用约定(Windows 64位默认)
  1. 参数传递规则(前4个整数/指针) RCX → 第1个 RDX → 第2个 R8 → 第3个 R9 → 第4个 第5个及以后从右到左压栈

  2. 必须预留 32 字节"影子空间"(Shadow Space)

复制代码
sub rsp, 32                ; 必须分配,即使不使用
call SomeFunction
add rsp, 32                ; 调用后回收
  1. 完整调用示例
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位默认)
  1. 整数/指针前6个参数寄存器 RDI, RSI, RDX, RCX, R8, R9 浮点参数走 XMM0~XMM7

  2. 调用者负责栈清理,无影子空间要求,但栈必须 16 字节对齐

🛠️ 第四章 寄存器保存规则(所有约定通用的核心)

  1. 被调用者必须保存(Callee-saved) x86:EBX, ESI, EDI, EBP x64:RBX, RBP, R12-R15

  2. 调用者必须保存(Caller-saved) x86:EAX, ECX, EDX x64:RAX, RCX, RDX, R8-R11, XMM0-XMM5

  3. 经典保存/恢复模板

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 只有调用者知道参数个数

掌握这些调用约定,你就真正拥有了"看懂编译器、写出稳定底层代码"的能力。无论逆向、驱动、性能优化还是系统编程,这些知识都是绕不过去的基石。

相关推荐
雪影风痕1 小时前
华为安全防火墙部署
服务器·网络协议·tcp/ip·网络安全
鹿衔`2 小时前
CDH 6.3.2 集群外挂部署 Spark 3.5.7 连接 Paimon 1.1.1 (二)
大数据·分布式·spark
aitoolhub2 小时前
课程表模板在线制作:稿定设计的实用方案
大数据·深度学习·教育电商·在线设计·教育培训
-曾牛3 小时前
CSRF跨站请求伪造:原理、利用与防御全解析
前端·网络·web安全·网络安全·渗透测试·csrf·原理解析
介一安全3 小时前
【Frida Android】实战篇11:企业常用加密场景 Hook(1)
android·网络安全·逆向·安全性测试·frida
2301_800256113 小时前
8.3 查询优化 核心知识点总结
大数据·数据库·人工智能·sql·postgresql
samFuB3 小时前
【工具变量】全国社保落户制度改革城市DID数据(2010-2025年)
大数据
互联网资讯3 小时前
融合AI大模型的Geo优化系统服务商如何选?避坑指南
大数据·人工智能·ai搜索优化·geo系统·geo优化系统·geo系统搭建
黑客思维者3 小时前
IEEE 1547.3-2023与IEC62443标准异同分析
安全·系统安全·devsecops