欢迎光临
我们一直在努力

U-Boot 跳转到 Linux Kernel 完整流程

U-Boot 把内核镜像、设备树读到内存,做好 ARM64 启动约定(寄存器、内存、异常等级、参数),最后用 br 指令直接跳转到内核入口地址;U-Boot 不再运行,内核接管整个系统。

注意:ARM64 没有传统 32 位的 bootm 入口调用,ARM64 标准命令是 booti。

零、前置前提(ARM64 Linux 启动约定,必须全部满足)

Linux 内核文档 Documentation/arm64/booting.rst 规定的硬性要求,U-Boot 的 booti 就是用来满足这套约定:

1、CPU 处于 EL1(Exception Level 1)

  • SOC平台带 ATF(BL31):上电 BL31 在 EL3,BL31 启动 U-Boot 时把 U-Boot 放在 EL1;U-Boot 跑 booti 时不碰 EL3,直接在 EL1 跳转内核。
  • MMU:跳转前 MMU 必须关闭,内核自己会初始化 MMU、页表。

2、寄存器约定(最重要)

内核入口函数 _text 拿到这几个寄存器,就知道 dtb 在哪里。

  • x0:0(必须是 0)
  • x1:0
  • x2:设备树 blob(dtb)在内存里的物理地址
  • x3:0

3、内存:

内核镜像、dtb 都已经拷贝到物理内存指定位置,地址不能重叠;U-Boot 自身占用的内存区域,内核不能覆盖。

4、缓存:

跳转前必须做 cache 清理(dc civac),保证数据从 cache 刷到主存,避免 dtb / 镜像数据残缺。

5、中断:

DAIF 屏蔽(IRQ、FIQ 关闭),内核上来再自己开中断。

补充:ARM64 内核不支持旧的 ATAGS,只认 DTB,所以 x2 必须是合法 dtb 地址。

阶段 1:booti 命令入口(HUSH 命令层)

HUSH解析器 → run_list_real → run_pipe_real → cmd_call → do_booti()

do_booti 是 U-Boot 里 booti 命令的实现,文件:cmd/boot.c

booti [kernel_addr] [initrd_addr] [dtb_addr]
# 示例:booti 0x42000000 – 0x43000000
# “-”代表没有initramfs

do_booti 主要做:

  • 解析三个参数:kernel 镜像地址、initrd、dtb 地址
  • 校验镜像:如果是 FIT 镜像,校验签名(开了安全启动才会);普通 Image 不校验
  • 调用 boot_prep_linux():准备 Linux 启动环境
  • 调用 boot_jump_linux():真正的跳转准备 + 最后跳转
  • 阶段 2:boot_prep_linux —— 准备工作

    核心工作:

    1、把 dtb 地址合法性检查,复制 dtb(按需),保证 dtb 在 2MB 对齐、不被内核覆盖的安全区域

    2、处理 initramfs(量产一般不用,填 – 就跳过)

    3、准备 bootargs(内核命令行)

    • 从环境变量 bootargs 拿字符串:console=ttyS0 root=/dev/mmcblk0p2 rw rootwait
    • 把 bootargs 写到 dtb 的 chosen 节点 bootargs 属性

    Linux 5.4 优先读 dtb /chosen/bootargs,这就是 U-Boot 传给内核命令行的方式!

    4、内存预留:标记 U-Boot 本身、BL31、ATF、secure 区域,内核设备树里 reserved-memory 节点,内核不会去占用。

    阶段 3:boot_jump_linux —— 跳转前最后一步(重点)

    文件:arch/arm64/lib/boot.c 伪代码逻辑:

    void boot_jump_linux(ulong image, ulong dtb, ulong initrd)
    {
    void (*kernel_entry)(void);
    // 1. 关闭U-Boot自己的定时器、watchdog、外设
    // 2. 刷新cache:dc civac,把cache数据全部回写到DDR
    flush_dcache_all();
    // 3. 关闭MMU(如果U-Boot开了MMU)
    disable_mmu();
    // 4. 关闭中断
    disable_irq();
    // 5. 填充约定寄存器
    kernel_entry = (void (*)(void))image;
    // 准备 x0,x1,x2,x3
    // 汇编:mov x2, dtb_phys_addr
    // 6. 跳转到内核入口
    kernel_entry();
    // 这里一旦跳转成功,后面代码永远跑不到!
    panic("Kernel returned!");
    }

    关键点:kernel_entry 就是内核 Image 的起始地址,也就是内核的 _text 符号。

    阶段 4:汇编跳转(最底层,ARM64)

    U-Boot 汇编代码,设置好 x0~x3,然后一条跳转指令:

    mov x0, #0
    mov x1, #0
    mov x2, dtb_phys
    mov x3, #0
    br x4 // x4里面放内核入口地址

    br 是无条件跳转,跳转之后,CPU 取指地址直接切到内核 Image。

    这一刻:U-Boot 彻底交出控制权,U-Boot 的代码不再执行,栈、全局变量全部作废。

    内核不会返回,一旦返回,说明内核启动崩溃,触发 panic。

    阶段 5:内核接管之后(内核侧启动,不属于 U-Boot)

    内核 _text 入口开始执行:

  • 早期汇编:设置栈、判断 CPU 核、关闭看门狗、初始化页表、打开 MMU
  • 早期 C 代码:解析 dtb(x2 传进来的 dtb),拿到 bootargs、内存信息、串口、时钟、eMMC、GMAC 等资源
  • 初始化驱动、挂载根文件系统、启动用户空间 init
  • 两种镜像格式的区别(Image / FIT)

    1、原始 Image(现在常用) booti 0x42000000 – 0x43000000

    U-Boot 不解析镜像内部,直接把这个地址当成入口,寄存器带 dtb 地址跳转。

    2、FIT 镜像(itb,把 kernel+dtb+initrd 打包在一起)

    bootm 0x42000000

    bootm 会解析 itb,自动提取 kernel、dtb,再调用底层和 booti 一样的跳转逻辑。

    小结

    一条极简时序串起来

    U-Boot autoboot → run bootcmd → mmc read 加载Image到0x42000000
    → mmc read 加载dtb到0x43000000
    → booti 0x42000000 – 0x43000000
    do_booti → boot_prep_linux(填充chosen/bootargs)
    → boot_jump_linux(刷cache、关MMU、关中断)
    → 汇编设置 x0/x1/x2/x3 → br 跳内核入口
    ↓
    Linux _text 开始执行

    赞(0)
    未经允许不得转载:171主机测评 » U-Boot 跳转到 Linux Kernel 完整流程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址