ARM Trusted Firmware 启动流程:从汇编到 C 语言的渐进式初始化
引言
在 ARM Trusted Firmware(ATF)的启动过程中,一个看似矛盾的现象引起了我们的注意:启动代码在尚未完全初始化 C 运行时环境的情况下,就已经开始调用 C 语言函数。本文将深入分析这一设计背后的原理,特别关注 _init_c_runtime 这一关键步骤,揭示 ATF 如何通过渐进式初始化策略,安全地从汇编环境过渡到完整的 C 应用程序环境。
一、启动序列概览
ATF 的启动流程遵循严格的分阶段初始化策略,其核心思想是从简单到复杂,从物理到虚拟,从受限到完整。典型的 BL1 启动序列如下:
assembly
/* bl1_entrypoint.S */
func bl1_entrypoint
/* 阶段 1:基本汇编环境初始化 */
el3_entrypoint_common \
_init_sctlr=1 \
_warm_boot_mailbox=!PROGRAMMABLE_RESET_ADDRESS \
_secondary_cold_boot=!COLD_BOOT_SINGLE_CPU \
_init_memory=1 \
_init_c_runtime=1 \ /* 初始化最小 C 环境 - 关键步骤! */
_exception_vectors=bl1_exceptions
/* 阶段 2:平台早期设置(第一次 C 调用)*/
bl bl1_early_platform_setup /* C 语言实现,但受限 */
/* 阶段 3:架构设置(第二次 C 调用)*/
bl bl1_plat_arch_setup /* C 语言实现,启用 MMU */
/* 阶段 4:主 C 入口点 */
b bl1_main /* 完整的 C 应用程序 */
endfunc
二、_init_c_runtime:最小 C 环境的建立
_init_c_runtime=1 是启动流程中的关键参数,它触发了一系列初始化操作,为后续的 C 函数调用奠定基础。让我们深入分析这个宏展开后的具体操作:
2.1 el3_entrypoint_common 宏的展开
assembly
/* el3_common_macros.S */
.macro el3_entrypoint_common _init_sctlr, _warm_boot_mailbox, \
_secondary_cold_boot, _init_memory, _init_c_runtime, \
_exception_vectors
/* ... 其他初始化 ... */
.if \_init_c_runtime
/* 关键:初始化 C 运行时环境 */
bl init_c_runtime
/* 设置栈指针 */
bl plat_set_my_stack
.endif
/* ... 后续代码 ... */
.endm
2.2 init_c_runtime 函数的详细实现
assembly
/* aarch64/early_exceptions.S */
func init_c_runtime
/* 1. 清零 BSS 段(未初始化的全局变量)*/
adrp x0, __BSS_START__
adrp x1, __BSS_END__
sub x1, x1, x0
cbz x1, 1f /* 如果 BSS 大小为 0,跳过 */
/* 使用 DC ZVA 指令高效清零(如果支持)*/
mrs x2, dczid_el0
and x2, x2, #0xF
cbz x2, 2f /* 如果块大小不为 0,使用 DC ZVA */
/* 传统方式:逐字节清零 */
mov x2, xzr
mov x3, xzr
0: stp x2, x3, [x0], #16
subs x1, x1, #16
b.hi 0b
b 1f
/* DC ZVA 方式:按缓存行清零 */
2: mov x3, #4
lsl x2, x3, x2 /* 计算块大小 */
sub x4, x2, #1
bic x0, x0, x4 /* 对齐到块边界 */
3: dc zva, x0
add x0, x0, x2
subs x1, x1, x2
b.hi 3b
1: /* 2. 复制数据段(从 ROM 到 RAM)*/
adrp x0, __DATA_START__
adrp x1, __DATA_END__
adrp x2, __DATA_RAM_START__
sub x1, x1, x0 /* 计算数据段大小 */
cbz x1, 4f /* 如果数据段为空,跳过 */
/* 逐 16 字节复制 */
5: ldp x3, x4, [x0], #16
stp x3, x4, [x2], #16
subs x1, x1, #16
b.hi 5b
4: /* 3. 初始化一致性内存(如果需要)*/
bl zeromem_coherent /* 清零共享内存区域 */
/* 4. 使数据缓存失效 */
bl inv_dcache_range /* 防止脏缓存数据 */
/* 5. 设置栈保护金丝雀值 */
#if STACK_PROTECTOR_ENABLED
bl set_stack_protector_canary
#endif
ret
endfunc init_c_runtime
2.3 栈的初始化:plat_set_my_stack
assembly
/* 平台特定的栈设置 */
func plat_set_my_stack
/* 获取栈基址和大小 */
adrp x0, platform_normal_stacks
mov x1, #PLATFORM_STACK_SIZE
/* 根据 CPU ID 计算栈指针 */
mrs x2, mpidr_el1
and x2, x2, #MPIDR_CPU_MASK
mul x3, x2, x1 /* 栈偏移 = CPU_ID × 栈大小 */
add x0, x0, x3
add sp, x0, x1 /* SP = 栈基址 + 栈大小(满递减栈)*/
/* 设置当前异常级别的栈指针 */
msr sp_el0, sp /* 如果使用 SP_EL0 */
// 或 msr sp_el3, sp /* 如果使用 SP_EL3 */
ret
endfunc plat_set_my_stack
2.4 初始化完成后的内存布局
内存布局(_init_c_runtime 完成后):
┌─────────────────────┐
│ ROM/Flash │
├─────────────────────┤
│ 代码 (.text) │ ← 只读,未复制
│ 只读数据 (.rodata) │ ← 只读,未复制
│ 初始化数据 (.data) │ ← 已复制到 RAM
└─────────────────────┘
↓ 复制
┌─────────────────────┐
│ RAM │
├─────────────────────┤
│ 栈 (Stack) │ ← plat_set_my_stack 设置
│ 堆 (Heap) │ ← 尚未初始化
│ 数据段 (.data) │ ← 从 ROM 复制而来
│ BSS 段 (.bss) │ ← 已清零
│ 一致性内存 │ ← 已清零
└─────────────────────┘
三、已初始化与未初始化的对比
3.1 已完成的初始化(_init_c_runtime=1 后)
| 组件 | 状态 | 说明 |
|---|---|---|
| 栈 | ✅ 就绪 | plat_set_my_stack 设置栈指针 |
| BSS 段 | ✅ 清零 | 未初始化的全局变量归零 |
| 数据段 | ✅ 复制 | 已初始化的全局变量从 ROM 复制到 RAM |
| 一致性内存 | ✅ 清零 | 共享内存区域初始化 |
| 数据缓存 | ✅ 失效 | 防止脏缓存数据污染 |
| 栈保护 | ✅ 设置 | 金丝雀值检测栈溢出 |
3.2 尚未完成的初始化
| 组件 | 状态 | 说明 |
|---|---|---|
| MMU | ❌ 未启用 | 仍使用物理地址访问 |
| 完整异常处理 | ❌ 未就绪 | 只有基本的异常向量表 |
| 标准库 | ❌ 未初始化 | 无 malloc、printf 等支持 |
| 浮点单元 | ❌ 未启用 | 只能使用通用寄存器 |
| 动态内存分配 | ❌ 不可用 | 堆管理器未初始化 |
| 线程局部存储 | ❌ 未设置 | TLS 区域未初始化 |
四、早期 C 函数的约束与实现
4.1 第一个 C 函数:bl1_early_platform_setup
这个函数在 _init_c_runtime 完成后立即调用,必须遵守严格的编程约束:
c
/* bl1_early_platform_setup.c - 高度受限的 C 环境 */
void __attribute__((section(".text.cold"), noinline))
bl1_early_platform_setup(void)
{
/* 允许的操作(依赖 _init_c_runtime 的成果): */
/* 1. 使用栈上的局部变量 */
uint32_t config = read_mmio(REG_BASE);
/* 2. 访问已初始化的全局变量(在 .data 段)*/
extern const uint32_t platform_config[];
uint32_t value = platform_config[0]; /* ✅ 安全:已在 _init_c_runtime 复制 */
/* 3. 调用简单的辅助函数 */
early_console_init(); /* ✅ 假设该函数也遵守相同约束 */
/* 4. 使用静态常量 */
static const uint32_t magic = 0xDEADBEEF;
/* 禁止的操作: */
/* 1. 访问未初始化的全局变量(虽然 BSS 已清零,但仍需谨慎)*/
// extern uint32_t counter; /* ❌ 语义上未初始化,即使值为 0 */
/* 2. 动态内存分配 */
// void *ptr = malloc(100); /* ❌ 堆管理器未初始化 */
/* 3. 依赖标准库 */
// printf("Debug: %d\n", x); /* ❌ stdio 未初始化 */
/* 4. 使用大栈帧 */
// char buf[4096]; /* ❌ 栈大小可能不足 */
}
4.2 为什么需要早期 C 函数?
某些硬件初始化必须在启用 MMU之前完成,而这些操作用 C 语言实现更清晰:
- 内存控制器配置:需要直接访问物理地址的 DDR 控制器寄存器
- 系统时钟/PLL 设置:CPU 和总线时钟必须在早期配置
- 串口控制台初始化:用于早期调试输出,需要物理地址访问
- 安全硬件配置:信任区控制器、加解密引擎等
五、编译器与链接器的深度配合
5.1 链接器脚本的关键作用
链接器脚本定义了内存布局,确保 _init_c_runtime 能正确找到各个段:
/* bl1.ld.S - 简化的链接器脚本 */
MEMORY {
ROM (rx) : ORIGIN = 0x00000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x04000000, LENGTH = 64K
}
SECTIONS {
/* 1. 代码段(ROM) */
.text : {
*(.vectors) /* 异常向量表 */
*(.entrypoint) /* 入口点代码 */
*(.text.cold) /* 早期 C 函数:bl1_early_platform_setup */
*(.text) /* 主 C 代码 */
*(.text.*)
} > ROM
/* 2. 只读数据(ROM) */
.rodata : ALIGN(16) {
*(.rodata)
*(.rodata.*)
__RODATA_END__ = .;
} > ROM
/* 3. 数据段(在 ROM 中,但会被复制到 RAM)*/
.data : ALIGN(16) {
__DATA_START__ = .;
*(.data)
*(.data.*)
__DATA_END__ = .;
} > ROM
/* 4. BSS 段(仅存在于 RAM)*/
.bss (NOLOAD) : ALIGN(16) {
__BSS_START__ = .;
*(.bss)
*(COMMON)
__BSS_END__ = .;
} > RAM
/* 5. 栈区域(平台特定)*/
.stack (NOLOAD) : ALIGN(16) {
__STACK_START__ = .;
. += PLATFORM_STACK_SIZE;
__STACK_END__ = .;
} > RAM
/* 6. 数据段在 RAM 中的目标位置 */
.data_ram : ALIGN(16) {
__DATA_RAM_START__ = .;
. += SIZEOF(.data);
__DATA_RAM_END__ = .;
} > RAM AT> ROM
/* 符号供汇编代码使用 */
__DATA_LOAD_START__ = LOADADDR(.data);
__DATA_LOAD_END__ = __DATA_LOAD_START__ + SIZEOF(.data);
}
5.2 编译选项的精细控制
不同阶段的代码使用不同的编译选项:
makefile
# 早期初始化函数的编译标志(_init_c_runtime 后立即调用)
EARLY_CFLAGS := \
-mgeneral-regs-only # 只使用通用寄存器,避免浮点
-nostdlib # 不链接标准库
-ffreestanding # 独立环境,不依赖操作系统
-fno-builtin # 不使用编译器内置函数
-Os # 优化尺寸而非速度
-fno-exceptions # 无 C++ 异常处理
-fno-rtti # 无运行时类型信息
-fno-stack-protector # 早期环境可能无栈保护支持
-fno-common # 禁止未初始化的全局变量
-fshort-wchar # 使用短 wchar_t
-mstrict-align # 强制对齐访问
-fno-asynchronous-unwind-tables # 无异常展开表
# 主 C 代码的编译标志(MMU 启用后)
MAIN_CFLAGS := \
-std=gnu11 # C11 标准
-ffunction-sections # 函数级链接
-fdata-sections # 数据级链接
-fstack-protector-strong # 栈保护
-O2 # 优化速度
-g3 # 调试信息
六、_init_c_runtime 的变体与场景
6.1 冷启动 vs 热启动
assembly
/* 冷启动(完整初始化)*/
func bl1_entrypoint_cold
el3_entrypoint_common \
_init_sctlr=1 \
_init_memory=1 \
_init_c_runtime=1 \ /* 完整初始化 C 环境 */
_exception_vectors=bl1_exceptions
/* ... */
endfunc
/* 热启动/恢复(部分初始化)*/
func bl1_entrypoint_warm
el3_entrypoint_common \
_init_sctlr=0 \ /* 保持现有 SCTLR */
_init_memory=0 \ /* 内存已配置 */
_init_c_runtime=0 \ /* 不复位 C 环境! */
_exception_vectors=bl1_warm_exceptions
/* 直接恢复 C 上下文 */
b warm_boot_resume
endfunc
6.2 从核启动
assembly
/* 从核(Secondary CPU)启动 */
func bl1_secondary_entry
/* 从核需要自己的 C 环境 */
el3_entrypoint_common \
_init_sctlr=1 \
_secondary_cold_boot=1 \
_init_memory=0 \ /* 内存已由主核配置 */
_init_c_runtime=1 \ /* 需要初始化自己的 C 环境 */
_exception_vectors=bl1_secondary_exceptions
/* 设置从核特定的栈 */
bl plat_set_secondary_stack
/* 调用从核 C 初始化 */
bl secondary_early_setup
/* 加入调度 */
b secondary_main
endfunc
七、调试与错误处理
7.1 早期调试技术
由于标准 printf 不可用,需要特殊的调试方法:
c
void bl1_early_platform_setup(void)
{
/* 方法 1:LED 指示灯 */
early_led_on(LED_RED); /* 进入函数 */
/* 方法 2:串口原始输出 */
early_uart_putc('S'); /* 'S' for Start */
early_uart_putc('T');
early_uart_putc('A');
early_uart_putc('R');
early_uart_putc('T');
early_uart_putc('\n');
/* 方法 3:内存日志 */
static uint32_t early_log[16] __attribute__((section(".early_log")));
early_log[0] = 0xDEADBEEF; /* 标记开始 */
early_log[1] = (uintptr_t)__builtin_return_address(0);
/* 方法 4:看门狗定时器 */
early_watchdog_init(1000); /* 1 秒超时 */
/* 实际初始化代码... */
early_led_off(LED_RED);
early_led_on(LED_GREEN); /* 成功 */
}
7.2 错误检测与恢复
assembly
/* 在汇编中调用早期 C 函数并检查错误 */
bl bl1_early_platform_setup
/* 检查返回值(约定:x0 = 返回码,0=成功)*/
cmp x0, #0
b.eq 1f
/* 错误处理 */
early_error_handler:
/* 1. 禁用中断 */
msr daifset, #0xF
/* 2. 输出错误码 */
mov x1, x0 /* 错误码 */
bl early_uart_putx /* 输出十六进制 */
/* 3. 点亮错误 LED */
mov x0, #ERROR_LED_PATTERN
bl early_led_set
/* 4. 停止或重启 */
wfi /* 等待中断(可能不会发生)*/
b . /* 死循环 */
1: /* 继续正常执行 */
八、性能优化考虑
8.1 缓存友好的初始化
assembly
func init_c_runtime_optimized
/* 使用预取优化 BSS 清零 */
adrp x0, __BSS_START__
adrp x1, __BSS_END__
sub x2, x1, x0 /* 大小 */
/* 预取下一缓存行 */
prfm pstl1keep, [x0, #128]
/* 使用 STP 指令一次存储 16 字节 */
mov x3, xzr
mov x4, xzr
mov x5, xzr
mov x6, xzr
bss_clear_loop:
stp x3, x4, [x0], #16
stp x5, x6, [x0], #16
prfm pstl1keep, [x0, #128] /* 预取 */
subs x2, x2, #32
b.hi bss_clear_loop
/* 数据复制类似优化 */
ret
endfunc
8.2 大小优化
对于资源受限的系统,可以精简 _init_c_runtime:
assembly
func init_c_runtime_minimal
/* 只初始化绝对必要的部分 */
/* 1. 设置最小栈 */
ldr x0, =minimal_stack_end
mov sp, x0
/* 2. 清零关键 BSS 变量 */
ldr x0, =critical_bss_start
ldr x1, =critical_bss_end
sub x2, x1, x0
cbz x2, 1f
mov x3, xzr
0: str x3, [x0], #8
subs x2, x2, #8
b.hi 0b
1: ret
endfunc
九、总结
_init_c_runtime 是 ATF 启动流程中的关键枢纽,它架起了汇编世界与 C 语言世界之间的桥梁。通过这一步骤,系统:
- 建立了执行 C 代码的基本环境:栈、BSS、数据段
- 保持了最小化和可控性:只初始化必要的部分,避免过度复杂
- 为渐进式初始化奠定基础:允许后续启用 MMU、异常处理等高级功能
- 支持多种启动场景:冷启动、热启动、从核启动等
这种设计体现了嵌入式系统开发的核心理念:在正确的时间做正确的事情,不多也不少 。_init_c_runtime 初始化了足够的环境来调用早期 C 函数,但又没有过度初始化到需要完整 C 运行时的程度。
理解这一机制对于开发可靠、高效的固件至关重要。它不仅解释了 ATF 的启动原理,也为其他嵌入式系统的设计提供了参考模式。通过这种分层、渐进的初始化策略,系统能够在保证可靠性的同时,提供清晰的代码结构和良好的可维护性。