Android ABI

다양한 Android 기기는 각기 다른 CPU를 사용하므로 서로 다른 명령 집합을 지원합니다. CPU와 명령 세트의 각 조합에는 고유한 ABI(Application Binary Interface)가 있습니다. ABI에는 다음 정보가 포함되어 있습니다.

  • 사용할 수 있는 CPU 명령 집합(및 확장)
  • 런타임 시 메모리 저장 및 로드의 엔디언 Android는 항상 리틀 엔디언입니다.
  • 정렬 제약 조건을 비롯하여 애플리케이션과 시스템 간에 데이터를 전달하기 위한 규칙 및 시스템이 함수를 호출할 때 스택을 사용하고 등록하는 방법
  • 실행 가능한 바이너리의 형식(예: 프로그램 및 공유 라이브러리) 및 이러한 바이너리에서 지원하는 콘텐츠의 유형 Android는 항상 ELF를 사용합니다. 자세한 내용은 ELF 시스템 V Application Binary Interface를 참조하세요.
  • C++ 이름이 맹글링되는 방식. 자세한 내용은 일반/Itanium C++ ABI를 참조하세요.

이 페이지에는 NDK에서 지원하는 ABI 및 각 ABI의 작동 방식에 관한 정보가 나와 있습니다.

ABI는 플랫폼에서 지원하는 네이티브 API를 참조할 수도 있습니다. 32비트 시스템에 영향을 주는 이러한 종류의 ABI 문제 목록은 32비트 ABI 버그를 참조하세요.

지원되는 ABI

표 1. ABI 및 지원되는 명령 집합

ABI 지원되는 명령 세트 Notes
armeabi-v7a
  • armeabi
  • Thumb-2
  • VFPv3-D16
  • ARMv5/v6 기기와 호환되지 않음
    arm64-v8a
  • AArch64
  • Armv8.0만 해당됨
    x86
  • x86(IA-32)
  • MMX
  • SSE/2/3
  • SSSE3
  • MOVBE 또는 SSE4를 지원하지 않음
    x86_64
  • x86-64
  • MMX
  • SSE/2/3
  • SSSE3
  • SSE4.1, 4.2
  • POPCNT
  • x86-64-v1만 해당됨

    참고: 예전에는 NDK에서 ARMv5(armeabi), 32비트 및 64비트 MIPS를 지원했지만 NDK r17에서는 이러한 ABI 지원이 삭제되었습니다.

    armeabi-v7a

    이 ABI는 32비트 ARM CPU용입니다. 여기에는 Thumb-2 및 Neon(VFP) 하드웨어 부동 소수점 명령, 특히 16개의 전용 64비트 부동 소수점 레지스터가 있는 VFPv3-D16이 포함되어 있습니다.

    Android 전용이 아닌 ABI 요소에 관한 자세한 내용은 ARM 아키텍처용 ABI (Application Binary Interface)를 참고하세요.

    CMake를 구성할 때 ndk-build 또는 ANDROID_ARM_MODEAndroid.mkLOCAL_ARM_MODE를 사용하지 않는 한 NDK의 빌드 시스템은 기본적으로 Thumb-2 코드를 생성합니다.

    고급 SIMD(Neon) 및 VFPv3-D32를 비롯한 다른 확장은 선택사항입니다. 자세한 내용은 NEON 지원을 참고하세요.

    이 ABI는 -mfloat-abi=softfp를 사용하여 컴파일러가 함수 호출 시 정수 레지스터의 모든 float 값과 정수 레지스터 쌍의 모든 double 값을 전달해야 한다는 규칙을 적용합니다. 이는 호출 규칙에만 영향을 미칩니다. 컴파일러는 여전히 하드웨어 부동 소수점 명령을 사용합니다.

    이 ABI는 64비트 long double(double과 동일한 IEEE binary64)을 사용합니다.

    arm64-v8a

    이 ABI는 64비트 ARM CPU용입니다.

    Android 전용이 아닌 ABI 요소에 관한 자세한 내용은 ARM의 아키텍처 알아보기를 참고하세요. 또한 ARM에서는 64비트 Android 개발 시 필요한 몇 가지 포팅 관련 조언도 제공합니다.

    C 및 C++ 코드에서 Neon 내장 함수를 사용하여 고급 SIMD 확장을 활용할 수 있습니다. Armv8-A용 Neon 프로그래머 가이드에서는 일반적으로 Neon 내장 기능 및 Neon 프로그래밍에 관한 자세한 정보를 제공합니다.

    Android에서는 플랫폼별 x18 레지스터가 ShadowCallStack용으로 예약되어 있으므로 코드를 사용하여 수정하면 안 됩니다. 최신 버전의 Clang은 기본적으로 Android에서 -ffixed-x18 옵션을 사용하므로 직접 작성한 어셈블러(또는 매우 오래된 컴파일러)가 있지 않는 한 이 문제를 걱정할 필요가 없습니다.

    이 ABI는 128비트 long double(IEEE binary128)을 사용합니다.

    x86

    이 ABI는 일반적으로 'x86', 'i386' 또는 'IA-32'로 알려진 명령 집합을 지원하는 CPU용입니다.

    Android의 ABI는 기본 명령 집합과 MMX, SSE, SSE2, SSE3, SSSE3 확장을 포함합니다.

    ABI는 MOVBE, SSE4의 변이와 같은 다른 선택적 IA-32 명령 집합 확장은 포함하지 않습니다. 이러한 확장을 활성화하기 위한 런타임 기능 검색을 사용하는 한 확장을 계속 사용할 수 있으며, 이러한 확장을 지원하지 않는 기기에는 대체 기능을 제공할 수 있습니다.

    NDK 도구 모음은 16바이트 스택 정렬 후에 함수를 호출하는 것으로 가정합니다. 기본 도구와 옵션은 이 규칙을 적용합니다. 어셈블리 코드를 작성할 때 스택 정렬 상태를 유지하고 다른 컴파일러도 이 규칙을 준수하도록 해야 합니다.

    자세한 내용은 다음 문서를 참조하세요.

    이 ABI는 64비트 long double(double과 동일한 IEEE binary64이며 더 일반적인 80비트 Intel 전용 long double이 아님)을 사용합니다.

    x86_64

    이 ABI는 흔히 'x86-64'라고 하는 명령 집합을 지원하는 CPU용입니다.

    Android의 ABI는 기본 명령 집합과 MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT 명령을 포함합니다.

    ABI는 MOVBE, SHA, SSE4의 변이와 같은 다른 선택적 x86-64 명령 집합 확장은 포함하지 않습니다. 이러한 확장을 활성화하기 위한 런타임 기능 검색을 사용하는 한 확장을 계속 사용할 수 있으며, 이러한 확장을 지원하지 않는 기기에는 대체 기능을 제공할 수 있습니다.

    자세한 내용은 다음 문서를 참조하세요.

    이 ABI는 128비트 long double(IEEE binary128)을 사용합니다.

    특정 ABI를 위한 코드 생성

    Gradle

    Gradle(Android 스튜디오를 통해 사용하든 명령줄에서 사용하든 상관없이)은 기본적으로 지원이 중단되지 않은 모든 ABI를 빌드합니다. 애플리케이션에서 지원하는 ABI 세트를 제한하려면 abiFilters를 사용합니다. 예를 들어 64비트 ABI만 빌드하려면 build.gradle에서 다음 구성을 설정합니다.

    android {
        defaultConfig {
            ndk {
                abiFilters 'arm64-v8a', 'x86_64'
            }
        }
    }
    

    ndk-build

    ndk-build는 기본적으로 지원이 중단되지 않은 모든 ABI를 빌드합니다. Application.mk 파일에서 APP_ABI를 설정하여 특정 ABI를 대상으로 지정할 수 있습니다. 다음 스니펫은 APP_ABI를 사용하는 몇 가지 예를 보여줍니다.

    APP_ABI := arm64-v8a  # Target only arm64-v8a
    APP_ABI := all  # Target all ABIs, including those that are deprecated.
    APP_ABI := armeabi-v7a x86_64  # Target only armeabi-v7a and x86_64.
    

    APP_ABI에 지정할 수 있는 값에 관한 자세한 내용은 Application.mk를 참조하세요.

    CMake

    CMake를 사용하면 한 번에 하나의 ABI를 빌드하고 명시적으로 ABI를 지정해야 합니다. ANDROID_ABI 변수를 사용하여 지정할 수 있는데 명령줄에서 지정해야 합니다(CMakeLists.txt에서 설정할 수 없음). 예:

    $ cmake -DANDROID_ABI=arm64-v8a ...
    $ cmake -DANDROID_ABI=armeabi-v7a ...
    $ cmake -DANDROID_ABI=x86 ...
    $ cmake -DANDROID_ABI=x86_64 ...
    

    NDK로 빌드하기 위해 CMake에 전달해야 하는 다른 플래그는 CMake 가이드를 참조하세요.

    빌드 시스템의 기본 동작은 fat APK로도 알려진 단일 APK에 각 ABI용 바이너리를 포함하는 것입니다. fat APK는 단일 ABI용 바이너리만 포함한 APK보다 훨씬 큽니다. 따라서 호환성의 폭은 더 넓어지지만 APK가 커진다는 단점이 있습니다. 기기 호환성을 최대로 유지하면서 APK 크기를 줄이려면 App Bundle 또는 APK 분할을 활용하는 것이 좋습니다.

    설치 시 패키지 관리자는 대상 기기에 가장 알맞은 기계어 코드만 압축을 풉니다. 자세한 내용은 설치 시 네이티브 코드의 자동 압축 풀기를 참조하세요.

    Android 플랫폼에서의 ABI 관리

    이 섹션에서는 Android 플랫폼이 APK에서 네이티브 코드를 관리하는 방법을 자세히 설명합니다.

    앱 패키지의 네이티브 코드

    Play 스토어와 패키지 관리자는 모두 다음 패턴과 일치하는 APK 내부의 파일 경로에서 NDK가 생성한 라이브러리를 찾을 수 있을 것으로 예상합니다.

    /lib/<abi>/lib<name>.so
    

    여기에서 <abi>지원되는 ABI에 나열되어 있는 ABI 이름 중 하나이며 <name>Android.mk 파일에서 LOCAL_MODULE 변수를 대상으로 정의한 라이브러리의 이름입니다. APK 파일은 zip 파일에 불과하므로 파일을 열어서 공유된 네이티브 라이브러리가 속한 위치를 간단히 확인할 수 있습니다.

    시스템이 예상한 위치에서 네이티브 공유 라이브러리를 찾지 못하면 이러한 라이브러리를 사용할 수 없습니다. 그럴 때는 앱 자체에서 라이브러리를 복사한 후 dlopen()을 실행해야 합니다.

    fat APK에서 각 라이브러리는 상응하는 ABI와 이름이 일치하는 디렉터리 아래에 위치합니다. 예를 들어 fat APK에는 다음이 포함될 수 있습니다.

    /lib/armeabi/libfoo.so
    /lib/armeabi-v7a/libfoo.so
    /lib/arm64-v8a/libfoo.so
    /lib/x86/libfoo.so
    /lib/x86_64/libfoo.so
    

    참고: 4.0.3 이하를 실행하는 ARMv7 기반 Android 기기는 두 개의 디렉터리가 모두 있는 경우 armeabi-v7a 디렉터리 대신 armeabi 디렉터리에서 네이티브 라이브러리를 설치합니다. APK에서 /lib/armeabi//lib/armeabi-v7a/ 다음에 오기 때문입니다. 이 문제는 4.0.4부터 수정되었습니다.

    Android 플랫폼 ABI 지원

    빌드별 시스템 속성이 다음과 같이 표시하기 때문에 Android 시스템은 런타임 시 어떤 ABI를 지원하는지 알게 됩니다.

    • 시스템 이미지 자체에 사용되는 기계어 코드에 해당하는, 기기의 기본 ABI
    • 필요한 경우 시스템 이미지에서도 지원하는 다른 ABI에 해당하는 보조 ABI

    이 메커니즘은 시스템이 설치 시 패키지로부터 최상의 기계어 코드를 추출하도록 합니다.

    최상의 성능을 위해서는 직접 기본 ABI용으로 컴파일해야 합니다. 예를 들어 전형적인 ARMv5TE 기반 기기는 기본 ABI인 armeabi만 정의합니다. 반면 전형적인 ARMv7 기반 기기는 기본 ABI를 armeabi-v7a로, 보조 ABI를 armeabi로 정의합니다. 각각에서 생성되는 애플리케이션 네이티브 바이너리를 실행할 수 있기 때문입니다.

    64비트 기기는 32비트 변형도 지원합니다. arm64-v8a 기기를 예로 들면 이 기기는 armeabi 및 armeabi-v7a 코드도 실행할 수 있습니다. 다만 애플리케이션이 arm64-v8a를 대상으로 한다면 애플리케이션의 armeabi-v7a 버전을 실행하는 기기를 사용하는 것보다 64비트 기기에서 더욱 잘 실행된다는 점에 유의해야 합니다.

    또한 여러 x86 기반 기기가 armeabi-v7aarmeabi NDK 바이너리를 실행할 수 있습니다. 이런 기기에서 기본 ABI는 x86, 보조 ABI는 armeabi-v7a가 됩니다.

    특정 ABI용 APK를 강제 설치할 수 있습니다. 이는 테스트에 유용합니다. 다음 명령어를 사용합니다.

    adb install --abi abi-identifier path_to_apk
    

    설치 시 네이티브 코드의 자동 압축 풀기

    애플리케이션을 설치할 때 패키지 관리자 서비스는 APK를 검사하여 다음 양식의 공유 라이브러리가 있는지 찾아봅니다.

    lib/<primary-abi>/lib<name>.so
    

    공유 라이브러리를 찾을 수 없는데 보조 ABI를 정의했다면 서비스는 다음 양식의 공유 라이브러리가 있는지 검사합니다.

    lib/<secondary-abi>/lib<name>.so
    

    찾으려는 라이브러리를 찾으면 패키지 관리자는 이 라이브러리를 애플리케이션의 네이티브 라이브러리 디렉터리(<nativeLibraryDir>/) 아래 /lib/lib<name>.so에 복사합니다. 다음 스니펫은 nativeLibraryDir을 검색합니다.

    Kotlin

    import android.content.pm.PackageInfo
    import android.content.pm.ApplicationInfo
    import android.content.pm.PackageManager
    ...
    val ainfo = this.applicationContext.packageManager.getApplicationInfo(
            "com.domain.app",
            PackageManager.GET_SHARED_LIBRARY_FILES
    )
    Log.v(TAG, "native library dir ${ainfo.nativeLibraryDir}")
    

    Java

    import android.content.pm.PackageInfo;
    import android.content.pm.ApplicationInfo;
    import android.content.pm.PackageManager;
    ...
    ApplicationInfo ainfo = this.getApplicationContext().getPackageManager().getApplicationInfo
    (
        "com.domain.app",
        PackageManager.GET_SHARED_LIBRARY_FILES
    );
    Log.v( TAG, "native library dir " + ainfo.nativeLibraryDir );
    

    공유 객체 파일이 하나도 없으면 애플리케이션이 빌드되고 설치되지만 런타임 시 다운됩니다.

    ARMv9: C/C++용 PAC 및 BTI 사용 설정

    PAC/BTI를 사용 설정하면 일부 공격 벡터로부터 보호할 수 있습니다. PAC는 함수의 프롤로그에 암호화 방식으로 서명하고 반환 주소가 여전히 에필로그에 올바르게 서명되었는지 확인하여 반환 주소를 보호합니다. BTI는 각 브랜치 대상이 프로세서에 착륙해도 괜찮음을 프로세서에 알리기만 하는 특수 명령어여야 한다고 요구하므로, 코드의 임의 위치로 이동하는 것을 방지합니다.

    Android는 새 명령을 지원하지 않는 이전 프로세서에서는 아무 작업도 하지 않는 PAC/BTI 명령을 사용합니다. ARMv9 기기에만 PAC/BTI 보호가 포함되지만 ARMv8 기기에서도 동일한 코드를 실행할 수 있습니다. 라이브러리의 여러 변형이 필요하지 않습니다. ARMv9 기기에서도 PAC/BTI는 64비트 코드에만 적용됩니다.

    PAC/BTI를 사용 설정하면 코드 크기가 약간, 일반적으로 1%가 증가합니다.

    공격 벡터 PAC/BTI 대상에 대한 자세한 설명과 보호 작동 방식에 관해 알아보려면 Arm의 아키텍처 알아보기 - 복잡한 소프트웨어 보호 지원(PDF)을 참조하세요.

    빌드 변경사항

    ndk-build

    Android.mk의 각 모듈에서 LOCAL_BRANCH_PROTECTION := standard를 설정합니다.

    CMake

    CMakeLists.txt의 각 대상에 target_compile_options($TARGET PRIVATE -mbranch-protection=standard)를 사용합니다.

    기타 빌드 시스템

    -mbranch-protection=standard를 사용하여 코드를 컴파일합니다. 이 플래그는 arm64-v8a ABI용으로 컴파일할 때만 작동합니다. 연결할 때 이 플래그를 사용하지 않아도 됩니다.

    문제 해결

    Google은 PAC/BTI용 컴파일러 지원과 관련된 문제는 인식하지 못하지만 다음과 같은 이점이 있습니다.

    • 연결할 때 BTI 코드와 BTI 이외 코드를 섞지 마세요. 라이브러리에서 BTI 보호가 사용 설정되지 않게 됩니다. llvm-readelf를 사용하여 결과 라이브러리에 BTI 메모가 있는지 확인할 수 있습니다.
    $ llvm-readelf --notes LIBRARY.so
    [...]
    Displaying notes found in: .note.gnu.property
      Owner                Data size    Description
      GNU                  0x00000010   NT_GNU_PROPERTY_TYPE_0 (property note)
        Properties:    aarch64 feature: BTI, PAC
    [...]
    $
    
    • 이전 버전의 OpenSSL(1.1.1i 이전)에는 손으로 작성한 어셈블러에 PAC 오류를 일으키는 버그가 있습니다. 현재 OpenSSL로 업그레이드

    • 이전 버전의 일부 앱 DRM 시스템에서는 PAC/BTI 요구사항을 위반하는 코드가 생성됩니다. 앱 DRM을 사용 중이며 PAC/BTI를 사용 설정할 때 문제가 발생하면 DRM 공급업체에 문의하여 수정된 버전을 확인하세요.