[TOC]
arch/arm/boot: Linux 32位ARM架构的启动基石
arch/arm/boot
目录是 Linux 内核源码中专门为 32 位 ARM 架构(AArch32)设计的启动代码所在地。它的核心使命是充当引导加载程序(Bootloader,如 U-Boot)与体系结构无关的通用内核代码之间的桥梁。这个目录下的代码负责处理内核启动最初始、最底层的阶段,为 C 语言环境的建立和主内核的执行做好万全准备。
一、 核心职责
arch/arm/boot
目录下的代码和构建脚本共同完成了以下几项至关重要的任务:
生成可启动的内核镜像: 该目录下的
Makefile
是构建过程的核心,它负责将编译好的内核vmlinux
(一个 ELF 格式的文件)进行处理,最终生成 Bootloader 可以直接加载和执行的镜像格式,最常见的有zImage
和uImage
。内核解压缩: 为了减小存储体积和加快加载速度,内核镜像通常是经过压缩的。
arch/arm/boot
包含了一个微型的、自解压的前端程序(Decompressor)。当 Bootloader 跳转到内核镜像的入口点时,首先执行的就是这个前端程序,它的唯一任务就是将压缩的内核主体解压到内存的正确位置。处理 Bootloader 传入的参数: Bootloader 需要向内核传递关键的硬件信息和启动参数,以便内核能够正确初始化。
arch/arm/boot
负责处理两种主要的参数传递方式:- 设备树 (Device Tree Blob, DTB): 这是现代 ARM Linux 系统的标准方式。Bootloader 将 DTB 的内存地址传递给内核,
arch/arm/boot
中的代码负责接收并验证这个地址,然后将其传递给主内核。 - ATAGs (ARM Tags): 这是旧版的参数传递机制,主要用于 ARMv5 及更早的架构。
arch/arm/boot
仍然保留了处理 ATAGs 的代码以实现向后兼容。
- 设备树 (Device Tree Blob, DTB): 这是现代 ARM Linux 系统的标准方式。Bootloader 将 DTB 的内存地址传递给内核,
非常早期的硬件初始化: 在主内核的驱动程序开始工作之前,
arch/arm/boot
中的汇编代码会进行一些最基本、最必要的硬件设置,例如:- 确保 CPU 处于正确的模式(通常是 Supervisor 模式)。
- 屏蔽所有中断。
- 有时会进行一些基本的内存控制器或缓存的初始化。
二、 关键文件与子目录解析
1. Makefile
这个文件是理解整个目录工作方式的入口。它定义了如何从 vmlinux
构建出 zImage
。关键步骤包括:
- 将
vmlinux
压缩(通常使用 Gzip 或 LZMA)。 - 将压缩后的内核数据与
arch/arm/boot/compressed/
目录下的自解压代码链接在一起,形成最终的zImage
文件。 uImage
的生成则是在zImage
的基础上添加了一个 U-Boot 特定的头部信息,其中包含了镜像类型、加载地址、入口点等元数据。
2. compressed/
这是 arch/arm/boot
中最重要的子目录,包含了自解压前端程序的所有源码。可以把它看作是一个“迷你内核”。
head.S
: 这是内核镜像的真正入口点。当 Bootloader 跳转到zImage
时,CPU 执行的第一条指令就在这个文件里。它的主要工作(全部用汇编语言完成)包括:- 环境检查: 检查 CPU ID,确认处理器模式和特性。
- 参数保存: 从寄存器
r0
,r1
,r2
中获取 Bootloader 传递过来的参数(如 ATAGs 或 DTB 的地址),并将它们安全地保存起来。 - 建立临时栈: 为即将运行的 C 代码设置一个临时的栈空间。
- 调用 C 函数: 跳转到
misc.c
中的decompress_kernel()
函数。
misc.c
: 这是自解压前端的 C 语言部分。decompress_kernel()
: 这个函数是解压缩逻辑的核心。它首先会确定内核应该被解压到内存的哪个地址,然后调用相应的解压算法(如gunzip()
)将压缩的内核镜像解压到目标地址。- 早期打印: 它可能会初始化一个最简单的串口(early console),以便在解压过程中或出现错误时打印调试信息。
- 调用主内核: 解压完成后,它会计算出主内核的入口地址,并将之前保存的 Bootloader 参数(如 DTB 地址)设置好,最后跳转到主内核的入口点(位于
arch/arm/kernel/head.S
)。
vmlinux.lds.S
: 这是一个链接器脚本,它指导链接器如何将head.S
、misc.c
和压缩后的内核数据组合成一个完整的、可执行的zImage
镜像。
3. dts/
(Device Tree Source)
这个目录包含了所有被官方内核支持的 32 位 ARM 平台(SoC 和开发板)的设备树源文件 (.dts
和 .dtsi
)。
.dtsi
(Include): 定义了某个 SoC 家族共有的硬件信息,如 CPU、内存控制器、中断控制器、GPIO、串口等。.dts
(Source): 描述了一个具体开发板的硬件布局。它会#include
对应的.dtsi
文件,并添加或修改板级特有的硬件信息,如内存大小、外设连接、引脚配置等。
在内核编译过程中,这些 .dts
文件会被设备树编译器(dtc
)编译成二进制的 .dtb
文件。Bootloader 负责将与当前硬件匹配的 .dtb
文件加载到内存中,供内核使用。
三、 启动流程串讲
结合上述文件,一个典型的 ARM Linux 启动流程如下:
Bootloader 阶段: U-Boot 被启动。它初始化内存、串口等基本硬件。然后从存储介质(如 eMMC, SD 卡)中将
zImage
(或uImage
)和board.dtb
文件加载到 RAM 的指定地址。跳转到内核: U-Boot 设置好启动参数(通常
r0=0
,r1=machine_type
,r2=DTB_address
),然后执行bootm
或booti
命令,跳转到zImage
在内存中的地址。执行
arch/arm/boot/compressed/head.S
:- CPU 开始执行
head.S
中的汇编代码。 - 代码保存
r2
中的 DTB 地址。 - 建立一个简单的执行环境。
- CPU 开始执行
执行
arch/arm/boot/compressed/misc.c
:head.S
跳转到decompress_kernel()
。- 该函数在屏幕(如果配置了 early console)上打印出 “Uncompressing Linux… done, booting the kernel.”。
- 它将压缩的内核主体解压到最终的运行地址。
交接给主内核:
- 解压程序将之前保存的 DTB 地址再次放入
r2
寄存器。 - 跳转到解压后内核的入口点(位于
arch/arm/kernel/head.S
)。
- 解压程序将之前保存的 DTB 地址再次放入
主内核启动: 从这一刻起,
arch/arm/boot
的历史使命已经完成。控制权完全移交给了主内核,后者将开始进行更复杂的硬件初始化、内存管理、驱动加载等一系列操作,最终启动init
进程。
四、 总结
arch/arm/boot
目录是 ARM Linux 内核能够成功启动的“第一推动力”。它以一个高度优化的、自包含的迷你程序的形式存在,完美地衔接了硬件、Bootloader 和复杂的通用内核。它通过解压缩和参数传递这两大核心功能,为内核主体创造了一个干净、可预期的初始运行环境,是理解嵌入式 Linux 底层启动过程不可或缺的关键部分。
解压过程
- 依据arch/arm/kernel/vmlinux.lds 生成linux内核源码根目录下的vmlinux,这个vmlinux属于未压缩,带调试信息、符号表的最初的内核,大小约23MB;
- 将上面的vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/Image,这是不带多余信息的linux内核,Image的大小约3.2MB
- 将 arch/arm/boot/Image 用gzip -9 压缩生成arch/arm/boot/compressed/piggy.gz大小约1.5MB;
- 编译arch/arm/boot/compressed/piggy.S 生成arch/arm/boot/compressed/piggy.o大小约1.5MB,这里实际上是将piggy.gz通过piggy.S编译进piggy.o文件中。而piggy.S文件仅有6行,只是包含了文件piggy.gz;
- 依据arch/arm/boot/compressed/vmlinux.lds 将arch/arm/boot/compressed/目录下的文件head.o 、piggy.o 、misc.o链接生成 arch/arm/boot/compressed/vmlinux,这个vmlinux是经过压缩且含有自解压代码的内核,大小约1.5MB;
- 将arch/arm/boot/compressed/vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/zImage大小约1.5MB;这已经是一个可以使用的linux内核映像文件了;
- 将arch/arm/boot/zImage添加64Bytes的相关信息打包为arch/arm/boot/uImage大小约1.5MB;
Makefile
TEXT_OFFSET 内核映像的字节偏移量 0x00008000
1 | # 文本偏移量。此列表按地址进行数字排序,以便 |
piggy_data
- 生成vmlinux时一同生成piggy_data
- piggy_data包含了内核的压缩数据,压缩算法由
CONFIG_KERNEL_GZIP
等决定 - piggy_data.o,使用piggy_data的内容生成.o文件
include/asm/unified.h
AR_CLASS M_CLASS 选择使用的架构指令
1 | #ifdef CONFIG_CPU_V7M |
compressed/efi-header.S
__nop 空指令
1 | .macro __nop |
__initial_nops 执行2个__nop
1 | .macro __initial_nops |
compressed/fdt_check_mem_start.c 从FDT中检查内存是否可用,返回可用内存起始地址
- 检查传入的
R1
,即mov r8, r2 @ save atags pointer
传入的FDT指针是否为空 - 32位地址空间,检查
#address-cells
和#size-cells
是否大于2 - 检查
/chosen
节点下是否存在linux,usable-memory-range
属性,是否有可用内存限制 - 检查
device_type
为memory
的节点,是否存在linux,usable-memory
或reg
属性 - 获取内存的起始地址和大小,检查是否在可用内存范围内
- 如果在可用内存范围内,则返回传入的
mem_start
;否则更新可用内存的起始地址使用最小的可用内存 - 如果没有找到可用内存,则使用传入的
mem_start
作为返回值 - 返回值:要使用的物理内存起始地址
1 | uint32_t fdt_check_mem_start(uint32_t mem_start, const void *fdt) |
compressed/piggy.S 设置piggy_data的内容与piggy_data_end的地址
1 | //只读属性 |
compressed/misc.c 杂项
decompress_kernel 调用解压内核函数,并打印日志
1 | void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p, |
arch/arm/boot/compressed/decompress.c 不同解压的中间层
- makefile和konconfig中进行了选择配置需要的解压算法
1 |
|
compressed/decompress.c
- 根据所选的解压算法,包含不同的解压算法.同时只能选择一个解压算法
- 注意定义了
STATIC
和STATIC_RW_DATA
,并且decompress_inflate.c
是**include
**的.分析时需要注意包括这两个宏
1 |
|
compressed/head.S
- 使用__nop指令填充32字节,跳过a.out头部.
- 其中
__initial_nops
跳过2 - 循环5个
M_CLASS( nop.w )
跳过一个
- 其中
- 跳转到1,重定向栈顶地址,开启缓存页表
- cache_on开启缓存,call_cache_fn获取处理器ID.(V7M直接返回)
restart
函数重定向并获取解压后的内核大小,判断页表是否会覆盖内核映像,如果会则拷贝内核到新的内存区域,重定向zimage的地址,并跳转到重定向后的restart的地址进行执行.不会则跳转到wont_overwrite
函数wont_overwrite
修正LC0存储的内容,BSS区域的起始和结束地址,GOT表的起始和结束地址,顺序执行到not_relocated
标签
start 入口函数
1 | .section ".start", "ax" |
LC0 保存着内核的基地址和数据段结束地址
- got:全局偏移表,存储动态链接的函数的地址
1 | .align 2 |
LC1 存放着内核的栈顶地址和数据段结束地址的位置无关偏移量
- 在
LC1
中存放着内核的栈顶地址和数据段结束地址 - 通过存储栈顶地址-
LC1
的地址来获取位置无关的地址
1 | .type LC1, #object |
Lheadroom 内核代码大小+16kb(页表)+DTB预留大小(1mb)
1 | .Lheadroom: |
1 使用位置无关执行重定向栈顶地址,并开启缓存页表
- 对齐128MB
- 修正sp栈顶指针
- 通过
fdt_check_mem_start
检查内存是否可用,并返回可用内存起始地址 - 确定最终的内核映像地址
- 判断页表是否会覆盖内核映像.(在内核映像地址范围下的16kb页表与1mb的DTB)
- 设置之后的页表超过了内核映像,则跳过cache_on不进行设置.r4的LSB位表示
- 如果r4的LSB位为1,则跳过cache_on
- 如果r4的LSB位为0,则调用cache_on开启缓存
- 否则调用
cache_on
开启缓存
1 | .text |
cache_on 开启缓存
- 32字节对齐,传入8,调用
call_cache_fn
,然后返回 - 这段代码的目的是开启缓存(cache)。为了开启指令缓存(I cache)和数据缓存(D cache),需要设置一些页表。页表被放置在内核执行地址向下16KB的位置,希望这个位置没有被其他东西使用。如果被使用了,可能会导致程序崩溃。
- 在进入这个例程时:
- r4 寄存器包含内核执行地址
- r7 寄存器包含架构编号
- r8 寄存器包含 ATAGs 指针
- 在退出这个例程时,以下寄存器的值可能会被破坏:
- r0, r1, r2, r3, r9, r10, r12
- 这个例程必须保留 r4, r7, r8 的值。
1 | .align 5 |
call_cache_fn 获取处理器ID
- 将
r12
设置为proc_types
的地址 - 获取处理器ID
- 根据处理器ID跳转到不同的函数
CONFIG_CPU_V7M
则在其他地方实现,这里直接返回
1 | /* |
reloc_code_end 记录重定向代码的结束地址
1 | restart: //331 |
dbgkc 打印调试内核信息
1 | .macro kputc,val |
cache_clean_flush 清除缓存并刷新
1 | /* |
restart 重定向并获取解压后的内核大小
- 重定向栈顶地址
- 重定向数据段结束地址
- 调用
get_inflated_image_size
获取解压后的内核大小 - 分配64k的内存空间用于malloc
1
2
3#arch/arm/boot/compressed/Makefile
MALLOC_SIZE := 65536
AFLAGS_head.o += -DTEXT_OFFSET=$(TEXT_OFFSET) -DMALLOC_SIZE=$(MALLOC_SIZE) - 检查页表是否会覆盖内核映像
- 不会则跳转到
wont_overwrite
函数 - 会则修正内核映像地址,拷贝内核到新的内存区域,重定向zimage的地址,并跳转到重定向后的restart的地址进行执行
1 | restart: adr r0, LC1 //获取LC1的地址 |
Linflated_image_size_offset 解压后的内核大小偏移量
- 减去 4 的目的是为了获取解压后内核镜像大小的地址,因为这个大小通常存储在 input_data_end 之前的4个字节中。减去当前地址
.
是为了得到一个相对偏移量,这样在运行时可以正确地访问解压后内核镜像大小的位置。
1 | .long (input_data_end - 4) - . |
get_inflated_image_size 获取解压后的内核大小
1 | /* |
- 验证如下:
Image未压缩的,去除调试信息的内核大小为2992000(0x2DA780)字节
1 | -rwxrwxr-x 1 embedsky embedsky 2992000 Mar 2 14:49 Image* |
piggy_data的最后四字节转换成小端也为2992000(0x2DA780)字节
1 | hexdump -C arch/arm/boot/compressed/piggy_data | tail -n 2 |
wont_overwrite 修正LC0存储的内容,BSS区域的起始和结束地址,GOT表的起始和结束地址
1 | wont_overwrite: |
not_relocated 重定向已完成执行
- 循环清除 BSS 区域
- 判断是否跳过缓存设置,如果跳过则直接调用cache_on开启缓存
- 传入参数进行解压内核
- r0 = 内核执行地址
- r1 = 堆栈上方的 malloc 空间
- r2 = 表示动态内存分配空间的最大地址
- r3 = 架构 ID
1 | not_relocated: mov r0, #0 |
__enter_kernel 进入内核
V7M
不执行ARM
,执行THUMB
和M_CLASS
r4
为内核的入口地址
1 | mov r0, #0 @ must be 0 |