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

Пролог

В ARM Cortex-M (Arm v7-M) процессорах есть очень полезный блочок. Называется MPU (Memory Рrotection Unit). Попробуем разобраться что это такое и зачем нужно. .

Определения

Выровненный адрес - адрес, который может принимать значения с некоторым периодом начиная с нуля. Периоды выровненных адресов как правило - это значения степеней двойки: 8; 16; 32; 64; 128; 256; 512; и т. п.

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

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

MMU - Блок управления памятью или устройство управления памятью (англ. memory management unit, MMU) — компонент аппаратного обеспечения компьютера, отвечающий за управление доступом к памяти, запрашиваемым центральным процессором. Его функции заключаются в трансляции адресов виртуальной памяти в адреса физической памяти (то есть управление виртуальной памятью), защите памяти, управлении кэш-памятью, арбитражем шины и, в более простых компьютерных архитектурах (особенно с небольшой разрядностью шины адреса), переключением блоков памяти.

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

MPU - это аппаратная часть процессора, которая предоставляет защиту памяти. Это упрощенная версия MMU, которая делает только защиту памяти. MPU не поддерживает виртуальную память. MPU вызывает исключение при нарушении доступа к памяти.

Что надо из доков?

Для пуска MPU нужны всего вот эти два исходных документа.

Называние дока

Страниц

Версия

Arm v7-M Architecture Reference Manual

858

--

ARM Cortex-M7 Processor Technical Reference Manual

145

r0p2

Реализация

MPU это часть ядра ARM Cortex-M7, а точнее часть ядра ARMv7-M.

В физической памяти регистры MPU расположены по адресу 0xE000_ED90. Перечень регистров MPU можно посмотреть в файле ARM Cortex-M7 Processor Technical Reference Manual.

Однако детализация битовых полей указана в другом документе Arm v7-M Architecture Reference Manual. MPU поддерживает 16 интервалов.

Пример на активацию MPU присутствует в SDK от вендора \Example\xxxxxx\MPU\Mpu_Example\Sources\main.c

При работе с MPU надо учитывать один очень важный момент. Стартовый адрес региона памяти должен быть выровнен на его собственный размер. Если у вас, к примеру, регион 16 Кб, то выравнивать нужно на 16 Кб. Если регион памяти 64 Кб, то выравниваем на 64 Кб. И так далее. Если этого не сделать, то MPU может автоматически “обрезать” регион под размер соответствующий его стартовому адресу. Вот пример

BaseAddr

size

Alig, bits

Alig. Addr

Aligned Addr

0x2100373c

32

5

0x21003720

0010_0001_0000_0000_0011_0111_0010_0000

0x2100375c

64

6

0x21003740

0010_0001_0000_0000_0011_0111_0100_0000

0x2100379c

128

7

0x21003780

0010_0001_0000_0000_0011_0111_1000_0000

0x00000000

65536

16

0x00000000

0000_0000_0000_0000_0000_0000_0000_0000

Это требование прописано в доке Arm v7-M Architecture Reference Manual

Программная часть

MPU конфигурируется структурой типа MPU_Region_InitTypeDef, которая определена в файле XXX_core_common_feature.h. SDK не богат на API для MPU выделено всего три функции.

Название функции

файл

MPU_ConfigRegion

XXX_SDK_V2_3_2\Template\Driver\Source\module_driver_mpu.c

MPU_Enable

module_driver_mpu.c

MPU_Disable

module_driver_mpu.c

Как обычно: setter-ы есть, а getter-ов нет. Поэтому пришлось расширять и дописать ещё своих функций для getter-ов, чтобы была хоть какая-то диагностика.

Первым делом следует проинициализировать регионы MPU.

mpi
mpi

После инициализации надо непременно проверить, что настройки MPU в самом деле применились в аппаратуре.

cmpur
cmpur

Для проверки, что MPU работает у меня заготовлена вот такая функция. Она пытается прочитать один dword по запретному нулевому адресу.

bool test_null_ptr(void) {
    LOG_INFO(TEST, "%s()", __FUNCTION__);
    bool res = true;
    uint32_t* pAddr = NULL;
    uint32_t val = *pAddr;
    LOG_INFO(TEST, "Val:0x%08x", val);
    return res;
}

В ARM Cortex-M7, когда происходят срабатывание по MPU, то выстреливает прерывание MemManage_Handler. Когда срабатывает прерывание по MPU, то из него не выйти просто так. Если внутри MemManage_Handler не отключать MPU, то MemManage_Handler будет снова и снова вызываться, блокируя исполнения main. Поэтому в прерывании надо временно отключить MPU.

Вот мы научились отлавливать нарушения доступа к памяти. Чудно! Однако тут же возникает новая проблема. Вот я настроил все 16 регионов MPU. Сработало прерывание MemManage_Handler. Как же я пойму от какого региона выстрелило это прерывание? Карта регистров MPU про это не сообщает. Эта задача решается. Как сработало прерывание MPU внутри ISR надо сохранить значения двух регистров.

Name

Description

Address

size

MMFAR

MemManage Fault Address Register

0xE000ED34

4

CFSR

Configurable Fault Status Register

0xE000ED28

4

MMFSR

MemManage Status Register

0xE000ED28

1

Они расположены в структуре SCB. Регистр CFSR содержит внутри себя регистр MMFSR. Вот MMFSR битом MMARVALID как раз и укажет, что обнаружен валидный адрес в регистре MMFAR. Если в MMFAR 1 то надо читать MMFAR. Там будет лежать физический адрес по которому произошло нарушение доступа. В регистре MMFSR отдельно можно только выявить факт исполнения кода из региона (бит IACCVIOL). А на запись и чтение ARMы выделили только один бит - DACCVIOL.

Проверка запрета на чтение

Попробуем сделать запретное чтение. Сработало MPU прерывание и прошивка узнала из какого это произошло региона.

Запрет на запись в RAM память средствами MPU

Для проверки запрета записи в ICTM память у меня есть тест test_mpu_ban_itcm_write. Shell команда tr mpu_ban_itcm_write показывает, что попытка записи в ITСM в самом деле инициирует прерывание MemManage_Handler.

Запрет исполнения кода из RAM памяти

Я заготовил вот такой модульный тест. Тут секция ITCM_Fun это интервал физических адресов от 0x00008000 до 0x0000FFFF. При этом MPU накладывает запрет на выполнение интервала от 0x0000_0000 до 0x0000FFFF. То есть функция mpu_itcm_function вызывается из середины запретного интервала. Про то как делать исполнение кода из RAM можно почитать здесь.

__attribute__((section(".ITCM_Fun")))
static uint32_t mpu_itcm_function(const uint32_t in_value) {
    uint32_t out_value = in_value + 1;
    LOG_INFO(TEST,"%s(),In:%u,Out:%u",__FUNCTION__,in_value,out_value);
    return out_value;
}

bool test_mpu_ban_exe(void) { 
    LOG_INFO(TEST, "%s():", __FUNCTION__);
    bool res = true;

    MpuHandle_t* Node = MpuGetNode(TEST_MPU_NUM);
    ASSERT_NE(NULL, Node);
    Node->it_done = false;

    LOG_INFO(TEST, "mpu_itcm_function:0x%p", mpu_itcm_function);

    uint32_t value = mpu_itcm_function(1);
    (void) value ;

    ASSERT_TRUE(Node->it_done);
    ASSERT_EQ(2, value);

    return res;
}

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

Если же из MemManage_Handler вызвать функцию из запрещенного к исполнению региона, то сработает уже HardFault_Handler.

Видно, что остальные модульные тесты проходят.

Хорошо. Научились включать MPU. Как бы теперь сделать что-н по настоящему полезное?

Определение переполнения стека при помощи MPU

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

У меня заготовлена функция try_recursion специально для погружений в глубину доступной стековой RAM памяти. Она измеряет насколько глубоко в памяти программа продвинулась в в байтах. Результат помещается в переменную *stack_size.

static bool call_recursion(uint32_t stack_top_addr, 
                           uint32_t cur_depth, 
                           uint32_t max_depth, 
                           uint32_t* stack_size) {
    bool res = false;
    if(cur_depth < max_depth) {
        res = call_recursion(stack_top_addr, 
                             cur_depth + 1, 
                             max_depth, 
                             stack_size);
    } else if(cur_depth == max_depth) {
        uint32_t cur_stack_use = stack_top_addr - ((uint32_t)&res);
        *stack_size = cur_stack_use;
        res = true;
    } else {
        res = false;
    }
    return res;
}

bool try_recursion(const uint32_t stack_top_addr, 
                   const uint32_t max_depth, 
                   uint32_t* const stack_size) {
    bool res = false;
    res = call_recursion(stack_top_addr, 0, max_depth, stack_size);
    LOG_INFO(CORE, "Depth:%u,StackSize:%u,byte", max_depth, *stack_size);
    return res;
}

При достаточно высоких значениях аргумента max_depth стековые кадры начинают наползать на тот самый регион MPU под стеком. Вот так это работает в натуре. У меня как раз в LD скрипте было указано 4k Byte для стека. Всё сходится.

Аналогично можно следить и за Heap памятью.

Проверить это можно тоже в UART-CLI терминале, попробовав выделить и прописать чрезмерное количество heap памяти.

Вот и получается, что MPU стоит на страже stack и heap памяти.

Можно даже сделать две группы регионов MPU. Регион на предупреждение (желтый) и регион на саму ошибку (красный).

Недостатки MPU на ARM Cortex-M7

1--Надо выравнивать адреса под размер региона.
2--Нет режима write_only.

3--Надо вручную программно выяснять в каком конкретном регионе произошло вторжение в память.

4--Размеры регионов MPU могут быть только степенями двойки.

5--При срабатывании прерывания MPU не ясно по какой причине сработало прерывание: сработал запрет на чтение или сработал запрет на запись.

Приложения MPU
Как можно использовать MPU?

1--Можно сделать защиту стековой RAM памяти. При помощи MPU можно зарегистрировать факт переполнения стековой RAM памяти.
2--Зашита прошивки. Вы можете наложить запрет на запись тех адресов, в которых лежит исполняемый код прошивки.
3--Защита ключей шифрования от чтения.
4--Вся область RAM памяти может отображаться как доступная для чтения и записи. Если нам не нужно выполнять код из RAM, можно установить бит запрета выполнения (XN).

Итоги

Удалость включать MPU на ARM Cortex-M7. Удалось генерировать прерывания по несанкционированному и чтению записи интервалов физической памяти.

Удалось при помощи MPU выявлять факт переполнения стека (StackOverflow) в микроконтроллере.

Словарь

Словарь

Расшифровка

MPU

Memory protection unit

RISC

Reduced Instruction Set Computer

MMU

Memory Management Unit

ARM

Advanced RISC Machine

ITCM

Instruction Tightly-Coupled Memory

SCB

System Control Block

Ссылки

Название

URL

Как исполнить Си-код из RAM памяти на MK

https://habr.com/ru/articles/935360/

Memory protection unit

https://en.wikipedia.org/wiki/Memory_protection_unit

Virtual memory

https://en.wikipedia.org/wiki/Virtual_memory

ARM Cortex-M MPU Configuration: Access Permissions and Memory Protection - System on Chips

https://www.systemonchips.com/arm-cortex-m3-mpu-configuration-access-permissions-and-memory-protection/

Как защититься от переполнения стека (на Cortex M)? @Amomum

https://habr.com/ru/articles/425071/

ARM Cortex-M

https://en.m.wikipedia.org/wiki/ARM_Cortex-M

ARM Cortex-M7 Processor Technical Reference Manual

AN4838: модуль управления защитой памяти

https://microsin.net/programming/arm/an4838-managing-memory-protection-unit-stm32.html

Arm v7-M Architecture Reference Manual

Задача о пересечении интервалов (или зачем программисту MК стабильная сортировка)

https://habr.com/ru/articles/892526/

О кэшах в микроконтроллерах ARM @alexkalmuk

https://habr.com/ru/companies/embox/articles/526470/

Вопросы

--Как можно прочитать настройки всех 16-ти регионов MPU, просто анализируя регистры MPU? Там же нет отдельного регистра на каждый регион. Как понять, что настройки в самом деле применились корректно?

--Как будет вести себя MPU, если два региона памяти нахлестываются друг на друга (пересекаются)? Where there is an overlap between two regions, the register with the highest region number takes priority.

--Вы настроили MPU на запрет записи памяти. Вот программа дошла до строчки записать в память. Сработало прерывание MPU. Окажется ли прописанное значение в памяти после выхода из обработчика прерываний MPU? Да. Память пропишется вопреки запрету через MPU. MPU только уведомляет о нарушении доступа но не перекрывает сам доступ.

Комментарии (0)


  1. checkpoint
    24.09.2025 20:26

    А какая еще может быть польза от MPU, кроме как отслеживание переполнений стека/кучи ?


    1. aabzel Автор
      24.09.2025 20:26

      1--Зашита прошивки. Вы можете наложить запрет на запись тех адресов, в которых лежит исполняемый код прошивки.

      2--Защита ключей шифрования от чтения.

      3--Вся область RAM памяти может отображаться как доступная для чтения и записи. Если нам не нужно выполнять код из RAM, можно установить бит запрета выполнения (XN).


      1. checkpoint
        24.09.2025 20:26

        1) и 3) это защита от бага в самой прошивке ? На счет 2) не понятно от кого защита. Если есть возможность исполнить код, то и отключить MPU не проблема. Я сначала подумал, что с помощью MPU можно как-то отделить "ядро" от "приложения" в примитивной операционной системы. Но потом подумал еще и засомневался в этой идеи.

        У меня есть простецкая однозадачная ОС для STM-ок в которой приложения отделены от ядра, но защиты никакой нет, т.е. приложение может легко запороть ядро системы, случайно или намеренно. Может ли тут мне помочь MPU ?


        1. AbitLogic
          24.09.2025 20:26

          У RISC-V есть режимы S, M, U, они позволят


    1. Sun-ami
      24.09.2025 20:26

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

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


      1. checkpoint
        24.09.2025 20:26

        То, о чем Вы пишите это задача для MMU. На сколько я понимаю, в MPU нет приоритезации уровней исполнения, а значит любой кусок кода может отключить MPU и сходить посмореть что там в "системе" или даже пропатчить её. Я как раз подумывают над тем как бы "скрыть" ядро системы от приложений и вижу, что с помощью только лишь MPU это не реализуемо.


        1. Sun-ami
          24.09.2025 20:26

          Я сам не использовал MPU, но, насколько я понимаю, от отключения MPU можно защититься настройкой MPU так, чтобы его регистры, регистры управления прерываниями, и область памяти с векторами прерываний были защищены от записи. Тогда при попытке записи произойдёт прерывание MemManage Fault, которое будет гарантировано обработано кодом ОС. Правда, в этой статье говорится, что запись при этом всё-таки произойдёт, поэтому в этом случае вероятно потребуется перезагрузка, хотя не обязательно. Наверное, это недоработка в железе, запись в этом случае происходить не должна.


      1. VelocidadAbsurda
        24.09.2025 20:26

        На Cortex-M для такого нужно ещё и наличие TrustZone (она есть в «новых» ядрах типа М33). Она добавляет уровни привилегий, позволяющие закрыть регистры MPU/MMU от юзерского кода. Без неё же, как уже написали, отключить/перенастроить MPU может кто угодно.