4. TA开发入门

好,咱们今天聊聊TA开发。很多刚接触OP-TEE的朋友,上来就被一堆概念搞懵了。什么TA、CA、共享内存、会话……其实没那么复杂。你想想看,TA说白了就是一个运行在安全世界的小程序。它跟普通Linux程序最大的区别是什么?嗯,它不能随便访问外设,不能随便打印日志,所有操作都得通过OP-TEE内核来协调。

我个人习惯把TA比作一个“安全沙箱里的服务员”。它只做一件事:接收CA(普通世界的客户端)发来的请求,处理完,再把结果还回去。今天我们就把这个服务员的工作流程拆开看看。

4.1 TA的基本结构

一个标准的TA,从代码结构上看,通常包含这几个部分:

  • 头文件与UUID定义:每个TA都有一个唯一的UUID,就像身份证号。OP-TEE靠这个来识别该加载哪个TA。
  • 入口函数表:告诉OP-TEE内核,我这个TA支持哪些操作。
  • 属性定义:比如TA是单实例还是多实例,栈大小、堆大小等。
  • 命令处理函数:真正干活的代码。

我刚开始写TA时,最常犯的错误就是UUID写错了。你想想看,UUID错了,CA根本找不到TA,但编译还不报错。调试起来特别痛苦。后来我养成了一个习惯:每次新建TA,第一件事就是检查UUID是否跟CA端匹配。

下面是一个典型的TA目录结构:

ta/
├── include/
│   └── ta_my_test.h      // 定义命令ID和共享内存结构
├── ta_my_test.c          // 主逻辑
├── user_ta_header_defines.h // UUID、栈大小等宏定义
├── sub.mk                // 编译配置
└── Makefile              // 顶层构建文件

4.2 TA的入口函数

TA的入口函数,其实不是一个函数,而是一组函数指针。OP-TEE用了一个结构体来管理它们:

static struct ta_ops my_ta_ops = {
    .create = TA_CreateEntryPoint,
    .destroy = TA_DestroyEntryPoint,
    .open_session = TA_OpenSessionEntryPoint,
    .close_session = TA_CloseSessionEntryPoint,
    .invoke_command = TA_InvokeCommandEntryPoint,
};

这五个函数,就是TA的“生命周期回调”。我画了一张图,帮你理解它们之间的关系:

TA生命周期与命令处理流程 1. 加载TA 2. TA_CreateEntryPoint 3. 打开会话 4. TA_InvokeCommandEntryPoint 多次调用 5. TA_CloseSessionEntryPoint 6. TA_DestroyEntryPoint

看到这张图,你应该能理解整个流程了。我来逐个解释一下:

  • TA_CreateEntryPoint:TA被加载时调用一次。我一般在这里做全局初始化,比如分配内存池、初始化密码学上下文。注意,这个函数只调用一次,不是每次会话都调。
  • TA_DestroyEntryPoint:TA被卸载时调用。清理资源,释放内存。
  • TA_OpenSessionEntryPoint:每次CA发起会话时调用。可以在这里做会话级别的初始化,比如验证CA的身份。
  • TA_CloseSessionEntryPoint:会话关闭时调用。清理会话相关的资源。
  • TA_InvokeCommandEntryPoint:核心函数。CA每次发命令,都会走到这里。
我的经验:很多新手把初始化逻辑全塞在TA_InvokeCommandEntryPoint里,每次命令都重复初始化。这其实很浪费。把一次性的初始化放到TA_CreateEntryPoint里,性能会好很多。

4.3 TA的命令处理流程

好,现在咱们聚焦到最核心的部分:命令处理。CA发来一个命令,TA怎么接?怎么处理?怎么返回?

先看一个典型的TA_InvokeCommandEntryPoint实现:

TEE_Result TA_InvokeCommandEntryPoint(void *session_context,
                                       uint32_t cmd_id,
                                       uint32_t param_types,
                                       TEE_Param params[4])
{
    // 根据命令ID分发
    switch (cmd_id) {
    case CMD_ENCRYPT:
        return handle_encrypt(session_context, param_types, params);
    case CMD_DECRYPT:
        return handle_decrypt(session_context, param_types, params);
    case CMD_GENERATE_KEY:
        return handle_generate_key(session_context, param_types, params);
    default:
        EMSG("Unknown command ID: %u", cmd_id);
        return TEE_ERROR_BAD_PARAMETERS;
    }
}

这个函数有四个参数:

参数 含义 说明
session_context 会话上下文指针 可以在这里存放会话级别的数据,比如登录状态
cmd_id 命令ID 由CA和TA共同约定,比如0x01表示加密
param_types 参数类型编码 告诉TA每个参数是输入、输出还是值传递
params[4] 参数数组 最多4个参数,可以是内存引用或值

这里有个关键点:param_types。它编码了每个参数的类型。OP-TEE定义了四种参数类型:

  • TEE_PARAM_TYPE_NONE:无参数
  • TEE_PARAM_TYPE_VALUE_INPUT:输入值(32位整数对)
  • TEE_PARAM_TYPE_VALUE_OUTPUT:输出值
  • TEE_PARAM_TYPE_VALUE_INOUT:输入输出值
  • TEE_PARAM_TYPE_MEMREF_INPUT:输入内存引用
  • TEE_PARAM_TYPE_MEMREF_OUTPUT:输出内存引用
  • TEE_PARAM_TYPE_MEMREF_INOUT:输入输出内存引用
我曾经踩过的坑:有一次我写TA,CA传了一个内存引用,但我在TA端用TEE_PARAM_TYPE_VALUE_INPUT去解析。结果呢?数据全乱了,而且还不报错。后来查了半天才发现是参数类型不匹配。记住:CA和TA的参数类型必须严格一致,否则就是未定义行为。

处理内存引用时,我建议你养成一个习惯:先检查参数类型,再访问数据。比如:

TEE_Result handle_encrypt(void *session_context,
                           uint32_t param_types,
                           TEE_Param params[4])
{
    // 检查参数类型
    if (param_types != TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT,
                                       TEE_PARAM_TYPE_MEMREF_OUTPUT,
                                       TEE_PARAM_TYPE_NONE,
                                       TEE_PARAM_TYPE_NONE)) {
        return TEE_ERROR_BAD_PARAMETERS;
    }

    // 获取输入数据
    void *input_data = params[0].memref.buffer;
    size_t input_size = params[0].memref.size;

    // 获取输出缓冲区
    void *output_data = params[1].memref.buffer;
    size_t output_size = params[1].memref.size;

    // 执行加密...
    // 记得更新实际输出大小
    params[1].memref.size = actual_output_size;

    return TEE_SUCCESS;
}

看到没?处理流程其实很清晰:

  1. 检查参数类型是否匹配
  2. 从params数组中提取输入数据
  3. 执行业务逻辑
  4. 把结果写回输出参数
  5. 返回TEE_SUCCESS或错误码

核心要点:TA的命令处理,本质上就是一个“解析-处理-返回”的循环。你只要把每个命令的处理逻辑封装好,整个TA的骨架就搭起来了。

嗯,说到这里,我想起一个细节。很多人在写TA时,喜欢在TA_InvokeCommandEntryPoint里直接处理所有逻辑。我个人习惯是:每个命令单独写一个函数,比如handle_encrypt、handle_decrypt。这样代码清晰,也方便单元测试。你想想看,如果所有逻辑都堆在一个函数里,后期维护起来得多痛苦?

最后,关于返回值。OP-TEE定义了一套标准的错误码,比如TEE_SUCCESS、TEE_ERROR_BAD_PARAMETERS、TEE_ERROR_OUT_OF_MEMORY等。我建议你尽量使用这些标准错误码,而不是自定义一些奇怪的数字。为什么呢?因为CA端可以根据错误码做统一的错误处理,调试起来也方便。