BL31 位于 EL3 安全世界, 提供运行时服务,并不像 BL1 和 BL2 一样 boot 后就释放空间,其他的子系统通过 SMC 调用向 BL31 发送请求,包括安全世界的切换。
BL31 不是必须得,如果实际没有实现 EL3,或者不需要 Secure Monitor,则可以不用 BL31.
GIC 初始化
默认配置 GIC,其中 GIC 有一些配置在 secure state 才能配置,所以在这里配一下。
BL31 中,每个 platform 都有一个分配中断分组的全局变量qemu_interrupt_props
(以 QEMU 为例),在 BL31 中可以将一些中断划分为 G1S 或 G0。当然,我猜测 BL32 也可以重设,毕竟 BL32 也要设定一些特殊中断,比如说安全 uart。
SMC Calling Convention (SMCCC)
在非安全状态和低于EL3运行的软件要请求EL3的服务,使用smc调用。这些请求需要遵循的是SMCCC,它为每个SMC请求分配函数标识符,并描述了如何传递和返回参数。
EHF
配置完 GIC 后,紧接着就是可选的 EHF 功能。
GICv3将中断分为三种类型:Group0,secure Group1,non-secure Group1。EHF 就是支持Group0中断的,Group0总是以FIQ的形式触发。开启EHF的情况下需要通过SCR_EL3的配置来使得FIQ路由到EL3。在应用中,我们会将一些需要陷入EL3的中断配置成Group0,同时开启ATF,就能完成这种场景所需的功能。
关于中断,我们的目标肯定是配置某些中断在某个服务中处理,可能是Linux内核、Hypervisor、TEEOS、EL3。这个功能的输入是每个中断,也就是INTID,目的就是让每个中断路由到特定的运行时。在GICv3中,实现这个过程需要两步:
- 配置INTID到某个中断分组,Group0、Secure Group1,Non-secure Group1
- 每个中断分组运行在特定的安全状态下触发的中断类型是确定的,也是不用的。参考这个表格。所以说,需要通过SCR_ELx寄存器来设置FIQ和IRQ的路由到哪个EL。
因为设置中断路由目标的单位是【中断组】,所以需要做以上两种设置。
默认 ATF 中关中断,这个我们可以想想,EL31 负责安全世界的切换,有点像处于异常处理上下文中,也没有什么中断需要触发,所以关中断也是合理的。
EHF 通过编译选项 EL3_EXCEPTION_HANDLING
控制开关,EHF 的功能就是让 EL3 能处理其他中断(除了原本的 SMC),原本的情况是直接返回,让上面的 EL 处理。开启 EHF 后,其他的应用可以注册一些中断处理函数到 EL3,并设置 SCR_EL3.IRQ/FIQ
,置 1 之后,IRQ/FIQ 会被路由到 EL3,调用之前注册好的 handler。
EHF 的全称是 Exception Handling Framework,既然是 Framework,一个框架,就是提供注册的接口。
其他的子模块填充一个 intr_type_descs
成员,它是一个数组
- 下标 type 代表想要运行在哪个状态的中断 Trap 到 EL3?是 SEL1?EL3?还是 Non-secure
- 我们知道,每种 type 下,都有三种类型的中断(G0、G1S、G1NS),有时我们只想让 EL3 handle 其一,或者都。
- EHF 提供的粒度是 IRQ 和 FIQ。知道当前运行的 type,和你想要 trap EL3 的中断分组,就能唯一得到中断是 IRQ/FIQ。
- 所以你可以单独设计这个 IRQ/FIQ 是否 Trap,来达到你想要只 trap 一种分组的目的。当然这不是一一对应的,IRQ 可能对应多个中断分组,这是没办法的事情。只能人为做更加严格的中断分组。
- 配置完成后,框架会将 IRQ 和 FIQ 置位的情况(成员
scr_el3[2]
) 汇总,存到上下文中,返回时到该 type 时恢复。
typedef struct intr_type_desc {
interrupt_type_handler_t handler;
u_register_t scr_el3[2]; // 2 => 两种安全状态下的scr_el3
uint32_t flags; // bit[0]: routing model for interrupt from not EL3 but secure state
// bit[1]: routing modul for interrupt from not EL3 but non-secure state
// '1' => this interrupt will be routed to EL3
// '0' => this interrupt will be routed to current EL
} intr_type_desc_t;
#define INTR_TYPE_S_EL1 U(0)
#define INTR_TYPE_EL3 U(1)
#define INTR_TYPE_NS U(2)
#define MAX_INTR_TYPES U(3)
#define INTR_TYPE_INVAL MAX_INTR_TYPES
static intr_type_desc_t intr_type_descs[MAX_INTR_TYPES];
ATF 中中断的注册(这三种类型的 handler 程序的注册),以 INTR_TYPE_S_EL1 为例:
在开机 bl32_main 调用 opteed_setup()时,将 opteed_sel1_interrupt_handler()函数注册成了 INTR_TYPE_S_EL1 类型中断,同时也会将 REE(Linux)使用的 SCR_EL3.FIQ 配置成 1,也意味着当 CPU 运行在 REE 时,来了一个 secure group1 的中断,此中断在 REE 中被标记 FIQ 后将被 target 到 EL3,进入 EL3(ATF)的中断处理函数,也就是刚才注册的 opteed_sel1_interrupt_handler()函数,在该函数中,会将 cpu 切换到 TEE 中,去处理这个中断。这就是 REE 切换到 TEE 的一种方式。
由此我们再看 EHF 的实现原理。
/*
The ``flags`` field stores the routing model for the interrupt type in
bits[1:0]. Bit[0] stores the routing model when execution is in the secure
state. Bit[1] stores the routing model when execution is in the non-secure
state. As mentioned in Section `Routing model`_, a value of ``0`` implies that
the interrupt should be targeted to the FEL. A value of ``1`` implies that it
should be targeted to EL3. The remaining bits are reserved and SBZ. The helper
macro ``set_interrupt_rm_flag()`` should be used to set the bits in the
``flags`` parameter.
*/
// EL3 不是要处理中断吗,当SCR_EL3.IRQ/FIQ打开时,会进入EL3的异常处理函数,
// ehf_init()做的就是初始化好这个框架,使得EL3在接收到FIQ、IRQ后不会直接返回,
// 为其注册了一个大的 handler,在里面根据情况调用其他type的处理函数。
ehf_init()
=> set_interrupt_rm_flag(flags, NON_SECURE); // flag | (1 << 1)
=> if SPMC is not present in S-EL2
=> set_interrupt_rm_flag(flags, SECURE); // flag | (1 << 0)
=> register_interrupt_type_handler(type=INTR_TYPE_EL3,,flags=flags)
register_interrupt_type_handler()
=> set_routing_model(INTR_TYPE_EL3, flags)
set_routing_model()
=> intr_type_descs[INTR_TYPE_EL3].flags = flags;
// 两次调用分别将FIQ和IRQ的情况汇总到最终上下文中的SCR寄存器
=> set_scr_el3_from_rm(type=INTR_TYPE_EL3, flags, SECURE);
=> set_scr_el3_from_rm(type=INTR_TYPE_EL3, flags, NON_SECURE);
set_scr_el3_from_rm()
// 看传入secure state在flag中对应的bit是1还是0
=> flag = get_interrupt_rm_flag(flags, security_state);
// 计算SCR_ELx中对应secure state(FIQ/IRQ)的控制 bit
=> bit_ops = plat_interrupt_type_to_line(INTR_TYPE_EL3, security_state);
=> intr_type_descs[type].scr_el3[security_state] = (u_register_t)flag << bit_pos;
/*
* Update scr_el3 only if there is a context available. If not, it
* will be updated later during context initialization which will obtain
* the scr_el3 value to be used via get_scr_el3_from_routing_model()
*/
=> cm_write_scr_el3_bit(security_state, bit_pos, flag);
/*******************************************************************************
* This function updates a single bit in the SCR_EL3 member of the 'cpu_context'
* pertaining to the given security state using the value and bit position
* specified in the parameters. It preserves all other bits.
******************************************************************************/
cm_write_scr_el3_bit()
=> ctx = cm_get_context(security_state);
scr_el3 = read_ctx_reg(state, CTX_SCR_EL3);
scr_el3 &= ~(1UL << bit_pos);
scr_el3 |= (u_register_t)value << bit_pos;
write_ctx_reg(state, CTX_SCR_EL3, scr_el3);
SDEI
SDEI: Software Delegated Exception Interface,软件委派异常接口。
其实就是在 EL1(或 EL2)能够注册 SDEI 中断,其实就是切换到 EL3 中将该中断注册成 group0 中断,然后当事件到来时,中断将直接 target 到 EL3,在 EL3 的处理程序中会 dispatcher 到 EL1(EL2)中再处理。
用图来表示就是上述的过程,大概分为以下几步:
- Linux/hyperv 注册一个中断为 SDEI 中断,调用 SDEI API
- SDEI API 准备好参数,调用 smc 指令陷入 EL3,EL3 将这个中断号配置为Group0分组,并设置好 SCR_EL3.IRQ/FIQ
- 当运行在 EL1/2 时,来了一个注册为 SDEI 的中断,因为是 Group0 且 SCR_EL3.FIQ 被置位,所以直接路由到 EL3 处理。
- EL3 fiq handler 识别到这是一个注册为 SDEI 的中断,则返回上层注册的 hangler 去执行,处理完后再返回。
SDEI 和 EHF 的关系
结论:SDEI 依赖 EHF。
没有开 EHF 的情况下,BL31 是不处理 FIQ、IRQ 的,没有 dispatch 的行为。
当某个Group0的中断触发FIQ,进入EL3,对SDEI来说,也可以当作是一个event signal动作。所以说,这个中断可以在EL3直接处理,也可以作为SDEI的事件触发源,SDEI会进行dispatch行为,委托注册的handler进行处理。
SDEI 的应用
Hypervisor
引入 Hypervisor 后,如果开启了中断直通,那么 hostVM 就不能控制 guestVM 了,这里的”控制“的含义是将其拉到 EL2 hypervisor。为什么呢?
- 因为没有中断直通的情况下,hostVM 发送 IPI 给 guestVM,Hypervisor 会拦截 IPI,识别到这是一个控制 guestVM 的行为,就会停在 Hypervisor 完成相应的服务。
- 但是,如果开启了中断直通,这个 IPI 不会被 Hypervisor 拦截,而是直接到了 GuestVM 里,GuestVM 并不一定会选择陷入 Hypervisor,这取决于 guestVM 的实现,所以说不能保证。
此时可以通过把某个 IPI 注册为 SDEI 中断,使得发送 IPI 首先被 EL3 拦截,通过注册 Hypervisor 对应的处理函数,就使得 guestVM 一定进入 EL2。
NMI
NMI 的含义是不可屏蔽中断,如果某个中断我们想让其快速触发,通过注册为 SDEI 中断,即便是在 EL1/2 是屏蔽中断的环境下,这个中断也能直接到达 EL3,dispatch 到 EL1/2 完成处理,不受中断屏蔽上下文的影响。