[自制操作系统] 第06回 迈入保护模式
目录
一、前景回顾
二、A20地址线
三、全局描述符表
四、CR0寄存器的PE位
五、迈入保护模式
六、测试
上回我们说到,保护模式下有着三大特点:地址映射、特权级和分时机制。本来接下来是要向这三点一一发起进攻,不过我们首先需要先迈入保护模式中,不然在实模式下讲解保护模式显得不伦不类。怎么进入保护模式呢?其实也很简单,就三个步骤:
1、打开A20地址线
2、加载全局描述符表GDT
3、将CR0寄存器的pe位置1
我们知道在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是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 $
继续完善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
回到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表中有四项,与我们事先的设计一样。
这一回就算结束了,内容还是比较多的。预知后事如何,请看下回分解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?