Click the blue "Arm Selected" in the top left corner and select "Set as Star"
Environment: Linux kernel 4.4, (SCR.IRQ=0, SCR.FIQ=1) OP-TEE 3.6 (SCR.IRQ=0, SCR.FIQ=0) ARMV8GICV3
When the CPU is in the secure side and a non-secure interrupt arrives, depending on SCR.NS=0 / the interrupt in the non-secure group 1 group, the CPU interface will give the CPU a FIQ. (Since SCR.FIQ=0, the FIQ will be routed to EL1), jumping to the OP-TEE FIQ interrupt exception vector table. In the OP-TEE FIQ handler, an SMC call is made to jump to ATF, and ATF then switches to normal EL1 (Linux). At this point, the state of SCR.NS changes, and according to SCR.NS=1 / the interrupt in the non-secure group 1 group, the CPU interface will send another IRQ exception to the CPU. The CPU jumps to the Linux IRQ interrupt exception vector table. After processing, it returns sequentially to ATF – back to OP-TEE
We will break down the above steps from the code: Before the CPU enters TEE, the CPU enters TEE through functions like optee_open_session(), optee_close_session(), optee_invoke_func(), etc., and these functions all call optee_do_call_with_arg(). In this function, SMC is called again.
The prototype of the optee_do_call_with_arg() function is as follows: (Note the Chinese comments)
u32 optee_do_call_with_arg(struct tee_context *ctx, phys_addr_t parg) { struct optee *optee = tee_get_drvdata(ctx->teedev); struct optee_call_waiter w; struct optee_rpc_param param = { }; struct optee_call_ctx call_ctx = { }; u32 ret; param.a0 = OPTEE_SMC_CALL_WITH_ARG; reg_pair_from_64(¶m.a1, ¶m.a2, parg); /* Initialize waiter */ optee_cq_wait_init(&optee->call_queue, &w); while (true) { struct arm_smccc_res res; // Note, here SMC is called, -->ATF-->TEE optee->invoke_fn(param.a0, param.a1, param.a2, param.a3, param.a4, param.a5, param.a6, param.a7, &res); // After returning from TEE, execute here (whether TEE returns normally, RPC returns, or comes from an interrupt) // If the CPU is switched from TEE due to a REE interrupt, then the IRQ has been triggered here, and after the IRQ is executed, the program continues from here down // Note: The CPU coming from TEE due to a REE interrupt is also considered an RPC return if (res.a0 == OPTEE_SMC_RETURN_ETHREAD_LIMIT) { /* Out of threads in secure world, wait for a thread become available. */ optee_cq_wait_for_completion(&optee->call_queue, &w); } else if (OPTEE_SMC_RETURN_IS_RPC(res.a0)) { // If it is RPC, interrupt returns here, then look at the optee_handle_rpc() function prototype param.a0 = res.a0; param.a1 = res.a1; param.a2 = res.a2; param.a3 = res.a3; optee_handle_rpc(ctx, ¶m, &call_ctx); // If it comes from an interrupt, the optee_handle_rpc() function does nothing, the program continues executing the above while(true), then will call optee->invoke_fn again, and the CPU will switch back to TEE again } else { // If it is a normal return, go here, exit optee_do_call_with_arg() ret = res.a0; break; } } optee_rpc_finalize_call(&call_ctx); /* We're done with our thread in secure world, if there's any thread waiters wake up one. */ optee_cq_wait_final(&optee->call_queue, &w); return ret; }
Then we look at steps 1 and 2: In OP-TEE, generate FIQ, jump to the FIQ interrupt vector table, then call SMC to switch to ATF
LOCAL_FUNC elx_irq , : #if defined(CFG_ARM_GICV3) native_intr_handler irq #else foreign_intr_handler irq #endif END_FUNC elx_irq LOCAL_FUNC elx_fiq , : #if defined(CFG_ARM_GICV3) foreign_intr_handler fiq // In OP-TEE runtime, a REE interrupt arrives, triggering FIQ, the program will call here #else native_intr_handler fiq #endif END_FUNC elx_fiq
Then look at the specific implementation of foreign_intr_handler: it actually saves some registers and stack registers of the current process, restores the interrupt mode registers and tmp_stack stack, and then calls SMC to switch to ATF, where smdid = TEESMC_OPTEED_RETURN_CALL_DONE
/* The handler of foreign interrupt. */ .macro foreign_intr_handler mode:req /* Update core local flags */ ldr w1, [sp, #THREAD_CORE_LOCAL_FLAGS] lsl w1, w1, #THREAD_CLF_SAVED_SHIFT orr w1, w1, #THREAD_CLF_TMP .ifc \\mode\,fiq orr w1, w1, #THREAD_CLF_FIQ .else orr w1, w1, #THREAD_CLF_IRQ .endif str w1, [sp, #THREAD_CORE_LOCAL_FLAGS] /* get pointer to current thread context in x0 */ get_thread_ctx sp, 0, 1, 2 /* Keep original SP_EL0 */ mrs x2, sp_el0 /* Store original sp_el0 */ str x2, [x0, #THREAD_CTX_REGS_SP] /* store x4..x30 */ store_xregs x0, THREAD_CTX_REGS_X4, 4, 30 /* Load original x0..x3 into x10..x13 */ load_xregs sp, THREAD_CORE_LOCAL_X0, 10, 13 /* Save original x0..x3 */ store_xregs x0, THREAD_CTX_REGS_X0, 10, 13 /* load tmp_stack_va_end */ ldr x1, [sp, #THREAD_CORE_LOCAL_TMP_STACK_VA_END] /* Switch to SP_EL0 */ msr spsel, #0 mov sp, x1 /* Mark current thread as suspended */ mov w0, #THREAD_FLAGS_EXIT_ON_FOREIGN_INTR mrs x1, spsr_el1 mrs x2, elr_el1 bl thread_state_suspend mov w4, w0 /* Supply thread index */ /* Update core local flags */ /* Switch to SP_EL1 */ msr spsel, #1 ldr w0, [sp, #THREAD_CORE_LOCAL_FLAGS] lsr w0, w0, #THREAD_CLF_SAVED_SHIFT str w0, [sp, #THREAD_CORE_LOCAL_FLAGS] msr spsel, #0 /* Note that we're exiting with SP_EL0 selected since the entry functions expects to have SP_EL0 selected with the tmp stack set. */ ldr w0, =TEESMC_OPTEED_RETURN_CALL_DONE ldr w1, =OPTEE_SMC_RETURN_RPC_FOREIGN_INTR mov w2, #0 mov w3, #0 /* w4 is already filled in above */ smc #0 b . /* SMC should not return */ .endm
Then we reach step 3: Look at the ATF’s opteed_smc_handler() function, we directly look at case TEESMC_OPTEED_RETURN_CALL_DONE. In OP-TEE, when FIQ is triggered, foreign_intr_handler calls SMC, and upon entering ATF, it goes here. Here, it will restore the registers of the Linux system, fill ELR_EL3 with the PC pointer value of the Linux side, and after SMC_RET4, the CPU will switch back to Linux
case TEESMC_OPTEED_RETURN_CALL_DONE: /* This is the result from the secure client of an earlier request. The results are in x0-x3. Copy it into the non-secure context, save the secure state and return to the non-secure state. */ assert(handle == cm_get_context(SECURE)); cm_el1_sysregs_context_save(SECURE); /* Get a reference to the non-secure context */ ns_cpu_context = cm_get_context(NON_SECURE); assert(ns_cpu_context); /* Restore non-secure state */ cm_el1_sysregs_context_restore(NON_SECURE); cm_set_next_eret_context(NON_SECURE); SMC_RET4(ns_cpu_context, x1, x2, x3, x4);
Next, steps 4 and 5: The corresponding code for this part is the optee_do_call_with_arg() that was posted at the beginning of this article. After the program returns to this function, due to the change in the state of SCR.NS, the CPU interface will send another IRQ to the ARM Core, and the CPU will immediately enter the Linux kernel’s IRQ interrupt vector table. After the interrupt handler finishes executing, the PC points here again, and then the following logic follows
else if (OPTEE_SMC_RETURN_IS_RPC(res.a0)) { // If it is RPC, interrupt returns here, then look at the optee_handle_rpc() function prototype param.a0 = res.a0; param.a1 = res.a1; param.a2 = res.a2; param.a3 = res.a3; optee_handle_rpc(ctx, ¶m, &call_ctx); // If it comes from an interrupt, the optee_handle_rpc() function does nothing, the program continues executing the above while(true), and will call optee->invoke_fn again, and the CPU will switch back to TEE }
In the OPTEE_SMC_RPC_FUNC_FOREIGN_INTR business logic in optee_handle_rpc(), actually nothing is done, it just returns. After the sub-function returns, the while loop in optee_do_call_with_arg() continues to execute, and optee->invoke_fn() again switches the CPU to ATF.
void optee_handle_rpc(struct tee_context *ctx, struct optee_rpc_param *param, struct optee_call_ctx *call_ctx) { struct tee_device *teedev = ctx->teedev; struct optee *optee = tee_get_drvdata(teedev); struct tee_shm *shm; phys_addr_t pa; switch (OPTEE_SMC_RETURN_GET_RPC_FUNC(param->a0)) { case OPTEE_SMC_RPC_FUNC_ALLOC: shm = tee_shm_alloc(ctx, param->a1, TEE_SHM_MAPPED); if (!IS_ERR(shm) && !tee_shm_get_pa(shm, 0, &pa)) { reg_pair_from_64(¶m->a1, ¶m->a2, pa); reg_pair_from_64(¶m->a4, ¶m->a5, (unsigned long)shm); } else { param->a1 = 0; param->a2 = 0; param->a4 = 0; param->a5 = 0; } break; case OPTEE_SMC_RPC_FUNC_FREE: shm = reg_pair_to_ptr(param->a1, param->a2); tee_shm_free(shm); break; case OPTEE_SMC_RPC_FUNC_FOREIGN_INTR: //---See the following English comment, if it comes from an interrupt, do nothing /* A foreign interrupt was raised while secure world was executing, since they are handled in Linux a dummy RPC is performed to let Linux take the interrupt through the normal vector. */ break; case OPTEE_SMC_RPC_FUNC_CMD: shm = reg_pair_to_ptr(param->a1, param->a2); handle_rpc_func_cmd(ctx, optee, shm, call_ctx); break; default: pr_warn("Unknown RPC func 0x%x\n", (u32)OPTEE_SMC_RETURN_GET_RPC_FUNC(param->a0)); break; } param->a0 = OPTEE_SMC_CALL_RETURN_FROM_RPC; }
Next, we return to ATF, entering the ATF’s opteed_smc_handler() function, and then assigning optee_vectors->fast_smc_entry to ELR_EL3, then ERET exits ATF, jumping to the fast_smc_entry in the OP-TEE thread vector table
if (is_caller_non_secure(flags)) { /* This is a fresh request from the non-secure client. The parameters are in x1 and x2. Figure out which registers need to be preserved, save the non-secure state and send the request to the secure payload. */ assert(handle == cm_get_context(NON_SECURE)); cm_el1_sysregs_context_save(NON_SECURE); /* We are done stashing the non-secure context. Ask the OPTEE to do the work now. */ /* Verify if there is a valid context to use, copy the operation type and parameters to the secure context and jump to the fast smc entry point in the secure payload. Entry into S-EL1 will take place upon exit from this function. */ assert(&optee_ctx->cpu_ctx == cm_get_context(SECURE)); /* Set appropriate entry for SMC. * We expect OPTEE to manage the PSTATE.I and PSTATE.F flags as appropriate. */ if (GET_SMC_TYPE(smc_fid) == SMC_TYPE_FAST) { cm_set_elr_el3(SECURE, (uint64_t)&optee_vectors->fast_smc_entry); // When Linux processes the interrupt and returns to TEE, it goes here, assigning the fast_smc_entry address to ELR_EL3. The fast_smc_entry address is the address of the fast_smc_entry function in OP-TEE, which is passed during OP-TEE initialization and saved in a global variable. }
Finally, in the fast_smc_entry vector of the OP-TEE thread vector table, it will restore the registers and PC value of the OP-TEE process before this, thus completing the entire interrupt handling process.




[Store Address]

[Customer Service Consultation]

