TEE-OS学习轨迹第十三篇:OP-TEE OS 编译构建体系架构
OP-TEE OS 构建体系全解析
基于 PLATFORM=vexpress-qemu_armv8a 平台、原生 kern.ld.S 链接脚本、tee-pager_v2.bin 分页镜像产物,从 Makefile 分层架构、编译流程、链接全链路、链接脚本安全设计四个维度完整拆解,同步对齐 TF-A 可信启动与 Android TEE 安全规范。
一、构建体系总览与正确目录结构
OP-TEE 是 ARM TrustZone 架构下的可信执行环境(TEE),对应 Android 系统的 TEE 服务子系统,采用CFG 宏驱动 + 分层 Makefile 继承的构建体系,所有功能裁剪、内存布局、硬件适配均通过编译宏控制,最终生成可被 TF-A 作为 BL32 加载的安全固件。
核心目录与文件对应关系
optee_os/ ├── Makefile # 顶层构建入口 ├── core/ │ └── arch/arm/ │ ├── arm.mk # ARM架构通用编译规则 │ ├── cpu/ # CPU核专属优化(cortex-a57等) │ ├── kernel/ │ │ ├── kern.ld.S # 你提供的核心态主链接脚本 │ │ ├── text_unpaged.ld.S # 分页:不可换页代码列表 │ │ ├── rodata_unpaged.ld.S # 分页:不可换页只读数据列表 │ │ ├── text_init.ld.S # 分页:初始化专属代码列表 │ │ └── rodata_init.ld.S # 分页:初始化专属数据列表 │ └── plat-vexpress/ # vexpress 平台家族专属目录 │ ├── conf.mk # 平台主配置文件(按 flavor 分支) │ ├── sub.mk # 平台源码列表与编译旗标 │ ├── main.c # 平台初始化入口 │ └── platform_config.h # 平台硬件参数头文件 ├── lib/ # 系统库 ├── ta/ # 内置可信应用 ├── ldelf/ # TA用户态加载器 └── out/ └── arm-plat-vexpress/ # 编译产物输出目录 ├── core/ ├── tee.elf └── tee-pager_v2.bin二、Makefile 分层架构与目标生成逻辑
OP-TEE 采用三层继承式 Makefile 设计,自上而下逐层收敛配置、聚合源码,平台配置决定硬件基线,功能宏决定模块裁剪。
1. 顶层 Makefile:全局入口与调度
核心职责分为四步,是所有编译目标的总调度:
- 参数解析:解析命令行传入的 PLATFORM=vexpress-qemu_armv8a、DEBUG=、CROSS_COMPILE=、自定义 CFG_xxx 等参数
- 平台定位:根据 PLATFORM 变量,自动匹配并引入 core/arch/arm/$(PLATFORM).mk 平台配置文件
- 模块聚合:依次引入 core/core.mk、lib/lib.mk、ta/ta.mk、ldelf/ldelf.mk,聚合所有参与编译的源码与编译规则
- 目标分发:将 all 目标拆解为核心镜像、TA、ldelf 等子目标,按依赖顺序串行执行
核心逻辑片段:
# 加载指定平台的配置文件 include core/arch/arm/$(PLATFORM).mk # 加载各子系统构建规则 include core/core.mk include lib/lib.mk include ta/ta.mk include ldelf/ldelf.mk # 最终目标:生成分页固件 + 所有内置TA all: $(link-out-dir)/tee-pager_v2.bin ta_all ldelf_all2. 平台配置层:硬件适配的核心
conf.mk:硬件配置总入口
这是平台配置的核心文件,内部通过 PLATFORM_FLAVOR 做条件分支,为不同版型设置专属参数。典型结构如下:
# 默认版型 PLATFORM_FLAVOR ?= qemu_virt # 按版型分支配置 ifeq ($(PLATFORM_FLAVOR),qemu_armv8a) $(call force,CFG_TZDRAM_START,0x0E100000) $(call force,CFG_TZDRAM_SIZE,0x01000000) $(call force,CFG_TEE_CORE_NB_CORE,4) $(call force,CFG_WITH_PAGER,y) $(call force,CFG_GICV2,y) $(call force,CFG_PL011,y) endif ifeq ($(PLATFORM_FLAVOR),fvp) # FVP 板子的专属配置... endif # 引入 ARMv8 通用CPU配置 include core/arch/arm/cpu/cortex-armv8-0.mk- BL32 的加载地址 0x0E100000、分页模式、核心数,全部在此文件中定义
- $(call force,...) 表示强制赋值,不可被外部参数覆盖,保证硬件参数一致性
sub.mk:平台源码列表
列出该平台需要编译的源文件与专属头文件路径:
platform-srcs += core/arch/arm/plat-vexpress/main.c platform-srcs += drivers/serial/pl011.c platform-srcs += drivers/gic/gicv2.c global-incdirs-y += core/arch/arm/plat-vexpress/3. 核心构建层:core/core.mk
聚合所有核心态(S-EL1)源码,通过 CFG 宏做条件编译,核心逻辑:
# ARM64架构专属汇编入口、异常向量、上下文切换代码 core-arm64-srcs += core/arch/arm/kernel/entry_a64.S core-arm64-srcs += core/arch/arm/kernel/thread_a64.S # 通用核心子系统 core-subdirs += core/kernel core/mm core/tee core/crypto # 分页模式专属源码 ifeq ($(CFG_WITH_PAGER),y) core-srcs += core/arch/arm/kernel/pager.c core-srcs += core/arch/arm/kernel/tee_pager.c endif # 平台专属源码加入编译列表 core-srcs += $(platform-srcs)完整包含链路(自顶向下)
顶层 Makefile ↓ core/core.mk ↓ core/arch/arm/arm.mk ↓ core/arch/arm/plat-vexpress/conf.mk ← 根据 PLATFORM_FLAVOR 加载对应硬件配置 ↓ core/arch/arm/plat-vexpress/sub.mk ← 加入平台专属源码 ↓ lib/lib.mk、ta/ta.mk、ldelf/ldelf.mk顶层 Makefile 核心调度逻辑
参数解析与拆分:解析命令行参数,自动拆分 PLATFORM 为平台名与 flavor
# 自动拆分 PLATFORM=xxx-yyy 为 PLATFORM=xxx + PLATFORM_FLAVOR=yyy ifneq (,$(findstring -,$(PLATFORM))) ops := $(join PLATFORM PLATFORM_FLAVOR,$(addprefix =,$(subst -, ,$(PLATFORM)))) $(foreach op,$(ops),$(eval override $(op))) endif- 架构与平台加载:根据 ARCH=arm 引入 ARM 架构规则,再根据 PLATFORM 引入对应平台的 conf.mk
- 模块聚合:依次引入核心、库、TA、加载器的构建规则,聚合所有源码
- 目标生成:定义 all 目标,依赖核心固件、所有 TA、ldelf 加载器
4. 分页模式专属打包规则
当 CFG_WITH_PAGER=y 时,不会直接生成单一 tee.bin,而是走分段拆分+头部封装的专用流程:
- 先链接出完整带符号的 tee.elf
- 用 objcopy 拆分出三类独立二进制:常驻段、初始化段、可分页段
- 通过 Python 打包脚本拼接 pager 头部、各段数据,生成 tee-pager_v2.bin
- 运行时由 pager 模块捕获缺页异常,按需加载可分页代码,最小化常驻内存攻击面
三、分特权级编译流程与安全编译设计
OP-TEE 严格按特权级分离编译,核心态(S-EL1)、用户态 TA(S-EL0)、加载器各自使用独立编译选项,所有编译旗标对齐 Android TEE 安全规范。
1. 编译选项优先级
所有旗标按以下层级叠加,后者覆盖前者,保证平台与安全选项优先级最高:
工具链默认选项 → ARM架构通用选项 → vexpress通用配置 → QEMU专属配置 → CFG功能宏 → 用户自定义参数交叉编译工具链与 TF-A 一致,使用 aarch64-none-elf-gcc,通过 CROSS_COMPILE 参数指定。
2. 核心态(S-EL1)编译:最高安全等级
运行在安全异常等级 1,是 OP-TEE 的可信内核,编译选项偏向最小攻击面与安全加固:
- 架构约束:-march=armv8-a -mstrict-align -mgeneral-regs-only,禁用浮点单元,精简安全态上下文切换,避免浮点寄存器泄露
- 安全加固:-fstack-protector-all -fno-common -fno-exceptions -fno-unwind-tables,开启全量栈保护、禁止公共块、禁用异常机制,符合 Android 安全编译规范
- 位置无关:-fpie,核心态编译为位置无关可执行,支持 ASLR 地址随机化
- 优化策略:DEBUG 模式 -O0 -g 关闭优化、保留调试符号;Release 模式 -Os -flto 链接时优化,最小化镜像体积与攻击面
- 产物输出:所有 .c/.S 编译为 .o 目标文件,输出到 out/arm-plat-vexpress-qemu_armv8a/core/ 对应子目录,自动生成 .d 依赖文件支持增量编译
3. 用户态(S-EL0)编译:TA 沙箱约束
所有内置 TA 与 ldelf 加载器运行在安全用户态,编译约束更严格:
- 纯 PIC 编译:-fPIC -fPIE,位置无关代码,运行时动态加载到 TA 内存池,地址不固定
- 沙箱隔离:禁用系统调用、禁用全局变量共享、强制栈边界检查,保证单个 TA 被攻破后无法越权访问内核或其他 TA
- 自动签名:编译完成后用 TA 签名私钥对镜像签名,加载时强制验签,防止 TA 被篡改
四、完整链接执行流程
kern.ld.S 不会直接传给链接器,必须经过「预处理 → 全量链接 → 段拆分 → 封装打包」四步,最终生成可被 TF-A 加载的固件。
步骤1:C 预处理器生成纯链接脚本
OP-TEE 的链接脚本是支持 C 预处理器的汇编级文件,必须先展开所有宏、头文件、条件编译,才能生成可用的 ld 脚本:
cpp -P \ -Icore/include -Icore/arch/arm/include \ -Icore/arch/arm/plat-vexpress/ \ -DCFG_WITH_PAGER=y \ -DCFG_TZDRAM_START=0x0E100000 \ -DTEE_LOAD_ADDR=0x0E100000 \ -DSMALL_PAGE_SIZE=4096 \ core/arch/arm/kernel/kern.ld.S \ > out/arm-plat-vexpress-qemu_armv8a/core/kern.ld这一步是配置驱动内存布局的核心:平台差异、功能开关、安全特性全部通过宏展开体现在最终链接规则中。
步骤2:全量链接生成 ELF 文件
调用交叉链接器,使用生成的 kern.ld,把所有核心态 .o、系统库 .a 合并为完整的带符号 ELF:
aarch64-none-elf-ld -T kern.ld -Map tee.map \ --start-group $(core_objs) $(lib_objs) --end-group \ -o tee.elf- 生成的 tee.elf 包含完整符号表、调试信息、段信息,是调试与安全分析的核心文件
- tee.map 是内存映射文件,可查看每个符号、每个段的精确地址,常用于安全审计与边界校验
步骤3:分页模式段拆分
开启分页后,使用 objcopy 按段属性拆分出三类独立二进制:
# 提取常驻核心段(unpaged):系统运行全程驻留内存 aarch64-none-elf-objcopy -O binary \ -j .text -j .rodata -j .data -j .bss \ tee.elf core.bin # 提取初始化段(init):启动后可回收为堆内存 aarch64-none-elf-objcopy -O binary \ -j .text_init -j .rodata_init \ tee.elf init.bin # 提取可分页段(pageable):按需加载,不常驻内存 aarch64-none-elf-objcopy -O binary \ -j .rodata_pageable -j .text_pageable \ tee.elf pageable.bin步骤4:封装生成最终固件
调用专用 Python 打包脚本,按 Pager V2 格式拼接头部元数据、常驻段、初始化段、可分页段,生成最终的 tee-pager_v2.bin。该文件就是放入 FIP 包的 BL32 镜像,日志中 BL32 大小 0x7B238 就是该文件的精确字节数。
五、kern.ld.S 链接脚本深度解析
基于原生源码,从地址锚点、段布局、安全设计、分页机制四个维度逐段拆解。
1. 头部配置与依赖
#include <mm/core_mmu.h> // 提供页大小、MMU配置宏 #include <platform_config.h> // 平台硬件参数,核心地址全部来自这里 #include <util.h>
#include <mm/core_mmu.h> // 提供页大小、MMU配置宏 #include <platform_config.h> // 平台硬件参数,核心地址全部来自这里 #include <util.h> OUTPUT_FORMAT(CFG_KERN_LINKER_FORMAT) // elf64-littleaarch64 OUTPUT_ARCH(CFG_KERN_LINKER_ARCH) // aarch64 ENTRY(_start) // 镜像入口函数- 入口点 _start 对应汇编启动入口,地址等于镜像基地址 TEE_LOAD_ADDR,也就是 TF-A 跳转到 BL32 的目标地址
- 所有宏都来自头文件与平台配置,保证链接脚本和代码使用同一套地址定义,避免不一致风险
2. 核心地址锚点(可信启动对齐关键)
SECTIONS { . = TEE_LOAD_ADDR; ASSERT(!(TEE_LOAD_ADDR & (SMALL_PAGE_SIZE - 1)), "text start should be page aligned") __text_start = .;- 地址锚点:整个镜像的链接起始地址由 TEE_LOAD_ADDR 决定,直接映射平台配置的 CFG_TZDRAM_START=0x0E100000,和 TF-A BL32 加载地址 100% 对齐
- 页对齐断言:强制要求起始地址按 4KB 页边界对齐,符合 MMU 内存映射硬件要求,是内存安全的基础
- __text_start 作为代码段起始符号,供代码中做边界校验使用
3. 通用常驻段(全模式生效)
这部分代码和数据系统运行全程常驻内存,不会被换出,是最小可信攻击面。
(1).text可执行代码段
.text : { KEEP(*(.text._start)) // 强制保留启动入口,放在最开头 __identity_map_init_start = .; *(.identity_map.data) // MMU开启前访问的数据(恒等映射) *(.identity_map .identity_map.*) // MMU开启前执行的初始化代码 __identity_map_init_end = .; #ifdef CFG_WITH_PAGER *(.text) #include <text_unpaged.ld.S> // 仅引入不可换页的核心代码 #else *(.text .text.*) // 非分页模式包含全部代码 #endif . = ALIGN(8); } __text_end = .;- 安全设计:代码段只读可执行,和后续数据段权限严格隔离,实现 NX(不可执行)防护,防止代码注入攻击
- 恒等映射区:MMU 开启前运行的代码,物理地址=虚拟地址,是启动初期的临界安全区
- text_unpaged.ld.S 枚举了所有必须常驻的核心代码(异常向量、调度核心、pager 自身逻辑),分页模式下不会被换出,避免递归缺页异常
(2).rodata只读数据段
.rodata : ALIGN(8) { __rodata_start = .; #ifdef CFG_WITH_PAGER *(.rodata .rodata.__unpaged .rodata.__unpaged.*) #include <rodata_unpaged.ld.S> // 仅保留不可换页的核心常量 #else *(.rodata .rodata.*) KEEP(*(SORT(.scattered_array*))); // 散列注册数组(驱动、TA自动注册) #endif . = ALIGN(8); __rodata_end = .; }- 分页模式下只保留核心常量、配置表,大部分只读数据放到可分页段,减小常驻体积
- scattered_array 是 OP-TEE 的核心注册机制,驱动、TA 接口通过散列数组自动注册,无需手动修改入口表,降低代码修改引入的安全风险
(3)数据与未初始化段
段名 | 属性 | 作用 | 安全设计 |
.data | RW 可读写 | 已初始化全局变量、静态变量 | 双地址设计(ROM地址/RAM地址),支持重定位与地址随机化 |
.bss | RW 可读写 | 未初始化全局变量 | 不占用 bin 文件体积,启动时清零,避免残留敏感数据 |
.nozi | NOLOAD 不加载 | MMU页表、内核栈、临时缓冲区 | 不初始化,避免清零开销;每个CPU核心独立栈空间,防止栈溢出跨核破坏 |
.heap1 | NOLOAD 不加载 | 内核堆内存 | 按16KB大页对齐,匹配MMU L1页表粒度 |
4. 分页模式专属段(CFG_WITH_PAGER=y)
这是 tee-pager_v2.bin 的核心设计,通过分段换页最小化常驻内存攻击面。
(1)初始化段
.text_init : { __text_init_start = .; #include <text_init.ld.S> // 初始化专属代码列表 KEEP(*(.text.startup.*)); . = ALIGN(8); __text_init_end = .; } .rodata_init : { __rodata_init_start = .; #include <rodata_init.ld.S> __rodata_init_end = .; }- 仅在系统启动阶段执行,包含驱动初始化、外设配置、服务注册代码
- 初始化完成后,该段内存可以被回收用作堆内存,进一步释放安全内存空间
(2)可分页段
.rodata_pageable : ALIGN(8) { __rodata_pageable_start = .; *(.rodata*) __rodata_pageable_end = .; } .text_pageable : ALIGN(8) { __text_pageable_start = .; *(.text*) . = ALIGN(SMALL_PAGE_SIZE); __text_pageable_end = .; }- 这部分代码/数据不常驻内存,运行时由 pager 模块根据缺页异常按需加载到物理页
- 所有段按 4KB 页边界对齐,支持 MPU/MMU 按页设置权限,实现细粒度内存保护
(3)分页安全断言
脚本末尾包含强制内存校验:
ASSERT(TEE_LOAD_ADDR >= TEE_RAM_START, "Load address before start of physical memory") ASSERT(TEE_LOAD_ADDR < (TEE_RAM_START + TEE_RAM_PH_SIZE), "Load address after end of physical memory") ASSERT((TEE_RAM_START + TEE_RAM_PH_SIZE - __init_end) > SMALL_PAGE_SIZE * 2 + (__pageable_end - __pageable_start) / 4096 * 32 + ..., "Too few free pages to initialize paging")- 校验加载地址在安全物理内存范围内,防止越界
- 校验剩余空闲页足够支撑分页表、重定位表开销,避免启动时内存不足导致的安全漏洞
5. 收尾:虚拟内存符号与丢弃段
(1)虚拟内存映射符号
脚本末尾定义了一系列 __vcore_* 开头的全局符号,对应 MMU 映射后的虚拟地址区域:
- __vcore_unpg_rx_start/size:不可换页可执行区域的虚拟地址与大小
- __vcore_unpg_rw_start/size:不可换页可读写区域的虚拟地址与大小
- 这些符号供 MMU 初始化代码使用,按区域设置不同的页表权限(RX/RO/RW),实现段级权限隔离
(2)重定位与丢弃校验
- .rel/.rela 重定位段:支持 ASLR 与物理重定位;非重定位模式下断言其大小为0,防止意外的重定位项引入安全风险
- /DISCARD/ 丢弃段:删除 .comment、.eh_frame、.interp 等无用段,最小化镜像体积与攻击面
六、Android 安全视角:设计价值与 TF-A 可信启动对齐
1. 信任链的严格对齐
- 加载地址 TEE_LOAD_ADDR 与 TF-A BL32 加载地址严格一致,入口点 _start 位于镜像起始位置,保证 BL31 跳转后直接执行可信入口
- BL2 验签的对象就是完整的 tee-pager_v2.bin 全量字节,验签范围与镜像文件逐字节对应,信任链连续无断点
2. 内存安全的多层防护
符合 Android TEE 安全规范要求:
- 段权限隔离:代码段可执行不可写、数据段可写不可执行,实现 NX 防护
- 栈隔离:每个 CPU 核心独立栈空间,链接时固定地址,避免栈溢出跨核破坏
- 页对齐与MPU支持:所有段按页边界对齐,支持按页设置权限,实现细粒度内存保护
- ASLR 支持:位置无关编译 + 重定位段,支持地址随机化,降低漏洞利用成功率
3. 分页机制的安全意义
分页模式不仅是节省内存,更重要的是最小化常驻攻击面:
- 仅核心异常处理、调度、pager 自身代码常驻内存,大部分功能代码按需加载
- 即使某段代码存在漏洞,也只有被加载时才会暴露在攻击面中,提升整体安全水位
