C运行时库入口点(初始化库,然后调用程序的主函数) 被声明为 :
int _tmainCRTStartup(void);
而Windows入口点签名实际上是
DWORD CALLBACK RawEntryPoint(void);
CALLBACK
(在x86上)定义为__stdcall
。
int
和DWORD
types是兼容的,所以这不是问题,但为什么_tmainCRTStartup
不必声明__stdcall
?
正如Raymond所说,在这种特殊情况下, stdcall
和cdecl
实例在物理上是相同的(尽管编译器可能不会让int (__stdcall *)(void)
stdcall
int (__stdcall *)(void)
函数指针隐式转换为int (__stdcall *)(void)
cdecl
int (__cdecl *)(void)
)。
再看另一种方式:
呼叫约定是呼叫者和被呼叫者之间在其相互环境中的协议。 每个人(尤其是Windows世界)通常都会谈到cdecl
和stdcall
的基本事情,那就是顺序或参数传递以及清理堆栈的责任。
但是这些协议包含的不仅仅是这些。 它们定义了被调用者应该保存哪些寄存器。 他们定义堆栈的对齐方式(例如GCC和Microsoft x64)。 它们可以包含主叫方和被叫方共享的任何其他内容,这是相当多的。 例如,Microsoft x64调用约定要求调用程序为4个机器字保留空间,即使它们被传递到寄存器中。
问题是这些协议是在每个主叫方和被叫方之间分开制定的。 真。 现代编译器和连接器,当他们知道它是安全的时候,就根据具体情况在呼叫者和被呼叫者之间进行这些协议。 这些可能不是全球公认的呼叫公约,但它们仍然是呼叫者和被呼叫者之间的协议。 (有些人称之为“自定义调用约定”,如下所示: 什么是自定义调用约定?但我更喜欢使用ad-hoc调用约定 。
为了让人们更容易,有一些标准(或多或少)的调用约定设置了一般规则。 例如,而不是说为void x(int a)
推栈上,并为void y(int a, int b)
推b,然后在堆栈上,并为void z(int a, int b, int c)
推c然后b,然后a在堆栈上我们会说“像从堆栈中的右向左推参数”。 顺便说一下,这就是cdecl
所做的。
但在退化的情况下,不同调用约定的实例会解析为调用者和被调用者之间的相同实际协议。 就像二次方程有两个解,除了这两个解都是相同数的退化情况。
PE入口点的实际调用约定是“在以下代码调用时工作预期方式1 ”
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)
(代码来自Windows XP SP3,但通用原则)
您可以调用PE入口点DWORD __stdcall RawEntryPoint(void)
或int __cdecl _tmainCRTStartup(void)
或者甚至可以调用它uint32_t __fastcall FastEntryPoint()
或unsigned long __vectorcall VectorEntryPoint()
如果你想。
所有这些调用约定几乎都是一样的,除了如何接收参数。 没有参数,这并不重要。 在这种情况下,你所看到的是一个文档问题,而不是更多。 他们可以说“返回地址在堆栈后面,所以RET
工作,你应该在EAX
返回一个整数值”。
kernel32!BaseProcessStart
和PE入口点之间的实际调用约定可以用任何这些名称来描述。
1我认为预期的方式在这里意义何在。