Оценка размера стека

В многопоточном встроенном программном обеспечении (написанном на C или С++) потоку должно быть предоставлено достаточно пространства стека, чтобы он мог завершить свои операции без переполнения. Правильная калибровка стека имеет решающее значение в некоторых встроенных средах реального времени, потому что (по крайней мере, в некоторых системах, с которыми я работал) операционная система НЕ обнаружит это для вас.

Обычно размер стека для нового потока (кроме основного потока) обозначается во время создания потока (т.е. в аргументе pthread_create() или тому подобное). Часто эти размеры стека жестко закодированы до значений, которые, как известно, хороши на момент написания или тестирования кода.

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

Я лично видел эту проблему в том случае, когда код, выполняемый в потоке, объявляет экземпляры структуры в стеке. Когда структура добавляется для хранения дополнительных данных, размер стека раздувается соответственно, что потенциально может привести к переполнению стека. Я предполагаю, что это может быть огромной проблемой для установленных кодовых баз, где сразу невозможно сразу определить полные эффекты добавления полей в структуру (слишком много потоков/функций для поиска всех мест, где используется эта структура).

Поскольку обычный ответ на вопрос о размере стека - "они не переносимы", допустим, что компилятор, операционная система и процессор - все известные величины для этого исследования. Пусть также предполагается, что рекурсия не используется, поэтому мы не имеем дело с возможностью сценария "бесконечной рекурсии".

Каковы некоторые надежные способы оценки необходимого размера стека для потока? Я бы предпочел методы, которые находятся в автономном режиме (статический анализ) и автоматический, но все идеи приветствуются.

Ответ 1

Runtime-оценка

Онлайн-метод состоит в том, чтобы нарисовать полный стек с определенным значением, например 0xAAAA (или 0xAA, независимо от вашей ширины). Затем вы можете проверить, насколько большой стек максимально вырос в прошлом, проверяя, какая часть картины оставлена ​​нетронутой.

Посмотрите эту ссылку для объяснения с иллюстрацией.

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

Статическая оценка

Есть несколько статических проверок, и я думаю, что даже существует взломанная версия gcc, которая пытается это сделать. Единственное, что я могу вам сказать, это то, что статическую проверку очень сложно сделать в общем случае.

Также рассмотрите этот вопрос.

Ответ 2

Вы можете использовать инструмент статического анализа, например StackAnalyzer, если ваша цель соответствует требованиям.

Ответ 3

Если вы хотите потратить значительные деньги, вы можете использовать коммерческий инструмент статического анализа, такой как Klocwork. Хотя Klocwork в первую очередь нацелен на обнаружение дефектов программного обеспечения и уязвимостей безопасности. Однако он также имеет инструмент под названием "kwstackoverflow", который может использоваться для обнаружения в задаче или потоке. Я использую встроенный проект, над которым я работаю, и у меня были положительные результаты. Я не думаю, что любой инструмент, подобный этому, идеален, но я считаю, что эти коммерческие инструменты очень хороши. Большинство инструментов, с которыми я столкнулся, сталкиваются с указателями функций. Я также знаю, что многие производители компиляторов, такие как Green Hills, теперь создают аналогичную функциональность прямо в своих компиляторах. Это, вероятно, лучшее решение, потому что компилятор имеет глубокое знание всех деталей, необходимых для принятия точных решений о размере стека.

Если у вас есть время, я уверен, что вы можете использовать язык сценариев, чтобы сделать свой собственный инструмент анализа. script должен будет определить точку входа задачи или потока, сформировать полное дерево вызовов функций, а затем вычислить объем пространства стека, который использует каждая функция. Я подозреваю, что есть, вероятно, бесплатные инструменты, которые могут генерировать полное дерево вызовов функций, чтобы это стало проще. Если вы знаете специфику вашей платформы, создающую пространство стека, каждая функция может быть очень простой. Например, первая инструкция сборки функции PowerPC часто представляет собой слово хранилища с инструкцией по обновлению, которая корректирует указатель стека на величину, необходимую для функции. Вы можете взять размер в байтах прямо из первой инструкции, что делает определение общего пространства стека относительно простым.

Эти типы анализа будут давать вам приблизительную оценку верхнего предела стека для использования стека, что вы точно знаете. Конечно, эксперты (например, те, с кем я работаю) могут жаловаться, что вы выделяете слишком много пространства для стека, но это динозавры, которые не заботятся о хорошем качестве программного обеспечения:)

Еще одна возможность, хотя и не рассчитать использование стека, заключается в использовании блока управления памятью (MMU) вашего процессора (если он есть) для обнаружения. Я сделал это на VxWorks 5.4, используя PowerPC. Идея проста, просто поместите страницу защищенной от записи памяти в самый верх вашего стека. Если вы переполнитесь, произойдет выполнение процессора, и вы быстро получите предупреждение о проблеме. Конечно, это не говорит вам, сколько вам нужно увеличить размер стека, но если вы хорошо разбираетесь в отладке файлов исключений/ядра, вы, по крайней мере, можете определить последовательность вызовов, которая переполнила стек. Затем вы можете использовать эту информацию для увеличения размера стека.

-djhaus

Ответ 4

Не бесплатно, но Coverity выполняет статический анализ стека.

Ответ 5

Статическая (автономная) проверка стека не так сложна, как кажется. Я реализовал его для нашей встроенной среды IDE (RapidiTTy) — он в настоящее время работает для ARM7 (NXP LPC2xxx), Cortex-M3 (STM32 и NXP LPC17xx), x86 и нашего внутреннего MIPS ISA-совместимого FPGA-софт-ядра.

По сути, мы используем простой анализ исполняемого кода для определения использования стека для каждой функции. Наиболее значительное распределение стека выполняется в начале каждой функции; просто ознакомьтесь с тем, как он изменяется с разными уровнями оптимизации и, если применимо, наборами инструкций ARM/Thumb и т.д. Помните также, что задачи обычно имеют свои собственные стеки, а ISR часто (но не всегда) разделяют отдельную область стека!

Как только вы используете каждую функцию, довольно легко создать дерево вызовов из анализа и вычислить максимальное использование для каждой функции. Наша IDE создает для вас планировщики (эффективные тонкие RTOS), поэтому мы точно знаем, какие функции обозначаются как "задачи" и которые являются ISR, поэтому мы можем рассказать о наихудшем использовании для каждой области стека.

Конечно, эти цифры почти всегда превышают максимальный фактический. Подумайте о функции типа sprintf, которая может использовать много пространства стека, но сильно варьируется в зависимости от строки формата и параметров, которые вы предоставляете. Для этих ситуаций вы также можете использовать динамический анализ — заполните стек с известным значением в вашем запуске, затем некоторое время запустите в отладчике, остановитесь и посмотрите, сколько из каждого стека все еще заполнено вашим значением (тестирование стиля с высоким водяным знаком).

Ни один из подходов не идеален, но объединение обоих даст вам довольно хорошую картину того, как будет выглядеть реальное использование.

Ответ 6

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

Ответ 7

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

Это работает с использованием хорошо опробованного метода заполнения стеков известным шаблоном и предполагает, что единственный способ, которым это можно переписать, - это обычное использование стека, хотя, если оно записывается любым другим способом, стек переполнение - это наименьшее из ваших забот!

Ответ 8

Мы попытались решить эту проблему во встроенной системе на моей работе. Это сошло с ума, есть слишком много кода (как наших, так и сторонних фреймворков), чтобы получить надежный ответ. К счастью, наше устройство было основано на Linux, поэтому мы вернулись к стандартному поведению предоставления каждого потока 2mb и позволили менеджеру виртуальной памяти оптимизировать использование.

Наша единственная проблема с этим решением - это один из сторонних инструментов, выполняемых mlock на всем своем пространстве памяти (в идеале для повышения производительности). Это привело к загрузке всего 2 МБ стека для каждого потока его потоков (75-150 из них). Мы потеряли половину нашего пространства памяти до тех пор, пока не выяснили его и не прокомментировали линию нарушения.

Sidenote: диспетчер виртуальной памяти Linux (vmm) выделяет ОЗУ в 4 тыс. кусков. Когда новый поток запрашивает 2 МБ адресного пространства для своего стека, vmm присваивает страницы фиктивной памяти всем, кроме самой верхней страницы. Когда стек превращается в фиктивную страницу, ядро ​​обнаруживает ошибку страницы и меняет поддельную страницу на реальную (которая потребляет еще 4 тыс. Оперативной памяти). Таким образом, стек потоков может вырасти до любого требуемого размера (при условии, что он меньше 2 МБ), а vmm обеспечит использование только минимального объема памяти.

Ответ 9

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

В некотором смысле использование пространства стека немного напоминает выделение памяти, но без (простого) способа определить, удалось ли ваше распределение, а не контролировать использование стека, приведет к постоянной борьбе за то, чтобы выяснить, почему ваша система снова терпит крах. Так, например, если система выделяет память для локальных переменных из стека, либо выделяйте эту память с помощью malloc(), либо, если вы не можете использовать malloc(), напишите свой собственный обработчик памяти (это достаточно простая задача).

Нет-нет:

void func(myMassiveStruct_t par)
{
  myMassiveStruct_t tmpVar;
}

Да-да:

void func (myMassiveStruct_t *par)
{
  myMassiveStruct_t *tmpVar;
  tmpVar = (myMassiveStruct_t*) malloc (sizeof(myMassicveStruct_t));
}

Кажется довольно очевидным, но часто это не так - особенно, когда вы не можете использовать malloc().

Конечно, у вас все еще будут проблемы, поэтому это всего лишь помощь, но не решает вашу проблему. Тем не менее, это поможет вам оценить размер стека в будущем, поскольку, как только вы найдете хороший размер для своих стеков, и если после этого, после некоторых модификаций кода, снова закончится пространство стека, вы сможете обнаружить множество ошибок или другие проблемы (слишком глубокие стеки вызовов для одного).

Ответ 10

Не уверен на 100%, но я думаю, что это тоже можно сделать. Если у вас открыт порт jtag, вы можете подключиться к Trace32 и проверить максимальное использование стека. Хотя для этого вам придется дать начальный довольно большой размер произвольного стека.