2018-09-08 系统机制——陷阱分发

陷阱分发

    陷阱(trap)指的是这样一种机制,当异常或中断发生时,处理器捕捉到一个执行线程,并且将控制权转移到操作系统中某一固定地址处。在Windows中,处理器会将控制权转给陷阱处理器。所谓陷阱处理器是指与某个特定的中断或异常相关联的函数。

    中断是一个异步事件(可以在任何时候发生),并且与处理器正在执行的任务毫无关系。中断主要由I/O设备、处理器时钟或者定时器产生的,并且可以被启用或者禁用。相反的,异常是一个同步条件,它往往是一个特殊指令执行的结果。内核把系统服务调用也看作异常(不过从技术上讲,它们是系统陷阱)。

    当硬件异常或者中断产生时,处理器将在被中断的线程的内核栈中记录下足够多的机器状态信息,因而可以回到控制流中的该点处继续执行,就好像什么也没有发生过一样。如果该线程在用户模式下继续执行,那么Windows就切换到该线程的内核模式栈。然后,Windows在被中断线程的内核栈上创建一个陷阱帧,并且把线程的执行状态保存到陷阱帧里。

    在大多数情况下,内核安装了前端陷阱处理函数,在内核将控制权转交给与特定陷阱相关的处理函数之前或之后,由这些前端陷阱处理函数来执行一些常规的陷阱处理任务。例如,如果陷阱条件是一个设备中断,则内核的硬件中断陷阱处理器将控制权交给一个由设备驱动程序提供给该中断设备的中断服务例程。


中断分发

    硬件中断处理。在Windows所支持的硬件平台上,外部I/O中断进入到中断控制器的一根线上。该控制器接着在某一根线上中断处理器。处理器一旦被中断,就会询问控制器以获得此中断请求(IRQ)。中断控制器将该IRQ转译成一个中断号,利用该编号作为索引,在一个称为中断分发表(IDT)的结构中找到一个IDT项,并且将控制权传递给恰当的中断分发例程。每个处理器都有单独的IDT,所以,如果合适,不同的处理器可以运行不同的ISR。

    x86中断控制器。 绝大多数x86系统或者依赖于i8259A可编程中断控制器(PIC),或者依赖于i82489高级可编程中断控制器(APIC)的某个变种;今天的计算机都包含APIC。IBM PC体系架构额外定义了一个从PIC,它的中断按多路复用的方式,连接到主PIC的某一根中断线上。这样就提供了总共15根中断线(主PIC上7根,从PIC上8根,这8根中断线通过主PIC上的第8根中断线进行多路复用)。APIC工作在多处理器系统上,有256根中断线。APIC支持一种包含15个中断的PIC兼容模式,并且只将中断递交给主处理器。APIC实际上由几个部件构成:一个接收设备中断的I/O APIC、一些本地APIC,以及一个与i8259A兼容的中断控制器。

    x64中断控制器。因为x64体系架构与x86操作系统兼容,所以x64系统必须提供与x86同样的中断控制器。然而,一个重要的区别是,Windows的x64版本将无法运行于不具有APIC的系统上,因为它们使用APIC进行中断控制。

    IA64中断控制器。IA64体系机构依赖于精简的高级可编程中断控制器(SAPIC),这是APIC的一个演化。即使固件中有中断传送和负载均衡的功能,Windows也不使用,而是采用轮转方式静态的将中断分配给相应的处理器。

    软件中断请求级别(IRQL)。虽然中断控制器已经实现了中断优先级,但是Windows仍然强制使用它自己的中断优先级方案,称为中断请求级别(IRQL)。在x86系统上,内核内部使用0~31的数值来表示IRQL;而在x64和IA64系统上,内核采用0~15的数值来表示IRQL。数值越大,代表中断优先级越高。每个处理器的IRQL设置决定了该处理器可以接收哪些中断。IRQL也被用来实现对某些内核模式数据结构的同步访问。内核模式的线程运行时,它通过调用KeRaiseIrql或KeLowerIrql直接的提升或降低处理器的IRQL。当中断源的IRQL高于处理器当前的级别,则从该源处发生的中断会打断处理器;如果中断源的IRQL等于或者低于当前级别,则这样的中断会被屏蔽,知道有一个正在执行的线程降低IRQL级别为止。因为访问PIC是一个相对比较慢的操作,所以,那些要求访问I/O总线来改变IRQL的HAL,比如PIC和32位ACPI系统的HAL,它们实现了一种性能优化,称为延迟IRQL。每个中断级别都有特定的目的,例如,内核发出一个处理器间的中断(IPI),以请求另一个处理器执行某个动作,比如调度一个特定的线程使其执行,或者更新它的地址转译快查缓冲区(TLB)的缓存。

    将中断映射到IRQL。IRQL级别与中断控制器定义的中断请求(IRQ)并不相同。在Windows中,一种称为总线驱动程序的设备驱动程序用以确定它的总线上出现了哪些设备,以及哪些中断可以分配给每一个设备。总线驱动程序将这些信息汇报给即插即用管理器,即插即用管理器在考虑所有其他设备的可接受的中断分配方案以后,确定为每个设备分配哪个中断。然后,它调用即插即用中断仲裁者,由它将中断映射到对应的IRQL。在非ACPI系统上使用根仲裁者,而在ACPI兼容的系统上,ACPI HAL有它自己的仲裁者。

    预定义的IRQL。从最高级别开始,预定义的IRQL使用情况如下:

        内核仅当它在KeBugCheckEx中停止了系统并屏蔽了所有中断时,才会使用高端级别的IRQl。

        电源失败级别最初出现在WindowsNT的原始设计文档中,该文档指定了系统电源失败代码的行为,但是此IRQL从来没有被真正用到过。

        处理器间的中断级别被用于向另一个处理器请求执行某个动作,比如更新该处理器的TLB缓存、系统停机或者系统崩溃。

        时钟级别主要用于系统的时钟,内核利用该终端级别来跟踪日期和时间,以及为线程测量或分配CPU时间。

        当内核的性能剖析功能被打开时,系统的实时时钟就会用到性能剖析级别。当内核的性能剖析功能被激活时,内核的性能剖析陷阱处理器就会记录下中断发生时被执行代码的地址。随着时间的推移,一张地址采样表会建立起来。

        同步IRQl级别是内核内部使用的,分发器和调度器代码利用该级别来保护对全局线程调度代码和等待/同步代码的访问。通常被定义为紧跟在设备IRQL之后的最高IRQL级别。

        设备IRQL用于对设备中断进行优先级区分。

        CMCI(可纠正的机器检查中断)级别用于在下述条件下向操作系统发出信号:当CPU或固件通过MCE(机器检查错误)接口报告了一个虽然严重但是可以纠正的硬件条件或错误时。

        DPC/Dispatch级别和APC级别的中断是由内核和设备驱动程序产生的软件中断。

        最低的IRQL级别为被动级别,它实际上根本不是一个中断级别,它是普通线程运行时的设置,此时所有的中断都允许发生。

    中断对象。内核提供了一种可移植的机制,使得设备驱动程序可以为它们的设备注册ISR,这种机制就是一个被称为中断对象的内核控制对象。中断对象中包含了所有“供内核将一个设备的ISR与一个特定级别的中断关联起来而需要”的信息,包括该ISR的地址、该设备中断时所在的IRQL,以及内核中与该ISR关联的IDT项。中断对象被初始化时,少量的汇编语言代码指令(称为分发代码)将被从中断处理模板KiInterruptTemplate中复制过来,保存在该对象中。当中断发生时,这些代码将被执行。在x64 Windows系统上,内核优化了中断分发过程,其做法是,使用一些特殊的例程,通过忽略一些不必要的功能来节省处理器的指令开销。比如,针对性能中断或性能剖析中断,使用KiInterruptDispatchLBControl处理器,它支持现代处理器所提供的最后分支控制MSR。另一个内核中断处理器是KiFloatingDispatch,它用于那些要求保存浮点状态的中断。浮点分发例程仅在32位系统上被支持。中断对象的ISR的地址保存在ServiceRoutine域中(正如在!idt的输出中所显示的):中断发生时,实际执行的中断代码被存放在该中断对象末尾的DiapatchCode数组中。这里存储的中断代码编写如下:首先在栈中建立起陷阱帧,然后调用在DispatchAddress域中存储的函数,并且将指向该中断对象的指针传递给它。


Windows和实时处理过程

    因为Windows并不以任何一种可控的方式对设备IRQ进行优先处理,并且用户应用程序只能在处理器的IRQL为被动级别的时候执行,所以,Windows往往不适合用作实时操作系统。尽管如此,有一些第三方厂商为Windows提供了实时内核。这些厂商使用的办法是,将实时内核嵌在一个自定义的HAL中,并且将Windows当做实时操作系统中的一个任务来运行。

    将一个ISR与某个特定级别的中断关联起来,这一操作称为连接中断对象,而将一个ISR与一个IDT项断开关联,则称为断开中断对象。这些操作是通过调用内核函数IoConnectInterruprEx和IoDisconnectInterruprEx来完成的,正是这两个操作,使得设备驱动程序可以在加载到系统中的时候“打开”一个ISR,而如果驱动程序被卸载,则可以“关闭”该ISR。

    使用中断对象注册ISR,可以避免设备驱动程序直接操纵中断硬件,也可以不必知道任何有关IDT的细节。这一内核特性有助于创建可移植的设备驱动程序,因为它使得开发人员无需使用汇编语言来编码,也无须在设备驱动程序中反映处理器的差异。

    如果共享同一中断的多个设备同时请求服务,那么,有些设备,由于其ISR尚未被服务到,一旦中断分发器降低了IRQL,它们将再次中断系统。只有当所有想要使用同一中断的设备驱动程序向内核表示它们可以共享该中断时,这种链式结构才被允许;如果它们未能向内核做出这种表态,则即插即用管理器会重新组织它们的中断分配方案,以确保尊重每一个设备的共享需求。


基于线的中断与基于消息信号的中断

    共享的中断通常是高中断延迟的来源,也可能引起稳定性问题。它们往往并不是期望的行为,而是一台计算机中因为物理中断线数量有限而导致的副作用。通过IRQ线来产生中断的其他问题还有,不正确地管理IRQ信号可能会导致机器上发生中断风暴或者其他种类的死锁,因为只有当ISR确认了IRQ信号以后才会驱动该信号“高”或“低”。如果由于出现错误,而导致这两者都没有发生,那么系统可能永久地处于一种被破坏的状态,更进一步中断可能被屏蔽,或两者兼而有之。最后,在多处理器环境中基于线的中断也会带来更差的可伸缩性。

    所有这些问题的一个解决方案是,在PCI22标准中率先引入的一种新的中断机制,称为基于消息信号的中断。尽管这仍然是该标准的一个可选组件,在客户机器上很少能找得到,但是,越来越多的服务器和工作站实现了MSI支持,并且所有最新的Windows版本完全支持MSI。在MSI模型中,一个设备通过往一个特定的内存地址写数据的做法,来向它的驱动程序递交消息。这一写操作会引发一个中断,然后Windows调用ISR,把消息的内容和该消息被递交的地址传递给ISR。设备也可以递交多个消息(可多达32)到该内存地址上,根据事件来递交不同的载荷部分。

    因为这一通信建立在内存值的基础上,并且,因为内容随着中断一起被递交,所以,对IRQ线的需求不再必要,而且,驱动程序ISR也不必再向设备查询与该中断有关的数据,从而降低了延迟。

    最后,MSI模型有一个扩展,称为MSI-X,它是在PCI 3.0中引入的,增加了对32位消息的支持,允许最多2048个(而不仅仅32个)不同的消息,更重要的是,每个MSI载荷可以使用不同的地址(可以动态决定)。这使得MSI载荷可以写到不同的物理地址范围中,这些地址范围分别属于不同的处理器,或者不同的目标处理器集合,而这可以有效地支持NUMA架构的中断递交,将中断发送给相应的发起了设备请求的那个处理器。通过在中断完成过程中监视负载和最近的NUMA节点,可以改进中断的延迟和可伸缩性。


中断亲和性和优先级

    在既支持ACPI又包含APIC的系统上,Windows允许驱动程序开发人员和管理员在一定程度上控制处理器亲和性(选择一个处理器或一组处理器允许接收相应的中断)和亲和性策略(如何选择处理器,以及在一个组中选择哪些处理器)。更进一步,它支持一种基于IRQL选择的中断优先级机制。

    Windows不是一个实时操作系统,同样的,这些IRQ优先级只是给系统的提示,它们仅仅控制了与中断相关联的IRQL,而没有提供除Windows的IRQL优先级方案外的额外优先级。因为IRQ优先级也被存储在注册表中,所以,对于那些没有充分利用此特性的驱动程序,若它们要求更低的延迟,管理员可以尽管设置。

    软件中断。虽然大多数中断是硬件产生的,但是,Windows内核也为了各种各样的任务而产生软件中断,这样的任务包括:激发线程分发,并非时间紧急的中断处理,处理定时器到期,在特定线程的环境中异步地执行一个过程,支持异步I/O操作。

    分发或者延迟过程调用(DPC)中断。当线程不能继续往下执行时,比如它已经终止或者主动进入等待状态,内核就会直接调用分发器,从而立即导致环境切换。然而,有时候,内核检测到它已经深入到许多层代码中,这时应该进行重新调度。这种情况下,内核请求分发操作,但将它推迟到完成当前的行为以后再进行。使用DPC软件中断是实现这种延迟的一种便捷途径。

    内核在需要对共享的内核数据结构的访问进行同步时,总是将处理器的IRQL提升到DPC/Dispatch或更高级别。这禁止了另外的软件中断和线程分发动作。当内核检测到应该进行线程分发时,它会请求一个DPC/Dispatch级别的中断;但是,因为IRQL是在此级别或更高级别上,所以处理器将保留住该中断。利用软件中断来激活线程分发器,这是一种将分发过程推迟到条件合适时才进行的方法,然而,Windows也使用软件中断来推迟其他类型的处理过程。

    除了线程分发之外,内核也在此IRQL上处理延迟的过程调用(DPC)。DPC是完成某项系统任务的函数——这一系统任务不像当前任务那样时间紧迫。这些函数之所以被称为延迟的,是因为它们可能不会立即执行。

    DPC赋予了操作系统这样一种能力:产生一个中断并且在内核模式下执行一个系统函数。内核利用DPC来处理器定时器到期(并解除那些正在等待定时器的线程),以及在一个线程的时限到期后重新调度处理器。

    DPC是通过DPC对象来表示的,DPC对象是一种内核控制对象,它对于用户模式程序是不可见的,但是对于设备驱动程序和其他的系统代码则是可见的。DPC对象所包含的最重要的信息是,内核处理该DPC中断时将要调用的那个系统函数的地址。正在等待执行的DPC例程被存储在由内核管理的队列中,每个处理器都有一个这样的队列,称为DPC队列。要请求一个DPC,系统代码需要调用内核来初始化一个DPC对象,然后把它放到DPC队列中。

    目标定在某个特定CPU上的DPC被认为是定向的DPC,如果一个DPC是高优先级的,则内核将该DPC对象插在队列的前端;否则,对于所有其他的优先级,内核把DPC对象放在队列的末尾。

    内核通常通过DPC/Dispatch级别的中断来激发DPC队列的“抽干”动作。只有当一个DPC被定位在发出ISR请求的处理器上,并且该DPC的优先级高于低级时,内核才会产生这样一个中断。如果该DPC的优先级是低级,则只有当针对该处理器并且尚未处理的DPC请求的数量在某个阈值以上,或者在一个时间窗口内该处理器上的DPC请求的数量很少时,内核才会请求中断。如果DPC被定位于并不在运行其ISR的CPU之上,并且该DPC的优先级是高级或者中-高级,那么,内核将立即用信号通知目标CPU(给它发送一个分发IPI),以便“抽干”它的DPC队列,但只有当目标处理器空闲时才可以。如果优先级为中级或者低级,则目标处理器上排队的DPC数量必须超过一定阈值,这样内核才会激发DPC/Dispatch中断。

    因为DPC执行时根本不管系统中当前哪个线程在运行,所以,它们也往往是客户机系统或者工作站系统招致用户能感知到不响应的一个主要原因,因为即使最高优先级的线程也会被待处理的DPC打断。有些DPC运行时间很长,因而用户可能会感觉到视频或声音时断时续,甚至鼠标和键盘出现非正常的延迟,所以,针对需要长时间运行DPC的驱动程序,Windows提供了线程化DPC。

    线程化DPC的工作方式是,在被动级别上,在一个实时优先级的线程上执行DPC例程。这使得DPC可以抢占绝大多用用户模式线程,但又允许其他的中断、非线程化的DPC、APC和更高优先级的线程来抢占此DPC例程。

    异步过程调用(APC)中断。异步过程调用提供了一种在特定用户线程环境(从而也是在特定的进程地址空间)中执行用户程序和系统代码的途径。因为APC被插入队列,以便在特定线程环境执行,并且运行在低于DPC/Dispatch级别的IRQL上,所以,它们不需要在像DPC那样的限制下工作。APC例程可以访问资源、等待对象句柄、引发页面错误,以及调用系统服务。

    APC是由称为APC对象的内核控制对象来描述的。正在等待执行的APC驻留在由内核管理的APC队列中,DPC队列是系统范围的,与此不同,APC队列是与特定线程相关的,即每个线程有它自己的APC队列。当内核接到请求,要将APC排队时,它会将该APC插入将来执行此APC例程的那个线程的队列中。然后,内核请求一个APC级别的软件中断,当该线程最终开始运行时,它就会执行此APC。

    有两种APC类型:内核模式和用户模式。内核模式的无须目标线程的干涉或者同意,就可以中断该线程并执行一个过程。内核模式的APC也有两种:普通的和特殊的。特殊的APC在APC级别上执行,并且允许APC例程修改某些APC参数;普通的在被动级别上执行,并且接收被特殊APC例程修改过的参数。

    执行体使用内核模式的APC来完成那些必须在特定线程的地址空间才能完成的操作系统任务。它可以利用特殊的内核模式APC来指示某个线程停止执行一个可中断的系统服务。

    内核模式APC的另一个重要用途与线程的挂起和终止有关。因为这些操作可以从任意的线程中发起,也可以指定其他任意的线程,所以,内核使用APC来询问线程环境,以及终止目标线程。

    有几个Windows API,比如ReadFileEx、WriteFileEx和QueueUserAPC,用到了用户模式APC。只有当一个线程处于可警醒的等待状态时,用户模式APC才可以交付给该线程。内核模式的APC运行在APC级别上,与此不同,用户模式的APC运行在被动级别上。

    APC的交付可能会导致等待队列的重新排序,这里,等待队列是指哪些线程正在为了什么而等待,以及它们等待的顺序。如果当一个APC被交付时,线程正处于等待状态,则在APC例程完成以后,此等待将被重新发起或者重新执行。如果等待尚未被解决,则线程返回到等待状态,但现在,在它所等待的对象中,它位于等待列表的末尾。


定时器处理

    Windows对系统时钟进行编程,以便使机器按照最合适的间隔来激发中断,继而允许驱动程序、应用程序和管理员根据需要修改时钟间隔。通常,系统时钟或者由PIT(可编程的中断定时器)芯片来维护,或者由RTC(实时时钟)来维护。PIT的工作基础是一个石英晶体,它被调整为1/3NTSC彩色载波频率,HAL利用各种可以实现的复合技术来达到毫秒级的时间间隔。另一方面,RTC运行在32.768KHz的频率上,由于是2的幂次,因此比较容易配置成在2的幂次的各种时间间隔上工作。

    有些类型的Windows应用程序要求非常快的响应时间,比如多媒体应用程序。事实上,有些多媒体任务要求低于1ms的时钟中断率。由于这个原因,Windows实现了一组API和相应的机制允许降低系统时钟中断的间隔,但这会导致更多的时钟中断。

    Windows在可能的情况下都尽量将时钟定时器恢复到原始值。每当一个进程请求改变时钟定时器时,Windows都增加一个内部的引用计数,并将此计数与该进程关联起来。类似地,驱动程序(也可能会改变时钟率)被加到一个全局的引用计数上。当所有的驱动程序都已恢复时钟,所有修改过时钟的进程都已退出或者恢复了时钟时,Windows将时钟恢复到默认的值。

    定时器到期。对于RTC或PIT所产生的中断,与之关联的ISR主要任务之一是,跟踪记录系统时间,主要是由KeUpdateSystemTime例程来完成的。它的第二项任务是:跟踪记录逻辑运行时间,比如进程/线程执行时间和系统嘀嗒时间,开发人员利用诸如GetTickCount之类的API,在其应用程序中对各种操作进行计时时,这些API用到的底层数值即系统嘀嗒时间。这部分工作是由KeUpdateRunTime来完成的。然而,KeUpdateRunTime在做这些工作以前,会首先检查是否有定时器已经到期。

    Windows定时器可以是绝对定时器,这意味着将来某个确定的到期时间;也可以是相对定时器,其中包含一个负的到期值,用作从定时器插入时刻的当前时间开始一个正的偏移值。在系统内部,所有的定时器都被转换成一个绝对的到期时间。

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

    因为定时器是通过手链接在一起的,所以到期代码会从头到尾解析一遍该链表。一个定时器到期,无外乎两种主要任务:定时器被看作分发器同步对象(线程正在等待此定时器,作为超时的一部分,或者直接作为一次等待),等待测试和等待满足算法将在此定时器上运行;定时器被看做控制对象,与DPC回调例程关联在一起,当定时器到期时,此回调例程被执行,这种方法仅保留给驱动程序使用,并且对定时器的到期动作可以做到非常低的延迟。

    选择处理器。在插入定时器时,必须选择使用哪个表即选择一个最恰当的处理器。如果该定时器没有与之关联的DPC,那么,内核扫描当前处理器所在组中尚未停运的所有处理器。如果当前处理器已停运,则从该组中选择下一个处理器,否则使用当前处理器。另一方面,如果定时器有一个关联的DPC,那么插入代码只是简单的看一看与该DPC关联的目标处理器,并且选择该处理器的定时器表。

    定时器合并。在没有定时器到期的时间段里将时钟中断最少化从而使处理器睡眠,这样可以显著提升状态间隔的长度,但是,由于定时器的粒度是15ms,许多定时器可能将被加到指定手的队列中,并且频繁的到期。降低软件定时器到期工作的数量既可以帮助降低延迟,也可以让其他处理器待在睡眠状态更长时间。为了充分优化空闲时间的持续长度,内核需要采用一种合并机制来把分散的定时器手组合到一个单独的手中,让它处理多个定时器到期。

    定时器合并能工作的前提条件是,绝大多数驱动程序和用户模式应用程序并不特别在意其定时器的确切激发周期。周期性的定时器设置了可容忍的延迟后,在定时器被对齐到最优的周期间隔倍数之前,Windows会使用一个称为“移转”的过程,这一过程会导致定时器在周期之间漂移。


异常分发

    中断可以在任何时候发生,与此不同的是,异常是直接由当前正在运行的程序在执行过程中产生的条件。Windows使用了一种称为结构化异常处理的设施,使得应用程序可以在异常发生时获得控制。然后应用程序可以修正此条件,并返回到异常发生处,将栈展开;或者,向系统报告该异常不可识别,因而系统应该继续搜索一个有可能处理此异常的异常处理器。

    在x86和x64处理器上,所有的异常都有预定义的中断号,直接对应于IDT中的表项,而每个表项又指向每个特定异常的陷阱处理器。

    所有的异常,除了那些简单到直接由陷阱处理器就可以解决的以外,都是由一个称为异常分发器的内核模块来服务的。异常分发器的任务是找到一个能够处置该异常的异常处理器。有些异常也允许原封不动的滤回用户模式。例如,有些特定类型的内存访问违例或者算术溢出产生的异常,操作系统不会对它们进行处理。32位应用程序可以建立起基于帧的异常处理器来处理这些异常。“基于帧”是指将一个异常处理器与一个特定过程的激活动作关联起来。当一个过程被调用时,代表该过程激活动作的“栈帧”被压到栈中。一个栈帧可以关联一个或者多个异常处理器,每个异常处理器包保护源程序中一块特定的代码。对于64位应用程序,结构化异常处理机制并不使用基于帧的处理器,而是对于每个函数,有一张异常处理器表被编译到映像模块中,内核检查与每个函数相关联的处理器。另一种异常处理机制被称为向量化的异常处理。这种方法仅被用于用户模式应用程序。

    如果在内核模式中发生了异常,则异常分发器只是简单的调用一个例程来找到一个基于帧的异常处理器,将来由它处理该异常。因为未处理的内核模式异常被认为是严重的操作系统错误,所以可以假定,分发器总会找到一个异常处理器。但对于有些陷阱,并不能找到异常处理器,这样的严重错误会导致代码为UNEXPECTED_KERNEL_MODE_TRAP的错误检查。

    调试器断点是常见的异常来源,因此,异常分发器采取的第一个动作是,看一看引发该异常的进程是否有一个相关联的调试器进程。如果有,那么,异常分发器向该进程关联的调试对象(在系统内部称为一个端口),发送一个调试器对象消息。如果没有,那么,异常分发器切换到用户模式下,将陷阱帧按照CONTEXT数据结构的格式复制到用户栈中,并且调用例程来找到一个结构化的或向量化的异常处理器。如果没有找到,则异常分发器切换回内核模式下,并且再次调用调试器,以便让用户做更多的调试工作。如果调试器不在运行,并且没有找到用户模式异常处理器,那么,内核向与该线程的进程关联在一起的异常端口发送一个消息。该异常端口如果存在,则是由控制该线程的环境子系统注册的。该异常端口使得环境子系统有机会将一个异常转译成一个与环境相关的信号或者异常。然而,如果内核在处理该异常的过程中已经向前走的很远了,而且子系统并没有处理该异常,那么,内核就向系统全局的错误端口发送一个消息,并且执行一个默认的异常处理器,它只是简单的将引发该异常的线程所在的进程终止掉。

    未处理的异常。所有的Windows线程都有一个异常处理器,负责处理所有未被处理的异常。该异常处理器是在Windows内部的线程启动函数中声明的。线程启动函数是在用户创建进程或者任何额外线程的时候运行的,它调用由环境子系统提供的线程启动例程,这个例程在初始的线程环境结构中指定,而这个线程启动例程又调用在CreateThread中由用户提供的线程启动例程。如果一个线程的异常没有被处理,则Windows的未处理异常过滤器将被调用。此过滤器函数的用途是,当一个异常未被处理时,为其提供一种由系统定义的行为:启动WerFault.exe进程。然而,在默认配置中,WindowsErrorReportingService服务将处理该异常,所以,未处理异常过滤器永远不会被执行。

    Windows错误报告。Windows错误报告是一项极为复杂的机制,它可以自动提交用户模式的进程崩溃和内核模式的系统崩溃。当一个未处理的异常被未处理异常过滤器捕捉到时,它会首先建立起执行环境信息,并打开一个ALPC端口,连接到WER服务上。于是WER服务开始分析崩溃程序的状态,并执行恰当的动作来通知用户。在默认配置的系统上,将向Microsoft的在线崩溃分析服务发送一个错误报告。最终,这一服务将获得一个针对该问题的解决方案,它向用户显示一条提示消息,告诉用户采取怎样的步骤来解决此问题。如果未处理异常过滤器本身崩溃,则Windows的WER机制将在被崩溃的线程之外执行前述工作,这使得任何种类的进程或线程崩溃都可以被记录下来,并且相应的通知到用户。

    WER服务通过NtSetInformationProcess来注册全局错误端口,结果是,所有的Windows进程都有了一个错误端口,而该端口实际上是由WER服务注册的一个ALPC端口对象。


系统服务分发

    系统服务分发。在Pentium II之前的x86处理器上,Windows使用int 0x2e(46)指令,它会导致一个陷阱。Windows填充IDT的46号表项,使其指向系统服务分发器。该陷阱导致执行线程转换到内核模式中,并且进入系统服务分发器。EAX寄存器中传递的数值参数指明了所请求的系统服务号,EDX寄存器指向调用者传递给该系统服务的参数列表。要回到用户模式,系统服务分发器需要使用iret指令(即中断返回指令)。

    在更高级的处理器上,Windows使用专门的sysenter指令,这是Intel特别为快速系统服务分发而定义的指令。Windows在引导时将内核的系统服务分发器例程的地址保存在与该指令相关联的一个MSR中。该指令一旦被执行,就会导致变换到内核模式下,并且执行系统服务分发器。为了返回到用户模式,系统服务分发器通常执行sysexit指令。在有些情况下,如当处理器上单步标志被打开时,系统服务分发器改而使用iret指令,因为sysexit不允许返回到用户模式时有不同的EFLAGS寄存器。

    在x64体系架构上,Windows使用syscall指令,系统调用号通过EAX传递,前四个参数在寄存器中传递,剩下参数放在栈中。在IA64体系架构上,Windows使用epc(Enter Privileged Mode)指令,前八个系统调用参数在寄存器中传递,余下的放在栈中传递。

    内核模式下系统服务分发。内核利用系统调用号,在系统服务分发表中找到相应的系统服务信息,在32位系统上,系统服务分发表类似于中断分发表,只不过在这里每个表项都包含了一个指向系统服务的指针,而不是指向中断处理例程。在64位系统上,该表的实现略有不同——它包含的不是指向系统服务的指针,而是相对于该表本身的偏移。

    系统服务分发器KiSystemService将调用者的参数从线程的用户模式栈中复制到内核模式栈中,然后执行该系统服务。内核使用了第二个表,称为参数表,参数表是一个字节数组,每一项描述了要复制的字节数。在64位系统上,Windows通过一个称为系统调用表缩紧的过程,将这一信息实际编码在服务表内部。如果传递给一个系统服务的参数指向了用户空间中的缓冲区,那么,内核模式的代码在复制数据时,要先检查这些缓冲区是否可以访问。只有当线程的原先模式属性被设置为用户模式,才会执行缓冲区检查工作。

    内核模式代码也可以进行系统调用。这里不需要中断或sysenter操作,CPU已经在正确的特权级上了。内核可以访问所有它自己的例程,只需简单的调用即可,然而,对于外部的情形,只有当这些系统调用被导出以后,驱动程序才能访问它们。实际上,只有少数系统调用被导出。

    Zw版本实质上仅被用于驱动程序。因为这一原先模式值只有在内核每次建立陷阱帧的时候才会更新,所以,在一个简单的API调用内部,这一值实际上不会发生变化,若直接调用Nt版本函数,内核保存的原先模式值指明了它是用户模式下的,但又检测到传递过来的地址是一个内核模式地址,于是,该调用失败。然而,这并非实际发生的情形。导出的API实际上并不是Nt版本的简单别名或包装函数,而是对应的Nt系统调用的“翻版”,使用了同样的系统调用分发机制。建立一个假的中断栈,并直接调用KiSystemService例程,实质上是在模拟CPU中断。系统服务分发器将执行同样的操作,就好像此调用来自于用户模式,只不过它会检测到该调用的实际特权级,并且设置原先模式为内核。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容