Как мы писали Android-приложение на ассемблере

Обложка поста

Перевод статьи «Writing Your First Android App, in Assembly»

Рассказывает Uri Shaked — эксперт-разработчик в Google

В этой статье я собираюсь поделиться нестандартным подходом к разработке приложений для Android. Стандартный подход заключается в установке Android Studio и создании простого приложения «Hello World» на языках Java или Kotlin. Но это можно сделать и по-другому, как вы вскоре увидите. Но сначала небольшая предыстория.

Как работает мой телефон на Android?

Однажды вечером моя знакомая по имени Ариэлла спросила: «Как работает мой смартфон? Что внутри? Как электричество, единицы и нули заставляют всё это работать?»

Ариэлле не было чуждо программирование. Более того, она реализовала несколько проектов для Arduino, которые включали программирование и электронику. Возможно, именно это заставило её жаждать больше знаний. К счастью, один из курсов информатики в университете был посвящён именно этому — или, по крайней мере, дал мне достаточно опыта, чтобы найти хороший ответ.

Следующие пару недель мы провели, разбирая основны — как микроскопические примеси в кремниевой решётке изменяют свои свойства, превращая их в полупроводники, и как можно управлять потоком электронов через эти полупроводники, образуя транзисторы. Затем мы перешли на уровень выше, и я показал ей, как можно построить логические вентили, такие как NAND (логическое И-НЕ) и NOR (логическое ИЛИ-НЕ), комбинируя транзисторы особым образом.

Мы продолжали исследовать логические элементы различного типа и объединять их для выполнения вычислений (таких как добавление двух двоичных чисел) и ячеек памяти (триггеров). Когда у нас всё получилось, мы разработали простой воображаемый процессор, который содержал два регистра общего назначения и пару простых инструкций (например, добавление двух регистров), и написали простую программу, которая умножит эти два числа.

Если вы хотите ознакомиться с вышеизложенным поближе, прочтите руководство по 8-битному компьютеру с нуля — оно объясняет почти всё, начиная с основ. Хотел бы я знать об этом тогда!

Hello, Android!

В этот момент я почувствовал, что у неё уже достаточно опыта, чтобы понять, как работает процессор её смартфона. У неё был 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, World\n"
  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, World\n"
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-bundle\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin

После того как вы нашли ассемблер, сохраните свой исходный код в файл с именем 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, чтобы скопировать его во временную папку её устройства 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-ассемблер, и придумала простой проект, который она хотела реализовать: игра Seven-Boom. Seven-Boom — это израильская разновидность Fizz Buzz. Игроки поочерёдно считают, и всякий раз, когда число делится на 7 или содержит цифру 7, они должны сказать «бум» (Boom).

Завершение этой игры было довольно сложной задачей, так как Ариэлле пришлось написать метод, который выводил бы числа на экран по одной цифре за раз — поскольку идея заключалась в том, чтобы писать всё с нуля, используя ассемблерный код и без вызова каких-либо стандартных функций библиотеки С. Но после нескольких дней возни и тяжёлой работы она сделала это!

Вы можете найти её первую программу для Android здесь. Некоторые из идентификаторов в коде на самом деле являются еврейскими словами (например, _sifra_ahrona, что означает «последние цифры» — она нашла это более забавным, а я нашёл это милым).

Написание ассемблерного кода для вашего устройства Android — это отличный способ познакомиться с архитектурой ARM и лучше понять внутреннюю работу устройства, которое вы используете ежедневно. Я призываю вас пойти дальше и написать небольшую программу на ассемблере для вашего телефона на Android. Это может быть простая игра «Быки и коровы», клон «Палача» или что угодно, что вам нравится.