Windows驱动开发要点总结一

582 篇文章 3 订阅 ¥99.90 ¥99.00
282 篇文章 5 订阅

1 概述

驱动程序大体可分为两类三种:
第一类:传统型驱动
    传统型驱动的特点就是所有的IRP都需要自己去处理,自己实现针对不同IRP的派发函数。其可以分
为以下两种:
    1. Nt式驱动:此驱动通过注册系统服务来加载,并且不支持即插即用功能(即没有处理IRP_MJ_PNP
这个IRP)。
    2. WDM驱动:此驱动不通过注册系统服务来加载,需啊哟自己编写inf文件。同时,它与NT式驱动相
比最大的特点就是支持即插即用功能。
    第二类:微过滤驱动
    微过滤驱动是微软推出的一个驱动框架。它将驱动程序内创建设备对象之类的操作全部封装了,让
用户无需理会此部分繁杂的工作。用户只需要针对不同的IRP处理好他们响应的前-后操作还有用户态与
内核态的通信即可,即可以理解为微过滤驱动对IRP的处理类似于用户态的API HOOK。

    对于刚开始编写驱动程序的新人来说,使用微过滤驱动是最好不过的了。因为它将大量的内部逻辑
进行了封装,我们只需要实现相应的处理逻辑即可。
    驱动的编译方式有很多种。
    .微软官方推荐使用WDK提供的Build Environments来对相应系统编译驱动,此方法需要用户自行创
建Source文件编译,对于不熟悉的人略显困难
    .VS+ddkwizard。ddkwizard插件会在VS中添加一个DDK工程,它可以生成基本的WDM驱动模板,相对
来讲会稍微方便些,但是还是需要一些配置。
    .直接使用VS编译。对于做惯用户态开发的人来讲,这个是最方便的,目前我用的也是这种方式。它
的配置项会相对多一点,但是相信这不是障碍。下面就介绍一下如何配置VS(以VS2005为例)

VS2005配置驱动编译环境方法:
1. 安装WDK,并配置系统环境变量:WDKROOT-D:\WinDDK\7600.16385.1
2. 启动VS2005,在菜单栏“工具”-“选项”内选择“项目和解决方案”-“VC++目录”依次添加所需的
目录,具体配置如下图:
注意:$(WDKROOT)\inc\api一定要放在第一个,否则会导致编译失败
3. 具体的项目属性按照如下设置:
注:创建项目时选择“Win32项目”
按照以上图片配置好后就可以直接编译驱动程序了,此配置是XP系统的,其他系统的链接不同的库就可
以了。

注:如果在编译过程中遇到如下错误:
error LNK2019: 无法解析的外部符号 @__security_check_cookie@4
请您将“项目属性”-“C/C++”-“代码生成”的“缓冲区安全检查”设为“否”

2 构建和加载

将代码植入内核中的直接方式是使用可加载模块。
正如其名称所示,设备驱动程序通常是用于设备的。然而,通过驱动程序可以引入任何代码。
DDK提供了两种不同的构建环境:检查(checked)构建和自由(free)构建环境。
在开发设备驱动程序时使用检查构建环境,对于发行代码则使用自由构建环境。
检查构建环境可以是“开始”菜单的“程序”中 Windows DDK图标组之下的一个链接。打开了构建环境
的命令shell后,将当前目录改为驱动程序目录,并输入命令“build”。理想情况下.不会出现任何错
误,此时就得到了我们的第一个驱动程序。一个提示:要确保驱动程序目录所在位置的完整路径中不包
含任任何空格。

已建立的驱动程序正确加载方法是使用服务控制管理器(Service Control Manager,SCM)。
使用SCM需要创建注册表键,当驱动程序通过SCM加载时,它是不可分页的。
系统上的每个进程都维护唯一的页目录,都拥有自己私有的CR3寄存器的值。
因此,两个不同的进程可以同时访问内存地址0x00400000,但将其转换成两个独立的物理内存地址;这

也是为何一个进程无法查看另一个进程内存的原因。
CPU负责跟踪为软件代码和内存分配环的情况,并在各环之间实施访问限制。

3 基础数据结构

1) DRIVER_OBJECT驱动对象

WDK中对驱动对象的定义  
每个驱动程序都会有一个唯一的驱动对象与之对应  
它是在驱动加载时被内核对象管理程序创建的  


DRIVER_OBJECT 成员说明:
DeviceObject : 每个驱动程序都会有至少一个设备对象。每个设备对象都有一个指向下一个设备对象的


指针,最后一个设备对象指向空。此参数指的是驱动对象的第一个设备对象。设备对象的创建与删除都


是由程序员自行处理的。
DriverName : 驱动名称,由UNICODE_STRING记录。一般格式为\Driver\[DriverName]。
HardwareDatabase :  设备的硬件数据库名称。一般格式为\REGISTRY\MACHINE\HARDWARE\DESCRIPTION


\SYSTEM。
DriverStartIo : 记录StartIO派发函数地址,用于序列化操作。
DriverUnload : 指定驱动卸载时的回调函数地址。
MajorFunction : 记录处理IRP的派发函数的函数地址。
FastIoDispatch : 文件驱动中会用到此成员,用于处理快速IO请求。


2) DEVICE_OBJECT设备对象

WDK定义的设备对象  


DEVICE_OBJECT成员说明:
DriverObject : 指向驱动程序中的驱动对象。如果多个设备对象属于同一个驱动程序,则它们所指的驱


动对象是相同的。
NextDevice : 指向下一个设备对象。
AttachedDevice : 指向下一个设备对象。如果有更高一层的驱动附加到这个驱动的时候,其指向的就是


更高一层的那个驱动。
CurrentIrp : 使用StartIO派发函数的时候,它指向的是当前的IRP结构
Flags : 标志域,32位无符号整形,其值有以下几种:
DO_BUFFERED_IO : 读写操作使用缓冲方式(系统复制缓冲区)访问用户模式数据。
DO_EXCLUSIVE : 一次只允许一个线程打开设备句柄。
DO_DIRECT_IO : 读写操作使用直接方式(内存描述表)访问用户模式数据。
DO_DEVICE_INITIALIZING : 设备对象正在初始化。
DO_POWER_PAGABLE : 必须在PASSIVE_LEVEL级上处理IRP_MJ_PNP请求。
DO_POWER_INRUSH : 设备上电期间需要大电流。
DeviceExtension : 指向设备扩展对象。设备扩展对象是一个程序员自己定义的结构体。在驱动程序中


,应该尽量避免全局变量的使用,因为全局变量不容易同步,所以将全局变量存在设备扩展中是一个非


常好的解决方案。
DeviceType : 设备类型,常用的设备类型有:
FILE_DEVICE_BEEP:蜂鸣器设备对象。
FILE_DEVICE_CD_ROM:CD光驱设备对象。
FILE_DEVICE_CD_ROM_FILE_SYSTEM:CD光驱文件系统设备对象。
FILE_DEVICE_CONTROLLER:控制器设备对象。
FILE_DEVICE_DATALINK:数据链设备对象。
FILE_DEVICE_DFS:DFS设备对象。
FILE_DEVICE_DISK:磁盘设备对象。
FILE_DEVICE_DISK_FILE_SYSTEM:磁盘文件系统设备对象。
FILE_DEVICE_FILE_SYSTEM:文件系统设备对象。
FILE_DEVICE_INPORT_PORT:输入端口设备对象。
FILE_DEVICE_KEYBOARD:键盘设备对象。
FILE_DEVICE_MAILSLOT:邮槽设备对象。
FILE_DEVICE_MIDI_IN:MIDI输入设备对象。
FILE_DEVICE_MIDI_OUT:MIDI输出设备对象。
FILE_DEVICE_MOUSE:鼠标设备对象。
FILE_DEVICE_MULTI_UNC_PROVIDER:多UNC设备对象。
FILE_DEVICE_NAMED_PIPE:命名管道设备对象。
FILE_DEVICE_NETWORK:网络设备对象。
FILE_DEVICE_NETWORK_BROWSER:网络浏览器设备对象。
FILE_DEVICE_NETWORK_FILE_SYSTEM:网络文件系统设备对象。
FILE_DEVICE_NULL:空设备对象。
FILE_DEVICE_PARALLEL_PORT:并口设备对象。
FILE_DEVICE_PHYSICAL_NETCARD:物理网卡设备对象。
FILE_DEVICE_PRINTER:打印机设备对象。
FILEDEVICE_SCANNER:扫描仪设备对象。
FILE_DEVICE_SERIAL_MOUSE_PORT:串口鼠标设备对象。
FILE_DEVICE_SERIAL_PORT:串口设备对象。
FILE_DEVICE_SCREEN:屏幕设备对象。
FILE_DEVICE_SOUND:声音设备对象。
FILE_DEVICE_STREAMS:流设备对象。
FILE_DEVICE_TAPE:磁带设备对象。
FILE_DEVICE_TAPE_FILE_SYSTEM:磁带文件系统设备对象。
FILE_DEVICE_TRANSPORT:传输设备对象。
FILE_DEVICE_UNKNOW:未知设备对象。
FILE_DEVICE_VIDEO:视频设备对象。
FILE_DEVICE_VIRTUAL_DISK:虚拟磁盘设备对象。
FILE_DEVICE_WAVE_IN:声音输入设备对象。
FILE_DEVICE_WAVE_OUT:声音输出设备对象。
FILE_DEVICE_8042_PORT:8042端口设备。
FILE_DEVICE_NETWORK_REDIRECTOR:网卡设备对象。
FILE_DEVICE_BATTERY:电池设备对象。
FILE_DEVICE_BUS_EXTENDER:总线扩展设备对象。
FILE_DEVICE_MODEM:调制解调器设备对象。
FILE_DEVICE_VDM:VDM设备对象。
FILE_DEVICE_MASS_STORAGE:大容量存储设备对象。
FILE_DEVICE_SMB:SMB设备对象。
FILE_DEVICE_KS:内核流设备对象。
FILE_DEVICE_CHANGER:充电设备对象。
FILE_DEVICE_SMARTCARD:智能卡设备对象。
FILE_DEVICE_ACPI:ACPI设备对象。
FILE_DEVICE_DVD:DVD设备对象。
根据设备的需要,需要填写响应的设备类型。当制作虚拟设备时,应当选择FILE_DEVICE_UNKONWN类型的
设备。
StackSize : 在多层驱动情况下,驱动与驱动之间会形成类似堆栈的结构。IRP会依次从最高层传递到最
底层。StackSize就是驱动的层数。
AlignmentRequirement : 设备在大容量传输的时候,需要内存对齐,以保证传输速度。


3) 设备扩展

设备对象中只包含了设备的基本信息,如果需要保存其他的信息可以使用设备扩展。
设备扩展是由程序员自定义的,可以按照自己的需要添加相关的信息。设备扩展保存在非分页内存中。
在驱动程序中应该尽量避免使用全局函数,因为全局函数往往导致函数的不可重入性。将全局变量以设
备扩展方式储存,加以适当的同步保护措施是一个很好的解决方案。除此之外设备扩展往往还会记录一
下信息:
设备对象的反向指针。
设备状态或驱动环境信息。
中断对象指针。
控制器对象指针。
由于设备扩展是驱动程序专用的,它的结构必须在驱动程序的头文件中定义。


4 字符串



4.1 使用字符串结构 

传统 C语言的字符串相当的不安全。很容易导致缓冲溢出漏洞。这是因为没有任何地
方确切的表明一个字符串的长度。仅仅用一个’\0’字符来标明这个字符串的结束。一旦碰
到根本就没有空结束的字符串(可能是攻击者恶意的输入、或者是编程错误导致的意外),
程序就可能陷入崩溃。 
  使用高级C++特性的编码者则容易忽略这个问题。因为常常使用std::string和CString
这样高级的类。不用去担忧字符串的安全性了。 
  在驱动开发中,一般不再用空来表示一个字符串的结束。而是定义了如下的一个结构: 


typedef struct _UNICODE_STRING { 
    USHORT Length;        // 字符串的长度(字节数) 
    USHORT MaximumLength;    // 字符串缓冲区的长度(字节数) 
    PWSTR  Buffer;       // 字符串缓冲区 
  } UNICODE_STRING, *PUNICODE_STRING; 
 
  以上是 Unicode 字符串,一个字符为双字节。与之对应的还有一个 Ansi 字符串。Ansi
字符串就是 C 语言中常用的单字节表示一个字符的窄字符串。 
 
  typedef struct _STRING { 
    USHORT Length; 
    USHORT MaximumLength; 
    PSTR Buffer; 
  } ANSI_STRING, *PANSI_STRING; 


在驱动开发中四处可见的是 Unicode 字符串。因此可以说:Windows 的内核是使用
Uincode 编码的。ANSI_STRING 仅仅在某些碰到窄字符的场合使用。
  UNICODE_STRING 并不保证Buffer 中的字符串是以空结束的。因此,类似下面的做法都
是错误的,可能会会导致内核崩溃:




 UNICODE_STRING str; 
  …  
  len = wcslen(str.Buffer);      // 试图求长度。 
  DbgPrint(“%ws”,str.Buffer);    // 试图打印 str.Buffer。 
   
  如果要用以上的方法,必须在编码中保证 Buffer 始终是以空结束。但这又是一个麻烦
的问题。所以,使用微软提供的 Rtl系列函数来操作字符串,才是正确的方法。


4.2 字符串的初始化 

  UNICODE_STRING 结构中并不含有字符串缓冲的空间。以下的代码是完全错误的,内核会立
刻崩溃: 
   
  UNICODE_STRING str; 
  wcscpy(str.Buffer,L”my first string!”); 
  str.Length = str.MaximumLength = wcslen(L”my first string!”) * sizeof(WCHAR); 
 
  以上的代码定义了一个字符串并试图初始化它的值。这样做是不对的。因为 str.Buffer 只是一个未初始化的指针。它并没有指向有意义的空间。相反以下的方法是
正确的: 


 // 先定义后,再定义空间 
UNICODE_STRING str; 
  str.Buffer = L”my first string!”; 
  str.Length = str.MaximumLength = wcslen(L”my first string!”) * sizeof(WCHAR); 
    … … 
   
  上面代码的第二行手写的常数字符串在代码中形成了“常数”内存空间。这个空间位于
代码段。将被分配于可执行页面上。一般的情况下不可写。为此,要注意的是这个字符串空
间一旦初始化就不要再更改。否则可能引发系统的保护异常。实际上更好的写法如下: 


//请分析一下为何这样写是对的: 
UNICODE_STRING str = {  
    sizeof(L”my first string!”) – sizeof((L”my first string!”)[0]), 
    sizeof(L”my first string!”), 
    L”my first_string!” }; 
 
  但是这样定义一个字符串实在太繁琐了。但是在头文件 ntdef.h中有一个宏方便这种定
义。使用这个宏之后,我们就可以简单的定义一个常数字符串如下: 


UNICODE_STRING str = RTL_CONSTANT_STRING(L“my first string!”); 


这只能在定义这个字符串的时候使用。为了随时初始化一个字符串,可以使用
RtlInitUnicodeString。示例如下: 
   
  UNICODE_STRING str; 
  RtlInitUnicodeString(&str,L”my first string!”); 
 
  用本小节的方法初始化的字符串,不用担心内存释放方面的问题。因为我们并没有分配
任何内存。


4.3 字符串的拷贝

 因为字符串不再是空结束的,所以使用 wcscpy来拷贝字符串是不行的。UNICODE_STRING
可以用 RtlCopyUnicodeString来进行拷贝。在进行这种拷贝的时候,需要注意:拷贝目的字符串的 Buffer必须有足够的空间。如果 Buffer的空间不足,字符串会拷贝不完
全。这是一个比较隐蔽的错误。 
  下面举一个例子。 
 
  UNICODE_STRING dst;     // 目标字符串 
  WCHAR dst_buf[256];       // 我们现在还不会分配内存,所以先定义缓冲区 
  UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”); 
   
  // 把目标字符串初始化为拥有缓冲区长度为 256的UNICODE_STRING 空串。 
  RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR)); 
  RtlCopyUnicodeString(&dst,&src);  // 字符串拷贝! 

以上这个拷贝之所以可以成功,是因为 256比 L” My source string!”的长度要大。
如果小,则拷贝也不会出现任何明示的错误。但是拷贝结束之后,字符串实际上被截短了。 
  我曾经犯过的一个错误是没有调用 RtlInitEmptyString。结果 dst 字符串被初始化认
为缓冲区长度为 0。虽然程序没有崩溃,却实际上没有拷贝任何内容。 
  在拷贝之前,最谨慎的方法是根据源字符串的长度动态分配空间。


4.4 字符串的连接 

  会常常碰到这样的需求:要把两个字符串连接到一起。简单的追加一个字符串并不困难。重
要的依然是保证目标字符串的空间大小。下面是范例: 


NTSTATUS status; 
  UNICODE_STRING dst;     // 目标字符串 
  WCHAR dst_buf[256];       // 我们现在还不会分配内存,所以先定义缓冲区 
  UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”); 
   
  // 把目标字符串初始化为拥有缓冲区长度为 256的UNICODE_STRING 空串 
  RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR)); 
  RtlCopyUnicodeString(&dst,&src);  // 字符串拷贝! 
   
  status = RtlAppendUnicodeToString( 
      &dst,L”my second string!”); 
  if(status != STATUS_SUCCESS) 
  { 
    …… 
  } 


NTSTATUS 是常见的返回值类型。如果函数成功,返回 STATUS_SUCCESS。否则的话,是
一个错误码。RtlAppendUnicodeToString 在目标字符串空间不足的时候依然可以连接字符
串,但是会返回一个警告性的错误 STATUS_BUFFER_TOO_SMALL。 
  另 外 一 种 情 况 是 希 望 连 接 两 个 UNICODE_STRING , 这 种 情 况 请 调 用
RtlAppendUnicodeStringToString。这个函数的第二个参数也是一个 UNICODE_STRING 的指
针。

4.5 字符串的打印 

  字符串的连接另一种常见的情况是字符串和数字的组合。有时数字需要被转换为字符
串。有时需要把若干个数字和字符串混合组合起来。这往往用于打印日志的时候。日志中可
能含有文件名、时间、和行号,以及其他的信息。 
  熟悉 C语言的读者会使用sprintf。这个函数的宽字符版本为 swprintf。该函数在驱动
开发中可以使用,但不安全。微软建议使用 RtlStringCbPrintfW 来代替它。
RtlStringCbPrintfW 需要包含头文件 ntstrsafe.h。在连接的时候,还需要连接库
ntsafestr.lib。 
  下面的代码生成一个字符串,字符串中包含文件的路径,和这个文件的大小。 
   

 #include <ntstrsafe.h> 
  // 任何时候,假设文件路径的长度为有限的都是不对的。应该动态的分配 
  // 内存。但是动态分配内存的方法还没有讲述,所以这里再次把内存空间 
  // 定义在局部变量中,也就是所谓的“在栈中” 
  WCHAR buf[512] = { 0 }; 
  UNICODE_STRING dst; 
  NTSTATUS status; 
  …… 



 // 字符串初始化为空串。缓冲区长度为 512*sizeof(WCHAR) 
 RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR)); 
  
 // 调用 RtlStringCbPrintfW 来进行打印 
 status = RtlStringCbPrintfW( 
   dst->Buffer,L”file path = %wZ file size = %d \r\n”, 
   &file_path,file_size); 
 // 这里调用 wcslen没问题,这是因为 RtlStringCbPrintfW打印的 
 // 字符串是以空结束的。 
 dst->Length = wcslen(dst->Buffer) * sizeof(WCHAR); 


 RtlStringCbPrintfW 在目标缓冲区内存不足的时候依然可以打印,但是多余的部分被
截去了。返回的 status 值为 STATUS_BUFFER_OVERFLOW。调用这个函数之前很难知道究竟需
要多长的缓冲区。一般都采取倍增尝试。每次都传入一个为前次尝试长度为2 倍长度的新缓
冲区,直到这个函数返回STATUS_SUCCESS 为止。 
  值得注意的是 UNICODE_STRING 类型的指针,用%wZ 打印可以打印出字符串。在不能保
证字符串为空结束的时候,必须避免使用%ws 或者%s。其他的打印格式字符串与传统 C 语言
中的 printf 函数完全相同。可以尽情使用。 
  另外就是常见的输出打印。printf 函数只有在有控制台输出的情况下才有意义。在驱
动中没有控制台。但是 Windows内核中拥有调试信息输出机制。可以使用特殊的工具查看打
印的调试信息(请参阅附录 1“WDK的安装与驱动开发的环境配置”)。 
  驱动中可以调用DbgPrint()函数来打印调试信息。这个函数的使用和printf基本相同。
但是格式字符串要使用宽字符。DbgPrint()的一个缺点在于,发行版本的驱动程序往往不希
望附带任何输出信息,只有调试版本才需要调试信息。但是 DbgPrint()无论是发行版本还
是调试版本编译都会有效。为此可以自己定义一个宏:


#if DBG 
    KdPrint(a)  DbgPrint##a 
  #else 
    KdPrint (a) 
  #endif 
     
  不过这样的后果是,由于 KdPrint (a)只支持 1 个参数,因此必须把 DbgPrint 的所有
参数都括起来当作一个参数传入。导致 KdPrint 看起来很奇特的用了双重括弧: 
 
  // 调用 KdPrint 来进行输出调试信息 
  status = KdPrint (( 
    L”file path = %wZ file size = %d \r\n”, 
    &file_path,file_size)); 
 
  这个宏没有必要自己定义,WDK 包中已有。所以可以直接使用 KdPrint 来代替DbgPrint
取得更方便的效果。

5 内存与链表

 

5.1 内存的分配与释放 

  内存泄漏是 C 语言中一个臭名昭著的问题。作为内核开发者,将有必要自己来面对它。在传统的 C 语言中,分配内存常常使用的函数是 malloc。这个函数的使用非常简
单,传入长度参数就得到内存空间。在驱动中使用内存分配,这个函数不再有效。驱动中分
配内存,最常用的是调用ExAllocatePoolWithTag。
一个字符串被复制到另一个字符串的时候,最好根据源字符串的空间长度来分配目标字符串的长度。下面的举例,是把一个字符串 src拷贝到字
符串 dst。 


 // 定义一个内存分配标记 
  #define MEM_TAG ‘MyTt’ 
  // 目标字符串,接下来它需要分配空间。 
  UNICODE_STRING dst = { 0 }; 
  // 分配空间给目标字符串。根据源字符串的长度。 
  dst.Buffer =  
    (PWCHAR)ExAllocatePoolWithTag(NonpagedPool,src->Length,MEM_TAG); 
  if(dst.Buffer == NULL) 
  { 
    // 错误处理 
    status = STATUS_INSUFFICIENT_RESOUCRES; 
    …… 
  } 
  dst.Length = dst.MaximumLength = src->Length; 
  status = RtlCopyUnicodeString(&dst,&src); 
  ASSERT(status == STATUS_SUCCESS); 


ExAllocatePoolWithTag 的第一个参数 NonpagedPool 表明分配的内存是锁定内存。这
些内存永远真实存在于物理内存上。不会被分页交换到硬盘上去。第二个参数是长度。第三
个参数是一个所谓的“内存分配标记”。 
  内存分配标记用于检测内存泄漏。想象一下,我们根据占用越来越多的内存的分配标记,
就能大概知道泄漏的来源。一般每个驱动程序定义一个自己的内存标记。也可以在每个模块
中定义单独的内存标记。内存标记是随意的 32 位数字。即使冲突也不会有什么问题。 
  此外也可以分配可分页内存,使用 PagedPool即可。 
  ExAllocatePoolWithTag 分配的内存可以使用 ExFreePool 来释放。如果不释放,则永
远泄漏。并不像用户进程关闭后自动释放所有分配的空间。即使驱动程序动态卸载,也不能
释放空间。唯一的办法是重启计算机。 
  ExFreePool 只需要提供需要释放的指针即可。举例如下: 


 ExFreePool(dst.Buffer); 
  dst.Buffer = NULL; 
  dst.Length = dst.MaximumLength = 0; 
   
  ExFreePool 不能用来释放一个栈空间的指针。否则系统立刻崩溃。像以下的代码: 
 
  UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”); 
  ExFreePool(src.Buffer); 
 
  会招来立刻蓝屏。所以请务必保持 ExAllocatePoolWithTag和 ExFreePool 的成对关系。


5.2 使用 LIST_ENTRY 

  Windows 的内核开发者们自己开发了部分数据结构,比如说 LIST_ENTRY。 
  LIST_ENTRY 是一个双向链表结构。它总是在使用的时候,被插入到已有的数据结构中。
一个例子。我构筑一个链表,这个链表的每个节点,是一个文件名和一个文件大小两
个数据成员组成的结构。此外有一个 FILE_OBJECT的指针对象。在驱动中,这代表一个文件
对象。本书后面的章节会详细解释。这个链表的作用是:保存了文件的文件名和长度。只要
传入 FILE_OBJECT 的指针,使用者就可以遍历链表找到文件名和文件长度。 
 
  typedef struct { 
    PFILE_OBJECT file_object; 
    UNICODE_STRING file_name; 
    LARGE_INTEGER file_length; 
  } MY_FILE_INFOR, *PMY_FILE_INFOR; 


 一些读者会马上注意到文件的长度用 LARGE_INTEGER 表示。这是一个代表长长整型的数
据结构。这个结构我们在下一小小节“使用长长整型数据”中介绍。 
  为了让上面的结构成为链表节点,我必须在里面插入一个 LIST_ENTRY 结构。至于插入
的位置并无所谓。可以放在最前,也可以放中间,或者最后面。但是实际上读者很快会发现
把 LIST_ENTRY放在开头是最简单的做法: 
 
  typedef struct { 
    LIST_ENTRY list_entry; 
    PFILE_OBJECT file_object; 
    UNICODE_STRING file_name; 
    LARGE_INTEGER file_length; 
  } MY_FILE_INFOR, *PMY_FILE_INFOR;   
 
  list_entry 如果是作为链表的头,在使用之前,必须调用 InitializeListHead 来初始
化。下面是示例的代码: 


// 我们的链表头
  LIST_ENTRY    my_list_head; 
 
  // 链表头初始化。一般的说在应该在程序入口处调用一下 
  void MyFileInforInilt() 
  { 
    InitializeListHead(&my_list_head); 
  } 


  // 我们的链表节点。里面保存一个文件名和一个文件长度信息。 
  typedef struct { 
    LIST_ENTRY list_entry; 
    PFILE_OBJECT file_object; 
    PUNICODE_STRING file_name; 
    LARGE_INTEGER file_length; 
  } MY_FILE_INFOR, *PMY_FILE_INFOR; 
 
  // 追加一条信息。也就是增加一个链表节点。请注意 file_name 是外面分配的。 
  // 内存由使用者管理。本链表并不管理它。 
  NTSTATUS MyFileInforAppendNode( 
    PFILE_OBJECT file_object,  
    PUNICODE_STRING file_name, 
    PLARGE_INTEGER file_length) 


 { 
    PMY_FILE_INFOR my_file_infor =  
      (PMY_FILE_INFOR)ExAllocatePoolWithTag( 
        PagedPool,sizeof(MY_FILE_INFOR),MEM_TAG); 
    if(my_file_infor == NULL) 
      return STATUS_INSUFFICIENT_RESOURES; 
 
    // 填写数据成员。 
    my_file_infor->file_object = file_object; 
    my_file_infor->file_name = file_name; 
    my_file_infor->file_length = file_length; 
 
    // 插入到链表末尾。请注意这里没有使用任何锁。所以,这个函数不是多 
    // 多线程安全的。在下面自旋锁的使用中讲解如何保证多线程安全性。 
    InsertHeadList(&my_list_head, (PLIST_ENTRY)& my_file_infor); 
    return STATUS_SUCCESS;   
  }


以上的代码实现了插入。可以看到 LIST_ENTRY 插入到 MY_FILE_INFOR 结构的头部的好
处。这样一来一个 MY_FILE_INFOR 看起来就像一个 LIST_ENTRY。不过糟糕的是并非所有的
情况都可以这样。比如 MS 的许多结构喜欢一开头是结构的长度。因此在通过 LIST_ENTRY
结构的地址获取所在的节点的地址的时候,有个地址偏移计算的过程。可以通过下面的一个
  
典型的遍历链表的示例中看到:


for(p = my_list_head.Flink; p != &my_list_head.Flink; p = p->Flink) 

    PMY_FILE_INFOR elem =  
      CONTAINING_RECORD(p,MY_FILE_INFOR, list_entry); 
    // 在这里做需要做的事… 
    } 
  } 
 
  其中的 CONTAINING_RECORD 是一个 WDK 中已经定义的宏,作用是通过一个 LIST_ENTRY
结构的指针,找到这个结构所在的节点的指针。定义如下:


#define CONTAINING_RECORD(address, type, field) ((type *)( \ 
   (PCHAR)(address) - \ 
   (ULONG_PTR)(&((type *)0)->field))) 
从上面的代码中可以总结如下的信息: 
 LIST_ENTRY 中的数据成员Flink指向下一个 LIST_ENTRY。 
 整个链表中的最后一个 LIST_ENTRY的 Flink不是空。而是指向头节点。 
 得到 LIST_ENTRY 之后,要用 CONTAINING_RECORD 来得到链表节点中的数据。 


5.3 使用长长整型数据 

  这里解释前面碰到的 LARGE_INTEGER 结构。与可能的误解不同,64 位数据并非要在 64
位操作系统下才能使用。在 VC中,64 位数据的类型为__int64。定义写法如下: 
 
  __int64 file_offset; 
 
  上面之所以定义的变量名为 file_offset,是因为文件中的偏移量是一种常见的要使用
64 位数据的情况。同时,文件的大小也是如此(回忆上一小节中定义的文件大小)。32位数
据无符号整型只能表示到4GB。而众所周知,现在超过 4GB的文件绝对不罕见了。但是实际
上__int64 这个类型在驱动开发中很少被使用。基本上被使用到的是一个共用体:
LARGE_INTEGER。这个共用体定义如下: 
 

 typedef __int64 LONGLONG;   
  typedef union _LARGE_INTEGER { 
    struct { 
      ULONG LowPart; 
      LONG HighPart; 
    }; 
    struct { 
      ULONG LowPart;


    LONG HighPart; 
    } u; 
      LONGLONG QuadPart; 
  } LARGE_INTEGER; 
 
  这个共用体的方便之处在于,既可以很方便的得到高 32位,低 32 位,也可以方便的得
到整个 64位。进行运算和比较的时候,使用 QuadPart 即可。


 LARGE_INTEGER a,b; 
  a.QuadPart = 100; 
  a.QuadPart *= 100; 
  b.QuadPart = a.QuadPart; 
  if(b.QuadPart > 1000) 
  { 
    KdPrint(“b.QuadPart  <  1000,  LowPart  =  %x  HighPart  =  %x”, 
b.LowPart,b.HighPart); 
  } 
   
  上面这段代码演示了这种结构的一般用法。在实际编程中,会碰到大量的参数是
LARGE_INTEGER类型的。 

5.4 使用自旋锁


 链表之类的结构总是涉及到恼人的多线程同步问题,这时候就必须使用锁。

 锁存在的意义? 这和多线程操作有关。在驱动开发的代码中,大多是
在于多线程执行环境的。就是说可能有几个线程在同时调用当前函数。 
 这样一来,前文中提及的追加链表节点函数就根本无法使用了。因为
FileInforAppendNode这个函数只是简单的操作链表。如果两个线程同时调用这个函数来
作链表的话:注意这个函数操作的是一个全局变量链表。换句话说,无论有多少个线程同
执行,他们操作的都是同一个链表。这就可能发生,一个线程插入一个节点的同时,另一
线程也同时插入。他们都插入同一个链表节点的后边。这时链表就会发生问题。到底最后
入的是哪一个呢?要么一个丢失了。要么链表被损坏了。 
 如下的代码初始化获取一个自选锁: 
 KSPIN_LOCK my_spin_lock; 
 KeInitializeSpinLock(&my_spin_lock); 


  KeInitializeSpinLock 这个函数没有返回值。下面的代码展示了如何使用这个
SpinLock。在 KeAcquireSpinLock 和KeReleaseSpinLock之间的代码是只有单线程执行的。
其他的线程会停留在 KeAcquireSpinLock等候。直到 KeReleaseSpinLock 被调用。KIRQL是
一个中断级。KeAcquireSpinLock 会提高当前的中断级。但是目前忽略这个问题。中断级在
后面讲述。 
   
KIRQL irql; 
  KeAcquireSpinLock(&my_spin_lock,&irql); 
  // To do something … 
  KeReleaseSpinLock(&my_spin_lock,irql); 
   
  初学者要注意的是,像下面写的这样的“加锁”代码是没有意义的,等于没加锁: 
   
  void MySafeFunction() 
  { 
    KSPIN_LOCK my_spin_lock; 
    KIRQL irql; 
    KeInitializeSpinLock(&my_spin_lock); 
    KeAcquireSpinLock(&my_spin_lock,&irql); 
    // 在这里做要做的事情… 
    KeReleaseSpinLock(&my_spin_lock,irql); 
  }


原因是 my_spin_lock 在堆栈中。每个线程来执行的时候都会重新初始化一个锁。只有
所有的线程共用一个锁,锁才有意义。所以,锁一般不会定义成局部变量。可以使用静态变
量、全局变量,或者分配在堆中(见前面的 1.2.1 内存的分配与释放一节)。请读者自己写
出正确的方法。 
  LIST_ENTRY 有一系列的操作。这些操作并不需要使用者自己调用获取与释放锁。只需
要为每个链表定义并初始化一个锁即可: 
    
  LIST_ENTRY    my_list_head;    // 链表头 
  KSPIN_LOCK  my_list_lock;    // 链表的锁 
   
  // 链表初始化函数 
  void MyFileInforInilt() 
  { 
    InitializeListHead(&my_list_head); 
    KeInitializeSpinLock(&my_list_lock); 
  }


  链表一旦完成了初始化,之后的可以采用一系列加锁的操作来代替普通的操作。比如插
入一个节点,普通的操作代码如下: 
 
  InsertHeadList(&my_list_head, (PLIST_ENTRY)& my_file_infor); 
 
  换成加锁的操作方式如下: 
 
  ExInterlockedInsertHeadList( 
    &my_list_head,


  (PLIST_ENTRY)& my_file_infor, 
    &my_list_lock); 
 
  注 意 不 同 之 处 在 于 , 增 加 了 一 个 KSPIN_LOCK 的 指 针 作 为 参 数 。 在
ExInterlockedInsertHeadList 中,会自动的使用这个 KSPIN_LOCK 进行加锁。类似的还有
一个加锁的 Remove函数,用来移除一个节点,调用如下: 
 
  my_file_infor = ExInterlockedRemoveHeadList ( 
    &my_list_head,  
    &my_list_lock); 
 
  这个函数从链表中移除第一个节点。并返回到 my_file_infor中。 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《CSDN Windows驱动开发技术详解》是一本针对Windows操作系统驱动开发的技术指南。本书内容全面,详细介绍了Windows驱动开发的基础知识、核心技术和实际应用。 首先,本书对Windows驱动开发的基础知识进行了系统的介绍。包括了Windows操作系统的架构、驱动模型和驱动类型等基本概念,使读者对驱动开发的背景和基本原理有了更清晰的认识。 其次,本书详细介绍了Windows驱动开发中常用的核心技术。包括驱动的加载与卸载、设备与驱动的通信机制、内存管理、中断处理、异步请求处理等内容。通过对这些关键技术的讲解和实例演示,读者可以深入了解并掌握Windows驱动开发的核心要点。 此外,本书还涵盖了一些高级的驱动开发技术。例如,对于特殊设备的驱动开发驱动的安全性设计以及对驱动进行调试和性能优化等内容进行了详细的阐述。这些内容帮助读者进一步提升自己的驱动开发技能,应对更加复杂的开发场景。 最后,本书还提供了大量的实际案例和项目实战经验。这些案例涉及不同类型的驱动开发,例如文件系统驱动、网络设备驱动、USB设备驱动等。通过对这些案例的学习和实践,读者可以更好地理解和应用书中所介绍的技术。 总而言之,《CSDN Windows驱动开发技术详解》全面而深入地介绍了Windows驱动开发的方方面面,是一本理论与实践相结合的好书。无论是初学者还是有一定经验的开发者,都可以从中获得有效的指导和实用的技术。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值