从零开始写操作系统内核:只需几十行汇编,512 字节搞定裸机启动

2 阅读6分钟

别被“操作系统”四个字吓到。它的复杂度远低于你写一个网页组件。核心逻辑不到 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. 常见问题(就三个,多了没有)

  1. 编译后不是 512 字节?
    检查 msg 是否以 , 0 结尾,或者是否写入了空格导致长度变化,但 times 指令会自动补足,通常不会出错。

  2. QEMU 黑屏无反应?
    hexdump -C my_os.bin | tail -n 2 查看最后两字节是否为 55 AA。如果不是,检查 dw 0xAA55 是否拼写错误。

  3. 文字显示乱码或错位?
    检查 shl di, 1 这行是否被误删。如果漏掉,字符会重叠导致花屏。


9. 结语:你已掌握底层世界的第一把钥匙

今天你只用了不到 40 行汇编,就完成了一个完整操作系统内核的编写和运行。这个过程中,你直接和 CPU、内存、显存打了交道,中间没有隔任何第三方软件。

记住这个感觉:写操作系统并不总是庞杂晦涩的。在它的起点,逻辑极其简单、透明、可控。

如果你觉得有趣,下一步可以试着把这 35 行代码拆分成两个文件:一个负责“显存驱动”,一个负责“排版计算”——模块化从此刻开始。但无论怎么拆分,这份 “不依赖外部、自举运行” 的最小内核,永远是你代码世界的基石。

从零开始,就是这么简单。 🖥️