一、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;
}

// __attribute__((cdecl))
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;
}

// __attribute__((fastcall))
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貌似是为了快而直接开出的一块空间(包含了对齐,局部变量,参数影子空间等)。