Electronic Joint Business

Solution for E-Business

IRP

IRP 操作详解

每个操作系统都需要一个显性或隐性的 I/O 模型来处理进出外设的数据流。而 Windows I/O 模型的特性之一就是支持异步 I/O,其它一些特点包括:1

  • I/O 管理器为所有内核驱动提供了一致的接口,无论是底层驱动,中间层驱动或者文件系统驱动。所有请求都以 IRP (I/O 请求包)的形式发送给驱动。
  • I/O 操作是层叠的。I/O 管理器提供 I/O 系统服务供用户模式下子系统调用来执行 I/O 操作。I/O 管理器负责解释这些调用,并创建一个或多个 IRP,然后将它们路由给堆叠的驱动层层传递到物理设备。
  • I/O 管理器定义了一整套标准例程,其中一些要求驱动必须支持,另一些则是可选的。所有驱动遵循相对一致的实现模型,并根据不同的外设而有所差异,另外总线驱动、功能驱动、过滤驱动和文件系统驱动等也有不同的功能要求。
  • 正如 Windows 操作系统一样,驱动也是基于对象的。驱动、设备和物理硬件都被表示为对象。I/O 管理器和其他系统组件提供了内核态的支持例程,驱动通过例程调用来操作相关对象使得工作得以完成。

除了用来传递传统的 IO 请求外,I/O 管理器也可以和PnP 管理器、电源管理器一起发送包含 PnP 和电源请求的 IRP。

用户的 I/O 请求和文件对象
由于内核驱动被环境子系统所隐藏,因此用户是不可见的,这些子系统实现了一些大家熟悉的编程接口,比如 Windows 或 POSIX。对用户态代码(包括子系统)来说,可见的只是 I/O 管理器所控制的命名文件对象(named file object) 。

图1-1 显示了用户、子系统和 I/O 管理器的关系。

诸如 Win32 子系统这样的受保护子系统,将 I/O 请求通过 I/O 系统服务传递给相应的内核驱动。上图所示的子系统依赖于显示、显卡适配器、键盘和鼠标设备驱动的支持。

>>> 阅读全文

 

, ,

IRP

在 Windows NT内核的 IO 模型中,有两类 IRP:threaded irp和non-threaded irp,顾名思义,前者跟thread绑定,后者跟thread无关。当一个threaded irp被创建时,创建线程会有一个队列保存该irp,直到irp完成之后才释放。当你试图让这条线程退出时,系统会检测队列看里面是否还有irp没完成,如果有,线程会一直等待,直到所有的irp全部完成。而non-thread irp则正好相反,如果该irp已经返回到了创建它的地方你还继续complete它,BSOD将会发生。1

Threaded IRP
如前面所讲,threaded irp和线程绑定在一起。当user mode程序发起IO操作(比如WriteFile函数),或者一个驱动向另一个驱动发起IO操作(比如ZwWriteFile),IOManager首先找到文件handle对应的驱动栈,获取栈顶的PDEVICE_OBJECT,生成一个irp,将IO操作的各种信息放入irp中,并将此irp放入线程的irp队列中,然后调用IoCallDriver将irp转给驱动栈一层层处理,每往下一层,IO_STACK_LOCATION中的指针就会往下移一个位置(实际情况比这稍微复杂一点点,以后再说,现在先把它想成是一个栈,每往下一层就是push一次),直到最后跟硬件打完交道返回。返回的过程正好与上述相反。最末一层处理完irp后,会调用IoCompleteRequest,此函数会将IO_STACK_LOCATION指针往上移(pop),并最后调用上层驱动设置的CompletRoutine。每层的CompleteRoutine做完相应工作后都会调用IoCompleteRequest讲irp继续往上转,直到栈顶PDEVICE_OBJECT。此时IO_STACK_LOCATION已经在顶部,所谓再上层的CompleteRoutine也不存在,所以本层CompleteRoutine中操作将irp归还给IOManager,IOManager负责回收资源,并从线程irp队列中去除相应的项。

这个过程和函数调用很像,像到你都会发问:为什么搞成这种模式,函数调用和返回不也有压栈出栈的操作吗,干嘛不借用这套机制反而要另起炉灶单独搞一套。答案很简单,就是效率。nt内核的IO操作全是异步的,调用IoCallDriver的线程和CompleteRoutine响应到的线程,不一定是同一条。关于这点我们以后再谈,但它却引出了一个目前我们就需要关心的问题:既然CompleteRotine的线程环境可能已经切换了,那么IOManager如何知道从哪个线程的irp队列里去除相应项?答案也很简单,就是保存在irp中。每一个threaded irp的PIRP->Thread域都保存它的创建线程,IOManager就是用它来寻找线程。注意:有人会想从PIRP->Thread里获得当前线程的信息,这是不对的。

Non-threaded irp
non-threaded irp一般都由驱动创建而不是IOManager,线程不在irp队列里保存它的实例,并且它的PIRP->Thread为NULL。当CompleteRoutine一路返回调到该irp的创建者时,千万不要返还给IOManager,直接free掉就可以了。

irp创建函数
从user mode一路下来的irp一定是threaded irp这很明显。那么内核态的函数中,哪些是创建threaded irp,哪些是创建non-threaed irp的?以下表格列出了各函数:

>>> 阅读全文

 

, , ,

Windows 驱动开发(二)实现 IOCTLs

这是关于编写设备驱动程序系列教程的第二篇。我们会从上一篇谈到的东西继续下去。这一系列文章的主旨一点点打造编写设备驱动程序所需的知识。我们会利用上一篇文章的代码进行修改构建。在本文中,我们扩展这些代码以提供包括读取功能,处理输入/输出控制(也称为 IOCTL)并且更多地了解 IRP。

在开始之前,我先澄清一些常见问题。

哪里可以下载 DDK 或 WDK?
每一版 Windows 的发布,微软会同步推出驱动程序工具包,目前最新的是 WDK8,可以用来为 Vista, Windows 7 和 Windows 8 开发驱动。你可以在微软站点上下载该工具包。

我可以在驱动中使用 windows.h 头文件吗?
不要混用 Windows SDK 与 Windows DDK 的头文件。定义冲突会让代码编译变得十分困难。如果用户模式程序用到部分 DDK 的内容,通常我们会将需要的 DDK 或 SDK 的定义抽取出来放到源代码中。你还可以将 DDK 和 SDK 相关的文件隔离开来,每一个 .C 文件都可以包含正确的头文件而不会冲突。

可以用同样的方式实现某种特定类型的驱动吗?
这里谈的是适用大多数 Windows 驱动程序开发的通用框架。驱动程序不需要紧贴着硬件实现,而是通常有一个驱动程序栈。如果想实现某种特定类型的驱动程序,本文是了解驱动程序通常是如何工作的起点。后续的不同之处在于如何向系统宣告设别、 要实现哪些 IOCTL、 需要和哪些下层驱动进行通讯、以及其他一些要求实现的部分如配套驱动或用户模式组件。具体实现某种特定类型的驱动时,你应该在 MSDN 、DDK 多阅读一些与该类型驱动相关的内容。有些框架封装了我们在这里谈到的大部分内容,利用这些框架写起例子来会更简单。

>>> 阅读全文

 

, , ,

Windows 驱动开发 (一) 什么是驱动?

文章评价:
本系列文章将从头开始教你如何编写简单的 Windows NT 设备驱动程序。的确,你可以在互联网上找到各种有关编写设备驱动程序的资源和教程,不过,其中大部分都是编写 “Hello,World” 之类的入门程序,这也导致了在真正编写设备驱动程序时,很难搜索到有用的信息。也许你手边也有些教程,为啥你还需要阅读本文呢?我个人认为,信息越多总是越好,尤其对于那些第一次接触到的概念。能从多角度来观察同一个事物总是会带来新的观点。不同的人会用不同的方式写作,并从自己的角度描述信息,这取决于作者熟悉哪一方面或他觉得哪一方面值得说明。这种情况下,我个人建议任何想要编写设备驱动程序的人不要拘泥于本文或者其他文章。你应该多看看各种的示例和代码段,并研究其中的差异。

本教程会介绍如何创建一个简单的设备驱动,然后动态加载/卸载这个驱动,最后我们会涉及如何在用户模式下与驱动进行通讯。

创建简单的设备驱动

首先我们先来了解什么是子系统。

什么是子系统?
在解释如何编写设备驱动前,我会引入一些基础知识。我们将从编译器开始。编译器和链接器会按操作系统可以理解的格式生成二进制文件。在 Windows 中,这种格式称为 “PE”,即”可移植的可执行文件- Portable Executable”格式。在该格式中,有个称为子系统的概念。子系统 和 PE 头部信息中的其他选项一起描述了如何加载可执行文件,其中也包括了二进制文件的入口点的信息。(注:在 PE 可选头部中包含了很多关于可执行映像的重要信息,例如初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等等。)

>>> 阅读全文

 

, , ,