О чем эта статья
В двух предыдущих статьях я рассказывал как отлаживать приложения для Android без исходного кода на Java и о некоторых особенностях установки breakpoints. Если уважаемый читатель ещё не ознакомился с этими статьями — я настоятельно рекомендую начать с них, а уже потом читать эту статью.
Так уж вышло что до сих пор я рассказывал исключительно об отладке байткода Dalvik и ни словом не обмолвился об отладке native методов. А ведь именно в native методах часто скрывается самое вкусное — хитрые защиты, интересные malware фичи, уязвимости нулевого дня. Поэтому сегодня я сжато, без «воды», расскажу как отлаживать native методы без исходного кода на C/C++ (ну или на чем, уважаемый читатель, они у вас там написаны).
Что бы извлечь пользу из моего рассказа нужно быть уже немного «в теме». В частности желательно что бы читатель
- понимал синтаксис Smali, мог вписывать в .smali файлы свой код и мог отличить декларации и вызовы native методов от обычных методов, умел пересобирать .apk файлы используя Apktool;
- представлял себе что такое Java Native Interface (JNI) и как это работает;
- знал для чего используются методы System.load(...) и System.loadLibrary(...), как они работают в Android, и по аргументам этим методов в Smali коде мог самостоятельно определить в каких .so библиотеках находятся JNI функции соответствующие тем или иным native методам;
- умел найти эти JNI функций в .so библиотеках;
- хотя бы на начальном уровне знал ассемблер ARM (в статье предполагается что отладка будет выполняться на устройстве с архитектурой ARM либо на эмуляторе который эту архитектуру эмулирует);
- имел какой-то опыт работы с gdb и gdbserver;
Вот пожалуй и все знания и навыки которые будут нужны читателю. Перейдём к инструментам.
Инструменты
Сегодня нам понадобится:
- gbd и gdbserver для ARM из последней версии Android NDK. Установка описана тут.
- Утилита
adb
из последней версии Android SDK. Установка описана здесь. - Если отладка идёт на реальном Android-устройстве — на нём нужны права root.
Перейдём к подготовке.
Подготовка
Предполагается что читатель достаточно опытен что бы самостоятельно выполнить следующие подготовительные действия:
- Дизассемблировать .apk файл приложения с помощью Apktools, исследовать файлы в поддиректории
папка/куда/дизассемблировали/приложение/smali
и выяснить:- какие native методы вызываются из байткода Dalvik;
- в какой .so библиотеке находятся соответствующие этим методам JNI функции;
- Вытащить эту .so библиотеку из поддиректории
папка/куда/дизассемблировали/приложение/lib
либо с устройства на котором будет производиться отладка, исследовать её и узнать как называются JNI функции которые соответствуют тем или иным native методам вызываемым из байткода Dalvik. - Пересобрать .apk приложение с помощью Apktools с оцпией
-d
и загнать по отладку в NetBeans как написало в этой моей статье. - Сделать так, что бы отладка в NetBeans остановилась после вызовов
System.loadLibrary(...)
илиSystem.load(...)
, которые загружают .so с интересующими нас JNI функциями, но до первого вызова какого-либо из native методов. Можно использовать трюк о котором я писал тут.
Теперь, когда все приготовления закончены, можно переходить собственно к отладке.
Отладка
Идея в том, что бы загнать наше приложение сразу под два отладчика: под отладчик встроенный в NetBeans — что бы отлаживать байткод Dalvik, и под gdb — исключительно что бы отлаживать вызовы native методов. Звучит слегка странно, но на практике вполне себе работает. Хотя и не всегда — см. следующий раздел «Подводные камни».
Итак, если читатель выполнил все подготовительные действия из предыдущего раздела «Подготовка», то сейчас у него на устройстве или эмуляторе наверняка запущено пересобранное приложение, на компьютере открыт NetBeans и отладка стоит где-нибудь после вызова System.load(...)
или System.loadLibrary(...)
, но ещё до первого вызова native метода. Причем читатель уже в курсе в какой библиотеке какие JNI функции каким native методам соответствуют. С этого мы и начнём.
Дальше идёт пошаговая инструкция. Она писалась для Windows, но думаю будет работать и для Linux и MacOS. Пожалуйста, следуйте инструкции в точности:
- Командной
abd shell
откройте ADB консоль вашего устройства или эмулятора. Найдите PID процесса вашего приложения воспользовавшись командойps
в ADB консоли. В этой же консоли выполните команду:gdbserver :5039 --attach %PID%
где
%PID%
и есть PID процесса вашего приложения. В ответ gdbserver должен вывести что-то вроде:Attached; pid = %PID% Listening on port 5039
Начиная с этого момента отладка в NetBeans «замёрзнет». Т.е. вы конечно сможете там кликать на кнопки, но это бесполезно т.к. приложение которое вы пытаетесь отлаживать в NetBeans в данный момент остановлено под отладчиком GDB. Не паникуйте, всё так и должно быть!
- Откройте новую консоль на вашем компьютере, выполните команду
adb forward tcp:5039 tcp:5039
на вашем компьютере.
- В этой же консоли запустите gdb из Android NDK:
gdb libMyNativeLibrary.so
где
gdb libMyNativeLibrary.so
— та самая .so библиотека в которой находятся интересующие нас JNI функции. В результате откроется консоль gdb. - В консоли gdb наберите следующие команды:
(gdb) target remote :5039
После этих манипуляций консоли gdb должно появится сообщение наподобии
Remote debugging using :5039 0x4009d58c in ?? ()
а в консоли ADB (она у нас ещё открыта, помните?) что-то типа
Remote debugging from host 127.0.0.1
- В консоли GDB выполните
(gdb) info functions
что бы увидеть список функций. В списке среди прочих функций должны быть и интересующие вас JNI функции, что-то типа:
0x5b5f7bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x5b5f7c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x5b5f7c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
- В консоли GDB поставте breakpoints на адреса интересующих вас JNI функций, в нашем случае это
(gdb) break *0x5b5f7bac (gdb) break *0x5b5f7c0c (gdb) break *0x5b5f7c1c
- Возобновите выполнение вашего приложения с помощью команды
c
в GDB консоли. После выполнения этой команды, отладка в NetBeans «размёрзнется» и вы снова сможете отлаживать байткод Dalvik.
Теперь, каждый раз когда в байткоде Dalvik будет встречаться вызов native метода вроде
const/high16 v8, 0x4100
invoke-static {v8}, Lmy/app/for/debug/MainActivity;->theCoolestNativeMethodEver(F)V
отладка в NetBeans будет замирать, зато gdb будет вас радовать сообщениями вроде
Breakpoint 1, 0x5b5f7c1c in Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver () from libMyNativeLibrary.so
Вот это собственно оно и есть. Дальше x/i $pc
, stepi
— в общем вперёд отлаживать одну ARM-инструкцию за другой (помните я говорил в начале что ARM ассемблер будет нужен? — ну вот...)
Подводные камни
О, их тут много. Целый сад подводных камней. Вот наиболее запоминающиеся глюки, которые попались мне при использовании GNU gdb (GDB) 7.4.1 в связке с GNU gdbserver (GDB) 7.4.1 на Android 4.0.3 в устройстве Ainol Aurora (той, старой ещё):
- Если у вас после некоторого времени gdb регулярно отваливается от gdbserver по watchdog timeout, выполните в консоли gdb
set watchdog 18000
— это должно помочь. - Иногда в результате
info functions
в списке функций отображаются не адреса функций в памяти, а смещения в .so файле, например:0x000c0bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x000c0c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x000c0c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
В этом случае положите
libMyNativeLibrary.so
в каталог, который для gdb является текущим при старте, и перезапустите gdb снова то й же самой команднойgdb libMyNativeLibrary.so
. - Иногда в результате
info functions
в списке функций отображаются и адреса функций в памяти и смещения в .so файле, например:0x5b5f7bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x5b5f7c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x5b5f7c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver 0x000c0bac Java_my_app_for_debug_MainActivity_coolNativeMethod 0x000c0c0c Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod 0x000c0c1c Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
Игнорируйте смещения в .so файле, ставьте breakpoints на адреса функций в памяти.
- Если команда
break
на имена функций работает нормально — вы счастливчик, если нет… ну собственно у меня она и не работает, поэтому в данной статье я ставлю breakpoints на адреса функций. - Может не работать
set stop-on-solib-events
. У меня не работает. - Время от времени вы будете видеть надпись
Cannot access memory at address 0x1
. Игнорируйте.
Я уверен что это далеко не полный список глюков, которые таит в себе отладка native методов без исходных кодов, и что другим исследователям попадутся совершенно другие, уникальные глюки которые не попались мне. Если кто что ещё найдёт — прошу делится в комментариях. Также в комментария прошу задавать вопросы и/или вносить технические поправки к тексту. Постараюсь ответить как можно быстрее, но если буду тупить — наберитесь пожалуйста терпения. Постараюсь ответить всем.
Happy debugging!
P.S. Просьба к минусующим статью отписываться в комментариях что именно не понравилось. Постараюсь исправить, если это возможно.
Автор: dimakovalenko