引导加载程序在 qemu 中工作,但在 virtualbox 和硬件上失败
bootloader works in qemu but fails in virtualbox and on hardware
我的引导加载程序由两个 512 字节的阶段组成。阶段 1 由 bios 加载到 MBR 区域。
stage1 然后继续从驱动器加载 stage2 并跳转到它。
我用十六进制编辑器确认最终二进制“program.bin”的大小正好是 1024 字节长并且包含两个“签名”(每个阶段的最后两个字节,0xAA55 用于阶段 1(MBR 签名)阶段 2 为 0xCC77)。
预期产出是:
1 // stage1 started
0000 or 0080 // drive# in hex
CC77 // stage2 "signature" in hex
2 // stage2 started
这在 QEMU 中工作正常,但在 virtualbox 和硬件上失败。
在我看来,stage2 加载无声地失败(没有调用错误分支),我希望解决这个问题两周但没有成功。
QEMU 输出
硬件和虚拟盒输出
stage1.asm:
global _start
extern _stage2
extern _stage2data
BITS 16
_start:
; init registers
xor ax, ax
mov es, ax
mov gs, ax
mov ss, ax
mov sp, 0x7C00 ; right before MBR, counting upwards
mov ax, 0x7C0 ; set DS to 0x7c0 so pointing at 0x0 resolves to 0x7C0:x0000 = 0x7C00
mov ds, ax
cld ; set direction flag to make string operations count forward
; mark start of stage 1 by printing "1"
mov al, '1'
call real_mode_print_char
call real_mode_new_line
print_drive_number:
; drive# is put into DL by BIOS
mov dh, 0x0
mov bx, dx
call real_mode_print_hex
load_sector2:
mov al, 0x01 ; load 1 sector
mov bx, 0x7E00 ; destination, right after your bootloader
mov cx, 0x0002 ; cylinder 0, sector 2
; mov dl, [BootDrv] ; boot drive
xor dh, dh ; head 0
call read_sectors_16
jnc execute_stage2 ; if carry flag is set, disk read failed
jmp error
execute_stage2:
mov bx, [_stage2data] ; print data at _stage2data to confirm stage 2 was loaded
call real_mode_print_hex
jmp _stage2 ; start execude instructions of _stage2
error:
; print "E" if an error occurs
mov al, 'E'
call real_mode_print_char
; infinite loop
loop:
jmp loop
; read_sectors_16
;
; Reads sectors from disk into memory using BIOS services
;
; input: dl = drive
; ch = cylinder[7:0]
; cl[7:6] = cylinder[9:8]
; dh = head
; cl[5:0] = sector (1-63)
; es:bx -> destination
; al = number of sectors
;
; output: cf (0 = success, 1 = failure)
read_sectors_16:
push ax
mov si, 0x02 ; maximum attempts - 1
.top:
mov ah, 0x02 ; read sectors into memory (int 0x13, ah = 0x02)
int 0x13
jnc .end ; exit if read succeeded
dec si ; decrement remaining attempts
jc .end ; exit if maximum attempts exceeded
xor ah, ah ; reset disk system (int 0x13, ah = 0x00)
int 0x13
jnc .top ; retry if reset succeeded, otherwise exit
.end:
pop ax
retn
# print a number in hex
# IN
# bx: the number
# CLOBBER
# al, cx
real_mode_print_hex:
mov cx, 4
.lp:
mov al, bh
shr al, 4
cmp al, 0xA
jb .below_0xA
add al, 'A' - 0xA - '0'
.below_0xA:
add al, '0'
call real_mode_print_char
shl bx, 4
loop .lp
call real_mode_new_line
ret
real_mode_new_line:
mov al, 0x0D
call real_mode_print_char
mov al, 0x0A
call real_mode_print_char
ret
real_mode_print_char:
push bx
xor bx, bx ; Attribute=0/Current Video Page=0
mov ah, 0x0e
int 0x10 ; Display character
pop bx
ret
; boot signature
TIMES 510-($-$$) db 0
mbr_id:
dw 0xAA55
stage2.asm:
global _stage2
global _stage2data
BITS 16
_stage2:
mov al, '2'
call bios_print_char
loop:
jmp loop
bios_print_char:
push bx
xor bx, bx ; Attribute=0/Current Video Page=0
mov ah, 0x0e
int 0x10 ; Display character
pop bx
ret
; boot signature
TIMES 510-($-$$) db 0
_stage2data:
dw 0xCC77
linker 脚本“linker.ld”:
ENTRY(_start)
OUTPUT_FORMAT(binary)
SECTIONS
{
output :
{
stage1.elf(.text)
stage2.elf(.text)
}
}
我使用以下命令编译和 link 所有内容:
nasm -f elf64 stage1.asm -o stage1.elf
nasm -f elf64 stage2.asm -o stage2.elf
ld -m elf_x86_64 -o program.bin stage2.elf stage1.elf -nostdlib -T linker.ld
i 运行 QEMU 上的二进制文件:
qemu-system-x86_64 -drive format=raw,file=program.bin
到运行它在硬件上我将二进制文件写入USB:
dd if=program.bin of=/dev/sdb1 && sync
你的引导装载程序实际上看起来很不错。正如@jester 在真实硬件上指出的那样,如果您使用软盘仿真 (FDD) 启动 USB,那么您很可能需要 。屏幕截图中有迹象表明您正在将 USB 作为硬盘仿真 (HDD) 启动,因为驱动器编号似乎是 0x0080。如果是这种情况,则不需要 BPB。
使用 USB HDD 仿真时,您可能需要一个分区 table,其中一个分区标记为 active/bootable,以便某些 BIOS 将驱动器识别为 bootable。如果没有它,一些 BIOS 可能会拒绝将驱动器识别为它应该引导的东西,即使它在最后 2 个字节中具有正确的磁盘签名 (0xaa55
)。
我认为真正的问题在于您写入 USB 驱动器的方式。您正在使用:
dd if=program.bin of=/dev/sdb1 && sync
/dev/sdb1
其实是第一个分区,不是驱动器的开头。看起来你想要的是写入驱动器的开头:
dd if=program.bin of=/dev/sdb && sync
你可能会问:你写的bootloader不写到驱动器的开头怎么会是运行呢?我怀疑您的 USB 驱动器在分区 1 中使用 Master Boot Record (MBR) that chain loaded the Volume Boot Record (VBR) 引导加载程序进行了格式化,然后开始执行它。如果 U 盘在 Windows 中被格式化和分区,那么这种链式加载 MBR 是很有可能的。 Windows 通常将 USB 格式化为一个大分区,并将 MBR 放在驱动器的第一个扇区,从第一个分区的第一个扇区链式加载 VBR。
其他观察结果
由于您似乎将所有内容都作为硬盘媒体启动,因此在处理较大的媒体(通常大于 8GiB)时,您可能希望考虑使用 Int 13h/AH=42h rather than Int 13h/AH=2h. Int 13/AH=2 is very limited in what it can load with CHS addressing rather than LBA addressing 等扩展磁盘功能。
我的引导加载程序由两个 512 字节的阶段组成。阶段 1 由 bios 加载到 MBR 区域。 stage1 然后继续从驱动器加载 stage2 并跳转到它。
我用十六进制编辑器确认最终二进制“program.bin”的大小正好是 1024 字节长并且包含两个“签名”(每个阶段的最后两个字节,0xAA55 用于阶段 1(MBR 签名)阶段 2 为 0xCC77)。
预期产出是:
1 // stage1 started
0000 or 0080 // drive# in hex
CC77 // stage2 "signature" in hex
2 // stage2 started
这在 QEMU 中工作正常,但在 virtualbox 和硬件上失败。 在我看来,stage2 加载无声地失败(没有调用错误分支),我希望解决这个问题两周但没有成功。
QEMU 输出
硬件和虚拟盒输出
stage1.asm:
global _start
extern _stage2
extern _stage2data
BITS 16
_start:
; init registers
xor ax, ax
mov es, ax
mov gs, ax
mov ss, ax
mov sp, 0x7C00 ; right before MBR, counting upwards
mov ax, 0x7C0 ; set DS to 0x7c0 so pointing at 0x0 resolves to 0x7C0:x0000 = 0x7C00
mov ds, ax
cld ; set direction flag to make string operations count forward
; mark start of stage 1 by printing "1"
mov al, '1'
call real_mode_print_char
call real_mode_new_line
print_drive_number:
; drive# is put into DL by BIOS
mov dh, 0x0
mov bx, dx
call real_mode_print_hex
load_sector2:
mov al, 0x01 ; load 1 sector
mov bx, 0x7E00 ; destination, right after your bootloader
mov cx, 0x0002 ; cylinder 0, sector 2
; mov dl, [BootDrv] ; boot drive
xor dh, dh ; head 0
call read_sectors_16
jnc execute_stage2 ; if carry flag is set, disk read failed
jmp error
execute_stage2:
mov bx, [_stage2data] ; print data at _stage2data to confirm stage 2 was loaded
call real_mode_print_hex
jmp _stage2 ; start execude instructions of _stage2
error:
; print "E" if an error occurs
mov al, 'E'
call real_mode_print_char
; infinite loop
loop:
jmp loop
; read_sectors_16
;
; Reads sectors from disk into memory using BIOS services
;
; input: dl = drive
; ch = cylinder[7:0]
; cl[7:6] = cylinder[9:8]
; dh = head
; cl[5:0] = sector (1-63)
; es:bx -> destination
; al = number of sectors
;
; output: cf (0 = success, 1 = failure)
read_sectors_16:
push ax
mov si, 0x02 ; maximum attempts - 1
.top:
mov ah, 0x02 ; read sectors into memory (int 0x13, ah = 0x02)
int 0x13
jnc .end ; exit if read succeeded
dec si ; decrement remaining attempts
jc .end ; exit if maximum attempts exceeded
xor ah, ah ; reset disk system (int 0x13, ah = 0x00)
int 0x13
jnc .top ; retry if reset succeeded, otherwise exit
.end:
pop ax
retn
# print a number in hex
# IN
# bx: the number
# CLOBBER
# al, cx
real_mode_print_hex:
mov cx, 4
.lp:
mov al, bh
shr al, 4
cmp al, 0xA
jb .below_0xA
add al, 'A' - 0xA - '0'
.below_0xA:
add al, '0'
call real_mode_print_char
shl bx, 4
loop .lp
call real_mode_new_line
ret
real_mode_new_line:
mov al, 0x0D
call real_mode_print_char
mov al, 0x0A
call real_mode_print_char
ret
real_mode_print_char:
push bx
xor bx, bx ; Attribute=0/Current Video Page=0
mov ah, 0x0e
int 0x10 ; Display character
pop bx
ret
; boot signature
TIMES 510-($-$$) db 0
mbr_id:
dw 0xAA55
stage2.asm:
global _stage2
global _stage2data
BITS 16
_stage2:
mov al, '2'
call bios_print_char
loop:
jmp loop
bios_print_char:
push bx
xor bx, bx ; Attribute=0/Current Video Page=0
mov ah, 0x0e
int 0x10 ; Display character
pop bx
ret
; boot signature
TIMES 510-($-$$) db 0
_stage2data:
dw 0xCC77
linker 脚本“linker.ld”:
ENTRY(_start)
OUTPUT_FORMAT(binary)
SECTIONS
{
output :
{
stage1.elf(.text)
stage2.elf(.text)
}
}
我使用以下命令编译和 link 所有内容:
nasm -f elf64 stage1.asm -o stage1.elf
nasm -f elf64 stage2.asm -o stage2.elf
ld -m elf_x86_64 -o program.bin stage2.elf stage1.elf -nostdlib -T linker.ld
i 运行 QEMU 上的二进制文件:
qemu-system-x86_64 -drive format=raw,file=program.bin
到运行它在硬件上我将二进制文件写入USB:
dd if=program.bin of=/dev/sdb1 && sync
你的引导装载程序实际上看起来很不错。正如@jester 在真实硬件上指出的那样,如果您使用软盘仿真 (FDD) 启动 USB,那么您很可能需要
使用 USB HDD 仿真时,您可能需要一个分区 table,其中一个分区标记为 active/bootable,以便某些 BIOS 将驱动器识别为 bootable。如果没有它,一些 BIOS 可能会拒绝将驱动器识别为它应该引导的东西,即使它在最后 2 个字节中具有正确的磁盘签名 (0xaa55
)。
我认为真正的问题在于您写入 USB 驱动器的方式。您正在使用:
dd if=program.bin of=/dev/sdb1 && sync
/dev/sdb1
其实是第一个分区,不是驱动器的开头。看起来你想要的是写入驱动器的开头:
dd if=program.bin of=/dev/sdb && sync
你可能会问:你写的bootloader不写到驱动器的开头怎么会是运行呢?我怀疑您的 USB 驱动器在分区 1 中使用 Master Boot Record (MBR) that chain loaded the Volume Boot Record (VBR) 引导加载程序进行了格式化,然后开始执行它。如果 U 盘在 Windows 中被格式化和分区,那么这种链式加载 MBR 是很有可能的。 Windows 通常将 USB 格式化为一个大分区,并将 MBR 放在驱动器的第一个扇区,从第一个分区的第一个扇区链式加载 VBR。
其他观察结果
由于您似乎将所有内容都作为硬盘媒体启动,因此在处理较大的媒体(通常大于 8GiB)时,您可能希望考虑使用 Int 13h/AH=42h rather than Int 13h/AH=2h. Int 13/AH=2 is very limited in what it can load with CHS addressing rather than LBA addressing 等扩展磁盘功能。