4. 核心数据结构

好,咱们今天来啃硬骨头。这几个数据结构,说白了就是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时,我都会在priv里塞一个时间戳。这样排查问题时,能快速知道这个Context是什么时候创建的,是不是被复用了。

创建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不是线程安全的!如果你在多线程里共享同一个Session,记得加锁。我有一回没注意,两个线程同时发命令,结果TEE那边收到了乱序的数据包,直接崩溃了。

打开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;
};
核心要点:Value类型传小数据(两个64位整数),MemRef类型传大数据(通过共享内存)。选错了类型,轻则性能下降,重则数据截断。

举个例子,传一个加密命令:

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之间交换大块数据的唯一通道。说白了,就是一块双方都能读写的内存区域。

管理共享内存有三个关键操作:

  1. 分配:从内核申请一块物理连续的内存
  2. 注册:告诉TEE「这块内存我要用」,TEE会建立映射
  3. 释放:用完归还,避免内存泄漏

分配和注册的代码:

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);
注意:共享内存的分配是稀缺资源。我见过有人每个命令都分配释放,结果系统内存碎片化严重,最后分配失败。我的建议是:复用共享内存。在Session打开时预分配一块,整个生命周期内重复使用。

释放操作:

tee_shm_free(shm);

就这么简单。但别忘了,忘了释放就是内存泄漏。我在项目里加了一个调试宏,每次分配时记录调用栈,释放时校验,这样就能追踪到谁忘了释放。

4.6 知识体系总览

说了这么多,咱们用一张图来总结一下这几个数据结构的关系:

TEE Context 与TEE世界的连接 teedev, priv TEE Session 一次具体的通话 session_id, ctx 1:N TEE Operation 命令的载体 func, session, params 包含 TEE Parameter 数据的快递盒 Value / MemRef 1:N 共享内存 数据高速公路 分配 / 注册 / 释放 MemRef引用 核心关系:Context → Session → Operation → Parameter → 共享内存 一个Context可打开多个Session,一个Operation包含多个Parameter

这张图把关系理得很清楚了。从上到下,一层套一层。你写代码时,脑子里要有这张图,就不会搞混了。

4.7 避坑指南

最后,我把自己踩过的坑总结一下,你遇到了可以少走弯路:

  • 共享内存大小限制:不同平台限制不同,有的最大64KB,有的1MB。分配前先查一下设备能力。
  • Parameter顺序:CA和TA必须约定好参数的顺序和类型。我见过有人改了TA的参数顺序,忘了改CA,结果数据全对不上。
  • Session超时:长时间不用的Session可能会被TEE回收。建议加心跳机制,或者用完就关。
  • 内存对齐:共享内存的偏移量通常需要4字节对齐。不对齐的话,某些TA会直接返回错误。
我的调试小技巧:在开发阶段,我会在每次invoke前后打印所有Parameter的内容。虽然日志会很多,但排查问题特别快。等稳定了再关掉。

好了,核心数据结构就讲到这里。这些结构体是OP-TEE驱动的基石,你花时间把它们吃透,后面的开发就会顺风顺水。