资源描述
Delphi驱动开发研究序篇—
基础知识
本教程讲述了如何在以Windows NT为基础的操作系统上用Delphi开发内核模式驱动程序(KMD),包括NT4.0、2000、XP和2003等操作系统。开发Windows 95/98/ME使用的VxD驱动程序方面的知识并不在本教程讲述的范围内。
一、内核模式驱动结构概述
1.主要组成部分
根据地址空间、代码权限和职责的不同,Windows NT内部划分为两个截然不同的部分。 地址空间的享用方式也非常容易理解,整个32位系统的4GB内容被划分为两个相等的部分,用户模式(user-mode)的进程使用的地址空间被映射到低位的2GB上(地址范围00000000 - 7FFFFFFFh),而高位的2GB(地址范围80000000h - 0FFFFFFFFh)则供操作系统的组成部分来使用,如设备驱动程序、系统内存池、系统使用的数据结构等,在这部分中,内存共享的权限和职责等方面就要复杂一点了。
下面就是用户模式进程的一些简单分类:
◎ 系统支持进程--如Logon进程(位于\%SystemRoot%\System32\Winlogon.exe)
◎ 服务进程--如Spooler进程(位于\%SystemRoot%\System32\spoolsv.exe)
◎ 用户应用程序--任何Win32、Windows 3.1、DOS、POSIX或者OS/2程序
◎子系统--Windows内置3个子系统:Win32(位于\%SystemRoot%\System32\Csrss.exe)、POSIX子系统(位于\%SystemRoot%\System32\Psxss.exe)和OS/2子系统(位于\%SystemRoot%\System32\Os2ss.exe),在Windows XP以及后续的操作系统中,POSIX和OS/2子系统已经被去掉了。
而下面是内核模式的一些模块:
◎ 运行模块--内存管理、进程和线程的管理、安全机制等
◎ 内核--线程调度、中断、异常的分派等(运行模块和内核位于\%SystemRoot%\System32\Ntoskrnl.exe)
◎ 设备驱动程序--硬件设备驱动程序、文件系统和网络驱动程序
◎ 硬件抽象层(Hardware Abstraction Layer, HAL)--将内核、设备驱动程序和运行模块和具体的硬件平台隔离开(位于\%SystemRoot%\System32\Hal.dll)
◎ 窗口和图形系统--实现GUI函数,如处理窗口、用户界面的控制和绘画等(位于\%SystemRoot%\System32\Win32k.sys)
2.内核模式和用户模式
Intel x86体系结构的处理器定义了4个级别的权限(称为Ring),Windows系统使用了Ring0(供特权模式使用)和Ring3(供用户模式使用),Windows系统只使用了2个级别的权限级别的原因是为了和其他一些硬件系统兼容,这些硬件系统只有2个级别的权限,如Compaq Alpha和Silicon Graphics MIPS等。
每个用户模式的进程有其私有的地址空间,这些进程在最低的权限级别下运行(称为Ring3或者用户模式),它们不允许执行CPU的特权指令,对系统所属的数据、地址空间以及硬件等的访问也是被严格限制的,例如,如果某个用户程序访问4G地址空间中的高位2G,那么系统就会立即将其终止执行。要注意的是,进程调用系统功能的时候,可以切换到内核模式执行,但是调用结束后,就返回到用户模式了。
用户模式的进程总是被认为是对操作系统稳定性的潜在威胁,所以它们的权限被严格地限制,任何触及这些限制的举动都将使进程被终止。
而内核模式的组件则可以共享这些受保护的内核模式内存空间,在特权级别下运行(也称为Ring0),允许执行任何CPU指令,包括特权指令,可以无限制地访问系统数据、代码和硬件资源。
内核模式代码运行在系统地址空间中,并总是被认为是可信任的,一旦被装载运行后,驱动程序就是系统的一部分,可以无限制地做任何事情。
总的来说,用户模式程序被完全从操作系统隔离,这对操作系统的完整性来说是件好事情,但对某些种类的应用程序来说就太头痛了,比如Debug工具。幸运地是,这些在用户模式几乎不可能完成的任务完全可以通过内核模式的驱动程序来完成,因为这些驱动程序的操作是不受限制的。因此,如果你打算从用户模式存取操作系统内部的数据结构或者函数的话,唯一的方法就是将一个内核模式驱动程序装载到系统的地址空间中(并调用它),这是很简单的事情,操作系统完全支持这样的操作。
二、Windows NT设备驱动程序
1.设备驱动程序的分类
Windows NT支持的设备驱动程序的范围很广,它们的分类如下:
用户模式的驱动程序:
◎ 虚拟设备驱动程序(Virtual Device Drivers/VDD)--用户模式的组件,用于为16位的MS-DOS应用程序提供虚拟的执行环境,虽然和Windows 95/98里面的VxD从功能上看起来是差不多的,但实际上两者根本不同。
◎ 打印驱动程序--将与设备无关的图形转换到和打印机相关的指令
内核模式驱动程序:
◎ 文件系统驱动程序--实现标准的文件系统模型
◎ 传统设备驱动程序--用于在没有其他驱动程序帮助的情况下控制硬件设备,它们是为老版本的Windows NT系统所写的,但是也可以不加修改地运行在Windows 2000/XP/2003系统上
◎ 视频驱动程序--不用多介绍了吧?
◎ 流驱动程序--支持多媒体设备,如声卡
◎ WDM驱动程序--即Windows Driver Model,WDM包括对Windows NT电源管理和即插即用的支持,WDM可以在Windows 2000、Windows 98和Windows ME下实现,所以在这些操作系统下,WDM驱动程序在源代码级别是兼容的,在有些情况下,在二进制代码级别上也是兼容的
在不同的资料中,对驱动程序的分类方法可能完全不同,但这并不是问题。
从名称理解,设备驱动程序是用于控制某个设备的,但这个"设备"并不一定指的是物理上存在的设备,它也可以是虚拟设备。
从文件结构上讲,设备驱动程序就是一个普普通通的PE格式文件,就像其他EXE或者DLL文件一样。设备驱动程序是一个可装载的内核模式模块,一般以SYS为扩展名。他们之间的不同点在于两种的装载方法是完全不同的。实际上,我们可以把设备驱动程序理解成一个内核模式的DLL,用于完成在用户模式下所不能完成的功能,本质上的不同就在于我们无法直接存取设备驱动程序的代码和数据(注:DLL的代码和数据是可以被直接存取的,这方面的资料可以参考《Windows环境下32位汇编语言程序设计一书》中的DLL一章),唯一的存取方式是通过I/O管理器,它提供了简单的驱动程序管理环境。
刚开始学习KMD的开发的时候,你可能感觉自己根本就是一个菜虫(旁白:就是比菜鸟还低级,呵呵~~~),因为你以前用Windows API开发程序的经验在这里根本帮不上忙,即使你以前写过n多个(n趋向无穷大……)用户模式下的应用系统也没用。内核提供了完全不同的函数和数据结构,以至于你要从头开始了解,而且资料奇缺无比,一般情况下,可供参考的只有头文件。
2.分层的和单层的设备驱动程序
大部分控制硬件设备的驱动程序是分层的驱动程序,分层驱动的概念就是当用户模式发出一个请求时,每个请求从高层次的驱动程序逐层处理并流传到低层次的驱动程序中,一个I/O请求的处理可能分步在多个驱动程序中,例如,如果一个应用程序发出读盘请求,处理请求会在多个驱动程序中流过,在其中你也可以再加入n多个过滤驱动程序(比如插入一个加解密的模块)。
单层的驱动程序是最简单的一类驱动程序,这一类驱动程序通常并不依赖于其他已装载的驱动程序,他们的接口仅仅针对用户模式的应用程序,开发和调试这一类驱动程序是非常简单的,我们即将开始讨论的就是这类程序,其他类型的驱动程序将在以后讨论。
3 线程上下文(Thread Context)
在大多数情况下,我们的系统中只安装了一个CPU,所以,对于所有这些运行中的程序来说,操作系统对每个进程中的线程所使用的CPU时间进行调度,循环为每个线程分配时间片,这就造成了多个程序同时执行的假象。如果系统中安装了多个CPU,那么操作系统的调度算法将复杂得多,因为它要将各CPU上的线程进行平衡。如果Windows检测到一个新线程要开始运行了,它将进行一次上下文切换(context switch)(注:上下文(Content)实际上就是线程运行的环境,也就是运行时各寄存器和其他东东的状态,更自然的理解就是"线程状态")。所谓上下文切换就是保存线程运行时的机器状态,然后将另一个线程的状态恢复并重新开始执行。如果重新开始执行的线程属于另一个进程,那么该进程的地址空间也将被同时切换过来(通过在CR3寄存器中装入页表)。
每个用户进程都有私有的地址空间,所以他们的页表都是不同的,CPU通过切换页表来将虚拟地址映射到物理地址,设备驱动程序并不需要直接做这些工作。上下文切换比较耗CPU时间,所以驱动程序一般不创建它们自己的线程,它们一般在下列环境中的一个中运行:
(1)在发起I/O请求的用户线程中运行
(2)在内核模式下的系统线程中运行
(3)作为中断运行(并不处于哪个特定的进程或线程中,因为它们都被暂时挂起了)
在处理I/O请求包(IRPs)时,我们总是运行在和用户模式的调用者相同的进程上下文中运行,这样我们就能对用户程序的地址空间进行寻址。但是当驱动程序被加载或者卸载的时候,我们将在系统进程中运行,这时存取的只能是系统的地址空间。
4.中断请求级别
中断是任何操作系统都少不了的组成部分,中断使处理器打断正常的程序流程来首先处理它们,中断分硬件中断和软件中断两种,中断是分优先级的,一个高优先级的中断可以打断低优先级的中断的执行。
Windows中把中断优先级称为IRQLs(interrupt request levels),在系统中表示为从0(被动)到31(高级)的整数,其中大的数值对应高优先级的中断。注意IRQL值的含义和线程调度优先级的含义是完全两码事情。
严格来说,IRQL=0的中断并不是中断,因为它无法打断任何其他代码的执行(因为没有比0更低级的代码了),所有的用户模式线程在这个级别上运行,该级别也称为被动级别(passive level)。我们后面要讨论的驱动程序代码也在这个级别上运行,注意这并不意味着其他的驱动程序也在被动级别下运行。
因此这里还有两个重要的结论:
首先:当驱动程序运行于用户模式程序的线程中时,代码的执行可能被高IRQL级别的代码打断,一些函数可以用来获取当前的IRQL值,并可以对其进行提升或者降低。
第二:被动模式IRQL下的代码可以调用任何的内核函数(DDK指明了每个函数允许调用的IRQL级别),可以对已分页的或未分页的内存进行寻址(注:即已映射过的虚拟地址还是物理内存地址)。反过来,当在一个比较高的IRQL级别下对分页内存进行寻址时(指等于或高于DISPATCH_LEVEL),系统将崩溃,因为这时内存管理器的IRQL级别反而比较低,以至于无法处理页错误了。
5.系统崩溃
我想每个人都见过著名的蓝屏死机画面,即"Blue Screen Of Death",简称为BSOD,也许根本不需要解释它是怎么出现或者在什么时候出现的,因为在后面的KMD开发过程中,你会很频繁地遇到它们。
在内核模式下,Windows不对任何系统内存进行保护,由于内核模式的驱动程序可以对系统内存和操作系统的地址空间进行任意存取,所以你必须对你开发的驱动程序进行严格的测试,以防它危及到系统的稳定。
你可以把这个作为最基本的原则,另外,如果没有线程上下文、中断优先级、内核模式和用户模式等方面的概念,开发内核模式驱动程序将是不可能的事。
6. Driver Development Kit
Windows DDK是MSDN专业版和宇宙版的一部分,它也可以从 NT内部信息,包括系统函数、数据结构等的丰富资源,不幸的是,微软已经停止了免费发放DDK,所以现在只好去买正版的CD了(没有枪,没有炮,盗版游击队给我们造~~~)
除了文档,DDK还包含了一堆的库文件(*.lib),这些库可以在链接的时候用上。这些库有两种版本:普通的版本(称为free build)和特殊的包含Debug信息的版本(称为checked build),它们分别位于%ddk%\libfre\i386和%ddk%\libchk\i386目录下,check build是在编译Windows源代码时加上DEBUG标志后生成的,在开发驱动程序时,它们可以提供更加精确的错误定位,但是你首先要根据你的操作系统选择合适的lib版本才行。
7.驱动程序的调试
调试内核模式的代码需要合适的调试器,Compuware的SoftIce是个不错的选择(见 Kernel Debugger,它需要两台计算机:主机和目标机器,目标机器是被调试的机器,主机是运行调试软件的机器。Mark Russinovich ( ;) 也写了一个工具,叫做LiveKd,它允许在单台机器上运行Microsoft Kernel Debugger,而不再需要两台机器了。
第二篇--工具及环境搭建
上篇教程主要是讲解了用Delphi开发Windows驱动程序需要解决的一些技术上的问题,虽然啰嗦了一大堆,也不知道讲清楚了没有^_^。本篇我们开始讲述用Delphi构建驱动开发环境。
用Delphi开发驱动程序所必须的工具:
l Dcc32.exe – Delphi编译器,我用的是Delphi 2007的dcc32
l rmcoff -- 我用BCB开发的Delphi目标文件符号名修改、OMF到coff格式转换以及删除obj文件中无用代码及重复符号的工具
l Link.exe -- microsoft链接器,不要使用7.1xx版的,似乎有bug
l DDK相关结构、APIs的Delphi声明文件(我已经完成部分结构、APIs的声明转换,放在我的KmdKit4D工具包里)
有上面的东东就可以开发Windows驱动程序了,下面就让我们来写一个最简单的驱动程序:
unit driver;
interface
uses nt_status, ntoskrnl;
function _DriverEntry(DriverObject:PDriverObject;RegistryPath:PUnicodeString):NTSTATUS; stdcall;
implementation
procedure DriverUnload(DriverObject:PDriverObject); stdcall;
begin
DbgPrint('DriverUnload(DriverObject:%08X)',DriverObject);
DbgPrint('DriverUnload(-));
end;
function _DriverEntry(DriverObject:PDriverObject;RegistryPath:PUnicodeString):NTSTATUS; stdcall;
begin
DbgPrint('DriverEntry(DriverObject:%08X;RegistryPath:%08X)',DriverObject,RegistryPath);
DriverObject^.DriverUnload:=@DriverUnload;
Result:=STATUS_SUCCESS;
DbgPrint('DriverEntry(-):%08X',Result);
end;
end.
以上就是一个最简单的驱动程序,就像其他的可执行程序一样,每个驱动程序也有一个入口点,这是当驱动被装载到内存中时首先被调用的,驱动的入口点是DriverEntry过程(注:过程也就是子程序),DriverEntry这个名称只是一个标记而已,你可以把它命名为其他任何名字--只要它是入口点就行了。DriverEntry过程用来对驱动程序的一些数据结构进行初始化,它的函数原型定义如下:
function _DriverEntry(DriverObject:PDriverObject;RegistryPath:PUnicodeString):NTSTATUS; stdcall;
当然你也可以不用DriverEntry这个名字,任意的名字都可以,不过前面的下划线是必需的。nt_status和 ntoskrnl两个单元包含了常用的数据结构和APIs的声明。由于我常开发Unix下的程序,所以我习惯使用make编译程序,个人感觉make比较智能和方便,因此在推荐大家使用make编译程序。我用的是borland make 5.2版。Makefile的写法可以参考
NAME=driver
DCC=dcc32
INCLUDE=d:\mickeylan\KmdKit4D\include
LIB_PATH=d:\mickeylan\KmdKit4D\lib
DCCFLAGS=-U$(INCLUDE) -B -CG -JP -$A-,C-,D-,G-,H-,I-,L-,P-,V-,W+,Y-
LIBS=ntoskrnl.lib hal.lib win32k.lib ntdll.lib
LINKFLAGS=/NOLOGO /ALIGN:32 /BASE:0x10000 /SUBSYSTEM:NATIVE /DRIVER /LIBPATH:$(LIB_PATH) /FORCE:UNRESOLVED /FORCE:MULTIPLE /ENTRY:DriverEntry
all : $(NAME).sys
$(NAME).sys : $(NAME).obj
omf2d $(NAME).obj /U_*
link $(LINKFLAGS) $(LIBS) /out:$(NAME).sys $(NAME).obj
$(NAME).obj : $(NAME).pas
$(DCC) $(DCCFLAGS) $(NAME).pas
clean :
del *.obj
del *.dcu
del *.sys
在命令行下执行make即可编译生成驱动文件,是不是很简单^_^。此程序的源码放在KmdKit4D的sample\basic目录下,该目录下还有一个loaddriver.bat,执行此批处理文件即可加载驱动,并且可以在DbgView的窗口里看见驱动程序输出的调试信息。
到这里,你应该对用Delphi开发驱动程序有了个大体的了解了,下面让我们再来写一个很有趣的驱动程序以加深了解。这个程序是从Four-F的KmdKit的giveio转换来的(我比较懒,不想写新的^_^),写个驱动程序让用户模式下的进程能通过读取端口来访问电脑的CMOS。
大家都知道,端口是被Windows保护起来的,正常情况下,用户模式下的程序是无法直接操作端口的,通过我们的驱动程序修改I/O许可位图(I/O permission bit map,IOPM),这样用户模式下的相应进程就被允许自由地存取I/O端口,这方面详细资料见
TSS的设计意图是为了在任务切换的时候保存处理器状态,从执行效率的考虑出发,Windows NT并没有使用这个特征,它只维护一个TSS供多个进程共享,这就意味着IOPM也是共享的,因此某个进程改变了IOPM的话,造成的影响是系统范围的。
ntoskrnl.exe中有些未公开的函数是用来维护IOPM的,它们是Ke386QueryIoAccessMap和Ke386SetIoAccessMap函数。
function Ke386QueryIoAccessMap(
dwFlag:DWORD;
pIopm:PVOID): NTSTATUS; stdcall;
Ke386QueryIoAccessMap函数从TSS中拷贝2000h字节的当前IOPM到指定的内存缓冲区中,缓冲区指针由pIopm参数指定。
各参数描述如下:
◎ dwFlag--0表示将全部缓冲区用0FFh填写,也就是所有的位都被设置,所有的端口都被禁止访问;1表示从TSS中将当前IOPM拷贝到缓冲区中
◎ pIopm--用来接收当前IOPM的缓冲区指针,注意缓冲区的大小不能小于2000h字节
如果函数执行成功的话会在返回值的低8位返回非0值;如果执行失败则返回零。
function Ke386SetIoAccessMap(
dwFlag:DWORD;
pIopm:PVOID): NTSTATUS; stdcall;
Ke386SetIoAccessMap函数刚好相反,它从pIopm参数指定的缓冲区中拷贝2000h字节的IOPM到TSS中去。
各参数描述如下:
◎ dwFlag--这个参数只能是1,其他任何值函数都会返回失败
◎ pIopm--指向包含IOPM数据的缓冲区,缓冲区的尺寸不能小于2000h字节
如果函数执行成功的话会在返回值的低8位返回非0值;如果执行失败则返回零
当IOPM拷贝到TSS后,IOPM的偏移指针必须被定位到新的数据中去,这可以通过Ke386IoSetAccessProcess函数来完成,这也是ntoskrnl.exe中的一个很有用的未公开函数。
function Ke386IoSetAccessProcess(
pProcess: PKPROCESS;
dwFlag:DWORD): NTSTATUS; stdcall;
Ke386IoSetAccessProcess允许或者禁止对进程使用IOPM。其参数说明如下:
◎ pProcess--指向KPROCESS结构
◎ dwFlag--0表示禁止对I/O端口进行存取,将IOPM的偏移指针指到TSS段外面;1表示允许存取I/O端口,将IOPM的偏移指针指到TSS段的88h中
如果函数执行成功的话会在返回值的低8位返回非0值;如果执行失败则返回零
顺便提一下,ntoskrnl中的所有函数都有前缀,通过这个前缀你就可以辨别该函数属于系统功能中的哪一类。不同的前缀表示不同的功能--如i前缀表示内部使用(internal)、p表示私有函数(private)、f表示fastcall。再如,Ke表示内核函数(kernel),Psp表示内部进程支持函数(internal process support),Mm表示内存管理函数(Memory Manager)等等。
Ke386IoSetAccessProcess函数的第一个参数指向进程对象,也就是KPROCESS结构(在\include\nt_status.dcu中定义),Ke386IoSetAccessProcess会将KPROCESS结构中IopmOffset字段的值设置为合适的值。
unit giveio;
interface
uses
nt_status, ntoskrnl, ntutils;
const
IOPM_SIZE = $2000;
function _DriverEntry(DriverObject:PDriverObject;pusRegistryPath:PUnicodeString):NTSTATUS; stdcall;
implementation
function _DriverEntry(DriverObject:PDriverObject;pusRegistryPath:PUnicodeString):NTSTATUS; stdcall;
var
status:NTSTATUS;
oa:OBJECT_ATTRIBUTES;
hKey:HANDLE;
kvpi:KEY_VALUE_PARTIAL_INFORMATION;
pIopm:PVOID;
pProcess: PVOID;
iRet: NTSTATUS;
resultLen: ULONG;
KeyValue: TUnicodeString;
begin
DbgPrint('giveio: Entering DriverEntry');
status := STATUS_DEVICE_CONFIGURATION_ERROR;
InitializeObjectAttributes(oa, pusRegistryPath, 0, 0, nil);
iRet := ZwOpenKey(hKey, KEY_READ, @oa);
if iRet = STATUS_SUCCESS then
begin
RtlInitUnicodeString(KeyValue, 'ProcessId');
if (ZwQueryValueKey(hKey, @KeyValue,
KeyValuePartialInformation, PVOID(@kvpi),
sizeof(kvpi), resultLen) <> STATUS_OBJECT_NAME_NOT_FOUND) and
(resultLen <> 0) then
begin
DbgPrint('giveio: Process ID: %08X', kvpi.dData);
{Allocate a buffer for the I/O permission map}
pIopm := MmAllocateNonCachedMemory(IOPM_SIZE);
if pIopm <> nil then
begin
if PsLookupProcessByProcessId(kvpi.dData, pProcess) = STATUS_SUCCESS then
begin
DbgPrint('giveio: PTR KPROCESS: %08X', @pProcess);
iRet := Ke386QueryIoAccessMap(0, pIopm);
if iRet and $ff <> 0 then
begin
{I/O access for 70h port}
asm
pushad
mov ecx, pIopm
add ecx, 70h / 8
mov eax, [ecx]
btr eax, 70h MOD 8
mov [ecx], eax
{I/O access for 71h port}
mov ecx, pIopm
add ecx, 71h / 8
mov eax, [ecx]
btr eax, 71h MOD 8
mov [ecx], eax
popad
end;
iRet := Ke386SetIoAccessMap(1, pIopm);
if iRet and $FF <> 0 then
begin
iRet := Ke386IoSetAccessProcess(pProcess, 1);
if iRet and $FF <> 0 then
begin
DbgPrint('giveio: I/O permission is successfully given');
end else
begin
DbgPrint('giveio: I/O permission is failed');
status := STATUS_IO_PRIVILEGE_FAILED;
end;
end else
begin
status := STATUS_IO_PRIVILEGE_FAILED;
end;
end else
begin
status := STATUS_IO_PRIVILEGE_FAILED;
end;
ObfDereferenceObject(pProcess);
end else
begin
status := STATUS_OBJECT_TYPE_MISMATCH;
end;
MmFreeNonCachedMemory(pIopm, IOPM_SIZE);
end else
begin
DbgPrint('giveio: Call to MmAllocateNonCachedMemory failed');
status := STATUS_INSUFFICIENT_RESOURCES;
end;
end;
ZwClose(hKey);
end;
DbgPrint('giveio: Leaving DriverEntry');
result := status;
end;
end.
以下是makefile:
NAME=giveio
DCC=dcc32
INCLUDE=e:\mickeylan\KmdKit4D\include
LIB_PATH=e:\mickeylan\KmdKit4D\lib
DCCFLAGS=-U$(INCLUDE) -B -CG -JP -$A-,C-,D-,G-,H-,I-,L-,P-,V-,W+,Y-
LIBS=ntoskrnl.lib hal.lib win32k.lib ntdll.lib
LINKFLAGS=/NOLOGO /ALIGN:32 /BASE:0x10000 /SUBSYSTEM:NATIVE /DRIVER /LIBPATH:$(LIB_PATH) /FORCE:UNRESOLVED /FORCE:MULTIPLE /ENTRY:DriverEntry
all : $(NAME).sys
$(NAME).sys : $(NAME).obj
omf2d $(NAME).obj /U_*
link $(LINKFLAGS) $(LIBS) /out:$(NAME).sys $(NAME).obj ntutils.obj
$(NAME).obj : $(NAME).pas
$(DCC) $(DCCFLAGS) $(NAME).pas
clean :
del *.obj
del *.dcu
del *.sys
通过上面的两个例子的学习,相信大家已经能用Delphi写些基本的驱动程序了。本教程的第二部分也就到此为止了,后面还会有更精彩的内容。
第三篇一个完整的驱动程序示例
(注:本篇的原理部分均摘自罗云彬大侠翻译的驱动开发教程)
在前面的两篇教程中我们写了三个玩具驱动程序,为什么说是玩具驱动呢?因为它们确确实实是驱动程序,而且也能完成一些有趣的功能,但是它们都不完整,没有同用户交流的功能,这一篇就让我们来完成一个简单的全功能驱动程序。
在写程序之前,我们有必要了解一些基础知识。
在用户模式下,我们可以通过访问某个地址来直接调用dll中的函数,但是在内核模式下,从系统的稳定性考虑,这样做是非常危险的。所以,系统提供了和内核模式通讯的媒介--I/O管理器,它是I/O子系统的部件之一。I/O管理器将应用程序、系统部件和设备连接起来,并定义了一个架构来支持设备驱动程序。下图是I/O管理器如何在用户模式程序和驱动程序之间进行沟通的简单图解。
一般来说,用户模式的操作都被转换成了对具体硬件设备的I/O操作,仅对于某些设备,设备由驱动程序来创建和控制,这些设备就是虚拟设备。当然,创建这些设备并不意味着你创造了什么硬件,而仅仅是在内存中创建了一个新的对象而已。每个对象和一个物理设备或者逻辑设备对应,用于描述它们的特征。
创建设备后,驱动程序告诉I/O管理器:“这里有个我控制的设备,如果你收到了操作这个设备的I/O请求的话,直接发给我好了,剩下的由我来搞定!”。驱动程序知道如何对自己管理的设备进行I/O操作,I/O管理器唯一的职责在于创建I/O请求并把它发送给适当的设备驱动程序。用户模式的代码不知道(也不必知道)其中的细节,也不用知道究竟是哪个驱动程序在管理哪个设备。
下面先让我们来看一下用户模式下的控制程序:
program VirToPhys;
{$APPTYPE CONSOLE}
uses
SysUtils, Windows, WinSvc, Dialogs, nt_status;
const
NU
展开阅读全文