操作系统——硬盘(四)

操作系统——MBR与硬盘(四)

2020-09-14 15:56:14 hawk


概述

  这一篇博客主要介绍一下硬盘基础知识,然后学习CPU如何与硬盘进行沟通,最后完善前面写的MBR程序,完成对磁盘的操作。


磁盘机制

磁盘工作原理

  实际上对于磁盘的工作原理,我们如果弄懂了下面这张图,磁盘的工作机制对于我们就会非常简单。

 

 

 

  下面主要就是介绍这幅图,首先左面的主轴上有多个盘面,这里仅仅画出了两个盘面,仅用于示意。每个盘面都包含上、下两面,每面都存储数据,并且每面都由且仅由一个磁头进行数据的读取,即一个盘片上有两个磁头,而盘面和磁头是一一对应关系,因此我们可以使用磁头号来标识盘面,一般从上到下以0开始计数。这里需要说明的是,由于磁头臂是固定的,因此磁头仅仅只能做圆周运动,其在扇区上轨迹为弧形,但可以近似为径向运动,考虑到盘片的自转,因此两个动作合成,使磁头可以读取盘片上的任意位置。

  下面在简单介绍一下磁道、扇区和柱面等概念。为了方便管理,会将盘面划分为多个同心环,同时在以盘面的圆心进行画一部分圆,则同心环与圆相交的部分作为最基本的数据存储单元,即被称为 扇区。而那些同心环,则被称为磁道。磁道的编号类似于磁头号,从0开始进行编号。如果我们把处于同一个主轴的磁道垂直扇面相连,称之为柱面。

  这里可能有些人觉得柱面这个概念毫无意义,并非如此。实际上机械式硬盘的寻道时间就是整个机械式硬盘的性能瓶颈。如果我们的写入数据没有特别的方式的话,如果待写入的数据小于一个磁道的剩余容量还好说,直接进行定位,然后进行后面的操作即可;如果待写入的数据要占用多个磁道,则需要多次的变更磁道,多了寻道时间。而如果我们采取按柱面存取,即当0面上的某磁道空间不足的时候,其他数据写入第1面相同编号的磁道上,如果还不足,则继续第2面上的相同编号的磁道,,直到所有的对应编号的磁道都不够使用时,才会更换新的柱面。因此,对于基于这种方法的磁盘来说,盘面越多,则其性能越快。

硬盘控制器——硬盘的IO接口

  前面已经分析过了,CPU通过IO接口与外设进行通信,硬盘自然也不例外。对于硬盘这个外设来说,其IO接口就是硬盘控制器。这里需要介绍几个专有名词,IDE以及ATA等。

  以前硬盘和硬盘控制器是分开的,但是之后的新技术使硬盘和硬盘控制器整合在了一起,这个新的接口被称作集成设备电路(Integraded Drive Electronics,IDE)。而随着IDE的不断发展,其成为了全球硬盘标准,被称为ATA(Advanced Technology Attachment)。而根据技术的发展,实际上ATA也产生了多种分类,主要是硬盘串行接口(Serial ATA)、并行ATA(Parallel ATA)。

  对于PATA来说,一般的主机只支持4个并行ATA;而对于新出现的SATA来说,支持多少块硬盘取决于主板的能力。实际上由于SATA后出现,而兼容性又是计算机科学方面的优良传统,这里我们主要讲述一下PATA相关的知识。

  实际上,PATA与主板通过PATA接口的线缆相连接,这个线缆也被称为IDE线。一个IDE线上可以挂载两块硬盘,一个称为主盘(Master),另一个是从盘(Slave)。而由于前面讲到的,一般的主机仅仅支持4个并行ATA,也就是其提供两个IDE插槽,一个称作IDE0,另一个是IDE1。当然其有另外的术语,这两个IDE插槽又被称为通道,其中IDE0叫做Primary通道,IDE1叫做Secondary通道。

硬盘控制器端口

  正如前面介绍外设的时候分析到的,我们通过控制IO接口的端口来控制IO接口,从而使CPU与外设进行通信。对于硬盘控制器来说,其端口就是指硬盘控制器上的寄存器。下面我们介绍一下后面可能会使用到的部分端口,其对应的功能如下所示

IO端口 端口用途
Primary通道 Secondary通道 读操作时 写操作时
Command Block registers
0x1f0 0x170 Data Data
0x1f1 0x171 Error Features
0x1f2 0x172 Sector count Sector count
0x1f3 0x173 LBA low LBA low
0x1f4 0x174 LBA mid LBA mid
0x1f5 0x175 LBA high LBA high
0x1f6 0x176 Device Device
0x1f7 0x177 Status Command
Control Block registers
0x3f6 0x376 Alternate status Device Control

 

  下面我们就上面这张表进行简单的解释。实际上端口可以简单的被分为两组,一组为Command Block registers和Control Block registers。其中Command Block registers用于向硬盘驱动器写入命令或者从硬盘控制器获得硬盘状态;而Control Block registers用于控制硬盘工作状态。然后是对应的端口用途。

  对于Data寄存器来说,其用来管理数据,是16位(表上其他的寄存器位8位)。一般来说,对于读磁盘时,不断读此寄存器相当于读出硬盘控制器中的缓冲数据;对于写磁盘来说,将数据写入该磁盘相当于向硬盘控制器写入数据。

  对于Error寄存器来说,只有在读取硬盘失败的时候,端口为0x171/0x1f1的寄存器中才记录失败的信息,尚未读取的扇区数放在Sector count寄存器中。

  对于Feature寄存器来说,只有在写硬盘时,并且需要额外指定参数时,将参数写入Feature寄存器中。

  而下面则是逻辑块地址(Logical Block Address,LBA)寄存器,这里需要首先简单的介绍一下LBA相关信息。前面分析的时候,我们都是按照“柱面-磁头-扇区”来进行定位,这种方法简称为CHS(Cylinder Head Sector)。但是这种方法对于程序员来说过于复杂,我们希望使用逻辑上的地址进行定位,而不需要考虑物理结构,这就是LBA。目前LBA共有两种,LBA28和LBA48,即28比特/48比特描述扇区的地址,而由于每个扇区都是512字节,则其支持的最大硬盘容量为128GB/128PB。而LBA low、LBA mid和LBA high则分别对应地址的0-7位、8-15位和16-23位。明显仅仅靠这些寄存器无法唯一标识LBA,则还需要使用Device寄存器,其低4位用来表示LBA28的24-27位,从而与上面可以共同完成LBA28的表示;第4位标识通道中的主盘或从盘;第5、7位为固定的1,称为MBS位;第6位标识是否启用LBA模式。

  对于Status寄存器来说,其在读硬盘时,用来给出硬盘的状态信息。第0位时ERR位,如果为1,表示命令出错,原因可以查看Error寄存器;第3位是data request位,如果为1,表示硬盘数据已经准备完毕,可以读取数据;第6位是DRDY,如果为1,表示硬盘检测正常;第7位是BSY位,如果为1,表示硬盘正忙,其余所有位都无效。其余的如果感兴趣大家可以自行查阅(书上没讲,我也不会。。)

  最后需要了解的则是Command寄存器,用来存储让硬盘执行的命令,主要有三个——identif(0xEC,即硬盘识别);read sector(0x20,即读扇区);write sector(0x30,即写扇区)

  如果大家对于其他的寄存器感兴趣的话,可以查阅ATA系列硬盘规范。(这里过于底层,因此我个人认为只需要了解实验中用到的命令即可)

常用硬盘操作方法

  硬盘中可用的指令有很多,想要更加详细的了解的话可以参考ATA手册,这里主要介绍数据的传送方式(注意这仅仅是指令执行的步骤,不设计具体的指令执行)

  1.  查询传送方式

  2.  中断传送方式

 

  我们依次解释上面的几种传送方式。

  对于查询传送方式,也称为程序I/O、PIO(Programming Input/Output Model),指传输之前,先获取硬盘控制器的Status寄存器,如果为数据可用,则获取数据即可;

  对于中断传送方式,也成为中断驱动I/O,当硬盘设备准备好数据后,通过发送中断通知来通知CPU,然后CPU直接获取数据。

MBR完善

  前面我们的MBR程序都可以正常运行,但是实际上并没有实现什么有用的功能。这里我们将修改和完善MBR程序,从而使其可以读写磁盘。

  这里我们在简单的分析一下,实际上MBR指令大小最多510(512扇区-2标志),则这么小的空间中,实际上是很难完成内核的准备与加载到内存中的,因此我们一般是在另一个程序中完成初始化环境和加载内核的任务,这个程序就是loader,即加载器。因此实际上在MBR中必须要实现的功能就是从硬盘上把loader加载到内存中,并且将接力棒交给它。

  而由于MBR是占据了硬盘的第0扇区(LBA方式),那么很自然的,我们会想到将loader放到邻近的扇区上即可。这里我们不妨直接放到第1扇区(这里只要是约定好的固定位置即可,书上是第2扇区),因此MBR的功能就是将loader从硬盘的第1扇区上加载到内存。这里我们将其放到前面分析的实模式1MB中标名为可用内存的范围中。这里说明一下,实际上loader中定义了一些极其重要的数据结构(如GDT等),这些数据结构内核仍然会使用到,因此不能覆盖掉。并且考虑到后面还会有内核的载入,因此我们将loader加载到一个1MB内存布局的低可用内存地址即可,这里可以随便选择(因为MBR是自己实现的,因此自己指定即可),这里我选择0x700(只要是1MB内存布局中可用内存,且之后不会被覆盖即可)。

  总结一下,这次MBR我们需要实现磁盘的读写函数,从而实现将位于第1扇区(LBA方式)的loader加载到0x700地址处即可。


实验

  这次我们仍然基于之前实现的MBR程序,但是对其稍加改造,结合本章的内容,使其可以读写Primary通道的salve硬盘。该实验源代码在该链接处

  我们首先直接给出源代码,然后在对其进行分析。MBR的源代码如下所示

;   简单的主引导程序,这里实现了读写Primary的slave硬盘的函数
;    从而将位于第二扇区(LBA方式)的loader加载到0x700(实际上只要是1MB可用内存,并且之后不会被覆盖即可)
;------------------------------------------------------------------------

%include "boot.inc"
;    类似于C语言的宏定义
;--------------------------------------------------------------------------------
;    这个文件的主要定义如下所示
;    LOADER_BASE_ADDR equ 0x700
;    LOADER_START_SECTOR equ 0x1


SECTION MBR vstart=0x7c00    ;这个地址表示将起始地址设置为0x7c00——因为BIOS会将MBR程序加载到0x7c00处

    mov    ax, cs
    mov    ds, ax        ;由于BIOS跳转到MBR时,使用指令jmp 0:0x7c00,因此cs段寄存器为0,这里将ds段寄存器也设置为了0
    mov    sp, 0x7c00    ;根据已知,至少0x500-0x7DFF为可用区域,则将其当用作栈即可(这里需要说明一下,可能会与loader负载,需要注意一下)

    mov    ax, 0xB800
    mov    es, ax        ;根据已知,实模式1MB内存中0xB8000-0xBFFFF为文本模式的显示适配器,方便之后通过直接寻址读写内存


;    下面首先清空屏幕,这里使用BIOS提供的中断即可,
;------------------------------------------------------------------------
;    INT 0x10;    功能号:0x06    功能描述:上卷窗口
;------------------------------------------------------------------------
;    输入:
;        AH--功能号:    0x06
;        AL--上卷的行数(如果为0,表示全部)    
;        BH--上卷行属性
;        (CL, CH)--窗口左上角(X, Y)位置
;        (DL, DH)--窗口右下角(X, Y)位置
;    输出:
;

    mov    ax, 0x600    ;AH = 0x06;    AL = 0x0;
    mov    bx, 0x700    ;BH = 0x07;    BL = 0x0;
    mov    cx, 0x0        ;CH = 0x0;    CL = 0x0;
    mov    dx, 0x184f    ;DH = 0x18;    DL = 0x4f;
    int    0x10
;------------------------------------------------------------------------
;    我们将上面的系统调用分析一下
;    输入:    AH--0x06;    AL--0x0;    BH--0x7;    CL--0x0;CH--0x0;    DL--0x4f;DH--0x18;
;    也就是我们调用了功能号为0x6的BIOS中断,窗口左上角为(0x0/0, 0x0/0),窗口右上角坐标为(0x4f/80, 0x18/25),上卷所有的窗口
;    在VGA文本模式中,一般一行容纳80个字符,共25行,也就相当于清空了整个屏幕




;    向1MB内存中的文本模式的显示适配器区域写入数据
;------------------------------------------------------------------------
;    每个字符2字节,其低字节为字符对应的ASCII码,高字节为字符的属性
;    由于其为背景白色,前景黑色,不闪烁,其高字节值为 01110000b
;------------------------------------------------------------------------

    mov    cx, 0x0
    mov byte    al, [format]        ;初始化计数器cx,。由于前面已经设置了ds段寄存器为0,该指令相当于将字符属性字节读入ax寄存器中

    LOOP:
        mov    di, cx

        mov byte    dl, [di + string]    ;这里通过变址寻址访问内存,由于前面设置了ds段寄存器为0,这里直接获取字符串中的对应字符
        sub    dl, 0
        jz    LOOPEND            ;判断字符串是否结束。有条件跳转,因此仅仅修改段偏移地址,由于cs始终为0,自然跳转到LOOPEND对应的位置

        add    di, di
        mov byte    [es:di], dl        ;这里通过变址寻址访问内存

        add    di, 1
        mov byte    [es:di], al        ;这里通过变址寻址访问内存
        
    add    cx, 1
           jmp near    LOOP            ;无条件相对近跳转,会重新跳转到LOOP处执行循环

    LOOPEND:
;------------------------------------------------------------------------
;    我们将上面的指令分析一下
;    可以看到,对于内存寻址来说,这里通过直接寻址进行寻址
;    我们每一次输入两个字节信息,其中低字节是上面分析的字符的属性
;    高字节是字符对应的ascii码,从而完成了内存的写入。





;    下面我们使用函数调用来读取硬盘中的MBR程序至约定好的0x700处,调用代码如下所示
;------------------------------------------------------------------------
    mov    eax, LOADER_START_SECTOR        ;loader程序起始扇区的LBA地址,前面的宏定义
    mov    bx, LOADER_BASE_ADDR            ;loader程序加载到内存中的地址,前面的宏定义
    mov    cx, 1                    ;我们这里先读取1个扇区的大小,根据自己loader实际大小变化
    call    rd_disk_n

;------------------------------------------------------------------------
;    我们将上面的指令分析一下
;    实际上其相当于将硬盘中LOADER_START_SECTOR扇区开始,cx个扇区的内容读入到起始地址为LOADER_BASE_ADDR的内存中



;    下面我们MBR结束执行,讲执行流直接交给loader程序即可
;-------------------------------------------------------------------------
    jmp    LOADER_BASE_ADDR            ;这是相对近转移,因为仍然处于同一个段中,所以仅需要给出相对偏移即可




;    下面我们实现一个函数调用
;--------------------------------------------------------------------------
;    函数名称: rd_disk_n    功能描述:读取硬盘的n个扇区到指定地址
;--------------------------------------------------------------------------
;    输入:
;        eax--LBA扇区号
;        bx--讲数据写入的内存地址
;        cx--读入的扇区数
;    输出:
;--------------------------------------------------------------------------
rd_disk_n:
    
    mov    esi, eax;    备份eax寄存器的值
    mov    di, cx;        备份cx寄存器的值

;    第1步,设置要读取的扇区数
    mov    dx, 0x1f2
    mov    al, cl
    out    dx, al        ;向0x172寄存器写入cx的值

    mov    eax, esi    ;恢复eax的值

;    第2步,将LBA地址存入LBA寄存器
;    将LBA的低8位存入0x1f3
    mov    dx, 0x1f3
    out    dx, al        ;向0x173寄存器写入LBA扇区号的低8位

;    将LBA的8-15位存入0x1f4
    mov    dx, 0x1f4
    shr    eax, 8
    out    dx, al        ;向0x174寄存器写入LBA扇区号的8-15位

;    将LBA的16-23位存入0x1f5
    mov    dx, 0x1f5
    shr    eax, 8
    out    dx, al        ;向0x1f5寄存器写入LBA扇区号的16-23位


;    将LBA的24-27存入0x1f6,并且设置对应的第4位为1,从盘;第6位1,LBA;第5,7位1,MBS
    mov    dx, 0x1f6
    shr    eax, 8
    or    al, 11110000b
    out    dx, al        ;向0x1f6寄存器写入     1111 | LBA扇区号的24-27位

;    第3步,将0x1f7端口写入读命令,即read sector,0x20
    mov    dx, 0x1f7
    mov    al, 0x20
    out    dx, al


;    第4步,检测硬件状态
.not_ready:
    nop            ;相当于什么都不做,防止打扰磁盘的工作
    in    al, dx        ;读取硬盘Status寄存器

    and    al, 00001000b    ;第3位表示硬盘数据已经准备好了
    cmp    al, 00001000b
    jnz    .not_ready    ;如果数据未准备好,则继续等待


;    从0x1f0端口读取数据
    mov    ax, di        ;将前面备份的cx赋值给ax,也就是读取的扇区数
    mov    dx, 256        ;data寄存器16位,一次读取2字节,共cx * 512 / 2次
    mul    dx        ;这里是mul指令,格式为mul register,计算register * ax/al
                ;如果register是8位,则为register * al,结果放在ax寄存器中
                ;如果register是16位,则为register * ax,高16位放在dx寄存器中,低16位放在ax寄存器中
                ;这里是第2中情况,即将总字节数放置在(dx, ax)寄存组中,因为仅仅256就大于8位,因此这种是合理的

    mov    cx, ax        ;这里设置循环次数,虽然上面说了乘积为16位,但是实际上我们的loader不会太大
                ;因此其字节数不会超过16位,我们只需要低位即可

    mov    dx, 0x1f0

.go_on_read:
    in    ax, dx        ;读取硬盘Data寄存器
    mov word    [bx], ax;变址寻址,将数据存储到目的地址中,前面ds段寄存器已经被置为0

    add    bx, 2    
    loop    .go_on_read    ;重复循环cx次,这里需要说明,由于在实模式下,因此bx最多16位
                ;即其遍历最大0x0-0xffff的值,也就是其最多可以读入的内存为64KB


    ret            ;因为其为近跳转,没有段寄存器的变化,所以ret即可



;    下面进行常量设置
;------------------------------------------------------------------------
    string db "Hawk's MBR", 0;        即伪操作指令,表示每一个元素大小为1字节, 并且在结尾为\x00表明字符串结束
    format db 00011101b;            这里是显存中的字符属性,表明其为背景蓝色,前景色浅品红色,不闪烁   
;    下面进行空白填充,确保最后程序为512字节
;------------------------------------------------------------------------
    times 510 - ($ - $$) db 0
;------------------------------------------------------------------------
;    我们将上面的汇编语句分析一下
;    $表示当前行的地址,$$表示当前SECTION的起始地址,times也是伪操作指令,相当于将后面的数据重复指定次数
;    这个指令确保了将程序填充至512字节,中间部分以0填充




;    下面我们最后填充该512字节的最后两个字节,为0x55,0xaa,从而使BIOS成功识别MBR
;------------------------------------------------------------------------
    db    0x55, 0xaa

 

  实际上这篇汇编代码的注释已经非常详细,这里稍微在简单说明一下——这个MBR程序仍然首先输出相关的字符串,即“Hawk‘s MBR”,表示该MBR程序成功加载,这个和前面的MBR基本没有什么大的区别。然后这个程序需要将硬盘上的loader程序加载到内存上,也就是读取硬盘,并将程序流跳转,这里需要有几点注意

  1.  关于loader程序在硬盘上的地址以及加载到内存上的地址,其在另外一个文件中(include/boot.inc)中,内容如下所示

;-------------------------- loader 和 kernel------------------
LOADER_BASE_ADDR equ 0x700
LOADER_START_SECTOR equ 0x1

 

  我们只需要类似于c语言的include一样,将其包含入mbr.S文件中即可,只需要在生成机器码的时候指定好文件位置即可。

  2.  对于读取硬盘的功能,这里是实现了一个读取硬盘的函数——rd_disk_n函数,里面主要用我们前面讲述的IO接口相关的知识进行完成——通过in、out完成IO接口的端口读写,通过对于IO接口的端口读、写特定的数据,完成相关的功能。如果对于这些还有疑惑的话,可以在具体看一下前面的博客,或者读一些作者的原书,我认为已经写的非常详细了。这里再额外说明的是,对于实模式下,虽然32位寄存器仍然存在,且资源可用;但是基本操作仍然是在16位下进行的——比如内存地址查找,loop循环指令默认的寄存器cx,需要注意一下(这里因为我没有太深入学习过汇编语言,所以也不是很懂,)。

  3.  程序执行流的跳转,这里类似于BIOS转移到MBR的指令,并且考虑到处于同一个段上,因此直接通过无条件近距离相对近转移完成程序流的转移即可。

  4.  loader程序的大小——这个是根据MBR程序如何加载决定的。在我实现的这个MBR程序中,将loader程序的数据通过变址寻址的方式写入内存,没有什么特殊的操作,因此这种情况下最多可以写到的地址范围也就是实模式下相关寄存器的宽度,也就是16位,即64KB。因此之后通过这个MBR加载的loader大小不能超过64KB。如果我们另外实现了其他的MBR,其加载内存的方式不同于我们目前实现的这个,则loader的大小上限根据其可加载到的内存的范围不同而不同,这里需要明确一下。

 

  而这里我们为了确认确实将loader程序加载进入了内存中,并且完成了程序执行流的跳转,简单实现一个输出字符串的功能,然后悬停——和前面的MBR程序十分相似,源代码如下所示

;    这里实现一个简单的loader,并不是先具体功能,仅仅实现字符串并悬停
;    方便观察成功加载了loader
;------------------------------------------------------------------------

%include "boot.inc"
;    类似于C语言的宏定义
;--------------------------------------------------------------------------------
;    这个文件的主要定义如下所示
;    LOADER_BASE_ADDR equ 0x700
;    LOADER_START_SECTOR equ 0x1


SECTION    LOADER    vstart=LOADER_BASE_ADDR     ;这个地址表示将起始地址设置为LOADER_BASE_ADDR——因为MBR会将loader程序加载到LOADER_BASE_ADDR处


;    另起一行输出相关信息

    mov    ax, es
    add    ax, 10
    mov    es, ax        ;前面es段寄存器设置为显存文本模式的起始地址。而由于VGA文本模式一行80个字符,每个字符显存中用2字节表示
                ;由于是段寄存器,(地址 >> 4)则下一行只需要修改为es+(80 * 2 / 16),即可获取VGA文本模式的第二行



;    向1MB内存中的文本模式的显示适配器区域写入数据
;------------------------------------------------------------------------
;    每个字符2字节,其低字节为字符对应的ASCII码,高字节为字符的属性
;    由于其为背景蓝色,前景色浅品红色,不闪烁,其高字节值为 00011101b
;------------------------------------------------------------------------

    mov    cx, 0x0
    mov byte    al, [format]        ;初始化计数器cx,。由于前面已经设置了ds段寄存器为0,该指令相当于将字符属性字节读入ax寄存器中

    LOOP:
        mov    di, cx

        mov byte    dl, [di + string]    ;这里通过变址寻址访问内存,由于前面设置了ds段寄存器为0,这里直接获取字符串中的对应字符
        sub    dl, 0
        jz    LOOPEND            ;判断字符串是否结束。有条件跳转,因此仅仅修改段偏移地址,由于cs始终为0,自然跳转到LOOPEND对应的位置

        add    di, di
        mov byte    [es:di], dl        ;这里通过变址寻址访问内存

        add    di, 1
        mov byte    [es:di], al        ;这里通过变址寻址访问内存
        
    add    cx, 1
           jmp near    LOOP            ;无条件相对近跳转,会重新跳转到LOOP处执行循环

    LOOPEND:
;------------------------------------------------------------------------
;    我们将上面的指令分析一下
;    可以看到,对于内存寻址来说,这里通过直接寻址进行寻址
;    我们每一次输入两个字节信息,其中低字节是上面分析的字符的属性
;    高字节是字符对应的ascii码,从而完成了内存的写入。


;    下面进行循环,确保程序悬停在该处,从而观察输出
;------------------------------------------------------------------------

    jmp    $

;------------------------------------------------------------------------
;    我们将上面的指令分析一下
;    $表示当前行的地址,这样子相当于始终执行这一行指令,从而使程序悬停




;    下面进行常量设置
;------------------------------------------------------------------------
    string db "Hawk's LOADER", 0;        即伪操作指令,表示每一个元素大小为1字节, 并且在结尾为\x00表明字符串结束
    format db 00011101b;            这里是显存中的字符属性,表明其为背景蓝色,前景色浅品红色,不闪烁   

 

  对于这篇汇编代码来说,其注释写的也比较详细了,这里再稍微说明一下

  1.  注意es段寄存器的设置——我们在MBR程序中,就是将es段寄存器设置为输出的显存的起始地址,这里为了区别上面MBR的输出,进行另起一行输出——而由于VGA文本模式下其文本布局为80 * 25,即一行有80个字符,并且在文本模式下一个字符2字节,因此我们需要在原来的显存的起始地址基础上增加80 * 2,但是有考虑到是段寄存器,也就是其值为地址 >> 4,因此我们只需要在原始es的值的基础上再加上80 * 2 / 16 = 10即可。

  2.  这里不再需要填充至512字节以及结尾的标志位。因为是直接通过MBR加载,并不需要结尾的标志位判断是否存在loader程序(因为这些都是自己提前准备好的),因此不需要填充或者在结尾添加标志位。

 

  下面就是构建相关的虚拟硬盘,命令如下所示

nasm -I include/ -o mbr.bin mbr.S
nasm -I include/ -o loader.bin loader.S
dd if=mbr.bin of=hawk.img bs=512 count=1 conv=notrunc
dd if=./loader.bin of=hawk.img seek=1 bs=512 count=1 conv=notrunc

 

  结果如图所示

 

 

 

  这里需要说明一下,因为我们之前在MBR程序中写的loader程序在第1扇区(LBA方式),因此我们在通过dd写入虚拟硬盘的时候,也需要写入到第1扇区(LBA方式),因此通过seek选项跳过1个block,从而写入到第1扇区(LBA方式)。下面就是启动qemu的模拟器显示相关的内容。由于我们在MBR中读取的时候,设置的硬盘是Primary通道的salve硬盘,因此我们在启动的时候也需要设置成对应的硬盘,而qemu是通过-hda、-hdb、-hdc和-hdd来表示的,这里是Primary通道的slave硬盘,因此我们选择-hdb进行启动即可,命令如下所示

qemu-system-i386 -hdb hawk.img

 

  结果如图所示

 

 

 

 

 

   当我们使用-hda的话,则Primary通道的slave硬盘并不存在,则读取的Status寄存器中第3位始终为0,则程序会不停重复检查硬盘状态。可以看到,我们成功完成了读取硬盘上loader程序,并且将控制流转移到了loader程序上。

posted @ 2020-09-15 12:41  hawkJW  阅读(984)  评论(0编辑  收藏  举报