68.1 CRT(win32)
程序一开始就从main()函数执行的?事实并非如此。如果我们用IDA或者HIEW打开一个可执行文件,我们可以看到OEP(Original Entry Point)指向了其它代码块。这些代码做了一些维护和准备工作之后再把控制流交给我们的代码。这就是所谓的startup-code或叫CRT code(C RunTime)。
main()函数通过一个数组接收命令行传递过来的参数,环境变量与此类似。通常情况下,传递一个字符串到程序之后,CRT code会用空格来分割它们。CRT code同样也准备了一个envp来存放环境变量。如果是GUI版本的win32程序,入口函数需要使用WinMain()来代替main()函数,它也有自己的参数。
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
);
CRT code同样会准备好它所需要的所有参数。
此外,main()函数的返回值是它的退出码。CRT code将它作为ExitProcess()的参数。
通常,每个编译器都有它自己的CRT code。
下面是MSVC 2008特有的CRT code。
___tmainCRTStartup proc near
var_24 = dword ptr -24h
var_20 = dword ptr -20h
var_1C = dword ptr -1Ch
ms_exc = CPPEH_RECORD ptr -18h
push 14h
push offset stru_4092D0
call __SEH_prolog4
mov eax, 5A4Dh
cmp ds:400000h, ax
jnz short loc_401096
mov eax, ds:40003Ch
cmp dword ptr [eax+400000h], 4550h
jnz short loc_401096
mov ecx, 10Bh
cmp [eax+400018h], cx
jnz short loc_401096
cmp dword ptr [eax+400074h], 0Eh
jbe short loc_401096
xor ecx, ecx
cmp [eax+4000E8h], ecx
setnz cl
mov [ebp+var_1C], ecx
jmp short loc_40109A
loc_401096: ; CODE XREF: ___tmainCRTStartup+18
; ___tmainCRTStartup+29 ...
and [ebp+var_1C], 0
loc_40109A: ; CODE XREF: ___tmainCRTStartup+50
push 1
call __heap_init
pop ecx
test eax, eax
jnz short loc_4010AE
push 1Ch
call _fast_error_exit
pop ecx
loc_4010AE: ; CODE XREF: ___tmainCRTStartup+60
call __mtinit
test eax, eax
jnz short loc_4010BF
push 10h
call _fast_error_exit
pop ecx
loc_4010BF: ; CODE XREF: ___tmainCRTStartup+71
call sub_401F2B
and [ebp+ms_exc.disabled], 0
call __ioinit
test eax, eax
jge short loc_4010D9
push 1Bh
call __amsg_exit
pop ecx
loc_4010D9: ; CODE XREF: ___tmainCRTStartup+8B
call ds:GetCommandLineA
mov dword_40B7F8, eax
call ___crtGetEnvironmentStringsA
mov dword_40AC60, eax
call __setargv
test eax, eax
jge short loc_4010FF
push 8
call __amsg_exit
pop ecx
loc_4010FF: ; CODE XREF: ___tmainCRTStartup+B1
call __setenvp
test eax, eax
jge short loc_401110
push 9
call __amsg_exit
pop ecx
loc_401110: ; CODE XREF: ___tmainCRTStartup+C2
push 1
call __cinit
pop ecx
test eax, eax
jz short loc_401123
push eax
call __amsg_exit
pop ecx
loc_401123: ; CODE XREF: ___tmainCRTStartup+D6
mov eax, envp
mov dword_40AC80, eax
push eax ; envp
push argv ; argv
push argc ; argc
call _main
add esp, 0Ch
mov [ebp+var_20], eax
cmp [ebp+var_1C], 0
jnz short $LN28
push eax ; uExitCode
call $LN32
$LN28: ; CODE XREF: ___tmainCRTStartup+105
call __cexit
jmp short loc_401186
$LN27: ; DATA XREF: .rdata:stru_4092D0
mov eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 401044
mov ecx, [eax]
mov ecx, [ecx]
mov [ebp+var_24], ecx
push eax
push ecx
call __XcptFilter
pop ecx
pop ecx
$LN24:
retn
$LN14: ; DATA XREF: .rdata:stru_4092D0
mov esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 401044
mov eax, [ebp+var_24]
mov [ebp+var_20], eax
cmp [ebp+var_1C], 0
jnz short $LN29
push eax ; int
call __exit
$LN29: ; CODE XREF: ___tmainCRTStartup+135
call __c_exit
loc_401186: ; CODE XREF: ___tmainCRTStartup+112
mov [ebp+ms_exc.disabled], 0FFFFFFFEh
mov eax, [ebp+var_20]
call __SEH_epilog4
retn
在这里我们看到代码调用了GetCommandLineA(),setargv()和setenvp()去填充argc,argv,envp全局变量。
最后,使用这些参数去调用main()函数。
有些函数调用了与自身类似的函数,如heap_init(),ioinit()。
如果你尝试在CRT code代码中使用malloc(),它将异常退出下面的错误:
runtime error R6030
- CRT not initialized
在C++中,全局对象的初始化也同样发生在main()函数执行之前的CRT:51.4.1。
main()函数的返回值传给cexit()或$LN32,后者调用doexit()。
能否摆脱CRT?这个当然,如果你知道你在做什么的话。
MSVC的链接器可以通过/ENTRY选项设置入口函数。
#include <windows.h>
int main()
{
MessageBox (NULL, "hello, world", "caption", MB_OK);
};
让我们用MSVC 2008来编译它。
cl no_crt.c user32.lib /link /entry:main
我们可以获得一个大小为2560字节的runnable.exe。它有一个PE头,调用MessageBox的指令,数据段中有两串字符串,而MessageBox函数导入自user32.DLL。
这个程序能够正常运行,但你不能在main()函数里面使用WinMain()的四个参数。准确点来说你能,但是这些参数并没有在执行的时候准备好。
cl no_crt.c user32.lib /link /entry:main /align:16
它会报一个链接警告:
LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run
我们可以获得一个720字节的exe文件。它可以在Windows 7 x86上正常运行,但是没办法在x64上运行(当你运行它的时候会将先是一条错误信息)。更多的优化可能可以提高执行效率,但如你所见,很快就出现了兼容问题。