ARM Trusted Firmware 启动流程:从汇编到 C 语言的渐进式初始化

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 ❌ 未启用 仍使用物理地址访问
完整异常处理 ❌ 未就绪 只有基本的异常向量表
标准库 ❌ 未初始化 mallocprintf 等支持
浮点单元 ❌ 未启用 只能使用通用寄存器
动态内存分配 ❌ 不可用 堆管理器未初始化
线程局部存储 ❌ 未设置 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 语言实现更清晰:

  1. 内存控制器配置:需要直接访问物理地址的 DDR 控制器寄存器
  2. 系统时钟/PLL 设置:CPU 和总线时钟必须在早期配置
  3. 串口控制台初始化:用于早期调试输出,需要物理地址访问
  4. 安全硬件配置:信任区控制器、加解密引擎等

五、编译器与链接器的深度配合

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 语言世界之间的桥梁。通过这一步骤,系统:

  1. 建立了执行 C 代码的基本环境:栈、BSS、数据段
  2. 保持了最小化和可控性:只初始化必要的部分,避免过度复杂
  3. 为渐进式初始化奠定基础:允许后续启用 MMU、异常处理等高级功能
  4. 支持多种启动场景:冷启动、热启动、从核启动等

这种设计体现了嵌入式系统开发的核心理念:在正确的时间做正确的事情,不多也不少_init_c_runtime 初始化了足够的环境来调用早期 C 函数,但又没有过度初始化到需要完整 C 运行时的程度。

理解这一机制对于开发可靠、高效的固件至关重要。它不仅解释了 ATF 的启动原理,也为其他嵌入式系统的设计提供了参考模式。通过这种分层、渐进的初始化策略,系统能够在保证可靠性的同时,提供清晰的代码结构和良好的可维护性。

相关推荐
疑惑的杰瑞2 小时前
【C】顺序结构
c语言·内存划分
小龙报2 小时前
【初阶数据结构】从 “数组升级” 到工程实现:动态顺序表实现框架的硬核拆解指南
c语言·数据结构·c++·算法·机器学习·信息与通信·visual studio
SELSL2 小时前
Linux文件属性及目录
linux·c语言·linux目录文件·linux文件属性、目录api·linux文件属性
秦苒&3 小时前
【C语言指针五】转移表、回调函数、qsort、qsort函数的模拟实现
c语言·开发语言·c#
旧梦吟3 小时前
脚本网页 C与汇编详解
c语言·css3·html5
晨非辰3 小时前
基于Win32 API控制台的贪吃蛇游戏:从设计到C语言实现详解
c语言·c++·人工智能·后端·python·深度学习·游戏
2301_789015623 小时前
每日精讲:环形链表、两个数组中的交集、随机链表的复制
c语言·数据结构·c++·算法·leetcode·链表·排序算法
2301_789015624 小时前
C++:二叉搜索树
c语言·开发语言·数据结构·c++·算法·排序算法
SystickInt12 小时前
C语言 strcpy和memcpy 异同/区别
c语言·开发语言