尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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再做任何功能开发。先让屏幕出现字符再谈内存管理、中断、进程切换否则后面所有调试都会叠加在“启动到底通没通”这个未验证假设上越查越乱。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

曲面细分与简化实战: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
BrowserSkill:基于Chrome DevTools协议的命令行浏览器自动化框架

BrowserSkill:基于Chrome DevTools协议的命令行浏览器自动化框架

1. 项目概述:BrowserSkill到底是什么,它解决的不是“浏览器插件”这个表层问题 BrowserSkill——这个名字乍一听像某个新出的Chrome插件,或者某个AI工具的副产品。但如果你真去翻过它的GitHub仓库、CLI命令手册,甚至亲手跑过几条…

📅 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

本月热门

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

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

📞 💬