字符与控制台 — 从「画出一个 A」到「日志哗哗流」
字符与控制台 — 从「画出一个 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。
流水线三步:
makefont.py把 256 个字符的文本格式转成 4096 字节二进制(16×256)11;objcopy -I binary -O elf64-x86-64给裸二进制加上链接信息,变成 hankaku.o;- 链接进内核后,数据以
_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。 实现三件套:
- 控制台做成全局变量(placement new 老手艺,第 4 章的);
printk(const char* fmt, ...)用 va_list 收变长参数,格式化处理委托给 vsprintf (sprintf 的亲戚,吃 va_list 而不是...);- 内部固定 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 比特一个像素,8×16 字符 = 16 字节;
(行<<dx)&0x80取点; - 接口与实现分离让 WriteAscii 不关心像素怎么画;传引用是在声明「这东西必然存在」;
- 拆分编译的收益 = 只重编译改过的 .cpp;函数主体放 .cpp,头文件只放声明;
- objcopy 能把裸二进制焊进可执行文件(binary* 变量),自举期缺文件系统时的标准解法;
- Newlib 把 OS 依赖切成空壳函数——OS 每长一个功能,就解锁一批标准库;
- 控制台的滚屏 = buffer_ 上移 + 整屏重绘;「保存状态以支持重绘」是 GUI 系统的元模式;
- 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」) |
Footnotes
-
出处:「5.1 尝试写入字符(osbook_day05a)」(
text/40-ch05-01-5-1-osbook-day05a.txt:30(搜「字体」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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: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.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(搜「直接嵌入内核文件」))。 ↩ -
出处:「5.3 增加字体(osbook_day05c)」(
text/42-ch05-03-5-3-osbook-day05c.txt:12(搜「東雲」);text/42-ch05-03-5-3-osbook-day05c.txt:22(搜「直接嵌入内核文件」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「5.4 字符串绘制和sprintf()(osbook_day05d)」(
text/43-ch05-04-5-4-sprintf-osbook-day05d.txt:44(搜「Newlib」))。 ↩ -
出处:「5.4 字符串绘制和sprintf()(osbook_day05d)」(
text/43-ch05-04-5-4-sprintf-osbook-day05d.txt:56(搜「分离出来」))。 ↩ -
出处:「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: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.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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「5.3 增加字体(osbook_day05c)」(
text/42-ch05-03-5-3-osbook-day05c.txt:12(搜「東雲」);text/42-ch05-03-5-3-osbook-day05c.txt:22(搜「直接嵌入内核文件」))。 ↩ -
出处:「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:76(搜「制表符」))。 ↩