ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RHEL 9 编译 Linux 0.01 内核:工具链重建与 QEMU 运行指南

RHEL 9 编译 Linux 0.01 内核:工具链重建与 QEMU 运行指南 简介这份PDF资料围绕在Redhat 9.0平台上实现Linux 0.01编译与运行的方法展开面向Linux操作系统学习者与系统开发人员重点解决早期内核无法直接在当前环境编译运行的问题。资源包含1个PDF文件共211KB内容紧凑便于快速查阅关键编译配置与代码修改要点。目前已有348人浏览学习作为一份经过实践验证的技术文献具有明确参考价值。文中详细介绍了编译环境选择、源代码语法修改、动态链接系统调用增加、根文件系统创建等关键技术并参照Linux 0.11源码对与Shell运行相关的代码进行精心调整给出了Makefile选项修改、boot.s文件按ATT语法重写、使用objcopy转换为纯二进制镜像等具体编程要点。同时对比指出Linux 0.01与0.11在文件系统上并无本质差别。读者据此可尝试复现Linux 0.01的独立启动、在Shell下执行基本命令以及用GCC编译运行C语言程序从而加深对操作系统底层机制的理解。1. 在 RedHat 9.0 上编译 Linux 0.01卡点不在内核而在工具链RedHat 9.0RHEL 9自带的 GCC 是 11.xbinutils 是 2.36 左右Linux 0.01 写成于 1991 年作者当时用的编译器是 GCC 1.40引导扇区还用了一套源自 MINIX 的汇编器 as86/ld86。把这套 30 年前的代码放到 RHEL 9 上直接跑make你会看到一堆unrecognized command line option紧跟着是 ld 拿 64 位格式去链接 32 位内核目标文件最后生成一个既不是 ELF 也不能启动的垃圾文件。这个标题真正要解决的问题不是“读懂内核”而是“复现 1991 年的构建链”哪些工具链还能用、哪些老参数必须删、哪些代码要用现代语法改写然后把产物喂给虚拟机跑起来。阅读这篇笔记的人多半手里已经有一份 0.01 的源码或者正在研究启动流程、页表初始化、最早的系统调用实现。下面这套流程我用在 RHEL 9 上做过同样的内核对构建链重建QEMU 和 Bochs 都能启动。2. 搭建工具链bin86、multilib 与 0.01 源码目录2.1 RHEL 9 需要的最小软件包RHEL 9 的默认仓库里没有专门为 Linux 0.01 准备的工具但绝大多数构建依赖都在标准源里。先确认系统能用的编译工具再补齐 32 位支持。软件包用途说明gcc编译 C 代码RHEL 9 自带 gcc 11必须配合-m32使用binutils提供 gas、ld、objdump链接 kernel 时要用到-m elf_i386make执行 0.01 的 Makefile系统自带版本不限libgcc.i68632 位 libgcc 运行时链接 kernel 时可能会引用__udivdi3等glibc-devel.i68632 位头文件单纯编内核不需要但能让 gcc 的 multilib 配置完整nasm提供 ndisasm用于反汇编引导扇区排错时很好用qemu-system-x86运行内核也可以换 bochs本文以 qemu 为主安装命令dnf groupinstall -y Development Tools dnf install -y nasm qemu-system-x86 dnf install -y libgcc.i686 glibc-devel.i686glibc-devel.i686是很多 RHEL 用户忽略的一项。如果只装了gccgcc -m32在链接阶段会报找不到crt1.o因为 x86_64 的 multilib 体系需要配套的 32 位启动文件。虽然 Linux 0.01 内核本身是 freestanding 代码不链接 libc但后续编译tools/build这个宿主工具时仍可能踩到 32 位库缺文件的问题。2.2 为什么引导扇区必须用 as86/ld86Linux 0.01 的boot/bootsect.s和boot/setup.s使用的是 MINIX 汇编器语法注释以!开头段定义采用.text.data的简化写法指令风格和 GNU as 完全不同。GNU as 无法直接编译这两个文件。0.01 的 Makefile 里已经写死了调用as86和ld86这两个程序来自一个叫 bin86 的独立工程和 GCC 不是同一套发布物。RHEL 9 的官方源里没有 bin86。常见做法是下载 bin86 源码包在本地编译安装。以 bin86 0.16.21 为例tar xf bin86-0.16.21.tar.gz cd bin86-0.16.21 make sudo make install prefix/usr/local as86 --version ld86 --versionprefix/usr/local是为了避免和系统已有的/usr/bin下的同名程序冲突。装完后确认as86和ld86可用0.01 的引导部分就能进入正常构建流程。如果编译 bin86 时遇到-fno-stack-protector相关的报错说明你的系统默认开了栈保护在 CFLAGS 里显式加上-fno-stack-protector再 make 即可。2.3 源码目录结构与 tools/build 的角色拿到 0.01 源码后先看目录结构find . -maxdepth 2 -type d | sort典型的目录是boot/、kernel/、mm/、fs/、lib/、include/、tools/。boot/下是bootsect.s、setup.s、head.skernel/下是调度、系统调用、中断处理的 C 和汇编tools/下有一个build.c。这个build.c不是内核本身而是一个运行在宿主机上的镜像组装工具。它的作用是把编译出的bootsect、setup、tools/system三者按引导扇区布局合并成最终的内核映像Image。所以整个构建链路是先编出三段独立产物再让build拼装而不是用dd手工拼接。3. 让 make 顺利跑完的关键修改-m32、老 CFLAGS 和链接格式3.1 从 x86_64 回到 32 位0.01 内核的 C 代码和汇编器代码都假设int是 32 位、long是 32 位。现代 x86_64 的默认数据模型是 LP64long是 64 位直接编译会让内存管理部分的计算全部错位。所以第一步是把 GCC 的输出目标改成 32 位。Makefile 里需要改三处CFLAGS 加-m32汇编head.s时给 GNU as 加--32链接生成tools/system时给 ld 加-m elf_i386。0.01 的 Makefile 结构比较简单但不同发布包的行号不一样不要直接抄补丁。核心改动形如CFLAGS -m32 -Wall -O2 -fomit-frame-pointer -fno-builtin \ -fno-stack-protector -fno-pic -fno-PIE \ -nostdinc -Iinclude ASFLAGS --32 LDFLAGS -m elf_i386 -Ttext 0 -s-Ttext 0是所有早期 x86 内核共用的约束setup.s会把整个 system 段搬到物理地址0x00000然后跳过去执行head.s所以内核代码段必须按0x0为起点链接。如果你把-Ttext设成0x100000现代内核常用-Ttext 0x1000000head.s里的绝对跳转全部会错位启动后大概率出现页异常。-fno-stack-protector是 RHEL 9 上必须加的。Redhat 系 GCC 默认打开了 stack protector会在函数入口插入对%gs段偏移量的访问而 0.01 没有初始化%gs对应的 TSS 字段内核会在第一次函数调用时直接崩溃。-fno-pic也是必须的PIC 代码里对全局变量的访问会经过 GOT而 0.01 的链接脚本里根本没有 GOT 的概念。3.2 删除 GCC 已经不认识的老编译选项0.01 的 Makefile 里有几个当年 GCC 1.40 的选项现在拿出来用只会报错。最典型的是老选项现状处理方式-fcombine-regsGCC 4.0 之后移除直接删除-mstring-insnsx86 后端早已移除直接删除-fstrength-reduce仍在但已无实际优化效果可留可删-fomit-frame-pointer仍支持保留遇到unrecognized command line option时错误信息会直接指出是哪个选项。用这个错误信息做逆向来清理即可不需要保留任何有争议的老参数。清理完以后再编译C 代码的报错基本只剩两类隐式函数声明和 KR 函数定义带来的 warning。Linux 0.01 既有 KR 风格也有 ANSI 风格的定义现代 GCC 对 KR 风格仍有兼容性默认只会警告不会被拒绝。所以不要在 Makefile 里加-Werror否则return type defaults to int这种 warning 会直接中断构建。3.3 处理内联汇编的兼容性0.01 的 C 代码偶尔会出现__asm__和寄存器变量。现代 GCC 对这些语法的接受度还算高但有一个常见差异老代码里写register char _res asm(ax)这类硬编码寄存器变量时在不同优化等级下行为可能不一致。0.01 的 Makefile 默认-O建议保留这个优化级别不要升到-O2。-O2会让 GCC 更激进地重排指令寄存器变量与内联汇编的交互更容易出问题。保持-O是最接近当年编译环境的做法。如果make过程中遇到 asm 相关的错误错误信息通常会指向include/asm/下的某个头文件。这种问题没有统一补丁因为不同来源的 0.01 源码可能已经被人改过。常见修法是把%eax写成%%eax或者在 asm 语句末尾补上memoryclobber。对于只想让内核跑起来做研究的读者如果某段内联汇编实在改不动一个务实的替代方案是去查赵炯《Linux 内核完全注释》配套的 0.01 修正源码那份源码已经考虑了现代编译器兼容性。3.4 三个关键产物的确认编译正常结束后tools/build会执行最终在当前目录生成Image。先检查中间产物的大小和格式ls -l boot/bootsect boot/setup tools/system tools/build Image readelf -h tools/system 21 | head -5readelf会报File format not recognized这很正常因为tools/system是未经 ELF 封装的原始二进制不是普通可执行文件。bootsect应该正好 512 字节这是 BIOS 加载引导扇区的固定大小。如果bootsect超过 512 字节tools/build会报错或生成的镜像无法启动。4. 生成软盘镜像Image、根文件系统与 0xAA554.1 把 Image 写进可启动软盘镜像Linux 0.01 的设计目标是软盘启动QEMU 和 Bochs 都以软盘镜像为输入。先做一个 1.44 MB 的空白镜像再把 Image 从偏移 0 写入dd if/dev/zero ofboot.img bs1024 count1440 dd ifImage ofboot.img convnotrunc第一条命令生成全零的 1,474,560 字节文件第二条命令把编译好的 Image 覆盖到文件头部。convnotrunc保证 dd 不会修改目标文件的大小Image 没写到的扇区保持为零。这样得到的boot.img就是一块虚拟软盘。根文件系统单独做一个镜像。0.01 的根设备通常指向第二块软驱所以习惯命名为rootfs.imgdd if/dev/zero ofrootfs.img bs1024 count1440 mkfs.minix -c rootfs.imgmkfs.minix在 util-linux 包里RHEL 9 自带。如果执行时提示找不到需要先dnf install util-linux。但这里有一个坑mkfs.minix只能创建空文件系统你还需要把/bin/sh和必要的命令放进去。0.01 配套的 rootfs 网上能搜到现成的通常是约 1.44 MB 的 MINIX 文件系统镜像直接作为rootfs.img使用即可。自己做 rootfs 意味着要挂载 MINIX 文件系统而 RHEL 9 内核默认不保证启用 MINIX 模块这属于一个分支话题研究内核启动流程时可以直接用现成镜像跳过。4.2 验证引导扇区末尾的 0xAA55BIOS 在读取引导扇区后会检查第 510 和 511 字节是否为0x55 0xaa这是最基础的可启动判断。用xxd直接看镜像尾部xxd -s 0x1f0 -l 32 boot.img输出末尾两行应该能看到55 aa。如果这里不是55 aa说明bootsect没有被正确拼进镜像或者拼接时偏移错了。这个验证比直接启动 QEMU 快得多是任何引导镜像排错的第一步。4.3 用 file 快速确认镜像类型file boot.img如果构建正确大多数情况下会输出类似DOS/MBR boot sector的字样。虽然 Linux 0.01 的引导扇区不是标准的 DOS MBR 结构但file识别到第 510 字节的55 aa后也会把它归类为 MBR。这一步排查可以帮你确认文件头没有被意外截断。5. 在 QEMU 里跑通 0.01显示、多软驱与启动日志判断5.1 QEMU 启动命令与参数说明QEMU 的 i386 模拟器可以直接读取软盘镜像。启动命令推荐qemu-system-i386 -m 16 -fda boot.img -fdb rootfs.img -boot ordera -display curses-m 16指定 16 MB 内存0.01 的内存管理按最大 16 MB 设计给多了反而可能触发它内部没有处理的边界条件。-fda是 A 盘放引导内核的boot.img-fdb是 B 盘放根文件系统。Linux 0.01 的启动代码里根设备号写的是/dev/fd1即 B 盘所以根文件系统放第二软驱是常见做法。-boot ordera强制从软驱 A 启动避免 QEMU 默认从硬盘或光驱寻找引导扇区。-display curses是我在无图形界面的 RHEL 服务器上最常用的选项。QEMU 的 curses 后端会把虚拟机的文本屏幕渲染到终端里。0.01 的早期输出走的是显卡文本模式curses能直接看到Linux version 0.01的字样不需要额外配 VNC。如果终端不支持 curses 或者需要远程查看图形输出可以用qemu-system-i386 -m 16 -fda boot.img -fdb rootfs.img -boot ordera -display vnc:15.2 启动成功与常见失败对照现象含义处理QEMU 窗口黑屏几秒后无反应引导扇区未被识别检查boot.img偏移 0x1fe 处是否为55 aa出现Boot failed或类似提示BIOS 没找到可启动介质确认-boot ordera且-fda指向了正确的镜像能进入 0.01 但马上挂载失败根文件系统格式或设备号不对确认-fdb rootfs.img且 rootfs 是 MINIX 格式内核输出后又重启页表或 IDT 初始化异常检查Makefile是否用了-O2或-fpic最理想的结果是屏幕打印出类似Linux version 0.01的开机信息然后进入根文件系统挂载阶段。后面的输出取决于 rootfs 里有没有/bin/sh。能走到这一步说明编译链、链接地址、引导扇区这三个最难的关卡都已经通了。5.3 用 -d 选项抓 QEMU 内部日志QEMU 对 x86 异常有内置日志。如果内核启动到一半异常重启又看不出原因可以加-d和-D把日志写到文件qemu-system-i386 -m 16 -fda boot.img -fdb rootfs.img -boot ordera -display curses -d int,cpu_reset -D qemu.logint会记录每次中断或异常的发生cpu_reset会记录 CPU 重置发生的位置。日志文件里如果出现大量#GP或#PF配合objdump反汇编就能定位到具体指令。这是 0.01 这类早期内核最实用的排错手段因为早期内核的 panic 往往没有完整的寄存器 dump。5.4 Bochs 作为备选运行环境QEMU 跑不起来时Bochs 的调试器能力更强。Bochs 可以在引导扇区加载处下断点单步跟踪从实模式进入保护模式的完整过程。配置示例为bochsrc.txtfloppya: 1_44boot.img, statusinserted boot: a display_library: curses log: bochs.log启动后按 Ctrl-C 能进入 Bochs 调试器。输入b 0x7c00在引导扇区加载地址下断点然后c继续。到达断点后可以n单步用info reg查看寄存器。现代 RHEL 9 上装 Bochs 通常需要手动编译因为官方源里不一定有但其调试价值值得折腾一次。如果 QEMU 已经正常启动Bochs 可以跳过。6. 用 objdump 反汇编引导扇区验证跳转目标编译适配早期内核时最怕的不是报错而是编译通过了但运行完全没反应。一个最能暴露问题的验证手段是反汇编引导扇区。Linux 0.01 的bootsect.s是 16 位实模式代码BIOS 会把软盘第一个扇区加载到内存0x7c00处执行所以反汇编时要告诉 objdump 这是 16 位代码objdump -D -b binary -m i386 -Maddr16,data16 -Mintel boot.img | head -30输出中首先看到的是一条段间跳转指令。0.01 的bootsect.s开头处理完自身重定位后会用jmpi跳转到自身所在的段继续执行。如果 objdump 显示的跳转目标是0x07c0:0x0044这类落在0x7c00附近的地址说明 boot 的段地址设置正确如果是一条指向0x0000或0xffff的跳转则说明bootsect在构建时被注入了错误的重定位参数。更具体的方法是结合源码对指令序列。0.01 的bootsect.s开头先设置ax为启动盘号然后初始化ds和es段寄存器最后跳到go标签。反汇编后你能看到对应的mov和jmp指令。把这 20 行机器码和源码逐行对照能确认你构建出来的bootsect和你手里的bootsect.s是同一个东西而不是被编译器改写过语义的变体。如果想看带真实地址的 16 位代码可以用 ndisasmndisasm -o 0x7C00 -b 16 boot.img | head -30-o 0x7C00让 ndisasm 把文件偏移 0 解释为内存地址0x7c00这样反汇编输出里的地址就是 CPU 实际运行时看到的地址。对比 objdump 和 ndisasm 的差异还能发现 objdump 在解析某些早期汇编器生成的短跳转指令时可能出现的偏移差异。用这两种工具交叉验证基本能确定引导扇区是否有二义性指令问题。本文还有配套的精品资源点击获取
返回列表