动手学操作系统(四、MBR读取硬盘加载Loader)

动手学操作系统(四、MBR读取硬盘加载Loader)

在上一节中,我们学习了使用MBR来直接控制显卡进行显示,在这一节中我们学习如何让MBR来操作硬盘,加载Loader来完成操作系统的后续启动过程。

0. 通用寄存器的介绍

介绍一些常用的寄存器,方便以后查找

Image

1. 为什么需要Lodaer?

我们的MBR受限于512字节的大小,在如此小的空间中,我们无法为操作系统的内核准备好完整的运行环境,更没办法将内核成功加载到内存并运行,所以我们需要另一个程序来完成初始化环境及加载内核的工作,这个程序就被称为Loader,简而言之就是,MBR负责从硬盘上把Loader加载到内存中,然后将控制权交给Loader,在Loader中要定义一些数据结构,这些数据结构在将来的内核中还是需要使用的,所以将Loader加载到内存中时不能进行覆盖。我们尽量将Loader放置在低地址处,多留一些空间给内核。

2. 硬盘介绍

硬盘属于存储介质,在硬盘的发展历史中,随机存取具有划时代的意义,程序中的算法不用再考虑存储时间,访问任意数据所用的时间几乎是相等的,这一改之前的存储设备其存取时间呈线性的历史。

为了让硬盘工作,我们需要通过读写硬盘控制器的端口,端口就是位于IO控制器上的寄存器,这里就算指的是硬盘控制器上的寄存器。硬盘的控制遵循标准ATA(Advanced Technology Attachment),ATA的标准有些冗长,这里只简单介绍我们需要使用到的部分,如下图所示

Image

3. MBR读取硬盘加载Loader

由于MBR占据了硬盘的第0扇区,则第1扇区是空闲的,为了安全性,间隔1个扇区,将Loader放置在第2扇区,参考实模式下的1MB内存布局,

Image

0x500~0x7BFF0x7E00~0x9FBFF这两段内存区域都是可以使用的,为了给内核留足够的空间,这里将Loader加载到内存地址的0x900处(当然,你也可以放置在别处)。

接下来就编写程序来进行加载,在编写程序之前,我们先优化一下我们的工程路径结构,将工程路径优化为如下,在src路径下新增了一个boot目录用于管理我们boot过程中的代码文件,然后在boot目录中增加一个lib目录用于管理我的一些配置文件比如boot.inc,然后新建一个loader.S用于编写Loader的代码,bin目录用于存放编译的结果,整个工程的目录如下所示,当然你也可以按照你喜欢的方式来进行组织

Image

分别写入以下的内容

mbr.S

; ~/d2los/src/boot/mbr.S
; 主引导程序 
; LOADER_BASE_ADDR equ 0xA000 
; LOADER_START_SECTOR equ 0x2
;------------------------------------------------------------
%include "boot.inc"
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

; 清屏
; 利用0x06号功能,上卷全部行,可以清屏。
; -----------------------------------------------------------
;INT 0x10   功能号:0x06    功能描述:上卷窗口
; ------------------------------------------------------
; 输入:
; AH 功能号= 0x06
; AL = 上卷的行数(如果为0,表示全部)
; BH = 上卷行属性
; (CL,CH) = 窗口左上角的(X,Y)位置
; (DL,DH) = 窗口右下角的(X,Y)位置
; 无返回值:
   mov     ax, 0600h
   mov     bx, 0700h
   mov     cx, 0                   ; 左上角: (0, 0)
   mov     dx, 184fh               ; 右下角: (80, 25),
                                   ; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
                                   ; 下标从0开始,所以0x18=24,0x4f=79
   int     10h                     ; int 10h

   ; 输出字符串: Hello World
   mov byte [gs:0x00],'H'
   mov byte [gs:0x01],0xA4     ; A表示绿色背景,4表示前景色为红色
   mov byte [gs:0x02],'e'
   mov byte [gs:0x03],0xA4
   mov byte [gs:0x04],'l'
   mov byte [gs:0x05],0xA4
   mov byte [gs:0x06],'l'
   mov byte [gs:0x07],0xA4
   mov byte [gs:0x08],'o'
   mov byte [gs:0x09],0xA4

   mov byte [gs:0x0A],' '
   mov byte [gs:0x0B],0xA4

   mov byte [gs:0x0C],'W'
   mov byte [gs:0x0D],0xA4
   mov byte [gs:0x0E],'o'
   mov byte [gs:0x0F],0xA4
   mov byte [gs:0x10],'r'
   mov byte [gs:0x11],0xA4
   mov byte [gs:0x12],'l'
   mov byte [gs:0x13],0xA4
   mov byte [gs:0x14],'d'
   mov byte [gs:0x15],0xA4
     
   mov eax,LOADER_START_SECTOR  ; 起始扇区LBA地址
   mov bx,LOADER_BASE_ADDR       ; 读入的地址
   mov cx,1                      ; 读取的扇区数
   call rd_disk_m_16             ; 读取程序的起始部分(一个扇区)
  
   jmp LOADER_BASE_ADDR
       
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:    
;-------------------------------------------------------------------------------
                                   ; eax=LBA扇区号
                                   ; ebx=将数据读入的内存地址
                                   ; ecx=读取的扇区数
      mov esi,eax    ;保存eax
      mov di,cx      ;保存cx
;读入硬盘:
;1:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi   ;恢复ax

;2:将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   ;lba第24~27位
      or al,0xe0   ; 设置74位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;3:0x1f7端口写入读取命令0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;4:检查硬盘状态
  .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   ; di为要读取的扇区数,一个扇区有512字节,每次读取一个字,
                   ; 共需di*512/2次,所以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [bx],ax
      add bx,2     
      loop .go_on_read
      ret
   
   times 510-($-$$) db 0   ; 剩余部分填充0
   db 0x55,0xaa

boot.inc

; ~/d2los/src/boot/lib/boot.inc
;-------------	 loader和kernel   ----------
LOADER_BASE_ADDR equ 0x900 
LOADER_START_SECTOR equ 0x2

loader.S

; ~/d2los/src/boot/loader.S
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

; 清屏
; 利用0x06号功能,上卷全部行,可以清屏。
; -----------------------------------------------------------
;INT 0x10   功能号:0x06    功能描述:上卷窗口
; ------------------------------------------------------
; 输入:
; AH 功能号= 0x06
; AL = 上卷的行数(如果为0,表示全部)
; BH = 上卷行属性
; (CL,CH) = 窗口左上角的(X,Y)位置
; (DL,DH) = 窗口右下角的(X,Y)位置
; 无返回值:
   mov     ax, 0600h
   mov     bx, 0700h
   mov     cx, 0                   ; 左上角: (0, 0)
   mov     dx, 184fh               ; 右下角: (80, 25),
                                   ; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
                                   ; 下标从0开始,所以0x18=24,0x4f=79
   int     10h                     ; int 10h
   
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'H'
mov byte [gs:0x01],0xA4     ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],'i'
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],' '
mov byte [gs:0x05],0xA4   
mov byte [gs:0x06],'L'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'o'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0a],'a'
mov byte [gs:0x0b],0xA4
mov byte [gs:0x0c],'d'
mov byte [gs:0x0d],0xA4
mov byte [gs:0x0e],'e'
mov byte [gs:0x0f],0xA4
mov byte [gs:0x10],'r'
mov byte [gs:0x11],0xA4

jmp $		       ; 通过死循环使程序悬停在此

然后编译

cd ~/d2los
nasm -I src/boot/lib -o bin/mbr.bin src/boot/mbr.S
nasm -I src/boot/lib -o bin/loader.bin src/boot/loader.S

这里关键-I的作用是,添加一个包含文件的目录,其实就是添加你的库目录。

然后装载到硬盘

cd ~/d2los
dd if=./bin/mbr.bin of=~/bochs/bin/hardisk60MB.img bs=512 count=1 seek=0 conv=notrunc
dd if=./bin/loader.bin of=/home/sjh/bochs/bin/hardisk60MB.img bs=512 count=2 seek=1 conv=notrunc

这里的关键字解释如下:

bs=512 指定块大小为 512 字节(一个扇区大小)。
count=1 仅复制一个块(一个扇区)。
seek=0 从输出文件的开头(第一个扇区)开始写入。
conv=notrunc 保持文件大小不变。

bs=512 指定块大小为 512 字节。
count=2 复制两个块(两个扇区)。
seek=1 从输出文件的第二个扇区开始写入(第一个扇区之后)。

正如我们之前所说的,我们将编写好的Loader代码装载到硬盘的第二扇区,模拟其一直存储在硬盘中,然后使用MBR读取硬盘加载Loader,从而将控制权交给Loader,让Loader继续完成其功能。

然后运行

~/bochs/bin/bochs -f ~/bochs/bin/bochsrc.disk 

如果写入有误,可以使用命令

dd if=/dev/zero of=~/bochs/bin/hardisk60MB.img bs=1M count=60

来清空虚拟硬盘。

为了方便起见,我们可以将上述的命令写成.sh文件,方便以后直接使用

command_compile.sh

nasm -I src/boot/lib -o bin/mbr.bin src/boot/mbr.S
nasm -I src/boot/lib -o bin/loader.bin src/boot/loader.S
dd if=./bin/mbr.bin of=/home/sjh/bochs/bin/hardisk60MB.img bs=512 count=1 seek=0 conv=notrunc
dd if=./bin/loader.bin of=/home/sjh/bochs/bin/hardisk60MB.img bs=512 count=2 seek=1 conv=notrunc

command_run.sh

~/bochs/bin/bochs -f ~/bochs/bin/bochsrc.disk 

效果如下

Image

然后我们详细解释一下代码,在boot.inc文件中,我们定义了两个宏分别是LOADER_BASE_ADDRLOADER_START_SECTORequ是用于定义宏的关键字类似于C语言中的#define

Reference

[1]《一个64位操作系统的设计与实现》
[2]《操作系统真象还原》

  • 28
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值