4. 内核入口点:start_kernel函数之前的汇编初始化

嵌入式Linux内核的启动并非从C语言编写的start_kernel()开始。在进入第一个C函数之前,内核必须经过一段精心设计的汇编代码阶段。这段汇编代码负责将处理器从引导加载程序(Bootloader)交付的原始状态,转换到C语言执行环境所要求的稳定状态。理解这一阶段是调试早期启动崩溃、移植内核到新硬件平台的基石。

4.1 汇编入口:从Bootloader到内核的交接

Bootloader(如U-Boot、Barebox)在跳转到内核之前,会设置好特定的硬件状态。内核汇编入口点(通常为stext或_start)接收这个状态,并立即执行一系列关键操作。不同架构(ARM、ARM64、RISC-V)的细节不同,但核心逻辑一致。

关键交接契约(以ARM32为例):
  • MMU关闭:Bootloader必须关闭MMU和D-Cache,I-Cache可选关闭。
  • 寄存器状态:r0 = 0,r1 = 机器类型ID,r2 = ATAGS或设备树(DTB)物理地址。
  • 中断关闭:所有中断(IRQ、FIQ)必须处于屏蔽状态。
  • 栈指针:未初始化,内核汇编代码会立即设置临时栈。
调试提示: 如果内核在汇编阶段崩溃(无任何打印输出),首先检查Bootloader是否满足上述交接契约。使用JTAG调试器或硬件断点检查r0、r1、r2的值,以及SCTLR(系统控制寄存器)中MMU和Cache的位状态。

4.2 汇编初始化核心流程

从入口点到start_kernel(),汇编代码通常按以下顺序执行。下表总结了每个阶段的目标和关键操作。

阶段 目标 关键汇编操作 常见调试点
1. 安全模式切换 确保CPU处于SVC(超级用户)模式,关闭所有中断 修改CPSR寄存器,设置I和F位 检查CPSR模式位,确认中断掩码
2. 临时栈建立 为后续C函数调用提供栈空间 设置sp寄存器指向内部SRAM或未使用的DDR区域 栈指针是否指向有效可写内存
3. CPU ID与核检测 识别当前CPU,如果是多核则决定主/从核路径 读取MPIDR(ARM)或mhartid(RISC-V)寄存器 从核是否陷入死循环或WFE
4. 早期页表与MMU初始化 建立恒等映射(Identity Mapping)或临时页表 写入TTBR0/TTBR1,配置MAIR,使能MMU 页表地址对齐、内存属性配置错误
5. 设备树(DTB)处理 验证DTB合法性,复制到安全位置 校验DTB魔数,计算大小,memcpy到内核保留区域 DTB地址越界、魔数错误导致死机
6. 跳转到start_kernel 准备C环境,调用主入口函数 设置C函数的参数(r0, r1, r2),执行b start_kernel 链接脚本中start_kernel地址是否正确

4.3 汇编初始化流程图

下图展示了ARM64架构下,从内核入口到start_kernel()的典型汇编执行路径。该图突出了关键决策点和硬件状态变化。

ARM64 内核汇编初始化流程 ARM64 内核汇编初始化流程(stext → start_kernel) 内核入口 stext 设置SVC模式,关闭IRQ/FIQ 建立临时栈(sp指向SRAM) 读取MPIDR,识别CPU ID 主核? 从核:进入WFE 等待唤醒 主核:继续初始化 建立恒等映射页表 使能MMU与D-Cache 跳转 start_kernel()

4.4 关键汇编代码片段解析

以下以ARM64汇编为例,展示入口点stext中几个关键操作的实现。这些代码位于arch/arm64/kernel/head.S。

4.4.1 保存Bootloader传入参数

/*
 * 内核入口点 stext
 * x0 = 设备树(DTB)物理地址
 * x1 = 0
 * x2 = 0
 * x3 = 0
 */
SYM_CODE_START(stext)
    /* 保存DTB地址到x20,后续会用到 */
    mov x20, x0

    /* 确保当前异常级别为EL1(内核态) */
    bl  __calc_phys_offset      /* 计算物理地址偏移 */
    bl  __primary_switch        /* 主初始化路径 */
SYM_CODE_END(stext)

4.4.2 设置临时栈与异常向量

__primary_switch:
    /* 设置临时栈,指向__init_stack区域 */
    adrp    x25, __init_stack
    add     x25, x25, #512      /* 预留512字节栈空间 */
    mov     sp, x25

    /* 设置异常向量表基址(VBAR_EL1) */
    adrp    x0, vectors
    msr     vbar_el1, x0
    isb

    /* 使能SError和Debug异常,关闭IRQ/FIQ */
    msr     daif, #(DAIF_IRQ | DAIF_FIQ)
    isb

4.4.3 使能MMU(简化版)

__enable_mmu:
    /* 设置页表基址(TTBR0_EL1) */
    msr     ttbr0_el1, x25      /* x25为页表物理地址 */
    isb

    /* 配置内存属性(MAIR_EL1) */
    ldr     x0, =MAIR_EL1_SET
    msr     mair_el1, x0

    /* 配置TCR(Translation Control Register) */
    ldr     x0, =TCR_EL1_FLAGS
    msr     tcr_el1, x0

    /* 使能MMU:设置SCTLR_EL1的M位 */
    mrs     x0, sctlr_el1
    orr     x0, x0, #SCTLR_ELx_M
    msr     sctlr_el1, x0
    isb

    /* 刷新TLB,确保页表生效 */
    tlbi    vmalle1
    dsb     nsh
    isb
    ret
常见陷阱:MMU使能瞬间
使能MMU的指令(msr sctlr_el1, x0)执行后,下一条指令的PC将立即被MMU翻译。如果此时页表中没有包含当前PC地址的有效映射(即恒等映射缺失),CPU将立即触发指令异常(Instruction Abort),导致系统死机。调试时,务必确认页表覆盖了内核代码段所在的物理地址范围。

4.5 汇编阶段调试技术

由于汇编阶段没有控制台输出,调试手段非常有限。以下是嵌入式工程师常用的几种方法:

  • JTAG/SWD硬件调试器:最强大的方法。可以单步执行汇编指令,检查寄存器(SP、LR、SCTLR、TTBR0)和内存(页表内容、栈数据)。推荐使用OpenOCD + GDB组合。
  • LED/GPIO指示灯:在汇编代码中插入GPIO操作。例如,在进入start_kernel前点亮一个LED,如果LED未亮,说明汇编阶段崩溃。
  • 早期打印(Early Printk):部分架构支持在汇编中直接操作UART寄存器输出字符。ARM64的earlycon机制可以在MMU使能前通过物理地址访问串口。
  • QEMU仿真:在QEMU中运行内核,使用-d in_asm或-d cpu参数跟踪每条汇编指令的执行,定位异常指令。
调试案例:MMU使能后死机
现象:内核在汇编阶段卡死,无任何输出。
分析:使用QEMU + GDB,在__enable_mmu函数返回处设置断点。单步执行msr sctlr_el1, x0后,发现PC跳转到0x0地址,触发异常。
根因:页表未包含内核入口地址的恒等映射。修正页表创建逻辑,确保IDMAP_TEXT_OFFSET对应的虚拟地址映射到正确的物理地址。
修复:在arch/arm64/mm/mmu.c的__create_page_tables函数中,增加对内核起始物理地址的恒等映射条目。

4.6 不同架构的差异对比

下表对比了ARM32、ARM64和RISC-V在汇编初始化阶段的关键差异,帮助你在移植或调试不同平台时快速定位问题。

特性 ARM32 ARM64 RISC-V
入口符号 stext 或 _start stext _start
异常级别/模式 SVC模式(CPSR) EL1(异常级别1) M模式或S模式(mstatus)
页表格式 二级页表(L1 + L2) 四级页表(L0-L3) Sv39 / Sv48 页表
设备树传递 r2 寄存器 x0 寄存器 a0 寄存器
多核启动 PSCI或Spin-Table PSCI(通过SMC调用) CLINT/PLIC中断唤醒
MMU使能寄存器 SCTLR(M位) SCTLR_EL1(M位) satp(S-mode)或mstatus(M-mode)

4.7 总结:汇编初始化的核心使命

汇编初始化阶段虽然代码量不大(通常几百行),但它是内核稳定运行的基石。它的核心使命可以概括为三点:

  1. 环境准备:从Bootloader的原始状态,转换到C语言执行环境(栈、异常向量、MMU)。
  2. 硬件抽象:屏蔽不同CPU架构和SoC的启动细节,为后续通用C代码提供统一的接口。
  3. 早期诊断:在没有任何内核服务的情况下,通过GPIO、早期串口或JTAG提供最基本的调试能力。

掌握这一阶段的代码和调试方法,是解决“内核无声无息死掉”这类最棘手问题的关键。下一阶段,我们将进入C语言世界,分析start_kernel()如何初始化内核的核心子系统。