招聘的岗位分析报告:wdm驱动开发之路(5)

来源:百度文库 编辑:中财网 时间:2024/04/29 18:24:15
wdm驱动开发之路(5)


WDM开发之路(五)
    上一篇中我们学习了核心层编程环境和异常处理的一部分,这篇我们继续学习异常处理的余下部分和内存管理。
    在上一篇中,我们知道在Try-Except块中的过滤表达式值可以有以下三种:
EXCEPTION_EXECUTE_HANDLE (1)
EXCEPTION_CONTINUE_SEARCH (0)
EXCEPTION_CONTINUE_EXECUTION (-1)

EXCEPTION_EXECUTE_HANDLE 指示错误处理程序执行Except块中的用户定义异常处理语句。
EXCEPTION_CONTINUE_SEARCH 操作系统将继续扫描堆栈以定位相应的错误处理程序。此时用户定义的错误处理程序将得不到执行,如果找不到合适的其它错误处理程序来执行,此时系统将崩溃。
EXCEPTION_CONTINUE_EXECUTION 指示操作系统返回到出现异常处重试。在内核模式中不能返回此种类型的异常,因为你没办法改变导致异常的情况。
更多的有关异常的信息可以调用以下两个函数来获得:(它们是微软编译器内部所实现的,这也从一个侧面说明了其它厂商的编译器不能生成驱动的原因)
注:微软的驱动程序使用一些专有的格式,比如,vxd就是使用LE格式,但普通Win32程序则使用PE格式文件,由于微软不向其它厂商开放使用许可,所以象borland等厂商的编译器不能生成驱动程序。
GetExceptionCode() 返回当前异常的数值代码。它是一个NTSTATUS类型。此函数只能用于__except表达式和它后面的处理代码中。
    GetExceptionInformation() 返回EXCEPTION_POINTERS结构的内存地址,此结构包含异常相关的详细信息,包括发生时的寄存器详细内容等。
程序中的bug可以导致异常并使系统调用异常处理机制。应用程序开发者应该熟悉Win32 API中的RaiseException函数,它可以生成任意异常。在WDM驱动程序中,你可以调用表中列出的例程。由于下面规则,我不能给你举一个使用这些函数的例子:
    仅当你知道存在一个异常处理代码并知道你真正在做什么时,才可以在非任意线程上下文下生成一个异常。
用于生成异常的服务函数
服务函数    描述
ExRaiseStatus    用指定状态代码触发异常
ExRaiseAccessViolation    触发STATUS_ACCESS_VIOLATION异常
ExRaiseDatatypeMisalignment    触发STATUS_DATATYPE_MISALIGNMENT异常
特别地,不要通过触发异常来告诉你的调用者你一般执行状态中的信息,你完全可以返回状态代码。应该尽量避免使用异常,因为堆栈回卷机制非常消耗资源。
系统错误 BugCheck(导致系统不能继续运行的致命错误)
Bug check是系统检测到的错误,一旦发现这种错误,系统立即以一种可控制的方式关闭。许多内核模式部件运行时都进行一致性检测,如果某个系统部件发现一个不可恢复的错误,将生成一个bug check。如果可能,所有内核模式部件都先登记遇到的错误,然后继续运行,而不是调用KeBugCheckEx,除非这种错误将使系统本身变得不可靠。程序可以在任何IRQL上调用KeBugCheckEx。如果程序发现一个不可恢复的错误,并且该程序继续运行将会破坏系统,那么该程序就调用KeBugCheckEx函数,这个函数将使系统以一种可控制的方式关闭。
当内核模式中出现不可恢复错误时,会出现一个称为死亡蓝屏(BSOD blue screen of death)的画面,驱动程序开发者应该十分熟悉它。在内部,这种错误被称为bug check,它的主要特征是,系统尽可能以正常的方式关闭并弹出一个死亡蓝屏。一旦死亡蓝屏出现,则表明系统已经死掉必须重启动。






可以按下面方式调用KeBugCheckEx:
KeBugCheckEx(bugcode, info1, info2, info3, info4);
bugcode是一个数值,指出出错的原因,info1、info2等是整型参数,将出现在死亡蓝屏中以帮助程序员了解错误细节。该函数从不返回(!)。
我不将解释死亡蓝屏中的信息。Microsoft自己的bugcheck代码在DDK头文件bugcodes.h中列出;对该代码的更完整解释以及各种参数的含义可以在KBase文章Q103059 “Descriptions of Bug Codes for Windows NT”中找到。
如果需要,你也可以创建自己的bugcheck代码。Microsoft定义的值是从1(APC_INDEX_MISMATCH)到0xDE(POOL_CORRUPTION_IN_FILE_AREA)之间的整数。为了创建你自己的bugcheck代码,你需要定义一个整型常量(类似STATUS_SEVERITY_SUCCESS的状态代码),并指出customer标志或非0的facility代码。例如:
#define MY_BUGCHECK_CODE 0x002A0001
...
KeBugCheckEx(MY_BUGCHECK_CODE, 0, 0, 0, 0);
使用非0的facility代码(例子中为42)或customer标志(例子中为0)是为了与Microsoft使用的代码区分开。
现在,我已经告诉你如何生成自己的BSOD,那么我们什么时候使用它呢?回答是决不,或者仅在驱动程序的内部调试中使用。我们不可能写出这样的驱动程序,它发现了一个错误并且只有通过关闭系统才能解决。更好的做法是记录这个错误(使用错误登记工具,如系统事件日志或WMI)并返回一个状态码。
这是在不得已的情况下为了尽可能减少损失而采取的措施。如果不这样处理,系统将以一种不可预料的方式结束运行,可能会造成不可挽回的损失。我曾经在编写驱动时发生过源代码无缘故地丢失的情况,这也是不健壮的驱动程序产生的副作用。
下面我们将要一起学习核心层的内存管理。在windows下由于采用了保护模式,内存和实模式的dos下完全不同。记得在dos下我们可以访问几乎所有的内存区域,如访问硬盘的Bios记取硬的序列号,主板的B ios区以记取主板序列号,破除BIOS口令等。但在Windows下,系统对内存进行分区访问,分为系统区和用户区。并且由于采用了硬盘交换技术,我们可以申请超过实际内存大小很多的内存。但此时我们得到的内址地址再也不是内存的物理地址了,取而代之是的虚拟内存地址。
    虽然Windows这样的帖心保护给我们写应用程序带来了很大方便,不用再费心考虑内存是否够用的问题,但也给我们写驱动程序带来了很多麻烦。我们在驱动程序中使用内存时要加倍小心,否则一丁点小的错误都会要了系统的命(死亡蓝屏或立即死机)。
    首先我们来看几个概念性的问题:
虚拟内存:
    把一定的空间划分成固定大小的块,这些块在技术上叫“页”,X86系列的处理器的页的大小为4k,alpha系统的为8K,这些页可以放在内存中,也可以放到磁盘上。对于程序来说,它们所使用的内存就是由一系列处理过的内存地址表示的区域(虚拟内存地址),可能在物理内存中也可能在磁盘上的文件中。它并不清楚所使用的内存的真实形态。这一切由操作系统透明处理。当然,应用程序也可以显式地申请操作系统告诉它这些内存的物理地址。
如上图所示,假设一个程序使用了24K的内存,我们把它分成6页。如果系统只有12K的物理内存可用时,它会把频繁使用或将要使用的区域放到物理内存中,以加快运行速度和满足内存分配需要。但如果所有的应用程序经常所需的内存总和大大超过物理内存的数量,那操作系统就会不断地在物理内存和磁盘文件之间换进换出这些分页块,虽然还能运行程序,但这将导致系统变得很慢。这是用时间换空间的方法了。
分页一(物理内存中)
分页二(磁盘上)
分页三(物理内存中)
分页四(磁盘上)
分页五(物理内存中)
分页六(磁盘上)









可分页内存和不可分页内存:
    上面讲了内存可以分页,但并不是每个分页内存区都可以换到磁盘文件中。我们把可以换出来的分页叫作“可分页内存”,只能永久驻留在物理内存中而不能被换出来页的叫作“非分页内存”。
如果在DISPATCH_LEVEL或者更高的中断级中访问分页内存,就会引起缺页故障,内核会崩溃。如果在PASSIVE_LEVEL中断级访问没有驻留在物理内存中的分页内存时,内核会阻塞我们的线程,直到内存管理器把此页重新装回物理内存中。
    不要随意使用非分页内存,因为系统的资源是有限的,如果永久驻留在物理内存中的页太多,将导致可分页内存更加频繁地进行交换,降低系统性能。
    在开发驱动程序时我们必须遵守这样几个原则:
决不(或几乎从不)直接引用用户模式的内存地址。
因为我们不能确切知道用户模式内存地址所指向的真实物理地址。
执行在高于或等于DISPATCH_LEVEL级的代码不可以引发页故障。
在驱动程序的checked版中,你可以使用PAGED_CODE预处理宏(在wdm.h中声明)来帮助发现有违背这个规则的代码。例如:
NTSTATUS DispatchPower(PDEVICE_OBJECT fdo, PIRP Irp)
{
  PAGED_CODE()
  ...
}
PAGED_CODE包含条件编译语句。在checked-build方式中,如果当前IRQL太高,它就打印出一行信息并生成一个断言失败。在free-build方式中,它不做任何事。如果测试驱动程序时包含DispatchPower代码的页正好在内存中,那么你不会发现已经在一个提升的IRQL上调用了DispatchPower函数。即使这样,PAGED_CODE仍能查出问题。如果该页碰巧不在内存中,系统将产生一个bug check。
我们可以调用以下几个函数来分配可分而和非可分页内存块:
ExAllocatePool(…)
调用方式如下:
PVOID p = ExAllocatePool(type, nbytes);
type参数是表2中列出的POOL_TYPE枚举常量,nbytes是要分配的字节数。返回值是一个内核模式虚拟地址指针,指向已分配的内存块。如果内存不足,则返回一个NULL指针。如果指定的内存池类型为“must succeed”类型,即NonPagedPoolMustSucceed或NonPagedPoolCacheAlignedMustS,那么内存不足将导致一个代码为MUST_SUCCEED_POOL_EMPTY的bug check。
表2
内存池类型    描述
NonPagedPool    从非分页内存池中分配内存
PagedPool    从分页内存池中分配内存
NonPagedPoolMustSucceed    从非分页内存池中分配内存,如果不能分配则产生bugcheck
NonPagedPoolCacheAligned    从非分页内存池中分配内存,并确保内存与CPU cache对齐
NonPagedPoolCacheAlignedMustS    与NonPagedPoolCacheAligned类似,但如果不能分配则产生bugcheck
PagedPoolCacheAligned    从分页内存池中分配内存,并确保内存与CPU cache对齐
调用ExAllocatePool时的最基本原则是被分配内存块是否可以交换出内存。这取决于驱动程序的哪一部分需要访问这块内存。如果在大于或等于DISPATCH_LEVEL级上使用该内存块,那么必须从非分页池中分配内存。如果你总是在低于DISPATCH_LEVEL级上使用内存块,那么既可以从非分页池中分配内存也可以从分页池中分配内存。
你获得的内存块至少是按8字节边界对齐的。如果把某结构的实例放到分配的内存中,那么编译器赋予结构成员的4或8字节偏移在新内存中也将是4或8字节偏移。但在某些RISC平台上,结构成员可能以双字和四字对齐。出于性能上的考虑,希望内存块能适合处理器cache行的最少可能数,使用XxxCacheAligned类型代码可以达到这个要求。如果请求的内存多于一页,那么内存块将从页的边界开始。
ExAllocatePoolWithTag
调用ExAllocatePool是从内核模式堆中分配内存的标准方式。另一个函数ExAllocatePoolWithTag,与ExAllocatePool稍有不同,它提供了一个有用的额外特征。当使用ExAllocatePoolWithTag时,系统在你要求的内存外又额外地多分配了4个字节的标签。这个标签占用了开始的4个字节,位于返回指针所指向地址的前面。调试时,如果你查看分配的内存块会看到这个标签,它帮助你识别有问题的内存块。例如:
PVOID p = ExAllocatePoolWithTag(PagedPool, 42, 'KNUJ');
在这里,我使用了一个32位整数常量作为标签值。在小结尾的计算机如x86上,组成这个标签的4个字节的顺序与正常拼写相反。
WDM.H中声明的内存分配函数受一个预处理宏POOL_TAGGING控制。WDM.H(NTDDK.H中也是)中无条件地定义了POOL_TAGGING,结果,无标签的函数实际上是宏,它真正执行的是有标签函数并加入标签‘ mdW’(指明为WDM的内存块)。如果在未来版本的DDK中没有定义POOL_TAGGING,那么带标签函数将成为无标签函数的宏。Microsoft现在还没打算改变POOL_TAGGING的设置。
由于POOL_TAGGING宏的存在,当你在程序中调用ExAllocatePool时,最终被调用的将是ExAllocatePoolWithTag。如果你关闭了该宏,自己去调用ExAllocatePool,但ExAllocatePool内部仍旧调用ExAllocatePoolWithTag并带一个‘enoN’(即None)的标签。因此你无法避免产生内存标签。所以你应该明确地调用ExAllocatePoolWithTag并加上一个你认为有意义的标签。实际上,Microsoft强烈鼓励你这样做。
ExAllocatePool的其它形式
尽管ExAllocatePoolWithTag函数是分配堆内存时应该使用的函数,但在某些特殊场合你也可以使用该函数的另外两种形式: 
•    ExAllocatePoolWithQuota 分配一块内存并充入当前线程的调度配额中,该函数仅用于顶层驱动程序,如文件系统驱动程序或其它运行在非任意线程上下文中的驱动程序。 
•    ExAllocatePoolWithQuotaTag 同上,但加入一个标签。 
释放内存块
调用ExFreePool可以释放由ExAllocatePool分配的内存块:
ExFreePool((PVOID) p);
你确实需要记录分配的内存以便在该内存不再需要时释放它,因为没有人为你做这些事。例如,在AddDevice函数中,有一个IoRegisterDeviceInterface调用,该函数存在副作用:它分配了一块内存以保存接口名。你有责任在以后释放该内存。
不用说,访问从内核模式内存池中分配来的内存必须格外小心。因为驱动程序代码可能执行在处理器的最高特权模式下,在这里,系统对内存数据没有任何保护。
运行时控制分页能力
表3列出了一些服务函数,你可以在运行时使用它们调整驱动程序的分页布局。这些函数的功能是释放被不再需要的代码和数据所占用的物理内存。在第八章中,我将讲述如何向电源管理器寄存你的设备,这样,在一段不活动时期后设备可以自动掉电。掉电期间是释放锁定内存页的最佳时期。
表3. 动态锁定和解锁驱动程序占用内存页的例程
服务函数    描述
MmLockPagableCodeSection    锁定含有给定地址的代码段
MmLockPagableDataSection    锁定含有给定地址的数据段
MmLockPagableSectionByHandle    用MmLockPagableCodeSection返回的句柄锁定代码段(仅用于Windows 2000)
MmPageEntireDriver    解锁所有属于某驱动程序的页
MmResetDriverPaging    恢复整个驱动程序的编译时分页属性
MmUnlockPagableImageSection    为一个锁定代码段或数据段解锁
限于篇幅,这类函数详细的用法可以参见DDK文档说明。这部分我们主要学习了内存管理部分。如我们前面所说,内存管理在内核编程中是非常重要的。通常我们在应用程序编写中的不良习惯都不应该带到驱动程序开发中。你必须清楚一点:在用户态最多只是弹出一个提示框的故障,在内核模式中等待你的将是“死机”。