班布蚋是什么:WDM驱动开发之路(4)

来源:百度文库 编辑:偶看新闻 时间:2024/04/27 13:39:00
WDM驱动开发之路(4)


走向WDM开发之路(四)
前面讲解了驱动程序原理和驱动程序的基本结构,这一节中我们将要一起学习核心层编程环境。
    大家都知道,普通的应用程序的运行需要一套API支持,在核心层也一样,并且应用层的API在核心层几乎不能使用(如果严格地说的话也有少许API可以同时用于核心层和应用层的)。核心层API按其用途分为以下几类:
I/O管理器类,此类函数以Io打头,这些函数用来和I/O管理器打交道的。
进程结构模块相关函数,此类函数以Ps打头。创建并管理内核模式的线程。
Executive执行支持函数,这类函数以Ex打头。提供堆管理和同步服务。
对象管理类函数,提供各种数据对象管理功能,此类函数以Ob打头。
安全引用监视类函数,使文件系统驱动程序执行安全检测。通常I/o请求到达驱动程序端时系统I/O管理器已经作了安全检测。此类函数以Se打头。
内存管理类函数,控制页表,页表提供虚拟内存到物理内存之间映射关系的定义,此类函数以Mm作为前缀。
运行时间库,这些函数以Rtl打头,提供一些常用函数,比如列表和串管理等,在内核模式程序中不能再调用ANSI标准函数了,以这些函数来代替它们的功能。
内核函数,这些函数以Ke打头。
内核流IRP管理函数,此类函数以Ks打头。
Win32例程调用函数。此类函数以Zw打头。通常情况下,内核模式程序不能调用提供给Win32应用程序的API函数,DDK为了使驱动程序也能调用Win32用户态api,提供了这样一组函数.。不过DDK中只提供了少数这样的函数给驱动程序调用。此类函数主要提供文件系统和注册表数据库的访问功能。
电源管理类函数,此类函数以Po打头。
硬件抽象层函数,此类函数以Hal打头。一般情况下,Windows NT/2K操作系统在硬件和核心层之间提供一组硬件抽象层函数来实现系统功能,这样可以实现驱动程序硬件无关的跨平台(源码级),核心层驱动只能调用这些函数来实现它们需要的功能。
    在写驱动程序时,我们经常需要作一些内存访问和字符串运算等操作。在写应用程序时我们自然会想到调用ANSI标准库函数来实现这些功能,但在核心层,我们通常应调用Rtl打头的类似函数来实现这些功能。严格意义上来说,标准运行库中的一些函数也可以在核心层使用,但由于你需要搞清很多不同的细节,所以最好调用DDK中提供的Rtl族函数来实现。这将提供最好的调用安全性(不会因细节不同原因而导致宕机)。
    调用DDK中的函数需要注意一些问题,概括如下:
1.    花括号问题
我们调用的DDk函数中有一部分其实是宏定义,而且DDK中的一些宏定义其实很差劲,比如以下定义:
#define ReMoveHeadList(ListHead)
(Listhead)->Flink;
{RemoveEntryList((ListHead)->Flink)}
如果以以下方式调用RemoveHeadList,则将导致编译错误:
if(SomethingInList)
Entry=RemoveHeadList(list);
使这个调用安全的唯一方法是使用花括号。
If(SomethingInList)
{
Entry=RemoveHeadList(list);
}
因此,为了调用安全起见,建议最好在所有的if、for和while等语句中使用花括号。

2.    函数调用的边效
同样由于DDK中的许多函数其实是宏定义,我们都知道应该避免在宏的参数中使用带有边效的表达式,原因是宏的参数可能多次被调用。如:
int a=2,b=42,c;
c=min(a++,b);
而这个min()函数其实是一个定义如下的宏:
#define min(x,y) ((x)<(y)?(x):(y))
如果我们用上面的表达式调用这个宏的话,将会产生边效。将上面的调用展开如下:
a++
最后我们将发现a的值变成了4,而min()返回的值是3。这是因为a++这个表达式被调用了两次导致的。Min()的返回值是在第一次调用后得到的。
我们在编写驱动程序时要明确一点,那就是内存的使用必须非常严谨,该释放的一定要释放,该确定大小的一定要计算确定,而不能图省事分配一个大数组。如果不严谨的话会造成很多宕机的后果而找不到原因。
在驱动程序中,为了描述内核结构,我们大量使用使用Object这个词,这里的对象不是指C++对象而是,而是包含一些可用内核函数来访问的域的数据结构。在实际存储上和C++的类有相似之处,但意义完全不同。
还有一点我们必须明确,那就是处理器分中断级。
我们在微机原理中都学过中断,中断分硬件中断和软件中断。一个中断可以打断正在运行的普通程序的执行,并强制处理器执行一段代码。并且中断有优先级之分,低优先级的中断可以被高优先级的中断打断,软件中断也可以被硬件中断打断。这样可以保证重要的任务优先得到执行。
抽象的处理器中断级

中断性质    IRQL(中断级)    描述
无中断    PASSIVE_LEVEL    常规线程执行
软件中断    APC_LEVEL    异步过程调用执行
    DISPATCH_LEVEL    线程调度,延时过程调用执行
硬件中断    DIRQL    设备中断请求级处理程序执行
    PROFILE_LEVEL    配置文件定时器
    CLOCK2_LEVEL    时钟
    SYNCH_LEVEL    同步级
    IPI_LEVEL    处理器之间中断级
    POWER_LEVEL    电源故障级

驱动程序通常只使用这之中的三个中断级。驱动程序分发例程一般在PASSIVE_LEVEL级调用,此时为没有中断发生的时候。驱动程序中的许多回调例程在DISPATCH_LEVEL级运行。当发生硬件中断时,中断服务例程在DIRQL级(设备中断请求级)运行。
DIRQL是在处理器上可用的许多硬件中断级的总称。其中包括磁盘中断,串口中断,并口中断等等,并且它们之间也有优先级高低的问题。比如当同时发生磁盘和串口中断时,磁盘中断比串口中断优先得到执行。通常情况下,一个驱动程序只有一个DIRQL级,但是如果需要的话,系统也允许它有超过一个的DIRQL级。驱动程序的中断优先级用它的DIRQL的最高级表示(如果一个驱动中只有一个DIRQL的话,驱动程序的优先级则是这个DIRQL的优先级,否则 则是里面最高DIRQL的优先级)。驱动程序的优先级很重要,因为它决定了这个驱动程序可以执行的操作类型。例如,驱动程序的硬件中断程序不能访问换出到交换文件中的内存。
一个驱动程序有可能在任何时候被比它的优先级高的中断程序打断。这同时也意味着驱动程序的中断服务例程可以中断它自己的分发例程。如果一个常规的驱动程序例程要与它自己的硬件打交道,它可以使用一个内核函数把自己的中断级临时提高到DIRQL,这时,它自己的中断级别低的中断程序也将被停止执行。
与中断优先级对应的还有调度优先级。所有线程通常在最低优先级PASSIVE_LEVEL运行,而调度程序使用优先级值确定下一步要运行哪个线程。在驱动中,它可以为调用它的线程指定一个临时优先级,这样可以提高系统的响应速度。
处理硬件中断的服务例程会停止正常的程序的执行。因此,必须使中断处理程序尽快地执行完毕。不是必需在中断处理程序中运行的代码应该尽量推迟到以后执行。为此,操作系统提供延迟过程调用(DPC)例程,这些例程在DISPATCH_LEVEL级运行,用于处理不是必须在中断处理例程中操作的中断相关代码。提示当前I/O完成的请求操作必须在DISPATCH_LEVEL级运行,因此,它应该在DPC例程中完成。
人总会犯错误的,软件也不例外。我们必须对软件中的错误作出恰当的处理。如果错误很少或不影响程序的执行,我们对它加以适当的纠正。如果程序错误很严重,那我们必须清除它分配的资源,并停止程序的执行。如果发生的是可以忽略的错误,那可以继续执行下面的代码。我们有三种方式来处理错误:状态代码、结构化异常处理、和Bug Check。通常情况下内核模式函数会向调用者返回一个状态代码来表示执行的结果是成功还是失败。
通常情况下,内核模式的函数都会返回一个32位的NTSTATUS状态码。此状态码标示出执行是否成功。NTSTATUS是一个由多个子域组成的32位整数,如图3-2。高两位(Severity)指出状态的严重性――成功、信息、警告、错误。客户位(Customer)是一个标志,完成的IRP将携带一个表明完成状态的状态代码,如果这个状态代码中的Customer标志被设置,那么这个状态代码将被不修改地传回应用程序(应用程序通过调用GetLastError函数获得)。通常,状态代码在返给应用程序前要翻译成Win32错误代码(Win32错误代码可以在KBase Q113996文章中查到)。facility代码指出该状态是由哪个系统部件导致的,一般用于减少开发组之间的代码关联。剩下的16位代码指出实际的状态。我们应该总是检测例程的返回状态。为了不让大量的错误处理代码干扰例子代码所表达的实际意图,我经常省略代码片段中错误检测部分,但你在实际练习中不要效仿我。
如果状态码高位为0,那么不管其它位是否设置,该状态代码仍旧代表成功。所以,绝对不要用状态代码与0比较来判断操作是否成功,应该使用NT_SUCCESS宏:
NTSTATUS status = SomeFunction(...);
if(!NT_SUCCESS(status))
{
  
}
不仅要检测调用例程的返回状态,还要向调用你的例程返回状态代码。在上一章中,我讲述了两个驱动程序例程,DriverEntry和AddDevice,它们都定义了NTSTATUS返回代码。所以,如果这些例程成功,则应返回STATUS_SUCCESS。如果在某个地方出错,应返回一个适当的错误状态代码,有时函数返回的状态代码就是出错函数返给你的状态代码。
并不是所有被调用例程导致的错误都要处理,有些错误是可以忽略的。例如,在电源管理中,带有IRP_MN_POWER_SEQUENCE子类型的电源管理请求,使用它可以避免上电过程中不必要的状态恢复过程。这个请求不仅对你是可选的,而且总线驱动程序在实现该请求上也是可选的。所以如果该请求执行失败,你不用做任何处理,继续其它工作。同样,你也可以忽略IoAllocateErrorLogEntry产生的错误,因为不能向错误登记表添加一条记录根本不是什么严重错误。
Windows 提供了一种处理异常情况的方法,它可以帮助我们避免潜在的系统崩溃。结构化异常处理与编译器的代码生成器紧密集成,它允许你在自己的代码段周围加上保护语句,如果被保护代码段中的任何语句出现异常,系统将自动调用异常处理程序。结构化异常处理还便于你提供清除语句,不管控制以何种方式离开被保护代码段,清除代码都会被执行。
许多读者并不熟悉结构化异常方法(在vc++中也支持结构化异常处理,在DELPHI中你将看到它生成的代码中便应用了结构化异常处理技术),所以我在这里先解释一些基本概念。使用这个方法可以写出更好更稳固的代码。在许多情况下,WDM驱动程序例程接收到的参数都是经过其它代码严格检验的,一般不会成为导致异常的原因。但我们仍要遵循这样基本原则:对用户模式虚拟内存直接引用的代码段应该用结构化异常帧保护起来。这样的引用通常发生在调用MmProbeAndLockPages、ProbeForRead,和ProbeForWrite函数时(以后会讲这些函数)。
结构化异常机制可以使内核模式代码在访问一个非法的用户模式地址后避免系统崩溃。但它不能捕捉其它处理器异常,例如被零除或试图访问非法的内核模式地址。从这一点上看,这种机制在内核模式中不象在用户模式中那样具有通用性。 
当使用Microsoft编译器时,你可以使用C/C++的Microsoft扩展,它隐藏了使用某些操作系统原语的复杂性。例如,用__try语句指定被保护代码段,用__finally语句指定终止处理程序,用__except语句指定异常处理程序。
最好总使用带有双下划线的关键字,如__try、__finally、和__except。在C编译单元中,DDK头文件WARNING.H也把try、finally、和except宏定义成这些双下划线的关键字。DDK例子程序使用这些宏而不是直接使用带双下划线的关键字。有一点需要注意:在C++编译单元中,try语句必须与catch语句成对出现,这是一个完全不同的异常机制,是C++语言的一部分。C++异常机制不能用于驱动程序中,除非你自己从运行时间库中复制出某些基础结构。Microsoft不推荐那样做,因为这将增加驱动程序的内存消耗并增大执行文件的大小。 
Try-Finally块
从try-finally块开始解释结构化异常处理最为容易,用它你可以写出象下面这样的清除代码:
__try
{
  
}
__finally
{
  
}
在这段伪代码中,被保护体是一系列语句和子例程。通常,这些语句会有副作用,如果没有副作用,就没有必要使用一个try-finally块,因为没有东西需要清除。终止处理程序包含一些恢复语句,用于部分或全部恢复被保护体产生的副作用。
语法上,try-finally按下面方式工作。首先,计算机执行被保护体。由于某种原因控制离开被保护体,计算机执行终止处理程序。
这里有一个简单的例子:
LONG counter = 0;
__try
{
  ++counter;
}
__finally
{
  --counter;
}
KdPrint(("%d\n", counter));
首先,被保护体执行并把counter变量的值从0增加到1。当控制穿过被保护体右括号后,终止处理程序执行,又把counter减到0。打印出的值将为0。
下面是一个稍复杂的修改:
VOID RandomFunction(PLONG pcounter)
{
  __try
  {
    ++*pcounter;
    return;
  }
  __finally
  {
    --*pcounter;
  }
}
该函数的结果是:pcounter指向的整型值不变,不管控制以何种原因离开被保护体,包括通过return语句或goto语句,终止处理程序都将执行。开始,被保护体增加计数器值并执行一个return语句,接着清除代码执行并减计数器值,之后该子程序才真正返回。
下面例子可以加深你对try-finally语句的理解:
static LONG counter = 0;
__try
{
  ++counter;
  BadActor();
}
__finally
{
  --counter;
}
这里我们调用了BadActor函数,我假定该函数将导致某种异常,这将触发堆栈回卷。作为回卷“执行和异常堆栈”过程的一部分,操作系统将调用我们的恢复代码并把counter恢复到以前的值。然后操作系统继续回卷堆栈,所以不论我们在__finally块后有什么代码都得不到执行。
Try-Except块
结构化异常处理的另一种使用方式是try-except块:
__try
{
  
}
__except()
{
  
}
try-except块中的被保护代码可能会导致异常。你可能调用了象MmProbeAndLockPages这类的内核模式服务函数,这些函数使用来自用户模式的指针,而这些指针并没有做过明确的有效性检测。也许是因为其它原因。但不管什么原因,如果程序在通过被保护代码段时没有发生任何错误,那么控制将转到异常处理代码后面继续执行,你可以认为这是正常情况。如果在你的代码中或任何你调用的子例程中发生了异常,操作系统将回卷堆栈,并对__except语句中的过滤表达式求值。结果将是下面三个值中的一个: 
•    EXCEPTION_EXECUTE_HANDLER 数值上等于1,告诉操作系统把控制转移到你的异常处理代码。如果控制走到处理程序的右大括号之外(如执行了return语句或goto语句),那么控制将转到紧接着异常处理代码的后面继续执行。(我看过了平台SDK中关于异常控制返回点的文档,但那不正确) 
•    EXCEPTION_CONTINUE_SEARCH 数值上等于0,告诉操作系统你不能处理该异常。系统将继续扫描堆栈以寻找其它处理程序。如果没有找到为该异常提供的处理程序,系统立即崩溃。 
•    EXCEPTION_CONTINUE_EXECUTION 数值上等于-1,告诉操作系统返回到异常发生的地方。
(限于篇幅,我们将在下一节中讲解错误处理其余部分和内存管理方面的内容)