第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驱动程序为例,讲解了驱动编程中最基本的一些要素。本章内容较为基础,对于初学者是不错的入门课程;而对于熟练的读者,算是让大家在学习更丰富的内容之前,热热身吧。