Как стать автором
Обновить

Выполняем сторонние программы на микроконтроллерах с Гарвардской архитектурой: как загружать программы без знания ABI?

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров15K
Всего голосов 63: ↑62 и ↓1+61
Комментарии37

Комментарии 37

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

Собственно, почему бы и не поделится своим видением!?

В следующей статье вас ждёт легенда своих лет: Nokia N-Gage QD, с ремонтом и рассказом о типовых болячках смартфонов Nokia тех лет, ковырянии в Symbian SDK и попытках написать игру под эту платформу. Мы рассмотрим некоторые особенности Symbian и попробуем разобраться, почему эта система проиграла войну с Android и iOS!

И хардварно оживил, и софтварно обогатил :)

Чесно говоря не понял, в чпм проблема загрузить .bss и .data если с .text проблем нет.

После вашей загрузки вы спрашиваете размер памяти выделяете и отдаете программе. Но это тоже самое что загрузить остальные сегменты и дать программе адреса где они есть. Может вместо GetGlobalStateSize лучше было GetDataAddress, GetBssAddress или что-то подобное.

.bss и .data если с .text проблем нет.

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

int a;

int main()
{
  a = 5;
}

Пусть a лежит по адресу 0x0 относительно .text, а программа загружается по адресу 0x100. Получается что после линковки, программа обратится именно к смещению 0x100, хотя программа может быть загружена куда угодно - например в 0x200. По итогу логика программы ломается.

	e.GetGlobalStateSize += (unsigned int)mmappedPtr;
	e.Start += (unsigned int)mmappedPtr;

А это точно сделает то что нужно (это указатели на функции, прибавит к ним не количество байт, а в sizeof(void(*)()) больше)

И ещё наверное опечатка, проверка поинтера на отрицательность

	const esp_partition_t* partition = ...;
	if(partition <= 0)

А это точно сделает то что нужно (это указатели на функции, прибавит к ним не количество байт, а в sizeof(void(*)()) больше)

Там всё нормально. Почему это он должен прибавить sizeof(void(*)())?


С проверкой очепятка, да, спасибо Там проверка на ESP_ERR должна быть.

Там всё нормально. Почему это он должен прибавить sizeof(void(*)())?

Потому что указатель на функцию и казалось бы должно прибавляться sizeof(X), но да, кажется оно работает именно прибавляя байты (что конечно далеко не каждый сишник знает)

p.s. это кажется расширение гцц

In GNU C, addition and subtraction operations are supported on pointers to void and on pointers to functions. This is done by treating the size of a void or of a function as 1.

A consequence of this is that sizeof is also allowed on void and on function types, and returns 1.

The option -Wpointer-arith requests a warning if these extensions are used.

В любом случае спасибо за наводку. Всегда считал такое поведение соответствующим стандарту ;)

А Thread-Local Storage заабьюзить не пробовали? Если в данном GCC всё реализовано как надо, то всё должно свестись к добавлению к статическим/глобальным переменным атрибута __thread, и те начнут адресоваться относительно регистра THREADPTR (который при старте выставить на выделенную область). Для TLS и секция инициализации предусмотрена, которую по идее можно средствами ld поместить в кодовый сегмент и при старте скопировать в выделенную область (по аналогии с инициализацией .data в RAM из flash на микроконтроллерах).

Вдогонку: ещё один класс проблем, решаемых relocations, но не решаемых PC-relative адресацией: всякие константные таблицы указателей вроде { CmdText, CmdHandler }[], которые ld положит в .text как есть и никто их не пересчитает при загрузке.

Глянул сейчас реализацию TLS, там большая зависимость от динамического линкера :(

Да, но по идее он должен делать довольно простые вещи - копировать данные инициализации в динамически выделенную память и записывать в THREADPTR её адрес. Секцию с данными инициализации в скрипте линкера помещаете в «основной» сегмент (где код), ее начало/длину там же вытаскиваете в виде двух uint32 в заголовок, а при загрузке делаете memcpy оттуда в выделенную вами область, выставляете THREADPTR на неё же, прыгаете на точку входа, всё, вы - динамический линкер. Основная «магия» всех этих действий будет в другом - GCC сгенериурет код, где все обращения к static/global будут относительно THREADPTR, который под вашим контролем.

эм... ка кто тут все очень свалено в кучу, особенно в начальной части, изложенное надо прямо таки осиливать.

для начала про старое. сегментная, или скорее страничная архитектура позволяет иметь памяти несколько программ одновременно, несмотря на то что они имеют готовый бинарник рассчитанный на определенный адрес. Просто они находятся в разных страницах, которые отображаются на один и тот же адрес. Они при этом как бы не могут исполнятся одновременно но реально одновременно на таких архитектурах мало что работает - либо один код, либо другой, а поверх этого - тот или иной контроль по времени.

При этом высшим пилотажем было создание т.н. релоцируемого кода который использовал только относительные переходы и мог быть просто размещен по любому адресу без особой релокации

динамическая же загрузка кода на не очень навороченных контроллерах уровня stm32 и esp32 это как бы и круто, но это усложнение эффективность которого надо просчитать. например для flipper zero так сделали, как бы есть подгружаемые приложения которые используют графический интерфейс в общем стандарте., но все равно люди делают альтернативные прошивки потому что чего то не хватает.

ну и с точки зрения безопасности это лишняя брешь. обязательно кому нить покажется интересным подгрузить свой код в, скажем, какой нить выключатель на esp32, потом это будет совсем "само" и т.д.

Я не стал во всех подробностях расписывать процесс загрузки бинарников. Там как раз недавно статья про ELF вышла в блоге Таймвеба, в ней особенности формата расписаны подробнее. Я же хотел НА ПРАКТИКЕ и без воды показать proof of concept.

Идём к сегментной памяти. Сегментная и страничная память - это разные вещи, страница памяти - это единица для MMU, в то время как сегмент - особенность 8086 для адресации большего объёма памяти, чем позволяет шина. Вообще, насколько мне известно, сегментация - это особенность исключительно 8086 (могу ошибаться), иных процессоров с таким способом организации памяти я не видел.

одновременно

Это почему? Сначала разберемся, что значит "одновременно" выполнять код? Если речь о вызове кода из, например, библиотеки в другом сегменте, то для этого существуют far-вызовы. Если речь о вытесняющей многозадачности, то щедуллеры сохраняют полностью весь стейт задачи в стек - в том числе и указатели на сегменты, поэтому нет никакой проблемы выполнять хоть десять потоков в разных сегментах "одновременно".

При этом высшим пилотажем было создание т.н. релоцируемого кода который использовал только относительные переходы и мог быть просто размещен по любому адресу без особой релокации

Так это ведь задача компилятора была :)

динамическая же загрузка кода на не очень навороченных контроллерах уровня stm32 и esp32 это как бы и круто, но это усложнение эффективность которого надо просчитать. например для flipper zero так сделали, как бы есть подгружаемые приложения которые используют графический интерфейс в общем стандарте., но все равно люди делают альтернативные прошивки потому что чего то не хватает.

Это нужно для достаточно узкого круга задач. Никто не говорит, что нужно писать плагины для умных выключателей. Это нужно для самодельных игровых консолей, компьютеров и других девайсов, где может пригодится выполнение пользовательского кода.

Скорее вопрос устоявшейся терминологии. Стариковским голосом: сходите к спектрумистам, они четко скажут что у них испокон времен были страницы (page) памяти, а никак не сегменты. Именно для "для адресации большего объёма памяти, чем позволяет шина". Помниться вроде и в случае 8051 там были даже не страницы а банки, для того же ограничения. Я не вижу большой проблемы называть это и сегментом памяти, но термин сегмент дейcтвительно фигурирует в случае 8086, может где то еще. Точно так же можно MMU называть например "регистр страниц", особенно когда он никакой виртуализации адресов не осущетвляет.

Про одновременность тоже все относительно. Реальная одновременность - на нескольких ядрах, и то они делят между собой общие шины, скажем к памяти. иначе идет тот или иной скачек по задачам, с разделением по времени. тот же шедулер, причем на неспешных процах даже сохранять все-все на стек - заметный оверхет, достаточно прерываний с сохранением только нужных регистров.

И заметте, никаких вумных компиляторов. Все на голом асме, а то и в машинном коде, и с точностью до такта процессора.

На Спектруме действительно страницы, потому что они физически переключаются в адресном пространстве при записи в соответствующий регистр. И пока подключена одна страница, остальные недоступны. А в 8086 вся память доступна постоянно. Сегменты - это способ обратиться к любому адресу в пределах мегабайта, имея только 16-битные регистры.

По моему в голосовалке не хватает "Использовал сторонний интерпретатор(PDP-11, 68000, Z80, ARM, STM32, AVR и т.д.)"

Ну это самое странное как по мне решение. Бесспорно интересное, но я бы предпочел собрать на базе МК обвязку для реального Z80

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

я например, мечтал сделать какой-нить байткод на том же avr для целей security но реальной задачи так и не вышло.

Это весьма популярная тема у любителей ретроархитектур. Из личного опыта ATmega328+STM8+FRAM оказываются самодостаточным i8080 компьютером с клавиатурой и выводом на телевизор, работающим под управлением CP/M (неспешно, конечно, но консоль вполне отзывчива).

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

Выше уже упомянули Флиппер - мультитул для хакеров, но можно развить идею и сделать мультитул для электронщиков, физиков, математиков и т.д. в форм-факторе инженерного программируемого калькулятора. Типа OpenRPNCalc, или NumWorks, или Электроника МК-161, но с GPIO, анализатором/генератором сигналов, осциллографом, встроенными и пользовательскими математическими функциями.

Интересно, какие сейчас доступны МК c double-precision FPU и хорошей документацией, взамен STM32F7/H7.

Вы специально сфоткали ESP32 на фоне аутентичного советского ковра? Что вы хотели этим сказать? :-)

Да я частенько фоткаю девайсы на фоне ковра)

А что за чатик такой? Как попасть?

Скинул в личк

Скинул в личку

Обратите внимание, что Start вызывает подфункции с помощью инструкции CALLX8, которая в отличии от обычного Immediate-версии CALL8

Строго наоборот: call8 это Immediate pc relative call

Callx8 - indirect call по адресу в регистре

И в вашем листинге это так и есть: start вызывает функции используя call8 относительно счетчика инструкций, а вот «сисколы» везде вызываются через callx8, его адрес вы везде продергиваете через стек (а изначально его передает в start загрузчик и это абсолютный адрес)

Пасиб, писал ночью и попутал. Главное ISA перед глазами было :

Всё ждал, где же в статье будет про специфику загрузки на гарвардской архитектуре, думал, может, какой-то хитрый хак. И тут раз – и лёгким движением рук (MMU) гарвардская архитектура превращается в фон неймановскую (точнее, реальная архитектура вообще не важна [код не видит, какие шины есть], а с софтовой точки зрения – никаких отличий от фон неймановской).

Это я на ESP32 эксплуатировал наличие MMU. А на AVR я бы просто использовал команду SPM - я ведь не просто так написал, что такой способ подходит только для самопрограммируемых МК.

Логично. Но я, собственно, к тому, что упоминание гарвардской архитектуры в заголовке не имеет отношения к тому, что описано в статье. С тем же успехом вы могли написать "на микроконтроллерах, которые я купил в таком-то магазине" – вроде и правда, но отношения к делу не имеет.

в соседней теме разговор перешел на близкую тему: патчинг бинарного кода обычной флешки (контроллер 8051 совместимый) с целю интеграции в нее своего выполняемого кода

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

Тоже вариант.

Давайте для общего понимания вкратце разберемся, как происходит загрузка программ в Windows/Linux:

Система создаёт процесс и загружает в память программы секции из ELF/PE. Обычные программы для своей работы используют 3 секции: .text (код), .data (не-инициализированный сегмент памяти для глобальных переменных), .bss (сегмент памяти для инициализированных переменных).

На картинке перед этим абзацем видны program header и сегменты и section header и секции. Program header -- это то, как выглядит ELF-файл с точки зрения загрузчика. Section header -- это то, как выглядит ELF-файл с точки зрения линковщика. Загрузчик оперирует сегментами -- непрерывными областями памяти с одинаковым доступом к ним. Сегмент может содержать одну или больше секций, но загрузчику до этого нет дела.

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

Некоторые программы не рассчитаны на загрузку в произвольный участок памяти, потому что это position-dependent executable. (Т.е. они могут при этом работать, и это, кстати, ваш случай, но только лишь в силу удачного стечения обстоятельств). Некоторые программы содержат код релокации в себе и не зависят от динамического линковщика, например static position-independent executable. (Я об этом писал подробнее здесь, в разделе "форматы исполняемых файлов").

Ну камон, это же просто иллюстрация из вики :)

А почему uclinux не глянули? У него футпринт ниже + формат бинарников как раз заточен под загрузку в системы без MMU (у ESP32 есть MMU, но мы с вами знаем какой :)).

Ну камон, это же просто иллюстрация из вики :)

Так я говорю не об иллюстрации, она как раз ок. Я говорю, что текст под ней -- не очень соответствует действительности.

А почему uclinux не глянули? У него футпринт ниже + формат бинарников как раз заточен под загрузку в системы без MMU

nommu ядро я как раз использовал, потому что других вариантов просто нет. А почему bFLT мне не подошёл в той же статье написано (TL;DR: формат неудобный, с сильными встроенными ограничениями, выгоды от его использования нет). Вместо этого я добавил в тулчейн поддержку FDPIC и получил выполнение кода откуда угодно (в т.ч. из флэша прямо из образа файловой системы) и работающую динамическую линковку.

Разве не в .bss лежат не инициализированные данные, а в дата наоборот, поправьте а статье.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий