第三章 MBR --> loader
第三章 MBR --> loader
本文是对《操作系统真象还原》第三章学习的笔记,欢迎大家一起交流。
a
知识介绍
在上一章的代码部分,我们通过 BIOS 中断进行字符输出,但是离开实模式之后,BIOS 中断就没法用了,因为 BIOS 中断向量表只在实模式下存在,因此我们肯定还会有别的方法来输出字符串,这就是通过显存输出。
能这样的原理是因为显卡的内存已经编排到了 cpu 能够寻址的范围之内,当 cpu 操作这部分“内存”时,实际上是直接在和显卡打交道。显卡拿到了数据处理之后,显示器最终会按照要求显示这些数据。内存中的显存映射的地址范围如下:
从起始地址 0xB8000 到 0xBFFFF,这片 32KB 大小的内存区域是用于文本显示。我们往 0xB8000 处输出的字符直接会落到显存中,显存中有了数据,自然显卡就将其搬到显示器屏幕上了,这后续的事情咱们是不需要处理的,咱们只要保证写进显存的数据是正确的就可以。
显示器上每个字符占两字节,低字节是字符 ASCII 码,高字节是用来控制颜色的。高字节低 4 位是字符前景色,也就是字符的颜色(RGB 是红蓝绿三种颜色的调和,I 位表示是否高亮),高字节高 4 位是字符的背景色(RGB 是红蓝绿三种颜色的调和,K 位控制是否闪烁)。所以我们向显卡的对应内存操作时,也应按照如下格式:
高八位的颜色组合如下:
在写代码之前再补充一个高端内存的小知识,内存只有 0--0xffff,但是可以表示到 0x10ffef,这是因为 0xFFFF*16+0xFFFF=0xFFFF0+0xFFFF
代码部分
;主引导程序
;
;LOADER_BASE_ADDR equ 0xA000
;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
; 清屏
;利用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],'1'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
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
jmp $ ; 通过死循环使程序悬停在此
times 510-($-$$) db 0
db 0x55,0xaa
初始化以及清屏的工作和第二章一样的,这里不再赘述,我们直接看操作显存部分。
13-14 行将 gs 寄存器初始化为 0xb800,方便后面直接向显存写字符
17-50 行就是向显存写字符的过程,以两个字节为一个单位,第八位是字符,高八位是控制颜色和背景色,我们这里统一写入 0xA4
,即 1010,0100
,结合上面颜色组合的表格(注意小端格式)可知,前景色为红色,绿色背景闪烁。
效果如下,我这里把字符串改了一下:
b
知识介绍
好了,接下来我们要用 MBR 做点实事了,MBR 只有 510B,能做的事情非常少,所以不能指望它做完所有事情。所以,我们用它把操作系统的 loader 加载到指定位置,然后跳转到 loader 执行,loader 由于大小可以比 MBR 大得多,所以能做的就很多了。
所以,MBR 要加载 loader,就必须要和磁盘打交道。打交道的方式很简单,就是通过 in 与 out 指令与磁盘暴露在外的寄存器交互。下图是 in 与 out 指令的用法:
磁盘端口寄存器对应的用途(详细请见书 p127):
其中 Status 与 Device 寄存器比较复杂,它们的结构的含义如下:
在写硬盘时 Status 就变成了 command 寄存器,这和 408 学的东西也一致,这里主要用到三个命令:
(1)identify:0xEC,即硬盘识别。
(2)read sector:0x20,即读扇区。
(3)write sector:0x30,即写扇区。
接下来我们来实现代码,实现 mbr 从硬盘读取 loader,然后跳转执行 loader。
首先约定一下读取硬盘的过程,好让我们做到心中有数:
(1)先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
(2)往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。
(3)往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4 位,选择操作的硬盘(master 硬盘或 slave 硬盘)。
(4)往该通道上的 command 寄存器写入操作命令。
(5)读取该通道上的 status 寄存器,判断硬盘工作是否完成。
(6)如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
(7)将硬盘数据读出。
读出之后写入内存,然后跳转执行即可,但是在此之前我们还需要解决两个问题,loader 写到硬盘的哪里,loader 写入内存的哪里。
loader 写到硬盘的哪里
由于 MBR 是占据了硬盘的第 0 扇区(以逻辑 LBA 方式,扇区从 0 开始编号,若是以物理 CHS 方式,扇区则从 1 开始编号),第 1 扇区是空闲的,可以用,但离得太近总感觉不如隔开一点心里踏实,所以把 loader 放到第 2 扇区。MBR 从第 2 扇区中把它读出来。
loader 写入内存的哪里
读出来放到哪里呢?原则上是找个空闲地方就行了,在实模式下的内存布局表格中查看下,只要在“用途”列中注明“可用区域”的地方都可以用。0x500~0x7BFF 和 0x7E00~9FBFF 这两段内存区域都可以。
首先,loader 中要定义一些数据结构(如 GDT 全局描述符表,不懂没关系,以后会说),这些数据结构将来的内核还是要用的,所以 loader 加载到内存后不能被覆盖。
其次,随着咱们不断添加功能,内核必然越来越大,其所在的内存地址也会向越来越高的地方发展,难免会超过可用区域的上限,咱们尽量把 loader 放在低处,多留出一些空间给内核。
所以,作者将 loader 的加载地址选为 0x900。为什么不是 0x500,这个多省空间?还是预留出一定空间吧,彼此隔开远一点心里才踏实,不差这点空间了,哈哈,完全是作者的偏好,大家随意啦。
代码部分
ok,下面上代码
MBR
;主引导程序
;------------------------------------------------------------
%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
; 输出字符串:MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁,4表示前景色为红色
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 ; 起始扇区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
db 0x55,0xaa
第一行 %include "boot.inc"
,我们在 include/boot.inc
定义了起始扇区 lba 地址以及写入内存的地址
;------------- loader和kernel ----------
LOADER_BASE_ADDR equ 0x900 ;写入内存的地址
LOADER_START_SECTOR equ 0x2 ;起始扇区lba地址
4--52 行和之前实验一样的部分我们先跳过不看,重点看读硬盘的过程,在 call 读取硬盘之前,先准备参数
然后再函数的开始,再次对这些重要的参数备份
然后就是按照我们上面的约定,一步一步从磁盘中读出数据,首先是先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
第二步,写 LBA 地址以及 device 寄存器,这里按照上面说的写即可,其中我们获取 LBA 各个部分的地址是通过右移 eax 获得的
第三步,往该通道上的 command 寄存器写入操作命令。
;第 4 步:检测硬盘状态,直到磁盘准备好了再继续,这里的 nop 相当于一个时间很短的 sleep
第五步:读取该通道上的 status 寄存器,判断硬盘工作是否完成。每次只能读两个字节,所以一个扇区要读 256 次,256*读取的扇区数即读取的总次数,读取之后写到内存中,不断循环即可,读完直接返回。
调用完函数之后直接 jmp 到 loader 即可
Loader
我们这里先写一个简单的 loader 验证跳转即可,在屏幕上显示 2 Loader
,代码很简单,没什么要说的
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4
mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4
mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4
jmp $ ; 通过死循环使程序悬停在此
但是我们这里的编译命令有变化,要加一个 -I
参数用于指定头文件位置,在用 dd 命令写入硬盘时也要加上 seek 命令用来写到 2 号扇区(0开始计算的情况下)
nasm -I include/ -o mbr.bin mbr.s
dd if=./mbr.bin of=../../../hd60M.img bs=512 count=1 conv=notrunc
nasm -I include/ -o loader.bin loader.s
dd if=./loader.bin of=../../../hd60M.img bs=512 count=1 seek=2 conv=notrunc
最后效果如下: