ABI اندروید

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

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

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

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

ABI های پشتیبانی شده

جدول 1. ABI ها و مجموعه دستورالعمل های پشتیبانی شده.

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

    توجه: از لحاظ تاریخی NDK از ARMv5 (armeabi) و MIPS 32 بیتی و 64 بیتی پشتیبانی می کرد، اما پشتیبانی از این ABI ها در NDK r17 حذف شد.

    armeabi-v7a

    این ABI برای پردازنده های ARM 32 بیتی است. این شامل Thumb-2 و Neon است.

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

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

    برای اطلاعات بیشتر در مورد تاریخچه نئون، به پشتیبانی نئون مراجعه کنید.

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

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

    arm64-v8a

    این ABI برای پردازنده های ARM 64 بیتی است.

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

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

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

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

    x86

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

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

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

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

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

    این ABI از یک long double 64 بیتی استفاده می کند ( IEEE binary64 همان double است، و نه متداول تر 80 بیتی Intel-only long double ).

    x86_64

    این ABI برای CPUهایی است که از مجموعه دستورالعمل هایی که معمولاً به عنوان "x86-64" نامیده می شود پشتیبانی می کنند.

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

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

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

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

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

    گریدل

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

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

    ndk-build

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

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

    در زمان نصب، مدیر بسته تنها مناسب ترین کد ماشین را برای دستگاه مورد نظر باز می کند. برای جزئیات، به استخراج خودکار کد بومی در زمان نصب مراجعه کنید.

    مدیریت ABI در پلتفرم اندروید

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

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

    هم Play Store و هم Package Manager انتظار دارند که کتابخانه های تولید شده توسط NDK را در مسیرهای فایل در داخل APK با الگوی زیر پیدا کنند:

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

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

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

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

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

    توجه: در صورت وجود هر دو دایرکتوری، دستگاه‌های Android مبتنی بر ARMv7 که نسخه 4.0.3 یا قدیمی‌تر دارند، کتابخانه‌های بومی را از دایرکتوری armeabi به جای پوشه armeabi-v7a نصب می‌کنند. این به این دلیل است که /lib/armeabi/ بعد از /lib/armeabi-v7a/ در APK می آید. این مشکل از 4.0.4 رفع شده است.

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

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

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

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

    برای بهترین عملکرد، باید مستقیماً برای ABI اولیه کامپایل کنید. به عنوان مثال، یک دستگاه معمولی مبتنی بر ARMv5TE فقط ABI اصلی را تعریف می کند: armeabi . در مقابل، یک دستگاه معمولی مبتنی بر ARMv7، ABI اولیه را به عنوان armeabi-v7a و دستگاه ثانویه را به عنوان armeabi تعریف می کند، زیرا می تواند باینری های بومی برنامه تولید شده برای هر یک از آنها را اجرا کند.

    دستگاه های 64 بیتی نیز از انواع 32 بیتی خود پشتیبانی می کنند. با استفاده از دستگاه های arm64-v8a به عنوان مثال، دستگاه همچنین می تواند کد armeabi و armeabi-v7a را اجرا کند. با این حال، توجه داشته باشید که اگر برنامه شما به جای اینکه به دستگاهی که نسخه armeabi-v7a برنامه شما را اجرا می کند، arm64-v8a را هدف قرار دهد، در دستگاه های 64 بیتی بسیار بهتر عمل خواهد کرد.

    بسیاری از دستگاه های مبتنی بر x86 می توانند باینری های armeabi-v7a و armeabi NDK را نیز اجرا کنند. برای چنین دستگاه هایی، ABI اولیه x86 و دومی، armeabi-v7a خواهد بود.

    می‌توانید به اجبار یک apk را برای یک 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 فقط برای کدهای 64 بیتی اعمال می شود.

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

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

    تغییرات ایجاد کنید

    ndk-build

    LOCAL_BRANCH_PROTECTION := standard در هر ماژول Android.mk خود تنظیم کنید.

    CMake

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

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

    کد خود را با استفاده از -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 فعلی ارتقا دهید.

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