AI Реализация модульной архитектуры прошивки методом ручной динамической линковки на примере STM32

AI

Команда форума
Редактор
Регистрация
23 Авг 2023
Сообщения
4,212
Реакции
0
Баллы
36
Ofline
Рассмотрен подход к созданию управляемого "бэкдора", позволяющего подгружать функции без остановки и перезагрузки. С помощью манипуляций с линкер-скриптом и средств языка C создаются "точки расширения" в прошивке, позволяющие в будущем внедрять новые функциональные модули без пересборки и перезаписи всей программы. Такой подход может быть полезен при разработке отказоустойчивых систем для оптимизации жизненного цикла встроенного ПО, так как позволяет заложить гибкость при непредвиденных модификациях.

Метод​

Разделение памяти​


Метод заключается в разделении адресного пространства прошивки: фиксируется положение базового кода и выделяются отдельные секции для динамически загружаемых блоков кода, которые объявляются с [I]attribute((section(".my_patchable_func")))[/I], что позволяет размещать код функции/переменных в заданной области ОЗУ/ПЗУ, описываемой линкер-скриптом.

В некотором смысле этот подход представляет собою внедрение микро-бутлоадера, который выполняет свою функцию не ДО основного кода, а ОДНОВРЕМЕННО с ним.

Проблемы связности. Как обновляемый код будет общаться с базовой частью?​


  1. Глобальные переменные: Чтобы обновляемая функция «видела» глобальные данные ядра, необходимо использовать таблицу указателей или фиксированные адреса (абсолютную адресацию). Ядро экспортирует таблицу адресов своих функций и данных, а модуль обращается к ним как к внешним API.


  2. Статические переменные: Лучше полностью избегать использования [I]static[/I] внутри динамических функций. Вместо этого все необходимые данные должны передаваться в функцию через аргументы или указатели на структуры, выделенные в «ядре». Это делает функцию позиционно-независимой и упрощает управление памятью.

Задачи, решаемые в рамках реализации​


  • Создание базовой прошивки: Проектирование ядра системы, которое содержит таблицу экспорта функций и переменных и загрузчик.


  • Формирование патча: Он должен уметь получать от базовой части все необходимые ресурсы и работать в сложившихся ограничениях (патч пользуется только остатками памяти, которые к тому же могут быть фрагментированы, а выполнение кода патча не должно ломать логику базовой части в том числе по таймингам).


  • Протокол передачи: Базовая часть должна уметь принимать патч, проверять целостность, а сам процесс инъекции кода должен быть по возможности неблокирующим.

Пример​


Код проекта примера можно найти в репозитории.

Для демонстрации подхода сконфигурирован проект в CubeMX для платы на STM32F103C8T6 (blue pill) с USB в режиме виртуального COM-порта.

В полученный стандартный линкер-скрипт добавлен сегмент RAM_PATCH в посередине ОЗУ (по адресу 0x20002800):

Код:
/* RAM (20K) */
MEMORY
{
    RAM_MAIN  (xrw) : ORIGIN = 0x20000000, LENGTH = 10K /* Main part (.data, .bss, heap) */
    RAM_PATCH (xrw) : ORIGIN = 0x20002800, LENGTH = 2K  /* Patch part */
    RAM_STACK (xrw) : ORIGIN = 0x20003000, LENGTH = 8K
    
    FLASH     (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
}

Добавлена секция [I]patch_buffer[/I] в которую будет загружаться код патча:

Код:
    /* PATCH */
    .patch_buffer (NOLOAD) :
    {
        . = ALIGN(4);
        _s_patch = .;      /* Patch starts here */
        KEEP(*(.patch_buffer))
        . = ALIGN(4);
        _e_patch = .;
    } >RAM_PATCH

В [I]main.c[/I] добавляется массив для доступа к области памяти патча и указатель на функцию, которую в будущем планируется добавить:

Код:
/* USER CODE BEGIN 0 */

/// @brief      Содержимое RAM_PATCH в виде буфера
/// @details    Линкер сам положит этот буфер в область ОЗУ RAM_PATCH
uint8_t patch_ram_buffer[2048] __attribute__((section(".patch_buffer")));

/// @brief      Тип функции патча
typedef void (*patch_func)(void);

/// @brief      Функция патча
patch_func patch;

/// @brief      Функция вызова патча
/// @details    Если по адресу патча лежит заданное магическое число, то передать исполнение патчу
void check_patch ();

/* USER CODE END 0 */

Функция [B]check_patch[/B] вызывается в супер-цикле и представляет собою простую проверку наличия кода патча по нужному адресу:

Код:
void check_patch ()
{
    uint32_t *magic_patch = (uint32_t*)patch_ram_buffer;

    if (*magic_patch != PATCH_MAGIC_NUMBER)
    {
        return;
    }

    // Инициализируем функцию патча 
    // Для ARM Cortex-M добавляем +1 к адресу, чтобы включить Thumb-режим
    patch = (patch_func)(&patch_ram_buffer[4] + 1);

    patch();
}

Если в памяти (по адресу, на который указывает ваш указатель) лежат нули, то попытка выполнить их как код приведет к тому, что процессор попытается исполнить инструкцию 0x00000000. Это фактически "синий экран" для МК (программа повиснет или перезагрузится, если настроен WDT). Поэтому в данном примере используется простая защита с помощью магического числа PATCH_MAGIC_NUMBER.

Функция загрузчика представляет собой проверку того, что получаемые данные это именно код, а не что иное, и копирует эти данные в область патча (usbd_cdc_if.c):

Код:
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t* Len)
{
    /* USER CODE BEGIN 6 */

    uint32_t magic_received = 0;
    
    if (*Len >= 4) 
    {
        memcpy(&magic_received, Buf, 4); // Копируем байты магического слова
    }

    if (*Len > 0)
    {
        // копируем принятые данные в память патча
        if (magic_received == PATCH_MAGIC_NUMBER)
        {   
            if (*Len <= sizeof(patch_ram_buffer))
            {
                memset(patch_ram_buffer, 0, sizeof(patch_ram_buffer));
                memcpy(patch_ram_buffer, Buf, *Len);
            }
        }
    }

    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
    USBD_CDC_ReceivePacket(&hUsbDeviceFS); // Продолжить прием

    return (USBD_OK);
    /* USER CODE END 6 */
}

Хотя простейшие патчи можно собирать без линкер-скрипта (используя позиционно-независимый код), здесь выбран путь использования линкер-скрипта для обеспечения жесткого контроля над размещением кода. Это гарантирует, что не произойдёт выхода за границы выделенного слота в RAM и что все адреса внутри патча будут корректно разрешены относительно базовой части. Кроме того предусмотрительно добавлены секции данных в линкер-скрипт патча, чтобы в будущем иметь возможность использовать глобальные переменные внутри динамических модулей:

Код:
MEMORY { RAM_PATCH (rwx) : ORIGIN = 0x20002804, LENGTH = 1996 }
SECTIONS {
    .text : { *(.text*) } > RAM_PATCH
    .data : { *(.data*) } > RAM_PATCH
    .bss  : { *(.bss*) *(COMMON) } > RAM_PATCH
}

Для ясности взят код простейшей мигалки:

Код:
#include <stdint.h>

// Адреса для Blue Pill (GPIOC, Pin 13)
#define GPIOC_BASE    0x40011000
#define GPIOC_ODR     (*(volatile uint32_t *)(GPIOC_BASE + 0x0C))

void patch_blink(void) {
    // Простой цикл задержки
    for (volatile uint32_t i = 0; i < 500000; i++);
    
    // Инвертируем состояние пина 13
    GPIOC_ODR ^= (1 << 13);
}

Cоздание бинарного файла:

Код:
# Папка патча
mkdir Patch

# Компиляция + Линковка + Создание бинарного файла
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c Core/Src/patch.c -o Patch/patch.o
arm-none-eabi-ld -T PATCH_SCRIPT.ld Patch/patch.o -o Patch/patch.elf
arm-none-eabi-objcopy -O binary -j .text Patch/patch.elf Patch/patch.bin

# Добавление магического слова в начале для идентификации
printf '\xEF\xBE\xAD\xDE' > Patch/full_patch.bin
cat Patch/patch.bin >> Patch/full_patch.bin

Теперь его можно отправить в COM-порт любым терминалом, поддерживающим отправку файлов, код в [I]CDC_Receive_FS[/I] примет данные, запишет их в RAM, а [I]check_patch()[/I] в [I]while(1)[/I] увидит магические числа и вызовет [I]patch()[/I].

Критика​


  • Не во всех случаях можно получить разрешение на внедрение такого "архитектурного решения".


  • Некорректное выравнивание данных или ошибка в адресации приведут к HardFault.


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


  • Недоступность исходников базовой части скорее всего сделает этот механизм очень ограниченным или совсем бесполезным.


  • Внедрения такого механизма усложняет код, из-за чего проще допустить ошибку.
 
Назад
Сверху Снизу
Яндекс.Метрика Рейтинг@Mail.ru