为什么 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的实现,因此暂时无法判断屏幕输出部分是否正确。
完成这些修正后,再判断是否存在只在某种固件路径下触发的问题。当前代码中的错误已足以解释启动失败,暂时不必先切换固件模式来绕过。
备注:内容仅供参考。
