深入解析OP-TEE的libteec核心API实现
在 OP-TEE 体系中,libteec是运行在 REE 用户态的客户端库。它完整实现了 GlobalPlatform (GP) 标准的 TEE Client API。libteec的核心文件是src/tee_client_api.c。它的本质工作非常纯粹:将上层面向对象的 GP API 转换为对 Linux 内核 TEE 驱动(/dev/tee0)的ioctl级联调用。
以下梳理其最核心的三个 API 的代码实现骨架、关键数据结构及参数序列化逻辑。
一、TEEC_InitializeContext:初始化上下文与建立通道
该函数负责打开 TEE 驱动的字符设备文件,并获取当前 TEE 的基本版本信息与软硬件能力(Capabilities)。C
TEEC_Result TEEC_InitializeContext(const char *name, TEEC_Context *ctx) { char devname[PATH_MAX] = { 0 }; int fd = 0; size_t n = 0; if (!ctx) return TEEC_ERROR_BAD_PARAMETERS; for (n = 0; n < TEEC_MAX_DEV_SEQ; n++) { uint32_t gen_caps = 0; snprintf(devname, sizeof(devname), "/dev/tee%zu", n); fd = teec_open_dev(devname, name, &gen_caps); if (fd >= 0) { ctx->imp.fd = fd; ctx->imp.reg_mem = gen_caps & TEE_GEN_CAP_REG_MEM; ctx->imp.memref_null = gen_caps & TEE_GEN_CAP_MEMREF_NULL; return TEEC_SUCCESS; } } return TEEC_ERROR_ITEM_NOT_FOUND; }二、TEEC_OpenSession:开启与指定 TA 的安全会话
在调用具体的 TA 业务前,需要通过 UUID 寻址来拉起目标 TA。此时,libteec会把 GP 的TEEC_Operation结构参数打包转换为内核识别的tee_ioctl_param。C
TEEC_Result TEEC_OpenSession(TEEC_Context *ctx, TEEC_Session *session, const TEEC_UUID *destination, uint32_t connection_method, const void *connection_data, TEEC_Operation *operation, uint32_t *ret_origin) { struct tee_ioctl_open_session_arg *arg = NULL; struct tee_ioctl_param *params = NULL; TEEC_Result res = TEEC_ERROR_GENERIC; uint32_t eorig = 0; int rc = 0; const size_t arg_size = sizeof(struct tee_ioctl_open_session_arg) + TEEC_CONFIG_PAYLOAD_REF_COUNT * sizeof(struct tee_ioctl_param); union { struct tee_ioctl_open_session_arg arg; uint8_t data[arg_size]; } buf; struct tee_ioctl_buf_data buf_data; TEEC_SharedMemory shm[TEEC_CONFIG_PAYLOAD_REF_COUNT]; memset(&buf, 0, sizeof(buf)); memset(&shm, 0, sizeof(shm)); memset(&buf_data, 0, sizeof(buf_data)); if (!ctx || !session) { eorig = TEEC_ORIGIN_API; res = TEEC_ERROR_BAD_PARAMETERS; goto out; } buf_data.buf_ptr = (uintptr_t)&buf; buf_data.buf_len = sizeof(buf); arg = &buf.arg; arg->num_params = TEEC_CONFIG_PAYLOAD_REF_COUNT; params = (struct tee_ioctl_param *)(arg + 1); uuid_to_octets(arg->uuid, destination); setup_client_data(arg, connection_method, connection_data); res = teec_pre_process_operation(ctx, operation, params, shm); if (res != TEEC_SUCCESS) { eorig = TEEC_ORIGIN_API; goto out_free_temp_refs; } rc = ioctl(ctx->imp.fd, TEE_IOC_OPEN_SESSION, &buf_data); if (rc) { EMSG("TEE_IOC_OPEN_SESSION failed"); eorig = TEEC_ORIGIN_COMMS; res = ioctl_errno_to_res(errno); goto out_free_temp_refs; } res = arg->ret; eorig = arg->ret_origin; if (res == TEEC_SUCCESS) { session->imp.ctx = ctx; session->imp.session_id = arg->session; } teec_post_process_operation(operation, params, shm); out_free_temp_refs: teec_free_temp_refs(operation, shm); out: if (ret_origin) *ret_origin = eorig; return res; }三、TEEC_InvokeCommand:核心业务指令的下发与数据交互
这是日常业务中最频繁调用的 API。由于它专注于执行指令,不需要像OpenSession那样搬运 UUID,所以它的逻辑更轻量,直奔TEE_IOC_INVOKE。C
TEEC_Result TEEC_InvokeCommand(TEEC_Session *session, uint32_t cmd_id, TEEC_Operation *operation, uint32_t *error_origin) { struct tee_ioctl_invoke_arg *arg = NULL; struct tee_ioctl_param *params = NULL; TEEC_Result res = TEEC_ERROR_GENERIC; uint32_t eorig = 0; int rc = 0; /* 1. 装填 Session ID 与要执行的命令号 */ const size_t arg_size = sizeof(struct tee_ioctl_invoke_arg) + TEEC_CONFIG_PAYLOAD_REF_COUNT * sizeof(struct tee_ioctl_param); union { struct tee_ioctl_invoke_arg arg; uint8_t data[arg_size]; } buf; struct tee_ioctl_buf_data buf_data; TEEC_SharedMemory shm[TEEC_CONFIG_PAYLOAD_REF_COUNT]; memset(&buf, 0, sizeof(buf)); memset(&buf_data, 0, sizeof(buf_data)); memset(&shm, 0, sizeof(shm)); if (!session) { eorig = TEEC_ORIGIN_API; res = TEEC_ERROR_BAD_PARAMETERS; goto out; } buf_data.buf_ptr = (uintptr_t)&buf; buf_data.buf_len = sizeof(buf); arg = &buf.arg; arg->num_params = TEEC_CONFIG_PAYLOAD_REF_COUNT; params = (struct tee_ioctl_param *)(arg + 1); arg->session = session->imp.session_id; arg->func = cmd_id; if (operation) { teec_mutex_lock(&teec_mutex); operation->imp.session = session; teec_mutex_unlock(&teec_mutex); } /* 2. 序列化参数 (TEEC_Operation -> tee_ioctl_param) */ res = teec_pre_process_operation(session->imp.ctx, operation, params, shm); if (res != TEEC_SUCCESS) { eorig = TEEC_ORIGIN_API; goto out_free_temp_refs; } /* 3. 发送关键 ioctl 陷落至内核,阻塞等待 Secure 世界(TA)处理完毕返回 */ rc = ioctl(session->imp.ctx->imp.fd, TEE_IOC_INVOKE, &buf_data); if (rc) { EMSG("TEE_IOC_INVOKE failed"); eorig = TEEC_ORIGIN_COMMS; res = ioctl_errno_to_res(errno); goto out_free_temp_refs; } res = arg->ret; eorig = arg->ret_origin; teec_post_process_operation(operation, params, shm); /* 4. 反序列化与收尾:将 TA 修改后的 Value 或 Memref 刷新回用户态 CA 内存空间 */ out_free_temp_refs: teec_free_temp_refs(operation, shm); out: if (error_origin) *error_origin = eorig; return res; }四、 核心桥梁:参数的预处理与内存共享机制
深入细读libteec,最复杂的逻辑其实隐藏在teec_pre_process_operation内部。GP 标准规定每次调用最多传递 4 个参数(TEEC_Parameter params[4]),类型可以是VALUE(纯数值)或MEMREF(内存引用)。
static TEEC_Result teec_pre_process_operation(TEEC_Context *ctx, TEEC_Operation *operation, struct tee_ioctl_param *params, TEEC_SharedMemory *shms) { TEEC_Result res = TEEC_ERROR_GENERIC; size_t n = 0; memset(shms, 0, sizeof(TEEC_SharedMemory) * TEEC_CONFIG_PAYLOAD_REF_COUNT); for (n = 0; n < TEEC_CONFIG_PAYLOAD_REF_COUNT; n++) shms[n].imp.id = -1; if (!operation) { memset(params, 0, sizeof(struct tee_ioctl_param) * TEEC_CONFIG_PAYLOAD_REF_COUNT); return TEEC_SUCCESS; } for (n = 0; n < TEEC_CONFIG_PAYLOAD_REF_COUNT; n++) { uint32_t param_type = 0; param_type = TEEC_PARAM_TYPE_GET(operation->paramTypes, n); switch (param_type) { case TEEC_NONE: params[n].attr = param_type; break; case TEEC_VALUE_INPUT: case TEEC_VALUE_OUTPUT: case TEEC_VALUE_INOUT: params[n].attr = param_type; params[n].a = operation->params[n].value.a; params[n].b = operation->params[n].value.b; break; case TEEC_MEMREF_TEMP_INPUT: case TEEC_MEMREF_TEMP_OUTPUT: case TEEC_MEMREF_TEMP_INOUT: res = teec_pre_process_tmpref(ctx, param_type, &operation->params[n].tmpref, params + n, shms + n); if (res != TEEC_SUCCESS) return res; break; case TEEC_MEMREF_WHOLE: res = teec_pre_process_whole( &operation->params[n].memref, params + n); if (res != TEEC_SUCCESS) return res; break; case TEEC_MEMREF_PARTIAL_INPUT: case TEEC_MEMREF_PARTIAL_OUTPUT: case TEEC_MEMREF_PARTIAL_INOUT: res = teec_pre_process_partial(param_type, &operation->params[n].memref, params + n); if (res != TEEC_SUCCESS) return res; break; default: return TEEC_ERROR_BAD_PARAMETERS; } } return TEEC_SUCCESS; }在libteec中,它们会映射到内核驱动的struct tee_ioctl_param,底层转换逻辑如下:
1. 值的映射 (TEEC_VALUE_INPUT / OUTPUT / INOUT)
最直观,不涉及复杂的内存映射,直接做拷贝。
- params[n].a = operation->params[n].value.a;
- params[n].b = operation->params[n].value.b;
2. 内存引用的映射 (TEEC_MEMREF_TEMP_*/TEEC_REGISTERED_MEM_*)
这是性能与安全的焦点。如果 CA 传入的是一块普通的 Buffer 内存(TEEC_MEMREF_TEMP_INPUT),由于非安全世界用户态的虚拟地址(VA)安全世界根本无法直接访问,libteec会执行以下高开销操作:
- 申请共享内存:通过ioctl(fd, TEE_IOC_SHM_ALLOC, &shm_alloc_arg)向内核 TEE 驱动申请一段专用的、连通非安全与安全世界的物理连续/非连续共享内存(Shared Memory)。
- 内容拷贝:如果涉及输入(Input),使用memcpy将用户态临时 Buffer 的内容拷贝到这段共享内存中。
- 记录标识:将分配到的shm_num(共享内存 ID)和offset填入tee_ioctl_param.c和tee_ioctl_param.a中。
- 回写与销毁:在teec_post_process_operation中,如果是 Output,再把数据从共享内存拷回给 CA 的临时 Buffer,并调用TEE_IOC_SHM_FREE释放共享内存。
