【转帖】文件系统驱动编程基础篇

文件系统驱动编程基础篇之一——我们的准备

一、导言

在四个月漫长的征战后,终于在国庆节的今天完成了基础篇系列。本文写作的初衷很简单,就是给平静的池水中加入一点波澜,如果大家在阅读后感受到一点生气,激起探索未知的热情,笔者也会感到由衷的喜悦。

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

参考资料*

1.《Programming the Microsoft Windows driver model》第一版(当前阶段主要阅读资料,尝试阅读前两章,体会驱动编程的难度),配套代码位于http://www.oneysoft.com/

2.《Windows NT File System Internals - A Developers Guide》第一章

3.《Kernel Debugging with WinDbg》(随WinDbg软件附带的doc文档,阅读基础部分)

4.《Using Serial Ports》(VMware官方文档)

5.《安装DriverStudio3.2 过程中出现DSDDKEnv8.dll failed to register错误的解决方法

6.《驱动程序和应用程序编译出现的问题及解决方法

7.《Essentials Of Building Windows Drivers

 

阅读基础:不限。

 

本章目的:了解操作系统的基本架构、构建调试器环境、驱动程序编译的一般步骤。

 

二、基本架构

      为了保证性能,汇编与c成为操作系统编写的首选语言,Windows家族的前辈都不例外,唯独Vista那庞大的身躯,让人不禁疑惑微软究竟如何才能诞下比恐龙还大的怪物。与此相反,WinPE作为维护型操作系统可以被一个32MU盘所容纳。不管外观上的诸多差异,功能上的强弱区别,我们所关心的是它们的共性——即操作系统的内核。与操作系统密不可分的文件系统,就成为我们研究内核的一条途径。

       Windows采用了基于对象模型(object-based model的设计方式,各功能划分为不同的组件,两幅常见的架构图如下:

 

      

我们把入口选定为Win32子系统(Win32 Subsystem),它是我们接触最多,也最熟悉的一个子系统。硬件抽象层(HAL)及其以下部分目前不在我们关注的焦点内,我们将精力集中在系统执行层和核心层,它们具体的功能请参看资料2的第一章。

内核模式下除了屈指可数的几个函数,一切都将是全新的——全新的思想、概念、模型、结构、函数,跨越的幅度不亚于从c语言编程迁移到c++语言,用c语言的思维来学习c++必然会产生阻碍。令人欣慰的是,这里没有特色之流的术语,绝大部分内容都符合人的记忆规律。你将很快掌握设备对象、IRPIO堆栈等基础结构,同时你还发现以前很难记忆的PEBTEB_ETHREAD等一系列无详细文档甚至无文档结构都已有迹可寻,如果你足够勤奋,甚至会在一年左右的时间后就可以阅读天书般的防火墙源码。

让我们先来完成必要的准备工作吧。

 

三、调试环境的构建

       在用户模式下编程,除了编译器几乎可以不需要其他辅助工具。想查看输出?直接ShowMessage即可,内核模式下,如果不希望摔得筋折骨断后才有所醒悟,你该在入门前就选好几件宝物——注意它既不是闻西同志的西瓜刀,更不是单车链。

 

(一)   Microsoft Visual Studio 200x Windows IFS Kit and DDK xxx Compuware DriverStudio 3.x VAssistX xxx VMware

如果记忆力超强的读者,也可以选择C++BuilderDelphi 插件的形式,有些站点正在致力于推广这方面的技术,对于新手而言,还是用原装货为上策。Windows IFS Kit and DDK是收费软件,电驴上有试用版,依照资料6的说明完成安装,注意选上xp2000部分;Compuware公司已经改行不做DriverStudio,幸好出家前支持了vs2005,安装完成后,根据资料5打上VisualStudio 2005 Integration fix补丁;VAssistX有试用限制,不巧有人发现不修改代码而使用trial-reset_32Armadillo壳,可以恢复试用时间,安全而可靠。VassistX增强了编译器的语法提示、代码搜索功能,大大提高了千行规模子程序的阅读与书写效率。VMware虚拟机用于减少重启的烦恼,提高调试的效率。

经过一阵忙碌后,一个已经设置好的编译器出现了:

 

 

(二)   VMware虚拟机下的Debugging Tools for Windows(即WinDbg

读者也可使用VirtualPC虚拟机。在虚拟机上安装双系统(Windows 20002003xppe等),可方便删除造成系统启动崩溃的“不良”驱动程序。为了方便,我们把虚拟机上的操作系统称为远程机。首先我们在本地机上安装WinDbg,运行后进行如下设置:

一)设置符号文件搜索目录,菜单FileSymbol File Path…-填入SRV*c:/symbols*http://msdl.microsoft.com/download/symbols串,它表示使用微软的文件符号服务器。如果你的电脑未联网,则只能通过其他方式获取微软站点上的符号文件,采用本地符号目录,调试时往往会出现一些版本不匹配的问题。符号文件非常重要,如果WinDbg找不到合适的符号文件,将无法解析代码里的数据结构。文件符号服务器上的符号文件用于解析操作系统文件(dll, exe等),而你的源代码将编译产生驱动程序的符号文件。

二)菜单FileOpen Executable…-随意选择一个可执行文件进行本地调试,WinDbg将搜索是否已经存在必须的符号文件,如果无,则通过互联网连接文件符号服务器下载必要的符号文件,保存的目录是c:/symbols。之所以进行这一步,是帮助初学者在连接远程机出现长时间的延时时,确定不是因为下载的原因造成的。

三)运行虚拟机,打开远程机系统根目录下的boot.ini文件(可能是隐藏文件),为操作系统复制一新行,并在后添加/debug /debugport=com1 /baudrate=115200,指明连接的方式(串口连接)和速率(115200比特率)。如图是安装了两操作系统的boot.ini文件:

 

 

重新运行远程机,启动界面如:

 

 

四)为虚拟机添加虚拟串口:关闭虚拟机,点击配置选项Eidt virtual machine settings,选择命名管道方式配置虚拟串口com1

 

 

五)测试虚拟串口:运行远程机,选择debug模式进入,此时将比正常启动多出近30秒的黑屏时间,如果你的硬盘马力强劲,你会听到运转的沙沙声^_^,接下来的情况将和正常启动时相同。在本地机上运行WinDbg,点击菜单FileKenerl Debug…,首次连接时按左下图进行配置,确定后WinDbg将开始尝试远程连接。为了加快连接速度,不妨多按几次重新同步的快捷键CtrlAltR,如果上述设置无误,则虚拟串口标志将不停闪动(中下图最右),连接成功后,WinDbg出现类似信息(右图):

   

 

六)调试器连接了远程机后,就获得了远程机的控制权,远程机则处于停机状态,此时可以查看内核的情况,如果希望它继续运行,只需要在调试器的命令行窗口输入g命令,则调试器归还控制权,远程机将继续运行。

 

(三)   http://www.osronline.com/index.cfm下载您中意的工具。

(四)   特别值得一提的几个小工具是:DbgviewDriver InstallerIrpTrackerDriverMonitorEzDriverInstaller,后两个工具为Driver Studio附带。

(五)   练习WinDbg的使用,尝试用(二)- 二)的方式单步跟踪一个简单的可执行程序,学习如何设置断点。

 

最后一步是个难点,请根据资料3努力的完成这个工作。

 

四、驱动程序的编译

(一)   sourcesmakefilesdirs文件

使用DriverStudio的一个目的是,借用vs强大的IDE编写代码,而调用DDK的编译器进行编译。早期的vs编译器也支持驱动代码的编译,但随着DDK的发展,现在它已经完全脱离了vs,两者不再保证编译代码的一致性。为了保证正确性,我们需要使用DDK的编译器来完成编译的工作。

基于效率的原因,我们还需要学习使用sources文件,它支持众多的编译指令,可以实现复杂的编译配置。驱动编程中,makfiles文件是一个无关大局的配角,一般无须改动,尽管它是必须的。如果源文件非常复杂,分布于多个目录,包含了多个工程,则可编写dirs文件。编译器通过dirs文件,找到各目录下的sources文件,逐一完成编译。下面我们将介绍sources文件的基本写法,更多指令的详细用法可查阅资料6Msdn

sources是一个无后缀名的文本文件,示例如下:

TARGETNAME=pnpevent

TARGETPATH=obj

TARGETTYPE=DRIVER

USE_PDB=1

INCLUDES=../../../generic

#TARGETLIBS=../../../generic/obj$(BUILD_ALT_DIR)/$(CPU)/generic.lib /

TARGETLIBS=../../../generic/obj$(BUILD_ALT_DIR)/i386/generic.lib /

 

SOURCES=   DriverEntry.cpp /

              stddcls.cpp /

              control.cpp /

              readwrite.cpp /

              driver.rc

 

#号表示注释,而*号表示目标平台类型,编译时会被Build工具替换成相应的串,如替换成“I386

TARGETNAME指明驱动程序名,TARGETPATH指明生成的路径(如果没有其他指令,编译器生成的是以obj打头的路径,如objchk_wxp_x86/i386)。TARGETTYPE指明代码类型,可创建普通驱动sys、内核模式的dll、库文件lib、用户模式程序exe、用户模式的dllTARGETLIBS指明需要引用的外部lib库文件。

INCLUDES指明需要引用的头文件目录,SOURCES指明本次需要编译的文件,文件名后的/表示下行仍为源文件。

还可以使用C_DEFINESUSER_C_FLAGS来定义宏。对于若干年前的驱动代码,编译器可能会提示无法识别某些废弃的编译指令,视情况手工删除或更改成等价指令即可。

 

(二)   编译DriverStudioVdwLibs2005.sln工程

该工程位于Compuware/DriverStudio/DriverWorks/source目录下,vs2005编译器加强了语法的规范性,不修改源文件是无法完成编译的。为此,请参考资料6,按照图三、(一)的方法直接调用DDK进行编译。

阅读资料1第三章的错误处理小节,编译附带的源代码Chap3/SEHTEST,学会使用DriverMonitor来加载驱动、Dbgview来查看该代码的输出信息。

 

五、代码书写规范与编译警告的处理

书写规范可参考本站的《华为编程规范和范例》,强调一点,驱动编程中使用了众多的宏,为避免宏扩展带来的潜在问题,使用ifwhilefor等时,其下的子句都须以{}扩之。缩进不规范的代码可通过Alt+F8修正。

if (x)

  return y; // 不推荐

 

if (x)

{  // 标准格式

  return y;

}  // 标准格式

 

调高编译器的告警级别,使任何警告都被当作错误而停止编译,这种严谨的态度有助于减少潜在的错误。建议在sources文件里至少设定W3级别:

MSC_WARNING_LEVEL=-W3

 

六、学习的误区

缺乏必要的计算机科学技术的理论基础,是非专业的编程爱好者遇到的一个最大的问题。在这个问题的处理上,如果采取回避的态度,相信很快就将潜力用尽,再也没有上升的空间。有的人编了几年程序,却还在害怕阅读其他语言书写的代码,是这个问题的一个表现。如果看到了这个问题,并希望解决它,也需要采取正确的方法。贪多求全,希望先把基础理论一口吞下再来实践编程的思想也是错误的,因为理论仅仅是实践的一个抽象,两者必然存在差别,大脑也需要在实践中建立起可靠的影像,而不是“大概,也许,基本上”这样的一种客套话,这也是能力的体现。

我们的建议是,根据自身的实际情况,在每一个发展阶段采取先学习理论再实践,或先实践再学习相应理论的方式,螺旋般的向上,速度虽然稍慢,却能为自己积累下发展的潜力。一个实际的例子是,计算机系的学生在入校后的前两年,计算机的实践水平往往比不上别的院系的学生,一旦他们发现实践上的很多问题可以通过已掌握的基础理论来解释之后,两者的前进速度就有了质的差别。高手的一个特点是哪怕只给他透露了一个关键的单词,你所保留的秘密很可能就被完全破解了,而对于菜鸟而言,即使把金山搬到门口,也还是身在福中不知福。

在基础篇中,我们推荐了大量的示例代码,这些代码可以帮助读者更好的理解基础的理论。

完成了基础篇,您仍然处于文件系统驱动编程的边缘,对它的理解甚至比不上采用了速成大法学习的其他人,您的收获则是奇怪为什么自己先前写了如此多的垃圾代码?您将大大扩宽自己的眼界,看到控件之外的许多更有意义的知识。

唯一没办法帮助大家的事情,就是外文阅读水平了。我们只能鼓励还仅仅掌握着1.5国语言的朋友,人手一册金山词霸,首页google在线翻译,每日半篇RFC,不出两月,拿下Msdn。当然如果你能在Msn上结交到海外的留学生,他们对您的帮助将会更大。

编程能力以2万行作为分界点,还达不到这个要求的朋友,请努力吧。

老师有句名言,“天才在于勤奋,聪明在于积累”,技巧性的东西固然可以帮助您提高效率,但始终无法代替基础,如果您想在科技领域真正有所成就,就请遵循这句话。

 

七、结语

基础篇将仅仅讲述一些与WDMWindows Driver ModelKMDF相当于WDM的封装类,Vista已自带)驱动编程有所关联的基础知识,限于能力,并不追求系统性和完整性。资料1作为大师级的著作,是笔者推荐的主要学习资料,本文中大部分驱动示例来源于它的配套代码。

留给初学者的一个问题是,究竟VMware的虚拟串口支持多高的通信速率呢?

本文作为下场之前的热身,参考完成时间为一至两个星期。

文件系统驱动编程基础篇之二——标准模型、基本例程、结构与函数

一、前略

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

笔者的实践环境为:

硬件:P35 Motherboard & ICH9 chipPentium Dual Cpu E2160 1.8g DDR2 1g

软件:Windows XP2VS 2005Visual AssistXDriverStudio 3.2MICROSOFT.WINDOWS.SERVER.V2003.IFS.DDKWindbg 6.8.0004.0,请安装好用于调试的xp虚拟机并配置好调试环境。

 

参考资料*

1.《Programming the Microsoft Windows driver model》第一版(当前阶段主要阅读资料,阅读第二至第五章)

2.《Kernel Debugging with WinDbg

 

阅读基础:掌握c语言,会使用MsdnWinDbg文档。

 

本章目的:了解驱动程序的标准模型,认识基本例程和初步了解常用结构与函数。

 

二、标准模型

WDM采用了结构化的编程方式,执行效率很高,但编写效率较低,这也是DriverStudio得以发展的重要原因。正如掌握了COM原理,使用ATL才能掌握精髓的道理一样,读者需要忍受记忆大量基础知识的“痛苦”,暂时放弃编写驱动程序的捷径。

下面的某些图示稍显陈旧,不过已经足以说明问题了。

 

 

尽管驱动程序分为多个种类(图1-4),但它们包含的基本内容(图1-5)是一致的。每个驱动程序都从初始化程序DriverEntry进入,通过某个派遣例程DispatchXXX派发特定命令(我们不妨称之为IRP),这些IRP有可能在派遣例程里就得到了解决,也有可能交给驱动程序的其他部分解决。如果驱动程序A本身不能处理这个IRP命令,它就需要将IRP传递到更下层的驱动B,由它们来处理,此时驱动程序A可能因为等待IRP完成而处于睡眠状态,或继续处理新的IRP,直到下层驱动B通知(或通过某种机制唤醒并通知)AIRP已经处理完毕了,此时A就将处理的结果(我们称之为NTSTATUS)返回原来派发这个IRP的发起人。这就是驱动程序处理IRP的一个简化过程。

我们提到了驱动程序是分层的这个概念,那么如何理解分层的概念呢?请看图示:

引用资料1的原话:WDM模型使用了如图2-1的层次结构。图中左边是一个设备对象堆栈。设备对象是系统为帮助软件管理硬件而创建的数据结构。一个物理硬件可以有多个这样的数据结构。处于堆栈最底层的设备对象称为物理设备对象(physical device object),或简称为PDO。在设备对象堆栈的中间某处有一个对象称为功能设备对象(functional device object),或简称FDO。在FDO的上面和下面还会有一些过滤器设备对象(filter device object)。位于FDO上面的过滤器设备对象称为上层过滤器,位于FDO下面(但仍在PDO之上)的过滤器设备对象称为下层过滤器。

由某个家伙(可能是用户模式下的应用程序,也可能是系统内核组件)发起的IRP从上层过滤器驱动程序一直顺流而下,传递到总线驱动程序处理后,再逐级返回上层,最终发起人得到处理的结果。

一般情况下,IRP也许不需要传递到总线驱动程序就被处理掉了,但如果大家都不认识这个IRP,他们就只好逐级下传了,如果此时有个搞破坏的驱动程序混了进来,拦截了这个IRP,轻则丢失用户信息、重启、死机,重则造成系统区的数据混乱,你除了重新安装操作系统再无任何事情可做。由此可见,我们不要求驱动程序“有理想”,但必须“有纪律”,每个驱动程序都必须严格按照规范书写代码,这要求编程人员具备较高的素质。

下面我们来了解驱动编程里最基本的标准模型,这个模型不能解决所有的编程需求,根据需要,它将存在各种变化。我们来看看这个驱动编程里的“基本定式”:

 

IO管理器,大家应该理解为该IRP的发起人,可能是张三,也可能是李四,而不是某个固定的组件。这个模型表明了单个驱动程序里各部件的合作与分工,注意它是个循环不断的过程,它的发起人与最终接受人是相同的,所谓“从哪里来,就回哪里去”。如果我们的编程不涉及真正的硬件,StartIo例程、中断服务例程ISRDPC例程均可能不存在。各部件的具体功能请参看资料1的第五章。

 

三、基本例程、常用数据结构与函数

为正确理解各类例程的具体功能,需要弄清涉及的众多内核函数、数据结构,读者应以本文和资料1为索引,认真的阅读Msdn上的相关内容。

我们的代码将从入口函数DriverEntry处开始执行,一般情况下,不要将它改名,否则需要修改DDK里的Build脚本。

驱动函数定义一般采用__stdcall约定,这个约定,在vsbcb里的实际行为是不同的。如DriverEntry,两种编译器编译后的库中名字(即外部名字)分别是_DriverEntry@8DriverEntry。我们还习惯以INOUT宏显式说明函数的参数是输入或输出参数。

DriverEntry里常见的几个例程由红字标出,包括添加(硬件、虚拟)设备函数AddDevice、驱动卸载函数DriverUnloadStartIo函数以及放置于MajorFunction数组里的派遣函数。DriverEntry还申请了分页池以保存注册表中的服务键,但作为文件系统驱动的DriverEntry,一般还会声明快速IO派遣函数,这组派遣函数没有出现在示例中。

 

extern "C"

NTSTATUS

DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)

{

     DriverObject->DriverUnload = DriverUnload;                                 <--1

         DriverObject->DriverExtension->AddDevice = AddDevice;

     DriverObject->DriverStartIo = StartIo;

     DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;                     <--2

         DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;

     DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = DispatchWmi;

     ...                                                                        <--3

     servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool, RegistryPath->Length + sizeof(WCHAR));    <--4

     if (!servkey.Buffer)

     {

         return STATUS_INSUFFICIENT_RESOURCES;

     }

     servkey.MaximumLength = RegistryPath->Length + sizeof(WCHAR);

     RtlCopyUnicodeString(&servkey, RegistryPath);

     return STATUS_SUCCESS;                                                      <--5

}

 

示例同时引用了多种数据结构,它们的详细注释可参看资料1第二章的第一小节的后半部分。我们首先掌握这些数据结构的可见域,即可由编程人员存取的域,在理解一些重要的函数时也会涉及部分非透明域,一般情况下可使用微软推荐的函数来间接访问它们。

不要混淆驱动对象和设备对象。驱动对象代表了内核加载的驱动镜像,DriverEntryAddDevice例程调用IoCreateDevice函数来创建设备对象时,驱动对象将作为该函数的一个输入参数。设备对象作为硬件或虚拟硬件的抽象,是一个极其重要的数据结构,用于处理设备的I/O请求。

我们将在WinDbg里实际查看这些数据结构,下面列出它们的定义图:

 

       内核函数根据执行的功能,大致分为如下几类,读者可通过例子接触到这些函数:

函数前缀

类别

Ex       

执行支持

Hal

硬件抽象层(仅NT/Windows 2000/XP

Io

I/O管理器(包括即插即用函数)

Ke

内核

Ks

内核流IRP管理函数

Mm

内存管理器

Ob

对象管理器

Po

电源管理(Vista下存在新的限制)

Ps

进程结构

Rtl

运行期库

Se

安全

Zw

其他函数

Cc

Cache函数

FsRtl

文件系统运行期库

 

四、WinDbg上的实践

我们已在上一篇介绍了如何用WinDbg查看KdPrint等内核函数输出的调试信息。事实上象vs或迅雷之类的软件也会产生调试信息,但它们由用户模式下的调试输出函数发出。

本次实践的对象是下篇将要用到的的示例代码,我们将演示如何在WinDbg里的常用操作,如设置断点,查看变量的值、数据结构等,代码位于WINDDK/3790/src/general/ioctl。如果你还不会编译驱动程序,请赶快完成上一篇拉下的作业。

WinDbg连接远程机,按g返还远程机的控制权,将编译好的驱动程序sioctl.sys和测试程序ioctlapp.exe复制到远程机上的任意目录里,如我们新建了一个目录c:/ioctl

Ctrl+break返回WinDbg后,用.cls命令清屏,延时加载bu sioctl!DriverEntry,此时输入bl查看已经设置的断点列表,WinDbg显示:

kd> bl

 0 eu             0001 (0001) (sioctl!DriverEntry)

 

0表示断点的id号,e表示断点的状态为允许,u表示断点未被解析,即当前加载的模块里未找到符合断点的符号。

输入g返回控制权,在远程机里打开cmd命令提示符窗口,输入iocatlapp.exe运行程序,程序立即在DriverEntry处断下(粉红括号):

接下来的操作其实和用户模式下的调试无大的区别,你既可以单步跟踪(F10F11),在源代码上设置断点(F9),也可以查看变量的赋值和结构(命令dvdt…)等,请读者随意发挥了。如用dt查看某个结构,如:

 

先用!pcr查看进程或线程内核对象地址,接着查看特定地址的eprocess结构内容:dt  -r1 _eprocess 81bef448,尾随_eprocess的这个当前进程的地址可以用!process取得。特别关注基础篇七提及的iopm

      +0x030 IopmOffset       : 0x20ac

+0x032 Iopl             : 0 ''

dt  -r1 _ethread 8055be40

 

bl显示的内容变更为:

kd> bl

 0 e f8d6a5b0 [d:/0vcprojects/ioctl/sys/sioctl.c @ 123]    0001 (0001) SIoctl!DriverEntry

 

请读者据资料2WinDbg帮助文档认真实践常用的命令,同时在网上阅读一些调试高手发表的文章。只要多实践,可以很轻松的掌握这项基本功,毕竟我们已经拥有源代码,这和通过反编译来破解信息的难度是不可相提并论的。

 

五、结语

本篇是驱动编程学习过程中必须跨过的生死关,如果时间充裕,建议将资料1的第二至第五章先通读一遍,再精读两至三遍。一些不影响大局的细节(如第三章)粗通即可,无须死记;一些新的概念,如第四章的同步技术,可多花费时间尽可能努力的理解这项技术。

驱动编程与汇编语言的学习有相似之处,入门总是先难后易。如果代码在读者的眼中和天书一般艰难,请不要怀疑自己的能力。不能理解的概念无非是因为在它之前还存在其他未知的知识,把一个庞大的论题分解为若干小块,逐步解决它们,总有豁然开朗的时候。

本篇不设置参考完成时间,根据个人的实际情况,尽快完成入门阶段的学习。

 

 

文件系统驱动编程基础篇之三——Ioctl控制操作

一、前略

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

参考资料*

1.《Programming the Microsoft Windows driver model》第一版(当前阶段主要阅读资料,阅读第二章三小节、第九章三小节)

2.《Inside Microsoft Windows 2000 3rd》(中文版为《Windows2000内部揭密》)第三章二小节

3.《Named Device Objects》以及相关链接

4.《Defining I/O Control Codes

5.《Windows核心编程》第13章一小节

6.《Understanding and Using Execution Context in NT Drivers》(译文http://blog.donews.com/zwell/archive/2004/12/15/203221.aspx

 

阅读基础:不限。

 

本章目的:了解上下文的概念、Ioctl的基本使用方式。

 

二、对象管理与命名空间(Namespace

内核空间中不同类型的对象都通过对象管理器统一管理,并通过命名空间这一逻辑上的概念来组织各个对象,类似于资源管理器。Device目录存放着通过IoCreateDevice创建的各种设备对象,包括文件系统驱动下创建的卷对象。FileSystem目录存放着文件系统驱动对象和文件系统识别器设备对象(这些内容将在进阶篇叙述)。更具体的描述请参看资料2

 

到目前为止,我们还未讨论过用户模式下的应用程序如何与驱动程序发生交互,请暂时忘记“中断门”、“陷阱门”这类“高深莫测”的术语(大肆宣扬这些术语反而有引入歧途的动机),这些包含在CPU硬件理论中的基础知识不会对我们学习驱动编程有直接的影响,相反,值得一提的却是CreateFile函数。文件是一个高度抽象的概念,既然内核中的对象可以被统一管理,外部的各种设备自然也不例外,它们都可以用文件来加以描述。从图中我们看到计算机中的串口COM1,它对应着设备对象Serial0,而C:盘,对应着是卷设备对象HarddiskVolume4,这是一种称为“符号链接”的映射,通过这个映射,用户模式下的程序才能看到内核中的设备对象,也才可以通过CreateFile打开它们。形象的说,符号链接类似于小名,如大狗一般就称为“旺财”,小狗就叫做“小白”。在内核中建立符号连接可使用IoCreateSymbolicLink,用户模式下可用DefineDosDevice

CreateFile的使用示例,注意“.”对应着命名空间里的“GLOBAL??”:

    if((hDevice = CreateFile( ".//IoctlTest",

            GENERIC_READ | GENERIC_WRITE,

            0,

            NULL,

            CREATE_ALWAYS,

            FILE_ATTRIBUTE_NORMAL,

            NULL)) == INVALID_HANDLE_VALUE) {

 

另一种途径就是Ioctl控制操作。

 

三、Ioctl控制码

Ioctl控制码的结构类似于消息(如WM_XXX)或NTSTATUS的定义方式,它是一个驱动程序预定义的4字节整数,定义它的宏为:

#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

                                       1631      213     01    1415

 

通过提供设备类型、功能码(可看作函数的序号)、缓冲方式和存取权限,该宏就创建了一个Ioctl码。设备驱动可以定义多个Ioctl码(通过不同的功能码来区分不同的功能函数)以提供不同的控制功能。

 

四、Ioctl的同异步与缓冲区操作

使用DeviceIoControl函数来实现用户模式下的Ioctl操作,它的定义如下:

BOOL DeviceIoControl(
  HANDLE hDevice,
  DWORD dwIoControlCode,
  LPVOID lpInBuffer,
  DWORD nInBufferSize,
  LPVOID lpOutBuffer,
  DWORD nOutBufferSize,
  LPDWORD lpBytesReturned,
  LPOVERLAPPED lpOverlapped
);

 

根据Ioctl码的不同,DeviceIoControl函数可以发出两种IRPIRP_MJ_FILE_SYSTEM_CONTROLIRP_MJ_DEVICE_CONTROL,前者代表了file system I/O control (FSCTL)请求,后者代表了设备的IOCTL请求。还有一种仅仅在内核模式下使用的IRP_MJ_INTERNAL_DEVICE_CONTROL,它用于内核不同组件间的通信。

DeviceIoControl需要提供输入和输出缓冲区:

 

该函数的使用请参看资料1的第九章三小节,最后一个参数的存在,使Ioctl可以以同步或异步(前提是hDeviceFILE_FLAG_OVERLAPPED方式打开)的方式来完成。同步方式下,函数必须等待内核中的操作完成才返回,否则将立即返回。

Ioctl码的缓冲区类型可分为三类:METHOD_BUFFEREDMETHOD_IN_DIRECTMETHOD_OUT_DIRECTMETHOD_NEITHER。不同类型的缓冲区体现了操作的效率上的不同:

 

 

我们暂不理会驱动程序如何找到用户模式下的输入、输出缓冲区,以及如何定义拷贝缓冲区,先来关注一下这三种方式的不同之处。上两图清晰的表达了自身的特点:Buffered方式在输入、输出数据时都要产生用户缓冲区与内核中的拷贝缓冲区间的复制操作,效率上较低,而Direct方式从字面上理解即为“直接”,从上图也可看出,它的输出操作是通过MDL方式完成的,这是一种用户模式内存映射到系统(内核)内存的方法,避免了复制操作,效率上就提高了。

Neither方式下,I/O管理器直接将用户模式下的缓冲区地址传递给内核驱动程序,不做任何映射变换。驱动如果想直接使用这个地址,必须处于这个进程的上下文中,因为只有在同一个上下文中,用户进程和驱动例程使用的同一个地址值,才能被系统映射到同一个内存页面。

也许大家会疑惑驱动例程究竟是由哪个线程执行的?事实上,不同例程的代码很可能由不同类型的线程来执行,有的属于用户模式下的线程,有的属于操作系统的线程,有的甚至根本不是线程对象。我们以“上下文”来描述驱动例程运行的线程环境,某时刻的它运行在三种上下文的一种中:

- 系统进程上下文System process context

- 特定用户线程(和进程)上下文A specific user thread (and process) context

- 任意用户线程(和进程)上下文Arbitrary user thread (and process) context

 

我们不妨简单的理解为,在某种上下文中,CPU正执行着我们的驱动例程指令。“上下文”做为一个概念上的抽象,在具体实现上有着明确的数据结构,为了更好的理解上下文,请阅读资料6。随着实践的增加,读者对此将有更深入的理解。

 

五、Ioctl上的实践

Ioctl有着广泛的应用,它使用简单,功能却很强大。笔者选择了一些示例,读者可以根据需要选读。

(一)   WINDDK/3790/src/general/ioctl

这个示例堪称Ioctl应用的标准样本。SioctlDeviceControl是实现自定义Ioctl码的函数,需重点研究。IO_STACK_LOCATION子域Parameters的操作规范,读者可以查阅MsdnIRP_MJ_DEVICE_CONTROL节的说明。Io堆栈的重要性不亚于IRP,需要熟悉它的结构与和相关的函数。

这个示例还演示了如何手动加载服务,这是一个三板斧的过程:

安装驱动程序流程:

1、调用OpenSCManager()打开服务控制管理器

2、调用CreateService()创建一个服务,服务类型为内核驱动

3、调用OpenService()取得服务句柄

 

启动服务:

4、调用StartService()启动服务

 

停止服务:

4、调用ControlService()停止服务

 

删除服务:

4、调用DeleteService()删除服务

5、调用CloseServiceHandle()关闭服务句柄

 

操作驱动程序流程:

1、调用CreateFile()取得设备句柄

2、调用DeviceIoControl()传递I/O控制代码

3、调用CloseHandle()关闭设备句柄

 

(二)《Rootkits——Windows内核的安全防护》第42小节的内核钩子

(三)《城里城外看SSDT

(四)《被占用文件操作三法》

示例henum需要在c文件首部添加#pragma comment(lib, "ntdll.lib"),并将DDK中的ntdll.lib复制到目录下,关键函数为NtQuerySystemInformationNtQueryInformationFile。后两个示例和文件自删除技术一样,思路来自对内核对象的理解。

(五)实现了读取磁盘序列号等操作的diskid32源码

 

六、结语

本篇内容难度不大,我们在理解Ioctl设计思路的同时也进一步熟悉了各种数据结构。除了示例一,其他示例仅要求以最大能力去理解,本文的参考完成时间为不超过两星期。

 

 

文件系统驱动编程基础篇之四——WMI管理规范

一、前略

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

参考资料*

1.《Programming the Microsoft Windows driver model》第一版(当前阶段主要阅读资料,阅读第十章)

2.Windows Management Instrumentation (WMI)》(Msdn上关于WMI的标准文档,建议认真阅读)

3.《Microsoft Windows Management Instrumentation: Background and Overview》(345可用作参考,不严谨,且较陈旧)

4.《Windows Management Instrumentation: International Support Overview

5.《Microsoft Windows Management Instrumentation: Advantages to Developers

6.百度百科关于WMI有关名词的注释

7.Windows Management Instrumentation (Windows 管理规范) 的秘密

8.WDM Provider

8.Managed Object Format (MOF)》(Msdn上关于MOF的标准文档,建议认真阅读)

9.WMI Property Qualifiers

10.MOF Data Types

11.COM API for WMI

12.WMI脚本入门》

13.WQL (SQL for WMI)

 

阅读基础:了解COM组件的调用方法。

 

本章目的:了解WMI管理规范在系统下的广泛运用,深入理解MOF,学会编写内核模式和用户模式下的WMI程序。

 

二、WMI简介

一)基于Web的企业管理(Web-Based Enterprise Management (WBEM))的提出是为了解决企业在快速发展的过程中,总成本(Total cost of ownership(TCO) 也随之快速增长的矛盾,它作为一项业界倡议,起始于1996年,规范了企业网络中受管资源的描述与使用。WBEM建立在通用信息模型(Common Information Model (CIM Desktop Management Task Force,即DMTF推动的工业化标准))规划(schema)的基础上。WBEM提出了一个标准化的的方法用于建立统一的框架,不同技术和平台上产生的管理信息均以相同的形式供管理程序访问,这样就减少了维护费用和企业网络的寿命周期成本(life cycle costs)。(注:Common除了译为“通用”,也可译为“公共”)

 

从根本上说,WBEM提供了数据定义的信息标准和组件交互的处理标准。

 

二)CIM是一种机制,用于为受管资源建模并以受管对象格式(Managed Object Format (MOF))表现这些模型。使用 CIM MOF,组成受管资源或资源网络的组件可以象在面向对象设计过程中使用的组件一样被建模和看待。

CIM由一个核心模型,许多通用模型以及扩展模型组成。核心模型是一系列类、连接和属性的集合,该对象组提供了所有管理域公用的基本信息模型;通用模型提供特定管理域的通用信息模型,这些特定的管理域,如系统、应用程序、设备、用户和网络等;扩展模型代表通用模型的特定技术扩展。

  • A core model—incorporates classes applicable to all management domains.
  • Common models—incorporate classes common to specific management domains, independent of particular technologies or implementations. Common domains include systems, applications, devices, users, and networks. These models provide a basis for the development of management applications and include a set of base classes for extension into technology-specific areas.
  • Extension models—these represent technology-specific extensions of the common models. These models are specific to environments, such as operating systems (for example, UNIX, or Microsoft Windows).

三)Windows Management Instrumentation——通常译为WMI管理规范,是微软提出的,与WBEM兼容的技术,同样也兼容于CIM2.02.5WMIWindows管理服务的主要组件,它提供的功能如下(从资料2节选,重点记忆红字即可mcieels):

  • A rich and consistent model of Windows 98 and Windows 2000 operation, configuration, and status. 是一个模型
  • A COM API that supplies a single point of access to all management information. COM访问
  • Interoperability with other Windows 2000 management services, which will simplify vendors' efforts to create well-integrated management applications. 可协作
  • A flexible architecture that allows vendors to extend the information model to cover new devices, applications, and other enhancements by writing code modules (WMI providers). 可扩展
  • A powerful event architecture that allows changes in management information to be identified, aggregated, compared to and associated with other management information, and forwarded to local or remote management applications. 有事件机制
  • A rich query languageWQL that enables detailed queries of the information model. 可查询
  • A scriptable API, which enables management application developers to use Visual Basic® or Windows Script Host (WSH). 可脚本访问

 

10-2是上图架构的简化,在WMI模型中,数据和事件被分成了消费者(Consumers)和生产者(Providers)两类。数据块就是抽象类的实例,其概念与C++中的类概念一致。如同C++中的类,WMI类也有数据成员和实现对象行为的方法。数据块中的内容并不是由WMI指定,而是由数据生产者和数据的使用目的决定的。送往驱动程序的数据最有可能来自管理者本身的操作。而驱动程序发出的数据通常是某种性能的统计数据,这些数据的消费者可能是某个性能监视程序。

 

 

WMI类允许同时存在全球化和本地化的数据,详看资料3

WMI类又可称为主类(master class),由基类(basic class)和amendment(修正,纠正之意,用于本地化,难以揣摩合适的译文^_^)类组成。主类包含所有的属性集和限定(qualifiers)。基类是主类的子集,包含所有的属性集和部分限定,不包含本地限定(localizable qualifiers)。amendment和主类具有相同的名字,是一个抽象类,包含了关于本地限定的属性子集(includes a subset of properties with localizable qualifiers),不包含主类的其他属性。

amendment总是位于包含基类定义的命名空间下的子命名空间。每一个子命名空间包含着特别本地化(particular locale)的amendment类,这样设计的结果是多种语言的子命名空间可以被加入到储存库(repository,见架构图)中,因此可同时存在多种语言的类定义:

ROOT/CIMV2 
ROOT/CIMV2/MS_409 

ROOT/CIMV2/MS_407

 

WMI允许存在多重命名空间,每个命名空间中包含的类属于一个或多个用户模式生产者。生产者使用平台SDK中公开的COM接口来注册Windows管理服务(Windows Management Service)。操作系统(包括所有设备驱动程序)支持一个名为root/cimv2的命名空间,里面包含了CIM版本2

 

四)WDM驱动程序可以作为WMI类实例的生产者。一个描述了驱动程序支持的各种类(驱动程序可以为这些类提供数据)的脚本称为驱动程序规划(schema)。我们可以使用MOFManaged Object Format)语言定义规划。系统则维护一个称为储存库(repository)的数据字典,它包含了所有已知的规划定义。如果驱动程序做得正确,系统将在初始化驱动程序时自动把规划放到储存库中。

WDM生产者是WMI生产者的一个组成部分,它可以访问WDM硬件驱动的类、实例、方法和事件。硬件驱动的类位于root/wmi命名空间,Wmi.mofWmicore.mof定义了主要的WDM类。WDM生产者允许管理程序从满足WMI-for-WDM的设备驱动访问数据和事件,生产者主要以IWbemServices接口的形式提供这些服务。

为了查看命名空间,可到微软站点下载安装WMITools软件。WMI CIM Studio以树视图的形式展现了分层的命名空间。

 

三、MOFWQL语言

MOF是一种基于接口定义语言 (IDL) 的语言,用于描述管理信息,即用于描述CIMMOF 语法是以文本形式描述对象定义的方法。MOF 汇编器(如mofcomp.exe)处理 MOF 文件,并向 CIM 储存库添加必需的对象定义。C++Builder类型库(Type Library)也使用了IDL

MOF语法类似于C++,但远比C++简单,如果在学习过程中遇到困难,可以联想两者的相似处。如MOF类的实例化可以想象成C++的构造函数,引用也可以联想成C++&

一个简单的MOF文件master.mof如下:

#pragma amendment ("MS_409")

 

[Description("Localized version of MyClass for American English") :

    Amended, LOCALE(0x409)]

限定风格

 


限定

Class myclass

{

     [DisplayName("User Name") : Amended,

     Description("The Name property contains the name of the user") :

     Amended, key]

    string Name;

 

    uint64 Value; // non-localized value field

 

     [DisplayName("Time Stamp") : Amended,

     Description("This property shows when the object was created") :

     Amended]

    uint64 Timestamp;

};

 

MOFC++类定义有相似之处,也使用#pragma预处理。#pragma amendment指示编译器输出语言中性和语言特定的两个版本,“MS_409表示本地标识符(LCID),类似还有“MS_408”等。

MOF的数据类型见资料10

MOF包含了丰富的限定Qualifier [ ]里以逗号分隔的标识符),用于描述类、实例、属性、方法和方法的参数。限定名不区分大小写,除此之外,它还遵守类似于C++命名的一些约束。限定可分为三大:标准限定(Standard qualifier),CIM限定(CIM qualifier),特殊限定(Unique qualifier),你还可以为自己的生产者创建自定义的限定。

限定可以被称为“限定风格”(Qualifier Flavor)的标志来修饰,语法为:

[qualifier1 : flavor1 flavor2 flavor3, qualifier2 : flavor1]

 

因此,综合了限定以及限定风格的分类后也可进行如下的划分(*WDM里常见):

Qualifier Type

Description

注释

Meta

Refines the definition of meta-constructs by clarifying the actual usage of a class or property declaration.

通过MOF语法阐明类或属性声明的实际用途,元限定完善了CIM模式元结构的定义。

Standard*

Supports the descriptions that all CIM-compliant implementations must handle.

 

Optional

Addresses situations not common to all CIM-compliant implementations.

 

WMI-specific*

Describes qualifiers specific to WMI, such as performance counter class qualifiers.

 

Qualifier Flavors

Provides additional information about a qualifier, such as whether a derived class or instance can override the qualifier's original value.

 

 

一些限定与风格:

Qualifier

分类

注释

示例

Amended

Flavor

基类不用该限定,amendment类用于本地化

DisplayName("User Name") : Amended

Amendment

WMI-specific – Standard

指明类里包含被本地化的amended限定

Amendment

Description

Standard

描述了被命名的元素,默认为NULL

Description("This property shows when the object was created")

DisplayName

Standard

代替真实的元素名而显示在UI的名字

DisplayName("Time Stamp")

Dynamic

WMI-specific – Standard

指明类的实例被动态创建

Dynamic

Guid

自定义

必须,驱动程序用来辨别生产者

wmi42.mof

Key*

Standard

键属性用来标识和区分每一个实例

Key

Locale

WMI-specific – Standard

为类或实例指定语言

Locale(0x409)

Provider

WMI-specific – Standard

限定的值是动态生产者的名字,生产者创建类实例和更新实例数据

Provider("WMIProv")

WMI

自定义

表示生产者类型

wmi42.mof

WmiDataId

WMI-specific – WDM

Index in the WNODE of the data for the property. The WDM provider uses this qualifier to determine how the data is formatted while extracting data from the WNODE and generating WMI classes. The starting value is 1. (除了InstanceNameActive属性,其他属性都必须带有该Id

WmiDataId(1)

* KeyMsdn上存在矛盾的说明。错误的说明是认为仅InstanceName能被声明为Key,但实际上可由多个属性组成复合键。

 

对于难以理解的限定,可通过CIM Studio来查看实际的效果。如属于Meta类别的Association是一个难以琢磨的限定,请看下面的mof文件:

#pragma namespace(".//root")

 

instance of __Namespace

{

    Name = "WMI" ;

} ;

 

#pragma namespace(".//root//WMI")

 

Class A{

    [key] string aKey;

};

 

Class C{

    [key] string cKey;

};

 

Class D{

    [key] string dKey;

};

 

Class E{

    [key] string eKey;

};

 

// The following class creates an association between the "A", "C", "D", "E" class

    [Association] Class B{

    [key] A ref aRef;

       [Key, Min(1)] C ref cRef;

       [key] D ref dRef;

       [key] E ref eRef;

};

 

它在命名空间root/wmi下创建了ABCDE五个类,它们的Association关系分别为:

     

 

MOF汇编器常用的三种操作:

1)  检查MOF文件语法:mofcomp –check master.mof

2)  创建语言中性和语言特定的MOF文件:mofcomp -MOF:g.mof -MFL:l.mof master.mof

3)  编译成二进制的BMF文件,该文件可以自定义资源的方式加入C++工程的资源文件中:mofcomp –B: bin.bmf l.mof

// g.mof0x409 = 1033

[LOCALE(1033)]

class myclass

{

  [key] string Name;

  uint64 Value;

  uint64 Timestamp;

};

 

// l.mof

#pragma namespace(".//root//default")

instance of __namespace{ name="ms_409";};

#pragma namespace(".//root//default//ms_409")

 

[Description("Localized version of MyClass for American English") : Amended,AMENDMENT, LOCALE(0x409)]

class myclass

{

  [DisplayName("User Name") : Amended,Description("The Name property contains the name of the user") : Amended,key] string Name;

  [DisplayName("Time Stamp") : Amended,Description("This property shows when the object was created") : Amended] uint64 Timestamp;

};

 

注释// /* */并非可随处添加,如汇编器提示“文件域意外的符号”时要特别注意注释是否放于错误的位置上。

instance of __namespace一行实例化了ms_409命名空间(即root/DEFAULT/ms_409),下一行则在这个命名空间加载myclass类。

 

通过语法检查的mof文件可以加入WMI命名空间(N表示默认加载的空间):

mofcomp -N:root/default g.mof

 

 

使用CIM Studio查看储存库里的g.mof,对于l.mofValue不会出现在属性表中:

 

 

通过CIM Studio里的MOF Generator工具还可以生成你感兴趣的节点的MOF文件。

请阅读如下toaster.mof,理解包含的语法,并在WMI命名空间下验证自己的理解:

[Dynamic, Provider("WMIProv"),
 WMI,
 Description("Toaster driver information"),
 guid("{BBA21300-6DD3-11d2-B844-00C04FAD5171}"),
 locale("MS//0x409")]
class ToasterDeviceInformation
{
    [key, read]
     string InstanceName;
    [read] boolean Active;
 
    [WmiDataId(1),
     read,
     WmiEnum{"0=I8042 Connector"
             "1=Serial Connector",
             "2=Parallel Connector",
             "3=USB Connector" },
     Description("How the toaster is connected to the computer")]
    uint32 ConnectorType;
 
    [WmiDataId(2),
     read,
     Description("This indicates the capacity in Kilo Watts of the toaster device.")]
    uint32   Capacity;
 
    [WmiDataId(3),
     read,
     Description("Number of errors that occurred on this device")]
    uint32   ErrorCount;
 
    [WmiDataId(4),
     read,
     Description("Indicates the number of controls on the toaster device.")]
    uint32   Controls;
 
    [WmiDataId(5),
     read,
     write,
     Description("The DebugPrintLevel property indicates the debug output level of toaster device.")]
    uint32 DebugPrintLevel;
                    
    [WmiDataId(6),
     read,
     Description("ModelName")]
    string ModelName;
 
};
 
[WMI, Dynamic, Provider("WMIProv"),
 guid("{01CDAFF1-C901-45b4-B359-B5542725E29C}"),
 locale("MS//0x409"),
 WmiExpense(1),
 Description("Notify Toaster Arrival")]
class ToasterNotifyDeviceArrival : WMIEvent
{
    [key, read]
    string                      InstanceName;
 
    [read]
    boolean                                       Active;
 
    [read,
     Description("Device Model Name"),
     WmiDataId(1)]    string      ModelName;
};

 

为了解系统默认生成的命名空间,可阅读system32/wbem路径下的MOF文件。为了掌握MOF的语法,可阅读DDK源代码目录下的MOF文件。

WQLAnsi-SQL的简化版本,它可以用来查询命名空间下的类,如select * from wmi42,对此不再详述。

 

三、WMI与驱动程序

请先阅读资料1第二节,以及wmi42示例的SYS部分。

驱动程序对WMI的支持,体现在对系统控制IRP,即IRP_MJ_SYSTEM_CONTROL的支持上。除了以Ioctl类似的方式实现该IRPAltWmi.cpp),更简单的方式是委托WMILIB来支持WMIWmi.cpp),这涉及到一个数据结构:

// This structure supplies context information for WMILIB to process the

// WMI irps. Memory for this structure may be paged.

typedef struct _WMILIB_CONTEXT

{

    // WMI data block guid registration info

    ULONG GuidCount;

    PWMIGUIDREGINFO GuidList;

 

    // WMI functionality callbacks

    PWMI_QUERY_REGINFO       QueryWmiRegInfo;

    PWMI_QUERY_DATABLOCK     QueryWmiDataBlock;

    PWMI_SET_DATABLOCK       SetWmiDataBlock;

    PWMI_SET_DATAITEM        SetWmiDataItem;

    PWMI_EXECUTE_METHOD      ExecuteWmiMethod;

    PWMI_FUNCTION_CONTROL    WmiFunctionControl;

} WMILIB_CONTEXT, *PWMILIB_CONTEXT;

 

Wmi.cpp是如何处理WMI请求的?针对系统控制IRP里的几个副功能码,我们定义了相应的回调函数,WMILIB_CONTEXT结构里的回调函数指针指向我们的函数。当WMI派遣例程DispatchWmi调用WmiSystemControl来处理IRP时,就会自动调用这些函数。AddDeviceRemoveDevice例程调用IoWMIRegistrationControl为设备对象注册或注销作为WMI数据生产者(WMI data provider)的驱动程序。注册之后,第一个被处理的系统控制IRP的副功能码是IRP_MN_REGINFO,派遣函数QueryRegInfo被调用以处理该功能码。在QueryRegInfo里,返回了系统用于创建数据块的MOF资源的名字。其他派遣函数就此略过,Msdn里对每个函数都有详细的叙述。

Wmi42正确启动后应该符合下图的情形(如果在安装上碰到问题,可暂时跳过,下篇将对安装问题作进一步的说明):

 

 

 

留有余力的读者,可进一步研究WMI的事件机制,在此略过。

 

四、WMICOM与用户程序

请阅读资料1第三节,以及Chap10/WMI42示例的TEST部分。

本篇是初次涉及COM编程,今后我们还将陆续接触COM在各领域的应用。COM既是一种技术,也是一种组件,这种组件对外公开了一些称为“接口”的抽象类,除此以外都是不透明的。假如我们剖开外壳可以看到内部的实体,即不是接口的那些类,由被称为“类厂”的类来创建(即用类来创建类)。实体通过接口对外提供了COM的所有服务。接口、实体、类厂可看成COM组件的基本元素。

接口的使用有一个很重要的约定——凡是查询/获取一次接口,都要用AddRef()增加一次计数,接口使用后都要用Release()释放本次计数。有时候我们使用API函数后没有调用AddRef(),却需要Release(),原因是这些函数内部已有调用代码。这样的常用API并没有几个,我们可以通过实践很快的掌握,一些不确定是否需要释放计数,以及如何释放计数的API可以查阅Msdn

COM出现后不久,一些“懒人”写出了自动维护约定的智能指针,随后ATL出现了。客观的说,微软在技术上的创新贡献是有目共睹的,但是ATL?它把所有看到的东西一律封装成模板,这样你可以仅用ATL就可以完成一个实用的Windows程序了,它的技术书籍重得甚至可拿来压舱底。相比大多数人的敬而远之,微软虽然也在逐渐淡化COM,但即便如此,今天很多的新生技术外表下都隐藏着COM

       WMI也是通过COM接口提供服务,详见资料11

TEST示例首先初始化COM和为进程设置了默认的安全值,接着打开root/wmi命名空间,设置接口权限,最后报告Wmi42类属性的信息。

 

在此例的基础上,可深入学习各类接口函数,弄清MOF语法如何以COM接口方式调用。

代码《let_us_try_wmi_samples》,《WMI C++ Application Examples》演示了如何使用WMI访问常见硬件,后篇系统的总结了用户模式下调用WMI的基本步骤。另可阅读Msdn上关于Vista下使用WMI的论述。

 

五、IClientSecurity接口与安全

vs2005Msdn里并没有IClientSecurity接口的说明(官方站点有此说明的),既然微软似乎有意忘记了,笔者也实在不愿花费时间陷入细节中去,我们可在在实践中来体会该接口的功能。

IClientSecurity

Gives the client control over the security settings for each individual interface proxy of an object. The methods of IClientSecurity can be used to set or query the security settings of a specific interface proxy or to copy an interface proxy.

Every object has one proxy manager, and every proxy manager exposes the IClientSecurity interface automatically. Therefore, the client can query the proxy manager of an object for IClientSecurity, using any interface pointer on the object. If the QueryInterface call succeeds, the IClientSecurity pointer can be used to call an IClientSecurity method, passing a pointer to the interface proxy that the client is interested in. If a call to QueryInterface for IClientSecurity fails, either the object is implemented in-process or it is remoted by a custom marshaler that does not support security. (A custom marshaler can support security by offering the IClientSecurity interface to the client.)

The interface proxies passed as parameters to IClientSecurity methods must be from the same object as the IClientSecurity interface. That is, each object has a distinct IClientSecurity interface: calling IClientSecurity on one object and passing a proxy to another object will not work. Also, you cannot pass an interface to an IClientSecurity method if the interface does not use a proxy. This means that interfaces implemented locally by the proxy manager cannot be passed to IClientSecurity methods, except for IUnknown, which is the exception to this rule.

For more information about proxies, see IMarshal - Default Implementation.

 When to Implement

The proxy manager for each object provides an implementation of IClientSecurity, so you would typically not implement this interface. If, however, you are defining objects that support custom marshaling, you may choose to implement IClientSecurity on the objects' custom proxies to maintain a consistent programming model for the objects' client applications. You may also choose to support this interface on in-process objects.

 When to Use

Call the methods of this interface to examine or modify the security settings of a particular connection to an out-of-process object. For example, you might temporarily establish a higher security level — one with complex encryption — only for the period when sensitive information or data is being sent to the object. Alternately, you might establish different proxies to the same object with different security levels. You could use these security levels to support different clients that are calling your object or to support different operations within your application.

 Methods in Vtable Order

 

IClientSecurity Methods

Description

QueryBlanket

Retrieves authentication information.

SetBlanket

Sets the authentication information that will be used to make calls on the specified proxy.

CopyProxy

Makes a copy of the specified proxy.

 

六、结语

除了Ioctl,现在我们又掌握了一个了解系统的重要手段,读者在关注WMI细节的同时,也请留意一个系统性的事物如何整合各个分系统。

本篇作为选读内容,不设参考完成时间。

 

 

文件系统驱动编程基础篇之五——注册表与Inf

一、前略

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

参考资料*

1.Programming the Microsoft Windows driver model》第一版(当前阶段主要阅读资料,阅读第二章一小节、第二章三小节、第三章五小节、第十二章)

2.Registry Keys for Drivers

3.Windows上获得IP地址的四种方法

4.Device Information Sets

5.Using a Device Interface

6.Using Device Installation Functions

7.From the Lab: Mapping USB devices via LNK files

8.Getting a file handle of a USB volume from its vid/pid/serial number

9.Fill Level field in DEVICE_NODE structure

10.Tracing USB Device artefacts on Windows XP operating system for forensic purpose

11.INF Models Section》关于硬件ID的命名规则部分

12. devids.txt

 

阅读基础:不限。

 

本章目的:了解注册表在驱动编程的重要作用,阅读并学会编写简单的Inf

 

二、注册表的配置

注册表以树形方式存储配置信息,树节点称为键(key),键可以包含子键(subkey)和称为值(value)的数据项。

 

一)需要关注的几种键(注:硬件键、类键、设备接口类应是所列位置下的子键)

1、硬件键*(设备键):HKLM/SYSTEM/CurrentControlSet/Enum/enumerator/deviceID Service指向服务键,值ClassClassGUID等指向类键

2、类键(软件键、驱动键):HKLM/System/CurrentControlSet/Control/Class 即设备创建类,区分了驱动程序的类别,如GUID_DEVCLASS_1394GUID_DEVCLASS_CDROM

3、服务键:/REGISTRY/MACHINE/SYSTEM/CurrentControlSet/Services/DriverName 键名取自驱动程序.sys的主名字,DriverEntry的第二个参数指向该键,如Raid卡驱动的fasttx2k.sysfasttx2k,又如Fastfat文件系统驱动的为/Registry/Machine/System/CurrentControlSet/Services/Fastfat

4、硬件配置文件:HKLM/SYSTEM/CurrentControlSet/HardwareProfiles

5、设备接口类(IoRegisterDeviceInterface注册):HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/DeviceClasses 后有详述

6、硬件描述:(1HKLM/HARDWARE/DESCRIPTION/System 登记biosvideo bios版本等,Identifier键表示运行的机型,如AT/AT COMPATIBLEFUJITSU FMR-

2HKLM/HARDWARE/DEVICEMAP/SERIALCOMM PARALLEL PORTS等)

7、文件系统:HKLM/SYSTEM/CurrentControlSet/Control/FileSystem/,兼容模式键Win31FileSystem,代码页恒定FatDisableCodePageInvariance(跟长文件名有关)

8、查询网卡IP地址的一个方法:HKLM/SOFTWARE/Microsoft/Windows NT/CurrentVersion/NetworkCards/9,以本机为例,GLOBAL??视图里的符号连接为//./{420FB3CF-4468-4C6B-99E5-EB2FEBAA22D1},即//./Device/NTPNP_PCI021(图中ServiceName即服务键,Parameters/Tcpip下包含IP地址等信息。CurrentVersion还包括了很多当前的硬件信息。而通过HKLM /SYSTEM/CurrentControlSet/Enum/PCI/VEN_11AB&DEV_4364&SUBSYS_00BA11AB&REV_14/4&2531c6b0&0&00E4这个网卡硬件键找到的服务键为yukonwxp,仅包含驱动信息,不包含IP地址信息)

9、查询串口的一个方法:HKLM /HARDWARE/DEVICEMAP/SERIALCOMM

 

*(资料1第二章一小节)Enum键下的第一级子键与系统中的各种总线枚举器相对应。/Enum/USB子键中包含了所有以前用过和现在存在的USB设备的描述。在USB42例子中,我将阐述怎样把设备的硬件ID(vendor 0574, product 102A)转换成键名(Vid_0574&Pid_102A),以及如何使有该ID的设备实例被表示为下一层的子键7&27&2就是该设备的硬件(或实例)键名。(实际在第十二章“设备标识符”部分详述)

硬件键

设备标识

枚举器

 

二)第3点中的服务键的写法与其他键有所不同,它以/REGISTRY打头,这是内核模式下根键的规定写法。

User-mode Handle

Corresponding Object Name

HKEY_LOCAL_MACHINE

/Registry/Machine

HKEY_USERS

/Registry/User

HKEY_CLASSES_ROOT

No kernel-mode equivalent

HKEY_CURRENT_USER

No simple kernel-mode equivalent, but see Registry Run-Time Library Routines

 

三)服务的启动类型,如Start3表示按需启动,scm在基础篇四已经有所论述了。

启动类型

注释

SERVICE_AUTO_START
0x00000002

A service started automatically by the service control managerscm during system startup. For more information, see Automatically Starting Services.

SERVICE_BOOT_START
0x00000000

A device driver started by the system loader. This value is valid only for driver services.

SERVICE_DEMAND_START
0x00000003

A service started by the service control manager when a process calls the StartService function. For more information, see Starting Services on Demand.

SERVICE_DISABLED
0x00000004

A service that cannot be started. Attempts to start the service result in the error code ERROR_SERVICE_DISABLED.

SERVICE_SYSTEM_START
0x00000001

A device driver started by the IoInitSystem function. This value is valid only for driver services.

 

四)Chap6/pnpevent示例驱动为例的具体键值,硬件键的命名问题详看资料1第十二章。

 

三、内核模式下注册表的访问

请先阅读资料1第三章五小节,本小节仅仅补充两个有删节的示例。

一)IoOpenDeviceRegistryKeyZwSetValueKey的示例:

     PLOCAL_DEVICE_INFO pLDI;

     PIO_STACK_LOCATION pIrpStack;

     NTSTATUS Status;

 

     HANDLE handle;

     UNICODE_STRING ValueName;

     ULONG Value = 0x101; // this is the value we're setting the key to.

 

     PAGED_CODE();

 

     pIrp->IoStatus.Information = 0;

     pLDI = (PLOCAL_DEVICE_INFO)pDO->DeviceExtension;    // Get local info struct

 

     Status = IoOpenDeviceRegistryKey(pLDI->NextLowerDriver/*pdo*/, PLUGPLAY_REGKEY_DEVICE, KEY_READ, &handle);

 

     if (NT_SUCCESS(Status)) {

         RtlInitUnicodeString(&ValueName, L"Value");

         Status = ZwSetValueKey(handle, &ValueName, 0, REG_DWORD, &Value, sizeof(ULONG));

         if (NT_SUCCESS(Status)) {

              ZwClose(handle);

         } else {

              KdPrint(("write reg failed")); // handle error.

         }

       }

 

IoOpenDeviceRegistryKey的参数DevInstKeyType指示打开哪个注册表键,Msdn里的说明有些让人困惑。实践上的结果为:PLUGPLAY_REGKEY_DEVICE(打开硬件键的Device Parameters子键)PLUGPLAY_REGKEY_DRIVER(打开类键)、PLUGPLAY_REGKEY_CURRENT_HWPROFILE(打开配置文件键)。

 

二)RtlQueryRegistryValuesRtlWriteRegistryValue的示例:

NTSTATUS SerialGetConfigDefaults(

                            IN PSERIAL_FIRMWARE_DATA    DriverDefaultsPtr,

                            IN PUNICODE_STRING          RegistryPath // 服务键

                            )

{

     NTSTATUS Status = STATUS_SUCCESS;    // return value

 

     //

     // We use this to query into the registry for defaults

     //

 

     RTL_QUERY_REGISTRY_TABLE paramTable[9]; // 注册表查询用到8个值,最后一个为0表示结束

 

     PWCHAR  path;

     ULONG   zero            = 0;

     ULONG   DbgDefault      = 0;//SER_DBG_DEFAULT;

     ULONG   DetectDefault   = 0;

     ULONG   notThereDefault = SERIAL_UNINITIALIZED_DEFAULT;

 

     PAGED_CODE();

 

     //

     // Since the registry path parameter is a "counted" UNICODE string, it

     // might not be zero terminated.  For a very short time allocate memory

     // to hold the registry path zero terminated so that we can use it to

     // delve into the registry.

     //

     // NOTE NOTE!!!! This is not an architected way of breaking into

     // a driver.  It happens to work for this driver because the author

     // likes to do things this way.

     //

 

     path = ExAllocatePool (PagedPool, RegistryPath->Length+sizeof(WCHAR));

 

     if (!path) {

         Status = STATUS_INSUFFICIENT_RESOURCES;

         return (Status);

     }

 

     RtlZeroMemory (DriverDefaultsPtr, sizeof(SERIAL_FIRMWARE_DATA));

     RtlZeroMemory (&paramTable[0], sizeof(paramTable));

     RtlZeroMemory (path, RegistryPath->Length+sizeof(WCHAR));

     RtlMoveMemory (path, RegistryPath->Buffer, RegistryPath->Length);

 

     paramTable[0].Flags         = RTL_QUERY_REGISTRY_DIRECT;

     paramTable[0].Name          = L"BreakOnEntry";

     paramTable[0].EntryContext  = &DriverDefaultsPtr->ShouldBreakOnEntry; // 保存查询结果

     paramTable[0].DefaultType   = REG_DWORD;

     paramTable[0].DefaultData   = &zero;

     paramTable[0].DefaultLength = sizeof(ULONG);

。。。

     paramTable[7].Flags         = RTL_QUERY_REGISTRY_DIRECT;

     paramTable[7].Name          = L"UartRemovalDetect";

     paramTable[7].EntryContext  = &DriverDefaultsPtr->UartRemovalDetect; // 保存查询结果

     paramTable[7].DefaultType   = REG_DWORD;

     paramTable[7].DefaultData   = &DetectDefault;

     paramTable[7].DefaultLength = sizeof(ULONG);

 

     Status = RtlQueryRegistryValues( RTL_REGISTRY_ABSOLUTE | RTL_REGISTRY_OPTIONAL,

         path, // HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/Serial

         &paramTable[0], // 以全0的结尾表结束,这样一次就可以查询多个值了

         NULL,

         NULL);

 

     if (!NT_SUCCESS(Status)) {

         DriverDefaultsPtr->ShouldBreakOnEntry   = 0;

         DriverDefaultsPtr->DebugLevel           = 0;

         DriverDefaultsPtr->UartRemovalDetect    = 0;

     }

 

     //

     // Check to see if there was a forcefifo or an rxfifo size.

     // If there isn't then write out values so that they could

     // be adjusted later.

     //

 

     if (DriverDefaultsPtr->ForceFifoEnableDefault == notThereDefault) {

 

         DriverDefaultsPtr->ForceFifoEnableDefault = SERIAL_FORCE_FIFO_DEFAULT;

         RtlWriteRegistryValue(

              RTL_REGISTRY_ABSOLUTE,

              path,

              L"ForceFifoEnable",

              REG_DWORD,

              &DriverDefaultsPtr->ForceFifoEnableDefault,

              sizeof(ULONG)

              );

     }

。。。

     //

     // We don't need that path anymore.

     //

 

     if (path) {

         ExFreePool(path);

     }

 

     //

     //  Set the defaults for other values

     //

     DriverDefaultsPtr->PermitSystemWideShare = FALSE;

 

     return (Status);

}

 

四、设备接口

设备创建类(Device Setup Classes)和设备接口类(Device Interface Classes)是驱动编程中经常碰到的两类键。设备创建类区分了驱动的类别,它的标准类定义于devguid.h,我们将在下一节详述。Winodow2000以前的驱动需要自定义设备对象的名字,同时提供和该名字关联的符号链接(如C:COM1)以使应用程序可以访问该设备,使用了设备接口类可以避免这种显式的命名。它在接口类键下创建了设备实例子键(##?…),子键下产生了符号链接名(#{…}。同设备创建类一样,也存在预定义的接口类,如usbiodef.h定义了GUID_DEVINTERFACE_USB_HUBmountmgr.h定义了MOUNTDEV_MOUNTED_DEVICE_GUID

设备接口类

设备实例*

 


符号链接*

存友好名或设备描述*

 

{2eb07ea0-7e70-11d0-a5d6-28db04c10000} (STATIC_KSCATEGORY_DATATRANSFORM):

SetupDiEnumDeviceInterfaces' devindex = 0, interfacedata.Reserved = 1436720

    Microsoft Kernel Acoustic Echo Canceller|Plug and Play Software Device Enumerator, 2232

SetupDiEnumDeviceInterfaces' devindex = 1, interfacedata.Reserved = 1436224

    Microsoft Kernel GS Wavetable Synthesizer|Plug and Play Software Device Enumerator, 2232

SetupDiEnumDeviceInterfaces' devindex = 2, interfacedata.Reserved = 1454616

    Microsoft Kernel DLS Synthesizer|Plug and Play Software Device Enumerator, 2232

SetupDiEnumDeviceInterfaces' devindex = 3, interfacedata.Reserved = 1454648

Microsoft Kernel DRM Audio Descrambler|Plug and Play Software Device Enumerator, 2232

* 键下存放相应内容的值,请自行查看注册表。

 

调用IoRegisterDeviceInterface函数,功能或过滤驱动程序的AddDevice函数可以注册一个或多个设备接口,SymbolicLinkName参数返回了如/??/Root#SYSTEM#0000#{2eb07ea0-7e70-11d0-a5d6-28db04c10000}/{8c07dd50-7a8d-11d2-8f8c-00c04fbf8fef}&dmusic的符号链接串,将来它可以被其他组件用来打开设备句柄。

设备接口在注册后需调用IoSetDeviceInterfaceState函数开放接口,并等到驱动完成IRP_MN_START_DEVICE后,其他组件才可以访问该设备。I/O管理器将查询注册表,如果Linked值为0 且引用计数ReferenceCount值不大于0,说明还未有设备引用该接口,它将创建上述符号链接,否则只是简单的增加引用计数。以后,驱动程序会执行一个功能相反的调用禁止该接口(用FALSE做参数调用IoSetDeviceInterfaceState)。最后,I/O管理器删除符号连接对象,但它保留了注册表项,即这个名字将总与设备的这个实例关联;但符号连接对象与硬件一同到来或消失。注意,该符号链接是创建于该设备的PDO上的,所以PDO的安全描述符将最终控制设备的访问权限,并非应用程序获得该符号链接就一定获准访问该设备。

示例代码Chap6/pnpevent/sys创建了两个设备接口,其中一个是由Generic.sysInitializeGenericExtension 调用RegisterInterface(pdx, &GUID_GENERIC_POWER)注册了一个电源管理接口,符号链接是/??/ROOT#SAMPLE#0001#{894a7461-a033-11d2-821e-444553540000},另一个就是GUID_INTERFACE_PNPEVENT,符号链接为??/Root#SAMPLE#0000#{6a061783-e697-11d2-81b5-00c04fa330a6}

在用户模式下,设备接口也有着重要的应用,为此系统提供了SetupDixxx函数——它是设备安装函数集(Device Installation Functions)的一部分——简化了设备接口的使用。为了解该部分内容,请先阅读资料1第二章三小节及示例代码Chap2/InterfaceEnum

 

一)枚举设备接口

根据示例Chap2/InterfaceEnum,我们可以总结出枚举设备接口的一个可行步骤:

 

函数

注释

1

SetupDiGetClassDevs

returns a device information set that contains all devices of a specified class. 返回设备集

2

SetupDiEnumDeviceInterfaces

returns a context structure for a device interface element of a device information set. Each call returns information about one device interface; the function can be called repeatedly to get information about several interfaces exposed by one or more devices. 返回“设备接口数据”结构

3

SetupDiGetDeviceInterfaceDetail

returns details about a particular device interface. 一般分两次调用来获得“设备信息数据”

SetupDiOpenDeviceInterfaceRegKey*

opens the registry subkey that is used by applications and drivers to store information specific to a device interface instance and returns a handle to the key. 设备接口类下的符号链接下的Device Parameters子键

4

SetupDiGetDeviceRegistryProperty

retrieves the specified Plug and Play device property. 设备在硬件键的以及相关的属性(如枚举器等)

5

通过设备信息集完成你感兴趣的工作

注:关键的三步:SetupDiGetClassDevs -> SetupDiEnumDeviceInterfaces -> SetupDiGetDeviceInterfaceDetail

* 设备硬件键下的Device Parameters可以用SetupDiOpenDevRegKey来访问

 

此外可能用到函数还有SetupDiCreateDeviceInfoListSetupDiDestroyDeviceInfoListSetupDiCreateDeviceInterface SetupDiOpenDeviceInterface等。

示例通过两重循环枚举了系统中设备接口类包含的所有设备,输出结果为:

{2accfe60-c130-11d2-b082-00a0c91efb8b} (StoragePortClassGuid):

    AH4453JX IDE Controller

Win XP Promise FastTrak TX4000/S150 TX Series (tm) Controller

 

如果掌握了设备接口的基础知识,SetupDixxx函数并不显得难以使用,但我们希望大家不要就此满足,对于SetupDixxx涉及到的设备信息集(Device Information Sets),我们有必要了解得更深入一些。

 

 

MsdnSetupDixxx函数的描述里,多次提及了设备信息集,但是没有明确说明它的具体结构,尽管我们不难猜测到是相当复杂的。从原理上说,设备信息集可看作是设备信息元组成的链表,可以用SetupDiGetClassDevs(...DIGCF_INTERFACEDEVICE...)来获取符合定义条件的设备信息集。每个信息元代表着一个设备,包含了设备实例的句柄(DevnodePnP管理器为了管理系统里所有的设备,使用它构造了分层的设备树)和关联该设备的设备接口链表。

当调用SetupDiEnumDeviceInterfaces来枚举这个设备信息集包含的设备接口时,如果DeviceInfoData参数为NULL,将会遍历所有的信息元。对于每个信息元,都将发生如下的操作:从本信息元的设备接口链表中筛选出满足InterfaceClassGuid参数的节点组成临时链表B,如果MemberIndex参数小于B的节点总数,则说明满足条件的节点就在B中,从中取出返回即可。否则MemberIndex = MemberIndex – B节点总数,继续遍历下一个信息元。举例来说,MemberIndex = 2C++一般以0为起点,因此实际上是查找第3个),信息元1的临时链表有2个节点,信息元2的临时链表有3个节点,则我们可以在信息元2找到该节点。

如果DeviceInfoData不为空,则根据它定位到合适的信息元,从中取出满足MemberIndex要求的设备接口。

 

二)枚举设备

既然知道了如何枚举设备接口,枚举设备就是一件轻而易举的事情了。利用先前获得的设备信息集,使用SetupDiEnumDeviceInfo函数来遍历每一个设备。

 

三)本站的《SetupAPI结合注册表获取USB优盘序列号

因为该文缺少原理上的说明,所以我们将之作为设备接口类的一个例子来学习。mountmgr.h里有如下一段话:

//

// Devices that wish to be mounted should report this GUID in

// IoRegisterDeviceInterface.

//

 

DEFINE_GUID(MOUNTDEV_MOUNTED_DEVICE_GUID, 0x53f5630d, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b);

 

意即需要绑定的设备都应该使用这个GUID来注册设备接口,U盘当然也不例外,因此代码中调用SetupDiGetClassDevs建立了设备信息集后,可以使用这个GUID来枚举设备。枚举循环中,SetupDiGetDeviceInterfaceDetail获取设备路径,构造出该设备的硬件键,用硬件键下的“ParentIdPrefix”值,与最初取得的HKLM/SYSTEM/MountedDevices键下的“/DosDevices/U盘符:”值比较,如果相等,则说明此设备就是想找的U盘。

 

1.  LpUSBKeyData-> /??/STORAGE#RemovableMedia#7&1633246c&0&RM#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}

2.  截取后的LpUSBKeyData-> 7&1633246c&0

3.设备路径-> //?/usbstor#disk&ven_kingston&prod_datatraveler_2.0&rev_pmap#5b8504002c22&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}

4.截取后的设备路径-> usbstor#disk&ven_kingston&prod_datatraveler_2.0&rev_pmap#5b8504002c22&0

5.硬件键-> SYSTEM/CurrentControlSet/Enum/usbstor/disk&ven_kingston&prod_datatraveler_2.0&rev_pmap/5b8504002c22&0

6.硬件键下的ParentIdPrefix-> 7&1633246c&0

7lpPathTemp-> disk&ven_kingston&prod_datatraveler_2.0&rev_pmap#5b8504002c22&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}

8lpPos1-> 5b8504002c22&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}

9lpPos2-> #{53f56307-b6bf-11d0-94f2-00a0c91efb8b}

10.截取后的lpPathTemp-> 5b8504002c22&0

11ID-> 5B8504002C22

 

ID号藏于何处呢,原来包含在设备路径(来自SP_DEVICE_INTERFACE_DETAIL_DATA结构)里。第7步起即为从设备路径中取出ID的字符串处理过程。

这个例子之所以有价值,是因为它用设备路径构造出了硬件键——将设备路径截头去尾,替换掉#,再连上硬件键的固有开始部分。设备路径代表了什么呢,原来是一个符号链接:

 

接下来思考一下能否改进这个程序。大家可以看到,获取了设备路径后,不再使用SetupDixxx,操作从流水线转换到了手工作坊,代码也开始变得复杂起来,放弃的原因可能是SetupDiGetDeviceRegistryProperty无法获取ParentIdPrefix,而在用户模式下似乎也没有其他更好的标志了。有兴趣的读者可以资料610为线索,寻找是否存在更有效的方法。

 

五、Inf文件

资料1第十二章已经清晰的描述了Inf文件,笔者仅仅打算叙述一下测试示例驱动时遇到的问题。比较有意思的一个是,用DriverStudioEzDriverInstaller安装Inf,提示“…Inf找不到 [ClassInstall32] ”,这是因为缺少必需的设备创建类,虽然我们可以在Inf里补全该段,但既然示例驱动都使用了这个设备创建类,我们也可以编写一个文本格式的注册表文件setup.reg注册一下,内容为:

Windows Registry Editor Version 5.00

 

[HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Class/{894A7460-A033-11d2-821E-444553540000}]

@="WDM Book Samples"

"Class"="Sample"

"EnumPropPages32"="samclass.dll"

"Icon"="-5"

 

导入注册表后,再将samclass.dll复制到system32目录下即可。

另外一个问题是使用Inf安装了驱动后,运行测试程序会提示驱动启动失败,原因是示例驱动使用了generic.sys,将它复制到system2/driver下即可。

在前面的章节,我们已经接触了sourcesMOF,此次涉及了Inf,虽然语法各异,但对于我们来说,应该明确的是它们的出现是为了完整的表达自身模型的需要。对于资料1未详述的问题,可以查阅Msdn,如需要了解Inf中某节xxx的语法格式,可以输入Inf xxx来查询,又如文件复制、注册表配置的具体路径也可以查阅Msdn获知,再如Inf经常出现的点式名字,如DriverInstall.NTDriverInstall.NT.ServicesDriverInstall.nt.hw等,它们的区别在Msdn中也可以找到。

留给初学者的作业是完全理解显卡的Inf文件,找出显卡支持的分辨率声明位置,并尝试使用DDKGenInf编写一个简单的Inf

 

六、结语

注册表的许多操作都以API的形式被固定下来,虽然我们没有必要了解它们的内部实现,但有些特别重要的键仍然值得我们加以关注。Inf为驱动程序的正确安装与工作提供了很多有用的信息。硬件键名的形式,取决于不同类别设备的标识符的定义方式。

现在我们仍然不知道,在文件系统驱动加载之前,能否在内核模式下正常的访问注册表,你能够给出正确的答案来吗?

本篇的参考完成时间为不超过三星期。

 

 

文件系统驱动编程基础篇之六——DirectShow

一、     前略

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

参考资料*

1.Programming Microsoft DirectShow for Digital Video and Television》及其示例代码

2.DirectShow for DirectX 8.1 SDK C++

3. Microsoft® Windows® Software Development Kit Update for Windows Vista™ 之相关文档及其示例代码

4.DirectShow开发快速入门之慨述》

5.《深入解析ATL ATL Internals Second Edition ——Working with ATL 8)》

6.COM 组件设计与应用(一)起源及复合文件》(http://www.vckbase.com/document/viewdoc/?id=1483

 

阅读基础:少量的COM编程的基础知识,了解如何调用COM组件的方法,最好掌握哪怕一丁点的ATL

 

本章目的:了解DirectX组件架构,学习编写简单的DirectShow程序。

 

二、DirectXDirectShow

首先看下面两段关于DirectXDirectShow相关内容的摘要:

DirectX的第一个版本作为Windows Games SDK发布于19959月,它作为Windows API的一部分用以替换Windows 3.1中的DCIWinGAPIATI的一个开发团队为微软带来了基本的游戏影像技术,微软方面,DirectX由专门的团队负责开发,Eisler 为团队领导,而St. JohnEngstrom则成为主程序设计师。

20054月,DirectShowDirectX移除,加入到Microsoft Platform SDK

 

Windows系统的架构下,我们不能如Dos般随心所欲的控制硬件,而希望游戏、影像开发人员对内核的理解都达到驱动编程人员的水平,是一件苛求的事情,但是为此将失去广大市场的关键问题必然要产生解决的办法,因此,DirectX或其他不同名的相同技术的出现是件必然的。

有些读者可能会对驱动编程的基础文章涉及DirectX的必要性有所疑问,确实,DirectX并不是学习驱动编程的一个障碍,笔者在初次的学习中也从未意识过要了解这方面的内容。但是换一个角度,如果你看到鸡窝里的五个鸡蛋,我们是不是很有理由猜测这是由五只母鸡组成的五好家庭?因此我们可很牵强的认为DirectX如同内核的外部据点,如果我们拔除了,必然有更大的把握向内核发动总攻。

笔者把DirectX加入本系列文章的另一个重要原因是——DirectX确实有值得炫耀的资本。对于编程人员来说,它的COM接口简直可用“优雅”来评价,当你用着如苦瓜般的IMarshal接口时,不妨来看看什么样的接口值得这样的评价。我挑选了DirectX里一个很有意思的部分——DirecShow来作为突破口,讲述如何“Direct”——直接的控制视频设备。

学习的进阶大略上可分为四个部分:学习调用组件、捕获和编辑影像、学习编写过滤器、掌握媒体格式的高阶部分。限于能力,只介绍前两个部分。读者不要满足于学会使用DirctShow,而是通过DirctShow的具体功能猜测系统驱动的功能,甚至设备的构成,减轻将来主攻驱动时的负担,此即由外而内的学习方法。

笔者将掠人之美,使用参考资料1提供的示例阐明编程的基本思路。

 

三、DirectShow基础和GraphEdit

   Windows系统中,DirectShowCOM组件的形式存在,下图描绘了DirectShow组件、硬件以及系统组件的关系(Leagcy为传统或遗留,旧的之意)。

   

现在我们只需了解DirectShow里包含了两大类型的对象:三类“Filter”(源、转换、渲染过滤器),以及由这些过滤器集组成提供特定功能的“Filter Graph”,这个Graph不妨看成是包含Filter的一个容器。

为了从可视化的角度理解DirectShow工作的方式,我们可使用SDK附带的GraphEdit工具。它最基本的功能是利用系统里已经安装的解码器,使用DirectShow组件来播放媒体文件,并直观的显示播放流程。下面演示一下如何播放一个媒体文件。

GraphEdit的界面如图:

 

 

从菜单文件——渲染媒体文件里选择你要播放的文件,这里我们选择一个微软支持的avi格式文件ff8-full-ending.avi,则界面上出现:

    每一个DirectShow组件都以矩形框表示,InputOutput(pin)作为每个过滤器的输入或输出。通常情况下,源(左上角的ffi_full_ending.avi)、渲染过滤器(Video RendererDefault DirectSound Device)是必须的,转换过滤器根据实际情况可选。从菜单图表——插入过滤器可以看到所有可用的过滤器。

上图隐含了DirctShow的“智能选择”功能,即如果你提供了任何两个逻辑关联的渲染器(如MPC – Avi SplitterVideo Renderer),DirectShow会自动查找之间的可用组件,形成一个正常的播放流程。读者如果想真正掌握DirectShow,应该学会手工添加、连接合适的过滤器而不仅仅依赖于“智能选择”功能。

由图可知,avi文件被播放前,经过分离器的分离,形成视频和音频两个部分,它们再选择合适的解码器,最后分别送到系统默认的渲染过滤器上进行渲染,影像就显现于屏幕上了。

    点一下界面上绿色三角的播放键,则可以欣赏我们的影片了。

 

    这一切出乎意料的简单,这使我们不由产生了这么一个想法——DirctShow的初级应用也是非常简单的,事实正是如此。在进价的第一部分,我们学习如何通过调用组件,完成一个媒体文件的播放。

 

四、进价之一——组件的调用

本篇将要接触到下表组件的调用,详细的信息可参考Msdn上的说明,这些基础工作留给有心的读者来完成吧:

IID

CREATE CLSID

REMARK

IBaseFilter

CLSID_VideoMixingRenderer9,…

primary interface for DirectShow filters

ICreateDevEnum

CLSID_SystemDeviceEnum

creates an enumerator for a category of filters

IEnumMoniker

CLSID_AudioInputDeviceCategory

enumerate the components of a moniker or to enumerate the monikers in a table of monikers

IEnumPins

 

The filter graph manager uses this interface when it connects filters. Applications can use it to retrieve pins on a filter

IFileSinkFilter

 

write media streams to a file

IFilterGraph

 

provides methods for building a filter graph

IfilterGraph2

 

extends the IFilterGraph and IGraphBuilder interfaces, which contain methods for building filter graphs

IGraphBuilder

CLSID_FilterGraph

inherited from IfilterGraph

IMediaControl

 

provides methods for controlling the flow of data through the filter graph

IMediaEvent

 

contains methods for retrieving event notifications and for overriding the Filter Graph Manager's default handling of events

IMediaEventEx

 

 

IMediaSeeking

 

contains methods for seeking to a position within a stream, and for setting the playback rate.

IMoniker

 

contains methods that allow you to use a moniker object, which contains information that uniquely identifies a COM object

IPropertyBag

 

Provides an object with a property bag in which the object can save its properties persistently

IPersistStream

 

provides methods for saving and loading objects that use a simple serial stream for their storage needs

IPin

 

The filter graph manager uses this interface to connect pins and perform flushing operations

IPropertyBag

 

Provides an object with a property bag in which the object can save its properties persistently.

IStream

 

The IStream interface lets you read and write data to stream objects

IStorage

 

supports the creation and management of structured storage objects

IVMRFilterConfig9

 

configure the VMR's operating mode and video rendering mechanisms

IVMRMixerControl9

 

enables an application to manipulate the incoming video streams on the VMR-9

IVMRWindowlessControl9

 

controls how the VMR-9 renders a video stream within a container window

 

编程可任选MicrosoftCodeGear公司的编译器。如果使用vs,可以在微软站点下载并安装最新的SDK,如果使用C++Builder,可通过互联网下载DirectX 9 SDK for Borland C++ Builder。本篇以vs2005作为默认编译器,示例选用参考资料1DSRenderDSBuildPIP9

(一)   DSRender

DSRender模拟了GraphEdit打开一个媒体文件播放的行为,代码很简单,仅仅用到了IGraphBuilderIMediaControlIMediaEvent三个接口提供的方法,播放窗口为系统创建,我们不能设置到自定义窗口。

 

代码里值得一提的是保存上图配置的函数SaveGraphFile,涉及了IStorageIPersistStream两个接口,同时涉及了复合文件(Compound file storage object)这一个概念。复合文件仿佛一个包含若干文件、子文件夹的文件夹,是若干普通文件、子复合文件的组合。通常使用的ReadFileWriteFile函数以字节指针形式操纵单一文件,而复合文件里的文件却是以流对象(Stream Object)的形式存在,并强调了流对象的嵌套行为。从这种角度来看,前者象结构化编程的C,后者象面向对象编程的C++。可阅读Msdn里的相关文章更深入的理解复合文件。

 

(二)   DSBuild

DsBuild模拟了手工添加过滤器的行为。稍微有点意思的是GetPin函数,它通过枚举每个过滤器的针脚,检查并返回所查询的输入或输出针脚。过滤器间的针脚相连非常的简单,通过IGraphBuilder接口的Connect方法将上游过滤器的输出针脚和下游过滤器的输入针脚相连即可。

 

五、进价之二——捕获和编辑影像

李逵的三板斧现在也该抡出最后一板了,倒不是黔驴已经技穷(汗一下,是吗,有人这样说吗?),笔者相信这已经足够了,经过努力阅读参考资料的读者已经找到了前进的方向,本篇的内容虽然不多,却点到了DirectShow很多基础的知识,如果不把它们从薄读到厚,那么本文还有什么意义呢?

    这部分的内容可以细读参考资料1的第49章,料想考试时在试卷上写,“本题请老师参考课本第xxx页完成”必定落个红灯的下场,故笔者打算稍微分析一下第9章所介绍的杀手锏——用Video Mixing RendererVMR)来生成影像的画中画程序PIP9

    即使包括头文件,Pip9的总代码量也不过1500行,提示和空格真正体现了本站资源《华为编程规范和范例(PDF)》里对注释不少于20%的要求,如果使用可视化控件编程,代码量大约又可减少1/3,兼之很多接口在上两个程序中已经接触过,因此本节的重点放在了画中画的主线——VMR9的操作上。首先我们从整体上把握VMR

   

从上图看VMR过滤器类似于可以完成具体功能的集成电路,而不再是一个晶体管。它用于管理多重流媒体的渲染,在VMR9版本下,可以管理多达16个的输入管脚。

需要重点查看的函数为:

1BlendVideo : ConfigureMultiFileVMR9 -> AddGraphToRot

2ConfigureMultiFileVMR9 : InitializeWindowlessVMR -> RenderFileToVMR9

 

上述6个函数完成了画中画的播放效果,涉及的VMR接口为IVMRFilterConfig9IVMRMixerControl9IVMRWindowlessControl9。在这里,我们终于可以随心所欲的实现自定义的播放窗口了。

欣赏着稍带回音效果的影像,现在的你是不是觉得暴风影音、快乐影音这些功能强大的播放器不再神秘了呢?

    如果仔细观察,将发现在自定义窗口上播放的影像可以很轻易用HyperSnap截取下来,而在默认窗口播放的影像却无法用HyperSnap捕获。兴奋之余,你也许不再满足只拥有如此简单的功能了,你还想获得调整播放速度、进度的自由,播放更多类型的影像文件,那就让我们来修正代码吧。

 

六、示例代码的修正

代码的修正基于几种基本原因:编译器的改变、bug的改正以及功能上的完善。在本机上实践时,发现调整了头文件、库文件路径后,资料1提供的源代码仍不能通过编译,提示某些符号无法找到的错误,在cpp文件首部添加#pragma comment(lib, "strmiids.lib")后解决。

不要指望只有千行有效代码的播放器拥有多强大的功能,所以读者需要阅读相关接口的其他方法,添加常用的控制功能。眼疾手快的读者可能还将大声叫嚷Pip9不支持asfwmv格式,但SDK里已经提供了相应源代码,你还等什么呢。

 

七、结语

本文并非以介绍DirectShow的开发为目的,基本的想法是希望读者了解驱动编程被微软披上了多件外衣这一个事实,在前进的路途中,不要被这些美丽的衣裳所迷惑。现在的我们仍无法解释诸如过滤器究竟如何与驱动交互,如何完成自己的过滤器的问题,但从全局来说,这些问题是你以前根本没有意识到的,即问题的深度已经有了一定的提高。我们大可保持一段时间的迷茫,等到水到渠成的时候,你将有充分的理由相信自己不再是一个菜鸟。

本篇作为选学内容,参考完成时间为两星期。了解了COM重要性的读者,可再花费两个月时间来打下COM编程的基础。

 

 

文件系统驱动编程基础篇之七——端口读写

一、前略

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

笔者的实践环境为:

硬件:P35 Motherboard & ICH9 chipPentium Dual Cpu E2160 1.8g DDR2 1g

软件:Windows XP2VS 2005Visual AssistXDriverStudio 3.2MICROSOFT.WINDOWS.SERVER.V2003.IFS.DDKWindbg 6.8.0004.0,请安装好用于调试的虚拟机并配置好调试环境。

 

参考资料*

1.《Programming the Microsoft Windows driver model》第一版(当前阶段主要阅读资料,在先前的基础上,本次需要完成前八章)

2.《Windows NT File System Internals - A Developers Guide》(资料1理解后可阅读,为后续章节做准备)

3.《OSR White Papers

4、《Intel 64 and IA-32 Architectures Software Developer s Manual Volume 3A/B System Programming Guide

5WinXXX部分源代码

6.《Kernel Debugging with WinDbg》(随WinDbg软件附带的文档)

 

阅读基础:了解计算机结构体系、掌握用户模式下常用API调用、了解常用汇编指令。请从现在开始的一年时间里阅读50万字以上的驱动编程外文资料,彻底突破外文关。

 

本章目的:初步掌握硬件驱动编程的基础知识,学习使用Windbg调试。初次阅读代码量在一万行以上的驱动程序。

 

二、问题的提出

xp下直接读写端口会发生什么事情呢?一个简单的测试程序如下:

int main(int argc, char* argv[])

{

    char result;

    asm {

        mov al, 0x10

        out 0x70, al

        in al, 0x71

        mov result, al

    }

    printf("%X", result);

    return 0;

}

运行后发现程序弹出了异常,无法执行out指令,给出的提示为:

 

这是早已预料的结果。笔者搜索了一下,找到了《Direct Port IO and Windows NT》,欣欣然之下禁不住想让大家知道原来“不需要”写驱动也可以访问端口了。它究竟是何方神圣?原来这是一种称之为iopm的方法,它涉及了 CPU硬件理论里关于TS段的基础知识:

通过修改I/O映射表,用户模式下的进程就具有了存取端口的权限。问题是如何修改I/O映射表?阅读了porttalkwinio的源代码后,发现所谓的iopm还是需要通过驱动代码修改映射表,此外你还需要修改当前进程的映射表偏移地址。当我们费力的写好近千行代码,换来的好处就是真的可以在用户模式下以较快的速度存取端口了。欣欣然的你不妨用文初给出的测试程序来检查一下成果:

 

如果把驱动编程想得这么的简单,那你就有些过于乐观了。现在尝试另一个端口,比如串口的0x3f8,看看有什么奇怪的事情发生?这次读出了0xff,那么0x3f9呢,还是0xff,一直读完串口的所有端口,结果都是0xff。假如我们首先用CreateFile来打开串口,怎么读出的值不再是0xff了,如果关闭了串口再来读一读,结果又是0xff了。Googlebaidu后,发现串口原理的资料很少,换个思路来查,查询串口的主要芯片——从8250一直搜索到16550A,这下果然搜出了有价值的资料,如《Interfacing the Serial RS232 Port》,《串行输入输出接口》,《常用的输入输出接口芯片》,《UART 内部寄存器对应端口及用途》等。但这些资料只说明了一个问题——很久很久以前,在DOS下是如何发送串口指令的——却还是没有解决Windows下的问题。

让我们再换一下思路。从DevView里看到串口驱动的基本情况,原来串口的PDOACPI

 

阅读《Advanced Configuration and Power Interface Specification》,可知ACPI是微软和几个厂家制定的协议,不由得想起使用WINPE启动的时候选择了ACPI,结果机子一次次的重启,本杂牌机无论从外观还是认证标签上看极象兼容ACPI,但假货毕竟是假货。折腾了半天后,因我们的目的不在于如何写ACPI驱动,故这个问题的答案即使和它有关,也请暂时绕过(实践会证明,很多问题都是冤家路窄,避是避不过的^_^。对ACPI调用IoCallDriver后,激活了串口)。

 

三、串口代码之实践

本篇最终目的是阅读并调试串口程序,因笔者使用的DDK版本是3790的,源代码位在WINDDK/3790/src/kernel/serial下。首先我们要做的是完善串口的调试输出信息函数,修改位于log.c下的SerialDbgPrintEx如下:

ULONG

SerialDbgPrintEx(IN ULONG Level, PCHAR Format, ...) // 级别,格式,不定的参数

{

         va_list arglist;

         ULONG rval;

         ULONG Mask = 0;

         ULONG cb;

         UCHAR buffer[SERIAL_DBGPRINT_BUFSIZE];

 

         RTL_QUERY_REGISTRY_TABLE paramTable[4];

         ULONG   zero            = 0;

 

         if (KeGetCurrentIrql() == PASSIVE_LEVEL)

         {

            RtlZeroMemory (&paramTable[0], sizeof(paramTable));

            paramTable[0].Flags         = RTL_QUERY_REGISTRY_DIRECT;

            paramTable[0].Name          = L"SerialDebugLevelBegin";

            paramTable[0].EntryContext  = &SerialDebugLevel; // 保存查询结果

            paramTable[0].DefaultType   = REG_DWORD;

            paramTable[0].DefaultData   = &zero;

            paramTable[0].DefaultLength = sizeof(ULONG);

 

            paramTable[1].Flags         = RTL_QUERY_REGISTRY_DIRECT;

            paramTable[1].Name          = L"SerialDebugLevelEnd";

            paramTable[1].EntryContext  = &SerialDebugLevelEnd; // 保存查询结果

            paramTable[1].DefaultType   = REG_DWORD;

            paramTable[1].DefaultData   = &zero;

            paramTable[1].DefaultLength = sizeof(ULONG);

 

            paramTable[2].Flags         = RTL_QUERY_REGISTRY_DIRECT;

            paramTable[2].Name          = L"SerialDebugLevelCloseHigh";

            paramTable[2].EntryContext  = &SerialDebugLevelCloseHigh; // 保存查询结果

            paramTable[2].DefaultType   = REG_DWORD;

            paramTable[2].DefaultData   = &zero;

            paramTable[2].DefaultLength = sizeof(ULONG);

 

            RtlQueryRegistryValues( RTL_REGISTRY_ABSOLUTE | RTL_REGISTRY_OPTIONAL,

                      SerialGlobals.RegistryPath.Buffer, // HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/Serial

                      &paramTable[0], // 以全0的结尾表结束,这样一次就可以查询多个值了

                      NULL,

                      NULL);

                   Mask = Level;

         }

         else if (SerialDebugLevelCloseHigh == 1) // 不显示高irql信息

         {

                   return STATUS_SUCCESS;

         }

 

         if (Mask < SerialDebugLevel || Mask > SerialDebugLevelEnd) {

            return STATUS_SUCCESS;

         }

 

         va_start(arglist, Format); // arglist越过了格式串,指向了参数

 

         cb = _vsnprintf(buffer, sizeof(buffer), Format, arglist);

 

         if (cb == -1) { // 串长于buffer

           buffer[sizeof(buffer) - 2] = '/n';

         }

 

         DbgPrint("IRQL %d, SERIAL: %s", KeGetCurrentIrql(), buffer);

 

         //rval = vDbgPrintEx(DPFLTR_SERIAL_ID, Level, Format, arglist);

 

         va_end(arglist);

 

         rval = STATUS_SUCCESS;

 

         return rval;

}

 

往注册表里添加SerialDebugLevelBeginSerialDebugLevelEndSerialDebugLevelCloseHigh三键,我们可以更灵活的动态控制调试信息。

 

编译好Checked模式的串口驱动程序后,接下来需要替换掉系统自带的串口驱动WINDOWS/system32/drivers/ serial.sys(大小为59KB)。希望你首先按照自己的思路来尝试替换,如果失败了再来尝试如下的方法:

 

1、  关闭系统还原

2、  改名WINDOWS/system32/dllcache/serial.sys

3、  WINDOWS/system32/CatRoot目录改名为其他名字

4、  将你编译好的checked模式的串口驱动程序覆盖WINDOWS/system32/drivers/serial.sys

5、  在硬件管理器里删除掉串口驱动,再重新扫描硬件变更,重新安装驱动时可能出现“是否替换”的提示,选择否,直到你发现驱动目录下的串口驱动程序确实是你自己编译过的程序

6、  运行Dbgview后在硬件管理器里停止串口驱动,再启动驱动,此时可以看见在Dbgview里看见调试信息(前提是注册表已设好上述三键的值)

 

为了方便阅读代码,简单说明一下串口寄存器。端口0x3f8-0x3fe 用于微机上COM1 串行口,0x2f8-0x2fe 对应COM2 端口。DLAB(Divisor Latch Access Bit)除数锁存访问位,是指线路控制寄存器的最高位7

 

UART 内部寄存器对应端口及用途

序号

端口                  

/

条件

用途

0

0x3f8 (0x2f8)

DLAB=0

发送保持寄存器。含有将发送的字符。TRANSMIT_HOLDING_REGISTER

 

 

DLAB=0

接收缓存寄存器。含有收到的字符。RECEIVE_BUFFER_REGISTER

 

 

/

DLAB=1

波特率因子低字节(LSB)。DIVISOR_LATCH_LSB

1

0x3f9 (0x2f9)

/

DLAB=1

波特率因子高字节(MSB)。DIVISOR_LATCH_MSB

 

 

/

DLAB=0

中断允许寄存器。INTERRUPT_ENABLE_REGISTER

 

 

 

 

7-4 0 保留不用;

 

 

 

 

3=1 modem 状态中断允许;

 

 

 

 

2=1 接收器线路状态中断允许;

 

 

 

 

1=1 发送保持寄存器空中断允许;

 

 

 

 

0=1 已接收到数据中断允许。

2

0x3fa (0x2fa)

 

中断标识寄存器。中断处理程序用以判断此次中断是4种中的那一种。INTERRUPT_IDENT_REGISTER

 

 

 

 

7-3 08250不用,165507-6=11 允许fifo =00 不允许fifo);

 

 

 

 

2-1 确定中断的优先级;

 

 

 

 

= 11 接收状态有错中断,优先级最高;

 

 

 

 

= 10 已接收到数据中断,优先级第2

 

 

 

 

= 01 发送保持寄存器空中断,优先级第3

 

 

 

 

= 00 modem 状态改变中断,优先级第4

 

 

 

 

0=0 有待处理中断;=1 无中断。

3

0x3fb (0x2fb)

 

线路控制寄存器LINE_CONTROL_REGISTER

 

 

 

 

7=1 除数锁存访问位(DLAB) =0 访问发送保持寄存器、接收缓存寄存器器或中断允许寄存器;

 

 

 

 

6=1 强迫SOUT送出空闲状态;0 禁止间断;

 

 

 

 

保持奇偶位 5=1 偶校验时,校验位=0 奇校验时,校验位=10 无数

 

 

 

 

43=11 偶校验;=01 奇校验;x0 不加校验位

 

 

 

 

2=1 5位数据位时)1.5位停止位,(678位数据位时)2位停止位;=0 1位停止位;

 

 

 

 

1-0 数据位长度:= 00 5位数据位;= 01 6位数据位;= 10 7位数据位;= 11 8位数据位。

4

0x3fc (0x2fc)

 

modem 控制寄存器。MODEM_CONTROL_REGISTER

 

 

 

 

7-5 0 保留;

 

 

 

 

4=1 芯片处于循环反馈诊断操作模式;(自测试循环)SERIAL_MCR_LOOP

 

 

 

 

3=1 辅助用户指定输出2,允许INTRPT 到系统;SERIAL_MCR_OUT2

 

 

 

 

2=1 辅助用户指定输出1PC 机未用;SERIAL_MCR_OUT1

 

 

 

 

1=1 使请求发送RTS 有效;SERIAL_MCR_RTS

 

 

 

 

0=1 使数据终端就绪DTR 有效。SERIAL_MCR_DTR

5

0x3fd (0x2fd)

 

线路状态寄存器LINE_STATUS_REGISTER

 

 

 

 

7=0 保留;

 

 

 

 

6=1 发送移位寄存器为空;

 

 

 

 

5=1 发送保持寄存器为空,可以取字符发送;

 

 

 

 

4=1 接收到满足间断条件的位序列;

 

 

 

 

3=1 帧格式错误;

 

 

 

 

2=1 奇偶校验错误;

 

 

 

 

1=1 超越覆盖错误;

 

 

 

 

0=1 接收器数据准备好,系统可读取。

6

0x3fe (0x2fe)

 

modem 状态寄存器。δ 表示信号发生变化。MODEM_STATUS_REGISTER

 

 

 

 

7=1 载波检测(CD)有效;

 

 

 

 

6=1 响铃指示(RI)有效;

 

 

 

 

5=1 数据设备就绪(DSR)有效;

 

 

 

 

4=1 清除发送(CTS)有效;

 

 

 

 

3=1 检测到δ 载波;

 

 

 

 

2=1 检测到响铃信号边沿;

 

 

 

 

1=1 δ 数据设备就绪(DSR)

 

 

 

 

0=1 δ 清除发送(CTS)

#define RECEIVE_BUFFER_REGISTER    ((ULONG)((0x00)*SERIAL_REGISTER_STRIDE))

#define TRANSMIT_HOLDING_REGISTER  ((ULONG)((0x00)*SERIAL_REGISTER_STRIDE))

#define INTERRUPT_ENABLE_REGISTER  ((ULONG)((0x01)*SERIAL_REGISTER_STRIDE))

#define INTERRUPT_IDENT_REGISTER   ((ULONG)((0x02)*SERIAL_REGISTER_STRIDE))

#define FIFO_CONTROL_REGISTER      ((ULONG)((0x02)*SERIAL_REGISTER_STRIDE))

#define LINE_CONTROL_REGISTER      ((ULONG)((0x03)*SERIAL_REGISTER_STRIDE))

#define MODEM_CONTROL_REGISTER     ((ULONG)((0x04)*SERIAL_REGISTER_STRIDE))

#define LINE_STATUS_REGISTER       ((ULONG)((0x05)*SERIAL_REGISTER_STRIDE))

#define MODEM_STATUS_REGISTER      ((ULONG)((0x06)*SERIAL_REGISTER_STRIDE))

 

前几章中涉及的代码量不过千行左右,此次阅读的个别子例程代码量已达到上千行,所以要特别注意阅读的技巧。首先大略了解各文件的基本功能,从DriverEntry起,分步阅读各个派遣例程,适时记录流程中涉及的关键函数,至少读懂全部代码的45%并攻克本文提出的问题。

如果你已经开始厌烦没完没了的书写重复的代码,那么你就应该有所理解为何微软在KMDF模型下提供现成的PNP和电源管理代码的原因了。

 

四、结语

如果不理解何为“浮光掠影”,阅读了本文后就应该有原来如此的感觉了。

我们的目的是文件系统驱动编程,为何要涉及硬件驱动编程的学习?原因很简单,硬件驱动编程和文件系统编程在出现分叉点之前的知识是通用的,必须扎实的掌握。我们在篇二时已经实现了一次三级跳,今天这次跳跃将使你的水平更加接近文件系统编程所需要的层次。

至此驱动编程的基础知识都已有所接触,如果你对资料1的前八章知识仍感到困难,不妨放松心情,休息几天,找出原因后继续前进。我希望大家完成本篇的时候,时间应控制在三个半月以下(应该算是严格的要求,但我相信大家的智慧)。接下来我们将使用三个月的时间学习文件系统的基本知识,并投入六个月以上的时间进行实践。希望一年之后,您有脱胎换骨的感觉。

 

 

文件系统驱动编程基础篇之八——安全机制

一、前略

本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

 

参考资料*

1.Programming Windows Security》(中文版为《Windows安全性编程》)

2.Microsoft® Windows® Internals, Fourth Edition》(中文版为《深入解析Windows操作系统》)第八章

3.《深入研究Windows内部原理系列之九:Windows的安全机制和实现》(该系列视频难度不大,但有着提纲挈领的作用)

4.Programming Microsoft Windows 2000 Unleashed》(中文版为《Windows2000编程技术内幕》)第七章

5.Programming Applications for Microsoft Windows》(中文版为《Windows核心编程》)3.1.2

6.Writing Secure Code

7.Microsoft Windows Security Inside Out for Windows XP and Windows 2000》(中文版为《精通Microsoft Windows 2000Windows XP安全技术》)

8.Creating a Security Descriptor for a New Object in C++

9.Enabling and Disabling Privileges in C++

10.Converting a Binary SID to String Format in C++

11.Windows 操作系统中的常见安全标识符

12.威胁和对策

13. WRK1.2Windows操作系统的教学简化版本)

14. NTFS-3G Stable Read/Write Driver

 

阅读基础:不限。

 

本章目的:了解系统的安全体系、基本的数据结构和安全函数。

 

二、用户模式下的安全结构

请先观看资料3的视频,了解Windows系统安全体系的基本框架,接下来我们为资料4理清脉络:

 

一)安全描述符

CreateFileCreateProcessRegCreateKeyEx这些常用的函数都有一个安全属性参数,安全属性主要包含着对象的安全描述符。这里需要强调的是安全描述符是针对对象而言的,用于描述对象的安全性信息,它是我们接触到的第一支主线的纲。下图表达了安全描述符和各子结构的关系,读者不要立即陷入结构的细节中去,可暂时把每个结构都当成一个黑盒来看待。

 

privilege特权,对系统相关操作而言的能力,比如是否允许关机、加载设备驱动、修改系统时间等,包含在访问令牌中

The right of a user to perform various system-related operations, such as shutting down the system, loading device drivers, or changing the system time. A user's access token contains a list of the privileges held by either the user or the user's groups.

access right 访问权力,对对象而言的,和特权的区别需要实践来体会

A permission granted to a process to manipulate a specified object in a particular way (by calling a system service). Different system object types support different access rights, which are stored in an object's access control list (ACL).

security identifier 安全标识符

(SID) A data structure of variable length that identifies user, group, and computer accounts. Every account on a network is issued a unique SID when the account is first created. Internal processes in Windows refer to an account's SID rather than the account's user or group name.

access control entry (ACE) acl分为sacldaclacl可包含着多个aceace描述了特定sid拥有的访问权力)

An individual entry in an access control list (ACL). An access control entry (ACE) contains an SID and describes the access rights to a system resource by a specific user or group of users. Each object has a set of all ACEs, which is used to determine whether an access request to the object is granted.

discretionary access control list 用来确定谁可以访问对象,谁不能访问对象,详看资料25小节

(DACL) An access control list that is controlled by the owner of an object and that specifies the access particular users or groups can have to the object.

system access control list 跟系统资源有关,详看资料27小节

(SACL) An ACL that controls the generation of audit messages for attempts to access a securable object. The ability to get or set an object's SACL is controlled by a privilege typically held only by system administrators.

trustee 受托人

The TRUSTEE structure identifies the user account, group account, or logon session to which an access control entry (ACE) applies. The structure can use a name or a security identifier (SID) to identify the trustee.

 

ACE类型:

Attribute

Description

ACCESS_ALLOWED_ACE

Grants specified rights to a user or group. This ACE is stored in a discretionary ACL (DACL).

ACCESS_DENIED_ACE

Denies specified rights to a user or group. This ACE is stored in a DACL.

SYSTEM_AUDIT_ACE

Specifies what types of access will cause system-level audits. This ACE is stored in a system ACL (SACL).

 

typedef struct _SECURITY_DESCRIPTOR {

  PSID  Owner; // 个体,注意这些都是指针,而不是结构

  PSID  Group; // 所属组

  PACL  Sacl;

  PACL  Dacl;

} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;

 

typedef struct _SECURITY_ATTRIBUTES {
  DWORD nLength;
  LPVOID lpSecurityDescriptor; // 指向安全描述符

} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

 

二)令牌

令牌(Token)是第二支主线的纲。在用户登录系统并完成密码的验证后,系统就为该用户创建了一个访问令牌(access token)。

 

 

访问令牌分为两种:主令牌(primary token)和模拟令牌(impersonation token)。访问令牌包含了SID,用户所属组的ID,用户权力(right)和用户特权(privilege)等安全信息。

每个进程都有这个用户访问令牌的副本,每个线程也可以有自己的访问令牌,但如果没有的话,它将使用进程的访问令牌。

当线程需要访问一个对象时,系统默认使用主令牌。如果某线程——如服务线程——模拟了客户来访问对象时,系统将为该服务线程创建模拟令牌,模拟令牌保存着客户的安全信息,服务线程用模拟令牌来访问安全对象。因此,模拟了客户的服务线程将同时拥有主令牌和模拟令牌。

打开进程的令牌的招牌动作:

HANDLE hToken;

 

OpenProcessToken(GetCurrentProcess(),

                             TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,

                             &hToken);

 

三)ACTRL_ACCESS

 

ACTRL_ACCESS用于设置对象和其属性的ACL列表。但笔者建议在熟练掌握了前两个内容后再考虑是否有必要学习这个结构。

 

四)SID

SID是一个可变长的数据结构,用来标识用户或组。

SID的典型字符串表示形式为“S-R-I-S”,以字母S打头,R表示修订号,现在都是1I表示授权(Authority6bytes),S表示子授权(SubAuthority),因为子授权个数不定,所以整个SID的长度是可变的,结构里使用了“子授权数”域来表示SID包含了多少个子授权。

知名SIDwell-known SIDs)是一组系统已经预定了的常用SID,如EveryoneS-1-1-0),AdministratorsS-1-5-32-544SID等,详见资料11。创建知名SID可用CreateWellKnownSid函数,示例参见Msdn

 

资料3关于安全结构的主要内容已经介绍完毕,请读者在资料8的示例基础上总结出使用安全描述符的一般步骤(注意它并没有直接操作ace,而是通过trustee,同时注意ace的继承性),以资料9的示例学习如何调整令牌里的特权,以资料4的示例学习如何使用模拟令牌。

 

三、内核模式下的安全

请先阅读资料2,这些论述来自于Windows源代码和文件系统驱动代码的总结,完全建立在实践的基础上。读者如果无法理解这些内容,也完全没有必要强记,因为很幸运,这些代码已经不是秘密了,你所需要的仅仅是将自身的能力提高到能够理解它们的水平。

用户模式下的安全归根结底都要表现为内核模式下的安全控制,如DACL的本质就是对object的访问控制。下两图分别表示了对象访问以及系统审核策略(audit policy)的处理流程。

 

 

 

安全描述符仍然出现在内核模式下(没有白学呢^_^),除此之外,也出现了安全上下文这个术语,今后在编程实践中,将可能碰到IO_SECURITY_CONTEXTSECURITY_SUBJECT_CONTEXT这些结构。

security context

The security attributes or rules that are currently in effect. For example, the current user logged on to the computer or the personal identification number entered by the smart card user. For SSPI, a security context is an opaque data structure that contains security data relevant to a connection, such as a session key or an indication of the duration of the session.

 

文件系统驱动对IRPIO堆栈传递进来的参数,也要进行安全性的检测,此时会调用如SeAccessCheck的一系列内核安全函数。

四、结语

至此基础篇就全部结束了,原计划中的文件分配表等内容都放弃了,被忽略了的同步和链表技术虽然颇为重要,但参考资料已经写得足够清晰,关键在于实践中的掌握。内核中与文件系统有着密切联系的几大系统组件——I/OMemoryCache管理器等,将在今后的文章中重点讲解。

今天我们第一次提及了WRK,它是微软为高等院校讲授操作系统理论而提供的简化Windows内核,而NTFS-3G是为非Windows操作系统实现读写NTFS格式的开源驱动,以后我们或许将提及它们的一些细节。

网游,炒股,泡MM,除了这些事情,工作之余的你是否思考过如何使自己的生活变得更有意义,更加充实?

 

 

 

 

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值