系統(tǒng)的啟動過程:
任何一臺計算機,在開機后,它要做的第一件事情就是引導(Booting),通過引導,計算機為自身搭建好運行環(huán)境,為以后OS的啟動與運行做好準備.首先,我們來看看一臺計算機是如何引導自身的.在機器加電后,電源供電穩(wěn)定后,電源會傳給8284A時鐘生成器一個"Power Good"低電位信號,隨后8284A會輸出有效的RESET信號,使CPU復位,這時CS:IP = FFFF:0000.CPU在這里執(zhí)行一條jmp far addr類指令,跳轉(zhuǎn)到實際BIOS映射代碼的位置,開始執(zhí)行BIOS代碼.
上述是機器在加電后的啟動過程,大家都知道計算機的啟動是分為冷啟動與熱啟動的,那么對于熱啟動,其過程又是怎樣的呢?其實熱啟動只不過是將鍵盤中斷程序置復位標志為1234h,然后再跳轉(zhuǎn)到BIOS處執(zhí)行,其主要是省去了在自檢過程中對存儲器的檢測.
在跳轉(zhuǎn)到BIOS后,首先會先關閉中斷,然后開始自檢(POST)工作,這個自檢主要檢測計算機最基本設備的運轉(zhuǎn)狀態(tài).其主要包括對,CPU內(nèi)部寄存器測試,BIOS芯片字節(jié)的檢查,8237 DMA控制器測試,基本32K RAM檢測等最基本內(nèi)容.由于被檢測設備在系統(tǒng)運行中的重要性,因此在此過程中,BIOS一旦檢測到任何異常,都將判為致命性錯誤,系統(tǒng)將被停機.
通過上面的自檢后,BIOS開始初始化8259可編程中斷控制器,并設置BIOS的8個主要中斷向量(int 10h-int 17h),然后初始化并測試CRT視頻接口以及顯示內(nèi)存(對于熱啟動這一步將跳過),在確認正常后,執(zhí)行其內(nèi)部的顯示卡標準驅(qū)動程序(注意這里的驅(qū)動跟安裝操作系統(tǒng)下的驅(qū)動是不一樣的),這段代碼會存放在C0000h,其主要目的是初始化顯示卡.然后BIOS會打印顯卡信息.
接著BIOS開始檢查其他設備,其包括對8259中斷控制器測試,8253定時器測試,鍵盤復位和卡鍵測試,擴展I/O測試,設置硬件中斷向量,擴展 RAM測試(這里的RAM測試會檢測除0-32K以外的整個RAM空間,對于熱啟動同樣也會跳過這一階段),.然后BIOS會搜索其他設備的ROM,如果找到,則會執(zhí)行它們.接著測試ROM-BASIC的字節(jié)檢查,測試磁盤驅(qū)動器(如:FDC等),測試打印機端口和RS-232,并設置他們的地址.
然后打開NMI(不可屏蔽中斷),最后就是調(diào)用Int 19h進行自舉.這一階段的自檢如果發(fā)生錯誤,系統(tǒng)會判斷其為一般性錯誤,并顯示出相應的提示信息.在此過程中,BIOS會將檢測收集到的數(shù)據(jù)保存在內(nèi)存低1K--2K的區(qū)域,并將BIOS中斷向量表,以及BIOS程序運行所需要的stack保存在內(nèi)存低0K--1K的地方.
下面就是系統(tǒng)自舉工作了,系統(tǒng)調(diào)用int 19h進行自舉,尋找啟動設備,如:軟驅(qū),硬盤,光驅(qū)等等.找到后系統(tǒng)讀取啟動設備的0號邏輯扇區(qū)(如是軟盤就讀取0面0道1扇區(qū)的整個內(nèi)容),并將讀取的內(nèi)容放到內(nèi)存地址為0000:7C00的地方.
當然,如果找不到啟動設備,BIOS就會調(diào)用Int 18h,并給出相應的提示信息,然后進入ROM-BASIC.(有些機器會在等待一段時間后自動進入CMOS.如:很久以前海洋的AMD 386DX/40主板)至此,BIOS的引導程序結束,CPU開始執(zhí)行0000:7C00處的代碼.在這里需要說明一下的是,BIOS的引導程序是與操作系統(tǒng)無關的,但隨后CPU開始執(zhí)行的代碼就開始與操作系統(tǒng)存在較大的相關性了,因此對于不同的操作系統(tǒng),下面這一部分可能會存在著較大的不同.不過,從目的上來講,它們是相同的,即都是為將要運行OS的內(nèi)核(Kernel)作準備.
進入這一部分的首要工作就是執(zhí)行啟動設備的引導程序.硬盤與軟盤的對于引導程序的存放結構是不同的.硬盤有一個叫做MBR(Master Boot Record)的扇區(qū),系統(tǒng)會首先執(zhí)行它,以判斷那個分區(qū)是啟動分區(qū),并讀入該分區(qū)的第一個扇區(qū),并執(zhí)行.并且在這個扇區(qū)中還存放著硬盤分區(qū)表(DPT),這個表的地位相當重要,因為它包含了各個分區(qū)的諸如:分區(qū)類型,起始位置,結束位置等重要參數(shù).下面我們來詳細介紹一下MBR的結構.MBR的結構分為三部分,首先是可執(zhí)行代碼,占446個字節(jié),然后是4個分區(qū)表,每個占16個字節(jié),共64個字節(jié),最后是簽字AA55H.
下表列出了分區(qū)信息的詳細內(nèi)容:
長度 描述
0 字節(jié) 分區(qū)狀態(tài)0:非活動分區(qū),80h活動分區(qū)(可引導)
1 字節(jié) 分區(qū)起始頭
2 字 分區(qū)起始扇區(qū)和起始柱
4 字節(jié) 分區(qū)類型
5 字節(jié) 分區(qū)中止頭
6 字 分區(qū)中止扇區(qū)和中止柱
8 雙字 分區(qū)起始絕對扇區(qū)
0Ch 雙字 分區(qū)扇區(qū)數(shù)
然后我們開始介紹MBR中的可執(zhí)行代碼部分:
首先,程序會檢測MBR的簽字是否合法,即判斷其最后字是否為AA55h.通過后,將自身移動到內(nèi)存中的其他地方,以備將來在此裝入引導分區(qū)的Boot扇區(qū).然后,程序檢查四個分區(qū)的分區(qū)狀態(tài),找出活動分區(qū),并將該分區(qū)的Boot扇區(qū)讀入到0000:7C00h處,并檢查其簽字是否合法,在通過后,程序跳轉(zhuǎn)到0000:7C00h處執(zhí)行,即將控制權交給活動分區(qū)的Boot程序;對于軟盤則沒有那么復雜,軟盤的第一個扇區(qū)就是它的Boot區(qū),系統(tǒng)自舉時將直接將其讀入到0000:7C00h處并執(zhí)行.
從功能上來講軟盤與硬盤的Boot區(qū)是相同的,其任務都是將OS的內(nèi)核(Kernel)讀入到內(nèi)存并執(zhí)行.但具體來看,由于絕大多數(shù)OS的Kernel是以文件形式存放在磁盤上的,要讀取它就要涉及到對文件系統(tǒng)的操作,這使得它們在實現(xiàn)上又是很不相同的.因此,對于Boot區(qū)的分析我們將放在后面的內(nèi)容中具體介紹.
2.保護模式簡述
最早的Intel系列的CPU只存在一種操作模式,即現(xiàn)在所說的實模式(Real Mode,以下簡稱RM).在Intel推出80286之后,為了增強CPU的處理能力,同時也為了適應當時的軟件開發(fā)需求,Intel提出了保護模式 (Protected Mode,以下簡稱PM),但在80286下的PM由于CPU本身設計的問題,并沒有使其發(fā)揮出很大的功效.在80386推出之后,Intel完善了 CPU的設計形成了最終的IA-32架構,并提出了另一種模式系統(tǒng)管理模式(System Management Mode).本章我們的討論就圍繞著這三種模式進行展開,并重點討論PM.
首先,對這三種模式做一簡單概述.
RM:此模式是主機在加電或復位后自動進入的模式,在此模式下其可以執(zhí)行16位指令,并可以切換到PM或者SMM.
PM:在此模式下,CPU能夠支持其自身的32位特性,使自身處于最高性能表現(xiàn).這些特性主要包括:
1. 最大可訪問4GB內(nèi)存空間.事實上,在RM下通過一些未公開的特性,也可以達到同樣的效果,但其對于代碼段 和堆棧空間卻是無效的.況且后面的所有特性都是基于PM的,對RM沒有效果.
2. 虛擬存儲.處于PM下的CPU其內(nèi)存管理單元(MMU)支持這項特性.前面我已經(jīng)說到,在PM下CPU最大尋址空 間為4GB,而在實際中,我們并沒有如此大的物理內(nèi)存空間.因此通過MMU,可以將外存設備(如:硬盤)的一部分 空間模擬成物理內(nèi)存進行使用.
3. 地址映射.即MMU可以在地址使用前對其進行轉(zhuǎn)換,即所謂的映射.
4. 改進的分段機制.本文后面將對此進行重點論述.
5. 內(nèi)存保護與任務保護.即在PM狀態(tài)下,引入了權限機制.通過權限控制可以達到保護相關代碼和數(shù)據(jù)的目的.
6. 改進的尋址模式.在RM下,只有常數(shù),BX或BP,SI或DI可以用來形成地址,而在RM下可以通過任意寄存器進行尋 址,并且可以包含一個為2,4或8的比例因子.
7. 多任務支持.在PM下,CPU提供了特殊的機制能夠進行快速的上下文切換.
SMM:該模式為操作系統(tǒng)實現(xiàn)特定平臺指定的功能提供了一種有效的機制.
值得注意的是,在PM下,CPU允許在受保護的情況下,執(zhí)行RM程序,這個特性被稱為虛擬8086模式(Virtual-8086 Mode),但其本質(zhì)上卻不是真正的RM.
對于三種模式關系的形象解釋可以通過下圖來描繪:
正如上面所說的,只有在PM下,CPU才能充分發(fā)揮其自身的所有特性,而計算機在啟動之后,默認的CPU操作模式卻是RM.因此擺在我們面前的一個主要問題就是如何在RM與PM之間相互切換.那么如何在RM和PM之間相互切換呢.核心步驟其實很簡單,只要改變CPU中的CR0寄存器中PE標志位的值,就可以實現(xiàn).在PE=1時,CPU進入PM,而在PE = 0時,則進入RM.但這僅僅是整個切換過程中的一小部分,在進入保護模式之前我們還需要做很多事情,其中最關鍵的就是建立好一個被稱為GDT的表.
在談到GDT之前,我們先回顧一下,在RM中,內(nèi)存中尋址的方式---段:偏移量.其中段(Segment)表明了一個基地址,其最大長度固定為 64KB(FFFFH),即16bit數(shù)所能表示的最大數(shù)值.而偏移量(Offset),就是指在指定段內(nèi)的位置.由此可見,通過段+偏移量這種表示方式,就可以表示出內(nèi)存中的絕對地址.需要指出的是,在CPU實際處理過程中,CPU會將段寄存器的值左移動4位,再與偏移量相加,形成地址,放入20位的總線當中.
在PM中,對于段模式來講,上面的尋址方式,在大部分上仍然是適用的.但由于PM是工作在32位下的,因此上面的各個值,也就都相應的變成了32位.與 RM不同的是,在PM下,一個段的長度不再固定,其可以在CPU允許的規(guī)則下任意設置.并且CPU為段模式提供了保護機制,即增加了對自身的訪問權限.因此在PM下,對于一個段,需要有三個變量給于描述,即基地址,段界限和訪問權限.
事實上,CPU將這三個值保存為一個64位長的段描述符.但出于兼容性的考慮,Intel并沒有將段寄存器改為64位可用--雖然,段寄存器在事實上確是 64位,但對于程序來講,高于16位的部分卻是不可見的--因此,我們需要另一種方法去存放這些數(shù)據(jù).Intel選擇了將這些段描述符統(tǒng)統(tǒng)存入到一個全局數(shù)組中的方法,在訪問段時,向相應的段寄存器填入該數(shù)組的下標值來實現(xiàn)間接引用.這個全局數(shù)組就稱其為GDT(全局描述符表).由于GDT可以存放在內(nèi)存中的任何位置,因此要引用它,就必須知道他的入口地址.Intel為我們提供了GDTR寄存器和LGDT指令.其中GDTR寄存器存放的是GDT的入口地址(32位)和其界限(16位),共48位.這里的入口地址是一個線性地址,界限則是表的字節(jié)長度減一.可見該表最多可以長達64KB,存儲8192條描述符號,而LGDT指令的作用就是將GDT裝載到放入GDTR寄存器當中.
顧名思義,GDT是全局描述符,因此其在內(nèi)存中存在且僅存在一個,并且它的存在對于所有的任務來講,都是可見的.顯然,這種做法對于多任務來講是不易管理的.因此,Intel又引入了LDT(局部描述符表),該描述符與GDT不同之處在于,LDT在系統(tǒng)中可以有許多個,但每個任務只允許有一個LDT,且其只能該任務自身可見.其與GDT的主要關系在于,每一個LDT都會作為一個段,存入GDT中.由于CPU在任何時刻只能執(zhí)行一個任務的代碼,因此存儲 LDT所需要的寄存器也就只需要一個,Intel將其命名為LDTR,與GDT相同,Intel為裝入LDT設置了LLDT指令.與GDT不同的是,LLDT指令的操作數(shù)卻是一個16位的段選擇子,即前面說到的要裝入的LDT在GDT中的索引值.這里需要指出的是,LDT并不是必須的,你的程序可以選擇使用,或者不使用它.
前面提到了一個新概念--段選擇子.我們說段選擇子是要引用段在GDT或LDT中的索引值,其實這種說法并不正確.因為段選擇子除了含有索引值以外,它還包含了其他內(nèi)容.
段選擇子的結構如下圖:
由于是從Word文檔中復制過來,表格無法顯示,詳情請查閱相關文檔。
段選擇子是一個16位的數(shù)據(jù)結構,其包含了三部分內(nèi)容.其中,其高13位正是前面所說的索引值,TI用來指定是在GDT中索引,還是在LDT中索引(0 = GDT, 1 = LDT),RPL則是用來指明請求特權級的.
談到這里,我們就已經(jīng)闡明了在PM的段模式下,如何引用一個內(nèi)存地址.首先,將段選擇子裝入相應的段寄存器中,然后CPU會自動根據(jù)段選擇子找到相應的段描述符,并找出基地址,最后在加上偏移量,就得到了所需要的內(nèi)存地址.
在本文開始的部分,我已經(jīng)說過GDT是進入 PM所必需的數(shù)據(jù)結構,下面就詳細的來討論一下如何設置好GDT,并將其裝入相應的寄存器.
首先必須注意的兩點是:
(1).GDT中的第一個描述符必須是空,即全為0.在程序中這個描述符不能用來進行內(nèi)存訪問,否則將產(chǎn)生General Protection異常.(2).由于GDT中的描述符都是64位長,因此為了讓CPU的訪問速度達到最快,需要將GDT的入口地址以8字節(jié)對齊,即放入8的倍數(shù)的位置.
下面,開始設置進入PM后的代碼段和數(shù)據(jù)段的描述符.
其格式如下:
G - 粒度
D/B - 大小(0 = 16位段; 1 = 32位段)
D - 保留
AVL - 用戶定義
P - 段是否存在 DPL - 描述符特權級
注意P位,這個位確定了段是否存在.這是什么意思呢.當該位被清除時,如果存在任務要訪問這個段.那么CPU會產(chǎn)生一個錯誤,并會從外存(如:硬盤)中調(diào)入該段并再次嘗試.當該位被清除時,描述符中的0到39位和48到63位能夠包含任意值.你也可以用這些空間來存儲該段在磁盤空間中的地址.
還有就是A位,CPU會在對其所在段寫入數(shù)據(jù)后,將該位置1,這樣在做段的磁盤交換時,可以決定是否將該段寫入磁盤.
下面要說的就是G位.你會發(fā)現(xiàn),在描述符中段界限僅僅為20位.那么其如何能夠設置成1MB到4GB之間的范圍呢.這里G位其了重要的作用.當G位被清零時,界限域就是段的最大合法偏移.而如果G位被置成1,那么會把描述符中的段界限左移12位形成32位界限,再將低12位全部填1.這樣,實際上就能夠指定1MB以下的任意長度,和以4K到4GB為單位的長度.
假定要在進入PM后,使代碼段和數(shù)據(jù)段能夠訪問全部線性空間,于是可將GDT設置為:
gdt dd 00000000h, 00000000h ;空
gdt.Code32 dd 0000ffffh, 00cf9a00h ;代碼段 讀/執(zhí)行 4GB空間 基地址=0 粒度=4096,386
gdt.Data32 dd 0000ffffh, 00cf9200h ;數(shù)據(jù)段 讀/寫 4GB空間 基地址=0 粒度=4096,386
這里你會發(fā)現(xiàn)GD中不同的描述符指向了同一塊內(nèi)存空間.這在系統(tǒng)中是允許的.在實際應用中,這也是經(jīng)常要使用到的,例如:操作系統(tǒng)可以將一個可執(zhí)行文件裝入數(shù)據(jù)段,然后再從同一位置開始執(zhí)行.
在設置好GDT以后,需要將其裝入相應的寄存器中.前面說過GDTR寄存器包含兩段內(nèi)容,因此我們需要先算出GDT的絕對物理地址.
GDTR的具體內(nèi)容
gdtr dw gdtr - gdt - 1;界限
dd gdt;前面GDT的地址
實現(xiàn)代碼如下:
mov eax, ds
shl eax, 4
add [gdtr+2], eax ; 生成絕對物理地址
lgdt [gdtr] ; 將gdtr裝入寄存器
到此,就完成了進入保護模式的最主要工作,可以進入PM模式了.
實現(xiàn)代碼如下:
mov eax, cr0
or al, 1
mov cr0, eax ; 修改CR0寄存器,置PE = 1
jmp dword 8:_premain32 ; 8為選擇子
你可能會問為什么要在代碼的最后添加一個jmp語句.這是因為,我們必須要清除CPU的指令預取隊列(流水線),并以此來設置CS段寄存器. 不過這僅僅是其一,還有一個重要的原因就是,我前面談到的那個段寄存器大于16位的不可見部分.需要指出的是,Intel對于這一點是未公開的,因此我下面對該問題的討論僅僅是由推斷得出來的.事實上,當我們執(zhí)行一條裝載CS寄存器的指令時,操作數(shù)被裝入了寄存器的可見部分,而CPU會自動根據(jù)操作數(shù),去設置其不可見部分.CS段寄存器所處的狀態(tài)與當前在哪個模式下并無關系.在剛剛進PM后,CS仍然認為當前處于16位段,即當前的地址仍是16位地址.因此,必須通過裝載一32位指令去切換到32位段,這也就是jmp在這里的意義.
雖然程序已經(jīng)進入了PM,但需要做的事情還遠沒有做完,因為我們還沒有配置IDT,即中斷描述符表,而要理解這個表又要牽涉到許多內(nèi)容,因此,我將在后面的文章中詳細介紹,這里就不多談了.
Intel之所以將這個模式起名為保護模式,其來源就在于特權保護.在PM下,每一個任務都擁有自己的特權級(PL).Intel將其分為四個級別,由零到三.數(shù)字越低級別則越高.例如:PL3級的程序?qū)τ谝恍┨囟ǖ闹噶?如:HLT,LGDT,LLDT等,沒有執(zhí)行的權限,并且其也不允許訪問擁有高特權級程序的數(shù)據(jù).
在PM下,I/O的訪問同樣也受到了特權保護.在EFLAGS中存在一個IOPL域.這個域的值決定了能夠執(zhí)行I/O操作的最低權限.例如,當IOPL為 3的時候,表明所有特權級的程序都能夠執(zhí)行I/O操作.這個域的值僅允許PL0級的程序進行修改,其他級的程序修改無效.
同樣,數(shù)據(jù)訪問在PM下也是受到保護的.當數(shù)據(jù)段寄存器要被加載時,CPU會將該段的描述符特權級(DPL)與一個被稱為有效特權級(EPL)的數(shù)值進行比較,如果DPL不小于EPL,則允許裝載寄存器,否則將會產(chǎn)生一個錯誤.這里的EPL就是選擇子的RPL和程序當前特權級(CPL)的數(shù)值較大的那一個.
對于堆棧,則有些不同,訪問SS寄存器,其DPL要求必須和CPL相等.
關于保護模式,本篇文章就介紹到的這里.對于保護模式的其他重要特性,如:分頁操作,多任務處理等,由于內(nèi)容很多,幾乎每一塊內(nèi)容都能當成一個專題來講,因此我將在以后的文章中對此進行詳細討論.
中斷和異常
學過8086/8088匯編的人肯定對于中斷這個概念都不陌生.在80386中,這個概念在一定程度上發(fā)生了變化,并引入了"異常"這個新概念.本篇文章就是圍繞在操作系統(tǒng)開發(fā)中涉及到中斷和異常的討論.
中斷
中斷在系統(tǒng)中是由外部事件所引起的,如:一次I/O操作的結束.其產(chǎn)生與CPU當前所執(zhí)行的指令沒有關系.從是否能夠被屏蔽來劃分,可將其分為兩類,即可屏蔽中斷與不可屏蔽中斷,其中前者由CPU的INTR引腳接收信號,后者由NMI引腳接收信號.
由于產(chǎn)生中斷的中斷源并不單一,因此在INTR接收中斷的時候,同樣還要接收一個8位的中斷向量號,以判斷是誰發(fā)出的中斷請求.CPU對于某個中斷向量號是由誰發(fā)出原則上并沒有規(guī)定.但在實際系統(tǒng)中,為了避免產(chǎn)生沖突,部分中斷向量號都有自己固定的中斷源,這一工作是由可編程中斷控制器(PIC)完成的.在80386系統(tǒng)上,該PIC是8259A.該芯片功能十分強大,不僅能向CPU提供中斷向量號,還可以自主處理中斷請求的優(yōu)先級.每個8259A 芯片可以支持8個中斷請求信號,并可進行級連.對于8259A的介紹,我們將在文章的后半部分進行.
對于是否屏蔽可屏蔽中斷可以通過8259A有選擇地控制,也可以通過CPU的CLI和STI指令實現(xiàn).CLI和STI指令可以設置EFLAGS寄存器的IF位,如果該位被清除,則CPU會禁止外部中斷傳遞信號給INTR引腳.但對于CPU內(nèi)部異常和NMI該位不起作用.在執(zhí)行這兩條指令時,必須要保證當前CPL小于等于IOPL,否則會引起通用保護故障.當然雖然NMI是不可屏蔽中斷,但通過將CMOS端口(0x70)中第7位置1這種手段也可以將 NMI也屏蔽掉的,當然需要打開該中斷將該位清零就可以了.
實現(xiàn)代碼如下:
#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執(zhí)行指令期間遇到非法指令所產(chǎn)生的.因此異常與當前指令存在著關系,例如:除零,特權級不正確等等,都會觸發(fā)異常.80386可以識別多種不同的異常,并以不同的中斷向量號來標示它們.在異常發(fā)生時,CPU就根據(jù)原先設定好的中斷向量號轉(zhuǎn)到不同的中斷處理程序(ISR)執(zhí)行.
根據(jù)是由是否可恢復和恢復點位置不同將異常劃分為三種.它們是故障(Fault),陷阱(Trap)和中止(Abort).
80386認為故障是可以排除的,因此在CPU遇到引起故障的指令的時候,會保存當前的CS和EIP值,并轉(zhuǎn)去執(zhí)行故障處理程序.在故障排除后,執(zhí)行 IRET指令回到剛在引發(fā)故障的位置,重新執(zhí)行剛才觸發(fā)故障的指令.例如,當程序企圖裝入一個不存在的段時將會引發(fā)一個故障,這時操作系統(tǒng)會將該段裝入, 并重新進行剛才的操作.
陷阱與故障的區(qū)別主要在于,在執(zhí)行陷阱處理程序之前,系統(tǒng)會保存CS和EIP的值為引起陷阱的下一條要執(zhí)行指令所在的位置.例如,軟中斷就是典型的陷阱.
中止是在系統(tǒng)發(fā)生嚴重錯誤時產(chǎn)生的.在引起中止后,當前執(zhí)行的程序不能被恢復執(zhí)行.并且系統(tǒng)在接受到中止后,中止處理程序要重新建立各種系統(tǒng)表.引起這類錯誤的主要原因是系統(tǒng)表的數(shù)據(jù)不一致或者非法等.
下表列出了80386在保護模式下的中斷和異常
向量號 名稱 異常類型 出錯代碼 相關指令
0 除法錯 故障 無 DIV,IDIV
1 調(diào)試 故障/陷阱 無 調(diào)試狀態(tài)下
3 斷點 陷阱 無 INT 3
4 溢出 陷阱 無 INTO
5 界限檢查 故障 無 BOUND
6 無效操作碼 故障 無 非法指令
7 無80X87 故障 無 ESC, WAIT再無協(xié)處理器情況下
8 雙重故障 中止 有 任何指令
9 NPX 中止 無 ESC的操作數(shù)超過段尾
0AH 無效TSS 故障 有 JMP、CALL、IRET或中斷
0BH 段不存在 故障 有 裝載段寄存器的指令
0CH 堆棧段異常 故障 有 任何使用SS寄存器的訪問
0DH 通用保護 故障 有 任何內(nèi)存訪問
0EH 頁異常 故障 有 任何內(nèi)存訪問
10H 協(xié)處理器出錯 故障 無 ESC, WAIT
11H-0FFH 軟中斷 陷阱 無 INT n
這里一些異常在發(fā)生時會將錯誤碼壓入堆棧,這些錯誤碼都是導致錯誤的選擇子.對于錯誤代碼為0的異常,其除了表示空選擇子導致的錯誤外,當CPU不能確定時也會返回該數(shù)值.
這里需要指出的是,對于異常0DH,它的產(chǎn)生沒有一個準確的原因,但通常來講是基于以下幾個原因:
1.試圖用一個超過段界限的偏移量訪問段
2.將一個不可執(zhí)行的段裝入CS寄存器
3.將一個只執(zhí)行段裝入除CS寄存器外的其他段寄存器
4.寫只讀段
5.使用空選擇子
6.轉(zhuǎn)換到一個忙任務
7.中斷描述表
8.門描述符
在系統(tǒng)中除了存儲段描述符和系統(tǒng)段描述符外,還有一類門描述符.這種描述符是用來描述控制轉(zhuǎn)移的入口點.任務內(nèi)特權級的改變和任務間且換都是通過這種描述符實現(xiàn)的.其中,門描述符共分為4種分別是,調(diào)用門(CallGates),陷阱門(Trap Gates),中斷門(Interrupt Gates)和任務門(TaskGates).由于本章內(nèi)容主要是涉及中斷和異常,因此,在這里我們將對陷阱門和中斷門進行詳細介紹,對于另兩種門會簡要的做一介紹.
陷阱門和中斷門
這兩種門是用來描述中斷和異常的入口的.其只能出現(xiàn)在IDT中(對于IDT后面將有詳細描述),不能出現(xiàn)在GDT和LDT中.
這兩種門的格式如下表:
中斷門描述符
由于是從Word文檔中復制過來,表格無法顯示,詳情請查閱相關文檔。
陷阱門描述符
由于是從Word文檔中復制過來,表格無法顯示,詳情請查閱相關文檔。
注:DPL - 描述符特權級
P - 門有效標志
D - 門規(guī)模(1 = 32位; 0 - 16位)
這里的段選擇子用來查找GDT和IDT,得到一個代碼段描述符,并最終得到代碼段的基地址,再加上圖中的偏移量就能夠得到中斷處理程序的入口了.由于中斷處理程序是在當前任務的上下文中運行的,因此可能會出現(xiàn)中斷處理程序與被中斷程序特權級不一致的問題,這時就會發(fā)生堆棧切換.對于由軟中斷所產(chǎn)生的中斷和異常CPU要求,CPL必須小于等于門的DPL.
在整個中斷處理程序中,CPU會將TF置成0,以禁止中斷處理程序單步執(zhí)行,并將NT置成0,以在使用IRET指令返回時是回到同一個任務.對于中斷門和陷阱門,其就在于對EFLAGS寄存器中IF標志的處理方法不同,當調(diào)用中斷門時,IF被清除.而調(diào)用陷阱門時則不對IF進行處理.
在從中斷處理程序返回的過程中,如果當初是通過陷阱門或中斷門進入的,則從堆棧頂彈出EIP和CS,以及EFLAGS.然后根據(jù)CS寄存器選擇子的RPL 字段確定返回后的特權級.值得注意的是,如果RPL為一個內(nèi)層特權級,則將會產(chǎn)生通用保護故障.對于需要提供出錯誤碼的中斷處理程序,則必須先人為地從堆棧中彈出出錯誤碼,在執(zhí)行IRET指令返回.
進入中斷和異常還可以通過任務門,即將中斷處理程序作為一個任務進行處理,使用該方法即將中斷處理程序當成一個任務來看待,對于這種方式的具體操作在以后的文章中會有討論.而對于調(diào)用門由于其只能出現(xiàn)在GDT和LDT中,因此與我們這里討論的中斷和異常無關.
中斷描述表(IDT)
前面我們提到了一個叫做IDT的表,這個表的作用實際上與在實模式下的IVT(中斷向量表)相同.不過在具體內(nèi)容上IDT要比IVT豐富的多,在IDT中裝載的是我們前面介紹過的門描述符,而不僅僅向IVT那樣僅包含一個中斷處理程序的地址.
IDT是由門描述符組成的一個數(shù)組,每個門描述符對應一個中斷/異常向量.像全局描述符(GDT)一樣,在系統(tǒng)中IDT也僅存在一個.其可以保存在內(nèi)存中的任何位置,CPU通過訪問IDTR寄存器獲取IDT的位置.IDTR的長度為48位,其中包括保存IDT的32位線性地址和16位的大小.對于IDTR 寄存器的操作包含兩個指令,一個是LIDT,另一個SIDT.LIDT用來將指定的IDT所在線性地址和其長度裝入IDTR寄存器.而SIDT則是將 IDTR寄存器的內(nèi)容讀出.值得注意的是,LIDT僅能在CPL為0時執(zhí)行,而SIDT則不受此限制,可以運行在任何特權級下.
當系統(tǒng)發(fā)生中斷或異常時,CPU會以所產(chǎn)生的中斷向量號為索引去查找IDT,通過找到的門描述符,轉(zhuǎn)到中斷處理程序處執(zhí)行.
下面是設置中斷描述表的代碼
#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 操作系統(tǒng)中斷調(diào)用
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);}
中斷優(yōu)先級
在系統(tǒng)發(fā)生中斷或異常時,為了能夠盡快處理緊急或重要的事務,系統(tǒng)將它們按類型賦予了不同的優(yōu)先級.CPU在處理時總是優(yōu)先處理優(yōu)先級最高的中斷或異常,而對于同一級別的中斷或異常,則按照先進先出(FIFO)的原則處理.
當CPU在處理中斷或異常時,如果又產(chǎn)生了其他的中斷或異常,這時CPU會檢查產(chǎn)生的中斷或異常的優(yōu)先級是否比當前處理的要高,如果是的話,則CPU會保存當前中斷處理的上下文,然后轉(zhuǎn)去處理優(yōu)先級最高的那個中斷或者異常.對于那些未接收處理的異常,系統(tǒng)則將它們?nèi)拥?而未接受處理的中斷則將保持懸掛狀態(tài).系統(tǒng)之所以這樣做,主要是基于以下原因,即硬件的異常永遠為最高優(yōu)先級,所以其永遠不會被丟棄.因此丟棄的異常都是由軟件所引起,而這些異常由于未被解決,所以在系統(tǒng)處理完當前中斷或異常后,會重新執(zhí)行這些引起異常的點,再次觸發(fā)異常,并等待解決.
下表按照優(yōu)先級由高到低的順序列出了中斷和異常類型:
中斷/異常類型
調(diào)試故障
80386響應 其它故障
中斷/異常 陷阱指令INT n和INTO
的優(yōu)先級 調(diào)試陷阱
NMI中斷
INTR中斷
8259A
在前面我們已經(jīng)看到,在CPU處理的各種中斷中,有很大一部分是來自外部硬件設備的中斷,這些中斷通過可編程控制器(PIC)控制.在IBMPC兼容機上該控制器為Intel 8259A芯片.
單個8259A芯片最多可以連接8個中斷源,但由于可以最多將9個該芯片級連,因此,其最多可以接受64個中斷源.在IBMPC機上采用2個8259A芯片級連,最多支持15個中斷源.這兩個芯片一個叫做Master,另一個叫做Slave.之所以這么稱呼是因為,由于CPU只具有INTR這一個中斷線, 所以Slave必須連級到Master上,占用MasterPIC的IRQ2,將IRQ9重定向到IRQ2上.
8259A芯片處理中斷的過程,主要是通過芯片內(nèi)3個內(nèi)部寄存器進行的.這三個寄存器分別為IMR,IRR和ISR其中,IMR用作過濾被屏蔽的中斷,IRR用來存放被懸掛的中斷并等待進一步處理,ISR用來保存CPU正在處理的中斷.
另外8259A芯片還有一個叫做優(yōu)先級仲裁的單元.該單元的作用是在8259A同時接受到多個中斷時,根據(jù)各個中斷的優(yōu)先級,挑選具有最高優(yōu)先級的中斷傳遞給CPU處理.
在大致介紹這幾個單元后,下面我們來看一些8259A在處理中斷時的具體過程.
首先,外部中斷請求(IR0到IR7)傳輸?shù)絀MR,IMR根據(jù)此中斷請求是否被屏蔽,以決定是將其丟棄,還是放入IRR中等待進一步處理.當8259A 等待到一個中斷時機時,優(yōu)先級仲裁單元會從所有放入IRR中的中斷請求中挑出一個優(yōu)先級最高的中斷,傳遞給CPU處理.值得注意的是中斷優(yōu)先級是隨著中斷請求號降低而提高的.在CPU的INTR引腳接收到8259A發(fā)送過來的信號后,CPU會暫停執(zhí)行下一條指令,并向8259A發(fā)送一個INTA信號.在 8259A接收到該信號后,就會將ISR中代表該中斷的位置1,并將IRR中相應的位清零.以表示該中斷正在被CPU處理.接著CPU會向8259A再發(fā)送一個INTA信號,向其請求中斷向量號.這時,8259A會根據(jù)先前設置好的起始向量號再加上中斷請求號計算出中斷向量號,并將其放入數(shù)據(jù)總線中.這時候,如果8259A的EOI通知被設定為自動模式,那么8259A就會自動將ISR中剛才置1的位清零.在CPU獲得該中斷向量號后,就會轉(zhuǎn)去調(diào)用該中斷服務程序.在處理完該中斷后如果8259A的EOI通知被設定為人工模式,則還要向8259A發(fā)送一個EOI.通常來講,這一工作往往是在中斷服務程序中完成.在8259A接收到該EOI通知后,就會將ISR中剛才置1的位清零.
以上就是8259A處理一個中斷的整個過程的簡述.由于中斷請求存在著優(yōu)先級,因此,如果在一個中斷處理期間,8259A又收到了新的中斷請求,則首先跟當前處理的優(yōu)先級進行比較,如果新到的中斷請求的優(yōu)先級高于當前處理的中斷請求,則馬上處理新到的中斷請求,否則則將新到的中斷請求放入IRR.
對于8259A的操作,是通過端口進行的.其中,Master的端口地址為0x20, 0x21, Slave的端口地址位0xA0,0xA1.8259A具有兩種命令,一種是ICW,其作用是用來初始化8259A芯片.另一個是OCW,其作用是用來向 8259A發(fā)送命令.雖然在系統(tǒng)啟動后BIOS會自動初始化8259A,但這并不是我們所需要的.因為在進入保護模式后,我們要設置IDT,因此我們必須根據(jù)所設置的IDT去初始化8259A.
對8259A的操作有兩類命令,其中一類是ICW,另一類是OCW.ICW用來對8259A進行初始化,而OCW則用來在初始化后對8259A發(fā)布命令. 有意思的是,8259A的兩個端口對于這兩類命令的發(fā)布是有固定安排的.對于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的倍數(shù).
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,其原因在于我們要允許中斷請求的按優(yōu)先級搶占.如果我們將EOI通知設定為自動模式,那么在CPU發(fā)出第二個INTA信號后,8259A中相應的ISR就會自動清零,而此時該中斷服務程序還沒有被調(diào)用.如果在該中斷服務程序被調(diào)用的過程中,8259A收到了優(yōu)先級比當前正在處理的中斷優(yōu)先級低的中斷請求,由于正在處理的中斷在ISR中相應的位已經(jīng)清零,因此這個新的中斷請求就完全可以搶占正在處理的優(yōu)先級比它高的中斷服務程序.
下面是初始化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發(fā)送一個操作控制字.需要屏蔽哪個中斷請求就將該字上相應的位置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進行操作.由于我們前面已經(jīng)將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