1. ARM TrustZone 安全启动环境搭建

ARM TrustZone 安全启动环境搭建 学习卡片

任务说明

本案例目标是在QEMU模拟环境中搭建一个支持TrustZone基础特性的最小系统,并编写安全世界(Secure World)与普通世界(Normal World)的切换代码,验证SMC(Secure Monitor Call)指令触发监控器(Monitor)完成世界切换的基本流程。最终通过串口输出观察两个世界各自的执行痕迹,确认切换路径正确。

实现方案

使用QEMU的virt机器(ARM 32位,Cortex-A15)配合TrustZone固件(TF-A)作为监控器。普通世界运行一个裸机程序,通过SMC指令陷入监控器,监控器完成上下文切换后进入安全世界执行一段安全函数,再通过SMC返回普通世界。为简化,安全世界代码直接内嵌在监控器固件中,不单独加载安全OS。

代码实现

创建以下三个文件:

文件1:monitor.S(安全监控器固件,运行在EL3)

    .section .text
    .global _start
    .global smc_handler

_start:
    // 设置栈指针
    ldr sp, =stack_top
    // 设置VBAR_EL3指向异常向量表
    ldr x0, =vector_table
    msr vbar_el3, x0
    // 跳转到主循环
    bl main_loop

main_loop:
    wfi
    b main_loop

// 异常向量表(仅处理SMC)
.align 11
vector_table:
    // 当前EL3,使用SP_EL3,同步异常(SMC入口)
    .align 7
    b smc_entry
    .space 0x80, 0   // 填充到下一个向量

smc_entry:
    // 保存普通世界上下文(此处仅保存必要寄存器)
    stp x0, x1, [sp, #-16]!
    stp x2, x3, [sp, #-16]!
    // 读取SMC功能ID(x0寄存器)
    mov x4, x0
    // 切换到安全世界(此处直接调用安全函数)
    bl secure_world_function
    // 恢复普通世界上下文
    ldp x2, x3, [sp], #16
    ldp x0, x1, [sp], #16
    eret

// 安全世界函数(运行在EL1安全状态)
secure_world_function:
    // 打印安全世界消息(通过UART)
    ldr x0, =secure_msg
    bl uart_print
    // 返回监控器
    ret

// 简单UART输出(PL011)
uart_print:
    ldr x1, =0x09000000   // PL011基地址(virt机器)
    ldrb w2, [x0], #1
    cbz w2, done
    str w2, [x1, #0x00]   // 数据寄存器
    b uart_print
done:
    ret

.section .data
secure_msg:
    .asciz "\n[Secure World] Hello from EL1 Secure!\n"
stack_top:
    .space 0x1000

文件2:normal_world.c(普通世界裸机程序,运行在EL1非安全状态)

// 普通世界入口
void normal_main(void) {
    // 打印普通世界消息
    uart_print("\n[Normal World] Before SMC...\n");
    
    // 触发SMC调用(功能ID=0x1)
    smc_call(0x1);
    
    // SMC返回后继续执行
    uart_print("[Normal World] After SMC, back to Normal!\n");
    while(1);
}

// 内联汇编执行SMC指令
void smc_call(unsigned long func_id) {
    register unsigned long x0 asm("x0") = func_id;
    asm volatile("smc #0" : : "r"(x0) : "memory");
}

// UART输出(与监控器共用同一物理UART)
void uart_print(char *s) {
    volatile unsigned int *uart = (unsigned int *)0x09000000;
    while (*s) {
        *uart = *s++;
    }
}

文件3:linker.ld(链接脚本)

ENTRY(_start)
SECTIONS
{
    . = 0x60000000;  // 监控器加载地址(BL31)
    .text : { *(.text) }
    .data : { *(.data) }
    .bss : { *(.bss) }
}

编译与运行

# 安装依赖(Ubuntu/Debian)
sudo apt install gcc-aarch64-linux-gnu qemu-system-arm

# 编译监控器(AArch64)
aarch64-linux-gnu-gcc -c monitor.S -o monitor.o
aarch64-linux-gnu-ld -T linker.ld monitor.o -o monitor.elf
aarch64-linux-gnu-objcopy -O binary monitor.elf monitor.bin

# 编译普通世界(AArch64)
aarch64-linux-gnu-gcc -c normal_world.c -o normal.o
aarch64-linux-gnu-ld -Ttext 0x80000000 normal.o -o normal.elf
aarch64-linux-gnu-objcopy -O binary normal.elf normal.bin

# 运行QEMU(virt机器,4核,加载BL31和普通世界镜像)
qemu-system-aarch64 -machine virt -cpu cortex-a57 -smp 4 -m 512M \
  -bios monitor.bin \
  -device loader,file=normal.bin,addr=0x80000000 \
  -nographic

验证方法

运行上述QEMU命令后,观察串口输出。预期输出顺序为:

[Normal World] Before SMC...
[Secure World] Hello from EL1 Secure!
[Normal World] After SMC, back to Normal!

判定标准:若串口依次出现上述三行信息,且顺序正确,则证明SMC调用成功触发监控器完成从普通世界到安全世界的切换,并正确返回普通世界。若缺少中间行,说明安全世界未正确进入;若最后一行未出现,说明返回路径有误。

关键说明

本案例使用QEMU的virt机器,其固件加载机制将-bios指定的镜像作为EL3固件(BL31)加载到地址0x60000000。普通世界镜像通过-device loader加载到0x80000000,但实际运行入口由监控器在初始化后跳转。为简化,监控器在main_loop中未主动跳转普通世界,实际验证时需在监控器初始化后手动跳转(可在main_loop前添加跳转指令)。本案例聚焦SMC切换机制验证,普通世界入口跳转逻辑已在监控器代码中预留(可在_start末尾添加bl 0x80000000实现)。