以文本方式查看主题

-  计算机科学论坛  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  Kmd教程----用C写驱动的辅助教程  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=51989)


--  作者:卷积内核
--  发布时间:8/30/2007 2:24:00 PM

--  Kmd教程----用C写驱动的辅助教程
0.前言

0.1 关于KmdTut

    KmdTut是一份以Win32汇编语言来开发Kernel mode驱动程序的教程,读者可以从http://www.freewebs.com/four-f/下载最新的英文版本,为了使用本教程,你还需要下载Kernel mode驱动程序开发包KmdKit,更多信息请参考http://www.wasm.ru/(很不幸,是俄文的,倒ing~~~)

0.2 关于KmdKit

    KmdKit的全称是Kernel Mode Driver development Kit for assembly language programmers,即内核模式驱动程序汇编开发包,KmdKit中包括了用宏汇编编译器开发驱动程序需要用到的所有东西。
    你可以从http://www.wasm.ru/或者http://www.freewebs.com/four-f/下载到最新版本的KmdKit。

0.3 关于KmdTut中文版

    本中文版由罗云彬和刘松翻译,首先发表在http://asm.yeah.net(罗云彬的编程乐园)中,如果需要转载或者引用,请注明出处。
    虽然本人(这里的本人当然是指罗云彬了~~)在一些使用习惯上并不十分赞同原作者的使用方式,如将汇编代码和Makefile的内容合并放在BAT文件中、还有过多的使用宏来定义字符串等方式,因为个人认为严格地按照常规的方式来写代码更有利于读者理解,但是在翻译的时候,还是首先尊重原作,读者对于文中的一些和个性化的表达方式可以自行取舍。

    ◎ 将汇编代码和编译命令一同放在BAT文件中的缺点:
    一般在汇编项目往往将编译命令放在Makefile中,并且使用nmake工具来进行编译,在本人著的《Windows环境下32位汇编语言程序设计》一书中,所有的例子都是这样写的。这样写的好处实际上就是nmake工具带来的好处——那就是源文件的从属关系清晰,编译链接的效率很高,符合流行的习惯等。

    ◎ 在代码中使用很多宏的缺点:
    本人并不推荐在源代码中使用复杂的宏,原因有两点:首先是当宏中使用了寄存器的时候,在源代码中并不能明显地表现出来,这样当寄存器中保存有其他数据的时候,数据就会被破坏,有时候这方面引起的错误是很难发现的;第二是宏的使用方便了编写者,却不利于代码交流,因为阅读者必须首先搞清楚宏的作用,程序中宏过多的话,就会是源代码的结构受到干扰。

    所以对文中的这些非常个性化的习惯,读者请根据自己的爱好自由取舍。

    另外,毕竟翻译网上教程并不如出书一样经过层层审校,所以翻译有误或者不贴切的地方是难免的,特别是错别字,本人是打拼音的,空格按得快了,往往全拼的词组就可能选错了,所以如果有明显不对的地方,请读者发挥您的丰富想像力谐音一把,如"寄出"说不定是"基础","乘虚"说不定就是"程序"等等,如果发现错误,请邮件告知asm@zj165.com
    为了让大家在枯燥的学习中有点乐趣,本人在文中加了些括号括出来的注解,有些是为了说明原文中比较含糊的东西,有些是……大家自己慢慢看就明白了。

    再者:本篇教程英文版只有这么多,已经全部翻译了,但是俄文版还有后续的章节,可惜大家都不懂俄文了,有没有懂俄文的自告奋勇翻译一番呢~~~~俄文原文见http://www.wasm.ru/


--  作者:卷积内核
--  发布时间:8/30/2007 2:25:00 PM

--  
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,这本书和开发设备驱动程序没什么关系,但是也是一本很有趣的书。

    这里列出的并不是全部,但这些似乎都有点儿看看的必要性。


--  作者:卷积内核
--  发布时间:8/30/2007 2:28:00 PM

--  
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数据库进行操作的时候会用到这个句柄。
    另外,忘了提醒大家,安装内核模式驱动程序需要超级用户的权限,为了安全起见,普通权限的用户没有被授权的话是无法执行特权代码的。当然,本文的例子总是假设你是有超级用户权限的。


--  作者:卷积内核
--  发布时间:8/30/2007 2:28:00 PM

--  
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的编程,那么我就不在这些宏上面做过多的解释了,但是后面的程序中有很多地方会用到它们。


--  作者:卷积内核
--  发布时间:8/30/2007 2:29:00 PM

--  
3-最简单的设备驱动程序

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工具来注册以及装载任何驱动程序。


--  作者:卷积内核
--  发布时间:8/30/2007 2:29:00 PM

--  
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值。


--  作者:卷积内核
--  发布时间:8/30/2007 2:30:00 PM

--  
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子程序。


--  作者:卷积内核
--  发布时间:8/30/2007 2:30:00 PM

--  
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软件来监视调试信息的输出。


W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
282.227ms