简介:本资源是中科大操作系统课程实验一的完整实现包,面向刚接触操作系统开发的初学者,聚焦Multiboot协议启动机制的实践入门。内容涵盖Multiboot Header编写、VGA显存直写“helloworld”、串口输出“HELLOWORLD”三大核心功能,并配套Ubuntu下QEMU环境搭建、共享文件夹配置、Makefile与链接脚本(.ld)使用等关键操作,帮助学习者打通从汇编编码、链接到模拟运行的全链路。压缩包共10个文件,含汇编源码(.S)、可重定位目标文件(.o)、二进制镜像(.bin)、链接描述文件(.ld)、Makefile构建脚本及两份PDF实验讲解文档,类型分布合理,便于理解编译-链接-加载流程;整体大小778KB,轻量易下载。已有293人学习下载,提供从协议解读、代码实现到调试验证的闭环参考,特别适合OS新手建立底层启动认知、积累实操经验。
1. 这不是“Hello World”,而是一次真实的内核启动:用 QEMU 模拟 x86 实模式,亲手把一段汇编+C代码喂给 Multiboot 兼容引导器,看到OK打印在黑底白字的实模式屏幕上——适合刚啃完《Orange'S》前两章、手头只有 Ubuntu 22.04、连nasm -f elf32都报错三次的 OS 新手
你不需要懂 GRUB 内部调度,也不用编译整个 Linux 内核;这个项目只做一件事:在 Ubuntu 上用 QEMU 搭起一个最小可启动环境,让一段自己写的 boot.S(汇编) + kernel.c(C)组合体,通过 Multiboot 规范被加载进内存、完成段寄存器初始化、跳转到 C 入口、最后在实模式 VGA 文本缓冲区(0xB8000)上打印两个字符。它不带中断、不接管键盘、不初始化堆栈帧——但只要你看到OK稳稳出现在 QEMU 窗口左上角,你就完成了操作系统开发里最硬核的第一关:控制权从固件真正移交到你写的代码。项目结构极简(仅 5 个文件),所有工具链全靠apt install解决,没有 Makefile 嵌套陷阱,没有交叉编译器版本对齐玄学,也没有任何外部依赖。如果你正卡在“为什么jmp $不停重启”或“为什么mov word [0xB8000], 0x074F没反应”,那这份 lab1 就是为你写的血泪调试笔记。
2. 环境准备与工具链搭建:Ubuntu 22.04 下零配置启动 QEMU+Multiboot 工作流
2.1 必装工具清单:nasm、gcc、qemu-system-x86_64、binutils-gold(关键!)
这个项目表面看只用汇编和 C,但背后有三处隐性依赖:一是 NASM 必须支持-f elf32输出(Ubuntu 默认源里的nasm满足);二是 GCC 编译内核 C 代码时必须生成 32 位 flat binary(不能用默认的 64 位x86_64-linux-gnu-gcc);三是链接阶段必须用ld的--oformat binary模式,且必须指定--script脚本控制入口和段布局——而默认ld(BFD 版本)在处理 Multiboot header 时容易把.multiboot_header段丢掉,导致 QEMU 报multiboot: no multiboot header found。解决方案是强制使用binutils-gold(更快更严格),并配专用链接脚本。
sudo apt update sudo apt install -y nasm gcc-11-multilib qemu-system-x86 qemu-utils # 安装 gold linker(关键!) sudo apt install -y binutils-gold # 验证:ld --version 应含 "gold" 字样 ld --version | grep gold提示:不要用
gcc-multilib代替gcc-11-multilib—— Ubuntu 22.04 中前者默认指向 gcc-11,但显式指定版本可避免未来系统升级导致的 ABI 不一致。qemu-utils提供qemu-img,虽本项目不用,但后续扩展磁盘镜像时会省去重装步骤。
2.2 目录结构与文件职责:5 个文件,各司其职,无冗余
项目根目录下只有以下 5 个文件,全部手写,无自动生成:
. ├── boot.S # 实模式入口:设置段寄存器、加载 Multiboot header、跳转到 kernel_main ├── kernel.c # C 语言内核主体:初始化 VGA 缓冲区、打印 "OK" ├── linker.ld # GNU ld 链接脚本:定义入口地址(0x100000)、.text/.data/.bss 段位置、保留 .multiboot_header 段 ├── Makefile # 四行命令:汇编 boot.S → 编译 kernel.c → 链接 → 启动 QEMU └── run.sh # 一行快捷脚本:make && qemu-system-x86_64 -kernel kernel.bin -monitor stdio注意:kernel.bin是最终输出,不是源码;-kernel参数是 QEMU 对 Multiboot 镜像的原生支持方式,不经过 GRUB 安装或 ISO 封装,直接加载二进制并校验 Multiboot header。
2.3 Multiboot Header 的硬编码细节:为什么必须放在 boot.S 开头且对齐 8 字节?
Multiboot 规范(v1)要求 header 必须位于镜像前 8KB 内,且起始地址必须是 8 字节对齐。header 本身是 12 字节固定结构,但为兼容旧版 GRUB,通常扩展为 16 字节(补 4 字节 magic)。本项目boot.S开头如下:
; boot.S 第 1–16 行:Multiboot header(必须紧贴文件开头!) .align 4 multiboot_header: .long 0x1BADB002 ; magic: valid multiboot .long 0x00000000 ; flags: no flags set .long -(0x1BADB002 + 0x00000000) ; checksum: must be 0 mod 2^32 .fill 4, 1, 0 ; padding to 16 bytes total关键点:
.align 4→ 强制 4 字节对齐(即 16 进制地址末位为 0/4/8/C),满足 8 字节对齐要求(因为 4 字节对齐是 8 字节对齐的子集);checksum计算必须为负数:(magic + flags)的 32 位补码,使三者相加为 0;- header 必须是文件第一个符号,NASM 默认按源码顺序排布段,所以
multiboot_header:必须写在boot.S最顶部,不能包在.section .text里(否则可能被重排)。
若 header 位置错误,QEMU 启动时会静默失败(黑屏无提示),或报multiboot: no multiboot header found—— 这是新手第一大翻车点。
2.4 链接脚本 linker.ld:控制 0x100000 加载地址与段合并逻辑
x86 实模式下,Multiboot 引导器默认将内核加载到物理地址0x100000(1MB),这是传统保护模式切换的安全起点(避开低地址中断向量表)。linker.ld必须精确声明此地址,并确保.multiboot_header段不被优化掉:
/* linker.ld */ ENTRY(multiboot_header) /* 入口符号必须是 header 地址,不是 kernel_main! */ SECTIONS { . = 0x100000; /* 加载地址:所有段从此开始映射 */ .text ALIGN(4K) : { *(.multiboot_header) /* 关键:显式保留 header 段,放最前 */ *(.text) } .data ALIGN(4K) : { *(.data) *(.rodata) } .bss ALIGN(4K) : { *(.bss) . = . + 0x1000; /* 预留 4KB bss 空间,避免未初始化变量越界 */ } /DISCARD/ : { *(.comment) *(.note.*) } /* 丢弃调试信息,减小 bin 体积 */ }注意:
ENTRY(multiboot_header)是硬性要求:QEMU 的 Multiboot 加载器会从该符号地址开始执行,而不是从_start或kernel_main;*(.multiboot_header)必须放在.text段最前面,否则链接器可能将其优化到末尾,导致 header 不在前 8KB;. = . + 0x1000是安全冗余:.bss在实模式下不自动清零,手动预留空间可避免kernel.c中全局变量覆盖 VGA 缓冲区。
3. 核心模块拆解:boot.S 与 kernel.c 的逐行执行链路
3.1 boot.S:从 reset 向量到 C 函数调用的完整控制流
x86 上电后 CPU 处于实模式,CS:IP = 0xF000:0xFFF0(ROM BIOS 地址),BIOS 自检后跳转到 0x7C00(MBR),而本项目由 QEMU 的-kernel参数绕过 MBR,直接将kernel.bin加载到0x100000并跳转到multiboot_header。此时 CS=0x1000,IP=0x0000(因 0x100000 / 16 = 0x10000,段基址取高 16 位)。boot.S接下来要做三件事:重置段寄存器、建立栈、跳转 C 入口。
; boot.S 续写(header 之后) .code32 cli ; 关中断:实模式下禁用,避免 IRQ 打断初始化 xor %ax, %ax ; 清零 ax mov %ax, %ds ; 数据段 → 0 mov %ax, %es ; 附加段 → 0 mov %ax, %fs ; fs/gs → 0(实模式下无用,但清零防脏数据) mov %ax, %gs mov %ax, %ss ; 栈段 → 0 ; 设置栈顶:实模式栈向下增长,选 0x90000(640KB + 64KB,避开 BIOS 数据区) mov $0x90000, %sp ; 跳转到 C 函数 kernel_main(注意:此处无 call,因无栈帧需求) jmp kernel_main逻辑说明:
.code32告诉 NASM 生成 32 位指令(尽管仍在实模式,但允许使用 32 位寄存器如%esp);cli是必须的:实模式下 0x20 端口(PIC)默认开启,若此时触发 IRQ,CPU 会尝试读取中断向量表(0x0000:0x0000),而该地址未初始化,导致不可预测行为;- 栈地址选
0x90000是经验值:低于0x100000(内核加载地址),高于0x7E00(传统 MBR 结束位置),避开 BIOS 保留区(0xA0000–0xFFFFF); jmp kernel_main是直接跳转,不压入返回地址——因为这是内核入口,永不返回。
3.2 kernel.c:在实模式 VGA 文本缓冲区写入字符的底层实现
kernel.c是纯 C,但必须用-m32 -ffreestanding -nostdlib编译(无标准库、无运行时初始化)。核心逻辑只有两行:计算 VGA 缓冲区地址、写入带属性的字符。
// kernel.c void kernel_main(void) { // VGA 文本缓冲区起始物理地址:0xB8000 // 每个字符占 2 字节:低字节=ASCII,高字节=属性(0x07=灰字黑底) volatile unsigned short* vga_buffer = (volatile unsigned short*)0xB8000; // 写入 'O'(0x4F) + 属性(0x07)→ 0x074F,位置 0(左上角第1字符) vga_buffer[0] = 0x074F; // 写入 'K'(0x4B) + 属性(0x07)→ 0x074B,位置 1(左上角第2字符) vga_buffer[1] = 0x074B; // 死循环:防止 CPU 执行到未初始化内存 for(;;); }参数说明:
volatile关键字:禁止编译器优化掉对vga_buffer的写入(否则可能被优化成寄存器操作,不写内存);unsigned short*:VGA 文本模式每个字符占 2 字节,必须用short(16 位)指针,不能用char*(否则只写 1 字节);0x074F拆解:低字节0x4F= ASCII 'O',高字节0x07= 属性字节(bit7=blink, bit6–4=bg, bit3–0=fg),0x07即 foreground=7(白色)、background=0(黑色)、blink=0;for(;;);是实模式必需:没有操作系统接管,CPU 执行完就继续取下一条指令,若后面是垃圾数据会崩溃。
3.3 Makefile:四行命令背后的编译器行为解析
Makefile 看似简单,但每行都针对实模式特殊约束:
# Makefile CC = gcc-11 -m32 -ffreestanding -nostdlib -nostartfiles -nodefaultlibs AS = nasm LD = ld.gold # 强制使用 gold linker kernel.bin: boot.o kernel.o $(LD) -T linker.ld -o $@ $^ boot.o: boot.S $(AS) -f elf32 -o $@ $< kernel.o: kernel.c $(CC) -c -o $@ $< run: kernel.bin qemu-system-x86_64 -kernel kernel.bin -monitor stdio .PHONY: clean run clean: rm -f *.o kernel.bin关键参数说明:
-m32:生成 32 位目标码(即使 host 是 64 位 Ubuntu);-ffreestanding:告知编译器不依赖标准库(无printf、malloc);-nostdlib -nostartfiles -nodefaultlibs:彻底禁用 crt0.o、libc.a 等启动文件,避免插入call __libc_start_main等无关代码;-f elf32:NASM 输出 ELF32 格式,供ld.gold链接(不能用-f bin,否则无法处理 Multiboot header 符号);ld.gold -T linker.ld:链接时严格按脚本布局,-T指定脚本,-o kernel.bin输出纯二进制(非 ELF)。
若漏掉任一 flag,常见现象是:kernel.bin体积异常大(含 libc 符号)、QEMU 启动后立即 triple fault、或vga_buffer写入无效。
4. 常见问题排查:5 条真实踩坑记录,每条对应一次凌晨三点的重启
4.1 现象:QEMU 窗口全黑,无任何输出,-monitor stdio里也无日志
原因:Multiboot header 未被识别,QEMU 未加载内核,直接进入空闲状态。常见于 header 位置错误(不在文件开头)、checksum 计算错误、或linker.ld未显式包含*(.multiboot_header)。
解决:用hexdump -C kernel.bin | head -n 20查看前 20 行,确认 offset 0x00 处为02 b0 ad 1b 00 00 00 00(magic+flags 小端序),且 offset 0x0c 处为00 00 00 00(checksum)。若不符,检查boot.S中.fill行数和checksum公式。
4.2 现象:QEMU 报错multiboot: no multiboot header found
原因:ld使用了 BFD 版本而非 gold,或linker.ld中ENTRY指向错误符号。BFD linker 在处理*(.multiboot_header)时可能将其合并到.text段末尾,导致 header 不在前 8KB。
解决:执行ld --version确认含gold;在linker.ld中添加ASSERT(SIZEOF(.multiboot_header) == 16, "multiboot header size mismatch"),并在 Makefile 中加$(LD) -T linker.ld --verbose kernel.o 2>&1 | grep multiboot查看段布局。
4.3 现象:屏幕显示乱码(如K或O),而非OK
原因:kernel.c中vga_buffer指针类型错误。若声明为char*,则vga_buffer[0] = 0x074F只写入低字节0x4F,高字节0x07被丢弃,导致属性丢失,字符显示为默认颜色(常为黑底黑字,不可见)。
解决:严格使用unsigned short*,并在编译时加-Wall,GCC 会对char*指针赋值short值发出 warning。
4.4 现象:make报错undefined reference to 'kernel_main'
原因:boot.S中jmp kernel_main的符号名与kernel.c中函数名大小写不一致,或kernel.c未编译(kernel.o不存在)。NASM 默认导出符号为小写,而 C 编译器对kernel_main无修饰,但若kernel.c中写成void Kernel_Main()则符号为Kernel_Main,大小写不匹配。
解决:统一用小写kernel_main;用nm kernel.o | grep main确认符号存在;检查kernel.c文件名是否拼错(如kernle.c)。
4.5 现象:QEMU 启动后立即蓝屏(triple fault),窗口关闭
原因:实模式下执行了非法指令,最常见是kernel.c中用了 64 位寄存器(如%rax)或未声明volatile导致vga_buffer写入被优化掉,CPU 取指时读到全零内存,解码为非法指令。
解决:在kernel.c中所有硬件访问指针前加volatile;用objdump -d kernel.bin反汇编,确认kernel_main函数内无movq、pushq等 q 后缀指令(应为movl、pushl);检查gcc-11 -m32是否生效(gcc-11 -m32 -dM -E - < /dev/null | grep __i686应输出#define __i686 1)。
5. 二次开发实战:从打印 "OK" 到实现简易字符串输出与内存探测
5.1 扩展 VGA 输出:封装vga_write_string()支持任意字符串
kernel.c当前只能硬编码打印两个字符。要支持字符串,需实现循环写入 + 位置偏移。实模式下无strlen,需以\0结尾:
// kernel.c 新增函数 void vga_write_char(char c, int pos) { volatile unsigned short* vga = (volatile unsigned short*)0xB8000; vga[pos] = (0x07 << 8) | (unsigned char)c; // 属性<<8 | ASCII } void vga_write_string(const char* str) { int i = 0; while (str[i] != '\0') { vga_write_char(str[i], i); i++; } } void kernel_main(void) { vga_write_string("USTC OS LAB1"); for(;;); }注意:const char* str在实模式下地址有效,因kernel.bin加载到0x100000,字符串字面量存于.rodata段,也在该镜像内;i作为局部变量存于栈(0x90000),安全。
5.2 探测可用内存:解析 Multiboot Info Structure 获取内存布局
Multiboot 规范要求引导器在启动时将内存信息存入multiboot_info_t结构(物理地址由eax传入kernel_main)。扩展kernel_main参数即可获取:
// kernel.c 修改 struct multiboot_info { uint32_t flags; uint32_t mem_lower; // 低位内存 KB(通常 640KB) uint32_t mem_upper; // 高位内存 KB(通常 ~3840KB) // ... 其他字段略 }; void kernel_main(unsigned int mb_info_addr) { struct multiboot_info* mb_info = (struct multiboot_info*)mb_info_addr; if (mb_info->flags & 0x00000001) { // 检查 flags bit 0(memory info valid) char buf[32]; // 简易 itoa:将 mem_upper 转为字符串 int val = mb_info->mem_upper; int i = 0; do { buf[i++] = '0' + (val % 10); val /= 10; } while (val); buf[i] = '\0'; // 反转字符串 for (int j = 0; j < i/2; j++) { char t = buf[j]; buf[j] = buf[i-1-j]; buf[i-1-j] = t; } vga_write_string("MEM UPPER: "); vga_write_string(buf); vga_write_string(" KB"); } for(;;); }关键点:
kernel_main签名改为void kernel_main(unsigned int mb_info_addr),Multiboot 引导器自动将 info structure 地址放入eax并跳转;mb_info_addr是物理地址,无需转换(实模式下段地址=物理地址);itoa手写避免依赖sprintf(无 libc)。
5.3 构建可复用的内核骨架:分离 boot 与 kernel 的编译单元
当前boot.S和kernel.c紧耦合。生产级 OS 会拆分为bootloader(独立项目)和kernel(本项目)。本项目可模拟此结构:将boot.S编译为boot.o,kernel.c编译为kernel.o,再用ld链接。这正是当前 Makefile 所做,但可进一步抽象:
# Makefile 增强版:支持多 kernel 版本 KERNEL_OBJS = kernel.o utils.o # 添加工具函数 boot.o: boot.S $(AS) -f elf32 -o $@ $< kernel.o: kernel.c $(CC) -c -o $@ $< utils.o: utils.c $(CC) -c -o $@ $< kernel.bin: $(KERNEL_OBJS) boot.o $(LD) -T linker.ld -o $@ boot.o $(KERNEL_OBJS)utils.c可存放itoa、memset等基础函数,避免重复造轮子。这种分层让boot.S专注引导,kernel.c专注业务,符合真实 OS 工程实践。
从那以后我每次新建 OS 实验,都会先跑通这个OK版本,再逐行替换功能——不是为了炫技,而是因为0xB8000上那个O字符,是我在裸机世界里刻下的第一个可信锚点。它不依赖任何库、不经过任何中间层,纯粹由我的汇编指令驱动 CPU 写入显存。当它稳定亮起,我就知道:这一行行代码,真的在物理世界里发生了作用。希望帮到你。
本文还有配套的精品资源,点击获取