공식 bootchain (qemu-virt, rock5b+)
ref
- TF-A 부팅 프로세스(공식)
- NuMicro TF-A 매뉴얼
- https://docs.radxa.com/en/rock5/rock5b
- Rock5B+ 이미지 다운로드 링크
- RKDevTool 다운로드 링크
- qemu 구조 설명
이 글의 목표
부팅을 하기위해서는 TF-A 포맷에 맞춰서 qemu와 rock5b+의 부트체인에 맞게 이미지를 배치해야한다. 각각의 부팅단계에 대해 설명하고, 어떤 위치에 플래시해야 부팅을 성공시킬 수 있는지 알아보는 것이 목표이다.
ARM64 기반 운영체제를 구현하기 위한 개발 보드로 Rock5B+를 선택했다.
공식문서나 커뮤니티가 잘 되어있고, 다양한 오픈소스 운영체제를 지원하기 때문에 부트체인부터 커널, 플랫폼 전반에 대해 참고할 수 있는 레퍼런스가 많을 것이라 판단했다.
프로젝트는 Android 와 비슷한 OS의 구현을 직접 해보며 아래의 목표들을 달성하려 한다.
- 부트로더부터 커널 영역. 메모리 할당 구조를 원하는 구조로 작성
- OP-TEE 지원
- 사용자 플랫폼 구조 (안드로이드의 Zygote나 샌드박스 등 방법을 차용)
- 그래픽 드라이버를 통한 GPU 가속 지원
- QEMU의 일부 지원
Rock5B+는 QEMU가 지원되지 않는데, 개발 생산성을 높이기 위해 QEMU를 보드에 맞게 수정하거나 커널 영역이라도 QEMU에서 돌릴 수 있도록 분리해서 구현해야한다.
이 글에서는 Rock5B+을 전체적으로 직접 빌드해서 실행해보며, QEMU를 위한 코드분리, 디버깅을 할 수 있는 방법 등을 찾아보는 것을 목표로 할것이다.
ARM Trusted Firmware-A 부트체인
ARM에서는 생각보다 많은 일을 하고있다. 칩과 명령어만 설계하는 것이 아니라 칩을 어떻게 사용할지에 대한 규격까지 설계하며, 그에 맞는 레퍼런스 구현까지 담당한다.
TF-A는 ARM이 정의한 ARMv8-A TrustZone 기반 시큐어부트 구조를 실제 코드로 구현한 공식 오픈소스 레퍼런스이다.
사실 퀄컴, 삼성, 미디어텍 같은 벤더들은 독자적인 시큐어 펌웨어를 직접 구현해서 사용하는데 세부적인 단계나 이름은 다르지만 ARM이 강제한 EL3, TrustZone 구조 때문에 개념적으로는 TF-A와 동일하다.
정확히 말하면 TF-A(시큐어부트) 개념과 하드웨어의 부트체인(클럭, DDR 초기화 등)은 분리되어 있다.
Rockchip은 보안을 위해 TF-A 구조를 따라가고 있지만, DDR 초기화 등은 시큐어부트 사양이 아니기 때문에 TF-A 구조 사이에 알아서 끼워넣어야 비로소 호환이 되는것이다.
-
BL1(BootROM) : TF-A에서 정의한 첫번째 SecureBoot 단계로, BL2를 검증하고 SRAM에 로드하는 역할을 한다. Rockchip을 포함한 대부분의 SoC에서 BL1의 역할도 담당한다.
SRAM은 SoC 내부의 작은 램이며 클럭, DDR 초기화 없이 전원 인가 즉시 사용 가능하다. -
BL2(idbloader) : SRAM에서 실행되어 BL31, BL32(옵션), BL33 바이너리 이미지들을 메모리에 로드하고 검증한뒤 BL31에게 파라미터를 전달한다.
rock5b+ 공식 u-boot 부트체인
Rock5B+은 전원이 켜지면 BootROM 역할을 하는 MaskROM 코드가 실행된다.
MaskROM은 SPI Flash → eMMC → SD Card 순서로 부트로더를 찾아보고 실행하게 된다. 없다면 RKDevTool의 RockUSB 프로토콜을 기다리며 부트로더를 필요한 위치에 작성할 수 있게 된다.
SPI Flash 메모리는 16MB로 크기가 작기 때문에 보통 SSD(NVME)에 운영체제를 설치하게 된다. SPI를 먼저 확인하기 때문에 eMMC나 SD카드로 부팅하려면 SPI를 전부 지워줘야 한다.
이 그림이 rock5b+ 에 완벽하게 해당되는 그림은 아니지만, 크게 변하지 않았기 때문에 그림에 나온 주소에 맞춰서 플래시하면 된다.
참고로 여기에서의 주소는 섹터의 번호를 말하기 때문에 * 512 를 해야 실제 주소가 된다.
spi-image.img 는 내부에 idbloader.img 가 패키징되어 있는데, 0x0 의 주소에 플래시된다. idbloader.img 를 직접 플래시할땐 0x40에 플래시하는데 spi-image.img의 0x8000 위치에서 idbloader.img 의 바이트패턴을 볼 수 있다.
idbloader.img
가장 먼저 실행되는 부트로더 코드이며, SPI Flash나 저장장치(eMMC, SDCard)의 첫부분에 위치해있다. DDR 램을 초기화(TPL:Tiny Program Loader)하고, ATF와 U-Boot를 메모리에 로드(SPL:Secondary Program Loader) 후 점프하는 역할을 한다.
플래시 해보기
SPI Flash
- 보드의
마스크롬 버튼누른 상태에서 전원연결. 진입 후 RKDevTool 을 사용하고 usb를 PC와 연결하면 MaskROM 기기가 인식된다.
보드에 맞는 SPL_Loader와 spi image를 다운로드하고, Loader와 Image를 지정한 후 Wire by Address 체크해서 플래시하면 다운받은 spi image가 플래시된다.
MaskROM은 혼자서 플래시하는 역할을 못하기 때문에 로더를 먼저 올려서 DDR 램 초기화나 컨트롤러 초기화 후 플래시해주는 역할을 한다.
보통은 SPINOR 영역에 플래시할땐 SPL Loader를 사용해도 되지만 eMMC 초기화까지 해주지는 않기 때문에 eMMC 플래시는 MiniLoader를 사용해야한다.
1. SPINOR 전부 지우기
ld 나 db 명령 같은건 MaskROM 코드로도 기본적으로 사용할 수 있다.
spl_loader나 miniloader를 램에 올려서 플래시메모리나 spinor 메모리를 읽고 쓸 수 있게 된다.
16MB 16*1024*1024 byte 크기의 이미지파일에 0을 채운 zero.img를 spinor에 플래시해도 된다.
SPI 영역에 부트로더가 설치되어 있다면 eMMC에 운영체제를 설치해도 찾아가지 않게되기 때문에 지우는 것이다.
rkdeveloptool db spl_loader.img # 얘는 emmc가 선택되어 있어도 spinor만 지울수있음
rkdeveloptool ef # 이때 버그로 지우는 sector 수가 emmc크기로 보일 수 있다.
# or
rkdeveloptool db miniloader.img
rkdeveloptool rfi # 1: 초기엔 eMMC 선택됨
# Flash Info:
# Manufacturer: SAMSUNG, value=00
# Flash Size: 59640 MB
# Flash Size: 122142720 Sectors
# Block Size: 512 KB
# Page Size: 2 KB
# ECC Bits: 0
# Access Time: 40
# Flash CS: Flash<0>
rkdeveloptool cs 9 # 9: spinor
rkdeveloptool rfi
# Flash Info:
# Manufacturer: SAMSUNG, value=00
# Flash Size: 16 MB
# Flash Size: 32768 Sectors
# Block Size: 64 KB
# Page Size: 2 KB
# ECC Bits: 0
# Access Time: 40
# Flash CS: Flash<0>
rkdeveloptool ef
2. TF-A(BL31) 빌드
git clone --depth 1 https://github.com/TrustedFirmware-A/trusted-firmware-a.git
cd trusted-firmware-a
make realclean
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=rk3588
cd ..
3. u-boot 빌드
u-boot 필수 패키지 설치
sudo apt install -y \
build-essential git bc \
bison flex \
device-tree-compiler \
libssl-dev \
python3 python3-pip swig python3-dev \
libgnutls28-dev \
libncurses5-dev libncursesw5-dev \
uuid-dev
위에서 빌드한 TF-A(BL31)을 대신 사용해도 될것이다.
git clone --depth 1 https://github.com/rockchip-linux/rkbin
git clone --depth 1 https://source.denx.de/u-boot/u-boot.git
cd u-boot
export BL31=../rkbin/bin/rk35/rk3588_bl31_v1.51.elf # 버전 확인해야함.
export ROCKCHIP_TPL=../rkbin/bin/rk35/rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.19.bin
make distclean # 이전 빌드설정 정리
make evb-rk3588_defconfig
make CROSS_COMPILE=aarch64-linux-gnu-
여기에서 빌드 후 폴더에 있는 idbloader.img, u-boot.itb 파일을 사용한다.
4. 플래시
윈도우보다 맥에서 rkdeveloptool을 사용하는 것이 더 좋다.
brew install automake autoconf libusb pkg-config git wget
git clone https://github.com/rockchip-linux/rkdeveloptool
cd rkdeveloptool
autoreconf -i
./configure
make -j $(nproc)
cp rkdeveloptool /opt/homebrew/bin/
rkdeveloptool -V
# 설치 이후 clone한 rkdeveloptool 폴더 삭제
maskrom 모드로 진입 후 rkdeveloptool 명령 사용해서 플래시한다.
u-boot를 플래시해야되는 섹터는 common/spl/Kconfig 파일의 SYS_MMCSD_RAW_MODE_U_BOOT_SECTOR 설정으로 결정된다.
evb-rk3588_defconfig 설정 파일에 CONFIG_ARCH_ROCKCHIP=y 로 설정되어 있고, Kconfig를 확인해보면 default 0x4000 if ARCH_ROCKCHIP 로 되어있어서 RAW 모드일때는 0x4000에 설정하면 된다.
# 연결된 기기 확인
rkdeveloptool ld
# miniloader 로드
sudo rkdeveloptool db miniloader.img
# eMMC 영역 전부 비우기
sudo rkdeveloptool ef
# eMMC에 raw 데이터 주소 맞춰서 쓰기 (idbloader는 eMMC 0x40에 위치)
sudo rkdeveloptool wl 0x40 idbloader.img
# u-boot.itb는 eMMC 0x4000에 위치
sudo rkdeveloptool wl 0x4000 u-boot.itb
bsp에서 rock-5b-plus-rk3588_defconfig 를 보면 아래 설정이 되어있는데, 이건 u-boot를 GPT 첫번째 파티션에서 로드한다는 의미이기 때문에 RAW 모드로 동작하지 않는다.
CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_USE_PARTITION=y
CONFIG_SYS_MMCSD_RAW_MODE_U_BOOT_PARTITION=0x1
5. UART 출력확인
기기에서 6(GND), 8(TX), 10(RX) 번 핀에 uart usb의 GND, RXD, TXD 순서로 연결하면 된다.
devmgmt.msc 에서 COM 포트번호를 찾고 putty로 serial 1500000 으로 접속하면 된다.
기본적 u-boot 부팅으로 접속하면 => 터미널까지 들어갈 수 있다.
...
Net: eth0: ethernet@fe1b0000
Hit any key to stop autoboot: 0
=> version
U-Boot 2026.01-gff498a3c5efb (Jan 18 2026 - 16:19:16 +0900)
aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
GNU ld (GNU Binutils for Ubuntu) 2.38
=>
uboot 코드에서 UBOOT_STOP_EARLY 출력하고 hang 했더니 UART에서 문자열 출력 후 멈춰있는 것을 확인할 수 있다.
// u-boot/common/board_r.c
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
puts("UBOOT_STOP_EARLY\n");
hang();
EDK2 부트체인
rock5b+
edk2-rk3588 프로젝트는 빌드스크립트가 있어서 쉽게 빌드할 수 있다.
_build_idblock → _build_fit → _pack_image 단계를 거쳐서 RK3588_NOR_FLASH.img 이미지를 만들고있다.
# _build()
# ├─ TF-A 빌드 → BL31
# ├─ EDK2 build → BL33 = BL33_AP_UEFI.Fv
# └─ _pack_image
# ├─ _build_idblock (BL2)
# ├─ _build_fit
# │ ├─ BL31 추출
# │ ├─ BL32 복사
# │ ├─ BL33_AP_UEFI.Fv 복사
# │ └─ FIT(itb) 생성
# └─ spi_nor_gpt.img + BL2 + FIT = ⭐ RK3588_NOR_FLASH.img
sudo apt install git gcc g++ build-essential gcc-aarch64-linux-gnu \
acpica-tools python3-pyelftools uuid-dev python-is-python3 \
device-tree-compiler
git clone https://github.com/edk2-porting/edk2-rk3588.git --recursive
cd edk2-rk3588
./build.sh --device rock-5b --release Release # (or Debug)
# Build done: RK3588_NOR_FLASH.img
sudo rkdeveloptool db spl_loader.img
sudo rkdeveloptool ef
sudo rkdeveloptool wl 0x0 RK3588_NOR_FLASH.img
_build_idblock (BL2)
idbloader는 하드웨어에 의존적인 DDRInit 작업을 해야하기 때문에 빌드되어 배포되는 이미지를 사용한다.
u-boot의 mkimage 툴을 사용해서 idbloader.bin 을 만들어낸다.
DDRBIN=bin/rk35/rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.18.bin
SPL=bin/rk35/rk3588_spl_v1.13.bin
mkimage -n rk3588 -T rksd -d ${DDRBIN}:${SPL} idbloader.bin
_build_fit (BL31, BL32, BL33)
TF-A, OP-TEE, UEFI 를 빌드하고 its 파일을 참고해서 u-boot에서 사용하는 컨테이너인 FIT(Flat Image Tree) 이미지로 만드는 과정이다.
각각의 코드에 포팅을 위한 패치셋을 적용 후 빌드한다.
# BL31(TF-A) build (최신커밋 하나만 가져옴)
git clone --depth 1 https://github.com/ARM-software/arm-trusted-firmware.git
apply_patchset ./arm-trusted-firmware-patches ./arm-trusted-firmware
cd arm-trusted-firmware
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=rk3588 DEBUG=0 all ${TFA_FLAGS}
cd ..
git clone https://github.com/OP-TEE/optee_os
cd optee_os
make \
CFG_ARM64_core=y \
CFG_TEE_CORE_LOG_LEVEL=3 \
DEBUG=1 \
O=out/arm64 \
PLATFORM=rockchip-rk3588
cd ..
make -C "./edk2/BaseTools"
source "./edk2/edksetup.sh"
build \
-a AARCH64 \
-t "${TOOLCHAIN}" \
-p "${ROOTDIR}/${DSC_FILE}" \
-b "${RELEASE_TYPE}" \
-D NETWORK_ALLOW_HTTP_CONNECTIONS=TRUE \
-D NETWORK_ISCSI_ENABLE=TRUE \
-D INCLUDE_TFTP_COMMAND=TRUE \
--pcd gRockchipTokenSpaceGuid.PcdFitImageFlashAddress=0x100000 \
${EDK2_FLAGS}
cd ..
BL31=../arm-trusted-firmware/build/rk3588_reference_pmic/release/bl31/bl31.elf
# BL31을 its가 필요로하는 단위로 분해함
/misc/extractbl31.py ${BL31}
BL32=../optee_os/out/arm64/core/tee-raw.bin
BL33=../Build/FV/BL33_AP_UEFI.Fv
cat ./misc/uefi_rk3588.its | sed "s,@DEVICE@,rock-5bp,g" > rock-5bp_EFI.its
# its 파일에 포함된 바이너리들의 메모리 로드 정보를 포함시킨 itb를 만든다.
# BL2의 로더가 이 itb를 파싱해서 load 메모리 주소에 바이너리를 각각 로드하고 BL31로 점프한다.
# BL31은 its파일의 firmware 에 적힌 atf-1 이며, extract로 꺼낸 bl31_0x00040000.bin 이녀석이다.
mkimage -f rock-5bp_EFI.its -E rock-5bp_EFI.itb
_pack_image (이미지 합치기)
적당한 위치에 합쳐서 하나의 이미지를 만든다.
그리고 이 이미지는 idbloader도 포함되어 있기 때문에 0x0 주소에 플래시하면 동작한다.
# GPT at 0x0, size:0x4400
dd if=${ROOTDIR}/misc/rk3588_spi_nor_gpt.img of=${WORKSPACE}/RK3588_NOR_FLASH.img
# idbloader at 0x8000
dd if=${WORKSPACE}/idbloader.bin of=${WORKSPACE}/RK3588_NOR_FLASH.img bs=1K seek=32
# FIT Image at 0x100000
dd if=${WORKSPACE}/${DEVICE}_EFI.itb of=${WORKSPACE}/RK3588_NOR_FLASH.img bs=1K seek=1024
qemu-virt
필요한 패키지 설치
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf
EDK 빌드
git clone https://github.com/tianocore/edk2.git
cd edk2
git submodule update --init
make -C BaseTools
source edksetup.sh
export GCC5_AARCH64_PREFIX=aarch64-linux-gnu-
build -a AARCH64 -t GCC5 -p ArmVirtPkg/ArmVirtQemuKernel.dsc
OP-TEE 빌드
git clone https://github.com/OP-TEE/optee_os
cd optee_os
make \
CFG_ARM64_core=y \
CFG_TEE_CORE_LOG_LEVEL=3 \
CROSS_COMPILE=aarch64-linux-gnu- \
CROSS_COMPILE_core=aarch64-linux-gnu- \
CROSS_COMPILE_ta_arm32=arm-linux-gnueabihf- \
CROSS_COMPILE_ta_arm64=aarch64-linux-gnu- \
DEBUG=1 \
O=out/arm64 \
PLATFORM=vexpress-qemu_armv8a
TF-A에 사용할 빌드된 이미지 파일들 확인하기
kdh@DESKTOP-MHEA7GE:~/qemu_test/arm-trusted-firmware$ ls -la ../optee_os/out/arm64/core | grep "tee"
-rw-r--r-- 1 kdh kdh 4806 Jan 11 00:26 .tee.elf.cmd
drwxr-xr-x 2 kdh kdh 4096 Jan 11 00:26 tee
-rw-r--r-- 1 kdh kdh 28 Jan 11 00:26 tee-header_v2.bin
-rw-r--r-- 1 kdh kdh 0 Jan 11 00:26 tee-pageable_v2.bin
-rw-r--r-- 1 kdh kdh 812040 Jan 11 00:26 tee-pager_v2.bin
-rw-r--r-- 1 kdh kdh 812040 Jan 11 00:26 tee-raw.bin
-rw-r--r-- 1 kdh kdh 812068 Jan 11 00:26 tee.bin
-rw-r--r-- 1 kdh kdh 9789703 Jan 11 00:26 tee.dmp
-rwxr-xr-x 1 kdh kdh 4459240 Jan 11 00:26 tee.elf
-rw-r--r-- 1 kdh kdh 3526674 Jan 11 00:26 tee.map
-rw-r--r-- 1 kdh kdh 207048 Jan 11 00:26 tee.symb_sizes
kdh@DESKTOP-MHEA7GE:~/qemu_test/arm-trusted-firmware$ ls -la ../edk2/Build/ArmVirtQemuKernel-AArch64/DEBUG_GCC5/FV/ | grep QEMU
-rw-r--r-- 1 kdh kdh 3145728 Jan 10 22:53 QEMU_EFI.fd
-rw-r--r-- 1 kdh kdh 786432 Jan 10 22:53 QEMU_VARS.fd
https://trustedfirmware-a.readthedocs.io/en/stable/plat/qemu.html#booting-via-flash-based-firmware
문서에서는 이렇게 정의되어 있음.
BL32 - bl32.bin -> tee-header_v2.bin
BL32 Extra1 - bl32_extra1.bin -> tee-pager_v2.bin
BL32 Extra2 - bl32_extra2.bin -> tee-pageable_v2.bin
BL33 - bl33.bin -> QEMU_EFI.fd (EDK II)
Image -> linux/arch/arm64/boot/Image
TF-A 빌드
git clone https://github.com/ARM-software/arm-trusted-firmware.git
cd arm-trusted-firmware
# make CROSS_COMPILE=aarch64-linux-gnu- PLAT=qemu BL32=bl32.bin \
# BL32_EXTRA1=bl32_extra1.bin BL32_EXTRA2=bl32_extra2.bin \
# BL33=bl33.bin BL32_RAM_LOCATION=tdram SPD=opteed all fip
# 방금 빌드한 파일들로 변경
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=qemu \
BL32=../optee_os/out/arm64/core/tee-header_v2.bin \
BL32_EXTRA1=../optee_os/out/arm64/core/tee-pager_v2.bin \
BL32_EXTRA2=../optee_os/out/arm64/core/tee-pageable_v2.bin \
BL33=../edk2/Build/ArmVirtQemuKernel-AArch64/DEBUG_GCC5/FV/QEMU_EFI.fd \
BL32_RAM_LOCATION=tdram SPD=opteed all fip
# 붙이기
dd if=build/qemu/release/bl1.bin of=flash.bin bs=4096 conv=notrunc
dd if=build/qemu/release/fip.bin of=flash.bin seek=64 bs=4096 conv=notrunc
만들어진 이미지 확인
kdh@DESKTOP-MHEA7GE:~/qemu_test/arm-trusted-firmware$ ./tools/fiptool/fiptool info build/qemu/release/fip.bin
Trusted Boot Firmware BL2: offset=0x128, size=0x6418, cmdline="--tb-fw"
EL3 Runtime Firmware BL31: offset=0x6540, size=0xC067, cmdline="--soc-fw"
Secure Payload BL32 (Trusted OS): offset=0x125A7, size=0x1C, cmdline="--tos-fw"
Secure Payload BL32 Extra1 (Trusted OS Extra1): offset=0x125C3, size=0xC6408, cmdline="--tos-fw-extra1"
Non-Trusted Firmware BL33: offset=0xD89CB, size=0x300000, cmdline="--nt-fw"
qemu-virt 실행
qemu-system-aarch64 -nographic \
-machine virt,secure=on \
-cpu cortex-a57 \
-smp 2 -m 1024 \
-bios flash.bin \
-serial mon:stdio \
-serial file:optee.log