Linux SMP启动罗曼史(下):PSCI的安全唤醒与TrustZone的密室协约

Posted by FY.Lian on 2025-04-10

图片

从 spin-table 到 PSCI 的演进必要性

尽管spin-table 机制(通过设置从核的启动地址寄存器(**CPU Release Address Register)**)能够实现多核冷启动,但其存在三大关键缺陷:

  1. 功能局限性:仅支持主核唤醒从核的冷启动操作,无法实现 CPU 热插拔、电源状态切换(IDLE/SUSPEND/RESET)等高级电源管理功能;
  2. 硬件耦合性:每个 SoC 需在设备树(DTB)中定义独特的cpu-release-addr属性,导致内核与硬件强绑定(如 NVIDIA Tegra 系列需特殊reset方法);
  3. 安全风险:电源操作直接暴露于非安全世界(Normal World),攻击者可能通过篡改 CPU 状态寄存器实施提权攻击。

PSCI(Power State Coordination Interface) 的引入解决了上述问题,其核心设计目标包括:

  • 标准化接口:定义跨平台的电源管理原语(如CPU_ON/CPU_OFF/SYSTEM_RESET),统一 ARMv8 生态的电源操作;
  • 安全隔离:通过SMC(Secure Monitor Call)指令将敏感操作委托给安全世界(Secure World)执行;
  • 拓扑抽象:支持多级集群(Multi-Cluster)、异构计算(Big.LITTLE)等复杂架构,通过 MPIDR(Multiprocessor Affinity Register) 标识 CPU 逻辑位置。

ATF 与 PSCI 的架构关系

ARM 可信固件(ARM Trusted Firmware, ATF) 是 PSCI 的参考实现平台,其分层架构如下:

图片

ATF 启动流程拆解

  1. BL1(Boot ROM 阶段)
  2. 固化于 SoC ROM 中,作为可信根(Root of Trust)
  3. 初始化安全环境(EL3 异常向量表、系统寄存器),加载并验证 BL2 镜像的数字签名(RSA-2048/SHA-256)。
  4. BL2(可信引导阶段)
  5. 解析 FIP(Firmware Image Package)格式容器,加载 BL31/BL32/BL33 组件;
  6. 根据设备树(DTB)中的psci节点(method = "smc")和cpu-map拓扑描述,构建 电源状态协调器(PSCI Coordinator)数据结构;
  7. 传递动态配置信息(如 BL31 的入口地址bl31_entrypoint)给 BL33(通常为 U-Boot)。
  8. BL31(运行时服务阶段)
  9. 常驻 EL3,作为安全监视器(Secure Monitor)
  10. 实现PSCI 服务分发器(psci_smc_handler),处理来自内核的 SMC 调用:
1
2
3
4
5
6
// ATF源码示例(services/std_svc/psci/psci_main.c)  uintptr_t psci_smc_handler(uint32_t smc_fid, uint64_t x1, uint64_t x2, ...) {  
switch (smc_fid) {
case PSCI_CPU_ON_AARCH64:
return psci_cpu_on(x1, x2, x3); // 处理CPU启动请求 case PSCI_SYSTEM_RESET:
return psci_system_reset(); // 处理系统复位 // 其他PSCI功能处理分支... }
}
  • 通过 GICv3 的 SGI(Software Generated Interrupt**)**触发从核复位,并设置其入口地址为secondary_entry

图片

PSCI配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// arch/arm64/boot/dts/arm/foundation-v8-psci.dtsi
/ {
psci {
compatible = "arm,psci-1.0";
method = "smc";
};
};
&cpu0 {
enable-method = "psci";
};
&cpu1 {
enable-method = "psci";
};
... ...
};

关于psci的配置可以参考内核文档:Documentation/devicetree/bindings/arm/psci.txt

在示例的dtsi中,每个cpuX节点都被添加了enable-method = "psci"属性,并且增加了psci的配置节点,表示接下来将使用psci启动SMP。配置中method属性指定了使用smc指令使AP核陷入EL3级(在psci-0.2之后的版本将主动忽略忽略cpu_oncpu_off等函数 ID)。

这个method属性可配置的值是包含smc和hvc指令,其都是从低运行级别请求高运行级别的指令,与其类似的还有svc系统调用指令。

bl31流程(等待唤醒)

下文以smc方式psci启动为例,由于涉及arm v8中EL3级的PSCI CORE INTERFACE还需结合ATF源码进行分析。

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
// plat/qemu/common/aarch64/plat_helpers.S
/* -----------------------------------------------------
* void plat_secondary_cold_boot_setup (void);
*
* This function performs any platform specific actions
* needed for a secondary cpu after a cold reset e.g
* mark the cpu's presence, mechanism to place it in a
* holding pen etc.
* -----------------------------------------------------
*/
func plat_secondary_cold_boot_setup
/* Calculate address of our hold entry */
bl plat_my_core_pos
lsl x0, x0, #PLAT_QEMU_HOLD_ENTRY_SHIFT
mov_imm x2, PLAT_QEMU_HOLD_BASE
/* Wait until we have a go */
poll_mailbox:
ldr x1, [x2, x0]
cbz x1, 1f
/* Clear the mailbox again ready for next time. */
mov x1, #PLAT_QEMU_HOLD_STATE_WAIT
str x1, [x2, x0]
/* Jump to the provided entrypoint. */
mov_imm x0, PLAT_QEMU_TRUSTED_MAILBOX_BASE
ldr x1, [x0]
br x1
1:
wfe
b poll_mailbox
endfunc plat_secondary_cold_boot_setup

与前面spin-table类似地,在AP核上电后将执行到bl31的plat_secondary_cold_boot_setup方法中等待进一步的初始化。这里面比较重要的是,其首先获取了当前cpu的偏移值记录到x0中(这里记作pos),并通过hold_base[pos]检查启动许可是否有效(非零),若有效则跳转到PLAT_QEMU_TRUSTED_MAILBOX_BASE所保存的地址处;若无效则跳转到1标记处循环等待。

1
2
3
4
5
6
7
8
9
10
// services/std_svc/std_svc_setup.c
/* Register Standard Service Calls as runtime service */
DECLARE_RT_SVC(
std_svc,
OEN_STD_START,
OEN_STD_END,
SMC_TYPE_FAST,
std_svc_setup,
std_svc_smc_handler
);

接下来是BP核中的内容,首先是定义了smc运行时服务,其中就包含了后面为psci服务的任务,然后通过DECLARE_RT_SVC宏将服务信息添加到rt_svc_descs段(定义见bl31/bl31.ld.S)中。

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
bl31_main
->runtime_svc_init
// common/runtime_svc.c
void __init runtime_svc_init(void)
{
... ...
for (index = 0U; index < RT_SVC_DECS_NUM; index++) {
... ...
/*
* The runtime service may have separate rt_svc_desc_t
* for its fast smc and yielding smc. Since the service itself
* need to be initialized only once, only one of them will have
* an initialisation routine defined. Call the initialisation
* routine for this runtime service, if it is defined.
*/
if (service->init != NULL) {
rc = service->init();
if (rc != 0) {
ERROR("Error initializing runtime service %s\n",
service->name);
continue;
}
}
... ...
}
}

主要看runtime_svc_init方法中,这里遍历了所有定义的服务信息(rt_svc_descs段),并注册和初始化服务。

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
service->init = std_svc_setup
->psci_setup
->plat_setup_psci_ops
// plat/qemu/common/qemu_pm.c
static const plat_psci_ops_t plat_qemu_psci_pm_ops = {
.cpu_standby = qemu_cpu_standby,
.pwr_domain_on = qemu_pwr_domain_on,
.pwr_domain_off = qemu_pwr_domain_off,
.pwr_domain_pwr_down_wfi = qemu_pwr_domain_pwr_down_wfi,
.pwr_domain_suspend = qemu_pwr_domain_suspend,
.pwr_domain_on_finish = qemu_pwr_domain_on_finish,
.pwr_domain_suspend_finish = qemu_pwr_domain_suspend_finish,
.system_off = qemu_system_off,
.system_reset = qemu_system_reset,
.validate_power_state = qemu_validate_power_state,
};
int plat_setup_psci_ops(uintptr_t sec_entrypoint,
const plat_psci_ops_t **psci_ops)
{
uintptr_t *mailbox = (void *) PLAT_QEMU_TRUSTED_MAILBOX_BASE;
*mailbox = sec_entrypoint;
secure_entrypoint = (unsigned long) sec_entrypoint;
*psci_ops = &plat_qemu_psci_pm_ops;
return 0;
}

在plat_qemu_psci_pm_ops中可以看到支持的各类操作,如CPU上电、下电、挂起等。这里需要关注的是qemu_pwr_domain_on方法,在后面的BP核启动AP核时会使用。

在plat_setup_psci_ops方法中也配置了PLAT_QEMU_TRUSTED_MAILBOX_BASE,即BP核的入口地址。

BP核启动AP核时陷入EL3。

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
std_svc_smc_handler
->psci_smc_handler
->psci_cpu_on
->is_valid_mpidr
->psci_validate_entry_point // 验证入口地址有效
->psci_cpu_on_start
->psci_plat_pm_ops->pwr_domain_on = qemu_pwr_domain_on // 给核心上电
// include/lib/psci/psci.h
#define PSCI_CPU_ON_AARCH32 U(0x84000003)
#define PSCI_CPU_ON_AARCH64 U(0xc4000003)
// lib/psci/psci_main.c
/*******************************************************************************
* PSCI top level handler for servicing SMCs.
******************************************************************************/
u_register_t psci_smc_handler(uint32_t smc_fid,
u_register_t x1,
u_register_t x2,
u_register_t x3,
u_register_t x4,
void *cookie,
void *handle,
u_register_t flags)
{
... ...
/* 64-bit PSCI function */
switch (smc_fid) {
case PSCI_CPU_SUSPEND_AARCH64:
ret = (u_register_t)
psci_cpu_suspend((unsigned int)x1, x2, x3);
break;
case PSCI_CPU_ON_AARCH64:
ret = (u_register_t)psci_cpu_on(x1, x2, x3);
break;
... ...
}

bl31接收到该异常后执行std_svc_smc_handler处理函数,此处的PSCI_CPU_ON_AARCH64定义为0xc4000003最终调用平台相关的电源管理接口,完成cpu的上电工作。

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
std_svc_smc_handler
->psci_smc_handler
->psci_cpu_on
->is_valid_mpidr
->psci_validate_entry_point // 验证入口地址有效
->psci_cpu_on_start
->psci_plat_pm_ops->pwr_domain_on = qemu_pwr_domain_on // 给核心上电
->cm_init_context_by_index // 通过cpu编号找到cpu_context_t,并保存cpu寄存器的值,在退出el3时进行恢复
->cm_setup_context // 设置cpu_context_t
// plat/qemu/common/qemu_pm.c
static const plat_psci_ops_t plat_qemu_psci_pm_ops = {
.cpu_standby = qemu_cpu_standby,
.pwr_domain_on = qemu_pwr_domain_on,
.pwr_domain_off = qemu_pwr_domain_off,
.pwr_domain_pwr_down_wfi = qemu_pwr_domain_pwr_down_wfi,
.pwr_domain_suspend = qemu_pwr_domain_suspend,
.pwr_domain_on_finish = qemu_pwr_domain_on_finish,
.pwr_domain_suspend_finish = qemu_pwr_domain_suspend_finish,
.system_off = qemu_system_off,
.system_reset = qemu_system_reset,
.validate_power_state = qemu_validate_power_state,
};
// arch/arm64/kernel/psci.c
/*******************************************************************************
* Platform handler called when a power domain is about to be turned on. The
* mpidr determines the CPU to be turned on.
******************************************************************************/
static int qemu_pwr_domain_on(u_register_t mpidr)
{
int rc = PSCI_E_SUCCESS;
unsigned pos = plat_core_pos_by_mpidr(mpidr);
uint64_t *hold_base = (uint64_t *)PLAT_QEMU_HOLD_BASE;
hold_base[pos] = PLAT_QEMU_HOLD_STATE_GO;
sev();
return rc;
}

此处先获取了BP核的偏移pos,接着将变量hold_base[pos]赋值为PLAT_QEMU_HOLD_STATE_GO,意味着添加了启动许可,最后调用sev指令唤醒BP核。这番操作与spin-table如出一辙,只不过一个在内核中另一个在bl31中。

那么BP核就会从bl31_warm_entrypoint开始执行,在plat_setup_psci_ops中会设置(每个平台都有自己的启动地址)。在el3_exit中主要开启了mmu,配置了电源管理模块,接着使用之前保存cpu_context结构中的数据,写入到cscr_el3、spsr_el3、elr_el3,最后通过eret指令使自己进入到Linxu内核。

唤醒AP核

对比 PSCI 与 spin-table 两种机制的cpu_operations结构体差异,我们可以发现架构设计的重要转变:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 以spin-table和PSCI两种操作集对比为例
const struct cpu_operations cpu_psci_ops = {
.name = "psci",
.cpu_init = cpu_psci_cpu_init, // 仅做状态标记
.cpu_prepare= cpu_psci_cpu_prepare, // 空实现
.cpu_boot = cpu_psci_cpu_boot // 触发SMC调用
};
const struct cpu_operations cpu_spin_table_ops = {
.name = "spin-table",
.cpu_init = spin_table_cpu_init, // 映射AP核启动地址
.cpu_prepare= spin_table_cpu_prepare,// 内存屏障操作
.cpu_boot = spin_table_cpu_boot // 写release变量
};

在 PSCI 实现中,cpu_psci_cpu_boot通过psci_ops.cpu_on发起 SMC 调用,这里隐藏着一个重要设计决策:内核不再直接操作 AP 核启动寄存器,而是将控制权移交 EL3 固件。这种抽象化处理使得同一份内核代码可以适配不同厂商的芯片实现。

具体到启动参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
static int cpu_psci_cpu_boot(unsigned int cpu)
{
phys_addr_t pa_secondary_entry = __pa_symbol(secondary_entry);
int err = psci_ops.cpu_on(cpu_logical_map(cpu), pa_secondary_entry);
if (err)
pr_err("failed to boot CPU%d (%d)\n", cpu, err);
return err;
}
psci_0_2_cpu_on
->__psci_cpu_on
->__invoke_psci_fn_smc
->arm_smccc_smc

故最终在cpu_psci_cpu_boot中调用到psci_ops.cpu_on=``psci_0_2_cpu_on函数,这里的第一个参数是CPU ID标识启动哪个cpu,第二个参数是AP核启动后进入内核执行的地址secondary_entry。这里的物理地址转换(__pa_symbol)值得注意 —— 由于 EL3 固件运行在 MMU 关闭环境,必须使用物理地址指定 AP 核的入口点。这解释了为何不能直接传递内核虚拟地址。

SMC 调用的参数构造包含精心设计的位域信息:

1
2
3
arm_smccc_smc(0xC4000003,   // [bit31]快速调用 | [bit30]32位约定 | [PSCI_CPU_ON]
cpuid, // 目标CPU的MPIDR
pa_secondary_entry, 0, 0, 0, 0, 0, &res)

当 ATF 在 EL3 接收到该请求后,会执行以下关键操作序列:

  1. 验证调用合法性(包括 MPIDR 有效性检查)
  2. 配置目标 CPU 的复位向量到 secondary_entry
  3. 释放目标 CPU 的复位信号
  4. 清理执行上下文以保证安全状态

AP 核最终从 secondary_entry 开始执行时,其上下文已经过 ATF 的初始化设置,包括关键寄存器的预设值和必要的安全策略加载。这比 spin-table 方案多了硬件抽象层的安全校验流程,但也带来了约 200-500 微秒的额外启动延迟(具体数值依赖芯片实现)。

AP核启动

上面介绍了两种AP核的启动方式,他们在进入到内核后都是从secondary_startup函数开始执行。

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
SYM_FUNC_START_LOCAL(secondary_startup)
/*
* Common entry point for secondary CPUs.
*/
mov x20, x0 // preserve boot mode
bl finalise_el2
bl __cpu_secondary_check52bitva
#if VA_BITS > 48
ldr_l x0, vabits_actual
#endif
bl __cpu_setup // initialise processor
adrp x1, swapper_pg_dir // 加载到x1,配置页表
adrp x2, idmap_pg_dir // 加载到x2,配置页表
bl __enable_mmu // 使能mmu
ldr x8, =__secondary_switched
br x8
SYM_FUNC_END(secondary_startup)
SYM_FUNC_START_LOCAL(__secondary_switched)
mov x0, x20
bl set_cpu_boot_mode_flag
str_l xzr, __early_cpu_boot_status, x3
adr_l x5, vectors // 设置异常向量表
msr vbar_el1, x5
isb // 指令同步屏障
adr_l x0, secondary_data //获得主处理器传递过来的AP核数据
ldr x2, [x0, #CPU_BOOT_TASK] // 获得AP核的idle进程的tsk结构
cbz x2, __secondary_too_slow
init_cpu_task x2, x1, x3
#ifdef CONFIG_ARM64_PTR_AUTH
ptrauth_keys_init_cpu x2, x3, x4, x5
#endif
bl secondary_start_kernel // 跳转到c程序 继续执行AP核初始化
ASM_BUG()
SYM_FUNC_END(__secondary_switched)

这里先保存启动模式信息,接着进行 EL2 相关配置、处理器及虚拟地址检查等操作,完成处理器初始化后加载页目录地址并启用 MMU。接着在__secondary_switched 函数中会设置启动模式标志、异常向量表,获取主处理器传递的数据来处理idle任务相关配置,最后调用 secondary_start_kernel 函数继续初始化,若流程异常则触发 ASM_BUG()

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// arch/arm64/kernel/smp.c
/*
* This is the secondary CPU boot entry. We're using this CPUs
* idle thread stack, but a set of temporary page tables.
*/
asmlinkage notrace void secondary_start_kernel(void)
{
// 获取当前CPU的多处理器标识寄存器(MPIDR)的值,并与掩码按位与,得到用于标识当前CPU的ID,后续用于区分不同CPU等操作
u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
// 指向所有内核线程共享的内存管理上下文结构体init_mm,二级CPU将关联此通用内存管理环境
struct mm_struct *mm = &init_mm;
// 用于获取当前CPU对应的特定操作函数集合结构体指针
const struct cpu_operations *ops;
// 获取当前CPU的编号,用于后续针对特定CPU的操作
unsigned int cpu = smp_processor_id();
/*
* All kernel threads share the same mm context; grab a
* reference and switch to it.
*/
mmgrab(mm);
current->active_mm = mm;
/*
* TTBR0 is only used for the identity mapping at this stage. Make it
* point to zero page to avoid speculatively fetching new entries.
*/
cpu_uninstall_idmap();
// 如果系统使用中断优先级屏蔽机制,就进行相应的初始化,确保中断按期望的优先级规则处理
if (system_uses_irq_prio_masking())
init_gic_priority_masking();
rcu_cpu_starting(cpu);
trace_hardirqs_off(); // 关闭硬件中断的跟踪功能,减少启动阶段不必要的开销,专注于关键初始化
/*
* If the system has established the capabilities, make sure
* this CPU ticks all of those. If it doesn't, the CPU will
* fail to come online.
*/
check_local_cpu_capabilities();
ops = get_cpu_ops(cpu); // 获取当前CPU对应的操作函数集合指针,用于后续执行特定于该CPU的操作
if (ops->cpu_postboot)
ops->cpu_postboot(); // 如果存在启动后特定操作函数,就执行它,完成CPU启动后的额外初始化工作
/*
* Log the CPU info before it is marked online and might get read.
*/
cpuinfo_store_cpu();
store_cpu_topology(cpu);
/*
* Enable GIC and timers.
*/
notify_cpu_starting(cpu);
ipi_setup(cpu); // 进行处理器间中断(IPI)相关设置,用于建立CPU间可靠的通信机制
numa_add_cpu(cpu); // 在NUMA架构下,将当前CPU添加到NUMA相关管理结构中,便于后续内存分配等考虑CPU与内存的关系
/*
* OK, now it's safe to let the boot CPU continue. Wait for
* the CPU migration code to notice that the CPU is online
* before we continue.
*/
pr_info("CPU%u: Booted secondary processor 0x%010lx [0x%08x]\n",
cpu, (unsigned long)mpidr,
read_cpuid_id());
update_cpu_boot_status(CPU_BOOT_SUCCESS); // 将当前CPU的启动状态更新为成功启动,方便其他模块判断启动情况
set_cpu_online(cpu, true); // 正式将当前CPU设置为在线状态,使其可参与系统的任务调度等正常运行操作
complete(&cpu_running); // 完成一个同步操作,通知其他等待的代码(如引导CPU相关代码)当前CPU已准备就绪
local_daif_restore(DAIF_PROCCTX); // 恢复本地(当前CPU)的DAIF寄存器状态到指定的进程上下文相关状态,保障中断等处理正常
/*
* OK, it's off to the idle thread for us
*/
cpu_startup_entry(CPUHP_AP_ONLINE_IDLE); // 进入idle状态
}

可以看到从 CPU 进入到内核后所进行的一系列配置操作,获取当前 CPU 的标识相关信息(例如“mpidr”)以及编号(“cpu”),并将其与内核线程共享的内存管理上下文(“init_mm”)相关联。紧接着开展一系列初始化工作,例如处理与内存映射有关的“TTBR0”设置、中断相关功能的初始化(这些都是与处理器强相关的初始化代码,一些通用的初始化工作已由主处理器完成)。

随后向系统的其他部分通告当前 CPU 已成功启动且能够参与运行,最终进入空闲线程等待调度器分配任务,至此,二级 CPU 顺利完成启动流程并融入到多处理器系统的运行之中。

参考

ARM64 SMP多核启动(上)- spin-table - yooooooo - 博客园

ARM64 SMP多核启动(下)- PSCI - yooooooo - 博客园

ChinaUnix博客 - Article Blog

GitHub - u-boot/u-boot: “Das U-Boot” Source Tree

GitHub - ARM-software/arm-trusted-firmware: Read-only mirror of Trusted Firmware-A


This is copyright.