《操作系统真象还原》第4章 保护模式入门 ing... 持续更新

目录

概述

实模式是 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)

  1. 低2位RPL位,值为00。
  2. TI位为0,表示在 GDT 中索引段描述符。
  3. 高13位 0x1 ,即 在 GDT 中索引第一个段描述符(GDT中第0个描述符不可用,但是LDT可用)。假设第1个段描述符的 3 个段基址部分,组合成的值为 0x1234。
  4. 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 待续 …

发布了23 篇原创文章 · 获赞 13 · 访问量 3001

猜你喜欢

转载自blog.csdn.net/qq_29856169/article/details/104326931