GRUB 支持ELF32和平面二进制文件。您的标头虽然暗示您提供的是ELF二进制文件。
将平面二进制文件与多重引导一起使用
如果您想告诉 Multiboot loader (GRUB) 您正在使用平面二进制文件,您必须将第16 位设置为 1:
MULTIBOOT_AOUT_KLUDGE equ 1 << 16
;FLAGS[16] indicates to GRUB we are not
;an ELF executable and the fields
;header address,load address,load end address,
;bss end address, and entry address will be
;available in our Multiboot header
它并不像指定这个标志那么简单。您必须提供一个完整的 Multiboot 标头,它为 Multiboot 加载程序提供将我们的二进制文件加载到内存中的信息。当使用ELF格式时,此信息位于我们代码之前的ELF标头中,因此不必显式提供。Multiboot 标头在GRUB 文档中进行了详细定义。
当使用NASM时-f bin
,需要注意我们需要为我们的代码指定原点。多重引导加载程序在物理地址加载我们的内核0x100000
。我们必须在我们的汇编文件中指定我们的原点,0x100000
以便在我们最终的平面二进制图像中生成适当的偏移量等。
这是一个从我自己的项目中剥离和修改的示例,它提供了一个简单的标题。调用_Main
的设置类似于示例中的 C 调用,但您不必那样做。通常我调用一个函数,该函数在堆栈上接受几个参数(使用 C 调用约定)。
[BITS 32]
[global _start]
[ORG 0x100000] ;If using '-f bin' we need to specify the
;origin point for our code with ORG directive
;multiboot loaders load us at physical
;address 0x100000
MULTIBOOT_AOUT_KLUDGE equ 1 << 16
;FLAGS[16] indicates to GRUB we are not
;an ELF executable and the fields
;header address, load address, load end address;
;bss end address and entry address will be available
;in Multiboot header
MULTIBOOT_ALIGN equ 1<<0 ; align loaded modules on page boundaries
MULTIBOOT_MEMINFO equ 1<<1 ; provide memory map
MULTIBOOT_HEADER_MAGIC equ 0x1BADB002
;magic number GRUB searches for in the first 8k
;of the kernel file GRUB is told to load
MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_AOUT_KLUDGE|MULTIBOOT_ALIGN|MULTIBOOT_MEMINFO
CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
KERNEL_STACK equ 0x00200000 ; Stack starts at the 2mb address & grows down
_start:
xor eax, eax ;Clear eax and ebx in the event
xor ebx, ebx ;we are not loaded by GRUB.
jmp multiboot_entry ;Jump over the multiboot header
align 4 ;Multiboot header must be 32
;bits aligned to avoid error 13
multiboot_header:
dd MULTIBOOT_HEADER_MAGIC ;magic number
dd MULTIBOOT_HEADER_FLAGS ;flags
dd CHECKSUM ;checksum
dd multiboot_header ;header address
dd _start ;load address of code entry point
;in our case _start
dd 00 ;load end address : not necessary
dd 00 ;bss end address : not necessary
dd multiboot_entry ;entry address GRUB will start at
multiboot_entry:
mov esp, KERNEL_STACK ;Setup the stack
push 0 ;Reset EFLAGS
popf
push eax ;2nd argument is magic number
push ebx ;1st argument multiboot info pointer
call _Main ;Call _Main
add esp, 8 ;Cleanup 8 bytes pushed as arguments
cli
endloop:
hlt
jmp endloop
_Main:
ret ; Do nothing
Multiboot 加载程序 ( GRUB ) 通常在文件的前 8k 中加载(无论是ELF还是平面二进制文件),在 32 位边界上查找 Multiboot 标头。如果Multiboot 标头 FLAG 的第16 位被清除,则假定您提供的是ELF映像。然后它解析ELF标头以检索将内核文件加载到内存所需的信息。如果设置了第16 位,则需要完整的 Multiboot 标头,以便加载程序具有将内核读入内存、执行初始化然后调用内核的信息。
然后,您将使用以下内容组装init.s
成平面二进制文件:
nasm -f bin -o init.bin init.s
将 ELF 与多重引导一起使用
要将 Jester 的评论与您的原始问题联系起来,您应该能够使用ELF启动并让它工作,但它不是因为一个小细节。在您的示例中,您使用它来制作init.bin:
nasm -f elf32 -o init.bin init.s
使用 时-f elf32
,NASM生成目标文件(它们不是可执行文件),必须链接(例如使用LD)才能生成最终的ELF (ELF32) 可执行文件。如果您使用以下方式完成了组装和链接过程,它可能会起作用:
nasm -f elf32 init.s -o init.o
ld -Ttext=0x100000 -melf_i386 -o init.bin init.o
请注意,使用-f elf32
时必须从init.s中删除ORG指令。ORG指令仅在使用. 多重引导加载程序将在物理地址加载我们,因此我们必须确保汇编和链接的代码是使用该起点生成的。使用时,我们在链接器(LD)命令行上指定入口点。或者,可以在链接描述文件中设置原点。-f bin
0x100000
-f elf32
-Ttext=0x100000
使用 NASM/LD/OBJCOPY 生成平面二进制图像
可以一起使用NASM / LD / OBJCOPY来生成最终的平面二进制图像,而不是-f bin
与NASM一起使用。如果您从init.s中删除ORG指令并使用这些命令,它应该生成一个平面二进制init.bin:
nasm -f elf32 init.s -o init.o
ld -Ttext=0x100000 -melf_i386 -o init.elf init.o
objcopy -O binary init.elf init.bin
在此,NASM被告知生成ELF32对象。我们将 init.s组装成一个名为init.o的ELF目标文件。然后我们可以使用链接器 ( LD ) 从init.o生成一个名为init.elf的ELF可执行文件。我们使用一个名为 objcopy 的特殊程序来剥离所有ELF标头并生成一个名为 init.bin 的平面二进制可执行文件。
这比仅使用NASM和-f bin
生成平面可执行文件init.bin的选项要复杂得多。那又何苦呢?通过上述方法,您可以告诉NASM生成可供gdb(GNU 调试器)使用的调试信息。如果您尝试在NASM中使用-g
(启用调试),则不会生成任何调试信息。您可以通过以下方式更改汇编顺序来生成调试信息:-f bin
nasm -g3 -F dwarf -f elf32 init.s -o init.o
ld -Ttext=0x100000 -melf_i386 -o init.elf init.o
objcopy -O binary init.elf init.bin
init.o将包含调试信息(以dwarf格式),这些信息将与LD链接到init.elf(保留调试信息)中。平面二进制文件不包含调试信息,因为当您将objcopy与-O binary
. 如果您在QEMU中启用远程调试工具并使用GDB进行调试,则可以使用init.elf。init.elf中的此调试信息为调试器提供信息,允许您单步执行代码、按名称访问变量和标签、查看源汇编程序代码等。
除了生成调试信息之外,使用NASM / LD / OBJCOPY进程生成内核二进制文件还有另一个原因。LD非常适合可配置。LD允许人们创建链接器脚本,以便您更好地调整最终二进制文件中的布局方式。这对于可能包含来自不同环境(C、汇编程序等)的混合代码的更复杂的内核很有用。对于小型玩具内核可能不需要它,但随着内核复杂性的增加,使用链接描述文件的好处将变得更加明显。
使用 GDB 远程调试 QEMU
如果您使用上一节中的方法在ELF可执行文件 ( init.elf ) 中生成调试信息,您可以启动QEMU并拥有它:
- 加载QEMU环境并在启动时停止 CPU。从手册页:
-S 不要在启动时启动 CPU(您必须在监视器中键入“c”)。
- 让QEMU监听 localhost:1234 上的GDB远程连接。从手册页:
-s -gdb tcp::1234 的简写,即在 TCP 端口 1234 上打开一个 gdbserver。
然后你只需要启动GDB就可以了:
- 使用带有调试符号和信息的ELF可执行文件 ( init.elf )启动GDB
- 连接到QEMU正在监听的 localhost:1234
- 设置您选择的调试布局
- 在我们的内核中设置一个断点来停止(在这个例子中是multiboot_entry)
这是从 CD-ROM 映像init.iso启动我们的内核并启动GDB以连接到它的示例:
qemu-system-x86_64 -cdrom ./init.iso -S -s &
gdb init.elf \
-ex 'target remote localhost:1234' \
-ex 'layout src' \
-ex 'layout regs' \
-ex 'break multiboot_entry' \
-ex 'continue'
您应该能够像调试普通程序一样使用GDB 。这假设您不会调试 16 位程序(内核)。
重要注意事项
正如 Jester 所指出的,当使用像GRUB这样的 Multiboot 兼容加载器时,CPU 处于 32 位保护模式(不是 16 位实模式)。与直接从 BIOS 引导不同,您将无法使用 16 位代码,包括大多数 PC-BIOS 中断。如果您需要处于实模式,则必须手动更改回实模式,或创建 VM86 任务(后者并非易事)。
这是一个重要的考虑因素,因为您在 MikeOS 中链接的一些代码是 16 位的。