Windows 内核结构 摘抄自《windows内核原理与实现》


2.2.1 Windows 内核结构

正如图2.2 所示,Windows 内核分为三层,与硬件直接打交道的这一层称为硬件抽象层(Hardware Abstraction Layer,简称HAL),这一层的用意是把所有与硬件相关联的代码逻辑隔离到一个专门的模块中从而使上面的层次尽可能做到独立于硬件平台。HAL之上是内核层,有时候也称为微内核(micro-kernel),这一层包含了基本的操作系统原语和功能,如线程和进程、线程调度、中断和异常的处理、同步对象和各种同步机制。在内核层之上则是执行体(executive)层,这一层的目的是提供一些可供上层应用程序或内核驱动程序直接调用的功能和语义。Windows 内核的执行体包含一个对象管理器,用于一致地管理执行体中的对象。执行体层和内核层位于同一个二进制模块中,即内核基本模块,其名称为ntoskrnl.exe。

内核层执行体层的分工是,内核层实现操作系统的基本机制,而所有的策略决定则留给执行体。执行体中的对象绝大多数封装了一个或者多个内核对象,并且通过某种方式(比如对象句柄)暴露给应用程序。这种设计体现了机制与策略分离的思想。图2.3 显示了Windows 内核的详细组成结构。

 

(点击也没法查看大图)图2.3 Windows 内核的组成结构

Windows 内核为用户模式代码提供了一组系统服务,供应用程序使用内核中的功能。应用程序通常并不直接调用这些系统服务,而是通过一组系统DLL,最终通过ntdll.dll 切换到内核模式下的执行体API 函数中,以调用内核中的系统服务。Ntdll.dll 是连接用户模式代码内核模式系统服务的桥梁。对于内核提供的每一个系统服务,该DLL 都提供一个相应的存根函数,这些存根函数的名称以“Nt”作为前缀,例如NtCreateProcess、NtOpenFile和NtSetTimer。另外,ntdll.dll 还提供了许多系统级的支持函数,比如映像加载器函数(以“Ldr”为前缀)、Windows 子系统进程通信函数(以“Csr”为前缀)、调试函数(以“Dbg”为前缀)、系统事件函数(以“Etw”为前缀),以及一般的运行支持函数(以“Rtl”为前缀)和字符串支持函数等。

执行体API 函数接收的参数来自于各种应用程序,因此,为了确保系统的健壮性,以及抵抗来自用户模式的恶意攻击,所有的执行体API 函数必须保证参数的有效性。这意味着它们必须在恰当的时刻检查参数的值,若是指针的话,还必须保证调用者可以访问指针所指的内存。通常,执行体系统服务函数会在其开始处,对所接收的参数逐一探查它们的可访问性。例如,以下代码就演示了这一做法:

1. PreviousMode = KeGetPreviousMode();  

2. if (PreviousMode != KernelMode) {  

3. try {  

4. ProbeForWrite(InputInformation,  

5. InputInformationLength,  

6. sizeof(ULONG));  

7. if (ARGUMENT_PRESENT(ReturnLength)) {  

8. ProbeForWriteUlong(ReturnLength);  

9. }  

10. } except(EXCEPTION_EXECUTE_HANDLER) {  

11. return GetExceptionCode();  

12. }  

13. } 

这里InputInformation 和ReturnLength 是该系统服务的直接参数,InputInformation 是个指针参数,它的可用长度由另一个参数InputInformationLength 来指定,因此,这段代码通过ProbeForWrite 来探查这一输入数据是否可写;另外,ReturnLength 是一个ULONG类型的输出参数,这段代码通过ProbeForWriteUlong 来验证该参数是否可写。一旦探查函数发生访问违例,则说明这两个参数有问题,于是该系统服务就将直接返回违例异常代码。

正如前文所述,用户模式和内核模式代码所能访问的地址空间有所不同。在32 位系统上,用户模式代码只能访问2 GB 以下的虚拟内存地址空间,而内核模式代码可以访问当前进程整个4 GB 虚拟地址范围。2 GB 以下称为进程地址空间,2 GB 以上称为系统地址空间。实际上,在两者之间有一块特殊的64 KB 地址空间,位于0x7fff0000~0x7fffffff,在两种模式下都不可访问。上述代码片段中的ProbeForWrite<Xxx>函数首先检查目标内存地址是否越过了此特殊区域,若越过则访问违例;然后,试图将目标地址处的值赋回该地址,即触发一次该地址处的读和写操作,若该内存地址处当前线程不可写,则引发异常,从而使代码片段中的except 子句截获控制。Windows 通过这种方法来捕捉到“用户模式代码传递一个系统空间地址”或者“传递一个无效内存地址”的情形,从而确保执行体函数接收到的参数已被检验过。

然而,执行体API 函数并不总是要检验参数的有效性,如果在调用该API 函数以前的模式是内核模式(Zw模式,由核心模式调用核心函数),那么,它不需要检验参数的有效性。执行体不会用坏的参数来调用它自己的服务,只有当执行体API 函数接收到一个或多个来自用户模式的参数时,它才使用Probe 函数族检查参数的有效性。每个线程都维护着一个状态值,用以说明它以前的处理器模式。当从用户模式切换到内核模式时,该值将被设置为UserMode,从而满足上述代码中的if 条件。

 

来源: <http://book.51cto.com/art/201011/235708.htm>

 

2.2.2 Windows 内核中的关键组件

Windows 操作系统虽然算不上真正意义上的微内核结构,但是它的内核部分有良好的设计以及清晰的模块结构,如前面图2.3 所示。现在我们来逐一介绍内核部分的各个关键组件,不过Windows 子系统部分将留到下一小节单独讲述。

HAL(硬件抽象层)

HAL 的设计目的是将硬件的差别隐藏起来,从而为操作系统的上层提供一个抽象的、一致的硬件资源模型,以使Windows 更容易被移植到不同的硬件平台上。理想的情形是,只要硬件厂商能够提供一个HAL,Windows 就能够在相应的硬件平台上运行。因此,HAL使得上层的模块无须考虑硬件的差异,它们通过HAL 而不是直接访问硬件。

在Windows 中,HAL 是一个独立的动态链接库。尽管Windows 随带了多个主流机器的HAL,但是在系统安装的时候只有一个会被选中,并拷贝和改名为hal.dll。HAL 提供了一些例程供其他内核模块或设备驱动程序调用,这使得一个驱动程序可以支持同样的设备在各种硬件平台上运行。HAL 不仅涵盖了处理器的体系结构,也涉及了中断控制器、单处理器或多处理器等硬件条件。表2.1 列出了在Intel x86 机器上Windows Server 2003系统中随带的HAL。

表2.1 Windows Server 2003 的HAL 列表(Intel x86 处理器)

 


内核(或微内核)

这是大内核中的小内核,将其称为微内核更可以说明它在整个内核模式代码中的地位。它是内核模块ntoskrnl.exe 中的下层部分(上层部分为执行体),最接近于HAL 层,负责线程调度和中断、异常的处理。对于多处理器系统,它还负责同步处理器之间的行为,以优化系统的性能。这一层的核心任务是,让系统中的所有处理器尽可能地忙和高效。内核层可在多个处理器上并发执行,它的代码以C 语言为主,也包含一部分汇编代码。

Windows 的内核实现了抢占式线程调度机制,按照优先级顺序将线程分配到处理器上,并且允许高优先级的线程中断或抢占低优先级的线程。每个处理器上的线程切换也是由内核来完成,它按照调度规则让处理器放弃当前线程,选择下一个要执行的线程。每个线程有一个基本优先级值(base priority),这是由程序在创建线程时指定的;每个线程还有一个动态优先级值,这是在线程执行过程中根据各种条件在基本优先级基础上由内核来调整的,目的是让系统更快地响应用户的动作,以及在系统服务和其他低优先级进程之间平衡处理器资源的分配。

Windows 的内核按照面向对象的思想来设计,它管理两种类型的对象:分发器对象(dispatcher object)和控制对象。分发器对象实现了各种同步功能,这些对象的状态会影响线程的调度。Windows 内核实现的分发器对象包括事件(event)、突变体(mutant)、信号量(semaphore)、进程(process)、线程(thread)、队列(queue)、(gate)和定时器(timer)。控制对象被用于控制内核的操作,但是不影响线程的调度,它包括异步过程调用(APC)、延迟过程调用(DPC),以及中断对象等。

内核层位于HAL 之上,但鉴于内核所提供功能与硬件体系结构的紧密关联性,它不可避免地需要引入一些与体系结构相关的代码,例如,在切换线程时,保存和恢复线程的执行环境取决于处理器体系结构。不过,如何选择下一个线程,这是与体系结构无关的。内核有义务将Windows 所支持的各种硬件体系结构进行抽象,使得体系结构的差异对Windows 代码的影响尽可能地小,并且有些功能可以通过HAL 来完成,毕竟HAL 才是真正的硬件抽象层。例如自旋锁和中断的功能是在HAL 中实现的,内核只需简单地使用HAL的导出函数即可。

执行体

执行体是内核模块ntoskrnl.exe 的上层部分,它包含5 种类型的函数:

导出的、可在用户模式下调用的函数。对这些函数的调用接口位于ntdll.dll 模块中。应用程序通过Windows API 来间接地调用这些函数。

虽已被导出并且可在用户模式下调用,但无法通过任何一个Windows API 来调用的函数。这样的例子包括LPC(Local Procedure Call,本地过程调用)函数、各种查询函数(如NtQueryInformation<Xxx>),以及一些专用的函数,比如NtCreatePagingFile等。对这些函数的调用需要直接链接ntdll.dll 来完成。

只能在内核模式下调用的导出函数,并且在Windows DDK 中有关于这些函数的文档。这些函数可以被设备驱动程序调用。

供执行体组件之间相互调用,但未被文档化的函数。这包括执行体内部使用的一组支持函数。

属于一个组件的内部函数
以上提到的组件是指执行体内部的组件,从大的方面来看,执行体包含以下组件(参考图2.3):

1.进程和线程管理器。负责创建进程和线程,以及终止进程和线程。在Windows 中,对于进程和线程的底层支持是在内核层提供的,执行体在内核层的进程和线程对象的基础上,又提供了一些语义和功能。

2.内存管理器。此组件实现了虚拟内存管理,既负责系统地址空间的内存管理,又为每个进程提供了一个私有的地址空间,并且也支持进程之间内存共享。内存管理器也为缓存管理器提供了底层支持。

3.安全引用监视器(SRM,Security Reference Monitor)。该组件强制在本地计算机上实施安全策略,它守护着操作系统的资源,执行对象的保护和审计。

4.I/O 管理器。它实现了与设备无关的输入和输出功能,负责将I/O 请求分发给正确的设备驱动程序以便进一步处理。

5.缓存管理器。它为文件系统提供了统一的数据缓存支持,允许文件系统驱动程序将磁盘上的数据映射到内存中,并通过内存管理器来协调物理内存的分配。

6.配置管理器。它负责系统注册表的实现和管理。

7.即插即用管理器。它负责列举设备,并为每个列举到的设备确定哪些驱动程序是必需的,然后加载并初始化这些驱动程序。当它检测到系统中的设备变化(增加或移除设备)时,负责发送恰当的事件通知。

8.电源管理器。它负责协调电源事件,向设备驱动程序发送电源I/O 通知。当系统电源状态变化时,通知设备驱动程序处理设备的电源状态。即插即用设备的管理和电源的管理也可以看做是I/O 管理器的扩展功能。

此外,执行体还包含4 组主要的支持函数,供以上这些执行体组件调用。差不多有1/3 的支持函数可以在Windows DDK 中找到相应的文档,因为设备驱动程序也要调用它们。这4 类支持函数如下所列:

1.    对象管理器。它负责创建、管理和删除Windows 执行体对象,以及用于表达操作系统资源的抽象数据类型,比如进程、线程和各种同步对象。

2.    LPC 设施。LPC 设施负责在同一台机器上的客户进程和服务器进程之间传递消息。LPC 是RPC(Remote Procedure Call,远程过程调用,关于网络上客户进程和服务器进程之间通信的工业标准)的一个优化版本。

3.    一组运行时库函数。其功能广泛,涵盖字符串处理、算术运算、数据类型转换以及安全结构处理等。

4.    执行体支持例程。例如系统内存分配(换页内存池和非换页内存池)、互锁的内存访问,以及对两种特殊类型同步对象(资源和互斥体)的支持。

设备驱动程序

在内核中除了内核模块ntoskrnl.exe 和HAL 以外,其他的模块几乎都以设备驱动程序的形式存在。Windows 操作系统中的设备驱动程序,并不一定对应于物理设备;驱动程序既可以创建虚拟设备,也可以完全与设备无关,而仅仅是内核的扩展模块。从软件结构角度而言,我们可以认为设备驱动程序是Windows 内核的一种扩展机制,系统通过设备驱动程序来支持新的物理设备或者扩展功能。

设备驱动程序是可以动态加载到系统中的模块,其文件扩展名为.sys,其格式是标准的PE 文件格式。驱动程序中的代码运行在内核模式下,尽管它们可以直接操纵硬件,但理想的情况是,调用HAL 中的函数与硬件打交道,因此,驱动程序往往用C/C++语言来编写,从而可以方便地在Windows 所支持的体系结构之间进行源代码层次上的移植。

PE 文件格式

PE(Portable Executable)[PE-SPEC]是在Windows NT 设计之初,为了建立一种可移植的、能够适应32 位操作系统需要的可执行文件格式而设计的。这里“可移植”的目标是指当初Windows NT 可在多种处理器(x86、MIPS、Alpha 等)上运行,不同体系结构平台上的Windows NT 使用相同的二进制可执行文件格式。Windows 发展至今,现在仅支持Intel 的处理器,但PE 文件格式仍然不失为一种定义良好且具有可扩展性的可执行文件格式,非Windows NT 内核的操作系统Windows 9x 系列也使用PE 文件格式,因此,PE 仍然是一种可移植的可执行文件格式。

PE 格式扩展了COFF(Common Object File Format),这是UNIX(AT&T UNIX SystemV)中引入的用于描述二进制目标文件的格式规范。尽管COFF 为目标文件定义了一个很好的框架,比以前的a.out 格式改进了很多,但它仍然有一些限制,比如文件中段(section)的数量有最大限制段的名称也有长度限制,无法支持像C++语言所需要的符号化调试信息等。因此,每一个采用COFF 的操作系统厂商几乎都或多或少地对其进行了扩展,比如AT&T 自己又在COFF 基础上定义了ELF(Extensible Linking Format),ELF 目前被广泛用于各种UNIX 类操作系统,包括Linux 和FreeBSD;IBM 在AIX 中使用XCOFF;而Microsoft 则在COFF 格式规范基础上定义了PE 格式。在Windows 平台上,可执行文件(扩展名为.exe)、目标文件(扩展名为.obj)、动态链接库(扩展名为.dll)以及设备驱动程序(扩展名为.sys)等多种文件类型使用了PE 文件格式。随着64 位系统的到来,PE文件格式也相应地有了一个扩展的版本,称为PE32+,允许使用64 位地址空间;原来的32 位PE 格式称为PE32。本书中我们仅涉及32 位PE 格式。

PE 文件的基本结构包括DOS 头、PE 头、段表,以及段数据,如图2.4 所示。DOS头的存在仅仅是为了兼容以前的MS-DOS 系统环境,其中嵌了一段代码可在MS-DOS环境下打印出“This program cannot be run in DOS mode”字样的提示信息。DOS 头后面是4 字节的PE 标志“PE\0\0”,接着是PE 头部分。PE 头分两部分:COFF 头和可选头,对于可执行映像文件,可选头也是必需的。PE 头部分包含了有关当前文件的一些全局信息,包括段的数量、符号信息、可执行文件的基地址和入口函数地址、代码段和数据段的大小、版本信息、栈和堆的初始大小,以及数据目录(data directory)等信息。PE头中的数据目录共有16 项,这些目录项指示了一个可执行文件的导入表、导出表、资源表、重定位表等。

在PE 头的后面是段表(section table),段表中的每一项指示了段的名称、段的虚拟地址位置和大小,以及其他一些有关该段的信息。对于可执行映像,段名称的长度不超过8 个ASCII 字符。段的个数由PE 头中的一个域指定。在段表后面是段数据,对于每一个段,在文件中的数据量可能小于该段的大小,在这种情况下,段后面的数据补零。例如,未初始化数据部分无须在文件中包含任何有效数据。另一个值得注意的特点是,段的位置按照PE 头中指定的边界对齐,所以,在段数据区,段与段之间可能有空隙

当Windows 系统加载一个可执行映像时,如果可执行映像中指定的基地址处已经被占用,那么该可执行映像将不得不被加载到其他的位置。为了让可执行映像中的代码区和数据区在移动位置以后仍然有效,PE 文件格式中的重定位表可以指示代码段和数据段中哪些位置(这是相对位置)的地址引用需要被重新定位。所以,在加载可执行映像文件时,一旦被加载的内存起始地址与PE 头中指定的基地址不一致,则必须要执行重定位操作。所谓重定位操作,是指遍历重定位段表中的每一个重定位项,将当前映像中的代码指令或数据引用按照新的内存起始地址而非原来指定的基地址来引用。

  

(点击查看大图)图2.4 PE 文件的基本结构

有关PE 文件格式的详细信息,请参考PE 文件格式规范[PE-SPEC]。另一个有用的参考资料是Matt Pietrek 写的两篇文章[MSDN-PE1][MSDN-PE2],他在文章中通过代码和例子介绍了如何解析一个可执行文件,包括PE 头的解析和重定位的概念等。他提供的PEDUMP 程序(有源代码)本身也是一份很好的学习和参考材料。

另外,Visual C++提供的工具dumpbin 可以列示出一个PE 文件的各种信息,例如,下面显示了Windows Server 2003 SP1 系统中的文件notepad.exe 的头信息:

C:\>dumpbin D:\Win2k3-Exe\notepad.exe /headers  

Microsoft (R) COFF/PE Dumper Version 9.00.30729.01  

Copyright (C) Microsoft Corporation. All rights reserved.  

Dump of file notepad.exe  

PE signature found  

File Type: EXECUTABLE IMAGE  

FILE HEADER VALUES  

14C machine (x86)  

3 number of sections  

42435B9A time date stamp Fri Mar 25 08:30:18 2005  

0 file pointer to symbol table  

0 number of symbols  

E0 size of optional header  

10F characteristics  

Relocations stripped  

Executable  

Line numbers stripped  

Symbols stripped  

32 bit word machine  

OPTIONAL HEADER VALUES  

…… (这里省略了可选头部分的内容)  

SECTION HEADER #1  

.text name  

7760 virtual size  

1000 virtual address (01001000 to 0100875F)  

7800 size of raw data  

400 file pointer to raw data (00000400 to 00007BFF)  

0 file pointer to relocation table  

0 file pointer to line numbers  

0 number of relocations  

0 number of line numbers  

60000020 flags  

Code  

Execute Read  

Debug Directories  

Time Type Size RVA Pointer  

-------- ------ ----- -------- --------  

42435B9A cv 24 00001910 D10 Format: RSDS, {B4CD0BCE-C210-4  

934-8161-DFC07F9870B0}, 1, notepad.pdb  

…… (这里省略了SECTION HEADER #2 和SECTION HEADER #3 部分的内容)  

Summary  

2000 .data  

9000 .rsrc  

8000 .text 

 来源: <http://book.51cto.com/art/201011/235710.htm>

 


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值