深入解析Windows操作系统笔记——CH3系统机制
2020-12-13 16:26
3.系统机制
微软提供了一些基本组件让内核模式的组件使用:
1.陷阱分发,包括终端,延迟的过程调用(DPC),异步过程调用(APC),异常分发以及系统服务分发
2.执行体对象管理器
3.同步,包括自旋锁,内核分发器对象,以及等待是如何实现的。
4.系统辅助线程
5.其他的机制,比如Windows全局标记
6.本地过程调用
7.内核事件跟踪
8.Wow64
3.系统机制... 1
3.1陷阱分发... 3
3.1.1 中断分发... 4
3.1.1.1 硬件中断... 4
3.1.1.2 软中断请求级别(IRQL)5
3.1.1.3 软中断... 8
3.1.2 异常分发... 11
3.1.3 系统服务分发... 14
3.1.3.1 32位系统服务分发... 14
3.1.3.2 64位系统服务分发... 14
3.1.3.3 内核模式的系统服务分发... 14
3.1.3.4 服务描述符表... 16
3.2 对象管理器... 17
3.2.1 执行体对象... 18
3.2.2 对象结构... 19
3.2.2.1 对象头和对象体... 20
3.2.2.2 对象类型... 21
3.2.2.3 对象方法... 22
3.2.2.4 对象句柄和进程句柄表... 23
3.2.2.5 对象安全性... 25
3.2.2.6 对象保持力... 25
3.2.2.7 资源记账... 26
3.2.2.8 对象名称... 27
3.2.2.9 会话名称空间... 28
3.3 同步... 28
3.3.1 高IRQL的同步... 28
3.3.1.1 互锁操作... 29
3.3.1.2 自旋锁... 29
3.3.1.3 排队自旋锁... 29
3.3.1.4 栈内排队自旋锁... 30
3.3.1.5 执行体的互锁操作... 30
3.3.2 低IRQL的同步... 30
3.3.2.1 内核分发对象... 31
3.3.2.2 快速互斥体和受限互斥体... 33
3.3.2.3 执行体资源... 33
3.3.2.4 压栈锁... 34
3.4 系统辅助线程... 34
3.5 windows全局标志... 35
3.6 本地过程调用(LPC)35
3.7 内核事件跟踪... 36
3.8 Wow64. 36
3.8.1 Wow64进程地址空间布局结构... 37
3.8.2 系统调用... 37
3.8.3 异常分发... 38
3.8.4 用户回调... 38
3.8.5 文件系统重定向... 38
3.8.6 注册表重定向和反射... 38
3.8.7 I/O请求... 38
3.9总结... 38
3.1陷阱分发
中断和异常是导致处理器转向正常控制流之外代码的两种系统条件。陷阱(trap)是指当异常或者中断发生时,处理器捕捉到一个执行线程,并将控制权转移到操作系统中某处固定地址处的机制。
在Windows中处理器将控制权转给一个陷阱处理器 (trap handle)。所谓陷阱处理器是指与某个特殊的中断或者一场相关的一个函数。
内核对待中断和异常是有区别的,中断是异步事件,并且与当前正在运行的任务毫无关系。中断主要由I/O设备,处理器时钟,定时器产生。中断可以允许和禁止。异常是一个同步过程,它是一个特殊指令执行的结果。
异常可以在同样数据在一个程序里重现。异常的例子:内存访问违例,特定的调试器指令,以及除0错误。内核把系统服务调用异常(从技术上讲,他们是系统陷阱(trap))。
当一个硬件异常或者中断产生的时候,处理器在被中断的线程的内核栈中记录机器状态信息,当它可以回到控制流中该点处继续执行。如果该线程在用户模式下执行,那么windows就切换到该线程的内核模式栈。然后windows在被中断的线程的内核栈上创建一个陷阱帧(trap frame),并把线程的执行状态保存在陷阱帧里。在内核调试器中输入dtnt!_ktrap_frame就可以看到陷阱定义。
多数情况下内核安装了前端陷阱处理函数,在内核将控制权交给与改陷阱香瓜的其他函数之后或者之前,由这些前段陷阱来执行一些常规的陷阱任务。
如陷阱条件是一个设备中断,则内核硬件中断陷阱处理器将控制权转交给一个由设备驱动程序提供给改中断设备的中断服务例程(ISR)。
若陷阱条件是因为调用了一个系统服务引发,那么通用的系统服务陷阱处理器将控制前交给执行体中指定的系统服务。内核不会为不处理的陷阱安装陷阱处理器。陷阱处理器一般使用KeBugCheckEx,当内核检测到可能导致数据被破坏的行为时,改函数会停止计算机。
3.1.1 中断分发
硬件产生的中断往往是有I/O设置激发的。当设备需要服务就会以中断的方式通知处理器。中断驱动的设置可以一步的进行I/O处理。
系统可以产生软中断,如内核可能触发一个软中断,触发线程分发过程,同时也以异步的方式打断一个线程的执行。
内核安装了中断陷阱处理器来响应设备中断,中断陷阱处理器将控制权递给一个负责该中断的外部例程(ISR)或者传递给一个响应中断的内部内核例程。
下面介绍硬件如何向处理器通知中断,内核支持中断类型,设备驱动如何与内核交互,以及内核如何识别软中断。
3.1.1.1 硬件中断
在windows锁支持的平台上,外部I/O中断进入中断控制器的一个引脚,该控制器在cpu的引脚上中断cpu。中断控制器将IRQ(中断请求)翻译成中断号,利用该中断号作为中断分发表的索引。并将控制权传递给恰当的中断分发例程。
在引导时,windows填充IDT(中断分发表),其中包含了指向内核中负责处理每个中断和异常的指针。
windows将硬件IRQ映射到IDT上,同时它利用IDT来为异常配置陷阱处理器。虽然windows支持最多256个IDT项,但是支持的IRQ数据量由中断控制机设计决定。
3.1.1.2 软中断请求级别(IRQL)
虽然中断控制器已经实现一层中断优先级,但是windows仍然强迫使用它自己的中断优先级方案,称为中断请求级别(IRQL)。
X86,X64,IA64中断请求级别:
中断是按优先级别来处理的高优先会抢占低优先级中断的执行权,当一个高优先级中断发送,处理器会把中断线程上下文保存起来,并调用与中断相关的陷阱分发器,陷阱分发器提升IRQL,并调用中断服务例程,调用完成后降低IRQL,回到中断发送前,被中断线程运行。但是当有其他,低优先级中断时,当IRQL降低,低优先级中断出现。这样,内核会恢复到上述过程来处理中断。
线程优先是线程的属性,IRQL是中断源的属性。每个处理器的IRQL设置可以随系统代码的执行变化。
每个处理器的IRQL设置决定了该处理器可以接收哪些中断。当一个内核模式线程运行时,可以通过KeRaiseIrql和KeLowerIrql来提升和降低处理器IRQL或通过调用内核同步对象的函数间接提高或者降低IRQL。当处理器的IRQL高于中断源则被屏蔽,否则被中断打断。
访问。
因为访问PIC(中断控制器)比较慢所以引入了优化技术延迟IRQL以避免访问PIC。当IRQL被提升,HAL记下新的IRQL而不是去修改中断屏蔽值。当一个较低中断发生则HAL将中断屏蔽值设置为对于第一个中断正常的值。这样当IRQL被提升的时候没有更低优先级中断,则HAL需要修改PIC。
一个内核模式线程根据请求,来降低和升高处理器IRQL。当中断发生时,陷阱处理器(或处理器本身)将改处理器的IRQL提升到中断源的IRQL.这样会把等于或者低于它的所有中断都屏蔽。保证了不被低级中断截掉。被屏蔽的中断由其他处理器处理或者被保存下来知道IRQL下降。
因此系统组件包括内核和设备驱动,都试图让IRQL保持在被动级别。这样可以提高设备启动可以更加及时的响应硬件中断。
每个中断级别都有特定的目的,如内核发出一个处理器间的中断,以请求另外一个处理器执行一个动作。
将中断映射到IRQL:IRQL级别和中断控制器定义的中断请求并不相同,在hal中决定一个中断分配给那个IRQL。然后调用HAL函数HalGetSystemInterruptVector把中断映射到对应的IRQL。
预定义的IRQL:以下介绍一下预定义的IRQL
1.只有当内核在KeBugCheckEx中停止了系统并屏蔽所有中断的时候,内核才会使用高级别的IRQL。
2.电源失败,出现在NT文档中,但是从来没有使用过。
3.处理器间的中断,用于向另外一个处理器请求执行一个动作。
4.时钟,主要用于系统时钟,内核利用该黄总段级别来跟踪具体时刻,以及现场测量或者分配cpu时间。
5.性能剖析(Profile),当内核的性能剖析功能被打开的时候,内核性能剖析陷阱处理器会记录下中断发生时被执行的代码的地址。(性能剖析器Kernrate)
6.设备IRQL,用来对设别中断优先级分区
7.DPC/Dispath级别和APC级别是由内核和设备驱动程序产生的软中断。
8.被动级别,最低的IRQL优先级别,它不是一个中断级别,它是普通线程运行时设置的,允许所有中断发生。
对于DPC/Dispatch级别或者更高级别上的代码,一个重要的限制是它不能等待一个对象。另外一个限制DPC/Dispatch或者更高级别的IRQL只能访问非换出页内存。若2个限制都违反了系统会崩溃,代码为IRQL_NOT_LESS_OR_EQUAL。在驱动上违反限制是一种常见的错误。在驱动上违反限制上一种创建的错误。
中断对象,内核提供了一种可移植的机制使得设备驱动程序可以为它们的设备注册ISR。这是一个称为中断对象的内核控制对象。
中断对象包含了所有“供内核将一个设别的ISR与一个特定级别的中断关联起来的所有信息”,包含该ISR的地址,该设备中断时所在的IRQL级别,以及内核中该ISR关联的IDT项。
驻留在中断对象中的代码调用了实际的中断分发器,通用是内核的KiInterruptDispatch或者KiChainedDispatch例程,并将指向中断对象的指针传递给它。
下图显示了中断控制流:
将ISR与特定中断级别关联起来的称为连接一个中断对象,将ISR与IDT项断开关联称为断开一个中断对象。这些操作通过内核函数IoconnectInterrupt和IoDisconnectInterrupt完成。
3.1.1.3 软中断
虽然大多数中断都是硬件产生,但是windows内核也为各种各样的任务产生软中断。包括:
1.激发线程分发
2.非时间紧急中断处理
3.处理器定时到期
4.特定线程的环境中异步执行一个过程
5.支持异步I/O操作
分发或者延迟过程调用(DPC)中断,当一个线程不能继续执行的时候,比如因为线程已经终止了或者主动进入等待状态,内核就会直接调用分发器,从而立即导致一个环境切换。但是有时检测到线程已深入到许多层代码中,这时应该进行重新调度,这种情况下内核请求分发,但是将它推迟到完成了当前的行为之后再进行。
当啮合对共享的内核数据访问,会把IRQL拉到DPC/Dispatch级别当内核检查到需要分发的时候,请求一个DPC/Dispatch中断。所以只有当内核完成了当前的活动,把IRQL拉低,分发中断才能处理。
延迟事务也在这个IRQL上运行,DPC是完成一项系统任务,但是不是那么紧迫,这些函数被称为延迟的,是因为不会了立即执行。
DPC赋予操作系统一种能力,产生一个中断并且在内核模式下执行系统函数。内核利用dpc来处理定时到期,以及一个线程的时限到期以后重新调度处理器。为了给硬件中断提供及时的服务,windows视图把IRQL保持在低于设备IRQL之下。为了达到这个目的,让设备驱动程序ISR执行最少必要的工作来响应他们的设备,将异变的中断状态保存起来,并将数据传输非时间紧迫的中断处理推迟到 DPC/Dispatch IRQL级别上的DPC中在执行。
DPC是通过DPC对象来表示的,DPC对象是内核控制对象,对于用户模式不可见,对于设备驱动和内核代码是可见的。DPC对象包含最重要的信息是DPC中断将要调用哪个系统函数地址。正在等待的DPC被存放在队列中,每个处理器都有一个队列称为DPC队列。要想请求一个DPC,系统会初始化DPC对象,然后放入DPC队列中。
默认情况下内核把DPC对象放在发生该DPC请求的处理器的DPC队列末尾。在设备驱动程序只需指定一个DPC优先级别和指定特定CPU,就可以改变这种默认方式。指定在某个CPU上叫定向DPC。如果一个DPC的优先级为低级或者中级则放入队列尾,否则放入队列头部。
当处理器的IRQL从DPC/Dispatch或更高降到某个更低的级别时,内核处理DPC。在处理DPC是IRQL在DPC/Dispatch级别上,并且将DPC来出来运行直到队列为空。当队列为空内核才让IRQL降低到DPC/Dispatch以下。让正常的线程执行过程继续执行。
DPC优先级可以以另一种方式影响到系统行为。内核通常通过一个DPC/Dispatch级别的中断来激发队列“抽干”的动作。只有当一个DPC被定为在ISR所在的处理器上,且改DPC的优先级是高级或者中级时,内核才产生一个中断,若为低级只有当DPC请求到一个阀值或一段时间后,内核才会请求中断。
若DPC被定为在一个不同于其ISR运行的CPU上,并DPC为高级。内核立即用一个信号通知CPU,以便”抽干”它的DPC队列。若优先级为中级或者低级则DPC数据超过阀值,内核才会激发一个DPC/Dispatch中断。
DPC中断产生的规则:
DPC优先级别 |
DPC被定为在ISR的处理器上 |
DPC被定为在另一个处理器上 |
低级 |
DPC队列长度超过最大的DPC队列长度值,或者DPC请求率小于最小的DPC请求率。 |
DPC队列长度超过最大的DPC队列长度或者系统空闲 |
中级 |
总是激发 |
DPC队列长度超过最大的DPC队列长度或者系统空闲 |
高级 |
总是激发 |
总是激发 |
DPC主要为设备驱动提供的,但是内核也使用DPC,内核使用DPC来处理限时到期事件。在系统时钟每个”嘀嗒”点上,就发生一个时钟IRQL级别的中断。时钟中断处理器对系统时间进行更新,将一个记录了当前线程运行多长时间的计数器递减。当计数器减到0,线程到期,内核可能需要重新调度该处理器,这个任务在DPC/Dispatch IRQL上完成。
时钟中断处理器将一个DPC插入到队列中以便激发分发过程。然后结束他的工作并且降低IRQL,因为DPC中断级别较低,所以在时钟中断完成前出现尚未处理的设备中断,都在DPC中断之前被处理。
APC异步调用,异步过程调用提供了一种在特定用户线程环境中执行用户程序和系统代码的途径。APC经过排队以便在特定线程的环境中执行。
APC是由一个内核控制对象(APC对象)来描述的,正在等待执行的APC驻留在一个由内核管理的APC队列中。APC队列是特定线程相关的,即每个线程有它自己的APC队列,当内核请求要将APC排队时,它将一个APC排队,她将APC插入到将来执行此APC例程的那个线程的队列中。当内核请求APC级别中断,当该线程最终开始执行的时候,会执行此APC。
有2种APC:内核和用户模式。内核模式的APC并不要求从目标获取许可就可以运行在改线程的环境中,而用户模式必须先获取许可。内核模式的APC有普通和特别2种,将IRQL提升到APC级别或调用KeEnterGuardRegion,就可以静止这两种类型的内核模式APC。
执行体使用内核模式的APC来完成那些必须要在特定线程的地址空间(执行环境)中才能完成的操作系统任务。它可以利用特殊的内核模式APC来指示某个线程停止执行一个可中断的系统服务。
用户模式APC(ReadFileEX,WriteFileEx和QueueUserApc),如ReadfileEx,WritefileEx允许调用者指定一个完成例程,当I/O完成是例程就会被调用。I/O完成机制是通过I/O的线程插入一个APC来实现的。内核APC运行在APC级别上,用户模式APC运行在被动级别上。
APC交付会导致等跌队列重新排序,如APC用来把等待资源的线程挂起,那么该线程就会进入等待访问这个资源队列的末尾。
3.1.2 异常分发
中断可以在任何时候发生,异常则是直接由当前正在运行的程序产生。windows引入了一种称为结构化异常处理的设施,应用程序可以在异常发生时获得控制,然后应用程序可以修正条件,并返回到异常发生处,将栈展开(使引发异常的子例程执行过程中止),或想系统报告,改异常不可识别,因为系统应该继续搜索一个有可能处理此异常的异常处理器。
x86上所有异常都在预定义的中断号,这些中断号对应IDT项。每个项指向了某个特定异常的陷阱处理器。
所有异常,除了简单的通过陷阱处理器,可以解决的之外,其他都由异常分发器的内核模块服务。异常分发器就是找到一个异常处理器,处理要处理的异常。
异常处理对用户来说都是透明的,有些异常也允许原封不动的回到用户模式。如内存访问违例,算法溢出,操作系统不对他们处理。环境子系统可以建立起基于帧的异常处理器来处理异常。
基于帧是将一个异常处理与一个特定的过程激活动作关联起来。当一个过程被调用,代表该过程的帧被压到栈中。一个栈帧可以关联多个异常处理器,每个异常处理器保护源程序中一块特定代码。当发生一个异常时,内核查找与当前帧关联在一起的某个异常处理器。如果没有找到内核继续查找与上一个栈帧关联在一起的某个处理。如果最终还是没有找到异常处理器,内核会调用自己默认的异常处理器。
异常发生,CPU硬件将控制权递交给内核陷阱处理器,内核陷阱处理器创建一个陷阱帧。正由于陷阱帧处理完异常后,系统可以从停止的地方恢复。
如果在内核模式下的异常,异常分发器调用一个例程来找到一个基于帧的异常处理器。由它来处理异常。
在用户模式下,windows子系统有一个调试器端口和异常端口,通过它们来接收windows进程中用户模式异常的通知。内核在它默认的异常处理器中用了这些端口。
调试器端口是最常见的异常来源,因此异常分发器采取动作,
1.查看引发该异常的进程是否有一个相关的调试器进程。若存在异常分发器发送一个调试器对象信息到调试对象相关的进程。
2.若该进程没有附载的调试器进程或调试器并没有处理该异常,那么异常分发器切换到用户模式下。将陷阱帧按照Context数据结构的格式拷贝到用户栈中,并调用一个例程来找到一个基于帧的异常处理器。
3.如果没有找到或者虽然找到了但是它不处理该异常,则异常分发器切换到内核模式下,并且再次调用调试器,以便让用户做更多的调试。
4.若调试器不在运行,并没有找到基于帧的处理器,那么内核向与该线程的进程关联在一起的异常端口发送一个信息。该异常端口如果存在的话,则一定是由控制该线程的环境子系统注册的。环境子系统监听该端口,在恰当的时机把一个异常转化为一个与环境相关的信号或异常。客户/服务器运行时子系统(CSRSS)简单的弹出一个消息框来通知用户发生了错误,并且终止进程。
5.当POSIX从内核收到一个消息,指定的一个线程产生了异常,当内核在处理异常过程走得比较深了,而子系统并没有处理该异常,那么内核执行一个默认的异常处理器,它只是简单的将引发该异常的线程所在的进程终止掉。
未处理的异常
所有windows线程都有一个异常处理器来处理未被处理的异常。该异常处理器是在windows内部的进程启动函数或线程启动函数中声明。如:
如果一个线程的异常没有被处理,则windows的未处理异常过滤器将会被调用。这个函数目的是,当一个异常未被处理时,可以提供一种系统统一的行为和方法。
3.1.3 系统服务分发
内核陷阱处理器分发中断,异常和系统服务调用
3.1.3.1 32位系统服务分发
在x86 Pentium II处理器以上,使用windows使用sysenter执行触发一个陷阱,这个是intel特别为快速系统服务定义的。为了支持这一指令,windows在引导时刻把内核的服务分发器的地址保存与该指令相关的寄存器中。
执行该指令会导致变化到内核模式下,并且执行系统服务分发器,为了返回到用户模式,系统服务分发器通常执行sysexit执行(当处理器单步标记被打开,系统分发器改而使用iretd指令。)
在K6和更高的32位AMD处理器上,windows使用syscall类似于sysenter,系统嗲用号也在EAX上,而调用者参数则保存在栈中。在完成了分发之后,内核执行sysret指令。
3.1.3.2 64位系统服务分发
在64位体系结构上,windows使用syscall指令进行系统分发(和AMD处理器上syscall类似),系统调用号存在EAX寄存器,前4个参数存放在寄存器汇总其他参数存放在栈中。
在IA64上使用EPC指令,前8个系统调用参数通过寄存器来传递,其他参数通过栈传递。
3.1.3.3 内核模式的系统服务分发
内核利用传递进来的参数找到系统分发表中的服务信息(类似IDT表)。
系统服务分发器KiSystemService将调用的参数从用户模式栈中复制到内核模式,然后执行服务。如果换地给一个系统服务的参数指向了用户空间中的缓冲区,那么在内核模式代码复制到缓冲区或从缓冲区读前先要查明缓冲区是否可以访问。
每个线程都有一个指针指向它的系统服务表,windows有2个系统服务表,最多可以支持4个。系统服务分发器确定哪个表包含了所有请求的服务,它将32位系统服务号中的其中2个位解释成一个索引表。系统服务号低12位被用在该表索引所指定的表中进行的索引。