QEMU(arm64) 임베디드 리눅스 타깃 만들기 (3) — 커널 크로스 컴파일과 부팅

2편까지 오면 타깃에 부트로더가 올라가 디스크에서 파일을 읽어 실행하는 데까지 된다. 이제 그 자리에 올릴 커널이 필요하다. 실물 보드라면 벤더가 준 커널 소스를 받아 크로스 컴파일하고 SD 카드에 복사하는 단계인데, QEMU virt는 메인라인 커널의 arm64 defconfig가 그대로 부팅된다는 점이 편하다. 이 글에서는 커널을 크로스 컴파일해 부트 파티션에 넣고, U-Boot의 booti로 올려 루트 파일 시스템을 마운트하는 데까지 확인한다.

이 시리즈의 다른 글

커널 크로스 컴파일

소스 트리를 더럽히지 않도록 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_MMIO1편에서 붙인 virtio-blk-device를 못 본다(PCI 방식이면 불필요)
VIRTIO_BLK/dev/vda가 안 생겨 루트를 못 찾는다
SERIAL_AMBA_PL011console=ttyAMA0가 조용하다
EXT4_FS루트 파티션을 마운트하지 못한다

부트 스크립트에 커널 올리기 넣기

2편에서 만든 boot.cmdecho 자리를 실제 부팅 명령으로 바꾼다.

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,0x9000000bootargs에 넣으면 콘솔 드라이버가 뜨기 전 로그부터 볼 수 있다.

주의사항

  • 커널 빌드는 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로 확장한다.

참고

답글 남기기