正常
🔥 内核加载踩栈问题:从"神秘崩溃"到"精确定位"
一、问题现象
症状:修改 MINI_KERNEL_LOAD_SEG 从 0x1000 改成 0x2000 后内核正常加载,但之前用 0x1000 时系统会神秘崩溃。
asm
// boot/common/boot.S
.set MINI_KERNEL_LOAD_SEG, 0x1000 // ❌ 崩溃!
.set MINI_KERNEL_LOAD_SEG, 0x2000 // ✅ 正常
.set MINI_KERNEL_LOAD_SEG, 0x5000 // ✅ 正常(之前的临时方案)崩溃特征:
- 没有明显的错误信息
- 不是立即崩溃,而是在
load_kernel_from_disk函数返回时 - 栈完全混乱,调试器无法追踪
二、初步排查:内存布局看似正确
理论分析(看似正确)
Kernel 大小: 1024 扇区 = 512KB = 0x80000 字节当 MINI_KERNEL_LOAD_SEG = 0x1000 时:
物理起始地址 = 0x1000 << 4 = 0x10000
物理结束地址 = 0x10000 + 0x80000 = 0x90000
Kernel 占用: 0x10000 ~ 0x90000此时 stage2 代码在 0x8000,理论上没有冲突。
❓ 疑问
既然 0x90000 已经远低于 stage2 的代码地址,为什么会崩溃?
三、关键发现:栈的位置
stage2.S 中的栈初始化
asm
// stage2.S, 第 11-15 行
movw $0x0900, %ax // SS = 0x0900
movw %ax, %ss
movw $0xFFFE, %sp // SP = 0xFFFE栈的物理地址范围
栈顶物理地址 = SS << 4 + SP
= 0x0900 << 4 + 0xFFFE
= 0x9000 + 0xFFFE
= 0x18FFE ≈ 0x19000
栈向下增长,假设最大 64KB:
栈底 ≈ 0x18FFE - 0xFFFF = 0x9000
栈范围: 0x9000 ~ 0x19000四、真相大白:内存冲突分析
当 MINI_KERNEL_LOAD_SEG = 0x1000 时
┌─────────────────────────────────────────────────────────────┐
│ 内存地址 │ 内容 │
├─────────────────────────────────────────────────────────────┤
│ 0x08000 │ stage2 代码 │
│ 0x09000 ~ 0x19000│ ██████ 栈 ██████ │
│ 0x10000 ~ 0x90000│ ████████████████ Kernel (512KB) ████████ │
└─────────────────────────────────────────────────────────────┘
重叠区域: 0x10000 ~ 0x19000 (约 40KB)崩溃过程还原
load_kernel_from_disk被调用,返回地址压入栈(约 0x18000)- 第一次磁盘读(127 扇区)写入
0x10000 ~ 0x1FE00 - 返回地址被覆盖!
- 函数返回时
ret从栈中弹出错误的地址 - 跳飞到未知位置 → 系统崩溃
为什么难以调试
- 崩溃发生在
ret时,不在磁盘读取代码内部 - 栈内容已被破坏,无法通过栈追踪
- GDB 断点可能在覆盖后已失效
五、正确的内存布局
方案:MINI_KERNEL_LOAD_SEG = 0x2000
物理起始地址 = 0x2000 << 4 = 0x20000
物理结束地址 = 0x20000 + 0x80000 = 0xA0000┌─────────────────────────────────────────────────────────────┐
│ 内存地址 │ 内容 │
├─────────────────────────────────────────────────────────────┤
│ 0x08000 │ stage2 代码 │
│ 0x09000 ~ 0x19000│ ██████ 栈 ██████ │
│ 0x20000 ~ 0xA0000│ Kernel (512KB) │
└─────────────────────────────────────────────────────────────┘
0x20000 > 0x19000 ✅ 完全没有重叠!安全间隙
栈顶: 0x19000
Kernel 起点: 0x20000
───────────────────────
间隙: 0x7000 (28KB) ✅ 安全!六、完整内存布局图
展开代码 (共 21 行)收起代码
┌─────────────────────────────────────────────────────────────────┐
│ 0x00000 ~ 0x00FFF │ IVT / BDA │
├─────────────────────────────────────────────────────────────────┤
│ 0x01000 ~ 0x03FFF │ 页表 (setup_page_tables) │
├─────────────────────────────────────────────────────────────────┤
│ 0x04000 ~ 0x04FFF │ [空闲] │
├─────────────────────────────────────────────────────────────────┤
│ 0x05000 ~ 0x05FFF │ E820 buffer │
├─────────────────────────────────────────────────────────────────┤
│ 0x06000 ~ 0x063FF │ VESA controller info │
├─────────────────────────────────────────────────────────────────┤
│ 0x06400 ~ 0x0640F │ VESA mode info │
├─────────────────────────────────────────────────────────────────┤
│ 0x07B00 ~ 0x07B10 │ DAP (Disk Address Packet) │
├─────────────────────────────────────────────────────────────────┤
│ 0x08000 ~ 0x08FFF │ stage2 代码 │
├─────────────────────────────────────────────────────────────────┤
│ 0x09000 ~ 0x19000 │ ███████ 栈 ███████ │
├─────────────────────────────────────────────────────────────────┤
│ 0x20000 ~ 0xA0000 │ Kernel ELF (512KB) ← MINI_KERNEL_LOAD_SEG=0x2000 │
└─────────────────────────────────────────────────────────────────┘七、验证方法
调试时检查 SS 和 SP
asm
// 在 load_kernel_from_disk 开头添加
movw %ss, %ax
outw %ax, $0xe9 // 输出 SS 到 debug console
movw %sp, %ax
outw %ax, $0xe9 // 输出 SP 到 debug console预期输出:
SS = 0x0900
SP = 0xFFFEQEMU monitor 检查
(qemu) xp/40xw 0x9000 # 查看栈区域内容
(qemu) xp/16xw 0x10000 # 查看内核加载区域八、配置修改总结
修改的文件
| 文件 | 修改内容 |
|---|---|
boot/common/boot.S | MINI_KERNEL_LOAD_PHYS = 0x20000, MINI_KERNEL_LOAD_SEG = 0x2000 |
boot/stage2.S | 注释更新为 →0x20000,pushl $0x20000 |
boot/elf_loader.c | KERNEL_PHYS_BASE = 0x20000 |
关键常量关系
MINI_KERNEL_LOAD_PHYS = 0x20000 # 物理地址
MINI_KERNEL_LOAD_SEG = 0x2000 # 段地址 = 物理地址 >> 4
MINI_KERNEL_LOAD_OFF = 0x0000 # 偏移验证:
0x2000 << 4 = 0x20000 ✅九、经验教训
1. 实模式栈容易被忽略
在保护模式/长模式开发中,我们习惯了在 0xFFFFFFFF80000000 这样的高地址设置栈。
但在实模式中,栈通常位于低内存(如 0x9000:FFFE),很容易被其他数据覆盖。
2. 内存布局必须全局考虑
不能只考虑 "我的代码在哪里"
必须考虑 "栈在哪里" "数据在哪里" "BIOS在哪里"3. 崩溃时的调试思路
当遇到神秘崩溃时:
- ✅ 检查返回地址是否被覆盖(栈被破坏)
- ✅ 检查全局变量是否被覆盖(数据被破坏)
- ✅ 画出完整内存布局图,寻找重叠区域
4. "看起来没问题" ≠ "真的没问题"
0x10000 ~ 0x90000 看似不覆盖 stage2 (0x8000)
但实际覆盖了栈 (0x9000 ~ 0x19000)十、一图胜千言
MINI_KERNEL_LOAD_SEG = 0x1000 的情况:
栈顶: 0x19000 ────────────┐
│ ← 返回地址在这里
Kernel: 0x10000 ~ 0x90000 │
│ ← 磁盘读覆盖这里
│
返回时 ret → 跳飞! ❌ ┘
─────────────────────────────────────────────
MINI_KERNEL_LOAD_SEG = 0x2000 的情况:
栈顶: 0x19000 ────────────┐
│ ← 返回地址安全
间隙: 0x19000 ~ 0x20000 │
Kernel: 0x20000 ~ 0xA0000 │
│ ← 磁盘读写这里,不影响栈
返回时 ret → 正常返回 ✅ │
┘🎯 总结
实模式内核加载时,栈的位置(SS:SP)决定了最小安全加载地址。
公式:
MINI_KERNEL_LOAD_PHYS > (SS << 4 + SP)
在这个案例中:
SS = 0x0900, SP = 0xFFFE
栈顶 ≈ 0x19000
MINI_KERNEL_LOAD_PHYS 必须 > 0x19000
选择 0x20000 (SEG = 0x2000) ✅