文章目錄
這是第三章 保護模式 第一部分,也是之前我在微機原理與接口技術中學過的一部分知識,但是當時學得不透徹,這裏下點功夫吧。
3.1 認識保護模式
1. 實模式跳轉到保護模式
(1) 具體代碼和實驗
代碼1 chapter3/a/pmtest1.asm
做了這些工作:
- 定義了一個叫做
GDT
的數據結構; - 後面16位代碼段進行了一些和
GDT
相關的操作; - 程序最後跳轉到32位代碼段,操作了一下顯存。
; ==========================================
; pmtest1.asm
; 編譯方法:nasm pmtest1.asm -o pmtest1.bin
; ==========================================
%include "pm.inc" ; 常量, 宏, 以及一些說明
org 07c00h
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]
這是第三章第一個代碼實驗,照着書中給出的代碼編譯後,dd if=pmtest1.bin of=a.img bs=512 count=1 conv=notrunc
了一個 a.img
,然後啓動 bochs
時發現報錯:
<img src="https://img-blog.csdnimg.cn/20200703223453700.png)看了一下書,發現書中說:“將第2章中我們用過的軟盤映像a.img和Bochs的配置文件bochsrc複製過來,並將生成的二進制寫入軟盤映像”。後者我做到了,前者沒有做。
所以,出錯的原因是——第二章中的 a.img
已經被填充爲 512
字節並且以 0xaa55
結束,BIOS因此認爲它是一個引導扇區,就去加載它。第三章中代碼沒有這樣做,纔會報錯。
解決方法當然不是直接在代碼最後面添加上 times 510-($-$$) db 0
和 dw 0xaa55
兩句代碼,先按照書上說的做。如下圖,可以在屏幕右側看到一個紅色的字母 P ,然後再也不動了。因爲程序最後一部分代碼寫入了兩個字節到顯存中。
當然,如果註釋掉全部類似 [SECTION ...]
的語句,然後在代碼最後加上那兩句話,也可以正常運行:
我用 ndisasm
反彙編新的 pmtest1.bin
後得到的 dispmtest1.asm
最後幾個字節如下,足以證明這種方法的可行性:
000001F8 0000 add [bx+si],al
000001FA 0000 add [bx+si],al
000001FC 0000 add [bx+si],al
000001FE 55 push bp
000001FF AA stosb
(2) 詳細分析
代碼的具體分析見下面。有一部分內容可能需要看了後面的書纔看得懂。
分析 ① GDT段:
[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 .gdt
中定義了 3
個描述符 Descriptor
,是個結構體數組,數組名即爲 GDT
;GdtLen
定義爲 GDT
的長度;GdtPtr
則是一個 6
個字節的數據結構,前2個字節是 GdtLen-1
即 GDT
段界限,後面4個字節是 GDT
段基址。
Descriptor
是 chapter3/a/pm.inc
中定義的一個生成描述符的宏,類似於一個結構體:
; 描述符, 共8字節
; usage: Descriptor Base, Limit, Attr
; Base: dd 段基址32位
; Limit: dd (low 20 bits available) 段限長20位
; Attr: dw (lower 4 bits of higher byte are always 0) 段屬性 1個半字節
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1(15~0)(Byte1-Byte0)
dw %1 & 0FFFFh ; 段基址1(15~0)(Byte3~Byte2)
db (%1 >> 16) & 0FFh ; 段基址2(23~16)(Byte4)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 屬性1 + 段界限2(19~16) + 屬性2 (Byte6-Byte5)
db (%1 >> 24) & 0FFh ; 段基址3(31~24)(Byte7)
%endmacro
這個結構體完美符合全局描述符表中描述符的定義:
Descriptor
宏接受3個參數,分別是段基址、段界限和段屬性,然後將三個參數加以轉換爲圖中描述符對應的格式,從低字節到高字節。
SECTION .gdt
中定義的 3
個描述符中,GDT
第一個描述符必須是空描述符/啞描述符/NULL描述符,這是處理器的規定(可能是爲了方便定義 GdtLen, GdtPtr
和選擇子),所以 LABEL_GDT
的段基址、段限長和屬性都是零,是一個空的描述符。LABEL_DESC_CODE32
是代碼段描述符,LABEL_DESC_VIDEO
是圖形顯示段的描述符。屬性這裏先不過多贅述。
接着是 3
個選擇子,直觀看來,選擇子的定義 LABEL_DESC_CODE32 - LABEL_GDT
是描述符對應GDT基地址的偏移。但是實際上,選擇子的結構如圖:
<img src="https://img-blog.csdnimg.cn/20200704001916547.png)當 RPL,TI
都爲零時,選擇子才能夠說是對應描述符相當於GDT基地址的偏移,由於描述符大小爲 8
字節,則選擇子必須爲 8
的倍數,因此其後三位必然爲零。
這一段到此結束,GDT中的描述符和相應選擇子都定義完成了。
分析 ② 16位代碼段:
[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 ; eax=0
mov ax, cs
shl eax, 4 ; eax*=16
add eax, LABEL_SEG_CODE32 ; eax=32位代碼段的標號地址
mov word [LABEL_DESC_CODE32 + 2], ax ; Byte3~Byte2, 段基址15~0
shr eax, 16 ; eax >>= 16
mov byte [LABEL_DESC_CODE32 + 4], al ; Byte4, 段基址23~16
mov byte [LABEL_DESC_CODE32 + 7], ah ; Byte7, 段基址31~24
這一段 [BITS 16]
指出其是一個16位代碼段,它初始化了 cs=ds=es=ss
等段寄存器。同時,它初始化了指向32位代碼段的描述符 LABEL_DESC_CODE32
,由於宏定義時段界限和屬性都已經指定如下:
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代碼段
真正要初始化的是段基址——這裏將 LABEL_SEG_CODE32
的物理地址賦給 eax
,然後分三個部分賦給描述符 DESC_CODE32
的相應位置。至此,這個段描述符初始化全部完成。
分析 ③ :這幾句將 GDT
的物理地址填充到了 GdtPtr
這個6字節的數據結構中。
; 爲加載 GDTR 作準備
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax<-gdt基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2]<-gdt基地址,4個字節
加載 GdtPtr
指示的6字節數據(包括GDT段限長和段基址)到 GDTR
全局描述符寄存器中,GDTR
結構如圖:
<img src="https://img-blog.csdnimg.cn/2020070400355693.png)
; 加載 GDTR
lgdt [GdtPtr]
關掉中斷。因爲保護模式下的中斷機制和實模式不同,更加複雜和強大,原有的中斷向量表不再適用。而且保護模式下,BIOS中斷都無法使用,它們是實模式下的代碼。重新設置保護模式的中斷模式之前,必須先關閉中斷:
; 關中斷
cli
打開地址線A20。原因在於:實模式下的程序只能夠尋址1MB內存,它依賴於16位的段地址左移4位,加上16位的偏移地址訪問內存。當邏輯段地址達到最大值 0xFFFFF
時再加1左移4位,超出了 20
位的範疇,進位自然丟失,回到最低地址 0x00000
。
後來,到了80286時代,處理器有24根地址線,爲了能夠在80286機器上運行8086程序而不出錯,就用8042鍵盤控制器強制第21根線 A20
爲0的做法。下面的代碼打開A20,使其爲 1
。
; 打開地址線A20
in al, 92h ; 操作端口92
or al, 00000010b
out 92h, al
切換到保護模式。CR0是處理器內部的控制寄存器,結構如下。打開第0位保護模式允許位 PE
使其爲 1
,系統就運行在保護模式之下了:
<img src="https://img-blog.csdnimg.cn/2020070416533773.png)
; 準備切換到保護模式
mov eax, cr0
or eax, 1
mov cr0, eax
跳轉,正式運行保護模式代碼。前面的cs仍然是實模式下16位代碼段的值,我們需要裝入32位代碼段的選擇子,用下面的 jmp
就可以做到這一點。這無疑是革命性的一躍!
; 真正進入保護模式, 必須加dword, 防止目標地址被截斷
jmp dword SelectorCode32:0 ; 執行這一句會把SelectorCode32裝入cs,
; 並跳轉到Code32Selector:0處
; END of [SECTION .s16]
總結上述過程,進入保護模式的主要步驟:
- 準備GDT中的描述符、GdtLen、GdtPtr、選擇子;
- 用lgdt加載gdtr;
- 關中斷,打開A20;
- 置CR0位的PE位;
- 跳轉,進入保護模式。
2. 保護模式的運行環境
我們把 pmtest1.bin
寫到了引導扇區運行,不過引導扇區空間有限,必須想個更好的方法。
一種是讓引導扇區讀取我們的代碼並執行,就像一個操作系統內核,不過這有點難。另外的方法是把程序編譯爲COM文件,然後讓DOS執行它。
-
在網站 http://bochs.sourceforge.net/guestos/freedos-img.tar.g 上下載
FreeDos
: -
採用
tar vxzf FreeDos.img.tar.gz
解壓; -
進入文件夾
freedos-img
,之後將a.img
的文件重命名爲freedos.img
,將其複製到bochs
工作的當前文件夾; -
採用上一章的方法用
bximage
生成一個新的軟盤映像,起名爲pm.img
,步驟同上次一樣,唯一不同的就是上次默認生成的文件名a.img
直接回車了,這次需要輸入pm.img
,此時當前工作目錄下多了一個pm.img
文件; -
修改當前工作目錄下的
bochsrc
配置文件,增加下面幾句話:floppya: 1_44="freedos.img", status=inserted floppyb: 1_44="pm.img", status=inserted boot: a
-
輸入
bochs -f bochsrc
,啓動bochs
,選擇[6]
,輸入c
,回車:
-
在
Bochs
中,待FreeDos
啓動後,使用format b:
格式化B:
盤:
出現了這一句話,說明之前已經格式化過一次了。所以重新再來一次format b:
。得到下來的畫面:
-
把前面的代碼
pmtest1.asm
複製一份爲pmtest1b.asm
,將其中的0x7c00
改爲0100h
,重新編譯爲pmtest1b.com
:nasm pmtest1b.asm -o pmtest1b.com
-
將
pmtest1b.com
複製到虛擬軟盤pm.img
上:sudo mount -o loop pm.img /mnt/floppy sudo cp pmtest1b.com /mnt/floppy/ sudo umount /mnt/floppy
發現提示
mount:掛載點/mnt/floppy不存在
,因此先要在/mnt
下創建一個floopy
目錄,然後重新執行上述命令。 -
在
FreeDos
中下達B:\pmtest1b.com
,如圖,右邊出現了一個紅色的字母P
:(不小心關掉了,這裏是重新啓動的FreeDos)
3. GDT(Global Descriptor Table)
(1) 32位PC機的工作模式
以下是《微機原理與接口技術》課程中提到的內容:
IA32
下,CPU有多種工作模式:
① 實模式(Real-Addressed Mode
)
② 保護模式(Protected Mode
) :應該是不支持並行的多任務。
③ 虛擬86模式(Virtual 86 Mode
)
④ 系統管理模式(System Management Mode
)
四種模式的關係如下:
我們關注的主要是前兩種。PC剛加電打開或系統復位後,工作在實模式下,它爲保護模式所需的數據結構做好各種配置和準備。之後,修改控制寄存器CR0中的保護模式允許位PE,使得PE=1
,從而讓CPU進入保護模式;當PE=0
時則返回實模式。
上面提到的16位到32位的革命性轉換,就是代碼中從16位跳轉到32位代碼段的那個歷史性的 jmp
。
(2) 從實模式到保護模式
實模式中,8086
爲16位的CPU、寄存器、數據總線和20位的地址總線(1MB的尋址能力),一個邏輯地址由段(16位)和偏移(16位)兩部分組成,段地址是地址的一部分,表示以xxxx0h開始的一段內存,物理地址=段基地址*16+偏移地址。
但是到了32位時代,尋址空間到了4GB,原來的16位寄存器已經不夠用了。爲此,我們需要保護模式,目的之一就是提供更大的尋址能力。
32位時代的地址仍然可以用段值:偏移來表示,只是段的概念發生了根本性的變化,雖然段值仍然由原來16位的 cs,ds
等段寄存器表示,但是它們已經變成了一個索引,指向數據結構GDT的一個表項,表項中詳細定義了段的起始地址、界限、屬性等內容,表項的名字是描述符(Descriptor
)。
即,GDT的作用是提供段式存儲機制,這種機制由段寄存器+GDT中的描述符共同構成。
(3) 描述符、選擇子結構和尋址方式
下面是代碼段和數據段描述符的結構圖:
此外,還有系統段描述符和門描述符。
本節代碼 pmtest1.asm
中GDT段定義了三個描述符,可以分別稱爲 DESC_DUMMY, DESC_CODE32, DESC_VIDEO
。GDT中每一個描述符都定義了一個段,其中 DESC_VIDEO
指向的是顯存。
它們如何和16位的 cs,ds,es,gs
等段寄存器起來,使這些段寄存器成爲相對於GDT的一個索引呢?在 [SECTION .s32]
中有這樣的代碼:
mov ax, SelectorVideo
mov gs, ax
在前面的GDT段中,定義了 SelectorVideo
:
; GDT 選擇子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
由此,段寄存器 gs
的值變成了 SelectorVideo
標號地址,SelectorVideo
則似乎是 DESC_VIDEO
相對於GDT段基址 LABEL_GDT
的一個偏移,即選擇子。當然,選擇子不完全是偏移,其結構如下:
<img src="https://img-blog.csdnimg.cn/20200704161815310.png)當最低的三位 TL,RPL
都爲零時,選擇子真正成爲對應描述符相對於GDT段基址的偏移。
這樣,我們明白了這些代碼的意義,gs
段寄存器值爲選擇子 SelectorVideo
,它指向GDT中對應顯存的描述符 DESC_VIDEO
,然後下面的32位代碼段,將 ax
的值寫入到顯存中偏移位 edi
的位置(段:偏移中,偏移地址的概念沒有變化)。
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
...
從 s32
這部分代碼,目前我們知道的段式尋址方式如下,邏輯地址(段:偏移)經過段機制(段選擇子和段描述符)變成線性地址(Linear Address
),這裏的線性地址可以看做是“物理地址”:
4. 描述符屬性
下面詳細介紹段描述符的幾個屬性:
P
位 (Present
) 存在位,爲1
表示段存在於內存中,否則段不在內存中;DPL
(Descriptor Privilege Level
) 描述符特權級位,0~3
,數字越小特權級越大;S
位指明描述符是數據段/代碼段(S=1
),還是系統段/門描述符(S=0
) ;TYPE
描述符,0~15
:
G
位(Granularity
)段界限粒度位,當G=0
時段界限粒度爲字節,否則爲4KB
;D/B
位:- 可執行代碼段描述符中,是
D
位,D=1
時指令默認使用32位地址及32位/8位操作數;D=0
時默認使用16位地址及16位/8位操作數; - 向下擴展數據段描述符中,是
B
位,B=1
時段的上部界限是4GB
;否則是64KB
; - 堆棧段時,
B=1
時隱式堆棧訪問指令(如push,pop,call
)使用32位堆棧指針寄存器esp
;B=0
時隱式堆棧訪問指令使用16位堆棧指針寄存器sp
。
- 可執行代碼段描述符中,是
AVL
保留位,可以被系統軟件使用。
這裏面,最難理解的是一致代碼段 Conforming Code Segment
:
-
一致:向特權級更高的一致代碼段轉移時,當前特權級會延續下去;而向特權級更高的非一致代碼段轉移時會報錯(
general-protection exception
,常規保護錯誤),除非使用調用門或者任務門。如果系統代碼不訪問受保護的資源和某些類型的異常處理,可以放入一致代碼段中,此時低特權級的程序可以訪問高特權級的一致代碼段;爲了防止低特權級的程序訪問,需要保護的系統代碼則應該放入非一致代碼段;
-
目標代碼是低特權級,則無論其是否是一致代碼段,都不能通過jmp或call轉移訪問。這樣也會導致常規保護錯誤;
-
相同特權級的代碼,可以直接訪問,無論是否是一致代碼段。
-
特別注意的是,所有數據段都是非一致的,即不可能被低特權級的代碼訪問;但是它可以被更高特權級和同特權級的代碼訪問,不用使用特定的門。
下面是節選自 chapter3/a/pm.inc
代碼的描述符類型定義:
; 描述符圖示 ...
;----------------------------------------------------------------------------
; 在下列類型值命名中:
; DA_ : Descriptor Attribute
; D : 數據段
; C : 代碼段
; S : 系統段
; R : 只讀
; RW : 讀寫
; A : 已訪問
; other: 可按照字面意思理解
;----------------------------------------------------------------------------
; 描述符類型
DA_32 EQU 4000h ; 32 位段
DA_DPL0 EQU 00h ; DPL = 0
DA_DPL1 EQU 20h ; DPL = 1
DA_DPL2 EQU 40h ; DPL = 2
DA_DPL3 EQU 60h ; DPL = 3
; 存儲段描述符類型
DA_DR EQU 90h ; 存在的只讀數據段類型值
DA_DRW EQU 92h ; 存在的可讀寫數據段屬性值
DA_DRWA EQU 93h ; 存在的已訪問可讀寫數據段類型值
DA_C EQU 98h ; 存在的只執行代碼段屬性值
DA_CR EQU 9Ah ; 存在的可執行可讀代碼段屬性值
DA_CCO EQU 9Ch ; 存在的只執行一致代碼段屬性值
DA_CCOR EQU 9Eh ; 存在的可執行可讀一致代碼段屬性值
; 系統段描述符類型
DA_LDT EQU 82h ; 局部描述符表段類型值
DA_TaskGate EQU 85h ; 任務門類型值
DA_386TSS EQU 89h ; 可用 386 任務狀態段類型值
DA_386CGate EQU 8Ch ; 386 調用門類型值
DA_386IGate EQU 8Eh ; 386 中斷門類型值
DA_386TGate EQU 8Fh ; 386 陷阱門類型值