(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 退出程序
本文详细解析了C程序启动过程中的关键步骤,包括初始化运行时环境、创建私有堆、设置命令行参数等,并深入探讨了mainCRTStartup函数的作用。
1490

被折叠的 条评论
为什么被折叠?



