1편부터 4편까지는 디스크 이미지를 자르고, U-Boot을 빌드하고, 커널을 컴파일하고, 루트 파일 시스템을 손으로 채웠다. 제품용 타깃이라면 이 과정을 재현 가능하게 자동화해야 하는데, 그 자리에 쓰는 것이 Yocto다. 레시피와 설정 파일에서 툴체인부터 이미지까지 통째로 만들어내고, 같은 입력이면 같은 결과가 나온다. 이 글에서는 같은 virt 타깃을 Yocto로 만들어 부팅시키고, 패키지를 추가해 이미지를 바꾸고, 애플리케이션 개발용 SDK까지 뽑아본다. 그리고 지금까지의 수동 조립과 무엇이 다른지 정리한다.
이 시리즈의 다른 글
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (1) — 타깃 디스크 이미지와 virt 보드 구성
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (2) — U-Boot 올리기
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (3) — 커널 크로스 컴파일과 부팅
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (4) — 루트 파일 시스템과 패키지 설치
- QEMU(arm64) 임베디드 리눅스 타깃 만들기 (6) — 커널 모듈, gdb 원격 디버깅, NFS 루트
빌드 환경 잡기
poky를 받아 MACHINE만 qemuarm64로 바꾸면 된다. 다른 머신용 빌드 디렉터리가 이미 있다면 DL_DIR과 SSTATE_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-minimalNOTE: 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.bin | 3편에서 직접 크로스 컴파일한 Image |
*.rootfs.ext4 | 4편에서 debootstrap으로 채운 루트 파티션 |
modules-*.tgz | 6편에서 따로 빌드할 커널 모듈 |
*.manifest | 이미지에 들어간 패키지 목록(수동 조립에는 없던 것) |
*.qemuboot.conf | runqemu가 쓸 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 slirpPoky (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.1MB | 75.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에 주소가 없어 호스트에서 접속하면 응답이 없다. -append에 ip=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 로그인을 허용한다. 제품 이미지에 남기지 않는다.runqemu는oe-init-build-env를 거친 셸에서만 동작한다. 새 터미널을 열었다면 다시 source해야 한다.
마무리
1~4편에서 손으로 쌓은 것과 같은 타깃을 Yocto로 만들어 runqemu로 띄웠고, 설정 두 줄로 SSH가 들어간 이미지를 다시 구웠으며, SDK를 뽑아 타깃용 애플리케이션까지 빌드해 돌려봤다. 결과물은 비슷하지만 과정이 기록으로 남는다는 점이 다르다. 다음 편에서는 지금까지 만든 타깃 위에서 실제 개발을 한다. 커널 모듈을 크로스 빌드해 올리고, gdb로 부팅을 붙잡고, NFS 루트로 rootfs를 호스트에 두는 방법을 정리한다.