《ORANGE-s-一个操作系统实现》保护模式(2.1)

保护模式

和开始理解操作系统一样,我们可能对什么是保护模式完全不了解,没关系,我们先从下面代码开始。

1. 认识保护模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
%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 .32]

可能上述代码,你看到一半就不耐烦了,没关系,先执行一下它,和前面一个章节一样,先生成pmtest.bin文件,看看效果再说,执行如图所示命令:
mark
得到的结果如下:
mark
mark
可以看到,在屏幕的右边有个红色的“P”,显然程序的最后一部分将其写入了显存中
(PS:该项目的地址都在第二篇博文里)

2. 代码解释

首先,看[SECTION .gdt]段,其中的Descriptor是在pm.inc中的宏。具体意义先不用管,只用了解这是一个数据结构,8字节大小。
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
; 节选自pm.inc
;描述符
; 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 字节

在这个段中,并列的三个Decriptor,所构成的数据就是GDT()
GdtLen是它的长度,GdtPtr包含6字节,前2字节是GDT的界限,后4字节是GDT的基地址.
后面两个段[SECTION .16]和[SECTION .32]很容易理解是16位和32位代码段,意思就是在16位中修改了GDT中的值,然后跳转到32位,执行最后一小段显示红色P的代码,然后进入无线循环。也就是真正进入保护模式。
从这个我们可以了解如下内容:

  • 我们定义了一个叫做GDT的数据结构(可能现在有人会想,为啥到现在都不讲什么是保护模式,它有什么用?不急慢慢往后看这部分的内容很多的)
  • 后面的16位的段,进行了一些对GDT的操作
  • 最后jmp到了32位,执行写入显存显示红字,然后结束了

我们不明白的有:

  • GDT是什么?有什么用?
  • 程序对它进行了什么操作
  • jmp SelectorCode32:0 和以前的jmp有什么不同

3.GDT(Global Descriptor Table)

首先解释保护模式和实模式。
在IA32下,cpu有保护模式和实模式两种工作模式。当我们打开PC时,cpu工作在实模式下,经过莫衷机制后,才进入保护模式。在保护模式下CPU有巨大的寻址能力,并且为32位操作系统提供了更好的硬件保障。
在Intel8086时,CPU有16位的寄存器、16位的数据总线以及20位的地址总线和1MB的寻址能力。一个地址由段和偏移两部分组成,物理地址的计算公式如下:

1
物理地址 = 段值 * 16 + 偏移

从80386开始,CPU进入32位,有32位地址线,所以寻址空间达到4GB。所以但从寻址看,16位已经不满足了。所以需要新方法来提供更强大的寻址能力。
在实模式下,16位寄存器通过段+偏移来达到1MB的寻址能力,而32为寄存器一个就可以寻址4GB的空间了,但在保护模式下,任然可用过段+偏移的方式,但是,这时的段的概念已经发生根本性的变化了。这时的段只是索引,指向了一个数据结构—GDT(也可以是LDT以后会讲)。其中的表项有个专门的明治,叫做描述符。
总的来说,GDT的作用是用来提供段式存储机制。这种机制是通过寄存器和GDT中的描述符共同提供的,下面我们看看代码段和数据段描述符的结构:
mark

在本例中的描述符有三个节选上面的 代码如下:

1
2
3
4
5
6
7
[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结束

其中DESC_VIDEO的基地址为0B000h,是显存处。
现在我们知道了GDT中的每一个描述符定义一个段,那么cs、ds等段寄存器是这样和这些段对应的:

1
2
mov ax,SelectorVideo
mov gs,ax

其中SelectorVideo为:

1
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT

其中SelectorVideo直观可以看出是DESC_VIDEO这个描述符相对于GDT基址的偏移。实际上它有专门的名称,叫做选择子,它也不是一个偏移。
总之,整个段式寻址示意图如下:
mark

到此,我们对这段代码理解的差不多了,仅剩下[SECTION .16]段了,其实也很容易了,既然下[SECTION .32]是32位程序,那么下[SECTION .16]的任务就是从实模式向保护模式跳转了。

4.实模式向保护模式的跳转

首先,我们把[SECTION .16]段,拿出来仔细看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[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]

  • 初始化32位代码段描述符
    1
    2
    3
    4
    5
       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

先把[SECTION .s32]赋值给eax,然后把他分为3个部分,分别给[LABEL_DESC_CODE32的相应位置,至此初始化完成。
然后,因为保护模式下的中方段处理机制是不一样的的,不关掉中断会出现错误。
接着打开A20,因为它默认是关闭的。(具体这块的解释因为涉及到8086的历史问题,就不再赘述了)
最后的一部分代码中的寄存器cr0如图所示
mark
所以代码中的0到1,是决定性的,因为cr0的0号位置是PE,此位是0时为实模式,为1时是保护模式!

执行完

1
mov cr0 , eax

系统就运行在保护模式下了。但是此时cs的值任然是实模式下的值,我们需要把代码段的选择子装入cs。所以才有了:

1
jmp dword SelectorCOde32:0

这个jmp看起来稍微复杂了点,因为它不得不放在16位的段中,目标却是32位的。至此,我们成功进入保护模式。
下面我们总结进入保护模式的主要步骤:

  1. 准备GDT.
  2. 用lgdt加载gdte.
  3. 关中断。
  4. 打开A20。
  5. 置cr0的PE位为1.
  6. 跳转,进入保护模式。
您的支持将鼓励我继续创作!