这里写个往期推荐,这样可以来回跳跃(狗头
0x00-环境准备
0x01-BIOS以及MBR
0x02-MBR支持显卡
0x03-MBR操作硬盘以及Loader
0x04-进入保护模式
0x00 基础知识们
今天我们来继续精进咱们的改进MBR,上次我们所做的事是使用显卡显存来实现屏幕输出,今天我们来了解了解磁盘相关知识.
1. 硬盘
首先我们来大概给出磁盘的基础知识,可能大伙学过机组或者说操作系统这两门课程的同学对此会比较熟悉,但我在这里还是简单说几句,首先就是磁盘的结构(注意下面解释的是机械键盘,而不是咱们目前比较常见的固态硬盘)。下面先给出图片:
一个磁盘由多个盘面组成,可以理解成下面途中光盘的A面B面
每个盘片被划分为一个个磁道,而这每个磁道也被称为柱面,你可以运用立体思维来想想,这么多个盘组合在一起那不就成了一个圆柱体了吗,然后圆柱体中我任取一个磁道,他们所有盘面的相对磁道恰好就构成了一个圆柱面,每个磁道又划分为一个个扇区。如下图:
由上,可用(柱面号,盘面号,扇区号)来定位任意一个“磁盘块”。这也被成为物理磁盘地址(由于磁盘属于块设备,所以一个地址对应了一个块,或者说一个簇)
可根据该地址读取一个“块”,操作如下:
-
根据“柱面号”移动磁臂,让磁头指向指定柱面;
-
激活指定盘面对应的磁头;
-
磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写。
而硬盘也如同外设,其中也必定要有IO接口来实现与CPU通信,就像对于显卡之于显示器,磁盘也有属于自己的IO接口,那就是硬盘控制器。
2. 硬盘控制端口
对于硬盘控制器而言,其IO模式与显卡不同,在使用显卡时,我们是使用了统一编址,但是在硬盘控制器下,其使用的是独立编制,这里有不懂的地方可以参考上一节。以下我给出硬盘控制器主要端口寄存器信息:
由上表可知端口分为两组,即为Commend Block registers和Control Block registers,其中Commend Block registers用来向硬盘驱动器写入命令字或从硬盘控制器中读出硬盘状态(因为硬盘会通过发送自己的状态到硬盘控制器,这种状态有且不限于正在忙,空闲等),而Control Block registers用来控制硬盘工作状态,这有点类似于咱们机组中学到的控制/状态寄存器,在应试教育中他俩是和到一起的,而事实上他俩之中Control Block registers也确实精简了许多,他们俩的功能越来越揉和,所以一下主要介绍Commend Block registers
3. 主盘、从盘、通道
这里首先给出几个基本概念解释:
在电脑中我们通常会碰到安装多个硬盘的情况,可能还得安装光驱什么的,这样就必须分出主从关系,这样就有了硬盘中的Master和Slave,Master就是主盘的意思,Slave就是从盘的意思,那么要这个主盘从盘干什么呢,这里就得涉及到一个系统启动的问题,因为我们大多数启动会设置为主盘来启动。在很久之前,主从盘的分工很明确,很多工作以主盘为中心,就比如系统一般安装在主盘上,可是在后来计算机发展兼容性的过程中,这俩之间的区别就越来越小了。
然后就是通道,一般主板上面会给出两个插槽,这两个插槽被称作通道,这儿我自己还有一点不清楚,但是就之前考研复习408得出的知识来说,这个通道是比DMA模式更加高级的存在,也就是负责IO的数据直接传到内存,且此过程不需CPU控制,我们也可以把它看作一个沟渠,也就是负责硬盘控制器传输到内存的一个管道。说回来,这两个通道分别被称为Primary通道和Secondary通道(由上面的表也可以分别看到),而每个通道上面才分主盘和从盘。
3. Command Block registers
咱们来按照上面的表来逐一解释一下各端口的作用(实际上就是硬盘控制器上的寄存器)
-
首先我们可以看到第一行的0x1F0所对应的端口,在读和写的时候分别作为Data寄存器,且唯独%只有这个寄存器是16位的,其他寄存器一般都是8位%。而这里他的作用就和其名字一致,接受磁盘传送来的数据或CPU传送的数据。
-
然后就是0x1F1,在读操作时他被用作Error寄存器,只有在读取硬盘失败的时候,里面才会有读取失败的信息。而在写操作时,他充当Features寄存器,他的作用是CPU传来指令的时候有时需要夹带一些额外参数,,这些参数就存放在Features寄存器中。
-
0x1F2在读写操作时都充当Sector count寄存器,他是用来指定待读取或待写入的扇区数。
-
0x1F3、0x1F4、0x1F5分别代表了LBA地址的low、mid、high位,这里我们再科普一下LBA是什么
CHS大家可能知道,在上面我们讲硬盘基础知识的时候设计到的,他是代表了硬盘的地址,跟我们所对应的内存地址是一致的,只不过硬盘地址代表一个块,而主存地址代表一个地址单位(一般是1字节),而且CHS的盘地址是从1开始的,即0盘0道1扇区,这在之前的篇章中也说过BIOS会加载位于0盘0道1扇区的MBR来交接。这里我们所说的LBA其实就是另外一种编址标准,他更适合程序员的认识,盘快地址从0开始。
而LBA分为两种,一种是LBA28,即用28位来描述一个扇区的地址,即2的28次方个扇区,总共寻址大约128G,还有一种是LBA48,即用48位来描述,和LBA28大同小异,这里我们选个简单的那就是LBA28.
现在我们来继续介绍上面三个端口,可知这三个端口都是8位,总共有24bit,但他如何表示28位呢,别慌,他把这后面4位放到别的寄存器了,之后碰到该寄存器我会继续解释。
-
0x1F6在读写操作时都作为devie寄存器,他充当一个杂项寄存器,可以发现他的宽度是8位,其中低4位用来存储LBA28模式下剩余的4位,而第四位指定通道上的主盘和从盘,第6位来设置启用LBA还是CHS模式,其他位固定为1.这里给出他的结构示意
-
0x1F7端口在读操作时用作Status寄存器,他用来给出硬盘的状态信息,第0位为ERR位,代表命令出错,具体原因存放在上面的Error寄存器中,地3位为data request位,若为1则表示硬盘将数据已经存放好,第6位为DRDY,表示硬盘就绪,这位是表示硬盘正常,可以继续执行一些指令。第七位为BSY位,为1则表示硬盘正忙。而在写硬盘时,该端口充当command寄存器,存放需要硬盘执行的命令,在咱们的系统中,主要使用三个命令:
- identify:0xEC,硬盘识别
- read sector: 0x20,读扇区
- write sector: 0x30,写扇区
这里给出0x1F7端口的结构:
4. 硬盘操作方法
我们要读取硬盘,大家可能也会想到就是使用in out的IO命令来对端口进行操作,就比如往data寄存器写或读数据,往command寄存器写命令,没错就是这样,但是总得有个先后顺序把,其中我们的顺序必须遵循一个大前提,那就是command寄存器一定要最后写,因为一旦command寄存器被写入那就开始执行命令了。这里给出一个基本顺序供大家参考。
- 选择通道,往通道的sector count寄存器写入我们即将操作的扇区数目
- 向0x1F3、0x1F4、0x1F5分别写入LBA地址的低24位
- 往device寄存器的低4字节写入LBA地址的高4位,设置第6位为1,即表明我们需要使用LBA寻址模式,再设置第四位来选择操作的硬盘是主盘还是从盘
- 往通道的command寄存器写入操作命令
- 读取通道上的status寄存器,判断硬盘工作是否完成
- 若上述命令选择了读硬盘,则进入下一个步骤,若为写或者其他,则完工
- 将硬盘数据读出
这里我们继续说,当我们使用读硬盘命令时,也就是说执行完1-5步,此时数据已经缓存到Data寄存器中,我们该如何从获取寄存器中的数据呢,一般方式如下:
- 无条件传送
- 查询传送
- 中断传送
- 直接存储器存取(DMA)
- I/O处理器传送
这里我们使用第2、3种方式,其他方式各位感兴趣可以自行搜索相关资料,这里我就不过多赘述。
0x01 使用MBR来操作硬盘
写到这儿,其实我们的MBR还没干啥本职工作,之前咱们只是简单的实现了打印字符,但是MBR本来是要加载Loader的,然后Loader来加载我们的操作系统(在这里的Loader你可以看作又是一段程序),而Loader也存放在硬盘上,所以我们额MBR就需要读取硬盘上的Loader了,所以也就需要了今天的知识。
1. Loader放哪儿呢和该去哪儿呢
Loader当然存放在硬盘上啦( 。我们现在来回忆一下,我们知道MBR是存放在地一个扇区,也就是0扇区(若采用CHS寻址,则为地一个扇区,此处我们使用LBA寻址,所以为第0个扇区),所以说1扇区是空闲的,我们理论上也可以把Loader放入1扇区,但是这里考虑到两者隔太近有可能会出现问题,不妨咱们就浪费一个扇区算了,将它放入2号扇区。
而这里我们MBR就是要将Loader从2号扇区读出来,然后再存入内存,但是内存该放哪儿了,在以下内存布局图中,只要是标注为可用的区域都可以使用,如下
开始地址 |
结束地址 |
大小 |
用途 |
FFFF0 |
FFFFF |
16B |
BIOS入口,这么小的一个位置实际上仅仅是一个跳转指令 |
F0000 |
FFFEF |
64KB-16B |
系统BIOS |
C8000 |
EFFFF |
160KB |
映射硬件适配器的ROM或内存映射式I/O |
C0000 |
C7FFF |
32KB |
显示适配器BIOS |
B8000 |
BFFFF |
32KB |
文本显示适配器 |
B0000 |
B7FFF |
32KB |
黑白显示适配器 |
A0000 |
AFFFF |
64KB |
彩色显示适配器 |
9FC00 |
9FFFF |
1KB |
EBDA |
7E00 |
9FBFF |
约608KB |
可用 |
7C00 |
7DFF |
512B |
MBR加载地址 |
500 |
7BFF |
约30KB |
可用 |
400 |
4ff |
256B |
BIOS数据区域 |
000 |
3FF |
1KB |
中断向量表 |
也就是说在0x500~0x7BFF,0x7E00~0x9FBFF都可以使用,考虑到我们以后的程序越来越多,这里我们就将Loader尽量放到低地址,这里我们选择放到0x900这里,我们不直接放在0x500的愿意跟上述硬盘地址一样,距离产生美。
2. MBR操作磁盘实现
首先为了让Loader跟MBR分离开来,我们另外写个文件,文件名为 “boot.inc”,
;---------- loader 和 kernel ------------
LOADER_BASE_ADDER equ 0x900 ;内存首址
LOADER_STAER_SECTOR equ 0x2 ;硬盘扇区
这里我也给出关键读硬盘代码,需要完整代码还是去github上面吧,因为代码完整贴出来太长了
mov eax,LOADER_START_SECTOR ;起始扇区LBA地址,注意这里使用32位寄存器是可行的,虽然说实模式下咱们只能用16位地址,但不是说用不了32位寄存器
mov bx,LOADER_BASE_ADDR ;写入的内存地址
mov cx,1 ;待读入的扇区数
call rd_disk_m_16 ;以下读取程序的其实部分(一个扇区),rd_disk_m_16表示在16位模式下读硬盘的section函数,我们在下面实现
jmp LOADER_BASE_ADDR ;代码运行至此说明Loader已经加载完毕
;------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-----------------------------------
;eax = 扇区LBA地址
;bx = 将数据写入的内存地址
;cx = 读入的扇区数
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地址0~7位存入0x1F3
mov dx,0x1F3
out dx,al
;LBA地址8~15位写入0x1F4
mov cl,8
shr eax,cl ;将eax中数据右移8位,这样就可以接着使用al来取中间8位了
mov dx,0x1F4
out dx,al
;LBA地址16~23位写入0x1F5
shr eax,cl
mov dx,0x1F5
out dx,al
shr eax,cl
and al,0x0f ;取LBR第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 ;空操作,相当与sleep一下
in al,dx ;此时我们读取0x1F7端口,此时它充当Status寄存器
and al,0x88 ;其中第3位为1表示硬盘控制器已经准备好,第7位为1表示硬盘忙,这里即为取对应位的值
cmp al,0x08 ;判断符号位是否与顺利执行时的符号一致
jnz .not_ready ;若未准备好,则继续回跳
;第5步:从0x1F0端口读取数据
mov ax,di ;di为之前备份的读入扇区数
mov dx, 256
mul dx
mov cx,ax ;这里cx来存放循环次数
;一个字为两字节,而我们需要读入一个扇区,即为512字节,每次读入一个字(这是因为data寄存器有16位),所以共需要di×512/2 = di*256次
mov dx,0x1F0
.go_on_read:
in ax,dx
mov [bx],ax ;bx存放的是加载的内存地址
add bx,2 ;因为每次存2字节,所以内存地址每次加2,然后继续读
loop .go_on_read ;这里cx作为循环控制次数
ret
由于咱们的程序逐渐多了起来,我们该适当整理整理文件了,我们先创建一个名为boot的文件夹,然后将mbr.S放进去,在boot目录下我们再创建一个include文件夹,再将我们刚写的boot.inc放入,具体操作如下:
mkdir boot
mv mbr.S boot.inc ./boot
cd boot
mkdir include
mv ./boot.inc ./include
然后我们就在boot目录下进行操作了,首先依然是编译,今天的编译同一往不同,需要加上I参数,具体解释可以参见nasm帮助手册,nasm -h 回车。其大概意思就是添加一个库目录,这样我们在寻找包含文件的时候就会不仅在本目录下找,也会在我们-I指定的目录下寻找
接下来我们执行下面的命令:
nasm -I include/ -o mbr.bin mbr.S
dd if=./mbr.bin of=你的安装路/bochs/hd60M.img bs=512 count=1 conv=notrunc
注意此时还不能执行,先编译了再说,具体情况我在下面讲
3. Loader编写
但是此时我们还不能执行,因为我们的内核加载器,也就是说Loader还没编写,咱们的MBR刚刚实现的工作就是加载位于2号扇区的Loader,但是此时2号扇区啥也没有。
我们这儿再编写一个简单Loader,用显卡输出到屏幕上就行,这里我推荐大家可以自行先尝试,跟前几次的显卡输出一样的(注意到boot目录下写奥):
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR ;起始地址按照之前约定一样
;按照显卡输出
;输出背景色为蓝色,钱景色为红色且跳动的字符串“I am loader”,注意为小端序
mov byte [gs:0x00], 'I'
mov byte [gs:0x01], 0x94
mov byte [gs:0x02], ' '
mov byte [gs:0x03], 0x94
mov byte [gs:0x04], 'a'
mov byte [gs:0x05], 0x94
mov byte [gs:0x06], 'm'
mov byte [gs:0x07], 0x94
mov byte [gs:0x08], ' '
mov byte [gs:0x09], 0x94
mov byte [gs:0x0a], 'l'
mov byte [gs:0x0b], 0x94
mov byte [gs:0x0c], 'o'
mov byte [gs:0x0d], 0x94
mov byte [gs:0x0e], 'a'
mov byte [gs:0x0f], 0x94
mov byte [gs:0x10], 'd'
mov byte [gs:0x11], 0x94
mov byte [gs:0x12], 'e'
mov byte [gs:0x13], 0x94
mov byte [gs:0x14], 'r'
mov byte [gs:0x15], 0x94
jmp $ ;循环等待
然后我们依次执行命令(就跟我们将mbr.S打入硬盘类似)
nams -I include -o loader.bin loader.S
dd if=./loader.bin of=你的路径/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc
我们此刻就可以来运行虚拟机了,结果如下图:
这里加入两张图依然是作为对照,证明他在闪烁(狗头
0x02 总结
今天操作磁盘的工作或许有点多,但他仍然处于简单的水平,本次的所有源代码依旧在github上面同步更新,分支名为UUMBR
传送门