[置頂]全面剖析《自己動手寫操作系統》第三章---保護模式
《自己動手寫操作系統》讀後感 http://blog.csdn.net/zgh1988/article/details/7059936
全面剖析《自己動手寫操作系統》第一章 http://blog.csdn.net/zgh1988/article/details/7060032
全面剖析《自己動手寫操作系統》第二章 http://blog.csdn.net/zgh1988/article/details/7062065
- 1 ; ==========================================
- 2 ; pmtest1.asm
- 3 ; 編譯方法:nasm pmtest1.asm -o pmtest1.com
- 4 ; ==========================================
- 5
- 6 %include "pm.inc" ; 常量, 宏, 以及一些說明
- 7
- 8 org 0100h
- 9 jmp LABEL_BEGIN
- 10
- 11 [SECTION .gdt]
- 12 ; GDT
- 13 ; 段基址, 段界限 , 屬性
- 14 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
- 15 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代碼段, 32
- 16 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 顯存首地址
- 17 ; GDT 結束
- 18
- 19 GdtLen equ $ - LABEL_GDT ; GDT長度
- 20 GdtPtr dw GdtLen - 1 ; GDT界限
- 21 dd 0 ; GDT基地址
- 22
- 23 ; GDT 選擇子
- 24 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
- 25 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
- 26 ; END of [SECTION .gdt]
- 27
- 28 [SECTION .s16]
- 29 [BITS 16]
- 30 LABEL_BEGIN:
- 31 mov ax, cs
- 32 mov ds, ax
- 33 mov es, ax
- 34 mov ss, ax
- 35 mov sp, 0100h
- 36
- 37 ; 初始化 32 位代碼段描述符
- 38 xor eax, eax
- 39 mov ax, cs
- 40 shl eax, 4
- 41 add eax, LABEL_SEG_CODE32
- 42 mov word [LABEL_DESC_CODE32 + 2], ax
- 43 shr eax, 16
- 44 mov byte [LABEL_DESC_CODE32 + 4], al
- 45 mov byte [LABEL_DESC_CODE32 + 7], ah
- 46
- 47 ; 爲加載 GDTR 作準備
- 48 xor eax, eax
- 49 mov ax, ds
- 50 shl eax, 4
- 51 add eax, LABEL_GDT ; eax <- gdt 基地址
- 52 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
- 53
- 54 ; 加載 GDTR
- 55 lgdt [GdtPtr]
- 56
- 57 ; 關中斷
- 58 cli
- 59
- 60 ; 打開地址線A20
- 61 in al, 92h
- 62 or al, 00000010b
- 63 out 92h, al
- 64
- 65 ; 準備切換到保護模式
- 66 mov eax, cr0
- 67 or eax, 1
- 68 mov cr0, eax
- 69
- 70 ; 真正進入保護模式
- 71 jmp dword SelectorCode32:0 ; 執行這一句會把 SelectorCode32 裝入 cs, 並跳轉到 Code32Selector:0 處
- 72 ; END of [SECTION .s16]
- 73
- 74
- 75 [SECTION .s32]; 32 位代碼段. 由實模式跳入.
- 76 [BITS 32]
- 77
- 78 LABEL_SEG_CODE32:
- 79 mov ax, SelectorVideo
- 80 mov gs, ax ; 視頻段選擇子(目的)
- 81
- 82 mov edi, (80 * 10 + 0) * 2 ; 屏幕第 10 行, 第 0 列。
- 83 mov ah, 0Ch ; 0000: 黑底 1100: 紅字
- 84 mov al, 'P'
- 85 mov [gs:edi], ax
- 86
- 87 ; 到此停止
- 88 jmp $
- 89
- 90 SegCode32Len equ $ - LABEL_SEG_CODE32
- 91 ; END of [SECTION .s32]
1 ; ==========================================
2 ; pmtest1.asm
3 ; 編譯方法:nasm pmtest1.asm -o pmtest1.com
4 ; ==========================================
5
6 %include "pm.inc" ; 常量, 宏, 以及一些說明
7
8 org 0100h
9 jmp LABEL_BEGIN
10
11 [SECTION .gdt]
12 ; GDT
13 ; 段基址, 段界限 , 屬性
14 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
15 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代碼段, 32
16 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 顯存首地址
17 ; GDT 結束
18
19 GdtLen equ $ - LABEL_GDT ; GDT長度
20 GdtPtr dw GdtLen - 1 ; GDT界限
21 dd 0 ; GDT基地址
22
23 ; GDT 選擇子
24 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
25 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
26 ; END of [SECTION .gdt]
27
28 [SECTION .s16]
29 [BITS 16]
30 LABEL_BEGIN:
31 mov ax, cs
32 mov ds, ax
33 mov es, ax
34 mov ss, ax
35 mov sp, 0100h
36
37 ; 初始化 32 位代碼段描述符
38 xor eax, eax
39 mov ax, cs
40 shl eax, 4
41 add eax, LABEL_SEG_CODE32
42 mov word [LABEL_DESC_CODE32 + 2], ax
43 shr eax, 16
44 mov byte [LABEL_DESC_CODE32 + 4], al
45 mov byte [LABEL_DESC_CODE32 + 7], ah
46
47 ; 爲加載 GDTR 作準備
48 xor eax, eax
49 mov ax, ds
50 shl eax, 4
51 add eax, LABEL_GDT ; eax <- gdt 基地址
52 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
53
54 ; 加載 GDTR
55 lgdt [GdtPtr]
56
57 ; 關中斷
58 cli
59
60 ; 打開地址線A20
61 in al, 92h
62 or al, 00000010b
63 out 92h, al
64
65 ; 準備切換到保護模式
66 mov eax, cr0
67 or eax, 1
68 mov cr0, eax
69
70 ; 真正進入保護模式
71 jmp dword SelectorCode32:0 ; 執行這一句會把 SelectorCode32 裝入 cs, 並跳轉到 Code32Selector:0 處
72 ; END of [SECTION .s16]
73
74
75 [SECTION .s32]; 32 位代碼段. 由實模式跳入.
76 [BITS 32]
77
78 LABEL_SEG_CODE32:
79 mov ax, SelectorVideo
80 mov gs, ax ; 視頻段選擇子(目的)
81
82 mov edi, (80 * 10 + 0) * 2 ; 屏幕第 10 行, 第 0 列。
83 mov ah, 0Ch ; 0000: 黑底 1100: 紅字
84 mov al, 'P'
85 mov [gs:edi], ax
86
87 ; 到此停止
88 jmp $
89
90 SegCode32Len equ $ - LABEL_SEG_CODE32
91 ; END of [SECTION .s32]
在下面我要講述3.1中所遇到的問題和迷惑。主要以pmtest1.asm爲例,從以下幾方面進行分析和講解。
1)[SECTION .XXX]爲何物?
2)全局描述符表(GDT)、描述符(Descriptor)、全局描述符表寄存器(GDTR)、選擇子(SelectorXXX) 是什麼?用來做什麼?
3)實模式下的尋址方式與保護模式下的尋址方式的區別?
4)描述符宏定義和初始化段描述符
5)爲加載gdtr做準備工作
6)其他
1、[SECTION .XXX]爲何物?
SECTION和SEGMENT的作用相類似,就是代表“段”的意思,從整個程序來看,該程序分爲3個模塊,分別是[SECTION .gdt]、[SECITON .s16]、[SECTION .s32]三部分。我們很容易就可以看出,其中的[SECTION .gdt]應該是數據段,其他的兩個是代碼段。通過[SECTION .XXX]將程序分成不同模塊,完成不同的功能,使得程序看起來清晰明瞭。
2、描述符(Descriptor)、全局描述符表(GDT)、全局描述符表寄存器(GDTR)、選擇子(SelectorXXX) 是什麼?用來做什麼?
段,在80X86中,分段機制將內存空間分成一個或者多個線性區域,我們把這些線性區域稱爲段。我們需要將這些段區分開來,於是分段機制爲每個段賦予3個屬性,分別是:1、段基址(Base address):指定段在線性地址空間中的開始地址。2、段界限(Limit):表示了段內最大可用偏移量,也就是說它定義了段的長度。3、段屬性(Attribute):指定了段的特性,包括:可讀,可寫或者可執行,特權等級等特性。
段描述符(Descriptor),在程序中,我們需要定義一個數據結構來表示段,它同樣包含三個元素,段基址(Base),段界限(Limit),段屬性(Attribute),我們稱它爲段描述符(Descriptor)。段描述符和段就是同一個概念,每個段描述符要佔用8個字節的空間。
段描述符表(Descriptor Table),在一個程序中,不只存在一個段(段描述符)。所以我們需要將這些段描述符組織起來,於是定義了一個存儲段描述符的數組,稱爲段描述符表。
段選擇子(SelectorXXX),把所有段描述符都存儲在段描述符表中,當我們使用其中某一個段的時候,我們並不直接指向該段,而是通過該段描述符在段描述符表中的位置來訪問的。故段選擇子,就是一個16位的標識符,用來標識該段描述符在描述符表中的位置。
段描述符表可以分爲兩類,一類是全局描述符表GDT(Global Descriptor Table),一類是局部描述符表LDT(Local Descriptor Table)。系統中供所有的任務共享的是全局描述符表,而不同的任務卻是使用自己的局部描述符表。
緊接着,如何讓系統知道段描述符表在什麼地方呢?處理器提供了內存管理寄存器,分別是全局描述符表寄存器(GDTR)、局部描述符表寄存器(LDTR)。GDTR寄存器中用於存放全局描述符表GDT的32位線性基地址和16位的表的長度值。LDTR寄存器中用於存放局部描述符表LDT的32位線性基地址和16位的表的長度值。通過系統指令,lgdt將GDT的線性基址和長度值加載到GDTR寄存器中,lldt將LDT的線性基址和長度值加載到LDTR寄存器中。
下面我們來分析程序中的代碼:
- 11 [SECTION .gdt]
- 12 ; GDT
- 13 ; 段基址, 段界限 , 屬性
- 14 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
- 15 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代碼段, 32
- 16 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 顯存首地址
- 17 ; GDT 結束
- 18
- 19 GdtLen equ $ - LABEL_GDT ; GDT長度
- 20 GdtPtr dw GdtLen - 1 ; GDT界限
- 21 dd 0 ; GDT基地址
- 22
- 23 ; GDT 選擇子
- 24 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
- 25 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
- 26 ; END of [SECTION .gdt]
11 [SECTION .gdt]
12 ; GDT
13 ; 段基址, 段界限 , 屬性
14 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
15 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代碼段, 32
16 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 顯存首地址
17 ; GDT 結束
18
19 GdtLen equ $ - LABEL_GDT ; GDT長度
20 GdtPtr dw GdtLen - 1 ; GDT界限
21 dd 0 ; GDT基地址
22
23 ; GDT 選擇子
24 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
25 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
26 ; END of [SECTION .gdt]
在程序中,14、15、16行定義了3個段描述符,LABEL_GDT(空描述符),LABEL_DESC_CODE32(32位代碼段描述符),LABEL_SESC_VIDEO(顯示內存描述符)。每個描述符都包含了3個屬性,段基址、段界限、段屬性。
將三個描述符組織到一起構成一個全局段描述符表(GDT)。12-17行完成了GDT的定義。
GdtLen爲GDT的長度。
GdtPtr爲一個數據結構,裏面包含兩個元素,第一個元素是2 bytes的GDT界限。第二個元素是4 bytes的GDT的基地址。該數據結構與全局描述符表寄存器(GDTR)的數據結構相同,所以在加載GDTR的時候(源代碼55行),就是將該GdtPtr加載到GDTR中。
由於第一個段LABEL_GDT是空描述符,它僅僅代表該GDT的初始地址,所以該描述符爲空描述符,一般情況下,不爲它創建選擇子。然後該程序建立了兩個選擇子(24、25行)SelectorCode32和SelectorVideo,分別對應着這兩個段LABEL_SESC_CODE32和LABEL_DESC_VIDEO。
這段代碼的結構大家應該明白了吧,下面我要分別介紹段描述符,全局描述符表寄存器,選擇子的數據結構。
段描述符(Descriptor)的結構圖如下:
段描述符佔有8個字節,在這裏我只想提醒大家一下,段基址分別佔有BYTE2、BYTE3、BYTE4和BYTE7。在下面初始化段描述符的時候需要用到這些。
段選擇子的結構圖如下圖所示:
在這裏簡單的介紹一下,它使用(15…3)來表示索引,故每一個描述符表最多隻用213個描述符,除去第一個空描述符,則可以使用的描述符爲8191個描述符。
TI標誌着該選擇子所指向的段描述符是全局描述符,還是局部描述符。當TI=0時,表示全局描述符,當TI=1時,表示局部描述符。
RPL請求優先級,稍後下一節將會提到。
全局描述符表寄存器(GDT)的結構圖如下所示:
在這裏,你可以和上面程序中的GdtPtr數據結構做比較,是不是格式相同。2個字節表示GDT界限,4個字節表示GDT基地址。
3、實模式下的尋址方式與保護模式下的尋址方式有什麼不同?
在實模式下,也就是在8086系統下的尋址方式。 Intel 8086是16位的CPU,它有着16位的寄存器(Register),16位的數據總線(Data Bus)以及20位的地址總線(Address Bus)和1MB的尋址能力。一個地址是由段和偏移兩部分組成的,物理地址遵循這樣的計算公式:
物理地址(Physical Address) = 段值(Segment) * 16 + 偏移(Offset)
其中段值和偏移都是16位的。故尋址範圍爲1MB。
在保護模式下,有了分段機制,所以它的尋址方式發生了很大的變化。具體如下圖所示:
在保護模式下,首先使用段選擇子 在 段描述符表 中查找到相對應的 段描述符,找到32位段基址,然後在與32位的偏移量相加,得到線性地址。段基址和段偏移量都是32位的,所以尋址範圍大小爲4GB。在程序中jmp dword SlectorCode32:0的作用,就是進入保護模式下的尋址方式。其中,在使用某個段時,它的段選擇子是存儲在段寄存器中的。
這裏面存在着一個問題,是否我們每次尋址都要先去全局描述符表寄存器(GDTR)中,查找到全局描述符表(GDT)的基址,然後再次根據選擇子的索引跳轉到該描述符所在的位置,然後取得段描述符中的基址,如果這樣的話,我們裏裏外外採訪了幾次內存,太浪費時間了。實際上段寄存器結構是這樣的:
這樣的好處就是,我們可以直接獲取段描述符。
4、描述符宏定義和初始化段描述符
- 1 ; 描述符
- 2 ; usage: Descriptor Base, Limit, Attr
- 3 ; Base: dd
- 4 ; Limit: dd (low 20 bits available)
- 5 ; Attr: dw (lower 4 bits of higher byte are always 0)
- 6 %macro Descriptor 3
- 7 dw %2 & 0FFFFh ; 段界限 1 (2 字節)
- 8 dw %1 & 0FFFFh ; 段基址 1 (2 字節)
- 9 db (%1 >> 16) & 0FFh ; 段基址 2 (1 字節)
- 10 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 屬性 1 + 段界限 2 + 屬性 2 (2 字節)
- 11 db (%1 >> 24) & 0FFh ; 段基址 3 (1 字節)
- 12 %endmacro ; 共 8 字節
1 ; 描述符
2 ; usage: Descriptor Base, Limit, Attr
3 ; Base: dd
4 ; Limit: dd (low 20 bits available)
5 ; Attr: dw (lower 4 bits of higher byte are always 0)
6 %macro Descriptor 3
7 dw %2 & 0FFFFh ; 段界限 1 (2 字節)
8 dw %1 & 0FFFFh ; 段基址 1 (2 字節)
9 db (%1 >> 16) & 0FFh ; 段基址 2 (1 字節)
10 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 屬性 1 + 段界限 2 + 屬性 2 (2 字節)
11 db (%1 >> 24) & 0FFh ; 段基址 3 (1 字節)
12 %endmacro ; 共 8 字節
2、3、4、5行註釋告訴我們,該宏定義需要三個變量,分別是段基址(4 bytes),段界限(4 bytes),段屬性(dw)。
回顧剛纔的段描述符結構,該宏定義,就是將變量Base,Limit,Attr分別安插到描述符中相應的位置。Base是1,Limit是2,Attr是3
7 是將Limit低16位賦值給描述符的BYTE0和BYTE1
8 是將Base低16位賦值給描述符的BYTE2和BYTE3
9 是將Base右移16位後的低8位(也就是原Base的第16—23位)賦值給描述符的BYTE4
10是將Limit右移8位之後的第8—11位和Attr的0—7和12—15位,組合起來存儲到描述符的BYTE5和BYTE6
11是將Base右移24位後的低8位(也就是原Base的24—32位)賦值給描述符的BYTE7
初始化段描述符代碼:
- 37 ; 初始化 32 位代碼段描述符
- 38 xor eax, eax
- 39 mov ax, cs
- 40 shl eax, 4
- 41 add eax, LABEL_SEG_CODE32
- 42 mov word [LABEL_DESC_CODE32 + 2], ax
- 43 shr eax, 16
- 44 mov byte [LABEL_DESC_CODE32 + 4], al
- 45 mov byte [LABEL_DESC_CODE32 + 7], ah
37 ; 初始化 32 位代碼段描述符
38 xor eax, eax
39 mov ax, cs
40 shl eax, 4
41 add eax, LABEL_SEG_CODE32
42 mov word [LABEL_DESC_CODE32 + 2], ax
43 shr eax, 16
44 mov byte [LABEL_DESC_CODE32 + 4], al
45 mov byte [LABEL_DESC_CODE32 + 7], ah
爲什麼要初始化?你會發現這裏只是修改了段描述符LABEL_DESC_CODE32的BYTE2,BYTE4,BYTE7。是不是突然恍然大悟?因爲在我們初始化該LABEL_DESC_CODE32描述符時,將其基地址初始化爲0,所以我們要修改描述符的基地址爲其實際的地址。這也是在前面介紹段描述符的時候,我提醒大家需要注意的地方,即描述符的基地址所佔有的字節是BYTE2,BYTE4,BYTE7。
5、爲加載gdtr做準備工作
- 47 ; 爲加載 GDTR 作準備
- 48 xor eax, eax
- 49 mov ax, ds
- 50 shl eax, 4
- 51 add eax, LABEL_GDT ; eax <- gdt 基地址
- 52 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
- 53
- 54 ; 加載 GDTR
- 55 lgdt [GdtPtr]
47 ; 爲加載 GDTR 作準備
48 xor eax, eax
49 mov ax, ds
50 shl eax, 4
51 add eax, LABEL_GDT ; eax <- gdt 基地址
52 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
53
54 ; 加載 GDTR
55 lgdt [GdtPtr]
這個很好理解,我們就是對GdtPtr進行賦值,主要是初始化GDT的基地址。也就是將GDT的初始地址,賦值給GdtPtr的BYTE2,BYTE3,BYTE4,BYTE5。使GdtPtr的數據結構剛好符合GDTR,然後執行lgdt [GdtPtr],加載全局描述符表寄存器。將GDT的基地址和界限賦值給GDTR。
6、其他
至於接下來的 關中斷、打開地址線A20、切換到保護模式、進入保護模式,跳轉到32位代碼段等一系列的問題,可以從書中找到合適的解釋。
參考書籍:Linux內核完全剖析基於0.12內核