【自制操作系统03】读取硬盘中的数据

通过 【自制操作系统01】硬核讲解计算机的启动过程【自制操作系统02】环境准备与启动区实现 的讲解,我们已经实现了一个最简单的操作系统(仅仅一条机器指令)。

今天我们要再往前进一步,逐渐将这个最简单的操作系统完善起来。之前最简单的操作系统是写在启动区的 512 字节里,这么小的空间以后肯定不能全部用来写操作系统的代码,所以它的主要任务就是将硬盘中更多的数据读取到内存里,并跳转到内存的那个位置开始运行。

这里不得不回顾一下每节课都说到的四次跳跃:

  1. 一跳:按下开机键,CPU 将 PC 寄存器的值强制初始化为 0xffff0,这个位置是 BIOS 程序的入口地址
  2. 二跳:该入口地址处是一个跳转指令,跳转到 0xfe05b 位置,开始执行
  3. 三跳:执行了一些硬件检测工作后,最后一步将启动区内容加载(复制)到内存 0x7c00,并跳转到这里
  4. 四跳:启动区代码主要是加载操作系统内核,并跳转到加载处

其实我们可以无限跳跃下去,只要感觉某一个环节的任务复杂了,就可以分成两步来走。但也完全可以从第三跳开始就再也不跳转了,把所有操作系统需要的指令和数据都从硬盘中加载到内存,然后执行,但这样显然不好。

一、代码总览

先不说别的,先发上来一份本章内容的全部代码

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 命令寄存器或状态寄存器 命令寄存器

所以如果要写一个程序来读文件的话,不难分析出整个过程就是:

  1. 在 1F2H 写入要读取的扇区数
  2. 在 1F3H ~ 1F6H 这四个端口写入计算好的起始 LBA 地址
  3. 在 1F7H 处写入读命令的指令号
  4. 不断检测 1F7H (此时已成为状态寄存器的含义)的忙位
  5. 如果第四步骤为不忙,则开始不断从 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 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

目前的系列包括

posted @ 2020-01-25 17:38  闪客sun  阅读(5634)  评论(6编辑  收藏  举报