今天開始學習intel處理器的保護模式。書的第二章
這節講述的是如何從實模式進入保護模式。用的例子是在保護模式下向屏幕上輸出字符P
如何進入保護模式呢?主要步驟如下:
下面是書的例子:
; ========================================== ; pmtest1.asm ; 編譯方法:nasm pmtest1.asm -o pmtest1.bin ; ========================================== %include "pm.inc" ; 常量, 宏, 以及一些說明 org 0100h jmp LABEL_BEGIN [SECTION .gdt] ; GDT ; 段基址, 段界限 , 屬性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代碼段 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 顯存首地址 ; GDT 結束 GdtLen equ $ - LABEL_GDT ; GDT長度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 選擇子 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ; END of [SECTION .gdt] [SECTION .s16] [BITS 16] LABEL_BEGIN: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0100h ; 初始化 32 位代碼段描述符 xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32 mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah ; 為加載 GDTR 作准備 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt 基地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加載 GDTR lgdt [GdtPtr] ; 關中斷 cli ; 打開地址線A20 in al, 92h or al, 00000010b out 92h, al ; 准備切換到保護模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正進入保護模式 jmp dword SelectorCode32:0 ; 執行這一句會把 SelectorCode32 裝入 cs, ; 並跳轉到 Code32Selector:0 處 ; END of [SECTION .s16] [SECTION .s32]; 32 位代碼段. 由實模式跳入. [BITS 32] LABEL_SEG_CODE32: mov ax, SelectorVideo mov gs, ax ; 視頻段選擇子(目的) mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。 mov ah, 0Ch ; 0000: 黑底 1100: 紅字 mov al, 'P' mov [gs:edi], ax ; 到此停止 jmp $ SegCode32Len equ $ - LABEL_SEG_CODE32 ; END of [SECTION .s32]
用到的Descriptor在pm.inc中定義,關於Descriptor定義的內容如下:
; 描述符 ; usage: Descriptor Base, Limit, Attr ; Base: dd ; Limit: dd (low 20 bits available) ; Attr: dw (lower 4 bits of higher byte are always 0) %macro Descriptor 3 dw %2 & 0FFFFh ; 段界限1 dw %1 & 0FFFFh ; 段基址1 db (%1 >> 16) & 0FFh ; 段基址2 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 屬性1 + 段界限2 + 屬性2 db (%1 >> 24) & 0FFh ; 段基址3 %endmacro ; 共 8 字節
剛開始看到上面的代碼,我有點束手無策。因為也是最近才開始學習匯編,上面的程序我連指令都認不全。所以下面這一節對上面程序中語法的部分做一些講解
%include "pm.inc":包含文件。類似c語言中的包含.h文件。
org 07c00h:org是origin的縮寫。告訴編譯器下一條匯編語句的偏移地址是07c00h。
[SECTION .gdt]:AT&T匯編語言格式,用於定義一個節。這裡是定義一個結構體數組,數組名稱是GDT,數組內部是三個Descriptor結構。
LABEL_GDT: Descriptor 0, 0, 0:Descriptor是在pm.inc中定義的宏,8個字節。上面有列出來內部的定義。個人猜測定義中的%1、%2、%3是這裡傳進去的參數,按照位置分別是1、2、3。猜測跟shell中的位置參數類似。(現在先猜測一下,到影響繼續學習的時候再深究)。上面定義的Descriptor這個宏能夠用比較自動化的方法把段基址、段界限和段屬性安排在一個描述符中合適的位置。這兒也不是很了解,不知道自動化是如何實現的
GdtLen equ $ - LABEL_GDT:equ是偽指令。這句話的意思是用GdtLen來代替$ - LABEL_GDT。從這兒看類似於c語言中的define。
GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址
這裡定義一個結構體數組GdtPtr,共有6個字節。前2字節(處於低位)是GDT的界限;後4字節(處於高位)是GDT的基地址。
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT:這句話定義GDT的選擇子(sector)SelectorCode32。在下面講解GDT裡面會有詳細介紹。
繼續向下看
[BITS 16]:用來指明此節一個16位代碼段。
lgdt [GdtPtr]:lgdt指令用來將GdtPtr這個結構體裝入寄存器GDTR中
cli:關中斷,對應的開中斷指令是sti
這裡面還出現了新的寄存器eax,下面的圖說明了eax,ax,ah,al的關系:
00000000 00000000 00000000 00000000 |===============EAX===============|--32個0,4個字節,2個字,1個雙字 |======AX=======|--16個0,2個字節,1個字 |==AH===| --8個0,1個字節 |===AL==|--8個0,1個字節
eax是32位的寄存器,但它實際上只是在原有的8086CPU的寄存器ax上增加了一倍的數據位數而已。所以eax和ax二者並不是獨立的,而是整體與部分的關系。舉例來說,對eax直接賦值,若更改了低16位自然會改變了ax值,同樣ax又會影響eax整體。而ah,al寄存器和ax之間的關系也是如此。同樣還有ebx,ecx,edx。(上面摘抄互聯網並作了一些補充)
IA32還加了兩個段寄存器fs與gs。用來減緩es的壓力。用法與es相同。
上面這些指令比較生疏。其他的指令在學習8086匯編的時候都是學過,比較熟悉。
那指令都認識了,但是對於上面代碼的運行還是一頭霧水,接下來對代碼的含義進行分析。
看上面的代碼,
程序首先被加載到內存07c00h處,然後直接跳轉到LABEL_BEGIN處。
在LABEL_BEGIN處,程序首先使ds和es寄存器指向與cs相同的段,上節裡面說了,這是為了以後進行數據操作的時候能定位到正確的位置。然後初始化棧。
接下來初始化32位代碼段描述符(32位代碼段就是指程序最下面的[SECTION .s32])。這段初始化代碼就是將下面那個32位代碼段基址寫到GDT中對應的描述符結構中。你看GDT結構體中LABEL_DESC_CODE32那一項的段界限與屬性都定義好了,只有段基址沒有定義,上面關於初始化32位代碼描述符的作用就是初始化描述符中的基址。
要說上面的這步是干什麼的,那麼首先需要了解IA32的尋址過程了,下面有詳細的介紹。不了解的需要先跳到下面關於GDT的講解,再回來繼續看。
到這裡,GDT已經初始化好了,接下來的lgdt [GdtPtr]是把GDT的基地址和段界限加載到GDTR這個寄存器中。看看上面的GdtPtr結構體,它可不是隨意定義的。它的結構與gdtr寄存器的結構是相同的,看看下面gdtr的結構,在對比上面介紹的GdtPtr,你就知道了。
32位基址 16位界限 H-------------------------------------------------------------------------L
再下面是關中斷,因為進入保護膜是之後中斷處理機制與現在是不同的,所以在進入之前需要關中斷。如果不關中斷將會出現錯誤。
關中斷之後的代碼就是純粹為了進入保護模式做准備的了。這裡主要有兩個步驟:
首先打開地址線A20。關於A20,書上是這樣說的:
那麼什麼是A20呢?這又是一個歷史問題。8086中,“段:偏移”這樣的模式能表示的最大內存是FFFF:FFFF,即10FFEFh。可是8086只有20位的地址總線,只能尋址到1MB,那麼如果試圖訪問超過1MB的地址時會怎樣呢?實際上系統並不會發生異常,而是回卷(wrap)回去,重新從地址零開始尋址。可是,到了80286時,真的可以訪問到1MB以上的內存了,如果遇到同樣的情況,系統不會再回卷尋址,這就造成了向上不兼容,為了保證百分之百兼容,IBM想出一個辦法,使用8042鍵盤控制器來控制第20個(從零開始數)地址位,這就是A20地址線,如果不被打開,第20個地址位將會總是零。顯然,為了訪問所有的內存,我們需要把A20打開,開機時它默認是關閉的。這裡打開A20的方式是讓92h這個端口的第1位(從低位0開始)的值置為1
接下來將cr0這個寄存器的第0位置為1。為什麼要這麼做呢?這是因為當該位為0時,CPU運行於實模式,為1時,運行於保護模式。所以當將cr0的第0位置1之後,我們就相當欲閉合了進入保護模式的開關。
也就是說,“mov cr0, eax”這一句之後,系統就運行於保護模式之下了。但是,此時cs的值仍然是實模式下的值,我們需要把代碼段的選擇子裝入cs。所以,我們需要第71行的jmp指令:
jmp dword SelectorCode32:0根據尋址機制我們知道,這個跳轉的目標將是描述符DESC_CODE32對應的段的首地址,即標號LABEL_SEG_CODE32處。
到這裡,執行jmp指令後,就真正進入了保護模式。
進入保護模式後,就開始運行[SECTION .s32]段的代碼。這段代碼比較簡單:就是在屏幕的第12行80列輸出一個紅色的P,然後進入無線循環。
至此,整個程序運行完畢。
上面的介紹中只是粗略的講了一下GDT,下面對IA32為什麼引入GDT進行詳細介紹。
如果你熟悉Intel 8086匯編,那麼你一定知道Intel 8086是16位的CPU,它有著16位的寄存器(Register)、16位的數據總線(Data Bus)以及20位的地址總線(Address Bus)和1MB的尋址能力。一個地址是由段和偏移兩部分組成的,物理地址遵循這樣的計算公式:
物理地址(Physical Address)=段值(Segment)×16+偏移(Offset)
其中,段值和偏移都是16位的。
從80386開始,Intel家族的CPU進入32位時代。80386有32位地址線,所以尋址空間可以達到4GB。所以,單從尋址這方面說,使用16位寄存器的方法已經不夠用了。這時候,我們需要新的方法來提供更大的尋址能力。
在實模式下,16位的寄存器需要用“段:偏移”這種方法才能達到1MB的尋址能力,如今我們有了32位寄存器,一個寄存器就可以尋址4GB的空間,是不是從此段值就被拋棄了呢?實際上並沒有,新政策下的地址仍然用“段:偏移”這樣的形式來表示,只不過保護模式下“段”的概念發生了根本性的變化。實模式下,段值還是可以看做是地址的一部分的,段值為XXXXh表示以XXXX0h開始的一段內存。而保護模式下,雖然段值仍然由原來16位的cs、ds等寄存器表示,但此時它僅僅變成了一個索引,這個索引指向一個數據結構的一個表項,表項中詳細定義了段的起始地址、界限、屬性等內容。這個數據結構,就是GDT(還可能是LDT)。GDT中的表項也有一個專門的名字,叫做描述符(Descriptor)。
也就是說,GDT的作用是用來提供段式存儲機制,這種機制是通過段寄存器和GDT中的描述符共同提供的。其中描述符有多種:代碼段欲數據段描述符、系統段描述、門描述符。上面的程序用到了代碼段的描述符,它的結構如下:
上面除了BYTE5和BTYE6中的一堆屬性看上去有點復雜以外,其他三個部分倒還容易理解,它們分別定義了一個段的基址和界限。不過,由於歷史問題,它們都被拆開存放。至於那些屬性,我們暫時先不管它。
好了,我們回頭再來看看代碼,Descriptor這個宏用比較自動化的方法把段基址、段界限和段屬性安排在一個描述符中合適的位置,有興趣的讀者可以研究這個宏的具體內容。本例的GDT中共有3個描述符,為方便起見,在這裡我們分別稱它們為DESC_DUMMY、DESC_CODE32和DESC_VIDEO。其中DESC_VIDEO的段基址是0B8000h,顧名思義,這個描述符指向的正是顯存。
現在我們已經知道,GDT中的每一個描述符定義一個段,那麼cs、ds等段寄存器是如何和這些段對應起來的呢?你可能注意到了,在[SECTION.s32]這個段中有兩句代碼是這樣的:
mov ax, SelectorVideo mov gs, ax
看上去,段寄存器gs的值變成了SelectorVideo,我們在上文中可以看到,SelectorVideo是這樣定義的:SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT。直觀地看,它好像是DESC_VIDEO這個描述符相對於GDT基址的偏移。實際上,它有一個專門的名稱,叫做選擇子(Selector),它也不是一個偏移,而是稍稍復雜一些,它的結構如圖3.5所示。
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 描述符索引 TI RPL
不難理解,當TI和RPL都為零時,選擇子就變成了對應描述符相對於GDT基址的偏移,就好像我們程序中那樣。這點還是不太了解
看到這裡,你肯定已經明白了mov [gs:edi], ax的意思,gs值為SelectorVideo,它指示對應顯存的描述符DESC_VIDEO,這條指令將把ax的值寫入顯存中偏移位edi的位置。
總之,整個尋址方式如下圖所示:
上面關於GDT的內容引用自書本。
到這裡,整個程序講解完畢。
書上還有關於描述符屬性的詳細解釋和突破軟盤引導512字節的限制。