64.2 stdcall
该调用方法与cdecl差不多,除了被调用者必须通过RET x指令代替RET指令将ESP指针设置为初始化状态,其中x = arguments number * sizeof(int)
。调用者无需调整栈指针(ESP)。
Listing 64.2: stdcall
push arg3
push arg2
push arg1
call function
function:
... do something ...
ret 12
这种调用方式在win32的标准库无处不在,但win64并不使用该调用方法(具体参见下文win64一节)。
举个例子,我们可以稍微把在91页中8.1的示例代码修改一下,增加一个__stdcall
修饰符。
int __stdcall f2 (int a, int b, int c)
{
return a*b+c;
};
编译出来的结果跟8.2几乎一模一样,但你可以看到它是通过RET 12而不是RET返回的。同时,调用者并没有调整栈指针(ESP)。
因此,很容易通过RETN n指令推导出函数参数的数量(n除以四)。
Listing 64.3: MSVC 2010
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_c$ = 16 ; size = 4
_f2@12 PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
imul eax, DWORD PTR _b$[ebp]
add eax, DWORD PTR _c$[ebp]
pop ebp
ret 12 ; 0000000cH
_f2@12 ENDP
; ...
push 3
push 2
push 1
call _f2@12
push eax
push OFFSET $SG81369
call _printf
add esp, 8
64.2.1 可变参数的函数
printf()系列的函数大概是C/C++里面唯一一系列具有可变参数的函数了,在这些函数的帮助下很容易理清cdecl和stdcall两种调用方式之间的重要区别。让我们先假设编译器知道每个调用printf()函数的参数的个数,无论如何,当我们调用printf()的时候,它已经存在于编译好的MSVCRT.DLL之中(我们讨论的是Windows),并没有任何关于传递多少个参数的信息,剩下的办法就是通过它的格式字符串获取得到参数个数。因此,如果printf()函数是一个stdcall调用方式的函数,它必须通过格式字符串计算参数个数用于恢复栈指针(ESP),这是一种相当危险的情况,程序员的一个错别字就可以导致程序崩溃。因此此类函数使用cdecl调用方式远比使用stdcall调用方式适合。