제6부 개발 워크플로우: 크로스 컴파일
6.1 PC에서 C++ 크로스 컴파일 툴체인 구성
제6부 개발 워크플로우의 첫 장. 2.1(두 프로세서 아키텍처)에서 QRB2210이 쿼드코어 Cortex-A53임을, 3.1(Debian Linux)에서 MPU가 일반 Debian 사용자 공간임을, 5.2(SSH 원격 접속)에서 보드 접속 방법을 다뤘다는 전제로 이어간다. 이 장은 PC(x86_64 또는 Apple Silicon)에서 aarch64용 C/C++ 바이너리를 만들어 보드로 보내는 실전 절차를 다룬다. Rust는 6.2, Docker·SSH 배포·원격 디버깅은 6.3에서 이어 다룬다.
이 장을 마치면 GNU 툴체인과 LLVM/Clang 두 경로로 크로스 컴파일을 수행할 수 있고, CMake 프로젝트를 크로스 빌드로 전환할 수 있으며, 크로스 컴파일에서 가장 흔히 문제가 되는 sysroot 불일치 문제를 진단·해결할 수 있다.
6.1.1 왜 크로스 컴파일인가
UNO Q의 MPU는 QRB2210, 쿼드코어 64비트 Arm Cortex-A53 @ 2.0GHz다(2.1). 웹 브라우징·스크립트 실행에는 충분하지만, C++ 대규모 프로젝트(OpenCV, ROS2 패키지, LLVM 기반 도구 등)를 보드 위에서 그대로 make -j4 하면 데스크톱·노트북 PC보다 실사용 기준 수 배 느리다 — 코어 수가 적고 클록도 낮은 데다, 빌드 중 다른 부팅·서비스 작업과 자원을 나눠 써야 하기 때문이다(정확한 배수는 프로젝트·PC 사양마다 다르므로 "검증 필요"로 남긴다).
크로스 컴파일은 이 병목을 PC로 옮긴다: 소스는 그대로, PC의 CPU 자원으로 aarch64 바이너리를 만들고, 완성된 실행 파일만 SSH로 보드에 전송해 실행한다.
트레이드오프도 있다: PC의 라이브러리 버전이 보드와 다르면 실행 시점에 문제가 생길 수 있다(6.1.5). "빠르지만 정확히 맞추는 데 신경 써야 하는" 방식이라고 이해하는 편이 정확하다.
6.1.2 GNU aarch64 툴체인 구성
가장 널리 쓰이고 자료도 많은 경로다. GNU 크로스 툴체인은 <target-triple>-gcc 형태로 실행 파일 이름이 붙는다 — aarch64 Linux glibc 대상이면 aarch64-linux-gnu-gcc·aarch64-linux-gnu-g++다.
6.1.2.1 설치
| 호스트 OS | 설치 방법 |
|---|---|
| Debian/Ubuntu (x86_64) | sudo apt install crossbuild-essential-arm64 |
| Fedora 계열 | sudo dnf install gcc-aarch64-linux-gnu gcc-c++-aarch64-linux-gnu(검증 필요 — 배포판별 패키지명 확인) |
| macOS | Homebrew에 aarch64-linux-gnu glibc 타깃용 공식 GCC 크로스 포뮬러는 없다(검증 필요) — 6.1.3의 Clang 경로 또는 6.3의 Docker 경로를 권장 |
| Windows | 네이티브 지원 없음 — WSL2(Ubuntu)에서 위 apt 방법 사용 권장 |
crossbuild-essential-arm64는 메타 패키지로, gcc-aarch64-linux-gnu·g++-aarch64-linux-gnu·libc6-dev-arm64-cross 등 필요한 패키지를 한 번에 설치한다. 설치 후 확인:
aarch64-linux-gnu-g++ --version
6.1.2.2 첫 크로스 빌드
cat > hello.cpp << 'EOF'
#include <iostream>
int main() { std::cout << "UNO Q에서 인사!\n"; return 0; }
EOF
aarch64-linux-gnu-g++ -O2 hello.cpp -o hello-aarch64
file hello-aarch64
# hello-aarch64: ELF 64-bit LSB pie executable, ARM aarch64, ...
file 명령 출력의 ARM aarch64를 확인했다면 성공이다. 이제 5.2의 방법으로 전송·실행한다:
scp hello-aarch64 [email protected]:/home/arduino/
ssh [email protected] ./hello-aarch64
([email protected]은 UNO Q의 전형적인 기본 SSH 사용자·mDNS 호스트명이지만, 개별 보드·네트워크 환경에 따라 호스트명 접미사(unoq1.local 등)나 IP가 다를 수 있다 — 실제 값은 5.2에서 확인한 자신의 보드 정보를 쓴다.)
6.1.3 LLVM/Clang으로 크로스 컴파일
Clang은 하나의 바이너리가 여러 타깃을 지원하는 구조라 --target=<triple> 플래그만으로 타깃을 바꾼다. macOS·최신 Linux 배포판에 Clang이 기본 포함되어 있는 경우가 많아 별도 크로스 GCC 없이 시작하기 좋다.
6.1.3.1 준비물 — 컴파일러 + sysroot
Clang 자체는 타깃을 바꿀 뿐, 타깃의 헤더·라이브러리(sysroot)는 별도로 필요하다. 두 가지 방법이 있다.
방법 A — 멀티아치 패키지로 sysroot 확보(Debian/Ubuntu 호스트):
sudo apt install clang lld libc6-dev-arm64-cross
clang++ --target=aarch64-linux-gnu -fuse-ld=lld hello.cpp -o hello-aarch64
libc6-dev-arm64-cross가 /usr/aarch64-linux-gnu/에 헤더·라이브러리를 설치해 주므로, Clang이 기본 탐색 경로에서 이를 찾아낸다(리눅스용 GNU 크로스 툴체인이 이미 설치돼 있다면 그 sysroot를 재사용하는 것과 같다).
방법 B — 독립 sysroot 다운로드(GNU 툴체인이 없는 환경, 예: macOS):
wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/aarch64-linux-gnu/sysroot-glibc-linaro-2.25-2019.12-aarch64-linux-gnu.tar.xz
tar -xf sysroot-glibc-linaro-2.25-2019.12-aarch64-linux-gnu.tar.xz
clang++ --target=aarch64-linux-gnu \
--sysroot=$(pwd)/sysroot-glibc-linaro-2.25-2019.12-aarch64-linux-gnu \
-fuse-ld=lld hello.cpp -o hello-aarch64
이 Linaro sysroot는 오래된 glibc(2.25)를 담고 있어 폭넓게 호환되지만, 보드의 실제 glibc보다 오래됐을 수도, 반대로 최신 심볼이 없을 수도 있다 — 근본 해결책은 6.1.5를 참고한다.
6.1.3.2 GNU vs Clang/LLVM, 언제 무엇을
| 기준 | GNU(aarch64-linux-gnu-g++) | Clang/LLVM(--target=) |
|---|---|---|
| 설치 난이도(Linux) | 쉬움(apt 한 줄) | 쉬움(apt + sysroot 패키지) |
| 설치 난이도(macOS) | 어려움(공식 크로스 GCC 없음) | 비교적 쉬움(Clang 기본 내장) |
| 여러 타깃 동시 지원 | 타깃마다 별도 툴체인 설치 | 컴파일러 하나로 --target= 전환 |
| 생태계 호환성 | 대부분의 Makefile·레거시 빌드가 전제 | CMake Clang 툴체인, sanitizer 계열과 호환성 좋음 |
| ABI·최적화 미세 차이 | 보드의 실제 GCC와 100% 동일 | GCC와 미묘한 ABI·내장함수 차이 가능(대개 문제 없음, 드물게 검증 필요) |
견해: 처음이라면 GNU 툴체인으로 시작하는 편이 자료도 많고 함정도 적다. macOS 호스트이거나 여러 아키텍처를 자주 오간다면 Clang이 유리하다.
6.1.4 CMake 툴체인 파일
Makefile을 직접 짜지 않는 대부분의 C++ 프로젝트는 CMake를 쓴다. CMake는 툴체인 파일(toolchain file) 하나로 크로스 빌드 설정을 캡슐화한다.
toolchain-aarch64.cmake:
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
# 실제 보드 sysroot를 확보했다면 지정(6.1.5) — 없으면 주석 처리해도 됨
# set(CMAKE_SYSROOT ${CMAKE_CURRENT_LIST_DIR}/sysroot)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # 실행 파일은 호스트(PC) 것을 사용
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # 라이브러리는 타깃(sysroot)에서만 탐색
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # 헤더도 타깃에서만
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
빌드:
cmake -B build-aarch64 -S . \
-DCMAKE_TOOLCHAIN_FILE=$(pwd)/toolchain-aarch64.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-aarch64 -j"$(nproc)"
CMAKE_FIND_ROOT_PATH_MODE_* 네 줄이 핵심이다 — find_package·find_library가 실수로 PC의 x86_64 라이브러리를 찾아 링크해 버리는 사고(빌드는 되는데 실행 시 "wrong ELF class" 같은 오류)를 막는다. PROGRAM만 NEVER로 두는 이유는, cmake가 코드 생성 도구(예: protoc)처럼 빌드 중 호스트에서 실행해야 하는 프로그램은 PC용을 그대로 써야 하기 때문이다.
6.1.5 sysroot 불일치 — 이 장에서 가장 중요한 함정
6.1.5.1 왜 생기나
크로스 컴파일 바이너리는 PC에 있는 헤더·라이브러리(sysroot)를 기준으로 만들어진다. 이 sysroot가 보드에서 실제로 사용되는 glibc·라이브러리 버전과 다르면, 빌드는 성공해도 실행이 실패한다. 전형적인 증상:
./hello-aarch64: /lib/aarch64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by ./hello-aarch64)
이는 PC 쪽 sysroot의 glibc가 보드보다 새 버전이라, 바이너리가 그 새 심볼을 참조해 버렸을 때 흔히 발생한다. UNO Q는 신제품이라 자료마다 Debian 코드명이 다르다 — 일부 커뮤니티 문서는 Debian 12(Bookworm), 일부는 Debian 13(Trixie)로 언급하며, 공식 Bookworm 이미지 배포도 확인된다(집필 시점 확정 어려움, 확인 필요 — 3.1 참조). 즉 "PC와 보드의 배포판 버전이 다를 수 있다"는 전제 자체가 이 보드에서는 특히 현실적이다. 반드시 보드에서 직접 확인하라:
ssh [email protected] cat /etc/os-release
ssh [email protected] ldd --version # glibc 버전 확인
6.1.5.2 해결책 네 가지
① 보드의 실제 sysroot를 그대로 복제한다 — 가장 확실한 방법. 헤더·라이브러리를 rsync로 PC에 내려받아 --sysroot로 지정하면 PC apt 저장소 버전과 무관하게 보드와 100% 일치한다.
mkdir -p sysroot/usr sysroot/lib
rsync -avL --rsync-path="sudo rsync" [email protected]:/usr/include sysroot/usr/
rsync -avL --rsync-path="sudo rsync" [email protected]:/usr/lib sysroot/usr/
rsync -avL --rsync-path="sudo rsync" [email protected]:/lib sysroot/
이후 GCC·Clang·CMake 모두 --sysroot=$(pwd)/sysroot(CMake는 CMAKE_SYSROOT)로 이 디렉터리를 가리키게 하면 된다. 단점은 보드 OS를 업데이트할 때마다 다시 동기화해야 한다는 것.
② 정적 링크로 런타임 glibc 의존을 없앤다(-static). 실행 시점 버전 문제는 사라지지만, glibc는 공식적으로 완전한 정적 링크를 권장하지 않는다(NSS 기반 이름 해석·DNS 조회 등 일부 기능이 정적 빌드에서 깨질 수 있다) — 네트워크 기능이 없는 단순 유틸리티에는 무난하지만, 모든 경우에 적합하지는 않다.
③ musl 기반 크로스 툴체인(aarch64-linux-musl-g++)으로 전환한다. musl은 애초에 정적 링크·이식성을 염두에 둔 C 라이브러리라 glibc 버전 문제 자체가 사라진다. 다만 apt로 바로 설치되지 않아 별도 배포판(예: musl.cc 사전빌드 툴체인)을 받아야 하는 손이 더 간다.
④ Docker로 PC 쪽 빌드 환경 자체를 보드와 같은 배포판으로 맞춘다 — 6.3에서 다루는 방법으로, sysroot를 수작업으로 관리하지 않아도 되는 대신 빌드 부하가 더 커진다.
견해: 프로젝트 하나만 가끔 빌드한다면 ①(sysroot rsync)이 가장 직관적이고 정확도가 높다. 팀·CI에서 반복적으로 빌드한다면 ④(Docker)로 재현성을 확보하는 편이 장기적으로 유리하다.
6.1.6 정리와 다음 장
- 크로스 컴파일은 PC의 CPU 자원으로 aarch64 바이너리를 만들어 보드 위 네이티브 빌드를 대체한다(6.1.1).
- GNU 툴체인(
aarch64-linux-gnu-g++)은 자료가 많고 Linux 호스트에서 가장 간단하다(6.1.2). Clang/LLVM은--target=로 여러 타깃을 한 컴파일러로 다루며 macOS에서 특히 유리하다(6.1.3). - CMake 프로젝트는 툴체인 파일 하나로 크로스 빌드를 캡슐화한다(6.1.4).
- sysroot 불일치(특히 glibc 버전)가 가장 흔한 함정이며, 보드 sysroot 복제·정적 링크·musl·Docker 네 가지 대응이 있다(6.1.5).
다음 6.2에서는 같은 문제를 Rust로 어떻게 푸는지(rustup target add·cross), 그리고 UNO Q 커뮤니티가 만든 스타터킷 unoq-starter-mpu의 실제 사용법을 본다.