页框分配致命盲区:随便释放 1MB 以上内存,直接把内核代码清零卡死

0 阅读7分钟

开发阶段:H2 真机磁盘适配第 5 轮 时间:2026-07-12 修复状态:✅ 已修复

做中文裸机 OS 调内存管理的时候踩了个超级致命的低级坑,模拟器跑完全没问题,真机启动到页表初始化直接卡死,排查半天才发现:我写的内存释放区间刚好覆盖了内核本身的代码段,第一次分配内存直接把系统自身代码清零,CPU 跑垃圾指令直接宕机。

一、离谱的真机卡死现场

当时 AHCI 磁盘驱动已经能跑通基础逻辑,页表映射也写完了,在 QEMU 模拟器里启动、进 Shell、读写文件全部丝滑,一点毛病没有。 结果把镜像烧进 U 盘真机开机,日志固定卡在页表打印那一行,之后彻底死寂:

plaintext [BOOT] 2 PL ACBDS20H40K [IDT] 完整初始化 [MEM] MAP: 4213198848 ← 卡死在这里,再也无输出

打印的 4213198848 就是 CR3 页表基地址,之后既不重启、不报通用保护异常,键盘按烂也没反应,屏幕定格不动,完全无从下手。

二、分段打印标记锁定崩溃区间

没办法只能老办法,在内存初始化、页框分配、AHCI 启动每一步都加 VGA 打印标记,分段缩小故障范围: 标记 M1:内存_初始化 () 执行完成 标记 M2:第一次调用页框_分配 () 标记 M3:AHCI 驱动初始化完成 标记 M4:Shell 主循环入口

真机反复测试后得到结果:M1 正常打印,M2 完全没输出。 故障精准锁定:内存初始化跑完,第一次申请物理页框直接崩溃。 一开始以为是页表、CR3 配置出问题,后面越调试越发现不对劲 —— 这不像是异常报错,更像是内核运行的代码本身被抹掉了。

三、根因:释放区间把内核代码标记成空闲

先给大家贴我真机完整物理内存布局,所有分段都是我引导程序硬写死的:

plaintext 0x000000 - 0x00FFFF BIOS + 二级引导代码 0x010000 - 0x04FFFF boot引导第二阶段 0x050000 - 0x05FFFF 桥梁缓冲区(键盘缓存、函数指针) 0x100000 - 0x14FFFF 【内核kernel.bin代码段】重点保护区 0x180000 - 0x1FFFFF 全局变量、内核栈 0x400000 - 0x403FFF Ring3用户程序与栈 0x404000 往上 真正闲置空闲内存

我的页框分配器用位图管理内存,逻辑规则是:

  1. 初始化时所有内存全部标记为「已占用」
  2. 只手动释放确定闲置的内存段,标记为空闲
  3. 分配时从低地址往上遍历,找到第一个空闲 4K 页返回地址,顺带整页清零

翻内存管理.zl 里位图初始化代码,一眼看到致命一行:

zl # 出事的旧代码 调用 释放_区域(1048576, 33554431)

1048576 换算十六进制就是 0x100000,也就是 1MB 起始位置。 这段代码直接把 0x100000 ~ 0x1FFFFFF 整块内存全部标记成空闲页!

完整崩溃连锁灾难

  1. 位图标记 0x100000 为空闲页框
  2. 第一次调用页框_分配,从低地址找空闲,直接返回 0x100000
  3. 分配函数默认会对拿到的页框执行全页清零操作
  4. 0x100000 正是内核代码开头 4KB,系统运行代码直接被全部置 0
  5. CPU 接下来要执行指令,读到一堆 0x00 空字节,属于非法无效指令
  6. 没有抛出异常、不会三重故障,整机直接静止卡死

为什么 QEMU 没事?

模拟器内存模拟规则和真机 CPU 执行逻辑差异巨大: QEMU 对非法空指令容错更高,而且内存寻址模拟不会严格按硬件执行流程卡死;真机 CPU 读到全 0 指令直接停滞,这也是很多模拟器正常、真机暴毙的典型底层坑。

四、两层修复,两次踩坑才敲定安全释放起点

第一步:改掉释放起始地址

原代码:从 0x100000(内核开头)释放

zl 调用 释放_区域(1048576, 33554431)

第一轮修改:改成从 0x400000 开始释放,本以为万事大吉,结果又翻车。 0x400000~0x403FFF 是我预留的 Ring3 用户代码区域,AHCI 申请内存时刚好覆盖这块,执行 iretq 切换特权级直接三重故障重启。

最终稳定修复方案

释放起始地址改为 4210688,对应十六进制 0x404000,跳过 Ring3 整块内存:

zl # 修复后安全代码 调用 释放_区域(4210688, 33554431)

完整永久内存保留清单

以后新增任何内存段,都要同步避开以下区域,绝对不能释放:

  1. 0x00000 ~ 0x04FFFF:BIOS、引导程序
  2. 0x05000 ~ 0x05FFFF:底层通信桥梁区
  3. 0x10000 ~ 0x14FFFF:内核可执行代码
  4. 0x18000 ~ 0x1FFFFF:全局变量、内核栈
  5. 0x40000 ~ 0x403FFF:Ring3 用户程序

只有 0x404000 及以上地址,才是可以放心分配的安全空闲内存。

五、真机完整修复验证

重新编译烧录 U 盘,真机完整启动日志正常输出:

plaintext [BOOT] 2 PL ACBDS20H40K [IDT] 完整初始化 [MEM] MAP: 4213198848 [MEM] M1:OK ; 内存初始化完成 [MEM] M2:4210688 ; 首次分配返回0x404000,不再碰内核 ✅ [AHCI] 初始化完成 Chinese OS v0.1 baremetal boot OK [0] > help Cmds: h(help) c(cls) m(mem) t(time) e(echo) w(wtest) ...

首次分配拿到的页框地址完全在内核保护区之外,不会覆盖任何系统代码,Shell、磁盘读写全部正常运行,BUG 彻底根治。

六、裸机内存开发 4 条血泪教训

  1. 页框释放区间必须逐条核对内存布局 只要释放范围包含代码、栈、缓存、用户程序任意一块,分配清零操作直接摧毁系统,每次新增内存段都要同步修改分配器。
  2. 位图初始化优先保守策略 正确设计逻辑:默认全内存标记占用,只释放百分百确认闲置的地址;拿不准的区域一律不释放,宁可浪费内存不要冒险。
  3. 低地址空闲页是最大隐患 分配器从低地址往上遍历,最先释放的内存会最先被分配使用,低地址区间一旦误释放,第一波分配直接撞内核。
  4. 内存布局改动是强联动点 修改引导加载地址、扩大内核体积、新增 Ring3 程序、扩充栈空间之后,必须同步检查页框分配器释放代码,漏掉一步真机必崩。

结尾碎碎念

自研中文裸机 OS,内存管理这块隐性 BUG 真的防不胜防,代码逻辑看着完全通顺,只是一个起始数字写错,就能让整机直接卡死。很多书本教程只会讲分配算法,不会提醒真机下内存段保护的坑,真机调试的踩坑经验才是独一份的干货。

有没有写裸机内存管理踩过同类覆盖内核 BUG 的朋友,评论区交流一下踩坑经历!

#自制操作系统 #裸机编程 #x86 底层 #内存管理 #页框分配器 #操作系统踩坑 #中文 OS 开发