TEE-OS学习轨迹第十五篇:OP-TEE OS 源码分析部分(二)基于原生 entry_a64.S 的深入分析
2026/6/22 22:08:14 网站建设 项目流程

基于原生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

1.保存启动入参

mov x19, x0 mov x20, x1 mov x21, x2 mov x22, x3
TF-A 跳转时通过 x0-x3 传入启动参数(可分页段地址、设备树指针、安全内存大小等),先存入 x19-x22 被调用者保存寄存器,避免后续函数调用被覆盖,后续传给 boot_save_args 统一保存。

2.挂载临时异常向量表

adr x0, reset_vect_table msr vbar_el1, x0
将 reset_vect_table 写入 VBAR_EL1,先挂一套最简异常向量(默认死循环),防止启动过程中出现异常直接跑飞。初始化完成后会替换为完整异常处理向量。

3.硬件安全特性早期配置

  • 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:内存初始化与安全前置

1.BSS 段清零

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 分区段。

2.物理重定位(可选)

开启 CFG_CORE_PHYS_RELOCATABLE 时,计算实际加载地址与编译链接地址的偏移,执行 relocate 函数做重定位,支持镜像加载到任意物理地址。

3.ASAN 影子内存初始化(可选)

开启地址消毒时,初始化影子内存区域,标记栈空间为合法访问,启动期就开启内存越界检测。

阶段 4:栈初始化与异常使能

1.set_sp宏:双栈设置

OP-TEE 采用 SP_EL0 + SP_EL1 双栈设计:
  • 先获取 CPU 核号,超过配置的最大核数则进入 unhandled_cpu 死循环挂起,防止异常核破坏系统
  • SP_EL0:临时运行栈,每个核按 stack_tmp_stride 偏移,预留栈守护区
  • SP_EL1:指向 thread_core_local[cpu_id],每个核的私有核心数据结构,存放线程上下文、栈信息、异常信息

2.初始化核心私有结构

填充 thread_core_local 的临时栈、中止栈、当前线程ID、标志位,为异常处理准备环境。

3.开异常

msr daifclr, #DAIFBIT_ABT
之前为了防止启动中异常崩溃一直关闭中止异常,现在栈和异常向量都就绪了,使能数据/指令中止异常,支持缺页、访问权限错误等异常处理。
/* Enable Console */ bl console_init

阶段 5:Cache 维护与串口初始化

1.全量刷 D-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 操作产生一致性问题。

2.串口初始化

调用 console_init 初始化 PL011 串口,至此可以输出启动日志,进入可调试状态。
/* Enable Console */ bl console_init

阶段 6:内存与 MMU 页表初始化

1.保存启动参数

调用 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 内存池、分页管理结构。

3.构建 MMU 页表

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 段严格对应。
执行步骤:
  1. 加载 boot_mmu_config 配置,写入 TCR_EL1(内存转换控制)、MAIR_EL1(内存属性)、TTBR0_EL1(页表基地址)
  2. 全量无效化 TLB,保证旧映射不干扰
  3. 置位 SCTLR_EL1.M,正式开启 MMU
  4. 更新 VBAR_EL1 异常向量表到虚拟地址
  5. 无效化 ICache、分支预测器,开启 I-Cache 和 D-Cache
  6. 调整 SP_EL0/SP_EL1 栈指针、返回地址 LR,加上虚拟地址偏移,保证返回后地址正确
这一步完成后,CPU 从物理地址模式切换到虚拟地址模式,后续所有代码都运行在虚拟地址空间。

阶段 8:MMU 开启后收尾

  1. 更新 thread_core_local 中的栈地址为虚拟地址
  2. 执行 boot_mem_relocate,更新内存分配器的虚拟地址偏移
  3. 重新初始化串口:之前用物理地址注册,现在切换为虚拟地址,保证日志正常输出
  4. 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 交互闭环

1.栈保护金丝雀更新

调用 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

3.清除启动线程标记

调用 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

4.向 BL31 返回向量表,完成启动

/* * 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 安全视角:启动期的安全设计

  1. 安全左移:WXN 不可执行、栈对齐检查、PAN、MTE 等硬件安全特性,在启动最早期就全部开启,整个初始化过程都在安全防护下执行
  2. 最小化攻击面:初始化段启动后立即释放,常驻内存仅保留核心调度、异常处理、分页逻辑,大幅减少攻击面
  3. 内存完整性:BSS 强制清零、Cache 全量维护、重定位校验,避免内存残留与一致性问题引入的漏洞
  4. 信任链连续:从 BL31 跳转过来后,第一时间完成安全配置,整个启动过程在可信环境内执行,信任链无断点

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询