BIOS 总是无法执行磁盘操作
BIOS Always Fails to Perform Disk Operations
我目前正在编写一个引导加载程序,旨在加载一个比引导扇区允许的时间更长的程序。但是,我每次运行这个程序(我在Virtualbox和QEMU都测试过),磁盘读取失败,磁盘重置也失败。
bootloader 被设计为加载它之后的扇区(这是一个 FAT16 卷,所以我在磁盘描述中将其设为保留扇区),运行 程序紧随其后。但是,磁盘读取总是失败(CF 设置为 1),磁盘重置也是如此。这在 Virtualbox 和 QEMU 中都会发生。
这是我的引导扇区和第二扇区的完整代码:
BITS 16
jmp strict short main ; Jump to main bootloader
nop ; Pad out remaining bytes until boot descriptor
; Disk descriptor
OEM_name db "HOUSE " ; Disk label
bytes_sector dw 0x0200 ; Bytes per sector
sectors_cluster db 0x01 ; Sectors per cluster
sectors_reserved dw 0x0002 ; Number of sectors reserved (in this case 1 for MARBLE, the rest for R)
fats db 0x02 ; Number of file allocation tables
max_root_entries dw 0x0200 ; Max number of root entries
sectors dw 0x1040 ; Number of sectors
medium_type db 0xF0 ; Type of medium (removable or fixed?)
sectors_fat dw 0x0010 ; Sectors per file allocation table
sectors_track dw 0x0020 ; Sectors per track
heads dw 0x0002 ; Number of heads
hidden_sectors dd 0x00000000 ; Number of sectors before partition
total_sectors dd 0x00000000 ; Number of sectors in medium (zero because 2B != 0)
drive_number db 0x00 ; Drive number (for BIOS int 0x13)
drive_signature db 0x00 ; NOT USED
ext_signature db 0x29 ; Extended boot signature
volume_serial dd 0x00000000 ; Volume's serial number
volume_label db "HOUSEHOUSE "; Volume label
fs_type db "FAT16 " ; Filesystem type
main:
mov sp, 0x1000 ; 4K of stack
mov ax, word [sectors_reserved] ; Read all reserved sectors
sub al, 0x01 ; Except this one
mov ah, 0x02 ; Read disk sectors
mov bx, r_buffer ; Read contents into our buffer
mov ch, 0x00 ; Cylinder 0
mov cl, 0x02 ; Sector 2
mov dh, 0x00 ; Head 0
jmp .read ; Read the disk
.reset:
pusha ; Push register states to stack
call lps
mov ah, 0x00 ; Reset disk
int 0x13 ; BIOS disk interrupt
jnc .read ; If successsul, read again
call lps
mov ah, 0x00 ; Otherwise, prepare to reboot
int 0x19 ; Reboot
.read:
call lps
int 0x13 ; BIOS disk interrupt
jc .reset ; If failed, reset disk
jmp r_buffer ; Otherwise, jump to R
lps: ; Debug message
pusha
mov ah, 0x0E
mov al, 0x52
mov bh, 0x00
int 0x10
mov ah, 0x00
int 0x16
popa
ret
times 510-($-$$) db 0x00 ; Pad remainder of boot sector with zeros
sig dw 0xAA55 ; Boot signature
r_buffer: ; Space in memory for loading R
r_start: ; Beginning of second sector
mov ax, 0x07C0
add ax, 0x0220
mov ss, ax
mov ax, 0x07C0
mov ds, ax
mov si, success ; Successful
call print_str ; Print!
hlt ; Halt here
print_str: ; Prints string pointed to by REGISTER SI to cursor location (si=str)
pusha ; Push register states to stack
mov ah, 0x0E ; Print in teletype mode
mov bh, 0x00 ; Page 0
.repeat:
lodsb ; Load next character from si
cmp al, 0x00 ; Is this a null character?
je .ret ; If it is, return to caller
int 0x10 ; Otherwise, BIOS interrupt
jmp .repeat ; Do the same thing
.ret:
popa ; Restore register states
ret ; Return to caller
.data:
success db "!", 0x00 ; Success!
times 1024-($-$$) db 0x00 ; Pad remainder of reserved sectors with zeros
正如我所说,第二扇区的代码应该是运行,但由于磁盘重置失败,这不会发生。
@ecm 了解我看到的大部分内容。在访问任何数据之前,您需要设置段寄存器并确保您有一个适合您在段寄存器中加载的值的 ORG
(原点)(尤其是 DS).默认情况下,当没有 ORG
指令(或等效指令)时,默认值为 0x0000.
在实模式下,每个逻辑地址都由两个部分组成——一个段和一个偏移量。 CPU计算出的物理地址是根据公式(段*16)+偏移量计算出来的。将 ORG
视为起始偏移量。如果您使用 0x0000 段,则需要 0x7c00 的偏移量。 (0x0000*16)+0x7c00=0x7c00。 0x7c00 是引导加载程序开始的地方。由于您正在将扇区写入 0x7e00 以上的内存,因此将堆栈设置为 0x0000:0x7c00 以从引导加载程序下方向内存开头向下增长会更简单。
由于您正在使用像 LODSB
这样的字符串指令,您应该确保使用 CLD
指令清除方向标志 (DF),以便字符串操作在内存中向前推进。当您的引导加载程序开始执行时,您不能指望 DF 是清晰的。
读取磁盘时Int 13h/AH=2 clobbers AX。如果出现错误,您需要重新加载 AH 2 和 AL 要读取的扇区数。如果您修改代码,您可以使用 SI 来存储要临时读取的扇区数。通常您不必检查磁盘重置是否失败,只需重新执行读取操作即可。尝试操作几次,然后进入失败状态/重启。我修改了您的代码以在 DI 中放置重试计数。每次重置完成时,重试计数减 1。如果重试计数 >= 0,则尝试读取。如果重试次数 <= 0,则它会重新启动。
当跳转到读取保留扇区的内存地址 0x7e00 处的代码时,您可以借此机会通过使用 FAR JMP 确保 CS 也设置为 0x0000 .
如果在引导扇区中将段设置为 0x0000,则可以在第二阶段重用它们。如果您打算从引导加载程序进入保护模式,我不建议使用 0x0000 以外的段值,以便前 64KiB 中的逻辑地址和物理地址相同。这在加载 GDT 并跳转到保护模式时非常有用。
使用 HLT
时,最好确保中断已关闭(使用 CLI
),否则 HLT
将在下一个中断时退出,处理器将继续执行在那之后发生了什么。在实际硬件上,即使中断关闭,也可能会发生不可屏蔽中断 (NMI),因此将 HLT
放入循环中是个好主意。
考虑到这些变化,这段代码应该可以工作:
BITS 16
org 0x7c00
jmp strict short main ; Jump to main bootloader
nop ; Pad out remaining bytes until boot descriptor
; Disk descriptor
OEM_name db "HOUSE " ; Disk label
bytes_sector dw 0x0200 ; Bytes per sector
sectors_cluster db 0x01 ; Sectors per cluster
sectors_reserved dw 0x0002 ; Number of sectors reserved (in this case
; 1 for MARBLE, the rest for R)
fats db 0x02 ; Number of file allocation tables
max_root_entries dw 0x0200 ; Max number of root entries
sectors dw 0x1040 ; Number of sectors
medium_type db 0xF0 ; Type of medium (removable or fixed?)
sectors_fat dw 0x0010 ; Sectors per file allocation table
sectors_track dw 0x0020 ; Sectors per track
heads dw 0x0002 ; Number of heads
hidden_sectors dd 0x00000000 ; Number of sectors before partition
total_sectors dd 0x00000000 ; Number of sectors in medium (zero because 2B != 0)
drive_number db 0x00 ; Drive number (for BIOS int 0x13)
drive_signature db 0x00 ; NOT USED
ext_signature db 0x29 ; Extended boot signature
volume_serial dd 0x00000000 ; Volume's serial number
volume_label db "HOUSEHOUSE "; Volume label
fs_type db "FAT16 " ; Filesystem type
main:
xor ax, ax ; AX = 0
mov ds, ax ; DS = ES = 0
mov es, ax
mov ss, ax ; SS:SP = 0x0000:0x7c00 (grows down below bootloader)
mov sp, 0x7c00
cld ; Set forward direction for string instructions
mov si, word [sectors_reserved] ; Read all reserved sectors
dec si ; Except this one. SI = sectors to read
mov di, 3 ; retry count of 3 and then give up
mov bx, r_buffer ; Read contents into our buffer
mov ch, 0x00 ; Cylinder 0
mov cl, 0x02 ; Sector 2
mov dh, 0x00 ; Head 0
jmp .read ; Read the disk
.reset:
call lps
dec di ; Reduce retry count
jge .read ; If retry >= 0 then try again
; Otherwise retry count exceeded - reboot
mov ah, 0x00
int 0x19 ; Reboot
.read:
mov ax, si ; Transfer sector read count to AX
mov ah, 0x02 ; Read disk sectors
call lps
int 0x13 ; BIOS disk interrupt
jc .reset ; If failed, reset disk
jmp 0x0000:r_start ; Otherwise, jump to r_start. Set CS=0
lps: ; Debug message
pusha
mov ah, 0x0E
mov al, 0x52
mov bh, 0x00
int 0x10
mov ah, 0x00
int 0x16
popa
ret
times 510-($-$$) db 0x00 ; Pad remainder of boot sector with zeros
sig dw 0xAA55 ; Boot signature
r_buffer: ; Space in memory for loading R
r_start: ; Beginning of second sector
mov si, success ; Successful
call print_str ; Print!
cli
.endloop:
hlt ; Halt here
jmp .endloop
print_str: ; Prints string pointed to by REGISTER SI
; to cursor location (si=str)
pusha ; Push register states to stack
mov ah, 0x0E ; Print in teletype mode
mov bh, 0x00 ; Page 0
.repeat:
lodsb ; Load next character from si
cmp al, 0x00 ; Is this a null character?
je .ret ; If it is, return to caller
int 0x10 ; Otherwise, BIOS interrupt
jmp .repeat ; Do the same thing
.ret:
popa ; Restore register states
ret ; Return to caller
.data:
success db "!", 0x00 ; Success!
times 1024-($-$$) db 0x00 ; Pad remainder of reserved sectors with zeros
观察和建议
- 对于引导加载程序开发,我强烈建议使用 BOCHS 进行初始测试。内置的调试器是实模式感知的,并且理解实模式分段,而 QEMU/GDB 则不然。学习使用调试工具是一项宝贵的技能。
我目前正在编写一个引导加载程序,旨在加载一个比引导扇区允许的时间更长的程序。但是,我每次运行这个程序(我在Virtualbox和QEMU都测试过),磁盘读取失败,磁盘重置也失败。
bootloader 被设计为加载它之后的扇区(这是一个 FAT16 卷,所以我在磁盘描述中将其设为保留扇区),运行 程序紧随其后。但是,磁盘读取总是失败(CF 设置为 1),磁盘重置也是如此。这在 Virtualbox 和 QEMU 中都会发生。
这是我的引导扇区和第二扇区的完整代码:
BITS 16
jmp strict short main ; Jump to main bootloader
nop ; Pad out remaining bytes until boot descriptor
; Disk descriptor
OEM_name db "HOUSE " ; Disk label
bytes_sector dw 0x0200 ; Bytes per sector
sectors_cluster db 0x01 ; Sectors per cluster
sectors_reserved dw 0x0002 ; Number of sectors reserved (in this case 1 for MARBLE, the rest for R)
fats db 0x02 ; Number of file allocation tables
max_root_entries dw 0x0200 ; Max number of root entries
sectors dw 0x1040 ; Number of sectors
medium_type db 0xF0 ; Type of medium (removable or fixed?)
sectors_fat dw 0x0010 ; Sectors per file allocation table
sectors_track dw 0x0020 ; Sectors per track
heads dw 0x0002 ; Number of heads
hidden_sectors dd 0x00000000 ; Number of sectors before partition
total_sectors dd 0x00000000 ; Number of sectors in medium (zero because 2B != 0)
drive_number db 0x00 ; Drive number (for BIOS int 0x13)
drive_signature db 0x00 ; NOT USED
ext_signature db 0x29 ; Extended boot signature
volume_serial dd 0x00000000 ; Volume's serial number
volume_label db "HOUSEHOUSE "; Volume label
fs_type db "FAT16 " ; Filesystem type
main:
mov sp, 0x1000 ; 4K of stack
mov ax, word [sectors_reserved] ; Read all reserved sectors
sub al, 0x01 ; Except this one
mov ah, 0x02 ; Read disk sectors
mov bx, r_buffer ; Read contents into our buffer
mov ch, 0x00 ; Cylinder 0
mov cl, 0x02 ; Sector 2
mov dh, 0x00 ; Head 0
jmp .read ; Read the disk
.reset:
pusha ; Push register states to stack
call lps
mov ah, 0x00 ; Reset disk
int 0x13 ; BIOS disk interrupt
jnc .read ; If successsul, read again
call lps
mov ah, 0x00 ; Otherwise, prepare to reboot
int 0x19 ; Reboot
.read:
call lps
int 0x13 ; BIOS disk interrupt
jc .reset ; If failed, reset disk
jmp r_buffer ; Otherwise, jump to R
lps: ; Debug message
pusha
mov ah, 0x0E
mov al, 0x52
mov bh, 0x00
int 0x10
mov ah, 0x00
int 0x16
popa
ret
times 510-($-$$) db 0x00 ; Pad remainder of boot sector with zeros
sig dw 0xAA55 ; Boot signature
r_buffer: ; Space in memory for loading R
r_start: ; Beginning of second sector
mov ax, 0x07C0
add ax, 0x0220
mov ss, ax
mov ax, 0x07C0
mov ds, ax
mov si, success ; Successful
call print_str ; Print!
hlt ; Halt here
print_str: ; Prints string pointed to by REGISTER SI to cursor location (si=str)
pusha ; Push register states to stack
mov ah, 0x0E ; Print in teletype mode
mov bh, 0x00 ; Page 0
.repeat:
lodsb ; Load next character from si
cmp al, 0x00 ; Is this a null character?
je .ret ; If it is, return to caller
int 0x10 ; Otherwise, BIOS interrupt
jmp .repeat ; Do the same thing
.ret:
popa ; Restore register states
ret ; Return to caller
.data:
success db "!", 0x00 ; Success!
times 1024-($-$$) db 0x00 ; Pad remainder of reserved sectors with zeros
正如我所说,第二扇区的代码应该是运行,但由于磁盘重置失败,这不会发生。
@ecm 了解我看到的大部分内容。在访问任何数据之前,您需要设置段寄存器并确保您有一个适合您在段寄存器中加载的值的 ORG
(原点)(尤其是 DS).默认情况下,当没有 ORG
指令(或等效指令)时,默认值为 0x0000.
在实模式下,每个逻辑地址都由两个部分组成——一个段和一个偏移量。 CPU计算出的物理地址是根据公式(段*16)+偏移量计算出来的。将 ORG
视为起始偏移量。如果您使用 0x0000 段,则需要 0x7c00 的偏移量。 (0x0000*16)+0x7c00=0x7c00。 0x7c00 是引导加载程序开始的地方。由于您正在将扇区写入 0x7e00 以上的内存,因此将堆栈设置为 0x0000:0x7c00 以从引导加载程序下方向内存开头向下增长会更简单。
由于您正在使用像 LODSB
这样的字符串指令,您应该确保使用 CLD
指令清除方向标志 (DF),以便字符串操作在内存中向前推进。当您的引导加载程序开始执行时,您不能指望 DF 是清晰的。
读取磁盘时Int 13h/AH=2 clobbers AX。如果出现错误,您需要重新加载 AH 2 和 AL 要读取的扇区数。如果您修改代码,您可以使用 SI 来存储要临时读取的扇区数。通常您不必检查磁盘重置是否失败,只需重新执行读取操作即可。尝试操作几次,然后进入失败状态/重启。我修改了您的代码以在 DI 中放置重试计数。每次重置完成时,重试计数减 1。如果重试计数 >= 0,则尝试读取。如果重试次数 <= 0,则它会重新启动。
当跳转到读取保留扇区的内存地址 0x7e00 处的代码时,您可以借此机会通过使用 FAR JMP 确保 CS 也设置为 0x0000 .
如果在引导扇区中将段设置为 0x0000,则可以在第二阶段重用它们。如果您打算从引导加载程序进入保护模式,我不建议使用 0x0000 以外的段值,以便前 64KiB 中的逻辑地址和物理地址相同。这在加载 GDT 并跳转到保护模式时非常有用。
使用 HLT
时,最好确保中断已关闭(使用 CLI
),否则 HLT
将在下一个中断时退出,处理器将继续执行在那之后发生了什么。在实际硬件上,即使中断关闭,也可能会发生不可屏蔽中断 (NMI),因此将 HLT
放入循环中是个好主意。
考虑到这些变化,这段代码应该可以工作:
BITS 16
org 0x7c00
jmp strict short main ; Jump to main bootloader
nop ; Pad out remaining bytes until boot descriptor
; Disk descriptor
OEM_name db "HOUSE " ; Disk label
bytes_sector dw 0x0200 ; Bytes per sector
sectors_cluster db 0x01 ; Sectors per cluster
sectors_reserved dw 0x0002 ; Number of sectors reserved (in this case
; 1 for MARBLE, the rest for R)
fats db 0x02 ; Number of file allocation tables
max_root_entries dw 0x0200 ; Max number of root entries
sectors dw 0x1040 ; Number of sectors
medium_type db 0xF0 ; Type of medium (removable or fixed?)
sectors_fat dw 0x0010 ; Sectors per file allocation table
sectors_track dw 0x0020 ; Sectors per track
heads dw 0x0002 ; Number of heads
hidden_sectors dd 0x00000000 ; Number of sectors before partition
total_sectors dd 0x00000000 ; Number of sectors in medium (zero because 2B != 0)
drive_number db 0x00 ; Drive number (for BIOS int 0x13)
drive_signature db 0x00 ; NOT USED
ext_signature db 0x29 ; Extended boot signature
volume_serial dd 0x00000000 ; Volume's serial number
volume_label db "HOUSEHOUSE "; Volume label
fs_type db "FAT16 " ; Filesystem type
main:
xor ax, ax ; AX = 0
mov ds, ax ; DS = ES = 0
mov es, ax
mov ss, ax ; SS:SP = 0x0000:0x7c00 (grows down below bootloader)
mov sp, 0x7c00
cld ; Set forward direction for string instructions
mov si, word [sectors_reserved] ; Read all reserved sectors
dec si ; Except this one. SI = sectors to read
mov di, 3 ; retry count of 3 and then give up
mov bx, r_buffer ; Read contents into our buffer
mov ch, 0x00 ; Cylinder 0
mov cl, 0x02 ; Sector 2
mov dh, 0x00 ; Head 0
jmp .read ; Read the disk
.reset:
call lps
dec di ; Reduce retry count
jge .read ; If retry >= 0 then try again
; Otherwise retry count exceeded - reboot
mov ah, 0x00
int 0x19 ; Reboot
.read:
mov ax, si ; Transfer sector read count to AX
mov ah, 0x02 ; Read disk sectors
call lps
int 0x13 ; BIOS disk interrupt
jc .reset ; If failed, reset disk
jmp 0x0000:r_start ; Otherwise, jump to r_start. Set CS=0
lps: ; Debug message
pusha
mov ah, 0x0E
mov al, 0x52
mov bh, 0x00
int 0x10
mov ah, 0x00
int 0x16
popa
ret
times 510-($-$$) db 0x00 ; Pad remainder of boot sector with zeros
sig dw 0xAA55 ; Boot signature
r_buffer: ; Space in memory for loading R
r_start: ; Beginning of second sector
mov si, success ; Successful
call print_str ; Print!
cli
.endloop:
hlt ; Halt here
jmp .endloop
print_str: ; Prints string pointed to by REGISTER SI
; to cursor location (si=str)
pusha ; Push register states to stack
mov ah, 0x0E ; Print in teletype mode
mov bh, 0x00 ; Page 0
.repeat:
lodsb ; Load next character from si
cmp al, 0x00 ; Is this a null character?
je .ret ; If it is, return to caller
int 0x10 ; Otherwise, BIOS interrupt
jmp .repeat ; Do the same thing
.ret:
popa ; Restore register states
ret ; Return to caller
.data:
success db "!", 0x00 ; Success!
times 1024-($-$$) db 0x00 ; Pad remainder of reserved sectors with zeros
观察和建议
- 对于引导加载程序开发,我强烈建议使用 BOCHS 进行初始测试。内置的调试器是实模式感知的,并且理解实模式分段,而 QEMU/GDB 则不然。学习使用调试工具是一项宝贵的技能。