设备对象栈。IRP请求一般会被传送到设备栈的最顶层设备对象,顶层设备对象可以选择直接结束IRP
请求,也可以选择将IRP请求向下层设备转发。如果是向下层设备转发,当IRP请求结束时,IRP会顺着
设备栈的反方向原路返回。当得知下层驱动程序已经结束IRP请求时,本层设备对象可以选择继续向上
返回,也可以重新选择将IRP再次传递给底层设备驱动。
两层设备之间通常用IRP传递请求。IRP由I/O管理器创建发出,I/O管理器是用户态与内核态之间的桥梁,
I/O管理器捕获用户态请求,将其转换为IRP请求,发送给驱动程序。
以CreateFile为例来理解(仅供参考),假设要打开一个文件"c:\filiename"这个名字在用户态下有意义,
它有一个symbol link(符号链接)"\??\filiename”,在内核模式下它的真实名字为"\Device\HarddiskVolume1\filiename"
用户态函数只能通过符号链接来打开某个设备,I/O管理器捕获到这个请求,调用对象管理器(Object Manager)
查找到文件的符号链接名(对象管理器中会得知用户态中c:对应的设备对象为\Device\HarddiskVolume1),
同时调用安全引用监视器检查该子系统是否有打开设备对象的权限。获取符号链接后,调用ZwCreateFile
完成打开设备的工作,并且得到一个文件句柄。ZwCreateFile通过系统服务调用内核函数NtCreateFile.在
NtCreateFile执行过程中,执行到nt!IoPorseDevice中调用t!IoGetAttachedDevice,通过PDO获得键盘设备栈最顶端的
设备对象,用得到的这个设备对象的StackSize(IO_STACK_LOCATION的大小)成员作为参数来调用函数
IoAllocateIrp创建IRP,调用nt!ObCreateObject创建文件对象,并初始化这个文件对象。调用nt!IoCallDriver将IRP发往驱动。
驱动处理完这个 IRP 之后,返回 nt!IopParseDevice 继续执行。
初步了解了IRP的创建,我们来看一下这个IRP的结构。IRP包括两个部分,首部和辅助。首部包括指向IRP输入输出
缓冲区的指针,当前拥有IRP驱动的指针,IO_STACK_LOCATION的数组索引,同时也有一个指向该IO_STACK_LOCATION
的指针。索引是从1开始,没有0。紧接首部的是一个IO_STACK_LOCATION(I/O堆栈单元)数组,它的大小由设备栈中
设备数决定。IO_STACK_LOCATION的每个元素和每个设备一一对应。
IRP的结构的大小是不固定的,大体结构如下:
--------------------
| IRP header |
--------------------
|IO_STACK_LOCATION |<-----lowest driver stack location #index1
--------------------
|IO_STACK_LOCATION |<-----next higher stack location #index2
--------------------
......
|IO_STACK_LOCATION |<-----topmost driver stack location #indexn //类似栈,遵循先进后出
设备对象收到IRP请求通过DriverObject成员找到驱动程序,驱动程序访问它对应的设备对象在IO_STACK_LOCATION
数组中元素检查参数,以决定如何操作(如MajorFunction对应的派遣函数)
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;//该字段定义了一个函数功能集,对应派遣函数
UCHAR MinorFunction;//MinorFunction提供了MajorFunction更详细的信息
UCHAR Flags; //Flags提供了函数期望驱动执行的附加信息
CHAR Control; //当内核驱动异步处理IRP时,驱动可以通过调用IoMarkIrpPending()将IRP标记为Pending状态,以排队IRP处理
union Parameters;//是几个子结构的联合,每个请求类型都有自己专用的参数,而每个子结构就是一种参数
PDEVICE_OBJECT DeviceObject;//与该堆栈单元对应的设备对象地址,由IoCallDriver函数填写
PFILE_OBJECT FileObject; //内核文件对象地址
PIO_COMPLETION_ROUTINE ComPletionRoutine; //IO完成例程地址,由与这个堆栈单元对应的驱动程序的上一层驱动设置
PVOID Context; //作为参数传递给完成例程
}IO_STACK_LOCATION
我们再来看IRP在分层驱动中的传递。当驱动程序准备向次低层驱动程序传递IRP时可以调用IoCallDriver例程,
它其中的一个工作是递减当前IO_STACK_LOCATION的索引,使之与下一层的驱动程序匹配。但该索引不会设置成0
,如果设置成0,系统将会崩溃。就是说,最底层的驱动程序不会调用IoCallDriver例程。
假设有一个请求,I/O管理器为这个请求创建了IRP并分配了4个IO_STACK_LOCATION 单元。
NT I/O管理器将IRP头中的StackCount字段初始化为IRP中Stack Location的总数。IRP头中的
CurrentLocation字段初始化为StackCount+1(即在这个分配了4个IO_STACK_LOCATION
单元的IRP中CurrentLocation被初始化为4+1=5),每一次通过IoCallDriver()调用驱动的派遣函数时,这个值减1。
当I/O管理器在调用驱动时,它总是将IRP的StackLocation指针指向下一个StackLocation;当被调用的驱动释放IRP时,
Stack Location真正被指回到前一个Stack Location。因此当过滤驱动的派遣函数运行时,I/O管理器使用Stack Location #4,
也就是分配的最后一个Stack Location。
好了,我们来看传递步骤
1.为下一个IO_STACK_LOCATION结构设置参数 可以通过IoGetNextIrpStackLocation获得下个结构的指针
如果需要设置完成例程,调用IoCopyCurrentIrpStackLocationToNext宏
如果不设置完成例程,调用IoSkipCurrentIrpStackLocation 宏(CurrentLocation+1)过滤驱动常用
2.根据需要设置完成例程
3调用IoCallDriver函数将IRP请求传递给下一层驱动。此时当前驱动不再拥有该IRP的访问权,如果还需要访问必须设置
完成例程。
通过以上整理,对分层驱动,IRP的运转机制,I/O堆栈有一个初步的概念上的了解,当然实际运用中会涉及到一些细节上
的问题,在以后继续的使用当中来完善这些知识点。