跳转至

字符与控制台 — 从「画出一个 A」到「日志哗哗流」

这一章讲什么: 屏幕上画点已经会了,但操作系统的标志是滚动的文字—— 本章就做出这个:字体(字符→点阵的数据表)→ 字符串 → 控制台(会换行、会滚屏)→ printk()(内核任意地方都能喊一嗓子的格式化日志)。 中间还顺手完成两件工程基建:多文件拆分编译,和把 C 标准库(Newlib)接进内核。

1. 先看现象:一个字母 A 的显影过程

主线例子,字符 'A' 上屏:

  字体数据(kFontA,16 个 8 位整数,每个整数 = 点阵的一行):
    0b00000000   ← 第 0 行:全 0,不画
    0b00011000   ← 第 1 行:中间两个点
    0b00111100
    …(一共 16 行,拼出 A 的形状)
  对每个像素列 dx(0~7),问一句:
    (kFontA[dy] << dx) & 0x80u  是不是非 0?
      是 → writer.Write(涂上颜色)
      否 → 跳过(露出背景)
  × 16 行 × 8 列 = 一个字符

  图说:字体用 1 比特表示一个像素(画/不画),比像素颜色的 3 字节省 24 倍。

字体(font)就是「表示字符形状的数据」:颜色只需要「涂不涂」两个状态, 所以每个像素 1 比特就够,一个 8×16 的字符正好用 16 个 8 位整数表示1。 位运算 (字<<dx) & 0x80 是在问「这一行的第 dx 位是不是 1」——书里拿 0xc1 逐位移给你看: C++ 里 0 是假、非 0 是真,所以得 0x80 就画、得 0 就跳2。

这个 WriteAscii() 只依赖 PixelWriter 接口而不是具体实现—— 「改变绘制像素的方式不会影响 WriteAscii」,这是模块化的基本思想3。 为什么参数用引用不用指针?专栏 5.1 的说法:引用的好处是几乎造不出空引用, 所以「传引用」本身就是在声明「这东西一定存在」4。

2. 顶层全景

  5.1  一个字符:8×16 点阵 + 位运算取点
  5.2  拆分编译:main.cpp → graphics.cpp / font.cpp / main.cpp + 三个 .hpp
  5.3  全套字体:東雲字体 → makefont.py → objcopy → 嵌进 kernel.elf(_binary_* 变量)
  5.4  字符串与格式化:WriteString → sprintf()(Newlib 登场,newlib_support.c 补空函数)
  5.5  控制台类:光标 + 换行 + 滚屏(buffer_ 记全屏字符,memcpy 上移)
  5.6  printk():全局控制台 + vsprintf,内核日志口上线

  图说:前三节是「字」,后三节是「流」。printk 一上线,后面每章的调试都靠它喊。

3. 核心原理

3.1 拆分编译:为什么 .cpp 要分家

main.cpp 涨到 130 行,作者决定分文件:graphics.cpp(画)、font.cpp(字)、main.cpp(主函数)5。 拆开的理由很实际:编译以 .cpp 为单位,改一个文件只重编译它,程序越大省得越多; 另外「每个文件只关注一个问题」更好跟踪6。

拆开的代价:别的文件看不见这里的定义了。解法是头文件放声明、.cpp 放实现, 用到的地方 #include 头文件;在类外定义成员函数要写成 类名::函数名, 漏了类名就变成全局函数7。一个经验法则:函数主体尽量放 .cpp—— 主体写进头文件的话,每次改它,所有包含这个头文件的 .cpp 都得重编译8。

3.2 把字体焊进内核:objcopy 魔法

100 多个字符的手写字体不现实,书里选了免费可用的「東雲」字体,8×16 点阵正好配套9。 关键决策:不读文件,直接把字体数据嵌进 kernel.elf。理由很朴素—— 文件读取功能还不存在;就算存在,「读不到字体文件时也没法显示报错信息」10。

流水线三步:

  1. makefont.py 把 256 个字符的文本格式转成 4096 字节二进制(16×256)11;
  2. objcopy -I binary -O elf64-x86-64 给裸二进制加上链接信息,变成 hankaku.o;
  3. 链接进内核后,数据以 _binary_... 开头的三个变量现身(名字 objcopy 定死,不能乱起), 程序里 extern 声明后当数组用;GetFont(c) 返回字符 c 那 16 字节的起始地址12。

3.3 Newlib:没有操作系统,也能有 sprintf

想显示格式化字符串(%d 这种),作者不打算自己写解释器,直接请来 Newlib—— 一个为高可移植性设计的 C 标准库,本来就面向「无操作系统的嵌入式设备」13。 它的巧思:把依赖操作系统的部分(如 malloc、printf 底层)从库里切出来, 由使用者(也就是你,操作系统作者)自己实现;现在 OS 啥都没有, 这些函数在 newlib_support.c 里先定义成空壳,strlen、 sprintf 这种纯内存操作的立刻能用; OS 功能长出来后再往空壳里填真实现,能用的标准函数就越来越多14。

这是全书的一条暗线:操作系统的每个新功能,都解锁一批标准库函数。

3.4 控制台:会滚动的字符打印机

控制台类(Console)的公开接口只有一个 PutString()。内部状态:

  • cursor_row_ / cursor_column_:光标位置(字符格子,不是像素);
  • buffer_:记住屏幕上显示过的全部字符(每行多留 1 字节给 '\0');
  • 前景色画字,背景色填空。

两个动作撑起一切:换行——x 回 0、y 进 1;滚屏——光标已经在最底行时, 不能往下走了,把整个显示区上移一行:buffer_ 里每行内容 memcpy 到上一行, 最底行用 memset 填 '\0',然后整屏重绘15。测试方法:输出 27 行(超过 25 行容量), 看最前面的行被顶掉没有——滚了,控制台就成了16。

滚屏此刻还慢得可观(整屏重绘),第 9 章会拿秒表回来收拾它。

3.5 printk:内核的喉咙

Linux 有 printk,从内核任何地方都能格式化输出;MikanOS 要一个同款17。 实现三件套:

  1. 控制台做成全局变量(placement new 老手艺,第 4 章的);
  2. printk(const char* fmt, ...) 用 va_list 收变长参数,格式化处理委托给 vsprintf (sprintf 的亲戚,吃 va_list 而不是 ...);
  3. 内部固定 1024 字节缓冲,所以一次最多印 1023 个字符18。

至此,内核有了「任意地方、任意数据、一行代码」的观测手段—— 后面二十五章的每一步,都是踩着 printk 的日志走过的。

4. 作者的判断与证据

判断:嵌字体优于读字体文件。 理由不是性能,而是引导顺序的死结: 错误提示本身依赖字体,字体加载失败时的报错无路可走19。这是自举(bootstrapping)问题的 第一次现身——「想用功能 X 需要先实现 X 的依赖,而依赖又依赖 X」。

判断:控制台要记下全屏字符。 滚动算法需要先擦再画,「因此还需要记住至少一个屏幕上显示过的信息」 ——buffer_ 就是这个记忆20。这是「保存状态以支持重绘」模式的首秀,第 9、10 章的图层系统是它的放大版。

证据: 每步都有屏幕截图作证(字体大集合、27 行滚屏、printk 格式化数字),验证方式依然是「跑起来看」。

5. 边界与局限

  • 只支持 ASCII 可见字符:控制字符里只有 '\n' 被特殊处理;'\t' 被当普通字符、'\a' 没实现 (书里说「可以发出声音或使图标闪烁,使其看起来更真实」——留了作业)21。
  • 滚屏是全屏重绘,后面章节证明这是性能瓶颈之一(第 9 章会测、会改)。
  • Newlib 函数大部分是空壳:malloc 还不能用(没有内存管理),C++ 的 new(普通版)依旧缺席—— 第 8、9 章才陆续解锁。
  • 字体只有 8×16 一种尺寸,日文、汉字要等第 28 章靠 FreeType 解决。

6. 可带走的

本章主线例子,一行写完: 16 行点阵数据 + 位运算逐点问真假 → 'A' 上屏 → 256 字符全嵌进内核 → PutString 换行滚屏 → printk 随便喊。

  1. 字体 = 位图查表:1 比特一个像素,8×16 字符 = 16 字节;(行<<dx)&0x80 取点;
  2. 接口与实现分离让 WriteAscii 不关心像素怎么画;传引用是在声明「这东西必然存在」;
  3. 拆分编译的收益 = 只重编译改过的 .cpp;函数主体放 .cpp,头文件只放声明;
  4. objcopy 能把裸二进制焊进可执行文件(binary* 变量),自举期缺文件系统时的标准解法;
  5. Newlib 把 OS 依赖切成空壳函数——OS 每长一个功能,就解锁一批标准库;
  6. 控制台的滚屏 = buffer_ 上移 + 整屏重绘;「保存状态以支持重绘」是 GUI 系统的元模式;
  7. printk 三件套:全局控制台 + va_list + vsprintf(1024 字节缓冲封顶)。

7. 原文地图

主题 原书章 原文位置
字体定义与 1 比特像素 5.1 尝试写入字符(osbook_day05a) text/40-ch05-01-5-1-osbook-day05a.txt:30(搜「字体」)
位运算取点(0xc1 示例) 5.1 尝试写入字符(osbook_day05a) text/40-ch05-01-5-1-osbook-day05a.txt:48(搜「0x80u」) · text/40-ch05-01-5-1-osbook-day05a.txt:52(搜「真假」)
依赖接口、传引用 5.1 尝试写入字符(osbook_day05a) text/40-ch05-01-5-1-osbook-day05a.txt:91(搜「接口」) · text/41-ch05-02-5-2-osbook-day05b.txt:1(搜「专栏5.1」)
引用 vs 指针(空引用) 5.2 拆分编译(osbook_day05b) text/41-ch05-02-5-2-osbook-day05b.txt:22(搜「空引用」)
拆分编译的理由与结构 5.2 拆分编译(osbook_day05b) text/41-ch05-02-5-2-osbook-day05b.txt:33(搜「130行」) · text/41-ch05-02-5-2-osbook-day05b.txt:55(搜「graphics.cpp」)
头文件只放声明 5.2 拆分编译(osbook_day05b) text/41-ch05-02-5-2-osbook-day05b.txt:76(搜「只会写入原型」)
東雲字体与嵌入决策 5.3 增加字体(osbook_day05c) text/42-ch05-03-5-3-osbook-day05c.txt:12(搜「東雲」) · text/42-ch05-03-5-3-osbook-day05c.txt:22(搜「直接嵌入内核文件」)
makefont.py 与 objcopy 5.3 增加字体(osbook_day05c) text/42-ch05-03-5-3-osbook-day05c.txt:28(搜「makefont.py」) · text/42-ch05-03-5-3-osbook-day05c.txt:40(搜「objcopy」)
extern 与 binary 变量 5.3 增加字体(osbook_day05c) text/42-ch05-03-5-3-osbook-day05c.txt:44(搜「_binary」) · text/42-ch05-03-5-3-osbook-day05c.txt:50(搜「都标有extern」)
Newlib 与 sprintf 5.4 字符串绘制和sprintf()(osbook_day05d) text/43-ch05-04-5-4-sprintf-osbook-day05d.txt:44(搜「Newlib」)
OS 依赖函数切出为空壳 5.4 字符串绘制和sprintf()(osbook_day05d) text/43-ch05-04-5-4-sprintf-osbook-day05d.txt:56(搜「分离出来」)
控制台类与 buffer_ 5.5 控制台类(osbook_day05e) text/44-ch05-05-5-5-osbook-day05e.txt:51(搜「kRows和kColumns」) · text/44-ch05-05-5-5-osbook-day05e.txt:59(搜「buffer_」)
换行与滚屏算法 5.5 控制台类(osbook_day05e) text/44-ch05-05-5-5-osbook-day05e.txt:80(搜「Newline」) · text/44-ch05-05-5-5-osbook-day05e.txt:91(搜「memcpy」) · text/44-ch05-05-5-5-osbook-day05e.txt:103(搜「27行」)
控制字符未处理 5.5 控制台类(osbook_day05e) text/44-ch05-05-5-5-osbook-day05e.txt:76(搜「制表符」)
printk 的实现 5.6 printk()(osbook_day05f) text/45-ch05-06-5-6-printk-osbook-day05f.txt:4(搜「printk()」) · text/45-ch05-06-5-6-printk-osbook-day05f.txt:15(搜「va_list」) · text/45-ch05-06-5-6-printk-osbook-day05f.txt:18(搜「1024」)

  1. 出处:「5.1 尝试写入字符(osbook_day05a)」(text/40-ch05-01-5-1-osbook-day05a.txt:30(搜「字体」))。 ↩

  2. 出处:「5.1 尝试写入字符(osbook_day05a)」(text/40-ch05-01-5-1-osbook-day05a.txt:48(搜「0x80u」); text/40-ch05-01-5-1-osbook-day05a.txt:52(搜「真假」))。 ↩

  3. 出处:「5.1 尝试写入字符(osbook_day05a)」(text/40-ch05-01-5-1-osbook-day05a.txt:91(搜「接口」); text/41-ch05-02-5-2-osbook-day05b.txt:1(搜「专栏5.1」))。 ↩

  4. 出处:「5.2 拆分编译(osbook_day05b)」(text/41-ch05-02-5-2-osbook-day05b.txt:22(搜「空引用」))。 ↩

  5. 出处:「5.2 拆分编译(osbook_day05b)」(text/41-ch05-02-5-2-osbook-day05b.txt:33(搜「130行」); text/41-ch05-02-5-2-osbook-day05b.txt:55(搜「graphics.cpp」))。 ↩

  6. 出处:「5.2 拆分编译(osbook_day05b)」(text/41-ch05-02-5-2-osbook-day05b.txt:33(搜「130行」); text/41-ch05-02-5-2-osbook-day05b.txt:55(搜「graphics.cpp」))。 ↩

  7. 出处:「5.2 拆分编译(osbook_day05b)」(text/41-ch05-02-5-2-osbook-day05b.txt:76(搜「只会写入原型」))。 ↩

  8. 出处:「5.2 拆分编译(osbook_day05b)」(text/41-ch05-02-5-2-osbook-day05b.txt:76(搜「只会写入原型」))。 ↩

  9. 出处:「5.3 增加字体(osbook_day05c)」(text/42-ch05-03-5-3-osbook-day05c.txt:12(搜「東雲」); text/42-ch05-03-5-3-osbook-day05c.txt:22(搜「直接嵌入内核文件」))。 ↩

  10. 出处:「5.3 增加字体(osbook_day05c)」(text/42-ch05-03-5-3-osbook-day05c.txt:12(搜「東雲」); text/42-ch05-03-5-3-osbook-day05c.txt:22(搜「直接嵌入内核文件」))。 ↩

  11. 出处:「5.3 增加字体(osbook_day05c)」(text/42-ch05-03-5-3-osbook-day05c.txt:28(搜「makefont.py」); text/42-ch05-03-5-3-osbook-day05c.txt:40(搜「objcopy」))。 ↩

  12. 出处:「5.3 增加字体(osbook_day05c)」(text/42-ch05-03-5-3-osbook-day05c.txt:44(搜「_binary」); text/42-ch05-03-5-3-osbook-day05c.txt:50(搜「都标有extern」))。 ↩

  13. 出处:「5.4 字符串绘制和sprintf()(osbook_day05d)」(text/43-ch05-04-5-4-sprintf-osbook-day05d.txt:44(搜「Newlib」))。 ↩

  14. 出处:「5.4 字符串绘制和sprintf()(osbook_day05d)」(text/43-ch05-04-5-4-sprintf-osbook-day05d.txt:56(搜「分离出来」))。 ↩

  15. 出处:「5.5 控制台类(osbook_day05e)」(text/44-ch05-05-5-5-osbook-day05e.txt:80(搜「Newline」); text/44-ch05-05-5-5-osbook-day05e.txt:91(搜「memcpy」); text/44-ch05-05-5-5-osbook-day05e.txt:103(搜「27行」))。 ↩

  16. 出处:「5.5 控制台类(osbook_day05e)」(text/44-ch05-05-5-5-osbook-day05e.txt:80(搜「Newline」); text/44-ch05-05-5-5-osbook-day05e.txt:91(搜「memcpy」); text/44-ch05-05-5-5-osbook-day05e.txt:103(搜「27行」))。 ↩

  17. 出处:「5.6 printk()(osbook_day05f)」(text/45-ch05-06-5-6-printk-osbook-day05f.txt:4(搜「printk()」); text/45-ch05-06-5-6-printk-osbook-day05f.txt:15(搜「va_list」); text/45-ch05-06-5-6-printk-osbook-day05f.txt:18(搜「1024」))。 ↩

  18. 出处:「5.6 printk()(osbook_day05f)」(text/45-ch05-06-5-6-printk-osbook-day05f.txt:4(搜「printk()」); text/45-ch05-06-5-6-printk-osbook-day05f.txt:15(搜「va_list」); text/45-ch05-06-5-6-printk-osbook-day05f.txt:18(搜「1024」))。 ↩

  19. 出处:「5.3 增加字体(osbook_day05c)」(text/42-ch05-03-5-3-osbook-day05c.txt:12(搜「東雲」); text/42-ch05-03-5-3-osbook-day05c.txt:22(搜「直接嵌入内核文件」))。 ↩

  20. 出处:「5.5 控制台类(osbook_day05e)」(text/44-ch05-05-5-5-osbook-day05e.txt:51(搜「kRows和kColumns」); text/44-ch05-05-5-5-osbook-day05e.txt:59(搜「buffer_」))。 ↩

  21. 出处:「5.5 控制台类(osbook_day05e)」(text/44-ch05-05-5-5-osbook-day05e.txt:76(搜「制表符」))。 ↩