竹林蹊径:深入浅出windows驱动开发
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第1章 Hello World驱动

1.1 从Hello World开始

国外的计算机领域有一个著名的组织,名字叫“计算机器协会”,它在互联网上专门维护了一个网页,上面列出了200多种版本的“Hello World”程序,仿佛代码的罗塞塔石碑。

在使用一门新的编程语言时,人们习惯把“Hello World”作为第一个程序。渐渐地,人们也习惯用类似“Hello World”的程序作为一切程序的第一步,以此唤出程序员心中对于编程乐观的一面。

如本节的题目一样,如何编写驱动程序是本节将要解决的问题。

那么,让我们看一下驱动程序的“Hello World”吧!

代码示例1-1 Hello World驱动程序

      #001 /*
      #002 *******************************************************
      #003 *= = 文件名称:Hello World.c
      #004 *= = 文件描述:驱动程序的Hello World例子
      #005 *= = 作    者:竹林蹊径
      #006 *= = 编写时间:2009-04-23 21:16:00
      #007 *******************************************************
      #008 */
      #009
      #010 #include <NTDDK.h>
      #011
      #012 //*====================================================
      #013 //*= = 函数名称:DriverEntry
      #014 //*= = 功能描述:驱动程序入口函数
      #015 //*= = 入口参数:PDRIVER_OBJECT, PUNICODE_STRING
      #016 //*= = 出口参数:NTSTATUS
      #017 //*====================================================
      #018
      #019 NTSTATUS
      #020 DriverEntry (
      #021    __in PDRIVER_OBJECT DriverObject,
      #022    __in PUNICODE_STRING RegistryPath
      #023    )
      #024 {
      #025    DbgPrint("Hello, Windows Driver!");
      #026    return STATUS_SUCCESS;
      #027 }
      #028
      #029 //*====================================================
      #030 //*= = 文件结束
      #031 //*====================================================

本示例代码可以从本书的\Chapter01\Hello World目录下找到。

以上就是驱动程序最简单的“Hello World”例子,去掉代码注释部分,放眼程序,全部代码屈指可数。

由第003行注释可知,这个例子的文件名称是Hello World.c,说明此文件是由C语言编写而成的。

我们知道,操作系统刚出现时是由机器语言和汇编语言编写的,后来为了可移植性等采用了C语言。早期的一些Windows操作系统也是采用C语言编写的,而在开发基于NT技术的Windows操作系统时,同时采用了C和C++这两种高级语言。

那么究竟是采用C语言还是C++语言来开发驱动程序呢?其实两者各有优缺点,读者可以在读本书的时候仔细体会。这里采用C语言编写了第一个例子,因为用C语言完全可以开发出所有的驱动程序,同时你会有似曾相识的感觉。但是微软提供了WDF驱动开发模型,这个在后面的章节中会讲到,如果需要使用WDF,C++无疑是最好的选择。

实际上,驱动程序编译成的二进制文件是SYS类型文件,和普通的EXE类型文件一样,也是PE格式。PE是Portable Executable File Format的简写,是微软Windows平台环境下主流的可执行程序标准格式,DLL也是常见的PE格式。所以,使用什么编程语言并不严格限定,如果你喜欢,即使用汇编或Delphi也可以开发驱动程序。比如在VxD驱动编程模型盛行时,很多人还是使用汇编开发设备驱动程序,并提供了相应的开发环境;更有甚者提供了EXE类型程序到SYS类型程序的转换工具,这些工具虽然大多没有流行起来,但是却佐证了这种方法的可行性。

不过,微软提供的内核编程接口和示例只有C/C++的,为了方便起见,我们约定本书使用C/C++语言来开发驱动程序。

开发驱动程序大致和开发普通应用程序一样,几乎拥有同样的流程:分析需求、设计、编码、调试、测试、发布、维护这几个主要环节。但是在后几个环节上,驱动程序的开发和普通应用程序的开发又有着很大的差别。

下面,我们来简单分析一下代码。

第010行包含了一个头文件NTDDK.h,这个头文件是NT驱动必须包含的一个头文件,WDM驱动则需要换成WDM.h。

第019~027行,整段代码只有一个DriverEntry函数。这个函数是所有驱动程序的入口函数,类似于Win32编程下的WinMain函数或C语言的main函数。

第021~022行,函数的两个参数,分别代表驱动对象的指针和注册表子键的字符串指针。这个暂且不作详细论述,我们将在下面章节中具体说明。其中,__in是一个宏,代表这个参数是入口参数,常见的还有__out,代表出口参数。

第025~026行,再看函数里面,我们看到只有两个语句,是不是很熟悉呢?

DbgPrint是一个函数,类似于C语言的printf函数,打印一串字符串,打印的内容为“Hello, Windows Driver!”。随后函数返回一个值STATUS_SUCCESS,这个值是个宏,从字面意思可知它代表成功,语句类似于main函数的“return 0;”。这里需要注意的是,打印的内容无法通过控制台查看,需要借助其他工具才能看到。

那么如何对这个程序进行编译呢?

我们需要一个开发环境,这个开发环境名为WDK,微软已经为我们提供了。关于开发环境的详细介绍,请参阅本书第2章。

一切都那么自然!下面对这个例子进行简单的扩充。

1.1.1 HelloDRIVER

这里是一个关于Hello World驱动程序示例代码的简单扩充。如果完全看不懂,那就直接跳到1.1.2节看代码解释吧!

代码示例1-2驱动程序HelloDRIVER声明文件

      #001 /*
      #002 *****************************************************************
      #003 *= = 文件名称:HelloDRIVER.h
      #004 *= = 文件描述:关于HelloDRIVER的头文件
      #005 *= = 作    者:竹林蹊径
      #006 *= = 编写时间:2009-04-23 21:16:00
      #007 *****************************************************************
      #008 */
      #009
      #010 #ifndef __HELLODRIVER_H__
      #011 #define __HELLODRIVER_H__
      #012
      #013 //*==============================================================
      #014 //*= = 头文件声明
      #015 //*==============================================================
      #016
      #017 #include <NTDDK.h>
      #018
      #019 //*==============================================================
      #020 //*= = 宏与结构体
      #021 //*==============================================================
      #022
      #023 typedef struct _DEVICE_EXTENSION {
      #024
      #025     PDEVICE_OBJECT DeviceObject;    // 指回设备对象的指针
      #026     UNICODE_STRING DeviceName;      // 设备名称
      #027     UNICODE_STRING SymbolicLink;    // 符号链接名
      #028
      #029 }DEVICE_EXTENSION, *PDEVICE_EXTENSION;
      #030
      #031 //*==============================================================
      #032 //*= = 函数声明
      #033 //*==============================================================
      #034
      #035 NTSTATUS
      #036 DriverEntry(
      #037     __in PDRIVER_OBJECT DriverObject,
      #038     __in PUNICODE_STRING RegistryPath
      #039     );
      #040
      #041 VOID
      #042 DriverUnload(
      #043     __in PDRIVER_OBJECT DriverObject
      #044     );
      #045
      #046 NTSTATUS
      #047 DefaultDispatch(
      #048     __in PDEVICE_OBJECT DeviceObject,
      #049     __in PIRP Irp
      #050     );
      #051
      #052 #endif  // End of __HELLODRIVER_H__
      #053
      #054 //*==============================================================
      #055 //*= = 文件结束
      #056 //*==============================================================

代码示例1-3驱动程序HelloDRIVER定义文件

      #001 /*
      #002 *****************************************************************
      #003 *= = 文件名称:HelloDRIVER.c
      #004 *= = 文件描述:驱动程序HelloDRIVER例子
      #005 *= = 作    者:竹林蹊径
      #006 *= = 编写时间:2009-04-23 21:16:00
      #007 *****************************************************************
      #008 */
      #009
      #010 #include "HelloDRIVER.h"
      #011
      #012 //*==============================================================
      #013 //*= = 预处理定义
      #014 //*==============================================================
      #015
      #016 #pragma alloc_text(INIT, DriverEntry)
      #017 #pragma alloc_text(PAGE, DefaultDispatch)
      #018 #pragma alloc_text(PAGE, DriverUnload)
      #019
      #020 //*==============================================================
      #021 //*= = 函数名称:DriverEntry
      #022 //*= = 功能描述:驱动程序入口函数
      #023 //*= = 入口参数:PDRIVER_OBJECT, PUNICODE_STRING
      #024 //*= = 出口参数:NTSTATUS
      #025 //*==============================================================
      #026
      #027 NTSTATUS
      #028 DriverEntry (
      #029    __in PDRIVER_OBJECT DriverObject,
      #030    __in PUNICODE_STRING RegistryPath
      #031    )
      #032 {
      #033     NTSTATUS status;
      #034     PDEVICE_OBJECT deviceObject;
      #035     PDEVICE_EXTENSION deviceExtension;
      #036     UNICODE_STRING symbolicLink;
      #037     UNICODE_STRING deviceName;
      #038     ULONG i;
      #039     KdPrint(("Enter HelloDRIVER DriverEntry!\n"));
      #040
      #041     UNREFERENCED_PARAMETER(RegistryPath);
      #042
      #043     RtlInitUnicodeString(&deviceName, L"\\Device\\HelloDRIVER");
      #044
      #045     // 处理派遣例程
      #046     for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
      #047     {
      #048         DriverObject->MajorFunction[i] = DefaultDispatch;
      #049     }
      #050
      #051     DriverObject->DriverUnload = DriverUnload;
      #052     DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatch;
      #053     DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatch;
      #054     DriverObject->MajorFunction[IRP_MJ_READ] = DefaultDispatch;
      #055     DriverObject->MajorFunction[IRP_MJ_WRITE] = DefaultDispatch;
      #056
      #057     // 创建设备
      #058     status = IoCreateDevice( DriverObject,
      #059                              sizeof(DEVICE_EXTENSION),
      #060                              &deviceName,
      #061                              FILE_DEVICE_UNKNOWN,
      #062                              0,
      #063                              TRUE,
      #064                              &deviceObject);
      #065     if(!NT_SUCCESS(status))
      #066     {
      #067         return status;
      #068     }
      #069
      #070     deviceObject->Flags = DO_BUFFERED_IO;
      #071     deviceExtension = (PDEVICE_EXTENSION)deviceObject->DeviceExtension;
      #072     deviceExtension->DeviceObject = deviceObject;
      #073     deviceExtension->DeviceName = deviceName;
      #074
      #075     RtlInitUnicodeString(&symbolicLink, L"\\??\\HelloDRIVER");
      #076     deviceExtension->SymbolicLink = symbolicLink;
      #077
      #078     // 创建符号链接
      #079     status = IoCreateSymbolicLink(&symbolicLink, &deviceName);
      #080
      #081     if(!NT_SUCCESS(status))
      #082     {
      #083         IoDeleteDevice(deviceObject);
      #084         return status;
      #085     }
      #086
      #087     KdPrint(("End of HelloDRIVER DriverEntry!\n"));
      #088     return status;
      #089 }
      #090
      #091 //*==============================================================
      #092 //*= = 函数名称:DriverUnload
      #093 //*= = 功能描述:驱动程序卸载函数
      #094 //*= = 入口参数:PDRIVER_OBJECT
      #095 //*= = 出口参数:VOID
      #096 //*==============================================================
      #097
      #098 VOID
      #099 DriverUnload(
      #100     __in PDRIVER_OBJECT DriverObject
      #101     )
      #102 {
      #103     PDEVICE_OBJECT deviceObject;
      #104     UNICODE_STRING linkName;
      #105     KdPrint(("Enter HelloDRIVER DriverUnload!\n"));
      #106
      #107     deviceObject = DriverObject->DeviceObject;
      #108
      #109    while(NULL != deviceObject)
      #110    {
      #111         PDEVICE_EXTENSION deviceExtesion = \
      #112                    (PDEVICE_EXTENSION)deviceObject->DeviceExtension;
      #113
      #114         // 删除符号链接与设备
      #115         linkName = deviceExtesion->SymbolicLink;
      #116         IoDeleteSymbolicLink(&linkName);
      #117         deviceObject = deviceObject->NextDevice;
      #118         IoDeleteDevice(deviceExtesion->DeviceObject);
      #119     }
      #120
      #121    KdPrint(("End of HelloDRIVER DriverUnload!\n"));;
      #122 }
      #123
      #124 //*==============================================================
      #125 //*= = 函数名称:DefaultDispatch
      #126 //*= = 功能描述:驱动程序默认派遣例程
      #127 //*= = 入口参数:PDEVICE_OBJECT, PIRP
      #128 //*= = 出口参数:NTSTATUS
      #129 //*==============================================================
      #130
      #131 NTSTATUS
      #132 DefaultDispatch(
      #133     __in PDEVICE_OBJECT DeviceObject,
      #134     __in PIRP Irp
      #135     )
      #136 {
      #137     NTSTATUS status;
      #138     KdPrint(("Enter HelloDRIVER DefaultDispatch!\n"));
      #139
      #140     UNREFERENCED_PARAMETER(DeviceObject);
      #141     status = STATUS_SUCCESS;
      #142
      #143     // 完成IRP请求
      #144     Irp->IoStatus.Status = status;
      #145     Irp->IoStatus.Information = 0;
      #146     IoCompleteRequest(Irp, IO_NO_INCREMENT);
      #147
      #148     KdPrint(("End of HelloDRIVER DefaultDispatch!\n"));
      #149     return status;
      #150 }
      #151
      #152 //*==============================================================
      #153 //*= = 文件结束
      #154 //*==============================================================

本示例代码可以从本书的\Chapter01\HelloDRIVER目录下找到。

1.1.2 代码解释

或许你惊讶于这个简单的扩充如此之长,其实如果细心看一下,它并不像你想象的那么多。

细心的读者一定发现了,目前所用的两个例子都携带了完整的程序注释。注释对于编程风格来说是很重要的一点,很多程序员对此不以为然,这是很不好的一个习惯。不同的编程人员习惯于不同的编程风格,希望读者能对此加以重视并形成自己的编程风格。良好的编程风格不仅有利于程序的可读性,对于程序的质量也起着不容忽视的作用。

希望对编码质量进一步提高的读者可以阅读《编程匠艺——编写卓越代码》或者《代码大全》等书。在之后的代码示例中,出于编排考虑,将不再贴出完整示例及注释。

接下来我们仔细看一下示例代码,并对此加以详细解释。

代码示例1-2是HelloDRIVER驱动程序的声明文件。这之后,我们对注释部分不再单独加以解释,请读者自行阅读。

第010、011、052行,这是C语言中常见的预处理,用来避免头文件重复包含导致编译错误。另外,也可以使用#pragma once来避免此错误,其作用是防止头文件多次被包含,保证头文件只被编译一次,比上个方法的可移植性稍差。

第017行,包含驱动所需的头文件。

第023~029行,这是一个结构体定义,用以描述驱动程序的设备扩展。它保存了我们自定义所需的一些信息,有助于更加方便地编程。

第35~50行,这是相关的函数声明。这些函数的具体实现存在于定义文件中,我们在下面加以详细介绍。

代码示例1-3是HelloDRIVER驱动程序的定义文件。

第010行,包含指定的声明文件。为每个定义文件写一个声明文件是一个不错的习惯。

第016~018行,这是一些预处理。在驱动开发中,需要为每一个函数指定其是分页内存还是非分页内存。INIT标识是指此函数在驱动加载时使用,是初始化相关的函数,驱动成功加载以后可以从内存卸载。PAGE标识是指此函数在驱动运行时可以被交换到磁盘上。如果不指定,编译器默认为非分页内存。

一般情况下,我们不需要考虑这些问题,但是有些特殊情况,代码是不允许被交换到磁盘上的,否则将导致操作系统蓝屏或者自动重启。这里需要注意一点,那就是函数声明必须在这些指定内存分配的预处理之前,否则无法通过编译。

从第027行开始,是DriverEntry函数的具体实现。1.1节我们说过,DriverEntry是驱动程序的入口函数,它由操作系统内核中的I/O管理器调用。

第033~038行,这是函数相关的变量定义。在C语言中,变量不允许被定义在函数流程处理中,也就是说,必须定义在函数体的开始处,否则出现编译错误。在C++语言中则没有这种限制。

第039行,可以看出KdPrint也是一个字符串打印函数。其实这个函数和上面的DbgPrint是同一个函数,是它的宏定义方式,用以打印调试信息。将其定义为宏的好处在于调试版本打印出具体信息供开发者参考,而在发行版本编译时完全被移除了,这样可以减小驱动文件大小并有助于提高程序的运行效率。

这里提一下调试版本和发行版本。在应用程序中调试版本和发行版本分别被称为Debug版本和Release版本,而在驱动程序中则被称为Check版本和Free版本。前后两者除了名字不一样外,没什么实质性的差别。而调试版本和发行版本的不同则在于前者包含了大量的调试信息,没有经过优化,方便开发者寻找程序缺陷和漏洞。

第041行,UNREFERENCED_PARAMETER是一个宏,经常被用来指定参数未被引用,可以避免不必要的警告。

说到警告,很多应用程序员大都不以为然。但是我们希望你在驱动开发中能改掉这个习惯,当然没有更好。因为驱动程序的崩溃会导致操作系统的崩溃,直接造成死机或蓝屏。除非你十分确定警告不会对驱动程序带来不稳定的因素,否则请修正它,因为做到没有警告是使驱动更加趋于稳定的一个基础。

第043行,对一个Unicode字符串进行初始化。Windows内核中大量使用Unicode字符串,其具体操作有一系列函数(详情请参看MSDN文档),这一系列函数属于Rtl系列,也就是微软推荐使用的运行时函数。

第046~049行,一个循环体。宏IRP_MJ_MAXIMUM_FUNCTION代表驱动程序最大的派遣函数指针数。这里使用一个默认的派遣函数来初始化它们,然后紧跟着在下面修改我们不打算使用默认的派遣函数指针。

这些派遣函数又可以称为回调函数,由定义实现,提供给操作系统调用。回调函数的意义和应用程序中的没有差别,只不过在驱动程序中,这些派遣函数是我们的主要工作重点。学习本书的主要任务也会建立在它们的基础之上。

对于普通的驱动程序,可以不考虑对所有的派遣函数指针进行初始化,但是如果想要实现一个过滤驱动程序,那么请参照以上方式初始化。具体实现方式,请参阅本书关于过滤驱动程序的章节。如果没有进行全部初始化,编译器会对未处理的派遣函数指针进行默认处理。

第051行,卸载函数。这个派遣函数必须单独提供,并且在操作系统版本不同的情况下,这个函数可能需要注意一些不同的东西。如果不打算对驱动程序进行卸载,这个函数可以不用提供。

第052~055行,提供给操作系统的创建、关闭以及读写的派遣函数。当然,还有更多的派遣函数需要提供,这里为了简单,我们使用DefaultDispatch来代替。

第058行,使用IoCreateDevice函数宏创建一个设备对象,其名称为“HelloDRIVER”。HelloDRIVER的设备类型为“FILE_DEVICE_UNKNOWN”,是一种独占设备,在运行时只能被一个应用程序所使用。

第065~068行,判断设备是否创建成功,并进行必要的失败处理。驱动程序中这样的处理对于驱动程序的健壮性起着不容忽视的作用。

第070行,设置设备的标识。有BUFFERED_IO和DO_DIRECT_IO两种,代表了两种不同的缓冲区处理方式。

第071~076行,这里初始化了一个Unicode字符串,同时也初始化了声明文件中定义过的设备扩展结构体。设备扩展中保存了我们自定义所需的一些信息。

第079~085行,使用IoCreateSymbolicLink函数宏创建了设备符号链接,并对创建结果判断以进行必要的失败处理。这个符号链接名主要用来与应用程序进行通信。如果创建失败,则删除已经创建的设备对象。

驱动程序的设备名称对应用程序是透明的,所以只能用于内核程序。这也是为什么要创建设备符号链接的原因。

从第098行开始,是DriverUnload函数的具体实现,它的功能是删除设备对象和设备符号链接。如果在DriverEntry函数中分配了资源,也要在这里释放。

第107行,由驱动对象指针参数得到设备对象指针。

第109~119行,遍历已经创建的所有设备符号链接和设备对象,并将其删除。

从第131行开始,是DefaultDispatch函数的具体实现,它的功能是直接完成了IRP (Input/Output Request Package,输入输出请求包)。

第144行,设置IRP的状态为成功。

第145行,因为打算直接完成IRP,所以操作信息的长度为空,这里将字节处理长度信息设置为0。

第146行,使用IoCompleteRequest函数直接完成IRP。

为了遵循一部分读者开发程序的习惯,这个示例只使用了一个文件。在编译程序时,声明文件是默认被包含到定义文件中的,所以暂时忽略不计。

1.1.3 驱动程序的编译和安装

编译驱动程序需要驱动开发环境。我们还没有介绍WDK的下载与安装,所以读者可以先看看这里的编译、安装效果,等阅读完第2章以后,再回过头来自己动手实践,相信你一定可以非常轻松地完成这个任务。

驱动程序的编译不像开发应用程序一样,可以简单地单击某个菜单或命令按钮就能通过IDE集成环境完全实现,而是需要使用一种叫做nmake的工具。

打开Hello World目录,可以看到有一个Makefile文件。文件内容如下:

      !INCLUDE $(NTMAKEENV)\makefile.def

这个文件几乎千篇一律,读者可以随便找一个Windows驱动程序的示例拷贝一个。微软建议不要去修改这个文件,我们最好遵循这个建议。

另外,读者还能发现文件夹里有一个Sources文件,具体内容如下:

      TARGETNAME = Hello World
      TARGETTYPE = DRIVER
      TARGETPATH = OBJ
      SOURCES = Hello World.c

这是一个简单的Sources文件例子。第1行用来指定驱动编译后的驱动程序文件名称;第2行用来指定生成的程序为驱动程序;第3行用来指定编译后生成文件的存放路径;最后1行用来指定要编译的源码文件。

大部分驱动程序开发人员都知道Sources文件的这种用法,却很少用它编译EXE或者DLL。具体论述参阅本书第2章。

这里一定不要写声明文件,否则将出现编译错误,错误信息提示为“编译目录出现错误”,而没有指明具体是什么错误。如果犯下这样的错误,一时间很难想到错误原因。

如果想要查看编译输出的错误信息和警告信息,可以从工程目录里寻找ERR类型文件与WRN类型文件。

打开WDK编译环境,这个环境是以命令行形式提供的,按如下操作进行即可。

如图1-1所示,单击“开始”→“程序”→“Windows Driver Kits”→“WDK6001. 10082”→“Build Environments”→“Windows XP”→“Launch Windows XP x86 Checked Build Environment”,打开WDK编译环境的命令行,通过DOS命令切换到工程文件的存放目录,输入“build”,然后按回车键,编译结果如图1-2所示。

图1-1 开始菜单中的WDK

图1-2 Hello World的编译结果

编译链接成功的SYS类型文件存放于当前目录的\objfre_wxp_x86\i386下。

同理,我们用同样的办法编译HelloDRIVER驱动程序。不过这里采用Check版本,选择“Launch Windows XP x86 Checked Build Environment”。文件生成的目录会稍微有一点差异,在当前目录的\objchk_wxp_x86\i386下。

接下来的问题是如何加载驱动程序。文件夹中的HelloDRIVER.sys驱动程序是无法直接加载的,也就是说,无法像运行EXE程序一样,直接双击运行。NT驱动需要有专门的程序来加载,这里使用DriverStudio软件里的Monitor工具。

打开Monitor软件,单击“File”→“Open Driver”,选择“HelloDRIVER.sys”,然后依次运行Start Driver和Stop Driver,可以看到如图1-3所示的信息。

图1-3 HelloDRIVER驱动的加载信息

同样,我们也可以用这个工具加载Hello World驱动程序。如果使用Monitor工具,可能需要安装DriverStudio软件,或者将其单独取出来。如果读者嫌麻烦,可以从网上找自己喜欢的驱动加载工具。

这里的驱动程序不需要进行调试和测试,因为我们给出的是已经经过测试与调试之后运行正常的代码。关于具体调试和测试的知识,我们会在以后的章节中详细论述。

1.1.4 查看我们的驱动

在前面我们提到,DbgPrint函数打印的字符串无法通过控制台查看,需要借助于其他的工具。

通过图1-3可以知道,使用Monitor工具可以查看驱动程序中的打印信息。这里简单介绍一下另外一个工具——DebugView软件,它也可以用来查看在驱动程序中打印的调试信息。

打开DebugView软件,然后利用Monitor加载Hello World程序,可以看到DebugView中显示了如图1-4所示的信息。

图1-4 使用DebugView查看打印信息

程序的“Hello, Windows Driver!”赫然在目!

如果想查看自己的设备驱动,可以按照上面的步骤再次加载HelloDRIVER驱动程序,然后重启计算机。

右键单击“我的电脑”→选择“属性”→“硬件”选项卡→“设备管理器”,打开“设备管理器”窗口,选择“查看”菜单下的“显示隐藏的设备”命令,可以看到已经加载成功的HelloDRIVER驱动。

笔者的计算机中显示的信息如图1-5所示。

图1-5 查看HelloDRIVER设备

这是通过操作系统自身看到的,当然,也可以通过其他软件来查看,比如WinObj工具,如图1-6所示。注意:本书里提到的工具,在第2章都会进行介绍。

图1-6 用WinObj查看HelloDRIVER设备

1.2 虚拟环境

1.2.1 使用虚拟环境进行驱动开发

因为内核调试会冻结它所运行的操作系统,并存在各种形式的系统不稳定等弊端,在实际的驱动开发工作中,大多数开发人员只是在最终阶段才会在实际的机器上进行开发调试,其余的大部分时间是使用虚拟环境进行驱动开发的。

使用虚拟环境进行开发,还可以节省很多资源,例如,对于没有能力购买另外一台PC的人,或者对于频繁出差需要在便携环境下进行内核调试的人来说,都提供了不错的解决方案。

这里通常使用的两类虚拟环境为VMware虚拟机和Virtual PC虚拟机。

1.2.2 使用VMware虚拟机

VMware Workstation是威睿公司推出的一款商业软件产品,是桌面虚拟化解决方案的软件代表,如图1-7所示。全球不同规模的客户都依赖它降低成本和运营费用,确保业务持续性以及加强安全性等。

图1-7 WMware 7.0.1版本下的Windows XP操作系统

VMware虚拟机在7.0版本之后的版本中增加了对Windows 7的支持。所以这里的设置以VMware 7.0以及之后的版本为准,但是配置却与之前的版本完全一样。

新建虚拟机之后,单击菜单项“VM”→“Settings”,会弹出如图1-8所示的对话框。单击“Add”按钮,选择“Serial Port”,再选择“Use named pipe”,在第一个编辑框中输入“\\.\pipe\com_1”,第二项选择“The end is the server.”,第三项选择“The other end is a virtual machine.”,然后单击“OK”按钮完成操作。

图1-8 VMware虚拟机的配置对话框

注意:一定要选中“Device status”下的复选框“Connect at power on”。

1.2.3 目标机设置

虚拟机里安装了想要调试的操作系统版本之后,还需要设置目标机为可调试的。在Windows Vista操作系统之后,目标机的可调试性设置与之前略有不同,下面一一介绍。

(1)Windows XP/Windows 2003的环境设置

打开系统盘,显示受保护的操作系统文件并且显示所有的文件和文件夹,找到boot.ini文件,去掉只读属性等,打开文件可以看到如下内容:

      [boot loader]
      timeout=30
      default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
      [operating systems]
      multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft    Windows    XP
  Professional" /noexecute=optin /fastdetect

复制最后一行文字,拷贝到下一行,并在最后添加“/debug /debug /debugport=com1/baudrate=115200”,显示如下:

      [boot loader]
      timeout=30
      default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
      [operating systems]
      multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft    Windows    XP
  Professional" /noexecute=optin /fastdetect /noguiboot
      multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft    Windows    XP
  Professional"  /noexecute=optin  /fastdetect  /debug  /debug  /debugport=com1
  /baudrate=115200

保存之后再次启动,即可发现引导菜单中多了一项“调试引导的系统”。

(2)Windows Vista/ Windows 7的环境设置

Windows Vista版本之前,Windows操作系统使用boot.ini进行引导,所以可以通过修改此文件进行系统的可调试性设置,但是Windows Vista之后的操作系统就无法再用法进行可调试性设置了。

在已经出版的《天书夜读——从汇编语言到Windows内核编程》和《寒江独钓——Windows内核安全编程》两本书中,关于Windows Vista的可调试性设置一直是使用bcdedit命令。这种方式比较烦琐,但是在系统启动时可以有选择性地进入,或者为普通启动,或者为调试启动,无须再重新进行选项设置。

对于这种方式的设置,读者可以参考前两本书,这里不再复述。这里讲述一种较为简单的设置方式,但是如果需要普通启动,则需要重新进行选项设置。

单击“开始”菜单→“所有程序”→“附件”,打开“命令提示符”,输入“msconfig”命令,打开“系统配置”对话框;或者使用“WIN+R”组合键打开“运行”对话框,输入“msconfig”命令,打开“系统配置”对话框。选择“引导”选项卡,单击“高级选项”按钮,如图1-9所示。

图1-9 Windows Vista/Windows 7的可调试性设置对话框

选中“调试”复选框,然后选择“调试端口”等。这里的设置与虚拟机的设置以及调试器的设置一定要一致,只有三者的设置一致,才能正常连接调试。这里采用的选项与之前的设置一样:

      -b -k com:port=\\.\pipe\com_1,baud=115200,pipe

将WinDBG连接至虚拟机进行调试,如图1-10所示。

运行VMware虚拟机里设置成功的调试操作系统,然后打开我们创建的WinDBG快捷方式进行连接。WinDBG调试器的快捷键以及菜单命令与Visual Studio系列软件一致,包括设置断点按F9键、运行按F5键等,读者可以看一下调试菜单中的菜单项。

图1-10是本书第一个例子Hello World驱动的调试截图,其中断在打印输出日志的语句上。

1.2.4 Virtual PC虚拟机

Virtual PC是微软为了争夺虚拟化市场而推出的一款软件产品,和VMware一样也十分优秀。它也可以像VMware一样,被用来进行驱动的双机调试。由于这里已经详细介绍了VMware虚拟机的使用,所以Virtual PC就不做过多的介绍了,它的设置与VMware虚拟机一样,并不复杂,读者可以自己尝试一下。

1.3 小结

本章以一个简单的Hello World驱动程序为例,讲解了驱动编程中最基本的一些要素。本章内容较为基础,对于初学者是不错的入门课程;而对于熟练的读者,算是让大家在学习更丰富的内容之前,热热身吧。