函数调用约定(x86 32位)

汇编yulabu2026-09-04 19 次阅读

32 位 x86 函数调用约定

本文讨论的是 32 位 x86 Windows 环境下常见的调用约定。
x86-64 下的参数传递规则与 32 位 x86 有明显区别,不能直接套用本文。

在理解了栈帧之后,我们知道函数调用过程中如何利用栈保存返回地址、参数、局部变量等数据。但这又引出了一个问题:参数究竟是怎么传给函数的?返回值又是怎么传回来的?

汇编语言本身没有像 C/C++ 那样由语言层面定义的“函数”这一抽象,但程序可以通过 call、ret、寄存器和栈等机制实现函数调用

c 复制代码
int func2(int a, int b)
{
    return a + b;
}

cpu最终可能执行的汇编是

asm 复制代码
add ...
ret

那么a和b是怎么来的?函数如何知道要用这两个数字?这就涉及到函数调用约定

什么是调用约定

或者说调用约定需要解决什么问题

**调用约定(Calling Convention)**是一套规定函数之间如何进行调用和协作的规则,包括参数如何传递、返回值如何传递、哪些寄存器由调用者或被调用者负责保存,以及由谁负责清理调用参数等。

它在程序运行时尤其重要,因为错误的调用约定可能导致程序崩溃或出现未定义行为

在传统 32 位 x86 架构上,C/C++函数调用通常会依照一定的约定来传递参数:

  • 参数是通过寄存器或栈传递的
  • 返回值通过寄存器传递
  • 栈的清理可以由调用者或被调用者负责,具体取决于调用约定

返回值如何传递?

调用约定除了规定参数如何传递,还需要规定函数执行完毕后,返回值放在哪里

在经典的 32 位 x86 C/C++ 调用约定中,整数类型的返回值通常通过 EAX 寄存器传递

c 复制代码
int add(int a, int b)
{
	return a + b;
}

编译后可能出现类似:

asm 复制代码
mov eax, [ebp+8]
add eax, [ebp+0Ch]
ret

这里

复制代码
EAX = a + b

函数执行 ret 返回之后,调用者就可以从 EAX 中获得返回值

_stdcall(标准调用约定)

一种常见的调用约定,特别是在 32 位 Windows API 的历史环境中大量使用

  • 参数传递顺序:同样从右至左依次压入栈中
  • 栈清理:由被调用者(即函数本身)负责清理栈上的参数。在函数执行完毕并返回前,函数会自动调整栈指针
  • 不支持可变参数:由于被调用者负责栈清理,无法支持可变参数函数

人话:被调用的函数自己清理自己搞乱的栈

优点:

  • 减少了调用者的栈管理负担,代码更简洁
  • 在固定参数函数中效率较高,因为调用者无需关心栈指针的调整

缺点:

  • 不支持可变参数函数
  • 被广泛用于Windows API和某些平台特定的库中,因此不如__cdecl灵活

函数末尾出现 ret N,cpu会先从栈顶取出返回地址并加载到指令指针寄存器 EIP,然后将 ESP 增加 N,最后跳转到该返回地址继续执行

ret N 会弹出返回地址,并在返回过程中额外将 ESP 增加 N 字节,用于回收调用参数占用的栈空间。

注意:ret N清理的实际上是调用参数的栈空间而非整个栈帧

_cdecl(C/C++程序默认函数调用约定)

在 32 位 MSVC 环境下,__cdecl 是普通 C/C++ 函数常见的默认调用约定,它具有以下特点:

  • 参数传递顺序:参数从右至左依次压入栈中
  • 栈清理:由调用者负责清理栈上的参数,这意味着每次调用函数时,调用者必须在函数返回后手动调整栈指针
  • 可变参数支持:因为调用者负责清理栈,这种调用约定天然支持可变参数函数(如 printf)

人话:调用者清理自己调用的函数搞乱的栈

**优点:**支持可变参数函数,适合需要灵活处理参数数量的函数

**缺点:**栈清理由调用者负责,如果频繁调用函数,栈清理开销会较大

在 call 返回后的后续指令中,如果看到 :

asm 复制代码
add esp,xxx

通常可以判断调用者正在回收此前压入栈中的参数

_fast call(快速调用约定)

该约定旨在通过寄存器传递参数来提高性能,适用于x86架构

在经典 MSVC 32 位 x86 __fastcall 约定中,参数通过ECX/EDX寄存器传递,剩余参数通过栈传递,被调用者负责栈平衡

最典型特征观察:call指令之前是否有编译器将头两个参数通过 mov 等指令放入 ECX/EDX,而不仅仅是push指令

具体区别

清理方式

  • __cdecl:调用者负责清理栈。这意味着每次调用函数后,调用者需要在汇编代码中清理参数(通常通过增加 ESP 寄存器的值)
  • __stdcall:被调用者负责清理栈。在函数返回时,栈的清理操作由函数自动完成,调用者不需要额外的栈清理步骤
  • _fastcall:同样由被调用者通过 ret N 清理栈的

传递顺序

无论是 __cdecl 还是 __stdcall,参数都从右至左压入栈中。这意味着在定义类似于 int add(int a, int b) 的函数时,参数 b 会比 a 先压入栈

可变参数支持

对于可变参数函数,例如 printf,被调用者无法仅根据函数类型确定实际传入了多少参数,因此无法可靠地自行确定需要清理多少参数栈空间。

__cdecl 让调用者负责清理栈,因此能够支持可变参数函数。

这就是为什么

c 复制代码
printf("%d %d %d", a, b, c);

这种函数可以工作

评论

正在加载评论区…