目录

文章目录

  • 目录
  • 概述
  • 初见保护模式
  • 代码
    • 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 0x00000000CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4VIDEO_DESC: dd 0x80000008
dd DESC_VIDEO_HIGH4 ; 此时 dpl 已改为 0GDT_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 + RPL0gdt_ptr: dw GDT_LIMIT
dd gdt_addrloader_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_ADDRLOADER_STACK_TOP equ LOADER_BASE_ADDRjmp loader_start                 ; 此处的物理地址是:;构建gdt及其内部的描述符GDT_BASE:   dd    0x00000000 dd    0x00000000CODE_DESC:  dd    0x0000FFFF dd    DESC_CODE_HIGH4DATA_STACK_DESC:  dd    0x0000FFFFdd    DESC_DATA_HIGH4VIDEO_DESC: dd    0x80000007        ;limit=(0xbffff-0xb8000)/4k=0x7dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0GDT_SIZE   equ   $ - GDT_BASEGDT_LIMIT   equ   GDT_SIZE -    1 times 60 dq 0                  ; 此处预留60个描述符的slotSELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0         ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0SELECTOR_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_BASEloadermsg 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_ADDRmov     bp, loadermsg           ; ES:BP = 字符串地址mov     cx, 17          ; CX = 字符串长度mov    ax, 0x1301      ; AH = 13,  AL = 01hmov   bx, 0x001f      ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)mov     dx, 0x1800      ;int    0x10                    ; 10h 号中断;----------------------------------------   准备进入保护模式   ------------------------------------------;1 打开A20;2 加载gdt;3 将cr0的pe位置1;-----------------  打开A20  ----------------in al,0x92or al,0000_0010Bout 0x92,al;-----------------  加载GDT  ----------------lgdt [gdt_ptr];-----------------  cr0第0位置1  ----------------mov eax, cr0or eax, 0x00000001mov 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_DATAmov ds, axmov es, axmov ss, axmov esp,LOADER_STACK_TOPmov ax, SELECTOR_VIDEOmov gs, axmov 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 待续 …

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

  1. 《操作系统真象还原》第二章

    <操作系统真象还原>第二章 编写MBR主引导记录 载入内存 过程: (1)程序被加载器(软件或硬件)加载到内存某个区域. (2)CPU的cs:ip寄存器被指向这个程序的起始地址. 从按下主 ...

  2. 《操作系统真象还原》第九章

    <操作系统真象还原>第九章 本篇对应书籍第九章的内容 本篇内容介绍了线程是什么,多线程的原理,多线程用到的核心数据结构,以及通过代码实现了内核多线程的调度 线程是什么? 执行流 过去,计算 ...

  3. 《操作系统真象还原》1-3章 学习记录

    文章目录 前言 一.开始实验前的一些基本问题解答? section的含义? vstart的含义? $ 和 $$区别? 实模式的特点? CPU如何和硬盘进行交互? CPU和IO设备交互方式? 程序载入内 ...

  4. 操作系统真象还原——第5章 从保护模式到内核

    目录 前言 5.1 获取物理内存容量 5.1.1 学习Linux获取内存的方法 5.1.2 实战内存容量检测 5.2 内存分页 为什么需要分页? 一级页表 二级页表 如何设计一个页表 分页机制的代码实 ...

  5. 《操作系统真象还原》第九章 ---- 终进入线程动斧开刀 豁然开朗拨云见日 还需解决同步机制才能长舒气

    文章目录 专栏博客链接 相关查阅博客链接 本书中错误勘误 进程 线程的自我小理解 线程 进程的状态 内核级线程 & 用户级线程 初步实现内核级线程 浪费两三个小时调试的辛酸史 编写thread ...

  6. 操作系统真象还原第2章:编写MBR主引导记录

    前言 这章的内容挺少的,也很简单,如果环境没配置错的话是没啥问题的.但是这章也很精彩,把引导的过程给说了出来,我也是看了几遍把这个过程给大致看懂了. 首先计算机一开机这个时cpu会自动把cs:ip指针 ...

  7. 操作系统真象还原第3章:完善MBR

    前言 这次我出现了一些BUG,导致我忙活了一阵子,还好的是解决了 老规矩还是把整个流程过一遍,当MBR忙活完后,就需要把自己手中的棒交给loader了,MBR的作用就是从硬盘中的内核加载器移动到内存中 ...

  8. 《操作系统真象还原》第二章 ---- 编写MBR主引导记录 初尝编写的快乐 雏形已显!

    文章目录 专栏博客链接 前引 相关术语 理清操作系统启动程序运行流程(部分) 编写MBR引导内容 编译并检验mbr.bin Linux dd 磁盘操作指令与参数 模拟操作试一试 结束语 专栏博客链接 ...

  9. 操作系统真象还原第5章:保护模式进阶,向内核进阶

    前言 由于涉及到马上要搞实习的事情,搞得我十分的浮躁,自己也是频繁失眠,想来还是自己太过懒了,没控制住自己,自己也在这一个多月没搞好,尤其是本来想花几天时间来写一个高性能服务器,也把游双大佬的linu ...

最新文章

  1. labview生成HTML报表,LabVIEW201
  2. vue-cli3 中跨域解决方案
  3. Django框架(18.Django中的自定义错误500页面和不存在404页面)
  4. 网络故障排除连载之七:设备兼容性故障排除
  5. 基金一般拿多长时间合适?
  6. 命中书中题有奖反馈活动_三级网络技术
  7. 精简的Linux启动过程
  8. git中使用emacs和vimdiff/Ediff工具
  9. gsoap使用心得![转]
  10. java对日期设置时间和对日期加减周
  11. mysql 字符串拼接 null_mysql字符串拼接并设置null值的实例方法
  12. 用dcloud平台的H5+实现消息推送APP端通知栏接收的问题
  13. win7用友u8安装教程_用友u8怎么安装?分享一组用友u8安装教程(图文)
  14. 如何取消PPT的密码保护?
  15. 洛谷 P1144 最短路计数 dijkstra
  16. Java实现将中文转成拼音和ASCII码
  17. 项目经验之:针对昨日FDO连接SDF文件报错,国外GIS论坛给出了一个思路.帮助我过关.
  18. echarts:silent:true去掉markline label的tooltip
  19. qt show widget_Qt Widget不显示
  20. 普林斯顿大学计算机排名,2020年普林斯顿大学排名TFE Times美国最佳计算机科学硕士专业排名第8...

热门文章

  1. python哲学内容 多行胜于单行_模板链(名词解释)
  2. 【§极品网游之我叫mt online电脑版免费中文版§】
  3. 邯郸翱翔学校春季班火热开启
  4. 关于磁盘随机读写与顺序读写
  5. 攻破MD5 SHA1 强碰撞性
  6. 一周讲座活动后的领悟
  7. js中的 == ===
  8. 常见ADSL猫的默认管理员用户名及其密码
  9. 2008年的各个节日
  10. Android studio 配置NDK ---实验步骤跟进记录(初定完结)