Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

교차 컴파일 — 하나의 소스, 다양한 타겟 🟡

학습 내용:

  • Rust 타겟 트리플(target triples)의 작동 원리와 rustup으로 추가하는 방법
  • 컨테이너/클라우드 배포를 위한 정적 musl 바이너리 빌드
  • 네이티브 툴체인, cross, cargo-zigbuild를 이용한 ARM(aarch64) 교차 컴파일
  • 다중 아키텍처 CI를 위한 GitHub Actions 매트릭스 빌드 설정

참조: 빌드 스크립트 — 교차 컴파일 중 build.rs는 호스트(HOST)에서 실행됩니다 · 릴리스 프로필 — 교차 컴파일된 릴리스 바이너리를 위한 LTO 및 strip 설정 · Windows — Windows 교차 컴파일 및 no_std 타겟

교차 컴파일(Cross-compilation)이란 한 머신(호스트)에서 다른 머신(타겟)에서 실행될 실행 파일을 빌드하는 것을 의미합니다. 호스트는 여러분의 x86_64 노트북일 수 있고, 타겟은 ARM 서버, musl 기반 컨테이너, 또는 Windows 머신일 수 있습니다. Rust는 rustc 자체가 이미 교차 컴파일러로 설계되었기 때문에 이 과정이 매우 수월합니다. 적절한 타겟 라이브러리와 호환되는 링커만 있으면 됩니다.

타겟 트리플의 구조 (The Target Triple Anatomy)

모든 Rust 컴파일 타겟은 타겟 트리플(이름과 달리 보통 4개 부분으로 구성됨)로 식별됩니다:

<아키텍처>-<벤더>-<운영체제>-<환경>

예시:
  x86_64  - unknown - linux  - gnu      ← 표준 Linux (glibc)
  x86_64  - unknown - linux  - musl     ← 정적 Linux (musl libc)
  aarch64 - unknown - linux  - gnu      ← ARM 64비트 Linux
  x86_64  - pc      - windows- msvc     ← MSVC를 사용하는 Windows
  aarch64 - apple   - darwin             ← Apple 실리콘 기반 macOS
  x86_64  - unknown - none              ← 베어 메탈 (OS 없음)

사용 가능한 모든 타겟 목록 확인:

# rustc가 컴파일할 수 있는 모든 타겟 표시 (~250개)
rustc --print target-list | wc -l

# 시스템에 설치된 타겟 표시
rustup target list --installed

# 현재 기본 타겟 표시
rustc -vV | grep host

rustup을 이용한 툴체인 설치

# 타겟 라이브러리 추가 (해당 타겟용 Rust std)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu

# 이제 교차 컴파일이 가능합니다:
cargo build --target x86_64-unknown-linux-musl
cargo build --target aarch64-unknown-linux-gnu  # 링커가 필요함 — 아래 내용 참조

rustup target add가 제공하는 것: 해당 타겟용으로 미리 컴파일된 std, core, alloc 라이브러리입니다. C 링커나 C 라이브러리는 제공하지 않습니다. C 툴체인이 필요한 타겟(대부분의 gnu 타겟)의 경우 별도로 설치해야 합니다.

# Ubuntu/Debian — aarch64용 교차 링커 설치
sudo apt install gcc-aarch64-linux-gnu

# Ubuntu/Debian — 정적 빌드를 위한 musl 툴체인 설치
sudo apt install musl-tools

# Fedora
sudo dnf install gcc-aarch64-linux-gnu

.cargo/config.toml — 타겟별 설정

매번 명령을 내릴 때마다 --target을 전달하는 대신, 프로젝트 루트나 홈 디렉토리의 .cargo/config.toml에서 기본값을 설정할 수 있습니다.

# .cargo/config.toml

# 이 프로젝트의 기본 타겟 (선택 사항 — 네이티브 기본값을 유지하려면 생략)
# [build]
# target = "x86_64-unknown-linux-musl"

# aarch64 교차 컴파일을 위한 링커
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = ["-C", "target-feature=+crc"]

# musl 정적 빌드를 위한 링커 (보통 시스템 gcc가 작동함)
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
rustflags = ["-C", "target-feature=+crc,+aes"]

# ARM 32비트 (라즈베리 파이, 임베디드)
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

# 모든 타겟에 대한 환경 변수
[env]
# 예: 커스텀 시스루트(sysroot) 설정
# SYSROOT = "/opt/cross/sysroot"

설정 파일 검색 순서 (가장 먼저 발견되는 파일 적용):

  1. <project>/.cargo/config.toml
  2. <project>/../.cargo/config.toml (상위 디렉토리로 올라가며 검색)
  3. $CARGO_HOME/config.toml (보통 ~/.cargo/config.toml)

musl을 이용한 정적 바이너리 (Static Binaries with musl)

최소한의 컨테이너(Alpine, scratch Docker 이미지)나 glibc 버전을 제어할 수 없는 시스템에 배포할 때는 musl을 사용하여 빌드하세요:

# musl 타겟 설치
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools  # musl-gcc 제공

# 완전한 정적 바이너리 빌드
cargo build --release --target x86_64-unknown-linux-musl

# 정적 빌드 여부 확인
file target/x86_64-unknown-linux-musl/release/diag_tool
# → ELF 64-bit LSB executable, x86-64, statically linked

ldd target/x86_64-unknown-linux-musl/release/diag_tool
# → not a dynamic executable (동적 실행 파일이 아님)

정적 빌드 vs 동적 빌드 트레이드오프:

특성glibc (동적)musl (정적)
바이너리 크기작음 (공유 라이브러리 사용)큼 (~5-15 MB 증가)
이식성일치하는 glibc 버전 필요모든 Linux에서 실행 가능
DNS 해결nsswitch 완전 지원기본 리졸버 (mDNS 미지원)
배포시스루트나 컨테이너 필요의존성 없는 단일 바이너리
성능malloc이 약간 더 빠름malloc이 약간 더 느림
dlopen() 지원지원함지원 안 함

프로젝트 적용: 호스트 OS 버전을 보장할 수 없는 다양한 서버 하드웨어에 배포할 때 정적 musl 빌드가 이상적입니다. 단일 바이너리 배포 모델은 "내 컴퓨터에선 되는데" 문제를 제거합니다.

ARM (aarch64) 교차 컴파일

데이터 센터에서 ARM 서버(AWS Graviton, Ampere Altra, Grace 등) 사용이 점점 늘고 있습니다. x86_64 호스트에서 aarch64용으로 교차 컴파일하는 방법은 다음과 같습니다:

# 1단계: 타겟 및 교차 링커 설치
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu

# 2단계: .cargo/config.toml에 링커 설정 (위 내용 참조)

# 3단계: 빌드
cargo build --release --target aarch64-unknown-linux-gnu

# 4단계: 바이너리 확인
file target/aarch64-unknown-linux-gnu/release/diag_tool
# → ELF 64-bit LSB executable, ARM aarch64

타겟 아키텍처용 테스트 실행을 위해서는 다음 중 하나가 필요합니다:

  • 실제 ARM 머신
  • QEMU 유저 모드 에뮬레이션
# QEMU 유저 모드 설치 (x86_64에서 ARM 바이너리 실행)
sudo apt install qemu-user qemu-user-static binfmt-support

# 이제 cargo test가 QEMU를 통해 교차 컴파일된 테스트를 실행할 수 있습니다.
cargo test --target aarch64-unknown-linux-gnu
# (속도가 느림 — 각 테스트 바이너리가 에뮬레이션됨. 매일 하는 개발용이 아닌 CI 검증용으로 사용하세요.)

.cargo/config.toml에서 QEMU를 테스트 러너로 설정:

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu"

cross 도구 — Docker 기반 교차 컴파일

cross 도구는 미리 설정된 Docker 이미지를 사용하여 설정 과정이 필요 없는 교차 컴파일 경험을 제공합니다.

# cross 설치 (crates.io의 안정 버전)
cargo install cross
# 또는 최신 기능을 위해 git에서 설치:
# cargo install cross --git https://github.com/cross-rs/cross

# 교차 컴파일 — 툴체인 설정이 필요 없습니다!
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-unknown-linux-musl
cross build --release --target armv7-unknown-linux-gnueabihf

# 교차 테스트 — Docker 이미지에 QEMU가 포함되어 있음
cross test --target aarch64-unknown-linux-gnu

작동 원리: crosscargo를 대신하여 올바른 교차 컴파일 툴체인이 미리 설치된 Docker 컨테이너 내에서 빌드를 실행합니다. 여러분의 소스 코드가 컨테이너에 마운트되고, 결과물은 평소와 같이 target/ 디렉토리에 저장됩니다.

Cross.toml을 이용한 Docker 이미지 커스터마이징:

# Cross.toml
[target.aarch64-unknown-linux-gnu]
# 추가 시스템 라이브러리가 포함된 커스텀 Docker 이미지 사용
image = "my-registry/cross-aarch64:latest"

# 빌드 전 시스템 패키지 설치
pre-build = [
    "dpkg --add-architecture arm64",
    "apt-get update && apt-get install -y libpci-dev:arm64"
]

[target.aarch64-unknown-linux-gnu.env]
# 컨테이너에 환경 변수 전달
passthrough = ["CI", "GITHUB_TOKEN"]

cross는 Docker(또는 Podman)가 필요하지만, 교차 컴파일러, 시스루트, QEMU를 수동으로 설치할 필요를 없애줍니다. CI 환경에서 권장되는 방식입니다.

Zig를 교차 컴파일 링커로 사용하기

Zig는 C 컴파일러와 약 40개 타겟에 대한 교차 컴파일 시스루트를 약 40MB 용량의 단일 다운로드 파일에 포함하고 있습니다. 덕분에 Rust를 위한 매우 편리한 교차 링커로 활용될 수 있습니다.

# Zig 설치 (단일 바이너리, 패키지 매니저 불필요)
# https://ziglang.org/download/ 에서 다운로드
# 또는 패키지 매니저 이용:
sudo snap install zig --classic --beta  # Ubuntu
brew install zig                          # macOS

# cargo-zigbuild 설치
cargo install cargo-zigbuild

왜 Zig인가? 핵심 장점은 glibc 버전 타겟팅입니다. Zig를 사용하면 링크할 glibc 버전을 정확히 지정할 수 있어, 오래된 Linux 배포판에서도 바이너리가 실행되도록 보장할 수 있습니다.

# glibc 2.17 타겟 빌드 (CentOS 7 / RHEL 7 호환성)
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17

# glibc 2.28을 사용하는 aarch64 빌드 (Ubuntu 18.04+)
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28

# musl 빌드 (완전 정적)
cargo zigbuild --release --target x86_64-unknown-linux-musl

.2.17 접미사는 Zig의 확장 기능입니다. Zig 링커에 glibc 2.17 심볼 버전을 사용하도록 지시하여, 결과 바이너리가 CentOS 7 이상에서 실행될 수 있게 합니다. Docker, 시스루트 관리, 교차 컴파일러 설치가 모두 필요 없습니다.

비교: cross vs cargo-zigbuild vs 수동 방식

기능수동 방식crosscargo-zigbuild
설정 노력높음 (타겟별 툴체인 설치)낮음 (Docker 필요)낮음 (단일 바이너리)
Docker 필요 여부아니요아니요
glibc 버전 타겟팅불가 (호스트 glibc 사용)불가 (컨테이너 glibc 사용)가능 (정확한 버전 지정)
테스트 실행QEMU 필요포함되어 있음QEMU 필요
macOS → Linux어려움쉬움쉬움
Linux → macOS매우 어려움지원 안 함제한적 지원
바이너리 크기 오버헤드없음없음없음

CI 파이프라인: GitHub Actions 매트릭스

여러 타겟을 빌드하는 운영 환경 수준의 CI 워크플로 예시입니다:

# .github/workflows/cross-build.yml
name: Cross-Platform Build

on: [push, pull_request]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-x86_64
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            name: linux-x86_64-static
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-aarch64
            use_cross: true
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            name: windows-x86_64

    runs-on: ${{ matrix.os }}
    name: Build (${{ matrix.name }})

    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: musl 도구 설치
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: sudo apt-get install -y musl-tools

      - name: cross 설치
        if: matrix.use_cross
        run: cargo install cross

      - name: 빌드 (네이티브)
        if: "!matrix.use_cross"
        run: cargo build --release --target ${{ matrix.target }}

      - name: 빌드 (cross)
        if: matrix.use_cross
        run: cross build --release --target ${{ matrix.target }}

      - name: 테스트 실행
        if: "!matrix.use_cross"
        run: cargo test --target ${{ matrix.target }}

      - name: 아티팩트 업로드
        uses: actions/upload-artifact@v4
        with:
          name: diag_tool-${{ matrix.name }}
          path: target/${{ matrix.target }}/release/diag_tool*

적용 사례: 다중 아키텍처 서버 빌드

현재 바이너리에는 교차 컴파일 설정이 없습니다. 다양한 서버 집합에 배포되는 하드웨어 진단 도구의 경우, 다음을 추가하는 것이 권장됩니다:

my_workspace/
├── .cargo/
│   └── config.toml          ← 타겟별 링커 설정
├── Cross.toml                ← cross 도구 설정
└── .github/workflows/
    └── cross-build.yml       ← 3개 타겟을 위한 CI 매트릭스

권장 .cargo/config.toml:

# 프로젝트를 위한 .cargo/config.toml

# 릴리스 프로필 최적화 (이미 Cargo.toml에 있음, 참조용)
# [profile.release]
# lto = true
# codegen-units = 1
# panic = "abort"
# strip = true

# ARM 서버용 aarch64 (Graviton, Ampere, Grace)
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

# 이식성 있는 정적 바이너리를 위한 musl
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"

권장 빌드 타겟:

타겟사용 사례배포 대상
x86_64-unknown-linux-gnu기본 네이티브 빌드표준 x86 서버
x86_64-unknown-linux-musl정적 바이너리, 모든 배포판컨테이너, 최소 설치 호스트
aarch64-unknown-linux-gnuARM 서버Graviton, Ampere, Grace

핵심 통찰: 워크스페이스 루트의 Cargo.toml에 있는 [profile.release]에는 이미 lto = true, codegen-units = 1, panic = "abort", strip = true가 설정되어 있습니다. 이는 교차 컴파일된 배포용 바이너리에 이상적인 릴리스 프로필입니다(릴리스 프로필 장의 영향도 표 참조). musl과 결합하면 런타임 의존성이 없는 약 10MB 크기의 단일 정적 바이너리가 생성됩니다.

교차 컴파일 문제 해결 (Troubleshooting)

증상원인해결책
linker 'aarch64-linux-gnu-gcc' not found교차 링커 툴체인 누락sudo apt install gcc-aarch64-linux-gnu
cannot find -lssl (musl 타겟)시스템 OpenSSL이 glibc에 링크됨vendored 기능 사용: openssl = { version = "0.10", features = ["vendored"] }
build.rs가 잘못된 바이너리 실행build.rs는 타겟이 아닌 호스트에서 실행됨build.rs 내에서 cfg!(target_os) 대신 CARGO_CFG_TARGET_OS 확인
로컬 테스트는 통과하나 cross에서 실패Docker 이미지에 테스트 파일 누락Cross.toml을 통해 테스트 데이터 마운트: [build.env] volumes = ["./TestArea:/TestArea"]
undefined reference to __cxa_thread_atexit_impl타겟의 glibc 버전이 너무 낮음cargo-zigbuild로 명시적 glibc 버전 사용: --target x86_64-unknown-linux-gnu.2.17
ARM에서 바이너리 세그폴트 발생잘못된 ARM 변종으로 컴파일됨타겟 트리플이 하드웨어와 일치하는지 확인: 64비트 ARM은 aarch64-unknown-linux-gnu
런타임에 GLIBC_2.XX not found 발생빌드 머신의 glibc가 더 최신임정적 빌드를 위해 musl을 사용하거나, cargo-zigbuild로 glibc 버전 고정

교차 컴파일 의사결정 트리

flowchart TD
    START["교차 컴파일이 필요한가?"] --> STATIC{"정적 바이너리인가?"}
    
    STATIC -->|예| MUSL["musl 타겟\n--target x86_64-unknown-linux-musl"]
    STATIC -->|아니요| GLIBC{"오래된 glibc가 필요한가?"}
    
    GLIBC -->|예| ZIG["cargo-zigbuild\n--target x86_64-unknown-linux-gnu.2.17"]
    GLIBC -->|아니요| ARCH{"타겟 아키텍처?"}
    
    ARCH -->|"동일 아키텍처"| NATIVE["네이티브 툴체인\nrustup target add + 링커"]
    ARCH -->|"ARM/기타"| DOCKER{"Docker 사용 가능한가?"}
    
    DOCKER -->|예| CROSS["cross build\nDocker 기반, 설정 불필요"]
    DOCKER -->|아니요| MANUAL["수동 시스루트 설정\napt install gcc-aarch64-linux-gnu"]
    
    style MUSL fill:#91e5a3,color:#000
    style ZIG fill:#91e5a3,color:#000
    style CROSS fill:#91e5a3,color:#000
    style NATIVE fill:#e3f2fd,color:#000
    style MANUAL fill:#ffd43b,color:#000

🏋️ 실습

🟢 실습 1: 정적 musl 바이너리

아무 Rust 바이너리나 x86_64-unknown-linux-musl용으로 빌드해 보세요. fileldd를 사용하여 정적으로 링크되었는지 확인하세요.

솔루션
rustup target add x86_64-unknown-linux-musl
cargo new hello-static && cd hello-static
cargo build --release --target x86_64-unknown-linux-musl

# 확인
file target/x86_64-unknown-linux-musl/release/hello-static
# 출력: ... statically linked ...

ldd target/x86_64-unknown-linux-musl/release/hello-static
# 출력: not a dynamic executable (동적 실행 파일이 아님)

🟡 실습 2: GitHub Actions 교차 빌드 매트릭스

x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, aarch64-unknown-linux-gnu 세 가지 타겟을 빌드하는 GitHub Actions 워크플로를 매트릭스 전략을 사용하여 작성해 보세요.

솔루션
name: Cross-build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - x86_64-unknown-linux-musl
          - aarch64-unknown-linux-gnu
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: cross 설치
        run: cargo install cross --locked
      - name: 빌드
        run: cross build --release --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/my-binary

핵심 요약

  • Rust의 rustc는 이미 교차 컴파일러입니다. 적절한 타겟과 링커만 준비하면 됩니다.
  • musl은 런타임 의존성이 전혀 없는 완전한 정적 바이너리를 생성하며, 컨테이너 환경에 이상적입니다.
  • **cargo-zigbuild**는 기업용 Linux 타겟을 위한 "glibc 버전" 문제를 해결해 줍니다.
  • **cross**는 ARM이나 기타 특이한 타겟을 위한 가장 쉬운 방법입니다. Docker가 시스루트를 알아서 처리합니다.
  • 항상 fileldd를 사용하여 바이너리가 배포 타겟과 일치하는지 테스트하세요.