3.2 了解VC++6.0的启动函数
VC++6. 0在控制台和多字节编码环境下的启动函数为mainCRTStartup,由系统库KERNEL32.dll负责调用。在mainCRTStartup中再调用main函数。使用VC++6.0进行调试时,入口断点总是停留在main函数的首地址处。如何挖掘main函数之前的代码呢?我们可以利用VC++6.0的栈回溯功能。在调试环境下,依次选择菜单View→Debug Windows→Call Stack打开出栈窗口(快捷键:Alt+7)。此窗口中显示了程序启动后,函数的调用流程,如图3-1所示。
图 3-1 栈回溯窗口
图3-1中显示程序运行时调用了三个函数,依次是KERNEL32、mainCRTStartup和main。其中显示的“KERNEL32!7c817077”表示在系统库KERNEL32.dll中的地址7c817077处调用mainCRTStartup,我们无法查看KERNEL32.dll的高级源码,而VC++则提供了mainCRTStartup函数的源码,安装完整版的VC++就可以查看。双击Call Stack窗口中的mainCRTStartup函数,查看函数的内部实现,如代码清单3-1所示。
代码清单3-1 mainCRTStartup函数在VC++6.0中的代码片段
//预编译宏
#else/*_WINMAIN_*/
#ifdef WPRFLAG
//宽字符版控制台启动函数
void wmainCRTStartup(
#else/*WPRFLAG*/
//多字节版控制台启动函数
void mainCRTStartup(
#endif/*WPRFLAG*/
#endif/*_WINMAIN_*/
void
)
{
//获取版本信息
_osver=GetVersion();
_winminor=(_osver>>8)&0x00FF;
_winmajor=_osver&0x00FF;
_winver=(_winmajor<<8)+_winminor;
_osver=(_osver>>16)&0x00FFFF;
//堆空间初始化过程,在此函数中,指定了程序中堆空间的起始地址
//_MT是多线程标记
#ifdef_MT
if(!_heap_init(1))
#else/*_MT*/
if(!_heap_init(0))
#endif/*_MT*/
fast_error_exit(_RT_HEAPINIT);
//初始化多线程环境
#ifdef_MT
if(!_mtinit())
fast_error_exit(_RT_THREAD);
#endif/*_MT*/
__try{
//宽字符处理代码略
//多字节版获取命令行
_acmdln=(char*)GetCommandLineA();
//多字节版获环境变量信息
_aenvptr=(char*)__crtGetEnvironmentStringsA();
//多字节版获取命令行信息
_setargv();
//多字节版获取环境变量信息
_setenvp();
#endif/*WPRFLAG*/
//初始化全局数据和浮点寄存器
_cinit();
//窗口程序处理代码略
//宽字符处理代码略
//获取环境变量信息
_initenv=_environ;
//调用main函数,传递命令行参数信息
mainret=main(_argc,_argv,_environ);
#endif/*WPRFLAG*/
#endif/*_WINMAIN_*/
//检查main函数返回值执行析构函数或atexit注册的函数指针,并结束程序
exit(mainret);
}
//退出结束代码略
}
代码清单3-1为VC++6.0控制台程序的默认启动函数,在该启动函数中做了一系列初始化工作。下面将详细解读启动函数的工作流程。
GetVersion函数:获取当前运行平台的版本号。控制台程序运行在Windows模拟的DOS下,因此这里获取的版本号为MS-DOS的版本信息。
_heap_init函数:用于初始化堆空间。在函数实现中使用HeapCreate申请堆空间,申请空间的大小由_heap_init传递的参数决定。_sbh_heap_init函数用于初始化堆结构信息。堆结构的说明将在第7章详细讲解。
GetCommandLineA函数:获取命令行参数信息的首地址。
_crtGetEnvironmentStringsA函数:获取环境变量信息的首地址。
_setargv函数:此函数根据GetCommandLineA获取命令行参数信息的首地址并进行参数分析,将分离出的参数的个数保存在全局变量_argc中,将分析出的每个命令行参数的首地址存放在数组中,并将这个字符指针数组的首地址保存在全局变量_argv中。这样就得到了命令行参数的个数,以及命令行参数信息。
_setenvp函数:此函数根据_crtGetEnvironmentStringsA函数获取环境变量信息的首地址并进行分析,将得到的每条环境变量字符串的首地址存放在字符指针数组中,并将这个数组的首地址存放在全局变量env中。
得到main函数所需的三个参数信息之后,当调用main函数时,便可以将_argc、_argv、env这三个全局变量作为参数,以栈传参方式传递到main函数中。
_cinit函数:用于全局数据和浮点寄存器的初始化。全局对象和IO流等的初始化都是通过这个函数实现的。利用函数_initterm进行数据链初始化,这个函数由两个参数组成,类型为“_PVFV*”,这是一个函数指针数组,其中保留了每个初始化函数的地址。初始化函数的类型为_PVFV,其定义原型如下:
typedef void(_cdecl*_PVFV)(void);
也就是说,这个初始化函数是无参数也无返回值的。大家知道,C++规定全局对象和静态对象必须在main函数前构造,在main函数返回后析构。所以,这里的_PVFV函数指针数组就是用来代理调用构造函数的,具体如代码清单3-2所示。
代码清单3-2_cinit函数的代码片段
//用于初始化寄存器
if(_FPinit!=NULL)
(*_FPinit)();//初始化浮点寄存器
//用于初始化C语法中的数据
_initterm(_xi_a,_xi_z);
//用于初始化C++语法中的数据
_initterm(_xc_a,_xc_z);
在代码清单3-2中,_FPinit是一个全局函数指针,类型也是_PVFV,如果编译器扫描代码时发现有浮点计算,则此指针保存了初始化浮点寄存器的代码地址,否则为0值。如果浮点寄存器未被初始化而进行浮点计算,程序会产生异常或错误(详见2.2节),这类错误应属于VC++6.0自身设计的Bug,在VC++6.0以后的版本中已将其修复。一般而言,第一个_initterm初始化的都是C支持库中所需的数据。参数_xi_a为函数指针数组的起始地址,_xi_z为结束地址。_initterm的实现如代码清单3-3所示。
代码清单3-3_initterm函数的代码片段
static void__cdecl_initterm(
_PVFV*pfbegin,
_PVFV*pfend)
{
//遍历数组的各元素
while(pfbegin<pfend)
{
//若函数指针不为空,则执行该函数
if(*pfbegin!=NULL)
(**pfbegin)();
++pfbegin;
}
}
C++所需数据的初始化操作会在代码清单3-2中第二次对_initterm调用时执行,一般都是全局对象或静态对象的初始化函数。关于全局对象的初始化流程的更多内容请见第10章。
VC编译器的版本不同,mainCRTStartup函数也可能会有所不同,如Microsoft Visual Studio 2005又称VC++8.0,其中mainCRTStartup的名字就变为了_tmainCRTStartup。本书只针对VC++6.0版本进行讲解,其升级版本中的基本原理与VC++6.0相同,只有少许变动,读者可自行分析。
那么,是不是所有由VC++编译出的控制台程序的启动函数都在mainCRTStartup中呢?这要根据编译选项确定。在默认情况下,入口函数为main,这时会从mainCRTStartup启动,再传入main所需要的三个参数,最后调用main函数。重新指定入口函数后,将直接从KERNEL32中调用重新指定的入口函数,而不会经过mainCRTStartup。通过修改编译选项,重新设置入口函数,依次选择菜单Procject→Settings→Link→Output,在Enty-point symbol中填写需要重新指定新入口的函数名称。编译后调试程序,结果如图3-2所示。
图 3-2 重设入口函数
从图3-2中我们看到,重新指定入口函数后,没有调用mainCRTStartup函数,直接调用了新的入口函数MyEntry。但是,由于没有调用mainCRTStartup函数,堆空间是没有被初始化的,当使用到堆空间时,程序会报错并崩溃,如图3-3所示。
图 3-3 堆空间错误
由于没有初始化堆空间,而在函数MyEntry中使用了malloc函数,因此该函数引发了这个错误。更多关于堆空间的知识请参见第7章。