QEMU(arm64) 임베디드 리눅스 타깃 만들기 (2) — U-Boot 올리기

1편에서 만든 디스크 이미지는 아직 아무도 읽어주지 않는다. virt 머신에는 x86의 BIOS/UEFI 같은 내장 펌웨어가 없어서, 전원을 넣어도 실행할 코드가 없으면 한 글자도 나오지 않기 때문이다. 실물 보드라면 SoC의 부트 ROM이 SD 카드 앞부분에서 부트로더를 읽어오는 자리인데, QEMU에서는 그 자리를 -bios 옵션으로 직접 채운다. 이 글에서는 U-Boot을 arm64로 크로스 빌드해 타깃에 올리고, U-Boot이 우리가 만든 디스크의 파티션을 읽어 부트 스크립트를 실행하는 데까지 확인한다.

이 시리즈의 다른 글

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_r0x40400000커널 이미지를 올릴 주소
ramdisk_addr_r0x44000000initramfs를 올릴 주소
fdt_addr_r없음DTB를 따로 올릴 주소 — virt에는 없다
fdtcontroladdr0x7e55bdc0QEMU가 넘겨준 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.scrmkimage로 헤더를 붙여야 한다. 평문 파일을 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로 올린다.

참고

답글 남기기