
一、动机:不是为了实用,而是为了"味道"
在现代操作系统里,内核、驱动、虚拟内存、用户态进程,这些概念都已经被包装得很"高级"。
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 会完成很小一部分初始化,然后按照固定流程:
- 扫描可启动设备(软盘、硬盘等)。
- 读取"被选中的那块盘"的第一个扇区(512 字节),加载到物理地址
0x7C00。 - 把控制权跳转到
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,于是搞出了一个折中方案:段寄存器 + 偏移量。
- 每个段寄存器(
CS、DS、SS等)保存一个 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通常携带返回值或错误码。
这个项目里的内核,核心就是一个"中断处理大开关":
- 保存所有通用寄存器的值;
- 读取
AH等寄存器,决定调用哪一个内部函数; - 调用文件系统、驱动层,完成实际操作;
- 把结果写回寄存器;
- 恢复寄存器并
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 程序,仅仅有内核还不够,至少还要几块拼图:
-
磁盘驱动
利用 BIOS 的磁盘中断来读写扇区,对上层暴露类似"读某柱面某扇区"的接口。
-
FAT12 文件系统实现
- 解析引导扇区(BPB);
- 读取 FAT 表,找到文件链;
- 实现按文件名查找、顺序读写文件内容。
-
一个基本的命令解释器
COMMAND.COM- 支持一小部分内建命令,例如
DIR、CD、TYPE; - 能够加载
.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 或真实机器上都可以启动。
构建过程一般包括以下几步:
- 编译 Bootloader(汇编)为 512 字节的二进制,引导扇区;
- 编译内核和各个工具程序(
COMMAND.COM等),生成纯二进制映像; - 创建一张新的 FAT12 镜像,格式化后,写入引导扇区;
- 使用工具(例如
mtools)把系统文件复制进镜像; - 用 QEMU 加载这张镜像,观察是否能顺利启动到命令行。
虚拟机部分可以用常见的开源模拟器来完成,例如:
QEMU 项目主页: https://www.qemu.org/
在虚拟机里调试好之后,再写入 U 盘,连接到支持 Legacy Boot 的真实 PC 上,就可以体验一次"现代硬件上启动类 DOS 系统"的完整流程。
八、写 DOS,有什么意义?
从实用性角度看,一个只能跑部分 DOS API 的小系统,显然无法和现代操作系统相比。
但这样的项目,仍然有几方面有趣的价值:
-
把抽象拆开重新认识
在主流教程里,"操作系统"往往从分页、内核态/用户态等概念讲起。
而 Real Mode DOS 逼着人重新面对"裸机":中断向量表、段寄存器、BIOS 调用,这些都变成了写代码时必须考虑的东西。
-
在 Rust 里练习"极限环境"编程
没有标准库、没有堆分配器、没有调试控制台,却仍然要写出可靠的代码。
这类约束很适合训练对所有权、生命周期、panic 机制的理解。
-
复古计算的乐趣
对很多开发者来说,第一次接触电脑的记忆,往往正是那种黑底绿字的界面。
亲手写出一个能跑老程序的小系统,有时候比再多一个 Web 框架、再多一套微服务还更让人放松。
对于对 DOS 生态本身有兴趣的读者,还可以顺带看看现代的兼容实现,比如 FreeDOS:
FreeDOS 官网: http://www.freedos.org/