《Undocumented Windows 2000 Secrets》翻译 --- 第三章(1)
第三章 编写内核模式驱动程序
翻译: Kendiv
更新: Monday, February 07, 2005
在下一章中,我们会经常访问那些仅在内核模式下才有效的系统资源。大量的示例代码都被设计为内核驱动例程( Kernel-mode driver routine )。因此,需要有关开发此种软件的基本知识。因为我不能假定所有读者都有这方面的经验,我会在此简要地介绍一下内核模式驱动程序编程,不过这仅集中在如何使用驱动开发向导(在本书光盘上)。
本章还将讨论 Windows 2000 服务控制管理器( Service Control Manager , SCM )的基本知识,这包括 SCM 如何允许在运行时加载、控制和卸载驱动程序, resulting in wonderfully short change-build-test turnaround cycles 。本章的题目或许会让人有些误解,驱动一词通常与控制硬件的底层软件相关。事实上,很多内核程序员每天都在做这些事情。不过, Windows 2000 的驱动程序分层模式允许做比这更多的事情。内核驱动程序可以完成任意复杂的任务,若不考虑它们运行于更高的 CPU 特权级别上而且使用不同的开发接口,那它们很像用户模式下的 DLL 。在此,我们将使用这种强大的开发技术来侦测 Windows 2000 的内部秘密,使用内核驱动程序就像驾驶从狭小的用户模式飞往 Windows 2000 内核的太空飞船。
创建一个驱动程序的骨架
即使长时间开发 Win32 应用程序和库的开发人员,在首次编写内核驱动程序时,也会感觉像是一个绝对的初学者。这是因为,内核模式下的代码运行在一个完全不同的操作系统环境中。 Win32 开发人员的工作仅局限在属于 Windows 2000 Win32 子系统的几个系统组件上。其他开发人员可能编写 POSI 或 OS/2 应用程序, Windows 2000 的附加子系统为它们提供支持。感谢子系统这个概念, Windows 2000 就像一个变色龙 --- 它可通过这些子系统(前面提及的)导出不同的应用程序开发接口来模拟不同的操作系统。与此相反,内核模式的代码可以看到“真实”的 Windows 2000 操作系统。它们使用的接口可以称之为“最终边界”。当然,这并不是说,内核模式完全摆脱了子系统。在第二章中,我们看到 win32k.sys 就是 Win32 GUI 和窗口管理器在内核模式下的分支,将它们放在内核是出于性能考虑。然而, win32k.sys 导出的 API 函数集合中只有一小部分出现在了 gdi32.dll 和 user32.dll 中,这也意味着只有这一小部分函数可以作为 Win32 API 函数来使用,因此, Win32K 决不只是 Win32 踏入内核世界的一脚,实际上,应把它看作是一个高性能的内核模式的图形引擎。
Windows 2000 DDK ( Device Driver Kit )
由于内核模式下的编程使用了不同的系统接口,在 Win32 编程中经常使用的头文件和库都将无法在内核模式下使用。针对 Win32 开发,微软提供了 Platform Software Development Kit ( SDK )。而与内核模式的驱动开发相关的是, Windows 2000 Device Driver Kit ( DDK )。随文档一起, DDK 还提供了特殊的头文件和导入库,这些都是 Windows 2000 内核模块必须的接口。安装完 DDK 之后,接下来你应该打开 Visual C/C++ ,把 DDK 的路径加入到编译器和链接器的目录列表中。在主菜单中选择 Tools à Options ,然后单击 DirectorIEs 。在目录选择下拉列表中选择 Include files ,然后将 DDK 的适当路径加入,如 图 3-1 所示。默认情况下, DDK 将安装到 NTDDK 目录下, included 文件位于 NTDDKinc 子目录中。需要注意的是,请将新添加的路径置于原有路径的上方,这样就会使用新的头文件或者库。
图 3-1 添加 DDK 头文件路径
图 3-2 添加 DDK 导入库路径
在添加完 DDK 头文件路径后,用同样的方法添加导入库的路径。 DDK 包含两组导入库,一组叫做 free ( release ) builds ,另一组叫做 checked ( debug ) builds 。其对应的目录为: NTDDKlibfrei386 和 NTDDKlibchki386 ,参见 图 3-2 。
DDK 开发环境与 Win32 模式有所不同,下面给出二者之间的一些明显区别:
l 对于 Win32 程序员来说,主要的头文件是 windows.h ,对于内核模式代码来说,应使用 ntddk.h 替代之。
l 主进入点函数叫做 DirverEntry() ,而不再是 WinMain() 或 main() 。 列表 3-1 给出了它们的原型。
l 不能再使用一些常见的 Win32 数据类型,如 BYTE 、 Word 和 DWORD 。 DDK 使用 UCHAR 、 USHORT 、 ULONG 等。不过,很容易就能定义你自己喜欢的类型, 列表 3-2 给出了这样的一个示例。
NTSTATUS DriverEntry ( PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath);
列表 3-1 DriverEntry 函数的原型
typedef UCHAR BYTE, *PBYTE;
typedef USHORT WORD, *PWORD;
typedef ULONG DWORD, *PDWORD;
列表 3-2 定义常见的 Win32 数据类型
此外,还需要注意 Windows NT 4.0 和 Windows 2000 所使用的 DDK 之间的差别,有三点不同需要注意,如下:
l 默认情况下, Windows NT 4.0 DDK 的主目录叫做 DDK ,而 Windows 2000 DDK 叫做 NTDDK
l 在 Windows NT 4.0 DDK 中,主要的头文件 ntddk.h 位于主目录之下。而在 Windows 2000 DDK 中,该文件被移到了 NTDDKDDK 子目录下。
l 导入库的路径也发生了变化: libi386free 变成了 libfrei386 , libi386checked 变成了 libchki386 。
我不知道微软的这种改变有什么实际意义,不过为了生活,我们还是需要了解其变化 J 。
可定制的驱动程序向导
开发内核驱动程序的主要困难在于 Visual C/C++ 没有提供此种类型的工程向导。幸运的是, MSDN 里有一系列不错的关于 Windows NT 内核驱动开发的文章,是 Ruediger R.Asche. 在 1994 至 1995 年编写的。其中的两篇文章( Asche 1995a , 1995b )详细说明了如何在 Visual C/C++ 中加入自定义的驱动程序向导,这些文章给了我很大的帮助,尽管原始向导的输出文件不能满足我的所有需求,但这是一个很好的开始。我提供的内核驱动向导将基于 Ruediger Asche 的原始向导产生的输出文件。
我提供的驱动向导的所有源代码位于本书光盘的 srcw2k_wiz 目录。通过阅读这些代码,你会发现它实际的标题“ SBS Windows 2000 Code Wizard ”。事实上,这是一个一般性的 Windows 2000 程序骨架生成器,该生成器可以产生多种类型的程序,包括 Win32 DLL 和应用程序。不过,光盘中的配置文件针对内核驱动开发做了一定的修改。基本上来说,我提供的向导是一个文件转换器,它读取一组文件,然后按照一些简单的规则将它们进行转换,最后将结果写入另一组文件中。输入文件是模板,输出文件是 C 工程文件。通过修改模板文件,该向导可以变成一个 DLL 向导等等。必须提供 7 个模板文件(如果丢失了某一个,会产生错误):
l 扩展名为 .tw 的文件是 workspace 模板,此种文件将会被保存为 Visual Studio 的工程文件 .dsw 。
l 扩展名为 .tp 的文件是工程模板,此种文件将被保存为 .dsp 文件。工程文件由于之关联的 workspace 文件引用,工程文件还包含生成工程的所有配置选项。
l 扩展名为 .tc 、 .th 、 .tr 和 .td 的文件都是 C 代码文件,这些文件最后会变成相应的 .c 、 .h 、 .rc 和 .def 文件。
l 扩展名为 .ti 的是 icon 文件,该文件会被直接保存为 .ico 文件。
这七个文件是一个新工程所必需的。 .def 文件以一种较老风格的方法从 DLL 中导出 API 函数,不过我更喜欢 __declspec(dllexport) 方式。因为驱动程序通常不导出函数,所以我省略了 .td 模板,导致的结果是,在开始时,向导会报告一个错误。我还省略了资源脚本和 icon 文件,不过经验告诉我,最好提供它们。采用的转换规则也非常简单,仅包含一个很短的字符串替换列表。在扫描模板文件时,转换器查找以 % 号开始的转义符。当它找到后,会根据 % 后的字符来决定执行什么样的动作。 表 3-1 列出了验证过的转义符。
表 3-1 中有几处需参考配置文件 ---w2k_wiz.ini 。 示例 3-1 给出了其默认设置。在使用向导之前,你应该将光盘 srcw2k_wizrelease 目录下的 w2k_wiz.exe 、 w2k_wiz.ini 和所有的 w2k_wiz.t* 模板文件复制到你的硬盘上,然后编辑配置文件,将对应内容改为你自己的设置。你还需要修改 Include 、 Free 和 Checked ,使其和你的 DDK 安装相匹配。如果你使用 Visual C/C++ 6.0 ,可以不改变 Root 的值。如果不,则将其设为你存放工程文件的根目录。如果以一个反斜线结尾,它将作为默认值。在 示例 3-1 中,其键值为: HKEY_CURRENT_USERSoftWareMicrosoftoDevStudio6.0Directories ,而 WorkspaceDir 用来存放基本的工作目录。
键入 w2k_wiz MyDriver 来执行该向导,它会当前目录下创建名为 MyDriver 的工程目录,该目录将存放向导生成的 MyDriver.dsw 、 MyDriver.dsp 、 MyDriver.c 、 MyDriver.h 、 MyDriver.rc 和 MyDriver.ico 文件。如果你指定了具体的路径,则会在你指定的路径下创建该目录。另一个合法的命令选项是星号,如: w2k_wiz *MyDriver 。在此种情况下,向导不会在当前目录下创建工程目录,而是去查找 Visual C/C++ 维护的默认的工程根目录,即 w2k_wiz.ini 中的 Root 所指向的位置。
w2k_wiz.ini
08-27-2000 Sven B. Schreiber
sbs@orgon.com
[Settings]
Text = <SBS Windows 2000 Code Wizard Project>
Company = <MyCompany>
Author = <MyName>
Email = <my@email>
Prefix = <MyPrefix>
Include = E:NTDDKinc
Free = E:NTDDKlibfrei386
Checked = E:NTDDKlibchki386
Root = HKEY_CURRENT_USERSoftwareMicrosoftDevStudio6.0DirectoriesWorkspaceDir
示例 3-1. 向导支持的自定义选项
运行驱动向导
现在,来试试这个驱动向导。 示例 3-2 给出了在 Windows 2000 控制台下执行 w2k_wiz *TestDrv 后的输出。这将在 Visual C/C++ 默认的工程根目录下创建一个名为 TestDrv 的工程目录。
显然,除了将 .td 模板转换为 .def 时出了错,其余转换都成功的完成了。因为该向导生成的驱动程序骨架不需要 .def 文件,所以不需要提供 .td 模板文件。现在,用 Visual C/C++ 打开一个新的 WorkSpace ,然后你会发现一个名为 TestDrv 的新目录,该目录中包含一个名为 TestDrv.dsw 的 WorkSpace 文件。该文件可以被正确的打开。接下来,你因该为生成项目选择活动的配置信息。驱动向导生成的 .dsp 文件提供了如下两个可用配置:
1. Win2k Kernel-mode Driver(debug)
2. Win2k Kernel-mode Driver(release)
默认情况下,将使用 debug 配置来生成项目,但是你可在任何时候从 Visual C/C++ 菜单 Build/Set Active Configuration 来选择不同的项目配置。最后,你要将光盘中的 srccommonincludeDrvInfo.h 复制到你自己的头文件目录中。在打开 TestDrv.rc 时,应使用文本模式来打开(如 图 3-3 所示),这是因为该文件使用了来自 DrvInfo.h 中的复杂的宏定义,这些宏会导致资源编辑器异常退出。这个错误从 Visual C/C++ 5.0 开始,在我印象中,一直没有被改正过。和编辑器不同,资源编译器( Resource Compiler )可以正常的处理这些宏。
图 3-3. 以文本模式打开 TestDrv.c 、 TestDrv.h 和 TestDrv.rc
现在,已经为第一次编译做好了所有准备。在示例 3-3 中,我通过选择 Build/Rebuild 菜单来建立 Driver 的 Release 版,看起来一切都正常。顺便说一下,头两行末尾的省略号表示我截断了 Build 命令的输出。
链接器会在 Debug 或 Release 目录下创建了一个名为 TestDrv.sys 的可执行文件,这依赖于你的生成配置。 Test Driver 的 Release 版大小为 5.5KB ,其 Debug 版为 8KB 。你可以使用本书光盘中的 MFVDasm 或 PEView 来验证 TestDrv.sys 是否包含有效的代码和数据。
深入驱动程序的骨架
列表 3-3 展示了向导生成的 TestDrv.c 。与之相关的头文件 TestDrv.h 在 列表 3-4 中。在 列表 3-3 中,请注意标题处的 <MyName> 和 <MyCompany> 标志。如果 w2k_wiz.ini 中的作者和公司名称正确,那你自己的名字和相应的公司名称将会替代它们。
// TestDrv.c
// 08-07-2000 <MyName>
// Copyright @2005 <MyCompany>
#define _TESTDRV_SYS_
#include <ntddk.h>
#include 'TestDrv.h'
// =================================================================
// DISCLAIMER
// =================================================================
/*
This software is provided 'as is' and any express or implied
warranties, including, but not limited to, the implied warranties of
merchantability and fitness for a particular purpose are disclaimed.
In no event shall the author <MyName> be liable for any
direct, indirect, incidental, special, exemplary, or consequential
damages (including, but not limited to, procurement of substitute
goods or services; loss of use, data, or profits; or business
interruption) however caused and on any theory of liability,
whether in contract, strict liability, or tort (including negligence
or otherwise) arising in any way out of the use of this software,
even if advised of the possibility of such damage.
*/
// =================================================================
// REVISION HISTORY
// =================================================================
/*
08-07-2000 V1.00 Original version.
*/
// =================================================================
// GLOBAL DATA
// =================================================================
PRESET_UNICODE_STRING (usDeviceName, CSTRING (DRV_DEVICE));
PRESET_UNICODE_STRING (usSymbolicLinkName, CSTRING (DRV_LINK ));
PDEVICE_OBJECT gpDeviceObject = NULL;
PDEVICE_CONTEXT gpDeviceContext = NULL;
// =================================================================
// DISCARDABLE FUNCTIONS
// =================================================================
NTSTATUS DriverInitialize (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath);
NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath);
// -----------------------------------------------------------------
#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverInitialize)
#pragma alloc_text (INIT, DriverEntry)
#endif
// =================================================================
// DEVICE REQUEST HANDLER
// =================================================================
NTSTATUS DeviceDispatcher (PDEVICE_CONTEXT pDeviceContext,
PIRP pIrp)
{
PIO_STACK_LOCATION pisl;
DWORD dInfo = 0;
NTSTATUS ns = STATUS_NOT_IMPLEMENTED;
pisl = IoGetCurrentIrpStackLocation (pIrp);
switch (pisl->MajorFunction)
{
case IRP_MJ_CREATE:
case IRP_MJ_CLEANUP:
case IRP_MJ_CLOSE:
{
ns = STATUS_SUCCESS;
break;
}
}
pIrp->IoStatus.Status = ns;
pIrp->IoStatus.Information = dInfo;
IoCompleteRequest (pIrp, IO_NO_INCREMENT);
return ns;
}
// =================================================================
// DRIVER REQUEST HANDLER
// =================================================================
NTSTATUS DriverDispatcher (PDEVICE_OBJECT pDeviceObject,
PIRP pIrp)
{
return (pDeviceObject == gpDeviceObject
? DeviceDispatcher (gpDeviceContext, pIrp)
: STATUS_INVALID_PARAMETER_1);
}
// -----------------------------------------------------------------
void DriverUnload (PDRIVER_OBJECT pDriverObject)
{
IoDeleteSymbolicLink (&usSymbolicLinkName);
IoDeleteDevice (gpDeviceObject);
return;
}
// =================================================================
// DRIVER INITIALIZATION
// =================================================================
NTSTATUS DriverInitialize (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath)
{
PDEVICE_OBJECT pDeviceObject = NULL;
NTSTATUS ns = STATUS_DEVICE_CONFIGURATION_ERROR;
if ((ns = IoCreateDevice (pDriverObject, DEVICE_CONTEXT_,
&usDeviceName, FILE_DEVICE_CUSTOM,
0, FALSE, &pDeviceObject))
== STATUS_SUCCESS)
{
if ((ns = IoCreateSymbolicLink (&usSymbolicLinkName,
&usDeviceName))
== STATUS_SUCCESS)
{
gpDeviceObject = pDeviceObject;
gpDeviceContext = pDeviceObject->DeviceExtension;
gpDeviceContext->pDriverObject = pDriverObject;
gpDeviceContext->pDeviceObject = pDeviceObject;
}
else
{
IoDeleteDevice (pDeviceObject);
}
}
return ns;
}
// -----------------------------------------------------------------
NTSTATUS DriverEntry (PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pusRegistryPath)
{
PDRIVER_DISPATCH *ppdd;
NTSTATUS ns = STATUS_DEVICE_CONFIGURATION_ERROR;
if ((ns = DriverInitialize (pDriverObject, pusRegistryPath))
== STATUS_SUCCESS)
{
ppdd = pDriverObject->MajorFunction;
ppdd [IRP_MJ_CREATE ] =
ppdd [IRP_MJ_CREATE_NAMED_PIPE ] =
ppdd [IRP_MJ_CLOSE ] =
ppdd [IRP_MJ_READ ] =
ppdd [IRP_MJ_WRITE ] =
ppdd [IRP_MJ_QUERY_INFORMATION ] =
ppdd [IRP_MJ_SET_INFORMATION ] =
ppdd [IRP_MJ_QUERY_EA ] =
ppdd [IRP_MJ_SET_EA ] =
ppdd [IRP_MJ_FLUSH_BUFFERS ] =
ppdd [IRP_MJ_QUERY_VOLUME_INFORMATION] =
ppdd [IRP_MJ_SET_VOLUME_INFORMATION ] =
ppdd [IRP_MJ_DIRECTORY_CONTROL ] =
ppdd [IRP_MJ_FILE_SYSTEM_CONTROL ] =
ppdd [IRP_MJ_DEVICE_CONTROL ] =
ppdd [IRP_MJ_INTERNAL_DEVICE_CONTROL ] =
ppdd [IRP_MJ_SHUTDOWN ] =
ppdd [IRP_MJ_LOCK_CONTROL ] =
ppdd [IRP_MJ_CLEANUP ] =
ppdd [IRP_MJ_CREATE_MAILSLOT ] =
ppdd [IRP_MJ_QUERY_SECURITY ] =
ppdd [IRP_MJ_SET_SECURITY ] =
ppdd [IRP_MJ_POWER ] =
ppdd [IRP_MJ_SYSTEM_CONTROL ] =
ppdd [IRP_MJ_DEVICE_CHANGE ] =
ppdd [IRP_MJ_QUERY_QUOTA ] =
ppdd [IRP_MJ_SET_QUOTA ] =
ppdd [IRP_MJ_PNP ] = DriverDispatcher;
pDriverObject->DriverUnload = DriverUnload;
}
return ns;
}
// =================================================================
// END OF PROGRAM
// =================================================================
列表 3-3. 驱动程序骨架的源代码
// TestDrv.h
// 08-07-2000 <MyName>
// Copyright @2005 <MyCompany>
// =================================================================
// PROGRAM IDENTIFICATION
// =================================================================
#define DRV_BUILD 1
#define DRV_VERSION_HIGH 1
#define DRV_VERSION_LOW 0
// -----------------------------------------------------------------
#define DRV_DAY 07
#define DRV_MONTH 02
#define DRV_YEAR 2005
// -----------------------------------------------------------------
// Customize these settings by editing the configuration file
// D:etc32w2k_wiz.ini
#define DRV_MODULE TestDrv
#define DRV_NAME <SBS Windows 2000 Code Wizard Project>
#define DRV_COMPANY <MyCompany>
#define DRV_AUTHOR <MyName>
#define DRV_EMAIL <my@email>
#define DRV_PREFIX <MyPrefix>
// =================================================================
// HEADER FILES
// =================================================================
#include 'drvinfo.h' // defines more DRV_* items
////////////////////////////////////////////////////////////////////
#ifndef _RC_PASS_
////////////////////////////////////////////////////////////////////
// =================================================================
// CONSTANTS
// =================================================================
#define FILE_DEVICE_CUSTOM 0x8000
// =================================================================
// STRUCTURES
// =================================================================
typedef struct _DEVICE_CONTEXT
{
PDRIVER_OBJECT pDriverObject;
PDEVICE_OBJECT pDeviceObject;
}
DEVICE_CONTEXT, *PDEVICE_CONTEXT, **PPDEVICE_CONTEXT;
#define DEVICE_CONTEXT_ sizeof (DEVICE_CONTEXT)
////////////////////////////////////////////////////////////////////
#endif // #ifndef _RC_PASS_
////////////////////////////////////////////////////////////////////
// =================================================================
// END OF FILE
// =================================================================
列表 3-4. 驱动程序骨架的头文件