语言调用规范是指进行一次函数调用所采用的传递参数的方法,返回值的处理以及调 用堆栈的清理。Microsoft C/C++ 语言中采用了五种调用规范,分别是__cdecl, __stdcall, __fastcall,thiscall和nake每一中调用规范都是利用eax作为返回值,如果 函数返回值是64位的,则利用edx:eax对来返回值。Nake调用规范非常的灵活,足以独立 的一篇文章描述,这里就不再描述nake调用规范。下表列出了前面四种规范调用的特点:
关键字 | 堆栈清理者 | 参数传递顺序 |
__cdecl | 调用者 | 从右至左 |
__stdcall | 被调用者 | 从右至左 |
__fastcall | 被调用者 | 从右至左,前两个参数由寄存器ecx,edx传递 |
thiscall | 被调用者或者调用者 | 从右至左 |
__cdecl 最大好处在于由于是调用者清理栈,它可以处理可变参数,缺点则在于它增 加了程序的大小,因为在每个调用返回的时候,需要多执行一条清理栈的指令。
__stdcall 是在windows程序设计中出现的最多的调用规则,所有的不可变参数的API 调用都使用这个规则。
__fastcall 在windows内核设计中被广泛的使用,由于两个参数由寄存器直接传递, 采用这种规则的函数效率要比以上两种规则高。
thiscall是C++成员函数的默认调用规范,编译期间,这种调用会根据函数是否支持可 变参数表来决定采用什么方式清理堆栈。如果成员函数不支持可变参数,那么它就是用参 数入栈,ecx保存this指针的方式进行调用,如果成员函数支持可变参数,那么它的调用 和__cdecl类似,唯一不同的是将this指针最后压入栈中进行传递。
调用者和被调用者必须采用同样的规则才能保证程序的正常执行,曾经看到很多程序 员犯的错误就是由于调用规范的不一样,致使程序异常,比如:
DWORD ThreadFunc(LPVOID lpParam)
{
//…
}
CreateThread(..,(LPTHREAD_START_ROUTINE)ThreadFunc, …);
如果在编译期间没有指定编译选项/Gz(指定未指明调用规范的函数采用__stdcall方 式),那么编译器自动将ThreadFunc处理成__cdecl调用规范(/Gd),这样可能在线程开 始的时候正常执行,然而退出的时候由于堆栈没有正常清理,造成访问违例或者非法指令 错误。
以上说了很多清理栈的问题,那么为什么清理栈很重要呢。堆栈是线程相关的,也就 是说每一个线程含有一个堆栈,这个堆栈上保存了局部变量,调用返回地址等很多线程相 关的数据,这也是为什么独立运行的线程可以调用同样一个函数而互不干扰的原因。堆栈 的特点恐怕大家已经非常熟悉了,那么根据上面的每一种调用,我给出一个简单的图示来 说明清理堆栈的重要性,以及为什么上面的例子代码会出错。
图一 这是线程堆栈在运行的时候的样子