我怎样才能 JMP 重定位我的 MBR 中的代码?
How can I JMP to relocated code in my MBR?
我正在尝试编写一个非常简单的 MBR,以开始学习如何编写 MBR/Kernel。这是我目前所拥有的(由其他 MBR 的片段创建)。我从使用 nasm 然后 ld 到 link 获得的二进制文件与仅对两者使用 nasm 有点不同,但这似乎不是问题所在。
我首先从 jmp 0:continue
开始,但似乎跳到 0000:7c22
(或 001d
与 nasm 单独......我相信我没有指定它开始在 7c00
) 但我希望跳转到 :7a22
或 :7a1d
,重定位代码的地址。我尝试只使用 jmp continue
然后如下面未注释的那样,将堆栈指针添加到继续指针,将其推送并返回。当 dd'ed 到我的第一个扇区时,我得到的只是一个 blinking 光标。感谢任何帮助。
; nasm+ld nasm comment
global _start
_start:
xor cx, cx ; 6631c9 31c9 Set segment registers to zero
mov es, cx ; 8ec1 8ec1
mov ds, cx ; 8ed9 8ed9
mov ss, cx ; 8ed1 8ed1
mov sp, 0x7A00 ; 66bc007a bc007a Stack
mov di, sp ; 6689e7 89e7 Bottom of relocation point
mov esi, _start ; be007c0000 66be00000000
cld ; fc fc
mov ch, 1 ; b501 b501 cx = 256
rep movsw ; f366a5 f3a5 Copy self to 0:7A00
;----------------------------------------------------------------------------------------------------------------------
xor eax,eax
mov ax, sp
add ax, continue
;jmp 0:continue ; ea227c00000000 ea1d000000 near JMP to copy of self
; or
;jmp continue ; (eb00)
push eax
ret
;----------------------------------------------------------------------------------------------------------------------
continue:
sti ; fb fb
ERROR:
mov esi, errormsg ; be3b7c0000 (be36) 66be36000000 Error Message loc
mov ah, 0x0E ; b40e b40e
mov bx, 7 ; 66bb bb0700
disp:
lodsb ; ac ac
cmp ah, 0x00 ; 80fc00 80fc00
je end ; 7404 7404
int 10h ; cd10 cd10
jmp disp ; ebf6 ebf6
end:
nop ; 90 90
jmp end ; ebfd ebfd infinte loop
errormsg db 10,'YOU MESSED UP.',13,0
times (0x1b8 - ($-$$)) nop ; 90 90 Padding
UID db 0xf5,0xbf,0x0f,0x18 ;Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01,0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D ;First Partition Entry
PT2 times 16 db 0 ;Second Partition Entry
PT3 times 16 db 0 ;Third Partition Entry
PT4 times 16 db 0 ;Fourth Partition Entry
BOOTSIG dw 0xAA55 ;Boot Signature[/code]
编译和链接使用:nasm -f bin -o mbr.bin mbr.asm
[BITS 16]
ORG 0x00007a00
; opcodes comment
global _start
_start:
xor cx, cx ; 31c9 Set segment registers to zero
mov es, cx ; 8ec1
mov ds, cx ; 8ed9
mov ss, cx ; 8ed1
mov sp, 0x7A00 ; bc007a Stack
mov di, sp ; 89e7 Bottom of relocation point
mov esi, 0x00007C00 ; 66be007c0000 Original location
cld ; fc
mov ch, 1 ; b501 CX = 256
rep movsw ; f3a5 Copy self to 0:7A00
jmp 0:continue ; ea1d7a0000 near JMP to copy of self
continue:
sti ; fb
ERROR:
mov esi, errormsg ; 66be357a0000 Error Message location
mov ah, 0x0E ; b40e 0E TTY Output
mov bx, 7 ; bb0700 Page number
disp:
lodsb ; ac Load next char
cmp al, 0x00 ; 3c00 Compare to zero
je end ; 7404 If so, end
int 10h ; cd10 Display char
jmp disp ; ebf6 Loop
end:
nop ; 90 Do Nothing
jmp end ; ebfd infinte loop
errormsg db 10,'YOU MESSED UP!',13,0
times (0x1b8 - ($-$$)) nop ; 90909090... Padding
UID db 0xf5,0xbf,0x0f,0x18 ;Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01
PT1more db 0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D
PT2 times 16 db 0
PT3 times 16 db 0
PT4 times 16 db 0
BOOTSIG dw 0xAA55 ;Boot Signature
hexdump -C mbr.bin
的输出:
00000000 31 c9 8e c1 8e d9 8e d1 bc 00 7a 89 e7 66 be 00 |1.........z..f..|
00000010 7c 00 00 fc b5 01 f3 a5 ea 1d 7a 00 00 fb 66 be ||.........z...f.|
00000020 35 7a 00 00 b4 0e bb 07 00 ac 3c 00 74 04 cd 10 |5z........<.t...|
00000030 eb f7 90 eb fd 0a 59 4f 55 20 4d 45 53 53 45 44 |......YOU MESSED|
00000040 20 55 50 21 0d 00 90 90 90 90 90 90 90 90 90 90 | UP!............|
00000050 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 |................|
*
000001b0 90 90 90 90 90 90 90 90 f5 bf 0f 18 00 00 80 20 |............... |
000001c0 21 00 0c 50 7f 01 00 08 00 00 b0 43 f9 0d 00 00 |!..P.......C....|
000001d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|
00000200
如您所见,您可以将整个引导加载程序的原点设置为 ORG 0x7A00
。这非常有效。将引导扇区复制到 0x7A00 的代码不依赖于任何绝对标签,仅依赖于相关标签。这个答案更像是一个思想实验和一种不同的方法。
如果我们想在复制之前显示一个字符串作为例子会怎样?有哪些可能的选择?
- NASM 允许 BIN 格式 (
-f bin
) 具有采用 virtual starting point(原点)和物理地址(开始)。此方法对于引导加载程序的布局方式限制太多。
- 使用 LD linker script 定义引导加载程序的布局。
- 重新组织代码以使用 0x0000 的 ORG(原点)并相应地设置段寄存器。请参阅我对这个问题的other answer。
此答案侧重于选项 2。解释 LD 链接器脚本的工作方式对于 Whosebug 而言过于宽泛。 LD manual 是最好的信息来源,它确实有示例。我们的想法是,我们允许将引导加载程序放置在 linker 脚本中。我们可以设置LMA(Load Memory Address)来指定section将被加载到内存中的内存地址。 VMA 是截面的原点。一个部分中的所有标签和地址都将相对于其 VMA 进行解析。
我们可以方便地使用具有特定 LMA 的部分将引导签名直接放入输出文件中,而不是在汇编代码中指定它。我们还可以从 linker 脚本中定义符号,这些符号可以使用 NASM extern
指令从汇编代码中访问。
所有这一切的一个优点是您可以按照您想要的任何顺序在汇编代码中定义部分,linker 脚本将重新排序。您还可以 link 多个 object 文件。包含您希望首先出现的引导代码的 object 文件应首先列出。
这个 linker 脚本的布局大致如下所示:
Non-relocatable portion of boot code (boot.text) Relative to an origin of 0x7c00
Non-relocatable portion of boot data (boot.data)
--------------------------------------- Word aligned
Relocatable portion of boot code (rel.text) - Relative to an origin of 0x7a00
Relocatable portion of boot data (rel.data)
Relocatable portion of partition data at offset 0x1b8 (partition.data)
---------------------------------------
Boot signature at offset 0x1fe
布置此引导加载程序的 linker 脚本可能类似于:
ENTRY(_start);
OUTPUT(elf_i386);
SECTIONS
{
/* Set the base of the main bootloader offsets */
_bootbase = 0x7c00; /* Where bootloader initially is loaded in memory */
_relbase = 0x7a00; /* Address entire bootsector will be copied to
This linker script expects it to be word aligned */
_partoffset = 0x1b8; /* Offset of UID and Partition data */
_sigoffset = 0x1fe; /* Offset of the boot signature word */
/* SUBALIGN(n) in an output section will override the alignment
* of any input section that is encontered */
/* This is the boot loader code and data that is expected to run from 0x7c00 */
.bootinit _bootbase : SUBALIGN(2)
{
*(boot.text);
*(boot.data);
}
/* Note that referencing any data in the partition table will
* only be usable from the code that is in the .bootrel section */
/* Partition data */
.partdata _relbase + _partoffset :
AT(_bootbase + _partoffset) SUBALIGN(0)
{
*(partition.data);
}
/* Boot signature */
.bootsig :
AT(_bootbase + _sigoffset) SUBALIGN(0)
{
SHORT(0xaa55);
}
/* Length of region to copy in 16-bit words */
_rel_length = 256;
/* Address to copy to */
_rel_start = _relbase; /* Word aligned start address */
/* Code and data that will expect to run once relocated
* is placed in this section. Aligned to word boundary.
* This relocateable code and data will be placed right
* after the .bootinit section in the output file */
.bootrel _relbase + SIZEOF(.bootinit) :
AT(_bootbase + SIZEOF(.bootinit)) SUBALIGN(2)
{
*(rel.text);
*(rel.data);
}
}
使用此 linker 脚本和其中定义的符号的代码修订副本可能如下所示:
BITS 16
extern _bootbase
extern _relbase
extern _rel_length
extern _rel_start
section boot.text
; comment
global _start
_start:
xor cx, cx ; Set segment registers to zero
mov es, cx
mov ds, cx
mov ss, cx
mov sp, 0x7A00 ; Stack
cld
.copymsg:
mov si, copymsg ; Copy message
mov ah, 0x0E ; 0E TTY Output
mov bx, 7 ; Page number
.dispcopy:
lodsb ; Load next char
test al, al ; Compare to zero
jz .end ; If so, end
int 10h ; Display char
jmp .dispcopy ; Loop
.end:
mov di, _rel_start ; Beginning of relocation point
mov si, _bootbase ; Original location to copy from
mov cx, _rel_length ; CX = words to copy
rep movsw ; Copy self to destination
jmp 0:rel_entry ; far JMP to copy of self
section rel.text
rel_entry:
sti ; Enable interrupts
mov si, successmsg ; Error Message location
mov ah, 0x0E ; 0E TTY Output
mov bx, 7 ; Page number
.disp:
lodsb ; Load next char
test al, al ; Compare to zero
je .end ; If so, end
int 10h ; Display char
jmp .disp ; Loop
cli ; Disable interrupts
.end:
hlt ; CPU hlt
jmp .end ; infinte loop
section rel.data
successmsg db 10,'Success!',13,0
section boot.data
copymsg db 10,'Before copy!',13,0
section partition.data
UID db 0xf5,0xbf,0x0f,0x18 ;Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01
db 0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D
PT2 times 16 db 0
PT3 times 16 db 0
PT4 times 16 db 0
作为确保 boot.text
部分中的代码可以访问 boot.data
部分中的数据的实验,我在复制之前显示了一个字符串。然后我对重新定位的代码执行 FAR JMP。重定位代码显示成功字符串。
我修改了代码以不使用像 ESI 这样的 32 位寄存器,因为您将在实模式下执行此代码。我还修改了你的无限循环以使用 HLT 指令。
代码和 linker 脚本可以修改为仅从重定位数据的开头复制到第 512 个字节,但这超出了本答案的范围。
看看反汇编
下面提供了原点为 0x7c00 的 .bootinit
部分。这是该部分的 OBJDUMP 片段(为简洁起见,没有数据):
Disassembly of section .bootinit:
00007c00 <_start>:
7c00: 31 c9 xor cx,cx
7c02: 8e c1 mov es,cx
7c04: 8e d9 mov ds,cx
7c06: 8e d1 mov ss,cx
7c08: bc 00 7a mov sp,0x7a00
7c0b: fc cld
00007c0c <_start.copymsg>:
7c0c: be 2e 7c mov si,0x7c2e
7c0f: b4 0e mov ah,0xe
7c11: bb 07 00 mov bx,0x7
00007c14 <_start.dispcopy>:
7c14: ac lods al,BYTE PTR ds:[si]
7c15: 84 c0 test al,al
7c17: 74 04 je 7c1d <_start.end>
7c19: cd 10 int 0x10
7c1b: eb f7 jmp 7c14 <_start.dispcopy>
00007c1d <_start.end>:
7c1d: bf 00 7a mov di,0x7a00
7c20: be 00 7c mov si,0x7c00
7c23: b9 00 01 mov cx,0x100
7c26: f3 a5 rep movs WORD PTR es:[di],WORD PTR ds:[si]
7c28: ea 3e 7a 00 00 jmp 0x0:0x7a3e
左列中的所有 VMA 地址似乎都是相对于原点 0x7c00 正确设置的。 FAR JUMP (jmp 0x0:0x7a3e
) 也跳到了所有东西都被重新定位(复制)的位置。 .bootrel
部分的类似缩写转储显示为:
Disassembly of section .bootrel:
00007a3d <rel_entry-0x1>:
...
00007a3e <rel_entry>:
7a3e: fb sti
7a3f: be 54 7a mov si,0x7a54
7a42: b4 0e mov ah,0xe
7a44: bb 07 00 mov bx,0x7
00007a47 <rel_entry.disp>:
7a47: ac lods al,BYTE PTR ds:[si]
7a48: 3c 00 cmp al,0x0
7a4a: 74 05 je 7a51 <rel_entry.end>
7a4c: cd 10 int 0x10
7a4e: eb f7 jmp 7a47 <rel_entry.disp>
7a50: fa cli
00007a51 <rel_entry.end>:
7a51: f4 hlt
7a52: eb fd jmp 7a51 <rel_entry.end>
左栏的VMA是相对于0x7A00开头的,是正确的。指令 mov si,0x7a54
是一个绝对的近内存地址,它被正确编码以引用 successmsg
地址(为简洁起见,我将数据剪掉,因此它不会出现)。
条目:
00007a3d <rel_entry-0x1>:
...
是关于将 .bootrel
部分对齐到偶数字边界的信息。使用此 linker 脚本 rel_entry
将始终具有偶数地址。
编译并链接此引导加载程序
最简单的方法是使用这些命令:
nasm -f elf32 -o boot.o boot.asm
ld -melf_i386 -Tlinker.ld -o boot.bin --oformat=binary boot.o
需要指出的是,我们使用的是ELF32格式NASM,而不是BIN 。 LD 然后用于创建二进制文件 boot.bin
,它应该是引导扇区的 512 字节映像。 linker.ld
是 linker 脚本文件的名称。
如果您希望能够方便地获得 object 转储,那么您可以使用这些命令来 assemble 和 link:
nasm -f elf32 -o boot.o boot.asm
ld -melf_i386 -Tlinker.ld -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
与第一种方法的不同之处在于我们不使用 --oformat=binary
选项和 LD。结果将是生成 ELF32 图像并将其放置在输出文件 boot.elf
中。我们不能直接使用 boot.elf
作为我们的启动映像,所以我们使用 OBJCOPY 将 ELF32 文件转换为二进制文件,调用boot.bin
。如果我们使用这样的命令来转储,可以看出这样做的用处ELF 文件的内容和反汇编:
objdump boot.elf -Mintel -mi8086 -Dx
-D
选项是disassemble all
-x
输出headers
-mi8086
disassemble as 16-bit 8086 code
-Mintel
反汇编应该是 INTEL 语法而不是默认的 ATT 语法
另一种重定位 MBR/bootloader 的替代方案不涉及链接器脚本,但允许 ALL 引导加载程序中的代码和数据被重定位到 segment:offset对=segment:0x0000。这并不限制引导加载程序在 64kb 内存中被复制。这涉及使用一些段数学和对代码的一些小改动。我将使用您的原始代码进行一些清理以及我在第一个答案中添加的说明,该说明在 JMP.
之前打印一个字符串
请记住,在 x86 中 real mode segmentation 物理地址由段寄存器的内容和偏移量决定。计算如下:
Physical address = (segment << 4) + offset
对于引导加载程序,许多人将 0x0000 放入 DS 和 ES,这是您在代码中所做的。您的原始代码使用 ORG 0x7c00
(实际上是偏移量)。当您获取段和偏移量并计算物理地址时,您将获得:
Physical address = (0x0000 << 4) + 0x7c00 = 0x07c00
0x07c00 是 BIOS 放在内存中的引导加载程序的物理地址。但是,通常有不止一种方法可以将单个物理地址表示为 segment:offset 对。
例如,如果我们使用 0x07c0 的段和 0x0000 的 ORG(偏移量),我们发现等式产生:
Physical address = (0x07c0 << 4) + 0x0000 = 0x07c00
如果将原始段寄存器设置为 0x07c0 并在引导加载程序中使用 ORG 0x0000
指令会怎么样。那行得通吗?是的,会的。链接器和 NASM 真的不知道用户如何设置段寄存器。当 NASM 创建对 NEAR 绝对地址的引用时,它假设这些段将由开发人员在 运行-时间.
如果我们使用ORG 0x0000
,那么为了从0x7c00复制到0x7a00,我们可以将0x07c0放入DS和SI 到 0 (0x07c0:0x0000)。我们可以将 ES 设置为 0x07a0,将 DI 设置为 0 (0x07a0:0x0000)。 CX 将设置为 256(我们将复制单词)。这会将 512 字节的内存从物理地址 0x07c00 复制到 0x07a00。复制整个引导加载程序后,我们然后使用 FAR JMP 将 CS 设置为跳转中指定的段,并将指令指针 (IP) 设置为标签的偏移量。
一旦我们跳转到复制引导加载程序的内存(从物理地址 0x7a00 开始的区域),我们就会相应地设置段寄存器。所以在我们的例子中,我们会在跳转到 0x07a0 之后设置寄存器 DS 和 ES。这应该允许所有引导加载程序代码和数据按预期在新位置 运行。
以下代码可以执行与上面建议的类似的操作:
BITS 16
ORG 0x0000
global _start
_start:
xor bp, bp
mov ss, bp ; Set SS segment register to zero
mov sp, 0x7A00 ; Set stack to SS:SP=0x0000:0x7A00
mov dx, 0x07C0
mov ds, dx ; DS = 0x07C0 - Segment:Offset = 0x07c0:0x0000 = 0x07C00
sub dx, 0x20 ; DX = 0x07C0-0x20=0x07A0
mov es, dx ; ES = 0x07A0 - Segment:Offset = 0x07a0:0x0000 = 0x07A00
cld
; Print message before the copy
mov si, copymsg ; Copy message (DS:[copymsg])
call outputstr
; Copy 256 words of memory from 0x07c00 to 0x07a00
mov di, bp ; Destination is ES:DI = 0x07a0:0x0000 = 0x07a00
mov si, bp ; Source is DS:SI = 0x07c0:0x0000 = 0x07c00
mov cx, 256 ; CX = 256 words to copy
rep movsw ; Copy self to destination
jmp 0x07A0:rel_entry
; far JMP jumps to phys address 0x07a00, sets
; CS = 0x07A0 and IP = rel_entry.
rel_entry:
sti ; Enable interrupts
mov ds, dx ; DS=ES=0x07A0
; ES already 0x07A0 before the jump, no need to set again
.success:
; Print the "Success!" message
mov si, successmsg ; Success Message location (DS:[successmsg])
call outputstr
cli ; Disable interrupts
.end:
hlt ; CPU hlt
jmp .end ; infinite loop
; Function: outputstr
; Inputs:
; SI = address of string
; Outputs:
; None
; Registers destroyed:
; AX, BX, SI
;
outputstr:
mov ah, 0x0E ; 0E TTY Output
mov bx, 7 ; Page number
.disp:
lodsb ; Load next char from DS:[SI], SI+=2
test al, al ; Compare to zero
je .end ; If so, end
int 0x10 ; Display char
jmp .disp ; Loop
.end:
ret
successmsg db 10,'Success!',13,0
copymsg db 10,'Before copy!',13,0
times (0x1b8 - ($-$$)) db 0x00 ; Padding
UID db 0xf5,0xbf,0x0f,0x18 ; Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01
PT1more db 0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D
PT2 times 16 db 0
PT3 times 16 db 0
PT4 times 16 db 0
BOOTSIG dw 0xAA55 ;Boot Signature
此引导加载程序调用函数 outputstr
以在复制之前的代码中和跳转到重定位代码之后向控制台显示消息。这是有效的,因为在这两种情况下,所有标签仍然处于相同的偏移量,只有段发生了变化。
上面的引导加载程序可以用如下命令组装:
nasm -f bin boot.bin boot.asm
我正在尝试编写一个非常简单的 MBR,以开始学习如何编写 MBR/Kernel。这是我目前所拥有的(由其他 MBR 的片段创建)。我从使用 nasm 然后 ld 到 link 获得的二进制文件与仅对两者使用 nasm 有点不同,但这似乎不是问题所在。
我首先从 jmp 0:continue
开始,但似乎跳到 0000:7c22
(或 001d
与 nasm 单独......我相信我没有指定它开始在 7c00
) 但我希望跳转到 :7a22
或 :7a1d
,重定位代码的地址。我尝试只使用 jmp continue
然后如下面未注释的那样,将堆栈指针添加到继续指针,将其推送并返回。当 dd'ed 到我的第一个扇区时,我得到的只是一个 blinking 光标。感谢任何帮助。
; nasm+ld nasm comment
global _start
_start:
xor cx, cx ; 6631c9 31c9 Set segment registers to zero
mov es, cx ; 8ec1 8ec1
mov ds, cx ; 8ed9 8ed9
mov ss, cx ; 8ed1 8ed1
mov sp, 0x7A00 ; 66bc007a bc007a Stack
mov di, sp ; 6689e7 89e7 Bottom of relocation point
mov esi, _start ; be007c0000 66be00000000
cld ; fc fc
mov ch, 1 ; b501 b501 cx = 256
rep movsw ; f366a5 f3a5 Copy self to 0:7A00
;----------------------------------------------------------------------------------------------------------------------
xor eax,eax
mov ax, sp
add ax, continue
;jmp 0:continue ; ea227c00000000 ea1d000000 near JMP to copy of self
; or
;jmp continue ; (eb00)
push eax
ret
;----------------------------------------------------------------------------------------------------------------------
continue:
sti ; fb fb
ERROR:
mov esi, errormsg ; be3b7c0000 (be36) 66be36000000 Error Message loc
mov ah, 0x0E ; b40e b40e
mov bx, 7 ; 66bb bb0700
disp:
lodsb ; ac ac
cmp ah, 0x00 ; 80fc00 80fc00
je end ; 7404 7404
int 10h ; cd10 cd10
jmp disp ; ebf6 ebf6
end:
nop ; 90 90
jmp end ; ebfd ebfd infinte loop
errormsg db 10,'YOU MESSED UP.',13,0
times (0x1b8 - ($-$$)) nop ; 90 90 Padding
UID db 0xf5,0xbf,0x0f,0x18 ;Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01,0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D ;First Partition Entry
PT2 times 16 db 0 ;Second Partition Entry
PT3 times 16 db 0 ;Third Partition Entry
PT4 times 16 db 0 ;Fourth Partition Entry
BOOTSIG dw 0xAA55 ;Boot Signature[/code]
编译和链接使用:nasm -f bin -o mbr.bin mbr.asm
[BITS 16]
ORG 0x00007a00
; opcodes comment
global _start
_start:
xor cx, cx ; 31c9 Set segment registers to zero
mov es, cx ; 8ec1
mov ds, cx ; 8ed9
mov ss, cx ; 8ed1
mov sp, 0x7A00 ; bc007a Stack
mov di, sp ; 89e7 Bottom of relocation point
mov esi, 0x00007C00 ; 66be007c0000 Original location
cld ; fc
mov ch, 1 ; b501 CX = 256
rep movsw ; f3a5 Copy self to 0:7A00
jmp 0:continue ; ea1d7a0000 near JMP to copy of self
continue:
sti ; fb
ERROR:
mov esi, errormsg ; 66be357a0000 Error Message location
mov ah, 0x0E ; b40e 0E TTY Output
mov bx, 7 ; bb0700 Page number
disp:
lodsb ; ac Load next char
cmp al, 0x00 ; 3c00 Compare to zero
je end ; 7404 If so, end
int 10h ; cd10 Display char
jmp disp ; ebf6 Loop
end:
nop ; 90 Do Nothing
jmp end ; ebfd infinte loop
errormsg db 10,'YOU MESSED UP!',13,0
times (0x1b8 - ($-$$)) nop ; 90909090... Padding
UID db 0xf5,0xbf,0x0f,0x18 ;Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01
PT1more db 0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D
PT2 times 16 db 0
PT3 times 16 db 0
PT4 times 16 db 0
BOOTSIG dw 0xAA55 ;Boot Signature
hexdump -C mbr.bin
的输出:
00000000 31 c9 8e c1 8e d9 8e d1 bc 00 7a 89 e7 66 be 00 |1.........z..f..|
00000010 7c 00 00 fc b5 01 f3 a5 ea 1d 7a 00 00 fb 66 be ||.........z...f.|
00000020 35 7a 00 00 b4 0e bb 07 00 ac 3c 00 74 04 cd 10 |5z........<.t...|
00000030 eb f7 90 eb fd 0a 59 4f 55 20 4d 45 53 53 45 44 |......YOU MESSED|
00000040 20 55 50 21 0d 00 90 90 90 90 90 90 90 90 90 90 | UP!............|
00000050 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 |................|
*
000001b0 90 90 90 90 90 90 90 90 f5 bf 0f 18 00 00 80 20 |............... |
000001c0 21 00 0c 50 7f 01 00 08 00 00 b0 43 f9 0d 00 00 |!..P.......C....|
000001d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|
00000200
如您所见,您可以将整个引导加载程序的原点设置为 ORG 0x7A00
。这非常有效。将引导扇区复制到 0x7A00 的代码不依赖于任何绝对标签,仅依赖于相关标签。这个答案更像是一个思想实验和一种不同的方法。
如果我们想在复制之前显示一个字符串作为例子会怎样?有哪些可能的选择?
- NASM 允许 BIN 格式 (
-f bin
) 具有采用 virtual starting point(原点)和物理地址(开始)。此方法对于引导加载程序的布局方式限制太多。 - 使用 LD linker script 定义引导加载程序的布局。
- 重新组织代码以使用 0x0000 的 ORG(原点)并相应地设置段寄存器。请参阅我对这个问题的other answer。
此答案侧重于选项 2。解释 LD 链接器脚本的工作方式对于 Whosebug 而言过于宽泛。 LD manual 是最好的信息来源,它确实有示例。我们的想法是,我们允许将引导加载程序放置在 linker 脚本中。我们可以设置LMA(Load Memory Address)来指定section将被加载到内存中的内存地址。 VMA 是截面的原点。一个部分中的所有标签和地址都将相对于其 VMA 进行解析。
我们可以方便地使用具有特定 LMA 的部分将引导签名直接放入输出文件中,而不是在汇编代码中指定它。我们还可以从 linker 脚本中定义符号,这些符号可以使用 NASM extern
指令从汇编代码中访问。
所有这一切的一个优点是您可以按照您想要的任何顺序在汇编代码中定义部分,linker 脚本将重新排序。您还可以 link 多个 object 文件。包含您希望首先出现的引导代码的 object 文件应首先列出。
这个 linker 脚本的布局大致如下所示:
Non-relocatable portion of boot code (boot.text) Relative to an origin of 0x7c00 Non-relocatable portion of boot data (boot.data) --------------------------------------- Word aligned Relocatable portion of boot code (rel.text) - Relative to an origin of 0x7a00 Relocatable portion of boot data (rel.data) Relocatable portion of partition data at offset 0x1b8 (partition.data) --------------------------------------- Boot signature at offset 0x1fe
布置此引导加载程序的 linker 脚本可能类似于:
ENTRY(_start);
OUTPUT(elf_i386);
SECTIONS
{
/* Set the base of the main bootloader offsets */
_bootbase = 0x7c00; /* Where bootloader initially is loaded in memory */
_relbase = 0x7a00; /* Address entire bootsector will be copied to
This linker script expects it to be word aligned */
_partoffset = 0x1b8; /* Offset of UID and Partition data */
_sigoffset = 0x1fe; /* Offset of the boot signature word */
/* SUBALIGN(n) in an output section will override the alignment
* of any input section that is encontered */
/* This is the boot loader code and data that is expected to run from 0x7c00 */
.bootinit _bootbase : SUBALIGN(2)
{
*(boot.text);
*(boot.data);
}
/* Note that referencing any data in the partition table will
* only be usable from the code that is in the .bootrel section */
/* Partition data */
.partdata _relbase + _partoffset :
AT(_bootbase + _partoffset) SUBALIGN(0)
{
*(partition.data);
}
/* Boot signature */
.bootsig :
AT(_bootbase + _sigoffset) SUBALIGN(0)
{
SHORT(0xaa55);
}
/* Length of region to copy in 16-bit words */
_rel_length = 256;
/* Address to copy to */
_rel_start = _relbase; /* Word aligned start address */
/* Code and data that will expect to run once relocated
* is placed in this section. Aligned to word boundary.
* This relocateable code and data will be placed right
* after the .bootinit section in the output file */
.bootrel _relbase + SIZEOF(.bootinit) :
AT(_bootbase + SIZEOF(.bootinit)) SUBALIGN(2)
{
*(rel.text);
*(rel.data);
}
}
使用此 linker 脚本和其中定义的符号的代码修订副本可能如下所示:
BITS 16
extern _bootbase
extern _relbase
extern _rel_length
extern _rel_start
section boot.text
; comment
global _start
_start:
xor cx, cx ; Set segment registers to zero
mov es, cx
mov ds, cx
mov ss, cx
mov sp, 0x7A00 ; Stack
cld
.copymsg:
mov si, copymsg ; Copy message
mov ah, 0x0E ; 0E TTY Output
mov bx, 7 ; Page number
.dispcopy:
lodsb ; Load next char
test al, al ; Compare to zero
jz .end ; If so, end
int 10h ; Display char
jmp .dispcopy ; Loop
.end:
mov di, _rel_start ; Beginning of relocation point
mov si, _bootbase ; Original location to copy from
mov cx, _rel_length ; CX = words to copy
rep movsw ; Copy self to destination
jmp 0:rel_entry ; far JMP to copy of self
section rel.text
rel_entry:
sti ; Enable interrupts
mov si, successmsg ; Error Message location
mov ah, 0x0E ; 0E TTY Output
mov bx, 7 ; Page number
.disp:
lodsb ; Load next char
test al, al ; Compare to zero
je .end ; If so, end
int 10h ; Display char
jmp .disp ; Loop
cli ; Disable interrupts
.end:
hlt ; CPU hlt
jmp .end ; infinte loop
section rel.data
successmsg db 10,'Success!',13,0
section boot.data
copymsg db 10,'Before copy!',13,0
section partition.data
UID db 0xf5,0xbf,0x0f,0x18 ;Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01
db 0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D
PT2 times 16 db 0
PT3 times 16 db 0
PT4 times 16 db 0
作为确保 boot.text
部分中的代码可以访问 boot.data
部分中的数据的实验,我在复制之前显示了一个字符串。然后我对重新定位的代码执行 FAR JMP。重定位代码显示成功字符串。
我修改了代码以不使用像 ESI 这样的 32 位寄存器,因为您将在实模式下执行此代码。我还修改了你的无限循环以使用 HLT 指令。
代码和 linker 脚本可以修改为仅从重定位数据的开头复制到第 512 个字节,但这超出了本答案的范围。
看看反汇编
下面提供了原点为 0x7c00 的 .bootinit
部分。这是该部分的 OBJDUMP 片段(为简洁起见,没有数据):
Disassembly of section .bootinit:
00007c00 <_start>:
7c00: 31 c9 xor cx,cx
7c02: 8e c1 mov es,cx
7c04: 8e d9 mov ds,cx
7c06: 8e d1 mov ss,cx
7c08: bc 00 7a mov sp,0x7a00
7c0b: fc cld
00007c0c <_start.copymsg>:
7c0c: be 2e 7c mov si,0x7c2e
7c0f: b4 0e mov ah,0xe
7c11: bb 07 00 mov bx,0x7
00007c14 <_start.dispcopy>:
7c14: ac lods al,BYTE PTR ds:[si]
7c15: 84 c0 test al,al
7c17: 74 04 je 7c1d <_start.end>
7c19: cd 10 int 0x10
7c1b: eb f7 jmp 7c14 <_start.dispcopy>
00007c1d <_start.end>:
7c1d: bf 00 7a mov di,0x7a00
7c20: be 00 7c mov si,0x7c00
7c23: b9 00 01 mov cx,0x100
7c26: f3 a5 rep movs WORD PTR es:[di],WORD PTR ds:[si]
7c28: ea 3e 7a 00 00 jmp 0x0:0x7a3e
左列中的所有 VMA 地址似乎都是相对于原点 0x7c00 正确设置的。 FAR JUMP (jmp 0x0:0x7a3e
) 也跳到了所有东西都被重新定位(复制)的位置。 .bootrel
部分的类似缩写转储显示为:
Disassembly of section .bootrel:
00007a3d <rel_entry-0x1>:
...
00007a3e <rel_entry>:
7a3e: fb sti
7a3f: be 54 7a mov si,0x7a54
7a42: b4 0e mov ah,0xe
7a44: bb 07 00 mov bx,0x7
00007a47 <rel_entry.disp>:
7a47: ac lods al,BYTE PTR ds:[si]
7a48: 3c 00 cmp al,0x0
7a4a: 74 05 je 7a51 <rel_entry.end>
7a4c: cd 10 int 0x10
7a4e: eb f7 jmp 7a47 <rel_entry.disp>
7a50: fa cli
00007a51 <rel_entry.end>:
7a51: f4 hlt
7a52: eb fd jmp 7a51 <rel_entry.end>
左栏的VMA是相对于0x7A00开头的,是正确的。指令 mov si,0x7a54
是一个绝对的近内存地址,它被正确编码以引用 successmsg
地址(为简洁起见,我将数据剪掉,因此它不会出现)。
条目:
00007a3d <rel_entry-0x1>:
...
是关于将 .bootrel
部分对齐到偶数字边界的信息。使用此 linker 脚本 rel_entry
将始终具有偶数地址。
编译并链接此引导加载程序
最简单的方法是使用这些命令:
nasm -f elf32 -o boot.o boot.asm
ld -melf_i386 -Tlinker.ld -o boot.bin --oformat=binary boot.o
需要指出的是,我们使用的是ELF32格式NASM,而不是BIN 。 LD 然后用于创建二进制文件 boot.bin
,它应该是引导扇区的 512 字节映像。 linker.ld
是 linker 脚本文件的名称。
如果您希望能够方便地获得 object 转储,那么您可以使用这些命令来 assemble 和 link:
nasm -f elf32 -o boot.o boot.asm
ld -melf_i386 -Tlinker.ld -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
与第一种方法的不同之处在于我们不使用 --oformat=binary
选项和 LD。结果将是生成 ELF32 图像并将其放置在输出文件 boot.elf
中。我们不能直接使用 boot.elf
作为我们的启动映像,所以我们使用 OBJCOPY 将 ELF32 文件转换为二进制文件,调用boot.bin
。如果我们使用这样的命令来转储,可以看出这样做的用处ELF 文件的内容和反汇编:
objdump boot.elf -Mintel -mi8086 -Dx
-D
选项是disassemble all-x
输出headers-mi8086
disassemble as 16-bit 8086 code-Mintel
反汇编应该是 INTEL 语法而不是默认的 ATT 语法
另一种重定位 MBR/bootloader 的替代方案不涉及链接器脚本,但允许 ALL 引导加载程序中的代码和数据被重定位到 segment:offset对=segment:0x0000。这并不限制引导加载程序在 64kb 内存中被复制。这涉及使用一些段数学和对代码的一些小改动。我将使用您的原始代码进行一些清理以及我在第一个答案中添加的说明,该说明在 JMP.
之前打印一个字符串请记住,在 x86 中 real mode segmentation 物理地址由段寄存器的内容和偏移量决定。计算如下:
Physical address = (segment << 4) + offset
对于引导加载程序,许多人将 0x0000 放入 DS 和 ES,这是您在代码中所做的。您的原始代码使用 ORG 0x7c00
(实际上是偏移量)。当您获取段和偏移量并计算物理地址时,您将获得:
Physical address = (0x0000 << 4) + 0x7c00 = 0x07c00
0x07c00 是 BIOS 放在内存中的引导加载程序的物理地址。但是,通常有不止一种方法可以将单个物理地址表示为 segment:offset 对。
例如,如果我们使用 0x07c0 的段和 0x0000 的 ORG(偏移量),我们发现等式产生:
Physical address = (0x07c0 << 4) + 0x0000 = 0x07c00
如果将原始段寄存器设置为 0x07c0 并在引导加载程序中使用 ORG 0x0000
指令会怎么样。那行得通吗?是的,会的。链接器和 NASM 真的不知道用户如何设置段寄存器。当 NASM 创建对 NEAR 绝对地址的引用时,它假设这些段将由开发人员在 运行-时间.
如果我们使用ORG 0x0000
,那么为了从0x7c00复制到0x7a00,我们可以将0x07c0放入DS和SI 到 0 (0x07c0:0x0000)。我们可以将 ES 设置为 0x07a0,将 DI 设置为 0 (0x07a0:0x0000)。 CX 将设置为 256(我们将复制单词)。这会将 512 字节的内存从物理地址 0x07c00 复制到 0x07a00。复制整个引导加载程序后,我们然后使用 FAR JMP 将 CS 设置为跳转中指定的段,并将指令指针 (IP) 设置为标签的偏移量。
一旦我们跳转到复制引导加载程序的内存(从物理地址 0x7a00 开始的区域),我们就会相应地设置段寄存器。所以在我们的例子中,我们会在跳转到 0x07a0 之后设置寄存器 DS 和 ES。这应该允许所有引导加载程序代码和数据按预期在新位置 运行。
以下代码可以执行与上面建议的类似的操作:
BITS 16
ORG 0x0000
global _start
_start:
xor bp, bp
mov ss, bp ; Set SS segment register to zero
mov sp, 0x7A00 ; Set stack to SS:SP=0x0000:0x7A00
mov dx, 0x07C0
mov ds, dx ; DS = 0x07C0 - Segment:Offset = 0x07c0:0x0000 = 0x07C00
sub dx, 0x20 ; DX = 0x07C0-0x20=0x07A0
mov es, dx ; ES = 0x07A0 - Segment:Offset = 0x07a0:0x0000 = 0x07A00
cld
; Print message before the copy
mov si, copymsg ; Copy message (DS:[copymsg])
call outputstr
; Copy 256 words of memory from 0x07c00 to 0x07a00
mov di, bp ; Destination is ES:DI = 0x07a0:0x0000 = 0x07a00
mov si, bp ; Source is DS:SI = 0x07c0:0x0000 = 0x07c00
mov cx, 256 ; CX = 256 words to copy
rep movsw ; Copy self to destination
jmp 0x07A0:rel_entry
; far JMP jumps to phys address 0x07a00, sets
; CS = 0x07A0 and IP = rel_entry.
rel_entry:
sti ; Enable interrupts
mov ds, dx ; DS=ES=0x07A0
; ES already 0x07A0 before the jump, no need to set again
.success:
; Print the "Success!" message
mov si, successmsg ; Success Message location (DS:[successmsg])
call outputstr
cli ; Disable interrupts
.end:
hlt ; CPU hlt
jmp .end ; infinite loop
; Function: outputstr
; Inputs:
; SI = address of string
; Outputs:
; None
; Registers destroyed:
; AX, BX, SI
;
outputstr:
mov ah, 0x0E ; 0E TTY Output
mov bx, 7 ; Page number
.disp:
lodsb ; Load next char from DS:[SI], SI+=2
test al, al ; Compare to zero
je .end ; If so, end
int 0x10 ; Display char
jmp .disp ; Loop
.end:
ret
successmsg db 10,'Success!',13,0
copymsg db 10,'Before copy!',13,0
times (0x1b8 - ($-$$)) db 0x00 ; Padding
UID db 0xf5,0xbf,0x0f,0x18 ; Unique Disk ID
BLANK times 2 db 0
PT1 db 0x80,0x20,0x21,0x00,0x0C,0x50,0x7F,0x01
PT1more db 0x00,0x08,0x00,0x00,0xb0,0x43,0xF9,0x0D
PT2 times 16 db 0
PT3 times 16 db 0
PT4 times 16 db 0
BOOTSIG dw 0xAA55 ;Boot Signature
此引导加载程序调用函数 outputstr
以在复制之前的代码中和跳转到重定位代码之后向控制台显示消息。这是有效的,因为在这两种情况下,所有标签仍然处于相同的偏移量,只有段发生了变化。
上面的引导加载程序可以用如下命令组装:
nasm -f bin boot.bin boot.asm