8

一般问题

我一直在开发一个简单的引导加载程序,并在某些环境中偶然发现了一个问题,这些指令不起作用:

mov si, call_tbl      ; SI=Call table pointer
call [call_tbl]       ; Call print_char using near indirect absolute call
                      ; via memory operand
call [ds:call_tbl]    ; Call print_char using near indirect absolute call
                      ; via memory operand w/segment override
call near [si]        ; Call print_char using near indirect absolute call
                      ; via register

其中每一个碰巧都涉及到绝对内存偏移的间接近调用。我发现如果我使用类似的JMP表会出现问题。相对的调用和跳转似乎没有受到影响。像这样的代码有效:

call print_char 

我已经通过讨论编写引导加载程序的注意事项的海报接受了 Stackoverflow 上提出的建议。特别是,我看到了这个Stackoverflow的答案与General Bootloader Tips。第一个提示是:

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

接受所有建议,我不依赖CS,我设置了一个堆栈,并将DS设置为适合我使用的ORG(原点偏移)。我创建了一个 Minimal Complete Verifiable 示例来演示该问题。我使用NASM构建了它,但这似乎不是NASM特有的问题。


最小的例子

测试代码如下:

[ORG 0x7c00]
[Bits 16]

section .text
main:
    xor ax, ax
    mov ds, ax            ; DS=0x0000 since OFFSET=0x7c00
    cli                   ; Turn off interrupts for potentially buggy 8088
    mov ss, ax
    mov sp, 0x7c00        ; SS:SP = Stack just below 0x7c00
    sti                   ; Turn interrupts back on

    mov si, call_tbl      ; SI=Call table pointer
    mov al, [char_arr]    ; First char to print 'B' (beginning)
    call print_char       ; Call print_char directly (relative jump)

    mov al, [char_arr+1]  ; Character to print 'M' (middle)
    call [call_tbl]       ; Call print_char using near indirect absolute call
                          ; via memory operand
    call [ds:call_tbl]    ; Call print_char using near indirect absolute call
                          ; via memory operand w/segment override
    call near [si]        ; Call print_char using near indirect absolute call
                          ; via register

    mov al, [char_arr+2]  ; Third char to print 'E' (end)
    call print_char       ; Call print_char directly (relative jump)

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

print_char:
    mov ah, 0x0e    ; Write CHAR/Attrib as TTY
    mov bx, 0x00    ; Page 0
    int 0x10
    retn

; Near call address table with one entry
call_tbl: dw print_char

; Simple array of characters
char_arr: db 'BME'

; Bootsector padding
times 510-($-$$) db 0
dw 0xAA55

出于测试目的,我构建了一个ISO映像和一个 1.44MB 软盘映像。我使用的是 Debian Jessie 环境,但大多数 Linux 发行版都类似:

nasm -f bin boot.asm -o boot.bin
dd if=/dev/zero of=floppy.img bs=1024 count=1440
dd if=boot.bin of=floppy.img conv=notrunc

mkdir iso    
cp floppy.img iso/
genisoimage -quiet -V 'MYBOOT' -input-charset iso8859-1 -o myos.iso -b floppy.img -hide floppy.img iso

我最终得到了一个名为 的软盘映像和floppy.img一个名为myos.iso.


期望与实际结果

在大多数情况下,此代码有效,但在许多环境中则无效。当它工作时,它只是在显示器上打印:

BMMME

B我使用具有相对偏移量的典型CALL打印出来,它似乎工作正常。在某些环境中,当我运行代码时,我会得到:

然后它似乎只是停止做任何事情。它似乎B正确打印出来,但随后发生了一些意想不到的事情。

似乎工作的环境:

  • QEMU用软盘和 ISO 启动
  • VirtualBox用软盘和 ISO 启动
  • VMWare 9用软盘和 ISO 启动
  • DosBox用软盘启动
  • 使用软盘映像在 Debian Jessie 上正式打包的Bochs (2.6)
  • Debian Jessie 上使用软盘映像和ISO的 Bochs 2.6.6(从源代码控制构建)映像
  • 90 年代中期使用软盘和ISO的 AST Premmia SMP P90 系统

无法按预期工作的环境:

  • 使用ISO映像在 Debian Jessie 上正式打包的Bochs (2.6)
  • 基于 486DX 的系统,带有 90 年代初的 AMI BIOS,使用软盘映像。CD 无法在此系统上启动,因此无法测试该 CD。

我发现有趣的是Bochs (2.6 版)在使用ISO的 Debian Jessie 上无法正常工作。当我从具有相同版本的软盘启动时,它按预期工作。

在所有情况下,ISO和软盘映像似乎都已加载并开始运行,因为在所有情况下,它至少能够B在显示器上打印出来。


我的问题

  • 当它失败时,为什么它只打印出 aB而仅此而已?
  • 为什么有些环境有效而有些环境失败?
  • 这是我的代码或硬件/BIOS 中的错误吗?
  • 如何修复它以便我仍然可以使用近乎间接的跳转和调用表来获得绝对内存偏移量?我知道我可以完全避免这些说明,这似乎解决了我的问题,但我希望能够了解如何以及是否可以在引导加载程序中正确使用它们。
4

1 回答 1

16

问题

您的问题的答案隐藏在您的问题中,只是并不明显。您引用了我的通用引导加载程序提示

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

您的代码正确设置了DS,并设置了自己的堆栈(SSSP)。您没有盲目地将CS复制到DS,但您所做的是依赖CS作为预期值(0x0000)。在我解释我的意思之前,我想提请您注意我最近给出的关于ORG指令(或任何链接器指定的原点)如何与 segment:offset 对一起使用的Stackoverflow 答案BIOS 跳转到物理地址 0x07c00。

答案详细说明了将CS复制到DS时如何在引用内存地址(例如变量)时导致问题。在总结中我说:

不要假设 CS 是我们期望的值,也不要盲目地将 CS 复制到 DS 。明确设置 DS。

关键是不要假设 CS 是我们期望的值。所以你的下一个问题可能是 - 我似乎没有使用CS是吗?答案是肯定的。通常,当您使用典型的CALLJMP指令时,它看起来像这样:

call print_char
jmp somewhereelse

在 16 位代码中,这两者都是相对跳转。这意味着您在内存中向前或向后跳转,但作为相对于JMPCALL之后的指令的偏移量。您的代码放置在段内的位置无关紧要,因为它是您当前所在位置的正负位移。CS的当前值实际上与相对跳跃无关,因此它们应该按预期工作。

您似乎并不总是正常工作的说明示例包括:

call [call_tbl]       ; Call print_char using near indirect absolute call
                      ; via memory operand
call [ds:call_tbl]    ; Call print_char using near indirect absolute call
                      ; via memory operand w/segment override
call near [si]        ; Call print_char using near indirect absolute call
                      ; via register

所有这些都有一个共同点。CALLJMP的地址是ABSOLUTE,而不是相对的。标签的偏移量会受到ORG(代码的原点)的影响。如果我们查看您的代码的反汇编,我们将看到:

objdump -mi8086 -Mintel -D -b binary boot.bin --adjust-vma 0x7c00
boot.bin:     file format binary

Disassembly of section .data:

00007c00 <.data>:
    7c00:   31 c0                   xor    ax,ax
    7c02:   8e d8                   mov    ds,ax
    7c04:   fa                      cli
    7c05:   8e d0                   mov    ss,ax
    7c07:   bc 00 7c                mov    sp,0x7c00
    7c0a:   fb                      sti
    7c0b:   be 34 7c                mov    si,0x7c34
    7c0e:   a0 36 7c                mov    al,ds:0x7c36
    7c11:   e8 18 00                call   0x7c2c              ; Relative call works
    7c14:   a0 37 7c                mov    al,ds:0x7c37
    7c17:   ff 16 34 7c             call   WORD PTR ds:0x7c34  ; Near/Indirect/Absolute call
    7c1b:   3e ff 16 34 7c          call   WORD PTR ds:0x7c34  ; Near/Indirect/Absolute call
    7c20:   ff 14                   call   WORD PTR [si]       ; Near/Indirect/Absolute call
    7c22:   a0 38 7c                mov    al,ds:0x7c38
    7c25:   e8 04 00                call   0x7c2c              ; Relative call works
    7c28:   fa                      cli
    7c29:   f4                      hlt
    7c2a:   eb fd                   jmp    0x7c29
    7c2c:   b4 0e                   mov    ah,0xe              ; Beginning of print_char
    7c2e:   bb 00 00                mov    bx,0x0              ; function
    7c31:   cd 10                   int    0x10
    7c33:   c3                      ret
    7c34:   2c 7c                   sub    al,0x7c             ; 0x7c2c offset of print_char
                                                               ; Only entry in call_tbl
    7c36:   42                      inc    dx                  ; 0x42 = ASCII 'B'
    7c37:   4d                      dec    bp                  ; 0x4D = ASCII 'M'
    7c38:   45                      inc    bp                  ; 0x45 = ASCII 'E'
    ...
    7dfd:   00 55 aa                add    BYTE PTR [di-0x56],dl

我在CALL语句所在的位置手动添加了一些注释,包括有效的相对注释和可能不可行的近/间接/绝对注释。我还确定了print_char函数在哪里,以及它在call_tbl.

从代码后面的数据区我们可以看到,call_tbl它位于 0x7c34,它包含一个 2 字节的绝对偏移量 0x7c2c。这都是正确的,但是当您使用绝对的 2 字节偏移量时,它假定在当前CS中。如果您已阅读此Stackoverflow 答案(我之前引用过)关于使用错误的DS和偏移量来引用变量时会发生什么,您现在可能意识到这可能适用于使用涉及NEAR 2-的绝对偏移量的JMP s CALL s字节绝对值。

作为一个例子,让我们以这个并不总是有效的调用为例:

call [call_tbl] 

call_tbl从 DS:[call_tbl] 加载。当我们启动引导加载程序时,我们将DS正确设置为 0x0000,因此它确实从内存地址 0x0000:0x7c34 中正确检索值 0x7c2c。然后处理器将设置 IP=0x7c2c 但它假设它是相对于当前设置的CS。由于我们不能假设CS是预期值,因此处理器可能会 CALL 或 JMP 到错误的位置。这完全取决于 BIOS 使用什么CS:IP跳转到我们的引导加载程序(它可能会有所不同)。

如果BIOS在 0x0000:0x7c00 处对我们的引导加载程序执行等效的FAR JMP ,则CS将设置为 0x0000,IP设置为 0x7c00。当我们遇到call [call_tbl]它时,它会解析为对 CS:IP=0x0000:0x7c2c的CALL 。这是物理地址 (0x0000<<4)+0x7c2c=0x07c2c,它实际上是print_char函数在内存中物理开始的位置。

一些 BIOS在 0x07c0:0x0000 处对我们的引导加载程序执行FAR JMP等效, CS将设置为 0x07c0,IP设置为 0x0000。这也映射到物理地址 (0x07c0<<4)+0=0x07c00 。当我们遇到call [call_tbl]它时,它会解析为对 CS:IP=0x07c0:0x7c2c的CALL 。这是物理地址 (0x07c0<<4)+0x7c2e=0x0f82c。这显然是错误的,因为print_char函数位于物理地址 0x07c2c,而不是 0x0f82c。

CS设置不正确会导致执行近/绝对寻址的JMPCALL指令出现问题。以及使用段覆盖的任何内存操作数。在此Stackoverflow 答案中可以找到在实模式中断处理程序中使用覆盖的示例CS:CS:


解决方案

由于已经证明我们不能依赖在BIOS 跳转到我们的代码时设置的CS ,我们可以自己设置CS。要设置CS,我们可以对我们自己的代码执行FAR JMP,这会将CS:IP设置为对我们正在使用的 ORG(代码和数据的原点)有意义的值。如果我们使用 ORG 0x7c00,那么这种跳转的示例:

jmp 0x0000:$+5

$+5表示使用比我们当前程序计数器高 5 的偏移量。far jmp 的长度为 5 个字节,因此这具有在我们的 jmp 之后执行远跳转到指令的效果。它也可以这样编码:

    jmp 0x0000:farjmp
farjmp:

当这些指令中的任何一条完成时,CS将被设置为 0x0000,IP将被设置为下一条指令的偏移量。对我们来说关键是CS将是 0x0000。当与 0x7c00 的 ORG 配对时,它将正确解析绝对地址,以便它们在 CPU 上物理运行时正常工作。0x0000:0x7c00=(0x0000<<4)+0x7c00=物理地址0x07c00。

当然,如果我们使用 ORG 0x0000 那么我们需要将CS设置为 0x07c0。这是因为 (0x07c0<<4)+0x0000=0x07c00。所以我们可以这样编码 far jmp:

jmp 0x07c0:$+5

CS将设置为 0x07c0,IP将设置为下一条指令的偏移量。

所有这一切的最终结果是我们将CS设置为我们想要的段,而不是依赖于我们无法保证 BIOS 完成跳转到我们的代码时的值。


不同环境的问题

正如我们所见,CS很重要。大多数 BIOS,无论是在模拟器、虚拟机还是真实硬件中,都相当于跳转到 0x0000:0x7c00,在这些环境中,您的引导加载程序可以正常工作。从CD引导时,一些环境(如旧 AMI Bioses 和Bochs 2.6)会使用CS:IP = 0x07c0:0x0000启动我们的引导加载程序。正如在那些靠近/绝对CALLJMP的环境中所讨论的,它们将从错误的内存位置继续执行,并导致我们的引导加载程序无法正常运行。

那么Bochs为软盘映像而不是ISO映像工作呢?这是Bochs早期版本的一个特点。从软盘启动时,虚拟 BIOS 跳转到 0x0000:0x7c00,而从 ISO 映像启动时,则使用 0x07c0:0x0000。这解释了为什么它的工作方式不同。这种奇怪的行为显然是由于对 El Torito 规范之一的字面解释而产生的,该规范特别提到了段 0x07c0。较新版本的Boch的虚拟 BIOS 被修改为两者都使用 0x0000:0x7c00。


这是否意味着某些 BIOS 有错误?

这个问题的答案是主观的。在 IBM 的 PC-DOS 的第一个版本(2.1 之前)中,引导加载程序假定 BIOS 跳转到 0x0000:0x7c00,但这并没有明确定义。一些 BIOS 制造商在 80 年代开始使用 0x07c0:0x0000 并破坏了一些早期版本的DOS。当发现这一点时,引导加载程序被修改为不做任何关于使用什么段:偏移对到达物理地址 0x07c00 的假设。当时人们可能认为这是一个错误,但基于 20 位段:偏移对引入的歧义。

自 80 年代中期以来,我认为任何假定CS是特定值的新引导加载程序都被错误编码。

于 2015-12-31T15:19:33.163 回答