别被“操作系统”四个字吓到。它的复杂度远低于你写一个网页组件。核心逻辑不到 40 行代码,只做三件事:数数、除二、往内存写字。
1. 真的这么简单吗?是的。
通常我们觉得“写操作系统”是超级计算机专家的事。但事实上,在计算机通电的最初瞬间,CPU 只做一件极其简单的事:从固定位置(0x7C00)取一条指令,然后执行它。
我们今天要做的,就是在这条“指令流”的开头,放上我们自己的代码。
全程只需要 5 个寄存器、5 个步骤、无复杂数据结构、无多层调用。 你甚至不需要懂汇编,跟着流程走一遍,你就能明白整个底层的奥秘。
2. 开发环境(极其简单,就两个工具)
我们不需要安装 Visual Studio 或 Eclipse,只需要两个命令行小工具:
- NASM:把我们的几十行汇编代码,“翻译”成 CPU 能懂的机器码。
- QEMU:一个轻量级虚拟机,用来“假装”是一台裸机,运行我们的代码。
安装命令(以 Ubuntu / WSL 为例,Mac 用 brew 同理):
sudo apt install nasm qemu-system-x86
不到 1 分钟,环境就绪。没有任何复杂的 IDE 配置。
3. 全部源码(核心逻辑不足 30 行)
新建文件 my_os.asm,把下面的代码复制进去。强烈建议你先完整看一遍,你会发现全是小学算术级别的东西:
[org 0x7C00] ; 固定加载地址(BIOS 规定)
[bits 16] ; CPU 初始模式(16位实模式)
; ---------- 常量定义 ----------
VGA_SEG equ 0xB800 ; 显存段地址(直接写这里就能显示文字)
ATTR equ 0x0A ; 颜色属性(0x0A = 亮绿色)
SCREEN_W equ 80 ; 屏幕一行 80 个字符
jmp main ; 跳过数据区
msg db "Hello World!", 0 ; 要显示的文本(以 0 结尾)
main:
xor ax, ax
mov ds, ax
mov es, ax ; 段寄存器归零,确保地址不出错
; ---- 第 1 步:数一下字符串有多长(不到 6 行汇编) ----
xor cx, cx
mov si, msg
str_len:
cmp byte [si], 0
jz len_done
inc cx
inc si
jmp str_len
len_done: ; 此时 CX = 12
; ---- 第 2 步:算一下从哪一列开始写(居中,就是做减法除 2) ----
mov ax, SCREEN_W
sub ax, cx ; AX = 80 - 12 = 68
shr ax, 1 ; 右移一位 = 除以 2,得 34(起始列)
; ---- 第 3 步:算出显存里的具体地址(行 × 80 + 列,再 × 2) ----
mov di, 12 * SCREEN_W ; 第 12 行开头
add di, ax ; 加上列号 34
shl di, 1 ; 乘以 2(每个字符占 2 字节)
; ---- 第 4 步:把显存段地址加载进 ES ----
mov ax, VGA_SEG
mov es, ax
mov si, msg ; 重置指针,准备打印
; ---- 第 5 步:循环写入(就 7 行汇编) ----
print:
cmp byte [si], 0
jz halt
mov al, [si]
mov byte [es:di], al ; 写 ASCII 码
mov byte [es:di+1], ATTR ; 写颜色属性
inc si
add di, 2
jmp print
halt:
jmp $ ; 死循环,系统停机
; ---------- 填充 0 并写入引导签名 ----------
times 510 - ($ - $$) db 0
dw 0xAA55
数一数:去掉注释、空行和宏定义,真正干活的汇编指令不到 35 行。这就是一个操作系统的全部内核代码!
4. 它为什么能称为“操作系统”?
因为它完全不依赖任何现有的软件环境:
- 没有调用
printf(那是 C 库的函数,依赖于操作系统)。 - 没有调用 BIOS 的显示中断(虽然可以,但我们没用)。
- 我们直接向物理内存地址
0xB800写入数据,这是最底层的硬件交互。
从 CPU 通电的那一刻起,到它进入 jmp $ 死循环,这台虚拟计算机的所有权完全由这 35 行代码掌控。它就是这台机器上唯一的、至高无上的控制程序。
5. 编译与运行(一条命令编译,一条命令运行)
编译成镜像
打开终端,进入 my_os.asm 所在目录,执行:
nasm -f bin my_os.asm -o my_os.bin
此时生成了一个仅 512 字节 的文件。
运行在裸机上(模拟)
qemu-system-x86_64 -drive format=raw,file=my_os.bin
一个窗口弹出来,屏幕第 12 行中央出现了绿色的 Hello World!。
恭喜!你的第一个操作系统成功启动。 整个过程生成的镜像文件比一张手机照片还小几百倍。
6. 复杂度评估:真的比写网页简单
我们用实打实的数据对比一下:
| 项目 | 本操作系统内核 | 普通网页组件 |
|---|---|---|
| 代码行数 | ~35 行汇编 | 通常 100+ 行 CSS + JS |
| 依赖环境 | NASM + QEMU(2 个工具) | Node.js / 浏览器 + 各种框架 |
| 调试难度 | 写错直接黑屏,定位极快 | 各种兼容性、异步回调、状态管理 |
| 运行载体 | 裸机 CPU | 依赖现有操作系统和浏览器 |
没有内存泄漏、没有垃圾回收、没有多线程锁、没有网络延迟。只有 CPU、内存、显存。这甚至比很多人第一次接触指针还简单。
7. 你想扩展它?依然很简单
既然有了这个最小内核,你可以在 halt 标签前(注意别覆盖签名)插入新的逻辑:
- 想显示其他文字:只改
msg那一行,居中算法自动适配长度。 - 想换个颜色:改
ATTR的值(0x07 是灰白,0x1F 是蓝底白字)。 - 想让文字闪烁:把属性高位置 1(例如 0x8A)。
如果觉得 512 字节不够用(确实不够放文件系统),只要在 halt 之前增加几行读磁盘的代码(int 0x13),就能把硬盘后续扇区的代码加载进内存并跳转执行。即使如此,核心调度代码依然只有几十行。
8. 常见问题(就三个,多了没有)
-
编译后不是 512 字节?
检查msg是否以, 0结尾,或者是否写入了空格导致长度变化,但times指令会自动补足,通常不会出错。 -
QEMU 黑屏无反应?
用hexdump -C my_os.bin | tail -n 2查看最后两字节是否为55 AA。如果不是,检查dw 0xAA55是否拼写错误。 -
文字显示乱码或错位?
检查shl di, 1这行是否被误删。如果漏掉,字符会重叠导致花屏。
9. 结语:你已掌握底层世界的第一把钥匙
今天你只用了不到 40 行汇编,就完成了一个完整操作系统内核的编写和运行。这个过程中,你直接和 CPU、内存、显存打了交道,中间没有隔任何第三方软件。
记住这个感觉:写操作系统并不总是庞杂晦涩的。在它的起点,逻辑极其简单、透明、可控。
如果你觉得有趣,下一步可以试着把这 35 行代码拆分成两个文件:一个负责“显存驱动”,一个负责“排版计算”——模块化从此刻开始。但无论怎么拆分,这份 “不依赖外部、自举运行” 的最小内核,永远是你代码世界的基石。
从零开始,就是这么简单。 🖥️