1. Тема дня

Сегодня разбираем диагностику аварий, возникающих из-за:

text
невалидного указателя
выхода за границы массива
переполнения стека
use-after-free
double free
повреждения heap
неправильного DMA-буфера
ошибочного адреса периферии
гонки данных
исполнения повреждённого кода

Для STM32:

text
HardFault
MemManage
BusFault
UsageFault
CFSR / HFSR
BFAR / MMFAR
MSP / PSP
exception stack frame
PC / LR / xPSR

Для ESP32:

text
Guru Meditation
register dump
backtrace
EXCCAUSE / EXCVADDR
Core Dump
GDB Stub
heap poisoning
hardware watchpoints

Главная мысль: Инструкция, на которой процессор упал, часто является только первой инструкцией, заметившей повреждение. Реальная ошибка могла произойти значительно раньше — при записи за границу буфера, освобождении объекта или работе DMA.

2. Зачем это нужно в твоих проектах

AT-парсер EC25 может повредить память из-за слишком длинного URC, неверной длины MQTT payload, строки без \0 или хранения указателя на уже перезаписанный DMA/ring buffer. Для DMA/ADC возможен сценарий:

text
ADC DMA пишет 512 samples
массив выделен только на 256 samples
DMA портит соседнюю структуру FreeRTOS
следующее переключение задач вызывает HardFault

Для CAN/RS-485 опасны неверные DLC/length, CRC за пределами кадра, указатель на stack buffer в queue. Crash должен превращаться в артефакт:

text
firmware version
Git SHA
reset reason
fault registers
PC/LR
task name
stack watermark
последние diagnostic events
точный ELF релиза

3. Теория

4.3.1 3.1. Три момента ошибки

text
1. Момент повреждения.
2. Момент обнаружения.
3. Момент падения.

Падение в free() или scheduler часто означает, что память была повреждена раньше.

4.3.2 3.2. Cortex-M faults

text
MemManage:
  нарушение MPU, запрещённый доступ, исполнение из XN-region.
BusFault:
  ошибка шины, отсутствующая память, ошибка Flash/RAM/peripheral access.
UsageFault:
  undefined instruction, invalid state, деление на ноль, unaligned access.

HardFault часто является эскалацией другого fault. Если HFSR.FORCED = 1, расшифровывай CFSR.

4.3.3 3.3. Главные fault-регистры STM32

c
uint32_t cfsr  = SCB->CFSR;
uint32_t hfsr  = SCB->HFSR;
uint32_t dfsr  = SCB->DFSR;
uint32_t afsr  = SCB->AFSR;
uint32_t bfar  = SCB->BFAR;
uint32_t mmfar = SCB->MMFAR;
uint32_t shcsr = SCB->SHCSR;

CFSR состоит из:

text
bits 0-7: MMFSR
bits 8-15: BFSR
bits 16-31: UFSR

BFAR и MMFAR используются только если установлены valid-флаги.

4.3.4 3.4. Exception stack frame

Cortex-M автоматически сохраняет:

text
R0
R1
R2
R3
R12
LR
PC
xPSR

PC — адрес аварийной инструкции, LR помогает понять вызов, R0-R3 часто содержат проблемный указатель или длину.

4.3.5 3.5. HardFault wrapper

c
void hardfault_capture(uint32_t *stack_pointer, uint32_t exc_return);
__attribute__((naked))
void HardFault_Handler(void)
{
    __asm volatile(
        "tst lr, #4            \n"
        "ite eq                \n"
        "mrseq r0, msp         \n"
        "mrsne r0, psp         \n"
        "mov r1, lr            \n"
        "b hardfault_capture   \n"
    );
}

EXC_RETURN bit 2 выбирает MSP/PSP. При FPU нужно учитывать extended frame.

4.3.6 3.6. Поиск строки по PC

text
arm-none-eabi-addr2line \
  -e build/firmware.elf \
  -f \
  -C \
  0x08012A9E

Используй точный ELF именно той сборки, которая упала.

4.3.7 3.7. ESP32 Guru Meditation

Типовой вывод:

text
Guru Meditation Error:
Core 1 panic'ed (LoadProhibited)
PC      : ...
A0/A1   : ...
EXCCAUSE: ...
EXCVADDR: ...
Backtrace: ...

EXCVADDR = 0x00000000 часто означает NULL pointer, 0x00000010 — поле структуры через NULL+offset.

4.3.8 3.8. ESP32 Core Dump

Core Dump сохраняет регистры, стеки задач, TCB и список задач.

text
idf.py coredump-info
idf.py coredump-debug

В GDB:

text
info registers
info threads
thread apply all bt
bt
frame 0
info locals

4.3.9 3.9. Heap poisoning и integrity checkpoints

ESP-IDF может использовать heap poisoning. Характерные значения:

text
0xABBA1234: head canary
0xBAAD5678: tail canary
0xCECECECE: неинициализированная память
0xFEFEFEFE: освобождённая память

Проверки:

c
heap_caps_check_integrity_all(true);

Их можно временно ставить до/после подозрительных участков.

4.3.10 3.10. Hardware watchpoint

Если известен адрес, который кто-то портит:

c
watch *(uint32_t *)0x20001000

CPU остановится на инструкции, которая реально изменила этот адрес.

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

text
1. Смотреть только на строку crash.
2. Использовать ELF другой версии.
3. Безусловно доверять BFAR/MMFAR.
4. Не учитывать FPU extended frame.
5. Делать printf() внутри HardFault.
6. Записывать Flash из fault handler.
7. Не проверять stack pointer перед чтением frame.
8. Игнорировать imprecise BusFault.
9. Исправлять проблему добавлением задержки.
10. Не архивировать crash artifacts.

5. Практическое задание на 30-60 минут

Создай CRASH_DEBUG_POLICY.md:

markdown
# Crash debugging policy
1. Every production build archives ELF, MAP and sdkconfig.
2. Every crash contains firmware version and Git SHA.
3. STM32 faults save CFSR, HFSR, BFAR, MMFAR and stacked PC/LR.
4. ESP32 production builds provide panic output or Core Dump.
5. Fault handlers do not allocate memory or use blocking APIs.
6. Flash is not written directly from an unsafe fault context.
7. Known corrupt addresses are investigated with watchpoints.
8. Heap corruption is narrowed using integrity checkpoints.
9. Crash reproducers become unit or HIL regression tests.
10. A delay is never accepted as a final race-condition fix.

Для STM32: добавь HardFault wrapper, вызови controlled fault, найди PC через addr2line. Для ESP32: включи core dump, вызови controlled assert, прочитай idf.py coredump-info.

6. Что почитать или попробовать дальше

  • ESP-IDF Fatal Errors, Core Dump, Heap Memory Debugging, JTAG Debugging.
  • ST materials по HardFault debugging.
  • ARM Cortex-M programming manual для своего ядра.

Короткий итог

text
crash detected
→ сохранить firmware identity
→ определить fault class
→ получить stacked PC/LR
→ проверить CFSR/HFSR и fault address
→ найти строку через exact ELF
→ проверить disassembly и operands
→ понять: причина здесь или память испорчена раньше
→ integrity checks / MPU / watchpoint
→ reproducer
→ regression test

Задание

Crash произошёл в scheduler после ADC DMA transfer. Объясните проверку гипотезы раннего повреждения по lengths, ownership и сохранённому build evidence.

Критерии самопроверки: Не считать crash line первопричиной; указать reproducer и наблюдаемое bounds/ownership violation.

Показать ответ автора

Сопоставить configured transfer count с allocated destination capacity и lifetime, проверить соседнюю память/integrity checkpoints и разобрать crash по точному сохранённому ELF. Scheduler crash может быть поздним симптомом ранней DMA write.

Задание

Перед переносом исходного HardFault wrapper на другой Cortex-M перечислите compatibility checks для безопасного чтения exception frame.

Критерии самопроверки: Не заявлять применимость одного wrapper ко всем Cortex-M variants. Явно обозначить target-specific assumptions и избегать Flash writes или сложного logging в fault handler.

Показать ответ автора

Проверить actual core/exception architecture, выбор MSP/PSP через EXC_RETURN, FPU/extended-frame behavior и stack-pointer validity. Разбирать только поддерживаемые target fault registers и valid fault-address fields.