基于原生entry_a64.S的深入分析
这是 AArch64 架构下 OP-TEE 核心态的冷启动汇编入口文件,路径为 core/arch/arm/kernel/entry_a64.S,符号 _start 与 kern.ld.S 中 ENTRY(_start) 严格对应——TF-A 的 BL31 跳转到 BL32 后,执行的第一条指令就从这里开始。
整个启动流程严格遵循「物理地址早期初始化 → 镜像重排与内存准备 → 开启MMU进入虚拟地址 → C语言分层初始化 → 返回向量表完成注册」的顺序,所有符号、段与 kern.ld.S 链接脚本一一对应。
一、启动总览与核心阶段划分
_start 函数全程在 S-EL1 安全异常等级执行,分为 10 个核心阶段,从纯物理地址的裸机状态,逐步初始化到可接收正常世界调用的稳态:
TF-A 跳转进入 _start ↓ 1. 寄存器与异常向量早期配置(物理地址,关MMU) ↓ 2. 分页模式专属:镜像分段重定位(init段拷贝) ↓ 3. BSS段清零、重定位、安全特性早期初始化 ↓ 4. 栈初始化、异常使能 ↓ 5. Cache维护、串口初始化 ↓ 6. 内存管理器初始化、MMU页表构建 ↓ 7. 开启MMU,切换到虚拟地址运行 ↓ 8. C语言三层初始化(early/late/runtime) ↓ 9. 安全特性收尾、栈保护更新 ↓ 10. SMC返回向量表给BL31,进入空闲等待
二、逐阶段源码深度解析
阶段 1:入口与早期寄存器配置(物理地址运行)
对应代码:_start 函数开头到 set_sctlr_el1
mov x19, x0 mov x20, x1 mov x21, x2 mov x22, x3
TF-A 跳转时通过 x0-x3 传入启动参数(可分页段地址、设备树指针、安全内存大小等),先存入 x19-x22 被调用者保存寄存器,避免后续函数调用被覆盖,后续传给 boot_save_args 统一保存。
adr x0, reset_vect_table msr vbar_el1, x0
将 reset_vect_table 写入 VBAR_EL1,先挂一套最简异常向量(默认死循环),防止启动过程中出现异常直接跑飞。初始化完成后会替换为完整异常处理向量。
- init_pan:可选,开启特权访问永不(Privileged Access Never),防止内核态意外访问用户态内存,阻断部分漏洞利用路径。
- set_sctlr_el1:配置系统控制寄存器 SCTLR_EL1,是安全加固的核心硬件配置:
- 开启 SCTLR_I:指令缓存 ICache
- 开启 SCTLR_SA:栈对齐检查
- 开启 SCTLR_SPAN:PAN 生效位
- CFG_CORE_RWDATA_NOEXEC 开启时置位 SCTLR_WXN:所有可写内存不可执行,硬件级 NX 防护,防止代码注入
- 可选开启 MTE 内存标签、BTI 分支目标标识、PAUTH 指针认证等 ARMv8.5+ 安全特性
阶段 2:分页模式专属:镜像分段重定位
对应代码:#ifdef CFG_WITH_PAGER 分支,也就是你使用 tee-pager_v2.bin 的核心逻辑
分页镜像的文件布局
tee-pager_v2.bin 在 Flash/内存中的原始排布是:
[Pager常驻代码/数据] → 已在正确链接地址 [Init初始化段] → 需要拷贝到 __init_start 位置 [boot_embdata 元数据] → 哈希、重定位表等附加数据
刚加载到内存时,init 段紧接在常驻段后面,不在链接脚本指定的 __init_start 地址,需要手动搬运。
拷贝逻辑
adr x0, __init_start // 目标地址 adr x1, __data_end // 源地址(常驻段结束,init段起始) adr x2, __init_end sub x2, x2, x0 // init段长度 ldr w4, [x1, x2] // boot_embdata 总长度 add x2, x2, x4 // 总拷贝长度 = init段 + 元数据
采用倒序拷贝(从后往前),类似 memmove,避免源地址和目标地址重叠时数据被覆盖。最后将内存末尾保存到 boot_cached_mem_end,供后续刷 Cache 使用。
非分页模式下逻辑简化:仅将 boot_embdata 元数据搬到空闲内存末尾,不做整段重排。
阶段 3:内存初始化与安全前置
adr_l x0, __bss_start adr_l x1, __bss_end clear_bss: str xzr, [x0], #8 cmp x0, x1 b.lt clear_bss
按 8 字节步长清零所有未初始化全局变量,防止残留随机敏感数据,同时保证变量初始值为 0 的语义正确。虚拟化场景下还会额外清零 .nex_bss 分区段。
开启 CFG_CORE_PHYS_RELOCATABLE 时,计算实际加载地址与编译链接地址的偏移,执行 relocate 函数做重定位,支持镜像加载到任意物理地址。
开启地址消毒时,初始化影子内存区域,标记栈空间为合法访问,启动期就开启内存越界检测。
阶段 4:栈初始化与异常使能
OP-TEE 采用 SP_EL0 + SP_EL1 双栈设计:
- 先获取 CPU 核号,超过配置的最大核数则进入 unhandled_cpu 死循环挂起,防止异常核破坏系统
- SP_EL0:临时运行栈,每个核按 stack_tmp_stride 偏移,预留栈守护区
- SP_EL1:指向 thread_core_local[cpu_id],每个核的私有核心数据结构,存放线程上下文、栈信息、异常信息
填充 thread_core_local 的临时栈、中止栈、当前线程ID、标志位,为异常处理准备环境。
msr daifclr, #DAIFBIT_ABT
之前为了防止启动中异常崩溃一直关闭中止异常,现在栈和异常向量都就绪了,使能数据/指令中止异常,支持缺页、访问权限错误等异常处理。
/* Enable Console */ bl console_init
阶段 5:Cache 维护与串口初始化
adr_l x0, __text_start adr_l x1, boot_cached_mem_end ldr x1, [x1] sub x1, x1, x0 bl dcache_cleaninv_range
对启动阶段 touched 的所有内存做 Cache 清洁无效化,保证后续开启 Cache 时,物理内存与 Cache 数据一致,避免和 TF-A 的 Cache 操作产生一致性问题。
调用 console_init 初始化 PL011 串口,至此可以输出启动日志,进入可调试状态。
/* Enable Console */ bl console_init
阶段 6:内存与 MMU 页表初始化
调用 boot_save_args,把最开始保存的 x19-x22 入参持久化到全局变量,供后续初始化流程使用。
mov x0, x19 mov x1, x20 mov x2, x21 mov x3, x22 mov x4, xzr bl boot_save_args
2.内存管理器初始化
#ifdef CFG_WITH_PAGER adr_l x0, __init_end /* pointer to boot_embdata */ ldr w1, [x0] /* struct boot_embdata::total_len */ add x0, x0, x1 add x0, x0, #0xfff /* round up */ bic x0, x0, #0xfff /* to next page */ mov_imm x1, (TEE_RAM_PH_SIZE + TEE_RAM_START) mov x2, x1 #else adr_l x0, __vcore_free_start adr_l x1, boot_embdata_ptr ldr x1, [x1] #ifdef CFG_DYN_CONFIG sub x1, x1, #THREAD_BOOT_INIT_TMP_ALLOC #endif adr_l x2, __vcore_free_end; #endif bl boot_mem_init
调用 boot_mem_init,传入可用内存起止地址:
- 分页模式:从 __init_end 之后到安全内存末尾
- 非分页模式:空闲内存区域初始化内核堆、TA 内存池、分页管理结构。
adr x1, boot_mmu_config bl core_init_mmu_map
调用 C 语言函数构建完整页表,基于链接脚本导出的 __vcore_* 段边界符号,按段设置内存权限(代码段 RX、数据段 RW、空闲段不可访问)。开启 ASLR 时会在此处生成随机地址偏移。
阶段 7:开启 MMU,切换虚拟地址
对应函数:enable_mmu,位于 .identity_map 恒等映射段
这是启动最核心的临界区,该段代码的物理地址 = 虚拟地址,保证开启 MMU 的瞬间不会因为地址跳变而跑飞,和 kern.ld.S 中的 .identity_map 段严格对应。
执行步骤:
- 加载 boot_mmu_config 配置,写入 TCR_EL1(内存转换控制)、MAIR_EL1(内存属性)、TTBR0_EL1(页表基地址)
- 全量无效化 TLB,保证旧映射不干扰
- 置位 SCTLR_EL1.M,正式开启 MMU
- 更新 VBAR_EL1 异常向量表到虚拟地址
- 无效化 ICache、分支预测器,开启 I-Cache 和 D-Cache
- 调整 SP_EL0/SP_EL1 栈指针、返回地址 LR,加上虚拟地址偏移,保证返回后地址正确
这一步完成后,CPU 从物理地址模式切换到虚拟地址模式,后续所有代码都运行在虚拟地址空间。
阶段 8:MMU 开启后收尾
- 更新 thread_core_local 中的栈地址为虚拟地址
- 执行 boot_mem_relocate,更新内存分配器的虚拟地址偏移
- 重新初始化串口:之前用物理地址注册,现在切换为虚拟地址,保证日志正常输出
- MTE 内存标签清零、虚拟化分区表初始化
阶段 9:四层主初始化
函数 | 执行阶段 | 核心职责 |
boot_init_primary_early | 早期硬件初始化 | GIC 中断控制器、安全定时器、平台外设早期初始化 |
boot_init_primary_late | 内核核心初始化 | 线程调度系统、密码学框架、SMC 调用接口、TA 加载器 |
boot_init_primary_runtime | 服务与应用初始化 | 内置 TA 验签加载、系统服务注册、会话管理初始化 |
boot_init_primary_final | 启动收尾 | 释放 .text_init / .rodata_init 初始化段内存到堆,唤醒所有从核,切换到空闲线程 |
1.boot_init_primary_early():硬件底层初始化
对应「硬件」阶段,完成最基础的硬件外设配置:
- GIC 中断控制器初始化(配置分组、优先级、注册中断处理)
- 安全物理定时器初始化
- 平台级外设早期初始化(串口、时钟、电源域)
- 核间通信机制初始化
2.boot_init_primary_late():内核核心初始化
对应「内核」阶段,初始化 OP-TEE 内核核心机制:
- 线程调度系统完整初始化(线程控制块、调度器、空闲线程)
- 内存管理单元收尾配置
- 分页机制完整初始化(分页模式下)
- 异常向量表替换为完整处理向量
- 栈保护、内存标签等安全特性收尾
3.boot_init_primary_runtime():系统服务初始化
对应「服务」阶段,初始化核心安全服务:
- 密码学服务框架初始化(软件算法库、硬件加密引擎)
- SMC 调用接口初始化(SMCCC 标准协议、功能分发)
- TA 加载器 ldelf 初始化(ELF 解析、验签、沙箱环境)
- TEE 内部核心 API 初始化(会话管理、对象管理、时间服务)
4.boot_init_primary_final():应用加载与启动收尾
对应「应用」阶段,加载可信应用并进入稳态:
- 内置 TA 批量加载:逐个验签、重定位、创建沙箱实例
- 初始化段内存回收:释放 .text_init / .rodata_init 占用的内存,叠加到内核堆
- 唤醒所有从核,进入多核心调度状态
- 切换到空闲线程,等待正常世界 SMC 调用
这就是「硬件→内核→服务→应用」自底向上初始化顺序的真实体现,初始化段内存释放是分页模式的核心优化——仅运行一次的代码启动后直接回收,最小化常驻内存体积。
阶段 10:安全收尾与 TF-A 交互闭环
调用 plat_get_random_stack_canaries 生成随机数,写入 __stack_chk_guard,开启栈溢出检测,符合 Android 安全编译规范。
#ifdef _CFG_CORE_STACK_PROTECTOR /* Update stack canary value */ sub sp, sp, #0x10 mov x0, sp mov x1, #1 mov x2, #0x8 bl plat_get_random_stack_canaries ldr x0, [sp] adr_l x5, __stack_chk_guard str x0, [x5] add sp, sp, #0x10 #endif
2.再次刷 Cache
对所有启动期内存再次做 Cache 清洁无效化,保证从核启动时(Cache 关闭状态)读到正确的物理内存数据,避免多核 Cache 不一致。
/* * In case we've touched memory that secondary CPUs will use before * they have turned on their D-cache, clean and invalidate the * D-cache before exiting to normal world. */ adr_l x0, __text_start adr_l x1, boot_cached_mem_end ldr x1, [x1] sub x1, x1, x0 bl dcache_cleaninv_range
调用 thread_clr_boot_thread,释放启动专用线程,供后续调度复用。
/* * Clear current thread id now to allow the thread to be reused on * next entry. Matches the thread_init_boot_thread in * boot.c. */ #ifndef CFG_NS_VIRTUALIZATION bl thread_clr_boot_thread #endif
/* * Pass the vector address returned from main_init Compensate for * the virtual map offset since cpu_on_handler() is called with MMU * off. */ ldr x0, boot_mmu_config + CORE_MMU_CONFIG_MAP_OFFSET adr x1, thread_vector_table sub x1, x1, x0 mov x0, #TEESMC_OPTEED_RETURN_ENTRY_DONE smc #0 /* SMC should not return */ panic_at_smc_return
通过 SMC 调用向 BL31 返回 OP-TEE 的线程向量表地址,BL31 收到后完成安全世界注册。
- 后续正常世界触发 SMC 调用时,BL31 会根据向量表路由到 OP-TEE 对应处理函数
- 该 SMC 不会返回,OP-TEE 直接进入空闲线程,等待调用
三、补充:从核启动入口cpu_on_handler
主核初始化完成、唤醒从核后,从核从 cpu_on_handler 入口启动,流程是主核的简化版:
- 只做本核的异常向量、SCTLR、栈、MMU 初始化
- 不重复执行全局硬件、内存、服务初始化
- 最终调用 boot_cpu_on_handler 完成从核注册,进入空闲线程参与调度
四、与链接脚本kern.ld.S的对应关系
所有地址符号都直接来自链接脚本,是配置驱动内存布局的完整闭环:
源码符号 | 链接脚本定义 | 作用 |
__text_start / __text_end | .text 段边界 | 代码段起止,Cache 维护、权限配置用 |
__data_end | .data 段结束 | 常驻段末尾,分页模式 init 段的源地址 |
__init_start / __init_end | .text_init 段边界 | 初始化段目标地址 |
__bss_start / __bss_end | .bss 段边界 | 清零范围 |
__vcore_free_start / __vcore_free_end | 空闲内存边界 | 内存管理器可用范围 |
.identity_map 段 | 恒等映射段 | 开启 MMU 的临界区代码 |
五、Android 安全视角:启动期的安全设计
- 安全左移:WXN 不可执行、栈对齐检查、PAN、MTE 等硬件安全特性,在启动最早期就全部开启,整个初始化过程都在安全防护下执行
- 最小化攻击面:初始化段启动后立即释放,常驻内存仅保留核心调度、异常处理、分页逻辑,大幅减少攻击面
- 内存完整性:BSS 强制清零、Cache 全量维护、重定位校验,避免内存残留与一致性问题引入的漏洞
- 信任链连续:从 BL31 跳转过来后,第一时间完成安全配置,整个启动过程在可信环境内执行,信任链无断点