【问题标题】:BIOS int 10h printing garbage on QEMUBIOS int 10h 在 QEMU 上打印垃圾
【发布时间】:2016-03-02 15:26:34
【问题描述】:

我在编写一个在 QEMU 中作为引导加载程序运行的 x86 实模式汇编程序时遇到问题。我正在尝试通过 BIOS 中断 0x10 打印文本。我的代码是:

print:
    pusha
.loop:
    mov AL, [SI]
    cmp AL, 0
    je .end
    call printChar
    inc SI
    jmp .loop
.end:
    popa
    ret

printChar:
    pusha
    mov AH, 0x0E
    mov BH, 0
    mov BL, 0x0F
    int 0x10
    popa
    ret

我使用[ORG 0x7c00] 作为起点。我测试了 printChar 标签并用 AL 中的一些字母调用它,它工作正常。当我尝试将内存地址加载到这样的消息中时:

loadMsg      db "Loading",0
mov SI, loadMessage
call print

我在 QEMU 模拟器上得到像“U”这样的垃圾输出。昨天,我写了一个和这个非常相似的代码,完全没有问题。是什么导致了我的问题,如何解决?

【问题讨论】:

  • 当 BIOS 引导程序将引导磁盘的第一个扇区(512 字节)加载到 RAM 中时,引导加载程序会加载到物理内存地址 0x7c00,然后跳转到它。您需要告诉 NASM 您的代码将从位置 0x7c00 开始,以便它知道如何正确生成内存中数据的偏移量。因为您不能保证 DS 包含引导加载程序跳转到您的代码时所期望的段 - 您需要手动设置它。

标签: assembly nasm x86-16 bootloader bios


【解决方案1】:

这是个问题:

loadMsg    db "Loading",0
mov        SI, loadMessage
call    print

除非程序跳过“加载”文本,否则它会执行这些字节可能(或可能不)代表的任何指令。像这样可以解决它:

    jmp      print_msg
    loadMsg    db "Loading",0
print_msg:
    mov        SI, loadMessage
    call    print

【讨论】:

  • 哦,我的错。我还在学习,这部分只是一个例子。我没有在代码中放入任何数据声明;)我没有按照惯例将数据放入代码中,但是阅读您所说的完全有道理,为什么不将数据放入代码段中,因为数据将被执行并且对处理器来说是垃圾
【解决方案2】:

我最近在 * 的回答中写了一些 General Bootloader Tips,可能对你有用。 提示 #1 可能适用于您的问题:

当 BIOS 跳转到您的代码时,您不能依赖具有有效或预期值的 CS、DS、ES、SS、SP 寄存器。当您的引导加载程序启动时,它们应该被适当地设置。您只能保证您的引导加载程序将从物理地址 0x00007c00 加载并运行,并且引导驱动器号已加载到 DL 寄存器中。

基于 printChar 有效的事实,并且写出整个字符串并不意味着 DS:SI 没有指向内存中的正确位置你的字符串驻留。造成这种情况的常见原因是开发人员错误地认为当 BIOS 跳转到引导加载程序时,CS 和/或 DS 寄存器已正确设置。它必须手动设置。在原点为 0x7c00 的情况下,DS 需要设置为 0。在 16 位实模式下,物理内存地址由 segment:offset pairs 使用公式 (segment<<4)+offset 计算得出。在您的情况下,您使用的偏移量为 0x7C00。 DS 中的值为 0 将产生正确的物理地址 (0

您可以在程序开始时将 DS 设置为 0,例如:

xor ax, ax       ; set AX to zero
mov ds, ax       ;     DS = 0  

QEMU 的情况下,BIOS 跳转到 0x07c0:0x0000 。这也代表相同的物理内存位置 (0x07c0CS=0x07c0(而不是 CS=0)。由于有许多段:偏移量对映射到相同的物理内存位置,因此您需要适当地设置 DS。您不能依赖 CS 是您期望的值。所以在 QEMU 中,这样的代码甚至无法正确设置 DS(使用 ORG 0x7c00 时):

mov ax, cs
mov ds, ax       ; Make DS=CS

这可能适用于一些模拟器,如 DOSBOX 和一些物理硬件,但不是全部。此代码可以工作的环境是 BIOS 跳转到 0x0000:0x7c00 时。在这种情况下,当 CS 到达您的引导加载程序代码时,它会为零,并且将 CS 复制到 DS 将起作用。不要假设 CS 在所有环境中都为零,这是我要提出的主要观点。始终将段寄存器明确设置为您想要的。

应该工作的代码示例是:

    BITS  16
    ORG   0x7c00
    GLOBAL main

main:
    xor ax, ax        ; AX = 0
    mov ds, ax        ; DS = 0
    mov bx, 0x7c00

    cli               ; Turn off interrupts for SS:SP update
                      ; to avoid a problem with buggy 8088 CPUs
    mov ss, ax        ; SS = 0x0000
    mov sp, bx        ; SP = 0x7c00
                      ; We'll set the stack starting just below
                      ; where the bootloader is at 0x0:0x7c00. The
                      ; stack can be placed anywhere in usable and
                      ; unused RAM.
    sti               ; Turn interrupts back on

    mov SI, loadMsg
    call print

    cli
.endloop:
    hlt
    jmp .endloop      ; When finished effectively put our bootloader
                      ; in endless cycle

print:
    pusha
.loop:
    mov AL, [SI]      ; No segment on [SI] means implicit DS:[SI]
    cmp AL, 0
    je .end
    call printChar
    inc SI
    jmp .loop
.end:
    popa
    ret

printChar:
    pusha
    mov AH, 0x0E
    mov BH, 0
    mov BL, 0x0F
    int 0x10
    popa
    ret

; Place the Data after our code
loadMsg db "Loading",0

times 510 - ($ - $$) db 0   ; padding with 0 at the end
dw 0xAA55                   ; PC boot signature

【讨论】:

  • 真的很感谢,帮了大忙。因此,由于我没有正确设置 DS,它只使用了 DS:SI,这可以是机器上的 BIOS 喜欢的任何内容。我有一个小疑问,将 SS 设置为 0 并将 SP 设置为 0X7C00,您不是将堆栈放入 0000:7C00 吗?并且堆栈不会开始“覆盖”引导加载程序?
  • 很高兴您解决了 DS 问题。至于 SS:SP 是 0000:7C00 。堆栈的工作方式(在 16 位实模式下)是先将 SP 减 2,然后在 SS:SP 处写入一个值。这意味着 SS:SP 为 0000:0x7c00 的第一个 PUSH 将递减到 0000:0x7BFE,然后将 2 字节 WORD 写入该位置。由于递减首先发生,它不会写在我们的代码之上。您可以将 SS:SP 放置在几乎与您的数据和代码不冲突的任何地方——我只是以 0x0000:0x7c00 为例。
  • 老兄,你真棒。我试图找到堆栈如何工作以找到将堆栈放在内存中的位置的答案,而您的答案很完美。所以堆栈“有点倒退”,它在添加值之前从 SP 中减去,例如,如果我将 SP 设置为 7E00h (7C00h + 512d) 然后将 stat 覆盖我的引导加载程序(从最后,假设 BIOS 加载了我的引导加载程序0000:7C00)。
  • @rcgldr 如果您不可能在有缺陷的 8088 上运行,则不需要它。如果您可能要在这样的硬件上运行,那么如果您有空间,那么拥有它是合理的。
  • @rcgldr :我很清楚它会禁用它直到下一条指令结束(在真正的 8088/8086 硬件上)它应该禁用任何段寄存器的中断(不仅仅是 SS)。这随着 80286+ 而发生了变化,它只针对 SS 完成。我特别提到了错误的 8088 是有原因的。我的代码针对可能存在的更广泛的硬件。自 80 年代中期以来,我一直在真正的 8088 硬件上进行汇编语言开发,我还解释了一个众所周知的错误。 1987 年的 PC 杂志刊登了一个故事:books.google.com/…
最近更新 更多