调用约定(Calling Convertions)定义了程序中调用函数的方式,决定了在函数调用时数据在堆栈中的组织方式。如参数的压栈顺序和堆栈清理工作。这里结合VC 6.0,根据具体的小程序讲解三种调用约定:cdecl,stdcall,fastcall。(还有一些其他的调用约定,俺就浅尝辄止了)
首先在VC6.0中创建一个项目,程序代码如下:
1#include <stdio.h>
2#include <string.h>
3
4void
5age_print(
6 char* name,
7 int age
8)
9{
10 printf("%s is %d years old.\n", name, age);
11}
12
13int main(void)
14{
15 char name[128];
16 int age;
17
18 strcpy(name, "joe");
19 age = 100;
20
21 age_print(name, age);
22
23 return 0;
24}
如何在VC6中设置调用约定呢?Project->Setting->C/C++,在category中选择Code Generation。可以看到在下面出现一个下拉框,名称是Calling convention,即可选择调用约定的方式,支持上面讲的三种方式:cdecl,stdcall,fastcall。
我们通过VC的调试方式看每种调用约定时堆栈的情况。
1、cdecl
此种方式下的汇编代码如下所示:
118: strcpy(name, "joe");
20040108E push offset string "joe" (00422038)
300401093 lea eax,[ebp-80h]
400401096 push eax
500401097 call strcpy (004011b0)
60040109C add esp,8
719: age = 100;
80040109F mov dword ptr [ebp-84h],64h
920:
1021: age_print(name, age);
11004010A9 mov ecx,dword ptr [ebp-84h]
12004010AF push ecx
13004010B0 lea edx,[ebp-80h]
14004010B3 push edx
15004010B4 call @ILT+0(_age_print) (00401005)
16004010B9 add esp,8
由strcpy(name, "joe");对应的汇编代码,可见name的值为ebp-80h;
由age=100;对应的汇编代码可见age的地址为ebp-84h。
执行到age_print(name,age)时,对应的汇编代码是首先push age,然后push name,由此可见cdecl方式下,参数是从右到左进行进栈的。最后通过call调用age_print,age_print对应的汇编语言具体如下:
14: void
25: age_print(
36: char* name,
47: int age
58: )
69: {
700401020 push ebp
800401021 mov ebp,esp
900401023 sub esp,40h
1000401026 push ebx
1100401027 push esi
1200401028 push edi
1300401029 lea edi,[ebp-40h]
140040102C mov ecx,10h
1500401031 mov eax,0CCCCCCCCh
1600401036 rep stos dword ptr [edi]
1710: printf("%s is %d years old.\n", name, age);
1800401038 mov eax,dword ptr [ebp+0Ch]
190040103B push eax
200040103C mov ecx,dword ptr [ebp+8]
210040103F push ecx
2200401040 push offset string "%s is %d years old.\n" (0042201c)
2300401045 call printf (004010f0)
240040104A add esp,0Ch
2511: }
260040104D pop edi
270040104E pop esi
280040104F pop ebx
2900401050 add esp,40h
3000401053 cmp ebp,esp
3100401055 call __chkesp (00401170)
320040105A mov esp,ebp
330040105C pop ebp
340040105D ret
由最后可以看到直接执行了ret,这说明age_print函数本身没有进行堆栈的清理工作,同时,我们可以在main的汇编代码中看到,在执行了call之后,执行了add esp,8指令,这说明清理了两个参数的空间,由此可见cdecl的堆栈清理工作是由调用函数进行的。
由此我们总结出cdecl调用约定的规则:参数进栈顺序是从右到左的,堆栈的清理工作是由调用者进行清理的。
对于参数的进栈顺序,在《Reversing Secrets of Reverse Engineering》中作者是在APPEND C中这么描述的,
The first parameter is pushed onto the stack first, and the last parameter is pushed last.
但是结果和作者所说相反,查了下wiki:
The cdecl calling convention is used by many C systems for the x86 architecture.[1] In cdecl, function parameters are pushed on the stack in a right-to-left order.
http://en.wikipedia.org/wiki/X86_calling_conventions#cdecl
好吧,这个问题到此结束。
2、fastcall
fastcall顾名思义是快速调用的意思。为什么叫这个名字?让我们来看相应的汇编代码就了然了。直接给出main函数调用age_print对应的汇编代码:
1 21: age_print(name, age);
2 004010A9 mov edx,dword ptr [ebp-84h]
3 004010AF lea ecx,[ebp-80h]
4 004010B2 call @ILT+10(_age_print) (0040100f)
由此可见,参数并没有进行压栈操作,而只是传递给了edx和ecx寄存器,直接使用寄存器进行存储参数,大家应该明白为什么fast了吧,fastcall的约定通常是使用ecx和edx分别接受第一个和第二个参数。
对于stdcall,有兴趣的可以自己验证下,呵呵,就写这么多吧。