尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
QEMU与Multiboot实战:从零构建可启动内核全流程
简介启动是操作系统学习的第一道门槛。这套Ustc OS课程Lab1资料提供了一套完整的QEMUMultiboot启动实验方案内含实验报告与源代码。资源共10个文件PDF报告讲解Multiboot头部设计与启动流程汇编源码和链接脚本定义头部结构与内存布局Makefile负责自动构建bin与o是生成的镜像和中间目标文件json可能为工程配置整体仅778KB小巧完整。实验需要在Ubuntu下安装并配置QEMU与主机建立共享文件夹理解Multiboot协议中的魔数、标志位、校验和编写合规的Multiboot头部随后在VGA显存写入helloworld并通过串口输出HELLOWORLD期间会用到汇编、Makefile与链接描述文件等基础知识点。目前已有293人学习适合刚接触操作系统、希望亲手验证x86启动过程的初学者。对照报告与源码可以清楚看到从汇编、链接脚本到QEMU运行的完整链路并理解虚拟机调试、编译构建与底层输出验证的配合方法。1. 从零开始的 OS 启动实验QEMU 与 Multiboot 规范实战刚接触操作系统课程时第一个拦路虎往往不是 C 语言或汇编语法而是“我写的内核到底怎么跑起来”。某高校的 OS 实验 Lab1 要求用 QEMU 模拟器加载一个符合 Multiboot 规范的内核镜像看似简单但牵扯到链接脚本、汇编入口、GRUB 引导协议和 QEMU 调试参数任何一个环节出错都是黑匣子。这篇文章把整个流程拆开揉碎从 Multiboot 头结构讲到 QEMU 的-kernel参数与-gdb调试再给出可直接复现的 Makefile 和汇编启动代码。适合刚接触内核开发、想弄懂“CPU 上电后到底执行了什么”的读者也适合做课程实验前想提前踩坑的同学。2. Multiboot 启动协议为什么 GRUB 能直接加载你的内核2.1 启动协议的本质操作系统与引导器之间的契约Multiboot 规范是 GRUB 等引导器与操作系统内核之间的一份“合同”。这份合同规定了内核镜像必须在文件头部的固定偏移处写入一个魔法数字、标志位和校验和引导器读取这些字段后确认“这是个合法内核”然后按照约定设置 CPU 状态、加载镜像到内存、跳转到入口点执行。我最初以为把编译出的 ELF 文件直接丢给 QEMU 就能跑结果 QEMU 直接报 “not a valid boot device”就是因为 ELF 文件头部没有 Multiboot 头。/* multiboot_header.S - 放在汇编入口文件最前面 */ .set ALIGN, 10 /* 对齐到 4KB 边界 */ .set MEMINFO, 11 /* 提供内存布局信息 */ .set FLAGS, ALIGN | MEMINFO /* Multiboot 标志 */ .set MAGIC, 0x1BADB002 /* 魔数固定值 */ .set CHECKSUM, -(MAGIC FLAGS) /* 校验和 */ .section .multiboot .align 4 .long MAGIC .long FLAGS .long CHECKSUM这段代码的作用是在内核文件的最前面放一个 12 字节的结构体。MAGIC是固定值0x1BADB002引导器看到它才知道“这是我认识的格式”FLAGS告诉引导器需要哪些服务ALIGN | MEMINFO表示要求 4KB 对齐并且希望引导器把内存布局信息传给内核CHECKSUM是三者之和为 0 的约束用于校验头是否损坏。注意.section .multiboot这个节区名称不是随意的链接脚本里必须把该节放在最前面否则引导器在文件开头找不到这个头。2.2 为什么用 QEMU 而不是真机真机启动涉及 BIOS/UEFI 固件、磁盘分区表、引导扇区任何一个字节写错就得重新烧录 U 盘调试效率极低。QEMU 是一个纯软件模拟器它模拟了 CPU、内存、磁盘和各类外设而且支持-kernel参数直接加载内核镜像、-gdb参数开放远程调试端口。我一般用qemu-system-i386 -kernel build/kernel.bin -gdb tcp::1234 -S启动-S让 CPU 在启动时暂停等 GDB 连接后再单步执行这样能观察到第一条指令长什么样。QEMU 参数作用调试场景-kernel加载内核镜像指定编译产物替代磁盘镜像-gdb tcp::1234开放 GDB 服务端口配合gdb远程调试-S启动时冻结 CPU等待调试器连接观察初始状态-d in_asm输出翻译后的汇编指令追踪 CPU 实际执行流-monitor stdio打开 QEMU 控制台查看寄存器、内存、设备状态2.3 链接脚本决定内核镜像的内存布局启动协议只负责“引导器如何找到内核”而“内核的代码段放在哪、数据段放在哪”是由链接脚本决定的。我一开始用默认链接脚本结果.text段被链接到0x08049000之类的地址但 Multiboot 规范要求入口点地址必须是 32 位平坦模式下的物理地址内核通常在 1MB 以上0x100000运行。/* link.ld - 内核链接脚本 */ OUTPUT_FORMAT(elf32-i386) ENTRY(_start) SECTIONS { . 1M; /* 内核加载到物理地址 1MB 处 */ .text : ALIGN(4K) { *(.multiboot) /* Multiboot 头必须在这 */ *(.text) } .data : ALIGN(4K) { *(.data) } .bss : ALIGN(4K) { *(COMMON) *(.bss) } }. 1M是把当前虚拟地址设为 1MB这决定了所有符号的地址。.text : ALIGN(4K)把代码段对齐到 4KB 边界Multiboot 头就在.multiboot节区里所以它在链接产物里的位置是0x100000偏移处。ENTRY(_start)告诉链接器入口点是_start符号引导器跳转的地址就是这个符号的地址。我踩过的坑是链接脚本里只写了* (.text)忘了*(.multiboot)结果头被丢到别的位置GRUB 报 “invalid magic number”。3. 汇编启动入口从实模式到保护模式的跳转3.1 为什么必须用汇编写入口C 语言的运行依赖栈、全局变量、库函数但这些在 CPU 刚跳进内核时统统不存在。Multiboot 规范要求引导器进入内核时 CPU 处于 32 位保护模式但栈指针esp是未定义的段寄存器也可能残留引导器的值。所以我用汇编先设置栈、清空ebx里的内存布局指针、然后跳转到 C 语言的kernel_main这一步是 C 世界的地基。# boot.S - 内核汇编入口 .set ALIGN, 10 .set MEMINFO, 11 .set FLAGS, ALIGN | MEMINFO .set MAGIC, 0x1BADB002 .set CHECKSUM, -(MAGIC FLAGS) .section .multiboot .align 4 .long MAGIC .long FLAGS .long CHECKSUM .section .text .global _start .type _start, function _start: mov $stack_top, %esp /* 设置内核栈指针 */ push %ebx /* ebx 指向 multiboot_info 结构 */ call kernel_main /* 跳转到 C 入口 */ loop: jmp loop /* 内核返回后死循环 */ .section .bss .align 16 stack_bottom: .skip 16384 /* 16KB 内核栈 */ stack_top:_start是整个内核的入口mov $stack_top, %esp把栈顶地址装入栈指针push %ebx是 Multiboot 规范规定的引导器进入内核时ebx寄存器指向一个multiboot_info结构体里面存有内存布局、命令行参数、模块信息等。call kernel_main是真正的 C 入口但注意kernel_main应该是永不返回的函数如果返回了loop死循环能兜底。.skip 16384分配了 16KB 的 BSS 段空间作为内核栈因为 C 语言的局部变量、函数调用返回地址都在这块栈上。3.2 从 QEMU 视角看启动瞬间按qemu-system-i386 -kernel build/kernel.bin -S -d in_asm -D qemu.log启动后QEMU 会把 CPU 执行的每一条指令记录到qemu.log。我在调试时最常看的是前几条指令入口地址是否为0x100000GRUBQEMU 内置是否已经切换到保护模式段寄存器是否如规范所述初始化。如果-d in_asm的输出里第一条指令不是mov $stack_top, %esp那大概率是链接脚本或汇编文件有问题。3.3 内核入口点的验证方法# 用 readelf 验证内核入口点 readelf -h build/kernel.bin # 输出里关注 Entry point address 字段 # 期望值: 0x00100000 左右由链接脚本决定 # 用 objdump 反汇编前几行 objdump -d build/kernel.bin # 第一行应是对应 _start 的汇编指令readelf -h查看 ELF 文件头Entry point address字段是引导器跳转的目标地址必须和链接脚本ENTRY(_start)设置一致。objdump -d反汇编整个镜像重点确认.multiboot节里的0x1BADB002出现在文件最前面而不是被链接到其他位置。我在第一次实验时忘记在 C 文件里extern void kernel_main(void)导致汇编里的call kernel_main被链接器解析成错误地址但编译不报错运行时直接跳飞到 0x00000000。用objdump -d逐条核对指令里的目标地址是个好习惯。3.4 栈的陷阱为什么不能随便给esp赋值我见过有同学直接在汇编里写mov $0x90000, %esp理由是“这个地址应该没被占用”。但问题在于如果不了解物理内存布局这个地址可能踩到 BIOS 数据区、帧缓冲或 ACPI 表格。我习惯的做法是像上面那样在 BSS 段里预留一块静态空间把栈顶符号交给链接器计算。BSS 段是未初始化数据段不占用镜像文件空间但加载器会在内存里把它清零所以这块栈是安全的。4. 编译、链接与 Makefile把三个文件串成一条流水线4.1 为什么需要 Makefile 而不是逐个命令敲因为内核构建涉及汇编、C 编译、链接三个步骤而且每次修改都要重新编译手动敲命令容易漏掉依赖关系。Makefile 能根据文件时间戳自动判断哪些需要重编还能统一管理编译参数。# Makefile - 内核构建脚本 C_SOURCES $(wildcard kernel/*.c) HEADERS $(wildcard kernel/*.h) OBJ ${C_SOURCES:.c.o kernel/boot.o} CC gcc CFLAGS -m32 -ffreestanding -nostdlib -fno-pic -fno-stack-protector LDFLAGS -m elf_i386 -T link.ld AS as ASFLAGS --32 all: kernel.bin kernel.bin: $(OBJ) $(LD) $(LDFLAGS) -o $ $^ %.o: %.c $(HEADERS) $(CC) $(CFLAGS) -c $ -o $ %.o: %.S $(AS) $(ASFLAGS) $ -o $ clean: rm -rf *.o kernel/*.o kernel.bin qemu: kernel.bin qemu-system-i386 -kernel kernel.bin -monitor stdio-m32告诉 GCC 生成 32 位代码因为 Multiboot 规范运行在 32 位保护模式-ffreestanding表示不依赖宿主系统的标准库因为内核里没有printf可用-nostdlib禁止链接标准库-fno-pic关闭位置无关代码避免生成额外的重定位信息-fno-stack-protector关闭栈保护因为内核里没有__stack_chk_fail函数。-m elf_i386是给链接器的参数指定 32 位 ELF 格式。${C_SOURCES:.c.o kernel/boot.o}把kernel/下所有.c文件展开成对应的.o文件再加上手动指定的kernel/boot.o。4.2 链接失败的常见错误信息undefined reference to _start这个报错最迷惑人因为你的汇编文件里明明定义了_start。原因通常是汇编文件没有被编译进链接过程或者编译后的.o文件没有传给链接器。检查 Makefile 里OBJ变量是否有kernel/boot.o。另一个常见错误是relocation truncated to fit: R_386_16 against .text这表示 16 位地址放不下某个跳转目标多半是用了--32汇编参数但链接脚本里地址超过 64KB 限制。4.3 调试构建流程的实践经验# 生成依赖关系文件排查头文件修改是否触发重编 gcc -MM kernel/*.c deps.mk # 查看整个构建过程的详细命令 make V1 # 用 nm 查看符号表确认入口点 nm build/kernel.bin | grep _startmake V1会输出每条实际执行的命令而不是默认的CC kernel/main.o简写能确认编译参数是否真的传进去了。nm输出符号表_start必须存在且地址落在0x00100000附近。如果地址不对回看链接脚本的. 1M;设置。5. 踩坑记录Multiboot 启动最容易翻车的五个细节5.1 现象QEMU 提示 “invalid or unsupported executable format”这是新手必经的一道坎。原因有三个一是链接脚本没有把.multiboot节放到镜像开头导致引导器在文件头部找不到 Multiboot 头二是编译时用了默认的 64 位模式生成的是ELF64文件而引导器只认 32 位三是链接脚本里OUTPUT_FORMAT丢了链接器默认输出原生格式。我的解决办法是先用readelf -h看Class字段是否为ELF32再用xxd或hexdump查看文件前 16 字节确认1B AD B0 02出现在偏移 0 处。如果是 64 位检查 Makefile 里-m32和--32是否都加了。5.2 现象内核能进入_start但执行到call kernel_main时 CPU 重启CPU 重启通常是三胞胎问题栈指针没设置、kernel_main地址错误、或 C 代码里访问了无效内存。我当时的场景是kernel_main里第一行就调用了一个全局变量初始化函数但这个函数引用了 BSS 段里的数组而 BSS 段在链接脚本里没对齐导致数组地址没落在 4KB 边界上。后来我在链接脚本里给.bss加了ALIGN(4K)并在kernel_main开头加了memset清零 BSS 段的逻辑问题消失。5.3 现象命令行里加了-gdb tcp::1234但 GDB 连不上-gdb tcp::1234是 QEMU 的 GDB 服务端口但 QEMU 只在第一次执行-S时暂停如果没用-SCPU 已经跑飞了自然连不上。正确组合是qemu-system-i386 -kernel build/kernel.bin -S -gdb tcp::1234然后另开一个终端gdb build/kernel.bin target remote :1234 info registers如果info registers显示eip0x0000fff0说明 CPU 还没被引导器接管如果eip在0x100000附近说明已经进入内核。5.4 现象Multiboot 头校验通过但内核永远无法访问内存布局信息Multiboot 规范规定引导器把内存布局指针放在ebx寄存器里但有些同学在汇编入口里直接push %ebx然后调用 C 函数结果 C 函数里用(multiboot_info_t *)(0x0)硬编码地址自然拿不到数据。正确做法是把ebx的值传给一个 C 函数参数比如声明kernel_main(uint32_t magic, uint32_t addr)然后在汇编里写出push %ebx push %eax call kernel_maineax存放的是 Multiboot 魔数0x2BADB002ebx存放内存布局指针这两个值都得传给 C 入口。5.5 现象内核编译通过但一运行就出现 Segmentation Fault这个现象在 QEMU 里表现为 CPU 异常-d int能看到#GP或#PF中断。最常见的原因是 C 代码里用了全局变量初始化比如int arr[100] {0}这个数组放在.data段但链接脚本没预留足够的空间。.data段确实要占镜像文件空间如果链接脚本只写了.text而忘了.data所有数据段符号都会被链接到错误的地址。检查链接脚本里是否包含了.data和.bss段定义。6. 进阶调试技巧用 QEMU 的 GDB 接口单步追踪引导流程6.1 在 GDB 里设置硬件断点验证 Multiboot 头Multiboot 头的读取是引导器做的我们看不到引导器代码但可以在_start处下断点然后查看寄存器和内存set disassembly-flavor intel target remote :1234 break *0x100000 continue x/12bx 0x100000 info registersx/12bx 0x100000查看内存地址0x100000处的 12 个字节应该能看到1B AD B0 02开头的 Multiboot 头。info registers检查eax是否为0x2BADB002、ebx是否为非零地址、cs段寄存器是否指向 32 位代码段。如果这些值都正确说明引导协议已经走通接下来可以放心调试 C 代码。6.2 用 QEMU Monitor 查看物理内存布局QEMU 的 Monitor 控制台启动时加-monitor stdio提供了info mem和xp命令可以在不打断程序的情况下查看物理内存。我在调试内存布局问题时常用(qemu) xp /20dw 0x100000 (qemu) info registers (qemu) info memxp是查看物理地址的命令/20dw表示以 32 位为单位显示 20 个值。info mem显示当前页表映射情况如果 CPU 还没开分页输出会提示不可用。这套组合拳能把“代码编译正确但运行异常”的问题快速缩小到地址范围。6.3 一个被忽视的细节GDB 的架构设置gdb -q build/kernel.bin set architecture i386 set disassembly-flavor intel target remote :1234QEMU 的 GDB 服务默认按当前 CPU 模式解析但如果宿主机是 64 位系统GDB 可能默认用i386:x86-64架构解析 32 位内核导致反汇编结果全是乱码。在target remote之前执行set architecture i386能强制切换这是我从一次“反汇编结果完全不可读”的现象里学到的。从那以后每次启动 GDB 调试内核我都强制走一遍这三条设置命令之后再也没被反汇编结果误导过。希望这篇笔记能帮你在 Lab1 上少走几个来回把精力留给后面的内存管理和进程调度。本文还有配套的精品资源点击获取
RELATED

相关推荐

Multiboot启动实验:从GRUB引导到C语言内核的完整流程

Multiboot启动实验:从GRUB引导到C语言内核的完整流程

简介:面向刚开始接触操作系统实验的学生,这份资源以QEMU与Multiboot协议为切入点,完整演示如何在汇编中编写multiboot header:直接写VGA显存输出“helloworld”,借助串口输出“HELLOWORLD”,并将Ubuntu与主…

📅 2026/10/16 9:37:36
曲面细分与简化实战:Catmull-Clark与QEM算法详解

曲面细分与简化实战:Catmull-Clark与QEM算法详解

1. 从“面数焦虑”说起:曲面细分与简化到底在解决什么问题做三维建模或者图形渲染的朋友,迟早会撞上一个绕不开的矛盾:模型面数。面数少了,模型棱角分明,曲面看起来像纸折的;面数多了,渲染一帧要…

📅 2026/10/16 9:37:36
Elasticsearch 9200端口拒绝连接:从报错到根因的排查指南

Elasticsearch 9200端口拒绝连接:从报错到根因的排查指南

刚看到这个报错的时候,八成是又在调测试环境,顺手敲了条curl http://walker001:9200想看下 Elasticsearch 集群状态,结果却出来一行curl: (7) Failed to connect to walker001 port 9200: 拒绝连接。干这行久了,这种提示见得太多&…

📅 2026/10/16 9:32:34
MORE NEWS

更多资讯

📰

面向对象核心:类与对象的区别、原理与实战

我经常被初学者问到一个问题:类和对象到底有什么区别?带新同学入门的时候,几乎每个人都会在这个地方卡一下。这个问题看似基础,其实特别值得花时间想透,因为后面所有东西——继承、多态、封装、设计模式——全都站在这…

📰

WGS84转ENU坐标转换从原理到实战,避免那些坑

在户外测量、无人机航线规划、自动驾驶路测或者机器人室内外定位的时候,你一定遇到过这种场景:手里拿到的是GPS设备输出的经纬度——也就是WGS84坐标系下的数据,但你要用这套数据去控制车辆或者飞机,去跟激光雷达点云做对齐&#…

📰

giflib 4.1.6 64位静态库在Windows MSVC工程中的配置与避坑指南

简介:面向需要在 osg 3.2.1 中编译 Plugin gif 的开发者,这份资源提供了 4.1.6 版本的 64 位静态链接库 giflib.lib 及配套头文件,解决了 GIF 图像编解码库与 OpenSceneGraph 集成时的链接难题。库内包含 giflib.lib、giflibd.lib&#xff08…

📰

从爬虫到Flutter影视App:移动端全栈实践完整指南

电影全栈系统的前几篇,我们把爬虫、后端接口、数据库、推荐服务都搭了起来,服务端的数据管道已经能稳定输出。但整个系统始终缺一个真正面向用户的东西,数据在接口里躺着,后台能看到,却没有任何一个贴近日常使用场景的…

📰

Go指针完全指南:从地址、多级指针到运算边界

先聊一个老生常谈的话题:指针。很多刚接触Go的程序员,写代码时对指针能避则避,总觉得这玩意儿是历史遗留的麻烦,又难理解又容易出bug。但只要你认真写过几个项目,就一定会撞上需要指针的时刻——比如修改实参、自定义数…

📰

Graphviz DOT语言从入门到精通:语法、布局引擎与工程实践

1. 为什么值得花时间啃透 DOT 这门“画图语言”第一次接触 Graphviz 的人,多半是被一段.dot文本劝退的:满屏的digraph、->、node [shapebox],看起来像某种上古配置文件,跟“画图”这件事八竿子打不着。但只要你真正用它画过一张…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬