Как гарантировать, что локальные переменные не удаляются во время оптимизации

Фон

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

Для определенных видов трудных невоспроизводимых сбоев я хотел бы иметь дополнительную информацию для меня. В этих случаях добавление дополнительных трассировок невозможно, поскольку подавляющее большинство вызовов выполняется успешно и добавит миллионы «ненужных» трассировок в минуту, что приведет к быстрой прокрутке файлов журнала. Поймать и проследить не всегда возможно, так как некоторые ошибки могут повредить среду, вызывая сбой трассировки.

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

Эта проблема

Благодаря оптимизации компиляторов подобный код не работает

void process (int i)
{
int save_me = i;
// Do something else
}

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

alloca похоже, что это может работать, за исключением того, что мы нацелены на некоторые платформы, которые не поддерживают alloca и я не уверен, насколько хорошо он играет вместе с C ++.

Я немного поэкспериментировал, и следующий код, похоже, способен сделать состояние «закрепленным» в стеке даже в оптимизированных сборках:

#include <cstdint>
#include <stdexcept>
#include <istream>
#include <sstream>

struct saved_state
{
saved_state ()
: head  (0xAABBCCDD)
, tail  (0xEEFF0000)
{
std::fill (state, state + 16, 0);
}

void push (std::int32_t input) volatile
{
for (auto i = 15U; i > 0U; --i)
{
state[i] = state[i - 1];
}
state[0] = input;
}

volatile std::uint32_t  head      ;
volatile std::int32_t   state [16];
volatile std::uint32_t  tail      ;
};

void invoke (std::int32_t i)
{
if (i > 10)
{
throw std::runtime_error ("Busted");
}
}

void process (std::istream & input)
{
saved_state volatile ss;

while (!input.eof ())
{
std::int32_t i;
if (input >> i)
{
ss.push (i);
invoke (i);
}
}
}

int main()
{
std::istringstream input ("1\n2\n30\n");
process (input);
return 0;
}

Вопрос

Могу ли я ожидать, что код сделает то, что я хочу? Кажется, это работает для нашего текущего набора компиляторов (Clang & gcc) но можно ли ожидать, что он продолжит работать?

Есть ли лучший способ достичь того, что я хочу сделать?

Под лучшим я подразумеваю более простое, более надежное или стандартное соответствие.

2

Решение

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

Если это ваша ситуация, то я думаю, что вы могли бы рассмотреть возможность отключения оптимизации только для этой функции / области кода. В Visual Studio вы можете сделать это с #pragma и я думаю, что нечто подобное существует для clang / gcc. В худшем случае вы можете извлечь соответствующие функции в отдельный файл и скомпилировать только этот файл без оптимизации.

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

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

2

Другие решения

Оптимизированные компиляции могут быть сложными для отладки:

Вы можете попробовать что-то вроде:

В вашем примере:

void process (int i)
{
int save_me = i;
// Do something else
}

формальный параметр (предварительно инициализированный) и автоматическая переменная находятся в одном стеке, на расстоянии всего нескольких байтов. Если происходит сбой во время «Сделай что-нибудь другое», оптимизатор уже выполнил свою задачу с элементами стека, для которых он больше не нужен.

Что мне повезло, так это:

void process (int i)
{
// Do something else

if (bool_that_compiler_can_not_predetermine_is_always_false)
{
std::cerr << "error:  int i is " << i << std::endl;
}
}

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

Конечно, есть и другие действия, которые вы можете выбрать, кроме cerr. Возможно запись в журнале? Возможно, что-то меньшее. Дело в том, что сбой в дампе ядра не может произойти после сброса значения i (или, если вам все еще нужно, save_me) до конца «процесса».

Оптимизаторы также могут переупорядочивать код, но расположение предложения if в конце процесса (я думаю) заставляет все части делать что-то еще до этого предложения.


Иногда я использую метки времени, чтобы создать предложение «не может быть правдой». (потому что :: время (0) очень эффективно).

Если у вас есть main, argc прост в использовании, то есть (0 == argc) или (argc> 100),
и лишние аргументы легко игнорируются.

1

По вопросам рекламы [email protected]