为什么 C 运行时库中的入口点没有声明为 __stdcall? [英] Why isn't the entry point in the C runtime library declared __stdcall?

查看:29
本文介绍了为什么 C 运行时库中的入口点没有声明为 __stdcall?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

C 运行时库入口点(初始化库,然后调用程序的主函数)声明为:

The C runtime library entry point (which initializes the library and then calls the program's main function) is declared as:

int _tmainCRTStartup(void);

Windows 入口点签名实际上是

DWORD CALLBACK RawEntryPoint(void);

其中 CALLBACK(在 x86 上)定义为 __stdcall.

where CALLBACK (on x86) is defined as __stdcall.

intDWORD 类型是兼容的,所以这不是问题,但是为什么不用声明 _tmainCRTStartup__stdcall ?

The int and DWORD types are compatible, so that isn't a problem, but why doesn't _tmainCRTStartup have to be declared __stdcall ?

推荐答案

正如 Raymond 所说,在这种特殊情况下,stdcallcdecl 的那些实例在物理上是相同的(尽管编译器可能不允许您将 int (__stdcall *)(void) 函数指针隐式转换为 int (__cdecl *)(void)).

As Raymond said, in this particular case those instances of stdcall and cdecl are physically identical (although a compiler might not let you implicitly convert a int (__stdcall *)(void) function pointer to int (__cdecl *)(void)).

换个角度看:

调用约定是调用者和被调用者之间就其相互环境达成的协议.关于 cdeclstdcall,每个人(尤其是在 Windows 世界中)通常谈论的基本事情是顺序或参数传递以及清理堆栈的责任.

Calling conventions are agreements between callers and callees on their mutual environment. The basic thing everyone (esp. in the Windows world) usually talks about with respect to cdecl and stdcall is the order or parameter passing and the responsibility to clean the stack.

但这些协议包含的远不止这些.它们定义了被调用者应该保留哪些寄存器.它们定义堆栈的对齐方式(例如 GCC 和 Microsoft x64).它们可以包括调用者和被调用者之间共享的任何其他内容,这是相当多的.例如,Microsoft x64 调用约定要求调用者为 4 个机器字保留空间,即使它们是在寄存器中传递的.

But these agreements contains a lot more than that. They define which registers should be preserved by the callee. They define the alignment of the stack (GCC and Microsoft x64, for example). They can include anything else that is shared between the caller and the callee, which is quite a lot. For example, the Microsoft x64 calling convention requires the caller to reserve space for 4 machine words, even though they are passed in registers.

问题是这些协议是在每个调用者和他的被调用者之间分别制定的.真的.现代编译器和链接器,当他们知道它是安全的时,会根据具体情况在调用者和被调用者之间达成这些协议.这些可能不是全球公认的调用约定,但它们仍然是调用者和被调用者之间的协议.(有些人称它们为自定义调用约定",例如:什么是自定义调用约定?,但我更喜欢术语 临时调用约定.)

The thing is that these agreement are made separately between each caller and his callee. Really. Modern compilers and linkers, when they know it's safe, make these agreements between callers and callees on a case by case basis. These are perhaps not globally recognized calling conventions, but they are nonetheless agreements between callers and callees. (Some people call them "custom calling conventions", like here: What are custom calling conventions? , but I prefer the term ad-hoc calling conventions.)

为了让人们更轻松,有一些标准(或多或少)调用约定来设置一般规则.例如,不是说 void x(int a) 将 a 压入堆栈,而 void y(int a, int b) 压入 b 然后a 在堆栈上,对于 void z(int a, int b, int c) 将 c 然后 b 然后 a 推入堆栈,我们说从右向左推参数在堆栈上".顺便说一下,这就是 cdecl 所做的,例如.

To make things easier for people, there are a few standard (more or less) calling conventions that set general rules. For example, instead of saying for void x(int a) to push a on the stack, and for void y(int a, int b) to push b and then a on the stack, and for void z(int a, int b, int c) to push c and then b and then a on the stack we say something like "push the arguments from right to left on the stack". Incidentally that's what cdecl does, for example.

但在退化的情况下,不同调用约定的实例会解析为调用者和被调用者之间相同的实际协议.就像二次方程有两个解一样,除了在退化的情况下,这两个解都是相同的数字.

But in degenerate cases it happens that instances of different calling conventions resolve to the same actual agreement between the caller and the callee. Just like quadratic equations have two solutions, except in the degenerate cases where those two solution are both the same number.

PE 入口点的实际调用约定是当被以下代码调用时,以预期的方式工作1:"

The actual calling convention for the PE entry point is "work the expected way1 when called by the following code:"

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,但原理普遍适用.)

(The code is from Windows XP SP3, but the principle applies universally.)

你可以调用 PE 入口点 DWORD __stdcall RawEntryPoint(void)int __cdecl _tmainCRTStartup(void) 或者你甚至可以调用它 uint32_t __fastcall FastEntryPoint()unsigned long __vectorcall VectorEntryPoint() 如果需要.

You can call the PE entry point DWORD __stdcall RawEntryPoint(void) or int __cdecl _tmainCRTStartup(void) or you can even call it uint32_t __fastcall FastEntryPoint() or unsigned long __vectorcall VectorEntryPoint() if you want.

所有这些调用约定几乎相同,除了它们如何接收参数.没有参数也没关系.在这种情况下,您看到的是文档问题,仅此而已.他们可能会说返回地址在堆栈中位于您后面,因此 RET 可以工作,您应该在 EAX 中返回单个整数值".

All those calling conventions are pretty much the same, besides in how they receive parameters. With no parameters it doesn't matter. In this case what you see is a documentation issue and not much more. They could have just as much said "the return address is behind you on the stack so RET works and you should return a single integer value in EAX".

kernel32!BaseProcessStart 和 PE 入口点之间的实际调用约定可以使用任何这些名称来描述.

The actual calling convention between kernel32!BaseProcessStart and the PE entry point can be described in using any of those names.

1 我认为预期方式意味着什么工作在这里很明显.

1 I think what work the expected way means is obvious here.

这篇关于为什么 C 运行时库中的入口点没有声明为 __stdcall?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆