Учебный пример, показывающий, что иногда printf в качестве отладки может скрывать ошибку - PullRequest
7 голосов
/ 24 июня 2010

Я помню, когда я был на каком-то курсе программирования на С, однажды учитель предложил мне использовать printf, чтобы наблюдать за выполнением программы, которую я пытался отлаживать. Эта программа имела ошибку сегментации с причиной, которую я не могу вспомнить в данный момент. Я последовал его совету, и ошибка сегментации исчезла. К счастью, умный ТА сказал мне отлаживать вместо использования printf s. В этом случае это было полезно.

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

Вопрос: Кто-нибудь из вас сталкивался с таким поведением? Как я мог воспроизвести что-то подобное?

Изменить:

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

Ответы [ 11 ]

18 голосов
/ 24 июня 2010

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

Таким образом, использование операторов printf действительнопричины.Отладка или printf должны решаться в каждом конкретном случае.Обратите внимание, что они не являются исключительными в любом случае - вы можете отладочный код, даже если он содержит printf вызовов: -)

7 голосов
/ 24 июня 2010

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

4 голосов
/ 24 июня 2010

Похоже, вы имеете дело с гейзенбаг .

Я не думаю, что есть что-то изначально "неправильное" с использованием printf в качестве инструмента отладки. Но да, как и у любого другого инструмента, у него есть свои недостатки, и да, было несколько случаев, когда добавление операторов printf создавало heisenbug. Тем не менее, я также видел, как heisenbugs появлялись в результате изменений макета памяти, внесенных отладчиком, и в этом случае printf оказался неоценимым в отслеживании шагов, приводящих к сбою.

2 голосов
/ 24 июня 2010

Я помню, как однажды пытался отладить программу на Macintosh (около 1991 г.), где сгенерированный код очистки компилятором для стекового кадра между 32K и 64K был ошибочным, поскольку он использовал добавление 16-битного адреса, а не 32-битного (16-разрядное количество, добавленное в регистр адресов, будет расширено на 68000). Последовательность была что-то вроде:

  copy stack pointer to some register
  push some other registers on stack
  subtract about 40960 from stack pointer
  do some stuff which leaves saved stack-pointer register alone
  add -8192 (signed interpretation of 0xA000) to stack pointer
  pop registers
  reload stack pointer from that other register

Чистым эффектом было то, что все было в порядке , за исключением , что сохраненные регистры были повреждены, и один из них содержал константу (адрес глобального массива). Если компилятор оптимизирует переменную в регистр во время раздела кода, он сообщает об этом в файле информации отладки, чтобы отладчик мог правильно вывести его. Когда константа так оптимизирована, компилятор, по-видимому, не включает такую ​​информацию, поскольку в этом не должно быть необходимости. Я отследил все, выполнив «printf» из адреса массива, и установил точки останова, чтобы я мог просматривать адрес до и после printf. Отладчик правильно сообщил адрес до и после printf, но printf вывел неправильное значение, поэтому я разобрал код и увидел, что printf помещает регистр A3 в стек; просмотр регистра A3 до того, как printf показал, что он имеет значение, весьма отличное от адреса массива (printf показал значение, которое фактически содержало A3).

Я не знаю, как я мог бы отследить это, если бы не смог использовать и отладчик, и printf вместе (или, если на то пошло, если бы я не понимал 68000 ассемблерный код).

2 голосов
/ 24 июня 2010

ИМХО Каждый разработчик все еще полагается на распечатки.Мы только что научились называть их «подробными журналами».

Более того, главная проблема, которую я видел, состоит в том, что люди относятся к printfs так, будто они непобедимы.Например, в Java нередко можно увидеть что-то вроде

System.out.println("The value of z is " + z + " while " + obj.someMethod().someOtherMethod());

Это замечательно, за исключением того, что z на самом деле участвовал в методе, а другой объект - нет, и есть уверенность, что вы не будетеполучите исключение из выражения obj.

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

1 голос
/ 20 февраля 2012

У меня просто был похожий опыт.Вот моя конкретная проблема и ее причина:

// Makes the first character of a word capital, and the rest small
// (Must be compiled with -std=c99)
void FixCap( char *word )
{
  *word = toupper( *word );
  for( int i=1 ; *(word+i) != '\n' ; ++i )
    *(word+i) = tolower( *(word+i) );
}

Проблема связана с условием цикла - я использовал «\ n» вместо нулевого символа «\ 0».Теперь я не знаю точно, как работает printf, но из этого опыта я предполагаю, что он использует некоторое место в памяти после моих переменных в качестве временного / рабочего пространства.Если оператор printf приводит к тому, что символ '\ n' записывается в каком-то месте после того, где хранится мое слово, тогда функция FixCap сможет остановиться в какой-то момент.Если я удаляю printf, то он продолжает зацикливаться, ища '\ n', но никогда не находит его до тех пор, пока он не выйдет из строя.

Итак, в конце корень моей проблемы в том, что иногда я печатаю«\ n», когда я имею в виду «\ 0».Это ошибка, которую я сделал раньше, и, вероятно, я повторю ее снова.Но теперь я знаю, чтобы искать это.

1 голос
/ 27 марта 2011

Мне удалось это сделать. Я читал данные из плоского файла. Мой ошибочный алгоритм пошел следующим образом:

  1. получить длину входного файла в байтах
  2. выделить массив символов переменной длины для использования в качестве буфера
    • файлы небольшие, поэтому я не беспокоюсь о переполнении стека, но как насчет входных файлов нулевой длины? ой!
  3. возвращает код ошибки, если длина входного файла равна 0

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

0 голосов
/ 24 июня 2010

Один из способов справиться с этим - настроить систему макросов, которая позволяет легко отключать printfs без необходимости удалять их в вашем коде. Я использую что-то вроде этого:

#define LOGMESSAGE(LEVEL, ...) logging_messagef(LEVEL, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__);

/* Generally speaking, user code should only use these macros.  They
 * are pithy. You can use them like a printf:
 *
 *    DBGMESSAGE("%f%% chance of fnords for the next %d days.", fnordProb, days);
 *
 * You don't need to put newlines in them; the logging functions will
 * do that when appropriate.
 */
#define FATALMESSAGE(...) LOGMESSAGE(LOG_FATAL, __VA_ARGS__);
#define EMERGMESSAGE(...) LOGMESSAGE(LOG_EMERG, __VA_ARGS__);
#define ALERTMESSAGE(...) LOGMESSAGE(LOG_ALERT, __VA_ARGS__);
#define CRITMESSAGE(...) LOGMESSAGE(LOG_CRIT, __VA_ARGS__);
#define ERRMESSAGE(...) LOGMESSAGE(LOG_ERR, __VA_ARGS__);
#define WARNMESSAGE(...) LOGMESSAGE(LOG_WARNING, __VA_ARGS__);
#define NOTICEMESSAGE(...) LOGMESSAGE(LOG_NOTICE, __VA_ARGS__);
#define INFOMESSAGE(...) LOGMESSAGE(LOG_INFO, __VA_ARGS__);
#define DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#if defined(PAINFULLY_VERBOSE)
#   define PV_DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#else
#   define PV_DBGMESSAGE(...) ((void)0);
#endif

logging_messagef() - это функция, определенная в отдельном файле .c. Используйте макросы XMESSAGE (...) в своем коде в зависимости от цели сообщения. Самое лучшее в этой настройке - это то, что она работает для отладки и ведения журнала одновременно, и функцию logging_messagef() можно изменить, чтобы сделать несколько разных вещей (от printf до stderr, в файл журнала, использовать syslog или некоторые другие системные журналы). средство и т. д.), а сообщения ниже определенного уровня можно игнорировать в logging_messagef(), когда они вам не нужны. PV_DBGMESSAGE() - для тех обильных отладочных сообщений, которые вы непременно захотите отключить в рабочей среде.

0 голосов
/ 24 июня 2010

Что будет в случае отладки? Распечатка массива char *[] перед вызовом exec() только для того, чтобы посмотреть, как он был токенизирован - я думаю, это вполне допустимое использование для printf().

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

Все зависит от того, что вы пишете, и ошибки, которую вы преследуете. Доступными инструментами являются отладчики, printf() (также объединяющие регистраторы в printf) утверждения и профилировщики.

Является ли отвёртка лучше других? Зависит от того, что вам нужно. Обратите внимание, я не говорю, что утверждения хорошие или плохие. Они просто еще один инструмент.

0 голосов
/ 24 июня 2010

Это даст вам деление на 0 при удалении строки printf:

int a=10;
int b=0;
float c = 0.0;

int CalculateB()
{
  b=2;
  return b;
}
float CalculateC()
{
  return a*1.0/b;
}
void Process()
{
  printf("%d", CalculateB()); // without this, b remains 0
  c = CalculateC();
}
...