第
1
章 从两个最简单的驱动开始
Windows 开发人员经常需要编写驱动程序 Windows 内核有深入了解和大量的内
核调试技巧,稍有不慎,就会导致系统崩溃。因此,第一次涉及 Windows 开发驱动程序
即使程序员很多, Win32 程序开发技能,往往很难开始。
本章向读者展示了两个最简单的 Windows 驱动程序,一个是 NT 类型驱动程序,另一个
是 WDM 驱动程序。这两个驱动程序没有操作特定的硬件设备,只是在系统中创建了
虚拟设备。在随后的章节中,它们将作为本书其他章节驱动程序的基本驱动程序框架
开发机构的重用。作者将带领读者编写代码、编译、安装和调试程序。我相信第一次编写驱动程序
对于程序读者来说,这将是非常令人兴奋和有趣的。代码的具体解释将分散在下一章中。
现在请和作者一起开始 Windows 驱动编程之旅!
1.1 DDK的安装
在编写第一个驱动程序之前,需要安装微软提供的 Windows 开发包装驱动程序 DDK
(Driver Development Kit)。笔者安装在计算机上。 Windows XP 2462 版本的 DDK,建议读
或者安装相同或更高的版本 DDK,如图 1-1 所示。
请选择完全安装,即安装 DDK 如图所示 1-2 所示。因为除了
DDK 在基本的编译环境外,DDK 它还提供了大量的源代码和实用工具 Windows 驱动
初学者学习和编写驱动程序将非常有用。
安装后,相应的项目将出现在开始菜单中。其中,主要用途是 Build Environment,
如图 1-3 所示。这个版本 DDK 同时安装 Windows 2000 和 Windows XP 编译环境。

第 1章 从两个最简单的驱动开始
图 1-1 DDK 的安装
图 1-2 DDK 的安装
图 13 DDK 的编译环境
1.2 第一个驱动程序 HelloDDK的代码分析
Windows 驱动程序分为两类,一类是不支持即插即用功能的 NT 式驱动程序,另一类
3
Windows驱动开发技术详解
是支持即插即用功能的 WDM 驱动程序。本节介绍的 HelloDDK 是一个最简单的 NT 式驱
动程序。在 1.4 节中将给出一个 WDM 式的驱动程序。
1.2.1 HelloDDK 的头文件
HelloDDK 的头文件主要是为了导入驱动程序开发所必需的 NTDDK.h 头文件,此头
文件里包含了对 DDK 的所有导出函数的声明。NT 式的驱动程序要导入的头文件是
NTDDK.h,而 WDM 式的驱动程序要导入的头文件为 WDM.h。另外,此头文件中定义了
几个标签,分别在程序中指明函数和变量分配在分页内存中或非分页内存中(分页和非分
页内存的概念将在第 3 章中讲述)。最后,该头文件给出了此驱动的函数声明。
#001 /************************************************************************
#002 * 文件名称:Driver.h
#003 * 作
者:张帆
#004 * 完成日期:2007-11-1
#005 *************************************************************************/
#006 #pragma once
#007
#008 #ifdef __cplusplus
#009 extern "C"
#010 {
#011 #endif
#012 #include <NTDDK.h>
#013 #ifdef __cplusplus
#014 }
#015 #endif
#016
#017 #define PAGEDCODE code_seg("PAGE")
#018 #define LOCKEDCODE code_seg()
#019 #define INITCODE code_seg("INIT")
#020
#021 #define PAGEDDATA data_seg("PAGE")
#022 #define LOCKEDDATA data_seg()
#023 #define INITDATA data_seg("INIT")
#024
#025 #define arraysize(p) (sizeof(p)/sizeof((p)[0]))
#026
#027 typedef struct _DEVICE_EXTENSION {
#028
PDEVICE_OBJECT pDevice;
#029
UNICODE_STRING ustrDeviceName;
//设备名称
#030
UNICODE_STRING ustrSymLinkName;
//符号链接名
#031 } DEVICE_EXTENSION, *PDEVICE_EXTENSION;
#032
#033 // 函数声明
#034
#035 NTSTATUS CreateDevice (IN PDRIVER_OBJECT pDriverObject);
#036 VOID HelloDDKUnload (IN PDRIVER_OBJECT pDriverObject);
#037 NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,
#038
#039
IN PIRP pIrp);
此段代码可以在配套光盘中本章的 NT_Driver 目录下找到。
4
第 1 章 从两个最简单的驱动谈起
代码 6~15 行,包含 ddk.h 头文件,所有的 NT 式驱动程序都要包含此头文件。因
为这里采用的是 C++语言编写,如果直接包含 ntddk.h,函数的符号表会导入错误,
所以需要加入 extern "C",这样可以保证符号表正确导入。关于 C++编写驱动需要
注意的地方,将在第 3 章进行论述。
代码 17~23 行,定义分页标记、非分页标记和初始化内存块。在 Windows 驱动程
序的开发中,所有程序的函数和变量要被指明被加载到分页内存中还是在非分页
内存中。程序代码中加入这里定义的宏,就会被指明函数和变量是位于分页或非
分页内存中。另外,有一个特殊的函数 DriverEntry 需要放在 INIT 标志的内存中。
INIT 标志指明该函数只是在加载的时候需要载入内存,而当驱动程序成功加载后,
该函数可以从内存中卸载掉。
代码 27~31 行,指定一个设备扩展结构体,这种结构体广泛应用于驱动程序中。
根据不同驱动程序的需要,它负责补充定义设备的相关信息。
代码 33~38 行是函数的声明。
1.2.2 HelloDDK 的入口函数
和普通的应用程序不同,Windows 驱动程序的入口函数不是 main 函数,而是一个叫
做 DriverEntry 的函数,代码将在下面列出。DriverEntry 函数由内核中的 I/O 管理器负责调
用,其函数有两个参数:pDriverObject 和 pRegistryPath。其中,pDriverObject 是 I/O 管理
器传递进来的驱动对象,pRegistryPath 是一个 Unicode 字符串,指向此驱动负责的注册表。
#001 /************************************************************************
#002 * 文件名称:Driver.cpp
#003 * 作
者:张帆
#004 * 完成日期:2007-11-1
#005 *************************************************************************/
#006
#007 #include "Driver.h"
#008
#009 /************************************************************************
#010 * 函数名称:DriverEntry
#011 * 功能描述:初始化驱动程序,定位和申请硬件资源,创建内核对象
#012 * 参数列表:
#013
#014
pDriverObject:从 I/O管理器中传进来的驱动对象
pRegistryPath:驱动程序在注册表的中的路径
#015 * 返回值:返回初始化驱动状态
#016 *************************************************************************/
#017 #pragma INITCODE
#018 extern "C" NTSTATUS DriverEntry (
#019
#020
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath
)
#021 {
#022
NTSTATUS status;
5
Windows驱动开发技术详解
#023
#024
KdPrint(("Enter DriverEntry\n"));
#025
//注册其他驱动调用函数入口
#026
#027
#028
#029
#030
#031
pDriverObject->DriverUnload = HelloDDKUnload;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;
#032
//创建驱动设备对象
#033
status = CreateDevice(pDriverObject);
#034
#035
#036
KdPrint(("DriverEntry end\n"));
return status;
#037 }
此段代码可以在配套光盘中本章的 NT_Driver 目录下找到。
代码 17 行,用#pragma 指明此函数是加载到 INIT 内存区域中,即成功卸载后,可
以退出内存。
代码 18 行,标志 DriverEntry 函数的开始。注意此处在函数体的前面用 extern "C"修
饰,这样在编译的时候会编译成_DriverEntry@8 的符号。如果不加入此修饰符号,
编译器会自动按照 C++的符号名编译,导致错误链接。
代码 23 行,打印一行调试信息。KdPrint 其实是一个宏,在调试版本(Checked 版)
中,会用 DbgPrint 代替。而在发行版(Free 版)中,则不执行任何操作,其功能
类似于 MFC 中的 TRACE 宏。由于驱动程序是运行在 Windows 的核心态,没有用
户界面,所以查看调试信息有别于 Win32 程序。关于查看调试信息的讲解将在第 3
章论述。
代码 26~30 行,驱动程序向 Windows 的 I/O 管理器注册一些回调函数。回调函数
是由程序员定义的函数,这些函数不是由驱动程序本身负责调用,而是由操作系
统负责调用。程序员将这些函数的入口地址告诉操作系统,操作系统会在适当的
时候调用这些函数。在这个例子中,这几个回调函数基本是自解释型的,读者可
以根据函数名分析出其作用。当驱动被卸载时,调用 HelloDDKUnload。当驱动程
序处理创建、关闭和读写相关的 IRP 时,调用 HelloDDKDispatchRoutine(这里只
是将处理函数简化为一个函数,实际情况要比这个复杂)。
代码第 33 行,调用 CreateDevice 函数,此函数的解释见下一节。
代码第 36 行,返回 CreateDevice 的执行结果。如果执行正确,驱动将被成功加载。
1.2.3 创建设备例程
CreateDevice 函数是一个帮助函数(Helper Function),辅助 DriverEntry 创建一个设备
对象。其完全可以展开放在 DriverEntry 中,但为了代码的条理性,笔者将其构造成一个辅
6
第 1 章 从两个最简单的驱动谈起
助函数。
#001 /************************************************************************
#002 * 函数名称:CreateDevice
#003 * 功能描述:初始化设备对象
#004 * 参数列表:
#005
pDriverObject:从 I/O管理器中传进来的驱动对象
#006 * 返回 值:返回初始化状态
#007 *************************************************************************/
#008 #pragma INITCODE
#009 NTSTATUS CreateDevice (
#010
IN PDRIVER_OBJECTpDriverObject)
#011 {
#012
NTSTATUS status;
#013
PDEVICE_OBJECT pDevObj;
#014
PDEVICE_EXTENSION pDevExt;
#015
#016
//创建设备名称
#017
#018
#019
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
#020
//创建设备
#021
#022
#023
#024
#025
#026
#027
#028
#029
#030
#031
#032
#033
status = IoCreateDevice( pDriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj );
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
#034
//创建符号链接
#035
#036
#037
#038
#039
#040
#041
#042
#043
#044
#045 }
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink( &symLinkName,&devName );
if (!NT_SUCCESS(status))
{
IoDeleteDevice( pDevObj );
return status;
}
return STATUS_SUCCESS;
此段代码可以在配套光盘中本章的 NT_Driver 目录下找到。
代码 16~18 行,构造一个 Unicode 字符串,此字符串用来存储此设备对象的名称。
Unicode 字符串大量运用在驱动程序开发中,有关 Unicode 的讲解请参考第 3 章。
代码 21~28 行,用 IoCreateDevice 函数创建一个设备对象。其对象名称来自于上一
步构造的 Unicode 字符串,设备类型为 FILE_DEVICE_UNKNOWN,且此种设备
7
Windows驱动开发技术详解
为独占设备,即设备只能被一个应用程序所使用。
代码第 30 行,表明此种设备为 BUFFERED_IO 设备。设备对内存的操作分为两种,
BUFFERED_IO 和 DO_DIRECT_IO,此部分讲解请参考第 3 章。
代码 31~33 行,填写设备的扩展结构体,在其他驱动程序的函数中,可以很方
便地得到这个结构体,进而得到该设备的自定义信息。此结构体的定义在
Driver.h 中。
代码 34~38 行,创建符号链接。驱动程序虽然有了设备名称,但是这种设备名称
只能在内核态可见,而对于应用程序是不可见的。因此,驱动需要暴露一个符号
链接,该链接指向真正的设备名称。
代码 39~44 行,当设备创建成功后返回。如果不成功,则删除该设备。
1.2.4 卸载驱动例程
卸载驱动例程用来设备被卸载的情况,由 I/O 管理器负责调用此回调函数。此例程遍
历系统中所有的此类设备对象。第一个设备对象的地址存在于驱动对象的 DeviceObject 域
中,每个设备对象的 NextDevice 域记录着下一个设备对象的地址,这样就形成一个链表。
卸载驱动例程的主要目的就是遍历系统中所有的此类设备对象,然后删除设备对象以及符
号链接。
#001 /************************************************************************
#002 * 函数名称:HelloDDKUnload
#003 * 功能描述:负责驱动程序的卸载操作
#004 * 参数列表:
#005
pDriverObject:驱动对象
#006 * 返回值:返回状态
#007 *************************************************************************/
#008 #pragma PAGEDCODE
#009 VOID HelloDDKUnload (IN PDRIVER_OBJECT pDriverObject)
#010 {
#011
#012
#013
#014
#015
#016
#017
#018
PDEVICE_OBJECT
pNextObj;
KdPrint(("Enter DriverUnload\n"));
pNextObj = pDriverObject->DeviceObject;
while (pNextObj != NULL)
{
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
pNextObj->DeviceExtension;
#019
//删除符号链接
#020
#021
#022
#023
UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
IoDeleteSymbolicLink(&pLinkName);
pNextObj = pNextObj->NextDevice;
IoDeleteDevice( pDevExt->pDevice );
}