Rust OS 入门:VGA 字符模式——往一块内存写字符,屏幕就有字了

内核启动后,屏幕上出现了 Hello, World!——在还没有文件系统、设备驱动和 std 的情况下,这靠的是往一块特殊内存(0xB8000)里写字符,由 VGA 硬件读取并显示出来。这篇讲的就是这块内存和 VGA 的工作方式。

本文是《Writing an OS in Rust》(https://os.phil-opp.com/)入门系列的一篇,跟着你刚跑通的 cargo bootimage 往下走,解释教程里 println! 的第一站——VGA 文本模式。它正好用上《Rust OS 入门:物理地址 ≠ RAM 地址》里讲的 MMIO:0xB8000 就是读写内存 = 操作硬件最经典的实例。

📚 基本概念速读

名称 定义 省流
VGA 字符模式 早期文字版显示器,屏幕是字符格不是像素 80×25 的字符画布
显存(video memory) 显卡能读取的一块特殊内存 CPU 写它 = 显卡读它
0xB8000 VGA 文本模式显存起始地址 往这写,屏幕就变
属性字节(attribute) 描述字符颜色(前景+背景)的字节 每个字符配 1 字节颜色
字体表(font) VGA 内部字符编号→像素图案的查表 硬件级字符渲染器
MMIO 把设备寄存器/显存映射进地址空间 写内存 = 操作硬件
Framebuffer 现代显卡:CPU 直接控制每个像素 像素级画布

🖥️ VGA 字符模式是什么

先把它想象成一个非常早期的文字版显示器:屏幕不是由你直接控制每一个像素,而是被划分成一个个字符格子:

1
2
3
4
5
┌────┬────┬────┬────┬────┐
│ H │ e │ l │ l │ o │
├────┼────┼────┼────┼────┤
│ W │ o │ r │ l │ d │
└────┴────┴────┴────┴────┘

经典 VGA 文本模式是 80 列 × 25 行。也就是说,你不需要告诉显卡”把 (100, 50) 这个像素变成白色”,只需要告诉它:

“第 3 行第 10 列,显示字符 A,颜色白色。”

这就是字符模式(text mode)。

🎯 CPU 怎么告诉 VGA 显示 A:0xB8000

这就用到了显存(video memory)。经典 PC 上,VGA 文本模式的显存起始地址是:

1
0xB8000

CPU 可以像访问普通内存一样访问它:

1
2
let ptr = 0xb8000 as *mut u8;
*ptr = b'A'; // 往 0xB8000 写 0x41

于是:

1
2
3
4
5
6
7
8
9
CPU
│
│ 写入 0x41 ('A')
↓
0xB8000(VGA 显存)
│
│ VGA 显卡读取这块内存
↓
屏幕显示 A

要注意的一点:

CPU 写的不是屏幕,而是一块会被 VGA 硬件特殊解释的内存。

这正是我们在《物理地址 ≠ RAM 地址》里讲的 MMIO:0xB8000 不是普通 RAM,而是映射到 VGA 硬件的显存区域。写它不像写普通内存那样只是存数据,而是给显卡下达显示指令。

🔤 为什么一个字符占两个字节

因为除了保存显示什么字符,还要保存用什么颜色显示。所以一个字符单元是 2 字节:

1
2
3
4
┌──────────────┬──────────────┐
│ char ASCII │ attribute │
└──────────────┴──────────────┘
ASCII 字符(1 byte) 颜色属性(1 byte)

例如 0x41 0x0F = 字符 'A' + 颜色属性 0x0F(白字黑底)。于是整块显存的排布是:

1
2
3
0xB8000 ─ 字符 'A' │ 颜色      ← 第 0 个字符
0xB8002 ─ 字符 'B' │ 颜色 ← 第 1 个字符
0xB8004 ─ 字符 'C' │ 颜色 ← 第 2 个字符

因为每个字符 2 字节,地址步长是 2:第 0 个字符在 0xB8000、第 1 个在 0xB8002、第 2 个在 0xB8004……

🔢 从字符到像素:VGA 是硬件级字符渲染器

CPU 写进去的 0x41 只是一个数字,VGA 并不会直接把它当成像素。VGA 内部有一张字体表(font),存着每个字符的位图:

1
2
3
4
5
6
7
8
字符 'A' 的字形(0/1 点阵)
01110
10001
10001
11111
10001
10001
10001

这些 0/1 最终对应成像素(前景色/背景色):

1
2
3
4
5
6
7
 ███
█ █
█ █
█████
█ █
█ █
█ █

完整链路:

1
2
CPU → 写入 'A' (0x41) → VGA 显存 → 字体表查 'A' 的像素图案
→ VGA 硬件按前景/背景色上色 → 显示器

所以字符模式本质是一个硬件帮你做好的字符渲染器——你只管给字符编号和颜色,画像素是显卡的活。

📐 整块显存有多大

80 × 25 = 2000 个字符,每个 2 字节 → 2000 × 2 = 4000 bytes ≈ 4 KB。整屏内容就这么多。

第 row 行、第 col 列的位置公式:

1
offset = (row × 80 + col) × 2

举例:第 0 行第 0 列 → 0;第 0 行第 1 列 → 2;第 1 行第 0 列 → 160(因为一行 80 字符 × 2 字节)。所以写屏本质上就是按公式往 0xB8000 + offset 写字节。

🖨️ 为什么操作系统刚启动时特别喜欢它

因为刚启动的内核什么都没有:没有文件系统、没有进程、没有网络、没有显卡驱动、甚至还没有完整的内存管理。但你想要调试输出:

1
println!("kernel started!");

VGA 字符模式只需要:

1
写几个内存地址 → VGA → 屏幕

这比操作现代 GPU 简单几个数量级,所以它是早期内核最实用的调试打印屏——kernel started!、initializing memory...、initializing interrupts... 都是这么一行行写出来的。

🧱 Rust OS 里的 Writer:println! 背后是什么

你可能在教程里看到过这样的结构:

1
2
3
4
5
struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer, // 指向 0xB8000 的显存
}

以及:

1
2
3
4
5
6
7
8
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
for byte in s.bytes() {
self.write_byte(byte);
}
Ok(())
}
}

这里的 buffer 本质上就指向 0xB8000,write_byte 做的就是把字符 + 颜色写进显存、必要时滚动屏幕:

1
2
3
4
5
6
7
8
9
println!("Hello")
↓ Rust 格式化(fmt)
Writer::write_str
↓ 逐个字符
write_byte(把 char+颜色 写进 0xB8000)
↓ store 指令(MMIO)
VGA 硬件
↓
屏幕

所以:

println! 能显示,不是 Rust 会显示文字,而是你实现了一个 Writer,把字符串转换成了对 VGA 显存的写操作。

(顺带一提,这正是 #![no_std] 下 println! 用不了的直接原因——宏靠 std::io,而裸机内核必须自己造一个写屏通道。还记得《从 main 到 _start》里说”OS kernel 完全可以自己实现 print!“吗?这就是那个实现。)

⚖️ VGA 字符模式 vs Framebuffer

VGA 字符模式 Framebuffer
你告诉显卡 “这里显示字符 A” “这个像素是 RGB(255,0,0)”
字体渲染 显卡查字体表帮你做 你自己写字体渲染逻辑
粒度 字符 像素
1
2
VGA Text Mode:      CPU → 字符 → VGA(帮你渲染)→ 屏幕
Framebuffer: CPU → 像素 → Framebuffer → 屏幕(全自己控制)

要画一个 A:字符模式写一个字节就完事;framebuffer 需要自己'A' → 字体库 → bitmap → 逐像素写入。

🕰️ 为什么现在基本不用它了

因为它能力太有限:80×25、纯字符、几种颜色。而现代桌面需要 1920×1080(约 200 万像素)、窗口、图片、动画、视频、GPU 加速——现代操作系统走的是:

1
GPU → Framebuffer / 显示驱动 → 显示器

而不是:

1
0xB8000 → VGA Text Mode

(顺带:0xB8000 这块地址今天依然存在——BIOS 固件和早期启动阶段还在用文本模式显示 POST 信息,只是进图形界面后内核就把显卡切到 framebuffer 模式了。)

🎨 一张图串起全部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你的 Rust Kernel
│
│ println!("Hello")
↓
自己实现的 Writer
│
│ 写内存(字符 + 颜色)
↓
0xB8000 VGA Buffer
│
↓
VGA 硬件
│ 查字体字形 → 转像素 → 上色
↓
屏幕

✅ 总结

VGA 字符模式 = 把一块特殊内存(0xB8000)当成屏幕上的字符表:CPU 往里面写字符 + 颜色,VGA 硬件按字体把字符转成像素显示出来。

对正在学 Rust OS 的你,VGA 的真正意义不是学会显示文字,而是第一次亲眼看到:

CPU 写内存 → 硬件观察这块内存 → 硬件产生可见输出。

这是理解 MMIO、设备驱动、framebuffer、硬件抽象 最好的入门例子——下一节开始,教程会把它封装成带 unsafe 隔离、能处理换行滚动的完整 VGA 模块,那就是把硬件操作包成安全抽象的第一课。

Happy Hacking! 🎉