optee学习(2) CA&TA调用流程分析

环境

  • ubuntu22.04
  • ADS + optee-fvp

调用流程梳理

这里直接从optee-examples中最简单的hello world入手来看的,从宏观上来看整个调用流程是 :

1
CA --> optee client --> tee driver --> ATF -->  TEE --> TA

根据个人的理解画了个省流版本的图,省略了部分调用

optee_ca_ta

CA & TA 的工作流程

  • CA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1. 初始化context用于和TEE交互
res = TEEC_InitializeContext(NULL, &ctx);

//2. 打开“会话”,此时TEE侧会验证并且加载对应的TA
res = TEEC_OpenSession(&ctx, &sess, &uuid,
TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);

//3. 交互,通过invoke command来触发,调用到TA里具体的逻辑
res = TEEC_InvokeCommand(&sess, TA_HELLO_WORLD_CMD_INC_VALUE, &op,
&err_origin);

//4. 使用完毕,关闭“会话”
TEEC_CloseSession(&sess);

// 5. 释放context对象
TEEC_FinalizeContext(&ctx);
  • TA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. 执行的入口,会话的另一端
TA_CreateEntryPoint // TA加载的时候执行
TA_OpenSessionEntryPoint


2. 交互,业务代码
TEE_Result TA_InvokeCommandEntryPoint(void __maybe_unused *sess_ctx,
uint32_t cmd_id,
uint32_t param_types, TEE_Param params[4])
{
(void)&sess_ctx; /* Unused parameter */

switch (cmd_id) {
case TA_HELLO_WORLD_CMD_INC_VALUE:
return inc_value(param_types, params);
case TA_HELLO_WORLD_CMD_DEC_VALUE:
return dec_value(param_types, params);
default:
return TEE_ERROR_BAD_PARAMETERS;
}
}


3. 交互完毕,关闭会话
TA_CloseSessionEntryPoint
TA_DestroyEntryPoint
  • CA 和 TA的对应关系
1
2
3
4
5
6
7
8
9
TEEC_OpenSession    ->   TA_CreateEntryPoint
TA_OpenSessionEntryPoint

TEEC_InvokeCommand -> TA_InvokeCommandEntryPoint


TEEC_CloseSession -> TA_CloseSessionEntryPoint
TA_DestroyEntryPoint

源码阅读

TEEC_InitializeContext

TEEC_InitializeContext → 打开tee driver,要用于通信了 ,主要是一些初始化的工作

1
2
3
TEEC_InitializeContext
teec_open_dev
ioctl(fd, TEE_IOC_VERSION, &vers)

注意此时的CMD是 TEE_IOC_VERSION,对应执行的是 tee_ioctl_version

TEEC_OpenSession

1
2
3
4
5
6
7
8
9
10
11
// context
// tee session
// TA的uuid,唯一
// connection method
// connection data
// operation
// ret
TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);
....

rc = ioctl(ctx->fd, TEE_IOC_OPEN_SESSION, &buf_data);

此时CMD是 TEE_IOC_OPEN_SESSION,到tee driver中查看对应的处理逻辑 :

image-20221112203936455

往后会调用到对应的handler:

1
2
rc = ctx->teedev->desc->ops->open_session(ctx, &arg, params);

在进TEE之前,传递的参数需要做转换,反过来也是;从REE往TEE走,其实是一个入口 do_call_with_arg,这些operations都定义在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* struct optee_ops - OP-TEE driver internal operations
* @do_call_with_arg: enters OP-TEE in secure world
* @to_msg_param: converts from struct tee_param to OPTEE_MSG parameters
* @from_msg_param: converts from OPTEE_MSG parameters to struct tee_param
*
* These OPs are only supposed to be used internally in the OP-TEE driver
* as a way of abstracting the different methogs of entering OP-TEE in
* secure world.
*/
struct optee_ops {
int (*do_call_with_arg)(struct tee_context *ctx,
struct tee_shm *shm_arg, u_int offs);
int (*to_msg_param)(struct optee *optee,
struct optee_msg_param *msg_params,
size_t num_params, const struct tee_param *params);
int (*from_msg_param)(struct optee *optee, struct tee_param *params,
size_t num_params,
const struct optee_msg_param *msg_params);
};

直接在目录中搜open_session

image-20221112204128044

发现有两个实现,这里的话ffa_abi.c中的应该是FF-A标准对应的那个实现,这里直接看smc的那个就行, 即linux/drivers/tee/optee/smc_abi.c 里 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
910      while (true) {
911 struct arm_smccc_res res;
912
913 trace_optee_invoke_fn_begin(&param);
914 optee->smc.invoke_fn(param.a0, param.a1, param.a2, param.a3,
915 param.a4, param.a5, param.a6, param.a7,
916 &res);
917 trace_optee_invoke_fn_end(&param, &res);
918
919 if (res.a0 == OPTEE_SMC_RETURN_ETHREAD_LIMIT) {
920 /*
921 * Out of threads in secure world, wait for a thread
922 * become available.
923 */
924 optee_cq_wait_for_completion(&optee->call_queue, &w);
925 } else if (OPTEE_SMC_RETURN_IS_RPC(res.a0)) {
926 cond_resched();
927 param.a0 = res.a0;
928 param.a1 = res.a1;
929 param.a2 = res.a2;
930 param.a3 = res.a3;
931 optee_handle_rpc(ctx, rpc_arg, &param, &call_ctx);
932 } else {
933 rc = res.a0;
934 break;
935 }
936 }

中间这个 smc.invoke_fn就是通过smc进入到ATF,然后ATF会转发到TEE处理

对于ATF来说,这是一个通过 SMC #0 过来的中断,这是core内部发生的,且异常等级发生了变化,所以应该是到了ATF的第三组向量表的sync中断处理程序处

image-20221112204311537

这里细节就不深入看了,主要是为了梳理工作流程,ATF里会调用到系统启动的时候注册的optee的tspd来处理,(opteed_smc_handler 函数)

image-20221112204331216

这个handler里会保存 non-secure的上下文,恢复secure的上下文,然后直接eret到TEE侧。

进入optee之后来到:

image-20221112204354659

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
53  uint32_t thread_handle_std_smc(uint32_t a0, uint32_t a1, uint32_t a2,
54 uint32_t a3, uint32_t a4, uint32_t a5,
55 uint32_t a6 __unused, uint32_t a7 __maybe_unused)
56 {
....

69 if (a0 == OPTEE_SMC_CALL_RETURN_FROM_RPC) {
70 thread_resume_from_rpc(a3, a1, a2, a4, a5);
71 rv = OPTEE_SMC_RETURN_ERESUME;
72 } else {
73 thread_alloc_and_run(a0, a1, a2, a3, 0, 0);
74 rv = OPTEE_SMC_RETURN_ETHREAD_LIMIT;
75 }

...

第一次走到 thread_alloc_and_run,传入参数是 thread_std_smc_entry, 所以会执行到 thread_std_smc_entry

image-20221112204452280

后续的流程 :

1
2
3
4
5
6
__thread_std_smc_entry
std_smc_entry(a0, a1, a2, a3);
std_entry_with_parg(...)
call_entry_std
tee_entry_std
__tee_entry_std

至此,到了关键的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
538  TEE_Result __tee_entry_std(struct optee_msg_arg *arg, uint32_t num_params)
539 {
540 TEE_Result res = TEE_SUCCESS;
541
542 /* Enable foreign interrupts for STD calls */
543 thread_set_foreign_intr(true);
544 switch (arg->cmd) {
545 case OPTEE_MSG_CMD_OPEN_SESSION:
546 entry_open_session(arg, num_params);
547 break;
548 case OPTEE_MSG_CMD_CLOSE_SESSION:
549 entry_close_session(arg, num_params);
550 break;
551 case OPTEE_MSG_CMD_INVOKE_COMMAND:
552 entry_invoke_command(arg, num_params);
553 break;
554 case OPTEE_MSG_CMD_CANCEL:
555 entry_cancel(arg, num_params);
556 break;
557 #ifndef CFG_CORE_FFA
558 #ifdef CFG_CORE_DYN_SHM
559 case OPTEE_MSG_CMD_REGISTER_SHM:
560 register_shm(arg, num_params);
561 break;
562 case OPTEE_MSG_CMD_UNREGISTER_SHM:
563 unregister_shm(arg, num_params);
564 break;
565 #endif
566 #endif
567
568 case OPTEE_MSG_CMD_DO_BOTTOM_HALF:
569 if (IS_ENABLED(CFG_CORE_ASYNC_NOTIF))
570 notif_deliver_event(NOTIF_EVENT_DO_BOTTOM_HALF);
571 else
572 goto err;
573 break;
574 case OPTEE_MSG_CMD_STOP_ASYNC_NOTIF:
575 if (IS_ENABLED(CFG_CORE_ASYNC_NOTIF))
576 notif_deliver_event(NOTIF_EVENT_STOPPED);
577 else
578 goto err;
579 break;
580
581 default:
582 err:
583 EMSG("Unknown cmd 0x%x", arg->cmd);
584 res = TEE_ERROR_NOT_IMPLEMENTED;
585 }
586
587 return res;
588 }

这次的cmd是 open session所以走 entry_open_session函数

1
2
3
4
373     res = tee_ta_open_session(&err_orig, &s, &tee_open_sessions, &uuid,
374 &clnt_id, TEE_TIMEOUT_INFINITE, &param);

// uuid,需要根据uuid来加载TA了

然后去加载对应的TA,在 tee_ta_open_session // tee_ta_manager.c

1
715     res = tee_ta_init_session(err, open_sessions, uuid, &s);

image-20221112204603153

加载完毕之后,如果成功加载了,那就调用 ts_ctx->ops->enter_open_session(&s->ts_sess);

image-20221112204739124

根据注册信息,应该是 user_ta_enter_open_session

image-20221112204803734

调用到 user_ta_enter 函数,此时还是在optee里的,需要跳到TA去执行

1
2
3
4
5
166      res = thread_enter_user_mode(func, kaddr_to_uref(session),
167 (vaddr_t)usr_params, cmd, usr_stack,
168 utc->uctx.entry_func, utc->uctx.is_32bit,
169 &utc->ta_ctx.panicked,
170 &utc->ta_ctx.panic_code);

S-EL1 → S-EL0,应该是eret过去的

1
2
3
__thread_enter_user_mode(regs, exit_status0, exit_status1);
b eret_to_el0
eret

跳转前设置好了上下文,所以eret后就回到了TA中执行,这就到了TA中的 TA_OpenSessionEntryPoint

TEEC_InvokeCommand

逻辑基本和上面OpenSession差不多,差别就在于传递的 InvokeCommand

所以最后是走到

1
2
user_ta_enter_invoke_cmd
user_ta_enter(s, UTEE_ENTRY_FUNC_INVOKE_COMMAND, cmd);

然后调用到TA的 TEEC_InvokeCommand 函数

TEEC_CloseSession

1
2
3
4
5
6
7
8
9
10
11
12
13
void TEEC_CloseSession(TEEC_Session *session)
{
struct tee_ioctl_close_session_arg arg;

memset(&arg, 0, sizeof(arg));

if (!session)
return;

arg.session = session->session_id;
if (ioctl(session->ctx->fd, TEE_IOC_CLOSE_SESSION, &arg))
EMSG("Failed to close session 0x%x", session->session_id);
}

也是类似的情况,调用到内核里tee_ioctl_close_session ,区别只是cmd不同,最后会一路到TA侧的 TA_CloseSessionEntryPoint

TEEC_FinalizeContext

关闭打开的驱动

1
2
3
4
5
void TEEC_FinalizeContext(TEEC_Context *ctx)
{
if (ctx)
close(ctx->fd);
}

调试

根据上面的流程梳理,只要在optee 往TA里跳的时候下个断,就能去分析TA了,然后再加载TA的符号就能快乐地debug了,没有源码那就纯黑盒调试TA了

结合optee的文档 里的描述,会用到TA的 .text段 LMA信息

1
2
$ objdump -h 8aaaf200-2450-11e4-abe2-0002a5d5c51b.elf | grep ".text"
1 .text 00012e5c 00000020 00000020 00001020 2**2

启动ADS,然后在加载tee的时候断住,加载tee的符号,参考我上一篇博客就行了。

如果想调试全部的过程,按照文章把 Linux kernel、 bl31 runtime 的符号也加载进来就行了

1
b user_ta_enter_open_session

然后执行CA,可以观察到已经断下来了

image-20221112205051157

其实这个时候TEE侧log已经看到了TA被加载到了哪里了,直接下断也可以的

1
b *EL0S:0x40060020

但是没断下来且报错了,很奇怪的是eret之后 还是显示SEL1,我查看了currentel寄存器之后发现确实是在EL0的

image-20221112205212578

问了下组里的大佬,这个反汇编窗口显示的ELxS/N 应该是这块内存的属性,而不是当前执行状态 (之前直接靠这个tag来做判断,看来是错的离谱了)

image-20221112205228983

个人猜测 因为TA加载是optee做的,所以可能optee分配出来的内存就是EL1S,所以跑到TA的时候,反汇编窗口地址tag会显示EL1S

然后尝试加载符号就行了:

1
add-symbol-file /home/muhe/Study/optee-fvp/out-br/build/optee_examples_ext-1.0/hello_world/ta/out/8aaaf200-2450-11e4-abe2-0002a5d5c51b.elf 0x40060020

image-20221112205413157

参考

https://blog.csdn.net/weixin_42135087/article/details/119384252

https://www.timesys.com/security/trusted-software-development-op-tee/

https://optee.readthedocs.io/en/latest/building/gits/optee_examples/optee_examples.html