Featured image of post 《操作系统真象还原》第四章——保护模式入门

《操作系统真象还原》第四章——保护模式入门

本文详细介绍了在计算机系统中如何使用全局描述符表(GDT)和局部描述符表(LDT)来管理代码段、数据段、显存以及设置特权级别。内容涵盖了GDT的结构、描述符的属性设置、控制寄存器CR0的使用以及进入保护模式的过程

本章节所有代码托管在miniOS_32

章节任务介绍

上一节,我们完善了mbr主引导程序的功能——加载loader程序到指定位置运行

但是上一节我们只是为了测试我们的mbr程序是否能够成功加载loader,因此loader程序仅仅只是在屏幕上打印出我们想要的字符串

在本节,我们的主要任务就是完善loader程序的功能——开启保护模式,让后续的内核工作在保护模式下

  • 如何开启保护模式?

    1. 打开A20
    2. 加载GDT
      • 在实模式下构造需要的段描述符(代码段、数据段、栈段、显存段)
      • 将四种段描述符装填进GDT,从而构造GDT
      • 构造四种内存段的段选择子
      • 构造GDTR(存储GDT基址的寄存器),从而让操作系统运行时将GDT加载进内存
    3. 置cr0寄存器的PE位为1
  • 进入保护模式之前,先在实模式下测试一段在屏幕上显示信息的代码,即loader_start,这段代码使用实模式的逻辑编写,因此,如果运行成功,则说明在进入保护模式之前我们的代码是在实模式下运行的

  • 保护模式测试

    • 在进入保护模式之后写一段屏幕显示信息,如果显示成功说明成功进入了保护模式

前置知识

保护模式概述

一般来说,操作系统负责整个计算机软、硬件的管理,它做任何事情都是可以的。但是,用户程序却应当有所限制,只允许它访问属于自己的数据,即使是转移,也只允许在自己的各个代码段之间进行。

问题在于,在实模式下,用户程序对内存的访问非常自由,没有任何限制,随随便便就可以修改任何一个内存单元

比如以下代码片段,这个程序首先将段地址设置到0xb800,传统上,这是文本模式下的显存。所以,它通过指令向显存写入一个字符H。然后,它又将段地址切换到0x8000,向这个段内偏移地址为6的地方写入一字节0xc7。紧接着,又将段地址切换到0,向段内偏移地址为0x30的地方写入一字节0。

image-20241211162202303

事实上我们知道,段地址为0的这1KB内存是中断向量表,它这样做实际上是破坏了中断向量表的内容,但是它这样做是不受限制的,没有人可以阻止。最后,它又向端口0x60发送一字节的数据,用来控制设备。

通过这一段程序可以看出,在实模式下,程序是可以“为所欲为”的。它想访问内存的哪一部分,都可以很轻松地通过设置段地址和偏移地址来办到,即使某个内存位置不属于当前程序,它照样可以切换到那里,并随意修改其中的内容。

在多用户、多任务时代,内存中会有多个用户(应用)程序在同时运行。为了使它们彼此隔离,防止因某个程序的编写错误或者崩溃而影响到操作系统和其他用户程序,使用保护模式是非常有必要的。

全局描述符表

我们知道,为了让程序在内存中能自由浮动而又不影响它的正常执行,处理器将内存划分成逻辑上的段,并在指令中使用段内偏移。

与实模式相同,在保护模式下,对内存的访问仍然使用段地址和偏移地址但是,在每个段能够访问之前,必须先进行登记

为此,我们有以下概念

  • 和一个段有关的信息需要8字节来描述,称为段描述符(Segment Descriptor),每个段都需要一个描述符
  • 为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起集中存放的,这就构成了一个描述符表
  • 最主要的描述符表是全局描述符表(Global Descriptor Table, GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保护模式前,必须要定义全局描述符表。
  • 为了跟踪全局描述符表,处理器内部有一个48位的寄存器,称为全局描述符表寄存器(GDTR)。
  • GDTR分为两部分,分别是32位的线性地址和16位的边界。32位的处理器具有32根地址线,可以访问的地址范围是0x00000000到0xFFFFFFFF,共2^32字节的内存,即4GB内存。
    • GDTR的32位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一
    • 因为GDT的界限是16位的,所以,该表最大是216字节,也就是65536字节(64KB)。又因为一个描述符占8字节,故最多可以定义8192个描述符

如下是全局描述表寄存器(GDTR)的格式

全局描述符表寄存器GDTR

理论上,全局描述符表可以位于内存中的任何地方。但是,

  • 由于在进入保护模式之后,处理器要立即按新的内存访问模式工作,所以,必须在进入保护模式之前定义GDT
  • 此外,由于在实模式下只能访问1MB的内存,故GDT通常都定义在1MB以下的内存范围中。当然,允许在进入保护模式之后换个位置重新定义GDT。

以下是GDT和GDTR的关系图

GDT和GDTR的关系

总结来说,

  • GDT相当于一个数组,数组中的元素是各个段描述符
  • GDTR这个寄存器中存放着GDT的首地址
  • GDTR的访问方式:lgdt 48位内存数据

段描述符

段描述符是一个专门用来描述一个内存段(比如代码段、数据段、栈段、显存段等等)属性的结构,如前所述,该结构的大小占据8个字节

以下是段描述符与内存段之间的关系

image-20241211164112816

以下是段描述符的格式,每个描述符在GDT中占8字节,也就是2个双字,或者说是64位。图中,下面是低32位(低双字),上面是高32位(高双字)。

image-20241211164930203

从上图可以看出

  • 描述符中指定了32位的段起始地址(段基址),以及20位的段边界。
  • 在实模式下,段地址并非真实的物理地址,在计算物理地址时,还要左移4位(乘以16)。
  • 而在32位保护模式下,段地址是32位的线性地址,如果未开启分页功能,该线性地址就是物理地址。

此外,我们给出段描述符中各个属性位的详细解释

  • 段界限:表示一个段的最大大小
    • 此段界限是一个单位量,其单位要么是字节,要么是4KB,具体可由G位进行指定
    • 最终段界限=此段界限值*单位
    • 实际段界限计算公式:(此段界限值+1)*(G位)-1
  • G:粒度位,用于解释段界限的单位
    • 为0:表示段界限的单位表示为字节
    • 为1:表示段界限的单位表示为4KB
  • S描述符类型
    • 为0,表示系统段
    • 为1,表示非系统段(代码段或者数据段)
  • type:用于指示描述符的子类型 image-20241211170233339
  • DPL:Descriptor Privilege Level,即描述符的特权级,用于指定要访问该段所必须具有的最低特权级。如果这里的数值是2,那么,只有特权级别为0、1和2的程序才能访问该段,而特权级为3的程序访问该段时,处理器会予以阻止。
    • 特权级共有0、1、2、3,用于解释段界限的含义,其中0是最高特权级别,3是最低特权级别
    • 特权级是一个数字,可以赋给一个程序,用来决定该程序能够执行哪些指令,或者能够访问哪些系统资源;也可以赋给系统资源,用来决定哪些程序可以访问它们
    • 刚进入保护模式时执行的代码具有最高特权级0(可以看成从实模式那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。
    • 当操作系统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如3特权级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有些处理器指令(特权指令)只能由0特权级的程序来执行,为的就是安全。
  • P:段存在位,用于指示描述符所对应的段是否存在
    • P位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果P位是“0”,处理器就会产生一个异常中断
    • 该中断处理过程是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内存,并将P位置1
  • D/B:标志位,为了能够在32位处理器上兼容运行16位保护模式的程序。该标志位对不同的段有不同的效果。
    • 对于代码段,此位称作“D”位,用于指示指令中默认的有效地址和操作数尺寸;
      • D=0,表示指令中的有效地址或者操作数是16位的;
      • D=1,指示32位的有效地址或者操作数。
    • 对于栈段和向下扩展的数据段来说,该位被叫作“B”位,用于指定在进行隐式的栈操作时,是使用寄存器SP还是寄存器ESP,隐式的栈操作指令包括push、pop和call等。
      • 如果该位是“0”,在访问那个段时,使用寄存器SP;
      • 否则就是使用寄存器ESP。
  • AVL:表示软件是否可以使用(Available),通常由操作系统来用,处理器并不使用它。
  • L:用于设置是否是64位代码段
    • 为1,表示64位代码段
    • 为0,表示32位代码段

选择子

段寄存器 CS、DS、ES、FS、GS、SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。

而在保护模式下时,在段寄存器中存入的是一个叫作选择子的东西–selector选择子可以看做是个索引值,用于在段描述符表中索引相应的段描述符,这样,便在段描述符中得到了内存段的起始地址和段界限值等相关信息。

image-20241211171032364

以下是选择子结构各属性说明

  • RPL:请求特权级
  • TI(table indicator):表示在GDT(全局描述符表)还是在LDT(局部描述符表)中索引描述符
    • 为0,在GDT中进行索引
    • 为1,在LDT中进行索引
  • 3 ~ 15位,具体描述符的索引值,2^13=8192,故最多可以索引8192个段

我们知道

  • 在实模式下,段地址并非真实的物理地址,在计算物理地址时,还要左移4位(乘以16),而在32位保护模式下,段地址是32位的线性地址,如果未开启分页功能,该线性地址就是物理地址。
  • 所以在保护模式下,直接用选择子对应的“段描述符中的段基址”加上“段内偏移地址”就是要访问的内存地址

具体来说,例如选择子是 0x8(1000),将其加载到 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作为访存地址。

控制寄存器CR0

控制寄存器是CPU的窗口,既可以用来展示CPU的内部状态,也可以用于控制CPU的运行机制

其中CR0的PE位是保护模式的开关

  • 为0时,表示在实模式下运行
  • 为1时表示在保护模式下运行

image-20241211172400487

进入保护模式

代码目录结构

1
2
3
4
5
6
7
.
├── include
│   └── boot.inc     //配置文件
├── loader.S		//loader程序代码
├── Makefile		//编译文件,可直接使用make进行编译代码
├── mbr.S			//mbr程序代码
└── start.sh		//编译脚本,如果不想使用make编译代码,也可以选择运行该脚本编译代码,该脚本的编译命令更加清晰

代码

boot.inc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
;-----loader and kernel-----
    LOADER_BASE_ADDR equ 0x900                      ;loader在内存中位置
    LOADER_START_SECTOR equ 0x2                     ;loader在磁盘中的逻辑扇区地址,即LBA地址


;-----  gdt描述符属性  ---------
    DESC_G_4K   equ   1_00000000000000000000000b    ;设置段界限的单位为4KB
    DESC_D_32   equ    1_0000000000000000000000b    ;设置代码段/数据段的有效地址(段内偏移地址)及操作数大小为32位
    DESC_L    equ       0_000000000000000000000b    ;64位代码段标记位,现在是32位操作系统,因此标记为0即可。
    DESC_AVL  equ        0_00000000000000000000b

;定义段界限位
    ;段界限的第2部分,即描述符的高32位中的第16~19位,最终的代码段段界限为0xFFFFF
    DESC_LIMIT_CODE2 equ 1111_0000000000000000b     ;定义代码段要用的段描述符高32位中16~19段界限为全1
    DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2           ;定义数据段要用的段描述符高32位中16~19段界限为全1
    DESC_LIMIT_VIDEO2  equ 0000_000000000000000b    ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0
    DESC_P  equ 1_000000000000000b                  ;定义了段描述符中的P标志位,表示该段描述符指向的段是否在内存中

;定义描述符的特权级别位
    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                 ;代码段和数据段都是非系统段,故类型字段s设置为1
    DESC_S_DATA equ DESC_S_CODE                     ;代码段和数据段都是非系统段,故类型字段s设置为1
    DESC_S_sys equ 0_000000000000b                  ;系统段的类型字段设置为0
;定义子类型位
    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

原书勘误,显存描述符应该为的最后应该是0x0b,而不是0x00

1
2
3
4
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

loader.S

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
%include "boot.inc"

SECTION loader vstart=LOADER_BASE_ADDR
;初始化栈指针地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start

;------------- 构建gdt及其内部的描述符 -------------
    ;使用GDT_BASE定义GDT的基址,dd是伪指令,用于定义双字变量,一个字是2个字节
    GDT_BASE: 
    ;定义GDT的第0个描述符(操作系统规定必须为全0)
        dd 0x00000000
        dd 0x00000000

    ;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000
    ;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFF
    CODE_DESC:  
        dd 0x0000FFFF
        dd DESC_CODE_HIGH4;段描述符的高4字节部分
    DATA_STACK_DESC: 
        dd 0x0000FFFF
        dd DESC_DATA_HIGH4

    ;定义显存段的描述符
    ;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型
    ;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,
    ;段粒度位4k,因此段界限的值为0x7fff/4k=7
    VIDEO_DESC: 
        dd 0x80000007
        dd DESC_VIDEO_HIGH4

    GDT_SIZE equ $-GDT_BASE
    GDT_LIMIT equ GDT_SIZE-1

    ;dq,定义四字变量,也就是8字节变量
    times 60 dq 0 ;此处预留60个描述符的空位
    
;------------- 构建选择子 -------------
    SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0


;------------- 定义gdtr(指向GDT的寄存器) -------------
    gdt_ptr dw GDT_LIMIT
            dd GDT_BASE
            
;------------- 加载器的显示信息 -------------
    loadermsg db '2 loader in real .'


loader_start:
;------------------------------------------
;INT 0x10 功能号:0x13 功能描述:打印字符串
;------------------------------------------
;输入:
    ;AH:功能号
    ;AL:显示输出方式
    ;   0——字符串中只含显示字符,显示属性在BH中,显示后光标位置不变
    ;   1——字符串中只含显示字符,显示属性在BH中,显示后光标位置改变
    ;   2——字符串中含显示字符和显示属性,显示后光标位置不变
    ;   3——字符串中含显示字符和显示属性,显示后光标位置改变
    ;BH:页码
    ;BL:属性
    ;CX:字符串长度
    ;(DH、DL):坐标(行、列)
    ;ES:BP 字符串地址
;无返回值
    mov sp,LOADER_BASE_ADDR
    mov bp,loadermsg
    mov cx,17
    mov ax,0x1301
    mov bx,0x001f;页号为0,蓝底粉红字
    mov dx,0x1800
    int 0x10

;------------- 准备进入保护模式 -------------
;1.打开A20
;2.加载gdt
;3.置cr0的PE位为1

    ;------------- 打开A20 -------------
    in al,0x92
    or al,0000_0010B
    out 0x92,al
    
    ;------------- 加载gdt -------------
    lgdt [gdt_ptr]

    ;------------- 置cr0的PE位为1 -------------
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax

    jmp dword 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        ;初始化站栈指针,将栈指针地址放入bsp寄存器
    mov ax,SELECTOR_VIDEO           ;初始化显存段寄存器,显存段的选择子放入gs寄存器
    mov gs,ax

    mov byte [gs:160],'p'

    jmp $

mbr.S

由于loader程序已经超过了512字节,因此我们要把mbr.S代码中从磁盘加载loader的读入扇区数增大,原来是读入1个扇区,我们直接修改为读入4个扇区,如下所示

1
2
3
4
    mov eax,LOADER_START_SECTOR ;起始扇区地址
    mov bx,LOADER_BASE_ADDR     ;要写入的内存地址
    mov cx,4                    ;待读入的扇区数目
    call rd_disk_m16            ;调用磁盘读取程序

完整的mbr.S代码如下

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
;主引导程序MBR,由BIOS通过jmp 0:0x7c00跳转
 
;------------------------------------------
%include "boot.inc"
;vstart=0x7c00表示本程序在编译时,起始地址编译为0x7c00
SECTION MBR vstart=0x7c00
;由于BIOS是通过jmp 0:0x7c00跳转到MBR,故此时cs为0
;对于 ds、es、fs、gs 这类 sreg,CPU 中不能直接给它们赋值,没有从立即数到段寄存器的电路实现,只有通过其他寄存器来中转,这里我们用的是通用寄存器ax来中转。
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
;初始化栈指针
    mov sp,0x7c00
;存入显存的段基址
    mov ax,0xb800
    mov gs,ax
;------------------------------------------
 
 
;------------------------------------------
;利用0x06号功能进行清屏
;INT 0x10 功能号:0x06 功能描述:上卷窗口清屏
;输入:
    ;AH 功能号:0x06
    ;AL=上卷的行数(如果为0,则表示全部)
    ;BH=上卷的行属性
    ;(CL,CH)=窗口左上角(X,Y)位置
    ;(DL,DH)=窗口右下角(X,Y)位置
;返回值
    ;无返回值
    mov ax,0x0600
    mov bx,0x0700
    mov cx,0        ;左上角(0,0)
    mov dx,0x184f   ;右下角(80,25)
                    ;0x18=24,0x4f=79
    int 0x10        ;调用BIOS中断函数
;------------------------------------------
 
;------------------------------------------
;将要显示的字符串写入到显存中
    mov byte [gs:0x00],'1'      ;在第一个字节的位置写入要显示的字符“1”
                                ;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
    mov byte [gs:0x01],0xA4
 
    mov byte [gs:0x02],' '
    mov byte [gs:0x03],0xA4
    
    mov byte [gs:0x04],'M'
    mov byte [gs:0x05],0xA4
    
    mov byte [gs:0x06],'B'
    mov byte [gs:0x07],0xA4
 
    mov byte [gs:0x08],'R'
    mov byte [gs:0x09],0xA4
 
    mov eax,LOADER_START_SECTOR ;起始扇区地址
    mov bx,LOADER_BASE_ADDR     ;要写入的内存地址
    mov cx,4                    ;待读入的扇区数目
    call rd_disk_m16            ;调用磁盘读取程序
 
    jmp LOADER_BASE_ADDR;
 
;------------------------------------------
;函数功能:读取硬盘的n个扇区
;参数:
    ;eax:LBA扇区号
    ;bx:要写入的内存地址
    ;cx:待读入的扇区数目
 
rd_disk_m16:
    ;out命令,通过端口向外围设备发送数据
    ;其中目的操作数可以是8为立即数或者寄存器DX,源操作数必须是寄存器AL或者AX
    ;因此需要将之前ax中保存的值进行备份
    mov esi,eax
    mov di,cx
 
;正式读写硬盘
;第一步:设置要读取的扇区数目
    mov dx,0x1f2
    mov ax,cx
    out dx,ax
    
    mov eax,esi		    ;恢复eax的值
 
;第二步:将LBA的地址存入0x1f3~0x1f6
    ;LBA的0~7位写入端口0x1f3
    mov dx,0x1f3
    out dx,al
 
    ;LBA的8~15位写入端口0x1f4
    mov cl,8
    shr eax,cl		    ;将ax右移8位
    mov dx,0x1f4
    out dx,al
 
    ;LBA的16~23位写入端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al
 
    ;LBA的24~27位写入端口0x1f6
    shr eax,cl		    ;右移8位
    and al,0x0f		    ;与0000 1111相与,取后四位
    or al,0xe0		    ;设置7~4位为1110,表示lba模式
    mov dx,0x1f6
    out dx,al
 
;第三步,向0x1f7端口写入读命令,0x20
    mov dx,0x1f7
    mov al,0x20
    out dx,al
 
;第四步,检测硬盘状态
    .not_ready:
        nop             ;空操作,什么也不做,相当于sleep,只是为了增加延迟
        in al,dx        ;由于读取硬盘状态信息的端口仍旧是0x1f7,因此不需要再为dx指定端口号
        
        and al,0x88     ;取出状态位的第3位和第7位
                        ;其中第3位若为1表示硬盘控制器已经准备好数据传输,第7位为1表示硬盘忙
        
        cmp al,0x08     ;cmp为减法指令,如果两个操作数相等,标志寄存器ZF会被置为1
                        ;此处是为了判断第7位是否为1,如果为1,说明硬盘忙,则减法的结果应该为0,相应ZF寄存器的值会被置为1
        
        jnz .not_ready  ;若未准备好,则继续等
                        ;JNZ是条件跳转指令,它表示"Jump if Not Zero",
                        ;也就是如果零标志位(ZF)不为0,则进行跳转。否则不进行跳转

;第五步,从0x1f0端口读取数据
    mov ax,di           ;di是之前备份的要读取的扇区数
    mov dx,256

    mul dx              ;一个扇区512字节,每次读取两个字节,需要读取di*256次
                        ;mul指令表示乘法操作,当只有一个操作数时,被乘数隐含在al或者ax中

    mov cx,ax           ;cx在此时表示要循环读取的次数
 
    mov dx,0x1f0
    .go_on_read:
        in ax,dx        ;从0x1f0端口读取2个字节的数据到寄存器ax
        mov [bx],ax     ;读取的数据写入bx寄存器所指向的内存
        add bx,2        ;目的内存地址偏移2个字节继续循环读取剩余的数据
        loop .go_on_read
        ret
 
    times 510-($-$$) db 0
    db 0x55,0xaa

编译

1
nasm -I ./include/ ./mbr.S -o mbr.bin
1
nasm -I ./include/ ./loader.S -o loader.bin

磁盘写入

注意loader.S写入磁盘时count参数为2

1
dd if=/home/minios/osCode/miniOS_32/ch4/mbr.bin of=/home/minios/bochs/hd60M.img bs=512 count=1 conv=notrunc
1
dd if=/home/minios/osCode/miniOS_32/ch4/loader.bin of=/home/minios/bochs/hd60M.img bs=512 count=2 seek=2 conv=notrunc

运行

image-20241211190213011

  • 左下角的字符串“2 loader inreal”是在实模式下用 BIOS 中断 0x10 打印的。
  • 左上角第2行的字符P,这是在保护模式下输出的。一个程序历经两种模式,各模式下都打印了字符,为了区别实模式下的打印,所以字符串中含有“inreal”。

输出p说明GDT成功建立,如下调试所示

1
2
3
4
5
6
7
<bochs:3> info gdt
Global Descriptor Table (base=0x00000903, limit=31):
GDT[0x00]=??? descriptor hi=0x00000000, lo=0x00000000
GDT[0x01]=Code segment, base=0x00000000, limit=0xffffffff, Execute-Only, Non-Conforming, Accessed, 32-bit
GDT[0x02]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
GDT[0x03]=Data segment, base=0x000b8000, limit=0x00007fff, Read/Write, Accessed
You can list individual entries with 'info gdt [NUM]' or groups with 'info gdt [NUM] [NUM]'
网站已运行
发表了20篇文章 · 总计 138,209字