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

StackOverflow https://stackoverflow.com/questions/3110711

Вопрос

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

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

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

Редактировать:

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

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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

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

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

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

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

Я помню, как однажды пытался отладить программу на Macintosh (около 1991 года), где сгенерированный компилятором код очистки для кадра стека размером от 32 до 64 КБ был ошибочным, поскольку в нем использовалось 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).

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

  1. Получить длину входного файла в байтах
  2. Выделите массив Chars с переменной длиной, чтобы служить буфером
    • Файлы маленькие, поэтому я не беспокоюсь о переполнении стека, но как насчет входных файлов нулевой длины? Упс!
  3. вернуть код ошибки, если входной длина файла равен

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

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

// 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. Это ошибка, которую я сделал раньше, и, вероятно, я сделаю снова. Но теперь я знаю, чтобы искать это.

Ну, может быть, вы могли бы научить его, как использовать GDB или другие программы отладки? Скажите ему, что если ошибка исчезнет Juste, благодаря «printf», то она действительно не исчезла и может появиться снова последним. Ошибка должна быть исправлена, а не игнорировать.

Это даст вам подразделение на 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();
}

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

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

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

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

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

#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 to stderr, к файлу журнала, использование системного журнала или какого -либо другого заведения системного регистрации и т. Д.), А сообщения ниже определенного уровня можно игнорировать в logging_messagef() Когда они вам не нужны. PV_DBGMESSAGE() для тех обильных сообщений отладки, которые вы, безусловно, захотите отключить в производстве.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top