动手学操作系统(四、MBR读取硬盘加载Loader)
在上一节中,我们学习了使用MBR来直接控制显卡进行显示,在这一节中我们学习如何让MBR来操作硬盘,加载Loader来完成操作系统的后续启动过程。
文章目录
- 动手学操作系统(四、MBR读取硬盘加载Loader)
- 1. 为什么需要Lodaer?
- 2. 硬盘介绍
- 3. MBR读取硬盘加载Loader
- Reference
1. 为什么需要Lodaer?
我们的MBR受限于512字节的大小,在如此小的空间中,我们无法为操作系统的内核准备好完整的运行环境,更没办法将内核成功加载到内存并运行,所以我们需要另一个程序来完成初始化环境及加载内核的工作,这个程序就被称为Loader,简而言之就是,MBR负责从硬盘上把Loader加载到内存中,然后将控制权交给Loader,在Loader中要定义一些数据结构,这些数据结构在将来的内核中还是需要使用的,所以将Loader加载到内存中时不能进行覆盖。我们尽量将Loader放置在低地址处,多留一些空间给内核。
2. 硬盘介绍
硬盘属于存储介质,在硬盘的发展历史中,随机存取具有划时代的意义,程序中的算法不用再考虑存储时间,访问任意数据所用的时间几乎是相等的,这一改之前的存储设备其存取时间呈线性的历史。
为了让硬盘工作,我们需要通过读写硬盘控制器的端口,端口就是位于IO控制器上的寄存器,这里就算指的是硬盘控制器上的寄存器。硬盘的控制遵循标准ATA(Advanced Technology Attachment),ATA的标准有些冗长,这里只简单介绍我们需要使用到的部分,如下图所示
3. MBR读取硬盘加载Loader
由于MBR占据了硬盘的第0扇区,则第1扇区是空闲的,为了安全性,间隔1个扇区,将Loader防止子地2扇区,参考实模式下的内存布局,
0x500~0x7BFF
和0x7E00~0x9FBFF
这两段内存区域都是可以使用的,为了给内核留足够的空间,这里将Loader加载到内存地址的0x900
处(当然,你也可以放置在别处)。
接下来就编写程序来进行加载,在编写程序之前,我们先优化一下我们的工程路径结构,将工程路径优化为如下,在src
路径下新增了一个boot
目录用于管理我们boot过程中的代码文件,然后在boot
目录中增加一个lib
目录用于管理我的一些配置文件比如boot.inc
,然后新建一个loader.S
用于编写Loader的代码,bin
目录用于存放编译的结果,整个工程的目录如下所示,当然你也可以按照你喜欢的方式来进行组织
分别写入以下的内容
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 ; 设置7至4位为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
然后装载到硬盘
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=1 seek=2 conv=notrunc
这里的关键字解释如下:
-
bs=512
: 设置块大小(block size)为512字节。这意味着 dd 命令在处理数据时会以512字节为单位进行读写。 -
count=1
: 指定要读取和写入的块的数量为1。这意味着 dd 命令会读取1个512字节的块并将其写入输出文件。 -
seek=2
: 在输出文件中跳过2个块(每个块大小为 bs 设置的512字节),然后开始写入。就是将loader装载自硬盘的第二个扇区,如果seek=0
就是将文件装载到第0个扇区
然后运行
~/bochs/bin/bochs -f ~/bochs/bin/bochsrc.disk
如果写入有误,可以使用命令
dd if=/dev/zero of=~/bochs/bin/hardisk60MB.img bs=1M count=60
来清空虚拟硬盘。
效果如下
然后我们详细解释一下代码,在boot.inc
文件中,我们定义了两个宏分别是LOADER_BASE_ADDR
和LOADER_START_SECTOR
,equ
是用于定义宏的关键字类似于C语言中的#define
,
Reference
[1]《一个64位操作系统的设计与实现》
[2]《操作系统真象还原》