保护模式
和开始理解操作系统一样,我们可能对什么是保护模式完全不了解,没关系,我们先从下面代码开始。
1. 认识保护模式
1 | "pm.inc" ;常量 ,宏 ,以及一些说明 |
可能上述代码,你看到一半就不耐烦了,没关系,先执行一下它,和前面一个章节一样,先生成pmtest.bin文件,看看效果再说,执行如图所示命令:
得到的结果如下:
可以看到,在屏幕的右边有个红色的“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中的描述符共同提供的,下面我们看看代码段和数据段描述符的结构:
在本例中的描述符有三个节选上面的 代码如下: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
2mov ax,SelectorVideo
mov gs,ax
其中SelectorVideo为:1
SelectorVideo LABEL_DESC_VIDEO - LABEL_GDT
其中SelectorVideo直观可以看出是DESC_VIDEO这个描述符相对于GDT基址的偏移。实际上它有专门的名称,叫做选择子,它也不是一个偏移。
总之,整个段式寻址示意图如下:
到此,我们对这段代码理解的差不多了,仅剩下[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[ .s16]
[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
5add 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如图所示
所以代码中的0到1,是决定性的,因为cr0的0号位置是PE,此位是0时为实模式,为1时是保护模式!
执行完1
mov cr0 , eax
系统就运行在保护模式下了。但是此时cs的值任然是实模式下的值,我们需要把代码段的选择子装入cs。所以才有了:1
jmp dword SelectorCOde32:0
这个jmp看起来稍微复杂了点,因为它不得不放在16位的段中,目标却是32位的。至此,我们成功进入保护模式。
下面我们总结进入保护模式的主要步骤:
- 准备GDT.
- 用lgdt加载gdte.
- 关中断。
- 打开A20。
- 置cr0的PE位为1.
- 跳转,进入保护模式。