
Это не шутка и не преувеличение. Существует реальная возможность создать нативную программу для ARM, которая будет запускаться на нескольких операционных системах без необходимости пересборки под каждую платформу. Всё это достигается с помощью одного хитрого приёма.
Я задался целью: можно ли написать программу, которая будет работать на четырёх разных ОС для ARM без перекомпиляции и адаптации под различные ABI (Application Binary Interface). Мотивацией было создание кроссплатформенных исполняемых файлов (ELF) для старых мобильных телефонов, чтобы портировать на них эмуляторы ретро-консолей. Изучив документацию по форматам исполняемых файлов, я пришёл к выводу, что это возможно, и успешно реализовал такой проект на практике, без использования виртуальных машин. Приглашаю всех интересующихся техническими деталями ознакомиться с результатами.
Исторический контекст и мотивация
Всё началось с легендарного японского телефона Sony CMD-J70 2001 года. Ещё до партнёрства с Ericsson, Sony выпускала устройства, привлекавшие внимание моддеров. Со временем для этого телефона был создан бинлоадер (PRGLoader), позволявший запускать произвольные программы на ассемблере.

Для того времени это было революционно. Большинство телефонов поддерживали только ограниченные Java/Mophun-приложения, а здесь открывался доступ к глубокой модификации прошивки: создание светомузыки, кастомных игр, обоев — функции, доступные лишь на дорогих смартфонах с Symbian или Windows Mobile.

Позже, с появлением платформы S-Gold на базе ядра ARM926EJ-S в телефонах Siemens, энтузиасты взломали алгоритм BootKEY и создали полноценный эльфлоадер. В отличие от простого бинлоадера, он позволял загружать программы на языке C, скомпилированные обычным компилятором, и предоставлял вытесняющую многозадачность.

На фотографии выше — результат этих усилий: нативный клиент почты, мессенджер NatICQ, порты эмуляторов. Программы можно было сворачивать, работая параллельно в браузере или файловом менеджере. Одним из активных разработчиков был хабраюзер @ilya_ZX.

Не отставали и энтузиасты Motorola. После выхода популярного телефона E398 в 2004 году, на форуме MotoFan нашли уязвимость в загрузчике, обошли RSA-верификацию прошивок и создали кастомные прошивки («монстрпаки»). Позже был реализован эльфлоадер EP1, открывший путь для нативного софта.

Может показаться, что это устаревшие технологии, но сообщество моддеров для этих устройств живо до сих пор. Разработчики вроде @EXL и @Azq2 продолжают создавать новый софт и эмуляторы, исследуя аппаратную часть.

Однако написание «эльфов» сопряжено с трудностями: отсутствие отладки, сложное API прошивки, риск зависания устройства. Это навело на мысль: можно ли создать универсальный эльфлоадер, который скроет аппаратные детали и позволит запускать один бинарный файл на разных платформах? Изучив ABI ARM и спецификацию ELF, я приступил к дизассемблированию тестовых программ и поиску решения.

Основы: формат ELF, ABI ARM и инструментарий
ELF (Executable and Linkable Format) — это распространённый формат исполняемых файлов, используемый в Unix-системах и embedded-устройствах. По сути, это аналог .exe (PE) в Windows. Формат гибок и поддерживает множество архитектур.

Программа состоит из секций — участков кода и данных. Основные из них:
- .text: исполняемый код программы.
- .data: инициализированные данные (например, массивы).
- .bss: неинициализированные данные (глобальные переменные, обнуляемые при старте).
- .rodata: константные данные (строки, константные массивы).
Загрузчик ОС размещает эти секции в памяти. Однако для библиотек (.so) адрес загрузки неизвестен заранее, поэтому используется позиционно-независимый код (PIC/PIE).
Существует три основных подхода к реализации PIC в ARM:
- Глобальная таблица смещений (GOT) с релокациями: код остаётся независимым, а адреса в специальной таблице (GOT) исправляются («релоцируются») динамическим линкером при загрузке. Это распространённый метод.

- Релокации для абсолютных адресов: программа компилируется так, будто она загружается по адресу 0x0, но линкер генерирует информацию для исправления всех абсолютных адресов (релокации типа R_ARM_ABS32). Это увеличивает количество релокаций, но может немного ускорить выполнение.

На изображении — пример релокаций для эмулятора NES.
- Использование регистра R9: код компилируется с флагами /rwpi и /ropi. Базовый адрес программы хранится в регистре R9, который инициализируется загрузчиком. Этот подход часто используется во встраиваемых системах без MMU.

В старых телефонах MMU часто не используется, поэтому эльфлоадеры загружают программы по выделенному адресу и используют PIC, чаще всего через релокации.
Для своего проекта я выбрал второй подход (релокации абсолютных адресов) и решил построить загрузчик поверх существующих, абстрагируя API прошивок в стандартизированные функции для работы с дисплеем, вводом, файлами и звуком. Цель — компиляция программ современным clang с поддержкой C99 для лёгкого портирования кода.

После экспериментов с компилятором был написан скрипт линкера, объединяющий все секции в одну (.text) для упрощения загрузки, и набор флагов для компиляции.
Затем я приступил к написанию самого эльфлоадера. Его задача:
- Верифицировать заголовок ELF.
- Проанализировать таблицы заголовков и загрузить секции в память.
- Найти таблицы символов и строк.
- Найти точку входа (ElfMain) и исправить адреса импортированных функций.
Для импорта функций я использовал специальный макрос, создающий указатели на функции с префиксом SYS_. Загрузчик находит эти символы и заменяет указатели на реальные адреса функций из рантайма, что позволяет избежать промежуточных функций (thunk) и даёт оптимизатору больше свободы.
Отладка велась с помощью QEMU и GDB, а также непосредственно на смартфоне с Windows Mobile, который стал первой целевой платформой.

Портирование на Windows Mobile (CE)
Благодаря схожести WinAPI в CE с десктопной версией, портирование прошло относительно гладко. Основной вопрос заключался в использовании стандартной библиотеки C (stdlib). Поскольку на других целевых платформах её реализация могла быть ограничена, я решил пробрасывать из хост-системы только базовые функции, вроде malloc и free, а более сложную логику реализовывать самостоятельно.
Для работы с графикой я отказался от использования платформозависимых функций отрисовки и решил работать напрямую с фреймбуфером. Это дало универсальность, хотя в некоторых телефонах (например, с GPU от ATI/Nvidia) аппаратное ускорение могло бы дать выигрыш в скорости.
Аналогично, для вывода текста были использованы битмапные шрифты, статически встроенные в загрузчик, что обеспечило портативность и независимость от системных рендереров.
После отладки была написана тестовая программа, выводящая изображение и текст, и успешно запущенная на Windows Mobile.

Следующим шагом стала портирование на более экзотическую платформу.
Портирование на MRP/MRE
Платформы MRP и MRE использовались в огромном количестве бюджетных китайских телефонов (Nokla TV, Fly, Explay, DEXP) и даже в некоторых оригинальных Nokia на S30+. Хотя эти устройства считались закрытыми, для многих существовал костыль для запуска нативных программ.

Эти платформы также используют позиционно-независимый код, но через регистр R9. Изначально мой загрузчик использовал тот же подход, но из-за event-based архитектуры MMI (где нельзя просто сделать бесконечный цикл) пришлось перейти на использование релокаций и реализовать проброс таймеров.

Отладка в таких условиях — задача не для слабонервных: ошибка часто приводит к перезагрузке устройства.
Адаптация функций работы с дисплеем и вводом для MRP/MRE оказалась straightforward, и вскоре тестовая программа запустилась уже на второй, совершенно отличной ОС.

Тест на сложность: портирование эмулятора NES
Чтобы доказать практическую пользу загрузчика, я решил портировать не «Hello, world», а полноценный эмулятор NES. Цель — возможность запуска игр и эмуляторов на старых кнопочных телефонах.
Был взят за основу быстрый эмулятор NES, отвязанный от конкретной платформы и превращённый в библиотеку с простым API (инициализация, загрузка ROM, эмуляция кадра).
Базовый порт для эльфлоадера заключался в инициализации дисплея, загрузке ROM, и организации цикла эмуляции (либо через таймер, либо обычный цикл, в зависимости от возможностей платформы).
Результат превзошёл ожидания: эмулятор успешно запустился и работал на разных платформах.



Итоги и перспективы
Таким образом, можно создать программу, которая работает на трёх различных операционных системах, не имеющих между собой почти ничего общего. Ключ — в глубоком понимании формата исполняемых файлов, ABI целевой архитектуры и работы компилятора.
Если вам интересна тема ремонта, моддинга и программирования для старых устройств — добро пожаловать в мой Telegram-канал «Клуб фанатов балдежа» и на YouTube.
Важно! Для подготовки будущих материалов ищутся старые устройства: телефоны-консоли с джойстиком (часто с предустановленными эмуляторами), подделки на брендовые смартфоны (2010-2014), первые смартфоны Xiaomi/Meizu, а также телефоны на Linux (Motorola серии ROKR, ZINE). Если у вас есть такие устройства и вы готовы их передать — пишите в Telegram (@monobogdan) или в комментарии. Спасибо всем за поддержку!


[моё]СмартфонТелефонПокупкаГаджетыПрограммированиеМоддингЭльфыLinuxUnixОперационная системаРеверс-инжинирингSiemensМоторолаНостальгияДлиннопост 109Больше интересных статей здесь: Гаджеты.
Источник статьи: Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции.