【操作系統】Orange‘s學習筆記(二) 第三章1 認識保護模式

這是第三章 保護模式 第一部分,也是之前我在微機原理與接口技術中學過的一部分知識,但是當時學得不透徹,這裏下點功夫吧。


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 0dw 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 ,是個結構體數組,數組名即爲 GDTGdtLen 定義爲 GDT 的長度;GdtPtr 則是一個 6 個字節的數據結構,前2個字節是 GdtLen-1GDT 段界限,後面4個字節是 GDT 段基址。

Descriptorchapter3/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]

總結上述過程,進入保護模式的主要步驟:

  1. 準備GDT中的描述符、GdtLen、GdtPtr、選擇子;
  2. 用lgdt加載gdtr;
  3. 關中斷,打開A20;
  4. 置CR0位的PE位;
  5. 跳轉,進入保護模式。

2. 保護模式的運行環境

我們把 pmtest1.bin 寫到了引導扇區運行,不過引導扇區空間有限,必須想個更好的方法。

一種是讓引導扇區讀取我們的代碼並執行,就像一個操作系統內核,不過這有點難。另外的方法是把程序編譯爲COM文件,然後讓DOS執行它

  1. 在網站 http://bochs.sourceforge.net/guestos/freedos-img.tar.g 上下載 FreeDos

  2. 採用 tar vxzf FreeDos.img.tar.gz 解壓;

  3. 進入文件夾 freedos-img ,之後將 a.img 的文件重命名爲freedos.img ,將其複製到 bochs 工作的當前文件夾;

  4. 採用上一章的方法用 bximage 生成一個新的軟盤映像,起名爲 pm.img ,步驟同上次一樣,唯一不同的就是上次默認生成的文件名 a.img 直接回車了,這次需要輸入 pm.img ,此時當前工作目錄下多了一個 pm.img 文件;

  5. 修改當前工作目錄下的 bochsrc 配置文件,增加下面幾句話:

    floppya: 1_44="freedos.img", status=inserted
    floppyb: 1_44="pm.img", status=inserted
    boot: a
    
  6. 輸入 bochs -f bochsrc ,啓動 bochs ,選擇 [6] ,輸入 c ,回車:

  7. Bochs 中,待 FreeDos 啓動後,使用 format b: 格式化 B: 盤:

    出現了這一句話,說明之前已經格式化過一次了。所以重新再來一次 format b: 。得到下來的畫面:

  8. 把前面的代碼 pmtest1.asm 複製一份爲 pmtest1b.asm ,將其中的 0x7c00 改爲 0100h ,重新編譯爲 pmtest1b.com

    nasm pmtest1b.asm -o pmtest1b.com
    
  9. 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 目錄,然後重新執行上述命令。

  10. 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位堆棧指針寄存器 espB=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 陷阱門類型值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章