【问题标题】:Reading from memory in 8086 real mode while using 'ORG 0x0000'使用“ORG 0x0000”时以 8086 实模式从内存中读取
【发布时间】:2016-03-28 10:50:21
【问题描述】:

我一直在搞乱 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 组装了它,并且遇到了同样的问题,这意味着这显然是我的错。

它打印如下内容:

【问题讨论】:

  • 如果您将 DS 设置为 0,则需要使用 ORG 0x7c00,这就是您使用 push cs; pop ds 所做的事情
  • 使用 ORG 0 时,汇编器假定生成的二进制文件在某个段的偏移量 0 处加载。但是 BIOS 实际上在段 0 的偏移量 0x7c00 处加载它。这意味着对于 ORG 0,汇编器认为 @ 987654328@ 介于偏移量 0x0000 和 0x0200 之间,但实际上它介于偏移量 0x7c00 和 0x7e00 之间。使用 ORG 0x7c00 汇编器现在知道它的实际加载位置,因此它可以使用正确的值 msg.
  • 如果 Ross Ridge 不愿意发布他的评论作为答案,您 (DUUI) 应该发布答案并接受它。谢谢!
  • @RossRidge :当您遇到跳转到远地址 0x07c0:0x0000 的仿真器或硬件时会发生什么(许多环境仍然如此)。我真的建议您设置您的 ORG 并将值编码到您想要的 DS 中。因此,如果您想使用 [ORG 0x7c00] 您的引导加载程序可以从 xor ax, ax mov ds, ax 开始。如果您想使用 [ORG 0x0000] ,请从 mov ax, 0x07c0 mov ds, ax 开始。这个想法是您根据满足您使用的 ORG 的内容明确设置 DS
  • 我在我的General Bootloader Tips Stackoverflow 回答中介绍了这个问题中的一些问题。最近我在另一个 Stack Overflow Answer Don't Assume 1st Stage is Invoked by BIOS with CS:IP=0x0000:0x7c00 中写了一个部分,这对于编写针对更广泛的真实和仿真硬件的引导加载程序可能很有用。

标签: assembly nasm x86-16 bootloader fasm


【解决方案1】:

ORG 指令

当您在汇编程序的顶部指定一个 ORG 指令(如 ORG 0x0000)并使用 BITS 16 时,您将通知 NASM 在将标签解析为代码时和数据,将生成的绝对偏移量将基于在 ORG 中指定的起始偏移量(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 Pairs

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

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

由于 VirtualBox 使用 0x0000:0x7c00 的 CS:IP ,它将在 (0x0000


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

假设我们正在使用 VirtualBox 并且上一节中的上述信息被认为是正确的,那么在进入我们的引导加载程序时 CS = 0x0000 和 IP = 0x7c00。如果我们采用示例代码(使用ORG 0x0000)我在此答案的第一部分中编写并查看反汇编信息(我将使用 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 开始。我想这样做是因为我在原始汇编代码中使用了ORG 0x0000。我还在代码中添加了一些 cmets 以显示我们的数据部分在哪里(以及字母 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:0x24mov al,0x0000:0x24 完全相同。此语法无效,但我将 DS 替换为 0x0000 以说明问题。 0x0000:0x24 是我们的代码在执行时会尝试读取我们的信 P 的地方。可是等等!即物理地址 (0x0000

有几种方法可以解决这个问题。最简单(也是首选的方法)是将正确的段实际放入 DS,而不是依赖于我们的程序运行时可能是什么 CS。由于我们将 ORG 设置为 0x0000,我们需要有一个 Data Segment(DS) = 0x07c0 。段:偏移量对 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,这将转换为 (0x07c0msg 变量。

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


为什么要打印立即数?

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

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

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

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


Ross Ridge 的建议及其在 VirtualBox 中的工作原理

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

使用我的第一个示例并将ORG 0x0000 修改为ORG 0x7c00,然后组装它。 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 是物理地址 (0x0000msg 字符串的正确位置。所以它起作用了。

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

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

DS=0x07c0 在这种情况下。当程序实际运行时,我们现在有类似mov al,0x07c0:0x7c24 的东西。呃,看起来很糟糕。这将转换为物理地址是什么? (0x07c0msg 字符串被加载的地方!

那么我们如何解决这个问题呢?修改 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 中的什么值可能已用于到达我们的引导加载程序,代码仍然会为我们的数据引用适当的内存位置。


不要假设 1st Stage 由 BIOS 调用 CS:IP=0x0000:0x7c00

在我在之前的 StackOverflow 答案中写的 General Bootloader Tips 中,提示 #1 非常重要:

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

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

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


总结

不要假设 CS 是我们期望的值,也不要盲目地将 CS 复制到 DS 。明确设置 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

【讨论】:

  • @DUUI 您的问题在其他问题中被间接提出,但您的问题是过去几个月来我第一次有机会编写一个足够通用的答案来帮助您,并希望能帮助某人一个类似(但可能不重复)的问题。通常会发生的情况是,您将代码移动到另一个环境(VirtualBox 以外的环境),并且突然由于莫名其妙的原因它无法正常工作。感觉需要一个答案。
  • push 带有立即操作数是 186+ 级指令。
  • @ecm 这是非常正确的。这是一个旧答案,但回顾这个问题,很明显我的代码是基于 OP 在他自己的代码中推送一个立即数的事实。
猜你喜欢
  • 2021-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 2014-01-18
相关资源
最近更新 更多