手写操作系统(五)-MBR操控硬盘

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.概述

CPU与外设通信的桥梁就是IO接口。对于硬盘也一样。

CPU针对硬盘的 IO 接口就是硬盘控制器。

我们需要通过读写硬盘控制器的端口让硬盘工作,端口就是位于IO控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。

常用的硬盘控制器主要端口寄存器如下图:

端口可以被分为两组,Command Block registers和Control Block registers。 Command Block registers用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态,Control Block registers 用于控制硬盘工作状态。

端口是按照通道给出的,一个通道上的主、从两块硬盘都用这些端口号。 要想操作某通道上的某块硬盘,需要单独指定。 比如上面有个叫device的寄存器,顾名思义,指的就是驱动器设备,也就是和硬盘相关。 不过此寄存器是8位的,一个通道上就两块硬盘,指定哪一个硬盘只用1位就够了,寄存器可是很宝贝的资源,不能浪费,所以此寄存器是个杂项,很多设置都需集中在此寄存器中了,其中的第4位,便是指定通道上的主或从硬盘, 0为主盘, 1为从盘。

端口用途在读硬盘和与硬盘时还是有点区别的,比如拿Primary通道上的0x1F1端口来说,读操作时,若读取失败,里面存储的是失败状态信息,所以称为error寄存器,并且0x1F2端口中存储未读的扇区数。写操作时就变成了feauture寄存器,此寄存器用于写命令的参数。

data寄存器

data寄存器是负责管理数据的,其作用是读取或写入数据。 数据的读写还是越快越好,所以此寄存器较其他寄存器宽一些, 16位,在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。 在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。

 

Error寄存器和Feature寄存器

读硬盘时,端口0x171或0x1F1的寄存器名字叫Error寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在Sector count寄存器中。 在写硬盘时,此寄存器有了别的用途,所以有了新的名字,叫Feature寄存器。 有些命令需要指定额外参数,这些参数就写在Feature寄存器中。 这两个寄存器都是8位宽度。

 

Sector count寄存器

Sector count寄存器用来指定待读取或待写入的扇区数。 硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。 这是8位寄存器,最大值为255,若指定为0,则表示要操作256个扇区。

 

LBA寄存器和device寄存器

硬盘中的扇区在物理上是用”柱面磁头-扇区”来定位的(Cylinder Head Sector),简称为CHS。对于磁头来说用”柱面磁头-扇区”来定位很直观。

为了对人来说比较直观的寻址方法,我们希望磁盘中扇区从0开始依次递增编号,不用考虑扇区所在的物理结构,也就是LBA,这是一种逻辑上为扇区址的方法,全称为逻辑块地址(Logical Block Address)。

LBA有两种,一种是LBA28,用28位比特来描述一个扇区的地址。最大寻址范围是2的28次方等于268435456个扇区,每个扇区是512字节,最大支持128GB。我们这里为图简单,采用LBA28模式。由于128GB已经不能满足日益增长的存储需求,硬盘越来越大了,得有相匹配的寻址方法与之配套,于是要介绍的另外一种是LBA48,用48位比特来描述一个扇区的地址,最大可寻址范围是2的48次方,等于281474976710656个扇区,乘以512字节后,最大支持131072TB,即128PB

介绍完了LBA,现在可以说LBA寄存器了,这里有LBA low、LBA mid、LBA high三个,它们三个都是8位宽度的。 LBA low寄存器用来存储28位地址的第0~7位, LBA mid寄存器用来存储第8~15位,LBA high寄存器存储第16~23位。在之前说过了, device寄存器是个杂项,它的宽度是 8 位。 在此寄存器的低 4 位用来存储LBA地址的第24~27位。 结合上面的三个LBA寄存器。 第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘。 第6位用来设置是否启用LBA方式,1代表启用LBA模式,0 代表启用CHS模式。 另外的两位:第 5 位和第 7 位是固定为 1 的,称为 MBS 位。

device寄存器示意图:

 

Status寄存器和command寄存器

在读硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 Status,它是 8 位宽度的寄存器,用来给出硬盘的状态信息。 第0位是ERR位,如果此位为1,表示命令出错了,具体原因可见error寄存器。 第3位是data request 位,如果此位为 1,表示硬盘已经把数据准备好了,主机现在可以把数据读出来。 第6 位是 DRDY,表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。 第7位是BSY位,表示硬盘是否繁忙,如果为1表示硬盘正忙着,此寄存器中的其他位都无效。 另外的4位暂不关注。

在写硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 command,和上面说过的 error 和feature 寄存器情况一样,只是用途变了,所以换了个名字表示新的用途,它和status寄存器是同一个。 此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。

Status寄存器示意图:

 

常用命令

在咱们的系统中,主要使用了三个命令。

  • identify: 0xEC 即硬盘识别。
  • read sector: 0x20 即读扇区。
  • write sector: 0x30 即写扇区。

 

硬盘操作方法

硬盘操作要约定好步骤,才能相互配合,步骤如下:

  1. 先选择通道,往该通道的sector count寄存器中写入待操作的扇区数。
  2. 往该通道上的三个LBA寄存器写入扇区起始地址的低24位。
  3. 往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位,选择操作的硬盘(master硬盘或slave硬盘)。
  4. 往该通道上的command寄存器写入操作命令。
  5. 读取该通道上的 status 寄存器,判断硬盘工作是否完成。
  6. 如果以上步骤是读硬盘,进入下一个步骤。 否则,完工。
  7. 将硬盘数据读出。

对于从硬盘读取数据后的处理,以下是常用的数据传送方式:

  • 无条件传送方式:在程序中直接将数据从存储器的指定位置传送到目标位置,不需要进行特定的条件检查或等待。硬盘不符合该方法。
  • 查询传送方式:在程序中通过查询(轮询)硬件状态的方式,确认硬件是否已经准备好数据,然后再进行数据传送。这种方式适合于需要等待硬件就绪的情况。比如status寄存器。
  • 中断传送方式:硬件完成操作后,会向CPU发送中断信号(硬件中断请求IRQ),CPU收到中断后,暂停当前执行的程序,转而执行与中断相关的中断服务程序(ISR),在ISR中完成数据传送等操作。
  • 直接存储器存取方式 (DMA):DMA允许外设(如硬盘控制器)直接访问系统内存,而无需CPU的介入,从而实现高速数据传输。在DMA方式下,硬盘可以直接将数据传输到指定的内存区域,减少CPU的负担和数据传输延迟。需要硬件支持。
  • I/O 处理机传送方式:这种方式通常指的是通过专门的I/O处理机或者协处理器来处理数据传输。这些设备具有自己的指令集和寄存器,能够独立于CPU执行数据传输任务,从而提高系统的效率和并行处理能力。需要硬件支持。

硬盘不符合第1 种方法,因为它需要在某种条件下才能传输。 第4种和第5 种需要单独的硬件支持,所以在我们采用第2、3这两种软件传输方式。

 

 

2.代码演示

目前,操作系统已经实现了一个简单的引导程序,接下来就是把Loader程序加载到内存里。

修改前面的程序,改名为mbr.asm,使用mbr加载Loader

;将loader放入0x900
LOADER_BASE_ADDR    equ 0x900

;表示已LBA方式,我们的loader在第2块扇区
LOADER_START_SECTOR equ 0x2

SECTION MBR vstart=0x7c00
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00
    mov ax,0xb800
    mov gs,ax
    
;清屏
    mov ax, 0600h
    mov bx, 0700h
    mov cx,0
    mov dx,184fh ;(80,25)
    
    int 10h

;输出当前我们在MBR
    mov byte [gs:0x00], '1'
    mov byte [gs:0x01], 0x94
    
    mov byte [gs:0x02], ' '
    mov byte [gs:0x03], 0x94
    
    mov byte [gs:0x04], 'M'
    mov byte [gs:0x05], 0x94
    
    
    mov byte [gs:0x06], 'B'
    mov byte [gs:0x07], 0x94
    
    mov byte [gs:0x08], 'R'
    mov byte [gs:0x09], 0x94
    
    mov eax,LOADER_START_SECTOR ;LBA 读入的扇区
    mov bx,LOADER_BASE_ADDR       ;写入的地址
    mov cx,1               ;等待读入的扇区数
    call rd_disk                ;以下读取程序的起始部分(一个扇区)
    
    jmp LOADER_BASE_ADDR      ;调到实际的物理内存


rd_disk:
    ;eax LBA的扇区号
    ;bx 数据写入的内存地址
    ;cx 读入的扇区数
    
    mov esi,eax       ;备份eax
    mov di,cx     ;备份cx
    
;读写硬盘
    ; 第1步:设置要读取的扇区数
    mov dx, 0x1f2
    mov al, cl
    out dx, al      ; 读取的扇区数
    mov eax,esi     ; 恢复 ax

    ; 第二步:将LBA的地址存入0x1f3 ~ 0x1f6
    
    ;LBA地址7-0位写入端口0x1f3
    mov dx, 0x1f3
    out dx,al
    
    ;LBA地址15-8位写给端口0x1f4
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al
    
    ;LBA地址23-16位写给端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al
    
    shr eax,cl
    and al,0x0f  ; 1ba 第 24~27 位
    or al,0xe0 ;设置7-4位为1110,此时才是lBA模式
    mov dx,0x1f6
    out dx,al
    
    ;第三步 向0x1f7写入读命令
    mov dx,0x1f7
    mov al,0x20
    out dx,al
    
    ;第四步 检测硬盘状态
    .not_ready:
        nop
        in al,dx
        and al,0x88; 4位为1,表示可以传输,7位为1表示硬盘忙
        cmp al,0x08
        jnz .not_ready
    
        ;第 5 步:从0x1f0端口读数据
        mov ax,di
        mov dx, 256
        mul dx
        mov cx,ax
        mov dx,0x1f0
    
    .go_on:
       in ax,dx
       mov [bx],ax
       add bx,2
       loop .go_on
       ret
    
    times 510 - ($-$$) db 0
    dw 0xaa55

代码解释:

用于从硬盘读取一个扇区的数据,并将其加载到内存中的指定位置后跳转执行。

LOADER_BASE_ADDR 定义了 loader 在内存中的位置, MBR 要把 loader 从硬盘读入后放到此处。 如前所述,它的值是 0x900,说明将来 loader 会在内存地址 0x900 处。

LOADER_START_SECTOR定义了loader在硬盘上的逻辑扇区地址,即LBA地址。 等于0x2,说明loader放在了第2块扇区。

;将loader放入0x900
LOADER_BASE_ADDR    equ 0x900

;表示已LBA方式,我们的loader在第2块扇区
LOADER_START_SECTOR equ 0x2

 

指定 MBR 的代码段起始地址为 0x7c00。

SECTION MBR vstart=0x7c00

前面说过$属于“隐式地”藏在本行代码前的标号,也就是编译器给当前行安排的地址,$$指代本 section 的起始地址,此地址同样是编译器给安排的。section含有vstart=0x7c00,故该节中的数据地址以0x7c00为起始编址。 此0x7c00便是虚拟的地址。

$$在编译后被替换为vstart的值0x7c00, $$以该节的虚拟起始地址为主。

 

设置段和栈:
使用 mov 指令将各个段寄存器(ds, es, ss, fs, gs)设置为当前代码段 cs。
将栈指针 sp 设置为 0x7c00,这是标准的引导扇区加载地址。

SECTION MBR vstart=0x7c00
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00
    mov ax,0xb800
    mov gs,ax

 

清屏:
使用 int 10h 中断调用来清屏,使用 mov 指令设置寄存器来配置清屏参数。

;清屏
    mov ax, 0600h
    mov bx, 0700h
    mov cx,0
    mov dx,184fh ;(80,25)
    
    int 10h

 

在屏幕上输出 “MBR”:
将字符 ‘1’、空格、’M’、’B’、’R’ 依次写入屏幕内存地址 0xb800:0x00 处,使用 mov byte 指令完成。

;输出当前我们在MBR
    mov byte [gs:0x00], '1'
    mov byte [gs:0x01], 0x94
    
    mov byte [gs:0x02], ' '
    mov byte [gs:0x03], 0x94
    
    mov byte [gs:0x04], 'M'
    mov byte [gs:0x05], 0x94
    
    
    mov byte [gs:0x06], 'B'
    mov byte [gs:0x07], 0x94
    
    mov byte [gs:0x08], 'R'
    mov byte [gs:0x09], 0x94

 

从硬盘读取 loader 到内存:

汇编语言能够直接操作寄存器,所以其传递参数可以直接存储在寄存器,也可以用栈。 如果需要用到某个正在使用中的寄存器,只要提前把该寄存器备份好就行了,如备份到其他寄存器或压入栈中。 此函数需要三个参数,我们选择用 eax、bx、cx 寄存器来传递参数。

这里补充一下:

  • eax 是 ax 的扩展,即 eax 包含了 ax 中的所有内容,并在高位添加了额外的16位数据,使其成为32位寄存器。
  • 当你在操作中使用 ax 时,实际上也在影响 eax 中对应的部分,因为 ax 是 eax 的一部分。

将要读取的扇区号(LBA格式)放入 eax,加载到的内存地址放入 ebx,读取的扇区数放入 ecx,

我们要读一个简单的 loader,其大小肯定不会超过 512 字节,所以此处读入的扇区数置为 1 即可。

用寄存器bx来指定数据从硬盘读进来后放在内存中的位置。 在这里,bx寄存器值为LOADER BASE ADDR,即0x900。

调用 rd_disk 过程来实际执行硬盘读取操作。

mov eax,LOADER_START_SECTOR ;LBA 读入的扇区
mov bx,LOADER_BASE_ADDR    ;写入的地址
mov cx,1                ;等待读入的扇区数
call rd_disk                ;以下读取程序的起始部分(一个扇区)

jmp LOADER_BASE_ADDR       ;调到实际的物理内存

 

rd_disk 过程:
这部分代码通过硬盘控制器端口(如0x1f2, 0x1f3等)进行 LBA 模式的数据读取操作,具体细节包括设置读取的扇区数、LBA 地址、发送读命令等。具体实现了硬盘读取的各个步骤。

rd_disk:
    ; eax为LBA的扇区号
    ; bx为数据写入的内存地址
    ; cx为读入的扇区数
    
    mov esi, eax     ; 备份eax到esi
    mov di, cx       ; 备份cx到di
    
    ; 读写硬盘
    ; 第一步:设置要读取的扇区数
    mov dx, 0x1f2    ; 设置端口号为0x1f2
    mov al, cl       ; 将要读取的扇区数加载到al寄存器
    out dx, al       ; 将al寄存器的值输出到端口0x1f2,设置要读取的扇区数
    mov eax, esi     ; 恢复eax的值

    ; 第二步:将LBA地址存入0x1f3 ~ 0x1f6
    ; 将LBA地址的低8位写入端口0x1f3
    mov dx, 0x1f3
    out dx, al
    
    ; 将LBA地址的8~15位写入端口0x1f4
    mov cl, 8
    shr eax, cl
    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
    and al, 0x0f     ; 清除高位,保留低4位
    or al, 0xe0      ; 设置7-4位为1110,即设置为LBA模式
    mov dx, 0x1f6
    out dx, al
    
    ; 第三步:向0x1f7写入读命令
    mov dx, 0x1f7
    mov al, 0x20     ; 设置读取命令
    out dx, al
    
    ; 第四步:检测硬盘状态
    .not_ready:
        nop
        in al, dx       ; 从0x1f7端口读取状态到al寄存器
        and al, 0x88    ; 检查第4位和第7位,若为1则硬盘可以传输,第7位为1表示硬盘忙
        cmp al, 0x08    ; 检查状态是否为0x08
        jnz .not_ready  ; 若状态不为0x08,则继续等待
    
    ; 第五步:从0x1f0端口读取数据
    mov ax, di        ; ax = di
    mov dx, 256       ; dx = 256
    mul dx             ; ax = ax * dx
    mov cx, ax        ; cx = ax
    mov dx, 0x1f0     ; dx = 0x1f0
    
    .go_on:
       in ax, dx       ; 从0x1f0端口读取数据到ax寄存器
       mov [bx], ax    ; 将读取的数据写入内存地址bx
       add bx, 2       ; bx = bx + 2,即移动到下一个字
       loop .go_on     ; 循环读取数据,直到读取完指定的扇区数
    
    ret                ; 返回

 

备份读取的扇区数到di寄存器, di寄存器是16位的,和cx大小一致。 cx的值会在读取数据时用到,所以在此提前备份。

rd_disk:
    ;eax LBA的扇区号
    ;bx 数据写入的内存地址
    ;cx 读入的扇区数
    
    mov esi,eax       ;备份eax
    mov di,cx     ;备份cx

 

按照我们操作硬盘的约定,先选定一个通道,再往sector count寄存器中写扇区数。 往端口中写入数据用out指令,注意out指令中dx寄存器是用来存储端口号的。

我们的虚拟硬盘属于ata0,是Primary通道,所以其sector count寄存器是由0x1f2端口来访问的。

; 读写硬盘
; 第一步:设置要读取的扇区数
mov dx, 0x1f2    ; 设置端口号为0x1f2
mov al, cl       ; 将要读取的扇区数加载到al寄存器
out dx, al       ; 将al寄存器的值输出到端口0x1f2,设置要读取的扇区数
mov eax, esi     ; 恢复eax的值

 

将LBA地址写入三个LBA寄存器和device寄存器的低4位。 端口0x1f3是寄存器LBA low,端口0x1f4是寄存器LBA mid,端口0x1f5是寄存器LBA high。 shr指令是逻辑右移指令,这里主要通过此指令置换出地址的相应部分,写入相应的 LBA 寄存器。 “or al,0xe0”,用了 or指令和0xe0做或运算,拼出device寄存器的值。 高4位为e,即高4位的二进制表示为1110,其第5位和第7位固定为1,第6位为1表示启用LBA。

; 第二步:将LBA地址存入0x1f3 ~ 0x1f6
; 将LBA地址的低8位写入端口0x1f3
mov dx, 0x1f3
out dx, al

; 将LBA地址的8~15位写入端口0x1f4
mov cl, 8
shr eax, cl
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
and al, 0x0f     ; 清除高位,保留低4位
or al, 0xe0      ; 设置7-4位为1110,即设置为LBA模式
mov dx, 0x1f6
out dx, al

 

这里就是写入命令,因为我们这里是读操作,所以读扇区的命令是0x20。 通过out指令写入command端口0x1f7后,硬盘就开始工作了。

;第三步 向0x1f7写入读命令
mov dx,0x1f7
mov al,0x20
out dx,al

 

这里检测status寄存器的BSY位。由于status寄存器依然是0x1f7端口,所以不需要再为dx重新赋值。nop表示空操作,即什么也不做,只是为了增加延迟,相当于sleep了一小下, 目的是减少打扰硬盘的工作。对同一端口在读写两种操作时有不同的用途,在读硬盘时,此端口中的值是硬盘的工作状态。

将Status寄存器的值读入到al寄存器,通过and操作,保留第4位和第7位,第4位若为1,表示数据已经准备好,可以传输了。若第7位为1,表示硬盘现在正忙着。只要判断第4位是否为1就好了,用cmp指令和0x08做减法运算,判断第4位是否为1,cmp指令并不改变操作数的值,只是根据结果去设置标志位,从而我们根据标志位反着去判断结果。

cmp指令会影响的标志位有ZF,CF、PF等,这里咱们借助ZF位来判断cmp的结果。

具体来说,cmp 指令的语法如下:

cmp destination,source

其中,destination 和 source 可以是寄存器、内存地址或立即数。cmp 指令执行时,它会计算 destination – source 的结果,并根据计算结果设置条件标志位.

比如ZF (Zero Flag):如果结果为零,则置位(1),否则清零(0)。

于是用jnz .not_ready来判断结果是否不等于0,即若等于0,则status寄存器的第4位为1,这表示可以读数据了。若不等于 0,说明 status 寄存器的第 4 位为 0,表示硬盘正忙(此时 status 寄存器第 7 位肯定为 1)。.not_ready是个标号,于是跳回去继续判断硬盘状态,直到硬盘把数据准备好才跳出这个循环。

; 第四步:检测硬盘状态
.not_ready:
    nop
    in al, dx       ; 从0x1f7端口读取状态到al寄存器
    and al, 0x88    ; 检查第4位和第7位,若为1则硬盘可以传输,第7位为1表示硬盘忙
    cmp al, 0x08    ; 检查状态是否为0x08
    jnz .not_ready  ; 若状态不为0x08,则继续等待

 

这里完成从硬盘取数据的过程。 由于 data 寄存器是 16 位,即每次 in 操作只读入 2 字节,根据读入的数据总量(扇区数*512 字节)来求得执行 in指令的次数。

这里的乘法用mul指令,在实模式下,mul指令可以做8位乘法和16位乘法,

格式是: mul 操作数。

操作数可以是寄存器或内存。 乘法运算至少要有两个数参与才行,这里的操作数只是一个乘数,被乘数隐含在al或ax寄存器中。 如果操作数是8位,被乘数就是al寄存器的值,乘积就是16位,位于ax寄存器。 如果操作数是16位,被乘数就是ax寄存器的值,乘积就是32位,积的高16位在dx寄存器,积的低16位在ax寄存器。

我们进行的是 16 位的乘法,其结果是 32 位,但由于我知道这两个乘数 ax 的值和 dx 的值都不大,ax的实际的值其实是1,乘出来的这个结果,其高位是0,所以在第115行的”mov cx,ax”我们只将这个结果的低16位移入cx作为循环读取的次数。 此处用8位乘法不合适,因为256超过了8位寄存器表示的范围。

; 第五步:从0x1f0端口读取数据
mov ax, di        ; ax = di
mov dx, 256       ; dx = 256
mul dx            ; ax = ax * dx
mov cx, ax        ; cx = ax
mov dx, 0x1f0     ; dx = 0x1f0

 

这里通过循环来将数据写入bx寄存器指向的内存,每读入2个字节, bx所指的地址便+2。值得注意的是由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。待写入的地址超过bx的范围时,从硬盘上读出的数据会把0x0000~0xfff的覆盖,所以此处加载的程序不能超过64KB,即2的16次方等于65536,由于本mbr是用来加载loader的,所以loader.bin要小于64KB才行。

同样不能把bx改为ebx,在实模式下, CPU依然会用16位偏移地址。这是实模式下访问内存的规定与缺陷“段基址+段内偏移地址”,段内偏移地址正因为是16位,只能访问 64KB 的段空间,所以才将段基址乘以 16来突破这 64KB,从而实现访问低调1MB 空间的。

返回指令ret,它用来从函数中返回。 如果我们没有定义函数,就不需要它了。 函数和一般代码相比,就是在被调用时,CPU会将返回地址压到栈中,所以在函数体中,要用ret指令将栈中的返回地址重新加载到程序计数器中,如cs: ip,这样程序便恢复到之前的执行顺序了。

.go_on:
    in ax, dx       ; 从0x1f0端口读取数据到ax寄存器
    mov [bx], ax    ; 将读取的数据写入内存地址bx
    add bx, 2       ; bx = bx + 2,即移动到下一个字
    loop .go_on     ; 循环读取数据,直到读取完指定的扇区数

ret                ; 返回

 

然后程序便回到了这里,

这是个跳转的指令。 jmp指令和call指令是必不可少的,jmp表示一去不回头,call表示去了还回来。各有各的用途。这里是MBR交出接力棒的一刻,采用jmp是唯一合适的选择。Jmp的操作数是LOADER_BASE_ADDR,即0x900,这是要跳到内核加载器。

 

然后跳到Loader.asm

LOADER_BASE_ADDR    equ 0x900
section loader vstart=LOADER_BASE_ADDR

mov ax,0xb800 ;指向文本模式的显示缓冲区
mov es,ax

mov byte [es:0x00],'O'
mov byte [es:0x01],0x07
mov byte [es:0x02],'K'
mov byte [es:0x03],0x06

jmp $

 

编译查看效果

nasm E:\OS\mbr.asm -o E:\OS\mbr.bin
nasm E:\OS\Loader.asm -o E:\OS\Loader.bin
dd if=mbr.bin of=E:\\OS\\dingst.vhd bs=512 count=1
dd if=Loader.bin of=E:\\OS\\dingst.vhd bs=512 count=1 seek=2

 

为了方便,我们提取出公用的配置boot.inc

boot.inc 是我们的配置文件,关于加载器的配置信息就写在里面,今后还会在此添加更多的配置信息。

这是nasm提供的宏,和C语言中的宏是一回事,C语言中的宏是由#define指令来实现的。

nasm 中的语法是:宏名 equ 值

; 将loader放入0x900
LOADER_BASE_ADDR    equ 0x900

;表示已LBA方式,我们的loader在第2块扇区
LOADER_START_SECTOR equ 0x2

MBR和Loader引用它

 

我们整理一下目录结构

编译时-I添加一个包含文件的路径。

nasm -I E:/OS/common/ -o E:/OS/bin/loader.bin E:/OS/asm/loader.asm
nasm -I E:/OS/common/ -o E:/OS/bin/mbr.bin E:/OS/asm/mbr.asm
dd if=bin/mbr.bin of=E:\\OS\\dingst.vhd bs=512 count=1
dd if=bin/Loader.bin of=E:\\OS\\dingst.vhd bs=512 count=1 seek=2

 

3.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇