Форумы Компьютеры Программирование трюки
.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() не является обязательной и просто вываливает номера строк, отображаемых отладчиком в специальном окне. Это на тот случай, если мы совсем не разбираемся в ассемблере. Кстати, дизассемблерный листинг приведенной программы выглядит так:
Страница:
«Пред.| След.»
1234
Меню Главная
7ba.Ru
MobTop - top mobile rating
[Сжатие: 32%]
[0.0203]