从今天开始,我们开始学习Windows下的驱动开发。与时俱进,我们采用的是WDF(Windows Driver Foundation)驱动框架。一方面,该框架对于简化了驱动,提供了PNP和电源管理的默认实现,使得我们暂时不必关注一些复杂的东西,简化了学习难度。另一方面,我们本着由浅入深的原则,一开始对于驱动框架中的东西不必要所有的都弄懂,先有一个概念,随着学习的深入,自己动手实践,有些东西慢慢地就理解了。
老实说,对于选择什么程序作为第一个学习的驱动程序,一直纠结于是否要用Hello World。最后还是决定使用它,一方面,这绝对是最简单的,简单到大家都不愿意看,也给刚刚起步的人一个驱动的直观印象。另一方面,也是最重要的,那就是第一个程序的重要性不在于这个程序本身。更重要的作用是帮助我们磨刀,我们要让这个简单的KMDF程序从编写到编译成功,并成功的安装运行。熟悉整个过程,熟悉工具的使用,毕竟,实践才是学习的最好方法。
由于太穷,手头只有一台笔记本电脑,当然我不能直接用来开发驱动,万一什么地方出问题系统直接炸了那不是呵呵了。所以掏出利器VMware Workstation,在虚拟机中装Win7肯定也不行,太卡,因此装上XP。本机Win7装上驱动开发环境WDK,Visual Studio 2008,用来编写编译驱动,然后将编译好的驱动安装到XP虚拟机中。本机Win7下装上WinDbg进行联机调试,这样,编辑编译调试环境都有了。
说明一下,虽然装了Visual Studio 2008,但是我们并不用它来编译驱动,它仅用作代码编辑,我习惯使用它。如果你愿意,也可以仅仅使用记事本,但是这样没有语法高亮,写出来代码黑乎乎一片,看起来并不具有一个良好的心情。不过这因人而异,随自己所好。编译所需的makefile和sources等文件我们都自己编写,有开发环境可以自动生成,但是学习时并不提倡这样,手动进行这些过程可以帮助我们理解驱动的工作过程,同时当我们编写的驱动安装失败或者无法工作时,也给解决方法提供一些灵感。
总结一下,我在开始一个最简单的驱动开发的时候用到了如下工具:
这个驱动的功能就是在设备被安装的时候打印出“Hello World!”字符串,在驱动被卸载的时候打印出“Thanks For Using!”字符串。顺便说一下,对于代码,我并不会一句一句地讲那么详细,因为网上并不缺乏驱动教程。你在看本教程之前,应该先去看看相关的教程和书籍,本教程的使用的例子基本上都来自于相关书籍和教程。那么,我写下这个教程有什么意义?一方面,学习是需要整理的,经常需要理一理自己学过的东西,有助于自己进步,所以我把它整理出来便于自己学习进步。另一方面,网上的教程和书籍一下子灌输了太多的东西,看着看着就迷失了。在内核下写程序,很重要的功力就是对于内核的了解,但一开始我们知道的甚少,一下子灌输内核的许多东西会造成很难学习,迷失不知怎么进行下一步。而且,这些书籍中的实例对于初学者过于复杂,没有循序渐进的过程,难免打击学习信心。所以我在教程中尽量简化,一步一步深入,慢慢地体会驱动开发。这也是我自己的学习过程。
回到我们的Hello World驱动,再编写它之前,你应该对于设备堆栈和设备安装和移除的过程有一些了解,你可以看看《Windows 7设备驱动程序开发》第7章“即插即用和电源管理”的部分内容。在设备插入时,框架会调用EvtDriverDeviceAdd回调函数,在该函数中创建设备对象。驱动卸载时,框架调用EvtDriverUnload回调函数,所以,我们只需要自己实现这两个回调函数,然后向框架注册即可。
下面是完整的代码:
#include <ntddk.h>
#include <wdf.h>
// 添加设备回调函数
NTSTATUS EvtDriverDeviceAdd( IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit);
// 驱动卸载回调函数
VOID EvtDriverUnload( IN WDFDRIVER Driver );
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath )
{
NTSTATUS status;
WDF_DRIVER_CONFIG config;
// 注册EvtDriverDeviceAdd回调函数
WDF_DRIVER_CONFIG_INIT(&config, EvtDriverDeviceAdd);
// 注册EvtDriverUnload回调函数
config.EvtDriverUnload = EvtDriverUnload;
// 创建驱动对象
status = WdfDriverCreate( DriverObject, RegistryPath,
WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
return status;
}
NTSTATUS EvtDriverDeviceAdd(IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit)
{
NTSTATUS status;
WDFDEVICE hDevice;
PAGED_CODE();
// 创建设备对象
status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice);
if( !NT_SUCCESS(status) )
{
KdPrint(("EvtDriverDeviceAdd failed.\n"));
return status;
}
KdPrint(("Hello World!\n"));
return status;
}
VOID EvtDriverUnload( IN WDFDRIVER Driver )
{
KdPrint(("Thanks For Using!\n"));
}
在编译驱动之前,除了源代码(.h和.c),还要一些用于编译的其它文件。这部分内容需要先涉及一些驱动安装相关的知识,了解驱动安装运行需要哪些文件,我们才好准备这些内容。这部分内容最完整的资料是WDK文档和MSDN文档,下面将一些重要内容做简单介绍,但是更详细的内容,读者还是要自己看文档。
MSDN给出了驱动开发和安装的七个步骤,如下:
也就是说,我们最后开发的所有东西都通过一个驱动包发布出去,商业的驱动包最后都打包成一个.exe,引导用户一步一步安装完成即可。最开始学习时,不需要做到这一步,但是我们需要提供一个最简单的驱动包。虽然不需要提供自安装功能,起码要包含安装运行所需要的所有内容。
根据MSDN,驱动包中包含以下文件:驱动文件、安装文件以及其它文件。其中驱动文件是由我们所写的源代码编译而成的.sys动态库,安装文件包含INF文件和签名文件(.cat),其它文件则是一些说明文件。测试签名本来是应该做的,但是这个过程并不简单,对于初学者来说,写INF文件已经是一个繁琐复杂的过程了。所以我们暂时先忽略它,相关的内容以后单独作一节来写。
现在我们就明确编译阶段的任务,生成.sys驱动,INF安装文件。与WDK中的示例一样,现在我们要编写几个文件:makefile,makefile.inc,sources,和INX。
Makefile和makefile.inc文件直接从示例中拷贝即可,基本保持不变,如果读者想弄清楚具体内容,需要知道makefile的相关知识。至于其中的一些宏定义,WDK中bin目录下有一个setenv.bat文件,从快捷方式打开WDK编译环境时会运行这个批处理文件(如图5.1),设置环境变量,这个文件中有一部分宏定义。
下面的重点是INX文件的编写。INX文件实际上是一个包含字符串变量的INF文件,其中的字符串变量表示了版本信息,通过一个INX文件可以编译出用于不同平台和框架版本的INF文件。编译它的工具为Stampinf.exe,位于WDK中bin目录下。关于INF文件的中文最详尽清楚的教程,应该是张佩、马勇和董鉴源编著的《竹林蹊径 深入浅出Windows驱动开发》,里面对于驱动安装和测试有比较详细地描述。读者应该先看一下该书及相关文档。而且,读者一定要去看看该书,应用我的一些术语说法引用自该书,书中解释得很清楚,我只是教大家写一个完整的INX文件。
那么我们直接开始编写INX文件了,编写的过程中请参考WDK示例。此处说一下,第一个INX文件不要直接拷贝示例然后修改,最好从一个空白文件一行一行写起。你只有知道一个INX文件包含哪些部分,每部分如何组成的,以后拷贝出问题后才知道怎么修改。
INX文件与ini文件格式类似,因此,可以新建一个HelloWorld.ini文件,便于在Notepad++中提供语法高亮,阅读起来更容易,一般INX文件与驱动同名。不过,ini的注释为单行,而INX文件中的注释不需要另起一行。
先添加版本域,这也是一个必选的域:
----------------------------------------------------------------------------------
; 版本域
[Version]
Signature="$WINDOWS NT$" ; 固定
Class=HUSTSample ; 我们的驱动不属于已有的设备类,定义一个新的设备类
ClassGuid={FDA3877E-5FF3-4c18-8235-7FEA16EE04E2} ; 设备类的GUID
Provider=%ProviderName% ; 驱动的作者,由字符串域中的ProviderName指定
DriverVer=01/12/2016,21.12.36.570 ; 驱动版本
; 字符串域
[Strings]
ProviderName=”HUSTD10” ; 驱动作者
----------------------------------------------------------------------------------
版本域中涉及到新添加的设备类,同样在《竹林蹊径 深入浅出Windows驱动开发》有比较详细的描述。Provider引用了字符串域中的内容,还有其它部分也在字符串域定义了相关内容,所以这里的字符串域并不是最后的内容,后面还会定义新的内容,读者需要把这些内容组合到一起。此处需要一个设备类的GUID,如果安装了Visual Studio,那么Tools目录下有一个guidgen.exe工具,可用来生成GUID,当然也可以去网站在线生成一个GUID,如http://www.guidgen.com/。
由于我们新添加了自己的设备类HUSTD10,需要在注册表的HKLM\SYSTEM\CurrentControlSet\Control\Class下添加相关注册表项,因此,INX文件要添加的下一个域为设备类安装域:
----------------------------------------------------------------------------------
; 设备类安装域
[ClassInstall32]
Addreg=SampleClassReg ; 写注册表之类,要写入的内容放在SampleClassReg子域中
----------------------------------------------------------------------------------
----------------------------------------------------------------------------------
; SampleClassReg子域
[SampleClassReg]
HKR,,,0,%ClassName% ; 设备类的名称,由字符串域的ClassName决定
HKR,,Icon,,-5 ; 设备管理器中显示的图标,使用5号系统图标
; 字符串域
[Strings]
ClassName=”HUST Sample Device” ; 设备类的名称
----------------------------------------------------------------------------------
搞定了设备类之后,我们需要给我们的设备指定厂商、产品和设备本身的相关信息,先指定厂商域:
----------------------------------------------------------------------------------
; 厂商域
[Manufacturer]
%MfgName%=Standard,NT$ARCH$ ; 厂商名由字串串域MfgName决定,产品域有两个,
;一个为Standard,一个为Standard.NT$ARCH$(带版本信息)
; 字符串域
[Strings]
MfgName=”HUSTD10” ; 厂商名称
----------------------------------------------------------------------------------
下面是两个产品域:
----------------------------------------------------------------------------------
; Standard产品域
[Standard]
%HelloWorld.DeviceDesc%=HelloWorld_Device, root\HelloWorld ; 设备描述(可以等同理
;解为设备名)由字符串域中的HelloWorld.DeviceDesc指定,设备安装域为HelloWorld_Device,
;硬件ID为root\HelloWorld
; Standard.NT$ARCH$产品域
[Standard.NT$ARCH$]
%HelloWorld.DeviceDesc%=HelloWorld_Device, root\HelloWorld ; 同上
; 字符串域
[Strings]
HelloWorld.DeviceDesc=”HelloWorld” ; 设备描述(等同理解为设备名)
----------------------------------------------------------------------------------
写了厂商域和产品域之后,接下来就是设备本身的信息了,也就是设备域,或者设备安装域,是很重要的一个部分,也被称作DDInstall域。
----------------------------------------------------------------------------------
; 设备安装域
[HelloWorld_Device.NT]
CopyFiles=Drivers_Dir ; 需要拷贝的文件列表,由Drivers_Dir子域指定,一般这个文件就
;是我们编写的驱动文件.sys了
; Drivers_Dir子域
[Drivers_Dir]
HelloWorld.sys ; 我们编写的驱动文件
----------------------------------------------------------------------------------
有人会注意到前面产品域中定义的设备域为HelloWorld_Device,为什么后面却变成了HelloWorld_Device.NT?实际上我们不仅仅只有HelloWorld_Device.NT,还可以定义HelloWorld_Device.NTx86,HelloWorld_Device.NTIA64,或者HelloWorld_Device.NTAMD64域,它们表示不同的硬件平台版本,当然也可以不写版本,就写HelloWorld_Device,这与HelloWorld_Device.NT是相同的,表示与硬件平台无关。
下面定义设备安装域一个重要的子域——服务子域,用来向系统注册服务,为设备的运行提供支持。
----------------------------------------------------------------------------------
; 设备域服务子域
[HelloWorld_Device.NT.Services]
AddService=HelloWorld, %SPSVCINST_ASSOCSERVICE%, HelloWorld_Service_Inst ; 添加服务
;指令,添加的服务名为HelloWorld,%SPSVCINST_ASSOCSERVICE%为一个标记,指定如何增加服
;务,在Setupapi.h中定义,HelloWorld_Service_Inst为服务安装子域
; 字符串域
[Strings]
SPSVCINST_ASSOCSERVICE = 0x00000002 ; 服务安装标记
----------------------------------------------------------------------------------
下面是服务安装子域:
----------------------------------------------------------------------------------
; HelloWorld_Service_Inst服务安装子域
[HelloWorld_Service_Inst]
DisplayName = %HelloWorld.SVCDESC% ; 服务显示名称,与服务名不同
ServiceType = 1 ; 服务类型SERVICE_KERNEL_DRIVER
StartType = 3 ; 启动类型SERVICE_DEMAND_START
ErrorControl = 1 ; 错误控制级别SERVICE_ERROR_NORMAL
ServiceBinary = %12%\HelloWorld.sys ; 镜像文件路径,即.sys文件路径
; 字符串域
[Strings]
HelloWorld.SVCDESC = “Hello World Service” ; 服务显示名称
----------------------------------------------------------------------------------
接下来是设备安装域另一个重要的子域——辅助安装器子域(CoInstallers),该子域的作用是把一个或多个DLL文件记录到注册表中,并把它们拷贝到相关目录下。
----------------------------------------------------------------------------------
;设备域辅助安装器子域
[HelloWorld_Device.NT.CoInstallers]
AddReg = HelloWorld_Device_CoInstaller_AddReg ; 写注册表子域
CopyFiles = HelloWorld_Device_CoInstaller_CopyFiles ; 拷贝文件列表
; 写注册表子域
[HelloWorld_Device_CoInstaller_AddReg]
HKR,,CoInstallers32,0x00010000,"WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll,WdfCo-
Installer" ; 注册表项为CoInstallers32,WdfCoInstaller为入口函数
; 拷贝文件列表
[HelloWorld_Device_CoInstaller_CopyFiles]
WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll ; 拷贝对应版本的.dll
----------------------------------------------------------------------------------
接着是Wdf域,安装框架的辅助安装程序之后,框架将在安装驱动程序时读取这部分。
----------------------------------------------------------------------------------
; Wdf域
[HelloWorld_Device.NT.Wdf]
KmdfService = HelloWorld, HelloWorld_wdfsect ; HelloWorld为操作系统分配给驱动程序
;的内核模式服务的名称,HelloWorld_wdfsect是一个子域
; HelloWorld_wdfsect子域
[HelloWorld_wdfsect]
KmdfLibraryVersion = $KMDFVERSION$ ; KMDF库的版本号
----------------------------------------------------------------------------------
由于INX文件中包含了文件拷贝指令CopyFiles,需要几个额外的域来指定文件位置,它们是源磁盘域([SourceDisksNames])、源文件域([SourceDisksFiles])和目的域([DestinationDirs])。
----------------------------------------------------------------------------------
; 源磁盘域
[SourceDisksNames]
1 = %DiskId1%,,,”” ; 只有一个磁盘,ID为1,磁盘描述为字符串域中的DiskId1
; 源文件域
[SourceDisksFiles]
; 本INX文件CopyFiles涉及两个文件的拷贝,HelloWorld.sys和辅助安装器WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll,分别指定它们
HelloWorld.sys = 1 ; 1号磁盘
WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll = 1 ; 1号磁盘
; 目的域
[DestinationDirs]
DefaultDestDir = 12 ; 默认目录,12是Windows定义的,等同于%SystemRoot%\system32\drivers
HelloWorld_Device_CoInstaller_CopyFiles = 11 ; 除了默认目录外,还有一个CopyFiles子域,
;此子域不使用默认目录,定义为11,等同于%SystemRoot%\system32。其它没有特殊定义的
;CopyFiles子域则使用默认目录
; 字符串域
[Strings]
DiskId1 = “HUSTD10 HelloWorld Disk #1” ; 1号磁盘描述
----------------------------------------------------------------------------------
好了,到此INX文件就写好了,下面给出一个完整版本的INX文件:
----------------------------------------------------------------------------------
; 完整的Hello Word驱动INX文件
; 参考WDK示例 echo.inx
;******************************************************************************
; 版本部分
;******************************************************************************
; 版本域
[Version]
Signature="$WINDOWS NT$" ; 固定
Class=HUSTSample ; 我们的驱动不属于已有的设备类,定义一个新的设备类
ClassGuid={FDA3877E-5FF3-4c18-8235-7FEA16EE04E2} ; 设备类的GUID
Provider=%ProviderName% ; 驱动的作者,由字符串域中的ProviderName指定
DriverVer=01/12/2016,21.12.36.570 ; 驱动版本
;******************************************************************************
; 设备类部分
;******************************************************************************
; 设备类安装域
[ClassInstall32]
Addreg=SampleClassReg ; 写注册表之类,要写入的内容放在SampleClassReg子域中
; SampleClassReg子域
[SampleClassReg]
HKR,,,0,%ClassName% ; 设备类的名称,由字符串域的ClassName决定
HKR,,Icon,,-5 ; 设备管理器中显示的图标,使用5号系统图标
;******************************************************************************
; 产商及设备安装部分
;******************************************************************************
; 厂商域
[Manufacturer]
%MfgName%=Standard,NT$ARCH$ ; 厂商名由字串串域MfgName决定,产品域有两个,一个
;为Standard,一个为Standard.NT$ARCH$(带版本信息)
; Standard产品域
[Standard]
%HelloWorld.DeviceDesc%=HelloWorld_Device, root\HelloWorld ; 设备描述(可以等
;同理解为设备名)由字符串域中的HelloWorld.DeviceDesc指定,设备安装域为HelloWo-
;rld_Device,硬件ID为root\HelloWorld
; Standard.NT$ARCH$产品域
[Standard.NT$ARCH$]
%HelloWorld.DeviceDesc%=HelloWorld_Device, root\HelloWorld ; 同上
; 设备安装域
[HelloWorld_Device.NT]
CopyFiles=Drivers_Dir ; 需要拷贝的文件列表,由Drivers_Dir子域指定,一般这个文件
;就是我们编写的驱动文件.sys了
; Drivers_Dir子域
[Drivers_Dir]
HelloWorld.sys ; 我们编写的驱动文件
; 设备域服务子域
[HelloWorld_Device.NT.Services]
AddService=HelloWorld, %SPSVCINST_ASSOCSERVICE%, HelloWorld_Service_Inst ; 添加
;服务指令,添加的服务名为HelloWorld,%SPSVCINST_ASSOCSERVICE%为一个标记,指定如何
;增加服务,在Setupapi.h中定义,HelloWorld_Service_Inst为服务安装子域
; HelloWorld_Service_Inst服务安装子域
[HelloWorld_Service_Inst]
DisplayName = %HelloWorld.SVCDESC% ; 服务显示名称,与服务名不同
ServiceType = 1 ; 服务类型SERVICE_KERNEL_DRIVER
StartType = 3 ; 启动类型SERVICE_DEMAND_START
ErrorControl = 1 ; 错误控制级别SERVICE_ERROR_NORMAL
ServiceBinary = %12%\HelloWorld.sys ; 镜像文件路径,即.sys文件路径
;******************************************************************************
; 辅助安装器部分
;******************************************************************************
;设备域辅助安装器子域
[HelloWorld_Device.NT.CoInstallers]
AddReg = HelloWorld_Device_CoInstaller_AddReg ; 写注册表子域
CopyFiles = HelloWorld_Device_CoInstaller_CopyFiles ; 拷贝文件列表
; 写注册表子域
[HelloWorld_Device_CoInstaller_AddReg]
HKR,,CoInstallers32,0x00010000,"WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll,
WdfCoInstalle-r" ; 注册表项为CoInstallers32,WdfCoInstaller为入口函数
; 拷贝文件列表
[HelloWorld_Device_CoInstaller_CopyFiles]
WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll ; 拷贝对应版本的.dll
; Wdf域
[HelloWorld_Device.NT.Wdf]
KmdfService = HelloWorld, HelloWorld_wdfsect ; HelloWorld为操作系统分配给驱动
;程序的内核模式服务的名称,HelloWorld_wdfsect是一个子域
; HelloWorld_wdfsect子域
[HelloWorld_wdfsect]
KmdfLibraryVersion = $KMDFVERSION$ ; KMDF库的版本号
;******************************************************************************
; 文件域部分
;******************************************************************************
; 源磁盘域
[SourceDisksNames]
1 = %DiskId1%,,,”” ; 只有一个磁盘,ID为1,磁盘描述为字符串域中的DiskId1
; 源文件域
[SourceDisksFiles]
; 本INX文件CopyFiles涉及两个文件的拷贝,HelloWorld.sys和辅助安装器WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll,分别指定它们
HelloWorld.sys = 1 ; 1号磁盘
WdfCoInstaller$KMDFCOINSTALLERVERSION$.dll = 1 ; 1号磁盘
; 目的域
[DestinationDirs]
DefaultDestDir = 12 ; 默认目录,12是Windows定义的,等同于%SystemRoot%\system32\
;drivers
HelloWorld_Device_CoInstaller_CopyFiles = 11 ; 除了默认目录外,还有一个Copy-
;Files子域,此子域不使用默认目录,定义为11,等同于%SystemRoot%\system32。其它没
;有特殊定义的CopyFiles子域则使用默认目录
;******************************************************************************
; 字符串部分
;******************************************************************************
; 字符串域
[Strings]
ProviderName="HUSTD10" ; 驱动作者
ClassName="HUST Sample Device" ; 设备类的名称
MfgName="HUSTD10" ; 厂商名称
HelloWorld.DeviceDesc="HelloWorld" ; 设备描述(等同理解为设备名)
SPSVCINST_ASSOCSERVICE = 0x00000002 ; 服务安装标记
HelloWorld.SVCDESC = "Hello World Service" ; 服务显示名称
DiskId1 = "HUSTD10 HelloWorld Disk #1" ; 1号磁盘描述
----------------------------------------------------------------------------------
该INX文件相对于最终的INF文件,多了表示版本的几个字符串变量,它们分别是: ARCH , KMDFCOINSTALLERVERSION 和 KMDFVERSION 。 ARCH 表示硬件平台,使用x86编译环境, ARCH 会被替换为“x86”, KMDFCOINSTALLERVERSION 表示辅助安装器(Co-installer)的版本, KMDFVERSION 表示KMDF库的版本。它们都会在生成INF文件时被替换,读者可以自行比较。
INX文件生成INF文件是通过WDK中bin目录下的StampInf.exe工具完成的,后面我们通过批处理命令在makefile中调用它。现在,我们先手动运行它来看看。进入WDK编译环境,切换到HelloWorld.inx目录下,输入stampinf -?查看帮助。
Stampinf直接修改文件,因此我们把HelloWorld.inx复制一份,命名为HelloWorld.inf,输入命令: stampinf -f HelloWorld.inf -a x86 -k 1.9,命令表示x86硬件平台,KMDF主版本号为1,次版本号为9。Stampinf还可以给DriverVer打上时间戳。
可以使用另一个工具来检查生成的inf文件的语法,这个工具为ChkInf,位于tools\Chkinf目录下,是一个脚本。切换到tools\Chkinf目录下,输入:chkinf D:\HUSTD10\HelloWorld.inf /O D:\Temp 命令,chkinf会将结果放到d:\temp目录下,用浏览器查看即可。
好了,Inf文件就说这么多,接下来还有一个文件要编写,即sources文件,这个文件比较简单,就不一步一步写了。
----------------------------------------------------------------------------------
TARGETNAME=HelloWorld # 编译后.sys文件名
TARGETTYPE=DRIVER # 编译类型为驱动
KMDF_VERSION_MAJOR=1 # KMDF主版本
INF_NAME=HelloWorld # 通过 INX生成的INF文件名,这个变量在makefile.inc中被引用
INCLUDES=$(INCLUDES);..\..\inc #头文件目录
NTTARGETFILE0=$(OBJ_PATH)\$(O)\$(INF_NAME).inf
PASS0_BINPLACE=$(NTTARGETFILE0) #通过INX生成的INF文件路径
C_DEFINES= $(C_DEFINES) #一些编译器的开关
SOURCES=driver.c # 源代码文件列表
----------------------------------------------------------------------------------
现在,编译所需的文件就都准备好了,打开编译环境,进入项目目录,输入build命令编译,编译生成了HelloWorld.sys,HelloWorld.inf以及用于调试的符号文件。下面我们来安装驱动。
由于HelloWorld驱动为纯软件驱动,所以驱动不能像我们插上U盘那样自动安装,因为即插即用管理器检测不到新的设备。而且驱动程序没有测试签名,只能用管理员身份,以批准驱动的安装。
(1)准备驱动包。把前面编译生成的HelloWorld.sys和HelloWorld.inf拷贝出来,然后把WDK中redist\wdf\x86目录下的辅助安装dll拷贝一份,里面有调试版本(checked)和Release版本两个版本的库。我们使用的调试版本编译的,所以把调试版本也带上。这样,驱动包内容有:
把它拷贝到虚拟机WinXP中。
(2)在控制面板中找到“添加硬件”向导,单击“是,我已经连接了此硬件”,然后在列表底部选择“添加新的硬件设备”,单击“安装我手动从列表选择的硬件”,在顶部选择“显示所有设备”,单击“从磁盘安装”,选择HelloWorld.inf文件(或者直接输入目录路径),点击“确定”,然后型号列表中显示“HelloWorld”,并且下面提示“这个驱动程序没有经过数字签署”,点击“下一步”,继续“下一步”,出现如下对话框:
提示需要HelloWorld.sys,点击“浏览”选择该文件,然后确定。等待安装完成,如图6.2。这时候在设备管理器中查看,多了一个”HUST Sample Device”设备类,下面有一个HelloWorld设备,如图6.3。
读者可以右键点击该设备,把属性和INF中相关内容比对一下。同样,读者也应该自己去比对一下注册表的相关条目。驱动的卸载也在设备管理器中完成。
内核中输出的内容需要一些工具才能看到,这些工具有好几个。开发程序调试是必须的,因此我们安装调试器WinDbg。它不仅可以看到内核输出,还可以调试驱动程序,尤其联机调试很方便。
WinDbg搭配虚拟机环境的搭建在谭文、杨潇和邵坚磊编著的《寒江独钓——Windows内核安全编程》中的第1章有详细地描述,这里就不再赘述了。
还记得我们在HelloWorld驱动中打印出来的两条信息吗?在设备安装的回调函数中我们打印了”Hello World!”,设备卸载的回调函数中打印了”Thanks For Using!”。现在我们在WinDbg中来看看。双机调试连接上虚拟机,然后在设备管理器中右键点击HelloWorld设备,选择“停用”,弹出的警告框选择“是”,这时候在WinDbg中打印出了”Thanks For Using!”,如图7.1。然后在设备管理器中右键点击HelloWorld设备,选择“启用”,这时候在WinDbg中又打印出了”Hello World!”,如图7.2。
为了调试HelloWorld驱动,我们需要在WinDbg中设置系统符号表路径和HelloWorld符号文件路径。网络顺畅的情况下,内核符号表我们设置微软符号服务器就行了,WinDbg会自己从服务器下载。如果网络不顺,那你需要把内核符号表下载到本地,这时候要注意版本。
例如我编译的是XP SP3的驱动,那么我需要下载XP的符号表,而不是Win7的。
我们使用符号服务器,在C盘建立一个本地缓存文件夹symbols,然后在WinDbg的Symbol File Path中添加srv*c:\symbols*http://msdl.microsoft.com/download/symbols。HelloWorld的符号文件就是pdb文件,和编写应用程序exe生成的类似。同样,添加其路径,用分号分隔,如图7.3。
此时,如果WinDbg在BUSY的话,用Ctrl+Break使其断下,可以看到其加载内核符号的信息,如图7.4。此时,C:\symbols目录下已经有了pdb文件的缓存。
为了调试HelloWorld,我们在DriverEntry入口函数中手动加入一个断点指令int 3,这部分内容同样在《寒江独钓——Windows内核安全编程》有比较详细的描述,包括后面的单步调试执行等等,我就不写了。更改代码后,重新编译驱动包,然后在设备管理器中选择HelloWorld设备,右键点击选择“更新设备驱动程序”即可,不需要卸载之后重装。调试操作后面的教程中还会涉及到,读者先跟着《寒江独钓——Windows内核安全编程》做就行了。
好了,到此为止,又臭又长的HelloWorld就写完了。虽然很长,却也没扯什么题外话,看起来比较乏味无聊。能力有限,反正看这个东西的人在乎的是内容,而不是好不好玩。毕竟没有多少人愿意学Windows内核。总结一下本节内容,基本上都是在做准备工作,没有什么编程的东西。但是不要小看准备工作,一方面准备工作出错,驱动跑不起来会浪费大量时间,另一方面,从准备工作中也可以学到不少东西。代码相关的所有东西我都打包放到github上,读者可以自己去下载,地址为https://github.com/hustd10/Windows-Driver。
我写出来的操作都是在XP下的,Win7大同小异。文中我省略给出相关文档资料的部分,读者如果不熟悉相关内容,自己一定要找来看看,把每一部分弄懂才能把知识串起来。另外,实践出真知,我尽量写得详细一些,不是让大家看完作罢,而是让大家能够像我一样实际运行起来。如果读者发现本文中的错误,请发邮件告知,邮箱[email protected]。我不是专业的驱动开发人员,写这个教程的时候我自己也在学,错误难免,而且很多内容都是“借鉴”过来的,望大家指正和谅解,谢谢你的阅读。
PS:之前用Word写,今天改用Markdown编辑器,本来是准备写完几套之后才发出来的,刚用编辑器,就顺便发出来看看页面编辑效果。涉足Windows内核纯属爱好,与我研究生专业无关,平时较忙,但该教程会持续更新。如果能够解决一些你的疑问,那么我的目的就达到了。