본문으로 건너뛰기
BSP Development · 7/21

TF-A·TrustZone 통합 — BL31·secure world·SMC 흐름 적용

· Hawk · 9분 읽기

#한 줄 요약

“ARMv8 BSP는 TF-A의 BL31이 secure monitor로 상주하고, U-Boot은 그 위의 BL33으로 동작합니다.” ARM Trusted Firmware-A는 거의 모든 Cortex-A 보드에서 반드시 통합해야 하는 secure world의 기반층입니다.

#TF-A가 필요한 이유

ARMv8 아키텍처에서 부팅 직후 모든 코드가 가장 높은 권한 레벨인 EL3에서 시작합니다. EL3는 secure monitor를 위한 자리이며, 이 자리에 머무를 코드가 있어야 OS가 SMC 호출로 PSCI·secure storage·trusted apps에 접근할 수 있습니다. ARM은 이 역할을 표준화한 reference 구현으로 *Trusted Firmware-A (TF-A)*를 제공합니다.

i.MX 8M, RK3588, AM62, STM32MP1, Qualcomm SDM 같은 ARMv8 SoC는 모두 TF-A를 BL31로 채택합니다. vendor가 직접 작성한 SMC monitor를 쓰는 경우는 거의 없으며, vendor는 TF-A에 자기 SoC port만 추가합니다.

TF-A 없이는 PSCI를 통한 CPU on/off가 불가능하므로 SMP 부팅도 안 됩니다. TrustZone-aware DRAM 컨트롤러 설정도 BL31의 일이며, OP-TEE 같은 TEE OS도 BL31의 boot 후에 깨어납니다.

#BL 단계 모델

TF-A는 부팅을 BL0부터 BL33까지 단계로 나눕니다. 각 단계의 역할과 다음 단계로의 전이는 다음과 같습니다.

단계역할다음 단계로 넘기는 일
BL0BootROM (SoC 내부, fix)boot mode pin 읽기, BL1 적재
BL1Trusted Boot ROM (선택적, ROM 안에 fuse-burn 가능)BL2 인증·적재
BL2Trusted Boot Firmware (DDR 초기화·서명 검증)SoC vendor마다 SPL 대체 또는 보완
BL31EL3 Runtime Firmware (Secure Monitor)PSCI·SMC handler로 상주, BL32(Secure World OS, OP-TEE 등, 선택) 기동
BL33Non-secure firmware (U-Boot)Linux 부팅 준비
Linux KernelEL2 또는 EL1에서 실행

BL0에서 시작해 BL33(U-Boot)까지 순차로 내려가며, BL31이 BL32를 곁가지로 기동한 뒤 BL33으로 이어집니다.

BL31은 상주 코드입니다. Linux로 진입한 뒤에도 SMC instruction이 호출되면 BL31이 깨어나 처리하고 EL1으로 돌아갑니다. SMC instruction은 사실상 secure world로의 trap입니다.

i.MX 8M Plus의 실제 흐름은 다음과 같습니다.

BootROM
→ flash.bin 적재 (offset 32KB)
flash.bin = SPL + DDR firmware + tee.bin + bl31.bin + u-boot.itb
→ SPL이 SRAM에서 동작 (DDR 초기화)
→ SPL이 DDR로 bl31.bin·tee.bin·u-boot.itb 적재
→ BL31 점프
→ BL31이 OP-TEE(BL32)·U-Boot(BL33) 시작

SPL이 BL2 역할을 겸하는 경우입니다. RK3588은 BootROM이 직접 BL2를 적재하고 BL2가 BL31을 호출하는 더 표준적인 흐름입니다.

#TF-A 소스 트리

trusted-firmware-a/
├── bl1/
├── bl2/
├── bl2u/
├── bl31/
├── bl32/
├── plat/
│ ├── nxp/
│ │ └── imx/
│ │ └── imx8m/
│ │ ├── imx8mp/
│ │ ├── imx8mq/
│ │ └── ...
│ ├── rockchip/
│ │ ├── rk3588/
│ │ └── ...
│ ├── ti/
│ │ ├── k3/
│ │ └── ...
│ ├── st/
│ │ └── stm32mp1/
│ └── arm/
├── drivers/
├── lib/
├── include/
└── services/
└── std_svc/

주요 디렉터리의 역할은 다음과 같습니다.

디렉터리역할
bl31/EL3 runtime
bl32/BL32 reference (TSP)
plat/nxp/imx/imx8m/imx8mp/i.MX 8M Plus port
plat/arm/Juno, FVP reference
drivers/UART, crypto, GIC, console
lib/libc, fdt, optee_utils
services/std_svc/PSCI 등 표준 서비스

새 BSP에 TF-A를 통합할 때 직접 만드는 디렉터리는 plat/<vendor>/<soc>/<board>/ 하나입니다. SoC는 이미 vendor가 port를 maintain하고 있으므로 우리는 보드별 plat_io_storage.c·platform_def.h·plat_psci.c 정도만 만듭니다.

#BL31 빌드 — i.MX 8M Plus 사례

Terminal window
git clone https://github.com/ARM-software/arm-trusted-firmware.git
cd arm-trusted-firmware
export CROSS_COMPILE=aarch64-linux-gnu-
# BL31 빌드 (DEBUG=1이면 console 출력 포함)
make PLAT=imx8mp bl31 DEBUG=1 LOG_LEVEL=40 \
BL32_BASE=0x56000000 BL32_SIZE=0x02000000
# 결과
ls build/imx8mp/debug/bl31.bin

BL32_BASE·BL32_SIZE는 OP-TEE가 들어갈 자리입니다. OP-TEE 없이 부팅한다면 안 줘도 됩니다.

LOG_LEVEL은 BL31의 console 출력량입니다. 운용에서는 0(none) 또는 10(notice)이고, 개발에서는 40(verbose)으로 둡니다.

빌드 결과 bl31.bin위치 독립적 raw binary입니다. SPL이 미리 정해진 주소에 적재한 뒤 점프합니다. i.MX 8M Plus는 0x970000이 BL31의 entry입니다.

#platform_def.h — 보드별 메모리 맵

plat/nxp/imx/imx8m/imx8mp/include/platform_def.h에 보드의 메모리 맵이 정의됩니다.

#define BL31_BASE U(0x970000)
#define BL31_LIMIT U(0x990000)
#define BL32_BASE U(0x56000000) /* OP-TEE */
#define BL32_LIMIT U(0x58000000)
#define BL33_BASE U(0x40200000) /* U-Boot proper */
#define IMX_BOOT_UART_BASE U(0x30890000) /* UART2 */
#define IMX_BOOT_UART_CLK_IN_HZ 24000000
#define PLATFORM_STACK_SIZE 0x800
#define PLATFORM_CORE_COUNT 4
#define MAX_MMAP_REGIONS 16
#define MAX_XLAT_TABLES 8
#define PLAT_NS_IMAGE_OFFSET BL33_BASE

BL31_BASE는 SRAM 안의 작은 영역입니다(보통 64~128KB). DDR이 깨어나기 전부터 동작해야 하므로 SRAM에 머뭅니다.

BL32_BASE는 OP-TEE가 들어갈 DDR 안 영역입니다. 이 영역은 TZASC(TrustZone Address Space Controller)로 secure-only로 보호됩니다. Linux에서 이 주소를 read하면 fault가 납니다.

BL33_BASE는 U-Boot proper의 적재 주소입니다. U-Boot defconfig의 CONFIG_SYS_TEXT_BASE와 일치해야 합니다.

#PSCI 구현

TF-A의 가장 큰 가치는 PSCI(Power State Coordination Interface)입니다. PSCI는 ARM SoC의 CPU on/off, 시스템 reset, suspend를 표준화한 SMC API입니다.

/* plat/nxp/imx/imx8m/imx8mp/imx8mp_psci.c (요약) */
static int imx_pwr_domain_on(u_register_t mpidr)
{
unsigned int cpu_id = MPIDR_AFFLVL0_VAL(mpidr);
uint64_t entry = BL31_BASE;
/* CPU 전원 켜기 (SoC 특정 GPC register) */
imx_set_cpu_secure_entry(cpu_id, entry);
imx_set_cpu_pwr_on(cpu_id);
return PSCI_E_SUCCESS;
}
static void imx_system_reset(void)
{
/* SoC reset (WDOG 또는 SCFW SMC) */
writel(SRC_BASE_ADDR + 0x0, 0x1);
while (1) wfi();
}
const plat_psci_ops_t imx_plat_psci_ops = {
.pwr_domain_on = imx_pwr_domain_on,
.pwr_domain_off = imx_pwr_domain_off,
.pwr_domain_suspend = imx_pwr_domain_suspend,
.pwr_domain_on_finish = imx_pwr_domain_on_finish,
.pwr_domain_suspend_finish = imx_pwr_domain_suspend_finish,
.system_off = imx_system_off,
.system_reset = imx_system_reset,
.validate_power_state = imx_validate_power_state,
.get_sys_suspend_power_state = imx_get_sys_suspend_power_state,
};
int plat_setup_psci_ops(uintptr_t sec_entrypoint,
const plat_psci_ops_t **psci_ops)
{
imx_mailbox_init(sec_entrypoint);
*psci_ops = &imx_plat_psci_ops;
return 0;
}

이 ops 테이블을 등록해 두면 Linux의 arm,psci-1.0 드라이버가 SMC로 PSCI_CPU_ON을 호출할 때마다 imx_pwr_domain_on()이 실행됩니다.

#device tree에서의 PSCI

Linux DT에 PSCI node가 있어야 secondary CPU bring-up이 동작합니다.

psci {
compatible = "arm,psci-1.0";
method = "smc";
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0>;
enable-method = "psci";
next-level-cache = <&A53_L2>;
};
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x1>;
enable-method = "psci";
next-level-cache = <&A53_L2>;
};
/* cpu2, cpu3 동일 */
};

enable-method = "psci"PSCI 사용을 의미합니다. 이 한 줄이 없으면 Linux는 SMP 부팅을 안 합니다. Ch 9에서 SMP bring-up 디버깅을 자세히 다룹니다.

#TrustZone 메모리 분할

TF-A는 SoC의 TZASC(TrustZone Address Space Controller)를 설정해 DDR 일부를 secure-only로 만듭니다.

/* plat_arch_setup() 또는 bl31_plat_arch_setup()에서 호출 */
#define TZASC_BASE 0x32F80000
void imx_init_tzasc(void)
{
/* Region 0: 0x40000000 ~ 0x55FFFFFF — Non-secure (Linux DDR) */
writel(0x40000000, TZASC_BASE + 0x100);
writel(0x55FFFFFF, TZASC_BASE + 0x104);
writel(0xC0000000, TZASC_BASE + 0x110); /* NS access */
/* Region 1: 0x56000000 ~ 0x57FFFFFF — Secure-only (OP-TEE) */
writel(0x56000000, TZASC_BASE + 0x120);
writel(0x57FFFFFF, TZASC_BASE + 0x124);
writel(0x80000000, TZASC_BASE + 0x130); /* S-only access */
/* Region 2: 0x58000000 ~ 0xFFFFFFFF — Non-secure */
writel(0x58000000, TZASC_BASE + 0x140);
writel(0xFFFFFFFF, TZASC_BASE + 0x144);
writel(0xC0000000, TZASC_BASE + 0x150);
}

설정 후 OP-TEE 영역은 Linux에서 접근 불가입니다. dmesg에서 OP-TEE 영역에 read를 시도하면 SError exception이 발생합니다.

비슷한 컨트롤러로 GIC의 secure interrupt 그룹, IOMUX의 secure-only mux, SDMA의 secure channel 등이 있습니다. 모두 BL31이 설정합니다.

#OP-TEE를 BL32로 통합

OP-TEE를 secure OS로 함께 부팅한다면 BL32 슬롯에 들어갑니다.

Terminal window
# OP-TEE 빌드
git clone https://github.com/OP-TEE/optee_os.git
cd optee_os
make CROSS_COMPILE64=aarch64-linux-gnu- CROSS_COMPILE=arm-linux-gnueabihf- \
PLATFORM=imx-mx8mpevk CFG_TEE_CORE_LOG_LEVEL=3 \
CFG_TZDRAM_START=0x56000000 CFG_TZDRAM_SIZE=0x02000000 \
CFG_TEE_RAM_VA_SIZE=0x00200000 \
O=out
# 결과
ls out/core/tee-raw.bin

tee-raw.binbl31.bin과 함께 flash.bin에 묶습니다. i.MX의 경우 NXP imx-mkimage 도구가 이 묶음을 만듭니다.

Terminal window
# imx-mkimage
make SOC=iMX8MP flash_evk_tee \
AHAB_IMG=ahab-container.img \
BL33=u-boot.itb \
BL32=tee-raw.bin \
BL31=bl31.bin \
DDR_FW_PATH=lpddr4_pmu_train_*.bin
ls iMX8M/flash.bin

flash.bin을 SD나 eMMC에 굽습니다. BootROM이 이 단일 binary를 적재해 분해한 뒤 각 단계로 점프합니다.

#SMC 호출 — Linux에서 BL31로

Linux에서 SMC instruction을 호출하면 EL3로 trap하면서 BL31이 핸들러를 실행합니다.

/* Linux kernel arch/arm64/include/asm/psci.h 흐름 */
static int __invoke_psci_fn_smc(unsigned long function_id,
unsigned long arg0,
unsigned long arg1,
unsigned long arg2)
{
struct arm_smccc_res res;
arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res);
return res.a0;
}
/* PSCI_CPU_ON 호출 */
psci_ops.cpu_on = __invoke_psci_fn_smc;
psci_ops.cpu_on(0x84000003, mpidr, entry_point, context_id);

arm_smccc_smc()는 단순히 smc #0 instruction을 실행합니다. 이 순간 EL3로 진입하고 BL31의 runtime_exceptions.S가 처리합니다.

Linux EL1
│ smc #0
BL31 EL3 vector (runtime_exceptions.S)
smc_handler64 → runtime_svc_handler
std_svc_smc_handler → psci_smc_handler
psci_cpu_on → plat_psci_ops.pwr_domain_on
imx_pwr_domain_on (우리 SoC 코드)
▼ eret
Linux EL1 (PSCI_E_SUCCESS)

이 round trip이 μs 단위로 일어납니다. Linux는 이 SMC가 보안 처리되는 normal RPC처럼 사용합니다.

#SiP service — vendor 확장

표준 PSCI 외에도 vendor가 SoC-specific SMC service를 추가할 수 있습니다. 이를 SiP(Silicon Provider) service라고 부릅니다.

plat/nxp/imx/imx8m/imx8mp/imx_sip_svc.c
#define IMX_SIP_GPC 0xC2000000
#define IMX_SIP_BUILDINFO 0xC2000003
#define IMX_SIP_LPDDR_DVFS 0xC2000004
#define IMX_SIP_AARCH32 0xC2000005
#define IMX_SIP_SRC 0xC2000006
#define IMX_SIP_HAB 0xC2000007
static uintptr_t imx_sip_handler(unsigned int 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)
{
switch (smc_fid) {
case IMX_SIP_LPDDR_DVFS:
SMC_RET1(handle, dram_dvfs_handler(x1, x2, x3));
case IMX_SIP_GPC:
SMC_RET1(handle, imx_gpc_handler(x1, x2, x3));
case IMX_SIP_SRC:
SMC_RET1(handle, imx_src_handler(x1, x2, x3, x4));
default:
return SMC_UNK;
}
}
DECLARE_RT_SVC(
imx_sip_svc,
OEN_SIP_START, OEN_SIP_END,
SMC_TYPE_FAST,
NULL,
imx_sip_handler
);

Linux의 i.MX DDR DVFS 드라이버는 이 SMC를 호출해 BL31에게 DDR 주파수 변경을 요청합니다. DDR controller register는 EL3-only이므로 Linux가 직접 못 만지고, BL31을 거쳐야 합니다.

#TF-A 디버깅

TF-A가 시작은 했는데 그 뒤가 멈춘다면 콘솔 출력이 가장 큰 단서입니다.

NOTICE: BL31: v2.10.0
NOTICE: BL31: Built : 14:23:01, May 19 2026
INFO: GICv3 with legacy support detected.
INFO: ARM GICv3 driver initialized in EL3
INFO: TZC: Configured region 0
INFO: BL31: Initialising Exception Handling Framework
INFO: BL31: Initializing runtime services
INFO: BL31: cortex_a53: CPU workaround for 855873 was applied
INFO: BL31: Initializing BL32
INFO: SPSR = 0x3c9
INFO: Entry point address = 0x56000000
NOTICE: OP-TEE 4.0.0 ...
INFO: BL31: Preparing for EL3 exit to normal world
INFO: Entry point address = 0x40200000
INFO: SPSR = 0x3c9

이 로그가 전부 안 나오면 BL31의 어디서 멈췄는지 알아내는 게 다음 작업입니다. LOG_LEVEL=40으로 verbose하게 빌드한 뒤 console에서 마지막 출력을 봅니다.

JTAG으로 BL31의 entry(0x970000)에 break를 걸어 step-by-step 실행할 수도 있습니다. gdb-multiarch로 OpenOCD와 연결합니다.

(gdb) target remote :3333
(gdb) symbol-file build/imx8mp/debug/bl31/bl31.elf
(gdb) break bl31_main
(gdb) c
Continuing.
Breakpoint 1, bl31_main () at bl31/bl31_main.c:73
73 NOTICE("BL31: %s\n", version_string);

.elf 파일이 있어야 source-level debug가 됩니다. make DEBUG=1로 빌드한 결과를 사용합니다.

#자주 하는 실수

#BL31 entry address가 SRAM 밖

BL31_BASE를 DDR 안에 두면 SRAM에서 동작해야 하는 부팅 초기에 DDR이 깨기 전이라 fault가 납니다. SoC vendor가 권장한 SRAM 안 주소를 사용합니다.

#PLATFORM_CORE_COUNT가 실제 코어 수와 다름

이 값이 부족하면 secondary CPU bring-up 시 PSCI가 invalid CPU로 거절합니다. 이 값이 많으면 BL31이 없는 CPU에 SMC를 보내려다 hang합니다.

#TZASC 영역이 OP-TEE 영역과 안 맞음

BL32_BASE·BL32_SIZE와 TZASC region 정의가 다르면 OP-TEE가 자기 영역인데 read fault를 받거나, Linux가 secure 영역에 접근 시도해서 SError를 받습니다. 두 정의를 반드시 일치시킵니다.

#BL33_BASE와 U-Boot의 CONFIG_SYS_TEXT_BASE 불일치

이 둘이 다르면 BL31이 점프했을 때 U-Boot이 링크된 주소가 아닌 곳에서 실행되어 GP 레지스터 참조가 망가집니다. 두 빌드 시스템에서 반드시 같은 주소를 씁니다.

#SiP SMC ID 충돌

vendor가 SiP service ID를 임의로 정해 표준 PSCI 영역(0x84000000~)과 겹치면 PSCI 동작이 망가집니다. SMC ID 공간을 명확히 분리합니다.

#secure-status = "okay" 잊음

DT에서 secure-only 디바이스를 정의할 때 status = "okay"만 두면 non-secure에서는 보이는데 secure에서는 안 보이는 상태가 됩니다. secure-status = "okay"를 함께 둡니다.

#정리

  • ARMv8 BSP에서 TF-A의 BL31은 secure monitor로 EL3에 상주하며 PSCI·SMC·TrustZone을 책임집니다.
  • BL2(또는 SPL)가 DDR을 초기화하고 BL31·BL32·BL33을 DDR에 적재한 뒤 BL31로 점프합니다.
  • plat/<vendor>/<soc>/<board>/에 메모리 맵(platform_def.h)과 PSCI 콜백(<board>_psci.c)을 추가하면 새 보드 port가 됩니다.
  • TZASC 컨트롤러로 DDR의 일부를 secure-only로 분리합니다. OP-TEE 영역이 대표입니다.
  • DT의 psci { method = "smc"; }cpu@N { enable-method = "psci"; }가 SMP 부팅을 가능하게 합니다.
  • Linux는 SMC instruction으로 BL31에 trap하며, 이 round trip이 PSCI·SiP service의 기반입니다.
  • 디버깅은 LOG_LEVEL=40 DEBUG=1 빌드 후 console 마지막 출력 + JTAG의 조합이 가장 효과적입니다.

#다음 편

Ch 8 — Linux 커널 설정에서는 TF-A가 깨운 환경 위에서 Linux 커널을 설정하고 빌드하는 흐름을 다룹니다.

#관련 항목