![](/img/trans.png)
[英]Should using MOV instruction to set SS to 0x0000 cause fault #GP(0) in 64-bit mode?
[英]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指令(如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!
应该正确显示。 我将在本答案的其余部分中使用此示例。
在 Virtual Box 的情况下,在加载物理地址 0x00007c00 的引导扇区后,它将有效地执行相当于FAR JMP到 0x0000:0x7c00 的操作。 FAR JMP (或等效的)不仅会跳转到给定的地址,还会将CS和IP设置为指定的值。 FAR JMP到 0x0000:0x7c00 将设置CS = 0x0000 和IP = 0x7c00。
如果不熟悉 16 位段:偏移量对背后的计算以及它们如何映射到物理地址,那么本文档是理解该概念的一个相当好的起点。 从 16 位段获取物理内存地址的一般方程:偏移对是(segment<<4)+offset = 20-bit physical address
。
由于 VirtualBox 使用 0x0000:0x7c00 的CS:IP ,它将在 (0x0000<<4)+0x7c00 = 20 位物理地址 0x07c00 的物理地址开始执行代码。 请注意,不能保证在所有环境中都是如此。 由于段:偏移对的性质,引用物理地址 0x07c00 的方法不止一种。 有关正确处理此问题的方法,请参阅此答案末尾的部分。
假设我们使用的是 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
。 我还在代码中添加了一些注释,以显示我们的数据部分在哪里(以及字母P
和A
在代码后面的位置)。
如果你在 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。 该内存地址恰好位于中断向量表中间的内存底部。 显然这不是我们想要的!
有几种方法可以解决这个问题。 最简单(也是首选的方法)是实际将适当的段放入DS中,而不依赖于我们的程序运行时CS可能是什么。 由于我们将ORG设置为 0x0000,因此我们需要数据段 ( 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:0x24
。 0x07c0:0x24
,这将转换为 (0x07c0<<4)+0x24 = 0x07c24 的物理地址。 这就是我们想要的,因为我们的引导加载程序由 BIOS 从该位置开始物理地放入内存中,因此它应该正确引用我们的msg变量。
故事的道德启示? 无论您对ORG使用什么,当我们启动程序时, DS寄存器中都应该有一个适用的值。我们应该明确设置它,而不是依赖于CS中的内容。
使用原始代码,前 2 个字符打印出乱码,但后两个字符没有。 正如在上一节中讨论的那样,前 2 个字符无法打印是有原因的,但是最后 2 个字符呢?
让我们更仔细地检查第三个字符O
的反汇编:
16: b0 4f mov al,0x4f ; 0x4f = 'O'
由于我们使用了立即数(常量)值并将其移入寄存器AL中,因此字符本身被编码为指令的一部分。 它不依赖于通过DS寄存器进行的内存访问。 因此,最后 2 个字符可以正确显示。
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
是物理地址 (0x0000<<4)+0x7c24 = 0x07c24。 这是加载引导加载程序的正确内存位置,也是我们的msg字符串的正确位置。 所以它有效。
使用ORG 0x7c00
是个坏主意吗? 不,没关系。 但我们有一个微妙的问题需要解决。 如果另一个 Virtual PC 环境或真实硬件没有使用 0x0000:0x7c00 的CS:IP FAR JMP到我们的引导加载程序,会发生什么情况? 这个有可能。 许多物理 PC 的 BIOS 实际上相当于远跳到0x07c0:0x0000
。 正如我们已经看到的那样,这也是物理地址0x07c00
。 在那种环境下,当我们的代码运行时CS = 0x07c0。 如果我们使用将CS复制到DS的原始代码, DS现在也有 0x07c0。 现在观察在这种情况下这段代码会发生什么:
7c08: a0 24 7c mov al,ds:0x7c24
在这种情况下DS = 0x07c0。 当程序实际运行时,我们现在有类似于mov al,0x07c0:0x7c24
的东西。 呃,那看起来很糟糕。 这转化为物理地址是什么意思? (0x07c0<<4)+0x7c24 = 0x0F824。 它位于我们的引导加载程序之上的某个位置,它将包含计算机启动后出现的所有内容。 可能为零,但应假定为垃圾。 显然不是我们的味精字符串被加载的地方!
那么我们如何解决这个问题呢? 为了修改 Ross Ridge 的建议,并听取我之前给出的关于将DS明确设置为我们真正想要的段的建议(不要假设CS是正确的然后盲目地复制到DS )我们应该在引导加载程序启动时将 0x0000 放入DS如果我们使用ORG 0x7c00
。 所以我们可以改变这段代码:
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中的什么值到达我们的引导加载程序并不重要,代码仍会为我们的数据引用适当的内存位置。
在我在之前的 StackOverflow 回答中写的一般引导加载程序技巧中,技巧 #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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.