创建一个加载了 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 binNASM。如果从 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 与 LDinit.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 位的。