Android
Ваши программные наработки не пропадут даром
Часть 2: Прикладные интерфейсы ядра. Андрей Боровский расскажет то, что вы хотели знать про Android, но боялись спросить.
Продолжим увлекательное путешествие в мир программирования для Android. Но это не то программирование, про которое пишут толстые книги. В этом мире программы, не заметные пользователю, берут под контроль управление питанием системы и перехватывают пользовательский ввод.
Итак, разберемся, что же мы имеем. Самый важный инструментарий для нас – NDK. При установке новейшей (на момент написания текста) версии NDK – r7 – где-то в недрах вашей файловой системы должна появиться директория android-ndk-r7. Ее мы будем называть NDK_ROOT (включая полный путь к ней).
В NDK_ROOT вы увидите множество поддиректорий непонятного назначения. Не отчаивайтесь: во всем этом легко разобраться, тем более что для наших хакерских целей понадобится не весь NDK. Одна из важнейших для нас директорий – NDK_ROOT/toolchains. Как видно по названию, она содержит различные версии инструментария сборки приложений для Android. Фактически это стандартный инструментарий сборки приложений GNU/Linux. Директория NDK_ROOT/toolchains включает две поддиректории: x86-4.4.3, предназначенная для сборки программ Android для платформы x-86, и arm-linux-androideabi-4.4.3, ориентированная на платформу ARM (для другой версии NDK, цифры, естественно, могут быть другими). Раз мы условились писать программы для ARM, переходим в эту директорию. В ее поддиректориях есть все необходимое для сборки и отладки программ Linux на платформе ARM. Напомню, что сами инструменты сборки предназначены для запуска на ПК архитектуры Intel, а результатом сборки станут программы, для выполнения которых потребуется процессор семейства ARM (или его эмулятор).
Вторая важная директория – NDK_ROOT/platforms. Она содержит библиотеки и заголовочные файлы для API Android различных версий (уровней). В ней вы найдете несколько поддиректорий вида android-x, где x – номер уровня API. Между уровнями API и версиями ОС Android существует четкое соответствие. Ниже приводится перечень уровней API для наиболее популярных на данный момент версий Android (полную таблицу вы можете найти на сайте разработчиков Android – developer.android.com).
Шаг за шагом: '
Импорт папки.
|
Положите на место
|
'Поиск.
|
table name
В каждой директории android-x имеются поддиректории arch-x86 и arch-arm, для двух целевых платформ. Каждая директория содержит поддиректории /usr/lib и /usr/include – то, чего нам так не хватало для сборки программ под Android. Стоит отметить, что не все библиотеки Android менялись при переходе от одного уровня API к другому. В NDK-r6 файлы некоторых библиотек представляли собой символьные ссылки на файлы тех же библиотек более ранних уровней API. Сравнивая API разных уровней, вы заметите, что с номером уровня количество библиотек растет – и не только потому, что в новых версиях Android появляются новые компоненты, но и потому, что более новые версии Android предоставляют более широкий доступ на уровне Linux API к компонентам системы, введенным ранее. Итак, желая, чтобы ваши программы получили максимальный доступ к функциям Android, экспериментируйте с новейшей версией ОС. Нетрудно видеть также, что разные уровни API обладают обратной совместимостью. Рассмотрим наиболее интересные возможности, доступные программам Linux в разных версиях Android.
Android 1.5 (уровень API 3) На этом уровне нам доступны библиотека Bionic, библиотека времени выполнения C++ в несколько урезанном варианте, интерфейс работы с потоками POSIX Threads, тоже слегка урезанный, и стандартная библиотека математических функций. Все перечисленное линкуется с исполняемым файлом автоматически; специальных команд для подключения библиотек указывать не нужно.
Библиотека zlib (работа со сжатыми данными) должна подключаться явно (с помощью ключа --lz), как и библиотека libdl, требуемая для динамической загрузки разделяемых библиотек.
Android 2.0 -2.3 Добавлены поддержка OpenGL ES 2.0 (библиотека libGLESv2.so, уровень API 5), Android bitmap API (уровень API 8), поддержка EGL (libEGL.so, уровень API 9) и поддержка OpenSL ES (libOpenSLES.so, уровень API 9)
API 9 вообще можно назвать прорывом в области программирования для Android: именно в нем появился интерфейс программирования Android native application API для программ Linux (библиотека libandroid.so). Эта библиотека позволяет приложению Linux взаимодействовать с системой так же, как это делает приложение, написанное на Java (включая графический ввод-вывод).
107917.png Android 4 (уровень API 14) Добавлена поддержка интерфейса OpenMAX AL (библиотека libOpenMAXAL.so), позволяющего работать с аппаратно-ускоренными потоками мультимедиа-данных.
Теперь у нас есть все необходимые инструменты для написания программ Linux под Android. Осталось только овладеть несколькими трюками, чтобы научиться правильно (или неправильно – это с какой стороны взглянуть) ими пользоваться.
Наш первый Make-файл
Для сборки программ Linux под Android нам понадобится специальный Make-файл. Make-файл, предназначенный для сборки даже самой простой программы Linux-Android, выглядит довольно сложно и использует приемы, редкие в обычном программировании для Linux. Но этот Make-файл можно применить как шаблон для создания файлов, управляющих сборкой более сложных программ, причем менять шаблон придется не так уж сильно.
Освоение Android мы начнем с простейшей программы test:
- include <stdio.h>
- include <stdlib.h>
int main(int argc, char **argv)
{
printf(“Hello, world!\n”);
exit(0);
return 0;
}
Она отличается от традиционной для Linux программы Hello World. Выведя сообщение, мы вызываем функцию exit() – она завершает работу программы (оператор return ничего не делает и сохранен только ради поддержки стандартного синтаксиса). Функция exit() нужна нам потому, что в нашем распоряжении нет библиотеки GNU glibc, которая бы обслуживала программу. Без явного вызова exit() программа test будет выполняться и после выхода из main(), пока не вызовет какое-либо исключение. Далее мы рассмотрим целых два способа предоставить функции main() удобства библиотеки libc без самой этой библиотеки.
Рассмотрим Make-файл для программы test – он станет основой аналогичных файлов для всех остальных наших программ.
APP := test
SRC:=test.c
SDK_ROOT:=/home/andrei/android-sdk-linux_x86
NDK_ROOT:=/home/andrei/android-ndk-r7
NDK_API_LEVEL := android-3
NDK_HOST:=linux-x86
PREBUILD:=$(NDK_ROOT)/toolchains/arm-linux-androideabi-4.4.3/prebuilt/$(NDK_HOST)
BIN := $(PREBUILD)/bin
GDB_CLIENT := $(BIN)/arm-linux-androideabi-gdb
FS_ROOT := $(NDK_ROOT)/platforms/$(NDK_API_LEVEL)/arch-arm
INSTALL_DIR := /storage
DEBUG = -g
CPP := $(BIN)/arm-linux-androideabi-g++
CC := $(BIN)/arm-linux-androideabi-gcc
CFLAGS := $(DEBUG) -I$(FS_ROOT)/usr/include
LDFLAGS := -Wl,--entry=main,-rpath-link=$(FS_ROOT)/usr/lib,-dynamic-linker=/system/bin/linker -L$(FS_ROOT)/usr/lib
LDFLAGS += -nostdlib -lc
all: $(APP)
OBJS += $(APP).o
$(APP): $(OBJS)
$(CPP) $(LDFLAGS) -o $@ $^
$(APP).o: $(SRC)
$(CC) $(SRC) -c $(INCLUDE) $(CFLAGS) -o $@
install: $(APP)
$(SDK_ROOT)/platform-tools/adb push $(APP) $(INSTALL_DIR)/$(APP)
$(SDK_ROOT)/platform-tools/adb shell chmod 777 $(INSTALL_DIR)/$(APP)
shell:
$(SDK_ROOT)/platform-tools/adb shell
run:
$(SDK_ROOT)/platform-tools/adb shell $(INSTALL_DIR)/$(APP)
debug:
$(GDB_CLIENT) $(APP)
clean:
@rm -f *.o $(APP)
Смысл переменных, объявленных в начале файла, должен быть ясен. После всех манипуляций переменная BIN содержит полное имя директории, где хранится пакет GCC для ARM и его друзья. Переменная FS_ROOT указывает системе сборки, где искать директории include и lib для подключения добавочных библиотек. При записи значения в эту переменную используется переменная API_LEVEL, задающая уровень API (то есть поддиректорию директории NDK_ROOT/platforms) для данной сборки. Нашей первой программе много не надо, и мы используем API уровня 3. Переменная INSTALL_DIR хранит директорию файловой системы целевого устройства, куда должно быть скопировано собранное приложение.
Далее объявляются переменные, содержащие ключи для компилятора, компоновщика и отладчика. Если вы знакомы с инструментарием сборки GNU, значения переменных DEBUG, CC, CPP и CFLAGS не должны вызывать у вас вопросов. Интересное начинается с переменной LDFLAGS. Ключ --entry редко используется при сборке обычных программ Linux; он позволяет указать имя функции, которая будет точкой входа в программу. При использовании стандартной glibc точкой входа является функция _start(), предоставляемая glibc, и компоновщик знает об этом. У программы test нет функции _start(), и в качестве точки входа мы указываем функцию main().
Сочетание ключей --rpath-link и --dynamic-linker позволяет обойти проблему, связанную с тем, что файловая система целевого устройства выглядит не так, как файловая система ПК, на котором мы собираем программу. При сборке на ПК программа-компоновщик должна искать библиотеки не там, где подсказывает ей система (система предложит стандартные библиотеки Linux для ПК, которые нам не подходят), а там, где мы укажем. Так как на целевом устройстве эти библиотеки расположены совсем в других местах, мы используем динамический загрузчик библиотек (/system/bin/linker – путь к компоновщику в файловой системе целевого устройства), который выполнит «интеллектуальное» связывание программы с нужными ей библиотеками.
Сочетание ключей --no-stdlib и --lc выглядит издевательским: сначала мы запрещаем связывать программу со стандартной библиотекой C, а затем выполняем-таки связывание. Объясняется это сочетание тем, что стандартная библиотека C (Bionic), с которой мы связываем программу, не является стандартной glibc.
Между прочим, концепция, реализованная в представленном Make-файле, годится не только для Android, но и для любой Linux-подобной системы, у которой есть динамический загрузчик библитек. Все, что нужно – это обзавестись разделяемыми библиотеками и заголовочными файлами для соответствующей системы.
Обратите внимание на объявленную в Make-файле цель install. Для установки программы на целевое устройство используется описанная в предыдущей части утилита adb из SDK. Как было сказано в предыдущей части, для выполнения этих команд вам могут понадобиться права root, которые по умолчанию предоставляет только эмулятор устройств Android, входящий в состав SDK. Осталось собрать программу и убедиться в том, что она работает.
Чтобы преодолеть неудобства из-за отсутствия в программе стандартной функции _start(), напишем свой вариант этой функции (файл crt0.s):
crt0.s:
.text
.global _start
_start:
mov r0, sp
mov r1, #0
add r2, pc, #4
add r3, pc, #4;
b __libc_init
b main
.word __preinit_array_start
.word __init_array_start
.word __fini_array_start
.word __ctors_start
.word 0
.word 0
...
Здесь мы ограничимся фрагментом файла (полный вариант – на диске). Не вдаваясь в детали сверх меры, отметим, что функция _start() просто вызывает функцию __libc_init(), а затем функцию main(). __libc_init() подготавливает среду окружения для функции main(). Помимо прочего, в результате вызова __libc_init() функция main() получает корректные значения параметров argc и argv (__libc_init() записывает их в регистры r0 и r1) и может завершаться оператором return без вызова функции exit(). Make-файл для сборки программы придется слегка поменять:
SRC:=test.c crt0.s
OBJS := $(APP).o crt0.o
LDFLAGS := -Wl,--entry=_start,-rpath-link=$(FS_ROOT)/usr/lib,-dynamic-linker=/system/bin/linker -L$(FS_ROOT)/usr/lib
Думаю, что модификации в пояснениях особо не нуждаются.
Наша функция _start полезна скорее в целях показа, что происходит в стандартной программе ARM Linux. В каталоге lib выбранной вами платформы NDK вы найдете файл crtbegin_dynamic.o, содержащий, по сути, объектный код той же функции _start, любезно скомпилированной для нас разработчиками Android. Чтобы воспользоваться этой функцией вместо приведенной выше, достаточно модифицировать значение переменной OBJS:
OBJS := $(APP).o $(FS_ROOT)/usr/lib/crtbegin_dynamic.o
Блокировки отключения питания
Теперь исследуем Android с точки зрения программиста Linux. Одной из специфических черт ядра Android являются блокировки отключения питания [wakelocks]. Как и большинство других мобильных устройств, устройства Android автоматически переходят в один из режимов экономии энергии, если какое-то время не происходит событий, требующих активной работы устройства. Очевидно, не все программы устроит такой режим работы: некоторым важна гарантия, что система не сразу уйдет в спячку. Этой цели и служат блокировки отключения питания.
Рассмотрим пример программы, которая блокирует отключение питания на 10 минут (файл wakelock.c)
int main(int argc, char ** argv)
{
int lock_fd;
int unlock_fd;
char * name = “native_test_lock”;
lock_fd = open(“/sys/power/wake_lock”, O_WRONLY);
if (lock_fd < 0) {
printf (“locking failed\n”);
return 1;
}
write(lock_fd, name, strlen(name));
close(lock_fd);
printf(“locking system in a wake state for 10 minutes\n”);
sleep(600);
unlock_fd = open(“/sys/power/wake_unlock”, O_WRONLY);
if (unlock_fd < 0) {
printf (“unlocking failed\n”);
return 1;
}
write(unlock_fd, name, strlen(name));
close(unlock_fd);
printf(“unlocking system\n”);
return 0;
}
Как видим, интерфейс блокировок отключения питания весьма прост и даже не требует специального API. Для включения блокировки нужно лишь записать уникальную строку (имя блокировки) в файл /sys/power/wake_lock; а для отключения – записать ту же строку в файл /sys/power/wake_unlock. Убедиться в том, что блокировка успешно создана, можно с помощью команды
cat /proc/wakelocks
Перехват событий клавиатуры
Ну, а теперь напишем настоящую хакерскую программу (в лучшем, или, наоборот, в худшем смысле этого слова): программу-кейлоггер, незаметно для остальной системы фиксирующую нажатия на клавиатуре Android. Программа пригодится для общего понимания работы ввода в ОС Android, отладки и тестирования ввода и других хороших вещей. Надеюсь, вы не думаете применять этот код для плохих вещей? О нет, я вас такому не учил.
В программе перехвата данных, поступающих с клавиатуры Android, мы задействуем механизм т. н. input events [событий ввода]. В любой системе Linux, включая Android, есть директория /dev/input/ – она содержит файлы, соответствующие различным устройствам ввода. Считывая данные из этих файлов, вы получите информацию о событиях ввода соответствующего устройства. Информация о событии передается в структуре такого вида:
typedef struct {
struct timeval time;
unsigned short type;
unsigned short code;
unsigned int value;
} input_event;
Поле timeval содержит информацию о времени наступления события. Поле type содержит данные о типе события. Для клавиатуры Android возможны два типа событий: событие, связанное с клавишей (физической или виртуальной) – код 1, и событие, повторяющее предыдущее – код 0x14. Поле code содержит скан-код клавиши. Поле value содержит состояние клавиши, вызвавшее событие (1 – клавиша нажата, 0 – клавиша отпущена).
В системе Android события клавиатуры можно добыть из файла /dev/input/event0. Информация о событии считывается из него в формате описанной выше структуры input_event. Для работы с событиями ввода мы напишем API, состоящий из трех простых функций (файл keys.c):
int keys_open(int blocking)
{
int block = blocking == KEYS_OPEN_NONBLOCKING ? O_NONBLOCK : 0;
int input = open(“/dev/input/event0”, O_RDONLY|block);
return input;
}
int keys_get(int handle, input_event *ie)
{
ie->code = 0;
read(handle, ie, sizeof(input_event));
if (ie->code) {
return 1;
}
return 0;
}
void keys_close(int handle)
{
close(handle);
}
Файл keys.c и заголовочный файл keys.h вы найдете на диске. Сама программа-кейлоггер (файл input.c), выглядит так:
int main(int argc, char ** argv)
{
input_event event;
int handle;
handle = keys_open(KEYS_OPEN_NONBLOCKING);
while(1) {
if (keys_get(handle, &event))
printf(“type: %i; code: %i; value: %i\n”, event.type, event.code, event.value);
}
return 0;
}
Данные о нажатых и отпущенных клавишах распечатываются на экран консоли в бесконечном цикле.
В зависимости от значения параметра, переданного функции keys_open(), функция keys_get() может работать в блокирующем либо неблокирующем режиме. Мы вызываем функцию в неблокирующем режиме, в котором она возвращает управление сразу же, независимо от того, появились ли новые события ввода или нет. Получив информацию об очередном событии ввода, функция keys_get() возвращает значение 1, в противном случае – 0.
Как уже отмечалось, программа не мешает работе системы, и пользователь устройства, скорее всего, вообще не заметит ее присутствия. Для запуска программы не требуется прав суперпользователя, хотя такие права, скорее всего, понадобятся для ее установки. Конечно, настоящий кейлоггер не стал бы выводить коды нажатых клавиш на консоль, а отсылал бы их по сети... Впрочем, забудьте, что я это говорил.
Возможно, вы захотите исследовать, за какие устройства отвечают другие файлы в директории /dev/input/. Это можно сделать с помощью простой программы, показанной ниже.
int main (int agrc, char ** argv)
{
int fd = -1;
char name[256]= “Unknown”;
if ((fd = open(argv[1], O_RDONLY)) < 0) {
perror(“evdev open”);
exit(1);
}
if(ioctl(fd, EVIOCGNAME(sizeof(name)), name) < 0) {
perror(“evdev ioctl”);
}
printf(“The device on %s says its name is %s\n”,
argv[1], name);
close(fd);
return 0;
}
Вы найдете этот код в файле devices.c. Запускать скомпилированную программу надо так:
devices /dev/input/event1
В результате будет распечатано понятное для человека название устройств ввода, которому соответствует указанный файл.
Код этого примера приводится в статье «Using the Input System, Part II», опубликованной в журнале «Linux Journal» в марте 2003 года. Автор статьи 2003 года, естественно, и не помышлял о программировании для ОС Android, которой в то время не существовало даже в проекте. Таким образом, мы смогли перенести на платформу Android программу, предназначенную для «обобщенной ОС Linux», что, собственно, и было одной из целей данной серии.
Данные, которые выводят все программы, написанные нами до сих пор, можно увидеть, только используя отладочную оболочку или эмулятор терминала. Настала пора писать программы, которые выводят данные с помощью стандартных средств Android – то есть графического интерфейса.
107921.png
107922.png
Версия Андроид
Уровень API
1.5
3
2.0
5
2.1.x
7
2.2.x
8
2.3–2.3.2
9
3.0.x
11
3.1.x
12
3.2
13
4.0–4.0.2
14
pic1.psd
107582.png Файл crtbegin_dynamic.o, дизассемблированный с помощью программы objdump.
pic2.psd
107844.png Наша блокировка отключения питания в окне виртуального терминала Android.
pic3.psd
107656.png Информация о нажатых клавишах на экране отладочного терминала.
pic4.psd
Интерфейсы
Обратите внимание, что хотя написанные нами программы умеют делать не так уж и мало, мы пока что не использовали библиотек Android (за исключением библиотеки Bionic, без которой можно было бы и обойтись). Фактически, многие библиотеки Linux являются «обертками» вокруг интерфейсов ядра, предоставляемых прикладному уровню. К этим интерфейсам можно обращаться напрямую, без посредничества библиотек, хотя с библиотеками, конечно, удобнее.
105927.png Перечень устройств ввода нетбука Toshiba AC-100.