通过 【自制操作系统01】硬核讲解计算机的启动过程 和 【自制操作系统02】环境准备与启动区实现 的讲解,我们已经实现了一个最简单的操作系统(仅仅一条机器指令)。
今天我们要再往前进一步,逐渐将这个最简单的操作系统完善起来。之前最简单的操作系统是写在启动区的 512 字节里,这么小的空间以后肯定不能全部用来写操作系统的代码,所以它的主要任务就是将硬盘中更多的数据读取到内存里,并跳转到内存的那个位置开始运行。
这里不得不回顾一下每节课都说到的四次跳跃:
- 一跳:按下开机键,CPU 将 PC 寄存器的值强制初始化为 0xffff0,这个位置是 BIOS 程序的入口地址
- 二跳:该入口地址处是一个跳转指令,跳转到 0xfe05b 位置,开始执行
- 三跳:执行了一些硬件检测工作后,最后一步将启动区内容加载(复制)到内存 0x7c00,并跳转到这里
- 四跳:启动区代码主要是加载操作系统内核,并跳转到加载处
其实我们可以无限跳跃下去,只要感觉某一个环节的任务复杂了,就可以分成两步来走。但也完全可以从第三跳开始就再也不跳转了,把所有操作系统需要的指令和数据都从硬盘中加载到内存,然后执行,但这样显然不好。
一、代码总览
搜索小说 198200.com先不说别的,先发上来一份本章内容的全部代码
mbr.asm
;----BIOS把启动区加载到内存的该位置,所以需设置地址偏移量
section mbr vstart=0x7c00
;----设置堆栈地址
mov sp,0x7c00
;----卷屏中断,目的是清屏
mov ax,0x0600
mov bx,0x0700
mov cx,0
mov dx,0x184f
int 0x10
;----直接往显存中写数据
mov ax,0xb800
mov gs,ax
mov byte [gs:0x00],'m'
mov byte [gs:0x02],'b'
mov byte [gs:0x04],'r'
;----读取硬盘(第2扇区)并加载到内存(0x900)
mov eax,0x02 ;起始扇区lba地址,LBA=(柱面号*磁头数+磁头号)*扇区数+扇区编号-1
mov bx,0x900 ;写入的内存地址,之后用
mov cx,4 ;待读入的扇区数
call read_disk
jmp 0x900
;----读硬盘方法,eax为lba扇区号,bx为待写入内存地址,cx为读入的扇区数
read_disk:
mov esi,eax ;备份
mov di,cx ;备份
;第一步,设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al
mov eax,esi ;恢复
;第二步,设置LBA地址
mov cl,8
;0-7位写入0x1f3
mov dx,0x1f3
out dx,al
;8-15位写入0x1f4
mov dx,0x1f4
shr eax,cl
out dx,al
;16-23位写入0x1f5
mov dx,0x1f5
shr eax,cl
out dx,al
;24-27位写入0x1f6
mov dx,0x1f6
shr eax,cl
and al,0x0f ;lba的24-27位
or al,0xe0 ;另外4位为1110,表示lba模式
out dx,al
;第三步,写入读命令
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
;第五步,读数据
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
;----512字节的最后两字节是启动区标识
times 510-($-$$) db 0
db 0x55,0xaa
loader.asm
section loader vstart=0x900
mov byte [gs:0xa0],'l'
mov byte [gs:0xa2],'o'
mov byte [gs:0xa4],'a'
mov byte [gs:0xa6],'d'
mov byte [gs:0xa8],'e'
mov byte [gs:0xaa],'r'
Makefile
mbr.bin: mbr.asm
nasm -I include/ -o out/mbr.bin mbr.asm -l out/mbr.lst
loader.bin: loader.asm
nasm -I include/ -o out/loader.bin loader.asm -l out/loader.lst
os.raw: mbr.bin loader.bin
../bochs/bin/bximage -hd -mode="flat" -size=60 -q target/os.raw
dd if=out/mbr.bin of=target/os.raw bs=512 count=1
dd if=out/loader.bin of=target/os.raw bs=512 count=4 seek=2
brun:
make install
make only-bochs-run
only-bochs-run:
../bochs/bin/bochs -f ../bochs/bochsrc.disk -q
install:
make clean
make -r os.raw
clean:
rm -rf target/*
rm -rf out/*
二、磁盘
如果你粗略地读了一下代码,起码可以知道 mbr.asm 中的代码,前半部分是在屏幕上输出一个 mbr 字符串,这是上节课为了做最小操作系统而用直观方式写的代码,可有可无。后半部分仅仅是读取了几个扇区的硬盘数据,加载到内存中的某个位置,然后跳转到此位置,这部分是关键,也是 mbr 的职责所在。
那怎么读取硬盘中的数据呢,这就要从磁盘的结构说起。硬件的东西并不是很懂,所以也只能说个大概。硬盘属于磁盘的一种,磁盘分为硬盘和软盘。但他们的逻辑结构是一样的:
盘片(platter)
磁头(head)
磁道(track)
扇区(sector)
柱面(cylinder)
我不想管它怎么动的,我只需要想明白,确定一个磁头、柱面、扇区,就确定了一个 512 字节大小的区域,这就够了。这也就是硬盘的 CHS 表示法,即 Cylinder(柱面)、Head(磁头)、Sector(扇区),只要知道了硬盘的 CHS 的数目,即可确定硬盘的容量,硬盘的容量 = 柱面数 × 磁头数 × 扇区数 × 512B。
如果不考虑这个物理结构,其实硬盘就是 n 多个 512 字节的区域构成的,我们完全可以从 0 开始编号,每 512 字节加一,这样就可以完全不用考虑什么扇区啦,柱面啦,这种是我比较喜欢的(看来还是软件工程师思想呀),这种方式叫做 LBA 表示法。
LBA = (柱面号 * 磁头数 + 磁头号) * 扇区数 + 扇区编号 - 1
所以 CPU 要和硬盘打交道,要么用这个 CHS 表示法,就至少要告诉硬盘柱面、磁头、扇区号是多少,要么用 LBA 表示法告诉硬盘一个 LBA 号码,然后再给硬盘一个是读还是写的信号。硬盘制作厂商千千万,CPU制作厂商也是各不相同,自然就会想到一定有一个硬盘接口标准,这个标准就叫做 ATA 标准,也可以俗称为 IDE 硬盘接口技术标准。这个标准可以下载 AT_Attachment_with_Packet_Interface 共三册的内容,但我们用不到那么多,我这里找到了一个还算原汁原味的中文版的论文 《IDE接口硬盘读写技术》 ,看这个基本就够用了。
三、IDE硬盘接口技术
CPU 与外设是通过 IO 接口交互的,所以最核心的就是这个技术标准定义的 IO 接口都有哪些,分别有什么作用
I/O地址 | 读(主机从硬盘读数据) | 写(主机数据写入硬盘) |
---|---|---|
1F0H | 数据寄存器 | 数据寄存器 |
1F1H | 错误寄存器(只读寄存器) | 特征寄存器 |
1F2H | 扇区计数寄存器 | 扇区计数寄存器 |
1F3H | 扇区号寄存器或 LBA 块地址 0~7 | 扇区号或 LBA 块地址 0~7 |
1F4H | 磁道数低 8 位或 LBA 块地址 8~15 | 磁道数低 8 位或 LBA 块地址 8~15 |
1F5H | 磁道数高 8 位或 LBA 块地址 16~23 | 磁道数高 8 位或 LBA 块地址 16~23 |
1F6H | 驱动器/磁头或 LBA 块地址 24~27 | 驱动器/磁头或 LBA 块地址 24~27 |
1F7H | 命令寄存器或状态寄存器 | 命令寄存器 |
所以如果要写一个程序来读文件的话,不难分析出整个过程就是:
- 在 1F2H 写入要读取的扇区数
- 在 1F3H ~ 1F6H 这四个端口写入计算好的起始 LBA 地址
- 在 1F7H 处写入读命令的指令号
- 不断检测 1F7H (此时已成为状态寄存器的含义)的忙位
- 如果第四步骤为不忙,则开始不断从 1F0H 出读取数据到内存指定位置,知道读完
这五步刚刚好对应着上面的代码
最后,别忘了我们这些代码仍然是要加载到启动区的,所以最后两个字节依然要是启动区标识符 0x55 0xaa
四、运行代码
写好了 mbr.asm,我们再写一个 loader.asm,设置其起始地址为 0x900(因为读写磁盘后存入的内存位置就是这个,这是我们自己定义的),并把它放在磁盘的第二扇区(这也是我们自己定的,只要和读盘的代码保持一致就行)
#### loader.asm
section loader vstart=0x900
mov byte [gs:0xa0],'l'
mov byte [gs:0xa2],'o'
mov byte [gs:0xa4],'a'
mov byte [gs:0xa6],'d'
mov byte [gs:0xa8],'e'
mov byte [gs:0xaa],'r'
剩下的精华就在于我们的 Makefile 文件了,可以参考下上面的代码
执行 make brun,可以看到如下效果,说明加载磁盘中的 loader 代码到内存这个过程生效了。
五、开源项目和课程规划
如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们,一起来开发。
参考书籍
《操作系统真相还原》这本书真的赞!强烈推荐
项目开源
项目开源地址:https://gitee.com/sunym1993/flashos
当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。
如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。
课程规划
本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。
目前的系列包括
- 【自制操作系统01】硬核讲解计算机的启动过程
- 【自制操作系统02】环境准备与启动区实现
- 【自制操作系统03】读取硬盘中的数据