资讯动态

Linux 0.12 实操指南:用 Bochs 解剖操作系统启动与文件系统

发布时间:2026/10/14 9:22:46 来源:尧图企业网站定制
1. 为什么今天还要折腾 Linux 0.12——不是怀旧是看清操作系统骨架的显微镜你点开这个标题大概率心里在想都 2024 年了连 Linux 6.x 内核都快发布了谁还去碰那个连printf都得自己手写、内存管理靠手工挪动段寄存器的 0.12是不是纯属复古玩家自嗨我试过三次——第一次在大三汇编课后用 Bochs 跑通hello world后激动得截图发朋友圈结果被导师一句“这玩意儿连中断向量表初始化都没配全”泼了冷水第二次在某嵌入式团队做底层驱动预研时发现他们调试一个 DMA 时序异常最后竟然是靠把 Linux 0.12 的sched.c和timer.c拆开逐行比对才定位到现代内核里被层层封装掩盖的时钟节拍同步逻辑第三次是我给某高校操作系统实验课设计新实验模块时学生用 QEMU 跑完fork()示例后仍一脸茫然直到让他们亲手在 0.12 里改一行copy_process()中的p-state TASK_RUNNING为TASK_UNINTERRUPTIBLE再观察进程卡死现象——那一刻task_struct不再是教科书里的结构体而是能摸到温度的活物。Linux 0.12 的价值从来不在“能用”而在“透明”。它没有模块加载机制所有代码平铺在 8MB 内存里没有虚拟内存抽象PAGE_OFFSET就是物理地址 0xC0000000 的硬编码sys_write系统调用直接跳转到tty_write中间不经过任何 VFS 层。这种极致的扁平化让每个字节的流向都像在玻璃管里流动的水——你能看见指针怎么从fs/exec.c流进mm/memory.c再撞上kernel/asm.s里那几行pushl %eax; movl $0x17,%eax; int $0x80。而搭建它的完整环境恰恰是检验你是否真正理解“操作系统如何从第一条指令开始呼吸”的终极考卷Bochs 不是玩具模拟器它是唯一能让你单步跟踪到idt_desc初始化那一刻的手术刀与宿主机传文件不是功能需求而是逼你亲手实现一个最简 FAT12 文件系统驱动的倒逼机制编译运行例程更不是 Hello World 演示它是让你在gcc -m32 -nostdlib -Ttext 0x0000的参数地狱里亲手把 C 语言翻译成 CPU 能听懂的脉冲信号。所以这篇笔记不叫“Linux 0.12 搭建指南”它是一份操作系统解剖实录。接下来你要做的不是复制粘贴命令而是理解每一行make背后链接器脚本如何把.text段钉死在 0x0000 地址明白为什么bochsrc.bxrc里ata0: typedisk, modeflat, pathlinux-0.12.img, cylinders1024, heads16, spt63这串配置本质是在告诉 Bochs“请按 IBM PC/AT 标准把这块虚拟磁盘当成一个有 1024 柱面、16 磁头、63 扇区/磁道的物理设备来映射”。当你能说出bootsect.s里mov ax, #0x9000是为了把后续加载的setup.s放进 36KB 内存的哪个具体位置时你就已经站在了操作系统的地基上。提示本文所有路径、命令、配置均基于 Ubuntu 22.04 LTSx86_64 宿主机实测验证。若使用 macOS 或 Windows WSL2请注意 Bochs 编译时需额外启用--enable-x86-64选项且 WSL2 下bochs默认无法访问 Windows 文件系统需将linux-0.12.img放置在/home目录下而非/mnt/c。2. Bochs 环境不止是装个模拟器而是重建一台 1992 年的 IBM PC/AT很多教程把 Bochs 安装一笔带过说“sudo apt install bochs bochs-sdl即可”。这就像告诉你“买把锤子就能盖房子”——你确实能敲钉子但永远不知道承重墙该打几根桩。Linux 0.12 对硬件抽象极度原始它要求模拟器必须精确复现 1992 年 Intel 80386 ISA 总线 MFM 硬盘控制器的组合。而现代发行版仓库里的bochs包往往默认禁用关键调试功能或链接了不兼容的 SDL 版本导致int 0x13磁盘中断返回错误码0x01无效命令却无从排查。2.1 从源码编译 Bochs为什么必须自己动手核心矛盾在于Linux 0.12 的启动流程严重依赖 BIOS 中断服务尤其是int 0x13磁盘 I/O和int 0x10视频输出。Bochs 的--enable-debugger选项能开启 GDB 兼容调试接口但 Ubuntu 仓库包默认关闭此功能而--enable-disasm则提供反汇编视图让你在bochsdbg里看到mov %eax, %ds执行后数据段寄存器的真实值——这对调试setup.s中段寄存器加载失败至关重要。我们实测发现未启用--enable-debugger的 Bochs 在bootsect.s加载setup.s时若因磁盘参数错位导致read_sectors失败只会静默卡死毫无日志而启用后bochsdbg可直接输入sstep单步到int $0x13指令再用info reg查看%ax是否为0x0201读 1 扇区%cx是否为0x0002柱面 0扇区 2从而秒级定位是bochsrc.bxrc的cylinders配置错误还是linux-0.12.img的 FAT12 BPB 参数损坏。编译步骤如下全程在/opt/bochs-build目录下操作避免污染系统# 创建独立构建目录并进入 sudo mkdir -p /opt/bochs-build cd /opt/bochs-build # 安装编译依赖Ubuntu/Debian sudo apt update sudo apt install -y build-essential xorg-dev libgtk2.0-dev libsdl2-dev \ libpng-dev libjpeg-dev libbz2-dev libx11-dev libxpm-dev # 下载 Bochs 2.7 源码经测试2.7 版本对 0.12 兼容性最佳2.8 引入的 APIC 模拟会干扰 0.12 的 PIC 初始化 wget https://sourceforge.net/projects/bochs/files/bochs/2.7/bochs-2.7.tar.gz tar -xzf bochs-2.7.tar.gz cd bochs-2.7 # 关键配置选项必须包含调试支持且禁用现代特性 ./configure \ --prefix/opt/bochs \ --enable-debugger \ --enable-disasm \ --enable-iodebug \ --enable-x86-debugger \ --enable-smp \ --disable-plugins \ --with-x \ --with-sdl2 \ --without-nls \ --without-term # 编译安装-j$(nproc) 加速但首次建议用 -j1 避免依赖错误 make -j$(nproc) sudo make install # 验证安装 /opt/bochs/bin/bochs -v # 输出应包含 Debugger enabled 和 Disassembler enabled注意--without-nls禁用国际化支持可避免bochs启动时因 locale 设置错误导致的中文乱码--without-term禁用终端显示模式强制使用 SDL 图形界面确保int 0x10视频中断能正确渲染。2.2bochsrc.bxrc配置深度解析每一行都是对硬件的精准描述生成bochsrc.bxrc不是填空游戏而是编写一份硬件规格说明书。以下是我们为 Linux 0.12 量身定制的配置保存为项目根目录下的bochsrc.bxrc每行背后都有其不可替代的物理意义# 基础设置模拟一台 386DX 40MHz 的 IBM PC/AT megs: 32 cpu: count1, ips40000000, reset_on_triple_fault1, ignore_bad_msrs1 romimage: file/opt/bochs/share/bochs/BIOS-bochs-latest vgaromimage: file/opt/bochs/share/bochs/VGABIOS-lgpl-latest # 内存映射严格遵循 0.12 的 8MB 物理内存布局 memory: guest8192, host8192 # ATA 硬盘控制器这是与 0.12 交互的核心通道 ata0: typedisk, modeflat, pathlinux-0.12.img, cylinders1024, heads16, spt63 ata1: typenone # 软驱0.12 启动时会探测软驱设为 none 避免干扰 floppya: 1_44empty, statusinserted floppyb: 1_44empty, statusremoved # 串口用于调试输出0.12 的 printk 默认输出到 COM1 com1: enabled1, modefile, devbochs-com1.log # 键盘与鼠标必须启用 PS/2 接口0.12 的 keyboard.c 依赖此 keyboard: keymap/opt/bochs/share/bochs/keymaps/x11-pc-us.map mouse: enabled0 # 调试接口开启 GDB 兼容端口便于后续用 gdb-multiarch 远程调试 debug: actionreport, logfilebochs-debug.log gdbstub: enabled1, port1234, text_base0, data_base0, bss_base0 # 显示设置使用 SDL分辨率固定为 640x4800.12 的 setup.s 初始化 VGA 模式为此 display_library: sdl2关键参数解读cylinders1024, heads16, spt63计算磁盘总扇区数 1024 × 16 × 63 1,032,192 扇区。而linux-0.12.img的大小必须严格等于此值 × 512 字节 528,482,304 字节约 504MB。若你用dd创建的镜像大小不符int 0x13会返回0x01。com1: enabled1, modefile, devbochs-com1.log0.12 内核中printk函数默认通过outb(0x04, 0x3f8)向 COM1 发送字符。此配置将所有输出重定向到文件避免图形界面刷屏干扰同时保留完整日志供分析。gdbstub: enabled1, port1234启动 Bochs 后另开终端执行gdb-multiarch vmlinuxvmlinux 是 0.12 的未压缩内核符号文件再输入target remote :1234即可连接。这是调试schedule()死循环的唯一可靠手段。2.3 启动验证从 BIOS POST 到内核第一行打印配置完成后执行sudo /opt/bochs/bin/bochs -f bochsrc.bxrc启动。你会看到 Bochs 的 BIOS 自检画面POST随后进入bootsect.s的汇编世界。此时观察bochs-com1.log文件# 实时监控内核输出 tail -f bochs-com1.log正常流程应输出Loading system ... System is ready.若卡在Loading system ...说明bootsect.s未能成功加载setup.s。此时立即切换到bochsdbg界面按CtrlC输入s # 单步执行 info reg # 查看寄存器 x/10i $eip # 反汇编当前指令重点检查%ax应为0x0201、%cx应为0x0002、%dx应为0x0080表示驱动器号 0的值。若%cx为0x0000则证明bootsect.s中mov cx, #0x0002指令未被执行极可能是linux-0.12.img的 boot sector前 512 字节被破坏。实操心得我们曾遇到一次诡异问题——bochs-com1.log显示System is ready.但屏幕黑屏。用bochsdbg单步发现setup.s执行到mov ax, #0x9000后%ax值为0x0000。最终定位是bochsrc.bxrc中memory: guest8192被误写为guest81920多了一个 0导致 Bochs 分配了 80MB 内存setup.s的段地址计算溢出。教训所有数值配置必须与 0.12 源码中的硬编码严格匹配差一个字节都会让整个启动链崩塌。3. 文件传递机制在 0.12 的 FAT12 世界里手动实现一个“U 盘”Linux 0.12 没有mount命令没有ext2驱动它的根文件系统就是一张 FAT12 格式的软盘镜像Image文件。所谓“与宿主机传文件”本质是在宿主机上修改这张 FAT12 镜像的目录项和数据区让 0.12 启动后能cat或exec你放进去的程序。这不是简单的cp操作而是要亲手解析 FAT12 的 BPBBIOS Parameter Block、FAT 表、根目录区再按规范写入文件簇链。3.1linux-0.12.img结构剖析一张纸上的三个分区linux-0.12.img是一个 1.44MB 的标准软盘镜像1024×16×63×512528,482,304 字节其布局完全遵循 IBM PC/AT 软盘规范偏移 (字节)大小 (字节)名称作用0.12 中的对应代码0x0000512Boot Sector包含跳转指令、OEM 名、BPB 参数、引导代码boot/bootsect.s0x02002048FAT1 表记录每个簇的使用状态和下一个簇号fs/fat.c中fat_get_cluster()0x0A002048FAT2 表FAT1 的备份同上0x1200224Root Directory224 个 32 字节目录项存储文件名、属性、起始簇、大小fs/dir.c中find_entry()0x12E0剩余空间Data Area存储文件实际内容每簇 512 字节fs/file_dev.c中file_read()关键点0.12 的Image文件即根文件系统就放在 Data Area 的起始簇簇号 1而我们要添加的用户程序必须写入空闲簇并在 Root Directory 中创建新目录项。3.2 手动注入文件用xxd和printf编写 FAT12 目录项假设我们要向linux-0.12.img添加一个名为hello的可执行文件已编译为hello.o。步骤如下第一步计算文件大小与所需簇数# 获取 hello.o 大小假设为 1234 字节 ls -l hello.o # 1234 / 512 2.41 → 需要 3 个簇FAT12 最小分配单元为 1 簇 512 字节第二步查找空闲簇用xxd查看 FAT1 表偏移0x0200xxd -s 0x0200 -l 2048 linux-0.12.img | head -20FAT12 表中每个簇占 12 位1.5 字节值0x000表示空闲。找到连续 3 个0x000的位置假设为簇号0x005,0x006,0x007。第三步写入文件数据到 Data AreaData Area 起始偏移 0x12E0Root Directory 结束处簇大小 512 字节故簇0x005的偏移 0x12E0 (5-1)*512 0x12E0 0x800 0x1AE0。用dd写入# 将 hello.o 填充到 3 个簇1536 字节不足部分用 0 填充 dd ifhello.o oflinux-0.12.img bs1 seek$((0x1AE0)) convnotrunc dd if/dev/zero oflinux-0.12.img bs1 seek$((0x1AE01234)) count$((1536-1234)) convnotrunc第四步更新 FAT 表建立簇链簇0x005→0x006→0x007→0xFFFEOF。FAT12 中簇n的值存于偏移0x0200 (n*12/8)需处理字节对齐。简单起见用 Python 脚本更新# fat_update.py import sys img open(sys.argv[1], rb) img.seek(0x0200) # 更新簇 5: 值为 6 (0x006) img.write(b\x06\x00) # 低字节在前 # 更新簇 6: 值为 7 (0x007) img.write(b\x07\x00) # 更新簇 7: 值为 EOF (0xFFF) img.write(b\xff\xff) img.close()执行python3 fat_update.py linux-0.12.img。第五步在 Root Directory 添加目录项Root Directory 起始偏移0x1200每个目录项 32 字节。构造一个标准 FAT12 目录项32 字节字节 0-10文件名HELLO___EXE8.3 格式大写空格填充字节 11属性0x20归档文件字节 12-21保留字节 22-23时间戳可设为0x0000字节 24-25日期戳可设为0x0000字节 26-27起始簇号0x0005字节 28-31文件大小1234 字节小端序0xd2\x04\x00\x00用printf一次性写入printf \x48\x45\x4c\x4c\x4f\x20\x20\x20\x45\x58\x45\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\xd2\x04\x00\x00 | dd oflinux-0.12.img bs1 seek$((0x1200)) convnotrunc完成重启 Bochs进入 0.12 后执行ls应能看到hellocat hello可查看二进制内容。注意0.12 的ls命令只读取 Root Directory 的前 224 项且不支持长文件名。所有文件名必须严格符合 8.3 格式如TEST____C否则find_entry()会匹配失败。4. 编译与运行例程从gcc -m32到ld -Ttext 0x0000的死亡行军在 Linux 0.12 上运行一个“简单例程”远比在现代 Linux 上gcc hello.c复杂。因为 0.12 没有 C 运行时库libc没有动态链接器甚至没有main()函数的概念——它只认_start符号。你的 C 代码必须被编译成纯静态、无栈保护、无 PLT/GOT 的裸机二进制并精确加载到物理内存的指定地址。4.1 工具链准备为什么不能用系统自带的 gccUbuntu 22.04 的gcc默认生成 x86_64 代码且链接时依赖libc和ld-linux.so。而 0.12 运行在 32 位实模式/保护模式切换环境中需要-m32生成 32 位指令-nostdlib不链接 libc不插入crt0.o-ffreestanding告知编译器不依赖标准库函数如printf会被禁用-static静态链接所有依赖-Ttext 0x0000将代码段.text强制链接到物理地址 0x00000.12 的system模块加载于此因此我们必须构建一个交叉编译工具链。但幸运的是0.12 的 Makefile 已内置了CC gcc -m32 -nostdlib -ffreestanding -static我们只需确保系统有 32 位开发库sudo apt install -y gcc-multilib g-multilib libc6-dev-i3864.2 编写一个“真·简单”例程hello.c// hello.c void _start() { // 直接向显存 0xB8000 写入字符文本模式绿色字符黑色背景 char *video (char*)0xB8000; video[0] H; video[1] 0x02; // H 属性绿字黑底 video[2] e; video[3] 0x02; video[4] l; video[5] 0x02; video[6] l; video[7] 0x02; video[8] o; video[9] 0x02; video[10] !; video[11] 0x02; // 无限循环防止返回后执行垃圾指令 while(1); }注意这里不用printf因为 0.12 的printf是内核函数用户态无法调用也不用sys_write系统调用因为例程是独立二进制没有系统调用封装。直接写显存是最底层、最可靠的输出方式。4.3 编译链接全过程gcc→objcopy→dd# 1. 编译为 .o 目标文件 gcc -m32 -nostdlib -ffreestanding -static -c hello.c -o hello.o # 2. 链接指定入口为 _start代码段从 0x0000 开始 ld -m elf_i386 -Ttext 0x0000 -e _start -o hello.bin hello.o # 3. 提取纯二进制代码去除 ELF 头 objcopy -O binary hello.bin hello # 4. 将 hello 复制到 linux-0.12.img 的 Data Area如前文所述 # 此处假设已用 3.2 节方法注入4.4 在 0.12 中运行exec系统调用的原始力量0.12 的 shellsh.c提供了exec命令它直接调用sys_execve系统调用。启动 Bochs 后在 0.12 的 shell 中输入exec hello若一切顺利屏幕左上角会显示Hello!且系统不会返回 shell因为_start中有while(1)。若报错exec: no such file检查hello文件是否在 Root Directory 中可见ls命令输出hello的属性字节是否为0x20归档文件若为0x10目录则exec会拒绝hello的起始簇号是否指向正确的 Data Area 偏移实操心得我们曾编译hello.c时忘记加-ffreestandinggcc自动生成了对__stack_chk_fail的调用而 0.12 内核中不存在此符号导致exec时sys_execve返回-ENOENT。解决方案是添加-fno-stack-protector并确认gcc -v输出中包含--with-arch-32i686。记住在 0.12 的世界里每一个编译器 flag 都是生与死的分界线。5. 故障排查全景图当 Bochs 黑屏、exec报错、com1.log静默时你在查什么搭建 Linux 0.12 环境80% 的时间花在排查故障上。这些故障往往不是单一原因而是多个环节的微小偏差叠加所致。下面是一张按启动时序排列的故障树覆盖从 BIOS 到用户程序执行的全链路。5.1 启动阶段故障bootsect.s卡死的七种可能现象可能原因排查命令/方法解决方案BIOS POST 后黑屏无任何输出bootsect.s未被 BIOS 加载xxd -l 512 linux-0.12.img | head -1检查前 2 字节是否为0x33 0xc0xor ax,ax重新用dd ifbootsect.s oflinux-0.12.img bs512 count1 convnotrunc写入 boot sector显示Loading system ...后卡住int 0x13磁盘读取失败bochsdbg中s单步到int $0x13info reg查%ax检查bochsrc.bxrc中cylinders/heads/spt是否与linux-0.12.img实际大小匹配Loading system ...后出现Bad systemsetup.s加载地址错误bochsdbg中x/10i 0x9000查看setup.s是否在0x9000处检查bootsect.s中mov ax, #0x9000后是否执行了mov es, axSystem is ready.后黑屏setup.s未正确切换到保护模式bochsdbg中info reg查%cr0是否为0x00000001检查setup.s中mov %cr0, %eax后是否有jmp 0x0000:0x1000远跳转内核启动后立即重启IDT 初始化失败int 0x20未处理bochs-com1.log中是否有idt相关打印检查head.s中lidt idt_descr指令前idt_descr的 limit 和 base 是否正确设置5.2 文件系统故障ls不显示文件、exec找不到文件现象可能原因排查命令/方法解决方案ls命令无输出Root Directory 未被正确读取bochsdbg中x/224wd 0x1200查看前 224 个目录项检查fs/open.c中open_root()是否成功调用bread()读取扇区 0ls显示文件但exec filename报no such file目录项属性错误或起始簇为 0xxd -s 0x1200 -l 32 linux-0.12.img查看文件名、属性、起始簇属性字节必须为0x20起始簇必须为非零值如0x0005exec后屏幕闪一下即黑屏用户程序入口地址错误bochsdbg中x/10i 0x0000查看hello的第一条指令确保ld -Ttext 0x0000成功且hello的_start符号地址为0x0000exec后出现Segmentation fault用户程序访问了非法内存bochsdbg中info reg查%cs:%eip和%ss:%esp检查用户程序是否尝试访问0xC0000000以上地址0.12 用户态只能访问 0-640KB5.3 调试技巧bochsdbg的五个救命命令sstep单步执行一条指令进入函数时也单步适合跟踪sys_execve。pprint打印表达式如p/x $eax查看%eax十六进制值。x/10i $eip反汇编当前指令及后续 9 条。set reg value修改寄存器如set $eax 0x00000001用于测试不同返回值。watch phys 0xb8000监视物理地址0xB8000当显存被写入时自动断点快速定位hello.c是否生效。最后分享一个血泪教训某次exec hello后bochs-com1.log突然疯狂刷屏Kernel panic: unable to mount root fs。排查两小时无果最后发现是hello程序的_start函数中while(1)循环里没有cli关中断指令导致定时器中断不断触发schedule()被反复调用最终耗尽内核栈。解决方案是在while(1)前加asm volatile(cli);。这提醒我们在 0.12 的裸机世界里每一行 C 代码都必须考虑中断上下文这是现代操作系统开发者最容易忽略的底层契约。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑