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 | ┌────┬────┬────┬────┬────┐ |
经典 VGA 文本模式是 80 列 × 25
行。也就是说,你不需要告诉显卡”把 (100, 50)
这个像素变成白色”,只需要告诉它:
“第 3 行第 10 列,显示字符
A,颜色白色。”
这就是字符模式(text mode)。
🎯 CPU 怎么告诉 VGA 显示 A:0xB8000
这就用到了显存(video memory)。经典 PC 上,VGA 文本模式的显存起始地址是:
1 | 0xB8000 |
CPU 可以像访问普通内存一样访问它:
1 | let ptr = 0xb8000 as *mut u8; |
于是:
1 | CPU |
要注意的一点:
CPU 写的不是屏幕,而是一块会被 VGA 硬件特殊解释的内存。
这正是我们在《物理地址 ≠ RAM 地址》里讲的
MMIO:0xB8000 不是普通 RAM,而是映射到 VGA
硬件的显存区域。写它不像写普通内存那样只是存数据,而是给显卡下达显示指令。
🔤 为什么一个字符占两个字节
因为除了保存显示什么字符,还要保存用什么颜色显示。所以一个字符单元是 2 字节:
1 | ┌──────────────┬──────────────┐ |
例如 0x41 0x0F = 字符 'A' + 颜色属性
0x0F(白字黑底)。于是整块显存的排布是:
1 | 0xB8000 ─ 字符 'A' │ 颜色 ← 第 0 个字符 |
因为每个字符 2 字节,地址步长是 2:第 0 个字符在
0xB8000、第 1 个在 0xB8002、第 2 个在
0xB8004……
🔢 从字符到像素:VGA 是硬件级字符渲染器
CPU 写进去的 0x41 只是一个数字,VGA
并不会直接把它当成像素。VGA
内部有一张字体表(font),存着每个字符的位图:
1 | 字符 'A' 的字形(0/1 点阵) |
这些 0/1 最终对应成像素(前景色/背景色):
1 | ███ |
完整链路:
1 | CPU → 写入 'A' (0x41) → VGA 显存 → 字体表查 'A' 的像素图案 |
所以字符模式本质是一个硬件帮你做好的字符渲染器——你只管给字符编号和颜色,画像素是显卡的活。
📐 整块显存有多大
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 | struct Writer { |
以及:
1 | impl fmt::Write for Writer { |
这里的 buffer 本质上就指向
0xB8000,write_byte 做的就是把字符 +
颜色写进显存、必要时滚动屏幕:
1 | println!("Hello") |
所以:
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 | VGA Text Mode: CPU → 字符 → VGA(帮你渲染)→ 屏幕 |
要画一个 A:字符模式写一个字节就完事;framebuffer
需要自己'A' → 字体库 → bitmap → 逐像素写入。
🕰️ 为什么现在基本不用它了
因为它能力太有限:80×25、纯字符、几种颜色。而现代桌面需要
1920×1080(约 200 万像素)、窗口、图片、动画、视频、GPU
加速——现代操作系统走的是:
1 | GPU → Framebuffer / 显示驱动 → 显示器 |
而不是:
1 | 0xB8000 → VGA Text Mode |
(顺带:0xB8000 这块地址今天依然存在——BIOS
固件和早期启动阶段还在用文本模式显示 POST
信息,只是进图形界面后内核就把显卡切到 framebuffer 模式了。)
🎨 一张图串起全部
1 | 你的 Rust Kernel |
✅ 总结
VGA 字符模式 = 把一块特殊内存(
0xB8000)当成屏幕上的字符表:CPU 往里面写字符 + 颜色,VGA 硬件按字体把字符转成像素显示出来。
对正在学 Rust OS 的你,VGA 的真正意义不是学会显示文字,而是第一次亲眼看到:
CPU 写内存 → 硬件观察这块内存 → 硬件产生可见输出。
这是理解 MMIO、设备驱动、framebuffer、硬件抽象
最好的入门例子——下一节开始,教程会把它封装成带 unsafe
隔离、能处理换行滚动的完整 VGA
模块,那就是把硬件操作包成安全抽象的第一课。
Happy Hacking! 🎉