以下图片均来自王道考研视频我们先来看看硬盘的结构:
这是物理图上的磁盘构造。
一个盘片被分成一个个的磁道。
在划分一下就会出现我们的扇区,每个扇区的大小是固定的,一般来说是512个字节。最内侧的扇区面积小,因此数据密度就密。在我们操作磁盘的时候,一般就以扇区为最小单位。
这样许多个盘片组合起来就是一整个磁盘。下图是一个解剖图:
这些磁头都是连接在同一个磁臂上的。这样就组成了一个柱子形状,我们称之为柱面。有了上图的基础,我们就能用柱面号,盘面号,扇区号来唯一标识一块磁盘块。其实柱面号也可以理解为我们要指定的磁道是哪一个,也就是王道这句话:
如果想读写一个磁盘大致的操作分为如下几步:
我们有这些知识就足够我们理解磁盘读写了。
扇区也就是我们操作磁盘的最小单位,一次最少也要对一个扇区读写,最多能达到256个扇区。
想要操作硬盘读写,我们需要对端口进行输入输出,也就是IO端口形式。
这个端口不同于网络协议中的端口,它是外部设备内部的寄存器,是用来提供一个通道给我们操控这个外部设备的。也称之为端口。因此分配给硬盘的寄存器是需要我们查阅的。
硬盘读写是有两种形式:
-CHS模式 也就是/柱面/磁头/磁道 也就是用坐标系来标识一个磁盘块,这个模式比较复杂,一般不用。
在Linux中,硬件外部设备都被抽象成了文件,硬盘也不例外,每个扇区都可以任务是一个文件,因此产生出了另外一种模式 -LBA模式 /逻辑块地址/ 只需要知道想操作哪一块扇区就完事了很方便
LBA模式也有2种,一种是用28位比特来表示一个扇区,一个是48位比特来表示一个扇区,分别成为LBA28,LBA48,我们只用LBA28模式就好了。
下面我们介绍下硬盘控制器的端口:
一个Primary通道上是可以挂2个硬盘,一个主盘,一个从盘,但是做实验时我们只有一个主盘因此暂时不管从盘的问题。磁盘是很复杂的东西,我们写操作系统小麻雀的时候,用以上的端口完全足够了。下面介绍下这些端口的作用:
0x1F0 是一个16位的端口,用来读写数据的
0x1F1 检测前一个指令的错误(一般不用,出错的时候用)
0x1F2 用于读写扇区的数量
0x1F3 起始扇区的0-7位
0x1F4 起始扇区的8-15位
0x1F5 起始扇区的16-23位
0x1F6 第四位是存的剩下4位(28比特的最后四位),图示如下:
0x1F7 此端口是用来存储硬盘执行的命令只介绍三个我们用的:
0xEC 硬盘识别
0x20 读扇区
0x30 写扇区
基本上就介绍完了。下面我们开始上代码(我会给出相应详细的注释):
;第一步
;初始化一些寄存器(数据)
mov edi,0x1000 ;读到内存的哪个位置
mov ecx,2 ;起始扇区
mov bl,4 ;扇区数量
call read_disk ;调用读扇区这个函数
;第二步实现read_disk函数
read_disk:
mov dx,0x1f2 ;读写扇区数量,参考前面讲的寄存器
mov al,bl ;读取扇区数量放入al寄存器
out dx,al ;往端口写
inc dx ;0x1f3
mov al,cl ;起始扇区的低八位
out dx,al
inc dx ;0x1f4
shr ecx,8 ;讲中八位移到低8位的操作 右移操作符
mov al,cl ;起始扇区的中八位
out dx,al
inc dx ;0x1f5
shr ecx,8 ;将高八位移到低8位的操作
mov al,cl ;起始扇区的高八位
out dx,al
inc dx ;0x1f6
shr ecx,8
and cl,0b1111 ;将高4位置为0
mov al,0b1110_0000 ;0x1f6的device寄存器 参考上图
or al,cl ;合二为一
inc dx ;0x1f7
mov al,0x20 ;读操作
out dx,al
xor ecx,ecx ;ecx清0
mov cl,bl ;得到读写扇区的数量 循环变量作用于loop
.read:
push cx
call .waits ;等待数据
call .reads; 读取扇区
pop cx
loop .read
ret
.waits:
mov dx,0x1f7 ;使用status寄存器 检索硬盘状态
.check:
in al,dx ;往端口读
jmp $+2 ;空指令
jmp $+2
jmp $+2
and al,0b1000_1000 ;将第4位和第八位清0
cmp al,0b0000_1000 ;判断DRQ是否准备好数据 cmp指令影响ZF位
jnz .check ;如果不等于循环检查 直到DRQ为1为止
ret
.reads:
mov dx,0x1f0 ;读硬盘操作
mov cx,256 ;一次读取一个字 循环变量
.readw:
in ax,dx
jmp $+2 ;空指令
jmp $+2
jmp $+2
mov [edi],ax ;将读取的数据放到0x1000内存位置
add edi,2 ;每次加2
loop .readw
ret
效果图可以参考内核加载器那篇文章,用的就是此套函数将加载器加载到0x1000的内存位置,写硬盘逻辑刚好是相反的,目前还没用到,就没有实现此功能。