键盘控制器的幽灵:BIOS 偷偷塞 6 个回车,裸机 OS 开机疯狂自动换行

键盘控制器的幽灵:BIOS 偷偷塞 6 个回车,裸机 OS 开机疯狂自动换行

BUG 编号:BUG-026
开发阶段:H-2 真机磁盘适配第 8 轮调试
时间:2026-07-12
状态:已完全修复

自己从零写中文裸机 OS,又撞上一个只在真机发作、模拟器完全隐身的阴间 BUG,折腾半天才搞明白根源是 BIOS 自检留下的 “幽灵扫描码”,今天完整分享踩坑全过程。

一、离谱故障:我没碰键盘,系统疯狂自动敲回车

当时 AHCI 磁盘驱动刚修复完 MMIO 读写卡死的问题,内核在 QEMU 里跑得稳稳当当,所有初始化流程全正常。我把镜像写到 U 盘真机启动,前面日志输出一切顺利:IDT、页表、AHCI 全部初始化完成,正常打印出系统开机标语。

结果一开启中断 STI 进入 Shell,诡异的一幕直接出现:

plaintext


[AHCI] 初始化完成 Chinese OS v0.1 baremetal boot OK [0] > [6] >

肉眼看屏幕只显示两行,但串口完整日志拉出来直接吓一跳,系统连续自动弹出 6 次命令提示符,等于后台连续触发 6 次回车换行:

plaintext


[0] > [0] > [0] > [0] > [0] > [0] >

我全程手放在键盘上,一下按键都没按,凭空多出 6 次回车输入。没有报错、没有崩溃、没有异常中断日志,单纯 Shell 疯狂空换行,完全没法正常输入指令,调试直接卡住。

二、层层排查,锁定罪魁祸首是键盘控制器残留数据

猜想 1:QEMU 模拟器的虚假中断后遗症

之前我遇到过 BUG-016,QEMU 开启中断后会凭空生成键盘中断,当时写了清缓冲区代码专门适配模拟器。但真机硬件和 QEMU 虚拟键盘逻辑完全不一样,这套清理代码真机不生效,排除模拟器兼容问题。

猜想 2:真机 BIOS 自检污染键盘硬件

转念想到:电脑开机 BIOS POST 阶段会完整跑一遍键盘硬件自检流程,自检过程中 8042 键盘控制器会生成一堆测试扫描码,BIOS 跑完自检直接撒手不管,根本不会清空控制器输出缓冲区。这些残留扫描码就一直存在硬件缓存里,等我们 OS 接管硬件。

实锤验证:打印原始扫描码,全是回车键编码

为了确认猜想,我直接在键盘中断处理函数里加 VGA 打印,把每次捕获到的原始扫描码打在屏幕上。真机开机瞬间直接刷出 6 组相同数值:
1C 1C 1C 1C 1C 1C

查硬件手册一眼看懂:0x1C就是回车键的标准通码!
真相彻底浮出水面:BIOS 自检后,键盘控制器缓冲区囤了 6 个回车扫描码;我们执行 STI 开启 IRQ1 键盘中断后,硬件立刻连续触发 6 次中断,每次读取一个回车扫描码,内核转译成 ASCII 换行符丢进输入缓冲区,Shell 就疯狂自动换行。

三、底层原理:8042 键盘控制器的硬件特性(新手极易忽略)

先简单说下 x86 主板标配的 8042 键盘控制器两个核心端口,写裸机键盘驱动必须吃透:

表格



端口地址功能
0x60数据端口,读写键盘扫描码
0x64状态端口,读取控制器工作状态


状态寄存器 bit0 是 OBF 标志位,全称 Output Buffer Full:只要这个位等于 1,代表 0x60 端口里存着未读取的扫描码,必须读走清空。

为什么 QEMU 模拟器一点事没有?

QEMU 只是软件模拟硬件,简化了完整 BIOS 自检流程,虚拟键盘控制器开机默认是空缓冲区,不会生成任何残留扫描码。模拟器对硬件脏状态完全不模拟,很多底层坑只有真机才能暴露。

为什么真机一定会出现这个问题?

真实物理主板 BIOS POST 会完整执行键盘自检序列,自检时主动生成测试按键扫描码存入控制器缓存,自检结束不会主动清空缓冲区。OS 启动后直接接管硬件,缓存里的旧数据全部遗留下来,一开中断就疯狂触发输入。

四、完整三层修复方案:STI 前后双清理,硬件 + 软件缓冲区全部清空

单纯清理一处缓冲区治标不治本,必须分两步:开中断前清空硬件控制器缓存,开中断后清空内核软件环形缓冲区,两套逻辑配合才能彻底杜绝幽灵输入。

1、新增函数:键盘_清空控制器 (),STI 前清理硬件缓存

键盘驱动.zl新增硬件层清理逻辑,循环读取 0x64 状态端口,只要 OBF 标志置 1,就读取 0x60 端口丢弃残留扫描码;最多循环 16 次防止死锁,避免硬件卡死:

zl


定义 函数 键盘_清空控制器() { # 清空键盘控制器输出缓冲区,清除BIOS遗留扫描码 定义 整数 计数 为 0 定义 整数 状态 为 0 当 (计数 小于 16) { 状态 为 调用 键盘_读状态() # 读取0x64状态端口 状态 与 1 # 判断bit0 OBF是否有数据 如果 (状态 不等于 0) { 端口输入(96) # 读取0x60,丢弃残留扫描码 } 计数 加 1 } 返回 0 }

2、新增函数:键盘_清空缓冲区 (),STI 后清理内核软件缓存

就算硬件清完,STI 指令生效到主循环启动的间隙,仍可能瞬间产生中断输入,需要重置内核全部键盘环形缓冲区、命令缓冲区标记,彻底清空软件层残留:

zl


定义 函数 键盘_清空缓冲区() { # 重置内核所有键盘缓存标记,丢弃开中断瞬间产生的虚假输入 调用 内存_写字节(328192, 0) # 缓冲区头指针归零 调用 内存_写字节(328193, 0) # 缓冲区尾指针归零 调用 内存_写字节(328194, 0) # 缓存计数清零 调用 内存_写字节(328198, 0) # 中断命令长度清零 调用 内存_写字节(328200, 0) # 扩展扫描码前缀标志清零 调用 内存_写字节(328448, 0) # 命令缓冲区数据清零 返回 0 }

3、修改内核启动流程,调整 STI 执行顺序

修改build_kernel.py启动代码,严格遵循「清硬件→开中断→清软件缓存」顺序:

python


# 第一步:STI开启中断前,清空BIOS残留的硬件扫描码 调用 键盘_清空控制器() # 第二步:全局开启硬件中断STI STI指令 # 第三步:STI执行完成后,清空内核软件键盘缓冲区 调用 键盘_清空缓冲区()

五、真机修复验证,系统终于干净稳定

重新编译内核刷入 U 盘真机启动,开机日志恢复正常,只打印一个干净提示符,再也没有自动换行:

plaintext


[AHCI] 初始化完成 Chinese OS v0.1 baremetal boot OK [0] >

手动输入命令、字母全部正常识别,无任何多余虚假输入:

plaintext


[0] > help Cmds: h(help) c(cls) m(mem) t(time) e(echo) w(wtest) ... [0] > abcdef

不管反复重启多少次,开机都不会再自动弹出多余回车,BUG 彻底根治。

六、裸机开发 4 条血泪经验,新手一定要记牢


  1. 千万不要把 QEMU 模拟器当成验收标准
    模拟器会简化大量硬件真实行为,BIOS 自检、硬件缓存残留、MMIO 访问限制这类隐性 BUG,只有真机才能暴露。模拟器跑通不代表硬件兼容,真机测试必不可少。
  2. 所有硬件初始化,默认硬件全是 “脏状态”
    BIOS 开机自检会篡改定时器、键盘控制器、内存映射、PCI 设备等各类硬件寄存器 / 缓冲区。OS 接管硬件第一件事,必须重置、清空所有外设状态,不能默认硬件是空缓存。
  3. 开启中断 STI 要做双层缓存清理
    只在 STI 前清理硬件远远不够:STI 指令生效存在时间差,间隙里硬件仍会触发中断写入内核缓存;正确流程是:清硬件缓存 → STI 开中断 → 清空内核软件缓冲区。
  4. 固定硬件初始化标准流程,规避大量隐性 BUG
    后续开发统一遵循这套模板,大幅减少诡异硬件故障:
    ① 初始化外设、清空硬件缓冲区,重置硬件状态
    ② 执行 STI 开启全局中断
    ③ 清空内核软件环形缓冲区,丢弃启动瞬间虚假输入
    ④ 进入系统主循环正常工作

写在最后

从零自研中文操作系统、自制中文编译器这段时间,踩过 A20 线、MMIO 访问、BIOS 残留硬件缓存一大堆模拟器和真机行为不一致的坑。底层开发最折磨人的就是这种无崩溃、无报错、只在真机发作的幽灵 BUG。

如果你也在写裸机内核、PS/2 键盘驱动、x86 底层程序,一定要在 STI 前后做好键盘控制器缓存清理,别像我一样被 BIOS 遗留的扫描码卡调试。

欢迎做自制 OS、底层汇编、编译器开发的朋友在评论区聊聊你们遇到过哪些模拟器正常、真机直接翻车的奇葩 BUG!

#自制操作系统 #裸机编程 #x86 底层开发 #键盘驱动 #8042 控制器 #操作系统踩坑实录 #自研编程语言

编辑于 2026-07-31 · 著作权归作者所有