一、cdecl
cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。
主要要求如下:
- 函数实参在线程栈上按照从右至左的顺序依次压栈。
- 函数结果保存在寄存器EAX/AX/AL中。
- 编译后的函数名前缀是一个下划线字符。
- 调用者负责从线程栈中弹出实参(即清栈)。
例子如下:
1 2 3 4 5 6 7 8 9 10 11
| int main() { int r = 0; r = func(1, 2); return 0; }
int func(int a, int b) { int r = a + b; return r; }
|
编译后会产生如下代码:
main函数:
1 2 3 4 5 6 7 8
| ... 1 sub $0x8, esp 2 push $0x2 3 push $0x1 4 call _Z4funcii ; push eip, jmp _Z4funcii 5 add $0x10, esp 6 mov eax, -0xc(ebp) ...
|
func函数:
1 2 3 4 5 6 7 8 9 10
| 12 push ebp 13 mov esp, ebp 14 sub $0x10, esp 15 mov 0x8(ebp), edx 16 mov 0xc(ebp), eax 17 add edx, eax 18 mov eax, -0x4(ebp) 19 mov -0x4(ebp), eax 20 leave ; mov ebp, esp; pop ebp 21 ret ; pop eip
|
可以看到:main函数在调用func函数之前先进行了sub $0x8,这是因为在现代的x86-64架构中,调用函数时需要保持栈的16字节对齐。而最后栈空间的清理也是由调用者main函数的add $0x10来完成
二、stdcall
stdcall是由微软创建的调用约定,是Windows API的标准调用约定。非微软的编译器并不总是支持该调用协议。
stdcall与cdecall各个方面都非常类似,主要有以下不同:
- stdcall是由被调用者来清除参数的栈空间,因为这个特性stdcall也不支持可变参数。
- 不仅以下划线为前缀还以@加上参数的字节数作为后缀。
同样的例子产生的汇编代码如下:
main函数:
1 2 3 4 5 6 7 8
| ... 1 sub $0x8, esp 2 push $0x2 3 push $0x1 4 call _Z4funcii ; push eip, jmp _Z4funcii 5 add $0x8, esp 6 mov eax, -0xc(ebp) ...
|
func函数:
1 2 3 4 5 6 7 8 9 10
| 12 push ebp 13 mov esp, ebp 14 sub $0x10, esp 15 mov 0x8(ebp), edx 16 mov 0xc(ebp), eax 17 add edx, eax 18 mov eax, -0x4(ebp) 19 mov -0x4(ebp), eax 20 leave ; mov ebp, esp; pop ebp 21 ret $0x8 ; pop eip; add $0x8, esp
|
可以看到:func函数在返回的时候同时也ret $0x8来进行了参数栈空间的清理。
三、fastcall
fastcall 是一种注重速度的调用约定,通过寄存器传递参数来提高函数调用效率。
主要特点如下:
- 前两个参数(从左到右)通过寄存器传递:第一个参数用ECX,第二个参数用EDX。
- 剩余的参数(如果有)按照从右到左的顺序压入栈中。
- 函数结果保存在寄存器EAX中。
- 被调用者负责清栈(与stdcall相同)。
- 编译后的函数名以
@为前缀,以@加上参数的字节数作为后缀。
例子如下:
1 2 3 4 5 6 7 8 9 10 11
| int main() { int r = 0; r = func(1, 2, 3); return 0; }
int func(int a, int b, int c) { int r = a + b + c; return r; }
|
编译后会产生如下代码:
main函数:
1 2 3 4 5 6 7 8 9
| ... 1 sub $0xc, esp 2 push $0x3 3 mov $0x2, edx 4 mov $0x1, ecx 5 call _Z4funciii ; push eip, jmp _Z4funciii 6 add $0xc, esp 7 mov eax, -0xc(ebp) ...
|
_Z4funciii函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 13 push ebp 14 mov esp, ebp 15 sub $0x18, esp 16 mov ecx, -0x14(ebp) 17 mov edx, -0x18(ebp) 18 mov -0x14(ebp), edx 19 mov -0x18(ebp), eax 20 add eax, edx 21 mov 0x8(ebp), eax 22 add edx, eax 23 mov eax, -0x4(ebp) 24 mov -0x4(ebp), eax 25 leave ; mov ebp, esp; pop ebp 26 ret $0x4 ; pop eip; add $0x4, esp
|
可以看到:
- main函数将第1个和第2个参数分别放入ecx和edx寄存器,第3个参数通过
push $0x3压入栈中。
- 因为有1个参数在栈上(4字节),func函数返回时使用
ret $0x4清理栈空间。
sub $0x18貌似是为了快而直接开出的一块空间(包含了对齐,局部变量,参数影子空间等)。
函数调用约定: cdecl, stdcall, fastcall