从零编写操作系统 25 - 从字符到像素:接入 Mode 13h
前 24 篇一直把 0xB8000 当作屏幕:一个字符占两个字节,内核写入字符码和颜色,VGA 文本模式负责把它显示出来。窗口系统需要另一种输出契约。内核必须能描述一个像素的位置,并且知道这段内存的宽、高、步长和颜色格式。
本篇增加独立的图形启动镜像。第二阶段引导器在实模式设置 BIOS Mode 13h,回读确认后发布 BootInfo v2;内核验证 0xA0000、320×200、每行 320 字节和 8 位索引色,再通过同一个有界像素函数绘制整帧、矩形与边框。历史文本镜像继续使用 BootInfo v1,所以已有 Shell、管道和早期启动实验不需要假装理解新的字段语义。
专项实验同时读取串口、GDB 符号、物理页位图和 64,001 个显存字节,并保存 QEMU 截图。强制失败镜像则证明模式设置未通过时不会进入图形内核。
BIOS 调用必须留在实模式
Mode 13h 是一个适合教学的最小图形入口:320×200 个像素,每个像素占一个字节,一帧共 64,000 字节。标准 VGA 图形窗口从物理地址 0xA0000 开始。像素 (x, y) 的地址为:
1 | |
每个字节保存调色板索引。值 14 和 15 在 BIOS 默认调色板中通常显示为黄色和白色,但它们不是压缩后的 RGB 值。改变 DAC 调色板后,同一份显存字节可以呈现不同颜色。
设置模式依赖 INT 10h,而当前内核进入保护模式后没有虚拟 8086 模式或 BIOS thunk。调用位置因此固定在 stage2 尚未设置 CR0.PE 的阶段:
1 | |
AH=00h 请求设置模式,AH=0Fh 回读当前模式。SeaBIOS 的设模式路径不会把内部失败转换成一套通用返回码,所以本项目只能在固定环境中检查最终模式是否为 0x13。这项回读能保护当前启动契约,不能扩张为“所有 BIOS 视频失败都可被完整诊断”。
显示参数是一份带版本的启动契约
Day 04 定义的 BootInfo v1 恰好是 40 字节,末尾字段描述文本屏幕:
| 偏移 | v1 语义 | 文本镜像的值 |
|---|---|---|
| 28 | video | 0xB8000 |
| 32 | columns | 80 |
| 34 | rows | 25 |
| 36 | pitch | 160 字节 |
| 38 | mode | 3 |
| 39 | cell bytes | 2 |
若只把 video 改成 0xA0000,内核仍会把 320 解释成列数、把最后一个字节解释成字符单元大小。内存布局没有变化,并不表示数据含义没有变化。
图形镜像发布 BootInfo v2,复用相同偏移和总长度,但字段按版本解释:
| 偏移 | v2 语义 | 图形镜像的值 |
|---|---|---|
| 28 | framebuffer base | 0xA0000 |
| 32 | width | 320 像素 |
| 34 | height | 200 像素 |
| 36 | pitch | 320 字节 |
| 38 | BIOS mode | 0x13 |
| 39 | bits per pixel | 8 |
stage2 负责生产这份数据,C 结构负责消费。两边保持同一组偏移,并用 _Static_assert(sizeof(struct bootinfo) == 40) 锁住总长度。图形内核还会逐项核对固定值:
1 | |
文本镜像仍要求 version == 1。mbr.img、Shell 镜像和管道镜像没有被切换到图形模式,旧回归继续观察 0xB8000 与原来的保护模式标记。新增能力通过 mbr-graphics.img 单独进入,避免一次字段升级重写前 24 篇的测试前提。
可迁移的规则是:共享结构的字节数相同,不代表新旧消费者可以互换。字段语义变化时,生产者、消费者和版本检查必须一起更新;无法理解新版本的一方应在使用数据前拒绝它。
所有像素先经过坐标边界
当前像素原语只接受无符号坐标,先检查宽高,再计算地址:
1 | |
合法坐标是半开区间 [0, 320) × [0, 200)。右下角 (319, 199) 对应偏移 63,999;(320, 0) 和 (0, 200) 都必须在地址计算前返回失败。
实验在偏移 64,000 保存一个哨兵,完成所有绘制后再次读取。两次越界调用使 rejected 变成 2,哨兵仍为 0。GDB 还直接检查四个边角、内部黄色像素和中心白色像素。截图证明宿主显示面看到了画面,显存字节与哨兵则证明画面来自预期地址范围;两种证据各自覆盖不同问题。
矩形函数目前假定调用者传入的矩形完整落在屏幕内。Day 26 会加入有符号坐标与裁剪,并在计算 x + width 之前处理整数溢出。本篇没有把只适用于已知常量矩形的循环包装成通用绘图库。
显存不能成为普通物理页
Mode 13h 的显存占据 0xA0000..0xAFFFF 的前 64,000 字节。当前页表恒等映射前 64 MiB,因此内核可以直接用物理地址访问它;物理页分配器则只把 1 MiB 以上的可用页加入候选集合。
这两个条件缺一不可。只有映射而没有保留,分配器可能把显存交给用户页或内核栈;只有保留而没有映射,C 代码会在第一次像素写入时触发页故障。
图形内核先执行:
1 | |
专项 GDB 检查进一步读取 eligible 位图,确认第 0xA0 个物理页没有 eligible 位。它不依赖一次 page_free 返回值来替代内部状态观察。随后 64,000 字节绘制与哈希又证明该范围在当前页表中确实可读写。
设备内存保护可以概括为“映射与所有权分开验证”:页表回答 CPU 能否访问,分配器回答普通内存使用者能否取得。遇到 framebuffer、MMIO 或固件保留区时,两张表都要检查。
图形模式后串口承担诊断
原来的 console_init() 同时初始化 VGA 文本和 COM1。切换到 Mode 13h 后,继续写 0xB8000 不能作为可见文本证据,图形内核的诊断全部以串口为准:
1 | |
最终哈希 0x4130a99d 覆盖整帧 64,000 字节。pixels=185120 是绘制操作次数,同一位置可能被背景、矩形和边框多次覆盖,不能把它当作不同像素的数量。
强制失败镜像把回读结果改为失败值。stage2 在保护模式之前恢复 mode 3,经文本显存和 COM1 输出:
1 | |
日志中不存在保护模式成功标记,也不存在内核图形统计。失败路径先恢复可读诊断面,再停机;它没有带着一份无效 BootInfo 继续启动。
截图与内存证据需要同时保留
QEMU 的 screendump 得到一张 640×400 PNG:
客体分辨率仍是 320×200。QEMU 对传统模式的导出结果进行了 2 倍缩放,检查脚本因此分别验证 BootInfo 中的 320×200和 PNG 头中的 640×400,没有用宿主图片尺寸反推客体 pitch。
完整串口记录、GDB 显存与分配位图现场、强制失败串口记录和累计回归记录保存了本次运行结果。专项检查之外,保护模式、管道、Shell、文件描述符、FAT16、ATA、进程、系统调用、ELF、内存、容量边界和真实 IRQ1 键盘检查均再次退出 0。
当前边界
当前图形输出固定为 BIOS Mode 13h、320×200、8 位索引色和默认调色板。没有 VBE、线性高分辨率 framebuffer、DAC 编程、双缓冲、字体、滚动、鼠标、窗口或用户态图形协议。
像素与矩形绘制发生在内核启动演示中。矩形只接受已知合法的无符号参数,不支持负坐标和任意裁剪。全帧写入结果在 QEMU 8.2.2、SeaBIOS 1.16.3、pc-i440fx-7.2、TCG、qemu32、1 CPU 和 64 MiB 中得到验证;没有据此声称真实 VGA 硬件、其他 BIOS 或其他模拟器已经兼容。
机制速查
| 遇到的问题 | 可迁移做法 | 本篇落点 |
|---|---|---|
| 固件能力只在早期环境可用 | 在切换执行模式前完成调用并发布结果 | 实模式 INT 10h 后进入保护模式 |
| 结构尺寸相同但字段含义变化 | 版本号与生产/消费两端同步升级 | BootInfo v1/v2 |
| 线性缓冲区容易越界 | 坐标验证先于地址计算 | [0,width) × [0,height) |
| 设备地址不能交给普通分配器 | 分别验证映射可达与资源保留 | 恒等映射 + eligible 位图 |
| 主显示面可能失去诊断能力 | 保留独立的带外输出 | COM1 串口日志 |
| 截图只能证明看起来正确 | 组合界面、内存和状态证据 | PNG + GDB + 串口 + 失败镜像 |
下载与复现
下载第 25 篇完整源码。附件 SHA-256:
1 | |
解压后执行:
1 | |
附件包含第 01–25 篇的累计工程和打包前形成的 docs/day25-* 证据,不含 build/。冻结后会在新的空目录中重建并重跑专项及十二项累计检查;复验收据验证已冻结的附件,因此不反向写入 ZIP 自身。
练习
- 将
put_pixel的边界检查暂时移到地址计算之后,用(UINT_MAX, 1)说明为何后续通用裁剪必须防止整数溢出,再恢复正确实现。 - 改写绘制图案,但保持同一组关键像素断言,观察截图仍然可见而整帧哈希发生变化;据此区分局部断言和全帧证据。
- 让图形内核错误地接受 BootInfo v1,记录它会怎样解释 80、25、160 和 2,再恢复按版本拒绝的边界。
上一篇:24 - 管道。
下一篇:26 - 字体与缓冲。
参考资料
- SeaBIOS
stdvgamodes.c:标准 VGA 模式表中 Mode 13h 的 320×200、8 bpp packed 配置。 - SeaBIOS
vgabios.c:INT 10h设模式和读取当前模式的固定实现。 - QEMU
hw/display/vga.c:VGA low-memory 区和显示刷新路径;本文结论限定在固定 QEMU 版本。

