Что же, начнём по порядку.
Что необходимо
- Минимальные знания C.
- Минимальные знания Java.
- Некоторое понимание того как взаимодействуют элементы системы Android.
- Рутованый Android телефон.
- IDE с поддержкой Android SDK/NDK (в моём случае eclipse, его очень легко настроить для работы с Android и описано это много раз).
- Тулчейн для кросс компиляции которым было собрано ядро на целевом устройстве.
- Собранное ядро для нашего устройства с правильной локальной версией.
Стоит сказать что я использовал ОС Linux Ubuntu 11.10 и все примеры буду приводить для неё.
Первые 3 пункта очевидны, как добиться 4 и 5 легко найти в интернете. Последние два рассмотрим подробно.
Выбор тулчейна для кросс компиляции модулей ядра (драйверов)
В данной статье мы не рассматриваем возможность прошивки собственноручно собранного ядра на свой телефон поэтому мы должны придерживаться определённых правил.
Для того чтобы узнать каким компилятором собрано ядро на нашем устройстве выполняем команду:
cat /proc/version
c помощью любого эмулятора терминала или используя утилиту adb:
adb shell "cat /proc/version"
В результате получаем строку вроде этой:
Linux version 3.0.69-g26a847e (blindnumb@iof303) (gcc version 4.7.2 20120701 (prerelease) (Linaro GCC 4.7-2012.07)) #1 PREEMPT Mon Mar 18 12:19:10 MST 2013
Видим что у нас установлено ядро версии 3.0.69 локальная версия "-g26a847e" и собрано оно тулчейном Linaro GCC 4.7-2012.07. Зная версию находим необходимый тулчейн и распаковываем в любую папку. У меня путь выглядел так:
/home/user/android/android_prebuilt_linux-x86_toolchain_arm-gnueabihf-linaro-4.7
Сборка ядра
Для начала узнаем какое именно ядро использует наше устройство. Это можно сделать выполнив команду указанную выше или зайдя на устройстве в настройки, раздел «О телефоне».
Как было сказано выше в моём случае это 3.0.69-g26a847e. Немного поковырявшись на гитхабе прошивки (PACman for HTC Desire S) я определил что это ядро AndromadusMod.
Копируем найденные иходники себе на локальную машину (я предварительно форкнул необходимый репозиторий себе в гитхаб и выполнил git clone, производители вроде Google и изготовители кастомных прошивок держат исходники ядра в репозиториях с открытым доступом, некоторые просто позволяют скачать исходники в виде архива). Для меня это выглядело так:
/home/user/android/saga-3.0.69
Теперь нужно найти конфигурацию с которой собрано ядро нашего устройства. В большинстве случаев конфигурация лежит на самом устройстве и получить её можно с помощью adb, распаковать и скопировать в папку с исходниками ядра:
adb pull /proc/config.gz . gunzip ./config.gz cp ./config /home/user/android/saga-3.0.69/arch/arm/my_device_defconfig
Необходимо также немного подредактировать конфигурацию — установить локальную версию на идентичную той что мы узнали ранее и выключить автоматическое назначение локальной версии. Сделать это можно с помощью любого текстового редактора:
CONFIG_LOCAL_VERSION="-g26a847e" CONFIG_LOCAL_VESION_AUTO=n
После переходим в папку с исходниками, настраиваем переменные окружения для сборки и собственно собираем ядро:
cd /home/user/android/saga-3.0.69 export ARCH=arm export CROSS_COMPILE=/home/user/android/android_prebuilt_linux-x86_toolchain_arm-gnueabihf-linaro-4.7/bin/arm-eabi- export LOCALVERSION= all make my_device_defconfig make
Теперь можно перейти к программированию.
Написание кода
Android приложение
Учитывая огромное количество статей о написании Android приложения я рассмотрю только моменты связанные с задачей.
Наше приложение будет иметь всего 1 Activity:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:text="@string/btnText1" android:onClick="onClick"/> <EditText android:id="@+id/editText1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_alignParentRight="true" android:layout_below="@+id/button1" android:layout_marginTop="42dp" android:ems="10" android:textSize="16sp" android:inputType="textMultiLine" /> </RelativeLayout>
Выглядит это в итоге вот так:
На кнопку мы назначаем событие которое получит информацию от нашего драйвера и запишет её в текстовое поле:
public class MainActivity extends Activity { private EditText text; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (EditText)findViewById(R.id.editText1); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } public void onClick(View view) { switch (view.getId()) { case R.id.button1: text.setText(IoctlWrapper.getData()); } } }
Теперь создадим класс обёртку для нашей jni библиотеки:
public class IoctlWrapper { public static native String getKData(); //Объявление нашего нативного метода, который будет общаться с драйвером public static String getData() { return getKData(); } static { System.loadLibrary("ioctlwrap"); } }
JNI
Создадим папку jni в корне проекта Android приложения.
Далее сгенерируем Си хедер для нашей нативной библиотеки:
cd src javac -d /tmp/ com/propheta13/amoduse/IoctlWrapper.java cd /tmp javah -jni com.propheta13.amoduse.IoctlWrapper
Получаем хедер и копируем в ранее созданную папку, создадим соответствующий .c и конфигурацию сборки Android.mk:
cd [PATH TO ANDROIDPROJ]/jni cp /tmp/com_propheta13_amoduse_IoctlWrapper.h ./ioctlwrap.h touch ./ioctlwrap.c touch ./Android.mk
Содержимое Android.mk:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := ioctlwrap LOCAL_SRC_FILES := ioctlwrap.c
Алгоритм работы библиотеки:
- Открыть ноду драйвера.
- Выделить буфер под информацию из драйвера
- Получить информацию с помощью ioctl запроса.
- Закрыть ноду.
- Преобразовать информацию в Java строку и передать в обёртку.
Полный код:
const char string[] = "Driver open failed."; #define BUF_SIZE 4096 JNIEXPORT jstring JNICALL Java_com_propheta13_amoduse_IoctlWrapper_getKData (JNIEnv *env, jclass jcl) { char *info_buf = NULL; int dfd = 0, rc = 0; dfd = open(TKMOD_DEV_PATH, O_RDONLY); if(dfd == 0) { jstring RetString = (*env)->NewStringUTF(env, string); goto exit; } info_buf = malloc(BUF_SIZE); rc = ioctl(dfd, TKMOD_IOCTL_GET_DATA, info_buf); if(rc) { strerror_r(rc, info_buf, BUF_SIZE); } jstring RetString = (*env)->NewStringUTF(env, info_buf); free(info_buf); close(dfd); exit: return RetString; }
Драйвер ядра
Полностью описывать процесс написания драйвера я не буду, сделаю лишь пару заметок:
- Драйвер написанный для этой статьи не делает ничего сверхъестественного — только возвращает список имён сетевых интерфейсов.
- Для общения с драйвером использу
Makefile для сборки позволяет указывать ядро для которого требуется собрать данный драйвер, для этого нужно правильно указать переменные окружения и использовать команду:
make KMODDIR=[path to kernel]
Запуск
Для начала зальём собранный драйвер на устройство, и установим его в ядро, заодно сделаем ноду драйвера доступной для всех:
adb push ./test_kmod.ko /data/local/tmp root@android:/ # rmmod test_kmod root@android:/ # insmod /data/local/tmp/test_kmod.ko root@android:/ # chmod 777 /dev/tkmod_device
Если версия ядра модифицирована правильно и ядро совпадает с тем которое было на устройстве ошибок быть не должно.
После можно запускать Android приложение напрямую через eclipse или установить его. Нажимаем единственную кнопку и получаем результат:
Логи ядра можно получить командой:
root@android:/ # dmesg | grep [TEST_KMOD] # можно и не фильтровать но так лучше видно.
<6>[ 8695.448028] [TEST_KMOD] == Module init == <7>[ 8775.583587] [TEST_KMOD] tkmod opened. Descriptor: 0xc2e98e00. <7>[ 8775.583770] [TEST_KMOD] TKMOD_IOCTL_GET_DATA request. <6>[ 8775.583923] [TEST_KMOD] name = lo <6>[ 8775.584167] [TEST_KMOD] name = dummy0 <6>[ 8775.584259] [TEST_KMOD] name = rmnet0 <6>[ 8775.584320] [TEST_KMOD] name = rmnet1 <6>[ 8775.584503] [TEST_KMOD] name = rmnet2 <6>[ 8775.584564] [TEST_KMOD] name = rmnet3 <6>[ 8775.584655] [TEST_KMOD] name = rmnet4 <6>[ 8775.584777] [TEST_KMOD] name = rmnet5 <6>[ 8775.584930] [TEST_KMOD] name = rmnet6 <6>[ 8775.585021] [TEST_KMOD] name = rmnet7 <6>[ 8775.585113] [TEST_KMOD] name = gre0 <6>[ 8775.585266] [TEST_KMOD] name = sit0 <6>[ 8775.585357] [TEST_KMOD] name = ip6tnl0 <7>[ 8775.585479] [TEST_KMOD] tkmod_ 0xc2e98e00 closed successfuly.
Заключение
Показанное применение данной связки не единственное. Использование драйверов ядра позволяет напрямую работать с любыми интерфейсами устройства, влиять на работу любого приложения и системы в целом, также позволяет работать с интерфейсами которые спрятаны глубоко в системе за целой кучей API и фреймворков — например драйвер который будет писать необходимую вам информацию прямо в буфер видеопамяти устройства. Данное решение применимо не только для телефонов но и для любых устройств на базе Android.
Полные исходники лежат на GitHub.
На этом заканчиваю, спасибо за внимание. Надеюсь что данный материал окажется для кого нибудь полезным.
Использованные ресурсы:
1. developer.android.com — Android SDK/NDK и прочее.
2. www.vogella.com — довольно неплохие и понятные статьи по разработке Android приложений.
3. blog.edwards-research.com/2012/04/tutorial-android-jni/ — туториал по использованию JNI.
4. docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/functions.html — справочный материал по интерфейсу JNI.
5. Linux Device Drivers, 3ed — библия программиста Linux Kernel.
ссылка на оригинал статьи http://habrahabr.ru/post/174077/
Добавить комментарий