제6부 개발 워크플로우: 크로스 컴파일
6.3 Docker 기반 빌드와 SSH 원격 배포·디버깅
제6부의 마지막 장. 6.1(C++)·6.2(Rust)에서 PC에 직접 크로스 툴체인을 설치하는 방법을 다뤘다. 이 장은 그 툴체인을 Docker 컨테이너로 캡슐화하는 방법(dockcross류 이미지, QEMU 에뮬레이션)과, 빌드 산출물을 SSH로 배포하고 원격에서 디버깅하는 워크플로우를 다룬다. 세 장을 합치면 "PC에서 작성하고, 빌드하고, 보드로 보내고, 보드 위에서 그대로 디버깅한다"는 제6부의 목표가 완성된다.
6.3.1 세 가지 빌드 경로 정리
지금까지 다룬 크로스 빌드 방법을 한자리에 모으면 다음과 같다.
| 경로 | 설정 부담 | sysroot 정확도 | 속도 | 적합한 상황 |
|---|---|---|---|---|
| 직접 툴체인 설치(6.1·6.2.1) | apt 한 줄 | PC apt 버전에 의존 — 불일치 가능(6.1.5) | 가장 빠름 | 개인 PC, 소규모 프로젝트 |
cross(Rust 전용, 6.2.4) | Docker만 있으면 됨 | 이미지에 고정된 sysroot(보드와 다를 수 있음) | 빠름(컨테이너 오버헤드 적음) | Rust CI, musl·여러 타깃 병행 |
| Docker 크로스 이미지(dockcross류, 이 장) | Docker만 있으면 됨 | 이미지에 고정된 sysroot | 빠름 | C/C++ CI, 재현성 우선 |
| QEMU 에뮬레이션 네이티브 빌드(이 장) | Docker + binfmt 등록 | 보드와 동일 배포판이면 완전 일치 | 느림(에뮬레이션) | sysroot 불일치를 근본적으로 없애야 할 때 |
6.3.2 dockcross로 C/C++ 크로스 빌드하기
dockcross는 다양한 타깃용 크로스 툴체인(GCC, CMake, Ninja 등)을 이미 갖춘 Docker 이미지 모음이다. dockcross/linux-arm64 이미지가 aarch64(UNO Q의 MPU와 같은 계열)를 대상으로 한다. 베이스 이미지는 Debian Bookworm 기반에 GCC·make·autotools·CMake·Ninja·Git·Python이 포함돼 있다(이미지 버전에 따라 세부 구성이 달라질 수 있어 정확한 버전은 확인 필요).
6.3.2.1 실행 스크립트 생성
docker run --rm dockcross/linux-arm64 > ./dockcross-linux-arm64
chmod +x ./dockcross-linux-arm64
이 명령이 만드는 ./dockcross-linux-arm64는 컨테이너를 감싼 래퍼 스크립트다 — 이후 이 스크립트에 실제 빌드 명령을 인자로 넘기면, 현재 디렉터리를 컨테이너에 마운트하고 그 안에서 명령을 실행한 뒤 결과 파일만 호스트에 남긴다.
6.3.2.2 빌드
./dockcross-linux-arm64 cmake -Bbuild -S. -GNinja
./dockcross-linux-arm64 ninja -Cbuild
# 또는 Makefile 기반 프로젝트라면
./dockcross-linux-arm64 make
이미지 안에 CMake 툴체인 파일이 이미 구성돼 있어(정확한 경로는 이미지 문서를 확인) 6.1.4에서 직접 작성한 toolchain-aarch64.cmake 없이도 동작한다 — 다만 그 sysroot가 UNO Q의 실제 보드와 정확히 같다는 보장은 없다(6.1.5의 문제가 여전히 적용된다). 정확한 일치가 필요하면 다음 절의 QEMU 방법이나, 6.1.5.2의 sysroot 복제를 커스텀 Dockerfile에 반영하는 방법을 쓴다.
6.3.3 QEMU 에뮬레이션 — sysroot를 완전히 맞춰야 할 때
크로스 컴파일이 아니라, 보드와 동일한 배포판 이미지를 PC에서 에뮬레이션으로 그대로 실행해 네이티브 빌드하는 방법이다. 느리지만 sysroot 불일치 문제가 원천적으로 사라진다.
6.3.3.1 준비 — binfmt 등록
Docker Desktop(macOS/Windows)은 자동으로 되어 있는 경우가 많다. Linux 호스트는 한 번 등록이 필요하다:
docker run --rm --privileged tonistiigi/binfmt --install arm64
6.3.3.2 실행
docker run --rm --platform linux/arm64 \
-v "$(pwd)":/src -w /src \
debian:trixie \
bash -c "apt-get update && apt-get install -y build-essential && g++ -O2 hello.cpp -o hello-aarch64"
(보드가 실제로 Bookworm이라면 debian:bookworm으로 맞춘다 — 6.1.5.1에서 언급했듯 UNO Q의 정확한 기본 Debian 코드명은 확인 필요하니, ssh [email protected] cat /etc/os-release로 먼저 확인하고 같은 코드명의 이미지를 쓰는 것이 안전하다.)
이 방법은 --platform linux/arm64로 컨테이너가 aarch64로 기동되고, 그 안의 apt·g++도 전부 aarch64용이라 보드와 완전히 같은 소프트웨어 스택이 된다. 대가는 속도다 — QEMU 사용자 공간 에뮬레이션이라 대형 프로젝트(예: OpenCV 전체 빌드)에서는 네이티브 빌드보다도 느릴 수 있다. 다만 PC의 코어 수·클록이 보드보다 훨씬 높다면, 에뮬레이션 오버헤드를 감안해도 여전히 이득일 수 있다(프로젝트마다 다르므로 실측 권장).
견해: 평소에는 6.1·6.2의 직접 크로스 컴파일(가장 빠름)을 쓰고, GLIBC_2.xx not found 류의 sysroot 문제가 반복되거나 해결이 번거로운 프로젝트에서만 이 QEMU 방법으로 전환하는 것이 실용적이다.
6.3.4 SSH로 원격 배포하기
6.1·6.2에서 이미 scp 예시를 봤다 — 여기서는 반복 배포에 적합한 rsync 기반 스크립트로 정리한다.
#!/usr/bin/env bash
# deploy.sh — 빌드 산출물을 UNO Q로 동기화하고 실행
set -euo pipefail
DEVICE="${DEVICE:-unoq.local}"
DEVICE_USER="${DEVICE_USER:-arduino}"
REMOTE_DIR="${DEVICE_DIR:-/home/$DEVICE_USER/deploy}"
LOCAL_BIN="${1:?사용법: ./deploy.sh <로컬 바이너리 경로> [실행 인자...]}"
shift
ssh "$DEVICE_USER@$DEVICE" "mkdir -p $REMOTE_DIR"
rsync -avz --progress "$LOCAL_BIN" "$DEVICE_USER@$DEVICE:$REMOTE_DIR/"
BIN_NAME=$(basename "$LOCAL_BIN")
ssh "$DEVICE_USER@$DEVICE" "chmod +x $REMOTE_DIR/$BIN_NAME && $REMOTE_DIR/$BIN_NAME $*"
scp보다 rsync를 권장하는 이유는 변경분만 전송하기 때문이다 — 반복 빌드·배포 주기에서 매번 전체 파일을 다시 보내지 않는다. 사전에 ssh-copy-id [email protected]로 키 기반 인증을 설정해 두면(5.2) 비밀번호 입력 없이 스크립트를 자동화할 수 있다.
재부팅 후에도 앱이 자동 실행되게 하려면 systemd 유닛을 보드에 등록하는 방법도 있다(개요만 소개 — 유닛 파일 자체는 표준 systemd 문법을 따른다):
# /etc/systemd/system/myapp.service (보드 위에서 작성)
[Unit]
Description=My UNO Q App
After=network.target
[Service]
ExecStart=/home/arduino/deploy/myapp
Restart=on-failure
User=arduino
[Install]
WantedBy=multi-user.target
ssh [email protected] 'sudo systemctl enable --now myapp.service'
6.3.5 원격 디버깅 — gdbserver
보드 위에서 크래시하는 버그를 매번 로그만으로 추적하는 것은 비효율적이다. gdbserver로 PC의 GDB가 보드 위 프로세스를 직접 제어할 수 있다.
6.3.5.1 준비
보드(UNO Q, Debian aarch64)에:
sudo apt install gdbserver
PC에 아키텍처 무관 멀티아치 GDB(Debian/Ubuntu):
sudo apt install gdb-multiarch
(gdb-multiarch 대신 6.1.2에서 설치한 GNU 툴체인에 포함된 aarch64-linux-gnu-gdb를 써도 된다.)
6.3.5.2 기본 워크플로우 — TCP 포트
보드에서 대상 프로그램을 gdbserver로 실행한다(포트는 임의로 정할 수 있다):
ssh [email protected]
./deploy/myapp-debug # 이렇게 직접 실행하지 말고
gdbserver :2345 ./deploy/myapp-debug # gdbserver로 감싸서 실행
이미 실행 중인 프로세스에 연결(attach)하는 것도 가능하다: gdbserver :2345 --attach <PID>.
PC에서:
gdb-multiarch ./target/aarch64-unknown-linux-gnu/debug/myapp-debug
(gdb) set sysroot ./sysroot # 6.1.5.2에서 만든 보드 sysroot 재사용 — 공유 라이브러리 심볼 해석에 필요
(gdb) target remote unoq.local:2345
(gdb) break main
(gdb) continue
set sysroot를 생략하면 공유 라이브러리(libc 등) 심볼이 맞지 않아 백트레이스가 부정확해질 수 있다 — 6.1.5에서 만든 sysroot 디렉터리가 여기서도 그대로 쓸모 있다.
6.3.5.3 SSH 파이프 변형 — 포트를 열지 않는 방법
방화벽·보안 정책상 TCP 포트를 새로 열기 꺼려진다면, gdbserver의 표준입출력을 SSH로 그대로 연결하는 방법이 있다:
gdb-multiarch -ex "target remote | ssh [email protected] gdbserver - ./deploy/myapp-debug" \
./target/aarch64-unknown-linux-gnu/debug/myapp-debug
gdbserver -의 -는 표준입출력을 통신 채널로 쓰라는 뜻이다. 이 방식은 SSH 연결 자체가 이미 인증·암호화를 제공하므로 별도 포트 노출이 필요 없다는 것이 장점이다.
6.3.6 VS Code에서 원격 디버깅 설정
C/C++ 확장(cppdbg)을 쓴다면 launch.json에 miDebuggerServerAddress를 지정해 위 TCP 방식을 그대로 IDE에 연결할 수 있다.
{
"version": "0.2.0",
"configurations": [
{
"name": "UNO Q 원격 디버그 (gdbserver)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/target/aarch64-unknown-linux-gnu/debug/myapp-debug",
"miDebuggerServerAddress": "unoq.local:2345",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"setupCommands": [
{ "text": "set sysroot ${workspaceFolder}/sysroot" }
]
}
]
}
디버그 세션 시작 전 보드에서 gdbserver :2345 ./deploy/myapp-debug를 먼저 실행해 두어야 한다(자동화하려면 preLaunchTask로 SSH 명령을 미리 실행하는 태스크를 연결할 수 있다). Rust 프로젝트라면 program 경로만 해당 크레이트의 디버그 빌드 산출물로 바꾸면 동일하게 동작한다(디버그 심볼이 있는 빌드를 배포해야 한다 — --release가 아니라 기본 cargo build --target aarch64-unknown-linux-gnu).
6.3.7 제6부 정리 — 크로스 컴파일에서 배포까지
- 6.1는 C/C++을, 6.2은 Rust를 PC에서 aarch64로 크로스 컴파일하는 방법과 sysroot·glibc 불일치라는 공통 함정을 다뤘다.
- 이 장(6.3)은 그 툴체인을 Docker로 캡슐화하는 두 가지 방식 — 크로스 이미지(dockcross, 빠르지만 sysroot는 이미지 고정)와 QEMU 에뮬레이션(느리지만 보드와 완전 일치) — 그리고
rsync/SSH 배포·gdbserver원격 디버깅을 다뤘다. - 세 장을 관통하는 원칙: "먼저 빠른 방법(직접 툴체인)으로 시작하고, sysroot 문제가 해결되지 않으면 정확도가 높은 방법(sysroot 복제 → QEMU)으로 단계적으로 옮겨간다."
다음 제7부(7.1~)는 미들웨어·데이터 계층 — ROS2 Jazzy, Zenoh, PostgreSQL·SQLite로 이어진다. 다만 제7부는 이 장의 PC 크로스 컴파일·Docker 워크플로우를 그대로 가져다 쓰지 않는다 — 7.1의 ROS2 소스 빌드·Docker 이미지는 보드 위에서 직접 수행하는 별도 경로이며, 두 부는 제5부까지만 공통 전제로 할 뿐 서로 독립적이다(목차 2).