Skip to content

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

一、问题现象 ​

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

崩溃过程还原 ​

  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注释更新为 →0x20000,pushl $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) ✅

r04_protected_mode-1-gc50cfe1 · c50cfe1 · 2026-10-01