C运行时库入口点(初始化库然后调用程序的主函数)is declared as:
int _tmainCRTStartup(void);
而Windows entry point signature is actually
DWORD CALLBACK RawEntryPoint(void);
其中CALLBACK
(在x86上)定义为__stdcall
。
int
和DWORD
类型兼容,因此这不是问题,但为什么_tmainCRTStartup
不必声明__stdcall
?
答案 0 :(得分:3)
正如Raymond所说,在这种特殊情况下,stdcall
和cdecl
的实例在物理上是相同的(尽管编译器可能不允许您隐式地将int (__stdcall *)(void)
函数指针转换为{{1 }})。
以另一种方式看待它:
呼叫约定是呼叫者和被叫者之间就其共同环境达成的协议。每个人(特别是在Windows世界中)通常谈论int (__cdecl *)(void)
和cdecl
的基本事情是传递的顺序或参数以及清理堆栈的责任。
但这些协议包含的内容远不止于此。它们定义了被调用者应该保留哪些寄存器。它们定义了堆栈的对齐方式(例如GCC和Microsoft x64)。它们可以包含调用者和被调用者之间共享的任何其他东西,这是非常多的。例如,Microsoft x64调用约定要求调用者为4个机器字保留空间,即使它们是在寄存器中传递的。
问题是这些协议是在每个呼叫者和他的被叫者之间单独制定的。真。现代编译器和链接器在知道它是安全的时候,会根据具体情况在呼叫者和被叫者之间达成协议。这些可能不是全球公认的呼叫惯例,但它们仍然是呼叫者和被叫者之间的协议。 (有些人称之为“自定义调用约定”,例如:What are custom calling conventions?,但我更喜欢术语 ad-hoc调用约定。)
为了让人们更轻松,有一些标准(或多或少)调用约定可以设置一般规则。例如,不是说stdcall
推送堆栈,而是void x(int a)
推送b然后推送堆栈,而void y(int a, int b)
推送c然后b和然后在堆栈上我们说“在堆栈中从右向左推送参数”。顺便说一下,这就是void z(int a, int b, int c)
所做的事情。
但是在退化的情况下,不同的调用约定的实例会解析调用者和被调用者之间的相同实际协议。就像二次方程有两个解,除了退化情况,这两个解都是相同的数。
PE入口点的实际调用约定是“在以下代码调用时以预期的方式 1 :”
cdecl
(代码来自Windows XP SP3,但该原则普遍适用。)
您可以拨打PE入口点kernel32!BaseProcessStart:
7c816014 6a0c push 0Ch
7c816016 684060817c push offset kernel32!`string'+0x98 (7c816040)
7c81601b e8b6c4feff call kernel32!_SEH_prolog (7c8024d6)
7c816020 8365fc00 and dword ptr [ebp-4],0
7c816024 6a04 push 4
7c816026 8d4508 lea eax,[ebp+8]
7c816029 50 push eax
7c81602a 6a09 push 9
7c81602c 6afe push 0FFFFFFFEh
7c81602e ff15b013807c call dword ptr [kernel32!_imp__NtSetInformationThread (7c8013b0)]
7c816034 ff5508 call dword ptr [ebp+8]
7c816037 50 push eax
7c816038 e8bb60ffff call kernel32!ExitThread (7c80c0f8)
或DWORD __stdcall RawEntryPoint(void)
,或者如果需要,您甚至可以将其称为int __cdecl _tmainCRTStartup(void)
或uint32_t __fastcall FastEntryPoint()
。
除了接收参数之外,所有这些调用约定几乎相同。没有参数也没关系。在这种情况下,您看到的是文档问题,而不是更多。他们可以同样多地说“返回地址在堆栈后面,所以unsigned long __vectorcall VectorEntryPoint()
有效,你应该在RET
”中返回一个整数值。
EAX
与PE入口点之间的实际调用约定可以使用任何这些名称来描述。
1 我认为预期方式的作用在这里显而易见。