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

第2章 商业驱动开发技术

大多数初学者将来都要加入到正规化的商业化开发过程中。在目前的中国公司中,实施正规开发过程的公司很少,大多数小公司都是一两个人几条枪的传统作坊方式开发。在这样的非正规开发过程中,会导致很多问题,比如代码无法回溯,从而无法解决新加入特性而引入的bug;发生蓝屏故障后,无法正确定位到相应的代码行,无法查看当时的变量值,从而无法快速修复bug;以及其他类似的由于环境不完善导致的时间浪费。虽然我们是初学者,但是要在高起点上介入开发过程,笔者将结合自己的十余年开发经验,讲解一下驱动开发过程中的版本控制、符号存档,以及诸如驱动程序发布等相关的问题,让大家对正规的商业开发环境有所了解。

在所有解决问题的方法中,最简单的也是最有效的方法就是版本回溯。在不理解原理或者不好直接找出原因的情况下,这是比较方便有效地修复bug的方法。当然,前提是在以前的某个版本中可以正常工作,后来的版本修改导致不能工作后,通过比较不同版本之间的不同来解决问题。通常可以使用的版本控制系统有CVS、VSS、ClearCase及SVN等,其中VSS和ClearCase是商业软件。现在社区和一般公司通常采用SVN进行版本控制,使用cruisecontrol.net进行持续集成操作。限于篇幅和本书的主题,在此不方便讲述CruiseControl.net实施的持续集成技术,有兴趣的读者可以搜索相关资料进行研究(google.com)。

为了方便大多数开发人员的配置和测试,我们的版本控制系统基于VisualSVN Server进行架设,客户端使用TortoiseSVN软件。实际上,在正式的商业开发环境中,一般都是基于Linux或UNIX进行版本管理的,这通常需要专业的配置管理员进行设置,比如笔者所在公司的研发部就使用Freebsd系统安装SVN服务器进行管理。本章我们要介绍的实际上是软件配置管理技术,但是因为本书的目标是讲解驱动开发知识,所以这里只讲述其中的几种技术,比如版本控制和符号管理。

为了方便配置和讲述,也方便大家尽快熟悉环境,我们将在Windows XP操作系统上搭建开发环境。

2.1 建立开发调试环境

工欲善其事,必先利其器。

记得很久以前看过侯俊杰编著的《深入浅出MFC》一书,因为时间的关系,以及笔者不太用MFC写程序的原因,已经记不得书中讲的具体内容了,但是有一句话却记得很清楚:勿在浮沙筑高台。在本节中,将重点讲述开发环境准备的方法和步骤。

2.1.1 SVN环境

为了简化设置,我们选择VisualSVN Server软件来架设服务器。VisualSVN Server软件带有图形化的管理工具,操作方便、简单。我们需要到以下网址下载该软件:

http://www.visualsvn.com/server/download/

下载完成后,直接运行并安装它即可。在安装过程中一切参数都取默认值,不需要修改。

安装完成后,自动运行管理控制台界面,如图2-1所示。

图2-1 VisualSVN Server管理控制台界面

我们将创建一个仓库,命名为:drivertest。

在左侧的“Repositories”项上右键单击,选择第一项“Create New Repository”,创建新的仓库,如图2-2所示。

图2-2 创建新的仓库

如图2-3所示,输入新仓库名字:drivertest。注意方框中所显示的内容,这就是后面要使用的SVN服务器地址。

图2-3 输入新仓库名字

为了简化起见,我们可以把计算机名字换成SVN服务器所在机器的IP地址。我们这台机器的IP地址为:10.0.1.191,所以,后面使用的SVN服务器地址为:

https://10.0.1.191/svn/drivertest

接下来,需要创建一个新用户,用户名为test,密码也为test,如图2-4所示。

图2-4 创建SVN用户

当然,更复杂的方式是可以创建一个组,然后在组中创建一个新用户。这已经属于SVN配置管理的范畴了,在这里只需要能使用即可。如果读者对SVN管理有兴趣,请参看SVN文档以进一步了解这方面的知识。

2.1.2 创建工程,导入SVN

为了方便描述,我们使用easySYS生成一个测试工程,本驱动程序不会完成任何有用的功能,仅用于测试目的。我们将用它演示怎么建立符号服务器,使用符号服务器调试程序或分析bug。这里采用zgmap@zgmap修改后的easySYS 0.3.2.6.2版本,此版本支持最新的Visual Studio 2010环境。

我们已经成功在E盘根下生成了这个测试工程,启动WDK的编译环境后,输入“build -cZ”命令,就会成功编译出驱动程序。

如图2-5所示,通过快捷方式启动一个类似DOS的命令行窗口,输入“build-cZ”命令进行编译,确保能够正确编译,并生成一个可执行镜像文件。

图2-5 启动WDK命令行快捷方式

打开编译后的目录,查看生成的结果,如图2-6所示。

图2-6 编译后生成的结果

矩形框中的文件就是生成的驱动程序,圆角框中的文件就是这个驱动程序对应的符号文件。

接下来,安装SVN的客户端程序TortoiseSVN,将此工程加入版本控制系统。可以从http://tortoisesvn.net/downloads这个地址下载TortoiseSVN软件,安装时选择默认参数即可,无须改变任何参数设置。

打开工程目录的上一级目录,比如工程位于F:\TestDrv目录下,我们打开F:\目录,然后在TestDrv目录上单击右键,选择“TortoiseSVN”→“Import”,将源码导入SVN仓库,如图2-7所示。

图2-7 选择将源码导入SVN仓库选项

输入SVN路径:https://10.0.1.191/svn/drivertest,然后输入上面创建的用户名和密码(都是test),单击“确定”按钮,至此代码上传完成。上传完成后,为了方便开发,可以将刚才的TestDrv目录删除(在删除之前确认已经成功上传代码)。创建新的TestDrv目录,并右键单击此目录,选择“SVN Checkout”,如图2-8所示。

图2-8 选择从SVN仓库提取代码选项

在如图2-9所示的界面中,不需要修改默认参数。

图2-9 SVN服务器地址示例

确定后,就可以从SVN仓库提取代码了,如图2-10所示。

图2-10 正在从SVN仓库提取代码界面

至此,我们已经成功创建了版本控制环境。我们提供的修订号为2,每次修改代码提交后,修订号都会自动增加一。

2.1.3 建立符号服务器

我们需要安装WDK(Windows Driver Kits),在安装时将调试器也选上。这里使用的是WDK 7.1,下载地址如下:

http://www.microsoft.com/downloads/en/details.aspx?displaylang=en&FamilyID=36a26 30f-5d56-43b5-b996-7633f2ec14ff

下载后的文件名为“GRMWDK_EN_7600_1.ISO”,用虚拟光驱软件加载此ISO文件,然后安装。你可以安装所有的选项;当然,如果你很清楚各个选项的作用,也可以有选择地安装一些必需的组件。

注意:有可能在本书发行时,WDK已经升级,你可以搜索一下最新版本,然后安装,安装过程是相似的。

安装完成后,我们需要用到WDK自带的symstore.exe这个程序文件,它是建立符号服务器的关键工具。下面是symstore.exe的用法。

symstore.exe add /r /f %1 /s %2 /t "自定义服务器名称" /v "%3"

● %1为pdb文件所在的目录。

● %2为符号服务器路径,比如共享目录或网络盘符。

● %3为版本号,版本号可以任意,比如1.0。

我们可以将对此程序的调用写入批处理文件中,然后在每次编译完成后运行一下,这样就可以将符号文件添加到符号服务器中了。

为了便于描述,我们创建了两个目录:symbols作为符号服务器目录,symcache作为微软符号服务器的本地cache。为了能描述远程情况下符号服务器的使用,我们将symbols目录共享(见图2-11),通过共享方式访问,共享路径为:\\10.0.1.191\symbols。

图2-11 工作目录示意图

为符号添加批处理代码为(不包括分隔用的虚线部分):

      ---------------------------------------------------------------------
      rem %1 i386 directory; %2 symbol server path ; %3 version
      if exist D:\WinDDK\7600.16385.0\Debuggers\symstore.exe "d:\WinDDK\7600.
  16385.0\Debuggers\symstore.exe" add /r /f %1 /s %2 /t "KahserLab"  /v "%3"
      if exist "C:\Program Files\Debugging Tools for Windows (x86)\symstore.
  exe" "C:\Program Files\Debugging Tools for Windows (x86)\symstore.exe" add
  /r /f %1 /s %2 /t "KahserLab"  /v "%3"
      if exist "C:\Program Files\Debugging Tools for Windows (x64)\symstore.
  exe" "C:\Program Files\Debugging Tools for Windows (x64)\symstore.exe" add
  /r /f %1 /s %2 /t "KahserLab"  /v "%3"
      if exist "C:\WinDDK\7600.16385.1\Debuggers\symstore.exe" "C:\WinDDK\7600.
  16385.1\Debuggers\symstore.exe"  add /r /f %1 /s %2 /t "KahserLab"  /v "%3"
      ---------------------------------------------------------------------

再编辑一个buildit.cmd命令脚本文件,内容为:

      build -cZ
      call addsymbol.cmd E:\testdrv\objchk_wxp_x86\i386  \\10.0.1.191\symbols 1.0

打开WDK的命令行编译环境,运行buildit.cmd后,将看到在symbols目录下已经有内容了,它们就是符号文件,如图2-12所示。

图2-12 符号服务器目录示意图

它会自动维护版本,再编译一次,打开TestDrv.sys目录看一下,显示内容如图2-13所示。

图2-13 TestDrv.sys目录示意图

可以看到,每次编译后都会生成一个新目录,里面放的文件就是与该驱动相关的文件,如图2-14所示。

图2-14 符号服务器目录中的文件示意图

其他目录中的内容也类似。

2.1.4 用符号调试

为了调试驱动程序,需要正确设置微软公用符号服务器地址和我们的符号服务器地址。在接下来的调试中,我们将模拟蓝屏故障,然后用符号服务器上的数据调试和查找错误位置。

在这个例子中,只有当前版本的驱动,所以蓝屏后在调试器中可以正确加载驱动对应的源码。在实际的商业环境中,发布出去的驱动程序有可能是几天前的代码,甚至是几周前的代码,那怎么将源码和驱动程序对应上呢?笔者在前面已经做了铺垫:我们的SVN服务器在提交时,会产生一个修订号,每次提交的修订号都在原来的基础上加一。笔者的做法是,将版本信息中的最后一部分改为修订号。为了达到此目的,笔者将为测试工程TestDrv添加一个资源文件,并放入版本信息,如图2-15所示。

图2-15 测试驱动程序的版本号示例

注意图2-15中带下画线的“6”,它就是我们要记录的SVN的修订号。当提交新的代码后,这个数值要及时修改。在商业环境中,所有的编译工作都是在专门的服务器上完成的,通常叫做“持续构建系统”,比如cruisecontrol.net软件,就是免费好用的持续构建系统软件。通常我们会使用程序取出当前源码的修订号,然后将它写入版本资源文件中,或者共用的版本号定义文件中。限于篇幅,在这里不讨论怎么自动生成版本号,有兴趣的读者可以访问驱动开发网技术社区与我们探讨。为了简化起见,这里使用手工方式修改。

当驱动程序不幸出现蓝屏故障时,只需要在打开Windbg软件之前,在驱动程序文件上单击右键,选择“属性”,查看一下版本号的最后一部分的数字,然后通过SVN客户端的仓库管理功能将对应修订号的源代码提取到本地目录中即可。

选择“TortoiseSVN”→“Show log”(见图2-16),将看到如图2-17所示的界面,其中显示了前几条提交日志。如果提交次数比较多,为了看到更多的日志内容,可以单击左下角的“Show All”按钮。

图2-16 选择查看SVN提交日志选项

图2-17 SVN日志显示界面

找到需要提取的版本,在修订号上单击右键,选择“Checkout”,如图2-18所示。可能需要指定源码的保存目录,请指定一个不同于原来代码的目录即可。

图2-18 选择“Checkout”提取指定版本的代码

下面设置一下符号路径,在操作系统的环境变量中添加“_NT_SYMBOL_PATH”,值为:srv*E:\symcache*http://msdl.microsoft.com/download/symbols;\\10.0.1.191\symbols。其中,E:\symcache为我们前面创建的微软符号cache目录;后面的内容是我们创建的符号服务器路径。右键单击“我的电脑”,选择“属性”选项,弹出“系统属性”对话框;进入“高级”选项卡页面,单击“环境变量”按钮,弹出“环境变量”对话框;单击“新建”按钮,打开“新建系统变量”对话框,按图2-19所示进行配置。

图2-19 符号服务器环境变量配置界面

既然万事俱备,那我们就来人工蓝屏一次,测试一下符号服务器。为了证实是我们的符号服务器在起作用,将驱动程序编译并安装后,笔者会删除本机编译生成的符号文件目录(不是符号服务器目录E:\symbols)。如图2-20所示,将系统的故障转储方式设置为“核心内存转储”。

图2-20 设置故障转储方式

修改驱动程序,让它产生一个会导致蓝屏的错误,用osrloader程序加载它,如图2-21所示。

图2-21 加载驱动

在第一个方框中设置好驱动路径,在第二个方框中设置好启动方式,然后顺序单击下方的“Register Service”和“Start Service”按钮,我们将看到蓝屏,如图2-22所示。

图2-22 蓝屏转储

在图2-22中,方框处显示的是转储进度,等它到100%,虚拟机就会自动重启,重启后,在WINDOWS目录下有一个名称为“MEMORY.DMP”的内存转储文件,我们要分析的就是它。如果虚拟机不能自动重启,请关掉虚拟机电源后重新开启,具体方法见图2-23,先单击第一个方框中的按钮,再单击第二个方框中的按钮,就可以重新启动VMWare了。

图2-23 重新开启VMWare

重新启动后,我们打开Windbg程序,通过菜单“File”→“Open Crash Dump”打开Dump文件(见图2-24),此文件是系统目录(%systemroot%)下的MEMORY.DMP文件。

图2-24 选择“File”→“Open Crash Dump”选项打开Dump文件

在打开的文件选择对话框中,定位到MEMORY.DMP文件,Windbg将对它进行加载。在打开时,Windbg会询问是否保存工作空间配置,单击“Yes”按钮即可,如图2-25所示。

图2-25 Windbg询问对话框

此时,Windbg会加载Crash Dump文件,出现如图2-26所示的界面,你需要在命令窗口输入“!analyze –v”命令或者直接单击视图中的链接运行此命令。

图2-26 开始分析Crash Dump

输入命令或单击链接后,系统会加载符号表。由于是第一次使用Windbg,系统会从微软符号服务器中下载相关的符号文件,并保存到前面我们设置的符号cache目录(F:\symcache)中。如果此时打开F:\symcache目录,会看到已经下载了很多符号文件,如图2-27所示。

图2-27 symcache目录

至此,符号已经下载完毕了,我们看一下Windbg的界面显示,如图2-28和图2-29所示。

图2-28 Dump分析界面(1)

图2-29 Dump分析界面(2)

大家是不是很容易看到错误所在的位置了?界面中已经显示了错误所在的行,甚至还显示了有错误的代码附近几行的源码。

在图2-30中,方框部分就是错误代码。对于RtlInitUnicodeString初始化的Unicode字符串,不能用RtlFreeUnicodeString释放。从代码中可以看出,字符串是栈变量,但是RtlFreeUnicodeString是一个堆释放函数,所以会出现错误。

图2-30 错误源码

下面我们试着看一下当时的内存变量。打开菜单“View”→“Call Stack”,就可以看到当时的调用栈了,如图2-31所示。

图2-31 调用栈

在图2-31中,双击方框所在的行,将使Windbg打开源码并定位到对应的源码行。在通常情况下,Windbg会定位于下一行语句,比如上面的源码,Windbg定位于它的下一行代码处。如果不确定有错误的是哪行,通过一个方法可以正确定位。请看图2-31中方框上面的一行,调用的语句是nt!RtlFreeUnicodeString,再结合双击方框所在行看到的代码,很自然地就知道目标代码是RtlFreeUnicodeString(&ustrTestStr)。可以将鼠标移到对应的代码上停留一会儿,会弹出黄色的提示框,显示当前变量的值,是不是很方便?限于技术原因,我们无法抓取提示框,图2-32是用手机拍摄的画面。

图2-32 Windbg中的源码行

其他具体的Windbg调试方法,请参看本书的调试相关章节,也可以参看Windbg联机帮助。这里笔者推荐两本书:《软件调试》和《Microsoft.NET和Windows应用程序调试》。

2.2 64位驱动开发技术

2009年7月,Windows 7发布。这一次发布带来的不光是一个新的操作系统,而且也是一个新时代的开始。从Windows 7开始,伴随着内存的降价,计算机中超过4GB内存的配置成为主流,在这种情况下,需要安装Windows 7 64位版来支持更大的内存。在一些设计和制作企业中,大型的设计软件已经要求使用64位平台来支持大内存以提高大文件的处理速度。我们可能已经习惯了平时的编程方法,但是有可能没有意识到这种陈旧的习惯会导致64位环境下出现问题,比如编译无法通过,编译通过后运行结果不对,等等。

2.2.1 64位驱动编写技术

1.64位操作系统目录部署

为了保持应用程序的兼容性,以及减少应用程序从Win32移植到64位系统的开销,系统的目录名保持不变。因此,\WINDOWS\system32目录中存储的是本地64位程序。因为WOW64要钩住所有的系统调用,所以它转换了所有路径相关的APIs,并将路径名\WINDOWS\system32替换为\WINDOWS\system32\SysWOW64。WOW64还将\WINDOWS\system32\IME重定位到\WINDOWS\system32\IME (x86),以帮助32位应用程序兼容于64位系统。另外,32位程序被装载到\Program Files (x86)目录下,而64位程序则被装载到正常的\Program Files目录下。

少数目录因为兼容性要求的原因而没有被重定位,使得32位应用程序访问到的是原始目录位置。这些目录包括:

● %windir%\system32\drivers\etc

● %windir%\system32\spool

● %windir%\system32\catroot2

● %windir%\system32\logfiles

最后,WOW64提供了一种禁用文件重定位的机制,线程可以调用函数Wow64EnableWow64FsRedirection。该机制在Windows Server 2003及以后的版本中有效。

2.程序目录

● 32位:Program Files (x86),所有的32位程序

● 64位:Program Files

3.系统目录

● 64位:system32

● 32位:SysWOW64

在32位程序运行时,如果不特意处理,访问system32目录时,会被WOW64层自动重定向到SysWOW64目录。

4.数据类型

在64位环境下,所有的指针、句柄都是64位,所以为了适应64位环境,Windows提供了一些指针类型的数据类型,以及其他兼容64位环境的类型,如表2-1和表2-2所示。

表2-1 固定精度的数据类型

此外,当需要数据类型的精度随着处理器位数大小变化时,请使用指针精度数据类型。这些类型又称为“多态”数据类型。这些类型通常以“_PTR”后缀结尾,如表2-2所示。

表2-2 指针精度的数据类型

注意:在需要使用数据保存指针或句柄时,请尽量使用同类型变量,如果因为程序原因不能使用同类型变量时,请使用指针精度变量。

2.2.2 32位应用程序与64位驱动混合模式

编写驱动程序时,除了要考虑移植驱动本身的问题外,还有一个问题需要解决:当移植完驱动后,上层的应用程序怎么办?可能有读者说“把它改为64位”。这种想法没错,但是有时不可行。比如调用了一个没有源码的第三方库,此时将程序移植到64位是不现实的。但是需要支持64位操作系统,怎么办?我们需要解决32位应用程序与64位驱动配合工作的问题。

1.驱动要支持32位IOCTL

某些IOCTL可能包含有指针的结构,所以要特别小心地区别对待它,必须根据被调用者解析结构或者输出结构。

有3种办法可以解决这个问题:

(1)尽量避免使用IOCTL传递包含有指针的结构。

(2)通过API IoIs32bitProcess()来判断上层调用者的程序类型。

(3)在64位程序中采用新的IOCTL命令。

举例如下:

(1)IOCTL structure in header file

      typedef struct _IOCTL_PARAMETERS {
      PVOID Addr;
      SIZE_T Length;
      HANDLE Handle;
      } IOCTL_PARAMETERS, *PIOCTL_PARAMETERS;

(2)32-bit IOCTL structure

      //
      // This structure is defined
      // inside the driver source code
      //
      typedef struct _IOCTL_PARAMETERS_32 {
      VOID*POINTER_32 Addr;
      INT32 Length;
      VOID*POINTER_32 Handle;
      } IOCTL_PARAMETERS_32, *PIOCTL_PARAMETERS_32;

(3)32-Bit and 64-Bit IOCTL

      #ifdef _WIN64
      case IOCTL_REGISTER:
      if (IoIs32bitProcess(Irp)) {
      /* If this is a 32 bit process */
      params32 = (PIOCTL_PARAMETERS_32)(Irp>AssociatedIrp.SystemBuffer);
      if(irpSp->Parameters.DeviceIoControl.InputBufferLength < sizeof(IOCTL_PARAMETERS_32)) {
      status = STATUS_INVALID_PARAMETER;
      } else {
      LocalParam.Addr = params32->Addr;
      LocalParam.Handle = params32->Handle;
      LocalParam.Length = params32->Length;
      /* Handle the ioctl here */
      status = STATUS_SUCCESS;
      Irp->IoStatus.Information = sizeof(IOCTL_PARAMETERS);
      }
      } else { /* 64bit process IOCTL */
      } else { /* 64bit process IOCTL */
      params = (PIOCTL_PARAMETERS)
      (Irp->AssociatedIrp.SystemBuffer);
      if (irpSp->Parameters.DeviceIoControl.InputBufferLength
      < sizeof(IOCTL_PARAMETERS)) {
      status = STATUS_INVALID_PARAMETER;
      } else {
      RtlCopyMemory(&LocalParam, params,
      sizeof(IOCTL_PARAMETERS));
      /* Handle the ioctl here */
      status = STATUS_SUCCESS;
      }
      Irp->IoStatus.Information = sizeof(IOCTL_PARAMETERS);
      }
      break;

2.使用固定精度的数据类型代替指针精度的数据类型

为了解决这种因为数据类型导致的32位和64位数据长度不一致的问题,我们可以采用固定精度的数据类型来代替指针精度的数据类型处理数据。

例如,下面的结构:

      struct  foo
      {
      Void * P;
      HANDLE h;
      }

上面的指针和句柄类型都是指针精度的数据类型,根据32位或64位指令的情况进行变化。无法让一个驱动兼容所有的应用程序,为了让它可以兼容64位和32位应用程序,我们可以改为固定精度的数据类型:

      struct  foo
          {
          ULONG64 P
          ULONG64 h;
          }

这样,所有的数据长度都固定为64位,就不会出现由于指针精度的长度变化而引起的故障。当然,这样处理会牺牲一些程序效率。

3.避免固定精度的数据类型对不齐

前面说了,可以用固定精度的数据类型代替指针精度的数据类型来解决32位和64位驱动兼容问题,不过在跨CPU类型时将会出现其他问题。

在32位和64位混合编程中,会发生数据长度一致,但是对齐长度不一样的情况。不是所有的IOCTL/FSCTL缓冲对不齐的问题都可以通过修改指针精度的数据类型为固定精度的数据类型而得到解决。这意味着内核模式驱动的IOCTL和FSCTL请求中传递的固定精度的数据类型或者指针精度的数据类型需要进行转换(thunked),所谓thunked就是在不同长度或类型之间的转换。前面我们提到的32位和64位之间的数据传递方式就是一种转换,因为长度不一致,只能赋值而不能直接进行内存拷贝。

哪些数据类型会被影响?

● 结构类型的固定精度数据会被影响。

这是因为在不同的平台上结构的对齐长度不一样。例如,__int64、LARGE_INTEGER以及KFLOATING_SAVE在x86平台上是4字节对齐的(包括x86-64 CPU和64位Windows环境),但是在安腾平台上是8字节对齐的。

需要知道指定数据类型在特定平台上的对齐长度,可以使用TYPE_ALIGNMENT宏。

怎么解决这个问题呢?

下面的例子中使用METHOD_NEITHER类型的IOCTL请求,因此Irp->UserBuffer指针是直接从用户模式的应用程序传递到内核驱动的。IOCTL和FSCTL传递的指针无法保证有效性,有可能传递下来的地址当前无效,因此需要调用ProbeForRead或ProbeForWrite来检查缓冲区的有效性。假如32位的应用程序用Irp->UserBuffer传递一个有效的缓冲区地址,p->DeviceTime指向一个LARGE_INTEGER结构,它是4字节对齐的(不是说它只有4字节,而是按4字节对齐)。ProbeForRead通过Alignment参数传入的值检查对齐情况,Alignment参数的值其实就是TYPE_ALIGNMENT (LARGE_INTEGER) 的结果。在x86平台上,这个宏返回4(字节);在安腾平台上,它会返回8,从而导致ProbeForRead出错,返回状态码STATUS_DATATYPE_MISALIGNMENT。如果在代码中去掉对ProbeForRead的调用并不能解决问题,只会导致硬件诊断错误,就是会出现蓝屏死机。

      typedef struct _IOCTL_PARAMETERS2 {
          LARGE_INTEGER DeviceTime;
      } IOCTL_PARAMETERS2, *PIOCTL_PARAMETERS2;
      #define SETTIME_FUNCTION 1
      #define IOCTL_SETTIME CTL_CODE(FILE_DEVICE_UNKNOWN, \
                    SETTIME_FUNCTION, METHOD_NEITHER, FILE_ANY_ACCESS)
      ...
      case IOCTL_SETTIME:
          PIOCTL_PARAMETERS2 p = (PIOCTL_PARAMETERS2)Irp->UserBuffer;
          try {
              if (Irp->RequestorMode != KernelMode) {
                    ProbeForRead ( p->DeviceTime,
                                sizeof( LARGE_INTEGER ),
                                TYPE_ALIGNMENT( LARGE_INTEGER ));
          }
          status = DoSomeWork(p->DeviceTime);
      } except( EXCEPTION_EXECUTE_HANDLER ) {

下面通过简单的示例讲述怎么解决上面描述的问题。

方案一:拷贝缓冲区

避免缓冲区对不齐的安全方法是在访问缓冲区内容之前进行拷贝操作,下面是例子。

      case IOCTL_SETTIME: {
          PIOCTL_PARAMETERS2 p = (PIOCTL_PARAMETERS2)Irp->UserBuffer;
      #if _WIN64
          IOCTL_PARAMETERS2 LocalParams2;
          RtlCopyMemory(&LocalParams2, p, sizeof(IOCTL_PARAMETERS2));
          p = &LocalParams2;
      #endif
          status = DoSomeWork(p->DeviceTime);
          break;
      }

这个方案可以进行一些性能优化,首先检查这个缓冲区内容是否对齐,如果对齐就直接用,否则驱动程序进行缓冲区拷贝。

      case IOCTL_SETTIME: {
          PIOCTL_PARAMETERS2 p = (PIOCTL_PARAMETERS2)Irp->UserBuffer;
      #if _WIN64
          IOCTL_PARAMETERS2 LocalParams2;
          if ( (ULONG_PTR)p & (TYPE_ALIGNMENT(IOCTL_PARAMETERS2)-1)) {
                  // The buffer contents are not correctly aligned for this
                  // platform, so copy them into a properly aligned local
                  // buffer.
                  RtlCopyMemory(&LocalParams2, p, sizeof(IOCTL_PARAMETERS2));
                  p = &LocalParams2;
            }
      #endif
            status = DoSomeWork(p->DeviceTime);
            break;
      }

方案二:使用UNALIGNED宏

UNALIGNED宏告诉C编译器生成可以无故障存取的DeviceTime域的代码。注意:在安腾平台上使用这个宏,会导致驱动程序体积变大,速度变慢。

      typedef struct _IOCTL_PARAMETERS2 {
            LARGE_INTEGER DeviceTime;
      } IOCTL_PARAMETERS2;
      typedef IOCTL_PARAMETERS2 UNALIGNED *PIOCTL_PARAMETERS2;

● 指针类型一样会被影响。

前面描述的对不齐的问题一样会发生在缓冲型的I/O请求中。在下面的例子中,IOCTL缓冲区包含一个指向LARGE_INTEGER结构的指针。

      typedef struct _IOCTL_PARAMETERS3 {
            LARGE_INTEGER *pDeviceCount;
      } IOCTL_PARAMETERS3, *PIOCTL_PARAMETERS3;0
      #define COUNT_FUNCTION 1
      #define IOCTL_GETCOUNT CTL_CODE(FILE_DEVICE_UNKNOWN, \
                    COUNT_FUNCTION, METHOD_BUFFERED, FILE_ANY_ACCESS)

就像前面描述的基于METHOD_NEITHER的IOCTL和FSCTL请求缓冲区一样,嵌入到缓冲型的I/O请求中的指针也是直接从应用层程序中传递到内核驱动中的。在这些指针上没有进行校验,所以在可以安全地访问这些内嵌的指针之前,需要在一个try/catch块中调用ProbeForRead或ProbeForWrite进行缓冲区指针校验。

在前面的例子中,假设32位应用程序传递一个有效的PdeviceCount,pDeviceCount指向的LARGE_INTEGER结构已经按4字节对齐了。ProbeForRead或ProbeForWrite函数通过参数Alignment传入的值进行对齐检查,在这种情况下,它的值是TYPE_ALIGNMENT (LARGE_INTEGER)。在x86平台上(包括x86-64平台)它返回4;在安腾平台上,它返回8,导致ProbeForRead或ProbeForWrite函数发生STATUS_DATATYPE_MISALIGNMENT异常。

解决这个问题的办法是进行适当的以对齐为目的LARGE_INTEGER结构的缓冲区拷贝,或者对下面的结构使用UNALIGNED宏。

        typedef struct _IOCTL_PARAMETERS3 {
            LARGE_INTEGER UNALIGNED *pDeviceCount;
        } IOCTL_PARAMETERS3, *PIOCTL_PARAMETERS3;

2.3 驱动程序的发布与测试

经过千辛万苦,解决了上面的各种问题后,我们终于完成了驱动程序开发,接下来的任务就是测试和发布了。测试最简单的办法就是配合应用程序运行,但是这是不够的,我们有时会犯一些细节性的错误,这在简单的测试环境下是无法发现的。在x86-64平台下,还需要考虑一个问题:数字签名和WHQL。

2.3.1 驱动程序签名

从Windows Vista开始,Windows要求应用程序和驱动程序需要签名以提高系统安全性。对于驱动程序来讲,从Windows Vista开始的64位平台上,驱动程序必须签名。普通应用程序的签名只需要可信根证书即可,换句话说,只要将签名证书的根安装到信任根证书区域即可。在Windows的命令行状态下,运行certmgr.msc,即可以看到各种证书,包括我们需要查看的根证书,如图2-33所示。

图2-33 受信任的根证书

注意:右侧方框中所显示的内容是我们安装的证书,然后再查看一下由它签发的用户证书,如图2-34所示。

图2-34 签名用个人证书

所有签名用的最终证书都是个人证书,即使是由权威机构签发给公司用户的证书也是一样。图2-34中右侧的第二个方框中的证书,是由VeriSign签发的Class 3证书,同样处于个人证书目录下。

驱动程序的签名,需要使用由VeriSign和GlobalSign签发的证书,其他机构签发的证书无效。原因是这两个机构是Windows根信任的,微软为这两个机构签发了根证书的信任证书,根据PKI机制的传递性,由这两个信任根签发的最终用户证书将受到微软的信任。这种信任是严格的,不会因为用户的操作而改变,即使将自己生成的根证书安装到受信任的根证书区域,也无济于事。讨论了上面这些内容,笔者想说明的是,驱动签名证书必须向上述两个机构或者他们的代理机构购买。具体的代理机构信息可以从驱动开发网上查找。

为了清楚起见,笔者在这里简单说明一下签名的原理。权威机构会给用户签发一份最终的签名证书,证书包括如下几部分:

(1)用户的公私钥对,基于RSA算法。

(2)用户的证书信息,包括用户名称、类型等。

(3)上一级CA对本证书的签名信息。

下面说一下简化的签名过程。将要签名的文件通过Hash算法进行信息摘要,比如MD5或者SHA1等算法。到目前为止,MD5算法因为中国王小云教授的碰撞研究,已经停止使用。对整个文件的Hash结果,再通过RSA的私钥进行加密,并将加密后的结果和相关信息写入文件尾部。写入的信息包括:证书信息、加密后的结果等。

在验证时,需要取出尾部的证书相关信息,通过证书自带的公钥解开签名信息,查对原始文件生成的Hash值是否跟签名相符,如果相符,即认为正确签名了。看到这里,有的读者可能会问:岂不是我也可以伪造一个签名信息了吗?其实在签证时,先要验证文件中用户证书是否有效,然后再用用户证书中的公钥解开签名结果进行比较。用户证书的验证过程如下:

(1)使用用户证书中自带的相关信息验证证书完整性,确保证书由上一级CA机构签发,并且没有经过修改。

(2)验证上一级CA证书由更上一级CA证书签名,验证过程同文件签名验证过程。

(3)直到发现本级CA证书由自己签发为止。这就是所谓的根CA证书的自验证。从图2-35中可以看到,自签名证书的“颁发给”与“颁发者”是相同的。

图2-35 根证书的自签名

通常,应用程序或网站签名证书的验证过程到此为止,只要保证自验证的CA证书存在于受信任的根证书存放区域即可。对于驱动程序签名证书来讲,Windows还需要验证这个CA证书是不是微软信任的,这就是通常所说的交叉签名。目前公开发售的可交叉签名证书仅仅有上面所述的两种。由于历史原因,目前兼容性最好的是VeriSign的签名证书,特别是对于需要进行WHQL认证的驱动程序来讲,全球只有VeriSign证书可用。

对于以上的介绍,我们总结一下。对于驱动程序签名来讲,可以使用的证书只有两种:

(1)VeriSign代码签名证书。

(2)GlobalSign代码签名证书。

如果需要进行WHQL认证(Windows硬件设备质量实验室(认证)),能够使用的签名证书只有一种:VeriSign代码签名证书。

实际使用证书进行签名时,我们可以使用WDK自带的signtools.exe工具。具体的命令行如下:

      signtool.exe sign /v /ac MSCV-VSClass3.cer /s "my" /n "MyCertName"  /t
  http://timestamp.verisign.com/scripts/timestamp.dll %1

各参数说明如下:

● MSCV-VSClass3.cer:VeriSign根证书的签名证书,由微软提供。

● “MyCertName”是使用的签名证书的名称,即证书的CommonName,如图2-36中画线的部分所示。

图2-36 证书CommonName(图中画线部分)

● %1为要签名的驱动程序的全路径,如果有空格,请在前后加上引号。

如果需要签名的程序带Inf文件,那么在使用上比较复杂,具体可以查看WDK自带的Inf2Cat工具的使用帮助。在这里,推荐使用驱动开发网提供的签名工具,界面如图2-37所示。我们可以使用它的纯代码签名功能签只有SYS文件的驱动程序,也可以使用它的Inf文件签名功能签带Inf的驱动程序,它会自动修正Inf文件语法,自动生成CAT文件并签名。具体文件可以从本书配套的软件包中寻找。在使用之前,先在参数设置页面中设置好具体的参数,包括驱动开发网用户名、用户密码、要使用的签名证书,并正确选择好时间戳服务器和需要处理的扩展名。

图2-37 驱动开发网提供的签名工具界面

对于驱动程序的数字签名,上面讲了几个方面的内容,包括签名原理、证书选择、签名工具的使用与选择。如果读者还有不清楚的地方,请访问本书的技术支持网站寻求技术支持。

如果仅在测试模式下使用,请使用驱动开发网签名工具自带的测试证书签名称的驱动程序,并在操作系统启动时按F8键,选择测试模式,此时操作系统不再检查根证书的信任关系。不过这种模式仅限于在驱动程序测试阶段使用。注意:请不要在未经用户允许的情况下修改用户机器的工作模式。签名是为了操作系统和用户数据的安全,如果随意修改用户机器配置,使其工作于测试模式,将是非常危险的行为。

2.3.2 驱动程序测试

驱动程序开发完成后,我们需要进行一些功能性测试,以保证它工作于最佳状态。通常需要进行以下几方面的测试。

1.内存使用测试

内存使用测试的目的是查看驱动程序有没有发生内存分配失败的情况。我们可以使用操作系统自带的Verifier工具进行测试,启动方式是直接在操作系统的命令行状态下运行Verifier,也可以从system32目录下双击verifier.exe启动它。启动后,可以进行一系列的设置,如图2-38至图2-41所示。

图2-38 Verifier界面

图2-39 内存测试时选择“标准设置”

图2-40 手动选择要测试的驱动

图2-41 选择要测试的驱动后单击“完成”按钮

此时,只需要重新启动计算机,即可看到测试情况。重新启动计算机后,再次运行Verifier程序,如图2-42所示,选择“显示当前经过验证的驱动程序信息”,单击“下一步”按钮;列出当前要显示的测试类型,选择如图2-43所示,单击“下一步”按钮,即可看到如图2-44所示的状态。

图2-42 选择“显示当前经过验证的驱动程序信息”

图2-43 列出当前要显示的测试类型

图2-44 显示测试状态

打开内存测试程序,可以提高故障查找能力。比如,由于驱动程序内存分配错误,将空的指针传递到下一层的驱动程序中而导致下一层的驱动程序蓝屏,此时很难找出真正的原因,通常都会认为是下一层驱动程序有bug导致蓝屏,殊不知这是因为驱动程序错误而将错误扩大到下一层驱动程序中。此时,只要打开Verifier程序,在重启后这个bug将会出现在你的驱动程序中,从而让你能快速定位bug。

2.功能测试

功能测试通常使用黑盒测试技术。将驱动程序和应用程序连接好,让它在实际工作环境下运行,通过应用程序的反应来验证驱动程序功能是否符合设计文档的要求。在这种情况下没有其他好的办法,唯一可以借助的是一些自动化测试工具,如WinRunner等,用来自动操作应用程序,模拟实际工作环境。

3.休眠测试

将驱动程序正确安装到计算机后,打开休眠模式,让计算机在休眠后再被唤醒,确认驱动程序在休眠状态下可以正常工作。这对于硬件驱动程序来说,是验证电源管理功能的必要步骤。通常一次测试不足以发现故障现象,可以重复多次进行测试。

4.长时间运行测试

如果在常规的测试状态下无法发现问题,并不能说明驱动程序已经非常稳定了。此时,我们需要配合应用程序,让驱动程序在实际工作环境或者接近于实际工作环境的情况下长时间运行,确保驱动程序的功能正常、稳定。

2.3.3 WHQL

对于硬件驱动程序或者会产生Miniport设备的驱动程序来说,如果希望驱动程序更加稳定,符合Windows的严格要求,可以申请WHQL测试。

Windows硬件设备质量实验室 (WHQL) 是创建并管理用于测试系统和外围设备与微软Windows操作系统的硬件兼容性测试(HCT)工具。制造商用HCT来检测他们的硬件产品,以便获取使用“Designed for Windows”徽标的资格,并将其产品列入硬件兼容性列表(HCL)和Windows目录。微软提供Windows徽标计划来帮助客户识别那些能满足平台特性的基本定义,并保证终端用户拥有高品质的Windows操作体验和外围设备体验。

微软强烈推荐他的客户仅使用获得“Designed for Windows”徽标的设备驱动。这个测试保证驱动程序可以工作在微软的操作系统上,不含病毒并且不干扰系统上的其他设备。每个授权驱动都是含有.cat文件的一个数字签名,表明这个驱动是被检验过的并且没有被改动过。在Windows XP以及更高级的操作系统下,驱动签名机制默认设置为启用状态,试图安装一个未经签署的驱动程序的用户会收到一个警告信息,被告知驱动程序签名的重要性。

下面是简要的WHQL申请过程。

(1)从微软网站下载Winlogo Kit (WLK)工具包,可以从http://connect.microsoft.com下载。

(2)准备一台完全通过WHQL认证的计算机作为测试平台。如果使用兼容计算机硬件,有可能由于计算机本身所带硬件出现问题而导致测试失败。通常品牌计算机都符合要求,请注意查看计算机前面的“Designed for Windows”徽标。

(3)根据Winlogo Kit帮助文档的要求,对自己的驱动程序进行测试。

(4)访问微软网站,申请WHQL账号。

(5)提交测试结果信息。

(6)从微软网站取回测试结果。

更详细的进一步指导文档,请访问微软技术支持网站或者驱动开发网技术社区获得。

2.4 小结

本章我们讨论了商业驱动开发环境下的技术,包括通过版本控制系统和符号服务器快速定位故障位置,64位环境下的驱动程序开发技术细节,如何正确处理32位应用程序与64位驱动程序之间的通信,以及驱动程序数字签名、测试、WHQL认证技术。这些技术都是开发商业驱动程序所必需的,掌握好这些技术对于提高我们的开发技术、增长开发经验非常有益。