當前位置:編程學習大全網 - 編程語言 - 段描述符GDT LDT

段描述符GDT LDT

保護模式, (Protected Mode,或有時簡寫為 pmode) 是壹種 80286 系列和之後的 x86 兼容 CPU 操作模式。保護模式有壹些新的特色,設計用來增強 多工 和系統穩定度,像是 內存保護,分頁 系統,以及硬件支援的 虛擬內存。大部分的現今 x86 操作系統 都在保護模式下運行,包含 Linux、FreeBSD、以及 微軟 Windows 2.0 和之後版本。

另外壹種 286 和其之後 CPU 的操作模式是 真實模式,壹種向前兼容且關閉這些特色的模式。設計用來讓新的芯片可以執行舊的軟件。依照設計的規格,所有的 x86 CPU 都是在真實模式下開機來確保傳統操作系統的向前兼容性。在任何保護模式的特色可用前,他們必須要由某些程序手動地切換到保護模式。在現今的電腦,這種切換通常是由 操作系統 在開機時候必須完成的第壹件工作的壹個。它也可能當 CPU 在保護模式下運行時,使用 虛擬86模式 來執行設計給真實模式的程序碼。 盡管用軟件的方式也有某些可能在真實模式的系統下使用多工,但保護模式下內存保護的特色,可以避免有問題的程序破壞其他工作或是 操作系統 核心所擁有的內存。保護模式也有中斷正在執行程序的硬件支援,可以把 execution content 交給其他工作,得以實現 先占式多工。 大部分可以使用保護模式的 CPU 也擁有 32 位元暫存器 的特色 (例如 80386 系列和其後任何的芯片),導入了融合保護模式而成為 32 位元處理的概念。80286 芯片雖有支援保護模式,但是仍然只有 16 位元暫存器。Windows 2.0 和之後版本中的保護模式增強稱為 "386 增強模式",是因為他們除了保護模式外,還需要 32 位元的暫存器,並且無法在 286 上面執行 (即使 286 支援保護模式)。 即使在 32 位元芯片上已經打開了保護模式,但是 1 MB 以上的內存並無法存取,是由於壹種仿照 IBM XT 系統設計特性的 memory wrap-around(內存連續) 的因素。這種限制可以由打開 A20 line 來回避。 在保護模式下,前面 32 個中斷都是保留給 CPU 例外處理用。舉個例子,中斷 0D (十進制 13) 是 壹般保護模式錯物 和 中斷 00 是 除以零。 在8086/8088時代,處理器只存在壹種操作模式(Operation Mode),當時由於不存在其它操作模式,因此這種模式也沒有被命名。自從80286到80386開始,處理器增加了另外兩種操作模式——保護模式PM (Protected Mode)和系統管理模式SMM(System Management Mode),因此,8086/8088的模式被命名為實地址模式RM(Real-address Mode)。 PM是處理器的native模式,在這種模式下,處理器支持所有的指令和所有的體系結構特性,提供最高的性能和兼容性。對於所有的新型應用程序和操作系統來說,建議都使用這種模式。為了保證PM的兼容性,處理器允許在受保護的,多任務的環境下執行RM程序。這個特性被稱做虛擬8086模式(Virtual -8086 Mode),盡管它並不是壹個真正的處理器模式。Virtual-8086模式實際上是壹個PM的屬性,任何任務都可以使用它。 RM提供了Intel 8086處理器的編程環境,另外有壹些擴展(比如切換到PM或SMM的能力)。當主機被Power-up或Reset後,處理器處於RM下。 SMM是壹個對所有Intel處理器都統壹的標準體系結構特性。出現於Intel386 SL芯片。這個模式為OS實現平臺指定的功能(比如電源管理或系統安全)提供了壹種透明的機制。當外部的SMM interrupt pin(SMI#)被激活或者從APIC(Advanced Programming Interrupt Controller)收到壹個SMI,處理器將進入SMM。在SMM下,當保存當前正在運行程序的整個上下文(Context)時,處理器切換到壹個分離的地址空間。然後SMM指定的代碼或許被透明的執行。當從SMM返回時,處理器將回到被系統管理中斷之前的狀態。 由於機器在Power-up或Reset之後,處理器處於RM狀態,而對於Intel 80386以及其後的芯片,只有使用PM才能發揮出最大的作用。所以我們就面臨著壹個從RM切換到PM的問題。 本文不討論SMM,本節的重點集中於在Booting階段如何從RM切換到PM,這裏不會過多的討論PM的細節,因為《Intel Architecture Software Developer’s Manual Volume 3: System Programming》中有非常詳盡和準確的介紹。

編輯本段What is GDT

在Protected Mode下,壹個重要的必不可少的數據結構就是GDT(Global Descriptor Table)。 為什麽要有GDT?我們首先考慮壹下在Real Mode下的編程模型: 在Real Mode下,我們對壹個內存地址的訪問是通過Segment:Offset的方式來進行的,其中Segment是壹個段的Base Address,壹個Segment的最大長度是64 KB,這是16-bit系統所能表示的最大長度。而Offset則是相對於此Segment Base Address的偏移量。Base Address+Offset就是壹個內存絕對地址。由此,我們可以看出,壹個段具備兩個因素:Base Address和Limit(段的最大長度),而對壹個內存地址的訪問,則是需要指出:使用哪個段?以及相對於這個段Base Address的Offset,這個Offset應該小於此段的Limit。當然對於16-bit系統,Limit不要指定,默認為最大長度64KB,而 16-bit的Offset也永遠不可能大於此Limit。我們在實際編程的時候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)來指定Segment,CPU將段積存器中的數值向左偏移4-bit,放到20-bit的地址線上就成為20-bit的Base Address。 到了Protected Mode,內存的管理模式分為兩種,段模式和頁模式,其中頁模式也是基於段模式的。也就是說,Protected Mode的內存管理模式事實上是:純段模式和段頁式。進壹步說,段模式是必不可少的,而頁模式則是可選的——如果使用頁模式,則是段頁式;否則這是純段模式。 既然是這樣,我們就先不去考慮頁模式。對於段模式來講,訪問壹個內存地址仍然使用Segment:Offset的方式,這是很自然的。由於 Protected Mode運行在32-bit系統上,那麽Segment的兩個因素:Base Address和Limit也都是32位的。IA-32允許將壹個段的Base Address設為32-bit所能表示的任何值(Limit則可以被設為32-bit所能表示的,以2^12為倍數的任何指),而不象Real Mode下,壹個段的Base Address只能是16的倍數(因為其低4-bit是通過左移運算得來的,只能為0,從而達到使用16-bit段寄存器表示20-bit Base Address的目的),而壹個段的Limit只能為固定值64 KB。另外,Protected Mode,顧名思義,又為段模式提供了保護機制,也就說壹個段的描述符需要規定對自身的訪問權限(Access)。所以,在Protected Mode下,對壹個段的描述則包括3方面因素:[Base Address, Limit, Access],它們加在壹起被放在壹個64-bit長的數據結構中,被稱為段描述符。這種情況下,如果我們直接通過壹個64-bit段描述符來引用壹個段的時候,就必須使用壹個64-bit長的段積存器裝入這個段描述符。但Intel為了保持向後兼容,將段積存器仍然規定為16-bit(盡管每個段積存器事實上有壹個64-bit長的不可見部分,但對於程序員來說,段積存器就是16-bit的),那麽很明顯,我們無法通過16-bit長度的段積存器來直接引用64-bit的段描述符。 怎麽辦?解決的方法就是把這些長度為64-bit的段描述符放入壹個數組中,而將段寄存器中的值作為下標索引來間接引用(事實上,是將段寄存器中的高13 -bit的內容作為索引)。這個全局的數組就是GDT。事實上,在GDT中存放的不僅僅是段描述符,還有其它描述符,它們都是64-bit長,我們隨後再討論。 GDT可以被放在內存的任何位置,那麽當程序員通過段寄存器來引用壹個段描述符時,CPU必須知道GDT的入口,也就是基地址放在哪裏,所以Intel的設計者門提供了壹個寄存器GDTR用來存放GDT的入口地址,程序員將GDT設定在內存中某個位置之後,可以通過LGDT指令將GDT的入口地址裝入此積存器,從此以後,CPU就根據此積存器中的內容作為GDT的入口來訪問GDT了。 GDT是Protected Mode所必須的數據結構,也是唯壹的——不應該,也不可能有多個。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可見的,對任何壹個任務而言都是這樣。 除了GDT之外,IA-32還允許程序員構建與GDT類似的數據結構,它們被稱作LDT(Local Descriptor Table),但與GDT不同的是,LDT在系統中可以存在多個,並且從LDT的名字可以得知,LDT不是全局可見的,它們只對引用它們的任務可見,每個任務最多可以擁有壹個LDT。另外,每壹個LDT自身作為壹個段存在,它們的段描述符被放在GDT中。 IA-32為LDT的入口地址也提供了壹個寄存器LDTR,因為在任何時刻只能有壹個任務在運行,所以LDT寄存器全局也只需要有壹個。如果壹個任務擁有自身的LDT,那麽當它需要引用自身的LDT時,它需要通過LLDT將其LDT的段描述符裝入此寄存器。LLDT指令與LGDT指令不同的時,LGDT指令的操作數是壹個32-bit的內存地址,這個內存地址處存放的是壹個32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作數是壹個16-bit的選擇子,這個選擇子主要內容是:被裝入的LDT的段描述符在GDT中的索引值——這壹點和剛才所討論的通過段積存器引用段的模式是壹樣的。 LDT只是壹個可選的數據結構,妳完全可以不用它。使用它或許可以帶來壹些方便性,但同時也帶來復雜性,如果妳想讓妳的OS內核保持簡潔性,以及可移植性,則最好不要使用它。 引用GDT和LDT中的段描述符所描述的段,是通過壹個16-bit的數據結構來實現的,這個數據結構叫做Segment Selector——段選擇子。它的高13位作為被引用的段描述符在GDT/LDT中的下標索引,bit 2用來指定被引用段描述符被放在GDT中還是到LDT中,bit 0和bit 1是RPL——請求特權等級,被用來做保護目的,我們這裏不詳細討論它。 前面所討論的裝入段寄存器中作為GDT/LDT索引的就是Segment Selector,當需要引用壹個內存地址時,使用的仍然是Segment:Offset模式,具體操作是:在相應的段寄存器裝入Segment Selector,按照這個Segment Selector可以到GDT或LDT中找到相應的Segment Descriptor,這個Segment Descriptor中記錄了此段的Base Address,然後加上Offset,就得到了最後的內存地址。如下圖所示:

編輯本段Setup GDT

由上壹節的討論得知,GDT是Protected Mode所必須的數據結構,那麽我們在進入Protected Mode之前,必須設定好GDT,並通過LGDT將其裝入相應的寄存器。 盡管GDT允許被放在內存的任何位置,但由於GDT中的元素——描述符——都是64-bit長,也就是說都是8個字節,所以為了讓CPU對GDT的訪問速度達到最快,我們應該將GDT的入口地址放在以8個字節對齊,也就是說是8的倍數的地址位置。 GDT中第壹個描述符必須是壹個空描述符,也就是它的內容應該全部為0。如果引用這個描述符進行內存訪問,則是產生General Protection異常。 如果壹個OS不使用虛擬內存,段模式會是壹個不錯的選擇。但現代OS沒有不使用虛擬內存的,而實現虛擬內存的比較方便和有效的內存管理方式是頁式管理。但是在IA-32上如果我們想使用頁式管理,我們只能使用段頁式——沒有方法可以完全禁止段模式。但我們可以盡力讓段的效果降低的最小。 IA-32提供了壹種被稱作“Basic Flat Model”的分段模式可以達到這種效果。這種模式要求在GDT中至少要定義兩個段描述符,壹個用來引用Data Segment,另壹個用來引用Code Segment。這2個Segment都包含整個線性空間,即Segment Limit = 4 GB,即使實際的物理內存遠沒有那麽多,但這個空間定義是為了將來由頁式管理來實現虛擬內存。 在這裏,我們只是處於Booting階段,所以我們只需要初步設置壹下GDT,等真正進入Protected Mode,啟動了OS Kernel之後,具體OS打算如何設置GDT,使用何種內存管理模式,由Kernel自身來設置,Booting只需要給Kernel的數據段和代碼段設置全部線性空間就可以了。 段描述符的格式如下圖所示: 具體到代碼段和數據段,它們的格式如下圖所示: 下面就是在Booting階段為進入Protected Mode而設置的臨時的gdt。這裏定義了3個段描述符:第壹個是系統規定的空描述符,第2個是引用4 GB線性空間的代碼段,第3個是引用4 GB線性空間的數據段。這是"Basic Flat Model"所要求的最下GDT設置,但就booting階段,只是為了進入Protected Mode,並為內核提供壹個連續的,最大的線性空間這個目的而言,已經足夠了。 # Descriptor tables gdt: .word 0, 0, 0, 0 # dummy .word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb) .word 0 # base address = 0 .word 0x9A00 # code read/exec .word 0x00CF # granularity = 4096, 386 # (+5th nibble of limit) .word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb) .word 0 # base address = 0 .word 0x9200 # data read/write .word 0x00CF # granularity = 4096, 386 # (+5th nibble of limit)

編輯本段Load GDT

設置好GDT之後,我們需要通過LGDT指令將設定的gdt的入口地址和gdt表的大小裝入GDTR寄存器。 GDTR寄存器包括兩部分:32-bit的線性基地址,以及16-bit的GDT大小(以字節為單位)。需要註意的是,對於32-bit線性基地址,必須是32-bit絕對物理地址,而不是相對於某個段的偏移量。而我們在Booting階段,在進入Protected Mode之前,我們CS和DS設置很可能不是0,所以我們必須計算出gdt的絕對物理地址。 為了執行LGDT指令,妳需要把這兩部分內容放在內存的某個位置,然後將這個位置的內存地址作為操作數傳遞給LGDT指令。然後LGDT指令會自動將保存在這個位置的這兩部分值裝入GDTR寄存器。 # 這是存放GDTR所需的兩部分內容的位置 gdt_48: .word 0x8000 # gdt limit=2048, # 256 GDT entries .word 0, 0 # gdt base (filled in later) # 下面這段代碼用來計算GDT的32-bit線性地址,並將其裝入GDTR寄存器。 xorl %eax, %eax # Compute gdt_base movw %ds, %ax # (Convert %ds:gdt to a linear ptr) shll , %eax addl $gdt, %eax movl %eax, (gdt_48+2) lgdt gdt_48 # load gdt with whatever is appropriate

編輯本段Other Preparing Stuff

在進入Protected Mode之前,除了需要設置和裝入GDT之外,還需要做如下壹些事情: 屏蔽所有可屏蔽中斷; 裝入IDTR; 所有協處理器被正確的Reset。 由於在Real Mode和Protected Mode下的中斷處理機制有壹些不同,所以在進入Protected Mode之前,務必禁止所有可屏蔽中斷,這可以通過下面兩種方法之壹: 使用CLI指令; 對8259A可編程中斷控制器編程以屏蔽所有中斷。 即使當我們進入Protected Mode之後,也不能馬上將中斷打開,這時因為我們必須在OS Kernel中對相關的Protected Mode中斷處理所需的數據結構正確的初始化之後,才能打開中斷,否則會產生處理器異常。 在Real Mode下,中斷處理使用IVT(Interrupt Vector Table),在Protected Mode下,中斷處理使用IDT(Interrupt Descriptor Table),所以,我們必須在進入Protected Mode之前設置IDTR。 IDTR的格式和GDTR相同,IDTR的裝入方式和GDTR也相同。由於IDT中相關的中斷處理程序需要讓OS Kernel來設定,所以在Booting階段,我們只需要將IDTR中IDT的基地址和Size都設為0就可以了,隨後,等進入Protected Mode之後,由OS Kernel來真正設置它。 關於中斷機制和中斷處理,請參考 Interrupt & Exception ,這裏不再贅述。 # # 這是存放IDTR所需的兩部分內容的位置 # idt_48: .word 0 # idt limit = 0 .word 0, 0 # idt base = 0L # 對於IDTR的處理,只需要這壹條指令即可 lidt idt_48 # load idt with 0,0 # # 通過設置8259A PIC,屏蔽所有可屏蔽中斷 # movb xFF, %al # mask all interrupts for now outb %al, xA1 call delay movb xFB, %al # mask all irq's but irq2 which outb %al, x21 # is cascaded # 保證所有的協處理都被正確的Reset xorw %ax, %ax outb %al, xf0 call delay outb %al, xf1 call delay # Delay is needed after doing I/O delay: outb %al,x80 ret 5. Let's Go 好了,壹切準備就緒,Fire!:) 進入Protected Mode,還是進入Real Mode,完全靠CR0寄存器的PE標誌位來控制:如果PE=1,則CPU切換到PM,否則,則進入RM。 設置CR0-PE位的方法有兩種: 第壹種是80286所使用的LMSW指令,後來的80386及更高型號的CPU為了保持向後兼容,都保留了這個指令。這個指令只能影響最低的4 bit,即PE,MP,EM和TS,對其它的沒有影響。 # #通過LMSW指令進入Protected Mode # movw , %ax # protected mode (PE) bit lmsw %ax # This is it! 第二種是Intel所建議的在80386以後的CPU上使用的進入PM的方式,即通過MOV指令。MOV指令可以設置CR0寄存器的所有域的值。 # #通過MOV指令進入Protected Mode # movl %cr0, %eax xorb , %al # set PE = 1 movl %eax, %cr0 # go!! OK,現在已經進入Protected Mode了。 很簡單,right?But It's not over yet!

編輯本段Start Kernel

我們已經從Real Mode進入Protected Mode,現在我們馬上就要啟動OS Kernel了。 OS Kernel運行在32-bit段模式,而當前我們卻仍然處於16-bit段模式。這是怎麽回事?為了了解這個問題,我們需要仔細探討壹下IA-32的段模式的實現方法。 IA-32***提供了6個16-bit段寄存器:CS,DS,SS,ES,FS,GS。但事實上,這16-bit只是對程序員可見的部分,但每個寄存器仍然包括64-bit的不可見部分。 可見部分是為了供程序員裝載段寄存器,但壹旦裝載完成,CPU真正使用的就只是不可見部分,可見部分就完全沒有用了。 不可見部分存放的內容是什麽?具體格式我沒有看到相關資料,但可以確定的是隱藏部分的內容和段描述符的內容是壹致的(請參考段描述的格式),只不過格式可能不完全相同。但格式對我們理解這壹點並不重要,因為程序員不可能能夠直接操作它。 我們以CS寄存器為例,對於其它寄存器也是壹樣的: 在Real Mode下,當我們執行壹個裝載CS寄存器的指令的時候(jmp,call,ret等),相關的值會被裝入CS寄存器的可見部分,但同時CPU也會根據可見部分的內容來設置不可見部分。比如我們執行"ljmp x1234, $go "之後,CS寄存器的可見部分的內容就是1234h,同時,不可見部分的32-bit Base Address域被設置為00001234h,20-bit的Limit域被設置為固定值10000h,也就是64 KB,Access Information部分的其它值我們不去考慮,只考慮其D/B位,由於執行此指令時處於Real Mode模式,所以D/B被設置為0,表示此段是壹個16-bit段。當對CS寄存器的可見部分和不可見部分的內容都被設置之後,CS寄存器的裝載工作完成。隨後當CPU需要通過CS的內容進行地址運算的時候,則僅僅引用不可見部分。 在Protected Mode下,當我們執行壹個裝載CS寄存器的指令的時候,段選擇子(Segment Selector)被裝入CS寄存器的可見部分,同時CPU根據此選擇子到相應的描述符表中(GDT或LDT)找到相應的段描述符並將其內容裝載入CS寄存器的不可見部分。隨後CPU當需要通過CS的內容進行地址運算的時候,也僅僅引用不可見部分。 從上面的描述可以看出,事實上CPU在引用段寄存器的內容進行地址運算時,Real Mode和Protected Mode是壹致的。另外,也明白了為什麽我們在Real Mode下設置的段寄存器的內容到了Protected Mode下仍然引用的是16-bit段。 那麽我們如何將CS設置為引用32-bit段?方法就像我們前面所討論的,使用jmp或call指令,引用壹個段選擇子,到GDT中裝載壹個引用32-bit段的段描述符。 需要註意的是,如果CS寄存器的內容指出當前是壹個16-bit段,那麽當前的地址模式也就是16-bit地址模式,這與妳當前是出於Real Mode還是Protected Mode無關。而我們裝載32-bit段的jmp指令或call指令必須使用的是32-bit地址模式。而我們當前的boot部分代碼是16-bit代碼,所以我們必須在此jmp/call指令前加上地址轉換前綴代碼66h。 下面的例子就是使用jmp指令裝入32-bit段。Jmpi指令的含義是段間跳轉,其Opcode為Eah,其格式為:jmpi Offset, Segment Selector。 # 由於當前的代碼是16-bit代碼,而我們要執行32-bit地址模式的指令,指令前 # 需要有地址模式切換前綴66h,如果我們直接寫jmp指令,由編譯器來生成代碼 # 的話,是無法作到這壹點的,所以我們直接寫相關數據。 .byte 0x66, 0xea # prefix + jmpi-opcode .long 0x1000 # Offset .word __KERNEL_CS # CS segment selector 上面的代碼相當於32-bit指令: jmpi 0x1000,__KERNEL_CS 如果__KERNEL_CS段選擇子所引用的段描述符設置的段空間為線形地址[0,4 GB],而我們將OS Kernel放在物理地址1000h,那麽此jmpi指令就跳轉到OS Kernel的入口處,並開始執行它。 此時,Booting階段結束,OS正式開始運行!

  • 上一篇:0元成立公司:不可錯過創業精英的創業心得
  • 下一篇:深海我們人類知多少?
  • copyright 2024編程學習大全網