使用 'ORG 0x0000' 时以 8086 实模式从内存中读取

Reading from memory in 8086 real mode while using 'ORG 0x0000'

我一直在摆弄 x86-16 汇编和 运行 它与 VirtualBox。出于某种原因,当我从内存中读取并尝试将其打印为字符时,我得到的结果与我的预期完全不同。但是,当我将字符硬编码为指令的一部分时,它工作正常。 这是代码:

ORG 0
BITS 16

push word 0xB800        ; Address of text screen video memory in real mode for colored monitors
push cs
pop ds                  ; ds = cs
pop es                  ; es = 0xB800
jmp start

; input = di (position*2), ax (character and attributes)
putchar:
    stosw
    ret

; input = si (NUL-terminated string)
print:
    cli
    cld
    .nextChar:
        lodsb   ; mov al, [ds:si] ; si += 1
        test al, al
        jz .finish
        call putchar
        jmp .nextChar
    .finish:
        sti
        ret

start:
    mov ah, 0x0E
    mov di, 8

    ; should print P
    mov al, byte [msg]
    call putchar

    ; should print A
    mov al, byte [msg + 1]
    call putchar

    ; should print O
    mov al, byte [msg + 2]
    call putchar

    ; should print !
    mov al, byte [msg + 3]
    call putchar

    ; should print X
    mov al, 'X'
    call putchar

    ; should print Y
    mov al, 'Y'
    call putchar

    cli
    hlt

msg: db 'PAO!', 0

; Fill the rest of the bytes upto byte 510 with 0s
times 510 - ($ - $$) db 0

; Header
db 0x55
db 0xAA

打印标签和其中的说明可以忽略,因为我还没有使用它,因为我一直在尝试打印存储在内存中的字符。我用 FASM 和 NASM 组装了它,但遇到同样的问题,这显然是我的错。

它打印出如下内容:

ORG 指令

当您在 assembler 程序的顶部指定 ORG 指令(例如 ORG 0x0000 并使用 BITS 16 时,您正在通知NASM 将标签解析为代码和数据时,将生成的绝对偏移量将基于 ORG[=230 中指定的起始偏移量=](16 位代码将被限制为一个 WORD/2 字节的偏移量)。

如果您在开头有 ORG 0x0000 并在代码开头放置标签 start:,则 start 将具有 0x0000 的绝对偏移量。如果您使用 ORG 0x7C00,那么标签 start 将具有 0x7c00 的绝对偏移量。这将适用于任何数据标签和代码标签。

我们可以简化您的示例,以查看在处理数据变量和硬编码字符时生成的代码中发生了什么。尽管此代码执行的操作与您的代码不完全相同,但它足够接近以显示哪些有效,哪些无效。

示例使用 ORG 0x0000:

BITS 16
ORG 0x0000

start:
    push cs
    pop  ds      ; DS=CS
    push 0xb800
    pop es       ; ES = 0xB800 (video memory)
    mov ah, 0x0E ; AH = Attribute (yellow on black)

    mov al, byte [msg]
    mov [es:0x00], ax   ; This should print letter 'P'
    mov al, byte [msg+1]
    mov [es:0x02], ax   ; This should print letter 'A'
    mov al, 'O'
    mov [es:0x04], ax   ; This should print letter 'O'
    mov al, '!'
    mov [es:0x06], ax   ; This should print letter '!'

    cli
    hlt

msg: db "PA"

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

如果你在 VirtualBox 上 运行 这个,前 2 个字符将是垃圾,而 O! 应该正确显示。我将在本回答的其余部分中使用此示例。


VirtualBox / CS:IP / Segment:Offset 对

在 Virtual Box 的情况下,它会在加载后有效地执行 FAR JMP 到 0x0000:0x7c00 的 等效 物理地址 0x00007c00 处的引导扇区。 FAR JMP(或等效)不仅会跳转到给定地址,还会设置 CSIP 到指定的值。 FAR JMP 到 0x0000:0x7c00 将设置 CS = 0x0000 和 IP = 0x7c00 。

如果不熟悉 16 位 segment:offset 对背后的计算以及它们如何映射到物理地址,那么这个 document 是理解该概念的一个相当好的起点。从 16 位 segment:offset 对获取物理内存地址的一般方程是 (segment<<4)+offset = 20-bit physical address .

由于 VirtualBox 使用 0x0000:0x7c00 的 CS:IP,它将在 (0x0000<<4)+0x7c00 = 20- 的物理地址开始执行代码位物理地址 0x07c00 。请注意,不能保证在所有环境中都是如此。由于 segment:offset 对的性质,引用物理地址 0x07c00 的方法不止一种。请参阅此答案末尾的部分,了解如何正确处理此问题。


您的引导加载程序出了什么问题?

假设我们使用的是 VirtualBox 并且上一节中的上述信息被认为是正确的,那么 CS = 0x0000 和 IP = 0x7c00进入我们的引导加载程序后。如果我们采用示例代码(使用 ORG 0x0000),我在本答案的第一部分中编写并查看 disassembled 信息(我将使用 objdump 输出)我们会看到这个:

objdump -Mintel -mi8086 -D -b binary --adjust-vma=0x0000 boot.bin

00000000 <.data>:
   0:   0e                      push   cs
   1:   1f                      pop    ds
   2:   68 00 b8                push   0xb800
   5:   07                      pop    es
   6:   b4 0e                   mov    ah,0xe
   8:   a0 24 00                mov    al,ds:0x24
   b:   26 a3 00 00             mov    es:0x0,ax
   f:   a0 25 00                mov    al,ds:0x25
  12:   26 a3 02 00             mov    es:0x2,ax
  16:   b0 4f                   mov    al,0x4f
  18:   26 a3 04 00             mov    es:0x4,ax
  1c:   b0 21                   mov    al,0x21
  1e:   26 a3 06 00             mov    es:0x6,ax
  22:   fa                      cli
  23:   f4                      hlt
  24:   50                      push   ax          ; Letter 'P'
  25:   41                      inc    cx          ; Letter 'A'
        ...
 1fe:   55                      push   bp
 1ff:   aa                      stos   BYTE PTR es:[di],al

由于汇编成二进制文件时ORG信息丢失,所以我使用--adjust-vma=0x0000使得第一列值(内存地址)从0x0000开始。我想这样做是因为我在原始 assembler 代码中使用了 ORG 0x0000。我还在代码中添加了一些注释,以显示我们的数据部分在哪里(以及代码后放置字母 PA 的位置)。

如果你要在 VirtualBox 中 运行 这个程序,前 2 个字符将显示为乱码。那是为什么呢?首先回想一下 VirtualBox 通过将 CS 设置为 0x0000 并将 IP 设置为 0x7c00 来访问我们的代码。此代码然后将 CS 复制到 DS:

   0:   0e                      push   cs
   1:   1f                      pop    ds

既然CS是零,那么DS也是零。现在让我们看看这一行:

   8:   a0 24 00                mov    al,ds:0x24

ds:0x24实际上是我们数据段中msg变量的编码地址。偏移量 0x24 处的字节具有值 P(0x25 具有 A)。您可能会看到哪里出了问题。我们的 DS = 0x0000 所以 mov al,ds:0x24 真的和 mov al,0x0000:0x24 一样。此语法无效,但我将 DS 替换为 0x0000 以表明观点。 0x0000:0x24 是我们的代码在执行时将尝试从中读取我们的字母 P 的地方。可是等等!即物理地址(0x0000<<4)+0x24 = 0x00024。这个内存地址恰好在中断向量中间的内存底部table。显然这不是我们想要的!

有几种方法可以解决这个问题。最简单(也是首选的方法)是实际将适当的段放入 DS,而不是依赖于 CS 当我们的程序 运行秒。由于我们将 ORG 设置为 0x0000,因此我们需要一个 Data Segment(DS) = 0x07c0 。 segment:offset 对 0x07c0:0x0000 = 物理地址 0x07c00 。这是我们的引导加载程序的地址。所以我们所要做的就是通过替换来修改代码:

    push cs
    pop  ds      ; DS=CS

有:

    push 0x07c0
    pop  ds      ; DS=0x07c0 

VirtualBox 中的 运行 时,此更改应提供正确的输出。现在让我们看看为什么。此代码未更改:

   8:   a0 24 00                mov    al,ds:0x24

现在执行时DS=0x07c0。这就像说 mov al,0x07c0:0x240x07c0:0x24,这将转换为 (0x07c0<<4)+0x24 = 0x07c24 的物理地址。这就是我们想要的,因为我们的引导加载程序由 BIOS 从该位置开始物理地放入内存中,因此它应该正确引用我们的 msg 变量。

故事的寓意?无论你用什么 ORG 都应该在 DS 寄存器中有一个适用的值,当我们启动我们的 program.We 应该明确设置它时,而不是依赖于 CS.

中的内容

为什么要打印立即值?

原来的代码,前两个字符打印出乱码,但后两个字符没有。正如上一节中所讨论的,前 2 个字符无法打印是有原因的,但是最后 2 个字符呢?

让我们仔细检查第3个字符O的反汇编:

  16:   b0 4f                   mov    al,0x4f        ; 0x4f = 'O'

由于我们使用了立即数(常量)值并将其移入寄存器 AL,字符本身被编码为指令的一部分。它不依赖于通过 DS 寄存器的内存访问。因此,最后 2 个字符可以正确显示。


Ross Ridge 的建议及其在 VirtualBox 中有效的原因

Ross Ridge 建议我们使用 ORG 0x7c00,您发现它有效。为什么会这样?这个解决方案理想吗?

使用我的第一个示例并将 ORG 0x0000 修改为 ORG 0x7c00,然后 assemble。 objdump 会提供此反汇编:

objdump -Mintel -mi8086 -D -b binary  --adjust-vma=0x7c00 boot.bin

boot.bin:     file format binary   
Disassembly of section .data:

00007c00 <.data>:
    7c00:       0e                      push   cs
    7c01:       1f                      pop    ds
    7c02:       68 00 b8                push   0xb800
    7c05:       07                      pop    es
    7c06:       b4 0e                   mov    ah,0xe
    7c08:       a0 24 7c                mov    al,ds:0x7c24
    7c0b:       26 a3 00 00             mov    es:0x0,ax
    7c0f:       a0 25 7c                mov    al,ds:0x7c25
    7c12:       26 a3 02 00             mov    es:0x2,ax
    7c16:       b0 4f                   mov    al,0x4f
    7c18:       26 a3 04 00             mov    es:0x4,ax
    7c1c:       b0 21                   mov    al,0x21
    7c1e:       26 a3 06 00             mov    es:0x6,ax
    7c22:       fa                      cli
    7c23:       f4                      hlt
    7c24:       50                      push   ax          ; Letter 'P'
    7c25:       41                      inc    cx          ; Letter 'A'
        ...
    7dfe:       55                      push   bp
    7dff:       aa                      stos   BYTE PTR es:[di],al

VirtualBox 在跳转到我们的引导加载程序时将 CS 设置为 0x0000。我们的原始代码然后将 CS 复制到 DS,因此 DS = 0x0000。现在观察 ORG 0x7c00 指令对我们生成的代码做了什么:

    7c08:       a0 24 7c                mov    al,ds:0x7c24

注意我们现在如何使用偏移量 0x7c24!这就像 mov al,0x0000:0x7c24 是物理地址 (0x0000<<4)+0x7c24 = 0x07c24。这是加载引导加载程序的正确内存位置,也是 msg 字符串的正确位置。所以它有效。

使用 ORG 0x7c00 是个坏主意吗?不,没关系。但我们有一个微妙的问题需要解决。如果另一个 Virtual PC 环境或真实硬件没有使用 CS:IP 0x0000:0x7c00 FAR JMP 到我们的引导加载程序,会发生什么情况]?这个有可能。许多物理 PC 的 BIOS 实际上相当于远跳到 0x07c0:0x0000。正如我们已经看到的,这也是物理地址 0x07c00。在那种环境下,当我们的代码运行s CS = 0x07c0.如果我们使用将 CS 复制到 DS 的原始代码,那么 DS 现在也有 0x07c0。现在观察这段代码在那种情况下会发生什么:

    7c08:       a0 24 7c                mov    al,ds:0x7c24

DS=0x07c0 在这种情况下。当程序实际 运行 时,我们现在有类似 mov al,0x07c0:0x7c24 的东西。呃,那看起来很糟糕。这转化为物理地址是什么意思? (0x07c0<<4)+0x7c24 = 0x0F824。它位于我们的引导加载程序之上的某个位置,它将包含计算机启动后出现的所有内容。可能为零,但应假定为垃圾。显然不是我们的 msg 字符串被加载的地方!

那么我们如何解决这个问题呢?修改 Ross Ridge 的建议,并听取我之前给出的关于将 DS 明确设置为我们真正想要的段的建议(不要假设 CS 是正确的,然后盲目复制到 DS) 如果我们使用 ORG 0x7c00,我们应该在引导加载程序启动时将 0x0000 放入 DS。所以我们可以更改此代码:

ORG 0x7c00

start:
    push cs
    pop  ds      ; DS=CS

至:

ORG 0x7c00

start:
    xor ax, ax   ; ax=0x0000
    mov ds, ax   ; DS=0x0000

这里我们不依赖 CS 中不受信任的值。我们只需将 DS 设置为在给定我们使用的 ORG 的情况下有意义的段值。您可以像之前那样压入 0x0000 并将其弹出到 DS 中。我更习惯于将寄存器归零并将其移动到 DS.

通过采用这种方法,CS 中的什么值可能已被用于到达我们的引导加载程序并不重要,代码仍会为我们的数据引用适当的内存位置.


不要假设第一阶段由 BIOS 调用 CS:IP=0x0000:0x7c00

在我之前的 Whosebug 回答中写的 中,技巧 #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.

BIOS 可以使用 jmp 0x07c0:0x0000 对我们的代码进行 FAR JMP(或等效),一些仿真器和真实硬件就是这样做的。其他人像 VirtualBox 一样使用 jmp 0x0000:0x7c00

我们应该通过将 DS 明确设置为我们需要的值来解决这个问题,并将其设置为对我们在 ORG[ 中使用的值有意义的值=230=]指令。


总结

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

如果我们像前面讨论的那样将 DS 适当地设置为 0x07c0,那么您的代码可以固定为使用您最初使用的 ORG 0x0000。这可能看起来像:

ORG 0
BITS 16

push word 0xB800        ; Address of text screen video memory in real mode for colored monitors
push 0x07c0
pop ds                  ; DS=0x07c0 since we use ORG 0x0000
pop es

或者我们可以这样使用 ORG 0x7c00

ORG 0x7c00
BITS 16

push word 0xB800        ; Address of text screen video memory in real mode for colored monitors
push 0x0000
pop ds                  ; DS=0x0000 since we use ORG 0x7c00
pop es