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章。