FormatX(
Off)
(24.06.08 11:19)Кто-то в шутку сказал, что программисты в среднем тратят 10% времени на написание программы и 90% — на ее отладку. Разумеется, это преувеличение, и правильно спроектированная программа должна отлаживать себя сама или по крайней мере автоматизировать этот процесс. Сегодняшний выпуск трюков, как ты уже догадался, посвящен магии отладки.
FormatX(
Off)
(24.06.08 11:20)Трюк 1: «обрамление» отладочного кода
Многие программисты используют для «обрамления» отладочного кода директивы условной трансляции (пример использования которых приведен чуть ниже), в результате чего отладочный код автоматически удаляется из release-версии продукта:
Распространенный, но неудобный способ «обрамления» отладочного кода
#define _DEBUG_ // debug info is enabled
…
#ifdef _DEBUG_
pritnf("output debug info\n");
#endif
Однако, это не самый продвинутый вариант, и при желании его можно существенно оптимизировать, заменив директиву препроцессора #ifdef оператором if(0):
Оптимизированный способ «обрамления» отладочного кода
#define _DEBUG_ 1 // debug info is enabled
…
if(_DEBUG_)
{
pritnf("output debug info\n");
}
Если _DEBUG_ == 0, то выражение if(_DEBUG_) превращается в «мертвый код», автоматически детектируемый и удаляемый практически всеми оптимизирующими компиляторами.
Кстати говоря, оператор if(0) выгодно использовать для временного отключения части кода, что обычно делается с помощью комментариев. Однако при многократном включении/отключении большого количества строк, приходится тратить кучу времени на их комментирование, вставляя оператор «//» в начало каждой строки. Теоретически весь блок кода можно отключить с помощью оператора «/* - - - */», но воспользоваться этой возможностью удается далеко не всегда. Увы! Язык Си/Си++ не поддерживает вложенных комментариев последнего типа, и, если они встречаются в отключаемом коде, мы получаем сообщение об ошибке.
С другой стороны, код, отключенный посредством комментариев, в продвинутых средах разработки отмечается другим цветом (например, серым), а потому намного более нагляден, чем оператор if(0), который никак не выделяется в листинге. Поэтому однажды отключенный код рискует отправиться в забвение, и, чтобы этого не произошло, рекомендуется использовать директиву #pragma message, выводящую сообщение при компиляции о том, что такой-то участок кода временно отключен.
FormatX(
Off)
(24.06.08 11:21) приходится тратить кучу времени на их комментирование, вставляя оператор «//» в начало каждой строки. Теоретически весь блок кода можно отключить с помощью оператора «/* - - - */», но воспользоваться этой возможностью удается далеко не всегда. Увы! Язык Си/Си++ не поддерживает вложенных комментариев последнего типа, и, если они встречаются в отключаемом коде, мы получаем сообщение об ошибке.
С другой стороны, код, отключенный посредством комментариев, в продвинутых средах разработки отмечается другим цветом (например, серым), а потому намного более нагляден, чем оператор if(0), который никак не выделяется в листинге. Поэтому однажды отключенный код рискует отправиться в забвение, и, чтобы этого не произошло, рекомендуется использовать директиву #pragma message, выводящую сообщение при компиляции о том, что такой-то участок кода временно отключен.
FormatX(
Off)
(24.06.08 11:21)Трюк 2: условные точки останова - своими руками
Практически все современные отладчики поддерживают условные точки останова, однако их возможности довольно ограниченны. В частности, мы не можем вызывать API-функции, и потому даже такая простая задача, как остановка отладчика в определенном потоке, превращается в головоломку, для решения которой приходится прибегать к анализу регистра FS и прочим шаманским трюкам.
Лишь немногие отладчики позволяют загружать условные точки останова из текстового файла, который легко редактировать в своем любимом IDE с отступами, переносами строки и прочими атрибутами форматирования. А без форматирования мало-мальски сложное условие останова становится практически нечитаемым, и его приходится отлаживать вместе с отлаживаемой программой. Вот такая, значит, рекурсия получается.
Между тем если мы не хакаем двоичный файл, то намного удобнее внедрять точку останова непосредственно в сам исходный текст! На x86-платформе для этого достаточно вызвать ассемблерную инструкцию int 0x3. Естественно, это решение не универсально и к тому же системно зависимо, однако системно-зависимый код можно вынести в макрос/отдельную функцию.
«Ручные» точки останова сохраняются вместе с самой программой, что отвязывает нас от отладчика, и мы можем попеременно использовать SoftICE, OllyDebugger и Microsoft Visual C++, например. Кстати говоря, даже если на целевой машине отладчик вообще не установлен, точки останова, внедренные в программу, приведут к вызову Доктора Ватсона. Это, конечно, не отладчик, но все же лучше, чем совсем ничего.
FormatX(
Off)
(24.06.08 11:23) Естественно, это решение не универсально и к тому же системно зависимо, однако системно-зависимый код можно вынести в макрос/отдельную функцию.
«Ручные» точки останова сохраняются вместе с самой программой, что отвязывает нас от отладчика, и мы можем попеременно использовать SoftICE, OllyDebugger и Microsoft Visual C++, например. Кстати говоря, даже если на целевой машине отладчик вообще не установлен, точки останова, внедренные в программу, приведут к вызову Доктора Ватсона. Это, конечно, не отладчик, но все же лучше, чем совсем ничего.
FormatX(
Off)
(24.06.08 11:24)Пример использования рукотворных условных точек останова
#define BREAK1_ENABLED 1
#define BREAK1_TEXT "arg1 and arg2 are equal"
#define break_in __asm int 0x3
foo(int arg1, int arg2)
{
#ifdef BREAK1_ENABLED
if (arg1 == arg2) break_in;
#pragma message("BREAKPOINT:" BREAK1_TEXT __FILE__)
#endif
}
FormatX(
Off)
(24.06.08 11:24)Трюк 3: мистическое исчезновение ошибок
Некоторые виды ошибок мистическим образом исчезают при запуске программы под отладчиком, и можно дебажить программу хоть до посинения, но так и не получить никакого результата.
На самом деле прикладная программа практически не имеет никаких шансов определить, находится ли она под отладкой или нет. Исключение составляют специальные антихакерские приемы и пошаговое исполнение плюс ошибки синхронизации.
Более вероятная причина исчезновения ошибок заключается в том, что вместе с генерацией отладочной информации компилятор отрубает оптимизатор и выполняет ряд дополнительных действий, изменяющих логику поведения программы (например, инициализирует переменные).
FormatX(
Off)
(24.06.08 11:24)Чтобы не спугнуть ошибки, необходимо отлаживать release-версию программы. Вот так прямо в ассемблерных кодах и отлаживать. А как быть, если мы хотим подняться на уровень исходных текстов?! К сожалению, в общем случае это невозможно. Но тут есть одна хитрость, существенно упрощающая нам жизнь.
Используя предопределенный макрос __LINE__, мы без труда заставим компилятор генерировать информацию о номерах строк, автоматически внедряемых в программу. Конечно, это совсем не то же самое, что отладка на уровне исходных текстов, но все-таки какая-то зацепка уже появляется. Правильно расставив директивы __LINE__, мы легко сориентируемся, в какой части программы сейчас находится ошибка (правда, при этом следует помнить, что компилятор может переупорядочивать машинные команды по своему усмотрению, и потому номера строк, определенные при помощи __LINE__, не всегда соответствуют действительности и могут быть с некоторым сдвигом).
FormatX(
Off)
(24.06.08 11:25)Самое замечательное, что эта задача поддается автоматизации. Не составит большого труда написать плагин для OllyDebugger, распознающий внедренные номера строк и выводящий соответствующий фрагмент исходного текста на экран.
Рассмотрим следующий пример:
Простейший пример программы, автоматически внедряющей номера строк исходного текста в свою release-версию
// макрос для внедрения номеров строк
#define XX dbgline(__LINE__);
// служебная функция для внедрения номеров строк
static dbgline(int line)
{
char buf[1024];
sprintf(buf,"%x\n",line);
OutputDebugString(buf);
}
main()
{
XX // вывести номер строки (в данном случае == 15)
printf("hello, world!\n");
XX // вывести номер строки (в данном случае == 17)
}
FormatX(
Off)
(24.06.08 11:26)Мы определяем макрос XX, вызывающий функцию dbgline() и передающий ей номер строки в качестве аргумента, что приводит к генерации следующего машинного кода: PUSH __LINE__/CALL dbgline(), который можно найти и автоматически, используя __LINE__ в качестве опорной метки. Естественно, если программа занимает более одного файла, необходимо воспользоваться макросом __FILE__, который здесь не показан для упрощения.
А чтобы оптимизирующий компилятор не заинлайнил dbgline, мы объявляем ее как static. API-функция OutputDebugString() не является обязательной и просто вываливает номера строк, отображаемых отладчиком в специальном окне. Это на тот случай, если мы совсем не разбираемся в ассемблере. Кстати, дизассемблерный листинг приведенной программы выглядит так:
Страница: