页表:一段爱情故事(其实不是)

发布: (2026年1月10日 GMT+8 12:42)
11 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content of the article (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Simplified Chinese while preserving the original formatting, markdown syntax, and technical terms.

Diary Entry

Dear diary,

今天我发现,离开 UEFI 那舒适的怀抱就像在 40 岁时搬出父母的家。曾经神奇地工作的一切,现在都要求你真正理解世界是如何运作的。

当我坐下来喝咖啡时是 上午 9 点,我自信地认为从 UEFI 过渡到裸金属会很直接。毕竟,我已经成功实现了 AHCI 存储和键值存储。设置全局描述符表(GDT)并开始运行自己的内核有多难?我的狂妄可想而知。

计划看起来很合理:

  1. 调用 ExitBootServices
  2. 为 64‑bit 长模式设置合适的 GDT。
  3. 让键盘轮询输入工作。
  4. 运行内核 shell。

我甚至构建了一个直接写入 SSD 的日志系统,这样就可以在重启之间调试。还能出什么岔子呢?

一切。所有事情都可能出错。

First attempt

第一次尝试很有希望。ExitBootServices 成功,GDT 加载没有抱怨,我已经在内核模式下运行。我甚至能看到我的内核 shell 提示符。胜利似乎已成定局——直到我自信地执行 sti 指令来开启中断。

机器立刻三次故障

三次故障是 x86 处理器的方式,表示“我放弃了,你自己搞吧”,随后执行相当于把桌子掀翻并冲出去的数字等价操作。它既是最有帮助也是最无帮助的错误状态——你知道出了灾难性的问题,但 CPU 已决定告诉你具体原因太费劲了。

接下来的两个小时,我沉浸在我称之为**“中断拒绝阶段”**的时间里。

  • 肯定不是中断本身的问题。
  • 也许是 GDT 错了 → 重写了三次,每次都比上一次更繁复。
  • 也许是栈被破坏了 → 加入栈金丝雀和验证代码。
  • 也许是 UEFI 留下了某些状态干扰 → 试着清除我能想到的每个寄存器。

机器继续以同样机械的精准度三次故障,而我则继续冲咖啡。

Switching to polling

到中午,我接受了中断确实是问题所在,决定放弃。轮询键盘输入虽然不优雅,但能工作。我实现了一个简单的 PS/2 控制器轮询循环,基本键盘输入可以用了。内核 shell 也能运行,我甚至可以把日志保存到 SSD。

Milestone 5 技术上完成了,但感觉就像是跑步比赛时把车推到终点线才算赢。

Back to interrupts – Milestone 6

下午的时间花在了IDT 矿坑里,像中世纪抄写员一样仔细地编写中断服务例程。我写了优雅的宏系统来生成完美的栈帧,创建了能够优雅处理任何中断情况的高级处理器——结果把一切都彻底弄坏了

第一次在开启中断后测试,sti 之后立刻出现了 Debug Exception (Vector 1)。这其实是进步——不再是三次故障,而是出现了具体的异常。CPU 至少在尝试告诉我哪里出错了,尽管它的解释毫无意义。

调试异常会在你触发调试寄存器断点或设置陷阱标志进行单步执行时触发。我没有使用任何调试器,当然也没有有意设置陷阱标志。但 x86 处理器就像那个记得三十年前每一次小怨的亲戚——它们会把状态保留在最不方便的地方。

我又花了一个小时才意识到 UEFI 可能留下了调试状态。我添加了代码清除所有调试寄存器(DR0DR7)以及 RFLAGS 中的陷阱标志。调试异常消失了,但现在出现了新问题:计时器中断没有触发

“Silent treatment” phase

PIC 已经配置好,IDT 已经设置

Source:

up,虽然已经启用了中断,但我的计时器滴答计数器仍固执地保持在零。系统没有崩溃,这反而比它“壮观爆炸”时更让人沮丧。

  • 验证了 PIC 配置十七次。
  • 读 Intel 手册读到眼睛流血。
  • 一遍又一遍检查 IDT 条目。

纸面上看一切都正确,但硬件似乎在有礼貌地忽视我精心制作的中断处理程序。

突破

下午 6 点,我正向我的橡胶鸭(我桌面上放的真正的橡胶鸭,用来调试——别评判)解释问题。描述我的优雅 ISR 宏系统时,我意识到了问题:我想得太聪明了。

我的宏生成了复杂的栈帧管理代码,某种方式破坏了中断返回地址。当我查看实际的汇编输出时,看到的是一堆让意大利面工厂都羡慕的栈操作噩梦。

于是我把所有代码全部丢掉,写了最简单的中断处理程序,使用裸函数和内联汇编。没有花哨的宏,没有优雅的抽象——只有处理中断并干净返回的最小代码:

__attribute__((naked)) void isr_timer(void) {
    asm volatile (
        "push %rax\n"
        "incq g_timer_ticks\n"
        "movb $0x20, %al\n"
        "outb %al, $0x20\n"   // Send EOI
        "pop %rax\n"
        "iretq"
    );
}

它不优雅。它原始。它完美运行。

当我用新处理程序启用中断时,计时器立刻以 100 Hz 的频率开始滴答。键盘中断也开始完美捕获输入。经过八小时与复杂抽象的搏斗,答案竟是把中断处理程序写成 1985 年 的风格。

收获

花整整一天实现“现代”内核架构,却发现最原始的方法最可靠,这种体验让人深感谦卑。这提醒我们 简洁往往胜过优雅,尤其是在直接面对硬件时。

结束。

ke 花数小时精心烹饪一顿美食,却发现一块花生酱三明治既更满足也更不容易中毒。

到了晚上,我拥有了一个完整的中断驱动内核。计时器在滴答,键盘响应灵敏,内核 shell 运行无误。我可以实时观看计时器滴答递增,每一次都是对裸金属编程混沌的微小胜利。我把内核日志保存下来以便后续查看:

[KERNEL] Enabling interrupts (STI)...
[KERNEL] Interrupts ENABLED.
[KERNEL] Timer ticks after delay: 199
[KERNEL] Kernel mode active (interrupt mode)

这些简短的日志信息代表了八小时的调试、三次完整的中断系统重写,以及比任何人一天应摄入的咖啡都多得多的咖啡。但它们也代表了更多:一个成功从 UEFI 的保护拥抱转向裸金属严酷现实的工作内核。

回顾过去,教训显而易见。

  1. x86 处理器记得一切,原谅不了任何错误——从 UEFI 转换时务必清除调试寄存器。
  2. PIC 自 1980 年代以来几乎没有改变——尝试抽象它的怪癖通常会让情况更糟。
  3. 当复杂方案失效时,有时答案就是写出三十年前的代码。

最重要的是,我学会了从第一原理构建东西的特殊满足感,即使这些原理似乎是为了最大化人类的痛苦。每一次成功的中断都是对宇宙熵的微小胜利。每一次计时器滴答都是证明:在晶体管和电子的混沌中,我的代码正如预期般执行。

明天我将着手内容寻址存储和时间旅行调试。

g. 因为显然,我还没有受够苦,而业余操作系统开发的魅力就在于,总会有另一层复杂性等着让你谦卑。

但今晚,我打算坐在这里,看着我的计时器滴答计数器一次一次递增,逐个中断,并假装构建操作系统是消磨闲暇的合理方式。

Back to Blog

相关文章

阅读更多 »

中断处理程序的维护与调养

中断处理程序的养护与喂养 亲爱的日记,今天我决定给我的胚胎期操作系统一个声音。不是字面上的——那会很可怕——但……

Rhiza的内核编年史:内核开发

内核开发聚焦 这是一次以故事驱动的旅程,专注于 kernel-development。会话概览 - 会话 ID:rhiza-blog-1767663121960 - 时间范围:2…