通过 USB 驱动器启动的自定义引导加载程序在某些计算机上产生不正确的输出

Custom bootloader booted via USB drive produces incorrect output on some computers

我对汇编还很陌生,但我正在尝试深入低级计算领域。我正在尝试学习如何编写 运行 作为引导加载程序代码的汇编代码;如此独立于任何其他 OS,如 Linux 或 Windows。在阅读 this page 和其他一些 x86 指令集列表后,我想出了一些汇编代码,应该在屏幕上打印 10 个 A,然后打印 1 个 B。

      BITS 16
start: 
    mov ax, 07C0h       ; Set up 4K stack space after this bootloader
    add ax, 288     ; (4096 + 512) / 16 bytes per paragraph
    mov ss, ax
    mov sp, 4096

    mov ax, 07C0h       ; Set data segment to where we're loaded
    mov ds, ax

    mov cl, 10          ; Use this register as our loop counter
    mov ah, 0Eh         ; This register holds our BIOS instruction

.repeat:
    mov al, 41h         ; Put ASCII 'A' into this register
    int 10h             ; Execute our BIOS print instruction
    cmp cl, 0           ; Find out if we've reached the end of our loop
    dec cl              ; Decrement our loop counter
    jnz .repeat         ; Jump back to the beginning of our loop
    jmp .done           ; Finish the program when our loop is done

.done:
    mov al, 42h         ; Put ASCII 'B' into this register
    int 10h             ; Execute BIOS print instruction
    ret


times 510-($-$$) db 0   ; Pad remainder of boot sector with 0s
dw 0xAA55

所以输出应该是这样的:

AAAAAAAAAAB

我在 Windows 10 Ubuntu Bash 程序上使用 nasm 汇编程序 运行ning 汇编了代码。生成 .bin 文件后,我使用十六进制编辑器将其打开。我使用相同的十六进制编辑器将该 .bin 文件的内容复制到闪存驱动器的前 512 个字节中。将程序写入闪存驱动器后,我将其断开连接并将其插入配备 Intel Core i3-7100 的计算机。在启动时,我选择我的 USB 闪存驱动器作为启动设备,只得到以下输出:

A

在程序中更改了各种内容后,我终于感到沮丧,并在另一台计算机上尝试了该程序。另一台电脑是配备 i5-2520m 的笔记本电脑。我遵循了与之前提到的相同的过程。果然,它给了我预期的输出:

AAAAAAAAAAB

我立即在装有 i3 的原始计算机上进行了尝试,但仍然无法正常工作。

所以我的问题是:为什么我的程序适用于一个 x86 处理器,但不能适用于另一个?它们都支持 x86 指令集。给出了什么?


解法:
好的,我已经能够在一些帮助下找到真正的解决方案。如果您在下面阅读 Michael Petch 的回答,您会找到解决我的问题的解决方案,以及 BIOS 寻找 BPB 的另一个问题。

这是我的代码的问题:我正在将程序写入闪存驱动器的第一个字节。这些字节被加载到内存中,但一些 BIOS 中断正在为自己使用这些字节。所以我的程序被 BIOS 覆盖了。为防止这种情况,您可以添加 BPB 描述,如下所示。如果你的 BIOS 和我的一样工作,它只会覆盖内存中的 BPB,而不是你的程序。或者,您可以将以下代码添加到程序的顶部:

jmp start
resb 0x50

start: 
;enter code here

此代码(由 Ross Ridge 提供)会将您的程序推送到内存位置 0x50(从 0x7c00 偏移),以防止它在执行期间被 BIOS 覆盖。

另外请记住,无论何时调用任何子程序,您正在使用的寄存器的值都可能被覆盖。在调用子例程之前,请确保您使用 pushpop 或将您的值保存到内存中。查看下面 Martin Rosenau 的回答以了解更多相关信息。

感谢所有回答我问题的人。我现在对这些低级的东西是如何工作的有了更好的理解。

Assembly code only works on one of my two x86 processors

不是处理器而是 BIOS:

int指令实际上是call指令的特殊变体。该指令调用一些子例程(通常用汇编程序编写)。

(您甚至可以用自己的子例程替换该子例程 - 例如,这实际上是由 MS-DOS 完成的。)

在两台计算机上,您有两个不同的 BIOS 版本(甚至供应商),这意味着 int 10h 指令调用的子例程是由不同的程序员编写的,因此不会完全相同。

only to get the following output

我怀疑这里的问题是第一台计算机上 int 10h 调用的子例程不保存寄存器值,而第二台计算机上的例程保存寄存器值。

换句话说:

在第一台计算机上,int 10h 调用的例程可能如下所示:

...
mov cl, 5
mov ah, 6
...

...所以在 int 10h 调用后 ah 寄存器不再包含值 0Eh 并且 cl 寄存器甚至可能是这种情况被修改(然后将以无限循环结束)。

为避免该问题,您可以使用 push 保存 cl 寄存器(您必须保存整个 cx 寄存器)并在 int 指令后恢复它.您还必须在每次调用 int 10h 子例程之前设置 ah 寄存器的值,因为您无法确定它从那时起就没有修改过:

push cx
mov ah, 0Eh
int 10h
pop cx

mov sp, ... ... ret

请思考 Peter Cordes 的评论:

ret 指令是如何工作的,它与 spss 寄存器有什么关系?

这里的ret指令肯定不会如你所愿!

在软盘上,引导扇区通常包含以下代码:

mov ax, 0  ; (may be written as "xor ax, ax")
int 16h
int 19h

int 19h 完全符合您对 ret 指令的期望。

但是 BIOS 将再次启动计算机,这意味着它将从您的 U 盘加载代码并再次执行。

您将得到以下结果:

AAAAABAAAAABAAAAABAAAAAB...

因此插入了int 16h指令。当 ax 寄存器的值为 0 时,这将等待用户在键盘上按下一个键,然后再调用 int 16h 子例程。

或者,您可以简单地添加一个无限循环:

.endlessLoop:
    jmp .endlessLoop

mov ss, ...

当这两条指令之间发生中断时:

mov ss, ax
    ; <--- Here
mov sp, 4096

... spss 寄存器的组合不代表值的 "valid" 表示。

如果你运气不好,中断会将数据写入内存中你不需要的地方。它甚至可能会覆盖您的程序!

因此,您通常会在修改 ss 寄存器时锁定中断:

cli          ; Forbid interrupts
mov ss, ax
mov sp, 4096
sti          ; Allow interrupts again

这可能会成为关于这个主题的规范答案。

真实硬件/USB/笔记本电脑问题

如果您尝试使用 USB 在真实硬件上启动,那么您可能会遇到另一个问题,即使您在 BOCHSQEMU。如果您的 BIOS 设置为执行 USB FDD 仿真(而不是 USB HDD 或其他),您可能需要在引导加载程序的开头添加 BIOS Parameter Block(BPB)。你可以像这样创建一个假的:

org 0x7c00
bits 16

boot:
    jmp main
    TIMES 3-($-$$) DB 0x90   ; Support 2 or 3 byte encoded JMPs before BPB.

    ; Dos 4.0 EBPB 1.44MB floppy
    OEMname:           db    "mkfs.fat"  ; mkfs.fat is what OEMname mkdosfs uses
    bytesPerSector:    dw    512
    sectPerCluster:    db    1
    reservedSectors:   dw    1
    numFAT:            db    2
    numRootDirEntries: dw    224
    numSectors:        dw    2880
    mediaType:         db    0xf0
    numFATsectors:     dw    9
    sectorsPerTrack:   dw    18
    numHeads:          dw    2
    numHiddenSectors:  dd    0
    numSectorsHuge:    dd    0
    driveNum:          db    0
    reserved:          db    0
    signature:         db    0x29
    volumeID:          dd    0x2d7e5a1a
    volumeLabel:       db    "NO NAME    "
    fileSysType:       db    "FAT12   "

main:
    [insert your code here]

根据需要调整 ORG 指令,如果您只需要默认的 0x0000,则忽略它。

如果您要修改您的代码以使布局高于 Unix/Linux file 命令可能能够转储出它认为在磁盘映像中构成您的 VBR 的 BPB 数据。 运行 命令 file disk.img 你可能会得到这个输出:

disk.img: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", root entries 224, sectors 2880 (volumes <=32 MB) , sectors/FAT 9, sectors/track 18, serial number 0x2d7e5a1a, unlabeled, FAT (12 bit)


如何修改此问题中的代码

对于此 OP 的原始代码,可以将其修改为如下所示:

bits 16

boot:
    jmp main
    TIMES 3-($-$$) DB 0x90   ; Support 2 or 3 byte encoded JMPs before BPB.

    ; Dos 4.0 EBPB 1.44MB floppy
    OEMname:           db    "mkfs.fat"  ; mkfs.fat is what OEMname mkdosfs uses
    bytesPerSector:    dw    512
    sectPerCluster:    db    1
    reservedSectors:   dw    1
    numFAT:            db    2
    numRootDirEntries: dw    224
    numSectors:        dw    2880
    mediaType:         db    0xf0
    numFATsectors:     dw    9
    sectorsPerTrack:   dw    18
    numHeads:          dw    2
    numHiddenSectors:  dd    0
    numSectorsHuge:    dd    0
    driveNum:          db    0
    reserved:          db    0
    signature:         db    0x29
    volumeID:          dd    0x2d7e5a1a
    volumeLabel:       db    "NO NAME    "
    fileSysType:       db    "FAT12   "

main:
    mov ax, 07C0h       ; Set up 4K stack space after this bootloader
    add ax, 288     ; (4096 + 512) / 16 bytes per paragraph
    mov ss, ax
    mov sp, 4096

    mov ax, 07C0h       ; Set data segment to where we're loaded
    mov ds, ax

    mov cl, 10          ; Use this register as our loop counter
    mov ah, 0Eh         ; This register holds our BIOS instruction

.repeat:
    mov al, 41h         ; Put ASCII 'A' into this register
    int 10h             ; Execute our BIOS print instruction
    cmp cl, 0           ; Find out if we've reached the end of our loop
    dec cl              ; Decrement our loop counter
    jnz .repeat         ; Jump back to the beginning of our loop
    jmp .done           ; Finish the program when our loop is done

.done:
    mov al, 42h         ; Put ASCII 'B' into this register
    int 10h             ; Execute BIOS print instruction
    ret

times 510-($-$$) db 0   ; Pad remainder of boot sector with 0s
dw 0xAA55

其他建议

正如已经指出的那样 - 您不能 ret 结束引导加载程序。您可以将其放入无限循环或使用 cli 后跟 hlt 停止处理器。

如果您曾在堆栈上分配大量数据或开始写入引导加载程序 512 字节以外的数据,您应该设置自己的堆栈指针 (SS:SP ) 到不会干扰您自己的代码的内存区域。这个问题中的原始代码确实设置了一个堆栈指针。对于阅读本文的其他人来说,这是一个普遍的观察 Q/A。我在包含 .

的 Whosebug 答案中有更多相关信息

测试代码以查看您的 BIOS 是否正在覆盖 BPB

如果您想知道 BIOS 是否可能正在覆盖 BPB 中的数据并确定它写入了哪些值,您可以使用此引导加载程序代码来转储 BPB,因为引导加载程序在控制权转移给它后会看到它。一般情况下前3个字节应该是EB 3C 90后面跟着一连串的AA。任何不是 AA 的值都可能被 BIOS 覆盖。此代码在 NASM 中,可以用 nasm -f bin boot.asm -o boot.bin

组装成引导加载程序
; Simple bootloader that dumps the bytes in the BIOS Parameter
; Block BPB. First 3 bytes should be EB 3C 90. The rest should be 0xAA
; unless you have a BIOS that wrote drive geometry information
; into what it thinks is a BPB.

; Macro to print a character out with char in BX
%macro print_char 1
    mov al, %1
    call bios_print_char
%endmacro

org 0x7c00
bits 16

boot:
    jmp main
    TIMES 3-($-$$) DB 0x90   ; Support 2 or 3 byte encoded JMPs before BPB.

    ; Fake BPB filed with 0xAA
    TIMES 59 DB 0xAA

main:
    xor ax, ax
    mov ds, ax
    mov ss, ax              ; Set stack just below bootloader at 0x0000:0x7c00
    mov sp, boot
    cld                     ; Forward direction for string instructions

    mov si, sp              ; Print bytes from start of bootloader
    mov cx, main-boot       ; Number of bytes in BPB
    mov dx, 8               ; Initialize column counter to 8
                            ;     So first iteration prints address
.tblloop:
    cmp dx, 8               ; Every 8 hex value print CRLF/address/Colon/Space
    jne .procbyte
    print_char 0x0d         ; Print CRLF
    print_char 0x0a
    mov ax, si              ; Print current address
    call print_word_hex
    print_char ':'          ; Print ': '
    print_char ' '
    xor dx, dx              ; Reset column counter to 0
.procbyte:
    lodsb                   ; Get byte to print in AL
    call print_byte_hex     ; Print the byte (in BL) in HEX
    print_char ' '
    inc dx                  ; Increment the column count
    dec cx                  ; Decrement number of  bytes to process
    jnz .tblloop

    cli                     ; Halt processor indefinitely
.end:
    hlt
    jmp .end

; Print the character passed in AL
bios_print_char:
    push bx
    xor bx, bx              ; Attribute=0/Current Video Page=0
    mov ah, 0x0e
    int 0x10                ; Display character
    pop bx
    ret

; Print the 16-bit value in AX as HEX
print_word_hex:
    xchg al, ah             ; Print the high byte first
    call print_byte_hex
    xchg al, ah             ; Print the low byte second
    call print_byte_hex
    ret

; Print lower 8 bits of AL as HEX
print_byte_hex:
    push bx
    push cx
    push ax

    lea bx, [.table]        ; Get translation table address

    ; Translate each nibble to its ASCII equivalent
    mov ah, al              ; Make copy of byte to print
    and al, 0x0f            ;     Isolate lower nibble in AL
    mov cl, 4
    shr ah, cl              ; Isolate the upper nibble in AH
    xlat                    ; Translate lower nibble to ASCII
    xchg ah, al
    xlat                    ; Translate upper nibble to ASCII

    xor bx, bx              ; Attribute=0/Current Video Page=0
    mov ch, ah              ; Make copy of lower nibble
    mov ah, 0x0e
    int 0x10                ; Print the high nibble
    mov al, ch
    int 0x10                ; Print the low nibble

    pop ax
    pop cx
    pop bx
    ret
.table: db "0123456789ABCDEF", 0

; boot signature
TIMES 510-($-$$) db 0
dw 0xAA55

对于在将控制权转移到引导加载程序代码之前未更新 BPB 的任何 BIOS,输出应如下所示:

7C00: EB 3C 90 AA AA AA AA AA
7C08: AA AA AA AA AA AA AA AA
7C10: AA AA AA AA AA AA AA AA
7C18: AA AA AA AA AA AA AA AA
7C20: AA AA AA AA AA AA AA AA
7C28: AA AA AA AA AA AA AA AA
7C30: AA AA AA AA AA AA AA AA
7C38: AA AA AA AA AA AA