Включение бинарных файлов в прошивку

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

Cover Image

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

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

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

Поехали...

Предыстория

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

Адресное пространство старой и новой, а так же размер разделов, структура раздела с конфигом и многое другое были не совместимы, а переезд планировался с сохранением всей необходимой информацией. Но это не самое сложное.

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

Что мы имеем:

  • Исходники программы-мигратора на языке Си;
  • Бинарник программы-обновлялки и ещё нескольких других программ (хорошо было бы собирать перед сборкой прошивки), которые необходимо включить в прошивку;
  • GCC со всеми вытекающими.

Что делать?

Попробуем рассмотреть варианты, которыми можно выполнить нашу задачу:

  • Написать скрипт, который подготовит из бинарника Сишный массив (так себе вариант);
  • Написать маленький ассемблерный код, где будет включен наш бинарник (очень простой);
  • Использовать Binutils и описать лишь цели в makefile (красивый вариант).
  • Самый красивый вариант.

Так себе вариант

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

Но в итоге будет что-то вроде этого:

uint8_t updater_data[] = {0x00, 0x10, 0x00, 0x20, 0x00, 0x00, 0x00, 0x08, 0x84 ... };
size_t updater_data_len = 0x200;

Чем это плохо?

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

Очень простой

Необходимо написать небольшой ассемблерный файл, который будет компилится вместе с остальными файлами проекта.

Файл должен содержать примерно следующие инструкции.

        AREA    updater_Section, DATA, READONLY

        EXPORT  updater_data
updater_data
        INCBIN  updater.bin
updater_end

updater_length
        DCD     updater_end - updater_data
        EXPORT  updater_length

        END

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

Казалось бы нет ничего проще, но давайте рассмотрим следующий вариант.

Красивый вариант

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

Здесь мы не пишем никакой программный код, а лишь при сборке проекта вызываем нужные утилитки с нужными параметрами.

Нужно просто создать цель в makefile

$(OBJ_DIR)/bin_updater.o: $(OBJ_DIR)
    @objcopy -I binary --output-target elf32-littlearm --rename-section .data=.bin_files ./updater.bin $@

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

Далее необходимо в процессе линковки просто подсунуть наш объектный файл.

Теперь разберем как это использовать:

extern const uint8_t _binary___updater_bin_start[];
extern const uint8_t _binary___updater_bin_end[];

void func1(void)
{
    /* Например перебор байтов */
    for (uint8_t* p = _binary___updater_bin_start; p < _binary___updater_bin_end; p++)
    {
        /* CODE */
    }
}

Самый красивый вариант

const uint8_t updater_data[] = {
   #embed uint8_t "updater.bin"
};

Красиво, неправда ли? Но поспешу огорчить такой вариант это лишь предложение для включения в новый стандарт языка Си.

Вместо выводов

Выбирайте вариант, который вам больше всего нравится, а если есть что-то отличное от этого пишите в комментариях.