创建一个加载了 grub2 的简单多重引导内核
Creating a simple multiboot kernel loaded with grub2
我正在尝试按照此处的说明构建一个简单的 OS 内核:http://mikeos.sourceforge.net/write-your-own-os.html
除了,我不想从软盘启动,而是想创建一个 grub-based ISO 映像并在模拟器中启动多启动 CD。我已将以下内容添加到该页面列出的源代码中,用于多重启动 header:
MBALIGN equ 1<<0 ; align loaded modules on page boundaries
MEMINFO equ 1<<1 ; provide memory map
FLAGS equ MBALIGN | MEMINFO ; this is the Multiboot 'flag' field
MAGIC equ 0x1BADB002 ; 'magic number' lets bootloader find the header
CHECKSUM equ -(MAGIC + FLAGS) ; checksum of above, to prove we are multiboot
section .multiboot
align 4
dd MAGIC
dd FLAGS
dd CHECKSUM
我正在执行以下操作来创建图像:
nasm -felf32 -o init.bin init.s
cp init.bin target/boot/init.bin
grub2-mkrescue -o init.iso target/
然后我运行 qemu 启动它:
qemu-system-x86_64 -cdrom ./init.iso
从启动菜单中选择 'myos' 后,出现错误
error: invalid arch-dependent ELF magic
这是什么意思,我该如何解决?我试过弄乱 elf 格式,但似乎只有 -felf32
有效...
GRUB 支持 ELF32 和平面二进制文件。你的 header 虽然含蓄地说你提供了一个 ELF 二进制文件。
将平面二进制文件与多重启动一起使用
如果您想告诉多重引导加载程序 (GRUB) 您正在使用平面二进制文件,您必须将 bit 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 header,它为 Multiboot 加载程序提供将我们的二进制文件加载到内存中的信息。当使用 ELF 格式时,此信息位于我们代码之前的 ELF header 中,因此不必明确提供。 Multiboot header 在 GRUB documentation 中有非常详细的定义。
当使用 NASM 和 -f bin
时,请务必注意我们需要为我们的代码指定原点。多重引导加载程序将我们的内核加载到物理地址 0x100000
。我们必须在我们的 assembler 文件中指定我们的原点是 0x100000
以便在我们最终的平面二进制图像中生成适当的偏移等。
这是一个从我自己的项目中剥离和修改的示例,它提供了一个简单的 header。对 _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 header。如果 Multiboot header FLAG 的 bit 16 是明确的,则假定您提供的是 ELF 映像。然后它解析 ELF header 以检索将内核文件加载到内存中所需的信息。如果设置了 bit 16 则需要一个完整的 Multiboot header 以便加载程序具有将内核读入内存的信息,执行初始化,然后调用内核。
然后你会 assemble 你的 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生成object个文件(它们不可执行),必须是linked(例如 LD)生成最终的 ELF(ELF32) 可执行文件。如果您已经完成了 assemble 和 link 过程,它可能会起作用:
nasm -f elf32 init.s -o init.o
ld -Ttext=0x100000 -melf_i386 -o init.bin init.o
请注意,使用 -f elf32
时,您必须从 init.s 中删除 ORG 指令。 ORG 指令仅在使用 -f bin
时适用。 Multiboot 加载程序将在物理地址 0x100000
加载我们,因此我们必须确保 assembled 和 linked 代码是从该起始点生成的。使用 -f elf32
时,我们在 linker (LD) 命令行上使用 -Ttext=0x100000
指定入口点。或者,可以在 linker 脚本中设置原点。
使用 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被告知生成ELF32objects。我们 assemble init.s 进入一个名为 ELF object 的文件 init.o。然后我们可以使用 linker (LD) 从 init.o 生成一个 ELF 可执行文件 调用了 init.elf。我们使用一个名为 objcopy 的特殊程序来剥离所有 ELF headers 并生成一个名为 [=90= 的平面二进制可执行文件]init.bin。
这比仅使用 NASM 和 -f bin
选项来生成平面可执行文件要复杂得多 init.bin。那何必呢?使用上面的方法,您可以告诉 NASM 生成可以被 gdb(GNU 调试器)使用的调试信息。如果您尝试使用 -g
(启用调试)和 NASM 使用 -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 格式),这些信息将 link 与 LD 到 init.elf (保留调试信息)。平面二进制文件不包含调试信息,因为它们在以下情况下被剥离您可以将 objcopy 与 -O binary
一起使用。如果在 QEMU 中启用远程调试工具并使用 GDB,则可以使用 init.elf用于调试。 init.elf 中的调试信息向调试器提供信息,允许您单步执行代码、按名称访问变量和标签,请参阅源代码 assembler代码等
除了生成调试信息之外,还有一个原因需要使用 NASM/LD/ OBJCOPY 进程生成内核二进制文件。 LD 非常适合配置。 LD 允许人们创建 linker 脚本,使您可以更好地调整最终二进制文件中的布局方式。这对于可能包含来自不同环境(C、汇编程序等)的混合代码的更复杂的内核很有用。对于小型玩具内核,可能不需要它,但随着内核复杂性的增加,使用 linker script 的好处将变得更加明显。
使用GDB远程调试QEMU
如果您使用上一节中的方法在 ELF 可执行文件 (init.elf) 中生成调试信息,您可以启动 QEMU 并拥有它:
- 加载 QEMU 环境并在启动时停止 CPU。从手册页:
-S Do not start CPU at startup (you must type 'c' in the monitor).
- 使 QEMU 监听 localhost:1234 上的 GDB 远程连接。从手册页:
-s Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234.
然后您只需要启动 GDB 以便它:
- 使用我们的 ELF 可执行文件(init.elf)启动 GDB 并进行调试符号和信息
- 连接到 localhost:1234,其中 QEMU 正在侦听
- 设置您选择的调试布局
- 在我们的内核中设置一个停止点(在这个例子中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 这样的多重引导兼容加载器时,CPU 处于 32 位保护模式(而不是 16 位实模式)。与直接从 BIOS 引导不同,您将无法使用 16 位代码,包括大多数 PC-BIOS 中断。如果您需要处于实模式,则必须手动更改回实模式,或者创建一个 VM86 任务(后者并不简单)。
这是一个重要的考虑因素,因为您在 MikeOS 中 link编辑的一些代码是 16 位的。
我正在尝试按照此处的说明构建一个简单的 OS 内核:http://mikeos.sourceforge.net/write-your-own-os.html
除了,我不想从软盘启动,而是想创建一个 grub-based ISO 映像并在模拟器中启动多启动 CD。我已将以下内容添加到该页面列出的源代码中,用于多重启动 header:
MBALIGN equ 1<<0 ; align loaded modules on page boundaries
MEMINFO equ 1<<1 ; provide memory map
FLAGS equ MBALIGN | MEMINFO ; this is the Multiboot 'flag' field
MAGIC equ 0x1BADB002 ; 'magic number' lets bootloader find the header
CHECKSUM equ -(MAGIC + FLAGS) ; checksum of above, to prove we are multiboot
section .multiboot
align 4
dd MAGIC
dd FLAGS
dd CHECKSUM
我正在执行以下操作来创建图像:
nasm -felf32 -o init.bin init.s
cp init.bin target/boot/init.bin
grub2-mkrescue -o init.iso target/
然后我运行 qemu 启动它:
qemu-system-x86_64 -cdrom ./init.iso
从启动菜单中选择 'myos' 后,出现错误
error: invalid arch-dependent ELF magic
这是什么意思,我该如何解决?我试过弄乱 elf 格式,但似乎只有 -felf32
有效...
GRUB 支持 ELF32 和平面二进制文件。你的 header 虽然含蓄地说你提供了一个 ELF 二进制文件。
将平面二进制文件与多重启动一起使用
如果您想告诉多重引导加载程序 (GRUB) 您正在使用平面二进制文件,您必须将 bit 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 header,它为 Multiboot 加载程序提供将我们的二进制文件加载到内存中的信息。当使用 ELF 格式时,此信息位于我们代码之前的 ELF header 中,因此不必明确提供。 Multiboot header 在 GRUB documentation 中有非常详细的定义。
当使用 NASM 和 -f bin
时,请务必注意我们需要为我们的代码指定原点。多重引导加载程序将我们的内核加载到物理地址 0x100000
。我们必须在我们的 assembler 文件中指定我们的原点是 0x100000
以便在我们最终的平面二进制图像中生成适当的偏移等。
这是一个从我自己的项目中剥离和修改的示例,它提供了一个简单的 header。对 _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 header。如果 Multiboot header FLAG 的 bit 16 是明确的,则假定您提供的是 ELF 映像。然后它解析 ELF header 以检索将内核文件加载到内存中所需的信息。如果设置了 bit 16 则需要一个完整的 Multiboot header 以便加载程序具有将内核读入内存的信息,执行初始化,然后调用内核。
然后你会 assemble 你的 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生成object个文件(它们不可执行),必须是linked(例如 LD)生成最终的 ELF(ELF32) 可执行文件。如果您已经完成了 assemble 和 link 过程,它可能会起作用:
nasm -f elf32 init.s -o init.o
ld -Ttext=0x100000 -melf_i386 -o init.bin init.o
请注意,使用 -f elf32
时,您必须从 init.s 中删除 ORG 指令。 ORG 指令仅在使用 -f bin
时适用。 Multiboot 加载程序将在物理地址 0x100000
加载我们,因此我们必须确保 assembled 和 linked 代码是从该起始点生成的。使用 -f elf32
时,我们在 linker (LD) 命令行上使用 -Ttext=0x100000
指定入口点。或者,可以在 linker 脚本中设置原点。
使用 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被告知生成ELF32objects。我们 assemble init.s 进入一个名为 ELF object 的文件 init.o。然后我们可以使用 linker (LD) 从 init.o 生成一个 ELF 可执行文件 调用了 init.elf。我们使用一个名为 objcopy 的特殊程序来剥离所有 ELF headers 并生成一个名为 [=90= 的平面二进制可执行文件]init.bin。
这比仅使用 NASM 和 -f bin
选项来生成平面可执行文件要复杂得多 init.bin。那何必呢?使用上面的方法,您可以告诉 NASM 生成可以被 gdb(GNU 调试器)使用的调试信息。如果您尝试使用 -g
(启用调试)和 NASM 使用 -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 格式),这些信息将 link 与 LD 到 init.elf (保留调试信息)。平面二进制文件不包含调试信息,因为它们在以下情况下被剥离您可以将 objcopy 与 -O binary
一起使用。如果在 QEMU 中启用远程调试工具并使用 GDB,则可以使用 init.elf用于调试。 init.elf 中的调试信息向调试器提供信息,允许您单步执行代码、按名称访问变量和标签,请参阅源代码 assembler代码等
除了生成调试信息之外,还有一个原因需要使用 NASM/LD/ OBJCOPY 进程生成内核二进制文件。 LD 非常适合配置。 LD 允许人们创建 linker 脚本,使您可以更好地调整最终二进制文件中的布局方式。这对于可能包含来自不同环境(C、汇编程序等)的混合代码的更复杂的内核很有用。对于小型玩具内核,可能不需要它,但随着内核复杂性的增加,使用 linker script 的好处将变得更加明显。
使用GDB远程调试QEMU
如果您使用上一节中的方法在 ELF 可执行文件 (init.elf) 中生成调试信息,您可以启动 QEMU 并拥有它:
- 加载 QEMU 环境并在启动时停止 CPU。从手册页:
-S Do not start CPU at startup (you must type 'c' in the monitor).
- 使 QEMU 监听 localhost:1234 上的 GDB 远程连接。从手册页:
-s Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234.
然后您只需要启动 GDB 以便它:
- 使用我们的 ELF 可执行文件(init.elf)启动 GDB 并进行调试符号和信息
- 连接到 localhost:1234,其中 QEMU 正在侦听
- 设置您选择的调试布局
- 在我们的内核中设置一个停止点(在这个例子中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 这样的多重引导兼容加载器时,CPU 处于 32 位保护模式(而不是 16 位实模式)。与直接从 BIOS 引导不同,您将无法使用 16 位代码,包括大多数 PC-BIOS 中断。如果您需要处于实模式,则必须手动更改回实模式,或者创建一个 VM86 任务(后者并不简单)。
这是一个重要的考虑因素,因为您在 MikeOS 中 link编辑的一些代码是 16 位的。