本文列举几个常见的函数调用约定,并以汇编语言的形式进行解释
__cdecl ( C Declaration 的缩写)
:c 语言默认方式,参数从右向左入栈,主调函数负责栈平衡。函数名进行修饰时,会加前导下划线。__stdcall
:windows API 默认方式,参数从右向左入栈,被调函数负责栈平衡,函数名进行修饰时,会加上前缀下划线,并且后面紧跟一个 @ 符号,其后紧跟着参数的大小。__fastcall
:快速调用方式。这种方式选择将参数优先从寄存器传入(ECX 和 EDX),剩下的参数再从右向左从栈传入。寄存器传参要比栈传参快,因此名为:__fastcall
。由被调函数负责栈平衡。thiscall
:调用方式是唯一一种不能显示指定的修饰符。他是 C++ 类成员函数缺省的调用方式。由于成员函数调用还有一个 this 指针,因此必须用这种特殊的调用方式。参数从右向左入栈。如果参数个数确定,this 指针通过 ecx 传递给被调用者;如果参数个数不确定,this 指针在所有参数压入栈后被压入栈。参数个数不定时,由调用者清理堆栈,否则由函数自己清理堆栈。- 还有一些不常使用的调用约定,比如:
naked call
等等
一、__cdecl
调用约定
我们先来通过代码来观察,如下是 C 语言代码:
1 | int func(int a, int b, int c, int d, int e, int f) { |
来编译代码:
1 | gcc main.c -g -m32 -o main |
我们来看生成的汇编代码
1 | 000004ed <func>: |
我们从 main 函数看起,我们发现
- func 函数的这六个参数都是使用栈来传参的,并且是从右向左的顺序
- 平衡栈的方式是通过 “外部平衡栈” 的方式,
add esp, 0x18
就是用来平衡栈。因为调用 func 函数,使用栈来存储参数,所以调用完毕之后,要进行平衡栈。如上 func 函数有 6 个参数,每个参数占用 4 字节,一共 24 字节,也就是 0x18 大小。因此将 esp 寄存器加上 0x18 即可完成平衡栈的功能
二、__fastcall
调用约定
还是如上的代码,我们修改下代码看下:
1 | int __attribute__((fastcall)) func(int a, int b, int c, int d, int e, int f) { |
继续通过如下的方式编译
1 | gcc main.c -g -m32 -o main |
生成的汇编代码如下:
1 | 000004ed <func>: |
此时我们看到,使用了寄存器传参。还是从右向左的传参顺序。ecx 传递第一个参数,edx 传递第二个参数。而且使用的内部平衡栈的方式。也就是在被调用者的函数栈帧中。
ret 0x10
的意思就是从栈中弹出 16 个字节,并将控制流返回到调用函数的位置。这也就是内部平衡栈的说法。