【问题标题】:What is causing this bootloader to fail on hardware but not in DOSBOX? It displays all registers是什么导致此引导加载程序在硬件上失败但在 DOSBOX 中失败?它显示所有寄存器
【发布时间】:2020-09-25 18:40:40
【问题描述】:

我最近编写了一个 x86 'bootloader' 程序,它在 BIOS 跳转到我的程序后显示硬件寄存器的值。出于测试的目的,我将AX寄存器设置为一个已知值,以确保程序正确运行。

BITS 16
%macro pad 1-2 0
    times %1 - ($ - $$) db %2
%endmacro
[org 0x7C00]
    CLD                 ; clear direction flag (forward direction)
    CLI                 ; clear interrupt flag (disable interrupts, opposite of 65xx)
    
    MOV [0x8000], AX    ; display all registers,
    MOV [0x8004], BX    ;   including stack,
    MOV [0x8008], CX    ;   segment, & extra
    MOV [0x800C], DX    ;   registers

    MOV [0x8010], SP
    MOV [0x8014], BP
    MOV [0x8018], SI
    MOV [0x801C], DI
    
    MOV [0x8020], CS
    MOV [0x8024], SS    ; we also display DS register,
    MOV [0x8028], ES    ;   so we can't modify it or
    MOV [0x802C], DS    ;   we'll loose our data
    
    MOV [0x8030], FS
    MOV [0x8034], GS
    
    MOV AX, 0x0123      ; write 0x0123 to [0x8000]
    MOV [0x8000], AX    ;   for debugging
    
    MOV DI, 0x804C      ; DI is pointer to address 0x804C
                        ; (temporary data)
    MOV AH, 0x02
    MOV BH, 0x00        ; video page 0?
    MOV DX, 0x0401
    INT 0x10            ; move cursor to XY:($01, $04)

    ; display register data
    MOV AL, 'A'
    CALL printXl        ; print 'AX:'
    MOV DX, [0x8000]    ; recall value of AX register
                        ;   (set to 0x0123 for test)
    CALL printascii     ; print 16-bit value @ [0x8000]
    
    ;...                ; omitted code: display other registers
    
    MOV AH, 0x00        ; wait for keyboard press
    INT 0x16
    
    INT 0x18            ; boot Windows

printXl:
    MOV AH, 0x0E
    XOR BX, BX
    INT 0x10            ; display character in 'AL'
    MOV AL, 'X'
    ; falls through
prnt:                   ; referenced in omitted code
    MOV AH, 0x0E
    INT 0x10            ; display character 'X'/'S'
    MOV AL, ':'
    INT 0x10            ; display character ':'
    RET
    
printascii:
    MOV [DI], DX            ; store value for later recall
    MOV AH, 0x0E            ; INT 10,E
    
    MOV SI, hexascii        ; load address of 'hexascii'
    AND DX, 0xF000
    SHR DX, 0x0C            ; shift high nibble to lowest 4 bits
    ADD SI, DX
    CS LODSB                ; AL = CS:[0x1EE + DX >> 12];
    INT 0x10                ; display high nibble of character value
            
    MOV SI, hexascii
    MOV DX, [DI]
    AND DX, 0x0F00
    SHR DX, 0x08
    ADD SI, DX
    CS LODSB
    INT 0x10                ; display low nibble of character value
            
    MOV SI, hexascii
    MOV DX, [DI]
    AND DX, 0x00F0
    SHR DX, 0x04
    ADD SI, DX
    CS LODSB
    INT 0x10                ; display high nibble of character value
            
    MOV SI, hexascii        ;
    MOV DX, [DI]
    AND DX, 0x000F
    ADD SI, DX
    CS LODSB
    INT 0x10                ; display low nibble of character value
            
    RET
pad 0x01EE
hexascii:
    db "0123456789ABCDEF"   ;
    
pad 0x01FE                  ; pad to end of bootsector
    dw 0xAA55               ; bootsector signature

从 DOSBOX 运行时,我正确地看到了 AX:0123,但是从我的软盘启动时,我看到了 AX:FFFF。我不知道我做错了什么。作为参考,我的电脑是Intel Core 2 Quad

【问题讨论】:

  • 您使用了[org 0x7C00],但从未初始化DS 0。某些 BIOS 可能会以 CS=7c0、IP=0 进入您的 MBR,而谁知道 DS 是什么。
  • int 20h 不是 ROM-BIOS 中断服务。
  • 您也没有向我们展示“pad”宏。 (或者它可能是您的汇编程序内置的?这条lea 行似乎不是有效的 NASM 语法,因此它取决于您使用的汇编程序。)无论如何填充都是错误的,应该填充到 510 (1FEh)。
  • 一个可能的问题:由 ROM-BIOS 加载程序初始化的堆栈可能与您的寄存器存储区域重叠。
  • Boot loader doesn't jump to kernel code 的可能副本,Michael Petch 对引导加载程序的一般提示。回复:内存布局和使用0x8000 地址;如果 DS = 0,可能是安全的。Understanding of boot loader assembly code and memory locations 显示来自 MikeOS 的引导加载程序的内存映射。

标签: assembly x86-16 bootloader dosbox real-mode


【解决方案1】:

在保证 100% 安全的情况下,做自己想做的事是不可能的。

问题在于,要将数据存储在您必须知道将其存储在安全位置的任何地方(不覆盖堆栈,不被堆栈覆盖,不写入 ROM 或其他不是 RAM 的东西)破坏 RAM 中的任何其他内容,例如 BIOS 数据或您的代码);并且您必须先修改寄存器/s(主要是段寄存器),然后才能知道您将数据存储在安全的地方,因此您无法将这些寄存器的原始值安全地存储在任何地方。请注意,这是导致(至少一个)您最初的问题的原因 - 不想更改 DS(因为您想打印其原始值)并且最终不知道使用 DS 是否安全。

“最不安全”的替代方法是(暂时)使用 BIOS 留下的堆栈。 BIOS 可能会在某个有足够空间的地方留下一个堆栈,以确保如果在 BIOS 跳转到您的代码之后但在您可以执行单个指令(或自己设置安全堆栈)之前发生 IRQ,它不会导致问题,因此您可能可以在该堆栈上存储少量数据。然而;无法保证任何中断(包括 IRQ 和 BIOS 函数)消耗的堆栈都不会超过消耗后剩余的堆栈(因此您不希望在堆栈上存储大量数据);理想情况下,您应该在启用 IRQ 或调用任何 BIOS 函数之前将存储在堆栈中的数据传输到其他地方。

这会导致类似以下内容(NASM 语法,未经测试):

    org 0x7C00

start:
    cli
    push ds
    push ax
    xor ax,ax
    mov ds,ax

    call far [.fixCSIP]     ;Push CS and IP then set CS and IP to known values
.fixCSIP:
    dw 0x0000, .here        ;Values to load into CS and IP
.here:

    pop word [0x8020]       ;Move original value of CS

    pop ax                  ;ax = original value of "IP + (.fixCSIP - start)"
    sub ax,.fixCSIP-start   ;ax = original value of IP
    mov [0x8038],ax         ;Store original value of IP

    pop word [0x8000]       ;Move original value of AX
    pop word [0x802C]       ;Move original value of DS

    ;SP is now back to the value it originally had

    mov [0x8010],sp
    mov [0x8024],ss

    xor ax,ax
    mov ss,ax
    mov sp,0x7C00

    ;Now CS:IP, DS and SS:SP are all "known safe" values, so we can start being normal

    sti

    mov [0x8004], bx
    mov [0x8008], cx
    mov [0x800C], dx

    ...
    

【讨论】:

  • push 0 pop ds 作为一种在不破坏寄存器的情况下将 DS 归零的方法怎么样?再次假设您相信堆栈能够保存 2 个字节。
  • @NateEldredge:那也可以(可能会好一点)。
  • 您可以直接call 0:.here,无需添加内存间接。我还建议将前几个寄存器(尤其是sssp)存储到引导扇区加载程序本身的位置,从0:7C00h 开始。正如您的回答一样,初始堆栈可能位于 0:8000h ish 并且可能会覆盖您存储的值。
  • @NateEldredge :这具有在 80186 之前的英特尔处理器上不可用的缺点。
  • 您还在堆栈帧上混淆了csipip 将被首先弹出,然后是 cs
【解决方案2】:

引用布伦丹的回答:

问题在于,要将数据存储在您必须知道将其存储在安全位置的任何地方(不覆盖堆栈,不被堆栈覆盖,不写入 ROM 或其他不是 RAM 的东西)破坏 RAM 中的任何其他内容,例如 BIOS 数据或您的代码);并且您必须先修改寄存器(主要是段寄存器),然后才能知道您将数据存储在安全的地方,因此您无法将这些寄存器的原始值安全地存储在任何地方。

解决这个问题的方法是使用由 ROM-BIOS 设置的初始堆栈,它至少可以安全保存几十个字节,然后将前几个寄存器的值存储到占用的空间中通过我们自己的引导扇区加载程序。这个空间是为我们保留的,不能被 ROM-BIOS 设置的初始堆栈覆盖。在将堆栈切换到已知良好区域后,我们也可以使用其他内存,尽管在本示例中我们不需要。这是 NASM 源代码 (test.asm):

%if 0

Boot sector loader which displays register values
 by C. Masloch, 2020

Usage of the works is permitted provided that this
instrument is retained with the works, so that any entity
that uses the works is notified of this instrument.

DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.

%endif


        struc BS
bsJump: resb 3
bsOEM:  resb 8
bsBPB:
        endstruc

        struc EBPB              ;        BPB sec
bpbBytesPerSector:      resw 1  ; offset 00h 0Bh
bpbSectorsPerCluster:   resb 1  ; offset 02h 0Dh
bpbReservedSectors:     resw 1  ; offset 03h 0Eh
bpbNumFATs:             resb 1  ; offset 05h 10h
bpbNumRootDirEnts:      resw 1  ; offset 06h 11h -- 0 for FAT32
bpbTotalSectors:        resw 1  ; offset 08h 13h
bpbMediaID:             resb 1  ; offset 0Ah 15h
bpbSectorsPerFAT:       resw 1  ; offset 0Bh 16h -- 0 for FAT32
bpbCHSSectors:          resw 1  ; offset 0Dh 18h
bpbCHSHeads:            resw 1  ; offset 0Fh 1Ah
bpbHiddenSectors:       resd 1  ; offset 11h 1Ch
bpbTotalSectorsLarge:   resd 1  ; offset 15h 20h
bpbNew:                         ; offset 19h 24h

ebpbSectorsPerFATLarge: resd 1  ; offset 19h 24h
ebpbFSFlags:            resw 1  ; offset 1Dh 28h
ebpbFSVersion:          resw 1  ; offset 1Fh 2Ah
ebpbRootCluster:        resd 1  ; offset 21h 2Ch
ebpbFSINFOSector:       resw 1  ; offset 25h 30h
ebpbBackupSector:       resw 1  ; offset 27h 32h
ebpbReserved:           resb 12 ; offset 29h 34h
ebpbNew:                        ; offset 35h 40h
        endstruc

        struc BPBN              ; ofs B16 S16 B32 S32
bpbnBootUnit:           resb 1  ; 00h 19h 24h 35h 40h
                        resb 1  ; 01h 1Ah 25h 36h 41h
bpbnExtBPBSignature:    resb 1  ; 02h 1Bh 26h 37h 42h -- 29h for valid BPBN
bpbnSerialNumber:       resd 1  ; 03h 1Ch 27h 38h 43h
bpbnVolumeLabel:        resb 11 ; 07h 20h 2Bh 3Ch 47h
bpbnFilesystemID:       resb 8  ; 12h 2Bh 36h 47h 52h
        endstruc                ; 1Ah 33h 3Eh 4Fh 5Ah


        cpu 8086
        org 7C00h

start:
        jmp short entrypoint
        nop

        times (bsBPB + EBPB_size + BPBN_size) - ($ - $$) db 0

entrypoint:
        pushf
        cli                    ; An interrupt could use too much more stack space
        cld
        push bx
        push ds
        call 0:.next           ; Set CS:IP to match ORG
.next:
        pop bx                 ; BX = IP of return address pushed by call
        sub bx, .next - start  ; calculate original IP on entry to start
        push bx
         push cs
         pop ds                ; DS=0 to match ORG
        mov bx, start
        pop word [bx + reg_ip]      ; store into start + BPB space 
        pop word [bx + reg_cs]
        pop word [bx + reg_ds]
        pop word [bx + reg_bx]
        pop word [bx + reg_fl]
        mov word [bx + reg_sp], sp
        mov word [bx + reg_ss], ss
        mov word [bx + reg_ax], ax
        xor ax, ax
        mov ss, ax
        mov sp, bx              ; set sp immediately after ss
        sti
        mov word [bx + reg_cx], cx
        mov word [bx + reg_dx], dx
        mov word [bx + reg_es], es
        mov word [bx + reg_si], si
        mov word [bx + reg_di], di
        mov word [bx + reg_bp], bp

        mov si, table
        ; bx -> start
loop_table:
        mov al, 32
        call disp_al
        lodsw
        call disp_al
        xchg al, ah
        call disp_al
        cmp al, 32
        jbe .next
        mov al, '='
        call disp_al
        mov ax, [bx]
        inc bx
        inc bx
        call disp_ax_hex
.next:
        cmp si, table.end
        jb loop_table

exit:
        xor ax, ax
        int 16h
        int 19h


disp_al:
        push ax
        push bx
        push bp

        mov ah, 0Eh
        mov bx, 7
        int 10h

        pop bp
        pop bx
        pop ax
        retn

disp_ax_hex:                    ; ax
                xchg al,ah
                call disp_al_hex                ; display former ah
                xchg al,ah                      ;  and fall through for al
disp_al_hex:                    ; al
                push cx
                mov cl,4                          ; ror al,4 would require 186
                ror al,cl
                call disp_al_lownibble_hex      ; display former high-nibble
                rol al,cl
                pop cx
                                                ;  and fall through for low-nibble
disp_al_lownibble_hex:
                push ax                  ; save ax for call return
                and al,00001111b                ; high nibble must be zero
                add al,'0'                      ; if number is 0-9, now it's the correct character
                cmp al,'9'
                jna .decimalnum          ; if we get decimal number with this, ok -->
                add al,7                        ;  otherwise, add 7 and we are inside our alphabet
 .decimalnum:
                call disp_al
                pop ax
                retn


        struc registerstorage
reg_ss: resw 1
reg_bp: resw 1
reg_sp: resw 1
reg_cs: resw 1
reg_ip: resw 1
reg_fl: resw 1
reg_ds: resw 1
reg_si: resw 1
reg_es: resw 1
reg_di: resw 1
reg_ax: resw 1
reg_bx: resw 1
reg_cx: resw 1
reg_dx: resw 1
        endstruc

%if registerstorage_size + start > entrypoint
 %error Entrypoint is not safe
%endif

        align 2
table:
        dw "SS"
        dw "BP"
        dw "SP"
        dw "CS"
        dw "IP"
        dw "FL"
        db 13,10
        dw "DS"
        dw "SI"
        dw "ES"
        dw "DI"
        db 13,10
        dw "AX"
        dw "BX"
        dw "CX"
        dw "DX"
        db 13,10
.end:

        times 510 - ($ - $$) db 0
        dw 0AA55h

nasm test.asm -f bin -o test.bin 组装,然后作为引导扇区加载。示例:

 -boot protocol chain test.bin
 -r
 AX=0000 BX=0000 CX=F000 DX=0000 SP=7BF0 BP=07BE SI=07BE DI=0000
 DS=0000 ES=0060 SS=0000 CS=0000 IP=7C00 NV UP DI PL ZR NA PE NC
 0000:7C00 EB58              jmp     7C5A
 -g
  SS=0000 BP=07BE SP=7BF0 CS=0000 IP=7C00 FL=0046
  DS=0000 SI=07BE ES=0060 DI=0000
  AX=0000 BX=0000 CX=F000 DX=0000
 Boot load called
 -

-gBoot load called 之间的部分是引导扇区加载程序的输出。)

【讨论】:

  • 您可以在早期使用mov [cs: start + reg_ip], bx 而不是push bx / ... / pop [bx + reg_ip] 保存一些指令。同样,push ds / pop 可能只是设置 CS 之后设置 DS 之前的 mov [cs: bx + reg_ds], ds。不过,对于代码大小而言,这两个变化可能都是中性的或更糟。 CS 覆盖需要一个字节,disp16reg+disp8 也是如此。
  • 您可以通过对 4 个十六进制数字使用 4 次迭代循环来节省一些代码字节,而不是那种调用/失败黑客攻击。 godbolt.org/z/nef3EY 显示我的版本,从 mov si, table.end: 标签。我把它从你的版本的 0x7E 字节降到我的 0x6E 字节。 (我还删除了一些推送/弹出,让函数破坏了一些寄存器,并且在循环内的工作更少,因此它更高效,尽管这基本上无关紧要。即使在 disp_ax_hexdisp_al 中有完整的保存/恢复寄存器,我想我仍然节省了几个字节。)
  • 呃,好的,所以我的版本不适用于无法按文档 (ctyme.com/intr/rb-0106.htm) 工作的有缺陷的 BIOS。如果您打算在任何地方放置推送/弹出斧头作为此类错误 BIOS 的解决方法,如果我们只关心代码大小,它可能只是在 disp_al,那么您可以返回到 lodsw/ xchg al,ah 因为整个 AX 无论如何都会被存储/重新加载。或者cmp byte [si-1], ' ',所以我们不依赖 AX。或 mov cl, al / call disp_al / cmp cl, ' ' 以代码大小为代价。还是先比较一下,int 10h 是否保留 FLAGS?
  • @Peter Cordes:在我为 disp_ax_hex 的嵌套调用方式辩护时,该代码的其他用户通常也需要单独使用 disp_al_hex,因此使用两个 ax 是有一定意义的al 来电。也很自然地扩展到disp_dxax_hex
  • 啊,是的,如果已经将其编写为通用的可扩展方式来显示字节或单词,那是有道理的。这是一个好主意,如果您只需要 4 个十六进制数字,它就不会尽可能紧凑。 (相关:How to convert a binary integer number to a hex string? 是我关于十六进制输出的问答,有一些有趣的 SSE2 和 AVX512 版本,以及标量查找表和 cmov 版本。)
猜你喜欢
  • 2011-04-04
  • 2020-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-28
  • 2012-04-09
  • 1970-01-01
相关资源
最近更新 更多