一、函数调用的基本步骤
函数调用大致包括以下几个步骤。
(1)参数入栈:将参数从右向左依次压入系统栈中。
(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
(4)栈帧调整:具体包括:
保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。
将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。
给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。
二、函数调用约定:
调用约定的声明 |
参数入栈顺序 |
恢复栈平衡的位置 |
__cdecl |
右→左 |
母函数 |
__fastcall |
右→左 |
子函数 |
__stdcall |
右→左 |
子函数 |
三、具体的表现
(1) __cdecl
函数本身不清理堆栈,调用者负责清理堆栈。由于这种变化,C调用约定允许函数的参数的个数是不固定的
(2) __fastcall
函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈
被调用函数清理堆栈
(3) __stdcall
参数从右向左压入堆栈
函数自身修改堆栈
五、其他调用
(1) thiscall
thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个
this指针,因此必须特殊处理。
Microsoft Visual C++提供了thiscall调用约定,它把this保存到ECX寄存器中,并由非静态函数负责从堆栈中清除参数。
GNU g++编译器把this指针看作是动态成员函数的暗含的第一个参数,并使用cdecall约定中的方法来传递参数。
表现:
参数从右向左入栈
如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。
对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈
对于thiscall,在C++的类中可以定义,那么不管在VC中如何设置函数调用约定,下面的表现都是一定的,即:
func函数有子函数进行清栈,而func1函数有母函数进行清栈。
class TestCls { public: TestCls(int a) { m_iData = a; } int func(int a, int b, int c) { int e = m_iData + a + b + c; return e; } CString func1(CString strFMT, ...) { CString strTemp; va_list args; va_start(args, strFMT); strTemp.FormatV(strFMT, args); va_end(args); return strTemp; } private: int m_iData; };
要想把所有的调用约定都写完,就需要一本单独的书来完成了。调用约定一般指的是语言、编译器和CPU方面的,当你遇到一些很少见的
编译器编译出的代码时,你就需要好好的研究一下了。在一些情况下需要特别提到:优化的代码,自定义汇编语言代码,和系统调用。
当一个函数被输出供其它程序员使用时(如库函数),让这些函数遵循人们熟知的调用约定是很重要的,这样可以是程序员很快的掌握函数
接口。另一方面,如果一个函数仅仅是为了内部程序使用的,那这个函数的调用约定要在程序内部为人所熟知。因此好的编译器会代替某些调用
约定来产生运行速度更快的代码。例如在使用Microsoft Visual C++的/GL选项或者使用GNU gcc/g++的regparm关键字时,就会出现这种情况。
当程序员在使用汇编语言时,他又完全的控制权来决定函数的参数如何传递。汇编语言程序员可以用任何他们认为合理的方式来传递参数,
除非他们想让自己写的函数被其它程序员引用。传统的汇编语言一般在obfuscation routines和shellcode中见到。
系统调用是用来请求一个系统服务的特殊的函数调用方式。系统调用通常会将操作系统从用户模式切换为内核模式,以使操作系统内核提供
用户所请求的服务。不同操作系统和不同的CPU的系统调用的方式也是不同的。