-
-
[原创]汇编语言内核模式驱动程序设计
-
发表于: 2007-7-11 17:15 21104
-
说明:
考虑到大家从个人主页上下载时有阻塞现象,现在特重发为网页版·
内核模式驱动程序设计教程
原著:Four-f(俄) 翻译:罗云彬(1-5章)松松(6、7章)董岩(8—11章)
1.Kernel Mode驱动程序基础
本教程讲述了如何在Windows NT为基础的操作系统上用Win32汇编开发KMD,包括NT4.0、2000、XP和2003等操作系统。开发Windows 95/98/ME使用的VxD驱动程序方面的知识并不在本教程讲述的范围内,另外,毫无疑问本教程并不那么完美,可能还包含了诸多未发现的错误,如果您发现了问题,请告知作者,毕竟作者的母语并不是英语,把它翻译成英文已经够难为我了(注:原作者是俄国人也),也感谢masquer和Volodya的校对工作。
1.1 KMD结构概述
1.1.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)
图1.1 Windows NT结构简图
1.1.2 内核模式和用户模式
Intel x86体系结构的处理器定义了4个级别的权限(称为Ring),Windows系统使用了Ring0(供特权模式使用)和Ring3(供用户模式使用),Windows系统只使用了2个级别的权限级别的原因是为了和其他一些硬件系统兼容,这些硬件系统只有2个级别的权限,如Compaq Alpha和Silicon Graphics MIPS等。
每个用户模式的进程有其私有的地址空间,这些进程在最低的权限级别下运行(称为Ring3或者用户模式),它们不允许执行CPU的特权指令,对系统所属的数据、地址空间以及硬件等的访问也是被严格限制的,例如,如果某个用户程序访问4G地址空间中的高位2G,那么系统就会立即将其终止执行。要注意的是,进程调用系统功能的时候,可以切换到内核模式执行,但是调用结束后,就返回到用户模式了。
用户模式的进程总是被认为是对操作系统稳定性的潜在威胁,所以它们的权限被严格地限制,任何触及这些限制的举动都将使进程被终止。
而内核模式的组件则可以共享这些受保护的内核模式内存空间,在特权级别下运行(也称为Ring0),允许执行任何CPU指令,包括特权指令,可以无限制地访问系统数据、代码和硬件资源。
内核模式代码运行在系统地址空间中,并总是被认为是可信任的,一旦被装载运行后,驱动程序就是系统的一部分,可以无限制地做任何事情。
总的来说,用户模式程序被完全从操作系统隔离,这对操作系统的完整性来说是件好事情,但对某些种类的应用程序来说就太头痛了,比如Debug工具。幸运地是,这些在用户模式几乎不可能完成的任务完全可以通过内核模式的驱动程序来完成,因为这些驱动程序的操作是不受限制的。因此,如果你打算从用户模式存取操作系统内部的数据结构或者函数的话,唯一的方法就是将一个内核模式驱动程序装载到系统的地址空间中(并调用它),这是很简单的事情,操作系统完全支持这样的操作。
1.2 Windows NT设备驱动程序
1.2.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趋向无穷大……)用户模式下的应用系统也没用。内核提供了完全不同的函数和数据结构,以至于你要从头开始了解,而且资料奇缺无比,一般情况下,可供参考的只有头文件。
1.2.2 分层的和单层的设备驱动程序
大部分控制硬件设备的驱动程序是分层的驱动程序,分层驱动的概念就是当用户模式发出一个请求时,每个请求从高层次的驱动程序逐层处理并流传到低层次的驱动程序中,一个I/O请求的处理可能分步在多个驱动程序中,例如,如果一个应用程序发出读盘请求,处理请求会在多个驱动程序中流过,在其中你也可以再加入n多个过滤驱动程序(比如插入一个加解密的模块)。
单层的驱动程序是最简单的一类驱动程序,这一类驱动程序通常并不依赖于其他已装载的驱动程序,他们的接口仅仅针对用户模式的应用程序,开发和调试这一类驱动程序是非常简单的,我们即将开始讨论的就是这类程序,其他类型的驱动程序将在以后讨论。
1.3 线程上下文(Thread Context)
在大多数情况下,我们的系统中只安装了一个CPU,所以,对于所有这些运行中的程序来说,操作系统对每个进程中的线程所使用的CPU时间进行调度,循环为每个线程分配时间片,这就造成了多个程序同时执行的假象。如果系统中安装了多个CPU,那么操作系统的调度算法将复杂得多,因为它要将各CPU上的线程进行平衡。如果Windows检测到一个新线程要开始运行了,它将进行一次上下文切换(context switch)(注:上下文(Content)实际上就是线程运行的环境,也就是运行时各寄存器和其他东东的状态,更自然的理解就是"线程状态")。所谓上下文切换就是保存线程运行时的机器状态,然后将另一个线程的状态恢复并重新开始执行。如果重新开始执行的线程属于另一个进程,那么该进程的地址空间也将被同时切换过来(通过在CR3寄存器中装入页表)。
每个用户进程都有私有的地址空间,所以他们的页表都是不同的,CPU通过切换页表来将虚拟地址映射到物理地址,设备驱动程序并不需要直接做这些工作。上下文切换比较耗CPU时间,所以驱动程序一般不创建它们自己的线程,它们一般在下列环境中的一个中运行:
1. 在发起I/O请求的用户线程中运行
2. 在内核模式下的系统线程中运行
3. 作为中断运行(并不处于哪个特定的进程或线程中,因为它们都被暂时挂起了)
在处理I/O请求包(IRPs)时,我们总是运行在和用户模式的调用者相同的进程上下文中运行,这样我们就能对用户程序的地址空间进行寻址。但是当驱动程序被加载或者卸载的时候,我们将在系统进程中运行,这时存取的只能是系统的地址空间。
1.4 中断请求级别
中断是任何操作系统都少不了的组成部分,中断使处理器打断正常的程序流程来首先处理它们,中断分硬件中断和软件中断两种,中断是分优先级的,一个高优先级的中断可以打断低优先级的中断的执行。
Windows中把中断优先级称为IRQLs(interrupt request levels),在系统中表示为从0(被动)到31(高级)的整数,其中大的数值对应高优先级的中断。注意IRQL值的含义和线程调度优先级的含义是完全两码事情。
严格来说,IRQL=0的中断并不是中断,因为它无法打断任何其他代码的执行(因为没有比0更低级的代码了),所有的用户模式线程在这个级别上运行,该级别也称为被动级别(passive level)。我们后面要讨论的驱动程序代码也在这个级别上运行,注意这并不意味着其他的驱动程序也在被动级别下运行。
因此这里还有两个重要的结论:
首先:当驱动程序运行于用户模式程序的线程中时,代码的执行可能被高IRQL级别的代码打断,一些函数可以用来获取当前的IRQL值,并可以对其进行提升或者降低。
第二:被动模式IRQL下的代码可以调用任何的内核函数(DDK指明了每个函数允许调用的IRQL级别),可以对已分页的或未分页的内存进行寻址(注:即已映射过的虚拟地址还是物理内存地址)。反过来,当在一个比较高的IRQL级别下对分页内存进行寻址时(指等于或高于DISPATCH_LEVEL),系统将崩溃,因为这时内存管理器的IRQL级别反而比较低,以至于无法处理页错误了。
1.5 系统崩溃
我想每个人都见过著名的蓝屏死机画面,即"Blue Screen Of Death",简称为BSOD,也许根本不需要解释它是怎么出现或者在什么时候出现的,因为在后面的KMD开发过程中,你会很频繁地遇到它们。
在内核模式下,Windows不对任何系统内存进行保护,由于内核模式的驱动程序可以对系统内存和操作系统的地址空间进行任意存取,所以你必须对你开发的驱动程序进行严格的测试,以防它危及到系统的稳定。
你可以把这个作为最基本的原则,另外,如果没有线程上下文、中断优先级、内核模式和用户模式等方面的概念,开发内核模式驱动程序将是不可能的事(天哪,到现在我才发现,我连菜虫都算不上,我竟然是~~~~~~菜菜的单细胞生物!呜呜~~)
1.6 Driver Development Kit
Windows DDK是MSDN专业版和宇宙版的一部分,它也可以从http://www.microsoft.com/ddk/下载,对于开发设备驱动程序来说,DDK是关于Windows NT内部信息,包括系统函数、数据结构等的丰富资源,不幸的是,微软已经停止了免费发放DDK,所以现在只好去买正版的CD了(没有枪,没有炮,盗版游击队给我们造~~~)
除了文档,DDK还包含了一堆的库文件(*.lib),这些库可以在链接的时候用上。这些库有两种版本:普通的版本(称为free build)和特殊的包含Debug信息的版本(称为checked build),它们分别位于%ddk%\libfre\i386和%ddk%\libchk\i386目录下,check build是在编译Windows源代码时加上DEBUG标志后生成的,在开发驱动程序时,它们可以提供更加精确的错误定位,但是你首先要根据你的操作系统选择合适的lib版本才行。
1.7 汇编程序员使用的KmdKit
KmdKit包含了所有用汇编开发KMD所需要的东西:include文件、lib文件、宏定义、例子文件、工具和一些文章,你可以自己在软件包中找到更多的东西,下一节我们将从这个软件包中包括的一些例子开始学习KMD的编程。
1.8 驱动程序的调试
调试内核模式的代码需要合适的调试器,Compuware的SoftIce是个不错的选择(见 http://www.compuware.com/products/numega/index.htm),当然你也可以使用Microsoft Kernel Debugger,它需要两台计算机:主机和目标机器,目标机器是被调试的机器,主机是运行调试软件的机器。Mark Russinovich ( http://www.sysinternals.com/ ) 也写了一个工具,叫做LiveKd,它允许在单台机器上运行Microsoft Kernel Debugger,而不再需要两台机器了。
1.9 其他参考资料
1. David Solomon, Mark Russinovich, "Inside Microsoft Windows 2000. Third Edition", Microsoft Press, 2000
2. Though there is no source code in this book at all, it's the number one book for the device driver programmers.
3. Sven B. Schreiber, "Undocumented Windows 2000 Secrets. A Programming Cookbook", Addison-Wesley
4. The especially practical book, it has many Windows 2000 secrets revealed.
5. Walter Oney, "Programming the Microsoft Driver Model", Microsoft Press, 1999
6. Walter Oney, "Programming the Microsoft Windows Driver Model. 2nd edition", Microsoft Press, 2003(这是一本很好的书,它强调的是即插即用驱动程序的开发,但这并不降低了它的重要性,因为驱动开发方面的基本理论都是通用的)
7. Art Baker, Jerry Lozano, "The Windows 2000 Device Driver Book, A Guide for Programmers, Second Edition", Prentice Hall, 2000,这也是一本好书,选题范围和本教程类似
8. Rajeev Nagar, "Windows NT File System Internals. A Developer's Guide", O'Reilly
9. Prasad Dabak, Sandeep Phadke, and Milind Borate, "Undocumented Windows NT", M&T Books, 1999 ,这本书里面包含了n多公开的文档中找不到的东西。
10. Gary Nebbett, " Windows NT-2000 Native API Reference", MacMillan Technical Publishing, 2000,这里也有一堆公开的文档中找不到的函数和数据结构的说明
11. Jeffrey Richter, "Programming Applications for Microsoft Windows. Fourth Edition", Microsoft Press, 1999,这本书和开发设备驱动程序没什么关系,但是也是一本很有趣的书。
这里列出的并不是全部,但这些似乎都有点儿看看的必要性。
2. 服务
※ 和本节对应的例子代码见KmdKit\examples\simple\Beeper
读者也许有点疑惑:用户模式的服务关内核模式的驱动程序什么事呀?事实上,两者的确风马牛不相及,但是如果我们要和设备驱动程序通讯的话,我们必须首先安装它,启动它,而和设备驱动程序通讯的界面刚好和服务通讯的界面是类似的。
2.1 Windows服务
Windows NT使用某种机制来启动进程,并让它们不和某个具体的交互式的用户界面相关联,这些进程就被称为服务(service),服务的一个很好的例子就是Web服务器,这些Web服务都没有用户界面,服务是唯一以这种方式运行的应用程序(注:指没有用户界面,当然,严格地说病毒、木马以及所有不想见光的程序也是这样的~~),服务可以在系统启动的时候自动启动,也可以被手工启动,从这一点来看,设备驱动程序和服务是类似的。
Windows NT还支持驱动程序服务,只要使用的时候遵循设备驱动程序协议就可以了,这和用户模式的服务类似,所以,"服务"一词既可以指用户模式的服务进程或者内核模式的设备驱动程序,微软不知何故没有明确地区分两者的概念,所以下面的叙述可能看起来有点让人疑惑。可能有的地方我会说到"driver"一词,但在其他的文章中可能说到"service"一词,但既然这篇教程讲的是如何编写内核设备驱动程序,那么我们就约定无论说到"service"还是"driver",我们的意思都是指"驱动程序",当的确需要提及"服务"的时候,我会明确地指出来的。
另外,请读者时刻记得,文档中关于服务管理的函数其实是叙述得相当含糊的,因为这些函数既能用于驱动程序也能用于服务,在下面的文章中,我们只强调它们在驱动方面的用途和忽略服务方面的用途。
Windows NT中有三个组件和服务管理相关:
◎ 服务控制管理器(Service Control Manager/SCM)--用于启动服务以及和它通讯
◎ 服务控制程序(Service Control Program/SCP)--用于和SCM进行通讯,告诉它何时启动或者停止服务
(咦!第三个哪里去了,我也不知道,原文就这么两个呀,可能后面会提到吧~~)
服务程序中包含可执行代码,这两个组件对服务和驱动程序的处理方式是相同的。我们先来看看前面两个组件,在后面再讲述驱动程序。
2.2 服务控制管理器(SCM)
SCM的代码位于\%SystemRoot%\System32\Services.exe中,当系统启动的时候,SCM被WinLogon进程启动,然后它扫描注册表中HKLM\SYSTEM\CurrentControlSet\Services键下的相关内容,根据这些内容创建一个服务数据库,数据库中包括所有服务的相关参数,如果服务或者驱动被标为自动启动的,那么启动它们并检测启动中是否出错。
为了更深入一步,我们可以用注册表编辑器regedit.exe来打开并观察注册表中的 HKLM\SYSTEM\CurrentControlSet\Services\下面的内容。
想要查看系统中安装了哪些服务(注意不是驱动),可以在控制面板中选择"管理工具",再打开"服务"来查看。
要查看系统中安装了哪些驱动,可以在控制面板中选择"管理工具",再打开"计算机管理",在"系统信息"下的"软件环境"中,你可以看到所有驱动的列表,但是不幸的是,在Windows XP中,这个功能被取消了。
仔细对比一下上面三个地方的内容,我们可以发现这些内容是很一致的。
HKLM\SYSTEM\CurrentControlSet\Services\下面有很多子键,表示一个服务的内部名称,每个子键下包含了和这个服务相关的参数。
现在来考察一下安装一个服务所需的最低数量的参数,我们拿beeper.sys来举例,以后再来讨论这个驱动本身。
图2.1 beeper.sys驱动的注册表键值
这些参数的含义如下:
◎ DisplayName--用户程序访问服务时使用的名称,如果为空,那么注册表的键名会被作为它的名称
◎ ErrorControl--如果SCM启动服务的时候驱动报错,这个值决定了SCM如何对付这个错误,我们对两种取值有点兴趣:
• SERVICE_ERROR_IGNORE (0)--I/O管理器忽略这个错误,不作记录
• SERVICE_ERROR_NORMAL (1)--如果驱动被装入的时候报错,系统将给用户显示一个告警框,并将错误记录到系统日志中
你可以在控制面板中的"管理工具"中选择"事件查看器"来查看系统日志,例如,beeper.sys驱动在初始化的时候做完了所有该做的事(这个例子会让喇叭发声音,但是发声功能是在初始化函数DriverEntry中做的,初始化函数执行完,后面就没什么事了),所以它就返回一个错误,系统就会将它从内存中卸载。但是这里的ErrorControl参数等于SERVICE_ERROR_IGNORE,所以系统日志中并没有错误记录。
◎ ImagePath--指驱动文件的全路径文件名,如果该参数没有指定路径,那么系统会在\%SystemRoot%\Drivers目录下查找
◎ Start--指明何时装载驱动,这里我们关心的也是两个取值
• SERVICE_AUTO_START (2)--驱动在系统启动的时候装载
• SERVICE_DEMAND_START (3)--驱动由SCM根据用户要求装载
如果驱动的Start参数为SERVICE_AUTO_START (2),那么SCM会在系统启动的时候就装载它,这样的驱动被称为自动启动的服务,如果驱动的执行依赖于其他的驱动,SCM也会把其他的驱动也启动起来(要控制设备驱动被装载的顺序,可以使用Group、Tag和DependOnGroup等参数值;要控制服务被装载的顺序,可以使用Group和DependOnService参数)。Start参数还有其他的取值,如SERVICE_BOOT_START (0),但这个参数只能供设备驱动程序使用,I/O管理器将在用户模式的进程启动之前把装载这些驱动程序,这时SCM还没有启动呢!
◎ Type--用于指定服务的类型,既然我们这里讲的是KMD的编程,那么我们只对一个取值感兴趣,那就是SERVICE_KERNEL_DRIVER (1)
仔细观察图2.1后,你对beeper.sys有什么要说的吗?好的,我们看到beeper这个内核模式驱动程序位于C:\masm32\Ring0\Kmd\Article2\beeper目录下,它的名称为"Nice Melody Beeper",由用户控制启动,出错信息不被记录。
Path前面的"\??"前缀的含义你下面就会知道!
如果我们要启动SCM数据库中不存在的驱动程序,那么可以在任何时刻在SCP的帮助下动态装入(也许称为DCP/device control program更为贴切,但是微软的术语库中并没有这个词)。
2.3 服务控制程序(SCP)
从名称理解,服务控制程序(service control program/SCP)可以控制服务或者设备驱动程序,这些功能是在SCM的管理下,通过调用适当的函数来完成的,这些函数位于\%SystemRoot%\System32\advapi.dll (Advanced API)中。
这里是一段关于使用SCP来控制beeper.sys驱动的代码例子
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; Service Control Program for beeper driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
include \masm32\Macros\Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
start proc
local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR
invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
.if eax != NULL
mov hSCManager, eax
push eax
invoke GetFullPathName, $CTA0("beeper.sys"),sizeof acDriverPath,addr acDriverPath,esp
pop eax
invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
invoke StartService, hService, 0, NULL
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
.else
invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
NULL, MB_ICONSTOP
.endif
invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start
2.3.1 建立到SCM的连接
在上面的例子中,我们首先要做的事情是使用OpenSCManager函数来建立到SCM的连接,以便在指定的计算机上打开服务数据库。
OpenSCManager proto lpMachineName:LPSTR, lpDatabaseName:LPSTR, dwDesiredAccess:DWORD
函数使用的参数说明如下:
◎ lpMachineName--指向需要打开的计算机名字符串,字符串以NULL结尾,如果参数指定为NULL,表示连接到本机上的SCM
◎ lpDatabaseName--指向以NULL结尾的包含SCM数据库名称的字符串,字符串应该指定为"ServicesActive",如果参数指定为NULL,则默认打开"ServicesActive"
.const
szActiveDatabase db "ServicesActive", 0
SERVICES_ACTIVE_DATABASE equ offset szActiveDatabase
现在我们要打开的就是这个当前被激活的数据库,所以我们使用了NULL参数
◎ dwDesiredAccess--指定访问SCM的权限,这个参数告诉SCM我们需要进行什么样的操作,常用的取值有三个:
• SC_MANAGER_CONNECT--允许连接到SCM,这个取值是默认值,它的定义就是0
• SC_MANAGER_CREATE_SERVICE--允许创建服务
• SC_MANAGER_ALL_ACCESS--允许进行所有的操作
我们可以使用下面的代码连接到SCM:
invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
.if eax != NULL
mov hSCManager, eax
如果OpenSCManager函数执行成功,那么返回值就是被连接的SCM的句柄,我们在以后使用其他函数在对SCM数据库进行操作的时候会用到这个句柄。
另外,忘了提醒大家,安装内核模式驱动程序需要超级用户的权限,为了安全起见,普通权限的用户没有被授权的话是无法执行特权代码的。当然,本文的例子总是假设你是有超级用户权限的。
2.3.2 安装一个新的驱动
打开SCM后,我们可以用CreateService函数将驱动添加到服务数据库中,这里是该函数的原型,CreateService函数远不止三个参数,但不要害怕,这些参数都是很简单的:
CreateService proto hSCManager:HANDLE, lpServiceName:LPSTR, lpDisplayName:LPSTR, \
dwDesiredAccess:DWORD, dwServiceType:DWORD, dwStartType:DWORD, \
dwErrorControl:DWORD, lpBinaryPathName:LPSTR, lpLoadOrderGroup:LPSTR, \
lpdwTagId:LPDWORD, lpDependencies:LPSTR, lpServiceStartName:LPSTR, \
lpPassword:LPSTR
参数说明如下:
◎ hSCManager--不用说了吧?就是上一节中得到的SCM句柄
◎ lpServiceName--指向一个以0字符结尾的表示服务名称的字符串,字符串的最大长度是256个字符,名称中不允许使用/或者\字符(因为这些字符会和注册表的路径表示方式冲突),这个值和注册表中的键名是相对应的
◎ lpDisplayName--指向一个以0字符结尾表示服务名称的字符串,这个名称是供用户界面程序识别函数时使用的,同样,它的最大长度也是256个字符。这个值和注册表中的DisplayName键的值是相对应的
◎ dwDesiredAccess--指定需要访问服务的操作,可以有以下取值:
• SERVICE_ALL_ACCESS--可以进行所有操作
• SERVICE_START--允许调用StartService函数来启动服务
• SERVICE_STOP--允许调用ControlService函数来停止服务
• DELETE--允许调用DeleteService函数来删除服务
在这里我们只需要做两件事情:启动驱动和删除驱动,所以例子中使用了SERVICE_START和DELETE,我们不需要停止服务的操作,因为上面已经说过,这个驱动在初始化的时候就会返回错误(所以它不会有已经启动的状态)。
◎ dwServiceType--服务的类型,我们的教程中只用得到SERVICE_KERNEL_DRIVER,这个值和注册表中的Type键的值是相对应的
◎ dwStartType--表示在什么时候启动服务,如果我们需要手动启动驱动的话,那么使用SERVICE_DEMAND_START参数,如果驱动程序需要在系统启动的时候就被启动,那么使用SERVICE_AUTO_START参数,这个取值和注册表中的Start键的取值是相对应的
◎ dwErrorControl--表示当驱动初始化的时候出错该如何处理,取值SERVICE_ERROR_IGNORE表示忽略错误,取值SERVICE_ERROR_NORMAL表示将错误记录到系统日志中去,这个取值和注册表中的ErrorControl键值是相对应的
◎ lpBinaryPathName--指向以0结尾的表示驱动程序文件名的字符串,这个值和注册表中的ImagePath的键值是相对应的
◎ lpLoadOrderGroup--指向以0结尾的表示组名称的字符串,表示该驱动属于哪个组,既然我们的例子程序不属于任何组,那么这里就用NULL好了
◎ lpdwTagId--指向一个32位的缓冲区,用来接收驱动在lpLoadOrderGroup参数指定的组中的唯一的标识,我们的例子中不需要用到这个表示,所以参数指定为NULL
◎ lpDependencies--对于驱动程序来说,这个参数没什么用途,设置为NULL好了
◎ lpServiceStartName--指向一个以0结尾的表示帐号名称的字符串,用于指定服务允许在哪个帐号下运行,如果服务类型是SERVICE_KERNEL_DRIVER的话,该帐号就是系统装入服务的模块名称,我们在这里使用NULL,表示由默认的模块装入
◎ lpPassword--对于驱动程序来说,这个参数没什么用途,设置为NULL好了
现在来总结一下,最后的5个参数总是设置为NULL,我们就把它抛到脑后去好了,第一个参数是SCM句柄,而dwDesiredAccess参数也是很好理解的,剩下的参数是什么?聪明的你一定已经猜到了--它们实际上就是和注册表里面的键一一对应的!看看下表就明白了:
CreateService函数的参数 注册表
----------------------- -------------
lpServiceName 键名
lpDisplayName DisplayName
dwServiceType Type
dwStartType Start
dwErrorControl ErrorControl
lpBinaryPathName ImagePath
表2.1 参数和注册表键的对应关系
好了,现在回过头来看看例子代码:
push eax
invoke GetFullPathName,$CTA0("beeper.sys"),sizeof acDriverPath,addr acDriverPath,esp
pop eax
invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
首先,我们调用GetFullPathName函数来获取全路径的驱动程序文件名,并把它传递给CreateService函数。
然后CreateService函数将这个驱动程序加入到SCM的数据库中,并创建对应的注册表键,正如表2.1所示的,所有这些键将被CreateService函数加入到注册表中,如果你在源代码中把DeleteService一行去掉,将csp.asm重新编译并执行,就可以验证我说的了。
不要认为使用RegXXX之类的函数将相同的信息写入注册表就可以达到相同的结果,这样操作的话,键值是写到注册表里面了,但是SCM的数据库里面可什么都没有哦!
如果SCM数据库中指定的设备驱动程序已经存在,那么CreateService函数会返回一个错误,这时可以调用GetLastError函数获取具体原因,上例中会得到ERROR_SERVICE_EXISTS。如果CreateService函数成功地将驱动加入到了SCM数据库中,函数的返回值就是驱动的句柄,这个句柄在后面的驱动管理函数中将会被用到。
2.3.3 启动驱动程序
下一步要调用的函数是StartService,它的原型申明如下:
StartService proto hService:HANDLE, dwNumServiceArgs:DWORD, lpServiceArgVectors:LPSTR
参数说明如下:
◎ hService--就是上一小节中由CreateService返回的驱动的句柄
◎ dwNumServiceArgs--用于驱动程序的时候,这个参数总是设置为NULL
◎ lpServiceArgVectors--同上,也为NULL
启动驱动的方法就是这样的:
invoke StartService, hService, 0, NULL
StartService函数的执行过程和装入用户模式的DLL的过程类似,驱动程序文件的映像被装入到系统的地址空间中,文件可以被装入到任何地址中,然后系统会根据PE文件中的重定位表对其进行重定位操作,这样驱动程序的内存映像就被准备好了,接下来系统调用驱动的入口函数,也就是DriverEntry子程序,和装入DLL不同的是,DriverEntry子程序的执行是在系统进程的上下文中进行的。
StartService函数的调用是同步执行的,也就是说,只有驱动程序的DriverEntry过程返回后,函数才会返回(回想一下,如果函数不等人家执行完就直接返回了,那叫什么~~~那是异步!)。如果驱动初始化成功,那么DriverEntry过程应该返回STATUS_SUCCESS,这样StartService会返回一个非0值,这时,我们又回到了调用StartService的用户模式的上下文中了。
在这个例子中,我们并不关心StartService函数的返回值,理由前面已经说过了,那就是beeper驱动程序在DriverEntry中进行了发声音功能的演示,并返回一个错误码,后面再没有什么功能要做的了。
2.3.4 卸载驱动
怎样卸载驱动呢?
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
现在我们需要将系统恢复到以前的状态,调用DeleteService函数就可以将驱动从SCM数据库中删除,比较奇怪的是,并不需要将SCM句柄传递给DeleteService函数。
DeleteService函数的原型申明如下:
DeleteService proto hService:HANDLE
参数hService就是需要被卸载的服务的句柄
严格地说,这个函数并不真正将服务删除,它仅仅是将服务做了一个删除标志,只有当服务已经停止,并且服务的句柄被关闭后,SCM才真正将服务删除。调用了DeleteService函数后,我们还需要将服务的句柄保存以便在后面使用。如果再次调用DeleteService函数的话,函数会返回失败,这时用GetLastError得到的错误代码是ERROR_SERVICE_MARKED_FOR_DELETE。
现在我们不再需要和驱动程序通讯了,所以需要使用CloseServiceHandle函数将句柄关闭:
CloseServiceHandle proto hSCObject:HANDLE
参数hSCObject可以是服务或驱动的句柄,也可以是SCM数据库的句柄,驱动的句柄被关闭后,我们再次调用CloseServiceHandle函数来关闭SCM句柄。
2.4 字符串操作的宏
最后来解释一下源代码中的$CTA0是什么东东--这是一个宏,用来在只读数据段中定义一个以0结尾的字符串,它可以在invoke宏指令中使用,这不是唯一用到的宏,在\Macros\Strings.mac文件中还包括很多其他有用的宏,这些宏都是用于定义字符串的,文件中也有怎样使用它们的详细的解释。既然本教程的重点是讲述KMD的编程,那么我就不在这些宏上面做过多的解释了,但是后面的程序中有很多地方会用到它们。
3. 最简单的设备驱动程序
※ 和本章内容相关的源代码见:
KmdKit\examples\simple\Beeper
KmdKit\examples\simple\DateTime
3.1 如何编译和链接内核模式驱动程序
我总是把驱动程序的汇编源代码放到批处理文件中,这样的文件从内容上看是.asm和.bat文件的混合体,但是扩展名是.bat(注:读者在实际使用的时候是不是这样做完全可以根据个人喜好而定)
;@echo off
;goto make
.386 ; driver's code start
;::::::::::::::::::::::::::::::::
; the rest of the driver's code ;
;::::::::::::::::::::::::::::::::
end DriverEntry ; driver's code end
:make
set drv=drvname
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
如果你运行这个"自编译"的批处理文件的话,系统会做以下的事情:
对于汇编编译器来说,前面两行的命令是被注释掉的,所以会被忽略;但是对于命令解释器来说,这两行会被执行,因为命令解释器会忽略前面的分号。这样一来,执行会跳到最后面的:make标号处,接下来下面的编译和链接命令就会被执行,跳转语句和:make标号中间的汇编源代码会被忽略。另外,对于汇编编译器来说,所有end标号后面的内容都会被忽略,所以后面的:make及其后的内容会被编译器忽略。
这种方式用起来很方便,因为源代码和如何编译、链接程序的信息都被放在一起了,如果需要的话,读者也可以自己另外加些命令上去,在我编写的所有的驱动代码中,我都使用了这种方法。
下面我来解释一下:make后面的一些命令的含义。
set drv=drvname
这里定义一个环境变量,具体使用的时候用驱动的文件名来代替,下面的链接选项的含义是:
◎ /driver--告诉链接器创建Windows NT内核模式驱动程序,这个选项造成的最重要的影响是文件中会多出一个"INIT"节区(有关PE文件中节区等概念可以参考《Windows环境下32位汇编语言程序设计》一书的第17章:PE文件),另外还有".idata"节区,里面包含了一些IMAGE_IMPORT_DESCRIPTOR结构,指出了需要导入的函数和模块的名称。"INIT"节区的属性被标志为可丢弃,这样装载程序获取了相关的导入信息后,这个节区的内容即被丢弃
◎ /base:0x10000--将驱动映像的基地址设置为10000h
◎ /align:32--系统内存是很宝贵的,所以最好使用更有效的节区对齐数值
◎ /out:%dvr%.sys--链接器默认会创建以exe作为扩展名的文件,当指定了/DLL选项后,创建的是.dll文件,在这里,我们要强制让它创建以.sys为扩展名的文件
◎ /subsystem:native--在PE文件头中,有一个字段用来告诉映像装载程序使用哪个子系统:Win32、POSIX或者OS/2。我们需要为驱动指定合适的环境,当链接.exe或者.dll文件的时候,一般指定为Win32子系统。内核模式驱动程序不需要任何子系统,所以我们使用的参数是native
3.2 最简单的内核模式驱动程序
3.2.1 源代码
这里是一个最简单的内核模式驱动程序的源代码
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; simplest - Simplest possible kernel-mode driver
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; B U I L D I N G D R I V E R
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:make
set drv=simplest
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
3.2.2 DriverEntry过程
就像其他的可执行程序一样,每个驱动程序也有一个入口点,这是当驱动被装载到内存中时首先被调用的,驱动的入口点是DriverEntry过程(注:过程也就是子程序),DriverEntry这个名称只是一个标记而已,你可以把它命名为其他任何名字--只要它是入口点就行了。DriverEntry过程用来对驱动程序的一些数据结构进行初始化,它的函数原型定义如下:
DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING
不幸的是,Charles Simonyi创造的著名的"匈牙利表示法"并没有在DDK中使用,但是我还是在任何可能的地方使用,所以我为DriverObject和RegistryPath参数都加上了前缀。
PDRIVER_OBJECT和PUNICODE_STRING的定义可以分别在\include\w2k\ntddk.inc和\include\w2k\ntdef.inc中找到。
PDRIVER_OBJECT typedef PTR DRIVER_OBJECT
PUNICODE_STRING typedef PTR UNICODE_STRING
当I/O管理器调用DriverEntry过程的时候,它会传过来两个指针类型的参数,说明如下:
◎ pDriverObject--指向用于描述当前驱动的对象(所谓对象,在内存中也就表现为一个结构而已),这个对象刚被系统初始化。由于Windows NT是一个面向对象的操作系统,因此,驱动也是被作为一个对象来描述的,当驱动被装载到内存中的时候,系统会创建一个对象来描述这个驱动,对象在内存中的表示方式就是一个DRIVER_OBJECT结构(在\include\w2k\ntddk.inc中定义),pDriverObject参数指向这个对象,以便让驱动有存取它的机会,但我们现在还没必要用到它
◎ pusRegistryPath--指向一个定长的Unicode字符串,内容是驱动的注册表键的路径,前面的章节中我们已经讨论过了驱动的注册表键。驱动程序可以用它来获取或者保存一些要用到的信息。如果在以后的执行中还要用到这个字符串,驱动程序应该保留一份该Unicode字符串的拷贝而不是仅仅保存这个指针,因为指针指向的内存在DriverEntry过程返回后即被释放掉了
定长的Unicode字符串是用UNICODE_STRING结构来表示的,和用户模式代码不同,内核模式的代码往往采用用UNICODE_STRING结构定义的字符串,该结构在\include\w2k\ntdef.inc中定义如下:
UNICODE_STRING STRUCT
_Length WORD ?
MaximumLength WORD ?
Buffer PWSTR ?
UNICODE_STRING ENDS
结构中的各字段含义如下:
◎ _Length--字符串的长度,以字节表示(而不是以字符数量表示),这个长度不包括末尾的0字符,由于Length是汇编的保留字,所以我不得不在前面加了一个下划线
◎ MaximumLength--字符串缓冲区的长度,也是以字节数表示
◎ Buffer--指向Unicode字符串,不要想当然地认为这个字符串就是以0结尾的,很多时候尾部并没有0
这种结构的优点在于它清楚地表现出了字符串的当前长度和最大的可能长度,这样就允许对它进行一些运算(比如在后面加上一些字符等)。
前面举例的驱动程序是最最简单的,它仅仅可以被装载而已,但是即使被装载,它除了返回一个STATUS_DEVICE_CONFIGURATION_ERROR错误代码(全部代码的列表可以见\include\w2k\ntstatus.inc文件)外什么都不干;在这里如果返回的是STATUS_SUCCESS,那么驱动会保留在内存中,但是你却无法卸载它,因为程序中缺少了负责卸载的DriverUnload过程。
读者可以用KmdManager工具来注册以及装载任何驱动程序。
3.3 Beeper驱动程序
3.3.1 源代码
现在来看看Beeper驱动程序的源代码,在"服务"一节中我们已经看到过它的控制程序了。
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; beeper - Kernel Mode Drive
; Makes beep thorough computer speaker
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\hal.inc
includelib \masm32\lib\w2k\hal.lib
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; E Q U A T E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
TIMER_FREQUENCY equ 1193167 ; 1,193,167 Hz
OCTAVE equ 2 ; octave multiplier
PITCH_C equ 523 ; C - 523,25 Hz
PITCH_Cs equ 554 ; C# - 554,37 Hz
PITCH_D equ 587 ; D - 587,33 Hz
PITCH_Ds equ 622 ; D# - 622,25 Hz
PITCH_E equ 659 ; E - 659,25 Hz
PITCH_F equ 698 ; F - 698,46 Hz
PITCH_Fs equ 740 ; F# - 739,99 Hz
PITCH_G equ 784 ; G - 783,99 Hz
PITCH_Gs equ 831 ; G# - 830,61 Hz
PITCH_A equ 880 ; A - 880,00 Hz
PITCH_As equ 988 ; B - 987,77 Hz
PITCH_H equ 1047 ; H - 1046,50 Hz
; We are going to play c-major chord
TONE_1 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)
TONE_2 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)
TONE_3 equ (PITCH_G*OCTAVE) ; for HalMakeBeep
DELAY equ 1800000h ; for my ~800mHz box
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; M A C R O S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DO_DELAY MACRO
mov eax, DELAY
.while eax
dec eax
.endw
ENDM
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; MakeBeep1
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
MakeBeep1 proc dwPitch:DWORD
; Direct hardware access
cli
mov al, 10110110y
out 43h, al
mov eax, dwPitch
out 42h, al
mov al, ah
out 42h, al
; Turn speaker ON
in al, 61h
or al, 11y
out 61h, al
sti
DO_DELAY
cli
; Turn speaker OFF
in al, 61h
and al, 11111100y
out 61h, al
sti
ret
MakeBeep1 endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; MakeBeep2
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
MakeBeep2 proc dwPitch:DWORD
; Hardware access using WRITE_PORT_UCHAR and READ_PORT_UCHAR
; functions from hal.dll
cli
invoke WRITE_PORT_UCHAR, 43h, 10110110y
mov eax, dwPitch
invoke WRITE_PORT_UCHAR, 42h, al
mov eax, dwPitch
invoke WRITE_PORT_UCHAR, 42h, ah
; Turn speaker ON
invoke READ_PORT_UCHAR, 61h
or al, 11y
invoke WRITE_PORT_UCHAR, 61h, al
sti
DO_DELAY
cli
; Turn speaker OFF
invoke READ_PORT_UCHAR, 61h
and al, 11111100y
invoke WRITE_PORT_UCHAR, 61h, al
sti
ret
MakeBeep2 endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
invoke MakeBeep1, TONE_1
invoke MakeBeep2, TONE_2
; Hardware access using hal.dll HalMakeBeep function
invoke HalMakeBeep, TONE_3
DO_DELAY
invoke HalMakeBeep, 0
mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
:make
set drv=beeper
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
这个驱动程序会使用主板上的扬声器来播放C大调的几个音符,为了实现这个功能,程序使用了IN和OUT指令来访问I/O端口。众所周知的是,Windows NT把I/O端口当作重要的资源来保护,任何用户模式的程序如果使用了IN或者OUT指令来访问端口的话,会被Windows立马枪毙掉。但事实上还是有办法绕过这个限制使用户模式的程序直接访问I/O端口,我们马上会谈到这一点。
3.3.2 控制系统定时器
计算机中有三个定时器,分别是定时器0、1和2,由可编程定时芯片(Programmable Interval Timer/PIT)实现,其中定时器2用于发声,发声的频率取决于定时器计数器的初始设置值,定时器会将计数值从初始值开始递减到0,然后将计数值复原为初始值,以此循环。计数值的递减由频率为1,193,180 Hz的系统振荡器控制着,该频率值在所有的PC家族的机器中是固定的。振荡器每产生一个脉冲,计数值就减一,为了发出不同频率的声音,我们只需要设定不同的初始值即可,发声频率和初始值的关系是:声音频率=1193180/初始值。关于这方面更进一步的知识,读者可以在网上搜到更多的内容。
这里还有个微妙的区别,kernel32.dll中的QueryPerformanceFrequency函数的允许最大值等于1193180,但是hal.dll里面的HalMakeBeep函数使用的数值却是1193167,在这里我将使用这个数值,我不知道这是不是误差补偿的结果,但这并不妨碍我们的程序发出声音来。
好!现在我们用MakeBeep1子程序来发第一个C大调音符。
mov al, 10110110y
out 43h, al
首先,我们要设置定时器的控制寄存器,也就是将2进制值的10110110送到43h端口。
mov eax, dwPitch
out 42h, al
mov al, ah
out 42h, al
然后,我们用两个连续的操作将初始值的低位字节和高位字节送到42h端口。
in al, 61h
or al, 11y
out 61h, al
现在要将扬声器打开,这可以通过将61h端口的位0和位1设置为1来完成,不出意外的话,现在应该能够听到声音了。
DO_DELAY MACRO
mov eax, DELAY
.while eax
dec eax
.endw
ENDM
为了让声音延续一段时间,我们用DO_DELAY宏来进行一些延时,虽然这种延时方法有点过时,但还是很有效的。
in al, 61h
and al, 11111100y
out 61h, al
现在可以关闭扬声器了,千万别忘了扬声器是整个系统的资源哦,这只要将端口61h的位0和位1清零就好了。在程序中我们用了cli指令清除中断允许标志来关闭中断,这在多处理器的机器上会对其他程序有所影响的。
接下来,我们用MakeBeep2子程序来发第2个音符--C大调的mi音,实现上的区别在于这次使用hal.dll中的WRITE_PORT_UCHAR和READ_PORT_UCHAR函数来代替in/out指令。HAL隐藏了和硬件相关的细节,如操作I/O端口的方法等,这样代码就可以做到和机器无关。
第3个音符--C大调的so音符--则是用hal.dll中的HalMakeBeep函数来实现的,这个函数的参数不是定时器的初始值,而是发声的频率。
在Beeper.bat文件的前面部分,读者可以发现12个频率定义值,程序中只用到了3个,读者可以自行用剩余的定义去写一个合成器,要关闭扬声器的话,只要用参数0来再次调用HalMakeBeep就行了。
Beeper驱动程序的DriverEntry过程返回一个错误值,所以系统直接就把它从系统中清除掉了。当然,在以后的全功能驱动程序里面,这里应该返回STATUS_SUCCESS值。
3.3.3 自动启动驱动程序
scp.exe程序安装驱动程序的时候使用的是手动启动模式,前面我们也讨论过其他的启动方式,现在来试试让它随系统的启动而自动启动,有很多中办法可以实现这个功能,最简单的就是将源代码中调用DeleteService的一行去掉,然后将 SERVICE_DEMAND_START参数改为SERVICE_AUTO_START,并将SERVICE_ERROR_IGNORE参数改为 SERVICE_ERROR_NORMAL,再重新编译即可。
这样,运行scp.exe并退出后,注册表信息还会保留着。你可以尽管将它忘掉,但是每次系统启动的时候,beeper.sys会不厌其烦地通知你一下它还活着,而且在系统日志中,你也可以找到它的启动错误记录,在控制面板->管理工具->事件查看器中,选择系统日志,双击日志,就可以看到下面的信息:
图3.1 系统错误日志
最好别忘了将注册表中的对应键值去掉,否则在每次启动的时候你都会听到声音的。
3.4 Giveio驱动程序例子
3.4.1 Giveio驱动程序的控制代码
现在来写另一个SCP代码,以便用来控制一个新的例子--giveio.sys驱动程序:
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DateTime - Service Control Program for giveio driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
include \masm32\Macros\Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; M A C R O S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
CMOS MACRO by:REQ
mov al, by
out 70h, al
in al, 71h
mov ah, al
shr al, 4
add al, '0'
and ah, 0Fh
add ah, '0'
stosw
ENDM
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DateTime
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DateTime proc uses edi
local acDate[16]:CHAR
local acTime[16]:CHAR
local acOut[64]:CHAR
; See Ralf Brown's Interrupt List for details
;:::::::::::::::::: Set data format ::::::::::::::::::
mov al, 0Bh ; status register B
out 70h, al
in al, 71h
push eax ; save old data format
and al, 11111011y ; Bit 2: Data Mode - 0: BCD, 1: Binary
or al, 010y ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode
out 71h, al
;:::::::::::::::::::: Lets' fetch current date ::::::::::::::::::::
lea edi, acDate
CMOS 07h ; date of month
mov al, '.'
stosb
CMOS 08h ; month
mov al, '.'
stosb
CMOS 32h ; two most significant digit od year
CMOS 09h ; two least significant digit od year
xor eax, eax ; terminate string with zero
stosb
;:::::::::::::::::::: Lets' fetch current time :::::::::::::::::::
lea edi, acTime
CMOS 04h ; hours
mov al, ':'
stosb
CMOS 02h ; minutes
mov al, ':'
stosb
CMOS 0h ; seconds
xor eax, eax ; terminate string with zero
stosb
;:::::::::::::: restore old data format :::::::::::::
mov al, 0Bh
out 70h, al
pop eax
out 71h, al
;::::::::::::::::: Show current date and time :::::::::::::::
invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK
ret
DateTime endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; start
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: start proc
local fOK:BOOL
local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR
local hKey:HANDLE
local dwProcessId:DWORD
and fOK, 0 ; assume an error
; Open the SCM database
invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
.if eax != NULL
mov hSCManager, eax
push eax
invoke GetFullPathName, $CTA0("giveio.sys"),sizeof acDriverPath,addr acDriverPath, esp
pop eax
; Register driver in SCM active database
invoke CreateService,hSCManager,$CTA0("giveio"),$CTA0("Current Date and Time fetcher."), \
SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
$CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"),\
0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
.if eax == ERROR_SUCCESS
; Add current process ID into the registry
invoke GetCurrentProcessId
mov dwProcessId, eax
invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId),NULL,REG_DWORD, \
addr dwProcessId, sizeof DWORD
.if eax == ERROR_SUCCESS
; Start driver
invoke StartService, hService, 0, NULL
inc fOK ; Set OK flag
invoke RegDeleteValue, hKey, addr szProcessId
.else
invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
NULL, MB_ICONSTOP
.endif
invoke RegCloseKey, hKey
.else
invoke MessageBox, NULL, $CTA0("Can't open registry."), NULL, MB_ICONSTOP
.endif
; Remove driver from SCM database
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
.else
invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
NULL, MB_ICONSTOP
.endif
; If OK display current date and time to the user
.if fOK
invoke DateTime
.endif
invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start
3.4.2 使用注册表向驱动程序传递信息
这个例子中只有一点点新东西:
invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
$CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
.if eax == ERROR_SUCCESS
invoke GetCurrentProcessId
mov dwProcessId, eax
invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
addr dwProcessId, sizeof DWORD
.if eax == ERROR_SUCCESS
invoke StartService, hService, 0, NULL
在启动驱动前,我们在该驱动的注册表键中创建了一个名为ProcessId的键值,里面的数据是当前进程的进程ID,也就是SCP程序自己的进程ID,请注意我在这里使用$CTA0宏的方法,我使用了标号szProcessId,用这个标号指定了一个字符串"ProcessId",我们在后面还要用到这个标号,看来我的字符串宏真是太灵活了哇哈哈哈哈~~~(俄国人也会得意忘形呀!?)
一旦注册表值成功创建,我们就可以启动驱动程序了,这个额外的注册表键是干什么的呢?谜底后面就会揭晓!
inc fOK
invoke RegDeleteValue, hKey, addr szProcessId
.else
invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
NULL, MB_ICONSTOP
.endif
invoke RegCloseKey, hKey
当StartService函数返回后,我们可以认为驱动程序已经圆满地完成了任务,然后就将fOK标志设上,接下来调用RegDeleteValue的一句是可有可无的,因为下面调用DeleteService函数后,驱动注册表键下的所有键值都会被清除掉,但是从编程风格方面来说,明确地写上删除语句是一个很好的习惯。
.if fOK
invoke DateTime
.endif
在关闭所有SCM的句柄后,如果fOK标志位已经设置的话,程序就调用DateTime子程序。
3.4.3 存取CMOS
计算机主板上有块小芯片是用来存放系统配置信息的,如硬盘参数、内存配置以及系统时间等等,这块芯片一般被叫做"CMOS"(CMOS是Complementary Metal Oxide Semiconductor的缩写)。CMOS芯片由电池供电,内部建有一个实时时钟(RTC),我们可以通过存取70h和71h号I/O端口来获取其中的数据,"Ralf Brown's Interrupt List"里面有其详细的格式说明,网址见http://www-2.cs.cmu.edu/afs/cs/user/ralf/pub/WWW/files.html(注:http://asm.yeah.net上面有已经整理成单个hlp文件的全部上述资料)。
mov al, 0Bh ; status register B
out 70h, al
in al, 71h
push eax ; save old data format
and al, 11111011y ; Bit 2: Data Mode - 0: BCD, 1: Binary
or al, 010y ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode
out 71h, al
首先,我们通过状态寄存器B来设置一个便于使用的数据格式。使用CMOS宏可以从CMOS获取数据并同时将它的转换成我们需要的格式。
invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK
当获取到所有的相关数据后,程序的输出如下:
图3.2 DateTime.exe程序的输出结果
这个程序中最奇怪的事情就是我们竟然可以直接访问CMOS而不被系统阻止,前面已经说过,如果用户模式程序在Windows NT操作系统下使用IN或者OUT指令来存取I/O端口的话会被系统终止,但是我们却可以,这怎么可能呢?这是因为我运行程序前刚喝了脑x金!广告里面天天说,脑x金无所不能!~~~~~~呵呵,开个玩笑,这当然是因为有了Giveio驱动程序。
3.5 Giveio设备驱动程序
3.5.1 Giveio驱动程序的源代码
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; giveio - Kernel Mode Driver
; Demonstrate direct port I/O access from a user mode
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; E Q U A T E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
IOPM_SIZE equ 2000h ; sizeof I/O permission map
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
local status:NTSTATUS
local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE
local kvpi:KEY_VALUE_PARTIAL_INFORMATION
local pIopm:PVOID
local pProcess:LPVOID
invoke DbgPrint, $CTA0("giveio: Entering DriverEntry")
mov status, STATUS_DEVICE_CONFIGURATION_ERROR
lea ecx, oa
InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL
invoke ZwOpenKey, addr hKey, KEY_READ, ecx
.if eax == STATUS_SUCCESS
push eax
invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
pop ecx
.if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )
invoke DbgPrint, $CTA0("giveio: Process ID: %X"), \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data
; Allocate a buffer for the I/O permission map
invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
mov pIopm, eax
lea ecx, kvpi
invoke PsLookupProcessByProcessId, \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
.if eax == STATUS_SUCCESS
invoke DbgPrint, $CTA0("giveio: PTR KPROCESS: %08X"), pProcess
invoke Ke386QueryIoAccessMap, 0, pIopm
.if al != 0
; I/O access for 70h port
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
invoke Ke386SetIoAccessMap, 1, pIopm
.if al != 0
invoke Ke386IoSetAccessProcess, pProcess, 1
.if al != 0
invoke DbgPrint, $CTA0("giveio: I/O permission is successfully given")
.else
invoke DbgPrint, $CTA0("giveio: I/O permission is failed")
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
invoke ObDereferenceObject, pProcess
.else
mov status, STATUS_OBJECT_TYPE_MISMATCH
.endif
invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
mov status, STATUS_INSUFFICIENT_RESOURCES
.endif
.endif
invoke ZwClose, hKey
.endif
invoke DbgPrint, $CTA0("giveio: Leaving DriverEntry")
mov eax, status
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
:make
set drv=giveio
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
这个驱动的代码是基于Dale Roberts写的一个著名的例子(giveio)改编的,我决定使用它是因为它在这篇教程中使用真是太合适了!
3.5.2 I/O许可位图
谜底揭晓了:我们的驱动程序修改了I/O许可位图(I/O permission bit map,IOPM),这样进程就被允许自由地存取I/O端口,这方面详细资料见http://www.intel.com/design/intarch/techinfo/pentium/PDF/inout.pdf
每个进程都有自己的I/O许可位图,每个单独的I/O端口的访问权限都可以对每个进程进行单独授权,如果相关的位被设置的话,对对应端口的访问就是被禁止的,如果相关的位被清除,那么进程就可以访问对应的端口。既然I/O地址空间由64K个可单独寻址的8位I/O端口组成,IOPM表的最大尺寸就是2000h字节(注:每个端口的权限用1个bit表示,64K个端口除以8得到的就是IOPM的字节数,也就是65536/8=8192字节=2000h字节)。
TSS的设计意图是为了在任务切换的时候保存处理器状态,从执行效率的考虑出发,Windows NT并没有使用这个特征,它只维护一个TSS供多个进程共享,这就意味着IOPM也是共享的,因此某个进程改变了IOPM的话,造成的影响是系统范围的。
ntoskrnl.exe中有些未公开的函数是用来维护IOPM的,它们是Ke386QueryIoAccessMap和Ke386SetIoAccessMap函数。
Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID
Ke386QueryIoAccessMap函数从TSS中拷贝2000h字节的当前IOPM到指定的内存缓冲区中,缓冲区指针由pIopm参数指定。
各参数描述如下:
◎ dwFlag--0表示将全部缓冲区用0FFh填写,也就是所有的位都被设置,所有的端口都被禁止访问;1表示从TSS中将当前IOPM拷贝到缓冲区中
◎ pIopm--用来接收当前IOPM的缓冲区指针,注意缓冲区的大小不能小于2000h字节
如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID
Ke386SetIoAccessMap函数刚好相反,它从pIopm参数指定的缓冲区中拷贝2000h字节的IOPM到TSS中去。
各参数描述如下:
◎ dwFlag--这个参数只能是1,其他任何值函数都会返回失败
◎ pIopm--指向包含IOPM数据的缓冲区,缓冲区的尺寸不能小于2000h字节
如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
当IOPM拷贝到TSS后,IOPM的偏移指针必须被定位到新的数据中去,这可以通过Ke386IoSetAccessProcess函数来完成,这也是ntoskrnl.exe中的一个很有用的未公开函数。
Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD
Ke386IoSetAccessProcess允许或者禁止对进程使用IOPM。其参数说明如下:
◎ pProcess--指向KPROCESS结构(后面会对这个结构进行解释)
◎ dwFlag--0表示禁止对I/O端口进行存取,将IOPM的偏移指针指到TSS段外面;1表示允许存取I/O端口,将IOPM的偏移指针指到TSS段的88h中
如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
顺便提一下,ntoskrnl中的所有函数都有前缀,通过这个前缀你就可以辨别该函数属于系统功能中的哪一类。不同的前缀表示不同的功能--如i前缀表示内部使用(internal)、p表示私有函数(private)、f表示fastcall。再如,Ke表示内核函数(kernel),Psp表示内部进程支持函数(internal process support),Mm表示内存管理函数(Memory Manager)等等。
Ke386IoSetAccessProcess函数的第一个参数指向进程对象,也就是KPROCESS结构(在\include\w2k\w2kundoc.inc中定义),我特地在文件名中加了个w2k前缀是因为在不同的Windows NT版本中,未公开的数据结构的定义可能会有所变动,所以将使用这个include文件后编出的驱动程序在XP中使用可能不是个好主意。Ke386IoSetAccessProcess会将KPROCESS结构中IopmOffset字段的值设置为合适的值。
3.5.3 从注册表中读取信息
在调用Ke386IoSetAccessProcess的时候要用到进程对象的指针,有好几种办法可以获得该指针,我选择了最简单的办法--使用进程ID,这就是DateTime.exe程序首先获取当前进程的ID并把它保存在注册表中的原因。在这里我们使用注册表在用户模式的代码以及内核模式的驱动程序之间传递参数。由于DriverEntry过程是在系统进程环境中运行的,所以不这样做的话,我们无法得知它究竟是被哪个进程所启动的。
DriverEntry过程的第二个参数--pusRegistryPath是指向注册表键路径的字符串,我们可以用它来获取保存的进程ID。
现在来看看这一切是怎么实现的:
lea ecx, oa
InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL
在调用ZwOpenKey函数前,我们必须先初始化OBJECT_ATTRIBUTES结构(在\include\w2k\ntdef.inc中定义),我用InitializeObjectAttributes宏来完成这个功能,但读者以后最好手工来完成这个工作,因为InitializeObjectAttributes宏可能并不像你预料的那样运行(注:宏中间可能用到很多寄存器,但是有些寄存器的值可能是你需要保存的,过多的使用宏以后,寄存器在哪里被修改了你都不知道),你也可以这样做:
lea ecx, oa
xor eax, eax
assume ecx:ptr OBJECT_ATTRIBUTES
mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
mov [ecx].RootDirectory, eax ; NULL
push pusRegistryPath
pop [ecx].ObjectName
mov [ecx].Attributes, eax ; 0
mov [ecx].SecurityDescriptor, eax ; NULL
mov [ecx].SecurityQualityOfService, eax ; NULL
assume ecx:nothing
ZwOpenKey函数的第一个参数指向一个变量,用来返回注册表键的句柄;第二个参数是存取注册表的权限,你应该注意到ecx中保存有指向已经初始化的该注册表键的对象属性结构的指针。
invoke ZwOpenKey, addr hKey, KEY_READ, ecx
.if eax == STATUS_SUCCESS
push eax
invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
pop ecx
ZwQueryValueKey返回注册表键的句柄,我们可以用它从注册表中得到保存的进程ID,该函数的第二个参数是要获取数据的注册表键的名称,例子程序中我使用了$CCOUNTED_UNICODE_STRING宏来定义UNICODE_STRING结构和里面的字符串数据,如果你不喜欢使用宏,那么可以用下面的常规方法:
usz dw 'U', 'n', 'i', 'c', 'o', 'd', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 0
us UNICODE_STRING {sizeof usz - 2, sizeof usz, offset usz}
但是我可不喜欢这么麻烦,所以我写了下面这些宏:COUNTED_UNICODE_STRING、$COUNTED_UNICODE_STRING、CCOUNTED_UNICODE_STRING和$CCOUNTED_UNICODE_STRING(定义在\Macros\Strings.mac中)。
第三个参数指明要获取的数据类型,KeyValuePartialInformation是一个常量(在\include\w2k\ntddk.inc中定义);第四和第五个参数是指向KEY_VALUE_PARTIAL_INFORMATION结构的指针以及结构的长度,在结构的Data字段中我们会得到我们需要的进程ID;最后一个参数是指向返回数据字节数的指针,把它指向堆栈的话,就不必重新再分配一个变量了。
(注:原文作者这里用的一个小技巧可以学习一下,前面一句push eax,然后后面调用的最后一个参数是esp,在这里压入堆栈的堆栈指针正是指向push eax指令保留的堆栈空间,所以函数执行的时候在这里返回一个值,后面一句pop ecx实际上是弹出了函数返回在里面的数值,千万不要认为弹出的原始的eax值,这种方法可以免去定义一个临时使用的变量的麻烦,当然限制条件就是该临时变量的指针参数必须是函数的最后一个参数才行。这个技巧在整个教程中多次用到,请注意理解!)
3.5.4 让用户模式的进程可以存取I/O端口
.if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )
invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
mov pIopm, eax
调用ZwQueryValueKey成功后,我们使用MmAllocateNonCachedMemory 函数来申请一段不被cache的虚拟内存地址空间,供IOPM数据使用。
lea ecx, kvpi
invoke PsLookupProcessByProcessId, \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
.if eax == STATUS_SUCCESS
invoke Ke386QueryIoAccessMap, 0, pIopm
然后将进程ID传递给PsLookupProcessByProcessId函数,这样就可以在pProcess中得到指向进程对象的指针,后面的Ke386QueryIoAccessMap的函数将IOPM拷贝到缓冲区中。
.if al != 0
mov ecx, pIopm
add ecx, 70h / 8
mov eax, [ecx]
btr eax, 70h MOD 8
mov [ecx], eax
mov ecx, pIopm
add ecx, 71h / 8
mov eax, [ecx]
btr eax, 71h MOD 8
mov [ecx], eax
invoke Ke386SetIoAccessMap, 1, pIopm
.if al != 0
invoke Ke386IoSetAccessProcess, pProcess, 1
.if al != 0
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
现在将70h和71h号端口对应的数据位清除,并使用Ke386IoSetAccessProcess函数将修改后的IOPM写回去,以便存取这两个端口。
invoke ObDereferenceObject, pProcess
.else
mov status, STATUS_OBJECT_TYPE_MISMATCH
.endif
先前对PsLookupProcessByProcessId函数的调用会使进程对象的引用计数加1,每次当一个对象被引用的时候,对象管理器将对象的引用计数加1,并返回对象的指针;当内核模式程序完成对一个对象的操作后,应该再次调用对象管理器将对象的引用计数减1。同样道理,每次对象的句柄被获取后,引用计数也会被加1,当对象句柄被关闭后,引用计数减1,这是因为句柄也是用来访问对象的。所以,即使是一个对象的所有句柄都被关闭,如果系统还要用到这个对象的话,那么它的引用计数可能还是正数。只有当对象的引用计数减少到0的时候,对象管理器才将对象从内存中删除。
调用ObDereferenceObject函数可以将对象的引用计数减1,它的返回值是对象的先前状态。
invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
mov status, STATUS_INSUFFICIENT_RESOURCES
.endif
.endif
invoke ZwClose, hKey
.endif
接下来调用MmFreeNonCachedMemory来释放内存,并调用ZwClose函数关闭注册表句柄。
到这里为止,所有的工作都完成了,虽然驱动还是返回一个错误代码,系统还是会将它直接从内存中清除,但是用户模式的进程已经可以直接访问2个I/O端口了。
在这个例子中我们举例访问了CMOS,通过同样的办法改造前面的beeper.sys例子,我们也可以在用户模式进程中让系统扬声器发声。但是要记住的是:你还是无法直接在用户模式进程中使用类似于cli或者sti之类的特权指令,也不能直接调用hal.dll之类的dll中的函数,因为它们是在内核地址空间中运行的。你唯一能做到的就是用这种技巧访问所有的65535个I/O端口而已。
invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
mov pIopm, eax
invoke RtlZeroMemory, pIopm, IOPM_SIZE
lea ecx, kvpi
invoke PsLookupProcessByProcessId, \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
.if eax == STATUS_SUCCESS
invoke Ke386SetIoAccessMap, 1, pIopm
.if al != 0
invoke Ke386IoSetAccessProcess, pProcess, 1
.endif
invoke ObDereferenceObject, pProcess
.endif
invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
mov status, STATUS_INSUFFICIENT_RESOURCES
.endif
还要注意的是:访问系统扬声器或者访问CMOS都是很安全的,但是存取其他的I/O端口可能是很危险的,因为在用户模式下,你无法保证和其他进程的访问保持同步状态。
3.6 关于驱动程序的调试
现在可以更详细地讨论一下驱动的调试了,前面已经提到过,我们最好用SoftICE来调试驱动程序。
为了产生一个断点,读者可以在驱动代码的某处直接放上一个"Int 3"指令,"Int 3"会产生一个软件断点,这样就可以被SoftICE一类的内核调试器捕捉到。在此之前,要确定INT 3的捕捉是被打开的。在SoftICE的命令控制窗口中打I3HERE命令可以做到这一点,更详细的说明读者可以参考SoftICE的命令手册。要注意的是:如果没有调试器来捕捉断点的话,断点指令引起的结果就是一个BSOD蓝屏死机画面,所以在启动驱动前不要忘了先敲"i3here on"命令。在最新的SoftICE版本中,内核模式地址空间中的int 3捕捉默认就是打开的。
程序中还不时地调用了DbgPrint函数,这个函数会在调试器窗口中显示一个字符串,SoftICE可以很好地与之配合,你也可以用Mark Russinovich(www.sysinternals.com)写的DebugView软件来监视调试信息的输出。
4. I/O子系统
※ 和本节内容相关的源代码见KmdKit\examples\simple\VirtToPhys
4.1 I/O管理器
在用户模式下,我们可以通过访问某个地址来直接调用dll中的函数,与此不同的是,从系统的稳定性考虑,在内核模式下这样做的话是很危险的。所以,系统提供了和内核模式通讯的媒介--I/O管理器,它是I/O子系统的部件之一。I/O管理器将应用程序、系统部件和设备连接起来,并定义了一个架构来支持设备驱动程序。
图4.1是I/O管理器如何在用户模式程序和驱动程序之间进行沟通的简单图解。
图4.1 I/O子系统的简单架构
从上图可以看到,所有用户模式程序到设备(包括设备驱动程序)的调用都必须通过I/O管理器来完成。
一般来说,用户模式的操作都被转换成了对具体硬件设备的I/O操作,仅对于某些设备,设备由驱动程序来创建和控制,这些设备就是虚拟设备。当然,创建这些设备并不意味着你创造了什么硬件(不然我每天创建n个显卡再卖掉,然后换BMW,呵呵~~~~),而仅仅是在内存中创建了一个新的对象而已。每个对象和一个物理设备或者逻辑设备对应,用于描述它们的特征。
创建设备后,驱动程序告诉I/O管理器:"这里有个我控制的设备,如果你收到了操作这个设备的I/O请求的话,直接发给我好了,剩下的由我来搞定!"。驱动程序知道如何对自己管理的设备进行I/O操作,I/O管理器唯一的职责在于创建I/O请求并把它发送给适当的设备驱动程序。用户模式的代码不知道(也不必知道)其中的细节,也不用知道究竟是哪个驱动程序在管理哪个设备。
4.2 VirToPhys驱动程序的控制程序
4.2.1 控制程序源代码
严格地说,这个代码包括了注册和启动驱动程序,以及作为客户端程序和设备进行通讯的代码。
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; VirtToPhys.asm - Driver Control Program for VirtToPhys driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
include \masm32\include\winioctl.inc
include \masm32\Macros\Strings.mac
include common.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; BigNumToString
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
BigNumToString proc uNum:UINT, pszBuf:LPSTR
; This function accepts a number and converts it to a
; string, inserting commas where appropriate.
local acNum[32]:CHAR
local nf:NUMBERFMT
invoke wsprintf, addr acNum, $CTA0("%u"), uNum
and nf.NumDigits, 0
and nf.LeadingZero, FALSE
mov nf.Grouping, 3
mov nf.lpDecimalSep, $CTA0(".")
mov nf.lpThousandSep, $CTA0(" ")
and nf.NegativeOrder, 0
invoke GetNumberFormat, LOCALE_USER_DEFAULT, 0, addr acNum, addr nf, pszBuf, 32
ret
BigNumToString endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
start proc uses esi edi
local hSCManager:HANDLE
local hService:HANDLE
local acModulePath[MAX_PATH]:CHAR
local _ss:SERVICE_STATUS
local hDevice:HANDLE
local adwInBuffer[NUM_DATA_ENTRY]:DWORD
local adwOutBuffer[NUM_DATA_ENTRY]:DWORD
local dwBytesReturned:DWORD
local acBuffer[256+64]:CHAR
local acThis[64]:CHAR
local acKernel[64]:CHAR
local acUser[64]:CHAR
local acAdvapi[64]:CHAR
local acNumber[32]:CHAR
invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
.if eax != NULL
mov hSCManager, eax
push eax
invoke GetFullPathName, $CTA0("VirtToPhys.sys"), \
sizeof acModulePath, addr acModulePath, esp
pop eax
invoke CreateService, hSCManager, $CTA0("VirtToPhys"), \
$CTA0("Virtual To Physical Address Converter"), \
SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER,\
SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath,\
NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
; Driver's DriverEntry procedure will be called
invoke StartService, hService, 0, NULL
.if eax != 0
; Driver will receive I/O request packet (IRP) of type IRP_MJ_CREATE
invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ+GENERIC_WRITE,\
0, NULL, OPEN_EXISTING, 0, NULL
.if eax != INVALID_HANDLE_VALUE
mov hDevice, eax
lea esi, adwInBuffer
assume esi:ptr DWORD
invoke GetModuleHandle, NULL
mov [esi][0*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
mov [esi][1*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
mov [esi][2*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
mov [esi][3*(sizeof DWORD)], eax
lea edi, adwOutBuffer
assume edi:ptr DWORD
; Driver will receive IRP of type IRP_MJ_DEVICE_CONTROL
invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
esi, sizeof adwInBuffer, \
edi, sizeof adwOutBuffer, \
addr dwBytesReturned, NULL
.if ( eax != 0 ) && ( dwBytesReturned != 0 )
invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
addr acModulePath, sizeof acModulePath
lea ecx, acModulePath[eax-5]
.repeat
dec ecx
mov al, [ecx]
.until al == '\'
inc ecx
push ecx
CTA0 "%s \t%08Xh\t%08Xh ( %s )\n", szFmtMod
invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
pop ecx
invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
[esi][0*(sizeof DWORD)], \
[edi][0*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
[esi][1*(sizeof DWORD)], \
[edi][1*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
[esi][2*(sizeof DWORD)], \
[edi][2*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
[esi][3*(sizeof DWORD)], \
[edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acBuffer,\
$CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
addr acThis, addr acKernel, addr acUser, addr acAdvapi
assume esi:nothing
assume edi:nothing
invoke MessageBox,NULL,addr acBuffer,$CTA0("Modules Base Address"), \
MB_OK + MB_ICONINFORMATION
.else
invoke MessageBox,NULL,$CTA0("Can't send control code to device."),\
NULL,MB_OK + MB_ICONSTOP
.endif
; Driver will receive IRP of type IRP_MJ_CLOSE
invoke CloseHandle, hDevice
.else
invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
.endif
; DriverUnload proc in our driver will be called
invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss
.else
invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
.endif
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
.else
invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, \
MB_OK + MB_ICONSTOP
.endif
invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start
代码将输入的数据发送给设备,并将设备返回的数据格式化并显示出来,这里只有少量新的东西,它们是3个调用:CreateFile、DeviceIoControl和CloseHandle,这些调用的参数都是设备(注意:不是驱动)的句柄。
4.2.2 设备对象
在被装载后,VirtToPhys驱动程序创建了一个名为"devVirtToPhys"的设备(dev前缀并不是必须的,但我还是打算把它加上--原因后面再解释)。
设备的名称放在对象管理器中,对象管理器是系统中负责创建、删除、保护和跟踪对象的组件,根据约定,设备对象存放在\Device目录下,应用程序用Win32 API是无法直接访问的。
要想研究对象管理器维护的对象名称列表的话,可以使用工具Windows Object Explorer (WinObjEx)(见http://www.wasm.ru/)或者Mark Russinovich编写的Object Viewer(见http://www.sysinternals.com/)。
要观察VirtToPhys程序在你的计算机上创建的对象,仅仅需要运行VirtToPhys.exe程序,但是不要关闭窗口。
图4.2 对象管理器中的devVirtToPhys设备对象
图4.3 devVirtToPhys对象的属性
4.2.3 驱动程序对象
VirtToPhys驱动程序对象(注意这里没有任何前缀)被放在\Driver目录中。
图4.4 对象管理器中的VirtToPhys驱动程序对象
4.2.4 符号连接对象
内部设备名称无法被Win32应用程序使用,因为除\BaseNamedObjects和\??外的所有目录对用户模式程序来说都是不可见的--而设备名称又必须放置于对象管理器的特定目录下才行。
"\??"目录中包括了对真实存在的设备名称的符号链表,设备驱动程序负责在这个目录中创建一个符号连接,这样设备就可以被Win32应用程序访问。
因此,如果我们的驱动程序对应的设备对象希望被用户模式的代码打开的话,就必须在"\??"目录中创建一个符号连接,指向"\Device"目录中的设备对象,然后,当调用者需要获取设备句柄时,I/O管理器就能够找到它。
顺便提一句,你也可以在用户模式下通过QueryDosDevice函数检测符号连接是否存在,还可以通过DefineDosDevice函数来修改它。
打开"\??"目录后,你可以看到大量的符号连接,早先在Windows NT 4系统中,这个目录的名称为\DosDevices,后来才更名为"\??",这主要是基于性能上的考虑,因为这样目录名称按照字母表顺序会排在最前面。
为了向前兼容,对象管理器中的"\DosDevices"目录直接连到"\??"目录。
VirtToPhys驱动程序在"\??"目录中创建了指向"devVirtToPhys"设备的符号连接"slVirtToPhys",真实设备的全名是"\Device\devVirtToPhys",这里我用了"dev"前缀。
图4.5 对象管理器中的slVirtToPhys符号连接
图4.6 slVirtToPhys符号连接的对象属性
为各种对象的名称加上前缀是为了区别它们,最好不要将设备名、符号连接名称和驱动的名称混淆起来。另外,一个要点是符号连接名称必须定向到一个有效的设备名上面;另一个要点是,在同一个设备目录下不能有两个同名的对象,正像文件系统的一个目录下不能有两个同名文件一样。
这样,当StartService函数执行后,系统中就多了三个新的对象:"\Driver\VirtToPhys"驱动、"\Device\devVirtToPhys"设备和符号连接"\??\slVirtToPhys"。
不知道读者是否还记得,第二课"服务"中曾经提到过"\??",那是在驱动的路径名中出现的,如"\??\C:\masm32\...",实际上,"\??\C:"是一个符号连接,它指向名为"\Device\HarddiskVolume1"的内部设备,也就是系统中第一个物理硬盘上面的第一个分区。
4.2.5 文件对象
现在回头来看看源代码,当驱动被启动后,我们打算去调用它。为了完成这个功能,我们只需使用CreateFile函数来打开驱动,以此获得一个文件句柄。
文档中关于CreateFile的描述真是太多了,但是只有很少一部分是和设备驱动程序相关的。
CreateFile proto stdcall lpFileName:LPCSTR, dwDesiredAccess:DWORD, \
dwShareMode:DWORD, lpSecurityAttributes:LPVOID, \
dwCreationDistribution:DWORD, dwFlagsAndAttributes:DWORD, \
hTemplateFile:HANDLE
这个函数可以创建或者打开一个已存在的对象,而不仅仅是文件(这似乎和它的名字不符,但是有很多Create打头的函数都有创建和打开的功能),微软其实可以干脆把它命名为CreateObject好了。
在这里,设备也可以作为对象来考虑。
函数的参数描述如下:
◎ lpFileName--指向以0结尾的表示设备名称的字符串,这里要用到指向设备对象的符号连接名
◎ dwDesiredAccess--指定访问设备的方式,可以有两个取值:GENERIC_READ表示写操作,允许将数据写到设备中;GENERIC_WRITE表示读操作,允许从设备读取数据,这两个值可以合并起来使用
◎ dwShareMode--指定设备是否可以被共享,0表示设备不能被共享,这样并发的访问会失败,直到句柄被关闭为止;要共享设备的话,可以指定下面的两个值:FILE_SHARE_READ表示可以并发地读取设备,FILE_SHARE_WRITE表示可以并发地写设备
◎ lpSecurityAttributes--指向SECURITY_ATTRIBUTES结构的指针,在此无用,所以可以指定为NULL
◎ dwCreationDistribution--指明当文件存在或不存在时函数采取的动作,对于设备来说,这个参数应该使用OPEN_EXISTING
◎ dwFlagsAndAttributes--文件属性,在这里总是使用0
◎ hTemplateFile--指定文件模板的句柄,在这里总是使用0
如果CreateFile函数成功地创建或者打开了指定的设备,那么返回值就是设备句柄,否则返回值是INVALID_HANDLE_VALUE。大部分需要返回句柄的Windows函数在失败时返回0,但是CreateFile函数返回的INVALID_HANDLE_ VALUE值的定义却是-1。
我们这样使用CreateFile函数:
invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
0, NULL, OPEN_EXISTING, 0, NULL
最后五个参数就不用解释了吧?第二个参数合并了GENERIC_READ以及GENERIC_WRITE,表示既要将数据写到设备,也要从设备读取处理的结果数据。
现在来看看第一个参数,这是指向符号连接名称的字符串指针,名称格式是"\\.\slVirtToPhys","\\.\"是Win32中定义本地计算机的方法,CreateFile函数实际上是NtCreateFile函数的封装(位于\%SystemRoot%\System32\ntdll.dll中),后者将访问定向到系统服务中去(注意不要和Win32服务进程混淆)。
系统服务是各种子系统环境到内核的入口点,系统服务的表现就是在x86处理器的机器上执行int 2eh(Windows NT/W2K) 或sysenter指令(Windows XP/2003),触发执行这些指令使线程切换到内核模式,并进入系统服务分派程序。
NtCreateFile将本地计算机的别名"\\.\"用"\??"代替(这样\\.\slVirtToPhys就变成了\?? \slVirtToPhys)并调用内核的ObOpenObjectByName函数,通过符号连接名称,ObOpenObjectByName函数找到\Device\devVirtToPhys对象并返回它的指针。NtCreateFile使用这个指针创建新的文件对象并返回句柄。
操作系统把所有的I/O操作请求抽象化成一个虚拟的文件,隐藏了目标设备的I/O操作可能在结构上并不等同于文件的事实,然后由驱动程序负责将对虚拟文件的请求转换成对具体硬件的请求,这种抽象可以推广到所有用户进程和设备之间的界面,所有对这种虚拟文件的读写操作都被当作是简单的流操作来处理。
在CreateFile返回前,I/O管理器创建了IRP_MJ_CREATE类型的IRP并将其传给驱动处理,驱动中负责响应这个IRP的子程序代码会在发起I/O请求(就是调用CreateFile函数的代码)的线程环境中执行,该线程的IRQL等于PASSIVE_LEVEL。如果驱动的子程序成功返回,那么对象管理器在进程的句柄表中为文件对象创建一个句柄,然后将其一层层地返回,直到返回到CreateFile函数。
新创建的文件对象是个实体对象,并不列在对象管理器中,读者可以用Mark Russinovich编写的Process Explorer工具来观察到它们(可以从http://www.sysinternals.com下载)。
图4.7 文件对象
图4.8 文件对象的属性
现在来总结一下,"\\.\slVirtToPhys"会被转换成符号连接"\??\slVirtToPhys",最终用来找到需要的"\Device\devVirtToPhys"设备。然后可以从负责维护该设备的驱动程序中取得设备对象DEVICE_OBJECT,接下来I/O管理器将IRP_MJ_CREATE请求传递给驱动,驱动程序知道如何处理这个请求。如果驱动打算处理该请求,那么它返回成功代码,这样对象管理器创建对应这个设备的虚拟文件句柄并将它返回给用户模式代码。
句柄和符号连接为间接访问系统资源提供服务,这种"间接"方式将应用程序和系统的数据结构隔离开来。
4.2.6 和设备通讯
.if eax != INVALID_HANDLE_VALUE
mov hDevice, eax
CreateFile函数返回有效的设备句柄后,我们将它保存在hDevice变量中,现在可以用ReadFile、WriteFile以及DeviceIoControl函数来和设备通讯了。DeviceIoControl函数是用来和设备通讯的通用函数,它的原型如下:
DeviceIoControl proto stdcall hDevice:HANDLE, dwIoControlCode:DWORD, \
lpInBuffer:LPVOID, nInBufferSize:DWORD, \
lpOutBuffer:LPVOID, nOutBufferSize:DWORD, \
lpBytesReturned:LPVOID, lpOverlapped:LPVOID
DeviceIoControl函数的参数比CreateFile多,但用起来都很简单。
◎ hDevice--设备的句柄
◎ dwIoControlCode--控制代码,指出要进行什么操作,详细内容后面再做解释
◎ lpInBuffer--指向包含操作所需的数据的缓冲区指针,如果控制代码指明的操作并不需要输入数据的话,这里可以用NULL
◎ nInBufferSize--lpInBuffer参数指向的缓冲区的大小
◎ lpOutBuffer--指向用来接收输出数据的缓冲区,如果dwIoControlCode指明的操作不产生输出数据的话,这里可以用NULL
◎ nOutBufferSize--lpOutBuffer参数指向的缓冲区的大小
◎ lpBytesReturned--指向一个变量,用来返回放入lpOutBuffer缓冲区的数据的数量
◎ lpOverlapped--指向OVERLAPPED结构,这个参数仅在异步操作的时候才需要。我们的操作是同步的(就是在驱动的过程返回前DeviceIoControl函数也不返回),所以在这里使用NULL
4.2.7 I/O控制代码
设备驱动程序可以被当作内核模式函数包来看待,I/O控制代码就是用来指定访问其中的哪个函数的。DeviceIoControl函数的dwIoControlCode参数就是这个代码,它指出了我们需要进行的操作,以及如何进行操作。
控制代码是32位数字型常量,可以CTL_CODE宏来定义,它们定义在winioctl.inc和ntddk.inc文件中。
图4.9 I/O控制代码的定义
控制代码中各数据位字段的含义如下:
◎ DeviceType--设备类型(16bit)指出了设备的类型,微软保留了0-7FFFh的取值,剩下的8000h-0FFFFh供开发商定义新的内核模式驱动程序。我们可以在\include\w2k\ntddk.inc文件中找到一组FILE_DEVICE_XXX符号常量,这些值都是微软保留的值,我们可以使用其中的FILE_DEVICE_UNKNOWN。当然你也可以定义另外一个FILE_DEVICE_XXX值
◎ Access--存取代码(2bit)指明应用程序存取设备的方式,由于这个字段只有2位,所以只有4种可能性:
• FILE_ANY_ACCESS (0)--最大的存取权限,就是什么操作都可以
• FILE_READ_ACCESS (1)--读权限,设备将数据传递到指定的缓冲区
• FILE_WRITE_ACCESS (2)--写权限,可以从内存中向设备传递数据
• FILE_READ_ACCESS or FILE_WRITE_ACCESS (3)--读写权限,设备和内存缓冲区之间可以互相传递数据
◎ Function--功能代码(12bit)用来描述要进行的操作,我们可以用800h-0FFFh来定义自己的I/O控制代码,0-7FFh之间的值是被微软保留的,用来定义公用的I/O控制代码
◎ Method--缓冲模式(2bit)表示I/O管理器如何对输入和输出的数据进行缓冲,这个字段的长度是2位,所以有4种可能性:
• METHOD_BUFFERED (0)--对I/O进行缓冲
• METHOD_IN_DIRECT (1)--对输入不进行缓冲
• METHOD_OUT_DIRECT (2)--对输出不进行缓冲
• METHOD_NEITHER (3)--都不缓冲
缓冲模式的管理我们会在后面进行更详细的讨论,当前最重要的是,虽然进行缓冲会带来一些额外的内存开销,但却是最安全的,因为系统已经做好了相关的全部工作。在传输的数据小于一页(4Kb)的时候,驱动程序通常使用缓冲方式的I/O,因为对大量小块内存进行内存锁定带来的开销也是很大的。在VirtToPhys驱动程序中,我们使用带缓冲的方式。
读者可以手工去定义I/O控制代码,但是使用CTL_CODE宏会方便得多,它提供了创建IOCTL值的算法,具体如下:
CTL_CODE MACRO DeviceType:=<0>, Function:=<0>, Method:=<0>, Access:=<0>
EXITM %(((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Function) SHL 2) OR (Method))
ENDM
前面我曾说过,CTL_CODE宏在winioctl.inc文件和ntddk.inc文件中都有定义。
在例子程序中,由于定义的NUM_DATA_ENTRY、DATA_SIZE常量和IOCTL_GET_PHYS_ADDRESS控制代码在服务控制程序以及驱动中都要被用到,所以我们将它放在一个单独的common.inc文件中,这样万一有所修改的话,就可以直接反映到两个代码中。
NUM_DATA_ENTRY equ 4
DATA_SIZE equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)
4.2.8 交换数据
现在回到驱动的源代码中看看:
lea esi, adwInBuffer
assume esi:ptr DWORD
invoke GetModuleHandle, NULL
mov [esi][0*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
mov [esi][1*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
mov [esi][2*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
mov [esi][3*(sizeof DWORD)], eax
我们在adwInBuffer缓冲区中填写需要进行转换的虚拟地址。
lea edi, adwOutBuffer
assume edi:ptr DWORD
invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
esi, sizeof adwInBuffer, \
edi, sizeof adwOutBuffer, \
addr dwBytesReturned, NULL
调用DeviceIoControl的时候,我们将缓冲区传给驱动,这样它会将虚拟地址转换成物理地址。
.if ( eax != 0 ) && ( dwBytesReturned != 0 )
invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
addr acModulePath, sizeof acModulePath
lea ecx, acModulePath[eax-5]
.repeat
dec ecx
mov al, [ecx]
.until al == '\'
inc ecx
push ecx
CTA0 "%s \t%08Xh\t%08Xh ( %s )\n", szFmtMod
invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
pop ecx
invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
[esi][0*(sizeof DWORD)], \
[edi][0*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
[esi][1*(sizeof DWORD)], \
[edi][1*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
[esi][2*(sizeof DWORD)], \
[edi][2*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
[esi][3*(sizeof DWORD)], \
[edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acBuffer,\
$CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
addr acThis, addr acKernel, addr acUser, addr acAdvapi
assume esi:nothing
assume edi:nothing
invoke MessageBox,NULL,addr acBuffer,$CTA0("Modules Base Address"),\
MB_OK + MB_ICONINFORMATION
.else
invoke MessageBox,NULL,$CTA0("Can't send control code to device."),\
NULL,MB_OK + MB_ICONSTOP
.endif
如果DeviceIoControl函数成功返回,那么dwBytesReturned中的数值就等于驱动程序在adwOutBuffer缓冲区中返回的数据的长度,现在我们的任务就简单了,只要把返回值格式化一下并显示出来即可。我想后面的代码对读者来说应该是很好理解的。
图4.10 VirtToPhys.exe程序的输出
4.2.9 扫尾工作
invoke CloseHandle, hDevice
最后要做的是关闭设备句柄,这时I/O管理器向设备驱动程序发送两个IRP,第一个是IRP_MJ_CLEANUP,它告诉驱动程序设备句柄将要被关闭了;然后是IRP_MJ_CLOSE,它告诉驱动程序设备句柄已经被关闭了。你可以在收到IRP_MJ_CLEANUP时返回一个错误代码,这样就可以阻止设备句柄被关闭。驱动程序的子程序在处理这些IRP时,代码都是在发出I/O请求的线程环境中执行的(也就是调用CloseHandle的线程),它们的IRQL = PASSIVE_LEVEL。
在以后的教程中,我们继续讲解如何在驱动程序中处理IRP。
如果要让驱动在老版本的Windows NT中运行的话,读者需要将代码中的"\??"改成"\DosDevices"并重新编译,因为前面我已经说过,以前的Windows NT 4版本中"\??"目录的名称是"\DosDevices"。
(注:本文中主要讲述控制程序部分,驱动部分VirtToPhys.sys的全部源代码和结构解释详见下一节《全功能的驱动程序》)
5. 全功能的驱动程序分析
※ 本篇的源代码同第4节的源代码:KmdKit\examples\simple\VirtToPhys
5.1 VirtToPhys驱动程序的源代码
现在是到看看一个全功能驱动程序源代码的时候了,这里就是:
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; VirtToPhys - Kernel Mode Driver
; Translates virtual addres to physical address
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
include \masm32\include\w2k\w2kundoc.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
include ..\common.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O N S T A N T S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.const
CCOUNTED_UNICODE_STRING "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; GetPhysicalAddress
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
GetPhysicalAddress proc dwAddress:DWORD
mov eax, dwAddress
mov ecx, eax
shr eax, 22
shl eax, 2
mov eax, [0C0300000h][eax]
.if ( eax & (mask pde4kValid) )
.if !( eax & (mask pde4kLargePage) )
mov eax, ecx
shr eax, 10
and eax, 1111111111111111111100y
add eax, 0C0000000h
mov eax, [eax]
.if eax & (mask pteValid)
and eax, mask ptePageFrameNumber
and ecx, 00000000000000000000111111111111y
add eax, ecx
.else
xor eax, eax
.endif
.else
and eax, mask pde4mPageFrameNumber
and ecx, 00000000001111111111111111111111y
add eax, ecx
.endif
.else
xor eax, eax
.endif
ret
GetPhysicalAddress endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DispatchCreateClose
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
mov eax, pIrp
assume eax:ptr _IRP
mov [eax].IoStatus.Status, STATUS_SUCCESS
and [eax].IoStatus.Information, 0
assume eax:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, STATUS_SUCCESS
ret
DispatchCreateClose endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DispatchControl
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DispatchControl proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
local status:NTSTATUS
local dwBytesReturned:DWORD
and dwBytesReturned, 0
mov esi, pIrp
assume esi:ptr _IRP
IoGetCurrentIrpStackLocation esi
mov edi, eax
assume edi:ptr IO_STACK_LOCATION
.if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
.if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) &&\
( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )
mov edi, [esi].AssociatedIrp.SystemBuffer
assume edi:ptr DWORD
xor ebx, ebx
.while ebx < NUM_DATA_ENTRY
invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
mov [edi][ebx*(sizeof DWORD)], eax
inc ebx
.endw
mov dwBytesReturned, DATA_SIZE
mov status, STATUS_SUCCESS
.else
mov status, STATUS_BUFFER_TOO_SMALL
.endif
.else
mov status, STATUS_INVALID_DEVICE_REQUEST
.endif
assume edi:nothing
push status
pop [esi].IoStatus.Status
push dwBytesReturned
pop [esi].IoStatus.Information
assume esi:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, status
ret
DispatchControl endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverUnload
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverUnload proc pDriverObject:PDRIVER_OBJECT
invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax, pDriverObject
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
ret
DriverUnload endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; D I S C A R D A B L E C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code INIT
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
local status:NTSTATUS
local pDeviceObject:PVOID
mov status, STATUS_DEVICE_CONFIGURATION_ERROR
invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
0, FALSE, addr pDeviceObject
.if eax == STATUS_SUCCESS
invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
.if eax == STATUS_SUCCESS
mov eax, pDriverObject
assume eax:PTR DRIVER_OBJECT
mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)], offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)], offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)], offset DispatchControl
mov [eax].DriverUnload, offset DriverUnload
assume eax:nothing
mov status, STATUS_SUCCESS
.else
invoke IoDeleteDevice, pDeviceObject
.endif
.endif
mov eax, status
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
:make
set drv=VirtToPhys
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj
del %drv%.obj
move %drv%.sys ..
echo.
pause
5.2 驱动程序名称和符号连接名称
要描述设备以及符号连接的名称,就要先从UNICODE_STRING结构的定义讲起,我在前面已经提及,这种结构的字符串在内核中使用得非常普遍。
几乎在所有的驱动源码中--不管是用汇编还是用C写的--下面这样的字符定义序列是很常见的:
.const
uszDeviceName dw "\","D","e","v","i","c","e","\","D","e","v","N","a","m","e",0
uszSymbolicLinkName dw "\","?","?","\","D","e","v","N","a","m","e",0
.code
DriverEntry proc . . .
. . .
local usDeviceName:UNICODE_STRING
local usSymbolicLinkName:UNICODE_STRING
. . .
invoke RtlInitUnicodeString, addr usDeviceName, offset uszDeviceName
invoke RtlInitUnicodeString, addr usSymbolicLinkName, offset uszSymbolicLinkName
RtlInitUnicodeString函数的作用是计算Unicode字符串的大小并且填充UNICODE_STRING结构,一般来说,Unicode字符串都是在代码中静态定义的,并且在运行中保持不变,所以在链接的时候就把UNICODE_STRING结构给填好是完全可能的并且是很容易的,这样更容易理解、 更节省空间(省去8字节的UNICODE_STRING结构、最多3字节的对齐空间以及至少14字节调用RtlInitUnicodeString的代码)。这就是我为什么不喜欢以上代码的原因,我经常使用CCOUNTED_UNICODE_STRING宏来完成它,这样上面的代码就可以用2行来完成:
CCOUNTED_UNICODE_STRING "\\Device\\DevName", usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\??\\DevName", usSymbolicLinkName, 4
如果你认同我的做法的话,也可以在自己的驱动程序中这样定义驱动名称和符号连接名称:
.const
CCOUNTED_UNICODE_STRING "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4
(注:原作者的宏在处理英文的Unicode字符串的时候是不错的,但是中文字符串就不行了,所以如果用到中文串,还是乖乖地动态转换最方便,常用的方法是先用RtlInitAnsiString函数生成一个ANSI_STRING结构,再用RtlAnsiStringToUnicodeString函数将ANSI_STRING转换到UNICODE_STRING即可,把这两句写成一个子程序或者宏的话,使用起来也是很方便的)。
在早些的Windows NT版本中,对象管理器中的"\??"目录是没有的,所以在那种情况下使用要将"\??"改为"\DosDevices",这种用法在后续的Windows版本中也可以使用。为了向前兼容,系统在根目录下创建了一个"\DosDevices"连接,直接指向"\??"目录。
5.3 编写DriverEntry过程
每个内核模式驱动程序必须公开一个名为DriverEntry(当然,你完全可以取另外的名称)的过程,用来初始化驱动程序使用的各种资源,如常用的数据结构等。I/O管理器在装载驱动的时候调用这个过程,该过程在IRQL = PASSIVE_LEVEL下运行,所以在过程中可以存取分页的内存。DriverEntry过程在系统进程上下文中运行。
在深入一步之前,请注意这一行:
.code INIT
所有这样标记的代码将被放入PE文件的INIT节区中,在驱动程序初始化后,这部分代码就再也用不着了。INIT节区中的代码可以在DriverEntry过程返回后被丢弃,系统会自己决定在合适的时候丢弃它。
对于我们这个小小的驱动来说,这样做似乎没有多大的意义,因为我们的驱动是32字节对齐的(链接的时候使用了/align:32参数),这样这个节区占用不到一页的内存空间,所以即使指定了"INIT"也不会使它被丢弃。但是早些版本的Windows NT驱动程序往往有个很大的DriverEntry过程,其中有创建设备对象、申请资源、配置设备等大量代码,这样做的话就能明显地节省内存,所以如果你的DriverEntry足够大的话,这样做的意义就很明显了。
mov status, STATUS_DEVICE_CONFIGURATION_ERROR
默认情况下,我们先把返回值设置成"失败",这样用户模式程序的StartService函数调用就会返回失败。
5.3.1 创建虚拟设备
invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
0, FALSE, addr pDeviceObject
既然驱动程序的主要作用是用于控制一些设备,包括物理设备、虚拟设备或者逻辑设备,那么我们首先必须将这些设备创建起来(本例中是虚拟设备),这可以通过调用IoCreateDevice函数来完成,函数将创建并初始化一个由驱动程序使用的设备对象(DEVICE_OBJECT结构),其原型如下:
IoCreateDevice proto stdcall DriverObject:PDRIVER_OBJECT, DeviceExtensionSize:DWORD, \
DeviceName:PUNICODE_STRING, DeviceType:DEVICE_TYPE, \
DeviceCharacteristics:DWORD, Exclusive:BOOL, \
DeviceObject: PDEVICE_OBJECT
函数的参数描述如下:
◎ DriverObject--指向驱动对象(DRIVER_OBJECT结构),每个驱动程序在DriverEntry过程中会通过参数收到一个指向它的驱动对象的指针
◎ DeviceExtensionSize--指定设备扩展结构的大小(注:I/O管理器将自动分配这个内存,并把设备对象中的DeviceExtension指针指向这块内存),扩展结构的数据结构定义由驱动程序自己决定,我们的驱动太简单了,就没必要使用这个了
◎ DeviceName--指向一个Unicode字符串,用来指定设备名称,该名称必须是全路径的名称,在这里路径的含义并不是指硬盘上的路径,而是指对象管理器命名空间中的路径。这个参数在本例中是必须的,因为我们必须创建一个命名的设备,否则就无法创建一个符号连接,那样用户模式的进程也就无法访问设备了。设备名称在系统中必须是唯一的(注:在其他的应用中,你也可以创建不命名的设备)
◎ DeviceType--在系统定义的FILE_DEVICE_XXX常数中选定一个,用于指定设备的类型,当然也可以使用自定义的类型来表示一个新的类别,这里我们使用FILE_DEVICE_UNKNOWN
◎ DeviceCharacteristics--指明设备的额外属性,本例中使用0
◎ Exclusive--指明设备对象是否必须被独占使用,也就是说同时只能有一个句柄可以向设备发送I/O请求,在CreateFile函数的dwShareMode参数中可以指明是否独占设备。我们并不需要独占设备,这样这里使用FALSE
◎ DeviceObject--指向一个变量,如果函数调用成功的话,变量中将返回指向新创建的设备对象(DEVICE_OBJECT结构)的指针。
接下来,如果对IoCreateSymbolicLink的调用失败的话,我们需要从系统中将设备删除,所以要将IoCreateDevice函数返回的设备对象指针保存起来,以便在删除设备的时候使用。
设备对象指针在卸载驱动的DriverUnload过程中也要用到,但是那时在驱动对象中也可以得到设备对象指针,所以没有必要专门定义一个全局变量将设备对象指针保留到那个时候。
5.3.2 创建符号连接
.if eax == STATUS_SUCCESS
invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
如果设备被成功地创建,那么为了使它能被Windows子系统"看见",我们还需要创建符号连接(前面已经介绍过什么是符号连接了,不是吗?),这可以通过调用IoCreateSymbolicLink来完成,该函数需要的两个参数都是UNICODE_STRING类型的字符串指针--用来指定已存在的设备名称和还有需要新创建的连接名称。
5.3.3 指定分派过程
.if eax == STATUS_SUCCESS
mov eax, pDriverObject
assume eax:PTR DRIVER_OBJECT
mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],offset DispatchControl
符号连接成功创建后,就可以开始下一步了。
每个驱动程序都包括一个过程入口指针数组,用来指明不同的I/O请求被分派到那个函数来处理。每个驱动程序必须至少设置一个过程入口,用来处理IRP_MJ_XXX类型的请求。不同的驱动程序可以设置多个不同的过程入口,用来处理不同的IRP_MJ_XXX请求代码。例如,如果你需要得到"系统将要关闭"的通知的话,就必须"申明"处理该请求的分派过程,也就是在驱动对象的MajorFunction表中的IRP_MJ_SHUTDOWN一栏中填入该分派过程的地址。如果不需要处理某个请求,那么什么都不用做,因为I/O管理器在调用DriverEntry前默认将MajorFunction表中的每一项都填成了系统内部的IopInvalidDeviceRequest过程的地址,该过程会返回一个错误代码。
所以,你的责任就是要为每个你想要响应的I/O代码提供分派过程。
在驱动中我们必须至少处理3种I/O请求包,每个内核模式的驱动程序必须支持功能码IRP_MJ_CREATE,这样才能响应Win32的CreateFile函数调用,没有这个分派过程的话,Win32应用程序将无法获取设备的句柄;同样,IRP_MJ_CLOSE也是必须被支持的,否则就无法响应Win32的CloseHandle调用;最后,IRP_MJ_DEVICE_CONTROL允许用户模式程序通过Win32的DeviceIoControl调用来和驱动程序通讯,所以也必须被支持。
下面是这些功能码的说明:
◎ IRP_MJ_CREATE--用户模式代码调用CreateFile函数来获取目标设备对象的文件对象句柄时,I/O管理器发送此代码
◎ IRP_MJ_DEVICE_CONTROL--用户模式代码调用DeviceIoControl函数时,I/O管理器发送此代码
◎ IRP_MJ_CLOSE--用户模式代码调用CloseHandle函数来关闭目标设备对象的文件对象句柄时,I/O管理器发送此代码
例子程序中在同一个DispatchCreateClose过程中处理IRP_MJ_CREATE和IRP_MJ_CLOSE代码,我们稍候再详细分析这样做的原因。
在ntddk.inc中,还可以找到很多我们感兴趣的IRP_MJ_XXX类型的代码定义:
IRP_MJ_CREATE equ 0
. . .
IRP_MJ_CLOSE equ 2
IRP_MJ_READ equ 3
IRP_MJ_WRITE equ 4
. . .
IRP_MJ_DEVICE_CONTROL equ 0Eh
. . .
IRP_MJ_CLEANUP equ 12h
所有的IRP_MJ_XXX代码在ntddk.inc中都有定义,它们实际上就是MajorFunction数组的索引值而已,前面的代码只是填充了MajorFunction数组的三个元素而已:
mov [eax].DriverUnload,offset DriverUnload
DriverUnload过程的意图在于清理DriverEntry过程申请的一些资源,如果驱动需要被动态卸载的话,我们就必须提供卸载进程的分派过程,当用户模式代码使用SERVICE_CONTROL_STOP参数调用ControlService函数时,该分派过程就会被调用。
assume eax:nothing
mov status, STATUS_SUCCESS
这两句的意思是:驱动程序成功地完成初始化工作的话,那么就向系统返回STATUS_SUCCESS代码表示操作成功。
5.3.4 清理工作
.else
invoke IoDeleteDevice, pDeviceObject
.endif
.endif
如果调用IoCreateSymbolicLink失败,那么我们必须释放前面申请的一些资源,这里我们要删除前面用IoCreateDevice创建的设备对象,这可以通过调用IoDeleteDevice函数来完成。如果你还申请了别的一些资源的话,在这里也应该全部将它归还给系统。
请不要忘了,你必须随时留意你申请的内存和其他一些系统资源,在不需要再使用的话,要将它们释放掉。因为你现在是在内核模式下运行,这些清理工作必须自己完成,没人会帮你做这些事情。
mov eax, status
ret
最后,我们向系统返回状态代码,如果代码是STATUS_SUCCESS的话,驱动程序将保留在内存中,接下来I/O管理器会将对应的IRP请求发送给它;如果返回的是其他数值的话,系统会将驱动程序从内存中清除。
5.3.5 这里是新的对象
DriverEntry成功返回后,系统中多了三个新的对象,驱动"\Driver\VirtToPhys",设备"\Device\devVirtToPhys"以及到设备的符号连接"\??\slVirtToPhys"。
驱动对象描述了系统中存在的独立的驱动程序,I/O管理器通过驱动对象获取每个驱动中不同的分派过程的入口地址。
设备对象描述了系统中的一个设备,包括设备的各种特征。通过设备对象,I/O管理器得到管理这个设备的驱动对象的指针。
文件对象是设备对象在用户模式上的表现,通过文件对象,I/O管理器得到设备对象的指针。
符号连接对用户模式是可见的,它被对象管理器所使用。
图5.1显示了各对象之间的相互联系,它能帮你更彻底地理解后面的内容。
图5.1 驱动、设备和文件对象之间的关系
5.4 I/O分派过程
I/O管理器调用分派过程来响应用户模式或者内核模式的请求,在单层或者多层中的最高层的驱动中,分派过程保证是在发起I/O请求的线程上下文中执行的,就像DriverEntry过程一样,分派过程也是在IRQL = PASSIVE_LEVEL下执行的,这意味着它们可以存取分页的系统资源。
所有的分派过程的申明如下:
DispatchRoutine proto stdcall pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
参数描述如下:
◎ pDeviceObject--指向设备对象(DEVICE_OBJECT结构),如果同一个驱动程序负责多个设备的话,从这个参数就能分辨出是哪个设备发送过来的IRP
◎ pIrp--指向描述I/O请求的IRP结构
I/O管理器创建一个IRP结构,用来描述I/O请求,并把它的指针通过pIrp参数传递给设备驱动程序,具体怎样处理就是设备驱动程序的事情了。
这种统一格式的接口的好处在于:I/O管理器可以用同样的方法调用任何的分派过程,而不需要知道驱动程序内部的细节知识(注:反过来想一下,如果不同分派过程的调用格式不同,那么I/O管理器必须知道所有的过程的调用格式和参数定义)。
5.5 IRP_MJ_CREATE和IRP_MJ_CLOSE的分派过程
为什么不同类型的IRP可以用同一个分派过程来处理呢?这是因为在我们这个简单的驱动程序中,唯一要在IRP_MJ_CREATE和IRP_MJ_CLOSE中要做的事情就是将IRP标记为已处理。
如果两者的处理方法不同的话,你还是应该创建独立的DispatchCreate的DispatchClose过程。
前面已经说过,处理IRP_MJ_CREATE是为了响应CreateFile的调用,如果不处理这个代码的话,Win32应用程序将无法获取设备句柄;同样处理IRP_MJ_CLOSE代码是为了响应对CloseHandle的调用。
DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
mov eax, pIrp
assume eax:ptr _IRP
mov [eax].IoStatus.Status, STATUS_SUCCESS
and [eax].IoStatus.Information, 0
assume eax:nothing
我们填写I/O状态块来表示IRP的处理结果。
I/O状态块的Information字段被设置为0,表示设备句柄可以被打开。该字段对关闭的请求来说没有什么含义,但对其他的请求可能有不同的含义。
Status字段表示决定了CreateFile或CloseHandle的调用是否成功返回,所以我们要在这里填写STATUS_SUCCESS。
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, STATUS_SUCCESS
ret
DispatchCreateClose endp
现在必须调用IoCompleteRequest函数来表示驱动程序已经完成了IRP的处理,并将IRP返回给I/O管理器;然后返回STATUS_SUCCESS表示设备已经可以接收另一个I/O请求的处理了。
IoCompleteRequest的第一个参数告诉I/O管理器哪个IRP已经被处理完毕,第二个参数返回一个系统定义的表示实时优先级的常数,这是驱动程序为了补偿其他线程(在驱动的执行中)进行等待而给予的瞬间的优先级提高,例如对于音频设备,DDK建议使用IO_SOUND_INCREMENT值(等于8)。
例子程序中使用IO_NO_INCREMENT(等于0),也就是说当前线程的优先级保持不变。
IofCompleteRequest是一个fastcall类型的函数(注意名称中的f前缀),同样函数的stdcall版本是IoCompleteRequest,这是使用fastcall版本仅仅是为了教学的目的。
5.6 调用的约定
Windows NT内核API使用了3种调用约定:__stdcall、__cdecl和__fastcall,不幸的是,MASM编译器不支持最后一种调用方式。
__fastcall调用约定将前面两个dword类型的参数放入ECX和EDX寄存器,剩余的参数从右到左压入堆栈,由被调用的过程负责将参数从堆栈中清除。
Fastcall函数名称的修饰方式如下:函数名前面加上一个@符号,函数名后面加上@符号以及表示传递给函数的参数字节数的10进制数字,例如,IofCompleteRequest函数被修饰为:
@IofCompleteRequest@8
上面的修饰名表示这是一个fastcall类型的函数,函数名是IofCompleteRequest,它有两个dword类型的参数。
这个函数在\include\w2k\ntoskrnl.inc中定义,注意前缀SYSCALL
EXTERNDEF SYSCALL @IofCompleteRequest@8:PROC
IofCompleteRequest TEXTEQU <@IofCompleteRequest@8>
为了方便地调用fastcall类型的函数,我写了下面的宏:
fastcall MACRO api:REQ, p1, p2, px:VARARG
local arg
ifnb
% for arg, @ArgRev( )
push arg
endm
endif
ifnb
ifdifi ,
mov ecx, p1
endif
ifnb
ifdifi ,
mov edx, p2
endif
endif
endif
call api
ENDM
这里列出的是简化版的宏,全功能的版本在\include\w2k\ntddk.inc里面,当然原始的ntddk.h里面是没有这个宏的。
5.7 内存缓冲管理
I/O管理器提供了3种缓冲管理方式:buffered方式、direct方式和neither方式。
程序中只演示了使用DeviceIoControl函数来进行I/O处理,使用ReadFile和WriteFile函数来进行I/O处理的方法有点不同,你可以在\src\NtBuild中找到相关的例子。
5.7.1 Buffered I/O方式
开始I/O操作后,I/O管理器将用户缓冲区所属的虚拟内存页面提交以使其有效,然后从非分页内存池中分配一块足够容纳用户请求的内存块。
创建IRP的时候,I/O管理器将用户缓冲区的数据拷贝到申请的缓冲区中,并将其地址通过IRP结构的AssociatedIrp.SystemBuffer字段传递给驱动程序,数据的长度由IO_STACK_LOCATION结构的Parameters.DeviceIoControl.InputBufferLength字段指定(该结构的地址由IRP结构的Tail.Overlay.CurrentStackLocation字段指定,IoGetCurrentIrpStackLocation宏用来获取该结构地址)。
驱动程序处理IRP,并将输出数据拷贝到同一个缓冲区中。
当调用IofCompleteRequest函数来将IRP标志为已处理完毕的时候,I/O管理器将缓冲区中的数据拷贝到用户缓冲区,并释放缓冲区占用的内存,要拷贝的内存数量由IRP结构的IoStatus.Information字段指定。
正如读者所见,I/O管理器在整个过程中拷贝了2次数据,所以buffered I/O模式常用于一些慢速的、不传输大量数据的设备,就像我们的VirtToPhys一样。
但是这种模式也有很大的优点:I/O管理器负责解决了内存传输中可能出现的种种问题,我们根本不用去关心它。
5.7.2 Direct I/O方式
这种模式供direct memory access (DMA)使用。
我并没有详细研究过这种模式,所以在这篇教程中没有使用它。
当I/O管理器创建IRP时,它锁定用户缓冲区(将它标志为不可分页)并让驱动的代码可以通过80000000h以上的地址来存取它,I/O管理器用MemoryDescriptorLlist (MDL)结构来描述这块内存并将结构的指针放在IRP结构的MdlAddress字段中传递给驱动程序,当IRP使用完毕后,I/O管理器将缓冲区解锁。
5.7.3 Neither I/O方式
这种方式下,I/O管理器不进行任何方式的缓冲管理,一切由设备驱动程序自行处理。
驱动程序可以从stack location的Type3InputBuffer参数中得到输入缓冲区的用户模式虚拟地址,也可以从IRP的UserBuffer字段得到输出缓冲区的用户模式地址,但是如果你无法确定是否运行在用户模式调用者的进程上下文中的时候,这两个地址都是无法使用的。当然,作为作者,在本例中我们很清楚自己的驱动程序不是分层的,所以这些地址肯定可以使用。
我们知道,不分层的设备驱动程序总是在IRQL = PASSIVE_LEVEL下被用户模式调用,所以我们不需要关心用户缓冲区是否在内存中存在,即使它已经被交换出物理内存,内存管理器也会打理好一切的。
唯一的问题是:用户模式代码可能传过来一个错误的地址,或者在某处已经将缓冲区释放了--这在多线程的情况下完全可能发生。
我们必须预见到这种情况并且能正确地处理它,所以使用结构化异常处理(SEH)是很必要的(有关结构化异常处理,见《Windows环境下32位汇编语言程序设计》的第14章:异常处理,或者参考其他的相关资料),但要注意的是,内核模式的SEH和用户模式下使用的方法是一样的,所以你无法用它截获所有的异常,例如,就是安置了SEH后,除零错误还是会引发一个蓝屏死机画面。(使用了SEH的代码例子见\src\Article4-5\NtBuild)
5.8 IRP_MJ_DEVICE_CONTROL的分派过程
当驱动程序指定了IRP_MJ_DEVICE_CONTROL的分派过程后,I/O管理器收到用户模式代码对DeviceIoControl的调用后,就会把IRP传递给该分派过程。
and dwBytesReturned, 0
这句代码的意思是将I/O管理器将要拷贝的数据数量暂时设置为0。
mov esi, pIrp
assume esi:ptr _IRP
IoGetCurrentIrpStackLocation esi
mov edi, eax
assume edi:ptr IO_STACK_LOCATION
IoGetCurrentIrpStackLocation宏取出IRP的stack location的指针,也就是指向一个IO_STACK_LOCATION结构的指针,该结构中包含了一些常用的数据:
.if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
判断一下I/O控制代码,我们不应该处理不认识的代码。
NUM_DATA_ENTRY equ 4
DATA_SIZE equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)
我们要处理的IOCTL_GET_PHYS_ADDRESS控制代码是在common.inc文件中作为常数定义的,这个include文件在驱动程序和主程序中都用到了。
.if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )
检查一下输入和输出缓冲区的长度,如果长度不够则停止处理。
IO_STACK_LOCATION结构的OutputBufferLength和InputBufferLength字段和DeviceIoControl函数指定的nOutBufferSize、nInBufferSize参数的取值是相符的。
mov edi, [esi].AssociatedIrp.SystemBuffer
我们可以从IRP的stack location取得指向系统缓冲区的指针,这个缓冲区中包含了用户模式代码传递给驱动的数据,本例中的数据是4个虚拟地址,驱动程序要将它们转换成物理地址。
assume edi:ptr DWORD
告诉编译器edi寄存器指向的是dword类型的值,否则的话我们每次使用edi的时候就一定要加上PTR DWORD了。
xor ebx, ebx
.while ebx < NUM_DATA_ENTRY
invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
mov [edi][ebx*(sizeof DWORD)], eax
inc ebx
.endw
循环NUM_DATA_ENTRY次,每次从缓冲区中取出一个dword(也就是虚拟地址数据),然后调用GetPhysicalAddress以得到转换后的物理地址,并将它写回缓冲区的同样位置去。
mov dwBytesReturned, DATA_SIZE
mov status, STATUS_SUCCESS
所有工作完成后,将处理的总字节数放入dwBytesReturned,并把返回代码设置为成功。
.else
mov status, STATUS_BUFFER_TOO_SMALL
.endif
.else
mov status, STATUS_INVALID_DEVICE_REQUEST
.endif
遇到其他错误的话,返回对应的错误代码。
assume edi:nothing
push status
pop [esi].IoStatus.Status
完成IRP后,我们将当前status变量的值放入状态块的Status字段,这些状态值可以转换成Win32错误代码,对应如下:
Nt Status Win32 Error
============================= ================
STATUS_SUCCESS NO_ERROR
STATUS_BUFFER_TOO_SMALL ERROR_INSUFFICIENT_BUFFER
STATUS_INVALID_DEVICE_REQUEST ERROR_INVALID_FUNCTION
Ntdll.dll中的RtlNtStatusToDosError函数可以将内核状态代码转换到Win32错误代码,用户模式应用程序调用GetLastError就可以得到这个代码。
push dwBytesReturned
pop [esi].IoStatus.Information
状态块的Information字段需要放置I/O管理器要拷贝到用户缓冲区的数据字节数,最后DeviceIoControl函数的调用者会在lpBytesReturned指向的变量中得到这个字节数数值。
assume esi:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, status
ret
调用IofCompleteRequest来结束对IRP的处理。
最后不用忘记,即使是收到了不认识的I/O控制码,我们也应该将I/O状态块设置合适的NTSTATUS值,并将Information字段设置为0,并用IO_NO_INCREMENT参数调用IofCompleteRequest来结束对IRP的处理。
5.9 内存地址转换
内核模式的代码可以将虚拟地址转换成物理地址,MmGetPhysicalAddress函数可以完成这个功能,例子中的GetPhysicalAddress子程序可以基本上完成相同的功能(当然少了一些扩展的功能),不幸的是我没有机会描述其中的工作细节,读者请自行参考"Inside Microsoft Windows 2000"一书(David Solomon和Mark Russinovich著),这一段的代码如下:
GetPhysicalAddress proc dwAddress:DWORD
; Converts virtual address in dwAddress to corresponding physical address
mov eax, dwAddress
mov ecx, eax
shr eax, 22 ; (Address >> 22) => Page Directory Index, PDI
shl eax, 2 ; * sizeof PDE = PDE offset
mov eax, [0C0300000h][eax] ; [Page Directory Base + PDE offset]
.if ( eax & (mask pde4kValid) ) ; .if ( eax & 01y )
; PDE is valid
.if !( eax & (mask pde4kLargePage) ) ; .if ( eax & 010000000y )
; small page (4kB)
mov eax, ecx
; (Address >> 12) * sizeof PTE => PTE offset
shr eax, 10
and eax, 1111111111111111111100y
add eax, 0C0000000h ; add Page Table Array Base
mov eax, [eax] ; fetch PTE
.if eax & (mask pteValid) ; .if ( eax & 01y )
; PTE is valid
; mask PFN (and eax, 11111111111111111111000000000000y)
and eax, mask ptePageFrameNumber
; We actually don't need these two lines
; because of module base is always page aligned
and ecx, 00000000000000000000111111111111y ; Byte Index
add eax, ecx ; add byte offset to physical address
.else
xor eax, eax ; error
.endif
.else
; large page (4mB)
; mask PFN (and eax, 11111111110000000000000000000000y)
and eax, mask pde4mPageFrameNumber
and ecx, 00000000001111111111111111111111y ; Byte Index
add eax, ecx ; add byte offset to physical address
.endif
.else
xor eax, eax ; error
.endif
ret
GetPhysicalAddress endp
GetPhysicalAddress子程序将输入参数中的虚拟地址转换成物理地址后返回。
5.10 DriverUnload过程
DriverUnload过程非常直接了当,就是用于删除驱动程序创建的符号连接和设备对象。当用户模式代码以SERVICE_CONTROL_STOP参数调用ControlService函数时,该过程即被调用。
invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax, pDriverObject
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
DriverUnload过程做的工作和DriverEntry过程刚刚相反,它调用IoDeleteSymbolicLink函数来清除对象管理器命名空间中的符号连接,并调用IoDeleteDevice函数来删除设备对象本身。
前面也提到过,在内核模式下,你必须自己来释放所有申请的资源。
表5.1列出了驱动程序的各主要过程运行的进程上下文和IRQL,这些是你应该了解的,当然该表的内容仅对不分层或者分层驱动中的最高层有效。
表5.1:
User-mode Kernel-mode Process context IRQL
================== ========================== =========================== ============
StartService DriverEntry System PASSIVE_LEVEL
CreateFile IRP_MJ_CREATE User-mode caller PASSIVE_LEVEL
DeviceIoControl IRP_MJ_DEVICE_CONTROL User-mode caller PASSIVE_LEVEL
ReadFile IRP_MJ_READ User-mode caller PASSIVE_LEVEL
WriteFile IRP_MJ_WRITE User-mode caller PASSIVE_LEVEL
CloseHandle IRP_MJ_CLEANUP,IRP_MJ_CLOSE User-mode caller PASSIVE_LEVEL
ControlService(STP) DriverUnload System PASSIVE_LEVEL
5.11 编译驱动程序的方法
:make
set drv=skeleton
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj
del %drv%.obj
move %drv%.sys ..
echo.
pause
这些内容已经在第3节中解释过了,唯一新增的是多了个/ignore:4078选项,这是因为程序中有2个不同属性的INIT节区,所以链接器会报下面的警告(增加这个选项可以抑制该警告信息):
LINK : warning LNK4078: multiple "INIT" sections found with different attributes (E2000020)
5.12 添加资源
本例中我们还在驱动的资源中加上了版本信息,这可以用通常的资源脚本来完成(见rsrc.rc文件):
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "Comments", "Written by Four-F\0"
VALUE "CompanyName", "Four-F Software\0"
VALUE "FileDescription", "Kernel-Mode Driver VirtToPhys v1.00\0"
VALUE "FileVersion", "1, 0, 0, 0\0"
VALUE "InternalName", "VirtualToPhysical\0"
VALUE "LegalCopyright", "Copyright ? 2003, Four-F\0"
VALUE "OriginalFilename", "VirtToPhys.sys\0"
VALUE "ProductName", "Kernel-Mode Driver Virtual To Physical Address Converter\0"
VALUE "ProductVersion", "1, 0, 0, 0\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
这里没有什么特殊的东西,用常用的方法编译和链接就可以了。
5.13 关于调试
使用SoftIce中和驱动和设备有关的命令可以获取很多和驱动及其设备相关的有用信息,读者可以自行查看SoftIce的命令手册,在我的机器上,它的输出如下:
图5.2 driver VirtToPhys命令的输出
图5.3 device devVirtToPhys命令的输出
对你来说,SoftICE显示的信息应该是很好理解的,这些信息是从DRIVER_OBJECT以及 DEVICE_OBJECT结构中获取的,使用这些信息可以很容易在内存中找到这些对象并对它们的分派过程设置断点。
考虑到大家从个人主页上下载时有阻塞现象,现在特重发为网页版·
内核模式驱动程序设计教程
原著:Four-f(俄) 翻译:罗云彬(1-5章)松松(6、7章)董岩(8—11章)
1.Kernel Mode驱动程序基础
本教程讲述了如何在Windows NT为基础的操作系统上用Win32汇编开发KMD,包括NT4.0、2000、XP和2003等操作系统。开发Windows 95/98/ME使用的VxD驱动程序方面的知识并不在本教程讲述的范围内,另外,毫无疑问本教程并不那么完美,可能还包含了诸多未发现的错误,如果您发现了问题,请告知作者,毕竟作者的母语并不是英语,把它翻译成英文已经够难为我了(注:原作者是俄国人也),也感谢masquer和Volodya的校对工作。
1.1 KMD结构概述
1.1.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)
图1.1 Windows NT结构简图
1.1.2 内核模式和用户模式
Intel x86体系结构的处理器定义了4个级别的权限(称为Ring),Windows系统使用了Ring0(供特权模式使用)和Ring3(供用户模式使用),Windows系统只使用了2个级别的权限级别的原因是为了和其他一些硬件系统兼容,这些硬件系统只有2个级别的权限,如Compaq Alpha和Silicon Graphics MIPS等。
每个用户模式的进程有其私有的地址空间,这些进程在最低的权限级别下运行(称为Ring3或者用户模式),它们不允许执行CPU的特权指令,对系统所属的数据、地址空间以及硬件等的访问也是被严格限制的,例如,如果某个用户程序访问4G地址空间中的高位2G,那么系统就会立即将其终止执行。要注意的是,进程调用系统功能的时候,可以切换到内核模式执行,但是调用结束后,就返回到用户模式了。
用户模式的进程总是被认为是对操作系统稳定性的潜在威胁,所以它们的权限被严格地限制,任何触及这些限制的举动都将使进程被终止。
而内核模式的组件则可以共享这些受保护的内核模式内存空间,在特权级别下运行(也称为Ring0),允许执行任何CPU指令,包括特权指令,可以无限制地访问系统数据、代码和硬件资源。
内核模式代码运行在系统地址空间中,并总是被认为是可信任的,一旦被装载运行后,驱动程序就是系统的一部分,可以无限制地做任何事情。
总的来说,用户模式程序被完全从操作系统隔离,这对操作系统的完整性来说是件好事情,但对某些种类的应用程序来说就太头痛了,比如Debug工具。幸运地是,这些在用户模式几乎不可能完成的任务完全可以通过内核模式的驱动程序来完成,因为这些驱动程序的操作是不受限制的。因此,如果你打算从用户模式存取操作系统内部的数据结构或者函数的话,唯一的方法就是将一个内核模式驱动程序装载到系统的地址空间中(并调用它),这是很简单的事情,操作系统完全支持这样的操作。
1.2 Windows NT设备驱动程序
1.2.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趋向无穷大……)用户模式下的应用系统也没用。内核提供了完全不同的函数和数据结构,以至于你要从头开始了解,而且资料奇缺无比,一般情况下,可供参考的只有头文件。
1.2.2 分层的和单层的设备驱动程序
大部分控制硬件设备的驱动程序是分层的驱动程序,分层驱动的概念就是当用户模式发出一个请求时,每个请求从高层次的驱动程序逐层处理并流传到低层次的驱动程序中,一个I/O请求的处理可能分步在多个驱动程序中,例如,如果一个应用程序发出读盘请求,处理请求会在多个驱动程序中流过,在其中你也可以再加入n多个过滤驱动程序(比如插入一个加解密的模块)。
单层的驱动程序是最简单的一类驱动程序,这一类驱动程序通常并不依赖于其他已装载的驱动程序,他们的接口仅仅针对用户模式的应用程序,开发和调试这一类驱动程序是非常简单的,我们即将开始讨论的就是这类程序,其他类型的驱动程序将在以后讨论。
1.3 线程上下文(Thread Context)
在大多数情况下,我们的系统中只安装了一个CPU,所以,对于所有这些运行中的程序来说,操作系统对每个进程中的线程所使用的CPU时间进行调度,循环为每个线程分配时间片,这就造成了多个程序同时执行的假象。如果系统中安装了多个CPU,那么操作系统的调度算法将复杂得多,因为它要将各CPU上的线程进行平衡。如果Windows检测到一个新线程要开始运行了,它将进行一次上下文切换(context switch)(注:上下文(Content)实际上就是线程运行的环境,也就是运行时各寄存器和其他东东的状态,更自然的理解就是"线程状态")。所谓上下文切换就是保存线程运行时的机器状态,然后将另一个线程的状态恢复并重新开始执行。如果重新开始执行的线程属于另一个进程,那么该进程的地址空间也将被同时切换过来(通过在CR3寄存器中装入页表)。
每个用户进程都有私有的地址空间,所以他们的页表都是不同的,CPU通过切换页表来将虚拟地址映射到物理地址,设备驱动程序并不需要直接做这些工作。上下文切换比较耗CPU时间,所以驱动程序一般不创建它们自己的线程,它们一般在下列环境中的一个中运行:
1. 在发起I/O请求的用户线程中运行
2. 在内核模式下的系统线程中运行
3. 作为中断运行(并不处于哪个特定的进程或线程中,因为它们都被暂时挂起了)
在处理I/O请求包(IRPs)时,我们总是运行在和用户模式的调用者相同的进程上下文中运行,这样我们就能对用户程序的地址空间进行寻址。但是当驱动程序被加载或者卸载的时候,我们将在系统进程中运行,这时存取的只能是系统的地址空间。
1.4 中断请求级别
中断是任何操作系统都少不了的组成部分,中断使处理器打断正常的程序流程来首先处理它们,中断分硬件中断和软件中断两种,中断是分优先级的,一个高优先级的中断可以打断低优先级的中断的执行。
Windows中把中断优先级称为IRQLs(interrupt request levels),在系统中表示为从0(被动)到31(高级)的整数,其中大的数值对应高优先级的中断。注意IRQL值的含义和线程调度优先级的含义是完全两码事情。
严格来说,IRQL=0的中断并不是中断,因为它无法打断任何其他代码的执行(因为没有比0更低级的代码了),所有的用户模式线程在这个级别上运行,该级别也称为被动级别(passive level)。我们后面要讨论的驱动程序代码也在这个级别上运行,注意这并不意味着其他的驱动程序也在被动级别下运行。
因此这里还有两个重要的结论:
首先:当驱动程序运行于用户模式程序的线程中时,代码的执行可能被高IRQL级别的代码打断,一些函数可以用来获取当前的IRQL值,并可以对其进行提升或者降低。
第二:被动模式IRQL下的代码可以调用任何的内核函数(DDK指明了每个函数允许调用的IRQL级别),可以对已分页的或未分页的内存进行寻址(注:即已映射过的虚拟地址还是物理内存地址)。反过来,当在一个比较高的IRQL级别下对分页内存进行寻址时(指等于或高于DISPATCH_LEVEL),系统将崩溃,因为这时内存管理器的IRQL级别反而比较低,以至于无法处理页错误了。
1.5 系统崩溃
我想每个人都见过著名的蓝屏死机画面,即"Blue Screen Of Death",简称为BSOD,也许根本不需要解释它是怎么出现或者在什么时候出现的,因为在后面的KMD开发过程中,你会很频繁地遇到它们。
在内核模式下,Windows不对任何系统内存进行保护,由于内核模式的驱动程序可以对系统内存和操作系统的地址空间进行任意存取,所以你必须对你开发的驱动程序进行严格的测试,以防它危及到系统的稳定。
你可以把这个作为最基本的原则,另外,如果没有线程上下文、中断优先级、内核模式和用户模式等方面的概念,开发内核模式驱动程序将是不可能的事(天哪,到现在我才发现,我连菜虫都算不上,我竟然是~~~~~~菜菜的单细胞生物!呜呜~~)
1.6 Driver Development Kit
Windows DDK是MSDN专业版和宇宙版的一部分,它也可以从http://www.microsoft.com/ddk/下载,对于开发设备驱动程序来说,DDK是关于Windows NT内部信息,包括系统函数、数据结构等的丰富资源,不幸的是,微软已经停止了免费发放DDK,所以现在只好去买正版的CD了(没有枪,没有炮,盗版游击队给我们造~~~)
除了文档,DDK还包含了一堆的库文件(*.lib),这些库可以在链接的时候用上。这些库有两种版本:普通的版本(称为free build)和特殊的包含Debug信息的版本(称为checked build),它们分别位于%ddk%\libfre\i386和%ddk%\libchk\i386目录下,check build是在编译Windows源代码时加上DEBUG标志后生成的,在开发驱动程序时,它们可以提供更加精确的错误定位,但是你首先要根据你的操作系统选择合适的lib版本才行。
1.7 汇编程序员使用的KmdKit
KmdKit包含了所有用汇编开发KMD所需要的东西:include文件、lib文件、宏定义、例子文件、工具和一些文章,你可以自己在软件包中找到更多的东西,下一节我们将从这个软件包中包括的一些例子开始学习KMD的编程。
1.8 驱动程序的调试
调试内核模式的代码需要合适的调试器,Compuware的SoftIce是个不错的选择(见 http://www.compuware.com/products/numega/index.htm),当然你也可以使用Microsoft Kernel Debugger,它需要两台计算机:主机和目标机器,目标机器是被调试的机器,主机是运行调试软件的机器。Mark Russinovich ( http://www.sysinternals.com/ ) 也写了一个工具,叫做LiveKd,它允许在单台机器上运行Microsoft Kernel Debugger,而不再需要两台机器了。
1.9 其他参考资料
1. David Solomon, Mark Russinovich, "Inside Microsoft Windows 2000. Third Edition", Microsoft Press, 2000
2. Though there is no source code in this book at all, it's the number one book for the device driver programmers.
3. Sven B. Schreiber, "Undocumented Windows 2000 Secrets. A Programming Cookbook", Addison-Wesley
4. The especially practical book, it has many Windows 2000 secrets revealed.
5. Walter Oney, "Programming the Microsoft Driver Model", Microsoft Press, 1999
6. Walter Oney, "Programming the Microsoft Windows Driver Model. 2nd edition", Microsoft Press, 2003(这是一本很好的书,它强调的是即插即用驱动程序的开发,但这并不降低了它的重要性,因为驱动开发方面的基本理论都是通用的)
7. Art Baker, Jerry Lozano, "The Windows 2000 Device Driver Book, A Guide for Programmers, Second Edition", Prentice Hall, 2000,这也是一本好书,选题范围和本教程类似
8. Rajeev Nagar, "Windows NT File System Internals. A Developer's Guide", O'Reilly
9. Prasad Dabak, Sandeep Phadke, and Milind Borate, "Undocumented Windows NT", M&T Books, 1999 ,这本书里面包含了n多公开的文档中找不到的东西。
10. Gary Nebbett, " Windows NT-2000 Native API Reference", MacMillan Technical Publishing, 2000,这里也有一堆公开的文档中找不到的函数和数据结构的说明
11. Jeffrey Richter, "Programming Applications for Microsoft Windows. Fourth Edition", Microsoft Press, 1999,这本书和开发设备驱动程序没什么关系,但是也是一本很有趣的书。
这里列出的并不是全部,但这些似乎都有点儿看看的必要性。
2. 服务
※ 和本节对应的例子代码见KmdKit\examples\simple\Beeper
读者也许有点疑惑:用户模式的服务关内核模式的驱动程序什么事呀?事实上,两者的确风马牛不相及,但是如果我们要和设备驱动程序通讯的话,我们必须首先安装它,启动它,而和设备驱动程序通讯的界面刚好和服务通讯的界面是类似的。
2.1 Windows服务
Windows NT使用某种机制来启动进程,并让它们不和某个具体的交互式的用户界面相关联,这些进程就被称为服务(service),服务的一个很好的例子就是Web服务器,这些Web服务都没有用户界面,服务是唯一以这种方式运行的应用程序(注:指没有用户界面,当然,严格地说病毒、木马以及所有不想见光的程序也是这样的~~),服务可以在系统启动的时候自动启动,也可以被手工启动,从这一点来看,设备驱动程序和服务是类似的。
Windows NT还支持驱动程序服务,只要使用的时候遵循设备驱动程序协议就可以了,这和用户模式的服务类似,所以,"服务"一词既可以指用户模式的服务进程或者内核模式的设备驱动程序,微软不知何故没有明确地区分两者的概念,所以下面的叙述可能看起来有点让人疑惑。可能有的地方我会说到"driver"一词,但在其他的文章中可能说到"service"一词,但既然这篇教程讲的是如何编写内核设备驱动程序,那么我们就约定无论说到"service"还是"driver",我们的意思都是指"驱动程序",当的确需要提及"服务"的时候,我会明确地指出来的。
另外,请读者时刻记得,文档中关于服务管理的函数其实是叙述得相当含糊的,因为这些函数既能用于驱动程序也能用于服务,在下面的文章中,我们只强调它们在驱动方面的用途和忽略服务方面的用途。
Windows NT中有三个组件和服务管理相关:
◎ 服务控制管理器(Service Control Manager/SCM)--用于启动服务以及和它通讯
◎ 服务控制程序(Service Control Program/SCP)--用于和SCM进行通讯,告诉它何时启动或者停止服务
(咦!第三个哪里去了,我也不知道,原文就这么两个呀,可能后面会提到吧~~)
服务程序中包含可执行代码,这两个组件对服务和驱动程序的处理方式是相同的。我们先来看看前面两个组件,在后面再讲述驱动程序。
2.2 服务控制管理器(SCM)
SCM的代码位于\%SystemRoot%\System32\Services.exe中,当系统启动的时候,SCM被WinLogon进程启动,然后它扫描注册表中HKLM\SYSTEM\CurrentControlSet\Services键下的相关内容,根据这些内容创建一个服务数据库,数据库中包括所有服务的相关参数,如果服务或者驱动被标为自动启动的,那么启动它们并检测启动中是否出错。
为了更深入一步,我们可以用注册表编辑器regedit.exe来打开并观察注册表中的 HKLM\SYSTEM\CurrentControlSet\Services\下面的内容。
想要查看系统中安装了哪些服务(注意不是驱动),可以在控制面板中选择"管理工具",再打开"服务"来查看。
要查看系统中安装了哪些驱动,可以在控制面板中选择"管理工具",再打开"计算机管理",在"系统信息"下的"软件环境"中,你可以看到所有驱动的列表,但是不幸的是,在Windows XP中,这个功能被取消了。
仔细对比一下上面三个地方的内容,我们可以发现这些内容是很一致的。
HKLM\SYSTEM\CurrentControlSet\Services\下面有很多子键,表示一个服务的内部名称,每个子键下包含了和这个服务相关的参数。
现在来考察一下安装一个服务所需的最低数量的参数,我们拿beeper.sys来举例,以后再来讨论这个驱动本身。
图2.1 beeper.sys驱动的注册表键值
这些参数的含义如下:
◎ DisplayName--用户程序访问服务时使用的名称,如果为空,那么注册表的键名会被作为它的名称
◎ ErrorControl--如果SCM启动服务的时候驱动报错,这个值决定了SCM如何对付这个错误,我们对两种取值有点兴趣:
• SERVICE_ERROR_IGNORE (0)--I/O管理器忽略这个错误,不作记录
• SERVICE_ERROR_NORMAL (1)--如果驱动被装入的时候报错,系统将给用户显示一个告警框,并将错误记录到系统日志中
你可以在控制面板中的"管理工具"中选择"事件查看器"来查看系统日志,例如,beeper.sys驱动在初始化的时候做完了所有该做的事(这个例子会让喇叭发声音,但是发声功能是在初始化函数DriverEntry中做的,初始化函数执行完,后面就没什么事了),所以它就返回一个错误,系统就会将它从内存中卸载。但是这里的ErrorControl参数等于SERVICE_ERROR_IGNORE,所以系统日志中并没有错误记录。
◎ ImagePath--指驱动文件的全路径文件名,如果该参数没有指定路径,那么系统会在\%SystemRoot%\Drivers目录下查找
◎ Start--指明何时装载驱动,这里我们关心的也是两个取值
• SERVICE_AUTO_START (2)--驱动在系统启动的时候装载
• SERVICE_DEMAND_START (3)--驱动由SCM根据用户要求装载
如果驱动的Start参数为SERVICE_AUTO_START (2),那么SCM会在系统启动的时候就装载它,这样的驱动被称为自动启动的服务,如果驱动的执行依赖于其他的驱动,SCM也会把其他的驱动也启动起来(要控制设备驱动被装载的顺序,可以使用Group、Tag和DependOnGroup等参数值;要控制服务被装载的顺序,可以使用Group和DependOnService参数)。Start参数还有其他的取值,如SERVICE_BOOT_START (0),但这个参数只能供设备驱动程序使用,I/O管理器将在用户模式的进程启动之前把装载这些驱动程序,这时SCM还没有启动呢!
◎ Type--用于指定服务的类型,既然我们这里讲的是KMD的编程,那么我们只对一个取值感兴趣,那就是SERVICE_KERNEL_DRIVER (1)
仔细观察图2.1后,你对beeper.sys有什么要说的吗?好的,我们看到beeper这个内核模式驱动程序位于C:\masm32\Ring0\Kmd\Article2\beeper目录下,它的名称为"Nice Melody Beeper",由用户控制启动,出错信息不被记录。
Path前面的"\??"前缀的含义你下面就会知道!
如果我们要启动SCM数据库中不存在的驱动程序,那么可以在任何时刻在SCP的帮助下动态装入(也许称为DCP/device control program更为贴切,但是微软的术语库中并没有这个词)。
2.3 服务控制程序(SCP)
从名称理解,服务控制程序(service control program/SCP)可以控制服务或者设备驱动程序,这些功能是在SCM的管理下,通过调用适当的函数来完成的,这些函数位于\%SystemRoot%\System32\advapi.dll (Advanced API)中。
这里是一段关于使用SCP来控制beeper.sys驱动的代码例子
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; Service Control Program for beeper driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
include \masm32\Macros\Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
start proc
local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR
invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
.if eax != NULL
mov hSCManager, eax
push eax
invoke GetFullPathName, $CTA0("beeper.sys"),sizeof acDriverPath,addr acDriverPath,esp
pop eax
invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
invoke StartService, hService, 0, NULL
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
.else
invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
NULL, MB_ICONSTOP
.endif
invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start
2.3.1 建立到SCM的连接
在上面的例子中,我们首先要做的事情是使用OpenSCManager函数来建立到SCM的连接,以便在指定的计算机上打开服务数据库。
OpenSCManager proto lpMachineName:LPSTR, lpDatabaseName:LPSTR, dwDesiredAccess:DWORD
函数使用的参数说明如下:
◎ lpMachineName--指向需要打开的计算机名字符串,字符串以NULL结尾,如果参数指定为NULL,表示连接到本机上的SCM
◎ lpDatabaseName--指向以NULL结尾的包含SCM数据库名称的字符串,字符串应该指定为"ServicesActive",如果参数指定为NULL,则默认打开"ServicesActive"
.const
szActiveDatabase db "ServicesActive", 0
SERVICES_ACTIVE_DATABASE equ offset szActiveDatabase
现在我们要打开的就是这个当前被激活的数据库,所以我们使用了NULL参数
◎ dwDesiredAccess--指定访问SCM的权限,这个参数告诉SCM我们需要进行什么样的操作,常用的取值有三个:
• SC_MANAGER_CONNECT--允许连接到SCM,这个取值是默认值,它的定义就是0
• SC_MANAGER_CREATE_SERVICE--允许创建服务
• SC_MANAGER_ALL_ACCESS--允许进行所有的操作
我们可以使用下面的代码连接到SCM:
invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
.if eax != NULL
mov hSCManager, eax
如果OpenSCManager函数执行成功,那么返回值就是被连接的SCM的句柄,我们在以后使用其他函数在对SCM数据库进行操作的时候会用到这个句柄。
另外,忘了提醒大家,安装内核模式驱动程序需要超级用户的权限,为了安全起见,普通权限的用户没有被授权的话是无法执行特权代码的。当然,本文的例子总是假设你是有超级用户权限的。
2.3.2 安装一个新的驱动
打开SCM后,我们可以用CreateService函数将驱动添加到服务数据库中,这里是该函数的原型,CreateService函数远不止三个参数,但不要害怕,这些参数都是很简单的:
CreateService proto hSCManager:HANDLE, lpServiceName:LPSTR, lpDisplayName:LPSTR, \
dwDesiredAccess:DWORD, dwServiceType:DWORD, dwStartType:DWORD, \
dwErrorControl:DWORD, lpBinaryPathName:LPSTR, lpLoadOrderGroup:LPSTR, \
lpdwTagId:LPDWORD, lpDependencies:LPSTR, lpServiceStartName:LPSTR, \
lpPassword:LPSTR
参数说明如下:
◎ hSCManager--不用说了吧?就是上一节中得到的SCM句柄
◎ lpServiceName--指向一个以0字符结尾的表示服务名称的字符串,字符串的最大长度是256个字符,名称中不允许使用/或者\字符(因为这些字符会和注册表的路径表示方式冲突),这个值和注册表中的键名是相对应的
◎ lpDisplayName--指向一个以0字符结尾表示服务名称的字符串,这个名称是供用户界面程序识别函数时使用的,同样,它的最大长度也是256个字符。这个值和注册表中的DisplayName键的值是相对应的
◎ dwDesiredAccess--指定需要访问服务的操作,可以有以下取值:
• SERVICE_ALL_ACCESS--可以进行所有操作
• SERVICE_START--允许调用StartService函数来启动服务
• SERVICE_STOP--允许调用ControlService函数来停止服务
• DELETE--允许调用DeleteService函数来删除服务
在这里我们只需要做两件事情:启动驱动和删除驱动,所以例子中使用了SERVICE_START和DELETE,我们不需要停止服务的操作,因为上面已经说过,这个驱动在初始化的时候就会返回错误(所以它不会有已经启动的状态)。
◎ dwServiceType--服务的类型,我们的教程中只用得到SERVICE_KERNEL_DRIVER,这个值和注册表中的Type键的值是相对应的
◎ dwStartType--表示在什么时候启动服务,如果我们需要手动启动驱动的话,那么使用SERVICE_DEMAND_START参数,如果驱动程序需要在系统启动的时候就被启动,那么使用SERVICE_AUTO_START参数,这个取值和注册表中的Start键的取值是相对应的
◎ dwErrorControl--表示当驱动初始化的时候出错该如何处理,取值SERVICE_ERROR_IGNORE表示忽略错误,取值SERVICE_ERROR_NORMAL表示将错误记录到系统日志中去,这个取值和注册表中的ErrorControl键值是相对应的
◎ lpBinaryPathName--指向以0结尾的表示驱动程序文件名的字符串,这个值和注册表中的ImagePath的键值是相对应的
◎ lpLoadOrderGroup--指向以0结尾的表示组名称的字符串,表示该驱动属于哪个组,既然我们的例子程序不属于任何组,那么这里就用NULL好了
◎ lpdwTagId--指向一个32位的缓冲区,用来接收驱动在lpLoadOrderGroup参数指定的组中的唯一的标识,我们的例子中不需要用到这个表示,所以参数指定为NULL
◎ lpDependencies--对于驱动程序来说,这个参数没什么用途,设置为NULL好了
◎ lpServiceStartName--指向一个以0结尾的表示帐号名称的字符串,用于指定服务允许在哪个帐号下运行,如果服务类型是SERVICE_KERNEL_DRIVER的话,该帐号就是系统装入服务的模块名称,我们在这里使用NULL,表示由默认的模块装入
◎ lpPassword--对于驱动程序来说,这个参数没什么用途,设置为NULL好了
现在来总结一下,最后的5个参数总是设置为NULL,我们就把它抛到脑后去好了,第一个参数是SCM句柄,而dwDesiredAccess参数也是很好理解的,剩下的参数是什么?聪明的你一定已经猜到了--它们实际上就是和注册表里面的键一一对应的!看看下表就明白了:
CreateService函数的参数 注册表
----------------------- -------------
lpServiceName 键名
lpDisplayName DisplayName
dwServiceType Type
dwStartType Start
dwErrorControl ErrorControl
lpBinaryPathName ImagePath
表2.1 参数和注册表键的对应关系
好了,现在回过头来看看例子代码:
push eax
invoke GetFullPathName,$CTA0("beeper.sys"),sizeof acDriverPath,addr acDriverPath,esp
pop eax
invoke CreateService, hSCManager, $CTA0("beeper"), $CTA0("Nice Melody Beeper"), \
SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
首先,我们调用GetFullPathName函数来获取全路径的驱动程序文件名,并把它传递给CreateService函数。
然后CreateService函数将这个驱动程序加入到SCM的数据库中,并创建对应的注册表键,正如表2.1所示的,所有这些键将被CreateService函数加入到注册表中,如果你在源代码中把DeleteService一行去掉,将csp.asm重新编译并执行,就可以验证我说的了。
不要认为使用RegXXX之类的函数将相同的信息写入注册表就可以达到相同的结果,这样操作的话,键值是写到注册表里面了,但是SCM的数据库里面可什么都没有哦!
如果SCM数据库中指定的设备驱动程序已经存在,那么CreateService函数会返回一个错误,这时可以调用GetLastError函数获取具体原因,上例中会得到ERROR_SERVICE_EXISTS。如果CreateService函数成功地将驱动加入到了SCM数据库中,函数的返回值就是驱动的句柄,这个句柄在后面的驱动管理函数中将会被用到。
2.3.3 启动驱动程序
下一步要调用的函数是StartService,它的原型申明如下:
StartService proto hService:HANDLE, dwNumServiceArgs:DWORD, lpServiceArgVectors:LPSTR
参数说明如下:
◎ hService--就是上一小节中由CreateService返回的驱动的句柄
◎ dwNumServiceArgs--用于驱动程序的时候,这个参数总是设置为NULL
◎ lpServiceArgVectors--同上,也为NULL
启动驱动的方法就是这样的:
invoke StartService, hService, 0, NULL
StartService函数的执行过程和装入用户模式的DLL的过程类似,驱动程序文件的映像被装入到系统的地址空间中,文件可以被装入到任何地址中,然后系统会根据PE文件中的重定位表对其进行重定位操作,这样驱动程序的内存映像就被准备好了,接下来系统调用驱动的入口函数,也就是DriverEntry子程序,和装入DLL不同的是,DriverEntry子程序的执行是在系统进程的上下文中进行的。
StartService函数的调用是同步执行的,也就是说,只有驱动程序的DriverEntry过程返回后,函数才会返回(回想一下,如果函数不等人家执行完就直接返回了,那叫什么~~~那是异步!)。如果驱动初始化成功,那么DriverEntry过程应该返回STATUS_SUCCESS,这样StartService会返回一个非0值,这时,我们又回到了调用StartService的用户模式的上下文中了。
在这个例子中,我们并不关心StartService函数的返回值,理由前面已经说过了,那就是beeper驱动程序在DriverEntry中进行了发声音功能的演示,并返回一个错误码,后面再没有什么功能要做的了。
2.3.4 卸载驱动
怎样卸载驱动呢?
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
现在我们需要将系统恢复到以前的状态,调用DeleteService函数就可以将驱动从SCM数据库中删除,比较奇怪的是,并不需要将SCM句柄传递给DeleteService函数。
DeleteService函数的原型申明如下:
DeleteService proto hService:HANDLE
参数hService就是需要被卸载的服务的句柄
严格地说,这个函数并不真正将服务删除,它仅仅是将服务做了一个删除标志,只有当服务已经停止,并且服务的句柄被关闭后,SCM才真正将服务删除。调用了DeleteService函数后,我们还需要将服务的句柄保存以便在后面使用。如果再次调用DeleteService函数的话,函数会返回失败,这时用GetLastError得到的错误代码是ERROR_SERVICE_MARKED_FOR_DELETE。
现在我们不再需要和驱动程序通讯了,所以需要使用CloseServiceHandle函数将句柄关闭:
CloseServiceHandle proto hSCObject:HANDLE
参数hSCObject可以是服务或驱动的句柄,也可以是SCM数据库的句柄,驱动的句柄被关闭后,我们再次调用CloseServiceHandle函数来关闭SCM句柄。
2.4 字符串操作的宏
最后来解释一下源代码中的$CTA0是什么东东--这是一个宏,用来在只读数据段中定义一个以0结尾的字符串,它可以在invoke宏指令中使用,这不是唯一用到的宏,在\Macros\Strings.mac文件中还包括很多其他有用的宏,这些宏都是用于定义字符串的,文件中也有怎样使用它们的详细的解释。既然本教程的重点是讲述KMD的编程,那么我就不在这些宏上面做过多的解释了,但是后面的程序中有很多地方会用到它们。
3. 最简单的设备驱动程序
※ 和本章内容相关的源代码见:
KmdKit\examples\simple\Beeper
KmdKit\examples\simple\DateTime
3.1 如何编译和链接内核模式驱动程序
我总是把驱动程序的汇编源代码放到批处理文件中,这样的文件从内容上看是.asm和.bat文件的混合体,但是扩展名是.bat(注:读者在实际使用的时候是不是这样做完全可以根据个人喜好而定)
;@echo off
;goto make
.386 ; driver's code start
;::::::::::::::::::::::::::::::::
; the rest of the driver's code ;
;::::::::::::::::::::::::::::::::
end DriverEntry ; driver's code end
:make
set drv=drvname
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
如果你运行这个"自编译"的批处理文件的话,系统会做以下的事情:
对于汇编编译器来说,前面两行的命令是被注释掉的,所以会被忽略;但是对于命令解释器来说,这两行会被执行,因为命令解释器会忽略前面的分号。这样一来,执行会跳到最后面的:make标号处,接下来下面的编译和链接命令就会被执行,跳转语句和:make标号中间的汇编源代码会被忽略。另外,对于汇编编译器来说,所有end标号后面的内容都会被忽略,所以后面的:make及其后的内容会被编译器忽略。
这种方式用起来很方便,因为源代码和如何编译、链接程序的信息都被放在一起了,如果需要的话,读者也可以自己另外加些命令上去,在我编写的所有的驱动代码中,我都使用了这种方法。
下面我来解释一下:make后面的一些命令的含义。
set drv=drvname
这里定义一个环境变量,具体使用的时候用驱动的文件名来代替,下面的链接选项的含义是:
◎ /driver--告诉链接器创建Windows NT内核模式驱动程序,这个选项造成的最重要的影响是文件中会多出一个"INIT"节区(有关PE文件中节区等概念可以参考《Windows环境下32位汇编语言程序设计》一书的第17章:PE文件),另外还有".idata"节区,里面包含了一些IMAGE_IMPORT_DESCRIPTOR结构,指出了需要导入的函数和模块的名称。"INIT"节区的属性被标志为可丢弃,这样装载程序获取了相关的导入信息后,这个节区的内容即被丢弃
◎ /base:0x10000--将驱动映像的基地址设置为10000h
◎ /align:32--系统内存是很宝贵的,所以最好使用更有效的节区对齐数值
◎ /out:%dvr%.sys--链接器默认会创建以exe作为扩展名的文件,当指定了/DLL选项后,创建的是.dll文件,在这里,我们要强制让它创建以.sys为扩展名的文件
◎ /subsystem:native--在PE文件头中,有一个字段用来告诉映像装载程序使用哪个子系统:Win32、POSIX或者OS/2。我们需要为驱动指定合适的环境,当链接.exe或者.dll文件的时候,一般指定为Win32子系统。内核模式驱动程序不需要任何子系统,所以我们使用的参数是native
3.2 最简单的内核模式驱动程序
3.2.1 源代码
这里是一个最简单的内核模式驱动程序的源代码
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; simplest - Simplest possible kernel-mode driver
;
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; B U I L D I N G D R I V E R
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:make
set drv=simplest
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
3.2.2 DriverEntry过程
就像其他的可执行程序一样,每个驱动程序也有一个入口点,这是当驱动被装载到内存中时首先被调用的,驱动的入口点是DriverEntry过程(注:过程也就是子程序),DriverEntry这个名称只是一个标记而已,你可以把它命名为其他任何名字--只要它是入口点就行了。DriverEntry过程用来对驱动程序的一些数据结构进行初始化,它的函数原型定义如下:
DriverEntry proto DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING
不幸的是,Charles Simonyi创造的著名的"匈牙利表示法"并没有在DDK中使用,但是我还是在任何可能的地方使用,所以我为DriverObject和RegistryPath参数都加上了前缀。
PDRIVER_OBJECT和PUNICODE_STRING的定义可以分别在\include\w2k\ntddk.inc和\include\w2k\ntdef.inc中找到。
PDRIVER_OBJECT typedef PTR DRIVER_OBJECT
PUNICODE_STRING typedef PTR UNICODE_STRING
当I/O管理器调用DriverEntry过程的时候,它会传过来两个指针类型的参数,说明如下:
◎ pDriverObject--指向用于描述当前驱动的对象(所谓对象,在内存中也就表现为一个结构而已),这个对象刚被系统初始化。由于Windows NT是一个面向对象的操作系统,因此,驱动也是被作为一个对象来描述的,当驱动被装载到内存中的时候,系统会创建一个对象来描述这个驱动,对象在内存中的表示方式就是一个DRIVER_OBJECT结构(在\include\w2k\ntddk.inc中定义),pDriverObject参数指向这个对象,以便让驱动有存取它的机会,但我们现在还没必要用到它
◎ pusRegistryPath--指向一个定长的Unicode字符串,内容是驱动的注册表键的路径,前面的章节中我们已经讨论过了驱动的注册表键。驱动程序可以用它来获取或者保存一些要用到的信息。如果在以后的执行中还要用到这个字符串,驱动程序应该保留一份该Unicode字符串的拷贝而不是仅仅保存这个指针,因为指针指向的内存在DriverEntry过程返回后即被释放掉了
定长的Unicode字符串是用UNICODE_STRING结构来表示的,和用户模式代码不同,内核模式的代码往往采用用UNICODE_STRING结构定义的字符串,该结构在\include\w2k\ntdef.inc中定义如下:
UNICODE_STRING STRUCT
_Length WORD ?
MaximumLength WORD ?
Buffer PWSTR ?
UNICODE_STRING ENDS
结构中的各字段含义如下:
◎ _Length--字符串的长度,以字节表示(而不是以字符数量表示),这个长度不包括末尾的0字符,由于Length是汇编的保留字,所以我不得不在前面加了一个下划线
◎ MaximumLength--字符串缓冲区的长度,也是以字节数表示
◎ Buffer--指向Unicode字符串,不要想当然地认为这个字符串就是以0结尾的,很多时候尾部并没有0
这种结构的优点在于它清楚地表现出了字符串的当前长度和最大的可能长度,这样就允许对它进行一些运算(比如在后面加上一些字符等)。
前面举例的驱动程序是最最简单的,它仅仅可以被装载而已,但是即使被装载,它除了返回一个STATUS_DEVICE_CONFIGURATION_ERROR错误代码(全部代码的列表可以见\include\w2k\ntstatus.inc文件)外什么都不干;在这里如果返回的是STATUS_SUCCESS,那么驱动会保留在内存中,但是你却无法卸载它,因为程序中缺少了负责卸载的DriverUnload过程。
读者可以用KmdManager工具来注册以及装载任何驱动程序。
3.3 Beeper驱动程序
3.3.1 源代码
现在来看看Beeper驱动程序的源代码,在"服务"一节中我们已经看到过它的控制程序了。
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; beeper - Kernel Mode Drive
; Makes beep thorough computer speaker
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\hal.inc
includelib \masm32\lib\w2k\hal.lib
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; E Q U A T E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
TIMER_FREQUENCY equ 1193167 ; 1,193,167 Hz
OCTAVE equ 2 ; octave multiplier
PITCH_C equ 523 ; C - 523,25 Hz
PITCH_Cs equ 554 ; C# - 554,37 Hz
PITCH_D equ 587 ; D - 587,33 Hz
PITCH_Ds equ 622 ; D# - 622,25 Hz
PITCH_E equ 659 ; E - 659,25 Hz
PITCH_F equ 698 ; F - 698,46 Hz
PITCH_Fs equ 740 ; F# - 739,99 Hz
PITCH_G equ 784 ; G - 783,99 Hz
PITCH_Gs equ 831 ; G# - 830,61 Hz
PITCH_A equ 880 ; A - 880,00 Hz
PITCH_As equ 988 ; B - 987,77 Hz
PITCH_H equ 1047 ; H - 1046,50 Hz
; We are going to play c-major chord
TONE_1 equ TIMER_FREQUENCY/(PITCH_C*OCTAVE)
TONE_2 equ TIMER_FREQUENCY/(PITCH_E*OCTAVE)
TONE_3 equ (PITCH_G*OCTAVE) ; for HalMakeBeep
DELAY equ 1800000h ; for my ~800mHz box
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; M A C R O S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DO_DELAY MACRO
mov eax, DELAY
.while eax
dec eax
.endw
ENDM
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; MakeBeep1
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
MakeBeep1 proc dwPitch:DWORD
; Direct hardware access
cli
mov al, 10110110y
out 43h, al
mov eax, dwPitch
out 42h, al
mov al, ah
out 42h, al
; Turn speaker ON
in al, 61h
or al, 11y
out 61h, al
sti
DO_DELAY
cli
; Turn speaker OFF
in al, 61h
and al, 11111100y
out 61h, al
sti
ret
MakeBeep1 endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; MakeBeep2
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
MakeBeep2 proc dwPitch:DWORD
; Hardware access using WRITE_PORT_UCHAR and READ_PORT_UCHAR
; functions from hal.dll
cli
invoke WRITE_PORT_UCHAR, 43h, 10110110y
mov eax, dwPitch
invoke WRITE_PORT_UCHAR, 42h, al
mov eax, dwPitch
invoke WRITE_PORT_UCHAR, 42h, ah
; Turn speaker ON
invoke READ_PORT_UCHAR, 61h
or al, 11y
invoke WRITE_PORT_UCHAR, 61h, al
sti
DO_DELAY
cli
; Turn speaker OFF
invoke READ_PORT_UCHAR, 61h
and al, 11111100y
invoke WRITE_PORT_UCHAR, 61h, al
sti
ret
MakeBeep2 endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
invoke MakeBeep1, TONE_1
invoke MakeBeep2, TONE_2
; Hardware access using hal.dll HalMakeBeep function
invoke HalMakeBeep, TONE_3
DO_DELAY
invoke HalMakeBeep, 0
mov eax, STATUS_DEVICE_CONFIGURATION_ERROR
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
:make
set drv=beeper
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
这个驱动程序会使用主板上的扬声器来播放C大调的几个音符,为了实现这个功能,程序使用了IN和OUT指令来访问I/O端口。众所周知的是,Windows NT把I/O端口当作重要的资源来保护,任何用户模式的程序如果使用了IN或者OUT指令来访问端口的话,会被Windows立马枪毙掉。但事实上还是有办法绕过这个限制使用户模式的程序直接访问I/O端口,我们马上会谈到这一点。
3.3.2 控制系统定时器
计算机中有三个定时器,分别是定时器0、1和2,由可编程定时芯片(Programmable Interval Timer/PIT)实现,其中定时器2用于发声,发声的频率取决于定时器计数器的初始设置值,定时器会将计数值从初始值开始递减到0,然后将计数值复原为初始值,以此循环。计数值的递减由频率为1,193,180 Hz的系统振荡器控制着,该频率值在所有的PC家族的机器中是固定的。振荡器每产生一个脉冲,计数值就减一,为了发出不同频率的声音,我们只需要设定不同的初始值即可,发声频率和初始值的关系是:声音频率=1193180/初始值。关于这方面更进一步的知识,读者可以在网上搜到更多的内容。
这里还有个微妙的区别,kernel32.dll中的QueryPerformanceFrequency函数的允许最大值等于1193180,但是hal.dll里面的HalMakeBeep函数使用的数值却是1193167,在这里我将使用这个数值,我不知道这是不是误差补偿的结果,但这并不妨碍我们的程序发出声音来。
好!现在我们用MakeBeep1子程序来发第一个C大调音符。
mov al, 10110110y
out 43h, al
首先,我们要设置定时器的控制寄存器,也就是将2进制值的10110110送到43h端口。
mov eax, dwPitch
out 42h, al
mov al, ah
out 42h, al
然后,我们用两个连续的操作将初始值的低位字节和高位字节送到42h端口。
in al, 61h
or al, 11y
out 61h, al
现在要将扬声器打开,这可以通过将61h端口的位0和位1设置为1来完成,不出意外的话,现在应该能够听到声音了。
DO_DELAY MACRO
mov eax, DELAY
.while eax
dec eax
.endw
ENDM
为了让声音延续一段时间,我们用DO_DELAY宏来进行一些延时,虽然这种延时方法有点过时,但还是很有效的。
in al, 61h
and al, 11111100y
out 61h, al
现在可以关闭扬声器了,千万别忘了扬声器是整个系统的资源哦,这只要将端口61h的位0和位1清零就好了。在程序中我们用了cli指令清除中断允许标志来关闭中断,这在多处理器的机器上会对其他程序有所影响的。
接下来,我们用MakeBeep2子程序来发第2个音符--C大调的mi音,实现上的区别在于这次使用hal.dll中的WRITE_PORT_UCHAR和READ_PORT_UCHAR函数来代替in/out指令。HAL隐藏了和硬件相关的细节,如操作I/O端口的方法等,这样代码就可以做到和机器无关。
第3个音符--C大调的so音符--则是用hal.dll中的HalMakeBeep函数来实现的,这个函数的参数不是定时器的初始值,而是发声的频率。
在Beeper.bat文件的前面部分,读者可以发现12个频率定义值,程序中只用到了3个,读者可以自行用剩余的定义去写一个合成器,要关闭扬声器的话,只要用参数0来再次调用HalMakeBeep就行了。
Beeper驱动程序的DriverEntry过程返回一个错误值,所以系统直接就把它从系统中清除掉了。当然,在以后的全功能驱动程序里面,这里应该返回STATUS_SUCCESS值。
3.3.3 自动启动驱动程序
scp.exe程序安装驱动程序的时候使用的是手动启动模式,前面我们也讨论过其他的启动方式,现在来试试让它随系统的启动而自动启动,有很多中办法可以实现这个功能,最简单的就是将源代码中调用DeleteService的一行去掉,然后将 SERVICE_DEMAND_START参数改为SERVICE_AUTO_START,并将SERVICE_ERROR_IGNORE参数改为 SERVICE_ERROR_NORMAL,再重新编译即可。
这样,运行scp.exe并退出后,注册表信息还会保留着。你可以尽管将它忘掉,但是每次系统启动的时候,beeper.sys会不厌其烦地通知你一下它还活着,而且在系统日志中,你也可以找到它的启动错误记录,在控制面板->管理工具->事件查看器中,选择系统日志,双击日志,就可以看到下面的信息:
图3.1 系统错误日志
最好别忘了将注册表中的对应键值去掉,否则在每次启动的时候你都会听到声音的。
3.4 Giveio驱动程序例子
3.4.1 Giveio驱动程序的控制代码
现在来写另一个SCP代码,以便用来控制一个新的例子--giveio.sys驱动程序:
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DateTime - Service Control Program for giveio driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
include \masm32\Macros\Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; M A C R O S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
CMOS MACRO by:REQ
mov al, by
out 70h, al
in al, 71h
mov ah, al
shr al, 4
add al, '0'
and ah, 0Fh
add ah, '0'
stosw
ENDM
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DateTime
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DateTime proc uses edi
local acDate[16]:CHAR
local acTime[16]:CHAR
local acOut[64]:CHAR
; See Ralf Brown's Interrupt List for details
;:::::::::::::::::: Set data format ::::::::::::::::::
mov al, 0Bh ; status register B
out 70h, al
in al, 71h
push eax ; save old data format
and al, 11111011y ; Bit 2: Data Mode - 0: BCD, 1: Binary
or al, 010y ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode
out 71h, al
;:::::::::::::::::::: Lets' fetch current date ::::::::::::::::::::
lea edi, acDate
CMOS 07h ; date of month
mov al, '.'
stosb
CMOS 08h ; month
mov al, '.'
stosb
CMOS 32h ; two most significant digit od year
CMOS 09h ; two least significant digit od year
xor eax, eax ; terminate string with zero
stosb
;:::::::::::::::::::: Lets' fetch current time :::::::::::::::::::
lea edi, acTime
CMOS 04h ; hours
mov al, ':'
stosb
CMOS 02h ; minutes
mov al, ':'
stosb
CMOS 0h ; seconds
xor eax, eax ; terminate string with zero
stosb
;:::::::::::::: restore old data format :::::::::::::
mov al, 0Bh
out 70h, al
pop eax
out 71h, al
;::::::::::::::::: Show current date and time :::::::::::::::
invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK
ret
DateTime endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; start
;::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: start proc
local fOK:BOOL
local hSCManager:HANDLE
local hService:HANDLE
local acDriverPath[MAX_PATH]:CHAR
local hKey:HANDLE
local dwProcessId:DWORD
and fOK, 0 ; assume an error
; Open the SCM database
invoke OpenSCManager, NULL, NULL, SC_MANAGER_CREATE_SERVICE
.if eax != NULL
mov hSCManager, eax
push eax
invoke GetFullPathName, $CTA0("giveio.sys"),sizeof acDriverPath,addr acDriverPath, esp
pop eax
; Register driver in SCM active database
invoke CreateService,hSCManager,$CTA0("giveio"),$CTA0("Current Date and Time fetcher."), \
SERVICE_START + DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, \
SERVICE_ERROR_IGNORE, addr acDriverPath, NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
$CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"),\
0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
.if eax == ERROR_SUCCESS
; Add current process ID into the registry
invoke GetCurrentProcessId
mov dwProcessId, eax
invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId),NULL,REG_DWORD, \
addr dwProcessId, sizeof DWORD
.if eax == ERROR_SUCCESS
; Start driver
invoke StartService, hService, 0, NULL
inc fOK ; Set OK flag
invoke RegDeleteValue, hKey, addr szProcessId
.else
invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
NULL, MB_ICONSTOP
.endif
invoke RegCloseKey, hKey
.else
invoke MessageBox, NULL, $CTA0("Can't open registry."), NULL, MB_ICONSTOP
.endif
; Remove driver from SCM database
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
.else
invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), \
NULL, MB_ICONSTOP
.endif
; If OK display current date and time to the user
.if fOK
invoke DateTime
.endif
invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start
3.4.2 使用注册表向驱动程序传递信息
这个例子中只有一点点新东西:
invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, \
$CTA0("SYSTEM\\CurrentControlSet\\Services\\giveio"), \
0, KEY_CREATE_SUB_KEY + KEY_SET_VALUE, addr hKey
.if eax == ERROR_SUCCESS
invoke GetCurrentProcessId
mov dwProcessId, eax
invoke RegSetValueEx, hKey, $CTA0("ProcessId", szProcessId), NULL, REG_DWORD, \
addr dwProcessId, sizeof DWORD
.if eax == ERROR_SUCCESS
invoke StartService, hService, 0, NULL
在启动驱动前,我们在该驱动的注册表键中创建了一个名为ProcessId的键值,里面的数据是当前进程的进程ID,也就是SCP程序自己的进程ID,请注意我在这里使用$CTA0宏的方法,我使用了标号szProcessId,用这个标号指定了一个字符串"ProcessId",我们在后面还要用到这个标号,看来我的字符串宏真是太灵活了哇哈哈哈哈~~~(俄国人也会得意忘形呀!?)
一旦注册表值成功创建,我们就可以启动驱动程序了,这个额外的注册表键是干什么的呢?谜底后面就会揭晓!
inc fOK
invoke RegDeleteValue, hKey, addr szProcessId
.else
invoke MessageBox, NULL, $CTA0("Can't add Process ID into registry."), \
NULL, MB_ICONSTOP
.endif
invoke RegCloseKey, hKey
当StartService函数返回后,我们可以认为驱动程序已经圆满地完成了任务,然后就将fOK标志设上,接下来调用RegDeleteValue的一句是可有可无的,因为下面调用DeleteService函数后,驱动注册表键下的所有键值都会被清除掉,但是从编程风格方面来说,明确地写上删除语句是一个很好的习惯。
.if fOK
invoke DateTime
.endif
在关闭所有SCM的句柄后,如果fOK标志位已经设置的话,程序就调用DateTime子程序。
3.4.3 存取CMOS
计算机主板上有块小芯片是用来存放系统配置信息的,如硬盘参数、内存配置以及系统时间等等,这块芯片一般被叫做"CMOS"(CMOS是Complementary Metal Oxide Semiconductor的缩写)。CMOS芯片由电池供电,内部建有一个实时时钟(RTC),我们可以通过存取70h和71h号I/O端口来获取其中的数据,"Ralf Brown's Interrupt List"里面有其详细的格式说明,网址见http://www-2.cs.cmu.edu/afs/cs/user/ralf/pub/WWW/files.html(注:http://asm.yeah.net上面有已经整理成单个hlp文件的全部上述资料)。
mov al, 0Bh ; status register B
out 70h, al
in al, 71h
push eax ; save old data format
and al, 11111011y ; Bit 2: Data Mode - 0: BCD, 1: Binary
or al, 010y ; Bit 1: 24/12 hour selection - 1 enables 24 hour mode
out 71h, al
首先,我们通过状态寄存器B来设置一个便于使用的数据格式。使用CMOS宏可以从CMOS获取数据并同时将它的转换成我们需要的格式。
invoke wsprintf, addr acOut, $CTA0("Date:\t%s\nTime:\t%s"), addr acDate, addr acTime
invoke MessageBox, NULL, addr acOut, $CTA0("Current Date and Time"), MB_OK
当获取到所有的相关数据后,程序的输出如下:
图3.2 DateTime.exe程序的输出结果
这个程序中最奇怪的事情就是我们竟然可以直接访问CMOS而不被系统阻止,前面已经说过,如果用户模式程序在Windows NT操作系统下使用IN或者OUT指令来存取I/O端口的话会被系统终止,但是我们却可以,这怎么可能呢?这是因为我运行程序前刚喝了脑x金!广告里面天天说,脑x金无所不能!~~~~~~呵呵,开个玩笑,这当然是因为有了Giveio驱动程序。
3.5 Giveio设备驱动程序
3.5.1 Giveio驱动程序的源代码
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; giveio - Kernel Mode Driver
; Demonstrate direct port I/O access from a user mode
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; E Q U A T E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
IOPM_SIZE equ 2000h ; sizeof I/O permission map
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
local status:NTSTATUS
local oa:OBJECT_ATTRIBUTES
local hKey:HANDLE
local kvpi:KEY_VALUE_PARTIAL_INFORMATION
local pIopm:PVOID
local pProcess:LPVOID
invoke DbgPrint, $CTA0("giveio: Entering DriverEntry")
mov status, STATUS_DEVICE_CONFIGURATION_ERROR
lea ecx, oa
InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL
invoke ZwOpenKey, addr hKey, KEY_READ, ecx
.if eax == STATUS_SUCCESS
push eax
invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
pop ecx
.if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )
invoke DbgPrint, $CTA0("giveio: Process ID: %X"), \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [kvpi]).Data
; Allocate a buffer for the I/O permission map
invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
mov pIopm, eax
lea ecx, kvpi
invoke PsLookupProcessByProcessId, \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
.if eax == STATUS_SUCCESS
invoke DbgPrint, $CTA0("giveio: PTR KPROCESS: %08X"), pProcess
invoke Ke386QueryIoAccessMap, 0, pIopm
.if al != 0
; I/O access for 70h port
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
invoke Ke386SetIoAccessMap, 1, pIopm
.if al != 0
invoke Ke386IoSetAccessProcess, pProcess, 1
.if al != 0
invoke DbgPrint, $CTA0("giveio: I/O permission is successfully given")
.else
invoke DbgPrint, $CTA0("giveio: I/O permission is failed")
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
invoke ObDereferenceObject, pProcess
.else
mov status, STATUS_OBJECT_TYPE_MISMATCH
.endif
invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
mov status, STATUS_INSUFFICIENT_RESOURCES
.endif
.endif
invoke ZwClose, hKey
.endif
invoke DbgPrint, $CTA0("giveio: Leaving DriverEntry")
mov eax, status
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
:make
set drv=giveio
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj
del %drv%.obj
echo.
pause
这个驱动的代码是基于Dale Roberts写的一个著名的例子(giveio)改编的,我决定使用它是因为它在这篇教程中使用真是太合适了!
3.5.2 I/O许可位图
谜底揭晓了:我们的驱动程序修改了I/O许可位图(I/O permission bit map,IOPM),这样进程就被允许自由地存取I/O端口,这方面详细资料见http://www.intel.com/design/intarch/techinfo/pentium/PDF/inout.pdf
每个进程都有自己的I/O许可位图,每个单独的I/O端口的访问权限都可以对每个进程进行单独授权,如果相关的位被设置的话,对对应端口的访问就是被禁止的,如果相关的位被清除,那么进程就可以访问对应的端口。既然I/O地址空间由64K个可单独寻址的8位I/O端口组成,IOPM表的最大尺寸就是2000h字节(注:每个端口的权限用1个bit表示,64K个端口除以8得到的就是IOPM的字节数,也就是65536/8=8192字节=2000h字节)。
TSS的设计意图是为了在任务切换的时候保存处理器状态,从执行效率的考虑出发,Windows NT并没有使用这个特征,它只维护一个TSS供多个进程共享,这就意味着IOPM也是共享的,因此某个进程改变了IOPM的话,造成的影响是系统范围的。
ntoskrnl.exe中有些未公开的函数是用来维护IOPM的,它们是Ke386QueryIoAccessMap和Ke386SetIoAccessMap函数。
Ke386QueryIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID
Ke386QueryIoAccessMap函数从TSS中拷贝2000h字节的当前IOPM到指定的内存缓冲区中,缓冲区指针由pIopm参数指定。
各参数描述如下:
◎ dwFlag--0表示将全部缓冲区用0FFh填写,也就是所有的位都被设置,所有的端口都被禁止访问;1表示从TSS中将当前IOPM拷贝到缓冲区中
◎ pIopm--用来接收当前IOPM的缓冲区指针,注意缓冲区的大小不能小于2000h字节
如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
Ke386SetIoAccessMap proto stdcall dwFlag:DWORD, pIopm:PVOID
Ke386SetIoAccessMap函数刚好相反,它从pIopm参数指定的缓冲区中拷贝2000h字节的IOPM到TSS中去。
各参数描述如下:
◎ dwFlag--这个参数只能是1,其他任何值函数都会返回失败
◎ pIopm--指向包含IOPM数据的缓冲区,缓冲区的尺寸不能小于2000h字节
如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
当IOPM拷贝到TSS后,IOPM的偏移指针必须被定位到新的数据中去,这可以通过Ke386IoSetAccessProcess函数来完成,这也是ntoskrnl.exe中的一个很有用的未公开函数。
Ke386IoSetAccessProcess proto stdcall pProcess:PTR KPROCESS, dwFlag:DWORD
Ke386IoSetAccessProcess允许或者禁止对进程使用IOPM。其参数说明如下:
◎ pProcess--指向KPROCESS结构(后面会对这个结构进行解释)
◎ dwFlag--0表示禁止对I/O端口进行存取,将IOPM的偏移指针指到TSS段外面;1表示允许存取I/O端口,将IOPM的偏移指针指到TSS段的88h中
如果函数执行成功的话会在al中返回非0值(不是eax);如果执行失败会在al(不是eax)中返回零。
顺便提一下,ntoskrnl中的所有函数都有前缀,通过这个前缀你就可以辨别该函数属于系统功能中的哪一类。不同的前缀表示不同的功能--如i前缀表示内部使用(internal)、p表示私有函数(private)、f表示fastcall。再如,Ke表示内核函数(kernel),Psp表示内部进程支持函数(internal process support),Mm表示内存管理函数(Memory Manager)等等。
Ke386IoSetAccessProcess函数的第一个参数指向进程对象,也就是KPROCESS结构(在\include\w2k\w2kundoc.inc中定义),我特地在文件名中加了个w2k前缀是因为在不同的Windows NT版本中,未公开的数据结构的定义可能会有所变动,所以将使用这个include文件后编出的驱动程序在XP中使用可能不是个好主意。Ke386IoSetAccessProcess会将KPROCESS结构中IopmOffset字段的值设置为合适的值。
3.5.3 从注册表中读取信息
在调用Ke386IoSetAccessProcess的时候要用到进程对象的指针,有好几种办法可以获得该指针,我选择了最简单的办法--使用进程ID,这就是DateTime.exe程序首先获取当前进程的ID并把它保存在注册表中的原因。在这里我们使用注册表在用户模式的代码以及内核模式的驱动程序之间传递参数。由于DriverEntry过程是在系统进程环境中运行的,所以不这样做的话,我们无法得知它究竟是被哪个进程所启动的。
DriverEntry过程的第二个参数--pusRegistryPath是指向注册表键路径的字符串,我们可以用它来获取保存的进程ID。
现在来看看这一切是怎么实现的:
lea ecx, oa
InitializeObjectAttributes ecx, pusRegistryPath, 0, NULL, NULL
在调用ZwOpenKey函数前,我们必须先初始化OBJECT_ATTRIBUTES结构(在\include\w2k\ntdef.inc中定义),我用InitializeObjectAttributes宏来完成这个功能,但读者以后最好手工来完成这个工作,因为InitializeObjectAttributes宏可能并不像你预料的那样运行(注:宏中间可能用到很多寄存器,但是有些寄存器的值可能是你需要保存的,过多的使用宏以后,寄存器在哪里被修改了你都不知道),你也可以这样做:
lea ecx, oa
xor eax, eax
assume ecx:ptr OBJECT_ATTRIBUTES
mov [ecx].dwLength, sizeof OBJECT_ATTRIBUTES
mov [ecx].RootDirectory, eax ; NULL
push pusRegistryPath
pop [ecx].ObjectName
mov [ecx].Attributes, eax ; 0
mov [ecx].SecurityDescriptor, eax ; NULL
mov [ecx].SecurityQualityOfService, eax ; NULL
assume ecx:nothing
ZwOpenKey函数的第一个参数指向一个变量,用来返回注册表键的句柄;第二个参数是存取注册表的权限,你应该注意到ecx中保存有指向已经初始化的该注册表键的对象属性结构的指针。
invoke ZwOpenKey, addr hKey, KEY_READ, ecx
.if eax == STATUS_SUCCESS
push eax
invoke ZwQueryValueKey, hKey, $CCOUNTED_UNICODE_STRING("ProcessId", 4), \
KeyValuePartialInformation, addr kvpi, sizeof kvpi, esp
pop ecx
ZwQueryValueKey返回注册表键的句柄,我们可以用它从注册表中得到保存的进程ID,该函数的第二个参数是要获取数据的注册表键的名称,例子程序中我使用了$CCOUNTED_UNICODE_STRING宏来定义UNICODE_STRING结构和里面的字符串数据,如果你不喜欢使用宏,那么可以用下面的常规方法:
usz dw 'U', 'n', 'i', 'c', 'o', 'd', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g', 0
us UNICODE_STRING {sizeof usz - 2, sizeof usz, offset usz}
但是我可不喜欢这么麻烦,所以我写了下面这些宏:COUNTED_UNICODE_STRING、$COUNTED_UNICODE_STRING、CCOUNTED_UNICODE_STRING和$CCOUNTED_UNICODE_STRING(定义在\Macros\Strings.mac中)。
第三个参数指明要获取的数据类型,KeyValuePartialInformation是一个常量(在\include\w2k\ntddk.inc中定义);第四和第五个参数是指向KEY_VALUE_PARTIAL_INFORMATION结构的指针以及结构的长度,在结构的Data字段中我们会得到我们需要的进程ID;最后一个参数是指向返回数据字节数的指针,把它指向堆栈的话,就不必重新再分配一个变量了。
(注:原文作者这里用的一个小技巧可以学习一下,前面一句push eax,然后后面调用的最后一个参数是esp,在这里压入堆栈的堆栈指针正是指向push eax指令保留的堆栈空间,所以函数执行的时候在这里返回一个值,后面一句pop ecx实际上是弹出了函数返回在里面的数值,千万不要认为弹出的原始的eax值,这种方法可以免去定义一个临时使用的变量的麻烦,当然限制条件就是该临时变量的指针参数必须是函数的最后一个参数才行。这个技巧在整个教程中多次用到,请注意理解!)
3.5.4 让用户模式的进程可以存取I/O端口
.if ( eax != STATUS_OBJECT_NAME_NOT_FOUND ) && ( ecx != 0 )
invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
mov pIopm, eax
调用ZwQueryValueKey成功后,我们使用MmAllocateNonCachedMemory 函数来申请一段不被cache的虚拟内存地址空间,供IOPM数据使用。
lea ecx, kvpi
invoke PsLookupProcessByProcessId, \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
.if eax == STATUS_SUCCESS
invoke Ke386QueryIoAccessMap, 0, pIopm
然后将进程ID传递给PsLookupProcessByProcessId函数,这样就可以在pProcess中得到指向进程对象的指针,后面的Ke386QueryIoAccessMap的函数将IOPM拷贝到缓冲区中。
.if al != 0
mov ecx, pIopm
add ecx, 70h / 8
mov eax, [ecx]
btr eax, 70h MOD 8
mov [ecx], eax
mov ecx, pIopm
add ecx, 71h / 8
mov eax, [ecx]
btr eax, 71h MOD 8
mov [ecx], eax
invoke Ke386SetIoAccessMap, 1, pIopm
.if al != 0
invoke Ke386IoSetAccessProcess, pProcess, 1
.if al != 0
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
.else
mov status, STATUS_IO_PRIVILEGE_FAILED
.endif
现在将70h和71h号端口对应的数据位清除,并使用Ke386IoSetAccessProcess函数将修改后的IOPM写回去,以便存取这两个端口。
invoke ObDereferenceObject, pProcess
.else
mov status, STATUS_OBJECT_TYPE_MISMATCH
.endif
先前对PsLookupProcessByProcessId函数的调用会使进程对象的引用计数加1,每次当一个对象被引用的时候,对象管理器将对象的引用计数加1,并返回对象的指针;当内核模式程序完成对一个对象的操作后,应该再次调用对象管理器将对象的引用计数减1。同样道理,每次对象的句柄被获取后,引用计数也会被加1,当对象句柄被关闭后,引用计数减1,这是因为句柄也是用来访问对象的。所以,即使是一个对象的所有句柄都被关闭,如果系统还要用到这个对象的话,那么它的引用计数可能还是正数。只有当对象的引用计数减少到0的时候,对象管理器才将对象从内存中删除。
调用ObDereferenceObject函数可以将对象的引用计数减1,它的返回值是对象的先前状态。
invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
invoke DbgPrint, $CTA0("giveio: Call to MmAllocateNonCachedMemory failed")
mov status, STATUS_INSUFFICIENT_RESOURCES
.endif
.endif
invoke ZwClose, hKey
.endif
接下来调用MmFreeNonCachedMemory来释放内存,并调用ZwClose函数关闭注册表句柄。
到这里为止,所有的工作都完成了,虽然驱动还是返回一个错误代码,系统还是会将它直接从内存中清除,但是用户模式的进程已经可以直接访问2个I/O端口了。
在这个例子中我们举例访问了CMOS,通过同样的办法改造前面的beeper.sys例子,我们也可以在用户模式进程中让系统扬声器发声。但是要记住的是:你还是无法直接在用户模式进程中使用类似于cli或者sti之类的特权指令,也不能直接调用hal.dll之类的dll中的函数,因为它们是在内核地址空间中运行的。你唯一能做到的就是用这种技巧访问所有的65535个I/O端口而已。
invoke MmAllocateNonCachedMemory, IOPM_SIZE
.if eax != NULL
mov pIopm, eax
invoke RtlZeroMemory, pIopm, IOPM_SIZE
lea ecx, kvpi
invoke PsLookupProcessByProcessId, \
dword ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data, addr pProcess
.if eax == STATUS_SUCCESS
invoke Ke386SetIoAccessMap, 1, pIopm
.if al != 0
invoke Ke386IoSetAccessProcess, pProcess, 1
.endif
invoke ObDereferenceObject, pProcess
.endif
invoke MmFreeNonCachedMemory, pIopm, IOPM_SIZE
.else
mov status, STATUS_INSUFFICIENT_RESOURCES
.endif
还要注意的是:访问系统扬声器或者访问CMOS都是很安全的,但是存取其他的I/O端口可能是很危险的,因为在用户模式下,你无法保证和其他进程的访问保持同步状态。
3.6 关于驱动程序的调试
现在可以更详细地讨论一下驱动的调试了,前面已经提到过,我们最好用SoftICE来调试驱动程序。
为了产生一个断点,读者可以在驱动代码的某处直接放上一个"Int 3"指令,"Int 3"会产生一个软件断点,这样就可以被SoftICE一类的内核调试器捕捉到。在此之前,要确定INT 3的捕捉是被打开的。在SoftICE的命令控制窗口中打I3HERE命令可以做到这一点,更详细的说明读者可以参考SoftICE的命令手册。要注意的是:如果没有调试器来捕捉断点的话,断点指令引起的结果就是一个BSOD蓝屏死机画面,所以在启动驱动前不要忘了先敲"i3here on"命令。在最新的SoftICE版本中,内核模式地址空间中的int 3捕捉默认就是打开的。
程序中还不时地调用了DbgPrint函数,这个函数会在调试器窗口中显示一个字符串,SoftICE可以很好地与之配合,你也可以用Mark Russinovich(www.sysinternals.com)写的DebugView软件来监视调试信息的输出。
4. I/O子系统
※ 和本节内容相关的源代码见KmdKit\examples\simple\VirtToPhys
4.1 I/O管理器
在用户模式下,我们可以通过访问某个地址来直接调用dll中的函数,与此不同的是,从系统的稳定性考虑,在内核模式下这样做的话是很危险的。所以,系统提供了和内核模式通讯的媒介--I/O管理器,它是I/O子系统的部件之一。I/O管理器将应用程序、系统部件和设备连接起来,并定义了一个架构来支持设备驱动程序。
图4.1是I/O管理器如何在用户模式程序和驱动程序之间进行沟通的简单图解。
图4.1 I/O子系统的简单架构
从上图可以看到,所有用户模式程序到设备(包括设备驱动程序)的调用都必须通过I/O管理器来完成。
一般来说,用户模式的操作都被转换成了对具体硬件设备的I/O操作,仅对于某些设备,设备由驱动程序来创建和控制,这些设备就是虚拟设备。当然,创建这些设备并不意味着你创造了什么硬件(不然我每天创建n个显卡再卖掉,然后换BMW,呵呵~~~~),而仅仅是在内存中创建了一个新的对象而已。每个对象和一个物理设备或者逻辑设备对应,用于描述它们的特征。
创建设备后,驱动程序告诉I/O管理器:"这里有个我控制的设备,如果你收到了操作这个设备的I/O请求的话,直接发给我好了,剩下的由我来搞定!"。驱动程序知道如何对自己管理的设备进行I/O操作,I/O管理器唯一的职责在于创建I/O请求并把它发送给适当的设备驱动程序。用户模式的代码不知道(也不必知道)其中的细节,也不用知道究竟是哪个驱动程序在管理哪个设备。
4.2 VirToPhys驱动程序的控制程序
4.2.1 控制程序源代码
严格地说,这个代码包括了注册和启动驱动程序,以及作为客户端程序和设备进行通讯的代码。
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; VirtToPhys.asm - Driver Control Program for VirtToPhys driver
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\advapi32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\advapi32.lib
include \masm32\include\winioctl.inc
include \masm32\Macros\Strings.mac
include common.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; BigNumToString
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
BigNumToString proc uNum:UINT, pszBuf:LPSTR
; This function accepts a number and converts it to a
; string, inserting commas where appropriate.
local acNum[32]:CHAR
local nf:NUMBERFMT
invoke wsprintf, addr acNum, $CTA0("%u"), uNum
and nf.NumDigits, 0
and nf.LeadingZero, FALSE
mov nf.Grouping, 3
mov nf.lpDecimalSep, $CTA0(".")
mov nf.lpThousandSep, $CTA0(" ")
and nf.NegativeOrder, 0
invoke GetNumberFormat, LOCALE_USER_DEFAULT, 0, addr acNum, addr nf, pszBuf, 32
ret
BigNumToString endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; start
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
start proc uses esi edi
local hSCManager:HANDLE
local hService:HANDLE
local acModulePath[MAX_PATH]:CHAR
local _ss:SERVICE_STATUS
local hDevice:HANDLE
local adwInBuffer[NUM_DATA_ENTRY]:DWORD
local adwOutBuffer[NUM_DATA_ENTRY]:DWORD
local dwBytesReturned:DWORD
local acBuffer[256+64]:CHAR
local acThis[64]:CHAR
local acKernel[64]:CHAR
local acUser[64]:CHAR
local acAdvapi[64]:CHAR
local acNumber[32]:CHAR
invoke OpenSCManager, NULL, NULL, SC_MANAGER_ALL_ACCESS
.if eax != NULL
mov hSCManager, eax
push eax
invoke GetFullPathName, $CTA0("VirtToPhys.sys"), \
sizeof acModulePath, addr acModulePath, esp
pop eax
invoke CreateService, hSCManager, $CTA0("VirtToPhys"), \
$CTA0("Virtual To Physical Address Converter"), \
SERVICE_START + SERVICE_STOP + DELETE, SERVICE_KERNEL_DRIVER,\
SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, addr acModulePath,\
NULL, NULL, NULL, NULL, NULL
.if eax != NULL
mov hService, eax
; Driver's DriverEntry procedure will be called
invoke StartService, hService, 0, NULL
.if eax != 0
; Driver will receive I/O request packet (IRP) of type IRP_MJ_CREATE
invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ+GENERIC_WRITE,\
0, NULL, OPEN_EXISTING, 0, NULL
.if eax != INVALID_HANDLE_VALUE
mov hDevice, eax
lea esi, adwInBuffer
assume esi:ptr DWORD
invoke GetModuleHandle, NULL
mov [esi][0*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
mov [esi][1*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
mov [esi][2*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
mov [esi][3*(sizeof DWORD)], eax
lea edi, adwOutBuffer
assume edi:ptr DWORD
; Driver will receive IRP of type IRP_MJ_DEVICE_CONTROL
invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
esi, sizeof adwInBuffer, \
edi, sizeof adwOutBuffer, \
addr dwBytesReturned, NULL
.if ( eax != 0 ) && ( dwBytesReturned != 0 )
invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
addr acModulePath, sizeof acModulePath
lea ecx, acModulePath[eax-5]
.repeat
dec ecx
mov al, [ecx]
.until al == '\'
inc ecx
push ecx
CTA0 "%s \t%08Xh\t%08Xh ( %s )\n", szFmtMod
invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
pop ecx
invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
[esi][0*(sizeof DWORD)], \
[edi][0*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
[esi][1*(sizeof DWORD)], \
[edi][1*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
[esi][2*(sizeof DWORD)], \
[edi][2*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
[esi][3*(sizeof DWORD)], \
[edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acBuffer,\
$CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
addr acThis, addr acKernel, addr acUser, addr acAdvapi
assume esi:nothing
assume edi:nothing
invoke MessageBox,NULL,addr acBuffer,$CTA0("Modules Base Address"), \
MB_OK + MB_ICONINFORMATION
.else
invoke MessageBox,NULL,$CTA0("Can't send control code to device."),\
NULL,MB_OK + MB_ICONSTOP
.endif
; Driver will receive IRP of type IRP_MJ_CLOSE
invoke CloseHandle, hDevice
.else
invoke MessageBox, NULL, $CTA0("Device is not present."), NULL, MB_ICONSTOP
.endif
; DriverUnload proc in our driver will be called
invoke ControlService, hService, SERVICE_CONTROL_STOP, addr _ss
.else
invoke MessageBox, NULL, $CTA0("Can't start driver."), NULL, MB_OK + MB_ICONSTOP
.endif
invoke DeleteService, hService
invoke CloseServiceHandle, hService
.else
invoke MessageBox, NULL, $CTA0("Can't register driver."), NULL, MB_OK + MB_ICONSTOP
.endif
invoke CloseServiceHandle, hSCManager
.else
invoke MessageBox, NULL, $CTA0("Can't connect to Service Control Manager."), NULL, \
MB_OK + MB_ICONSTOP
.endif
invoke ExitProcess, 0
start endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end start
代码将输入的数据发送给设备,并将设备返回的数据格式化并显示出来,这里只有少量新的东西,它们是3个调用:CreateFile、DeviceIoControl和CloseHandle,这些调用的参数都是设备(注意:不是驱动)的句柄。
4.2.2 设备对象
在被装载后,VirtToPhys驱动程序创建了一个名为"devVirtToPhys"的设备(dev前缀并不是必须的,但我还是打算把它加上--原因后面再解释)。
设备的名称放在对象管理器中,对象管理器是系统中负责创建、删除、保护和跟踪对象的组件,根据约定,设备对象存放在\Device目录下,应用程序用Win32 API是无法直接访问的。
要想研究对象管理器维护的对象名称列表的话,可以使用工具Windows Object Explorer (WinObjEx)(见http://www.wasm.ru/)或者Mark Russinovich编写的Object Viewer(见http://www.sysinternals.com/)。
要观察VirtToPhys程序在你的计算机上创建的对象,仅仅需要运行VirtToPhys.exe程序,但是不要关闭窗口。
图4.2 对象管理器中的devVirtToPhys设备对象
图4.3 devVirtToPhys对象的属性
4.2.3 驱动程序对象
VirtToPhys驱动程序对象(注意这里没有任何前缀)被放在\Driver目录中。
图4.4 对象管理器中的VirtToPhys驱动程序对象
4.2.4 符号连接对象
内部设备名称无法被Win32应用程序使用,因为除\BaseNamedObjects和\??外的所有目录对用户模式程序来说都是不可见的--而设备名称又必须放置于对象管理器的特定目录下才行。
"\??"目录中包括了对真实存在的设备名称的符号链表,设备驱动程序负责在这个目录中创建一个符号连接,这样设备就可以被Win32应用程序访问。
因此,如果我们的驱动程序对应的设备对象希望被用户模式的代码打开的话,就必须在"\??"目录中创建一个符号连接,指向"\Device"目录中的设备对象,然后,当调用者需要获取设备句柄时,I/O管理器就能够找到它。
顺便提一句,你也可以在用户模式下通过QueryDosDevice函数检测符号连接是否存在,还可以通过DefineDosDevice函数来修改它。
打开"\??"目录后,你可以看到大量的符号连接,早先在Windows NT 4系统中,这个目录的名称为\DosDevices,后来才更名为"\??",这主要是基于性能上的考虑,因为这样目录名称按照字母表顺序会排在最前面。
为了向前兼容,对象管理器中的"\DosDevices"目录直接连到"\??"目录。
VirtToPhys驱动程序在"\??"目录中创建了指向"devVirtToPhys"设备的符号连接"slVirtToPhys",真实设备的全名是"\Device\devVirtToPhys",这里我用了"dev"前缀。
图4.5 对象管理器中的slVirtToPhys符号连接
图4.6 slVirtToPhys符号连接的对象属性
为各种对象的名称加上前缀是为了区别它们,最好不要将设备名、符号连接名称和驱动的名称混淆起来。另外,一个要点是符号连接名称必须定向到一个有效的设备名上面;另一个要点是,在同一个设备目录下不能有两个同名的对象,正像文件系统的一个目录下不能有两个同名文件一样。
这样,当StartService函数执行后,系统中就多了三个新的对象:"\Driver\VirtToPhys"驱动、"\Device\devVirtToPhys"设备和符号连接"\??\slVirtToPhys"。
不知道读者是否还记得,第二课"服务"中曾经提到过"\??",那是在驱动的路径名中出现的,如"\??\C:\masm32\...",实际上,"\??\C:"是一个符号连接,它指向名为"\Device\HarddiskVolume1"的内部设备,也就是系统中第一个物理硬盘上面的第一个分区。
4.2.5 文件对象
现在回头来看看源代码,当驱动被启动后,我们打算去调用它。为了完成这个功能,我们只需使用CreateFile函数来打开驱动,以此获得一个文件句柄。
文档中关于CreateFile的描述真是太多了,但是只有很少一部分是和设备驱动程序相关的。
CreateFile proto stdcall lpFileName:LPCSTR, dwDesiredAccess:DWORD, \
dwShareMode:DWORD, lpSecurityAttributes:LPVOID, \
dwCreationDistribution:DWORD, dwFlagsAndAttributes:DWORD, \
hTemplateFile:HANDLE
这个函数可以创建或者打开一个已存在的对象,而不仅仅是文件(这似乎和它的名字不符,但是有很多Create打头的函数都有创建和打开的功能),微软其实可以干脆把它命名为CreateObject好了。
在这里,设备也可以作为对象来考虑。
函数的参数描述如下:
◎ lpFileName--指向以0结尾的表示设备名称的字符串,这里要用到指向设备对象的符号连接名
◎ dwDesiredAccess--指定访问设备的方式,可以有两个取值:GENERIC_READ表示写操作,允许将数据写到设备中;GENERIC_WRITE表示读操作,允许从设备读取数据,这两个值可以合并起来使用
◎ dwShareMode--指定设备是否可以被共享,0表示设备不能被共享,这样并发的访问会失败,直到句柄被关闭为止;要共享设备的话,可以指定下面的两个值:FILE_SHARE_READ表示可以并发地读取设备,FILE_SHARE_WRITE表示可以并发地写设备
◎ lpSecurityAttributes--指向SECURITY_ATTRIBUTES结构的指针,在此无用,所以可以指定为NULL
◎ dwCreationDistribution--指明当文件存在或不存在时函数采取的动作,对于设备来说,这个参数应该使用OPEN_EXISTING
◎ dwFlagsAndAttributes--文件属性,在这里总是使用0
◎ hTemplateFile--指定文件模板的句柄,在这里总是使用0
如果CreateFile函数成功地创建或者打开了指定的设备,那么返回值就是设备句柄,否则返回值是INVALID_HANDLE_VALUE。大部分需要返回句柄的Windows函数在失败时返回0,但是CreateFile函数返回的INVALID_HANDLE_ VALUE值的定义却是-1。
我们这样使用CreateFile函数:
invoke CreateFile, $CTA0("\\\\.\\slVirtToPhys"), GENERIC_READ + GENERIC_WRITE, \
0, NULL, OPEN_EXISTING, 0, NULL
最后五个参数就不用解释了吧?第二个参数合并了GENERIC_READ以及GENERIC_WRITE,表示既要将数据写到设备,也要从设备读取处理的结果数据。
现在来看看第一个参数,这是指向符号连接名称的字符串指针,名称格式是"\\.\slVirtToPhys","\\.\"是Win32中定义本地计算机的方法,CreateFile函数实际上是NtCreateFile函数的封装(位于\%SystemRoot%\System32\ntdll.dll中),后者将访问定向到系统服务中去(注意不要和Win32服务进程混淆)。
系统服务是各种子系统环境到内核的入口点,系统服务的表现就是在x86处理器的机器上执行int 2eh(Windows NT/W2K) 或sysenter指令(Windows XP/2003),触发执行这些指令使线程切换到内核模式,并进入系统服务分派程序。
NtCreateFile将本地计算机的别名"\\.\"用"\??"代替(这样\\.\slVirtToPhys就变成了\?? \slVirtToPhys)并调用内核的ObOpenObjectByName函数,通过符号连接名称,ObOpenObjectByName函数找到\Device\devVirtToPhys对象并返回它的指针。NtCreateFile使用这个指针创建新的文件对象并返回句柄。
操作系统把所有的I/O操作请求抽象化成一个虚拟的文件,隐藏了目标设备的I/O操作可能在结构上并不等同于文件的事实,然后由驱动程序负责将对虚拟文件的请求转换成对具体硬件的请求,这种抽象可以推广到所有用户进程和设备之间的界面,所有对这种虚拟文件的读写操作都被当作是简单的流操作来处理。
在CreateFile返回前,I/O管理器创建了IRP_MJ_CREATE类型的IRP并将其传给驱动处理,驱动中负责响应这个IRP的子程序代码会在发起I/O请求(就是调用CreateFile函数的代码)的线程环境中执行,该线程的IRQL等于PASSIVE_LEVEL。如果驱动的子程序成功返回,那么对象管理器在进程的句柄表中为文件对象创建一个句柄,然后将其一层层地返回,直到返回到CreateFile函数。
新创建的文件对象是个实体对象,并不列在对象管理器中,读者可以用Mark Russinovich编写的Process Explorer工具来观察到它们(可以从http://www.sysinternals.com下载)。
图4.7 文件对象
图4.8 文件对象的属性
现在来总结一下,"\\.\slVirtToPhys"会被转换成符号连接"\??\slVirtToPhys",最终用来找到需要的"\Device\devVirtToPhys"设备。然后可以从负责维护该设备的驱动程序中取得设备对象DEVICE_OBJECT,接下来I/O管理器将IRP_MJ_CREATE请求传递给驱动,驱动程序知道如何处理这个请求。如果驱动打算处理该请求,那么它返回成功代码,这样对象管理器创建对应这个设备的虚拟文件句柄并将它返回给用户模式代码。
句柄和符号连接为间接访问系统资源提供服务,这种"间接"方式将应用程序和系统的数据结构隔离开来。
4.2.6 和设备通讯
.if eax != INVALID_HANDLE_VALUE
mov hDevice, eax
CreateFile函数返回有效的设备句柄后,我们将它保存在hDevice变量中,现在可以用ReadFile、WriteFile以及DeviceIoControl函数来和设备通讯了。DeviceIoControl函数是用来和设备通讯的通用函数,它的原型如下:
DeviceIoControl proto stdcall hDevice:HANDLE, dwIoControlCode:DWORD, \
lpInBuffer:LPVOID, nInBufferSize:DWORD, \
lpOutBuffer:LPVOID, nOutBufferSize:DWORD, \
lpBytesReturned:LPVOID, lpOverlapped:LPVOID
DeviceIoControl函数的参数比CreateFile多,但用起来都很简单。
◎ hDevice--设备的句柄
◎ dwIoControlCode--控制代码,指出要进行什么操作,详细内容后面再做解释
◎ lpInBuffer--指向包含操作所需的数据的缓冲区指针,如果控制代码指明的操作并不需要输入数据的话,这里可以用NULL
◎ nInBufferSize--lpInBuffer参数指向的缓冲区的大小
◎ lpOutBuffer--指向用来接收输出数据的缓冲区,如果dwIoControlCode指明的操作不产生输出数据的话,这里可以用NULL
◎ nOutBufferSize--lpOutBuffer参数指向的缓冲区的大小
◎ lpBytesReturned--指向一个变量,用来返回放入lpOutBuffer缓冲区的数据的数量
◎ lpOverlapped--指向OVERLAPPED结构,这个参数仅在异步操作的时候才需要。我们的操作是同步的(就是在驱动的过程返回前DeviceIoControl函数也不返回),所以在这里使用NULL
4.2.7 I/O控制代码
设备驱动程序可以被当作内核模式函数包来看待,I/O控制代码就是用来指定访问其中的哪个函数的。DeviceIoControl函数的dwIoControlCode参数就是这个代码,它指出了我们需要进行的操作,以及如何进行操作。
控制代码是32位数字型常量,可以CTL_CODE宏来定义,它们定义在winioctl.inc和ntddk.inc文件中。
图4.9 I/O控制代码的定义
控制代码中各数据位字段的含义如下:
◎ DeviceType--设备类型(16bit)指出了设备的类型,微软保留了0-7FFFh的取值,剩下的8000h-0FFFFh供开发商定义新的内核模式驱动程序。我们可以在\include\w2k\ntddk.inc文件中找到一组FILE_DEVICE_XXX符号常量,这些值都是微软保留的值,我们可以使用其中的FILE_DEVICE_UNKNOWN。当然你也可以定义另外一个FILE_DEVICE_XXX值
◎ Access--存取代码(2bit)指明应用程序存取设备的方式,由于这个字段只有2位,所以只有4种可能性:
• FILE_ANY_ACCESS (0)--最大的存取权限,就是什么操作都可以
• FILE_READ_ACCESS (1)--读权限,设备将数据传递到指定的缓冲区
• FILE_WRITE_ACCESS (2)--写权限,可以从内存中向设备传递数据
• FILE_READ_ACCESS or FILE_WRITE_ACCESS (3)--读写权限,设备和内存缓冲区之间可以互相传递数据
◎ Function--功能代码(12bit)用来描述要进行的操作,我们可以用800h-0FFFh来定义自己的I/O控制代码,0-7FFh之间的值是被微软保留的,用来定义公用的I/O控制代码
◎ Method--缓冲模式(2bit)表示I/O管理器如何对输入和输出的数据进行缓冲,这个字段的长度是2位,所以有4种可能性:
• METHOD_BUFFERED (0)--对I/O进行缓冲
• METHOD_IN_DIRECT (1)--对输入不进行缓冲
• METHOD_OUT_DIRECT (2)--对输出不进行缓冲
• METHOD_NEITHER (3)--都不缓冲
缓冲模式的管理我们会在后面进行更详细的讨论,当前最重要的是,虽然进行缓冲会带来一些额外的内存开销,但却是最安全的,因为系统已经做好了相关的全部工作。在传输的数据小于一页(4Kb)的时候,驱动程序通常使用缓冲方式的I/O,因为对大量小块内存进行内存锁定带来的开销也是很大的。在VirtToPhys驱动程序中,我们使用带缓冲的方式。
读者可以手工去定义I/O控制代码,但是使用CTL_CODE宏会方便得多,它提供了创建IOCTL值的算法,具体如下:
CTL_CODE MACRO DeviceType:=<0>, Function:=<0>, Method:=<0>, Access:=<0>
EXITM %(((DeviceType) SHL 16) OR ((Access) SHL 14) OR ((Function) SHL 2) OR (Method))
ENDM
前面我曾说过,CTL_CODE宏在winioctl.inc文件和ntddk.inc文件中都有定义。
在例子程序中,由于定义的NUM_DATA_ENTRY、DATA_SIZE常量和IOCTL_GET_PHYS_ADDRESS控制代码在服务控制程序以及驱动中都要被用到,所以我们将它放在一个单独的common.inc文件中,这样万一有所修改的话,就可以直接反映到两个代码中。
NUM_DATA_ENTRY equ 4
DATA_SIZE equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)
4.2.8 交换数据
现在回到驱动的源代码中看看:
lea esi, adwInBuffer
assume esi:ptr DWORD
invoke GetModuleHandle, NULL
mov [esi][0*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("kernel32.dll", szKernel32)
mov [esi][1*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("user32.dll", szUser32)
mov [esi][2*(sizeof DWORD)], eax
invoke GetModuleHandle, $CTA0("advapi32.dll", szAdvapi32)
mov [esi][3*(sizeof DWORD)], eax
我们在adwInBuffer缓冲区中填写需要进行转换的虚拟地址。
lea edi, adwOutBuffer
assume edi:ptr DWORD
invoke DeviceIoControl, hDevice, IOCTL_GET_PHYS_ADDRESS, \
esi, sizeof adwInBuffer, \
edi, sizeof adwOutBuffer, \
addr dwBytesReturned, NULL
调用DeviceIoControl的时候,我们将缓冲区传给驱动,这样它会将虚拟地址转换成物理地址。
.if ( eax != 0 ) && ( dwBytesReturned != 0 )
invoke GetModuleFileName, [esi][0*(sizeof DWORD)], \
addr acModulePath, sizeof acModulePath
lea ecx, acModulePath[eax-5]
.repeat
dec ecx
mov al, [ecx]
.until al == '\'
inc ecx
push ecx
CTA0 "%s \t%08Xh\t%08Xh ( %s )\n", szFmtMod
invoke BigNumToString, [edi][0*(sizeof DWORD)], addr acNumber
pop ecx
invoke wsprintf, addr acThis, addr szFmtMod, ecx, \
[esi][0*(sizeof DWORD)], \
[edi][0*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][1*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acKernel, addr szFmtMod, addr szKernel32, \
[esi][1*(sizeof DWORD)], \
[edi][1*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][2*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acUser, addr szFmtMod, addr szUser32, \
[esi][2*(sizeof DWORD)], \
[edi][2*(sizeof DWORD)], addr acNumber
invoke BigNumToString, [edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acAdvapi, addr szFmtMod, addr szAdvapi32, \
[esi][3*(sizeof DWORD)], \
[edi][3*(sizeof DWORD)], addr acNumber
invoke wsprintf, addr acBuffer,\
$CTA0("Module:\t\tVirtual:\t\tPhysical:\n\n%s\n%s%s%s"), \
addr acThis, addr acKernel, addr acUser, addr acAdvapi
assume esi:nothing
assume edi:nothing
invoke MessageBox,NULL,addr acBuffer,$CTA0("Modules Base Address"),\
MB_OK + MB_ICONINFORMATION
.else
invoke MessageBox,NULL,$CTA0("Can't send control code to device."),\
NULL,MB_OK + MB_ICONSTOP
.endif
如果DeviceIoControl函数成功返回,那么dwBytesReturned中的数值就等于驱动程序在adwOutBuffer缓冲区中返回的数据的长度,现在我们的任务就简单了,只要把返回值格式化一下并显示出来即可。我想后面的代码对读者来说应该是很好理解的。
图4.10 VirtToPhys.exe程序的输出
4.2.9 扫尾工作
invoke CloseHandle, hDevice
最后要做的是关闭设备句柄,这时I/O管理器向设备驱动程序发送两个IRP,第一个是IRP_MJ_CLEANUP,它告诉驱动程序设备句柄将要被关闭了;然后是IRP_MJ_CLOSE,它告诉驱动程序设备句柄已经被关闭了。你可以在收到IRP_MJ_CLEANUP时返回一个错误代码,这样就可以阻止设备句柄被关闭。驱动程序的子程序在处理这些IRP时,代码都是在发出I/O请求的线程环境中执行的(也就是调用CloseHandle的线程),它们的IRQL = PASSIVE_LEVEL。
在以后的教程中,我们继续讲解如何在驱动程序中处理IRP。
如果要让驱动在老版本的Windows NT中运行的话,读者需要将代码中的"\??"改成"\DosDevices"并重新编译,因为前面我已经说过,以前的Windows NT 4版本中"\??"目录的名称是"\DosDevices"。
(注:本文中主要讲述控制程序部分,驱动部分VirtToPhys.sys的全部源代码和结构解释详见下一节《全功能的驱动程序》)
5. 全功能的驱动程序分析
※ 本篇的源代码同第4节的源代码:KmdKit\examples\simple\VirtToPhys
5.1 VirtToPhys驱动程序的源代码
现在是到看看一个全功能驱动程序源代码的时候了,这里就是:
;@echo off
;goto make
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; VirtToPhys - Kernel Mode Driver
; Translates virtual addres to physical address
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.386
.model flat, stdcall
option casemap:none
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; I N C L U D E F I L E S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
include \masm32\include\w2k\ntstatus.inc
include \masm32\include\w2k\ntddk.inc
include \masm32\include\w2k\ntoskrnl.inc
include \masm32\include\w2k\w2kundoc.inc
includelib \masm32\lib\w2k\ntoskrnl.lib
include \masm32\Macros\Strings.mac
include ..\common.inc
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O N S T A N T S
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.const
CCOUNTED_UNICODE_STRING "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; GetPhysicalAddress
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
GetPhysicalAddress proc dwAddress:DWORD
mov eax, dwAddress
mov ecx, eax
shr eax, 22
shl eax, 2
mov eax, [0C0300000h][eax]
.if ( eax & (mask pde4kValid) )
.if !( eax & (mask pde4kLargePage) )
mov eax, ecx
shr eax, 10
and eax, 1111111111111111111100y
add eax, 0C0000000h
mov eax, [eax]
.if eax & (mask pteValid)
and eax, mask ptePageFrameNumber
and ecx, 00000000000000000000111111111111y
add eax, ecx
.else
xor eax, eax
.endif
.else
and eax, mask pde4mPageFrameNumber
and ecx, 00000000001111111111111111111111y
add eax, ecx
.endif
.else
xor eax, eax
.endif
ret
GetPhysicalAddress endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DispatchCreateClose
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
mov eax, pIrp
assume eax:ptr _IRP
mov [eax].IoStatus.Status, STATUS_SUCCESS
and [eax].IoStatus.Information, 0
assume eax:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, STATUS_SUCCESS
ret
DispatchCreateClose endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DispatchControl
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DispatchControl proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
local status:NTSTATUS
local dwBytesReturned:DWORD
and dwBytesReturned, 0
mov esi, pIrp
assume esi:ptr _IRP
IoGetCurrentIrpStackLocation esi
mov edi, eax
assume edi:ptr IO_STACK_LOCATION
.if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
.if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) &&\
( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )
mov edi, [esi].AssociatedIrp.SystemBuffer
assume edi:ptr DWORD
xor ebx, ebx
.while ebx < NUM_DATA_ENTRY
invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
mov [edi][ebx*(sizeof DWORD)], eax
inc ebx
.endw
mov dwBytesReturned, DATA_SIZE
mov status, STATUS_SUCCESS
.else
mov status, STATUS_BUFFER_TOO_SMALL
.endif
.else
mov status, STATUS_INVALID_DEVICE_REQUEST
.endif
assume edi:nothing
push status
pop [esi].IoStatus.Status
push dwBytesReturned
pop [esi].IoStatus.Information
assume esi:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, status
ret
DispatchControl endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverUnload
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverUnload proc pDriverObject:PDRIVER_OBJECT
invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax, pDriverObject
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
ret
DriverUnload endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; D I S C A R D A B L E C O D E
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
.code INIT
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
; DriverEntry
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
local status:NTSTATUS
local pDeviceObject:PVOID
mov status, STATUS_DEVICE_CONFIGURATION_ERROR
invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
0, FALSE, addr pDeviceObject
.if eax == STATUS_SUCCESS
invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
.if eax == STATUS_SUCCESS
mov eax, pDriverObject
assume eax:PTR DRIVER_OBJECT
mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)], offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)], offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)], offset DispatchControl
mov [eax].DriverUnload, offset DriverUnload
assume eax:nothing
mov status, STATUS_SUCCESS
.else
invoke IoDeleteDevice, pDeviceObject
.endif
.endif
mov eax, status
ret
DriverEntry endp
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
end DriverEntry
:make
set drv=VirtToPhys
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj
del %drv%.obj
move %drv%.sys ..
echo.
pause
5.2 驱动程序名称和符号连接名称
要描述设备以及符号连接的名称,就要先从UNICODE_STRING结构的定义讲起,我在前面已经提及,这种结构的字符串在内核中使用得非常普遍。
几乎在所有的驱动源码中--不管是用汇编还是用C写的--下面这样的字符定义序列是很常见的:
.const
uszDeviceName dw "\","D","e","v","i","c","e","\","D","e","v","N","a","m","e",0
uszSymbolicLinkName dw "\","?","?","\","D","e","v","N","a","m","e",0
.code
DriverEntry proc . . .
. . .
local usDeviceName:UNICODE_STRING
local usSymbolicLinkName:UNICODE_STRING
. . .
invoke RtlInitUnicodeString, addr usDeviceName, offset uszDeviceName
invoke RtlInitUnicodeString, addr usSymbolicLinkName, offset uszSymbolicLinkName
RtlInitUnicodeString函数的作用是计算Unicode字符串的大小并且填充UNICODE_STRING结构,一般来说,Unicode字符串都是在代码中静态定义的,并且在运行中保持不变,所以在链接的时候就把UNICODE_STRING结构给填好是完全可能的并且是很容易的,这样更容易理解、 更节省空间(省去8字节的UNICODE_STRING结构、最多3字节的对齐空间以及至少14字节调用RtlInitUnicodeString的代码)。这就是我为什么不喜欢以上代码的原因,我经常使用CCOUNTED_UNICODE_STRING宏来完成它,这样上面的代码就可以用2行来完成:
CCOUNTED_UNICODE_STRING "\\Device\\DevName", usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\??\\DevName", usSymbolicLinkName, 4
如果你认同我的做法的话,也可以在自己的驱动程序中这样定义驱动名称和符号连接名称:
.const
CCOUNTED_UNICODE_STRING "\\Device\\devVirtToPhys", g_usDeviceName, 4
CCOUNTED_UNICODE_STRING "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4
(注:原作者的宏在处理英文的Unicode字符串的时候是不错的,但是中文字符串就不行了,所以如果用到中文串,还是乖乖地动态转换最方便,常用的方法是先用RtlInitAnsiString函数生成一个ANSI_STRING结构,再用RtlAnsiStringToUnicodeString函数将ANSI_STRING转换到UNICODE_STRING即可,把这两句写成一个子程序或者宏的话,使用起来也是很方便的)。
在早些的Windows NT版本中,对象管理器中的"\??"目录是没有的,所以在那种情况下使用要将"\??"改为"\DosDevices",这种用法在后续的Windows版本中也可以使用。为了向前兼容,系统在根目录下创建了一个"\DosDevices"连接,直接指向"\??"目录。
5.3 编写DriverEntry过程
每个内核模式驱动程序必须公开一个名为DriverEntry(当然,你完全可以取另外的名称)的过程,用来初始化驱动程序使用的各种资源,如常用的数据结构等。I/O管理器在装载驱动的时候调用这个过程,该过程在IRQL = PASSIVE_LEVEL下运行,所以在过程中可以存取分页的内存。DriverEntry过程在系统进程上下文中运行。
在深入一步之前,请注意这一行:
.code INIT
所有这样标记的代码将被放入PE文件的INIT节区中,在驱动程序初始化后,这部分代码就再也用不着了。INIT节区中的代码可以在DriverEntry过程返回后被丢弃,系统会自己决定在合适的时候丢弃它。
对于我们这个小小的驱动来说,这样做似乎没有多大的意义,因为我们的驱动是32字节对齐的(链接的时候使用了/align:32参数),这样这个节区占用不到一页的内存空间,所以即使指定了"INIT"也不会使它被丢弃。但是早些版本的Windows NT驱动程序往往有个很大的DriverEntry过程,其中有创建设备对象、申请资源、配置设备等大量代码,这样做的话就能明显地节省内存,所以如果你的DriverEntry足够大的话,这样做的意义就很明显了。
mov status, STATUS_DEVICE_CONFIGURATION_ERROR
默认情况下,我们先把返回值设置成"失败",这样用户模式程序的StartService函数调用就会返回失败。
5.3.1 创建虚拟设备
invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
0, FALSE, addr pDeviceObject
既然驱动程序的主要作用是用于控制一些设备,包括物理设备、虚拟设备或者逻辑设备,那么我们首先必须将这些设备创建起来(本例中是虚拟设备),这可以通过调用IoCreateDevice函数来完成,函数将创建并初始化一个由驱动程序使用的设备对象(DEVICE_OBJECT结构),其原型如下:
IoCreateDevice proto stdcall DriverObject:PDRIVER_OBJECT, DeviceExtensionSize:DWORD, \
DeviceName:PUNICODE_STRING, DeviceType:DEVICE_TYPE, \
DeviceCharacteristics:DWORD, Exclusive:BOOL, \
DeviceObject: PDEVICE_OBJECT
函数的参数描述如下:
◎ DriverObject--指向驱动对象(DRIVER_OBJECT结构),每个驱动程序在DriverEntry过程中会通过参数收到一个指向它的驱动对象的指针
◎ DeviceExtensionSize--指定设备扩展结构的大小(注:I/O管理器将自动分配这个内存,并把设备对象中的DeviceExtension指针指向这块内存),扩展结构的数据结构定义由驱动程序自己决定,我们的驱动太简单了,就没必要使用这个了
◎ DeviceName--指向一个Unicode字符串,用来指定设备名称,该名称必须是全路径的名称,在这里路径的含义并不是指硬盘上的路径,而是指对象管理器命名空间中的路径。这个参数在本例中是必须的,因为我们必须创建一个命名的设备,否则就无法创建一个符号连接,那样用户模式的进程也就无法访问设备了。设备名称在系统中必须是唯一的(注:在其他的应用中,你也可以创建不命名的设备)
◎ DeviceType--在系统定义的FILE_DEVICE_XXX常数中选定一个,用于指定设备的类型,当然也可以使用自定义的类型来表示一个新的类别,这里我们使用FILE_DEVICE_UNKNOWN
◎ DeviceCharacteristics--指明设备的额外属性,本例中使用0
◎ Exclusive--指明设备对象是否必须被独占使用,也就是说同时只能有一个句柄可以向设备发送I/O请求,在CreateFile函数的dwShareMode参数中可以指明是否独占设备。我们并不需要独占设备,这样这里使用FALSE
◎ DeviceObject--指向一个变量,如果函数调用成功的话,变量中将返回指向新创建的设备对象(DEVICE_OBJECT结构)的指针。
接下来,如果对IoCreateSymbolicLink的调用失败的话,我们需要从系统中将设备删除,所以要将IoCreateDevice函数返回的设备对象指针保存起来,以便在删除设备的时候使用。
设备对象指针在卸载驱动的DriverUnload过程中也要用到,但是那时在驱动对象中也可以得到设备对象指针,所以没有必要专门定义一个全局变量将设备对象指针保留到那个时候。
5.3.2 创建符号连接
.if eax == STATUS_SUCCESS
invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
如果设备被成功地创建,那么为了使它能被Windows子系统"看见",我们还需要创建符号连接(前面已经介绍过什么是符号连接了,不是吗?),这可以通过调用IoCreateSymbolicLink来完成,该函数需要的两个参数都是UNICODE_STRING类型的字符串指针--用来指定已存在的设备名称和还有需要新创建的连接名称。
5.3.3 指定分派过程
.if eax == STATUS_SUCCESS
mov eax, pDriverObject
assume eax:PTR DRIVER_OBJECT
mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],offset DispatchCreateClose
mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],offset DispatchControl
符号连接成功创建后,就可以开始下一步了。
每个驱动程序都包括一个过程入口指针数组,用来指明不同的I/O请求被分派到那个函数来处理。每个驱动程序必须至少设置一个过程入口,用来处理IRP_MJ_XXX类型的请求。不同的驱动程序可以设置多个不同的过程入口,用来处理不同的IRP_MJ_XXX请求代码。例如,如果你需要得到"系统将要关闭"的通知的话,就必须"申明"处理该请求的分派过程,也就是在驱动对象的MajorFunction表中的IRP_MJ_SHUTDOWN一栏中填入该分派过程的地址。如果不需要处理某个请求,那么什么都不用做,因为I/O管理器在调用DriverEntry前默认将MajorFunction表中的每一项都填成了系统内部的IopInvalidDeviceRequest过程的地址,该过程会返回一个错误代码。
所以,你的责任就是要为每个你想要响应的I/O代码提供分派过程。
在驱动中我们必须至少处理3种I/O请求包,每个内核模式的驱动程序必须支持功能码IRP_MJ_CREATE,这样才能响应Win32的CreateFile函数调用,没有这个分派过程的话,Win32应用程序将无法获取设备的句柄;同样,IRP_MJ_CLOSE也是必须被支持的,否则就无法响应Win32的CloseHandle调用;最后,IRP_MJ_DEVICE_CONTROL允许用户模式程序通过Win32的DeviceIoControl调用来和驱动程序通讯,所以也必须被支持。
下面是这些功能码的说明:
◎ IRP_MJ_CREATE--用户模式代码调用CreateFile函数来获取目标设备对象的文件对象句柄时,I/O管理器发送此代码
◎ IRP_MJ_DEVICE_CONTROL--用户模式代码调用DeviceIoControl函数时,I/O管理器发送此代码
◎ IRP_MJ_CLOSE--用户模式代码调用CloseHandle函数来关闭目标设备对象的文件对象句柄时,I/O管理器发送此代码
例子程序中在同一个DispatchCreateClose过程中处理IRP_MJ_CREATE和IRP_MJ_CLOSE代码,我们稍候再详细分析这样做的原因。
在ntddk.inc中,还可以找到很多我们感兴趣的IRP_MJ_XXX类型的代码定义:
IRP_MJ_CREATE equ 0
. . .
IRP_MJ_CLOSE equ 2
IRP_MJ_READ equ 3
IRP_MJ_WRITE equ 4
. . .
IRP_MJ_DEVICE_CONTROL equ 0Eh
. . .
IRP_MJ_CLEANUP equ 12h
所有的IRP_MJ_XXX代码在ntddk.inc中都有定义,它们实际上就是MajorFunction数组的索引值而已,前面的代码只是填充了MajorFunction数组的三个元素而已:
mov [eax].DriverUnload,offset DriverUnload
DriverUnload过程的意图在于清理DriverEntry过程申请的一些资源,如果驱动需要被动态卸载的话,我们就必须提供卸载进程的分派过程,当用户模式代码使用SERVICE_CONTROL_STOP参数调用ControlService函数时,该分派过程就会被调用。
assume eax:nothing
mov status, STATUS_SUCCESS
这两句的意思是:驱动程序成功地完成初始化工作的话,那么就向系统返回STATUS_SUCCESS代码表示操作成功。
5.3.4 清理工作
.else
invoke IoDeleteDevice, pDeviceObject
.endif
.endif
如果调用IoCreateSymbolicLink失败,那么我们必须释放前面申请的一些资源,这里我们要删除前面用IoCreateDevice创建的设备对象,这可以通过调用IoDeleteDevice函数来完成。如果你还申请了别的一些资源的话,在这里也应该全部将它归还给系统。
请不要忘了,你必须随时留意你申请的内存和其他一些系统资源,在不需要再使用的话,要将它们释放掉。因为你现在是在内核模式下运行,这些清理工作必须自己完成,没人会帮你做这些事情。
mov eax, status
ret
最后,我们向系统返回状态代码,如果代码是STATUS_SUCCESS的话,驱动程序将保留在内存中,接下来I/O管理器会将对应的IRP请求发送给它;如果返回的是其他数值的话,系统会将驱动程序从内存中清除。
5.3.5 这里是新的对象
DriverEntry成功返回后,系统中多了三个新的对象,驱动"\Driver\VirtToPhys",设备"\Device\devVirtToPhys"以及到设备的符号连接"\??\slVirtToPhys"。
驱动对象描述了系统中存在的独立的驱动程序,I/O管理器通过驱动对象获取每个驱动中不同的分派过程的入口地址。
设备对象描述了系统中的一个设备,包括设备的各种特征。通过设备对象,I/O管理器得到管理这个设备的驱动对象的指针。
文件对象是设备对象在用户模式上的表现,通过文件对象,I/O管理器得到设备对象的指针。
符号连接对用户模式是可见的,它被对象管理器所使用。
图5.1显示了各对象之间的相互联系,它能帮你更彻底地理解后面的内容。
图5.1 驱动、设备和文件对象之间的关系
5.4 I/O分派过程
I/O管理器调用分派过程来响应用户模式或者内核模式的请求,在单层或者多层中的最高层的驱动中,分派过程保证是在发起I/O请求的线程上下文中执行的,就像DriverEntry过程一样,分派过程也是在IRQL = PASSIVE_LEVEL下执行的,这意味着它们可以存取分页的系统资源。
所有的分派过程的申明如下:
DispatchRoutine proto stdcall pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
参数描述如下:
◎ pDeviceObject--指向设备对象(DEVICE_OBJECT结构),如果同一个驱动程序负责多个设备的话,从这个参数就能分辨出是哪个设备发送过来的IRP
◎ pIrp--指向描述I/O请求的IRP结构
I/O管理器创建一个IRP结构,用来描述I/O请求,并把它的指针通过pIrp参数传递给设备驱动程序,具体怎样处理就是设备驱动程序的事情了。
这种统一格式的接口的好处在于:I/O管理器可以用同样的方法调用任何的分派过程,而不需要知道驱动程序内部的细节知识(注:反过来想一下,如果不同分派过程的调用格式不同,那么I/O管理器必须知道所有的过程的调用格式和参数定义)。
5.5 IRP_MJ_CREATE和IRP_MJ_CLOSE的分派过程
为什么不同类型的IRP可以用同一个分派过程来处理呢?这是因为在我们这个简单的驱动程序中,唯一要在IRP_MJ_CREATE和IRP_MJ_CLOSE中要做的事情就是将IRP标记为已处理。
如果两者的处理方法不同的话,你还是应该创建独立的DispatchCreate的DispatchClose过程。
前面已经说过,处理IRP_MJ_CREATE是为了响应CreateFile的调用,如果不处理这个代码的话,Win32应用程序将无法获取设备句柄;同样处理IRP_MJ_CLOSE代码是为了响应对CloseHandle的调用。
DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
mov eax, pIrp
assume eax:ptr _IRP
mov [eax].IoStatus.Status, STATUS_SUCCESS
and [eax].IoStatus.Information, 0
assume eax:nothing
我们填写I/O状态块来表示IRP的处理结果。
I/O状态块的Information字段被设置为0,表示设备句柄可以被打开。该字段对关闭的请求来说没有什么含义,但对其他的请求可能有不同的含义。
Status字段表示决定了CreateFile或CloseHandle的调用是否成功返回,所以我们要在这里填写STATUS_SUCCESS。
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, STATUS_SUCCESS
ret
DispatchCreateClose endp
现在必须调用IoCompleteRequest函数来表示驱动程序已经完成了IRP的处理,并将IRP返回给I/O管理器;然后返回STATUS_SUCCESS表示设备已经可以接收另一个I/O请求的处理了。
IoCompleteRequest的第一个参数告诉I/O管理器哪个IRP已经被处理完毕,第二个参数返回一个系统定义的表示实时优先级的常数,这是驱动程序为了补偿其他线程(在驱动的执行中)进行等待而给予的瞬间的优先级提高,例如对于音频设备,DDK建议使用IO_SOUND_INCREMENT值(等于8)。
例子程序中使用IO_NO_INCREMENT(等于0),也就是说当前线程的优先级保持不变。
IofCompleteRequest是一个fastcall类型的函数(注意名称中的f前缀),同样函数的stdcall版本是IoCompleteRequest,这是使用fastcall版本仅仅是为了教学的目的。
5.6 调用的约定
Windows NT内核API使用了3种调用约定:__stdcall、__cdecl和__fastcall,不幸的是,MASM编译器不支持最后一种调用方式。
__fastcall调用约定将前面两个dword类型的参数放入ECX和EDX寄存器,剩余的参数从右到左压入堆栈,由被调用的过程负责将参数从堆栈中清除。
Fastcall函数名称的修饰方式如下:函数名前面加上一个@符号,函数名后面加上@符号以及表示传递给函数的参数字节数的10进制数字,例如,IofCompleteRequest函数被修饰为:
@IofCompleteRequest@8
上面的修饰名表示这是一个fastcall类型的函数,函数名是IofCompleteRequest,它有两个dword类型的参数。
这个函数在\include\w2k\ntoskrnl.inc中定义,注意前缀SYSCALL
EXTERNDEF SYSCALL @IofCompleteRequest@8:PROC
IofCompleteRequest TEXTEQU <@IofCompleteRequest@8>
为了方便地调用fastcall类型的函数,我写了下面的宏:
fastcall MACRO api:REQ, p1, p2, px:VARARG
local arg
ifnb
% for arg, @ArgRev( )
push arg
endm
endif
ifnb
ifdifi ,
mov ecx, p1
endif
ifnb
ifdifi ,
mov edx, p2
endif
endif
endif
call api
ENDM
这里列出的是简化版的宏,全功能的版本在\include\w2k\ntddk.inc里面,当然原始的ntddk.h里面是没有这个宏的。
5.7 内存缓冲管理
I/O管理器提供了3种缓冲管理方式:buffered方式、direct方式和neither方式。
程序中只演示了使用DeviceIoControl函数来进行I/O处理,使用ReadFile和WriteFile函数来进行I/O处理的方法有点不同,你可以在\src\NtBuild中找到相关的例子。
5.7.1 Buffered I/O方式
开始I/O操作后,I/O管理器将用户缓冲区所属的虚拟内存页面提交以使其有效,然后从非分页内存池中分配一块足够容纳用户请求的内存块。
创建IRP的时候,I/O管理器将用户缓冲区的数据拷贝到申请的缓冲区中,并将其地址通过IRP结构的AssociatedIrp.SystemBuffer字段传递给驱动程序,数据的长度由IO_STACK_LOCATION结构的Parameters.DeviceIoControl.InputBufferLength字段指定(该结构的地址由IRP结构的Tail.Overlay.CurrentStackLocation字段指定,IoGetCurrentIrpStackLocation宏用来获取该结构地址)。
驱动程序处理IRP,并将输出数据拷贝到同一个缓冲区中。
当调用IofCompleteRequest函数来将IRP标志为已处理完毕的时候,I/O管理器将缓冲区中的数据拷贝到用户缓冲区,并释放缓冲区占用的内存,要拷贝的内存数量由IRP结构的IoStatus.Information字段指定。
正如读者所见,I/O管理器在整个过程中拷贝了2次数据,所以buffered I/O模式常用于一些慢速的、不传输大量数据的设备,就像我们的VirtToPhys一样。
但是这种模式也有很大的优点:I/O管理器负责解决了内存传输中可能出现的种种问题,我们根本不用去关心它。
5.7.2 Direct I/O方式
这种模式供direct memory access (DMA)使用。
我并没有详细研究过这种模式,所以在这篇教程中没有使用它。
当I/O管理器创建IRP时,它锁定用户缓冲区(将它标志为不可分页)并让驱动的代码可以通过80000000h以上的地址来存取它,I/O管理器用MemoryDescriptorLlist (MDL)结构来描述这块内存并将结构的指针放在IRP结构的MdlAddress字段中传递给驱动程序,当IRP使用完毕后,I/O管理器将缓冲区解锁。
5.7.3 Neither I/O方式
这种方式下,I/O管理器不进行任何方式的缓冲管理,一切由设备驱动程序自行处理。
驱动程序可以从stack location的Type3InputBuffer参数中得到输入缓冲区的用户模式虚拟地址,也可以从IRP的UserBuffer字段得到输出缓冲区的用户模式地址,但是如果你无法确定是否运行在用户模式调用者的进程上下文中的时候,这两个地址都是无法使用的。当然,作为作者,在本例中我们很清楚自己的驱动程序不是分层的,所以这些地址肯定可以使用。
我们知道,不分层的设备驱动程序总是在IRQL = PASSIVE_LEVEL下被用户模式调用,所以我们不需要关心用户缓冲区是否在内存中存在,即使它已经被交换出物理内存,内存管理器也会打理好一切的。
唯一的问题是:用户模式代码可能传过来一个错误的地址,或者在某处已经将缓冲区释放了--这在多线程的情况下完全可能发生。
我们必须预见到这种情况并且能正确地处理它,所以使用结构化异常处理(SEH)是很必要的(有关结构化异常处理,见《Windows环境下32位汇编语言程序设计》的第14章:异常处理,或者参考其他的相关资料),但要注意的是,内核模式的SEH和用户模式下使用的方法是一样的,所以你无法用它截获所有的异常,例如,就是安置了SEH后,除零错误还是会引发一个蓝屏死机画面。(使用了SEH的代码例子见\src\Article4-5\NtBuild)
5.8 IRP_MJ_DEVICE_CONTROL的分派过程
当驱动程序指定了IRP_MJ_DEVICE_CONTROL的分派过程后,I/O管理器收到用户模式代码对DeviceIoControl的调用后,就会把IRP传递给该分派过程。
and dwBytesReturned, 0
这句代码的意思是将I/O管理器将要拷贝的数据数量暂时设置为0。
mov esi, pIrp
assume esi:ptr _IRP
IoGetCurrentIrpStackLocation esi
mov edi, eax
assume edi:ptr IO_STACK_LOCATION
IoGetCurrentIrpStackLocation宏取出IRP的stack location的指针,也就是指向一个IO_STACK_LOCATION结构的指针,该结构中包含了一些常用的数据:
.if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
判断一下I/O控制代码,我们不应该处理不认识的代码。
NUM_DATA_ENTRY equ 4
DATA_SIZE equ (sizeof DWORD) * NUM_DATA_ENTRY
IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)
我们要处理的IOCTL_GET_PHYS_ADDRESS控制代码是在common.inc文件中作为常数定义的,这个include文件在驱动程序和主程序中都用到了。
.if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )
检查一下输入和输出缓冲区的长度,如果长度不够则停止处理。
IO_STACK_LOCATION结构的OutputBufferLength和InputBufferLength字段和DeviceIoControl函数指定的nOutBufferSize、nInBufferSize参数的取值是相符的。
mov edi, [esi].AssociatedIrp.SystemBuffer
我们可以从IRP的stack location取得指向系统缓冲区的指针,这个缓冲区中包含了用户模式代码传递给驱动的数据,本例中的数据是4个虚拟地址,驱动程序要将它们转换成物理地址。
assume edi:ptr DWORD
告诉编译器edi寄存器指向的是dword类型的值,否则的话我们每次使用edi的时候就一定要加上PTR DWORD了。
xor ebx, ebx
.while ebx < NUM_DATA_ENTRY
invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
mov [edi][ebx*(sizeof DWORD)], eax
inc ebx
.endw
循环NUM_DATA_ENTRY次,每次从缓冲区中取出一个dword(也就是虚拟地址数据),然后调用GetPhysicalAddress以得到转换后的物理地址,并将它写回缓冲区的同样位置去。
mov dwBytesReturned, DATA_SIZE
mov status, STATUS_SUCCESS
所有工作完成后,将处理的总字节数放入dwBytesReturned,并把返回代码设置为成功。
.else
mov status, STATUS_BUFFER_TOO_SMALL
.endif
.else
mov status, STATUS_INVALID_DEVICE_REQUEST
.endif
遇到其他错误的话,返回对应的错误代码。
assume edi:nothing
push status
pop [esi].IoStatus.Status
完成IRP后,我们将当前status变量的值放入状态块的Status字段,这些状态值可以转换成Win32错误代码,对应如下:
Nt Status Win32 Error
============================= ================
STATUS_SUCCESS NO_ERROR
STATUS_BUFFER_TOO_SMALL ERROR_INSUFFICIENT_BUFFER
STATUS_INVALID_DEVICE_REQUEST ERROR_INVALID_FUNCTION
Ntdll.dll中的RtlNtStatusToDosError函数可以将内核状态代码转换到Win32错误代码,用户模式应用程序调用GetLastError就可以得到这个代码。
push dwBytesReturned
pop [esi].IoStatus.Information
状态块的Information字段需要放置I/O管理器要拷贝到用户缓冲区的数据字节数,最后DeviceIoControl函数的调用者会在lpBytesReturned指向的变量中得到这个字节数数值。
assume esi:nothing
fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
mov eax, status
ret
调用IofCompleteRequest来结束对IRP的处理。
最后不用忘记,即使是收到了不认识的I/O控制码,我们也应该将I/O状态块设置合适的NTSTATUS值,并将Information字段设置为0,并用IO_NO_INCREMENT参数调用IofCompleteRequest来结束对IRP的处理。
5.9 内存地址转换
内核模式的代码可以将虚拟地址转换成物理地址,MmGetPhysicalAddress函数可以完成这个功能,例子中的GetPhysicalAddress子程序可以基本上完成相同的功能(当然少了一些扩展的功能),不幸的是我没有机会描述其中的工作细节,读者请自行参考"Inside Microsoft Windows 2000"一书(David Solomon和Mark Russinovich著),这一段的代码如下:
GetPhysicalAddress proc dwAddress:DWORD
; Converts virtual address in dwAddress to corresponding physical address
mov eax, dwAddress
mov ecx, eax
shr eax, 22 ; (Address >> 22) => Page Directory Index, PDI
shl eax, 2 ; * sizeof PDE = PDE offset
mov eax, [0C0300000h][eax] ; [Page Directory Base + PDE offset]
.if ( eax & (mask pde4kValid) ) ; .if ( eax & 01y )
; PDE is valid
.if !( eax & (mask pde4kLargePage) ) ; .if ( eax & 010000000y )
; small page (4kB)
mov eax, ecx
; (Address >> 12) * sizeof PTE => PTE offset
shr eax, 10
and eax, 1111111111111111111100y
add eax, 0C0000000h ; add Page Table Array Base
mov eax, [eax] ; fetch PTE
.if eax & (mask pteValid) ; .if ( eax & 01y )
; PTE is valid
; mask PFN (and eax, 11111111111111111111000000000000y)
and eax, mask ptePageFrameNumber
; We actually don't need these two lines
; because of module base is always page aligned
and ecx, 00000000000000000000111111111111y ; Byte Index
add eax, ecx ; add byte offset to physical address
.else
xor eax, eax ; error
.endif
.else
; large page (4mB)
; mask PFN (and eax, 11111111110000000000000000000000y)
and eax, mask pde4mPageFrameNumber
and ecx, 00000000001111111111111111111111y ; Byte Index
add eax, ecx ; add byte offset to physical address
.endif
.else
xor eax, eax ; error
.endif
ret
GetPhysicalAddress endp
GetPhysicalAddress子程序将输入参数中的虚拟地址转换成物理地址后返回。
5.10 DriverUnload过程
DriverUnload过程非常直接了当,就是用于删除驱动程序创建的符号连接和设备对象。当用户模式代码以SERVICE_CONTROL_STOP参数调用ControlService函数时,该过程即被调用。
invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
mov eax, pDriverObject
invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
DriverUnload过程做的工作和DriverEntry过程刚刚相反,它调用IoDeleteSymbolicLink函数来清除对象管理器命名空间中的符号连接,并调用IoDeleteDevice函数来删除设备对象本身。
前面也提到过,在内核模式下,你必须自己来释放所有申请的资源。
表5.1列出了驱动程序的各主要过程运行的进程上下文和IRQL,这些是你应该了解的,当然该表的内容仅对不分层或者分层驱动中的最高层有效。
表5.1:
User-mode Kernel-mode Process context IRQL
================== ========================== =========================== ============
StartService DriverEntry System PASSIVE_LEVEL
CreateFile IRP_MJ_CREATE User-mode caller PASSIVE_LEVEL
DeviceIoControl IRP_MJ_DEVICE_CONTROL User-mode caller PASSIVE_LEVEL
ReadFile IRP_MJ_READ User-mode caller PASSIVE_LEVEL
WriteFile IRP_MJ_WRITE User-mode caller PASSIVE_LEVEL
CloseHandle IRP_MJ_CLEANUP,IRP_MJ_CLOSE User-mode caller PASSIVE_LEVEL
ControlService(STP) DriverUnload System PASSIVE_LEVEL
5.11 编译驱动程序的方法
:make
set drv=skeleton
\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj
del %drv%.obj
move %drv%.sys ..
echo.
pause
这些内容已经在第3节中解释过了,唯一新增的是多了个/ignore:4078选项,这是因为程序中有2个不同属性的INIT节区,所以链接器会报下面的警告(增加这个选项可以抑制该警告信息):
LINK : warning LNK4078: multiple "INIT" sections found with different attributes (E2000020)
5.12 添加资源
本例中我们还在驱动的资源中加上了版本信息,这可以用通常的资源脚本来完成(见rsrc.rc文件):
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "Comments", "Written by Four-F\0"
VALUE "CompanyName", "Four-F Software\0"
VALUE "FileDescription", "Kernel-Mode Driver VirtToPhys v1.00\0"
VALUE "FileVersion", "1, 0, 0, 0\0"
VALUE "InternalName", "VirtualToPhysical\0"
VALUE "LegalCopyright", "Copyright ? 2003, Four-F\0"
VALUE "OriginalFilename", "VirtToPhys.sys\0"
VALUE "ProductName", "Kernel-Mode Driver Virtual To Physical Address Converter\0"
VALUE "ProductVersion", "1, 0, 0, 0\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
这里没有什么特殊的东西,用常用的方法编译和链接就可以了。
5.13 关于调试
使用SoftIce中和驱动和设备有关的命令可以获取很多和驱动及其设备相关的有用信息,读者可以自行查看SoftIce的命令手册,在我的机器上,它的输出如下:
图5.2 driver VirtToPhys命令的输出
图5.3 device devVirtToPhys命令的输出
对你来说,SoftICE显示的信息应该是很好理解的,这些信息是从DRIVER_OBJECT以及 DEVICE_OBJECT结构中获取的,使用这些信息可以很容易在内存中找到这些对象并对它们的分派过程设置断点。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
他的文章
- [求助]win7 64 无法启动到窗口界面 2700
- [求助]谁能解决这个高难度问题! 4748
- [求助]谁能解决这个问题,高难度! 7378
- [翻译]OllyDbg插件开发手册全部翻译件 194010
- [翻译][原创]OllyDbg命令行插件帮助 12859
看原图
赞赏
雪币:
留言: