调用约定阐释了程序中函数的调用方式。当一个调用约定形成,我们需要讨论的是被调用的函数是如何获取数据(例如参数),以及这些数据在堆栈中是如何存放的。对于逆向工程来说,深入了解调用约定是很有必要的。因为在逆向工程中会经常遇见不同的调用约定。而且,确定一个函数的调用约定在逆向工程中对于你理解函数也有比较好的帮助。
在我们讨论不同的调用约定之前,先了解一些基础的函数调用指令:CALL和RET。CALL指令先将当前指令指针值(它实际上存储的是CALL语句之后的那条指令地址)压入堆栈,然后通过使用一条无条件转移指令(jmp指令)转移到新的代码段地址。(其实这里也就是进入了调用函数的内部,从第一条指令开始执行)。
RET指令是和CALL指令相对应的。作为最后一条指令,基本上出现在每个函数的结尾。RET将地址(先前被CALL指令存储的)弹出堆栈,并存放到EIP(指令指针寄存器)中。然后从该地址开始继续执行。
cdecl 调用约定是C和C++标准的调用约定。它的独特之处就是允许函数接收动态数量的参数。因为它是按从右至左的顺序压参数入栈,由于是调用者负责把参数弹出栈,所以可以给函数定义个数不定的参数,如printf函数。
逆向工程中鉴别是否为cdecl调用相当简单。任何有着一个或多个参数,同时以简单的,没有其他操作数的RET结尾的函数都很可能是采用cdecl调用约定的函数。
对于“C”函数或者变量,修饰名是在函数名前加下划线。对于“C++”函数,有所不同。如函数void test(void)的修饰名是_test;对于不属于一个类的“C++”全局函数,修饰名是?test@@ZAXXZ。
stdcall调用约定按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。
对于“C”函数或者变量,修饰名以下划线为前缀,然后是函数名,然后是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是_func@12。对于“C++”函数,则有所不同。所有的Win32 API函数都遵循该约定。
stdcall调用约定的函数通常是用RET指令来清理堆栈。RET指令能够选择性的接受一个操作数,这个操作数也就是在被调函数返回其主函数之前所使用的堆栈字节数。这也就意味着在stdcall调用约定的函数中传递给RET的操作数通常也就暴露出了函数参数的个数。通过字节数/4也就得到了函数所接受到的参数个数。无论是在逆向工程中判定是否函数是使用的stdcall调用约定,还是确定一个函数有多少参数,这都是一个非常好的提示。
头两个DWORD类型或者占更少字节的参数被放入ECX和EDX寄存器,其他剩下的参数按从右到左的顺序压入栈。由被调用者把参数弹出栈。
对于“C”函数或者变量,修饰名以“@”为前缀,然后是函数名,接着是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是@func@12。对于“C++”函数,有所不同。
fastcall 起初是微软的一种特殊的调用约定方式。但是现在已经被绝大多数的编译器所支持。未来的编译器可能使用不同的寄存器来存放参数。
仅仅应用于“C++”成员函数。this指针存放于ECX寄存器,参数从右到左压栈。thiscall不是关键词,因此不能被程序员指定。
当一个C++类方法的函数所接受的参数个数固定的时候,Microsoft和Inter编译器会用这种调用约定。
一种快速识别这种约定调用的技巧是:函数调用之前,使用这种调用约定的函数指令流会将一个相关的有效指针保存到ecx中,同时将参数压入堆栈,但是没有使用edx寄存器。这是因为任何一个C++类方法都必须接受一个类指针(我们称作this指针)并且会经常用到该指针,编译器使用这种高效的技巧来传递和存储这个特殊的元素。
对于参数个数不确定的类方法,编译器通常会使用cdecl调用约定,并把this指针作为第一个参数首先压入堆栈中。
采用cdecl,stdcall,fastcall或thiscall的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。
naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用,如下:
__declspec( naked ) int func( formal_parameters ) { // Function body }
原来的一些调用约定可以不再使用。它们被定义成调用约定_stdcall或者_cdecl。例如:
#define CALLBACK __stdcall #define WINAPI __stdcall #define WINAPIV __cdecl #define APIENTRY WINAPI #define APIPRIVATE __stdcall #define PASCAL __stdcall
"C"或者"C++"函数在内部(编译和链接)通过修饰名识别(Decoration name)
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的字节数,格式为_functionname@number,例如:function(int a, int b),其修饰名为:_function@8
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
__fastcall调用约定在输出函数名前加上一个"@"符号,后面也是一个"@"符号和其参数的字节数,格式为@functionname@number。
1)、以"?"标识函数名的开始,后跟函数名;
2)、函数名后面以"@@YG"标识参数表的开始,后跟参数表;
3)、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;
4)、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5)、参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。
其格式为"?functionname@@YG*****@Z"或"?functionname@@YG*XZ",例如
int Test1(char *var1,unsigned long)----?Test1@@YGHPADK@Z
void Test2()-----“?Test2@@YGXXZ”
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。
VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.