3、早期内核初始化陷阱:setup_arch、trap_init、mm_init等早期函数中的常见卡住点

早期内核初始化,说白了就是内核刚接手硬件控制权的那段「婴儿期」。这时候连基本的内存管理都没建好,打印个日志都得小心翼翼。我调试过不少启动卡住的案子,十有八九都栽在这几个函数里。

为什么会这样?因为这时候内核太脆弱了。没有页表、没有中断、连个像样的栈都靠不住。任何一个指针越界,直接就是死机,连个Oops都来不及给你看。

3.1 setup_arch:架构相关的第一道坎

setup_arch是每个架构必须实现的函数。它负责解析设备树、建立初始页表、检测内存布局。我见过最典型的卡住点,就是设备树解析失败。

核心问题:设备树(DTB)地址不对,或者格式损坏,内核直接卡死在 early_init_dt_scan 里。

// 典型的设备树解析流程
void __init setup_arch(char **cmdline_p)
{
    // 1. 解析设备树
    early_init_dt_scan_nodes();
    
    // 2. 建立初始页表
    paging_init();
    
    // 3. 检测内存
    memblock_init();
    
    // 4. 初始化CPU
    cpu_init();
}

我个人习惯,遇到卡在这里的情况,先检查两个东西:

  • DTB地址是否对齐——很多bootloader传参时没注意对齐,内核读着读着就崩了
  • 内存节点是否合法——reg属性里的地址范围,有没有重叠?有没有超出物理地址空间?

小技巧:early_init_dt_scan 之前加一个 early_printk,打印DTB的起始地址和魔数(0xd00dfeed)。如果魔数不对,说明bootloader传参有问题。

3.2 trap_init:中断向量表的「空窗期」

trap_init负责初始化异常向量表。嗯,这里要注意:在它完成之前,任何异常都会导致不可预知的行为。

我记得有一次,客户报告说内核在启动早期随机死机。查了半天,发现是某个外设在初始化时触发了中断,但向量表还没设好,CPU直接跳到了0地址。

void __init trap_init(void)
{
    // 设置异常向量表基地址
    set_exception_vector(0, &__start_vectors);
    
    // 初始化各个异常处理入口
    set_handler(EXCEPTION_SYNC, sync_handler);
    set_handler(EXCEPTION_IRQ, irq_handler);
    set_handler(EXCEPTION_FIQ, fiq_handler);
    
    // 注意:这里有个时间窗口
    // 在set_handler完成之前,中断必须保持关闭
}

避坑指南:我曾经在调试一个ARM64平台时,发现 trap_init 里调用了 local_irq_enable。结果中断一开,还没注册完的定时器中断直接让系统崩溃。记住:trap_init 执行期间,必须保证所有中断是关闭的。

3.3 mm_init:内存管理的「先有鸡还是先有蛋」

mm_init 负责建立完整的内存管理子系统。这里有个经典悖论:你要分配内存来初始化内存管理器,但内存管理器还没初始化好。

内核的解决方案是 memblock 分配器——一个极简的内存分配器,只负责在早期阶段管理物理内存。

void __init mm_init(void)
{
    // 1. 初始化memblock分配器
    memblock_init();
    
    // 2. 建立页表
    paging_init();
    
    // 3. 初始化slab分配器
    kmem_cache_init();
    
    // 4. 初始化页分配器
    page_alloc_init();
}

你想想看,如果 memblock_init 里读到的内存信息是错的,后面所有分配都会出问题。我遇到过最坑的一次,是UEFI固件报告的内存映射里,把保留区域标记成了可用内存。结果内核把固件占用的内存分配出去了,一访问就挂。

阶段 分配器 特点 常见卡住点
setup_arch memblock 极简,只管理物理页 内存信息错误
mm_init早期 memblock + bootmem 过渡阶段 页表建立失败
mm_init后期 slab + buddy 完整分配器 slab初始化顺序

3.4 实战:一个典型的卡住案例

说个我亲身经历的事。某款ARM SoC,内核启动到 setup_arch 就卡死了,没有任何打印。加了 early_printk 后,发现卡在 early_init_dt_scan_memory 里。

查设备树,发现内存节点的 reg 属性写的是:

memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x80000000>;
};

看起来没问题?但仔细看,起始地址是0x80000000,大小是0x80000000(2GB)。问题在于,这颗SoC的物理地址空间只有0x80000000到0xFFFFFFFF,刚好2GB。但 memblock_add 时,内核会尝试把 0x80000000 + 0x80000000 = 0x100000000 作为结束地址——这超出了32位地址范围,导致溢出,变成0。

嗯,说白了就是地址计算溢出了。修复方法很简单,在设备树里把内存大小改成 0x7FFFFFFF,或者让bootloader在传参时做一次校验。

教训:早期初始化代码里,任何地址计算都要考虑溢出。尤其是物理地址范围刚好卡在边界上的时候。

3.5 调试早期卡住的通用方法

早期卡住最大的难点,就是没有日志。我总结了一套自己的调试流程:

  1. 加early_printk——在怀疑卡住的函数前后各加一个打印,缩小范围
  2. 检查硬件状态——用JTAG或硬件调试器,看PC指针停在哪里
  3. 验证输入参数——设备树地址、内存大小、CPU类型,这些参数对不对?
  4. 检查依赖顺序——A函数是不是依赖B函数先执行?有没有循环依赖?

个人经验:我习惯在 start_kernel 开头加一个无条件循环,然后用调试器单步执行。这样能精确控制内核的执行节奏,避免被异步事件干扰。

早期初始化就像走钢丝,一步错就全盘崩。但只要掌握了这几个关键函数的内部逻辑,再配合合适的调试手段,大部分卡住问题都能快速定位。

早期内核初始化流程与卡住点 start_kernel setup_arch ⚠ DTB解析失败 / 内存信息错误 trap_init ⚠ 中断提前触发 / 向量表未就绪 mm_init ⚠ 页表建立失败 / slab顺序错误 后续初始化 高风险函数 常见卡住点 安全通过

公众号:蓝海资料掘金营,微信deep3321