Why isn't the entry point in the C runtime lib

2020-05-09 19:05发布

问题:

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

int _tmainCRTStartup(void);

whereas the Windows entry point signature is actually

DWORD CALLBACK RawEntryPoint(void);

where CALLBACK (on x86) is defined as __stdcall.

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

回答1:

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)).

Look at it another way:

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.

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.)

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.

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)

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

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.

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".

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


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