磁盘/硬盘结构
关于磁盘/硬盘的基础概念,本节不做过多讲解,这里给出一篇写的很好的博客,硬盘概念:扇区,磁道,磁头,柱面,簇,针对本节知识来说,掌握扇区、磁头、柱面这三个概念即可。
综上所述, 1张软盘有80个柱面, 2个磁头, 18个扇区, 且一个扇区有512字节。 所以, 一张软盘的容量是:80×2×18×512 = 1 474 560 Byte = 1 440KB
读取硬盘中的内容
我们没有必要再实现一个读取硬盘数据的底层函数,因为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.
这是实验结果,表明读取成功.