新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 讨论HTML、XHTML、Web2.0、Ajax、XUL, ExtJS, jQuery, JSON、Social Networking System(SNS)、Rich Internet Applications (RIA)、Tagging System、Taxonomy(tagsonomy,folkonomy)、XForms、XFrames、XInclude, XBL (XML Binding Language)等话题
    [返回] 中文XML论坛 - 专业的XML技术讨论区XML.ORG.CN讨论区 - XML技术『 HTML/XHTML/Ajax/Web 2.0/Web 3.0 』 → 新书《竹林蹊径:深入浅出Windows驱动开发》连载(感谢 电子工业出版社 特许提供) 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 28189 个阅读者浏览上一篇主题  刷新本主题   平板显示贴子 浏览下一篇主题
     * 贴子主题: 新书《竹林蹊径:深入浅出Windows驱动开发》连载(感谢 电子工业出版社 特许提供) 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     admin 帅哥哟,离线,有人找我吗?
      
      
      
      威望:9
      头衔:W3China站长
      等级:计算机硕士学位(管理员)
      文章:5255
      积分:18406
      门派:W3CHINA.ORG
      注册:2003/10/5

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给admin发送一个短消息 把admin加入好友 查看admin的个人资料 搜索admin在『 HTML/XHTML/Ajax/Web 2.0/Web 3.0 』的所有贴子 点击这里发送电邮给admin  访问admin的主页 引用回复这个贴子 回复这个贴子 查看admin的博客楼主
    发贴心情 

    第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文件,看新类能否发挥作用。

    ----------------------------------------------

    -----------------------------------------------

    第十二章第一节《用ROR创建面向资源的服务》
    第十二章第二节《用Restlet创建面向资源的服务》
    第三章《REST式服务有什么不同》
    InfoQ SOA首席编辑胡键评《RESTful Web Services中文版》
    [InfoQ文章]解答有关REST的十点疑惑

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2011/2/15 11:20:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 HTML/XHTML/Ajax/Web 2.0/Web 3.0 』的所有贴子 点击这里发送电邮给Google AdSense  访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/5/21 1:24:08

    本主题贴数5,分页: [1]

     *树形目录 (最近20个回帖) 顶端 
    主题:  新书《竹林蹊径:深入浅出Windows驱动开发》连载(感谢 电子工业出版社 特..(25352字) - admin,2011年2月15日
        回复:  看着真是费神哈(14字) - sdk1980,2011年11月6日
        回复:  这本书很不错,学习了。。。。(28字) - 卷积内核,2011年10月13日
        回复:  学习一下谢谢(14字) - hjx_221,2011年6月4日
        回复:  第6章 内核驱动C++编程6.1 驱动中的..(26209字) - admin,2011年2月15日

    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    94.238ms