一、磁盘介绍
机械硬盘(Hard Disk Drive, HDD)是一种使用磁性存储和机械运动进行数据读写的存储设备。一个硬盘包含多个盘片。盘片上根据不同的圆环划分为磁道。所有盘片相同位置的磁盘组合形成柱面。磁盘划分为扇区,每个扇区的大小是512字节。早期硬盘在CHS寻址时代,每个磁盘划分为63个扇区。扇区是硬盘读写的最小单位,最小读写一个,最多读写 256 个扇区。当然早期的这种设计有磁道外圈空间浪费等诸多缺点,我们不进行详细展开。
二、硬盘读写
硬盘属于外部设备,对外部设备的读写需要端口实现。硬盘读写有两种方式,第一种是CHS模式(/ Cylinder / Head / Sector),第二种是LBA模式。CHS模式需要给出读写删除的柱面、磁盘和扇区的位置坐标,比较复杂。LBA模式把磁盘视为逻辑块,直接指定读取第几个扇区即可。
2.1、硬盘控制端口
硬盘的控制端口支持两个通道,每个通道可挂载两块硬盘。这里从bios的配置文件也可以看出来。我们这里只使用主通道。
接下来逐个介绍一下主通道各个端口的作用
- 0x1F0:16bit 端口,用于读写数据
- 0x1F1:检测前一个指令的错误
- 0x1F2:读写扇区的数量
- 0x1F3:起始扇区的 0 ~ 7 位
- 0x1F4:起始扇区的 8 ~ 15 位
- 0x1F5:起始扇区的 16 ~ 23 位
- 0x1F6: 0 ~ 3位:起始扇区的 24 ~ 27 位
- 4位: 0 主盘, 1 从片
- 6位: 0 CHS, 1 LBA
- 5 ~ 7:固定为1
- 0x1F7: 如果操作是out
- 0xEC: 识别硬盘
- 0x20: 读硬盘
- 0x30: 写硬盘
- 0x1F7: 操作是in / 8bit
- 0位 ERR
- 3位 DRQ 数据准备完毕
- 7位 BSY 硬盘繁忙
2.1、读硬盘的代码实现
接下来我们通过端口操作,使用汇编语言实现一下读取硬盘的函数代码。使用edi寄存器指定读取硬盘数据到指定内存为止,ecx寄存器指定读取的起始扇区,bl寄存器指定读取的扇区数量
read_disk:
mov dx, 0x1f2 ; 设置读写扇区的数量, 0x1f2 是读写扇区数量的端口
mov al, bl
out dx, al ; 读写扇区的数量送入 0x1f2端口
inc dx ; 0x1f3 端口+1
mov al, cl ; 起始扇区的前八位
out dx, al
inc dx
shr ecx, 8
mov al, cl ; 起始扇区的中八位
out dx, al
inc dx
shr ecx, 8
mov al, cl ; 起始扇区的高八位
out dx, al
inc dx
shr ecx, 8
and cl, 0b1111 ; 将高四位置为 0
mov al, 0b1110_0000
or al, cl
out dx, al ; 主盘 - LBA 模式
inc dx ; 0x1f7
mov al, 0x20 ; 读硬盘
out dx, al
xor ecx, ecx ; 将 ecx 清空
mov cl, bl ; 得到读写扇区的数量,为了循环读取下一个扇区
.read:
push cx ; 保存 cx
call .waits ; 等待数据准备完毕
call .reads ; 读取一个扇区的函数
pop cx ; 恢复 cx
loop .read
ret
.waits:
mov dx, 0x1f7
.check:
in al, dx
jmp $+2 ; 直接跳转到下一行,相当于nop,jmp消耗的时钟周期比较多。
jmp $+2 ; 一点点延迟, 等待硬盘准备完毕
jmp $+2
and al, 0b1000_1000 ; 获取硬盘的状态,只保留第3位和第7位
cmp al, 0b0000_1000 ; 判断硬盘是否繁忙且数据是否准备完毕
jnz .check ; 没准备好再次进入等待
ret
.reads:
mov dx, 0x1f0 ; 用于读取数据的端口
mov cx, 256 ; 一个扇区 256 字
.readw:
in ax, dx
jmp $+2; 一点点延迟
jmp $+2
jmp $+2
mov [edi], ax
add edi, 2
loop .readw
ret
2.2、读硬盘的代码验证
目前我们的硬盘只有主引导扇区内是有数据的,因此我们来测试一下读取主引导扇区的数据到指定内存位置:
mov edi, 0x1000; 读取的目标内存
mov ecx, 0; 起始扇区
mov bl, 1; 扇区数量
call readdisk
如上所示,我们读取第1个扇区到内存 0x1000 的位置。 需要说明的是,本文这里只贴了部分代码,在测试过程中,需要把这些代码写到boot.asm文件中,运行bochs -q进行测试。代码执行完成后,通过bochs的调试界面查看物理内存 0x1000 的位置已经被写入主引导扇区的数据,本文不再进行贴图。
2.3、写硬盘的实现和验证
主引导扇区并不会使用到写硬盘的功能,但是我们还是实现一下,写硬盘的代码和读硬盘的代码非常类似,只需要做少部分的修改。
read_disk:
...... ; 这部分与读硬盘的代码一致
inc dx ; 0x1f7
mov al, 0x30 ; 写硬盘
out dx, al
.write:
push cx; 保存 cx
call .writes; 写一个扇区 先写入再进行等到磁盘写入的动作
call .waits; 等待硬盘繁忙结束
pop cx; 恢复 cx
loop .write
.waits:
mov dx, 0x1f7
.check:
in al, dx
jmp $+2; nop 直接跳转到下一行
jmp $+2; 一点点延迟
jmp $+2
and al, 0b1000_0000
cmp al, 0b0000_0000
jnz .check
ret
.writes:
mov dx, 0x1f0
mov cx, 256; 一个扇区 256 字
.writew:
mov ax, [edi]
out dx, ax
jmp $+2; 一点点延迟
jmp $+2
jmp $+2
add edi, 2
loop .writew
ret
写硬盘一样使用edi指定读取的内存,ecs指定起始扇区,bl寄存器指定写扇区的数量。
mov edi, 0x1000; 读取的目标内存
mov ecx, 2; 起始扇区
mov bl, 1; 扇区数量
call write_disk
如上示例:将内存起始位置 0x1000 的512字节写入到第2个扇区。代码完成后,可以使用hexdump等工具查看硬盘上第二个扇区的数据。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付
