从0创建一个OS (八) 读取硬盘中的数据

本节将学习如何从硬盘中读取数据

关键字: 硬盘; 柱面(cylinder);磁头(head);扇区(sector); 进位(carry bit)

目标:利用boot sector从硬盘中读取数据(这一步的目地是为从硬盘中启动操作系统kernel做准备)

理论基础

本节的内容是自本系列博客开始以来最复杂的一节,希望读者在学习时尽量保持耐心,译者读这一节的原文和代码也花了一整天时间,希望大家Keep Patient.

磁盘/硬盘结构

关于磁盘/硬盘的基础概念,本节不做过多讲解,这里给出一篇写的很好的博客,硬盘概念:扇区,磁道,磁头,柱面,簇,针对本节知识来说,掌握扇区、磁头、柱面这三个概念即可。

读取硬盘中的内容

我们没有必要再实现一个读取硬盘数据的底层函数,因为BIOS会提供给我们,我们需要做的就是利用BIOS提供的接口完成我们的任务.

BIOS提供的接口为0x13号中断的硬盘读取功能,读取硬盘任务的相关参数写入特定寄存器,即可在部分寄存器和缓存中得到读取结果.

下面是0x13号中断的介绍,喜欢看英文文档的读者也可以直接看INT 13,2 - Read Disk Sectors,我也会将该中断的介绍放入代码中,方便读者在阅读代码时前后对照,理解相关代码的用意.

; ======================================================================================
;                  |||             BIOS 0x13号中断的读硬盘功能             |||
; --------------------------------------------------------------------------------------
; input:      AH = 02
;             AL = 一共需要读的sector数量
;             CH = 需要读的柱面号
;             CL = sector号
;             DH = 磁头号
;             DL = 驱动器号(0为第一个软盘,1为第二个软盘, 0x80为0号hdd, 0x81为1号hdd)
;             ES:BX = 指向读取出的数据将要存储的buffer的指针

; return:     AH = 状态(读取硬盘的状态,一般有00-no error, 01-给驱动器的命令错误...)
;             AL = 读取的sector数量
;             CF(CPU状态寄存器的一个域) == 0 则表示读取成功
;                                    == 1 则表示读取失败

; 注意事项:    - BIOS的读硬盘操作会重试(如果失败)至少3次,之后控制器将会根据错误状态复位
;             - ES:BX指向的buffer不能横跨64K的段边缘,否则就会产生DMA边缘错误
;             - 许多编程参考列表只包含软盘寄存器值
;             - 输入中CPU只会检查驱动器号的合法性,其余输入不会检查
;             - CX中的参数根据柱面数量的变化会有所变化
;             - 柱面号是一个10bit的值, 高2bit来自于CL(bit7, bit6),低8bit来自于CH(bit7~bit0)
; ======================================================================================

 

进位标志

如果在指令的运算中发生了进位,那么CPU的标志寄存器eFlagsCF(carry flag)位就会被置位,示意上一条指令发生了进位.

利用条件跳转指令 jc 可以在CF为1时发生跳转.

mov ax, 0xFFFF
add ax, 1 ; 这条指令会发生进位,指令完成后,ax = 0x0000, CF = 1
jc carry_bit_is_1
...

carry_bit_is_1:
                ...

...

 

源码

本节的源码有两个:

  1. 主函数,设置相关参数,读取硬盘,并打印buffer中的数,查看是否读取正确
  2. 读取硬盘函数,基于BIOS的13号中断实现
    这里我们以先子函数,后主函数的形式展现给大家,也希望大家以这个顺序阅读源码,有利于加深对代码的理解.

boot_sect_disk.asm

; ======================================================================================
;                  |||             BIOS 0x13号中断的读硬盘功能             |||
; --------------------------------------------------------------------------------------
; input:      AH = 02
;             AL = 一共需要读的sector数量
;             CH = 需要读的柱面号
;             CL = sector号
;             DH = 磁头号
;             DL = 驱动器号(0为第一个软盘,1为第二个软盘, 0x80为0号hdd, 0x81为1号hdd)
;             ES:BX = 指向读取出的数据将要存储的buffer的指针

; return:     AH = 状态(读取硬盘的状态,一般有00-no error, 01-给驱动器的命令错误...)
;             AL = 读取的sector数量
;             CF(CPU状态寄存器的一个域) == 0 则表示读取成功
;                                    == 1 则表示读取失败

; 注意事项:    - BIOS的读硬盘操作会重试(如果失败)至少3次,之后控制器将会根据错误状态复位
;             - ES:BX指向的buffer不能横跨64K的段边缘,否则就会产生DMA边缘错误
;             - 许多编程参考列表只包含软盘寄存器值
;             - 输入中CPU只会检查驱动器号的合法性,其余输入不会检查
;             - CX中的参数根据柱面数量的变化会有所变化
;             - 柱面号是一个10bit的值, 高2bit来自于CL(bit7, bit6),低8bit来自于CH(bit7~bit0)
; ======================================================================================




; ======================================================================================
;                     |||               disk_load函数说明          |||
; --------------------------------------------------------------------------------------
; 函数名: disk_load
; input: 需要传入3个参数
;      - es:bx(读取到的数据将要存储的buffer)
;      - dl(驱动器号)
;      - dh(需要读取的sector数量)
; 功能: 读取dl驱动器,0号柱面,0号磁头,2号sector开始的共dh个sector的数据,存储到es:bx指向的buffer
; ======================================================================================

disk_load:
    pusha

    ; 从硬盘读数据需要对所有相关寄存器设置特殊的值
    ; 对dx,我们在读取过程中会覆盖原始值,因此在这里先保存其值到栈,以便后续使用
    push dx

    mov ah, 0x02
    mov al, dh ; 需要读的sector数量
    mov cl, 0x02 ; cl是sector号
                 ; 1号sector存储我们的boot sector, 因此2号sector是第一个空闲sector
    mov ch, 0x00 ; ch是柱面号(cylinder)的低8bit,高2bit来自于cl

    ; dl是驱动器号,我们将驱动器号设置为一个参数,从外部传入,外部通过BIOS获得驱动器号

    mov dh, 0x00 ; 磁头号

    ; es:bx是读取出的数据将要存储的buffer的指针,由外部传入
    
    int 0x13 ; 调用BIOS 0x13号中断
    jc disk_error ; CF == 1则表示读取失败

    pop dx
    cmp al, dh ; 读取完成后,al中存放的是成功读取的sector数量
               ; 这里应该与需要读取的sector数量做对比
    jne sectors_error
    popa
    ret

disk_error:
    mov bx, DISK_ERROR
    call print
    call print_nl
    mov dh, ah ; ah为读取状态,这里为错误状态
    call print_hex ; 打印出错误状态码,错误码相关信息见http://stanislavs.org/helppc/int_13-1.html
    jmp disk_loop

sectors_error:
    mov bx, SECTORS_ERROR
    call print
    call print_nl

disk_loop:
    jmp $

DISK_ERROR: db "Disk Read Error", 0
SECTORS_ERROR: db "Incorrect Number of Sectors readed", 0

 

boot_sect_main.asm

[org 0x7C00]

mov bp, 0x8000 ;设置栈,使其远离0x7C00段
mov sp, bp

mov bx, 0x9000 ; 需要传入disk_load的buffer,也是读取到的数据将要存储的buffer es:bx, es默认为0
mov dh, 2 ; 读取2个sector

; 在不对dl做其它赋值的情况下,bios默认将dl设置为用于boot的硬盘号
; 如果在qemu-system-x86_64 file.bin时出了问题,建议使用qemu-system-x86_64 -fda file.bin
; -fda选项的作用: 使用文件作为软盘0/1的镜像

call disk_load

mov dx, [0x9000] ; 检查buffer中的第一个字节,看看是否为0xdada
call print_hex

call print_nl

mov dx, [0x9000 + 512] ; 检查buffer中读取到的第二个sector(硬盘的第三个sector)中的数据,看看是否为0xface
call print_hex

jmp $

%include "../05-bootsector-functions-strings/boot_sect_print.asm"
%include "../05-bootsector-functions-strings/boot_sect_print_hex.asm"
%include "boot_sect_disk.asm" 

times 510 - ($ - $$) db 0
dw 0xAA55

; boot sector 是hdd0的0号磁头的0号柱面的sector1(sector从1开始数,其余参数都是从0开始数)
; 对需要读取的sector2和sector3进行预先填充,这两个sector都是512个字节

times 256 dw 0xdada
times 256 dw 0xface

 

实验结果

由于本次的实验可能在不同的系统上会出一些问题,特此说明.

如果使用qemu-system-x86_64 boot_sect_main.bin显示boot不成功,可以尝试使用qemu-system-x86_64 -fda boot_sect_main.bin.

这是实验结果,表明读取成功.

在这里插入图片描述

posted @ 2020-11-18 17:02  EwanHai  阅读(325)  评论(0编辑  收藏  举报