一文講透!Windows內(nèi)核 & x86中斷機(jī)制詳解
搞內(nèi)核研究的經(jīng)常對(duì)中斷這個(gè)概念肯定不陌生,經(jīng)常我們會(huì)接觸很多與中斷相關(guān)的術(shù)語,按照軟件和硬件進(jìn)行分類:
硬件CPU相關(guān):
IRQ
IDT
cli&sti
軟件操作系統(tǒng)相關(guān):
APC
DPC
IRQL
一直以來對(duì)中斷這一部分內(nèi)容弄的一知半解,操作系統(tǒng)和CPU之間如何協(xié)同工作也是很模糊。最近花了點(diǎn)時(shí)間認(rèn)真把這塊知識(shí)進(jìn)行了梳理,不當(dāng)之處,還請(qǐng)高手指出,先行謝過了!
本文旨在解答下面這些問題:
IRQ和IRQL之間是什么關(guān)系?
Windows是如何在軟件層面上虛擬出IRQL這套中斷機(jī)制的
APC和DPC都是軟件中斷,既然是中斷那么對(duì)應(yīng)的IDT表項(xiàng)中的處理例程在哪里呢?
0x00 Intel 80386處理器的中斷
首先,讓我們忘記Windows,從最開始的80386處理器開始,看看Intel設(shè)計(jì)它的時(shí)候是如何處理中斷這個(gè)東西的。
先來看看這個(gè)誕生于1985年的CPU長什么樣子:

看看那些伸出來的引腳,下面是它的引腳標(biāo)注圖:

注意用紅圈標(biāo)注的兩個(gè)引腳,這兩個(gè)就是80386處理器為中斷留出的兩個(gè)引腳。其中INTR是可屏蔽中斷輸入口,NMI是不可屏蔽中斷輸入口。
那么中斷是如何輸入給處理器的呢?那么多外部設(shè)備,而這只有一個(gè)引腳(暫時(shí)只考慮可屏蔽中斷),這里就需要為CPU配備一個(gè)管理中斷的秘書——可編程中斷控制器PIC。這個(gè)秘書需要干哪些活呢?外部設(shè)備的中斷都從它來進(jìn)入中央處理器,所以它負(fù)責(zé)從外設(shè)接收中斷信號(hào),并根據(jù)優(yōu)先級(jí)向CPU發(fā)起中斷請(qǐng)求。最開始的這個(gè)PIC角色是一個(gè)代號(hào)為8259A的芯片在進(jìn)行扮演,這貨長這樣:

下面是它的引腳圖:

其中IR0-IR7共8個(gè)引腳負(fù)責(zé)連接外部設(shè)備, 8259A PIC的每個(gè)IR口都連接著一條IRQ線,用于接收外設(shè)的中斷信號(hào)。INT負(fù)責(zé)連接CPU的INTR引腳,用于向CPU發(fā)起中斷請(qǐng)求。通常情況下,使用兩片8259A芯片進(jìn)行級(jí)聯(lián),一片連接CPU,稱為主片,另一片連接到主PIC的IR2引腳,稱為從片,這樣總共就可以連接8+7=15個(gè)外設(shè)了。如下圖所示:

在8259A中,默認(rèn)情況下的優(yōu)先級(jí)是主片IR0的中斷請(qǐng)求優(yōu)先級(jí)最高,主片IR7最低,從片IR0-7所有中斷請(qǐng)求優(yōu)先級(jí)都相當(dāng)于IR2。所以IRQ線的優(yōu)先級(jí)由高到低次序?yàn)镮RQ0,IRQ1,IRQ8-15,IRQ3-7。這是默認(rèn)情況,可以通過編程改變。
在8259a芯片內(nèi)部有幾個(gè)重要的寄存器:
中斷請(qǐng)求寄存器: IRR,8bit,對(duì)應(yīng)IR0-IR7,當(dāng)對(duì)應(yīng)引腳產(chǎn)生中斷信號(hào)時(shí),該bit位置1。
中斷服務(wù)寄存器: ISR,8bit,對(duì)應(yīng)IR0-IR7,當(dāng)對(duì)應(yīng)引腳的中斷正在被CPU處理時(shí),該bit位置1。
中斷屏蔽寄存器: IMR,8bit,對(duì)應(yīng)IR0-IR7,當(dāng)對(duì)應(yīng)位為1時(shí),表示屏蔽該引腳產(chǎn)生的中斷信號(hào)。
還有一個(gè)中斷優(yōu)先級(jí)判決器: PR,當(dāng)中斷引腳有信號(hào)時(shí),結(jié)合這次產(chǎn)生中斷的IRQ號(hào)和ISR中記錄的當(dāng)前正在處理的中斷信息,根據(jù)優(yōu)先級(jí)來決定是否把這個(gè)新的中斷信號(hào)報(bào)告給CPU,以此來產(chǎn)生中斷嵌套。
下面是這15條IRQ線分別連接的外設(shè):

現(xiàn)在我們來看看這個(gè)秘書是如何和CPU之間進(jìn)行協(xié)調(diào)工作的。
現(xiàn)在假設(shè)我們敲擊了一個(gè)鍵盤按鍵,鍵盤有中斷事件產(chǎn)生,這一事件通過IRQ1這根線告知了主PIC,主PIC經(jīng)過內(nèi)部一些判斷處理后通過INT發(fā)送電信號(hào)到CPU側(cè)的INTR。CPU在執(zhí)行完當(dāng)前的指令后,檢查到INTR有信號(hào),說明有中斷請(qǐng)求來了,再檢查eflags中的IF不為零,表示當(dāng)前允許中斷,則發(fā)送信號(hào)給PIC的-INTA,告訴它把本次中斷的向量號(hào)發(fā)送過來。主PIC收到-INTA管腳上的信號(hào)后,通過D0-D7引腳,輸出此次中斷的中斷向量號(hào)到數(shù)據(jù)總線(這里簡(jiǎn)化了交互過程,實(shí)際上有兩次INTA信號(hào)的發(fā)送)。CPU拿到這個(gè)號(hào)后,就可以從IDT中尋找中斷服務(wù)例程(ISR)進(jìn)行處理了,后面的事大家都知道了。
那PIC中的中斷向量號(hào)是怎么來的呢?各個(gè)IRQ是如何對(duì)應(yīng)到IDT中的各個(gè)項(xiàng)呢?這里就利用了中斷控制器的可編程性來決定的了。
PIC全稱為可編程中斷控制器,那么它的可編程體現(xiàn)在哪些方面呢?參考資料2《i8259A中斷控制器分析一》一文有比較詳細(xì)的描述,大體包括編程指定主從片的IRQ線對(duì)應(yīng)的中斷在IDT表中的中斷向量號(hào)、8259a中斷控制器的中斷方式、優(yōu)先級(jí)方式、中斷嵌套方式,中斷屏蔽方式、中斷結(jié)束方式等等,這些都可以由操作系統(tǒng)編程指定。具體的編程格式在參考資料3《i8259A中斷控制器分析二》一文中有圖文介紹。
回到上一個(gè)問題,IRQ線上的中斷如何和IDT中的條目對(duì)應(yīng)起來,操作系統(tǒng)在初始化的時(shí)候,會(huì)通過對(duì)8259a芯片編程(讀寫I/O端口),將指定PIC芯片的起始向量號(hào),并要求低三位為0,起始向量號(hào)按照8對(duì)齊,這樣規(guī)定的原因是,當(dāng)中斷發(fā)生時(shí),低三位將自動(dòng)填充對(duì)應(yīng)的IRQ號(hào),這樣就可以和起始向量號(hào)相加直接送給數(shù)據(jù)總線從而被CPU拿到。具體到Windows中,系統(tǒng)初始化的時(shí)候?qū)IC的編程為:指定主片的起始中斷向量號(hào)為0x30,指定從片的起始中斷向量號(hào)為0x38。這樣,通過中斷控制器連接的15個(gè)外設(shè)將被平坦的映射到IDT中0x30-0x40這一范圍中。Windows內(nèi)核啟動(dòng)初始化過程中使用了hal!HalpInitializePICs對(duì)8259a芯片進(jìn)行編程,ReactOS中代碼如下:

其中0x20,0x21是主片的IO端口,0xa0,0xa1是從片的IO端口:

PRIMARY_VECTOR_BASE定義為:

具體8259a的編程方法就是讀寫IO端口,設(shè)置對(duì)應(yīng)的控制命令,不用深入研究。我們來看Windows編程8259a的時(shí)候指定了哪些東西。
1、指定了主片的工作方式為級(jí)聯(lián)、中斷方式為電信號(hào)邊沿觸發(fā)
2、指定了主片IRQ的中斷向量映射基址:0x30
3、指定了主片的級(jí)聯(lián)方式為使用了自己的IRQ2這個(gè)管腳
4、指定了主片的工作模式為80x86模式,中斷結(jié)束方式為普通結(jié)束模式
5、指定了從片的工作方式為級(jí)聯(lián)、中斷方式為電信號(hào)邊沿觸發(fā)
6、指定了從片IRQ的中斷向量映射基址:0x38
7、指定了從片的工作方式級(jí)聯(lián)方式為主片的IRQ2這個(gè)管腳
8、指定了從片的工作模式為80x86模式,中斷結(jié)束方式為普通結(jié)束模式
至此我們可以知道,在使用8259A中斷控制器的計(jì)算機(jī)上,通過IRQ線連接的那15個(gè)外設(shè)可屏蔽中斷是被操作系統(tǒng)線性的映射到了IDT中的一個(gè)范圍段。在Windows中是0x30-0x40(PS:在Linux中是0x20-0x2F),同時(shí)指定了中斷控制器的中斷方式為邊沿觸發(fā),結(jié)束模式為普通結(jié)束模式(也就是需要CPU側(cè)告知中斷處理有沒有結(jié)束并設(shè)置對(duì)應(yīng)bit位,不能自動(dòng)設(shè)置)。
0x02 8259a上的Windows IRQL
下面來看看IRQL。
從前面我們看到,硬件層面已經(jīng)對(duì)中斷的處理提供了很好的支持,需要操作系統(tǒng)做的也就兩點(diǎn):首先,初始化的時(shí)候?qū)IC進(jìn)行編程設(shè)置其工作方式并對(duì)IRQ進(jìn)行映射,讓這些中斷對(duì)應(yīng)到IDT中的各個(gè)項(xiàng),其次,實(shí)現(xiàn)這些IDT中的中斷服務(wù)例程。似乎這樣就夠了,那Windows弄出來的一套IRQL又是什么東西呢?
看看《Windows Internals》一書對(duì)IRQL的定義:

寫驅(qū)動(dòng)的時(shí)候經(jīng)常會(huì)接觸到IRQL這個(gè)概念,它實(shí)現(xiàn)了Windows里的中斷優(yōu)先級(jí)制度,高優(yōu)先級(jí)的中斷總是可以優(yōu)先被處理,而低優(yōu)先級(jí)的中斷則不得不等待高優(yōu)先級(jí)中斷被處理完后才得到處理。軟件虛擬出來的這一套機(jī)制怎么能管到硬件的優(yōu)先級(jí)呢?這是如何實(shí)現(xiàn)的呢?
先來解決兩個(gè)問題:
1、IRQ和IRQL的關(guān)系是什么?、使用KeRaiseIrql提升當(dāng)前IRQL后,為什么就能保證不被低優(yōu)先級(jí)的中斷打擾?
對(duì)于第一個(gè)問題,在使用8259a中斷控制器的計(jì)算機(jī)中,IRQL=27-IRQ,其就是一個(gè)線性關(guān)系。
關(guān)于第二個(gè)問題,《Windows Internals》一書是這樣解答的:

?下面我們具體來看Windows的實(shí)現(xiàn):
IRQL是一個(gè)完全虛擬出來的概念,Windows為了實(shí)現(xiàn)這一個(gè)虛擬的機(jī)制,完全虛擬了一個(gè)中斷控制器,它在KPCR中:
+0x024 Irql ? ? ? ? : UChar ?//IRQL+0x028 IRR ? ? ? ? ?: Uint4B ?//虛擬中斷請(qǐng)求寄存器+0x02c IrrActive ? ?: Uint4B ?//虛擬中斷在服務(wù)寄存器+0x030 IDR ? ? ? ? ?: Uint4B ?//虛擬中斷屏蔽寄存器
在前面第一部分提到過,通過兩片8259a芯片連接的15個(gè)中斷源被映射到處理器IDT中的一段范圍,具體Windows而言,是在0x30-0x40這個(gè)范圍。這15個(gè)IDT中的中斷描述符所描述的中斷處理例程(ISR)不同于int 3所對(duì)應(yīng)的KiTrap03和int 0e所對(duì)應(yīng)的KiTrap0E,他們的ISR指向的代碼位于各自的中斷對(duì)象KINTERRUPT的DispatchCode。下面是這個(gè)結(jié)構(gòu)的定義:
typedef struct _KINTERRUPT {
? ?CSHORT Type;
? ?CSHORT Size;
? ?LIST_ENTRY InterruptListEntry;
? ?PKSERVICE_ROUTINE ServiceRoutine;
? ?PVOID ServiceContext;
? ?KSPIN_LOCK SpinLock;
? ?ULONG TickCount;
? ?PKSPIN_LOCK ActualLock;
? ?PVOID DispatchAddress;
? ?ULONG Vector;
? ?KIRQL Irql;
? ?KIRQL SynchronizeIrql;
? ?BOOLEAN FloatingSave;
? ?BOOLEAN Connected;
? ?CHAR Number;
? ?UCHAR ShareVector;
? ?KINTERRUPT_MODE Mode;
? ?ULONG ServiceCount;
? ?ULONG DispatchCount;
? ?ULONG DispatchCode[106]; KINTERRUPT, *PKINTERRUPT;
復(fù)制代碼 DispatchCode里面的代碼是根據(jù)一個(gè)模板來的,這些ISR處理開始和KiTrap03這些一樣,首先會(huì)建立陷阱幀,然后會(huì)獲取自己所在KINTERRUPT對(duì)象地址,得到這兩個(gè)參數(shù)之后,便開始使用KiInterruptDispatch或KiChainedDispatch(如果對(duì)該中斷注冊(cè)了多個(gè)KINTERRUPT結(jié)構(gòu)構(gòu)成了鏈表使用此函數(shù))進(jìn)行中斷派遣。而在這兩個(gè)具體的派遣中都會(huì)先調(diào)用HalBeginSystemInterrupt,然后才會(huì)執(zhí)行對(duì)應(yīng)中斷的實(shí)際處理工作,最后會(huì)執(zhí)行HalEndSystemInterrupt完成此次中斷處理。下面我們重點(diǎn)來看看這兩個(gè)函數(shù)。
BOOLEANHalBeginSystemInterrupt(
? ?IN KIRQL Irql
? ?IN CCHAR Vector,
? ?OUT PKIRQL OldIrql);
輸入?yún)?shù)Irql表示本次發(fā)生的中斷對(duì)應(yīng)的的IRQL,Vector表示中斷向量號(hào),如前所述,這兩個(gè)參數(shù)都是DispatchCode從自己所在KINTERRUPT對(duì)象中取出來的。
HalBeginSystemInterrupt內(nèi)部使用IRQL參數(shù)在一個(gè)表格中進(jìn)行了分發(fā),這個(gè)表中除了個(gè)別函數(shù)不同外(其實(shí)也只是多了一層判斷),其他表項(xiàng)都是一致的,在ReactOS中名為HalpDismissIrqGeneric,該函數(shù)直接轉(zhuǎn)而調(diào)用其下劃線版本_HalpDismissIrqGeneric。這里就是IRQL優(yōu)先級(jí)實(shí)現(xiàn)的核心所在了。該函數(shù)不長,下面是ReactOS中的代碼(在Windows2000代碼中是匯編形式不如ReactOS使用的C語言形式直觀,所以采用了ReactOS的代碼進(jìn)行說明):

首先,判斷本次發(fā)生的中斷對(duì)應(yīng)的IRQL與當(dāng)前處理器(KPCR)中的IRQL進(jìn)行比較,如果大于了當(dāng)前處理器的IRQL,則表示來了一個(gè)優(yōu)先級(jí)更高的中斷,這時(shí)設(shè)置KPCR中的IRQL為這個(gè)新的更高的數(shù)值,后面返回了TRUE,表示需要處理這次中斷請(qǐng)求。如果不大于當(dāng)前處理器的IRQL的話,首先把本次中斷記錄記錄到KPCR中的虛擬中斷控制器的IRR值,然后就直接通過KiI8259MaskTable表中選取當(dāng)前處理器IRQL對(duì)應(yīng)的屏蔽碼寫入PIC,用以屏蔽那些IRQL比自己低的中斷源,后面返回FALSE,表示不處理這次中斷請(qǐng)求。為什么不在設(shè)置處理器新IRQL的時(shí)候就進(jìn)行設(shè)置屏蔽碼呢?《Windows Internals》是這樣解釋的:

HalpDismissIrqGeneric的返回值將直接作為HalBeginSystemInterrupt的返回值。以中斷派遣函數(shù)KiInterruptDispatch為例看看它是如何使用這個(gè)返回值的:

可以看出,如果HalBeginSystemInterrupt返回了FALSE,則直接導(dǎo)致本次中斷處理提前結(jié)束。只有當(dāng)HalBeginSystemInterrupt返回了TRUE時(shí),才繼續(xù)執(zhí)行真正的中斷處理例程。最后, 情況下都會(huì)調(diào)用KiExitInterrupt結(jié)束中斷處理過程,看一下這個(gè)函數(shù)。結(jié)合KiInterruptDispatch的代碼,可以看出,只有當(dāng)HalBeginSystemInterrupt返回的是TRUE時(shí),下面的if條件才會(huì)成立,從而進(jìn)入HalEndSystemInterrupt。

最后看一下HalEndSystemInterrupt,前面提到如果發(fā)生的中斷對(duì)應(yīng)的IRQL低于處理器的IRQL,則不會(huì)執(zhí)行其ISR,但會(huì)在KPCR中的虛擬中斷控制器的IRR中記錄起來,等到處理器執(zhí)行完了高IRQL的任務(wù)時(shí),到了HalEndSystemInterrupt的時(shí)候,就會(huì)降低處理器的IRQL并重新設(shè)置PIC的中斷屏蔽碼,另外很重要的就是去檢查IRR中的記錄,如果記錄中有比降低后的IRQL高的記錄,則派遣該中斷。
→【技術(shù)文檔】←
總結(jié)
最后總結(jié)一下使用8259a中斷控制器的計(jì)算機(jī)中Windows的IRQL。
首先,系統(tǒng)啟動(dòng)時(shí)對(duì)8259a芯片編程,設(shè)置其工作方式,并將15個(gè)中斷源(IRQ)映射到IDT中的0x30-0x40這一段。
第二,Windows自己定義了一個(gè)稱為中斷請(qǐng)求級(jí)的IRQL概念用來描述中斷的優(yōu)先級(jí)別,IRQL是一個(gè)DWORD,共計(jì)32個(gè)級(jí)別,Windows使用一個(gè)簡(jiǎn)單的線性關(guān)系來映射IRQ和IRQL:IRQL=27-IRQ。
第三,被映射中斷請(qǐng)求的0x30-0x40這一段的中斷描述符的每個(gè)ISR都指向了一個(gè)KINTERRUPT結(jié)構(gòu)中的DispatchCode,這段DispatchCode使用中斷派遣函數(shù)KiInterruptDispatch或KiChainedDispatch進(jìn)行中斷派遣。
第四,派遣過程為:先使用HalBeginSystemInterrupt對(duì)本次中斷的IRQL進(jìn)行判斷來決定是否需要處理本次中斷,若不需要,則設(shè)置中斷控制器的屏蔽碼,防止再被打擾,同時(shí)將本次中斷登記在KPCR中的虛擬中斷控制器IRR中。若需要?jiǎng)t提升IRQL,進(jìn)而執(zhí)行該中斷的實(shí)際處理例程,執(zhí)行完畢后使用HalEndSystemInterrupt降低IRQL,然后檢查IRR有沒有記錄沒被處理的中斷以便在這個(gè)時(shí)候進(jìn)行處理。