2편까지 오면 타깃에 부트로더가 올라가 디스크에서 파일을 읽어 실행하는 데까지 된다. 이제 그 자리에 올릴 커널이 필요하다. 실물 보드라면 벤더가 준 커널 소스를 받아 크로스 컴파일하고 SD 카드에 복사하는 단계인데, QEMU virt는 메인라인 커널의 arm64 defconfig가 그대로 부팅된다는 점이 편하다. 이 글에서는 커널을 크로스 컴파일해 부트 파티션에 넣고, U-Boot의 booti로 올려 루트 파일 시스템을 마운트하는 데까지 확인한다.
이 시리즈의 다른 글
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (1) — 타깃 디스크 이미지와 virt 보드 구성
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (2) — U-Boot 올리기
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (4) — 루트 파일 시스템과 패키지 설치
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (5) — Yocto로 타깃 이미지 빌드
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (6) — 커널 모듈, gdb 원격 디버깅, NFS 루트
커널 크로스 컴파일
소스 트리를 더럽히지 않도록 O=로 빌드 디렉터리를 따로 둔다. 같은 소스로 x86 빌드를 병행할 때도 이 방식이면 충돌하지 않는다.
#!/bin/bash
set -e
SRC=~/kbuild/linux-6.12
OUT=~/qemu-target/kbuild-arm64
export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
make -C "$SRC" O="$OUT" defconfig
make -C "$SRC" O="$OUT" -j$(nproc) Image
ls -lh "$OUT"/arch/arm64/boot/Image-rw-rw-r-- 1 noble noble 44M Aug 25 23:28 kbuild-arm64/arch/arm64/boot/Image
arm64는 zImage가 없고 압축하지 않은 Image가 표준 부팅 대상이다. defconfig 그대로면 44MB로 꽤 큰데, 1편에서 부트 파티션을 64MiB로 잡아둔 덕에 아슬아슬하게 들어간다.
타깃이 쓸 장치 드라이버가 켜져 있는지는 빌드 전에 확인해두는 편이 낫다. arm64 defconfig에는 virt에 필요한 것이 모두 y로 들어 있다.
$ grep -E 'CONFIG_VIRTIO_MMIO=|CONFIG_VIRTIO_BLK=|CONFIG_VIRTIO_NET=|CONFIG_SERIAL_AMBA_PL011=|CONFIG_EXT4_FS=' kbuild-arm64/.config
CONFIG_VIRTIO_BLK=y
CONFIG_VIRTIO_NET=y
CONFIG_SERIAL_AMBA_PL011=y
CONFIG_VIRTIO_MMIO=y
CONFIG_EXT4_FS=y
| 옵션 | 없으면 생기는 일 |
|---|---|
VIRTIO_MMIO | 1편에서 붙인 virtio-blk-device를 못 본다(PCI 방식이면 불필요) |
VIRTIO_BLK | /dev/vda가 안 생겨 루트를 못 찾는다 |
SERIAL_AMBA_PL011 | console=ttyAMA0가 조용하다 |
EXT4_FS | 루트 파티션을 마운트하지 못한다 |
부트 스크립트에 커널 올리기 넣기
2편에서 만든 boot.cmd의 echo 자리를 실제 부팅 명령으로 바꾼다.
echo "loading kernel from virtio 0:1 ..."
load virtio 0:1 ${kernel_addr_r} Image
setenv bootargs "console=ttyAMA0,115200 root=/dev/vda2 rootwait rw"
booti ${kernel_addr_r} - ${fdtcontroladdr}booti의 인자는 순서대로 커널 주소, initramfs 주소, DTB 주소다. 가운데 -는 initramfs를 쓰지 않는다는 뜻이고, DTB 자리에는 2편에서 확인한 fdtcontroladdr을 넣는다. 실물 보드 예제처럼 fdt_addr_r에 DTB 파일을 따로 로드할 필요가 없다.
u-boot-2026.01/tools/mkimage -A arm64 -T script -C none \
-n 'qemu-target boot' -d boot.cmd boot.scr
sudo qemu-nbd --connect=/dev/nbd0 work.qcow2
sudo mount /dev/nbd0p1 /mnt/tb
sudo cp kbuild-arm64/arch/arm64/boot/Image boot.scr /mnt/tb/
df -h /mnt/tb | tail -1
sudo umount /mnt/tb && sudo qemu-nbd --disconnect /dev/nbd0/dev/nbd0p1 63M 44M 20M 70% /mnt/tb
커널 하나로 부트 파티션의 70%가 찬다. 모듈까지 올릴 생각이라면 1편의 파티션 크기를 다시 잡거나 필요 없는 드라이버를 덜어내야 한다.
U-Boot에서 커널로
loading kernel from virtio 0:1 ...
45718016 bytes read in 62 ms (703.2 MiB/s)
## Flattened Device Tree blob at 7e55bdc0
Booting using the fdt blob at 0x7e55bdc0
Loading Device Tree to 000000007d401000, end 000000007d503fff ... OK
[ 0.000000] Linux version 6.12.0 (noble@noble) (aarch64-linux-gnu-gcc 13.3.0 ...) #1 SMP PREEMPT
[ 0.000000] Machine model: linux,dummy-virt
[ 0.000000] Kernel command line: console=ttyAMA0,115200 root=/dev/vda2 rootwait rw
U-Boot이 DTB를 메모리 위쪽으로 옮겨 커널에 넘겼고, 커널이 linux,dummy-virt를 알아봤다. 1편에서 dumpdtb로 봤던 그 디바이스 트리가 여기까지 흘러온 것이다.
[ 0.992895] virtio_blk virtio1: 1/0/0 default/read/poll queues
[ 0.996953] virtio_blk virtio1: [vda] 2097152 512-byte logical blocks (1.07 GB/1.00 GiB)
[ 1.010788] vda: vda1 vda2
[ 1.544740] EXT4-fs (vda2): mounted filesystem 7234d323-... r/w with ordered data mode.
[ 1.545409] VFS: Mounted root (ext4 filesystem) on device 254:2.
[ 1.599888] Freeing unused kernel memory: 10432K
1편에서 만든 파티션 두 개가 vda1·vda2로 잡히고, ext4 루트가 마운트됐다. 부트로더부터 커널, 디스크까지 한 줄로 이어진 것이 여기서 확인된다.
그리고 멈춘다
[ 1.602395] Run /bin/init as init process
[ 1.602530] Run /bin/sh as init process
[ 1.602819] Kernel panic - not syncing: No working init found. Try passing init= option to kernel.
[ 1.603463] CPU: 0 UID: 0 PID: 1 Comm: swapper/0 Not tainted 6.12.0 #1
[ 1.603642] Hardware name: linux,dummy-virt (DT)
[ 1.604838] SMP: stopping secondary CPUs
루트 파티션은 마운트됐지만 그 안이 mkfs 직후 상태라 실행할 init이 없다. 이 패닉은 실패가 아니라 커널이 제 할 일을 끝까지 했다는 증거다. 다음 편에서 이 빈 파티션을 채운다.
부팅이 막혔을 때 보는 순서
| 증상 | 어디까지 간 것인가 | 확인할 것 |
|---|---|---|
| U-Boot 프롬프트도 안 나옴 | 부트로더 이전 | -bios 경로, 2편 참고 |
load에서 파일을 못 찾음 | 부트로더 | fatls virtio 0:1로 파일 존재 확인 |
booti 후 아무 출력 없음 | 커널 진입 직후 | console= 값, SERIAL_AMBA_PL011 |
| Waiting for root device | 드라이버 초기화 | VIRTIO_MMIO/VIRTIO_BLK, root= 경로 |
| No working init found | 루트 마운트 성공 | 루트 파일 시스템 내용(4편) |
콘솔이 조용할 때 커널이 죽은 건지 출력만 안 나오는 건지 가르려면 earlycon을 붙인다. virt의 PL011은 0x9000000이므로 earlycon=pl011,0x9000000을 bootargs에 넣으면 콘솔 드라이버가 뜨기 전 로그부터 볼 수 있다.
주의사항
- 커널 빌드는
ARCH=arm64, U-Boot 빌드는ARCH=arm이다. 같은 타깃인데 값이 다르니 스크립트를 섞어 쓰지 말 것. - defconfig
Image가 44MB다. 부트 파티션을 작게 잡았다면load가 아니라cp단계에서 먼저 막힌다. booti의 DTB 자리를 비우면(booti $kernel_addr_r - -) 커널이 디바이스 트리 없이 떠서 아무 장치도 못 찾는다.root=/dev/vda2는 virtio-blk를 mmio로 붙였을 때의 이름이다.virtio-blk-pci로 바꾸면 번호가 달라질 수 있으니vda: vda1 vda2줄을 보고 맞춘다.- 커널을 다시 빌드해 복사할 때는 QEMU가 이미지를 잡고 있지 않은지 먼저 확인한다. 실행 중인 이미지를
qemu-nbd로 마운트해 덮어쓰면 파일 시스템이 깨진다.
마무리
메인라인 커널을 크로스 컴파일해 U-Boot으로 올렸고, 커널이 QEMU가 준 디바이스 트리로 virtio 디스크를 찾아 ext4 루트를 마운트하는 것까지 확인했다. 남은 것은 그 루트 안에 들어갈 내용이다. 다음 편에서는 BusyBox로 최소 루트 파일 시스템을 만들어 셸을 띄우고, 그 위에 패키지를 올린 데비안 계열 rootfs로 확장한다.