64.2 stdcall

该调用方法与cdecl差不多,除了被调用者必须通过RET x指令代替RET指令将ESP指针设置为初始化状态,其中x = arguments number * sizeof(int)。调用者无需调整栈指针(ESP)。

Listing 64.2: stdcall

  1. push arg3
  2. push arg2
  3. push arg1
  4. call function
  5. function:
  6. ... do something ...
  7. ret 12

这种调用方式在win32的标准库无处不在,但win64并不使用该调用方法(具体参见下文win64一节)。

举个例子,我们可以稍微把在91页中8.1的示例代码修改一下,增加一个__stdcall修饰符。

  1. int __stdcall f2 (int a, int b, int c)
  2. {
  3. return a*b+c;
  4. };

编译出来的结果跟8.2几乎一模一样,但你可以看到它是通过RET 12而不是RET返回的。同时,调用者并没有调整栈指针(ESP)。

因此,很容易通过RETN n指令推导出函数参数的数量(n除以四)。

Listing 64.3: MSVC 2010

  1. _a$ = 8 ; size = 4
  2. _b$ = 12 ; size = 4
  3. _c$ = 16 ; size = 4
  4. _f2@12 PROC
  5. push ebp
  6. mov ebp, esp
  7. mov eax, DWORD PTR _a$[ebp]
  8. imul eax, DWORD PTR _b$[ebp]
  9. add eax, DWORD PTR _c$[ebp]
  10. pop ebp
  11. ret 12 ; 0000000cH
  12. _f2@12 ENDP
  13. ; ...
  14. push 3
  15. push 2
  16. push 1
  17. call _f2@12
  18. push eax
  19. push OFFSET $SG81369
  20. call _printf
  21. add esp, 8

64.2.1 可变参数的函数

printf()系列的函数大概是C/C++里面唯一一系列具有可变参数的函数了,在这些函数的帮助下很容易理清cdecl和stdcall两种调用方式之间的重要区别。让我们先假设编译器知道每个调用printf()函数的参数的个数,无论如何,当我们调用printf()的时候,它已经存在于编译好的MSVCRT.DLL之中(我们讨论的是Windows),并没有任何关于传递多少个参数的信息,剩下的办法就是通过它的格式字符串获取得到参数个数。因此,如果printf()函数是一个stdcall调用方式的函数,它必须通过格式字符串计算参数个数用于恢复栈指针(ESP),这是一种相当危险的情况,程序员的一个错别字就可以导致程序崩溃。因此此类函数使用cdecl调用方式远比使用stdcall调用方式适合。