ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할
#한 줄 요약
“부트는 작은 단계가 큰 다음 단계를 RAM에 적재하는 수직 인계의 연쇄입니다.” — 각 단계는 전 단계가 검증한 코드만 실행하고, 다음 단계에 control을 넘긴 뒤 사라지거나 메모리에 남습니다.
“부트로더는 단일 binary”라는 인식은 너무 단순화된 표현입니다. 요즘 ARMv8-A 보드의 부트 체인은 5단계가 기본입니다. BootROM, BL1, BL2(또는 SPL), BL31, BL33(U-Boot Proper). 각 단계가 왜 따로 있어야 하는지, 어떤 권한 수준에서 동작하는지, 메모리는 어디에서 동작하는지를 정리합니다.
#왜 한 binary로 못 하나
가장 단순한 질문에서 시작합니다. “BootROM이 바로 커널을 적재하면 안 되나?”
세 가지 이유로 못 합니다.
#1. BootROM은 너무 작다
SoC mask ROM은 수십 KB입니다. 그 안에 DDR controller driver, MMC driver, ethernet driver, FIT image parser를 다 못 넣습니다.
#2. BootROM은 수정 불가다
기판 출고 후 영원히 그대로입니다. boot 로직이 바뀌면 어떻게 할까요. 그래서 BootROM은 최소한만 하고, 수정 가능한 다음 단계를 적재하는 일에 집중합니다.
#3. 권한 수준이 다르다
ARMv8-A는 부트가 진행됨에 따라 EL3 → EL3 → EL3 → (EL2 → EL1)로 권한 수준이 내려갑니다. 한 binary로 모든 권한 수준을 다루기는 깨끗하지 않습니다.
이 세 가지 때문에 부트는 다단입니다.
#ARMv8-A의 다섯 단계
ARM Trusted Firmware의 공식 단계 이름은 다음과 같습니다.
각 BL은 별도 binary입니다. 별도 프로젝트에서 빌드합니다.
| 단계 | 빌드 프로젝트 | 보통의 크기 |
|---|---|---|
| BL1 | TF-A | 수 KB ~ 수십 KB |
| BL2 | TF-A | 수십 KB ~ 백 KB |
| BL31 | TF-A | 약 100 KB |
| BL32 | OP-TEE | 수 MB |
| BL33 | U-Boot | 약 1 MB |
더 깊이 — ARM 아키 관점에서의 같은 주제
#U-Boot의 SPL/TPL 모델
U-Boot은 자체 다단 모델인 SPL/TPL을 가집니다. TF-A를 안 쓰는 ARMv7-A에서는 이 모델 단독으로 동작합니다.
TPL이 왜 필요한가. 일부 SoC는 내부 SRAM이 매우 작고(예: ROC-RK3399 보드의 일부 RK3399 SRAM은 32KB 이하), SPL이 거기 안 들어갑니다. 더 작은 TPL이 조금 큰 SPL을 메모리에 풀어 놓고 점프합니다. 마치 SPL이 U-Boot Proper를 풀어 놓는 것과 같은 구조입니다.
대부분의 ARMv8-A 보드는 TPL이 불필요하고 SPL → U-Boot Proper로 충분합니다.
#SPL과 BL2가 한 자리
ARMv8-A 보드에서 SPL과 BL2 중 하나가 DDR initialization 책임을 집니다. 둘 다 같은 자리입니다. 보드마다 둘 중 하나를 선택합니다.
| 모델 | 흐름 | DDR init 담당 |
|---|---|---|
| A — SPL 사용 | BootROM → SPL → BL31 → U-Boot Proper → Linux | SPL (BL31 적재) |
| B — BL2 사용 | BootROM → BL1 → BL2 → BL31 → U-Boot Proper → Linux | BL2 (BL31 적재) |
i.MX 8M, Rockchip RK3399는 모델 A입니다. 일부 ARM server SKU는 모델 B입니다. NXP가 SPL을 선호하는 이유는 U-Boot의 driver model을 그대로 재사용할 수 있어서입니다.
#권한 수준(Exception Level)의 변화
ARMv8-A는 네 개의 권한 수준을 가집니다.
EL3 — secure monitor (가장 높음)EL2 — hypervisorEL1 — kernelEL0 — user space (가장 낮음)추가로 secure / non-secure 구분이 EL0/EL1/EL2에 직교로 존재합니다.
부트 진행에 따라 권한 수준이 내려가는 흐름입니다.
| 단계 | EL | World |
|---|---|---|
| BL1 | EL3 | Secure |
| BL2 | EL3 또는 EL1-S | Secure |
| BL31 | EL3 | Secure (runtime) |
| BL32 (OP-TEE) | EL1-S | Secure |
| BL33 (U-Boot) | EL2 | Non-secure |
| Linux Kernel | EL2 → EL1 | Non-secure |
EL3 → EL2 권한 강하는 BL31이 합니다. ERET 명령으로 target EL과 PC를 설정한 뒤 점프합니다.
#각 단계의 메모리 모델
부트 단계별로 어디서 동작하고 어디에 다음 단계를 적재하는지가 중요합니다.
#BL1
- 실행 위치: secure SRAM 또는 boot ROM 안
- 메모리 모델: MMU off, cache off
- 다음 단계 적재: secure SRAM의 BL2 영역
#BL2
- 실행 위치: secure SRAM
- 메모리 모델: MMU on (secure 매핑), cache on (선택)
- DDR initialization 이후: DDR 사용 가능
- 다음 단계 적재: BL31 → secure DDR, BL32 → secure DDR, BL33 → non-secure DDR
#SPL (BL2 위치 사용 시)
- 실행 위치: SoC SRAM (예: i.MX 8M Plus는 0x920000)
- 메모리 모델: MMU off, cache off
- DDR initialization 책임
- 다음 단계 적재: BL31 → secure DDR, U-Boot Proper → non-secure DDR
#BL31
- 실행 위치: secure DDR
- 메모리 모델: MMU on, cache on
- 부팅 후에도 살아있음 (PSCI/SMC 핸들러)
- ERET로 BL33으로 점프
#BL33 (U-Boot Proper)
- 실행 위치: non-secure DDR (예: 0x40200000)
- 메모리 모델: MMU off (cache on), EL2
- driver model 활성화, 모든 driver probe
i.MX 8M Plus의 메모리 맵:0x00000000 - 0x00100000 boot ROM0x00910000 - 0x00940000 OCRAM (SPL 적재 위치)0x40000000 - 0xC0000000 DDR (2GB) 0x40000000 - 0x40200000 TF-A BL31 (보호 영역) 0x40200000 - 0x41000000 U-Boot Proper 0x40480000 - Linux kernel 적재 위치 0x43000000 - DTB 적재 위치#SPL의 핵심 코드 흐름
SPL이 어떻게 다음 단계를 적재하는지 봅니다. U-Boot 소스의 common/spl/spl.c가 시작점입니다.
/* common/spl/spl.c (간략화) */
void board_init_r(gd_t *dummy1, ulong dummy2){ struct spl_image_info spl_image; int ret;
/* 1. 기본 초기화 */ spl_set_bd(); mem_malloc_init(...);
/* 2. 부트 미디어 결정 */ boot_device = spl_boot_device();
/* 3. 부트 미디어별 로더 호출 */ switch (boot_device) { case BOOT_DEVICE_MMC1: ret = spl_mmc_load_image(&spl_image, ...); break; case BOOT_DEVICE_NAND: ret = spl_nand_load_image(&spl_image, ...); break; case BOOT_DEVICE_NOR: ret = spl_nor_load_image(&spl_image, ...); break; /* ... */ }
/* 4. 다음 단계로 점프 */ spl_board_prepare_for_boot(); jump_to_image(&spl_image);}spl_image는 적재된 이미지의 정보입니다.
struct spl_image_info { const char *name; u8 os; /* IH_OS_U_BOOT, IH_OS_LINUX 등 */ uintptr_t load_addr; /* 적재된 주소 */ uintptr_t entry_point; /* 점프할 주소 */ void *fdt_addr; /* DTB 위치 */ u32 size; u32 flags; void *arg;};ARMv8-A에서는 BL31에 먼저 점프하고, BL31이 다시 BL33으로점프합니다.
void __noreturn spl_invoke_atf(struct spl_image_info *spl_image){ uintptr_t bl33_entry = spl_image->entry_point; struct bl_params *bl_params;
bl_params = bl2_plat_get_bl_image_load_info();
/* BL31에 BL33 정보 전달 + BL31 진입점 호출 */ bl31_entry(bl_params->head->image_info, bl33_entry, ...);
/* 도달하지 않음 */}#FIT image — 한 파일에 모두
SPL이 BL31 binary, BL33 binary, DTB를 각각 적재할 수도 있고, FIT image에서 한 번에 적재할 수도 있습니다. FIT image는 Ch 15에서 다룹니다.
u-boot.itb (FIT image)├── images│ ├── atf-1 ← BL31│ ├── uboot ← BL33 (U-Boot Proper)│ └── fdt-1 ← DTB└── configurations └── conf-1 (default) - firmware: atf-1 - loadables: uboot - fdt: fdt-1SPL은 u-boot.itb 하나를 부트 미디어에서 읽어, images 안의 각 binary를 적재 위치로 풀어 놓습니다.
=> mkimage -f u-boot.its u-boot.itb=> mkimage -l u-boot.itbFIT description: Configuration to load ATF + U-BootCreated: Mon May 19 09:00:00 2026 Image 0 (atf-1) Description: ARM Trusted Firmware Type: Firmware Load Address: 0x40000000 Entry Point: 0x40000000 Image 1 (uboot) Description: U-Boot Type: Standalone Program Load Address: 0x40200000 Entry Point: 0x40200000 Image 2 (fdt-1) Description: imx8mp-evk Type: Flat Device Tree#RISC-V의 다단 부트
RISC-V는 OpenSBI가 ARMv8-A의 TF-A 자리를 차지합니다.
[BootROM] │ ▼[U-Boot SPL] - M-mode - DDR init, OpenSBI 적재 │ ▼[OpenSBI] - M-mode firmware - SBI 핸들러, MTIME interrupt - U-Boot Proper로 점프 (S-mode) │ ▼[U-Boot Proper] - S-mode - 부트 정책 │ ▼[Linux Kernel] - S-modeARMv8-A의 EL이 M-mode/S-mode/U-mode로 간략화된 형태입니다. SBI call이 SMC call에 대응됩니다.
#단계 간 인계 — 어떻게 정보를 넘기나
각 단계 사이에 주소, DTB, 부트 인자를 넘겨야 합니다. ARMv8-A 표준은 x0 ~ x3에 정보를 넣어 넘깁니다.
SPL → BL31 인계:x0 = BL31 진입점x1 = BL32 정보 (선택)x2 = BL33 진입점x3 = BL33 DTB 주소
BL31 → BL33 인계:x0 = DTB physical addressx1 = 0x2 = 0x3 = 0(MMU off, cache off)
BL33 → Linux 인계:x0 = DTB physical addressx1 = 0x2 = 0x3 = 0(MMU off, D-cache off)x0가 모든 단계에서 DTB를 가리킵니다. 이 일관된 ABI가 부트 체인을 단순하게 합니다.
#부트 로그로 보는 단계
i.MX 8M Plus EVK의 부트 로그입니다. 각 줄이 어느 단계 출력인지 표시했습니다.
U-Boot SPL 2024.04 (May 19 2026 - 09:00:00 +0000) ← SPL 시작SPL: PMIC voltage init doneDDRINFO: start DRAM init ← SPL이 DDR initDDRINFO: DRAM rate 4000MTSDDRINFO: ddrphy calibration doneDDRINFO: ddrmix config doneNormal BootTrying to boot from MMC2 ← SPL이 FIT image 적재 시도NOTICE: BL31: v2.10 ← BL31 (TF-A) 시작NOTICE: BL31: Built : 09:00:00, May 19 2026
U-Boot 2024.04 (May 19 2026 - 09:00:00 +0000) ← U-Boot Proper (BL33) 시작
CPU: Freescale i.MX8MP[8] rev1.1 1600 MHzCPU: Industrial temperature grade (-40C to 105C) at 47CReset cause: PORModel: NXP i.MX 8M Plus EVKDRAM: 2 GiBCore: 92 devices, 22 uclasses, devicetree: separateWDT: Started watchdog@30280000 with servicing every 1000ms (60s timeout)MMC: FSL_SDHC: 0, FSL_SDHC: 2Loading Environment from MMC... OKIn: serial@30890000Out: serial@30890000Err: serial@30890000Net: eth0: ethernet@30be0000Hit any key to stop autoboot: 0=>U-Boot SPL은 SPL, NOTICE: BL31은 TF-A, U-Boot 2024.04(앞에 SPL 없음)은 BL33.
#자주 하는 실수
#어느 단계가 부팅하는지 헷갈림
“U-Boot이 부팅 안 된다”고 할 때 SPL인지, U-Boot Proper인지, BL31인지 분리해서 봐야 합니다. 시리얼에 어디까지 출력됐는지로 판단합니다.
#BL31 binary가 제 위치에 없음
u-boot.itb(FIT image)를 만들 때 its 파일에 BL31의 load address가 잘못 적혔으면 DDR의 보호 영역 밖에 적재됩니다. BL31 점프 후 바로 hang입니다.
#CONFIG_SPL_TEXT_BASE와 실제 SRAM 주소 불일치
defconfig의 CONFIG_SPL_TEXT_BASE는 BootROM이 SPL을 적재할 SRAM 주소입니다. SoC 데이터시트와 다르면 SPL이 시작도 못 합니다.
#x0에 DTB 가상 주소를 넘김
BL33 또는 Linux 진입 시 x0는 반드시 physical address입니다. MMU가 off이므로 가상 주소가 의미 없습니다. SPL이 booti 직전에 MMU를 반드시 끄도록 합니다.
#SPL의 console init 누락
SPL이 preloader_console_init()을 호출하기 전에 어딘가에서 hang하면 시리얼에 아무것도 안 나옵니다. 보드 코드 첫 줄에 DEBUG_UART로 ‘A’를 강제 송출해 path를 검증합니다.
#정리
- 부트는 작은 단계가 큰 다음 단계를 RAM에 적재하는 수직 인계의 연쇄입니다.
- ARMv8-A의 표준은 BootROM → BL1 → BL2 → BL31 → BL33입니다. U-Boot의 SPL/TPL 모델은 BL2 자리를 SPL이 대신할 수 있습니다.
- 각 단계는 별도 binary이고 별도 프로젝트에서 빌드됩니다. TF-A는 BL1·BL2·BL31, U-Boot은 SPL·BL33을 만듭니다.
- 권한 수준은 부트 진행에 따라 내려갑니다. BL1~BL31은 EL3 secure, BL33은 EL2 non-secure, Linux는 EL2 → EL1.
- BL31은 runtime firmware입니다. 부팅 후에도 메모리에 살아 있고 PSCI/SMC 호출에 응답합니다.
- 단계 간 인계는 x0에 DTB physical address가 일관된 ABI입니다.
- FIT image는 BL31·BL33·DTB를 한 파일에 묶어 SPL이 한 번에 적재하게 합니다.
- 부트 로그의 prefix(
U-Boot SPL,NOTICE: BL31,U-Boot)로 어느 단계가 출력 중인지 식별합니다.
#다음 편
Ch 5: Falcon Mode — SPL이 커널을 직접 부팅에서는 U-Boot Proper를 건너뛰는 양산 옵션을 다룹니다. sub-second boot이 필요한 시스템에서 SPL이 직접 커널을 부트하는 흐름을 봅니다.
#관련 항목
Bootloader Internals · 4 of 37
- 1ROM부터 init까지 — 임베디드 부팅 단계의 빈자리 분석
- 2Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교
- 3U-Boot 빌드 시스템 분석 — Kconfig·Makefile·defconfig 동작 추적
- 4ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할
- 5U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅
- 6Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적
- 7U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조
- 8U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유
- 9DDR Controller 프로그래밍과 PHY Training — SPL의 가장 어려운 작업
- 10임베디드 스토리지 부팅 분석 — MMC·SCSI·NAND·SPI Flash 비교
- 11임베디드 네트워크 부팅 — TFTP·PXE·BOOTP 시퀀스 분석
- 12U-Boot USB 부팅 — fastboot·UMS·USB host 메커니즘
- 13U-Boot 환경 변수와 bootcmd — 부팅 시나리오 정의하기
- 14Modern U-Boot bootflow / bootmeth — 새 추상화 레이어 분석
- 15FIT image 구조 분석 — multi-image·hash·configuration 추적
- 16U-Boot Verified Boot — RSA 서명과 public key 임베딩 흐름
- 17임베디드 A/B 부팅 이중화 — OTA 안전성을 위한 부트 슬롯 설계
- 18U-Boot의 EFI 호환 분석 — bootefi 명령과 EFI loader 동작 원리
- 19Linux Boot ABI — ARM/ARM64 커널 진입 규약 추적
- 20임베디드 펌웨어 업데이트 — RAUC vs SWUpdate 비교
- 21새 보드 U-Boot 포팅 실전 — defconfig 작성부터 첫 부팅까지
- 22부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석
- 23SoC BootROM·eFuse·OTP — 부팅의 0단계 분석
- 24SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
- 25ARM Trusted Firmware-A 통합 — BL1·BL2·BL31·BL32·BL33 흐름
- 26DDR Training과 PHY Calibration — 보드별 파라미터 튜닝
- 27임베디드 Chain of Trust — 다단계 서명 검증의 전체 흐름
- 28임베디드 Flash Layout 설계 — partition·NAND·eMMC·UBI 비교
- 29U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
- 30부트로더 CI 구축 — build matrix와 자동 부팅 테스트
- 31TF-A BL31 EL3 Runtime 분석 — PSCI·SDEI·RAS dispatcher 추적
- 32PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석
- 33ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지
- 34U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
- 35EFI·UEFI에서 CXL 초기화 — CEDT 생성과 HDM Decoder 사전 설정
- 36부트 시 메모리 토폴로지 결정 — DDR + CXL.mem 통합 인식
- 37UEFI Secure Boot 인증서 만료 — 2011→2023 CA 롤오버와 PQC 대비
관련 글
SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
SPL과 TPL의 정확한 역할, SRAM 안에 들어가는 코드 구조, DDR이 없는 환경에서 어떻게 동작하는가.
Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교
임베디드 부트로더 생태계 — Das U-Boot, ARM Trusted Firmware, EDK II의 역할 분담.
U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
U-Boot PCIe 열거 과정 — Root Complex 초기화·Config Space scan·BAR sizing·resource 할당, CXL DVSEC 인식까지.
이 글을 참조하는 글 (9)
- ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지— Bootloader Internals
- PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석— Bootloader Internals
- TF-A BL31 EL3 Runtime 분석 — PSCI·SDEI·RAS dispatcher 추적— Bootloader Internals
- SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적— Bootloader Internals
- U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유— Bootloader Internals
- U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅— Bootloader Internals
- U-Boot 빌드 시스템 분석 — Kconfig·Makefile·defconfig 동작 추적— Bootloader Internals
- Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교— Bootloader Internals
- ROM부터 init까지 — 임베디드 부팅 단계의 빈자리 분석— Bootloader Internals