В данной статье мне хотелось бы поделиться необычным подходом к разработке мобильных приложений на Android. Стандартный подход заключается в работе на Android Studio и создании простого мобильного приложения типа «Hello World» на языках Java или Kotlin. Однако, можно сделать всё и по-другому, и скоро вы это увидите. Для сначала небольшая предыстория.
Однажды моя девушка по имени Анастасия спросила: «Как работает мой гаджет? Что у него внутри? Как электричество заставляет всё это работать?»
Анастасие не было чуждо программирование, Более того, она реализовала несколько проектов на языке Basic, которые включали программирование и электронику. Возможно, это заставило её получить больше информации о работе телефона. К счастью, один из курсов информатики в университете был посвящён именно этому — или, по крайней мере, дал мне достаточно опыта, чтобы найти хороший ответ.
Следующие пару тройку недель мы провели, постигая основны — как микроскопические примеси в кремниевой решётке изменяют свои свойства, превращая их в полупроводники; как можно управлять потоком электронов через эти полупроводники, образуя транзисторы. Затем мы перешли на уровень выше, и я рассказал ей, как можно построить логические вентили, такие как NAND (логическое И-НЕ) и NOR (логическое ИЛИ-НЕ), комбинируя транзисторы особым образом.
Мы продолжили свое исследование логических элементов различного типа и объединяли их для выполнения вычислений (таких как добавление двух двоичных чисел) и ячеек памяти. Когда же у нас всё начао получатся, мы разработали простой воображаемый процессор, который содержал для начало, два регистра общего назначения и пару простых инструкций и написали простую программу, которая умножит эти два числа.
Если Вам интересна эта тема, то ознакомиться с информацией о работе 8 битных процессоров можно тут - руководство по 8-битному компьютеру с нуля — оно раскроет почти всё аспекты. Хотел бы я иметь такое руководство в свое время =)
В какой то я осознал, что у Анастасии уже достаточно своего опыта, чтобы понять, как работает процессор её телефона. У неё был Galaxy S6 Edge, созданный на архитектуре ARM (как большинство современных смартфонов). Настало время создать программу «Hello, World», её первое приложение для Android, но только на ассемблере:
.text .globl _start _start: mov %r0, $1 // дескриптор файла номер 1 (стандартный вывод) ldr %r1, =message mov %r2, $message_len mov %r7, $4 // вызов системной функции 4 (запись) swi $0 mov %r0, $0 // выход из программы с кодом 0 (ok) mov %r7, $1 // вызов системной функции 1 (выход) swi $0 .data message: .ascii "Hello, Worldn" message_len = . - message
Если вы никогда раньше не видели ассемблерный код, этот блок кода может вас напугать, но не беспокойтесь — мы пройдёмся по нему вместе.
Программа состоит из двух разделов: текст, содержащий инструкции машинного кода, и данные, начиная со строки 15, которые содержат переменные, строки и другую информацию. Раздел .text
доступен только для чтения, а .data
— еще и для записи.
В строке 2 мы определяем глобальную функцию с именем _start
. Это точка входа в программу. По сути, операционная система начнёт выполнять ваш код с этой точки. А фактическое определение функции находится в строке 4.
Функция выполняет две вещи: строки 5–9 выводят сообщение на экран, а строки 11–13 завершают программу. На самом деле вы можете удалить строки 11–13, и программа выведет строку «Hello, World» и завершится, но это не будет чистым выходом — она просто завершится с ошибкой, пытаясь выполнить некоторую случайную недопустимую операцию, которая окажется следующей в памяти.
Печать на экран происходит с помощью «системного вызова», по другому системной функции. «Системный вызов» — это функция операционной системы. Мы вызываем такую функцию write()
, которую мы указываем, загружая значение 4 в регистр процессора с именем r7
(строка 8), а затем выполняем инструкцию swi $=0
(строка 9), которая переходит прямо в ядро Linux, на котором работает Android.
Параметры для системного вызова передаются через другие регистры: r0
указывает номер дескриптора файла, который мы хотим напечатать. Мы помещаем в него значение 1 (строка 5), которое указывает стандартный вывод (stdout
) - вывод на экран
То, куда попадают данные, записанные в стандартный поток вывода, зависит от того, как была запущена программа. Если запустить программу простейшим образом из эмулятора терминала, данные выводятся на экран. В других случаях они могут, сохраняться в файл на диске или передаваться по сети.
Регистр r1
указывает на адрес памяти данных, которые мы хотим записать, поэтому мы загружаем туда адрес строки «Hello, World» (строка 6), а регистр r2
сообщает, сколько байтов мы хотим записать. Мы установили для него значение message_len
(строка 7), которое вычисляется в строке 18 с использованием специального синтаксиса: символ точки обозначает текущий адрес памяти, поэтому . - message
означает текущий адрес памяти минус адрес message
. Поскольку мы определяем message_len
сразу после message
, это вычисляется как длина message
.
Вообщем то, код в строках 5–9 эквивалентен коду на С:
#define message "Hello, Worldn"
write(1, message, strlen(message));
Завершение программы — нам просто нужно загрузить код выхода в регистр r0
(строка 11), затем мы загружаем значение 1, которое является номером вызова системной функции exit(), в r7
(строка 12), и снова вызываем ядро (строка 13).
Вы можете найти полный список системных вызовов Android и их номеров в исходном коде операционной системы. Если Вам интересно можно посмотреть реализацию функций write() и exit(), которые вызывают соответствующие системные функции.
Для компиляции любой подобной ассемблерной программы вам понадобится Android NDK (Native Development Kit), содержащий набор компиляторов и инструментов сборки для платформы ARM. Его всегда можно скачать с официального сайта или установить через Android Studio:
Подключение NDK
После установки NDK вам нужно найти файл под названием arm-linux-androideabi-as, это ассемблер для платформы ARM. Если же Вы загрузили всё через Android Studio, найдите этот файл в папке Android SDK. К примеру у меня он находился здесь (относительно SDK):
ndk-bundletoolchainsarm-linux-androideabi-4.9prebuiltwindows-x86_64bin
После нахождения файла с ассемблер, сохраните свой исходный код в файл с именем hello.s (s — это стандартное расширение для файлов ассемблера в системах GNU). Затем выполните исполните следующую команду, чтобы преобразовать его в машинный код:
arm-linux-androideabi-as -o hello.o hello.s
Это создаст объектный ELF-файл с именем hello.o. Для его преобразования в двоичный файл, который может работать на устройстве андройд, требуется последний шаг — вызов компоновщика:
arm-linux-androideabi-ld -o hello hello.o
Теперь всё! Теперь у нас есть файл hello, содержащий вашу программу, готовый к запуску.
Мобильные приложения для Android обычно распространяются в формате APK. Это специальный тип ZIP-файла, который должен создается определённым образом, и включает классы Java (можно просто писать части своего приложения, используя собственный код C / C ++, но точкой входа по-прежнему должен быть Java).
Чтоб было прощемы избежим этой сложности при запуске нашего приложения и будем использовать adb, чтобы скопировать его в TEMP папку устройства Android, а затем adb shell
, чтобы запустить мобильное приложение и увидеть результат:
adb push hello /data/local/tmp/hello
adb shell chmod +x /data/local/tmp/hello
И запуск приложения:
adb shell /data/local/tmp/hello
Теперь у вас есть рабочее окружение, похожее на то, что было у Анастасии. Она провела несколько дней, изучая ARM-ассемблер, и придумала простой проект, который она хотела реализовать: Игру где игроки поочерёдно считают, и всякий раз, когда число делится на 7 или содержит цифру 7, они должны сказать «бум».
Окончание этой игры было довольно сложной задачей, так как Анастасия написала метод, который выводил числа на экран по одной цифре за раз — поскольку идея заключалась в том, чтобы писать всё с нуля, используя ассемблерный код и без вызова стандартных функций библиотеки языка С. Нескольких дней заморочек и тяжёлой работы она завершила проект!
Написание ассемблерного кода для устройства Android — это отличный способ познакомиться с архитектурой ARM и лучше понять внутреннюю работу устройства, которое вы используете ежедневно. Советую вам пойти дальше и разработать небольшое мобильное приложение на ассемблере для вашего телефона на Android. Это очень интересно и даст полное погружение в процесс.