PAYATHON 2026

为什么 GRUB 无法在 Legacy BIOS 虚拟机中启动带 Multiboot 头的内核?

数据小许

给出的代码有几处问题足以导致启动失败:Multiboot 魔数写错,flags 启用了未提供的地址字段,GRUB 菜单命令拼错,启动代码也没有在调用 C 函数前建立栈。建议先修正这些问题,再检查固件和 ISO 的引导支持。

目前没有具体的 GRUB 报错、最终 ELF 文件或虚拟机启动日志,无法确定哪一项最先触发,也不能据此认定 Legacy BIOS 不支持这个内核。

1. 修正 Multiboot 魔数和 flags

原始代码是:

MB_MAGIC equ 0x1BAD002
MB_FLAGS equ 0x0010003

Multiboot 1 要求的魔数是 0x1BADB002。原来的值少了一个 B,GRUB 无法通过它识别有效的 Multiboot 头。

0x0010003 等于 0x00010003,除了 bit 0 和 bit 1,还设置了 bit 16。设置 bit 16 后,头部必须包含 header_addr、load_addr、load_end_addr、bss_end_addr 和 entry_addr。当前代码只提供了前三个基本字段,后面紧跟的机器码可能被当作地址字段读取。

如果最终内核使用 ELF 格式,并由 ELF 头描述加载位置,可以清除 bit 16:

MB_MAGIC equ 0x1BADB002
MB_FLAGS equ 0x00000003
MB_CHECKSUM equ -(MB_MAGIC + MB_FLAGS)

其中,bit 0 请求启动模块按页对齐,bit 1 请求基本内存信息;它们不会自动安排内核各节的布局。魔数、flags 和 checksum 的 32 位无符号和必须为零。详见 Multiboot 头部字段规范。

2. 分开头部与代码,并在调用 C 前建立栈

原始汇编在 section .multiboot 后直接定义 _start,没有切换到 .text。因此,启动指令也会落入 .multiboot,节属性和链接布局会比较难控制。

更直接的运行时问题是,call kinit 会使用栈,但代码从未初始化 ESP。Multiboot 不保证提供可用的启动栈,内核需要自行建立。规范也不保证方向标志为零,因此进入 C 代码前应执行 cld。参见 Multiboot 机器状态约定。

对于按 32 位 ELF 构建、使用普通 i386 C 调用约定的内核,可以使用以下启动代码:

bits 32

MB_MAGIC equ 0x1BADB002
MB_FLAGS equ 0x00000003
MB_CHECKSUM equ -(MB_MAGIC + MB_FLAGS)

section .multiboot progbits alloc noexec nowrite align=4
dd MB_MAGIC
dd MB_FLAGS
dd MB_CHECKSUM

section .text
global _start
extern kinit

_start:
    cli
    cld
    mov esp, stack_top
    xor ebp, ebp
    call kinit

.hang:
    hlt
    jmp .hang

section .bss
align 16
stack_bottom:
    resb 16384
stack_top:

section .note.GNU-stack noalloc noexec nowrite progbits

示例预留了 16 KiB 栈空间,调用前的栈顶按 16 字节对齐。当前 kinit 没有参数,所以没有传递 Multiboot 信息。如果之后需要引导信息,应在其他代码覆盖寄存器前保存入口处的 EAX 和 EBX。

3. 在链接脚本中明确放置 Multiboot 头

Multiboot 1 要求整个头部位于内核镜像文件的前 8192 字节内,并按 4 字节对齐。这是对文件偏移的要求,与内核加载到 0x00100000 是两个不同的条件。参见 Multiboot 镜像格式要求。

原脚本没有明确安排 .multiboot,它的最终位置取决于链接器如何处理未匹配的输入节。不能仅凭源码断言它一定超出了范围,但可以在脚本中固定布局:

OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
ENTRY(_start)

SECTIONS
{
    . = 0x00100000;

    .multiboot ALIGN(4) :
    {
        KEEP(*(.multiboot))
    }

    .text ALIGN(0x1000) :
    {
        *(.text .text.*)
    }

    .rodata ALIGN(0x1000) :
    {
        *(.rodata .rodata.*)
    }

    .data ALIGN(0x1000) :
    {
        *(.data .data.*)
    }

    .bss ALIGN(0x1000) :
    {
        *(COMMON)
        *(.bss .bss.*)
    }
}

KEEP 可以防止启用节垃圾回收时丢弃这个头部。最终仍需检查 ELF 的文件偏移和加载段;链接脚本中的排列顺序不能直接证明镜像已符合规范。

4. 修正 GRUB 菜单配置

grub.cfg 中的菜单命令应为 menuentry,原来的 menutry 是拼写错误。对于 GRUB 2,加载 Multiboot 1 内核应使用 multiboot。Legacy BIOS 是固件模式,并不表示正在使用 GRUB Legacy。

调试时可以先生成以下配置:

set timeout=5
set default=0

menuentry "Crix" {
    multiboot /boot/crix
    boot
}

这些命令的含义见 GNU GRUB 手册。

Makefile 中生成配置的部分可以替换为:

	@printf '%s\n' 'set timeout=5' 'set default=0' 'menuentry "Crix" {' '    multiboot /boot/crix' '    boot' '}' > $(TARGET_DIR)/boot/grub/grub.cfg

将这一行放在创建 $(TARGET_DIR)/boot/grub 之后、运行 grub-mkrescue 之前,并删除原来的配置生成命令。Makefile recipe 行必须以实际 Tab 开头;如果原始材料只是在展示时丢失了缩进,则无需修改已有的正确缩进。

5. 检查构建产物,再区分故障阶段

bits 32 只控制 NASM 的指令编码,不能保证 GCC 输出 32 位代码,也不能保证链接结果是 32 位 ELF。汇编对象应使用 nasm -f elf32,C 对象和最终链接目标也必须与之匹配。内核应采用 freestanding 构建方式,并避免链接宿主系统的启动代码或依赖动态加载器。

重新构建后,可以先运行:

file kernel/crix
readelf -h kernel/crix
readelf -SW kernel/crix
readelf -lW kernel/crix

if grub-file --is-x86-multiboot kernel/crix; then
    echo "Multiboot header recognized"
else
    echo "Multiboot header not recognized"
fi

检查 ELF 类型、入口地址、.multiboot 的文件偏移,以及启动代码和栈是否处于合适的加载段中。头部应满足 offset % 4 == 0,且 offset + header_size <= 8192。grub-file 的识别结果只能作为辅助检查,不能证明 C 函数、显示驱动或整个启动过程能够正常运行。

随后重新复制内核、生成 ISO,再根据现象定位:

  • 连 GRUB 都没有出现:检查虚拟机是否从新 ISO 启动,以及镜像是否包含用于 Legacy BIOS 的 GRUB 引导内容。构建环境需要具备对应的 i386-pc 平台支持,具体安装方式取决于发行版。
  • 进入了 GRUB,但加载内核时报错:先检查实际生成的 grub.cfg、/boot/crix 路径和 Multiboot 头。调试期间保留菜单等待时间,并记录完整报错。
  • GRUB 已交出控制权,随后黑屏或重启:检查 _start 是否执行、栈是否有效,以及 C 构建选项和 print 实现。原始材料没有提供 cga.h 和 print 的实现,因此暂时无法判断屏幕输出部分是否正确。

完成这些修正后,再判断是否存在只在某种固件路径下触发的问题。当前代码中的错误已足以解释启动失败,暂时不必先切换固件模式来绕过。

备注:内容仅供参考。