라즈베리파이 OS의 설정을 조금 바꿔볼 때마다 SD 카드를 굽고 보드에 꽂고 전원을 넣는 건 느리다. 보드가 아직 손에 없거나, 망가뜨려도 되는 환경이 필요할 때도 있다. QEMU의 raspi3b 머신은 배포된 이미지를 그대로 부팅할 수 있어서 이 과정을 노트북 안에서 끝낼 수 있다. 다만 이미지를 그대로 물리면 부팅되지 않고, 리셋 루프와 시리얼 콘솔 문제를 차례로 넘어야 한다. 이 글에서는 라즈베리파이 OS Lite(arm64)를 로그인 프롬프트까지 올리는 과정을 정리한다.
이미지와 부팅 파일 준비
curl -LO https://downloads.raspberrypi.com/raspios_lite_arm64/images/\
raspios_lite_arm64-2026-06-19/2026-06-18-raspios-trixie-arm64-lite.img.xz
xz -dk 2026-06-18-raspios-trixie-arm64-lite.img.xz
mv 2026-06-18-raspios-trixie-arm64-lite.img rpios.img
fdisk -l rpios.imgDevice Boot Start End Sectors Size Id Type
rpios.img1 16384 1064959 1048576 512M c W95 FAT32 (LBA)
rpios.img2 1064960 5816319 4751360 2.3G 83 Linux
QEMU의 raspi 머신은 SD 카드 크기가 2의 거듭제곱일 것을 요구하므로 이미지를 4 GiB로 늘린다.
qemu-img resize -f raw rpios.img 4G
LOOP=$(sudo losetup -f -P --show rpios.img)
sudo mount ${LOOP}p1 /mnt
cp /mnt/kernel8.img /mnt/bcm2710-rpi-3-b.dtb /mnt/overlays/disable-bt.dtbo .
sudo umount /mnt커널과 DTB를 이미지 안에서 꺼내오는 이유는, QEMU가 라즈베리파이 펌웨어를 실행하지 않기 때문이다. 실제 보드라면 start4.elf가 config.txt를 읽어 커널과 DTB를 골라 올리지만 QEMU에서는 그 단계가 없어 -kernel·-dtb로 직접 넘겨야 한다.
첫 시도 — 리셋 루프
qemu-system-aarch64 -M raspi3b -nographic \
-kernel kernel8.img -dtb bcm2710-rpi-3-b.dtb \
-drive file=rpios.img,if=sd,format=raw \
-append "root=/dev/mmcblk0p2 rootfstype=ext4 rw rootwait console=ttyAMA1,115200"커널은 잘 뜨고 루트 파일시스템도 마운트되는데, systemd가 시작한 직후 아무 메시지 없이 보드가 리셋된다. 그리고 같은 과정을 무한히 반복한다.
[ 14.535004] systemd[1]: Detected first boot.
Welcome to Debian GNU/Linux 13 (trixie)!
[ 14.714569] systemd[1]: Hostname set to <raspberrypi>.
[ 16.636535] systemd[1]: bpf-restrict-fs: BPF LSM hook not enabled in the kernel, ...
[ 3.189437] printk: console [ttyAMA1] enabled <-- 여기서 다시 처음부터
$ grep -ac "Run /sbin/init" console.log
53
init=/bin/sh로 바꿔 부팅하면 셸에서 멈춘 채 리셋되지 않는다. 즉 커널이 아니라 systemd가 시작하는 무언가가 원인이다. 이미지의 rootfs를 열어보면 답이 있다.
$ grep -rn RuntimeWatchdog /mnt/usr/lib/systemd/system.conf.d/
40-rpi-enable-watchdog.conf:2:RuntimeWatchdogSec=1m
40-rpi-enable-watchdog.conf:3:RebootWatchdogSec=2m
라즈베리파이 OS는 BCM2835 하드웨어 워치독을 켜둔다. QEMU가 흉내 낸 워치독에 systemd가 이걸 물리는 순간 머신이 리셋된다. 드롭인으로 덮어쓴다.
[Manager]
RuntimeWatchdogSec=off
RebootWatchdogSec=off파일 이름이 99-인 것이 중요하다. systemd 드롭인은 디렉터리와 무관하게 파일명 사전순으로 합쳐지고 뒤에 오는 쪽이 이긴다. 00-으로 만들면 40-rpi-enable-watchdog.conf에 그대로 덮여 리셋 루프가 계속된다.
시리얼 콘솔에 로그인 프롬프트 띄우기
리셋이 멈추면 부팅은 끝까지 가는데, 이번에는 로그인 프롬프트가 tty1에만 뜨고 시리얼에는 나오지 않는다. 시리얼 getty를 켜도 마찬가지다.
[ TIME ] Timed out waiting for device dev-ttyAMA1.device - /dev/ttyAMA1.
[DEPEND] Dependency failed for serial-getty@ttyAMA1.service - Serial Getty on ttyAMA1.
라즈베리파이 3에서 PL011 UART는 블루투스 전용이고 사용자 콘솔은 mini UART가 맡는다. 그런데 QEMU에서는 mini UART 드라이버가 probe failed with error -22로 올라오지 않고, PL011은 블루투스 serdev에 묶여 getty가 쓸 tty 노드가 생기지 않는다. 실제 보드에서 이 배선을 바꿀 때 쓰는 disable-bt 오버레이를 DTB에 미리 합쳐두면 해결된다.
fdtoverlay -i bcm2710-rpi-3-b.dtb -o bcm2710-rpi-3-b-nobt.dtb disable-bt.dtbo
dtc -I dtb -O dts bcm2710-rpi-3-b-nobt.dtb | grep -A3 'aliases {'적용 전 적용 후
serial0 = "/soc/serial@7e215040" → serial0 = "/soc/serial@7e201000"
serial1 = "/soc/serial@7e201000" → serial1 = "/soc/serial@7e215040"
serial0이 mini UART(7e215040)에서 PL011(7e201000)로 바뀌었다. 이제 PL011이 ttyAMA0으로 잡히므로 여기에 getty를 붙인다.
# 루트 파티션: 시리얼 getty 활성화 + cloud-init 비활성화
sudo mount ${LOOP}p2 /mnt
sudo ln -sf /lib/systemd/system/serial-getty@.service \
/mnt/etc/systemd/system/getty.target.wants/serial-getty@ttyAMA0.service
sudo touch /mnt/etc/cloud/cloud-init.disabled
sudo umount /mnt
# 부트 파티션: 첫 로그인용 계정
sudo mount ${LOOP}p1 /mnt
echo "pi:$(echo raspberry | openssl passwd -6 -stdin)" | sudo tee /mnt/userconf.txt
sudo umount /mnt로그인
qemu-system-aarch64 -M raspi3b -nographic \
-kernel kernel8.img -dtb bcm2710-rpi-3-b-nobt.dtb \
-drive file=rpios.img,if=sd,format=raw \
-append "root=/dev/mmcblk0p2 rootfstype=ext4 rw rootwait console=ttyAMA0,115200"[ OK ] Started serial-getty@ttyAMA0.service - Serial Getty on ttyAMA0.
[ OK ] Reached target multi-user.target - Multi-User System.
Debian GNU/Linux 13 raspberrypi ttyAMA0
raspberrypi login: pi
Password:
Linux raspberrypi 6.18.34+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.18.34-1+rpt1 aarch64
pi@raspberrypi:~$ cat /proc/device-tree/model; echo
Raspberry Pi 3 Model B
pi@raspberrypi:~$ cat /etc/os-release | head -2
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
pi@raspberrypi:~$ nproc; grep -m1 'CPU part' /proc/cpuinfo
4
CPU part : 0xd03
pi@raspberrypi:~$ free -m | head -2
total used free shared buff/cache available
Mem: 917 124 735 3 110 792
pi@raspberrypi:~$ df -h / | tail -1
/dev/root 2.2G 2.0G 92M 96% /
게스트가 자기를 Raspberry Pi 3 Model B로 인식하고 CPU 파트 번호도 Cortex-A53의 0xd03으로 나온다. 전원 투입부터 로그인 프롬프트까지 실측 213초가 걸렸다.
raspi3b 머신의 한계
| 항목 | QEMU raspi3b |
|---|---|
| 메모리 | 1 GB 고정(게스트 인식 917 MB) |
| CPU | Cortex-A53 4코어, TCG 번역 |
| 네트워크 | 장치 없음 — 게스트 IP는 127.0.1.1뿐 |
| SD 이미지 | 크기가 2의 거듭제곱이어야 함 |
| 펌웨어 | 실행되지 않음 — config.txt·오버레이 무시 |
| 로그인까지 | 약 213초 |
주의사항
config.txt와cmdline.txt는 펌웨어가 읽는 파일이라 QEMU에서는 아무 효과가 없다. 부트 인자는-append로,dtoverlay는 위처럼fdtoverlay로 DTB에 미리 합쳐서 넘겨야 한다.- 네트워크 장치가 없어
systemd-networkd-wait-online이 3분 넘게 붙잡고 있다가 실패한다. cloud-init까지 얹히면 부팅이 4분 이상 더 늘어나므로cloud-init.disabled를 만들어두는 편이 낫다. - 펌웨어 메일박스에 의존하는 도구는 동작하지 않는다.
vcgencmd measure_temp는 온도 대신 명령 이름만 되돌려준다. - 전부 TCG 번역이라 성능 측정에는 쓸 수 없다. 부팅 순서, 설정 파일, 스크립트 동작을 확인하는 용도로만 쓴다.
- 이미지 수정은 게스트를 끄고 호스트에서
losetup -P로 붙여 하는 편이 빠르다. 게스트 안에서 고치려면 매번 재부팅에 수 분이 든다. - 워치독을 끄는 설정은 QEMU에서 돌리기 위한 것이다. 이 이미지를 그대로 실제 보드에 구우면 워치독이 꺼진 상태로 동작하니, 보드용 SD 카드는 따로 만든다.
마무리
막히는 지점은 세 개였다. 이미지 크기를 2의 거듭제곱으로 맞추는 것, systemd 워치독을 끄는 것, 그리고 disable-bt 오버레이로 시리얼 콘솔을 PL011로 옮기는 것이다. 셋 다 이미지를 호스트에서 마운트해 미리 손봐두면 되는 작업이라 한 번 만들어두면 이후로는 qemu-system-aarch64 한 줄로 3분 만에 라즈베리파이 OS 셸까지 들어갈 수 있다.