6.1.4 全局/静态变量
首先列出规则如下:
不能定义类的全局或者静态对象,除非这个类没有构造函数;否则全局对象将因初始化过程中含有无法解决的符号,而导致链接失败。
读者可能难以理解这个规定,所以要用实例进行更深的挖掘才行。以simClass的clsInt类为例,如果定义如下全局变量:
- clsInt gA;
对项目进行编译,会毫不留情地得到如下错误(也是链接错误):
- errors in directory c:\trunk\simclass
- c:\trunk\simclass\main.obj : error LNK2019:
unresolved external symbol _atexit referenced
in function "void __cdecl 'dynamic initializer
for 'gA''(void)" ( __EgA@@YAXXZ)
上面的链接错误,是由于函数 __EgA@@YAXXZ中找不到符号_atexit。这两个名字都怪得不得了!理解它们要从C++(www.cppentry.com)标准说起,C++(www.cppentry.com)标准规定对于全局对象的处理,编译器要保证全局对象在main()函数运行之前已经被初始化,并且保证main()函数在退出前被删除(析构)。变量的初始化与删除,需要编译器专门为它们各自创建一个函数,并在合适的时机进行调用。函数名称根据不同的编译器会有所不同,在这里看到,用于对gA进行初始化的是函数 __EgA@@YAXXZ,笔者通过IAD反汇编后看到,用于删除(析构)的是函数 __FgA@@YAXXZ。后者一点问题都没有,但前者遇到了问题,无法解析_atexit符号。笔者将其汇编代码拷贝如下:
- // 函数名,注释很明白地告诉我们,此函数是gA的初始化函数
- __EgA@@YAXXZ: ; DATA XREF: .CRT$XCU:_gA$initializer$o
- 0000031E mov edi, edi
- 00000320 push ebp
- 00000321 mov ebp, esp
-
- // 下面首先会调用clsInt的默认构造函数
- // 第一句是将m_nValue赋值为0
- 00000323 mov ds:clsInt gA, 0
-
- // 下面是DbgPrint调用
- 0000032D mov eax, ds:clsInt gA
- 00000332 push eax
- 00000333 push offset clsInt gA
- 00000338 push offset PrintString
- 0000033D call _DbgPrint
- 0000033D
- 00000342 add esp, 0Ch
-
- // 初始化已经完毕了,问题出在这里
- //初始化完毕后,把 __FgA@@YAXXZ地址作为参数,
调用_atexit以注册终止函数 - 00000345 push offset __FgA@@YAXXZ
- 0000034A call _atexit
- 0000034A
-
- // 恢复堆栈
- 0000034F add esp, 4
- 00000352 pop ebp
- 00000353 retn
- 00000353
- 00000353 _text$yc ends
上面的汇编代码,大部分都是正确的,只是到了最后调用_atexit函数时才出了错(_atexit是导入符号,实际函数名应去掉前面的"_",即atexit)。atexit是一个C标准函数,其作用是向系统注册终止函数,即主程序在终止之前需调用的处理函数。上面我们看到,atexit将 __FgA@@YAXXZ作为参数进行了调用以析构gA。在逻辑上是没有问题的,但atexit函数在内核中未实现。实际上,它有下面的一行调用:
- atexit( __FgA@@YAXXZ);
现在的问题就归结为:内核中没有C运行时函数atexit。请问:它可以有吗?它难道不可以有吗?
上面笔者也说过,内核代码和用户程序是非常不一样的。用户程序的生命周期由main()调用开始,main()调用结束,整个程序也即完结。而驱动程序却不一样,虽然我们有时候把DriverEntry比作main(),但二者在本质上不同,DriverEntry的生命周期非常短,其作用仅是将内核文件镜像加载到系统中时进行驱动初始化,调用结束后驱动程序的其他部分依旧存在,并不随它而终止。所以我们一般可把DriverEntry称为"入口函数",而不可称为"主函数"。因此作为内核驱动来说,它没有一个明确的退出点,这应该是atexit无法在内核中实现的原因吧。
从图6-2我们看到,用户程序是一个独立运行单位,main()函数是主线程,它的生命周期也就是程序的生命周期。而内核驱动呢?它的生命周期其实只是镜像文件的生命周期,即加载与卸载,并没有固定的主线程与之匹配甚至支配其生命周期;相反,驱动代码可以出现在任何线程环境中,被任何线程调用。
话说回来,其实驱动程序也是有明显的生命周期的,即从DriverEntry开始到DriverUnload结束的镜像文件的生命周期,如图6-3所示。这也并非不可利用,笔者觉得,如果在DriverEntry调用前执行全局对象的初始化函数,而同时把终止函数注册到DriverUnload中,或许能够解决问题,但前提是要求系统要做相应的改动了。因为DriverUnload是可选的,所以若采用这种方法,应采取措施为未提供DriverUnload函数的驱动设置默认的卸载函数。但随着微软对这方面研究的深入,笔者相信,这个问题一定是他们的问题列表中必须解决的一项。
|
| (点击查看大图)图6-2 用户程序 |
|
| (点击查看大图)图6-3 内核假想实现 |
本节内容代码,请参看本书simClass示例工程。
内核中使用C++(www.cppentry.com)还有一点需要注意,就是C++(www.cppentry.com)编译器会在不提醒的情况下,使用堆栈生成临时变量若干,而内核堆栈是非常有限的,所以常常需要对此保持一份警惕。