4. 核心数据结构:TEE Context、TEE Session、TEE Operation、TEE Parameter、共享内存管理
好,咱们今天来啃硬骨头。这几个数据结构,说白了就是OP-TEE驱动开发的「四梁八柱」。你如果搞不懂它们,后面写代码就是空中楼阁。我当年刚接触TEE时,就是被这些结构体绕晕过,后来踩了几个坑才真正明白它们的设计意图。
4.1 TEE Context:你与TEE世界的「连接线」
TEE Context,你可以把它想象成一根电话线。你拨通了TEE的号码,这根线就建立起来了。没有它,你连门都进不去。
在代码里,它长这样:
struct tee_context {
struct tee_device *teedev;
void *priv;
bool memremap_pages;
unsigned long flags;
};
这里我重点说两个字段:
- teedev:指向TEE设备的指针。一个系统里可能有多个TEE设备,比如一个安全世界、一个虚拟化实例。这个指针告诉你当前连的是哪条线。
- priv:私有数据指针。驱动开发者可以在这里挂自己的上下文数据。我习惯用它来存一些调试信息,比如当前会话的日志级别。
创建Context的流程很简单:
struct tee_context *ctx = tee_client_open_context(NULL, NULL, NULL, NULL);
if (IS_ERR(ctx)) {
pr_err("打开TEE Context失败\n");
return PTR_ERR(ctx);
}
嗯,这里要注意。第一个参数传NULL表示使用默认TEE设备。如果你有多个设备,得指定设备名。我曾经在一个多核平台上吃过亏,没指定设备,结果连到了错误的TEE实例,数据全乱了。
4.2 TEE Session:一次具体的「通话」
Context是电话线,Session就是一次具体的通话。你拨号、通话、挂断,对应着打开Session、调用命令、关闭Session。
Session的结构体:
struct tee_session {
struct tee_context *ctx;
u32 session_id;
struct list_head list;
struct mutex mutex;
void *priv;
};
关键点:
- session_id:TEE世界分配的唯一ID。这个ID很重要,后续所有操作都要带上它,告诉TEE「我是谁」。
- ctx:回指所属的Context。一个Context可以打开多个Session,就像一根电话线可以打多个电话(但一次只能一个)。
打开Session的典型代码:
struct tee_ioctl_open_session_arg arg;
memset(&arg, 0, sizeof(arg));
arg.num_params = 0;
arg.clnt_login = TEE_IOCTL_LOGIN_PUBLIC;
struct tee_session *sess = tee_client_open_session(ctx, &arg, NULL);
if (IS_ERR(sess)) {
pr_err("打开Session失败\n");
return PTR_ERR(sess);
}
4.3 TEE Operation:命令的「载体」
你想想看,你给TEE发一个命令,总得有个信封把命令装起来吧?TEE Operation就是这个信封。
它的定义:
struct tee_ioctl_invoke_arg {
__u32 func;
__u32 session;
__u32 cancel_id;
__u32 num_params;
__u32 pad;
struct tee_param params[];
};
- func:命令ID。比如0x01表示「加密」,0x02表示「解密」。这个ID由TA(Trusted Application)定义。
- session:Session ID,告诉TEE是哪个会话发的命令。
- params:参数数组。这里就是TEE Parameter的用武之地了。
我个人习惯把Operation的创建封装成一个函数:
static int invoke_tee_command(struct tee_session *sess, u32 cmd_id,
struct tee_param *params, u32 num_params)
{
struct tee_ioctl_invoke_arg arg;
memset(&arg, 0, sizeof(arg));
arg.func = cmd_id;
arg.session = sess->session_id;
arg.num_params = num_params;
return tee_client_invoke_func(sess, &arg, params);
}
这样调用起来就清爽多了。你想想看,每次都要手动填arg,多容易出错。
4.4 TEE Parameter:数据的「快递盒」
TEE Parameter是数据传递的基本单元。它支持四种类型:
| 类型 | 说明 | 典型用途 |
|---|---|---|
| TEE_PARAM_TYPE_NONE | 空参数,占位用 | 不需要传参的命令 |
| TEE_PARAM_TYPE_VALUE_INPUT | 输入值,传小数据 | 密钥ID、算法类型 |
| TEE_PARAM_TYPE_VALUE_OUTPUT | 输出值,接收小数据 | 返回值、状态码 |
| TEE_PARAM_TYPE_MEMREF_INPUT | 输入内存引用,传大数据 | 明文数据、证书 |
| TEE_PARAM_TYPE_MEMREF_OUTPUT | 输出内存引用,接收大数据 | 密文数据、签名 |
| TEE_PARAM_TYPE_MEMREF_INOUT | 双向内存引用 | 更新操作、读写共享内存 |
结构体定义:
struct tee_param {
__u64 attr;
union {
struct {
__u64 a;
__u64 b;
} value;
struct {
__u64 shm_offs;
__u64 shm_id;
__u64 shm_size;
} memref;
} u;
};
举个例子,传一个加密命令:
struct tee_param params[2];
// 参数0:输入明文数据(通过共享内存)
params[0].attr = TEE_PARAM_TYPE_MEMREF_INPUT;
params[0].u.memref.shm_id = shm->id;
params[0].u.memref.shm_offs = 0;
params[0].u.memref.shm_size = plaintext_len;
// 参数1:输出密钥ID(小数据)
params[1].attr = TEE_PARAM_TYPE_VALUE_OUTPUT;
params[1].u.value.a = 0; // 初始值,TEE会填
invoke_tee_command(sess, CMD_ENCRYPT, params, 2);
4.5 共享内存管理:数据的「高速公路」
共享内存是CA和TA之间交换大块数据的唯一通道。说白了,就是一块双方都能读写的内存区域。
管理共享内存有三个关键操作:
- 分配:从内核申请一块物理连续的内存
- 注册:告诉TEE「这块内存我要用」,TEE会建立映射
- 释放:用完归还,避免内存泄漏
分配和注册的代码:
struct tee_shm *shm = tee_shm_alloc(ctx, SHM_SIZE,
TEE_SHM_ALLOC);
if (IS_ERR(shm)) {
pr_err("共享内存分配失败\n");
return PTR_ERR(shm);
}
// 获取内核虚拟地址,可以直接读写
void *buf = tee_shm_get_va(shm);
memset(buf, 0, SHM_SIZE);
memcpy(buf, data, data_len);
释放操作:
tee_shm_free(shm);
就这么简单。但别忘了,忘了释放就是内存泄漏。我在项目里加了一个调试宏,每次分配时记录调用栈,释放时校验,这样就能追踪到谁忘了释放。
4.6 知识体系总览
说了这么多,咱们用一张图来总结一下这几个数据结构的关系:
这张图把关系理得很清楚了。从上到下,一层套一层。你写代码时,脑子里要有这张图,就不会搞混了。
4.7 避坑指南
最后,我把自己踩过的坑总结一下,你遇到了可以少走弯路:
- 共享内存大小限制:不同平台限制不同,有的最大64KB,有的1MB。分配前先查一下设备能力。
- Parameter顺序:CA和TA必须约定好参数的顺序和类型。我见过有人改了TA的参数顺序,忘了改CA,结果数据全对不上。
- Session超时:长时间不用的Session可能会被TEE回收。建议加心跳机制,或者用完就关。
- 内存对齐:共享内存的偏移量通常需要4字节对齐。不对齐的话,某些TA会直接返回错误。
好了,核心数据结构就讲到这里。这些结构体是OP-TEE驱动的基石,你花时间把它们吃透,后面的开发就会顺风顺水。