本文基于郑纲的《操作系统还原》,仅为个人学习笔记,前期的虚拟机配置等不再详细记录,其中不理解或者出错的地方还望提出意见!
硬盘
-
存储逻辑
为了更有效管理磁盘,将整个盘面划分为多个同心环,以圆心画扇形,扇形与每个同心环相交的弧状区域作为最基本的数据存储单元。这个同心环就称为磁道,而同心环上的弧状区域是扇形的一部分,故称之为扇区,它作为我们向硬盘存储数据的最基本单位,大小是 512 字节。我们写入的数据最终是写进了磁道上的扇区中。扇区是有自己的“头部”,头部之后的部分才是存储数据的 512 字节。头部中包含了扇区自身的信息:磁头号、磁道号和扇区号。
-
硬盘控制器端口
由表可知,端口用途在读硬盘和写硬盘时是有一点不同的。
1、读硬盘时,端口 0x171 或 0x1F1 的寄存器名字叫 Error 寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在 Sector count 寄存器中。在写硬盘时,此寄存器有了别的用途,所以有了新的名字,叫 Feature 寄存器。有些命令需要指定额外参数,这些参数就写在 Fea ture 寄存器中。
2、在读硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 Status,它是 8 位宽度的寄存器,用来给出硬盘的状态信息。在写硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 command。此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了
device 寄存器是个杂项,它的宽度是 8 位。在此寄存器的低 4 位用来存储 LBA 地址的第 24~27 位。结合上面的三个 LBA 寄存器。第 4 位用来指定通道上的主盘或从盘, 0 代表主盘, 1 代表从盘。第 6 位用来设置是否启用 LBA 方式, 1 代表启用 LBA 模式, 0 代表启用 CHS 模式。另外的两位:第 5 位和第 7 位是固定为 1 的,称为 MBS 位 -
硬盘操作方法
最主要的顺序就是 command 寄存器一定得是最后写,因为一旦 command 寄存器被写入后,硬盘就开始干活。
操作步骤:
( 1)先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
( 2)往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。
( 3)往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4
位,选择操作的硬盘( master 硬盘或 slave 硬盘)。
( 4)往该通道上的 command 寄存器写入操作命令。
( 5)读取该通道上的 status 寄存器,判断硬盘工作是否完成。
( 6)如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
( 7)将硬盘数据读出。
MBR操作硬盘
由于MBR只有512字节大小,无法为内核准备好环境,也无法将内核加载到内存并运行。于是通过单独的一个程序完成环境初始化以及内核的加载,即Loader,内核加载器。MBR的的任务则是负责将loader从硬盘上加载到内存中,并将接力棒交给它。
MBR 是占据了硬盘的第 0 扇区(以逻辑 LBA 方式,扇区从 0 开始编号,若是以物理 CHS 方式,扇区则从 1 开始编号),第 1 扇区是空闲的,可以用,但离得太近总感觉不如隔开一点心里踏实,所以把loader 放到第 2 扇区。 MBR 从第 2 扇区中把它读出。
loader 中要定义一些数据结构,如 GDT 全局描述符表,这些数据结构将来的内核还是要用的,所以 loader 加载到内存后不能被覆盖。随着不断添加功能,内核必然越来越大,其所在的内存地址也会向越来越高的地方发展,难免会超过可用区域的上限,咱们尽量把 loader 放在低处,多留出一些空间给内核,将 loader 的加载地址选为 0x900。
- 改造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 0x10
;jmp $ ; 使程序悬停在此
; 输出背景色绿色,前景色红色,并且跳动从字符串“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
mov eax,LOADER_START_SECTOR ; 起始扇区lba地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,4 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR + 0x300
;-----------------------------------------------------------------------------------
;功能:读取硬盘的n个扇区
rd_disk_m_16:
;-----------------------------------------------------------------------------------
; eax=LBA扇区号
; bx=将数据写入内存地址
; cx=读入的扇区数
mov esi,eax ; 备份eax
mov di,cx ; 备份cx
; 读写硬盘:
; 第一步:设置要读取的扇区数
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位写入端口0x1f6
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
; 第三步:向0x1f7端口写入命令,0x20
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 ; 若未准备好,继续等
; 第五步:从0x1f0端口读数据
; di为要读取的扇区数,一个扇区有512字节,每次读入一个字共需di*512/2次,所以di*256
mov ax,di
mov dx,256
mul dx
mov cx,ax
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
- 配置文件boot.inc
;----------------------- loader and kernel -----------------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
nasm 提供的宏, 和 C 语言中的宏是一回事。 只不过 nasm中的语法是:宏名 equ 值,
LOADER_BASE_ADDR 定义了 loader 在内存中的位置, MBR 要把 loader 从硬盘读入后放到此处。如前所述,它的值是 0x900,说明将来 loader 会在内存地址 0x900 处。
LOADER_START_SECTOR 定义了 loader 在硬盘上的逻辑扇区地址,即 LBA 地址。它等于 0x2,说明 loader 放在了第 2 块扇区。
内核加载器
loader 是要经过实模式到保护模式的过渡,并最终在保护模式下加载内核。这里只是一个简单版本。
%include "boot.inc"
section loader vstart=LOADER_BASE_ADD
; 输出背景色绿色,前景色红色,并且跳动的字符串"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 $ ; 通过死循环使程序悬停在此
实验结果
实验结果如上。