사전 크로스 컴파일
개념

크로스 컴파일

gabury1고친 사람 github-actions[bot]

크로스 컴파일은 지금 쓰는 컴퓨터와 다른 종류의 기계에서 돌 프로그램을 만드는 일입니다. 노트북에서 빌드한 프로그램을 종류가 다른 서버나 작은 보드로 옮겨 실행하는 식입니다. 돌릴 기계에서 직접 빌드하기 어려운 일이 많아 생긴 방법입니다.

쉽고 빠른 이해

크로스 컴파일은 빌드하는 컴퓨터와 실행할 컴퓨터가 다를 때 하는 빌드입니다. 노트북에서 작은 보드용 실행 파일을 만드는 것이 그 예입니다.

프로세서는 종류마다 알아듣는 명령이 다릅니다. 한 종류에 맞춰 만든 실행 파일은 다른 종류에서 실행되지 않습니다. 작은 기기는 너무 느리거나 빌드 도구를 올릴 수 없어서 직접 빌드하기도 어렵습니다. 실행할 기계에서 바로 빌드할 수 있으면 굳이 하지 않습니다.

어떻게 도나:

  1. 실행할 기계의 명령을 내놓는 컴파일러를 빌드하는 컴퓨터에 깝니다
  2. 실행할 기계용 라이브러리도 한 벌 갖춰 함께 묶습니다
  3. 나온 실행 파일을 실행할 기계로 옮겨 돌립니다

대가는 준비가 번거롭다는 점입니다. 도구와 라이브러리를 실행할 기계마다 따로 갖춰야 합니다. 빌드한 컴퓨터에서 결과를 바로 돌려 볼 수도 없습니다.

상세

자동차 공장이 한국에 있어도, 영국으로 보낼 차는 운전석을 오른쪽에 달아 만듭니다. 차는 만든 나라가 아니라 달릴 나라의 길에 맞춥니다.

컴파일러는 사람이 쓴 소스 코드를 기계어로 옮기는 프로그램입니다. 기계어는 프로세서가 번역 없이 곧바로 실행하는 명령입니다. 이 명령은 프로세서 종류마다 다르게 적힙니다.

그래서 컴파일러마다 어느 기계의 기계어를 내놓을지 정해져 있습니다. 보통은 컴파일러 자신이 도는 기계와 같은 종류를 고릅니다. 이렇게 하는 빌드를 네이티브 컴파일이라고 부릅니다.

크로스 컴파일은 그 둘을 떼어 놓습니다. 컴파일러는 이 기계에서 실행됩니다. 내놓는 기계어는 다른 기계의 것입니다. 다른 기계용 결과를 내놓는 컴파일러가 크로스 컴파일러입니다.

백엔드 개발에서 흔히 만나는 예가 있습니다. ARM 칩을 쓰는 노트북으로 개발합니다. 배포할 서버는 x86-64 칩을 씁니다. ARM 과 x86-64 는 둘 다 프로세서 계열의 이름입니다(ARM 은 Advanced RISC Machine, RISC 는 Reduced Instruction Set Computer 의 줄임말입니다).

이때 노트북에서 평소대로 빌드한 실행 파일을 서버로 옮기면 서버는 그 파일을 실행하지 못합니다. 서버가 알아듣지 못하는 기계어이기 때문입니다. 서버용 기계어를 내놓도록 빌드해야 합니다.

돌릴 기계에서 직접 빌드할 수 있으면 이런 일이 필요 없습니다. 그런데 그러기 어려운 기계가 많습니다. 작은 임베디드 기기는 컴파일러를 돌릴 만큼 메모리가 없거나 너무 느립니다. 운영체제가 없어 빌드 도구를 올릴 수조차 없는 기기도 있습니다.

컴퓨터 한 대에서 여러 종류의 기계용 결과를 한꺼번에 만들 때도 씁니다. 명령줄 도구 하나를 리눅스·맥·윈도우용으로, 다시 ARM 용과 x86-64 용으로 나눠 배포 파일을 여섯 벌 내놓는 경우가 그렇습니다. 지속적 통합 서버 한 대가 그 조합을 전부 빌드합니다.

호스트와 타깃

크로스 컴파일에는 기계가 둘 나옵니다. 앞의 노트북과 서버를 가지고 두 기계에 이름을 붙입니다.

컴파일러가 도는 기계를 호스트, 결과물이 돌 기계를 타깃이라고 부릅니다. 앞의 예에서는 노트북이 호스트, 서버가 타깃입니다. 자동차로 치면 공장이 호스트, 차가 달릴 나라가 타깃입니다. 네트워크에서 말하는 호스트와는 다른 뜻입니다.

호스트와 타깃이 같으면 네이티브 컴파일입니다. 다르면 크로스 컴파일입니다.

빌드는 전부 호스트에서 끝납니다. 타깃은 다 만든 실행 파일을 받아 돌리기만 합니다.

flowchart TD
    subgraph H["호스트 · 개발 노트북"]
        S["소스 코드"] --> C["크로스 컴파일러"]
        C --> E["타깃용 실행 파일"]
    end
    subgraph T["타깃 · 배포할 서버"]
        R["실행"]
    end
    E -->|"복사해 옮긴다"| R

그림은 컴파일러가 실행 파일을 바로 내놓는 것처럼 줄여 그렸습니다. 실행 파일이 되기까지 거치는 도구는 아래 「크로스 툴체인과 sysroot」에서 나눕니다.

호스트와 타깃을 가르는 세 가지 차이

두 기계가 「다르다」고 할 때 다를 수 있는 것은 셋입니다. 셋 중 하나만 달라도 결과물이 실행되지 않습니다.

첫째는 프로세서가 알아듣는 명령의 목록입니다. 이 목록이 명령어 집합입니다. ARM 과 x86-64 는 명령어 집합이 달라서 같은 덧셈도 다른 숫자로 적힙니다.

둘째는 운영체제입니다. 프로그램은 파일을 열거나 네트워크로 데이터를 보낼 때 운영체제에 부탁합니다. 이 부탁이 시스템 콜입니다. 부탁하는 방법은 운영체제마다 다릅니다.

실행 파일 형식도 운영체제마다 다릅니다. 실행 파일 형식은 기계어와 데이터를 파일 안 어디에 둘지 정한 틀입니다. 운영체제는 이 틀을 보고 파일을 메모리에 올립니다. 그래서 프로세서가 같아도 리눅스용 실행 파일은 윈도우에서 실행되지 않습니다.

셋째는 실행 파일과 라이브러리가 서로 맞춘 약속입니다. 함수를 부를 때 인자를 어디에 넣는지, 자료를 메모리에 어떤 모양으로 놓는지 같은 것입니다. 이 약속을 ABI(Application Binary Interface, 응용 프로그램 이진 인터페이스)라고 부릅니다.

셋을 한 표로 모으면 이렇습니다.

차이 무엇이 다른가 어긋나면
명령어 집합 프로세서가 알아듣는 명령 프로세서가 명령을 못 읽습니다
운영체제 시스템 콜 · 실행 파일 형식 운영체제가 파일을 실행하지 못합니다
ABI 함수 호출과 자료 배치의 약속 라이브러리를 부를 때 죽거나 틀린 값을 받습니다

앞의 둘은 실행하자마자 드러납니다. 셋째는 실행 파일이 뜬 뒤 라이브러리를 부를 때 가서야 드러나기도 합니다.

컴파일러와 빌드 도구는 타깃을 이 셋을 이어 붙인 이름 하나로 적습니다. 이 이름이 타깃 트리플입니다. 「트리플」은 세 토막이라는 뜻입니다. 만든 회사 이름을 끼워 네 토막이 되기도 합니다.

아래는 두 타깃의 이름입니다. 앞 토막이 프로세서, 가운데가 운영체제, 끝이 ABI 입니다. 끝의 gnu 는 리눅스에서 흔히 쓰는 ABI 의 이름입니다.

aarch64-linux-gnu  // 64비트 ARM · 리눅스
x86_64-linux-gnu   // x86-64 · 리눅스

두 이름은 앞 토막만 다릅니다. 운영체제와 ABI 는 같고 프로세서만 다른 두 기계라는 뜻입니다.

크로스 툴체인과 sysroot

크로스 컴파일러 하나만 깔아서는 빌드가 안 끝납니다. 컴파일러가 내놓은 결과를 실행 파일로 묶는 도구와, 타깃에서 쓸 라이브러리가 더 필요합니다.

컴파일러가 내놓는 것은 실행 파일의 조각입니다. 조각들을 이어 붙여 실행 파일 하나로 만드는 프로그램이 링커입니다. 링커도 타깃의 실행 파일 형식을 알아야 하므로 타깃용이 따로 있어야 합니다.

프로그램은 대개 남이 만든 코드를 불러 씁니다. 화면에 글자를 찍거나 파일을 여는 기본 기능은 표준 라이브러리가 줍니다. 호스트에 깔린 라이브러리는 호스트의 기계어로 되어 있어서 타깃용 실행 파일에 섞을 수 없습니다. 그래서 타깃용으로 빌드된 라이브러리가 한 벌 더 필요합니다.

라이브러리에는 헤더 파일이 따라옵니다. 함수 이름과 인자 모양을 적어 둔 파일입니다. 컴파일러는 이것을 보고 함수를 부르는 코드를 만듭니다. 헤더도 타깃의 것을 써야 자료 크기와 배치가 타깃과 맞습니다.

타깃의 라이브러리와 헤더를 호스트의 폴더 하나에 모아 둔 것을 sysroot라고 부릅니다. 타깃 기계의 파일 구조를 흉내 낸 폴더입니다. 컴파일러와 링커는 호스트의 파일 대신 이 폴더를 뒤집니다.

크로스 컴파일러와 링커, sysroot 처럼 빌드에 드는 도구를 한 벌로 묶은 것이 툴체인입니다. 툴체인은 타깃마다 한 벌씩 따로 갖춥니다.

툴체인 안에서 무엇이 무엇을 쓰는지 그리면 이렇습니다.

flowchart TD
    subgraph TC["타깃용 툴체인 · 호스트에 깐다"]
        CC["크로스 컴파일러"]
        LD["타깃용 링커"]
        subgraph SR["sysroot"]
            HD["타깃용 헤더 파일"]
            LIB["타깃용 라이브러리"]
        end
    end
    SRC["소스 코드"] --> CC
    HD --> CC
    CC --> OBJ["실행 파일 조각"]
    OBJ --> LD
    LIB --> LD
    LD --> OUT["타깃용 실행 파일"]

헤더는 컴파일러가, 라이브러리는 링커가 가져다 씁니다. 둘 다 호스트의 것이 아니라 sysroot 안의 타깃 것입니다.

정적 링크와 동적 링크

라이브러리를 실행 파일에 붙이는 방법이 둘 있습니다. 어느 쪽을 고르느냐에 따라 크로스 컴파일한 결과가 타깃에서 얼마나 쉽게 실행되는지가 갈립니다.

정적 링크는 빌드할 때 라이브러리 코드를 실행 파일 안에 복사해 넣습니다. 동적 링크는 실행 파일에 라이브러리 이름만 적어 둡니다. 실행할 때 타깃에 깔린 라이브러리를 찾아 붙입니다.

동적 링크로 만든 실행 파일은 타깃에 맞는 라이브러리가 깔려 있어야 실행됩니다. 호스트의 sysroot 에 둔 버전과 타깃에 깔린 버전이 다르면 실행할 때 어긋납니다. 두 기계가 떨어져 있어서 이 어긋남을 빌드할 때 알아채기 어렵습니다.

정적 링크로 만들면 필요한 코드가 실행 파일 하나에 다 들어갑니다. 타깃에 무엇이 깔렸는지 덜 따져도 됩니다. 그래서 크로스 컴파일한 결과를 정적 링크로 내보내는 일이 흔합니다.

정적 링크의 대가는 둘입니다. 실행 파일이 커집니다. 라이브러리에 보안 패치가 나오면 실행 파일을 다시 빌드해야 합니다.

호스트에서 돌려 볼 수 없는 결과물

크로스 컴파일에서 가장 번거로운 것은 테스트입니다. 네이티브 컴파일과 견주면 드러납니다.

네이티브 컴파일에서는 빌드한 뒤 바로 실행해 테스트를 돌립니다. 크로스 컴파일한 실행 파일은 호스트가 못 알아듣는 기계어라 호스트에서 돌지 않습니다. 테스트를 돌리려면 결과물을 타깃으로 옮겨야 합니다.

옮기지 않고 돌리는 길이 에뮬레이터입니다. 에뮬레이터는 다른 프로세서의 명령을 소프트웨어로 흉내 내어 실행하는 프로그램입니다. 호스트에서 타깃용 실행 파일을 돌려 볼 수 있습니다. 명령마다 흉내를 내는 만큼 진짜 기계보다 느립니다.

빌드 도중에 이 문제가 튀어나오기도 합니다. 어떤 빌드는 작은 프로그램을 만들어 바로 실행해 보고 설정을 정합니다. 이 기계에서 정수 하나가 몇 바이트인지 재는 프로그램이 그런 예입니다.

크로스 컴파일에서는 그 작은 프로그램이 문제가 됩니다. 타깃용으로 만들면 호스트에서 실행되지 않습니다. 호스트용으로 만들면 돌기는 하지만 타깃이 아니라 호스트의 값을 재 옵니다.

크로스 컴파일을 피하는 길

크로스 컴파일은 준비가 번거로워서 피할 수 있으면 피합니다. 피하는 길은 셋입니다. 타깃에서 직접 빌드하기, 에뮬레이터 안에서 빌드하기, 바이트코드로 배포하기입니다.

셋째 길에는 낱말 둘이 먼저 필요합니다. 바이트코드는 진짜 프로세서가 아니라 소프트웨어로 만든 기계가 읽는 명령입니다. 그 소프트웨어 기계가 가상 머신입니다. 가상 머신을 타깃마다 따로 만들어 두면 바이트코드 파일 하나가 여러 종류의 기계에서 실행됩니다.

길 어떻게 대가
타깃에서 직접 빌드 타깃이 빌드할 만큼 힘이 있으면 네이티브 컴파일을 합니다 타깃 종류마다 빌드할 기계를 따로 둬야 합니다
에뮬레이터 안에서 빌드 호스트에 타깃을 흉내 낸 환경을 띄우고 그 안에서 네이티브 컴파일을 합니다 빌드가 느립니다
바이트코드로 배포 기계어 대신 바이트코드를 내놓고 타깃의 가상 머신이 실행합니다 타깃에 가상 머신이 깔려 있어야 합니다

셋째 길을 고른 언어라도 기계어로 된 라이브러리를 끼워 쓰면 그 부분은 타깃마다 따로 빌드해야 합니다. 자바 서버가 압축이나 암호화를 맡는 기계어 라이브러리를 부를 때가 그렇습니다.

관련 항목

크로스 툴체인을 이루는 도구

크로스 컴파일러 · 툴체인 · 링커 · 어셈블러 · sysroot · 헤더 파일 · 표준 라이브러리

타깃을 가르는 기계의 특성

명령어 집합 · CPU · ABI · 운영체제 · 시스템 콜 · 엔디언 · 타깃 트리플 · 실행 파일 형식

크로스 컴파일로 오가는 프로세서 계열

ARM · x86-64 · x86 · RISC-V · MIPS

타깃용 결과물에 라이브러리를 붙이는 방식

정적 링크 · 동적 링크 · 공유 라이브러리 · 단일 바이너리 · glibc · musl

크로스 컴파일을 대신할 수 있는 다른 수단

네이티브 컴파일 · 에뮬레이터 · QEMU · 바이트코드 · 가상 머신 · 인터프리터

크로스 컴파일에 쓰이는 컴파일러와 언어

GCC · Clang · LLVM · Go · Rust · Zig

크로스 컴파일한 결과가 실리는 기기와 배포물

임베디드 · 펌웨어 · 마이크로컨트롤러 · 부트로더 · 컨테이너 이미지 · 멀티 아키텍처 이미지

크로스 컴파일이 속하는 상위 과정

컴파일러 · 컴파일 · 빌드 · 기계어 · 빌드 시스템 · 지속적 통합

크로스 컴파일을 겹쳐 쓰는 빌드 방식

캐나다 크로스 · 부트스트래핑 · 셀프 호스팅

다른 이름: cross compilation · cross-compilation · cross compile · 크로스컴파일 · 교차 컴파일