目录
文章目录
- 目录
- 概述
- 初见保护模式
- 代码
- 32push.S
- 全局描述符表
- 段描述符
- GDT、LDT及选择子
- GDT
- 选择子正式介绍
- LDT
- 打开A20地址线
- 保护模式的开关,CR0寄存器的 PE位
- 进入保护模式
- mbr.S
- boot.inc
- loader.S 代码
- 结果演示
概述
实模式是 32 位 CPU 中的概念,指 32 位的 CPU 处于 16 位运行模式下的状态,其本质上还是 32 位的 CPU,就像大学生去做小学生的题一样,无非是大马拉小车了。
初见保护模式
寄存器要保持向下兼容,不能推翻之前的方案从头再来,必须在原有的基础上扩展(extend),各寄存器在原有 16 位的基础上,再次向高位扩展了 16 位,成为了 32 位寄存器。
图 4-1 中,左边已经标注名字的寄存器有通用寄存器组,名字前统一加了字符 E 表示扩展,同样,EFLAGS 寄存器和 EIP 分别在 FLAGS 和 IP 基础上扩展而成。图下边的 6 个段寄存器,依然是 16 位。
代码
32push.S
%include "boot.inc"
section push32_test vstart=0x900
jmp loader_start
gdt_addr:
;构建 gdt 及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000008
dd DESC_VIDEO_HIGH4 ; 此时 dpl 已改为 0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
gdt_ptr: dw GDT_LIMIT
dd gdt_addr
loader_start:
;--------------- 准备进入保护模式 ----------------
;1 打开 A20
;2 加载 gdt
;3 将 cr0 的 pe 位置 1
;----------------- 打开 A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al
;----------------- 加载 GDT ----------------
lgdt [gdt_ptr]
;----------------- cr0 第 0 位置 1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
; 刷新流水线,避免分支预测的影响,这种 CPU 优化策略,最怕 jmp 跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用
jmp SELECTOR_CODE:p_mode_start
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,0x900
push byte 0x7
push word 0x8
push dword 0x9
jmp $
全局描述符表
到了保护模式下,内存段(如数据段、代码段等)不再是简单地用段寄存器加载一下段基址就能用啦,段的信息增加了很多,需要提前把段定义好才能使用。就像家庭成员需要上户口一样,在户口簿上登记过才算合法。
全局描述符表(Global Descriptor Table,GDT)是保护模式下内存段的登记表,这是不同于实模式的显著特征之一。
段描述符
内存段是一片内存区域,访问内存就要提供段基址,所以要有段基址属性。
为了限制程序访问内存的范围,还要对段大小进行约束,所以要有段界限属性。
从段描述符的低32位开始:
- 段描述符的低 32 位分为两部分,前 16 位用来存储段的段界限的前 0~15 位,后 16 位用来存储段基址的 0~15 位。
段描述符的高32位(主要的属性都在段描述符的高32位):
- 0~7位是段基址的16~23,24~31位是段基址的24~31位,加上在段描述符低32位中的段基址0~15位,这下32位基地址才算齐全
- type字段和S字段
- type字段:8~11位,用来指定本描述符的类型。
- S字段:S=0,为系统段;S=1,则为数据段
一个段描述符,分为系统段和数据段,由段描述符的S字段决定。
在CPU眼里,凡是硬件需要用到的东西都可以称之为系统,凡是软件用到的东西都可以称之为数据。代码、数据、甚至包括栈,它们作为硬件的输入,都是给硬件的数据而已,所以代码段在段描述符中也属于数据段(非系统段)。
type字段和S字段配合在一起才能确定段描述符的确切类型,只有S字段的值确定后,type字段的值才有具体意义。
- S=1时,非系统段的type子字段,如下表(部分非系统段今后再写):
先看代码段:
- A位表示Accessed位,由CPU进行设置,每当该段被CPU访问过后,CPU就将此位置1;所以,创建一个新段描述符时,应该将此位置0。在调试时,根据此位便能判断该描述符是否可用了
- C表示一致性代码段,也称为依从代码段,Conforming。这个位我也不是很懂,先跳过。
- R表示可读。1可读,0不可读。用来限制代码段的访问的。如果指令执行过程中,CPU发现某些指令访问R=0的段,则会抛x出异常。
- X表示该段是否可执行,Executable。指令和数据,在CPU看来是没有区别的,都是01这样的二进制数字,所以需要用type中的X位来标识。X=1,可执行的代码段;X=0,不可执行的数据段。
- E位用来标识段的扩展方向,Extend。E=0,向上扩展,即地址越来越高,通常用于代码段和数据段。E=1,向下扩展,地址越来越低,通常用于栈段。
- W位表示段是否可写。W=1,可写,通常用于数据段;W=0,不可写,通常用于代码段。对W=0的段进行写入,CPU会抛出异常。
- 13~14位是DPL字段,Descriptor Privilege Level,即描述符特权级。特权级是保护模式才有的东西,CPU从实模式进入保护模式后,特权级自动为0。用户程序通常处于3特权级,权限最小。
- 第15位,P字段,Present,即段是否存在。P=1,段存在于内存中;P=0,则不存在。P字段是由CPU来检查的,如果P=0,CPU则抛出异常,转到相应的异常处理程序,此异常处理程序是咱们来写的,在异常处理程序处理完成后要将P置1。换句话说,CPU只负责检查,我们负责赋值。
- 第16 ~ 19位,段界限的16 ~ 19位,20位的段界限补全了。
- 第20位,AVL字段,AVaiLable,可用的。这是对用户来说的,操作系统可以随意用此位。对于硬件来说,没有专门的用途
- 第21位,L字段。用来设置是否是64位代码段。L=1,64位代码段;L=0,32位代码段。这属于保留位,32位CPU编程下,置0即可。
- 第22位,D/B字段。用来指示有效地址(段内偏移地址)及操作数的大小。
有没有觉得奇怪,实模式已经是 32 位的地址线和操作数了,难道操作数不是 32 位大小吗?其实这是为了兼容 286 的保护模式,286 的保护模式下的操作数是 16 位。既然是指定“操作数”的大小,也就是对“指令”来说的,与指令相关的内存段是代码段和栈段,所以此字段是 D 或 B。
- 对于代码段来说,此位是D位,D=0,表示指令中的有效地址和操作数是16位的,指令有效地址用IP寄存器;D=1,表示指令中的有效地址及操作数是32位的,指令有效地址用 EIP寄存器;
- 对于栈段来说,此位是 B 位,用来指定操作数大小,此操作数涉及到栈指针寄存器的选择及栈的地址上限。若 B 为 0,使用的是 sp 寄存器,也就是栈的起始地址是 16 位寄存器的最大寻址范围,0xFFFF。
若 B 为 1,使用的是 esp 寄存器,也就是栈的起始地址是 32 位寄存器的最大寻址范围,0xFFFFFFFF。
- 第23位,G字段,Granularity,粒度,用来指定段界限的单位大小。此位是用来配合段界限,与段界限一起来决定段的大小。G=0,段界限的单位是1字节,这样段最大是 2 ^ 20 * 1字节,即1MB;G=1,段界限的单位是 4KB,段最大是 2 ^ 20 * 4KB,即4GB。
实际段界限 = (描述符中的段界限+1) * 粒度-1,假设段界限为0xfffff,G位为1时,实际段界限 = 0x100000 * 4KB -1 = 0xFFFFFFFF。如果偏移地址超过了段界限,CPU会抛出异常。
- 最后,第 24 ~ 31 位,段基址的第 24 ~ 31 位,补全段基址的最后8位。
GDT、LDT及选择子
GDT
一个段描述符只用来定义(描述)一个内存段。代码段要占用一个段描述符、数据段和栈段等,多个内存段也要各自占用一个段描述符,这些描述符放在哪里呢?答案是放在全局描述符表,GDT(Global Descriptor Table)。
全局描述符表 GDT 相当于是描述符的数组,数组中的每个元素都是 8 字节的描述符。可以用选择子(马上会讲到)中提供的下标在 GDT 中索引描述符。
全局描述符表位于内存中,需要用专门的寄存器 GDTR (GDT Register,专门存储GDT的内存地址和大小)指向它后,CPU才知道它在哪里。GDTR是个48位的寄存器,如下图:
GDTR 使用专门的赋值 指令lgdt 进行该寄存器的赋值,换句话说GDTR不能够使用 mov指令进行赋值。
如上图, 48 位内存数据划分为两部分,其中前 16 位是 GDT 以字节为单位的界限值,所以这 16 位相当于GDT 的字节大小减 1。 // 后 32 位是 GDT 的起始地址。由于 GDT 的大小是 16 位二进制,其表示的范围是 2 ^ 16 = 65536 字节。每个描述符大小是8字节,故,GDT中最多可容纳的描述符数量是 65536/8=8192个,即 GDT 中可容纳 8192 个段或门。
段描述符和内存段的关系如下图所示:
段描述符和段描述符表都有了,现在引入:段的选择子
段寄存器 CS、DS、ES、FS、GS、SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。
而在保护模式下时,由于段基址已经存入了段描述符中,所以段寄存器中再存放段基址是没有意义的,在段寄存器中存入的是一个叫作选择子的东西 — selector,选择子(索引值)。用选择子在段描述符表中索引相应的段描述符,得到内存段的 起始地址 和 段界限值 等相关信息。
保护模式下的段寄存器中已经是选择子,不再是直接的段基址。
选择子正式介绍
由于段寄存器是 16 位,所以选择子也是 16 位。
- 低 2 位即第 0~1 位,用来存储 RPL,即请求特权级。
- 在选择子的第 2 位是 TI 位,即 Table Indicator,用来指示选择子是在 GDT 中,还是 LDT 中索引描述符。
TI为 0 表示在 GDT 中索引描述符,TI 为 1 表示在 LDT 中索引描述符。 - 选择子的高 13 位,即第 3~15 位是描述符的索引值,即 GDT 中的下标,用此值在 GDT 中索引描述符 。
在保护模式下,段基址在段描述符中,用给出的选择子索引到描述符后,CPU 自动从段描述符中取出段基址,这样再加上段内偏移地址,便凑成了“段基址:段内偏移地址”的形式。
这里需要注意的是,在保护模式下,由于已经是 32 位地址线和 32 位寄存器啦,任意一寄存器都能够提供 32 位地址,故不需要再将 段基址乘以 16 后再与 段内偏移地址 相加啦,直接用选择子对应的“段描述符中的段基址”加上“段内偏移地址”就是要访问的内存地址。
举个例子:
选择子的值为0x8,将其加载到 ds寄存器 ,访问 ds:0x9 这样的内存,过程如下:(0x8 = 0000 0000 0000 1000 B)
- 低2位RPL位,值为00。
- TI位为0,表示在 GDT 中索引段描述符。
- 高13位 0x1 ,即 在 GDT 中索引第一个段描述符(GDT中第0个描述符不可用,但是LDT可用)。假设第1个段描述符的 3 个段基址部分,组合成的值为 0x1234。
- CPU将 0x1234 作为段基址,与段内偏移地址 0x9 相加,即 0x1234 + 0x9 = 0x123d,即 访存地址为0x123d。
LDT
LDT,Local Descriptor Table,它是 CPU 厂商为在硬件一级原生支持多任务而创造的表,按照 CPU 的设想,一个任务对应一个 LDT。其实在现代操作系统中很少有用 LDT 的,将要写的系统也未用到LDT。
CPU 厂商建议每个任务的私有内存段都应该放到自己的段描述符表中,该表就是 LDT,即每个任务都有自己的 LDT,随着任务切换,也要切换相应任务的 LDT。LDT 也位于内存中,其地址需要先被加载到某个寄存器后,CPU 才能使用 LDT,该寄存器是 LDTR,即 LDT Register。同样也有专门的指令用于加载 LDT,即 lldt。以后每切换任务时,都要用 lldt 指令重新加载任务的私有内存段
这里略略提下LDT而已,跳过…
打开A20地址线
在8086中,CPU只有20根地址线,A0 ~ A19,而在发展到80286时(80286是首款具有保护模式的CPU),其地址线扩展到了24根,寻址能力从 2 ^ 20 = 1MB 提升到了 2 ^ 24 = 16MB,Inteel为了向下兼容,在实模式下,80286CPU也仍和8086CPU一样使用 20 根地址线,换句话说就是,80286的第 21 根地址线 A20 是处于关闭状态的,当关闭了 A20 ,CPU访问 0x100000 ~ 0x10FFEF 之间的内存时,80286 将会像 8086/8088 那样回绕到 0,即相当于访问的 地址将会对 1M 求模,此为地址回绕(wrap-around),如下图:
那么,对于 80286 后续的 CPU,是如何对 A20 地址线进行控制的呢?通过 A20GATE 对 A20 地址线进行控制。
当 A20 地址线打开时,CPU 对 0x100000 ~ 0x10FFEF 的内存进行访问时,将不会再地址回绕,因为80286有 24 根地址线,系统将直接访问这块物理内存。
小结一下:
在保护模式下,我们需要突破第 20 条地址线(A20)去访问更大的内存空间。而这一切,只有关闭了地址回绕才能实现。而关闭地址回绕,就是上面所说的打开 A20Gate。
- 如果A20Gate 被打开,当访问到 0x100000~0x10FFEF 之间的地址时,CPU 将真正访问这块物理内存。
- 如果A20Gate 被禁止,当访问 0x100000~0x10FFEF之间的地址时,CPU 将采用8086/8088 的地址回绕。
打开A20Gate 的方式是极其简单的,将端口 0x92 的第 1 位置 1 就可以了,如下三个步骤:
in al,0x92
or al,00000010B
out 0x92,al
保护模式的开关,CR0寄存器的 PE位
CRx 是控制寄存器系列。
控制寄存器是CPU 的窗口,既可以用来展示CPU的内部状态,也可用于控制CPU 的运行机制。这次我们要用到的是CR0 寄存器。更准确地说,我们要用到CR0寄存器的第0 位,即PE位,Protection Enable,此位用于启用保护模式,是保护模式的开关。
当打开此位后,CPU 才真正进入保护模式,所以这是进入保护模式三步中的最后一步。
CR0控制寄存器如下图:
下图为CR0的全部字段:
PE 为 0 表示在实模式下运行,PE 为 1 表示在保护模式下运行。所以,我们的任务是将此位置 1。示例代码如下:
mov eax,cr0
or eax,0x00000001
mov cr0,eax
第 1 行代码是将 cr0 写入 eax。
第 2 行代码,通过或运算 or 指令将 eax 的第 0 位置 1。
第 3 行是将 eax 写回 cr0,这样 cr0 的 PE 位便为 1 了。
进入保护模式
保护模式是在 loader.bin 中进入的,需要更新 loader.S 和另外两个文件 —— mbr.S 和 boot.inc,代码可见此处
mbr.S
......
mov cx,4
call rd_disk_m_16
......
由于 loader.bin 超过了 512 字节,所以我们要把 mbr.S 中加载 loader.bin 的读入扇区数增大,目前它是 1 扇区,为了避免将来再次修改,直接改成读入 4 扇区。
boot.inc
;------------- loader和kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;-------------- gdt描述符属性 -------------
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暂置为0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b
DESC_P equ 1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_CODE equ 1_000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b
DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
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
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
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
;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
代码主要是新增段描述符的属性及选择子,都是以宏的方式实现的。
equ 是nasm 提供的伪指令,意为 equal,即等于。其指令格式是:符号名称 equ 表达式
描述符中的各个字段都是由 equ 来定义的,符号名一律采用 DESC_字段名_字段相关信息
的形式。
- 符号 DESC_G_4K,表示描述符的 G 位为 4K 粒度,其值等于(equ)1_00000000000000000000000b。1 正好处于第23位,即 G位,4K粒度。
1 右边的
字符_
没有特别的意义,人为加上去的,这样看起来显得比较清晰,nasm 编译器很人性化,为了人看得方便,它特意支持这种分隔符的写法,在编译阶段会忽略此分隔符。
- type字段,代码 22 行,
DESC_TYPE_CODE equ 1000_00000000b
;x=1,c=0,r=0,a=0,代码段是可执行的,非依从的,不可读的,已访问位a清0
Linux 等主流操作系统的内存段,用的是平坦模型,即整个内存都在一个段里,不用再像实模式那样用切换段基址的方式访问整个地址空间。在 32位保护模式中,寻址空间是 4G。所以,平坦模型在我们定义的描述符中,段基址为0,段界限*粒度 = 4G
。粒度为 4K,故段界限为 0xFFFFF。
- 代码第25行,
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
,定义了代码段的高 4 字节,后面的加法表达式是凑足段描述符这高 4 字节内容。
-
0x00 << 24
,表示 段基址 31 ~ 24 字段,由于平坦模式段基址为 0,故用的 0 偏移 24 位填充该字段。这只是 段描述符三处段基址中的一处,其他两处也是0。
DESC_D_32 equ 1_0000000000000000000000b
,表示的是描述符中的 D/B字段,对于代码段来说,就是D位,在此表示 32 位操作数。DESC_L equ 0_000000000000000000000b
表示描述符表中的 L 位,为0,表示为32位代码段- DESC_AVL 为 0,此位没实际意义,是留给操作系统用的。
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
,代码段 段界限的第2 部分(段界限的第1 部分在段描述符的低4 字节中),此处值为1111b,它与段界限的第1 部分将组成20 个二进制1,即总共的段界限将是0xFFFFF。DESC_P equ 1_000000000000000b
,表示段存在DESC_DPL_0 equ 00_0000000000000b
,DESC_DPL_0 表示该段描述符对应的内存段的特权级是0,即最高特权级DESC_S_CODE equ 1_000000000000b
,DESC_S_CODE 是代码段的S 位,此值为1,表示它是个普通的内存段,不是系统段。
29 ~ 35 行是定义 选择子属性 的。
接下来看一下 loader.S,如何进入保护模式的。
loader.S 代码
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start ; 此处的物理地址是:
;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl已改为0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的slot
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上
;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
loadermsg db '2 loader in real.'
loader_start:
;------------------------------------------------------------
;INT 0x10 功能号:0x13 功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址
;AL=显示输出方式
; 0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
; 1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
; 2——字符串中含显示字符和显示属性。显示后,光标位置不变
; 3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg ; ES:BP = 字符串地址
mov cx, 17 ; CX = 字符串长度
mov ax, 0x1301 ; AH = 13, AL = 01h
mov bx, 0x001f ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
mov dx, 0x1800 ;
int 0x10 ; 10h 号中断
;---------------------------------------- 准备进入保护模式 ------------------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al
;----------------- 加载GDT ----------------
lgdt [gdt_ptr]
;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
;jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:160], 'P'
jmp $
全局描述符表GDT只是一片内存区域,里面每隔8 字节便是一个表项,即段描述符。
我们这里分高四字节和低四字节分别定义段描述符。
dd 是伪指令,意为define double-word,即定义双字变量,一个字是2 字节,所以双字就是4 字节数据。
程序编译后的地址是从上到下越来越高的。也就是说,上面的dd 是定义的段描述符的低4 字节,下面的dd 是段描述符的高4 字节。例如,第 10~11 行的段描述符CODE_DESC,第 1 个“dd 0x0000FFFF”,这是段描述符的低 4 字节。
- 第 6~17 行是在构建全局描述符表,并直接在里面填充段描述符。GDT 的起始地址是标号 GDT_BASE所在的地址。
- 前面说过,GDT 中的第 0 个描述符不可用,所以第 7~8 行,直接将段描述符的低 4 字节和高 4 字节,分别用 dd 定义为 0。
- 代码段描述符 CODE_DESC :
- 段描述符的低四字节:
0x0000FFFF
,则FFFF
为 段描述符的低四字节中的低二字节,即表示段界限的第 0 ~ 15 位为FFFF
。 - 段描述符的高四字节:
DESC_CODE_HIGH4
,已经在boot.inc中定义好
- 数据段和栈段描述符 DATA_STACK_DESC :这里数据段和栈段共用一个段描述符,因为栈段也是数据段。但是,数据段是向上扩展的,而栈段是向下扩展的,一个段描述符只能定义一种扩展方向,栈段也可以使用向上扩展的数据段吗?是的,但是,这种情况下,栈段的段界限需要按照数据段的规则来检查了。
- 显存段描述符 VIDEO_DESC(第 16~17 行,
VIDEO_DESC: dd 0x80000007
) :如下给出的表所示,用于文本模式显示适配器的内存地址是 0xb8000~0xbffff,内存地址 0xc0000 显示适配器 BIOS 所在区域。
由于我们只支持文本模式的输出,所以为了方便显存操作,显存段不采用平坦模型。我们直接把段基址置为文本模式的起始地址 0xb8000(对应 0x80000007 高二字节),段大小为 0xbffff - 0xb8000 = 0x7fff,段粒度为 4k,因而 段描述符中的段界限 limit 等于 0x7fff / 4k = 7(对应 0x80000007 低二字节)。 - line 19 ~ 20,
GDT_SIZE equ $ - GDT_BASE
,通过地址差获得 GDT的大小。GDT_LIMIT equ GDT_SIZE - 1
,通过 GDT 的大小减一,获得段界限。为加载 GDT 做准备。 - line 21,为将来往 GDT 中添加其他描述符,预留 60 个段描述符空间
- line 22 ~ 24,构建代码段、数据段、显存段的选择子,按如下图所示进行构建
- line 28 ~ 29,定义全局描述符表 GDT 的指针。此指针是 lgdt 加载 GDT 到 gdtr寄存器 时用的。(lgdt指令格式:
ldgt 48位内存数据
)
这48位内存数据的前 16 位,是 GDT 以字节为单位的界限值,即 GDT 大小减去 1。后32位,是 GDT 的起始地址。 - line 30,定义一份字符串,用来显示将要进入保护模式了(此时还是在实模式下打印的,用的还是 BIOS 中断)。
- line 52,BIOS 调用中,利用 int 0x10打印字符串的功能,cx寄存器是字符串的长度,是 int 0x10 的参数,数一下刚好为17个字符。
- line 55,
mov dx,0x1800
,行数dh 为 0x18,列数dl 为 0x00,也是 int 0x10的参数。
文本模式下的行数是 25 行,即 0~24 行,所以 0x18 的十进制为24,即最后一行,所以,“2 loader inreal.”将出现在最后一行的行首。 - line 58 ~ 76,进入保护模式的三个步骤:
- 打开 A20 地址线
- 在 gdtr寄存器中加载 GDT 的地址和偏移量(界限值)
- 将 cr0寄存器 的 PE位 置1
line 70,gdt_ptr的前16位是GDT界限值,后32位是 GDT 的起始地址。
- line 83 ~ 89,用选择子初始化各段寄存器
mov byte [gs:160], 'P'
,往显存第80个字符的位置(第2行首字符的位置,1个字符占两个字节,第一行为 第0~79号的字符)写入字符P。该字符是在保护模式下打印的,而"2 loader in real",是在实模式下打印的,以示区分。
结果演示
make 生成 hd30M.img,mbr.bin,loader.bin三个文件,如下图:
接下来,启动 bochs模拟器,从 实模式 进入 保护模式 演示:
如上图,红框内均是需要输入的,由于上面的红框默认选择6,而我也是需要选择6,便回车键跳过了。bochs模拟器显示如下画面:
图中闪烁的字符是代码设定了的,为了让大家看到实际闪烁的效果,这里搞了gif动图。
实验成功!
这不是最开始的部分,但是由于前面的没有记录博客,就先这样吧,再补回。持续更新。
我正在 “做中学”,还有很多不足的地方。待学习深入,持续地对文章进行修改,该精简的进行精简,该细化的进行细化。
更新于 2020.2.19 待续 …