1、很少有专题讲内核中的 C+编程,中文资料恐怕更是罕见。由于 C+的普及性、与 C的亲密关系,以及大部分情况下程序员都使用 C+编译器编译 C 程序的事实,当初学者听说内核中 “ 不容易 ” (笔者也听说过 “ 无法 ” 二字)用 C+进行编程时,会大吃一惊。不管是说者无意,还是听者有心, Windows 内核的现状,决定了 C 语言是内核编程的首选。 其实内核驱动中也能使用 C+,也能使用类,但和用户程序中的用法有一些区别,一些特殊的地方需要特别注意。从笔者的经验来看, WDK 给出的 AVStream 小端口驱动示例工程,就都是 C+代码,这 是由于 AVStream 的模块性非常强,在实现
2、较大功能模块时,非得用类封装,否则难以表述清楚。 本章专门讲述如何在内核中编写 C+驱动程序。笔者先写一个简单的例子,显示类的一些基本特性,并由此交代出几项关键点;然后改造 WDF USB 设备驱动开发一章中的 WDFCY001 驱动的例子,将它全部改造成一个驱动类,并最终实现 C+的最大优点:多态。 6.1.1 一个简单的例子 首先我们尝试把用户程序中最简单的类拷贝到内核中,编译链接,看看行不行。下面就是笔者定义的整数类,它封装一个整数,对象能够被当成整数使用。 1. class clsInt 2. Public: 3. clsInt()m_nValue = 0; 4. clsInt(int
3、 nValue)m_nValue = nValue; 5. void print()KdPrint(“m_nValue:%d ”, m_nValue); 6. operator int()return m_nValue; 7. 8. private: 9. int m_nValue; 10. ; 上例是一个非常简单的类定义,我们将在 DriverEntry 函数中使用它,分别定义一个局部变量和动态创建一个对象。我们通过 Debug 信息来观察对象行踪,希望能够得到正确的输出。入口函数中的定义如下: 1. extern “C“ NTSTATUS DriverEntry( 2. IN PDRIVE
4、R_OBJECT DriverObject, 3. IN PUNICODE_STRING RegistryPath 4. ) 5. 6. / 创建两个对象,一个是局部变量,一个是动态创建的 7. clsInt obj1(1); 8. clsInt* obj2 = new(NonPagedPool, abcd) clsInt(2); 9. 10. / 打印 Log 信息 11. obj1.print(); 12. obj2-print(); 13. delete obj2; 14. 15. / 让模块加载失败 16. return STATUS_UNSUCCESSFUL; 17. 上面代码中先后
5、创建了两个 clsInt 对象,一个是在栈中创建的,初始变量为 1;一个是动态创建的,初始变量为 2。后者由于是动态创建的,必须手动调用 delete 函 数释放内存,所以其析构函数比前者先调用。我们必须从 Log 信息中得到类似的脉络,以证明其正确性。代码请参看 simClass 工程。图 6-1是 Log 信息的截图,我们如愿以偿地得到了想要的结果。 图 6-1 对象 Log 信息 6.1.2 new/delete 查看上面的代码,会发现一个不同于以往的 new操作符。这是怎么回事呢?我们这一节就讲讲它。在用户程序中,创建和释放一个对象使用 new/delete 方法,其底层乃是调用Hea
6、pAllocate/HeapFree 堆 API 从线程堆栈中申请空间。但问题是,内核 CRT 没有提供 new/delete 操作符,所以需要自己定义。自定义的 new/delete 操作符,自然也是能够从堆栈中分配内存的,内核中有 RtlAllocateHeap/RtlFreeHeap 堆栈服务函数。但在内核中,我们一般使用内存池来获取内存,实际上内存池和堆栈使用了同一套实现机制。使用 ExAllocatePool/ExFreePool 函数对从内存池申请 /释放内存,下面是一个例子。 1. _forceinline 2. void* _cdecl operator new(size_t
7、size, 3. POOL_TYPE pool_type, 4. ULONG pool_tag) 5. 6. ASSERT(pool_type (size), 15. pool_tag); 16. 上面的函数定义有几个细节的地方应当注意一下。首先注意 new 操作符重载,它的第一个参数一定是 size_t,用来表示将分配缓冲区的长度;其次注意分页内存和非分页内存的区别,即 pool_type 所表示者,在 DISPATCH_LEVEL 及以上的级别是不能分配分页内存的。 下面是使用 new 进行内存申请的一个例子。 1. / 定义一个 32位的 TAG 值 2. #define TAG abc
8、d 3. / 外部已经定义了一个 clsName 类 4. extern class clsName; 5. 6. / 为 clsName 申请对象空间 7. clsName* objName = NULL; 8. objName = new(NonPagedPool, TAG)clsName(); 上面的 new 操作和用户程序中的 new 操作具有同样的功效,但需要注意第一个参数 size_t 是必须外置的,编译器会自动用 sizeof(clsName)求取长度并作为第一个参数。一般地说,对于类似下面的语句: className objName = new() class Name() 其
9、执行过程是,首先由 new 操作符为新对象动态分配内存,并返回指针;然后再对此新创建的对象,选择与 className() 相符的构造函数进行初始化。 再来看看 delete 操作符的重载。 1. _forceinline 2. void _cdecl operator delete(void* pointer) 3. 4. ASSERT(NULL != pointer); 5. if (NULL != pointer) 6. ExFreePool(pointer); 7. 删除对象数组,即 delete操作符重载。 1. _forceinline 2. void _cdecl operato
10、r delete(void* pointer) 3. 4. ASSERT(NULL != pointer); 5. if (NULL != pointer) 6. ExFreePool(pointer); 7. 上面两个函数最终都会将指定地址的内存释放,但在释放之前,前者会调用指定对象的析构函数,后者会对数组中每个成员调用析构函数。示例如下: 1. extern clsName *objName; 2. extern clsName *objArray; 3. delete objName; 4. delete objArray; 6.1.3 extern “C“ 对 extern “C“编译
11、指令,大家不会感到陌生。它一般这样用: 1. extern “C“ 2. / 内容 3. 既然是编译指令,就一定是作用于编译时刻的。它告诉编译器,对于作用范围内的代码,以 C 编译器方式编译。一般是针对 C+/Java 等程序而用的。如果括号内仅有一项,那么括号可以省略。 最早让我们见识到它的作用的是在入口函数 DriverEntry 中。现在必须这样声明它: 1. extern “C“ NTSTATUS DriverEntry( 2. IN PDRIVER_OBJECT DriverObject, 3. IN PUNICODE_STRING RegistryPath 4. ); 初学者未必知
12、道这一点,如果 “ 忘记 ” 做上述改动,将得到如下错误: error LNK2019: unresolved external symbol _DriverEntry8 referenced in function _GsDriverEntry8 error LNK1120: 1 unresolved externals 很奇怪,这是一个链接错误,说明编译过程是通过的。怎么回事呢?认真看一下错误内容,原来是系统在链接时找不到入口函数 _DriverEntry8。这个奇怪的函数名,很显然是C编译器对 DriverEntry 进行编译后的结果,前缀 “_” 是 C 编译器特有的,后缀 “8” 是所有参数的长度 。原来我们现在使用的是 C+编译器,一定是它把 DriverEntry 编译成了系统无法认识的另一副模样了(实际上, C+编译器会把它编译成以 “?DriverEntry” 开头的一串很长的符号)。 一旦加上 extern “C“修饰符,上述问题即立刻消失了。 extern “C“提醒编译器要使用C编译格式编译 DriverEntry 函数,这样编译生成的函数名称为 “_DriverEntry8” ,链接器即可正确地识别出符号了。