从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的标志寄存器eFlags的CF(carry flag)位就会被置位,示意上一条指令发生了进位.
利用条件跳转指令 jc 可以在CF为1时发生跳转.
mov ax, 0xFFFF add ax, 1 ; 这条指令会发生进位,指令完成后,ax = 0x0000, CF = 1 jc carry_bit_is_1 ... carry_bit_is_1: ... ...
源码
本节的源码有两个:
- 主函数,设置相关参数,读取硬盘,并打印buffer中的数,查看是否读取正确
- 读取硬盘函数,基于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
.
这是实验结果,表明读取成功.