Комментарии 66
Сам когда занимался это проблемой, поступил следующим образом.
В самом начале записывал во весь стек «guard variable», а потом в каком-нибудь таймере периодически проверял начало стека на это число.
Так можно было кстати после продолжительной работы подключиться отладчиком и посмотреть сколько у нас «guard variable» осталось в стеке и какой запас еще есть.
Это можно сделать командой и даже прописать в автозапуск с помощью .ini-файла
Что за ini файл?
1. До сработки проверки по таймеру можно и не дожить, т.к. система уже ушла в разнос.
2. Ну поймали вы разрушение гарда по таймеру, дальше что? У вас нет стектрейса, чтобы понять, в какой момент это случилось.
3. Пусть даже вы дожили до 2. У вас ровно один вариант действий — увеличивать стек (на сколько?) Так может просто сразу увеличить стек по максимуму, особенно если вы не юзаете кучу? И надеяться, что хватит…
Но в большинстве случаев стектрейс и не нужен ( если конечно вы в стеке случайно не выделяете большой массив). Иногда достаточно просто знать что стека мало и нужно добавить еще или просто посмотреть сколько запаса осталось.
Что за ini файл?
В Кейле есть окно команд, в котором внезапно можно вводить команды — типа, добавить переменную в watch, поставить брейкпоинт, руками записать что-нибудь в память и тому подобное.
Если хочется, чтобы пачка команд запускалась при каждом запуске отладки, то ее можно засунуть в текстовый файл с расширением .ini, который нужно прописать во вкладке Options->Debug->Initialization File.
До сих пор удивляюсь, почему в CPU для embedded без MMU нету например вот такого решения stack limit.
Аппаратная проверка на каждом push/pop (STM/LDR для Cortex-M) вместо программных костылей imho намного эффективнее.
Кстати решение с переносом стека в начало RAM работает только если стек один. Когда на борту RTOS данный подход не поможет.
-finstrument-functions к сожалению также не совсем панацея (разве что вместе с проверкой границ стека во время context switch) так как позволяет проверять стек только на границах функции и если переполнение произошло по середине с последующим pop то содержимое памяти уже повреждено, а мы об этом не знаем :(
А в целом статья поднимает интересную тему, спасибо.
UPD: ARMv8-M поддерживает stack limit. Ждем STM32 на новой архитектуре
До сих пор удивляюсь, почему в CPU для embedded без MMU нету например вот такого решения stack limit.
Да, я тоже удивляюсь. Вроде бы даже на PIC'ах stack limit есть аппаратно.
Кстати решение с переносом стека в начало RAM работает только если стек один. Когда на борту RTOS данный подход не поможет.
-finstrument-functions к сожалению также не совсем панацея (разве что вместе с проверкой границ стека во время context switch) так как позволяет проверять стек только на границах функции и если переполнение произошло по середине с последующим pop то содержимое памяти уже повреждено, а мы об этом не знаем :
Собственно, проблемы-то разные. Одно дело — вылезание за границы стекового кадра, от чего можно частично защититься --stack-protect'ом, а другое — вылезание за границы стека вообще. Но их можно применять одновременно.
Другое дело, что если в коде есть, допустим, ассемблерная вставка, которая просто лезет куда-нибудь в стек, то тут только MMU спасет. Но если программист так делает, то тут уж он сам должен думать.
А так — хотя бы от глупых ошибок огородиться — уже хорошо.
Как эту проблему на RTOS решать я пока особо не думал. По идее само по себе переключение контекста выход за границы стека принести не должно; только если вы уже вылезли за его границы и такой контекст сохранился. Но это должны отловить instrument-functions.
При аллокации стека к размеру добавляем footer в котором содержится некий magic number. При переключении контекста начало и размер стека известны, соответственно можно проверить значение в футере и таким образом предположить было ли переполнение.
Как эту проблему на RTOS решать я пока особо не думал.Во FreeRTOS, например, есть собственные средства контроля использования стеков задач. Включается макросом
#define INCLUDE_uxTaskGetStackHighWaterMark 1
Затем можно вызывать функциюuxTaskGetStackHighWaterMark()
передавая в качестве параметра handle задачи, для контроля использования стека задачей. Полагаю, в других RTOS тоже должно быть нечто подобное.
С другой стороны, все это делается только на этапе отладки, во время разработки. Заполнил стек спец. символами, поставил на стресс тест на 2 дня, и через два дня посомтрел до куда максмимум стек долез. В любом случае в релизе нужно сделать так, чтобы максимальный размер уже был известен, это может кстати сделать компилятор, посчитать сразу максимальную вложенность стека. На неё и стоит оритентироваться.
Если по теме — проблема актуальная, но интрументацией она вроде неплохо решается на этапе отладки. Переполнение стека, на мой взгляд, это логическая ошибка (программист не расчитал нужный размер). Понятно, что в сложных системах эта величина трудно предсказуема, однако это не снимает с разработчика ответственности. Стек это такой же ресурс, как и всё остальное и в эмбедде надо чотко следить за всем.
Первый скриншот: безуспешно пытался понять, как можно нажать кнопку «Ok» на окне, где есть только «Break», «Continue» и «Ignore».
Упс. Да, действительно.
Если по теме — проблема актуальная, но интрументацией она вроде неплохо решается на этапе отладки. Переполнение стека, на мой взгляд, это логическая ошибка (программист не расчитал нужный размер). Понятно, что в сложных системах эта величина трудно предсказуема, однако это не снимает с разработчика ответственности. Стек это такой же ресурс, как и всё остальное и в эмбедде надо чотко следить за всем.
Отчасти вы, конечно, правы. Вот только инструментарий, на мой взгляд, как раз недостаточен. Все известные мне анализаторы стека либо делают это статически, но не учитывают пресловутые виртуальные методы и указатели на функции, либо делают это динамически. По факту, такой динамический инструмент я и предлагаю.
Просто получить ассерт при переполнении стека — это ведь не решение проблема, это только способ узнать, что проблема вообще есть.
Если вам известен какой-то другой способ заранее узнать нужный размер стека — поделитесь, пожалуйста.
Интересная идея, но кажется Кейл так сам не умеет.
— сколько каждая ф-ция потребляет стека
— вложенность ф-ций
— уровень прерываний
и… и всё
Как вы думаете, почему -fstack-usage это не считает?
просто надо сравнить асм код вызова обычного метода класса против виртуального и вызов функции по имени против вызова по указателю
Поэтому полностью обезопаситься можно только динамически — с помощью MMU или проверок, которые я предлагаю.
стек то тут при чем???
Причем тут генерируемый код? Если у нас вызов ф-ции идет по указателю, а указатель подбирается в программе динамически.
Как это причем тут стек? Смотрите:
void foo(); // использует чуть-чуть стека
void bar(); // использует очень много стека
void main()
{
typedef void (*Func)();
Func func[] = {&foo, &bar};
int a;
scanf("%i", &a );
func[a]();
}
Как компилятор должен во время компиляции узнать, сколько стека будет использовано?
а для if(x) foo() else bar(); — есть отличие ???
И что тогда делать? Посчитать максимум из всех возможных вызовов всех существующих в проекте функций? Теоретически, конечно, можно, только такая оценка будет слишком завышенной.
Не забываем еще про рекурсию, глубина которой тоже может определятся во время выполнения.
рассчитывать во всем на компилятор… вы и есть [стек] за меня будете? (с) мультик
рекурсия запрещена в таких задачах
Я не спорю, что путем самоограничений можно свести задачу к решаемой статически — если уж вы в «таких задачах» запретили рекурсию, то можно и виртуальные вызовы запретить.
Но меня интересовало более универсальное решение.
Если же виртуальные вызовы оставить, то высчитывать максимально возможное использование стека придется ручками. Ручками опять-таки не хочется.
void a() { /* мало стека */ }
void b() { /* много стека */ }
void c(int x)
{
if (x) a();
else b();
}
void d() { /* мало стека */ с(0); }
void e() { /* много стека */ c(1); }
Сейчас компилятор посчитает за максимально «большой» вызов e() -> c() -> b(), хотя он по логике программы невозможен.
Конечно, стек все равно может переполнится ДО вызова инструментальной функции, поэтому при проверке можно сделать некоторый запас.
С другой стороны, даже в этом случае проверка сработает корректно (а в stm32 все равно будет HardFault).
Но это вроде как ничем не гарантируется, просто это более-менее логично — можно сэкономить на инструкциях. А выделять-освобождать память в стеке несколько раз — зачем?
По идее, какая-нибудь alloca или VLA могут хапать еще стека уже после первоначального выделения стекового кадра.
Но alloca я не использую (потому что Кейловская реализация использует кучу), а VLA Кейл просто в куче выделяет.
оптимизацию почти никогда не включаю
Тут нет опечатки? А смысл? Избежать мифической ошибки компилятора?
а оптимизацию почти никогда не включаю
извиняюсь, а почему не включаете? это как то связано с стеком?
Если программа и на -О0 укладывается в рамки по размеру/времени работы, то зачем ее оптимизировать?
Но есть задачи такие что даже с максимальным уровнем оптимизации -О3 могут потребовать ещё более глубокой настройки компилятора и более пристального изучения архитектуры.
Например распознавание образов на лету, и в качестве примера нахождение трёх точек расположенных правильным треугольником:
www.youtube.com/watch?v=210UZUjZwBs
Благодаря хорошему использованию и компилятора и использованию особенности АРМ проца я смог добиться работы самого алгоритма за 100 микросекунд (именно микросекунд). А всё в целом включая работу с камерой и рендер 10мс. На -О0 и не использовании фишек проца это было слайдшоу менее 1фпс. Основная фишка — использование не 65кбайт RGB данных, а 4 килобайт битовой маски и спец битовые инструкции ARM по анализу и манипулированию битами в 32битном слове. (естественно не вляпавшись ни разу в написание чего либо на ассемблере)
Например сейчас я делаю нейросетку MobilNetv2 на мк,
распознаёт до 1000 объёктов с камеры.
image.prntscr.com/image/cXQdL9yKT9ijUf7q0PiqiQ.png
и хоть микроконтроллер более мощный с очень быстрой памятью
(вырезка из моей статьи с японского хабра)
prnt.sc/l1qvh8
вот там даже на уровне оптимизации -О3 один кадр рассчитывается за 30 секунд потому что там 300 миллионов умножений флоатов с накоплениями, более 3 миллионов весовых коэффицентов 2д КИХ фильтров.
после профилирования компилятора и грамотной настройки под каждую функцию отдельно и адаптации под кеши и виды памяти ужалось до 4-5 секунд, не меняя исходный код (50 мегабайт исходного кода самой сетки), обеспечивая бит — в бит сходство с прогой на ПК на каждом этапе расчётов.
Вот что значит уметь пользоваться компилятором и процом и архетектурой мк!
Но это ещё не всё, конуренты умудрились довести до 1-2фпс ту же самую сетку уже алгоритмической оптимизацией на том же железе (альфазакон — 8 битные флоаты и пурифинг). Так что есть куда рости в проф мастерстве.
Надеюсь заинтересовал в более углублённом изучении GCC и ядра процессора.
Это зависит от разного рода задач. Для других же быстрее — значит
- Обработать бОльшее количество данных за то же время, т.е. пропускная способность
- Быстрее уйти в сон — снижение энергопотребления
- Уложиться в тесные временные рамки. Для реалтайма не то, что -O3, часто приходится вручную читать выхлоп ассемблера, находя узкие места и тупые решения компилятора.
Кстати забавная особенность: на высоких уровнях оптимизации размер используемого стека может сокращаться (поскольку компиль вырезает хранение промежуточных результатов, инлайнит функции и творит прочие непотребства), поэтому на простеньких камнях это может оказаться существенно
Насчет стека — тоже неоднозначно. Я видел, как на -О3 распухает стек для main'a, потому что в него вся инициализация заинлайнилась. А потом этот стек так и остается съеденным, потому что main никогда не завершается, а компилятору это невдомек.
Я начал читать лекции в детском саду.
А вы еще опции компиляции только для себя открываете.
ЗЫ. Реалтайм это не про скорость исполнения.
ЗЫ2. gcc -O3 это путь к очень интересным багам, -O2 стабильный максимум
Я искал более-менее универсальное решение для конкретной проблемы — я его нашел. Что вас не устраивает?
Не имел дела с мелкими cortex. Может там есть такая же фича?
Судя по гуглу, что-то похожее есть и у М-кортексов
Но лично я этим не пользовался со стороны микроконтроллера, только со стороны отладчика, когда ловил переполнения стека
А вот Миландр 1986ВЕ1, зараза, на другом ядре, в нем этой функциональности вообще нет.
Существует такая книжка от TI "Ядро Cortex-M3 компании ARM" под авторством Джозеф Ю. 15-ая и 16-ая глава посвящена режиму отладки/трассировки и модулям отладки и трассировки. Нас интересует модуль DWT и регистр DEMCR.
Задача состоит в том, чтобы его настроить на генерацию прерывания DebugMon_Handler (на удивление в спецификации на МК оно отсутствует (как и многое чего), а в библиотеке на устройство присутствует, в любом случае отладка и трассировка как-то работает). Если же оно все таки не функционирует, в 15-ой главе описан способ получения прерывания HardFault из-за не правильно настройки.
Информация о регистрах модуля DWT можно найти здесь
К сожалению процессором 1986ВЕ1Т не обладаю, а отладочная под 1986ВЕ94Т на данный момент занята, так что проверить теорию не могу. Но мне кажется вектор верный, осталось только попрактиковаться, главное не забывать о безопасности, чтобы не убить МК вставляя подушку безопасности перед включением защитного кода.
Официально 1986ВЕ1Т имеет некое RISC-совместимое ядро, которое исключительно случайно очень напоминает Cortex-M0 :) В тех поддержке мне предложили его считать «функциональным аналогом».
И DWT там вполне может не быть, раз уж ITM они выкинули. Хотя проверить не помешает, конечно.
Имея отладочный комплект так же можно поиграться с конфигурацией внешней шиной, пытаясь вызвать Hard Fault.
Официально 1986ВЕ1Т имеет некое RISC-совместимое ядро, которое исключительно случайно очень напоминает Cortex-M0
Лет этак пять назад упоминалось, что там M1. Сейчас быстро нагуглить это я не смог (купили ядро для ПЛИС, сделали контроллер, и хотя бы официально это не подтверждают?..).
И да, 12-й exception у Cortex-M1 значится как reserved (хотя, казалось бы — ядро для «самостоятельного» встраивания должно иметь все возможные способы отладки). Увы…
Они даже SysTick слегка поломать умудрились, какая уж там отладка.
Отладку, кстати, тоже сломали; во время выполнения нельзя отладчиком в память смотреть, иначе рандомные HardFault'ы сыпятся.
Хороший, короче, микроконтроллер, прям всем советую :)
на удивление в спецификации на МК оно отсутствует
В спецификации на МК есть отсылка на спецификацию конкретного ядра, а в ней — ссылка на ARMv7 reference manual.
Там описание есть. Насколько быстро по нему получится сделать требуемое, я не проверял :-)
Мне, как хоббисту в эмбеде, кажется, что в статье не хватает вводной части с объяснением какие данные хранятся в стеке и чем это отличается от кучи. А также, какими способами можно переполнить стек и как гарантировано это сделать, для проверки.
Как защититься от переполнения стека (на Cortex M)?