目錄
一、前景回顧
二、A20地址線
三、全域描述符表
四、CR0暫存器的PE位
五、邁入保護模式
六、測驗
一、前景回顧
上回我們說到,保護模式下有著三大特點:地址映射、特權級和分時機制,本來接下來是要向這三點一一發起進攻,不過我們首先需要先邁入保護模式中,不然在實模式下講解保護模式顯得不倫不類,怎么進入保護模式呢?其實也很簡單,就三個步驟:
1、打開A20地址線
2、加載全域描述符表GDT
3、將CR0暫存器的pe位置1
二、A20地址線
我們知道在8086CPU中,只有20位地址線,即A0~A19,20位地址總線表示的記憶體范圍是1MB,即0x0~0XFFFFF,若記憶體超過了1MB,是需要第21條地址線也就是A20支持的,所以說,如果地址進位到1MB以上,如0x100000,由于沒有A20地址線的支持,相當于丟掉高位的1,導致地址變成了0x00000,對于一開始的8086來說,是沒有A20地址線的說法的,隨著后來的80286CPU的誕生,24位地址總線出現了,從而能夠訪問的記憶體范圍達到了16MB,為了兼容之前的16位CPU,于是有了A20地址線,80286運行在實模式下時,A20地址線是關閉的,當訪問的地址超過1MB,便會自動回卷到0x00000開始;當運行在保護模式下后,A20地址線又被打開,訪問超過1MB的地址,系統將會直接訪問這塊物理記憶體,
總之我們現在想要進入保護模式,就需要開啟A20地址線,方法也很簡單,只需要下面3行代碼:
1 in al, 0x92 2 or al, 0000_0010B 3 out 0x92, al
三、全域描述符表
在保護模式下,記憶體段(如資料段、代碼段等)不再是簡單地用段暫存器加載一下段基址就能使用,因為前面我們說了,保護模式下增加了“保護”的作用,所以對于每一個記憶體段來說,增加了一些資訊,需要提前把段定義好才能使用,就像家庭成員需要上戶口一樣,在戶口簿上登記好了才算合法,
這些資訊比較繁雜,光靠暫存器來存盤是不行的,哪怕現在有了32位暫存器,所以我們將關于記憶體段的這些描述資訊給存盤在記憶體中,
我們需要哪些屬性來描述記憶體段呢?
首先,要解決實模式下存在的問題,實模式下的用戶程式是可以破壞存盤代碼的記憶體區域,所以需要添加一個記憶體段型別屬性來阻止這種行為;實模式下的用戶程式和作業系統是同一級別的,所以需要添加一個特權級屬性來區分用戶程式和作業系統,這個特權級屬性在后面的特權級章節會提到,其次就是一些訪問記憶體段的必要屬性條件,記憶體段是一片記憶體區域,就需要提供這段記憶體區域的段基址,是一個區域,就會有范圍,還需要對段的大小進行限制,避免越界訪問,上面的這幾個屬性就是比較重要的,還有一些其他約束屬性會在后面的章節中提到,
總之,最后這些用來描述記憶體段的屬性就被放到了一個稱作段描述符的結構中,該結構專門用來描述一個記憶體段,大小為8位元組,如下圖所示:

低32位中,前16位用來存盤段的段界限的前0~15位,后16位為段基址的前0~15位,
再看高32位,0~7位是段基址的16~23,24~31是段基址的24~31,這樣就得到了段基址的32位地址,
一個段描述符用來定義一個記憶體段,那么多個記憶體段就需要多個段描述符,我們將這些段描述符存放在一個叫做全域描述符表的地方,全域描述符表就相當于是一個段描述符陣列,全域描述符表的地址被存放在一個叫做GDTR的暫存器中,這樣CPU就能通過這個暫存器來得到全域描述符表的地址,進而獲取到各個段描述符的資訊,

GDTR暫存器中的前0~15位存放的是GDT界限,其實就是當前GDT所占用的位元組數減去1,我們知道一個段描述符占用記憶體8位元組,GDT界限最大能表示范圍為2^16=64KB大小,也就是說GDT最多能存放64KB/8B=8192個段描述符,
現在段描述符和全域描述符表都有了,我們該如何使用呢?
還是和實模式下作對比,我們知道實模式下段暫存器存放的是段基址,但是保護模式下,我們已經有了段描述符和全域描述符表后,段暫存器中就不再存放段基址,而是存放段選擇子,結構如下:

第3~15位存放的是索引值,通過該索引值在GDT中索引相對應的段描述符,從而得到段基址,我們可以看到3~15位有13位,剛好可以表示到2^13=8192,這與我們GDT的范圍剛好對應上,
RPL欄位表示的是請求者的當前特權級,會在后面的特權級章節中提到,TI用來指示選擇子是在GDT還是LDT中索引段描述符,LDT現在已經不再使用了,這里就不再拓展,
總結一下現在段描述符和記憶體段的關系:

我們現在來看如何根據選擇子和偏移地址來得到訪存地址,
假如現在選擇子是0x08,將其加載到ds暫存器后,訪問ds:0x9這樣的記憶體,其程序是:CPU會檢查選擇子0x08的低2位,00,表示當前的RPL,第3位為0,表示在GDT中索引段描述符,于是用高13位0x01在GDT中索引段描述符,得到段描述符中的段基址0x1234,將段基址0x1234加上段內偏移地址0x09相加,得到0x1234+0x9=0x123d,用0x123d作為訪存地址,
四、CR0暫存器的PE位
CR0是CPU的控制暫存器之一,PE位,即Protection Enable,此位用于啟動保護模式,是保護模式的開關,打開此位后,CPU就真正進入了保護模式,

置PE位為1也簡單,輸入如下代碼:
1 mov eax, cr0 2 or eax, 0x00000001 3 mov cr0, eax
五、邁入保護模式
我們已經將邁入保護模式的三個步驟講的比較清楚了,現在在loader.S中鍵入如下代碼,即可實作由實模式進入保護模式:
1 %include "boot.inc"
2 section loader vstart=LOADER_BASE_ADDR
3 LOADER_STACK_TOP equ LOADER_BASE_ADDR
4 jmp loader_start
5
6 ;構建gdt及其內部描述符
7 GDT_BASE: dd 0x00000000
8 dd 0x00000000
9 CODE_DESC: dd 0x0000FFFF
10 dd DESC_CODE_HIGH4
11 DATA_STACK_DESC: dd 0x0000FFFF
12 dd DESC_DATA_HIGH4
13 VIDEO_DESC: dd 0x80000007
14 dd DESC_VIDEO_HIGH4
15
16 GDT_SIZE equ $-GDT_BASE
17 GDT_LIMIT equ GDT_SIZE-1
18 times 60 dq 0 ;此處預留60個描述符的空位
19
20 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
21 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
22 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
23
24 ;以下是gdt指標,前2個位元組是gdt界限,后4個位元組是gdt的起始地址
25 gdt_ptr dw GDT_LIMIT
26 dd GDT_BASE
27
28 ;---------------------進入保護模式------------
29 loader_start:
30 ;一、打開A20地址線
31 in al, 0x92
32 or al, 0000_0010B
33 out 0x92, al
34
35 ;二、加載GDT
36 lgdt [gdt_ptr]
37
38 ;三、cr0第0位(pe)置1
39 mov eax, cr0
40 or eax, 0x00000001
41 mov cr0, eax
42
43 jmp dword SELECTOR_CODE:p_mode_start ;重繪流水線
44
45 [bits 32]
46 p_mode_start:
47 mov ax, SELECTOR_DATA
48 mov ds, ax
49 mov es, ax
50 mov ss, ax
51 mov esp, LOADER_STACK_TOP
52 mov ax, SELECTOR_VIDEO
53 mov gs, ax
54
55 mov byte [gs:160], 'p'
56 jmp $
loader.S
繼續完善boot.inc檔案:
1 ;--------------------loader和kernel ---------------
2 LOADER_BASE_ADDR equ 0x900
3 LOADER_START_SECTOR equ 0x2
4 ;-------------------gdt描述符屬性------------------
5 ;使用平坦模型,所以需要將段大小設定為4GB
6 DESC_G_4K equ 100000000000000000000000b ;表示段大小為4G
7 DESC_D_32 equ 10000000000000000000000b ;表示運算元與有效地址均為32位
8 DESC_L equ 0000000000000000000000b ;表示32位代碼段
9 DESC_AVL equ 000000000000000000000b ;忽略
10 DESC_LIMIT_CODE2 equ 11110000000000000000b ;代碼段的段界限的第2部分
11 DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;相同的值 資料段與代碼段段界限相同
12 DESC_LIMIT_VIDEO2 equ 00000000000000000000b ;第16-19位 顯存區描述符VIDEO2 書上后面的0少打了一位 這里的全是0為高位 低位即可表示段基址
13 DESC_P equ 1000000000000000b ;p判斷段是否在記憶體中,1表示在記憶體中
14 DESC_DPL_0 equ 000000000000000b
15 DESC_DPL_1 equ 010000000000000b
16 DESC_DPL_2 equ 100000000000000b
17 DESC_DPL_3 equ 110000000000000b
18 DESC_S_CODE equ 1000000000000b ;S等于1表示非系統段,0表示系統段
19 DESC_S_DATA equ DESC_S_CODE
20 DESC_S_sys equ 0000000000000b
21 DESC_TYPE_CODE equ 100000000000b ;x=1,c=0,r=0,a=0 代碼段是可執行的,非一致性,不可讀,已訪問位a清0
22 DESC_TYPE_DATA equ 001000000000b ;x=0,e=0,w=1,a=0 資料段是不可執行的,向上拓展,可寫,已訪問位a清0
23
24 DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00 ;代碼段的高四個位元組內容
25 DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00 ;資料段的高四個位元組內容
26
27 DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B
28
29
30 ;------------選擇子屬性------------
31 RPL0 equ 00b
32 RPL1 equ 01b
33 RPL2 equ 10b
34 RPL3 equ 11b
35 TI_GDT equ 000b
36 TI_LDT equ 100b
boot.inc
回到loader.S中,
第3行的LOADER_STACK_TOP equ LOADER_BASE_ADDR,LOADER_STACK_TOP表示的是保護模式下的堆疊,我們進入到保護模式后,也需要為程式指定堆疊頂指標,這里我們就將0x900賦給esp作為堆疊頂指標,
第7~14行,便是GDT的構建,這里我們實作定義了4個段描述符,第一個段描述符為空,這是GDT表的規定,從第二個開始依次是代碼段描述符、資料段和堆疊段描述符、顯存段描述符 ,
第16~17行通過地址差來獲得GDT的大小,進而用GDT大小減去1來得到GDT界限,這是為了加載GDT做準備,
第18行預留60個段描述符的位置,這是為了以后拓展做準備,
第20~22行是在構建代碼段、資料段和堆疊段、顯存段的選擇子,
第25行構建了一個gdt_ptr指標,該指標指向的地址包含有6個位元組的資料,
第29~41行便是按照前面提到的三個步驟開始從實模式進入保護模式,
第43行,使用jmp命令來重繪指令流水線,因為CPU并不知道自己即將會進入保護模式下運行,所以CPU會把后面的代碼依舊按照實模式下16位指令格式來譯碼,而這與我們期望的不符,
第45行的[bits 32]是編譯器的偽指令,目的是告訴編譯器,將后面的代碼都按照32位來譯碼,
第46~53行便是對一系列暫存器的初始化,
第55行,只是為了測驗用,當代碼執行到此處時,會在螢屏的第二行開始顯‘P’字符,
第56行,讓CPU懸停在此,
六、測驗
通過nasm和dd命令將mbr.S和loader.S編譯寫入到硬碟中,隨后啟動bochs,可以看到生成了預期的p字符,此外,還可以在boch的命令列視窗輸入info gdt命令查看GDT表,可以看到GDT表中有四項,與我們事先的設計一樣,


這一回就算結束了,內容還是比較多的,預知后事如何,請看下回分解,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/491546.html
標籤:嵌入式

