引导加载程序的第二阶段使用Int 0x10/ah=0x0e打印垃圾字符。

3

我正在尝试学习汇编语言并编写引导程序。以下代码将软盘驱动器的内容加载到内存中并跳转到它(从地址0x1000开始加载)。这段代码应该在屏幕上打印“X”,但由于某种原因它打印了一个空格。请问有人能告诉我出了什么问题吗?

[bits 16]
jmp reset
reset:          ;Resets floppy drive
    xor ax,ax   ;0 = Reset floppy disk
    mov dl,0        ;Drive 0 is floppy
    int 0x13
    jc reset        ;If carry flag was set, try again

    mov ax,0x1000   ;When we read the sector, we are going to read address 0x1000
    mov es,ax       ;Set ES with 0x1000

floppy:
    mov ah,0x2  ;2 = Read floppy
    mov al,0x11 ;Reading one sector
    mov ch,0x0  ;Track 1 
    mov cl,0x2  ;Sector 2, track 1
    mov dh,0x0  ;Head 1
    mov dl,0x0  ;Drive = 0 (Floppy)
    int 0x13
    jc floppy   ;If carry flag was set, try again
    jmp 0x1000:0000 ;Jump to 0x1000, start of second program

times 510 - ($ - $$) db 0       ;Fill the rest of sector with 0 
dw 0xAA55   ;This is the boot signiture
;---
;--[segment 2]--
mov bx, var
mov ah, 0x0e
mov al, [bx]
int 0x10
jmp $

var:
db 'X'
times 737280 - ($ - $$) db 0

1
你将0x1000放入_ES_作为段。这很好,但是int 13h_/ah=02h期望内存位置为_ES:BX_。你在哪里初始化BX=0._BX_在引导加载程序运行时可以是任何值(但你需要它为0)? - Michael Petch
1
当BIOS跳转到您的引导加载程序时,启动驱动器位于_DL_寄存器中。在进行磁盘重置和读取时,您应该使用该值作为_DL_(只要启动驱动器是软盘驱动器A,您的代码就可以正常工作)。 - Michael Petch
1
int 10h/ah=0x0e 需要将页面号(在您的情况下可能为0)放在 BH 中。颜色放在 BL 中。 - Michael Petch
你可以使用类似于 mov al, [var] 的指令,直接将内存中的一个字节移动到一个8位寄存器中。 - Michael Petch
2
你的代码正在读取17个扇区! (mov al,0x11; 读取一个扇区) - Fifoernik
1个回答

11
我可以推断出你正在使用 NASM (或NASM 兼容) 汇编器。我不知道您用于构建引导加载程序的操作系统是什么,但我先假设是Linux或Windows。其他环境会有些相似。
为了使这更容易,您应将引导加载程序分成两个部分。一个是引导加载程序,第二个是在 0x1000:0x0000 加载的第二阶段。这使我们能够正确定义引导加载程序的起始点。 引导加载程序预计将被加载到物理地址 0x07c00,第二个阶段预计加载到 0x10000 ((0x1000<<4+)+0)。 我们需要汇编器来正确生成数据和代码的地址。
我写了一些StackOverflow答案,描述了我对代码所做修改的一些变化。其中一些更相关的是:
  • General Boot Loader Tips 给出了一些关于在引导加载程序中不要做出的一般准则和假设
  • Information on the pitfalls of not setting up DS properly and getting garbage when accessing memory variables. 这适用于您的第二阶段

如果您不了解 段:偏移 对,我建议您阅读这篇文章。 我提出这个问题是因为在您的问题和代码中似乎存在混淆。你似乎认为物理内存地址0x1000与段:偏移量0x1000:0x0000相同。 在您的问题中,您说:
jmp 0x1000:0000 ;Jump to 0x1000, start of second program

如果您查看该链接,您会发现这个段落:通过将段向左移动4位(乘以16进制),然后添加偏移量,segment:offset可以计算出物理地址。等式通常写作(segment<<4)+offset。在您的情况下,0x1000:0x0000是一个段为0x1000、偏移为0x0000的地址。使用等式计算内存中的物理地址,您将得到(0x1000<<4)+0x0000 = 0x10000(而不是0x1000)。
从您的代码中无法确定您如何使用NASM进行汇编。我提供了一个示例,但重要的部分是将引导加载程序拆分开来。假设我们将您的引导加载程序放入名为bootload.asm的文件中:
[bits 16]
[ORG 0x7c00]    ; Bootloader starts at physical address 0x07c00

    ; BIOS sets DL to boot drive before jumping to the bootloader

    ; Since we specified an ORG(offset) of 0x7c00 we should make sure that
    ; Data Segment (DS) is set accordingly. The DS:Offset that would work
    ; in this case is DS=0 . That would map to segment:offset 0x0000:0x7c00
    ; which is physical memory address (0x0000<<4)+0x7c00 . We can't rely on
    ; DS being set to what we expect upon jumping to our code so we set it
    ; explicitly
    xor ax, ax
    mov ds, ax        ; DS=0

    cli               ; Turn off interrupts for SS:SP update
                      ; to avoid a problem with buggy 8088 CPUs
    mov ss, ax        ; SS = 0x0000
    mov sp, 0x7c00    ; 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

reset:                ; Resets floppy drive
    xor ax,ax         ; 0 = Reset floppy disk
    int 0x13
    jc reset          ; If carry flag was set, try again

    mov ax,0x1000     ; When we read the sector, we are going to read address 0x1000
    mov es,ax         ; Set ES with 0x1000

floppy:
    xor bx,bx   ;Ensure that the buffer offset is 0!
    mov ah,0x2  ;2 = Read floppy
    mov al,0x1  ;Reading one sector
    mov ch,0x0  ;Track 1
    mov cl,0x2  ;Sector 2, track 1
    mov dh,0x0  ;Head 1
    int 0x13
    jc floppy   ;If carry flag was set, try again
    jmp 0x1000:0000 ;Jump to 0x1000, start of second program

times 510 - ($ - $$) db 0       ;Fill the rest of sector with 0
dw 0xAA55   ;This is the boot signature

请注意,我删除了这一行:

mov dl,0x0  ;Drive = 0 (Floppy)

这段代码将启动驱动器硬编码为软盘A:如果您从USB、硬盘或软盘B:启动,那么您的代码将无法正常工作,因为在这些情况下驱动器号很可能不是零。BIOS传递了实际用于加载引导程序的引导驱动器。该值位于寄存器DL中。这是您应该使用的BIOS磁盘函数的值。由于DL已经包含了引导驱动器,因此我们直接使用它。


第二阶段可以通过以下方式进行修改。我假设有一个名为stage2.asm的文件:

[BITS 16]
[ORG 0x0000]      ; This code is intended to be loaded starting at 0x1000:0x0000
                  ; Which is physical address 0x10000. ORG represents the offset
                  ; from the beginning of our segment.

; Our bootloader jumped to 0x1000:0x0000 which sets CS=0x1000 and IP=0x0000
; We need to manually set the DS register so it can properly find our variables
; like 'var'

mov ax, cs
mov ds, ax       ; Copy CS to DS (we can't do it directly so we use AX temporarily)

mov bx, var
mov ah, 0x0e
mov al, [bx]
xor bh, bh       ; BH = 0 = Display on text mode page 0
int 0x10
jmp $

var:
db 'X'

我没有尝试简化你的代码。目的是展示如何添加胶水来解决你的问题。两个文件都使用ORG指令指定起始点。引导程序需要被组装以便它们能够在内存地址0x07c00处工作。你正在将第二阶段加载到0x1000:0x0000,它映射到物理地址0x10000。我们将ORG设置为0x0000,因为 jmp 0x1000:0000 会设置CS= 0x1000和IP= 0x0000。由于IP是0x0000,我们希望ORG与之匹配,以便近内存引用相对于我们64k段的开头。
这将允许汇编器为您的变量和代码生成正确的内存引用。因为你在代码中没有正确地做这件事,所以你的第二阶段读取了错误的var内存位置,随后显示了不正确的字符。
一旦你将这2个文件拆分开来,你需要使用NASM进行组装,然后将它们放入磁盘映像中。与你的问题不同,我将使用DD来构建一个720k软盘映像,然后将引导程序放在开头(不截断磁盘),然后从紧随其后的扇区开始放置第二阶段。可以通过以下方式实现:
# Assemble both components as binary images with NASM
nasm -f bin bootload.asm -o bootload.bin
nasm -f bin stage2.asm -o stage2.bin

# Create a 720k disk image
dd if=/dev/zero of=disk.img bs=1024 count=720

# Place bootload.bin at the beginning of disk.img without truncating
dd if=bootload.bin of=disk.img conv=notrunc

# Place stage2.bin starting at the second 512byte sector and write
# it without truncating the disk image. bs=512 seek=1 will skip the
# first 512 byte sector and start writing stage2.bin there. 
dd if=stage2.bin of=disk.img bs=512 seek=1 conv=notrunc

您可以使用QEMU这样的工具来运行此镜像:
qemu-system-i386 -fda disk.img 

如果您使用的是Windows,并且没有访问DD的权限,那么您可以尝试使用以下修改版的stage2.asm:
[BITS 16]
[ORG 0x0000]      ; This code is intended to be loaded starting at 0x1000:0x0000
                  ; Which is physical address 0x10000. ORG represents the offset
                  ; from the beginning of our segment.

; Our bootloader jumped to 0x1000:0x0000 which sets CS=0x1000 and IP=0x0000
; We need to manually set the DS register so it can properly find our variables
; like 'var'

mov ax, cs
mov ds, ax       ; Copy CS to DS (we can't do it directly so we use AX temporarily)

mov bx, var
mov ah, 0x0e
mov al, [bx]
xor bh, bh       ; BH = 0 = Display on text mode page 0
int 0x10
jmp $

var:
db 'X'
; Extend the second stage to (720K - 512 bytes) 
; bootload.bin will take up first 512 bytes 
times 737280 - 512 - ($ - $$) db 0

接下来使用以下命令组装和构建720K软盘镜像:

nasm -f bin bootload.asm -o bootload.bin
nasm -f bin stage2.asm -o stage2.bin
copy /b bootload.bin+stage2.bin disk.img

disk.img是一个720K的磁盘映像,可用于QEMU或Bochs。最终disk.img的大小应为737,280字节。


如果您想将一个内存地址的值移动到寄存器中,可以直接进行操作,无需使用中间寄存器。在你的stage2.asm中,你有以下代码:

mov bx, var
mov ah, 0x0e
mov al, [bx]

它可以写成:

mov ah, 0x0e
mov al, [var]

这将会从内存位置var中移动一个字节,并直接移动到AL中。它的大小由NASM确定为一个字节,因为目标AL是一个8位寄存器。


在有人问之前,我在设置堆栈的代码周围放置了CLI/STI,以避免在80年代初制造的有缺陷的8088处理器上出现错误。如果不太可能在这种类型的处理器上运行代码,则可以删除CLI/STI对。 - Michael Petch
@VishnuShankar:你可以从这里直接下载Windows DD版本:http://www.chrysocome.net/downloads/dd-0.6beta3.zip。我将dd.exe(来自zip文件中)复制到Windows路径上的某个位置,然后我在示例中使用的命令就可以工作了。 - Michael Petch
我能将它们转换为.bin文件并合并它们,然后模拟合并后的bin文件吗? - RainingComputers
1
@MichaelPetch,为什么在你的“stage2.asm”中忽略了BIOS电传打字机功能的BH参数?你在对原始问题的评论中提到了它! - Fifoernik
1
@Fifoernik:因为我有几个不同版本的代码可供使用,而我选择了一个较旧(错误)的版本。但在这种情况下,它并不是他们问题的根本原因。我会进行修改。感谢您指出这一点。 - Michael Petch
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接