导引:点亮什么、为什么、设计图
这一章我们要点亮什么
两件看得见的事。
第一件,kprintf 终于像个 printf 了。011 的 kprintf 其实已经能 %d/%u/%x/%X/%p,也有 %Nd/%0Nd 的宽度与零补;这一章真正补的是左对齐(%-)、把负数零补的坑修对(-00042 而不是 000-42),并顺手把引擎抽成 header-only 模板让它能在 host 上单测。于是你能写出 kprintf("%-8x", 0xdead) 这种带对齐的诊断输出,而不必自己手拼字符串。
第二件,稍微惊悚一点:-O2(Release)构建的内核,以前根本起不来。它在加载 IDT 的当口安静地 Triple Fault,QEMU 直接退出,连一句遗言都不留。这一章把它修了,从那以后无论 Debug 还是 Release,内核都能稳稳跑到 idle loop。
为什么现在需要它
先说为什么是现在。011 之后内核已经能响应时钟中断,接下来 013 就要往屏幕上画字、动 framebuffer,用户态、文件系统也在不远处。可一旦系统复杂起来,调试就全压在 kprintf 一根线上——它就是我们的眼睛。这时候如果 kprintf 连个对齐都打不好、连个指针都格式化不利索,后面每一处排查都会被它拖累。所以在往系统里堆新东西之前,先花一章把 kprintf 做扎实,是一笔稳赚的投资。
重构的过程中,一个潜伏的 bug 被顺带逼了出来。我们想把 kprintf 的格式化引擎抽出来做单元测试,测试要在 host 上用 -O2 编译(因为那才是发布内核会跑的优化级别)。结果一编——内核在 idt_init 里 Triple Fault 了。这不是 kprintf 的锅,但它像一面镜子,照出了一个从 010 起就一直藏在那儿、只是之前没被 -O2 照到的硬件初始化缺陷:引导期从没初始化 SSE。
这就是这一章为什么是「两个地基」:一个是软件地基(kprintf),一个是硬件地基(SSE),它俩凑在一个 tag 里,因为正是在给 kprintf 做 -O2 单测的时候,后者才暴露。
设计图
kprintf 重构后的形状,核心是「格式化引擎」和「输出后端」彻底分家,中间靠一个回调连起来:
┌─────────────────────────────────────┐
│ vkprintf_impl<OutputFn>(out,fmt,va) │ ← 硬件无关,header-only 模板
│ 纯逻辑:解析 % 宽度 对齐,格式化数字 │ kernel/lib/private/vkprintf_impl.hpp
└───────────────┬─────────────────────┘
│ 每产出一个字符 → out(c)
▼
┌─────────────────────────────────────┐
│ 回调 lambda │
│ [&](char c){ g_serial.putc(c); } │ ← big kernel:喂给串口
└───────────────┬─────────────────────┘
▼
┌─────────────────────────────────────┐
│ 输出后端 │
│ big kernel: Serial(COM1) │ host 单测: std::string
│ (013 之后还会多一路: framebuffer) │
└─────────────────────────────────────┘同一个引擎,big kernel 喂给串口,host 单测喂给一个 std::string。013 之后还会多一路屏幕——到那时你就明白这层回调当初为什么要抽出来了。
SSE 那条线是另一回事,它埋在 boot 流程最开头:
mini kernel boot.S _start
│ cli
│ ★ 设 CR4.OSFXSR(bit9) + CR4.OSXMMEXCPT(bit10) + clts ← 本章新增
│ ... 进入长模式、加载 big kernel ...
▼
big kernel main: kprintf_init → GDT → IDT(idt_init 内部清零,可能被 -O2 向量化为 SSE)关键在于这个 ★ 出现在内核最早的可执行指令处。它的意义下面讲 SSE 那节展开。