Перейти к содержанию

Переполнение стека и ошибки памяти в прошивке

Стек на микроконтроллере не ограничен ничем, кроме соседних данных: переполнившись, он молча затирает переменные, кучу или указатель возврата, и программа ломается в случайном месте. Разбираем, как устроена память AVR и Cortex-M, как оценить глубину стека до запуска по -fstack-usage, как измерить её раскраской ОЗУ, как поймать переполнение в момент записи защитной областью MPU, что смотреть в HardFault и почему на микроконтроллере осторожно обращаются с malloc.

Обновлено Редакция GAW

У программы на компьютере стек ограничен операционной системой: при выходе за предел процесс получает ошибку и завершается. На микроконтроллере без операционной системы стек — просто область ОЗУ, указатель на которую уменьшается при каждом вызове. Ниже него лежат переменные или куча, и если стек дорастёт до них, процессор спокойно перезапишет чужие данные. Программа сломается позже и в другом месте: переменная получит странное значение, функция вернётся не туда, автомат перейдёт в несуществующее состояние. Такие ошибки трудно воспроизвести, потому что они зависят от того, какие прерывания пришли в самый глубокий момент вызова.

Коротко

  • Стек растёт вниз — от конца ОЗУ к статическим данным и куче; на пути нет никакой границы, если её не поставить.
  • Оценка до запуска: -fstack-usage даёт кадр каждой функции, -Wstack-usage=N предупреждает о больших; сумма по самой глубокой цепочке + кадры прерываний.
  • Измерение: раскраска свободного ОЗУ известным значением и поиск самого нижнего изменённого слова; в avr-libc 2.3 — _get_ram_unused().
  • Защита: область MPU без доступа под стеком (Cortex-M3/M4/M7 с MPU), MSPLIM у ARMv8-M, проверочное слово.
  • Куча: malloc только при запуске или пулы фиксированных блоков; куча растёт навстречу стеку.

Как устроена память

ATmega328P. ОЗУ — 2 КБ с адреса 0x0100 по 0x08FF. Снизу вверх: .data, .bss, затем с символа __heap_start — куча (если используется malloc). Стек начинается с RAMEND = 0x08FF и растёт вниз (DS40002061B, 7.5). Всё между концом .bss и вершиной стека — общее свободное место, которое делят куча и стек.

STM32 с шаблоном ST. ОЗУ с адреса 0x20000000: .data, .bss, затем куча, начинающаяся с символа end. Указатель стека при сбросе равен _estack — концу ОЗУ. Значения _Min_Heap_Size = 0x200 и _Min_Stack_Size = 0x400 из скрипта компоновщика не ограничивают ни кучу, ни стек: они только резервируют место в секции ._user_heap_stack, чтобы компоновщик сообщил об ошибке, если на стек и кучу не остаётся хотя бы столько. Подробно эта раскладка и проверка через map-файл разобраны в статье о скриптах компоновщика.

В обоих случаях стек при переполнении первым делом залезает в кучу, если она есть, а затем в .bss и .data. Чаще всего страдают переменные, объявленные последними: компоновщик кладёт их в конец секции, ближе к стеку.

Симптомы

  • Переменная, которую никто не меняет, получает случайное значение; добавление отладочной печати «лечит» ошибку или переносит её в другое место.
  • Сбой проявляется при определённой комбинации: длинная строка в разборщике плюс приход прерывания плюс включённая отладочная печать.
  • Сборка с -O0 ломается, а с -Os работает (или наоборот): кадры функций при разной оптимизации разные.
  • На Cortex-M — HardFault с PC, указывающим на мусор или в ОЗУ: функция вернулась по затёртому адресу возврата.
  • На AVR — перезапуск программы без флагов причины сброса: возврат по испорченному адресу привёл к адресу 0.

Оценка до запуска: -fstack-usage

Ключ GCC -fstack-usage пишет рядом с каждым объектным файлом файл .su, по строке на функцию: имя функции (GCC указывает в нём файл, строку и столбец), число байт и квалификатор (GCC, Developer Options):

КвалификаторЗначениеЧто делать
staticкадр фиксированного размера, выделяется при входе и освобождается при выходечисло — точный размер кадра
dynamic,boundedесть дополнительные изменения стека, но их максимум известенчисло — верхняя граница
dynamicизменения стека не ограничены: alloca, массив переменной длинычисло — только ограниченная часть; функцию нужно переписать или считать вручную

Условный пример .su для модуля протокола:

proto.c:41:13:frame_feed	24	static
proto.c:88:6:proto_handle	152	static
log.c:20:6:log_printf	96	dynamic,bounded

Размер кадра одной функции мало что говорит: стек расходуется по цепочке вызовов. Наибольшая глубина = максимум по всем путям в графе вызовов суммы кадров. Ключ -fcallgraph-info=su выводит граф вызовов с этими размерами в формате VCG, по которому сумму можно посчитать скриптом; с -flto граф для объектного файла не выводится. Предупреждение -Wstack-usage=256 выдаёт сообщение при компиляции, если кадр функции может превысить 256 байт, а для кадров без ограничения — «stack usage might be unbounded» (GCC, Warning Options). Его стоит держать включённым всегда: оно ловит случайный массив на 1 КБ в локальной переменной.

К цепочке основного кода добавляют прерывания:

  • Cortex-M: при входе в исключение ядро само кладёт кадр из восьми слов — 32 байта (R0–R3, R12, LR, PC, xPSR), а если в прерванном коде активен контекст FPU (CONTROL.FPCA = 1) — 104 байта (0x68), и ещё 4 байта, если стек выравнивается на 8 (ARMv7-M ARM, B1.5.6). Размер кадра определяет состояние прерванного кода: если FPU пользуется сам обработчик, увеличатся кадры тех исключений, которые вытеснят его. Плюс кадр самого обработчика из .su. Обработчики разных приоритетов вытесняют друг друга, и их кадры складываются по всей цепочке вложений. Обработчики одного приоритета друг друга не прерывают.
  • AVR: при входе в прерывание в стек уходит счётчик команд — два байта у ATmega328P (DS40002061B, 7.7) — и регистры, которые сохраняет пролог обработчика. Прерывания по умолчанию не вкладываются, если обработчик сам не разрешает их через ISR_NOBLOCK.

Статическая оценка неполна там, где компилятор не видит вызовов: вызовы через указатели на функции, рекурсия, библиотечные функции без .su (например, printf из предкомпилированной библиотеки). Для них нужна раскраска.

Измерение: раскраска стека

Идея простая: при запуске заполнить свободное ОЗУ известным значением, дать программе поработать во всех режимах, а затем найти самое нижнее слово, которое изменилось. Всё, что ниже, стек ни разу не трогал.

AVR. В avr-libc 2.3 появилась готовая функция _get_ram_unused() из <util/ram-usage.h>. Если программа её использует, код запуска в секции .init3 закрашивает ОЗУ от __heap_start до RAMEND значением 0xAA, а функция возвращает, сколько байт закраски осталось нетронутыми, — верхнюю оценку свободного места на момент вызова. Ограничения из документации: стек должен располагаться после статических данных и расти вниз, а с malloc функция несовместима.

#include <avr/io.h>
#include <util/ram-usage.h>
#include <stdio.h>

void report_ram(void)
{
    printf("RAM never touched: %u bytes\n", _get_ram_unused());
}

Cortex-M. Готовой функции в CMSIS нет, но её легко написать. Закрашивать нужно до того, как программа углубится в стек, — первой строкой main() или в своём Reset_Handler после обнуления .bss. Пример для раскладки без кучи, где свободное место — от конца .bss до вершины стека:

#include <stdint.h>
#include "stm32f4xx.h"

#define STACK_PAINT 0xA5A5A5A5u

extern uint32_t _ebss[];               /* end of .bss, from the linker script */

void stack_paint(void)                 /* first thing in main() */
{
    uintptr_t from = (uintptr_t)_ebss;
    uintptr_t to   = __get_MSP() - 64u;  /* keep clear of the current frame */
    for (uintptr_t a = from; a < to; a += 4u)
        *(volatile uint32_t *)a = STACK_PAINT;
}

uint32_t stack_free_bytes(void)        /* lowest untouched gap above .bss */
{
    uintptr_t a = (uintptr_t)_ebss;
    while (*(volatile const uint32_t *)a == STACK_PAINT)
        a += 4u;
    return (uint32_t)(a - (uintptr_t)_ebss);
}

Цикл поиска остановится на первом изменённом слове — это самая глубокая точка, до которой доходил стек. Значение 0xA5A5A5A5 выбрано потому, что оно маловероятно как адрес, счётчик или флаг. Если куча используется, закрашивают и проверяют только область между её текущей границей и стеком.

Раскраска показывает достигнутый максимум, а не возможный. Её результат зависит от того, какие ветки кода и какие комбинации прерываний встретились во время прогона. Поэтому стек проверяют после длительной работы во всех режимах, с нагрузкой на интерфейсы, и закладывают запас — обычно не меньше четверти от измеренного. Значение stack_free_bytes() полезно периодически выводить в журнал и в рабочей прошивке: если оно со временем уменьшается, где-то есть редкий глубокий путь.

Защита во время работы

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

Защитная область MPU

У ядер Cortex-M3, M4 и M7 с блоком защиты памяти (MPU) можно объявить под стеком небольшую область без доступа. Первая же запись в неё вызовет исключение MemManage — в момент переполнения, с адресом нарушения в регистре MMFAR. MPU есть не во всех микроконтроллерах: у STM32F103x4…xE его нет (__MPU_PRESENT = 0 в заголовках ST stm32f103x6.h, stm32f103xb.h, stm32f103xe.h), а у STM32F103xF и xG XL-плотности (stm32f103xg.h: __MPU_PRESENT = 1) и у STM32F4 — есть.

Параметры области у Cortex-M4 (DUI 0553B, 4.5): восемь областей, размер — степень двойки 2^(SIZE+1), минимум 32 байта; базовый адрес выровнен на размер области. Поле AP = 000 запрещает любой доступ, бит XN — выполнение команд. Бит PRIVDEFENA в MPU_CTRL оставляет для привилегированного кода карту памяти по умолчанию везде, где нет других областей, так что одной защитной области достаточно.

#include "stm32f4xx.h"

#define STACK_SIZE 0x400u               /* same as _Min_Stack_Size in the linker script */
#define GUARD_SIZE 32u                  /* 2^(4+1) */

extern uint32_t _estack[];

void mpu_stack_guard(void)
{
    uintptr_t guard = ((uintptr_t)_estack - STACK_SIZE - GUARD_SIZE)
                      & ~(uintptr_t)(GUARD_SIZE - 1u);   /* aligned to its size */

    MPU->CTRL = 0;                                       /* off while reconfiguring */
    MPU->RNR  = 0;                                       /* region 0 */
    MPU->RBAR = (uint32_t)guard;
    MPU->RASR = MPU_RASR_XN_Msk                          /* no execution */
              | (0u << MPU_RASR_AP_Pos)                  /* AP = 000: no access */
              | (4u << MPU_RASR_SIZE_Pos)                /* 32 bytes */
              | MPU_RASR_ENABLE_Msk;
    MPU->CTRL = MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk;
    SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;             /* MemManage, not HardFault */
    __DSB();
    __ISB();
}

При _estack = 0x20020000 (128 КБ ОЗУ с адреса 0x20000000) и стеке 0x400 защитная область займёт 0x2001FBE0–0x2001FBFF — сразу под стеком и над кучей. Её задевает и стек, растущий вниз, и куча, растущая вверх.

Две оговорки.

  1. Кадр функции больше защитной области может её перепрыгнуть. Если функция объявляет локальный массив на 200 байт, указатель стека уменьшится сразу на 200, и запись в массив придётся ниже защиты, в кучу. Размер защитной области выбирают не меньше самого большого кадра из .su, а ещё лучше — запрещают большие локальные массивы предупреждением -Wstack-usage.
  2. Обработчик ошибки сам работает на испорченном стеке. Кадр исключения при переполнении тоже пишется в защитную область; это отмечается флагом MSTKERR в регистре MMFSR, а ошибка может перейти в HardFault. Бит HFNMIENA по умолчанию равен нулю, и в обработчике HardFault MPU выключен (DUI 0553B, 4.5.2), так что обработчик выполнится, но стек под ним — чужая память. Поэтому обработчик делает минимум: сохраняет регистры в .noinit и сбрасывает микроконтроллер.

Предел стека в ARMv8-M

У ядер ARMv8-M с расширением Main (например, Cortex-M33) есть регистры предела стека MSPLIM и PSPLIM: попытка опустить указатель стека ниже предела вызывает исключение, и переполнение ловится аппаратно без MPU. В CMSIS для этого есть __set_MSPLIM(), и его вызывают в коде запуска значением нижней границы стека. У Cortex-M23 без расширения Main запись в MSPLIM игнорируется (CMSIS, cmsis_gcc_m.h). У новых AVR с регистром SPLIM avr-libc сама записывает в него __heap_start − 1 в секции .init2.

Проверочное слово

Где нет ни MPU, ни предела стека, остаётся программная проверка: на нижнюю границу стека кладут известное слово и периодически проверяют, цело ли оно — например, в основном цикле перед сбросом сторожевого таймера. Это ловит переполнение не в момент записи, а при следующей проверке, но по крайней мере превращает «случайный сбой» в зарегистрированное событие. Тот же принцип в каждой функции реализует ключ GCC -fstack-protector: в функции с буферами от 8 байт и с alloca компилятор кладёт проверочное значение и сверяет его на выходе. В прошивке без операционной системы функцию, которую вызывает неудачная проверка, и само проверочное значение должна предоставить программа.

HardFault: что смотреть

Когда что-то пошло не так на Cortex-M, обработчик ошибки получает главное — кадр стека, который ядро сохранило при входе. В нём восемь слов: R0, R1, R2, R3, R12, LR, PC, xPSR. Для точных ошибок PC — адрес команды, на которой произошла ошибка. Исключение — неточная ошибка шины (флаг IMPRECISERR в CFSR): запись в память завершилась ошибкой уже после того, как процессор ушёл дальше, и сохранённый PC с виноватой командой не связан (DUI 0552A, 4.3.10); виновника такой ошибки ищут среди обращений к памяти перед сохранённым адресом. Какой стек использовался, показывает бит 2 кода EXC_RETURN в LR: 0 — основной (MSP), 1 — стек процесса (PSP), как у задач RTOS. Подробнее о кадре — в глоссарии: кадр стека исключения.

#include <stdint.h>
#include "stm32f4xx.h"

typedef struct {
    uint32_t magic;                       /* marks a valid record after reset */
    uint32_t pc, lr, cfsr, hfsr, mmfar, bfar;
} fault_info_t;

fault_info_t fault_info __attribute__((section(".noinit")));

__attribute__((used))                     /* only referenced from asm: keep it with LTO */
void hardfault_c(const uint32_t *frame)
{
    fault_info.pc    = frame[6];          /* stacked PC */
    fault_info.lr    = frame[5];          /* stacked LR */
    fault_info.cfsr  = SCB->CFSR;
    fault_info.hfsr  = SCB->HFSR;
    fault_info.mmfar = SCB->MMFAR;        /* valid only if MMARVALID is set in CFSR */
    fault_info.bfar  = SCB->BFAR;         /* valid only if BFARVALID is set in CFSR */
    fault_info.magic = 0xFA017EADu;
    NVIC_SystemReset();
}

__attribute__((naked)) void HardFault_Handler(void)
{
    __asm volatile(
        "tst   lr, #4      \n"            /* EXC_RETURN bit 2: frame on MSP or PSP */
        "ite   eq          \n"
        "mrseq r0, msp     \n"
        "mrsne r0, psp     \n"
        "b     hardfault_c \n");
}

Атрибут naked убирает пролог, чтобы указатель стека не сдвинулся до того, как его прочитают; в такой функции допустим только ассемблер. Запись в .noinit переживает сброс, и после перезапуска программа может отправить её в журнал. Регистры состояния ошибок лежат по адресам CFSR — 0xE000ED28, HFSR — 0xE000ED2C, MMFAR — 0xE000ED34, BFAR — 0xE000ED38 (DUI 0552A, табл. 4-12). Сохранённый PC ищут в map-файле или листинге objdump -d, а подробная расшифровка битов CFSR — в глоссарии: HardFault.

Куча и malloc

Динамическая память на микроконтроллере опасна не сама по себе, а тремя свойствами:

  • Фрагментация. После тысяч выделений и освобождений разного размера свободная память дробится на куски, и запрос на 100 байт может не выполниться при 500 свободных. В программе, которая работает месяцами, это вопрос времени. Документация avr-libc отдельно предупреждает о фрагментации кучи со временем.
  • Встреча со стеком. Куча растёт вверх навстречу стеку. В avr-libc malloc при __malloc_heap_end = 0 не поднимает кучу выше текущего указателя стека минус __malloc_margin — по умолчанию 32 байта (malloc.dox); этого хватает, только если все прерывания и вложенные вызовы после malloc укладываются в эти 32 байта. Функция _sbrk() из примеров ST отказывает в памяти, если куча поднялась бы выше текущего SP, — но проверка выполняется в момент выделения, а стек может вырасти позже.
  • Неопределённое время. Время malloc зависит от состояния кучи, и в обработчиках прерываний и жёстких по времени участках её не вызывают.

Практические правила:

  1. Выделять память только при запуске и никогда не освобождать — тогда фрагментации нет, а нехватка проявится сразу при первом включении.
  2. Для объектов, которые создаются и удаляются в работе (сообщения, буферы пакетов), использовать пулы блоков фиксированного размера: массив одинаковых блоков и список свободных. Выделение и освобождение занимают постоянное время, фрагментации нет.
  3. Всегда проверять результат malloc на NULL и решать, что делать при отказе, ещё при проектировании.
  4. Учитывать кучу при раскраске: если она есть, раскрашивают и проверяют только промежуток между её границей и стеком.

Другие ошибки памяти

Переполнение стека — не единственная ошибка, которая портит память молча:

  • Выход за границы массива — запись в buf[len] вместо buf[len − 1], копирование строки без проверки длины. Портит соседнюю переменную в .bss так же, как стек.
  • Указатель на локальную переменную, возвращённый из функции или сохранённый в глобальной структуре: после выхода из функции на этом месте окажется кадр другой функции.
  • Невыровненное обращение: Cortex-M0 и M0+ не умеют читать слово по адресу, не кратному 4, и вызывают HardFault; M3 и M4 умеют, но не для всех команд — подробности в глоссарии: невыровненный доступ.
  • Обращение к памяти, которой нет, — указатель, не инициализированный или испорченный: на Cortex-M — BusFault или HardFault, на AVR — тихое чтение регистров ввода-вывода или чужой памяти.

Логику, не завязанную на регистры, — разборщики, буферы, расчёты — выгодно прогонять на компьютере с ключами GCC -fsanitize=address,undefined: санитайзер адресов ловит выход за границы массивов в момент обращения, а санитайзер неопределённого поведения — переполнения знаковых чисел и неверные сдвиги. Как вынести такой код в тесты, рассказано в статье о модульном тестировании прошивки.

Типичные ошибки

  • Размер стека «на глаз» — 0x400 из шаблона ST не значит ничего, пока не посчитана самая глубокая цепочка с прерываниями.
  • Большой локальный массив — буфер на 512 байт в функции разбора пакета съедает четверть ОЗУ ATmega328P и перепрыгивает любую защитную область. Буферы такого размера делают статическими.
  • Рекурсия — глубина зависит от данных; в прошивках её заменяют циклом или явным стеком с ограничением.
  • printf с плавающей точкой в обработчике или глубоко в стеке — у полноценной реализации кадр в сотни байт.
  • Не учтён FPU: на Cortex-M4F прерывание кода, который пользовался FPU, кладёт кадр в 104 байта, а не 32.
  • Раскраска проверена после короткого прогона — редкий глубокий путь (ошибка связи, длинный пакет, вложенное прерывание) не встретился.
  • Защитная область MPU меньше самого большого кадра — переполнение проходит мимо неё.
  • malloc и free в рабочем цикле — фрагментация через часы или дни работы.

Частые вопросы

Как узнать, сколько стека использует прошивка?

Двумя способами вместе. До запуска — ключом GCC -fstack-usage: он пишет для каждой функции размер её кадра в файл .su, а сумма по самой глубокой цепочке вызовов плюс кадры прерываний даёт оценку сверху. Во время работы — раскраской: ОЗУ между данными и стеком заполняют известным значением, а через время смотрят, сколько слов осталось нетронутыми. На AVR это делает _get_ram_unused() из avr-libc 2.3.

Почему при переполнении стека STM32 не падает сразу?

Потому что ниже стека лежат обычные данные — куча или переменные из .bss, — и запись туда ничем не запрещена. Ошибка проявляется позже, когда испорченная переменная используется или функция возвращается по затёртому адресу. Сразу поймать переполнение позволяет защитная область MPU под стеком (у STM32F4, F7, H7 и других ядер с MPU) или регистр MSPLIM у ядер ARMv8-M, например Cortex-M33.

Можно ли пользоваться malloc на микроконтроллере?

Можно, но с ограничениями: выделять память лучше один раз при запуске и не освобождать. При постоянных malloc и free куча фрагментируется, и через часы работы запрос на 100 байт может не выполниться при 500 свободных. Кроме того, куча растёт навстречу стеку, а время работы malloc зависит от состояния кучи. В прошивках чаще используют статические массивы и пулы блоков фиксированного размера.

Сколько стека нужно обработчику прерывания на Cortex-M?

Ядро само кладёт в стек кадр из восьми слов — 32 байта, а если в прерванном коде был активен контекст FPU (бит CONTROL.FPCA), то 104 байта; использование FPU самим обработчиком увеличивает только кадры прерываний, которые вытеснят уже его; плюс возможное выравнивание на 4 байта и собственный кадр обработчика из .su. При вложенных прерываниях разных приоритетов кадры складываются: стек считают для самой глубокой цепочки вложений.

Что смотреть в HardFault, чтобы понять причину?

Сохранённый ядром кадр стека: в нём PC и LR. Для точных ошибок PC указывает на команду, вызвавшую ошибку; при неточной ошибке шины (флаг IMPRECISERR) — адрес возврата с виновной командой не связан, и её ищут среди предшествующих обращений к памяти. Затем регистры CFSR (0xE000ED28) и HFSR (0xE000ED2C): по их битам видно тип ошибки — доступ к запрещённой памяти, ошибка шины, неверная команда, деление на ноль, — а MMFAR и BFAR при установленных флагах достоверности хранят адрес обращения. Адрес PC ищут в map-файле или листинге.

Источники

  1. GCC 16.2 Manual: Developer Options (-fstack-usage, -fcallgraph-info), Warning Options (-Wstack-usage, -Wframe-larger-than), Instrumentation Options (-fstack-protector, -fsanitize), ARM Attributes (naked) — -fstack-usage пишет файл .su: функция, число байт, квалификаторы static, dynamic, bounded; -fcallgraph-info=su — граф вызовов с размерами стека, с -flto не выводится; -Wstack-usage=N предупреждает, если стек функции может превысить N, учитывая alloca и массивы переменной длины, формы сообщений «stack usage is», «might be», «might be unbounded»; -Wframe-larger-than — приблизительная оценка только кадра; -fstack-protector — проверочное значение в функциях с буферами от 8 байт и с alloca; -fsanitize=address и undefined; naked — в теле только базовый asm
  2. AVR-LibC 2.3.2: <util/ram-usage.h>, Memory Areas and Using malloc() (malloc.dox), Memory Sections (sections.dox), crt1/gcrt1.S — _get_ram_unused() с версии 2.3: код запуска в .init3 закрашивает ОЗУ от __heap_start до RAMEND значением __ram_color_value (по умолчанию 0xaa), функция возвращает верхнюю оценку свободного места; предполагается стек после статических данных, растущий вниз, несовместимо с malloc; malloc: при __malloc_heap_end = 0 куча не поднимается выше текущего SP минус __malloc_margin, по умолчанию 32 байта; фрагментация; на устройствах с регистром SPLIM код запуска записывает в него __heap_start − 1
  3. Arm. Cortex-M4 Devices Generic User Guide, DUI 0553B; Cortex-M3 Devices Generic User Guide, DUI 0552A — DUI 0553B, 4.5 — MPU: восемь областей; MPU_CTRL: ENABLE, HFNMIENA (при 0 MPU выключен в обработчиках HardFault, NMI и при FAULTMASK), PRIVDEFENA (карта по умолчанию как фоновая область для привилегированного кода); MPU_RBAR: адрес выровнен на размер области; MPU_RASR: XN, AP, SIZE (размер 2^(SIZE+1), минимум 32 байта при SIZE = 4), ENABLE; AP = 000 — нет доступа; MMFSR: MSTKERR — ошибка MemManage при записи кадра исключения, MMARVALID и MMFAR; DUI 0552A, табл. 4-12 — CFSR 0xE000ED28, HFSR 0xE000ED2C, MMFAR 0xE000ED34, BFAR 0xE000ED38; 4.3.10 — при IMPRECISERR адрес возврата в кадре не связан с командой, вызвавшей ошибку; EXC_RETURN с битом 2 = 1 — возврат на PSP
  4. Arm. ARMv7-M Architecture Reference Manual, DDI 0403E.e, 2021 — B1.5.6 — стек полный нисходящий, кадр исключения 0x20 байт (R0–R3, R12, LR, PC, xPSR); расширенный кадр 0x68 с регистрами FPU, если CONTROL.FPCA = 1 в прерываемом контексте; возможно выравнивание ещё на 4 байта; B1.4.4 — CONTROL.FPCA: контекст FPU активен
  5. Arm. CMSIS 6.4.0: cmsis_gcc_m.h; CMSIS 5.9.0: core_cm4.h — __get_MSP(), __set_MSPLIM() — запись MSPLIM только у ARMv8-M с расширением Main, у Cortex-M23 игнорируется; MPU->RNR, RBAR, RASR, CTRL, MPU_RASR_XN_Msk, MPU_RASR_AP_Pos = 24, MPU_RASR_SIZE_Pos = 1, MPU_RASR_ENABLE_Msk, MPU_CTRL_PRIVDEFENA_Msk, MPU_CTRL_ENABLE_Msk, SCB_SHCSR_MEMFAULTENA_Msk, SCB_CFSR_MSTKERR_Msk; SCB->CFSR, HFSR, MMFAR, BFAR; NVIC_SystemReset()
  6. STMicroelectronics. cmsis-device-f1, коммит c8e9a4a: STM32F103XB_FLASH.ld, stm32f103xb.h; cmsis-device-f4: stm32f411xe.h; STM32CubeF1, коммит c7656f1: syscalls.c — _Min_Heap_Size = 0x200 и _Min_Stack_Size = 0x400 только резервируют место в секции ._user_heap_stack для проверки компоновщиком; символ end — начало кучи; __MPU_PRESENT = 0 в stm32f103x6.h, stm32f103xb.h, stm32f103xe.h и 1 в stm32f103xg.h и stm32f411xe.h; _sbrk() возвращает −1 и ENOMEM, если куча поднялась бы выше текущего SP
  7. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet, DS40002061B, 2020 — 7.5 — стек растёт вниз от RAMEND; 7.7 — при входе в прерывание в стек кладётся счётчик команд (два байта), при RETI он извлекается; SRAM ATmega328P — 0x0100…0x08FF

Нашли ошибку?