애플에서 M1 프로세서를 출시하고 나서부터 ARM 아키텍처가 각광받고 있지만, 보통 ARM 아키텍처를 사용하는 환경은 저전력, 제한된 리소스를 사용하기 때문에 쾌적한 빌드환경을 갖추기가 어렵다. 보통은 이러한 제한때문에 대부분의 개발 작업을 x86환경에서 크로스 컴파일을 사용해 타겟 아키텍처에서 실행가능한 형태로 빌드한다.

회사에서 raspberry PI 혹은 nvidia jetson 보드를 이용해 신제품을 프로토타이핑 하는 경우가 잦았는데, 타겟 보드에서 테스트를 하려고 할 때마다 빌드 시간이 너무 느려 답답한 경우가 많았다. 그래서 크로스 컴파일 환경을 한번 갖춰놓으니 빌드 시간도 단축시킬 수 있었고, CI 서버에도 크로스 컴파일 자동화를 적용하여 전체적인 개발 속도를 높일 수 있었다.

abi, eabi?

크로스컴파일 툴체인을 설치하기 위해 검색하다 보면

  • aarch64-none-linux-**
  • aarch64-linux-gnu-**
  • aarch64-linux-gnueabi-**

등 여러 단어로 조합된 다양한 종류의 툴체인을 볼 수 있다.

일반적으로 toolchain의 네이밍은 [arch]-[vendor]-(os-)abi의 형태로 이루어진다.

예를들어 aarch64-none-linux-gnueabi라고 한다면 aarch64아키텍처를 사용하고, 특정 vendor가 없는 즉, bare-metal을 뜻하고, linux OS에 맞춘 eabi전용 toolchain을 뜻한다.

마지막에 붙는 ABI 혹은 EABI는 Application Binary Interface의 줄임말로 직역하면 응용프로그램 이진 인터페이스이다. 간단하게 API가 소스코드 레벨에서의 호환성을 뜻한다면 ABI는 Low-level관점에서의 호환성을 뜻한다. EABI에서 E는 “Embedded“를 뜻하며, ARM에서 PowerPC등 임베디드 환경에 적합하도록 개정한 ABI표준을 뜻한다.

aarch64 툴체인 설치

우선 아래의 명령어로 aarch64 툴체인을 설치한다

$ sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

ubuntu 18.04 버전 기준으로 gcc/g++ 7.5.0 버전이 설치된다.
설치가 완료되었으면 아래처럼 툴체인에 포함된 파일을들 확인해볼수 있다.

$ find /usr/bin -type f | grep aarch64
/usr/bin/aarch64-linux-gnu-gcc-ar-7
/usr/bin/aarch64-linux-gnu-readelf
/usr/bin/aarch64-linux-gnu-ld.bfd
/usr/bin/aarch64-linux-gnu-gcc-nm-7
/usr/bin/aarch64-linux-gnu-gcc-nm
/usr/bin/aarch64-linux-gnu-cpp-7
/usr/bin/aarch64-linux-gnu-objcopy
/usr/bin/aarch64-linux-gnu-g++-7
/usr/bin/aarch64-linux-gnu-gprof
/usr/bin/aarch64-linux-gnu-gcc-ar
/usr/bin/aarch64-linux-gnu-c++filt
/usr/bin/aarch64-linux-gnu-objdump
/usr/bin/aarch64-linux-gnu-gcc-7
/usr/bin/aarch64-linux-gnu-ranlib
/usr/bin/aarch64-linux-gnu-gcov-tool
/usr/bin/aarch64-linux-gnu-g++
/usr/bin/aarch64-linux-gnu-ld.gold
/usr/bin/aarch64-linux-gnu-nm
/usr/bin/aarch64-linux-gnu-gcov-7
/usr/bin/aarch64-linux-gnu-gcov
/usr/bin/aarch64-linux-gnu-gcov-dump-7
/usr/bin/aarch64-linux-gnu-strip
/usr/bin/aarch64-linux-gnu-ld
/usr/bin/aarch64-linux-gnu-elfedit
/usr/bin/aarch64-linux-gnu-size
/usr/bin/aarch64-linux-gnu-as
/usr/bin/aarch64-linux-gnu-gcov-tool-7
/usr/bin/aarch64-linux-gnu-gcc-ranlib-7
/usr/bin/aarch64-linux-gnu-gcov-dump
/usr/bin/aarch64-linux-gnu-dwp
/usr/bin/aarch64-linux-gnu-gcc-ranlib
/usr/bin/aarch64-linux-gnu-cpp
/usr/bin/aarch64-linux-gnu-addr2line
/usr/bin/aarch64-linux-gnu-gcc
/usr/bin/aarch64-linux-gnu-ar
/usr/bin/aarch64-linux-gnu-strings

위의 툴체인이 정상적으로 설치가 되었다면 크로스 컴파일을 위한 모든 준비는 완료된 것이다.
간단한 예제코드로 제대로 크로스 컴파일이 되는지 확인한다

#include <iostream>
int main() {
    std::cout << "Hello World!\n";
    return 0;
}
$ g++ hello.cpp -o hello
$ g++-aarch64-linux-gnu hello.cpp -o hello_aarch64

하나는 일반적인 g++, 다른 하나는 방금 설치한 g++-aarch64-linux-gnu로 빌드한다.

두 파일을 file 커맨드로 비교해보면

$ file hello
hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=3fe43e50a35fbee86be5bf31079a9dac3977761b, not stripped
$ file hello
hello_aarch64: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 3.7.0, BuildID[sha1]=12564e9f3a43d6f629f3e89520bb95a7b375fff5, not stripped

각각 x86-64, aarch64 아키텍처에 맞게 빌드가 된 것을 볼수 있다. 당연하게도 aarch64로 빌드된 파일을 x86에서 실행시키려고 하면 아래 처럼 오류가 난다.

-bash: ./hello_aarch64: cannot execute binary file: Exec 형식 오류

CMake 프로젝트에 적용하기

실제 프로젝트에서는 cmake,meson등 빌드 시스템으로 프로젝트가 복잡하게 구성돼있으므로, 위 처럼 명령어 한줄로 크로스 컴파일을 할 수는 없다.

cmake는 크로스 컴파일을 구성하는데 도움을 주는 공식 문서를 제공한다. 문서에서 설명하는 방법을 통해 실제 프로젝트에서는 어떻게 크로스 컴파일을 적용해야할지 테스트를 해보자

# the name of the target operating system
set(CMAKE_SYSTEM_NAME Windows)

# which compilers to use for C and C++
set(CMAKE_C_COMPILER   i586-mingw32msvc-gcc)
set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)

# where is the target environment located
set(CMAKE_FIND_ROOT_PATH  /usr/i586-mingw32msvc
    /home/alex/mingw-install)

# adjust the default behavior of the FIND_XXX() commands:
# search programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# search headers and libraries in the target environment
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)