call/jump 附近的表并不总是在引导加载程序中工作
Near call/jump tables don't always work in a bootloader
一般问题
我一直在开发一个简单的引导加载程序,但在某些环境中遇到了一个问题,在这些环境中,此类指令不起作用:
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 to absolute memory offsets. I have discovered that I have issues if I use similar JMP 表。相对的调用和跳转似乎不受影响。像这样的代码有效:
call print_char
我采纳了 Whosebug 上讨论编写引导加载程序的注意事项的海报提出的建议。特别是我看到这个 回答 General Bootloader Tips。第一个提示是:
- When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts. You can only be guaranteed that your bootloader will be loaded and run from physical address 0x07c00 and that the boot drive number is loaded into the DL register.
采纳了大家的意见,我没有依赖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
然后它似乎停止做任何事情。它似乎正确地打印出了 B
但随后发生了意想不到的事情。
似乎可行的环境:
- QEMU 使用软盘和 ISO
启动
- VirtualBox 使用软盘和 ISO
启动
- VMWare 9 使用软盘和 ISO
启动
- DosBox 用软盘启动
- 使用软盘映像在 Debian Jessie 上正式打包 Bochs(2.6)
- Bochs 2.6.6(从源代码控制构建)在 Debian Jessie 上使用软盘映像和 ISO 映像
- 90 年代中期的 AST Premmia SMP P90 系统使用软盘和 ISO
未按预期工作的环境:
- 在 Debian Jessie 上使用 ISO 映像
正式打包 Bochs(2.6)
- 基于 486DX 的系统,带有 90 年代初使用软盘映像的 AMI BIOS。 CD 无法在此系统上启动,因此无法测试 CD。
我发现有趣的是 Bochs(2.6 版)在使用 ISO 的 Debian Jessie 上无法正常工作。当我使用相同版本的软盘启动时,它按预期工作。
在所有情况下,ISO 和软盘映像似乎都已加载并启动 运行ning,因为在 ALL 情况下它至少能够在显示器上打印出 B
。
我的问题
- 当它失败时,为什么它只打印出一个
B
,仅此而已?
- 为什么有些环境可以工作而有些环境会失败?
- 这是我的代码还是 hardware/BIOS 中的错误?
- 如何修复它以便我仍然可以使用 near 间接跳转和调用表到绝对内存偏移量?我知道我可以完全避免使用这些说明,这似乎可以解决我的问题,但我希望能够了解如何以及是否可以在引导加载程序中正确使用它们。
问题
你的问题的答案隐藏在你的问题中,只是不明显。你引用了我的 :
- When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts. You can only be guaranteed that your bootloader will be loaded and run from physical address 0x00007c00 and that the boot drive number is loaded into the DL register.
您的代码正确设置了 DS,并设置了自己的堆栈(SS,以及 SP).你没有盲目复制CS到DS,而是你所做的是依赖CS预期值 (0x0000)。在我解释我的意思之前,我想提请你注意最近的 我给出了 ORG 指令(或任何指定的原点)链接器)与 BIOS 使用的 segment:offset 对一起工作以跳转到物理地址 0x07c00.
答案详细说明了如何将 CS 复制到 DS 引用内存地址(例如变量)时会导致问题。我在总结中说:
Don't assume CS is a value we expect, and don't blindly copy CS to DS . Set DS explicitly.
关键是不要假设 CS 是我们期望的值。所以你的下一个问题可能是 - 我似乎没有使用 CS 是吗?答案是肯定的。通常,当您使用典型的 CALL 或 JMP 指令时,它看起来像这样:
call print_char
jmp somewhereelse
在16中bit-code这两个都是相对跳跃。这意味着您在内存中向前或向后跳转,但作为相对于 JMP 或 CALL 之后的指令的偏移量。您的代码在段中的位置无关紧要,因为它是您当前所在位置的 plus/minus 位移。 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
所有这些都有一个共同点。 CALLed 或 JMPed 的地址是 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 语句所在的位置手动添加了一些注释,包括有效的相关语句和可能无效的 near/indirect/absolute 语句。我还确定了 print_char
函数的位置,以及它在 call_tbl
.
中的位置
从代码后的数据区域我们确实看到 call_tbl
位于 0x7c34,它包含一个 2 字节的绝对偏移量 0x7c2c。这都是正确的,但是当您使用绝对 2 字节偏移量时,它被假定为在当前 CS 中。如果您已经阅读了这个 (我之前引用过)关于当错误的 DS 和偏移量用于引用变量时会发生什么,您现在可能会意识到这可能适用到 JMPs CALLs 使用涉及 NEAR 2 字节绝对值的绝对偏移量。
举个例子,让我们以这个并不总是有效的调用为例:
call [call_tbl]
call_tbl
从 DS:[call_tbl] 加载。当我们启动引导加载程序时,我们正确地将 DS 设置为 0x0000,这样就可以从内存地址 0x0000:0x7c34 正确检索值 0x7c2c。然后处理器将设置 IP=0x7c2c 但它假设它是相对于当前设置的 CS。由于我们不能假设 CS 是预期值,因此处理器可能会 CALL 或 JMP 到错误的位置。这完全取决于 CS:IP BIOS 用来跳转到我们的引导加载程序的内容(可能会有所不同)。
在 BIOS 对 0x0000:0x7c00 处的引导加载程序执行相当于 FAR JMP 的情况下,CS 将设置为 0x0000,IP 将设置为 0x7c00。当我们遇到 call [call_tbl]
时,它会解析为 CALL 到 CS:IP=0x0000:0x7c2c 。这是物理地址 (0x0000<<4)+0x7c2c=0x07c2c,实际上是 print_char
函数在内存中物理开始的位置。
一些 BIOS 对我们在 0x07c0:0x0000 的引导加载程序执行相当于 FAR JMP 的操作,CS 将设置为 0x07c0和 IP 到 0x0000。这也映射到物理地址 (0x07c0<<4)+0=0x07c00 。当我们遇到 call [call_tbl]
时,它会解析为 CALL 到 CS:IP=0x07c0 :0x7c2c 。这是物理地址(0x07c0<<4)+0x7c2e=0x0f82c。这显然是错误的,因为 print_char
函数位于物理地址 0x07c2c,而不是 0x0f82c。
CS 设置不正确会导致 JMP 和 CALL 指令出现问题 Near/Absolute寻址。以及任何使用 CS:
段覆盖的内存操作数。在这个
中可以找到在实模式中断处理程序中使用 CS:
覆盖的示例
解决方案
因为已经表明我们不能依赖在 BIOS 跳转到我们的代码时设置的 CS 我们可以设置 CS 我们自己。要设置 CS,我们可以对我们自己的代码执行 FAR JMP,这将设置 [=90=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[ 启动我们的引导加载程序=159=] = 0x07c0:0x0000。正如在那些环境中所讨论的那样 near/absolute CALLs 和 JMPs 将从错误的内存位置继续执行并导致我们的引导加载程序运行不正确。
那么 Bochs 为软盘映像而不是 ISO 映像呢?这是 Bochs 早期版本的一个特点。当从软盘引导时,虚拟 BIOS 跳转到 0x0000:0x7c00,当它从 ISO 映像引导时,使用 0x07c0:0x0000。这解释了为什么它的工作方式不同。这种奇怪的行为显然是由于对特别提到段 0x07c0 的 El Torito 规范之一的字面解释而产生的。 Boch 的较新版本的虚拟 BIOS 被修改为对两者都使用 0x0000:0x7c00。
这是否意味着某些 BIOS 存在 Bug?
这个问题的答案是主观的。在 IBM 的 PC-DOS(2.1 之前)的第一个版本中,引导加载程序假定 BIOS 跳转到 0x0000:0x7c00,但这并没有明确定义。一些 BIOS 制造商在 80 年代开始使用 0x07c0:0x0000 并破坏了一些早期版本的 DOS。当发现这一点时,引导加载程序被修改为表现良好,不会对使用 segment:offset 对到达物理地址 0x07c00 做出任何假设。当时人们可能认为这是一个错误,但基于 20 位 segment:offset 对引入的歧义。
自 80 年代中期以来,我认为任何假定 CS 为特定值的新引导加载程序都被错误编码。
一般问题
我一直在开发一个简单的引导加载程序,但在某些环境中遇到了一个问题,在这些环境中,此类指令不起作用:
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 to absolute memory offsets. I have discovered that I have issues if I use similar JMP 表。相对的调用和跳转似乎不受影响。像这样的代码有效:
call print_char
我采纳了 Whosebug 上讨论编写引导加载程序的注意事项的海报提出的建议。特别是我看到这个
- When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts. You can only be guaranteed that your bootloader will be loaded and run from physical address 0x07c00 and that the boot drive number is loaded into the DL register.
采纳了大家的意见,我没有依赖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
我使用带有相对偏移量的典型 CALL 打印出 B
,它似乎工作正常。在某些环境中,当我 运行 我刚得到的代码时:
B
然后它似乎停止做任何事情。它似乎正确地打印出了 B
但随后发生了意想不到的事情。
似乎可行的环境:
- QEMU 使用软盘和 ISO 启动
- VirtualBox 使用软盘和 ISO 启动
- VMWare 9 使用软盘和 ISO 启动
- DosBox 用软盘启动
- 使用软盘映像在 Debian Jessie 上正式打包 Bochs(2.6)
- Bochs 2.6.6(从源代码控制构建)在 Debian Jessie 上使用软盘映像和 ISO 映像
- 90 年代中期的 AST Premmia SMP P90 系统使用软盘和 ISO
未按预期工作的环境:
- 在 Debian Jessie 上使用 ISO 映像 正式打包 Bochs(2.6)
- 基于 486DX 的系统,带有 90 年代初使用软盘映像的 AMI BIOS。 CD 无法在此系统上启动,因此无法测试 CD。
我发现有趣的是 Bochs(2.6 版)在使用 ISO 的 Debian Jessie 上无法正常工作。当我使用相同版本的软盘启动时,它按预期工作。
在所有情况下,ISO 和软盘映像似乎都已加载并启动 运行ning,因为在 ALL 情况下它至少能够在显示器上打印出 B
。
我的问题
- 当它失败时,为什么它只打印出一个
B
,仅此而已? - 为什么有些环境可以工作而有些环境会失败?
- 这是我的代码还是 hardware/BIOS 中的错误?
- 如何修复它以便我仍然可以使用 near 间接跳转和调用表到绝对内存偏移量?我知道我可以完全避免使用这些说明,这似乎可以解决我的问题,但我希望能够了解如何以及是否可以在引导加载程序中正确使用它们。
问题
你的问题的答案隐藏在你的问题中,只是不明显。你引用了我的
- When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts. You can only be guaranteed that your bootloader will be loaded and run from physical address 0x00007c00 and that the boot drive number is loaded into the DL register.
您的代码正确设置了 DS,并设置了自己的堆栈(SS,以及 SP).你没有盲目复制CS到DS,而是你所做的是依赖CS预期值 (0x0000)。在我解释我的意思之前,我想提请你注意最近的
答案详细说明了如何将 CS 复制到 DS 引用内存地址(例如变量)时会导致问题。我在总结中说:
Don't assume CS is a value we expect, and don't blindly copy CS to DS . Set DS explicitly.
关键是不要假设 CS 是我们期望的值。所以你的下一个问题可能是 - 我似乎没有使用 CS 是吗?答案是肯定的。通常,当您使用典型的 CALL 或 JMP 指令时,它看起来像这样:
call print_char
jmp somewhereelse
在16中bit-code这两个都是相对跳跃。这意味着您在内存中向前或向后跳转,但作为相对于 JMP 或 CALL 之后的指令的偏移量。您的代码在段中的位置无关紧要,因为它是您当前所在位置的 plus/minus 位移。 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
所有这些都有一个共同点。 CALLed 或 JMPed 的地址是 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 语句所在的位置手动添加了一些注释,包括有效的相关语句和可能无效的 near/indirect/absolute 语句。我还确定了 print_char
函数的位置,以及它在 call_tbl
.
从代码后的数据区域我们确实看到 call_tbl
位于 0x7c34,它包含一个 2 字节的绝对偏移量 0x7c2c。这都是正确的,但是当您使用绝对 2 字节偏移量时,它被假定为在当前 CS 中。如果您已经阅读了这个
举个例子,让我们以这个并不总是有效的调用为例:
call [call_tbl]
call_tbl
从 DS:[call_tbl] 加载。当我们启动引导加载程序时,我们正确地将 DS 设置为 0x0000,这样就可以从内存地址 0x0000:0x7c34 正确检索值 0x7c2c。然后处理器将设置 IP=0x7c2c 但它假设它是相对于当前设置的 CS。由于我们不能假设 CS 是预期值,因此处理器可能会 CALL 或 JMP 到错误的位置。这完全取决于 CS:IP BIOS 用来跳转到我们的引导加载程序的内容(可能会有所不同)。
在 BIOS 对 0x0000:0x7c00 处的引导加载程序执行相当于 FAR JMP 的情况下,CS 将设置为 0x0000,IP 将设置为 0x7c00。当我们遇到 call [call_tbl]
时,它会解析为 CALL 到 CS:IP=0x0000:0x7c2c 。这是物理地址 (0x0000<<4)+0x7c2c=0x07c2c,实际上是 print_char
函数在内存中物理开始的位置。
一些 BIOS 对我们在 0x07c0:0x0000 的引导加载程序执行相当于 FAR JMP 的操作,CS 将设置为 0x07c0和 IP 到 0x0000。这也映射到物理地址 (0x07c0<<4)+0=0x07c00 。当我们遇到 call [call_tbl]
时,它会解析为 CALL 到 CS:IP=0x07c0 :0x7c2c 。这是物理地址(0x07c0<<4)+0x7c2e=0x0f82c。这显然是错误的,因为 print_char
函数位于物理地址 0x07c2c,而不是 0x0f82c。
CS 设置不正确会导致 JMP 和 CALL 指令出现问题 Near/Absolute寻址。以及任何使用 CS:
段覆盖的内存操作数。在这个
CS:
覆盖的示例
解决方案
因为已经表明我们不能依赖在 BIOS 跳转到我们的代码时设置的 CS 我们可以设置 CS 我们自己。要设置 CS,我们可以对我们自己的代码执行 FAR JMP,这将设置 [=90=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[ 启动我们的引导加载程序=159=] = 0x07c0:0x0000。正如在那些环境中所讨论的那样 near/absolute CALLs 和 JMPs 将从错误的内存位置继续执行并导致我们的引导加载程序运行不正确。
那么 Bochs 为软盘映像而不是 ISO 映像呢?这是 Bochs 早期版本的一个特点。当从软盘引导时,虚拟 BIOS 跳转到 0x0000:0x7c00,当它从 ISO 映像引导时,使用 0x07c0:0x0000。这解释了为什么它的工作方式不同。这种奇怪的行为显然是由于对特别提到段 0x07c0 的 El Torito 规范之一的字面解释而产生的。 Boch 的较新版本的虚拟 BIOS 被修改为对两者都使用 0x0000:0x7c00。
这是否意味着某些 BIOS 存在 Bug?
这个问题的答案是主观的。在 IBM 的 PC-DOS(2.1 之前)的第一个版本中,引导加载程序假定 BIOS 跳转到 0x0000:0x7c00,但这并没有明确定义。一些 BIOS 制造商在 80 年代开始使用 0x07c0:0x0000 并破坏了一些早期版本的 DOS。当发现这一点时,引导加载程序被修改为表现良好,不会对使用 segment:offset 对到达物理地址 0x07c00 做出任何假设。当时人们可能认为这是一个错误,但基于 20 位 segment:offset 对引入的歧义。
自 80 年代中期以来,我认为任何假定 CS 为特定值的新引导加载程序都被错误编码。