Хотел бы поделиться историей расследования сложного бага...
С некоторого момента появился довольно неприятный баг в системе Caustic. В определенные моменты, очень редко происходит Hard Fault - прерывание, которое случается, когда что-то идет совсем не так. Например, запись по невалидному адресу, или передача управления области памяти, в которой выполнение кода запрещено. Причем Hard Fault происходит сам собой в непредсказуемом месте. Воспроизвести специально - не получается, баг может не проявляться часами. Известно только, что если создать большую нагрузку из сетевых и bluetooth-пакетов, он проявляется с бОльшей вероятностью.
После долгого анализа кода до меня дошло, что не так. Я использую в коде стандартные контейнеты STL (vector, list, map, queue, string и т.п.). Кроме того, я иногда явно использую new/delete, умные указатели и т.п. Короче говоря, использую динамическую память. Также, её используют все стандартные контейнеры STL. Делать так - хорошая практика, STL позволяет писать меньше кода, не изобретать велосипеды, совершать меньше ошибок (STL идеально отлажена) и экономить оперативную память.
Работа с динамической памятью в облегченной версии libc для микроконтроллера (эта версия называется newlib) осуществляется через функцию-драйвер _sbrk. Этот драйвер реализует пользователь (у меня вот так:
https://github.com/DAlexis/caustic-lase ... lib.c#L109 ) Когда libc считает, что уже выделенного хипа мало, она просто вызывает _sbrk(количество_байт_которые_нужно_выделить). Думаю практически все, кто программирует stm32 под GCC, это знают. Структура хипа (где какие фрагменты памяти сейчас свободны или заняты) находится в ведении libc, реализовывать самому это не нужно, это и так сделано хорошо.
Пока все в порядке. Точно так же libc работает и на компьютере, _sbrk там - по-сути просто системный вызов. Причем new/delete (malloc/free) всегда потокобезопасны под Windows и под Linux. Тут начинается самое интересное: про FreeRTOS системная библиотека ничего не знает. Поэтому крайне редко, но случается ситуация, когда один поток выполняет, например, операцию new, не успевает её закончить, и управление передается другому потоку, который тоже вызывает new/delete. Обыкновенный data race, структура хипа портится, и программа идет в разнос: возникают невалидные адреса, переписывается чужая память и случается hard fault.
Очевидное решение - защитить heap мьютексами. В C++ возможно глобальное переопределение операций new и delete. Я добавил примерно следующий код:
Код:
void * operator new(std::size_t n)
{
ScopedLock<Mutex> lck(Kernel::heapMutex);
Kernel::heapAllocatedTotal += n;
return malloc(n);
}
void operator delete(void * p)
{
ScopedLock<Mutex> lck(Kernel::heapMutex);
free(p);
}
void *operator new[](std::size_t n)
{
ScopedLock<Mutex> lck(Kernel::heapMutex);
Kernel::heapAllocatedTotal += n;
return malloc(n);
}
void operator delete[](void *p) throw()
{
ScopedLock<Mutex> lck(Kernel::heapMutex);
free(p);
}
Здесь ScopedLock - полный аналог unique_lock на компьютере, просто RAII-холдер, а Mutex - класс-обертка для мьютекса во FreeRTOS