在匯編語言中,或者你有學習過諸如微機原理或計算機組成原理等課程的話,那么你很可能聽說過實模式和保護模式的概念。他們到底是什么,有什么不同,又如何尋址?
在王爽的《匯編語言》最后,有關於Intel微處理器的三種工作模式的介紹。
繼Intel 8086推出之后,Intel又推出了划時代的80386微處理器,它可以在實模式、保護模式和虛擬8086模式下工作,從那以后的微處理器都提供了這三種工作模式,直到現在。Intel系列微處理器的三種工作模式如下:
- 實模式:工作模式相當於一個8086
- 保護模式:提供支持多任務環境的工作方式,建立保護機制
- 虛擬8086模式:可以從保護模式切換至其中的一種8086工作方式,這種方式的提供使用戶可以方便的在保護模式下運行一個或多個8086程序
當我們的系統開機時,cpu首先工作在實模式下完成一些工作,之后跳入保護模式,為我們的系統提供多任務環境的支持。而當我們需要在保護模式的系統上運行實模式下的程序時(比如學習匯編時所用的DOS系統),我們就需要在當前的保護模式下弄一個“假”的實模式,這就是虛擬8086模式。
GDT和描述符
在實模式下(可以理解為工作在8086上時),我們的CPU是16位的,提供了16位的寄存器,16位數據總線,20位的地址總線,可尋址范圍位1M。物理地址遵循下面的計算公式:
其中的段地址和偏移地址都是16位的。
從80386開始,Intel家族的CPU進入了32位時代,這時候CPU有32位的地址總線,所以可尋址范圍為4G。CPU同樣擁有的是32位的寄存器,一個寄存器即可尋址4GB的空間。
在實模式下,我們采用段地址:偏移地址的尋址方式是因為我們只有16為的寄存器,單個寄存器的尋址范圍達不到1MB,但現在我們擁有了32位的寄存器,單個寄存器的可尋址范圍已經可以達到4GB了,那么是不是就不需要段寄存器了?答案是否定的。在保護模式下,地址仍然采用“段地址:偏移地址”的方式來表示,只是段的概念發生了根本性的變化。
實模式下,段值(段地址的值)還是地址的一部分。在保護模式下,雖然段值仍然由原來的16位的cs、ds等寄存器表示,但是此時它們僅僅是一個索引,這些個索引指向一個數據結構的表項,表項中詳細定義了一個段的起始地址、界限、屬性等內容,這個數據結構,叫做GDT(其實還可能是LDT,我們先討論大多數情況),GDT中的每一個表項,叫做描述符
尋址過程
我們在來看一下保護模式下的尋址過程。在此之前,有幾點要說明:
- GDT是一個數據結構,它是保存在內存中的,所以它應該有一個起始地址,它是一系列描述符的集合
- GDT的起始地址由一個專門的寄存器來存放 -- gdtr,gdtr寄存器是48位的,這個寄存器我們稍后在探討
- GDT中的每一個描述符描述一個段,其中包括段的起始地址(基址)等屬性
- 保護模式的偏移地址和實模式下的是相同的,只不過是32位
好了,下面有一張圖,我們可以看着這張圖過一遍保護模式下是如何尋址的。
- 尋址時,先找到gdtr寄存器,從中得到GDT的基址
- 有了GDT的基址,又有段寄存器中保存的索引,可以得到段寄存器“所指”的那個表項,既所指的那個描述符
- 得到了描述符,就可以從描述符中得到該描述符所描述的那個段的起始地址
- 有了段的起始地址,將偏移地址拿過來與之相加,便能得到最后的線性地址
- 有了線性地址(虛擬地址),經過變換,即可得到相應的物理地址
相信到這里,你已經對尋址過程有了個大概的了解,然后我們看看我們上面所未詳細提及的東西
gdtr寄存器
gdtr是一個48位的寄存器,其中保存了GDT的基地址和界限(或者說GDT的長度),高32位為GDT的基地址,低16位為界限。還記得保護模式中的段寄存器也是16位的嗎,它們和gdtr中的界限是對應的啊。
描述符
GDT中的每個描述符占8個字節,其結構如下
我們可以不用管其中的屬性,僅看段基址和段界限。是不是和上面的尋址聯系上了呢。
你可能會問,問什么段基址和段界限都被分開了,卻不放在一起?這主要還是歷史遺留問題,我們就不在探討了。
代碼
光看理論終究還是水中月,我們看一段簡單的代碼實際體會一下。
[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
上面的代碼中,我們定義了一個角.gdt的段,其中前三個LABLE_xxx后是我們用一個叫Descriptor宏定義了三個選擇子,其中的數值並不一定正確,因為我們只是定義了,還並沒有初始化。 Descriptor的作用是將段基址、段界限和屬性放在一個選擇子中相應的位置,其定義在文章末尾,感興趣的話可以看下。
GdtPtr
是不是和gdtr中所放的內容一樣呢?沒錯,當我們在實模式進入保護模式之前,我們需要將GdtPtr的值加載到gdtr寄存器:使用指令lgdt [GdtPtr]
那最后兩個GDT選擇子又是什么呢?好像是描述符相對於GDT基地址的偏移,其實並不全對,它稍稍復雜一些,如下圖所示。
其中TI和RPL是選擇子的一些屬性,剩下的高13位表示的是描述符在描述符表的位置,即GDT中第幾個描述符
最后,我們看一下如何使用上面的東西吧
[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
上述代碼將一個字母P顯示在屏幕上。gs中保存的是顯存的選擇子,edi為偏移地址,然后使用mov [gs:edi], ax
將ax的內容寫入到地址為gs所指的描述符中的段基址+edi的內存處,由於這里寫入的是顯存,所以將會將一個字母P顯示在屏幕上。
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 字節
完
參考:
- 《匯編語言》 王爽
- 《一個操作系統的實現》 於淵