现在的位置: 首页 > 综合 > 正文

罕见内核驱动C++编程实例 new/delete方法

2013年01月01日 ⁄ 综合 ⁄ 共 5647字 ⁄ 字号 评论关闭

  很少有专题讲内核中的C++编程,中文资料恐怕更是罕见。由于C++的普及性、与C的亲密关系,以及大部分情况下程序员都使用C++编译器编译C程序的事实,当初学者听说内核中“不容易”(笔者也听说过“无法”二字)用C++进行编程时,会大吃一惊。不管是说者无意,还是听者有心,Windows内核的现状,决定了C语言是内核编程的首选。

  其实内核驱动中也能使用C++,也能使用类,但和用户程序中的用法有一些区别,一些特殊的地方需要特别注意。从笔者的经验来看,WDK给出的AVStream小端口驱动示例工程,就都是C++代码,这是由于AVStream的模块性非常强,在实现较大功能模块时,非得用类封装,否则难以表述清楚。

  本章专门讲述如何在内核中编写C++驱动程序。笔者先写一个简单的例子,显示类的一些基本特性,并由此交代出几项关键点;然后改造《WDF USB设备驱动开发》一章中的WDFCY001驱动的例子,将它全部改造成一个驱动类,并最终实现C++的最大优点:多态。

  一个简单的例子

  首先我们尝试把用户程序中最简单的类拷贝到内核中,编译链接,看看行不行。下面就是笔者定义的整数类,它封装一个整数,对象能够被当成整数使用。

  

以下是代码片段: class clsInt{ Public: clsInt(){m_nValue = 0;} clsInt(int nValue){m_nValue = nValue;} void print(){KdPrint((“m_nValue:%d/n”, m_nValue));} operator int(){return m_nValue;} private: int m_nValue; };

上例是一个非常简单的类定义,我们将在DriverEntry函数中使用它,分别定义一个局部变量和动态创建一个对象。我们通过Debug信息来观察对象行踪,希望能够得到正确的输出。入口函数中的定义如下:

  

以下是代码片段: extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { // 创建两个对象,一个是局部变量,一个是动态创建的 clsInt obj1(1); clsInt* obj2 = new(NonPagedPool, "abcd") clsInt(2); // 打印Log信息 obj1.print(); obj2->print(); delete obj2; // 让模块加载失败 return STATUS_UNSUCCESSFUL; }

上面代码中先后创建了两个clsInt对象,一个是在栈中创建的,初始变量为1;一个是动态创建的,初始变量为2。后者由于是动态创建的,必须手动调用delete函数释放内存,所以其析构函数比前者先调用。我们必须从Log信息中得到类似的脉络,以证明其正确性。代码请参看simClass工程。图6-1是Log信息的截图,我们如愿以偿地得到了想要的结果。

  

 

  图6-1 对象Log信息

new/delete

  查看上面的代码,会发现一个不同于以往的new操作符。这是怎么回事呢?我们这一节就讲讲它。在用户程序中,创建和释放一个对象使用 new/delete方法,其底层乃是调用HeapAllocate/HeapFree 堆API从线程堆栈中申请空间。但问题是,内核CRT没有提供new/delete操作符,所以需要自己定义。自定义的new/delete操作符,自然也是能够从堆栈中分配内存的,内核中有RtlAllocateHeap/RtlFreeHeap堆栈服务函数。但在内核中,我们一般使用内存池来获取内存,实际上内存池和堆栈使用了同一套实现机制。使用ExAllocatePool/ExFreePool函数对从内存池申请/释放内存,下面是一个例子。

  下面是使用new进行内存申请的一个例子。

  

以下是代码片段: // 定义一个32位的TAG值 #define TAG "abcd" // 外部已经定义了一个clsName类 extern class clsName; // 为clsName申请对象空间 clsName* objName = NULL; objName = new(NonPagedPool, TAG)clsName();

上面的new操作和用户程序中的new操作具有同样的功效,但需要注意第一个参数size_t是必须外置的,编译器会自动用sizeof(clsName)求取长度并作为第一个参数。一般地说,对于类似下面的语句:

  className objName = new(…) className(…)

  其执行过程是,首先由new操作符为新对象动态分配内存,并返回指针;然后再对此新创建的对象,选择与className(…) 相符的构造函数进行初始化。

  再来看看delete操作符的重载。

  

以下是代码片段: __forceinline void __cdecl operator delete(void* pointer) { ASSERT(NULL != pointer); if (NULL != pointer) ExFreePool(pointer); }

删除对象数组,即delete[]操作符重载。

  

以下是代码片段: __forceinline void __cdecl operator delete[](void* pointer) { ASSERT(NULL != pointer); if (NULL != pointer) ExFreePool(pointer); }

上面两个函数最终都会将指定地址的内存释放,但在释放之前,前者会调用指定对象的析构函数,后者会对数组中每个成员调用析构函数。示例如下:

  

以下是代码片段: extern clsName *objName; extern clsName *objArray[]; delete objName; delete[] objArray;

extern "C"

  对extern "C"编译指令,大家不会感到陌生。它一般这样用:

  

以下是代码片段: extern "C"{ //…内容 }

既然是编译指令,就一定是作用于编译时刻的。它告诉编译器,对于作用范围内的代码,以C编译器方式编译。一般是针对C++/Java等程序而用的。如果括号内仅有一项,那么括号可以省略。

  最早让我们见识到它的作用的是在入口函数DriverEntry中。现在必须这样声明它:

  

以下是代码片段: extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath );

初学者未必知道这一点,如果“忘记”做上述改动,将得到如下错误:

  

以下是代码片段: error LNK2019: unresolved external symbol _DriverEntry@8 referenced in function _GsDriverEntry@8 error LNK1120: 1 unresolved externals

很奇怪,这是一个链接错误,说明编译过程是通过的。怎么回事呢?认真看一下错误内容,原来是系统在链接时找不到入口函数_DriverEntry@8。这个奇怪的函数名,很显然是C编译器对DriverEntry进行编译后的结果,前缀“_”是C编译器特有的,后缀“@8”是所有参数的长度。原来我们现在使用的是C++编译器,一定是它把DriverEntry编译成了系统无法认识的另一副模样了(实际上,C++编译器会把它编译成以“?DriverEntry@@”开头的一串很长的符号)。

  一旦加上extern "C"修饰符,上述问题即立刻消失了。extern "C"提醒编译器要使用C编译格式编译DriverEntry函数,这样编译生成的函数名称为“_DriverEntry@8”,链接器即可正确地识别出符号了。

  全局/静态变量

  首先列出规则如下:

  不能定义类的全局或者静态对象,除非这个类没有构造函数;否则全局对象将因初始化过程中含有无法解决的符号,而导致链接失败。

  读者可能难以理解这个规定,所以要用实例进行更深的挖掘才行。以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++标准说起,C++标准规定对于全局对象的处理,编译器要保证全局对象在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++还有一点需要注意,就是C++编译器会在不提醒的情况下,使用堆栈生成临时变量若干,而内核堆栈是非常有限的,所以常常需要对此保持一份警惕。

  本文转自《竹林蹊径:深入浅出Windows驱动开发》一书,张佩,马勇,董鉴源编著。ISBN 978-7-121-12555-3;2011年2月出版;定价:69.00元

抱歉!评论已关闭.