首页 文章

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

提问于
浏览
6

我对汇编很新,但是我试图学习如何编写将作为引导加载程序代码运行的汇编代码;所以独立于任何其他操作系统,如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汇编程序汇编代码 . 在生成.bin文件之后,我使用十六进制编辑器打开它 . 我使用相同的十六进制编辑器将该.bin文件的内容复制到闪存驱动器的前512个字节中 . 将程序写入闪存驱动器后,我将其断开并将其插入装有Intel Core i3-7100的计算机 . 在启动时,我选择了我的USB闪存驱动器作为启动设备,只是为了得到以下输出:

A

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

AAAAAAAAAAB

我立即在i3的原始计算机上尝试过它,但它仍然没有用 .

所以我的问题是:为什么我的程序使用一个x86处理器而不是另一个?它们都支持x86指令集 . 是什么赋予了?


Solution:
好的,我在下面的回答中,你会找到一个解决方案来解决我的问题,以及BIOS寻找BPB的另一个问题 .

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

jmp start
resb 0x50

start: 
;enter code here

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

还要记住,每当调用任何子例程时,您使用的寄存器的值都可能被覆盖 . 确保在调用子例程之前使用 pushpop 或将值保存到内存中 . 请看Martin Rosenau在下面的回答,了解更多相关信息 .

感谢所有回复我问题的人 . 我现在对这种低级别的东西如何运作有了更好的理解 .

2 回答

  • 4

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

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

    如果您尝试使用USB在真实硬件上启动,那么即使您在BOCHS和QEMU中工作,也可能会遇到另一个问题 . 如果您的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]
    

    如果您只需要默认的0x0000,请将 ORG 指令调整为您需要的或省略它 .

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

    disk.img:DOS / MBR引导扇区,代码偏移量0x3c 2,OEM-ID“mkfs.fat”,根条目224,扇区2880(卷<= 32 MB),扇区/ FAT 9,扇区/磁道18,串行数字0x2d7e5a1a,未标记,FAT(12位)


    如何修改本课程中的代码

    在这个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的其他人的一般观察 . 我有更多的信息在我的Stackoverflow答案中包含General Bootloader Tips .


    测试代码以查看您的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 bx, 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

  • 6

    汇编代码仅适用于我的两个x86处理器之一

    它不是处理器而是BIOS:

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

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

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

    仅获得以下输出

    我怀疑这里的问题是第一台计算机上的 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将再次启动计算机,这意味着它将从USB记忆棒加载代码并再次执行 .

    你会得到以下结果:

    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
    

相关问题