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 标准编译步骤
- 清理:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean - 配置:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xxx_defconfig - 菜单配置(可选):
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig - 编译内核与设备树:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage dtbs -j4 - 生成 uImage(如需要):
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- uImage - 编译模块:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules - 安装模块:
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 头)
4.2.2 zImage 与 uImage 对比
| 特性 | zImage | uImage |
|---|---|---|
| 文件头 | 自解压头(约 20 字节) | 64 字节 U-Boot 镜像头 |
| 加载地址 | 由自解压代码动态重定位 | 头中 ih_load 字段指定 |
| 入口点 | 自解压代码起始 | 头中 ih_ep 字段指定 |
| 校验 | 无内置校验 | 头含 CRC32 校验 |
| U-Boot 兼容性 | 需 U-Boot 2014+ 且支持 zImage | 所有 U-Boot 版本 |
| 生成命令 | make zImage | make 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 启动阶段划分
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 仍指向物理地址,必须保证物理地址可访问。
内核映射:将内核从物理地址映射到
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 寄存器传递给内核。内核在汇编阶段完成以下操作:
- 保存 r2:在
__mmap_switched中将 r2 存入__atags_pointer全局变量。 - 映射 DTB 区域:在
__create_page_tables中为 DTB 所在物理页建立段映射,确保 MMU 开启后仍可访问。 - 解析 DTB:C 语言阶段
setup_arch→early_init_dt_scan解析 DTB,获取内存大小、中断控制器、时钟等基础信息。
设备树启动 vs 传统 ATAG 启动:
传统方式:r1 = 机器类型号,r2 = ATAG 链表地址(已废弃)
设备树方式:r1 = 0(未使用),r2 = DTB 物理地址(推荐)
内核通过
传统方式: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_COMPILE | export ARCH=arm; export CROSS_COMPILE=arm-linux-gnueabihf- |
| U-Boot 无法引导 zImage | U-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 开启后立即输出。