首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

Windows内核的一些概念

本文是对官方文档的翻译和整理。


1. 用户态与内核态

在一台运行着Windows操作系统的计算机上,其处理器有两种工作模式:用户模式(user mode)和内核模式(kernel mode)。 应用程序一般使用用户模式,操作系统组件则一般使用内核模式。而驱动程序则可能工作在内核模式下,也可能是用户模式。

在打开一个用户模式下的应用程序时,windows会为这个程序创建一个进程,该进程有一个独立的虚拟地址空间(virtual address space)和一个句柄列表(handle table)。因为虚拟地址空间时私有的独立的,所以用户程序之间不能够直接交换数据。所以说用户程序是独立的, 一旦一个用户程序崩溃了,它也不能够给操作系统或者其它用户程序带来什么危害。

然而,虚拟地址空间的私有性、独立性对于用户程序而言也是有限制的。用户模式下的处理器并不能够访问操作系统保留的虚拟地址空间。 这是为了防止用户程序随意地篡改操作系统的运行数据。

所有运行在内核态的代码共享着一个虚拟地址空间。这意味着所有运行在内核模式下的驱动都不是独立的。一旦内核态的驱动错误地向一个虚拟地址写数据了, 将可能导致系统或者其它驱动的崩溃。右图描述了用户态和内核态的组件之间的关系。

2. 虚拟地址空间

处理器在需要读写一个地址时,就会用到虚拟地址空间。它会先将一个虚拟地址转换为一个物理地址(physical address)。虚拟地址空间有如下几个优势:

一个进程可用的虚拟地址空间范围就是其虚拟地址空间。每一个用户进程都有一个自己的虚拟地址空间。 比如说一个32的进程通常有一个0x00000000到0x7FFFFFFF的2GB的地址空间,一个64位的进程其虚拟地址空间则有8TB。虚拟地址空间有时也成为虚拟内存。

左图描述了一些虚拟内存的特性。在这个图中有两个64位的进程:Notepad.exe和MyApp.exe。 每一个进程都有一个从0x000'0000'0000到0x7FF'FFFF'FFFF的私有的虚拟内存。可以看到这两个程序分别有三个和两个从0x7F7'9395'0000开始的缓存叶, 它们被分别映射到了不同的物理内存叶上。

2.1 用户空间和系统空间

像Notepad.exe、MyApp.exe这样的用户程序是运行在用户模式下的。操作系统的核心组件以及很多驱动程序都是运行在内核态的。 每一个用户进程都有一个自己的虚拟内存,我们称之为用户空间。而所有的内核态程序共用一个虚拟内存,称之为系统空间。

在32位的Windows系统中,通常将低2GB的空间用作用户空间,高2GB的内存地址用作系统空间。用户可以在引导过程中为用户空间申请超过2GB的内存。 这意味着挤占了系统空间的内存。在64位的Windows系统中,理论上有2^64字节的空间,实际上只有一小部分得到了使用。通常只有8TB的空间用作用户空间, 而更大的248TB的空间用作系统空间。

用户态的程序只能访问用户空间,而内核态的程序既可以访问系统空间也可以访问用户空间。运行在内核态的驱动程序必须仔细考虑对用户空间的读写访问, 如下的一个应用场景对此做出了解释:

  1. 用户程序发起了一个读取外设数据的请求,并且提供了存放数据的缓存。
  2. 运行在内核态的设备驱动程序执行读操作,并将控制权交给调用者。
  3. 一段时间以后,外设产生了一个中断告知读操作已经结束了。这个操作由运行在内核态的驱动程序处理。
  4. 此时,驱动程序不能够直接将数据写到第一步中用户程序提供的缓存中。因为用户程序提供的地址是一个虚拟内存, 它所映射的物理内存很有可能已经发生了改变。

2.2 Paged pool and nonpaged pool

在用户空间中,其对应的所有物理内存叶都可以交换到磁盘中。系统空间中,有的内存叶可以交换到磁盘中,有的则不可以。 内存空间中有两个空间可以用来动态的分配内存,它们被称为paged pool和nonpaged pool。paged pool中的内存是可以交换的, 而nonpaged pool中的内存是不可以交换的。

3. 设备节点和设备栈

Windows用一个即插即用设备树(Plug and Play device tree, PnP),以后简称设备树(device tree)来管理外设。 一般情况下,设备数上的一个节点代表一个设备或者是一个复杂设备的某一项功能,但有些节点只是软件意义上的设备没有物理实体。

设备树上的节点称为设备节点(device node),这棵树的根节点称为根设备节点(root device node)。 如左图所示,一般将根节点画在一棵树的最下面。这个图中也描述了,PnP环境中父子节点之间的继承关系。设备树中的一些节点代表着总线,有子节点连接在其上。 比如说,其中的PCI总线节点就代表了主板上的PCI总线。PnP管理器要求PCI总线驱动枚举连接在其上的外设,这些枚举出来的设备就作为PCI总线节点的子节点, 挂在设备树上。更进一步的这些设备又衍生出了各种不同的外设,比如USB宿主控制器、声卡、PCI Express端口等。

有些连接在PCI总线上的设备本身就是一种总线,比如说USB总线、声卡控制器等。设备树会要求这些总线驱动器进一步枚举挂在其上的设备, 我们就得到了各种不同的外设,比如鼠标、键盘、各种声卡等。

至于说一个节点是一个外设还是一种总线,完全取决于我们主观意愿。比如说,我们完全可以把一个显示适配器看作是一个外设, 它负责准备刷新屏幕的帧。此外,我们还可以将其看作是一种总线,它可以检测和枚举连接在主机上的显示器。

3.1 设备对象和设备栈

设备对象就是结构体DEVICE_OBJECT的实例。 在设备树中的每一个设备节点都有一个设备对象的有序表(ordered list),而其中每一个设备对象都有一个与之对应的驱动。 设备对象的有序表,以及与之相关联的驱动被称为该设备的设备栈(device stack)。

按照约定,每一个设备栈都有一个顶部(top)和一个底部(bottom)。第一个创建的设备对象放置在设备栈的底部,最新创建的设备对象则放置在设备栈的顶部。 左图中列出了一个叫做Proseware Gizmo的设备栈,它有三个<设备对象,驱动>对,从栈顶到栈底分别对应着AfterThought.sys、Proseware.sys、Pci.sys三个驱动。 PCI总线节点则有两个<设备对象,驱动>对,分别关联着Pci.sys和Acpi.sys两个驱动。

在启动的过程中,PnP管理器要求每个总线驱动枚举挂在其上的外设,比如这里的PCI总线驱动Pci.sys就是用于枚举PCI设备的。 Pci.sys就会为每一个枚举到的设备建立一个设备对象,它们被称为物理外设对象(Physical Device Object, PDO)。

PnP管理器就会接管这些PDO并从注册表中查找一个与之匹配的驱动。设备栈一定有且仅有一个功能驱动(function driver), 但可以有一个或者多个过滤驱动(filter driver)。功能驱动是设备栈的主要驱动用于处理读、写以及关于设备控制的请求,而过滤驱动则是一个辅助的角色。 装载功能驱动和每一个过滤驱动时,都会创建一个设备对象并与驱动一起添加到设备栈中。由功能驱动创建的设备对象称为功能设备对象(Function Device Object, FDO), 而由过滤驱动创建的设备对象称为过滤设备对象(Filter Device Object, Filter DO)。

在设备栈中,PDO总是在栈底。FDO和Fiter DO总是在PDO之上。有时FDO会在栈顶,此时设备栈被称为upper filter driver。 有时FDO之上还有Filter DO,此时设备栈称为lower filter driver

安装设备驱动的时候,安装器将根据一个信息文件(INF)来确定哪个驱动是功能驱动,哪些驱动是过滤驱动。通常情况下,INF文件是由Windows或者设备厂商提供的。

3.2 用户态的设备栈

到目前为止,我们讨论的都是内核态的设备栈。但是在一些情况下,外设还有一个用户态的设备栈, 它们都是建立在用户态驱动框架(User-Mode Driver Frameworks, UMDF)下的。 UMDF是windows驱动框架(Windows Driver Frameworks, WDF)的一种模式。 在UMDF中,驱动都是用户态的DLL,设备对象都是实现IWDF设备接口的COM对象。在UMDF设备栈中,设备对象被称为WDF设备对象(WDF device object, WDF DO)。 右图是一个兼具内核态和用户态设备栈的驱动示意图。

4. I/O请求包

大部分发送给外设驱动的包都是I/O请求包(I/O Request Packets, IRPs)。 操作系统组件或者驱动通过调用IoCallDriver 来发送IRP。函数IoCallDriver有两个参数,一个是指向DEVICE OBJECT对象的指针,另一个则是指向IRP对象的指针。

通常一个IRP是由设备栈中多个驱动处理的,IRP首先发送给栈顶的,然后依次将IRP向栈底传播。有些IRP会一直传输到PDO, 但一些IRP在上层的几个驱动中就已经处理完了,就不会在向下传播。

5. Minidrivers, Miniport drivers, and driver pairs

一个Minidriver和一个miniport驱动构成一个驱动对。驱动对可以简化驱动的开发过程,一个驱动用于处理一系列设备共有的一般性任务, 另一个驱动则用于处理一个外设特定的个性化任务。而处理外设个性化任务的驱动有各种各样的名字,包括miniport driver, miniclass driver, 以及minidriver。 微软提供一般性的驱动,而设备制造商则需要提供个性化的驱动。

每一个内核态的驱动都必须实现一个称为 DriverEntry的函数, 在装载驱动的时候就会调用这个函数。我们在DriverEntry中填充DRIVER_OBJECT的一些字段,主要是一些函数的入口指针, 比如说调用Unload函数的DeviceUnload字段。 DRIVER_OBJECT的MajorFunction成员则是一个指向处理IRP的函数的指针列表,通常驱动会为各种不同类型的IRP实现多个处理函数,并将其入口指针赋予MajorFunction。 IRP的各种请求可以通过宏IRP_MJ_READ、IRP_MJ_WRITE等加以区分和标识,这些常数也是MajorFunction的索引,如果需要一个IRP_MJ_WRITE的处理函数, 我们就需要给MajorFunction[IRP_MJ_WRITE]赋值。 我们可以通过调试工具!drvobj来查看DRIVER_OBJECT的详细内容。

6. WDK的头文件

WDK(Windows driver kit)包含了构建内核态和用户态驱动的所有头文件。 在这些头文件中记录了windows的版本信息,所以可以根据这些版本信息为各种不同版本的windows系统写驱动。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1