mainCRTStartup

本文探讨了操作系统加载程序后的入口点及初始化过程,介绍了VC++环境下控制台和GUI程序的不同入口函数,以及如何通过链接器选项控制程序行为。还深入解析了mainCRTStartup的工作原理和全局变量初始化顺序的控制方法。

转自:http://blog.sina.com.cn/s/blog_53c1950a01011vch.html

操作系统装载应用程序后,做完初始化工作就转到程序的入口点执行。程序的默认入口点由连接程序设置, 不同的连接器选择的入口函数也不尽相同。在VC++下,连接器对控制台程序设置的入口函数是 mainCRTStartup,mainCRTStartup 再调用main 函数;对图形用户界面(GUI)程序设置的入口函数是 WinMainCRTStartup,WinMainCRTStartup 调用你自己写的 WinMain 函数。具体设置哪个入口点是由连接器的“/subsystem:”选项确定的,它告诉操作系统如何运行编译生成的.EXE文件。可以指定四种方式:CONSOLE|WINDOWS|NATIVE|POSIX。如果这个选项参数的值为 WINDOWS,则表示该应用程序运行时不需要控制台,有关连接器参数选项的详细说明请参考 MSDN 库。

以下四种组合,可以实现console和windows模式的混合,可以达到不弹出DOS窗口的效果,也可以达到在Windows程序中向控制台输出printf信息的目的。
#pragma comment( linker, "/subsystem:windows /entry:WinMainCRTStartup" )
#pragma comment( linker, "/subsystem:windows /entry:mainCRTStartup" )
#pragma comment( linker, "/subsystem:console /entry:mainCRTStartup" )
#pragma comment( linker, "/subsystem:console /entry:WinMainCRTStartup" )

int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR      lpCmdLine,
int        nCmdShow)
{
      // ...
}

int main(void)
{
      // ...
}

另外:

C或者C++语言,明面上的入口函数是main(argc,argv),或者tmain、wmain、WinMain等等。

进一步,很容易获知,是C Runtime的startup代码中的void mainCRTStartup(void)函数,调用了编程者写的main函数。这个函数定义在Visual C++安装目录的crt\src\目录下的某个.c文件中(视VC++的版本不同,存放的文件也不同)。它在执行一些初始化操作,如获取命令行参数、获取环境变量值、初始化全局变量、初始化io的所需各项准备之后,调用main(argc,argv)。main函数返回后,mainCRTStartup还需要调用全局变量的析构函数或者atexit()所登记的一些函数。往深里说,是在链接生成可执行文件时,告诉链接器这个可执行文件的entry就是mainCRTStartup。当然,编程者也可以在链接时的命令行或者在源程序中指定本程序的entry,例如:
#pragma comment( linker, "/subsystem:windows /entry:WinMainCRTStartup" )
从而在可执行文件执行时,操作系统创建进程与主线程后,就从mainCRTStartup开始执行主线程。

mainCRTStartup所做的初始化准备工作,例如获取命令行参数、获取环境变量值,是通过调用相应的Windows系统调用来实现的。但是,初始化全局变量,就值得多思考了。众多的全局变量按照什么顺序初始化,按照什么顺序销毁?编程者如何指定全局变量初始化的先后顺序?实现机制是什么?答案是MSDN的关于C语言预处理pragma指令init_seg的帮助文章或者我在百度百科上写的init_seg文章(http://baike.baidu.com/view/9067594.htm)。

简单说,编译器把全局变量的初始化代码的入口地址当作一个无参数无返回值类型的函数指针(即 void (*fp)()的原型),都依次存放在可执行文件的.CRT节中,构成了一个函数指针表。mainCRTStartup通过调用_initterm函数(定义在cinitexe.c文件)遍历这个函数指针数组,依次调用每个函数指针,完成全局变量的初始化工作。

那么,如何指定全局变量初始化的这些函数地址在.CRT节中存放的顺序呢?这就是链接器的一个小常识了,对于.CRT$xyz或者.CRT$uvw这样的section name,在$前面的才是链接后的最终的节名,链接器会合并.obj文件中这些同名的节;而$后面的这些字符串,是告诉链接器,按照$后面的字符串的字典顺序依次合并这些同名的节。在C Runtime库中已经定义了两个节:.CRT$XCA和.CRT$XCZ作为函数指针表的表头和表尾。编译器默认把全局变量的初始化代码地址放入.CRT$XCU中。编程者也可以把自己写的全局变量的初始化代码的地址放入"XCA"与"XCZ"之间。例如:
#pragma init_seg(".CRT$XCM")
那么从这行起到编译单元(translate unit)的结束,所有在这个作用域内定义的全局变量的初始化代码入口地址都放在.obj文件的.CRT$XCM节中。如果,另外一个编译单元的全局变量初始化代码入口地址放在了.CRT$XCN节中,那么这两个编译单元在全局变量初始化时就分出了先后,当然是"XCM"在"XCN"之前.

编程时,完全可以自行模仿上述的算法与数据结构,自己定义全局变量的初始化与销毁。例如,构建一个.myCRT节,作为函数指针表。在定义全局变量时,通过#pragma init_seg(".myCRT",dtorRegister)告诉编译器,把全局变量初始化代码入口地址都填入.myCRT节中,其析构代码入口地址通过调用编程者自己写的dtorRegister函数,登记入编程者自定义的一个全局函数指针数组中。这样,就可以在编程者选定的时机,依次遍历执行全局变量初始化,或者依次遍历执行全局变量的dtor了。

关于C与C++的startup代码,最有权威的就是Matt Pietrek发表在《MSDN Magazine》2001年第1期的《Under the hood: Reduce EXE and DLL size with LIBCTINY.LIB》。他写了一个非常精简、因此非常容易读懂的tiny的C Runtime库,包括了startup,以及printf这样的C标准库函数的实现,可以与简单的C源程序编译链接为尺寸超级小的可执行文件,媲美直接用汇编写的同样功能的可执行文件的尺寸。 此文与源代码在网上很容易搜到。

最后,需要知道,C与C++程序中的main函数具有特别的待遇。cpp文件中的main函数,无需指出它是extern "C",缺省地就具有了这一属性。其次,无论是int main(), void main();还是main(argc,argv),main(argc,argv,env);或者在已经定义了_UNICODE宏的情况下,仍然使用main函数而不是wmain(),都能得到正确的运行结果。以前我还发现exit()函数也是很特殊,无需头文件仍然能编译、运行正常。虽然exit()算是声明在STDLIB.H之中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值