在上面的几篇博文中,介绍了 IRP 与派遣函数,通过例子“磁盘设备的绝对读写”演示了在应用程序中向一个设备发出I/O请求,并实现了驱动程序中处理一个I/O请求——由 DeviceIoControl 这个 Win32API 经过一系列调用,在内核中生成的 IRP_MJ_DEVICE_CONTROL 这个IRP。
首先我们需要来看看什么叫“缓冲I/O设备”
还记得我们在之前的“NT驱动的基本结构”一篇博文中,我们调用内核函数创建设备后,对 DEVICE_OBJECT 进行了一些操作,比如:取得设备扩展地址,不知道大家注意到这行代码了没有:
pDevObj->Flags |= DO_BUFFERED_IO;
我们修改了 DEVICE_OBJECT 结构中的 Flags 成员,使用按位或的方式增加了一个 DO_BUFFERED_IO 标志,将设备修改成了缓冲I/O设备。
提示:其实缓冲I/O设备设个名词不一定准确,或许叫“使用缓冲I/O工作的设备”更好一点。 |
让我们先来看看什么是”缓冲I/O设备”,并以此为线索了解一些原理。
当我们调用 ReadFile(Ex) 和 WriteFile(Ex) 读写文件,管道或者设备时,我们需要提供一个缓冲区的指针,如果是同步读(注意是同步),那么 ReadFile 返回后,我们要读的数据就在缓冲区里了,如果是同步写(同样,注意是同步),则 WriteFile 结束后,我们要写的数据就写完了,当然都是没有出错的情况下。
这一切看起来似乎并没有什么特殊的,我们早已经对这两个函数熟悉到不能再熟悉,但是你有没有发现有些不对劲,但是又不知道在哪里呢?
我们来回忆一下API调用过程,我们调用的 ReadFile 和 WriteFile ,是 Win32 子系统提供的编程接口,也就是 Win32API ,Win32API 会对实参进一步包装,可能调用其他子系统API,但最终都会调用从 ntdll 导出的 NT Native API ,nativeAPI 调用 KiFastSystemCall ,随后进入内核,进入内核在老硬件平台和老版本 winNT 上是通过软中断 int 2e,现在一般是有专门的指令,为了减少从 R3 切换到 R0 带来的性能损失,进入内核后,调用内核模式下的函数,比如 ZwReadFile ,这个函数会查 SSDT,找到 NtReadFile 并调用之,之后,会调用内核中 I/O管理器的接口,I/O管理器构造生成 IRP ,并发送到相应设备所在驱动程序的派遣函数中。
你可能会奇怪,对啊,没有什么啊,我们不就是要写派遣函数从而处理这些IRP么?那么问题来了,Windows 是一个多任务抢占式调度的操作系统(上世纪的 Windows1.0 等不是抢占式的,是多任务协作式调度的),虽然在某一个具体的时刻,一个 CPU(或一个 CPU 核心)不可能是多任务的,但从宏观上说,操作系统中有大量的线程是并发执行的!于是,“进程上下文”和“线程上下文”在频繁地被切换,上下文中记录了 CPU 的执行现场,寄存器,堆栈地址等,用于上下文切换后恢复现场使得程序得以继续执行,进程上下文的切换就意味着用户模式虚拟内存(线性地址空间)的切换,博主现在发现,在驱动开发(1)基础知识一篇博文中对虚拟内存一笔带过真是个错误,所以在这里简单说一下:
虚拟内存也叫线性地址空间,在 Intel 80x86 架构的 CPU 上工作的 32bit Windows 操作系统,虚拟内存的大小恒为4GB,虚拟内存是Windows操作系统的最基本机制之一,原理是将线性地址映射到物理内存,他不能被关闭,也不能修改大小,如果你问为什么是4GB,原因很简单,32位的指针的寻址能力只有32位,因此指针最多表示4GB的地址空间。虚拟内存一般是低2GB是用户模式下的,而高2GB由内核使用,虚拟内存分为非页内存和分页内存,非页内存不能交换到磁盘上,而分页内存可以暂时转储到磁盘上,一旦一个虚拟内存页面被转储到文件,那么此页面就被打上一个“脏的”标志,一旦程序访问这样的内存页面,就会触发一个“缺页中断”,从而引发异常处理程序,异常处理程序会将页面从磁盘移到物理内存中,并映射到程序试图访问的虚拟内存地址上。虚拟内存解决了这么几个问题:1。使不同进程的内存空间是私有的,为多任务提供了基础;2。内存页面可以设置访问规则,比如不可执行标记,就是传说中的 DEP ,为了防止hacker通过溢出攻击入侵,但是早已被破解,原理是溢出调用 VirtualProtect 修改