第三章 Windows驱动编译、环境配置、安装及调试
调用约定
调用约定指函数在被调用时会按照不同的规则翻译成不同的汇编代码。以堆栈为例进行解释,当调用一个函数的时候,首先将返回地址压入栈中,紧接着会将函数的参数依次压入堆栈,当函数返回时会以相反的顺序依次退出堆栈,因此函数在被调用前和调用后的堆栈保持平衡。
不同的调用约定,会指明不同参数的入栈顺序,还会支出不同的清理堆栈的方法。C/C++四中不同的调用约定区编译函数:C语言调用约定 函数由__cdecl修饰,标准调用约定,函数由__stdcall修饰;快速调用约定,函数由__fastcall修饰,C++类成员函数的嗲用约定,函数由thiscall修饰,不同的调用约定编译后,会产生不同的汇编代码。
一般程序中很少见到关键字指定函数的调用约定,编译器会选择默认的调用约定,在VC编译器中,默认使用C语言的调用约定,而在windows驱动程序的编写中需要使用标准调用约定,尤其入口函数。系统会寻找_DriverEntry@8作为驱动程序的入口点,而C语言会将DriverEntry编译成_DriverEntry。所以配置环境是需要修改这一点。
函数的导出名
同一函数,用C语言编译器和C++编译器编译出来的符号名是不一样的。 因为链接的时候链接器不知道源程序的函数名,而只会去目标文件中寻找相应的函数符号表。VC或DDK提供的编译器cl.exe,既可以编译C语言,又可以编译C++,默认情况下会根据源文件的扩展名来判断使用哪种方式编译。当是.cpp时,采用c++方式编译,当文件扩展名是.c时候,编译器会用C编译器方式编译。例如,同样使用标准约定的函数
void __stdcall Foo(int a,int b)
在C++编译器中会编译成 ?Foo@@YGXHH@Z,而在C编译器中编译成符号_FOO@8。C++ 复杂的函数符号名是为了支持函数的重载功能。Windows驱动程序的入口函数规定为_DriverEntry@8,因此用C++编译的时候会导致符号链接错误。解决办法是采用extern “C”修饰符。例如
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(
IN PDRIVER_OBJECT PDriverObject,
IN PUNICODE_STRING pRegistrtPath)
另外C++程序中需要包含ntddk.h或者wdm.hde 时候,可能会出现错误错误,原因就是按照C++的编译方式使得找不到符号里链接。因此需要
#ifdef __cplusplus
extern "C"
{
endif
#include <NTDDK.h>
#ifdef __cplusplus
}
#endif
注:编译是将源文件编程目标文件的过程,链接是将目标文件变成最终二进制映像的过程。
运行时函数调用
windows驱动程序与普通的Win32应用程序一样都是用C语言或者C++编写的,但是很多语言技巧要慎重使用。在windows驱动程序中,不能使用编译器运行时函数(Run Time Function),甚至C语言的malloc函数和C++语言默认new操作符都不能使用(如果要使用必须重载)。
编译器厂商一般在发布编译器的时候会同时将其运行时函数一起发布给用户。运行时函数是一个程序运行所必不可少的函数,他编译器提供,针对不同的操作系统实现也有所不同,但是接口基本上是标准的。例如,malloc函数是典型的运行时函数,所有编译器厂商都必须提供这个函数,不同厂商在不同操作系统上,实现方法是不同的。
驱动程序里为什么不能使用编译器提供的运行时函数?原因很简单,因为大部分运行时函数是通过Win32 API实现的,而API是针对Ring3(用户模式)的程序的,Windows驱动程序是运行在Ring0(内核模式)。内核模式下程序无法调用用户模式提供的API函数的。
windows 为用户提供了内核态的运行时函数,他可以代替应用程序的运行时函数。在内核态的运行时函数一般形如Rtlxxx。有一些运行时函数如strcpy等他们的实现不依赖API读者完全可以将其用在驱动编写中。不过读者并不清楚这个函数的实现,最好用DDK提供的运行时函数。建议在驱动程序中,尽量全部使用DDK提供的运行时函数。
编译版本
DDK编译环境为用户提供两种编译版本,Checked版本和Free版本,Check版本和Free版本的关系就类似于VC的Debug版本和Release版本。Free版本是最终发行版本,因此要进行优化并删除所有调试符号。编译环境会将编译器的全部优化参数打开。因此此版本编译出来的驱动体积最小,运行速度最快,但无法进行源码级调试。Checked版本与Free版本相反,是一个未优化的调试版本,里面含有大量的调试符号,来对应源码的具体位置。可以使用Softice或者WinDbg进行调试。
用VC编译驱动程序
VC提供了强大的集成开发环境,其编译驱动程序同样也是调用编译器cl.exe和链接程序link.exe。
VC 创建工程一般都通过Wizard向导创建,但是VC并没有提供创建一个驱动程序的Wizard向导。需要建立一个空的VC工程。并进行一系列的修改。
1、修改编译选项:编译选项集中在“C/C++”选项卡中的“Project Options”内容为:
/nologo /Gz /MLd /W3 /Z7 /Od /D WIN32=100 /D _X86_=1 /D WINVER=0x500 /D DBG=1 /Fo"MyDriver_Check/" /Fd"MyDriver_Check/" /FD /c
/nologo :代表不显示编译的版权信息。
/Gz :默认函数采用标准调用(__stdcall)
/W3 : 采用第三级的警告模式。
/WX: 将警告信息变成错误信息,最大程度保证代码的可靠性。
/Z7 : 用Z7模式产生调试信息。VC默认的 Program Database for "Edit & Continue",这个和link的/driver选项冲突。
/Od: 关闭调试模式。驱动程序不需要像Win32程序那样用VC调试器调试,而需要用内核调试器调试。
/D WIN32=100 /D _X86_=1 /D WINVER=0x500 /D DBG=1 : 定义一些宏,这些是必须的。
/Fo"MyDriver_Check/" : 设置中间的生成的目标代码的路径。
/Fo"MyDriver_Check/" : 设置pdb文件的目录位置,pdb文件中包含了大量的符号,这是调试驱动时候所必须的。
/FD : 生成文件依赖。
/c : 只进行编译,而不链接。
2、修改链接选项 ; 在编译选项集中在“Link”选项卡中的“Project Options”里,在Project Option中填写:
ntoskrnl.lib /nologo /base:"0x1000" /stack:0x400000,0x1000 /entry:"DriverEntry" /subsystem:console /incremental:no /pdb:"MyDriver_Check/HelloDDK.pdb" /debug /machine:I386 /nodefaultlib /out:"MyDriver_Check/HelloDDK.sys" /pdbtype:sept /subsystem:native /driver /SECTION:INIT.D /RELEASE /IGNORE:4078
ntoskrnl.lib NT式 驱动程序需要链接此库。如果是WDM驱动程序,则需要链接wdm.lib 。
/nologo 链接时不显示版权信息
/base:"0x1000" 加载驱动时,设定加载在虚拟内存中的位置。
/stack:0x400000,0x1000 设定函数使用堆栈的大小。
/entry:"DriverEntry" 入口函数的地址,此函数必须是符号标准函数调用 的。
/subsystem:console 设置子系统
/incremental:no 非递增式的链接。
/pdb:"MyDriver_Check/HelloDDK.pdb" 设置pdb文件名。
/debug 以debug方式链接
/machine:I386 产生代码是386兼容平台的
/nodefaultlib 不使用默认的库。
/out:"MyDriver_Check/HelloDDK.sys" 输出二进制代码的名称。
/pdbtype:sept 设置pdb文件类型。
/subsystem:native 子系统是内核系统。
/driver 编译驱动。
/SECTION:INIT,D 将INIT的段设置为可抛弃的。
/RELEASE /IGNORE:4078 忽略4078号警告。
3、其他修改:在修改完编译和链接参数后,还可以根据需要设定一些特殊的设置。例如可以将生成的sys驱动程序文件复制到\system32\drivers目录中,在Post-build step中进行设定:copy $(TargetPath) $(WINDIR)\system32\drivers
查看调试信息
驱动程序的调试主要有两个途径:其一是在关键地方打印出调试信息,也就是俗称的打log,其二是调用内核调试工具,诸如Softice或者WinDbg等进行内核调试。
打印调试语句
在驱动编写中,里面打印调试信息都是调用语句KdPrint,其实他不是一个函数,而是一个宏。在Checked版本中,他将参数传给内核函数DbPrint,此函数会将调试信息记录下来。而在Free版本中,KdPrint则什么也不做。KdPrint语法十分类似于printf,但由于是宏,所以使用时需要双重括号。
// 直接打印字符串
KdPrint(("Enter Hello WDMAddDevice\n"));
//打印字符串
char *name = "hello";
KdPrint(("%s\n",name));
查看打印语句
可以利用Windbg或者DriverMonitor或者 DbgView 查看打印信息。
WDM式驱动的加载
WDM式驱动的加载需要由一个以INF为扩展名的文本文件来描述安装的过程。系统安装的时候,会根据不同的VernderID和ProductID寻找INF文件,然后根据INF文件上的指示,将驱动程序(.sys文件)和相关文件复制到系统指定的目录下,并修改注册表。同时会通知PNP管理器和I/O管理器创建设备,并运行驱动程序的入口程序DriverEntry。INF文件包含了WDM设备驱动程序需要的所有信息,这包括要创建或修改注册表信息、复制的文件等。
简单的INF文件剖析
;; Win2K DDK 文档中有详细参考
;--------- Version Section 版本区域---------------------------------------------------
[Version]
Signature="$CHICAGO$"
Provider=Zhangfan_Device
DriverVer=11/1/2007,3.0.0.3
; If device fits one of the standard classes, use the name and GUID here,如果一个设备室标准类别,使用标准类的名称和GUID
; otherwise create your own device class and GUID as this example shows.否则创建一个自定义的类别名称,并自定义它的GUID
Class=ZhangfanDevice
ClassGUID={EF2962F0-0D55-4bff-B8AA-2221EE8A79B0}
;--------- SourceDiskNames and SourceDiskFiles Section 安装磁盘节 -----------------------
; These sections identify source disks and files for installation. They are
; shown here as an example, but commented out.这个节确定安装盘和安装文件的路径,读者可以按照自己的需要修改
[SourceDisksNames]
1 = "HelloWDM",Disk1,,
[SourceDisksFiles]
HelloWDM.sys = 1,MyDriver_Check,
;--------- ClassInstall/ClassInstall32 Section -------------------------------
; Not necessary if using a standard class 如果使用标准类别设备,下面的是不需要的
; 9X Style
[ClassInstall]
Addreg=Class_AddReg
; NT Style
[ClassInstall32]
Addreg=Class_AddReg
[Class_AddReg]
HKR,,,,%DeviceClassName%
HKR,,Icon,,"-5"
;--------- DestinationDirs Section 目标文件节-------------------------------------------
[DestinationDirs]
YouMark_Files_Driver = 10,System32\Drivers
;--------- Manufacturer and Models Sections 制造厂商 ----------------------------------
[Manufacturer]
%MfgName%=Mfg0
[Mfg0]
; PCI hardware Ids use the form 在这里描述PCI的Vendor ID和Product ID
; PCI\VEN_aaaa&DEV_bbbb&SUBSYS_cccccccc&REV_dd
;改成你自己的ID
%DeviceDesc%=YouMark_DDI, PCI\VEN_9999&DEV_9999
;---------- DDInstall Sections -----------------------------------------------
; --------- Windows 9X -----------------
; Experimentation has shown that DDInstall root names greater than 19 characters
; cause problems in Windows 98 如果在DDInstall中的字符串超过19,将会导致严重的问题
[YouMark_DDI]
CopyFiles=YouMark_Files_Driver
AddReg=YouMark_9X_AddReg
[YouMark_9X_AddReg]
HKR,,DevLoader,,*ntkern
HKR,,NTMPDriver,,HelloWDM.sys
HKR, "Parameters", "BreakOnEntry", 0x00010001, 0
; --------- Windows NT -----------------
[YouMark_DDI.NT]
CopyFiles=YouMark_Files_Driver
AddReg=YouMark_NT_AddReg
[YouMark_DDI.NT.Services]
Addservice = HelloWDM, 0x00000002, YouMark_AddService
[YouMark_AddService]
DisplayName = %SvcDesc%
ServiceType = 1 ; SERVICE_KERNEL_DRIVER
StartType = 3 ; SERVICE_DEMAND_START
ErrorControl = 1 ; SERVICE_ERROR_NORMAL
ServiceBinary = %10%\System32\Drivers\HelloWDM.sys
[YouMark_NT_AddReg]
HKLM, "System\CurrentControlSet\Services\HelloWDM\Parameters",\
"BreakOnEntry", 0x00010001, 0
; --------- Files (common)文件节 -------------
[YouMark_Files_Driver]
HelloWDM.sys
;--------- Strings Section 字符串节 ---------------------------------------------------
[Strings]
ProviderName="Zhangfan."
MfgName="Zhangfan Soft"
DeviceDesc="Hello World WDM!"
DeviceClassName="Zhangfan_Device"
SvcDesc="Zhangfan"
WDM设备安装在注册表中的变化
WDM式驱动程序的安装会在三个方面修改注册表,分别是硬件子键(Hardware)、类(Class)、服务子键(Service),注册表从这三个方面的子键描述WDM设备。在安装好WDM驱动后,会根据INF的信息,在注册表中有所体现。