近呼叫/跳转表并不总是在引导加载程序

时间:2015-12-31 15:19:33

标签: assembly x86 nasm bootloader real-mode

一般问题

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

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

其中每一个都涉及间接近CALL到绝对记忆偏移。我发现如果我使用类似的JMP表,我会遇到问题。相对的呼叫和跳转似乎没有受到影响。像这样的代码有效:

call print_char 

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

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

接受所有建议后,我不依赖于 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 ISO 映像。

期望与实际结果

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

  

BMMME

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

  

然后它似乎停止做任何事情。它似乎正确地打印出B但随后发生了意外情况。

似乎有用的环境:

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

无法按预期工作的环境:

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

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

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

我的问题

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

1 个答案:

答案 0 :(得分:12)

问题

您的问题的答案隐藏在您的问题中,它并不是显而易见的。你引用了我的General Bootloader Tips

  
      
  1. 当BIOS跳转到您的代码时,您无法依赖 CS DS ES SS < / em>, SP 寄存器具有有效或预期的值。应在引导加载程序启动时正确设置它们。只能保证您的引导加载程序将从物理地址0x00007c00加载并运行,并且引导驱动器号已加载到 DL 寄存器中。
  2.   

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

答案详细说明了如何将 CS 复制到 DS 时引用内存地址(例如变量)时会出现问题。在我所说的摘要中:

  

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

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

call print_char
jmp somewhereelse

在16位代码中,这两者都是相对跳跃。这意味着您在内存中向前或向后跳转,但作为相对于 JMP CALL 之后的指令的偏移量。将代码放在某个段中的位置并不重要,因为它是您当前所在位置的正/负位移。 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

所有这些都有一个共同点。 CALL ed或 JMP ed的地址是 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 answer(我前面提到过),当使用错误的 DS 和偏移来引用变量时会发生什么,您现在可能会意识到这可能适用于 JMP s CALL ,它们使用涉及 NEAR 2字节绝对值的绝对偏移量。

作为一个例子,让我们接受这个并不总是有效的电话:

call [call_tbl] 
从DS:[call_tbl]加载

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]时,它会解析为 CALL 到CS:IP = 0x0000:0x7c2c。这是物理地址(0x0000 <&lt;&lt; 4)+ 0x7c2c = 0x07c2c,实际上是函数物理启动的内存中的print_char函数。

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

CS 设置不正确会导致执行Near / Absolute寻址的 JMP CALL 指令出现问题。以及使用CS:的段覆盖的任何内存操作数。在Stackoverflow answer

中可以找到在实模式中断处理程序中使用CS:覆盖的示例

解决方案

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

jmp 0x0000:$+5

$+5表示使用比当前程序计数器高5的偏移量。远jmp是5个字节长所以这有影响我们在jmp之后远程跳转到指令。它也可以用这种方式编码:

    jmp 0x0000:farjmp
farjmp:

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

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

jmp 0x07c0:$+5

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

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

不同环境的问题

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

那么 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,但这并没有明确定义。 80年代的一些BIOS制造商开始使用0x07c0:0x0000并打破了一些早期版本的 DOS 。当发现这一点时,引导加载程序被修改为表现良好,不对任何段:偏移对用于达到物理地址0x07c00做出任何假设。当时人们可能认为这是一个错误,但是基于20位段引入的歧义:偏移对。

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