Skip to content

🔥 内核加载踩栈问题:从"神秘崩溃"到"精确定位"

一、问题现象

症状:修改 MINI_KERNEL_LOAD_SEG0x1000 改成 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)

崩溃过程还原

  1. load_kernel_from_disk 被调用,返回地址压入栈(约 0x18000)
  2. 第一次磁盘读(127 扇区)写入 0x10000 ~ 0x1FE00
  3. 返回地址被覆盖!
  4. 函数返回时 ret 从栈中弹出错误的地址
  5. 跳飞到未知位置 → 系统崩溃

为什么难以调试

  • 崩溃发生在 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 = 0xFFFE

QEMU monitor 检查

(qemu) xp/40xw 0x9000   # 查看栈区域内容
(qemu) xp/16xw 0x10000  # 查看内核加载区域

八、配置修改总结

修改的文件

文件修改内容
boot/common/boot.SMINI_KERNEL_LOAD_PHYS = 0x20000, MINI_KERNEL_LOAD_SEG = 0x2000
boot/stage2.S注释更新为 →0x20000pushl $0x20000
boot/elf_loader.cKERNEL_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. 崩溃时的调试思路

当遇到神秘崩溃时:

  1. ✅ 检查返回地址是否被覆盖(栈被破坏)
  2. ✅ 检查全局变量是否被覆盖(数据被破坏)
  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) ✅

035_multi_terminal-40-g5d72b8b · 5d72b8b · 2026-06-26