11.1.2 入口函数如何实现(3)
这是入口函数的头部。下面的代码出现于该函数的开头,显得杂乱无章。不过其中关键的内容是给一系列变量赋值:
posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA)); posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
GetVersionExA(posvi); _osplatform = posvi->dwPlatformId; _winmajor = posvi->dwMajorVersion; _winminor = posvi->dwMinorVersion; _osver = (posvi->dwBuildNumber) & 0x07fff; if ( _osplatform != VER_PLATFORM_WIN32_NT ) _osver |= 0x08000; _winver = (_winmajor << 8) + _winminor; |
被赋值的这些变量,是VC7里面预定义的一些全局变量,其中_osver和_winver表示操作系统的版本,_winmajor是主版本号,更具体的可以查阅MSDN。这段代码通过调用GetVersionExA(这是一个Windows API)来获得当前的操作系统版本信息,并且赋值给各个全局变量。
为什么这里为posvi分配内存不使用malloc而使用alloca呢?是因为在程序的一开始堆还没有被初始化,而alloca是唯一可以不使用堆的动态分配机制。alloca可以在栈上分配任意大小的空间(只要栈的大小允许),并且在函数返回的时候会自动释放,就好像局部变量一样。
由于没有初始化堆,所以很多事情没法做,当务之急是赶紧把堆先初始化了:
if ( !_heap_init(0) ) fast_error_exit(_RT_HEAPINIT); |
这里使用_heap_init函数对堆(heap)进行了初始化,如果堆初始化失败,那么程序就直接退出了。
__try { if ( _ioinit() < 0 ) _amsg_exit(_RT_LOWIOINIT); _acmdln = (char *)GetCommandLineA(); _aenvptr = (char *)__crtGetEnvironmentStringsA(); if ( _setargv() < 0 ) _amsg_exit(_RT_SPACEARG); if ( _setenvp() < 0 ) _amsg_exit(_RT_SPACEENV); initret = _cinit(TRUE); if (initret != 0) _amsg_exit(initret); __initenv = _environ; mainret = main(__argc, __argv, _environ); _cexit(); } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { mainret = GetExceptionCode(); _c_exit(); } /* end of try - except */ return mainret; } |
在这里是一个Windows的SEH的try-except块,里面做了什么呢?首先使用_ioinit函数初始化了I/O,接下来这段代码调用了一系列函数进行各种初始化,包括:
_setargv:初始化main函数的argv参数。
_setenv:设置环境变量。
_cinit:其他的C库设置。
在最后,可以看到函数调用了main函数并获得了其返回值。try-except块的except部分是最后的清理阶段,如果try块里的代码发生异常,则在这里进行错误处理。最后退出并返回main的返回值。
try-except块
try-except块是Windows结构化异常处理机制SEH的一部分。try-except块的使用方法如下:
_start -> __libc_start_main -> exit: void exit (int status) { while (__exit_funcs != NULL) { ... __exit_funcs = __exit_funcs->next; } ... _exit (status); } |
其中__exit_funcs是存储由__cxa_atexit和atexit注册的函数的链表,而这里的这个while循环则遍历该链表并逐个调用这些注册的函数,由于其中琐碎代码过多,这里就不具体列出了。最后的_exit函数由汇编实现,且与平台相关,下面列出i386的实现:
__try { code 1 } __except(...) { code 2 } |
当code 1出现异常(段错误等)的时候,except部分的code 2会执行以异常处理。更为详细的信息请查阅MSDN。
总结一下,这个mainCRTStartup的总体流程就是:
(1)初始化和OS版本有关的全局变量。
(2)初始化堆。
(3)初始化I/O。
(4)获取命令行参数和环境变量。
(5)初始化C库的一些数据。
(6)调用main并记录返回值。
(7)检查错误并将main的返回值返回。
事实上还是MSVC的入口函数的思路较为清晰。在第13章里,我们将仿照VC入口函数的思路实现一个Linux下的简易入口函数。
Q&A
Q:msvc的入口函数使用了alloca,它是如何实现的。
A:alloca函数的特点是它能够动态地在栈上分配内存,在函数退出时如同局部变量一样自动释放。结合之前我们介绍的函数标准进入和退出指令序列就知道,函数退出时的退栈操作是直接将ESP的值赋为EBP的值。因此不管在函数的执行过程中ESP减少了多少,最后也能够成功地将函数执行时分配的所有栈空间回收。在这个基础上,alloca的实现就非常简单,仅仅是将ESP减少一定数值而已。
Q:为什么MSVC的Win32程序的入口使用的是WinMain?
A:WinMain和main一样,都不是程序的实际入口。MSVC的程序入口是同一段代码,但根据不同的编译参数被编译成了不同的版本。不同版本的入口函数在其中会调用不同名字的函数,包括main/wmain/WinMain/wWinMain等。
【责任编辑:
云霞 TEL:(010)68476606】