Как стать автором
Обновить

Использование драйверов из Android приложения

Время на прочтение7 мин
Количество просмотров46K
Рут даёт практически абсолютную власть над Android устройством. Сегодня я расскажу вам как получить еще больше имея склонность к программированию и желание исследовать систему на своём устройстве. Кто заинтересовался — прошу под кат.

Что же, начнём по порядку.
Что необходимо

  1. Минимальные знания C.
  2. Минимальные знания Java.
  3. Некоторое понимание того как взаимодействуют элементы системы Android.
  4. Рутованый Android телефон.
  5. IDE с поддержкой Android SDK/NDK (в моём случае eclipse, его очень легко настроить для работы с Android и описано это много раз).
  6. Тулчейн для кросс компиляции которым было собрано ядро на целевом устройстве.
  7. Собранное ядро для нашего устройства с правильной локальной версией.


Стоит сказать что я использовал ОС 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:
activity_main.xml
<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>


Выглядит это в итоге вот так:

На кнопку мы назначаем событие которое получит информацию от нашего драйвера и запишет её в текстовое поле:
MainActivity class
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

Алгоритм работы библиотеки:
  1. Открыть ноду драйвера.
  2. Выделить буфер под информацию из драйвера
  3. Получить информацию с помощью ioctl запроса.
  4. Закрыть ноду.
  5. Преобразовать информацию в Java строку и передать в обёртку.

Полный код:
ioctlwrap.c
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 < 0)
	{
		strerror_r(rc, info_buf, BUF_SIZE);
	}
	jstring RetString = (*env)->NewStringUTF(env, info_buf);
	free(info_buf);

	close(dfd);

exit:
	return RetString;
}


Драйвер ядра

Полностью описывать процесс написания драйвера я не буду, сделаю лишь пару заметок:
  1. Драйвер написанный для этой статьи не делает ничего сверхъестественного — только возвращает список имён сетевых интерфейсов.
  2. Для общения с драйвером используется механизм ioctl.
  3. 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.
UPD

Поправил несколько опечаток, ошибок в коде. Спасибо: bmx666,Shirixae
Теги:
Хабы:
Всего голосов 55: ↑49 и ↓6+43
Комментарии11

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань