代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
1.全局描述符表 GDT
一个段描述符只用来定义(描述)一个内存段。代码段要占用一个段描述符、数据段和栈段等,多个内存段也要各自占用一个段描述符。
这些段描述符放在全局描述符表GDT (Global Descriptor Table)。 全局描述符表GDT相当于是描述符的数组,数组中的每个元素都是8字节的描述符。 可以用选择子中提供的下标在GDT中索引描述符。
全局体就是公用的。 全局描述符表位于内存中,需要用专门的寄存器指向它后,CPU才知道它在哪里。 这个专门的寄存器便是GDTR,即GDT Register,,专门用来存储GDT的内存地址及大小。
GDTR是个48位的寄存器。如图:

对此寄存器的访问,不能够用mov这样的指令为 gdtr 初始化,而使用lgdt指令。
虽然我们是为了进入保护模式才讲述的lgdt,因此看上去此指令是在实模式下执行的,但实际上,此指令在保护模式下也能够执行。言外之意便是进入保护模式需要有 GDT,但进入保护模式后,还可以再重新换个GDT加载。
在保护模式下重新换个GDT的原因是实模式下只能访问低端1MB空间,所以GDT只能位于1MB之内。根据操作系统的实际情况,有可能需要把GDT放在其他的内存位置,所以在进入保护模式后,访问的内存空间突破了1MB,可以将GDT放在合适的位置后再重新加载进来。
lgdt的指令格式是: Igdt48 位内存数据。
这48位内存数据划分为两部分:
- 其中前16位是GDT以字节为单位的界限值,所以这16位相当于GDT 的字节大小减 1。
- 后 32 位是 GDT 的起始地址。
由于 GDT 的大小是 16位二进制,其表示的范围是 2的16次方等于65536字节。每个描述符大小是8字节,故GDT中最多可容纳的描述符数量是65536/8=8192个,即 GDT 中可容纳 8192个段或门。
保护模式中的段描述符虽然看上去又怪异又复杂,但它本质上只是一段内存区域的“身份证”而已,尽管它不像实模式那样直接,即段的大小统一都是64KB,段基址代表内存的起始,偏移地址代表段内偏移量……但它代表的同样也是一段内存。
段描述符与内存段的关系如图

2.选择子
段寄存器 CS、DS、ES、FS、GS、SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。 而在保护模式下时,由于段基址已经存入了段描述符中,所以段寄存器中再存放段基址是没有意义的,在段寄存器中存入的是一个叫作选择子的东西——selector。
选择子“基本上”是个索引值,用此索引值在段描述符表中索引相应的段描述符,这样便在段描述符中得到了内存段的起始地址和段界限值等相关信息。
由于段寄存器是16位,所以选择子也是16位。
在其低2位即第0~1位,用来存储RPL,即请求特权级,可以表示0、1、2、3四种特权级。也就是请求者的当前特权级。
在选择子的第2位是TI位,即Table Indicator,用来指示选择子是在GDT中,还是LDT中索引描述符。TI为0表示在GDT中索引描述符,TI为1表示在LDT中索引描述符。
选择子的高13位,即第3~15位是描述符的索引值,用此值在GDT中索引描述符。
前面说过GDT相当于一个描述符数组,所以此选择子中的索引值就是 GDT 中的下标。
选择子结构如图

由于选择子的索引值部分是13位,即2的13次方是8192,故最多可以索引8192个段,这和GDT中最多定义8192个描述符是吻合的。
选择子的作用主要是确定段描述符,确定描述符的目的,一是为了特权级、界限等安全考虑,最主要的还是要确定段的基地址。 虽然到了保护模式,但IA32架构始终脱离不了内存分段,即访问内存必须要用“段基址:段内偏移地址”的形式。 保护模式下的段寄存器中已经是选择子,不再是直接的段基址。 段基址在段描述符中,用给出的选择子索引到描述符后,CPU自动从段描述符中取出段基址,这样再加上段内偏移地址,便凑成了“段基址:段内偏移地址”的形式。段内偏移通常由程序员在使用内存访问指令(如MOV、ADD等)时指定或计算得出。
例如选择子是0x8,将其加载到ds寄存器后,访问ds: 0x9这样的内存,其过程是: 0x8的低2位是RPL,其值为 00。 第2 是 TI,其值0,表示是在GDT中索引段描述符。 用0x8的高13位0x1在GDT中索引,也就是 GDT 中的第 1 个段描述符(GDT 中第 0 个段描述符不可用)。 假设第 1 个段描述符中的 3个段基址部分,其值为0x1234,CPU将0x1234作为段基址,与段内偏移地址0x9相加,0x1234+0x9=0x123d用所得的和0x123d作为访存地址。
GDT中的第0个段描述符是不可用的,原因是定义在GDT中的段描述符是要用选择子来访问的,如果使用的选择子忘记初始化,选择子的值便会是0,这便会访问到第0个段描述符。 所以 GDT中的第0个段描述符不可用。 也就是说,若选择到了GDT中的第0个描述符,处理器将发出异常。
3.局部描述符表 LDT
CPU厂商建议每个任务的私有内存段都应该放到自己的段描述符表中,该表就是LDT,即每个任务都有自己的LDT,随着任务切换,也要切换相应任务的LDT。
LDT也位于内存中,其地址需要先被加载到某个寄存器后, CPU才能使用LDT,该寄存器是 LDTR,即LDT Register。
同样也有专门的指令用于加载LDT,即lldt。 以后每切换任务时,都要用lldt指令重新加载任务的私有内存段。
段描述符中的type字段,其中LDT为系统段,换句话说,LDT虽然是个表,但其也是一片内存区域,所以也需要用个描述符在GDT中先注册。
段描述符是需要用选择子去访问的。
故 lldt的指令格式为: lldt 16位寄存器/16位内存
与GDT不同的是LDT中的第0个段描述符是可用的,因为提交的选择子中的TI位,TI位用于指定是GDT,还是LDT, TI为1则表示在LDT中索引段描述符,即T1为1必然是经过显式初始化的结果,完全排除了忘记初始化的可能。
4.A20地址线
实模式下寄存器都是16位的,如果段基址和段内偏移地址都为16位的最大值,即0xFFFF: 0xFFFF,最大地址是0xFFFFO+0xFFFF,即0x1OFFEF。
由于实模式下的地址线是20位,最大寻址空间是1MB,即0x00000~OxFFFFF。 超出1MB内存的部分在逻辑上也是正常的,但物理内存中却没有与之对应的部分。 为了让“段基址:段内偏移地址”策略继续可用,CPU采取的做法是将超过1MB的部分自动回绕到0地址,继续从0地址开始映射。
相当于把地址对 1MB 求模。 超过 1MB 多余出来的内存被称为高端内存区 HMA。
对于只有 20 位地址线的 CPU,不需要任何额外操作便能自动实现地址回绕。
地址(Address)线从0开始编号,在8086/8088中,只有20位地址线,即A0~A19。20位地址线表示的内存是2的20次方,最大是1MB,即0x0~0xFFFFF。内存若超过1MB,是需要第21条地址线支持的。所以说,若地址进位到1MB以上,如0x100000,由于没有第21位地址线,相当于丢掉了进位1,变成了 0x00000。这一“缺陷”甚至成了当时很多程序员利用的技巧。
地址回绕如图所示:

发展到了80286后,虽然地址总线从原来的 20位发展到了24位,从而能够访问的内存范围可达到2的24次方,等于16MB。
应该与8086/8088完全一样,即仍然只使用20条地址线。但80286有24条地址线,即A20~A23,也就是说A20地址线是开启的。如果访问0x100000~0x10FFEF之间的内存,系统将直接访问这块物理内存,并不会像8086/8088那样回绕到0,为了解决此问题,IBM 在键盘控制器上的一些输出线来控制第 21 根地址线(A20)的有效性,故被称为A20Gate。
如果 A20Gate 被打开,当访问到 0x100000~0x10FFEF之间的地址时,CPU 将真正访问这块物理内存。如果 A20Gate 被禁止,当访问 0x100000~0x1OFFEF 之间的地址时,CPU 将采用 8086/8088 的地址回绕。上面描述了地址回绕的原理,但地址回绕是为了兼容8086/8088的实模式。如今我们是在保护模式下,我们需要突破第20条地址线(A20)去访问更大的内存空间。
而这一切,只有关闭了地址回绕才能实现。而关闭地址回绕,就是上面所说的打开A20Gate.
打开A20Gate的方式是极其简单的,将端口0x92的第1位置1就可以了
in al,0x92 or al,0000_0010B out 0x92,al
5.CRO 寄存器的PE位
控制寄存器是CPU的窗口,既可以用来展示CPU的内部状态,也可用于控制CPU的运行机制。
这次我们要用到的是CR0寄存器。我们要用到CR0寄存器的第0位,即PE位,Protection Enable,此位用于启用保护模式,是保护模式的开关。
CR0全貌


PE(Protection Enable)位:
- 当 PE 位为 1 时,表示处理器工作在保护模式下,启用内存分段、特权级别等保护机制。
- 当 PE 位为 0 时,表示处理器工作在实模式下,不启用保护模式。
MP(Monitor Coprocessor)位:该位用于控制协处理器(如 FPU)监视功能的启用和禁用。
EM(Emulation)位:该位用于控制协处理器的工作模式,当 EM 位为 1 时表示使用仿真模式,否则表示使用硬件协处理器。
TS(Task Switched)位:该位指示处理器是否经历了任务切换,用于支持协处理器的任务切换。
ET(Extension Type)位:该位用于支持处理器的一些扩展特性,如支持 80387 数学协处理器。
NE(Numeric Error)位:该位用于启用数学协处理器的数字错误处理功能。
WP(Write Protect)位:当 WP 位为 1 时,表示只读页面不能被写入;当 WP 位为 0 时,只读页面可以被写入。
AM(Alignment Mask)位:该位用于控制内存对齐检查的开启和关闭。
NW(Not Write-through)位:该位用于控制缓存写透传(write-through)策略的禁用。
CD(Cache Disable)位:当 CD 位为 1 时,表示禁用处理器缓存;当 CD 位为 0 时,允许使用缓存。
PG(Paging)位:当 PG 位为 1 时,表示启用分页机制;当 PG 位为 0 时,禁用分页机制。
PE为0表示在实模式下运行,PE为1表示在保护模式下运行。
所以,我们的任务是将此位置1。
示例代码如下:
mov eax, cr0 or eax, 0x00000001 mov cr0, eax
第1行代码是将cr0写入eax。
第2行代码通过或运算or指令将eax的第0位置1。
第3行是将eax写回cro,这样cro的PE位便为1了。
6.代码编写
下面我们要更新loader.asm,大小会超过512字节,所以需要我们要在mbr读取loader时直接读取4个扇区

boot.inc文件
更新公用配置boot.inc
LOADER_BASE_ADDR equ 0x900 LOADER_START_SECTOR equ 0x2 ;-------------- gdt描述符属性 ------------- DESC_G_4K equ 1_00000000000000000000000b ; 段界限单位:4KB DESC_D_32 equ 1_0000000000000000000000b ; 有效地址和操作数是 32 位 DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。 DESC_AVL equ 0_00000000000000000000b ; 保留位 DESC_LIMIT_CODE2 equ 1111_0000000000000000b ; 代码段段界限的高位 DESC_LIMIT_DATA2 equ 1111_0000000000000000b ; 数据段段界限的高位 DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b ; 视频段段界限的高位 DESC_P equ 1_000000000000000b ; 段是否存在标志位 DESC_DPL_0 equ 00_0000000000000b ; 0特权级内存 DESC_DPL_1 equ 01_0000000000000b ; 1特权级内存 DESC_DPL_2 equ 10_0000000000000b ; 2特权级内存 DESC_DPL_3 equ 11_0000000000000b ; 3特权级内存 DESC_S_CODE equ 1_000000000000b ; 代码段的段描述为数据段 DESC_S_DATA equ 1_000000000000b ; 数据段的段描述为数据段 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
在保护模式中,我们使用平坦模型。 平坦模型就是整个内存都在一个段里,不用再像实模式那样用切换段基址的方式访问整个地址空间。 在32位保护模式中,寻址空间是4G,所以,平坦模型在我们定义的描述符中,段基址是0,段界限*粒度等于4G。 粒度我们选的是4k,故段界限是OxFFFFF。
equ是nasm提供的伪指令,意为equal,即等于,用于给表达式起个意义更明确的符号名,
其指令格式是:符号名称 equ 表达式
上面代码会出现1_000000000000b这种格式,字符“_”没有特别的意义,nasm编译器支持这种分隔符的写法,在编译阶段会忽略此分隔符。只是为了更清晰明了。
比如:
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_CODE_HIGH4,就是定义了代码段的高4字节。equ后面那一串加法表达式,就是在凑足段描述符这高4字节内容。其中(0x00<<24)表示”段基址24-31″字段,该字段位于段描述符高4字节中的第24~31位,由于平垣模式段基址是0,所以咱们用0偏移24位填充该字段。当然这只是一部分段基址,段基址在8字节的段描述符中存在3处,它们在每处都会是0,
继续看,DESC_G_4K表示4k的粒度。
DESC_D_32表示描述符中的D/B字段,对代码段来说是D位,在此表示32位操作数,
DESC_L表示段描述符中的L位,为0,表示为32位代码段。
DESC_AVL为0,DESC_LIMIT_CODE2是代码段的段界限的第2部分(段界限的第1部分在段描述符的低4字节中),此处值为1111b,它与段界限的第1部分将组成20个二进制1,即总共的段界限将是OxFFFFF,
DESC_P表示段存在。DESC_DPL_0表示该段描述符对应的内存段的特权级是0,即最高特权级。当CPU在该段上运行时,将有至高无上的特权。
DESC_S_CODE是代码段的S位,此值为1,表示它是个普通的内存段,不是系统段。
DESC_TYPE_CODE意义为x=1, c=0, r=0,a=0 ,即代码段是可执行的,非一致性,不可读,已访问位a清0, 0x00是段基址的第16~23位,位于段描述符高4字节的起始8位,如前所述,由于是平坦模型,所以段基址的任意部分都是0。
loader.asm文件
更新loader.asm文件
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR ; 程序开始的地址
jmp loader_start
LOADER_STACK_TOP equ LOADER_BASE_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 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 ; 第一个选择子
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:
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg
mov cx, 17
mov ax, 0x1301
mov bx, 0x001f
mov dx, 0x1800
int 0x10
; 进入保护模式三步骤
; 1.打开A20地址线
open_A20:
in al,0x92
or al,0000_0010B
out 0x92,al
; 2.加载gdt描述符
load_gdt:
lgdt [gdt_ptr]
; 3.修改cr0标志寄存器的PE位
change_cr0_PE:
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
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,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:160], 'P'
jmp $上面的代码详细的解释一下:
代码解析一
这个LOADER_STACK_TOP是用于loader在保护模式下的栈,它等于LOADER_BASE_ADDR,其实这是loader在实模式下时的栈指针地址,只不过进入保护模式后,得为保护模式下的esp初始化,所以用了相同的内存地址作为栈顶。LOADER_BASE_ADDR的值是0x900,这是loader被加载到内存中的位置,在此地址之下便是栈。
LOADER_STACK_TOP equ LOADER_BASE_ADDR ; 栈顶地址
代码解析二
全局描述符表GDT只是一片内存区域,里面每隔8字节便是一个表项,即段描述符。我们这里定义段描述符的方式很简单直接,是将描述符拆成高4字节和低4字节两部分,分别定义。不过这不是定义段描述符的固定方式,可以使用其他方式。
这里用的是dd指令来定义它们的,dd是伪指令,意为define double-word,即定义双字变量,一个字是2字节,所以双字就是4字节数据。程序编译后的地址是从上到下越来越高的,所以,下面用dd定义的数据地址要高于上面的dd所定义的数据地址,也就是说,上面的dd是定义的段描述符的低4字节,下面的dd是段描述符的高4字节。
下面的代码就是在构建全局描述符表,并直接在里面填充段描述符。GDT的起始地址是标号GDT_BASE所在的地址。
这里我们事先定义了3个有用的段描述符,不过第0个段描述符没用。
从第1个到第3个,分别是代码段描述符CODE_DESC、数据段和栈段描述符DATA_STACK_DESC,显存段描述符VIDEO_DESC,它们三个之间都是间隔8字节大小,也就是每个段描述符的大小。
GDT中的第0个描述符不可用,所以直接将段描述符的高4字节和低4字节,分别用dd定义为0。
段描述符的低4字节还是比较容易定义的,其中的低2字节是段界限的0~15位,高2字节是段基址的0~15位。
比如段描述符CODE_DESC举例,第1个”dd 0x0000FFFF”,这是段描述符的低4字节。
其中低2字节的FFFF是段界限的第0~15位,高2字节的0000是段基址的第0~15位。
段描述符的高4字节相对麻烦一些,所以在boot.inc中直接将段描述符的高4位提前定义好,到loader.S中直接用就行啦。

;构建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为0DATA_STACK_DESC 是数据段和栈段的段描述符,我们这里数据段和栈段共同使用一个段描述符,这当然是可以的,因为栈段也是数据段。 其定义的原理和CODE_DESC一样。
按理说,栈应该是向下扩展的,数据段是向上扩展的,一个段描述符只能定义一种扩展方向, type字段中的e要么是0(向上扩展),要么是1(向下扩展)。栈也能用向上扩展的数据段吗?当然可以,只不过在这种情况下,栈段的段界限按照数据段的规则来检查了。
段描述符中的各字段只是用来供CPU检查的,CPU不知道此段是用来干什么的,只有用此段的人才知道。 栈段向下扩展,是指栈指针esp指向的地址逐渐减小,不过那是push指令的作用,和段描述符的扩展方向无关,此扩展方向是用来配合段界限的, CPU在检查段内偏移地址的合法性时,就需要结合扩展方向和段界限来判断。 而且,用向上扩展的数据段做栈段,比用向下扩展的段更容易。 我们直接用普通的数据段做栈段,所以type中的e为0。
下面说说显存段描述符VIDEO_DESC。 用于文本模式显示适配器的内存地址是0xb8000~0xbffff,内存地址0xc0000显示适配器BIOS所在区域。 由于我们只支持文本模式的输出,所以为了方便显存操作,显存段不采用平坦模型。 我们直接把段基址置为文本模式的起始地址0xb8000,段大小为0xbffff-0xb8000=0x7f,段粒度为4k,因而段界限limit等于0x7fff/4k=7。
代码解析三
这里先是通过地址差来获得 GDT 的大小,进而用 GDT 大小减 1 得到了段界限,这是为加载 GDT 做准备。
这行代码使用了汇编的特殊符号 $,它表示当前位置的地址。GDT_BASE 是定义全局描述符表的起始地址,因此 $ – GDT_BASE 计算了从 GDT_BASE 到当前位置的字节数,即 GDT 的大小。
times纯粹是为了将来往GDT中添加其他描述符,提前保留空间而已。 以后我们还要往GDT中继续塞中断描述符表IDT和任务状态段TSS描述符。 dq用来定义了8字节数据,即define quad-word,定义4字,即8字节。 所以用times 60 dq 0提前预留60个描述符空位。 其实也不用保留那么多,就当是为了方便扩展吧,万一哪天用到了就省事了。 times是nasm提供的伪指令,用来重复执行times后面的表达式,相当于是个循环,只不过“直接”执行此循环的不是CPU,而是编译器nasm。
指令格式是:times 循环次数表达式
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 ; 第一个选择子 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 第二个选择子 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 第三个选择子
代码解析五
这里定义全局描述符表GDT的指针,此指针是 lgdt 加载GDT 到gdtr 寄存器时用的,这48 位内存数据的前 16 位是 GDT 以字节为单位的界限值,也就是 GDT 大小减 1。 后 32 位是 GDT的起始地址。
dw 指令用于定义一个16位的数据。
dd 指令用于定义一个32位的数据。
; 以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
代码解析六
这里只是定义个字符串,用来显示一下进入保护模式了。 其实它还是在实模式下打印的,用的还是BIOS中断。
用int 0x10打印字符串的功能, cx寄存器是字符串的长度,这是int 0x10 的参数。 “2 loader in real.”的长度是17。“mov dx, 0x1800”,其中行数 dh 为 0x18,列数 dl 为 0x00。 这也是 int 0x10 的参数。 由于在文本模式下的行数是 25行,即0~24行,所以0x18的十进制为24,即最后一行,所以,“2 loader in real.”将出现在最后一行的行首。
loadermsg db '2 loader in real'
; 打印字符串
loader_start:
mov sp LOADER_BASE_ADDR
mov bp, loadermsg
mov cx, 17
mov ax, 0x1301
mov bx, 0x001f
mov dx, 0x1800
int 0x10
代码解析七
这里是进入保护模式的三个步骤。分别如下。
- 打开 A20 地址线。
- 在gdtr寄存器中加载GDT的地址及偏移量(界限值)。
- 将cr0寄存器的pe位置1。
; 进入保护模式三步骤
; 1.打开A20地址线
open_A20:
in al,0x92
or al,0000_0010B
out 0x92,al
; 2.加载gdt描述符
load_gdt:
lgdt [gdt_ptr]
; 3.修改cr0标志寄存器的PE位
change_cr0_PE:
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
代码解析八
jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响
; 远跳将导致之前做的预测失效,从而起到了刷新的作用。回去看loader. S 中开始的“jmp loader_start”,其机器码是 E91702,共3字节大小。 其中的E9是操作码, 1702是操作数,由于是小端字节序,所以其十六进制是0x217,这是16位相对近转移。
此指令直接跳过GDT定义相关部分,直接到第32行。 第32行的标号loader_start在文件内的地址是”jmp loader_start”的3字节机器码+4个段描述符大小+预留的 60个描述符大小+gdt_ptr的6字节+loadermsg 的17个字节=3+32+480+6+17=538=0x21a。 再加上loader 被加载到的地址 0x900,在内存中的实际地址为 0x900+0x21a=0xbla。
如果不包括第4行的jmp loader_start,那么loader_start的地址将是0xbla-3=0xb17,也就是说,如果把mbr中跳入loader的语句jmp LOADER_BASE_ADDR,改成jmp LOADER_BASE_ADDR+ 0xb17,其结果也是一样的,直接跳转到loader.S中的loader_start。 但是不能怎么做。
CPU是按照程序中指令顺序来填充流水线的,也就是说按照程序计数器PC(x86中是cs: ip)中的值来装载流水线的,当前指令和下一条指令在空间上是挨着的。 如果当前执行的指令是jmp,下一条指令已经被送上流水线译码了,第三条指令已经被送上流水线取指啦。 仔细想想看,其实这个流水线没用了,因为CPU早已经跳到别处去执行了,第二、三条指令用不上了,所以CPU在遇到无条件转移指令jmp时,会清空流水线。
段描述符缓冲寄存器在CPU的实模式和保护模式中都同时使用,在不重新引用一个段时,段描述符缓冲寄存器中的内容是不会更新的,无论是在实模式,还是保护模式下, CPU都以段描述符缓冲寄存器中的内容为主。 实模式进入保护模式时,由于段描述符缓冲寄存器中的内容仅仅是实模式下的20位的段基址,很多属性位都是错误的值,这对保护模式来说必然会造成错误,所以需要马上更新段描述符缓冲寄存器,也就是要想办法往相应段寄存器中加载选择子。
流水线的工作是这样的:在mov cr0, eax执行的同时,jmp SELECTOR_CODE:p_mode_start和之后的部分指令已经被送上流水线了,但是,段描述符缓冲寄存器在实模式下时已经在使用了,其低20位是段基址,但其他位默认为0,也就是描述符中的D位为0,这表示当前的操作数大小是16位。流水线上的指令全是按照16位操作数来译码的,mov ax, SELECTOR_DATA开始的指令明明是32位指令,16位和32位的指令都有各自不同的意义,所以就出错了,所以,如果将jmp SELECTOR_CODE:p_mode_start跳转指令去掉,程序将mov ax, SELECTOR_DATA开始出错,原因就是这里是32位指令格式,而CPU是将其按照16位指令格式来译码的,译码之后在其执行时,必然是错误的。
执行结果
编译mbr和Loader
nasm -I common/ -o bin/mbr.bin asm/mbr.asm nasm -I common/ -o bin/loader.bin asm/loader.asm
复制mbr二进制程序到硬盘:
sudo dd if=bin/mbr.bin of=/bochs/bin/dreams.img bs=512 count=1 conv=notrunc sudo dd if=bin/loader.bin of=/bochs/bin/dreams.img bs=512 count=2 seek=2 conv=notrunc
执行
sudo /bochs/bin/bochs -f /bochs/bin/bochsrc.disk

7.参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


