В этом посте я расскажу о том, как я в 2022 году смог написать и скомпилировать эльф на macOS на М1, и покажу, что из этого получилось.
Прямо сразу: видео с тем, что получилось
Коротко о телефонах Siemens x65 и x75
- SoC Infineon PMB8870, также известная как SGold
- Процессор ARM926EJ-S
2 Мб оперативкиВ комментах пишут, что 8- 32 Мб флэш-памяти, из них ~10 отведено под доступную пользователю файловую систему, в остальном объёме находится прошивка
- Экран 132*176, большую часть времени работает в режиме RGB565 (16 бит на пиксель)
- В S65 и всей 75 серии есть слот для карты памяти RS-MMC (это как SD, только тоньше и в половину длины)
- В 75 серии присутствует аппаратный декодер mp3
- Связь с внешним миром: GSM/GPRS (без EDGE), последовательный порт в проприетарном разъёме, ИК-порт, в некоторых моделях bluetooth.
Структурная схема SoC из рекламных материалов производителя
Ядро процессора только одно, и оно занимается и GSM (как я понимаю, на достаточно высоком уровне), и рисованием интерфейса на экране.
Коротко об эльфах и их загрузчике
Строго говоря, полноценной операционной системы с настоящим API, динамическим линкером, виртуальной памятью и прочими современными радостями у этих телефонов нет. В процессоре есть MMU, но он не используется. Прошивка достаточно монолитна, и, по слухам, сделана на основе ОС реального времени Nucleus RTOS. Это всё оказывается проблемой, когда хочется динамически подгружать какой-то левый код — адреса нужных этому коду функций разные на разных моделях телефонов и на разных версиях прошивки для одной и той же модели. Разработчики загрузчика эльфов решили эту проблему оригинально: сделали интерфейс системных вызовов а-ля DOS. Раскладываешь аргументы функции по регистрам, вызываешь инструкцию swi XX
, где XX — номер нужной функции, а добавленный патчем обработчик прерывания уже по таблице для конкретной прошивки прыгает в нужное место.
В оригинальном репозитории на тему эльфов, который каким-то чудом всё ещё онлайн, да и по моей собственной памяти, эльфы компилировались какой-то очень странной проприетарной микроконтроллерной штукой под названием IAR. У неё вот этот способ вызова функций через swi — как будто бы встроенная в компилятор фича. Я уже был готов сам начать разбираться с правильным ABI для этих «системных вызовов», писать куски ассемблера, отлаживать всё это дело без отладчика и даже логирования, но наткнулся на вот этот репозиторий, в котором те же самые эльфы сопровождаются файлами проектов для CodeBlocks и собираются нормальным человеческим gcc. Выглядит так, что самая сложная работа всё-таки уже была сделана за меня. Так что берём это всё и…
Пытаемся собрать эльф
И сразу вопрос — а чем собирать-то будем? Я уже когда-то пробовал извратиться с Android NDK, но ничего адекватно работающего из этого не получилось. К счастью, в Homebrew есть пакет gcc-arm-embedded с описанием «Pre-built GNU bare-metal toolchain for 32-bit Arm processors» — выглядит как именно то, что нам нужно!
Ставим, собираем минимальный пример эльфа из репозитория выше, elfloader3/examples/C/main.c:
arm-none-eabi-gcc main.c -o hello.elf -I../../dev/include -DSGOLD -O2 -std=gnu99 -D__NO_LIBC -lcrt -lcrt_helper -lgcc -L../../dev/lib/libs -Wl,--defsym,__ex=0 -Wl,--gc-section -nostartfiles
(не показано: с десяток неудачных попыток)
Заливаем на телефон, запускаем. Ничего не происходит. Осознаём, что эльфы, которые генерирует gcc, всё-таки не настолько же простые, как из IAR. Ставим имеющийся в репозитории более новый эльфлоадер. Поверх старого, надеясь, что ничего не сломается.
Возвращение 2007 выглядит примерно вот так
Хорошая новость: телефон после установки патча включается и работает. Плохая: эльф всё равно не запускается, но теперь хотя бы показывает ошибку.
Динамические библиотеки, щито?! В 2007 такого не было.
После чтения исходников стало понятно, что библиотека должна быть в папке /ZBin/lib либо в памяти телефона, либо на карте. Кладём её туда и запускаем ещё раз. Получаем ошибку «Incorrect elf». Находим в исходниках рядом с ней строку (орфография сохранена):
If you wont to use the shared libraries, you must add to linker option '--defsym __ex=0' add use elfclose function!
Не буду писать много букв о том, как я это долго и мучительно отлаживал, но в итоге у меня получилось скомпилировать работающий эльф. Благодаря этому экспириенсу я узнал много нового о том, как операционные системы загружают исполняемые файлы. В итоге:
- Пришлось отказаться от предоставленной эльфлоадером «рантайм-библиотеки», потому что у меня так и не получилось заставить это работать. Видимо, GCC слишком новый.
- Не обязательно вызывать все функции прошивки через программные прерывания. Этот новый эльфлоадер так и не делает — он получает указатель на таблицу указателей на функции через
swi 0x80FF
, а дальше уже вызывает из неё. Я у себя сделал так же, в swilib.h уже все нужные макросы прописаны, удобно. - Ни в коем случае нельзя инициализировать поля с указателями в структурах статически (на этапе компиляции). Эльф загружается в по сути случайный адрес в оперативке, а компилятор прописывает в эти поля адреса как будто эльф загружен по адресу 0x00000000. Результат предсказуем. Если инициализировать во время выполнения, такой проблемы нет, адрес высчитывается относительно
pc
и получается правильный. - Выгрузку эльфа из памяти я так и не смог сделать. Идея там такая, что нужно, чтобы эльф экспортировал (или импортировал?) символ
__ex
, в который эльфлоадер положит указатель на свою структуру с информацией про сам этот эльф. При выходе нужно передать этот указатель в функциюelfclose()
. У меня так и не получилось заставить линкер экспортировать этот символ так, как оно ожидает, так что эта память просто течёт. Но для того, что мы тут пытаемся сделать, это вроде и не так уж страшно ¯_(ツ)_/¯
CC=arm-none-eabi-gcc
CFLAGS=-I../sie-dev/elfloader3/dev/include -DSGOLD -DX75 -D__NO_LIBC -O2 -std=gnu99 -Wl,-s -nostartfiles -nostdlib -fPIE -fshort-wchar
LIBS=-lgcc
DEPS=
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
BadApple: BadApple.o
$(CC) -o BadApple.elf BadApple.o $(CFLAGS) $(LIBS)
.PHONY: clean
clean:
rm -f *.o *.elf
Собственно сжатие и вывод видео
Изначально я хотел выводить картинку прямо в буфер экрана, но из этого ничего не получилось. Функция RamScreenBuffer()
действительно возвращает указатель на буфер экрана. Можно прочитать 132*176*2 байт и получится скриншот. Но писать туда бесполезно — ничего из записанного почему-то нигде не отображается. Скорее всего, потому что сама прошивка его перезаписывает раньше, чем свежезаписанные данные попадут в местный аналог видеокарты.
В итоге остановился на менее оптимальном способе: сделать растр (т.н. IMGHDR), обновлять в нём пиксели каждый кадр и рисовать его на экран. Он бывает с 1, 8 или 16 битами на пиксель, я выбрал 8, чтобы были градации серого, но чтобы и не приходилось слишком много данных гонять туда-сюда (это ооооочень медленно). Получилось не очень оптимально, но 15 кадров в секунду вроде бы тянет.
Алгоритм сжатия максимально тупой, вариация на тему RLE. Есть команды «пропустить пиксели» (которые не поменялись с прошлого кадра), «перезаписать пиксели», «заполнить указанным значением» и «заполнить нулями». Всё видео, 3287 кадров, заняло около 13 Мб. Для облегчения чтения файл поделен на страницы по 40 Кб. Предполагается читать страницу целиком и держать её в памяти — каждый кадр гарантированно находится в пределах одной страницы.
Исходники самого эльфа и кодировщика
FAQ
Q: Зачем ты это сделал?
A: Потому что могу, потому что хотел выпендриться, и потому что у меня всё равно почти нет жизни.
Q: А на других телефонах заработает?
A: На 75 серии — скорее да, чем нет. На 65 серии — скорее нет, чем да, потому что во встроенную память файл с видео не влезет, нужна карта. На S65 может заработать, если сконвертировать музыку в amr или wav. А может и не заработать. Есть только один способ узнать!
Q: Но ведь на этих сименсах и так можно смотреть видео? Я помню, я смотрел!
A: В окошке на полэкрана, с битрейтом «шакалы 20й степени» и однозначным FPS.
Автор: Григорий Клюшников