رابط‌های دودویی برنامه Android

دستگاه‌های Android مختلف از CPUهای مختلفی استفاده می‌کنند که به‌نوبه خود از مجموعه دستورالعمل‌های مختلفی پشتیبانی می‌کنند. هر ترکیبی از CPU و مجموعه دستورالعمل، «میانای دودویی کاربردی» (ABI) خودش را دارد. یک ABI شامل اطلاعات زیر است:

  • مجموعه دستورالعمل‌های CPU (و افزونه‌ها) که می‌توانند استفاده شوند.
  • ترتیب بایت‌های ذخیره‌سازی و بارگیری حافظه در زمان اجرا. Android همیشه بایت‌کوچک است.
  • قراردادهای انتقال داده بین برنامه‌ها و سیستم، ازجمله محدودیت‌های هم‌ترازی، و نحوه استفاده سیستم از پشته و ثبت‌ها هنگام فراخوانی توابع.
  • قالب فایل‌های دودویی اجرایی، مانند برنامه‌ها و کتابخانه‌های مشترک، و انواع محتوایی که پشتیبانی می‌کنند. Android همیشه از ELF استفاده می‌کند. برای اطلاعات بیشتر، میانای دودویی کاربردی ELF System V را ببینید.
  • نحوه درهم‌ریختگی نام‌های C++. برای اطلاعات بیشتر، به Generic/Itanium C++ ABI مراجعه کنید.

این صفحه ABIهایی را که NDK پشتیبانی می‌کند برمی‌شمارد و اطلاعاتی درباره نحوه عملکرد هر ABI ارائه می‌دهد.

‫ABI همچنین می‌تواند به میانای برنامه‌سازی کاربردی بومی پشتیبانی‌شده توسط پلاتفرم اشاره داشته باشد. برای فهرست کردن انواع مشکلات ABI که بر سیستم‌های ۳۲ بیتی تأثیر می‌گذارند، به اشکالات ABI‏ ۳۲ بیتی مراجعه کنید.

رابط‌های دودویی نرم‌افزار (ABIs) پشتیبانی‌شده

جدول ۱. رابط‌های دودویی برنامه و مجموعه‌های دستورالعمل پشتیبانی‌شده.

رابط دودویی نرم‌افزار (ABI) مجموعه‌های دستورالعمل پشتیبانی‌شده یادداشت‌ها
armeabi-v7a
  • armeabi
  • شست-۲
  • نئون
  • با دستگاه‌های 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
  • CMPXCHG16B
  • LAHF-SAHF
  • ‫x86-64-v2 کامل.

    توجه: در گذشته NDK از ARMv5 (armeabi)، و MIPS‏ ۳۲ بیتی و ۶۴ بیتی پشتیبانی می‌کرد، اما پشتیبانی از این ABIها در NDK r17 برداشته شد.

    armeabi-v7a

    این ABI برای CPUهای ARM‏ ۳۲ بیتی است. این معماری شامل Thumb-2 و Neon است.

    برای کسب اطلاعات درباره بخش‌هایی از «میانای دودویی کاربردی» که مختص Android نیستند، به میانای دودویی کاربردی (ABI) برای معماری ARM مراجعه کنید

    سیستم‌های ساخت NDK به‌طور پیش‌فرض کد Thumb-2 تولید می‌کنند، مگراینکه از LOCAL_ARM_MODE در Android.mk برای ndk-build یا ANDROID_ARM_MODE هنگام پیکربندی CMake استفاده کنید.

    برای اطلاعات بیشتر درباره سابقه Neon، به پشتیبانی Neon مراجعه کنید.

    به دلایل تاریخی، این ABI از -mfloat-abi=softfp استفاده می‌کند که باعث می‌شود هنگام فراخوانی تابع، همه مقادیر float در ثبات‌های عدد صحیح و همه مقادیر double در جفت‌های ثبات عدد صحیح ارسال شوند. باوجود نام آن، این گزینه فقط بر قرارداد فراخوانی ممیز شناور تأثیر می‌گذارد: کامپایلر همچنان از دستورالعمل‌های ممیز شناور سخت‌افزار برای محاسبات استفاده خواهد کرد.

    این ABI از long double ۶۴ بیتی (IEEE binary64 مشابه double) استفاده می‌کند.

    arm64-v8a

    این ABI برای واحدهای پردازش مرکزی ARM‏ ۶۴ بیتی است.

    برای جزئیات کامل بخش‌های ABI که مختص Android نیستند، به Learn the Architecture در Arm مراجعه کنید. Arm همچنین توصیه‌هایی برای انتقال در توسعه Android‏ ۶۴ بیتی ارائه می‌دهد.

    می‌توانید از Neon intrinsics در کد C و C++‎ استفاده کنید تا از افزونه Advanced SIMD بهره ببرید. راهنمای برنامه‌نویس Neon برای Armv8-A اطلاعات بیشتری درباره Neon intrinsics و برنامه‌نویسی Neon به‌طور کلی ارائه می‌دهد.

    در Android، ثبات x18 مختص پلاتفرم برای ShadowCallStack رزرو شده است و نباید توسط کد شما لمس شود. نسخه‌های فعلی Clang به‌طور پیش‌فرض از گزینه -ffixed-x18 در Android استفاده می‌کنند، بنابراین، مگر اینکه اسمبلر دست‌نویس (یا کامپایلر بسیار قدیمی) داشته باشید، نباید نگران این موضوع باشید.

    این ABI از long double ۱۲۸ بیتی (IEEE binary128) استفاده می‌کند.

    x86

    این ABI برای CPUهایی است که از مجموعه دستورالعمل‌هایی که معمولاً با نام «x86»، «i386»، یا «IA-32» شناخته می‌شوند پشتیبانی می‌کنند.

    میانای دودویی برنامه Android شامل مجموعه دستورالعمل پایه به‌علاوه افزونه‌های MMX، SSE، SSE2، SSE3، و SSSE3 است.

    ABI شامل هیچ‌کدام از افزونه‌های اختیاری مجموعه دستورالعمل IA-32 مثل MOVBE یا هر نوع SSE4 نمی‌شود. همچنان می‌توانید از این افزونه‌ها استفاده کنید، به‌شرطی که از کاوش ویژگی زمان اجرا برای فعال کردن آن‌ها استفاده کنید و برای دستگاه‌هایی که از آن‌ها پشتیبانی نمی‌کنند، جایگزین ارائه دهید.

    زنجیره ابزار NDK تراز پشته ۱۶ بایتی را قبل‌از فراخوانی تابع فرض می‌کند. ابزارها و گزینه‌های پیش‌فرض این قانون را اجرا می‌کنند. اگر کد اسمبلی می‌نویسید، باید مطمئن شوید که تراز پشته را حفظ می‌کنید و مطمئن شوید که کامپایلرهای دیگر نیز از این قانون پیروی می‌کنند.

    برای جزئیات بیشتر به اسناد زیر مراجعه کنید:

    این ABI از long double ۶۴ بیتی (IEEE binary64 مشابه double، و نه long double ۸۰ بیتی رایج‌تر که فقط مختص Intel است) استفاده می‌کند.

    x86_64

    این رابط دودویی نرم‌افزار برای واحدهای پردازش مرکزی است که از مجموعه دستورالعمل‌هایی که معمولاً با عنوان «x86-64-v2» شناخته می‌شود پشتیبانی می‌کنند.

    رابط دودویی برنامه Android شامل مجموعه دستورالعمل پایه به‌علاوه MMX، SSE، SSE2، SSE3، SSSE3، SSE4.1، SSE4.2، و دستورالعمل POPCNT است.

    ABI شامل هیچ‌کدام از افزونه‌های مجموعه دستورالعمل اختیاری x86-64 دیگر مثل MOVBE،‏ SHA، یا هر نوع AVX نمی‌شود. همچنان می‌توانید از این افزونه‌ها استفاده کنید، به‌شرطی که از کاوش ویژگی زمان اجرا برای فعال کردن آن‌ها استفاده کنید و برای دستگاه‌هایی که از آن‌ها پشتیبانی نمی‌کنند، جایگزین ارائه دهید.

    برای جزئیات بیشتر به اسناد زیر مراجعه کنید:

    این ABI از long double ۱۲۸ بیتی (IEEE binary128) استفاده می‌کند.

    تولید کد برای ABI خاص

    Gradle

    Gradle (چه ازطریق «استودیو Android» استفاده شود یا از خط فرمان) به‌طور پیش‌فرض برای همه ABIهای منسوخ‌نشده ساخته می‌شود. برای محدود کردن مجموعه «رابط‌های دودویی کاربردی» که برنامه‌تان پشتیبانی می‌کند، از abiFilters استفاده کنید. برای مثال، برای ساختن فقط برای رابط‌های دودویی کاربردی ۶۴ بیتی، پیکربندی زیر را در build.gradle خود تنظیم کنید:

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

    ndk-build

    ndk-build به‌طور پیش‌فرض برای همه ABIهای منسوخ‌نشده ساخته می‌شود. می‌توانید با تنظیم APP_ABI در فایل Application.mk، رابط دودویی برنامه خاصی را هدف قرار دهید. تکه‌کد زیر چند نمونه از استفاده از 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 ...
    

    برای پرچم‌های دیگری که باید به CMake ارسال شود تا با NDK ساخته شود، راهنمای CMake را ببینید.

    رفتار پیش‌فرض سیستم ساخت این است که باینری‌های هر ABI را در یک APK واحد (که به آن APK چاق نیز می‌گویند) قرار دهد. یک APK چاق به‌طور قابل‌توجهی بزرگ‌تر از یک APK است که فقط شامل باینری‌های یک ABI واحد است؛ این معاوضه باعث سازگاری گسترده‌تر می‌شود، اما به قیمت یک APK بزرگ‌تر. اکیداً توصیه می‌شود که از دسته‌های برنامه یا فایل‌های APK جداشده استفاده کنید تا اندازه فایل‌های APK خود را کاهش دهید و درعین‌حال حداکثر سازگاری دستگاه را حفظ کنید.

    در زمان نصب، مدیر بسته فقط مناسب‌ترین کد ماشینی را برای دستگاه هدف از حالت فشرده خارج می‌کند. برای جزئیات، به استخراج خودکار کد بومی در زمان نصب مراجعه کنید.

    مدیریت ABI در پلاتفرم Android

    این بخش جزئیاتی درباره نحوه مدیریت کد بومی در APK توسط پلاتفرم Android ارائه می‌دهد.

    کد بومی در بسته‌های برنامه

    هم «فروشگاه Play» و هم «مدیر بسته» انتظار دارند کتابخانه‌های تولیدشده توسط NDK را در مسیرهای فایل داخل APK مطابق با الگوی زیر پیدا کنند:

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

    در اینجا، <abi> یکی از نام‌های ABI فهرست‌شده در ABIهای پشتیبانی‌شده است، و <name> نام کتابخانه است که آن را برای متغیر LOCAL_MODULE در فایل Android.mk تعریف کرده‌اید. ازآنجایی‌که فایل‌های APK فقط فایل‌های zip هستند، باز کردن آن‌ها و تأیید اینکه کتابخانه‌های بومی هم‌رسانی‌شده در جای خود قرار دارند، بسیار ساده است.

    اگر سیستم کتابخانه‌های مشترک بومی را در جایی که انتظار دارد پیدا نکند، نمی‌تواند از آن‌ها استفاده کند. در چنین مواردی، خود برنامه باید کتابخانه‌ها را کپی کند و سپس dlopen() را اجرا کند.

    در APK چاق، هر کتابخانه در دایرکتوری‌ای قرار دارد که نام آن با ABI مربوطه مطابقت دارد. برای مثال، یک APK چاق ممکن است حاوی موارد زیر باشد:

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

    پشتیبانی پلاتفرم Android از میانای دودویی کاربردی

    سیستم Android در زمان اجرا می‌داند از کدام ABI(ها) پشتیبانی می‌کند، زیرا ویژگی‌های سیستم ویژه ساخت نشان می‌دهد:

    • رابط دودویی برنامه اصلی برای دستگاه، مطابق با کد ماشینی که در خود تصویر سیستم استفاده شده است.
    • به‌صورت اختیاری، واسط‌های دودویی برنامه ثانویه، مطابق با واسط‌های دودویی برنامه دیگری که ایمیج سیستم نیز پشتیبانی می‌کند.

    این سازوکار تضمین می‌کند که سیستم بهترین کد ماشینی را در زمان نصب از بسته استخراج می‌کند.

    می‌توانید نصب apk را برای ABI خاصی اجباری کنید. این کار می‌تواند برای آزمایش در دستگاه‌هایی که از بیش از یک ABI پشتیبانی می‌کنند مفید باشد. از فرمان زیر استفاده کنید:

    adb install --abi abi-identifier path_to_apk
    

    استخراج خودکار کد بومی در زمان نصب

    هنگام نصب برنامه، سرویس مدیر بسته فایل APK را اسکن می‌کند و به‌دنبال کتابخانه‌های مشترک با این قالب می‌گردد:

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

    اگر هیچ‌کدام پیدا نشد و ABI ثانویه تعریف کرده باشید، سرویس کتابخانه‌های مشترک را برای فرم زیر اسکن می‌کند:

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

    وقتی مدیر بسته کتابخانه‌های موردنظرش را پیدا می‌کند، آن‌ها را در /lib/lib<name>.so، زیر دایرکتوری کتابخانه بومی برنامه (<nativeLibraryDir>/) کپی می‌کند. قطعه‌های زیر nativeLibraryDir را بازیابی می‌کنند:

    کاتلین

    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}")

    جاوا

    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: فعال کردن PAC و BTI برای C/C++‎

    فعال کردن PAC/BTI دربرابر برخی‌از بردارهای حمله محافظت ایجاد می‌کند. PAC با امضای رمزنگاری‌شده نشانی‌های برگشت در پیش‌گفتار تابع و بررسی اینکه نشانی برگشت همچنان به‌درستی در پس‌گفتار امضا شده است، از نشانی‌های برگشت محافظت می‌کند. BTI با الزام اینکه هر هدف شاخه یک دستورالعمل ویژه باشد که کاری انجام نمی‌دهد جز اینکه به پردازنده بگوید فرود آمدن در آنجا مشکلی ندارد، از پرش به مکان‌های دلخواه در کد شما جلوگیری می‌کند.

    ‫Android از دستورالعمل‌های PAC/BTI استفاده می‌کند که در پردازنده‌های قدیمی‌تر که از دستورالعمل‌های جدید پشتیبانی نمی‌کنند، هیچ کاری انجام نمی‌دهند. فقط دستگاه‌های ARMv9 از محافظت PAC/BTI برخوردار خواهند بود، اما می‌توانید همان کد را در دستگاه‌های ARMv8 نیز اجرا کنید: نیازی به چندین گونه از کتابخانه شما نیست. حتی در دستگاه‌های ARMv9،‏ PAC/BTI فقط برای کد ۶۴ بیتی اعمال می‌شود.

    فعال کردن PAC/BTI باعث افزایش جزئی در اندازه کد می‌شود، معمولاً ۱٪.

    برای توضیح دقیق درباره بردارهای حمله‌ای که PAC/BTI هدف قرار می‌دهد و نحوه عملکرد محافظت، به آشنایی با معماری - ارائه محافظت برای نرم‌افزارهای پیچیده (PDF) از Arm مراجعه کنید.

    تغییرات ساخت

    ndk-build

    ‫LOCAL_BRANCH_PROTECTION := standard را در هر واحد Android.mk تنظیم کنید.

    CMake

    از target_compile_options($TARGET PRIVATE -mbranch-protection=standard) برای هر هدف در CMakeLists.txt استفاده کنید.

    سیستم‌های ساخت دیگر

    کدتان را بااستفاده از -mbranch-protection=standard ترجمه کنید. این پرچم فقط هنگام کامپایل کردن برای arm64-v8a ABI کار می‌کند. هنگام پیوند دادن، نیازی به استفاده از این پرچم نیست.

    عیب‌یابی

    ما از هیچ مشکلی در پشتیبانی کامپایلر برای 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 ارتقا دهید.

    • نسخه‌های قدیمی برخی‌از سیستم‌های مدیریت حقوق دیجیتال برنامه کدی تولید می‌کنند که الزامات PAC/BTI را نقض می‌کند. اگر از DRM برنامه استفاده می‌کنید و هنگام فعال کردن PAC/BTI با مشکل مواجه می‌شوید، برای دریافت نسخه اصلاح‌شده با فروشنده DRM خود تماس بگیرید.