برنامه نویسی 480 بازدید

100 اپلیکیشن برتر در لیست محبوب‌ترین اپلیکیشن‌های اندرویدی تا زمان نگارش این مقاله بیش از 54 میلیارد بار نصب شده‌اند. 85 درصد از این اپلیکیشن‌ها دارای کد «نیتیو» (native) با استفاده از بیش از 1000 کتابخانه نیتیو هستند. اگر تجربه کار روی چنین اپلیکیشن‌ها یا هر اپلیکیشن بزرگ دیگری را داشته باشید، می‌دانید که احتمال بروز کرش نیتیو بسیار بالا است.

توسعه‌دهندگان اندروید می‌بایست در زمینه دیباگ کردن «رد پشته» (Stack Trace) کرش نیتیو که در زبان اندرویدی «سنگ قبر» (Tombstone) نامیده می‌شود، تجربه مناسبی داشته باشند. اما کرش اپلیکیشن در بخش نیتیو (یعنی در کدهای سطح پایین C یا ++C) در اغلب موارد پیچیده و درک آن دشوار است. علاوه بر آن امکان از کار افتادن JVM (ماشین مجازی جاوا) پیش از بازگشت کنترل به کد جاوا/کاتلین نیز وجود دارد. این بدان معنی است که شما امکان به دست آوردن «استثنا» (Exception) را در سطح اپلیکیشن نخواهید داشت و تجربه کاربری ناخوشایندی رقم می‌خورد.

پیش از آغاز

مستندات توسعه‌دهندگان اندروید اطلاعات مفید زیادی در مورد عیب‌یابی کرش نیتیو (+) ارائه کرده است، اما جای مثال‌های جامع و مفیدی که به تفهیم بهتر موضوع کمک کند، خالی است.

نکته: اگر با کد نیتیو روی پلتفرم اندروید آشنایی ندارید، بهتر است ابتدا راهنمای NDK اندروید (+) را مطالعه کنید.

کتابخانه‌های نیتیو در بسیاری از اپلیکیشن‌ها مفید هستند؛ اما برخی از کاربردهای آن‌ها به شرح زیر است:

  • بهره‌گیری از سطوح بالاتری از عملکرد دستگاه برای رسیدن به تأخیر پایین یا اجرای اپلیکیشن‌های سنگین از نظر محاسبات مانند بازی یا شبیه‌سازی‌های فیزیکی.
  • استفاده مجدد از کتابخانه‌های C یا ++C که از سوی شما یا توسعه‌دهندگان دیگر توسعه یافته‌اند.
  • به علاوه کتابخانه‌های نیتیو قادرند امنیت اپلیکیشن را افزایش دهند و می‌توانند در اپلیکیشن‌هایی که برای پلتفرم‌های مختلف نوشته می‌شوند، مورد استفاده قرار گیرند.

مثال‌هایی از دنیای واقعی

تصور کنید در یک تیم Android SDK مشغول به کار هستید که در پروژه خود با کتابخانه شخص ثالثی سر و کار دارید که شامل کدهای نیتیو است. اشیای مشترک (فایل‌های so.) نیز به صورت pre-obfuscated هستند که موجب می‌شود دیباگ کردن هر گونه کرش دشوار باشد.

شاید کتابخانه مشترکی که در اپلیکیشن شما گنجانده شده، از قبل obfuscated باشد و می‌بایست از obfuscation مجدد جلوگیری کنید. اگر از obfuscation مجدد جلوگیری نکنید، احتمال بالایی وجود دارد که با مشکل مواجه شوید.

در زمان یکپارچه‌سازی این کتابخانه با اپلیکیشن، اگر با یک کرش در runtime در build-های release مواجه شوید که obfuscation شده است، عملاً با موقعیت بسیار دشواری روبرو شده‌اید. Obfuscation کد برای حفظ امنیت اپلیکیشن ضروری است و از این رو باید کرش را به سرعت پیش از انتشار بعدی رفع کنید.

در این موارد باید یک راه‌حل برای دیباگ کردن اپلیکیشن بیابید. به اپلیکیشن نمونه ساده زیر توجه کنید. مراحل تحلیل و دیباگ کردن برای رفع کرش نیتیو در این اپلیکیشن استفاده شده‌اند.

اپلیکیشن نمونه: NativeCrashApp

کرش نیتیو در اندروید
نمودار توالی برای اپلیکیشن نمونه / برای مشاهده تصویر در ابعاد اصلی روی آن کلیک کنید.

نکته: این اپلیکیشن نمونه به عنوان یک اپلیکیشن نهایی هیچ مناسبتی ندارد و صرفاً با مقاصد آموزشی ارائه شده است.

گردش کار اپلیکیشن ساده (و غیر ضروری) است، اما رفتار جالبی را شامل می‌شود. تابع ابتدایی و منفرد برای نمایش نام دستگاه در قالبی کاربرپسند به کاربر استفاده می‌شود و صرفاً یک نام بی‌معنی مدل از سوی Build.MODEL بازگشت نمی‌یابد. به این منظور از کتابخانه AndroidDeviceNames (+) استفاده شده است.

1. کاربر اپلیکیشن را اجرا می‌کند

اپلیکیشن در زمان اجرا شدن با استفاده از کتابخانه سفارشی اندروید به دنبال نام دستگاه می‌گردد.

کرش نیتیو در اندروید

2. فراخوانی‌های کتابخانه به سطح نیتیو

سطح نیتیو (کتابخانه ++C) از طریق JNI یا «رابط نیتیو جاوا» (Java Native Interface) فراخوانی می‌شود.

کرش نیتیو در اندروید

3. فراخوانی بازگشتی به کتابخانه اندروید از طریق بازتاب

در این مرحله یک فراخوانی بازگشتی به کتابخانه اندروید از طریق reflection برای بررسی نام دستگاه (قابل خواندن از سوی انسان) صورت می‌گیرد.

کرش نیتیو در اندروید

4. بازگشت دادن نام دستگاه به سطوح اولیه

در نهایت نام دستگاه به اپلیکیشن بازگشت و روی صفحه نمایش می‌یابد.

کرش نیتیو در اندروید
تصویری از اپلیکیشن نمونه

نکته: بدیهی است که همه این اتفاقات می‌توانست در Activity رخ دهد. کتابخانه Android و کتابخانه ++C کاملاً غیر ضروری هستند؛ اما این روش جالب‌تر است.

باگ کجاست؟

ما به منظور مقاصد آموزشی مقداری باگ در کد فوق اضافه کرده‌ایم. برای مشاهده این باگ‌ها به flavor مربوط به نسخه broken این پروژه در این آدرس (+) مراجعه کنید تا باگ‌هایی را که نیازمند دیباگ شدن هستند را ببینید.

Shrinking و Obfuscation کد

تصور کنید ما به عنوان یک توسعه‌دهنده مسئولیت‌پذیر اندروید، می‌خواهیم امنیت اپلیکیشن خود را از طریق ابزارهای Shrinking و Obfuscation کد افزایش دهیم. بدین ترتیب باید ابزار منتخب Obfuscation کد مانند ProGuard (+) را مورد استفاده قرار دهیم. در این فرایند کلاس‌ها، فیلدها، متدها و خصوصیت‌های بی‌استفاده تشخیص داده شده و از اپلیکیشن بسته‌بندی‌شده حذف می‌شوند.

باگ شماره 1

متأسفانه زمانی که build مربوط به release اپلیکیشن خود را تست می‌کنیم با یک کرش مواجه می‌شویم.

هیچ پیاده‌سازی برای کلاس (com.jacksoncheek.a.a.a(boolean وجود ندارد؛ اما شاید کلاً معنی این را نمی‌دانید. اگر فایل نگاشت mapping.txt را که ProGuard در خروجی ارائه کرده بررسی کنیم، می‌بینیم که شامل ترجمه‌ای بین یک کلاس، متد و نام فیلدهای اصلی و obfuscated است.

اینک می‌دانیم که ProGuard برخی از متدهای ما را به طور نادرستی obfuscate کرده است. این نوع از خطا در زمان obfuscation امری معمول است.

نکته پیشرفته: ProGuard کد نیتیو را بررسی نمی‌کند و از این رو به طور خودکار کلاس‌ها یا اعضای کلاس‌هایی را که از طریق reflection در کد نیتیو فراخوانی می‌شوند، نگهداری نمی‌کند. اینک زمان آن رسیده است که این متدها را نیز از طریق فلگ keep- در پروژه حفظ کنیم.

  • keepclasseswithmembernames  – نام کلاس و متدهای نیتیو را حفظ می‌کند.
  • includedescriptorclasses  – انواع بازگشتی و پارامترها را حفظ می‌کند.

باگ شماره 2

بدین ترتیب یک بار دیگر اپلیکیشن را تست می‌کنیم و با کرش دیگری مواجه می‌شویم.

به نظر می‌رسد که یک خطای دیگر obfuscation وجود دارد.

این خطا کمی پیچیده‌تر است. چنان که شاهد هستید، نام کلاس DevicePropertiesNative، نام متد getDeviceName؛ نوع پارامتر () یعنی void و نوع بازگشتی Ljava/lang/String پیدا نشده است.

بنابراین باید متدهای کلاس و نیتیو را از obfuscate شدن بازداریم؛ اما انواع بازگشتی و پارامترها چنین حالتی ندارند. این وضعیت تضمین می‌کند که کد «امضای متد» (method signature) با کتابخانه نیتیو سازگار خواهد بود.

ما باید یک قاعده keep- در پیکربندی ProGuard اضافه کنیم تا از obfuscate شدن متد ()getDeviceName جلوگیری کنیم. راهنمای ProGuard (+) اطلاعات زیادی در مورد گزینه‌های پیکربندی مختلف ارائه می‌کند.

باگ شماره 3

در ادامه پروژه را مجدداً تست می‌کنیم و می‌بینیم که بار دیگر یک کرش نیتیو داریم!

این یک خطای segmentation به صورت SIGSEGV در آدرس حافظه مجازی 0xff799ffc است؛ اما در عمل اطلاعات مفید چندانی ارائه نمی‌کند. SEGV_ACCERR زمانی رخ می‌دهد که یک اشاره‌گر بخواهد شیئی را که مجوزهای دسترسی نامعتبری دارد بنویسد.

اینک نوبت آن رسیده است که به بررسی log-ها بپردازیم و tombstone را که همان dump کرش برای کرش‌های نیتیو است، پیدا کنیم. اگر در log-ها برای یافتن ابتدای tombstone، عبارت *** *** را جستجو کنید، با اطلاعات زیر مواجه می‌شوید:

  • اثر انگشت بیلد: با مشخصه سیستم ro.build.fingerprint مطابقت دارد.
  • بازبینی سخت‌افزاری: با مشخصه سیستم ro.revision مطابقت دارد.
  • ABI (اینترفیس باینری اپلیکیشن): دستورالعمل پردازنده برای تعیین معماری است که armeabi-v7a برای دستگاه‌های اندرویدی متداول‌ترین گزینه است.
  • نام پردازش از کارافتاده >>> … <<< (و شناسه پردازش) و نام نخ به صورت …:name و شناسه نخ.
  • نوع سیگنال خاتمه به صورت SIGSEGV، روش دریافت آن سیگنال در SEGV_ACCER و آدرس خطا در حافظه.
  • ثبات‌های سی‌پی‌یو
  • محتوای پشته مورد فراخوانی (backtrace).

دیباگ کردن کرش‌های نیتیو

در این بخش با روش‌های دیباگ کردن کرش‌های نیتیو آشنا می‌شویم.

بررسی Backtrace

مقادیر PC (شمارنده برنامه) آدرس‌های متناظر حافظه با موقعیت کتابخانه مشترک هستند. این همان جایی است که بیشترین اطلاعات در مورد کرش نیتیو و مکان آن در کتابخانه را به دست می‌آوریم.

کرش ما در آدرس حافظه 000008e8 در ابتدای پشته فراخوانی در libproperty-checker.so رخ داده است.

پشته Android NDK دو ابزار ارائه می‌کند که به دیباگ کردن tombstone-ها کمک می‌کند و ndk-stack و addr2line نام دارند. ابزارهای NDK را با ابزار مدیریت اندروید استودیو نصب کنید و دایرکتوری NDK را به مسیر bash_profile. اضافه کنید.

ndk-stack

ابزار ndk-stack (+) اقدام به نمادسازی از ردهای پشته برای یک tombstone می‌کند. در واقع این ابزار آدرس‌های حافظه را به فایل‌های منبع مرتبط تبدیل می‌کند و شماره خطوط را از کد منبع کتابخانه نیتیو نمایش می‌دهد.

addr2line

امکان استفاده از این ابزار addr2line نیز برای دریافت آدرس حافظه‌ای که کد نیتیو موجب کرش شده وجود دارد. بدین ترتیب نام فایل منبع و خط مربوطه به دست می‌آید. این ابزار بخشی از مجموعه ابزار NDK است. باید مطمئن شوید که از addr2line برای نوع ABI صحیح دستگاه یعنی x86 (نامتداول)، armeabi یا armeabi-v7a (متداول) استفاده می‌کنید.

در این مورد مسیر addr2line برای انواع ABI به صورت x86 به صورت زیر است:

کاربرد

مثال

اکنون می‌دانیم که متد نیتیو به نام (accidentallyForceStackOverflow(int در فایل منبع propertyChecker.cpp و شماره خط 64 موجب بروز کرش نیتیو شده است.

کرش نیتیو

بدین ترتیب باگ نیتیو خود را یافته‌ایم. این کتابخانه به صورت تصادفی با فراخوانی یک تابع بازگشتی غیر پایانی به صورت نامتناهی موجب یک خطای «سرریز پشته» (stack overflow) شده است. راه‌حل سریع در این بخش حذف همه کاربردهای این متد است.

در دنیای واقعی ممکن است با نسخه‌های release از یک ارائه‌دهنده کتابخانه کار کنید و از این رو دسترسی به کد منبع برای دیباگ کردن نداشته باشید. از طرف دیگر همه فایل‌های so. برای دیباگ کردن با ndk-stack مناسب نیستند، زیرا کتابخانه‌های منتشر شده عموماً از stripped binaries استفاده می‌کنند که باعث می‌شود دیباگ کردن آن‌ها دشوارتر شود.

این همان جایی است که ابزار addr2line واقعاً به ابزار مفیدی تبدیل می‌شود. اگر نام متد نیتیو که کرش در آن رخ داده است در tombstone نمایش نیابد، که برای همه دستگاه‌ها هم چنین تضمینی وجود ندارد، می‌توانید از addr2line برای دریافت نام متد نیتیو استفاده کنید.

ابتدا فایل apk. را دی‌کامپایل بکنید (کافی است آن را unzip بکنید) و فایل‌های so. بسته‌بندی‌شده در اپلیکیشن را از دایرکتوری lib/ استخراج کنید. سپس کتابخانه مشترک را برای نوع دستگاه ABI مثلاً armeabi-v7a استخراج کنید.

نکته: این فایل‌ها در دایرکتوری /app/src/main/jniLibs نیز قرار دارند.

در این روش شماره خط فایل منبعی که کرش رخ داده است به دست نمی‌آید، چون APK تنها شامل فایل‌های stripped binaries است؛ اما نام متد را به صورت  (accidentallyForceStackOverflow(int به دست می‌آوریم که در نوع خود مفید است.

جمع‌بندی

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

  1. ابتدا باگ را روی انواع معماری‌های مختلف دستگاه‌ها بررسی کنید.
  2. فایل apk. را دی کامپایل کرده و مطمئن شوید که فایل‌های کتابخانه مشترک so. برای هر معماری موجود هستند.
  3. بررسی کنید که ابزار مدیریت بسته اندروید به درستی کد نیتیو را همراه با اپلیکیشن نصب می‌کند. به این منظور بررسی کنید که کتابخانه مشترک so. در runtime بارگذاری می‌شود یا نه. شما باید از ابزار Native Libs Monitor (+) برای بررسی آسان اپلیکیشن‌های دارای کتابخانه‌های نیتیو روی دستگاه خود استفاده کنید؛ اما هیچ تضمینی برای امنیت استفاده از این اپلیکیشن روی دستگاه‌هایی که build-های دیباگ مالکانه دارند وجود ندارد.
  4. قواعد keep- خاصی را به پیکربندی ProGuard اضافه کنید تا متدهای کلاس و نیتیو را از obfuscate شدن منع کنید. این مورد در خصوص انواع بازگشتی و پارامترها صدق نمی‌کند.
  5. Tombstone-های کرش نیتیو را با استفاده از ابزارهای ndk-stack و addr2line بررسی کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *