- Регистрация
- 23 Авг 2023
- Сообщения
- 4,212
- Реакции
- 0
- Баллы
- 36
Ofline
Рассмотрен подход к созданию управляемого "бэкдора", позволяющего подгружать функции без остановки и перезагрузки. С помощью манипуляций с линкер-скриптом и средств языка C создаются "точки расширения" в прошивке, позволяющие в будущем внедрять новые функциональные модули без пересборки и перезаписи всей программы. Такой подход может быть полезен при разработке отказоустойчивых систем для оптимизации жизненного цикла встроенного ПО, так как позволяет заложить гибкость при непредвиденных модификациях.
Метод заключается в разделении адресного пространства прошивки: фиксируется положение базового кода и выделяются отдельные секции для динамически загружаемых блоков кода, которые объявляются с
В некотором смысле этот подход представляет собою внедрение микро-бутлоадера, который выполняет свою функцию не ДО основного кода, а ОДНОВРЕМЕННО с ним.
Код проекта примера можно найти в репозитории.
Для демонстрации подхода сконфигурирован проект в CubeMX для платы на STM32F103C8T6 (blue pill) с USB в режиме виртуального COM-порта.
В полученный стандартный линкер-скрипт добавлен сегмент RAM_PATCH в посередине ОЗУ (по адресу
Добавлена секция
В
Функция
Если в памяти (по адресу, на который указывает ваш указатель) лежат нули, то попытка выполнить их как код приведет к тому, что процессор попытается исполнить инструкцию
Функция загрузчика представляет собой проверку того, что получаемые данные это именно код, а не что иное, и копирует эти данные в область патча
Хотя простейшие патчи можно собирать без линкер-скрипта (используя позиционно-независимый код), здесь выбран путь использования линкер-скрипта для обеспечения жесткого контроля над размещением кода. Это гарантирует, что не произойдёт выхода за границы выделенного слота в RAM и что все адреса внутри патча будут корректно разрешены относительно базовой части. Кроме того предусмотрительно добавлены секции данных в линкер-скрипт патча, чтобы в будущем иметь возможность использовать глобальные переменные внутри динамических модулей:
Для ясности взят код простейшей мигалки:
Cоздание бинарного файла:
Теперь его можно отправить в COM-порт любым терминалом, поддерживающим отправку файлов, код в
Метод
Разделение памяти
Метод заключается в разделении адресного пространства прошивки: фиксируется положение базового кода и выделяются отдельные секции для динамически загружаемых блоков кода, которые объявляются с
[I]attribute((section(".my_patchable_func")))[/I], что позволяет размещать код функции/переменных в заданной области ОЗУ/ПЗУ, описываемой линкер-скриптом.В некотором смысле этот подход представляет собою внедрение микро-бутлоадера, который выполняет свою функцию не ДО основного кода, а ОДНОВРЕМЕННО с ним.
Проблемы связности. Как обновляемый код будет общаться с базовой частью?
Глобальные переменные: Чтобы обновляемая функция «видела» глобальные данные ядра, необходимо использовать таблицу указателей или фиксированные адреса (абсолютную адресацию). Ядро экспортирует таблицу адресов своих функций и данных, а модуль обращается к ним как к внешним API.
Статические переменные: Лучше полностью избегать использования[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.
Возрастает сложность управления версиями: основной код и патчи должны быть жестко согласованы.
Недоступность исходников базовой части скорее всего сделает этот механизм очень ограниченным или совсем бесполезным.
Внедрения такого механизма усложняет код, из-за чего проще допустить ошибку.