ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux 0.01内核编译实践:在Redhat 9.0下重编与Bochs启动

Linux 0.01内核编译实践:在Redhat 9.0下重编与Bochs启动 简介这份PDF面向操作系统学习者与内核开发爱好者是一篇关于Linux 0.01早期内核改造实践的技术文献尤其适合希望从源码层面理解操作系统启动过程的读者。其基于Redhat 9.0平台完整讲解约9000行代码的Linux 0.01编译、运行与启动方法内容涵盖编译环境与GNU工具链选择、ATT语法汇编器适配、源代码语法修正、动态链接系统调用添加以及选用Linux 0.11根文件系统的依据并对两个版本的文件系统代码做了对比分析对理解早期Linux内核结构和系统复现实验有直接参考价值。压缩包只有一个PDF文档大小约211KB以期刊论文形式组织正文包含改造思路、关键步骤与运行实例适合配合内核源码逐节研读。目前已有348人浏览学习适用于操作系统课程设计、内核入门分析及Linux系统开发等场景。1. Linux 0.019000 行代码却是最值得重编一次的操作系统如果你正在做操作系统课程设计或编译原理实验大概率会遇到同一个困境手边能拿到的 Linux 源码动辄几百万行打开源码树根本不知道该从哪下刀。Linux 0.01 是 Linus 最早发布的版本全部代码只有 9000 行左右却已经具备多任务、多用户机制、进程管理、设备管理、内存管理和文件系统这些核心部件被很多人当作“操作系统的 Hello World”。但它的原始代码有一个硬伤1991 年的工具链早就变了as86 汇编器、老式 gcc 内嵌汇编语法、被删除的编译选项导致代码拿到今天直接编译必挂。这篇 PDF 记录的正是基于 Redhat 9.0 平台把 Linux 0.01 改造、编译、并在 bochs 虚拟机里独立启动的全过程包括 Makefile 修改、bootloader 重写、动态链接系统调用补全和根文件系统选型。适合想在一两个晚上完整走一遍“改内核源码 → 编内核 → 引导启动”整条链路的人。2. 环境选型为什么是 Redhat 9.0以及 Makefile 的三个改名三个去选项2.1 先接受一个现实Linux 0.01 的原始代码在今天已经编不过我第一次拿到 Linux 0.01 源码时第一反应是在 Ubuntu 上直接 make结果在内嵌汇编那一关就翻车。原因不复杂当年 Linus 的开发环境是 as86 汇编器加老式 gcc其中 boot.s 用 Intel 语法写成其余汇编文件全部是 ATT 语法到了 Redhat 9.0 时代as86 已经退出主流GNU 工具链的 as 成了事实标准。更麻烦的是老 gcc 支持的那些优化选项比如 -fcombine-regs、-mstring-insns在今天会被直接判定为非法参数。所以改造的第一原则不是“把代码升级到新语法”而是“选一个语法代差最小的平台”。Redhat 9.0 的工具链保留了部分老 GNU 时代的习惯和 0.01 源码之间的鸿沟比现代发行版小得多加上 bochs 对那个时代硬件抽象的支持非常完整所以论文选它作为编译环境是有充分理由的。我当时在虚拟机里装了一个 Redhat 9.0把 gcc、binutils、make 都装齐整个实验就在这一套环境里完成没有去折腾交叉编译。2.2 开发环境准备装好 gcc、binutils、make 和 bochs进入源码目录之前先把工具链检查一遍。Redhat 9.0 默认安装通常已经带有 gcc 和 binutils但为了确认建议先跑一遍# 检查核心工具是否存在同时看版本判断是不是 GNU 工具链 gcc --version as --version ld --version make --version bochs --version逻辑说明gcc 负责 C 源码编译as 负责 ATT 语法的汇编ld 负责链接make 负责按 Makefile 组织构建bochs 提供 x86 保护模式的仿真环境。五个工具缺一个后面每一步都没法继续。Redhat 9.0 自带的 gcc 版本比较接近 0.01 年代的语法习惯这正是它作为实验平台的价值——如果非要在 gcc 13 上硬编光内嵌汇编的约束写法就够改半天。参数说明--version只做存在性和版本确认如果哪一条提示 command not found用 Redhat 9.0 安装光盘里的 RPM 包补上。装完后建议顺手打开源码目录里的 include/linux/sys.h 扫一眼确认 sys_call_table 的声明结构后面补系统调用要频繁用到它。2.3 Makefile 的整体修改三个改名、三个去选项、一个加选项这是整个改造里最机械但也最容易漏的一步。Linux 0.01 源码里每个子目录都带独立 Makefile不是只改根目录那一份就完事。论文明确列出了四类改动工具名称改名、汇编选项调整、编译选项调整、CPU 指令集限制。# 以根目录 Makefile 为例改动点如下其它子目录照做 # 1) 三个改名gas - asgld - ldgar - ar # 2) 去掉 as 的 -c 选项老汇编器用来“只汇编不链接”现在已不需要 # 3) 去掉 gcc 的 -fcombine-regs 与 -mstring-insns选项本身已废弃 # 4) 增加 -m386限制指令集保证内核能在 80386 上跑 # 用 grep 找出所有 Makefile逐个确认 find . -name Makefile -o -name makefile | xargs grep -n gas\|gld\|gar逻辑说明gas、gld、gar 是早期 GNU 工具链带前缀的命名后来统一改成 as、ld、arMakefile 里还是旧名字的话make 会直接报 command not found。-m386 把编译目标钉在 80386 指令集上避免编译器生成 80486 之后才有的指令这个在 bochs 默认 CPU 模型下尤其重要。参数说明find ... | xargs grep是为了把所有用到旧工具名的位置一次性找出来避免漏改子目录。改完之后建议先执行 make clean 再 make否则旧的目标文件会和新编译产物混在一起产生一些难以定位的链接错误。3. 汇编迁移boot.s 重写成 ATT 语法再把内嵌汇编的寄存器约束删干净3.1 boot.s从 as86 迁移到 ATT 语法再用 objcopy 收尾boot.s 是 Linux 0.01 的 bootloader当年用 as86 汇编器加 Intel 语法写成。为了统一到 GNU as需要按 ATT 语法重写。文件很小规模大概几百行直接手工重写比用转换工具再整理更可控。重写后的启动代码大致长这样# boot.s 关键片段ATT 语法示意 .code16 .text .globl _start _start: movw %cs, %ax # cs 指向启动代码段 movw %ax, %ds # 初始化数据段寄存器 movw %ax, %es movw %ax, %ss movw $0x7c00, %sp # BIOS 加载 boot 到 0x7c00栈从这里往下长 call load_kernel # 读软盘把 setup 和 system 模块载入内存 ljmp $0x0000, $setup_start # 跳转到 setup 代码继续初始化逻辑说明boot.s 的核心任务是初始化段寄存器、建立临时栈、从软盘读入后续模块最后跳转给 setup 代码。注意 ATT 语法的方向是“源操作数在前、目的操作数在后”和 Intel 语法正好相反重写时最容易在这里翻车。$0x7c00是立即数BIOS 把引导扇区加载到物理地址 0x7c00栈指针设到这里是常规做法。参数说明.code16告诉 as 生成 16 位代码因为引导扇区运行在实模式下ljmp $0x0000, $setup_start里的 0x0000 是段地址setup_start 是段内偏移。汇编完成后as 默认输出 ELF 格式但引导扇区需要的是纯二进制镜像所以必须转换# 把 boot.o 里的代码段提取成纯二进制供引导扇区使用 as -o boot.o boot.s objcopy -O binary -j .text boot.o boot.bin逻辑说明-j .text只提取代码段-O binary去掉 ELF 头输出原始二进制。这一步不做的话bochs 会把 ELF 文件头当成可执行代码执行开机直接就死。参数说明tools/build.c 也要同步调整它负责把 boot、setup、system 三段拼接成最终的内核镜像文件如果 boot 的输出格式变了而 build.c 没跟上拼接结果会错位。3.2 C 引用汇编变量的下划线问题以及 align 参数变化老版本汇编器在引用 C 语言全局变量时要求变量名前加一个下划线例如_counter现在的 gcc 生成符号时已经不需要这个下划线所以源码里所有汇编文件引用 C 变量的地方都要去掉_。这个改动看着简单但涉及面很广boot、kernel、mm、fs 每个目录的 .s 文件里都有。# 先把所有引用 C 变量的下划线找出来逐个确认再改 grep -rn _[a-zA-Z][a-zA-Z0-9_]* boot/ kernel/ mm/ fs/逻辑说明.s文件里形如movl _jiffies, %eax的代码改成movl jiffies, %eax即可。注意只动汇编文件里对全局符号的引用不要动 C 源码里的变量名。参数说明grep 的结果里会混入字符串常量和注释所以要人工逐个过一遍。另一个隐蔽坑是 align 指令早期版本用乘方数表示对齐量align 2意思是 2 的 2 次方即 4 字节对齐现在的 as 直接使用字节数所以align 2要改成align 4。对照关系如下早期写法乘方数实际对齐字节数现在写法align 01align 1align 12align 2align 24align 4align 38align 8align 416align 16参数说明这个表只用于汇编指令 align 的换算。如果你编译通过但内核运行到某个边界就异常回头查一下是不是 align 值没换算数据对齐错了会引发 GP 异常。3.3 内嵌汇编删寄存器约束别连指令一起删掉Linus 在 0.01 里大量使用内嵌汇编但当时的语法要求人工指定变量使用的 CPU 寄存器后来编译器已经能够自动分配寄存器这些指定就成了多余甚至有害的代码。论文里明确提到要删掉asm(ax)这类代码以及内嵌汇编中所有对寄存器内容的无效声明比如cx,di,si。/* 修改前手写 clobber 列表和寄存器约束 */ __asm__(movl %1, %%eax\n\t addl %2, %%eax : a (result) : a (a), c (b) : cx, di, si); /* 修改后交给编译器自动分配寄存器 */ __asm__(movl %1, %%eax\n\t addl %2, %%eax : r (result) : 0 (a), r (b));逻辑说明修改前a强制结果放在 eaxcx,di,si告诉编译器这些寄存器会被改动但实际内嵌汇编里根本没有改动它们这是无效声明修改后用r让编译器自行选择寄存器输入操作数0表示与输出操作数共享同一个寄存器。参数说明冒号分隔的输出操作数、输入操作数、clobber 区是三段式结构删的时候别把逗号结构弄乱。最稳妥的做法是每个文件改完先单独编译比如gcc -c kernel/sched.c确认没有语法错误再继续下一个文件避免一次性改完几十个文件后找不到问题源头。4. init() 与动态链接从引导到 Shell中间隔着三个关键改造4.1 init() 函数的执行顺序setup、打开 tty0、然后才轮到 forkinit() 是内核启动后的第一个用户态进程入口Shell 能不能弹出来很大程度上看这个函数的执行顺序。论文给出的关键代码按逻辑恢复 OCR 错误后如下static char *argv[] { -, NULL }; static char *envp[] { HOME/usr/root, NULL }; static char vmsg[] \r\nLinux-0.01-rh9 ; static char gmsg[] Adapted\r\n\r\n; void init(void) { int i, pid; setup(); /* 读硬盘分区参数mount 根文件系统 */ (void) open(/dev/tty0, O_RDWR, 0); /* 打开终端 */ (void) dup(0); /* 复制句柄 - 标准输出 */ (void) dup(0); /* 再复制 - 标准错误 */ printf(%d buffers %d bytes buffer space\n\r, NR_BUFFERS, NR_BUFFERS * BLOCK_SIZE); printf(vmsg); printf(gmsg); while (1) { if ((i fork()) 0) printf(Fork failed in init\r\n); else if (!i) { /* 子进程 */ close(0); close(1); close(2); setsid(); (void) open(/dev/tty0, O_RDWR, 0); (void) dup(0); (void) dup(0); _exit(execve(/bin/sh, argv, envp)); } pid wait(i); printf(child %d died with code %04x\n, pid, i); sync(); } _exit(0); /* NOTE! _exit, not exit() */ }逻辑说明setup() 在第一行执行它读取硬盘分区参数并装入根文件系统随后以读写方式打开 /dev/tty0并用两次 dup(0) 复制出标准输出和标准错误句柄。之后进入无限循环fork 出子进程后子进程先关闭继承自父进程的三套标准句柄再重新打开 tty0 并用 dup 重建 stdin/stdout/stderr最后调用 execve 把 /bin/sh 装入内存运行。父进程则通过 wait() 阻塞等 Shell 退出后打印状态并再次循环。参数说明这里最需要注意的细节是最后一行_exit(0)而不是exit(0)。init 进程处在内核初始化上下文exit() 会多做用户态缓冲区 flush 一类的事在这个阶段会导致行为异常用 _exit 直接退出才是正确姿势。Shell 每次退出都会让父进程打印一行死亡状态这其实是调试时可以观察的重要信号。4.2 补动态链接系统调用这部分得对照 Linux 0.11 抄作业Shell 能启动不代表所有命令都能跑。多数 Shell 命令是动态链接的运行时需要内核提供完整的系统调用接口而 Linux 0.01 的系统调用表比 0.11 少了不止一项。论文的做法是分析 0.01 与 0.11 的文件系统代码发现二者在文件系统和可执行文件加载部分没有本质差别于是参照 0.11 补齐缺失的系统调用。# 把 0.11 的系统调用声明表和分发表拿来做 diff这是最直接的抄作业入口 diff -u linux-0.01/include/unistd.h linux-0.11/include/unistd.h | less diff -u linux-0.01/include/linux/sys.h linux-0.11/include/linux/sys.h | less逻辑说明include/unistd.h 里定义了__NR_开头的系统调用号宏include/linux/sys.h 里的 sys_call_table 是系统调用分发表内核根据调用号在这里查表跳转。diff 出来的差异项就是 0.01 缺失的调用。需要重点核对的调用包括 fork、execve、waitpid、open、close、read、write、dup、mount、signal、getdents、time、uname 等这些是 Shell 及其外部命令运行时的基础依赖。参数说明不要直接把 0.11 的 unistd.h 和 sys.h 整个替换过来0.11 里新增的数据结构和系统调用实现会带来新一轮语法不兼容。正确做法是只迁移与动态链接、Shell 运行相关的部分迁移完后在 sys.c、fork.c、exit.c 里补齐对应实现。提示补系统调用时先在 sys_call_table 里占好位置再写函数实现最后回填 unistd.h 的调用号宏。顺序反了容易出现“调用号对不上、查表查到空指针”的问题。4.3 根文件系统选用 Linux 0.11要用 Minix 结构不能拿现代发行版顶替这步决定内核能不能 mount 上根文件系统。Linux 0.01 和 0.11 使用的都是早期 Minix 文件系统而现代发行版默认的 ext4/xfs 无论是结构还是工具都不兼容所以不能直接拿当前系统的根文件系统顶上。论文实测对比了多个 0.11 根文件系统镜像最终选定了一个 60M 的镜像CHS 结构是 121 个柱面、16 个磁头、每道 63 扇区。# 如果自己从头生成根文件系统镜像几何参数必须与 bochs 配置严格一致 # 121 柱面 * 16 磁头 * 63 扇区 * 512 字节 62,447,616 字节接近 60M dd if/dev/zero ofhdc-0.11.img bs512 count$((121*16*63)) mkfs.minix -c hdc-0.11.img # 更省事且与论文一致的做法用现成 0.11 镜像losetup 挂载后检查命令 losetup /dev/loop0 hdc-0.11.img mount -t minix /dev/loop0 /mnt/root ls -l /mnt/root/bin /mnt/root/usr/bin # 确认 sh、ls、gcc 等是否可执行 umount /mnt/root losetup -d /dev/loop0逻辑说明dd 的 count 计算很关键121×16×63121968 个扇区正好等于镜像文件总大小bochs 才能按这个几何参数正确寻址。mkfs.minix 生成 Minix 文件系统挂载后可以往里补充 /bin/sh、/bin/ls、/usr/bin/gcc 等命令。参数说明CHS 结构不是随便填的它必须和 bochs 配置文件里的 cylinders、heads、spt 完全一致否则内核读磁盘时计算出的扇区偏移就是错的根文件系统自然挂不上。论文正文某处写的是 4 个磁头但 bochs 配置脚本和最终运行结果用的是 16 磁头我倾向认为那是排版笔误实际以 121/16/63 为准。5. 编译运行避坑五个真实踩过的坑现象、原因、解决一次讲清老内核编译的坑从来不缺下面这五条是我按这条链路走下来最典型的按“现象 → 原因 → 解决”记录。遇到同类问题可以直接对号入座。5.1 坑一改完 Makefile 依然 command not found现象make 刚开始执行第一行就报xxx: command not found连编译都进不去。原因只改了根目录的 Makefile子目录的 Makefile 里还写着 gas、gld、gar 这些旧工具名或者当前 PATH 里找不到 as/ld安装的 binutils 不在默认路径。解决先用find . -name Makefile -o -name makefile | xargs grep -n gas\|gld\|gar把所有旧名字位置揪出来逐个改成 as/ld/ar再which as ld确认工具路径存在。改完统一执行 make clean 再重编。5.2 坑二4 磁头与 16 磁头之争根文件系统挂不上去现象内核启动到 init() 阶段mount 根文件系统失败屏幕停在打印版本信息之后没有任何错误提示或者提示看不懂。原因根文件系统镜像的 CHS 几何参数和 bochs 配置不一致。论文正文某处写 4 磁头但 bochs 配置脚本用的是 16 磁头几何参数一旦错位内核计算磁盘偏移就全偏了。解决统一以 bochs 配置里的 121 柱面、16 磁头、63 扇区为准如果自己生成镜像dd 的 count 严格用 121×16×63121968。这属于典型的“看正文被带偏”的玄学问题实际以运行脚本为唯一标准。5.3 坑三删内嵌汇编约束时把指令也删了现象编译报 invalid operand或者编译过了但内核运行到某条指令直接死掉。原因内嵌汇编的冒号分三段输出操作数、输入操作数、clobber 区。手删 clobber 的时候把上一段末尾的逗号或某个输入操作数一并删了编译器给报错还好最怕编译过但语义变了。解决改之前每个文件先复制一份留底只删 clobber 区也就是最后一段冒号后面的内容再处理输出/输入操作数里显式的cx,di,si声明。每个文件改完单独gcc -c验证一次能省掉后面整体联调的排查时间。5.4 坑四全局替换下划线把字符串常量也伤了现象编译和链接都正常但启动后 printf 输出的版本信息变成乱码或者字符串缺字符。原因批量替换下划线时用了类似sed -i s/_\([a-zA-Z]\)/\1/g的全局规则把字符串常量里的下划线也一并替换掉了。解决只对 .s 汇编文件做符号名替换更好的做法是先 nm 看未解析符号再精准改引用点字符串常量手工恢复。汇编文件数量本来就少逐个人工过一遍比全局替换更可靠。5.5 坑五Shell 不启动、没有报错多半是动态链接系统调用没补全现象init() 打印完版本信息后光标停住或者系统直接重启Shell 提示符始终没出现。原因execve(/bin/sh) 返回 ENOSYS。Shell 二进制依赖的动态链接系统调用在内核里还是空的查 sys_call_table 发现该位置的函数指针没有初始化。解决回到第 4.2 节的 diff 流程把缺失的系统调用补齐并在 sys.c 里补函数实现。没有 strace 可用时我通常会在 execve 失败分支临时加一行 printk 打 errno看到 ENOSYS 就基本能确认是系统调用表问题。6. 复现与进阶bochs 启动参数、gcc 编译 hello以及验证内核改动的一次实验6.1 bochs 配置与启动命令论文里给出的 bochs 运行脚本是整套复现的最后一块拼图。配置如下# bochsrc16M 内存软盘引导硬盘挂 0.11 根文件系统镜像 megs: 16 floppy a: 1_44 linux-fd.img, statusinserted ata0-master: typedisk, pathhdc-0.11.img, modeflat, cylinders121, heads16, spt63 boot: a逻辑说明megs 把虚拟机内存设为 16M够 Linux 0.01 跑软盘镜像 linux-fd.img 放的是编译好的内核镜像boot: a 告诉 bochs 从 A 盘引导硬盘镜像 hdc-0.11.img 是根文件系统modeflat 表示整块磁盘不分区直接按 CHS 参数寻址。参数说明cylinders121、heads16、spt63 三个参数必须与根文件系统镜像的实际几何结构一致前面避坑章节已经强调过。启动命令用bochs -f bochsrc -q-q 跳过交互式配置直接启动。启动后逐个执行下列命令验证系统可用性命令预期结果df列出当前挂载的文件系统设备信息pwd输出当前工作目录ls显示当前目录内容rm hello删除当前目录下的 hello 文件gcc hello.c -o hello编译链接 C 程序生成可执行文件./hello输出 Hello, world!6.2 把构建过程固化成一个脚本整套流程手动操作容易漏我习惯把编译、写软盘镜像、启动 bochs 串成脚本#!/bin/bash # 重新编译内核先 clean 再 make避免旧产物干扰 make clean make # 把生成的内核镜像写入软盘镜像convnotrunc 防止文件被截断 dd ifImage oflinux-fd.img bs512 count2880 convnotrunc # 自动启动 bochs bochs -f bochsrc -q逻辑说明make clean 是为了彻底清理旧目标文件老内核的 Makefile 依赖关系不完整增量编译经常带出来一堆诡异错误dd 把编译产物 Image 写入软盘镜像最后启动 bochs。参数说明count2880 对 1.44M 软盘正好是 2880 个扇区convnotrunc 避免 dd 把镜像文件尾部截掉。6.3 进阶改一行内核代码观察重启后的可见变化验证这套链路最好用的方法不是反复跑 hello而是故意改一行内核代码看效果。比如在 init() 提示字符串里追加一个编译时间宏重新执行构建脚本重启后看版本提示是否变化或者改 kernel/sched.c 里时间片长度的默认值观察 Shell 响应快慢的变化。改动越“肉眼可见”越能确认“源码改动 — 编译 — 运行”整条链路是通的而不是拿了个现成镜像在自欺欺人。从那以后我每次做内核实验都强制走一遍“改一行 → 重编 → bochs 验证”的闭环再小的改动也不跳过因为老内核几乎没有错误提示可用跳过一步就是把问题往后推。这份 PDF 的改造思路和参数表值得放在手边对照着查希望帮到你。本文还有配套的精品资源点击获取
返回列表