Фон
На работе я часто заканчиваю отладку с использованием основных дампов оптимизированного кода.
Для определенных видов трудных невоспроизводимых сбоев я хотел бы иметь дополнительную информацию для меня. В этих случаях добавление дополнительных трассировок невозможно, поскольку подавляющее большинство вызовов выполняется успешно и добавит миллионы «ненужных» трассировок в минуту, что приведет к быстрой прокрутке файлов журнала. Поймать и проследить не всегда возможно, так как некоторые ошибки могут повредить среду, вызывая сбой трассировки.
Поскольку наши дампы ядра включают память стека вызовов, я подумал, что мог бы использовать область памяти стека вызовов для «трассировки».
Эта проблема
Благодаря оптимизации компиляторов подобный код не работает
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) но можно ли ожидать, что он продолжит работать?
Есть ли лучший способ достичь того, что я хочу сделать?
Под лучшим я подразумеваю более простое, более надежное или стандартное соответствие.
Из вашего вопроса кажется, что у вас есть ситуация, когда вы знаете, что есть редкая / трудная для отладки проблема в конкретной функции / области кода? Я предполагаю это, так как вы говорите о ручном инструменте, и я предполагаю, что вы не планируете делать это повсеместно спекулятивно в ожидании возможной проблемы.
Если это ваша ситуация, то я думаю, что вы могли бы рассмотреть возможность отключения оптимизации только для этой функции / области кода. В Visual Studio вы можете сделать это с #pragma
и я думаю, что нечто подобное существует для clang / gcc. В худшем случае вы можете извлечь соответствующие функции в отдельный файл и скомпилировать только этот файл без оптимизации.
Это может не помочь вам в тех проблемах, которые проявляются только в оптимизированных сборках, но когда вы попадаете на хитрые разновидности Heisenbugs, может появиться любой вид добавленной трассировки, чтобы скрыть проблему или сделать ее менее частой. В этом случае ваш единственный реальный выход — действительно хорошо разбираться в разборке …
Это сказало, volatile
говорит компилятору, что не разрешено оптимизировать чтение и запись, поэтому ваш подход должен быть надежным и может быть полезным инструментом для устранения ошибок определенного типа.
Оптимизированные компиляции могут быть сложными для отладки:
Вы можете попробовать что-то вроде:
В вашем примере:
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),
и лишние аргументы легко игнорируются.