QEMU(arm64) 임베디드 리눅스 타깃 만들기 (5) — Yocto로 타깃 이미지 빌드

1편부터 4편까지는 디스크 이미지를 자르고, U-Boot을 빌드하고, 커널을 컴파일하고, 루트 파일 시스템을 손으로 채웠다. 제품용 타깃이라면 이 과정을 재현 가능하게 자동화해야 하는데, 그 자리에 쓰는 것이 Yocto다. 레시피와 설정 파일에서 툴체인부터 이미지까지 통째로 만들어내고, 같은 입력이면 같은 결과가 나온다. 이 글에서는 같은 virt 타깃을 Yocto로 만들어 부팅시키고, 패키지를 추가해 이미지를 바꾸고, 애플리케이션 개발용 SDK까지 뽑아본다. 그리고 지금까지의 수동 조립과 무엇이 다른지 정리한다.

이 시리즈의 다른 글

빌드 환경 잡기

poky를 받아 MACHINEqemuarm64로 바꾸면 된다. 다른 머신용 빌드 디렉터리가 이미 있다면 DL_DIRSSTATE_DIR을 함께 쓰도록 지정해 다운로드와 캐시를 재활용한다.

#!/bin/bash
set -e
cd ~/yocto/poky
source oe-init-build-env ~/yocto/poky/build-arm64 > /dev/null

cat >> conf/local.conf <<'EOF'

MACHINE = "qemuarm64"
DL_DIR = "/home/noble/yocto/poky/build/downloads"
SSTATE_DIR = "/home/noble/yocto/poky/build/sstate-cache"
BB_NUMBER_THREADS = "6"
PARALLEL_MAKE = "-j 6"
EOF

bitbake core-image-minimal
NOTE: Tasks Summary: Attempted 4073 tasks of which 2314 didn't need to be rerun and all succeeded.

4073개 태스크 중 2314개를 sstate 캐시에서 그대로 가져왔다. 이 캐시가 없는 첫 빌드는 툴체인부터 만들어야 해서 훨씬 오래 걸린다.

나온 결과물

$ ls tmp/deploy/images/qemuarm64/
core-image-minimal-qemuarm64.rootfs.ext4
core-image-minimal-qemuarm64.rootfs.manifest
core-image-minimal-qemuarm64.rootfs.qemuboot.conf
core-image-minimal-qemuarm64.rootfs.tar.bz2
Image-qemuarm64.bin
modules-qemuarm64.tgz
파일1~4편에서 이에 해당하던 것
Image-qemuarm64.bin3편에서 직접 크로스 컴파일한 Image
*.rootfs.ext44편에서 debootstrap으로 채운 루트 파티션
modules-*.tgz6편에서 따로 빌드할 커널 모듈
*.manifest이미지에 들어간 패키지 목록(수동 조립에는 없던 것)
*.qemuboot.confrunqemu가 쓸 QEMU 실행 옵션

manifest는 이미지에 무엇이 왜 들어갔는지를 남겨준다. 4편처럼 손으로 만든 rootfs에서는 이런 목록을 따로 관리해야 한다.

$ head -8 core-image-minimal-qemuarm64.rootfs.manifest
base-files qemuarm64 3.0.14
base-passwd cortexa57 3.6.8
busybox cortexa57 1.36.1
busybox-hwclock cortexa57 1.36.1
busybox-syslog cortexa57 1.36.1
busybox-udhcpc cortexa57 1.36.1
eudev cortexa57 3.2.14
init-ifupdown qemuarm64 1.0

runqemu로 띄우기

1편에서 만든 run-target.sh에 해당하는 것이 runqemu다. qemuboot.conf에 적힌 커널·이미지·머신 옵션을 읽어 QEMU 명령을 조립해준다.

cd ~/yocto/poky && source oe-init-build-env ~/yocto/poky/build-arm64
runqemu qemuarm64 nographic slirp
Poky (Yocto Project Reference Distro) 5.0.19 qemuarm64 /dev/ttyAMA0

qemuarm64 login: root
root@qemuarm64:~# uname -a
Linux qemuarm64 6.6.144-yocto-standard #1 SMP PREEMPT aarch64 GNU/Linux
root@qemuarm64:~# df -h /
Filesystem                Size      Used Available Use% Mounted on
/dev/root                19.3M     12.9M      4.9M  72% /
root@qemuarm64:~# ls /bin | wc -l
70
root@qemuarm64:~# cat /proc/device-tree/model; echo
linux,dummy-virt

같은 virt 보드에 같은 linux,dummy-virt 디바이스 트리다. 다른 것은 내용물로, 루트 파일 시스템이 19.3MB에 /bin 실행 파일이 70개뿐이다. 4편에서 만든 데비안 rootfs가 318MB였던 것과 비교하면 임베디드용 이미지가 어떤 크기인지 감이 온다.

이미지에 패키지 추가하기

4편에서는 타깃을 띄워 apt install을 했지만, Yocto에서는 이미지를 다시 굽는 것이 정석이다. 무엇이 들어갔는지가 설정 파일에 남고 다음 빌드에서 똑같이 재현되기 때문이다.

# 타깃에 SSH와 커널 모듈을 추가한다
IMAGE_INSTALL:append = " openssh kernel-modules"
EXTRA_IMAGE_FEATURES += "debug-tweaks"
NOTE: Tasks Summary: Attempted 4093 tasks of which 4062 didn't need to be rerun and all succeeded.
기본openssh kernel-modules 추가
rootfs 크기23.1MB75.9MB
패키지 수30개333개
재빌드 태스크4073개(2314개 캐시)4093개(4062개 캐시)

두 번째 빌드는 4093개 중 4062개를 캐시에서 가져와 실제로 돌린 것은 31개뿐이다. 이미 만들어둔 것을 다시 만들지 않는 것이 sstate의 값이고, 수동 조립에서 “커널만 고쳤는데 rootfs를 다시 만들어야 하나” 같은 고민이 사라지는 지점이다. debug-tweaks는 root 로그인에 비밀번호를 요구하지 않게 하는 개발용 설정이라 제품 이미지에는 넣지 않는다.

직접 만든 명령으로 띄울 때

1편에서 쓰던 방식대로 QEMU 명령을 직접 조립해도 된다. 이때 ip=dhcp를 빠뜨리면 부팅은 되는데 네트워크가 안 붙는다.

root@qemuarm64:~# netstat -ltn
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
root@qemuarm64:~# ip -4 addr show eth0
root@qemuarm64:~#

sshd는 22번 포트를 열고 기다리는데 eth0에 주소가 없어 호스트에서 접속하면 응답이 없다. -appendip=dhcp를 넣으면 해결된다.

cd tmp/deploy/images/qemuarm64
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 512 -smp 2     -kernel Image-qemuarm64.bin     -drive file=core-image-minimal-qemuarm64.rootfs.ext4,if=none,format=raw,id=hd0     -device virtio-blk-device,drive=hd0     -netdev user,id=n0,hostfwd=tcp::2223-:22 -device virtio-net-device,netdev=n0     -append "console=ttyAMA0 root=/dev/vda rw ip=dhcp" -nographic
$ ssh -p 2223 root@localhost
root@qemuarm64:~# ip -4 -o addr show eth0
2: eth0    inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0

애플리케이션 개발용 SDK 뽑기

타깃에 올릴 애플리케이션을 만드는 사람에게 poky 트리 전체를 주는 대신, 크로스 툴체인과 타깃 sysroot만 담은 설치 파일을 뽑아 넘길 수 있다.

bitbake core-image-minimal -c populate_sdk
ls -lh tmp/deploy/sdk/
183M poky-glibc-x86_64-core-image-minimal-cortexa57-qemuarm64-toolchain-5.0.19.sh
 32K poky-glibc-...-toolchain-5.0.19.target.manifest
264K poky-glibc-...-toolchain-5.0.19.testdata.json
$ ./poky-glibc-x86_64-...-toolchain-5.0.19.sh -y -d ~/yocto/sdk-arm64
Extracting SDK........................................................done
Setting it up...done
SDK has been successfully set up and is ready to be used.

$ source ~/yocto/sdk-arm64/environment-setup-cortexa57-poky-linux
$ echo $CC
aarch64-poky-linux-gcc -mcpu=cortex-a57+crc -mbranch-protection=standard
-fstack-protector-strong -O2 -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security
-Werror=format-security --sysroot=/home/noble/yocto/sdk-arm64/sysroots/...

$CC에 타깃 CPU 옵션과 sysroot 경로, 배포판이 쓰는 보안 플래그가 모두 박혀 나온다. 3편에서 aarch64-linux-gnu-gcc를 직접 부를 때 손으로 맞춰야 했던 것들이다.

#include <stdio.h>
#include <sys/utsname.h>

int main(void)
{
	struct utsname u;

	uname(&u);
	printf("built with Yocto SDK, running on %s %s
", u.sysname, u.machine);
	return 0;
}
$ $CC sdkhello.c -o sdkhello
$ file sdkhello
sdkhello: ELF 64-bit LSB pie executable, ARM aarch64, dynamically linked,
interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 5.15.0, with debug_info

$ scp -P 2223 sdkhello root@localhost:~/
$ ssh -p 2223 root@localhost '~/sdkhello'
built with Yocto SDK, running on Linux aarch64

SDK로 빌드한 바이너리가 Yocto 이미지 위에서 그대로 돈다. 이 SDK 파일 하나만 넘기면 상대는 Yocto를 몰라도 타깃용 빌드를 할 수 있다.

Yocto와 수동 조립

1~4편 수동 조립Yocto
첫 결과물까지수십 분수 시간(캐시 없을 때)
구성 재현스크립트를 직접 관리local.conf·레시피에 기록
패키지 목록추적 안 됨manifest로 남음
라이선스 정보직접 수집자동 수집
부분 재빌드손으로 판단sstate가 판단
배우는 것부팅 경로 전체가 보임레시피 뒤에 감춰짐

브링업 단계에서 부팅이 어디서 막혔는지 봐야 할 때는 1~4편 방식이 빠르고, 여러 사람이 같은 이미지를 반복해서 만들어야 하는 단계에서는 Yocto가 맞다. 실제 프로젝트에서는 둘을 같이 쓰게 된다.

주의사항

  • 머신을 바꿀 때는 MACHINE만 고치지 말고 빌드 디렉터리를 새로 만든다. DL_DIR·SSTATE_DIR을 공유하면 캐시는 그대로 재활용된다.
  • 디스크가 넉넉해야 한다. core-image-minimal 하나에도 tmp가 수십 GB로 불어난다.
  • IMAGE_INSTALL:append의 앞 공백을 빼먹으면 앞 패키지 이름과 붙어버린다. " openssh"처럼 반드시 공백으로 시작한다.
  • debug-tweaks는 비밀번호 없는 root 로그인을 허용한다. 제품 이미지에 남기지 않는다.
  • runqemuoe-init-build-env를 거친 셸에서만 동작한다. 새 터미널을 열었다면 다시 source해야 한다.

마무리

1~4편에서 손으로 쌓은 것과 같은 타깃을 Yocto로 만들어 runqemu로 띄웠고, 설정 두 줄로 SSH가 들어간 이미지를 다시 구웠으며, SDK를 뽑아 타깃용 애플리케이션까지 빌드해 돌려봤다. 결과물은 비슷하지만 과정이 기록으로 남는다는 점이 다르다. 다음 편에서는 지금까지 만든 타깃 위에서 실제 개발을 한다. 커널 모듈을 크로스 빌드해 올리고, gdb로 부팅을 붙잡고, NFS 루트로 rootfs를 호스트에 두는 방법을 정리한다.

참고

답글 남기기