4. OP-TEE内存管理
内存管理,说白了就是TEE的命脉。我刚开始接触OP-TEE时,觉得这玩意儿不就是分分内存嘛,有啥难的?结果第一次调安全内存配置,系统直接崩了,连log都没来得及打印。嗯,从那以后我再也不敢小看这块了。
今天咱们就聊聊OP-TEE的内存管理。我会结合自己踩过的坑,把安全内存配置、内存隔离、MMU配置和动态内存分配这几个核心点讲透。
核心要点:OP-TEE的内存管理,本质上是把物理内存切成「安全区」和「非安全区」,然后通过硬件机制保证它们老死不相往来。
4.1 安全内存配置
先说说安全内存怎么配。OP-TEE启动时,需要告诉系统:「这块内存归我管,谁也别碰」。这个配置通常在设备树或平台配置文件中完成。
我个人习惯把安全内存分成三块:
- TEE核心代码段:存放OP-TEE OS本身,只读可执行
- TEE数据段:存放全局变量、栈等,读写不可执行
- 安全应用堆区:给TA用的动态内存,按需分配
举个例子,在conf.mk里你会看到类似这样的配置:
# 安全内存基地址和大小
CFG_TZDRAM_START = 0xBE000000
CFG_TZDRAM_SIZE = 0x02000000 # 32MB
# TEE核心内存
CFG_TEE_RAM_VA_SIZE = 0x00200000 # 2MB
这里有个坑——地址对齐。安全内存的起始地址必须按1MB对齐,这是ARM架构的要求。我曾经因为地址没对齐,调试了整整两天,最后发现是少了个0。
注意:安全内存的大小不是越大越好。你分多了,REE那边就不够用;分少了,TA跑不起来。一般建议先给32MB,跑起来后再根据实际需求调整。
4.2 内存隔离机制
内存隔离,说白了就是让安全世界和非安全世界「物理隔离」。ARM是怎么做到的呢?靠的是TZASC(TrustZone Address Space Controller)。
TZASC的工作原理很简单:它把物理内存分成若干个区域,每个区域可以配置成「安全」或「非安全」。安全区域只有TEE能访问,非安全区域两边都能访问——但TEE访问非安全区域时,数据会被自动加密。
我记得在某个项目中,客户要求TEE能直接读取REE的摄像头数据。当时我就想,这怎么搞?后来发现,只要把共享内存区域配成「非安全」,两边都能读写,问题就解决了。
配置TZASC的代码大概长这样:
/* 配置区域0为非安全 */
tzasc_configure_region(0, TZASC_REGION_NONSECURE,
0x40000000, 0x20000000);
/* 配置区域1为安全 */
tzasc_configure_region(1, TZASC_REGION_SECURE,
0xBE000000, 0x02000000);
你想想看,如果没有这个隔离机制,REE上的恶意程序直接读取安全内存,那TEE还有什么意义?
4.3 MMU配置
MMU(内存管理单元)是TEE的「翻译官」。它负责把虚拟地址翻译成物理地址,同时控制访问权限。
OP-TEE的MMU配置有几个关键点:
- 页表结构:ARMv7用两级页表,ARMv8用四级页表
- 内存属性:缓存策略、共享属性、执行权限
- 域控制:区分安全域和非安全域
我建议你重点关注内存属性。举个例子,TA的代码段应该是「只读可执行」,数据段是「读写不可执行」。如果配反了,要么跑不起来,要么有安全漏洞。
看一段实际的MMU配置代码:
/* 配置TEE核心内存映射 */
core_mmu_map_region(tee_mm_param, ARRAY_SIZE(tee_mm_param));
/* 配置TA内存映射 */
struct tee_mmap_region ta_mem = {
.va = TA_RAM_START,
.pa = TA_RAM_PHYS,
.size = TA_RAM_SIZE,
.attr = ATTR_READ | ATTR_WRITE | ATTR_NSEC,
};
这里有个细节:ATTR_NSEC表示这块内存是非安全的。为什么TA内存要配成非安全?因为TA本身是运行在用户态的,不能直接访问安全内核的内存。
小技巧:调试MMU问题时,可以打开OP-TEE的MMU调试日志。在conf.mk里加上CFG_TEE_CORE_DEBUG=y,就能看到页表映射的详细信息。
4.4 动态内存分配
动态内存分配,就是TA在运行时向TEE内核申请内存。OP-TEE用的是malloc和free,但底层实现和Linux完全不同。
OP-TEE的内存分配器有几个特点:
- 固定大小池:预分配几个大小固定的内存池,比如16字节、32字节、64字节...
- 无碎片:因为池大小固定,不会产生外部碎片
- 快速分配:直接从池里取,不需要复杂的查找算法
我曾经遇到过一个诡异的问题:TA跑着跑着就OOM了。查了半天,发现是某个TA申请了内存后忘了释放。嗯,从那以后我写TA代码时,都会在free后面加个NULL赋值,防止野指针。
看一个动态内存分配的例子:
/* TA中申请内存 */
void *buf = malloc(1024);
if (!buf) {
EMSG("内存分配失败");
return TEE_ERROR_OUT_OF_MEMORY;
}
/* 使用内存... */
/* 释放内存 */
free(buf);
buf = NULL; /* 防止野指针 */
这里要注意,TA能申请的最大内存是有限制的。默认情况下,每个TA最多能申请1MB。如果不够用,可以在conf.mk里调整:
CFG_TA_ASAN_MAX_HEAP_SIZE = 0x100000 # 1MB
4.5 知识体系总览
说了这么多,咱们用一张图来总结一下OP-TEE内存管理的整体架构:
这张图展示了OP-TEE内存管理的四个层次。从底层的硬件隔离,到内核的内存管理,再到TA的分配接口,最后是安全边界。每一层都环环相扣,缺一不可。
总结一下:
- 安全内存配置要关注地址对齐和大小
- 内存隔离靠TZASC实现物理隔离
- MMU配置要特别注意内存属性
- 动态内存分配用固定大小池,注意防泄漏
好了,关于OP-TEE内存管理就聊到这儿。这些内容都是我实际调试中一点一点积累出来的,希望能帮你少走弯路。记住,内存管理出了问题,别慌,先从配置开始查,一步步来,总能找到问题所在。