简介本资源为西北工业大学计算机学院计算机操作系统实验报告一主题为创建GeekOS内核线程面向正在学习操作系统课程、需要完成GeekOS实验的高校学生。报告完整记录了从编写引导扇区代码、在虚拟机中运行到创建键盘响应线程、多线程字符串输出再到自定义大小爱心交替打印的全过程并附有实验分析涵盖第一个线程初始化为何不调用Setup_Kernel_Thread、内核线程结构体三组指针的作用等关键问题。资源包内含1个docx文档约699KB内容详尽包含完整操作流程与相关代码便于读者对照复现实验、理解GeekOS多任务调度与中断管理机制。目前已有1516人学习下载适合需要参考实验报告结构、排错思路与代码实现的同学使用。1. 创建 GeekOS从零搭起一个能跑的操作系统实验环境很多人第一次听到「创建 GeekOS」会以为是要从零写一个操作系统内核其实不是。GeekOS 是一套教学用的微型操作系统骨架配套的操作系统实验通常要求你先把它在本地编译、链接、跑起来再在它的框架里改调度、改内存管理、改文件系统。真正卡住大多数人的不是后面那些算法而是第一步——环境搭不起来make一跑满屏报错模拟器黑屏或者 GRUB 引导镜像根本生成不了。这篇笔记就按一线实操的顺序把「创建 GeekOS」这件事拆成能照着复现的步骤先讲清它是什么、依赖哪些工具链再落到具体的编译命令、镜像生成、启动验证最后把几个高频翻车点单独拎出来讲。适合正在做操作系统实验、需要把 GeekOS 跑通再动手改内核的同学也适合想拿它当练手项目、熟悉 x86 保护模式和内核构建流程的开发者。GeekOS 的定位很明确它是一个运行在 x86 上的教学内核通常配合 Bochs 或 QEMU 这类模拟器启动源码里已经包含了引导扇区、保护模式切换、基本的中断处理和简单的进程调度框架。你要做的「创建」本质上是把这份骨架用正确的工具链编译成可引导的软盘镜像或硬盘镜像然后在模拟器里看到它打印出自己的启动信息。它解决的核心问题是让你在一个可控、可调试的环境里真正碰到内核代码而不是停留在课本上的概念。所以这一章之后我会把重点放在工具链版本、编译顺序、镜像制作和启动参数上这些才是决定你能不能跑通的关键。2. 工具链与依赖为什么你的 gcc 版本会直接决定成败GeekOS 这类老派教学内核对工具链的敏感程度远超一般应用项目。它通常要求 32 位编译目标、特定的链接脚本、以及能生成原始二进制或 ELF 的汇编器。你如果直接拿系统自带的 64 位 gcc 去编十有八九会在链接阶段报「incompatible」或者生成一个根本没法引导的镜像。所以这一章先把依赖讲透再给安装和验证命令。2.1 编译 GeekOS 到底需要哪些组件常见做法是准备这几样东西一个能输出 32 位代码的 GCC 交叉编译器或带-m32支持的本机 gcc、GNU binutils 里的as和ld、make构建工具、nasm汇编器部分版本用 ATT 语法则不需要、以及 Bochs 或 QEMU 作为运行环境。另外还需要mtools或dd来制作软盘镜像因为 GeekOS 默认的引导方式往往是从软盘镜像启动。这里有个容易被忽略的点不是所有发行版都默认装了 32 位运行库。你在 64 位系统上编译 32 位程序需要gcc-multilib和对应的libc6-dev-i386。如果缺了编译阶段就会报找不到gnu/stubs-32.h。我一般会先跑一条检查命令确认工具链齐不齐再动手改 Makefile。# 检查关键工具是否存在及版本 gcc --version ld --version make --version nasm -v 2/dev/null || echo nasm 未安装若项目用 ATT 语法可忽略 qemu-system-i386 --version 2/dev/null || echo QEMU 未安装 # 检查是否支持 32 位编译 echo int main(){return 0;} /tmp/t.c gcc -m32 /tmp/t.c -o /tmp/t echo 32位编译 OK || echo 缺少 multilib这段脚本的作用是快速定位环境缺口。gcc -m32那一步最关键它直接决定你能不能生成 32 位目标文件。如果这里失败后面所有编译都是白费。参数上没什么可调的重点看输出32位编译 OK说明基础具备报错则按提示装gcc-multilib。nasm是否必需取决于项目用的是 Intel 还是 ATT 汇编语法GeekOS 的某些版本两者都有先确认再决定装不装。2.2 用 Makefile 变量控制编译目标而不是硬改源码GeekOS 的构建系统通常是一个顶层 Makefile 加若干子目录 Makefile。新手最容易犯的错是直接去改源码里的编译选项结果改乱之后连原始版本都编不过。正确做法是通过 Makefile 变量覆盖比如指定CC、LD、AS和优化级别。# 在顶层 Makefile 或命令行传入不要直接改源码 CC : gcc CFLAGS : -m32 -ffreestanding -fno-stack-protector -Wall -O2 LD : ld LDFLAGS : -m elf_i386 -T linker.ld AS : as ASFLAGS : --32这里每个参数都有讲究。-m32强制 32 位输出-ffreestanding告诉编译器这是独立环境不要假设有标准库-fno-stack-protector关掉栈保护因为内核里没有对应的运行时支持-O2是常见优化级别调试阶段可以降到-O0方便看汇编。链接器用-m elf_i386指定 32 位 ELF 格式-T linker.ld指向链接脚本这个脚本决定了内核被加载到哪个虚拟地址。如果你把-m32漏了链接阶段就会报架构不匹配这是最高频的翻车点之一。提示改 Makefile 变量时优先用命令行覆盖例如make CCgcc-12这样不会污染源码树换版本也方便。2.3 验证工具链能否生成可引导的 32 位目标装完依赖别急着编整个内核先做一个最小验证写一个极简的汇编入口编译链接成 32 位 ELF再用objdump看它的架构和入口地址。这一步能提前暴露链接脚本和工具链的问题。# 最小验证生成 32 位 ELF 并检查 as --32 -o boot.o boot.s ld -m elf_i386 -Ttext 0x100000 -o boot.elf boot.o objdump -f boot.elf | grep -E architecture|start addressas --32生成 32 位目标文件ld -m elf_i386链接成 32 位 ELF-Ttext 0x100000指定代码段起始地址这个地址要和后续链接脚本保持一致。objdump -f输出里如果看到i386和正确的入口地址说明工具链没问题。如果这里就报错那问题一定在工具链或参数上不用往下走。这个习惯能帮你把问题范围缩小到最小避免在内核源码里大海捞针。3. 编译与镜像制作把源码变成模拟器能启动的软盘工具链确认之后就进入真正的「创建」环节编译内核、生成镜像、准备启动。这一章按顺序给命令每一步都说明它在做什么以及失败时该看哪里。3.1 分步编译先目标文件再链接别一把梭很多项目的 Makefile 支持make一把梭但调试阶段我建议分步来这样出错时能立刻定位是编译还是链接的问题。典型流程是先编译各个.c和.s文件成.o再统一链接成内核 ELF最后转成原始二进制或直接打包进镜像。# 分步编译便于定位错误 make clean make -C src # 编译源码目录生成 .o make -C src kernel.elf # 链接成 ELF具体目标名以项目为准 file src/kernel.elf # 确认是 32 位 ELFmake clean先清掉旧产物避免残留文件干扰。make -C src进入源码目录编译这一步如果报错通常是头文件路径或编译选项问题。链接出kernel.elf后用file确认它是ELF 32-bit如果是 64 位说明-m32没生效。这里的目标名kernel.elf只是常见叫法实际以你项目 Makefile 里的目标为准不要照抄。3.2 制作软盘镜像dd 与 mtools 两种常见路径GeekOS 常见启动方式是从软盘镜像引导所以需要把引导扇区和内核放到一个 1.44MB 的镜像里。有两种做法一种是用dd手动拼接另一种是用mtools操作 FAT 文件系统镜像。前者简单直接后者更接近真实软盘结构。# 方式一dd 拼接引导扇区与内核 dd if/dev/zero offd.img bs512 count2880 # 创建 1.44MB 空镜像 dd ifboot.bin offd.img bs512 count1 convnotrunc # 写入引导扇区 dd ifkernel.bin offd.img bs512 seek1 convnotrunc # 从第2扇区写入内核bs512 count2880正好是 1.44MB 软盘容量。第一条创建全零镜像第二条把引导扇区写到偏移 0第三条用seek1跳过第一个扇区把内核写到后面。convnotrunc很关键它保证写入时不截断已有内容。如果漏了这个参数第二次写入会把镜像截短导致引导失败。这种方式适合引导扇区直接加载内核的简单布局。# 方式二mtools 制作 FAT 软盘镜像 dd if/dev/zero offd.img bs512 count2880 mkfs.fat -F 12 fd.img mcopy -i fd.img kernel.bin ::KERNEL.BIN mcopy -i fd.img boot.bin ::BOOT.BINmkfs.fat -F 12创建 FAT12 文件系统这是软盘常用格式。mcopy -i把文件复制进镜像::表示镜像根目录。这种方式下引导扇区需要能识别 FAT 并加载KERNEL.BIN对引导代码要求更高。选哪种取决于你项目自带的引导程序不要混用。3.3 在模拟器里启动并确认内核真的跑起来了镜像做好后用 QEMU 或 Bochs 启动。QEMU 启动快、命令行简单适合快速验证Bochs 调试信息更细适合后面调内核。# QEMU 启动软盘镜像 qemu-system-i386 -fda fd.img -boot a -nographic # 若需要调试加 -s -S 等待 gdb 连接 qemu-system-i386 -fda fd.img -boot a -s -S-fda fd.img把镜像挂成软盘 A-boot a指定从软盘启动-nographic把输出直接打到终端方便看内核打印。如果屏幕一直黑或者反复重启先确认引导扇区最后两字节是不是0x55AA再看内核入口地址是否和链接脚本一致。-s -S是调试组合-s开 gdb 服务端口-S启动时暂停等你连上再跑。这一步能跑通说明「创建 GeekOS」的主体工作已经完成后面才是改内核逻辑。4. 避坑与排查创建 GeekOS 时最常见的 5 个翻车点这一章单独列高频问题每条按现象、原因、解决来写。这些坑我基本都踩过有些卡一整天都不夸张。4.1 链接报架构不兼容现象ld报i386:x86-64 architecture of input file ... is incompatible。原因是编译时没加-m32生成了 64 位目标文件链接器却按 32 位处理。解决在CFLAGS和ASFLAGS里统一加-m32和--32并确认ld用了-m elf_i386。改完先make clean再编避免旧目标文件残留。4.2 模拟器黑屏无输出现象QEMU 启动后没有任何打印或者不断重启。原因通常是引导扇区无效或内核没被正确加载。先检查镜像第一个扇区最后两字节是否为0x55AA用xxd fd.img | head看偏移 510 附近。再看引导代码里的加载扇区数和内核实际大小是否匹配加载少了内核不完整加载多了会读越界。解决核对引导程序里的扇区计数必要时用ls -l kernel.bin算实际扇区数。4.3 内核入口地址与链接脚本不一致现象能引导但一进内核就崩或者跳转到错误地址。原因是链接脚本里指定的加载地址和引导程序跳转的地址对不上。解决打开linker.ld看. 0x...那一行再对照引导汇编里的跳转目标两者必须一致。常见的是链接脚本写0x100000引导却跳到0x1000这种必崩。4.4 缺少 32 位运行库导致编译中断现象编译时报fatal error: gnu/stubs-32.h: No such file or directory。原因是系统只有 64 位开发库。解决安装gcc-multilib和libc6-dev-i386包名以你的发行版为准。装完重新验证gcc -m32能否通过。注意内核本身是 freestanding 的但构建过程中某些宿主工具可能仍需要 32 位库。4.5 镜像写入时被截断现象镜像大小不对或者写入第二个文件后引导失效。原因是dd写入时没加convnotrunc导致每次写入都从文件开头截断。解决所有对同一镜像的多次dd写入都加convnotrunc并用ls -l fd.img确认大小始终是 1474560 字节。这个坑很隐蔽因为命令本身不报错只是结果不对。5. 进阶验证用 GDB 单步跟到内核第一条指令环境跑通只是开始真正要改内核得能调试。这一章讲一个具体技巧用 QEMU 配合 GDB从引导一直单步跟到内核入口确认控制权交接的每一步。这个能力在后续改调度和内存管理时会反复用到。先准备一个带符号的内核 ELF链接时加-g保留调试信息。然后启动 QEMU 并暂停qemu-system-i386 -fda fd.img -boot a -s -S -nographic另开一个终端用 GDB 连接gdb (gdb) set architecture i386 (gdb) target remote localhost:1234 (gdb) info registers (gdb) x/8i $eip (gdb) break *0x100000 (gdb) continueset architecture i386告诉 GDB 按 32 位解析。target remote连上 QEMU 的调试端口。info registers看当前寄存器x/8i $eip反汇编当前指令附近。break *0x100000在内核入口下断点continue跑过去。如果断点命中说明引导成功把控制权交给了内核如果一直不命中回到第 4 章查引导和入口地址。这里有个实用习惯把0x100000换成你链接脚本里的实际入口地址别照抄。另外QEMU 的-s默认端口是 1234如果被占用可以换端口但 GDB 连接时也要对应改。单步时用si跟汇编ni跳过调用配合info registers看cr0是否置了保护模式位。这些细节能帮你在内核刚启动就定位问题而不是等到后面调度出错再回头猜。我自己的习惯是每次改完链接脚本或引导代码先跑一遍 GDB 到入口确认地址和寄存器状态正常再往下做功能。这个「后悔药」能省掉大量返工。希望帮到你。本文还有配套的精品资源点击获取