我一直在搞乱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组装了它,并且遇到了同样的问题,这显然是我的错。
答案 0 :(得分:9)
当您在汇编程序顶部指定{em> ORG 指令(如ORG 0x0000
)并使用BITS 16
时,您将通知 NASM 将标签解析为代码和数据,将生成的绝对偏移量将基于 ORG 中指定的起始偏移量(16位代码将限制为偏移量为 WORD < / em> / 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位段后面的计算:偏移对以及它们如何映射到物理地址,那么这个document是理解这个概念的一个相当好的起点。从16位段获取物理内存地址的一般公式:偏移对为(segment<<4)+offset = 20-bit physical address
。
由于VirtualBox使用0x0000:0x7c00的 CS:IP ,因此它将开始在物理地址(0x0000 <&lt; 4)+ 0x7c00 = 20位物理地址0x07c00处执行代码。请注意,并非所有环境都能保证这一点。由于segment:offset对的性质,有多种方法可以引用物理地址0x07c00。请参阅本答案末尾的部分,了解如何正确处理此问题。
假设我们使用VirtualBox并且上一节中的信息被认为是正确的,那么在进入我们的引导加载程序时 CS = 0x0000和 IP = 0x7c00。如果我们采用示例代码(使用ORG 0x0000
),我在本答案的第一部分中写了并查看反汇编信息(我将使用 objdump 输出)我们&#39; d看到这个:
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中运行此程序,前两个字符将显示为乱码。那为什么呢?首先回想起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 <&lt; 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 <&lt; 4)+ 0x24 = 0x07c24的物理地址。这就是我们想要的,因为我们的引导加载程序从该位置开始由BIOS实际放入内存中,因此它应该正确引用我们的 msg 变量。
故事的道德?当你启动我们的程序时,你在 ORG 中使用的是 DS 寄存器中应该有一个适用的值。我们应该明确地设置它,而不是依赖于< EM> CS
使用原始代码,前两个字符打印乱码,但最后两个字符没有。正如前一节所讨论的那样,前两个字符不会打印出来是有原因的,但是最后两个字符的作用是什么呢?
让我们更仔细地检查第3个字符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 <&lt; 4)+ 0x7c24 = 0x07c24。这是加载引导加载程序的正确内存位置,是 msg 字符串的正确位置。所以它有效。
使用ORG 0x7c00
是个坏主意吗?不,没关系。但我们有一个微妙的问题需要应对。如果另一个Virtual PC环境或真实硬件使用 CS:IP 0x0000:0x7c00而不是 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
的东西。哦,哦,看起来很糟糕。这转化为物理地址是什么? (0x07c0&lt;&lt; 4)+ 0x7c24 = 0x0F824。这是我们的引导加载程序之上的某个地方,它将包含计算机启动后发生的任何事情。可能是0,但应该假设它是垃圾。显然不是我们的 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 中可能用于访问引导加载程序的值并不重要,代码仍然会引用我们数据的相应内存位置。
在我之前的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