Windows系统机制之陷阱分发

根据wrk1.2源码,结合毛批&潘老师的《Windows内核原理与实现》&《Windows Internals》对Windows内核的设计进行归纳。

Windows系统机制之陷阱分发

这里的陷阱不是3种异常类型中(中止abort、陷阱trap、错误fault)中的陷阱,而是一个宏观的概念。根据上下文区分。

异常或中断发生时,处理器捕捉到一个执行线程,将控制权交给trap handler。

中断是异步的,由I/O设备、处理器时钟或定时器产生,可以被打开或关闭。

异常是同步的,一般是特殊指令执行的结果。例如内存访问违例、特定的调试器指令、除零错误。

##中断分发

###硬件中断处理

外部I/O中断进入中断控制器的一根线上,然后中断处理器。处理器获得IRQ,该IRQ由控制器转译IRQ成一个中断号,作为IDT的索引找到IDT项,然后转交给ISR或内部内核例程。

###中断控制器

  • x86:PIC或APIC
  • x64:APIC
  • IA64:SAPIC

IRQL

Windows不用中断控制器的中断优先级,强制使用自身的优先级方案,就是IRQL。

  1. 中断按优先级处理,高优先级抢占低优先级,所以中断是可嵌套的。
  2. 中断执行时,由陷阱分发器提升IRQL,调用服务例程,结束后再降低IRQL。执行过程中会把当前线程的状态保存起来。
  3. 屏蔽低优先级中断,保留直到降级
  4. IRQL的改变只在内核模式进行,用户模式代码永远是PASSIVE_LEVEL。

PCR中保存了当前的IRQL,PRCB中保存了保存的IRQL,在DebuggerSaveIrql。

中断映射到IRQL

中断分配在哪个IRQL上,由HAL决定,因为硬件上并没有什么IRQL。interrupt arbiter完成这一映射。

预置IRQL

  • 内核仅在KeBugCheckEx中停止了系统并屏蔽了所有中断后,才使用高端级别的IRQL。
  • 电源失败从来没真正用过,摆设。
  • 处理器间的中断级别用于向另一个处理器请求执行某个动作,比如更新该处理器的TLB、系统停机或崩溃。
  • 时钟级别用于系统时钟,内核利用该中断跟踪日期时间、线程quantum、CPU分配时间
  • 如果开启了性能剖析,则系统实时时钟会用到性能剖析级别。
  • 同步IRQL级别是内核内部使用,分发器和调度器代码利用该级别来保护对全局线程调度代码和等待/同步代码的访问。一般在设备IRQL级别之后的下一个级别。
  • CMCI级别在CPU或固件通过MCE接口报告了一个虽然严重但是可以纠正的硬件条件或错误时,向OS发出信号。
  • DPC/Dispatch级别和APC级别的中断是由内核和设备驱动程序产生的软件中断。
  • 最低的就是Passive级别,用于普通线程运行。

DPC级别以上不能等待对象(如果等就意味着需要调度另一个线程,而线程的调度设计上却是依赖于DPC级别的软件中断,如果可以随意调度的话,这就矛盾了),也不能使用非换页内存池(跟第一个差不多,只是迭代的更深,因为你访问未驻留内存就会出现缺页异常,此后就需要等待文件系统驱动程序去换入该页面,这就意味着又要调度器去做环境切换(没有用户线程在等待,就给空闲线程),同样违反了调度器调度时机的设计(此时IRQL依然在DPC级别以上),而另一方面,I/O完成通常发生在APC_LEVEL级别,所以即使不要求等待,这也是矛盾的,因为始终卡在DPC级别以上,I/O完成APC根本没机会执行)。

中断对象

为设备驱动注册ISR所用,中断对象包含了所有“供内核将一个设备的ISR与一个特定级别的中断关联起来而需要”的信息,包括三部分:

  • ISR地址
  • 设备中断时所在的IRQL
  • 内核中与该ISR关联的IDT项。

中断对象中从模板KiInterrupTemplate复制过来的代码将调用中断分发器(单一中断对象的中断向量由KiInterruptDispatch处理,多个中断对象间共享的中断向量由KiChainedDispatch处理)。

软件中断

Windows内核也为了各种任务产生软件中断,包括:

  • 激发线程分发
  • 并非时间紧急的中断处理
  • 处理定时器到期
  • 在特定线程的环境中异步执行一个过程
  • 支持异步I/O操作

分发或DPC中断

线程终止或主动进入等待,内核会直接调用分发器,导致环境切换。内核需要重新调度它的时候,会请求分发操作,但这要推迟到当前行为完成后进行,DPC软件中断就可用于此处。

具体展开:

内核访问共享的内核数据结构时,为了同步会提升IRQL到DPC或更高,这就禁止了另外的软件中断和线程分发动作。内核检测到应该进行线程分发时,会请求一个DPC级别中断,但由于当前IRQL在此级别或更高,所以处理器会保留该中断。内核完成当前活动后,IRQL会降低到DPC以下,这时才会看看有没有分发中断正在等待处理。所以,Windows中线程的分发是由软件中断来激活的。

上面的例子也说明了为什么这一IRQL的名称是分发/DPC,实际上线程分发依靠的就是这一级别的中断请求,只是描述上把线程分发单独提了出来,为了不和DPC的概念混淆,实际上本质上就是个DPC中断请求,DPC中断请求是个机制,线程分发可以看成是一个子集,DPC中断当然还应用在其他更多地方,比如上面列举的那几个。

DPC赋予了OS一种能力:产生一个中断并且在内核模式下执行一个系统函数。内核利用DPC来处理定时器到期,线程时限到期后重新调度处理器。

设备驱动程序利用DPC处理中断。为了给硬件中断提供及时的服务,Widnows试图在更多的时间把IRQL保持在设备IRQL级别下。和Linux一样,都是ISR先执行最必要的少量工作相应设备,然后保存中断状态并将数据传输或其他不紧迫的中断处理推迟,在位于DPC IRQL的DPC中执行(对比Linux中的do_softirq(),Windows的do_softirq是利用DPC来实现)。

DPC请求放在处理器的DPC队列末尾,当然DPC是有优先级的,优先级高的会插队。

当处理器IRQL从DPC或更高级别想要降低到某个级别(APC或PASSIVE)时,内核就处理DPC。此时IRQL仍在DPC级别,内核抽干队列,调用每个DPC,此后才可以顺利的降低IRQL到DPC以下,然后该干嘛干嘛。

DPC优先级 DPC定位在ISR处理器上 DPC定位在其他处理器上
低级 DPC队列长度超过了最大DPC队列长度值,或者DPC请求率小于最小DPC请求率 DPC队列长度超过了最大DPC队列长度值,或者系统空闲
中级 总是激发 DPC队列长度超过了最大DPC队列长度值,或者系统空闲
中-高级 总是激发 目标处理器空闲
高级 总是激发 目标处理器空闲

中-高级插在DPC队列尾,高级插在头部,行为处理上没有什么差别。

DPC主要为设备驱动程序提供,但内核自己也用,如处理时限到期事件。系统时钟的每一个滴答,都会产生时钟IRQL级中断。中断处理器对系统时间更新,然后将记录了当前线程已运行时间的计数器递减,当减到0时,线程到期,内核重新调度处理器,因为优先级较低,所以放在DPC/Dispatch IRQL完成。此时时钟中断处理器会把一个DPC插入队列来激活线程分发。

由于DPC优先级永远比用户的线程高,所以如果DPC运行时间很长的话,用户线程就会断断续续,比如声音视频的明显卡顿、键鼠的延迟,Windows提供线程化DPC解决这一问题。

线程化DPC工作方式:在PASSIVE级别上,在一个实时优先级(31)的线程上执行DPC例程。使得该DPC可以抢占绝大多数用户模式线程,但又允许其他的中断、非线程化的DPC、APC和更高优先级的线程来抢占此DPC例程。(说白了就是降级,给个似是而非的实时优先级31来跑,做个平衡)。

线程化DPC可以通过注册表项打开或关闭,默认开启。

APC中断

APC提供了一种在特定用户线程环境中执行用户程序和系统代码的途径。APC可以访问资源对象、等待对象句柄、引发页面错误以及调用系统服务,不像DPC那样受限。

APC由APC对象描述,由内核控制。等待执行的APC驻留于内核管理的APC队列中。DPC队列是处理器相关,而APC队列是特定线程相关,即每个线程都有自己的APC队列。内核接到请求,将APC入队,然后请求一个APC级别的软件中断,当该线程开始执行时,就会执行此APC。

APC分内核模式和用户模式。内核模式APC无需从目标线程处获得“许可”,用户则需要先鉴权。内核APC分普通的和特殊的,特殊的APC在APC级别上执行,且允许APC例程修改某些APC参数;普通的APC在PASSIVE级别上执行,接收被特殊APC例程修改过的参数。

IRQL提升到APC可以禁止普通内核模式APC,也可以通过KeEnterCriticalRegion(设置KTHREAD结构中KernelApcDisable)来禁止,调用KeEnterGuardedRegion(设置调用线程KTHREAD结构中SpecialApcDisable字段)可以禁止特殊内核模式APC。

APC类型 插入行为 交付行动
内核特殊 内核模式APC列表尾 IRQL降下来且线程未在守护区域内,就在APC级别被交付。
内核普通 最后一个特殊APC的正后方 关联的特殊APC执行后,在PASSIVE_LEVEL被交付。
用户普通 用户模式APC列表尾 IRQL降下来且现线程未在临界区内,线程处于可警醒态,在PASSIVE_LEVEL上被交付。
用户普通的线程退出(PsExitSpecialApc) 用户模式APC列表头 如果线程正在可被警醒的用户模式等待,则当返回用户模式时,在PASSIVE_LEVEL上被交付。

执行体使用内核模式APC来完成必须在特定线程地址空间中才能完成的OS任务。

  • 利用特殊的内核模式APC指示某个线程停止执行一个可中断的系统服务,如将一个异步I/O结果记录到一个线程的地址空间中
  • 环境子系统用特殊内核模式APC来让一个线程挂起或终止自身,或者获取或设置它的用户模式执行环境
  • UNIX应用子系统利用内核模式APC来模仿UNIX信号传递被递交给UNIX应用子系统的进程。

内核模式APC还与线程的挂起和终止有关。因为这些操作可以从任意线程中发起,也可以指定其他任意的线程,所以内核使用APC来询问线程环境,以及终止目标线程。设备驱动程序持有锁后,往往阻塞APC,或者进入一个临界区或守护区域,防止这些操作的发生,否则一旦被APC终止,这些所可能永远不会释放。

设备驱动程序也可以使用内核模式APC。

例如,如果有一个I/O操作被激发,线程进入等待态,那么另一个进程中的另一个线程就会被调度到运行态。I/O结束后,必定以某种方式回到原本线程环境,所以可以将I/O操作结果复制到包含该线程的进程地址空间的缓冲区中。I/O系统利用的就是特殊内核模式APC来完成这一过程,除非应用程序使用了SetFileIoOverlappedRange API或者I/O完成端口(数据缓冲区或者是全局内存,或者只有当线程从端口中获得一个完成项后才进行复制)。

ReadFileEx、WriteFileEx、QueueUserAPC用到了用户模式APC。I/O完成后在发起I/O线程里插入APC,线程处于可警醒等待态时,会处理交付的APC。

定时器处理

时钟IRQL级别较高,可以看出很重要。

关联的ISR,主要任务是跟踪记录系统时间(KeUpdateSystemTime例程),第二任务是记录线程执行时间和系统滴答时间(这才有了GetTickCount API,这一任务由KeUpdateRunTime例程完成)。

时钟在间隔倍数上才出发,所以系统时间的最底下几位将是64个已知位置之一。Windows把所有驱动程序和应用程序定时器组织在一组链表中,每一项对应于系统时间的一个可能倍数。这张表称为定时器表,在PRCB(CPU相关)中,使得每个处理器都可以完成它自己的独立的定时器到期处理,无需获得全局锁。

定时器到期也是由时钟中断ISR插入的DPC中断,此外会设置PRCB标志。

一些深入话题:

  • 插入定时器时,处理器的选择
  • 考虑到性能,定时器的合并

异常分发

中断是异步的,异常是同步的。Windows用SEH处理异常。

所有的异常都有预置中断号,对应IDT中表项,每个表项指向某个特定异常的陷阱处理器。这些异常由exception dispatcher内核模块分发,去找一个处理器。

32位使用基于栈帧的SEH处理,SEH链最后是UnExceptionHandler,作为默认处理。64位不在栈帧上,而是对每个函数都编译了一个异常处理表在PE中。

SEH应用于用户模式和内核模式。对比VEH类似SEH,但只在用户模式使用。

异常的分发参考张银奎的《软件调试》以及绿盟的恶意代码分析技术反调试篇上。

系统服务分发

上古时期windows用int 0x2e做系统调用,对应IDT的46号表项,指向系统服务分发器。eax传递的是系统服务号,edx传递参数列表。对应iret中断返回。

此后,硬件升级了,为了提速,windows用sysenter/sysexit指令。eax和edx的意义照旧。而在x64上,windows用syscall指令,eax传递系统调用号,前四个参数放在寄存器中传递(rcx,rdx,r8,r9),其他通过栈。

32位有个系统服务分发表,类似中断分发表,每个表项包含了一个指向系统服务的指针。64位也有该表,但是表内的不是指针,而是系统服务程序相对于该表本身的偏移(为了适配ABI、指令编码格式)。

系统服务分发器KiSystemService将调用者参数从线程用户模式栈复制到内核模式栈中,然后执行该系统服务。

NtCreateFile和ZwCreateFile的联系:

  • NtCreateFile是导出给用户模式的API,会产生陷阱帧,通过sysenter等触发中断。
  • ZwCreateFile是驱动程序所用,它本身就是内核态,而所做的处理就是模拟一个中断栈,然后直接调用KiSystemService。

Windows有两个系统服务表,第三方驱动程序不能通过扩展这些表或插入新的表项来加入自己的服务调用。32位windows的系统服务器分发器通过线程内核结构中的一个指针来定位到这些表;64位的windows通过全局地址来找到这些表。

分发器会有个转译机制:

传说中的SSDT和shadow SSDT。

SSDT定义了Ntoskrnl.exe中实现的核心执行体系统服务。SSDT shadow包含了Windows子系统的内核模式部分(win32k.sys)中实现的windows USER和GDI服务。

这就清楚了ntdll.dll, user32.dll, gdi32.dll的关系。

文章目录
  1. 1. Windows系统机制之陷阱分发
    1. 1.0.1. IRQL
    2. 1.0.2. 中断映射到IRQL
    3. 1.0.3. 预置IRQL
    4. 1.0.4. 中断对象
    5. 1.0.5. 软件中断
  2. 1.1. 定时器处理
  3. 1.2. 异常分发
  4. 1.3. 系统服务分发
,