用 Rust 复刻 Real Mode 世界

一、动机:不是为了实用,而是为了"味道"

在现代操作系统里,内核、驱动、虚拟内存、用户态进程,这些概念都已经被包装得很"高级"。

CPU 开机就在 Real Mode,内存没有保护,任意程序都可以写到任意地址,系统调用就是一次 int 21h 软件中断。

这个项目的目标很直接:

  • 在真实的 x86 机器上,以 Real Mode 运行;
  • 实现一部分经典 DOS API,让简单的 DOS 程序可以直接跑;
  • 提供最基础的功能:磁盘读写、FAT12 文件系统、简单的 COMMAND.COM 命令行。

换句话说,它不是要成为一个"新系统",而是要模仿当年的 MS-DOS,让现代开发者可以亲手体验那种"程序和硬件几乎零距离"的感觉。

如果有人想先回顾一下 Rust 这门语言本身,可以在中途顺手翻一眼官方站点:

Rust 官网: https://www.rust-lang.org/


二、从 BIOS 开机到 0x7C00:Bootloader 的工作

80 年代的 PC 上电之后,首先接管机器的是 BIOS

BIOS 会完成很小一部分初始化,然后按照固定流程:

  1. 扫描可启动设备(软盘、硬盘等)。
  2. 读取"被选中的那块盘"的第一个扇区(512 字节),加载到物理地址 0x7C00
  3. 把控制权跳转到 0x7C00------从这时起,就轮到 Bootloader 自己表演。

这个项目里的 Bootloader 干的事情,大体可以概括为:

  • 用 BIOS 的中断服务(比如 int 13h)从磁盘读数据;
  • 把内核和驱动一次性装进内存;
  • 设置好段寄存器和栈;
  • 跳转到内核入口。

一个典型的 Real Mode 启动头,伪代码大概类似这样:

asm 复制代码
; 以 0x7C00 为起点的启动扇区
[org 0x7C00]

cli                 ; 关中断
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00      ; 暂时把栈放在这里

; 这里调用 BIOS int 13h 读取更多扇区到内存
; ...

jmp 0x0000:0x8000   ; 跳到载入好的内核入口
times 510-($-$$) db 0
dw 0xAA55           ; 引导扇区签名

在现代 UEFI 世界里,上述很多事情已经被抽象掉;但在 Real Mode 里,这些步骤必须自己完成。


三、Real Mode:内存分段的"古早麻烦"

DOS 必须在 Real Mode 下工作,这是向 8086 时代兼容的历史包袱。

Real Mode 只有 16 位寄存器,看起来最多只能寻址 64 KB。

但 Intel 当年的设计希望能访问 1 MB,于是搞出了一个折中方案:段寄存器 + 偏移量

  • 每个段寄存器(CSDSSS 等)保存一个 16 位"段基址";
  • 实际物理地址 = 段值左移 4 位 + 偏移量。

例如:

段地址 0x2020,偏移 0x4300

对应物理地址 = 0x2020 * 16 + 0x4300 = 0x24500

同一个物理地址,可以有很多种"段:偏移"的写法,这给上下文切换、加载程序、传参数都带来了额外负担。

而 DOS API 又大量依赖"段:偏移"的约定,例如把缓冲区地址放在 ES:BX 里传递,因此内核不得不和这些细节打交道。

这也是为什么现代操作系统教程几乎清一色建议"尽快切换到 Protected Mode"------那里有平坦的 32 位地址空间,还有分页、权限管理等一整套机制,而这些在 Real Mode 里完全不存在。


四、用 Rust 写一个 Real Mode 内核:no_std + 静态链接

项目选择 Rust 作为内核开发语言,一方面是出于对现代语言特性的偏爱,另一方面也想看看 Rust 在极简环境下能走多远。

为了在没有操作系统支持、没有 C 运行时的情况下运行,内核必须:

  • 禁用标准库:#![no_std]
  • 禁用默认入口点:#![no_main]
  • 自己提供入口函数和 panic 处理逻辑。

一个最简骨架,可以写成这样的形式:

rust 复制代码
#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn kstart() -> ! {
    // 在这里完成内核初始化,然后进入主循环
    loop {
        // ...
    }
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    // Real Mode 下没有日志系统,只能挂死
    loop {}
}

所有代码都被编译成 静态可执行映像 ,不依赖动态重定位。

原因很简单:当内核被加载进内存时,没有别的程序可以帮忙修改符号地址,也没有 ELF/PE 加载器可用,一切都必须"编译期说清楚"。

这带来一个有趣的副作用:

许多会在运行时触发 panic 的操作(例如切片越界、除以零)会导致编译器拒绝生成代码。

为了通过编译,开发者必须在可疑位置手动加上边界检查,反而逼出了更"安全"的写法。

至于如何在 Rust 里写操作系统,网上已经有非常详细的教程,例如 Philipp Oppermann 的系列文章就专门讲这一套流程:

Rust 写 OS 教程示例: https://os.phil-opp.com/


五、内核的角色:一个巨大的 int 21h 分发器

在 DOS 兼容世界里,系统调用主要通过 软件中断 0x21 实现。

调用约定大致是:

  • AH 放子功能号;
  • 其他寄存器(AL, BX, CX, DX, DS:DX, ES:BX 等)组合成参数;
  • 中断返回后,AX 通常携带返回值或错误码。

这个项目里的内核,核心就是一个"中断处理大开关":

  1. 保存所有通用寄存器的值;
  2. 读取 AH 等寄存器,决定调用哪一个内部函数;
  3. 调用文件系统、驱动层,完成实际操作;
  4. 把结果写回寄存器;
  5. 恢复寄存器并 iret,把控制权还给用户程序。

为了在中断之间保持状态(例如当前目录、当前磁盘),内核里还需要一块"全局工作区"。

在 Rust 代码里,这通常长这样:

rust 复制代码
// 注意:static mut 在 Rust 里属于不安全用法
static mut CURRENT_DRIVE: u8 = 0;
static mut CURRENT_DIR: [u8; 64] = [0; 64];

pub fn set_current_drive(d: u8) {
    unsafe {
        CURRENT_DRIVE = d;
    }
}

这些 static mut 变量本质上和传统汇编里的"全局内存区"是一个思路:

编译期就决定好它们的地址,系统调用之间靠它们共享状态。

在单任务、单线程的 Real Mode DOS 里,这种方式足够简单直接。


六、FAT12、磁盘驱动和 COMMAND.COM

为了跑起真正的 DOS 程序,仅仅有内核还不够,至少还要几块拼图:

  1. 磁盘驱动

    利用 BIOS 的磁盘中断来读写扇区,对上层暴露类似"读某柱面某扇区"的接口。

  2. FAT12 文件系统实现

    • 解析引导扇区(BPB);
    • 读取 FAT 表,找到文件链;
    • 实现按文件名查找、顺序读写文件内容。
  3. 一个基本的命令解释器 COMMAND.COM

    • 支持一小部分内建命令,例如 DIRCDTYPE
    • 能够加载 .COM 程序,把它放进内存某个固定位置,把 CS:IP 调整过去,然后执行。

一个典型的 COM 程序加载流程大致是:

text 复制代码
1. 打开指定文件
2. 在某个段(例如 0x1000:0x0000)预留足够空间
3. 读文件内容到该段的偏移 0x0100 起(保留 PSP)
4. 构造 PSP(Program Segment Prefix)
5. 设置 CS=DS=ES=目标段,IP=0x0100
6. 用一次 far jump 把控制权交给用户程序

七、构建流程:把 Rust 内核塞进一张软盘镜像

整个系统最终被打包成一张 FAT12 软盘镜像,在 QEMU 或真实机器上都可以启动。

构建过程一般包括以下几步:

  1. 编译 Bootloader(汇编)为 512 字节的二进制,引导扇区;
  2. 编译内核和各个工具程序(COMMAND.COM 等),生成纯二进制映像;
  3. 创建一张新的 FAT12 镜像,格式化后,写入引导扇区;
  4. 使用工具(例如 mtools)把系统文件复制进镜像;
  5. 用 QEMU 加载这张镜像,观察是否能顺利启动到命令行。

虚拟机部分可以用常见的开源模拟器来完成,例如:

QEMU 项目主页: https://www.qemu.org/

在虚拟机里调试好之后,再写入 U 盘,连接到支持 Legacy Boot 的真实 PC 上,就可以体验一次"现代硬件上启动类 DOS 系统"的完整流程。


八、写 DOS,有什么意义?

从实用性角度看,一个只能跑部分 DOS API 的小系统,显然无法和现代操作系统相比。

但这样的项目,仍然有几方面有趣的价值:

  1. 把抽象拆开重新认识

    在主流教程里,"操作系统"往往从分页、内核态/用户态等概念讲起。

    而 Real Mode DOS 逼着人重新面对"裸机":中断向量表、段寄存器、BIOS 调用,这些都变成了写代码时必须考虑的东西。

  2. 在 Rust 里练习"极限环境"编程

    没有标准库、没有堆分配器、没有调试控制台,却仍然要写出可靠的代码。

    这类约束很适合训练对所有权、生命周期、panic 机制的理解。

  3. 复古计算的乐趣

    对很多开发者来说,第一次接触电脑的记忆,往往正是那种黑底绿字的界面。

    亲手写出一个能跑老程序的小系统,有时候比再多一个 Web 框架、再多一套微服务还更让人放松。

对于对 DOS 生态本身有兴趣的读者,还可以顺带看看现代的兼容实现,比如 FreeDOS:

FreeDOS 官网: http://www.freedos.org/

相关推荐
爱吃烤鸡翅的酸菜鱼3 小时前
如何用【rust】做一个命令行版的电子辞典
开发语言·rust
不爱学英文的码字机器4 小时前
Rust 并发实战:使用 Tokio 构建高性能异步 TCP 聊天室
开发语言·tcp/ip·rust
朝九晚五ฺ7 小时前
用Rust从零实现一个迷你Redis服务器
服务器·redis·rust
Amos_Web7 小时前
Rust实战(三):HTTP健康检查引擎 —— 异步Rust与高性能探针
后端·架构·rust
是店小二呀9 小时前
使用Rust构建一个完整的DeepSeekWeb聊天应用
开发语言·后端·rust
是店小二呀1 天前
五分钟理解Rust的核心概念:所有权Rust
开发语言·后端·rust
IT_Beijing_BIT1 天前
Rust入门
开发语言·后端·rust
q***23921 天前
数据库操作与数据管理——Rust 与 SQLite 的集成
数据库·rust·sqlite
百锦再1 天前
第15章 并发编程
android·java·开发语言·python·rust·django·go