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

发布时间:2026/10/16 9:37:40

Multiboot启动实验:从GRUB引导到C语言内核的完整流程 简介面向刚开始接触操作系统实验的学生这份资源以QEMU与Multiboot协议为切入点完整演示如何在汇编中编写multiboot header直接写VGA显存输出“helloworld”借助串口输出“HELLOWORLD”并将Ubuntu与主机共享文件夹的建立、QEMU对Multiboot支持情况的确认、Makefile与链接描述文件的编写等准备环节一并整理进来。资源共10个文件、约778KB涵盖实验报告PDF、汇编源码S、链接脚本ld、Makefile、编译生成的bin与o文件以及VS Code调试所用的JSON配置按“源码—编译—镜像—加载验证”的流程组织便于边看边做也方便对照Makefile和ld理解构建细节。已有293人学习下载。除可直接运行的代码外资源还附有“实验一Multiboot启动”讲解PDF对链接描述文件、编译流程和串口/VGA输出的实现做了说明能帮助初学者理解二进制镜像如何被QEMU识别并启动。对想亲手走通OS Lab1、补齐汇编与链接加载基础的同学来说这是很好的对照起点。1. Multiboot 启动实验从“上电”到“跑起 C 代码”的关键一跳这套实验笔记是某高校操作系统课程 Lab1 的完整复现记录核心目标只有一个让一个用汇编写的最小内核在 QEMU 模拟的 x86 机器上被 GRUB 识别、加载、进入保护模式然后跳进 C 语言写的kernel_main。刚接触 OS 的人往往卡在同一个地方代码逻辑没问题但编译出来的二进制要么被 GRUB 拒绝要么在 QEMU 里黑屏重启。问题不在 C 代码而在 Multiboot 头、链接脚本和启动流程这三样配套工程上。适合正做到 Lab1、想看完整可运行工程的人也适合想把启动环节彻底搞懂的初学者。2. 搭建最小实验环境三个包、一个 Makefile、一条启动命令2.1 工具链选型为什么是 QEMU GRUB GCC 交叉编译实验在 Ubuntu 主机上做不需要买开发板不需要真机引导QEMU 一个软件就解决了“模拟一台 x86 机器”的需求。GRUB 承担引导器角色它按 Multiboot 规范扫描内核镜像找到头部魔数后把控制权交给_start。这两者是配对的没有 GRUB 的 Multiboot 头裸奔的_start只能靠 BIOS 中断自己加载复杂度会高一个量级。GCC 带-m32编译出 32 位代码即可不需要额外装交叉编译器。在 64 位 Ubuntu 上默认 GCC 输出 64 位目标不加-m32的话后面链接脚本、汇编入口全部要跟着改所以这里统一用-m32是最省事的选择。2.2 环境安装与版本确认包不多核心是 QEMU、GRUB 工具和文件系统制作工具sudo apt update sudo apt install -y qemu-system-x86 grub-pc-bin grub-common xorriso build-essentialgrub-pc-bin提供grub-mkrescue需要的平台模块xorriso负责把目录制作成可引导 ISO。grub-common提供grub-mkrescue本体。这三个漏装任何一个都会在生成 ISO 那一步报错后面第 5 章会专门排查。安装完先确认版本qemu-system-i386 --version | head -1 grub-mkrescue --version | head -1 gcc -m32 -v 21 | tail -1qemu-system-i386和qemu-system-x86_64是不同二进制实验要跑 32 位内核用前者。gcc 那行如果出现Target: x86_64-linux-gnu是正常的只要-m32能编过、ld -m elf_i386能链上即可不一定需要gcc-multilib但如果报缺少 32 位头文件再补装gcc-multilib。2.3 工程目录与 Makefile 骨架整个实验的工程结构很小建议按源码、构建产物、ISO 三类分开lab1/ ├── src/ │ ├── boot.S │ └── kernel.c ├── link.ld ├── Makefile └── iso/ # 构建时自动生成Makefile 直接照抄这份能用注意每行命令前的缩进必须是 Tab不是空格CC gcc LD ld CFLAGS -m32 -Wall -Wextra -fno-pic -fno-builtin \ -ffreestanding -fno-stack-protector -O2 -g LDFLAGS -m elf_i386 -T link.ld -nostdlib OBJS boot.o kernel.o all: os.iso boot.o: src/boot.S $(CC) $(CFLAGS) -c $ -o $ kernel.o: src/kernel.c $(CC) $(CFLAGS) -c $ -o $ kernel.bin: $(OBJS) $(LD) $(LDFLAGS) -o $ $^ os.iso: kernel.bin rm -rf iso mkdir -p iso/boot/grub cp kernel.bin iso/boot/kernel.bin printf set timeout2\nmenuentry lab1 os { multiboot /boot/kernel.bin; boot }\n iso/boot/grub/grub.cfg grub-mkrescue -o os.iso iso run: os.iso qemu-system-i386 -cdrom os.iso clean: rm -f *.o kernel.bin os.iso rm -rf iso .PHONY: all run clean-fno-pic是为了避免生成位置无关代码-fno-builtin和-ffreestanding是告诉编译器这是裸机代码不依赖 libc。-fno-stack-protector去掉栈保护否则编译器会在kernel_main里插入__stack_chk_fail的外部引用链接时报找不到符号。3. Multiboot 头与汇编引导GRUB 到底在找什么3.1 Multiboot 规范魔数、标志与校验和的契约GRUB 启动内核前会扫描文件前 8192 字节寻找一个 16 字节对齐的 Multiboot 头。这个头不是普通数据是内核和引导器之间的“契约”。头部结构只有三个 4 字节字段字段偏移值含义magic0x000x1BADB002固定魔数引导器靠它识别flags0x040x3bit0 要求页对齐bit1 要求内存信息checksum0x08-(0x1BADB002 0x3)三者相加必须为 0做完整性校验flags的 bit0 是让 GRUB 把所有段按页对齐到 4K 边界省得内核自己处理不对齐情况bit1 是让 GRUB 收集可用内存信息启动后通过ebx寄存器传给内核这个是 Lab1 后续内存管理实验的基础。如果checksum算错GRUB 会直接报invalid magic number头白写了。3.2 入口代码设置栈、关中断、调用 C 函数用 GAS 语法写src/boot.S注意是 ATT 风格.set ALIGN, 1 0 .set MEMINFO, 1 1 .set FLAGS, ALIGN | MEMINFO .set MAGIC, 0x1BADB002 .set CHECKSUM, -(MAGIC FLAGS) .section .multiboot .align 4 .long MAGIC .long FLAGS .long CHECKSUM .section .bss .align 16 stack_bottom: .skip 16384 stack_top: .section .text .global _start .type _start, function _start: movl $stack_top, %esp pushl %ebx call kernel_main addl $4, %esp 1: hlt jmp 1b .size _start, . - _start第一段.set计算出三份头部常量的具体数值写完系数后如果要调整flags不需要改魔数那行只改FLAGS定义即可。.section .multiboot单独开一个段放头部是为了配合链接脚本把它放到镜像最前面第 4 章会看到这个段名有多重要。进入_start后第一件事是设置栈顶。GRUB 跳进来的时候虽然已经在 32 位保护模式下但栈指针是未定义的不设栈就调用 C 函数任何压栈操作都会写进未知内存轻则乱跳重则三重故障重启。.skip 16384分配了 16KB 栈空间栈从高地址往低地址生长所以esp指向stack_top而不是stack_bottom。pushl %ebx是把 GRUB 传进来的 multiboot_info 结构体地址作为参数压栈传给kernel_main。cdecl 调用约定下参数从右往左压栈调用完由调用方清理所以后面有addl $4, %esp。最后hlt配合跳回标签1b形成死循环防止 C 函数返回后执行到未定义区域。3.3 从汇编到 CABI 与调用约定的边界C 侧入口函数写得很简单但要注意函数签名必须和汇编侧压栈的参数数量对得上#include stdint.h typedef uint32_t u32; void kernel_main(u32 mb_info_addr) { volatile char *vga (volatile char *)0xB8000; const char *msg boot ok; while (*msg) { *vga *msg; *vga 0x0f; } for (;;) { __asm__ volatile(hlt); } }mb_info_addr就是汇编里pushl %ebx传进来的那个地址这里先不读它只占参数位置。VGA 文本模式从0xB8000开始每两个字节表示屏幕上的一个字符低字节是 ASCII 码高字节是属性0x0f表示黑底白字不闪烁。这里最容易踩的坑是符号名的下划线问题。GAS 和 GCC 在 Linux 上不会给符号自动加下划线前缀所以汇编里的kernel_main必须和 C 里的函数名完全一致。如果哪一侧多写或少写了下划线链接期会报 undefined reference。4. 链接脚本与 ISO 镜像布局错了就只能对着黑屏发呆4.1 链接脚本把入口固定在 1MB 处link.ld是整个 Lab1 里最容易被忽略、但最决定生死的一份文件ENTRY(_start) SECTIONS { . 1M; .text : ALIGN(4K) { *(.multiboot) *(.text*) } .rodata : ALIGN(4K) { *(.rodata*) } .data : ALIGN(4K) { *(.data*) } .bss : ALIGN(4K) { *(COMMON) *(.bss*) } }. 1M是把虚拟地址起点设为 1MB。x86 在实模式下低端 1MB 内存被 BIOS、VGA 显存、中断向量表占用操作系统内核一般从 1MB 往上放。GRUB 加载 ELF 时会读取程序头把各段加载到链接脚本指定的地址然后跳转到ENTRY指定的_start。关键在.text段里先放*(.multiboot)再放*(.text*)。这样 Multiboot 头会被放在整个镜像的第一个可加载段的最前面GRUB 扫描时最先碰到它。如果顺序反了头部被埋进代码中间虽然理论上 GRUB 会扫描前 8KB但经常出现扫描到代码中间才找到头的情况能跑但不稳定。4.2 生成可引导 ISOgrub-mkrescue 的目录约定ISO 不是简单地把 kernel.bin 扔进去就完事GRUB 要能从光盘引导必须有boot/grub/grub.cfg和内核文件。Makefile 里已经生成了这份配置内容可以拆开看mkdir -p iso/boot/grub cp kernel.bin iso/boot/kernel.bin cat iso/boot/grub/grub.cfg EOF set timeout2 set default0 menuentry lab1 os { multiboot /boot/kernel.bin boot } EOF grub-mkrescue -o os.iso isogrub-mkrescue会把iso/目录包装成一个符合 El Torito 规范的可引导光盘镜像同时自动嵌入 GRUB 引导代码和核心模块。menuentry 里的multiboot命令是 Multiboot 1 协议的加载指令它会解析 ELF 格式的 kernel.bin按程序头加载段然后跳转。注意不要写成linux命令那套协议针对 Linux 内核加载方式完全不是一回事。4.3 QEMU 启动参数与两种加载方式的差异启动命令就一条qemu-system-i386 -cdrom os.iso-cdrom让 QEMU 从光驱镜像引导模拟 BIOS 读光盘、执行 GRUB、再走 Multiboot 流程。另一个常见做法是qemu-system-i386 -kernel kernel.bin但这个参数在 QEMU 里走的是 Linux 内核启动协议不是 Multiboot你的头部根本不会被识别结果是 QEMU 直接放弃引导。所以 Lab1 阶段老老实实用-cdrom最稳。如果加了-serial stdio或者-d int -D log.txtQEMU 会把 CPU 中断和异常日志写到文件里后面排查重启循环时特别有用这个放到第 6 章展开。5. 常见问题排查启动失败的五个真实场景5.1 GRUB 报 invalid magic number现象QEMU 启动后 GRUB 菜单正常但选中菜单回车屏幕闪一下又回到菜单或直接报error: invalid magic number。原因Multiboot 头没有被放进镜像的可加载段前部。最常见的是链接脚本里没写*(.multiboot)或者段名拼错成.multiboot2、.multiboot_header导致头部被丢弃整个镜像里根本找不到魔数。解决先看链接产物里的段分布objdump -h kernel.bin readelf -l kernel.binobjdump -h的输出里应该能看到.multiboot段且地址在 0x100000 附近。如果看不到这个段回到链接脚本检查通配符写没写对。readelf -l看程序头确保第一个 LOAD 段的偏移覆盖到.multiboot的位置。5.2 选择启动项后机器不断重启现象GRUB 菜单能选选完黑屏一瞬间QEMU 整个虚拟机重启反复循环。有时候是屏幕一闪而过完全看不清。原因这是 CPU 三重故障triple fault。内核_start执行时触发了无法恢复的异常CPU 直接复位。最常见是栈没设对movl $stack_top, %esp写成了mov stack_top, %esp变成把栈顶地址处的值加载进 espesp 指向一个垃圾地址pushl %ebx一压就崩。解决先确认_start头三条指令逐字对照。另一个隐藏原因.bss段的stack_top没有定义全局符号导致汇编里引用到 0。在stack_top:后面加一行.global stack_top可以排除这种问题。还不行就开 QEMU 日志qemu-system-i386 -cdrom os.iso -d int -D qemu.log打开qemu.log搜check_exception或triple fault能定位到具体是哪个异常触发了复位。5.3 用 -kernel 参数启动后黑屏无反应现象不用 ISO改成qemu-system-i386 -kernel kernel.bin启动后黑屏GRUB 菜单都没出现。原因-kernel参数走的是 Linux 启动协议QEMU 会按 Linux 的 boot protocol 解析这个文件它根本不知道 Multiboot 头是什么。这个参数在 Linux 内核调试里很常用但用在 Multiboot 内核上就是完全错误的使用方式。解决换回-cdrom os.iso让 GRUB 走完整引导流程。如果只是调试汇编入口不想每次重新生成 ISO可以用qemu-system-i386 -kernel配合 Multiboot 的变体qemu-system-i386 -kernel之外还有一个-bios参数但同样不是给 Multiboot 用的。不要在这个参数上花时间。5.4 链接报错 undefined reference to_start现象ld阶段报_start未定义或者报kernel_main未定义。原因ENTRY(_start)指定了入口符号但链接时符号表里找不到它。两种典型情况boot.S里写的是.global _start没错但文件没参与编译或者kernel.c的函数名写的是kernel_main_、kmain这类变体汇编侧却 call 的是kernel_main。解决整理构建依赖顺序先单独编汇编再编 C 最后链接确认boot.o在链接输入里。符号名用nm boot.o和nm kernel.o分别查两边必须精确一致。Linux 下没有下划线前缀不用怀疑是 macOS 那种_main问题。5.5 grub-mkrescue 报 xorriso 不存在现象make os.iso时提示grub-mkrescue: error: cannot find xorriso或者grub-mkrescue: command not found。原因grub-mkrescue依赖xorriso来制作 ISO而xorriso在 Ubuntu 上不是 grub 包的默认依赖经常需要手动安装。另一个情况是只装了grub-common没装grub-pc-bin导致grub-mkrescue找不到平台模块。解决sudo apt install -y xorriso grub-pc-bin grub-common装完后重新make clean make即可。这条看着蠢但实际翻车率极高尤其是从精简版、容器镜像里做实验的系统。6. 把启动流程验证到极简VGA 打印与内存布局检查VGA 打印是验证启动是否真正走通的最直接手段。在第 4 章的kernel_main里已经写了最简单的打印但这版打印串是固定的没有检查 GRUB 传进来的内存信息。进阶一点的验证是读 multiboot_info 结构体把低端内存大小打印出来能确认 GRUB 的加载确实是按 Multiboot 规范进行的。Multiboot 1 规范里eax 寄存器进入_start后保存的是魔术值0x2BADB002ebx 保存的是 multiboot_info 结构体地址。结构体第一个成员是 flags如果 bit0 置位偏移 4 处就是mem_lower单位 KB偏移 8 处是mem_upper。写一段读内存的代码struct multiboot_info { u32 flags; u32 mem_lower; u32 mem_upper; /* 后面的字段这里用不到 */ } __attribute__((packed)); void kernel_main(u32 mb_info_addr) { volatile struct multiboot_info *mb (volatile struct multiboot_info *)mb_info_addr; char buf[16]; volatile char *vga (volatile char *)0xB8000; int i 0; while (mb info ok[i]) { *vga mb info ok[i]; *vga 0x0a; /* 绿字黑底 */ i; } if (mb-flags (1 0)) { /* mem_lower 单位是 KB打印十六进制 */ u32 val mb-mem_lower; const char hex[] 0123456789abcdef; i 4; buf[4] \n; while (i 0) { i--; buf[i] hex[val 0xF]; val 4; } for (i 0; i 5; i) { *vga buf[i]; *vga 0x0a; } } for (;;) { __asm__ volatile(hlt); } }运行make run如果屏幕第一行显示mb info ok第二行显示内存大小的十六进制值就说明从汇编到 C、从 GRUB 传参到数据读取整条链路都是通的。这套验证做完Lab1 的启动部分就算彻底闭环了。从那以后我每次搭启动实验都强制自己先写一个最小可打印的 kernel_main再做任何功能开发。先让屏幕出现字符再谈内存管理、中断、进程切换否则后面所有调试都会叠加在“启动到底通没通”这个未验证假设上越查越乱。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/16 9:37:40

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

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

2026/10/16 9:37:40

Azkaban安装部署快速入门:从环境准备到生产模式实战与排坑

一直想把手上的数据处理任务梳理成正式的调度体系,但又不想在集群上引入太重的框架,这两年开始很多团队把目光投向Azkaban。它轻量、部署简单、通过Web界面拖拽依赖就能把任务串起来,在中小规模数据团队里确实非常好用。这篇文章是快速入门系…

2026/10/16 9:32:33

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

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

2026/10/16 11:04:41

BUUCTF PWN进阶:格式化字符串、栈迁移与ret2csu实战解析

刷 BUUCTF 的 PWN 题,从 41 到 45 这几道题给我的感觉是:前 40 题还在练手速,栈溢出让你把ret地址改了基本就能拿 shell;到了这个阶段,题目开始正经考“你懂不懂底层到底发生了什么”。格式化字符串怎么泄露地址、栈空…

2026/10/16 11:04:41

CLion源码编译OpenCV:构建可调试可追踪的开发闭环

1. 这不是“配环境”,而是构建一个可调试、可追踪、可复现的OpenCV开发闭环很多人点开这篇标题,第一反应是:“哦,又一个CLion配OpenCV的教程”。但我要先泼一盆冷水:如果你只是想把cv::imread跑起来,那大可…

2026/10/16 11:04:41

操作系统隔离与系统调用全链路:从用户态到内核态的穿越

6.S081这门操作系统课程讲到第三讲,终于开始触及最核心的部分:OS organization和system calls。前面几讲我们在用户态这边打转,fork、exec、wait这些系统调用用得很熟,但很少认真想过一个问题——你随手调一个write(),…

2026/10/16 11:04:41

多智能体协作新范式:DeepSeek Harness集成Codex与Claude Code实战

一眼看到这次更新的标题,我先是愣了一下——把 Codex、Claude Code 直接做成官方插件,这不再是"自己改造工具链"的玩法,而是把多个编程代理放进同一个调度框架里当子智能体用。我前阵子刚把一批并行任务从单代理切到多代理协作&…

2026/10/16 11:04:41

Netty带宽饱和下的连接处理:背压、水线与实战调优

刚经历了一次让人印象深刻的线上事故:某推送网关在业务高峰期整体带宽吃到接近上限,单个连接的数据还能正常处理,但新连接进来之后要么握手阶段超时,要么连进来以后迟迟拿不到响应。更诡异的是,已经建立的长连接里&…

2026/10/16 10:54:16

ConvLSTM实战:伦敦空气质量时空预测全流程解析

1. 问题定义与模型选型:为什么偏偏是ConvLSTM上一篇我们完成了伦敦地区空气质量数据的获取和预处理。拿到了20多个监测站点的逐小时污染物浓度记录,做了缺失值插补,也看了基本的时间分布规律。但数据准备好了并不代表能直接进入训练&#xff…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/16 0:01:11

Cephadm实战指南:Linux系统工程师的生产级Ceph集群部署与排障

简介:本资源是一份完整的Ceph分布式存储系统中文技术文档合集,面向Linux系统运维工程师、云平台架构师及存储方向开发者,帮助读者系统掌握Ceph集群的部署、配置与运维核心能力。文档覆盖对象存储(RGW)、块设备&#xf…

2026/10/16 0:01:11

ITK中SpatialObject几何体系解析:坐标系、变换与管道集成

1. 空间对象的定位:为什么 ITK 需要一套几何描述体系很多刚开始接触 ITK 的朋友,包括我自己,最初都是被它的图像滤波、配准、分割这些“正统”功能吸引进来的。等真正上手做点实际项目,比如把分割出来的器官边界做量化分析、给血管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑