КулЛиб электронная библиотека
Всего книг - 479584 томов
Объем библиотеки - 712 Гб.
Всего авторов - 222906
Пользователей - 103571

Впечатления

Сварщик Сварщиков про Dgipei: Провал. Том 1. Право жить (ЛитРПГ)

феноменальнейшая графомань

Рейтинг: 0 ( 0 за, 0 против).
Zlato про Образцов: Единая теория всего (Детективная фантастика)

здесь все 4 части

Рейтинг: 0 ( 0 за, 0 против).
Shcola про Щепетнов: Бандит-2 (Попаданцы)

Слышь, релизёр. Ты хоть обложку смени.

Рейтинг: -1 ( 1 за, 2 против).
OMu4 про Михалков: Весёлые зайцы (Сказки для детей)

Такую в FB2 не засунешь - тут каждая страница - шедевр!

Рейтинг: +2 ( 2 за, 0 против).
Serg55 про Дронт: В ту же реку 3 (Альтернативная история)

неплохая трилогия. Третья книга не дописана?
Первые две книги логичные и интересные, третья как-то непонятная

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Бояндин: Безвозмездный дар (Истории Ралиона 5) (Фэнтези: прочее)

Автор рассказал очередную историю... историю которая «стара как этот мир»... Один герой преследует, другой скрывается... Один герой жаждет обрести, другой жаждет покоя...

В финале же «благие намерения» оборачиваются полной противоположностью и то что еще вчера казалось неслыханным благом, превращается в проклятие... Что послужило этому виной? «Хотелки» своего персонального «Я»? Долг перед своей страной (королевством)? Просто желание доказать себе (или другим)? Думаю... все это... не так уж и важно... Ведь финал то будет один и тот же))

Рейтинг: +1 ( 1 за, 0 против).

Интересно почитать: Новый дом для старых людей

Отладка приложений для Microsoft .NET и Microsoft Windows [Джон Роббинс ] (pdf) читать онлайн

-  Отладка приложений для Microsoft .NET и Microsoft Windows  3.21 Мб (скачать pdf) (скачать pdf+fbd)  (читать)  (читать постранично) - Джон Роббинс

Книга в формате pdf! Изображения и текст могут не отображаться!


Настройки текста:



Бурные аплодисменты рецензентов
Если вы стали Bugslayer’ом с первой книгой Джона Роббинса, со второй его книгой вы ста
нете управляемым и неуправляемым BugslayerEx’ом.

Кристоф Назаррэ, менеджер разработок Business Objects
Хотя .NET оберегает от многих ошибок, которые мы бы допустили в Win32, отлаживать их
все равно приходится. Из книги Джона я узнал много нового о .NET и отладке. Попав в ту
пик, я прежде всего звоню Джону.

Джеффри Рихтер, соучредитель Wintellect
Это фантастическая книга для Windows и .NETразработчиков. Роббинс дает несметное чис
ло советов и средств, чтобы сделать процесс более эффективным, не говоря о том, что и бо
лее приятным. Он рассматривает отладку с разных сторон: написание кода, который легче
отлаживать, инструменты и их скрытые возможности, что происходит внутри отладчика и
как расширять Visual Studio.

Брайан Мориарти, специалист по персоналу и чемпион по коду QuickBooks, Intuit
Один из признаков выдающегося разработчика — способность признать, что всегда есть, чему
учиться. Новичок вы или гуру, книга Джона все равно чемунибудь да научит.

Барри Танненбаум, руководитель разработки BoundsChecker, Compuware NuMega Lab
Основное качество, отличающее опытного разработчика от новичка, — способность эффек
тивной отладки. В первом издании этой книги эффективная отладка разложена по полоч
кам, а в этом описаны все тонкости отладки управляемого кода. Используя арсенал средств,
представленных в этой книге и описанные Джоном подходы к отладке, разработчики спра
вятся с самыми трудными ошибками.

Джо Эббот, ведущий проектировщик Microsoft
На этих страницах Джон собрал действительно замечательную коллекцию сведений об отладке.
В то время как в других книгах обсуждение отладки ограничивается советами о том, как избе
жать ошибок и обзором некоторых методик их отслеживания, в книге Джона описываются по
лезные инструменты и API, которые толком нигде не описаны. Прибавьте к этому массу ценных
примеров, и перед вами не книга, а золотая жила для программистов .NET и Win32.

Келли Брок, Electronic Arts
Второе издание книги Джона Роббинса приятно удивило всех его поклонников. Если вы не
хотите потратить годы на изучение .NET или Win32, эта книга для вас. Впечатляет, что даже
самые сложные темы Джон Роббинс излагает просто и доступно. Мне кажется, что эта книга
должна стать эталоном книг для разработчиков. Я программирую для Windows уже 19 лет,
и, если мне придется оставить на полке единственную книгу, я оставлю эту.

Озирис Педрозо, Optimizer Consulting
Visual Studio .NET — прекрасное средство разработки, и когда я с ним столкнулся, то решил,
что имею все, что нужно. Но Джон Роббинс снова представил книгу, в которой объясняются
вещи, о которых я и не знал, что мне их нужно знать! Еще раз спасибо, Джон, за великолеп
ный ресурс для .NETразработчиков!

Питер Иерарди, Software Evolutions
Это самая увлекательная, глубокая, подробная и жизненная книга о секретах отладки в
Windows, написанная опытным ветераном, прошедшим огонь и воду. Прочтите ее и узнаете,
как избежать и исправить сложнейшие ошибки. Эта книга — главная надежда человечества
на улучшение качества ПО.

Спенсер Лау, разработчик, подразделение SQL Server Microsoft
Если вы хоть раз сорвали сроки проекта изза ошибок — читайте книгу Джона! Джон не толь
ко научит, как искать эти мерзкие ошибки, но и расскажет об инструментах и подходах, ко
торые прежде всего помогут избежать ошибок.

Джеймс Нэфтел, менеджер продукта, XcelleNet

John Robbins

Debugging applicatons
for Microsoft ®

.NET
WINDOWS

and Microsoft ®

Джон Роббинс

Отладка приложений
для Microsoft ®

.NET
WINDOWS

и Microsoft ®

Москва, 2004

УДК 004.45
ББК 32.973.26018.2
Р58

Роббинс Джон
Р58

Отладка приложений для Microsoft .NET и Microsoft Windows /Пер. с англ. —
М.: Издательство «Русская Редакция», 2004. — 736 стр.: ил.
ISBN 978–5–7502–0243—0
В книге описаны тонкости отладки всех видов приложений .NET и Win32: от
Webсервисов XML до служб Windows. Каждая глава снабжена примерами, кото
рые позволят увеличить продуктивность отладки управляемого и неуправляемо
го кода. На прилагаемом компактдиске содержится более 6 Мб исходных кодов
примеров и полезных отладочных утилит.
Книга состоит из 19 глав, 2 приложений и предметного указателя. Издание
снабжено компактдиском, содержащим исходные тексты примеров, утилиты и
инструментальные отладочные средства.
УДК 004.45
ББК 32.973.26018.2

Подготовлено к изданию по лицензионному договору с Microsoft Corporation, Редмонд, Вашинг
тон, США.
Macintosh — охраняемый товарный знак компании Apple Computer Inc. ActiveX, BackOffice,
JScript, Microsoft, Microsoft Press, MSDN, NetShow, Outlook, PowerPoint, Visual Basic, Visual C++, Visual
InterDev, Visual J++, Visual SourceSafe, Visual Studio, Win32, Windows и Windows NT являются товар
ными знаками или охраняемыми товарными знаками корпорации Microsoft в США и/или других
странах. Все другие товарные знаки являются собственностью соответствующих фирм.
Все названия компаний, организаций и продуктов, а также имена лиц, используемые в приме
рах, вымышлены и не имеют никакого отношения к реальным компаниям, организациям, про
дуктам и лицам.

ISBN 0735615365 (англ.)
ISBN 9785750202430

© Оригинальное издание на английском языке,
John Robbins, 2003
© Перевод на русский язык, Microsoft Corporation,
2004
© Оформление и подготовка к изданию, издатель
ство «Русская Редакция», 2004

Оглавление
Благодарности

XIII

Введение

XIV

Для кого эта книга? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XVI
Как читать эту книгу и что нового во втором издании . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XVI
Требования к системе . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XVIII
Файлы примеров . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XVIII
Обратная связь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XIX
Служба поддержки Microsoft Press . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XX

Ч А С Т Ь

I

СУЩНОСТЬ ОТЛАДКИ

1

Глава 1 Ошибки в программах: откуда они берутся
и как с ними бороться?

2

Ошибки и отладка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Что такое программные ошибки? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Обработка ошибок и решения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Планирование отладки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Необходимые условия отладки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Необходимые навыки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Выработка мастерства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Процесс отладки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Шаг 1. Воспроизведи ошибку . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Шаг 2. Опиши ошибку . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Шаг 3. Всегда предполагай, что ошибка твоя . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Шаг 4. Разделяй и властвуй . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Шаг 5. Мысли творчески . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Шаг 6. Усиль инструментарий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Шаг 7. Начни интенсивную отладку . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Шаг 8. Проверь, что ошибка устранена . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Шаг 9. Научись и поделись . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Последний секрет отладки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

Глава 2

Приступаем к отладке

Следите за изменениями проекта вплоть до его окончания . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Системы управления версиями . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Системы отслеживания ошибок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выбор правильных систем . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Планирование времени построения систем отладки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создавайте все компоновки с использованием символов отладки . . . . . . . . . . . . . .
При работе над управляемым кодом рассматривайте предупреждения
как ошибки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
При работе над неуправляемым кодом рассматривайте предупреждения
как ошибки (в большинстве случаев) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Разрабатывая неуправляемый код, знайте адреса загрузки DLL . . . . . . . . . . . . . . . . . .
Как поступать с базовыми адресами управляемых модулей? . . . . . . . . . . . . . . . . . . . . .
Разработайте несложную диагностическую систему для заключительных
компоновок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

26
26
27
31
32
33
34
38
41
44
48
56

VI

Оглавление

Частые сборки программы и дымовые тесты обязательны . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Частые сборки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Дымовые тесты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Работу над программой установки следует начинать немедленно . . . . . . . . . . . . . . . . . . . . . .
Тестирование качества должно проводиться с отладочными компоновками . . . . . . . . .
Устанавливайте символы ОС и создайте хранилище символов . . . . . . . . . . . . . . . . . . . . . . . . . .
Исходные тексты и серверы символов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

57
58
59
60
61
62
70

Глава 3

72

Отладка при кодировании

Assert, Assert, Assert и еще раз Assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Как и что утверждать . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Утверждения в .NET Windows Forms или консольных приложениях . . . . . . . . . . . . 83
Утверждения в приложениях ASP.NET и Web=сервисах XML . . . . . . . . . . . . . . . . . . . . . . 92
Утверждения в приложениях C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Различные типы утверждений в Visual C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
assert, _ASSERT и _ASSERTE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
ASSERT_KINDOF и ASSERT_VALID . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Главное в реализации SUPERASSERT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Trace, Trace, Trace и еще раз Trace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Трассировка в Windows Forms и консольных приложениях .NET . . . . . . . . . . . . . . 131
Трассировка в приложениях ASP.NET и Web=сервисах XML . . . . . . . . . . . . . . . . . . . . . 133
Трассировка в приложениях C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Комментировать, комментировать и еще раз комментировать . . . . . . . . . . . . . . . . . . . . . . . . . 135
Доверяй, но проверяй (Блочное тестирование) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

Ч А С Т Ь

I I

ПРОИЗВОДИТЕЛЬНАЯ ОТЛАДКА
Глава 4 Поддержка отладки ОС и как работают отладчики Win32

141
142

Типы отладчиков Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
Отладчики пользовательского режима . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
Отладчики режима ядра . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Поддержка отлаживаемых программ операционными системами Windows . . . . . . . . . 148
Отладка Just=In=Time (JIT) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Автоматический запуск отладчика (опции исполнения загружаемого
модуля) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
MiniDBG — простой отладчик Win32 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
WDBG — настоящий отладчик . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Чтение памяти и запись в нее . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Точки прерывания и одиночные шаги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Таблицы символов, серверы символов и анализ стека . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Шаг внутрь, Шаг через и Шаг наружу . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
Итак, вы хотите написать свой собственный отладчик . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Что после WDBG? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193

Глава 5 Эффективное использование отладчика
Visual Studio .NET

195

Расширенные точки прерывания . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Подсказки к точкам прерывания . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Быстрое прерывание на функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Модификаторы точек прерывания по месту . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
Несколько точек прерывания на одной строке . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
Окно Watch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Вызов методов в окне Watch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Команда Set Next Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212

Оглавление

Глава 6 Улучшенная отладка приложений .NET
в среде Visual Studio .NET

VII

215

Усложненные точки прерывания для программ .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
Условные выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
Окно Watch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Автоматическое развертывание собственных типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Советы и хитрости . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
DebuggerStepThroughAttribute и DebuggerHiddenAttribute . . . . . . . . . . . . . . . . . . . . . . . 224
Отладка в смешанном режиме . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Удаленная отладка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
ILDASM и промежуточный язык Microsoft . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
Начинаем работу с ILDASM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
Основы CLR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
MSIL, локальные переменные и параметры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Важные команды . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Другие инструменты восстановления алгоритма . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242

Глава 7 Усложненные технологии неуправляемого кода
в Visual Studio .NET

245

Усложненные точки прерывания для неуправляемого кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
Усложненный синтаксис точек прерывания . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
Точки прерывания в системных и экспортируемых функциях . . . . . . . . . . . . . . . . . 247
Условные выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
Точки прерывания по данным . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
Окно Watch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Форматирование данных и вычисление выражений . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Хронометраж кода в окне Watch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Недокументированные псевдорегистры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
Автоматическое разворачивание собственных типов . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
Удаленная отладка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Советы и уловки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Отладка внедренного кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Окно Memory и автоматическое обновление . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Контроль исключений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Дополнительные советы по обработке символов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Отключение от процессов Windows 2000 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Обработка дамп=файлов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Язык ассемблера x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
Основы архитектуры процессоров . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Кое=какие сведения о встроенном ассемблере Visual C++ .NET . . . . . . . . . . . . . . . . . 281
Команды, которые нужно знать . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
Частая последовательность команд: вход в функцию и выход из функции . . . 285
Вызов процедур и возврат из них . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
Соглашения вызова . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
Доступ к переменным: глобальные переменные, параметры и локальные
переменные . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
Дополнительные команды, которые нужно знать . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
Манипуляции со строками . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304
Распространенные ассемблерные конструкции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
Ссылки на структуры и классы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
Полный пример . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
Окно Disassembly . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313
Исследование стека «вручную» . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
Советы и хитрости . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320

VIII

Оглавление

Глава 8 Улучшенные приемы для неуправляемого кода
с использованием WinDBG

323

Прежде чем начать . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
Основы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
Что случается при отладке . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Получение помощи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Обеспечение корректной загрузки символов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Процессы и потоки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335
Общие вопросы отладки в окне Command . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
Просмотр и вычисление переменных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
Исполнение, проход по шагам и трассировка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341
Точки прерывания . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Исключения и события . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
Управление WinDBG . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
Магические расширения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
Загрузка расширений и управление ими . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
Важные команды расширения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
Работа с файлами дампа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
Создание файлов дампа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
Открытие файлов дампа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
Отладка дампа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361
Son of Strike (SOS) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Использование SOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363

Ч А С Т Ь

I I I

МОЩНЫЕ СРЕДСТВА И МЕТОДЫ ОТЛАДКИ
ПРИЛОЖЕНИЙ .NET
Глава 9 Расширение возможностей интегрированной
среды разработки Visual Studio .NET

371
372

Расширение IDE при помощи макросов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
Параметры макросов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Проблемы с проектами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
Элементы кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
CommenTater: лекарство от распространенных проблем? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
Введение в надстройки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
Исправление кода, сгенерированного мастером Add=In Wizard . . . . . . . . . . . . . . . . 389
Решение проблем с кнопками панелей инструментов . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
Создание окон инструментов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
Создание на управляемом коде страниц свойств окна Options . . . . . . . . . . . . . . . . . 395
Надстройка SuperSaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 399
Надстройка SettingsMaster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
Вопросы реализации SettingsMaster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
Будущие усовершенствования SettingsMaster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412

Глава 10

Мониторинг управляемых исключений

413

Введение в Profiling API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414
Запуск средства профилирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
ProfilerLib . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
ExceptionMon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
Внутрипроцессная отладка и ExceptionMon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
Использование исключений в .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430

Оглавление

Глава 11

Трассировка программы

IX

433

Установка ловушек при помощи Profiling API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
Запрос уведомлений входа и выхода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Реализация функций=ловушек . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Встраивание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Преобразователь идентификаторов функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Использование FlowTrace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
Некоторые сведения о реализации FlowTrace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
Что после FlowTrace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441

Ч А С Т Ь

I V

МОЩНЫЕ СРЕДСТВА И МЕТОДЫ ОТЛАДКИ
НЕУПРАВЛЯЕМОГО КОДА
Глава 12

Нахождение файла и строки ошибки по ее адресу

443
444

Создание и чтение MAP=файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446
Содержание MAP=файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447
Получение информации об исходном файле, имени функции
и номере строки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
PDB2MAP: создание MAP=файлов постфактум . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452
Использование CrashFinder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454
Некоторые сведения о реализации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
Что после CrashFinder? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462

Глава 13

Обработчики ошибок

464

Структурная обработка исключений против обработки исключений C++ . . . . . . . . . . . . 465
Структурная обработка исключений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
Обработка исключений C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 468
Избегайте использования обработки исключений C++ . . . . . . . . . . . . . . . . . . . . . . . . . . 470
API=функция SetUnhandledExceptionFilter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475
Использование API CrashHandler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
Преобразование структур EXCEPTION_POINTERS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
Минидампы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 503
API=функция MiniDumpWriteDump . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 504
Укрощение MiniDumpWriteDump . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505

Глава 14

Отладка служб Windows и DLL, загружаемых в службы

515

Основы служб . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 515
API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 516
Защита . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517
Отладка служб . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
Отладка базового кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
Отладка службы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519

Глава 15

Блокировка в многопоточных приложениях

527

Советы и уловки, касающиеся многопоточности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527
Не используйте многопоточность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 528
Не злоупотребляйте многопоточностью . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 528
Делайте многопоточными только небольшие изолированные
фрагменты программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 528
Выполняйте синхронизацию на как можно более низком уровне . . . . . . . . . . . . . 529
Работая с критическими секциями, используйте спин=блокировку . . . . . . . . . . . 532
Не используйте функции CreateThread/ExitThread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533

X

Оглавление

Опасайтесь диспетчера памяти по умолчанию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 534
Получайте дампы в реальных условиях . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535
Уделяйте особое внимание обзору кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 536
Тестируйте многопоточные приложения на многопроцессорных
компьютерах . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 537
Требования к DeadlockDetection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 540
Общие вопросы разработки DeadlockDetection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541
Использование DeadlockDetection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 542
Реализация DeadlockDetection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545
Перехват импортируемых функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 545
Детали реализации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 553
Что после DeadlockDetection? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 567

Глава 16

Автоматизированное тестирование

570

Проклятие блочного тестирования: UI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 570
Требования к Tester . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 571
Использование Tester . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 572
Сценарии Tester . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 573
Запись сценариев . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577
Реализация Tester . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 580
Уведомления и воспроизведение файлов в TESTER.DLL . . . . . . . . . . . . . . . . . . . . . . . . . . 580
Реализация TESTREC.EXE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 596
Что после Tester? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 607

Глава 17 Стандартная отладочная библиотека C
и управление памятью

609

Особенности стандартной отладочной библиотеки C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 610
Использование стандартной отладочной библиотеки C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 611
Ошибка в DCRT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 613
Полезные функции DCRT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 617
Выбор правильной стандартной отладочной библиотеки C для вашего
приложения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 618
Использование MemDumperValidator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
Использование MemDumperValidator в программах C++ . . . . . . . . . . . . . . . . . . . . . . . . 626
Использование MemDumperValidator в программах C . . . . . . . . . . . . . . . . . . . . . . . . . . . 627
Глубокая проверка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 628
Реализация MemDumperValidator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 632
Инициализация и завершение в программах C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633
И куда же подевались все сообщения об утечках памяти? . . . . . . . . . . . . . . . . . . . . . . . 634
Использование MemStress . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 635
Интересные проблемы с MemStress . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 637
Кучи операционной системы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 638
Советы по отслеживанию проблем с памятью . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 640
Обнаружение записи в неинициализированную память . . . . . . . . . . . . . . . . . . . . . . . . 640
Нахождение записи данных после окончания блока . . . . . . . . . . . . . . . . . . . . . . . . . . . . 641
Потрясающие ключи компилятора . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 647
Ключи проверки ошибок в период выполнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 647
Ключ проверки безопасности буфера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 653

Глава 18 FastTrace: высокопроизводительная утилита
трассировки серверных приложений

655

Фундаментальная проблема и ее решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 656
Использование FastTrace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 657
Объединение журналов трассировки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 658
Реализация FastTrace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 659

Оглавление

Глава 19

Утилита Smooth Working Set

XI

661

Оптимизация рабочего набора . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 662
Работа с SWS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 666
Настройка компиляндов SWS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 666
Выполнение приложений вместе с SWS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 668
Генерирование и использование файла порядка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 669
Реализация SWS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 671
Функция _penter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 671
Формат файла .SWS и перечисление символов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 675
Период выполнения и оптимизация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 680
Что после SWS? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 683

Ч А С Т Ь

V

ПРИЛОЖЕНИЯ
Приложение A

Чтение журналов Dr. Watson

685
686

Журналы Dr. Watson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 688

Приложение Б Ресурсы для разработчиков приложений
.NET и Windows

696

Книги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 696
Разработка ПО . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Отладка и тестирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 698
Технологии .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 699
Языки C/C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 700
ОС Windows и технологии Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 700
Процессоры Intel и аппаратные средства ПК . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
Программные средства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 702
Web=сайты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 703

Предметный указатель

704

Об авторе

710

Моей жене Пэм.
Я тебе еще не говорил сегодня, как я тобой горжусь?

Памяти Хелен Роббинс.
Ты всегда нас объединяла. Нам страшно не хватает тебя.

Благодарности

Если вы читали первое издание этой книги, какиенибудь статьи в рубрике «Bugs
layer», слушали мои выступления на конференциях или были на моих учебных
курсах, я вам очень признателен! Ваш интерес к отладке и написанию правиль
ного кода — это то, что заставило меня много и напряженно поработать над вто
рым изданием. Благодарю за переписку и дискуссии. Вы подвигли меня на боль
шое дело, спасибо.
Пять экстраординарных людей помогли этой книге выйти в свет, и у меня не
хватает слов, чтобы выразить им свою благодарность: Сэлли Стикни (редактор
проекта в квадрате!), Роберт Лайон (технический редактор), Джин Росс (техни
ческий редактор), Виктория Тулман (редактор рукописи) и Роб Нэнс (художник).
Из моих бессвязных записей и уродливых рисунков они сделали книгу, которую
вы держите в руках. Они приложили просто громадные усилия, и я не знаю, как
их благодарить.
Как и в первом издании, мне помогла замечательная «Команда Рецензентов».
Эти душевные ребята получали мои наброски и советовали абсолютно потряса
ющие отладочные трюки. Они представляют элиту нашего бизнеса, и мне нелов
ко, что я отнял у них столько времени. Вот они, все как на подбор: Джо Эббот
(Microsoft), Скотт Байлес (Gas Powered Games), Келли Брок (Electronic Arts), Пи
тер Иерарди (Software Evolutions), Спенсер Лау (Microsoft), Брайан Мориарти
(Intuit), Джеймс Нэфтел (XcelleNet), Кристоф Назарре (Business Objects), Озирис
Педрозо (Optimizer Consulting), Энди Пеннел (Microsoft), Джеффри Рихтер (Wintel
lect) и Барри Танненбаум (Compuware).
Мне также льстит, что я могу считать себя одним из Wintellect’уалов, сделав
ших огромный вклад в эту книгу: Джим Бэйл, Франческо Балена, Роджер Боссо
нье, Джейсон Кларк, Пола Дениэлс, Питер ДеБетта, Дино Эспозито, Гэри Эвинсон,
Дэн Фергас, Льюис Фрейзер, Джон Лэм, Берни МакКой, Джэф Просиз, Брэнт Рек
тор, Джеффри Рихтер, Кенн Скрибнер и Крис Шелби.
В заключение, как обычно, огромное спасибо моей жене Пэм. Она пожертво
вала многими вечерами и выходными, пока я писал. Даже когда я был в полном
отчаянии, она попрежнему верила в успех, воодушевляла меня, и я довел дело до
конца. Дорогая, с этим покончено. Получай своего муженька обратно.

Введение

Ошибки — жуткая гадость. Многоточие... Ошибки являются причиной обречен
ных на гибель проектов с сорванными сроками, ночными бдениями и опосты
левшими коллегами. Ошибки могут превратить вашу жизнь в кошмар, поскольку,
если изрядное их число затаится в вашем продукте, пользователи могут прекра
тить его применение, и вы потеряете работу. Ошибки — серьезный бизнес.
Много раз люди из нашей среды называли ошибки всего лишь досадным не
доразумением. Это утверждение далеко от истины, как никакое другое. Любой
разработчик расскажет вам о проектах с немыслимым количеством ошибок и даже
о компаниях, загнувшихся оттого, что их продукт содержал столько ошибок, что
был непригоден. Когда я писал первое издание этой книги, NASA потеряла кос
мический зонд, направленный на Марс, изза ошибок, допущенных при выработ
ке требований и проектировании ПО. Во время написания данного издания на
солдат американского спецназа упала бомба, направленная на другую цель. При
чиной была программная ошибка, возникшая при смене источника питания в
системе наведения. По мере того как компьютеры управляют все более ответствен
ными системами, медицинскими устройствами и сверхдорогой аппаратурой, про
граммные ошибки вызывают все меньше улыбок и не рассматриваются как нечто
самой собой разумеющееся.
Я надеюсь, что эта книга прежде всего поможет вам узнать, как писать програм
мы с минимальным числом ошибок и отлаживать их побыстрее. При правильном
подходе вы сэкономите на отладке массу времени. Речь не идет о выработке тре
бований и проектировании, но отлаживать вы наверняка научитесь более грамотно.
В этой книге описывается интегральный подход к отладке. Я рассматриваю от
ладку не как отдельный шаг, а как составную часть общего цикла производства
ПО. Я считаю, что ее следует начинать на этапе выработки требований и продол
жать вплоть до стадии производства.
Две вещи делают отладку в средах Microsoft .NET и Microsoft Windows сложной
и отнимающей много времени. Вопервых, отладка требует опыта — в основном
вам потребуется все постигать самим. Даже если у вас специальное образование,
бьюсь об заклад, что вы никогда не сталкивались со специальным курсом, посвя
щенным отладке. В отличие от таких эзотерических предметов, как методы авто
матической верификации программ на языках программирования, которые ни
один дурак не использует, или разработка отладчиков для дико прогрессивных и
жутко распараллеленных компьютеров, наука отладки, применяемая в коммерчес
ком ПО, похоже, совсем не популярна в вузовском истэблишменте. Некоторые
профессора наставляют: главное — не писать программы с ошибками. Хоть это и
выдающаяся мысль и идеал, к которому все мы стремимся, в действительности все
слегка подругому. Изучение систематизированных проверенных методик отлад

Ââåäåíèå

XV

ки не спасет от очередной ошибки, но следование рекомендациям этой книги
поможет вам сократить число ошибок, вносимых в код, а те из них, которые все
таки туда прокрались, найти быстрее.
Вторая проблема в том, что, несмотря на обилие прекрасных книг по отдель
ным технологиям .NET и Windows, ни в одной из них отладка не описана подробно.
Для отладки в рамках любой технологии нужно знать гораздо больше, чем отдель
ные аспекты технологии, описываемой в той или другой книге. Одно дело знать,
как встроить элемент управления ASP.NET на страницу, совсем другое — как пол
ностью отладить элемент управления ASP.NET. Для его отладки нужно знать все
тонкости .NET и ASP.NET, знать, как различные DLL помещаются в кэш ASP.NET и
как ASP.NET находит элементы управления. Многие книги объясняют реализацию
таких сложных функций, как соединение с удаленной базой данных с примене
нием современнейших технологий, но когда в вашей программе не работает
«db.Connect (“Foo”)» — а рано или поздно это обязательно случается! — прихо
дится самому разбираться во всей технологической цепочке. Кроме того, хотя есть
несколько книг по управлению проектами, в которых обсуждаются вопросы от
ладки, в них делается упор на управленческие и административные проблемы, а
не на задачи разработчиков. Эти книги могут включать прекрасную информацию
о планировании отладки, но от этого мало толку, когда вы сталкиваетесь с разру
шением базы данных или сбоем при возврате из функции обратного вызова.
Идея этой книги — плод моих проб и ошибок как разработчика и менеджера,
старающегося вовремя поставить высококачественный продукт, и как консультанта,
пытающегося помочь другим завершить свои разработки в срок. Год за годом я
накапливал знания и подходы, применяемые для решения двух описанных про
блем, чтобы облегчить разработку Windowsприложений. Для решения первой
проблемы (отсутствия формального обучения по вопросам отладки) я написал
первую часть этой книги — четкий курс отладки с уклоном в коммерческую раз
работку. Что касается второй проблемы (потребности в книге по отладке именно
в .NET, а также в традиционной Windowsсреде), я считаю, что написал книгу, за
полняющую пробел между специфическими технологиями и будничными, но жиз
ненно необходимыми практическими методами отладки.
Я считаю, мне просто повезло заниматься почти исключительно вопросами
отладки последние восемь лет. Сориентировать свою карьеру на отладку мне по
могли несколько событий. Первое: я был одним из первых инженеров, работав
ших в компании NuMega Technologies (ныне часть Compuware) над такими кру
тыми проектами, как BoundsChecker, TrueTime, TrueCoverage и SoftICE. Тогда же я
начал вести рубрику «Bugslayer» в «MSDN Magazine», а затем взялся и за первое
издание этой книги. Благодаря фантастической переписке по электронной почте
и общению с инженерами, разрабатывающими все мыслимые типы приложений,
я получил огромный опыт.
И, наконец, самое важное, что сформировало мое мировоззрение, — участие
в создании и работе Wintellect, что позволило мне пойти далеко вперед и помо
гать в решении весьма серьезных проблем компаниям по всему миру. Представь
те, что вы сидите на работе, на часах — полдень, в голове — никаких идей, а кли
ент может обанкротиться, если вы не найдете ошибку. Сценарий устрашающий,
но адреналина хоть отбавляй. Работа с лучшими инженерами в таких компаниях,

XVI

Ââåäåíèå

как Microsoft, eBay, Intuit и многими другими — лучший из известных мне спосо
бов узнать все методы и хитрости для устранения ошибок.

Äëÿ êîãî ýòà êíèãà?
Я написал эту книгу для разработчиков, которые не хотят допоздна сидеть на
работе, отлаживая программы, и хотят улучшить качество своего кода и органи
зации. Я также написал эту книгу для менеджеров и руководителей коллективов,
которые хотели бы иметь более эффективные команды разработчиков.
С технической точки зрения, «идеальный читатель» — это некто, имеющий опыт
разработки для .NET или Windows от одного до трех лет. Я также рассчитываю,
что читатель является членом реальной команды и уже поставил хотя бы один
продукт. Хоть я и не сторонник навешивать ярлыки, в программной отрасли разра
ботчики с таким уровнем опыта называются «средними».
Для опытных разработчиков тоже будет польза. Многие из наиболее заинте
ресованных корреспондентов в переписке по первому изданию этой книги были
опытные разработчики, которым, казалось бы, и учиться уже нечему. Я был заин
тригован тем, что эта книга помогла им добавить новые инструменты в свой ар
сенал. Так же, как и в первом издании, группа замечательных друзей под названи
ем «Команда Рецензентов» просматривала и критиковала все главы, прежде чем я
отправлял их в Microsoft Press. Эти инженеры, перечисленные в разделе «Благо
дарности» этой книги, — сливки общества разработчиков, благодаря им каждый
читатель этой книги узнает чтонибудь полезное.

Êàê ÷èòàòü ýòó êíèãó
è ÷òî íîâîãî âî âòîðîì èçäàíèè
Первое издание было ориентировано на отладку, связанную с Microsoft Visual Studio
6 и Microsoft Win32. Поскольку появилась совершенно новая среда разработки,
Microsoft Visual Studio .NET 2003, и совершенно новая парадигма программиро
вания, .NET, есть еще о чем рассказать. На самом деле в первом издании было 512
страниц, а в этой — около 850, так что новой информации хватает. Несколько моих
рецензентов сказали: «Непонятно, почему ты называешь это вторым изданием, это
же совершенно новая книга!» Чтобы вы правильно понимали, насколько второе
издание больше первого, замечу, что в первом издании 2,5 Мб исходных текстов,
а в этом — 6,9! Не забывайте: это только исходные тексты и вспомогательные файлы,
а не скомпилированные двоичные файлы (скомпилировав все, вы получите бо
лее 1 Гб). Что еще интересней, я даже не включил две главы из первого издания
во второе. Как видите, это совершенно новая книга.
Я разделил книгу на четыре части. Первые две (главы с 1 по 8) следует читать
по порядку, поскольку материал в них изложен в логической последовательности.
В части I «Сущность отладки» (главы с 1 по 3) я даю определение видов оши
бок и описываю процесс отладки, которому следуют все порядочные разработ
чики. По просьбе читателей первого издания я расширил и углубил обсуждение
этих тем. Я также рассматриваю инфраструктурные требования, необходимые для
правильной коллективной отладки. Настоятельно рекомендую уделить особое
внимание вопросу установки сервера символов в главе 2. Наконец, поскольку вы

Ââåäåíèå

XVII

можете (и должны) уделять огромное внимание отладке на этапе кодирования, я
рассказываю про упреждающую отладку при написании кода. Заключительное
слово в обсуждении темы первой части — в главе 3, в которой говорится об ут
верждениях в .NET и Win32.
Часть II «Производительная отладка» (главы с 4 по 8) я начинаю объяснением
поддержки отладки со стороны ОС и рассказываю о работе отладчика Win32, так
как Win32отладка имеет больше потаенных мест, чем .NET. Чем лучше вы разбе
ретесь с инструментарием, тем лучше сможете его применять. Я также достаточ
но глубоко разбираю отладчик Visual Studio .NET, так что вы научитесь выжимать
из него по максимуму как в .NET, так и в Win32. Одна вещь, которую я узнал, ра
ботая с программистами как опытными, так и очень опытными, — они использу
ют лишь крошечную часть возможностей отладчика Visual Studio .NET. Хотя та
кие сантименты могут казаться странными в устах автора книги об отладке, я хочу,
насколько это возможно, оградить вас от применения отладчика. Читая книгу, вы
увидите, что моя цель в первую очередь — научить вас избегать ошибок, а не на
ходить их. Я также хочу научить вас использовать максимум возможностей отлад
чика, поскольку всетаки настанут времена, когда вы будете его применять.
В части III «Мощные средства и методы отладки приложений .NET» (главы с 9
по 11) я предлагаю несколько утилит для .NETразработки. В главе 9 описаны
потрясающие возможности расширения Visual Studio .NET. Я представляю несколько
отличных макросов и надстроек, которые помогут ускорить разработку незави
симо от того, с чем вы работаете: с .NET или только с Win32. В главах 10 и 11
рассказывается об отличном интерфейсе .NET Profiling API и представляются два
инструмента, которые помогут вам отслеживать исключения и ход выполнения
ваших .NETприложений.
В заключительной части «Мощные средства и методы отладки неуправляемо
го кода» (главы с 12 по 19) предлагаются решения распространенных проблем
отладки, с которыми вы столкнетесь при написании Windowsприложений. Я
раскрываю темы от поиска исходного файла и номера строки для сбойного ад
реса, до корректной обработки сбоев приложений. Главы с 15 по 18 были и в первом
издании, однако я существенно изменил их текст, а некоторые утилиты (Deadlock
Detection, Tester и MemDumperValidator) полностью переписал. Кроме того, такие
утилиты, как Tester, прекрасно работают как с неуправляемым кодом, так и с .NET.
И, наконец, я добавил два новых отладочных инструмента для Windows: FastTrace
(глава 18) и Smooth Working Set (глава 19).
Приложения (А и Б) содержат дополнительную информацию, которую вы най
дете полезной в своих отладочных приключениях. В приложении А я объясняю,
как читать и интерпретировать журнал программы Dr. Watson. В приложении Б вы
обнаружите аннотированный список ресурсов (книг, инструментов, Webсайтов),
которые помогли мне отточить свое мастерство как разработчика/отладчика.
В первом издании я предложил несколько врезок с фронтовыми очерками об
отладке. Реакция была ошеломляющей, и в этом издании я существенно увеличил
их число. Надеюсь, поделившись с вами примерами некоторых действительно
«хороших» ошибок, я помог обнаружить (или внести!) аналогичные, и вы увиде
ли практическое применение рекомендуемых мной подходов и методик. Мне также
хотелось бы помочь вам избежать ошибок, сделанных мной.

XVIII

Ââåäåíèå

У меня был список вопросов, которые мне задали в связи с первым изданием,
и на них я ответил во врезках «Стандартный вопрос отладки».

Òðåáîâàíèÿ ê ñèñòåìå
Чтобы проработать эту книгу, вам потребуются:
쐽 Microsoft Windows 2000 SP3 или более поздняя версия, Microsoft Windows XP
Professional или Windows Server 2003;
쐽 Microsoft Visual Studio .NET Professional 2003, Microsoft Visual Studio .NET Enterprise
Developer 2003 или Microsoft Visual Studio .NET Enterprise Architect 2003.

Ôàéëû ïðèìåðîâ
Я уже сказал, что одних исходных текстов на диске 6,9 Мб. Учитывая, что это больше,
чем в ином коммерческом проекте, держу пари, что ни в одной другой книге по
.NET или Windows вы столько примеров не найдете. Здесь более 20 утилит или
библиотек и более 35 примеров программ, демонстрирующих отдельные кон
струкции. Между прочим, в это число не входят блочные тесты для утилит и биб
лиотек! Код большинства утилит проверялся в таком огромном количестве ком
мерческих приложений, что я сбился со счета, когда их число перевалило за 800.
Я горжусь, что столько компаний сочли мой код достаточно хорошим для своих
продуктов, и надеюсь, что вам он тоже пригодится.
Роберт Лайон, фантастический технический редактор этой книги, собрал
DEBUGNET.CHM, который выступает в роли READMEфайла и содержит инфор
мацию о том, как компоновать и использовать код в ваших проектах, а также
описывает каждую двоичную компоновку.
С файлами примеров также поставляются следующие стандартные средства от
Microsoft:
쐽 Application Compatibility Toolkit (ACT) версия 2.6;
쐽 Debugging Tools for Windows версия 6.1.0017.2.
Я разрабатывал и проверял все проекты в Microsoft Visual Studio .NET Enterprise
Edition 2003. Что касается ОС, я тестировал в Windows 2000 Service Pack 3, Windows
XP Professional Service Pack 1 и Windows Server 2003 RC2 (прежде называвшуюся
Windows .NET Server 2003).

ÂÍÈÌÀÍÈÅ! ANSI-êîä Windows 98/Me
Поскольку Microsoft Windows Me устарела, я не поддерживал ОС, предшествую
щие Windows 2000. Для Windows 2000 и более поздних я внес соответствующие
изменения, в том числе перевел весь свой код в UNICODE. Я использовал макро
сы из TCHAR.H, и интерфейсы к библиотекам, поддерживающим ANSIсимволы,
остались. Однако я не компилировал ни одной программы как ANSI/мультибайт,
так что здесь могут возникнуть проблемы с компиляцией или ошибки при выпол
нении.

Ââåäåíèå

XIX

ÂÍÈÌÀÍÈÅ! Ñåðâåð ñèìâîëîâ DBGHELP.DLL
В нескольких утилитах с неуправляемым кодом я использовал сервер символов
DBGHELP.DLL, поставляемый с Debugging Tools for Windows версии 6.1.0017.2.
Поскольку DBGHELP.DLL теперь можно поставлять со своими приложениями, я
включил эту библиотеку в каталоги Release и Output дерева исходных кодов. По
ищите более новую версию Debugging Tools for Windows по адресу www.micro#
soft.com/ddk/ debugging и скачать последнюю версию DBGHELP.DLL. Для компиля
ции DBGHELP.LIB включена в Visual Studio .NET.
Если захотите использовать мои утилиты с неуправляемым кодом, запишите
новую версию DBGHELP.DLL в каталог, содержащий утилиту. С Windows 2000 и
Windows XP поставляется версия DBGHELP.DLL, предшествующая 6.1.0017.2.

Îáðàòíàÿ ñâÿçü
Мне очень интересно ваше мнение об этой книге. Если у вас есть вопросы или
собственные фронтовые очерки об отладке, буду рад их услышать! Идеальное место
для ваших вопросов по этой книге и по отладке в целом — форум «Debugging and
Tuning» на www.wintellect.com/forum. Прелесть этого форума в том, что здесь вы
можете покопаться среди вопросов других читателей и отслеживать возможные
исправления и изменения.
Если у вас есть вопросы, которые неудобно публиковать на форуме, отправьте
email по адресу john@wintellect.com. Имейте в виду, что я порядочно разъезжаю и
получаю очень много электронной почты, так что вы не всегда получите ответ
мгновенно. Но я обязательно постараюсь вам ответить.
Спасибо за внимание и счастливой отладки!
Джон Роббинс
Февраль 2003
Холлис, Нью Гемпшир

XX

Ââåäåíèå

Ñëóæáà ïîääåðæêè Microsoft Press
Мы приложили все усилия, чтобы обеспечить точность сведений, изложенных в
книге и содержащихся в файлах примеров. Поправки к этой книге предоставля
ются Microsoft Press через World Wide Web по адресу:
http://www.microsoft.com/mspress/support/
Чтобы подключиться к базе знаний Microsoft Press и найти нужную информа
цию, откройте страницу:
http://www.microsoft.com/mspress/support/search.asp
Если у вас есть замечания, вопросы или предложения по поводу этой книги
или прилагаемого к ней CD или вопросы, на которые вы не нашли ответа в Know
ledge Base, присылайте их в Microsoft Press по электронной почте:
mspinput@microsoft.com
или обычной почтой:
Microsoft Press
Attn: Debugging Applications for Microsoft .NET and Microsoft Windows Editor
One Microsoft Way
Redmond, WA 980526399
Пожалуйста, обратите внимание на то, что по этим адресам не предоставляет
ся техническая поддержка.

Ч А С Т Ь

I

СУЩНОСТЬ ОТЛАДКИ

Г Л А В А

1
Ошибки в программах:
откуда они берутся
и как с ними бороться?

О

тладка — тема очаровательная, какой бы язык программирования или плат
форму вы ни использовали. Именно на этой стадии разработки ПО инженеры орут
на свои компьютеры, пинают их ногами и даже выбрасывают. Людям, обычно
немногословным и замкнутым, такая эмоциональность не свойственна. Отладка
также известна тем, что заставляет вас проводить над ней ночи напролет. Я не
встречал инженера, который бы звонил своей супруге (супругу), чтобы сказать:
«Милая (милый), я не могу приехать домой, так как мы получаем огромное удо
вольствие от разработки UMLдиаграмм и хотим задержаться!» Однако я встречал
массу инженеров, звонивших домой, причитая: «Милая, я не могу приехать домой,
так как мы столкнулись с потрясающей ошибкой в программе».

Ошибки и отладка
Ошибки в программах — это круто! Они помогают вам узнать, как все это рабо
тает. Мы все занялись своим делом потому, что нам нравится учиться, а вылавли
вание ошибок дает нам ни с чем не сравнимый опыт. Не знаю сколько раз, рабо
тая над новой книгой, я располагался в своем офисе, выискивая хороший «баг».
Как здорово находить и устранять такие ошибки! Конечно же, самые крутые ошибки
в программах — это те, что вы найдете до того, как заказчик увидит результат вашей
работы. А вот если ошибки в ваших программах находят заказчики, это совсем
плохо.
Разработка ПО аномальна по двум причинам. Вопервых, это новая и в чемто
еще незрелая область по сравнению с другими формами инженерного искусства,

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

3

такими как конструирование или разработка электрических схем. Вовторых,
пользователи вынуждены принимать ошибки в программах, в частности, в про
граммах для персональных компьютеров. Хоть они и мирятся с ними, но далеко
не в восторге, находя их. Но те же самые заказчики никогда не допустят ошибок
в конструкции атомного реактора или медицинского оборудования. ПО занима
ет все большее место в жизни людей, и близок час, когда оно перестанет быть
свободным искусством. Я не сомневаюсь, что законы, накладывающие ответствен
ность в других технических областях, в конце концов начнут действовать и для ПО.
Вы должны беспокоиться об ошибках в программах, так как в конечном счете
они дорого обходятся вашему бизнесу. Очень скоро заказчики начинают обращать
ся к вам за помощью, заставляя вас тратить свое время и деньги, поддерживая
текущую разработку, в то время как конкуренты уже работают над следующими
версиями. Затем невидимая рука экономики нанесет вам удар: заказчики начнут
покупать программы конкурентов, а не ваши. ПО в настоящее время востребова
но больше, чем капитальные вложения, поэтому борьба за высокое качество про
грамм будет только накаляться. Многие приложения поддерживают расширяемый
язык разметки (Extensible Markup Language, XML) для ввода и вывода, и ваши пользо
ватели потенциально могут переключаться с программы одного поставщика на
программу другого, переходя с одного Webсайта на другой. Это благо для пользо
вателей будет означать, что если наши программы будут содержать большое ко
личество ошибок, то ваше и мое трудоустройство окажется под вопросом. Это же
будет побуждать к созданию более качественных программ. Позвольте мне сфор
мулировать это иначе: чем больше ошибок в ваших программах, тем вероятней,
что вы будете искать новую работу. Нет ничего более ненавистного, чем заниматься
поиском работы.

Что такое программные ошибки?
Прежде, чем приступать к отладке, нужно дать определение ошибки. Мое опреде
ление таково: нечто, что вызывает головную боль у пользователя. Любая ошибка
может быть отнесена к одной из следующих категорий:
쮿 нелогичный пользовательский интерфейс;
쮿 неудовлетворенные ожидания;
쮿 низкая производительность;
쮿 аварийные завершения или разрушение данных.

Нелогичный пользовательский интерфейс
Нелогичный пользовательский интерфейс хоть и не является очень серьезным видом
ошибок, очень раздражает. Одна из причин успеха ОС Microsoft Windows — в оди
наковом в общих чертах поведении всех разработанных для Windows приложе
ний. Отклоняясь от стандартов Windows, приложение становится «тяжелым» для
пользователя. Прекрасный пример такого нестандартного, досаждающего пове
дения — реализация с помощью клавиатуры функции поиска (Find) в Microsoft
Outlook. Во всех других англоязычных приложениях на планете, разработанных
для Windows, нажатие Ctrl+F вызывает диалог для поиска текста в текущем окне.
А в Microsoft Outlook Ctrl+F переадресует открытое сообщение, что, как я пола
гаю, является ошибкой. Даже после многих лет работы с Outlook я никак не могу

4

ЧАСТЬ I

Сущность отладки

запомнить, что для поиска текста в открытом сейчас сообщении, надо нажимать
клавишу F4.
В клиентских приложениях довольно просто решить все проблемы нелогич
ности пользовательского интерфейса. Достаточно лишь следовать рекомендаци
ям книги Microsoft Windows User Interface (Microsoft Press, 1999), доступной так
же в MSDN Online по адресу http://msdn.microsoft.com/library/enus/dnwue/html/
welcome.asp. Если вы чегото не найдете в этой книге, посмотрите на другие при
ложения для Windows, делающие чтото похожее на то, что вы пытаетесь реали
зовать, и следуйте этой модели. Создается впечатление, что Microsoft имеет бес
конечные ресурсы и неограниченное время. Если вы задействуете преимущества
их всесторонних исследований в процессе решения проблем логичности, то это
не будет вам стоить руки или ноги.
Если вы работаете над интерфейсом Webприложения, ваша жизнь существенно
труднее: здесь нет стандартов на пользовательский интерфейс (UI). Как пользо
ватели, мы знаем, что довольно трудно найти хороший UI в браузере. Для разра
ботки хорошего пользовательского интерфейса для Webклиента я могу пореко
мендовать две книги. Первая — это образцовопоказательная библия Webдизай
на: «Jacob Nielsen, Designing Web Usability: The Practice of Simplicity». Вторая —
небольшая, но выдающаяся книга, которую вы должны дать всем доморощенным
спецам по эргономике, которые не могут ничего нарисовать, не промочив горло
(так некоторые начальники хотят делать UI, а сами никогда не работали на ком
пьютере). Это книга «Steve Krug, Don’t Make Me Think! A Common Sense Approach
to Web Usability». Разрабатывая чтолибо для Webклиента, помните, что не все
пользователи имеют 100мегабитные каналы. Поэтому сохраняйте UI простым и
избегайте загрузки с сервера множества мелочей. Исследуя замечательные кли
ентские Webинтерфейсы, компания User Interface Engineering (www.uie.com) на
шла, что такие простые решения, как CNN.com, нравятся всем пользователям. Про
стой набор понятных ссылок на информационные разделы кажется им выглядя
щим лучше, чем чтолибо еще.

Неудовлетворенные ожидания
Неудовлетворенные ожидания пользователя — одна из самых трудноразрешимых
ошибок. Она обычно возникает в самом начале проекта, если компания недоста
точно исследует реальные потребности пользователей. При обоих видах проек
тирования — будь то «коробочные продукты» (разрабатываемые для продажи) или
Информационные Технологии (программы собственной разработки для нужд
собственного предприятия) — причина этой ошибки восходит к проблемам вза
имодействия.
В общем, коллективы разработчиков не общаются напрямую с заказчиками
своих программ, поэтому они сами не изучают, что нужно пользователям. В иде
але все члены коллектива разработчиков должны наведываться к заказчикам, чтобы
увидеть, что они делают с их программами. У вас откроются глаза, если вы по
наблюдаете изза плеча заказчика, как используется ваша программа. Кроме того,
такой опыт позволит вам понять, что, по мнению заказчика, должна делать ваша
программа. Вообщето я бы весьма рекомендовал вам прекратить сейчас чтение
и разработать график встреч с заказчиком. Не могу сказать, что этого достаточно,
но чем больше вы говорите с заказчиком, тем лучшим разработчиком вы будете.

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

5

В дополнение к поездкам к заказчику поможет наличие команды, анализиру
ющей звонки и электронную почту в службу поддержки. Такая обратная связь по
зволит разработчикам увидеть проблемы, с которыми сталкиваются пользователи.
Порой уровень ожиданий пользователей существенно выше, чем может дать
разработка. Такая инфляция ожидания пользователя является классическим резуль
татом очковтирательства, и вы должны сопротивляться представлению в ложном
свете возможностей вашей разработки при данной цене. Когда пользователи не
знают, чего им ожидать от разработки, они склонны полагать, что разработка
содержит больше ошибок, чем на самом деле. Основное правило в такой ситуа
ции — никогда не обещать того, чего вы не можете сделать, и всегда делать то,
что обещали.

Низкая производительность
Пользователей очень расстраивают ошибки, приводящие к снижению произво
дительности при обработке реальных данных. Такие ошибки (а причина их — в
недостаточном тестировании) порой выявляются только на реальных больших
объемах данных. Один из проектов, над которым я работал, BoundsChecker 3.0
компании NuMega, содержал подобную ошибку в первой версии технологии Final
Check. Эта версия FinalCheck добавляла отладочную и контекстнозависимую
информацию прямо в текст программы, чтобы BoundsChecker подробней описывал
ошибки. Увы, мы недостаточно протестировали FinalCheck на реальных прило
жениях перед выпуском BoundsChecker 3.0. В итоге гораздо больше пользовате
лей, чем мы предполагали, не смогло задействовать эту возможность. В последу
ющих выпусках мы полностью переписали FinalCheck. Но первая версия имела низ
кую производительность, и поэтому многие пользователи больше с ней не рабо
тали, хотя это была одна из самых мощных и полезных функций. Что интересно,
мы выпустили BoundsChecker 3.0 в 1995 году, а семь лет спустя все еще были люди
(по крайней мере двое), которые говорили мне, что они не работают с FinalCheck
изза такого негативного опыта!
Бороться с низкой производительностью можно двумя способами. Вопервых,
сразу определите требования к производительности. Чтобы узнать, есть ли про
блемы производительности, ее нужно с чемто сравнивать. Важной частью пла
нирования производительности является сохранение ее основных показателей.
Если ваше приложение начинает терять 10% этих показателей или больше, оста
новитесь и определите, почему упала производительность, и предпримите шаги
по исправлению положения. Вовторых, убедитесь, что вы тестируете свои при
ложения по наиболее близким к реальной жизни сценариям, и начинайте делать
это в процессе разработки как можно раньше.
Вот один из наиболее часто задаваемых разработчиками вопросов: «Где взять
эти самые реальные данные для тестирования производительности?» Ответ — по
просить у заказчиков. Никогда не вредно спросить, можете ли вы получить их
данные, чтобы обеспечить тестирование. Если заказчик беспокоится о конфиден
циальности своих данных, попробуйте написать программу, которая изменит
важную часть информации. Заказчик запустит эту программу и, убедившись, что
измененные в результате ее работы данные не являются конфиденциальными,
передаст их вам. Чтобы стимулировать заказчика предоставить вам свои данные,
бывает полезно передать ему некоторое бесплатное ПО.

6

ЧАСТЬ I

Сущность отладки

Аварийные завершения или разрушение данных
Аварийные завершения и разрушение данных — это то, что ассоциируется с ошиб
ками у большинства программистов и пользователей. Я к этой категории отношу
также утечки памяти. Пользователи в принципе могли бы работать с этими ошиб
ками, но аварийные завершения их добивают. Вот почему большая часть этой книги
посвящена решению этих проблем. Кроме того, аварийные завершения и разру
шение данных — наиболее распространенный тип ошибок. Некоторые из них
разрешить легко, другие же почти неразрешимы. Главное, вы никогда не должны
поставлять разработку заказчику, зная, что она содержит хотя бы одну такую ошибку.

Обработка ошибок и решения
Хотя поставка ПО без ошибок возможна (при условии, что вы уделяете достаточ
но внимания деталям), я по опыту знаю, что большинство коллективов разработ
чиков не достигло такого уровня зрелости разработки ПО. Ошибки — это реаль
ность. Однако вы можете минимизировать количество ошибок в своих приложе
ниях. Это как раз то, что делают коллективы разработчиков, поставляющих вы
сококачественные разработки (и их много). Причины ошибок, в общем, таковы:
쮿 короткие или невозможные для исполнения сроки;
쮿 подход «Сначала кодируй, потом думай»;
쮿 непонимание требований;
쮿 невежество или плохое обучение разработчика;
쮿 недостаточная приверженность к качеству.

Короткие или невозможные для исполнения сроки
Мы все работали в коллективах, в которых «руководство» устанавливало сроки, либо
сходив к гадалке, либо, если первое стоило слишком дорого, с помощью магичес
кого шара1 . Хоть нам и хотелось бы верить, что в большинстве нереальных гра
фиков работ виновато руководство, чаще бывает, что его не в чем винить. В ос
нову планирования обычно кладется оценка объема работ программистами, а они
иногда ошибаются в сроках. Забавные они люди! Они интраверты, но почти все
гда оптимисты. Получив задание, программисты верят до глубины души, что мо
гут заставить компьютер пуститься в пляс. Если руководитель приходит и гово
рит, что в приложение надо добавить преобразование XML, рядовой инженер
говорит: «Конечно, босс! Это займет три дня». Конечно же, он может даже не знать,
что такое «XML», но он знает, что это займет три дня. Большой проблемой явля
ется то, что разработчики и руководители не принимают в расчет время обуче
ния, необходимое для того, чтобы смочь реализовать функцию. В разделе «Пла
нирование времени построения систем отладки» главы 2 я освещу некоторые ас
пекты, которые надо учитывать при планировании. Кто бы ни был виноват в оши
бочной оценке сроков поставки — руководство ли, разработчики или обе сторо

1

Детская игрушка «Magic 8Ball», выпускающаяся в США с 40х годов, которая «отвеча
ет на любые вопросы». На самом деле содержит 20 стандартных обтекаемых ответов.
— Прим. перев.

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

7

ны — главное, что нереальный график ведет к халтуре и снижению качества про
дукта.
Мне посчастливилось работать в нескольких коллективах, которые поставля
ли ПО к сроку. Каждый из этих коллективов владел ситуацией, и нам удавалось
определять реалистичные сроки поставки. Мы рассчитывали эти сроки на осно
ве набора реализуемых функций. Если компания находила предложенную дату
поставки неприемлемой, мы исключали какието возможности, чтобы поспеть к
сроку. Кроме того, план согласовывался с каждым членом коллектива разработ
чиков прежде, чем мы представляли его руководству. Так поддерживалась вера
коллектива в своевременное завершение задания. И что интересно: кроме того,
что эти продукты поставлялись в срок, они были самыми качественными из всех,
над которыми я работал.

Подход «Сначала кодируй, потом думай»
Выражение «Сначала кодируй, потом думай» придумал мой друг Питер Иерарди.
Каждого из нас в той или иной степени можно упрекнуть в таком подходе. Игры
с компиляторами, кодирование и отладка — забавное времяпрепровождение. Это
то, что нам интересно в нашем деле в первую очередь. Очень немногим из нас
нравится сидеть и ваять документы, описывающие, что мы собираемся делать.
Однако, если вы не пишете эти документы, вы столкнетесь с ошибками. Вмес
то того чтобы в первую очередь подумать, как избежать ошибок, вы начинаете
доводить код и разбираться с ошибками. Понятно, что такая тактика усложнит
задачу, потому что вы будете добавлять все новые ошибки в уже нестабильный
базовый исходный код. Компания, в которой я работаю, помогает в отладке са
мых трудных задач. Увы, зачастую, будучи приглашенными для оказания помощи
в разрешении проблем, мы ничего не могли поделать, потому что проблемы были
обусловлены архитектурой программ. Когда мы доводим эти проблемы до руко
водства заказчика и говорим, что для их решения надо переписать часть кода, мы
порой слышим: «Мы вложили в этот код слишком много денег, чтобы его менять».
Явный признак того, что у компании проблема «Сначала кодируй, потом думай»!
Отчитываясь о работе с клиентом, в качестве причины, по которой мы не смогли
помочь, мы просто пишем «СКПД».
К счастью, решить эту проблему просто: планируйте свои проекты. Есть несколь
ко хороших книг о сборе требований и планировании проектов. Я даю ссылки
на них в приложении Б и весьма рекомендую вам познакомиться с ними. Хотя это
не очень привлекательно и даже немного болезненно, предварительное плани
рование жизненно важно для исключения ошибок.
В отзывах на первое издание этой книги звучала жалоба на то, что я рекомен
довал планировать проекты, но не говорил, как это делать. Недовольство право
мерное, и хочу сказать, что я обращаю внимание на эту проблему и здесь, во вто
ром издании. Единственная загвоздка в том, что я на самом деле не знаю как! Вы
можете подумать, что я использую неблаговидный авторский прием — оставлять
непонятный вопрос читателю в качестве упражнения. Читайте дальше, и вы узна
ете, какие тактики планирования применял я сам. Надеюсь, что они подадут не
которые идеи и вам.

8

ЧАСТЬ I

Сущность отладки

Если вы прочтете мою биографию в конце книги, то заметите, что я не зани
мался программированием почти до 30 лет, т. е. в действительности это моя вто
рая профессия. Моей первой профессий было прыгать с самолетов, преследовать
врага, так как я был «зеленым беретом». Если это не было подготовкой к програм
мированию, то я не знаю, чем это было! Конечно, если вы встретите меня сейчас,
вы увидите лишь толстого коротышку с одутловатым зеленым лицом — результат
длительного сидения перед монитором. Но я был настоящим мужиком. Правда!
Проходя службу, я научился планировать. При проведении спецопераций шансы
погибнуть достаточно велики, так что вы крайне заинтересованы в наилучшем
планировании. Планируя одну из таких операций, командование помещает всю
группу в так называемую «изоляцию». В форте Брегг (Северная Калифорния), где
дислоцируется спецназ, есть места, где действительно изолируют команду для про
думывания сценариев операции. Ключевой вопрос при этом был: «Что может
привести к гибели?» Что, если, спрыгнув с парашютами, мы минуем точку невоз
врата, а ВВС не найдут место нашего приземления? А если у нас будут раненые
или убитые к моменту прыжка? А что случится, если после приземления мы не
найдем командира партизан, с которым предполагалась встреча? А если он при
ведет с собой больше людей, чем предполагалось? А если засада? Мы всегда при
думывали вопросы и искали на них ответы, прежде чем покинуть место изоляции.
Идея заключалась в том, чтобы иметь план действий в любой ситуации. Поверьте:
если есть шанс гибели при выполнении задания, вы захотите знать и учесть все
возможные варианты.
Когда я занялся программированием, я стал использовать этот вид планиро
вания в работе. В первый раз я пришел на совещание и сказал: «Что будет, если
Боб помрет до того, как мы минуем стадию выработки требований?» Все заерза
ли. Поэтому теперь я формулирую вопросы менее живодерски, вроде: «Что будет,
если Боб выиграет в лотерее и уволится до того, как мы минуем стадию выработ
ки требований?» Идея та же. Найдите все сомнительные места и путаницу в ва
ших планах и займитесь ими. Это не просто сделать, слабых инженеров это сво
дит с ума, но ключевые вопросы всегда всплывут, если вы копнете достаточно
глубоко. Скажем, на стадии выработки требований вы будете задавать такие воп
росы: «Что, если наши требования не соответствуют пожеланиям пользователей?»
Такие вопросы помогут предусмотреть в бюджете время и деньги на выработку
согласованных требований. На стадии проектирования вы будете спрашивать: «Что,
если производительность не будет достаточно высока?» Такие вопросы напомнят
вам о необходимости сесть и определить основные параметры производительности
и начать планирование, как вы собираетесь добиваться значений этих парамет
ров при тестировании в реальных условиях. Планировать будет существенно проще,
если вы сможете свести все вопросы в таблицу. Просто будьте благодарны, что ваша
жизнь не зависит от поставки ПО в срок.

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

Отладка: фронтовые очерки
Тяжелый случай с СКПД
Боевые действия
Клиент пригласил нас, так как у него возникли серьезные проблемы с бы
стродействием, а дата поставки стремительно приближалась. В первую оче
редь мы попросили сделать 15минутный обзор, чтобы быстро разобрать
ся в терминологии и получить представление о том, как устроен проект. Кли
ент дал нам одного из архитекторов системы, и он начал объяснение на
доске.
Обычно такие совещания с рисованием кружочков и стрелочек занимают
10–15 минут. Однако архитектор выступал уже 45 минут, а я еще ни в чем
толком не разобрался. Наконец я окончательно запутался и снова попро
сил сделать 10минутный обзор системы. Мне не нужно было знать все —
только основные особенности. Архитектор начал заново, но через 15 ми
нут он осветил только 25% системы!

Исход
Это была большая COMсистема, и теперь я начал понимать, в чем заклю
чались проблемы быстродействия. Было ясно, что ктото в команде был без
ума от COM. Он не удовлетворился глотком из стакана с живительной вла
гой COM, а хлебал из 200литровой бочки. Как я позже понял, системе нужно
было 8–10 основных объектов, а эта команда имела 80! Чтобы вы поняли,
насколько нелеп такой подход, представьте себе, что практически каждый
символ в строке был представлен COMобъектом. Классический случай
нулевого практического опыта авторов!
Примерно через полдня я отвел руководителя в сторонку и сказал, что
в таком виде мы с производительностью ничего не сделаем, потому что ее
убивают накладные расходы самой COM. Он не оченьто обрадовался, ус
лышав это, и немедленно выдал печально известную фразу: «Мы вложили в
этот код слишком много денег, чтобы его менять». Увы, но в этом случае
мы практически ничем не смогли помочь.

Полученный опыт
Этот проект страдал изза нескольких основных проблем с самого начала.
Вопервых, члены коллектива отдали проектирование не разработчикам. Во
вторых, они сразу начали кодирование, в то время как надо было начинать
с планирования. Не было абсолютно никаких других мыслей, кроме коди
рования, и кодирования прямо сейчас. Классический случай проблемы «Сна
чала кодируй, потом думай», которой предшествовало «Бездумное Проек
тирование». Я не могу не подчеркнуть это: вам необходимо произвести
реалистичную оценку технологии и планировать свою разработку до того,
как включите компьютер.

9

10

ЧАСТЬ I

Сущность отладки

Непонимание требований
Надлежащее планирование также минимизирует основной источник ошибок в
разработке — расползания функций. Расползание функций — добавление перво
начально не планировавшихся функций — это симптом плохого планирования
и неадекватного сбора требований. Добавление функций в последнюю минуту, будь
то реакция на давление конкурентов, любимая «штучка» разработчика или нажим
руководства, вызывает появление большего числа ошибок в ПО, чем чтолибо еще.
Разработка ПО очень зависит от мелочей. Чем больше деталей вы проясните
до начала кодирования, тем меньше риск. Есть только один способ достичь долж
ного внимания к мелочам — планировать ключевые события и реализацию своих
проектов. Конечно, это не означает, что вам нужно отойти от дел и сочинить тыся
чи страниц документации, описывающей, что вы собираетесь делать.
Лучший документ такого рода, созданный мной, был просто серией рисунков
на бумаге (бумажные прототипы) UI. Основываясь на исследованиях и результа
тах обучения у Джэйреда Спула и его компании User Interface Engineering, моя
команда рисовала UI и прорабатывала сценарии поведения пользователей. Делая
это, мы должны были сосредоточиться на требованиях к разработке и точно по
нять, как пользователи собирались исполнять свои задачи. Если вставал вопрос,
какое поведение предполагалось по данному сценарию, мы доставали свои бумаж
ные прототипы и вновь работали над сценарием.
Даже если бы вы могли спланировать все на свете, вы все равно должны пони
мать требования к своей разработке, чтобы правильно их реализовать. В одной
из компаний, где я работал (к счастью, меньше года), требования к разработке
казались очень простыми и понятными. На поверку, однако, большинство членов
коллектива недостаточно понимало потребности пользователей, чтобы разобрать
ся, что же программа должна делать. Компания допустила классическую ошибку,
радикально увеличив «поголовье» разработчиков, не удосужившись обучить но
вичков. Вследствие этого, хоть и планировалось исключительно все, разработка
запоздала на несколько лет, и рынок отверг ее.
В этом проекте были две большие ошибки. Первая: компания не желала тра
тить время на то, чтобы тщательно объяснить потребности пользователей разра
ботчикам, которые были новичками в предметной области, хотя некоторые из нас
просили об обучении. Вторая: многие разработчики, старые и молодые, не про
являли интереса к изучению предметной области. В итоге команда каждый раз
меняла направление, когда сотрудники отделов маркетинга и продаж в очеред
ной раз объясняли требования. Код был настолько нестабильным, что понадоби
лись месяцы, чтобы заставить работать безотказно даже простейшие пользователь
ские сценарии.
Вообще лишь немногие компании проводят обучение своих разработчиков в
предметной области. Хоть многие из нас и закончили колледжи, мы многого не
знаем о том, как заказчики будут использовать наши разработки. Если компании
затрачивают адекватное время, честно помогая своим разработчикам понять пред
метную область, они могут исключить ошибки, вызванные непониманием требо
ваний.
Но проблема не только в компаниях. Разработчики сами обязаны обучаться в
предметной области. Некоторые считают, что, создавая средства решения зада

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

11

чи, можно дистанцироваться от предметной области. Нет — разработчик отвеча
ет за решение задачи, а не просто предоставляет возможность решения!
Примером предоставления возможности решения является ситуация, когда вы
проектируете пользовательский интерфейс, формально работоспособный, но не
соответствующий технологии работы пользователей. Другой пример — построе
ние приложения, позволяющее решать сиюминутные задачи, но не дающее воз
можности приспособиться к изменяющимся потребностям бизнеса.
При решении пользовательских проблем, а не предоставлении возможности
решения разработчик старается узнать предметную область, так что созданное вами
ПО становится «расширением» пользователя. Лучший разработчик не тот, кто может
манипулировать битами, а тот, кто может решать проблемы пользователя.

Невежество и плохое обучение разработчика
Еще одна существенная причина ошибок исходит от разработчиков, не разбира
ющихся в ОС, языке программирования или технологиях, используемых в про
ектах. Увы, программистов, готовых признать такой недостаток и стремящихся к
обучению, немного.
Во многих случаях, однако, малограмотность является не столько персональ
ным недостатком, сколько правдой жизни. В наши дни так много пластов и взаи
мозависимостей вовлечено в разработку ПО, что невозможно найти такого чело
века, кто знал бы все тонкости каждой ОС, языка программирования и техноло
гии. Не знать не стыдно: это не признак слабости и не делает вас главным недо
умком в конторе. В здоровом коллективе признание сильных и слабых сторон
каждого его члена работает на успех. Учитывая навыки, имеющиеся или отсутству
ющие у разработчиков, коллектив может получить максимальную выгоду от вло
жений в обучение. Устраняя слабые стороны каждого, коллектив сможет лучше
приспосабливаться к непредвиденным обстоятельствам и, как следствие, наращи
вать совокупный потенциал всей команды. Коллектив может также точнее плани
ровать разработку, если его члены добровольно признают, что они чегото не знают.
Вы можете предусмотреть время для обучения и создать более реалистичный гра
фик работ, если члены команды откровенно признают пробелы в своем образо
вании.
Лучший способ научиться технологии — создать чтолибо с ее помощью. Очень
давно, когда NuMega послала меня изучать Microsoft Visual Basic, чтобы мы могли
писать программы для разработчиков на Visual Basic, я представил план, чему я
собираюсь учиться, и это потрясло моего босса. Идея заключалась в создании
приложения, оскорблявшем вас; оно называлось «Обидчик». Версия 1 представ
ляла собой форму с единственной кнопкой, щелчок которой выводил случайное
оскорбление из числа закодированных в тексте программы. Вторая версия чита
ла оскорбления из базы данных и позволяла вам добавлять оскорбления, исполь
зуя форму. Третья версия была подключена к корпоративному серверу Microsoft
Exchange и позволяла посылать оскорбления работникам компании. Моему руко
водителю понравилось то, что я собираюсь делать, чтобы изучить технологию. Все
ваши руководители заботятся о том, чтобы всегда была возможность доложить боссу
о вашей работе в тот или иной день. Если вы предоставите своему руководителю
такую информацию, вы попадете в любимчики. Когда я впервые столкнулся с .NET,
я просто снова использовал идею Обидчика, который стал называться Обидчик.NET!

12

ЧАСТЬ I

Сущность отладки

О том, какие навыки и знания критичны для разработчиков, я расскажу в раз
деле «Необходимые условия отладки».

Стандартный вопрос отладки
Нужно ли пересматривать код?
Безусловно! К сожалению, многие компании подходят к этому совершенно
неверно. Одна компания, на которую я работал, требовала формальные
пересмотры кода точно так же, как это описано в одном из этих фантасти
ческих учебников для программистов, который был у меня в колледже. Все
было расписано по ролям: был Архивариус для записи комментариев, Сек
ретарь для ведения протокола, Привратник, открывающий дверь, Руково
дитель, надувающий щеки, и т. д. На самом деле было 40 человек в комнате,
но никто из них не читал код. Это была пустая трата времени.
Вариант пересмотра кода, который мне нравится, как раз неформаль
ный. Вы просто садитесь с распечаткой текста программы и читаете его
строка за строкой вместе с разработчиком. При чтении вы отслеживаете
входные данные и результаты и можете представить все, что происходит в
программе. Подумайте о том, что я только что написал. Если это напоми
нает вам отладку программы, вы совершенно правы. Сосредоточьтесь на том,
что делает программа, — именно в этом назначение обзора кода программы.
Другая хитрость, гарантирующая, что пересмотр код результативен, —
привлечение младших разработчиков для пересмотра кода старших. Это не
только дает понять менее опытным, что их вклад значим, но и отличный
способ познакомить их с разработкой и показать им любопытные приемы
и хитрости.

Недостаточная приверженность к качеству
Последняя причина появления ошибок в проектах, на мой взгляд, самая серьез
ная. Я не встречал компании или программиста, не говоривших, что они привер
женцы качества. Увы, на самом деле это не так. Если вы когдалибо сталкивались
с компанией или программистами, работавшими качественно, вы понимаете, о чем
речь. Они гордятся своим детищем и готовы корпеть над всеми частями продук
та, а не только над теми, что им интересны. Например, вместо того чтобы копаться
в деталях алгоритма, они выбирают более простой алгоритм и думают, как лучше
его протестировать. В конце концов заказчика интересуют не алгоритмы, а каче
ственный продукт. Компании и отдельные программисты, по настоящему привер
женные качеству, демонстрируют одни и те же характерные черты: тщательное
планирование, персональную ответственность, основательный контроль качества
и прекрасные способности к общению. Многие компании и отдельные програм
мисты проходят через разные этапы разработки больших систем (планирование,
кодирование и т. п.), но только тот, кто уделяет внимание деталям, поставляет про
дукцию в срок и высокого качества.
Хорошим примером приверженности качеству служит мой первый ежемесяч
ный обзор кода в компании NuMega. Вопервых, я был поражен, насколько быст

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

13

ро я получил результаты, хотя обычно приходится умолять руководителей хоть о
какойто обратной связи. Одним из ключевых разделов обзора была запись о
количестве зарегистрированных в разработке ошибок. Я был ошеломлен тем, что
NuMega будет оценивать эту статистику как часть моего обзора производитель
ности. Однако, хотя отслеживание ошибок — жизненно важная часть сопровож
дения продукта, никакая другая компания, из числа тех, где я работал, не прово
дила таких проверок столь очевидным образом. Разработчики знают, где кроют
ся ошибки, но нужно заставлять их включать информацию о них в систему сле
жения за ошибками. NuMega нашла нужный подход. Когда я увидел раздел обзо
ра, посвященный количеству ошибок, поверьте, я стал регистрировать все, что я
нашел, независимо от того, насколько это тривиально. Несмотря на заинтересо
ванность технических писателей, специалистов по качеству, разработчиков и
руководителей в здоровой конкуренции регистрировать как можно больше оши
бок, несколько сюрпризов все же затаились. Но важнее то, что у нас было реальное
представление, в каком состоянии находится проект в каждый момент времени.
Другой отличный пример — первое издание этой книги. На компактдиске,
прилагаемом к книге, было около 2,5 Мб исходных текстов программ (это не
компилированные программы — только исходные тексты!). Это очень много, и я
рад, что это во много раз больше, чем прилагается к большинству других книг.
Многие люди не могут себе даже представить, что я потратил больше половины
времени, ушедшего на эту книгу, на тестирование этих программ. Народ балдеет,
находя ошибки в кодах Bugslayer 2 , и чего я меньше всего хочу — это получать
письма типа «Ага! Ошибочка в Bugslayer!». Без ошибок на том компактдиске не
обошлось, но их было только пять. Моим обязательством перед читателями было
дать им только лучшее из того, на что я способен. Моя цель в этом издании — не
более пяти ошибок в более чем 6 Мб исходных текстов этого издания.
Руководя разработкой, я следовал правилу, которое, я уверен, стимулировало
приверженность к качеству. Каждый член коллектива должен подтвердить готов
ность продукта при достижении каждой вехи проекта. Если ктолибо не считал,
что проект готов, проект не поставлялся. Я бы лучше исправил небольшую ошиб
ку и дал бы дополнительный день на тестирование, чем выпустил бы чтото та
кое, чем коллектив не мог бы гордиться. Это правило соблюдалось не только для
того, чтобы все представляли, что качество обеспечено, это также приводило к
пониманию каждым своей доли участия в результате. Я заметил интересный фе
номен: у членов коллектива никогда не было шанса остановить выпуск изза чу
жой ошибки — «хозяин» ошибки всегда опережал остальных.
Приверженность качеству задает тон для всей разработки: она начинается с про
цесса найма и простирается через контроль качества до кандидата на выпуск. Все
компании говорят, что хотят нанимать лучших работников, но лишь немногие
предлагают соблазнительную зарплату и пособия. Кроме того, некоторые компа
нии не желают обеспечивать специалистов оборудованием и инструментарием,
необходимым для высококачественных разработок. К сожалению, многие компа

2

Название рубрики в журнале «MSDN Magazine». В русском переводе, выпускаемом из
дательством «Русская Редакция», рубрика называется «Отладка и оптимизация». —
Прим. перев.

14

ЧАСТЬ I

Сущность отладки

нии не хотят тратить $500 на инструментарий, позволяющий за несколько минут
найти причину ошибки, приводящей к аварийному завершению, но спокойно
выбрасывают на ветер тысячи долларов на зарплату разработчиков, неделями
барахтающихся в попытках найти эту самую ошибку.
Компания также показывает свою приверженность качеству, когда делает са
мое сложное — увольняет тех, кто не работает по стандартам, установленным в
организации. При формировании команды из высококлассных специалистов вы
должны суметь сохранить ее. Все мы видели человека, который, кажется, только
кислород переводит, но получает повышения и премии, как вы, хотя вы убивае
тесь на работе, пашете ночами, а иногда и в выходные, чтобы завершить продукт.
В результате хороший работник быстро осознает, что его усилия того не стоят.
Он начинает ослаблять свое рвение или, что хуже, искать другую работу.
Будучи руководителем проекта, я, хоть не без боязни, но уволил одного чело
века за два дня до Рождества. Я знал, что люди в коллективе чувствовали, что он
не работал по стандартам. Если бы после Рождества они вернулись и увидели его
на месте, я бы начал терять коллектив, который столько формировал. Я зафикси
ровал факт низкой производительности этого сотрудника, поэтому у меня были
веские причины. Поверьте, в армии мне было стрелять легче, чем «устранить» этого
человека. Было бы намного проще не вмешиваться, но мои обязательства перед
коллективом и компанией — качественно делать работу, на которую я нанят. Все
го за все время моей работы в разных организациях я уволил трех человек. Луч
ше было пройти через такое потрясение, чем иметь в коллективе того, кто тор
мозил работу. Я сильно мучался при каждом увольнении, но я должен был это делать.
Быть приверженным качеству очень трудно, и это значит, что вы должны делать
то, что будет задерживать вас до ночи, но это необходимо для поставок хороше
го ПО и заботы о ваших работниках.
Если вы окажетесь в организации, которая страдает от недостаточной привер
женности качеству, то поймете, что нет простых путей переделать ее за одну ночь.
Руководитель должен найти подходы к вашим работникам и своему руководству
для распространения приверженности качеству во всей организации. Рядовой же
разработчик может сделать свой код самым надежным и расширяемым в проек
те, что будет примером для остальных.

Планирование отладки
Пришло время подумать о процессе отладки. Многие начинают думать об отлад
ке, только споткнувшись на фазе кодирования, но вы должны думать о ней с са
мого начала, с фазы выработки требований. Чем больше времени вы уделите про
цессу планирования, тем меньше времени (и денег) вы потратите на отладку впос
ледствии.
Как я уже говорил, расползание функций может стать убийцей проекта. Чаще
всего незапланированные функции добавляют ошибки и наносят вред проекту.
Однако это не означает, что ваши планы должны быть высечены в граните. Иног
да нужно изменять или добавлять функции для повышения конкурентоспособности
разработки или лучшего удовлетворения потребностей пользователей. Главное, что
до того, как вы начнете менять свою программу, надо определить и спланировать,

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

15

что конкретно вы будете менять. И помнить, что добавление функции затрагива
ет не только кодирование, но и тестирование, документацию, а иногда и марке
тинговые материалы.
В отличной книге Стива МакКонелла (Steve McConnell) «Code Complete» (Micro
soft Press, 1993, стр. 25–26) есть упоминание о стоимости исправления ошибки,
которая по мере разработки растет экспоненциально, как и стоимость отладки
(во многом по тому же сценарию, как и при добавлении или удалении функций).
Планирование отладки производится совместно с планированием тестирова
ния. Во время планирования вам нужно предусмотреть разные способы ускоре
ния и улучшения обоих процессов. Одна из лучших мер предосторожности —
написание утилит для дампа файлов и проверки внутренних структур (а при не
обходимости и двоичных файлов). Если проект читает и записывает двоичные
данные в файлы, вы должны включить в чейто план написание тестовой програм
мы, выводящей данные в читаемом формате. Программа дампа должна также про
верять данные и их взаимозависимости в двоичных файлах. Такой шаг сделает
тестирование и отладку проще.
Планируя отладку, вы минимизируете время, проведенное в отладчике, и это
ваша цель. Может показаться, что такой совет звучит странно в книге об отладке,
но смысл в том, чтобы попытаться избежать ошибок. Если вы встраиваете доста
точно отладочного кода в свои приложения, то этот код (а не отладчик) подска
жет вам, где сидят ошибки. Я освещу подробнее вопросы, касающиеся отладоч
ного кода, в главе 3.

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

Необходимые навыки
Хорошие отладчики должны обладать серьезными навыками разрешения проблем,
что весьма характерно для ПО. К счастью, вы можете учиться и оттачивать свое
мастерство. Великих отладчиков/программистов отличает от хороших отладчи
ков/программистов то, что кроме умения разрешать проблемы, они понимают, как
все части проекта работают в составе проекта в целом.
Вот те области, в которых вы должны быть знатоком, чтобы стать великим или
по крайней мере лучшим отладчиком/программистом:
쮿 ваш проект;
쮿 ваш язык программирования;
쮿 используемая технология и инструментарий;
쮿 операционная система/среда;
쮿 центральный процессор.

Знай свой проект
Знание проекта есть первая линия защиты UI, логики работы и проблем произ
водительности. Зная, как и где в исходных текстах реализованы функции, вы смо
жете быстро понять, кто что делает.

16

ЧАСТЬ I

Сущность отладки

К сожалению, все проекты разные, и единственный путь изучить проект —
прочитать проектную документацию, если она есть, и пройтись по коду с отлад
чиком. Современные системы разработки имеют браузеры классов, позволяющие
увидеть основы устройства программы. Но вам может понадобиться настоящее
средство просмотра, такое как Source Insight от Source Dynamics. Кроме того, вы
можете задействовать средства моделирования, такие как Microsoft Visual Studio.NET
Enterprise Architect, интегрированную с Microsoft Visio, чтобы увидеть взаимосвя
зи или диаграммы UML (Unified Modeling Language), описывающие программу. Даже
минимальные комментарии в тексте программы лучше, чем ничего, если это пре
дотвратит дизассемблирование.

Знай язык реализации
Знать язык (языки) программирования вашего проекта труднее, чем может пока
заться. Я говорю не только об умении программировать на этом языке, но и о
знании того, как исполняется программа, написанная на нем. Скажем, програм
мисты C++ иногда забывают, что локальные переменные, являющиеся классами
или перегруженными операторами, могут создавать временные объекты в стеке.
В свою очередь оператор присваивания может выглядеть совершенно невинным
и при этом требовать большого объема кода для своего исполнения. Многие ошиб
ки, особенно связанные с производительностью, — результат неправильного при
менения средств языка программирования. Поэтому полезно изучать индивиду
альные особенности используемых языков.

Знай технологию и инструментарий
Владение технологиями — первый большой шаг в борьбе с трудными ошибками.
Например, если вы понимаете, что делает COM, чтобы создать COMобъект и воз
вратить его интерфейс, вы существенно сократите время на поиск причины за
вершения с ошибкой запроса интерфейса. Это же относится к фильтрам ISAPI. Если
у вас проблемы с правильно вызванным фильтром, вам надо знать, где и когда
INETINFO.EXE должен был загружать ваш фильтр. Я не говорю, что вы должны знать
наизусть файлы и строки исходного текста или книги. Я говорю, что вы должны
хотя бы в общем понимать используемые технологии и, что более важно, точно
знать, где найти подробное описание того, что вам нужно.
Кроме знания технологии, жизненно важно знать инструментарий. В этой книге
значительное место уделяется передовым методам использования отладчика, но
многие другие средства (скажем, распространяемые с Platform SDK) остаются за
пределами книги. Вы поступите очень мудро, если посвятите один день просмот
ру и ознакомлению с инструментарием, имеющимся в вашем распоряжении.

Знай свою операционную систему/среду
Знание основ работы ОС/среды позволит просто устранять ошибки, а не ходить
вокруг них. Если вы работаете с неуправляемым кодом, вы должны суметь отве
тить на вопросы типа: что такое динамически подключаемая библиотека (DLL)?
Как работает загрузчик образов? Как работает реестр? Для управляемого кода вы
должны знать, как ASP.NET находит используемые компоненты, когда вызывают
ся финализаторы, чем отличается домен приложения от сборки и т. д. Многие са

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

17

мые неприятные ошибки появляются изза неправильного использования средств
ОС/среды. Мой друг Мэтт Питрек (Matt Pietrek), научивший меня прелестям от
ладки, утверждает, что знание ОС/среды и центрального процессора отличает богов
отладки от простых смертных.

Знай свой центральный процессор
И последнее, что нужно знать, чтобы стать богом отладки неуправляемого кода, —
это центральный процессор. Вы должны хоть чтото знать о центральном про
цессоре для разрешения наиболее неприятных ошибок. Было бы хорошо, если бы
аварийное завершение всегда наступало там, где доступен исходный текст, но
обычно при аварийном завершении отладчик показывает окно с дизассемблиро
ванным текстом. Я всегда удивляюсь, как много программистов не знает (более
того, не хочет знать) язык ассемблера. Он не настолько сложен, тричетыре часа,
потраченные на его изучение сэкономят вам бесчисленные часы, затрачиваемые
на отладку. Еще раз: я не говорю, что вы должны уметь писать собственные про
граммы на ассемблере, я даже не думаю, что сам умею это делать. Главное, чтобы
вы могли прочитать их. Все, что вам нужно знать о языке ассемблера, имеется в
главе 7.

Выработка мастерства
Имея дело с технологиями, вы должны непрерывно учиться и идти вперед. Хотя я
и не могу помочь вам в работе над вашими конкретными проектами, в приложе
нии Б я перечислил все ресурсы, помогавшие мне (а они помогут и вам) стать
лучшим отладчиком.
Кроме чтения книг и журналов по отладке, вам также нужно писать утилиты,
причем любые. Лучший способ научиться — это работать, а в нашем случае — про
граммировать и отлаживать. Это не только отточит ваши главные навыки, такие
как программирование и отладка, но, если рассматривать эти утилиты как насто
ящие проекты (т. е. завершать их к сроку и с высоким качеством), то вы разовьете
и дополнительные навыки, такие как планирование проектов и оценка графика
исполнения.
Кстати, завершенные утилиты — прекрасный материал, который можно пока
зать на собеседовании при приеме на работу. Хотя очень немногие программис
ты берут свои программы на собеседования, работодатели рассматривают таких
кандидатов в первую очередь. То, что вы располагаете рядом работ, выполненных
в свободное время дома, — свидетельство того, что вы можете завершать свои ра
боты самостоятельно и что вы увлечены программированием, а это позволит вам
практически сразу войти в состав 20% лучших программистов.
Если же мне было нужно больше узнать о языках, технологиях и ОС, очень
помогало знакомство с текстом программ других разработчиков. Большое коли
чество текстов программ, с которыми можно познакомиться, витает в Интернете.
Запуская разные программы под отладчиком, вы можете увидеть, как другие бо
рются с ошибками. Если чтото мешает вам написать утилиту, вы можете просто
добавить функцию к одной из утилит из числа найденных.
Для изучения технологии, ОС и виртуальной машины (процессора) можно
порекомендовать и методику восстановления алгоритма (reverse engineering). Это

18

ЧАСТЬ I

Сущность отладки

ускорит ваше изучение языка ассемблера и функций отладчика. Прочитав главы
6 и 7, вы будете достаточно знать о промежуточном языке (Microsoft Intermediate
Language, MSIL) и языке ассемблера IA32. Я не советовал бы вам начинать с пол
ного восстановления текста загрузчика ОС — лучше начать с задач поскромнее.
Так, весьма поучительно познакомиться с реализацией CoInitializeEx для неуправ
ляемого кода и класса System.Diagnostics.TraceListener — для управляемого.
Чтение книг и журналов, написание утилит, знакомство с текстами других
программистов и восстановление алгоритмов — отличные способы повысить
мастерство отладки. Однако самые лучшие ресурсы — это ваши друзья и коллеги.
Не бойтесь спрашивать их, как они сделали чтолибо или как чтото работает; если
их не поджимают сроки, они должны быть рады помочь вам. Я люблю, когда люди
задают мне вопросы, так как сам узнаю больше, чем те, кто задает мне вопросы! Я
постоянно читаю программистские группы новостей, особенно то, что пишут
парни из Microsoft, кого называют MVP (Most Valuable Professionals, наиболее ценные
профессионалы).

Процесс отладки
В заключение обсудим процесс отладки. Трудновато было определить процесс,
работающий для всех видов ошибок, даже «глюков» (которые, кажется, падают с
Луны и никакого объяснения не имеют). Основываясь на своем опыте и беседах
с коллегами, я со временем понял подход к отладке, которому интуитивно следу
ют все великие программисты, а менее опытные (или просто слабые) часто не счи
тают очевидным.
Как вы увидите, чтобы реализовать этот процесс отладки, не нужно быть семи
пядей во лбу. Самое трудное — начинать этот процесс каждый раз, приступая к
отладке. Вот девять шагов, связанных с рекомендуемым мной подходом к отладке
(рис. 11).
쮿 Шаг 1. Воспроизведи ошибку.
쮿 Шаг 2. Опиши ошибку.
쮿 Шаг 3. Всегда предполагай, что ошибка твоя.
쮿 Шаг 4. Разделяй и властвуй.
쮿 Шаг 5. Мысли творчески.
쮿 Шаг 6. Усиль инструментарий.
쮿 Шаг 7. Начни интенсивную отладку.
쮿 Шаг 8. Проверь, что ошибка исправлена.
쮿 Шаг 9. Научись и поделись.
В зависимости от ошибки вы можете полностью пропустить некоторые шаги,
если проблема и место ее возникновения совершенно очевидны. Вы всегда долж
ны начинать с шага 1 и пройти через шаг 2. Однако гдето между шагами 3 и 7 вы
можете найти решение и исправить ошибку. В таких случаях, исправив ошибку,
перейдите к шагу 8 для проверки сделанных исправлений.

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

19

Воспроизведи ошибку

Опиши ошибку

Всегда предполагай,
что ошибка твоя

Разделяй и властвуй

Мысли творчески

Сформулируй
новую гипотезу

Усиль инструментарий

Исправь
ошибку

Начни
интенсивную отладку

Проверь, что ошибка
исправлена
Исправленная ошибка
Научись и поделись

Рис. 11.

Процесс отладки

Шаг 1. Воспроизведи ошибку
Воспроизведение ошибки — наиболее критичный шаг в процессе отладки. Иног
да это трудно или даже невозможно сделать, но, не повторив ошибку, вы, возможно,
не устраните ее. Пытаясь повторить ошибку, можно дойти до крайности. У меня
была ошибка, которую я не мог повторить простым перезапуском программы.
Я думал, что какоето сочетание данных могло быть причиной, но, когда я запус
кал программу под отладчиком и вводил данные, необходимые для повтора ошибки,
прямо в память, все работало. Если вы сталкиваетесь с проблемами синхрониза
ции, возможно, вам придется предпринять некоторые действия по загрузке тех
же задач для повторения состояния, при котором возникала ошибка.
Теперь вы, возможно, думаете: «Ну, вот! Конечно же, главное воспроизвести
ошибку. Если бы я всегда смог это сделать, мне бы не нужна была ваша книга!»
Все зависит от вашего определения слова «воспроизводимость». Мое определение —
воспроизведение ошибки на одной машине один раз в течение 24 часов. Этого
достаточно для моей компании, чтобы начать работу над ошибкой. Почему? Все

20

ЧАСТЬ I

Сущность отладки

просто. Если вам удается повторить ошибку на одной машине, то на 30 машинах
вам удастся повторить ее 30 раз. Люди сильно заблуждаются, если пытаются по
вторить ошибку на всех доступных машинах. Если у вас есть 30 человек, чтобы
долбить по клавишам, — хорошо. Однако наибольшего эффекта можно добиться,
автоматизировав UI, чтобы вывести ошибку на чистую воду. Вы можете восполь
зоваться программой Tester из главы 17 или коммерческими средствами регрес
сионного тестирования.
Если вам удалось повторить ошибку в результате какихто действий, оцените,
можете ли вы повторить ошибку, действуя в другом порядке. Какието ошибки
проявляются только при определенных действиях, другие могут быть воспроиз
ведены различными путями. Идея в том, чтобы посмотреть на поведение программы
со всех возможных точек зрения. Повторяя ошибку различными способами, вы
гораздо лучше ощущаете данные и граничные условия, вызывающие ее. Кроме того,
некоторые ошибки могут маскировать собой другие. Чем больше способов вос
произвести ошибку удастся найти, тем лучше.
Даже если не удается повторить ошибку, вы все равно должны ее зарегистри
ровать в протоколе ошибок системы. Если у меня есть ошибка, которую я не могу
повторить, я в любом случае регистрирую ее, помечая, что я не смог воспроизве
сти ее. Позже другой программист, ответственный за эту часть программы, будет
понимать, что здесь чтото не так. Регистрируя ошибку, которую вам не удалось
повторить, опишите ее как можно подробнее — описания может оказаться дос
таточно вам или другому специалисту, чтобы решить проблему в другой раз. Хо
рошее описание особенно важно, так как вы можете установить связь между раз
ными ошибками, которые не удалось воспроизвести, позволяя вам начать рассмот
рение различных вариантов поведения.

Шаг 2. Опиши ошибку
Если вы типичный студент технического колледжа, вы скорей всего любили ма
тематические и технические дисциплины и не жаловали гуманитарные. В реаль
ной жизни искусство писать почти столь же важно, как и ваше техническое мас
терство, так как вам нужно уметь описывать свои ошибки как устно, так и пись
менно. Сталкиваясь с ошибкой, надо всегда останавливаться после того, как ее
удалось воспроизвести, и описывать ее. В идеале вы это делаете в журнале регис
трации ошибок системы, и, даже если вы обязаны устранить эту ошибку, описать
ее также полезно. Описание ошибки часто помогает устранить ее. Я и не вспом
ню, сколько раз описание других специалистов помогало мне посмотреть на
ошибку с другой стороны.

Шаг 3. Всегда предполагай, что ошибка твоя
За все годы, что я занимаюсь разработкой ПО, лишь несколько раз я сталкивался
с ошибкой, причиной которой был компилятор или исполняющая среда. В слу
чае ошибки есть все шансы, что она ваша, и вы всегда должны считать и надеять
ся, что это так. Если источник ошибки — ваш код, вы по крайней мере можете
устранить ее. Если же виноват компилятор или среда, проблема серьезней. Вы
должны исключить любую возможность наличия ошибки в вашем коде, прежде
чем начнете тратить время на поиск ее гделибо еще.

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

21

Шаг 4. Разделяй и властвуй
Если вы воспроизвели ошибку и хорошо описали ее, у вас есть предположения о
ее природе и месте, где она прячется. На этом этапе вы начинаете приводить в
порядок и проверять свои предположения. Важно помнить фразу: «Обратись к
исходнику, Люк!» 3 . Отвлекитесь от компьютера, прочтите исходный текст и по
думайте, что происходит при работе программы. Чтение исходного текста заста
вит вас потратить больше времени на анализ проблемы. Взяв за исходную точку
состояние машины на момент аварийного завершения или появления проблемы,
проанализируйте различные сценарии, которые могли привести к этой части кода.
Если ваше предположение о том, что не так работает, не приводит к успеху, оста
новитесь и переоцените ситуацию. Вы уже знаете об ошибке немного больше —
теперь вы можете переоценить свое предположение и попробовать снова.
Отладка напоминает алгоритм поиска делением пополам. Вы делаете попыт
ки найти ошибку и с каждой итерацией, соответствующей очередному предпо
ложению, вы, надо надеяться, исключаете фрагменты программы, где ошибок нет.
Продолжая, вы исключаете из программы все больше и больше, пока ошибка не
окажется в какомто одном фрагменте. Так как вы продолжаете развивать гипоте
зу и узнаете все больше об ошибке, то можете обновить описание ошибки, отра
зив новые сведения. Делая это, я, как правило, проверяю трипять основательных
гипотез, прежде чем перейду к следующему шагу. Если вы чувствуете, что уже близко,
можете проделать немного «легкой» отладки на этом шаге для окончательной
проверки предположения. Под «легкой» я понимаю двойную проверку состояний
и значений переменных, не просматривая все подряд.

Шаг 5. Мысли творчески
Если ошибка, которую вы пытаетесь исключить, — одна из тех неприятных оши
бок, появляющихся только на определенных машинах или которую трудно вос
произвести, посмотрите на нее с разных точек зрения. Это шаг, на котором вы
должны начать думать о несоответствии версий, различиях в ОС, проблемах дво
ичных файлов или их установки и других внешних факторах.
Прием, который иногда, к моему удивлению, работает, состоит в том, чтобы
отключится от проблемы на деньдругой. Иногда вы так сосредоточены на про
блеме, что за деревьями леса не видите и начинаете пропускать очевидные фак
ты. Отключаясь от ошибки, вы даете шанс поработать над проблемой подсозна
нию. Я уверен, каждый из читающих эту книгу находил ошибки по дороге с рабо
ты домой. Конечно же, трудно отключиться от ошибки, если она задерживает
поставку и босс стоит у вас над душой.
В нескольких компаниях, в которых я работал, прерыванием наивысшего при
оритета было нечто называемое «разговор об ошибке». Это означает, что вы со

3

«Use the source, Luke!» — популярный у программистов каламбур, получившийся из
фразы героя «Звездных войн» ОбиВан Кеноби «Use the force, Luke!» («Применяй силу,
Люк»). Используется, когда хотят привлечь внимание к исходному тексту, вместо того
чтобы искать ответ на вопрос в конференциях или у службы поддержки. В форумах
и переписке чаще используют более экспрессивный, хоть и менее легитимный ко
роткий вариант: RTFS (Read The Fucking Source). — Прим. перев.

22

ЧАСТЬ I

Сущность отладки

вершенно выбиты из колеи и должны подробно обсудить ошибку с кемлибо. Идея
такова: вы идете в чейто кабинет и представляете свою проблему на доске. Сколько
раз я приходил в чужой кабинет, открывал маркер, касался им доски и решал
проблему, не проронив ни слова! Именно подготовка разума представить проблему
помогает миновать дерево, в которое вы уперлись, и увидеть лес. Человека для
разговора об ошибке вы должны выбрать не из числа коллег, с которыми вы тес
но работаете над той же частью проекта. Таким образом, вы можете быть увере
ны, что ваш собеседник не сделает тех же предположений о проблеме, что и вы.
Что интересно, этот «ктото» даже не обязательно должен быть человеком. Мои
кошки, оказывается, — прекрасные отладчики, и они помогли мне найти множе
ство мерзких ошибок. Я собирал их вместе, обрисовывал проблему на доске и давал
сработать их сверхъестественным способностям. Конечно же, было трудновато
объяснить происходящее почтальону, стоящему на пороге, учитывая, что в такие
дни я не принимал душ и ходил в одних трусах.
Есть один человек, с которым следует избегать разговора об ошибках, — это
ваша супруга или иная значимая для вас персона. Почемуто тот факт, что вы тес
но связаны с этим человеком, означает наличие неразрешимых проблем. Вы, воз
можно, видели это, пытаясь описать ошибку: глаза собеседника стекленеют, и он
или она вотвот упадет в обморок.

Шаг 6. Усиль инструментарий
Я никогда не понимал, почему некоторые компании позволяют своим програм
мистам разыскивать ошибки неделями, расходуя на это тысячи долларов, хотя
соответствующий инструментарий помог бы им найти эту ошибку (и все ошиб
ки, с которыми они встретятся в будущем) за считанные минуты.
Некоторые компании, такие как Compuware и Rational, разрабатывают прекрас
ные средства как для управляемого, так и для неуправляемого кода. Я всегда про
пускаю свои тексты программ через эти средства, прежде чем приступить к труд
ному этапу отладки. Так как ошибки неуправляемого кода всегда труднее найти,
чем ошибки управляемого, эти средства гораздо важнее. Compuware NuMega пред
лагает BoundsChecker (средство обнаружения ошибок), TrueTime (средство ана
лиза производительности) и TrueCoverage (средство исследования кода програм
мы). Rational предлагает Purify (обнаружение ошибок), Quantify (производитель
ность) и PureCoverage (средство исследования кода). Суть в том, что, если вы не
используете средства сторонних производителей для облегчения отладки своих
проектов, вы тратите на отладку больше времени, чем необходимо.
Для тех из вас, кто не знаком с этими средствами, объясню, что каждое из них
делает. Средство обнаружения ошибок, кроме всего прочего, следит за неправиль
ным обращением к памяти, некорректными параметрами вызовов системных API
и COMинтерфейсов, утечками памяти и ресурсов. Средство анализа производи
тельности помогает выследить, где ваше приложение работает медленно, — а это
всегда совсем не то место, что вы думаете. Информация об исследовании кода про
граммы полезна потому, что, если вы ищете ошибку, вы хотите видеть только ре
ально исполняемые строки программы.

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

23

Шаг 7. Начни интенсивную отладку
Я отличаю интенсивную отладку от легкой (упомянутой в шаге 4) по тому, что вы
делаете, применяя отладчик. При легкой отладке вы просматриваете только не
сколько состояний и парочку переменных. При интенсивной же вы проводите
много времени, исследуя действия программы. Именно во время интенсивной
отладки вы используете расширенные функции отладчика. Ваша цель — как мож
но больше тяжелой ноши отдать отладчику. В главах с 6 по 8 обсуждаются раз
личные расширенные функции отладчика.
Как и при легкой отладке, в случае интенсивной вам нужно иметь представле
ние о том, где может таиться ошибка, а затем применить отладчик для подтверж
дения предположения. Никогда не сидите в отладчике из любопытства. Я настоя
тельно советую записать ваше предположение до запуска отладчика. Это помо
жет полностью сосредоточится именно на том, чего вы пытаетесь достичь.
При интенсивной необходимо регулярно просматривать изменения, сделан
ные для устранения ошибок, обнаруженных с помощью отладчика. Мне нравится
работать на этом этапе на двух машинах, установленных рядом. В этом случае я
могу устранять ошибку, работая на одной машине, а на другой запускать ту же
программу в нормальных условиях. Основная идея заключается в двойной и трой
ной проверке любых внесенных изменений, чтобы не дестабилизировать нормаль
ную работу программы. Хочу предупредить: начальство терпеть не может, когда
вы проверяете программу только на предопределенных граничных условиях, а не
в нормальной среде.
Если вы правильно планируете проект, проводите отладку по шагам, описан
ным выше, и следуйте рекомендациям главы 2 — будем надеяться, вы не потрати
те много времени на интенсивную отладку.

Шаг 8. Проверь, что ошибка устранена
Если вы думаете, что ошибка окончательно устранена, следующий шаг в процес
се отладки — тестирование, тестирование и еще раз тестирование исправлений.
Я уже сказал о необходимости тестировать исправления? Если ошибка — в изо
лированном модуле в строке программы, вызываемой один раз, тестировать ис
правление просто. Если же был исправлен центральный модуль, особенно если
он управляет структурой данных или чемто подобным, то надо быть очень вни
мательным, чтобы исправление не вызвало дополнительных проблем или побоч
ных эффектов в других частях проекта.
При тестировании исправлений, особенно критических частей, надо прове
рить, что все работает при любом сочетании данных, хороших и плохих. Нет ничего
хуже, чем появление двух новых ошибок в результате исправления одной. Если
изменяете критический модуль, оповестите остальных членов коллектива о вне
сенных изменениях. Таким образом, они будут в курсе возможного появления
«волновых эффектов».

24

ЧАСТЬ I

Сущность отладки

Отладка: фронтовые очерки
Куда девалась интеграция?
Боевые действия
Один из программистов, с которым я работал в NuMega, думал, что нашел
серьезную ошибку в интеграции Visual C++ Integrated Development Environ
ment (VC IDE) компании NuMega, так как она не работала на его машине.
Для тех из вас, кто не знаком с VC IDE от NuMega, я немного расскажу о ней.
Интеграция программных продуктов от NuMega с VC IDE существует уже
много лет. Такая интеграция позволяет появляться окнам, панелям инстру
ментов и меню от NuMega непосредственно в среде VC IDE.

Исход
Этот программист потратил несколько часов, используя отладчик ядра Soft
ICE для поиска ошибки. Он установил точки останова практически по всей
ОС. Наконец он нашел свою «ошибку». Он заметил, что при запуске VC IDE
CreateProcess вызывается с указанием пути \\R2D2\VSCommon\MSDev98\Bin\
MSDEV.EXE вместо пути C:\VSCommon\MSDev98\Bin\MSDEV.EXE, с которым,
как он думал, должен происходить вызов. Иначе говоря, вместо запуска VC
IDE с его локальной машины (C:\VSCommon\MSDev98\Bin\MSDEV.EXE) он
запускал ее со своей старой машины (\\R2D2\VSCommon\MSDev98\Bin\
MSDEV.EXE). Как такое могло случиться?
Он только что получил новую машину и установил полностью VC IDE
компании NuMega для работы. Чтобы упростить себе жизнь, он скопиро
вал ярлыки рабочего стола (файлы LNK) со старой машины, на которой VC
IDE была установлена без средств интеграции, на свою новую машину, пе
ретащив ярлыки мышью. При перетаскивании ярлыков система изменяет
внутренние пути, чтобы отобразить размещение исходных файлов в новых
условиях. Поскольку он всегда запускал VC IDE щелчком ярлыка на рабо
чем столе, который ссылался на старую машину, то и в новых условиях он
также запускал VC IDE со старой машины.

Полученный опыт
Этот программист неправильно начинал отладку, сразу же запуская отлад
чик ядра, вместо попытки повторить проблему разными способами. На шаге
1 процесса отладки («Воспроизведи ошибку») я рекомендовал попытаться
повторить ошибку различными способами, чтобы убедиться, что перед вами
действительно ошибка, а не несколько ошибок, маскирующих и усложня
ющих другую. Если бы этот программист следовал шагу 5 («Мысли творчес
ки»), то он был бы освобожден от этой работы, потому что он сначала по
думал бы о проблеме вместо немедленного погружения в нее.

ГЛАВА 1

Ошибки в программах: откуда они берутся и как с ними бороться?

25

Шаг 9. Научись и поделись
Исправляя «хорошую» ошибку (т. е. потребовавшую усилий для того, чтобы ее найти
и исправить), вы должны быстро подвести итог пройденному. Мне нравится за
носить хорошие ошибки в журнал, чтобы позже можно было посмотреть, что я
делал правильно для поиска и решения проблемы. Но еще важнее: я хочу знать,
что я делал неправильно, чтобы обходить тупики и отлаживаться и находить
ошибки быстрее. Почти все о программировании вы узнаёте в процессе отладки,
поэтому необходимо использовать все возможности, чтобы извлекать из отладки
уроки.
Один из самых важных шагов, который необходимо сделать после исправле
ния хорошей ошибки, — поделиться с коллегами информацией, которую вы по
лучили, исправляя ошибку, особенно если эта ошибка специфична для проекта.

Последний секрет отладки
Я бы хотел поделиться с вами последним секретом отладки: отладчик может от
ветить на все ваши вопросы, только если вы задаете ему корректные вопросы. Еще
раз: я советую всегда иметь в голове предположение (чтото такое, что вы хотите
доказать или опровергнуть), прежде чем запустить отладчик. Как я рекомендовал
на шаге 7, до того как я прикоснусь к отладчику, я записываю свое предположе
ние, чтобы всегда быть уверенным, что у меня есть цель.
Помните: отладчик — это только инструмент вроде отвертки. Он делает толь
ко то, что вы ему прикажете. Настоящий отладчик — это мягкое вещество в ва
шем твердом черепе.

Резюме
В этой главе положено начало определению программных ошибок и описанию
решения связанных с ними проблем. Затем обсуждалось, что вы должны знать к
моменту начала отладки. Наконец, представлен процесс отладки, которому вы дол
жны следовать в работе над отладкой программы.
Лучший способ отладить программу — исключить ошибки. Если вы хорошо
планируете свои проекты, привержены качеству и знаете, как ваша разработка
связана с технологиями, ОС и процессором, вы можете минимизировать время,
затрачиваемое на отладку.

Г Л А В А

2
Приступаем к отладке

В этой главе я опишу важные инфраструктурные инструменты и требования, ко
торые помогут оптимизировать отладку приложений в процессе их создания.
Некоторые касаются разработки, а другие представляют собой программные ути
литы, однако все они имеют одну общую черту: они позволяют непрерывно сле
дить за развитием проекта. Я убежден, что постоянный контроль — один из важ
нейших факторов своевременной разработки качественного ПО.
Все идеи, описываемые в этой и следующей главах, основаны на моем опыте
работы над реальными программными продуктами, а также на консультировании
некоторых компаний. Я не могу представить себе работу без этих инструментов
и методов. Многим людям — и мне в том числе — пришлось потратить много сил,
чтобы извлечь эти важные уроки, и я с радостью поделюсь ими, чтобы помочь вам
сэкономить время и сохранить душевное спокойствие. Возможно, читателям, ра
ботающим в группах из двухтрех человек, покажется, что некоторые из моих
советов их не касаются, однако это не так. Как бы я ни работал над проектом — в
одиночку или в составе большой группы, — я подхожу к нему одинаково. Я при
нимал участие в самых разных проектах и знаю, что мои рекомендации приго
дятся любым группам разработчиков: и маленьким, и самым большим.

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

ГЛАВА 2

Приступаем к отладке

27

кументация на всем протяжении проекта ведется плохо, в результате чего един
ственными реальными документами становятся контрольные журналы систем
управления версиями и отслеживания ошибок.
Надеюсь, я вас убедил. Увы, я постоянно сталкиваюсь с группами, которые еще
не стали использовать эти инструменты, особенно системы отслеживания оши
бок. Как человек, интересующийся историей, я утверждаю: чтобы знать, куда вы
идете, вы должны знать, где вы находитесь сейчас и где были раньше. Единствен
ный гарантированный способ достижения этой цели — использование назван
ных мной систем. Наблюдая за частотой обнаружения и решения проблем, при
меняя систему отслеживания ошибок, можно точнее прогнозировать дату завер
шения работы над проектом. Система управления версиями дает представление
о степени изменения кода, благодаря чему можно определить объем дополнитель
ного тестирования. Кроме того, эти инструменты предоставляют единственный
эффективный способ оценки того, насколько действенны изменения, совершае
мые в ходе разработки ПО.
Если в вашей группе появится новый разработчик, эти инструменты окупятся
за один день. Пусть в самом начале он поработает с системами управления вер
сиями и отслеживания ошибок и проследит путь изменения проекта. Конечно,
идеально было бы иметь качественную проектную документацию, но если ее нет,
системы управления версиями и отслеживания ошибок по крайней мере предос
тавят информацию об эволюции кода и укажут на все проблемные области.
Я говорю об этих двух системах одновременно, потому что они неразделимы.
Система отслеживания ошибок фиксирует все события, которые могут привести
к изменению исходных текстов программы. Система управления версиями реги
стрирует все сделанные изменения. В идеале следует поддерживать связь между
обнаруженными проблемами и изменениями исходных кодов. Это позволяет оп
ределить причины и следствия исправления ошибок. Если вы не будете поддер
живать такую связь, вы будете часто удивляться некоторым изменениям кода про
граммы. Почти всегда при разработке более поздней версии программы прихо
дится искать программиста, внесшего то или иное изменение, при этом остается
только надеяться на то, что он помнит причину своего поступка.
Существуют интегрированные программные продукты, которые автоматичес
ки следят за связью изменений исходных текстов программы с ошибками. Если
такая возможность в вашей системе отсутствует, поддерживайте связь вручную.
Этого можно достигнуть, включая номер ошибки в комментарии, описывающие
метод ее исправления. Регистрируя измененный файл в системе управления вер
сиями, указывайте в комментарии к нему номер исправленной ошибки.

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

28

ЧАСТЬ I

Сущность отладки

файл в систему управления версиями, спросите себя, сможет ли эта информация
понадобиться в ближайшие пару лет сопровождающим программистам. Если да,
ей самое место в системе управления версиями.

Блочные тесты также нужно включать в систему управления версиями
Хотя я только что объяснил важность регистрации в системе управления версия
ми всего, что только может понадобиться, во многих компаниях этим советом
пренебрегают. Одна из крупнейших проблем, с которыми я когдалибо сталки
вался в компаниях по разработке ПО, возникла изза отсутствия в системе управ
ления версиями блочных тестов (unit test). Если термин «блочный тест» вам не
знаком, я вкратце поясню его. Блочный тест — это фрагмент кода, который уп
равляет выполнением основной программы. (Иногда эти тесты еще называют те
стовыми приложениями или средствами тестирования.) Это тестовый код, со
здаваемый разработчиком программы для проведения тестирования «прозрачного
ящика», или «белого ящика»1 , позволяющего удостовериться в том, что основные
операции программы действительно имеют место. Подробное описание блочных
тестов см. в главе 25 «Блочное тестирование» книги Стива Макконнелла (Steve
McConnell. Code Complete. — Microsoft Press, 1993).
Включение блочных тестов в систему управления версиями позволяет достиг
нуть двух основных целей. Вопервых, вы облегчите труд разработчиков, которые
будут сопровождать программы. Очень часто при модернизации или исправле
нии кода им — а таким человеком вполне можете оказаться вы сами — приходит
ся изобретать колесо. Это не только требует огромных усилий, но и понастоя
щему удручает. Вовторых, вы упростите сотрудникам отдела контроля качества
общее тестирование программы, благодаря чему они смогут сосредоточиться на
более важных областях тестирования, таких как производительность и масшта
бируемость программы, а также ее полнота и соответствие требованиям заказчи
ка. Обязательное включение блочных тестов в систему управления версиями —
один из признаков опыта и профессионализма.
Конечно, регистрация блочных тестов в системе управления версиями авто
матически означает, что нужно будет поддерживать их соответствие изменениям
кода. Да, это возлагает на вас дополнительную ответственность, однако нет ниче
го хуже, когда сотрудник, осуществляющий поддержку программы, преследует вас
и упрекает в разгильдяйстве за то, что блочные тесты больше не работают. Уста
ревшие блочные тесты в системе управления версиями — большее зло, чем их
полное отсутствие.
Просмотрев исходные коды программ, прилагаемых к книге, вы заметите, что
все мои блочные тесты входят в их состав. Есть даже отдельный сценарий, позво
ляющий автоматически создать все блочные тесты для всех моих утилит и при
меров. В этой книге я рекомендую только то, что использую сам.
Некоторые читатели, возможно, думают, что поддержка блочных тестов, которую
я так отстаиваю, потребует массы дополнительной работы. В действительности
это не совсем так, потому что большинство разработчиков (я очень на это наде

1

Glass box, white box — программа, поведение которой строго детерминировано. —
Прим. перев.

ГЛАВА 2

Приступаем к отладке

29

юсь!) уже проводит блочное тестирование. Я только советую регистрировать эти
тесты в системе управления версиями, своевременно обновлять их, а также, воз
можно, написать какойлибо сценарий для их компиляции и компоновки. Следуя
правильным методам работы вы сэкономите огромное количество времени. На
пример, большую часть программ для этой книги я разрабатывал на компьютере
с Microsoft Windows 2000 Server. Чтобы сразу приступить к тестированию на ком
пьютере с Microsoft Windows XP, мне нужно было только извлечь код тестов из
системы управления версиями и выполнить сценарии их создания. Многие про
граммисты разрабатывают одноразовые блочные тесты, чем осложняют тестиро
вание в среде других ОС изза невозможности легкого переноса блочных тестов
на другую платформу и их компиляции и компоновки. Если все члены группы будут
включать блочные тесты в свой код, это позволит им сэкономить много недель
работы.

Контроль над изменениями
Отслеживание изменений имеет огромное значение, однако наличие хорошей
системы отслеживания ошибок не означает, что разработчикам разрешается вно
сить крупномасштабные изменения в исходные коды программы, когда захочет
ся. Это сделало бы все отслеживание изменений бесполезным. Идея в том, чтобы
контролировать изменения в ходе разработки программы, ограничивая права на
совершение определенных типов изменений на определенных этапах проекта; это
позволяет постоянно иметь представление о состоянии общих исходных кодов
группы. О наилучшей схеме контроля над изменениями, о которой я когдалибо
слышал, мне рассказал мой друг Стив Маньян (Steve Munyan); он называет ее «Зе
леный, желтый и красный период». В зеленый период любой разработчик может
регистрировать любые измененные файлы в общих исходных кодах группы. На
чальные стадии проекта обычно полностью выполняются в зеленом периоде,
потому что в это время группа разрабатывает новые функции программы.
Желтый период наступает, когда проект входит в стадию исправления ошибок
или приближается к прекращению разработки нового кода. В это время изменять
код разрешается только для исправления ошибок. Добавлять к программе новые
функции и вносить в нее другие изменения нельзя. Чтобы исправить ошибку,
разработчик должен получить разрешение у технического лидера группы или
руководителя проекта. Исправляя ошибку, он должен описать свои действия и на
что они влияют. При этом каждое исправление ошибки превращается по сути в
миниобзор кода. Выполняя такой обзор кода, важно помнить об использовании
утилиты различия версий из состава системы управления версиями, чтобы гаран
тировать, что произошли именно те и только те изменения, которые были запла
нированы. В некоторых группах, в которых я работал, проект находился в стадии
желтого периода с самого начала, потому что группе нравилось проводить обзо
ры кода, требуемые на этом этапе. Мы несколько смягчали требования и позво
ляли обращаться за утверждением изменений к любому другому члену группы.
Интересно, что изза постоянных обзоров кода разработчики находили много
ошибок еще до регистрации файлов в общих исходных кодах группы.
Красный период начинается, когда вы прекращаете разрабатывать новый код
или приближаетесь к важной контрольной точке. На этом этапе все изменения

30

ЧАСТЬ I

Сущность отладки

кода требуют утверждения руководителя проекта. Когда я был руководителем
проекта (член группы, ответственный за код в целом), я даже шел на изменение
прав доступа к системе управления версиями, разрешая членам группы только
чтение информации, но не запись. Я делал это главным образом потому, что знал
ход мысли разработчиков: «Это всего лишь небольшое изменение; я исправлю
ошибку, и это больше ни на что не повлияет». Несмотря на благие намерения, одно
небольшое изменение могло означать, что вся группа должна будет начать план
тестирования с нуля.
Руководитель проекта должен строго придерживаться правила красного пери
ода. Если выполнение программы приводит к воспроизводимой критической
ошибке или искажению данных, решение об изменении принимается автомати
чески, потому что оно необходимо. Однако обычно принять решение об исправ
лении конкретной проблемы не так легко. Чтобы решить, насколько важным яв
ляется исправление ошибки, я всегда задавал себе следующие вопросы, держа в
уме интересы компании:
쮿 скольких людей касается эта проблема?
쮿 затронет изменение ядро или второстепенную часть программы?
쮿 если изменение будет сделано, какие компоненты приложения придется тес
тировать заново?
Позвольте мне дополнить этот список некоторыми конкретными цифрами и опи
сать общие правила для стадий бетатестирования. Если проблема серьезна, т. е.
приводит к аварийному завершению программы или искажению данных и, веро
ятно, коснется более 15% наших внешних тестировщиков, решение об ее исправ
лении принимается автоматически. Если ошибка приводит к изменению файла
данных, я также принимаю решение об ее исправлении, чтобы позднее нам не
пришлось изменять форматы файлов и чтобы бетатестировщики могли получить
более объемные наборы данных для последующих бетаверсий программы.

Важность меток
Команда записи метки — одна из наиболее важных команд при работе с систе
мой управления версиями. В Microsoft Visual SourceSafe она называется меткой
(label), в MKS Source Integrity — контрольной точкой (checkpoint), а в PVCS Version
Manager — меткой версии (version label). Но, как бы она ни называлась, метка ука
зывает на конкретный набор общих для группы исходных текстов программы.
Метка позволяет получить нужную версию исходных кодов программы. Если вы
создадите ошибочную метку, возможно, вы никогда не получите исходные коды,
использованные для создания конкретной версии программы, и не сможете об
наружить причину ее отказа.
Я всегда помечаю:
1. достижение всех контрольных точек работы над программой;
2. все переходы между зеленым, желтым и красным периодами разработки;
3. все компоновки (builds), отсылаемые за пределы группы;
4. все ветви дерева разработки, создаваемые в системе управления версиями;
5. правильное выполнение ежедневной компоновки программы и дымовых тес
тов (о них см. ниже одноименный раздел этой главы).

ГЛАВА 2

Приступаем к отладке

31

Во всех случаях я следую схеме: , чтобы названия меток были описательными.
Есть и третье правило записи меток, о котором многие забывают. Сотрудники
отдела контроля качества обычно работают с компоновками контрольных точек
и ежедневными компоновками, поэтому, сообщая об ошибках, они имеют в виду
конкретные версии программы. Разработчики изменяют код довольно быстро,
поэтому нужно позаботиться, чтобы можно было легко вернуться к версии фай
лов, нужных для воспроизведения ошибки.

Стандартный вопрос отладки
Что делать, если мы не можем воспроизвести компоновку,
посланную за пределы группы?
Отсылая компоновку за пределы группы, обязательно делайте полную ко
пию ее каталога на CD/DVD или ленточном накопителе. Копия должна вклю
чать все исходные и промежуточные файлы программы, файлы символов
и окончательный результат. Включайте в нее также пакет для установки,
отсылаемый заказчику. Следует даже подумать о создании копии средств
компоновки программы. CD/DVD и ленточные накопители — очень недо
рогой способ страховки от будущих проблем.
Даже когда я делал все для сохранения конкретной компоновки в сис
теме управления версиями, повторное создание программы порой приво
дило к получению двоичных файлов, отличающихся от первоначальной
версии. Имея архив полного дерева компоновки, вы сможете отлаживать
программы пользователей при помощи тех же двоичных файлов, которые
вы им в свое время послали.

Системы отслеживания ошибок
Система отслеживания ошибок не только накапливает сведения об ошибках, но
и является прекрасным средством для хранения разных заметок и списка зада
ний, особенно на этапе разработки исходного кода. Некоторые программисты
любят хранить заметки и списки заданий в мобильных ПК, но при этом важная
информация часто теряется среди отладочных файлов со случайными шестнад
цатеричными данными и рисунков, выполненных для борьбы со сном во время
планерок. Сохранив эти заметки в системе отслеживания ошибок и пометив, что
они принадлежат вам, вы консолидируете их в одном месте, что облегчит их по
иск. Кроме того, хотя вам, возможно, нравится думать, что код, над которым вы
работаете, «принадлежит» вам, на самом деле это не так — он принадлежит груп
пе. Если вы будете хранить свой список заданий в системе отслеживания ошибок,
другие члены группы, которым понадобится ваш код, смогут проверить ваш спи
сок и узнать, что именно вы сделали. И еще одно преимущество хранения спис
ков заданий и заметок в системе отслеживания ошибок: они будут постоянно
напоминать вам, что нужно сделать, поэтому вам не придется лихорадочно отла
живать ошибку в последний момент изза того, что вы про нее забыли или поче
мулибо еще. Я использую систему отслеживания ошибок постоянно, чтобы важ

32

ЧАСТЬ I

Сущность отладки

ные заметки и задания можно было внести в нее сразу, как только они придут мне
в голову.
Я люблю назначать заметкам и спискам заданий в системе отслеживания ошибок
наименьший приоритет ошибок. Это позволяет отделить их от настоящих оши
бок, и в то же время ничто не мешает повысить их приоритет, если надо. При этом
следует организовать методику сообщений об ошибках так, чтобы они не вклю
чали кодов ошибок с наименьшим приоритетом, иначе можно будет запутаться.
Не бойтесь изучать данные системы отслеживания ошибок: они содержат всю
правду о проекте. Планируя модернизацию программы, поработайте с системой
отслеживания ошибок и найдите модули или функции, в которых было зарегис
трировано наибольшее число проблем. Выделите некоторое время на то, чтобы
члены группы лучше поработали над соответствующими разделами программы.
При внедрении системы отслеживания ошибок убедитесь, что к ней имеют
доступ все, кому это нужно: как минимум, это члены групп разработки и техни
ческой поддержки. Если система отслеживания ошибок позволяет назначить раз
ные уровни доступа, возможно, стоит разрешить соответствующий доступ к ней
другим людям, скажем, инженерам по сбыту (технические эксперты, работающие
в торговых организациях и оказывающие продавцам помощь при продаже слож
ной продукции) и сотрудникам отдела маркетинга. Например, некоторым членам
отделов продаж и маркетинга можно разрешить регистрировать сообщения об
ошибках и запросы о реализации функций, но не получать информацию об об
наруженных ошибках. Представители этих двух групп, как правило, больше об
щаются с клиентами, чем обычные инженеры, и могут предоставить бесценную
обратную связь. Естетственно, вы должны обучить их составлению отчетов об
ошибках. Они с радостью согласятся помочь, но им нужно дать необходимые
указания, чтобы они делали это правильно. Если представители этих двух групп
будут оставлять свои запросы и сообщения о проблемах в той же системе, что и
другие сотрудники, эффективность работы с ней только повысится. Идея систе
мы отслеживания ошибок как раз в том, чтобы все сообщения об ошибках и за
просы о реализации функций находились в одном месте. Если эта информация
будет храниться в разных местах — в электронном почтовом ящике руководите
ля проекта, в записных книжках инженеров и, конечно, в системе отслеживания
ошибок — уследить за ней будет гораздо сложнее.

Выбор правильных систем
Система управления версиями должна соответствовать вашим потребностям.
Очевидно, если вы работаете в компании с требованиями класса «highend», та
кими как поддержка нескольких платформ, вам скорее всего придется выбирать
более дорогую систему или использовать решение с открытым кодом, скажем, CVS.
Если же вы работаете в небольшой группе и разрабатываете программу только для
Windows, можно рассмотреть варианты подешевле. Потратьте некоторое время на
тщательную оценку системы, которую вы планируете внедрить, уделив особое
внимание прогнозированию будущих потребностей. Вам придется работать с
системой управления версиями довольно долго, поэтому убедитесь, что она бу
дет развиваться вместе с вашим проектом. Выбор правильной системы управле

ГЛАВА 2

Приступаем к отладке

33

ния версиями очень важен, но еще важнее, чтобы вы вообще ее использовали: хоть
какаято система управления версиями лучше, чем никакая.
Я знаю массу случаев, когда разработчики пытались использовать собственные
системы отслеживания ошибок, однако я настоятельно рекомендую потратить
средства на коммерческий продукт или использовать решение с открытым кодом.
Информация системы отслеживания ошибок слишком важна, чтобы ее хранение
можно было доверить приложению, которое трудно поддерживать и которое не
сможет развиваться вместе с вашими потребностями в течение длительного сро
ка. Кроме того, это позволяет избежать траты времени на разработку внутренней
системы вместо работы над коммерческой программой.
При выборе системы отслеживания ошибок следует руководствоваться теми
же критериями, что и при выборе системы управления версиями. Однажды я, как
руководитель проекта, выбрал систему отслеживания ошибок, не уделив должно
го внимания ее наиболее важной части, составлению отчетов об ошибках. Систе
ма была достаточно проста в плане внедрения и использования, однако поддер
жка отчетов в ней была столь ограниченна, что сражу же по достижении первой
внешней контрольной точки нам пришлось переносить все наши ошибки на другую
систему. Мне было очень неловко перед коллегами за эту оплошность.
Как я уже говорил, очень важно, чтобы система отслеживания ошибок обеспе
чивала интеграцию с системой управления версиями. Большинство систем управ
ления версиями для Windows поддерживают Интерфейс контроля над исходным
кодом Microsoft (Microsoft Source Code Control Interface, MSSCCI). Если ваша сис
тема отслеживания ошибок также поддерживает MSSCCI, вы сможете согласовать
исправления ошибок с конкретными версиями файлов.
Некоторые люди называют код «кровью» группы разработчиков. Если это так,
то системы управления версиями и отслеживания ошибок — артерии, от которых
зависит правильное кровообращение. Не работайте без них.

Планирование времени
построения систем отладки
Составляя план и расписание проекта, выделите время на создание систем отлад
ки. Вам нужно заранее решить, как вы будете реализовывать обработчики крити
ческих ошибок (см. главу 13), средства создания дампов файлов и другие инстру
менты, которые понадобятся для воспроизведения реальных проблем. Мне все
гда нравилось рассматривать системы обработки ошибок так, как если бы они
являлись одной из функций программы. Это позволяет другим сотрудникам ком
пании узнать, что вы собираетесь делать с ошибками при их появлении.
Планируя системы отладки, разработайте политику предупредительной отладки.
Первые и наиболее сложные этапы этого процесса заключаются в определении
способа возвращения ошибок. Какое бы решение вы ни приняли, всегда исполь
зуйте только один способ. Мне известен один давний проект (к счастью я в нем
не участвовал), в котором применялись три способа возвращения ошибок: при
помощи возвращаемых функциями значений, при помощи исключений setjmp/
longjmp и при помощи глобальной переменной, аналогичной переменной errno
стандартной библиотеки C. Эти разработчики провели немало тяжелых минут,
пытаясь отследить ошибки, пересекающие границы различных подсистем.

34

ЧАСТЬ I

Сущность отладки

При разработке приложений для платформы .NET выбрать способ обработки
ошибок довольно легко. Вы можете или продолжать использовать возвращаемые
значения, или применить исключения. Привлекательность .NET в том, что в от
личие от неуправляемого кода в ней есть стандартный класс исключений, Sys
tem.Exception, который является базовым для всех прочих исключений. Механизм
исключений .NET имеет и один недостаток: вам все же придется вести подроб
ную документацию и проводить инспекцию кода программы, чтобы точно знать,
какое исключение генерируется методом. Как вы увидите по моим программам,
для сообщений о нормальном завершении блока программы и ожидаемых ошибках
я все же предпочитаю использовать возвращаемые значения, так как это немного
быстрее, чем выполнение кода throw и catch. Однако для всех непредвиденных
ошибок я всегда использую исключения.
С другой стороны, при написании неуправляемого кода вы по сути вынужде
ны применять только возвращаемые значения. Проблема в том, что в C++ нет стан
дартного класса исключений, генерируемых автоматически, а такие технологии,
как COM, не позволяют исключениям пересекать границы адресного простран
ства отделенного потока или процесса. Как я покажу в главе 13, исключения C++ —
одна из самых проблемных в смысле производительности и ошибок областей. Ока
жите себе большую услугу и забудьте об исключениях в языке C++. С теоретичес
кой точки зрения они великолепны, но реальность далеко не всегда соответству
ет теории.

Создавайте все компоновки с использованием
символов отладки
Некоторые из моих советов по поводу систем отладки не вызывают никаких со
мнений. Так, я годами твержу о том, что все компоновки, в том числе заключи
тельные (release), нужно создавать, применяя полный набор символов отладки —
данные, позволяющие отладчику показывать исходные тексты, номера строк, имена
переменных и информацию о типах данных вашей программы. Вся эта инфор
мация хранится в файлах с расширением .PDB (Program Database, база данных
программы), связанных с конкретными модулями. Разумеется, если вы работаете
на условиях почасовой оплаты, нет ничего плохого в том, чтобы проводить все
рабочее время за отладкой на уровне ассемблера. Увы, большинство из нас не может
позволить себе такой роскоши и поэтому нуждается в средствах быстрого обна
ружения ошибок.
Конечно, у отладки заключительных компоновок при помощи символов есть
свои минусы. Например, оптимизированный код, создаваемый компилятором по
требованию (justintime compiler, JIT compiler) или компилятором неуправляемого
кода, не всегда соответствует потоку исполнения исходного кода, поэтому рабо
тать с заключительным кодом сложнее, чем с отладочным. Другая проблема ис
следования неуправляемых заключительных компоновок в том, что компилятор
иногда оптимизирует регистры стека так, что это не позволяет увидеть полный
стек вызовов, как при обычной отладочной компоновке. Кроме того, при вклю
чении символов отладки в двоичный файл он слегка увеличивается изза строки
раздела отладки, определяющей файл .PDB. Однако эти несколько байтов — нич

ГЛАВА 2

Приступаем к отладке

35

то в сравнении с тем, насколько символы облегчают и ускоряют исправление
ошибок.
В проектах, создаваемых при помощи мастеров (wizard), отладочные симво
лы для заключительных компоновок применяются по умолчанию, но при необ
ходимости это можно сделать и вручную. Если вы работаете над проектом C#, от
кройте диалоговое окно Property Pages (страницы свойств) (рис. 21) и выберите
папку Configuration Properties (свойства конфигурации). Щелкните в раскрываю
щемся списке Configuration (конфигурация) пункт All Configurations (все конфи
гурации) или Release (заключительная конфигурация); выберите в папке Configu
ration Properties страницу свойств Build (компоновка программы) и задайте в поле
Generate Debugging Information (генерировать отладочную информацию) значе
ние True. Это устанавливает флаг /debug:full для компилятора CSC.EXE.
По непонятной мне причине диалоговое окно Property Pages проекта, разра
батываемого в среде Microsoft Visual Basic .NET, отличается от аналогичного окна
проекта C#, однако ключ компилятора в обоих случаях один и тот же. Подключе
ние полного набора отладочных символов для заключительных компоновок Visual
Basic .NET показано на рис. 22. Откройте диалоговое окно проекта Property Pages
и выберите папку Configuration Properties. Щелкните в раскрывающемся списке Con
figuration пункт All Configurations или Release; выберите в папке Configuration Pro
perties страницу свойств Build и установите флажок Generate Debugging Information.

Рис. 21.

Включение генерирования отладочной информации для проекта C#

Чтобы включить создание PDBфайла для неуправляемой программы C++, нужно
задать компилятору ключ /Zi. Откройте в окне Property Pages папку C/C++ стра
ницу свойств General (общие свойства) и задайте в поле Debug Information Format
(формат отладочной информации) значение Program Database (/Zi). Убедитесь, что
вы не выбрали пункт Program Database For Edit & Continue (база данных программы
для режима «отредактировать и продолжить»), а то заключительная компоновка
окажется большой и медленной, так как в нее будет занесена вся дополнительная
информация, необходимая для специфического режима отладки, позволяющего
внести изменение в программу и продолжить ее выполнение. Правильные пара
метры компилятора см. на рис. 23, где показаны и другие параметры, позволяю
щие оптимизировать создание компоновок; их я опишу в разделе «Какие допол

36

ЧАСТЬ I

Сущность отладки

нительные параметры компилятора и компоновщика помогут заранее позаботиться
об отладке неуправляемого кода?».

Рис. 22. Включение генерирования отладочной информации
для проекта Visual Basic .NET

Рис. 23. Настройка компилятора C++ для генерирования
отладочной информации
После установки ключа компилятора вам понадобится задать соответствующие
ключи компоновщика: /INCREMENTAL:NO, /DEBUG и /PDB. Для указания параметров ком
поновки с приращением нужно открыть окно Property Pages, выбрать папку Linker
(компоновщик), страницу свойств General и задать соответствующее значение в
поле Enable Incremental Linking (включить компоновку с приращением). Распо
ложение ключа см. на рис. 24.
Выберите в окне Property Pages папку Linker, перейдите на страницу Debugging
(отладка) и задайте в поле Generate Debug Info (генерировать отладочную инфор
мацию) значение Yes (/DEBUG). Чтобы задать ключ /PDB, введите в поле Generate
Program Database File (генерировать файл базы данных программы), находящее
ся сразу же под полем Generate Debug Info, значение $(Каталог_файла)/$(Наз
вание_проекта).PDB. Если вы не заметили, в системе проектов Microsoft Visual Studio

ГЛАВА 2

Приступаем к отладке

37

.NET наконецто решены серьезные проблемы предыдущих версий, связанные с
общими ключами компоновки. Значения, начинающиеся с символа $ и заключен
ные в скобки, являются макрокомандами, о назначении которых часто можно
догадаться по названиям. Об остальных макрокомандах можно узнать, щелкнув
на странице свойств почти любое поле ввода и выбрав из списка пункт .
Во всплывающем диалоговом окне будут указаны все макрокоманды и во что они
преобразуются. Установка ключей /DEBUG и /PDB показана на рис. 25. Остальные
параметры важны для неуправляемого кода C++. Я опишу их в разделе «Какие
дополнительные параметры компилятора и компоновщика помогут заранее по
заботиться об отладке неуправляемого кода?».

Рис. 24.

Отключение компоновки с приращением для компоновщика C++

Правильная настройка создания отладочных символов для C++ требует зада
ния еще двух ключей: /OPT:REF и /OPT:ICF. Они находятся в папке Linker на страни
це Optimization (рис. 26). Выберите в разделе References (ссылки) значение Elimi
nate Unreferenced Data (/OPT:REF) (удалять неиспользуемые данные). В поле Enable
COMDAT Folding (удаление избыточных записей COMDAT) выберите Remove Redun
dant COMDATs (/OPT:ICF) (удалять избыточные записи COMDAT). При установлен
ном ключе /DEBUG компоновщик включает в итоговый файл все функции незави
симо от того, вызываются они или нет; в случае отладочных компоновок это за
дано по умолчанию. Ключ /OPT:REF указывает компоновщику включать в итоговый
файл только те функции, что вызываются программой. Если вы забудете добавить
ключ /OPT:REF, заключительное приложение будет содержать функции, которые
никогда не вызываются, что сделает его гораздо более объемным, чем следовало
бы. Ключ /OPT:ICF задает комбинирование идентичных записей данных COMDAT,
так что для всех ссылок на постоянное значение у вас будет только одна констан
тная переменная.
После создания заключительных компоновок с PDBфайлами, содержащими
полную информацию, храните эти файлы в безопасном месте с двоичными фай
лами, которые вы поставляете заказчику. В случае утраты PDBфайлов вам при
дется вернуться к отладке на уровне ассемблера. Обращайтесь с ними так же, как
с распространяемыми двоичными файлами.

38

ЧАСТЬ I

Сущность отладки

Рис. 25.

Настройка отладочных параметров компоновщика C++

Рис. 26.

Оптимизация компоновщика C++

Если мысль о ручном изменении параметров проекта с целью его компонов
ки с символами отладки, а также о правильном указании всех остальных ключей
компоновки внушает вам страх, не волнуйтесь, все не так уж и плохо. Для главы 9
я написал очень полезный модуль надстройки SettingsMaster, который возьмет всю
работу по изменению параметров проекта на себя. SettingsMaster по умолчанию
сконфигурирован так, чтобы задавать все ключи, рекомендуемые в этой главе.

При работе над управляемым кодом рассматривайте
предупреждения как ошибки
Если вы писали на управляемом коде чтонибудь более серьезное, чем «Hello World!»,
вы наверняка заметили, что его компиляторы гораздо строже относятся к ошиб
кам компилирования. Программисты, привыкшие работать с C++ и не очень хо
рошо знакомые с .NET, часто удивляются числу дополнительных ограничений этой
платформы: например, в C++ вы могли приводить переменные почти к любому
типу, и компилятор смотрел на это сквозь пальцы. Компиляторы управляемого кода

ГЛАВА 2

Приступаем к отладке

39

не только гарантируют явный тип данных, но и помогут в исправлении ошибок,
но для этого их нужно настроить, скажем, сделать инструменты как можно более
интеллектуальными.
Если вы откроете документацию к Visual Studio .NET, выберете панель Contents
(содержание) и перейдете к разделу Visual Studio .NET\Visual Basic and Visual
C#\Reference\Visual C# Language\C# Compiler Options\Compiler Errors CS0001 Thro
ugh CS9999, вы увидите список всех ошибок компилятора C#. (Ошибки компиля
тора Visual Basic .NET также включены в документацию, но, к великому удивлению,
они не проиндексированы в разделе Contents.) Просматривая список ошибок, вы
заметите, что некоторые из них называются предупреждениями (Compiler Warning)
и имеют определенный уровень диагностики, например, Compiler Warning (level 4)
CS0028. Затем вы обнаружите уровни диагностики от 1 до 4. Генерируя предуп
реждение, компилятор сообщает, что конкретная конструкция исходного кода пра
вильна с точки зрения синтаксиса, но может быть неверна в данном контексте. В
качестве показательного примера можно привести предупреждение CS0183 (The
given expression is always of the provided (‘type’) type [Данное выражение всегда имеет
тип (‘type’)]), проиллюстрированное следующим фрагментом кода:

// Генерирует предупреждение CS0183, потому что строка (или, точнее, любой
// тип в .NET) ВСЕГДА имеет базовый тип Object.
public static void Main ( )
{
String StringOne = " Something pithy. . ." ;
if ( StringOne is String )
// CS0183
{
Console.WriteLine ( StringOne ) ;
}
}
Если компилятор настолько любезен, что сообщает обо всех контекстуальных
проблемах подобного рода, разве разумно не обращать на них внимания? Я не
люблю называть их предупреждениями, так как на самом деле это ошибки. Если
вы когданибудь интересовались разработкой компиляторов, особенно синтакси
ческим анализом, вероятно, вам в голову приходили две мысли: вопервых, что
синтаксический анализ очень сложен, и вовторых, что люди, создающие компи
ляторы, сделаны из особого теста. (Хорошо это или плохо, решайте сами.) Если
разработчики компилятора пошли на то, чтобы включить в него конкретное пре
дупреждение, значит, они хотели сообщить вам нечто очень важное, что, по их
мнению, может являться ошибкой. Когда ктонибудь просит нас помочь найти
ошибку, первое, что мы делаем, — проверяем, компилируется ли код без предуп
реждений. Если это не так, я говорю, что буду рад помочь, но только после того,
как будут устранены все предупреждения.
К счастью, Visual Studio .NET по умолчанию создает проекты с подходящим
уровнем диагностики, так что вам не понадобится задавать его вручную. Если вы
создаете проект C# вручную, присвойте ключу /WARN значение / WARN:4. В создава
емых вручную проектах Visual Basic .NET рассмотрение предупреждений как оши
бок по умолчанию отключено, так что включите его.

40

ЧАСТЬ I

Сущность отладки

Уровни диагностики заданы в Visual Studio .NET правильно, однако обращение
с предупреждениями как с ошибками по умолчанию отключено. Это неверно.
Чистая компиляция кода близка к благочестию, поэтому для компиляторов C# и
Visual Basic .NET нужно задать ключ /WARNASERROR+. Это не позволит даже начать
отладку, пока код не скомпилируется абсолютно чисто. Если вы работаете над
проектом C#, откройте окно Property Pages, выберите папку Configuration Properties,
страницу Build и задайте в поле Treat Warnings As Errors (считать предупрежде
ния ошибками), расположенном в столбце Errors And Warnings (ошибки и предуп
реждения), значение True (рис. 21). В случае проекта Visual Basic .NET нужно от
крыть окно Property Pages, папку Configuration Properties, выбрать страницу Build
и установить флажок Treat Compiler Warnings As Errors (рис. 22).
Если компилятор будет считать предупреждения ошибками, он окажет вам
огромную помощь (особенно при работе над проектами C#), прекращая сборку
программы при обнаружении таких проблем, как CS0649 [Field ‘field’ is never assigned
to, and will always have its default value ‘value’ (Полю ‘field’ никогда не присваива
ется значение, поэтому оно всегда будет иметь значение ‘value’, заданное по умол
чанию)], которая показывает, что у вас не инициализирован член класса. Однако
другие сообщения, такие как CS1573 [Parameter ‘parameter’ has no matching param
tag in XML comment (but other parameters do) (Параметр ‘parameter’ не имеет со
ответствующего ему тега в комментарии XML (хотя другие параметры имеют та
кие теги)], могут быть настолько надоедливыми, что вы захотите отключить об
ращение с предупреждениями как с ошибками. Не делайте этого!
Сообщение CS1573 выводится, когда вы задаете крайне полезный ключ /DOC для
создания XMLдокументации для вашей сборки и не комментируете какойто ис
пользованный параметр. (Я считаю большим преступлением то, что Visual Basic
.NET и C++ не поддерживают ключ /DOC и документацию XML.) Это самая настоя
щая ошибка, потому что, если вы создаете документацию XML, ктонибудь из ва
шей группы скорее всего будет читать ее, и если вы не опишете все параметры
или чтонибудь в этом роде, вы окажете своей группе очень плохую услугу.
Есть одно предупреждение, которое неверно считать ошибкой. Это предупреж
дение CS1596: [XML documentation not updated during this incremental rebuild; use
/incrementalto update XML documentation (Во время этой компиляции с прира
щением документация XML не была обновлена; для ее обновления используйте
ключ /incremental)] Документация XML чрезвычайно полезна, но отключение ком
пиляции с приращением очень замедляет создание программы. Отключить эту
ошибку невозможно, поэтому эту проблему можно решить, лишь отключив ком
пиляцию с приращением или для отладочных, или для заключительных компо
новок. Быстрая компиляция нравится всем, поэтому я отключаю компиляцию с
приращением и создаю документацию XML только для заключительных компо
новок. Так я обеспечиваю быстроту компиляции и при этом получаю документа
цию XML, когда она мне нужна.

ГЛАВА 2

Приступаем к отладке

41

При работе над неуправляемым кодом рассматривайте
предупреждения как ошибки (в большинстве случаев)
По сравнению с управляемым кодом неуправляемый код C++ не только позволя
ет вам при компиляции выстрелить себе в ногу2 , но и дает заряженный пистолет
со взведенным курком. В C++ предупреждение на самом деле означает, что ком
пилятор делает предположение по поводу намерений программиста. В качестве
прекрасного примера можно привести такое предупреждение, как C4244 [‘conver
sion’ conversion from ‘type1’ to ‘type2’, possible loss of data (Преобразование ‘con
version’ из ‘type1’ в ‘type2’ может привести к потере данных)], которое всегда воз
никает при преобразовании знакового типа в беззнаковый и наоборот. В данном
случае имеется только 50% шансов, что компилятор прочитает ваши мысли и
правильно решит, что ему нужно сделать со старшим битом.
Очень часто исправление подобной ошибки тривиально: достаточно, напри
мер, выполнить явное приведение типа переменной. Общая идея в том, чтобы
сделать код как можно менее неопределенным, чтобы компилятор не был вынужден
делать какиелибо предположения. Некоторые из предупреждений просто неза
менимы для прояснения кода. Таким является, например, предупреждение C4101
(‘identifier’: unreferenced local variable), сообщающее, что локальная переменная
нигде не используется. Исправление этого предупреждения облегчит проведение
обзоров кода и сделает программу гораздо понятнее для программистов, которые
будут ее сопровождать: никто не будет тратить время на выяснение того, для чего
же нужна эта дополнительная переменная и где она используется. Другие предуп
реждения, такие как C4700 [local variable ‘name’ used without having been initialized
(локальная переменная ‘name’ используется, не будучи инициализированной)], ука
зывают на точное место ошибки. Мне известны случаи, когда простое повыше
ние уровня диагностики и исправление появившихся предупреждений приводи
ло к исчезновению ошибок, на поиск которых могли бы уйти недели.
Проекты Visual C++, создаваемые при помощи мастеров, имеют по умолчанию
уровень диагностики 3, что соответствует в CL.EXE ключу /W3. Еще выше уровень
4, /W4, а ключ /WX позволяет даже сделать так, чтобы все предупреждения рассмат
ривались компилятором как ошибки. Для задания уровня диагностики откройте
окно Property Pages, папку C/C++ и выберите страницу свойств General. В поле
Warning Level (уровень диагностики) укажите значение Level 4 (/W4). Двумя стро
ками ниже находится поле Treat Warnings As Errors, в котором следует задать зна
чение Yes (/WX). Правильные значения обоих полей см. на рис. 23.
Я с радостью заявил бы, что компиляцию всегда следует выполнять на уровне
диагностики 4 и все предупреждения нужно считать ошибками, однако реальность
не позволяет мне сделать это. Входящая в состав Visual C++ библиотека стандар
тных шаблонов (Standard Template Library, STL) имеет много недоработок, не по
зволяющих работать с ней на уровне диагностики 4. Компилятор также имеет
несколько проблем с шаблонами. К счастью, эти проблемы поддаются решению.

2

Поанглийски: «shoot yourself in the foot». Вероятно, автор обыгрывает название изве
стной книги: Allen I. Holub. Enough Rope to Shoot Yourself in the Foot: Rules for C and
C++ Programming. — McGrawHill, 1995. — Прим. перев.

42

ЧАСТЬ I

Сущность отладки

Вы можете подумать, что достаточно задать уровень диагностики 4 и не счи
тать предупреждения ошибками, но такой подход дискредитирует саму суть опи
санной идеи. Я обнаружил, что разработчики очень быстро перестают обращать
внимание на предупреждения в окне Build. Если не исправлять все предупрежде
ния, какими бы безобидными они ни казались, по мере их возникновения, более
важные предупреждения начинают теряться в потоке вывода среди других сооб
щений. Хитрость в том, чтобы более явно указывать, какие предупреждения вы
желаете исправлять. Конечно, вы должны избавляться от большинства предупреж
дений путем улучшения кода программы, однако можно также отключить специ
фические ошибки, используя директиву #pragma warning. Кроме того, она позволяет
управлять уровнем диагностики ошибок в конкретных заголовочных файлах.
Хорошим примером уместного понижения уровня диагностики может служить
включение заголовочных файлов, которые не компилируются на уровне 4. Пони
зить уровень диагностики можно через расширенную директиву #pragma warning,
появившуюся в Visual C++ 6. В следующем фрагменте я понижаю уровень диагно
стики для включения подозрительного заголовочного файла и сразу же возвра
щаю ему прежнее значение, чтобы мой код компилировался на уровне 4:

#pragma warning ( push , 3 )
#include "IDoNotCompileAtWarning4.h"
#pragma warning ( pop )
Директива #pragma warning позволяет также запретить отдельные предупрежде
ния. Она полезна, например, когда вы применяете безымянную структуру или
объединение и получаете на уровне диагностики 4 ошибку C4201, «nonstandard
extension used: nameless struct/union» (использовано нестандартное расширение:
структура/объединение не имеет имени). Вот как при помощи директивы #pragma
warning запретить это предупреждение (заметьте: я закомментировал свои действия
и объяснил их). При запрещении отдельных предупреждений ограничивайте
диапазон действия #pragma warning специфическими разделами программы. Поместив
директиву на слишком высоком уровне, вы можете замаскировать другие ошиб
ки своей программы.

// Я запрещаю предупреждение "nonstandard extension used: nameless struct/union",
// потому что мне не нужен машинонезависимый код
#pragma warning ( disable : 4201 )
struct S
{
float y;
struct
{
int a ;
int b ;
int c ;
} ;
} *p_s ;
// Снова разрешаю предупреждение.
#pragma warning ( default : 4201 )

ГЛАВА 2

Приступаем к отладке

43

Существует одно предупреждение, C4100 [«‘identifier’: unreferenced formal parame
ter» (‘identifier’: неиспользуемый формальный параметр)], исправление которого
иногда вызывает недоумение. Если у вас есть параметр, который не применяется,
его, пожалуй, следует удалить из определения метода. Однако при написании
программы на объектноориентированном языке программирования можно вы
полнить наследование от метода, которому, как потом оказывается, параметр не
нужен, но изменять базовый класс нельзя. Вот правильный способ обработки
ошибки C4100:

// Этот код сгенерирует ошибку C4100:
int ProblemMethod ( int i , int j )
{
return ( 5 ) ;
}
// Правильный способ избежания ошибки C4100:
int GoodMethod ( int /* i */ , int /* j */ )
{
return ( 22 ) ;
}

Стандартный вопрос отладки
STL, поставляемая с Visual Studio .NET, сложна для понимания
и отладки. Что-нибудь может мне помочь?
Я понимаю, что STL из состава Visual Studio .NET писали гораздо более ум
ные люди, чем я, но даже в этом случае ее почти невозможно понять. С одной
стороны, концепция STL хороша: эта библиотека широко используется и
имеет согласованный интерфейс. С другой стороны, природа STL, постав
ляемой с Visual Studio .NET, и шаблонов вообще такова, что при возникно
вении проблемы вам придется приложить гораздо больше усилий для ее
понимания, чем для отладки на уровне ассемблера.
Вместо STL из Visual Studio .NET я рекомендую свободно распространя
емую STL от компании STLport (www.stlport.org). Библиотека STLport не
только бесконечно понятней, но и включает гораздо лучшие средства под
держки многопоточности и отладки. Учитывая эти преимущества и то, что
она не налагает никаких ограничений на коммерческое использование, я
настоятельно рекомендую использовать именно ее, а не STL из Visual Studio
.NET, если, конечно, вам вообще нужна STL.
Если вы не используете STL, этот способ работает прекрасно. Однако при ра
боте с STL он эффективен не всегда. Применяя STL, лучше всего включать в пре
компилированные заголовочные файлы только заголовочные файлы STL. Это
значительно облегчает изоляцию директив #pragma warning ( push , 3 ) и #pragma warning
( pop ) в заголовочных файлах. Другое важное преимущество заключается в суще
ственном ускорении компиляции. Прекомпилированный заголовочный файл
представляет по сути дерево синтаксического анализа, благодаря чему позволяет
сэкономить много времени, так как STL — очень объемная библиотека. Наконец,
чтобы получить полный контроль над утечками и искажениями памяти при ис

44

ЧАСТЬ I

Сущность отладки

пользовании стандартной библиотеки C, нужно держать заголовочные файлы STL
в одном месте. О стандартной библиотеке C для отладки см. главу 17.
Основной смысл сказанного в том, что с самого начала проекта нужно выпол
нять компиляцию на уровне диагностики 4 и рассматривать все предупреждения
как ошибки. Когда вы впервые повысите уровень диагностики проекта, вы скорее
всего будете удивлены числом появившихся предупреждений. Изучите их и ис
правьте. Возможно, это приведет и к исчезновению нескольких ошибок. Если вы
думаете, что заставить программу компилироваться с ключами /W4 и /WX нельзя, я
могу доказать обратное: весь неуправляемый код примеров с прилагаемого к этой
книге CD компилируется с обоими флагами, заданными для всех конфигураций.

Разрабатывая неуправляемый код,
знайте адреса загрузки DLL
Если вы когданибудь гуляли по лесу, то знаете: чтобы не заблудиться, очень важ
но запоминать всякие примечательные объекты. Не имея ориентиров, можно
просто ходить по кругу. При аварийном завершении приложения нужно иметь
аналогичный ориентир, который поможет найти правильный путь и не блуждать
впустую по коду своей программы в окне отладчика.
Первым важным ориентиром при крахе программы являются базовые адреса
DLL или элементов управления на базе ActiveX (OCX), указывающие на область
их размещения в памяти. Когда клиент предоставит вам адрес аварийного завер
шения программы, вам нужно быстро определить, к какой DLL он относится, по
первым двум или трем цифрам. Я не утверждаю, что вы должны знать адреса всех
системных DLL, но нужно помнить хотя бы базовые адреса DLL, используемых в
вашем проекте.
Если все ваши DLL будут загружаться по уникальным адресам, вы будете иметь
отличные ориентиры, которые помогут искать причину проблемы. Ну, а если все
ваши DLL будут иметь одинаковые адреса загрузки? Очевидно, ОС не сможет ото
бразить их на одну и ту же область памяти. При загрузке DLL, желающей распо
ложиться в уже занятой области памяти, ОС должна будет «переадресовать» DLL,
выделив ей другое место. И как же определить, где какая DLL загружена? Увы, мы
не можем узнать, как поступит ОС на разных компьютерах. А значит, при получе
нии адреса аварийного завершения программы вы не будете иметь представле
ния о том, откуда этот адрес взялся. В свою очередь это означает, что ваш началь
ник будет очень недоволен, так как вы не сможете объяснить ему причину сбоя
приложения.
Для проектов, созданных при помощи мастеров, по умолчанию справедливо
следующее: библиотеки DLL элементов ActiveX, созданных в среде Visual Basic 6,
загружаются по адресу 0x11000000, а DLL, написанные на Visual C++, — по адресу
0x10000000. Готов спорить, что по меньшей мере половина имеющихся на дан
ный момент в мире DLL пытается загрузиться по одному из этих адресов. Изме
нение адреса загрузки DLL называется модификацией базового адреса (или пе
реадресацией) и является простой операцией, позволяющей задать другой адрес
загрузки, отличный от используемого по умолчанию.
Прежде чем приступить к обсуждению модификации базового адреса, рассмот
рим два простых способа, позволяющих определить наличие конфликтов при

ГЛАВА 2

Приступаем к отладке

45

загрузке DLL. Первый подразумевает использование окна Modules (модули) отлад
чика Visual Studio .NET. Запустите приложение в среде Visual Studio .NET и откройте
окно Modules, для чего нужно выбрать меню Debug, подменю Windows или нажать
CTRL+ALT+U, если комбинации клавиш настроены по умолчанию. Если базовый
адрес модуля был модифицирован, его значок будет отмечен красным кружком с
восклицательным знаком. Кроме того, диапазон занимаемых модулем адресов будет
отмечен звездочкой. На рис. 27 показано окно Modules с переадресованной биб
лиотекой SYMSRV.DLL во время сеанса отладки.

Рис. 27.

Переадресованная DLL в окне Modules отладчика Visual Studio .NET

Второй способ — загрузить бесплатное приложение Process Explorer, написанное
моим хорошим другом и когдато соседом Марком Руссиновичем (Mark Russinovich)
из Sysinternals (www.sysinternals.com). Как следует из названия, Process Explorer
позволяет узнать разнообразную информацию о процессах, например, загружен
ные DLL и открытые описатели (handle). Это настолько полезный инструмент, что
если у вас его еще нет, немедленно прекратите чтение и загрузите его! Кроме того,
вам следует прочитать главу 14, где описаны дополнительные приемы и хитрос
ти, которые могут облегчить отладку при помощи Process Explorer.
Узнать, была ли переадресована DLL, очень легко. Просто выполните описан
ные ниже действия. На рис. 28 показано, как выглядит окно Process Explorer, если
DLL процесса была переадресована.
1. Запустите Process Explorer и свой процесс.
2. Выберите в меню View пункт View DLLs.
3. Выберите в меню Options пункт Highlight Relocated DLLs (Выделить переадре
сованные DLL).
4. Выберите свой процесс в верхней половине основного окна.
Все переадресованные DLL будут выделены желтым цветом.

Рис. 28.

Переадресованные DLL в окне программы Process Explorer

46

ЧАСТЬ I

Сущность отладки

Еще один отличный инструмент, показывающий переадресованные DLL не
только с модифицированным, но и с исходным адресом, — программа ProcessSpy
из прекрасной статьи Кристофа Назарра (Christophe Nasarre) «Escape from DLL Hell
with Custom Debugging and Instrumentation Tools and Utilities, Part 2» (Избавление
от ада DLL при помощи собственных отладочных инструментов и утилит, часть
2), опубликованной в журнале MSDN Magazine в августе 2002 года. По функцио
нальности программы Process Explorer и ProcessSpy похожи, однако ProcessSpy
поставляется с исходным кодом, так что вы можете узнать, как она колдует.
Переадресация DLL ОС не только затрудняет поиск причин краха приложения,
но и замедляет его выполнение. При переадресации ОС должна прочитать инфор
мацию о модификации адресов, проработать все участки программы, получаю
щие доступ к DLL, и изменить их, потому что DLL будет размещена в памяти не
на своем излюбленном месте. Если в приложении будет два конфликта адресов
загрузки, время его запуска может увеличиться аж вдвое!
Есть и еще одна крупная проблема: переадресовав модуль, ОС не сможет выг
рузить его из памяти полностью, если ей понадобится выделить место для друго
го кода. Если модуль загружается по предпочитаемому адресу загрузки, ОС может
выгрузить его на диск, а затем загрузить обратно. Однако, если базовый адрес
модуля был модифицирован, значит, была изменена и область памяти, содержа
щая код этого модуля. Поэтому ОС должна гдето хранить эту память (возможно,
в страничном файле), даже если модуль выгружен из памяти. Легко догадаться, что
это может «съедать» большие блоки памяти и замедлять работу компьютера изза
затрат на их перемещение.
Базовый адрес DLL можно модифицировать двумя способами. Первый — с
помощью утилиты REBASE.EXE из состава Visual Studio .NET. REBASE.EXE имеет массу
опций, но лучше всего вызывать ее из командной строки с ключом /b, указывая
после него стартовый базовый адрес и названия DLL. Хочу вас обрадовать: как
только вы модифицируете базовый адрес какойлибо DLL, вам почти никогда
больше не придется возвращаться к ней. Модифицируйте базовые адреса DLL только
до ее регистрации. Если вы модифицируете базовый адрес DLL после ее регист
рации, она не загрузится.
В табл. 21 приведен фрагмент документации Visual Studio .NET, посвященный
модификации базовых адресов DLL. Как видите, рекомендуется использовать ал
фавитную схему. Я обычно следую ей, потому что она проста. DLL ОС загружают
ся по адресам от 0x70000000 до 0x78000000, так что следование правилам табл.
21 избавит вас от конфликтов с ОС. Конечно, вам всегда следует изучать адрес
ное пространство своих приложений при помощи Process Explorer или ProcessSpy,
чтобы узнать, не загружена ли уже какаянибудь DLL по тому адресу, который вы
хотите использовать.
Если в приложение включены четыре DLL — APPLE.DLL, DUMPLING.DLL, GIN
GER.DLL и GOOSEBERRIES.DLL, для правильной модификации их адресов нужно
выполнить REBASE.EXE трижды. Это проиллюстрировано следующими тремя ко
мандами:

REBASE /b 0x60000000 APPLE.DLL
REBASE /b 0x61000000 DUMPLING.DLL
REBASE /b 0x62000000 GINGER.DLL GOOSEBERRIES.DLL

ГЛАВА 2

Приступаем к отладке

47

Если в командной строке указать несколько DLL, как я только что поступил с биб
лиотеками GINGER.DLL и GOOSEBERRIES.DLL, утилита REBASE.EXE модифициру
ет их базовые адреса так, чтобы они загружались друг за другом, начиная с ука
занного адреса.

Табл. 2-1. Схема модификации базовых адресов DLL
Первая буква названия DLL

Базовый адрес

A–C

0x60000000

D–F

0x61000000

G–I

0x62000000

J–L

0x63000000

M–O

0x64000000

P–R

0x65000000

S–U

0x66000000

V–X

0x67000000

Y–Z

0x68000000

Другой метод модификации базового адреса DLL — указать адрес загрузки при
компоновке DLL. В Visual C++ это можно сделать, открыв окно Property Pages, папку
Linker и выбрав страницу свойств Advanced (расширенные настройки). Шестнад
цатеричный адрес загрузки DLL следует указать в поле Base Address (базовый адрес).
Этот адрес будет передан компоновщику LINK.EXE вместе с ключом /BASE (рис. 29).
Утилиту REBASE.EXE позволяет автоматически задавать адреса загрузки несколь
ких DLL одновременно без ограничений, но при задании адресов во время ком
поновки следует быть внимательнее. Если вы укажете адреса загрузки нескольких
DLL слишком близко, то в отладочном окне Module увидите, что их адреса будут
модифицированы. Поэтому, чтобы никогда впоследствии не волноваться об ад
ресах загрузки, их нужно задавать с достаточным интервалом.
В примере с REBASE.EXE я задал бы адреса загрузки этих DLL так:

APPLE.DLL
DUMPLING.DLL
GINGER.DLL
GOOSEBERRIES.DLL

0x60000000
0x61000000
0x62000000
0x62100000

Обратите особое внимание на библиотеки GINGER.DLL и GOOSEBERRIES.DLL,
потому что их названия начинаются с одинаковой буквы. В таких случаях я за
даю другой адрес загрузки при помощи третьей по старшинству цифры. Если бы
я собрался использовать еще одну DLL, название которой также начиналось бы с
буквы «G», я бы указал адрес загрузки 0x62200000.
Ознакомиться с проектом, в котором адреса загрузки заданы вручную, можно
на примере проекта WDBG из главы 4. Я забыл сказать, что ключ /BASE позволяет
указать текстовый файл, содержащий адреса загрузки всех DLL приложения. В
проекте WDBG я применил именно такой способ.
Для переадресации DLL и OCX можно использовать оба метода: модифициро
вать базовые адреса DLL при помощи утилиты REBASE.EXE или вручную, однако,
пожалуй, лучше всего следовать второму методу и выполнять переадресацию DLL
вручную. Все примеры DLL на CD, прилагаемом к этой книге, я переадресовал

48

ЧАСТЬ I

Сущность отладки

вручную. Основное преимущество такого метода в том, что MAPфайл будет со
держать специфический заданный адрес. MAPфайл — это текстовый файл, ука
зывающий, по каким адресам компоновщик размещает все символы и строки
программы. При заключительных компоновках MAPфайлы следует создавать все
гда, так как они являются единственными простыми текстовыми описаниями
символов. MAPфайлы окажутся особенно полезными в будущем, когда вам нуж
но будет найти причину краха программы, а ваш отладчик не сможет работать со
старыми символами. Если же переадресацию DLL выполнять посредством RE
BASE.EXE, создаваемый компоновщиком MAPфайл будет содержать первоначаль
ный базовый адрес, и для его преобразования в модифицированный адрес пона
добятся некоторые вычисления (о MAPфайлах см. главу 12).

Рис. 29.

Задание базового адреса DLL

Меня часто спрашивают: «Базовые адреса каких файлов модифицировать?»
Следуйте простому правилу: если код написан вами или кемнибудь из вашей груп
пы, модифицируйте его базовый адрес. В противном случае не трогайте его. Если
вы используете компоненты сторонних фирм, вам придется располагать свои
двоичные файлы в памяти, учитывая уже занятые этими компонентами области.

Как поступать с базовыми адресами управляемых модулей?
В данный момент вы, возможно, думаете, что, раз уж управляемые компоненты
компилируются в DLL, их базовые адреса также следует модифицировать. Более
того, если вы изучали ключи компиляторов C# и Visual Basic .NET, то, может быть,
видели ключ /BASEADDRESS для задания базового адреса. Однако в случае управляе
мого кода все немного не так. Если вы изучите управляемую DLL с помощью про
граммы DUMPBIN.EXE из состава Visual Studio .NET, служащей для просмотра дампов
файлов Portable Executable (PE), или при помощи великолепного инструмента
PEDUMP, созданного Мэттом Питреком (Matt Pietrek) (MSDN Magazine, февраль
2002), вы заметите одну импортируемую функцию _CorDllMain из библиотеки
MSCOREE.DLL и одно значение в таблице переадресации.
Думая, что в управляемых DLL может находиться какойто исполняемый код, я
дизассемблировал несколько DLL, однако в разделе кода модуля все выглядело, как
данные. Я еще немного почесал голову и заметил коечто очень интересное. Точ

ГЛАВА 2

Приступаем к отладке

49

ка входа модуля, т. е. точка, с которой начинается его выполнение, оказалась рас
положенной по тому же адресу, что и импортируемая функция _CorDllMain. Это
подтвердило, что в модуле нет неуправляемого исполняемого кода.
В конечном счете модификация базовых адресов управляемых модулей не
принесет вам такого же огромного преимущества, как в случае неуправляемого
кода. Тем не менее я выполняю ее, так как мне кажется, что загрузчик ОС всетаки
не остается в стороне, вследствие чего переадресация управляемой DLL при заг
рузке будет замедлять запуск программы. Если вы решаете модифицировать ба
зовые адреса управляемых DLL, это нужно делать во время компоновки. Если мо
дифицировать адрес зарегистрированной управляемой DLL при помощи
REBASE.EXE, система безопасности заметит, что DLL была изменена, и откажется
загружать ее.

Стандартный вопрос отладки
Какие дополнительные параметры компилятора C# помогут мне
заранее позаботиться об отладке управляемого кода?
Хотя управляемый код устраняет многие ошибки, отравлявшие нашу жизнь
при работе с неуправляемым кодом, некоторые ошибки все же могут ска
заться на работе вашей программы. К счастью, есть очень полезные ключи
командной строки, задав которые можно облегчить обнаружение таких
ошибок. Хорошая новость для любителей Visual Basic .NET: эта среда абсо
лютно правильно настроена по умолчанию, поэтому вам не понадобится
задавать дополнительных ключей компилятора. Если вы не желаете настра
ивать компилятор вручную, модуль надстройки SettingsMaster из главы 9
сделает это за вас.

/checked+

(проверка целочисленной арифметики)

В областях потенциальных проблем можно использовать ключевое слово
checked, но это нужно делать при написании кода. Ключ командной строки
/checked+ позволяет включить проверку целочисленного переполнения для
всей программы. Если результат окажется вне диапазона допустимых зна
чений типа данных, программа автоматически сгенерирует исключение
периода выполнения. Задание этого ключа приводит к небольшому увели
чению объема кода, поэтому я предпочитаю оставлять его включенным в
отладочных компоновках и использовать ключевое слово checked для явной
проверки подобных ошибок в заключительных компоновках. Для установ
ки этого ключа нужно открыть окно Property Pages, папку Configuration
Properties, выбрать страницу Build и задать в поле Check For Arithmetic
Overflow/ Underflow (Проверка арифметического переполнения) значение
True.

/noconfig

(игнорировать файл CSC.RSP)

Интересно, но задать этот ключ в среде Visual Studio .NET невозможно. Тем
не менее, если вы захотите собирать программу из командной строки, знать
о его предназначении не помешает. По умолчанию, прежде чем обрабаты
см. след. стр.

50

ЧАСТЬ I

Сущность отладки

вать командную строку, компилятор C# читает файл CSC.RSP, в котором также
указаны ключи командной строки. Чтобы автоматизировать свою работу,
вы можете задать в нем любые допустимые ключи. Стандартный файл CSC.RSP
из состава Visual Studio .NET содержит огромное число ключей /REFERENCE
для распространенных сборок, которые все мы постоянно используем. А вот
для таких библиотек, как System.XML.dll, этот ключ не нужен, так как файл
CSC.RSP содержит запись /r: System.XML.dll. Файл CSC.RSP находится в ката
логе версии .NET Framework: \Micro
soft.NET\Framework\.

Стандартный вопрос отладки
Какие дополнительные параметры компилятора и компоновщика
помогут позаботиться об отладке неуправляемого кода?
Существует много ключей, способных помочь повысить производительность
приложения и облегчить его отладку. Кроме того, как я уже говорил, я не
совсем согласен со значениями параметров компилятора и компоновщика
Visual C++ по умолчанию в проектах, создаваемых при помощи мастеров.
Поэтому я всегда изменяю некоторые их параметры. Если вы не желаете
делать это вручную, используйте модуль надстройки SettingsMaster из гла
вы 9.

Ключи компилятора CL.EXE
Задать эти ключи вручную можно, открыв окно Property Pages, папку C/C++,
страницу Command Line (командная строка) и введя их в поле Additional
Options (дополнительные ключи), однако гораздо лучше указывать их в
соответствующих им местах. Задание ключей командной строки в поле
Additional Options может привести к проблемам, потому что разработчики
не привыкли искать их в этом месте.

/EP /P

(препроцессорная обработка с выводом в файл)

В случае проблем с макрокомандами могут пригодиться ключи /EP и /P. Они
приказывают препроцессору обработать исходный файл, преобразовав все
макрокоманды в обычную форму и включив все указанные файлы, и сохра
нить результат в файле с тем же именем, но с расширением .I. Открыв этот
файл, вы сможете узнать, во что преобразуются ваши макрокоманды. Убе
дитесь, что у вас хватает места на диске, потому что файлы .I могут зани
мать по несколько мегабайт. Чтобы препроцессор сохранил в файле ком
ментарии, нужно также указать ключ /C (не удалять комментарии).
Для задания ключей /EP и /P откройте окно Property Pages, папку C/C++,
выберите страницу Preprocessor (препроцессор) и укажите в поле Generate
Preprocessed File (генерировать файл, прошедший препроцессорную обра
ботку) значение Without Line Numbers (/EP /P) (без номеров строк). Поле
Keep Comments (сохранять комментарии), расположенное на той же стра
нице, позволяет задать компилятору ключ /C. Помните, что эти ключи не

ГЛАВА 2

Приступаем к отладке

51

вызывают компиляцию файла .I, поэтому при компоновке программы вы
столкнетесь с ошибками. Определив проблему, отключайте их. Знаю на
собственном опыте: регистрация проекта в системе управления версиями
с заданными ключами /EP и /P не понравится ни вашим товарищам по группе,
ни руководителю.

/X

(игнорировать стандартный путь включения файлов)

Создание правильной компоновки может оказаться проблематичным, если
на компьютере установлены несколько компиляторов и пакетов для разра$
ботки ПО (SDK). Если не задан ключ /X, компилятор, вызываемый MAK$
файлом, вызовет переменную среды INCLUDE. Ключ /X позволяет контроли$
ровать включение заголовочных файлов: он заставляет компилятор игно$
рировать переменную INCLUDE и искать заголовочные файлы только в мес$
тах, указанных явно посредством ключа /I. Задать ключ /X можно, открыв
окно Property Pages, папку C/C++, страницу Preprocessor и выбрав соответ$
ствующее значение в поле Ignore Standard Include Path (игнорировать стан$
дартный путь включения файлов).

/Zp

(выравнивание членов структур)

Этот флаг использовать не следует. Выравнивание членов структур в памя$
ти надо задавать не в командной строке, а в директиве #pragma pack в специ$
фических заголовочных файлах. Невыполнение этого условия порой при$
водит к очень трудноуловимым ошибкам. Начиная проект, разработчики
задавали ключ /Zp. Когда они переходили к другой компоновке или если
работу над кодом продолжала другая группа, про ключ /Zp забывали, и струк$
туры начинали немного отличаться, так как по умолчанию применялся иной
метод выравнивания. На поиск причины тех ошибок пришлось потратить
кучу времени. Для установки этого ключа нужно открыть окно Property Pages,
папку C/C++, выбрать страницу Code Generation (генерирование кода) и
задать нужное значение свойства Struct Member Alignment (выравнивание
членов структур).
Используя директиву #pragma pack, не забывайте про ее новый вариант
#pragma pack (show), выводящий при компиляции значение выравнивания в
окно Build. Это поможет вам следить за текущим выравниванием в различ$
ных разделах кода.

/Wp64

(определять проблемы совместимости с 64-разрядными
платформами)

Этот ключ позволяет сэкономить много времени при работе над совмес$
тимостью кода с 64$разрядными системами. Установить его можно, открыв
окно Property Pages, папку C/C++, выбрав страницу General и задав в поле
Detect 64$bit Portability Issues (определять проблемы совместимости с 64$
разрядными платформами) значение Yes (/Wp64). Лучше всего /Wp64 при$
менять с самого начала проекта. Если вы зададите этот ключ, уже проделав
значительную работу над программой, то вас поразит количество обнару$
женных проблем, так как он предъявляет очень высокие требования. Кро$
см. след. стр.

52

ЧАСТЬ I

Сущность отладки

ме того, некоторые поставляемые Microsoft макрокоманды, которые, как
предполагалось, помогут решить вопросы совместимости с платформами
Win64, например SetWindowLongPtr, при компиляции с ключом /Wp64 приво$
дят к выводу сообщений об ошибке.

/RTC

(проверка ошибок в период выполнения)

Самые полезные ключи, известные сообществу программистов на C++! Всего
их три: /RTCc обеспечивает проверку потери данных при их преобразова$
нии в меньший тип, /RTCu помогает предотвращать использование неини$
циализированных переменных, /RTCs проверяет кадры стека путем иници$
ализации всех локальных переменных известным значением (0xCC), предот$
вращает применение недопустимых индексов локальных переменных и
проверяет правильность указателей стека для предотвращения искажения
данных. Для установки этих ключей откройте окно Property Pages, папку C/
C++, страницу Code Generation и выберите соответствующие значения в
полях Smaller Type Check (проверка при преобразовании к меньшему типу)
и Basic Runtime Checks (базовые виды проверки периода выполнения). Эти
ключи настолько важны, что в главе 17 мы обсудим их особо.

/GS

(проверка безопасности буферов)

Один из наиболее распространенных приемов в арсенале создателей ви$
русов — переполнение буфера, при котором адрес возврата перезаписыва$
ется так, чтобы управление получал код злоумышленника. К счастью, ключ
/GS позволяет включить в программу специальные фрагменты, гарантиру$
ющие, что адрес возврата не был перезаписан. Это значительно затрудняет
создание вирусов такого типа. Ключ /GS задан по умолчанию для заключи$
тельных компоновок, и я также советую использовать его в отладочных
компоновках. Если когда$нибудь этот ключ сообщит, что кто$то перезапи$
сал только адрес возврата, вы увидите, как много недель ужасно сложной
отладки это вам сэкономит. Установите ключ /GS, открыв окно Property Pages,
папку C/C++, страницу Code Generation и задав в поле Buffer Security Check
(проверка безопасности буферов) значение Yes (/GS). В главе 17 я объяс$
ню, как изменять принятые по умолчанию сообщения об ошибках, обна$
руженных ключом /GS.

/O1

(минимизировать размер кода)

В проектах C++, создаваемых мастерами, для заключительных компоновок
по умолчанию применяется ключ /O2 (максимизировать скорость). Однако
Microsoft создает все свои коммерческие приложения с ключом /O1, и вам
также следует делать это. Задать этот ключ можно, открыв окно Property Pages,
папку C/C++, страницу Optimization и выбрав соответствующие значение
свойства Optimization. Программисты Microsoft обнаружили, что после на$
хождения наилучшего алгоритма и написания компактного кода скорость
выполнения приложения можно значительно повысить, уменьшив число
ошибок страниц памяти. Как я слышал, они говорят: «Ошибки страниц могут
испортить вам весь день!»

ГЛАВА 2

Приступаем к отладке

53

Страница представляет собой наименьший блок кода или данных (4 кб
для компьютеров с архитектурой x86), с которым диспетчер памяти может
работать как с единым целым. Ошибка страницы происходит при обраще$
нии к недействительной странице памяти. Это может быть обусловлено
самыми разными причинами: например, попыткой получения доступа к
странице из списка резервных или измененных страниц или к странице,
которая больше не находится в памяти. Для исправления ошибки страни$
цы ОС должна прекратить выполнение программы и загрузить в регистры
процессора новый адрес страницы. Если ошибка страницы «мягкая» (т. е.
страница уже находится в памяти), накладные расходы не очень велики, тем
не менее они все равно лишние. Однако если ошибка «жесткая», ОС вынуж$
дена загрузить в память нужную страницу с диска. Разумеется, это требует
выполнения сотен тысяч команд, замедляя работу приложения. Минимиза$
ция объема двоичного файла позволяет уменьшить общее число использу$
емых приложением страниц, а значит, и снизить вероятность ошибок стра$
ницы. Пусть загрузчик и диспетчер управления кэш$памятью ОС очень хо$
роши, но зачем допускать больше ошибок страниц, если есть возможность
уменьшить их число?
Кроме задания ключа /O1, рекомендую подумать об утилите Smooth Wor$
king Set (SWS) из главы 19, которая помогает вынести наиболее часто вы$
зываемые функции в начало двоичного файла, минимизировав таким об$
разом рабочий набор, т. е. число страниц, находящихся в оперативной па$
мяти. Если часто используемые функции расположены в начале файла, ОС
сможет выгрузить ненужные страницы на диск. Это позволит ускорить
выполнение приложения.

/GL

(оптимизация всей программы)

Программисты Microsoft много сделали для улучшения генераторов кода,
благодаря чему компактность и скорость выполнения программ, создавае$
мых в среде Visual C++ .NET, заметно улучшились. Одно из крупных изме$
нений состоит в том, что вместо оптимизации отдельных файлов (извест$
ных также как компилянды) при компиляции теперь можно выполнять кросс$
файловую оптимизацию программы при ее компоновке. Я уверен, что все
программисты, впервые компилирующие проект C++ в среде Visual C++ .NET,
замечают серьезное уменьшение объема программы. Удивительно, но для
заключительных компоновок Visual C++ этот ключ по умолчанию не исполь$
зуется. Установите его: откройте окно Property Pages, папку Configuration
Properties, страницу General и задайте в поле Whole Program Optimizations
(оптимизация всей программы) значение Yes. Это одновременно установит
и соответствующий ключ компоновщика, /LTCG.

/showIncludes

(выводить список включаемых файлов)

О назначении этого ключа говорит само название. При компиляции файла
он составляет иерархический список всех включаемых файлов, позволяю$
щий узнать, что, куда и откуда включается. Задайте этот ключ, открыв окно
см. след. стр.

54

ЧАСТЬ I

Сущность отладки

Property Pages, папку C/C++, страницу Advanced и указав в поле Show Includes
(показывать включаемые файлы) значение Yes (/showIncludes).

Ключи для компоновщика LINK.EXE
Задать эти ключи вручную можно, открыв окно Property Pages, папку Linker,
страницу Command Line и введя их в текстовом поле Additional Options,
однако гораздо лучше указывать их в соответствующих им местах. Как я уже
писал в разделе, посвященном ключам компилятора, программисты не при$
выкли искать ключи командной строки в текстовом поле Additional Options,
так что это может привести к проблемам.

/MAP
/MAPINFO:LINES
/MAPINFO:EXPORTS

(генерировать MAP-файл)
(включать в MAP-файл номера строк)
(включать в MAP-файл информацию об экспортируемых
функциях)

Эти ключи обеспечивают создание MAP$файла для компонуемого образа
программы (о MAP$файлах см. главу 12). Я советую всегда создавать MAP$
файл, так как это единственный способ получения информации о симво$
лах в текстовом виде. Используйте все три ключа, чтобы MAP$файл содер$
жал наиболее полную информацию. Задать их можно, открыв окно Property
Pages, папку Linker и выбрав нужные значения на странице Debugging.

/NODEFAULTLIB

(игнорировать библиотеки)

Многие системные заголовочные файлы включают директивы #pragma comment
( lib#, XXX ), определяющие, с какой библиотекой компоновать файл, где
XXX — название библиотеки. Ключ /NODEFAULTLIB указывает компоновщику
игнорировать эти директивы. Данный ключ позволяет программисту самому
выбирать компонуемые библиотеки и порядок компоновки. Вам придется
указывать все нужные библиотеки в командной строке компоновщика, но
вы хотя бы будете точно знать, какие библиотеки вы используете и в каком
порядке. Управление порядком компоновки может оказаться очень важным,
когда один символ встречается в нескольких библиотеках, что может при$
водить к трудноуловимым ошибкам. Задать этот ключ можно, открыв окно
Property Pages, папку Linker, страницу Input (ввод) и указав в поле Ignore All
Default Libraries (игнорировать все библиотеки, используемые по умолча$
нию) значение Yes.

/OPT:NOWIN98
Если от вашей программы не требуется поддержка ОС Windows 9x/Me, этот
ключ позволит немного уменьшить размер исполняемых файлов, сняв ог$
раничение, требующее, чтобы их разделы выравнивались по границе 4 кб.
Для установки этого ключа нужно открыть окно Property Pages, папку Linker,
страницу Optimization и задать нужное значение в поле Optimize For Win$
dows98 (оптимизировать программу для ОС Windows98).

ГЛАВА 2

/ORDER

Приступаем к отладке

55

(располагать функции в определенном порядке)

Если вы собираетесь применять утилиту Smooth Working Set (см. главу 19),
ключ /ORDER позволит указать файл, описывающий порядок расположения
функций. Он отключает компоновку с приращением, поэтому задавайте его
только для завершающих компоновок. Этот ключ задается так: откройте в
окне Property Pages папку Linker, страницу Optimization и введите значение
в поле Function Order (порядок функций).

/VERBOSE
/VERBOSE:LIB

(выводить сообщения о прогрессе компоновки)
(выводить только сообщения, касающиеся поиска
библиотек)

В случае проблем с компоновкой эти сообщения смогут показать вам, ка$
кие символы ищет компоновщик и где он их находит. Информация может
оказаться очень объемной, но, возможно, она поможет вам найти причину
проблемы. Однажды эти два ключа помогли мне при отладке очень стран$
ной ошибки, когда на уровне ассемблера вызываемая функция выглядела
совсем не так, как я предполагал. Оказалось, что в двух разных библиоте$
ках имелись две различных функции с одинаковыми сигнатурами, и ком$
поновщик использовал неправильный вариант. Задать эти ключи можно,
открыв окно Property Pages, папку Linker, страницу General, в поле Show
Progress (показывать информацию о прогрессе компоновки).

/LTCG

(генерация кода во время компоновки)

Используется вместе с ключом компилятора /GL для выполнения перекрес$
тной оптимизации компиляндов. Он устанавливается автоматически при
задании ключа /GL.

/RELEASE

(задание контрольной суммы)

Если ключ /DEBUG указывает компоновщику генерировать отладочный код,
то неверно названный ключ /RELEASE не делает, как можно было бы пред$
положить, противоположное и не приказывает компоновщику создать оп$
тимизированную заключительную компоновку. Вообще$то этот ключ сле$
довало бы назвать /CHECKSUM. Он всего лишь вносит значения контрольной
суммы в заголовок файла Portable Executable (PE). Это необходимо для заг$
рузки драйверов устройств, но не нужно приложениям, работающим в поль$
зовательском режиме. Однако установка этого ключа для завершающих ком$
поновок будет совсем не лишней, так как отладчик WinDBG (см. главу 8)
всегда выводит соответствующее сообщение, если двоичный файл не содер$
жит значения контрольной суммы. В отладочных компоновках ключ /RELEASE
использовать не следует, так как он требует отключения компоновки с при$
ращением. Чтобы установить ключ /RELEASE для завершающих компоновок,
откройте окно Property Pages, папку Linker, страницу Advanced и выберите
в поле Set Checksum (использовать контрольную сумму) значение Yes
(/RELEASE).
см. след. стр.

56

ЧАСТЬ I

/PDBSTRIPPED

Сущность отладки

(не включать частные символы в PDB-файл)

Одной из сложнейших отладочных проблем является получение чистого
стека вызовов. Причина, по которой вы не можете получить хорошие сте$
ки вызовов, в том, что код «плавающих стеков» не включает специальных
данных о кадре стека с отсутствующим указателем (FPO, Frame pointer omis$
sion), которые помогли бы расшифровать имеющийся стек. Так как данные
FPO для вашего приложения содержатся в PDB$файлах, вы можете просто
предоставить эти файлы клиенту. Конечно, это вполне обоснованно заста$
вит вас и вашего менеджера нервничать, но не забывайте, что до появле$
ния Visual C++ .NET у вас было гораздо больше проблем с получением чис$
тых стеков вызовов.
Если вы когда$нибудь устанавливали символы ОС от Microsoft (см. раз$
дел «Установите символы ОС и создайте хранилище символов»), вы, веро$
ятно, заметили, что символы Microsoft предоставляли вам полную инфор$
мацию о стеках вызовов, не выдавая никаких секретов. Для этого програм$
мисты Microsoft делают следующее: они включают в PDB$файлы только
открытые функции и крайне важные данные FPO, но не закрытую инфор$
мацию вроде переменных и данных об исходных кодах и номерах строк.
Ключ /PDBSTRIPPED позволяет вам безопасно создавать аналогичный тип
символов для своего приложения, не выдавая никаких секретов. Есть новость
и получше: сокращенный PDB$файл генерируется одновременно с его пол$
ной версией, поэтому я очень рекомендую устанавливать этот ключ для
завершающих компоновок. Откройте диалоговое окно проекта Property Pages,
папку Linker, страницу Debugging и задайте в поле Strip Private Symbols (не
включать закрытые символы) расположение и название файла символов. Я
всегда использую строку $(OutDir)/ $(ProjectName)_STRIPPED.PDB, чтобы было
ясно, какой PDB$файл является сокращенной версией, а какой — полной.
Если вы отсылаете сокращенные PDB$файлы заказчику, удалите из назва$
ний часть «_STRIPPED», чтобы их могли загрузить такие программы, как
Dr. Watson.

Разработайте несложную диагностическую систему
для заключительных компоновок
Больше всего я ненавижу ошибки, которые происходят только на компьютерах
одного$двух пользователей. Все остальные работают с программой без проблем,
но у этих происходит что$то совсем не то, почти не поддающееся пониманию.
Хотя вы всегда можете попросить пользователя прислать непослушный компью$
тер вам, эта стратегия не всегда удобна. Если клиент живет на одном из островов
в Карибском море, вы, конечно, согласились бы слетать туда и отладить пробле$
му на месте. Однако я почему$то не слышал, чтобы многие компании так щепе$
тильно относились к качеству своей продукции. Не встречались мне и разработ$
чики, готовые с радостью отправиться за Северный полярный круг.
Если проблема происходит только на одной$двух машинах, нужно узнать по$
ток выполнения программы на этих компьютерах. Многие разработчики уже

ГЛАВА 2

Приступаем к отладке

57

поддерживают слежение за потоком выполнения при помощи регистрационных
файлов и журналов событий, но я хочу особо подчеркнуть, насколько важны та$
кие журналы для решения проблем. Протоколирование потока выполнения ока$
жется при решении проблем гораздо полезнее, если вся группа будет подходить
к этому организованно.
При протоколировании информации чрезвычайно важно следовать опреде$
ленному шаблону. Если данные будут иметь согласованный формат, разработчи$
кам будет гораздо легче проанализировать файл и выяснить интересующие их
моменты. Если протоколировать информацию правильно, можно получить про$
сто огромный объем полезных данных, а написав сценарий на Perl’е или каком$
то другом языке — легко разделить информацию на важную и второстепенную,
существенно ускорив ее обработку.
Ответ на вопрос, что^ протоколировать, зависит главным образом от проекта,
однако в любом случае нужно регистрировать хотя бы ошибочные и аномальные
ситуации. Кроме того, следует попытаться учесть логический смысл операции
программы. Так, если ваша программа работает с файлами, не стоит записывать в
журнал такие подробности, как «Переходим в файле к смещению 23»; вместо это$
го нужно протоколировать открытие и закрытие файла. Тогда, увидев, что после$
дняя запись в журнале гласит «Подготавливаем открытие D:\Foo\BAR.DAT», вы уз$
наете, что ваш BAR.DAT скорее всего поврежден.
Глубина протоколирования зависит также от вызываемого им снижения про$
изводительности. Я обычно протоколирую все, что мне может понадобиться, и
наблюдаю за производительностью заключительных компоновок, когда протоко$
лирование не ведется. Современные средства слежения за производительностью
позволяют легко узнать, получает ли управление ваш код протоколирования. Если
да, вы можете немного снизить объем регистрируемой информации, пока не до$
стигнете приемлемого баланса с производительностью приложения. Определить,
что^ именно протоколировать, сложно. В главе 3 я расскажу, что нужно протоко$
лировать в управляемых приложениях, а в главе 18 покажу, как выполнять высо$
коскоростную трассировку неуправляемых приложений с минимальными усили$
ями. Другим полезным средством является очень быстрая, но неправильно назван$
ная система Event Tracing, встроенная в Windows 2000 и более поздние версии (см.
о ней по адресу: http://msdn.microsoft.com/library/default.asp?url=/library/en$us/
perfmon/base/ event_tracing.asp).

Частые сборки программы
и дымовые тесты обязательны
Два из самых важных элементов инфраструктуры — система сборки программы
и комплект дымовых тестов. Система сборки выполняет компиляцию и компоновку
программы, а комплект дымовых тестов включает тесты, которые запускают про$
грамму и подтверждают, что она работает. Джим Маккарти (Jim McCarthy) в кни$
ге «Dynamics of Software Development» (Microsoft Press, 1995) называет ежеднев$
ное проведение сборки программы и дымовых тестов сердцебиением проекта. Если
эти процессы неэффективны, проект мертв.

58

ЧАСТЬ I

Сущность отладки

Частые сборки
Проект надо собирать каждый день. Порой мне говорят, что некоторые проекты
бывают столь огромны, что их невозможно собирать каждый день. Означает ли
это, что они включают более 40 миллионов строк кода, лежащих в основе Windows
XP или Windows Server 2003? Учитывая, что эти ОС — самые масштабные коммер$
ческие программные проекты в истории и все же собираются каждый день, я так
не думаю. Итак, неежедневная сборка программы оправданий не имеет. Вы не только
должны собирать проект каждый день, но и автоматизировать этот процесс.
При сборке следует одновременно собирать и заключительную, и отладочную
компоновки. Как я покажу ниже, отладочные компоновки очень важны. Неудач$
ная сборка программы — большой грех. Если разработчики зарегистрировали код,
который не компилируется, виновного нужно наказать. Публичная порка, веро$
ятно, была бы несколько жесткой формой наказания (хотя и не слишком), но есть
и другой метод: заставьте провинившегося публично раскаяться в преступлении
и покупать пончики для всей группы. По крайней мере в группах, в которых ра$
ботал я, это всегда давало отличные результаты. Если в вашей группе нет штатно$
го сотрудника, отвечающего за сборку программы, вы можете наказать человека,
по вине которого провалилась сборка программы, возложив на него ответствен$
ность за сборку до тех пор, пока эта обязанность не перейдет к его товарищу по
несчастью.
Одна из лучших практик ежедневной сборки проекта, которую я когда$либо
использовал, заключается в оповещении членов группы по электронной почте при
окончании сборки. При автоматизированной ночной сборке программы каждый
член группы может утром сразу же узнать, увенчалась ли сборка успехом; если нет,
группа может предпринять немедленные действия по исправлению ситуации.
Чтобы избежать проблем со сборкой программы, каждый член группы должен
иметь одинаковые версии всех инструментов и компонентов сборки. Как я уже
упоминал, в некоторых группах это гарантируется путем хранения системы сборки
программы в системе управления версиями. Если члены группы работают с раз$
ными версиями инструментов, включая разные версии пакетов обновлений (service
pack), они создают идеальную почву для ошибок при сборке программы. Если
убедительных причин использования кем$нибудь другой версии компилятора нет,
никакой разработчик не должен обновлять свои инструменты по собственной воле.
Кроме того, все члены группы должны использовать для сборки своих частей
программы одни и те же сценарии и компьютеры. Так образуется надежная связь
между тем, что создается разработчиками, и тем, что тестируется тестировщиками.
При каждой сборке программы система сборки будет извлекать самую после$
днюю версию исходных кодов из системы управления версиями. В идеале разра$
ботчикам также следует ежедневно использовать файлы системы управления вер$
сиями. В случае крупного проекта разработчики должны иметь возможность лег$
кого получения ежедневно компилируемых двоичных файлов, чтобы избежать
длительной компиляции программы на своих компьютерах. Нет ничего хуже, чем
тратить время на решение сложной проблемы только затем, чтобы обнаружить,
что проблема связана с более старой версией файла на машине разработчика. Дру$
гое преимущество частого извлечения файлов из системы управления версиями
состоит в том, что это помогает навязать правило «никакая сборка программы не

ГЛАВА 2

Приступаем к отладке

59

должна заканчиваться неудачей». При частом извлечении файлов из системы уп$
равления версиями любая проблема общей сборки программы автоматически ста$
новится локальной проблемой каждого разработчика. Если руководителей неудача
ежедневной сборки программы раздражает, то разработчики просто лопаются от
гнева, если вы нарушаете их локальную сборку. Зная, что неудача общей сборки
программы означает неудачу сборки для всех членов группы, разработчики бу$
дут более ответственно подходить к регистрации в общих исходных текстах только
тщательно проверенного кода.

Стандартный вопрос отладки
Когда прекращать модернизацию компилятора и других инструментов?
Как только вы завершили разработку функциональности приложения, что
также известно как стадия бета$1, вам определенно не следует модернизи$
ровать никакие инструменты. Схема оптимизации нового компилятора,
какой бы хорошей она ни казалась, не оправдывает изменения кода про$
граммы. Ко времени достижения стадии бета$1 значительный объем тести$
рования уже выполнен, и, если вы измените инструменты, начать его при$
дется с нуля.

Дымовые тесты
Так называют тест, проверяющий основные функции приложения. Термин «ды$
мовой тест» берет начало в электронике. На некотором этапе разработки продукции
инженеры по электронике подключают устройство в сеть и смотрят, не задымит$
ся ли оно (в буквальном смысле). Если устройство не дымит или, что еще хуже,
не загорается, значит, группа достигла определенного прогресса. Обычно дымо$
вой тест приложения заключается в проверке его основных функций. Если они
работают, можно начинать серьезное тестирование программы. Дымовой тест
играет роль базового показателя состояния кода.
Дымовой тест представляет собой просто контрольную таблицу функций, ко$
торые может выполнять программа. Начните с малого: установите приложение,
запустите его и закройте. По мере цикла разработки дымовые тесты также долж$
ны развиваться, чтобы можно было исследовать новые функции программы. Ды$
мовой тест должен включать по крайней мере по одному тесту для каждой функ$
ции и каждого крупного компонента программы. Это значит, что, работая в отде$
ле готовой продукции, вы должны тестировать каждую функцию, упомянутую в
рекламных проспектах. Если вы сотрудник ИТ$отдела, тестируйте основные фун$
кции, которые вы обещали реализовать менеджеру по информатизации и своим
клиентам. Помните: дымовой тест вовсе не должен проверять абсолютно все пути
выполнения вашей программы, его надо использовать, чтобы узнать, выполняет
ли программа основные функции. Как только она прошла дымовой тест, сотруд$
ники отдела технического контроля могут начинать свой тяжкий труд, пытаясь
нарушить работу программы новыми изощренными способами.
Чрезвычайно важный компонент дымового теста — та или иная форма теста
производительности. Многие забывают про это, в результате чего приходится

60

ЧАСТЬ I

Сущность отладки

расплачиваться на более поздних этапах цикла разработки программы. Если у вас
есть сравнительный тест какой$либо операции программы (например, как долго
запускалась последняя версия программы), неудачу теста можно определить как
замедление выполнения операции на 10% или более. Я всегда удивляюсь тому, сколь
часто небольшое изменение в безобидном на вид месте программы может при$
водить к огромному снижению производительности. Наблюдая за производитель$
ностью программы на протяжении всего цикла ее разработки, вы сможете решать
проблемы с производительностью до того, как они выйдут из под контроля.
В идеале при проведении дымового теста выполнение программы должно быть
автоматизировано, чтобы она могла работать без взаимодействия с пользовате$
лем. Инструмент, применяемый для автоматизации ввода информации и выпол$
нения действий с приложением, называется средством регрессивного тестирова$
ния. Увы, не всегда можно автоматизировать тестирование каждой функции, осо$
бенно при изменении UI. На рынке много хороших средств регрессивного тес$
тирования, поэтому, если вы работаете над крупным сложным приложением и не
можете позволить себе, чтобы кто$либо из вашей группы отвечал исключительно
за проведение и поддержку дымовых тестов, возможно, следует подумать о покупке
такого инструмента. Если уговорить начальника приобрести коммерческий ин$
струмент не получается, можете использовать приложение Tester из главы 16, за$
писывающее ввод мыши и клавиатуры в файл JScript или VBScript, который затем
можно воспроизвести.
К неудачному выполнению дымового теста следует относиться так же серьез$
но, как и к неудачной сборке программы. На создание дымового теста уходит очень
много усилий, поэтому никакой разработчик не должен относиться к нему лег$
комысленно. Именно дымовой тест говорит группе контроля качества о том, что
полученная ими версия программы достаточно хороша, чтобы с ней можно было
работать, поэтому проведение дымового теста должно быть обязательным. Если
у вас есть автоматизированный дымовой тест, возможно, его стоит предоставить
и разработчикам, чтобы они также могли автоматизировать свое тестирование.
Кроме того, автоматизированный дымовой тест надо проводить с каждой ежед$
невной сборкой программы, чтобы можно было сразу оценить ее качество. Как и
при ежедневной сборке, результаты дымового теста следует сообщать членам груп$
пы по электронной почте.

Работу над программой установки
следует начинать немедленно
Начинайте работать над программой установки сразу же после начала проекта.
Это первая часть вашего приложения, которую видят пользователи. Слишком
многие программы оставляют плохое первое впечатление, показывая, что програм$
ма установки была создана в последнюю минуту. Если вы начнете работу над про$
граммой установки как можно раньше, у вас будет время на ее тестирование и
отладку. Разработав ее на ранней стадии проекта, вы сможете включить ее в ды$
мовой тест. Это позволит вам провести ее многократное тестирование, а ваши тесты
еще на один шаг приблизятся к имитации того, как пользователи будут работать
с программой.

ГЛАВА 2

Приступаем к отладке

61

Ранее я рекомендовал собирать и заключительную, и отладочную версии про$
граммы. Вам также понадобится программа установки, которая позволит устанав$
ливать обе версии. Хотя управляемые приложения поддерживают хваленый ме$
тод установки при помощи команды XCOPY, он годится только для простейших
программ. Реальные управляемые приложения скорее всего должны будут иници$
ализировать базы данных, помещать сборки в глобальный кэш сборок и выпол$
нять другие операции, которые просто невозможны при обычном копировании.
Программисты, разрабатывающие неуправляемые приложения, должны также
помнить, что технология COM все еще жива и здорова, а COM требует внесения
такого объема информации в реестр, что без программы установки правильная
установка приложения становится почти невозможной. Программа установки
отладочных компоновок позволяет разработчикам легко установить отладочную
версию приложения и быстро приступить к решению проблемы.
Еще одно преимущество как можно более раннего создания программы уста$
новки в том, что другие сотрудники компании гораздо раньше смогут начать те$
стирование приложения. Получив программу установки, сотрудники службы тех$
нической поддержки начнут использовать приложение и предоставлять обратную
связь достаточно рано, чтобы вы успели придумать оптимальный способ реше$
ния обнаруженных ими проблем.

Тестирование качества должно проводиться
с отладочными компоновками
Если вы будете следовать моим рекомендациям из главы 3, вы получите несколь$
ко прекрасных средств диагностики своего кода. Проблема в том, что диагности$
ка обычно приносит выгоду только разработчикам. Чтобы сотрудники группы
контроля качества оказывали более эффективную помощь в отладке ошибок, они
также должны использовать отладочные компоновки. Вы будете удивлены тем, как
много проблем вы найдете и решите, если группа контроля качества проведет
тестирование отладочных компоновок.
Есть одно очень важное условие: запретить вывод информации макросами ASSERT,
чтобы они не мешали работе автоматизированных тестов отдела контроля каче$
ства. В главе 3 я расскажу о применении макросов ASSERT для управляемого и не$
управляемого кода. И управляемый код, и мой макрос SUPERASSERT для неуправля$
емого кода поддерживают отключение всплывающих информационных окон и
вывода других данных, вызывающих неудачу автоматизированных тестов.
На начальных стадиях цикла разработки программы сотрудники группы кон$
троля качества должны тестировать и отладочные, и заключительные компонов$
ки. По мере развития проекта им следует все большее внимание уделять заклю$
чительным компоновкам. Пока вы не достигнете точки альфа$версии, когда в
программе будет реализовано достаточно функций, чтобы ее можно было пока$
зать клиентам, группа контроля качества должна тестировать отладочные компо$
новки два$три дня в неделю. При приближении к контрольной точке бета$1 вре$
мя тестирования отладочных компоновок нужно снизить до двух дней в неделю.
По достижении точки бета$2, когда все функции программы реализованы и ос$
новные ошибки исправлены, это время надо уменьшить до одного дня в неделю.

62

ЧАСТЬ I

Сущность отладки

Миновав контрольную точку предварительной версии (release candidate), следует
перейти на тестирование только заключительных компоновок.

Устанавливайте символы ОС
и создайте хранилище символов
Как известно любому человеку, который провел более 5 минут над разработкой
программ для Windows, секрет эффективной отладки состоит в согласованном
использовании корректных символов. Если вы пишете управляемый код, то без
символов отладка вообще может оказаться невозможной. Работая без символов
над неуправляемым кодом, вы, возможно, не получите чистые стеки вызовов из$
за «плавающих стеков» — для этого нужны данные FPO, содержащиеся в PDB$файле.
Если вы думаете, что заставить всех членов группы и сотрудников компании
применять корректные символы очень сложно, представьте, насколько хуже об$
стоит дело в группе разработчиков ОС Microsoft. Они работают над крупнейшим
коммерческим приложением в мире, имеющем более 40 миллионов строк кода.
Они выполняют сборку каждый день, и в каждый конкретный момент времени во
многих странах мира выполняются тысячи различных компоновок ОС. Не прав$
да ли, с этой точки зрения, ваши проблемы с символами — сущая чепуха: даже
если вы думаете, что работаете над большим проектом, ваши неудобства ни в ка$
кое сравнение не идут с такой огромной символьной болью!
Кроме проблемы с символами перед программистами Microsoft также стояла
проблема получения нужных двоичных файлов. Одна из разработанных в Microsoft
технологий, призванных помочь отлаживать ошибки, называется минидамп, или
аварийный дамп. Минидамп представляет собой файлы, содержащие сведения о
состоянии приложения на момент аварийного завершения. Если вы имеете опыт
работы с другими ОС, можете называть его дампом ядра. Привлекательность ми$
нидампа объясняется тем, что, имея файлы, характеризующие состояние прило$
жения, вы сможете загрузить его в отладчик, и все данные будут такими, как если
бы крах приложения произошел на ваших глазах. О создании собственных ми$
нидампов, а также о работе с ними в отладчиках я расскажу в следующих главах.
Большая проблема минидампов заключается в загрузке правильных двоичных
файлов. Даже если вы создаете программу на платформе Windows Server 2003 или
более новой, минидамп клиента может быть создан в системе Windows 2000 только
с первым пакетом обновления. В этом случае справедливо то же утверждение, что
и в ситуации с символами: если вы не можете загрузить точные двоичные файлы,
находившиеся в памяти во время создания минидампа, вы полностью заблуждае$
тесь, если думаете, что он позволит вам легко справиться с проблемой.
Разработчики Microsoft понимали, что им просто необходимо сделать что$то,
чтобы облегчить свою жизнь. Мы, программисты, не работающие в Microsoft, также
жаловались, что из$за отсутствия символов и двоичных файлов ОС, соответству$
ющих многочисленным обновлениям и исправлениям, установленным на конк$
ретном компьютере, отладка превращается в пытку. Концепция сервера символов
проста: хранить все символы и двоичные файлы публичных компоновок в извес$
тном месте и наделить отладчики необходимым интеллектом, чтобы они могли
использовать корректные символы и двоичные файлы для каждого загружаемого
в процесс модуля — независимо от того, загружается ли он вашей программой или

ГЛАВА 2

Приступаем к отладке

63

ОС — без взаимодействия с пользователем. Вся прелесть в том, что реальность почти
столь же проста! С серверами символов связано несколько проблем, которые я
опишу чуть ниже, но, если сервер символов создан и настроен как надо, никто в
вашей группе или компании никогда не будет страдать от отсутствия корректных
символов или двоичных файлов независимо от того, разрабатывает ли он управ$
ляемый, неуправляеымй или смешанный код и использует ли отладчик Visual Studio
.NET или WinDBG. И еще одна приятная новость: к этой книге я прилагаю несколько
файлов, которые возьмут на себя всю работу по получению отличных символов
и двоичных файлов для ОС и ваших программ.
В документации к Visual Studio .NET упоминается один метод создания серве$
ра символов для отладки, но он требует выполнения нескольких одинаковых дей$
ствий для каждой загружаемой программы, что очень неудобно. Кроме того, там
не обсуждается самое важное: как заполнить сервер символами и двоичными
файлами. Так как именно в этом огромное преимущество применения сервера
символов, то для достижения символьной нирваны вам понадобится сделать сле$
дующее.
Получить физический сервер, к которому сможет получать доступ любой со$
трудник, работающий над вашими проектами, довольно просто. Вы, вероятно,
захотите назвать этот сервер \\SYMBOLS, чтобы сразу было ясно, какую функцию
он выполняет. В оставшейся части я буду использовать именно это имя сервера.
Он не обязательно должен быть очень мощным, так как будет выполнять функ$
цию обычного файлового сервера. Однако я очень рекомендую, чтобы сервер имел
довольно большой объем дискового пространства. Для начала вполне хватит от
40 до 80 Гб. Установив все серверное ПО, создайте два каталога с общим досту$
пом под названием OSSYMBOLS и PRODUCTSYMBOLS, разрешив запись и чтение
всем разработчикам и сотрудникам отдела контроля качества. Вы, наверное, уже
догадались по названиям, что в одном каталоге будут храниться символы и дво$
ичные файлы ОС, а во втором — аналогичные файлы ваших программ. Для про$
стоты администрирования их следует хранить отдельно. Я полагаю, вы сможете
получить в свое распоряжение этот сервер. Все сражения за него я оставляю вам
в качестве упражнения.
Следующий шаг к достижению символьной нирваны — установка пакета Debug$
ging Tools for Windows. Его можно или загрузить с сайта Microsoft по адресу www.mic$
rosoft.com/ddk/debugging, или установить с CD, прилагаемого к книге. Обратите
внимание: двоичные файлы для сервера символов созданы группой разработчи$
ков Windows, а не Visual Studio .NET. Проверьте, существует ли обновленная вер$
сия Debugging Tools for Windows; похоже, группа разработчиков обновляет этот
пакет довольно часто. После установки Debugging Tools for Windows укажите ус$
тановочный каталог в системной переменной среды PATH. Разрешите запись ин$
формации в сервер символов и ее чтение для четырех важнейших двоичных фай$
лов: SYMSRV.DLL, DBGHELP.DLL, SYMCHK.EXE и SYMSTORE.EXE.
Если вы работаете с прокси$сервером, требующим регистрации при каждом
подключении к Интернету, я вам сочувствую. К счастью, группа разработчиков
Windows не осталась безучастной к вашей боли. В пакет Debugging Tools for Windows
версии 6.1.0017 входит новая версия библиотеки SYMSRV.DLL, удовлетворяющая
требованиям компаний, следящих за каждым Интернет$пакетом. Изучите в доку$

64

ЧАСТЬ I

Сущность отладки

ментации к Debugging Tools for Windows раздел «Using Symbol Servers and Symbol
Stores» (Использование серверов и хранилищ символов), в котором обсуждается
работа с прокси$серверами и межсетевыми экранами. Там сказано, как задать
переменную среды _NT_SYMBOL_PROXY, чтобы избежать ввода имени пользователя и
пароля при каждом запросе на загрузку символов. Следите за появлением новых
версий Debugging Tools for Windows на сайте www.microsoft.com/ddk/debugging.
Группа разработчиков Windows постоянно работает над улучшением серверов сим$
волов, поэтому я рекомендую следить за появлением новых версий этого пакета.
Как только вы установите Debugging Tools for Windows, вам останется только
создать системную среду для Visual Studio и отладчика WinDBG. Лучше всего за$
дать переменную среды в системных параметрах (т. е. параметрах для всего ком$
пьютера). Для получения доступа к этой области в Windows XP/Server 2003 нуж$
но щелкнуть правой кнопкой значок My Computer (Мой компьютер) и выбрать в
контекстном меню пункт Properties (Свойства). Выберите вкладку Advance (Допол$
нительно) и нажмите кнопку Environment Variables (Переменные среды) в ниж$
ней части страницы. Диалоговое окно Environment Variables показано на рис. 2$
10. Если переменной среды _NT_SYMBOL_PATH нет, создайте ее и присвойте ей следу$
ющее значение (обратите внимание, что указанное выражение должно быть вве$
дено в одной строке):

SRV*\\Symbols\OSSymbols*http://msdl.microsoft.com/download/symbols;
SRV*\\Symbols\ProductSymbols

Рис. 210.

Диалоговое окно Environment Variables

Переменная _NT_SYMBOL_PATH будет указывать Visual Studio .NET и WinDBG, где
искать ваши серверы символов. В указанной строке заданы два отдельных серве$
ра символов, отделенные точкой с запятой: один для символов ОС, а другой для
символов ваших программ. Буквы SRV в начале обеих частей строки приказыва$
ют отладчикам загрузить библиотеку SYMSRV.DLL и передать ей значения, распо$

ГЛАВА 2

Приступаем к отладке

65

ложенные после SRV. В случае первого сервера символов вы сообщаете SYMSRV.DLL,
что символы ОС будут храниться в каталоге \\Symbols\OSSymbols; вторая звездочка
является HTTP$адресом, который SYMSRV.DLL будет использовать для загрузки
любых символов (но не двоичных файлов), отсутствующих в сервере символов.
Этот раздел переменной _NT_SYMBOL_PATH обеспечит обновление символов ОС. Вторая
часть переменной _NT_SYMBOL_PATH говорит библиотеке SYMSRV.DLL о том, что спе$
цифические символы ваших программ следует искать только в общем каталоге
\\Symbols\ProductSymbols. Если вы хотите задать другие пути поиска, можете до$
бавить их к строке переменной _NT_SYMBOL_PATH, разделив их точками с запятой.
Так, в следующей строке указано, чтобы поиск символов ваших программ осуще$
ствлялся и в корневом системном каталоге System32, потому что именно в этот
каталог Visual Studio .NET помещает PDB$файлы стандартной библиотеки C и MFC
при установке:

SRV*\\Symbols\OSSymbols*http://msdl.microsoft.com/download/symbols;
SRV*\\Symbols\ProductSymbols;c:\windows\system32
В полной степени достоинства сервера символов обнаруживаются при его
заполнении символами ОС, загруженными с сайта Microsoft. Если вы опытный
«охотник на насекомых», то, вероятно, уже установили символы ОС. Однако это
всегда немного разочаровывает, так как почти на всех компьютерах установлены
те или иные пакеты исправлений, а определенные символы ОС никогда не вклю$
чают символы этих пакетов. К счастью, серверы символов гарантируют, что вы
всегда сможете получить абсолютно правильные символы ОС без всякого труда!
Это огромное благо, которое здорово облегчит вашу жизнь. Оно стало возмож$
ным благодаря тому, что Microsoft открыла доступ к символам для всех ОС от
Microsoft Windows NT 4 до последних версий Windows XP/.NET Server 2003, включая
все пакеты обновлений и исправления.
В начале следующего сеанса отладки отладчик автоматически увидит, что пе$
ременная _NT_SYMBOL_PATH задана и, если нужного ему файла символов не найдет$
ся, начнет загрузку символов ОС с Web$сайта Microsoft и поместит их в ваше хра$
нилище символов. Внесем ясность: сервер символов загрузит с сайта только нуж$
ные ему символы, а не все символы ОС. Размещение хранилища символов в об$
щем каталоге сэкономит вам много времени: если один из членов группы уже
загрузил нужный вам символ, вам не понадобится загружать его повторно.
В самом по себе хранилище символов нет ничего удивительного. Это обыч$
ная база данных, которая для нахождения файлов использует файловую систему.
На рис. 2$11 показано, как выглядит часть дерева моего сервера символов в окне
Windows Explorer. Корневой каталог называется OSSymbols, и все файлы симво$
лов, такие как ADVAPI32.PDB, находятся на первом уровне. Под именем каждого
файла символов находится каталог, название которого соответствует дате/времени,
сигнатуре и прочей информации, необходимой для полного определения конк$
ретной версии файла символов. Помните: при наличии нескольких вариантов
файла (например, ADVAPI32.PDB) для различных версий ОС, у вас будет и несколько
каталогов, соответствующих каждому варианту. В каталоге сигнатур скорее всего
будет находиться конкретный файл символов для данного варианта. Есть меры
предосторожности, которые нужно соблюдать, создавая при помощи специаль$

66

ЧАСТЬ I

Сущность отладки

ных текстовых файлов указатели на другие файлы в хранилище символов, но если
вы будете делать все так, как я рекомендую, все будет в порядке.

Рис. 211.

Пример базы данных сервера символов

Загрузка символов во время отладки очень полезна, однако она не способствует
получению двоичных файлов ОС. Кроме того, лучше было бы не возлагать ответ$
ственность за получение символов на разработчиков, а изначально наполнить
серверы символов всеми двоичными файлами и символами всех поддерживаемых
вами ОС. Это позволило бы вам работать с любыми минидампами клиентов и
любыми отладочными проблемами, с которыми вы столкнетесь в своем отделе.
Пакет Debugging Tools for Windows (в состав которого входит WinDBG) вклю$
чает два очень полезных инструмента: Symbol Checker (SYMCHK.EXE), предназна$
ченный для загрузки в ваш символьный сервер символов Microsoft, и Symbol Store
(SYMSTORE.EXE), который заботится о загрузке в хранилище символов двоичных
файлов. Я понимал, что для наполнения своего сервера символами и двоичными
файлами для всех версий ОС, которые я хочу поддерживать, мне придется рабо$
тать с обоими инструментами, поэтому я решил автоматизировать этот процесс.
Я хотел, чтобы создание сервера символов ОС было простым и легким, чтобы он
постоянно был заполнен последними двоичными файлами и символами и чтобы
это практически не требовало работы.
Создавая первый сервер символов ОС, установите первую версию ОС без вся$
ких пакетов обновлений и исправлений. Установите пакет Debugging Tools for
Windows и укажите его установочный каталог в переменной PATH. Для получения
двоичных файлов и символов ОС запустите мой файл OSSYMS.JS, про который я
расскажу чуть ниже. Когда OSSYMS.JS завершит свою работу, установите первый
пакет обновлений и выполните OSSYMS.JS повторно. Установив все пакеты обнов$
лений и скопировав все их двоичные файлы и символы, установите все обновле$
ния, рекомендованные функцией Windows Update ОС Windows 2000/XP/.NET Server
2003, и запустите OSSYMS.JS в последний раз. Повторите этот процесс для всех
ОС, которые вам нужно поддерживать. Теперь, чтобы ваш сервер символов посто$
янно находился в отличном состоянии, нужно будет только запускать OSSYMS.JS
каждый раз, когда вы установите исправление или новый пакет обновлений. Ради

ГЛАВА 2

Приступаем к отладке

67

целей планирования я подсчитал, что это требует чуть менее 1 Гб для каждой версии
ОС и примерно такого же объема для каждого пакета обновлений.
Возможно, вы думаете, что OSSYMS.JS (и вспомогательный файл WRITEHOT$
FIXES.VBS, который нужно скопировать в тот же каталог, что и OSSYMS.JS) пред$
ставляет собой простую оболочку для вызова программ SYMCHK.EXE и SYMSTO$
RE.EXE, но это не так. На самом деле это очень полезная оболочка. Если вы изу$
чите ключи командной строки обеих программ, вам непременно захочется авто$
матизировать их работу, потому что в ключах очень легко запутаться. Запустив
программу OSSYMS.JS без параметров командной строки, вы увидите текст, опи$
сывающий все ее функции:

OSsyms  Version 1.0  Copyright 20022003 by John Robbins
Debugging Applications for Microsoft .NET and Microsoft Windows
Fills your symbol server with the OS binaries and symbols.
Run this each time you apply a service pack/hot fix to get the perfect
symbols while debugging and for mini dumps.
SYMSTORE.EXE and SYMCHK.EXE must be in the path.
Usage: OSsyms [e|v|b|s|d]
 The symbol server in \\server\share.
e
 Do EXEs as well as DLLs.
v
 Do verbose output.
d
 Debug the script. (Shows what would execute.)
b
 Don't add the binaries to the symbol store.
s
 Don't add the symbols to the symbol store.
(Not recommended)
Единственный необходимый параметр — путь к серверу символов в формате
\\сервер\общий_каталог. Когда вы запускаете программу OSSYMS.JS, она сначала
определяет версию ОС и уровень установленного пакета обновлений и находит
все исправления. Это позволяет приложению SYMSTORE.EXE правильно заполнить
информацию о программе, ее версии и поле комментария, чтобы вы могли точ$
но определить, какие символы и двоичные файлы хранятся в сервере символов.
Про специфические ключи командной строки SYMSTORE.EXE и то, как узнать, что
находится в вашей базе данных, я расскажу ниже. Огромная важность информа$
ции об установленных пакетах обновлений и исправлений объясняется тем, что
при получении минидампа она позволяет быстро определить, есть ли в сервере
символов двоичные файлы и символы для этого конкретного случая.
После сбора нужной системной информации программа OSSYMS.JS выполня$
ет рекурсивный поиск всех двоичных файлов DLL в каталоге ОС (%SYSTEMROOT%) и
копирует их в сервер символов. Выполнив копирование, OSSYMS.JS вызывает про$
грамму SYMCHK.EXE для автоматической загрузки из Интернета всех имеющихся
символов для этих DLL. Если вы хотите сохранить в сервере символов все EXE$
файлы и их символы, укажите в командной строке OSSYMS.JS после пути к серве$
ру символов ключ–e.
Чтобы узнать, какие двоичные файлы и символы были сохранены в сервере
символов, а какие были проигнорированы (с указанием причин), прочитайте

68

ЧАСТЬ I

Сущность отладки

информацию, содержащуюся в текстовых файлах DllBinLog.TXT и DllSymLog.TXT,
в которых описаны результаты добавления в сервер двоичных файлов и симво$
лов DLL соответственно. В случае EXE$файлов соответствующие файлы называ$
ются ExeBinLog.TXT и ExeSymLog.TXT.
Выполнение OSSYMS.JS может потребовать времени. Копирование двоичных
файлов в сервер символов выполняется быстро, однако загрузка символов из сети
Интернет может затянуться. При загрузке символов ОС для DLL и EXE$файлов нужно
будет загрузить скорее всего около 400 Мб данных. Следует избегать добавления
двоичных файлов в сервер символов несколькими комьютерами одновременно.
Это объясняется тем, что SYMSTORE.EXE использует в качестве базы данных фай$
ловую систему и текстовый файл, поэтому она не поддерживает транзакций. Про$
грамма SYMCHK.EXE не использует текстовую базу данных SYMSTORE.EXE, поэтому
сохранение символов несколькими разработчиками одновременно вполне допу$
стимо.
Microsoft постоянно размещает на своем сайте все большее число символов для
своей продукции. Программа OSSYMS.JS достаточно гибка, чтобы можно было легко
указывать серверу символов дополнительные каталоги хранения двоичных фай$
лов и соответствующих символов. Чтобы добавить в сервер символов новые дво$
ичные файлы, найдите глобальную переменную g_AdditionalWork, расположенную
в начале файла OSSYMS.JS. Этой переменной присвоено значение null, поэтому в
функции main она не обрабатывается. Чтобы сохранить в сервере символов но$
вый набор файлов, создайте Array и добавьте в него в качестве элемента класс
SymbolsToProcess. Ниже показано, как включить сохранение в сервере символов всех
DLL, которые находятся в каталоге Program Files. Заметьте: первый элемент не обязан
быть переменной среды — он может быть названием конкретного каталога, ска$
жем, «e:\ Program Files». Однако использование общей системной переменной среды
позволяет избежать жесткого задания названий дисков.

var g_AdditionalWork = new Array
(
new SymbolsToProcess ( "%ProgramFiles%" ,
"*.dll"
,
"PFDllBinLog.TXT" ,
"PFDllSymLog.TXT" )
) ;

//
//
//
//

Начальный каталог.
Ищем все DLL.
Журнал для двоичных файлов.
Журнал для символов.

Я объяснил, как сохранить в сервере символов двоичные файлы и символы ОС.
Давайте теперь рассмотрим, как с помощью программы SYMSTORE.EXE сделать то
же самое для ваших программ. SYMSTORE.EXE имеет много ключей командной
строки (табл. 2$2).

Табл. 2-2. Важные ключи командной строки программы SYMSTORE
Ключ

Описание

add

Добавляет файлы в хранилище символов.

del

Удаляет из хранилища символов конкретный набор файлов.

/f File

Добавляет в хранилище символов конкретный файл или каталог.

/r

Рекурсивно добавляет в хранилище символов файлы или каталоги.

/s Store

Корневой каталог хранилища символов.

ГЛАВА 2

Табл. 2-2. Важные ключи командной строки …
Ключ

Описание

/t Product

Название программы.

/v Version

Версия программы.

/c

Дополнительные комментарии.

Приступаем к отладке

69

(продолжение)

/o

Подробный вывод, полезный для отладки.

/i ID

Идентификатор транзакции из файла history.txt, используемый
при удалении файлов.

/?

Справка.

Наилучший способ использования SYMSTORE.EXE состоит в автоматическом
сохранении EXE$, DLL$ и PDB$файлов дерева проекта после его ежедневной сборки
(если дымовой тест покажет, что программа работает), после каждой контрольной
точки и при передаче компоновки за пределы группы. Если вы не обладаете дис$
ковым пространством огромного объема, то разработчикам не следует сохранять
в сервере символов свои локальные компоновки. Например, следующая команда
сохраняет в хранилище символов все PDB$ и двоичные файлы, которые будут
обнаружены во всех каталогах, дочерних по отношению к каталогу D:\BUILD (вклю$
чая и его).

symstore add /r /f d:\build\*.* /s \\Symbols\ProductSymbols
/t "MyApp" /v "Build 632" /c "01/22/03 Daily Build"
При добавлении файлов ключ /t (название программы) требуется всегда, но
для ключей /v (версия) и /c (комментарии) это, увы, не так. Советую всегда ис$
пользовать ключи /v и /c, потому что информация о том, какие файлы хранятся в
сервере символов вашей программы, никогда не может оказаться лишней. По мере
заполнения сервера символов вашей программы это приобретает особую важность.
Символы, хранящиеся в сервере символов ОС, имеют меньший объем из$за того,
что они не включают всех частных символов и типов, однако символы вашей про$
граммы могут достигать огромных размеров, что может приводить к заметному
уменьшению дискового пространства при работе над полугодовым проектом.
Непременно сохраняйте в сервере символов все компоновки, соответствую$
щие достижению контрольных точек, и компоновки, отсылаемые за пределы груп$
пы. Однако мне нравится держать в хранилище символов двоичные файлы и сим$
волы ежедневных компоновок не более чем за последние четыре недели. Как видно
из табл. 2$2, SYMSTORE.EXE поддерживает и удаление файлов.
Для гарантии того, что вы удаляете те файлы, которые действительно собира$
лись удалить, нужно посмотреть специальный каталог 000admin, находящийся в
общем каталоге сервера символов. В этом каталоге есть файл HISTORY.TXT, со$
держащий историю всех транзакций сервера символов и, если вы добавляли файлы
в сервер символов, набор пронумерованных файлов, включающих списки фай$
лов, которые на самом деле были добавлены в сервер символов в результате тран$
закций.
HISTORY.TXT является файлом со значениями, разделенными запятыми (Comma
separated value, CSV), поля которого приведены в табл. 2$3 (для добавления фай$
лов) и в табл. 2$4 (для удаления файлов).

70

ЧАСТЬ I

Табл. 2-3.

Сущность отладки

Поля CSV файла HISTORY.TXT для добавления файлов

Поле

Описание

ID

Номер транзакции. Это число имеет 10 разрядов, поэтому в об$
щей сложности сервер символов может выполнить
9,999,999,999 транзакций.

Add

При добавлении файлов это поле всегда имеет значение add.

File или Ptr

Показывает, что было добавлено: файл (file) или указатель
(ptr) на файл, находящийся в другом месте.

Date

Дата транзакции.

Time

Время начала транзакции.

Product

Название программы, указанное после ключа /t.

Version

Версия программы, указанная после ключа /v (необязательный
параметр) .

Comment

Текст комментария, указанный после ключа /c (необязательный
параметр) .

Unused

Неиспользуемое поле, зарезервированное на будущее.

Табл. 2-4. Поля CSV файла HISTORY.TXT для удаления файлов
Поле

Описание

ID

Номер транзакции.

Del

При удалении файлов это поле всегда имеет значение del.

Deleted Transaction

10$разрядный номер удаленной транзакции.

Как только вы определили номер транзакции, которую желаете удалить, сде$
лать это при помощи SYMSTORE.EXE очень просто:

symstore del /i 0000000009 /s \\Symbols\ProductSymbols
При удалении файлов из сервера символов я заметил одну странную вещь: не
выводится абсолютно никакой информации, подтверждающей, что удаление увен$
чалось успехом. Если вы забудете указать какой$то важный ключ командной строки,
например, само название сервера символов, вы не получите никаких предупреж$
дений и, возможно, будете ошибочно думать, что файлы были удалены. Поэтому
после удаления я всегда проверяю файл HISTORY.TXT, чтобы убедиться, что уда$
ление действительно имело место.

Исходные тексты и серверы символов
После упорядочения символов и двоичных файлов следующий элемент голово$
ломки — упорядочение исходных файлов. Правильные стеки вызовов — прекрасное
достижение, но пошаговое изучение комментариев к исходному коду не нравит$
ся никому. К сожалению, пока Microsoft не интегрирует компиляторы с системой
управления версиями, чтобы по мере создания компоновок компиляторы могли
извлекать и помечать исходные тексты программы, вам придется кое$что делать
вручную.
Возможно, вы не заметили, но все компиляторы из состава Visual Studio .NET
уже включают в PDB$файлы полный путь к исходным файлам программы. В пре$

ГЛАВА 2

Приступаем к отладке

71

дыдущих версиях компиляторов это не поддерживалось, что чрезвычайно ослож$
няло получение нужных исходных текстов. Полный путь повышает ваши шансы
на получение необходимых исходных файлов программы при отладке ее преды$
дущих версий или изучении минидампа.
На компьютере для сборки программы следует при помощи команды SUBST
отобразить корень дерева проекта на диск S:. В результате этого при сборке про$
граммы диск S: будет корневым каталогом информации об исходных текстах,
включаемой во все PDB$файлы, которые вы будете добавлять в хранилище сим$
волов. Если разработчику нужно будет отладить предыдущую версию исходного
кода, он сможет извлечь ее из системы управления версиями и отобразить ее при
помощи команды SUBST на диск S:. Благодаря этому отладчик, показывая исходный
код программы, сможет загрузить правильную версию файлов символов с мини$
мумом проблем.
Хотя я вкратце описал серверы символов, вам непременно следует полностью
прочитать раздел «Symbols» в документации к пакету Debugging Tools for Windows.
Технология серверов символов настолько важна для успешной отладки, что в ва$
ших интересах знать о ней как можно больше. Надеюсь, я смог доказать важность
серверов символов и описать способы их лучшего применения. Если вы еще не
создали свой сервер символов, я приказываю вам прекратить чтение и сделать это.

Резюме
В этой главе я описал чрезвычайно важные инфраструктурные требования по
минимизации времени отладки. Они варьируются от систем управления версия$
ми и отслеживания ошибок, параметров компилятора и компоновщика до пре$
имуществ ежедневных сборок и дымовых тестов и важности использования сим$
волов.
Возможно, ваша уникальная среда разработки предъявляет какие$нибудь до$
полнительные инфраструктурные требования, однако предложенные в этой гла$
ве рекомендации справедливы для всех сред. В их важности я убедился, работая
над реальными проектами. Если вы еще не используете в своей компании какие$
либо из этих инфраструктурных инструментов и методов, я настоятельно сове$
тую немедленно реализовать их. Они позволят вам сэкономить на отладке мно$
гие сотни часов.

Г Л А В А

3
Отладка при кодировании

В главе 2 я заложил основу общепроектной инфраструктуры, обеспечивающей
более эффективную работу. В этой главе мы определим, как облегчить отладку,
когда вы погрязли в кодовых баталиях. Большинство называет этот процесс за$
щитным программированием (defensive programming), но я предпочитаю думать
о нем несколько шире и глубже — как о профилактическом программировании
(proactive programming) или отладке при кодировании. По моему определению,
защитное программирование — это код обработки ошибок, сообщающий вам, что
возникла ошибка. Профилактическое программирование позволяет узнать, почему
возникла ошибка.
Создание защищенного кода — лишь часть борьбы за исправление ошибок.
Обычно специалисты пытаются провести очевидные защитные маневры — ска$
жем, проверить, что указатель на строку в C++ не равен NULL, — но они часто не
принимают дополнительных мер: не проверяют тот же параметр, чтобы удосто$
вериться в наличии достаточного объема памяти для хранения строки максимально
допустимого размера. Профилактическое программирование подразумевает вы$
полнение всех возможных действий, чтобы избежать необходимости применения
отладчика и вместо этого заставить код самостоятельно сообщать о проблемных
участках. Отладчик — одна из самых больших в мире «черных дыр» для времени,
и, чтобы ее избежать, нужны точные сообщения кода о любых отклонениях от
идеала. При вводе любой строки кода остановитесь и подумайте, что вы предпо$
лагаете в хорошем развитии ситуации и как проверить, что именно такое состо$
яние будет при каждом исполнении этой строки кода.
Все просто: ошибки не появляются в коде по волшебству. «Секрет» в том, что
вы и я вносим их при написании кода и эти досадные ошибки могут появляться
из тысяч источников. Они могут стать следствием таких критических проблем,
как недостатки дизайна приложения, или таких простых, как опечатки. Хотя не$

ГЛАВА 3

Отладка при кодировании

73

которые ошибки легко устранить, есть и такие, которых не исправить без серьез$
ных изменений в коде. Хорошо бы взвалить вину за ошибки в вашем коде на грем$
линов, но следует признать, что именно вы и ваши коллеги вносите их туда. (Если
вы читаете эту книгу, значит, в основном в ошибках виноваты ваши коллеги.)
Поскольку вы и другие разработчики отвечаете за ошибки в коде, возникает
проблема поиска путей создания системы проверок и отчетов, позволяющей на$
ходить ошибки в процессе работы. Я всегда называл такой подход «доверяй, но
проверяй» по знаменитой фразе Рональда Рейгана о том, как Соединенные Шта$
ты собираются приводить в жизнь один из договоров об ограничении ядерных
вооружений с бывшим Советским Союзом. Я верю, что мы с моими коллегами будем
использовать код правильно. Однако для предотвращения ошибок я проверяю все:
данные, передаваемые другими в мой код, внутренние операции в коде, любые
допущения, сделанные в моем коде, данные, передаваемые моим кодом наружу,
данные, возвращаемые от вызовов, сделанных в моем коде. Можно хоть что$то
проверить — я проверяю. В столь навязчивой проверке нет ничего личного по
отношению к коллегам, и у меня нет (серьезных) психических проблем. Я про$
сто знаю, откуда появляются ошибки, и знаю, что если вы хотите обнаруживать
ошибки как можно раньше, то ничего нельзя оставлять без проверки.
Прежде чем продолжить, подчеркну один закон моей философии разработки:
ответственность за качество кода целиком лежит на инженерах$разработчиках, а
не на тестировщиках, техническом персонале или менеджерах. Именно мы с вами
пишем, реализуем и исправляем код, так что только мы можем принять значимые
меры, чтобы сделать создаваемый нами код настолько безошибочным, насколько
это возможно.
Одно из самых удивительных мнений, с которыми мне, как консультанту, до$
водилось сталкиваться, заключается в том, что разработчики должны только раз$
рабатывать, а тестировщики — только тестировать. Основная проблема такого
подхода в том, что разработчики пишут большие порции кода и отправляют их
тестировщикам, весьма поверхностно убедившись в правильности работы. Не
говоря уже о том, что ситуации, когда разработчики не ответствечают за тести$
рование кода, приводят к несоблюдению сроков и низкому качеству продукта.
По$моему, разработчик — это тестировщик и разработчик: если разработчик
не тратит хотя бы 40–50% времени разработки на тестирование своего кода, он
не разрабатывает. Обязанность тестировщика — сосредоточиться на таких про$
блемах, как подгонка, тестирование на устойчивость и производительность. Тес$
тировщик крайне редко должен сталкиваться с поиском причин краха. Крах кода
напрямую относится к компетенции инженера$разработчика. Ключ тестирования,
выполняемого разработчиком, — в блочном тестировании (unit test). Ваша зада$
ча — запустить максимально большой фрагмент кода, чтобы убедиться, что он не
приводит к краху и соответствует установленным спецификациям и требовани$
ям. Вооруженные результатами блочного тестирования модулей тестировщики мо$
гут сосредоточиться на проблемах интеграции и общесистемном тестировании.
Мы подробно поговорим о тестировании модулей в разделе «Доверяй, но прове$
ряй (Блочное тестирование)».

74

ЧАСТЬ I

Сущность отладки

Assert, Assert, Assert и еще раз Assert
Надеюсь, большинство из вас уже знает, что такое утверждение (assertion), так как
это самый важный инструмент профилактического программирования в арсена$
ле отладочных средств. Для тех, кто не знаком с этим термином, дам краткое
определение: утверждение объявляет, что в определенной точке программы дол$
жно выполняться некое условие. Если условие не выполняется, говорят, что утвер$
ждение нарушено. Утверждения используются в дополнение к обычной проверке
на ошибки. Традиционно утверждения — это функции или макросы, выполняе$
мые только в отладочных компоновках и отображающие окно с сообщением о
том, что условие не выполнено. Я расширил определение утверждений, включив
туда компилируемый по условию код, проверяющий условия и предположения,
которые слишком сложно обработать в функции или макросе обычного утверж$
дения. Утверждения — ключевой компонент профилактического программиро$
вания, потому что они помогают разработчикам и тестировщикам не только опре$
делить наличие, но и причины возникновения ошибки.
Даже если вы слышали об утверждениях и порой вставляете их в свой код, вы
можете знать их недостаточно, чтобы применять эффективно. Разработчики не
могут быть слишком жирными или слишком худыми — они не могут использо$
вать слишком много утверждений. Метод, которому я всегда следовал, чтобы опре$
делить достаточное количество утверждений, прост: утверждений достаточно, если
мои подчиненные жалуются на появление множества информационных окон о
нарушении утверждений, как только они пытаются вызвать мой код, используя не$
верную информацию или предположения.
Достаточное количество утверждений даст вам основную информацию для
выявления проблем на ранних стадиях. Без утверждений вы потратите массу вре$
мени на отладчик, продвигаясь в обратном направлении от сбоя в поисках того
места, откуда все стало не так. Хорошее утверждение сообщит, где и почему на$
рушены условия. Хорошее утверждение при нарушении условия позволит вам пе$
рейти в отладчик, чтобы вы смогли увидеть полное состояние программы в точ$
ке сбоя. Плохое утверждение скажет, что что$то не так, но не объяснит что, где и
почему.
Побочное преимущество от утверждений в том, что они служат прекрасной
дополнительной документацией к вашему коду. Утверждения отражают ваши на$
мерения. Я уверен, что вы прилагаете массу усилий, чтобы сохранять документа$
цию соответствующей текущему положению, но я уверен и в том, что документа$
ция нескольких проектов испарилась. Хорошие утверждения позволяют сопро$
вождающему разработчику вместо общих условий сбоя точно увидеть, какой ди$
апазон значений параметра вы ожидаете или что, по вашим предположениям, может
пойти не так в ходе нормального исполнения. Утверждения никогда не заменят
точных комментариев, но, используя их для прояснения загадочного «вот что я
имел ввиду, а совсем не то, что написано в документации», вы сэкономите кучу
времени при работе над проектом.

ГЛАВА 3

Отладка при кодировании

75

Как и что утверждать
Мой стандартный ответ на вопрос «что утверждать?» — утверждайте все. Я бы с
удовольствием заявил, что утверждение следует создать для каждой строки кода,
но это нереальная, хоть и прекрасная цель. Следует утверждать каждое условие,
поскольку именно оно может в будущем оказаться решением мерзкой ошибки. Не
переживайте, что внесение слишком большого числа утверждений снизит произ$
водительность программы, — как правило, утверждения активны только в отла$
дочных сборках, а созданные возможности по обнаружению ошибок с лихвой
перевесят небольшую потерю производительности.
В утверждениях не следует менять переменные или состояния программы.
Воспринимайте все данные, которые вы проверяете в утверждениях, как доступ$
ные только для чтения. Поскольку утверждения активны только в отладочных
сборках, если вы изменяете данные, применяя утверждения, отладочные и финаль$
ные сборки будут работать по$разному, и отследить различия будет очень трудно.
В этом разделе я хочу сосредоточиться на том, как использовать утверждения
и что утверждать. Я покажу это на примерах кодов. Замечу, что в этих примерах
Debug.Assert — это утверждение .NET из пространства имен System.Diagnostic, а
ASSERT — встроенный метод C++, который я представлю ниже.

Отладка: фронтовые очерки
Удар по карьере
Боевые действия
Давным$давно я работал в компании, у программного продукта которой были
серьезные проблемы с надежностью. Как старший Windows$инженер это$
го чудовищного проекта, я обнаружил, что многие проблемы возникали от
недостаточного понимания причин сбоев в обращениях к другим модулям.
Я написал служебную записку, в которой советовал то же, что и в этой гла$
ве, рассказав участникам проекта, почему и когда им следовало использо$
вать утверждения. Я обладал некоторыми полномочиями и внес это в кри$
терии оценки кода, чтобы следить за правильным использованием утверж$
дений.
Отправив записку, я ответил на несколько вопросов, возникших у лю$
дей по поводу утверждений, и думал, что все пришло в порядок. Три дня
спустя мой начальник ворвался в мой кабинет и начал вопить, что я всех
подвел, приказав отозвать служебную записку об утверждениях. Я был оше$
ломлен, и у нас начался весьма жаркий спор по поводу данных мною реко$
мендаций. Я не вполне понимал, что пытается сказать мой босс, но это было
как$то связано с тем, что стабильность продукта упала еще сильнее. Пять
минут мы кричали друг на друга, и я вызвался доказать начальнику что люди
использовали утверждения неверно. Он вручил мне распечатку кода, вы$
глядевшую примерно так:

BOOL DoSomeWork ( HMODULE * pModArray , int iCount , LPCTSTR szBuff )
{
ASSERT ( if ( ( pModArray == NULL ) &&
см. след. стр.

76

ЧАСТЬ I

Сущность отладки

( IsBadWritePtr ( pModArray ,
( sizeof ( HMODULE ) * iCount ) ) &&
( iCount != 0 ) &&
( szBuff != NULL ) ) )
{
return ( FALSE ) ;
}
) ;
for ( int i = 0 ; i < iCount ; i++ )
{
pModArray[ i ] = m_pDataMods[ i ] ;
}

}

Исход
Стоит отметить, что мы с боссом не очень$то ладили. Он считал меня зеле$
ным юнцом, не стоящим и не знающим абсолютно ничего, а я его — неве$
жественным тупицей, который без бутылки ни в чем не разберется. По мере
чтения кода мои глаза все больше вылезали из орбит! Человек, писавший
его, абсолютно не понимал предназначения утверждений и просто прохо$
дил код, заключая все обычные процедуры обработки ошибок в утвержде$
ния. Поскольку в финальных сборках утверждения отключаются, человек,
писавший код, полностью удалял проверку на ошибки из финальных сбо$
рок!
К этому моменту я уже побагровел и орал во весь голос: «Того, кто это
написал, нужно уволить! Не могу поверить, что у нас работает такой неве$
роятный и полный @#!&*&$ идиот!» Мой начальник притих, выхватил рас$
печатку из моих рук и тихо сказал: «Это мой код». Ударом по карьере стал
мой истерический смех, понесшийся вдогонку ретирующемуся боссу.

Полученный опыт
Подчеркну: используйте утверждения как дополнение к обычным средствам
обработки ошибок, а не вместо них. Если у вас есть утверждение, то рядом
в коде должна быть какая$то процедура обработки ошибок. Что до моего
босса, то когда несколько недель спустя я пришел к нему в кабинет уволь$
няться, поскольку получил работу в компании получше, он был готов танце$
вать на столе и петь о том, что это был лучший день в его жизни.

Как утверждать
Первое правило: каждый элемент нужно проверять отдельно. Если вы проверяете
несколько условий в одном утверждении, то не сможете узнать, какое именно
вызвало сбой. В следующем примере я демонстрирую одну и ту же функцию с
разными утверждениями. Хотя утверждение в первой функции обнаружит невер$
ный параметр, оно не сможет сообщить, какое условие нарушено или даже какой
из трех параметров неверен.

ГЛАВА 3

Отладка при кодировании

77

// Ошибочный способ написания утверждений. Какой параметр неверен?
BOOL GetPathItem ( int i , LPTSTR szItem , int iLen )
{
ASSERT ( ( i > 0
) &&
( NULL != szItem
) &&
( ( iLen > 0 ) && ( iLen < MAX_PATH )
) &&
( FALSE == IsBadStringPtr ( szItem , iLen ) ) ) ;

}
// Правильный способ. Каждый параметр проверяется отдельно,
// так что вы сможете узнать, какой из них неверный.
BOOL GetPathItem ( int i , LPTSTR szItem , int iLen )
{
ASSERT ( i > 0 ) ;
ASSERT ( NULL != szItem ) ;
ASSERT ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) ;
ASSERT ( FALSE == IsBadStringPtr ( szItem , iLen ) ) ;

}
Утверждая условие, старайтесь проверять его полностью. Например, если в .NET
ваш метод принимает в виде параметра строку и вы ожидаете наличия в ней не$
ких данных, то проверка на null опишет ошибочную ситуацию лишь частично.

// Пример частичной проверки ошибочной ситуации.
bool LookupCustomerName ( string CustomerName )
{
Debug.Assert ( null != CustomerName , "null != CustomerName" ) ;

}
Ее можно описать полностью, добавив проверку на пустую строку.

// Пример полной проверки ошибочной ситуации.
bool LookupCustomerName ( string CustomerName )
{
Debug.Assert ( null != CustomerName , "null != CustomerName" ) ;
Debug.Assert ( 0 != CustomerName.Length ,"\"\" != CustomerName.Length" ) ;

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

// Пример плохо написанного утверждения: nCount должен быть положительным,
// но утверждение не срабатывает, если nCount отрицательный.
void UpdateListEntries ( int nCount )
{
ASSERT ( nCount ) ;

}

78

ЧАСТЬ I

Сущность отладки

// Правильное утверждение, проверяющее необходимое значение в явном виде.
void UpdateListEntries ( int nCount )
{
ASSERT ( nCount > 0 ) ;

}
Неверный пример проверяет только то, что nCount не равен 0, что составляет
лишь половину нужной информации. Утверждения, в которых допустимые зна$
чения проверяются явно, сами себе служат документацией и, кроме того, гаран$
тируют обнаружение неверных данных.

Что утверждать
Теперь мы можем перейти к вопросу о том, что утверждать. Если вы еще не дога$
дались по приведенным до сих пор примерам, позвольте прояснить, что в пер$
вую очередь следует утверждать передающиеся в метод параметры. Утверждение
параметров особенно важно для интерфейсов модулей и методов классов, вызы$
ваемых другими участниками вашей команды. Поскольку эти шлюзовые функции
являются точками входа в ваш код, стоит убедиться в корректности всех параметров
и предположений. В истории «Удар по карьере» я уже обращал ваше внимание на
то, что утверждения ни в коем случае не должны вытеснять обычную обработку
ошибок.
По мере продвижения в глубь модуля, параметры его закрытых методов будут
требовать все меньше проверки в зависимости от места их происхождения. Во
многом решение о том, допустимость каких параметров проверять, сводится к
здравому смыслу. Не вредно проверять каждый параметр каждого метода, однако,
если параметр передается в модуль извне и однажды уже полностью проверялся,
делать это снова не обязательно. Но, утверждая каждый параметр в каждой функ$
ции, вы можете обнаружить внутренние ошибки модуля.
Я нахожусь строго между двумя крайностями. Определение подходящего для
вас количества утверждений параметров потребует некоторого опыта. Получив
представление о том, где в вашем коде обычно возникают проблемы, вы поймете,
где и когда проверять внутренние параметры модуля. Я научился одной предо$
сторожности: добавлять утверждения параметров при каждом нарушении рабо$
ты моего кода из$за плохого параметра. Тогда ошибка не будет повторяться, так
как ее обнаружит утверждение.
Еще одна обязательная для утверждения область — возвращаемые методами
значения, поскольку они сообщают, была ли работа метода успешной. Одна из
самых больших проблем, с которыми я сталкивался, отлаживая код других раз$
работчиков, в том, что они просто вызывают методы, не проверяя возвращаемое
значение. Как часто приходилось искать ошибку лишь затем, чтобы выяснить, что
ранее в коде произошел сбой в каком$то методе, но никто не позаботился прове$
рить возвращаемое им значение! Конечно, к тому времени, как вы обнаружите
нарушителя, ошибка уже проявится, так что через какие$нибудь 20 минут программа
обрушится или повредит данные. Правильно утверждая возвращаемые значения,
вы по крайней мере узнаете о проблеме при ее появлении.

ГЛАВА 3

Отладка при кодировании

79

Напомню: я не выступаю за применение утверждений для каждого возможно$
го сбоя. Некоторые сбои являются ожидаемыми, и вам следует соответствующим
образом их обрабатывать. Инициация утверждения при каждом неудачном поис$
ке в базе данных скорее всего заставит всех отключить утверждения в проекте.
Учтите это и утверждайте возвращаемые значения там, где это важно. Обработка
в программе корректных данных никогда не должна приводить к срабатыванию
утверждения.
И, наконец, я рекомендую использовать утверждения, когда вам нужно прове$
рить предположение. Так, если спецификации класса требуют 3 Мб дискового
пространства, надо проверить это предположение утверждением условной ком$
пиляции внутри данного класса, чтобы убедиться, что вызывающие выполняют свою
часть обязательств. Еще пример: если ваш код должен обращаться к базе данных,
надо проверять, существуют ли в ней необходимые таблицы. Тогда вы сразу узна$
ете, в чем проблема, и не будете недоумевать, почему другие методы класса воз$
вращают такие странные значения.
В обоих предыдущих примерах, как и в большинстве случаев утверждения
предположений, нельзя проверять предположения в общем методе или макросе
утверждения. В таких случаях поможет технология условной компиляции, кото$
рую я упомянул в предыдущем абзаце. Поскольку код, выполняемый в условной
компиляции, работает с «живыми» данными, следует соблюдать особую осторож$
ность, чтобы не изменить состояние программы. Чтобы избежать серьезных про$
блем, которые могут появиться от введения кода с побочными эффектами, я пред$
почитаю, если возможно, реализовывать такие типы утверждений отдельными ме$
тодами. Таким образом вы избежите изменения локальных переменных внутри
исходного метода. Кроме того, компилируемые по условию методы утверждений
могут пригодиться в окне Watch, что вы увидите в главе 5, когда мы будем гово$
рить об отладчике Microsoft Visual Studio .NET. Листинг 3$1 демонстрирует ком$
пилируемый по условию метод, который проверяет существование таблицы до
начала интенсивной работы с данными. Заметьте: этот метод предполагает, что
вы уже передали строку подключения и имеете полный доступ к базе данных.
AssertTableExists подтверждает существование таблицы, чтобы вы могли опираться
на это предположение, не получая странных сообщений о сбоях из глубин ваше$
го кода.

Листинг 3-1.

AssertTableExists проверяет существование таблицы

[Conditional("DEBUG")]
static public void AssertTableExists ( string ConnStr ,
string TableName )
{
SqlConnection Conn = new SqlConnection ( ConnStr ) ;
StringBuilder sBuildCmd = new StringBuilder ( ) ;
sBuildCmd.Append
sBuildCmd.Append
sBuildCmd.Append
sBuildCmd.Append

(
(
(
(

"select * from dbo.sysobjects where " ) ;
"id = object_id('" ) ;
TableName ) ;
"')" ) ;
см. след. стр.

80

ЧАСТЬ I

Сущность отладки

// Выполняем команду.
SqlCommand Cmd = new SqlCommand ( sBuildCmd.ToString ( ) , Conn ) ;
try
{
// Открываем базу данных.
Conn.Open ( ) ;
// Создаем набор данных для заполнения.
DataSet TableSet = new DataSet ( ) ;
// Создаем адаптер данных.
SqlDataAdapter TableDataAdapter = new SqlDataAdapter ( ) ;
// Устанавливаем команду для выборки.
TableDataAdapter.SelectCommand = Cmd ;
// Заполняем набор данных из адаптера.
TableDataAdapter.Fill ( TableSet ) ;
// Если чтонибудь появилось, таблица существует.
if ( 0 == TableSet.Tables[0].Rows.Count )
{
String sMsg = "Table : '" + TableName +
"' does not exist!\r\n" ;
Debug.Assert ( false , sMsg ) ;
}
}
catch ( Exception e )
{
Debug.Assert ( false , e.Message ) ;
}
finally
{
Conn.Close ( ) ;
}
}
Прежде чем описать специфические проблемы различных утверждений для .NET
и машинного кода, хочу показать пример того, как я обрабатываю утверждения.
В листинге 3$2 показана функция StartDebugging отладчика машинного кода из
главы 4. Этот код — точка перехода из одного модуля в другой, так что он демон$
стрирует все утверждения, о которых говорилось в этом разделе. Я выбрал метод
C++, потому что в «родном» C++ всплывает гораздо больше проблем и поэтому надо
утверждать больше условий. Я рассмотрю некоторые проблемы этого примера ниже
в разделе «Утверждения в приложениях C++».

ГЛАВА 3

Листинг 3-2.

Отладка при кодировании

81

Пример исчерпывающего утверждения

HANDLE DEBUGINTERFACE_DLLINTERFACE __stdcall
StartDebugging ( LPCTSTR
szDebuggee
,
LPCTSTR
szCmdLine
,
LPDWORD
lpPID
,
CDebugBaseUser * pUserClass
,
LPHANDLE
lpDebugSyncEvents )
{
// Утверждаем параметры.
ASSERT ( FALSE == IsBadStringPtr ( szDebuggee , MAX_PATH ) ) ;
ASSERT ( FALSE == IsBadStringPtr ( szCmdLine , MAX_PATH ) ) ;
ASSERT ( FALSE == IsBadWritePtr ( lpPID , sizeof ( DWORD ) ) )
ASSERT ( FALSE == IsBadReadPtr ( pUserClass ,
sizeof ( CDebugBaseUser * ) )
ASSERT ( FALSE == IsBadWritePtr ( lpDebugSyncEvents ,
sizeof ( HANDLE ) *
NUM_DEBUGEVENTS ) ) ;
// Проверяем их существование.
if ( ( TRUE == IsBadStringPtr ( szDebuggee , MAX_PATH )
)
( TRUE == IsBadStringPtr ( szCmdLine , MAX_PATH )
)
( TRUE == IsBadWritePtr ( lpPID , sizeof ( DWORD ) ) )
( TRUE == IsBadReadPtr ( pUserClass ,
sizeof ( CDebugBaseUser * ) ) )
( TRUE == IsBadWritePtr ( lpDebugSyncEvents ,
sizeof ( HANDLE ) *
NUM_DEBUGEVENTS )
)
{
SetLastError ( ERROR_INVALID_PARAMETER ) ;
return ( INVALID_HANDLE_VALUE ) ;
}

;
) ;

||
||
||
||

)

// Строка для события стартового подтверждения.
TCHAR szStartAck [ MAX_PATH ] = _T ( "\0" ) ;
// Загружаем строку для стартового подтверждения.
if ( 0 == LoadString ( GetDllHandle ( )
,
IDS_DBGEVENTINIT
,
szStartAck
,
MAX_PATH
) )
{
ASSERT ( !"LoadString IDS_DBGEVENTINIT failed!" ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Описатель стартового подтверждения, которого будет ждать
// эта функция, пока не запустится отладочный поток.
HANDLE hStartAck = NULL ;
// Создаем событие стартового подтверждения.
см. след. стр.

82

ЧАСТЬ I

Сущность отладки

hStartAck = CreateEvent ( NULL ,
TRUE ,
FALSE ,
szStartAck )
ASSERT ( NULL != hStartAck ) ;
if ( NULL == hStartAck )
{
return ( INVALID_HANDLE_VALUE ) ;
}

// Безопасность по умолчанию.
// Событие с ручным сбросом.
// Начальное состояние=Not signaled.
; // Имя события.

// Связываем параметры.
THREADPARAMS stParams ;
stParams.lpPID = lpPID ;
stParams.pUserClass = pUserClass ;
stParams.szDebuggee = szDebuggee ;
stParams.szCmdLine = szCmdLine ;
// Описатель для отладочного потока.
HANDLE hDbgThread = INVALID_HANDLE_VALUE ;
// Пробуем создать поток.
UINT dwTID = 0 ;
hDbgThread = (HANDLE)_beginthreadex ( NULL
0
DebugThread
&stParams
0
&dwTID
ASSERT ( INVALID_HANDLE_VALUE != hDbgThread ) ;
if (INVALID_HANDLE_VALUE == hDbgThread )
{
VERIFY ( CloseHandle ( hStartAck ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}

,
,
,
,
,
) ;

// Ждем, пока отладочный поток не придет в норму и продолжаем.
DWORD dwRet = ::WaitForSingleObject ( hStartAck , INFINITE ) ;
ASSERT (WAIT_OBJECT_0 == dwRet ) ;
if (WAIT_OBJECT_0 != dwRet )
{
VERIFY ( CloseHandle ( hStartAck ) ) ;
VERIFY ( CloseHandle ( hDbgThread ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Избавляемся от описателя подтверждения.
VERIFY ( CloseHandle ( hStartAck ) ) ;
// Проверяем, что отладочный поток еще выполняется. Если это не так,
// отлаживаемое приложение, вероятно, не может запуститься.

ГЛАВА 3

Отладка при кодировании

83

DWORD dwExitCode = ~STILL_ACTIVE ;
if ( FALSE == GetExitCodeThread ( hDbgThread , &dwExitCode ) )
{
ASSERT ( !"GetExitCodeThread failed!" ) ;
VERIFY ( CloseHandle ( hDbgThread ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
ASSERT ( STILL_ACTIVE == dwExitCode ) ;
if ( STILL_ACTIVE != dwExitCode )
{
VERIFY ( CloseHandle ( hDbgThread ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Создаем события синхронизации, чтобы главный поток
// мог сообщить отладочному циклу, что делать.
BOOL bCreateDbgSyncEvts =
CreateDebugSyncEvents ( lpDebugSyncEvents , *lpPID ) ;
ASSERT ( TRUE == bCreateDbgSyncEvts ) ;
if ( FALSE == bCreateDbgSyncEvts )
{
// Это серьезная проблема. Отладочный поток выполняется, но
// я не смог создать события синхронизации, необходимые потоку
// пользовательского интерфейса для управления отладочным потоком.
// Мое единственное мнение — выходить. Я закрою отладочный поток
// и просто выйду. Больше я ничего не могу сделать.
TRACE ( "StartDebugging : CreateDebugSyncEvents failed\n" ) ;
VERIFY ( TerminateThread ( hDbgThread , (DWORD)1 ) ) ;
VERIFY ( CloseHandle ( hDbgThread ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Просто на случай, если ктото изменит функцию
// и не сможет правильно указать возвращаемое значение.
ASSERT ( INVALID_HANDLE_VALUE != hDbgThread ) ;
// Жизнь прекрасна!
return ( hDbgThread ) ;
}

Утверждения в .NET Windows Forms
или консольных приложениях
Перед тем как перейти к мелким подробностям утверждений .NET, хочу отметить
одну ключевую ошибку, которую я встречал практически во всех кодах .NET, осо$
бенно во многих примерах, из которых разработчики берут код для создания своих
приложений. Все забывают, что можно передать в объектном параметре значе$
ние null. Даже когда разработчики используют утверждения, код выглядит при$
мерно так:

84

ЧАСТЬ I

Сущность отладки

void DoSomeWork ( string TheName )
{
Debug.Assert ( TheName.Length > 0 ) ;

Если TheName имеет значение null, то вместо срабатывания утверждения вызов
свойства Length приводит к исключению System.NullReferenceException, тут же об$
рушивая ваше приложение. Это тот ужасный случай, когда утверждение вызывает
нежелательный побочный эффект, нарушая основное правило утверждений. И,
разумеется, отсюда следует, что если разработчики не проверяют наличие пустых
объектов в утверждениях, то не делают этого и при обычной проверке парамет$
ров. Окажите себе огромную услугу: начните проверять объекты на null.
То, что приложения .NET не должны заботиться об указателях и блоках памя$
ти означает, что по крайней мере 60% утверждений, использовавшихся нами в дни
C++, ушли в прошлое. В сфере утверждений команда .NET добавила в простран$
ство имен System.Diagnostic два объекта — Debug и Trace, активных, только если в
компиляции приложения вы определили DEBUG или TRACE соответственно. Оба эти
определения могут быть указаны в диалоговом окне Property Pages проекта. Как
вы видели, метод Assert обрабатывает утверждения в .NET. Довольно интересно,
что и Debug и Trace обладают похожими методами, включая Assert. Мне кажется,
что наличие двух возможных утверждений, компилирующихся по разным усло$
виям, может сбить с толку. Следовательно, поскольку утверждения должны быть
активны только в отладочных сборках, для утверждений я использую только
Debug.Assert. Это позволяет избежать сюрпризов от конечных пользователей, зво$
нящих мне с вопросами о странных диалоговых окнах или сообщениях о том, что
что$то пошло не так. Я настоятельно рекомендую вам делать то же самое, внося
свой вклад в целостность мира утверждений.
Есть три перегруженных метода Assert. Все они принимают значение булев$
ского типа в качестве первого или единственного параметра, и, если оно равно
false, инициируется утверждение. Как видно из предыдущих примеров, где я ис$
пользовал Debug.Assert, один из методов принимает второй параметр типа string,
который отображается в выдаваемом сообщении. Последний перегруженный метод
Assert принимает третий параметр типа string, предоставляющий еще больше дан$
ных при срабатывании утверждения. По моему опыту случай с двумя параметра$
ми — самый простой для использования, так как я просто копирую условие, про$
веряемое в первом параметре, и вставляю его как строку. Конечно, теперь, когда
нужное в утверждении условное выражение находится в кавычках, проверяя пра$
вильность кода, следует контролировать, чтобы строковое значение всегда совпа$
дало с реальным условием. Следующий код демонстрирует все три метода Assert
в действии.

Debug.Assert ( i > 3 )
Debug.Assert ( i > 3 , "i > 3" )
Debug.Assert ( i > 3 , "i > 3" , "This means I got a bad parameter")
Объект Debug в .NET интересен тем, что позволяет представлять результат раз$
ными способами. Исходящая информация от объекта Debug (и соответственно
объекта Trace) проходит через другой объект — TraceListener. Классы$потомки

ГЛАВА 3

Отладка при кодировании

85

TraceListener добавляются в свойство объекта Debug — набор Listener. Прелесть такого
подхода в том, что при каждом нарушении утверждения объект Debug перебирает
набор Listener и по очереди вызывает каждый объект TraceListener. Благодаря этой
удобной функциональности даже при появлении новых усовершенствованных
способов уведомления для утверждений вам не придется вносить серьезных из$
менений в код, чтобы задействовать их преимущества. Более того, в следующем
разделе я покажу, как добавить новые объекты TraceListener, вообще не изменяя
код, что обеспечивает превосходную расширяемость!
Используемый по умолчанию объект TraceListener называется DefaultTraceListener.
Он направляет исходящую информацию в два разных места, самым заметным из
которых является диалоговое окно утверждения (рис. 3$1). Как видите, большая
его часть занята информацией из стека и типами параметров. Также указаны ис$
точник и строка для каждого элемента. В верхних строках окна выводятся стро$
ковые значения, переданные вами в Debug.Assert. На рис. 3$1 я в качестве второго
параметра передал в Debug.Assert строку «Debug.Assert assertion».
Результат нажатия каждой кнопки описан в строке заголовка информацион$
ного окна. Единственная интересная клавиша — Retry. Если вы исполняете код в
отладчике, вы просто переходите в отладчик на строку, следующую за утвержде$
нием. Если вы не в отладчике, щелчок Retry инициирует специальное исключе$
ние и запускает селектор отладчика по требованию, позволяющий выбрать заре$
гистрированный отладчик для отладки утверждения.
В дополнение к выводу в информационном окне Debug.Assert также направля$
ет всю исходящую информацию через OutputDebugString, поэтому ее получает под$
ключенный отладчик. Эта информация предоставляется в схожем формате, кото$
рый показан в следующем коде. Поскольку DefaultTraceListener выполняет вывод
через OutputDebugString, вы можете воспользоваться прекрасной программой Марка
Руссиновича (Mark Russinovich) DebugView (www.sysinternals.com), чтобы просмот$
реть его, не находясь в отладчике. Ниже я расскажу об этом подробнее.

—— DEBUG ASSERTION FAILED ——
—— Assert Short Message ——
Debug.Assert assertion
—— Assert Long Message ——

at
at
at
at
at
at
at
at

HappyAppy.Fum() d:\asserterexample\asserter.cs(15)
HappyAppy.Fo(StringBuilder sb) d:\asserterexample\asserter.cs(20)
HappyAppy.Fi(IntPtr p) d:\asserterexample\asserter.cs(24)
HappyAppy.Fee(String Blah) d:\asserterexample\asserter.cs(29)
HappyAppy.Baz(Double d) d:\asserterexample\asserter.cs(34)
HappyAppy.Bar(Object o) d:\asserterexample\asserter.cs(39)
HappyAppy.Foo(Int32 i) d:\asserterexample\asserter.cs(46)
HappyAppy.Main() d:\\asserterexample\asserter.cs(76)

86

Рис. 31.

ЧАСТЬ I

Сущность отладки

Информационное окно DefaultTraceListener

Обладая информацией, предоставляемой Debug.Assert, вы никогда больше не
будете раздумывать, почему сработало утверждение! .NET Framework также пре$
доставляет два других объекта TraceListener. Для записи исходящей информации
в текстовый файл используйте класс TextWriterTraceListener, а для записи ее в журнал
событий — класс EventLogTraceListener. К сожалению, классы TextWriterTraceListener
и EventLogTraceListener практически бесполезны, потому что записывают только
поля сообщений ваших утверждений и не включают информацию о стеке. Хоро$
шая новость в том, что реализовать собственные объекты TraceListener неслож$
но, поэтому в рамках BugslayerUtil.NET.DLL я пошел дальше и написал для вас ис$
правленные версии TextWriterTraceListener и EventLogTraceListener: Bugslayer
TextWriterTraceListener и BugslayerEventLogTraceListener соответственно.
И BugslayerTextWriterTraceListener, и BugslayerEventLogTraceListener — вполне
заурядные классы. BugslayerTextWriterTraceListener наследует напрямую от TextWri
terTraceListener, и все, что он делает, — переопределяет метод Fail, который
Debug.Assert вызывает для вывода информации. Помните, что при использовании
BugslayerTextWriterTraceListener или TextWriterTraceListener соответствующий тек$
стовый файл с исходящей информацией не сбрасывается на диск, если не задать
true атрибуту autoflush элемента trace в конфигурационном файле приложения,
не вызвать явно Close для потока или файла или не задать Debug.AutoFlush значе$
ние true, чтобы каждая запись автоматически вызывала сброс на диск. По каким$
то причинам класс EventLogTraceListener является закрытым, поэтому я не мог на$
следовать от него напрямую и создал потомок прямо от абстрактного класса
TraceListener. Однако я все$таки получил информацию о стеке весьма интересным
способом. Как показано ниже, стандартный класс StackTrace, предоставляемый .NET,
позволяет в любой момент легко получить информацию о стеке.

StackTrace StkTrc = new StackTrace ( ) ;
В сравнении с действиями, которые надо было выполнять в машинном коде,
чтобы получить такую информацию, способ, предоставляемый .NET, служит пре$
красным примером того, как .NET облегчает вашу жизнь. StackTrace возвращает
набор объектов StackFrame, представляющих стек. Просмотрев документацию на
StackFrame, вы увидите, что в нем есть все виды интересных методов для получе$
ния строки и номера источника. Объект StackTrace содержит метод ToString, и я
был абсолютно уверен, что через него как$то можно добавлять источник и стро$
ку в итоговую информацию о стеке. Увы, я ошибался. Поэтому мне пришлось 30

ГЛАВА 3

Отладка при кодировании

87

минут писать и тестировать класс BugslayerStackTrace, наследующий от StackTrace
и переопределяющий ToString, чтобы иметь возможность добавить информацию
об источнике и строке к каждому методу. В листинге 3$3 показаны два метода из
BugslayerStackTrace, выполняющие эти действия.

Листинг 3-3. BugslayerStackTrace, собирающий полную информацию о стеке,
в том числе сведения об источнике и строке
///
/// Создает читаемое представление информации о стеке.
///
///
/// Читаемое представление информации о стеке.
///
public override string ToString ( )
{
// Обновляем StringBuilder для хранения всего необходимого.
StringBuilder StrBld = new StringBuilder ( ) ;
// Первое, что надо внести, — перевод строки.
StrBld.Append ( DefaultLineEnd ) ;
// Зациклить и сделать! Здесь нельзя использовать foreach,
// так как StackTrace не наследует от IEnumerable.
for ( int i = 0 ; i < FrameCount ; i++ )
{
StackFrame StkFrame = GetFrame ( i ) ;
if ( null != StkFrame )
{
BuildFrameInfo ( StrBld , StkFrame ) ;
}
}
return ( StrBld.ToString ( ) ) ;
}
/*/////////////////////////////////////////////////////////////////
// Закрытые методы
/////////////////////////////////////////////////////////////////*/
///
/// Выполняет мелкую работу по преобразованию фрейма
/// в строку и внесению его в StringBuilder.
///
///
/// StringBuilder для внесения результатов.
///
///
/// Фрейм стека для преобразования.
///
private void BuildFrameInfo ( StringBuilder StrBld ,
см. след. стр.

88

ЧАСТЬ I

Сущность отладки

StackFrame

StkFrame )

{
// Получаем метод через механизм отражения.
MethodBase Meth = StkFrame.GetMethod ( ) ;
// Если ничего не получили, выходим отсюда.
if ( null == Meth )
{
return ;
}
// Присваиваем метод.
String StrMethName = Meth.ReflectedType.Name ;
// Вносим отступ функции (function indent), если он есть.
if ( null != FunctionIndent )
{
StrBld.Append ( FunctionIndent ) ;
}
// Получаем тип
StrBld.Append (
StrBld.Append (
StrBld.Append (
StrBld.Append (

и имя класса.
StrMethName ) ;
"." ) ;
Meth.Name ) ;
"(" ) ;

// Вносим параметры, включая все их имена.
ParameterInfo[] Params = Meth.GetParameters ( ) ;
for ( int i = 0 ; i < Params.Length ; i++ )
{
ParameterInfo CurrParam = Params[ i ] ;
StrBld.Append ( CurrParam.ParameterType.Name ) ;
StrBld.Append ( " " ) ;
StrBld.Append ( CurrParam.Name ) ;
if ( i != ( Params.Length  1 ) )
{
StrBld.Append ( ", " ) ;
}
}
// Закрываем список параметров.
StrBld.Append ( ")" ) ;
// Получаем источник и строку, только
if ( null != StkFrame.GetFileName ( )
{
// Мне надо определять источник?
// вставить в конце разрыв строки
if ( null != SourceIndentString )
{

если они есть.
)
Если да, то нужно
и отступ.

ГЛАВА 3

Отладка при кодировании

89

StrBld.Append ( LineEnd ) ;
StrBld.Append ( SourceIndentString ) ;
}
else
{
// Просто добавляем пробел.
StrBld.Append ( ' ' ) ;
}
// Здесь получаем имя файла и строку с проблемой.
StrBld.Append ( StkFrame.GetFileName ( ) ) ;
StrBld.Append ( "(" ) ;
StrBld.Append ( StkFrame.GetFileLineNumber().ToString());
StrBld.Append ( ")" ) ;
}
// Всегда добавляйте перевод строки.
StrBld.Append ( LineEnd ) ;
}
Теперь, когда у вас есть другие классы TraceListener, которые стоит добавить в
набор Listeners, мы в коде можем добавлять и удалять объекты TraceListener. Как
и в любом наборе .NET, чтобы добавить объект в набор, вызовите метод Add, а чтобы
избавиться от объекта — метод Remove. Стандартный TraceListener называется
«Default». Вот как добавить BugslayerTextWriterTraceListener и удалить Default
TraceListener:

Stream AssertFile = File.Create ( "BSUNBTWTLTest.txt" ) ;
BugslayerTextWriterTraceListener tListener =
new BugslayerTextWriterTraceListener ( AssertFile ) ;
Debug.Listeners.Add ( tListener ) ;
Debug.Listeners.Remove ( "Default" ) ;

Управление объектом TraceListener через файлы конфигурации
Если вы разрабатываете консольные приложения и приложения Windows Forms,
то по большей части DefaultTraceListener должен удовлетворить все ваши потреб$
ности. Однако появляющееся время от времени информационное окно может
нарушить работу любых автоматизированных тестов. Или, может быть, вы исполь$
зуете компонент сторонних производителей в службе Win32, и его отладочная
сборка правильно использует Debug.Assert. В обоих случаях вам потребуется от$
ключить информационное окно, вызываемое DefaultTraceListener. Можно добавить
код для удаления объекта DefaultTraceListener, но его можно удалить и не прика$
саясь к коду.
Любому двоичному коду .NET может быть сопоставлен внешний конфигура$
ционный файл XML. Этот файл располагается в том же каталоге, что и двоичный
файл, и имеет такое же имя с добавленным в конце словом .CONFIG. Например,
конфигурационный файл для FOO.EXE называется FOO.EXE.CONFIG. Можно лег$

90

ЧАСТЬ I

Сущность отладки

ко добавить конфигурационный файл к проекту, добавив новый XML$файл с именем
APP.CONFIG. Этот файл будет автоматически скопирован в каталог конечных фай$
лов и назван в соответствии с именем двоичного файла.
Элемент assert, расположенный внутри system.diagnostics в конфигурацион$
ном файле XML, имеет два атрибута. Если задать false первому атрибуту — assertuie
nabled, .NET не будет отображать информационные окна, но исходящая инфор$
мация по$прежнему будет направляться через OutputDebugString. Второй атрибут —
logfilename — позволяет указать файл, в который следует записывать любой вы$
вод утверждений. Интересно что при указании файла в атрибуте logfilename, в этом
файле также появятся все операторы трассировки, о которых я расскажу ниже.
В следующем отрывке показан минимальный конфигурационный файл. Он демон$
стрирует, как просто отключить информационные окна утверждений. Не забудь$
те: главный конфигурационный файл MACHINE.CONFIG включает такие же пара$
метры, что и обычные конфигурационные файлы, так что с их помощью вы вправе
отключить информационные окна на всей машине.

Как я уже отмечал, можно добавлять и удалять приемники информации (liste$
ners), не затрагивая код, и, как вы, вероятно, догадались, это как$то связано с кон$
фигурационным файлом. В документации он выглядит вполне очевидным, но на
момент написания этой книги документация содержала ошибки. Экспериментально
я выявил все нужные приемы для корректного управления приемниками без из$
менений кода.
Все действия выполняются над элементом trace конфигурационного файла. Этот
элемент содержит один очень важный необязательный атрибут, которому всегда
следует задавать true, — autoflush. Сделав так, вы предписываете сбрасывать ис$
ходящий буфер на диск при каждой операции записи. В противном случае вам
придется добавлять в код вызовы для сброса информации.
Внутри trace содержится элемент listener, через который добавляются и уда$
ляются объекты TraceListener. Удалить объект TraceListener очень просто. Укажи$
те элемент remove и задайте его атрибуту name строковое имя нужного объекта
TraceListener. Ниже приведен полный конфигурационный файл, удаляющий Default
TraceListener.

ГЛАВА 3

Отладка при кодировании

91

Элемент add содержит два необходимых атрибута: name представляет строку,
определяющую имя объекта TraceListener в том виде, в котором оно помещается
в свойство TraceListener.Name, а type вызывает замешательство, и я объясню поче$
му. В документации показано только добавление типа, находящегося в глобаль$
ном кэше сборок (GAC), и сказано, что добавление собственного приемника го$
раздо сложнее, чем нужно. Один необязательный атрибут — initializeData — пред$
ставляет строку, передаваемую конструктору объекта TraceListener.
Чтобы добавить объект TraceListener из GAC, в элементе type надо только пол$
ностью указать класс объекта TraceListener. Согласно документации для добавле$
ния объекта TraceListener, не находящегося в GAC, вам придется иметь дело со всей
атрибутикой вроде региональных параметров (culture) и маркеров открытых клю$
чей (public key tokens). К счастью, все, что нужно сделать, — это просто указать
полностью класс, добавить запятую и имя сборки. Во избежание инициации ис$
ключения System.Configuration.ConfigurationException не добавляйте запятую и имя
класса. Вот как правильно добавить глобальный класс TextWriterTraceListener:

Чтобы добавить объекты TraceListener, не находящиеся в GAC, надо разместить
сборку, содержащую потомки класса TraceListener, в одном каталоге с двоичным
файлом. Испробовав все комбинации путей и параметров конфигурации, я выяс$
нил, что включить сборку из другого каталога через конфигурационный файл
нельзя. Добавляя потомок класса TraceListener, поставьте запятую и имя сборки.
Вот как добавить BugslayerTextWriterTraceListener из BugslayerUtil.NET.DLL:

92

ЧАСТЬ I

Сущность отладки

Утверждения в приложениях ASP.NET и Web-сервисах XML
Я действительно рад видеть платформу для разработки, в которую изначально
заложены идеи по обработке утверждений. Пространство имен System.Diagnostics
содержит все эти полезные классы, квинтэссенция которых — Debug. Как и боль$
шинство из вас, я начал изучать .NET с создания консольных приложений и при$
ложений Windows Forms, поскольку в то время они проще всего уживались в моей
голове. Когда я перешел к ASP.NET, я уже использовал Debug.Assert и подумал, что
Microsoft правильно поступила, избавившись от информационных окон. Безуслов$
но, они поняли, что при работе в ASP.NET мне потребуется возможность при сра$
батывании утверждения перейти в отладчик. Представьте мое удивление, когда я
инициировал утверждение и ничего не прекратилось! Я увидел обычный вывод
утверждения в окне Output отладчика, но не увидел вызовов OutputDebugString с
информацией об утверждении. Поскольку Web$сервисы XML в .NET по существу
являются приложениями ASP.NET без пользовательского интерфейса, я проделал
то же самое с Web$сервисом и получил те же результаты. (Далее в этом разделе в
термине ASP.NET я буду совмещать ASP.NET и Web$сервисы XML .) Поразительно!
Это означало, что в ASP.NET нет настоящих утверждений! А без них можно и не
программировать! Единственная хорошая новость в том, что в приложениях ASP.NET
DefaultTraceListener не отображает обычное информационное окно.
Без утверждений я чувствовал себя голым и знал, что с этим надо что$то де$
лать. Подумав, не создать ли новый объект для утверждений, я решил, что правиль$
нее всего будет держаться Debug.Assert как единственного способа обработки утвер$
ждений. Это позволяло мне решить сразу несколько ключевых проблем. Первая
заключалась в наличии единого способа работы с утверждениями для всей плат$
формы .NET — я совсем не хотел беспокоиться о том, будет ли код запущен в
Windows Forms или ASP.NET, и применять неверные утверждения. Вторая пробле$
ма касалась библиотек сторонних производителей, в которых имеется Debug.Assert:
как их использавать, чтобы их утверждения появлялись в том же месте, где и все
другие.
Третья проблема состояла в том, чтобы сделать обращение к библиотеке утвер$
ждений максимально безболезненным. Написав массу утилит, я понял важность
легкой интеграции библиотеки утверждений в приложение. Последняя пробле$
ма, которую я хотел решить, заключалась в наличии серверного элемента управ$
ления, позволяющего легко видеть утверждения на странице. Весь код находится
в BugslayerUtil.NET.DLL, так что вы можете открыть этот проект с тестовой про$
граммой BSUNAssertTest, расположенной в подкаталоге Test каталога Bugslayer$
Util.NET. Прежде чем открыть проект, не забудьте создать виртуальный каталог в
Microsoft Internet Information Services (IIS), ссылающийся на каталог BSUNAssertTest.
Проблемы, которые я хотел решить, указывали на создание специального класса,
наследуемого от TraceListener. Через секунду я расскажу об этом коде, но незави$
симо от того, насколько классным получился бы TraceListener, мне нужен был способ
подключить свой объект TraceListener и удалить DefaultTraceListener. Как бы там
ни было, это требовало изменений в коде с вашей стороны, потому что мне нуж$
но выполнить некоторый код. Чтобы упростить применение утверждений и обес$
печить максимально ранний вызов библиотеки утверждений, я использовал класс,
наследуемый от System.Web.HttpApplication, так как его конструктор и метод Init

ГЛАВА 3

Отладка при кодировании

93

вызываются в приложении ASP.NET в первую очередь. Первым шагом на пути к
нирване утверждений является наследование от вашего класса Global из Glo$
bal.ASAX.cs (или Global.ASAX.vb) с использованием моего класса AssertHttpApplication.
Это позволит правильно подключить мой ASPTraceListener и поместить в ссылку
на него в отделе состояния приложения в разделе «ASPTraceListener», так что вы
сможете в ходе работы изменять параметры вывода. Если все, что вам нужно в при$
ложении, — это возможность остановить его при срабатывании утверждения, то
больше от вас ничего не потребуется.
Для вывода утверждений на страницу я написал очень простой элемент управ$
ления, который вполне логично называется AssertControl. Чтобы добавить его на
панель инструментов, щелкните правой кнопкой вкладку Web Forms и выберите
из контекстного меню команду Add/Remove Items. В диалоговом окне Customize
Toolbox перейдите на вкладку .NET, щелкните кнопку Browse и в окне File Open
перейдите к BugslayerUtil.NET.DLL. Теперь вы можете просто перетаскивать Assert$
Control на любую страницу, в которой вам потребуются утверждения. Вам не при$
дется прописывать элемент управления в вашем коде, потому что класс ASPTrace
Listener обнаружит его на странице и создаст соответствующий вывод. AssertControl
будет найден, даже если он вложен в другой элемент управления. Если при обра$
ботке страницы на сервере ни одно утверждение не инициировалось, AssertControl
не выводит ничего. Иначе он отображает те же сообщения утверждений и инфор$
мацию о стеке, что выводятся в Windows$ или консольных приложениях. Поскольку
на странице могут инициироваться несколько утверждений, AssertControl отобра$
жает их все. На рис. 3$2 показана страница BSUNAssertTest после инициации ут$
верждения. Текст в нижней части страницы — это вывод AssertControl.
Вся работа выполняется в классе ASPTraceListener, большая часть которого пред$
ставлена в листинге 3$4. Чтобы объединить в себе все необходимое, ASPTraceListener
включает несколько свойств, позволяющих перенаправлять и изменять вывод в
процессе работы (табл. 3$1).

Табл. 3-1. Свойства вывода и управления ASPTraceListener
Свойство

Значение по умолчанию

Описание

ShowDebugLog

true

Показывает вывод в подключенном
отладчике.

ShowOutputDebugString

false

Показывает вывод через
OutputDebugString.

EventSource

null/Nothing

Имя источника события для записи
вывода в журнал событий. Внутри
BugslayerUtil.NET.DLL не получаются
разрешения и не выполняются про$
верки безопасности для доступа
к журналу событий. Перед установ$
кой EventSource вам придется запро$
сить разрешения.

Writer

null/Nothing

Объект TextWriter для записи вывода
в файл.

LaunchDebuggerOnAssert

true

Если подключен отладчик, он сразу
останавливает выполнение при
инициации утверждения.

94

ЧАСТЬ I

Сущность отладки

Вывод
AssertControl

Рис. 32. Приложение ASP.NET, отображающее утверждение
через AssertControl
Всю работу по выводу информации утверждения, которая включает поиск эле$
ментов управления утверждений на странице, выполняет метод ASPTraceListener.Hand
leOutput, показанный в листинге 3$4. Моя первая попытка создания метода Handle
Output была гораздо запутаннее. Я мог получить текущий IHttpHandler для текуще$
го HTTP$запроса из статического свойства HttpContext.Current.Handler, но не на$
шел способа определить, являлся ли обработчик реальной System.Web.UI.Page. Если
бы я смог выяснить, что это страница, я мог бы легко идти дальше и найти эле$
менты управления утверждений на странице. Моя первая попытка заключалась в
написании кода с использованием интерфейсов отражения, чтобы я смог сам
просматривать цепи наследования. Когда я заканчивал примерно пятисотую строку
кода, Джефф Просиз (Jeff Prosise) невинно поинтересовался, не слышал ли я про
оператор is, который определяет совместимость типа объекта, существующего в
период выполнения, с заданным типом. Создание функциональности моего соб$
ственного оператора is стало интересным упражнением, но мне надо было со$
всем другое.
Получив объект Page, я начал искать на странице AssertControl. Я знал, что он
мог заключаться в другом элементе управления, поэтому задействовал небольшую
рекурсию для полного просмотра. Разумеется, при этом надо было убедиться в на$
личии вырождающегося цикла, иначе я легко мог закончить зацикливанием.

ГЛАВА 3

Отладка при кодировании

95

В ASPTraceListener.FindAssertControl я решил задействовать преимущество ключе$
вого слова out, которое позволяет передавать параметр метода ссылкой, но не тре$
бует его инициализации. Логичнее рассматривать ненайденный элемент управ$
ления как null, и ключевое слово out позволяло это сделать.
Последнее, что я делаю с утверждением в методе ASPTraceListener.HandleOutput, —
определяю, переходить ли при инициации утверждения в отладчик. Прекрасный
объект System.Diagnostics.Debugger позволяет общаться с отладчиком из вашего кода.
Если в последнем идет отладка кода, свойство Debugger.IsAttached будет иметь зна$
чение true, и, просто вызвав Debugger.Break, вы можете имитировать точку преры$
вания в отладчике. Конечно, такое решение предполагает, что вы отлаживаете этот
конкретный Web$сайт. Мне еще нужно предусмотреть случай вызова отладчика,
когда вы работаете не из него.
В классе Debugger содержится замечательный метод Launch, позволяющий запу$
стить отладчик и подключить его к вашему процессу. Однако, если учетная запись
пользователя, под которой выполняется процесс, не находится в группе Debugger
Users, Debugger.Launch не сработает. Если нужно подключать отладчик из кода ут$
верждения, когда отладчик не запущен, придется получить учетную запись для
работы ASP.NET, находящуюся в группе Debugger Users. Прежде чем продолжить,
должен сказать, что, разрешая ASP.NET вызывать отладчик, вы потенциально со$
здаете угрозу безопасности, поэтому делайте это только на отладочных машинах,
не подключенных к Интернету.
ASP.NET в Windows 2000 и XP работает под учетной записью ASPNET, так что
именно ее надо добавить в группу Debugger Users. Добавив учетную запись, пере$
запустите IIS, чтобы Debugger.Launch отобразил диалог Just$In$Time (JIT) Debugging.
В Windows Server 2003 ASP.NET работает под учетной записью NETWORK SERVICE.
Добавив NETWORK SERVICE в группу Debugger Users, перезагрузите машину.
Обеспечив работу Debugger.Launch настройкой параметров безопасности, я должен
был убедиться, что Debugger.Launch будет вызываться только при подходящих усло$
виях. Вызов Debugger.Launch, когда в систему сервера никто не вошел, привел бы к
большим проблемам, потому что отладчик по требованию мог бы ждать нажатия
клавиши в окне, до которого никто не смог бы добраться! В классе ASPTraceListener
мне следовало убедиться, что HTTP$запрос производится с локальной машины,
потому что это указывает на то, что кто$то вошел в систему и отлаживает утвер$
ждение. Метод ASPTraceListener.IsRequestFromLocalMachine проверяет, не является ли
127.0.0.1 адресом хоста или не равна ли серверная переменная LOCAL_ADDR адресу
хоста пользователя.
Последнее замечание по поводу вызова отладчика касается Terminal Services.
Если у вас открыто окно Remote Desktop Connection с подключением к серверу,
Web$адрес для любых запросов к серверу, как и следует ожидать, будет представ$
ляться в виде IP$адреса сервера. По умолчанию мой код утверждения при совпа$
дении адреса запроса с адресом сервера вызывает Debugger.Launch. Тестируя при$
ложение ASP.NET и запустив с помощью Remote Desktop браузер на сервере, я
получил сильный шок при срабатывании утверждения. (Помните, что я не отла$
живал процесс ни на одной машине.)
Я ожидал увидеть информационное окно с предупреждением о нарушении
правил безопасности или диалоговое окно JIT Debugger, но увидел лишь завис$

96

ЧАСТЬ I

Сущность отладки

ший браузер. Я был здорово растерян, пока не подошел к серверу и не подвигал
мышь. Там на фоне экрана регистрации находилось мое информационное окно!
Мне стало ясно, что, хотя это выглядело как ошибка, все было объяснимо. Поскольку
информационное окно или диалог JIT Debugger вызываются из$под учетной за$
писи ASPNET/NETWORK SERVICE, ASP.NET не знает, что подключение осуществ$
лялось через сеанс Terminal Services. Эти учетные записи не могут отслеживать,
из какого сеанса был вызван Debugger.Launch. Соответственно вывод направлялся
только на реальный экран компьютера.
Хорошая новость в том, что если вы подключили отладчик, то независимо от
того, сделали вы это в окне Remote Desktop Connection или на другой машине,
вызов Debugger.Launch работает точно так, как должен, и прерывает выполнение,
переходя в отладчик. Кроме того, если вы направили вызов серверу из браузера
на другой машине, то вызов Debugger.Launch не остановит выполнение. Мораль: если
для подключения к серверу вы собираетесь использовать Remote Desktop Connection
и запустить браузер внутри этого окна (скажем, на сервере), вам следует подклю$
чить отладчик к процессу ASP.NET на этом сервере.
То, что Microsoft не предусмотрела утверждения в ASP.NET, непростительно, но,
вооружившись хотя бы AssertControl, вы можете начать программировать. Если вы
ищете элемент управления, чтобы научиться писать к ним расширения, AssertControl
может послужить экспериментальным скелетом. Интересным расширением Assert$
Control могло бы стать использование в коде JavaScript для создания улучшенно$
го UI вроде диалогового окна Web, чтобы сообщать пользователям о возникших
проблемах.

Листинг 3-4.

Важные методы ASPTraceListener

public class ASPTraceListener : TraceListener
{
/* КОД УДАЛЕН ДЛЯ КРАТКОСТИ * /
// Метод, вызываемый при нарушении утверждения.
public override void Fail ( String Message
,
String DetailMessage )
{
// По независящим от меня причинам практически невозможно
// всегда знать число элементов в стеке для Debug.Assert.
// Иногда их 4, иногда — 5. Увы, единственный способ, которым
// я могу решить эту проблему, — выяснить вручную. Лентяй.
StackTrace StkSheez = new StackTrace ( ) ;
int i = 0 ;
for ( ; i < StkSheez.FrameCount ; i++ )
{
MethodBase Meth = StkSheez.GetFrame(i).GetMethod ( ) ;
// Если ничего не получили, выходим отсюда.
if ( null != Meth )
{
if ( "Debug" == Meth.ReflectedType.Name )

ГЛАВА 3

Отладка при кодировании

97

{
i++ ;
break ;
}
}
}
BugslayerStackTrace Stk = new BugslayerStackTrace ( i ) ;
HandleOutput ( Message , DetailMessage , Stk ) ;
}
/* КОД УДАЛЕН ДЛЯ КРАТКОСТИ * /
///
/// Закрытый заголовок сообщения об утверждении.
///
private const String AssertionMsg = "ASSERTION FAILURE!\r\n" ;
///
/// Закрытая строка с переводом каретки и возвратом строки.
///
private const String CrLf = "\r\n" ;
///
/// Закрытая строка с разделителем.
///
private const String Border =
"————————————————————\r\n" ;
///
/// Выводит утверждение или сообщение трассировки.
///
///
/// Обрабатывает весь вывод утверждения или трассировки.
///
///
/// Отображаемое сообщение.
///
///
/// Отображаемый подробный комментарий.
///
///
/// Значение, содержащее информацию о стеке для утверждения.
/// Если не равно null, эта функция вызвана из утверждения.
/// Вывод трассировки устанавливает этот параметр в null.
///
protected void HandleOutput ( String
Message
,
String
DetailMessage ,
BugslayerStackTrace Stk
)
{
// Создаем StringBuilder для помощи в создании
// текстовой строки для вывода.
см. след. стр.

98

ЧАСТЬ I

Сущность отладки

StringBuilder StrOut = new StringBuilder ( ) ;
// Если StackArray не null, это утверждение.
if ( null != Stk )
{
StrOut.Append ( Border ) ;
StrOut.Append ( AssertionMsg ) ;
StrOut.Append ( Border ) ;
}
// Присоединяем сообщение.
StrOut.Append ( Message ) ;
StrOut.Append ( CrLf ) ;
// Присоединяем подробное сообщение, если оно есть.
if ( null != DetailMessage )
{
StrOut.Append ( DetailMessage ) ;
StrOut.Append ( CrLf ) ;
}
// Если это утверждение, показываем стек под разделителем.
if ( null != Stk )
{
StrOut.Append ( Border ) ;
}
// Просматриваем и присоединяем
// всю имеющуюся информацию о стеке.
if ( null != Stk )
{
Stk.SourceIndentString = "
" ;
Stk.FunctionIndent = " " ;
StrOut.Append ( Stk.ToString ( ) ) ;
}
// Поскольку в нескольких местах
// мне понадобится строка, создаем ее.
String FinalString = StrOut.ToString ( ) ;
if ( ( true == m_ShowDebugLog
) &&
( true == Debugger.IsLogging ( ) )
)
{
Debugger.Log ( 0 , null , FinalString ) ;
}
if ( true == m_ShowOutputDebugString )
{
OutputDebugStringA ( FinalString ) ;
}
if ( null != m_EvtLog )

ГЛАВА 3

Отладка при кодировании

99

{
m_EvtLog.WriteEntry ( FinalString ,
System.Diagnostics.EventLogEntryType.Error ) ;
}
if ( null != m_Writer )
{
m_Writer.WriteLine ( FinalString ) ;
// ДОбавляем CRLF, просто на всякий случай.
m_Writer.WriteLine ( "" ) ;
m_Writer.Flush ( ) ;
}
// Всегда выполняйте вывод на страницу!
if ( null != Stk )
{
// Выполняем вывод предупреждения в текущий TraceContext.
HttpContext.Current.Trace.Warn ( FinalString ) ;
// Ищем на странице AssertionControl.
// Сначала убедимся, что описатель представляет страницу!
if ( HttpContext.Current.Handler is System.Web.UI.Page )
{
System.Web.UI.Page CurrPage =
(System.Web.UI.Page)HttpContext.Current.Handler ;
// Обходим сложности, если на странице нет
// элементов управления (в чем я сомневаюсь!)
if ( true == CurrPage.HasControls( ) )
{
// Ищем элемент управления.
AssertControl AssertCtl = null ;
FindAssertControl ( CurrPage.Controls ,
out AssertCtl
) ;
// Если он есть, добавляем утверждение.
if ( null != AssertCtl )
{
AssertCtl.AddAssertion ( Message
,
DetailMessage ,
Stk
) ;
}
}
}
// Наконец, если нужно, запускаем отладчик.
if ( true == m_LaunchDebuggerOnAssert )
{
// Если отладчик уже подключен, я могу просто применить
// Debugger.Break. Не важно, где именно запущен отладчик,
см. след. стр.

100

ЧАСТЬ I

Сущность отладки

// если он работает в этом процессе.
if ( true == Debugger.IsAttached )
{
Debugger.Break ( ) ;
}
else
{
// С изменениями в модели безопасности версии
// .NET RTM, учетная запись ASPNET, которую использует
// ASPNET_WP.EXE, перенесена из System в User.
// Для работы Debugger.Launch надо добавить
// ASPNET в группу Debugger Users. Хотя в отладочных
// системах это безопасно, в рабочих системах
// следует соблюдать осторожность.
bool bRet = IsRequestFromLocalMachine ( ) ;
if ( true == bRet )
{
Debugger.Launch ( ) ;
}
}
}
}
else
{
// TraceContext доступен прямо из HttpContext.
HttpContext.Current.Trace.Write ( FinalString ) ;
}
}
///
/// Определяет, пришел ли запрос от локальной машины.
///
///
/// Проверяет, равен ли IPадрес адресу 127.0.0.1
/// или серверной переменной LOCAL_ADDR.
///
///
/// Возвращает true, если запрос пришел от локальной машины,
/// в противном случае — false.
///
private bool IsRequestFromLocalMachine ( )
{
// Получаем объект для запроса.
HttpRequest Req = HttpContext.Current.Request ;
// Замкнут ли клиент на себя?
bool bRet = Req.UserHostAddress.Equals ( "127.0.0.1" ) ;
if ( false == bRet )
{
// Получаем локальный IPадрес из серверных переменных.

ГЛАВА 3

Отладка при кодировании

101

String LocalStr =
Req.ServerVariables.Get ( "LOCAL_ADDR" ) ;
// Сравниваем локальный IPадрес с IPадресом запроса.
bRet = Req.UserHostAddress.Equals ( LocalStr ) ;
}
return ( bRet ) ;
}
///
/// Ищет на странице элементы управления утверждений.
///
///
/// Все элементы управления утверждений носят имя "AssertControl",
/// так что этот метод просто просматривает набор элементов
/// управления на странице и ищет это имя. Кроме того,
/// он рекурсивно просматривает вложенные элементы.
///
///
/// Набор элементов для просмотра.
///
///
/// Исходящий параметр, который содержит найденный элемент управления.
///
private void FindAssertControl ( ControlCollection CtlCol
,
out AssertControl AssertCtrl )
{
// Просматриваем все элементы управления из массива.
foreach ( Control Ctl in CtlCol )
{
// Это тот элемент?
if ( "AssertControl" == Ctl.GetType().Name )
{
// Да! Выходим.
AssertCtrl = (AssertControl)Ctl ;
return ;
}
else
{
// Если этот элемент имеет вложенные, просматриваем их тоже.
if ( true == Ctl.HasControls ( ) )
{
FindAssertControl ( Ctl.Controls ,
out AssertCtrl ) ;
// Если один из вложенных элементов
// содержал искомый, то можно выходить.
if ( null != AssertCtrl )
{
return ;
см. след. стр.

102

ЧАСТЬ I

Сущность отладки

}
}
}
}
// В этом наборе его не нашли.
AssertCtrl = null ;
return ;
}
}

Утверждения в приложениях C++
Многие годы в старой компьютерной шутке, сравнивающей языки программиро
вания с машинами, C++ всегда сравнивают с болидом Формулы 1: быстрый, но
опасный для вождения. В другой шутке говорится, что C++ дает вам пистолет, чтобы
прострелить себе ногу, и, когда вы проходите «Hello World!», курок уже почти спу
щен. Я думаю, можно сказать, что C++ — это болид Формулы 1 с двумя ружьями,
чтобы вы могли прострелить себе ногу во время аварии. Тогда как даже малейшая
ошибка способна обрушить ваше приложение, интенсивное использование утвер
ждений в C++ — единственный способ получить шанс на отладку таких приложе
ний.
C и C++ также включают все виды функций, которые помогут максимально
подробно описать условия утверждений (табл. 32).

Табл. 3-2.

Вспомогательные функции для описательных утверждений C и C++

Функция

Описание

GetObjectType

Функция подсистемы интерфейса графических устройств (GDI),
возвращающая тип описателя GDI.

IsBadCodePtr

Проверяет, что указатель на область памяти может быть запущен.

IsBadReadPtr

Проверяет, что по указателю на область памяти можно считать
указанное количество байт.

IsBadStringPtr

Проверяет, что по указателю на строку можно читать данные
до ограничителя строки NULL или до указанного максимального
числа символов.

IsBadWritePtr

Проверяет, что по указателю на область памяти можно записать
указанное количество байт.

IsWindow

Проверяет, является ли параметр HWND допустимым окном.

Функции IsBad* небезопасны в многопоточной среде. В то время как один поток
вызывает IsBadWritePtr, чтобы проверить права доступа к участку памяти, другой
поток может менять содержимое памяти на которую указывает указатель. Эти
функции дают вам лишь описание ситуации на отдельный момент времени. Не
которые читатели первого издания этой книги утверждали, что, поскольку функ
ции IsBad* небезопасны в многопоточной среде, их вообще лучше не трогать, раз
они могут вызвать ложное ощущение безопасности. Категорически не согласен.
Гарантировать полностью безопасную проверку памяти в многопоточной среде
практически нельзя, если только вы не выполняете доступ к каждому байту в рамках

ГЛАВА 3

Отладка при кодировании

103

структурной обработки исключений. Такое возможно, но код станет настолько мед
ленным, что вы не сможете работать на компьютере. Еще одна проблема, кото
рую порой сильно преувеличивают, в том, что функции IsBad* в очень редких слу
чаях могут проглатывать исключения EXCEPTION_GUARD_PAGE. За все годы, которые я
занимаюсь разработкой под Windows, я никогда не сталкивался с этой пробле
мой. Я, безусловно, согласен мириться с такими недостатками функций IsBad* за
те преимущества, которые получаю от информированности о плохом указателе.
Следующий код демонстрирует одну из ошибок, которые я совершал в утвер
ждениях C++:

// Неверное использование утверждения.
BOOL CheckDriveFreeSpace ( LPCTSTR szDrive )
{
ULARGE_INTEGER ulgAvail ;
ULARGE_INTEGER ulgNumBytes ;
ULARGE_INTEGER ulgFree ;
if ( FALSE == GetDiskFreeSpaceEx ( szDrive
&ulgAvail
&ulgNumBytes
&ulgFree
{
ASSERT ( FALSE ) ;
return ( FALSE ) ;
}

}

,
,
,
) )

Хотя я использовал правильное утверждение, я не отображал невыполненное
условие. Информационное окно утверждения показывало лишь выражение «FALSE»,
что не оченьто помогало. Используя утверждения, старайтесь сообщать в инфор
мационном окне максимально подробную информацию о сбое утверждения.
Мой друг Дейв Энжел (Dave Angel) обратил мое внимание на то, что в C и C++
можно просто применить логический оператор NOT (!), используя строку в каче
стве операнда. Такая комбинация дает гораздо лучшее выражение в информаци
онном окне утверждения, так что вы хотя бы имеете представление о том, что
случилось, не заглядывая в исходный код. Вот правильный способ утверждения
условия сбоя:

// Правильное использование утверждения
BOOL CheckDriveFreeSpace ( LPCTSTR szDrive )
{
ULARGE_INTEGER ulgAvail ;
ULARGE_INTEGER ulgNumBytes ;
ULARGE_INTEGER ulgFree ;
if ( FALSE == GetDiskFreeSpaceEx ( szDrive
&ulgAvail
&ulgNumBytes
&ulgFree
{
ASSERT ( !"GetDiskFreeSpaceEx failed!" ) ;

,
,
,
) )

104

ЧАСТЬ I

Сущность отладки

return ( FALSE ) ;
}

}
Фокус Дейва можно усовершенствовать, применив логический условный опе
ратор AND (&&) так, чтобы выполнять нормальное утверждение и выводить текст
сообщения. Вот как это сделать (заметьте: при использовании логического AND в
начале строки не ставится «!»):

BOOL AddToDataTree ( PTREENODE pNode )
{
ASSERT ( ( FALSE == IsBadReadPtr ( pNode , sizeof ( TREENODE) ) ) &&
"Invalid parameter!"
) ;

}

Макрос VERIFY
Прежде чем перейти к макросам и функциям утверждений, с которыми вы стол
кнетесь при разработке под Windows, а также к связанным с ними проблемам, я
хочу поговорить о макросе VERIFY, широко используемом в разработках на осно
ве библиотеки классов Microsoft Foundation Class (MFC). В отладочной сборке
макрос VERIFY ведет себя, как обычное утверждение, поскольку он определен как
ASSERT. Если условие равно 0, макрос VERIFY инициирует обычное информацион
ное окно утверждения, предупреждая о проблемах. В финальной сборке макрос
VERIFY не выводит информационного окна, однако его параметр остается в исход
ном коде и вычисляется в ходе нормальной работы.
По сути макрос VERIFY позволяет создавать обычные утверждения с побочны
ми эффектами, и эти побочные эффекты остаются в финальных сборках. В иде
але ни в каких типах утверждений не следует использовать условия, вызывающие
побочные эффекты. Однако макрос VERIFY может пригодиться: когда функция воз
вращает код ошибки, который вы все равно не стали бы проверять иначе. Напри
мер, если при вызове ResetEvent для очистки освободившегося описателя собы
тия происходит сбой, то не остается ничего другого, кроме как завершить работу
приложения, поэтому большинство разработчиков вызывает ResetEvent, не про
веряя возвращаемое значение ни в отладочных, ни в финальных сборках. Если
выполнять вызов через макрос VERIFY, то по крайней мере в отладочных сборках
вы будете получать уведомления о том, что нечто пошло не так. Конечно, тот же
результат можно получить и благодаря ASSERT, но VERIFY позволяет избежать со
здания новой переменной только для хранения и проверки возвращаемого зна
чения из вызова ResetEvent — переменной, которая скорее всего будет использо
вана только в отладочных сборках.
Думаю, большинство программистов MFC использует макрос VERIFY потому, что
им так удобнее, но попробуйте отказаться от этой привычки. В большинстве слу
чаев вместо применения VERIFY следовало бы проверять возвращаемые значения.
Хороший пример частого использования VERIFY — функциячлен CString::LoadString,
загружающая строки ресурсов. Здесь макрос VERIFY сгодится для отладочных сбо

ГЛАВА 3

Отладка при кодировании

105

рок, так как при сбое LoadString он предупреждает вас об этом. Однако в финаль
ных сборках сбой LoadString приведет к вызову неинициализированной переменной.
Если повезет, вы получите пустую строку, но чаще всего это ведет к краху финальной
сборки. Мораль: проверяйте возвращаемые значения. Если хотите задействовать
макрос VERIFY, подумайте, не послужит ли игнорирование возвращаемого значе
ния причиной проблем в финальных сборках.

Отладка: фронтовые очерки
Исчезающие файлы и потоки
Боевые действия
В работе над версией BoundsChecker в NuMega мы испытывали невероят
ные трудности со случайными сбоями, которые было почти невозможно
повторить. Единственной зацепкой было то, что описатели файлов и пото
ков внезапно становились недействительными. Это означало, что файлы
случайно закрывались и иногда нарушалась синхронизация потоков. Раз
работчики пользовательского интерфейса также сталкивались с периоди
ческими сбоями, но только при работе в отладчике. Наконец эти пробле
мы привели к тому, что все члены команды прекратили свою работу и ста
ли пытаться исправить эти ошибки.

Исход
Команда чуть было не облила меня смолой и не вываляла в перьях, потому
что, как выяснилось, виноват в этой проблеме был я. Я отвечал за отладоч
ные циклы в BoundsChecker. В отладочном цикле используется отладочный
API Windows для запуска и управления другими процессами и отлаживае
мой программой, а также для реакции на события отладки, генерируемые
отладчиком. Как добросовестный программист, я видел, что функция WaitFor
DebugEvent возвращала описатели для некоторых уведомлений о событиях
отладки. Например, при запуске процесса в отладчике последний мог по
лучать структуру, содержащую описатель процесса и начальный поток для
него.
Я был очень осторожен и знал, что, если API предоставил описатель ка
когото объекта, который вам больше не нужен, следует вызвать CloseHandle,
чтобы освободить память, занимаемую этим объектом. Поэтому, когда от
ладочный API предоставлял описатель, я закрывал его, как только он мне
становился не нужен. Это выглядело вполне оправданно.
Однако, к моему великому огорчению, я не читал написанное мелким
шрифтом в документации отладочного API, где говорилось, что отладочный
API сам закрывает каждый процесс и создаваемые им описатели потоков.
Получалось так, что я удерживал некоторые описатели, возвращаемые от
ладочным API, до тех пор пока они были мне нужны, но закрывал их после
использования — после того как их уже закрыл отладочный API.
Чтобы понять, как это привело к нашей проблеме, надо знать, что, ког
да вы закрываете описатель, ОС помечает его значение как свободное. Micro
см. след. стр.

106

ЧАСТЬ I

Сущность отладки

soft Windows NT 4, которую мы тогда использовали, весьма агрессивна в
отношении повторного применения значений описателей. (Microsoft Win
dows 2000/XP демонстрируют такую же агрессивность по отношению к
значениям описателей.) Элементы нашего UI, интенсивно применявшие
многопоточность и открывавшие много файлов, постоянно создавали и
использовали новые описатели. Поскольку отладочный API закрывал мои
описатели, и ОС обращалась к ним повторно, иногда элементы UI получа
ли один из описателей, с которыми работал я. Закрывая позже свои копии
описателей, я на самом деле закрывал потоки и описатели файлов UI!
Я едва избежал смолы и перьев, показав что эта же ошибка присутство
вала в отладочных циклах предыдущих версий BoundsChecker. До сих пор
нам просто везло. Разница в том, что та версия, над которой мы работали,
имела новый улучшенный UI, гораздо интенсивнее работавший с файлами
и потоками, что создало условия для выявления моей ошибки.

Полученный опыт
Если б я читал написанное мелким шрифтом в документации отладочного
API, то избежал бы этих проблем. Кроме того — и это главный урок! — я
понял, что нужно всегда проверять возвращаемые значения CloseHandle. Хотя,
закрывая неверный поток, вы не сможете ничего предпринять, ОС сообщает
вам, что чтото не так, и к этому надо относиться со вниманием.
Замечу также: если, работая в отладчике, вы пытаетесь дважды закрыть
описатель или передать неверное значение в CloseHandle, ОС Windows ини
циируют исключение «Invalid Handle» (0xC0000008). Увидев такое значение
исключения, можете прерваться и выяснить, почему это произошло.
А еще я понял, что очень полезно бегать быстрее своих коллег, когда они
гонятся за тобой с котлами смолы и мешками перьев.

Различные типы утверждений в Visual C++
Хотя в C++ я описываю все свои макросы и функции утверждений с помощью
простого ASSERT, о котором расскажу через секунду, сначала все же хочу коротко
остановиться на других типах утверждений, доступных в Visual C++, и немного
рассказать об их использовании. Тогда, встретив какоенибудь из них в чужом коде,
вы сможете его узнать. Кроме того, хочу предупредить вас о проблемах, возни
кающих с некоторыми реализациями.

assert, _ASSERT и _ASSERTE
Первый тип утверждения из библиотеки исполняющей системы C — макрос assert
из стандарта ANSI C. Эта версия переносима на все компиляторы и платформы C
и определяется включением ASSERT.H. В мире Windows, если в работе с консоль
ным приложением инициируется утверждение, assert направит вывод в stderr. Если
ваше Windowsприложение содержит графический пользовательский интерфейс,
assert отобразит сведения о сбое в информационном окне.

ГЛАВА 3

Отладка при кодировании

107

Второй тип утверждения в библиотеке исполняющей системы C ориентиро
ван только на Windows. В него входят утверждения _ASSERT и _ASSERTE, определен
ные в CRTDBG.H. Единственная разница между ними в том, что вариант _ASSERTE
также выводит выражение, переданное в виде параметра. Поскольку это выраже
ние так важно, особенно при тестировании инженерами отладки, всегда выбирайте
_ASSERTE, применяя библиотеку исполняющей среды C. Оба макроса являются ча
стью исключительно полезного отладочного кода библиотеки исполняющей среды,
и утверждения — лишь одна из многих его функций.
Хотя assert, _ASSERT и _ASSERTE удобны и бесплатны, у них есть недостатки. Макрос
assert содержит две проблемы, способные несколько вас огорчить. Первая заклю
чается в том, что отображаемое имя файла усекается до 60 символов, так что иногда
вы не сможете понять, какой файл инициировал исключение. Вторая проблема
assert проявляется в работе с проектами, не содержащими UI, такими как службы
Windows или внепроцессные COMсерверы. Поскольку assert направляет свой вывод
в stderr или в информационное окно, вы можете его пропустить. В случае инфор
мационного окна ваше приложение зависнет, так как вы не можете закрыть ин
формационное окно, если не отображаете UI.
С другой стороны, макросы исполняющей среды C решают проблему с ото
бражением по умолчанию информационного окна, позволяя через вызов функ
ции _CrtSetReportMode перенаправить утверждение в файл или в функцию API
OutputDebugString. Однако все поставляемые Microsoft утверждения страдают од
ним пороком: они изменяют состояние системы, а это нарушение главного зако
на для утверждений. Влияние побочных эффектов на вызовы утверждений едва
ли не хуже, чем полный отказ от их использования. Следующий пример демонст
рирует, как поставляемые утверждения могут вносить различия между отладоч
ными и финальными сборками. Сможете ли вы обнаружить проблему?

// Направляем сообщение в окно. Если время ожидания истекло, значит, другой
// поток завис, так что его нужно закрыть. Напомню, единственный способ
// проверить сбой SendMessageTimeout — вызвать GetLastError.
// Если функция возвращает 0 и код последней ошибки 0,
// время ожидания SendMessageTimeout истекло.
_ASSERTE ( NULL != pDataPacket ) ;
if ( NULL == pDataPacket )
{
return ( ERR_INVALID_DATA ) ;
}
LRESULT lRes = SendMessageTimeout ( hUIWnd
,
WM_USER_NEEDNEXTPACKET ,
0
,
(LPARAM)pDataPacket
,
SMTO_BLOCK
,
10000
,
&pdwRes
) ;
_ASSERTE ( FALSE != lRes ) ;
if ( FALSE == lRes )
{
// Получаем код последней ошибки.
DWORD dwLastErr = GetLastError ( ) ;

108

ЧАСТЬ I

Сущность отладки

if ( 0 == dwLastErr )
{
// UI завис или обрабатывает данные слишком медленно.
return ( ERR_UI_IS_HUNG ) ;
}
// Если ошибка другая, значит, проблема в данных,
// передаваемых через параметры.
return ( ERR_INVALID_DATA ) ;
}
return ( ERR_SUCCESS ) ;

Проблема здесь в том, что поставляемые утверждения уничтожают код после
дней ошибки. До проверки исполняется «_ASSERTE ( FALSE != lRes )», отобража
ется информационное окно, и код последней ошибки меняется на 0. Так что в от
ладочных сборках всегда будет казаться, что завис UI, а в финальных сборках про
явятся случаи, когда переданные SendMessageTimeout параметры были неверны.
То, что предоставляемые системой утверждения уничтожают код последней
ошибки, может никак не отразиться на вашем коде, но я видел и другое: две ошибки,
на обнаружение которых ушло немало времени, были вызваны именно этой про
блемой. Но, к счастью, если вы будете использовать утверждение, представленное
ниже в этой главе в разделе «SUPERASSERT», я позабочусь об этой проблеме за вас и
расскажу коечто, о чем не сообщают системные версии утверждений.

ASSERT_KINDOF и ASSERT_VALID
Если вы программируете, применяя MFC, в вашем распоряжении есть два допол
нительных макроса утверждений, специфичных для MFC и являющих собой фан
тастические примеры профилактической отладки. Если вы объявляли классы с
помощью DECLARE_DYNAMIC или DECLARE_SERIAL, то, используя макрос ASSERT_KINDOF, можете
проверить, является ли указатель на потомок CObject определенным классом или
потомком определенного класса. Утверждение ASSERT_KINDOF — всего лишь оболочка
метода CObject::IsKindOf. Следующий фрагмент сначала проверяет параметр в ут
верждении ASSERT_KINDOF, а затем выполняет действительную проверку ошибок в
параметрах.

BOOL DoSomeMFCStuffToAFrame ( CWnd * pWnd )
{
ASSERT ( NULL != pWnd ) ;
ASSERT_KINDOF ( CFrameWnd , pWnd ) ;
if ( ( NULL == pWnd ) ||
( FALSE == pWnd>IsKindOf ( RUNTIME_CLASS ( CFrameWnd ) ) ) )
{
return ( FALSE ) ;
}

// Выполняем прочие действия MFC; pWnd гарантированно
// является CFrameWnd или его потомком.

}

ГЛАВА 3

Отладка при кодировании

109

Второй специфичный для MFC макрос утверждений — ASSERT_VALID. Это утвер
ждение интерпретирует AfxAssertValidObject, который полностью проверяет кор
ректность указателя на класспотомок CObject. После проверки правильности ука
зателя ASSERT_VALID вызывает метод AssertValid объекта. AssertValid — это метод,
который может быть переопределен в потомках для проверки всех внутренних
структур данных в классе. Этот метод предоставляет прекрасный способ глубо
кой проверки ваших классов. Переопределяйте AssertValid во всех ключевых классах.

SUPERASSERT
Рассказав вам о проблемах с поставляемыми утверждениями, я хочу продемонст
рировать, как я исправил и расширил утверждения так, чтобы они действительно
сообщали, как и почему возникли проблемы, и делали еще больше. На рис. 33 и
34 показаны примеры диалоговых окон SUPERASSERT, сообщающих об ошибках.
В первом издании этой книги вывод SUPERASSERT представлял собой информаци
онное окно, в котором показывалось расположение невыполненного утвержде
ния, код последней ошибки, преобразованный в текст, и стек вызова. Как видно
из рисунков, SUPERASSERT определенно подрос! (Однако я не стал называть его
SUPERPUPERASSERT!)

Рис. 33.

Пример свернутого диалогового окна SUPERASSERT

Самое изумительное в труде писателя — потрясающие дискуссии с читателя
ми, в которых я участвовал по электронной почте и лично. Мне повезло учиться
у таких поразительно умных ребят! Вскоре после выхода первого издания между
Скоттом Байласом (Scott Bilas) и мной состоялся интересный обмен письмами по
электронной почте, в которых мы обсудили его мысли о том, что должны делать
сообщения утверждений и как их использовать. Изначально я применял инфор
мационное окно, так как хотел оставить утверждение максимально легковесным.
Однако, обменявшись массой интересных соображений со Скоттом, я убедился,
что сообщения утверждений должны предлагать больше функций, таких как по
давление утверждений (assertion suppression). Скотт даже предложил код для свер
тывания диалоговых окон (dialog box folding), свой макрос ASSERT для отслежива
ния числа пропусков (ignore) и т. п. Вдохновленный идеями Скотта, я создал но
вую версию SUPERASSERT. Я сделал это сразу после выхода первой версии и с тех
пор использовал новый код во всех своих разработках, так что он прошел серь
езную обкатку.
На рис. 33 показаны части диалогового окна, которые видны постоянно. Поле
ввода Failure содержит причину сбоя (Assertion или Verify), невыполненное выра
жение, место сбоя, расшифрованный код последней ошибки и число сбоев дан

110

ЧАСТЬ I

Сущность отладки

ного конкретного утверждения. Если утверждение работает под Windows XP, Server
2003 и выше, оно также отображает общее число описателей ядра (kernel handle)
в процессе. В SUPERASSERT я преобразую коды последней ошибки в их текстовое
описание. Получение сообщений об ошибках в текстовом виде исключительно
полезно при сбоях функций API: вы видите, почему произошел сбой, и можете
быстрее запустить отладчик. Так, если в GetModuleFileName происходит сбой по
причине малого объема буфера ввода, SUPERASSERT установит код последней ошибки
равным 122, что соответствует ERROR_INSUFFICIENT_BUFFER из WINERROR.H. Сразу увидев
текст «The data area passed to a system call is too small» («Область данных, передан
ная системному вызову, слишком мала»), вы поймете, в чем именно проблема и
как ее устранить. На рис. 33 — стандартное сообщение Windows об ошибке, но
вы вправе добавить свои ресурсы сообщений к преобразованию сообщений о
последней ошибке в SUPERASSERT. Подробнее о собственных ресурсах сообщений
см. раздел MSDN «Message Compiler». Дополнительный стимул к использованию
ресурсов сообщений в том, что они здорово облегчают локализацию ваших при
ложений.

Рис. 34.

Пример развернутого диалогового окна SUPERASSERT

Кнопка Ignore Once, расположенная под полем ввода Failure, просто продол
жает выполнение. Она выделена по умолчанию, так что, нажав Enter или пробел,
вы можете сразу продолжить работу, изучив причину сбоя. Abort Program вызы
вает ExitProcess, чтобы попытаться корректно завершить приложение. Кнопка Break
Into Debugger инициирует вызов DebugBreak, так что вы можете начать отладку сбоя,
перейдя в отладчик или запустив отладчик по требованию. Кнопка Copy To Clipboard

ГЛАВА 3

Отладка при кодировании

111

из второго ряда копирует в буфер обмена весь текст из поля ввода Failure, а также
информацию из всех потоков для которых есть данные из стека. Последняя кноп
ка — More>> или Less 0 )
{
g_iGlobalIgnoreCount— ;
return ( FALSE ) ;
}
// Надо ли пропустить это локальное утверждение?
if ( ( NULL != piIgnoreCount ) && ( *piIgnoreCount > 0 ) )
{
*piIgnoreCount = *piIgnoreCount  1 ;
return ( FALSE ) ;
}
// Содержит возвращаемое значение функций обработки строк (STRSAFE).
HRESULT hr = S_OK ;
// Сохраняем код последней ошибки, чтобы не сбить
// его при работе с диалогом утверждения.
DWORD dwLastError = GetLastError ( ) ;

ГЛАВА 3

Отладка при кодировании

119

TCHAR szFmtMsg[ MAX_PATH ] ;
DWORD dwMsgRes = ConvertErrorToMessage ( dwLastError ,
szFmtMsg
,
sizeof ( szFmtMsg ) /
sizeof ( TCHAR ) ) ;
if ( 0 == dwMsgRes )
{
hr = StringCchCopy ( szFmtMsg
,
sizeof ( szFmtMsg ) / sizeof ( TCHAR ) ,
_T ( "Last error message text not available\r\n" ) ) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
}
// Получаем информацию о модуле.
TCHAR szModuleName[ MAX_PATH ] ;
if ( 0 == GetModuleWithAssert ( dwIP , szModuleName , MAX_PATH ))
{
hr = StringCchCopy ( szModuleName
,
sizeof ( szModuleName ) / sizeof (TCHAR) ,
_T ( "" )
);
ASSERT ( SUCCEEDED ( hr ) ) ;
}
// Захватываем синхронизирующий объект,
// чтобы не дать другим потокам достигнуть этой точки.
EnterCriticalSection ( &g_cCS.m_CritSec ) ;
// Буфер для хранения сообщения с выражением.
TCHAR szBuffer[ 2048 ] ;
#define BUFF_CHAR_SIZE ( sizeof ( szBuffer ) / sizeof ( TCHAR ) )
if ( ( NULL != szFile ) && ( NULL != szFunction ) )
{
// Выделяем базовое имя из полного имени файла.
TCHAR szTempName[ MAX_PATH ] ;
LPTSTR szFileName ;
LPTSTR szDir = szTempName ;
hr = StringCchCopy ( szDir
,
sizeof ( szTempName ) / sizeof ( TCHAR ) ,
szFile
);
ASSERT ( SUCCEEDED ( hr ) ) ;
szFileName = _tcsrchr ( szDir , _T ( '\\' ) ) ;
if ( NULL == szFileName )
{
szFileName = szTempName ;
szDir = _T ( "" ) ;
}
else
{
см. след. стр.

120

ЧАСТЬ I

Сущность отладки

*szFileName = _T ( '\0' ) ;
szFileName++ ;
}
DWORD dwHandleCount = 0 ;
if ( TRUE == SafelyGetProcessHandleCount ( &dwHandleCount ) )
{
// Используем новые функции STRSAFE,
// чтобы не выйти за пределы буфера.
hr = StringCchPrintf (
szBuffer
,
BUFF_CHAR_SIZE
,
_T ( "Type
: %s\r\n"
)\
_T ( "Expression : %s\r\n"
)\
_T ( "Module
: %s\r\n"
)\
_T ( "Location
: %s, Line %d in %s (%s)\r\n")\
_T ( "LastError
: 0x%08X (%d)\r\n"
)\
_T ( "
%s"
)\
_T ( "Fail count : %d\r\n"
)\
_T ( "Handle count : %d"
),
szType
,
szExpression
,
szModuleName
,
szFunction
,
iLine
,
szFileName
,
szDir
,
dwLastError
,
dwLastError
,
szFmtMsg
,
*piFailCount
,
dwHandleCount
);
ASSERT ( SUCCEEDED ( hr ) ) ;
}
else
{
hr = StringCchPrintf (
szBuffer
,
BUFF_CHAR_SIZE
,
_T ( "Type
: %s\r\n"
) \
_T ( "Expression : %s\r\n"
) \
_T ( "Module
: %s\r\n"
) \
_T ( "Location : %s, Line %d in %s (%s)\r\n")\
_T ( "LastError : 0x%08X (%d)\r\n"
) \
_T ( "
%s"
) \
_T ( "Fail count : %d\r\n"
) ,
szType
,
szExpression
,
szModuleName
,
szFunction
,
iLine
,

ГЛАВА 3

Отладка при кодировании

szFileName
szDir
dwLastError
dwLastError
szFmtMsg
*piFailCount
ASSERT ( SUCCEEDED ( hr ) ) ;
}
}
else
{
if ( NULL == szFunction )
{
szFunction = _T ( "Unknown function" ) ;
}
hr = StringCchPrintf ( szBuffer
BUFF_CHAR_SIZE
_T ( "Type
: %s\r\n"
_T ( "Expression : %s\r\n"
_T ( "Function : %s\r\n"
_T ( "Module
: %s\r\n"
_T ( "LastError : 0x%08X (%d)\r\n"
_T ( "
%s"
szType
szExpression
szFunction
szModuleName
dwLastError
dwLastError
szFmtMsg
ASSERT ( SUCCEEDED ( hr ) ) ;
}

121

,
,
,
,
,
);

,
,
)
)
)
)
)
)

\
\
\
\
,
,
,
,
,
,
,
) ;

if ( DA_SHOWODS == ( DA_SHOWODS & GetDiagAssertOptions ( ) ) )
{
OutputDebugString ( szBuffer ) ;
OutputDebugString ( _T ( "\n" ) ) ;
}
if ( DA_SHOWEVENTLOG ==
( DA_SHOWEVENTLOG & GetDiagAssertOptions ( ) ) )
{
// Делаем запись в журнал событий,
// только если все действительно кошерно.
static BOOL bEventSuccessful = TRUE ;
if ( TRUE == bEventSuccessful )
{
bEventSuccessful = OutputToEventLog ( szBuffer ) ;
}
}
см. след. стр.

122

ЧАСТЬ I

Сущность отладки

if ( INVALID_HANDLE_VALUE != GetDiagAssertFile ( ) )
{
static BOOL bWriteSuccessful = TRUE ;
if ( TRUE == bWriteSuccessful )
{
DWORD dwWritten ;
int
iLen = lstrlen ( szBuffer ) ;
char * pToWrite = NULL ;
#ifdef UNICODE
pToWrite = (char*)_alloca ( iLen + 1 ) ;
BSUWide2Ansi ( szBuffer , pToWrite , iLen + 1 ) ;
#else
pToWrite = szBuffer ;
#endif
bWriteSuccessful = WriteFile ( GetDiagAssertFile ( )
pToWrite
iLen
&dwWritten
NULL
if ( FALSE == bWriteSuccessful )
{
OutputDebugString (
_T ( "\n\nWriting assertion to file failed.\n\n"
}

,
,
,
,
) ;

) ) ;

}
}
// По умолчанию воспринимаем возвращаемое значение как IGNORE.
// Это особенно уместно, если пользователю не нужно окно MessageBox.
INT_PTR iRet = IDIGNORE ;
// Отображаем диалог, только если он нужен пользователю
// и если процесс выполняется интерактивно.
if ( ( DA_SHOWMSGBOX == ( DA_SHOWMSGBOX & GetDiagAssertOptions()))&&
( TRUE == BSUIsInteractiveUser ( )
) )
{
iRet = PopTheFancyAssertion ( szBuffer
,
szEmail
,
dwStack
,
dwStackFrame ,
dwIP
,
piIgnoreCount ) ;
}
// Я закончил критическую секцию!
LeaveCriticalSection ( &g_cCS.m_CritSec ) ;

ГЛАВА 3

Отладка при кодировании

123

SetLastError ( dwLastError ) ;
// Хочет ли пользователь перейти в отладчик?
if ( IDRETRY == iRet )
{
return ( TRUE ) ;
}
// Хочет ли пользователь прервать программу?
if ( IDABORT == iRet )
{
ExitProcess ( (UINT)1 ) ;
return ( TRUE ) ;
}
// Единственный оставшийся вариант — игнорировать утверждение.
return ( FALSE ) ;
}
// Занимается отображением диалогового окна утверждения.
static INT_PTR PopTheFancyAssertion ( TCHAR * szBuffer
LPCSTR szEmail
DWORD64 dwStack
DWORD64 dwStackFrame
DWORD64 dwIP
int * piIgnoreCount
{

,
,
,
,
,
)

// В этой подпрограмме я не выделяю память, потому что это может вызвать
// фатальные проблемы. Я собираюсь сильно повысить приоритет этих потоков,
// чтобы забрать ресурсы от других потоков и приостановить их.
// Если на этом этапе я попытаюсь выделить память, то могу попасть
// в ситуацию, когда потоки с малым приоритетом будут владеть CRT
// или синхронизирующим объектом кучи и он понадобится этому потоку.
// Следовательно, мы получим большое веселое зависание.
// (Да, я так уже делал, вот почему я это знаю!)
THREADINFO aThreadInfo [ k_MAXTHREADS ] ;
DWORD aThreadIds [ k_MAXTHREADS ] ;
// Первый поток в массиве информации о потоках  это ВСЕГДА текущий
// поток. Это массив с нулевой базой, так что код диалога может
// рассматривать все потоки как равные. Однако в этой функции массив
// рассматривается как массив с единичной базой, поэтому текущий поток
// не приостанавливается вместе с остальными.
UINT uiThreadHandleCount = 1 ;
aThreadInfo[ 0 ].dwTID = GetCurrentThreadId ( ) ;
aThreadInfo[ 0 ].hThread = GetCurrentThread ( ) ;
aThreadInfo[ 0 ].szStackWalk = NULL ;
см. след. стр.

124

ЧАСТЬ I

Сущность отладки

// Сначала надо сразу повысить приоритет текущего потока. Я не хочу,
// чтобы создавались новые потоки, пока я готовлюсь их приостановить.
int iOldPriority = GetThreadPriority ( GetCurrentThread ( ) ) ;
VERIFY ( SetThreadPriority ( GetCurrentThread ( )
,
THREAD_PRIORITY_TIME_CRITICAL ) ) ;
DWORD dwPID = GetCurrentProcessId ( ) ;
DWORD dwIDCount = 0 ;
if ( TRUE == GetProcessThreadIds ( dwPID
,
k_MAXTHREADS
,
(LPDWORD)&aThreadIds ,
&dwIDCount
) )
{
// Должен быть хоть один поток!!
ASSERT ( 0 != dwIDCount ) ;
ASSERT ( dwIDCount < k_MAXTHREADS ) ;
// Вычисляем количество описателей.
uiThreadHandleCount = dwIDCount ;
// Если количество описателей равно 1, это однопоточное
// приложение, и мне ничего не нужно делать!
if ( ( uiThreadHandleCount > 1
) &&
( uiThreadHandleCount < k_MAXTHREADS ) )
{
// Открываем каждый описатель, приостанавливаем его
// и сохраняем описатель, чтобы запустить его позже.
int iCurrHandle = 1 ;
for ( DWORD i = 0 ; i < dwIDCount ; i++ )
{
// Конечно, не останавливать этот поток!!
if ( GetCurrentThreadId ( ) != aThreadIds[ i ] )
{
HANDLE hThread =
OpenThread ( THREAD_ALL_ACCESS ,
FALSE
,
aThreadIds [ i ] ) ;
if ( ( NULL != hThread
) &&
( INVALID_HANDLE_VALUE != hThread ) )
{
// Если SuspendThread возвращает 1,
// хранить значение этого потока незачем.
if ( (DWORD)1 != SuspendThread ( hThread ) )
{
aThreadInfo[iCurrHandle].hThread = hThread ;
aThreadInfo[iCurrHandle].dwTID =
aThreadIds[ i ] ;
aThreadInfo[iCurrHandle].szStackWalk = NULL;
iCurrHandle++ ;
}

ГЛАВА 3

Отладка при кодировании

125

else
{
VERIFY ( CloseHandle ( hThread ) ) ;
uiThreadHandleCount— ;
}
}
else
{
// Или для этого потока установлена какаято защита,
// или он закрылся сразу после того, как я собрал
// информацию о потоках. Значит, надо уменьшить
// общее число описателей потоков, или их будет
// на один больше.
TRACE( "Can't open thread: %08X\n" ,
aThreadIds [ i ]
) ;
uiThreadHandleCount— ;
}
}
}
}
}
// Возвращаем прежнее значение приоритета потока!
SetThreadPriority ( GetCurrentThread ( ) , iOldPriority ) ;
// Убеждаемся, что ресурсы приложения установлены.
JfxGetApp()>m_hInstResources = GetBSUInstanceHandle ( ) ;
// Сам диалог утверждения.
JAssertionDlg cAssertDlg ( szBuffer
szEmail
dwStack
dwStackFrame
dwIP
piIgnoreCount
(LPTHREADINFO)&aThreadInfo
uiThreadHandleCount

,
,
,
,
,
,
,
) ;

INT_PTR iRet = cAssertDlg.DoModal ( ) ;
if ( ( 1 != uiThreadHandleCount
) &&
( uiThreadHandleCount < k_MAXTHREADS )
)
{
// Снова повышаем приоритет потока!
int iOldPriority = GetThreadPriority ( GetCurrentThread ( ) ) ;
VERIFY ( SetThreadPriority ( GetCurrentThread ( )
,
THREAD_PRIORITY_TIME_CRITICAL ) );
// Если в ходе работы я приостановил другие потоки, надо
// запустить их, закрыть описатели и удалить массив.
см. след. стр.

126

ЧАСТЬ I

Сущность отладки

for ( UINT i = 1 ; i < uiThreadHandleCount ; i++ )
{
VERIFY ( (DWORD)1 !=
ResumeThread ( aThreadInfo[ i ].hThread ) ) ;
VERIFY ( CloseHandle ( aThreadInfo[ i ].hThread ) ) ;
}
// Возвращаем прежнее значение приоритета потока.
VERIFY ( SetThreadPriority ( GetCurrentThread ( ) ,
iOldPriority
) ) ;
}
return ( iRet ) ;
}
BOOL BUGSUTIL_DLLINTERFACE
SuperAssertionA ( LPCSTR szType
,
LPCSTR szExpression ,
LPCSTR szFunction
,
LPCSTR szFile
,
int
iLine
,
LPCSTR szEmail
,
DWORD64 dwStack
,
DWORD64 dwStackFrame ,
int * piFailCount ,
int * piIgnoreCount )
{
int iLenType = lstrlenA ( szType ) ;
int iLenExp = lstrlenA ( szExpression ) ;
int iLenFile = lstrlenA ( szFile ) ;
int iLenFunc = lstrlenA ( szFunction ) ;
wchar_t * pWideType = (wchar_t*)
HeapAlloc ( GetProcessHeap ( )
,
HEAP_GENERATE_EXCEPTIONS ,
( iLenType + 1 ) *
sizeof ( wchar_t )
) ;
wchar_t * pWideExp = (wchar_t*)
HeapAlloc ( GetProcessHeap ( )
,
HEAP_GENERATE_EXCEPTIONS ,
( iLenExp + 1 ) *
sizeof ( wchar_t )
) ;
wchar_t * pWideFile = (wchar_t*)
HeapAlloc ( GetProcessHeap ( )
,
HEAP_GENERATE_EXCEPTIONS ,
( iLenFile + 1 ) *
sizeof ( wchar_t ) );
wchar_t * pWideFunc = (wchar_t*)
HeapAlloc ( GetProcessHeap ( )
,
HEAP_GENERATE_EXCEPTIONS ,
( iLenFunc + 1 ) *
sizeof ( wchar_t ) ) ;

ГЛАВА 3

BSUAnsi2Wide
BSUAnsi2Wide
BSUAnsi2Wide
BSUAnsi2Wide

(
(
(
(

Отладка при кодировании

szType , pWideType , iLenType + 1
szExpression , pWideExp , iLenExp
szFile , pWideFile , iLenFile + 1
szFunction , pWideFunc , iLenFunc

)
+
)
+

127

;
1 ) ;
;
1 ) ;

BOOL bRet ;
bRet = RealSuperAssertion ( pWideType
pWideExp
pWideFunc
pWideFile
iLine
szEmail
dwStack
dwStackFrame
(DWORD64)_ReturnAddress ( )
piFailCount
piIgnoreCount

,
,
,
,
,
,
,
,
,
,
) ;

VERIFY ( HeapFree ( GetProcessHeap ( ) , 0 , pWideType ) ) ;
VERIFY ( HeapFree ( GetProcessHeap ( ) , 0 , pWideExp ) ) ;
VERIFY ( HeapFree ( GetProcessHeap ( ) , 0 , pWideFile ) ) ;
return ( bRet ) ;
}
BOOL BUGSUTIL_DLLINTERFACE
SuperAssertionW ( LPCWSTR szType
,
LPCWSTR szExpression ,
LPCWSTR szFunction
,
LPCWSTR szFile
,
int
iLine
,
LPCSTR szEmail
,
DWORD64 dwStack
,
DWORD64 dwStackFrame ,
int * piFailCount ,
int * piIgnoreCount )
{
return ( RealSuperAssertion ( szType
,
szExpression
,
szFunction
,
szFile
,
iLine
,
szEmail
,
dwStack
,
dwStackFrame
,
(DWORD64)_ReturnAddress ( ) ,
piFailCount
,
piIgnoreCount
) ) ;
}
см. след. стр.

128

ЧАСТЬ I

Сущность отладки

// Возвращает количество инициаций утверждения в приложении.
// В этом количестве учитываются все пропуски утверждения.
int BUGSUTIL_DLLINTERFACE GetSuperAssertionCount ( void )
{
return ( g_iTotalAssertions ) ;
}
static BOOL SafelyGetProcessHandleCount ( PDWORD pdwHandleCount )
{
static BOOL bAlreadyLooked = FALSE ;
if ( FALSE == bAlreadyLooked )
{
HMODULE hKernel32 = ::LoadLibrary ( _T ( "kernel32.dll" ) ) ;
g_pfnGPH = (GETPROCESSHANDLECOUNT)
::GetProcAddress ( hKernel32
,
"GetProcessHandleCount" ) ;
FreeLibrary ( hKernel32 ) ;
bAlreadyLooked = TRUE ;
}
if ( NULL != g_pfnGPH )
{
return ( g_pfnGPH ( GetCurrentProcess ( ) , pdwHandleCount ) );
}
else
{
return ( FALSE ) ;
}
}
static SIZE_T GetModuleWithAssert ( DWORD64 dwIP ,
TCHAR * szMod ,
DWORD dwSize )
{
// Пытаемся получить базовый адрес памяти для значения из стека.
// По базовому адресу я попытаюсь получить модуль.
MEMORY_BASIC_INFORMATION stMBI ;
ZeroMemory ( &stMBI , sizeof ( MEMORY_BASIC_INFORMATION ) ) ;
SIZE_T dwRet = VirtualQuery ( (LPCVOID)dwIP
,
&stMBI
,
sizeof ( MEMORY_BASIC_INFORMATION ) );
if ( 0 != dwRet )
{
dwRet = GetModuleFileName ( (HMODULE)stMBI.AllocationBase ,
szMod
,
dwSize
) ;
if ( 0 == dwRet )
{
// Сдаемся и просто возвращаем EXE.
dwRet = GetModuleFileName ( NULL , szMod , dwSize ) ;
}

ГЛАВА 3

Отладка при кодировании

129

}
return ( dwRet ) ;
}
Сам код диалога в ASSERTDLG.CPP довольно скромен, так что его не стоило
приводить в книге. Когда мы со Скоттом Байласом обсуждали, на чем должно быть
написано диалоговое окно, мы решили, что это должен быть простой язык, не
требующий дополнительных двоичных файлов, кроме DLL, содержащей диалоговое
окно, — все указывало на MFC. Когда я писал диалоговое окно, библиотека шаб
лонов Windows Template Library (WTL) еще не вышла. Но скорее всего я и не стал
бы ее использовать, так как отношусь к шаблонам с опаской. Лишь немногие раз
работчики на самом деле понимают все переплетения в шаблонах, и большин
ство ошибок, с которыми приходилось бороться моей компании, были прямым
следствием применения шаблонов. Несколько лет назад мы с Джеффри Рихтером
(Jeffrey Richter) участвовали в проекте, для которого требовался исключительно
легковесный UI, и разработали простую библиотеку классов UI под именем JFX.
Джеффри будет утверждать, что JFX означает «Jeffrey’s Framework», но на самом
деле это «John’s Framework», что бы он ни говорил. Как бы то ни было, для созда
ния UI я использовал JFX. Полный исходный код содержится среди файлов при
меров к этой книге. В каталоге JFX есть пара тестовых программ, показывающих,
как использовать JFX, и код диалога SUPERASSERT. Хорошая новость: JFX исключи
тельно мал и компактен — финальная версия BugslayerUtil.DLL, включающая го
раздо больше, чем просто SUPERASSERT, занимает менее 70 Кб.

Стандартный вопрос отладки
Почему в условных операторах ты всегда размещаешь
константы слева?
Я всегда использую операторы вроде «if ( INVALID_HANDLE_VALUE == hFile )»
вместо «if ( hFile == INVALID_HANDLE_VALUE )». Я делаю это во избежание оши
бок. Вы можете пропустить один знак равенства, и тогда первая версия
приведет к ошибке компиляции. Вторая версия может не вызвать преду
преждения (в зависимости от уровня диагностики компилятора), и вы из
мените значение переменной. Компиляторы при попытке присвоить зна
чение константе выдают ошибку компиляции. Если вам приходилось искать
ошибки, связанные со случайным присвоением, вы знаете, как трудно их
обнаружить.
Присмотревшись к моему коду, вы увидите, что я размещаю констант
ные переменные в левой части равенств. Как и в случае константных зна
чений, компилятор сообщит об ошибке при попытке присвоить значение
константной переменной. Выяснилось, что гораздо проще исправлять ошиб
ки компиляции, чем искать ошибки в отладчике.
Некоторые разработчики жаловались (иногда очень громко), что мой
способ написания условных операторов ухудшает читабельность кода. Не
согласен. На чтение и перевод моих условных операторов требуется на одну
секунду больше времени. Я готов пожертвовать этой секундой, чтобы не
тратить огромное количество времени позже.

130

ЧАСТЬ I

Сущность отладки

Trace, Trace, Trace и еще раз Trace
Утверждения — возможно лучший прием профилактического программирования
из всех, что вы узнали, а операторы Trace при правильном использовании вместе
с утверждениями действительно позволят отлаживать приложения без отладчи
ка. Для некоторых опытных программистов среди вас операторы Trace — суть
отладка в стиле printf. Мощность отладки в стиле printf нельзя недооценивать,
поскольку так отлаживалось большинство приложений до изобретения интерак
тивных отладчиков. Трассировка в .NET интригует, так как, когда Microsoft впер
вые публично упомянула про .NET, ключевые преимущества были ориентирова
ны не на разработчиков, а на администраторов сетей и ITперсонал, ответствен
ных за развертывание написанных разработчиками приложений. Одним из важ
нейших новых преимуществ Microsoft называла возможность для ITперсонала
легко включать трассировку, чтобы находить проблемы в приложениях! Читая это,
я был ошеломлен, поскольку это говорило о том, что Microsoft откликнулась на
страдания наших конечных пользователей, сталкивающихся с ошибками в про
граммах.
Тонкость трассировки — в определении объема нужной информации для ре
шения проблем на машинах, на которых не установлена среда разработки. Запи
сать слишком много — получатся большие файлы, работа с которыми станет му
кой, слишком мало — вы не сможете решить проблему. Действия по балансиров
ке требуют наличия ровно такого объема записанной информации, чтобы избе
жать экстренного перелета за 8 000 километров к пользователю, у которого толь
ко что появилась та мерзкая ошибка, — перелета, в котором вам придется сидеть
на среднем сиденье рядом с плачущим ребенком и больным пассажиром. В об
щем, это значит, что вам понадобятся два уровня трассировки: один, отражающий
основную работу в программе, чтобы видеть, что и когда вызывалось, и второй —
для добавления в файл ключевых данных, чтобы вы могли отыскивать проблемы,
связанные с потоками данных.
К сожалению, все приложения разные, так что я не могу назвать вам точное
число операторов трассировки или другие признаки данных, которых будет до
статочно для журнала трассировки. Один из лучших подходов, которые я видел,
заключался в том, чтобы дать нескольким новым членам команды пример журна
ла и спросить, дает ли он им достаточно информации для начала поиска пробле
мы. Если через пару часов они с отвращением отказываются, вероятно, инфор
мации мало. Если же через часдва у них в общих чертах появится представление
о том, где находилось приложение на момент повреждения или краха, — это при
знак того, что ваш журнал содержит нужный объем информации.
Как я отмечал в главе 2, следует иметь общекомандную систему ведения жур
налов. Частью разработки этой системы должно стать определение формата трас
сировки, особенно для облегчения работы с отладочной трассировкой. Без тако
го формата эффективность трассировки быстро исчезает, так как никто не захо
чет пробираться сквозь тонны текста без особых на то причин. Хорошая новость
для приложений .NET в том, что Microsoft проделала большую работу, чтобы об
легчить управление выводом. В машинных приложениях вам придется создавать
собственные системы, но ниже я дам вам коекакие рекомендации в разделе «Трас
сировка в приложениях C++».

ГЛАВА 3

Отладка при кодировании

131

Перед тем как ринуться в разбор особенностей для разных платформ, хочу
упомянуть об одном исключительном инструменте, который всегда должен быть
на машинах для разработки: DebugView. Мой бывший сосед Марк Руссинович (Mark
Russinovich) написал DebugView и массу других потрясающих инструментов, ко
торые можно скачать с сайта Sysinternals (www.sysinternals.com). У них отличная
цена (бесплатно!), многие инструменты доступны с исходным кодом и решают
некоторые очень сложные проблемы, а потому вам стоит посещать Sysinternals хоть
раз в месяц. DebugView отслеживает все вызовы к OutputDebugString пользователь
ского режима или к DbgPrint режима ядра, так что вы сможете видеть всю отла
дочную информацию, не работая в отладчике. Что делает DebugView еще более
полезным, так это его способность работать с другими машинами, и вы сможете
следить за всеми машинами распределенной системы с одного компьютера.

Трассировка в Windows Forms и консольных приложениях .NET
Как я сказал, Microsoft наделала маркетингового шума вокруг трассировки в при
ложениях .NET. В общем, они неплохо потрудились при создании хорошей архи
тектуры, которая лучше управляет трассировкой в реальных разработках. Говоря
об утверждениях, я уже упоминал объект Trace, поскольку он необходим для трас
сировки. Как и Debug, для обработки вывода объект Trace использует концепцию
применения TraceListener. Поэтому мой код утверждений в ASP.NET менял прием
ники для обоих объектов: так весь вывод направляется в одно место. В коде ут
верждений из ваших разработок вам лучше поступать так же. Вызовы методов
объекта Trace активны, только если определен параметр TRACE. По умолчанию он
определен в проектах и отладочных, и финальных сборок, создаваемых Visual Studio
.NET, поэтому скорее всего методы уже активны.
Объект Trace содержит четыре метода для вывода информации трассировки:
Write, WriteIf, WriteLine и WriteLineIf. Вероятно, вы догадались о разнице между Write
и WriteLine, но понять методы *If сложнее: они позволяют осуществлять услов
ную трассировку. Если первый параметр метода *If принимает значение true, вы
полняется трассировка, false — нет. Это довольно удобно, но при неосторожном
обращении может привести к серьезным проблемам с производительностью. Так,
написав код, вроде показанного в первой части следующего отрывка, вы будете
испытывать издержки от конкатенации строк при каждом выполнении этой строки
кода, так как необходимость трассировки определяется внутри вызова Trace.
WriteLineIf. Гораздо лучше следовать второму примеру из фрагмента, где опера
тор if для вызова Trace.WriteLine используется только при необходимости, мини
мизируя издержки от конкатенации строк.

// Испытываем издержки каждый раз.
Trace.WriteLineIf ( bShowTrace , "Parameters: x=" + x + " y =" + y ) ;
// Выполняем конкатенацию, только когда это необходимо.
if ( true == bShowTrace )
{
Trace.WriteLine ("Parameters: x=" + x + " y =" + y ) ;
}

132

ЧАСТЬ I

Сущность отладки

Думаю, разработчики .NET оказали нам всем большую услугу, добавив класс
TraceSwitch. При наличии методов *If в объекте Trace, позволяющих выполнять
трассировку по условию, остается лишь шаг до определения класса, предоставля
ющего несколько уровней трассировки и единый способ их установки. Важней
шая часть TraceSwitch — это имя, присваиваемое ему в первом параметре конст
руктора. (Второй параметр — это описательное имя.) Имя позволяет управлять
объектом снаружи приложения, о чем я расскажу через секунду. В объектах Trace
Switch заключены уровни трассировки (табл. 33). Для проверки соответствия
TraceSwitch определенному уровню служит набор свойств, таких как TraceError,
возвращающих true, если объект соответствует данному уровню. В сочетании с
методами *If использование объектов TraceSwitch вполне очевидно.

public static void Main ( )
{
TraceSwitch TheSwitch = new TraceSwitch ( "SwitchyTheSwitch",
"Example Switch" );
TheSwitch.Level = TraceLevel.Info ;
Trace.WriteLineIf ( TheSwitch.TraceError ,
"Error tracing is on!" ) ;
Trace.WriteLineIf ( TheSwitch.TraceWarning ,
"Warning tracing is on!" ) ;
Trace.WriteLineIf ( TheSwitch.TraceInfo ,
"Info tracing is on!" ) ;
Trace.WriteLineIf ( TheSwitch.TraceVerbose ,
"VerboseSwitching is on!" ) ;
}
Табл. 3-3. Уровни TraceSwitch
Уровень трассировки

Значение

Off — Выкл.

0

Error — Ошибки

1

Warnings (and errors) — Предупреждения (и ошибки)

2

Info (warnings and errors) — Информация (предупреждения и ошибки)

3

Verbose (everything) — Полная информация

4

Чудо объектов TraceSwitch в том, что ими легко управлять снаружи приложе
ния из вездесущего файла CONFIG. В элементе switches, вложенном в элемент
system.diagnostic, указываются элементы add, с помощью которых добавляются и
устанавливаются имена и уровни. В листинге 37 показан полный конфигураци
онный файл для приложения. В идеале для каждой сборки в приложении надо иметь
отдельный объект TraceSwitch. Помните, что параметры TraceSwitch также можно
применять к глобальному файлу MACHINE.CONFIG.

Листинг 3-7.

Установка флагов TraceSwitch в конфигурационном файле

ГЛАВА 3

Отладка при кодировании

133

Трассировка в приложениях ASP.NET и Web-сервисах XML
Несмотря на наличие прекрасно продуманных объектов Trace и TraceSwitch, ASP.NET
и — как расширение — Webсервисы XML содержат совершенно иную систему
трассировки. Исходя из размещения вывода трассировки ASP.NET, я могу понять
причину этих различий, но все равно считаю, что они сбивают с толку. Класс
System.Web.UI.Page содержит собственный объект Trace, наследуемый от System.Web.Tra
ceContext. Чтобы не путать эти два разных варианта трассировки, я буду ссылать
ся на вариант ASP.NET как на TraceContext.Trace. Два ключевых метода Trace
Context.Trace — это Write и Warn. Оба они обрабатывают вывод трассировки, но Warn
записывает вывод красным цветом. Каждый метод имеет три перегруженных вер
сии, и оба принимают одинаковые параметры: обычное сообщение и категорию
с вариантами сообщений, но есть версия, принимающая категорию, сообщение
и System.Exception. Эта последняя версия записывает строку исключения, а также
источник и строку где было сгенерировано исключение. Чтобы избежать лишних
издержек в обработке, когда трассировка отключена, проверяйте, имеет ли свой
ство IsEnabled значение true.
Самый простой способ включить трассировку — задать атрибуту Trace дирек
тивы @Page, располагающейся в начале ваших ASPXфайлов, значение true.

Волшебная маленькая директива включает тонны информации трассировки, ко
торая появляется в нижней части страницы, что довольно удобно, но так ее ви
дите и вы, и пользователи. Честно говоря, информации трассировки так много,
что я очень хотел бы, чтобы она была поделена на несколько уровней. Иметь
информацию о файлах cookie (Cookies), наборах заголовков (Headers Collections)
и серверных переменных (Server Variables) приятно, но чаще всего она не нужна.
Все разделы вполне очевидны, но я хочу выделить раздел Trace Information, так
как здесь появляются все вызовы к TraceContext.Trace. Даже если вы не вызывали
TraceContext.Trace.Warn/Write, вы все равно увидите информацию в разделе Trace
Information, потому что ASP.NET сообщает о вызове нескольких своих методов.
В этом разделе и появляется красный текст при вызове TraceContext.Trace.Warn.
Устанавливать атрибут Trace в начале каждой страницы приложения скучно,
поэтому разработчики ASP.NET ввели в WEB.CONFIG раздел, позволяющий управ
лять трассировкой. Этот раздел, вполне логично названный trace, показан ниже:

134

ЧАСТЬ I

Сущность отладки

Атрибут enabled управляет включением трассировки для данного приложения.
Атрибут requestLimit указывает, сколько запросов трассировки кэшировать в па
мяти для каждого приложения. (Через секунду мы обсудим, как просмотреть эти
кэшированные запросы.) Элемент pageOutput сообщает ASP.NET, показывать ли вывод
трассировки. Если pageOutput задано true, вывод появляется на странице, как если
бы вы установили атрибут Trace в директиве Page. Вероятно, вам не захочется менять
элемент traceMode поскольку так информация в разделе трассировки Trace Infor
mation отсортирована по времени. Если вы хотите увидеть сортировку по катего
риям, задайте traceMode значение SortByCategory. Последний атрибут — localOnly —
сообщает ASP.NET, должен ли вывод быть видим только на локальной машине или
он должен быть виден для всех клиентских приложений.
Чтобы увидеть кэшированные запросы трассировки, когда pageOutput задано false,
добавьте к каталогу приложения HTTPобработчик trace.axd, который отобразит
страницу, позволяющую выбрать сохраненную информацию трассировки, кото
рую вы хотите увидеть. Скажем, если имя вашего каталога — http://www.wintel
lect.com/schedules, то, чтобы увидеть сохраненную информацию трассировки,
используйте путь http://www.wintellect.com/schedules/trace.axd. Достигнув преде
ла requestLimit, ASP.NET прекращает записывать информацию трассировки. Запись
можно перезапустить, просмотрев страницу trace.axd и щелкнув ссылку Clear Current
Trace в верхней части страницы.
Как видите, если не соблюдать осторожность в трассировке, ее увидят конеч
ные пользователи, а это всегда пугает, так как разработчики печально известны
операторами трассировки, способными повредить карьере, если вывод попадет в
плохие руки. К счастью, установив localOnly в true, вы сможете просматривать
трассировку только на локальном сервере, даже при доступе к журналу трасси
ровки через HTTPобработчик trace.axd. Чтобы просмотреть журналы трассиров
ки вашего приложения, вам просто придется применить величайший программ
ный продукт, известный человечеству, — Terminal Services, и вы получите доступ
к серверу прямо из своего офиса, даже не вставая изза стола. Стоит также изме
нить раздел customErrors файла WEB.CONFIG для использования страницы default
Redirect, чтобы при попытке доступа к trace.axd с удаленной машины конечные
пользователи не увидели ошибку ASP.NET «Server Error in ‘Имя_приложения’ Application».
Кроме того, тех, кто пытается получить доступ к trace.axd, стоит заносить в жур
нал, особенно потому, что неудавшаяся попытка доступа, вероятно, указывает на
хакера.
Сейчас ктото из вас, возможно, думает об одной проблеме с трассировкой в
ASP.NET: ASP.NET содержит TraceContext.Trace, отправляющий свой вывод в одно

ГЛАВА 3

Отладка при кодировании

135

место, а DefaultTraceListener для объекта System.Diagnostic.Trace отправляет свой
вывод кудато еще. В обычном ASP.NET это огромная проблема, но если вы при
меняете код утверждений из BugslayerUtil.NET, описанный выше, то ASPTraceListener
также используется как единый TraceListener для объекта System.Diagnostic.Trace,
так что я перенаправляю всю информацию трассировки в TraceContext.Trace, чтобы
вся она появлялась в одном месте.

Трассировка в приложениях C++
Почти всю трассировку в таких приложениях выполняет макрос C++, обычно
носящий имя TRACE и активный только в отладочных сборках. В конечном счете
функция, вызываемая им, вызовет OutputDebugString из Windows API, так что ин
формацию трассировки можно видеть в отладчике или в DebugView. Помните: вызов
OutputDebugString приводит к переходу в режим ядра. Это не очень важно для от
ладочных сборок, но может отрицательно сказаться на производительности фи
нальных сборок, так что учтите все вызовы, которые могут остаться в финальных
сборках. Вообще в поисках способов повысить производительность Windows в
целом, команда Windows удалила массу трассировок, на которые мы все привык
ли полагаться, таких как сообщение о конфликте загрузки DLL, появлявшееся при
загрузке DLL, и это привело к очень хорошему росту производительности.
Если у вас нет макроса TRACE, можете использовать мой — из состава Bugs
layerUtil.DLL. Всю работу выполняют функции DiagOutputA/W из DIAGASSERT.CPP.
Преимущество моего кода в том, что вы можете вызвать SetDiagOutputFile, пере
дав ему как параметр описатель файла, и записывать всю трассировку в файл.
В дополнение к макросу TRACE в главе 18 описывается мой инструмент FastTrace
для серверных приложений C++. Последнее, что хочется делать в приложениях,
интенсивно использующих многопоточность, — это принуждать все потоки бло
кироваться на синхронизирующий объект при включении трассировки. Инстру
мент FastTrace дает максимально возможную производительность трассировки без
потерь важных потоков информации.

Комментировать, комментировать
и еще раз комментировать
Однажды мой друг Франсуа Полин (Franç ois Poulin), который весь день занима
ется сопровождением кода, написанного другими, пришел со значком, на кото
ром было написано: «Кодируй так, как будто тот, кто сопровождает твой код, —
буйнопомешанный, который знает, где ты живешь». Франсуа, несомненно, псих,
но в его словах есть огромный смысл. Хотя вам может казаться, что ваш код явля
ет собой образец ясности и совершенно очевиден, без подробных комментариев
для сопровождающих разработчиков он так же плох, как сырой ассемблер. Иро
ния в том, что сопровождающим разработчиком вашего кода легко можете стать
вы сами! Незадолго до начала работы над вторым изданием этой книги я полу
чил по электронной почте письмо из компании, в которой работал лет 10 назад,
с просьбой обновить проект, который я для них писал. Взглянуть на код, кото
рый я писал так давно, было потрясающе! Потрясало и то, насколько плохие я делал
комментарии. Вводя каждую строку кода, вспоминайте значок Франсуа.

136

ЧАСТЬ I

Сущность отладки

Наша задача двойственна: разработать решение для пользователя и сделать его
пригодным к сопровождению в будущем. Единственный способ сделать код со
провождаемым — комментировать его. Под словами «комментировать его» я под
разумеваю не просто создание комментариев, повторяющих то, что делает код;
я подразумеваю документирование ваших предположений, подходов к решению
задачи и причин, по которым выбран именно такой подход. Также следует соот
носить свои комментарии с кодом. Обычные кроткие программисты сопровож
дения могут впасть в сомнамбулическое состояние, пытаясь обновить код, дела
ющий не то, что он должен делать согласно комментариям.
Создавая комментарии, я руководствуюсь следующими правилами.
쮿 Каждая функция или метод требуют одногодвух предложений, проясняющих:
• что делает подпрограмма;
• какие в ней приняты допущения;
• что должно содержаться в каждом из входных параметров;
• что должно содержаться в каждом из выходных параметров в случае успе
ха и неудачи;
• каждое из возможных возвращаемых значений;
• каждое исключение, самостоятельно генерируемое функцией.
쮿 Каждая часть функции, не являющаяся совершенно понятной из кода, требует
одногодвух предложений, объясняющих что она делает.
쮿 Любой интересный алгоритм заслуживает полного описания.
쮿 Любые нетривиальные ошибки, исправленные в коде, должны быть проком
ментированы с указанием номера ошибки и описания исправлений.
쮿 Удачно размещенные операторы трассировки и утверждения, а также хорошие
схемы именования тоже могут служить хорошими комментариями и давать
прекрасный контекст для кода.
쮿 Комментируйте так, словно вам самому придется сопровождать этот код че
рез пять лет.
쮿 Старайтесь не оставлять в модулях закомментированный «мертвый» код. Дру
гие разработчики никогда не понимают, следовало ли удалить закомментиро
ванный код насовсем или это было сделано лишь временно для тестирования.
Вернуться к участкам кода, которых больше нет в текущей версии, вам помо
жет система контроля версий.
쮿 Если вам хочется сказать: «Я настоящий хакер» или «Это было действительно
сложно», — то, вероятно, лучше не комментировать функцию, а переписать ее.
Корректное и полное документирование в коде отличает профессионала от того,
кто просто играет в него. Дональд Кнут (Donald Knuth) както заметил, что хоро
шо написанная программа должна читаться как хорошо написанная книга. Хотя
я не представляю себя захваченным сюжетом исходного кода TeX, я абсолютно
согласен с мнением дра Кнута.
Я рекомендую вам изучить главу 19 или сногсшибательную книгу Стива Мак
Коннелла (Steve McConnell) «Совершенный Код» (Code Complete. — Microsoft Press,
1993). В этой главе рассказано как я учился писать комментарии. С правильными

ГЛАВА 3

Отладка при кодировании

137

комментариями, даже если ваш программист сопровождения окажется психом, вам
ничто не угрожает.
Раз уж мы обсуждаем комментарии, хочу заметить, как сильно я люблю ком
ментарии XMLдокументации, введенные в C#, и как преступно то, что они не
поддерживаются остальными языками от Microsoft. Надеюсь, в будущем все язы
ки получат первоклассные комментарии XMLдокументации. Имея ясный формат
комментариев, который может быть извлечен при компоновке, вы можете начать
создание целостной документации для вашего проекта. По правде говоря, я так
люблю комментарии XMLдокументации, что создал не очень сложный макрос
CommenTater (см. главу 9), который добавляет и обновляет ваши комментарии XML
документации и следит, чтобы вы не забывали добавлять их.

Доверяй, но проверяй (Блочное тестирование)
Я всегда считал, что Энди Гроув (Andy Grove) — бывший председатель совета ди
ректоров Intel — был прав, назвав свою книгу «Выживают только одержимые» («Only
the Paranoid Survive»). Это особенно верно для программистов. У меня много хо
роших друзей — прекрасных программистов, но когда дело касается взаимодей
ствия их кода с моим, я проверяю их данные до последнего бита. Вообщето у меня
даже есть здоровый скепсис в отношении себя самого. С помощью утверждений,
трассировки и комментариев я проверяю разработчиков своей команды, вызы
вающих мой код. С помощью блочного тестирования я проверяю себя. Блочные
тесты — это строительные леса, которые вы возводите, чтобы вызвать ваш код из
за пределов программы как целого и убедиться, что код работает в соответствии
с ожиданиями.
Первое, что я делаю для самопроверки, — начинаю писать блочные тесты од
новременно с кодом, разрабатывая их параллельно. Определив интерфейс моду
ля, я пишу для него функциизаглушки (stub functions) и сразу создаю тестовую
программу (или «обвязку» — harness) для вызова этих интерфейсов. Добавляя
фрагменты функциональности, я добавляю новые варианты тестов в тестовую
программу. С таким подходом я могу протестировать каждое следующее измене
ние в отдельности и распределить создание тестовой программы по циклу раз
работки. Если всю обычную работу вы делаете после реализации главного кода,
то, как правило, у вас маловато времени для качественной работы над тестовой
программой и реализации эффективного теста.
Второй способ проверить себя — подумать о том, как тестировать код, прежде
чем его писать. Старайтесь не попасть в ловушку, думая, что, до того как вы смо
жете тестировать код, приложение должно быть написано полностью. Если вы
обнаружили, что стали жертвой такого заблуждения, сделайте шаг назад и пере
смотрите тестирование. Я понимаю, что иногда компиляция вашего кода зависит
от важной функциональности другого разработчика. В таких случаях ваш тест
должен состоять из заглушек для интерфейсов, с которыми возможна компиля
ция. Как минимум, запрограммируйте интерфейсы вручную, чтобы они возвра
щали нужные данные и вы смогли откомпилировать и запустить свой код.
Побочное преимущество от обеспечения тестируемости ваших разработок в
том, что вы быстро находите проблемы, которые можете устранить, чтобы сде

138

ЧАСТЬ I

Сущность отладки

лать ваш код более расширяемым и пригодным для многократного использова
ния. Поскольку многократное использование — это Святой Грааль программис
тов, то все, что бы вы ни сделали для повышения используемости вашего кода, будет
не напрасно. Хороший пример такой удачи — BugslayerStackTrace из Bugslayer
Util.NET.DLL. Когда я впервые реализовывал код трассировки в ASP.NET, я встроил
код для просмотра стека в класс ASPTraceListener. При тестировании я быстро понял,
что информация о стеке может понадобиться мне и в других местах. Я извлек код
просмотра стека из ASPTraceListener и поместил в отдельный класс — Bugslayer
StackTrace. Когда мне потребовалось написать классы BugslayerTextWriterTraceListener
и BugslayerEventLogTraceListener, у меня уже был базовый код, заранее созданный
и полностью протестированный.
При кодировании следует выполнять блочные тесты постоянно. Кажется, я
мыслю отдельными функциональными модулями примерно по 50 строчек кода.
Каждый раз, добавляя или изменяя чтолибо, я перезапускаю блочный тест, что
бы проверить, не нарушил ли я чегонибудь. Я не люблю сюрпризов и поэтому
стараюсь свести их к минимуму. Настоятельно рекомендую вам выполнять блоч
ные тесты перед внесением своего кода в главные исходные файлы. В некоторых
компаниях существуют специальные тесты внесения (checkin tests), которые
должны выполняться до внесения кода. Я видел, как эти тесты внесения радикально
снижали число неудавшихся компоновок и дымовых тестов.
Ключ к наиболее эффективному блочному тестированию заключается в двух
словах: покрытие кода (code coverage). Если из этой главы вы не вынесете ниче
го, кроме этих двух слов, я буду считать, что она удалась. Покрытие кода — это
просто процент строк, запущенных в вашем модуле. Если в вашем модуле 100 строк
и вы запустили 85, то покрытие кода составляет 85%. Простая истина в том, что
незапущенная строка — это строка, ждущая своей аварии.
Как консультанта, меня постоянно спрашивают, есть ли единый рецепт отлич
ного кода. Сейчас я в том месте, откуда я впадаю в «религиозный экстаз», — на
столько сильна моя вера в покрытие кода. Если бы вы сейчас стояли передо мной,
то я бы прыгал вверхвниз, восхваляя достоинства покрытия кода с евангелист
ским рвением. Многие разработчики говорили мне, что следование моему совету
и попытки получить хорошее покрытие кода привели к резкому повышению ка
чества кода. Это действует, и в этом весь секрет.
Получить статистику покрытия кода можно двумя способами. Первый способ
сложный и включает использование отладчика и установку точек прерывания в
каждой строке вашего модуля. По мере выполнения строк удаляйте точки преры
вания. Продолжайте выполнять код, пока не удалите все точки прерывания, и вы
получите стопроцентное покрытие. Легкий путь заключается в применении ин
струмента для покрытия от сторонних производителей, такого как TrueCoverage
от Compuware NuMega, Visual PureCoverage от Rational или CCover от Bullseye. Лично
я не вношу код в главные исходные файлы, пока не запущу минимум 85–90% строк
моего кода. Знаю, некоторые из вас сейчас застонали. Да, получение хорошего
покрытия кода может занять много времени. Иногда приходится выполнять го
раздо больше тестов, чем вы когдалибо думали, и это может требовать времени.
Получение хорошего покрытия подразумевает запуск вашего приложения в от
ладчике и изменение переменных с данными для запуска участков кода, до кото

ГЛАВА 3

Отладка при кодировании

139

рых трудно добраться иначе. Однако ваша работа в том, чтобы писать целостный
код, и, по моему мнению, покрытие кода, пожалуй, — единственный способ до
биться этого на этапе блочного тестирования.
Нет ничего хуже бездействующего QAперсонала, застрявшего на аварийных
сборках. Если в ходе блочного тестирования вы получите 90%ое покрытие кода,
ваши люди из отдела анализа качества могут использовать свое время для тести
рования приложения на разных платформах и проверки работоспособности ин
терфейсов между подсистемами. Работа QAотдела в том, чтобы тестировать про
дукт как единое целое и сосредоточиться на качестве в целом, а ваша — в том,
чтобы протестировать модуль и сосредоточиться на качестве этого модуля. Ког
да обе стороны делают свою работу, результатом становится высококачественный
продукт.
Ладно, я не жду, что разработчики будут проводить тесты на всех ОС Microsoft
семейства Win32, которые могут применяться пользователями. Однако, если они
смогут получить 90%ое покрытие хотя бы для одной ОС, команда выиграет две
трети борьбы за качество. Если вы не используете один из инструментов покры
тия от сторонних производителей, вы обманываете себя с качеством.
Помимо покрытия кода, в своих проектах блочного тестирования я часто за
пускаю инструменты определения ошибок и проверки производительности от
сторонних фирм (см. главу 1). Эти инструменты помогают мне гораздо раньше
отлавливать ошибки в цикле разработки, поэтому я трачу меньше времени на
общую отладку. Однако из всех инструментов определения ошибок и контроля
производительности, что у меня есть, я использую продукты покрытия кода на
несколько порядков чаще, чем чтолибо еще. К тому времени как я получаю дос
таточную величину покрытия кода, я решаю почти все ошибки и проблемы с
производительностью в коде.
Если вы будете следовать рекомендациям этого раздела, то к концу разработ
ки получите вполне эффективные блочные тесты, но на этом работа не заканчи
вается. Если вы посмотрите на коды, прилагаемые к этой книге, то в главном ка
талоге с исходным кодом для каждого инструмента увидите каталог Tests. В этом
каталоге хранятся мои блочные тесты для данного инструмента. Я сохраняю блоч
ные тесты как часть кодовой базы, чтобы их легко могли найти. Кроме того, ког
да я вношу изменения в исходный код, то легко могу провести тест и проверить,
не нарушил ли я чегонибудь. Настоятельно рекомендую вам зарегистрировать ваши
тесты в своей системе контроля версий. И наконец, хотя большинство блочных
тестов вполне очевидно, не забудьте задокументировать все важные допущения,
чтобы другие разработчики не тратили время на борьбу с вашими тестами.

140

ЧАСТЬ I

Сущность отладки

Резюме
В этой главе были представлены лучшие технологии профилактического програм
мирования, используемые для отладки при кодировании. Лучшая методика заклю
чается в повсеместном применении утверждений, чтобы получить контроль над
ошибочными ситуациями. Представленные коды утверждений .NET в Bugslayer
Util.NET.DLL и код SUPERASSERT устраняют все проблемы с утверждениями, предо
ставляемыми компиляторами Microsoft. В дополнение к утверждениям правиль
ная трассировка и комментарии могут облегчить вам и другим людям сопровож
дение и отладку кода. Наконец, самый важный критерий оценки качества для про
граммистов — блочное тестирование. Если вы сможете правильно протестиро
вать свой код перед внесением его в главные исходные файлы, то избежите мас
сы ошибок и проблем для обслуживающих инженеров в будущем.
Единственный способ правильно протестировать модуль — запустить при
выполнении тестов инструмент для учета покрытия кода. До внесения кода в глав
ные исходные файлы надо стараться получить покрытие минимум в 85–90%. Чем
больше времени вы потратите на отладку при разработке, тем меньше его потре
буется для отладки позднее.

Ч А С Т Ь

I I

ПРОИЗВОДИТЕЛЬНАЯ
ОТЛАДКА

Г Л А В А

4
Поддержка отладки ОС
и как работают
отладчики Win32

Изучение работы инструментария — ключевая часть нашей работы. Зная воз
можности инструментов, вы можете максимизировать их отдачу и меньше вре
мени тратить на отладку. В основном отладчики очень помогают, но иногда они
способны быть источником коварных проблем. Особенно интересна отладка не
управляемого кода, поскольку здесь вмешивается ОС и меняет поведение процес
сов, так как они работают под отладчиком. Кроме того, имеется весьма интерес
ная поддержка внутри самой ОС, помогающая в некоторых сложных ситуациях
при отладке. В этой главе я объясню, что такое отладчик, покажу, как работают
отладчики в ОС Microsoft Win32, а также мы обсудим хитрые приемы, необходи
мые для эффективного использования средств отладки Win32.
После краткого обзора Win32отладчиков я перейду к особенностям специаль
ных функций, доступных при запуске процесса под отладчиком. Чтобы показать,
как работают отладчики, я представлю пару, исходные коды которых находятся в
прилагаемых к этой книге файлах примеров: MinDBG выполняет тот минимум
функций, который позволяет ему называться отладчиком, а WDBG является при
мером настоящего отладчика Win32 и делает все, что положено, включая мани
пуляции с таблицами символов для просмотра локальных переменных и струк
тур, управление точками прерывания, генерацию дизассемблированного кода, а
также координацию с графическим интерфейсом пользователя (GUI). При обсуж
дении WDBG я также освещу такие темы, как работа точек прерывания, и расска
жу о типах файлов символов. В завершение я расскажу о написанной мной очень
крутой оболочке для сервера символов, которая упрощает работу с локальными

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

143

переменными и аргументами. Этот сервер был самым трудным кодом, написан
ным мной для этой книги, и я уверен, что вы найдете его весьма полезным!

Почему нет главы, посвященной отладчикам .NET?
Вы, возможно, удивляетесь, почему в этой книге нет главы, посвященной
работе отладчиков Microsoft .NET. Сначала я предполагал написать такую
главу, но в результате исследования отладочного API .NET (.NET Debugging
API) я понял, что в отличие от практически недокументированых отладчи
ков Win32 команда разработчиков исполняющей среды .NET проделала
огромную работу по описанию отладочного интерфейса .NET. Кроме того,
приведенный здесь пример отладчика показывает, как сделать все, что тре
буется от отладчика .NET. Этот пример почти на 98% — консольный отлад
чик CORDBG. В нем нет только команд дизассемблирования неуправляемого
кода. Работа над отладчиком .NET заняла у меня пару недель, и я быстро
понял, что здесь делать нечего (разве что изложить своими словами пре
красную документацию по .NET) и мне не удастся показать чтолибо новое,
кроме того, что видно из примера CORDBG. Файлы Debug.doc и DebugRef.doc,
описывающие отладочный API .NET, уже установлены на ваш компьютер в
процессе установки Visual Studio .NET и находятся в каталоге \SDK\v1.1\Tool Developers Guide\Docs.
И последнее. Прежде чем погрузиться в эту главу, я хочу определить два тер
мина, которые буду использовать на протяжении всей книги: отладчик (debugger)
и отлаживаемая программа (debuggee). Отладчик — это просто процесс, способ
ный управлять другим процессом для его отладки, а отлаживаемая программа —
это процесс, запускаемый под отладчиком. В некоторых ОС отладчик называют
родительским процессом, а отлаживаемую программу — дочерним.

Типы отладчиков Windows
Если вы программировали для Win32, то, возможно, слышали о нескольких ти
пах отладчиков. В мире Microsoft Windows доступны два типа: отладчики пользо
вательского режима и отладчики режима ядра.
Большинство программистов в основном знакомо с отладчиками пользователь
ского режима. Не будет сюрпризом узнать, что отладчики первого типа предназ
начены для отладки приложений пользовательского режима. Отладчики второго
типа, как следует из их названия, позволяют отлаживать ядро ОС. Такие отладчи
ки применяют главным образом разработчики драйверов.

Отладчики пользовательского режима
Отладчики пользовательского режима служит для отладки любых приложений, ра
ботающих в пользовательском режиме. Сюда входят любые программы с GUI, а
также, что для вас будет неожиданностью, такие приложения, как службы Windows.
Обычно отладчики пользовательского режима используют графические интерфей
сы. Главный признак отладчиков пользовательского режима — это то, что они при
меняют отладочный API Win32. Так как ОС помечает отлаживаемую программу как

144

ЧАСТЬ II

Производительная отладка

работающую в специальном режиме, вы можете вызвать функцию API IsDebugger
Present для определения, работает ли ваш процесс под отладчиком. Проверка,
работаете ли вы под отладчиком, может пригодиться, если вам требуется боль
ше диагностической информации, только когда к вашему процессу подключен
отладчик.
В Microsoft Windows 2000 и более ранних ОС проблема отладочного API Win32
заключается в том, что если процесс был однажды запущен под отладчиком и
отладчик завершается, то отлаживаемая программа тоже завершается. Иначе го
воря, отлаживаемая программа была постоянно отлаживаемой. Это ограничение
было прекрасно, когда все работали над клиентскими приложениями, но оно было
бедствием при отладке серверных приложений, особенно когда программисты
пытались отлаживать рабочие серверы. В Microsoft Windows XP/Server 2003 и более
поздних версиях вы можете подсоединять к работающим процессам и отсоеди
нять от них все, что вам понадобится, без какихлибо условий. В Visual Studio .NET
вы можете отсоединиться от процесса, выбрав Detach (отсоединить) в диалого
вом окне Processes (процессы).
Интересно, что Visual Studio .NET теперь предлагает службу Visual Studio Debugger
Proxy (DbgProxy) под Windows 2000, позволяющую отлаживать процесс, а затем
от него отсоединиться. DbgProxy работает как отладчик, т. е. ваше приложение
работает под отладчиком. Теперь вы и под Windows 2000 можете отсоединить, а
затем повторно присоединить к процессу все, что надо. Но я все еще наблюдаю
одну проблему программистов: независимо от используемой ими ОС (Windows
XP/Server 2003 или DbgProxy под Windows 2000), они продолжают «вечную от
ладку», забывая задействовать преимущества новой возможности отсоединения.
Для интерпретирующих языков и исполняющих сред, применяющих принцип
виртуальной машины, сами виртуальные машины предлагают полный комплект
отладки и не используют отладочный API Win32. Вот некоторые примеры сред
такого типа: виртуальные машины Java от Microsoft или Sun, механизм сценариев
Microsoft для Webприложений и, конечно, общеязыковая исполняющая среда
Microsoft .NET (common language runtime, CLR).
Как я уже говорил, отладка приложений .NET освещена в документах (каталог
Tool Developers Guide). Я также не буду касаться отладочных интерфейсов Java и
языков сценариев, которые выходят за рамки данной книги. О том, как писать
отладчик сценариев, см. в MSDN тему «Microsoft Windows Script InterfacesIntro
duction». Как и при отладке в CLR .NET, объекты отладчика сценариев предостав
ляют богатые интерфейсы для доступа к сценариям, в том числе встроенным в
документы.
Отладочным API Win32 пользуется неожиданно большое количество программ.
Сюда входят отладчик Visual Studio .NET при отладке неуправляемого кода, кото
рый я освещаю в деталях в главах 5 и 7, отладчик Windows (Windows Debugger,
WinDBG), обсуждаемый в главе 8, BoundsChecker от Compuware NuMega, программа
Platform SDK Depends (которая может быть установлена в составе Visual Studio .NET),
отладчики Borland Delphi и C++ Builder, а также символьный отладчик NT (NT
Symbolic Debugger, NTSD). Я уверен, имеется и много других.

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

145

Стандартный вопрос отладки
Как мне защитить Win32-программу от вмешательства отладчика?
Программисты, работающие на вертикальном рынке приложений с соб
ственными алгоритмами чаще всего меня спрашивают о том, как защитить
свои приложения и не дать конкурентам вмешаться в них с помощью от
ладчика. Вы, конечно, можете вызвать IsDebuggerPresent, который скажет,
работает ли отладчик пользовательского режима, но если у человека есть
хоть чуточку мозгов, то первое, что он сделает при восстановлении алго
ритма, — заставит IsDebuggerPresent возвращать 0 и, таким образом, будет
казаться, что отладчика нет.
Совершенного способа защититься от настырного хакера, имеющего
физический доступ к вашим исполняемым кодам, нет, но вы хотя бы може
те немного усложнить ему жизнь во время исполнения программы. Весьма
интересно, что до сих пор во всех ОС Microsoft IsDebuggerPresent работает
одинаково. Нет никакой гарантии, что они не изменят этого, но есть хоро
шие шансы, что все останется так же и в будущем.
Следующая функция, которую вы можете добавить к своему коду, делает
то же, что и IsDebuggerPresent. Конечно же, добавление только этой функ
ции не исключит возможности вмешиваться в ваш процесс с помощью от
ладчика. Чтобы затруднить отладку, между основными командами разбро
саны другие безобидные команды, так что хакеры не смогут искать IsDebug
gerPresent по последовательности байтов. Об антихакерских технологиях
можно написать целую книгу. Однако, если вы можете провести «двухчасо
вой тест», означающий, что если среднему программисту требуется более
двух часов на взлом вашего приложения, то ваше приложение, вероятно,
защищено от всех хакеров, кроме самых настырных и талантливых.

BOOL AntiHackIsDebuggerPresent ( void )
{
BOOL bRet = TRUE ;
__asm
{
// Получить блок информации потока (Thread Information block, TIB).
MOV
EAX , FS:[00000018H]
// Байты со смещением 0x30 в TIB — это поле указателя, который
// указывает на структуру, имеющую отношение к отладчику.
MOV
EAX , DWORD PTR [EAX+030H]
// Второй DWORD в этой отладочной структуре указывает,
// что процесс отлаживается.
MOVZX
EAX , BYTE PTR [EAX+002H]
// Возвращаем результат.
MOV
bRet , EAX
}
return ( bRet ) ;
}

146

ЧАСТЬ II

Производительная отладка

Отладчики режима ядра
Отладчики режима ядра располагаются между центральным процессором и ОС.
Это значит, что, когда вы останавливаетесь в отладчике режима ядра, ОС тоже
останавливается. Как можно себе представить, внезапная остановка ОС полезна,
если вы работаете над проблемами согласования по времени и синхронизации.
Существуют три отладчика режима ядра: отладчик ядра KD, WinDBG и SoftICE.

Отладчик ядра KD
Windows 2000/XP/Server 2003 интересны тем, что на самом деле часть отладчика
режима ядра является частью NTOSKRNL.EXE — главного файла ядра ОС. Этот от
ладчик доступен как в рабочей, так и в отладочной версии ОС. Для переключения
в режим отладки ядра для систем на базе процессоров x86 установите параметр
загрузки /DEBUG в файле BOOT.INI и дополнительно /DEBUGPORT при необходимос
ти установить порт связи для отладчика режима ядра на порт, отличный от порта
по умолчанию (COM1). KD работает на отдельной машине, называемой хостом, и
взаимодействует с целевой машиной через нульмодемный кабель или, скажем,
через кабель интерфейса 1394 (FireWire) при работе с Windows XP/Server 2003.
Отладчик режима ядра NTOSKRNL.EXE делает достаточно для управления цен
тральным процессором, позволяя отлаживать ОС. Основная работа по отладке —
управление символами, обработка расширенных точек прерывания и дизассемб
лирование — происходит на стороне KD. Когдато в Microsoft Windows NT 4 Device
Driver Kit (DDK) был описан протокол связи через нульмодемный кабель. Одна
ко Microsoft больше не приводит описание этого протокола.
KD входит в Debugging Tools for Windows (отладочные средства Windows),
которые можно загрузить с http://www.microsoft.com/ddk/debugging (текущая вер
сия на момент написания этой книги также доступна на прилагаемом к книге
компактдиске). Вся сила KD становится очевидной при ознакомлении с коман
дами, предлагаемыми им для доступа к внутренним состояниям ОС. Если вам ког
далибо хотелось увидеть, что происходит в ОС, эти команды покажут вам это.
Знание работы драйверов устройств Windows поможет разобраться с выводом этих
команд. Интересно, что при всей своей мощи KD почти никогда не применялся
за пределами Microsoft, так как это консольное приложение и им весьма утоми
тельно пользоваться при отладке на уровне исходного кода. Однако для команд
разработчиков ОС Microsoft этот отладчик ядра — единственный выбор.

WinDBG
WinDBG входит в состав Debugging Tools for Windows. Этот гибридный отладчик
можно задействовать и как отладчик режима ядра, и как отладчик пользовательс
кого режима, а при небольшой доработке WinDBG позволяет одновременно от
лаживать программы режима ядра и пользовательского режима. При отладке в
режиме ядра WinDBG предлагает все возможности KD, так как он обращается к
тому же отладочному ядру, что и KD. Однако WinDBG предоставляет графический
интерфейс, который вовсе не так легко задействовать, как отладчик Visual Studio
.NET, хоть и проще, чем KD. WinDBG позволяет отлаживать драйверы устройств
почти так же просто, как будто вы работаете с приложениями пользовательского
режима.

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

147

Как отладчик пользовательского режима WinDBG весьма хорош, и я настоя
тельно рекомендую, чтобы вы установили его. WinDBG предлагает гораздо боль
ше возможностей, чем отладчик Visual Studio .NET, так как предоставляет вам куда
больше сведений о вашем процессе. Однако за это надо платить: WinDBG слож
нее в использовании, чем отладчик Visual Studio .NET. И все же я бы посоветовал
вам потратить некоторое время и силы на изучение WinDBG, а я вам покажу клю
чевые возможности и приемы работы с ним в главе 8. Эти затраты окупятся за
счет того, что он поможет вам найти ошибку значительно быстрее, чем исполь
зуя отладчик Visual Studio .NET. Я провожу около 95% времени в отладчике Visual
Studio .NET, а остальное время — в WinDBG.

SoftICE
Этот отладчик режима ядра компании Compuware NuMega, как мне известно, —
единственный коммерческий отладчик режима ядра на рынке. Это также един
ственный отладчик режима ядра, работающий на одной машине. В отличие от
других отладчиков режима ядра SoftICE прекрасно отлаживает программы пользо
вательского режима. Как я уже говорил, отладчики режима ядра располагаются
между центральным процессором и ОС. SofICE также располагается между цент
ральным процессором и ОС при отладке программ пользовательского режима,
останавливая всю ОС.
Вас может не вдохновить то, что SoftICE может остановить ОС. Но давайте
рассмотрим такой случай. Что, если вам нужно отлаживать чувствительный к вре
менным задержкам код? При использовании такой функции API, как SendMessage
Timeout, вы легко выйдете за пределы этого времени, пока вы проходите по шагам
в другом потоке с помощью обычного отладчика с графическим интерфейсом.
Используя SoftICE, вы можете ходить от оператора к оператору сколь угодно дол
го, так как таймер, от которого зависит исполнение SendMessageTimeout, не будет
работать, пока вы работаете под SoftICE. SoftICE — единственный отладчик, по
зволяющий эффективно отлаживать многопоточные приложения. То, что SoftICE
останавливает всю ОС, когда он активен, означает, что разрешение проблем со
гласования времени производится гораздо проще.
То, что SoftICE располагается между центральным процессором и ОС, упрощает
и отладку межпроцессного взаимодействия. Если вы занимаетесь COMпрограм
мированием с множеством внешних серверов, вы можете просто устанавливать
точки прерывания во всех процессах и ходить по шагам между ними. Наконец, в
SoftICE вы запросто пройдете по шагам из пользовательского режима в режим ядра
и обратно.
Другое важное преимущество SoftICE над другими отладчиками в том, что в нем
собрана феноменальная коллекция информационных команд, которые позволя
ют увидеть практически все, что происходит в ОС. Хотя KD и WinDBG тоже име
ют солидный набор таких команд, в SoftICE их гораздо больше. В SoftICE вы мо
жете просмотреть практически все: от состояния всех событий синхронизации
до полной информации о HWND и расширенной информации о любом потоке си
стемы. SoftICE может рассказать вам все, что происходит в вашей системе.
Как можно ожидать, вся эта замечательная грубая сила имеет свою цену. SoftICE,
как и любой отладчик режима ядра, имеет весьма крутую кривую обучения, так

148

ЧАСТЬ II

Производительная отладка

как по существу он сам является ОС. Однако ваши затраты на обучение окупятся
с лихвой от предоставляемых им преимуществ.

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

Отладка Just-In-Time (JIT)
Из некоторых маркетинговых материалов по Visual Studio .NET может показать
ся, что в Visual Studio за JITотладкой скрывается чудо, однако чудеса происходят
в самой ОС. При отказе приложения Windows анализирует состояние раздела ре
естра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug, что
бы определить, какой отладчик ей вызвать для отладки приложения. Если этот раз
дел пуст, Windows XP выводит стандартное диалоговое окно аварийного завер
шения, а Windows 2000 — информационное окно с адресом аварийного завер
шения. Если в этом разделе реестра задано значение и заполнены остальные зна
чения, под Windows XP в левом нижнем углу становится активной кнопка Debug
(отладка), и вы получаете возможность отлаживать приложение. Под Windows 2000
доступна кнопка Cancel, позволяющая запустить отладчик.
JITотладка использует три следующих важных значения в разделе AeDebug
реестра:
쮿 Auto;
쮿 UserDebuggerHotKey;
쮿 Debugger.
Если Auto содержит значение 0 (нуль), ОС генерирует стандартное диалоговое
окно аварийного завершения и делает доступной кнопку Cancel, позволяя присо
единить отладчик. При значении 1 (единица) отладчик запускается автоматичес
ки. Если вы хотите свести с ума когонибудь из своих коллег, установите незамет
но на их системах значение Auto, равное 1, — они не будут понимать, почему при
каждом аварийном завершении приложения у них запускается отладчик. Значе
ние UserDebuggerHotKey идентифицирует «горячую» клавишу перехода к отладке (мы
очень скоро обсудим ее использование). Последнее и самое важное значение
Debugger указывает отладчик, который должна запускать ОС при аварийном завер
шении приложения. Есть только одно требование к отладчику: он должен поддер
живать присоединение к процессу. После обсуждения значения UserDebuggerHotKey
я объясню подробнее значение Debugger и его формат.

«Быстрые» клавиши прерывания и значение UserDebuggerHotKey
Иногда нужно переключиться на отладчик побыстрее. Если вы отлаживаете кон
сольное приложение, нажатие Ctrl+C или Ctrl+Break вызовет специальное исклю
чение DBG_CONTROL_C, которое переключит вас прямо в отладчик и позволит начать
отладку.

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

149

У ОС Windows есть милая возможность: в приложениях с графическим интер
фейсом вы можете переключиться на отладчик в любой момент времени. При
работе под отладчиком по умолчанию нажатие клавиши F12 заставляет вызвать
DebugBreak почти в тот момент, когда была нажата кнопка. Кстати, даже если вы
используете клавишу F12 как акселератор или иначе обрабатываете ввод с клавиа
туры сообщений для клавиши F12, вы все равно попадете в отладчик.
Клавиша прерывания по умолчанию — F12, но, если надо, можно указать и
другую. Значение UserDebuggerHotKey есть цифровое значение VK_*, соответствую
щее клавише, которую вы желаете применять как «горячую» клавишу отладчика.
Так, если вы хотите для переключения в отладчик задействовать Scroll Lock, уста
новите значение UserDebuggerHotKey в 0x91 и для вступления нового значения в силу
перезагрузите компьютер. Замечательной шуткой для ваших коллег может оказаться
замена значения UserDebuggerHotKey на 0x45 (латинская буква E) — каждый раз, когда
они нажмут клавишу E, программа переключится на отладчик. Однако я не несу
никакой ответственности, если ваши коллеги ополчатся на вас и сделают вашу
жизнь несчастной.

Значение Debugger
В разделе реестра AeDebug есть значение Debugger, которое и определяет основные
действия. Сразу после установки ОС значение Debugger выглядит похожим на строку,
передаваемую функции API wsprintf: drwtsn32 p %ld e %ld g. Так оно и есть: p является
идентификатором аварийно завершающегося процесса, а e — описатель собы
тия, нужный отладчику, чтобы сигнализировать, что в его цикле произошел вы
ход из первого потока. Сигнал об этом событии сообщает ОС, что отладчик ус
пешно присоединился к процессу. –g говорит программе Dr. Watson, что надо
продолжить выполнение программы после присоединения.
Вы всегда можете изменить значение Debugger, чтобы вызывать другой отлад
чик. Чтобы сделать отладчик Visual Studio .NET «родным» отладчиком, откройте
Visual Studio .NET и выберите Options из меню Tool. В диалоговом окне Options
выберите папку Debugging, затем — страницу свойств JustInTime и убедитесь, что
установлен флажок рядом с пунктом Native. Вы можете настроить WinDBG или
Dr. Watson своим предпочтительным отладчиком путем запуска из командной
строки WinDBG –I (заметьте: ключ чувствителен к регистру ввода) или DRWTSN32 –I.
Изменив значение Debugger, обязательно завершите Task Manager (Диспетчер за
дач), если он выполнялся. Диспетчер задач кэширует раздел реестра AeDebug во время
своей работы, поэтому, если вы попытаетесь отладить процесс из списка на стра
ничке Processes (Процессы) Диспетчера задач, отладчик может не заработать, если
предыдущим отладчиком был Visual Studio .NET.

Выбор отладчика, запускающегося при аварийном завершении
Хорошо иметь возможность оперативной отладки, когда отладчик вызывается при
аварийном завершении приложения, но здесь есть существенное ограничение: вы
можете иметь одновременно только один такой отладчик, задаваемый значени
ем Debugger. Как мы увидим в следующих главах, отладчики имеют сильные и сла
бые стороны в зависимости от конкретной ситуации. Ничего не может быть хуже,
если «выскочит» не тот отладчик, о котором вы знаете, что он позволит запросто
найти ошибку, которую вы несколько недель пытались воспроизвести.

150

ЧАСТЬ II

Производительная отладка

Это серьезная проблема, и я решил приложить руку к ее решению. Однако,
поскольку все, похожее на ошибку, запускает JITотладку под Visual Studio .NET, я
сделал много проб и ошибок, чтобы воплотить свою идею. Прежде всего расска
жу, как работает программа Debugger Chooser или, для краткости, DBGCHOOSER.
Идея, заложенная в DBGCHOOSER, состоит в том, что она работает как про
граммапрокладка, вызываемая при аварийном завершении отлаживаемой програм
мы и передающая настоящему отладчику информацию, нужную для отладки при
ложения. Для настройки DBGCHOOSER сначала скопируйте ее в каталог своего
компьютера, где она не может быть случайно удалена. ОС пытается запустить
отладчик, заданный значением Debugger раздела реестра AeDebug, и, если отладчик
недоступен, у вас не будет шансов отладить приложение в случае его аварийного
завершения. Для инициализации DBGCHOOSER просто запустите его (рис. 41).
Первый запуск DBGCHOOSER устанавливает умолчания, характерные для большин
ства машин программистов. Если какието ваши отладчики не указаны здесь, ука
жите их пути. Уделите особое внимание отладчику Visual Studio .NET, так как обо
лочка оперативного отладчика, используемая Visual Studio .NET, отсутствует в пути
по умолчанию. По щелчку кнопки OK в диалоговом окне настройки DBGCHOOSER
записывает параметры отладчика в INIфайл, хранящийся в каталоге Windows, и
настраивает себя отладчиком по умолчанию в разделе реестра AeDebug.

Рис. 41.

Диалоговое окно настройки DBGCHOOSER

Как только случится одно из редких (я надеюсь) аварийных завершений, пос
ле щелчка кнопки Debug диалогового окна аварийного завершения вы увидите
диалоговое окно выбора отладчика (рис. 42). Просто выберите нужный отлад
чик и начните отладку.
В реализации DBGCHOOSER нет ничего особенного. Первое, что может заин
тересовать, это то, что, когда вызывается CreateProcess для выбранного пользова
телем отладчика, нужно обеспечить установку флага наследования описателей в
TRUE. Чтобы с описателями все было классно, я заставил DBGCHOOSER ждать за
вершения порожденного отладчика. Таким образом, я знаю, что все наследуемые
описатели сохранены для отладчика. Хотя прийти к этой идее было труднее, чем
ее реализовать, чтобы заставить Visual Studio .NET правильно работать, пришлось
немного потрудиться. Все классно работало с WinDBG, Microsoft Visual C++ 6 и

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

151

Dr. Watson, но, когда я подошел к Visual Studio .NET (на самом деле к VS7JIT.EXE,
который в свою очередь вызывает отладчик Visual Studio .NET), стало выскакивать
сообщение, что JITотладка заблокирована и отладку запустить невозможно.

Рис. 42.

Диалоговое окно выбора отладчика программы DBGCHOOSER

Вопервых, я был в некотором замешательстве от того, что происходит, но с
помощью прекрасной программы мониторинга реестра Regmon от Марка Русси
новича и Брайса Когсвелла с www.sysinternals.com я увидел, что VS7JIT.EXE прове
рял значение Debugger раздела реестра AeDebug, установлен ли он как оперативный
отладчик. Если нет, выскакивало сообщение о том, что оперативная отладка за
блокирована. У меня была возможность проверить, что это так, остановив DBGC
HOOSER в отладчике, когда он был активизирован благодаря аварийному завер
шению, и изменив значение раздела реестра Debugger так, чтобы он указывал на
VS7JIT.EXE. Я не понимал, почему VS7JIT.EXE считает это столь важным, что он не
может заниматься отладкой, если он не является оперативным отладчиком. Я
быстренько написал в DBGCHOOSER, как обмануть VS7JIT.EXE путем подмены
значения Debugger на VS7JIT.EXE перед его порождением, и все в этом мире стало
прекрасно. Чтобы сделать DBGCHOOSER.EXE вновь оперативным отладчиком, я
создал поток, который ждет 5 секунд и восстанавливает значение Debugger.
Как я упоминал, когда завел речь о DBGCHOOSER, мое решение несовершен
но изза проблем в оперативном отладчике Visual Studio .NET. В Windows XP я
проверял различные варианты запуска и работы Visual Studio .NET, но нашел, что
VS7JIT.EXE прекращает свою работу. Поиграв с ним немного, я понял, что в дей
ствительности исполняются два экземпляра VS7JIT.EXE, в то время как Visual Studio
.NET запускается как оперативный отладчик. Один экземпляр порождает Visual
Studio .NET IDE (среду интерактивной разработки), а другой работает под DCOM
сервером RPCSS. В редких случаях, только при тестировании готовой реализации,
я приводил систему в состояние, когда попытка породить VS7JIT.EXE была безус
пешной, так как не мог запуститься экземпляр DCOM. В основном я сталкивался с
этой проблемой, работая над кодом восстановления значения Debugger раздела
реестра AeDebug. Идя по такому пути реализации DBGCHOOSER, я столкнулся с этой
проблемой пару раз, и только когда тестировал различные случаи одновремен
ного аварийного завершения нескольких процессов. Я не смог вычислить точную
причину и никогда не видел этого при нормальной работе.

152

ЧАСТЬ II

Производительная отладка

Автоматический запуск отладчика
(опции исполнения загружаемого модуля)
Трудней всего отлаживать приложения, запускаемые другими процессами. В эту
категорию попадают службы Windows и внепроцессные COMсерверы. Зачастую
можно вызвать APIфункцию DebugBreak, чтобы заставить отладчик присоединиться
к вашему процессу. Однако DebugBreak не работает с двумя экземплярами. Вопер
вых, иногда она не работает со службами Windows. Если вам надо отлаживать
процедуру запуска службы, то вызов DebugBreak позволит отладчику присоединиться,
но время, затраченное отладчиком на свой запуск, может превысить таймаут за
пуска службы, и Windows ее остановит. Вовторых, DebugBreak не работает, если
вам нужно отлаживать внепроцессный COMсервер. При вызове DebugBreak обра
ботчик ошибок COM обнаружит исключение, возникающее в точке прерывания
и прекратит выполнение внешнего COMсервера. К счастью, Windows позволяет
указать, что приложение должно запускаться в отладчике. Это позволяет запустить
отладчик прямо с первого оператора. Прежде чем разрешить эту возможность,
убедитесь, что при конфигурировании своей службы вы разрешили ей взаимодей
ствовать с рабочим столом.
Для настройки автоматической отладки лучше всего указать эту опцию в ре
дакторе реестра. В разделе HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\Current
Version\Image File Execution Options создайте свой раздел, имя которого совпадает
с именем файла вашего приложения. Так, если ваше приложение называется FOO.EXE,
создайте раздел реестра FOO.EXE. В разделе реестра вашего приложения создайте
новое строковое значение Debugger и введите в нем полный путь и имя файла
выбранного вами отладчика.
Теперь, когда вы запускаете свое приложение, отладчик запускается автомати
чески при загрузке приложения. Если надо указать отладчику какиелибо параметры
командной строки, укажите их также в значении Debugger. Например, если вы хо
тите задействовать WinDBG и автоматически инициировать отладку сразу после
запуска WinDBG, заполните Debugger строкой d:\windbg\windbg.exe g.
Для использования Visual Studio .NET в качестве предпочтительного отладчи
ка придется сделать немного больше. Первая проблема в том, что Visual Studio .NET
не может отлаживать исполняемый модуль без файла решения. Если вы разраба
тываете исполняемый модуль (иначе говоря, вы располагаете решением и исход
ным кодом), можете применить это решение. Однако последняя открытая ком
поновка и будет запускаться. Значит, если вам надо отлаживать поставляемую
компоновку (release build) или двоичный образ, для которого у вас нет исходно
го кода, откройте проект, настройте активное решение как Release и закройте
решение. Если вы не располагаете файлом решения для исполняемого файла, в
меню File выберите пункт Open Solution и откройте исполняемый образ как ре
шение. Запустите отладку и, когда появится запрос сохранения файла решения,
сохраните его.
Имея решение, вы сможете им пользоваться, а командная строка, указываемая
в параметре Debugger, будет выглядеть следующим образом. Если вы не добавили
вручную каталог Visual Studio .NET \ Common7\IDE к
системной переменной среды PATH, укажите полный путь и каталог для DEVENV.EXE.

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

153

Ключ /run командной строки DEVENV.EXE заставляет его начать отладку решения,
указанного в командной строке.

g:\vsnet\common7\ide\devenv /run d:\disk\output\wdbg.sln
Вторая проблема, с которой вы встретитесь, в том, что строковый параметр
Debugger может быть не более 65 символов в длину. Если вы установили Visual Studio
.NET по умолчанию, то почти наверняка путь будет очень длинным. Все, что вам
нужно сделать, — это поработать с командой SUBST и назначить пути к DEVENV.EXE
и вашему решению буквам устройств.
Ветераны могут помнить, что параметр Debugger легко установить с помощью
GFLAGS.EXE — небольшой утилиты, поставляемой вместе с WinDBG. Увы,
GFLAGS.EXE работает неправильно и принимает командную строку длиной толь
ко до 25 символов для параметра Debugger. В итоге проще всего создать раздел
реестра для процесса и параметр Debugger вручную.

Стандартный вопрос отладки
Мой шеф посылает мне так много почты, что я не могу ничего делать.
Есть ли какой-нибудь способ замедлить эту жуткую почту от ШСИ?
Хотя многие начальники «стараются сделать как лучше», их непрекращаю
щиеся сообщения по электронной почте могут отвлекать вас и не давать
вам работать. К счастью, есть простое решение, которое очень хорошо ра
ботает и даст вам около недели замечательного спокойствия, в результате
вы сможете работать, укладываясь в сроки. Чем менее технически опытны
шеф и администраторы сети, тем больше времени вы получите.
В предыдущем разделе я говорил о разделе реестра Image File Execution
Options и о том, что, когда вы настроите параметр Debugger вашего процес
са, процесс будет автоматически запускаться под отладчиком. Вот как из
бавиться от почты ШСИ (шефа, сидящего как на иголках).
1. Зайдите в кабинет шефа.
2. Откройте REGEDIT.EXE. Если шеф в кабинете, объясните ему, что вам надо
запустить на его машине утилиту, которая позволит ему получить доступ
к Webсервисам XML, над которыми вы работаете (на самом деле не важно,
создаете вы Webсервисы XML или нет — одни только эти модные сло
вечки заставят босса охотно предоставить вам возможность поковыряться
в его машине).
3. В разделе Image File Execution Options создайте раздел OUTLOOK.EXE (за
мените его на имя другой почтовой программы, если используется не
Microsoft Outlook). Скажите боссу, что вы делаете это для того, чтобы
предоставить ему почтовый доступ к Webсервисам XML.
4. Создайте параметр Debugger и введите значение SOL.EXE. Скажите шефу,
что SOL нужен для того, чтобы ваши Webсервисы XML получили доступ
к машинам Sun Solaris.
5. Закройте REGEDIT.EXE.
6. Скажите шефу, что у него все настроено и он может пользоваться Web
сервисами XML. Теперь главное — удалиться из кабинета с серьезным
см. след. стр.

154

ЧАСТЬ II

Производительная отладка

лицом. (Не дать себе рассмеяться во время этого эксперимента значи
тельно труднее, чем кажется, поэтому сначала попрактикуйтесь на сво
их коллегах!)
В этой ситуации вы всегонавсего сделали так, что при каждом запуске
шефом Outlook в действительности будет запускаться Косынка (Solitaire).
(Так как большинство руководителей все равно проводит свое рабочее время,
играя в Косынку, ваш шеф отвлечется на пару игр прежде, чем до него дой
дет, что он хотел запустить Outlook.) Возможно, он так и будет щелкать ярлык
Outlook, пока не откроет столько копий Косынки, что ему не хватит вирту
альной памяти и понадобится перезагрузить машину. После парочки таких
дней многократных циклов щелчков ярлыка и перезагрузки машины ваш
шеф вызовет к себе администратора сети посмотреть его машину.
Администратор возбудится, потому что теперь он имеет задачу поинте
реснее, чем сбрасывать пароли барышням из бухгалтерии. Он будет забав
ляться в кабинете шефа с его машиной по меньшей мере день, удерживая
таким образом шефа в стороне от машины. Если ктото спросит ваше мне
ние, вот готовый ответ: «Я слышал о странностях взаимодействия EJB и NTFS
через основы архитектуры DCOM, необходимой для доступа к MFT с исполь
зованием алгоритма сортировки методом наименьших квадратов». Админи
стратор заберет у шефа его машину и несколько дней будет развлекаться с
ней на своем рабочем месте. В конце концов он заменит жесткий диск и
переустановит все заново, на что уйдет еще деньдва. К тому времени, ког
да шеф получит свою машину обратно, у него скопится почта за четыре дня,
на разбор которой у него уйдет еще минимум один день, а вы можете спо
койно игнорировать сообщения еще день или два. Если же почта ШСИ опять
начинает учащаться, просто повторите вышеперечисленные шаги еще раз.
Важное замечание: вы используете этот метод на свой страх и риск.

MiniDBG — простой отладчик Win32
На первый взгляд, отладчик Win32 — простая программа, к которой предъявляет
ся всего парочка требований. Первое: отладчик должен устанавливать специаль
ный флаг DEBUG_ONLY_THIS_PROCESS в параметре dwCreationFlags функции CreateProcess.
Этот флаг сообщает ОС, что вызывающий поток должен войти в цикл отладки для
управления запущенным процессом. Если отладчик может управлять нескольки
ми процессами, порожденными изначальной отлаживаемой программой, он дол
жен указывать флаг DEBUG_PROCESS при создании процесса.
Поскольку используется вызов CreateProcess, отладчик и отлаживаемая программа
исполняются в разных процессах, благодаря чему устойчивость Win32систем в
процессе отладки весьма высока. Даже если отлаживаемая программа производит
беспорядочную запись в память, она все равно не сможет привести к сбою отлад
чика. (Отладчики 16разрядных версий Windows и ОС Macintosh до OS X весьма
чувствительны к повреждению отлаживаемых программ, поскольку исполняются
в одном процессе с ними.)

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

155

Второе требование: после запуска отлаживаемой программы отладчик должен
войти в свой цикл путем вызова функции API WaitForDebugEvent для приема отла
дочных уведомлений. Завершив обработку некоторого события отладки, он вы
зывает ContinueDebugEvent. Имейте в виду, что функции отладочного API могут быть
вызваны только тем потоком, что установил специальные флаги отладки при со
здании процесса путем вызова CreateProcess. Вот какой небольшой по объему код
нужен для создания отладчика Win32:

void main ( void )
{
CreateProcess ( ..., DEBUG_ONLY_THIS_PROCESS ,... ) ;
while ( 1 == WaitForDebugEvent ( ... ) )
{
if ( EXIT_PROCESS )
{
break ;
}
ContinueDebugEvent ( ... ) ;
}
}
Как видите, минимальный отладчик Win32 не требует многопоточности, пользо
вательского интерфейса или чеголибо еще. И все же, как и в большинстве Windows
приложений, разница между минимальным и приемлемым значительна. В действи
тельности отладочный API Win32 почти требует, чтобы цикл отладчика работал в
отдельном потоке. Как следует из имени, WaitForDebugEvent (ждать события отлад
ки) блокирует внутренние события ОС, пока отлаживаемая программа не выпол
нит действия, заставляющие ОС остановить исполнение отлаживаемой програм
мы, после чего ОС может сообщить отладчику об этом событии. Если отладчик
имеет единственный поток, то пользовательский интерфейс полностью заморо
жен, пока в отлаживаемой программе не возникнет событие отладки.
Все время в режиме ожидания отладчик принимает уведомления о событиях в
отлаживаемой программе. Следующая структура DEBUG_EVENT, заполняемая функцией
WaitForDebugEvent, содержит всю информацию о событии отладки (табл. 41):

typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;

156

ЧАСТЬ II

Производительная отладка

RIP_INFO RipInfo;
} u;
} DEBUG_EVENT
Табл. 4-1. События отладки
Событие отладки

Описание

CREATE_PROCESS_DEBUG_EVENT

Генерируется, когда в рамках отлаживаемого процесса
создается новый процесс или когда отладчик начинает
отладку уже активного процесса. Ядро системы генери
рует это событие отладки до начала выполнения процес
са в пользовательском режиме и до того, как ядро гене
рирует другие события отладки для нового процесса.
Структура DEBUG_EVENT содержит структуру
CREATE_PROCESS_DEBUG_INFO, содержащую описатель нового
процесса, описатель файла образа исполняемого про
цесса, описатель начального потока процесса и другую
информацию, описывающую процесс.
Описатель процесса имеет права доступа PROCESS_VM_READ
и PROCESS_VM_WRITE. Если отладчик имеет те же права до
ступа к описателю процесса, он может читать память
процесса и производить запись в нее через функции
ReadProcessMemory и WriteProcessMemory.
Описатель исполняемого файла процесса имеет права
доступа GENERIC_READ и открыт для совместного чтения.
Описатель начального потока процесса имеет права до
ступа к потоку THREAD_GET_CONTEXT, THREAD_SET_CONTEXT и
THREAD_SUSPEND_RESUME. Если отладчик имеет эти типы до
ступа к потоку, он читает регистры потока и записывает
в них с помощью функций GetThreadContext и
SetThreadContext, а также может приостанавливать поток
и возобновлять его исполнение с помощью функций
SuspendThread и ResumeThread.

CREATE_THREAD_DEBUG_EVENT

Генерируется, когда в отлаживаемом процессе создается
новый поток или когда начинается отладка уже активно
го процесса. Это событие отладки генерируется до того,
как новый поток начнет свое исполнение в пользова
тельском режиме.
Структура DEBUG_EVENT содержит структуру
CREATE_THREAD_DEBUG_INFO. Последняя содержит описатель
нового потока и его адрес запуска. Описатель имеет пра
ва доступа к потоку THREAD_GET_CONTEXT, THREAD_SET_CONTEXT
и THREAD_SUSPEND_RESUME. Если отладчик имеет эти же пра
ва, он может читать регистры потока и записывать в них
с помощью функций GetThreadContext и SetThreadContext,
а также приостанавливать исполнение потока и возоб
новлять его с помощью функций SuspendThread
и ResumeThread.

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

Табл. 4-1. События отладки

157

(продолжение)

Событие отладки

Описание

EXCEPTION_DEBUG_EVENT

Генерируется, когда в отлаживаемом процессе возникает
исключение. Возможные исключения включают попытку
обращения к недоступной памяти, исполнение операто
ра, на котором установлена точка прерывания, попытку
деления на 0 и любые другие исключения, перечислен
ные в разделе документации MSDN «Structured Exception
Handling» (структурная обработка исключений).
Структура DEBUG_EVENT содержит структуру EXCEPTION_DE
BUG_INFO. Последняя описывает исключение, вызвавшее
событие отладки.
Кроме стандартных условий возникновения исключе
ний, может происходить дополнительное исключение
в процессе отладки консольного приложения. При вводе
с консоли Ctrl+C ядро генерирует исключение
DBG_CONTROL_C для процессов, обрабатывающих в процессе
отладки сигнал Ctrl+C. Этот код исключения не предназ
начен для обработки в приложениях. Приложение ни
когда не должно иметь обработчик этого исключения.
Оно нужно только отладчику и применяется, только ког
да отладчик присоединен к консольному процессу.
Если процесс не находится в состоянии отладки или
если отладчик оставляет исключение DBG_CONTROL_C нео
бработанным, производится поиск списка функцийоб
работчиков исключений приложения. (О функцияхоб
работчиках исключений консольного процесса см. доку
ментацию MSDN по функции SetConsoleCtrlHandler.)

EXIT_PROCESS_DEBUG_EVENT

Возникает, когда завершается последний поток процесса
или вызывается функция ExitProcess. Оно возникает сра
зу после того, как ядро выгружает все DLL процесса и об
новляет код завершения процесса.
Структура DEBUG_EVENT содержит структуру EXIT_PRO
CESS_DEBUG_INFO, описывающую код завершения процесса.
При возникновении этого события отладчик освобожда
ет все внутренние структуры, ассоциированные с про
цессом. Описатель, указывающий в отладчике на завер
шающийся процесс и описатели всех потоков этого про
цесса, закрываются ядром. Отладчик не должен закры
вать эти описатели.

EXIT_THREAD_DEBUG_EVENT

Возникает, когда завершается поток, являющийся частью
отлаживаемого процесса. Ядро генерирует это событие
сразу после обновления кода завершения потока.
Структура DEBUG_EVENT содержит структуру EXIT_THREAD_DE
BUG_INFO, описывающую код завершения потока.
При возникновении этого события отладчик освобожда
ет все внутренние структуры, ассоциированные с пото
ком. Описатель, указывающий в отладчике на завершаю
щийся процесс, закрывается системой. Отладчик не дол
жен закрывать этот описатель.

см. след. стр.

158

ЧАСТЬ II

Производительная отладка

Табл. 4-1. События отладки
Событие отладки

(продолжение)

Описание
Событие отладки не возникает, если завершающийся
поток является последним потоком процесса. В этом
случае вместо него возникает событие отладки
EXIT_PROCESS_DEBUG_EVENT.

LOAD_DLL_DEBUG_EVENT

Возникает при загрузке DLL отлаживаемым процессом.
Это событие возникает, когда системный загрузчик раз
решает ссылки на DLL или когда отлаживаемый процесс
вызывает функцию LoadLibrary, а также при каждой за
грузке DLL в адресное пространство процесса. Если счет
чик ссылок на DLL уменьшается до 0, DLL выгружается.
При следующей загрузке DLL снова возникает это
событие.
Структура DEBUG_EVENT содержит структуру LOAD_DLL_DE
BUG_INFO, которая включает описатель файла вновь загру
женной DLL, ее базовый адрес и другие данные, описы
вающие DLL.
Обычно при обработке этого события отладчик загружа
ет таблицу символов, ассоциированную с DLL.

OUTPUT_DEBUG_STRING_E VENT

Возникает, когда отлаживаемый процесс обращается к
функции OutputDebugString.
Структура DEBUG_EVENT содержит структуру OUTPUT_DE
BUG_STRING_INFO, которая описывает адрес, размер и фор
мат отладочной строки.

UNLOAD_DLL_DEBUG_EVENT

Возникает, когда отлаживаемый процесс выгружает DLL
с помощью функции FreeLibrary. Это событие возникает
только при последней выгрузке DLL из адресного про
странства процесса (т. е. когда счетчик ссылок на DLL
станет равным 0).
Структура DEBUG_EVENT содержит структуру UNLOAD_DLL_DE
BUG_INFO, которая описывает базовый адрес DLL в адрес
ном пространстве процесса, выгружающего DLL.
Обычно при получении этого события отладчик выгру
жает таблицу символов, ассоциированную с DLL.
При завершении процесса ядро автоматически выгружа
ет все DLL процесса, но не генерирует событие отладки
UNLOAD_DLL_DEBUG_EVENT.

При обработке событий отладки, возвращаемых функцией WaitForDebugEvent,
отладчик полностью управляет отлаживаемой программой, так как ОС останав
ливает все потоки отлаживаемой программы и не управляет ими, пока не вызва
на функция ContinueDebugEvent. Если отладчику нужно читать из адресного простран
ства отлаживаемой программы или записывать в него, он может вызвать функ
ции ReadProcessMemory и WriteProcessMemory. Если память имеет атрибут «только для
чтения», можно использовать функцию VirtualProtect, чтобы изменить уровень
защиты при необходимости произвести запись в эту часть памяти. Если отладчик
редактирует код отлаживаемой программы, используя вызовы функции Write
ProcessMemory, надо вызывать функцию FlushInstructionCache для очистки кэша ко

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

159

манд для этой части памяти. Если вы забыли вызвать FlushInstructionCache, ваши
изменения смогут работать, только если эта память не кэшируется центральным
процессором. Если память уже кэширована центральным процессором, измене
ния не вступят в силу до повторного считывания в кэш центрального процессо
ра. Вызов FlushInstructionCache особенно важен в многопроцессорных машинах.
Если отладчику нужно получить или установить текущий контекст отлаживаемой
программы или регистров центрального процессора, он может вызвать GetThread
Context или SetThreadContext.
Единственным событием отладки Win32, которому требуется особая обработ
ка, является точка прерывания загрузчика, или начальная точка прерывания. После
того как ОС посылает первые уведомления CREATE_PROCESS_DEBUG_EVENT и LOAD_DLL_DE
BUG_EVENT для неявно загруженных модулей, отладчик принимает EXCEPTION_DEBUG_EVENT.
Это событие отладки и является точкой прерывания загрузчика. Отлаживаемая
программа исполняет эту точку прерывания, так как CREATE_PROCESS_DEBUG_EVENT
указывает только, что процесс загружен, а не что он исполняется. Точка прерыва
ния загрузчика, которую ОС заставляет сработать при каждой загрузке отлажива
емой программы, является тем первым событием, благодаря которому отладчик
узнает, что отлаживаемая программа уже исполняется. В настоящих отладчиках
инициализация основных структур данных, таких как таблицы символов, проис
ходит при создании процесса, и отладчик начинает показывать дизассемблиро
ванный код или редактировать код отлаживаемой программы в точке прерыва
ния загрузчика.
При возникновении точки прерывания загрузчика отладчик должен зарегист
рировать, что он «видел» точку прерывания и может обрабатывать все остальные
точки прерывания. Вся остальная обработка первой точки прерывания (а в об
щем, и остальных точек) зависит от типа центрального процессора. Для семей
ства Intel Pentium отладчик должен продолжить исполнение путем вызова функ
ции ContinueDebugEvent с указанием флага DBG_CONTINUE, что позволит продолжить
исполнение отлаживаемой программы.
Листинг 41 демонстрирует MinDBG — минимальный отладчик, доступный в
наборе файлов к этой книге. MinDBG обрабатывает все события отладки и пра
вильно исполняет отлаживаемый процесс. Кроме того, он показывает, как присо
единиться к существующему процессу и отсоединиться от отлаживавшегося про
цесса. Для запуска процесса под MinDBG передайте имя процесса в командной
строке с нужными отлаживаемой программе параметрами. Для присоединения к
существующему процессу и его отладки, укажите в командной строке десятичный
идентификатор процесса, предварив его символом «минус» («–»). Так, если иден
тификатор процесса равен 3245, вам надо передать в командной строке –3245,
чтобы заставить отладчик присоединиться к этому процессу. Если вы работаете
под Windows XP/Server 2003 и более поздними системами, можете отсоединить
ся от процесса простым нажатием Ctrl+Break. Имейте в виду, что при работе с
MinDBG на самом деле обработчики событий отладки не делают ничего, кроме
как показывают некоторую базовую информацию. Превращение минимального
отладчика в настоящий потребует значительных усилий.

160

ЧАСТЬ II

Листинг 4-1.

Производительная отладка

MINDBG.CPP

/*————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright (c) 19972003 John Robbins — All rights reserved.
Самый простой в мире отладчик программ Win32
——————————————————————————————————————————————————————————————————————*/
/*//////////////////////////////////////////////////////////////////////
// Обычные включаемые файлы.
//////////////////////////////////////////////////////////////////////*/
#include "stdafx.h"
/*//////////////////////////////////////////////////////////////////////
// Прототипы и типы.
//////////////////////////////////////////////////////////////////////*/
// Показывает минимальную справку.
void ShowHelp ( void ) ;
// Обработчик нажатия Break.
BOOL WINAPI CtrlBreakHandler ( DWORD dwCtrlType ) ;
// Функции отображения.
void DisplayCreateProcessEvent ( CREATE_PROCESS_DEBUG_INFO & stCPDI ) ;
void DisplayCreateThreadEvent ( DWORD
dwTID ,
CREATE_THREAD_DEBUG_INFO & stCTDI ) ;
void DisplayExitThreadEvent ( DWORD
dwTID ,
EXIT_THREAD_DEBUG_INFO & stETDI ) ;
void DisplayExitProcessEvent ( EXIT_PROCESS_DEBUG_INFO & stEPDI ) ;
void DisplayDllLoadEvent ( HANDLE
hProcess ,
LOAD_DLL_DEBUG_INFO & stLDDI
) ;
void DisplayDllUnLoadEvent ( UNLOAD_DLL_DEBUG_INFO & stULDDI ) ;
void DisplayODSEvent ( HANDLE
hProcess ,
OUTPUT_DEBUG_STRING_INFO & stODSI
) ;
void DisplayExceptionEvent ( EXCEPTION_DEBUG_INFO & stEDI ) ;
// Определение типа для DebugActiveProcessStop.
typedef BOOL (WINAPI *PFNDEBUGACTIVEPROCESSSTOP)(DWORD) ;
/*//////////////////////////////////////////////////////////////////////
// Глобальные переменные области видимости файла.
//////////////////////////////////////////////////////////////////////*/
// Флаг, показывающий необходимость отсоединения.
static BOOL g_bDoTheDetach = FALSE ;
/*//////////////////////////////////////////////////////////////////////
// Точка входа.
//////////////////////////////////////////////////////////////////////*/
void _tmain ( int argc , TCHAR * argv[ ] )
{

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

161

// Проверка наличия аргументов в командной строке.
if ( 1 == argc )
{
ShowHelp ( ) ;
return ;
}
// Необходим достаточно большой буфер для команды
// или параметров командной строки.
TCHAR szCmdLine[ MAX_PATH + MAX_PATH ] ;
// Идентификатор процесса, если производится присоединение к нему.
DWORD dwPID = 0 ;
szCmdLine[ 0 ] = _T ( '\0' ) ;
// Проверка, начинается ли командная строка со знака "", так как это
// означает идентификатор процесса, к которому мы присоединяемся.
if ( _T ( '' ) == argv[1][0] )
{
// Попытка вычленить идентификатор процесса из командной строки.
// Передвинуться за символ '' в строке.
TCHAR * pPID = argv[1] + 1 ;
dwPID = _tstol ( pPID ) ;
if ( 0 == dwPID )
{
_tprintf ( _T ( "Invalid PID value : %s\n" ) , pPID ) ;
return ;
}
}
else
{
dwPID = 0 ;
// Я собираюсь запустить процесс.
for ( int i = 1 ; i < argc ; i++ )
{
_tcscat ( szCmdLine , argv[ i ] ) ;
if ( i < argc )
{
_tcscat ( szCmdLine , _T ( " " ) ) ;
}
}
}
// Место для возвращаемого значения.
BOOL bRet = FALSE ;
// Установить обработчик CTRL+BREAK.
bRet = SetConsoleCtrlHandler ( CtrlBreakHandler , TRUE ) ;
см. след. стр.

162

ЧАСТЬ II

Производительная отладка

if ( FALSE == bRet )
{
_tprintf ( _T ( "Unable to set CTRL+BREAK handler!\n" ) ) ;
return ;
}
// Если идентификатор процесса равен 0, я запускаю процесс.
if ( 0 == dwPID )
{
// Попытаемся запустить отлаживаемый процесс. Этот вызов функции
// выглядит, как обычный вызов CreateProcess, кроме специального
// необязательного флага DEBUG_ONLY_THIS_PROCESS.
STARTUPINFO
stStartInfo
;
PROCESS_INFORMATION stProcessInfo ;
memset ( &stStartInfo , NULL , sizeof ( STARTUPINFO
));
memset ( &stProcessInfo , NULL , sizeof ( PROCESS_INFORMATION));
stStartInfo.cb = sizeof ( STARTUPINFO ) ;
bRet = CreateProcess ( NULL
szCmdLine
NULL
NULL
FALSE
CREATE_NEW_CONSOLE |
DEBUG_ONLY_THIS_PROCESS
NULL
NULL
&stStartInfo
&stProcessInfo

,
,
,
,
,
,
,
,
,
) ;

// Не забудьте закрыть описатели процесса и потока,
// возвращаемые CreateProcess.
VERIFY ( CloseHandle ( stProcessInfo.hProcess ) ) ;
VERIFY ( CloseHandle ( stProcessInfo.hThread ) ) ;
// Посмотрим, запустился ли процесс отлаживаемой программы.
if ( FALSE == bRet )
{
_tprintf ( _T ( "Unable to start %s\n" ) , szCmdLine ) ;
return ;
}
// Сохранить идентификатор процесса на случай
// необходимости отсоединения.
dwPID = stProcessInfo.dwProcessId ;
}
else
{

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

163

bRet = DebugActiveProcess ( dwPID ) ;
if ( FALSE == bRet )
{
_tprintf ( _T ( "Unable to attach to %u\n" ) , dwPID ) ;
return ;
}
}
// Отлаживаемая программ запущена, поэтому запускаем цикл отладчика.
DEBUG_EVENT stDE
;
BOOL
bSeenInitialBP = FALSE ;
BOOL
bContinue
= TRUE ;
HANDLE
hProcess
= INVALID_HANDLE_VALUE ;
DWORD
dwContinueStatus
;
// Цикл до тех пор, пока не потребуется остановиться.
while ( TRUE == bContinue )
{
// Пауза до возникновения события отладки.
BOOL bProcessDbgEvent = WaitForDebugEvent ( &stDE , 100 ) ;
if ( TRUE == bProcessDbgEvent )
{
// Обработка конкретных событий отладки.
// Так как MinDBG — это только минимальный отладчик,
// он обрабатывает только несколько событий.
switch ( stDE.dwDebugEventCode )
{
case CREATE_PROCESS_DEBUG_EVENT :
{
DisplayCreateProcessEvent(stDE.u.CreateProcessInfo);
// Сохраним описатель, который понадобится позже.
// Заметьте: вы не можете закрыть этот описатель.
// Если вы это сделаете, CloseHandle завершится с ошибкой.
hProcess = stDE.u.CreateProcessInfo.hProcess ;
//
//
//
//

Описатель файла можно закрыть безболезненно.
Если вы закроете поток, CloseHandle провалится
глубоко в ContinueDebugEvent, когда вы будете
завершать приложение.
VERIFY(CloseHandle(stDE.u.CreateProcessInfo.hFile));

dwContinueStatus = DBG_CONTINUE ;
}
break ;
case EXIT_PROCESS_DEBUG_EVENT :
{
DisplayExitProcessEvent ( stDE.u.ExitProcess ) ;
bContinue = FALSE ;
dwContinueStatus = DBG_CONTINUE ;
см. след. стр.

164

ЧАСТЬ II

Производительная отладка

}
break ;
case LOAD_DLL_DEBUG_EVENT
:
{
DisplayDllLoadEvent ( hProcess , stDE.u.LoadDll ) ;
// Не забудьте закрыть описатель соответствующего файла.
VERIFY ( CloseHandle( stDE.u.LoadDll.hFile ) ) ;
dwContinueStatus = DBG_CONTINUE ;
}
break ;
case UNLOAD_DLL_DEBUG_EVENT :
{
DisplayDllUnLoadEvent ( stDE.u.UnloadDll ) ;
dwContinueStatus = DBG_CONTINUE ;
}
break ;
case CREATE_THREAD_DEBUG_EVENT :
{
DisplayCreateThreadEvent ( stDE.dwThreadId
,
stDE.u.CreateThread ) ;
// Заметьте, что вы не можете закрыть описатель потока.
// Если вы это сделаете, CloseHandle провалится глубоко
// в ContinueDebugEvent.
dwContinueStatus = DBG_CONTINUE ;
}
break ;
case EXIT_THREAD_DEBUG_EVENT
:
{
DisplayExitThreadEvent ( stDE.dwThreadId ,
stDE.u.ExitThread ) ;
dwContinueStatus = DBG_CONTINUE ;
}
break ;
case OUTPUT_DEBUG_STRING_EVENT :
{
DisplayODSEvent ( hProcess , stDE.u.DebugString ) ;
dwContinueStatus = DBG_CONTINUE ;
}
break ;
case EXCEPTION_DEBUG_EVENT
:
{
DisplayExceptionEvent ( stDE.u.Exception ) ;

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

165

// Единственное исключение, требующее специальной
// обработки, — это точка прерывания загрузчика.
switch(stDE.u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_BREAKPOINT :
{
// Если возникает исключение по точке прерывания
// и оно первое, я продолжаю свое веселье, иначе
// я передаю исключение отлаживаемой программе.
if ( FALSE == bSeenInitialBP )
{
bSeenInitialBP = TRUE ;
dwContinueStatus = DBG_CONTINUE ;
}
else
{
// Хьюстон, у нас проблема!
dwContinueStatus =
DBG_EXCEPTION_NOT_HANDLED ;
}
}
break ;
// Все остальные исключения передаем
// отлаживаемой программе.
default
:
{
dwContinueStatus =
DBG_EXCEPTION_NOT_HANDLED ;
}
break ;
}
}
break ;
// Для всех остальных событий – просто продолжаем.
default
:
{
dwContinueStatus = DBG_CONTINUE ;
}
break ;
}
// Передаем управление ОС.
#ifdef _DEBUG
BOOL bCntDbg =
#endif
ContinueDebugEvent ( stDE.dwProcessId ,
stDE.dwThreadId ,
dwContinueStatus ) ;
см. след. стр.

166

ЧАСТЬ II

Производительная отладка

ASSERT ( TRUE == bCntDbg ) ;
}
// Необходимо ли отсоединение?
if ( TRUE == g_bDoTheDetach )
{
// Отсоединение работает только в XP или более поздней версии,
// поэтому я должен выполнить GetProcAddress, чтобы найти
// DebugActiveProcessStop.
bContinue = FALSE ;
HINSTANCE hKernel32 =
GetModuleHandle ( _T ( "KERNEL32.DLL" ) ) ;
if ( 0 != hKernel32 )
{
PFNDEBUGACTIVEPROCESSSTOP pfnDAPS =
(PFNDEBUGACTIVEPROCESSSTOP)
GetProcAddress ( hKernel32
,
"DebugActiveProcessStop" ) ;
if ( NULL != pfnDAPS )
{
#ifdef _DEBUG
BOOL bTemp =
#endif
pfnDAPS ( dwPID ) ;
ASSERT ( TRUE == bTemp ) ;
}
}
}
}
}
/*//////////////////////////////////////////////////////////////////////
// Мониторы обработки Ctrl+Break
//////////////////////////////////////////////////////////////////////*/
BOOL WINAPI CtrlBreakHandler ( DWORD dwCtrlType )
{
// Я буду обрабатывать только Ctrl+Break.
// Все другое убивает отлаживаемую программу.
if ( CTRL_BREAK_EVENT == dwCtrlType )
{
g_bDoTheDetach = TRUE ;
return ( TRUE ) ;
}
return ( FALSE ) ;
}
/*//////////////////////////////////////////////////////////////////////
// Отображает справку к программе.
//////////////////////////////////////////////////////////////////////*/

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

167

void ShowHelp ( void )
{
_tprintf ( _T ( "Start a program to debug:\n" )
_T ( " MinDBG " )
_T ( " lm
start
end
00400000 0040a000
10200000 10287000
10480000 1053c000
60000000 6004a000
6d510000 6d58d000
70a70000 70ad4000
71950000 71a34000
77c00000 77c07000
77c10000 77c63000
77c70000 77cb0000
77d40000 77dc6000
77dd0000 77e5d000
77e60000 77f46000
77f50000 77ff7000
78000000 78086000

Производительная отладка

module name
AssertTest (deferred)
MSVCR71D (deferred)
MSVCP71D (deferred)
BugslayerUtil (deferred)
dbghelp
(deferred)
SHLWAPI
(deferred)
COMCTL32 (deferred)
VERSION
(deferred)
msvcrt
(deferred)
GDI32
(deferred)
USER32
(deferred)
ADVAPI32 (deferred)
kernel32 (deferred)
ntdll
(pdb symbols)
\\zeno\WebSymbols\ntdll.pdb\3D6DE29B2\ntdll.pdb
RPCRT4
(deferred)

Так как загрузка символов занимает огромный объем памяти, WinDBG исполь
зует отложенную загрузку символов, т. е. загружает символы, только когда они
нужны. Поскольку исходное предназначение WinDBG — отлаживать ОС, загрузка
всех символов ОС при первом присоединении к ядру системы сделает WinDBG
бесполезным. Таким образом, только что приведенный пример показывает, что я
загрузил только символы NTDLL.DLL. Остальные помечены как «deferred» (отло
жены), потому что у WinDBG нет причин получать доступ к ним. Если б я загру
зил файл исходного текста ASSERTTEST.EXE и нажал F9 для установки точки пре
рывания на строке, WinDBG начал бы загрузку этих символов, пока не нашел бы
нужный в этом файле. Вот зачем нужно информационное окно с запросом необ
ходимости загрузки символов. Однако на уровне командной строки вы можете бо
лее тонко управлять выбором загружаемых символов.
Чтобы заставить загрузить символ, команда LD (Load Symbols — загрузить сим
волы) делает небольшой трюк. LD принимает только имя файла в командной строке,
поэтому, чтобы загрузить символы программы ASSERTTEST.EXE, я ввел ld asserttest
и получил такой результат:

0:000> ld asserttest
*** WARNING: Unable to verify checksum for AssertTest.exe
Symbols loaded for AssertTest
WinDBG весьма обстоятелен при работе с символами и сообщает о символах
все, что может быть потенциально ошибочным. Так как я использую отладочную
версию ASSERTTEST.EXE, то у меня не был задан ключ /RELEASE при сборке про
граммы, отключающий инкрементальную компоновку. Как я говорил в главе 2,
ключ /RELEASE называется неправильно, он должен бы называться /CHECKSUM, так как
он лишь добавляет контрольную сумму к двоичному файлу и PDBфайлу.
Чтобы загрузить все символы, укажите символ «звездочка» как параметр команды
LD: ld *. Порывшись в документации WinDBG, вы увидите другую команду — RELOAD
(Reload Module — перезагрузить модуль), которая в сущности делает то же, что и
LD. Для загрузки всех символов с помощью .RELOAD, задайте параметр /f: .RELOAD /f.

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

333

Если вы отлаживаете большую программу, .RELOAD может оказаться немного по
лезнее, так как она будет сообщать только о тех модулях, у которых имеются про
блемы с символами, тогда как LD покажет результат загрузки каждого модуля. В
любом случае вы сразу узнаете, какие символы некорректны.
Вы также можете проверить правильность загрузки символов командой LM. После
загрузки всех символов LM выводит следующее (я перенес последний элемент каждой
строки на следующую строку, чтобы все поместилось по ширине на странице):

0:000> lm
start
end
00400000 0040a000
10200000 10287000
10480000 1053c000
60000000 6004a000
6d510000 6d58d000

70a70000 70ad4000
71950000 71a34000

77c00000 77c07000
77c10000 77c63000
77c70000 77cb0000
77d40000 77dc6000
77dd0000 77e5d000
77e60000 77f46000
77f50000 77ff7000
78000000 78086000

module name
AssertTest C (pdb symbols)
D:\Dev\BookTwo\Disk\Output\AssertTest.pdb
MSVCR71D
(pdb symbols)
e:\winnt\system32\msvcr71d.pdb
MSVCP71D
(pdb symbols)
e:\winnt\system32\msvcp71d.pdb
BugslayerUtil C (pdb symbols)
D:\Dev\BookTwo\Disk\Output\BugslayerUtil.pdb
dbghelp
(pdb symbols)
\\zeno\WebSymbols\dbghelp.pdb\
819C4FBAB64844F3B86D0AEEDDCE632A1\dbghelp.pdb
SHLWAPI
(pdb symbols)
\\zeno\WebSymbols\shlwapi.pdb\3D6DE26F2\shlwapi.pdb
COMCTL32
(pdb symbols)
\\zeno\WebSymbols\MicrosoftWindowsCommonControls
60100comctl32.pdb\3D6DD9A81\
MicrosoftWindowsCommonControls
60100comctl32.pdb
VERSION
(pdb symbols)
e:\winnt\symbols\dll\version.pdb
msvcrt
(pdb symbols)
\\zeno\WebSymbols\msvcrt.pdb\3D6DD5921\msvcrt.pdb
GDI32
(pdb symbols)
\\zeno\WebSymbols\gdi32.pdb\3D6DE59F2\gdi32.pdb
USER32
(pdb symbols)
\\zeno\WebSymbols\user32.pdb\3DB6D4ED1\user32.pdb
ADVAPI32
(pdb symbols)
\\zeno\WebSymbols\advapi32.pdb\3D6DE4CE2\advapi32.pdb
kernel32
(pdb symbols)
\\zeno\WebSymbols\kernel32.pdb\3D6DE6162\kernel32.pdb
ntdll
(pdb symbols)
\\zeno\WebSymbols\ntdll.pdb\3D6DE29B2\ntdll.pdb
RPCRT4
(pdb symbols)
\\zeno\WebSymbols\rpcrt4.pdb\3D6DE2F92\rpcrt4.pdb

Буква «C» после имени модуля указывает, что в модуле или файле символов
отсутствует контрольная сумма символов. Символ «решетка» после имени модуля
указывает, что символы в файле символов и исполняемом файле не соответству
ют друг другу. Да, WinDBG загрузит символы посвежее, даже если это неправиль
но. В предыдущем примере жизнь хороша, и все символы коррректны. Однако со

334

ЧАСТЬ II

Производительная отладка

вершенно нормально, что «решетка» стоит рядом с COMCTL32.DLL. Это потому,
что он, видимо, меняется с каждым пакетом обновления, исправляющим ошибку
защиты в Microsoft Internet Explorer, и шансы получить в распоряжение коррект
ную таблицу символов для COMCTL32.DLL почти нулевые. Чтобы поточнее узнать,
какие модули и соответствующие файлы символов загружены, укажите v в коман
де LM. Чтобы показать единственный модуль в следующем примере, я задал пара
метр m для выбора конкретного модуля.

0:000> lm v m gdi32
start
end
module name
77c70000 77cb0000 GDI32
(pdb symbols)
\\zeno\WebSymbols\
gdi32.pdb\3D6DE59F2\gdi32.pdb
Loaded symbol image file: E:\WINNT\system32\GDI32.dll
Image path: E:\WINNT\system32\GDI32.dll
Timestamp: Thu Aug 29 06:40:39 2002 (3D6DFA27) Checksum: 0004285C
File version:
5.1.2600.1106
Product version: 5.1.2600.1106
File flags:
0 (Mask 3F)
File OS:
40004 NT Win32
File type:
2.0 Dll
File date:
00000000.00000000
CompanyName:
Microsoft Corporation
ProductName:
Microsoft® Windows® Operating System
InternalName:
gdi32
OriginalFilename: gdi32
ProductVersion: 5.1.2600.1106
FileVersion:
5.1.2600.1106 (xpsp1.0208281920)
FileDescription: GDI Client DLL
LegalCopyright: © Microsoft Corporation. All rights reserved.
Чтобы точно узнать, где WinDBG загружает символы и почему, расширенная
команда !sym предлагает параметр noisy. Вывод в окнах Command показывает, через
что проходит сервер символов WinDBG, чтобы найти и загрузить символы. Во
оружившись этими результатами, вы сможете решить всевозможные проблемы за
грузки символов, с которыми столкнетесь. Чтобы отключить многословный вы
вод, исполните команду !sym quiet.
И последнее о символах. WinDBG имеет встроенный браузер символов. Коман
да X (Examine Symbols — проверить символы) позволяет просматривать символы
глобально, применительно к модулю или в локальном контексте. Указав формат
module!symbol, вы избавите себя от выслеживания места хранения символа. Кроме
того, команда X не чувствительна к регистру, что упрощает жизнь. Чтобы увидеть
адрес символа LoadLibraryW в памяти, введите:

0:000> x kernel32!LoadLibraryw
77e8a379 KERNEL32!LoadLibraryW
Формат module!symbol поддерживает «звездочку», поэтому, если вы хотите, на
пример, увидеть в модуле KERNEL32.DLL чтолибо, имеющее «lib» в имени симво

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

335

ла, введите x kernel32!*Lib*, что хорошо работает и тоже не чувствительно к ре
гистру. Чтобы увидеть все символы модуля, напишите «звездочку» вместо имени
символа. Использование «звездочки» в качестве параметра приведет к выводу ло
кальных переменных в текущей области видимости, что идентично команде DV
(Display Variables — отобразить переменные), которую мы обсудим в разделе «Про
смотр и вычисление переменных».

Процессы и потоки
Разобравшись с символами, можно перейти к запуску процессов под управлени
ем WinDBG. Подобно Visual Studio .NET, WinDBG способен отлаживать одновре
менно любое количество процессов. Немного интереснее его делает то, что вы
располагаете лучшим контролем над отлаживаемыми процессами, порожденны
ми из отлаживаемого процесса.

Отладка дочерних процессов
В самом низу диалогового окна Open Executable (рис. 82) имеется флажок Debug
Child Processes Also (отлаживать также и дочерний процесс). Установив его, вы
сообщаете WinDBG, что вы также хотите отлаживать любые процессы, запущен
ные отлаживаемым процессом. При работе под Microsoft Windows XP/Server 2003,
если вы забыли установить этот флажок при открытии процесса, вы можете из
менить этот параметр «на лету» командой .CHILDDBG (Debug Child Processes — от
лаживать дочерний процесс). Собственно .CHILDDBG сообщит вам текущее состоя
ние. Команда .CHILDDBG 1 включит отладку дочерних процессов, а .CHILDDBG 0 от
ключает ее.
Чтобы показать возможности работы со многими процессами и потоками, я
приведу несколько результирующих распечаток отладки процессора командной
строки (CMD.EXE). После того как CMD.EXE начнет исполняться, я запущу NOTE
PAD.EXE. Если вы проделаете те же шаги при разрешенной отладке дочерних про
цессов, как только загрузите NOTEPAD.EXE, WinDBG остановится на точке преры
вания загрузчика для NOTEPAD.EXE. То, что WinDBG остановил NOTEPAD.EXE, —
логично, но это останавливает и CMD.EXE, так как оба процесса теперь работают
совместно в одном цикле отладки.
Чтобы увидеть в графическом интерфейсе исполняющиеся сейчас процессы,
выберите Processes And Threads (процессы и потоки) из меню View. Вы увидите
нечто вроде того, что изображено на рис. 83. В окне Processes And Threads про
цессы изображены как корневые узлы, а потоки процессов — как дочерние. Чис
ла рядом с CMD.EXE (000:9AC) являются номером процесса WinDBG, после кото
рого указан идентификатор процесса Win32. Для CMD.EXE поток 000:9B0 обозна
чает идентификатор потока WinDBG и идентификатор потока Win32. Номера
процессов и потоков WinDBG уникальны в течение всего времени работы WinDBG.
Это значит, что никогда не может появиться другой процесс с номером 1, пока я
не перезапущу WinDBG. Номера процессов и потоков WinDBG важны, так как они
служат для установки точек прерывания для процессов и потоков, а также могут
использоваться в качестве модификаторов в командах.

336

Рис. 83.

ЧАСТЬ II

Производительная отладка

Окно Processes And Threads

Просмотр процессов и потоков в окне Command
Все, что WinDBG отображает в окне, позволяет просмотреть соответствующая
команда окна Command. Для просмотра процессов и потоков служит команда |
(Process Status — состояние процесса). Результат работы для двух процессов, по
казанных на рис. 83, выглядит так:

1:001> |
0
id: 9ac
. 1
id: 3d0

create
child

name: cmd.exe
name: notepad.exe

Точка в левой позиции индицирует активный процесс, т. е. все вводимые вами
команды будут работать с этим процессом. Другое интересное поле показывает,
как был запущен процесс в отладчике. «Create» означает, что процесс создан Win
DBG, а «child» — процесс, порожденный родительским процессом.
Перегруженная команда S имеет два варианта: |S (Set Current Process — уста
новить текущий процесс), а ~S (Set Current Thread — установить текущий поток)
изменяет текущий активный процесс. К вашим услугам также окно Processes And
Threads (процессы и потоки), вызываемое двойным щелчком процесса, который
вы хотите сделать активным. Полужирным начертанием выделен активный про
цесс. Используя команду S, необходимо задать процесс в виде префикса коман
ды. Так, для переключения со второго процесса на первый, нужно ввести |0s. Чтобы
выяснить, какой процесс активен, взгляните на крайние слева номера строки ввода
окна Command. При смене процессов номера меняются. В примере с CMD.EXE и
NOTEPAD.EXE при переключении на первый процесс путем повторной выдачи
команды | результат выглядит немного иначе:

0:000> |
. 0
id: 9ac
# 1
id: 3d0

create
child

name: cmd.exe
name: notepad.exe

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

337

Разница — в символе «#» перед процессом NOTEPAD.EXE. Символ «#» указывает
процесс, вызвавший исключение, остановившее его в WinDBG. Так как NOTEPAD.EXE
находится на точке прерывания, то последняя и является причиной исключения.
Просмотр потоков почти идентичен просмотру процессов. Я собираюсь запу
стить NOTEPAD.EXE, поэтому я в WinDBG нажимаю F5. В NOTEPAD.EXE я открою
диалоговое окно File Open (открыть файл), так как оно создаст целый букет по
токов, а в WinDBG нажму Ctrl+Break для прерывания внутри отладчика. Если вы
проделываете то же самое и у вас открыто окно Processes And Threads, вы увиди
те, что NOTEPAD.EXE имеет четыре потока, а CMD.EXE — два.
Команда ~ (Thread Status — состояние потока) показывает активные потоки
текущего процесса. Переключение к процессу NOTEPAD.EXE и ввод команды ~
выводит следующую информацию:

1:001> ~
. 1 Id:
2 Id:
3 Id:
4 Id:

3d0.39c
3d0.1a4
3d0.8f0
3d0.950

Suspend:
Suspend:
Suspend:
Suspend:

1
1
1
1

Teb:
Teb:
Teb:
Teb:

7ffde000
7ffdd000
7ffdc000
7ffdb000

Unfrozen
Unfrozen
Unfrozen
Unfrozen

Как и в случае с |, команда ~ использует точку для индикации текущего пото
ка, а символ «#» — для обозначения потока, который либо вызвал исключение, либо
был активен при подключении к нему отладчика. В следующем столбце отобра
жается номер потока WinDBG. Так же, как и с номерами процессов, может быть
только один поток с номером 2 за все время жизни экземпляра WinDBG. Далее
идут значения ID — идентификаторы процессов Win32, за которыми следуют иден
тификаторы потоков. Счетчик приостановок (suspend count) немного сбивает с
толку. Значение счетчика 1 указывает на то, что поток не приостанавливался. Спра
вочная система по команде ~ показывает значение счетчика приостановок, рав
ное 0, которого я никогда не видел. После счетчика приостановок идет линейный
адрес (linear address) блока переменных окружения потока (Thread Environment
Block, TEB). TEB — это то же, что и блок информации о потоке (Thread Information
Block, TIB), обсуждавшийся в главе 7, который в свою очередь является адресом
блока данных потока, содержащего информацию экземпляра потока, такую как
стек и параметры инициализации COM. Наконец, Unfrozen (размороженный) ин
дицирует, использовали ли вы команду ~F (Freeze Thread — заморозить поток) для
«замораживания» потока. Замораживание потока в отладчике сродни вызову Suspend
Thread для этого потока из вашей программы. Это остановит поток до его «размо
розки».
По умолчанию команда работает для текущего потока, но иногда хочется уви
деть информацию и о другом потоке. Скажем, чтобы увидеть регистры другого
потока, надо использовать модификатор потока перед командой R (Registers — ре
гистры): ~2r. Если у вас открыто несколько процессов, нужно также добавлять к
командам модификатор процесса. Команда |0~0r показывает регистры для первого
процесса и первого потока независимо от того, какие процесс и поток активны.

Создание процессов из окна Command
Теперь, когда вы научились просматривать процессы и потоки, я могу перейти к
некоторым более продвинутым приемам запуска процессов под WinDBG. Коман

338

ЧАСТЬ II

Производительная отладка

да .CREATE (Create Process — создать процесс) позволяет вам запускать произволь
ные процессы. Это весьма полезно, если необходимо отлаживать различные ас
пекты COM+ или других кросспроцессных приложений. Основные параметры
.CREATE — полный путь к процессу, который надо запустить, и параметры коман
дной строки этого процесса. Так же, как и при обычном запуске любого процес
са, лучше заключить путь и имя процесса в кавычки, дабы избежать проблем с про
белами. Ниже показано применение .CREATE для запуска программы Solitaire на одной
из моих машин для программирования:

.create "e:\winnt\system32\sol.exe"
После нажатия клавиши Enter WinDBG сообщает, что процесс будет создан для
дальнейшего исполнения. Это значит, что WinDBG должен разрешить «раскрутить
ся» схеме отладчика, чтобы обработать уведомление о создании процесса. WinDBG
уже сделал вызов CreateProcess, но отладчик его еще не видит! Нажав F5, вы осво
бодите цикл отладки. Появляется уведомление о создании процесса, и WinDBG
остановится на точке прерывания загрузчика. Если вы применяете команду | для
просмотра процессов, WinDBG покажет процессы, запущенные .CREATE с пометкой
«create», как будто вы запустили сеанс отладчика, указав этот процесс.

Присоединение к процессам и отсоединение от них
При отладке уже работающего процесса вам пригодится команда .ATTACH (Attach
to Process — присоединиться к процессу). Сейчас мы обсудим все аспекты присо
единения к процессу. В следующем разделе мы обсудим неразрушающее присое
динение, при котором процесс не работает в цикле отладчика.
Команда .ATTACH требует указания ID процесса для присоединения к процессу.
Если вы располагаете физическим доступом к машине, на которой выполняется
процесс, можно увидеть ID процесса в диспетчере задач (Task Manager), но при
удаленной отладке это сделать трудновато. К счастью, разработчики WinDBG до
бавили команду .TLIST (List Process Ids — вывести ID процессов) для вывода спис
ка исполняющихся на машине процессов. Если вы отлаживаете сервисы Win32,
укажите параметр –v команды .TLIST, чтобы увидеть, какие сервисы в каких про
цессах выполняются. Вывод .TLIST выглядит так:

0n1544 e:\winnt\system32\sol.exe
0n1436 E:\Program Files\Windows NT\Pinball\pinball.exe
0n2120 E:\WINNT\system32\winmine.exe
Впервые увидев этот вывод, я подумал, что в этой команде ошибка и ктото слу
чайно напечатал «0n» вместо «0x». Однако позже я узнал, что 0n — такой же стандар
тный префикс ANSI для десятичных значений, как 0x для шестнадцатиричных.
Располагая десятичным значением ID процесса, вы передаете его как параметр
команде .ATTACH (если, конечно, вы используете префикс 0n, или это не будет ра
ботать). Так же, как и при создании процесса, WinDBG чтолибо скажет о том, что
подключение произойдет при следующем исполнении, поэтому вам нужно нажать
F5 для запуска цикла отладки. С этого момента вы отлаживаете процесс, к кото
рому присоединились. Разница только в том, что | пометит процесс как «attach»
в своем выводе.

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

339

При отладке под Windows XP/Server 2003 для освобождения отладчика служит
команда .DETACH (Detach from Process — отсоединиться от процесса). Так как это
работает только в текущем процессе, вам нужно переключиться на процесс, от
которого хотите отсоединиться, прежде чем исполните команду .DETACH. В любой
момент вы можете снова присоединиться к процессу для полной его отладки.
Если вы просто хотите присоединиться к процессу сразу после запуска WinDBG,
когда еще не открыто окно Command, нажмите F6 либо выберите из меню File
Attach To A Process (присоединиться к процессу). В появившемся диалоговом окне
Attach To Process (присоединиться к процессу) можно раскрыть узлы дерева для
просмотра командных строк процессов. Если, как случается, процесс содержит
сервисы Win32, вы их тоже увидите. Выбрав процесс, щелкните OK, и вы погру
зитесь в отладку.

Неразрушающее присоединение
Только что описанное полное присоединение прекрасно, так как вы располагае
те доступом ко всем способам отладки, например, к точкам прерывания. Однако
в Microsoft Windows 2000 процесс, запущенный однажды под отладчиком, будет
работать под ним вечно. Это не всегда удобно, если вы пытаетесь отлаживать
рабочие серверы, так как вам придется оставлять когото зарегистрированным на
этом сервере с полными правами администратора, чтобы мог работать WinDBG,
не говоря уж о замедлении процессов отладчиком. К счастью, в Windows XP/Server
2003 можно отсоединяться от отлаживаемых процессов (то, о чем я просил еще
во времена Microsoft Windows 3.1!).
Чтобы сделать промышленную отладку под Windows 2000 попроще, WinDBG
предлагает неразрушающее присоединение. WinDBG приостанавливает процесс,
чтобы вы могли исследовать его с помощью команд, но вы не можете осуществ
лять обычные задачи отладки, скажем, устанавливать точки прерывания. Это при
емлемый компромисс: вы можете получить полезную информацию, например,
состояние описателей, причем затем процесс будет работать на полной скорости.
Возможно, самый лучший вариант неразрушающей отладки — использование
отдельного экземпляра WinDBG. Как вы скоро увидите, для продолжения процес
са, возобновляющего все потоки, рабочее пространство нужно закрыть. Если вы
уже отлаживаете процессы, WinDBG должен будет сразу остановить эти процес
сы. Прежде чем выбрать отлаживаемый процесс, в нижней части диалогового окна
Attach To Process (рис. 84) установите флажок Noninvasive (неразрушающее), и
вы не попадете в полную отладку.
Когда вы щелкнете OK, WinDBG будет готов к нормальной отладке. Однако
предупреждение в верхней части окна Command, показанное здесь, поможет вам
вспомнить, что вы делаете:

WARNING: Process 1612 is not attached as a debuggee
The process can be examined but debug events will not be received
Внимание: Процесс 1612 не присоединен как отлаживаемый процесс.
Процесс доступен для исследования, но события отладки не обрабатываются.

340

Рис. 84.

ЧАСТЬ II

Производительная отладка

Подготовка к неразрушающей отладке

Присоединившись, можно исследовать в процессе что угодно. Завершив иссле
дование, надо освободить процесс, чтобы продолжить его исполнение. Лучший
способ освободить отлаживаемую программу — дать команду Q (Quit — завершить).
Она закроет рабочее пространство, но WinDBG продолжит работать — потом вы
сможете опять присоединиться. .DETACH тоже работает, но вам придется завершить
WinDBG, так как нет способа присоединиться к процессу снова в этом же сеансе.

Общие вопросы отладки в окне Command
В этом разделе я объясню, как начать отладку с помощью WinDBG, и расскажу о
ключевых командах, позволяющих эффективно выполнять отладку из окна Com
mand. Вы узнаете также о некоторых хитростях. Пересказывать документацию я
не буду, но прочитать ее настоятельно рекомендую.

Просмотр и вычисление переменных
Просмотр локальных переменных — это вотчина команды DV (Display Local Variables
— отобразить локальные переменные). Единственное, что слегка путает при ра
боте с WinDBG, — это просмотр локальных переменных вверх по стеку. На са
мом деле эта команда исполняется в виде нескольких команд, которые делают то,
что происходит автоматически при щелчке в окне Call Stack (стек вызовов).
Первый шаг — дать команду K (Display Stack Backtrace — отобразить обратную
трассировку стека) с модификатором N, чтобы увидеть стек вызовов с номерами
фреймов в самой левой колонке каждого элемента стека (между прочим, моя
любимая команда отображения стека — KP — показывает стек со значениями па
раметров, передаваемых функциям в каждом элемента стека). Номера фреймов
обычны в том смысле, что вершина стека всегда имеет номер 0, следующий эле
мент — 1 и т. д. Эти номера фреймов понадобятся вам, чтобы указать команде .FRAME
(Set Local Context — установить локальный контекст) переместиться вниз по сте
ку. Значит, чтобы просмотреть локальные переменные функции, которая вызвала

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

341

текущую функцию, вы используете последовательность команд, приведенную ниже.
Для перемещения контекста обратно к вершине стека дайте команду .frame 0:

.frame 1
dv
Команда DV возвращает достаточно информации, чтобы предоставить вам суть
происходящего с локальными переменными. Следующий вывод получен в резуль
тате исполнения команды DV при отладке программы PDB2MAP.EXE из главы 12.

cFuncFMT = CResString
cIM = CImageHlp_Module
szBaseName = Array [260]
pMark = cccccccc
dwBase = 0x400000
bEnumRet = 0xcccccccc
argc = 2
argv = 00344e18
fileOutput = 00000000
szOutputName = Array [260]
iRetValue = 0
bRet = 1
hFile = 000007c8
cRS = CResString
Увидеть больше позволяет команда DT (Display Type — отобразить тип), кото
рая может выполнять проход по связанным спискам и перемалывание массивов.
К счастью, вы можете задать в DT параметр ?, чтобы быстро получить справку,
находясь в центре боевых действий.
Еще DT может производить поиск типов символов. Вместо передачи ей имени
или адреса переменной вы указываете параметр в формате module!type, где type —
либо полное имя типа, либо содержит звездочку для поиска подвыражений. Так,
увидеть типы, начинающиеся с «IMAGE» в PDB2MAP, позволяет dt pdb2map!IMAGE*.
Указав тип полностью, вы увидите все поля этого типа, если это класс или струк
тура, либо лежащий в основе базовый тип, если это typedef.
Последняя из команд вычисления — ?? (Evaluate C++ Expression — вычислить
выражение C++) — служит для проверки арифметики указателей и управления
другими потребностями в вычислениях C++. Внимательно прочтите документа
цию по работе с выражениями, так как этот процесс не так прост, как кажется.
Теперь, когда вы можете просмотреть и вычислить все свои переменные, самое
время обратиться к исполнению, проходу по шагам и остановке программ.

Исполнение, проход по шагам и трассировка
Как вы, наверное, уже поняли, нажатие F5 продолжает исполнение после его пре
рывания в WinDBG. Вы не могли этого заметить, но нажатие F5 просто выполня
ет команду G (Go — дальше). Совершенно ясно, что в качестве параметра коман
ды G вы можете указать адрес команды. WinDBG использует этот адрес как одно
разовую точку прерывания, и, таким образом, вы можете запустить исполнение

342

ЧАСТЬ II

Производительная отладка

до этого места. Замечали ли вы, что нажатие Shift+F11 (команда Step Out), выпол
няет команду G, дополненную адресом (иногда в форме выражения)? Этот адрес
есть адрес возврата на вершину стека. Вы можете проделать то же самое в окне
Command, но вместо ручного вычисления адреса возврата можно использовать
псевдорегистр $ra в качестве параметра, чтобы WinDBG сам вычислил адрес воз
врата. Имеются и другие псевдорегистры, но не все из них применимы в пользо
вательском режиме. Задайте «PseudoRegister Syntax» в справочной системе WinDBG,
чтобы найти остальные псевдорегистры. Заметьте: эти псевдорегистры WinDBG
характерны только для WinDBG и не используются в Visual Studio .NET.
Для управления трассировкой и движением по шагам служат команды T (Trace)
и P (Step) соответственно. Напомню, что трассировка будет проходить внутрь любой
встреченной функции, тогда как прохождение по шагам — сквозь вызовы функ
ций. Один аспект, отличающий WinDBG от Visual Studio .NET, состоит в том, что
WinDBG не переключается автоматически между движением по шагам в тексте
исходного кода и в командах ассемблерного кода только потому, что вы случай
но переключаете фокус ввода между окнами Source (исходный код) и Disassembly
(дизассемблированный код). По умолчанию WinDBG движется по шагам в стро
ках исходного текста, если они загружены из места размещения исполняемого
файла. Если вы хотите идти шагами по ассемблерным командам, то либо сними
те флажок Source Mode (режим исходного текста) в меню Debug, либо дайте команду
.LINES (Toggle Source Line Support — переключить поддержку исходного кода) с
параметром –d.
Как и G, команды T и P делают то же самое, что и нажатие кнопок F11 (или F8)
и F10 в окне Command. Вы можете также указать или адрес, до которого должна
выполняться трассировка, или движение по шагам, или количество шагов, кото
рое необходимо выполнить. Это пригодится, так как иногда это проще, чем уста
навливать точку прерывания. В сущности это команда «runtocursor» (исполняй
до курсора), выполняемая вручную.
Две относительно новые команды для движения по шагам и трассировки: TC
(Trace to Next Call — трассировать до следующего вызова) и PC (Step to Next Call —
шаг до следующего вызова). Разница между ними в том, что они выполняют движе
ние по шагам или трассировку, пока не попадется следующий оператор CALL. При
выполнении PC, если указатель команд находится на команде CALL, исполнение будет
продолжаться, пока не произойдет возврат из подпрограммы. TC сделает шаг внутрь
подпрограммы и остановится на следующей команде CALL. Я нахожу TC и PC по
лезными, когда хочу пропустить часть функции, но не выходить из нее.

Трассировка данных и наблюдение за ними
Одна из самых больших проблем при выявлении проблем производительности
программ (быстродействия) в том, что почти невозможно прочесть и точно уви
деть, что происходит на самом деле. Так, код Standard Template Library (стандарт
ной библиотеки шаблонов, STL) создает одну из самых больших проблем быст
родействия при отладке приложений других программистов. В результате в код
впихивается столько inlineфункций (а код STL вообще почти невозможно читать),
что анализ путем чтения просто нереален. Но, так как STL негласно выделяет для
себя столько памяти и производит всякие блокировки там и сям, жизненно важ

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

343

но иметь способ увидеть, что на самом деле вытворяют функции, использующие
STL. К счастью, WinDBG имеет ответ на эту головоломку — и в этом главное отли
чие WinDBG от Visual Studio .NET — команду WT (Trace and Watch Data — трасси
ровать данные и наблюдать за ними).
WT показывает в иерархическом виде вызовы всех функций в вызове одной
функции. В конце трассировки WT показывает точно, какие функции вызывались
и сколько раз вызывалась каждая. Кроме того (и это важно при решении проблем
быстродействия), WT показывает, сколько было сделано переходов в режим ядра.
Для повышения быстродействия главное исключить побольше переходов в режим
ядра, поэтому то, что WT — один из немногих способов увидеть такую информа
цию, делает ее ценной вдвойне.
Как вы догадываетесь, вся эта трассировка может генерировать в окне Command
тонны хлама, которые, возможно, вы захотите сохранить в виде файла. К счастью,
программисты WinDBG удовлетворили требования полного сохранения всей си
стемы регистрации. Открыть файл регистрации очень просто — укажите имя файла
регистрации как параметр команды .LOGOPEN (Open Log File — открыть файл ре
гистрации). Вы также можете добавлять к существующему файл регистрации ко
мандой .LOGAPPEND (Append Log File — добавить файл регистрации). При заверше
нии отладки вызовите .LOGCLOSE (Close Log File — закрыть файл регистрации).
Эффективное использование WT для получения поддающегося интерпретации
вывода без всего лишнего, через что придется продираться, требует планирова
ния. WT трассирует, пока не попадется адрес возврата из текущей подпрограммы.
А значит, вам нужно тщательно позиционировать указатель команд за одиндва
шага до применения WT. Первое место — непосредственный вызов функции, ко
торую вы хотите исполнить. Это нужно делать на уровне ассемблерного кода,
поэтому вам понадобится установить точку прерывания прямо на команде вызо
ва подпрограммы или установить движение по шагам на уровне ассемблерного
кода и дойти поэтапно до команды вызова. Второе место — на первой команде
функции. Вы можете шагать до команды PUSH EBP или установить точку прерыва
ния на открывающей фигурной скобке функции в окне Source (исходный код).
Прежде чем перейти к параметрам WT, я хочу обсудить ее вывод. Для простоты
я написал WTExample — маленькую программу с несколькими функциями, вызы
вающими самих себя (вы найдете ее среди примеров на CD). Я устанавливаю точку
прерывания на первую команду в wmain и даю WT для получения результатов под
Windows XP SP1, как показано в листинге 81 (заметьте: я сократил некоторые
пробелы и перенес некоторые строки, чтобы листинг поместился на странице).

Листинг 8-1.

Вывод команды wt WinDBG

0:000> wt
Tracing WTExample!wmain to return address 0040139c
3
0 [ 0] WTExample!wmain
3
0 [ 1] WTExample!Foo
3
0 [ 2]
WTExample!Bar
3
0 [ 3]
WTExample!Baz
3
0 [ 4]
WTExample!Do
3
0 [ 5]
WTExample!Re
см. след. стр.

344

ЧАСТЬ II

3
3
3
3
3
6
3
3
18
15
16

0
0
0
0
0
0
0
0
0
18
0

[ 6]
[ 7]
[ 8]
[ 9]
[10]
[11]
[12]
[13]
[14]
[13]
[14]

20
15
26
3
2

34
0
49
0
0

[13]
[14]
[13]
[14]
[15]

1
31
3
14

0
55
0
0

[14]
[13]
[14]
[15]

4
36
9
37
4
8
2
11
2
13
5
2
7
5
2
7
5
2
7
5
2
7
5
2
7
5

14 [14]
73 [13]
0 [14]
82 [13]
119 [12]
123 [11]
0 [12]
125 [11]
0 [12]
127 [11]
140 [10]
0 [11]
142 [10]
149 [ 9]
0 [10]
151 [ 9]
158 [ 8]
0 [ 9]
160 [ 8]
167 [ 7]
0 [ 8]
169 [ 7]
176 [ 6]
0 [ 7]
178 [ 6]
185 [ 5]

Производительная отладка

WTExample!Mi
WTExample!Fa
WTExample!So
WTExample!La
WTExample!Ti
WTExample!Do2
kernel32!Sleep
kernel32!SleepEx
kernel32!_SEH_prolog
kernel32!SleepEx
ntdll!
RtlActivateActivationContextUnsafeFast
kernel32!SleepEx
kernel32!BaseFormatTimeOut
kernel32!SleepEx
ntdll!ZwDelayExecution
SharedUserData!
SystemCallStub
ntdll!ZwDelayExecution
kernel32!SleepEx
kernel32!SleepEx
ntdll!
RtlDeactivateActivationContextUnsafeFast
kernel32!SleepEx
kernel32!SleepEx
kernel32!_SEH_epilog
kernel32!SleepEx
kernel32!Sleep
WTExample!Do2
WTExample!_RTC_CheckEsp
WTExample!Do2
WTExample!_RTC_CheckEsp
WTExample!Do2
WTExample!Ti
WTExample!_RTC_CheckEsp
WTExample!Ti
WTExample!La
WTExample!_RTC_CheckEsp
WTExample!La
WTExample!So
WTExample!_RTC_CheckEsp
WTExample!So
WTExample!Fa
WTExample!_RTC_CheckEsp
WTExample!Fa
WTExample!Mi
WTExample!_RTC_CheckEsp
WTExample!Mi
WTExample!Re

ГЛАВА 8

2
7
5
2
7
5
2
7
5
2
7
5
2
7
6
2
8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

0
187
194
0
196
203
0
205
212
0
214
221
0
223
230
0
232

[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[
[

6]
WTExample!_RTC_CheckEsp
5]
WTExample!Re
4]
WTExample!Do
5]
WTExample!_RTC_CheckEsp
4]
WTExample!Do
3]
WTExample!Baz
4]
WTExample!_RTC_CheckEsp
3]
WTExample!Baz
2]
WTExample!Bar
3]
WTExample!_RTC_CheckEsp
2]
WTExample!Bar
1] WTExample!Foo
2]
WTExample!_RTC_CheckEsp
1] WTExample!Foo
0] WTExample!wmain
1] WTExample!_RTC_CheckEsp
0] WTExample!wmain

240 instructions were executed in 239 events (0 from other threads)
Function Name
Invocations MinInst MaxInst AvgInst
SharedUserData!SystemCallStub
1
2
2
2
WTExample!Bar
1
7
7
7
WTExample!Baz
1
7
7
7
WTExample!Do
1
7
7
7
WTExample!Do2
1
13
13
13
WTExample!Fa
1
7
7
7
WTExample!Foo
1
7
7
7
WTExample!La
1
7
7
7
WTExample!Mi
1
7
7
7
WTExample!Re
1
7
7
7
WTExample!So
1
7
7
7
WTExample!Ti
1
7
7
7
WTExample!_RTC_CheckEsp
13
2
2
2
WTExample!wmain
1
8
8
8
kernel32!BaseFormatTimeOut
1
15
15
15
kernel32!Sleep
1
4
4
4
kernel32!SleepEx
2
4
37
20
kernel32!_SEH_epilog
1
9
9
9
kernel32!_SEH_prolog
1
18
18
18
ntdll!
RtlActivateActivationContextUnsafeFast 1
16
16
16
ntdll!
RtlDeactivateActivationContextUnsafeFast 1
14
14
14
ntdll!ZwDelayExecution
2
1
3
2
1 system call was executed
Calls System Call
1 ntdll!ZwDelayExecution

345

346

ЧАСТЬ II

Производительная отладка

Начальная часть вывода (отображение иерархического дерева) — это инфор
мация о вызовах. Перед каждым вызовом WinDBG отображает три числа: первое —
количество ассемблерных команд, исполняемых функцией до вызова следующей
функции, второе не документировано, но похоже на полное число исполненных
ассемблерных команд при трассировке до возврата, последнее число в скобках —
это текущий уровень вложенности иерархического дерева.
Вторая часть вывода — отображение итогов — немного менее понятна. В до
полнение к подведению итогов вызовов каждой функции она отображает счет
чик вызовов каждой функции, а также минимальное количество ассемблерных
команд, вызываемых при выполнении функции, максимальное количество команд,
вызываемых при выполнении функции, и среднее количество вызванных команд.
Последние строки итогов показывают количество системных вызовов. Вы може
те увидеть, что WTExample иногда вызывает Sleep для обращения к режиму ядра.
Сам факт, что вы располагаете количеством обращений к ядру, потрясающе крут.
Как вы можете себе представить, WT может дать огромный вывод и замедлить
ваше приложение, так как каждая строка вывода требует парочки межпроцессных
передач информации между отладчиком и отлаживаемой программой. Если вы
хотите увидеть крайне важную итоговую информацию, то параметр –nc команды
WT подавит вывод иерархии. Конечно, если вы интересуетесь только иерархией,
укажите параметр –ns. Чтобы увидеть содержимое регистра возвращаемого зна
чения (EAX в языке ассемблера x86), задайте –or, а чтобы увидеть адрес, исходный
файл и номер строки (если это доступно) для каждого вызова — –oa. Последний
параметр — –l — позволяет установить максимальную глубину вложенности ото
бражаемых вызовов. Параметр –l полезен, если вы хотите увидеть только глав
ные моменты того, что исполняется, или сохранить в выводе только функции вашей
программы.
Я настоятельно советую вам посмотреть ключевые циклы и операции в своих
программах с помощью WT, чтобы точно знать, что происходит за кулисами. Не
знаю, сколько проблем производительности, неправильного использования язы
ков и технологий я выследил с ее помощью!

Общий вопрос отладки
Некоторые имена в моих программах на C++ огромны. Как
использовать WinDBG, чтобы не заработать туннельного синдрома?
К счастью, WinDBG теперь поддерживает текстовые псевдонимы (aliases).
Определить пользовательский псевдоним и эквивалент расширения позво
ляет команда AS (Set Alias — установить псевдоним). Например, команда as
LL kernel32!LoadLibraryW назначит строку «LL» для расширения ее до kernel
32!LoadLibraryW везде, где вы ее вводите в командной строке. Увидеть назна
ченные вами псевдонимы позволяет команда AL (List Aliases — список псев
донимов), а удалить — AD (Delete Alias — удалить псевдоним).
Есть еще одно место, указанное в документации, где вы можете опреде
лить псевдонимы с фиксированными достаточно странными именами, —
это команда R (Registers — регистры). Псевдонимы с фиксированными име
нами — $u0, $u1, …, $u9. Чтобы определить псевдоним с фиксированным

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

347

именем, надо ввести точку перед u: r $.u0=kernel32!LoadLibraryA. Увидеть, какие
псевдонимы назначены фиксированным именам, позволяет лишь команда
.ECHO (Echo Comment — вывести комментарий): .echo $u0.

Точки прерывания
WinDBG предлагает те же виды точек прерывания, что и Visual Studio .NET, плюс
несколько уникальных. Важно, что WinDBG дает гораздо больше возможностей в
момент срабатывания точек прерывания и позволяет увидеть, что происходит после
этого. Прежние версии WinDBG имели хорошее диалоговое окно, где очень про
сто было устанавливать точки прерывания. Увы, это диалоговое окно отсутствует
в переписанной версии WinDBG, которой мы располагаем сейчас, поэтому при
установке точки прерывания мы все должны делать вручную.

Общие точки прерывания
Первое, за что я хочу взяться в точках прерывания, — это две команды, устанав
ливающие точки прерывания: BP и BU. Обе имеют одинаковые параметры и моди
фикаторы. Можно считать, что версия команды BP — это строгая точка прерыва
ния, всегда ассоциируемая в WinDBG с адресом. Если модуль, содержащий такую
точку, выгружен, WinDBG исключает точку BP из списка точек прерывания. С дру
гой стороны, точки прерывания BU ассоциированы с символом, поэтому WinDBG
отслеживает символ, а не адрес. Если символ перемещается, точка BU также пере
мещается. А значит, точка BU будет активна, но заблокирована, если модуль выг
ружается из процесса, но будет немедленно реактивирована, как только модуль
вернется в процесс, даже если ОС переместит модуль. Основная разница между
точками прерывания BP и BU в том, что WinDBG сохраняет точки BU в рабочих
пространствах WinDBG, а BP — нет. Наконец, при установке точки прерывания в
окне Source путем нажатия F9 WinDBG устанавливает точку BP. Я рекомендую ис
пользовать точки прерывания BU вместо BP.
Имеется ограниченное диалоговое окно Breakpoints (точки прерывания) —
щелкните меню Edit, затем Breakpoints, — но я управляю точками прерывания из
окна Commands, так как, помоему, это проще. Команда BL (Breakpoint List — спи
сок точек прерывания) позволяет увидеть все активные сейчас точки прерывания.
Вы можете прочитать документацию к выводу команды BL, но я хочу заметить, что
первое поле — это номер точки прерывания WinDBG, а второе — буква, обозна
чающая статус точки прерывания: d (disabled — запрещена), e (enabled — разре
шена) и u (unresolved — неразрешима). Вы можете разрешить или заблокировать
точки прерывания командами BE (Breakpoint Enable — разрешить точку прерыва
ния) и BD (Breakpoint Disable — заблокировать точку прерывания). Указание звез
дочки (*) в любой из этих команд будет разрешать/блокировать все точки пре
рывания. Наконец, вы можете разрешить/заблокировать конкретные точки пре
рывания, указанием номера точки прерывания в командах BE и BD.
Синтаксис команды для установки точки прерывания пользовательского ре
жима x86 таков:

[~Thread] bu[ID] [Address [Passes]] ["CommandString"]

348

ЧАСТЬ II

Производительная отладка

Если вы просто вводите BU, WinDBG устанавливает точку прерывания на месте
текущего указателя команд. Модификатор потока (~Thread) — это просто номер
потока WinDBG, который делает установки точки прерывания в конкретном по
токе тривиальной. Если вы пожелаете указать номер точки прерывания WinDBG,
укажите его сразу после BU. Если точка прерывания с таким номером уже суще
ствует, то WinDBG заменит имеющуюся точку прерывания новой, которую вы
устанавливаете сейчас. Поле адреса (Address) может содержать любое допустимое
адресное выражение, которые я описывал в начале раздела «Ситуации при отлад
ке». В поле проходов (Passes) вы указываете, сколько раз вы хотели бы пропустить
эту точку останова до того, как произойдет останов. Сравнение в этом поле дей
ствует по правилу «больше или равно», максимальное значение — 4 294 967 295.
Так же как при отладке неуправляемого кода в Visual Studio .NET, поле Passes
уменьшается только в случае исполнения программы «на полной скорости», а не
при проходе по шагам или трассировке.
Последнее поле, которое можно использовать при установке точки прерыва
ния, — это чудесная командная строка (CommadString). Это правда, что вы може
те ассоциировать команды с точкой прерывания! Наверное, лучший способ про
демонстрировать эту удивительную штуку — рассказать, как я применил эту тех
нологию для устранения почти неразрешимой ошибки. Одна ошибка проявлялась
только после нескольких длинных последовательностей данных, прошедших че
рез определенный участок кода. Как обычно, это занимало уйму времени, чтобы
выполнились все условия, а я не мог просто тратить день или неделю на просмотр
состояний переменных при каждой остановке программы на точке прерывания
(увы, я работал не на условиях почасовой платы!). Мне нужен был способ регис
трировать все значения переменных, чтобы изучить поток данных в системе. Так
как можно объединять массу команд с помощью точки с запятой, я постепенно
построил огромную команду, которая выводила все переменные путем вызова DT
и ??. Я также разбросал несколько команд .ECHO, чтобы видеть, где я был, и иметь
общую строку, которая появлялась бы каждый раз, когда срабатывала точка пре
рывания. Командную строку я завершил командой «;G», чтобы исполнение про
граммы продолжалось после точки прерывания после полного дампа значений
переменных. Я, конечно же, включил регистрацию и просто запустил процесс на
исполнение, пока он не завершился аварийно. Просмотрев весь файл регистра
ции, я сразу увидел образчик данных и быстренько исправил ошибку. Не будь в
WinDBG такой прекрасной возможности расширения точек прерывания, я бы
никогда не нашел эту ошибку.
Команда J (Execute If — Else — выполнить если — то) особенно хороша для
применения в командной строке точки прерывания. Она позволяет исполнять
команды по условию, основанному на частном выражении. Иначе говоря, J пре
доставляет возможность использовать условные точки прерывания в WinDBG.
Формат команды:

j expression 'if true command' ; 'if false command'
Выражение (expression) — это любое выражение, с которым может справить
ся вычислитель WinDBG. Текст в одиночных кавычках — это командные строки
для истинного (true) и ложного (false) значения выражения. Всегда заключайте
командные строки в одиночные кавычки, так как вы получаете возможность вклю

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

349

чать в командные строки точки с запятой для построения больших выражений.
И, конечно же, вы можете включать подкоманды J в командные строки для true и
false. Из документации не совсем ясно, что делать, если нужно оставить одно из
условий (true или false) пустым, т. е. если вы не хотите исполнять никакие коман
ды для этого условия, просто введите два символа одиночной кавычки рядом для
пропущенного условия.

Точки прерывания по обращению к памяти
Помимо блестящих точек прерывания исполнения, WinDBG имеет феноменаль
ную команду BA (Break On Access — прервать в случае обращения), позволяющую
остановиться, если фрагмент памяти считывается или записывается вашим про
цессом. Visual Studio .NET предлагает только точки прерывания по изменению
состояния памяти, а вам нужно пользоваться классом аппаратных точек преры
вания Майка Мореарти (Mike Morearty) для доступа ко всей мощи, предлагаемой
аппаратными точками прерывания Intel x86. Однако WinDBG сам располагает всей
этой мощью.
Формат точек прерывания по обращению к памяти для пользовательского
режима Intel x86 таков:

[~Thread] ba[ID] Access Size [Address [Passes]] ["CommandString"]
Как видите, BA предлагает гораздо больше возможностей, чем просто останов
ка при обращении к памяти. Так же, как и в случае команд BP и BU, вы можете при
казать останавливаться, если только указанный поток «прикасается» к памяти, ус
тановить счетчик проходов и ассоциировать эту удивительную командную стро
ку с определенным типом обращения. Поле типа обращения (Access) — это оди
ночный символ указывающий, хотите ли вы остановиться в случае чтения (r),
записи (w) или исполнения (e). Поле размера (Size) указывает, сколько байт вы со
бираетесь поставить под надзор. Так как BA использует Intel Debug Registers (ре
гистры отладки Intel) для реализации своего волшебства, вы ограничены только
возможностью надзора за 1, 2 или 4 байтами в каждый момент, вы также ограни
чены четырьмя точками прерывания BA. Как и при установке точек прерывания
по данным в Visual Studio .NET, надо помнить о проблемах выравнивания памяти,
поэтому, если вы хотите надзирать за 4 байтами памяти, адрес этой памяти дол
жен заканчиваться на 0, 4, 8 или C. Поле адреса (Address) — это адрес памяти,
обращение к которой должно вызвать прерывание. Хотя WinDBG менее требова
телен к переменным, я все же предпочитаю использовать реальные шестнадцате
ричные адреса, чтобы быть уверенным, что точка прерывания установлена имен
но там, где я хочу.
Чтобы увидеть BA в действии, можете воспользоваться программой MemTouch
из числа файловпримеров к этой книге. Программа просто размещает локаль
ный фрагмент памяти szMem, передаваемый одной из функций, которая «прикаса
ется» к памяти, а другая функция просто читает эту память. Установите точку пре
рывания на адрес szMem, чтобы указать место прерывания. Чтобы получить адрес
локальной переменной, дайте команду DV. Получив этот адрес, вы можете указать
это значение в BA. Чтобы узнать, что делать с командами, возможно, стоит вызвать
«kp;g», в результате чего вы увидите время доступа, а затем продолжите испол
нение.

350

ЧАСТЬ II

Производительная отладка

Исключения и события
WinDBG предлагает продвинутые средства управления исключениями и событи
ями. Исключения — это все аппаратные исключительные ситуации, такие как
нарушение доступа, приводящее к аварийному завершению программы. События —
это все стандартные события, передаваемые отладчикам средствами Microsoft Win32
Debugging API. Это значит, например, что вы можете установить прерывание
WinDBG, когда загрузился модуль, и, таким образом, получить управление еще до
того, как будет исполнена точка входа модуля.
Для управления исключениями и событиями из окна Command служат коман
ды SXE, SXI, SXN (Set Exceptions — установить исключения), но они сильно сбивают
с толку. К счастью, в WinDBG есть диалоговое окно Event Filters (фильтры собы
тий) (рис. 85), доступное из меню Debug.

Рис. 85.

Диалоговое окно Event Filters

Но даже оно все еще чуточку путает при попытке понять, что происходит при
исключении, так как WinDBG использует странноватую терминологию в коман
дах SX* и диалоговом окне Event Filters. Групповое поле Execution (исполнение) в
нижнем правом углу указывает, как WinDBG будет управлять исключением
(табл. 82). Поскольку поле Exceptions указывает, что вы хотите передавать функ
ции API ContinueDebugEvent, напомню, что мы обсуждали ее в главе 4.

Табл. 8-2.

Состояния прерываний по исключению

Состояние

Описание

Enabled (разрешено)

При возникновении исключения (исключительной ситуации)
оно исполняется и происходит прерывание в отладчик.

Disabled (блокирована)

При первом возникновении исключения отладчик игнори
рует его, при повторном исполнение останавливается,
и осуществляется выход в отладчик.

Output (вывод сообщения) При возникновении исключения прерывание в отладчик не
производится. Однако выводится информационное сообще
ние об этом исключении.

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

Табл. 8-2.

Состояния прерываний по исключению

351

(продолжение)

Состояние

Описание

Ignore (игнорируется)

При возникновении исключения отладчик его игнорирует.
Сообщение не отображается.

Вы можете игнорировать группу элементов управления Continue в нижнем
правом углу. Она важна, только если вы хотите производить различную обработ!
ку точки прерывания, одиночного шага и недопустимых исключений. Если вы
добавляете к списку собственную структурную обработку исключений, сохрани!
те параметры группы Continue по умолчанию, Not Handled — без обработки.
В результате каждый раз при возникновении исключения WinDBG будет коррек!
тно передавать его прямо отлаживаемой программе. Вы же не хотите, чтобы от!
ладчик съедал исключения кроме тех, которые он сам вызывает, таких как точка
прерывания и одиночный шаг.
После выбора собственно исключения самой важной кнопкой в этом диало!
говом окне является Commands (команды). Только имя может подсказать вам, что
она делает. Щелчок этой кнопки выводит окно Filter Command (команда фильт!
ра) (рис. 8!6). Первое поле ввода названо неправильно — оно должно называться
First!Chance Exception.

Рис. 86.

Диалоговое окно Filter Command

В окне Filter Command можно вводить команды WinDBG, исполняемые при
возникновении в отлаживаемой программе конкретного исключения. Когда мы в
разделе «Контроль исключений» главы 7 обсуждали диалоговое окно Exception в
Visual Studio .NET, я показал, как устанавливать исключения C++, чтобы остано!
виться на первом исключении, чтобы было можно контролировать, где ваши про!
граммы вызывают throw, а после нажатия F10 — и catch. Проблема в том, что Visual
Studio .NET останавливается всякий раз, когда вырабатывается исключительная
ситуация C++, а вам приходится сидеть и каждый раз на этом месте нажимать F5,
в то время как ваше приложение обрабатывает множество команд throw.
Что хорошо в WinDBG и в возможности ассоциировать команды с исключе!
ниями, так это то, что вы можете применять эти команды для регистрации всей
важной информации и эффективно продолжать исполнение без вашего вмеша!
тельства в ход исполнения. Чтобы настроить обработку исключений C++, выбе!
рите C++ EH Exception из списка исключений в диалоговом окне Event Filter и
щелкните кнопку Commands. В диалоговом окне Filter Command (команда филь!
тра) введите в поле ввода Command kp;g, чтобы WinDBG зарегистрировал состо!
яние стека и продолжил выполнение. Теперь у вас будет состояние стека вызовов
при каждом выполненном throw, а WinDBG продолжит корректное исполнение. И все
же, чтобы увидеть последнее событие или исключение, происшедшее в процессе,
дайте команду .LASTEVENT (Display Last Event — отобрази последнее событие).

352

ЧАСТЬ II

Производительная отладка

Управление WinDBG
Теперь, когда вы познакомились с важными командами отладки, я хочу обратить!
ся к нескольким мета!командам, которые я использую ежедневно в процессе от!
ладки с помощью WinDBG.
Простейшая, но чрезвычайно полезная команда — .CLS (Clear Screen — очис!
тить экран) — позволяет очистить окно Command, чтобы начать вывод сначала.
Так как WinDBG способен изрыгать огромные объемы информации, требующей
место для хранения, время от времени полезно очищать «рабочее поле».
Если ваше приложение работает со строками Unicode, вам захочется настро!
ить отображение указателей USHORT как строк Unicode. Команда .ENABLE_UNICODE (Enable
Unicode Display — разрешить отображение Unicode), введенная с параметром 1,
настроит все так, чтобы команда DT корректно отображала ваши строки. Если нужно
настроить национальные параметры для корректного отображения строк формата
Unicode, то команда .LOCALE (Set Locale — установить местный диалект) в качестве
параметра принимает локализующий идентификатор. Если приходится работать
с битами и вы хотите видеть значения битов, команда .FORMATS (Show Number Formats
— отобразить форматы чисел) покажет значения передаваемых параметров всех
числовых форматов, в том числе двоичных.
А вот команда .SHELL (Command Shell) позволяет запустить программу MS!DOS
из отладчика и перенаправить ее вывод в окно Command. Отлаживая на той же
машине, на которой выполняется отлаживаемая программа, конечно же, проще
переключиться с помощью Alt+Tab, но красота .SHELL в том, что при выполнении
удаленной отладки, программа MS!DOS выполняется на удаленной машине. .SHELL
можно использовать также для запуска единственной внешней программы, с пе!
ренаправлением ее вывода в окно Command. После выдачи .SHELL окно Command
в строке ввода будет отображать INPUT> для обозначения, что программа MS!DOS
ожидает ввода. Для завершения программы MS!DOS и возврата к окну Command,
используйте либо команду MS!DOS exit, либо, что предпочтительнее, .SHELL_QUIT
(Quit Command Prompt), так как она прекратит исполнение программы MS!DOS,
даже если она заморожена.
Последнюю мета!команду, о которой я упомяну, я искал в отладчике много лет,
но обнаружил лишь теперь. При написании обработчиков ошибок вы обычно
знаете, что к моменту обработки ошибок в вашем процессе возникли серьезные
неприятности. Вам также известно в 9 случаях из 10, что если происходит обра!
ботка какой!то ошибки, то, вероятно, вам нужно посмотреть значения каких!то
переменных или состояние стека вызовов, а кроме того, вы захотите записать
конкретную информацию. Я всегда хотел иметь способ закодировать то, что нужно
выполнить прямо в моем процессе обработки ошибки. Сделав это, команды бу!
дут исполняться, позволяя программистам службы сопровождения и мне отлажи!
вать быстрее. Моя идея была такова: так как вызовы OutputDebugString проходят через
отладчик, можно было бы встроить команды в OutputDebugString. Вы могли бы сказать
отладчику, что искать в начале текста OutputDebugString, а что все остальное после
этого будут команды, подлежащие исполнению.
Именно так работает команда WinDBG .OCOMMAND (Expect Commands from Tar!
get — ожидать команды от отлаживаемой программы). Вы вызываете .OCOMMAND, ука!
зывая искомый префикс строки в начале вызовов OutputDebugString. Если этот пре!

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

353

фикс присутствует, WinDBG исполнит остальную часть текста как командную стро!
ку. Очевидно, что вам нужно быть осторожным при использовании строк, а то
WinDBG сойдет с ума, пытаясь исполнить вызовы OutputDebugString во всей вашей
программе. В качестве такой строки мне нравится WINDBGCMD: — я разбрасываю
командные строки WinDBG во всех своих программах.
При использовании.OCOMMAND необходимо в конце каждой команды добавлять
«;g», иначе WinDBG остановится по завершении команды. В следующей функции
все команды завершаются «;g», чтобы выполнение продолжалось. Чтобы они на!
чали работать, я выдаю команду .ocommand WINDBGCMD: при запуске программы:

void Baz ( int )
{
// Чтобы это воспринималось как команды WinDBG, выполните команду
// ".ocommand WINDBGCMD:" внутри WinDBG.
OutputDebugString ( _T ( "WINDBGCMD: .echo \"Hello from WinDBG\";g" ));
OutputDebugString ( _T ( "WINDBGCMD: kp;g" ) ) ;
OutputDebugString ( _T ("WINDBGCMD: .echo \"Stack walk is done\";g")) ;
}

Магические расширения
Теперь вы знаете достаточно команд (представляющих лишь малую толику воз!
можных), чтобы у вас голова пошла кругом, и вы, возможно, удивляетесь, почему
я трачу так много времени на обсуждение WinDBG. WinDBG труднее в использо!
вании, чем Visual Studio .NET, и кривая обучения не только крута — она почти вер!
тикальна! Вы увидели, что WinDBG предлагает классные возможности по точкам
прерывания, но вы все еще, возможно, удивляетесь, почему игра стоит свеч.
WinDBG — достойная вещь благодаря командам расширения. Эти команды по!
зволяют увидеть то, что по!другому увидеть невозможно. Microsoft предложила це!
лый букет замечательных расширений, которые мастера отладки используют для
разрешения самых неприятных проблем.
Я хочу сосредоточиться на наиболее важных. Найдите время на чтение доку!
ментации об остальных расширениях. В разделе Reference\Debugger Extension
Commands документации к WinDBG имеются два ключевых раздела: General Exten!
sions (общие расширения) и User!Mode Extensions (расширения пользовательского
режима).
Физически расширения являются файлами DLL, экспортирующими особые
имена функций для своей работы. В каталоге Debugging Tools For Windows есть
несколько каталогов, таких как W2KFRE (Windows 2000 Free Build — свободная
поставка для Windows 2000) и WINXP. Эти каталоги содержат команды расшире!
ния для разных ОС. Как писать свои расширения, вы можете прочитать в файле
README.TXT, прилагаемом к примеру EXTS в каталоге \SDK\SAMPLES\EXTS.

Загрузка расширений и управление ими
Прежде чем рассмотреть команды расширения, надо поговорить о том, как уви!
деть, какие расширения вы уже загрузили, как загрузить ваше собственное и как

354

ЧАСТЬ II

Производительная отладка

получить справку из расширения. Загруженные расширения покажет команда .CHAIN
(List Debugger Extensions — список расширений отладчика). Она выведет и поря!
док поиска команд сверху вниз на дисплее, и то, как WinDBG ищет библиотеки
DLL расширений. Под Windows 2000 отображение для четырех библиотек расши!
рений пользовательского режима (DBGHELP.DLL, EXT.DLL, UEXT.DLL и NTSDEXTS.DLL)
выглядит так (зависит от расположения каталога Debugging Tools for Windows):

0:000> .chain
Extension DLL search Path:
G:\windbg\winext;G:\windbg\pri;G:\windbg\WINXP;G:\windbg;
Extension DLL chain:
dbghelp: image 6.1.0017.1, API 5.2.6, built Sat Dec 14 15:32:30 2002
[path: G:\windbg\dbghelp.dll]
ext: image 6.1.0017.0, API 1.0.0, built Fri Dec 13 01:46:07 2002
[path: G:\windbg\winext\ext.dll]
exts: image 6.1.0017.0, API 1.0.0, built Fri Dec 13 01:46:07 2002
[path: G:\windbg\WINXP\exts.dll]
uext: image 6.1.0017.0, API 1.0.0, built Fri Dec 13 01:46:08 2002
[path: G:\windbg\winext\uext.dll]
ntsdexts: image 5.2.3692.0, API 1.0.0, built Tue Nov 12 14:16:20 2002
[path: G:\windbg\WINXP\ntsdexts.dll]
Загрузка расширений проста — укажите имя библиотеки DLL (без расширения
.DLL) как параметр команды .LOAD (Load Extension DLL — загрузить DLL расшире!
ния). Для выгрузки укажите имя библиотеки DLL в качестве параметра команде
.UNLOAD (Unload Extension DLL — выгрузить DLL расширения).
Принято, что все команды расширения вводятся в нижнем регистре и в отли!
чие от обычных и мета!команд они чувствительны к регистру. Кроме того, команды
в библиотеке расширения называются так же: например, команда help предназ!
начена для того, чтобы быстро информировать, что имеется в этой библиотеке
DLL расширения. При загруженных расширениях по умолчанию ввод команды !help
не показывает всю доступную справку. Чтобы вызвать команду расширения кон!
кретной библиотеки DLL расширения, добавьте имя DLL и точку для команды
расширения: !dllname.command. Следовательно, чтобы увидеть справку о NTSD!
EXTS.DLL, нужно ввести !ntsdexts.help.

Важные команды расширения
Теперь, когда вы вооружены некоторыми основами работы с расширениями, я хочу
обратиться к командам расширения, которые облегчат вашу жизнь. Все эти рас!
ширения являются частью набора расширений по умолчанию, загружаемого все!
гда, поэтому, пока вы специально не выгрузите что!то из этих расширений, они
будут всегда доступны.
Первая важная команда — !analyze –v — позволяет быстро проанализировать
текущее исключение. Я специально показал эту команду с параметром –v, потому
что без него вы не увидите большую часть информации. Команда !analyze не раз!
решает все ваши ошибки — ее идея заключается в том, что она предоставляет вам
ту информацию, которую вы обычно хотите видеть во время аварийного завер!
шения, такую как запись исключения и стек вызовов.

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

355

Так как критические секции являются облегченными объектами синхрониза!
ции, многие программисты пользуются ими. WinDBG предлагает две команды
расширения для заглядывания внутрь критической секции, чтобы узнать состоя!
ние блокировок объектов и какие потоки владеют ими. Если у вас есть адрес кри!
тической секции, можно применить команду !critsec, передавая ей как параметр
адрес секции. А увидеть все заблокированные критические секции позволяет !locks.
Все критические секции процесса она покажет с параметром –v. В Windows XP/
Server 2003 дополнительный параметр –o покажет сиротские критические секции.
Если вы программируете защищенные приложения Win32, очень трудно по!
нять, какая текущая информация безопасности применена к текущему потоку.
Команда !token (Windows XP/Server 2003) или !threadtoken (Windows 2000) пока!
жет состояние заимствования прав текущего потока и остальную информацию бе!
зопасности, такую как идентификация пользователя и групп, плюс отобразит в тек!
стовом виде все привилегии, ассоциированные с потоком.
Есть одна команда, которая сохранила мне бессчетное количество часов от!
ладки, — !handle. Как можно понять из названия, она делает что!то с описателями
в процессе. Если просто ввести !handle, вы увидите значения описателей, тип объек!
та, содержащегося в описателе, и секцию, подводящую итоги, сколько объектов
каждого типа имеется в процессе. Некоторые из этих типов могут показаться вам
бессмысленными, если вы не программировали драйверы или не читали книгу
Дэвида Соломона и Марка Руссиновича «Inside Microsoft Windows 2000»1. Табл. 8!3
предлагает переводы (трансляцию) с языка команды !handle на язык терминов
пользовательского режима некоторых типов.

Табл. 8-3.

Трансляция (перевод) типов описателей

Термин команды
!handle

Термин пользовательского режима

Desktop

Win32 desktop (рабочий стол Win32)

Directory

Win32 object manager namespace directory (каталог рабочего про!
странства менеджера объектов Win32)

Event

Win32 event synchronization object (объект синхронизации события
Win32)

File

Disk file, communication endpoint, or device driver interface (диско!
вый файл, конечная точка коммуникационной связи или интер!
фейс драйвера устройства)

IoCompletionPort

Win32 IO completion port (порт завершения ввода/вывода Win32)

Job

Win32 job object (объект задания Win32)

Key

Registry key (раздел реестра)

KeyedEvent

Non!user!creatable events used to avoid critical section out of memory
conditions (созданные не пользователем события, используемые
предотвращения выхода критической секции за пределы памяти)

Mutant

Win32 mutex synchronization object (объект синхронизации мью!
текс Win32)

см. след. стр.

1

Соломон Д., Руссинович М. Внутреннее устройство Microsoft Windows 2000. — М.:
«Русская Редакция», 2001. — Прим. перев.

356

ЧАСТЬ II

Производительная отладка

Табл. 8-3. Трансляция (перевод) типов описателей
Термин команды
!handle

(продолжение)

Термин пользовательского режима

Port

Interprocess communication endpoint (конечная точка межпроцесс!
ного взаимодействия)

Process

Win32 process (процесс Win32)

Thread

Win32 thread (поток Win32)

Token

Win32 security context (контекст защиты Win32)

Section

Memory!mapped file or page!file backed memory region (отображаемый
на память файл или страничный файл выгрузки региона памяти)

Semaphore

Win32 semaphore synchronization object (объект синхронизации се!
мафор Win32)

SymbolicLink

NTFS symbolic link (символьная связь NTFS)

Timer

Win32 timer object (объект таймер Win32)

WaitablePort

Interprocess communication endpoint (конечная точка межпроцесс!
ного взаимодействия)

WindowStation

Top level of window security object (объект защиты окна верхнего
уровня)

Даже просмотр описателей замечателен, но, указав параметр ? в !handle, вы
увидите, что команда способна на большее. Чтобы появилось больше информа!
ции об описателе, можно задать в первом параметре значение описателя, а во
втором — битовое поле, указывающее, что вы хотите узнать об этом описателе. В
качестве второго параметра вы всегда должны задавать F, так как в результате вам
будет показано все. Например, я отлаживаю программу WDBG из главы 4, описа!
тель 0x1CC является событием. Вот как получить детальную информацию об этом
описателе:

0:006> !handle 1cc f
Handle 1cc
Type
Event
Attributes 0
GrantedAccess
0x1f0003:
Delete,ReadControl,WriteDac,WriteOwner,Synch
QueryState,ModifyState
HandleCount 3
PointerCount 6
Name
\BaseNamedObjects\WDBG_Happy_Synch_Event_614
Object Specific Information
Event Type Manual Reset
Event is Waiting
Вы видите не только предоставленные права, но также имя и, что важнее, что
событие находится в состоянии ожидания (т. е. в занятом состоянии). Так как !handle
покажет эту информацию для всех типов, теперь вы легко увидите взаимные бло!
кировки, поскольку вы можете проверить состояния всех событий, семафоров и
мьютексов, чтобы понять, кто из них блокирован, а кто нет.

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

357

Вы можете посмотреть подробную информацию для всех описателей процес!
са, передавая два параметра 0 и F. Если вы работаете над большим процессом, вывод
может занять кучу времени на перемалывание всех деталей. Чтобы узнать о кон!
кретном классе описателей, укажите два первых параметра 0 и F, а третий — имя
класса. Например, чтобы увидеть все события, введите !handle 0 f Event.
Выше я касался применения !handle для просмотра состояний событий, чтобы
сделать вывод, почему ваше приложение взаимоблокируется. Другое замечатель!
ное использование !handle — оценка потенциальной утечки ресурсов. Так как !handle
показывает общее количество всех текущих описателей процесса, вы можете легко
сравнить результаты !handle до и после. Если вы видите, что общее количество
описателей изменилось, вы точно скажете, утечка какого типа описателей проис!
ходит. Так как отображается детальная информация, такая как разделы реестра и имя
описателя, вы легко видите, утечка какого из описателей происходит.
Я выследил массу утечек ресурсов и взаимных блокировок с помощью !handle —
это единственный способ получить информацию об описателях в процессе от!
ладки, так что стоит потратить немного времени на ознакомление с ней и выво!
димыми с ее помощью данными.

Общий вопрос отладки
Функции Win32 API, такие как CreateEvent, создающие описатели, имеют
необязательный параметр «имя». Должен ли я назначать имена моим
описателям?
Абсолютно, безусловно, ДА! Команда !handle может показать состояния каж!
дого из ваших описателей. Но это лишь малая часть того, что необходимо
для поиска проблем. Если описатели не именованы, очень трудно сопоста!
вить сами описатели с происходящим в отладчике, скажем, при взаимных
блокировках. Не дав имена своим описателям, вы делаете свою жизнь за!
метно сложнее, чем она должна быть.
Однако вы можете просто пойти и начать давать сногсшибательные имена
в этом необязательном поле. Когда вы создаете событие, например, имя,
даваемое этому событию, такое как «MyFooEventName», глобально для всех
процессов, выполняемых на машине. Хотя можно подумать, что второй
процесс, вызывающий CreateEvent, дает ему уникальное имя внутренними
средствами, на самом деле CreateEvent вызывает OpenEvent и возвращает вам
описатель глобально именованного события. Теперь допустим, что у вас два
исполняющихся процесса и в каждом из них есть поток, ожидающий со!
бытия MyFooEventName. Когда один из процессов сигнализирует о событии,
этот сигнал будут видеть оба процесса и начнут исполняться. Очевидно, что
если вы подразумеваете, что сигнал воспринимается только одним процес!
сом, то вы просто создаете сверхтрудную для отлова ошибку.
Чтобы давать правильные имена описателям, вы должны быть уверены,
что генерируете уникальные имена для всех описателей, сигналы которых
должны восприниматься единственным процессом. Взгляните, что я делал
в WDBG в главе 4: я добавлял идентификатор процесса или потока к имени
для обеспечения уникальности.

358

ЧАСТЬ II

Производительная отладка

Другие интересные команды расширения
Прежде, чем перейти к управлению файлами вывода, хочу отметить несколько
команд расширения, которые вы найдете интересными в критических ситуаци!
ях, например, когда нужно найти какую!то действительно вызывающую ошибку.
Первая — !imgreloc — просто просматривает все загруженные модули и сообща!
ет, были ли все модули загружены в предпочитаемые вами адреса. Теперь у вас нет
оправдания за то, что вы не проверили. Вывод команды выглядит так (ОС пере!
местила второй модуль TP4UIRES):

0:003> !imgreloc
00400000 tp4serv — at preferred address
00c50000 tp4uires — RELOCATED from 00400000
5ad70000 uxtheme — at preferred address
6b800000 S3appdll — at preferred address
76360000 WINSTA — at preferred address
76f50000 wtsapi32 — at preferred address
77c00000 VERSION — at preferred address
77c10000 msvcrt — at preferred address
77c70000 GDI32 — at preferred address
77cc0000 RPCRT4 — at preferred address
77d40000 USER32 — at preferred address
77dd0000 ADVAPI32 — at preferred address
77e60000 kernel32 — at preferred address
77f50000 ntdll — at preferred address
Если вы так ленивы, что не можете вызвать командную строку и вывести команду
NET SEND для посылки сообщения другим пользователям, вы можете просто ввести
!net_send. На самом деле это полезно, если вам нужно привлечь чье!то внимание
в процессе удаленной отладки. Ввод просто !net_send покажет вам необходимые
для посылки сообщения параметры.
Поскольку вы располагаете командой !dreg для вывода информации о регист!
рах, вы также располагаете командой !evlog для отображения журнала событий.
Если каждую из них просто ввести в командной строке, вы получите подсказку,
как их использовать. Обе — прекрасные помощники для просмотра регистров или
журналов событий. Если вы используете их, особенно при удаленной отладке, сюр!
призов не ждите.
Если у вас проблемы с обработкой исключений, команда !exchain поможет
просмотреть цепочки обработки исключений текущего потока и увидеть, какие
функции имеют зарегистрированные обработчики исключений. Вот образец выво!
да команды при отладке программы ASSERTTEST.EXE.

0012ffb0: AssertTest!except_handler3+0 (004027a0)
CRT scope 0, filter: AssertTest!wWinMainCRTStartup+22c (00401e1c)
func: AssertTest!wWinMainCRTStartup+24d (00401e3d)
0012ffe0: KERNEL32!_except_handler3+0 (77ed136c)
CRT scope 0, filter: KERNEL32!BaseProcessStart+40 (77ea847f)
func: KERNEL32!BaseProcessStart+51 (77ea8490)

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

359

Работу с кучами ОС (т. е. кучами, создаваемыми вызовами функции API Create
Heap) облегчит команда !heap. Вы можете думать, что вы не используете никакие
кучи ОС, но код ОС, исполняющийся внутри вашего процесса, к ним обращается.
Вы можете испортить память в одной из этих куч (см. главу 17), а !heap покажет ее.
Наконец, я хочу коснуться очень интересной и недокументированной коман!
ды !for_each_frame из расширения EXT.DLL. Как можно понять из ее имени2 , она
исполняет командную строку, переданную в качестве параметра команды, для
каждого кадра (фрейма) стека. Прекрасный вариант использования этой коман!
ды — !for_each_frame dv, в результате чего будут выведены локальные переменные
каждого кадра стека.

Работа с файлами дампа
Понимая, какие типы команд может исполнять WinDBG, вы можете перейти к
последнему набору команд — командам файлов дампа. Как я уже упоминал в раз!
деле «Основы», сильная сторона WinDBG — управление файлами дампа. Прелесть
WinDBG и файлов дампа заключается в том, что почти все информационные коман!
ды работают и с файлами дампа, причем почти так же, как и тогда, когда возни!
кали проблемы.

Создание файлов дампа
Выполняя «живую» отладку, вы можете вызвать команду .DUMP (Create Dump File —
создать файл вывода), чтобы создать файл дампа. Замечу, что при создании фай!
ла дампа нужно указывать расширение в имени файла. .DUMP производит запись
именно в тот файл, какой вы ей указали (полное имя файла и путь к нему) без
добавления отсутствующего расширения. Вы всегда должны использовать расши!
рение .DMP.
Оставив проблему расширения в стороне, я хочу обсудить некоторые общие
возможности, предлагаемые .DUMP до того, как перейти к типам файлов дампа.
Первый ключ — /u — добавляет дату, время и PID (идентификатор процесса) к
имени файла, чтобы обеспечить уникальные имена файлов дампа без необходи!
мости бороться с их именами. Так как файлы дампа являются столь замечатель!
ным средством выполнения снимков сеанса отладки, позволяющим анализиро!
вать поведение программы позже, /u заметно упрощает вашу жизнь. Чтобы обес!
печить лучшее понимание, что происходило в конкретное время, ключ /c позво!
ляет ввести комментарий, который будет отображаться, когда вы загрузите файл
вывода. Наконец, если вы отлаживаете несколько процессов сразу, ключ /a запи!
шет файлы дампа для всех загруженных процессов. Убедитесь, что вы используе!
те /u совместно с /a, чтобы дать каждому процессу свое имя.
WinDBG может создавать два типа файлов дампа: полный и краткий. Полный
включает все о процессе, от стеков текущих потоков до состояния всей памяти
(даже все загруженные процессом двоичные данные). Он указывается с помощью
ключа /f. Иметь полный файл дампа удобно, так как в нем содержится всего зна!
чительно больше, чем вам необходимо, однако он съедает огромный объем дис!
ковой памяти.
2

For each frame — для каждого кадра. — Прим. перев.

360

ЧАСТЬ II

Производительная отладка

Для создания файла минидампа достаточно указать ключ по умолчанию /m, если
вы не указываете никаких ключей в .DUMP. Записанный таким образом файл ми!
нидампа будет таким же, как и файл минидампа по умолчанию, создаваемый
Visual Studio .NET, и будет содержать версии загруженных модулей, сведения о стеке
для выполнения вызовов стека для всех активных потоков.
Вы также можете указать WinDBG добавить дополнительную информацию к
минидампу, задавая флаги в ключе /m. Самый полезный — h (/mh) — в дополнение
к информации по умолчанию для минидампа запишет информацию об активных
описателях. Это значит, что вы сможете, используя команду !handle, просмотреть
состояния всех описателей, созданных при записи дампа. Если понадобится ана!
лизировать проблемы с указателями, можно указать i (/mi), чтобы WinDBG вклю!
чил в файл вывода вторичную память. Этот ключ просматривает указатели на стек
или страничную память и выводит состояние памяти, на которую ссылается ука!
затель, в виде небольшого участка памяти около этого места. Таким образом, вы
можете узнать, на что ссылаются указатели. Имеется множество других ключей
краткого вывода, которые вы можете указать для записи дополнительной инфор!
мации, но h и i я использую всегда.
Последний ключ, позволяющий сэкономить много дискового пространства, —
/b — сожмет файл дампа в файл .CAB. Это замечательный ключ, но пропущенное
расширение в файле дампа делает его использование проблематичным. Так как
.DUMP не добавляет автоматически расширение, то вам инстинктивно захочется
добавить расширение .CAB к файлу дампа. Однако при указании расширения .CAB
WinDBG создает временный .DMP!файл с именем .CAB.DMP внутри реаль!
ного .CAB!файла. К счастью, WinDBG прекрасно прочтет такой файл из .CAB!файла.
Несмотря на все эти мелкие проблемы с возможностью записи .CAB!файлов,
мне все же очень нравится использовать ее. В дополнение к сохранению только
.DMP!файлов в .CAB!файлах можно указать ключ /ba, если вы хотите также сохра!
нить и таблицу символов в .CAB!файле! Чтобы гарантированно сохранить все
символы процесса, запустите команду ld * (load all symbols — загрузить все сим!
волы) перед созданием файла дампа. Таким образом, вы можете быть уверены, что
вы располагаете всеми корректными символами, когда переносите .CAB!файл на
машину, которая может не иметь доступа к вашему хранилищу символов. Используя
/b, помните, что WinDBG записывает файл дампа и создает соответствующий .CAB!
файл в каталоге %TEMP% машины. Вы, конечно, понимаете, что, имея большой про!
цесс, создавая полный дамп с помощью /f и задавая /ba для создания .CAB!файла
с символами, вам понадобится огромный шмат свободного пространства на дис!
ке в каталоге %TEMP%.

Открытие файлов дампа
Файлы дампа не принесут большой пользы, если вы не умеете открывать их. Про!
ще всего сделать это из нового экземпляра WinDBG. В меню File выберите Open
Crash Dump (открыть файл вывода аварийного завершения) или нажмите Ctrl+D
для вызова диалогового окна Open Crash Dump, а затем найдите каталог, в кото!
ром находится файл дампа. Интересно, хотя это и не описано в документации,
что WinDBG также откроет .CAB!файл, содержащий .DMP!файл. После того как файл

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

361

вывода будет открыт, WinDBG автоматически получает, что было записано, и вы
можете начинать просматривать файл дампа.
Если вы создавали дамп на той же машине, где собирали процесс, ваша жизнь
весьма проста, так как WinDBG проделает всю работу по получению символов и
информации о номерах строк исходного кода. Однако большинство из нас будут
анализировать файлы дампа, созданные на других машинах и других версиях ОС.
После открытия файла дампа начинается работа по получению символов, настройке
путей к исходному коду и исполняемым модулям.
Во!первых, определите, в каких модулях пропущена информация о символах,
запустив команду LM с ключом v. Если какие!то модули сообщают «no symbols loaded»
(символы не загружены), надо настроить путь для поиска символов. Посмотрите
на информацию о версиях, ассоциированную с этими модулями, и соответствен!
но обновите Symbol File Path (путь к файлу символов), выбрав Symbol File Path в
меню File.
Второй шаг — настройка путей к файлам исполняемых образов. Как я уже го!
ворил в главе 2, WinDBG нужен доступ к двоичным файлам до того, как он смо!
жет загрузить символы для минидампов. Если вы следовали моим рекомендаци!
ям и поместили свои программы и необходимые различным ОС двоичные фай!
лы и символы в свой сервер символов, подключить двоичные файлы легко. В ди!
алоговом окне Executable Image Search Path (путь к исполняемому образу), дос!
тупном при выборе Image File Path (путь к файлу образа) меню File, можно про!
сто вставить из буфера обмена ту же строку, что вы указали для символьного сер!
вера. WinDBG автоматически найдет ваш сервер символов для соответствующих
двоичных файлов.
Если двоичных файлов в хранилище символов нет, нужно указать путь вруч!
ную и надеяться, что вы указали его к корректным версиям модулей. Это особен!
но трудно с двоичными файлами ОС, так как очередное исправление может из!
менить любое их количество. В действительности, каждый раз, внося «горячие»
изменения или устанавливая пакет обновлений, вы должны перезагрузить свое
хранилище символов, запустив файл OSSYMS.JS (см. главу 2).
И наконец, необходимо настроить путь к исходным текстам, выбрав Source File
Path (путь к исходному файлу) из меню File. Настроив все три пути, перезагрузи!
те символы командой .RELOAD /f, после которой последует команда LM, позволяю!
щая увидеть все еще некорректные символы. Если минидамп доставлен от заказ!
чика, вам, возможно, не удастся загрузить все двоичные файлы и символы, так как
там может оказаться другой уровень внесенных исправлений или программ тре!
тьих поставщиков, напихавших кучу DLL в другие процессы. Однако ваша цель —
загрузить все символы ваших программ и как можно больше символов ОС. Как!
никак, если вам удалось загрузить все символы, отладка становится простым
делом!

Отладка дампа
Если вам удалось корректно загрузить символы и двоичные файлы, то отладка файла
дампа почти идентична отладке «живьем». Очевидно, что некоторые команды, такие
как BU, не будут работать с файлами дампа, но большинство других будет, особен!

362

ЧАСТЬ II

Производительная отладка

но команды расширения. При возникновении проблем с командами, обратитесь
к таблице окружения в документации по этой команде и проверьте, что вы може!
те использовать ее при отладке файлов дампа.
Если вы имеете несколько файлов дампа сразу, вы также можете отлаживать
их совместно командой .OPENDUMP (Open Dump File — открыть файл дампа). От!
крыв файл дампа таким способом, необходимо дать команду G (Go — запустить),
чтобы WinDBG смог все запустить.
Наконец, команда, доступная только при отладке файла дампа, — .DUMPCAB (Create
Dump File CAB — создать CAB файл дампа) — создаст .CAB!файл из текущего фай!
ла дампа. Если вы добавите параметр –a, все символы будут записаны в этот файл.

Son of Strike (SOS)
Имеется замечательная поддержка для отладки дампов приложений неуправляе!
мого кода, но не для приложений управляемого кода, и, хотя управляемые прило!
жения меньше подвержены появлению в них ошибок, отлаживать их гораздо труд!
нее. Рассмотрим, например, те проекты, в которые были произведены значитель!
ные вложения в COM+ или другие технологии неуправляемого кода. Вы можете и
хотите создавать новые внешние интерфейсы в .NET или компоненты, усиливаю!
щие ваши COM!компоненты путем использования COM interop. Когда эти прило!
жения завершаются аварийно или зависают, вы тут же получаете головную боль,
так как почти невозможно продраться сквозь ассемблерный код, исследовать стеки
вызовов и даже найти исходные тексты и строки для этих .NET!частей прило!
жения.
Чтобы помочь вам увидеть .NET!части дампа или «живого» приложения, неко!
торые очень умные люди в Microsoft сделали расширение отладчика, названное
SOS или Son of Strike («Дитя забастовки»). Основная документация находится в
файле SOS.HTM в каталоге \SDK\v1.1\Tool
Developers Guide\Samples\SOS. Там вы определенно увидите, что «основная» — это
действительно работающий термин. В сущности это список команд расширения
SOS.DLL и краткие сведения об их использовании.
Если вы работаете с большими системами .NET, особенно с тяжелыми тран!
закциями ASP.NET, вам также захочется загрузить 170!страничный PDF!файл «Pro!
duction Debugging for .NET Framework Applications» (Отладка промышленных при!
ложений для .NET Framework) с http://msdn.microsoft.com/library/default.asp?url=/
library/en!us/dnbda/html/DBGrm.asp. Если вы хотите знать, как управлять завис!
шими процессами ASNET_WP.EXE, работать с потенциальными проблемами управ!
ления памятью в .NET и контролировать другие экстремально!пограничные про!
блемы, это прекрасный документ. Его авторы определенно отладили массу жиз!
ненных систем промышленного уровня, и их знание поможет вам преодолеть
многие трудности.
Вы кратко познакомитесь с командами SOS, основываясь на этих двух доку!
ментах и документе сверхоткровенных трюков, но вот как начать работать с SOS
внутри WinDBG, там не сказано. В этом разделе я хочу помочь вам сделать пер!
вые шаги. Надеюсь, вы узнаете здесь достаточно, чтобы понимать документ «Produc!
tion Debugging for .NET Framework Applications». Я не охвачу всего, например, всех

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

363

команд сборщика мусора, так как они рассмотрены в «Production Debugging for
.NET Framework Applications».
Прежде всего я хочу показать простой способ загрузить SOS.DLL в WinDBG.
SOS.DLL является частью собственно .NET Framework, поэтому фокус в том, что!
бы включить нужные каталоги в ваш путь поиска (переменная окружения PATH),
чтобы WinDBG справился с загрузкой SOS.DLL. Вам надо открыть командную строку
MS!DOS и выполнить VSVARS32.BAT, находящийся в каталоге \Common7\Tools. VSVARS32.BAT настраивает вашу среду так, что
все соответствующие каталоги .NET будут включены в ваш путь поиска.
После однократного исполнения VSVARS32.BAT вы получаете возможность за!
грузить SOS.DLL командой .load sos из окна Command WinDBG. WinDBG всегда
помещает последнее загруженное расширение на верхушку цепочки, поэтому
команда !help покажет вам краткий список всех команд SOS.DLL.

Использование SOS
Возможно, лучше всего показать, как пользоваться SOS, на примере. Программа
ExpectApp из набора файлов к этой книге покажет вам, как подступиться к важ!
ным командам. Чтобы сохранить изложение на приемлемом уровне, я написал этот
код, чтобы просто вызвать несколько методов с локальными переменными и в
конце вызвать исключительную ситуацию. Я пройду через отладку примера EXCEPT!
APP.EXE с помощью SOS, чтобы вы увидели, какие команды помогут узнать, где вы
находитесь, когда приложение, использующее управляемый код, валится или за!
висает. Так вам будет проще применять SOS для решения проблем и понимать
«Production Debugging for .NET Framework Applications»
Откомпилировав EXCEPTAPP.EXE и настроив переменные среды, как я описал
выше, откройте EXCEPTAPP.EXE в WinDBG и остановитесь на точке прерывания
загрузчика. Чтобы заставить WinDBG остановиться, когда приложение .NET вы!
зовет исключительную ситуацию, надо сообщить WinDBG о номере исключения,
сгенерированного .NET. Проще всего это можно сделать, щелкнув кнопку Add в
диалоговом окне Event Filters и введя в диалоговом окне Exception Filter 0xE0434F4D.
Затем выберите Enabled в группе Execution и Not Handled в группе Continue. Щел!
кнув OK, вы успешно настроите WinDBG так, чтобы он останавливался каждый
раз, когда вырабатывается исключение .NET. Если значение 0xE0434F4D кажется
чем!то знакомым, узнать, что это такое, поможет команда .formats.
Настроив исключения, запустите EXCEPTAPP.EXE, пока она не остановится на
исключении .NET. WinDBG сообщит о нем, как о первом исключении, и остано!
вит приложение на реальном вызове Win32 API RaiseException. Загрузив SOS ко!
мандой .load sos, выполните !threads (ее вы всегда будете хотеть исполнять пер!
вой в SOS) и вы увидите, какие потоки в приложении или дампе имеют код .NET.
В случае EXCEPTAPP.EXE команда потоков WinDBG ~ указывает, что в приложении
исполняются три команды. Однако команда всеобщей важности !threads показы!
вает, что только потоки 0 и 2 имеют некоторый код .NET (чтобы все поместилось
на странице, я привожу информацию индивидуальных потоков в виде таблицы, в
WinDBG вы видите это как длинные горизонтальные строки):

364

ЧАСТЬ II

Производительная отладка

0:000> !threads
PDB symbol for mscorwks.dll not loaded
succeeded
Loaded Son of Strike data table version 5 from
"e:\WINNT\Microsoft.NET\Framework\v1.1.4322\mscorwks.dll"
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Row Heading
WinDBG Thread ID 0

2

Win32 Thread ID 884 9dc
ThreadObj 00147c60 001631c8
State 20 1220
PreEmptive GCEnabled

Enabled

GC Alloc Context 04a45f24:04a45ff4

00000000:00000000

Domain 00158300 00158300
Lock Count

0

0

APT Ukn Ukn
Exception System.ArgumentException

(Finalizer)

Важная информация в отображении команды !threads содержит поле Domain
(домен), говорящее о том, много ли доменов приложения (AppDomain) исполня!
ется в рамках процесса, и поле Exceptions, которое оказывается перегруженным.
В примере EXCEPTAPP.EXE первый поток вызвал System.ArgumentException, поэтому
вы можете видеть текущее исключение для любого потока. Третий поток EXCEPT!
APP.EXE показывает специальное значение (Finalizer), обозначающее, как вы можете
полагать, завершающий поток процесса. Вы также увидите (Theadpool Worker),
(Threadpool Completion Port) или (GC) в поле Exception. Когда вы видите одно из этих
специальных значений, знайте, что они представляют потоки времени исполне!
ния, а не ваши потоки.
Так как мы определили, что поток WinDBG 0 содержит исключение EXCEPT!
APP.EXE, вы захотите взглянуть на стек вызовов с помощью !clrstack –all, чтобы
увидеть все детали стека, включая параметры и локальные переменные. Хотя
!clrstack имеет ключи для просмотра локальных переменных (l) и параметров
(p), не указывайте их вместе, иначе они аннулируют друг друга. Чтобы увидеть
весь стек вызовов сразу, дайте команду ~*e !clrstack.

** Имейте в виду, что я вырезал отсюда регистры **
0:000> !clrstack –all
Thread 0
ESP
EIP
0012f5e0 77e73887 [FRAME: HelperMethodFrame]
0012f60c 06d3025f [DEFAULT] [hasThis] Void ExceptApp.DoSomething.Doh
(String,ValueClass ExceptApp.Days)

ГЛАВА 8

Улучшенные приемы для неуправляемого кода с использованием WinDBG

365

at [+0x67] [+0x16] c:\junk\cruft\exceptapp\class1.cs:14
PARAM: this: 0x04a41b5c (ExceptApp.DoSomething)
PARAM: value class ExceptApp.Days StrParam
PARAM: unsigned int8 ValueParam: 0x07
0012f630 06d301e2 [DEFAULT] [hasThis] Void ExceptApp.DoSomething.Reh
(I4,String)
at [+0x6a] [+0x2b] c:\junk\cruft\exceptapp\class1.cs:23
PARAM: this: 0x04a41b5c (ExceptApp.DoSomething)
PARAM: class System.String i: 0x00000042
PARAM: int8 StrParam: 77863812
LOCAL: class System.String s: 0x04a45670 (System.String)
LOCAL: value class ExceptApp.Days e: 0x003e5278 0x0012f63c

Похоже, в отображении параметров есть ошибка, так как команда !clrstack не
всегда корректно отображает тип параметра. В методе DoSomething.Doh вы можете
увидеть, что он принимает значения String (StrParam) и Days (ValueParam). Однако
информация PARAM: показывает параметр StrParam как value class ExceptApp.Days и
ValueParam как unsigned int8. К счастью для параметров размерного типа, даже ког!
да тип их неверен, рядом с именем параметра отображается его корректное зна!
чение. В примере с ValueParam переданное значение 7 соответствует перечисле!
нию Fri.
Прежде чем перейти к постижению значений размерных классов и объектов,
я хочу отметить одну команду просмотра стека, которую, возможно, вы найдете
полезной. Если вы работаете над сложными вызовами между .NET и неуправляе!
мым кодом, вам понадобится увидеть стек вызовов, включающий все, и в этом случае
команда !dumpstack — ваш лучший друг. В целом она делает отличную работу, но,
имея полную PDB!базу символов для .NET Framework, она могла бы делать ее луч!
ше. Временами !dumpstack сообщает «Use alternate method which may not work»
(воспользуйтесь альтернативным методом, так как этот может не работать), что,
кажется, указывает на то, что производится попытка просмотреть стек при отсут!
ствии информации о некоторых символах.
Строки LOCAL: показывают, что в DoSomething.Reh имеются две локальных пере!
менных: s (объект String) и e (размерный класс Days). После каждого выводится
шестнадцатеричный адрес, описывающий тип. Для размерного класса Days име!
ются два числа 0x003E5278 и 0x0012F63C. Первое — таблица методов, второе — рас!
положение значения в памяти. Чтобы увидеть это значение в памяти, просто дай!
те команду распечатки содержимого памяти WinDBG, такую как dd 0x0012F63C.
Просмотр таблицы методов, описывающей данные метода, информацию о
модуле и карту интерфейса среди всего прочего осуществляется командой SOS
!dumpmt. Выполнение !dumpmt 0x003E5278 для примера EXCEPTAPP.EXE выводит:

0:000> !dumpmt 0x003e5278
EEClass : 06c03b1c
Module : 001521a0
Name: ExceptApp.Days
mdToken: 02000002 (D:\Dev\ExceptApp\bin\Debug\ExceptApp.exe)
MethodTable Flags : 80000
Number of IFaces in IFaceMap : 3

366

ЧАСТЬ II

Производительная отладка

Interface Map : 003e5380
Slots in VTable : 55
В таблице методов, отображаемой двумя первыми числами, видно, в каком
модуле определен этот метод, а также класс исполняющей системы .NET. Для ин!
терфейсов в документации SOS есть прекрасный пример, как пройтись по кар!
там интерфейсов, и я бы одобрил ваше знакомство с ним. Если у вас есть горячее
желание увидеть все методы в виртуальной таблице конкретного класса или объекта
вместе с описателями методов, можно задать ключ md в команде перед значением
таблицы методов. В случае EXCEPTAPP.EXE и ее размерного класса ExceptApp.Days,
будут перечислены 55 методов. Как указывает документация SOS в разделе «How
Do I… ?», просмотр дескрипторов методов полезен при установке точек прерыва!
ния на конкретных методах.
Так как мы рассматриваем информацию класса и модуля для таблицы методов
ExceptApp.Days, я сделаю небольшое отступление. Как только вы получите адрес класса
исполняющей системы .NET, !dumpclass покажет вам все, что вы только могли по!
желать узнать о классе, в том числе и информацию обо всех полях данных клас!
са. Чтобы увидеть информацию о модуле, дайте команду !dumpmodule. В докумен!
тации есть пример, как с помощью вывода !dumpmodule пройтись по памяти и най!
ти классы и таблицы методов модуля.
Теперь, когда у нас есть основы размерного класса, взглянем осмысленно на
локальную переменную s класса String в DoSomething.Reh:

LOCAL: class System.String s: 0x04a45670 (System.String)
Так как s — объект, то после имени переменной отображается только одно
шестнадцатеричное значение — размещение объекта в памяти. С помощью команды
!dumpobj вы увидите всю информацию об объекте:

0:000> !dumpobj 0x04a45670
Name: System.String
MethodTable 0x79b7daf0
EEClass 0x79b7de3c
Size 92(0x5c) bytes
mdToken: 0200000f (e:\winnt\microsoft.net\framework\v1.1.4322\mscorlib.dll)
String: Tommy can you see me? Can you see me?
FieldDesc*: 79b7dea0
MT
Field Offset
Type
Attr
Value Name
79b7daf0 4000013
4 System.Int32 instance
38 _arrayLength
79b7daf0 4000014
8 System.Int32 instance
37 m_stringLength
79b7daf0 4000015
c System.Char instance
54 m_firstChar
79b7daf0 4000016
0
CLASS
shared static Empty
>> Domain:Value 00158298:04a412f8 > Domain:Value 00158298:04a4130c > и >. Если бы в EXCEPTAPP.EXE
содержалось несколько доменов приложения, вы бы увидели сведения о двух до!
менах и значениях для статического поля WhitespaceChars.
Теперь, когда я осветил некоторые основные команды, я хочу связать их вме!
сте и показать, как с их помощью искать полезную информацию. Так как программа
EXCEPTAPP.EXE остановлена WinDBG в результате возникшего исключения, было
бы хорошо увидеть, какая исключительная ситуация возникла и что содержат
некоторые поля, в результате чего можно было бы узнать, почему EXCEPTAPP.EXE
остановилась.
Из команды !threads мы знаем, что первый поток в настоящее время исполня!
ет исключительную ситуацию System.ArgumentException. Приглядевшись к выводу
команд !clrstack или !dumpstack, вы заметите, что нет никаких локальных пере!
менных или параметров, для которых был бы указан тип System.ArgumentException.
Хорошие новости в том, что хорошая команда показывает все объекты, находя!
щиеся в настоящее время в стеке текущего потока:

0:000> !dumpstackobjects
ESP/REG Object Name
ebx
04a45670 System.String
Tommy can you see me? Can you see me?
0012f50c 04a45f64 System.ArgumentException
0012f524 04a45f64 System.ArgumentException
0012f538 04a45f64 System.ArgumentException
0012f558 04a44bc4 System.String
Reh =
0012f55c 04a45f64 System.ArgumentException
0012f560 04a45670 System.String
Tommy can you see me? Can you see me?
0012f564 04a4431c System.Byte[]
0012f568 04a43a58 System.IO.__ConsoleStream
0012f5a0 04a45f64 System.ArgumentException

Так как !dumpstackobjects бродит по стеку вверх, вы увидите некоторые элементы
много раз, так как они передаются как параметры ко многим функциям. В преды!
дущей распечатке вы могли увидеть несколько объектов System.ArgumentException,
но, посмотрев на значение объекта рядом с каждым объектом, вы заметите, что
все они ссылаются на один и тот же экземпляр объекта 0x04A45F64.
Чтобы увидеть объект System.ArgumentException я использую команду !dumpobj.
Я перенес колонку Name на следующую строку, чтобы все поместилось на одной
странице.

0:000> !dumpobj 04a45f64
Name: System.ArgumentException
MethodTable 0x79b87b84
EEClass 0x79b87c0c
Size 68(0x44) bytes
mdToken: 02000038 (e:\winnt\microsoft.net\framework\v1.1.4322\mscorlib.dll)

368

ЧАСТЬ II

Производительная отладка

FieldDesc*: 79b87c70
MT
Field Offset
79b7fcd4 400001d
4
79b7fcd4 400001e
8

Type
CLASS
CLASS

79b7fcd4 400001f

c

CLASS

79b7fcd4 4000020
79b7fcd4 4000021

10
14

CLASS
CLASS

79b7fcd4 4000022
79b7fcd4 4000023
79b7fcd4 4000024

18
1c
20

CLASS
CLASS
CLASS

79b7fcd4 4000025

24

CLASS

79b7fcd4 4000026

2c System.Int32

79b7fcd4
79b7fcd4
79b7fcd4
79b7fcd4
79b87b84

30 System.Int32
28
CLASS
34 System.Int32
38 System.Int32
3c
CLASS

4000027
4000028
4000029
400002a
40000d7

Attr
Value Name
instance 00000000 _className
instance 00000000
_exceptionMethod
instance 00000000
_exceptionMethodString
instance 04a456cc _message
instance 00000000
_innerException
instance 00000000 _helpURL
instance 00000000 _stackTrace
instance 00000000
_stackTraceString
instance 00000000
_remoteStackTraceString
instance
0
_remoteStackIndex
instance 2147024809 _HResult
instance 00000000 _source
instance
0 _xptrs
instance 532459699 _xcode
instance 04a45708 m_paramName

Важным свойством в исключении является Message. Так как я не могу вызвать
метод прямо из WinDBG, чтобы увидеть это значение, я взгляну на поле _message,
так как это и есть то место, где свойство Message хранит реальную строку. Так как
поле _message помечено как CLASS, то шестнадцатеричное число в столбце Value
является экземпляром объекта. Чтобы увидеть этот объект, я выполню еще одну
команду !dumpobj, чтобы просмотреть его. Как мы видим, объект String имеет спе!
циальное поле, поэтому мы можем видеть его действительное значение, которое
выворачивается в безобидное «Thowing an exception».

0:000> !dumpobj 04a456cc
Name: System.String
MethodTable 0x79b7daf0
EEClass 0x79b7de3c
Size 60(0x3c) bytes
mdToken: 0200000f (e:\winnt\microsoft.net\framework\v1.1.4322\mscorlib.dll)
String: Thowing an exception
FieldDesc*: 79b7dea0
MT
Field Offset
Type
Attr
Value Name
79b7daf0 4000013
4 System.Int32 instance
21 m_arrayLength
79b7daf0 4000014
8 System.Int32 instance
20 m_stringLength
79b7daf0 4000015
c System.Char instance
54 m_firstChar
79b7daf0 4000016
0
CLASS
shared static Empty
>> Domain:Value 00158298:04a412f8 > Domain:Value 00158298:04a4130c 0) Then
DumpElements(ow, SubCodeElems, Level + 1)
End If
End If
Next
End Sub

CommenTater: лекарство
от распространенных проблем?
Одним из абсолютно бесценных свойств C# являются документирующие коммен!
тарии XML. Так называют тэги XML, содержащиеся в комментариях, описывающих
свойства или методы в конкретном файле. Фактически IDE помогает вам, автома!
тически включая такие комментарии для конструкций программы, перед которыми
вы пишете ///. Есть три очень веских причины, почему всегда следует заполнять
документирующие комментарии C#. Во!первых, это стандартизирует коммента!
рии между отдельными группами и во всей вселенной C#. Во!вторых, технология
IntelliSense среды разработки автоматически отображает информацию, указанную
в тэгах и , что облегчает использование вашего кода другими
программистами, предоставляя им гораздо больше данных об элементах вашей
программы. Если код является частью проекта, для получения преимуществ доку!
ментирующих комментариев ничего делать не нужно. Если вы предоставляете
решение только в двоичной форме, документирующие комментарии могут быть
собраны в XML!файл при компиляции, поэтому и в такой ситуации вы можете
предоставить пользователям отличный набор подсказок. Для этого нужно только
разместить итоговый XML!файл в том же каталоге, что и двоичный файл, и Visual
Studio .NET будет автоматически отображать комментарии в подсказках IntelliSense.

380

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

Наконец, при помощи XSLT из итогового XML!файла можно создать полную
систему документации к вашей программе. Заметьте, что я не имею в виду коман!
ду Build Comment Web Pages (создать Web!страницы комментариев) из меню Tools.
Эта команда не учитывает много важной информации, например, тэги ,
поэтому она не так уж и полезна. Как я покажу чуть ниже, для генерирования
документации можно использовать гораздо лучшие средства.
Чтобы максимально эффективно документировать свой код, изучите в доку!
ментации к Visual Studio .NET все, что касается тэгов комментариев XML. Для со!
здания файла XML!документа откройте окно Property Pages (страницы свойств),
папку Configuration Properties (конфигурационные свойства), страницу Build (сбор!
ка программы) и заполните поле XML Documentation File (файл XML!документа!
ции) (рис. 9!3). Это поле нужно заполнять отдельно для каждой конфигурации,
чтобы файл документации создавался при каждой сборке программы.

Рис. 93. Установка ключа командной строки /doc для создания
файла документирующих комментариев XML
Чтобы вы могли создать полный вывод из файлов комментариев XML, я раз!
местил в каталоге DocCommentsXSL на CD файл трансформации XSL и каскадную
таблицу стилей. Однако гораздо лучше использовать средство NDoc, которое можно
загрузить по адресу http://ndoc.sourceforge.net. NDoc обрабатывает XML!коммен!
тарии и создает файл помощи HTML, который выглядит в точности, как докумен!
тация MSDN к библиотеке классов .NET Framework. NDoc даже предоставляет ссылки
на общие методы вроде GetHashCode, так что из него вы можете переходить прямо
в документацию MSDN! NDoc — прекрасный способ документирования кода ва!
шей группы, и я настоятельно рекомендую его использовать. Благодаря реализо!
ванной в Visual Studio .NET 2003 обработке программы после ее сборки (post build
processing) вы можете с легкостью включить NDoc в свой процесс сборки.
Так как документирующие комментарии настолько важны, мне захотелось
разработать метод автоматического их добавления в мой код C#. Примерно в то
же время, когда я об этом подумал, я обнаружил, что окно Task List (список зада!
ний) автоматически отображает все комментарии, начинающиеся с ключевых фраз
вроде «TODO», когда вы нажимаете в нем правую кнопку и выбираете в меню Show
Tasks (показать задания) пункт All (все) или Comment (комментарии). Я решил

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

381

создать макрос или надстройку, которые добавляли бы все пропущенные докумен!
тирующие комментарии и обрабатывали в них фразу «TODO», чтобы можно было
легко просматривать комментарии и гарантировать их правильное заполнение.
Результатом стал CommenTater. Вот пример метода, обработанного CommenTater:

///
/// TODO  Add Test function summary comment
///
///
/// TODO  Add Test function remarks comment
///
///
/// TODO  Add x parameter comment
///
///
/// TODO  Add y parameter comment
///
///
/// TODO  Add return comment
///
public static int Test ( Int32 x , Int32 y )
{
return ( x ) ;
}
Visual Studio .NET делает перебор элементов кода в исходном файле тривиаль!
ной задачей, поэтому я был очень доволен, так как думал, что мне нужно будет
только просмотреть элементы кода, получить строки, предшествующие любому
методу или свойству, и вставить комментарии в случае их отсутствия. Когда я
обнаружил, что все элементы кода имеют свойство DocComment, возвращающее дей!
ствительный комментарий для данного элемента, я тут же снял шляпу перед раз!
работчиками за то, что они продумали все заранее и сделали элементы кода по!
настоящему полезными. Теперь мне нужно было только присвоить свойству Doc
Comment нужное значение, и все было бы чудесно.
Возможно, сейчас вам следует открыть файл CommenTater.VB из каталога Com!
menTater. Исходный код этого макроса слишком объемен, чтобы воспроизводить
его в книге, поэтому следите за моими мыслями по файлу. Моя основная идея
заключалась в создании двух процедур, AddNoCommentTasksForSolution и CurrentSource
FileAddNoCommentTasks. Вы можете по их именам сказать, на каком уровне они ра!
ботают. Большей частью базовый алгоритм похож на примеры из листинга 9!1: я
просто просматриваю все элементы кода и использую их свойства DocComment.
Первая проблема, с которой я столкнулся, была связана с тем, что я считаю
небольшим недостатком объектной модели элементов кода. Свойство DocComment
не является общим для класса CodeElement, который может быть использован в
качестве базового класса для любого общего элемента кода. Поэтому мне пришлось
преобразовать общий объект CodeElement в действительный тип элемента, опира!
ясь на свойство Kind. Вот почему процедура RecurseCodeElements содержит большой
оператор Select…Case.

382

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

Вторая проблема была полностью на моей совести. Я почему!то никогда не
осознавал, что со свойством DocComment конструкции кода нужно обращаться, как
с полноценным XML!фрагментом. Я создавал нужную строку комментария, но, когда
я пытался назначить ее свойству DocComment, генерировалось исключение Argument
Exception. Я был очень озадачен этим, так как думал, что свойство DocComment до!
пускает чтение и запись, но на деле все выглядело так, будто оно поддерживало
только чтение. Из!за какого!то помутнения я не понимал, что генерирование
исключения объяснялось тем, что я не заключал документирующие комментарии
XML в элементы . Вместо этого я решил, что столкнулся с непонятной
проблемой, и стал искать альтернативные средства включения текста комментария.
Так как отдельные элементы кода имеют свойство StartPoint, мне просто нуж!
но было создать соответствующий объект EditPoint и ввести текст. Эксперимен!
ты быстро показали, что все работало правильно, и я начал разрабатывать набор
процедур для добавления текста. Делать это вручную требуется не так уж и редко,
поэтому я закомментировал первоначальные процедуры и оставил в конце фай!
ла CommenTater.VB.
Первую версию макроса я часто использовал в своих проектах. Иногда макро!
сы могут быть слишком медленными, поэтому я рассматривал возможность пре!
образования CommenTater в полноценную надстройку, но меня его скорость все!
гда устраивала. Первая версия CommenTater только добавляла пропущенные ком!
ментарии. Это было прекрасно, но скоро я понял, что мне по!настоящему хочет!
ся, чтобы CommenTater был умнее и сравнивал имеющиеся комментарии к функ!
циям с тем, что на самом деле присутствует в коде. При изменении прототипов
функций, скажем, при добавлении/удалении параметров, я часто забываю обно!
вить соответствующие комментарии. Добавив эту функциональность сравнения,
я сделал бы CommenTater еще полезнее.
Начав думать о том, что потребуется для обновления существующих коммен!
тариев, я слегка загрустил. Если вы помните, в тот момент я думал, что свойство
DocComment допускает только чтение, поэтому я решил, что для правильного обнов!
ления комментариев придется выполнять значительный объем манипуляции с
текстом, и это меня не привлекало. Однако, когда я взглянул на CommenTater в
отладчике макросов, на меня снизошло радостное озарение, и я понял, что для
записи в свойство DocComment нужно просто размещать вокруг каждого коммента!
рия элементы . Когда я преодолел собственную глупость, написать
процедуру ProcessFunctionComment оказалось гораздо проще (листинг 9!2).
В этот момент в игру вступила мощь библиотеки классов Microsoft .NET Frame!
work. Чтобы выполнить всю трудную работу, нужную для получения информации
из существующих строк документирующих комментариев и их преобразования,
я использовал прекрасный класс XmlDocument. Процедура ProcessFunctionComment должна
была поддерживать переупорядочение комментариев, поэтому я должен был по!
добрать порядок размещения отдельных узлов в файле. Хочу отметить, что я фор!
матирую комментарии так, как мне нравится, поэтому CommenTater может изме!
нить тщательное форматирование ваших комментариев, но никакой информации
он не выбросит.

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

Листинг 9-2.

383

Процедура ProcessFunctionComment из файла CommenTater.VB

' Эта процедура получает имеющиеся комментарии к функциям
' и гарантирует, что все в порядке. Она может преобразовывать
' ваши комментарии, поэтому вы можете захотеть изменить ее.
Private Sub ProcessFunctionComment(ByVal Func As CodeFunction)
Debug.Assert("" Func.DocComment, """"" Func.DocComment")
' Объект, содержащий исходный документирующий комментарий.
Dim XmlDocOrig As New XmlDocument()
' ЭТО ЗДОРОВО! После присвоения свойству PreserveWhitespace
' значения True класс XmlDocument будет отвечать почти за все,
' что касается форматирования...
XmlDocOrig.PreserveWhitespace = True
XmlDocOrig.LoadXml(Func.DocComment)
Dim RawXML As New StringBuilder()
' Получение узла "summary".
Dim Node As XmlNode
Dim Nodes As XmlNodeList = XmlDocOrig.GetElementsByTagName("summary")
If (0 = Nodes.Count) Then
RawXML.Append(SimpleSummaryComment(Func.Name, "function"))
Else
RawXML.AppendFormat("{0}", vbCrLf)
For Each Node In Nodes
RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)
Next
RawXML.AppendFormat("{0}", vbCrLf)
End If
' Получение узла "remarks".
Nodes = XmlDocOrig.GetElementsByTagName("remarks")
If (Nodes.Count > 0) Then
RawXML.AppendFormat("{0}", vbCrLf)
For Each Node In Nodes
RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)
Next
RawXML.AppendFormat("{0}", vbCrLf)
ElseIf (True = m_FuncShowsRemarks) Then
RawXML.AppendFormat("{0}TODO  Add {1} function " + _
"remarks comment{0}", _
vbCrLf, Func.Name)
End If
' Получение всех параметров, описанных в документирующих комментариях.
Nodes = XmlDocOrig.GetElementsByTagName("param")
см. след. стр.

384

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

' Имеет ли функция параметры?
If (0 Func.Parameters.Count) Then
' Занесение всех существующих параметров комментариев
' в хэштаблицу с именем параметра в качестве ключа.
Dim ExistHash As New Hashtable()
For Each Node In Nodes
Dim ParamName As String
Dim ParamText As String
ParamName = Node.Attributes("name").InnerXml
ParamText = Node.InnerText
ExistHash.Add(ParamName, ParamText)
Next
' Просмотр параметров.
Dim Elem As CodeElement
For Each Elem In Func.Parameters
' Есть ли этот элемент в хэше заполненных параметров?
If (True = ExistHash.ContainsKey(Elem.Name)) Then
RawXML.AppendFormat("{1}{2}{1}" + _
"{1}", _
Elem.Name, _
vbCrLf, _
ExistHash(Elem.Name))
' Удаление этого ключа.
ExistHash.Remove(Elem.Name)
Else
' Был добавлен новый параметр.
RawXML.AppendFormat("{1}TODO  Add " + _
"{0} parameter comment{1}{1}", _
Elem.Name, vbCrLf)
End If
Next
' Если в хэштаблице чтото осталось, параметр был или удален,
' или переименован. Я добавлю описания оставшихся параметров
' с пометками TODO, чтобы пользователь мог удалить их вручную.
If (ExistHash.Count > 0) Then
Dim KeyStr As String
For Each KeyStr In ExistHash.Keys
Dim Desc = ExistHash(KeyStr)
RawXML.AppendFormat("{1}{2}{1}{3}" + _
"{1}{1}", _
KeyStr, _
vbCrLf, _
Desc, _
"TODO  Remove param tag")
Next
End If
End If

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

385

' Обработка возвращаемых значений, если таковые имеются.
If ("" Func.Type.AsFullName) Then
Nodes = XmlDocOrig.GetElementsByTagName("returns")
' Обработка узлов "returns".
If (0 = Nodes.Count) Then
RawXML.AppendFormat("{0}TODO  Add return comment" + _
"{0}{0}", _
vbCrLf)
Else
RawXML.AppendFormat("{0}", vbCrLf)
For Each Node In Nodes
RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)
Next
RawXML.AppendFormat("{0}", vbCrLf)
End If
End If
' Обработка узлов "example".
Nodes = XmlDocOrig.GetElementsByTagName("example")
If (Nodes.Count > 0) Then
RawXML.AppendFormat("{0}", vbCrLf)
For Each Node In Nodes
RawXML.AppendFormat("{0}{1", Node.InnerXml, vbCrLf)
Next
RawXML.AppendFormat("{0}", vbCrLf)
End If
' Обработка узлов "permission".
Nodes = XmlDocOrig.GetElementsByTagName("permission")
If (Nodes.Count > 0) Then
For Each Node In Nodes
RawXML.AppendFormat("{1}", _
Node.Attributes("cref").InnerText, _
vbCrLf)
RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)
RawXML.AppendFormat("{0}", vbCrLf)
Next
End If
' Наконец, узлы "exception".
Nodes = XmlDocOrig.GetElementsByTagName("exception")
If (Nodes.Count > 0) Then
For Each Node In Nodes
RawXML.AppendFormat("{1}", _