首页 | 安全文章 | 安全工具 | Exploits | 本站原创 | 关于我们 | 网站地图 | 安全论坛
  当前位置:主页>安全文章>文章资料>网络基础>文章内容
The NT Insider:Stop Interrupting Me -- Of PICs and APICs
来源:xfocus.net 作者:董岩 发布时间:2005-03-17  

The NT Insider:Stop Interrupting Me -- Of PICs and APICs

董岩 译
greatdong_2001@163.com

尽管 Windows 的设计是使其能够运行在多种平台上,而实际上我们大多数都用的是32位的 x86 系统。正如我们在最近一期 The NT Insider (“Don't Call Us – Calling Conventions for the x86”, V10N1)所见到的,在尝试分析 crash dump 时,对 x86 构建调用帧的透彻理解所带来的益处是无与伦比的——特别是在没有符号的时候。在编写驱动程序时,程序员就成了操作系统的一部分,深刻理解操作系统所运行的平台有助于开发和调试。

记住,永远要使用 HAL 来编写平台相关的代码。但这并不意味着就可以对所运行的平台体系一无所知。我们编写了一系列的文章来探索平台的一些内幕,这些内幕都是我们所感兴趣的也是我们认为每一个驱动程序开发人员都应该知道的。下面这篇文章就是这个系列文章中的第一篇。我们假设读者了解设备的基本知识,还知道一些 Windows 驱动程序的东西:中断服务程序、IRQLs 和其它相关的东西。


The Interrupt

要是没有输入输出,CPU 可就没什么事好做了(duh! 没有输入输出的计算机也肯定没什么意思)。当设备状态发生变化时,要么是因为它要传送数据,要么是因为一些其它的外部情况需要人注意,设备设置设备相关的寄存器的某个设备相关的位。为了检测到设备状态的变化,驱动程序要反复测试此位。但是这样效率太低了。另外一种解决办法就是当设备状态变化时它可以产生一个中断将变化异步地通知给系统。通过使用中断,在我们不使用设备时可以忽略设备的存在,因此不用浪费时钟周期进行查询并提高了系统整体的性能。

因为中断是设备和设备驱动开发的不可分割的一部分,所以我们将探索中断的内部运行。同时,我们还希望解释清楚从设备产生中断到设备驱动得到通知这之间的过程。我们的解释是站在驱动开发者的角度的,这也就意味着我们不会无限度地追究硬件的细节。因此,如果读者是搞硬件的,熟悉中断控制器的连接,请不要抱怨我们没有讲到 8259 与 8259A-2 之间的区别、何时及如何对 OCW4 编程或是 CPU 第二次将 INTA 置为有效时作了哪些数据交换。可不是我们不知道哦(当然也不绝对),只是从本文的出发点来看,我们不用管这些东西。


Interrupt Descriptor Table

要理解中断,先理解“中断描述符表(IDT)”可是很重要的。简单讲,IDT 就是一个函数指针的数组。每个数组成员中的函数要么指向一个中断处理程序(在驱动开发里叫 Interrupt Service Routine)要么指向一个异常处理程序。这里我们只关心中断,异常就忽略了。IDT 的索引就是“中断向量”,中断向量是一个 UCHAR 值。注意中断和异常处理程序的数目限制在256个。每个 CPU 都有自己的 IDT,可以通过 WinDBG 的 kdex2x86.idt 命令察看。下面的代码节选自在我的测试系统上使用此命令所得到的输出。

1: kd> !kdex2x86.idt

IDT for processor #0

...

dd: 80ac2ac2 (nt!_KiUnexpectedInterrupt173)

de: 80ac2acc (nt!_KiUnexpectedInterrupt174)

df: 80ac2ad6 (nt!_KiUnexpectedInterrupt175)

e0: 80ac2ae0 (nt!_KiUnexpectedInterrupt176)

e1: 804e0084 (HAL!HalpIpiHandler)

e2: 80ac2af4 (nt!_KiUnexpectedInterrupt178)

e3: 804dfdd8 (HAL!HalpLocalApicErrorService)

e4: 80ac2b08 (nt!_KiUnexpectedInterrupt180)

...

当设备产生中断时,中断向量又是如何成为 CPU 的 IDT 的索引,进而调用了相应的终端服务程序的呢?唔,这时就要提到硬件中断控制器了。下面就来看一下两种 Intel “可编程中断控制器”(PIC)的实现,一种是传统的 8259 PIC,另一种则是更高级的“Advanced PIC”(APIC)。我们还要挖掘一下 Windows 的中断处理机制,使得我们可以明白 PIC 和 APIC 是如何在现实世界中使用的。


The 8259

起初 IBM PC 使用 Intel 的 8259 PIC。8259 只支持单处理器的系统且只提供标号为 IRQ0-IRQ7 的8条“中断请求线”使设备与中断相关联,因此最多只能处理8个中断。之后,系统中加入了第二个 8259,这个 8259 通过三条级联线(CAS0-CAS2)与主 8259 级联起来。第二个 8259 的 INT 线还要连到主 8259 的 IRQ2 上,这样算上新添加的8个 IRQs 中再减去用于连接的一个 IRQ,就有了总共15个 IRQs。Figure 1 就是简化的示意图。


Figure 1

所有现代的主板都要么使用两个物理 8259 芯片,要么使用其它芯片来模拟这两个芯片。好了,历史课到此结束,我们再深入一点儿细节。

每个需要中断的设备都需要得到一个唯一的 IRQ 然后连接到 8259 上。为了产生一个指定 IRQ 的中断,设备要使总线上相应的 IRQ 线有效。

8259 为每个 IRQ 都分配了一个优先级,IRQ0 最高,号越大优先级越低。因此 IRQ0 是最高,IRQ15 最低,对吗?哦,不,也不全对。还记着第二个 8259 连到了 IRQ2 上吗?实际的 IRQ 优先级为:


IRQ0, IRQ1, (now off to the second 8259) IRQ8-IRQ15, (now back to the first 8259) IRQ3-IRQ7

乍一看这种安排也不是那么显然,但如果歪过脑袋来眯缝着眼看似乎还是合理的。

每一个 IRQ 都是“可屏蔽的”,意思就是可以通过编程 8259 的“Interrupt Mask Register”(IMR)可以将其禁用。如果 IRQ 被屏蔽掉,则连到这个 IRQ 的设备中断请求就都被忽略了。再有,高优先级的 IRQ 比低优先级的 IRQ 先得到服务,而且高优先级的 IRQ 还可以打断低优先级的 IRQ。因此若服务 IRQ1 时 IRQ0 又来了,则 IRQ1 中断的处理停止,IRQ0 中断被送往 CPU。

很重要的一点是,尽管存在着硬件优先级,但 Windows 系统并没有实际使用它。Windows 系统通过直接操纵 IMR (见本文 conclustion 处的 sidebar)为 8259 加上了自己的优先级机制。

现在我们知道了设备时如何连接到 8259 以及 8259 是如何连接到一起的,那 8259 又是如何与 CPU 连接的呢?

x86 体系的 CPU 有两条中断线,LINT0 和 LINT1。在 8259 的配置中,LINT1 连到了“Non-Maskable Interrupt”(NMI),当检测到严重的、潜在的、不可恢复的错误时就会产生 NMI。之所以叫做“Non-Maskable Interrupt”是因为没有办法阻断它——除了处理器,没有谁能屏蔽它。引起 NMI 的一个典型的例子就是内存校验错。

LINT0 被用作“Interrupt Input Line”(INTR),它连接在主 8259 的 INT 脚上。8259 通过使 INT 有效来将中断通知给系统。当 CPU 确认后,8259 通过总线向 CPU 发送一个8位的值(之前被 O/S 编程进了 PIC)。这个8位的值就是相应 IRQ 的中断向量。这个中断向量被用作 IDT 的索引来确定中断服务程序(ISR)的地址。然后,CPU 跳转到 ISR,进行服务此中断所需的处理。

唔,看起来还不算很难。但是,要是需要中断的设备多于15个该怎么办呢?那就要共享中断了,这时链接起来的 ISRs 就粉墨登场了。在这种情形下,OS 调用第一个注册到所给中断向量上的 ISR,将中断通知给它。假设此中断是一个 level-triggered 的中断(比如 PCI 总线上的 line-based 的中断),OS 会调用与所给中断向量号相关联的所有的 ISRs 直到找到返回 TRUE 的那个,返回 TRUE 就说明这个中断就是用于此设备的。共享中断存在一些问题,因为写得不好的 ISR 会使系统挂掉。实际上,甚至写得好的 ISR 都有可能挂掉系统。要得到对此问题的解释,可以参阅 http://www.microsoft.com/hwdev/platform/proc/apic.asp 处的文章。再有,8259 不能用在多处理器系统中,这就太过时了,因为如今我们使用的桌面计算机大多都有两颗 CPU。还是用 APIC 吧。


The APIC

通常所说的 APIC 实际上由两个部分:“Local APIC”(LAPIC)和“I/O APIC”(IOAPIC)。系统中每个(逻辑上的)CPU 一般都有一个片上的 LAPIC。因此,若系统有四个 CPU 则有四个 LAPICs(注意,因为是每个逻辑处理器有一个 LPIC,所以如果有两个超线程的 CPU 也会有四个 LPIC)。IOPIC 是 Intel 芯片组的一部分,Pentium IV 及以后的系统可以有任意数量的 IOAPICs。Pentium IV 之前的 CPU 则限制在8个。一个 IOPIC 可以设计为支持最多64条“中断输入线” (Interrupt Input Lines,INTINs),但是大多数标准的系统的 IOAPICs 都是24条 INTINs。INTINs 与 8259 里的 IRQs 作用相同。换句话说,每个产生中断的设备都会取得一个 INTIN。如你所见,在 APIC 的配置中可以使用足够多的 INTINs 来减少 8259 中存在的共享问题。好,为了节约时间,我们来仔细看一下 APIC 的这两部分。


LAPIC

如前所述,LAPIC 一般都在实际的 CPU 上,Pentium 之后的 CPU 上都有。其他厂商的处理器可能也可能不把 APICs 作为 CPU 的一部分。不管怎样,还记得 LINT0 和 LINT1 这两条线吗?在 APIC 的配置中,这些线都联结到 LAPIC,LAPIC 再连接到系统总线(Pentium IV 之前的 Intel CPU 实际有一条单独的 APIC 总线,但我们这里忽略这些硬件细节)。系统中的所有 CPU 都如法炮制,这就使得“处理器间中断”(Interprocessor Interrupts,IPIs)能够通过系统总线从一个 LAPIC 送至另一个 CPU 的 LAPIC,如 Figure 2 所示。

Figure 2 – The LAPIC and IOAPIC

需要注意 LAPIC 中的一个重要的寄存器,即“Task Priority Register”(TPR)。通过将 TPR 设置为一定的值,操作系统可以设置 CPU 运行的优先级。这些优先级从0至15,0 的优先级最低。中断的 INTIN 为一个范围从16至255间的一个向量,当中断从 IOAPIC 送往 LAPIC 时,中断的优先级由下式计算:

priority = ceil(vector / 16)

x86 已经预定义了范围在0至31的向量,因此 OS 从31开始定义设备中断。当请求中断时,如果得到的优先级小于或等于目标处理器 LAPIC 的当前 TPR 值,中断就不会在那个 CPU 上生效。这样 OS 就可以通过控制分配给中断处理程序在 IDT 中的中断向量并通过执行这些处理程序时向 LAPIC 的 TPR 写入相应的值就可以控制中断的优先级。OS 给中断赋的向量值越高,中断的优先级就越高。


IOAPIC

为了让本文相对简单,我们只讨论有24条 INTINs 的单 IOAPIC 的系统。IOAPIC 也连在系统总线上,但并不是直接相连。它实际上连到了一个桥(也是 Intel 芯片组的一部分)上,这个桥又连到了系统总线上(见 Figure 3)。正像 8259,系统中所有需要中断的设备都分别连在总线上的一条 INTIN 线上。但是不同的是,每一条 INTIN 线并没有隐含的优先级。记住,在 APIC 里,优先级是由中断向量号和 LAPIC 里的 TPR 处理的。

在 IOAPIC 里有一个叫“I/O Redirection Table”(IOREDTBL)的寄存器。每一个 INTIN 都有一个64位的 IOREDTBL 表项而且每一个值都详细描述了相应中断。IOREDTABLE 表项描述了此中断的亲缘处理器,此中断是 edge 触发的还是 level 触发的,中断的 polarity,中断是否被屏蔽以及与此中断相关联的向量等等。当然,OS 要负责将相应的值填入此表来使 IDT 和连到 INTINs 的设备的特征相匹配。

关于IOREDTBL 还有一些有意思的问题,比如用来描述每个中断 CPU 亲缘性的位掩码只能是8位宽。唉!我们这里还是不要关心这些复杂的东西们了。我们只限于讨论8个或更少的 CPUs。


Aside: PCI to IOAPIC

在我们讨论中断是如何处理的之前,也许应该先来看一下 PCI 总线是如何连接到 IOAPIC 的。对此问题本身就能写一篇好文章,但是这里只要知道每一条 PCI 中断请求线(PIRQxs)都连到一条 IOAPIC INTIN 线上就足够了。这可以是一个硬布线的连接,也可以是可以通过 BIOS 动态调整的连接。


Handling APIC-Based Interrupts In Windows

到目前为止我所讲到的细节可以用在任意一个使用 PIC 或 APIC 操作系统上。所有的 OSs 都会有一个 IDT,都需要对 PIC 进行编程来反映 IDT,并且都必须有处理中断的 ISRs。因为这是 The NT Insider,本文就不会是大全,除非我们想将所有这些都与 x86 上的 Windows 实际如何处理中断联系起来。由于 8259 即将过时,我们就只讨论 APIC。

OK,那么当一个设备发出中断时到底发生了什么呢?假设有一个 level-triggered 的设备连在 INTIN16 上,OS 为其分配的向量号为 0x42。当设备向要发出中断时,它就令 INTIN16 有效,然后……

INTIN16 的有效会将我们带到 IOAPIC 中用于 INTIN16 的 IOREDTBL。若 IOAPIC 看到该中断未被屏蔽,就会通过桥芯片组在系统总线上产生一条消息。

我们使用简化了的情况,有一个空闲的 CPU 看到了这个中断,然后跳转到 IDT 中索引 0x42 处指向的中断处理程序。记住,这里不用像 8259 那样回到 IOAPIC 察看 ISR 在那个向量上,向量是系统总线上送出的消息的一部分。

因为 CPU 知道我们要处理一个中断,它就在调用由 IDT[0x42] 指向的 Windows general interrupt dispatcher 之前将 EIP、ESP、SS 和 CS 寄存器压栈。

general Windows interrupt dispatcher 将 EBP、EAX、EBX、ECX、EDX、EDI、ESI、ES、DS、FS 或 GS 寄存器压栈。

为了验证以上步骤的正确性,我们在 general dispatcher 压入所有需要的寄存器后在它要调用的函数中的一个函数里设一个断点。结果,对于有 chained ISRs 的 IDT 表项调用的是 KiChainedDispatch 函数,而对 non-chained 的则调用的是 KiInterruptDispatch(这些函数名是用某个不太聪明的反汇编程序得到的:在 ISR 里设一个断点后就能在堆栈上看到它们)。部分堆栈如下:

nt!KiInterruptDispatch+0x89 (FPO: [0,2] TrapFrame @ f42f3c30)

HAL!KfLowerIrql+0x35 (FPO: [0,0,0])

nt!KeSetPriorityThread+0xc2 (FPO: [Non-Fpo])

nt!PspExitThread+0x9c (FPO: [Non-Fpo])

那么到底是怎么会事呢?我们可以看到 generic Windows interrupt handler 调用了 KiInterruptDispatch,但是堆栈上其它的东西是干什么的呢?记住,中断可是在任意时刻都有可能发生的,这个中断就发生在线程退出的时候。我们并没有直接讨论过当中断发生时对于 CPU 上被中断的代码都发生过什么,而现在时机已到。当中断送达时,一个“trap frame”就被创建了,它保存着中断发生前的 CPU 的状态(有读者说到:“Aha!这就是为什么在调用我的 ISR 之前 CPU 会将寄存器压栈!”)。在 WinDBG 里,“.trap”命令会允许我们将我们的上下文设置为所提供的 trap frame 的上下文。我们现在就来用 KiInterruptDispatch 那行的 TrapFrame 旁边的值来做一下……

1: kd> .trap f42f3c30

ErrCode = 00000000

eax=00000000 ebx=00000000 ecx=00000000 edx=ffdff538 esi=81e32da8 edi=00000009

eip=804e049d esp=f42f3ca4 ebp=f42f3cb4 iopl=0 nv up ei pl zr na po nc

cs=0008 ss=0010 ds=01ff es=01ff fs=077a gs=7f30 efl=00000246

HAL!KfLowerIrql+35:

804e049d 3bc8 cmp ecx,eax

1: kd> kb

*** Stack trace for last set context - .thread/.cxr resets it

ChildEBP RetAddr Args to Child

f42f3ca0 80a35ad8 8214c228 81e32da8 00000000 HAL!KfLowerIrql+0x35

f42f3cb4 80bb295a 00e32da8 00000010 81e32da8 nt!KeSetPriorityThread+0xc2

f42f3d40 80bb3360 00000000 00000000 f6a79680 nt!PspExitThread+0x9c


哦,我们现在的所在就是中断发生前的地方!因此我猜当我们从中断返回时,如果我们将 CPU 的上下文恢复成这个 trap frame 的,那就没人知道发生过中断。

一旦保存了所需的寄存器,我们就处于某一个 KiXxxRoutines 中了,Windows 取得中断的优先级。用 Windows 里的话讲,这叫中断的“Interrupt Request Level”(IRQL – 读作 ER-QUEL)。HAL 决定何时赋予设备资源以及赋予设备中断什么样的 IRQL。注意这与设备用来产生中断的物理 INTIN 线可没什么关系,而且也不能更改。设备 IRQL(DIRQL)与设备的中断优先级有关(IRQL 高则中断优先级高),也因此就与填入 LAPIC TPR 的值有关。

Windows 下面要做的就是将当前 CPU 的 IRQL(IRQL 是个 per-CPU 的概念) 提升到 HAL 所赋予的 DIRQL。在此之后,CPU 就只能被更高 IRQL 的中断所中断。

现在我们就在 DIRQL 上了,我们已经保证了不会被同一 CPU 上的相同中断所打断,因此对于当前的 CPU,ISR 的执行是同步的。Windows 现在还必须得到一把锁来保证 ISR 的执行要与系统中的其它 CPU 同步。因为 INTIN 的共享,与此中断相关联的 ISR 可能有多个,在 ISR 执行前每个 ISR 必须得到自己的锁,执行完 ISR 后还要释放该锁。一旦 Windows 得到了该锁,我们就可以执行 ISR 了。

对于 Windows 中的中断,每个 IDT 向量还相应有一个 PKINTERRUPT 对象的链表。因为这个结构体是半公开的,就不能访问此结构体的所有的域,但是可以使用调试器的“dt”命令将其 dump 出来:

1: kd> dt nt!_KINTERRUPT

+0x000 Type : Int2B

+0x002 Size : Int2B

+0x004 InterruptListEntry : _LIST_ENTRY

+0x00c ServiceRoutine : Ptr32

+0x010 ServiceContext : Ptr32 Void

+0x014 SpinLock : Uint4B

+0x018 TickCount : Uint4B

+0x01c ActualLock : Ptr32 Uint4B

+0x020 DispatchAddress : Ptr32

+0x024 Vector : Uint4B

+0x028 Irql : UChar

+0x029 SynchronizeIrql : UChar

+0x02a FloatingSave : UChar

+0x02b Connected : UChar

+0x02c Number : Char

+0x02d ShareVector : UChar

+0x030 Mode : _KINTERRUPT_MODE

+0x034 ServiceCount : Uint4B

+0x038 DispatchCount : Uint4B

+0x03c DispatchCode : [106] Uint4B


可以从结构体的定义看出这些实际上都是由 InterruptListEntry 域链接起来的。而且从这里还可以看到 ISR 所分到的向量、相应的 DIRQL 和 ISR 执行前必须得到的自旋锁。因为我们在上一步已经得到了这个自旋锁,Windows 就会从第一个 PKINTERRUPT 开始并调用其 ISR(结构体的 ServiceRoutine 域)。

ISR 会检查硬件看其是否正在中断,我们假设其正在中断。ISR 会告诉设备停止中断,可能会将一个 DpcForIsr 入队列然后返回 TRUE,指示中断已经处理。记住,我们的中断是 level-triggered 所以处理可以停止,因为我们找到了处理此中断的 ISR。如果 ISR 返回了 FALSE,Windows 就会认为在一个 INTIN 上游多个设备,释放自旋锁并移向链表中的下一个 PKINTERRUPT。然后再获得锁并调用其 ISR,重复此过程直到找到返回 TRUE 的那个。

在 ISR 返回后,Windows 会作以下处理:

。释放 ISR 锁。
。将 CPU 的 IRQL 恢复为中断发生前的 IRQL。
。恢复以前的寄存器并发出一条“Interrupt Return”(IRET)。我们这里使用 IRET 而不是一般的 RET 是因为我们要让 CPU 知道我们是从 ISR 返回的,这样 CPU 会恢复 EIP、ESP、SS 和 CS 寄存器(即恢复我们前面所讨论的的“trap frame”)。

成了!我们刚才追踪了中断的处理,从设备最初通过 IOAPIC 的 INTINs 发出中断一直到相应驱动程序的 ISR 的返回。


Conclusion

关于本文所讨论的话题还有许多可以说的,但是我猜在我解释更多细节之前我必须先得编写 The NT Insider Bathroom Bible。如果读者的兴趣的火焰此刻要熄灭,我强烈建议读者找一份 Intel architecture manuals 并祭起你最爱的调试器。你永远也不会知道你会发现什么!(译注:让我想起了“生活就像一盒巧克力……”好像把读者当成了阿甘!)。


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

WAIT! Doesn’t IRQ == Interrupt Priority?

只在 Windows 下是这样,尽管硬件文档可能说法不同。根据配置,要么用的是 programmable interrupt controller(PIC),要么是 advanced programmable interrupt controller(APIC)。如果读过本文关于此问题的讲解就会知道 APIC 的 INTIN 线并没有隐含的优先级,但是 PIC 的 IRQ 线却有隐含的优先级。这可能会使一些人觉得设备的 IRQ(也就是 PIC 优先级)与 IRQL(设备的中断优先级)有着直接的关系。但是,这是错误的。这种误解很常见,现在是忘掉它的时候了!设备连接到 PIC 的地点、方式以及原因与设备在 Windows 中的设备优先级绝对无关。

例如,假设系统使用的是 PIC。设备 X 连接到 IRQ3,设备 Y 连接到 IRQ7。如果 Windows 使用 PIC 的优先级,则 X 的中断总要优先于 Y 的,因为 X 的 IRQ 优先级更高(硬件中断)。然而,Windows 并没有使用 PIC 的硬件中断优先级来区分中断。所以,IRQ7 的 IRQL 高于 IRQ3 的情况是很正常的。

在 Windows 上,设备的 IRQ 永远不会说明设备中断的紧急程度。不管用的是 PIC 还是 APIC 都是这样的。能改变么?唔,实在是不能。Windows 就是这么干的


 
[推荐] [评论(0条)] [返回顶部] [打印本页] [关闭窗口]  
匿名评论
评论内容:(不能超过250字,需审核后才会公布,请自觉遵守互联网相关政策法规。
 §最新评论:
  热点文章
·北京地区设置ADSL猫(惟帆的 KM3
·关于ip地址分类
·浅谈watchdog timeout出现的原因
·利用snmp实现remote ping
·端口大全(1-100中文)
·从IRQ到IRQL(APIC版)
·使用kgdb调试linux内核及内核模
·基于PassThru的NDIS中间层驱动程
·支持 PS/2 与 USB 的键盘过滤驱
·一种网络劫持分析、调试与编程实
·利用iptables打破电信对路由方式
·绕过Copy-On-Write机制安装全局H
  相关文章
·A Crash Course on the Depths o
·IDASDK对指令操作数的识别
·非API函数检测操作系统类型
·浅析本机API
·深入分析进程PID相同的奥秘
·RSA算法基础--实践
·再谈进程PID相同的深入探究
·利用snmp实现remote ping
·使用ASP下载SQL数据库
·BroNIDS的安装与配置
·利用iptables打破电信对路由方式
·使用kgdb调试linux内核及内核模
  推荐广告
CopyRight © 2002-2022 VFocuS.Net All Rights Reserved