第4章:TA开发入门:TA的基本结构、TA的入口函数、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_CreateEntryPoint:TA被加载时调用一次。我一般在这里做全局初始化,比如分配内存池、初始化密码学上下文。注意,这个函数只调用一次,不是每次会话都调。
- TA_DestroyEntryPoint:TA被卸载时调用。清理资源,释放内存。
- TA_OpenSessionEntryPoint:每次CA发起会话时调用。可以在这里做会话级别的初始化,比如验证CA的身份。
- TA_CloseSessionEntryPoint:会话关闭时调用。清理会话相关的资源。
- TA_InvokeCommandEntryPoint:核心函数。CA每次发命令,都会走到这里。
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:输入输出内存引用
处理内存引用时,我建议你养成一个习惯:先检查参数类型,再访问数据。比如:
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;
}
看到没?处理流程其实很清晰:
- 检查参数类型是否匹配
- 从params数组中提取输入数据
- 执行业务逻辑
- 把结果写回输出参数
- 返回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端可以根据错误码做统一的错误处理,调试起来也方便。
好了,TA的基本结构、入口函数和命令处理流程,就讲到这里。下一章我们会深入TA的编译和链接,看看一个TA是怎么从源码变成可执行文件的。