QEMU raspi4b로 라즈베리파이 4 에뮬레이션하기 — 되는 것과 안 되는 것

라즈베리파이 4용으로 뭔가를 고칠 때마다 보드에 SD 카드를 꽂았다 뺐다 하는 건 번거롭고, 보드가 손에 없으면 아예 막힌다. QEMU는 8.2까지 라즈베리파이 3B(raspi3b)가 최신이었는데 9.0부터 raspi4b 머신이 들어왔다. 다만 Pi 4는 이더넷과 USB가 모두 PCIe 뒤에 붙는 구조라 에뮬레이션되는 범위가 Pi 3과 꽤 다르다. 이 글에서는 우분투 기본 패키지로는 왜 안 되는지부터 시작해 raspi4b로 라즈베리파이 OS를 로그인 프롬프트까지 올리고, 실제로 무엇이 되고 무엇이 안 되는지를 게스트 안에서 하나씩 확인한다.

우분투 기본 QEMU에는 raspi4b가 없다

$ qemu-system-aarch64 -M help | grep -i raspi
raspi0     Raspberry Pi Zero (revision 1.2)
raspi1ap   Raspberry Pi A+ (revision 1.1)
raspi2b    Raspberry Pi 2B (revision 1.1)
raspi3ap   Raspberry Pi 3A+ (revision 1.0)
raspi3b    Raspberry Pi 3B (revision 1.2)

$ apt-cache policy qemu-system-arm | head -3
qemu-system-arm:
  Installed: 1:8.2.2+ds-0ubuntu1.18
  Candidate: 1:8.2.2+ds-0ubuntu1.18

Ubuntu 24.04는 QEMU 8.2.2를 제공하고 업데이트 저장소에도 그 버전뿐이라 raspi4b가 아예 없다. 소스로 빌드하는 수밖에 없는데, aarch64 타깃 하나만 켜면 6코어 기준 10분 남짓이면 끝난다.

sudo apt install -y ninja-build meson pkg-config libglib2.0-dev \
    libpixman-1-dev libslirp-dev python3-venv zlib1g-dev

mkdir -p ~/qemu-build && cd ~/qemu-build
wget https://download.qemu.org/qemu-11.1.0.tar.xz
tar xf qemu-11.1.0.tar.xz && cd qemu-11.1.0
./configure --target-list=aarch64-softmmu --prefix=$HOME/qemu-11 --disable-docs
make -j$(nproc)

./build/qemu-system-aarch64 -M help | grep raspi4b
raspi4b              Raspberry Pi 4B (revision 1.5)

머신 이름 뒤의 revision 1.5는 2GB 모델을 뜻하고, 이 값이 뒤에서 메모리 크기 제약으로 이어진다.

이미지와 부팅 파일 준비

라즈베리파이 OS Lite(arm64) 이미지를 받아 압축을 풀고, QEMU의 SD 카드 모델이 요구하는 2의 거듭제곱 크기로 늘린다. 커널과 DTB는 이미지의 부트 파티션에서 직접 꺼내 쓰는 것이 가장 확실하다.

#!/bin/bash
set -e
wget -O raspios-lite-arm64.img.xz \
    https://downloads.raspberrypi.com/raspios_lite_arm64_latest
xz -dk -T0 raspios-lite-arm64.img.xz
qemu-img resize -f raw raspios-lite-arm64.img 4G

L=$(sudo losetup -f --show -P raspios-lite-arm64.img)
sudo mount "${L}p1" /mnt/rb

cp /mnt/rb/kernel8.img /mnt/rb/bcm2711-rpi-4-b.dtb .
cp /mnt/rb/overlays/disable-bt.dtbo .
# 첫 부팅 때 만들 계정 (userconf.txt가 없으면 로그인 자체가 불가능하다)
printf 'noble:%s\n' "$(openssl passwd -6 raspberry)" | sudo tee /mnt/rb/userconf.txt

sudo umount /mnt/rb && sudo losetup -d "$L"

이미지를 4G로 늘리지 않으면 SD 카드 모델이 크기를 거부한다. 이 제약은 raspi3b에서와 똑같아서 QEMU(arm64)에 라즈베리파이 올리기에서 정리한 준비 과정이 대부분 그대로 통한다.

첫 시도 — 콘솔이 조용하다

~/qemu-build/qemu-11.1.0/build/qemu-system-aarch64 -M raspi4b -m 2G \
    -kernel kernel8.img -dtb bcm2711-rpi-4-b.dtb \
    -drive file=raspios-lite-arm64.img,format=raw,if=sd \
    -display none -serial stdio \
    -append "console=ttyAMA0,115200 root=/dev/mmcblk1p2 rootfstype=ext4 rw rootwait"
qemu-system-aarch64: warning: bcm2711 dtb: brcm,bcm2711-pcie has been disabled!
qemu-system-aarch64: warning: bcm2711 dtb: brcm,bcm2711-rng200 has been disabled!
qemu-system-aarch64: warning: bcm2711 dtb: brcm,bcm2711-thermal has been disabled!
qemu-system-aarch64: warning: bcm2711 dtb: brcm,bcm2711-genet-v5 has been disabled!

경고 네 줄만 나오고 그대로 멈춘 것처럼 보인다. 커널이 죽은 건지 콘솔만 안 나오는 건지 가르려면 UART 주소로 직접 찍는 earlycon을 붙여본다.

-append "earlycon=pl011,0xfe201000 console=ttyAMA0,115200 root=/dev/mmcblk1p2 ..."
[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd083]
[    0.000000] Linux version 6.18.34+rpt-rpi-v8 ... aarch64-linux-gnu-gcc-14
[    0.000000] Machine model: Raspberry Pi 4 Model B
[    0.000000] earlycon: pl11 at MMIO 0x00000000fe201000 (options '')
[    0.000000] printk: legacy bootconsole [pl11] enabled

커널은 멀쩡히 돌고 있었다. Pi 4의 PL011은 기본 설정에서 블루투스가 물고 있어 사용자 콘솔이 되지 못하는데, 부트 파티션에 들어 있는 disable-bt 오버레이를 DTB에 미리 합치면 serial0이 PL011로 넘어온다.

fdtoverlay -i bcm2711-rpi-4-b.dtb -o pi4-nobt.dtb disable-bt.dtbo
dtc -I dtb -O dts pi4-nobt.dtb | sed -n '/aliases/,/};/p' | head -3
	aliases {
		serial0 = "/soc/serial@7e201000";
		serial1 = "/soc/serial@7e215040";

serial@7e201000이 PL011이다. 이제 console=ttyAMA0-serial stdio로 나온다.

워치독 리셋 루프 끊기

[   25.096265] systemd[1]: Detected first boot.
[   25.489310] systemd[1]: Hostname set to <raspberrypi>.
[   25.700450] systemd[1]: Initializing machine ID from random generator.
[   28.806341] systemd[1]: bpf-restrict-fs: BPF LSM hook not enabled in the kernel, ...
[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd083]
[    0.000000] Linux version 6.18.34+rpt-rpi-v8 ...

systemd가 뜨자마자 아무 메시지 없이 부팅이 처음부터 다시 시작된다. 라즈베리파이 OS가 켜두는 하드웨어 워치독이 QEMU에서 물리는 순간 리셋이 걸리는 것이라, 루트 파일 시스템에 드롭인을 하나 넣어 끈다.

L=$(sudo losetup -f --show -P raspios-lite-arm64.img)
sudo mount "${L}p2" /mnt/rr

sudo mkdir -p /mnt/rr/etc/systemd/system.conf.d
printf '[Manager]\nRuntimeWatchdogSec=0\nRebootWatchdogSec=0\n' \
    | sudo tee /mnt/rr/etc/systemd/system.conf.d/99-no-watchdog.conf

# 시리얼 getty를 켜고, 네트워크를 기다리는 cloud-init은 꺼둔다
sudo mkdir -p /mnt/rr/etc/systemd/system/getty.target.wants
sudo ln -sf /lib/systemd/system/serial-getty@.service \
    /mnt/rr/etc/systemd/system/getty.target.wants/serial-getty@ttyAMA0.service
sudo touch /mnt/rr/etc/cloud/cloud-init.disabled

sudo umount /mnt/rr && sudo losetup -d "$L"

드롭인 파일명을 99-로 시작해야 한다. 라즈베리파이 OS가 넣어둔 40-rpi-enable-watchdog.conf보다 뒤에 와야 이기기 때문이다.

로그인까지

#!/bin/bash
exec ~/qemu-build/qemu-11.1.0/build/qemu-system-aarch64 \
    -M raspi4b -m 2G \
    -kernel kernel8.img -dtb pi4-nobt.dtb \
    -drive file=raspios-lite-arm64.img,format=raw,if=sd \
    -display none -serial stdio \
    -append "console=ttyAMA0,115200 root=/dev/mmcblk1p2 rootfstype=ext4 rw rootwait"
[  OK  ] Started serial-getty@ttyAMA0.service - Serial Getty on ttyAMA0.
[  OK  ] Reached target getty.target - Login Prompts.

Debian GNU/Linux 13 raspberrypi ttyAMA0

My IP address is 127.0.1.1 ::ffff:127.0.1.1

raspberrypi login: noble
Password:
noble@raspberrypi:~$ uname -a
Linux raspberrypi 6.18.34+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.18.34-1+rpt1 aarch64 GNU/Linux
noble@raspberrypi:~$ tail -3 /proc/cpuinfo
CPU part	: 0xd08
CPU revision	: 3

Model		: Raspberry Pi 4 Model B

CPU part 0xd08은 Cortex-A72로, Pi 3B의 Cortex-A53(0xd03)과 구분되는 지점이다. 호스트에서 QEMU 실행부터 로그인 프롬프트까지 134초가 걸렸다. IP가 127.0.1.1뿐인 것이 이미 다음 절의 결론을 예고한다.

QEMU가 꺼버리는 장치들

부팅할 때 나온 경고 네 줄은 QEMU가 넘겨받은 DTB를 직접 수정해 노드를 지운 결과다. 소스에 지울 목록이 그대로 박혀 있다.

/* Temporarily disable following devices until they are implemented */
const char *nodes_to_remove[] = {
    "brcm,bcm2711-pcie",
    "brcm,bcm2711-rng200",
    "brcm,bcm2711-thermal",
    "brcm,bcm2711-genet-v5",
};

게스트 안에서 확인해보면 이 목록이 그대로 체감된다.

noble@raspberrypi:~$ ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
noble@raspberrypi:~$ dmesg | grep -ci genet
0
noble@raspberrypi:~$ ls /sys/bus/usb/devices/ | wc -l
0
noble@raspberrypi:~$ dmesg | grep -ci xhci
0
noble@raspberrypi:~$ vcgencmd measure_temp
measure_temp
noble@raspberrypi:~$ cat /sys/class/thermal/thermal_zone0/temp
cat: /sys/class/thermal/thermal_zone0/temp: No such file or directory
기능상태이유
SD 카드 / 루트 파일 시스템동작emmc2 모델링됨
시리얼 콘솔(PL011)동작disable-bt 병합 필요
GPIO동작gpiochip0에 58개 노출
SPI조건부 동작DTB에서 노드를 켜야 /dev/spidev0.0 생성
I2C안 뜸status만 바꿔서는 드라이버가 붙지 않음
이더넷없음GENET 컨트롤러 미구현
USB없음xHCI가 PCIe 뒤에 있는데 PCIe 미구현
하드웨어 난수(RNG200)없음미구현, DTB에서 제거됨
온도 센서없음thermal 미구현
vcgencmd무의미VideoCore 펌웨어가 없음

네트워크가 없다는 건 SSH도, apt install도 안 된다는 뜻이다. 게스트에 파일을 넣으려면 QEMU를 끄고 호스트에서 이미지를 루프 마운트하는 수밖에 없다.

config.txt는 읽히지 않는다

여기서 한 가지 더 주의할 점이 있다. 이 방식은 start4.elf 펌웨어를 건너뛰고 커널과 DTB를 QEMU가 직접 올리기 때문에, 부트 파티션의 config.txt에 적은 dtparam·dtoverlay·enable_uart가 하나도 반영되지 않는다. 앞에서 콘솔을 살릴 때 enable_uart=1이 아니라 오버레이를 DTB에 직접 합쳐야 했던 이유가 이것이다. 기본 DTB에서 꺼져 있는 버스를 켜려면 DTS로 풀어 status를 고친 뒤 다시 빌드한다.

dtc -I dtb -O dts pi4-nobt.dtb > nobt.dts
# spi@7e204000 노드의 status = "disabled" 를 "okay" 로 고친 뒤
dtc -q -I dts -O dtb -o pi4-bus.dtb pi4-bus.dts
noble@raspberrypi:~$ ls /dev/gpiochip* /dev/i2c-* /dev/spidev*
ls: cannot access '/dev/i2c-*': No such file or directory
 /dev/gpiochip0   /dev/gpiochip4   /dev/spidev0.1
 /dev/gpiochip1   /dev/spidev0.0

noble@raspberrypi:~$ head -3 /sys/kernel/debug/gpio
gpiochip0: 58 GPIOs, parent: platform/fe200000.gpio, pinctrl-bcm2711:
 gpio-0   (ID_SDA              )
 gpio-1   (ID_SCL              )

SPI는 노드를 켜자 /dev/spidev0.0이 올라왔지만 I2C는 같은 방법으로 안 뜬다. dtparam=i2c_arm=on이 status뿐 아니라 pinctrl 설정까지 함께 바꾸는데, 그 부분은 펌웨어가 하던 일이라 status만 고쳐서는 드라이버가 붙지 않는다.

-m 2G인데 게스트는 916MiB

for M in 512M 1G 2G 4G 8G; do
    printf "%-5s -> " "$M"
    timeout 8 $Q -M raspi4b -m "$M" -display none -serial none -S 2>&1 | head -1
done
512M  -> qemu-system-aarch64: Invalid RAM size, should be 2 GiB
1G    -> qemu-system-aarch64: Invalid RAM size, should be 2 GiB
2G    -> qemu-system-aarch64: warning: bcm2711 dtb: brcm,bcm2711-pcie has been disabled!
4G    -> qemu-system-aarch64: Invalid RAM size, should be 2 GiB
8G    -> qemu-system-aarch64: Invalid RAM size, should be 2 GiB
noble@raspberrypi:~$ free -h | head -2
               total        used        free      shared  buff/cache   available
Mem:           916Mi       126Mi       732Mi       3.5Mi       110Mi       789Mi

-m 2G 말고는 아예 실행이 거부되는데, 정작 게스트는 916MiB만 본다. 이유는 커널에 넘길 부팅 정보를 만드는 쪽에 있다.

boot_ram_size = MIN(vcram_base, UPPER_RAM_BASE - vcram_size);
setup_boot(machine, &soc->cpu[0].core, board_processor_id(board_rev),
           boot_ram_size);
...
s->binfo.ram_size = ram_size;   /* = boot_ram_size */
if (info->ram_size > UPPER_RAM_BASE) {
    raspi_add_memory_node(fdt, UPPER_RAM_BASE, ram_size - UPPER_RAM_BASE);
}

Pi 4의 RAM은 0x0부터의 저지대 뱅크와 0x40000000(UPPER_RAM_BASE)부터의 상단 뱅크로 나뉜다. 그런데 DTB를 고치는 쪽이 보는 info->ram_size는 GPU 몫을 뺀 저지대 크기(0x3c000000)라 항상 조건이 거짓이 되고, 상단 뱅크 노드가 추가되지 않는다. DTB의 memory 노드를 직접 2GB로 고쳐 넣어봐도 QEMU가 부팅 직전에 덮어쓴다.

$ dtc -I dtb -O dts patched.dtb | grep -A3 'memory@'
	memory@0 {
		reg = <0x00 0x00 0x3c000000>;
		device_type = "memory";
	};

언제 쓸 만한가

raspi3braspi4bvirt
필요 QEMU8.2 (배포판 기본)9.0 이상8.2
CPUCortex-A53 ×4Cortex-A72 ×4선택 가능
게스트가 보는 RAM1GB916MiB-m으로 지정
네트워크없음없음virtio-net
맞는 용도Pi 3 부팅 재현Pi 4 부팅·GPIO 계열 확인커널·rootfs 개발

보드 고유의 부팅 경로나 DTB 동작, GPIO·I2C·SPI처럼 SoC 안에 들어 있는 주변장치를 다루는 코드라면 raspi4b가 실물 없이 확인하기 좋다. 반대로 네트워크나 USB가 필요한 작업, 패키지를 설치해가며 붙이는 개발은 virt 머신 쪽이 훨씬 편하다.

주의사항

  • Raspberry Pi 5(raspi5b) 머신은 아직 업스트림에 없다. Pi 5는 주변장치가 RP1이라는 별도 칩으로 빠져 있어 모델링 범위가 다르다.
  • -dtb는 생략할 수 없다. 빼면 커널이 한 줄도 못 찍고 멈춘다.
  • 이미지 크기는 2의 거듭제곱이어야 한다. qemu-img resize로 4G나 8G에 맞춘다.
  • 콘솔이 조용하다고 커널이 죽었다고 단정하지 말 것. earlycon=pl011,0xfe201000을 붙여 UART에 직접 찍어보면 구분된다.
  • 워치독 드롭인은 반드시 99-로 시작해야 한다. systemd 드롭인은 디렉터리와 무관하게 파일명 사전순으로 합쳐져 뒤가 이긴다.
  • 네트워크가 없으므로 userconf.txt로 계정을 미리 만들어두지 않으면 로그인할 방법이 없다.
  • x86 호스트에서는 KVM 없이 TCG로 돌아 실물보다 훨씬 느리다. 로그인까지 134초가 걸린 것도 그래서다.

마무리

QEMU 11.1의 raspi4b는 라즈베리파이 4를 “Cortex-A72 네 개와 SD 카드, 시리얼, GPIO가 달린 보드”까지 재현한다. 여기에 콘솔을 살리는 disable-bt 오버레이와 워치독을 끄는 드롭인 두 가지만 얹으면 라즈베리파이 OS가 로그인 프롬프트까지 올라온다. 다만 이더넷·USB·온도 센서는 DTB에서 아예 잘려 나가고 RAM도 916MiB로 묶이므로, 보드 고유 부팅 동작을 확인하는 용도로 범위를 좁혀 쓰는 것이 맞다.

참고

답글 남기기