(1)
之前在调试exe时感觉很奇怪,为什么Entry Point并非直接进入到main函数。
举例来说,如果将一段空的C代码build为exe:
void main(){ }
编译环境为:VC6 release。
再将该exe文件进行反汇编,那么从EP开始的代码部分大概形如:
……
一段汇编代码
……
call Main函数
……
另一段汇编代码
……
retn
也就是说, 在执行一个exe文件时,总是要先运行一些指令,才能够开始调用Main函数。同样,当main函数执行完毕后,还需要运行一些指令完成收尾。为了弄清楚main函数调用前这些代码以及main函数执行后的代码,需要从CRT(C RunTime ,C的运行时库)开始研究。
(2)
Visual Studio自带了CRT的源码,VC6中CRT位于“VC98\CRT\SRC”目录。CRT 中的 crt0.c 文件规定了一整套C程序固定的执行流程。在 crt0.c 开头的注释部分有如下描述:
大概意思是,当C Run-Time Library 完成了初始化工作之后,才开始执行用户自定义的main 函数、WinMain 函数。
crt0.c 为了规定C程序执行的流程,定义了函数mainCRTStartup 和WinMainCRTStartup ,这两个函数也被称作启动函数 。它们的作用在函数注释中已经写的很清楚:
mainCRTStartup函数大概形如:
void mainCRTStartup(void){ int mainret; …… __try { …… mainret = main(__argc, __argv, _environ); //在这里调用用户写的main函数 exit(mainret); } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { _exit( GetExceptionCode() ); } }
当Windows系统执行一个C程序时,真正首先执行的是(win)mainCRTStartup函数。mainCRTStartup首先进行了一系列准备工作,例如heap的初始化、IO的初始化、获得命令行参数等等。当所有的准备工作都完成之后,再去调用用户自定义的main函数。最后,执行exit函数退出程序。因此对于exe,(win)mainCRTStartup函数才是真正的Entry point。
另外,crt0.c 中还有相似的函数:wWinMainCRTStartup、wmainCRTStartup,它们是Unicode版本程序的EP,这里可以暂时不用去管。windows为了照顾Unicode程序,很多API都提供了两种版本,一种是针对ANSI字符,还有一种是针对Unicode字符。
这四个函数是放在一起定义的,crt0.c 中的源码如下:
#ifdef _WINMAIN_ /* _WINMAIN_被定义时,表示GUI程序 */
#ifdef WPRFLAG /* WPRFLAG被定义时,表示Unicode字符 */
void wWinMainCRTStartup(
#else
void WinMainCRTStartup(
#endif
#else /* 下面为CUI程序 */
#ifdef WPRFLAG
void wmainCRTStartup(
#else
void mainCRTStartup(
#endif
#endif
void){
……
}
可以根据上面的源代码总结如下:
mainCRTStartup | Console apps | ANSI |
wmainCRTStartup | Console apps | Unicode |
WinMainCRTStartup | Windows apps | ANSI |
wWinMainCRTStartup | Windows apps | Unicode |
(3)
来具体看一下(win)mainCRTStartup函数。下面将mainCRTStartup函数的主要语句摘录了出来,这里去除了一些条件编译的代码,忽略了windows apps(_WINMAIN_)、Unicode版本的程序(WPRFLAG)、多线程(_MT),仅仅分析Console apps。
int mainret; // 获取Win32的版本 _osver = GetVersion(); _winminor = (_osver >> 8) & 0x00FF ; _winmajor = _osver & 0x00FF ; _winver = (_winmajor << 8) + _winminor; _osver = (_osver >> 16) & 0x00FFFF ; // 创建了一个属于该进程的私有堆 if ( !_heap_init(0) ) fast_error_exit(_RT_HEAPINIT); __try { // 初始化低级IO _ioinit(); // 获取命令行缓冲区指针 _acmdln = (char *)GetCommandLineA(); // 获取环境变量指针 _aenvptr = (char *)__crtGetEnvironmentStringsA(); // 设置argv参数 _setargv(); // 设置环境变量 _setenvp(); // 初始化C数据 _cinit(); __initenv = _environ; // 调用main函数 mainret = main(__argc, __argv, _environ); exit(mainret); } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { _exit( GetExceptionCode() ); }
从这段代码可以大体窥视出 C 程序的启动流程:
1.获取WIN32平台的版本
2.调用_heap_init函数创建一个私有堆
3.初始化低级IO
4.获取命令行缓冲区、环境变量的指针
5.设置命令行参数与环境变量
6.初始化C数据
7.调用main函数
8.将main函数的调用结果传入exit 退出程序