[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

Нет, это не шутка и не кликбейт, такое действительно возможно! Недавно я задался вопросом: а можно ли написать для ARM программу на C, которая будет бесшовно работать сразу на 4-х операционных системах без необходимости перекомпиляции для разных платформ и ABI? Моей целью была возможность писать кроссплатформенные эльфы для мобильных телефонов из нулевых и попытаться портировать на них эмуляторы ретро-консолей. Погрузившись в документацию на исполняемые форматы, я пришёл к выводу, что да — это возможно и смог реализовать такую программу на практике без виртуальных машин!

❯ Зачем и почему?

Давным-давно, в далёком 2001 году, мир увидел легендарный японский телефон — Sony CMD-J70. Ещё до создания совместного подразделения с Ericsson, Sony выпускала достаточно занимательные девайсы, которые привлекали внимание не только рядовых пользователей, но и моддеров всех мастей. Уже через пару лет после выхода, телефон реверсили и моддили все кому не лень: кто-то менял графику, кто-то писал патчи, а со временем написали даже PRGLoader — загрузчик внешних «экзешников», позволявший запускать на телефоне произвольный софт, написанный на ассемблере!

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

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

Легенды мира моддинга телефонов
Легенды мира моддинга телефонов

Недавно мы с вами вспоминали о легендарном Siemens M55 и узнали, что у него находится под капотом. Несмотря на диковинную архитектуру Infineon C166, даже под этот телефон разрабатывались патчи и была написана как минимум одна кастомная игра. Но рассвет моддинг-сцены Siemens произошёл с выходом платформы S-Gold на базе стандартного ядра ARM926EJ-S, когда в ~2004 году энтузиасты полностью взломали алгоритм генерации BootKEY для загрузчика, а затем в 2006 году реализовали полноценный эльфлоадер, который позволял загружать программы написанные на C и скомпилированные обычным компилятором ADS. В отличии от бинлоадера для CMD-J70, «эльфятник» позволял угонять функции RTOS для создания потоков и привносил в бюджетные телефоны полноценную вытесняющую многозадачность с настоящим диспетчером задач и возможностью запуска несколько программ одновременно:

Единицы читателей поймут, что происходит на данной фотографии...
Единицы читателей поймут, что происходит на данной фотографии...

Энтузиасты раскапывали прошивку в дизассемблере, изучали её и пытались понять как работают различные её подсистемы. Результатом стало появление нативного клиента почты с предком пуш-уведомлений, аськи (NatICQ), порты самых разных эмуляторов ретро-консолей и даже полная программная поддержка MP3 в тех телефонах, где её отродясь не было! И представьте себе, почти все эти программы можно было свернуть и продолжить работу в браузере или, например, Card Explorer'е!

Пример патча, который лежит в основе возможности проигрывания MP3 на телефонах Siemens без поддержки этого формата
Пример патча, который лежит в основе возможности проигрывания MP3 на телефонах Siemens без поддержки этого формата

Но если вы думаете что одними телефонами Siemens энтузиасты были едины, то вы ошибаетесь — ведь круче были только «моторолки»! В 2004-году, недорогая Motorola E398 с двумя громкими динамиками, светомузыкой и поддержкой MicroSD-флэшек, стала настоящим бестселлером и привлекла к себе не меньше энтузиастов, чем Siemens. Ребята сплотились на форуме MotoFan, нашли уязвимость в загрузчике и хакнули верификацию RSA-подписи у прошивок, позволив не только модифицировать Seem'ы (что-то типа EEPROM), но и создавать для телефона кастомные прошивки — монстрпаки, которые прибавляли громкость и без того не самым тихим динамикам и в различных аспектах изменяли главное меню устройства. Со временем, @Andy51 и ещё несколько энтузиастов реализовали эльфлоадер (EP1) для E398, раскопали прошивку и написали много полезного софта, время от времени переключаясь на Linux-телефоны от Motorola...

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

Вероятно многие читатели подумают мол «было и было, мой айфон/сяоми может запускать любой произвольный софт и эти ухищрения давным-давно неактуальны...». Но как бы не так: про моторолки и сименсы не просто так всё чаще вспоминают, у них до сих пор есть активное моддерское коммьюнити, которое продолжает пилить для них кастомный софт и далее колупать прошивку. Всё тот же @EXL портировал крутой софтрендер для E398 и в 2025 году наконец-то взломал C350, @Azq2 пилит аппаратный эмулятор Infineon S-Gold, и многие другие гики делают свой вклад в моддинг сцену уже не таких мейнстримных, но отнюдь не устаревших устройств!

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

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

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

❯ Формат ELF, ABI ARM и тулчейн

Начнём с самого простого: что же такое эти самые эльфы? Elf — формат исполняемых файлов, широко применяемый как в мире Unix-систем, так и в embedded-устройствах. Самые распространенные тулчейны — GCC и clang/llvm, по умолчанию собирают программы именно в этом формате и по своей сути, это прямой аналог .exe (PE) файлов из Windows. Помимо кода, Elf также содержит в себе множество секций и различных данных, при этом разработчики формата старались сделать его настолько гибким, чтобы его можно было использовать на любых архитектурах: от x86, до risc-v.

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

Каждая программа состоит из так называемых секций — участков кода, данных и метаданных, необходимых для её загрузки в память. Среди секций простой программы можно выделить как минимум четыре основных:

.text — хранит в себе код программы и обычно записывается в память с флагами MMU R X (чтение и выполнение)

.data — преинициализированные данные, имеет флаги R W (чтение и запись). Например, заполненная структура в C:

int a[] = { 1, 2, 3 };

.bss — не инициализированные данные, иными словами глобальные переменные, которые при старте программы должны быть забиты нулями. Имеет те же флаги, что и .data.

.rodata — различные константы: строковые, const-преинициализированные массивы, а также структуры и т.п., имеет только флаг R и на системах с MMU попытка запись в эту секцию повлечет SIGSEGV.

За загрузку всех этих секций отвечает загрузчик Elf в ядре ОС. Однако это справедливо только для простых программ, которые загружаются в фиксированный адрес виртуальной памяти и которые не используют внешние библиотеки (.so, аналог в Windows — .dll). Поскольку адрес загрузки для всех библиотек предсказать невозможно, разработчики ABI придумали позиционно-независимый код (PIC и его производное — PIE), который может загружаться в любую область памяти и оттуда выполняться.

Реализация PIC может достигаться тремя разными способами:

  • Первый способ заключается в использовании глобальной таблицы смещений (GOT) и релокаций. Релокации — специальные данные в Elf, которые позволяют переместить программу в другой адрес путём патчинга адресов в секции .got «на лету»: иными словами, сам код (.text) остаётся позиционно-независимым (дабы библиотеку можно было загрузить один раз и использовать во множестве процессов) и обращается к GOT относительно PC, но в самом GOT (который представляет из себя массив void* addresses[]) указатели на остальные сегменты находятся так, будто программа загружается по смещению 0x0. Задача динамического линкера — посчитать абсолютный адрес для всех указателей в GOT: в простейшем случае, это got[address] += baseAddress.

    Релокации могут затрагивать сразу literal pools в обход GOT, если архитектура предусматривает их наличие.

Пример таблицы релокаций в программе BattDump для Motorola
Пример таблицы релокаций в программе BattDump для Motorola
  • Релокацией занимается динамический линкер также именуемый как интерпретатор в мире Unix (тот самый ld.so, что часто «not found» :) ), а самих релокаций есть много разных видов в зависимости от архитектуры процессора. В ARM чаще всего встречается R_ARM_REL32
  • Второй способ заключается в том, что мы компилируем программу так, будто она должна загружаться по фиксированному адресу 0x0 — то есть без PIC, однако просим линкер (--emit-relocs) создать информацию о всех обращениях к памяти в виде всё тех же релокаций. Вместо R_ARM_REL32, линкер создаёт релокации R_ARM_ABS32, которые можно разрешить обычным сложением.

    С таким подходом количество релокаций кратно увеличивается, однако из-за отсутствия GOT немного повышается быстродействие программы (вместо трёх LDR для загрузки слова из памяти нужно всего два: из Literal pool в регистр и затем из фактической памяти).
[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции
  • Третий способ поддерживается не везде, но в ARM он является одним из самых распространенных в embedded-среде: код собирается с флагами /rwpi и /ropi полностью не зависит ни от GOT, ни имеет каких либо релокаций. Вместо этого, для адресации базового адреса программы он использует выделенный регистр R9, который загрузчик должен заполнить адресом, куда он загрузил программу (mov r9, textSectionBase). Такой подход теоретически чуточку быстрее, чем GOT, но медленнее второго подхода из-за необходимости добавлять сложение регистра с PC перед каждым фетчем из памяти, а также сохранения/восстановления контекста при вызове API-функций.
[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

Поскольку в телефонах MMU обычно не используется, эльфлоадеры загружают программы по тому адресу, что им выделяет системный аллокатор памяти и вынуждены использовать PIC. Чаще всего используются релокации (как минимум на Siemens и Motorola), на некоторых платформах используется второй подход с использованием регистра R9.

Для большей гибкости, я решил выбрать второй подход и построить свой эльфлоадер поверх уже существующих загрузчиков, обернув API прошивок в ряд собственных стандартизированных функций: работа с дисплеем, вводом, файлами, а также звуком. При этом эльфы должны собираться современным компилятором clang с поддержкой C99, чтобы была возможность легко портировать современные single-header программы по типу эмуляторов, да и в целом не писать код на манер Ansi C, когда переменную нигде нельзя объявить кроме начала блока.

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

Далее я сутками игрался с компиляторами и пытался заставить выдать их подходящий для моих целей код и по итогу написал скрипт для линкера, который для простоты загрузки файла объединяет все секции в один .text (таким образом остаётся всего один Program Header):

OUTPUT_FORMAT("elf32-littlearm") SECTIONS { . = 0x0; .text : { *(.r9ptr) *(.text*) *(.data*) *(.bss*) *(.rodata*) *(.functions) } .rel : { *(.rel*) } /DISCARD/ : { *(.ARM.*) } }

И следующий набор опций для компилятора, который устанавливает архитектуру и целевой процессор, ABI для FPU, включает генерацию релокаций и отключает выравнивание в линкере для выходного файла (иначе файлы забиты нулями и весят целых 64Кб:

CLANGFLAGS = -mno-unaligned-access -O3 -ffast-math -ffixed-r9 -T ld.script -target armv5e-none-eabi -nostartfiles -fno-exceptions -fno-rtti -mfloat-abi=soft -I$(ELFROOT) -Ilibnesemu/ LDDFLAGS = -Wl,-zmax-page-size=1,--emit-relocs

Когда компилятор наконец-то начал выдавать корректный код, я принялся писать сам эльфлоадер. За качество кода и отсутствие нормальной структуры не ругайте — это эмбеддед, тут можно ;))

На входе лоадеру поступает адрес загруженного в память эльфа и его длина. Задача эльфятника — верифицировать заголовок и убедится что он собран с подходящими параметрами:

// Read and verify ELF header Elf32_Ehdr* hdr = (Elf32_Ehdr*)data; PRINT("Loading ELF..."); if(hdr->e_machine != EM_ARM) { PRINT("Not an EM_ARM executable"); return 0; } if(hdr->e_ident[EI_DATA] != PLATFORM_ELF_ENDIANESS) { PRINT("Endianess mismatch"); return 0; // Wrong endianess }

Проанализировать таблицу заголовков с служебной информацией о том, что находится по тому или иному смещению в файле: загружаемая секция, таблица символов или строк, а затем загрузить все секции в участок памяти, который нам выдал аллокатор. На MMU-системах адрес должен быть выровнен по размеру страницы, иначе система не даст выдать страницам флаг EXEC!

ret = (ExecInfo*)ExecAlloc(sizeof(ExecInfo)); sections = (Elf32_Phdr*)(&data[hdr->e_phoff]); sh = (Elf32_Shdr*)&data[hdr->e_shoff]; symSectionIndex = hdr->e_shstrndx; codeSize = 0x0; PRINT("Processing program headers"); // Process program headers and determine total size for(i = 0; i < hdr->e_phnum; i++) { Elf32_Phdr hdr = sections[i]; if(hdr.p_type == PT_LOAD) { if(hdr.p_offset == 0x0) continue; codeSize += hdr.p_memsz; } } PRINT("Allocating memory for .text"); textSection = (char*)ExecAlloc(codeSize); textOffset = textSection; ret->CodeSection = textSection; if(!textSection) { free(ret); PRINT("Failed to allocate .text section"); return 0; }

Далее найти секцию с таблицей символов и с строками, где содержатся имена символов:

PRINT("Analyzing section table"); for(i = 0; i < hdr->e_shnum; i++) { Elf32_Shdr sec = sh[i]; if(sec.sh_type == SHT_STRTAB && i != hdr->e_shstrndx && strTable == 0) { strTable = &data[sec.sh_offset]; PRINT("Found string table"); } if(sec.sh_type == SHT_SYMTAB) { PRINT("Found symbol table"); symbols = (Elf32_Sym*)&data[sec.sh_offset]; symNum = sec.sh_size / sizeof(Elf32_Sym); } if(sec.sh_type == SHT_REL && relocs == 0) { UtilPrint("Found relocations"); relocs = (Elf32_Rel*)&data[sec.sh_offset]; relNum = sec.sh_size / sizeof(Elf32_Rel); } if(sec.sh_type == SHT_RELA) { PRINT("Found unsupported relocation types"); return 0; } } if(!strTable || !symbols) { free(ret); PRINT(".strtab or .symtab not found"); return 0; }

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

PRINT("Relocation fix-up"); for(i = 0; i < relNum; i++) { Elf32_Rel rel = relocs[i]; int sym = ELF32_R_SYM(rel.r_info); switch(ELF32_R_TYPE(rel.r_info)) { case R_ARM_ABS32: *((unsigned int*)&textSection[rel.r_offset]) += (unsigned int)textSection; break; case R_ARM_JUMP24: break; case R_ARM_CALL: break; default: PRINT("Unsupported relocation type"); } } PRINT("Patching import table"); // Analyze symbol table and patch all imported function pointers to real counterparts for(i = 0; i < symNum; i++) { Elf32_Sym sym = symbols[i]; uint8_t* symName = &strTable[sym.st_name]; int symType = ELF32_ST_TYPE(sym.st_info); if(symType == STT_OBJECT && strstr((const char*)symName, "SYS_")) { int funcNumber = ExecFindFunction(symName); if(funcNumber == -1) { PRINT("Failed to import function: "); UtilPrint((char*)symName); PRINT(""); continue; } //drawDebug(FuncExportTable[funcNumber].Pointer == 0 ? "Not OK" : "OK"); *((unsigned int*)&textSection[sym.st_value]) = (unsigned int)FuncExportTable[funcNumber].Pointer; } if(symType == STT_FUNC && strstr((const char*)symName, "ElfMain")) { PRINT("ElfMain function is found"); ret->Main = (ExecMainFunction)&textSection[sym.st_value]; }

В Elf уже есть механизм импорта функций из сторонних библиотек, который называется Platform Linkage Table. Для импорта функций прошивки, эльфлоадер Siemens использует инструкцию для вызова программного прерывания - SWI (ближайший аналог - int 21h, int 10h из x86), Motorola же патчит thunk-функции на лету, которые сами вызывают настоящую функцию:

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

А я решил поступить несколько изящнее. В моем эльфятнике, функции импортируются с помощью специального макроса, который создаёт переменную-указатель на функцию, который изначально располагается в секции .functions. При этом с помощью ключевого слова asm, символу присваивается иное имя — с префиксом SYS_, которое означает то, что загрузчик эльфа должен пропатчить адреса функций на реальные (которые предварительно зарегистрированы в рантайме) в процессе загрузки программ и таким образом, избежать thunk-функций и позволить оптимизатору легко выкидывать указатели на неиспользуемые функции:

#ifndef LOADER #define IMPORT(name, ret, ...) __attribute__ ((section(".functions"))) ret (* name )( __VA_ARGS__ ) asm( "SYS_" #name ) #define IMPORTNOARGS(name, ret) __attribute__ ((section(".functions"))) ret (* name )() asm( "SYS_" #name ) #else #define IMPORT(name, ret, ...) ret name( __VA_ARGS__ ) #define IMPORTNOARGS(name, ret) ret name() #endif

Лучший способ отладить эльфлоадер — в QEMU с GDB под Linux. Однако я решил время не терять и отлаживал его сразу на настоящем смартфоне с Windows Mobile. А раз WM стал первой поддерживаемой платформой — на нем мы с вами и реализуем рантайм.

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

Портируем на Windows Mobile (CE)

Поскольку всю жизнь я сижу в основном на Windows, а WinAPI в CE практически полностью копирует десктопную версию, никаких проблем с портированием рантайма не возникло. Единственный выбор который передо мной встал: стоит ли прокидывать stdlib из хост-системы в «эльфятник», или же воспользоваться реализацией newlib в clang/gcc. В процессе портирования на другие платформы выяснилось, что нормально libc реализован, по сути, только на Windows, во все остальных реализациях были лишь самые основные функции по типу malloc, free, memcpy, strcmp и т.п. Поэтому я решил не городить велосипеды и прокинул из хост-системы лишь аллокатор - т.е. malloc и free:

// stdlib IMPORT(elf_malloc, void*, int size); IMPORT(elf_free, void, void* ptr); /*IMPORT(elf_strcmp, int, char* str1, char* str2); IMPORT(elf_strcpy, char*, char* dst, char* src); IMPORT(elf_strlen, int, char* str); IMPORT(elf_strstr, char*, char* string, char* substring); IMPORTNOARGS(elf_rand, int); IMPORT(elf_memcpy, void*, void* dst, const void* src, uint32_t length); IMPORT(elf_memset, void*, void* dst, int what, uint32_t length); IMPORT(elf_memmove, void*, void* dst, void* src, uint32_t length);*/

Далее я сразу решил, что платформозависимые функции для работы с дисплеем использовать не буду и из хост-системы мне нужен будет лишь указатель на фреймбуфер, а блиттинг, рисование текста и прочие операции я реализую сам. На первый взгляд может показаться что это единственное верное решение, однако на практике в некоторых телефонах (Motorola E398, Razr V3) активно использовались 2D GPU от ATI и Nvidia, которые рисуют (BitBLT) изображение значительно быстрее любой программной реализации.

Ниже представлена черновая реализация без преобразования пиксельформатов (поскольку на подавляющем числе телефонов использовался 565) и поддержки прозрачности через колоркей. Её можно оптимизировать до быстрого копирования по сканлайнам через memcpy:

for(i = 0; i < bitmap->Height; i++) { for(j = 0; j < bitmap->Width; j++) { LCD_PLOT_565(clamp(x + j, 0, lcd->Width), clamp(y + i, 0, lcd->Height), bmp[i * bitmap->Width + j]); } }

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

__inline int LcdDrawChar(LcdInfo* lcd, char chr, uint32_t x, uint32_t y, uint16_t color) { if(x >= 0 && y >= 0 && x + FONT_WIDTH < lcd->Width && y + FONT_HEIGHT < lcd->Height) { int i, j; unsigned char* glyph = &embedded_font[chr * 8]; for(i = 0; i < FONT_HEIGHT; i++) { short* fb = &((short*)lcd->Pixels)[(y + i) * lcd->Width + x]; for(j = 0; j < FONT_WIDTH; j++) { if((*glyph >> (FONT_WIDTH - j)) & 0x1) *fb = color; fb++; } glyph++; } return true; } return false; } void LcdDrawString(LcdInfo* lcd, char* str, uint32_t x, uint32_t y, uint16_t color) { SWITCH_CONTEXT; if(lcd && x >= 0 && y >= 0) { unsigned int i; for(i = 0; i < strlen(str); i++) { if(!LcdDrawChar(lcd, str[i], x, y, color)) return; // Out of screen x += FONT_WIDTH; } } END_CONTEXT; }

Отладив эльфлоадер, я написал небольшую тестовую программу для вывода картинки и текста

#include <system.h> int ElfMain(void* ptr) { LcdInfo* lcd = lcdInit(); lcdDrawBitmap(lcd, bitmap, 0, 0); lcdDrawString(lcd, "Test", 0, 0, COLOR_BLUE) return 100; }

И получил следующий результат:

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

Ой, тут endianess пикселей (которые хранятся как halfword) сбился, давайте попробуем ещё раз:

Privet
Privet

Вот теперь всё работает! Пришло время портировать эльфятник на весьма необычную платформу, о потенциалах моддинга которой знают единицы...

❯ Портируем на MRP/MRE

И имя этой платформе, вернее даже двумя платформам — MRP и WRE. Эти платформы использовались на бюджетных китайских телефонах с 2007 по 2016 год, и иногда встречаются и по сей день. Встретить их можно было везде: легендарная Nokla TV E71/E72, клоны 6700, бюджетные телефоны Fly/Explay/DEXP и даже в оригинальных телефонах Nokia на платформе S30+ (например 230)!

Легендарная «нокла»!
Легендарная «нокла»!

И хотя люди часто считали такие устройства бесполезными в плане установки сторонних приложений, многие ранние «нонейм»-телефоны поддерживали запуск нативных программ через небольшой костыль — установку специального «загрузчика» dsm_gm.mrp и ввод комбинации *#220807# в номеронабиратель. Конечно, знали об этом костыле единицы и в 2010 году MediaTek решила сделать свою платформу под названием MRE (MAUI Runtime Environment), приложения для которой можно было запускать прямо из проводника без установки! SDK для обеих платформ сейчас свободно лежит в сети.

Обе платформы, по сути, занимаются тем же самым, чем и мой эльфятник — прокидывают нативные функции MMI (оболочка телефона) в приложения и для загрузки позиционно-независимых программ используют третий подход с регистром R9, который обязательно необходимо где-то хранить и восстанавливать. Изначально мой эльфятник использовал такой же подход, из-за чего я написал отдельный костыль для «свичинга» контекстов, причем восстановление R9 я делал в отдельной функции из-за бага ассемблера в IAR:

#define SWITCH_CONTEXT unsigned int staticBase; __asm { MOV staticBase, sb; \ LDR r0, [sb]; \ MOV sb, r0 } #define ELF_CONTEXT(ptr) unsigned int staticBase; void* elfStaticBase = ptr; __asm { MOV staticBase, sb; \ MOV r9, elfStaticBase } #define END_CONTEXT RestoreSB(staticBase);

Но я не учел то, что MMI хоть и построены по event-based принципу, в них нельзя так просто взять и сделать while(true) {}, а необходимо использовать таймеры, что влечет за собой постоянные костыли с свичингом контекстов, что по итогу только снижает производительность. По итогу я перешел на релокации и реализовал проброс таймеров.

Никаких отладчиков, программа что-то записала не туда? Ребут и сиди, отлаживай с printf!
Никаких отладчиков, программа что-то записала не туда? Ребут и сиди, отлаживай с printf!

Во всем остальном, MRP и MRE простые как табуретка, никаких проблем с пробросом ввода и графики не возникло:

LcdInfo* LcdInit() { LcdInfo* ret; ret = (LcdInfo*)malloc(sizeof(LcdInfo)); ret->Width = screenInfo.width; ret->Height = screenInfo.height; ret->Pixels = (void*)w_getScreenBuffer(); return ret; } void LcdFree() { } void LcdLock(LcdInfo* info) { } void LcdFlush(LcdInfo* info) { mrc_refreshScreen(0, 0, 240, 320); }

И вот, наша программа уже запускается на двух совершенно разных ОС без каких либо проблем!

[Технолонг] Ваши ИИ так не смогут! Пишем один «exe», который работает на 3-х разных ОС без перекомпиляции

❯ А если что-то посложнее Hello, world?

Наверняка у читателя возникнет вопрос мол «окей, твой эльфятник может и способен запускать простые программы, но как насчет чего-то посложнее?». И конечно же, для тестов я решил портировать не абы что, а целый эмулятор NES! В конце-концов, одна из целей разработки такого эльфятника — возможность запускать Java-игр и эмуляторов на многих кнопочных телефонах из нулевых.

Какое то время назад, я обнаружил весьма шустрый эмулятор NES от неизвестного разработчика из Китая. Код был неважного качества, никаких копирайтов в нём не было. Но поскольку сам эмулятор был быстрый (быстрее, наверное, только vNesC, который является прямым source-портом Java-эмулятора vNes на C), я отвязал его от целевой платформы и превратил в небольшую библиотеку для легкого портирования на любые платформы путем вызова всего нескольких функций:

typedef struct { uint16_t* FrameBuffer; uint8_t* JoyState; } emuContext; emuContext* emuInitialize(); uint8_t emuLoadROM(void* rom, int length); void emuReset(); void emuDoFrame(); void emuShutdown();

И, соответственно, базовый порт на наш эльфятник выглядит примерно так:

#include <string.h> #define FUNC_PROTOTYPES #include <system.h> #include <nes.h> #include "nes_rom.h" emu_context* ctx; LcdInfo* lcdInfo; void EmuTick() { emuDoFrame(); LcdLock(lcdInfo); short* pixels = (short*)lcdInfo->Pixels; for(int i = 0; i < EMU_FRAMEBUFFER_HEIGHT; i++) { memcpy(&pixels[i * lcdInfo->Width], &ctx->FrameBuffer[i * EMU_FRAMEBUFFER_WIDTH], lcdInfo->Width * 2); } LcdFlush(lcdInfo); } void EmuSetupTimer() { TimerAttach(1, EmuTick); // As fast as possible } void EmuSetupRegularLoop() { while(true) EmuTick(); // TODO: If elfloader port will be usable on Android, add FPS limit :) } int ElfMain(unsigned int* basePtr, void* test) { lcdInfo = LcdInit(); ctx = emuInitialize(); if(!emuLoadROM(nes_rom, sizeof(nes_rom))) { UtilPrint("Failed to load ROM"); return 100; } emuReset(); switch(GetMainLoopType()) { case PLATFORM_LOOP_MMI_TIMER: EmuSetupTimer(); break; case PLATFORM_LOOP_REGULAR: EmuSetupRegularLoop(); break; } return 100; }

А вот и результат:

Китайская реплика Galaxy S3
Китайская реплика Galaxy S3
Та самая Nokla!
Та самая Nokla!
QTek
QTek

❯ Заключение

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

Если вам понравилась статья и вы хотите меня поддержать, у меня есть Boosty, а также виджет доната в комментариях на DTF. А ещё мне можно отправить какое-нибудь интересно железо: устройства на WinCE/WinMobile, китайские кнопочники, китайские подделки на iPhone/Samsung из начала 2010-х, ретро-ПК железо - всё это я очень люблю и ценю :)

Что думаете о такой методике кроссплатформенных бинарников?
Нормас тема. Мужик сказал — мужик сделал, захотел запустить один бинарь на разных ОС — запустил!
Это — читерство! Так нечестно!!! Вот как только ты встроишь в винду поддержку эльфов помимо PE и будешь бранчить код в зависимости от платформы в startup.s — так и поговорим!
Это вообще туфта!
Извините что не в тему, но Mach-O > Elf > PE
Во, bodyawm исправился со времен статьи про запуск пользовательского софта на микроконтроллерах с Xtensa. Таки асилил ELF!
Но ниасилил нормальные релокации через GOT!
И что, даже APCS не конфликтовал с AAPCS и APCS-CE? Не верю
Что думаете о статье?
Это определенно торт. Такие материалы нужны DTF!
Это не торт!!! На DTF нужно больше научпопа и контента, понятного рядовому пользователю!!!
Это не торт!!! На DTF нужны ещё более подробные и комплексные статьи с описанием каждого бита!
Где обещанный контент с аппаратным моддингом ВАЗ 2110?!
121
24
10
7
2
57 комментариев