인디노트

Android NDK - 독립 실행형 툴체인 본문

소스 팁/Java, Android, Kotlin

Android NDK - 독립 실행형 툴체인

인디개발자 2019. 2. 1. 17:04

https://developer.android.com/ndk/guides/standalone_toolchain?hl=ko


독립 실행형 툴체인

Android NDK와 함께 독립적으로 제공되거나 기존 IDE와 함께 플러그인으로 제공되는 툴체인을 사용할 수 있습니다. 이미 자체적인 빌드 시스템이 있고 이 시스템을 위한 Android에 지원을 추가하기 위해 크로스 컴파일러를 호출하는 기능만 필요한 경우 이러한 유연성이 유용할 수 있습니다.

전형적인 사용 사례는 CC 환경 변수에 당연히 크로스 컴파일러가 있을 것으로 생각되는 오픈 소스 라이브러리의 구성 스크립트를 호출하는 것입니다.

참고: 이 페이지에서는 컴파일, 링크 및 저수준 아키텍처에 대한 독자의 이해도가 상당한 수준인 것으로 가정하고 설명합니다. 뿐만 아니라, 여기서 설명하는 기술은 대부분의 사용 사례에는 불필요한 것입니다. 대부분의 경우, NDK 빌드 시스템을 고수하는 대신 독립 실행형 툴체인을 먼저 사용해보는 것이 좋습니다.

툴체인 선택

무엇보다도 먼저, 독립 실행형 툴체인이 대상으로 삼을 처리 아키텍처를 정할 필요가 있습니다. 표 1에 나와 있는 것처럼, 각 아키텍처는 서로 다른 툴체인 이름에 대응됩니다.

표 1. 다양한 명령 집합에 대한 APP_ABI 설정

아키텍처툴체인 이름
ARM 기반arm-linux-androideabi-<gcc-version>
x86 기반x86-<gcc-version>
MIPS 기반mipsel-linux-android-<gcc-version>
ARM64 기반aarch64-linux-android-<gcc-version>
X86-64 기반x86_64-<gcc-version>
MIPS64 기반mips64el-linux-android--<gcc-version>

Sysroot 선택

다음으로 해야 할 일은 sysroot를 정의하는 것입니다. sysroot는 대상에 대한 시스템 헤더와 라이브러리를 포함한 디렉터리입니다. sysroot를 정의하려면 네이티브 지원을 위해 대상으로 삼으려는 Android API 레벨을 알아야 합니다. 사용 가능한 네이티브 API는 Android API 레벨에 따라 다릅니다.

각 Android API 레벨에 대한 네이티브 API는 $NDK/platforms/ 아래에 있고, 각 API 레벨 디렉터리는 다양한 CPU와 아키텍처에 대한 하위 디렉터리를 포함합니다. 다음 예시에서는 ARM 아키텍처에 대해 Android 5.0(API 레벨 21)을 대상으로 한 빌드에 대해 sysroot를 정의하는 방법을 보여줍니다.

SYSROOT=$NDK/platforms/android-21/arch-arm
Android API 레벨과 이러한 레벨에서 지원하는 각 네이티브 API에 대한 자세한 내용은 Android NDK 네이티브 API를 참조하세요.

컴파일러 호출

컴파일러를 호출하는 두 가지 방법이 있습니다. 한 가지 방법은 간단하지만 빌드 시스템으로의 리프팅은 대부분 남겨둡니다. 다른 방법은 더 고급스럽고 복잡한 방법이지만, 유연성이 더 뛰어납니다.

간단한 방법

가장 간단하게 빌드하는 방법은 명령줄에서 직접 알맞은 컴파일러를 호출하는 방법이며, 이때 대상으로 삼은 플랫폼을 위한 시스템 파일의 위치를 표시하려면 --sysroot 옵션을 사용합니다. 예:

export CC="$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/ \ linux-x86/bin/arm-linux-androideabi-gcc-4.8 --sysroot=$SYSROOT" $CC -o foo.o -c foo.c

이 방법은 간단하긴 하지만, 유연성이 부족합니다. 이 방법에서는 C++ STL (STLport, libc++ 또는 GNU libstdc++)을 사용할 수 없습니다. 예외나 RTTI가 지원되지도 않습니다.

Clang을 위해서는, 다음 두 가지 추가 단계를 수행할 필요가 있습니다.

    1. 표 2에 나타낸 것처럼, 대상 아키텍처에 알맞은 -target을 추가합니다.
    2. 표 2. 아키텍처와 -target에 대응하는 값

      아키텍처
      armeabi-target armv5te-none-linux-androideabi
      armeabi-v7a-target armv7-none-linux-androideabi
      arm64-v8a-target aarch64-none-linux-android
      x86-target i686-none-linux-android
      x86_64-target x86_64-none-linux-android
      mips-target mipsel-none-linux-android
    3. 다음 예시에서처럼, -gcc-toolchain 옵션을 추가하여 어셈블러 및 링커 지원을 추가합니다.
    4. -gcc-toolchain $NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64
    최종적으로, Clang을 사용하여 컴파일하기 위한 명령은 다음과 같은 형태입니다.
    export CC="$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/ \ linux-x86/bin/arm-linux-androideabi-gcc-4.8 --sysroot=$SYSROOT" -target \ armv7-none-linux-androideabi \ -gcc-toolchain $NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64" $CC -o foo.o -c foo.c

고급 방법

NDK는 명령줄에서 사용자 지정 툴체인 설치를 수행할 수 있는 make-standalone-toolchain.sh 셸 스크립트를 제공합니다. 이 접근 방식에서는 간단한 방법에 설명되어 있는 절차보다 더 큰 유연성을 발휘할 수 있습니다.

이 스크립트는 $NDK/build/tools/ 디렉터리에 있으며, 여기서 $NDK는 NDK의 설치 루트 디렉터리입니다. 이 스크립트의 사용 예는 아래에 나와 있습니다.

$NDK/build/tools/make-standalone-toolchain.sh \ --arch=arm --platform=android-21 --install-dir=/tmp/my-android-toolchain

이 명령어를 실행하면 /tmp/my-android-toolchain/이라는 이름의 디렉터리가 생성되고, 이 디렉터리에는 android-21/arch-arm sysroot의 복사본과 32비트 ARM 아키텍처용 툴체인 바이너리의 복사본이 포함됩니다.

툴체인 바이너리는 호스트별 경로에 종속되지 않거나 이러한 경로를 포함하지 않습니다. 즉, 어떤 위치에든 이러한 바이너리를 설치하거나, 필요하다면 이동할 수도 있습니다.

기본적으로, 빌드 시스템은 32비트 ARM 기반 GCC 4.8 툴체인을 사용합니다. 하지만 --arch=<toolchain>을 옵션으로 지정하여 다른 값을 지정할 수 있습니다. 표 3은 다른 툴체인에 사용할 값을 나타냅니다.

표 3. --arch를 사용하는 툴체인과 해당 값

툴체인
mips64 컴파일러--arch=mips64
mips GCC 4.8 컴파일러--arch=mips
x86 GCC 4.8 컴파일러--arch=x86
x86_64 GCC 4.8 컴파일러--arch=x86_64
mips GCC 4.8 컴파일러--arch=mips

또는 --toolchain=<toolchain> 옵션을 사용할 수 있습니다. 표 4는 <toolchain>에 대해 지정할 수 있는 값을 나타낸 것입니다.

표 4. --toolchain을 사용하는 툴체인과 해당 값

툴체인
arm
  • --toolchain=arm-linux-androideabi-4.8
  • --toolchain=arm-linux-androideabi-4.9
  • --toolchain=arm-linux-android-clang3.5
  • --toolchain=arm-linux-android-clang3.6
  • x86
  • --toolchain=x86-linux-android-4.8
  • --toolchain=x86-linux-android-4.9
  • --toolchain=x86-linux-android-clang3.5
  • --toolchain=x86-linux-android-clang3.6
  • mips
  • --toolchain=mips-linux-android-4.8
  • --toolchain=mips-linux-android-4.9
  • --toolchain=mips-linux-android-clang3.5
  • --toolchain=mips-linux-android-clang3.6
  • arm64
  • --toolchain=aarch64-linux-android-4.9
  • --toolchain=aarch64-linux-android-clang3.5
  • --toolchain=aarch64-linux-android-clang3.6
  • x86_64
  • --toolchain=x86_64-linux-android-4.9
  • --toolchain=x86_64-linux-android-clang3.5
  • --toolchain=x86_64-linux-android-clang3.6
  • mips64
  • --toolchain=mips64el-linux-android-4.9
  • --toolchain=mips64el-linux-android-clang3.5
  • --toolchain=mips64el-linux-android-clang3.6
  • 참고: 표 4는 완전한 목록이 아닙니다. 다른 조합도 유효할 수 있지만 확인된 것은 아닙니다.

    다음 두 가지 방법 중 하나를 사용해 Clang/LLVM 3.6을 복사할 수도 있습니다. --toolchain 옵션이 다음 예시와 같은 형태가 되도록 -clang3.6을 --toolchain 옵션에 추가할 수 있습니다.

    --toolchain=arm-linux-androideabi-clang3.6

    명령줄에서 -llvm-version=3.6을 별개의 옵션으로 추가할 수도 있습니다.

    참고: 특정 버전을 지정하는 대신에 <version>을 사용할 수도 있는데, 그러면 기본값이 사용 가능한 Clang의 가장 높은 버전이 됩니다.

    기본적으로, 빌드 시스템은 32비트 호스트 툴체인에 맞게 빌드합니다. 64비트 호스트 툴체인을 대신 지정할 수 있습니다. 표 5는 다른 플랫폼에 대해 -system과 함께 사용할 값을 나타낸 것입니다.

    표 5. -system을 사용하는 호스트 툴체인과 해당 값

    호스트 툴체인
    64비트 Linux-system=linux-x86_64
    64비트 MacOSX-system=darwin-x86_64
    64비트 Windows-system=windows-x86_64
    64비트 또는 32비트 명령 호스트 툴체인의 지정에 대한 자세한 내용은 64비트 및 32비트 툴체인을 참조하세요.

    --stl=stlport를 지정하여 기본 libgnustl 대신 libstlport를 복사할 수 있습니다. 위와 같이 지정하고 공유 라이브러리에 링크하려면 -lstlport_shared를 명시적으로 사용해야 합니다. 이 요구사항은 GNU libstdc++에 대해 -lgnustl_shared를 사용해야 하는 것과 유사합니다.

    마찬가지로, --stl=libc++를 지정하여 LLVM libc++ 헤더와 라이브러리를 복사할 수 있습니다. 공유 라이브러리에 링크하려면 -lc++_shared를 명시적으로 사용해야 합니다.

    다음 예시에서처럼, 직접 이렇게 설정할 수 있습니다.

    export PATH=/tmp/my-android-toolchain/bin:$PATH export CC=arm-linux-androideabi-gcc # or export CC=clang export CXX=arm-linux-androideabi-g++ # or export CXX=clang++

    -install-dir 옵션을 생략하면 make-standalone-toolchain.sh 셸 스크립트가 tmp/ndk/<toolchain-name>.tar.bz2에서 tarball을 생성합니다. 이 tarball은 바이너리의 아카이브뿐 아니라, 재배포도 쉽게 수행할 수 있게 해줍니다.

    이 독립 실행형 툴체인은 유효한 예외 및 RTTI 지원과 함께 C++ STL 라이브러리의 작업 복사본을 포함한다는 점에서 추가적인 이점도 제공합니다.

    추가적인 옵션과 세부정보를 보려면 --help를 사용하세요.

    Clang을 사용한 작업

    --llvm-version=<version> 옵션을 사용하여 독립 실행형 설치에서 Clang 바이너리를 설치할 수 있습니다. <version>은 3.5나 3.6과 같은 LLVM/Clang 버전 번호입니다. 예:

    build/tools/make-standalone-toolchain.sh \ --install-dir=/tmp/mydir \ --toolchain=arm-linux-androideabi-4.8 \ --llvm-version=3.6

    참고로, Clang 바이너리는 동일한 어셈블러, 링커, 헤더, 라이브러리 및 C++ STL 구현에 의존하기 때문에 GCC 바이너리와 함께 복사됩니다.

    이 작업을 수행하면 <install-dir>/bin/@ 아래에 clang 및 clang++로 명명된 두 스크립트도 설치됩니다. 이러한 스크립트는 기본 대상 아키텍처 플래그가 있는 실제 clang 바이너리를 호출합니다. 다시 말해, 위 두 스크립트는 어떤 수정도 하지 않고 작동해야 하고, 이러한 스크립트를 가리키도록 CC 및 CXX 환경 변수를 설정하는 것만으로도 자신의 빌드에서 두 스크립트를 사용할 수 있어야 합니다.

    Clang 호출

    llvm-version=3.6으로 빌드된 ARM 독립 실행형 설치 시, Unix 시스템에서의 Clang 호출은 단 한 줄이면 됩니다. 예를 들면 다음과 같습니다.

    `dirname $0`/clang36 -target armv5te-none-linux-androideabi "$@"

    clang++는 같은 방식으로 clang++31을 호출합니다.

    ARM이 있는 Clang 대상

    ARM용으로 빌드할 때, Clang은 -march=armv7-a 및/또는 -mthumb 옵션의 존재 여부에 따라 대상을 변경합니다.

    표 5. 지정 가능한 -march 값과 결과 대상

    모두
    -march 값결과 대상
    -march=armv7-aarmv7-none-linux-androideabi
    -mthumbthumb-none-linux-androideabi
    -march=armv7-a와 -mthumbthumbv7-none-linux-androideabi

    원한다면 자신의 -target으로 재정의할 수도 있습니다.

    독립 실행형 패키지에서는 Clang이 미리 정의된 상대 위치에서 as와 ld를 찾기 때문에, -gcc-toolchain 옵션이 불필요합니다.

    clang과 clang++는 메이크파일에서 별도의 구성이나 변경 없이 gcc와 g++를 쉽게 대체할 수 있어야 합니다. 이에 의문이 생길 때는 다음 옵션을 추가하여 올바로 작동 중인지 확인하면 됩니다.

    • -v: 컴파일러 드라이버 문제와 관련된 명령어 덤프
    • -###: 암시적으로 미리 정의된 옵션을 포함한 명령줄 옵션 덤프
    • -x c < /dev/null -dM -E: 미리 정의된 전처리기 정의 덤프
    • -save-temps*.i 또는 *.ii 전처리된 파일 비교

    Clang에 대한 자세한 내용은 http://clang.llvm.org/를 참조하고, 특히 GCC 호환성 섹션을 잘 살펴보세요.

    ABI 호환성

    ARM 툴체인이 생성하는 기계어 코드는 기본적으로 공식 Android armeabi ABI와 호환 가능해야 합니다.

    -mthumb 컴파일러 플래그를 사용하여 16비트 Thumb-1 명령의 생성을 강제 적용하는 것이 좋습니다(기본값은 32비트 ARM 명령).

    armeabi-v7a ABI를 대상으로 하려면 다음 플래그를 설정해야 합니다.

    CFLAGS= -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16

    첫 번째 플래그는 Thumb-2 명령을 활성화합니다. 두 번째 플래그는 하드웨어 FPU 명령을 활성화하는 한편, 시스템이 ABI 호환성에 중요한 코어 레지스터에서 부동 소수점 매개변수를 전달하는지 확인합니다.

    참고: r9b 이전의 NDK 버전에서는 이러한 플래그를 따로 사용하지 마세요. 이러한 플래그를 전부 설정하거나 전혀 설정하지 않아야 합니다. 그렇지 않으면, 예측할 수 없는 동작이 발생하고 다운될 수 있습니다.

    NEON 명령을 사용하려면 다음과 같이 -mfpu 컴파일러 플래그를 변경해야 합니다.

    CFLAGS= -march=armv7-a -mfloat-abi=softfp -mfpu=neon

    이 설정은 ARM 사양에 따라 VFPv3-D32의 사용을 강제 적용합니다.

    또한, 링커에 다음 두 개의 플래그를 제공해야 합니다.

    LDFLAGS= -march=armv7-a -Wl,--fix-cortex-a8

    첫 번째 플래그는 링커에게 armv7-a에 맞춤화된 libgcc.alibgcov.acrt*.o를 선택하도록 지시합니다. 몇몇 Cortex-A8 구현에서는 CPU 버그에 대한 해결 방법으로 두 번째 플래그가 필요합니다.

    NDK 버전 r9b 이후로는 double 값이나 부동 소수점 값을 취하거나 반환하는 모든 Android 네이티브 API가 ARM에 대한 attribute((pcs("aapcs")))를 가집니다. 이에 따라 -mhard-float(-mfloat-abi=hard를 의미함)에서 사용자 코드를 컴파일하고 softfp ABI를 준수하는 Android 네이티브 API와 계속 링크할 수 있습니다. 이에 대한 자세한 내용은$NDK/tests/device/hard-float/jni/Android.mk의 설명을 참조하세요.

    x86에서 NEON 내장 함수를 사용하려는 경우, 빌드 시스템은 표준 ARM NEON 내장 함수 헤더와 같은 arm_neon.h라는 이름을 가진 특별한 C/C++ 언어 헤더를 사용하여 NEON 내장 함수를 네이티브 x86 SSE 내장 함수로 변환할 수 있습니다.

    기본적으로, x86 ABI는 SSSE3까지 SIMD를 지원하고 헤더가 NEON 함수의 93%(2,009개 중 1,869개)까지 커버합니다.

    MIPS ABI를 대상으로 할 때 특정 컴파일러 플래그를 사용할 필요가 없습니다.

    ABI 지원에 대해 자세히 알아보려면 x86 지원을 참조하세요.

    경고 및 제한 사항

    Windows 지원

    Windows 바이너리는 Cygwin에 종속되지 않습니다. 이처럼 종속성이 없으므로 Windows 바이너리의 속도가 더 빨라집니다. 하지만C:/foo/bar와는 반대로, cygdrive/c/foo/bar와 같은 Cygwin 경로 사양을 알 수 없다는 단점이 있습니다.

    NDK 빌드 시스템은 Cygwin에서 컴파일러로 전달되는 모든 경로가 자동으로 변환되도록 하고 다른 복잡한 문제들도 관리합니다. 사용자 지정 빌드 시스템이 있는 경우에는 이러한 복잡한 문제를 스스로 해결해야 할 수도 있습니다.

    Cygwin/MSys 지원과 관련된 정보를 보려면 android-ndk 포럼을 방문하세요.

    wchar_t 지원

    사실, Android 플랫폼은 Android 2.3(API 레벨 9)까지는 wchar_t를 지원하지 않았습니다. 이 사실로 인해 다음과 같은 여러 가지 문제가 생깁니다.

    • Android 2.3 이상의 플랫폼을 대상으로 하는 경우, wchar_t의 크기는 4바이트이고 대부분의 wide-char 함수는 C 라이브러리에서 사용할 수 있습니다(멀티바이트 인코딩/디코딩 함수와 wsprintf/wsscanf는 제외).
    • 더 낮은 API 레벨을 대상으로 하는 경우 wchar_t의 크기는 1바이트이고 와이드 문자 함수는 어떤 것도 작동하지 않습니다.

    wchar_t 유형에 대한 모든 종속성을 제거하고 더 나은 표현으로 바꾸는 것이 좋습니다. Android에서 제공되는 지원은 기존 코드의 마이그레이션에 도움이 되도록 하는 목적만 있을 뿐입니다.

    예외, RTTI, STL

    툴체인 바이너리는 기본적으로 C++ 예외와 RTTI를 지원합니다. 소스를 빌드할 때 C++ 예외와 RTTI를 비활성화하려면(예를 들어, 더욱 가벼운 기계어 코드를 생성하기 위해) -fno-exceptions와 -fno-rtti를 사용하세요.

    GNU libstdc++와 함께 이러한 기능을 사용하려면 libsupc++로 명시적으로 링크해야 합니다. 그러려면 바이너리 링크 시 -lsupc++를 사용하세요. 예:

    arm-linux-androideabi-g++ .... -lsupc++

    STLport 또는 libc++ 라이브러리를 사용할 때는 이렇게 할 필요가 없습니다.

    C++ STL 지원

    독립 실행형 툴체인은 C++ 표준 템플릿 라이브러리 구현의 복사본을 포함합니다. 이 구현은 앞서 설명한 --stl=<name> 옵션에 대해 지정한 내용에 따라 GNU libstdc++, STLport 또는 libc++를 위한 것입니다. 이러한 STL 구현을 사용하려면 프로젝트를 올바른 라이브러리와 링크해야 합니다.

    • -lstdc++를 사용하여 구현의 정적 라이브러리 버전에 링크합니다. 그렇게 하면 필요한 C++ STL 코드가 전부 최종 바이너리에 포함됩니다. 이 방법은 단일 공유 라이브러리나 실행 파일만 생성하려는 경우에 이상적입니다.

      다음은 저희가 권장하는 방법입니다.

    • 또는 -lgnustl_shared를 사용하여 GNU libstdc++의 공유 라이브러리 버전에 링크합니다. 이 옵션을 사용하는 경우 코드의 올바른 로드를 위해 기기에 libgnustl_shared.so를 복사해야 하기도 합니다. 표 6은 각 툴체인 유형마다 이 파일의 위치를 보여줍니다.
    • 참고: GNU libstdc++는 GPLv3 라이선스에 따라 사용권이 부여되며, 링크 예외가 있습니다. 관련 요구사항을 준수할 수 없는 경우에는 프로젝트에 공유 라이브러리를 재배포할 수 없습니다.

    • -lstlport_shared를 사용하여 STLport의 공유 라이브러리 버전에 링크합니다. 이때 코드의 올바른 로드를 위해 기기에 libstlport_shared.so를 복사하는 것도 잊지 마세요. 표 6은 각 툴체인마다 이 파일의 위치를 보여줍니다.
    • 표 6. 지정 가능한 -march 값과 결과 대상

      툴체인위치
      arm$TOOLCHAIN/arm-linux-androideabi/lib/
      arm64$TOOLCHAIN/aarch64-linux-android/lib/
      x86$TOOLCHAIN/i686-linux-android/lib/
      x86_64$TOOLCHAIN/x86_64-linux-android/lib/
      mips$TOOLCHAIN/mipsel-linux-android/lib/
      mips64$TOOLCHAIN/mips64el-linux-android/lib/

      참고: 프로젝트에 여러 개의 공유 라이브러리나 실행 파일이 있는 경우 공유 라이브러리 STL 구현에 링크해야 합니다. 그렇지 않으면, 빌드 시스템이 전역적으로 고유하게 정의하지 않아 예측할 수 없는 런타임 동작을 일으킬 수 있습니다. 이 동작에는 다운과 예외를 올바로 포착하지 못하는 것이 포함될 수 있습니다.

      라이브러리의 공유 버전을 단순히 libstdc++.so로 부르지 않는 이유는 이 이름이 런타임에 시스템 고유의 최소 C++ 런타임과 충돌을 일으키기 때문입니다. 이러한 이유로, 빌드 시스템은 GNU ELF 라이브러리에 대해 새 이름을 강제 적용합니다. 정적 라이브러리에는 이 문제가 없습니다.


    반응형
    Comments