ARM平台设备树配置与内核移植工程案例详解 - 第4节

4.1 内核编译步骤与配置体系

ARM Linux 内核的编译并非简单的 make,而是一个包含配置、依赖生成、交叉编译、目标格式转换的多阶段过程。对于嵌入式平台,编译前必须通过设备树(DTS)和内核配置(Kconfig)完成硬件描述与功能裁剪。

4.1.1 内核源码树结构概览

目录/文件作用与移植相关度
arch/arm/ARM 体系结构相关代码,含启动、MMU、中断★★★★★
arch/arm/boot/dts/设备树源文件(.dts / .dtsi)★★★★★
arch/arm/mach-xxx/具体 SoC 平台板级代码(新内核已弱化)★★★☆☆
drivers/各类设备驱动★★★★☆
include/dt-bindings/设备树绑定头文件(宏定义、中断号等)★★★★☆
scripts/编译脚本、设备树编译工具、打包工具★★☆☆☆

4.1.2 标准编译步骤

  1. 清理:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
  2. 配置:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xxx_defconfig
  3. 菜单配置(可选):make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
  4. 编译内核与设备树:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage dtbs -j4
  5. 生成 uImage(如需要):make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- uImage
  6. 编译模块:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
  7. 安装模块:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=/path/to/rootfs
实践提示:在嵌入式开发中,zImage 是自解压内核镜像,uImage 是在 zImage 前添加 64 字节 U-Boot 头(含加载地址、入口点、CRC 校验)。多数现代 U-Boot 已支持直接引导 zImage + 设备树,uImage 不再是必须。

4.2 zImage 与 uImage 生成原理

4.2.1 编译链接与镜像生成流程

内核编译的最终产物是压缩后的自解压镜像。其生成链路如下:

编译链路:
vmlinux (ELF) → arch/arm/boot/compressed/vmlinux (重定位+压缩) → zImage (自解压) → uImage (加 U-Boot 头)
zImage 与 uImage 生成流程 zImage / uImage 生成流程 vmlinux (ELF) arch/arm/boot/ compressed/vmlinux zImage uImage 链接所有 .o 生成 未压缩 ELF 内核 piggy.gzip + 解压 stub 重定位后压缩 自解压头 + 压缩内核 可直接被 bootloader 加载 zImage + 64B U-Boot 头 含 loadaddr / entry / CRC

4.2.2 zImage 与 uImage 对比

特性zImageuImage
文件头自解压头(约 20 字节)64 字节 U-Boot 镜像头
加载地址由自解压代码动态重定位头中 ih_load 字段指定
入口点自解压代码起始头中 ih_ep 字段指定
校验无内置校验头含 CRC32 校验
U-Boot 兼容性需 U-Boot 2014+ 且支持 zImage所有 U-Boot 版本
生成命令make zImagemake uImage 或 mkimage
常见错误:编译 uImage 时若未指定 LOADADDR,默认使用 0x8000(SDRAM 起始偏移)。对于 S3C2440 等平台,应显式指定:make LOADADDR=0x30008000 uImage,否则内核无法正确解压。

4.3 内核启动第一阶段汇编代码分析

ARM 内核启动的第一阶段由汇编语言编写,位于 arch/arm/kernel/head.S 和 arch/arm/boot/compressed/head.S。这一阶段负责从 Bootloader 接管控制权,完成最低限度的硬件初始化,并跳转到 C 语言入口 start_kernel。

4.3.1 启动阶段划分

ARM 内核启动第一阶段流程 ARM 内核启动第一阶段(汇编阶段) Bootloader 跳转 (r0=0, r1=machid, r2=dtb) 设置 SVC 模式 关中断 (cpsr) CPU 类型检测 处理器 ID 匹配 创建页表 段映射 / 节映射 使能 MMU 跳转 start_kernel r0 = 0 (sb) r1 = 机器类型 / r2 = FDT 地址 cpsr 设置:SVC32 模式 I/F 位置 1 关中断 读取 MIDR / 匹配 proc_info 获取 __cpu_proc_init __create_page_tables 1:1 映射 + 内核映射 __enable_mmu __mmap_switched → start_kernel

4.3.2 关键汇编代码段解析

以下代码取自 arch/arm/kernel/head.S,是内核启动入口 stext 的核心逻辑:

/*
 * 内核入口点 stext
 * r0 = 0 (sb 值)
 * r1 = 机器类型号 (传统 ATAG 方式)
 * r2 = 设备树 DTB 物理地址 (FDT 方式)
 */
ENTRY(stext)
    /* 设置当前 CPU 为 SVC 模式,关中断 */
    msr    cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE

    /* 查找当前 CPU 的 proc_info 结构 */
    bl    __lookup_processor_type
    /* 若未找到,跳转错误 */
    teq    r10, #0
    beq    __error_p

    /* 查找机器类型或 FDT 匹配的 machine_desc */
    bl    __lookup_machine_type
    teq    r8, #0
    beq    __error_a

    /* 创建一级页表(段映射) */
    bl    __create_page_tables

    /* 使能 MMU,跳转到 __mmap_switched */
    ldr    r13, =__mmap_switched
    adr    lr, BSYM(__enable_mmu)
    mov    pc, lr
ENDPROC(stext)

4.3.3 页表创建与 MMU 使能

__create_page_tables 是启动阶段最关键的汇编函数之一。它建立恒等映射(identity mapping)和内核虚拟地址映射:

__create_page_tables:
    /* 页表基址存放在 r4 (swapper_pg_dir) */
    pgtbl    r4, r8

    /* 清空 16KB 页表空间 */
    mov    r0, r4
    mov    r3, #0
    add    r6, r0, #0x4000
1:    str    r3, [r0], #4
    str    r3, [r0], #4
    str    r3, [r0], #4
    str    r3, [r0], #4
    teq    r0, r6
    bne    1b

    /* 建立恒等映射:物理地址 = 虚拟地址 (前 1MB) */
    ldr    r7, [r10, #PROCINFO_IO_OFFSET]
    orr    r3, r7, #MMU_SECTION_FLAGS
    str    r3, [r4, r7, lsr #18]

    /* 建立内核虚拟地址映射 (PAGE_OFFSET) */
    ldr    r6, =PAGE_OFFSET
    orr    r3, r7, #MMU_SECTION_FLAGS
    str    r3, [r4, r6, lsr #18]

    /* 映射 DTB 区域(确保 MMU 开启后仍可访问) */
    mov    r6, r2
    orr    r3, r6, #MMU_SECTION_FLAGS
    str    r3, [r4, r6, lsr #18]

    mov    pc, lr
ENDPROC(__create_page_tables)
关键寄存器说明:
r4:一级页表基址(swapper_pg_dir,位于内核 BSS 段末尾)
r7:当前物理地址基址(从 proc_info 获取)
r2:DTB 物理地址(由 Bootloader 传入)
恒等映射:MMU 开启瞬间,PC 仍指向物理地址,必须保证物理地址可访问。
内核映射:将内核从物理地址映射到 PAGE_OFFSET(通常 0xC0000000)。

4.3.4 跳转到 start_kernel

MMU 使能后,通过 __mmap_switched 完成数据段复制、BSS 清零、保存处理器 ID 和机器类型,最终跳转到 C 入口:

__mmap_switched:
    /* 复制数据段 */
    adr    r3, __data_loc
    ldmia    r3!, {r4, r5, r6}
    sub    r3, r3, #12
    cmp    r4, r5
    beq    2f
1:    ldr    r6, [r3], #4
    str    r6, [r4], #4
    cmp    r4, r5
    blo    1b
2:    /* 清零 BSS */
    mov    r0, #0
    ldr    r3, =__bss_start
    ldr    r4, =__bss_stop
3:    str    r0, [r3], #4
    cmp    r3, r4
    blo    3b

    /* 保存启动参数 */
    ldr    r0, =__machine_arch_type
    str    r1, [r0]        @ 保存机器类型
    ldr    r0, =__atags_pointer
    str    r2, [r0]        @ 保存 DTB 指针

    /* 跳转到 start_kernel */
    b    start_kernel
ENDPROC(__mmap_switched)

4.4 设备树在启动流程中的传递

在支持设备树(FDT)的启动方式中,Bootloader 将 DTB 物理地址通过 r2 寄存器传递给内核。内核在汇编阶段完成以下操作:

  1. 保存 r2:在 __mmap_switched 中将 r2 存入 __atags_pointer 全局变量。
  2. 映射 DTB 区域:在 __create_page_tables 中为 DTB 所在物理页建立段映射,确保 MMU 开启后仍可访问。
  3. 解析 DTB:C 语言阶段 setup_arch → early_init_dt_scan 解析 DTB,获取内存大小、中断控制器、时钟等基础信息。
设备树启动 vs 传统 ATAG 启动:
传统方式:r1 = 机器类型号,r2 = ATAG 链表地址(已废弃)
设备树方式:r1 = 0(未使用),r2 = DTB 物理地址(推荐)
内核通过 CONFIG_ARM_APPENDED_DTB 也支持将 DTB 附加在 zImage 末尾,此时 r2 可忽略。

4.5 常见编译与启动问题排查

现象可能原因解决方案
编译报错 arch/arm/boot/zImage: No such file未指定 ARCH 或 CROSS_COMPILEexport ARCH=arm; export CROSS_COMPILE=arm-linux-gnueabihf-
U-Boot 无法引导 zImageU-Boot 版本过旧,不支持 zImage 引导使用 mkimage 生成 uImage,或升级 U-Boot
启动卡在 Uncompressing Linux...解压地址与加载地址冲突检查 LOADADDR 是否与 SDRAM 起始地址重叠
启动后无控制台输出设备树中 chosen/stdout-path 未配置在 DTS 中添加 chosen { stdout-path = &uart0; };
MMU 开启后异常恒等映射未正确建立检查 __create_page_tables 中物理地址计算
调试建议:在汇编阶段插入 printascii 调试(需提前初始化 UART),或在 U-Boot 中使用 bootz 命令时添加 console=ttyS0,115200 内核参数。对于 MMU 相关异常,可使用 earlyprintk 在 MMU 开启后立即输出。