_cdecl_stdcall_fastcall 三种函数调用约定

需求复述

结合栈帧结构、函数调用时栈的核心作用(参数传递、局部变量分配、保存返回地址、寄存器恢复),详细拆解 _cdecl_stdcall_fastcall 三种函数调用约定的技术细节,并通过具体的地址、汇编示例分析每种约定的执行流程和核心差异。

核心讲解(基于x86 32位架构)

首先明确基础前提:x86 32位下栈是向下生长 (从高地址→低地址),ebp 指向栈帧底部(高地址),esp 指向栈帧顶部(低地址);函数调用的通用栈帧流程是「参数传递→保存返回地址→建立栈帧→执行逻辑→销毁栈帧→恢复现场」,三种调用约定的核心差异体现在参数传递方式栈清理责任上。

通用栈帧结构(先理解基础)

以函数调用 func(a,b) 为例,栈帧的通用布局(高地址→低地址):

复制代码
[旧ebp(调用者栈基址)]  <-- ebp(当前栈帧底部)
[返回地址]
[参数a]
[参数b]
[局部变量1]
[局部变量2]
...

注:实际地址偏移需结合压栈顺序计算,下文会用具体数值举例。


1. _cdecl(C默认调用约定)

核心规则 :参数从右到左压栈,调用者负责清理参数栈;支持可变参数(如 printf);名字修饰为「_函数名」(如 _add)。

示例分析:int add(int a, int b) { return a + b; }main 调用

假设初始状态:mainebp=0x0012FF80esp=0x0012FF80(栈空),以下是完整执行流程+地址变化:

步骤 操作(汇编伪代码) ebp esp 栈地址&值(关键) 说明
1 push 2(压入b) 0x0012FF80 0x0012FF7C 0x0012FF7C = 0x2 从右到左压参,先压第二个参数b
2 push 1(压入a) 0x0012FF80 0x0012FF78 0x0012FF78 = 0x1 再压第一个参数a
3 call _add 0x0012FF80 0x0012FF74 0x0012FF74 = 0x00401020 call 指令:先压返回地址(main的下一条指令地址),再跳转到_add
4 push ebp(add内) 0x0012FF80 0x0012FF70 0x0012FF70 = 0x0012FF80 保存调用者(main)的ebp
5 mov ebp, esp(add内) 0x0012FF70 0x0012FF70 - 建立add的栈帧,ebp指向当前栈底
6 mov eax, [ebp+8](add内) 0x0012FF70 0x0012FF70 eax=1 取参数a:ebp+8 = 0x0012FF78(对应步骤2的a)
7 add eax, [ebp+12](add内) 0x0012FF70 0x0012FF70 eax=3 取参数b:ebp+12 = 0x0012FF7C(对应步骤1的b)
8 mov esp, ebp(add内) 0x0012FF70 0x0012FF70 - 销毁局部变量(本例无),esp回到栈底
9 pop ebp(add内) 0x0012FF80 0x0012FF74 - 恢复main的ebp
10 ret(add内) 0x0012FF80 0x0012FF78 - 弹出返回地址到EIP,跳回main
11 add esp, 8(main内) 0x0012FF80 0x0012FF80 - 调用者清理参数栈(2个int共8字节)

核心特点 :调用者清理栈是关键,因此支持可变参数(如 printf 不知道参数个数,只能由调用者清理),但代码体积更大(每个调用处都要加清理指令)。


2. _stdcall(Windows API常用)

核心规则 :参数从右到左压栈,被调用者 负责清理参数栈;不支持可变参数;名字修饰为「_函数名@参数字节数」(如 _add@8,8是2个int的总字节数)。

示例分析:同上述 add(1,2)(仅差异处)

步骤1-7 与 _cdecl 完全一致,差异在步骤8-11:

步骤 操作(汇编伪代码) 关键差异
8 mov esp, ebp(add内) 同_cdecl
9 pop ebp(add内) 同_cdecl
10 ret 8(add内) ret 8:弹出返回地址后,直接将esp+8(清理8字节参数),esp回到0x0012FF80
11 -(main内) 调用者无需清理栈,省去 add esp,8

核心特点 :被调用者通过 ret N 指令(N为参数总字节数)清理栈,代码体积更小;但因需要提前知道参数个数,无法支持可变参数(Windows API无可变参数,因此适用)。


3. _fastcall(高性能调用约定)

核心规则 :前两个DWORD参数(int/指针等)用寄存器传递(ECX=第一个参数EDX=第二个参数),剩余参数从右到左压栈;被调用者清理栈;名字修饰为「@函数名@参数字节数」(如 @add@8)。

示例分析:同上述 add(1,2)(完全不同的参数传递方式)
步骤 操作(汇编伪代码) ebp esp 关键说明
1 mov ecx, 1(main内) 0x0012FF80 0x0012FF80 第一个参数a放入ECX
2 mov edx, 2(main内) 0x0012FF80 0x0012FF80 第二个参数b放入EDX
3 call @add@8(main内) 0x0012FF80 0x0012FF7C 仅压入返回地址(0x00401020),无参数压栈
4 push ebp(add内) 0x0012FF80 0x0012FF78 保存main的ebp
5 mov ebp, esp(add内) 0x0012FF78 0x0012FF78 建立add的栈帧
6 mov eax, ecx(add内) 0x0012FF78 0x0012FF78 从ECX取a=1
7 add eax, edx(add内) 0x0012FF78 0x0012FF78 从EDX取b=2,eax=3
8 mov esp, ebp(add内) 0x0012FF78 0x0012FF78 销毁栈帧
9 pop ebp(add内) 0x0012FF80 0x0012FF7C 恢复main的ebp
10 ret 0(add内) 0x0012FF80 0x0012FF80 无栈参数,ret 0等价于ret

扩展场景 :若函数有3个参数(如 sum(a,b,c)),则前两个参数(a/b)用ECX/EDX,第三个参数c从右到左压栈,被调用者通过 ret 4 清理c的4字节栈空间。

核心特点:寄存器访问比内存快,因此性能最优;但寄存器数量有限,仅前两个参数受益。


三种调用约定对比表
特性 _cdecl _stdcall _fastcall
参数传递方式 全部从右到左压栈 全部从右到左压栈 前两个寄存器,剩余压栈
栈清理责任 调用者 被调用者 被调用者
名字修饰 _函数名 _函数名@参数字节数 @函数名@参数字节数
可变参数支持 支持(如printf) 不支持 不支持
代码体积 较大 较小 最小
典型应用 C默认、可变参数函数 Windows API 性能敏感的函数

总结

  1. 三种调用约定的核心差异是参数传递方式栈清理责任_cdecl 调用者清理(支持可变参数),_stdcall/_fastcall 被调用者清理(性能/体积更优)。
  2. _fastcall 是性能最优的,通过寄存器传递前两个参数减少栈操作,逆向时需注意寄存器(ECX/EDX)而非栈来获取前两个参数。
  3. 栈帧的核心作用始终围绕:保存返回地址、维护参数/局部变量、恢复寄存器/ebp,调用约定仅改变「参数怎么传」和「栈谁来清」。
相关推荐
之歆2 小时前
Linux 网络配置与 TCP/IP 协议
linux·网络·tcp/ip
Turboex邮件分享3 小时前
邮件队列堵塞的深度排查与紧急清空/重定向实战
运维·网络
瑞华丽PLM3 小时前
电子通讯行业深度定制的国产PLM选型报告与数字化转型建议
网络·plm·国产plm·瑞华丽plm·瑞华丽
麦德泽特3 小时前
OpenWrt在机器人中的高级网络应用:AP+STA模式、中继与防火墙配置实战
运维·网络·机器人
小同志004 小时前
网络原理-HTTP/HTTPS(四)--认识请求 “正⽂“ (body)
网络·网络协议·http
dozenyaoyida4 小时前
RS预览失败问题分析和解决
网络·经验分享·嵌入式硬件·tcp·wifi6兼容性·视频预览卡顿
德迅云安全_初启4 小时前
2026年十大危险DNS攻击类型及预防措施
linux·服务器·网络
IPDEEP全球代理4 小时前
动态住宅IP与动态数据中心IP有什么不同?
运维·网络
Coisinilove4 小时前
数通第二次培训10.26
网络·数通·现代网络通信