第10章 函数调用约定

1. 函数调用约定

Calling Convention ,它是对函数调用时如何传递参数的一种约定.调用函数前先要把参数压入栈然后再传递给函数.栈就是定义在进程中的一段内存空间,向下(低地址方向)扩展,且其大小被记录在PE头中.也就是说,进程运行时确定栈内存的大小(与malloc/new动态分配内存不同).

  • 函数执行完毕后,栈中的参数如何处理?
    由于只是临时使用存储在栈中的值,即使不再使用,清除工作也会浪费CPU资源.下一次再向栈存入其他值时,原有值会被自然覆盖掉,并且栈内存是固定的,所以既不能也没必要释放内存.

  • 函数执行完毕后,ESP值如何变化?
    ESP值要恢复到函数调用之前,这样可引用的栈大小才不会减缩.栈内存是固定的,ESP用来指定栈的当前位置,若ESP指向栈底,则无法再使用该栈.函数调用后如何处理ESP,这就是函数用来约定要解决的问题.主要的函数调用约定如下.

    • cdecl
    • stdcall
    • fastcall

术语说明
调用者 == 调用函数的一方.
被调用者 == 被调用的函数.
比如在 main()函数中调用printf()函数时,调用者为main(),被调用者为printf().

1.1 cdecl

cdecl是主要在C语言中使用的方式,调用者负责处理栈.

1
2
3
4
5
6
7
8
9
10
11
#include "stdio.h"
int add(int a, int b)
{
return (a + b);
}
int main(int argc, char* argv[])
{
return add(1, 2);
}

用OllyDbg调试cdecl.exe文件,从图中可以看到401013~40101C地址间的代码可以发现,add()函数的参数1,2与逆序方式压入栈,调用add()函数(401000)后,使用 ADD ESP,8命令整理栈.调用者main()函数直接清理其压入栈的函数参数,这样的方式就是cdecl.
cdecl.exe文件

提示
cdecl方式的好处在于,它可以像C语言中的printf()函数一样,向被调用函数传递长度可变的参数.这种长度可变的参数在其他调用约定中很难实现.

1.2 stdcall

stdcall方式常用语Win 32 API,该方式由被调用者清理栈.因C语言中默认的函数调用方式为cdecl.若想使用stdcall方式编译源码,只能使用_stdcall关键字即可.

1
2
3
4
5
6
7
8
9
10
11
#include "stdio.h"
int _stdcall add(int a, int b)
{
return (a + b);
}
int main(int argc, char* argv[])
{
return add(1, 2);
}

用OllyDbg调试stdcall.exe文件,从图中可以看到,在main()函数中调用add()函数后,省略了清理栈的代码(ADD ESP,8).

栈的清理工作由add()函数中最后(40100A)的 RETN 8 命令来执行. RETN 8 命令的含义是 RETN + POP8字节,即返回后使用ESP增加指定空间.

stdcall.exe
stdcall方式的好处在于,被调用者函数内部存在着栈清理代码,与每次调用函数时都要用ADD ESP,XXX命令的cdecl命令相比,代码尺寸更小.虽然 Win32 API 是使用C语言编写的,但其采用的方式是stdcall方式.

1.3 fastcall

fastcall方式与stdcall方式基本类似,但该方式通常会使用寄存器(而非栈内存)去传递那些需要传递给函数的部分参数.若某函数有4个参数,则前两个参数分别使用ECX,EDX寄存器传递.

fasgcall方式的优势在于可以实现对函数的快速调用(因为从CPU的立场看,访问寄存器的速度要远比内存快得多.)单从函数调用本身来看,fastcall方式非常快,但是有时需要额外的系统分销来管理ECX,EDX寄存器.