Near call/jump 表并不总是在引导加载程序中工作 [英] Near call/jump tables don't always work in a bootloader

查看:33
本文介绍了Near call/jump 表并不总是在引导加载程序中工作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

一般问题

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

mov si, call_tbl ;SI=调用表指针呼叫 [call_tbl] ;使用近间接绝对调用调用 print_char;通过内存操作数呼叫 [ds:call_tbl] ;使用近间接绝对调用调用 print_char;通过带段覆盖的内存操作数在 [si] 附近打电话;使用近间接绝对调用调用 print_char;通过注册

这些中的每一个都碰巧涉及到 CALLa> 到绝对内存偏移量.我发现如果我使用类似的 JMP 桌子.相对的调用和跳转似乎不受影响.像这样的代码有效:

调用print_char

我采纳了 Stackoverflow 上讨论编写引导加载程序的注意事项的海报提出的建议.特别是我看到了这个Stackoverflow回答一般引导加载程序提示.第一个提示是:

<块引用>

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

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

<小时>

最小示例

要测试的代码如下:

[ORG 0x7c00][第 16 位]节.text主要的:异或斧头,斧头mov ds, ax ;DS=0x0000 因为 OF​​FSET=0x7c00cli;关闭潜在错误 8088 的中断mov ss, 斧头mov sp, 0x7c00 ;SS:SP = 堆栈低于 0x7c00斯蒂;重新打开中断mov si, call_tbl ;SI=调用表指针mov al, [char_arr] ;打印B"的第一个字符(开始)调用 print_char ;直接调用print_char(相对跳转)移动 al, [char_arr+1] ;打印M"的字符(中间)呼叫 [call_tbl] ;使用近间接绝对调用调用 print_char;通过内存操作数呼叫 [ds:call_tbl] ;使用近间接绝对调用调用 print_char;通过带段覆盖的内存操作数在 [si] 附近打电话;使用近间接绝对调用调用 print_char;通过注册mov al, [char_arr+2] ;打印E"的第三个字符(结束)调用 print_char ;直接调用print_char(相对跳转)结尾:命令行.endloop:hlt;暂停处理器jmp .endloop打印字符:移动啊, 0x0e ;将 CHAR/Attrib 写为 TTY移动 bx, 0x00 ;第 0 页整数 0x10返回;近呼地址表,一目了然call_tbl: dw print_char;简单的字符数组char_arr:数据库'BME';引导扇区填充乘以 510-($-$$) db 0dw 0xAA55

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

nasm -f bin boot.asm -o boot.bindd if=/dev/zero of=floppy.img bs=1024 count=1440dd if=boot.bin of=floppy.img conv=notrunc目录cp软盘.img iso/genisoimage -quiet -V 'MYBOOT' -input-charset iso8859-1 -o myos.iso -b floppy.img -hide floppy.img iso

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

<小时>

预期与实际结果

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

<块引用>

BMMME

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

<块引用>

B

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

似乎有效的环境:

  • QEMU 用软盘和 ISO 启动
  • VirtualBox 使用软盘和 ISO 启动
  • VMWare 9 使用软盘和 ISO 启动
  • DosBox 用软盘启动
  • 使用软盘映像在 Debian Jessie 上正式打包 Bochs(2.6)
  • 使用软盘映像和 ISO 映像在 Debian Jessie 上的 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 和软盘映像似乎都加载并开始运行,因为在 ALL 情况下,它至少能够打印出 B 显示.

<小时>

我的问题

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

解决方案

问题

你的问题的答案隐藏在你的问题中,只是不明显.您引用了我的一般引导加载程序提示:

<块引用>

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

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

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

<块引用>

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

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

调用print_charjmp在别处

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

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

call [call_tbl] ;使用近间接绝对调用调用 print_char;通过内存操作数呼叫 [ds:call_tbl] ;使用近间接绝对调用调用 print_char;通过带段覆盖的内存操作数在 [si] 附近打电话;使用近间接绝对调用调用 print_char;通过注册

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

objdump -mi8086 -Mintel -D -b binary boot.bin --adjust-vma 0x7c00boot.bin:文件格式二进制.data 节的反汇编:00007c00<.数据>:7c00: 31 c0 xor ax,ax7c02: 8e d8 mov ds,ax7c04:fa cli7c05: 8e d0 mov ss,ax7c07: bc 00 7c mov sp,0x7c007c0a:fb sti7c0b: 是 34 7c mov si,0x7c347c0e:a0 36 7c 移动,ds:0x7c367c11: e8 18 00 呼叫 0x7c2c ;相对调用工作7c14: a0 37 7c 移动 al,ds:0x7c377c17: ff 16 34 7c 调用 WORD PTR ds:0x7c34 ;接近/间接/绝对调用7c1b: 3e ff 16 34 7c call WORD PTR ds:0x7c34 ;接近/间接/绝对调用7c20: ff 14 call WORD PTR [si] ;接近/间接/绝对调用7c22: a0 38 7c 移动 al,ds:0x7c387c25: e8 04 00 呼叫 0x7c2c ;相对调用工作7c28:fa cli7c29: f4 hlt7c2a:eb fd jmp 0x7c297c2c: b4 0e mov ah,0xe ;print_char 的开头7c2e: bb 00 00 mov bx,0x0 ;功能7c31: cd 10 int 0x107c33:c3 ret7c34: 2c 7c sub al,0x7c ;print_char 的 0x7c2c 偏移量;仅在 call_tbl 中输入7c36: 42 公司 dx ;0x42 = ASCII 'B'7c37: 4d dec bp ;0x4D = ASCII 'M'7c38: 45 inc bp ;0x45 = ASCII 'E'...7dfd: 00 55 aa 添加字节 PTR [di-0x56],dl

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

从代码后面的数据区我们确实看到 call_tbl 位于 0x7c34 并且它包含 0x7c2c 的 2 字节绝对偏移量.这都是正确的,但是当您使用绝对 2 字节偏移量时,它被假定在当前 CS 中.如果您已阅读此 Stackoverflow 答案(我之前参考过的),了解当错误的 DS 和偏移量用于引用变量,您现在可能意识到这可能适用于 JMPCALL 使用涉及 NEAR 的绝对偏移量2 字节绝对值.

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

调用 [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] 时,它会解析为 CALL 到 CS:IP=0x0000:0x7c2c .这是物理地址 (0x0000<<4)+0x7c2c=0x07c2c,这实际上是 print_char 函数在内存中的物理开始位置.

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

CS 设置不正确会导致JMPCALL 指令出现问题,这些指令执行近/绝对寻址.以及使用 CS: 段覆盖的任何内存操作数.可以在此 Stackoverflow 答案CS: 覆盖的示例一个>

<小时>

解决方案

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

jmp 0x0000:$+5

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

 jmp 0x0000:farjmpfarjmp:

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

当然,如果我们使用 ORG 0x0000 那么我们需要将 CS 设置为 0x07c0.这是因为(0x07c0<<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 启动我们的引导加载程序.正如在那些接近/绝对的环境中所讨论的,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,但这并没有明确定义.一些 80 年代的 BIOS 制造商开始使用 0x07c0:0x0000,并破坏了 DOS 的一些早期版本.当发现这一点时,引导加载程序被修改为表现良好,不会对使用哪个段:偏移对到达物理地址 0x07c00 做出任何假设.当时人们可能认为这是一个错误,但这是基于 20 位段:偏移对引入的歧义.

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

General Problem

I've been developing a simple bootloader and have stumbled on a problem on some environments where instructions like these don't work:

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

Each one of these happen to involve indirect near CALL to absolute memory offsets. I have discovered that I have issues if I use similar JMP tables. Calls and Jumps that are relative don't seem to be affected. Code like this works:

call print_char 

I have taken the advice presented on Stackoverflow by posters discussing the dos and don'ts of writing a bootloader. In particular I saw this Stackoverflow answer with General Bootloader Tips. The first tip was:

  1. 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.

Taking all the advice, I didn't rely on CS, I set up a stack, and set DS to be appropriate for the ORG (Origin offset) I used. I have created a Minimal Complete Verifiable example that demonstrates the problem. I built this using NASM, but it doesn't seem to be a problem specific to NASM.


Minimal Example

The code to test is as follows:

[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

I build both an ISO image and a 1.44MB floppy image for test purposes. I'm using a Debian Jessie environment but most Linux distros would be similar:

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

I end up with a floppy disk image called floppy.img and an ISO image called myos.iso.


Expectations vs Actual Results

Under most conditions this code works, but in a number of environments it doesn't. When it works it simply prints this on the display:

BMMME

I print out B using a typical CALL with relative offset it seems to work fine. In some environments when I run the code I just get:

B

And then it appears to just stop doing anything. It seems to print out the B properly but then something unexpected happens.

Environments that seem to work:

  • QEMU booted with floppy and ISO
  • VirtualBox booted with floppy and ISO
  • VMWare 9 booted with floppy and ISO
  • DosBox booted with floppy
  • Officially packaged Bochs(2.6) on Debian Jessie using floppy image
  • Bochs 2.6.6(built from source control) on Debian Jessie using floppy image and ISO image
  • AST Premmia SMP P90 system from mid 90s using floppy and ISO

Environments that don't work as expected:

  • Officially packaged Bochs(2.6) on Debian Jessie using ISO image
  • 486DX based system with AMI BIOS from the early 90s using floppy image. CDs won't boot on this system so the CD couldn't be tested.

What I find interesting is that Bochs (version 2.6) doesn't work as expected on Debian Jessie using an ISO. When I boot from the floppy with the same version it works as expected.

In all cases the ISO and the floppy image seemed to load and start running since in ALL cases it was at least able to print out B on the display.


My Questions

  • When it fails, why does it only print out a B and nothing more?
  • Why do some environments work and others fail?
  • Is this a bug in my code or the hardware/BIOS?
  • How can I fix it so that I can still use near indirect Jump and Call tables to absolute memory offsets? I am aware I can avoid these instructions altogether and that seems to solve my problem, but I'd like to be able to understand how and if I can use them properly in a bootloader.

解决方案

The Problem

The answer to your question is buried in your question, it just isn't obvious. You quoted my General Bootloader Tips:

  1. 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.

Your code correctly sets up DS, and sets its own stack (SS, and SP). You didn't blindly copy CS to DS, but what you do do is rely on CS being an expected value (0x0000). Before I explain what I mean by that, I'd like to draw your attention to a recent Stackoverflow answer I gave about how the ORG directive (or the origin point specified by any linker) works together with the segment:offset pair used by the BIOS to jump to physical address 0x07c00.

The answer details how CS being copied to DS can cause problems when referencing memory addresses (variables for example). In the summary I stated:

Don't assume CS is a value we expect, and don't blindly copy CS to DS . Set DS explicitly.

The key thing is Don't assume CS is a value we expect. So your next question may be - I don't seem to be using CS am I? The answer is yes. Normally when you use a typical CALL or JMP instruction it looks like this:

call print_char
jmp somewhereelse

In 16 bit-code both of these are relative jumps. This means that you jump forward or back in memory but as an offset relative to the instruction right after the JMP or CALL. Where your code is placed within a segment doesn't matter as it is a plus/minus displacement from where you currently are. What the current value of CS is doesn't actually matter with relative jumps, so they should work as expected.

Your example of instructions that don't always seem to work correctly included:

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

All of these have one thing in common. The addresses that are CALLed or JMPed are ABSOLUTE, not relative. The offset of the label will be influenced by the ORG (origin point of the code). If we look at a disassembly of your code we will see this:

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

I've manually added some comments where the CALL statements are, including both the relative ones that work and the near/indirect/absolute ones may not. I've also identified where the print_char function is, and where it was in the call_tbl.

From the data area after the code we do see that the call_tbl is at 0x7c34 and it contains a 2 byte absolute offset of 0x7c2c. This is all correct, but when you use an absolute 2-byte offset it is assumed to be in the current CS. If you have read this Stackoverflow answer (that I referenced earlier) about what happens when the wrong DS and offset is used to reference a variable, you might now realize that this may apply to JMPs CALLs that use absolute offsets involving NEAR 2-byte absolute values.

As an example let us take this call that doesn't always work:

call [call_tbl] 

call_tbl is loaded from DS:[call_tbl]. We properly set DS to 0x0000 when we start the bootloader so this does correctly retrieve the value 0x7c2c from memory address 0x0000:0x7c34. The processor will then set IP=0x7c2c BUT it assumes it is relative to the currently set CS. Since we can't assume CS is an expected value, the processor potentially can CALL or JMP to the wrong location. It all depends on what CS:IP the BIOS used to jump to our bootloader with (it can vary).

In the case where the BIOS does the equivalent of a FAR JMP to our bootloader at 0x0000:0x7c00, CS will be set to 0x0000 and IP to 0x7c00. When we encounter call [call_tbl] it would have resolved to a CALL to CS:IP=0x0000:0x7c2c . This is physical address (0x0000<<4)+0x7c2c=0x07c2c which is in fact where the print_char function in memory that the function physically starts at.

Some BIOSes do the equivalent of a FAR JMP to our bootloader at 0x07c0:0x0000, CS will be set to 0x07c0 and IP to 0x0000. This too maps to physical address (0x07c0<<4)+0=0x07c00 .When we encounter call [call_tbl] it would have resolved to a CALL to CS:IP=0x07c0:0x7c2c . This is physical address (0x07c0<<4)+0x7c2e=0x0f82c. This is clearly wrong since the print_char function is at physical address 0x07c2c, not 0x0f82c.

Having CS set incorrectly will cause problems for JMP and CALL instructions that do Near/Absolute addressing. As well any memory operands that use a segment override of CS:. An example of using the CS: override in a real mode interrupt handler can be found in this Stackoverflow answer


Solution

Since it has been shown that we can't rely on CS that is set when the BIOS jumps to our code we can set CS ourselves. To set CS we can do a FAR JMP to our own code which will set CS:IP to values that make sense for the ORG (origin point of the code and data) we are using. An example of such a jump if we use ORG 0x7c00:

jmp 0x0000:$+5

$+5 says to use an offset that is 5 above our current program counter. A far jmp is 5 bytes long so this has the affect of doing a far jump to the instruction after our jmp. It could have been coded this way too:

    jmp 0x0000:farjmp
farjmp:

When either of these instructions is complete CS will be set to 0x0000 and IP will be set to the offset of the next instruction. They key thing for us is that CS will be 0x0000. When paired with an ORG of 0x7c00 it will properly resolve absolute addresses so that they work properly when physically running on the CPU. 0x0000:0x7c00=(0x0000<<4)+0x7c00=physical address 0x07c00.

Of course if we use ORG 0x0000 then we need to set CS to 0x07c0. This is because (0x07c0<<4)+0x0000=0x07c00. So we could code the far jmp this way:

jmp 0x07c0:$+5

CS will be set to 0x07c0 and IP will be set to the offset of the next instruction.

The end result of all this is that we are setting CS to the segment we want, and not rely on a value that we can't guarantee when the BIOS finishes jumping to our code.


Issues with Different Environments

As we have seen the CS can matter. Most BIOSes whether in an emulator, virtual machine or real hardware do the equivalent of a far jump to 0x0000:0x7c00 and in those environments your bootloader would have worked. Some environment like older AMI Bioses and Bochs 2.6 when booting from a CD are starting our bootloader with CS:IP = 0x07c0:0x0000. As discussed in those environments near/absolute CALLs and JMPs will proceed to execute from the wrong memory locations and cause our bootloader to function incorrectly.

So what about Bochs working for a floppy image and not for an ISO image? This is a peculiarity in earlier versions of Bochs. When booting from a floppy the virtual BIOS jumps to 0x0000:0x7c00 and when it boots from an ISO image is uses 0x07c0:0x0000. This explains why it works differently. This odd behavior apparently came about because of literal interpretation of one of the El Torito specifications that specifically mentioned segment 0x07c0. Newer versions of Boch's virtual BIOSes were modified to use 0x0000:0x7c00 for both.


Does this Mean some BIOSes have a Bug?

The answer to this question is subjective. In the first versions of IBM's PC-DOS (prior to 2.1) the bootloader assumed that the BIOS jumped to 0x0000:0x7c00, but this wasn't clearly defined. Some BIOS manufacturers in the 80s started using 0x07c0:0x0000 and broke some early versions of DOS. When this was discovered bootloaders were modified to be well behaved as to not make any assumptions about what segment:offset pair was used to reach physical address 0x07c00. At the time one may have considered this a bug, but was based on the ambiguities introduced with 20-bit segment:offset pairs.

Since the mid 80s, it is my opinion that any new bootloader that assumes CS is a specific value has been coded in error.

这篇关于Near call/jump 表并不总是在引导加载程序中工作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆