Учебный пример, показывающий, что иногда printf в качестве отладки может скрывать ошибку
Я помню, когда я был на каком-то курсе программирования на С, учитель однажды предложил мне использовать printf
наблюдать за выполнением программы, которую я пытался отладить. Эта программа имела ошибку сегментации с причиной, которую я не могу вспомнить в данный момент. Я последовал его совету, и ошибка сегментации исчезла. К счастью, умный ТА сказал мне отлаживать вместо использования printf
s. В этом случае это было полезно.
Итак, сегодня я хотел показать кому-то, что с помощью printf
потенциально может скрыть ошибку, но я не могу найти тот старый код, который имел эту причудливую ошибку (функция? хммм).
Вопрос: Кто-нибудь из вас сталкивался с таким поведением? Как я мог воспроизвести что-то подобное?
Редактировать:
Я вижу, что мой вопрос частично ориентирует мое мнение на "использование printf
неправильно ". Я не совсем так говорю, и мне не нравится принимать крайние мнения, поэтому я немного редактирую вопрос. Я согласен с этим printf
это хороший инструмент, но я просто хотел воссоздать случай, когда printf
s ошибка сегментации исчезает и, следовательно, доказать, что нужно быть осторожным.
11 ответов
Есть случаи при добавлении printf
Call изменяет поведение кода, но бывают случаи, когда отладка делает то же самое. Наиболее ярким примером является отладка многопоточного кода, где остановка выполнения потока может изменить поведение программы, поэтому искомая ошибка может не произойти.
Итак, используя printf
У заявлений есть веские причины. Ли отлаживать или printf
должно быть решено в каждом конкретном случае. Обратите внимание, что оба они не являются исключительными в любом случае - вы можете отлаживать код, даже если он содержит printf
звонки:-)
Вам будет очень трудно убедить меня не использовать ведение журнала (а printf в этой ситуации - специальная форма ведения журнала) для отладки. Очевидно, что для отладки сбоя, в первую очередь необходимо получить обратную трассировку и использовать очистку или аналогичный инструмент, но если причина не очевидна, ведение журнала является безусловно одним из лучших инструментов, которые вы можете использовать. Отладчик позволяет вам сконцентрироваться на деталях, а логирование дает вам большую картину. Оба полезны.
Похоже, вы имеете дело с heisenbug.
Я не думаю, что с использованием printf
в качестве инструмента отладки. Но да, как и у любого другого инструмента, у него есть свои недостатки, и да, было несколько случаев, когда добавление операторов printf создавало heisenbug. Тем не менее, я также видел, как heisenbugs появлялись в результате изменений макета памяти, внесенных отладчиком, и в этом случае printf оказался неоценимым в отслеживании шагов, приводящих к сбою.
Я помню, как однажды попытался отладить программу на Macintosh (около 1991 года), где сгенерированный код очистки компилятором для стекового кадра между 32K и 64K был ошибочным, потому что он использовал добавление 16-битного адреса, а не 32-битного (16 количество -бит, добавленное в регистр адресов, будет расширено на 68000). Последовательность была что-то вроде:
скопировать указатель стека в некоторый регистр поместите некоторые другие регистры в стек вычесть около 40960 из указателя стека делать некоторые вещи, которые оставляют только сохраненный регистр указателя стека добавить -8192 (подписанная интерпретация 0xA000) в указатель стека поп-регистры перезагрузить указатель стека из этого другого регистра
В итоге все было в порядке, за исключением того, что сохраненные регистры были повреждены, и один из них содержал константу (адрес глобального массива). Если компилятор оптимизирует переменную в регистр во время раздела кода, он сообщает об этом в файле информации отладки, чтобы отладчик мог правильно вывести его. Когда константа так оптимизирована, компилятор, по-видимому, не включает такую информацию, поскольку в этом не должно быть необходимости. Я отследил все, выполнив "printf" из адреса массива, и установил точки останова, чтобы я мог просматривать адрес до и после printf. Отладчик правильно сообщил адрес до и после printf, но printf вывел неправильное значение, поэтому я разобрал код и увидел, что printf помещает регистр A3 в стек; просмотр регистра A3 до того, как printf показал, что он имеет значение, весьма отличное от адреса массива (printf показал значение, которое фактически содержало A3).
Я не знаю, как я мог бы отследить это, если бы не смог использовать и отладчик, и printf вместе (или, если на то пошло, если бы я не понимал 68000 ассемблерный код).
ИМХО Каждый разработчик все еще полагается на распечатки. Мы только что научились называть их "подробными журналами".
Более того, главная проблема, которую я видел, состоит в том, что люди относятся к printfs так, будто они непобедимы. Например, в Java не редкость увидеть что-то вроде
System.out.println("The value of z is " + z + " while " + obj.someMethod().someOtherMethod());
Это замечательно, за исключением того, что z на самом деле участвовал в методе, а другой объект - нет, и нужно убедиться, что вы не получите исключение из выражения obj.
Другая вещь, которую делают распечатки, это то, что они вводят задержки. Я видел код с условиями гонки, который иногда "исправлялся", когда вводились распечатки. Я не удивлюсь, если какой-то код использует это.
Мне удалось это сделать. Я читал данные из плоского файла. Мой неисправный алгоритм пошел следующим образом:
- получить длину входного файла в байтах
- выделить массив символов переменной длины для использования в качестве буфера
- файлы небольшие, поэтому я не беспокоюсь о переполнении стека, но как насчет входных файлов нулевой длины? упс!
- вернуть код ошибки, если длина входного файла равна 0
Я обнаружил, что моя функция надежно выдаст ошибку сегмента - если только в теле функции не будет printf, и в этом случае она будет работать точно так, как я планировал. Исправление ошибки 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". Это ошибка, которую я сделал раньше, и, вероятно, я повторю ее снова. Но теперь я знаю, чтобы искать это.
Это даст вам деление на 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();
}
Один из способов справиться с этим - настроить систему макросов, которая позволяет легко отключать 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()
для тех обильных отладочных сообщений, которые вы непременно захотите отключить в работе.
Что будет в случае отладки? Печать char *[]
массив до вызова exec()
просто чтобы увидеть, как он был размечен - я думаю, что это вполне допустимое использование для printf()
,
Однако, если формат подается на printf()
имеет достаточную стоимость и сложность, что может фактически изменить выполнение программы (в основном скорость), лучше использовать отладчик. Опять же, отладчики и профилировщики также стоят дорого. Любой может выставить расы, которые могут не появиться в их отсутствие.
Все зависит от того, что вы пишете, и ошибки, которую вы преследуете. Доступны инструменты отладчики, printf()
(также объединяя регистраторы в printf) утверждения и профилировщики.
Лезвие отвертки лучше, чем другие виды? Зависит от того, что вам нужно. Обратите внимание, я не говорю, что утверждения хорошие или плохие. Они просто еще один инструмент.
Ну, может быть, вы могли бы научить его, как использовать GDB или другие программы отладки? Скажите ему, что если ошибка исчезла, просто благодаря "printf", то она действительно не исчезла и могла появиться снова позже. Ошибка должна быть исправлена, а не проигнорирована.