-- 作者:admin
-- 发布时间:2/15/2011 11:20:00 AM
--
第6章 内核驱动C++编程 6.1 驱动中的类 很少有专题讲内核中的C++编程,中文资料恐怕更是罕见。由于C++的普及性、与C的亲密关系,以及大部分情况下程序员都使用C++编译器编译C程序的事实,当初学者听说内核中“不容易”(笔者也听说过“无法”二字)用C++进行编程时,会大吃一惊。不管是说者无意,还是听者有心,Windows内核的现状,决定了C语言是内核编程的首选。 其实内核驱动中也能使用C++,也能使用类,但和用户程序中的用法有一些区别,一些特殊的地方需要特别注意。从笔者的经验来看,WDK给出的AVStream小端口驱动示例工程,就都是C++代码,这是由于AVStream的模块性非常强,在实现较大功能模块时,非得用类封装,否则难以表述清楚。 本章专门讲述如何在内核中编写C++驱动程序。笔者先写一个简单的例子,显示类的一些基本特性,并由此交代出几项关键点;然后改造《WDF USB设备驱动开发》一章中的WDFCY001驱动的例子,将它全部改造成一个驱动类,并最终实现C++的最大优点:多态。 6.1.1 一个简单的例子 首先我们尝试把用户程序中最简单的类拷贝到内核中,编译链接,看看行不行。下面就是笔者定义的整数类,它封装一个整数,对象能够被当成整数使用。 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信息 6.1.2 new/delete 查看上面的代码,会发现一个不同于以往的new操作符。这是怎么回事呢?我们这一节就讲讲它。在用户程序中,创建和释放一个对象使用 new/delete方法,其底层乃是调用HeapAllocate/HeapFree 堆API从线程堆栈中申请空间。但问题是,内核CRT没有提供new/delete操作符,所以需要自己定义。自定义的new/delete操作符,自然也是能够从堆栈中分配内存的,内核中有RtlAllocateHeap/RtlFreeHeap堆栈服务函数。但在内核中,我们一般使用内存池来获取内存,实际上内存池和堆栈使用了同一套实现机制。使用ExAllocatePool/ExFreePool函数对从内存池申请/释放内存,下面是一个例子。 __forceinline void* __cdecl operator new(size_t size, POOL_TYPE pool_type, ULONG pool_tag) { ASSERT((pool_type < MaxPoolType) && (0 != size)); if(size == 0)return NULL; // 中断级检查。分发级别和以上的级别只能分配非分页内存 ASSERT(pool_type == NonPagedPool || (KeGetCurrentIrql() < DISPATCH_LEVEL)); return ExAllocatePoolWithTag(pool_type, static_cast<ULONG>(size), pool_tag); } 上面的函数定义有几个细节的地方应当注意一下。首先注意new操作符重载,它的第一个参数一定是size_t,用来表示将分配缓冲区的长度;其次注意分页内存和非分页内存的区别,即pool_type所表示者,在DISPATCH_LEVEL及以上的级别是不能分配分页内存的。 下面是使用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; 6.1.3 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”,链接器即可正确地识别出符号了。 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++标准说起,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++编译器会在不提醒的情况下,使用堆栈生成临时变量若干,而内核堆栈是非常有限的,所以常常需要对此保持一份警惕。 6.1.5 栈的忧虑 普通的Win32线程有两个栈:一个是用户栈,另一个是内核栈;而如果是内核中创建的系统工作线程,则只有内核栈。只要代码在内核中运行,线程就一定是使用其内核栈的。栈的主要作用是维护函数调用帧,以及为局部变量提供空间。 用户栈可以指定其大小,默认是1MB,通过编译指令/stack可改设其他值。 普通内核栈的大小是固定的,由系统根据CPU架构而定,x86系统上为12KB,x64系统上为24KB,安腾系统上为32KB。对于GUI线程,普通内核栈空间可能不够,所以系统又定义了“大内核栈”概念,可以在需要的时候增长栈空间。只有GUI线程才能使用大内核栈,这也是系统规定的。 关于GUI线程,笔者多说几句。Windows的发明,将GDI和USER模块,即“窗口与图形模块”的实现移到了内核中,称为Windows子系统内核服务,并形成一个win32k.sys内核文件。而用户层仅留调用接口,由User32.dll和GDI32.dll两个文件暴露出来。判断一个线程是不是GUI线程的依据,竟非常的简单:线程初建时,都是普通线程,第一次调用Windows子系统内核服务(只要用户程序调用了User32.dll和GDI32.dll中的函数,并导致相关内核服务在内核中被执行),系统即立刻将之转变为GUI线程,并从而切换到“大内核栈”;倘若至线程结束,并未有任何一个子系统内核服务被调用,那么它一直都是普通线程,一直使用普通内核栈。 正是由于窗口与图形模块的内移,才导致了相关服务必须在内核中执行,从而不得不引入“大内核栈”概念。笔者知道UNIX系列的操作系统,包括Linux、Mac,都是在用户层实现窗口与图形子系统的,这类操作系统甚至可以毫不影响地在多个图形子系统间进行切换。回忆Windows NT 4以前的操作系统,其设计也和UNIX一样,相关模块放在用户层实现。在这种情况下,对于上述操作系统,按照笔者的理解,它们就没必要使用大内核栈的概念了——笔者仔细查过Linux和UNIX相关书籍,确实未找到“大内核栈”的说明。 和C编译器相比,C++编译器更善于为目标代码做较多优化,并因为创建数量不等的临时变量而占用一定的栈空间。对于用户栈和大内核栈,临时变量带来的栈空间支出一般不足以构成问题。但对于普通内核栈,C++编译器并不知道自己正在多么奢侈地挥霍着有限而珍贵的资源,几十K甚至十几K的内存很容易被耗尽,内核栈溢出因此成为一个非常大的威胁。 下面给读者举一个语言上的例子。对于表达式: A = b + c 如果a、b、c三个变量的类型为: int a, b, c; 虽然不同的编译器间各有不同的实现,但一般来说编译后的结果是这样的:先把b的值存入一个寄存器中(如eax),将寄存器和c相加,再把寄存器值传入变量a。这里面不涉及临时变量。 但如果a、b、c三个变量的类型为一个类,如ClsSome: ClsSome a, b, c; 则编译后的结果就不像表面上那么简单了,编译器会创建一个ClsSome类型的临时变量tmp,并将b与c相加后的结果存入tmp中,最后用赋值操作将临时对象tmp赋值给a。临时变量tmp是编译器神不知鬼不觉创建的,程序员很难预知这一背后动作。 对于上面的对象例子,如果有更多的对象参与并实现了更复杂的操作,则编译器创建的临时变量数将更多,可能超乎你的想象。Lippman在其《Inside The C++ Object Model》一书中举了一例,是三个对象之间的四则运算: a = b + c - b*c; // 见其书6.3节,原是a[i] = b[i]+c[i] – b[i]*c[i],是一个对象数组 Lippman举此例后,称这里面将会导致创建5个临时变量,岂不令人惊讶! 对于因为内核栈空间的瓶颈而引起的忧虑,目前并没有好的解决方法。可能读者会疑惑一个问题,即为什么不能把内核栈也设计得和用户栈一样呢?比如把内核栈默认大小设置为1MB,用户栈这么做并没有带来任何问题啊。 提出这个问题的读者很会动脑子,但他忽略了一个问题,就是用户空间和内核空间的不同之处。用户空间是进程独立的,以x86系统为例,在正常情况下,每个进程都有独立的2GB用户空间,所以用户栈的1MB并不起眼。 而内核空间是全局共享的,所有内核栈都在同一个内核空间中申请内存资源。如果内核栈也像用户栈一样,将大小设到1MB,我们来算一笔账吧。系统中的线程成百上千,就算平均500个线程吧,每个线程一个1MB大小的内核栈,一共占了500MB。这还了得吗?岌岌可危。500个线程太保守了,笔者在写作的当下系统中有967个线程(见图6-4),那就用掉将近1半的内核空间了!再倘若用户开启了/3G开关,那么内核空间就只有1GB——系统要喊救命了,可了不得! 在任务管理器中,在选择列对话框中勾上“线程数”,能看到各进程含有的线程数,将所得数相加能得到一个大概的系统线程总数;但更好的办法是查看系统的性能计数,可使用perfmon来查看,如图6-4所示。 图6-4 查看系统线程数 内核栈的问题,正是内核中使用C++的一个最大障碍。在实际编程时,为了尽量避免发生栈溢出错误,需要经常对栈剩余空间保持一份警惕,尤其在可能形成很深的调用栈(如递归调用)的情况下。内核函数IoGetStackLimits与IoGetRemainingStackSize分别用来获取当前内核栈的边界与剩余空间,可使用这两个函数实时控制栈状况。可在函数入口处包含下列代码。 // 如果当前内核栈空间小于150字节,就让函数返回 if(IoGetRemainingStackSize() < 150) return; // 如有可能,可指定一个特殊的错误值 6.2 类封装的驱动程序 上面的clsInt太过简单了,无法回答这样的问题:在内核中使用类能带来什么好处?simClass工程无法回答上述问题,笔者只是借助它引出并解决一些基本问题。下面我们思考这样一个问题:就驱动本身而言,如何把内核驱动封装成一个类? 内核驱动,无外乎就是一些数据结构:驱动对象、设备对象、文件对象、IRP等;而对这些数据结构的处理就是内核函数:WDM驱动乃是分发函数(Dispatch Function),WDF乃是事件(Event)。 这不正好吗?上述二者恰好是类封装的基本要素!类者,数据加方法。笔者将把诸如驱动对象、设备对象等一切用到的数据结构,作为成员数据;把分发函数或者事件、回调,作为成员函数。一个“驱动类”就此初露峥嵘了。 想法是不错的,但遇到两个问题,下面一一说明。 6.2.1 寻找合适的存储所 定义类之前要解决的第一个问题是,一旦类对象被创建后,它的生命周期基本上要和驱动程序的生命周期相当,在哪里保存类对象呢?创建全局变量当然是一种方法,但存在多个驱动实例时就会发生冲突。在WDM驱动中,有设备扩展可以保存自己的变量。KMDF则更丰富,笔者最终决定在WDFDRIVER对象中保存类对象。达成的效果如图6-5所示。 驱动对象和设备对象是驱动程序的核心,而回调函数又是核心的核心。在图6-5中,驱动对象和设备对象的回调函数,都在DrvClass类中实现。而为了让C++类对象的生命周期和驱动对象保持一致,用一个WDMMEMORY对象将它封装起来,并作为驱动对象的子对象,由框架自动维护,在驱动对象存在时,C++类对象将一直是有效的。 首先看看怎么把一个自定义的内容保存到驱动对象中,这又要用到框架对象的“环境变量”概念了,前面我们学过给设备对象设置环境变量,现在轮到驱动对象了。让我们重新来做一遍。 图6-5 对象模块图 第1步,定义一个获取环境块指针的函数。 WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DRIVER_CONTEXT, GetDriverContext); 上面的宏将定义一个名称为GetDriverContext的函数,这个函数的伪代码如下: *DRIVER_CONTEXT GetDriverContext(WDFOBJECT Object) { // XXX是一个固定的地址,由于未文档化,无法知道其具体定义 return (DRIVER_CONTEXT*)Object->XXX; } 以后只需要进行如下调用,即能取得驱动对象的环境块指针(前提是传入正确的对象句柄)。 // 获取环境变量 DRIVER_CONTEXT *pContext = GetDriverContext(WdfDriver); 第2步,在WdfDriverCreate创建框架驱动对象的同时,设置环境变量的结构,通过WDF_DRIVER_CONFIG完成。下面代码的前面部分,实现了此步。 第3步,调用GetDriverContext获取环境变量,并将其封装到一个WDFMEMORY对象中,并指定第2步中创建的驱动对象为其父对象,以令框架自动维护其生命周期。下面代码的后面部分,实现了此步。 NTSTATUS DrvClass::DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { KDBG(DPFLTR_INFO_LEVEL, "DrvClass::DriverEntry"); WDFMEMORY hDriver; WDF_OBJECT_ATTRIBUTES attributes; WDF_DRIVER_CONFIG config; NTSTATUS status = STATUS_SUCCESS; WDFDRIVER WdfDriver; // 设定驱动环境块长度 // 宏内部会调用sizeof(…)求结构体长度,并用粘连符(##)获得其名称 WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, DRIVER_CONTEXT); WDF_DRIVER_CONFIG_INIT(&config, DrvClass::PnpAdd_sta); status = WdfDriverCreate(DriverObject, // WDF驱动对象 RegistryPath, &attributes, &config, // 配置参数 &WdfDriver); // 取得驱动环境块 PDRIVEDR_CONTEXT pContext = GetDriverContext(WdfDriver); ASSERT(pContext); pContext->par1 = (PVOID)this; // 把类对象用WDFMEMORY对象封装后,作为WDFDRIVER对象的子对象 WDF_OBJECT_ATTRIBUTES_INIT(&attributes); attributes.ParentObject = WdfDriver; attributes.EvtDestroyCallback = DrvClassDestroy; WdfMemoryCreatePreallocated(&attributes, (PVOID)this, sizeof(DrvClass), &hDriver); KDBG(DPFLTR_INFO_LEVEL, "this = %p", this); return status; } 驱动程序将在入口函数DriverEntry中动态创建一个类对象,并即刻调用方法DrvClass::DriverEntry,以创建驱动对象并将其作为对象的存储所。 以这种方法实现的妙处是,对象的维护是自动化的,我们不用操心太切。一切看上去,很是完美。下面是DrvClassDestroy函数的实现,WDF框架会在销毁内存对象时自动调用它,我们在其中销毁类对象。 VOID DrvClassDestroy(IN WDFOBJECT Object) { PVOID pBuf = WdfMemoryGetBuffer((WDFMEMORY)Object, NULL); delete pBuf; } 6.2.2 类方法与事件函数 KMDF中的事件函数,分开来说:驱动对象有EvtDriverDeviceAdd和EvtDriverUnload,我们将实现前者;设备对象有一系列PNP/Power事件;还有其他对象的事件函数,且忽略之,详见代码。 事件函数说到底是一种回调函数。类普通成员函数,由于编译后会增加this参数,所以无法成为回调函数。只能使用类静态函数,并通过静态函数再回调成员函数。这是一种很通用的实现手段。以EvtDriverDeviceAdd事件函数为例,我们要在类中为它定义两个相关函数。 Class DrvClass { // 定义类静态函数,它是全局的,可以作为回调函数 static NTSTATUS PnpAdd_sta( IN WDFDRIVER Driver, IN PWDFDEVICE_INIT DeviceInit); // 再定义类成员函数,将由静态函数内部调用 virtual NTSTATUS PnpAdd( IN WDFDRIVER Driver, IN PWDFDEVICE_INIT DeviceInit, DrvClass* pThis); // 其他接口函数 // …… } 要能够通过静态函数回调成员函数,即通过PnpAdd_sta回调PnpAdd函数。前提是要能够获得对象指针,因为我们已经把对象指针保存在驱动对象的环境块中了,所以达到此目的不是难事。代码如下: NTSTATUS DrvClass::PnpAdd_sta(IN WDFDRIVER Driver, IN PWDFDEVICE_INIT DeviceInit) { // 取得环境块 PDRIVEDR_CONTEXT pContext = GetDriverContext(Driver); // 环境块中存有对象指针 DrvClass* pThis = (DrvClass*)pContext->par1; // 再调用成员函数 return pThis->PnpAdd(Driver, DeviceInit); } 所有其他的事件函数,都必须采用相同的方法实现。 6.2.3 KMDF驱动实现 其实上面的内容,一直是围绕KMDF进行讲解的。DrvClass内部的DriverEntry成员函数已经讲解过了,现在看看真正的入口函数该如何定义吧。 extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { // 动态创建对象,此步在后面将被修改 DrvClass* myDriver = new(NonPagedPool, 'CY01')DrvClass(); if(myDriver == NULL)return STATUS_UNSUCCESSFUL; return myDriver->DriverEntry(DriverObject, RegistryPath); } 干净得不得了,驱动程序在加载之初就以快捷无比的速度向我们定义的类靠拢了。至于第1行代码动态创建对象的操作,当前这样实现已经完全可以了,但在后面将被修改,以支持多态。 6.2.4 WDM驱动实现 如果使用WDM方式进行类封装,对于非PNP类驱动,可以在入口函数中创建控制设备对象,并把类对象保存在设备对象的设备扩展中;对于PNP类驱动,应当在AddDevice函数中建立设备栈时创建类对象,并将其保存在功能设备对象的设备扩展中。笔者会以前者为例,简单讲一下实现。WDMClass示例工程,读者参照代码,在它的基础上很容易扩展出功能更为完善的驱动程序。 这里列出具体的封装过程。首先是类定义,定义一个通用的分发函数如下: class WDMDrvClass{ public: static NTSTATUS DispatchFunc_sta( DEVICE_OBJECT Device, PIRP Irp); virtual NTSTATUS DispatchFunc( DEVICE_OBJECT Device, PIRP Irp); // 其他…… }; 同理,定义一个静态函数和一个类成员函数,静态函数将通过对象指针调用成员函数。入口函数中要这样定义: typedef struct{ WDMDrvClass pThis; //…… }DEVICE_EXTENSION; NTSTATUS DriverEntry( PDRIVER_OBJECT Driver, PUNICODE_STRING Register) { // 创建动态对象 WDMDrvClass* pDrv = new(NonPagedPool, 'SAMP') WDMDrvClass(); // 设置分发函数,全部指向DispatchFunc_sta for(int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) Driver->DispatchFunction[i] = pDrv->DispatchFunc_sta; } // 创建控制设备对象,并同时创建设备扩展区 IoCreateDeviceObject(..., sizeof(DEVICE_EXTENSION)); // 把对象指针保存到设备扩展中 DEVICE_EXTENSION* pContext = (DEVICE_EXTENSION*)DeviceObject->DeviceExtension; pContext->pThis = pDrv; return STATUS_SUCCESS; } 这一切就绪之后,我们还是来看看DispatchFunc_sta该如何实现吧。诚如我们所知,所有的驱动分发函数的第一个参数总是设备对象,正是我们所创建的那个。通过它,我们总是能够在静态函数中得到对象指针。下面是DispatchFunc_sta函数的实现。 NTSTATUS WDMDrvClass::DispatchFunc_sta( DEVICE_OBJECT Device, PIRP Irp) { PDEVICE_EXTENSION pContext = Device->DeviceExtension; WDMDrv pThis = pContext->pThis; return pThis-> DispatchFunc(Device, Irp); } 与上述KMDF的实现类似,其他更详细的实现内容,请参阅工程代码。 6.3 多态 如果纯粹是为了尝鲜,在驱动中加入一个类,内部却只是一团硬板,那就完全多此一举了。所以本节笔者将带领大家在内核中实现类的多态。以CY001 USB设备驱动为例进行讲解,代码请参考本书工程UsbBaseClass和CY001UsbClass,前者以基类实现设备驱动,后者以子类实现设备驱动。 6.3.1 基类、子类 笔者对基类的要求是能够实现USB设备的最基本要素,使得设备能够在系统中显现,能够正常运行和移除。所以设备栈一定要成功建立,基本的Pnp/Power接口也必须要提供,但用户层接口可以暂不考虑。最终的结果是PnpAdd函数实现得非常完整,因为必须要将设备栈建立起来;EvtDevicePrepareHardware和EvtDeviceReleaseHardware函数也得以完整实现,这样设备能够正确运行和移除,但细节方面的设置如休眠等则以接口留出。 子类必须实现更完善的功能,如休眠、唤醒设置。下面的例子分别对应着基类和子类的实现。 // 配置设备驱动的电源管理功能 NTSTATUS DrvClass::InitPowerManagement() { return STATUS_SUCCESS; } 这是基类的实现,空空如也,子类则要复杂许多倍。 // 配置设备驱动的电源管理功能 NTSTATUS CY001Drv::InitPowerManagement() { NTSTATUS status = STATUS_SUCCESS; WDF_USB_DEVICE_INFORMATION usbInfo; KDBG(DPFLTR_INFO_LEVEL, "[InitPowerManagement]"); // 获取设备信息 WDF_USB_DEVICE_INFORMATION_INIT(&usbInfo); WdfUsbTargetDeviceRetrieveInformation(m_hUsbDevice, &usbInfo); // 设置设备的休眠和远程唤醒功能 // …… 详见代码 return status; } 6.3.2 实现多态 怎么能够实现多态呢?当前,动态对象是在入口函数中创建的,而按照现有逻辑,入口函数是不允许修改的。笔者要提供一个机会,让库使用者可以创建动态对象。为此笔者特地有一个规定,所有库使用者必须定义一个宏,以注册自己的驱动类。 REGISTER_DRV_CLASS(DriverName) 如果不使用子类,则需要定义下面的宏而直接使用基类。 REGISTER_DRV_CLASS_NULL() 那么这两个宏到底有什么作用呢?要看宏定义了: // 注册子类 #define REGISTER_DRV_CLASS(DriverName) DrvClass* GetDrvClass(){ return (DrvClass*)new(NonPagedPool, '10YC') DriverName();} // 注册基类 #define REGISTER_DRV_CLASS_NULL()DrvClass* GetDrvClass(){ return new(NonPagedPool, 'ESAB') UsbBaseClass();} 两个宏都是为了定义一个名称为GetDrvClass的函数。前者注册驱动类的子类,并在GetDrvClass的实现中动态创建子类对象,在返回时将子类对象的指针转换为基类对象指针;后者则声明直接使用基类,并在GetDrvClass的实现中动态创建一个基类对象,并返回其指针。 调用者不必关心被创建的对象到底是来自基类还是子类,他只要使用在基类中定义的接口就可以了,而借助虚拟函数的运行时绑定策略,即可实现多态。 需要注意的是宏REGISTER_DRV_CLASS(DrvClass),其作用和REGISTER_DRV_ CLASS_NULL()是一样的,都将定义一个GetDrvClass函数。 好戏还要看DriverEntry()函数中的实现,重新修改后的函数代码如下: NTSTATUS Driver(DRIVER_OBJECT Driver, UNICODE_STRING Register) { DrvClass* pDriver = GetDrvClass(); return pDriver->DriverEntry(Driver, Register); } 真是无与伦比的简洁,它通过GetDrvClass函数实现了多态,并立刻将驱动的实现交付到了pDriver对象的手中,而pDriver可以是基类,也可以是任意一个从基类继承的子类。 实现多态的核心是两个类注册宏,以及在入口函数中对GetDrvClass函数的调用。需要注意的是,如果用户同时定义了两个宏,那么系统就会因为发现两个完全一样的GetDrvClass函数而使编译失败;反之,如果上述两个宏一个都没有定义,那么在链接时,将因为无法找到函数定义而链接失败。 驱动工程UsbBaseClass使用驱动基类直接驱动CY001 USB设备,从SOURCE文件中可以看到,它含有的编译文件为DrvClass.cpp和GetDrvClass.cpp两个文件,前者是基类的定义文件,后者只有一行代码,即REGISTER_DRV_CLASS_NULL()。这是最简单的驱动工程。 驱动工程CY001USBClass使用驱动子类CY001DrvClass驱动CY001 USB设备,从SOURCE文件中可以看到,它依旧包含了DrvClass.cpp文件,此外还包含了若干个子类的实现文件。 所以,读者只要在DrvClass甚至CY001DrvClass类的基础上实现子类化,并注册新的子类,就能够实现功能扩展。 如图6-6所示是本节所讲的多态实现原理图。 图6-6 多态关系图 6.3.3 测试 编译UsbBaseClass工程的代码,用得到的CY001.sys文件替代system32\drivers目录下的同名文件,以驱动CY001 USB设备。尝试使用本书中的UsbKitApp程序,会发现能够正确枚举到USB设备,但软件的具体功能如获取描述符等,无法正常使用。 以同样的方法测试编译CY001UsbClass工程后得到的CY001.sys文件,并运行UsbKitApp程序以测试,会发现和WDFCY001工程的测试结果完全一样。 6.4 小结 使用本章中介绍的方法,可以轻松实现驱动的类封装。特别是本章介绍的实现多态的方法,可以使得驱动代码的复用性得到很大增强。建议读者在CY001UsbClass的基础上,再子类化一个MyCY001Ex(或其他你喜欢的名字)类,在父类的基础上添加自己的功能,并尝试使用编译这个新工程生成的sys文件,看新类能否发挥作用。
|