从 Linux 0.01 源码阅读到编译运行:一次操作系统原理的深度实践
简介Linux 0.01内核源代码压缩包是理解Linux起源与操作系统底层原理的经典入门资料。它收录了林纳斯·托瓦兹1991年写成的早期内核代码规模小、结构相对简洁覆盖进程管理、内存管理、磁盘I/O等基础模块适合正在学习操作系统、计算机组成或对Linux内核感兴趣的学生与开发者研读。压缩包共91个文件、大小仅95KB其中以45个C源码文件、31个头文件为主另有7个汇编文件实现启动与系统调用入口5个Makefile用于组织构建流程。资源按boot、kernel、fs、mm、lib、include等目录拆分可对照Makefile或VSCode工作区配置文件开展逐模块分析。当前已有755人学习下载通过这份源头代码可以直观看到进程调度、文件系统、设备驱动等核心机制的最初实现并体会Linux从微内核雏形走向现代系统的演进脉络为系统学习操作系统提供了完整且便于检索的原始素材。1. 一万行源码的起点为什么今天还有人翻出 0.01 的压缩包现代 Linux 内核的代码量已经以千万行计随便一个子系统拎出来都压过整个第一版内核。手里这份“linux 0.01内核源代码.zip”解压之后全部 C 代码加汇编只有一万行上下却能编译出一个真实启动的操作系统。它解决的是操作系统教材里最难讲清楚的黑匣子问题调度器凭什么选某个进程、fork 到底复制了什么、系统调用表长什么样、页表是谁在什么时候建的。适合不满足于调 API、想亲手把内核跑一遍的人也适合准备内核岗位面试的开发者。我不建议任何人靠它学现代内核的工程实践但如果你想看清操作系统的底色这一万行是绕不开的起点。2. 解压之后先盘目录结构、重点文件与推荐阅读顺序2.1 从 zip 到可读源码解压与版本确认拿到 zip我习惯先看清单再落盘# 先看清单再决定要不要解压 unzip -l linux-0.01.zip # 解压到独立目录保留原始包做对照 unzip linux-0.01.zip -d ./linux-0.01 cd linux-0.01unzip -l只列内容不写文件能提前确认包内有没有自带顶层目录。有些流传的包把源码直接打在根上解压出来散落一地后面对比原始文件都找不到参照。解压到独立目录算是给自己留后悔药后面改 Makefile、改调度代码随时能和原始版本做 diff。版本确认不用看注释里的年份看结构就够了。真正的 0.01 没有arch/目录没有 Kconfig整个内核平铺在boot/ kernel/ mm/ fs/ init/ lib/ include/七个目录里boot/下是.s汇编而不是现代内核的startup_32.c。看到这两个特征基本就是 1991 年那个版本的直系后代。市面上有些包被后人加过注释或补丁目录结构不变正文细节会有差异遇到看不懂的地方先怀疑版本改动别急着怀疑自己。2.2 七个目录一张表每个目录到底在干什么目录职责建议第一个读的文件boot/引导扇区、实模式到保护模式、建立页表boot.s、head.sinit/内核入口初始化后转向用户态main.ckernel/进程调度、系统调用、信号、时钟、打印sched.c、system_call.smm/分页管理、物理内存分配memory.c、page.sfs/Minix 文件系统实现inode.c、namei.clib/给用户程序用的字符串、内存、文件函数string.c、malloc.cinclude/全局头文件task_struct 和系统调用表都在这里linux/sched.h、linux/sys.h0.01 没有现代内核的分层抽象一个文件就是一类机制的完整实现kernel/sched.c一个文件就是整个调度器mm/memory.c里就是全部分页管理逻辑。这个特点既是缺点也是优点——往目录里一看就知道边界不用顺着几十个头文件追依赖。但相应地它几乎没有冗余代码每一行都可能承担职责读的时候不能像读现代代码那样跳着看。读include/linux/sched.h时重点看 task_struct 在 0.01 里到底有几个字段。现代内核的 task_struct 有几十上百个字段而 0.01 里一个结构体就能数完但它已经包含了状态、时间片、内核栈指针、文件描述符表和父进程指针——这些是今天几乎所有线程实现都在用的核心概念。include/linux/sys.h里的系统调用表也一样一张函数指针表序号就是用户态传进中断的调用号没有哈希、没有动态注册却把机制表达得清清楚楚。2.3 我推荐的阅读顺序从 main.c 往两头读不要从 boot.s 开始常见翻车姿势是拿起源码就啃 boot.s结果在实模式寄存器里绕一个月。我更推荐反着走。先读init/main.c一百多行是内核从汇编进入 C 之后的第一站接着读kernel/sched.c和kernel/system_call.s搞懂进程是怎么被调度、怎么被切换的再回头看boot/head.s知道进入 main.c 之前 CPU 处于什么状态最后才碰fs/和mm/这两个大块。这个顺序的逻辑是先建立“代码在做什么”的整体印象再补“CPU 怎么走到这里”的底层细节读 head.s 时能省掉大量查手册的时间。第一遍读的时候不要追求每个寄存器都懂先记录三个结论页表在哪里建立、中断门在哪里设置、任务切换靠哪条指令。第二遍再逐行展开。读 task_struct 的字段时对照现代内核的结构体看会发现状态位、内核栈、文件描述符这些概念三十多年没变过变的只是组织方式。这是 0.01 最值得读的地方概念极简但每个概念都是后来一切复杂性的种子。3. 从 zip 到 Image工具链选择、Makefile 三处改动与最小编译命令3.1 为什么 0.01 不能像现代内核那样直接 make0.01 的年代没有 Linux 可以自举内核是在另一套系统上交叉编译出来的工具链行为和今天差别很大。它有两个特殊依赖。第一boot/boot.s是 8086 实模式汇编用的是当年 as86 的语法现代 x86-64 的 gas 根本不认第二内核主体按 32 位模式编译链接而现代发行版默认工具链输出 64 位不对齐的话链接阶段就会断。所以直接 make 必然翻车。我现在在新机器上都是同一套做法先把 32 位交叉编译能力凑齐# 缺什么装什么bin86 是编译 boot.s 的关键 sudo apt-get install build-essential bin86 makebuild-essential 提供 gcc 和 makebin86 提供 as86 与 ld86是编译 boot.s 的必要工具。部分新发行版把 bin86 移出了官方源这种时候从源码编译 bin86 是成熟路径装完先验证as86 --version。如果这一步都过不去后面所有步骤无从谈起。不要用 nasm 替换 as860.01 的 boot.s 用的是 as86 的伪指令语法nasm 解析不了这条路上没有捷径也不值得自己写翻译脚本。3.2 Makefile 的 3 个必调参数让新 gcc 编 1991 年的内核0.01 的 Makefile 极朴素编译选项集中在变量里。我每次换环境都要动三处。第一删除旧版 gcc 才认识的优化选项像-fstrength-reduce -fcombine-regs这类新版 gcc 收到就直接报错。第二关闭现代编译器默认行为里不能用于裸机内核的部分主要就是栈保护和 PIE。第三确认 as86/ld86 的变量没有被 Makefile 里残留的旧路径覆盖。我的常见改法直接在 Makefile 顶部整块替换AS as86 LD ld86 CFLAGS -m32 -O0 -fno-stack-protector -fno-pie \ -fno-strength-reduce -fno-combine-regs \ -I ./include这里-m32强制 32 位输出-fno-stack-protector是编裸机代码的必备开关否则 gcc 会在每个函数里插入栈检查代码链接时找不到对应符号-O0是为了后续调试时变量不被优化掉。如果 gcc 版本很新还可能要加-fno-builtin防止编译器把源码里的memcpy换成自己的内建实现导致链接符号对不上。改完 Makefile先跑一句make -n查看实际要执行的命令确认关键的 as86 和 gcc 都带上了预期参数再正式 make能省掉不少来回。3.3 编译并检查产物Image 不是终点只是起点配置正确之后最小命令就是一行# 清理历史产物后完整编译 make clean make第一次编译成功我建议按名字逐一确认产物boot.s.o、kernel/下每个.o、以及最后生成的Image。直接检查引导扇区很关键# 确认 Image 的大小再抽查第一扇区字节 ls -l Image dd ifImage bs512 count1 2/dev/null | od -A x -t x1z | head -1od输出里如果第一字节是跳转指令常见的是ea说明 boot.s 被正确编进去了。Image 的链路是引导扇区只有 512 字节内核主体跟在后面boot.s 运行后会通过 BIOS 中断把后面的内容读进内存。有人会在这个环节加-Wall重新编译一遍我建议不要。0.01 大量使用老式写法开-Wall之后警告会淹没真正有用的信息真想看警告等编译通过后单独编某一个文件把输出重定向到日志里慢慢看。现代内核编译讲究配置裁剪和模块化0.01 的做法完全不同——所有目录的对象在一个 Makefile 里串起来最终揉成一个 Image。改代码主要集中在kernel/和mm/改完重新 make 链路很短这也是我推荐它做实验的原因从改一行 C 代码到看到行为变化可能只需要一分钟。只是要记住链接顺序在 Makefile 里是定死的boot之外的对象顺序不要自己调否则可能出现符号都有但运行时崩在奇怪位置的现象排查起来相当费时。4. 让内核在 QEMU 里站起来引导镜像、最小根文件系统与启动参数4.1 用 dd 把 Image 做成可引导的软盘镜像0.01 时代的启动载体是软盘BIOS 读入第一扇区boot.s 再把后续扇区搬进内存。到今天实验里最稳的载体仍然是软盘镜像——QEMU 对软盘控制器的模拟最接近老硬件不容易被现代磁盘接口的差异干扰。先把空镜像建好再把 Image 覆盖到前几个扇区# 先造一张 1.44MB 空软盘镜像 dd if/dev/zero offloppy.img bs1024 count2880 # 把引导扇区和内核主体写进前两个扇区convnotrunc 防止截断镜像 dd ifImage offloppy.img bs512 count2 convnotrunc第二条命令里的convnotrunc是血泪经验dd 默认写完输出文件会截断不加这个参数Image 写入后软盘镜像的剩余部分会被清空后面的根文件系统直接没了。这里只覆盖前两个扇区是因为 0.01 的 boot 会自己根据约定去读后续扇区而不是要求内核主体立刻躺在偏移 0 处。如果要确认写入位置xxd floppy.img | head看前 32 字节即可和 3.3 里查 Image 的方法一致。4.2 准备根文件系统内核启动后要找一个 shell0.01 的初始化走到用户态之后会尝试打开设备、执行/bin/sh。镜像里没有这个文件内核就会在 mount 相关的位置停下日志里出现文件系统相关的错误信息。这一步是很多人第一次跑 0.01 时最大的拦路虎需要手工构造一份最小根文件系统。常规链路是先用环回设备挂载软盘镜像格式化出老版本 Minix 格式的文件系统# 把软盘镜像挂成环回设备格式化为老版本 Minix 文件系统 losetup /dev/loop0 floppy.img mkfs.minix -1 /dev/loop0 mount /dev/loop0 /mntmkfs.minix默认生成的格式版本在 0.01 眼里可能太新显式指定老版本格式-1参数在不同发行版写法略有差异用前查一下mkfs.minix --help才不会挂载失败。挂载之后把一份静态链接的 shell 或你准备的最小 init 程序放到/mnt/bin/sh再按 0.01 的include/里定义的主次设备号建立/dev下的节点。节点的主次设备号一定要以 0.01 头文件里的定义为准不要凭现代 Linux 的常识猜。这个细节也是 0.01 适合做实验的原因之一你会被迫把设备号的对应关系读一遍源码而不是背下来。最后umount /mnt、losetup -d /dev/loop0这个软盘镜像才同时具备引导和根文件系统两层身份。如果手头没有兼容那个年代系统调用方式的 shell可以退一步先不构造根文件系统只观察内核启动到 mount 之前的日志。能走到设备初始化和文件系统挂载尝试这一步已经证明编译、链接、引导都是通的剩下的是系统调用兼容层面的工作不影响学内核主线。4.3 QEMU 启动参数与预期输出镜像准备到位启动命令很短# 从软盘启动给 16MB 内存即可 qemu-system-i386 -fda floppy.img -boot ordera -m 16-fda指定第一软盘为 floppy.img-boot ordera强制从软盘启动-m 16给 16MB 内存。0.01 能识别的内存范围有限给几十 GB 没有意义反而可能让它的内存探测逻辑走到没验证过的分支。启动后如果能看到内核版本打印、设备初始化信息说明 boot head main 这条链路是通的这一趟编译算真正有了回报。黑屏的话先别怀疑内核代码回 4.1 检查镜像构造。常见问题集中在两类前两个扇区根本没写对或者用了带截断的 dd 把镜像写坏了。0.01 的启动高度依赖 BIOS 读盘而 QEMU 的软盘模拟非常保守大多数问题出在镜像布局而非源码逻辑。解读启动日志有个习惯我建议备一份源码在另一个窗口日志打印到哪个模块就切到对应文件看上下文。0.01 没有现代内核的动态调试框架日志顺序几乎等于代码执行顺序这是它一个很奢侈的优点。日志停在哪一行就去看对应文件的上一级调用这比来回试参数高效得多。5. 避坑编译和启动 0.01 的 5 条真实踩坑记录这部分记录的是我在不同机器上反复折腾 0.01 时遇到的真实问题类型按现象、原因、解决三段写方便你遇到同样问题时直接对号入座。前三条基本换成新机器就会遇到后两条靠运气但反复踩的人最多。5.1 make 一开始就报错找不到 as86现象终端直接提示as86: command not foundboot.s 连编译的机会都没有。 原因现代发行版默认工具链里根本没有 8086 汇编器bin86 属于偏门包很多系统既不预装源里也可能没有。 解决先sudo apt-get install bin86装不上就从源码编译装完务必验证as86 --version能输出版本。这条属于环境问题和源码无关遇到先查工具链再查代码能少走一半弯路。5.2 gcc 报 unrecognized option编译第一行就中断现象编译内核主体某个 C 文件时gcc 直接拒绝执行提示类似unrecognized command-line option选项名以-f开头。 原因0.01 原始 Makefile 里写了不少 1991 年 gcc 才认识的优化选项新版本把这些开关删掉了留下的只有报错。 解决把报错的选项从CFLAGS里删掉或者整块换成第 3 章的等价写法。这个过程没有任何玄学就是老代码清理过期选项。要注意一次可能报多个选项别删一个就重跑先make clean再一次性把报错清完。5.3 链接阶段报错找不到 crt0.o 或类似启动对象现象前面编译都正常最后链接 Image 时 ld86 报错说找不到某个对象文件通常是 crt0 相关。 原因老工具链默认在自己的搜索路径里找启动对象新版工具链路径变了或者 bin86 源码编译时装到的位置不在默认路径里。 解决在 Makefile 里给链接器加-L路径指向实际存在该文件的位置。这一条比较坑的地方在于报错时机在最后一步很容易让人误以为是源码改坏了其实和内核代码毫无关系。5.4 QEMU 黑屏没有任何输出现象启动窗口黑漆漆一片看不到引导输出也没有崩溃提示。 原因最常见的是引导扇区没有写到镜像偏移 0 处或者 dd 时丢失了convnotrunc导致镜像被截断再或者文件系统区域覆盖了引导区。 解决按 4.1 重建镜像先xxd确认前两个扇区内容再确认后面文件系统没有被破坏。如果镜像构造没问题换-boot ordera显式指定启动顺序同时关掉其他启动设备。老内核在闪屏和输出之间过渡很快有时像黑屏其实是已经崩在某个早期阶段配合 QEMU 的-d日志或改用 Bochs 调试器加断点能更快定位。5.5 挂载根文件系统时报错卡在 mount 阶段现象启动日志走到文件系统挂载附近就停下来提示找不到超级块或设备甚至直接 panic。 原因根文件系统格式太新或者文件系统写到了镜像里内核预期之外的偏移再或者镜像里根本没有合法的超级块。 解决用mkfs.minix显式生成老版本格式格式化前确认软盘镜像总大小正确文件系统区域不要覆盖前两个引导扇区。如果用了环回设备还要确认losetup的偏移没有指错。这条是踩坑重灾区问题几乎都出在文件系统构造链条上真正由源码引起的反而少。我的体会是报错越早越安全链接之后的运行问题才是真正费时间的而运行问题里九成以上出在镜像布局和工具链版本而不是你改的那几行代码。6. 已经能启动之后三个亲手改内核的验证实验编译跑通只是开始真正有价值的是动手改。我建议按下面三个实验递进验证。第一个实验是改启动横幅。在init/或kernel/里找到版本信息的打印调用改成你自己的标识字符串重新 make再走一遍第 4 章的镜像流程。这个实验成本最低却能完整验证“改代码—重编译—重做镜像—运行观察”的闭环这个闭环之后会反复用到。第二个实验是加一个自定义系统调用。0.01 的系统调用机制非常直接在include/linux/sys.h里往函数表尾部加一行在kernel/sys.c里写实现用户态程序通过中断指令调用。你不需要理解现代内核复杂的系统调用注册框架就能完整看到“用户态请求—内核分发—执行—返回”的整条路径。这个实验做完对系统调用的理解会超过背十遍面试题。第三个实验是用 Bochs 验证 head.s 建页表的过程。Bochs 内置调试器可以查看 CR3 和线性地址翻译结果比 QEMU 更适合做底层验证。在 head.s 里故意把某个页表项的 present 位清零再让 main.c 访问对应地址观察异常发生的位置。这个实验能证明页表是真实生效的而不是代码里写着玩玩。我的习惯是每次改内核前先把当前能启动的镜像复制一份因为老内核一次改动往往牵连三四个文件跑挂了之后要翻半天才能想起来改过什么。先备份再实验这个习惯救过我很多次。等这三个实验都亲手做过一遍你手里的 0.01 就不再是别人写的老源码而是自己验证过的操作系统骨架。希望帮到你。本文还有配套的精品资源点击获取