自制操作系统 - 磁盘读写

磁盘读写

Posted by 王富杰 on Friday, January 5, 2024

一、磁盘介绍

机械硬盘(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等工具查看硬盘上第二个扇区的数据。

「真诚赞赏,手留余香」

WangFuJie Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付