1편에서 만든 디스크 이미지는 아직 아무도 읽어주지 않는다. virt 머신에는 x86의 BIOS/UEFI 같은 내장 펌웨어가 없어서, 전원을 넣어도 실행할 코드가 없으면 한 글자도 나오지 않기 때문이다. 실물 보드라면 SoC의 부트 ROM이 SD 카드 앞부분에서 부트로더를 읽어오는 자리인데, QEMU에서는 그 자리를 -bios 옵션으로 직접 채운다. 이 글에서는 U-Boot을 arm64로 크로스 빌드해 타깃에 올리고, U-Boot이 우리가 만든 디스크의 파티션을 읽어 부트 스크립트를 실행하는 데까지 확인한다.
이 시리즈의 다른 글
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (1) — 타깃 디스크 이미지와 virt 보드 구성
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (3) — 커널 크로스 컴파일과 부팅
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (4) — 루트 파일 시스템과 패키지 설치
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (5) — Yocto로 타깃 이미지 빌드
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (6) — 커널 모듈, gdb 원격 디버깅, NFS 루트
U-Boot 크로스 빌드
U-Boot은 QEMU의 virt 머신용 defconfig를 이미 가지고 있다. 보드 이식 작업 없이 qemu_arm64_defconfig 하나면 된다.
sudo apt install -y bison flex libssl-dev swig python3-dev libgnutls28-dev bc
cd ~/qemu-target
wget https://ftp.denx.de/pub/u-boot/u-boot-2026.01.tar.bz2
tar xf u-boot-2026.01.tar.bz2 && cd u-boot-2026.01
export CROSS_COMPILE=aarch64-linux-gnu- ARCH=arm
make qemu_arm64_defconfig
make -j$(nproc)
ls -l u-boot.bin-rw-rw-r-- 1 noble noble 1444512 Aug 25 23:09 u-boot.bin
ARCH=arm이 맞다. arm64도 U-Boot에서는 arch/arm 아래에 있어서 ARCH=arm64로 주면 defconfig를 찾지 못한다.
-bios로 부트로더 올리기
1편의 run-target.sh에 -bios 한 줄만 더하면 된다.
#!/bin/bash
set -e
IMG=${IMG:-work.qcow2}
BIOS=${BIOS:-u-boot-2026.01/u-boot.bin}
exec qemu-system-aarch64 \
-M virt -cpu cortex-a57 -m 1G -smp 2 \
-bios "$BIOS" \
-drive if=none,file="$IMG",format=qcow2,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-device virtio-net-device,netdev=net0 \
-nographic "$@"U-Boot 2026.01 (Aug 25 2026 - 23:08:32 +0000)
DRAM: 1 GiB
using memory 0x7e65c000-0x7f69c000 for malloc()
Core: 51 devices, 14 uclasses, devicetree: board
Flash: 64 MiB
Loading Environment from Flash... *** Warning - bad CRC, using default environment
In: serial,usbkbd
Out: serial,vidconsole
Err: serial,vidconsole
No USB controllers found
Net: eth0: virtio-net#30
Hit any key to stop autoboot: 0
=>
1편에서 붙여둔 virtio 장치를 U-Boot이 그대로 잡았다. DRAM 1 GiB는 -m 1G가, eth0: virtio-net은 -device virtio-net-device가 반영된 결과다.
U-Boot이 보는 보드
=> bdinfo
boot_params = 0x0000000000000000
DRAM bank = 0x0000000000000000
-> start = 0x0000000040000000
-> size = 0x0000000040000000
flashstart = 0x0000000000000000
flashsize = 0x0000000004000000
baudrate = 115200 bps
relocaddr = 0x000000007f69c000
current eth = virtio-net#30
ethaddr = 52:54:00:12:34:56
fdt_blob = 0x000000007e55bdc0
devicetree = board
serial addr = 0x0000000009000000
RAM이 0x40000000부터 1GiB, 시리얼이 0x9000000이다. 1편에서 dumpdtb로 꺼내본 memory@40000000·pl011@9000000과 정확히 같은 값이고, devicetree = board는 QEMU가 넘겨준 디바이스 트리를 U-Boot이 그대로 쓰고 있다는 뜻이다. 그래서 QEMU 옵션을 바꾸면 U-Boot 재빌드 없이 구성이 따라온다.
디스크 읽기
=> virtio info
Device 0: QEMU VirtIO Block Device
Type: Hard Disk
Capacity: 1024.0 MB = 1.0 GB (2097152 x 512)
=> part list virtio 0
Partition Map for virtio device 0 -- Partition Type: DOS
Part Start Sector Num Sectors UUID Type
1 2048 131072 bd21d4b6-01 0c Boot
2 133120 1964032 bd21d4b6-02 83
1편에서 parted로 나눈 파티션 두 개가 그대로 보인다. 부트로더 관점에서 타깃 디스크가 정상이라는 확인이 여기서 끝난다.
부트 스크립트 만들기
부팅 절차를 매번 손으로 치지 않으려면 명령을 boot.scr로 묶어 디스크에 둔다. U-Boot은 평문 스크립트가 아니라 mkimage로 헤더를 붙인 형식을 읽는다.
echo "=== boot.scr running ==="
echo "board: ${board}"
echo "fdt at: ${fdtcontroladdr}"
echo "loaded from virtio 0:1"u-boot-2026.01/tools/mkimage -A arm64 -T script -C none \
-n 'qemu-target boot' -d boot.cmd boot.scr
# 1편의 mount-qcow2.sh로 FAT 파티션에 복사한다
sudo qemu-nbd --connect=/dev/nbd0 work.qcow2
sudo mount /dev/nbd0p1 /mnt/tb
sudo cp boot.scr /mnt/tb/
sudo umount /mnt/tb && sudo qemu-nbd --disconnect /dev/nbd0=> fatls virtio 0:1
11 README
191 boot.scr
2 file(s), 0 dir(s)
=> load virtio 0:1 $kernel_addr_r boot.scr
191 bytes read in 1 ms (186.5 KiB/s)
=> source $kernel_addr_r
## Executing script at 40400000
=== boot.scr running ===
board: qemu-arm
fdt at: 7e55bdc0
loaded from virtio 0:1
부트로더가 우리 디스크에서 코드를 읽어 실행하는 경로가 완성됐다. 다음 편에서는 이 자리에 커널을 올리는 명령이 들어간다.
주소 변수는 미리 확인해둔다
=> printenv kernel_addr_r fdt_addr_r ramdisk_addr_r
kernel_addr_r=0x40400000
## Error: "fdt_addr_r" not defined
ramdisk_addr_r=0x44000000
| 변수 | 값 | 쓰임 |
|---|---|---|
kernel_addr_r | 0x40400000 | 커널 이미지를 올릴 주소 |
ramdisk_addr_r | 0x44000000 | initramfs를 올릴 주소 |
fdt_addr_r | 없음 | DTB를 따로 올릴 주소 — virt에는 없다 |
fdtcontroladdr | 0x7e55bdc0 | QEMU가 넘겨준 DTB의 위치 |
실물 보드용 예제를 그대로 가져오면 fdt_addr_r에서 걸린다. virt에서는 DTB를 파일에서 올릴 필요 없이 QEMU가 이미 메모리에 올려둔 것을 fdtcontroladdr로 가리켜 쓰면 된다.
환경변수를 디스크에 저장하기
qemu_arm64_defconfig는 환경변수를 플래시에 저장하도록 설정돼 있는데, 이 상태로 saveenv를 하면 실패한다.
=> saveenv
Saving Environment to Flash... Un-Protected 2 sectors
Erasing Flash...
.. done
Erased 2 sectors
Writing to Flash... Flash buffer write timeout at address 4000000 data 7e146361
Timeout writing to Flash
Protected 2 sectors
Failed (1)
환경변수 자리(CONFIG_ENV_ADDR=0x4000000)가 두 번째 플래시 뱅크인데 U-Boot이 첫 64MiB 뱅크만 등록해서 생기는 문제다. 실물 보드도 환경변수를 SD 카드에 두는 경우가 많으니, 같은 방식으로 FAT 파티션에 저장하도록 바꾼다.
cd u-boot-2026.01
./scripts/config --disable CONFIG_ENV_IS_IN_FLASH \
--enable CONFIG_ENV_IS_IN_FAT \
--set-str CONFIG_ENV_FAT_INTERFACE virtio \
--set-str CONFIG_ENV_FAT_DEVICE_AND_PART "0:1" \
--set-str CONFIG_ENV_FAT_FILE "uboot.env"
make olddefconfig && make -j$(nproc)Loading Environment from FAT... OK
=> setenv mytest qemu-target
=> saveenv
Saving Environment to FAT... OK
=> reset
resetting ...
Loading Environment from FAT... OK
=> printenv mytest
mytest=qemu-target
리셋 후에도 값이 남아 있다. 이제 부팅 인자나 커널 경로를 타깃 안에서 바꿔두고 재부팅할 수 있다.
주의사항
- U-Boot에서 arm64는
ARCH=arm이다.ARCH=arm64로 주면 defconfig를 찾지 못한다. boot.scr는mkimage로 헤더를 붙여야 한다. 평문 파일을source하면 “Bad magic number”로 거부된다.virt에는fdt_addr_r이 없다. 실물 보드 예제를 복사해 쓰다 보면 여기서 막히므로fdtcontroladdr을 쓴다.-bios로 올린 U-Boot은 플래시에 환경변수를 못 쓴다. FAT에 저장하도록 재빌드하는 편이 실물 보드 구성과도 가깝다.- QEMU를 종료할 때
Ctrl+C는 U-Boot 콘솔로 전달된다.-nographic세션은Ctrl+A에 이어x를 눌러 빠져나온다. - 호스트에서 이미지를
qemu-nbd로 마운트한 채로 QEMU를 띄우면 안 된다. 부트 스크립트를 복사한 뒤에는 반드시--disconnect한다.
마무리
타깃에 부트로더가 올라갔고, 그 부트로더가 우리 디스크의 파티션을 읽어 스크립트를 실행하는 것까지 확인했다. U-Boot이 QEMU에서 받은 디바이스 트리를 그대로 쓰기 때문에 RAM 크기나 장치 구성을 바꿔도 재빌드가 필요 없다는 점도 확인했다. 다음 편에서는 커널을 크로스 컴파일해 boot.scr 안에서 booti로 올린다.