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()函数,它也有自己的参数。

  1. int CALLBACK WinMain(
  2. _In_ HINSTANCE hInstance,
  3. _In_ HINSTANCE hPrevInstance,
  4. _In_ LPSTR lpCmdLine,
  5. _In_ int nCmdShow
  6. );

CRT code同样会准备好它所需要的所有参数。

此外,main()函数的返回值是它的退出码。CRT code将它作为ExitProcess()的参数。

通常,每个编译器都有它自己的CRT code。

下面是MSVC 2008特有的CRT code。

  1. ___tmainCRTStartup proc near
  2. var_24 = dword ptr -24h
  3. var_20 = dword ptr -20h
  4. var_1C = dword ptr -1Ch
  5. ms_exc = CPPEH_RECORD ptr -18h
  6. push 14h
  7. push offset stru_4092D0
  8. call __SEH_prolog4
  9. mov eax, 5A4Dh
  10. cmp ds:400000h, ax
  11. jnz short loc_401096
  12. mov eax, ds:40003Ch
  13. cmp dword ptr [eax+400000h], 4550h
  14. jnz short loc_401096
  15. mov ecx, 10Bh
  16. cmp [eax+400018h], cx
  17. jnz short loc_401096
  18. cmp dword ptr [eax+400074h], 0Eh
  19. jbe short loc_401096
  20. xor ecx, ecx
  21. cmp [eax+4000E8h], ecx
  22. setnz cl
  23. mov [ebp+var_1C], ecx
  24. jmp short loc_40109A
  25. loc_401096: ; CODE XREF: ___tmainCRTStartup+18
  26. ; ___tmainCRTStartup+29 ...
  27. and [ebp+var_1C], 0
  28. loc_40109A: ; CODE XREF: ___tmainCRTStartup+50
  29. push 1
  30. call __heap_init
  31. pop ecx
  32. test eax, eax
  33. jnz short loc_4010AE
  34. push 1Ch
  35. call _fast_error_exit
  36. pop ecx
  37. loc_4010AE: ; CODE XREF: ___tmainCRTStartup+60
  38. call __mtinit
  39. test eax, eax
  40. jnz short loc_4010BF
  41. push 10h
  42. call _fast_error_exit
  43. pop ecx
  44. loc_4010BF: ; CODE XREF: ___tmainCRTStartup+71
  45. call sub_401F2B
  46. and [ebp+ms_exc.disabled], 0
  47. call __ioinit
  48. test eax, eax
  49. jge short loc_4010D9
  50. push 1Bh
  51. call __amsg_exit
  52. pop ecx
  53. loc_4010D9: ; CODE XREF: ___tmainCRTStartup+8B
  54. call ds:GetCommandLineA
  55. mov dword_40B7F8, eax
  56. call ___crtGetEnvironmentStringsA
  57. mov dword_40AC60, eax
  58. call __setargv
  59. test eax, eax
  60. jge short loc_4010FF
  61. push 8
  62. call __amsg_exit
  63. pop ecx
  64. loc_4010FF: ; CODE XREF: ___tmainCRTStartup+B1
  65. call __setenvp
  66. test eax, eax
  67. jge short loc_401110
  68. push 9
  69. call __amsg_exit
  70. pop ecx
  71. loc_401110: ; CODE XREF: ___tmainCRTStartup+C2
  72. push 1
  73. call __cinit
  74. pop ecx
  75. test eax, eax
  76. jz short loc_401123
  77. push eax
  78. call __amsg_exit
  79. pop ecx
  80. loc_401123: ; CODE XREF: ___tmainCRTStartup+D6
  81. mov eax, envp
  82. mov dword_40AC80, eax
  83. push eax ; envp
  84. push argv ; argv
  85. push argc ; argc
  86. call _main
  87. add esp, 0Ch
  88. mov [ebp+var_20], eax
  89. cmp [ebp+var_1C], 0
  90. jnz short $LN28
  91. push eax ; uExitCode
  92. call $LN32
  93. $LN28: ; CODE XREF: ___tmainCRTStartup+105
  94. call __cexit
  95. jmp short loc_401186
  96. $LN27: ; DATA XREF: .rdata:stru_4092D0
  97. mov eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 401044
  98. mov ecx, [eax]
  99. mov ecx, [ecx]
  100. mov [ebp+var_24], ecx
  101. push eax
  102. push ecx
  103. call __XcptFilter
  104. pop ecx
  105. pop ecx
  106. $LN24:
  107. retn
  108. $LN14: ; DATA XREF: .rdata:stru_4092D0
  109. mov esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 401044
  110. mov eax, [ebp+var_24]
  111. mov [ebp+var_20], eax
  112. cmp [ebp+var_1C], 0
  113. jnz short $LN29
  114. push eax ; int
  115. call __exit
  116. $LN29: ; CODE XREF: ___tmainCRTStartup+135
  117. call __c_exit
  118. loc_401186: ; CODE XREF: ___tmainCRTStartup+112
  119. mov [ebp+ms_exc.disabled], 0FFFFFFFEh
  120. mov eax, [ebp+var_20]
  121. call __SEH_epilog4
  122. retn

在这里我们看到代码调用了GetCommandLineA(),setargv()和setenvp()去填充argc,argv,envp全局变量。

最后,使用这些参数去调用main()函数。

有些函数调用了与自身类似的函数,如heap_init(),ioinit()。

如果你尝试在CRT code代码中使用malloc(),它将异常退出下面的错误:

  1. runtime error R6030
  2. - CRT not initialized

在C++中,全局对象的初始化也同样发生在main()函数执行之前的CRT:51.4.1。

main()函数的返回值传给cexit()或$LN32,后者调用doexit()。

能否摆脱CRT?这个当然,如果你知道你在做什么的话。

MSVC的链接器可以通过/ENTRY选项设置入口函数。

  1. #include <windows.h>
  2. int main()
  3. {
  4. MessageBox (NULL, "hello, world", "caption", MB_OK);
  5. };

让我们用MSVC 2008来编译它。

  1. cl no_crt.c user32.lib /link /entry:main

我们可以获得一个大小为2560字节的runnable.exe。它有一个PE头,调用MessageBox的指令,数据段中有两串字符串,而MessageBox函数导入自user32.DLL。

这个程序能够正常运行,但你不能在main()函数里面使用WinMain()的四个参数。准确点来说你能,但是这些参数并没有在执行的时候准备好。

  1. cl no_crt.c user32.lib /link /entry:main /align:16

它会报一个链接警告:

  1. LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run

我们可以获得一个720字节的exe文件。它可以在Windows 7 x86上正常运行,但是没办法在x64上运行(当你运行它的时候会将先是一条错误信息)。更多的优化可能可以提高执行效率,但如你所见,很快就出现了兼容问题。