系統的啓動過程:
任何一臺計算機,在開機後,它要做的第一件事情就是引導(Booting),通過引導,計算機爲自身搭建好運行環境,爲以後OS的啓動與運行做好準備.首先,我們來看看一臺計算機是如何引導自身的.在機器加電後,電源供電穩定後,電源會傳給8284A時鐘生成器一個"Power Good"低電位信號,隨後8284A會輸出有效的RESET信號,使CPU復位,這時CS:IP = FFFF:0000.CPU在這裏執行一條jmp far addr類指令,跳轉到實際BIOS映射代碼的位置,開始執行BIOS代碼.
上述是機器在加電後的啓動過程,大家都知道計算機的啓動是分爲冷啓動與熱啓動的,那麼對於熱啓動,其過程又是怎樣的呢?其實熱啓動只不過是將鍵盤中斷程序置復位標誌爲1234h,然後再跳轉到BIOS處執行,其主要是省去了在自檢過程中對存儲器的檢測.
在跳轉到BIOS後,首先會先關閉中斷,然後開始自檢(POST)工作,這個自檢主要檢測計算機最基本設備的運轉狀態.其主要包括對,CPU內部寄存器測試,BIOS芯片字節的檢查,8237 DMA控制器測試,基本32K RAM檢測等最基本內容.由於被檢測設備在系統運行中的重要性,因此在此過程中,BIOS一旦檢測到任何異常,都將判爲致命性錯誤,系統將被停機.
通過上面的自檢後,BIOS開始初始化8259可編程中斷控制器,並設置BIOS的8個主要中斷向量(int 10h-int 17h),然後初始化並測試CRT視頻接口以及顯示內存(對於熱啓動這一步將跳過),在確認正常後,執行其內部的顯示卡標準驅動程序(注意這裏的驅動跟安裝操作系統下的驅動是不一樣的),這段代碼會存放在C0000h,其主要目的是初始化顯示卡.然後BIOS會打印顯卡信息.
接着BIOS開始檢查其他設備,其包括對8259中斷控制器測試,8253定時器測試,鍵盤復位和卡鍵測試,擴展I/O測試,設置硬件中斷向量,擴展RAM測試(這裏的RAM測試會檢測除0-32K以外的整個RAM空間,對於熱啓動同樣也會跳過這一階段),.然後BIOS會搜索其他設備的ROM,如果找到,則會執行它們.接着測試ROM-BASIC的字節檢查,測試磁盤驅動器(如:FDC等),測試打印機端口和RS-232,並設置他們的地址.
然後打開NMI(不可屏蔽中斷),最後就是調用Int 19h進行自舉.這一階段的自檢如果發生錯誤,系統會判斷其爲一般性錯誤,並顯示出相應的提示信息.在此過程中,BIOS會將檢測收集到的數據保存在內存低1K--2K的區域,並將BIOS中斷向量表,以及BIOS程序運行所需要的stack保存在內存低0K--1K的地方.
下面就是系統自舉工作了,系統調用int 19h進行自舉,尋找啓動設備,如:軟驅,硬盤,光驅等等.找到後系統讀取啓動設備的0號邏輯扇區(如是軟盤就讀取0面0道1扇區的整個內容),並將讀取的內容放到內存地址爲0000:7C00的地方.
當然,如果找不到啓動設備,BIOS就會調用Int 18h,並給出相應的提示信息,然後進入ROM-BASIC.(有些機器會在等待一段時間後自動進入CMOS.如:很久以前海洋的AMD 386DX/40主板)至此,BIOS的引導程序結束,CPU開始執行0000:7C00處的代碼.在這裏需要說明一下的是,BIOS的引導程序是與操作系統無關的,但隨後CPU開始執行的代碼就開始與操作系統存在較大的相關性了,因此對於不同的操作系統,下面這一部分可能會存在着較大的不同.不過,從目的上來講,它們是相同的,即都是爲將要運行OS的內核(Kernel)作準備.
進入這一部分的首要工作就是執行啓動設備的引導程序.硬盤與軟盤的對於引導程序的存放結構是不同的.硬盤有一個叫做MBR(Master Boot Record)的扇區,系統會首先執行它,以判斷那個分區是啓動分區,並讀入該分 區的第一個扇區,並執行.並且在這個扇區中還存放着硬盤分區表(DPT),這個表的地位相當重要,因爲它包含了各個分區的諸如:分區類型,起始位置,結束位置等重要參數.下面我們來詳細介紹一下MBR的結構.MBR的結構分爲三部分,首先是可執行代碼,佔446個字節,然後是4個分區表,每個佔16個字節,共64個字節,最後是簽字AA55H.
下表列出了分區信息的詳細內容:
? 長度 描述
0 字節 分區狀態0:非活動分區,80h活動分區(可引導)
1 字節 分區起始頭
2 字 分區起始扇區和起始柱
4 字節 分區類型
5 字節 分區中止頭
6 字 分區中止扇區和中止柱
8 雙字 分區起始絕對扇區
0Ch 雙字 分區扇區數
然後我們開始介紹MBR中的可執行代碼部分:
首先,程序會檢測MBR的簽字是否合法,即判斷其最後字是否爲AA55h.通過後,將自身移動到內存中的其他地方,以備將來在此裝入引導分區的Boot扇區.然後,程序檢查四個分區的分區狀態,找出活動分區,並將該分區的Boot扇區讀入到0000:7C00h處,並檢查其簽字是否合法,在通過後,程序跳轉到0000:7C00h處執行,即將控制權交給活動分區的Boot程序;對於軟盤則沒有那麼複雜,軟盤的第一個扇區就是它的Boot區,系統自舉時將直接將其讀入到0000:7C00h處並執行.
從功能上來講軟盤與硬盤的Boot區是相同的,其任務都是將OS的內核(Kernel)讀入到內存並執行.但具體來看,由於絕大多數OS的Kernel是以文件形式存放在磁盤上的,要讀取它就要涉及到對文件系統的操作,這使得它們在實現上又是很不相同的.因此,對於Boot區的分析我們將放在後面的內容中具體介紹.
2.保護模式簡述
最早的Intel系列的CPU只存在一種操作模式,即現在所說的實模式(Real Mode,以下簡稱RM).在Intel推出80286之後,爲了增強CPU的處理能力,同時也爲了適應當時的軟件開發需求,Intel提出了保護模式(Protected Mode,以下簡稱PM),但在80286下的PM由於CPU本身設計的問題,並沒有使其發揮出很大的功效.在80386推出之後,Intel完善了CPU的設計形成了最終的IA-32架構,並提出了另一種模式系統管理模式(System Management Mode).本章我們的討論就圍繞着這三種模式進行展開,並重點討論PM.
首先,對這三種模式做一簡單概述.
RM:此模式是主機在加電或復位後自動進入的模式,在此模式下其可以執行16位指令,並可以切換到PM或者SMM.
PM:在此模式下,CPU能夠支持其自身的32位特性,使自身處於最高性能表現.這些特性主要包括:
1. 最大可訪問4GB內存空間.事實上,在RM下通過一些未公開的特性,也可以達到同樣的效果,但其對於代碼段 和堆棧空間卻是無效的.況且後面的所有特性都是基於PM的,對RM沒有效果.
2. 虛擬存儲.處於PM下的CPU其內存管理單元(MMU)支持這項特性.前面我已經說到,在PM下CPU最大尋址空 間爲4GB,而在實際中,我們並沒有如此大的物理內存空間.因此通過MMU,可以將外存設備(如:硬盤)的一部分 空間模擬成物理內存進行使用.
3. 地址映射.即MMU可以在地址使用前對其進行轉換,即所謂的映射.
4. 改進的分段機制.本文後面將對此進行重點論述.
5. 內存保護與任務保護.即在PM狀態下,引入了權限機制.通過權限控制可以達到保護相關代碼和數據的目的.
6. 改進的尋址模式.在RM下,只有常數,BX或BP,SI或DI可以用來形成地址,而在RM下可以通過任意寄存器進行尋 址,並且可以包含一個爲2,4或8的比例因子.
7. 多任務支持.在PM下,CPU提供了特殊的機制能夠進行快速的上下文切換.
SMM:該模式爲操作系統實現特定平臺指定的功能提供了一種有效的機制.
值得注意的是,在PM下,CPU允許在受保護的情況下,執行RM程序,這個特性被稱爲虛擬8086模式(Virtual-8086 Mode),但其本質上卻不是真正的RM.
對於三種模式關係的形象解釋可以通過下圖來描繪:
正如上面所說的,只有在PM下,CPU才能充分發揮其自身的所有特性,而計算機在啓動之後,默認的CPU操作模式卻是RM.因此擺在我們面前的一個主要問題就是如何在RM與PM之間相互切換.那麼如何在RM和PM之間相互切換呢.核心步驟其實很簡單,只要改變CPU中的CR0寄存器中PE標誌位的值,就可以實現.在PE=1時,CPU進入PM,而在PE = 0時,則進入RM.但這僅僅是整個切換過程中的一小部分,在進入保護模式之前我們還需要做很多事情,其中最關鍵的就是建立好一個被稱爲GDT的表.
在談到GDT之前,我們先回顧一下,在RM中,內存中尋址的方式---段:偏移量.其中段(Segment)表明了一個基地址,其最大長度固定爲64KB(FFFFH),即16bit數所能表示的最大數值.而偏移量(Offset),就是指在指定段內的位置.由此可見,通過段+偏移量這種表示方式,就可以表示出內存中的絕對地址.需要指出的是,在CPU實際處理過程中,CPU會將段寄存器的 值左移動4位,再與偏移量相加,形成地址,放入20位的總線當中.
在PM中,對於段模式來講,上面的尋址方式,在大部分上仍然是適用的.但由於PM是工作在32位下的,因此上面的各個值,也就都相應的變成了32位.與RM不同的是,在PM下,一個段的長度不再固定,其可以在CPU允許的規則下任意設置.並且CPU爲段模式提供了保護機制,即增加了對自身的訪問權限.因此在PM下,對於一個段,需要有三個變量給於描述,即基地址,段界限和訪問權限.
事實上,CPU將這三個值保存爲一個64位長的段描述符.但出於兼容性的考慮,Intel並沒有將段寄存器改爲64位可用--雖然,段寄存器在事實上確是64位,但對於程序來講,高於16位的部分卻是不可見的--因此,我們需要另一種方法去存放這些數據.Intel選擇了將這些段描述符統統存入到一個全局數組中的方法,在訪問段時,向相應的段寄存器填入該數組的下標值來實現間接引用.這個全局數組就稱其爲GDT(全局描述符表).由於GDT可以存放在內存中的任何位置,因此要引用它,就必須知道他的入口地址.Intel爲我們提供了GDTR寄存器和LGDT指令.其中GDTR寄存器存放的是GDT的入口地址(32位)和其界限(16位),共48位.這裏的入口地址是一個線性地址,界限則是表的字節長度減一.可見該表最多可以長達64KB,存儲8192條描述符號,而LGDT指令的作用就是將GDT裝載到放入GDTR寄存器當中.
顧名思義,GDT是全局描述符,因此其在內存中存在且僅存在一個,並且它的存在對於所有的任務來講,都是可見的.顯然,這種做法對於多任務來講是不易管理的.因此,Intel又引入了LDT(局部描述符表),該描述符與GDT不同之處在於,LDT在系統中可以有許多個,但每個任務只允許有一個LDT,且其只能該任務自身可見.其與GDT的主要關係在於,每一個LDT都會作爲一個段,存入GDT中.由於CPU在任何時刻只能執行一個任務的代碼,因此存儲LDT所需要的寄存器也就只需要一個,Intel將其命名爲LDTR,與GDT相同,Intel爲裝入LDT設置了LLDT指令.與GDT不同的是,LLDT指令的操作數卻是一個16位的段選擇子,即前面說到的要裝入的LDT在GDT中的索引值.這裏需要指出的是,LDT並不是必須的,你的程序可以選擇使用,或者不使用它.
前面提到了一個新概念--段選擇子.我們說段選擇子是要引用段在GDT或LDT中的索引值,其實這種說法並不正確.因爲段選擇子除了含有索引值以外,它還包含了其他內容.
段選擇子的結構如下圖:
由於是從Word文檔中複製過來,表格無法顯示,詳情請查閱相關文檔。
段選擇子是一個16位的數據結構,其包含了三部分內容.其中,其高13位正是前面所說的索引值,TI用來指定是在GDT中索引,還是在LDT中索引(0 = GDT, 1 = LDT),RPL則是用來指明請求特權級的.
談到這裏,我們就已經闡明瞭在PM的段模式下,如何引用一個內存地址.首先,將段選擇子裝入相應的段寄存器中,然後CPU會自動根據段選擇子找到相應的段描述符,並找出基地址,最後在加上偏移量,就得到了所需要的內存地址.
在本文開始的部分,我已經說過GDT是進入 PM所必需的數據結構,下面就詳細的來討論一下如何設置好GDT,並將其裝入相應的寄存器.
首先必須注意的兩點是:
(1).GDT中的第一個描述符必須是空,即全爲0.在程序中這個描述符不能用來進行內存訪問,否則將產生General Protection異常.(2).由於GDT中的描述符都是64位長,因此爲了讓CPU的訪問速度達到最快,需要將GDT的入口地址以8字節對齊,即放入8的倍數的位置.
下面,開始設置進入PM後的代碼段和數據段的描述符.
其格式如下:
G - 粒度
D/B - 大小(0 = 16位段; 1 = 32位段)
D - 保留
AVL - 用戶定義
P - 段是否存在 DPL - 描述符特權級
注意P位,這個位確定了段是否存在.這是什麼意思呢.當該位被清除時,如果存在任務要訪問這個段.那麼CPU會產生一個錯誤,並會從外存(如:硬盤)中調入該段並再次嘗試.當該位被清除時,描述符中的0到39位和48到63位能夠包含任意值.你也可以用這些空間來存儲該段在磁盤空間中的地址.
還有就是A位,CPU會在對其所在段寫入數據後,將該位置1,這樣在做段的磁盤交換時,可以決定是否將該段寫入磁盤.
下面要說的就是G位.你會發現,在描述符中段界限僅僅爲20位.那麼其如何能夠設置成1MB到4GB之間的範圍呢.這裏G位其了重要的作用.當G位被清零時,界限域就是段的最大合法偏移.而如果G位被置成1,那麼會把描述符中的段界限左移12位形成32位界限,再將低12位全部填1.這樣,實際上就能夠指定1MB以下的任意長度,和以4K到4GB爲單位的長度.
假定要在進入PM後,使代碼段和數據段能夠訪問全部線性空間,於是可將GDT設置爲:
gdt dd 00000000h, 00000000h ;空
gdt.Code32 dd 0000ffffh, 00cf9a00h ;代碼段 讀/執行 4GB空間 基地址=0 粒度=4096,386
gdt.Data32 dd 0000ffffh, 00cf9200h ;數據段 讀/寫 4GB空間 基地址=0 粒度=4096,386
這裏你會發現GD中不同的描述符指向了同一塊內存空間.這在系統中是允許的.在實際應用中,這也是經常要使用到的,例如:操作系統可以將一個可執行文件裝入數據段,然後再從同一位置開始執行.
在設置好GDT以後,需要將其裝入相應的寄存器中.前面說過GDTR寄存器包含兩段內容,因此我們需要先算出GDT的絕對物理地址.
GDTR的具體內容
gdtr dw gdtr - gdt - 1;界限
dd gdt;前面GDT的地址
實現代碼如下:
mov eax, ds
shl eax, 4
add [gdtr+2], eax ; 生成絕對物理地址
lgdt [gdtr] ; 將gdtr裝入寄存器
到此,就完成了進入保護模式的最主要工作,可以進入PM模式了.
實現代碼如下:
mov eax, cr0
or al, 1
mov cr0, eax ; 修改CR0寄存器,置PE = 1
jmp dword 8:_premain32 ; 8爲選擇子
你可能會問爲什麼要在代碼的最後添加一個jmp語句.這是因爲,我們必須要清除CPU的指令預取隊列(流水線),並以此來設置CS段寄存器. 不過這僅僅是其一,還有一個重要的原因就是,我前面談到的那個段寄存器大於16位的不可見部分.需要指出的是,Intel對於這一點是未公開的,因此我下面對該問題的討論僅僅是由推斷得出來的.事實上,當我們執行一條裝載CS寄存器的指令時,操作數被裝入了寄存器的可見部分,而CPU會自動根據操作數,去設置其不可見部分.CS段寄存器所處的狀態與當前在哪個模式下並無關係.在剛剛進PM後,CS仍然認爲當前處於16位段,即當前的地址仍是16位地址.因此,必須通過裝載一32位指令去切換到32位段,這也就是jmp在這裏的意義.
雖然程序已經進入了PM,但需要做的事情還遠沒有做完,因爲我們還沒有配置IDT,即中斷描述符表,而要理解這個表又要牽涉到許多內容,因此,我將在後面的文章中詳細介紹,這裏就不多談了.
Intel之所以將這個模式起名爲保護模式,其來源就在於特權保護.在PM下,每一個任務都擁有自己的特權級(PL).Intel將其分爲四個級別,由零到三.數字越低級別則越高.例如:PL3級的程序對於一些特定的指令,如:HLT,LGDT,LLDT等,沒有執行的權限,並且其也不允許訪問擁有高特權級程序的數據.
在PM下,I/O的訪問同樣也受到了特權保護.在EFLAGS中存在一個IOPL域.這個域的值決定了能夠執行I/O操作的最低權限.例如,當IOPL爲3的時候,表明所有特權級的程序都能夠執行I/O操作.這個域的值僅允許PL0級的程序進行修改,其他級的程序修改無效.
同樣,數據訪問在PM下也是受到保護的.當數據段寄存器要被加載時,CPU會將該段的描述符特權級(DPL)與一個被稱爲有效特權級(EPL)的數值進行比較,如果DPL不小於EPL,則允許裝載寄存器,否則將會產生一個錯誤.這裏的EPL就是選擇子的RPL和程序當前特權級(CPL)的數值較大的那一個.
對於堆棧,則有些不同,訪問SS寄存器,其DPL要求必須和CPL相等.
關於保護模式,本篇文章就介紹到的這裏.對於保護模式的其他重要特性,如:分頁操作,多任務處理等,由於內容很多,幾乎每一塊內容都能當成一個專題來講,因此我將在以後的文章中對此進行詳細討論.
中斷和異常
學過8086/8088彙編的人肯定對於中斷這個概念都不陌生.在80386中,這個概念在一定程度上發生了變化,並引入了"異常"這個新概念.本篇文章就是圍繞在操作系統開發中涉及到中斷和異常的討論.
中斷
中斷在系統中是由外部事件所引起的,如:一次I/O操作的結束.其產生與CPU當前所執行的指令沒有關係.從是否能夠被屏蔽來劃分,可將其分爲兩類,即可屏蔽中斷與不可屏蔽中斷,其中前者由CPU的INTR引腳接收信號,後者由NMI引腳接收信號.
由於產生中斷的中斷源並不單一,因此在INTR接收中斷的時候,同樣還要接收一個8位的中斷向量號,以判斷是誰發出的中斷請求.CPU對於某個中斷向量號是由誰發出原則上並沒有規定.但在實際系統中,爲了避免產生衝突,部分中斷向量號都有自己固定的中斷源,這一工作是由可編程中斷控制器(PIC)完成的.在80386系統上,該PIC是8259A.該芯片功能十分強大,不僅能向CPU提供中斷向量號,還可以自主處理中斷請求的優先級.每個8259A芯片可以支持8箇中斷請求信號,並可進行級連.對於8259A的介紹,我們將在文章的後半部分進行.
對於是否屏蔽可屏蔽中斷可以通過8259A有選擇地控制,也可以通過CPU的CLI和STI指令實現.CLI和STI指令可以設置EFLAGS寄存器的IF位,如果該位被清除,則CPU會禁止外部中斷傳遞信號給INTR引腳.但對於CPU內部異常和NMI該位不起作用.在執行這兩條指令時,必須要保證當前CPL小於等於IOPL,否則會引起通用保護故障.當然雖然NMI是不可屏蔽中斷,但通過將CMOS端口(0x70)中第7位置1這種手段也可以將NMI也屏蔽掉的,當然需要打開該中斷將該位清零就可以了.
實現代碼如下:
#define PORT_CMOS 0x70
void disable_NMI(){
byte val;
val = inportb(PORT_CMOS);
outportb(PORT_CMOS, val | 0x80);}
void enable_NMI(){
byte val;
val = inportb(PORT_CMOS);
outportb(PORT_CMOS, val & 0x7F);}
異常
異常是在CPU執行指令期間遇到非法指令所產生的.因此異常與當前指令存在着關係,例如:除零,特權級不正確等等,都會觸發異常.80386可以識別多種不同的異常,並以不同的中斷向量號來標示它們.在異常發生時,CPU就根據原先設定好的中斷向量號轉到不同的中斷處理程序(ISR)執行.
根據是由是否可恢復和恢復點位置不同將異常劃分爲三種.它們是故障(Fault),陷阱(Trap)和中止(Abort).
80386認爲故障是可以排除的,因此在CPU遇到引起故障的指令的時候,會保存當前的CS和EIP值,並轉去執行故障處理程序.在故障排除後,執行IRET指令回到剛在引發故障的位置,重新執行剛纔觸發故障的指令.例如,當程序企圖裝入一個不存在的段時將會引發一個故障,這時操作系統會將該段裝入,並重新進行剛纔的操作.
陷阱與故障的區別主要在於,在執行陷阱處理程序之前,系統會保存CS和EIP的值爲引起陷阱的下一條要執行指令所在的位置.例如,軟中斷就是典型的陷阱.
中止是在系統發生嚴重錯誤時產生的.在引起中止後,當前執行的程序不能被恢復執行.並且系統在接受到中止後,中止處理程序要重新建立各種系統表.引起這類錯誤的主要原因是系統表的數據不一致或者非法等.
下表列出了80386在保護模式下的中斷和異常
向量號 名稱 異常類型 出錯代碼 相關指令
0 除法錯 故障 無 DIV,IDIV
1 調試 故障/陷阱 無 調試狀態下
3 斷點 陷阱 無 INT 3
4 溢出 陷阱 無 INTO
5 界限檢查 故障 無 BOUND
6 無效操作碼 故障 無 非法指令
7 無80X87 故障 無 ESC, WAIT再無協處理器情況下
8 雙重故障 中止 有 任何指令
9 NPX 中止 無 ESC的操作數超過段尾
0AH 無效TSS 故障 有 JMP、CALL、IRET或中斷
0BH 段不存在 故障 有 裝載段寄存器的指令
0CH 堆棧段異常 故障 有 任何使用SS寄存器的訪問
0DH 通用保護 故障 有 任何內存訪問
0EH 頁異常 故障 有 任何內存訪問
10H 協處理器出錯 故障 無 ESC, WAIT
11H-0FFH 軟中斷 陷阱 無 INT n
這裏一些異常在發生時會將錯誤碼壓入堆棧,這些錯誤碼都是導致錯誤的選擇子.對於錯誤代碼爲0的異常,其除了表示空選擇子導致的錯誤外,當CPU不能確定時也會返回該數值.
這裏需要指出的是,對於異常0DH,它的產生沒有一個準確的原因,但通常來講是基於以下幾個原因:
1.試圖用一個超過段界限的偏移量訪問段
2.將一個不可執行的段裝入CS寄存器
3.將一個只執行段裝入除CS寄存器外的其他段寄存器
4.寫只讀段
5.使用空選擇子
6.轉換到一個忙任務
7.中斷描述表
8.門描述符
在系統中除了存儲段描述符和系統段描述符外,還有一類門描述符.這種描述符是用來描述控制轉移的入口點.任務內特權級的改變和任務間且換都是通過這種描述符實現的.其中,門描述符共分爲4種分別是,調用門(CallGates),陷阱門(Trap Gates),中斷門(Interrupt Gates)和任務門(TaskGates).由於本章內容主要是涉及中斷和異常,因此,在這裏我們將對陷阱門和中斷門進行詳細介紹,對於另兩種門會簡要的做一介紹.
陷阱門和中斷門
這兩種門是用來描述中斷和異常的入口的.其只能出現在IDT中(對於IDT後面將有詳細描述),不能出現在GDT和LDT中.
這兩種門的格式如下表:
中斷門描述符
由於是從Word文檔中複製過來,表格無法顯示,詳情請查閱相關文檔。
陷阱門描述符
由於是從Word文檔中複製過來,表格無法顯示,詳情請查閱相關文檔。
注:DPL - 描述符特權級
P - 門有效標誌
D - 門規模(1 = 32位; 0 - 16位)
這裏的段選擇子用來查找GDT和IDT,得到一個代碼段描述符,並最終得到代碼段的基地址,再加上圖中的偏移量就能夠得到中斷處理程序的入口了.由於中斷處理程序是在當前任務的上下文中運行的,因此可能會出現中斷處理程序與被中斷程序特權級不一致的問題,這時就會發生堆棧切換.對於由軟中斷所產生的中斷和異常CPU要求,CPL必須小於等於門的DPL.
在整個中斷處理程序中,CPU會將TF置成0,以禁止中斷處理程序單步執行,並將NT置成0,以在使用IRET指令返回時是回到同一個任務.對於中斷門和陷阱門,其就在於對EFLAGS寄存器中IF標誌的處理方法不同,當調用中斷門時,IF被清除.而調用陷阱門時則不對IF進行處理.
在從中斷處理程序返回的過程中,如果當初是通過陷阱門或中斷門進入的,則從堆棧頂彈出EIP和CS,以及EFLAGS.然後根據CS寄存器選擇子的RPL字段確定返回後的特權級.值得注意的是,如果RPL爲一個內層特權級,則將會產生通用保護故障.對於需要提供出錯誤碼的中斷處理程序,則必須先人爲地從堆棧中彈出出錯誤碼,在執行IRET指令返回.
進入中斷和異常還可以通過任務門,即將中斷處理程序作爲一個任務進行處理,使用該方法即將中斷處理程序當成一個任務來看待,對於這種方式的具體操作在以後的文章中會有討論.而對於調用門由於其只能出現在GDT和LDT中,因此與我們這裏討論的中斷和異常無關.
中斷描述表(IDT)
前面我們提到了一個叫做IDT的表,這個表的作用實際上與在實模式下的IVT(中斷向量表)相同.不過在具體內容上IDT要比IVT豐富的多,在IDT中裝載的是我們前面介紹過的門描述符,而不僅僅向IVT那樣僅包含一箇中斷處理程序的地址.
IDT是由門描述符組成的一個數組,每個門描述符對應一箇中斷/異常向量.像全局描述符(GDT)一樣,在系統中IDT也僅存在一個.其可以保存在內存中的任何位置,CPU通過訪問IDTR寄存器獲取IDT的位置.IDTR的長度爲48位,其中包括保存IDT的32位線性地址和16位的大小.對於IDTR寄存器的操作包含兩個指令,一個是LIDT,另一個SIDT.LIDT用來將指定的IDT所在線性地址和其長度裝入IDTR寄存器.而SIDT則是將IDTR寄存器的內容讀出.值得注意的是,LIDT僅能在CPL爲0時執行,而SIDT則不受此限制,可以運行在任何特權級下.
當系統發生中斷或異常時,CPU會以所產生的中斷向量號爲索引去查找IDT,通過找到的門描述符,轉到中斷處理程序處執行.
下面是設置中斷描述表的代碼
#define PARAM_1 ebp+8+4*0
_LIDT:
push ebp
mov ebp, esp
mov eax, [PARAM_1]
lidt [eax]
pop ebp
ret
_SIDT:
push ebp
mov ebp, esp
mov eax, [PARAM_1]
sidt [eax]
pop ebp
ret
#define ACS_PRESENT 0x80
#define ACS_INT 0x0E
#define ACS_INT_GATE (ACS_INT | ACS_PRESENT)
#define ACS_DPL_0 0x00
#define ACS_DPL_1 0x20
#define ACS_DPL_2 0x40
#define ACS_DPL_3 0x60
//IDTR結構
typedef struct IDT_REG {
word limit;
dword base;}IDT_REG;
//中斷描述符
typedef struct INT_DESCRIPTOR{
word offs0_15;
word sel;
byte paramcnt;
byte attrs;
word offs16_31;}INT_DESCRIPTOR;
//設置中斷描述表
static void setup_IDT(){
dword i;
//清空IDT
memset (&idt, 0, sizeof(idt));
// Int 0Dh - 通用保護故障
idt[0x0D].offs0_15 = ((dword)(&isr_0D_wrapper))&0xFFFF;
idt[0x0D].offs16_31 = ((dword)(&isr_0D_wrapper)) >> 16;
idt[0x0D].sel = 8;
idt[0x0D].paramcnt = 0;
idt[0x0D].attrs = ACS_INT_GATE;
// Int 0Eh - 頁面錯誤
idt[0x0E].offs0_15 = ((dword)(&isr_0E_wrapper))&0xFFFF;
idt[0x0E].offs16_31 = ((dword)(&isr_0E_wrapper)) >> 16;
idt[0x0E].sel = 8;
idt[0x0E].paramcnt = 0;
idt[0x0E].attrs = ACS_INT_GATE;
// IRQ0...0Fh 設置0x20爲起始中斷向量號,從0x20到0x2F的初始化
//省略若干項
idt[0x26].offs0_15 = ((dword)(&fd_handler))&0xFFFF;
idt[0x26].offs16_31 = ((dword)(&fd_handler)) >> 16;
idt[0x26].attrs = ACS_INT_GATE | ACS_DPL_1;
idt[0x26].sel = 8;
idt[0x26].paramcnt = 0;
idt[0x2E].offs0_15 = ((dword)(&ide_interrupt))&0xFFFF;
idt[0x2E].offs16_31 = ((dword)(&ide_interrupt)) >> 16;
idt[0x2E].attrs = ACS_INT_GATE | ACS_DPL_1;
idt[0x2E].sel = 8;
idt[0x2E].paramcnt = 0;
// SYS_INT 操作系統中斷調用
idt[0x30].offs0_15 = ((dword)(&isr_30_wrapper))&0xFFFF;
idt[0x30].offs16_31 = ((dword)(&isr_30_wrapper)) >> 16;
idt[0x30].sel = 8;
idt[0x30].paramcnt = 0;
idt[0x30].attrs = ACS_INT_GATE | ACS_DPL_3;
idtr.base = (dword) &idt;
idtr.limit = sizeof(idt)-1;
LIDT(&idtr);}
中斷優先級
在系統發生中斷或異常時,爲了能夠儘快處理緊急或重要的事務,系統將它們按類型賦予了不同的優先級.CPU在處理時總是優先處理優先級最高的中斷或異常,而對於同一級別的中斷或異常,則按照先進先出(FIFO)的原則處理.
當CPU在處理中斷或異常時,如果又產生了其他的中斷或異常,這時CPU會檢查產生的中斷或異常的優先級是否比當前處理的要高,如果是的話,則CPU會保存當前中斷處理的上下文,然後轉去處理優先級最高的那個中斷或者異常.對於那些未接收處理的異常,系統則將它們扔掉.而未接受處理的中斷則將保持懸掛狀態.系統之所以這樣做,主要是基於以下原因,即硬件的異常永遠爲最高優先級,所以其永遠不會被丟棄.因此丟棄的異常都是由軟件所引起,而這些異常由於未被解決,所以在系統處理完當前中斷或異常後,會重新執行這些引起異常的點,再次觸發異常,並等待解決.
下表按照優先級由高到低的順序列出了中斷和異常類型:
中斷/異常類型
調試故障
80386響應 其它故障
中斷/異常 陷阱指令INT n和INTO
的優先級 調試陷阱
NMI中斷
INTR中斷
8259A
在前面我們已經看到,在CPU處理的各種中斷中,有很大一部分是來自外部硬件設備的中斷,這些中斷通過可編程控制器(PIC)控制.在IBMPC兼容機上該控制器爲Intel 8259A芯片.
單個8259A芯片最多可以連接8箇中斷源,但由於可以最多將9個該芯片級連,因此,其最多可以接受64箇中斷源.在IBMPC機上採用2個8259A芯片級連,最多支持15箇中斷源.這兩個芯片一個叫做Master,另一個叫做Slave.之所以這麼稱呼是因爲,由於CPU只具有INTR這一個中斷線,所以Slave必須連級到Master上,佔用MasterPIC的IRQ2,將IRQ9重定向到IRQ2上.
8259A芯片處理中斷的過程,主要是通過芯片內3個內部寄存器進行的.這三個寄存器分別爲IMR,IRR和ISR其中,IMR用作過濾被屏蔽的中斷,IRR用來存放被懸掛的中斷並等待進一步處理,ISR用來保存CPU正在處理的中斷.
另外8259A芯片還有一個叫做優先級仲裁的單元.該單元的作用是在8259A同時接受到多箇中斷時,根據各個中斷的優先級,挑選具有最高優先級的中斷傳遞給CPU處理.
在大致介紹這幾個單元后,下面我們來看一些8259A在處理中斷時的具體過程.
首先,外部中斷請求(IR0到IR7)傳輸到IMR,IMR根據此中斷請求是否被屏蔽,以決定是將其丟棄,還是放入IRR中等待進一步處理.當8259A等待到一箇中斷時機時,優先級仲裁單元會從所有放入IRR中的中斷請求中挑出一個優先級最高的中斷,傳遞給CPU處理.值得注意的是中斷優先級是隨着中斷請求號降低而提高的.在CPU的INTR引腳接收到8259A發送過來的信號後,CPU會暫停執行下一條指令,並向8259A發送一個INTA信號.在8259A接收到該信號後,就會將ISR中代表該中斷的位置1,並將IRR中相應的位清零.以表示該中斷正在被CPU處理.接着CPU會向8259A再發送一個INTA信號,向其請求中斷向量號.這時,8259A會根據先前設置好的起始向量號再加上中斷請求號計算出中斷向量號,並將其放入數據總線中.這時候,如果8259A的EOI通知被設定爲自動模式,那麼8259A就會自動將ISR中剛纔置1的位清零.在CPU獲得該中斷向量號後,就會轉去調用該中斷服務程序.在處理完該中斷後如果8259A的EOI通知被設定爲人工模式,則還要向8259A發送一個EOI.通常來講,這一工作往往是在中斷服務程序中完成.在8259A接收到該EOI通知後,就會將ISR中剛纔置1的位清零.
以上就是8259A處理一箇中斷的整個過程的簡述.由於中斷請求存在着優先級,因此,如果在一箇中斷處理期間,8259A又收到了新的中斷請求,則首先跟當前處理的優先級進行比較,如果新到的中斷請求的優先級高於當前處理的中斷請求,則馬上處理新到的中斷請求,否則則將新到的中斷請求放入IRR.
對於8259A的操作,是通過端口進行的.其中,Master的端口地址爲0x20, 0x21, Slave的端口地址位0xA0,0xA1.8259A具有兩種命令,一種是ICW,其作用是用來初始化8259A芯片.另一個是OCW,其作用是用來向8259A發送命令.雖然在系統啓動後BIOS會自動初始化8259A,但這並不是我們所需要的.因爲在進入保護模式後,我們要設置IDT,因此我們必須根據所設置的IDT去初始化8259A.
對8259A的操作有兩類命令,其中一類是ICW,另一類是OCW.ICW用來對8259A進行初始化,而OCW則用來在初始化後對8259A發佈命令.有意思的是,8259A的兩個端口對於這兩類命令的發佈是有固定安排的.對於0x20和0xA0端口,你可以向它們寫入ICW1,OCW2,OCW3,讀取IRR和ISR.對於0x21和0xA1端口,你可以向它們寫入ICW2,ICW3,ICW4,並能夠讀寫IMR寄存器.
下面我們分別來討論這幾個命令
ICW1:該命令作爲初始化序列的第一條命令,一旦向端口送入該命令,8259A就認爲初始化序列開始.
位 功能
7:5 MCS-80/85模式下的中斷向量地址
4 必須設置爲1
3 0:Edge Triggered Interrupts 1:Level Triggered Interrupts
2 0:Call Address Interval of 8 1:Call Address Interval of 4
1 0:Cascaded PICs 1:Single PIC
0 0:Don't need ICW4 1:Will be Sending ICW4
在設置時,對於80x86的CPU,其應設置爲(00010001),也就是0x11.
ICW2:該命令用來指定所初始化的8259A中斷請求的起始向量.其中ICW2的低3位必須爲0,其這麼做的原因在於當該8259A接收到一箇中斷請求時,低3位會自動填充爲所接受到的向量號.因此這也就決定了我們設置的起始中斷向量,必須爲8的倍數.
ICW3:Master PIC和Slave PIC對於ICW3命令具有不同的格式
對於Master PIC,Slave PIC被接到了Master PIC的哪個IRQ上,則ICW3中相應的位就置1.在8259A中,由於SlavePIC是級連在Master PIC的IRQ2上的,因此ICW3的值應該爲(00000100),也就是0x04.而對於SlavePIC其高5位必須設置爲零,低3位爲該PIC被級連到哪個Master PIC的IRQ號,在8259A中,其SlavePIC的值爲(00000010),即0x02.
ICW4:
位 功能
7:5 保留,設置爲0
4 0:Not Special Fully Nested Mode 1:Special Fully Nested Mode
3:2 0x:Non-Buffered Mode 10:Buffered Mode - Slave 11:Buffered Mode - Master
1 0:Normal EOI 1:Auto EOI
0 0:MCS-80/85 1:8086/8088 Mode
在80x86模式下,我們採用默認的Full Nested Mode,將ICW4設置爲(000000001),即0x01.
而我們之所以我們要採用NormalEOI,其原因在於我們要允許中斷請求的按優先級搶佔.如果我們將EOI通知設定爲自動模式,那麼在CPU發出第二個INTA信號後,8259A中相應的ISR就會自動清零,而此時該中斷服務程序還沒有被調用.如果在該中斷服務程序被調用的過程中,8259A收到了優先級比當前正在處理的中斷優先級低的中斷請求,由於正在處理的中斷在ISR中相應的位已經清零,因此這個新的中斷請求就完全可以搶佔正在處理的優先級比它高的中斷服務程序.
下面是初始化8259A的代碼:
void init_8259A(byte master_vector,byte slave_vector){
outportb (PORT_8259A_M, 0x11); /* 開始對8259A進行初始化*/
outportb (PORT_8259A_S, 0x11);
outportb (PORT_8259A_M+1, master_vector); /* 起始中斷向量號*/
outportb (PORT_8259A_S+1, slave_vector);
outportb (PORT_8259A_M+1, 1<<2); /*設置對IRQ2的掩碼 */
outportb (PORT_8259A_S+1, 2); /* 設置對IRQ2的級連*/
outportb (PORT_8259A_M+1, 1); /* 完成對8259A的初始化*/
outportb (PORT_8259A_S+1, 1);}
在介紹完初始化這幾個命令後,我們開始介紹如何通過OCW對8259A進行操作.
OCW1:該命令用來屏蔽所設定的中斷請求.其操作方式是,向你要屏蔽的中斷請求所在的8259A發送一個操作控制字.需要屏蔽哪個中斷請求就將該字上相應的位置1即可.
實例代碼如下:
#define PORT_INT_MASK_M 0x21
#definePORT_INT_MASK_S 0xA1
void mask_IRQ(byte IRQ){
byte mask;
if(IRQ > 15)
return;
if(IRQ < 8){
mask = inportb(PORT_INT_MASK_M);
mask |= 1 << IRQ;
outportb(PORT_INT_MASK_M, mask);}
else{
mask = inportb(PORT_INT_MASK_S);
mask |= 1 << (IRQ-8);
outportb(PORT_INT_MASK_S, mask);}
}
void unmask_IRQ(byte IRQ){
byte mask;
if(IRQ > 15)
return;
if(IRQ < 8){
mask = inportb(PORT_INT_MASK_M);
mask &= !(1 << IRQ);
outportb(PORT_INT_MASK_M, mask);}
else{
mask = inportb(PORT_INT_MASK_S);
mask &= !(1 << (IRQ-8));
outportb(PORT_INT_MASK_S, mask);}
}
OCW2:
位 功能
7:5
4:3 Must be set to 0
2:0
如果OCW2中的bit6被設置爲0,那麼該命令將對整個8259A有效.否則,將針對bit2:0這3位所代表的IRQ進行操作.由於我們前面已經將8259A設置爲手動EOI模式,所以在這裏我們要將bit7:5設置爲(001)
OCW3:
位 功能
7 Must be set to 0
6:5 10:Reset Special Mask 11:Set Special Mask
4 Must be set to 0
3 Must be set to 1
2 0:No Poll Command 1:Poll Command
1:0 10:Next Read Returns IMR 11:Next Read Returns ISR