函数的调用约定

本文列举几个常见的函数调用约定,并以汇编语言的形式进行解释

  • __cdecl ( C Declaration 的缩写):c 语言默认方式,参数从右向左入栈,主调函数负责栈平衡。函数名进行修饰时,会加前导下划线。
  • __stdcall:windows API 默认方式,参数从右向左入栈,被调函数负责栈平衡,函数名进行修饰时,会加上前缀下划线,并且后面紧跟一个 @ 符号,其后紧跟着参数的大小。
  • __fastcall:快速调用方式。这种方式选择将参数优先从寄存器传入(ECX 和 EDX),剩下的参数再从右向左从栈传入。寄存器传参要比栈传参快,因此名为:__fastcall。由被调函数负责栈平衡。
  • thiscall:调用方式是唯一一种不能显示指定的修饰符。他是 C++ 类成员函数缺省的调用方式。由于成员函数调用还有一个 this 指针,因此必须用这种特殊的调用方式。参数从右向左入栈。如果参数个数确定,this 指针通过 ecx 传递给被调用者;如果参数个数不确定,this 指针在所有参数压入栈后被压入栈。参数个数不定时,由调用者清理堆栈,否则由函数自己清理堆栈。
  • 还有一些不常使用的调用约定,比如:naked call 等等

一、__cdecl 调用约定

我们先来通过代码来观察,如下是 C 语言代码:

1
2
3
4
5
6
7
8
int func(int a, int b, int c, int d, int e, int f) {
return a + b + c + d + e + f;
}

int main() {
int res = func(1, 2, 3, 4, 5, 6);
return 0;
}

来编译代码:

1
2
gcc main.c -g -m32 -o main
objdump -d -M intel main

我们来看生成的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
000004ed <func>:
4ed: 55 push ebp
4ee: 89 e5 mov ebp,esp
4f0: e8 51 00 00 00 call 546 <__x86.get_pc_thunk.ax>
4f5: 05 e7 1a 00 00 add eax,0x1ae7
4fa: 8b 55 08 mov edx,DWORD PTR [ebp+0x8]
4fd: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
500: 01 c2 add edx,eax
502: 8b 45 10 mov eax,DWORD PTR [ebp+0x10]
505: 01 c2 add edx,eax
507: 8b 45 14 mov eax,DWORD PTR [ebp+0x14]
50a: 01 c2 add edx,eax
50c: 8b 45 18 mov eax,DWORD PTR [ebp+0x18]
50f: 01 c2 add edx,eax
511: 8b 45 1c mov eax,DWORD PTR [ebp+0x1c]
514: 01 d0 add eax,edx
516: 5d pop ebp
517: c3 ret

00000518 <main>:
518: 55 push ebp
519: 89 e5 mov ebp,esp
51b: 83 ec 10 sub esp,0x10
51e: e8 23 00 00 00 call 546 <__x86.get_pc_thunk.ax>
523: 05 b9 1a 00 00 add eax,0x1ab9
528: 6a 06 push 0x6
52a: 6a 05 push 0x5
52c: 6a 04 push 0x4
52e: 6a 03 push 0x3
530: 6a 02 push 0x2
532: 6a 01 push 0x1
534: e8 b4 ff ff ff call 4ed <func>
539: 83 c4 18 add esp,0x18
53c: 89 45 fc mov DWORD PTR [ebp-0x4],eax
53f: b8 00 00 00 00 mov eax,0x0
544: c9 leave
545: c3 ret

我们从 main 函数看起,我们发现

  • func 函数的这六个参数都是使用栈来传参的,并且是从右向左的顺序
  • 平衡栈的方式是通过 “外部平衡栈” 的方式,add esp, 0x18 就是用来平衡栈。因为调用 func 函数,使用栈来存储参数,所以调用完毕之后,要进行平衡栈。如上 func 函数有 6 个参数,每个参数占用 4 字节,一共 24 字节,也就是 0x18 大小。因此将 esp 寄存器加上 0x18 即可完成平衡栈的功能

二、__fastcall调用约定

还是如上的代码,我们修改下代码看下:

1
2
3
4
5
6
7
8
int __attribute__((fastcall)) func(int a, int b, int c, int d, int e, int f) {
return a + b + c + d + e + f;
}

int main() {
int res = func(1, 2, 3, 4, 5, 6);
return 0;
}

继续通过如下的方式编译

1
2
gcc main.c -g -m32 -o main 
objdump -d -M intel main

生成的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
000004ed <func>:
4ed: 55 push ebp
4ee: 89 e5 mov ebp,esp
4f0: 83 ec 08 sub esp,0x8
4f3: e8 5c 00 00 00 call 554 <__x86.get_pc_thunk.ax>
4f8: 05 e4 1a 00 00 add eax,0x1ae4
4fd: 89 4d fc mov DWORD PTR [ebp-0x4],ecx
500: 89 55 f8 mov DWORD PTR [ebp-0x8],edx
503: 8b 55 fc mov edx,DWORD PTR [ebp-0x4]
506: 8b 45 f8 mov eax,DWORD PTR [ebp-0x8]
509: 01 c2 add edx,eax
50b: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
50e: 01 c2 add edx,eax
510: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
513: 01 c2 add edx,eax
515: 8b 45 10 mov eax,DWORD PTR [ebp+0x10]
518: 01 c2 add edx,eax
51a: 8b 45 14 mov eax,DWORD PTR [ebp+0x14]
51d: 01 d0 add eax,edx
51f: c9 leave
520: c2 10 00 ret 0x10

00000523 <main>:
523: 55 push ebp
524: 89 e5 mov ebp,esp
526: 83 ec 10 sub esp,0x10
529: e8 26 00 00 00 call 554 <__x86.get_pc_thunk.ax>
52e: 05 ae 1a 00 00 add eax,0x1aae
533: 6a 06 push 0x6
535: 6a 05 push 0x5
537: 6a 04 push 0x4
539: 6a 03 push 0x3
53b: ba 02 00 00 00 mov edx,0x2
540: b9 01 00 00 00 mov ecx,0x1
545: e8 a3 ff ff ff call 4ed <func>
54a: 89 45 fc mov DWORD PTR [ebp-0x4],eax
54d: b8 00 00 00 00 mov eax,0x0
552: c9 leave
553: c3 ret

此时我们看到,使用了寄存器传参。还是从右向左的传参顺序。ecx 传递第一个参数,edx 传递第二个参数。而且使用的内部平衡栈的方式。也就是在被调用者的函数栈帧中。

ret 0x10 的意思就是从栈中弹出 16 个字节,并将控制流返回到调用函数的位置。这也就是内部平衡栈的说法。