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

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


 [Настройки текста]  [Cбросить фильтры]

Бурные аплодисменты рецензентов
Если вы стали 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}", _
Node.Attributes("cref").InnerText, _
vbCrLf)
RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)
см. след. стр.

386

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

RawXML.AppendFormat("{0}", vbCrLf)
Next
End If
Func.DocComment = FinishOffDocComment(RawXML.ToString())
End Sub
Разработав обновление документирующих комментариев, я подумал, что не!
плохо было бы реализовать обработку контекста отмены. Благодаря этому вы могли
бы в случае ошибки нажать Ctrl+Z и восстановить все изменения, сделанные Com!
menTater. Увы, контекст отмены представляет реальную проблему. Когда у меня
нет открытого контекста отмены, все изменения вносятся в документирующие
комментарии прекрасно. Однако при открытии перед внесением изменений кон!
текста отмены все путается и выглядит так, будто контекст отмены и элементы
кода мешают друг другу. Когда CommenTater выполняет запись в свойство DocCom
ment, стартовые точки элементов кода не обновляются, в результате чего обнов!
ление происходит по старым позициям, повреждая файл. Я обнаружил, что, если
вместо использования контекста отмены для глобального внесения всех измене!
ний применять его для обновления каждого комментария к методу или свойству,
все работает. Это хуже, чем глобальная отмена всех изменений, но хоть какая!то
форма отмены. Надеюсь, Microsoft решит проблему с контекстом отмены, чтобы
вы могли использовать его глобально для отмены крупномасштабных изменений.
Одна интересная проблема была связана с зарезервированными символами XML,
которые вполне могут содержаться в именах функций. Ваша функция может на!
зываться operator &, но вторая попытка использовать символ & в документирую!
щем комментарии XML приведет к исключению, указывающему на некорректный
символ. Конечно, это же справедливо для символов > и также вызовут проблемы. Чтобы синтакси!
ческий анализатор XML не жаловался, функция BuildFunctionComment в CommenTater
производит все нужные замены (например, подставляет &amp; вместо &).
CommenTater — очень полезный макрос, но вы могли бы внести в него одно
прекрасное дополнение, работая над которым вы к тому же очень многое узнали
бы об объектной модели IDE. Учитывая наличие тэга , вы могли бы
документировать генерируемые функцией исключения. Попробуйте сделать так,
чтобы ваш код искал функцию каждого оператора throw и автоматически добав!
лял новый элемент для этого конкретного типа исключения. Конечно, когда ме!
тод больше не генерирует исключение, вам следует отмечать соответствующие тэги
как требующие удаления.

Стандартный вопрос отладки
Есть ли какие-нибудь хитрости отладки макросов и надстроек,
написанных на управляемом коде?
Приступив к созданию макросов и надстроек, вы очень быстро заметите,
что IDE Visual Studio .NET безумно любит поглощать исключения. Конечно,
IDE хочет, чтобы никакие исключения не нарушали ее работу, но она с та!
ким усердием пережевывает все необработанные исключения, что вы мо!

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

387

жете даже не подозревать, что ваша программа генерирует исключения. Когда
я разрабатывал свои первые макросы, я минут 20 сидел, удивляясь, почему
я не могу достигнуть установленной точки прерывания.
В конце концов, чтобы исключить сюрпризы, я открыл окно Exceptions,
выбрал в дереве исключений узел Common Language Runtime Exceptions
(исключения общеязыковой исполняющей среды) и отметил в блоке When
The Exception Is Thrown (что делать при генерировании исключения) пункт
Break Into The Debugger (выходить в отладчик) (рис. 9!4). Вероятно, после
этого вы гораздо чаще будете прерываться в отладчике, но зато это изба!
вит вас от любых сюрпризов.
На рис. 9!4 вы можете увидеть, что я не выбрал узел JScript Exceptions,
потому что при разработке надстроек я не использую JScript .NET. Если вы
достаточно храбры для создания надстроек на JScript .NET, выберите и этот
узел, чтобы прерываться на всех исключениях.

Рис. 94. Параметры окна Exceptions, приказывающие выходить
в отладчик при всех исключениях
А вот при отладке макросов я заметил, что, даже если в окне Exceptions
указано выходить в отладчик при всех исключениях, это может быть не так.
Чтобы отладчик макросов начал работать правильно, после настройки окна
Exceptions установите где!нибудь в своем коде точку прерывания.

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

388

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

쐽 добавление в IDE собственных инструментальных и диалоговых окон;
쐽 добавление в IDE собственных командных панелей (т. е. меню и панелей ин!
струментов);
쐽 добавление собственных страниц свойств в диалоговое окно Options.
Как вы скоро увидите, разрабатывать и отлаживать надстройки намного труд!
нее, чем макросы, поэтому я рекомендую пытаться сделать все, что можно, используя
макросы, и только в случае неудачи приступать к сражению с надстройками.
По своей сути надстройки — это объекты COM, подключающиеся к IDE. Если
вы волновались о том, как бы не забыть все, что вы изучили о COM за последние
годы, не беспокойтесь: кое!что вам понадобится в мире надстроек. Интересно, что
вы можете писать свои надстройки на Visual Basic .NET или C#, потому что управ!
ляемые языки тоже поддерживают COM. Как и многим из вас, мне очень нравит!
ся C++, но повышение производительности, обеспечиваемое .NET, мне нравится
еще больше, поэтому в этой главе я буду основное внимание уделять вопросам,
связанным с созданием надстроек на управляемых языках.
Как обычно, путешествие в мир надстроек следует начать с документации.
Посетите также страницу http://msdn.microsoft.com/vstudio/downloads/samples/
automation.asp, которая содержит все примеры надстроек и мастеров от Microsoft.
Вам непременно захочется провести за чтением кода этих примеров немало вре!
мени, так как лучшего способа обучения еще никто не придумал.
Многие надстройки реализованы на нескольких языках, поэтому никаких про!
блем в данном смысле возникнуть не должно. Некоторые из более сложных при!
меров, таких как RegExplore, доступны только на C++. Замечу, что код C++ в при!
мерах Microsoft — прекрасный пример плохого программирования. Значитель!
ная часть кода изобилует магическими макросами, которые обрабатывают ошиб!
ки при помощи goto и основаны на предполагаемых именах. Печально, но похо!
жий код генерирует и мастер создания надстроек (Add!In wizard). Если вы реши!
те писать надстройки на C++, не следуйте примеру Microsoft!
Не знаю, как насчет всего остального, но вам обязательно захочется исполь!
зовать пакет Unsupported Tools (неподдерживаемые средства). Вы можете или загру!
зить его с указанного сайта, или найти его текущую версию на CD, в каталоге
UnsupportedAddInTools. Этот пакет включает программу Generate ICO Data for
Extensibility (генерирование данных ICO для системы расширяемости) (Generate!
IcoData.exe), которая генерирует шестнадцатеричные данные для значка, нужные
для его вывода в окне About. Как это сделать, я покажу ниже. В Unsupported Tools
входит также отличная надстройка Extensibility Browser (браузер расширяемос!
ти) (ExtBrws.dll), которая отображает все свойства с поздним связыванием для
объекта DTE (Development Tools Environment, среда инструментов разработки),
корневого объекта в модели расширяемости Visual Studio .NET. Так как некоторые
из этих свойств не очень хорошо описаны в документации, отображение их при
помощи ExtBrws.dll может оказаться полезным. Если вы считаете себя опытным
COM!программистом, можете просмотреть эти свойства, используя программу OLE/
COM Object Viewer.

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

389

Исправление кода, сгенерированного
мастером Add-In Wizard
Если надстройку C# или Visual Basic .NET вы создаете, используя мастер Add!In
Wizard среды Visual Studio .NET, который можно найти в диалоговом окне New
Project (новый проект) в каталоге Other Projects\Extensibility Projects (другие про!
екты\проекты расширяемости), сгенерированный им код может требовать неко!
торых исправлений. В этом разделе я хочу рассказать о том, что сделать сразу после
создания скелета надстройки, чтобы облегчить процесс разработки и не обезу!
меть, решая проблемы в созданном коде. По ходу дела я укажу на ряд важных
подробностей работы надстроек.
Нажав кнопку Finish в мастере надстроек, в самую первую очередь надо открыть
редактор реестра. Мастер надстроек создает некоторые параметры реестра, ко!
торые нужно экспортировать в REG!файл. Путь к нужному разделу реестра начи!
нается или на HKEY_LOCAL_MACHINE, или на HKEY_CURRENT_USER в зависимости от того,
указали ли вы, чтобы надстройка была доступна всем пользователям. Оставшаяся
часть пути одинакова: \Software\Microsoft\VisualStudio\7.1\AddIns\.
Сохраните все параметры, относящиеся к этому разделу, который далее я буду
называть разделом надстройки.
Изучив содержание раздела, созданного мастером надстроек, вы заметите, что
роль некоторых параметров, например, AboutBoxDetails, AboutBoxIcon, FriendlyName
и Description, в пояснениях не нуждается. Пара других параметров требует более
подробного рассмотрения, так как они очень важны для отладки и разработки
надстройки. Первый — CommandPreload — определяет, приказать ли надстройке за!
регистрировать команды, которые она, возможно, хочет зарегистрировать. Мно!
гие из моих проблем при отладке надстроек были связаны с некорректной реги!
страцией команд.
Описание CommandPreload в документации, похоже, ошибочно: это не булево поле.
Когда CommandPreload имеет значение 1, Visual Studio .NET загружает надстройку для
регистрации ее команд; если 2 — полагает, что надстройка уже зарегистрировала
свои команды. Если у вас возникли проблемы с выполнением команд надстрой!
ки, присвойте CommandPreload значение 1 и перезагрузите IDE, чтобы гарантиро!
вать их регистрацию.
Параметр LoadBehavior характеризует загрузку надстройки. Если данное бито!
вое поле равно 0, значит, ваша надстройка не загружается. Значение 1 указывает,
что надстройка должна быть загружена при запуске IDE; 4 — что надстройка дол!
жна быть загружена при компоновке программы из командной строки. В Visual
Studio .NET 2002 была одна проблема: при компоновке из командной строки над!
стройки загружались всегда, даже если вы указывали не использовать их в таких
случаях. К счастью, в Visual Studio .NET 2003 эта ошибка исправлена.
Есть два параметра реестра, которые не создаются мастером надстроек по
умолчанию, но их нужно добавить, если вы хотите использовать собственные
растровые изображения на панели команд или другие ресурсы Win32. Это пара!
метры SatelliteDllName и SatelliteDllPath. Работать с собственными управляемы!
ми растровыми изображениями и ресурсами в управляемых надстройках было бы
весьма удобно, но Visual Studio .NET требует только COM, поэтому вы должны
поместить свои ресурсы в DLL ресурсов Microsoft Win32. Как можно догадаться

390

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

по названиям, SatelliteDllName содержит только имя DLL, а SatelliteDllPath содержит
путь к сателлитной DLL. В документации к SatelliteDllPath говорится, что IDE в
конечном счете будет искать DLL по указанному пути (во время предыдущих по!
пыток поиска к указанному пути прибавляются региональные идентификаторы),
но не загрузит ее оттуда, и вы не получите никаких ресурсов. Например, если
SatelliteDllPath содержит путь C:\FOO\ и вы работаете на компьютере, настро!
енном на американский вариант английского языка, ваша сателлитная DLL долж!
на находиться в каталоге C:\FOO\1033.
Задав сателлитную DLL, вы можете локализовать значения, указываемые в раз!
деле реестра вашей надстройки. Если указанное вами строковое значение состо!
ит из символа #, за которым следует число, IDE будет искать это значение в стро!
ковой таблице вашей сателлитной DLL. Сателлитные DLL используются обеими
надстройками из этой главы: и SuperSaver, и SettingsMaster.
Нам осталось рассмотреть один странный параметр реестра — AboutBoxIcon. Он
содержит код значка надстройки, который вы хотите вывести в окне About. Как я
говорил выше, этот шестнадцатеричный код может быть сгенерирован програм!
мой GenerateIcoData из состава Unsupported Tools. Его нужно скопировать в поле
параметра AboutBoxIcon, имеющее тип REG_BINARY.
Оба проекта — и SuperSaver, и SettingsMaster — включают файлы .ADDIN.REG, присваивающие нужные значения всем параметрам обеих надстро!
ек. Эти REG!файлы позволяют удалять и быстро восстанавливать нужные значе!
ния реестра, облегчая установку. Единственный их недостаток в том, что вы дол!
жны жестко закодировать значение SatelliteDllPath.
Наведя порядок со значениями реестра, нужно заняться исправлением сгене!
рированного мастером кода. Вероятно, сначала вам захочется изменить атрибут
ProgId, ассоциированный с созданным классом Connect. Мастеру нравится добав!
лять к имени надстройки слово «.Connect», что излишне. К сожалению, мастер
надстроек во многих местах жестко кодирует имя команды, поэтому, если вы уда!
лите из атрибута ProgId слово «.Connect», измените еще несколько мест:
쐽 раздел надстройки в реестре;
쐽 использование команды в методе QueryStatus (файл CONNECT.CS/.VB);
쐽 использование команды в методе Exec (файл CONNECT.CS/.VB).
Я настоятельно рекомендую создавать для имен команд константы и исполь!
зовать их везде, где требуются имена. Для своих надстроек я создал файл RESCON!
STANTS.CS/.VB, содержащий все константы для всех команд. Так я исключаю про!
блемы с опечатками, и, если мне хочется изменить имя команды, сделать это очень
просто.
Наверное, самый большой недостаток кода, сгенерированного мастером, в том,
что он глотает исключения при регистрации команд и добавлении элементов на
панели инструментов. Когда я только начал разрабатывать надстройки, я недоуме!
вал, почему некоторые из моих команд недоступны. Оказалось, они не регистри!
ровались, потому что регистрация генерировала исключение, вызывавшее пропуск
оставшейся части функции. Сгенерированный мастером код похож на следующий
фрагмент, и это довольно опасно. Выполняя обзоры кода, вы обязательно долж!
ны убеждаться, что пустые выражения catch представляют собой что!то действи!
тельно безопасное.

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

391

try
{
Command command = commands.AddNamedCommand (...) ;
CommandBar commandBar = (CommandBar)commandBars["Tools"] ;
CommandBarControl commandBarControl =
command.AddControl ( commandBar ,
1
) ;
}
catch(System.Exception /*e*/)
{
}
Все действия по созданию команд и панелей инструментов выполняются в
надстройках по умолчанию в методе OnConnection, когда параметр режима под!
ключения содержит значение ext_cm_UISetup. Я всегда выношу создание команд и
панелей инструментов в отдельный метод, за пределы OnConnection. Между прочим,
при режиме подключения ext_cm_UISetup ваша надстройка выгружается сразу после
возврата из метода OnConnection. При режиме подключения ext_cm_Startup или
ext_cm_AfterStartup надстройка перезагружается.
Перед регистрацией своих команд и добавлением командных панелей удаляйте
все команды и панели инструментов, которые вы, возможно, уже добавили. Так
вы гарантируете, что любые регистрируемые вами для надстройки команды и па!
нели команд создаются «свежими». Удаление добавленных команд и панелей ин!
струментов позволит также безопасно изменять параметры команд или команд!
ных панелей и избегать проблем с исключениями, возможных при наличии пре!
дыдущих элементов с тем же именем.
Для облегчения разработки надстроек я всегда создаю макрос, удаляющий
команды и командные панели, создаваемые моими надстройками. Такой макрос
я могу использовать и для уничтожения следов надстройки. Перед запуском мак!
роса, удаляющего команды, ваша надстройка должна быть выгружена. Это зна!
чит, что вы должны отключить надстройку в диалоговом окне Add!In Manager
(диспетчер надстроек), закрыть запущенные копии IDE и удалить раздел надстрой!
ки в реестре.
Если методам создания команд и панелей инструментов что!то не нравится,
они генерируют исключения. Обязательно помещайте все, что можно, в блоки
try...catch и сообщайте о причинах исключений, чтобы знать, что происходит.
Примеры удаления и установки команд и командных панелей вы увидите в коде
надстроек SuperSaver и SettingsMaster.

Решение проблем с кнопками панелей инструментов
Исправив код, сгенерированный мастером надстроек, вы, вероятно, столкнетесь
с проблемой правильного показа растровых изображений на панелях инструмен!
тов. Решить ее нетрудно, но это не описано в документации. Поиск заклинаний
потребовал от меня некоторых усилий, поэтому я надеюсь, что это обсуждение
поможет вам сэкономить время и сберечь нервы.
Для загрузки на панель инструментов собственные растровые изображения
нужно разместить в сателлитной DLL Win32; встроенные управляемые растровые
изображения на панелях инструментов использовать нельзя. Создавая команду

392

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

методом Commands.AddNamedCommand, вы должны передать ему значение false в пара!
метре MSOButton и идентификатор ресурса растрового изображения в сателлитной
DLL в параметре Bitmap.
Самые неприятные проблемы с размещением собственных растровых изоб!
ражений на панелях инструментов — самиизображения! Во!первых, поддержи!
ваются только 16!цветные изображения. Если вы находите свое изображение стран!
ным, значит, в нем используется больше цветов. Вторая проблема состоит в полу!
чении правильной маски.
При взгляде на растровые изображения в надстройке RegExplorer мне показа!
лось, что в них в качестве цвета маски применяется зеленый. Получить правиль!
ную маску очень важно, так как именно благодаря этому ваше изображение мо!
жет казаться трехмерным при наведении на него курсора. При создании изобра!
жений для своих кнопок я также использовал в качестве маски зеленый цвет. Но,
когда я загрузил свою надстройку, маска определенно не работала, и все места,
которые я хотел сделать прозрачными, имели безобразный ярко!зеленый цвет. В
ходе расследования я выяснил, что на самом деле в качестве маски нужно было
использовать не истинный зеленый, а цвет с RGB!значением 0, 254, 0 (зеленому
соответствует 0, 255, 0).
Однако даже после изменения в палитре значения зеленого цвета на 0, 254, 0
маска осталась зеленой. Оказалось, что я использовал устаревший графический
редактор, который так хотел мне «помочь», что «исправлял» палитру, отображая
зеленый как 0, 255, 0, а не так, как я хотел. Тогда я с помощью редактора растро!
вых изображений Visual Studio .NET переназначил один из цветов палитры (я всегда
устанавливаю вместо розового, принятого по умолчанию, цвет 0, 254, 0), и все по!
лучилось. Помните: когда вы откроете растровое изображение в редакторе Visual
Studio .NET, он изменит зеленый цвет в палитре на 0, 254, 0, потому что это бли!
жайший цвет к зеленому. Это значит, что, если вы хотите использовать в своем
изображении истинный зеленый цвет, вам придется изменить значение другого
элемента на 0, 255, 0.
Исправив цвет маски, следует обновить и свой код, чтобы гарантировать, что
ваши панели инструментов не будут отличаться от других. Кнопки панелей инст!
рументов, добавляемые к объекту CommandBar, по умолчанию отображаются как
кнопки с текстом. Чтобы они отображались как стандартные, нужно вручную
перебрать все элементы CommandBarControl объекта CommandBar и присвоить им стиль
MsoButtonStyle.msoButtonIcon. Вот как я сделал это в SuperSaver:

foreach ( CommandBarControl ctl in
SuperSaverCmdBar.Controls )
{
if ( ctl is CommandBarButton )
{
CommandBarButton btn = (CommandBarButton)ctl ;
btn.Style = MsoButtonStyle.msoButtonIcon ;
}
}

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

393

Создание окон инструментов
Почти все надстройки, добавляющие команды, имеют и панель инструментов с
растровыми изображениями, но иногда желательно добавить в Visual Studio .NET
полноценный пользовательский интерфейс. Отобразить из надстройки управля!
емое диалоговое окно не сложнее, чем из приложения Windows Forms. Для ото!
бражения полного окна, называемого окном инструментов, требуется чуть боль!
ше работы.
В IDE Visual Studio .NET два типа окон: документов и инструментов: в окнах до!
кументов вы редактируете код. Все остальные — это окна инструментов (напри!
мер, приведу окна Task List, Solution Explorer и Toolbox). Окно инструментов мо!
жет быть стыкуемым или, если вы щелкнете правой кнопкой его заголовок и от!
мените параметр Dockable, оно может отображаться как полное окно в основной
области редактирования.
Так как все окна инструментов являются COM!объектами, их можно создавать
на C++ и бороться со всеми неприятностями, которые за этим последуют. Созда!
ние окон инструментов на управляемом коде в документации не описано, но зато
в число предоставляемых Microsoft примеров входит подобный проект, грамот!
но названный ToolWindow.
Суть создания управляемого окна инструментов в том, чтобы ваша управляе!
мая надстройка создала компонент ActiveX, который в свою очередь обеспечива!
ет работу CLR. Как только это сделано, можно приказать компоненту ActiveX за!
грузить и отобразить в окне ActiveX нужный элемент управления. Этот компонент
ActiveX иногда называют элементом управления «хост!прослойка» (host shim
control), так как он просто внедряется в выполнение управляемого кода, чтобы
вы могли сделать то, что вам нужно.
Все звучит так, как будто этот «хост!прослойку» написать очень сложно, но есть
хорошая новость: он содержится в примере ToolWindow, и вы можете его исполь!
зовать. Увы, он почти не проверяет ошибок, поэтому, если что!то потерпит крах,
вам останется только чесать голову. А теперь самое приятное: я проработал весь
код этого элемента, реализовал проверку ряда ошибок и добавил диагностические
выражения, чтобы вы знали, что происходит при его использовании.
Я переименовал свой «хост!прослойку» в VSNetToolHostShim и включил в пример
SimpleToolWindow на CD. Все, что делает SimpleToolWindow, заключается в добав!
лении в IDE Visual Studio .NET окна редактирования a la окно WinDBG. Так как
создание элемента управления «хост!прослойка» контролируется вашей управля!
емой надстройкой, вы можете использовать VSNetToolHostShim из любого своего
проекта окна инструментов.
Объяснить необходимые действия проще всего на примере обработчика OnCon
nection из проекта SimpleToolWindow. Вы должны создать окно инструментов с
элементом управления VSNetToolWinShim, который возвращает ссылку на элемент
управления VSNetToolHostShim. Используя возвращенный объект VSNetToolHostShim,
вызовите метод HostUserControl2, чтобы загрузить свой управляемый элемент и
создать кнопку для открытия окна инструментов. Все это в действии можно уви!
деть в листинге 9!3.

394

ЧАСТЬ III

Листинг 9-3.

Мощные средства и методы отладки приложений .NET

Использование VSNetToolHostShim

public void OnConnection ( object
ext_ConnectMode
object
ref System.Array
{

application
connectMode
addInInst
custom

,
,
,
)

try
{
ApplicationObject = (_DTE)application;
AddInInstance = (AddIn)addInInst;
// Ваше окно инструментов должно иметь уникальный GUID.
String guid = "{E16579A45E964d848905566988322B37}" ;
// Объект для получения элемента VSNetToolHostShim.
Object RefObj = null ;
// Создание основного окна инструментов
// путем загрузки "хостапрослойки".
TheToolWindow = ApplicationObject.Windows.
CreateToolWindow ( AddInInstance
,
"VSNetToolHostShim.VSNetToolWinShim",
"Scratch Pad Window"
,
guid
,
ref RefObj
);
// До вызова метода HostUserControl нужно сделать
// окно видимым, иначе все пойдет не по плану.
TheToolWindow.Visible = true ;
// Получение "прослойки"(это переменная уровня класса):
// private VSNetToolHostShimLib.IVSNetToolWinShim ShimObj ;
ShimObj = (VSNetToolHostShimLib.VSNetToolWinShimClass)
RefObj ;
// Получение данной сборки. Это нужно, чтобы я мог
// передать "прослойке" расположение надстройки.
System.Reflection.Assembly CurrAsm =
System.Reflection.Assembly.GetExecutingAssembly ( ) ;
// Получение каталога данной надстройки и присоединение к пути
// имени DLL ресурсов. Это нужно для загрузки кнопкиярлычка.
StringBuilder StrSatDll = new StringBuilder ( ) ;
String StrTemp = CurrAsm.Location.ToLower ( ) ;
int iPos = StrTemp.IndexOf ("simpletoolwindow.dll" ) ;
StrSatDll.Append ( CurrAsm.Location.Substring ( 0 , iPos ));
StrSatDll.Append ("SimpleToolWindowResources.DLL" ) ;

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

395

// Этот метод загружает управляемый элемент в элемент управления
// ActiveX и приказывает ему загрузить растровое изображение.
ShimObj.HostUserControl2 ( TheToolWindow
,
CurrAsm.Location
,
"SimpleToolWindow.ScratchPadControl" ,
StrSatDll.ToString ( )
,
1
);
}
catch ( System.Exception eEx )
{
MessageBox.Show ( eEx.Message + "\r\n" +
eEx.StackTrace.ToString ( ) ,
"ExceptBion in OnConnection"
) ;
}
}

Создание на управляемом коде страниц
свойств окна Options
Создать управляемое окно инструментов относительно легко. Разработать управ!
ляемую страницу свойств, отображаемую в диалоговом окне Options (рис. 9!5),
немного сложнее. Это важно потому, что именно в окне Options пользователи
обычно будут искать страницу изменения параметров вашей надстройки, и так
вы сможете улучшить свою репутацию.

Рис. 95.

Страница свойств надстройки SettingsMaster в окне Options

Как вы, наверное, уже догадались, страница свойств в окне Options представ!
ляет собой элемент управления ActiveX, реализующий интерфейс IDTToolsOptionsPage.
Чтобы узнать, есть ли у вас такая страница свойств, Visual Studio .NET изучает раздел
надстройки в реестре. В основном разделе надстройки она ищет раздел Options.
В разделе Options будут находиться один или больше разделов, которые будут до!
бавлены в дерево окна Options как узлы верхнего уровня. У вас будет один раздел

396

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

этого уровня, названный так же, как и надстройка. В этом разделе будет находиться
очередной набор разделов, формирующих подузлы верхнего узла дерева. По умол!
чанию первый раздел называется General. В каждом заключительном разделе бу!
дет находиться строковый параметр Control, содержащий ProgID элемента управ!
ления ActiveX, создаваемого для отображения страницы свойств.
Наверное, лучше всего проиллюстрировать сказанное на примере. Разделы
реестра для страницы свойств SettingsMaster (рис. 9!5) выглядят так:

HKEY_CURRENT_USER\
Software\
Microsoft\
VisualStudio\
7.1\
AddIns\
SettingsMaster\
Options\
SettingsMaster\
General


1




Release



IncrementalBuild
Boolean
0





Листинг 9-6. Проект SettingsMaster для включения 4-го уровня диагностики
в заключительных компоновках проектов C#

{B5E9BD346D3E4B5D925E8A43B79820B4}

Release



WarningLevel
Enum
4

408

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET





Схема конфигурационного файла для неуправляемых приложений C++ похо
жа, однако она должна учитывать, что неуправляемые приложения определяют
инструмент, с которым вы хотите работать. Базовая схема представлена в следу
ющем фрагменте (список полей см. в табл. 93):



















Табл. 9-3. Схема конфигурации проекта на неуправляемом C++
Узел

Описание



Основной элемент, содержащий одну или более конфигураций.



Содержит строку GUID для неуправляемого C++. Она всегда будет
иметь значение, указанное в примере.
Пример:


{B5E9BD326D3E4B5D925E8A43B79820B4}



Набор свойств одной конфигурации сборки проекта.



Имя конфигурации. Соответствует целевой конфигурации в дис
петчере конфигураций IDE Visual Studio .NET.
Пример:

Debug


Набор инструментов данной конфигурации.



Свойства отдельного инструмента.

ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

Табл. 9-3. Схема конфигурации проекта …

409

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

Узел

Описание



Имя конкретного инструмента. Им может быть любой из объектов
инструментов, поддерживаемых объектом VCProject, а именно:
VCAlinkTool, VCAuxiliaryManagedWrapperGeneratorTool, VCCLCompilerTool,
VCCustomBuildTool, VCLibrarianTool, VCLinkerTool,
VCManagedResourceCompilerTool, VCManagedWrapperGeneratorTool,
VCMidlTool, VCNMakeTool, VCPostBuildEventTool, VCPreBuildEventTool,
VCPreLinkEventTool, VCPrimaryInteropTool, VCResourceCompilerTool или
VCXMLDataGeneratorTool. Вы также можете указать специальный
объект VCConfiguration проекта VCProject для доступа к общим пара
метрам проекта.
Пример:

VCCLCompilerTool


Набор свойств данной конфигурации инструментов.



Описание отдельного свойства.



Имя свойства проекта. Это свойство должно существовать в объек
те автоматизации VCProject. Если свойство инструмента использу
ется только для DLL, добавьте атрибут Type и присвойте ему значе
ние "DLL", а если оно используется только для EXEфайлов — "EXE".
Если свойство используется и для EXEфайлов, и для DLL, не вклю
чайте атрибут Type.
Пример:

BasicRuntimeChecks
Пример:


OptimizeForWindowsApplication


Тип свойства. Тип может иметь только значения Boolean, String или
Enum. Для свойства типа String вы должны включить атрибут типа
OpType, Overwrite или Append, определяющий, как будет изменено
строковое значение. Для свойства типа Enum вы должны включить
атрибут типа Name, который представляет собой имя перечислимого
типа, используемого конкретным свойством объекта Project.
Пример:

Boolean
Пример:


Enum


Значение, которое вы хотите присвоить свойству. В случае типов
Boolean оно может быть равно или 1, или 0. Для типов String это
строка, которую вы хотите присоединить или записать вместо пре
дыдущей. Для типов Enum это численное значение перечисления.
Пример:

4

410

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

Как и в случае конфигурации .NET, я хочу привести пару простых примеров
конфигураций проектов на неуправляемом C++. В листинге 97 показано, как вклю
чить оптимизацию для всей программы в заключительных компоновках. Код ли
стинга 98 задает DEFфайл для отладочных и заключительных компоновок.

Листинг 9-7. Проект SettingsMaster для включения оптимизации всей программы
в заключительных компоновках проекта на неуправляемом C++

{B5E9BD326D3E4B5D925E8A43B79820B4}

Release


VCConfiguration




WholeProgramOptimization
Boolean
1







Листинг 9-8. Проект SettingsMaster, определяющий DEF-файл для отладочных
и заключительных компоновок проекта на неуправляемом C++

{B5E9BD326D3E4B5D925E8A43B79820B4}

Debug


VCLinkerTool



ModuleDefinitionFile
String
.\$(ProjectName).DEF






ГЛАВА 9

Расширение возможностей интегрированной среды разработки VS .NET

411


Release


VCLinkerTool



ModuleDefinitionFile
String
.\$(ProjectName).DEF






Полные примеры для любого языка см. в файлах, которые я включил на CD
вместе с проектом SettingsMaster; они настраивают ваши проекты с учетом всех
рекомендаций, данных в главе 2. Для проектов .NET их можно использовать как
есть, а вот некоторые указанные по умолчанию параметры проектов на неуправ
ляемом C++ вам, возможно, захочется изменить. В проектах C++ я включаю стро
ки Unicode и другие параметры, которые нравятся лично мне, но в ваших проек
тах они могут вызвать проблемы. Все узлы, на которые вам стоит обратить вни
мание, я прокомментировал в XMLфайлах.

Вопросы реализации SettingsMaster
Многие из вас могут быть счастливы просто от использования надстройки Settings
Master, но ее реализация также представляет некоторый интерес. Когда я только
подумал о SettingsMaster, я начал разрабатывать макрос, потому что это гораздо
проще, чем создавать полную надстройку. Исходный макрос вы найдете в катало
ге SettingsMaster\OriginalMacro. Когда макрос заработал, я не захотел переводить
весь код на C#, поэтому я реализовал надстройку на Visual Basic .NET. Так как Visual
Basic .NET — это то же самое, что и C#, только без точек с запятой, переключаться
между языками очень легко.
Самая сложная часть работы над SettingsMaster состояла в определении схемы
XML. Благодаря относительно небольшому размеру файлов SettingsMaster я смог
использовать удивительный класс XmlDocument, что сделало навигацию по документу
тривиальной. Если б мне понадобилось создать эту надстройку еще раз, я попро
бовал бы разработать схему XML, позволяющую объединить всю информацию в
один файл. Изучая код, вы увидите, что в нескольких местах я продублировал
обработку двух типов проектов.
Чудо SettingsMaster основано на удивительном механизме отражения .NET.
Возможность создания класса на лету и вызова его методов или установки и по
лучения свойств — одна из самых лучших в.NET. Так как у меня были конфигура
ции и проекты, я применил отражение для создания отдельных инструментов

412

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

и свойств. Я включил в код массу комментариев, так что вы легко поймете, как
все работает.
Наибольшая проблема при создании SettingsMaster была связана с созданием
значения перечислимого типа. Так как .NET строго типизирована, если бы я не
смог создать специфическое значение Enum, мне было бы сложно задать множе
ственные параметры через объекты Project и VCProject. После ряда безуспешных
проб я обратился за помощью. Франческо Балена (Francesco Balena) напомнил мне,
что с этим успешно справляется метод System.Enum.Parse. Все остальное оказалось
простой нудной работой с XMLфайлами.

Будущие усовершенствования SettingsMaster
SettingsMaster — очень полезная надстройка, но если вы ищете проект, то можете
внести в SettingsMaster ряд усовершенствований, чтобы сделать ее еще лучше.
쐽 В SettingsMaster отсутствует редактор конфигурационных XMLфайлов. С ре
шетками свойств (property grid) работать довольно легко, поэтому вы могли
бы создать такой редактор, чтобы избавиться от редактирования конфигура
ционных файлов вручную. Этот редактор конфигураций следует сделать до
ступным с командной панели SettingsMaster, а также из страницы свойств окна
Options.
쐽 Есть одна функция, добавить которую будет относительно легко: это обработ
чик событий, отслеживающий загрузку проектов и автоматически обновляю
щий их параметры.
쐽 Некоторые глобальные параметры проектов на неуправляемом C++ устанав
ливаются при помощи объекта VCPlatform. Было бы неплохо реализовать под
держку этого объекта, чтобы пользователи могли задавать каталоги включае
мых файлов и другие свойства, полезные при работе в группе.
쐽 Отличной функцией была бы команда записи текущих параметров проекта в
конфигурационный файл SettingsMaster, чтобы вы могли применить их к дру
гим проектам.
쐽 Чтобы предоставить пользователям дополнительной обратной связи, вы мог
ли бы выводить изменения, сделанные SettingsMaster, в окно Output.

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

Г Л А В А

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

Наверное, вы уже поняли, что в разработке под Microsoft Visual Studio .NET го
раздо больше исключений, чем в традиционной разработке под Microsoft Win32.
Прелесть .NET в том, что обработка исключений была встроена изначально. Она
не заимствовала прикрученные и привитые исключения, с которыми мы бились
долгие годы, работая с приложениями Microsoft Windows и C++. Теперь исключе
ния поддерживаются естественно и полноценно.
Но, как и всегда, исключения — для исключительных условий. Не стоит при
менять исключения вместо таких конструкций, как операторы switch и case, если
не хотите получить истинно медленный код. В этой главе я представлю утилиту
ExceptionMon, которая служит для наблюдения за исключениями, возникающи
ми в приложении. Хотя через диалоговое окно исключений в отладчике можно
установить все исключения CLR на остановку при инициации, вам потребовалась
бы вечность, потому что пришлось бы постоянно нажимать кнопку Continue.
ExceptionMon позволяет наблюдать за исключениями почти без хлопот.
ExceptionMon использует одно из великолепнейших средств .NET — Profiling
API. Я писал средства профилирования (profilers) и инструменты обнаружения
ошибок (error detection tools) без поддержки ОС и, когда в .NET увидел Profiling
API, сразу возблагодарил богов разработки. Profiling API прекрасно продуман и
работает точно, как заявлено. Его сила позволяет видеть то, что практически не
увидеть иными способами. Интересно, что название Profiling API несколько об
манчиво, так как Profiling API позволяет гораздо больше простого хронометри
рования операций. К концу главы в вашей голове будут роиться идеи других «про
двинутых» инструментов, которые можно создать, используя Profiling API. Вооб
ще я буду применять Profiling API в следующих главах как основу для других пре
красных инструментов.

414

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

Наш путь к ExceptionMon начнется с обсуждения средств и целей Profiling API.
Разобравшись с этим, я объясню как работать с ExceptionMon и как она реализо
вана. И в заключение я расскажу о своем видении применения исключений в мире
.NET.

Введение в Profiling API
Документация и примеры для Profiling API из .NET отсутствуют в MSDN, но они
есть у вас на компьютере, если вы установили Visual Studio .NET. Волшебное мес
то — \SDK\v1.1\Tools Developers Guide. Там вы найде
те каталог Docs с Wordдокументами, описывающими все: от Profiling API до Debug
ging API и Metadata API, а также полными спецификациями ECMA для общеязыко
вой инфраструктуры (Common Language Infrastructure, CLI). Каталог Samples со
держит примеры .NETкомпиляторов, примеры Profile API и средство обхода за
висимостей сборок (assembly dependency walker). В документах и примерах мас
са полезного, и, если вам любопытно, как в .NET все работает, каталог Samples —
прекрасное место для начала исследований. Документ, описывающий Profiling
API, — вполне очевидно — Profiling.DOC.
Есть два способа профилирования. В первом применяется процесс выборки
(sampling). В нем средство профилирования через определенные интервалы в
миллисекундах «заглядывает» в профилируемое приложение (profilee) и проверяет,
что выполняется в данный момент, — это средство профилирования с выборкой
имен (name sampling profiler). Второй метод — безвыборочный (nonsampling), где
средство профилирования синхронно контролирует каждый вызов и возврат,
отслеживая все, что происходит в профилируемом приложении. .NET Profiling API
легко работает с обоими типами профилирования. Как я уже говорил, Profiling API
позволяет делать гораздо больше, чем просто профилировать. Вот полный спи
сок элементов, о которых вы можете получать уведомления, создавая программу
с помощью Profiling API (табл. 101). Получение этих уведомлений относительно
тривиально, так что в будущем вы наверняка увидите массу изящных инструментов.

Табл. 10-1.

Обеспечение Profiling API

Элемент

Типы уведомлений

Исполняющая среда

Приостановка (suspend) и возобновление (resume) управляе
мого выполнения (всех потоков), приостановка и возобнов
ление отдельного управляемого потока

AppDomain

Старт (startup), завершение (shutdown)

Сборка

Загрузка (load), выгрузка (unload)

Модуль

Загрузка, выгрузка, присоединение (attach)

Класс

Загрузка, выгрузка

Функция

JITкомпиляция, поиск в кэше, изъятие (удаление из памя
ти), подстановка (inlined), выгрузка

Поток

Создание (create), уничтожение (destroy), присвоение пото
ку ОС

Remoting

Активизация клиента, отправка клиентом сообщения, полу
чение клиентом ответа, получение сервером сообщения, ак
тивизация, отправка сервером ответа

ГЛАВА 10

Табл. 10-1. Обеспечение Profiling API

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

415

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

Элемент

Типы уведомлений

Переключения

Управляемое в неуправляемое, неуправляемое в управляе
мое, создание COM VTable, уничтожение COM Vtable

Приостановка
исполняющей среды

Приостановка, отмена приостановки, возобновление,
приостановка потока, возобновление потока

Сбор мусора

Выделение объекта, выделения по классам, перемещение
ссылки, объектные ссылки, корневые ссылки

Исключение

Инициация, поиск, фильтрация, вход в перехватчик
(catcher), перехватчик обнаружен, вызов ОСобработчика,
раскрутка функции, раскрутка finally, обнаружен перехват
чик CLR, запущен перехватчик CLR

Для написании средства профилирования реализуется интерфейс ICorProfiler
Callback. Хотя было бы прекрасно писать средство профилирования в управляе
мом коде, изза архитектуры, поддерживаемой Profiling API, этого делать нельзя.
Средство профилирования выполняется в адресном пространстве профилируе
мого управляемого приложения. Возможность использования управляемого кода
вызывала бы чрезвычайно опасные ситуации разного рода. Так, если бы вы полу
чили уведомление о проводимой операции сбора мусора и вам потребовалось бы
выделить управляемую память для хранения собираемых элементов, это иници
ировало бы рекурсивный сбор мусора. Не нужно говорить, что архитекторы Micro
soft выбрали более разумный подход, минимизирующий взаимное влияние. Для
поддержки управляемых средств профилирования все уведомления должны быть
межпроцессовыми (crossprocess), что серьезно замедлило бы профилируемое
приложение.
Поскольку средства профилирования — это всего лишь COM DLL, концепции
должны быть знакомы всем, кто занимался разработкой под Windows с 2000 года.
Всю нудную работу я инкапсулировал в библиотеке, что позволит вам сосредото
читься на важных моментах, не увязая в COM. Ниже я расскажу о ProfilerLib под
робнее. Хочу отметить ключевой COMаспект: ваш COMкод средства профили
рования будет вызываться в модели со свободными потоками (freethreaded model),
так что вам придется защищать структуры данных от повреждения в многопоточной
среде (multithreaded corruption).
В интерфейсе ICorProfilerCallback лишь два метода нужны всегда: Initialize и
Shutdown. Initialize вызывается самым первым. Вам передается интерфейс IUnknown,
через который надо будет сразу запросить интерфейс ICorProfilerInfo и сохра
нить возвращенный интерфейс, чтобы запрашивать информацию о профилиру
емом приложении.
Многие методы ICorProfilerCallback получают идентификаторы. С помощью
сохраненного интерфейса ICorProfilerInfo идентификатор преобразуется в удобное
значение. Так, метод ICorProfilerCallback::ModuleLoadFinished получает значение
ModuleID, представляющее идентификатор загруженного метода. Чтобы определить
имя модуля и другую полезную информацию, такую как адрес загрузки (load address)
и идентификатор сборки, вызовите метод ICorProfilerInfo::GetModuleInfo. Допол
нительные задачи, выполняемые с помощью методов интерфейса, включают по
лучение интерфейсов метаданных, инициацию сбора мусора и запуск отладки

416

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

процесса. Не буду описывать интерфейс ICorProfilerInfo полностью — подробную
информацию см. в файле Profiling.DOC.
Сохранив интерфейс ICorProfilerInfo в методе ICorProfilerCallback::Initialize,
следует сообщить CLR, какие уведомления вы хотели бы видеть. Красота системы
ICorProfilerCallback в том, что вы будете получать уведомления только для нуж
ных вам элементов, так что CLR сможет минимизировать использование ресур
сов и выполнять профилируемое приложение как можно быстрее. Элементы, для
которых требуются уведомления, позволяет указать метод ICorProfilerInfo::Set
EventMask, принимающий битовое поле, которое указывает нужные элементы.
Устанавливаемые битовые флаги описаны в табл. 102. Большинство не требу
ет объяснений. Некоторые значения — для которых в колонке Неизменяемый
указано «Да» — могут быть установлены только во время вызова метода ICorProfiler
Callback::Initialize. Если флаг уведомления не неизменяемый, его можно пере
ключать в любой момент работы профилирующего средства. Чтобы увидеть вклю
ченные флаги уведомлений, вызовите метод ICorProfilerInfo::GetEventMask. Флаг
COR_PRF_ENABLE_OBJECT_ALLOCATED устанавливается в методе ICorProfilerCallback::Ini
tialize, указывая на необходимость установки CLR на отслеживание выделения
объектов, а COR_PRF_MONITOR_OBJECT_ALLOCATED включает и выключает уведомления.

Табл. 10-2.

Флаги уведомлений SetMethod

Флаг1

Неизменяемый

Описание

ALL

Да

Включает все флаги уведомлений.

APPDOMAIN_LOADS

Нет

Уведомление о каждой загрузке или выгруз
ке AppDomain.

ASSEMBLY_LOADS

Нет

Уведомление о каждой загрузке или выгруз
ке сборки.

CACHE_SEARCHES

Нет

Уведомление, когда код периода инсталля
ции находит функции, запущенные через
Native Image Generator (NGEN).

CCW

Нет

Уведомление о каждой COMоболочке.

CLASS_LOADS

Нет

Уведомление о каждой загрузке или выгруз
ке класса.

CLR_EXCEPTIONS

Нет

Уведомление о каждой внутренней обработ
ке исключений в CLR.

CODE_TRANSITIONS

Да

Уведомление о каждом переключении меж
ду управляемым и неуправляемым кодом.

DISABLE_INLINING

Да

Отключает подстановку методов во всем
процессе. Если не установлен, уведомления
о подстановках проходят через уведомле
ние ICorProfilerCallback.JITInlining.

DISABLE_OPTIMIZATIONS

Да

Предписывает JITкомпилятору отключить
оптимизации.

ENABLE_IN_PROC_DEBUGGING

Да

Разрешает использование внутрипроцесс
ной отладки (inprocess debugging) вместе
с Profiling API.

ENABLE_JIT_MAPS

Да

Разрешает отслеживание JITсопоставлений.

1

Для ясности из имен флагов удалены префиксы COR_PRF_ и COR_PRF_MONITOR_.

ГЛАВА 10

Табл. 10-2.

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

Флаги уведомлений SetMethod

417

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

Флаг

Неизменяемый

Описание

ENABLE_OBJECT_ALLOCATED

Да

Уведомление о каждом объекте, выделенном
из кучи собранного мусора.

ENABLE_REJIT

Да

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

ENTERLEAVE

Нет

Ловушки (hooks) на входе и выходе функ
ции вызова (call function).

EXCEPTIONS

Нет

Уведомление о каждом неCLRисключении
(т. е. обо всех общих исключениях).

FUNCTION_UNLOADS

Нет

Уведомление о выгрузке функций.

GC

Да

Уведомление о готовящемся сборе мусора.

JIT_COMPILATION

Нет

Уведомление о каждой функции непосред
ственно до и сразу после ее JITкомпиляции.

MODULE_LOADS

Нет

Уведомление о каждой загрузке и выгрузке
модуля.

NONE

Нет

Не посылать уведомления.

OBJECT_ALLOCATED

Нет

Уведомление о каждом объекте, выделяемом
в кучу собранного мусора.

REMOTING

Да

Уведомление о пересечении каждого кон
текста удаленного взаимодействия
(remoting).

REMOTING_ASYNC

Да

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

REMOTING_COOKIE

Да

Создание файлов «cookie», чтобы средство
профилирования могло спаривать обратные
вызовы удаленного взаимодействия.

SUSPENDS

Нет

Уведомление о приостановке CLR.

THREADS

Нет

Уведомление о каждом создании и уничто
жении потока.

После возврата S_OK из метода ICorProfilerCallback::Initialize вы будете полу
чать запрошенные уведомления через соответствующий метод ICorProfilerCallback.
Я расскажу, что с этим делать чуть позже, так как сначала хочу упомянуть после
дний необходимый метод — ICorProfilerCallback::Shutdown.
Если профилируемый процесс начинает выполнение в виде управляемого
приложения, метод Shutdown будет обязательно вызван. Однако, если приложение
начинает выполнение как неуправляемое приложение, загружающее CLR, как Visual
Studio .NET, то ваш метод Shutdown вызван не будет. Чтобы обеспечить остановку
средства профилирования, в DllMain средства профилирования надо обрабатывать
флаг DLL_PROCESS_DETACH и проверять, вызван ли метод Shutdown. Если нет, следует
провести очистку вручную, помня, что, поскольку приложение завершается, надо
быть осведомленным о выполняемых операциях. Пример действий в такой ситу
ации см. в коде ExceptionMon.
Кроме специальных алгоритмов, необходимых для реализации вашего кон
кретного профиля, основная работа будет заключаться в том, чтобы следить за

418

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

значениями, получаемыми методами уведомления ICorProfilerCallback. Многие
методы уведомления получают значения идентификаторов, которые можно при
менять для получения определенной информации об объекте. Эти уникальные для
Profiling API идентификаторы являются просто адресами элементов в памяти, благо
интерфейс ICorProfilerInfo предлагает методы, помогающие преобразовать эти
идентификаторы в реальные значения. Обычно для этого нужно вызвать соответ
ствующий метод ICorProfilerInfo, получить интерфейс метаданных, напрямую свя
занный с идентификатором, и задействовать этот интерфейс в работе.
Метаданные ссылаются на данные, описывающие каждый объект в .NET. Само
описание объектов с помощью метаданных — загвоздка .NET. При разработке
управляемых приложений метаданные доступны через механизм отражения. При
разработке неуправляемых приложений, которым нужен доступ к метаданным,
используется интерфейс чтения (reader interface) IMetaDataImport и интерфейс за
писи (writer interface) IMetaDataEmit. В основном работа, выполняемая в средствах
профилирования, сопряжена с чтением данных через IMetaDataImport. IMetaDataEmit
используется компиляторами для создания метаданных в скомпилированных в .NET
двоичных файлах. Интерфейсы метаданных подробно описываются в файле Meta
data Unmanaged API.DOC, так что я отправлю вас туда, так как по большей части
работа с метаданными — чистая морока.
Наверное, лучший способ продемонстрировать работу с идентификаторами и
метаданными — показать, как получить имя класса и метода из идентификатора
функции (function ID). Значения идентификаторов функций получаются многи
ми методами ICorProfilerCallback, такими как ExceptionUnwindFunctionEnter (чтобы
показать, какая функция раскручена), JITCompilationFinished (чтобы показать, ка
кая функция прошла JITкомпиляцию) и ManagedToUnmanagedTransition (чтобы по
казать, какая функция переключается на неуправляемый код). В листинге 101
показан метод GetClassAndMethodFromFunctionId из ProfilerLib, который получает имя
класса и метода из идентификатора функции. Как видите, для этого надо лишь
прорваться через интерфейс метаданных.

Листинг 10-1.

GetClassAndMethodFromFunctionId

BOOL CBaseProfilerCallback ::
GetClassAndMethodFromFunctionId ( FunctionID
LPWSTR
UINT
LPWSTR
UINT
{
// Магия метаданных в том, как найти эту информацию.
// Возвращаемое значение.
BOOL bRet = FALSE ;
// Маркер для идентификатора функции.
mdToken MethodMetaToken = 0 ;
// Интерфейс метаданных.
IMetaDataImport * pIMetaDataImport = NULL ;

uiFunctionId
szClass
uiClassLen
szMethod
uiMethodLen

,
,
,
,
)

ГЛАВА 10

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

// Запрашиваем через ICorProfilerInfo интерфейс
// метаданных для этого идентификатора функции.
HRESULT hr = m_pICorProfilerInfo>
GetTokenAndMetaDataFromFunction ( uiFunctionId
IID_IMetaDataImport
(IUnknown**) &pIMetaDataImport
&MethodMetaToken
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( SUCCEEDED ( hr ) )
{
// Маркер для класса.
mdTypeDef ClassMetaToken ;
// Суммарные копии символов.
ULONG ulCopiedChars ;

419

,
,
,
);

// Получаем из метаданных информацию о методе.
hr = pIMetaDataImport>GetMethodProps ( MethodMetaToken ,
&ClassMetaToken ,
szMethod
,
uiMethodLen
,
&ulCopiedChars ,
NULL
,
NULL
,
NULL
,
NULL
,
NULL
) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
ASSERT ( ulCopiedChars < uiMethodLen ) ;
if ( ( SUCCEEDED ( hr )
) &&
( ulCopiedChars < uiMethodLen ) )
{
// Имея маркер метаданных для класса, я могу найти класс.
hr = pIMetaDataImport>GetTypeDefProps ( ClassMetaToken ,
szClass
,
uiClassLen
,
&ulCopiedChars ,
NULL
,
NULL
) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
ASSERT ( ulCopiedChars < uiClassLen ) ;
if ( ( SUCCEEDED ( hr )
) &&
( ulCopiedChars < uiClassLen ) )
{
bRet = TRUE ;
}
else
{
bRet = FALSE ;
}
}
см. след. стр.

420

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

else
{
bRet = FALSE ;
}
pIMetaDataImport>Release ( ) ;
}
else
{
bRet = FALSE ;
}
return ( bRet ) ;
}

Запуск средства профилирования
До сих пор я рассказывал, как работают средства профилирования, но так и не
упомянул, как их запускать. Увы, это слабое звено системы профилирования.
Загружаемое средство профилирования определяется двумя переменными
окружения. Первая, которой следует установить ненулевое значение, — Cor_Enable_Pro
filing; она сообщает CLR, что следует включить профилирование. Второй — Cor_Pro
filer — следует задать CLSID или ProgID средства профилирования. Вот как уста
новить средство профилирования ExceptionMon из командной строки.

set Cor_Enable_Profiling=0x1
set COR_PROFILER={F6F3B5B74EEC48f682F3A9CA97311A1D}
Установка переменных окружения прекрасно работает для Windows Forms и
консольных приложений .NET, но как профилировать приложения Microsoft ASP.NET?
Ох, это непросто. Поскольку надо устанавливать переменные окружения, придется
установить две переменные в системном окружении (рис. 101), так как отсюда
Microsoft Internet Information Services (IIS) и ASPNET_WP.EXE/W3WP.EXE будут
считывать переменные окружения.
С помощью Visual Studio .NET 2003 и .NET Framework 1.1 можно перезапустить
IIS, чтобы новый экземпляр ASPNET_WP.EXE принял новые глобальные перемен
ные окружения. Чтобы перезапустить IIS, вызовите консоль Internet Information
Services, щелкните правой кнопкой имя машины, укажите на All Tasks и выберите
из контекстного меню команду Restart IIS. В диалоговом окне Start/Stop/Reboot
выберите Restart Internet Services On из раскрывающегося списка
и щелкните OK. ASPNET_WP.EXE/W3WP.EXE не запустится, пока вы не запросите
IIS об ASP.NETприложении. Проверить, установлены ли переменные окружения,
позволяет Process Explorer (см. главу 3): дважды щелкните ASPNET_WP.EXE/W3WP.EXE
в верхнем окне и в диалоговом окне Properties перейдите на вкладку Environment.
Устанавливая переменные системного окружения, вы сталкиваетесь с еще од
ной проблемой. Будучи общесистемным (systemwide), любой процесс, загружа
ющий CLR, автоматически профилируется. Это может соответствовать вашим на
мерениям, но с ростом количества процессов, загружающих CLR, могут быстро

ГЛАВА 10

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

421

возникнуть проблемы. Так, если в вашем средстве профилирования есть ошибка
(я знаю, что этого не может быть, но просто в порядке шутки) и вы собираетесь
отладить его с помощью Visual Studio .NET, ваше средство профилирования так
же будет загружено и в Visual Studio .NET, что может сорвать всю отладку. Можно
использовать удаленную отладку, но я предпочитаю устанавливать еще одну пе
ременную окружения, указывающую, в каком процессе или процессах запускать
средство профилирования. Тогда при старте вы сможете проверить определенную
переменную окружения и указать, запускаться ли в данном процессе. Если запус
каться в процессе не нужно, просто вызовите ICorProfilerInfo::SetEventMask, пе
редав COR_PRF_MONITOR_NONE как маску в методе ICorProfilerCallback::Initialize.

Рис. 101.

Установка переменных системного окружения

Поскольку проверка того, запускать ли средство профилирования в определен
ном процессе, — операция стандартная, я реализовал в ProfilerLib метод CBasePro
filerCallback::SupposedToProfileThisProcess, выполняющий для вас эту проверку. Пе
редайте как параметр проверяемую переменную окружения, и функция вернет TRUE
в следующих случаях.
1. Переменная окружения не установлена, что предполагает желание профили
ровать все процессы.
2. Значение переменной окружения полностью соответствует диску, пути и име
ни текущего процесса.
3. Значение переменной окружения совпадает только с именем файла текущего
процесса.
Здесь я хочу закончить введение в Profiling API. Он позволяет делать гораздо
больше того, что было рассказано. Но, вместо того чтобы затягивать ваши глаза
поволокой все новых подробностей и оставить вас в раздумьях о том, как приме
нить Profiling API, думаю, лучше всего объяснить это, показав вам применение
некоторых из самых «продвинутых» его функций. К концу этих двух глав, касаю

422

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

щихся Profiling API, у вас сформируется гораздо более глубокое понимание, чем
то, что можно составить с одним лишь Profiling.DOC.

ProfilerLib
Прежде чем мы погрузимся в глубины средства профилирования ExceptionMon,
хочу посвятить немного времени разговору о ProfilerLib. Как вы, наверное, дога
дались из предшествующих разговоров о COM Profiling API, здесь много стерео
типного кода. Поскольку я не в восторге от того, чтобы при разработке ПО снова
и снова набирать одно и то же, я быстро понял, что мне нужна библиотека для
выполнения черной работы, особенно с учетом большого числа методов, поддер
живаемых интерфейсом ICorProfilerCallback.
Два примера средств профилирования, поставляемых с Visual Studio .NET, так
же придерживаются курса повторного использования COMкода, но их способ
написания кода непригляден тем, что смешивает всю инфраструктуру во вспомо
гательные структуры данных. Я работал над тем, чтобы исправить это, когда в
«MSDN Magazine» за декабрь 2001 года появилась колонка Мэтта Питрека (Matt
Pietrek) «Under the Hood». Мэтт взял пример кода средства профилирования и
устранил путаницу. Я решил, что это хорошая основа, поэтому взял код Мэтта и
улучшил его еще больше, упростив написание средств профилирования.
Процесс настройки для использования ProfilerLib довольно прост. Сначала надо
создать DLLпроект и настроить его на подключение ProfilerLib.LIB, а в файл STDAFX.H
проекта включить ProfileLib.H. В одном из ваших CPPфайлов определите следую
щие переменные и присвойте им значения, нужные для вашего средства профи
лирования:

// Строка GUID средства профилирования
wchar_t * G_szProfilerGUID
// CLSID средства профилирования
GUID G_CLSID_PROFILER
// Префикс ProgID средства профилирования
wchar_t * G_szProgIDPrefix
// Имя средства профилирования
wchar_t * G_szProfilerName
// Для примера вот значения для ExceptionMon
wchar_t * G_szProfilerGUID =
L"{F6F3B5B74EEC48f682F3A9CA97311A1D}" ;
GUID G_CLSID_PROFILER =
{ 0xf6f3b5b7 , 0x4eec , 0x48f6 ,
{ 0x82 , 0xf3 , 0xa9 , 0xca , 0x97 , 0x31 , 0x1a , 0x1d } } ;
wchar_t * G_szProgIDPrefix = L"ExceptionMonProfiler" ;
wchar_t * G_szProfilerName = L"ExceptionMon" ;
Объявив уникальные COMзначения, добавьте DllMain в ваш CPPфайл:

ГЛАВА 10

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

423

HINSTANCE G_hInst = NULL ;
extern "C" BOOL WINAPI DllMain ( HINSTANCE hInstance ,
DWORD
dwReason ,
LPVOID
)
{
switch ( dwReason )
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls ( hInstance ) ;
G_hInst = hInstance ;
break ;
default :
break ;
}
return( TRUE ) ;
}
ProfilerLib содержит базовый класс CBaseProfilerCallback, который реализует
методы, необходимые интерфейсу ICorProfilerCallback. В своем средстве профи
лирования наследуйте классы обратного вызова от CBaseProfilerCallback и пере
определяйте конкретные методы для получения нужных вам уведомлений. Так вы
сможете сосредоточиться только на важных элементах, а не на остальных мето
дах, которые просто путаются под ногами.
Присвоив имя наследующему классу, реализуйте функцию AllocateProfilerCallback
со следующим прототипом. В этой функции создайте наследующий от CBaseProfiler
Callback класс и верните его. Код в ProfilerLib.h позаботится об остальном.

ICorProfilerCallback * AllocateProfilerCallback ( ) ;
Наконец, возьмите из ProfilerLib файл EXAMPLE.DEF, скопируйте его в свой
проект, переименуйте и замените в нем оператор LIBRARY для правильного выпол
нения всех экспортов, необходимых в создании COM DLL.
Кроме выполнения всей рутины COM, ProfilerLib вносит в CBaseProfilerCallback
дополнительные методы, которые облегчат вам жизнь. Некоторые из них я уже
упоминал, но есть и другие. Встречая чтолибо, что, по моему мнению, может быть
использовано повторно, я добавляю это в ProfilerLib, так что не забудьте прове
рить файлы проектов — вы увидите, что для вас написаны и другие экономящие
время подпрограммы.
Как вы могли догадаться из кода, в подкаталоге Tests каталога ProfilerLib есть
программапример DoNothing. Это простейшее средство профилирования, кото
рое можно сделать, и оно демонстрирует применение ProfilerLib. Оно обрабаты
вает все уведомления, но лишь подает звуковой сигнал при инициализации и
выгрузке. Это мой запатентованный метод разработки «Отладка ушами». Кроме того,
все другие написанные мною утилиты, которые используют Profiling API, приме
няют ProfilerLib в качестве базовых классов, так что вы сможете увидеть более
сложные примеры использования. ProfilerLib спасла мне огромное количество
времени, и, надеюсь, сэкономит массу времени и вам.

424

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

ExceptionMon
Установив и запустив ProfilerLib, я смог приступить к ExceptionMon. Взглянув на
интерфейс ICorProfilerCallback, вы увидите, что к вашим услугам все виды потря
сающих обратных вызовов, позволяющие точно узнать, что делается при возник
новении исключения. Как будто ктото в Microsoft читал мои мысли! Могло пока
заться, что реализация ExceptionMon была совершенно тривиальной. Как всегда,
на самом деле оказалось не так.
В ExceptionMon я хотел записывать инициированные исключения, вызванные
обработчики finally и где обрабатывалось исключение. Методыуведомлений об
работки исключений, содержащиеся интерфейсе ICorProfilerCallback и приведенные
ниже, подходили точьвточь. Дополняет картину то, что вы также получаете иден
тификаторы функции и объекта инициированного и перехваченного исключения.

STDMETHOD ( ExceptionThrown ) ( ObjectID thrownObjectId ) ;
STDMETHOD ( ExceptionUnwindFinallyEnter ) ( FunctionID functionId ) ;
STDMETHOD ( ExceptionCatcherEnter ) ( FunctionID functionId ,
ObjectID objectId
) ;
С помощью ProfilerLib я быстро набросал первоначальный вариант Exception
Mon, записывая вывод в файл журнала в том же каталоге, откуда загружен про
цесс ExceptionMon. Первая маленькая проблема, с которой я столкнулся, — как
лучше отладить ExceptionMon. Поскольку CLR выполняло всю работу по внесению
указанного средства профилирования в адресное пространство, я хотел обеспе
чить возможность начать отладку с самого начала. Раз уж мы используем пере
менные окружения для запуска средства профилирования, я решил пойти дальше
и добавить еще одну — EXPMONBREAK, установка которой заставляла ExceptionMon
вызвать DebugBreak, чтобы я смог подключить отладчик.
Хотя средство профилирования можно отлаживать как любую неуправляемую
DLL, загруженную в процесс, я предпочитаю вызов DebugBreak, так как средство
профилирования будет загружено в Visual Studio .NET, поскольку здесь размеща
ется CLR. Можно ограничить загрузку процессов, установив переменную окруже
ния EXPMONPROC, чтобы отлаживать только один процесс. Однако для нужд разра
ботки и отладки я предпочитаю запускать для тестирования несколько программ.
Используя схему EXPMONBREAK, я легко могу подключить несколько отладчиков к
нескольким процессам.
Раз я говорю о переменных окружения, следует упомянуть две важнейшие для
мониторинга исключений — ASPNET_WP.EXE/W3WP.EXE. По умолчанию Exception
Mon не сбрасывает файл вывода на диск, поэтому, чтобы увидеть отчет об исклю
чениях, надо остановить ASPNET_WP.EXE/W3WP.EXE. Однако, если установить пе
ременную окружения EXPMONFLUSH, все записи сбрасываются на диск немедленно.
Еще одна проблема с записью файлов в том, что ExceptionMon поместит файл
журнала в каталог процесса, тогда как стандартная учетная запись ASPNET, веро
ятно, не имеет разрешения на создание файлов в %SYSTEMROOT%\Microsoft.NET\
Framework\%FRAMEWORKVERSION%, где располагается ASPNET_WP.EXE. В Windows
Server 2003 применяется учетная запись NETWORK SERVICE, а W3WP.EXE распо
лагается в %SYSTEMROOT%\System32\inetsrv. Полный путь и имя для файла выво
да для ExceptionMon можно указать в переменной окружения EXPMONFILENAME. Оче

ГЛАВА 10

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

425

видно, вам придется выполнять двойную проверку наличия у учетной записи
ASPNET прав на создание и запись файла в данном каталоге.
Первоначальная версия ExceptionMon работала прекрасно, так как получала
идентификаторы функции и объекта и могла просто вызывать соответствующие
методы в интерфейсе ICorProfilerInfo для получения маркеров (tokens) класса и
функции, чтобы найти имена в метаданных. Код из листинга 101 — CBaseProfiler
Callback::GetClassAndMethodFromFunctionId — демонстрирует все, что нужно сделать
для получения имен классов и методов по идентификатору функции.

Внутрипроцессная отладка и ExceptionMon
Доведя базовую версию до рабочего состояния, я подумал, что полезно было бы
добавить обзор стека на момент инициации исключения. Тогда вы смогли бы
понять, как сложилась такая ситуация, и взглянуть на условия. В документации для
Profiling API я заметил, что ICorProfilerInfo::SetEventMask можно передать бито
вый параметр COR_PRF_ENABLE_INPROC_DEBUGGING, чтобы включить внутрипроцессную
отладку.
При внутрипроцессной отладке Profiling API передает уведомления о событи
ях, но вам потребуется способ получения более подробной информации, чем та,
что можно получить через интерфейс ICorProfilerInfo. Поскольку в Microsoft уже
разработали прекрасный «продвинутый» отладочный API, работающий рука об руку
с CLR, идея состояла в том, чтобы предоставить ограниченную версию отладоч
ного API, способного выполнять такие задачи, как контроль значений перемен
ных в реальном времени и просмотр стека.
Полностью отладочный API описывается в DebugRef.DOC из того же каталога,
что и документы по Profiling API и API метаданных. Как и все документы в катало
ге Tools Developers Guide, DebugRef.DOC пространен в описании интерфейсов,
методов и значений параметров и вполне конкретен в применении. Каталог Samples
содержит рабочий отладчик, составляющий примерно 98% исходного кода реаль
ного CORDBG, но сам код иногда трудно прослеживать, хотя в конечном счете
он раскрывает свои секреты.
Читая документацию по отладочному API, уделите особое внимание тому, ка
кие методы вызываются из внутрипроцессной отладки. Если под именем метода
есть зеленый текст «Not Implemented InProcess», использовать его нельзя. Вы уви
дите, что большинство методов, которые нельзя использовать, относится к уста
новке точек прерывания и изменению значений. Поскольку главная причина
проведения внутрипроцессной отладки — простой сбор информации, все важные
элементы полностью доступны.
Первый этап в применении внутрипроцессной отладки с Profiling API — уста
новка флага COR_PRF_ENABLE_INPROC_DEBUGGING при вызове ICorProfilerInfo::SetEventMask.
Интересно, что его простая установка вызывает два побочных эффекта. Первый
состоит в том, что, раз вы потребовали внутрипроцессную отладку, профилируе
мое приложение будет выполняться медленнее. Это потому, что CLR не будет ис
пользовать прекомпилированный (precompiled) код, скомпилированный с помо
щью NGEN.EXE, заставляя этот код пройти JITкомпиляцию, как в обычных усло
виях. Возможно, вы не используете NGEN.EXE, но его весьма интенсивно приме
няет .NET Framework, так что здесь будут потери.

426

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

Если вы запускали NGEN.EXE, то могли заметить параметр командной строки
/PROF, добавляющий к создаваемому коду информацию профилирования. Хоть это
и может показаться хорошим решением, пока Profiling API не поддерживает его,
так что использовать его нельзя. Я всетаки считаю, что замедление кода окупает
ся преимуществами.
Вторая проблема, с которой вам придется столкнуться, не документирована и
в первый раз совершенно сбила меня с толку. Метод ICorProfilerCallback::Excep
tionThrown получает идентификатор объекта, который описывает сгенерирован
ный класс. В моей первой реализации, не использующей внутрипроцессную от
ладку, я всегда получал идентификатор, способный передать CBaseProfilerCall
back::GetClassAndMethodFromFunctionId. Простое добавление флага COR_PRF_ENAB
LE_INPROC_DEBUGGING к ICorProfilerInfo::SetEventMask даже без действительного при
менения API внутрипроцессной отладки меняет чтото изнутри, так что в каче
стве идентификатора объекта передается только 0. Хоть API внутрипроцессной
отладки и содержит методы для получения информации, было весьма неприятно
выяснять, что случилось с идентификатором объекта просто изза установки флага!
Чтобы задействовать отладочные интерфейсы, надо вызвать метод ICorProfiler
Info::BeginInprocDebugging для запуска процесса получения соответствующего ин
терфейса. Как часть этого вызова передается указатель DWORD на файл «cookie»
контекста. Этот файл следует сохранить, чтобы передать его методу ICorProfiler
Info::EndInProcDebugging, который надо вызвать, чтобы указать на окончание внут
рипроцессной отладки. Второй шаг — получение соответствующего отладочно
го интерфейса. Если вас интересует только текущий поток, вызовите метод ICorPro
filerInfo::GetInprocInspectionIThisThread, чтобы получить интерфейс IUnknown, че
рез который можно запросить интерфейс ICorDebugThread. Чтобы провести обще
процессную отладку, вызовите ICorProfilerInfo::GetInprocInspectionInterface и за
просите через возвращенный IUnknown интерфейс ICorDebug. Лично я не понимаю,
почему два метода ICorProfilerInfo не могут просто возвращать соответствующие
интерфейсы.
Получив отладочный интерфейс, вы готовы к обращению к отладочному API
за необходимой информацией. В моем случае я хотел получить последнее исклю
чение в потоке, так что все, что мне надо было сделать, это вызвать метод ICorDebug
Thread::GetCurrentException, чтобы получить интерфейс ICorDebugValue, описываю
щий последнее инициированное исключение. Странно, что каждый раз при вы
зове метода ICorDebugThread::GetCurrentException происходил сбой, так что я и вправду
начал волноваться удастся ли заставить ExceptionMon работать!
Проштудировав документацию на профилирующий и отладочный API, я обна
ружил предложение, говорящее, что для выполнения любых операций со стеком
во внутрипроцессной отладке надо вызвать ICorDebugThread::EnumerateChains. От
ладочный API использует концепцию стековых цепочек (stack chains) чтобы свя
зать информацию об управляемом и неуправляемом стеках, составляющую пол
ную информацию о стеке. Я не видел, чтобы вызов ICorDebugThread::GetCurrentException
был както связан со стеком, но решил, что стоит попробовать вызвать ICorDebug
Thread::EnumerateChains, прежде чем делать чтото еще. Хотя это не документиро
вано (по крайней мере, неявно), я выяснил, что для работы с отладочным API, надо
сначала вызвать ICorDebugThread::EnumerateChains, иначе большинство методов не

ГЛАВА 10

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

427

сработает. В листинге 102 показан методоболочка, который я использую в Excep
tionMon для запуска внутрипроцессной отладки.

Листинг 10-2.

BeginInprocDebugging

HRESULT CExceptionMon ::
BeginInprocDebugging ( LPDWORD
pdwProfContext
,
ICorDebugThread **
pICorDebugThread ,
ICorDebugChainEnum ** pICorDebugChainEnum )
{
// Сообщаем Profiling API о необходимости внутрипроцессной отладки.
HRESULT hr = m_pICorProfilerInfo>
BeginInprocDebugging ( TRUE
,
pdwProfContext );
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( SUCCEEDED ( hr ) )
{
IUnknown * pIUnknown = NULL ;
// Запрашиваем у Profiling API интерфейс IUnknown,
// от которого можно получить IcorDebugThread.
hr = m_pICorProfilerInfo>
GetInprocInspectionIThisThread ( &pIUnknown ) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( SUCCEEDED ( hr ) )
{
hr = pIUnknown>
QueryInterface ( __uuidof ( ICorDebugThread ) ,
(void**)pICorDebugThread
) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
// В любом случае IUnknown мне больше не нужен.
pIUnknown>Release ( ) ;
//
//
//
//
if
{

Я делаю это в ходе обычной работы потому,
что, если прежде всего из ICorDebugThread
не вызвать ICorDebugThread::EnumerateChains,
многие другие методы не сработают.
( SUCCEEDED ( hr ) )
hr = (*pICorDebugThread)>
EnumerateChains ( pICorDebugChainEnum ) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( FAILED ( hr ) )
{
(*pICorDebugThread)>Release ( ) ;
}

}
}
}
return ( hr ) ;
}

428

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

Добившись от ICorDebugThread::GetCurrentException возврата правильного зна
чения, я решил что я уже у цели, поскольку оставалось лишь получить имя класса
из ICorDebugValue. Увы, просматривая соответствующие интерфейсы — ICorDebug
GenericValue, ICorDebugHeapValue, ICorDebugObjectValue, ICorDebugReferenceValue и Icor
DebugValue, я понял, что придется еще многое сделать, так как только ICorDebugObject
Value содержал метод GetClass, необходимый для получения интерфейса класса,
который предоставил бы имя. Это означало, что мне придется поработать, чтобы
преобразовать исходный ICorDebugValue от ICorDebugThread::GetCurrentException в
ICorDebugObjectValue. Проще всего мне показать вам код, выполняющий всю рабо
ту (листинг 103). Как видите, нужно разыменовать (dereference) объект и запро
сить интерфейс ICorDebugObjectValue.

Листинг 10-3.

GetClassNameFromValueInterface

HRESULT CExceptionMon ::
GetClassNameFromValueInterface ( ICorDebugValue * pICorDebugValue ,
LPTSTR
szBuffer
,
UINT
uiBuffLen
)
{
HRESULT hr = S_FALSE ;
ICorDebugObjectValue * pObjVal = NULL ;
ICorDebugReferenceValue * pRefVal = NULL ;
//
//
//
//
hr

Получаем ссылку на это значение. Так должны поступать исключения.
Если получить ICorDebugReferenceValue не удалось, значит,
это тип ICorDebugGenericValue. Я ничего не могу сделать
с ICorDebugGenericValue, так как мне нужно имя класса.
= pICorDebugValue>

QueryInterface ( __uuidof ( ICorDebugReferenceValue ),
(void**)&pRefVal
);
if ( SUCCEEDED ( hr ) )
{
// Разыменовываем значение.
ICorDebugValue * pDeRef ;
hr = pRefVal>Dereference ( &pDeRef ) ;
if ( SUCCEEDED ( hr ) )
{
// Разыменовав, я могу запросить объектное значение.
hr = pDeRef>
QueryInterface ( __uuidof ( ICorDebugObjectValue ),
(void**)&pObjVal
);
// Разыменование мне больше не нужно.
pDeRef>Release ( ) ;
}
// Ссылка мне больше не нужна.

ГЛАВА 10

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

429

pRefVal>Release ( ) ;
}
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( SUCCEEDED ( hr ) )
{
// Получаем интерфейс класса для этого объекта.
ICorDebugClass * pClass ;
hr = pObjVal>GetClass ( &pClass ) ;
// Объектная ссылка мне больше не нужна.
pObjVal>Release ( ) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( ( SUCCEEDED ( hr ) ) )
{
// Получаем маркер синонима типа для класса.
mdTypeDef ClassDef ;
hr = pClass>GetToken ( &ClassDef ) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( SUCCEEDED ( hr ) )
{
// Для просмотра маркера класса мне нужен модуль,
// чтобы запросить интерфейс метаданных.
ICorDebugModule * pMod ;
hr = pClass>GetModule ( &pMod ) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( SUCCEEDED ( hr ) )
{
// Получаем метаданные.
IMetaDataImport * pIMetaDataImport = NULL ;
hr = pMod>
GetMetaDataInterface ( IID_IMetaDataImport ,
(IUnknown**)&pIMetaDataImport ) ;
ASSERT ( SUCCEEDED ( hr ) ) ;
if ( SUCCEEDED ( hr ) )
{
// Наконец, получаем имя класса.
ULONG ulCopiedChars ;
hr = pIMetaDataImport>
GetTypeDefProps ( ClassDef
szBuffer
uiBuffLen
&ulCopiedChars

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

430

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

NULL
NULL
ASSERT ( ulCopiedChars < uiBuffLen ) ;
if ( ulCopiedChars == uiBuffLen )
{
hr = S_FALSE ;
}

,
) ;

pIMetaDataImport>Release ( ) ;
}
pMod>Release ( ) ;
}
}
pClass>Release ( ) ;
}
}
return ( hr ) ;
}
Имя класса для исключения получено — оставалось просмотреть стек. Я уже
получил интерфейс ICorDebugChainEnum, так что просмотр стека сводился к следо
ванию алгоритму, описанному в файле DebugRef.DOC. Единственное, что интересно
в просмотре стека: нельзя просмотреть неуправляемый стек с помощью отладоч
ного API. Чтобы проверить, является ли цепочка управляемой, вызовите ICorDebug
Chain::IsManaged.
Для меня ExceptionMon оказался бесценным помощником в слежении за ис
ключениями, которые генерируют мои приложения. Я совершенно доволен вы
водом в текстовый файл, но вам может прийти мысль добавить возможность вы
вода информации через GUI, чтобы видеть исключения почти в реальном време
ни. Это несложно, и это прекрасный способ изучить программирование Windows
Forms!

Использование исключений в .NET
Теперь, когда ExceptionMon следит за вашими исключениями, хочу поговорить о
применении исключений в .NET. То, что в .NET исключения встроены внутрь, —
определенно повод для ликования. Для тех, кто перешел из C++ Win32, исключе
ния C++ казались прекрасной идеей, но их реализация оставляла желать много
лучшего. Поскольку .NET обладает ясной и целостной манерой обработки исклю
чений библиотеки классов .NET Framework (FCL), разработка в .NET становится
гораздо проще.
Я готов был написать отдельную главу по обработке исключений, но мой кол
лега Джеффри Рихтер (Jeffrey Richter) уже проделал замечательную работу в сво
ей книге «Applied Microsoft .NET Framework Programming» (издательство Microsoft
Press, 2002 год) и «Applied Microsoft .NET Framework Programming in Microsoft Visual
Basic .NET» (Microsoft Press, 2003). Его главы по обработке исключений (глава 18
в обеих книгах) следует прочесть всем, кто занимается разработкой в .NET. Но я
хочу особо подчеркнуть некоторые аспекты использования и создания собствен
ных исключений в программах.

ГЛАВА 10

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

431

Первое: исключения для исключительных событий. Мы все слышали это, но я
обнаружил, что у многих разработчиков проблемы с определением. Мое опреде
ление состоит в том, что исключение следует инициировать, только когда встре
чается ошибка или непредвиденные условия. Одной из виденных мною у разра
ботчиков ошибок было использование исключений вместо оператора switch…case.
(Я правда это видел!) Инициируйте исключения, только когда чтото не так. Не
возвращайте общие коды состояния с помощью исключений.
Аргумент в поддержку постоянного использования исключений состоит в том,
что разработчики никогда не проверяют возвращаемые значения. Для меня это
ложный аргумент, так как, если разработчики не проверяют возвращаемые зна
чения, значит, они не выполняют свои обязанности и их следует уволить. Я имею
в виду, что мне встречались люди, которые злоупотребляют исключениями, тогда
как код был бы намного четче и быстрее, если б они просто возвращали значе
ние. В своем коде я применяю такое общее правило: всегда инициировать исклю
чения в открытых методах и свойствах при ошибке. Таким образом, для тех, кто
использует мой код, формируется единый подход к обработке ошибок. Внутри
своего класса вместо инициации внутренних вспомогательных функций я при
меняю возвращаемые значения, оставляя инициацию исключений для главных
методов. Разумеется, если один из этих внутренних методов действительно попа
дает в ошибочные условия, я тут же инициирую исключение. Все это вполне ло
гично.
Я говорил об ущербе производительности, потому что, несмотря на свободу
исключений в .NET, внутри они реализуются через SEH. Если хотите это прове
рить, отладьте приложение .NET, используя отладку в неуправляемом режиме (native
mode–only debugging), — вы увидите те же первые случаи исключения при ини
циации вашего исключения. Это подразумевает переход в режим ядра при каж
дом исключении. Идея, повторяю, в том, чтобы инициировать исключения при
ошибках, а не в нормальном ходе выполнения программы.
Серьезнейшая проблема с исключениями состоит в том, что трудно узнать, что
перехватывать при использовании FCL. Как говорит Джеффри в главе об исклю
чениях (правило, которое вы, вероятно, затвердили), перехватывайте только те
исключения, что подходят используемым объектам. Каждый метод и свойство в
документации FCL содержит раздел Exceptions. Когда я применяю каждое свой
ство или метод, то всегда дважды сверяюсь со справкой (к счастью, справка по F1
достаточно «поумнела», чтобы открывать правильный раздел) и проверяю все
инициируемые исключения, дабы убедиться, что я перехватываю лишь те, что
инициируются согласно документации. Следите за перехватом исключений, что
бы избежать неожиданностей.
Microsoft в C# использует те же документирующие комментарии, что и для
создания справочной документации MSDN, и, как я говорил в главе 9, почти та
кую же документацию можно создавать с помощью прекрасного инструмента Ndoc
(его можно скачать по адресу http://ndoc.sourceforge.net). Чтобы облегчить жизнь
тем, кто использует ваши объекты, заполняйте тэги и ука
зывайте все исключения, инициируемые в вашем коде. Кроме того, неплохо бы
дважды проверить все выполняемые вами FCLвызовы и указать, какие исключе
ния могут быть инициированы в этих методах, чтобы предоставить полный от

432

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

чет. При проверке кода контроль того, что исключения полностью документиро
ваны, — одна из моих «горячих клавиш», и я всегда стараюсь убедиться, что это так.
Раз я упомянул о проверке кода и исключениях, укажу еще три цели, к кото
рым всегда стремлюсь. Первая: блоки Finally в любых методах или свойствах, от
крывающие чтото, что может быть истолковано как ресурс с описателем (handle
based resource), что обеспечивает очистку этих элементов. Я также ищу любые блоки
catch (Exception) {…} или catch {…} и убеждаюсь, что они выполняют инициацию.
Наконец, я всегда перепроверяю, чтобы повторные инициации не содержали после
себя параметра, как здесь:

try
{
// Чтото выполняем.
}
catch (DivideByZeroException e )
{
// НЕ ДЕЛАЙТЕ ЭТОГО!!
throw e ;
}
Повторно инициируя исключение, вы теряете информацию о его происхождении.
Последнее, что хочется отметить об исключениях, касается оператора using в
C#. Оператор using разворачивается в тот же ILкод, что и блок try…finally, и вы
зывает метод Dispose для единственного объекта, указанного в операторе using. При
менение оператора using абсолютно оправданно, но я предпочитаю этого не де
лать, так как происходящее за кадром не вполне очевидно.

Резюме
Концепция исключений в .NET радикально отличается от исключений в Win32.
Благодаря ExceptionMon, у вас теперь есть способ мониторинга исключений, а
значит, теперь вы сможете применять их более эффективно. Советую поэкспери
ментировать с ExceptionMon — вы удивитесь тому, что происходит в ваших при
ложениях.
Волшебная сила ExceptionMon в невероятном Profiling API. Поскольку Profiling
API позволяет видеть все интересное, что происходит внутри CLR, в ваших руках
огромная сила для написания таких инструментов, о которых в прошлом можно
было только мечтать. Как вы увидите дальше, Profiling API позволяет делать еще
больше.
Надеюсь, я дал вам пищу для размышлений о применении и реализации ис
ключений в ваших собственных проектах .NET. Но все же библией в изучении
исключений остаются главы книг Джеффри Рихтера, и я рекомендую вам прочесть
их. Ключ в том, чтобы продумывать и планировать использование исключений с
самого начала. Теперь в нашем распоряжении есть этот прекрасный инструмент,
но если мы будем использовать его неправильно, то можем спровоцировать про
блемы по мере развития продукта.

Г Л А В А

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

В главе 10 я вкратце затронул описание возможностей Profiling API. В этой гла
ве я расскажу про Profiling API подробнее и рассмотрю программу, которую мне
всегда хотелось иметь в своем распоряжении. В главе 6 я упоминал очень полез
ную команду wt (Watch and Trace — наблюдение и трассировка) консольного от
ладчика управляемого кода CORDBG.EXE. Как вы помните, она позволяет увидеть
поток вызовов методов, а значит, и поток выполнения всей программы. Команда
wt обеспечивает фантастический способ обнаружения «узких мест», которые просто
невозможно найти путем простого изучения исходного кода.
К сожалению, CORDBG — консольное приложение, что не способствует его
пониманию. Кроме того, CORDBG работает медленно, так как использует для трас
сировки пошаговый механизм отладки. Я хочу, чтобы трассировка была быстрой,
а выводимая в результате информация — простой в использовании. Вот для это
го и нужна моя любимая утилита FlowTrace. Она дает вам силу wt без всякого горь
кого привкуса!
Сначала я покажу, насколько легко и эффективно устанавливать ловушки для
вызовов методов при помощи Profiling API. Объяснив, как использовать програм
му FlowTrace, я опишу некоторые вопросы ее реализации, чтобы ее работа стала
понятнее. Наконец, функциональность FlowTrace легко расширить, поэтому в конце
главы я поделюсь коекакими идеями, которые помогут вам сделать эту програм
му еще полезнее.

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

434

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

получении правильных временных интервалов, связанных с элементами пользо
вательских приложений, оставалось только мечтать. Теперь этот механизм уведом
ления о вызовах функций встроен в Profiling API — еще одно доброе дело со сто
роны Microsoft, заслуживающее искренней благодарности. Благодаря этому раз
работчики инструментов могут сосредоточиться на решении важных проблем
профилирования, не тратя длительного времени на создание инфраструктуры
своих утилит.

Запрос уведомлений входа и выхода
Profiling API позволяет получать уведомления обо всех вызовах методов и обо всех
возвратах из них. Ключи /Gh и /GH (включающие функцииловушки _penter и _pexit
соответственно) неуправляемого компилятора C++ играют по сути ту же роль, что
и Profiling API, однако Profiling API делает уведомления еще проще, предоставляя
также FunctionID выполняемой функции.
Как и в случае всех остальных уведомлений, исполняющей среде сначала нуж
но сообщить, что вы хотите получать уведомления входа и выхода; для этого надо
установить в битовой маске при помощи операции ИЛИ флаг COR_PRF_MONITOR_ENTER
LEAVE и передать маску методу ICorProfilerInfo::SetEventMask. Готов спорить, что,
как только вы запросите уведомления входа и выхода, вам не понадобится изме
нять их на протяжении всего существования процесса. И все же славные парни
из Microsoft позволяют вам включать/отключать уведомления входа и выхода сколь
ко душе угодно. Не забывайте про эту возможность, так как на ее основе можно
создать очень интересные инструменты, измеряющие, например, только время
обработки исключений.
После этого исполняющей среде нужно сообщить, какие функцииловушки вам
хотелось бы вызывать; это делается при помощи метода ICorProfilerInfo::SetEnter
LeaveFunctionHooks, который принимает три указателя на вызываемые функции
ловушки: функцию входа, функцию выхода и функцию выхода типа tailcall. На
значение первых двух функций очевидно, а вот третья нуждается в пояснении.
В настоящей версии CLR функции tailcall никогда не вызываются. Вызов типа tailcall
имеет место, когда кадр стека текущего метода удаляется до выполнения коман
ды call. Иначе говоря, если при вызове метода кадр стека вызывающего метода уже
не нужен, он очищается. В будущих версиях CLR компилятор будет поддерживать
tailcallоптимизацию, тогда она вам и понадобится. Так как для большинства пользо
вателей Profile API различия между функцией выхода типа tailcall и обычной фун
кцией выхода не имеют значения, можете с чистой совестью применять обычную
функцию выхода.

Реализация функций-ловушек
Особенность установки ловушек заключается в определении функцийловушек.
Для обеспечения максимально высокой производительности Profiling API требу
ет, чтобы они использовали соглашение вызова naked. По сути ваши функции встра
иваются в код JITкомпилятором, так что вы должны сами написать для них про
лог и эпилог.
Объявления typedef для всех функцийловушек выглядят так:

ГЛАВА 11

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

435

typedef void FunctionEnter ( FunctionID funcID ) ;
Из документации не совсем ясно (к счастью, это становится понятным при изу
чении примеров профилирования), что в одном аспекте функцииловушки похожи
на стандартные вызовы: они сами отвечают за извлечение параметра FunctionID
из стека. В комментариях в файле CorProf.IDL, которому всегда нужно доверять
больше, чем Profiling.DOC, указано, что функцииловушки должны сохранять и все
изменяемые регистры, в том числе регистры для работы с числами с плавающей
точкой.
Пример функцииловушки — в листинге 111. Функцииловушки используют
соглашение вызова naked, поэтому вы сами должны написать пролог и эпилог. Вся
действительная работа выполняется в методе CFlowTrace::FuncEnter, таким образом,
функцияловушка — всего лишь оболочка для его вызова. Пролог (первые три
команды PUSH) сохраняет изменяемые регистры в стеке. Последние четыре коман
ды — эпилог, который восстанавливает сохраненные регистры и выполняет воз
врат из функции. Команда RET 4 возвращает и удаляет из стека переданный функ
цииловушке идентификатор функции, сохраняя мне одну команду POP.
Четыре команды, расположенные в середине функции, вызывают метод CFlow
Trace::FuncEnter, передавая ему указатель на экземпляр класса и идентификатор
функции. Идентификатор функции был передан функцииловушке входа. Теперь
он находится в стеке на 16 (0x10) байт выше: до трех сохраненных регистров и
адреса возврата. Команда PUSH [ESP + 10] помещает в стек его копию для передачи
функции FlowTrace::FuncEnter. Внимательные читатели заметили, что в объявлении
функции CFlowTrace::FuncEnter указано, что она принимает только один параметр.
Это объясняется тем, что в методы классов C++ всегда сначала передается указатель
на экземпляр класса (или указатель this); это скрытый параметр. Я пытался напи
сать на встроенном ассемблере функциюловушку меньшего объема, но мне кажется,
что функцию, представленную в листинге 111, уменьшить уже невозможно.

Листинг 11-1.

Пример функции-ловушки

void __declspec ( naked ) NakedEnter ( FunctionID /*funcID*/ )
{
__asm
{
PUSH EAX
// Сохранение изменяемых регистров.
PUSH ECX
PUSH EDX
PUSH [ESP + 10h]
MOV ECX , g_pFlowTrace
PUSH ECX
CALL CFlowTrace::FuncEnter
POP EDX

//
//
//
//
//

В стек в качестве параметра
помещается идентификатор функции.
В стек помещается указатель
на экземпляр класса.
Вызов метода FuncEnter.

// Восстановление сохраненных
// регистров.

POP ECX
POP EAX
см. след. стр.

436

ЧАСТЬ III

RET 4

Мощные средства и методы отладки приложений .NET

// Возврат и удаление из стека
// полученного идентификатора
// функции.

}
}

Встраивание
При обсуждении уведомлений от функцийловушек нельзя не рассмотреть вопрос
встраивания (inlining). Ядро исполняющей подсистемы CLR оптимизировано,
поэтому, чтобы сэкономить пару тактов процессора, оно очень часто будет встра
ивать методы прямо в код. Это значит, что вы увидите сообщения о вызовах и
возвратах не для всех методов, а только для тех, которые не были встроены.
Если вы хотите получить полный список всех вызовов программы, есть два
способа отключения встраивания. Однако, как вы можете представить, запреще
ние встраивания может заметно ухудшить быстроту выполнения управляемого кода.
Проще всего отключить встраивание — установив при помощи операции ИЛИ флаг
OR_PRF_DISABLE_INLINING в битовой маске, передаваемой методу ICorProfilerInfo::Set
EventMask при обработке уведомления ICorProfilerCallback::Initialize. Недостаток
этого метода в том, что флаг COR_PRF_DISABLE_INLINING нельзя изменить, поэтому вы
выключите его на все время жизни процесса независимо от того, где выполняет
ся ваш код.
Второй способ предоставляет более точный контроль над встраиванием, но
требует гораздо больше работы. В число получаемых вами уведомлений JIT вхо
дит уведомление JITInlining, которое, как можно догадаться по его названию, ука
зывает, что функция встраивается в другую функцию (для получения уведомле
ний JIT нужно операцией ИЛИ установить в маске событий (event mask) флаг
COR_PRF_MONITOR_JIT_COMPILATION). JITInlining имеет такие параметры: FunctionID
вызывающей функции, FunctionID вызываемой (встраиваемой функции) и указа
тель на BOOL, который при установке в FALSE предотвращает встраивание.
Уведомление JITInlining позволяет сделать очень интересные вещи. Например,
вы можете оставить встраивание включенным для классов библиотеки классов .NET
Framework (.NET Framework class library, FCL), отключив его для другого кода. Однако
при этом нужно быть внимательным, так как CLR вызывает JITInlining огромное
количество раз и, если ваш код каждый раз будет просматривать значения Func
tionID вызывающей и вызываемой функции, это приведет к куда более серьезно
му снижению быстродействия, чем выключение встраивания для всего процесса.
Вы можете рассмотреть вариант сохранения интересующих вас значений FunctionID,
но помните, что изза сборки мусора CLR они могут измениться, поэтому для
поддержания таблиц данных в правильном состоянии вам придется обрабатывать
уведомления сборки мусора.

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

ГЛАВА 11

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

437

функциямловушкам. CLR вызывает ее перед любой из функцийловушек. Вы не
обязаны применять FunctionIDMapper, однако это может открыть перед вами очень
интересные возможности.
Изменение значений FunctionID при помощи FunctionIDMapper можно выполнить
только один раз; это следует делать в методе ICorProfilerCallback::Initialize пу
тем передачи указателя на функцию методу ICorProfilerInfo::SetFunctionIDMapper.
В свое время у меня возникли проблемы изза того, что прототип этой функции
в файле Profiling.DOC описан неверно. FunctionIDMapper возвращает тип UINT_PTR,
соответствующий FunctionID, а не указанный в документе void. Вот правильный ее
прототип:

UINT_PTR __stdcall FunctionIDMapper ( FunctionID functionId ,
BOOL *pbHookFunction ) ;
Интересно, что FunctionIDMapper использует стандартное соглашение вызова, а
не соглашение naked, как функции, требуемые другими функциямиловушками.
Параметр FunctionID — это функция, для которой CLR вызывает одну из функций
ловушек. Указатель на Boolean позволяет указать CLR, следует ли ей на самом деле
вызывать функциюловушку. Если вы хотите разрешить вызов ловушки, присвой
те *pbHookFunction значение TRUE. Если вы установите его в FALSE, функцияловуш
ка вызываться не будет. Чтобы изменить значение, передаваемое в качестве пара
метра функцииловушке, нужно возвратить это значение из FunctionIDMapper.
Мне кажется, что FunctionIDMapper будет интересной прежде всего тому, кто ра
ботает с Profiling API в рамках крупных проектов. Так, почти при всех вызовах фун
кцийловушек вы должны просматривать имена функций и методов. Вместо это
го можно задействовать FunctionIDMapper, которая будет просматривать функции
и передавать нужные значения функцииловушке. При этом просмотр функций
будет выполняться в одном месте.
Благодаря контролю над действительными вызовами функцийловушек вы
получаете в свое распоряжение еще больше возможностей. Так, если вам нужно
протоколировать или анализировать выполнение только одного потока, то при
помощи FunctionIDMapper вы можете определить идентификатор потока и, если он
вас не интересует, пропустить функциюловушку. Возможность пропуска функции
ловушки облегчит реализацию программы профилирования. Я сам воспользовался
этим преимуществом при написании программы FlowTrace.

Использование FlowTrace
Я ознакомил вас с основами установки ловушек при помощи Profiling API и те
перь хочу перейти к описанию работы с FlowTrace. В результате этого вы лучше
поймете некоторые вопросы реализации этой утилиты. Прежде всего хочу отме
тить, что для настройки и запуска любых программ, работающих с Profiling API,
нужно задать много переменных среды. Как и утилита ExceptionMon из главы 10,
FlowTrace позволяет определить, хотите ли вы выполнить вызов DebugBreak в на
чале программы (FLOWTRACEBREAK), а также указать, какой именно процесс вы же
лаете профилировать (FLOWTRACEPROC). Кроме того, при помощи переменной сре
ды FLOWTRACEFILEDIR можно указать конкретный каталог, в котором будут создаваться
файлы вывода. Файл настройки FlowTrace, имеющий расширение .FLS, я опишу чуть

438

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

ниже, а пока скажу, что, если вы зададите переменную FLOWTRACEFILEDIR, FlowTrace
будет искать файл .FLS в указанном вами каталоге, а не там, где находится ваш
исполняемый файл.
Чтобы облегчить работу с многопоточными приложениями, FlowTrace запи
сывает сведения о потоках в разные файлы. Имена файлов формируются так:

__.FLOW
Отдельные части имени файла говорят сами за себя. Интерес представляет толь
ко то, что вместо идентификатора потока Win32 я использовал идентификатор
управляемого потока. Как вы увидите в разделе, посвященном реализации FlowTrace,
взаимосвязь управляемых потоков и потоков Win32 отсутствует.
Наконец, перед использованием FlowTrace можно сконфигурировать необяза
тельный файл параметров. Это простой файл инициализации, расположенный в
том же каталоге, что и исполняемый файл, или в каталоге, указанном в перемен
ной среды FLOWTRACEFILEDIR. Он называется так же, как и программа, только имеет
расширение .FLS. Я мог бы задействовать для этого ультрасовременный файл XML,
но мне не хотелось проводить несколько месяцев за написанием и тестировани
ем кода C++, работающего с MSXML3.DLL, и раздувать рабочий набор FlowTrace
десятками мегабайт. В усложнении вещей нет никакого смысла, если их можно
сделать простыми.
Первый из трех необязательных параметров FlowTrace позволяет включить/
отключить встраивание. По умолчанию встраивание включено, благодаря чему все
процессы, выполняемые под FlowTrace, работают быстрее. Второй параметр пред
назначен для запрещения протоколирования потока финализации (finalizer thread).
Все процессы .NET имеют сборщик мусора, работающий в отдельном потоке. Я
обнаружил, что большинство вызовов потока финализации связано с очисткой
объектов, созданных CLR. Чтобы минимизировать объем выводимой информации,
я решил регистрировать только реальные действия процесса, выполняемые в других
потоках, потому что мне кажется, что это дает более полезные сведения. Итак, по
умолчанию FlowTrace не регистрирует поток финализации, но позволяет с лег
костью включить эту функцию. Третий параметр определяет, протоколировать ли
стартовый код для первоначальной AppDomain, создаваемой основным потоком, так
как метод System.AppDomain.SetupDomain создает большой объем вывода. По умол
чанию я не регистрирую стартовый код. В листинге 112 приведен файл .FLS по
умолчанию. Чтобы включить конкретный параметр, присвойте ему 1; чтобы от
ключить — 0.

Листинг 11-2.

Файл .FLS по умолчанию

; Пример конфигурационного файла .FLS. Назовите этот файл
; так же, как и исполняемый, и поместите его в тот же каталог.
; Здесь указаны все общие параметры. Все они имеют
; значения по умолчанию, принимаемые, если исполняемый
; файл не имеет соответствующего файла .FLS.
[General Options]

ГЛАВА 11

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

439

; Значение 1 отключает встраивание. Это приведет к получению
; гораздо большего объема информации о выполнении программы.
TurnOffInlining=0
; Отключить обработку потока финализации.
IgnoreFinalizerThread=1
; Пропускать все вызовы при создании AppDomain в основном потоке.
SkipStartupCodeOnMainThread=1

Некоторые сведения о реализации FlowTrace
Теперь я хочу обсудить некоторые вопросы реализации FlowTrace. Первая про
блема заключалась в том, что в будущем между управляемыми потоками и пото
ками Win32 однозначного соответствия не будет. В первых версиях Microsoft .NET
Framework такое соответствие имеется. Сначала я реализовал FlowTrace при по
мощи надежного варианта локальной памяти потока, гарантирующего, что каж
дый поток имеет специфические данные. Однако, изучая Profiling.DOC, я заметил
специальное уведомление о потоке ICorProfilerCallback::ThreadAssignedToOSThread.
В описании говорится: «Во время выполнения конкретный поток исполняющей
среды может переключаться между различными потоками, что зависит от испол
няющей среды и внешних компонентов, выполняющихся в процессе». Конечно,
это не могло не привлечь моего внимания, и после консультации с программис
тами Microsoft я понял, что простое решение с локальной памятью потока в буду
щем работать не будет.
К счастью, уведомления интерфейса ICorProfilerCallback о создании и унич
тожении потока предоставляют идентификатор управляемого потока в качестве
параметра; кроме того, узнать идентификатор управляемого потока в любое вре
мя позволяет ICorProfilerInfo::GetCurrentThreadID, так что идентификация управ
ляемого потока проблем не представляет. Обратная сторона такого подхода зак
лючалась в том, что мне нужно было создать собственную «локальную память уп
равляемого потока» при помощи глобального класса отображения (map) библио
теки стандартных шаблонов (STL). Конечно, для предотвращения проблем с не
сколькими потоками я должен был сделать ее критической секцией. Многие ме
тоды обратного вызова интерфейса IcorProfilerCallback в Profiling API очень не
гативно относятся к блокировке при их обработке, поэтому я был немного
смущен. Однако длительное тестирование позволяет мне утверждать, что это не
оказывает заметного эффекта.
Вторая проблема была связана с тем, как пропускать стартовые вызовы, выпол
няемые методом System.AppDomain.SetupDomain в основном потоке. Поэксперимен
тировав с многочисленными управляемыми приложениями, я заметил, что в на
чале приложение включает три потока. В документации к Profiling API упомина
ется, что при внедрении программы профилирования в управляемый процесс для
ее старта выделяется специальный поток, который, к счастью, не выполняет управ
ляемого кода. Я обнаружил, что первое уведомление о создании потока всегда
относилось к основному потоку приложения, а второе — к потоку финализации.

440

ЧАСТЬ III

Мощные средства и методы отладки приложений .NET

Узнав, как идентифицировать потоки, я смог составить план пропуска стартово
го кода. Для этого мне нужно было не регистрировать действий основного пото
ка, пока функцияловушка выхода не увидит вызова System.AppDomain.ResetBinding
Redirects.
Я мог видеть, какой поток выполняет функцию финализации, но я хотел так
же, чтобы его можно было игнорировать. Сначала я хотел установить ловушку
FunctionIDMapper, чтобы можно было проверять идентификатор управляемого по
тока. Если бы этот идентификатор соответствовал потоку финализации, я присва
ивал бы параметру pbHookFunction значение FALSE, чтобы CLR не вызывала функ
циюловушку. При тестировании этого способа на простейшей программе, напи
санной на промежуточном языке, все работало великолепно.
Однако, тестируя FlowTrace с простым приложением Microsoft Windows Forms,
я получил сообщения об ошибке, утверждавшие, что специфичные для управляе
мого потока данные имеют значение NULL. Я игнорировал поток финализации,
поэтому я не добавлял его в отображение управляемых потоков: я хотел, чтобы
оно имело минимальный размер. Изучая эти ошибки, я заметил, что для потока
финализации всегда вызывалась функцияловушка входа. Я решил, что допустил
в алгоритме какуюто ошибку, но в результате тщательной проверки так ничего и
не обнаружил.
Зарегистрировав только эти специфические проблемы с потоком финализа
ции и проштудировав документацию, я наконец выяснил, в чем дело. В приложе
ниях Windows Forms все еще присутствуют некоторые странности COM, в том числе
недостатки моделей разделенных потоков. То, что я видел, было кросспоточны
ми вызовами с маршалингом из основного потока в поток финализации. Инте
ресно, что CLR никогда не вызывала ловушку FunctionIDMapper. Значит, у меня не
было способа заблокировать вызовы ловушек. Я надеялся, что мне не придется
проверять поток финализации в функцияхловушках, чтобы не снижать быстро
действия программы, но ничего не оставалось делать. Поэтому для избежания
протоколирования вызовов финализации я должен был проверять поток фина
лизации.
Все получилось, и я смог при необходимости отключать протоколирование
финализации. Гдето через день я понял, что вызывать FunctionIDMapper нужно, только
когда пользователь специально запрашивает, чтобы я не отслеживал поток фина
лизации. Первоначально я делал это во всех случаях.
Последняя проблема, с которой я должен был справиться, состояла в том, что
бы гарантировать правильность выводимой информации в любых обстоятельствах.
Это означало, что я должен был регистрировать для функций все «разворачивае
мые» исключения, так как я никогда не встречал для них функциюловушку вы
хода. Задача оказалась достаточно простой. Для этого нужно было только вести
для потока счетчик развертывания, когда CLR вызывала ICorProfilerCallback::Ex
ceptionUnwindFunctionLeave. Как только исключение достигало ICorProfilerCallback::Ex
ceptionCatcherLeave, я просто вычитал число развернутых функций для текущего
уровня программы.

ГЛАВА 11

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

441

Что после FlowTrace
FlowTrace — очень полезное средство обучения. Но, как и все утилиты, ее можно
сделать еще лучше. Если вам нужен интересный проект, вот список некоторых
отличных функций, которые вы могли бы попытаться добавить в FlowTrace.
쐽 Добавьте в файл .FLS функцию, позволяющую начинать и останавливать про
токолирование для конкретных классов и методов. Благодаря этому вы смо
жете точно указывать, что вы желаете регистрировать, не путаясь средивсех
остальных сообщений. В идеале эта функция должна поддерживать протоко
лирование как для отдельных, так и для всех потоков. Еще лучше реализовать
указание интересующих вас классов и методов при помощи регулярных вы
ражений; так вы сможете еще точнее определять, что нужно и не нужно реги
стрировать.
쐽 Было бы неплохо начинать работу вообще без протоколирования и запускать
его в конкретной точке при помощи внешнего события. Конечно, нужно реа
лизовать и остановку протоколирования!
쐽 Вы можете добавить в FlowTrace возможность вывода времени выполнения
функций. Вся необходимая для этого инфраструктура в FlowTrace уже реали
зована.
쐽 Вместо сохранения вывода в текстовом файле вы могли бы записывать и ото
бражать информацию в псевдореальном времени при помощи приложения с
графическим интерфейсом.
쐽 Наконец, файлы вывода FlowTrace могут быть очень объемными. Неплохо было
бы иметь возможность фильтрации частых базовых вызовов, таких как
Object..ctor.

Резюме
Определить последовательность вызовов по исходному коду иногда очень труд
но. Надеюсь, FlowTrace сделает слежение за потоком выполнения ваших управля
емых приложений более простой задачей, и вы сможете оптимизировать отладку
и настройку производительности своих программ. Подумайте, какие другие ути
литы можно разработать на основе Profiling API, настолько облегчающего уста
новку ловушек для входа и выхода из функций. Конечно, у Profiling API есть и
недостатки, но я все равно считаю, что это одно из самых удивительных средств
платформы .NET.

Ч А С Т Ь

I V

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

Г Л А В А

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

Ваша программа потерпела крах. ОС оказалась достаточно любезной и предос
тавила вам адрес ошибки. Что дальше? Мой друг Крис Селлз (Chris Sells) называет
такой сценарий проблемой «моя программа ушла в отпуск, оставив мне только
этот никчемный адрес». Конечно, иметь адрес ошибки лучше, чем не иметь ниче
го, но знать исходный файл и номер строки ошибки было бы еще полезнее. Вы
можете предоставить исходные коды своим клиентам, чтобы они отладили про
блему сами, но я не думаю, что это станет реальностью в ближайшем будущем.
Адрес ошибки — обычно все, что вы получаете, и то, если вы очень удачливы.
Microsoft прилагает немалые усилия, чтобы облегчить своим сотрудникам поиск
проблем с Microsoft Windows 2000/XP/Server 2003. Поиск ошибок усложняется, если
ваши пользователи просто сообщают вам, что ваша программа не работает. Так,
обработчик необработанных исключений, используемый по умолчанию, выводит
«чудесное» диалоговое окно «Приносим извинения за неудобства» (рис. 121).
К великому сожалению, в этом окне больше не выводится адрес ошибки! Класси
ческий пример изменения, дружественного к пользователям, но неприятного для
нас, программистов.
Для получения адреса ошибки щелкните ссылку To See What Data This Error Report
Contains, Click Here (для просмотра данных, содержащихся в отчете, щелкните
здесь), а в следующем диалоговом окне — ссылку To View Technical Information About
The Error Report, Click Here (для просмотра технических сведений об ошибке
щелкните здесь). В диалоговом окне Error Report Contents (содержание отчета об
ошибке) (рис. 122) вы увидите адрес ошибки и все загруженные модули. Кроме
того, вы получите понастоящему полезный дамп стека, достаточно большой, чтобы
можно было изучить историю неправильного потока почти до начала времен. Уди
вительно, но ктото принял очень странное решение и поместил всю эту замеча

ГЛАВА 12

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

445

тельную информацию в статический элемент управления, поэтому скопировать
ее нельзя. Надеюсь, в следующем пакете обновлений для Windows XP эта досад
ная оплошность будет исправлена. Есть и хорошие новости: Windows 2000/Server
2003 всегда выводят в диалоговом окне адрес ошибки.

Рис. 121.

Стандартное диалоговое окно сообщения об ошибке Windows XP

Рис. 122.

Диалоговое окно «Содержание отчета об ошибке» Windows XP

Вас порадует и то, что независимо от нажатой в диалоговом окне кнопки бу
дет запускаться Dr. Watson, если, конечно, он назначен стандартным отладчиком,
что имеет место по умолчанию. Если это не так, пусть ваши пользователи сдела
ют Dr. Watson отладчиком по умолчанию, выполнив команду DRWTSN32.EXE –i. Что
бы получать информацию Dr. Watson, вы должны объяснить пользователям, как
запускать Dr. Watson и копировать аварийный дамп памяти при помощи пользо
вательского интерфейса Dr. Watson (подробнее о журнале регистрации Dr. Watson
и его интерпретации см. приложение А). Вы будете очень довольны, если пользо
ватели смогут прислать вам минидампы, поэтому попросите их установить в окне
Dr. Watson флажок Create Crash Dump File (создание файла аварийной копии па

446

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

мяти) и запомнить место записи файлов. Конечно, большинство из нас не боится
получить адрес ошибки. Если вы обо всем позаботились заранее, то при ошибке
ваши приложения автоматически будут высылать вам минидампы, о которых я
расскажу в главе 13. Очевидно, что адрес ошибки нужно както преобразовать в
имя исходного файла и номер некорректной строки. Этой теме и посвящена данная
глава. Я расскажу про два основных способа преобразования адреса в нужную
информацию: при помощи MAPфайлов и при помощи утилиты CrashFinder, при
лагаемой к книге.
Чтобы получить максимум от рассматриваемых в этой главе методов, настройте
компилятор, как указано в главе 2. Все заключительные компоновки нужно создавать
с полным набором отладочных символов и MAPфайлами. Кроме того, вам нуж
но разрешать все конфликты с адресами загрузки DLL. Если эти условия соблю
дены не будут, то описанные ниже методы окажутся бесполезными, и определить
исходный файл и строку ошибки вы сможете только путем гадания.

Создание и чтение MAP-файла
Меня часто спрашивают, почему я так настойчиво рекомендую создавать MAP
файлы для заключительных компоновок. Если коротко, то MAPфайлы — единствен
ный способ представления данных о глобальных символах, исходных файлах и
номерах строк вашей программы в текстовом виде. Использовать CrashFinder
проще, зато MAPфайлы можно читать всегда и везде, и они не требуют никакой
поддерживающей программы и двоичных файлов вашего приложения, предостав
ляя ту же информацию. Поверьте, рано или поздно вам понадобится найти рас
положение ошибки в более старой версии вашей программы, и у вас будет толь
ко один способ сделать это — изучить MAPфайл.
MAPфайлы полезны только в случае заключительных компоновок, потому что
при создании MAPфайла компоновщик вынужден отключать компоновку с при
ращением. Включить генерирование MAPфайлов в Microsoft Visual C++ .NET го
раздо проще, чем в предыдущих версиях Visual Studio. Откройте диалоговое окно
Property Pages, папку Linker, страницу Debugging и просто выберите значение Yes
в полях Generate Map File (генерировать MAPфайл), Map Exports (включать в MAP
файл информацию об экспортируемых функциях) и Map Lines (включать в MAP
файл информацию о номерах строк). Это установит ключи компоновщика /MAP,
/MAPINFO:EXPORTS и /MAPINFO:LINES. Как вы, наверное, догадались, программа Settings
Master из главы 9 с файлами проекта по умолчанию сделает это автоматически.
Если вы работаете над реальным проектом, то, вероятно, сохраняете двоичные
файлы в отдельном каталоге. По умолчанию компоновщик записывает MAPфайл
в тот же каталог, что и промежуточные файлы, поэтому вам нужно явно указать,
чтобы он хранился вместе с двоичными файлами. Для этого можно ввести в поле
Map File Name (имя MAPфайла) выражение $(OutDir)/$(ProjectName).map. $(OutDir) —
это встроенный макрос, который система сборки программы заместит именем
реального каталога вывода, а вместо $(ProjectName) будет использовано название
проекта. На рис. 123 показаны все значения параметров MAPфайлов для заклю
чительной компоновки проекта MapDLL, прилагаемого к книге.

ГЛАВА 12

Рис. 123.

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

447

Значения параметров MAPфайлов в диалоговом окне Property Pages

Вполне возможно, что в повседневной работе MAPфайлы вам не понадобят
ся, но почти наверняка они потребуются вам в будущем. Работа CrashFinder и ва
шего отладчика основана на таблице символов, которую они читают при помо
щи сервера символов. Если формат таблицы символов изменится или вы забуде
те сохранить файлы базы данных программы (Program Database, PDB), у вас воз
никнут крупные неприятности. Ответственность за сохранение PDBфайлов ле
жит на вас, но формат таблиц символов вам неподвластен. Он часто изменяется.
Например, многие люди, перешедшие с Microsoft Visual Studio 6 на Microsoft Visual
Studio .NET, заметили, что такие средства, как CrashFinder, отказываются работать
с программами, откомпилированными при помощи Visual Studio .NET. Microsoft
изменила формат таблицы символов и делает это регулярно. В таких случаях един
ственное ваше спасение — MAPфайлы.
Даже если лет через пять вы будете писать программы с помощью Visual Studio
.NET 2007 Service Pack 6 для Windows Server 2008, я абсолютно уверен, что неко
торые ваши клиенты еще будут работать с вашими приложениями, созданными в
2003 году. Когда они обратятся к вам за помощью и предоставят адрес ошибки,
несколько дней вам понадобится только на то, чтобы найти компактдиски со
старой версией Visual Studio .NET, нужной для чтения сохраненных PDBфайлов.
Если у вас будут MAPфайлы, вы найдете место проблемы за пять минут.

Содержание MAP-файла
Пример MAPфайла показан в листинге 121. В начале MAPфайла указывается имя
модуля, время компоновки и предпочтительный адрес загрузки. После заголовка
располагается информация о разделах, показывающая, какие разделы созданы
компоновщиком из файлов OBJ и LIB.
После информации о разделах находятся действительно полезные данные:
информация об открытых (public) функциях. Обратите внимание на слово «от
крытых». Если у вас есть статические функции, сведения о них сохраняются в
аналогичной таблице после таблицы об открытых функциях. К счастью, их но
мера строк не разделяются и выводятся вместе.

448

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Важные сведения заключены в именах открытых функций и данных столбца
Rva+Base, содержащего стартовые адреса функций. Буква f после некоторых ад
ресов Rva+Base означает, что адрес соответствует действительной функции, а не
глобальной переменной или какойлибо импортируемой функции. За разделом
открытых функций следует информация о строках в формате:

24 0001:00000006
Первое число — номер строки, а второе — смещение от начала раздела кода, к
которому эта строка относится. Согласен, это звучит несколько запутанно, поэтому
позднее я объясню, что нужно сделать для преобразования адреса в исходный файл
и номер строки.
Если модуль содержит экспортируемые функции, их список будет приведен в
заключительном разделе MAPфайла. Такую же информацию вы получите, запус
тив программу DUMPBIN с параметрами /EXPORTS .

Листинг 12-1.

Пример MAP-файла

MapDLL
Timestamp is 3e2b44a3 (Sun Jan 19 19:36:51 2003)
Preferred load address is 03900000
Start
0001:00000000
0002:00000000
0002:00000030
0002:00000128
0002:00000190
0002:00000194
0002:00000198
0002:0000019c
0002:000001a0
0002:000001a4
0002:000001b8
0002:000001cc
0002:000001f4
0002:00000280
0003:00000000
0003:00000004
0003:00000008
0003:0000000c
0003:00000010
0003:00000014
Address
0000:00000001
0001:00000000
0001:00000006

Length
00000304H
00000028H
000000f8H
00000063H
00000004H
00000004H
00000004H
00000004H
00000004H
00000014H
00000014H
00000028H
00000082H
0000007bH
00000004H
00000004H
00000004H
00000004H
00000004H
00000014H

Name
.text
.idata$5
.rdata
.rdata$debug
.rdata$sxdata
.rtc$IAA
.rtc$IZZ
.rtc$TAA
.rtc$TZZ
.idata$2
.idata$3
.idata$4
.idata$6
.edata
.CRT$XCA
.CRT$XCZ
.CRT$XIA
.CRT$XIZ
.data
.bss

Class
CODE
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA
DATA

Publics by Value

Rva+Base

Lib:Object

___safe_se_handler_count
_DllMain@12
?MapDLLFunction@@YAHXZ

00000001
03901000 f
03901006 f


MapDLL.obj
MapDLL.obj

ГЛАВА 12

0001:00000023
0001:0000003c
0001:000000fa
0001:000001de
0001:000001e4
0001:0000020a
0001:0000021c
0001:00000260
0001:000002a4
0001:000002ac
0001:000002e7
0001:000002f8
0001:000002fe
0002:00000000
0002:00000004
0002:00000008
0002:0000000c
0002:00000010
0002:00000014
0002:00000018
0002:0000001c
0002:00000020
0002:00000024
0002:0000007c
0002:000000a0
0002:000000c4
0002:000000e0
0002:00000190
0002:00000194
0002:00000198
0002:0000019c
0002:000001a0
0002:000001a4
0002:000001b8
0003:00000000
0003:00000004
0003:00000008
0003:0000000c
0003:00000010
0003:00000018
0003:0000001c
0003:00000020
0003:00000024
entry point at

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

449

?MapDLLHappyFunc@@YAPADPAD@Z 03901023 f MapDLL.obj
__CRT_INIT@12
0390103c f MSVCRT:crtdll. obj
__DllMainCRTStartup@12
039010fa f MSVCRT:crtdll. obj
__initterm
039011de f MSVCRT:MSVCR71 .dll
__onexit
039011e4 f MSVCRT:atonexi t.obj
_atexit
0390120a f MSVCRT:atonexi t.obj
__RTC_Initialize
0390121c f MSVCRT:initsec t.obj
__RTC_Terminate
03901260 f MSVCRT:initsec t.obj
___CppXcptFilter
039012a4 f MSVCRT:MSVCR71 .dll
__SEH_prolog
039012ac f MSVCRT:sehprol g.obj
__SEH_epilog
039012e7 f MSVCRT:sehprol g.obj
__except_handler3
039012f8 f MSVCRT:MSVCR71 .dll
___dllonexit
039012fe f MSVCRT:MSVCR71 .dll
__imp__printf
03902000
MSVCRT:MSVCR71 .dll
__imp__free
03902004
MSVCRT:MSVCR71 .dll
__imp___initterm
03902008
MSVCRT:MSVCR71 .dll
__imp__malloc
0390200c
MSVCRT:MSVCR71 .dll
__imp___adjust_fdiv
03902010
MSVCRT:MSVCR71 .dll
__imp____CppXcptFilter
03902014
MSVCRT:MSVCR71 .dll
__imp___except_handler3
03902018
MSVCRT:MSVCR71 .dll
__imp____dllonexit
0390201c
MSVCRT:MSVCR71 .dll
__imp___onexit
03902020
MSVCRT:MSVCR71 .dll
\177MSVCR71_NULL_THUNK_DATA 03902024
MSVCRT:MSVCR7 1.dll
??_C@_0CE@EBHAJKCA@Whoops?0?5a?5crash?5is?5about?5to?5 occu@
0390207c
MapDLL.obj
??_C@_0CD@OILENIKO@Hello?5from?5InternalStaticFunctio@
039020a0
MapDLL.obj
??_C@_0BM@DFMPKPOD@Hello?5from?5MapDLLFunction?$CB?6?$ AA@
039020c4
MapDLL.obj
__load_config_used
039020e0
MSVCRT:loadcfg .obj
___safe_se_handler_table 03902190

___rtc_iaa
03902194
MSVCRT:initsec t.obj
___rtc_izz
03902198
MSVCRT:initsec t.obj
___rtc_taa
0390219c
MSVCRT:initsec t.obj
___rtc_tzz
039021a0
MSVCRT:initsec t.obj
__IMPORT_DESCRIPTOR_MSVCR71 039021a4
MSVCRT:MSVCR7 1.dll
__NULL_IMPORT_DESCRIPTOR 039021b8
MSVCRT:MSVCR71 .dll
___xc_a
03903000
MSVCRT:cinitex e.obj
___xc_z
03903004
MSVCRT:cinitex e.obj
___xi_a
03903008
MSVCRT:cinitex e.obj
___xi_z
0390300c
MSVCRT:cinitex e.obj
___security_cookie
03903010
MSVCRT:seccook .obj
__adjust_fdiv
03903018

___onexitend
0390301c

___onexitbegin
03903020

__pRawDllMain
03903024

0001:000000fa

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

450

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

0001:00000016

?InternalStaticFunction@@YAXXZ 03901016 f

MapDLL .obj

Line numbers for .\Release\MapDLL.obj(d:\dev\booktwo\disk\chapter
examples\chapter 12\mapfile\mapdll\mapdll.cpp) segment .text
11
25
32
38

0001:00000000
0001:00000006
0001:00000016
0001:00000028

20
27
33
39

0001:00000000
0001:00000012
0001:00000022
0001:00000033

21
28
37
41

0001:00000003 26 0001:00000006
0001:00000015 31 0001:00000016
0001:00000023 36 0001:00000023
0001:0000003b

Line numbers for R:\VSNET2003\Vc7\lib\MSVCRT.lib(f:\vs70builds\2292\vc
\crtbld\crt\src\atonexit.c) segment .text
81 0001:000001e4 76 0001:000001e4 90 0001:00000209 96 0001:0000020a
95 0001:0000020a 97 0001:0000021b
Line numbers for R:\VSNET2003\Vc7\lib\MSVCRT.lib(f:\vs70builds\2292\vc
\crtbld\crt\src\crtdll.c) segment .text
134
158
172
189
225
234
250
260
266
275
289
294

0001:0000003c
0001:00000052
0001:00000081
0001:000000ab
0001:000000c3
0001:000000f3
0001:00000106
0001:00000124
0001:0000014b
0001:0000016e
0001:00000198
0001:000001bc

129
163
178
192
226
240
252
262
268
283
291
295

0001:0000003c
0001:00000065
0001:0000008b
0001:000000b2
0001:000000cf
0001:000000f4
0001:0000010c
0001:0000012d
0001:0000015a
0001:00000177
0001:0000019b
0001:000001d4

135
168
179
219
234
241
257
263
269
286
292
299

0001:00000044
0001:0000007a
0001:00000090
0001:000000b8
0001:000000e5
0001:000000f7
0001:00000111
0001:00000136
0001:0000015c
0001:00000181
0001:000001a9
0001:000001d6

136
170
184
220
236
249
258
265
272
288
298

0001:0000
0001:0000
0001:0000
0001:0000
0001:0000
0001:0000
0001:0000
0001:0000
0001:0000
0001:0000
0001:0000

004c
007e
009a
00c1
00ec
00fa
011e
0142
015e
018a
01b7

Exports
ordinal
1
2

name

?MapDLLFunction@@YAHXZ (int __cdecl MapDLLFunction(void))
?MapDLLHappyFunc@@YAPADPAD@Z (char * __cdecl MapDLLHappyFunc(cha r *))

Получение информации об исходном файле,
имени функции и номере строки
Алгоритм извлечения данных об исходном файле, имени функции и номере строки
из MAPфайла прост, но для этого нужно выполнить несколько действий в шест
надцатеричной системе счисления. Допустим, ошибка произошла по адресу
0x03901038 в модуле MAPDLL.DLL из листинга 121.
Прежде всего из MAPфайлов проекта нужно выбрать файл, содержащий ад
рес ошибки. Для этого достаточно взглянуть на предпочтительный адрес загруз

ГЛАВА 12

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

451

ки и последний адрес в разделе открытых функций. Если адрес ошибки попадает
в этот диапазон, значит, это и есть нужный вам MAPфайл.
Чтобы определить нужную функцию, найдите в столбце Rva+Base первую функ
цию с адресом, превышающим адрес ошибки. Предыдущий элемент в MAPфайле
и будет функцией, в которой случилась ошибка. В нашем случае это функция
?MapDLLHappyFunc@@YAPADPAD@Z с адресом 0x03901023. Любое имя функции, начина
ющееся с вопросительного знака, — это расширенное имя C++.
Вы, возможно, удивляетесь, почему я не упоминал про расширение имен C++
в главе 6, когда рассказывал про расширения имен для различных соглашений
вызова. Хотя оба типа расширений играют сходную роль, их источники различ
ны. Расширения имен соглашений вызова просто указывают генератору кода, по
каким правилам создавать код занесения параметров в стек и очистки стека, и
основаны на определениях ОС. Расширение имен C++ обусловлено языком. C++
допускает перегрузку методов, поэтому компилятор должен както их различать.
Для этого он «расширяет» имя метода при помощи информации о типе возвра
щаемого значения, соглашении вызова и параметрах. Так компилятор сможет точно
узнать, какую функцию вы хотите вызвать. Для преобразования имени передайте
его в командной строке программе UNDNAME.EXE из Visual Studio .NET. В нашем
примере ?MapDLLHappyFunc@@YAPADPAD@Z преобразуется в char * __cdecl MapDLLHappyFunc(char
*). Некоторые из вас, вероятно, смогли определить, что первоначальным именем
функции было MapDLLHappyFunc, только по ее расширенному имени. Другие расши
ренные имена C++ расшифровать труднее, особенно в случае перегруженных
функций.
Для определения смещения строки нужно выполнить простое шестнадцатерич
ное вычитание по формуле:

(адрес ошибки) – (предпочтительный адрес загрузки) – 0x1000
Помните: адреса представляют собой смещения от начала первого раздела кода,
поэтому нужное преобразование выполняется именно по такой формуле. Вы скорее
всего догадались, зачем вычитается предпочтительный адрес загрузки, но знаете
ли вы, почему нужно вычесть еще и 0x1000? Адрес ошибки — это смещение от
начала раздела кода, но раздел кода не является первой частью двоичного файла.
Первая часть двоичного файла состоит из заголовка PE и связанной с ним заг
лушки DOS, которые занимают 0x1000 байт. Да, все двоичные файлы Win32 по
прежнему вынуждены бороться с тяжелым наследием MSDOS.
Мне неизвестно, почему компоновщик генерирует MAPфайлы, требующие этого
вычисления. Разработчики компоновщика включили столбец Rva+Base в MAPфайл
недавно, поэтому я не понимаю, что им помешало подкорректировать номера строк.
Как только вы подсчитали смещение, ищите в разделе строк наибольшее чис
ло, не превышающее найденное значение. Помните, что при генерировании кода
компилятор может перемешивать его, так что номера строк не всегда располага
ются в восходящем порядке. Вот что получается в моем примере:

0x03901038 – 0x03900000 – 0x1000 = 0x38
Просмотрев листинг 121, вы увидите, что самое большое смещение, не пре
вышающее 0x38, входит в выражение 39 0001:00000033 (строка 39), которое от
носится к файлу MAPDLL.CPP.

452

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

PDB2MAP: создание MAP-файлов постфактум
При обсуждении поиска адресов ошибок меня постоянно спрашивают, что делать,
если значительная часть цикла разработки была пройдена без создания MAPфай
лов. Другие внимательные разработчики также обращают внимание, что для по
лучения отличных MAPфайлов нужно задавать базовые адреса всех DLL при сборке
программы. Работая над проектом, приближающимся к завершению, вы, вероят
но, не захотите дестабилизировать сборку приложения, изменяя некоторые па
раметры. Кроме того, без программы SettingsMaster из главы 9 вносить эти гло
бальные изменения проекта в Visual Studio неудобно, поэтому разработчики обычно
предпочитают задавать базовые адреса своих DLL через REBASE.EXE.
Не оставляя без внимания ни одной интересной задачи, я рассмотрел эту про
блему внимательней. Мне нужен был способ нумерации функций, файлов и строк
исходного кода. Учитывая, что сервер символов DBGHELP.DLL уже включает эти
возможности, следующий шаг на пути к генерированию MAPфайла из PDBфай
ла оказался простым.
Первая проблема, с которой я столкнулся, заключалась в том, что функции
SymGetSymNext и SymGetSymPrev возвращают не то, что вы ожидаете. Я думал, что могу
получить адрес в исходном файле, после чего вызывать SymGetSymPrev до тех пор,
пока не окажусь в начале исходного файла и SymGetSymNext, пока не доберусь до
его конца. Я забыл принять во внимание один небольшой аспект — встраиваемые
функции. Эти функции и соответствующие им строки исходного кода располага
ются в теле других функций, поэтому информация о номерах строк на самом деле
хранится в виде диапазонов. Это означало, что для концентрации данных об ис
ходном коде и номерах строк мне нужно было разработать схему остлеживания
всех диапазонов. Как только я справился с этим препятствием, написать программу
оказалось довольно просто.
Еще одна проблема с серверами символов была связана с библиотекой стан
дартных шаблонов. Сначала я стал разрабатывать структуры данных при помощи
STL, но вскоре обнаружил, что даже частичная реализация PDB2MAP.EXE работа
ла мучительно медленно. Эта ошибка была на моей совести, потому что я исполь
зовал класс вектора для линейного поиска, а это просто глупо. Попробовав не
сколько похожих вариантов, я понял, что STL всегда будет работать гораздо мед
ленней, так как она выполняет много неявных операций выделения памяти и ко
пирования. Поскрипев зубами и попытавшись понять некоторые подробности ре
ализации STL, я осознал, что напрасно усложнял проблему. В конце концов я сам
написал простую систему из нескольких массивов, очень быструю и крайне лег
кую для понимания. Она также оказалась гораздо проще для сопровождения, чем
то, что я мог сделать при помощи STL.
Файлы, генерируемые PDB2MAP, очень похожи на MAPфайлы. Так как сервер
символов DBGHELP.DLL не предоставляет сведений о статических функциях, мне
пришлось от этого отказаться. Увидев файл .P2M, вы поймете, что проблем с его
пониманием у вас не будет. Я счел ненормальную систему нумерации строк MAP
файлов пережитком прошлого, поэтому реализовал в PDB2MAP более простой и
современный подход. Моя информация о строках генерируется на основании
действительных адресов памяти.

ГЛАВА 12

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

453

Наконец, возможно, вас заинтересует содержание файла .P2M. Как я уже гово
рил в главе 2, компактный код — хороший код. Однако узнать влияние различ
ных ключей компилятора на размер отдельных функций можно только по обще
му размеру двоичного файла. Кроме того, мы никак не можем узнать, как на кон
кретную функцию влияет встраивание функций. Работая над PDB2MAP, я понял,
что могу также сообщать размеры символов, потому что эту возможность поддер
живает сервер символов DBGHELP.DLL. Поэтому, как показано в листинге 122,
представляющем собой сокращенный файл .P2M, после заголовочной информа
ции между адресом и именем функции выводится также ее размер. Вы увидите
размеры почти всех функций, однако DBGHELP.DLL не гарантирует, что эта ин
формация будет возвращена, поэтому в файлах .P2M возможно появление разме
ров, равных 0.

Листинг 12-2.

Сокращенный файл .P2M

PDB2MAP Generated Map File
Image: AssertTest
Timestamp is 3E0E7E2A > Sat Dec 28 23:46:34 2002
Preferred load address is 00400000
Address
Size Function
0x00401050
36 ??2@YAPAXI@Z
0x00401080
260 ?MyThread@@YGKPAX@Z
0x00401190
38 ?SleepThread@@YGKPAX@Z
0x004011C0
535 ?TestThree@@YAXPAD@Z
0x004013E0
258 ?TestTwo@@YAXXZ
0x004014F0
421 ?TestOne@@YAXPAG@Z
0x004016A0
453 _wWinMain@16
0x00401A5E
6 _InitCommonControls@0
0x00401A64
6 _SuperAssertionW
. . .
Line numbers for
d:\dev\booktwo\disk\bugslayerutil\tests\asserttest\asserttest.cpp
16
21
27
33
43
47
53
60
64
74
81
92
97

:
:
:
:
:
:
:
:
:
:
:
:
:

0x00401080
0x004010E5
0x00401194
0x004011D7
0x00401223
0x0040131A
0x004013E0
0x0040143A
0x004014A5
0x0040153E
0x00401638
0x0040171B
0x00401838

18
22
28
39
44
48
55
61
67
76
82
93
98

:
:
:
:
:
:
:
:
:
:
:
:
:

0x0040109F
0x0040113C
0x004011A8
0x004011DE
0x0040127A
0x0040131F
0x004013F7
0x0040143C
0x004014F0
0x00401548
0x0040163D
0x00401772
0x00401845

19
23
29
40
45
49
57
62
68
78
90
94
99

:
:
:
:
:
:
:
:
:
:
:
:
:

0x004010CB
0x0040113E
0x004011AA
0x00401201
0x0040129D
0x00401334
0x0040140F
0x0040143E
0x00401515
0x004015CF
0x004016A0
0x004017D2
0x00401852

20
26
32
41
46
50
59
63
70
80
91
96
100

:
:
:
:
:
:
:
:
:
:
:
:
:

0x004
0x004
0x004
0x004
0x004
0x004
0x004
0x004
0x004
0x004
0x004
0x004
0x004

010DE
01190
011C0
01207
012B2
0139C
01427
01498
01527
01632
016C4
01829
01854

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

454

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Line numbers for f:\vs70builds\2292\vc\crtbld\crt\src\atonexit.c
76 : 0x00402810
96 : 0x00402853


81 : 0x00402814
97 : 0x00402866

90 : 0x0040284B

95 : 0x004 02850

Использование CrashFinder
Как вы только что увидели, читать MAP и P2Mфайлы не так уж и сложно. Одна
ко это довольно утомительное и определенно немасштабируемое решение для
других членов вашей группы, таких как сотрудники отдела контроля качества,
техподдержки и даже руководители. Для улучшения масштабируемости CrashFinder
я решил включить в отчеты об ошибках максимально подробную информацию,
чтобы сделать его полезным для всех членов группы разработки: от программис
тов до тестировщиков и инженеров по сопровождению программы. Если вы сле
дуете моим советам и правильно создаете символы отладки (см. главу 2), все чле
ны вашей группы смогут работать с CrashFinder без всяких проблем.
При использовании CrashFinder в группе вам нужно быть особенно вниматель
ным к поддержанию доступа к двоичным образам и связанным с ними PDBфай
лам, поскольку CrashFinder не хранит информации о вашем приложении, кроме
путей к двоичным файлам. Это позволяет вам использовать один проект CrashFinder
на всем протяжении цикла разработки. Если бы CrashFinder хранил более подроб
ные сведения о вашем приложении, такие как таблицы символов, то вам, вероят
но, нужно было бы создавать отдельный проект CrashFinder для каждой компо
новки. Если вы примете мой совет и обеспечите легкий доступ к двоичным фай
лам и PDBфайлам, то при возникновении ошибки тестировщики и члены груп
пы сопровождения должны будут только запустить CrashFinder и добавить важ
ную информацию в отчет об ошибке. Все мы знаем, что чем больше мы знаем о
конкретной проблеме, тем проще будет ее решение.
Для некоторых приложений вам, наверное, захочется создать несколько про
ектов CrashFinder. Скажем, вы могли бы иметь один проект, указывающий на ме
сто ежедневной компоновки, а также проекты для каждой версии программы,
соответствующей контрольной точке. Если вы решите включить в проект Crash
Finder системные DLL, вам нужно будет создать отдельные проекты для каждой
поддерживаемой вами ОС. Вам также понадобятся отдельные проекты CrashFinder
для всех версий программы, отсылаемых тестировщикам за пределы группы раз
работки, поэтому для каждой такой версии нужно будет отдельно хранить двоич
ные образы и PDBфайлы.
Работа над CrashFinder оказалась весьма интересной. Говорят, он значительно
облегчает труд, и я очень горжусь этим. Более того, целым рядом усовершенство
ваний пользовательского интерфейса и функциональности CrashFinder обязан не
мне, а другим талантливым программистам. К версии CrashFinder, которую вы
найдете на CD, приложили руку Скотт Блюм (Scott Bloom), Чинг Минг Квок (Ching
Ming Kwok), Джефф Шанхольтц (Jeff Shanholtz), Рич Петерс (Rich Peters), Пабло
Преседо (Pablo Presedo), Джулиан Онионс (Julian Onions) и Кен Глэдстоун (Ken
Gladstone). Всем им я очень признателен.

ГЛАВА 12

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

455

На рис. 124 показан пользовательский интерфейс CrashFinder с одним из моих
личных проектов. В верхней части дочернего окна — древовидный список, ото
бражающий исполняемый файл и его DLL. Зеленые галочки говорят о том, что
символы для каждого двоичного образа были загружены правильно. Если бы Crash
Finder не смог загрузить символы, он указал бы на это при помощи красной бук
вы X и развернул проблемный элемент списка, поясняя причину проблемы.

Рис. 124.

Пользовательский интерфейс CrashFinder

Красный X появляется по трем причинам. Вопервых, это может означать, что
CrashFinder не нашел PDBфайла, соответствующего двоичному файлу. Лучше всего
хранить двоичные и PDBфайлы в одном месте — тогда проблем быть не должно.
Вторая причина в том, что при открытии сохраненного проекта CrashFinder больше
не может найти двоичный файл. Наконец, проблема может быть обусловлена
конфликтом адреса загрузки файла с какойлибо из DLL проекта. ОС не допуска
ет возможность конфликта DLL, поэтому CrashFinder также этого не позволяет. При
конфликте загрузки вы можете изменить адрес конфликтующей DLL только для
текущего проекта CrashFinder. Как я уже говорил, модификация базовых адресов
DLL очень важна для успешного поиска ошибок.
Вся магия преобразования мистического адреса в информацию об исходном
файле, функции и номере строки отображается в нижней части дочернего окна.
Однако сначала я должен рассказать, как загрузить двоичные файлы в проект
CrashFinder. Нажав на панели инструментов кнопку New, вы увидите стандартное
диалоговое окно работы с файлами, предлагающее добавить в проект двоичный
образ. Если у вас есть EXEфайл, его нужно добавить в проект в первую очередь.
В диалоговом окне Add Binary Image (добавить двоичный образ) вы можете выб
рать несколько двоичных файлов, включив тем самым в CrashFinder сразу весь свой
проект.

456

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

После выбора пункта Open (открыть) происходит много операций. В новую
версию CrashFinder внесено одно отличное усовершенствование: теперь он авто
матически ищет все неявно загружаемые DLL и добавляет их в проект. Если вы явно
загрузили DLL как объекты COM, добавить их можно, выбрав в меню Edit (изме
нить) пункт Add Image (добавить образ). CrashFinder также добавит в проект все
дополнительные неявно скомпонованные модули.
Добавляя в проект двоичные образы, помните: проект CrashFinder может вклю
чать только один EXEфайл. Если приложение состоит из нескольких EXEфай
лов, создайте для каждого отдельный проект CrashFinder. CrashFinder — приложе
ние с многодокументным интерфейсом (multipledocument interface, MDI), поэтому
вы легко сможете открыть все проекты для всех EXEфайлов. Когда вы добавляете
в проект DLL, CrashFinder проверяет, не конфликтует ли ее адрес загрузки с про
чими DLL проекта. Обнаружив конфликт, CrashFinder позволит изменить адрес заг
рузки конфликтующей DLL только для текущего проекта. Это очень удобно, когда
у вас есть проект CrashFinder для отладочной компоновки и вы случайно забыли
модифицировать базовый адрес своих DLL.
По мере развития приложения вы можете удалять двоичные образы из проек
та командой Remove Image (удалить образ) из меню Edit. Кроме того, вы всегда
можете изменить адрес загрузки двоичного образа, выбрав пункт Image Properties
(свойства образа) из меню Edit. Так как CrashFinder автоматически добавляет в
проект системные DLL, нужные вашим двоичным файлам, вы сможете облегчить
отладку, если используете сервер символов (см. главу 2). Теперь у вас есть еще более
веская причина установки сервера символов: благодаря его поддержке Crash
Finder’ом вы сможете изучать ошибки даже в системных модулях. Просмотрев
информацию, изображенную для VERSION.DLL, вы увидите, что она загружает
символы из моего сервера символов.
Назначение CrashFinder заключается в преобразовании адреса ошибки в ин
формацию об исходном файле, имени функции и номере строки. Интересующие
вас адреса нужно вводить в поле Hexadecimal Address(es) (шестнадцатеричные
адреса) — в нижней половине дочернего окна. Когда вы нажмете на кнопку Find
(найти), исходный файл, имя функции и номер строки появятся в поле вывода в
низу окна. При желании вы можете ввести несколько адресов, разделив их про
белами или запятыми. Так, вы можете ввести полный список адресов стека вызо
вов из журнала Dr. Watson, получив сразу все нужные сведения.
По умолчанию CrashFinder не показывает смещение адреса от начала функции
или от строки исходного кода. Чтобы увидеть эти сведения, укажите это в диало
говом окне Options (свойства). Смещение от начала функции показывает, на сколько
байт адрес отстоит от начала функции. Смещение от строки говорит, на сколько
байт отстоит адрес от начала ближайшей строки исходного кода. Помните, что
одной строке исходного кода могут соответствовать много ассемблерных команд,
особенно если вы используете вызовы функций как параметры. Работая с Crash
Finder, вы не сможете просмотреть адрес, не являющийся корректным адресом
команды. Скажем, при неправильном обращении с указателем this вы можете
столкнуться с ошибкой по такому адресу, как 0x00000001. К счастью, эти типы
ошибок не так часты, как обычные ошибки нарушения доступа к памяти, кото
рые CrashFinder находит с легкостью.

ГЛАВА 12

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

457

Некоторые сведения о реализации
CrashFinder — простое приложение, написанное при помощи библиотеки MFC,
поэтому большая его часть должна быть вам понятной. Тем не менее, чтобы вам
было легче реализовать функции, которые я предлагаю в разделе «Что после
CrashFinder?», мне хотелось бы пояснить три главных аспекта этой программы:
работу сервера символов, функцию, в которой выполняется основной объем ра
боты, и архитектуру данных CrashFinder.
CrashFinder использует сервер символов DBGHELP.DLL (см. главу 4). Я только
хочу обратить ваше внимание на то, что загрузка всей информации об исходных
файлах и номерах строк выполняется явно, путем передачи флага SYMOPT_LOAD_LINES
в функцию SymSetOptions. По умолчанию сервер символов DBGHELP.DLL не загру
жает информацию об исходных файлах и номерах строк.
Почти все операции CrashFinder обеспечивает класс документа, CCrashFinderDoc.
Он содержит класс CSymbolEngine, выполняет весь просмотр символов и контро
лирует представление данных. Ключевая функция CrashFinder, CCrashFinderDoc::Load
AndShowImage, показана в листинге 123. Именно в ней осуществляется проверка
корректности двоичного образа, его сравнение с существующими элементами
проекта для предотвращения конфликтов адресов загрузки, загрузка символов и
включение образа в конец дерева. Эта функция вызывается и при добавлении образа
в проект, и при открытии проекта. Выполнение всех этих задач в функции CCrash
FinderDoc::LoadAndShowImage позволило мне сосредоточить базовую логику CrashFinder
в одном месте и хранить в проекте только имена двоичных образов вместо ко
пий таблицы символов.

Листинг 12-3.

Функция CCrashFinderDoc::LoadAndShowImage

BOOL CCrashFinderDoc :: LoadAndShowImage ( CBinaryImage * pImage
,
BOOL
bModifiesDoc ,
BOOL
bIgnoreDups )
{
// Проверка данных, внешних по отношению к этой функции.
ASSERT ( this ) ;
ASSERT ( NULL != m_pcTreeControl ) ;
// Строка для вывода информационных сообщений.
CString sMsg
;
// Состояние отображения дерева.
int
iState = STATE_NOTVALID ;
// Возвращаемое значение типа BOOL
BOOL
bRet
;
// Проверка правильности параметра.
ASSERT ( NULL != pImage ) ;
if ( NULL == pImage )
{
// Недопустимое значение указателя.
return ( FALSE ) ;
}
см. след. стр.

458

ЧАСТЬ IV

//
//
//
//
//
//
if
{

Мощные средства и методы отладки неуправляемого кода

Проверка корректности образа. Если он корректен, я проверяю,
не включен ли он уже в список и не конфликтует ли он с другими
адресами загрузки. Если образ некорректен, я все равно добавляю
его в список, потому что игнорировать данные пользователя нельзя.
Если образ плохой, я просто отображаю его с соответствующим значком
и не загружаю его в символьную машину.
( TRUE == pImage>IsValidImage ( ) )

//
//
//
//
//
//
//
//
//
//

В этом блоке выполняется просмотр элементов массива данных,
при этом осуществляется поиск следующих проблем.
1. Двоичный образ уже находится в списке. Если это так,
я могу только прервать операцию.
2. Двоичный файл хочет загрузиться по адресу, который уже
имеется в списке. В этом случае я вывожу для двоичного
образа окно Properties, чтобы его адрес загрузки можно
было изменить.
3. Проект уже включает образ EXE, и pImage также
указывает на исполняемый файл.

// Я всегда исхожу из предположения, что данные, на которые
// указывает pImage, корректны. Я — оптимист!
BOOL bValid = TRUE ;
INT_PTR iCount = m_cDataArray.GetSize ( ) ;
for ( INT_PTR i = 0 ; i < iCount ; i++ )
{
CBinaryImage * pTemp = (CBinaryImage *)m_cDataArray[ i ] ;
ASSERT ( NULL != pTemp ) ;
if ( NULL == pTemp )
{
// Недопустимое значение указателя!
return ( FALSE ) ;
}
// Совпадают ли имена образов (два значения CString)?
if ( pImage>GetFullName ( ) == pTemp>GetFullName ( ) )
{
if ( FALSE == bIgnoreDups )
{
// Сообщить об этом пользователю!!
sMsg.FormatMessage ( IDS_DUPLICATEFILE
,
pTemp>GetFullName ( ) ) ;
AfxMessageBox ( sMsg ) ;
}
return ( FALSE ) ;
}
// Если текущий образ из структуры данных некорректен,
// у меня неприятности. Выше я смог проверить совпадение

ГЛАВА 12

//
//
//
//
//
//
if
{

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

459

имен образов, однако конфликт адресов загрузки и попытку
добавления двух EXEфайлов проверить труднее. Если pTemp
некорректен, я должен пропустить эти проверки. Это может
привести к проблемам, но pTemp будет отмечен в списке как
некорректный, поэтому пользователь сможет сам переназначить
соответствующие свойства.
( TRUE == pTemp>IsValidImage ( FALSE ) )

// Проверка, позволяющая исключить
// добавление в проект двух EXEфайлов.
if ( 0 == ( IMAGE_FILE_DLL &
pTemp>GetCharacteristics ( ) ) )
{
if ( 0 == ( IMAGE_FILE_DLL &
pImage>GetCharacteristics ( ) ) )
{
// Сообщить пользователю!!
sMsg.FormatMessage ( IDS_EXEALREADYINPROJECT ,
pImage>GetFullName ( ) ,
pTemp >GetFullName ( ) ) ;
AfxMessageBox ( sMsg ) ;
// Попытка загрузки двух EXEобразов приводит
// к автоматическому отбрасыванию данных
// для соответствующего pImage.
return ( FALSE ) ;
}
}
// Проверка конфликтов адресов загрузки.
if ( pImage>GetLoadAddress ( ) ==
pTemp>GetLoadAddress( )
)
{
sMsg.FormatMessage ( IDS_DUPLICATELOADADDR
pImage>GetFullName ( )
pTemp >GetFullName ( )

,
,
) ;

if ( IDYES == AfxMessageBox ( sMsg , MB_YESNO ) )
{
// Пользователь желает изменить
// свойства вручную.
pImage>SetProperties ( ) ;
// Проверка того, что адрес загрузки на самом
// деле был изменен и уже не конфликтует
// с другими двоичными образами.
int iIndex ;
if ( TRUE ==
IsConflictingLoadAddress (
см. след. стр.

460

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

pImage>GetLoadAddress(),
iIndex
))
{
sMsg.FormatMessage
( IDS_DUPLICATELOADADDRFINAL ,
pImage >GetFullName ( )
,
((CBinaryImage*)m_cDataArray[iIndex])>GetFullName());
AfxMessageBox ( sMsg ) ;
// Данные pImage некорректны, поэтому
// выполняется выход из цикла.
bValid = FALSE ;
break ;
}
}
else
{
// Данные pImage некорректны, поэтому
// выполняется выход из цикла.
bValid = FALSE ;
pImage>SetBinaryError ( eAddressConflict ) ;
break ;
}
}
}
}
if ( TRUE == bValid )
{
// Этот образ в порядке (по крайней мере до загрузки символов).
iState = STATE_VALIDATED ;
}
else
{
iState = STATE_NOTVALID ;
}
}
else
{
// Этот образ некорректен.
iState = STATE_NOTVALID ;
}
if ( STATE_VALIDATED == iState )
{
bRet = (BOOL)
m_cSymEng.SymLoadModule64 ( NULL
,
(PWSTR)pImage>
GetFullNameString ( ) ,
NULL
,
pImage>GetLoadAddress ( )
,

ГЛАВА 12

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

461

0
);
// Внимание! SymLoadModule возвращает
// адрес загрузки образа, а не TRUE.
ASSERT ( FALSE != bRet ) ;
if ( FALSE == bRet )
{
TRACE ( "m_cSymEng.SymLoadModule failed!!\n" ) ;
iState = STATE_NOTVALID ;
}
else
{
CImageHlp_Module cModInfo ;
BOOL bRet =
m_cSymEng.SymGetModuleInfo64(pImage>GetLoadAddress(),
&cModInfo
);
ASSERT ( TRUE == bRet ) ;
if ( TRUE == bRet )
{
// Проверка того, что тип символа не равен SymNone.
if ( SymNone != cModInfo.SymType )
{
iState = STATE_VALIDATED ;
// Задание информации о символах образа.
pImage>SetSymbolInformation ( cModInfo ) ;
}
else
{
iState = STATE_NOTVALID ;
// Выгрузка модуля. Символьная машина загружает
// модуль даже без символов, поэтому я должен
// выгрузить его. Я хочу, чтобы загружались
// только достойные этого модули.
m_cSymEng.SymUnloadModule64(
pImage>GetLoadAddress());
pImage>SetBinaryError ( eNoSymbolsAtAll ) ;
}
}
else
{
iState = STATE_NOTVALID ;
}
}
}
// Для данного pImage устанавливается признак
// состояния загрузки символов.
if ( STATE_VALIDATED == iState )
{
pImage>SetExtraData ( TRUE ) ;
}
см. след. стр.

462

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

else
{
pImage>SetExtraData ( FALSE ) ;
}
// Элемент помещается в массив.
m_cDataArray.Add ( pImage ) ;
// Изменился ли документ в результате добавления элемента?
if ( TRUE == bModifiesDoc )
{
SetModifiedFlag ( ) ;
}
// Образ помещается в дерево.
bRet = m_cTreeDisplay.InsertImageInTree ( pImage ,
iState ) ;
ASSERT ( bRet ) ;
// Все OK!!
return ( bRet ) ;
}
Наконец, два слова об архитектуре данных CrashFinder. Основная структура дан
ных CrashFinder — простой массив объектов класса CBinaryImage. Объект CBinaryImage
соответствует одному двоичному образу и содержит базовую информацию о нем,
такую как адрес загрузки, свойства и имя. При добавлении в проект двоичного
образа документ помещает объект CBinaryImage в основной массив данных и со
храняет соответствующий указатель в слоте данных узла дерева. При выборе
элемента дерева оно возвращает его указатель документу, который получает CBinary
Image и просматривает его символьную информацию.

Что после CrashFinder?
Первая версия CrashFinder поддерживала все описанные функции, но была недо
статочно удобной в использовании, что было исправлено мной и упомянутыми
выше людьми с учетом многих пожеланий. Тем не менее CrashFinder всегда мож
но усовершенствовать. Если вы хотите лучше разобраться в образах двоичных фай
лов, попытайтесь добавить в CrashFinder какиенибудь из следующих функций.
쐽 Реализуйте поддержку двоичных файлов ОС и сделайте так, чтобы CrashFinder
автоматически переключался между ними. Это повысит эффективность поис
ка ошибок в коде ОС. В настоящий момент CrashFinder работает только с сис
темными DLL, установленными на компьютере пользователя.
쐽 Выводите в древовидном списке более подробную информацию для каждого
двоичного файла. Класс CBinaryImage позволяет сделать это при помощи мето
да GetAdditionalInfo. Вы можете добавить функцию вывода информации о за
головке, импортируемых и экспортируемых функциях.

ГЛАВА 12

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

463

쐽 Реализуйте возможность вставки (pasting) списков DLL, чтобы их можно было
добавлять в проект автоматически. В окне отладчика Output выводится спи
сок всех загружаемых приложением DLL, поэтому было бы удобно, если б
CrashFinder позволял вставлять этот текст и умел отыскивать в нем имена DLL.
쐽 Добавьте в CrashFinder поддержку аварийных дампов памяти. Тогда CrashFinder
мог бы сопоставлять ошибку с аварийным дампом и определять все ее обсто
ятельства.

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

Г Л А В А

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

Н

и для кого не секрет, что пользователи ненавидят диалоговое окно сообщения
об ошибке, появляющееся при аварийном завершении приложения. Если вы чи
таете эту книгу, это означает, что вы делаете все возможное для профилактики
ошибок. Однако все мы знаем, что ошибки происходят даже в самых лучших про
граммах, и к ним нужно быть готовым.
Как было бы хорошо, если бы вместо раздражающего пользователей сообще
ния об ошибке появлялось дружественное к ним окно, которое описывало бы
проблему и спрашивало его о том, что он делал в момент ошибки! А еще лучше,
если б это любезное и вежливое окно записывало не только обычную информа
цию об адресе ошибки и стеке вызовов, предоставляемую такими утилитами, как
Dr. Watson, но и внутреннее состояние приложения, позволяющее узнать состоя
ние выполнения программы и ее данных в момент ошибки! И разве не было бы
уж совсем великолепно, если б диалоговое окно автоматически отсылало вам
информацию об ошибке по электронной почте и сохраняло отчет об ошибке в
вашей системе отслеживания ошибок?
Обработчики ошибок могут сделать эти мечты реальностью, обеспечив вас всей
этой полезной информацией. Обработчиками ошибок я называю и обработчики
исключений, и фильтры необработанных исключений. Если вы работали с язы
ком C++, обработчики исключений должны быть вам известны. Вероятно, вам хуже
знакомы фильтры необработанных исключений, которые представляют собой
интересные функции, позволяющие получить контроль прямо перед появлением
отвратительного окна с сообщением об ошибке. Обработчики исключений име
ются только в C++, в то время как фильтры необработанных исключений можно
использовать и в C, и в C++.
В этой главе я приведу код, который вы можете включать в свои приложения
для получения такой информации, как значения регистров и стеки вызовов. Кро

ГЛАВА 13

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

465

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

Структурная обработка исключений
против обработки исключений C++
Изучение обработки исключений осложняется тем, что C++ поддерживает два
основных ее типа: структурную обработку исключений (structured exception han
dling, SEH), предоставляемую ОС, и обработку исключений C++, входящую в со
став языка. Многие считают оба типа обработки исключений одинаковыми. Могу
вас заверить, что в основе этих двух типов обработки исключений лежат совер
шенно разные подходы. Общая их черта в том, что исключения обоих типов пред
назначены для применения в исключительных ситуациях, а не при нормальном
выполнении программы. Помоему, многих людей путают слухи, что оба типа
исключений можно комбинировать. Ниже я коснусь различий и сходств между
этими типами обработки исключений, а также расскажу про то, как избежать
крупнейшего источника связанных с ними ошибок.

Структурная обработка исключений
SEH обеспечивается ОС и предназначена для непосредственной обработки таких
ошибок, как нарушение доступа. SEH не привязана к конкретному языку; в про
граммах C и C++ она обычно реализуется в виде пар __try/__except и __try/__finally.
Использование пары __try/__except заключается в размещении некоторого кода
внутри блока __try и описании обработки возникающих в нем исключений в блоке
__except (также называемом обработчиком исключений). Входящий в состав пары
__try/__finally блок __finally (его еще называют обработчиком завершения) вы
полняется при выходе из функции всегда, даже при преждевременном заверше
нии блока __try, что позволяет гарантировать освобождение ресурсов.
Типичная функция с SEH представлена в листинге 131. Блок __except выгля
дит почти так же, как вызов функции, только в его скобках указано значение спе
циального выражения, называемого фильтром исключений. Фильтр исключений
имеет значение EXCEPTION_EXECUTE_HANDLER, которое указывает на то, что блок __except
должен выполняться каждый раз при возникновении любого исключения в бло
ке __try. Возможны еще два значения фильтра исключений: EXCEPTION_CONTINUE_EXE
CUTION позволяет проигнорировать исключение, а EXCEPTION_CONTINUE_SEARCH пере
дает исключение по цепи вызовов следующему блоку __except. Вы можете делать
фильтр исключений сколь угодно простым или сложным, обрабатывая только те
исключения, которые желаете.

466

ЧАСТЬ IV

Листинг 13-1.

Мощные средства и методы отладки неуправляемого кода

Пример обработчика SEH

void Foo ( void )
{
__try
{
__try
{
// Какиенибудь действия.
}
__except ( EXCEPTION_EXECUTE_HANDLER )
{
// Этот блок будет выполнен, если код в блоке __try
// вызовет нарушение доступа или другую ошибку.
// Блок __except называют также обработчиком исключений.
}
}
__finally
{
// Этот блок будет выполнен независимо от того,
// вызвала ли функция ошибку. Выполняйте здесь
// все действия, необходимые для освобождения ресурсов.
}
}
Процесс поиска и выполнения обработчика исключения иногда называют
развертыванием (unwinding) исключения. Обработчики исключений хранятся во
внутреннем стеке; по мере роста цепи вызовов функций в этот внутренний стек
помещаются обработчики исключений (если они есть) всех новых функций. При
возникновении исключения ОС находит стек обработчиков исключений данно
го потока и начинает вызывать их, пока какойнибудь из них не согласится обра
ботать данное исключение. По мере прохождения исключения через стек обра
ботчиков ОС очищает стек вызовов и выполняет все обработчики завершений,
которые ей при этом встречаются. Если развертывание достигает конца стека
обработчиков исключений, появляется окно сообщения об ошибке или загружа
ется JITотладчик.
Обработчик исключений может определить значение исключения посредством
специальной функции GetExceptionCode, которая может вызываться только в фильтрах
исключений. При работе над математической программой вы, например, могли
бы установить обработчик исключений, который обрабатывал бы попытки деле
ния на 0 и возвращал значение NaN (not a number — не число). Пример такого об
работчика см. в листинге 132. Фильтр исключений вызывает GetExceptionCode, и,
если исключение вызвано делением на 0, выполняется обработчик. Любому дру
гому исключению соответствует значение EXCEPTION_CONTINUE_SEARCH, приказывающее
ОС выполнить следующий блок __except в цепи вызовов.

ГЛАВА 13

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

467

Листинг 13-2. Пример обработчика SEH, включающего обработку
фильтра исключений
""""long IntegerDivide ( long x , long y )
{
long lRet ;
__try
{
lRet = x / y ;
}
__except ( EXCEPTION_INT_DIVIDE_BY_ZERO ==
GetExceptionCode ( )
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH
)
{
lRet = NaN ;
}
return ( lRet ) ;
}
Фильтром исключений может быть даже вызов вашей собственной функции,
если только она определяет обработку исключения, возвращая одно из коррект
ных значений фильтра исключений. Кроме специальной функции GetExceptionCode,
в выражении фильтра исключений можно вызывать функцию GetExceptionInfor
mation. Она возвращает указатель на структуру EXCEPTION_POINTERS, которая полно
стью описывает причину ошибки и состояние процессора в момент ее возник
новения. Как вы уже догадались, структуру EXCEPTION_POINTERS я использую ниже.
Возможности SEH не ограничиваются обработкой ошибок. Вы можете опре
делять собственные исключения, используя APIфункцию RaiseException. Большин
ство программистов ее не применяет, хотя она обеспечивает один из способов
быстрого выхода из глубоко вложенных условных выражений. Этот способ бо
лее корректен, чем старый метод, основанный на использовании функций стан
дартной библиотеки setjmp и longjmp.
Чтобы грамотно работать с SEH, вам нужно знать о двух ее ограничениях. Первое
не очень существенно: число кодов ваших ошибок ограничено одним беззнако
вым целым. Вторая проблема серьезнее: SEH плохо сочетается с C++, потому что
исключения C++ на самом деле реализованы при помощи SEH, и беспорядочное
комбинирование обоих типов исключений вызывает недовольство компилятора.
Причина конфликта в том, что при развертывании SEH деструкторы объектов C++,
созданных в стеке, не вызываются. В конструкторах объектов C++ часто выпол
няется самая разнообразная инициализация, например, выделение памяти для
внутренних структур данных, поэтому пропуск деструкторов может приводить к
утечкам памяти и другим проблемам.
Если вы хотите лучше изучить основы SEH, то, кроме Microsoft Developer Network
(MSDN), я рекомендую еще два источника. Самый лучший обзор SEH можно най
ти в книге Джеффри Рихтера (Jeffrey Richter. Programming Applications for Microsoft

468

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Windows. — Microsoft Press, 1999). Если вас интересует действительная реализа
ция SEH, прочитайте статью Мэтта Питрека (Matt Pietrek) «A Crash Course on the
Depths of Win32 Structured Exception Handling» («Тонкости структурной обработ
ки исключений Win32») в «Microsoft Systems Journal» в январе 1997 г.
Я хочу также упомянуть про одно усовершенствование SEH, появившееся в
Microsoft Windows XP/Server 2003, — векторную обработку исключений. При обыч
ной SEH способа глобального уведомления об исключениях нет. Обычно в повсед
невной работе векторная обработка исключений не нужна, однако она предо
ставляет одну очень полезную возможность — получение первого и последнего
уведомления обо всех SEHисключениях приложения. Как только я узнал, что
Microsoft добавила в ОС векторную обработку исключений, у меня сразу же со
зрел план программы мониторинга SEHисключений, позволяющей следить за
генерируемыми приложением исключениями, не запуская его под управлением
отладчика.
Для получения векторных исключений просто вызовите функцию AddVectored
ExceptionHandler, вторым параметром которой является указатель на функцию,
которая будет вызываться при возникновении в вашей программе любого перво
го случая исключения (firstchance exception). Первый параметр — булево значе
ние, показывающее, как вы хотите получать уведомления: до или после нормаль
ного развертывания цепи исключений. При исключении ваша функция обратно
го вызова получит указатель на структуру EXCEPTION_POINTERS, описывающую исклю
чение. Как вы уже поняли, эта информация делает получение исключений триви
альным.
На CD вы найдете проект XPExceptMon, иллюстрирующий использование век
торных исключений, записывая все исключения, возникающие в вашей програм
ме. Вся работа по установке и удалению ловушки векторных исключений выпол
няется в функции DllMain библиотеки XPExceptMon.DLL, поэтому задействовать ее
в своих приложениях вам будет очень просто. Я хотел просто продемонстриро
вать работу с векторными исключениями, поэтому все, что делает XPExceptMon,
заключается в записи типа и адреса исключения в текстовый файл. Чтобы попрак
тиковаться с сервером символов DBGHELP.DLL, можете добавить в XPExceptMon
просмотр функций и анализ стека.
Если вам хотелось бы получать уведомления об исключениях для более ран
них версий Windows, вам повезло. Юджин Гершник (Eugene Gershnik) написал
отличную статью «Visual C++ ExceptionHandling Instrumentation» («Обработка ис
ключений в Visual C++»), опубликованную в декабрьском номере «Windows Develo
per Magazine» за 2002 год. Юджин не только показывает, как устанавливать ловушки
для исключений, но и отлично описывает саму обработку исключений.

Обработка исключений C++
Обработка исключений C++ входит в состав языка C++, поэтому большинству
программистов она скорее всего известна лучше, чем SEH. Для обработки исклю
чений C++ используются ключевые слова try и catch. Ключевое слово throw позволяет
инициировать развертывание исключений. В то время как число кодов ошибок
SEH ограничено одним беззнаковым целым, обработчик catch языка C++ может

ГЛАВА 13

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

469

перехватывать любые типы переменных, включая классы. Если вы выполните
наследование классов обработки ошибок от общего базового класса, вы сможете
обрабатывать почти любые ошибки. Именно такой иерархический подход к об
работке ошибок реализован в библиотеке Microsoft Foundation Class (MFC) с ба
зовым классом CException. Обработка исключений C++ показана в листинге 133,
где выполняется чтение объекта класса CFile библиотеки MFC.

Листинг 13-3.

Пример обработчика исключений C++

BOOL ReadFileHeader ( CFile * pFile , LPHEADERINFO pHeader )
{
ASSERT ( FALSE == IsBadReadPtr ( pFile , sizeof ( CFile * ) ) ) ;
ASSERT ( FALSE == IsBadReadPtr ( pHeader ,
sizeof ( LPHEADERINFO ) ) ) ;
if ( ( TRUE == IsBadReadPtr ( pFile , sizeof ( CFile * ) ) ) ||
( TRUE == IsBadReadPtr ( pHeader ,
sizeof ( LPHEADERINFO )
) ) )
{
return ( FALSE ) ;
}
BOOL bRet ;
try
{
pFile>Read ( pHeader , sizeof ( HEADERINFO ) ) ;
bRet = TRUE ;
}
catch ( CFileException * e )
{
// Если заголовочный файл не был прочитан изза обнаружения
// преждевременного конца файла, исключение обрабатывается,
// в противном случае развертывание продолжается.
if ( CFileException::endOfFile == e>m_cause )
{
e>Delete();
bRet = false;
}
else
{
// Само по себе ключевое слово throw генерирует то же
// исключение, что было передано в этот блок catch.
throw ;
}
}
return ( bRet ) ;
}
Обработка исключений C++ имеет некоторые недостатки, о которых следует
помнить. Вопервых, ошибки вашей программы не обрабатываются автоматически.
Вовторых, обработка исключений C++ не так уж и грациозна. Даже если ваша
программа никогда не генерирует исключений, компилятор проделает много

470

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

работы, устанавливая и удаляя блоки try и catch, что может оказаться слишком на
кладно при высоких требованиях к быстродействию программы. Если вы новичок
в обработке исключений C++, для начала их изучения прекрасно подойдет MSDN.

Избегайте использования обработки исключений C++
Обработка исключений C++ — один из самых частых вопросов, с которыми я
сталкиваюсь как консультант. При разработке Windowsприложений программи
сты тратят на решение проблем с исключениями C++ больше времени, чем на все
прочие ошибки (за исключением искажений памяти). Опираясь на большой опыт
разрешения многих ужасных ситуаций, я рекомендую избегать обработки исклю
чений C++, потому что это бесконечно облегчит вашу жизнь и упростит отладку
программ.
Первая проблема обработки исключений C++ в том, что ее нельзя считать
чистым свойством языка. Очень часто она кажется чужеродной и незавершенной.
Отсутствие стандартного класса ANSI, содержащего информацию об исключении,
означает, что у нас нет согласованного способа обработки общих ошибок. Кое
кто из вас, возможно, считает, что стандартным механизмом для обработки всех
исключений является конструкция catch (...), но ниже вы увидите, насколько она
небезопасна.
Обработка исключений C++ принадлежит к числу технологий, великолепных
с теоретической точки зрения, но приводящих к проблемам, если вы пишете что
нибудь посерьезнее, чем «Hello World!». Я часто сталкиваюсь с абсолютно безум
ными ситуациями, когда один из членов группы так влюбляется в исключения C++,
что начиняет ими свой код в невообразимом количестве. Это заставляет работать
с исключениями C++ и всех остальных членов группы, даже если далеко не все из
них до конца понимают тонкости проектирования и использования исключений.
При этом ктолибо из программистов неизбежно забывает про перехват какого
нибудь случайного неожиданного исключения, и приложение терпит крах. Кро
ме того, программы, в которых для сообщения об ошибках используются и воз
вращаемые значения, и исключения C++, практически не поддаются модерниза
ции и сопровождению, вследствие чего многие компании предпочитают отказаться
от существующего кода и начать разработку программы с самого начала, значи
тельно увеличивая расходы.
Многим программистам исключения C++ нравятся тем, что они позволяют
отказаться от проверки возвращаемых из функций значений. Однако этот аргу
мент не только ошибочен, но и служит оправданием плохого стиля программи
рования. Если у одного из членов вашей группы постоянно возникают проблемы
с проверкой возвращаемых значений, проведите соответствующую консультацию.
Если и после этого он не будет выполнять проверку возвращаемых значений, смело
увольняйте его — он просто отлынивает от работы.
До этого момента я обсуждал вопросы проектирования исключений C++ и
управления ими. Но не стоит также забывать, что с ними связано много дополни
тельных затрат. Для создания блоков try и catch нужно проделать большую рабо
ту, что заметно ухудшает быстродействие программы, даже если вы редко (если
вообще) генерируете исключения.

ГЛАВА 13

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

471

Кроме того, в компиляторах Microsoft исключения C++ реализованы при по
мощи SEH, а это означает, что каждый оператор throw вызывает функцию Raise
Exception. В этом нет ничего плохого, но каждый throw вызывает всеми любимое
переключение в режим ядра. Само по себе переключение выполняется очень
быстро, но манипуляции с вашим исключением в режиме ядра требуют огром
ных затрат. В разделе «Советы и уловки» главы 7 я рассказал о способе контроля
исключений C++, который сможет точнее указать вам на эти затраты.
Похоже, иногда разработчики забывают о цене исключений C++. Однажды меня
наняла одна компания, чтобы я решил проблему с производительностью програм
мы. Приступив к работе, я никак не мог понять, почему функция _except_handler3,
выполняющаяся при обработке исключений, вызывается столько раз. При проверке
кода я понял, что вместо испытанной конструкции switch...case ктото исполь
зовал обработку исключений C++. Чтобы ускорить приложение, компании при
шлось переписать значительную часть кода этого программиста просто для воз
врата значений перечисления. На мой вопрос, почему он решил прибегнуть к об
работке исключений C++, он ответил, что думал, что оператор throw просто изме
няет указатель команд. Итак, исключения C++ допустимы только в тех програм
мах, быстродействие которых не имеет никакого значения.

Никогда, ни за что, НИ В КОЕМ СЛУЧАЕ не используйте catch ( ... )
Мне очень нравится конструкция catch (...), потому что она весьма благоприят
но сказывается на моем банковском счете, вызывая больше ошибок, чем вы мо
жете себе представить. С ней связаны две огромных проблемы. Первая заключа
ется в ее спецификации согласно стандарту ANSI. Многоточие означает, что блок
catch перехватывает любой тип исключений.
Однако изза отсутствия в catch какойлибо переменной вы не можете узнать,
как вы очутились в этом блоке и почему. Это, например, означает, что исключе
ние может быть вызвано присвоением случайного значения указателю команд.
Вследствие невозможности узнать тип и причину исключения единственное бе
зопасное и благоразумное действие, которое вы можете предпринять, — завер
шить приложение. Некоторым из вас, возможно, покажется, что это слишком ра
дикальное решение, но лучшего я предложить не могу.
Вторая проблема связана с реализацией catch (...). Многие программисты не
осознают, что в стандартной библиотеке C для Windows блок catch (...) перехва
тывает не только исключения C++, но и исключения SEH! Вы не только не будете
знать, как вы оказались в блоке catch, но и сможете очутиться в нем изза нару
шения доступа к памяти или какойнибудь другой аппаратной ошибки. В этом
случае вы можете не только заблудиться, но и столкнуться с совершенно неста
бильным поведением программы в блоке catch, поэтому самым лучшим решени
ем будет немедленное завершение процесса.
Меня просто поражает, как часто программисты, и довольно опытные в том
числе, пишут чтонибудь вроде:

BOOL DoSomeWork ( void )
{
BOOL bRet = TRUE ;
try
{

472

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// ...Какиенибудь действия...
}
catch ( ... )
{
// ВНИМАНИЕ! ЭТОТ БЛОК ПУСТ!
}
return ( bRet ) ;
}
Если в такой ситуации произойдет нарушение доступа, оно будет перехвачено
блоком catch (...), но вы об этом даже не узнаете. Минут через двадцать ваша про
грамма потерпит крах, однако о его причине вы не будете иметь абсолютно ни
какого представления, потому что стек вызовов не будет иметь причиннослед
ственного отношения. Вам останется только удивляться проблеме. Опираясь на
свой богатый опыт, я утверждаю, что главной причиной непонятных ошибок яв
ляется catch (...). Гораздо лучше допустить аварийное завершение приложения,
потому что тогда у вас хотя бы будет неплохой шанс обнаружения ошибки. При
использовании catch (...) вероятность ее обнаружения снижается до 5% и ниже.
Если вы еще не догадались, я сам скажу, что советую вам удалить все блоки catch
(...) из ваших программ. Фактически я ожидаю, что вы сделаете это сразу, иначе
вы скоро обратитесь ко мне за услугами и поможете мне выплатить очередной
взнос за автомобиль.

Отладка: фронтовые очерки
О вреде catch (...)
Боевые действия
Однажды я ехал в аэропорт, собираясь вылететь к клиенту. Тут мне позво
нил руководитель отделения и сказал, что мы получили отчаянный — про
сто безумный — запрос о проведении консультации. Помогать обезумевшим
людям — наша работа, поэтому я решил узнать, что случилось. Поднявший
трубку менеджер не просто обезумел — он был на грани инфаркта! Он сказал,
что сотрудники его компании бьются над решением абсолютно непонят
ной ошибки, задерживающей выпуск программы. Он также сказал, что это
еще не самое страшное, и добавил, что если ошибка не будет решена и
программа не будет закончена, его компания обанкротится. Более 10 про
граммистов уже работали над этой ошибкой три недели подряд, но безре
зультатно. В то время моя жизнь была не особо увлекательной по сравне
нию с работой на предыдущих должностях, поэтому возможность спасти
чьюто компанию определенно привлекла мой интерес. Этот человек спро
сил меня, насколько быстро я смогу добраться к ним. Я ответил, что выле
таю в Сиэтл на неделю, поэтому не смогу заняться их проблемой до окон
чания этого срока. Мы недавно основали компанию Wintellect, и у нас не
было свободных сотрудников.
В этот момент менеджер начал говорить гораздо громче (ладно, что греха
таить, он начал орать), утверждая, что не может столько ждать. Он спро
сил, буду ли я занят в Сиэтле постоянно. Это было не так, и он предложил

ГЛАВА 13

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

473

мне работать над их проблемой по вечерам. Меня это вполне устраивало,
и я сказал, что смогу работать над их ошибкой каждый день до полуночи, а
он тут же отодвинул трубку от уха и прокричал комуто: «Он будет в Сиэт
ле! Немедленно вылетайте!» Менеджер сказал мне, что отправил в аэропорт
двух программистов с необходимым оборудованием. Когда я спросил его,
что случится, если мы не решим проблему до того, как мне придется уехать
из Сиэтла, он ответил: «Мы последуем за вами в НьюХэмпшир». Ошибка ав
томатически перешла в категорию суперсерьезных.
Следующим вечером я приехал в отель на встречу с этими программис
тами и увидел двух человек, едва стоявших на ногах. Они работали над этой
ошибкой примерно три недели подряд почти без перерыва. Увидев прило
жение, я сразу покрылся холодным потом! Они разрабатывали модуль GINA
(Graphical Identification and Authentication, — графическая идентификация
и аутентификация), предназначенный для регистрации на терминальном
сервере при помощи смарткарты! Да уж, трудно представить более непри
ятного приложения для отладки! Так как значительная часть программы
выполнялась внутри LSASS.EXE, вы могли начать отладку, но любой щелчок
вне отладчика блокировал компьютер. Чтобы сделать мою жизнь еще ин
тересней, программисты повсюду использовали STL, поэтому программа не
только содержала в себе ошибку, но и была очень тяжелой в понимании.
Всем нам стоит помнить, что основное достоинство STL заключается не в
повторно используемых структурах данных, а в гарантии занятости. Пони
мание и сопровождение кода, написанного при помощи STL, доступно только
его автору, что обеспечивает ему уверенность в завтрашнем дне и постоян
ный источник дохода.
Я спросил их, могут ли они показать мне чтонибудь, напоминающее
воспроизводимую ошибку или искажение данных. Они дали мне листинг
и указали 10 или 12 мест, где они сталкивались с ошибками. Сначала я пред
положил, что ошибка объясняется записью данных в неинициализирован
ную память. Потратив несколько часов на изучение работы системы и при
выкание к отладке их приложения, я попытался найти эти неинициализи
рованные указатели. Пробираясь через дебри исходного кода, я обнаружил
огромное число блоков catch (...). К концу первого вечера я сказал им уда
лить все операторы catch (...), чтобы мы могли видеть искажение данных
немедленно и начали сужать диапазон причин проблемы.

Исход
Когда я вернулся к ним на следующий день, физические возможности про
граммистов приближались к концу. Они сообщили мне, что, когда они за
комментировали все операторы catch (...), приложение перестало иници
ализироваться. Отправив их спать, я начал просматривать код инициали
зации и быстро обнаружил следующее:

//catch ( ... )
//{
return ( FALSE ) ;
//}
см. след. стр.

474

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

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

Полученный опыт
Урок прост: не используйте catch (...)! В данном конкретном случае ком
пании пришлось потратить недели труда (и огромные деньги) в поисках
легкой ошибки, которую нельзя было воспроизвести изза catch (...).

Не используйте _set_se_translator
В первом издании этой книги я описал интересную APIфункцию _set_se_translator,
которая волшебным образом преобразует ваши ошибки SEH в исключения C++.
Для этого она вызывает определенную вами функцию, вызывающую throw для каж
дого типа, который вы хотите использовать для преобразования. Сейчас я могу
признаться, что тот мой совет оказался ошибочным, хотя в его основе и лежали
добрые намерения. При использовании _set_se_translator вы быстро обнаружи
те, что в заключительных компоновках она не работает.
Первая проблема с _set_se_translator в том, что она не является глобальной;
область ее действия ограничена конкретным потоком. Это значит, что вам, веро
ятно, придется переработать свою программу, чтобы гарантировать вызов _set_se_trans
lator в начале каждого потока. Увы, это не всегда просто. Кроме того, если вы
разрабатываете компонент, используемый другими, не контролируемыми вами
процессами, _set_se_translator полностью запутает обработку исключений этих
процессов, если они ожидают исключения SEH, а вместо этого получают исклю
чения C++.
Более серьезная проблема касается скрытых деталей реализации обработки
исключений C++. Обработка исключений C++ может быть реализована двумя спо
собами: асинхронным и синхронным. При асинхронном режиме генератор кода
предполагает, что исключение может быть сгенерировано любой командой, при
синхронном исключения генерируются явно только оператором throw. Различия
между асинхронной и синхронной обработкой исключений не кажутся такими
уж большими, но на самом деле это именно так.
Асинхронные исключения имеют один недостаток: компилятор должен сгене
рировать для каждой функции то, что называется кодом слежения за временем
жизни объекта (object lifetime tracking code). Компилятор полагает, что исключе
ние может быть сгенерировано любой командой, поэтому каждая функция, по
мещающая объект C++ в стек, должна включать код, гарантирующий при возник

ГЛАВА 13

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

475

новении исключения вызов деструкторов каждого объекта. А так как предполага
ется, что исключения — редкие или почти невозможные события, неиспользуе
мый код слежения за временем жизни объектов приводит к значительному сни
жению быстродействия программы.
Синхронная обработка исключений решает эту проблему, генерируя код сле
жения за временем жизни объекта, только когда метод в дереве вызовов включа
ет явный throw. Фактически синхронные исключения настолько хороши, что именно
этот тип исключений используется компилятором. Однако компилятор предпо
лагает, что исключения происходят только в результате явного throw в стеке вы
зовов, в то время как функция преобразования выполняет throw, который нахо
дится вне нормального потока выполнения программы и, таким образом, являет
ся асинхронным. Изза этого ваш тщательно разработанный класс оболочки ис
ключений C++ никогда не будет обработан, и ваше приложение все равно потер
пит крах. Чтобы лучше изучить различия между асинхронной и синхронной об
работкой исключений, включите асинхронный режим, добавив в командную строку
компилятора ключ /EHa и удалив ключи /GX и /EHs.
Ситуация ухудшается еще и тем, что в отладочных компоновках _set_se_translator
работает правильно. Проблемы возникают только в заключительных компонов
ках. Это объясняется тем, что в отладочных компоновках применяется синхрон
ная обработка исключений вместо асинхронной, используемой по умолчанию в
заключительных компоновках. Изза описанных мной проблем просмотрите свой
код и убедитесь в том, что эта функция нигде не вызывается.

API-функция SetUnhandledExceptionFilter
Ошибки имеют привычку никогда не происходить там, где вы их ожидаете. Увы,
когда пользователи сталкиваются с ошибкой вашей программы, они просто ви
дят диалоговое окно сообщения об ошибке; возможно, Dr. Watson предоставит им
некоторую информацию, которую они смогут послать вам, чтобы облегчить по
иск проблемы. Как я уже говорил, для получения информации, действительно
нужной для исправления ошибок, вы можете разработать собственные диалого
вые окна и обработчики. Я всегда называю эти обработчики исключений вместе
с соответствующими им фильтрами исключений обработчиками ошибок.
Судя по моему опыту, обработчики ошибок значительно облегчают отладку. Во
многих проектах мы получали контроль сразу же после краха приложения, запи
сывали всю информацию об ошибке (включая состояние системы пользователя)
в файл, и, если проект был клиентским приложением, выводили диалоговое окно
с телефонным номером службы поддержки. В некоторых случаях мы реализовы
вали возможность циклического изучения основных объектов программы, что
позволяло нам опускаться до уровня классов и регистрировать активные объек
ты и состояние их данных. Можно сказать, что записываемая нами информация
о состоянии программы была чуть ли не избыточной. Такие отчеты об ошибках
предоставляли нам 90%ый шанс воспроизведения проблемы пользователя. Если
это не проактивная отладка, то я не знаю, что это такое!
Создание обработчиков ошибок обеспечивает APIфункция SetUnhandledExcep
tionFilter. Удивительно, но эта возможность присутствует в Win32 со времен

476

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Microsoft Windows NT 3.5, однако почти не упоминается в документации. На мо
мент написания этой главы данная функция встречается в MSDN Online только
восемь раз.
Надо сказать, что я нашел эту функцию очень эффективной. Просто взглянув
на ее имя — SetUnhandledExceptionFilter, вы можете догадаться, что она делает. Она
позволяет указать функциюфильтр необработанных исключений, которая будет
вызываться при возникновении в процессе необработанного исключения. Един
ственным параметром SetUnhandledExceptionFilter является указатель на функцию
фильтр исключений, которая вызывается в заключительном блоке __except при
ложения. Этот фильтр исключений может возвращать те же значения, что и лю
бой другой фильтр исключений: EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_EXE
CUTION или EXCEPTION_CONTINUE_SEARCH. Вы можете выполнять в фильтре исключений
почти любые действия по их обработке, но при этом нужно помнить о перепол
нении стека. Чтобы обезопасить себя, вам, вероятно, следует избегать вызовов
функций стандартной библиотеки C, а также MFC. Я должен был предупредить вас
об этих неприятностях, однако я могу гарантировать, что большинство ваших
ошибок будет ошибками нарушения доступа, поэтому, реализуя в фильтре и об
работчике исключений полную систему обработки ошибок, вы, скорее всего, не
столкнетесь с какимилибо проблемами, если будете проверять причину исклю
чения и избегать вызовов функций при переполнении стека.
Ваш фильтр исключений получает указатель на структуру EXCEPTION_POINTERS.
В листинге 134 я привожу несколько функций, которые помогут вам выполнять
преобразование этой структуры. Благодаря этому вы сможете писать собственные
обработчики ошибок, так как в каждой компании к ним предъявляются различ
ные требования.
Используя SetUnhandledExceptionFilter, нужно кое о чем помнить. Вопервых, вы
не сможете отлаживать установленный фильтр необработанных исключений,
применяя стандартные отладчики пользовательского режима. Это ограничение
определенно имеет смысл, так как при выполнении программы под отладчиком
ОС должна взять на себя управление заключительным фильтром исключений, чтобы
сообщить отладчику правильную информацию о последней ошибке. Это затруд
няет отладку заключительного обработчика ошибок. Одно из возможных реше
ний данной проблемы — вызвать фильтр необработанных исключений из обыч
ного фильтра исключений SEH. Пример такого подхода вы найдете в функции Baz
из файла BugslayerUtil\Tests\CrashHandler\CrashHandler.CPP, который находится на CD.
Другая проблема в том, что фильтр исключений, указываемый вами при вызо
ве SetUnhandledExceptionFilter, является для вашего процесса глобальным. Если вы
создаете самый лучший обработчик ошибок в мире для своего элемента управле
ния ActiveX и ошибка возникает в контейнере, то даже несмотря на то, что она
не ваша, будет выполнен ваш обработчик ошибок. Однако не позволяйте этой
проблеме воспрепятствовать использованию SetUnhandledExceptionFilter. Ниже я
приведу некоторый код, который поможет справиться с этой неприятностью.

ГЛАВА 13

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

477

Стандартный вопрос отладки
Что можно сделать с переполнением стека при бесконечной рекурсии?
Нам повезло: бесконечная рекурсия встречается не так часто, но такую
ошибку почти невозможно отладить. Если вы когданибудь видели прило
жение, которое приостанавливается на секундудругую, после чего полно
стью исчезает безо всякого сообщения об ошибке, почти наверняка это было
вызвано бесконечной рекурсией. Если приложение не оставляет даже шан
са на свою отладку, обнаружить ошибку чрезвычайно сложно.
К счастью, при помощи новой функции resetstkoflw стандартной биб
лиотеки вы можете попробовать получить некоторое пространство в сте
ке, чтобы хотя бы сообщить об ошибке. Если вы хотите увидеть, как _reset
stkoflw делает свою магию, посмотрите ее реализацию в файле RESETSTK.C.

Использование API CrashHandler
Мой модуль BUGSLAYERUTIL.DLL включает API CrashHandler, который вы можете
использовать для ограничения своего обработчика ошибок конкретным модулем
или модулями. Это достигается благодаря передаче всех необработанных исклю
чений моему фильтру. При вызове моего фильтра необработанных исключений
я проверяю модуль, из которого пришло исключение. Если исключение возник
ло в одном из указанных вами модулей, я вызываю ваш обработчик ошибок; если
оно пришло из других модулей, я вызываю первоначальный фильтр необработан
ных исключений. Вызов первоначального фильтра исключений означает, что мой
API CrashHandler могут использовать несколько модулей, не мешая друг другу. Если
никакие модули не указаны, ваш обработчик ошибок будет вызываться всегда. Все
функции API CrashHandler приведены в листинге 134. Я советую вам тщательно
изучить этот код, потому что, если вы его поймете, вы неплохо разберетесь в
обработке исключений, использовании символьной машины DBGHELP.DLL и ана
лизе стека.

Листинг 13-4.

CRASHHANDLER.CPP

/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright © 19972003 John Robbins — All rights reserved.
——————————————————————————————————————————————————————————————————————*/
#include "pch.h"
#include "BugslayerUtil.h"
#include "CrashHandler.h"
// Внутренний заголовочный файл проекта
#include "Internal.h"
/*//////////////////////////////////////////////////////////////////////
// Определения с областью видимости файла
см. след. стр.

478

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

//////////////////////////////////////////////////////////////////////*/
// Максимальный размер символов для модуля
#define MAX_SYM_SIZE 512
#define BUFF_SIZE 2048
#define SYM_BUFF_SIZE 1024
// Константы формата строк. Чтобы избежать частого
// преобразования строк ANSI в формат UNICODE вручную,
// я делаю это с помощью функции wsprintf. Работая с ANSI,
// в строках формата нужно использовать %s, а не %S.
#ifdef UNICODE
#define k_NAMEDISPFMT
_T ( " %S()+%04d byte(s)" )
#define k_NAMEFMT
_T ( " %S " )
#define k_FILELINEDISPFMT _T ( " %S, line %04d+%04d byte(s)" )
#define k_FILELINEFMT
_T ( " %S, line %04d" )
#else
#define k_NAMEDISPFMT
_T ( " %s()+%04d byte(s)" )
#define k_NAMEFMT
_T ( " %s " )
#define k_FILELINEDISPFMT _T ( " %s, line %04d+%04d byte(s)" )
#define k_FILELINEFMT
_T ( " %s, line %04d" )
#endif
#ifdef _WIN64
#define k_PARAMFMTSTRING
#else
#define k_PARAMFMTSTRING
#endif

_T ( " (0x%016X 0x%016X 0x%016X 0x%016X)" )
_T ( " (0x%08X 0x%08X 0x%08X 0x%08X)" )

// Определение типа компьютера.
#ifdef _X86_
#define CH_MACHINE IMAGE_FILE_MACHINE_I386
#elif _AMD64_
#define CH_MACHINE IMAGE_FILE_MACHINE_AMD64
#elif _IA64_
#define CH_MACHINE IMAGE_FILE_MACHINE_IA64
#else
#pragma FORCE COMPILE ABORT!
#endif
/*//////////////////////////////////////////////////////////////////////
// Глобальные переменные с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Новый фильтр необработанных исключений (обработчик ошибок)
static PFNCHFILTFN g_pfnCallBack = NULL ;
// Первоначальный фильтр необработанных исключений
static LPTOP_LEVEL_EXCEPTION_FILTER g_pfnOrigFilt = NULL ;
// Массив модулей, ограничивающих применение обработчика ошибок
static HMODULE * g_ahMod = NULL ;

ГЛАВА 13

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

479

// Размер массива g_ahMod в элементах
static UINT g_uiModCount = 0 ;
// Статический буфер, возвращаемый различными функциями,
// обеспечивает передачу данных без использования стека.
static TCHAR g_szBuff [ BUFF_SIZE ] ;
// Статический буфер для просмотра символов
static BYTE g_stSymbol [ SYM_BUFF_SIZE ] ;
// Статическая структура, содержащая сведения
// об исходном файле и номере строки
static IMAGEHLP_LINE64 g_stLine ;
// Структура кадра стека, используемая при анализе стека
static STACKFRAME64 g_stFrame ;
// Флаг, указывающий на состояние инициализации символьной машины
static BOOL g_bSymEngInit = FALSE ;
// Первоначальный вариант этого кода изменял при анализе стека
// структуру CONTEXT. Поэтому, если пользователь применял для
// записи минидампа структуру EXCEPTION_POINTERS, включающую
// указатель на CONTEXT, дамп получался некорректным. Теперь
// я сохраняю CONTEXT глобально, во многом как и кадр стека.
static CONTEXT g_stContext ;
/*//////////////////////////////////////////////////////////////////////
// Объявления функций с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Обработчик исключений
LONG __stdcall CrashHandlerExceptionFilter ( EXCEPTION_POINTERS *
pExPtrs
) ;
// Функция преобразования идентификатора
// простого исключения в строковое значение
LPCTSTR ConvertSimpleException ( DWORD dwExcept ) ;
// Внутренняя функция, отвечающая за весь анализ стека
LPCTSTR __stdcall InternalGetStackTraceString ( DWORD dwOpts ) ;
// Функция, инициализирующая в случае надобности символьную машину
void InitSymEng ( void ) ;
// Функция, очищающая в случае надобности символьную машину
void CleanupSymEng ( void ) ;
/*//////////////////////////////////////////////////////////////////////
// Класс деструктора
//////////////////////////////////////////////////////////////////////*/
см. след. стр.

480

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// См. примечание об автоматических классах в MEMDUMPERVALIDATOR.CPP.
// Отключение предупреждения "инициализаторы в области инициализации
// библиотеки" (initializers put in library initialization area)
#pragma warning (disable : 4073)
#pragma init_seg(lib)
class CleanUpCrashHandler
{
public :
CleanUpCrashHandler ( void )
{
}
~CleanUpCrashHandler ( void )
{
// Имеются ли неосвобожденные блоки выделенной памяти?
if ( NULL != g_ahMod )
{
VERIFY ( HeapFree ( GetProcessHeap ( ) ,
0
,
g_ahMod
) ) ;
g_ahMod = NULL ;
// ИСПРАВЛЕННАЯ ОШИБКА. Спасибо Геннадию Майко (Gennady Mayko).
g_uiModCount = 0 ;
}
if ( NULL != g_pfnOrigFilt )
{
// Восстановление первоначального фильтра необработанных исключений.
SetUnhandledExceptionFilter ( g_pfnOrigFilt ) ;
g_pfnOrigFilt = NULL ;
}
}
} ;
// Статический класс
static CleanUpCrashHandler g_cBeforeAndAfter ;
/*//////////////////////////////////////////////////////////////////////
// Реализация функций обработчика ошибок.
//////////////////////////////////////////////////////////////////////*/
BOOL __stdcall SetCrashHandlerFilter ( PFNCHFILTFN pFn )
{
// Если pFn равен NULL, новый фильтр необработанных исключений удаляется.
if ( NULL == pFn )
{
if ( NULL != g_pfnOrigFilt )
{
// Восстановление первоначального фильтра необработанных исключений.
SetUnhandledExceptionFilter ( g_pfnOrigFilt ) ;
g_pfnOrigFilt = NULL ;
if ( NULL != g_ahMod )

ГЛАВА 13

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

481

{
// ИСПРАВЛЕННАЯ ОШИБКА:
// Раньше я вызвал функцию "free" вместо "HeapFree."
VERIFY ( HeapFree ( GetProcessHeap ( ) ,
0
,
g_ahMod
) ) ;
g_ahMod = NULL ;
// ИСПРАВЛЕННАЯ ОШИБКА. Спасибо Геннадию Майко.
g_uiModCount = 0 ;
}
g_pfnCallBack = NULL ;
}
}
else
{
ASSERT ( FALSE == IsBadCodePtr ( (FARPROC)pFn ) ) ;
if ( TRUE == IsBadCodePtr ( (FARPROC)pFn ) )
{
return ( FALSE ) ;
}
g_pfnCallBack = pFn ;
//
//
//
if
{

Если новый обработчик ошибок еще не установлен, выполняется
установка CrashHandlerExceptionFilter; первоначальный
фильтр необработанных исключений при этом сохраняется.
( NULL == g_pfnOrigFilt )
g_pfnOrigFilt =
SetUnhandledExceptionFilter(CrashHandlerExceptionFilter);

}
}
return ( TRUE ) ;
}
BOOL __stdcall AddCrashHandlerLimitModule ( HMODULE hMod )
{
// Проверка тривиального случая.
ASSERT ( NULL != hMod ) ;
if ( NULL == hMod )
{
return ( FALSE ) ;
}
// Создание временного массива. Он должен создаваться в той памяти,
// которая точно будет в нашем распоряжении даже при плохом
// самочувствии процесса. Куча стандартнойбиблиотеки этому условию
// не удовлетворяет, поэтому я создаю временный массив в куче процесса.
HMODULE * phTemp = (HMODULE*)
HeapAlloc ( GetProcessHeap ( )
,
HEAP_ZERO_MEMORY |
см. след. стр.

482

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

HEAP_GENERATE_EXCEPTIONS
,
(sizeof(HMODULE)*(g_uiModCount+1)) ) ;
ASSERT ( NULL != phTemp ) ;
if ( NULL == phTemp )
{
TRACE ( "Serious trouble in the house!  "
"HeapAlloc failed!!!\n"
);
return ( FALSE ) ;
}
if ( NULL == g_ahMod )
{
g_ahMod = phTemp ;
g_ahMod[ 0 ] = hMod ;
g_uiModCount++ ;
}
else
{
// Копирование старых значений.
CopyMemory ( phTemp
,
g_ahMod
,
sizeof ( HMODULE ) * g_uiModCount ) ;
// Освобождение старой памяти.
VERIFY ( HeapFree ( GetProcessHeap ( ) , 0 , g_ahMod ) ) ;
g_ahMod = phTemp ;
g_ahMod[ g_uiModCount ] = hMod ;
g_uiModCount++ ;
}
return ( TRUE ) ;
}
UINT __stdcall GetLimitModuleCount ( void )
{
return ( g_uiModCount ) ;
}
int __stdcall GetLimitModulesArray ( HMODULE * pahMod , UINT uiSize )
{
int iRet ;
__try
{
ASSERT ( FALSE == IsBadWritePtr ( pahMod ,
uiSize * sizeof ( HMODULE ) ) ) ;
if ( TRUE == IsBadWritePtr ( pahMod ,
uiSize * sizeof ( HMODULE ) ) )
{
iRet = GLMA_BADPARAM ;
__leave ;
}

ГЛАВА 13

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

483

if ( uiSize < g_uiModCount )
{
iRet = GLMA_BUFFTOOSMALL ;
__leave ;
}
CopyMemory ( pahMod
,
g_ahMod
,
sizeof ( HMODULE ) * g_uiModCount ) ;
iRet
}
__except
{
iRet
}
return (

= GLMA_SUCCESS ;
( EXCEPTION_EXECUTE_HANDLER )
= GLMA_FAILURE ;
iRet ) ;

}
LONG __stdcall CrashHandlerExceptionFilter (EXCEPTION_POINTERS* pExPtrs)
{
LONG lRet = EXCEPTION_CONTINUE_SEARCH ;
// Если исключение имеет тип EXCEPTION_STACK_OVERFLOW, с ним почти
// ничего нельзя сделать изза плачевного состояния стека. Любые
// действия скорее всего приведут к еще одной ошибке сразу же около
// вашего фильтра исключений. Я не рекомендую этого делать, однако
// вы можете попытаться какнибудь изменить указатель стека, чтобы
// освободить место для вызова этих функций. Конечно, если вы измените
// регистр указателя стека, у вас возникнут проблемы с анализом стека.
// Я использую безопасный подход и выполняю несколько вызовов функции
// OutputDebugString. Они также могут привести к повторной ошибке,
// но попробовать это стоит, так как OutputDebugString требует совсем
// небольшого пространства в стеке (816 байт). Чтобы ваши пользователи
// могли хотя бы рассказать вам, что они видят, попросите их загрузить
// с сайта www.sysinternals.com программу DebugView, написанную
// Марком Руссиновичем (Mark Russinovich). Единственная проблема в том,
// что в стеке может не быть места даже для размещения указателя команд.
// К счастью, исключение EXCEPTION_STACK_OVERFLOW случается довольно
// редко. Вас, возможно, интересует, почему я не вызываю здесь новую
// функцию _resetstkoflw. Она вызывается только при критических
// исключениях, поэтому, если приложение "умирает", попытка восстановления
// стека ни к чему не приведет. Функция _resetstkoflw полезна,
// только если ее вызвать до этого момента.
__try
{
// Обратите внимание: я все же вызываю ваш обработчик ошибок.
// Если же переполнение стека нарушит работу вашего
// обработчика ошибок, я вывожу информацию о типе исключения.
см. след. стр.

484

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

if ( EXCEPTION_STACK_OVERFLOW ==
pExPtrs>ExceptionRecord>ExceptionCode )
{
OutputDebugString(_T("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"));
OutputDebugString(_T("EXCEPTION_STACK_OVERFLOW occurred\n"));
OutputDebugString(_T("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"));
}
if ( NULL != g_pfnCallBack )
{
// Сейчас нужно инициализировать символьную машину,
// чтобы подготовить ее к работе и иметь возможность
// получения базового адреса модуля по адресу ошибки.
InitSymEng ( ) ;
// Проверка списка g_ahMod.
BOOL bCallIt = FALSE ;
if ( 0 == g_uiModCount )
{
bCallIt = TRUE ;
}
else
{
HINSTANCE hBaseAddr = (HINSTANCE)
SymGetModuleBase64( GetCurrentProcess ( )
,
(DWORD64)pExPtrs>
ExceptionRecord>
ExceptionAddress);
if ( NULL != hBaseAddr )
{
for ( UINT i = 0 ; i < g_uiModCount ; i ++ )
{
if ( hBaseAddr == g_ahMod[ i ] )
{
bCallIt = TRUE ;
break ;
}
}
}
}
if ( TRUE == bCallIt )
{
// Прежде чем вызвать обработчик ошибок, я проверяю его
// наличие в памяти. Пользователь может забыть отменить
// регистрацию, и обработчик ошибок может быть
// некорректным, если он был выгружен. Однако, если
// по тому же адресу будет загружена какаянибудь другая
// функция, я ничего не смогу сделать.
ASSERT ( FALSE == IsBadCodePtr((FARPROC)g_pfnCallBack));

ГЛАВА 13

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

485

if ( FALSE == IsBadCodePtr ( (FARPROC)g_pfnCallBack ) )
{
lRet = g_pfnCallBack ( pExPtrs ) ;
}
}
else
{
// Вызов предыдущего фильтра, но только после
// проверки. Я становлюсь немного подозрительным.
ASSERT ( FALSE == IsBadCodePtr((FARPROC)g_pfnOrigFilt));
if ( FALSE == IsBadCodePtr ( (FARPROC)g_pfnOrigFilt ) )
{
lRet = g_pfnOrigFilt ( pExPtrs ) ;
}
}
CleanupSymEng ( ) ;
}
}
__except ( EXCEPTION_EXECUTE_HANDLER )
{
lRet = EXCEPTION_CONTINUE_SEARCH ;
}
return ( lRet ) ;
}
/*//////////////////////////////////////////////////////////////////////
// Реализация функций преобразования структуры EXCEPTION_POINTERS.
//////////////////////////////////////////////////////////////////////*/
LPCTSTR __stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs )
{
ASSERT ( FALSE == IsBadReadPtr ( pExPtrs ,
sizeof ( EXCEPTION_POINTERS ) ) ) ;
if ( TRUE == IsBadReadPtr ( pExPtrs ,
sizeof ( EXCEPTION_POINTERS ) ) )
{
TRACE0 ( "Bad parameter to GetFaultReason\n" ) ;
return ( NULL ) ;
}
// Переменная для хранения возвращаемого значения
LPCTSTR szRet ;
__try
{
// Инициализация символьной машины, если она не инициализирована.
InitSymEng ( ) ;
// Текущая позиция в буфере
см. след. стр.

486

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

int iCurr = 0 ;
// Переменная для хранения временного значения. Это
// позволяет свести использование стека к минимуму.
DWORD64 dwTemp ;
iCurr += BSUGetModuleBaseName ( GetCurrentProcess ( ) ,
NULL
,
g_szBuff
,
BUFF_SIZE
) ;
iCurr += wsprintf ( g_szBuff + iCurr , _T ( " caused an " ) ) ;
dwTemp = (DWORD_PTR)
ConvertSimpleException(pExPtrs>ExceptionRecord>
ExceptionCode);
if ( NULL != dwTemp )
{
iCurr += wsprintf ( g_szBuff + iCurr ,
_T ( "%s" )
,
dwTemp
) ;
}
else
{
iCurr += FormatMessage( FORMAT_MESSAGE_IGNORE_INSERTS |
FORMAT_MESSAGE_FROM_HMODULE,
GetModuleHandle (_T("NTDLL.DLL")) ,
pExPtrs>ExceptionRecord>
ExceptionCode ,
0
,
g_szBuff + iCurr
,
BUFF_SIZE
,
0
);
}
ASSERT ( iCurr < ( BUFF_SIZE  MAX_PATH ) ) ;
iCurr += wsprintf ( g_szBuff + iCurr , _T ( " in module " ) ) ;
dwTemp =
SymGetModuleBase64( GetCurrentProcess ( ) ,
(DWORD64)pExPtrs>ExceptionRecord>
ExceptionAddress ) ;
ASSERT ( NULL != dwTemp ) ;
if ( NULL == dwTemp )
{
iCurr += wsprintf ( g_szBuff + iCurr , _T ( "" ) );
}
else

ГЛАВА 13

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

487

{
iCurr += BSUGetModuleBaseName ( GetCurrentProcess ( ) ,
(HINSTANCE)dwTemp
,
g_szBuff + iCurr
,
BUFF_SIZE  iCurr
) ;
}
#ifdef _WIN64
iCurr += wsprintf ( g_szBuff + iCurr
,
_T ( " at %016X" ) ,
pExPtrs>ExceptionRecord>ExceptionAddress);
#else
iCurr += wsprintf ( g_szBuff + iCurr
,
_T ( " at %04X:%08X" )
,
pExPtrs>ContextRecord>SegCs ,
pExPtrs>ExceptionRecord>ExceptionAddress);
#endif
ASSERT ( iCurr < ( BUFF_SIZE  200 ) ) ;
// Начало поиска адреса исключения.
PIMAGEHLP_SYMBOL64 pSym = (PIMAGEHLP_SYMBOL64)&g_stSymbol ;
ZeroMemory ( pSym , SYM_BUFF_SIZE ) ;
pSym>SizeOfStruct = sizeof ( IMAGEHLP_SYMBOL64 ) ;
pSym>MaxNameLength = SYM_BUFF_SIZE 
sizeof ( IMAGEHLP_SYMBOL64 ) ;
DWORD64 dwDisp ;
if ( TRUE ==
SymGetSymFromAddr64 ( GetCurrentProcess ( )
,
(DWORD64)pExPtrs>ExceptionRecord>
ExceptionAddress ,
&dwDisp
,
pSym
))
{
iCurr += wsprintf ( g_szBuff + iCurr , _T ( "," ) ) ;
// Копируемая в буфер информация о символах
// не должна превышать объем свободного места.
// Помните: имена символов имеют формат ANSI!
int iLen = lstrlenA ( pSym>Name ) ;
// Проверка того, что у нас хватает пространства
// для самого длинного имени символа и смещения.
if ( iLen > ( ( BUFF_SIZE  iCurr) 
( MAX_SYM_SIZE + 50 ) ) )
{
#ifdef UNICODE
// Получение места в стеке для преобразования строки.
TCHAR * pWideName = (TCHAR*)_alloca ( iLen + 1 ) ;
см. след. стр.

488

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

BSUAnsi2Wide ( pSym>Name , pWideName , iLen + 1 ) ;
lstrcpyn ( g_szBuff + iCurr
,
pWideName
,
BUFF_SIZE  iCurr  1 ) ;
#else
lstrcpyn ( g_szBuff + iCurr
,
pSym>Name
,
BUFF_SIZE  iCurr  1 ) ;
#endif // UNICODE
// Выход
szRet = g_szBuff ;
__leave ;
}
else
{
if ( dwDisp > 0 )
{
iCurr += wsprintf ( g_szBuff + iCurr ,
k_NAMEDISPFMT
,
pSym>Name
,
dwDisp
) ;
}
else
{
iCurr += wsprintf ( g_szBuff + iCurr ,
k_NAMEFMT
,
pSym>Name
) ;
}
}
}
else
{
// Если символ не был найден, информация об исходном файле
// и номере строки также не будет получена, поэтому выходим.
szRet = g_szBuff ;
__leave ;
}
ASSERT ( iCurr < ( BUFF_SIZE  200 ) ) ;
// Поиск информации об исходном фале и номере строки.
ZeroMemory ( &g_stLine , sizeof ( IMAGEHLP_LINE64 ) ) ;
g_stLine.SizeOfStruct = sizeof ( IMAGEHLP_LINE64 ) ;
DWORD dwLineDisp ;
if ( TRUE ==
SymGetLineFromAddr64 ( GetCurrentProcess ( )
,
(DWORD64)pExPtrs>
ExceptionRecord>

ГЛАВА 13

&dwLineDisp
&g_stLine

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

489

ExceptionAddress ,
,
) )

{
iCurr += wsprintf ( g_szBuff + iCurr , _T ( "," ) ) ;
// Копируемая в буфер информация об исходном файле и номере
// строки не должна превышать объем свободного места.
int iLen = lstrlenA ( g_stLine.FileName ) ;
if ( iLen > ( BUFF_SIZE  iCurr 
MAX_PATH  50
) )
{
#ifdef UNICODE
// Получение места в стеке для преобразования строки.
TCHAR * pWideName = (TCHAR*)_alloca ( iLen + 1 ) ;
BSUAnsi2Wide(g_stLine.FileName , pWideName , iLen + 1);
lstrcpyn ( g_szBuff + iCurr
,
pWideName
,
BUFF_SIZE  iCurr  1 ) ;
#else
lstrcpyn ( g_szBuff + iCurr
,
g_stLine.FileName
,
BUFF_SIZE  iCurr  1 ) ;
#endif // UNICODE
// Выход
szRet = g_szBuff ;
__leave ;
}
else
{
if ( dwLineDisp > 0 )
{
iCurr += wsprintf ( g_szBuff + iCurr
k_FILELINEDISPFMT
g_stLine.FileName
g_stLine.LineNumber
dwLineDisp
}
else
{
iCurr += wsprintf ( g_szBuff + iCurr
,
k_FILELINEFMT
,
g_stLine.FileName
,
g_stLine.LineNumber ) ;
}
}
}

,
,
,
,
);

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

490

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

szRet = g_szBuff ;
}
__except ( EXCEPTION_EXECUTE_HANDLER )
{
ASSERT ( !"Crashed in GetFaultReason" ) ;
szRet = NULL ;
}
return ( szRet ) ;
}
// Вспомогательная функция, позволяющая изолировать заполнение
// структуры кадра стека, которое зависит от процессора.
void FillInStackFrame ( PCONTEXT pCtx )
{
// Инициализация структуры STACKFRAME.
ZeroMemory ( &g_stFrame , sizeof ( STACKFRAME64 ) ) ;
#ifdef _X86_
g_stFrame.AddrPC.Offset
= pCtx>Eip
g_stFrame.AddrPC.Mode
= AddrModeFlat
g_stFrame.AddrStack.Offset
= pCtx>Esp
g_stFrame.AddrStack.Mode
= AddrModeFlat
g_stFrame.AddrFrame.Offset
= pCtx>Ebp
g_stFrame.AddrFrame.Mode
= AddrModeFlat
#elif _AMD64_
g_stFrame.AddrPC.Offset
= pCtx>Rip
g_stFrame.AddrPC.Mode
= AddrModeFlat
g_stFrame.AddrStack.Offset
= pCtx>Rsp
g_stFrame.AddrStack.Mode
= AddrModeFlat
g_stFrame.AddrFrame.Offset
= pCtx>Rbp
g_stFrame.AddrFrame.Mode
= AddrModeFlat
#elif _IA64_
#pragma message ( "IA64 NOT DEFINED!!" )
#pragma FORCE COMPILATION ABORT!
#else
#pragma message ( "CPU NOT DEFINED!!" )
#pragma FORCE COMPILATION ABORT!
#endif
}

;
;
;
;
;
;
;
;
;
;
;
;

LPCTSTR BUGSUTIL_DLLINTERFACE __stdcall
GetFirstStackTraceString ( DWORD
dwOpts ,
EXCEPTION_POINTERS * pExPtrs )
{
ASSERT ( FALSE == IsBadReadPtr ( pExPtrs
,
sizeof ( EXCEPTION_POINTERS * ))) ;
if ( TRUE == IsBadReadPtr ( pExPtrs
,
sizeof ( EXCEPTION_POINTERS * ) ) )
{
TRACE0 ( "GetFirstStackTraceString  invalid pExPtrs!\n" ) ;

ГЛАВА 13

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

491

return ( NULL ) ;
}
// Заполнение структуры кадра стека.
FillInStackFrame ( pExPtrs>ContextRecord ) ;
// Чтобы не повредить поля структуры EXCEPTION_POINTERS,
// я выполняю их копирование.
g_stContext = *(pExPtrs>ContextRecord) ;
return ( InternalGetStackTraceString ( dwOpts ) ) ;
}
LPCTSTR BUGSUTIL_DLLINTERFACE __stdcall
GetNextStackTraceString ( DWORD
dwOpts ,
EXCEPTION_POINTERS * /*pExPtrs*/)
{
// Вся проверка ошибок выполняется в InternalGetStackTraceString.
// Предполагается, что GetFirstStackTraceString уже инициализировала
// информацию о кадре стека.
return ( InternalGetStackTraceString ( dwOpts ) ) ;
}
BOOL __stdcall CH_ReadProcessMemory ( HANDLE
DWORD64
qwBaseAddress
PVOID
lpBuffer
DWORD
nSize
LPDWORD
lpNumberOfBytesRead
{
return ( ReadProcessMemory ( GetCurrentProcess ( ) ,
(LPCVOID)qwBaseAddress ,
lpBuffer
,
nSize
,
lpNumberOfBytesRead
) ) ;
}

,
,
,
,
)

// Внутренняя функция, отвечающая за весь анализ стека
LPCTSTR __stdcall InternalGetStackTraceString ( DWORD dwOpts )
{
// Возвращаемое значение
LPCTSTR szRet ;
// Базовый адрес модуля. Я проверяю его сразу же после вызова
// функции StackWalk, чтобы гарантировать корректность модуля.
DWORD64 dwModBase ;
__try
{
// Инициализация символьной машины, если она не инициализирована.
InitSymEng ( ) ;
см. след. стр.

492

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// Примечание: При использовании функций получения информации
//
об исходном файле и номере строки StackWalk
//
может вызвать нарушение доступа.
BOOL bSWRet = StackWalk64 ( CH_MACHINE
,
GetCurrentProcess ( )
,
GetCurrentThread ( )
,
&g_stFrame
,
&g_stContext
,
CH_ReadProcessMemory
,
SymFunctionTableAccess64
,
SymGetModuleBase64
,
NULL
);
if ( ( FALSE == bSWRet ) || ( 0 == g_stFrame.AddrFrame.Offset ))
{
szRet = NULL ;
__leave ;
}
// Прежде чем я начну все вычислять, мне нужно удостовериться
// в том, что адрес, возвращенный из StackWalk, действительно
// существует. Мне известны случаи, когда StackWalk возвращала
// TRUE, но адрес не относился к модулю данного процесса.
dwModBase = SymGetModuleBase64 ( GetCurrentProcess ( ) ,
g_stFrame.AddrPC.Offset ) ;
if ( 0 == dwModBase )
{
szRet = NULL ;
__leave ;
}
int iCurr = 0 ;
// Как минимум помещаем в буфер адрес.
#ifdef _WIN64
iCurr += wsprintf ( g_szBuff + iCurr
_T ( "0x%016X" )
g_stFrame.AddrPC.Offset
#else
iCurr += wsprintf ( g_szBuff + iCurr
_T ( "%04X:%08X" )
g_stContext.SegCs
g_stFrame.AddrPC.Offset
#endif

,
,
) ;
,
,
,
) ;

// Выводить параметры?
if ( GSTSO_PARAMS == ( dwOpts & GSTSO_PARAMS ) )
{
iCurr += wsprintf ( g_szBuff + iCurr
,
k_PARAMFMTSTRING
,
g_stFrame.Params[ 0 ]
,

ГЛАВА 13

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

g_stFrame.Params[ 1 ]
g_stFrame.Params[ 2 ]
g_stFrame.Params[ 3 ]

493

,
,

) ;
}
// Выводить имя модуля?
if ( GSTSO_MODULE == ( dwOpts & GSTSO_MODULE ) )
{
iCurr += wsprintf ( g_szBuff + iCurr , _T ( " " ) ) ;
ASSERT ( iCurr < ( BUFF_SIZE  MAX_PATH ) ) ;
iCurr += BSUGetModuleBaseName ( GetCurrentProcess ( )
(HINSTANCE)dwModBase
g_szBuff + iCurr
BUFF_SIZE  iCurr

,
,
,
) ;

}
ASSERT ( iCurr < ( BUFF_SIZE  MAX_PATH ) ) ;
DWORD64 dwDisp ;
// Выводить имя символа?
if ( GSTSO_SYMBOL == ( dwOpts & GSTSO_SYMBOL ) )
{
// Начало поиска адреса исключения.
PIMAGEHLP_SYMBOL64 pSym = (PIMAGEHLP_SYMBOL64)&g_stSymbol ;
ZeroMemory ( pSym , SYM_BUFF_SIZE ) ;
pSym>SizeOfStruct = sizeof ( IMAGEHLP_SYMBOL64 ) ;
pSym>MaxNameLength = SYM_BUFF_SIZE 
sizeof ( IMAGEHLP_SYMBOL64 ) ;
pSym>Address = g_stFrame.AddrPC.Offset ;
if ( TRUE ==
SymGetSymFromAddr64 ( GetCurrentProcess ( )
,
g_stFrame.AddrPC.Offset
,
&dwDisp
,
pSym
) )
{
if ( dwOpts & ~GSTSO_SYMBOL )
{
iCurr += wsprintf ( g_szBuff + iCurr , _T ( "," ));
}
// Копируемая в буфер информация о символах
// не должна превышать объем свободного места.
// Имена символов имеют формат ANSI
int iLen = ( lstrlenA ( pSym>Name ) * sizeof ( TCHAR));
if ( iLen > ( BUFF_SIZE  iCurr 
( MAX_SYM_SIZE + 50 ) ) )
{
#ifdef UNICODE
см. след. стр.

494

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// Получение места в стеке для преобразования строки.
TCHAR * pWideName = (TCHAR*)_alloca ( iLen + 1 ) ;
BSUAnsi2Wide ( pSym>Name , pWideName , iLen + 1 ) ;
lstrcpyn ( g_szBuff + iCurr
,
pWideName
,
BUFF_SIZE  iCurr  1 ) ;
#else
lstrcpyn ( g_szBuff + iCurr
,
pSym>Name
,
BUFF_SIZE  iCurr  1 ) ;
#endif // UNICODE
// Выход
szRet = g_szBuff ;
__leave ;
}
else
{
if ( dwDisp > 0 )
{
iCurr += wsprintf ( g_szBuff + iCurr
k_NAMEDISPFMT
pSym>Name
dwDisp
}
else
{
iCurr += wsprintf ( g_szBuff + iCurr ,
k_NAMEFMT
,
pSym>Name
)
}

,
,
,
) ;

;

}
}
else
{
// Если символ не был найден, информация об исходном файле
// и номере строки также не будет получена, поэтому выходим.
szRet = g_szBuff ;
__leave ;
}
}
ASSERT ( iCurr < ( BUFF_SIZE  MAX_PATH ) ) ;
// Выводить информацию об исходном файле и номере строки?
if ( GSTSO_SRCLINE == ( dwOpts & GSTSO_SRCLINE ) )
{

ГЛАВА 13

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

495

ZeroMemory ( &g_stLine , sizeof ( IMAGEHLP_LINE64 ) ) ;
g_stLine.SizeOfStruct = sizeof ( IMAGEHLP_LINE64 ) ;
DWORD dwLineDisp ;
if ( TRUE == SymGetLineFromAddr64 ( GetCurrentProcess ( ) ,
g_stFrame.AddrPC.Offset,
&dwLineDisp
,
&g_stLine
))
{
if ( dwOpts & ~GSTSO_SRCLINE )
{
iCurr += wsprintf ( g_szBuff + iCurr , _T ( "," ));
}
// Копируемая информация об исходном файле и номере
// строки не должна превышать объем свободного места.
int iLen = lstrlenA ( g_stLine.FileName ) ;
if ( iLen > ( BUFF_SIZE  iCurr 
( MAX_PATH + 50
) ) )
{
#ifdef UNICODE
// Получение места в стеке для преобразования строки.
TCHAR * pWideName = (TCHAR*)_alloca ( iLen + 1 ) ;
BSUAnsi2Wide ( g_stLine.FileName ,
pWideName
,
iLen + 1
) ;
lstrcpyn ( g_szBuff + iCurr
,
pWideName
,
BUFF_SIZE  iCurr  1 ) ;
#else
lstrcpyn ( g_szBuff + iCurr
,
g_stLine.FileName
,
BUFF_SIZE  iCurr  1 ) ;
#endif
// Выход
szRet = g_szBuff ;
__leave ;
}
else
{
if ( dwLineDisp > 0 )
{
iCurr += wsprintf( g_szBuff + iCurr
k_FILELINEDISPFMT
g_stLine.FileName
g_stLine.LineNumber
dwLineDisp

,
,
,
,
) ;
см. след. стр.

496

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

}
else
{
iCurr += wsprintf ( g_szBuff + iCurr
k_FILELINEFMT
g_stLine.FileName
g_stLine.LineNumber

,
,
,
) ;

}
}
}
}
szRet = g_szBuff ;
}
__except ( EXCEPTION_EXECUTE_HANDLER )
{
ASSERT ( !"Crashed in InternalGetStackTraceString" ) ;
szRet = NULL ;
}
return ( szRet ) ;
}
LPCTSTR __stdcall GetRegisterString ( EXCEPTION_POINTERS * pExPtrs )
{
// Проверка параметра.
ASSERT ( FALSE == IsBadReadPtr ( pExPtrs
,
sizeof ( EXCEPTION_POINTERS ) ) ) ;
if ( TRUE == IsBadReadPtr ( pExPtrs
,
sizeof ( EXCEPTION_POINTERS ) ) )
{
TRACE0 ( "GetRegisterString  invalid pExPtrs!\n" ) ;
return ( NULL ) ;
}
#ifdef _X86_
// Этот вызов помещает в стек 48 байт, что может
// представлять проблему, если стек близок к переполнению.
wsprintf(g_szBuff ,
_T ("EAX=%08X EBX=%08X ECX=%08X EDX=%08X ESI=%08X\n")\
_T ("EDI=%08X EBP=%08X ESP=%08X EIP=%08X FLG=%08X\n")\
_T ("CS=%04X DS=%04X SS=%04X ES=%04X ")\
_T ("FS=%04X GS=%04X" ) ,
pExPtrs>ContextRecord>Eax
,
pExPtrs>ContextRecord>Ebx
,
pExPtrs>ContextRecord>Ecx
,
pExPtrs>ContextRecord>Edx
,
pExPtrs>ContextRecord>Esi
,
pExPtrs>ContextRecord>Edi
,
pExPtrs>ContextRecord>Ebp
,
pExPtrs>ContextRecord>Esp
,

ГЛАВА 13

pExPtrs>ContextRecord>Eip
pExPtrs>ContextRecord>EFlags
pExPtrs>ContextRecord>SegCs
pExPtrs>ContextRecord>SegDs
pExPtrs>ContextRecord>SegSs
pExPtrs>ContextRecord>SegEs
pExPtrs>ContextRecord>SegFs
pExPtrs>ContextRecord>SegGs
#elif _AMD64_
wsprintf ( g_szBuff ,
_T ("RAX=%016X RBX=%016X RCX=%016X
_T ("RDI=%016X RBP=%016X RSP=%016X
_T (" R8=%016X R9=%016X R10=%016X
_T ("R13=%016X R14=%016X R15=%016X"
pExPtrs>ContextRecord>Rax
,
pExPtrs>ContextRecord>Rbx
,
pExPtrs>ContextRecord>Rcx
,
pExPtrs>ContextRecord>Rdx
,
pExPtrs>ContextRecord>Rsi
,
pExPtrs>ContextRecord>Rdi
,
pExPtrs>ContextRecord>Rbp
,
pExPtrs>ContextRecord>Rsp
,
pExPtrs>ContextRecord>Rip
,
pExPtrs>ContextRecord>EFlags ,
pExPtrs>ContextRecord>R8
,
pExPtrs>ContextRecord>R9
,
pExPtrs>ContextRecord>R10
,
pExPtrs>ContextRecord>R11
,
pExPtrs>ContextRecord>R12
,
pExPtrs>ContextRecord>R13
,
pExPtrs>ContextRecord>R14
,
pExPtrs>ContextRecord>R15
) ;
#elif _IA64_
#pragma message ( "IA64 NOT DEFINED!!" )
#pragma FORCE COMPILATION ABORT!
#else
#pragma message ( "CPU NOT DEFINED!!" )
#pragma FORCE COMPILATION ABORT!
#endif

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

497

,
,
,
,
,
,
,
) ;

RDX=%016X RSI=%016X\n")\
RIP=%016X FLG=%016X\n")\
R11=%016X R12=%016X\n")\
) ,

return ( g_szBuff ) ;
}
LPCTSTR ConvertSimpleException ( DWORD dwExcept )
{
switch ( dwExcept )
{
case EXCEPTION_ACCESS_VIOLATION
:
return ( _T ( "EXCEPTION_ACCESS_VIOLATION" ) ) ;
см. след. стр.

498

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

break ;
case EXCEPTION_DATATYPE_MISALIGNMENT
:
return ( _T ( "EXCEPTION_DATATYPE_MISALIGNMENT" ) ) ;
break ;
case EXCEPTION_BREAKPOINT
:
return ( _T ( "EXCEPTION_BREAKPOINT" ) ) ;
break ;
case EXCEPTION_SINGLE_STEP
:
return ( _T ( "EXCEPTION_SINGLE_STEP" ) ) ;
break ;
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED
:
return ( _T ( "EXCEPTION_ARRAY_BOUNDS_EXCEEDED" ) ) ;
break ;
case EXCEPTION_FLT_DENORMAL_OPERAND
:
return ( _T ( "EXCEPTION_FLT_DENORMAL_OPERAND" ) ) ;
break ;
case EXCEPTION_FLT_DIVIDE_BY_ZERO
:
return ( _T ( "EXCEPTION_FLT_DIVIDE_BY_ZERO" ) ) ;
break ;
case EXCEPTION_FLT_INEXACT_RESULT
:
return ( _T ( "EXCEPTION_FLT_INEXACT_RESULT" ) ) ;
break ;
case EXCEPTION_FLT_INVALID_OPERATION
:
return ( _T ( "EXCEPTION_FLT_INVALID_OPERATION" ) ) ;
break ;
case EXCEPTION_FLT_OVERFLOW
:
return ( _T ( "EXCEPTION_FLT_OVERFLOW" ) ) ;
break ;
case EXCEPTION_FLT_STACK_CHECK
:
return ( _T ( "EXCEPTION_FLT_STACK_CHECK" ) ) ;
break ;
case EXCEPTION_FLT_UNDERFLOW
:
return ( _T ( "EXCEPTION_FLT_UNDERFLOW" ) ) ;
break ;
case EXCEPTION_INT_DIVIDE_BY_ZERO
:
return ( _T ( "EXCEPTION_INT_DIVIDE_BY_ZERO" ) ) ;
break ;

ГЛАВА 13

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

499

case EXCEPTION_INT_OVERFLOW
:
return ( _T ( "EXCEPTION_INT_OVERFLOW" ) ) ;
break ;
case EXCEPTION_PRIV_INSTRUCTION
:
return ( _T ( "EXCEPTION_PRIV_INSTRUCTION" ) ) ;
break ;
case EXCEPTION_IN_PAGE_ERROR
:
return ( _T ( "EXCEPTION_IN_PAGE_ERROR" ) ) ;
break ;
case EXCEPTION_ILLEGAL_INSTRUCTION
:
return ( _T ( "EXCEPTION_ILLEGAL_INSTRUCTION" ) ) ;
break ;
case EXCEPTION_NONCONTINUABLE_EXCEPTION :
return ( _T ( "EXCEPTION_NONCONTINUABLE_EXCEPTION" ) ) ;
break ;
case EXCEPTION_STACK_OVERFLOW
:
return ( _T ( "EXCEPTION_STACK_OVERFLOW" ) ) ;
break ;
case EXCEPTION_INVALID_DISPOSITION
:
return ( _T ( "EXCEPTION_INVALID_DISPOSITION" ) ) ;
break ;
case EXCEPTION_GUARD_PAGE
:
return ( _T ( "EXCEPTION_GUARD_PAGE" ) ) ;
break ;
case EXCEPTION_INVALID_HANDLE
:
return ( _T ( "EXCEPTION_INVALID_HANDLE" ) ) ;
break ;
case 0xE06D7363
:
return ( _T ( "Microsoft C++ Exception" ) ) ;
break ;
default :
return ( NULL ) ;
break ;
}
}
// Инициализация символьной машины в случае надобности.
void InitSymEng ( void )
{
if ( FALSE == g_bSymEngInit )
см. след. стр.

500

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

{
// Получение параметров символьной машины.
DWORD dwOpts = SymGetOptions ( ) ;
// Включение загрузки информации о номерах строк.
SymSetOptions ( dwOpts
|
SYMOPT_LOAD_LINES
) ;
// Установка флага fInvadeProcess.
BOOL bRet = SymInitialize ( GetCurrentProcess ( ) ,
NULL
,
TRUE
) ;
ASSERT ( TRUE == bRet ) ;
g_bSymEngInit = bRet ;
}
}
// Очистка символьной машины в случае надобности
void CleanupSymEng ( void )
{
if ( TRUE == g_bSymEngInit )
{
VERIFY ( SymCleanup ( GetCurrentProcess ( ) ) ) ;
g_bSymEngInit = FALSE ;
}
}
Для установки собственной функциифильтра исключений просто вызовите
SetCrashHandlerFilter, которая сохраняет указатель на вашу функциюфильтр ис
ключений в статической переменной и вызывает SetUnhandledExceptionFilter для
установки действительного фильтра исключений — CrashHandlerExceptionFilter. Если
вы не укажете модулей, ограничивающих фильтрацию исключений, CrashHandler
ExceptionFilter будет всегда вызывать ваш обработчик исключений независимо от
того, в каком модуле произошла ошибка. Это было сделано намеренно, чтобы вы
могли устанавливать собственный заключительный обработчик исключений един
ственным вызовом API. Лучше всего вызывать SetCrashHandlerFilter пораньше и
обязательно вызывать ее еще раз с параметром NULL прямо перед выгрузкой фильтра,
чтобы мой обработчик мог удалить вашу функциюфильтр. Диаграмма обработ
чика ошибок показана на рис. 131.
Добавление модуля, ограничивающего обработку ошибок, выполняет, функция
AddCrashHandlerLimitModule. Для этого нужно только передать в нее HMODULE нужно
го модуля. Если вы хотите ограничить обработку ошибок несколькими модуля
ми, просто вызовите AddCrashHandlerLimitModule для каждого из них. Массив опи
сателей модулей создается в куче основного процесса.
Изучая листинг 134, вы увидите, что я не вызываю функций стандартной биб
лиотеки C. Поскольку функции обработчика ошибок вызываются только в экст
раординарных ситуациях, я не могу быть уверен в стабильной работе библиотечных
функций. Для освобождения выделенной памяти я применяю автоматический

ГЛАВА 13

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

501

статический класс, деструктор которого вызывается при выгрузке BUGSLAYER
UTIL.DLL. Я также предоставляю две функции, обеспечивающие получение размера
массива ограничивающих модулей в элементах и копирование массива: GetLimit
ModuleCount и GetLimitModulesArray. Реализацию функции RemoveCrashHandlerLimitModule
(удаление модуля, ограничивающего обработку ошибок) я оставил вам.
Необработанное исключение

Обработчик ошибок
CrashHandler API (BugslayerUtil.dll)

SetCrashHandlerFilter(
ExceptCallBack);

g_pfnOrigFilt =
SetUnhandledExceptionFilter(
CrashHandlerExceptionFilter);
CrashHandlerExceptionFilter

Обработчик
исключений
ExcepCallBack

Да

Список модулей пуст?
Исключение в определенном модуле?

Нет

Первоначальный
фильтр исключений
g_pfnOrigFilt

Список модулей

Рис. 131.

Диаграмма обработчика ошибок

Некоторый интерес представляет то, как я инициализирую сервер символов
DBGHELP.DLL. Код обработчика ошибок может быть вызван в любое время, поэтому
мне был нужен способ загрузки всех модулей процесса в момент ошибки. Это
выполняется автоматически функцией SymInitialize, которая получает третий па
раметр, fInvadeProcess, имеющий значение TRUE.
Еще один интересный момент — как я работаю с символами ANSI в мире Uni
code. Так как код CrashHandler, представленный мной в первом издании, оказался
очень популярным и применяется в бесчисленном множестве программ, я дол
жен был учесть потребности программистов, желающих работать с новым CRASH
HANDLER.CPP в существующих проектах. Я не хотел использовать для преобразо
вания символов свою крупную оболочку SYMBOLENGINE.LIB для символьной ма
шины, потому что это препятствовало бы непосредственной модернизации кода.
В конце концов я решил выполнять большинство преобразований при помощи
функции wsprintf с форматом %S, который при компиляции Unicode указывает, что
параметр является строкой ANSI.
В тех фрагментах, где я должен был выполнять преобразование сам, я решил
выделять память в стеке при помощи функции _alloca, а не в куче стандартной
библиотеки C или ОС, потому что кучи могут быть повреждены и стать причи
ной ошибки. Если ошибка будет вызвана переполнением стека, любой выполняе

502

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

мый мной код, вероятно, приведет к повторной ошибке задолго до того, как про
грамма достигнет одного из вызовов _alloca.

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

CrashHandlerTest.exe caused an EXCEPTION_ACCESS_VIOLATION in module
CrashHandlerTest.exe at 001B:004011D1, Baz()+0088 byte(s),
d:\dev\booktwo\disk\bugslayerutil\tests\crashhandler\crashhandler.cpp,
line 0061+0003 byte(s)
Наибольший интерес представляют функции GetFirstStackTraceString и GetNext
StackTraceString. Как показывают имена, эти функции позволяют вам анализиро
вать стек. Как и в случае APIфункций FindFirstFile и FindNextFile, для анализа всего
стека вы можете вызвать GetFirstStackTraceString и затем продолжить вызывать
GetNextStackTraceString, пока она не вернет FALSE. Второй параметр этих функций
является указателем на структуру EXCEPTION_POINTERS, передаваемым вашей функ
ции обработчика ошибок. Код обработчика ошибок поступает правильно: он кэ
ширует значение, переданное в GetFirstStackTraceString, так что структура EXCEP
TION_POINTERS в вашем обработчике ошибок остается нетронутой на тот случай, если
вы позднее захотите передать указатель на нее в функции записи минидампов.
GetNextStackTraceString на самом деле не использует переданную ей структуру
EXCEPTION_POINTERS, но я не хотел нарушать совместимость CRASHHANDLER.CPP с
программами, которые уже работают с ним.
Первый параметр функций GetFirstStackTraceString и GetNextStackTraceString —
это параметр флагов, позволяющий контролировать объем информации, которую

ГЛАВА 13

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

503

вы желаете видеть в итоговой строке. Если включены все флаги, будет выведено
чтото вроде:

001B:004017FD (0x00000001 0x00000000 0x00894D00 0x00000000)
CrashHandlerTest.exe, wmain()+1407 byte(s), d:\dev\booktwo\disk\bugslayerutil\tes
ts\crashhandler\crashhandler.cpp,
line 0226+0007 byte(s)
В скобках выводятся первые четыре возможных параметра функции. Список
флагов приведен в табл. 131. Некоторые из вас, возможно, удивляются, почему в
качестве одного из вариантов я не включил вывод информации о локальных пе
ременных. Это объясняется двумя причинами. Вопервых, CrashHandler предназ
начен прежде всего для использования клиентами. Если вы не желаете выдавать
секреты, вы, вероятно, не предоставляете своим клиентам PDBфайлы с частной
информацией. Вовторых, локальные переменные, особенно в расширенном виде,
занимают довольно большой объем памяти. Я и так чувствовал, что приближаюсь
к пределам возможностей изза статических буферов, поэтому решил, что описа
ние локальных переменных будет чрезмерным.

Табл. 13-1.

Флаги GetFirstStackTraceString и GetNextStackTraceString

Флаг

Выводимая информация

0

Только адрес стека

GSTSO_PARAMS

Первые четыре возможных параметра

GSTSO_MODULE

Имя модуля

GSTSO_SYMBOL

Имя символа для адреса стека

GSTSO_SRCLINE

Информация об исходном файле и номере строки для адреса стека

Чтобы показать функции GetFirstStackTraceString и GetNextStackTraceString в
действии, я прилагаю к этой книге две тестовых программы: BugslayerUtil\Tests\
CrashHandler выполняет методы CrashHandler, а CrashTest отображает пример ди
алогового окна, которое вы можете вывести при необработанной ошибке. Благо
даря этим двум программам вы должны получить достаточно хорошее представ
ление о том, как использовать представленные мной функции. На рис. 132 пока
зано окно сообщения об ошибке, выводимое программой CrashTest.

Минидампы
Возможно, вы удивляетесь, зачем я продолжаю разработку и сопровождение кода
для манипулирования структурами EXCEPTION_POINTERS в библиотеке CrashHandler,
потому что вы много слышали о минидампах. Я делаю это главным образом по
тому, что не хочу нарушать совместимость моего кода со многими программами,
в которых он уже используется. Однако возможности минидампов настолько уди
вительны, что я уверен, что многие программисты просто заменят код своих об
работчиков ошибок вызовами функций создания минидампов, причем сделают это
так быстро, как только смогут.
Я уже объяснял, как читать файлы минидампов при помощи Microsoft Visual
Studio .NET и WinDBG в главах 7 и 8 соответственно. Теперь я хочу рассказать, как
создавать собственные минидампы прямо из своей программы. Я считаю, что API

504

ЧАСТЬ IV

Рис. 132.

Мощные средства и методы отладки неуправляемого кода

Диалоговое окно программы CrashTest

минидампов — самое полезное средство, предоставленное Microsoft разработчи
кам неуправляемого кода за последние несколько лет после технологии серверов
символов и самой Visual Studio .NET! Однако создание собственных минидампов
имеет свои тонкости, поэтому я покажу вам, как получать самые лучшие мини
дампы, что позволит вам значительно ускорить исправление ошибок.

API-функция MiniDumpWriteDump
Всю работу по созданию минидампов выполняет функция MiniDumpWriteDump, со
держащаяся в DBGHELP.DLL версии 5.1 или более поздней. Это значит, что все
версии этой библиотеки из Microsoft Windows 2000 (до Service Pack 3 включительно)
эту функцию не экспортируют. Кроме того, в MiniDumpWriteDump из библиотек
DBGHELP.DLL до версии 6.0 есть ошибка, приводящая к взаимной блокировке при
записи минидампа из текущего процесса. К счастью, эти версии библиотек рас
пространялись только с Debugging Tools for Windows, поэтому их не должно быть
на машинах пользователей. DBGHELP.DLL уже не имеет ограничений на распрос
транение, поэтому, чтобы гарантировать своим приложениям благополучную
жизнь, включайте в их состав DBGHELP.DLL 6.1.17.1 или ее более позднюю вер
сию и устанавливайте ее в каталог своей программы, но не в каталог %SYSTEM
ROOT%\System32. DBGHELP.DLL входит в пакет Debugging Tools for Windows (т. е.
WinDBG), который вы можете найти на CD. Чтобы получить последнюю версию
DBGHELP.DLL, зайдите на сайт http://www.microsoft.com/ddk/debugging/ и загру
зите Debugging Tools for Windows. После установки всех компонентов вы сможе
те скопировать DBGHELP.DLL из каталога Debugging Tools for Windows.
В следующем фрагменте представлен прототип MiniDumpWriteDump. Имена пара
метров говорят сами за себя, однако я хотел бы коечто отметить. Первый пара
метр, описатель процесса, должен иметь права на чтение и запрос. Так как мно

ГЛАВА 13

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

505

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

BOOL MiniDumpWriteDump ( HANDLE
DWORD
HANDLE
MINIDUMP_TYPE
PMINIDUMP_EXCEPTION_INFORMATION
PMINIDUMP_USER_STREAM_INFORMATION
PMINIDUMP_CALLBACK_INFORMATION

hProcess
,
ProcessId
,
hFile
,
DumpType
,
ExceptionParam ,
UserStreamParam,
CallbackParam );

Четвертый параметр — тип дампа, который вы хотите записать. Похоже, это
перечисление изменяется с каждой версией Debugging Tools for Windows, поэто
му убедитесь, что у вас установлена соответствующая версия Debugging Tools for
Windows и установите компоненты SDK для получения последнего заголовочно
го файла DBGHELP.H. Из документации не совсем ясно, что флаги перечисления
MINIDUMP_TYPE можно объединять, запрашивая запись в дамп дополнительной ин
формации. Что бы вы ни делали, всегда устанавливайте флаг MiniDumpWithHandle
Data для получения информации об описателях.
Если вы работаете над приложениями, к безопасности которых предъявляют
ся повышенные требования, или ваши клиенты очень обеспокоены защитой дан
ных, знайте, что MiniDumpWriteDump может вывести информацию, которую выводить
не следовало бы. Для защиты пользователей Microsoft включила в DBGHELP.DLL
6.1.17.1 и более поздние ее версии два флага: MiniDumpFilterMemory и MiniDump
FilterModulePaths. Первый удаляет из дампа частные данные, которые не требуют
ся для анализа стека, а второй исключает из путей к модулям имена пользовате
лей и важные имена каталогов. Флаг MiniDumpFilterModulePaths очень полезен, од
нако он может затруднить нахождение модулей в минидампе.
Для нас также представляет интерес пятый параметр — ExceptionParam. Для до
бавления в минидамп информации об ошибке ему нужно присвоить значение
указателя на структуру EXCEPTION_POINTERS. В качестве двух последних параметров
вы почти всегда будете передавать NULL. Тем не менее параметр UserStreamParam может
пригодиться, если вам захочется записать в минидамп собственную информацию:
состояние программы, предпочтения пользователей, список объектов и все, что
ваша душа пожелает. Чтение пользовательских данных при помощи функции
MiniDumpReadDumpStream вам придется выполнять самому, но я могу вас обрадовать:
содержание минидампа при этом будет ограничено только вашим воображением.

Укрощение MiniDumpWriteDump
Увидев MiniDumpWriteDump, я сразу же понял, что я должен написать для нее функ
циюоболочку. Это было нужно, вопервых, чтобы скрыть функцию GetProcAddress,
так как я хотел гарантировать работу моего кода под управлением имевшейся
версии Windows 2000, и вовторых, чтобы избежать необходимости открытия файла

506

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

перед каждым вызовом MiniDumpWriteDump. Создав первую версию простой оболоч
ки, я понял, что мне никогда больше не придется ничего делать, кроме установки
параметра ExceptionParam для указания на обрабатываемую при ошибке структуру
EXCEPTION_POINTERS,. Моя функция записи минидампа из вашей функции обработ
чика ошибок называется CreateCurrentProcessCrashDump. Я также написал функцию
IsMiniDumpFunctionAvailable, которая возвращает TRUE, если MiniDumpWriteDump при
сутствует в адресном пространстве. Вы можете найти обе функции в файле MINI
DUMP.CPP утилиты BugslayerUtil.
Все шло отлично, пока однажды я не решил написать функцию, которая запи
сывала бы минидамп в любой момент выполнения программы, а не только при
ошибке. Мы работали над серверным приложением, и нам хотелось иметь воз
можность записи минидампа при конкретном внешнем событии. Благодаря это
му мы могли бы изучать состояние приложения постфактум, не подключая к ком
пьютеру отладчик. Увы, минидампы, создаваемые MiniDumpWriteDump, не всегда были
удобочитаемы.
Отображая эти минидампы, WinDBG всегда выводил нечто, выглядевшее как
фальшивый стек вызовов. Visual Studio .NET работала лучше, но иногда показыва
ла странные плавающие стеки, даже когда у меня повсюду были отличные симво
лы. Почесав немного затылок, я понял, в чем дело. MiniDumpWriteDump записывала
стек вызовов для собственного потока, начинающийся глубоко внутри самой
MiniDumpWriteDump. Хотя у меня и были отличные символы, изучать код было очень
сложно.
Так как я записывал дамп, а не обрабатывал ошибку, я несколько недоумевал.
Все файлы дампов, которые я записывал при ошибке, были прекрасно сформи
рованы и читались обоими отладчиками. Конечно, чтобы WinDBG читал настоя
щие файлы дампа ошибки, я должен был использовать команды .ecxr;kp для со
здания структуры информации об исключении и просмотра стека. Я подумал, что
для получения файла минидампа с хорошими стеками вызовов можно реализо
вать похожую идею: подделать структуру MINIDUMP_EXCEPTION_INFORMATION, заполнив
ее теми же значениями, которые имеют место при ошибке.
Вся проблема подделки структуры MINIDUMP_EXCEPTION_INFORMATION сводится к
занесению правильной информации о регистрах в структуру CONTEXT, чтобы под
дельная ошибка казалась отладчикам настоящей. После многих проб и ошибок я
написал функцию SnapCurrentProcessMiniDump. Теперь при получении минидампа в
любое время всегда будет выполняться правильный анализ стека. Вам, возможно,
захочется изучить код листинга 135, потому что он работает довольно интересно.
Первая проблема заключалась в том, откуда брать регистры и какое значение
надо присваивать адресу исключения. В конце концов я решил, что мне нужны те
же значения регистров, которые имеют место при вызове моей функции SnapCurrent
ProcessMiniDump. Чтобы получить правильные значения регистров, я должен был
добраться до них раньше кода, сгенерированного компилятором, поэтому я ис
пользовал соглашение вызова naked.
В итоге я создал структуру CONTEXT в стеке и написал на встроенном языке ас
семблера копирование регистров в соответствующие поля структуры. Первую
ошибку я допустил сам, потому что копировал 16разрядные сегментные регист
ры в поля CONTEXT, упустив из вида, что поля для этих сегментов были 32разряд

ГЛАВА 13

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

507

ными. В результате я оставлял мусор в старших словах полей. Для исправления
этой ошибки мне пришлось сначала копировать сегментные регистры в EAX и потом
сохранять его в полях структуры. Значения EBP и ESP нужно было находить иначе,
потому что с их помощью в начале функции создавался кадр стека, но и это не
вызвало проблем. В структуру заносятся те же значения ESP и EBP, какими они были
во время вызова SnapCurrentProcessMiniDump. Макрос SNAPPROLOG в листинге 135
представляет собой пролог, а SNAPEPILOG — эпилог, необходимые для функций с
соглашением вызова naked. Единственный регистр, значение которого не заносится
в CONTEXT во время пролога, — это регистр EIP, для которого потребовалось чуть
больше работы.
Я думал, что регистров общего назначения хватит, но все равно не исключа
лась возможность того, что для правильного отображения пользовательской ин
формации отладчику нужны и другие регистры, например, регистры для работы
с числами с плавающей точкой или дополнительные регистры. Поэтому я решил
получить и их значения, вызвав GetThreadContext для выполняющегося потока. Так
как мой код не изменяет эти регистры, я получаю их действительные значения в
момент вызова. Конечно, я передаю в функцию GetThreadContext адрес другой струк
туры CONTEXT, иначе я просто перезаписал бы уже сохраненные значения регист
ров. После получения регистров через GetThreadContext я копирую в итоговую струк
туру значения, сохраненные мной ранее в первой структуре.
Итак, я получил корректные значения регистров, имевшие место при вызове
SnapCurrentProcessMiniDump. Сначала я хотел поместить в стек адрес возврата и как
значение регистра EIP, и как адрес исключения. Получить адрес возврата уже просто,
так как Microsoft задокументировала внутреннюю функцию _ReturnAddress. С ее
помощью вы можете получить адрес возврата из любого места функции. Чтобы
задействовать _ReturnAddress, вам надо добавить в свой код две следующих стро
ки, чтобы компилятор не жаловался, что функция не определена. Я предпочитаю
размещать эти строки в прекомпилированном заголовочном файле, чтобы они
были доступны глобально.

extern "C" void * _ReturnAddress ( void ) ;
#pragma intrinsic ( _ReturnAddress )
Благодаря тому, что остальные регистры у меня имеют те же значения, какие
имели до вызова, очень немногие люди заметили бы разницу, если б я просто
использовал адрес возврата как значение EIP и адрес исключения. Однако я по
ступил более предусмотрительно, изучив значения, отстоящие на несколько байт
от адреса возврата, на предмет наличия идентификаторов операций 0xE8 и 0xFF,
определяющих команды ближнего и дальнего вызовов соответственно. После
поправки все регистры имеют абсолютно правильные значения, такие же, как и
при вызове функции. Определение типа CALL вы можете увидеть в CalculateBegin
ningOfCallInstruction.
Остальные действия после получения нужных регистров заключаются в запол
нении структуры MINIDUMP_EXCEPTION_INFORMATION, открытии описателя файла и вы
зове MiniDumpWriteDump. Все это вы можете увидеть в функции CommonSnapCurrent
ProcessMiniDump в листинге 135.

508

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

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

Листинг 13-5.

SnapCurrentProcessMiniDump и ее друзья из файла MINIDUMP.CPP

// Ниже приведены фрагменты из файла MINIDUMP.CPP, иллюстрирующие
// работу функции SnapCurrentProcessMiniDump.
// Расстояние (в байтах) от адреса возврата до команд
// ближнего и дальнего вызовов. Эти значения используются
// в функции CalculateBeginningOfCallInstruction.
#define k_CALLNEARBACK 5
#define k_CALLFARBACK 6
// Общий пролог для функций SnapCurrentProcessMiniDumpA и
// SnapCurrentProcessMiniDumpW, использующих соглашение naked.
#define SNAPPROLOG(Cntx)
__asm PUSH EBP
/* Явное сохранение регистра EBP. */
__asm MOV EBP , ESP
/* Формирование кадра стека.
*/
__asm SUB ESP , __LOCAL_SIZE
/* Место для локальных переменных.*/
/* Копирование значений всех легкодоступных регистров. */
__asm MOV Cntx.Eax , EAX
__asm MOV Cntx.Ebx , EBX
__asm MOV Cntx.Ecx , ECX
__asm MOV Cntx.Edx , EDX
__asm MOV Cntx.Edi , EDI
__asm MOV Cntx.Esi , ESI
/* Я обнуляю весь регистр EAX, но записываю значения сегментов
*/
/* только в его младшее слово. Это гарантирует правильную
*/
/* инициализацию старших слов полей сегментных регистров
*/
/* в структуре CONTEXT, так как на самом деле они 32разрядные.
*/
__asm XOR EAX , EAX
__asm MOV AX , GS
__asm MOV Cntx.SegGs , EAX
__asm MOV AX , FS
__asm MOV Cntx.SegFs , EAX
__asm MOV AX , ES
__asm MOV Cntx.SegEs , EAX
__asm MOV AX , DS
__asm MOV Cntx.SegDs , EAX
__asm MOV AX , CS
__asm MOV Cntx.SegCs , EAX
__asm MOV AX , SS
__asm MOV Cntx.SegSs , EAX
/* Получение предыдущего значения EBP. */

\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\
\

ГЛАВА 13

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

__asm MOV EAX , DWORD PTR [EBP]
__asm MOV Cntx.Ebp , EAX
/* Получение предыдущего значения ESP. */
__asm MOV EAX , EBP
/* Предыдущее значение регистра ESP на два
/* двойных слова превышает значение EBP.
__asm ADD EAX , 8
__asm MOV Cntx.Esp , EAX
/* Сохранение изменяемых регистров. */
__asm PUSH ESI
__asm PUSH EDI
__asm PUSH EBX
__asm PUSH ECX
__asm PUSH EDX
// Общий эпилог для функций SnapCurrentProcessMiniDumpA и
// SnapCurrentProcessMiniDumpW, использующих соглашение naked.
#define SNAPEPILOG(eRetVal)
__asm POP
EDX
/* Восстановление
__asm POP
ECX
/* сохраненных регистров.
__asm POP
EBX
__asm POP
EDI
__asm POP
ESI
__asm MOV
EAX , eRetVal /* Возвращаемое значение.
__asm MOV
ESP , EBP
/* Восстановление указателя стека.
__asm POP
EBP
/* Восстановление регистра EBP.
__asm RET
/* Возврат в вызвавшую функцию.

509

\
\
\
\
*/ \
*/ \
\
\
\
\
\
\
\

\
*/ \
*/ \
\
\
\
*/ \
*/ \
*/ \
*/

BSUMDRET CommonSnapCurrentProcessMiniDump ( MINIDUMP_TYPE eType
,
LPCWSTR
szDumpName ,
PCONTEXT
pCtx
)
{
// Надеемся на лучшее.
BSUMDRET eRet = eDUMP_SUCCEEDED ;
// Пытался ли я уже получить экспортируемую фцию MiniDumpWriteDump?
if ( ( NULL == g_pfnMDWD ) && ( eINVALID_ERROR == g_eIMDALastError))
{
if ( FALSE == IsMiniDumpFunctionAvailable ( ) )
{
eRet = g_eIMDALastError ;
}
}
// Если указатель на MiniDumpWriteDump равен NULL, выполняется выход.
if ( NULL == g_pfnMDWD )
{
eRet = g_eIMDALastError ;
}
if ( eDUMP_SUCCEEDED == eRet )
см. след. стр.

510

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

{
//
//
//
//
//

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

EXCEPTION_RECORD stExRec ;
EXCEPTION_POINTERS stExpPtrs ;
MINIDUMP_EXCEPTION_INFORMATION stExInfo ;
// Обнуление
ZeroMemory (
ZeroMemory (
ZeroMemory (
//
//
//
//
//

всех отдельных значений структур.
&stExRec , sizeof ( EXCEPTION_RECORD )) ;
&stExpPtrs , sizeof ( EXCEPTION_POINTERS ) ) ;
&stExInfo ,sizeof(MINIDUMP_EXCEPTION_INFORMATION));

Присвоение адресу исключения начала команды CALL.
Интересно, что код исключения задавать не требуется.
При открытии файла .DMP, созданного этим фрагментом
программы, в VS.NET код исключения будет иметь вид:
0x00000000: The operation completed successfully.

// Запрещение предупреждения C4312: 'type cast' : conversion
// from 'DWORD' к типу 'PVOID' of greater size (преобразование
// типа 'DWORD' к типу 'PVOID', имеющему больший размер).
#pragma warning ( disable : 4312 )
stExRec.ExceptionAddress = (PVOID)(pCtx>Eip) ;
#pragma warning ( default : 4312 )
// Заполнение структуры stExpPtrs (типа EXCEPTION_POINTERS).
stExpPtrs.ContextRecord = pCtx ;
stExpPtrs.ExceptionRecord = &stExRec ;
// Наконец я заполняю структуру информации об исключении.
stExInfo.ThreadId = GetCurrentThreadId ( ) ;
stExInfo.ClientPointers = TRUE ;
stExInfo.ExceptionPointers = &stExpPtrs ;
// Создание файла для записи минидампа.
HANDLE hFile = CreateFile ( szDumpName
GENERIC_READ | GENERIC_WRITE
FILE_SHARE_READ
NULL
CREATE_ALWAYS
FILE_ATTRIBUTE_NORMAL
NULL
ASSERT ( INVALID_HANDLE_VALUE != hFile ) ;
if ( INVALID_HANDLE_VALUE != hFile )
{
// Запись файла минидампа.

,
,
,
,
,
,
) ;

ГЛАВА 13

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

511

BOOL bRetVal = g_pfnMDWD ( GetCurrentProcess ( ) ,
GetCurrentProcessId ( ) ,
hFile
,
eType
,
&stExInfo
,
NULL
,
NULL
) ;
ASSERT ( TRUE == bRetVal ) ;
if ( TRUE == bRetVal )
{
eRet = eDUMP_SUCCEEDED ;
}
else
{
eRet = eMINIDUMPWRITEDUMP_FAILED ;
}
// Закрытие файла.
VERIFY ( CloseHandle ( hFile ) ) ;
}
else
{
eRet = eOPEN_DUMP_FAILED ;
}
}
return ( eRet ) ;
}
BSUMDRET __declspec ( naked )
SnapCurrentProcessMiniDumpW ( MINIDUMP_TYPE eType
,
LPCWSTR
szDumpName )
{
// Место хранения значений регистров,
// имевших место при вызове этой функции.
CONTEXT stInitialCtx ;
// Место хранения заключительных значений регистров.
CONTEXT stFinalCtx ;
// Возвращаемое значение.
BSUMDRET
eRet ;
// Локальное возвращаемое значение типа Boolean.
BOOL
bRetVal ;
// Выполнение пролога.
SNAPPROLOG ( stInitialCtx ) ;
eRet = eDUMP_SUCCEEDED ;
// Проверка параметрастроки.
ASSERT ( FALSE == IsBadStringPtr ( szDumpName , MAX_PATH ) ) ;
if ( TRUE == IsBadStringPtr ( szDumpName , MAX_PATH ) )
см. след. стр.

512

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

{
eRet = eBAD_PARAM ;
}
if ( eDUMP_SUCCEEDED == eRet )
{
// Обнуление структуры заключительного контекста.
ZeroMemory ( &stFinalCtx , sizeof ( CONTEXT ) ) ;
// Я хочу получить все характеристики контекста.
stFinalCtx.ContextFlags = CONTEXT_FULL
CONTEXT_CONTROL
CONTEXT_DEBUG_REGISTERS
CONTEXT_EXTENDED_REGISTERS
CONTEXT_FLOATING_POINT

|
|
|
|
;

// Получение значений всех регистров для контекста данного потока.
bRetVal = GetThreadContext ( GetCurrentThread ( ) ,&stFinalCtx);
ASSERT ( TRUE == bRetVal ) ;
if ( TRUE == bRetVal )
{
COPYKEYCONTEXTREGISTERS ( stFinalCtx , stInitialCtx ) ;
// Получение адреса возврата и адреса команды call,
// вызвавшей данную функцию. Всем остальным регистрам
// присвоены значения, которые они имели до вызова,
// поэтому указатель команд устанавливается аналогично.
UINT_PTR dwRetAddr = (UINT_PTR)_ReturnAddress ( ) ;
bRetVal = CalculateBeginningOfCallInstruction ( dwRetAddr );
ASSERT ( TRUE == bRetVal ) ;
if ( TRUE == bRetVal )
{
// Установка указателя команд на начало команды call.
stFinalCtx.Eip = (DWORD)dwRetAddr ;
// Вызов общей функции, выполняющей
// фактическую запись минидампа.
eRet = CommonSnapCurrentProcessMiniDump ( eType
,
szDumpName ,
&stFinalCtx );
}
else
{
eRet = eGETTHREADCONTEXT_FAILED ;
}
}
}
// Эпилог.
SNAPEPILOG ( eRet ) ;
}

ГЛАВА 13

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

513

// Я должен был вынести этот блок за пределы функций
// SnapCurrentProcessMiniDumpA/W, так как они используют
// соглашение вызова naked и поэтому не могут работать с SEH.
BOOL CalculateBeginningOfCallInstruction ( UINT_PTR & dwRetAddr )
{
BOOL bRet = TRUE ;
// При обработке исключений я обеспечиваю полную защиту.
// Мне нужно быть чрезвычайно внимательным, так как я читаю
// стек и могу исказить его вершину. Я не хочу, чтобы вызов
// функции SnapCurrentProcessMiniDump приводил к краху
// приложения, поэтому я обрабатываю все возможные исключения.
__try
{
BYTE * pBytes = (BYTE*)dwRetAddr ;
if ( 0xE8 == *(pBytes  k_CALLNEARBACK) )
{
dwRetAddr = k_CALLNEARBACK ;
}
else if ( 0xFF == *(pBytes  k_CALLFARBACK) )
{
dwRetAddr = k_CALLFARBACK ;
}
else
{
bRet = FALSE ;
}
}
__except ( EXCEPTION_EXECUTE_HANDLER )
{
bRet = FALSE ;
}
return ( bRet ) ;
}

Резюме
В этой главе я описал обработчики ошибок, включающие в себя обработчики
исключений и фильтры необработанных исключений. Обработчики ошибок по
зволяют получить более подробную информацию об ошибках и оставить у пользо
вателей при этом более благоприятное впечатление. Одно из условий более быс
трой отладки состоит в своевременном получении необходимой информации —
как раз для этого и нужны обработчики ошибок.
Исключения C++ и исключения SEH иногда путают. Исключения C++ входят в
спецификацию языка C++, в то время как исключения SEH обеспечивает ОС; эти
два типа обработки исключений абсолютно разные, хотя и тесно сплетены в де
талях реализации.
Надеюсь, я смог показать вам всю голую правду об обработке исключений C++,
и теперь вы дважды подумаете, прежде чем решитесь использовать их в своем

514

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

приложении. Вопервых, изза дополнительных расходов они ухудшают быстро
действие программ. Однако гораздо хуже то, что в реализации компиляторов
Microsoft блок catch (...) «съедает» ошибки SEH. Фактически это делает блок catch
(...) самой плохой конструкцией программирования, которую только можно
вообразить. Кроме того, нужно избегать функции _set_se_translator, потому что
она работает не так, как многие предполагают. Возможно, некоторые пуристы
объектноориентированного программирования и языка C++ найдут мои взгля
ды на исключения C++ излишне жесткими, но мне кажется, что единственная
чистота, которая имеет значение, — это своевременная разработка качественных
программ. Если вы откажетесь от обработки исключений C++, ваши программы
станут чистыми настолько, насколько это вообще возможно.
Своим существованием обработчики ошибок обязаны магической функции
SetUnhandledExceptionFilter, позволяющей установить конечный фильтр исключе
ний SEH, который обеспечивает получение управления прямо перед появлением
диалогового окна сообщения об ошибке и записать бесценную информацию о
причинах проблемы. Приведенные мной функции API CrashHandler облегчат уста
новку фильтров необработанных исключений и выполнят за вас всю грязную
работу по преобразованию информации об ошибке, благодаря чему вы сможете
сосредоточиться на отображении информации и уникальных компонентах свое
го приложения.
В конце главы я рассказал про одну чрезвычайно полезную новую APIфунк
цию — MiniDumpWriteDump. Она имеет несколько недостатков, но я о них позабо
тился, сделав создание и последующее изучение минидампов максимально удоб
ным. Вооружившись минидампами, созданными в самые подходящие моменты, вы
сможете решать любые проблемы своих клиентов.

Г Л А В А

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

После драйверов устройств сложнее всего отлаживать код служб Microsoft Win
dows и DLL, загружаемых в службы. Может показаться, что, раз службы являются
всего лишь процессами пользовательского режима без UI, то отлаживать их столь
же легко, как и консольные приложения. Увы, все не так просто. На самом деле со
службами Windows и DLL, загружаемыми в службы, связано столько подводных
камней, особенно касающихся защиты в Windows, что при работе с ними вам может
захотеться рвать на себе волосы. При появлении Microsoft Windows NT очень
немногие разработчики писали службы или вообще знали, что они существуют.
Однако в сегодняшнем мире COM+, Microsoft Internet Information Services (IIS),
расширений Microsoft Exchange Server и Windows Clustering многим разработчи
кам придется иметь дело со службами. И отлаживать их.
В этой главе я представлю обзор основных характеристик служб. Чтобы по
нять, как отлаживать службы и DLL, загружаемые в службы, такие как ISAPIфиль
тры и расширения, надо знать, как службы работают. Затем я поясню аспекты,
напрямую связанные с отладкой служб. По мере прохождения этапов отладки
службы я буду отмечать моменты, касающиеся определенных технологий служб
Microsoft.

Основы служб
Служба характеризуется тремя основными свойствами:
쐽 служба должна работать постоянно, даже если в системе компьютера никто не
зарегистрирован или при первоначальном запуске компьютера;

516

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

쐽 служба не имеет UI;
쐽 службу могут контролировать и управлять ею как локальные, так и удаленные
клиенты.
Решая, как реализовать свое приложение: в виде службы или обычного прило
жения пользовательского режима, — спросите себя, предъявляются ли к пробле
ме, которую вы хотите решить, эти три требования. Если да, надо реализовать
приложение как службу. А если вы решили написать службу (и хотите ее отлажи
вать), убедитесь, что вы четко понимаете работу служб. Сведений, приведенных в
этом разделе, хватит, чтобы составить представление о том, с чем вам придется
столкнуться. Если хотите узнать о службах больше, ознакомьтесь с прекрасной
книгой Джеффри Рихтера (Jeffrey Richter) и Джейсона Кларка (Jason Clark) «Prog
ramming ServerSide Applications for Microsoft Windows 2000» (Microsoft Press, 2000 г.)1.
Прекрасный пример случая, когда следует писать службу, — создание прило
жения, контролирующего источник бесперебойного питания (UPS). Все, что нужно
делать ПО для UPS, — следить, когда UPS сообщит о сбое питания в сети, а когда
заряд батареи подойдет к концу, программе следует инициировать управляемое
выключение (controlled shutdown). Очевидно, что если ПО для UPS не работает
постоянно (первый критерий в решении, должно ли приложение быть службой),
выключения не произойдет, и, когда в UPS закончится заряд батарей, компьютер
просто остановится. В ПО для UPS нет необходимости в UI (второй критерий),
так как ему лишь надо выполняться в фоновом режиме, следя за UPS. Наконец, если
вы работаете над системой бесперебойного питания для хранилищ данных, сис
темные администраторы определенно захотят проверять состояние удаленных UPS
(третий критерий).
Пока все довольно просто. Теперь приступим к работе служб. Первый аспект,
о котором я расскажу, — специальные функции API, вызываемые для превраще
ния обычного процесса пользовательского режима в службу.

API
Некоторые качества служб потребуют от вас определенных действий, чтобы при
способиться к ним. Вопервых, не важно, какую точку входа вы используете в служ
бах: main или WinMain. Поскольку службы не имеют UI, точки входа для консоль
ных приложений или приложений с GUI взаимозаменяемы.
Внутри main или WinMain прежде всего следует вызвать функцию API StartService
CtrlDispatcher. Ей передается структура SERVICE_TABLE_ENTRY, в которой указывает
ся имя и главная точка входа службы. Диспетчер управления службами (Service
Control Manager, SCM), запускающий все службы, с которым в конечном счете
общается StartServiceCtrlDispatcher, чтобы установить вашу службу, является сред
ством ОС, которое, как следует из его названия, управляет всеми службами. Если
ваша служба не вызовет StartServiceCtrlDispatcher в течение 30 секунд с момента
запуска, SCM завершит ее работу. Как вы увидите ниже, такое ограничение по
времени может сделать запуск отладки чуть интереснее.

1

Рихтер Дж., Кларк Дж. Программирование серверных приложений для Microsoft
Windows 2000. — М.: «Русская Редакция», 2001. — Прим. перев.

ГЛАВА 14

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

517

Когда вы вызываете SCM, он создает поток для вызова точки входа вашей службы.
К точке входа службы предъявляется одно жесткое требование: нужно зарегист
рировать обработчик через RegisterServiceCtrlHandlerEx и вызвать SetServiceStatus
в течение 82 секунд с момента запуска. Если за это время служба не выполняет
вызовов, SCM считает, что в службе произошел сбой, хотя и не завершает ее. Если
в конце концов служба вызовет RegisterServiceCtrlHandlerEx, она продолжит вы
полнение в нормальном режиме. Считая, что произошел сбой, SCM должен бы за
вершить работу службы, но этого не происходит, — такое поведение, каким бы
странным оно ни казалось, облегчает отладку выполняющейся службы.
RegisterServiceCtrlHandlerEx принимает еще другой указатель — на функцию
обработчик. SCM вызывает функциюобработчик для управления рабочими харак
теристиками службы в таких операциях, как остановка, приостановка или возоб
новление.
Когда служба переходит между состояниями, запускаясь, останавливаясь и
приостанавливаясь, она общается с SCM через функцию API SetServiceStatus. Боль
шинству служб надо просто вызвать SetServiceStatus и инициировать основное
состояние, в которое они переходят, — в этой функции нет ничего особенного.
Я сгладил некоторые подробности, связанные с функциями API, но обычно
вызовы StartServiceCtrlDispatcher, RegisterServiceCtrlHandlerEx и SetServiceStatus —
все, что нужно ОС от вашей службы, чтобы обеспечить ее работоспособность. За
метьте, я ничего не упомянул о требованиях к коммуникационным протоколам,
используемым службой для связи между написанным вами UI контроллера и ва
шей службой. К счастью, службы имеют доступ ко всем обычным функциям Win
dows API, так что вы вправе использовать проецируемые в память файлы (memory
mapped files), почтовые ящики (mail slots), именованные каналы (named pipes) и
т. д. В службах вам действительно доступны те же варианты, что и в обычном меж
процессном взаимодействии. Самая сложная проблема со службами, как я гово
рил в начале главы, — это защита.

Защита
Если не указать иное, службы выполняются под специальной учетной записью
System. Поскольку Windows для всех объектов реализует защиту на уровне пользо
вателей, учетная запись System допустима для машины, а не для сети в целом.
Следовательно, процесс под учетной записью System не имеет доступа к сетевым
ресурсам. Для многих служб, скажем, для упомянутого выше примера с UPS, про
блем защиты в процессе разработки может не возникнуть. Но, если вы, например,
пытаетесь разделить проецируемую память от службы к клиентскому приложению
с UI, а защита установлена неправильно, вы столкнетесь с ошибками нарушения
прав доступа от клиентских приложений, когда они будут пытаться проецировать
общую память.
К сожалению, отладкой проблем защиты не решить — вам придется обеспе
чить программирование служб и клиентских приложений с правильно настроенной
защитой. Полное описание программирования защиты в Windows займет отдель
ную книгу, так что приготовьтесь провести некоторое время, планируя програм
мирование защиты с самого начала разработки. Чтобы у вас сложилось представ
ление о диапазоне нюансов с защитой в службах, настоятельно рекомендую ста

518

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

тью Фрэнка Кима (Frank Kim) «Why Do Certain Win32 Technologies Misbehave in
Windows NT Services?» в мартовском номере «Microsoft Systems Journal» за 1998 год.
Есть и другие прекрасные ресурсы: рубрика Кейта Брауна (Keith Brown) «Security
Briefs» в «Microsoft Systems Journal» и его книга «Programming Windows Security»
(AddisonWesley, 2000). Наконец, одна из лучших книг о реальном мире защиты
Windows — «Writing Secure Code, Second Edition» Майкла Говарда (Michael Howard)
и Дэвида Лебланка (David LeBlanc) (Microsoft Press, 2003)2.
Теперь, пронесшись вихрем по службам, обратимся к сердцу этой главы — от
ладке служб.

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

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

Службы COM+
Если вы собираете службу COM+ с помощью Active Template Library (ATL), вам ничего
не нужно делать с безопасностью. По умолчанию ATL запускается как исполняе
мый файл пользовательского режима пока вы не зарегистрируете свое приложе
ние с параметром командной строки Service.

2

Говард M., Лебланк Д. Защищенный код. — М.: Русская Редакция, 2003. — Прим. перев.

ГЛАВА 14

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

519

ISAPI-фильтры и расширения
Экспортируемые функции, которые надо предоставить для фильтров и расшире
ний, довольно просты, и можно легко создать тестовую программу, действующую
как фиктивная IISсистема. Вы можете протестировать все свои базовые алгорит
мы в контролируемой среде, так что их можно полностью отладить до запуска
службы в IIS.

Exchange Server
Можно собирать службы Exchange Server, запускающиеся в виде консольных при
ложений, используя вспомогательные функции из WINWRAP.LIB. Запуск службы
со стартовым параметром notserv вызовет ее исполнение в виде обычного про
цесса; notserv должен быть первым среди указанных параметров.

Отладка службы
После тестирования и отладки общей логики можете приступать к отладке ваше
го кода, выполняющегося как служба. Вся первоначальная отладка должна прохо
дить в системе, где все под вашим контролем. В идеале нужна вторая машина ря
дом с главной машиной для разработки, которую можно использовать для перво
начальной отладки. На второй машине должна быть та же по версии и особенно
стям система Windows, что рекомендуется пользователям для среды, в которой будет
работать ваша служба. Цель отладки базового кода — проверка основной логики,
тогда как предварительная отладка службы выполняется, чтобы «перетряхнуть»
основной код, относящийся к службе. В ходе отладки вашего первого кода служ
бы следует выполнить четыре задачи:
쐽 включить Allow Service To Interact With Desktop;
쐽 установить идентификационные данные службы;
쐽 подключиться к службе;
쐽 отладить стартовый код.

Включение Allow Service To Interact With Desktop
Независимо от типа отлаживаемой службы следует включить Allow Service To Interact
With Desktop (Разрешить взаимодействие с рабочим столом) на вкладке Log On
(Вход в систему) диалогового окна Properties (Свойства) службы. Хотя в службе
не должно быть элементов UI, уведомления утверждений (assertion notifications),
которые позволяют получить управление в отладчике, очень помогут. Уведомле
ния утверждений в сочетании с прекрасным регистрирующим кодом (logging code),
таким, какой предоставляет вам ATL для записи в журнал событий, могут облег
чить отладку служб.
На начальных стадиях отладки я включаю диалог утверждений SUPERASSERT,
чтобы быстро оценить общее состояние моего кода. (О SUPERASSERT см. главу 3.)
Но по мере выполнения службы я устанавливаю параметры утверждений так, чтобы
все утверждения проходили только через операторы трассировки.
Пока я не уверюсь в коде службы, я обычно оставляю параметр Allow Service
To Interact With Desktop включенным. Одну мерзкую ошибку, встретившуюся в
написанной мною както службе, я долго не мог обнаружить, поскольку я отклю

520

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

чил его при выведенном информационном окне. Поскольку защита ОС не позво
ляет обычным службам выводить информационные окна, моя служба просто за
висала. До того как отключить Allow Service To Interact With Desktop, я дважды
удостоверяюсь, что моя служба (и все используемые ею DLL) не вызывает инфор
мационные окна, проверяя с помощью DUMPBIN/IMPORTS, что ни MessageBoxA, ни
MessageBoxW не импортируются там, где я этого не жду.
Если для своих утверждений вы используете SUPERASSERT, вам повезло. Даже когда
вы забываете отключить его диалоговые уведомления, прежде чем отобразить
замечательное диалоговое окно с уведомлением, SUPERASSERT проверяет, что про
цесс выполняется с видимым рабочим столом. Вообще эта особенность столь
полезна, что я инкапсулировал ее в функции BSUIsInteractiveUser (BSUFUNCTI
ONS.CPP) из BugslayerUtil.DLL.

Установка идентификационных данных службы
Чтобы избежать проблем с защитой при попытках заставить службу работать,
можно установить идентификационные данные службы. По умолчанию все службы
выполняются под учетной записью System, которая иногда называется учетной за
писью LocalSystem. Однако вы вправе настроить службу на работу под учетной
записью пользователя с более высоким уровнем доступа, скажем, члена группы с
расширенными правами.
В диалоговом окне Properties вашей службы щелкните вкладку Log On. Устано
вите переключатель This Account (С учетной записью), щелкните кнопку Browse
(Обзор) и выберите нужную учетную запись из диалогового окна Select User (Вы
бор: Пользователь). Выбрав пользователя, введите и подтвердите пароль для этой
учетной записи. Для исполняемых служб COM+ установить идентификационные
данные для регистрации позволяет также DCOMCNFG.EXE.

Подключение к службе
После запуска службы отладка обычно не так сложна. Все, что надо сделать, —
подключиться к процессу службы из отладчика Microsoft Visual Studio .NET. В за
висимости от службы и сложности кода подключение к службе из отладчика мо
жет оказаться единственным, что нужно сделать для отладки. Чтобы подключить
ся к активному процессу из отладчика Visual Studio .NET, сделайте так.
1. Запустите DEVENV.EXE.
2. Выбрав Debug Processes из меню Tools, откройте диалоговое окно Processes.
3. Установите флажок Show System Processes и щелкните кнопку Refresh, чтобы
увидеть все процессы службы. Выберите из списка процессы, которые нужно
отладить, и щелкните кнопку Attach. Если при щелчке кнопки Attach нажать
любую клавишу Ctrl, вы автоматически начнете отладку неуправляемого кода,
пропустив диалог Attach To Process.
4. Если появился диалог Attach To Process, убедитесь, что установлен вариант Native
и щелкните OK.
Прекрасная новинка отладчика Visual Studio .NET: теперь вы можете не повто
рять все предыдущие действия каждый раз, когда надо подключиться к службе,
потому что вы можете создать проект, выполняющий подключение к службе, ког

ГЛАВА 14

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

521

да вы приступаете к отладке. Давайте создадим специальные проекты подключе
ния (обратите внимание: следующие действия предполагают, что вы запускаете
отладчик под учетной записью, имеющей все привилегии администратора и на
ходящейся на машине в группе Debugger Users).
1. Соберите приложение. По завершении сборки закройте существующее реше
ние.
2. Из меню File выберите Open Solution.
3. В диалоговом окне Open Solution измените значение в раскрывающемся списке
Files Of Type на Executable Files и перейдите туда, где находится собранный EXE
файл службы. Выберите EXEфайл как решение и щелкните кнопку Open.
4. Сохраните решение, выбрав Save All из меню File, и в диалоговом окне Save File
As присвойте ему имя вроде _Attach.SLN.
5. Щелкните правой кнопкой узел .EXE в окне Solution Explorer и из контекстно
го меню выберите команду Properties.
6. На странице Debugging диалогового окна Property Pages проекта установите поле
Attach на Yes (рис. 141).

Рис. 141.

Страница свойств Attach Debugging

7. Щелкните OK в диалоговом окне Property Pages проекта.
8. Установите и запустите вашу службу обычным способом.
9. Когда будете готовы отлаживать службу, просто нажмите F5 в Visual Studio .NET
с загруженным проектом подключения — и все!
В качестве альтернативного метода подключения отладчика можно вызвать
функцию API DebugBreak. Когда появится диалоговое окно Application Error, про
сто щелкните кнопку Cancel (Windows 2000) или Debug (Microsoft Windows XP/
Server 2003) и отлаживайте, как обычно. Помните: если вы собираете службу COM+,
вызов DebugBreak следует выполнять за пределами любых COMметодов или ини
циаций свойств. Иначе COM поглотит исключение точки прерывания, генериру
емое DebugBreak, и отладчик не будет подключен. Кроме того, не вызывайте Debug
Break как часть начального стартового кода службы — причины я объясню в раз
деле «Отладка стартового кода».

522

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Другой способ подключения отладчика к вашей службе — если вы зарегист
рированы в системе с правами администратора — использовать Task Manager.
Вызовите Task Manager, выберите вкладку Processes, щелкните правой кнопкой
процесс, который хотите отладить, и из контекстного меню выберите команду
Debug. ОС позволяет легко подключить отладчик, если вам известно, какой про
цесс надо отладить.
Подключать отладчик к службам могут только пользователи с правами адми
нистратора на локальной машине, иначе при попытке отладить процесс, выпол
няющийся не под вашей учетной записью, команда Debug выведет информаци
онное окно Unable To Attach Debugger (невозможно подключить отладчик).

ISAPI-фильтры и расширения IIS
До IIS 5 все ISAPIфильтры выполнялись внутри INETINFO.EXE — главной IISслужбы,
т. е. вы просто подключались к INETINFO.EXE и отлаживали единый процесс. В IIS 5
и выше изза новой объединенной внепроцессной модели расширения выполня
ются в DLLHOST.EXE. ISAPIфильтры попрежнему выполняются внутри IISпроцесса
INETINFO.EXE. Новая модель делает IIS гораздо стабильнее и, по утверждению
Microsoft, гораздо более масштабируемыми. Единственная проблема с отладкой в
том, что вы можете не знать, в каком процессе DLLHOST.EXE выполняется ваше
расширение.
В документации IIS сказано, что следует настроить свои расширения на выпол
нение внутри IIS, чтобы иметь возможность их отлаживать. Единственная проблема
изменения места выполнения ваших расширений в том, что развертывать их надо,
используя объединенную внепроцессную модель. Поскольку я проповедую отладку
в сценариях, близких к тем, в которых будут работать ваши пользователи, я пока
жу вам трюк, который позволит отлаживать расширения, даже когда они выпол
няются в DLLHOST.EXE, т. е. там, где они будут работать.
Но, прежде чем поговорить об отладчике, надо знать, как определить, в каком
процессе выполняется ваш фильтр или расширение, так как выполняются несколько
экземпляров DLLHOST.EXE. Вопервых, скачайте фантастическую бесплатную ути
литу Process Explorer с Webсайта Марка Руссиновича (Mark Russinovich) и Брюса
Когсвелла (Bruce Cogswell) www.sysinternals.com. Я впервые упомянул Process Explo
rer в главе 2, потому что это прекрасный инструмент, которым можно определить,
перемещались ли DLL, загруженные в ваше адресное пространство.
Process Explorer покажет вам описатели, открытые процессом, и — главное —
какие DLL в какие процессы загружены. Чтобы найти вашу DLL с помощью Process
Explorer, сначала нажмите Ctrl+D чтобы указать, что вы хотите просмотреть DLL,
затем нажмите Ctrl+F и в диалоговом окне Process Explorer Search укажите имя файла
вашей DLL в поле ввода DLL Substring. Щелкните кнопку Search, и Process Explorer
покажет список имен и идентификаторов процессов (PID), в которые загружена
ваша DLL. Имея PID, вы можете подключить отладчик Visual Studio .NET к процес
су командой Debug Process из меню Tools. Не забудьте прочитать врезку о других
возможностях Process Explorer, так как это один из лучших инструментов кото
рые можно держать на жестком диске.
Если вы ищете инструмент, эквивалентный Process Explorer, но работающий
из командной строки, это TLIST.EXE, поставляемый в комплекте Debugging Tools

ГЛАВА 14

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

523

for Windows (т. е. WinDBG). Он может показывать MTSпакеты, а также в какие про
цессы какие DLL загружены. Запуск TLIST ? покажет все параметры командной
строки, поддерживаемые TLIST.EXE. Ключ –k показывает все процессы, содержа
щие в себе MTSпакеты, ключ –m — какие процессы содержат определенную DLL.
Ключ –m поддерживает синтаксис регулярных выражений. Так, чтобы увидеть все
модули, загружающие KERNEL32.DLL, следует указать *KERNEL32.DLL как шаблон.
Поскольку вы ищете загруженную DLL, вам, очевидно, придется убедиться, что
она загружается, прежде чем отлаживать ее. Фильтры выполняются внутри INET
INFO.EXE, так что вы не можете подключить отладчик до запуска служб IIS. Так что,
если вы хотите отладить инициализацию, вам не повезло. Если вы отлаживаете
расширения, то, проявив изобретательность, вы сможете отладить инициализа
цию. Идея в том, чтобы создать фиктивное расширение и заставить IIS его загру
зить, подключившись к вашему Webсайту через Microsoft Internet Explorer, что вы
нудит IIS запустить объединенный внепроцессный исполняемый файл DLLHOST.EXE.
Обнаружив PID нового DLLHOST.EXE, вы сможете подключить отладчик. Затем
можно установить точку прерывания на LdrpRunInitializeRoutines, чтобы попасть
прямо в DllMain вашего расширения. В своей рубрике «Under the Hood» («Microsoft
Systems Journal», сентябрь 1999) Мэтт Питрек (Matt Pietrek) объясняет, как уста
новить току прерывания на LdrpRunInitializeRoutines. Установив точку прерыва
ния, вы можете загружать настоящее расширение с помощью Internet Explorer и
отлаживать инициализацию.

Отладка стартового кода
Самое сложное в отладке служб — отладка стартового кода. SCM будет ждать все
го 30 секунд, чтобы служба запустилась и вызвала StartServiceCtrlDispatcher, по
казывая, что выполнение идет нормально. Хотя для процессора это время — по
чти целая жизнь, его легко можно потратить, пошагово выполняя код и следя за
переменными.
Если все, чем вы располагаете, — это отладчик Visual Studio .NET, то единствен
ный корректный способ отладить стартовый код вашей службы — использовать
операторы трассировки. DebugView Марка Руссиновича (см. главу 3) позволяет
видеть операторы по ходу работы службы. К счастью, стартовый код службы обычно
проще, чем ее главный код, так что отладка с помощью операторов трассировки
не слишком болезненна.
Для служб, не способных запускаться быстро, ограниченное время ожидания
SCM может представлять проблему. Медленная аппаратная часть или природа ва
шей службы иногда могут диктовать большое время запуска. Если ваша служба
предрасположена к превышению времени запуска, вам помогут два поля — dwCheck
Point и dwWaitHint, которые содержит структура SERVICE_STATUS, передаваемая SetServi
ceStatus.
Когда ваша служба запускается, вы вправе сообщить SCM, что вы переходите в
состояние SERVICE_START_PENDING, поместить большое значение в поле dwWaitHint (время
в мс) и установить поле dwCheckPoint в 0, чтобы SCM не использовал стандартные
значения времени. Если при старте службы вам нужно больше времени, вы впра
ве повторять вызов SetServiceStatus сколько угодно, увеличивая поле dwCheckPoint
перед каждым следующим вызовом.

524

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Последнее, что я хотел сказать об отладке стартового кода: SCM будет добав
лять записи в журнал событий, объясняя почему он не смог запустить определен
ную службу. В Event Viewer, найдите в столбце Source строку «Service Control
Manager». Если вы также используете журнал событий для легкой трассировки, то
среди записей SCM и вашей информации трассировки вы сможете найти реше
ние многих проблем запуска. Если вы используете журнал событий, убедитесь, что
взаимосвязи вашей службы установлены так, что ваша служба запускается после
службы журнала событий.

Стандартный вопрос отладки
Почему каждому разработчику нужен Process Explorer?
Я уже говорил, что чудесная программа Марка Руссиновича Process Explorer
позволяет легко выяснить, какой экземпляр DLLHOST.EXE загрузил DLL и
определить, имеются ли в процессе перемещенные DLL. Однако Process
Explorer способен на большее — например, быть прекрасным инструмен
том отладки, и я хочу уделить секунду рассказу о некоторых его замечатель
ных функциях.
По умолчанию Process Explorer обновляется периодически, как Task Mana
ger. Хотя это обновление прекрасно для общего мониторинга, изза него
вы можете пропустить некоторые детали при отладке. Лучше настроить
Process Explorer на обновление вручную, выбрав меню View и установив
Update Speed на Paused.
Наверное, лучший способ показать вам мощь Process Explorer — неболь
шая демонстрация. Вы можете повторять все операции, чтобы увидеть ин
струмент в действии. Первый шаг — запустить Process Explorer, указав да
лее NOTEPAD.EXE, так как я буду использовать его для демонстрации. На
стройте Process Explorer на ручное обновление, выбрав меню View и уста
новив Update Speed на Paused.
Первый трюк, который можно выполнять с помощью Process Explorer, —
определение, какие DLL поступают в ваше адресное пространство вследствие
определенной операции. В Process Explorer нажмите F5 чтобы обновить
экран, выберите экземпляр NOTEPAD.EXE, запущенный секунду назад, и
нажмите Ctrl+D, чтобы изменить вид на отображение DLL для Блокнота.
Активизируйте Блокнот и выберите Open из его меню File. Оставьте диало
говое окно Open в Блокноте открытым и переключитесь в Process Explorer.
Нажмите F5, чтобы обновить отображение в Process Explorer, и вы увидите
несколько строк зеленого цвета, появившихся в отображении DLL для NOTE
PAD.EXE (рис. 142). Зеленый цвет показывает, какие DLL поступили в адрес
ное пространство с момента последнего обновления. Конечно, вы также
можете увидеть, какие DLL покинули адресное пространство, переключив
шись обратно на Блокнот и закрыв диалоговое окно Open, а затем вернув
шись в Process Explorer и обновив отображение кнопкой F5. Все DLL, поки
нувшие адресное пространство, отображаются красным. Эта возможность
быстро увидеть, что приходит и уходит из ваших процессов, полезна для
определения причин загрузки и выгрузки модулей. Выделение цветом, по

ГЛАВА 14

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

525

казывающее, что было загружено и выгружено, также применяется к спис
ку EXEфайлов в верхней половине экрана Process Explorer.

Рис. 142. Отображение DLL в Process Explorer, показывающее новые DLL,
добавленные в процесс Блокнота
Второй трюк Process Explorer позволяет получать все виды интересной
информации о процессе просто двойным щелчком этого процесса. Появ
ляющееся диалоговое окно показывает четыре или пять вкладок в зависи
мости от процесса. Первая — Image — показывает путь и текущий каталог
процесса, а также предлагает кнопку, позволяющую завершить процесс.
Вторая — Performance — показывает важные данные о производительнос
ти, касающиеся процессора, памяти, ввода/вывода и GDIописателей. Тре
тья — Security — показывает группы для процессов и предоставленный до
ступ. Если процесс — хост или служба Microsoft Win32, вкладка Services по
казывает имена служб, выполняющихся в этом процессе. Последняя вклад
ка — Environment — показывает список активных для данного процесса пе
ременных окружения. С помощью вкладок Security и Environment я нахо
дил некоторые очень интересные проблемы, касающиеся программирова
ния защиты, так как Process Explorer — практически единственный инстру
мент, позволяющий легко увидеть эту информацию.
Последний трюк Process Explorer позволяет увидеть, какие описатели
открыты в данный момент любым процессом! В прошлом я применял эту
функцию для обнаружения большого количества разных проблем с описа
телями. В Process Explorer нажмите Ctrl+H, чтобы изменить нижнюю поло
вину экрана на отображение описателей. Первый отображаемый столбец
представляет значение описателя, а второй — тип описателя (объяснение
см. в обсуждении !handle из главы 8). Третий столбец содержит биты досту
па для описателя, а четвертый — имя объекта. Как сказано в главе 8, имено
вание описателей критично для обнаружения проблем. Если вам нужны
подробности о какомто описателе, дважды щелкните его, чтобы увидеть
см. след. стр.

526

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

свойства описателя и больше, чем вы когдалибо хотели, узнать о конкрет
ных значениях этого описателя, связанных с разрешениями.
Как и в отображении DLL, вы можете видеть описатели, создаваемые и
закрываемые в процессе. Выберите экземпляр NOTEPAD.EXE, запущенный
ранее. Нажмите Ctrl+H, чтобы перейти к отображению описателей, и обно
вите содержимое, нажав F5. Переключитесь на Блокнот и вновь откройте
диалоговое окно Open. Когда оно откроется, переключитесь обратно на
Process Explorer и опять обновите отображение. Все новые описатели в
процессе Блокнота выделяются зеленым. Если вы закроете диалоговое окно
Open Блокнота и еще раз обновите Process Explorer, все закрытые описате
ли будут выделены красным.
Я использовал отображение описателей в Process Explorer для поиска
утечек описателей больше, чем могу сосчитать. По умолчанию Process Explo
rer покажет только те описатели, что имеют имена. Вы также можете уви
деть все безымянные описатели, нажав Ctrl+U. Если вы отслеживаете про
блемы с описателями, вам, вероятно, захочется просмотреть все описате
ли, чтобы видеть все типы, где может быть утечка.
Интересная особенность отображения описателей позволяет принуди
тельно закрыть определенный описатель, щелкнув его правой кнопкой и
выбрав Close Handle. Когда я спросил Марка, зачем он внес такую функцию,
он ответил: «Потому что мог». Когда я засмеялся и сказал, что это было до
вольно опасно, он сказал, что это мое дело — заботиться о причинах нали
чия этой функции. Главная причина наугад закрывать описатели в Process
Explorer — прокрасться в кабинет вашего менеджера и закрыть половину
описателей Outlook, чтобы он не смог отправлять вам надоедливые сооб
щения по электронной почте. Я решил, что такой причины вполне доста
точно!

Резюме
В этой главе рассказано о некоторых испытаниях и злоключениях, являющихся
частью отладки служб Windows и DLL, загружаемых в службы. Службы обладают
особым статусом в ОС, и вследствие проблем, связанных с безопасностью, вам
необходимо понимать, что представляют собой службы и как они себя ведут.
Отладка служб требует больше предварительного планирования, чем обычная
отладка.
Первый шаг в отладке служб и любых DLL, загружаемых в службы, — отладка
максимального количества базового кода при выполнении в виде обычного при
ложения. На втором этапе нужно обеспечить использование преимуществ среды
для служб, таких как включение взаимодействия с рабочим столом и применение
таких инструментов, как Process Explorer, для поиска информации, ускоряющей
отладку.

Г Л А В А

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

Без сомнения, наибольшие проблемы при разработке современного ПО связа
ны с многопоточной блокировкой. Даже если вы думаете, что предусмотрели все,
ваше многопоточное приложение может зависнуть, когда вы этого меньше всего
ждете. Отладка многопоточных блокировок заметно осложняется тем, что после
возникновения такой ошибки начинать отладку уже поздно.
В этой главе я опишу некоторые методы и хитрости, помогающие мне при
разработке многопоточных программ. Я также представлю свою утилиту Deadlock
Detection — почти единственное средство, которое поможет найти причину ошибки
и узнать, как избегать некоторых типов блокировки в будущем.

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

528

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

쐽 получайте дампы в реальных условиях;
쐽 уделяйте особое внимание обзору кода;
쐽 тестируйте многопоточные приложения на многопроцессорных компьютерах;

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

Не злоупотребляйте многопоточностью
Разрабатывая серверные приложения, нужно быть чрезвычайно внимательным,
чтобы не создать чрезмерное число потоков. Одна из очень частых ошибок при
написании серверных приложений состоит в обработке каждого соединения в
отдельном потоке. Когда средняя группа разработчиков тестирует программу в
самом напряженном режиме, создавая около 10 одновременных соединений, все
идет по плану. При первом пробном запуске приложение может работать вели
колепно, но когда дело доходит до реальных задач, оно начинает тормозить из
за низкой масштабируемости.
При работе над серверными приложениями используйте преимущества пулов
потоков, которые прекрасно поддерживаются Microsoft Windows 2000/XP/Server
2003 посредством семейства функций QueueUserWorkItem. Это позволяет выполнять
тонкую настройку баланса между числом потоком и объемом работы. Програм
мисты привыкли к обработке пулов потоков средствами Microsoft Internet Infor
mation Services (IIS) и COM+, однако разработка собственной системы пулинга
потоков не относится к тем вещам, которые хорошо знакомы многим програм
мистам, поэтому тщательно проанализируйте собственную ситуацию. При непра
вильном использовании пулов потоков блокировка становится гораздо более ве
роятной, чем можно представить.

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

ГЛАВА 15

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

529

В случае же серверных приложений вы должны оценить, действительно ли
дополнительные затраты на создание ивыполнение потоков приведут к повыше
нию быстродействия программы. Хоть потоки и гораздо «легче» процессов, они
требуют большого объема работы. Поэтому убедитесь, что выгода от создания
потоков оправдает все затраты. Так, многие серверные приложения должны об
мениваться информацией с некоторой базой данных. Цена ожидания записи в базу
может быть весьма высока. Если вам не требуется запись транзакций, вы можете
создать для записи информации в базу данных отдельный объект пула потоков и
продолжить выполнение других задач. Это позволит вам быстрее реагировать на
запросы и выполнить больший объем работы.

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

class CUseCriticalSection;
class CCriticalSection
{
public
:
CCriticalSection ( DWORD dwSpinCount = 4000 )
{
InitializeCriticalSectionAndSpinCount ( &m_CritSec ,
dwSpinCount ) ;
}
~CCriticalSection ( )
{
DeleteCriticalSection ( &m_CritSec ) ;
}
friend CUseCriticalSection ;
public
:
CRITICAL_SECTION m_CritSec ;
} ;
class CUseCriticalSection
{
public
:
CUseCriticalSection ( const CCriticalSection & cs )
{
m_cs = &cs ;

530

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

EnterCriticalSection ( ( LPCRITICAL_SECTION)&(m_cs>m_CritSec));
}
~CUseCriticalSection ( )
{
LeaveCriticalSection ( (LPCRITICAL_SECTION)&(m_cs>m_CritSec) );
m_cs = NULL ;
}
private
:
CUseCriticalSection ( void )
{
m_cs = NULL ;
}
const CCriticalSection * m_cs ;
} ;
С точки зрения объектноориентированного программирования все просто
великолепно, но такая реализация пагубно сказывается на быстродействии про
граммы. Объектоболочка CUseCriticalSection создается в начале области видимости
своего объявления и уничтожается, когда эта область заканчивается. Почти все
программисты используют класс синхронизации так:

void DoSomethingMultithreaded ( )
{
CUseCriticalSection ( g_lpCS ) ;
for ( . . . )
{
CallSomeOtherFunction ( . . . ) ;
}
// Это единственный элемент данных, понастоящему нуждающийся в защите.
m_xFoo = z ;
YetAnotherCallHere ( . . . ) ;
}
Конструктор получает критическую секцию после первой фигурной скобки,
т. е. сразу же после пролога функции, в то время как деструктор вызывается толь
ко перед последней фигурной скобкой, перед эпилогом. Это значит, что крити
ческая секция удерживается на протяжении всей функции DoSomethingMultithreaded,
в том числе когда она вызывает другие функции, которым критическая секция не
нужна. Такой подход просто убивает быстродействие.
Взглянув на DoSomethingMultithreaded, вы, вероятно, подумали: «Насколько ресур
соемким на самом деле может быть получение объекта синхронизации?» Если
конкуренция за объект сихронизации отсутствует, затраты невелики. Однако, если
один из потоков многопоточной программы не может получить объект синхро
низации, затраты могут быть астрономическими!

ГЛАВА 15

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

531

Посмотрим, что происходит при вызове WaitForSingleObject для получения объек
та синхронизации. Так как после чтения главы 7 ваши знания ассемблера прибли
жаются к божественному уровню, вы можете сами проследить за всем в окне
Disassembly: оно четко покажет все, о чем я буду рассказывать. Заметьте: я рассмат
риваю функцию WaitForSingleObject из Windows XP — в Windows 2000 она немного
иная. Сама по себе WaitForSingleObject — это просто оболочка для WaitForSingle
ObjectEx, которая выполняет около 40 строк ассемблерных команд и вызывает две
функции для присвоения значений некоторым данным. Незадолго до своего окон
чания WaitForSingleObjectEx вызывает функцию NtWaitForSingleObject из NTDLL.DLL.
Итак, WaitForSingleObject — это оболочка для второй оболочки. Если вы дизассем
блируете код, начиная с адреса памяти, по которому располагается NtWaitFor
SingleObject (для этого надо ввести в поле Address окна Disassembly выражение
{,,ntdll}_NtWaitForSingleObject@12), то узнаете, что на самом деле происходит вы
зов странной функции ZwWaitForSingleObject, которая также находится в NTDLL.DLL
(в Windows 2000 на функции NtWaitForSingleObject вы остановитесь). Взглянув на
дизассемблированную функцию ZwWaitForSingleObject, вы увидите нечто вроде:

_ZwWaitForSingleObject@12:
77F7F4A3 mov
eax,10Fh
77F7F4A8 mov
edx,7FFE0300h
77F7F4AD call
edx
77F7F4AF ret
0Ch
77F7F4B2 nop
Реальные действия происходят по адресу 0x7FFE0300. Если вы посмотрите, что
находится по этому адресу, то увидите:

7FFE0300 mov
7FFE0302 sysenter
7FFE0304 ret

edx,esp

Среднюю строку во фрагменте занимает магическая команда SYSENTER. Вы може
те увидеть ее только в этом контексте и никогда — в своем коде, поэтому в гла
ве 7 я ее не описывал. О роли этой команды можно догадаться по названию: она
выполняет переключение из пользовательского режима в режим ядра. В Windows
2000 эту же функцию выполняет команда INT 2E. Зачем я все это? Просто я хотел
показать, что SYSENTER отправляет поток в режим ядра, и подчеркнуть все затраты,
связанные с выведением потока из очереди потоков, ожиданием и прочими дей
ствиями, необходимыми для координации потоков. Разумеется, при переключе
нии в режим ядра, которое требуется для получения объекта ядра, переданного в
WaitForSingleObject, выполняются тысячи команд, выводящих поток из очереди
активных потоков и помещающих его в очередь ожидающих.
Внимательный читатель может подумать, что при вызове WaitForSingleObject для
ожидания описателя ядра эти затраты неизбежны. Точно: описатели ядра, исполь
зуемые для синхронизации процессов, выбора не оставляют. Поэтому большин
ство людей для внутренней синхронизации, которая не требует межпроцессной
синхронизации, использует верную критическую секцию, как я показал выше на
примере класса CUseCriticalSection. Почти все мы читали когдато, что критиче
ские секции хороши тем, что они не требуют переключения в режим ядра. Все

532

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

так, однако большинство программистов забывает про одну важную деталь. Что,
если получить критическую секцию не удастся? Очевидно, в таких случаях долж
на быть выполнена какаято синхронизация. Так и есть: для этого служит описа
тель семафора (semaphore handle) Microsoft Win32.
Я привел это пространное описание, чтобы объяснить проблему чрезмерно дол
гого удержания объектов синхронизации. Мне попадались приложения, работу ко
торых удавалось значительно ускорить, просто обнаружив участки конкуренции
и удалив классыоболочки. Я обнаружил, что гораздо лучше явно вызывать функ
ции получения и освобождения объектов синхронизации только до и после фак
тического доступа к данным, даже если вам понадобится выполнять эти вызовы
два, три или больше раз в одной функции. В случае критических секций это дает
особенно большой рост быстродействия. Кроме того, использование синхрони
зации только для фактического доступа к данным — один из лучших методов за
щиты от случайной блокировки.
Еще раз: ничего плохого в классахоболочках вроде CUseCriticalSection нет —
проблема в их неправильном применении. Например, вполне допустимо:

void DoSomeGoodMultithreaded ( )
{
for ( . . . )
{
CallSomeOtherFunction ( . . . ) ;
}
// Доступ к этому элементу данных нужно защитить,
// но блокировка не должна быть слишком долгой.
{
CUseCriticalSection ( g_lpCS ) ;
m_xFoo = z ;
}
YetAnotherCallHere ( . . . ) ;
}
В этом случае также используется вспомогательный класс CUseCriticalSection,
однако благодаря ограничению его области видимости объект синхронизации
приобретается и освобождается в одном локализованном месте и не удерживает
ся слишком долго.

Работая с критическими секциями,
используйте спин-блокировку
Как я уже говорил, критические секции — предпочтительный метод синхрониза
ции, если она выполняется только внутри процесса. При этом вы получите боль
шой прирост производительности, если будете помнить про спинблокировку!
Когдато Microsoft’овцы заинтересовались производительностью многопоточ
ных приложений и разработали несколько сценариев тестирования, чтобы полу
чить более подробную информацию. После долгих исследований они обнаружи

ГЛАВА 15

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

533

ли один противоречащий интуиции, хотя и не новый в области компьютинга факт:
иногда гораздо выгоднее не выполнять операцию на самом деле, а просто подож
дать. Помните, когда мы только начинали, нам говорили никогда не ждать? Но в
случае критических секций именно это и следует делать.
Обычно критические секции применяются для защиты небольших данных.
Выше я говорил, что критическая секция защищается семафором и переключе
ние в режим ядра для ее получения очень накладно. В первоначальном варианте
функция EnterCriticalSection просто узнавала, можно ли получить критическую
секцию. Если нет, EnterCriticalSection переключалась в режим ядра. Как правило,
к тому моменту, когда поток успевает переключиться в режим ядра и обратно, ока
зывается, что другой поток уже освободил критическую секцию миллион компь
ютерных лет назад. Странный вывод сотрудников Microsoft заключался в том, что
при работе на многопроцессорных системах надо проверять, доступна ли кри
тическая секция, и, если нет, переходить в состояние спинблокировки и ждать, а
потом проверять ее доступность снова. Очевидно, что на однопроцессорных си
стемах счетчик циклов спинблокировки игнорируется. Если критическая секция
недоступна и после второй проверки, выполняется переключение в режим ядра.
Суть сказанного в том, что удержание потока в пользовательском режиме в пас
сивном состоянии все же много выгоднее, чем переключение в режим ядра.
Для присвоения значения счетчику циклов спинблокировки критической сек
ции служат две функции: InitializeCriticalSectionAndSpinCount, которую следует ис
пользовать вместо InitializeCriticalSection, и SetCriticalSectionSpinCount, позво
ляющая изменить первоначальное значение вашего счетчика или значение счет
чика библиотечного кода, использующего только InitializeCriticalSection. Разу
меется, для этого вам понадобится доступ к указателю на критическую секцию из
своего кода.
Подобрать значение счетчика спинблокировки может оказаться нелегко. Если
у вас есть дветри недели для проработки всех сценариев, займите этим начина
ющих программистов — они все равно бездельничают. Однако большинству из
нас не так везет. Я всегда инициализирую этот счетчик значением 4000. Именно
оно используется в Microsoft для куч ОС, и я всегда находил свой код менее тре
бовательным, чтобы уменьшать это число. С другой стороны, оно достаточно ве
лико, чтобы почти всегда удерживать код в пользовательском режиме.

Не используйте функции CreateThread/ExitThread
Одна из самых коварных ошибок, допускаемых при разработке многопоточных
приложений, связана с функцией CreateThread. Конечно, возникает вопрос: если
потоки нельзя создавать при помощи CreateThread, как же их вообще создавать?
Вместо CreateThread следует всегда использовать _beginthreadex, функцию создания
потоков из стандартной библиотеки C. Как вы уже догадались, раз уж CreateThread
дополняет функция ExitThread для завершения потока, _beginthreadex тоже имеет
соответствующую функцию _exitthreadex, которую также нужно использовать вместо
ExitThread.
Возможно, вы вызываете CreateThread в своей программе и не испытываете
проблем. Увы, при этом возможны очень тонкие ошибки, потому что при исполь
зовании CreateThread не инициализируется стандартная библиотека C. Работа стан

534

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

дартной библиотеки C основана на некоторых данных, отдельных для каждого
потока, и определенные ее функции были разработаны до того, как стали нор
мой высокопроизводительные многопоточные приложения. Так, функция strtok
хранит обрабатываемую строку в памяти отдельного потока. Функция _beginthreadex
гарантирует наличие данных, отдельных для потоков, а также всех остальных вещей,
нужных стандартной библиотеке C. Для гарантии правильной очистки потока
вызывайте _exitthreadex, которая правильно освобождает ресурсы стандартной
библиотеки C, если вам нужно преждевременно завершить поток.
_beginthreadex работает так же и принимает те же параметры, что и CreateThread.
Поток завершается возвратом из функции потока или вызовом _endthreadex. Для
преждевременного завершения потоков служит _endthreadex. Как и CreateThread,
_beginthreadex возвращает описатель потока, который нужно затем передать Close
Handle, чтобы избежать утечки описателей.
В документации к _beginthreadex вы увидите функцию стандартной библиоте
ки C по имени _beginthread. Избегайте ее, как чумы, потому что, помоему, ее по
ведение по умолчанию просто ошибочно. Описатель, возвращаемый _beginthread,
кэшируется, поэтому при быстром завершении потока и его перезаписи другим
потоком описатель может оказаться неверным. Даже в документации к _beginthread
указано, что безопаснее использовать _beginthreadex. При обзоре кода отметьте все
вызовы _beginthread и _endthread, чтобы изменить их затем на _beginthreadex и
_endthreadex соответственно.

Опасайтесь диспетчера памяти по умолчанию
Одна из компаний хотела сделать серверное приложение максимально быстрым.
Когда программисты обнаружили, что увеличение числа потоков, которое, по их
мнению, должно было обеспечивать масштабируемость вычислительной мощно
сти, не возымело эффекта, они обратились к нам. Одна из первых вещей, кото
рые я сделал, заключалась в остановке программы в отладчике и изучении распо
ложения каждого потока при помощи окна Threads (потоки).
Приложение интенсивно работало с библиотекой STL, которая, как я говорил
при обсуждении WinDBG в главе 8, сама по себе может ухудшать быстродействие,
выделяя огромные объемы памяти. Остановив серверное приложение, я хотел
увидеть, какие потоки находились в системе управления памятью стандартной
библиотеки C. У всех нас есть исходный код управления памятью (ведь вы уста
навливаете исходный код стандартной библиотеки C при каждой установке Micro
soft Visual Studio, да?), и я увидел, что всю систему управления памятью защищает
одна критическая секция. Это всегда пугало меня, так как мне кажется, что это может
приводить к проблемам с производительностью. Но когда я взглянул на клиентс
кое приложение, то просто пришел в ужас: 38 из 50 потоков были заблокирова
ны на критической секции системы управления памятью стандартной библиоте
ки C! Большая часть программы находилась в состоянии ожидания, ничего не делая!
Стоит ли говорить, что это не вызвало у программистов особой радости.
Для большинства программ поставляемая Microsoft стандартная библиотека C
подходит прекрасно, не вызывая проблем с памятью. Однако в более крупных
серверных приложениях однаединственная критическая секция может все испор
тить. Итак, прежде всего я хочу порекомендовать вам всегда тщательно обдумы

ГЛАВА 15

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

535

вать использование STL и, если избежать этого не удается, обратите внимание на
STLPort версии I (см. главу 2). Ранее я уже указывал на многие проблемы с STL.
В контексте крупных многопоточных приложений библиотека STL от Microsoft
может приводить к появлению узких мест.
Более серьезная проблема: что делать с единственной критической секцией
стандартной библиотеки C? Для ее решения нужно предоставить каждому потоку
отдельную кучу, а не использовать единственную глобальную кучу для всех пото
ков. Это позволило бы потокам никогда не переключаться в режим ядра для вы
деления или освобождения памяти. Конечно, создания отдельной кучи для каж
дого потока недостаточно, так как порой память выделяется в одном потоке и
освобождается в другом. К счастью, эта головоломка имеет три решения.
Первое — коммерческие системы управления памятью, обрабатывающие код
работы с кучами отдельных потоков. Жаль, но цены на такие системы просто
грабительские, и ваш начальник никогда не согласится на покупку. Второе реше
ние обеспечивает значительное повышение производительности Windows 2000
и основано на усовершенствованиях, внесенных Microsoft в механизм работы куч
ОС (куч, создаваемых функцией HeapCreate и используемых при помощи HeapAlloc
и HeapFree). Чтобы задействовать преимущества кучи ОС, можно заменить все
выделения памяти при помощи malloc/free соответствующими функциями Heap*.
Что до функций new и delete языка C++, то для их замены нужно предоставить
глобальные функции. Если ваша программа будет выполняться на многопроцес
сорных системах, то третье решение может заключаться в использовании вели
колепной библиотеки Hoard, написанной Эмери Бергером (Emery Berger) и пред
назначенной для управления памятью многопроцессорных компьютеров (http://
www.hoard.org). Эта библиотека заменяет функции работы с памятью C и C++ и
очень быстро работает на многопроцессорных системах. Если изза дублирова
ния символов у вас возникнут проблемы с ее компоновкой, укажите компонов
щику LINK.EXE ключ командной строки /FORCE:MULTIPLE. Помните, что Hoard пред
назначена для многопроцессорных систем, поэтому на однопроцессорных ком
пьютерах она может работать даже медленнее, чем диспетчер памяти по умол
чанию.

Получайте дампы в реальных условиях
Один из наиболее огорчительных случаев имеет место, когда ваша программа
блокируется в реальных условиях и, несмотря на все усилия, вы не можете вос
произвести ошибку. Однако, благодаря последним усовершенствованиям библио
теки DBGHELP.DLL, вы больше никогда не окажетесь в такой ситуации. Новые фун
кции работы с минидампами позволяют сделать снимок блокировки и отладить
ее в удобное для вас время. Функцию записи минидампа и мою улучшенную обо
лочку для нее, SnapCurrentProcessMiniDump, находящуюся в библиотеке BUGSLAYER
UTIL.DLL, я описал в главе 13.
Чтобы получить дамп в реальных условиях, нужно просто создать фоновый по
ток, который создает и ожидает некоторое событие. При возникновении собы
тия поток должен вызывать SnapCurrentProcessMiniDump и записывать дамп на диск.
Соответствующая функция показана в следующем фрагменте псевдокода. Для ус

536

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

тановки события создайте отдельный исполняемый файл и скажите пользовате
лям запускать его в нужной ситуации.

DWORD WINAPI DumperThread ( LPVOID )
{
HANDLE hEvents[2] ;
hEvents[0] = CreateEvent ( NULL
,
TRUE
,
FALSE
,
_T ( "DumperThread" ) ) ;
hEvents[1] = CreateEvent ( NULL
,
TRUE
,
FALSE
,
_T ( "KillDumperThread" ) ) ;
int iRet = WaitForMultipleObjects ( 2 , hEvents , FALSE , INFINITE);
while ( iRet != 1 )
{
// Возможно, каждому файлу следует присваивать уникальное имя.
SnapCurrentProcessMiniDump ( MiniDumpWithFullMemory ,
_T ( "Program.DMP" ) ) ;
iRet = WaitForMultipleObjects ( 2 , hEvents , FALSE , INFINITE);
}
VERIFY ( CloseHandle ( hEvents[ 0 ] ) ) ;
VERIFY ( CloseHandle ( hEvents[ 1 ] ) ) ;
return ( TRUE ) ;
}

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

ГЛАВА 15

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

537

мую в разделе «Отладка: фронтовые очерки. Взаимоблокировка, не имеющая смыс
ла», и печально известный мьютекс Win16 в Microsoft Windows 9x/Me. Обращай
те внимание на все, что может вызвать конкуренцию в вашей программе.

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

Отладка: фронтовые очерки
Как я спас нескольких людей от увольнения
Боевые действия
Когда вицепрезидент одной компании позвонил мне и сказал, что хотел
бы нанять меня для решения проблемы с блокировкой, я понял, что работа
предстоит нелегкая. Он был весьма раздражен и недоволен тем, что его
компании пришлось прибегнуть к услугам консультанта. Он позвонил двум
ведущим программистам, и мы вчетвером собрались на онлайновую кон
ференцию. Вицепрезидент злился, что разработчики слишком долго без
действовали изза этой ошибки. Представляю, как чувствовали себя эти два
программиста, когда их грязное белье отправляли по телефону какомуто
парню, которого они даже не знали. Как сказал вицепрезидент, они рабо
тали над переносом приложения «с настоящей ОС» (UNIX) на «эту (выре
зано цензурой) детскую ОС под названием Windows», и это «вычеркнуло из
его жизни целый год». Конечно, когда я спросил его, зачем им понадоби
лось переносить программу на другую платформу, он признал, что «это было
нужно, чтобы остаться на плаву». Я невольно улыбнулся на своем конце
провода!
см. след. стр.

538

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Программисты прислали мне по электронной почте код, и мы начали
изучать его, в то время как вицепрезидент с гордым видом расхаживал по
своему кабинету. Когда я добрался до места блокировки, у меня тут же вы
ступил холодный пот, а сердце забилось гораздо чаще. Я понимал, что если
я скажу, что для исправления ошибки нужно только удалить буквы S, E, N и
D и напечатать вместо них P, O, S и T, вицепрезидент просто сойдет с ума
и, вполне возможно, уволит этих двух разработчиков.

Исход
Какоето время я молчал, собираясь с мыслями. Наконец, глубоко вздохнув
и сказав: «Ого, это очень, очень серьезно», — я сообщил вицепрезиденту,
что на исправление проблемы нам потребуется несколько часов. Было бы
лучше, если б мы с программистами остались одни, потому что наверняка
у него есть гораздо более важные дела, чем слушать телефонные разгово
ры, состоящие большей частью из перечисления всяких шестнадцатерич
ных чисел. Нам повезло: он купился на это, и я сказал программистам, что
перезвоню им.
Перезвонив, я сообщил им, что они сделали одну очень частую ошибку,
которую допускают многие разработчики программ для UNIX: дело в том,
что возврат из функции, посылающей сообщения из одного потока в дру
гой, в некоторых версиях UNIX выполняется сразу. Однако в Windows воз
врат из SendMessage не происходит, пока сообщение не будет обработано. Я
нашел в их коде место, где поток, которому они посылали сообщение, уже
был заблокирован на объекте синхронизации, поэтому SendMessage вызывала
блокировку. Когда я сказал им, что для исправления проблемы нужно толь
ко заменить SendMessage вызовом PostMessage, настроение у них резко упа
ло. Тогда я попытался их успокоить, сказав, что непонимание происходя
щего было вполне законным. Мы провели остаток дня, разбирая другие
вопросы, такие как модификация базовых адресов DLL и создание прило
жений с полным набором отладочных символов. Вернувшись к телефон
ному разговору с вицепрезидентом, я сказал ему, что это была одна из са
мых хитрых ошибок, с которыми мне приходилось сталкиваться, но его про
граммисты оказали мне поистине неоценимую помощь. В итоге все оста
лись довольны. Вицепрезидент избавился от проблемы, программисты
узнали много полезного, а я спас их от увольнения!

Полученный опыт
Если вы работаете над многопоточной программой и хотите передавать
сообщения между потоками, тщательно обдумайте взаимодействие объек
тов синхронизации и сообщений. В подобной ситуации всегда старайтесь
использовать PostMessage. Конечно, если вы передаете при помощи сообщений
значения, превышающие по объему 32 бита, вызовы PostMessage не будут
работать, потому что переданные вами параметры могут быть повреждены
к тому времени, когда другой поток обработает сообщение. В таких случа
ях вызывайте SendMessageTimeOut — она по крайней мере выполнит возврат

ГЛАВА 15

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

539

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

Отладка: фронтовые очерки
Взаимоблокировка, не имеющая смысла
Боевые действия
Разрабатывая приложение, программисты столкнулись с непонятной бло
кировкой. Поборовшись с ней пару дней, они обратились ко мне.
Их программа имела интересную архитектуру и была в высокой степе
ни многопоточной. Блокировка происходила только в определенных слу
чаях, причем всегда в процессе загрузки ряда DLL. Программа блокирова
лась при вызове функции WaitForSingleObject, проверявшей, смог ли поток
создать некоторые совместно используемые объекты.
Программисты были опытными и уже перепроверили код несколько раз
на пример потенциальных взаимоблокировок, однако остались в полном
замешательстве. Я еще раз спросил, искали ли они взаимоблокировки, и они
заверили меня, что да.

Исход
Я хорошо помню эту ситуацию, потому что она относится к тем случаям,
когда я почувствовал себя героем через 5 минут после запуска отладчика.
Как только программисты воспроизвели блокировку, я взглянул в окно Call
Stack (стек вызовов) и заметил, что программа ожидала описателя потока
внутри функции DllMain. При загрузке их программой определенной DLL в
ее функции DllMain создавался другой поток. Прежде чем продолжить вы
полнение, DllMain вызывала WaitForSingleObject для подтверждения события,
гарантирующего, что созданный поток смог правильно инициализировать
некоторые важные общие объекты.
Разработчики не знали, что каждый процесс имеет критическую секцию
процесса, которую ОС использует для синхронизации различных действий,
происходящих за кулисами процесса. Помимо прочего, критическая секция
процесса служит для сериализации выполнения DllMain при четырех вари
антах ее вызова: DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH и
DLL_PROCESS_DETACH. На причину вызова DllMain указывает ее второй параметр.
Итак, вызов LoadLibrary заставлял ОС захватить критическую секцию
процесса, чтобы ОС могла вызвать DllMain по причине DLL_PROCESS_ATTACH.
После этого DllMain создавала второй поток. При создании процессом но
вого потока ОС всегда захватывает критическую секцию процесса для вы
зова функции DllMain каждой загруженной DLL по причине DLL_THREAD_ATTACH.
В этой конкретной программе второй поток блокировался потому, что пер
вый поток удерживал критическую секцию процесса. К несчастью, первый
поток вызывал затем WaitForSingleObject, чтобы убедиться в правильной
см. след. стр.

540

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

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

Полученный опыт
Урок очевиден: чтобы избежать блокировки, связанной с объектами ядра,
не вызывайте внутри DllMain функции Wait* или EnterCriticalSection, пото
му что критическая секция процесса блокирует остальные потоки. Как вы
смогли убедиться, даже опытные программисты ошибаются в многопоточ
ных программах, так что еще раз: проблемы подобного типа часто проис
ходят там, где вы их ожидаете меньше всего.

Требования к DeadlockDetection
Вероятно, вы заметили, что выше я привел мало рекомендаций по поводу исправ
ления блокировок. Большинство советов было профилактическими мерами и
касались предотвращения блокировок, а не их разрешения. Всем известно, что
исправить блокировки, используя отладчик, непросто. В этом разделе я предос
тавлю вам дополнительную помощь — утилиту DeadlockDetection.
Вот основные требования, которыми я руководствовался при ее разработке.
1. Указание точного места блокировки в пользовательском коде. От утилиты,
которая только сообщает, что вызов EnterCriticalSection заблокирован, толку
мало. Эффективное средство должно указывать адрес (а значит, и исходный
файл и номер строки) блокировки, чтобы можно было быстро ее исправить.
2. Отображение объекта синхронизации, вызвавшего блокировку.
3. Вывод информации о заблокированной функции Windows и переданных в нее
параметрах. Это позволило бы узнать значения таймаута и значения, передан
ные в функцию.
4. Определение потока, вызвавшего блокировку.
5. Утилита должна быть «легкой», чтобы как можно меньше влиять на пользова
тельскую программу.
6. Обработка выводимой информации должна быть расширяемой. Утилита дол
жна поддерживать разные способы обработки информации, собранной в си
стеме обнаружения блокировок, и давать возможность настройки и расшире
ния вывода информации не только вам, но и другим программистам.
7. Средство должно обеспечивать легкую интеграцию с пользовательскими про
граммами.
Работая с утилитами, подобными DeadlockDetection, следует помнить, что они
неизбежно влияют на поведение исследуемого приложения. Можно рассматри
вать это как еще одно наглядное подтверждение принципа неопределенности
Гейзенберга. DeadlockDetection сама может вызывать в ваших программах блоки
ровки, которые вы иначе не обнаружили бы, потому что выполняемая ею работа

ГЛАВА 15

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

541

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

Общие вопросы разработки DeadlockDetection
Чтобы DeadlockDetection удовлетворяла названным требованиям, я должен был
ответить на ряд вопросов. Сначала я должен был определить, какие функции нужно
отслеживать для воспроизведения полной истории блокировки (табл. 151).

Табл. 15-1. Функции, отслеживаемые утилитой DeadlockDetection
Тип

Функции

Функции работы с потоками

CreateThread, ExitThread, SuspendThread, ResumeThread,
TerminateThread, _beginthreadex, _beginthread,
_exitthreadex, _exitthread, FreeLibraryAndExitThread

Функции работы
с критическими секциями

InitializeCriticalSection,
InitializeCriticalSectionAndSpinCount,
DeleteCriticalSection, EnterCriticalSection,
LeaveCriticalSection, SetCriticalSectionSpinCount,
TryEnterCriticalSection

Функции работы с мьютексами

CreateMutexA, CreateMutexW, OpenMutexA, OpenMutexW,
ReleaseMutex

Функции работы с семафорами

CreateSemaphoreA, CreateSemaphoreW, OpenSemaphoreA,
OpenSemaphoreW, ReleaseSemaphore

Функции работы с событиями

CreateEventA, CreateEventW, OpenEventA, OpenEventW,
PulseEvent, ResetEvent, SetEvent

Функции блокировки

WaitForSingleObject, WaitForSingleObjectEx,
WaitForMultipleObjects, WaitForMultipleObjectsEx,
MsgWaitForMultipleObjects, MsgWaitForMultipleObjectsEx,
SignalObjectAndWait

Специальные функции

CloseHandle, ExitProcess, GetProcAddress, LoadLibraryA,
LoadLibraryW, LoadLibraryExA, LoadLibraryExW, FreeLibrary

Обдумав проблему сбора информации, необходимой для удовлетворения пер
вых четырех требований, я понял, что мне нужно перехватывать функции из табл.
151 (устанавливать для них ловушки), регистрируя получение и освобождение
объектов синхронизации. Перехват — нетривиальная задача; ее решение я рас
смотрю в разделе «Перехват импортируемых функций». Для перехвата импорти
руемых функций код DeadlockDetection должен находиться в DLL, потому что
ловушки работают только в том адресном пространстве, в котором создаются. Это
значит, что пользователь должен загружать DLL утилиты DeadlockDetection в свое
адресное пространство. Данное требование не такое уж и жесткое, если учесть все
его достоинства. Реализованная в форме DLL, утилита допускала бы легую интег
рацию с пользовательской программой, что позволило бы удовлетворить требо
вание 7.
Вы могли заметить, что я не включил в табл. 151 некоторые функции работы
с сообщениями, способные вызывать блокировку, такие как SendMessage, PostMessage

542

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

и WaitMessage. Сначала я намеревался реализовать и их поддержку, однако когда я
запустил под управлением DeadlockDetection классическую программу Чарльза
Петцольда (Charles Petzold) «Hello World!» с графическим пользовательским ин
терфейсом, DeadlockDetection сообщила столько вызовов, что работа программы
в конечном счете была нарушена. Чтобы сделать DeadlockDetection как можно
компактнее и быстрее, мне пришлось отказаться от этих функций.
Решение проблемы сбора информации, удовлетворяющей требованиям 1–4,
прямо вытекает из выбранного мной подхода внутрипроцессного перехвата фун
кций. Это значит, что при любом вызове функций работы с потоками и функций
синхронизации управление будет передаваться в код DeadlockDetection со всей
нужной мне информацией.
Сделать DeadlockDetection максимально компактной и быстрой (требование 5)
оказалось довольно трудно. Я старался, чтобы код был как можно более эффек
тивным, однако в связи с заданными мной целями при этом возникли трудности.
Так как вам лучше известно, какие типы объектов синхронизации вы используете
в своей программе, я решил сгруппировать их, чтобы вы могли указать именно
те функции, которые хотите перехватывать. Скажем, если вас интересует только
блокировка на мьютексах, вы можете обрабатывать только функции работы с
мьютексами.
Я позволяю во время выполнения указывать, какие наборы функций работы с
объектами синхронизации вы хотите отслеживать. Кроме того, вы можете вклю
чать/отключать DeadlockDetection любое число раз. Вы даже можете назначить
своей программе сочетание клавиш (accelerator) или специальный пункт меню,
который включает/выключает всю систему DeadlockDetection. Такое ограничение
области и времени действия необходимо для соответствия требованию 5 и по
могает удовлетворить требованию 7.
После этого мне осталось разобраться только с требованием 6: обеспечить
максимальную расширяемость обработки выводимой информации. Я хотел пре
доставить вам широкие возможности конфигурирования параметров вывода, а не
навязывать какойлибо жестко закодированный формат. Отделив перехват функ
ций и основную логику программы от кода вывода, я смог улучшить возможность
повторного использования кода, потому что разработать только новый модуль
вывода гораздо проще, чем переписывать ядро программы. Я назвал модули вы
вода расширениями DeadlockDetection или, сокращенно, DeadDetExt. DeadDetExt —
это просто DLL, которые экспортируют несколько функций, вызываемых Dead
lockDetection в случае необходимости.
Что ж, пришло время описать работу с DeadlockDetection.

Использование DeadlockDetection
Перед использованием DeadlockDetection нужно разместить в одном месте DEAD
LOCKDETECTION.DLL, ее файл инициализации и нужную библиотеку DeadDetExt.
Файл инициализации — это простой файл INI, в котором должно быть указано
хотя бы имя загружаемого файла DeadDetExt. Например, файл DEADLOCKDETEC
TION.INI, который загружает поставляемую вместе с утилитой библиотеку TEXT
FILEDDEXT.DLL, содержит следующую информацию:

ГЛАВА 15

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

543

[Initialization]
; Единственное обязательное значение, имя файла DeadDetExt,
; который будет обрабатывать вывод.
ExtDll = "TextFileDDExt.dll"
; Если этот параметр равен 1, DeadlockDetection будет
; выполнять инициализацию в собственной функции DllMain,
; чтобы протоколирование могло быть начато как можно раньше.
StartInDllMain = 0
; Если StartInDllMain равняется 1, этот ключ задает первоначальные
; параметры DeadlockDetection. В нем указываются значения флагов DDOPT_*.
; InitialOpts = 0
; Список модулей, игнорируемых при перехвате функций
; синхронизации. IMM32.DLL — это DLL Input Method Editor (редактор
; методов ввода), которую Windows XP загружает во все процессы.
; Создавайте список в последовательном порядке, начиная с номера 1.
[IgnoreModules]
Ignore1=IMM32.DLL
Как вы можете увидеть по некоторым параметрам INI, DeadlockDetection мо
жет выполнять инициализацию при простом вызове LoadLibrary. Для проактивной
отладки было бы неплохо, чтобы во время инициализации ваше приложение
проверяло конкретный раздел реестра или переменную среды и вызывало при их
наличии LoadLibrary с указанным именем DLL. Благодаря этому вам не нужно было
бы использовать условную компиляцию, и у вас были бы средства чистой загруз
ки DLL в свое адресное пространство. Конечно, это подразумевает, что загружае
мые вами таким образом DLL должны полностью инициализироваться в собствен
ных функциях DllMain и не должны требовать вызова какихнибудь других экс
портируемых функций.
Чтобы вы могли указывать параметры инициализации DeadlockDetection в своем
коде, а не при помощи файла INI, вам нужно включить в свою программу файл
DEADLOCKDETECTION.H и скомпоновать ее с библиотекой DEADLOCKDETEC
TION.LIB. Если вы хотите инициализировать DeadlockDetection сами, вызовите в
нужном месте функцию OpenDeadlockDetection, которая принимает единственный
параметр — первоначальные флаги протоколирования. Все флаги DDOPT_* указа
ны в табл. 152. Вызывать OpenDeadlockDetection следует до того, как ваша программа
начнет создавать потоки, чтобы вы могли записать всю важную информацию об
объектах синхронизации.
Изменять параметры протоколирования можно в любой момент при помощи
функции SetDeadlockDetectionOptions. Она принимает тот же набор объединенных
при помощи операции ИЛИ флагов, что и OpenDeadlockDetection. Чтобы увидеть
текущие параметры, вызовите GetDeadlockDetectionOptions. Во время выполнения
программы можете изменять параметры протоколирования сколько вашей душе
угодно. Для приостановления и возобновления протоколирования служат функ
ции SuspendDeadlockDetection и ResumeDeadlockDetection соответственно.

544

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Табл. 15-2. Параметры протоколирования DeadlockDetection
Флаг

Ограничивает протоколирование

DDOPT_WAIT

Функциями ожидания

DDOPT_THREADS

Функциями работы с потоками

DDOPT_CRITSEC

Функциями работы с критическими секциями

DDOPT_MUTEX

Функциями работы с мьютексами

DDOPT_SEMAPHORE

Функциями работы с семафорами

DDOPT_EVENT

Функциями работы с событиями

DDOPT_ALL

Регистрирует все перехваченные функции

Вместе с исходным кодом DeadlockDetection вы можете найти на диске мою
библиотеку DeadDetExt под названием TEXTFILEDDEXT.DLL. Это относительно
простое расширение записывает всю информацию в текстовый файл. При запус
ке DeadlockDetection вместе с TEXTFILEDDEXT.DLL расширение создает текстовый
файл в том же каталоге, в котором находится выполняемая программа. Текстовый
файл будет иметь имя выполняемой программы с расширением .DD. Например,
при запуске программы DDSIMPTEST.EXE итоговый файл будет назван DDSIMP
TEST.DD. Вот пример вывода, сгенерированного TEXTFILEDDEXT.DLL (листинг 151).

Листинг 15-1. Данные, выводимые утилитой DeadlockDetection
при помощи TEXTFILEDDEXT.DLL
TID
Ret Addr
C/R Ret Value Function & Params
0x00000DF8 [0x004011B2] (R) 0x00000000 InitializeCriticalSection 0x00404150
0x00000DF8 [0x004011CC] (R) 0x000007C0 CreateEventA 0x00000000, 1, 0,
0x004040F0 [The Event Name]
0x00000DF8 [0x004011EF] (R) 0x000007BC CreateThread 0x00000000, 0x00000000,
0x00401000, 0x00000000,
0x00000000, 0x0012FF5C
0x00000DF8 [0x00401212] (R) 0x000007B8 CreateThread 0x00000000, 0x00000000,
0x004010BC, 0x00000000,
0x00000000, 0x0012FF5C
0x00000DF8 [0x00401229] (C)
EnterCriticalSection 0x00404150
0x000000A8 [0x00401030] (C)
EnterCriticalSection 0x00404150
0x00000F04 [0x004010F3] (R) 0x000007B0 OpenEventA 0x001F0003, 0, 0x004040BC
[The Event Name]
0x00000DF8 [0x00401229] (R) 0x00000000 EnterCriticalSection 0x00404150
0x00000DF8 [0x0040123E] (C)
WaitForSingleObject 0x000007C0,
INFINITE
0x00000F04 [0x00401121] (C)
EnterCriticalSection 0x00404150
Заметьте: сведения об именах функций и их параметрах представлены в лис
тинге 151 на нескольких строках, чтобы они помещались на странице. Инфор
мация выводится в таком порядке.
1. Идентификатор выполняемого потока.
2. Адрес возврата, показывающий, какая из ваших функций вызвала функцию
синхронизации. При помощи утилиты CrashFinder из главы 12 можно просмот
реть адреса возврата и узнать, как вы оказались в ситуации блокировки.

ГЛАВА 15

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

545

3. Индикатор вызова/возврата, который помогает определить действия, проис
шедшие до или после конкретных функций.
4. Возвращаемое функцией значение, если ваша программа его сообщает.
5. Имя функции синхронизации.
6. Список параметров функции синхронизации. Значения в квадратных скобках
описывают данные в понятной людям форме. Особое внимание я уделил вы
воду строковых значений, но вы легко реализуете вывод более подробной ин
формации, скажем, отдельных флагов.
Если при запуске вашей программы она заблокируется, завершите процесс и
изучите файл вывода, чтобы узнать, какая функция синхронизации была вызвана
последней. Для обновления информации TEXTFILEDDEXT.DLL сбрасывает файловые
буферы в файл при каждом вызове функций WaitFor*, EnterCriticalSection и TryEnter
CriticalSection.
Предупреждение: если вы включите полное протоколирование всех функций,
почти мгновенно будут созданы очень большие файлы. Так, создав пару потоков
при помощи приложения MTGDI из числа примеров к Visual C++, я за минуту или
две сгенерировал 11Мбайтный текстовый файл.

Реализация DeadlockDetection
Как видите, работать с DeadlockDetection довольно просто. Однако под просто
той ее использования скрывается весьма сложная реализация. В первую очередь
я хочурассказать про перехват функций.

Перехват импортируемых функций
Способов перехвата вызываемых программой функций много. Можно выполнять
поиск всех команд CALL и заменять их операнды собственным адресом, но этот
подход сложен и подвержен ошибкам. К счастью, в случае DeadlockDetection мне
нужно перехватывать импортируемые функции, поэтому их гораздо легче обра
батывать, чем команды CALL.
Импортируемая функция — это функция, которая располагается в DLL. Напри
мер, вызывая OutputDebugString, ваша программа вызывает функцию, находящую
ся в KERNEL32.DLL. Кода я только начал писать программы для Win32, я думал, что
вызов импортируемых функций аналогичен вызовам любых других функций:
команда CALL или команда перехода передает управление по нужному адресу и
начинает выполнение импортируемой функции. Единственное различие могло бы
состоять в том, что в случае импортируемой функции загрузчик программ ОС
должен был бы просмотреть исполняемый файл и исправить адреса, чтобы они
соответствовали той области памяти, в которую будет загружена вызываемая DLL.
Однако, взглянув на действительную реализацию вызовов импортируемых функ
ций, я был поражен ее простотой и элегантностью.
Недостаток только что описанного мной подхода станет очевидным, если учесть
наличие огромного числа APIфункций и возможность вызова одной и той же
функции во многих местах. Если бы загрузчик должен был найти и исправить
каждый вызов, скажем, функции OutputDebugString, загрузка программы могла бы
продолжаться вечность. Даже если б компоновщик создавал таблицу, где указы

546

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

вал бы место каждого вызова OutputDebugString, загрузка программы была бы му
чительно медленной изза огромного объема работы, связанной с циклами и за
писью в память.
Так как же загрузчик сообщает программе о том, где находится импортируе
мая функция? Решение чертовски умно. Представив, куда направляются вызовы
OutputDebugString, вы вскоре поймете, что каждый вызов должен обращаться к одному
и тому же адресу памяти, по которому OutputDebugString была загружена. Конеч
но, ваша программа не может знать этот адрес заранее, поэтому все вызовы Output
DebugString выполняются посредством единственного косвенного адреса. При за
грузке вашего исполняемого файла и нужных ему DLL загрузчик корректирует этот
единственный косвенный адрес, чтобы он соответствовал итоговому адресу за
грузки OutputDebugString. Чтобы косвенная адресация работала, компилятор гене
рирует при каждом вызове импортируемой функции переход к этому косвенно
му адресу. Косвенный адрес хранится в исполняемом файле в разделе .idata (или
import). Если вы импортируете функцию, объявляя ее как __declspec(dllimport), то
вместо косвенного перехода будет косвенный вызов, что экономит несколько
команд на каждом вызове функции.
Чтобы установить ловушку для импортируемой функции, нужно отыскать в
исполняемом файле раздел импорта, найти адрес нужной функции и заменить его
адресом функцииловушки. Вам может показаться, что это потребует большого
объема работы, однако все не так уж плохо, так как формат файлов Win32 Portable
Executable (PE) организован очень разумно.
Метод установки ловушки для импортируемых функций описан в главе 10 ве
ликолепной книги Мэтта Питрека (Matt Pietrek) «Windows 95 System Programming
Secrets» (IDG Books, 1995). Мэтт просто ищет для модуля раздел импорта и про
сматривает в цикле импортируемые функции, используя значение, возвращаемое
функцией GetProcAddress. Обнаружив нужную функцию, он перезаписывает ее
первоначальный адрес адресом функцииловушки.
С момента издания книги Мэтта в 1995 г. в мире программирования произош
ли два небольших изменения. Вопервых, когда Мэтт писал свою книгу, большин
ство программистов не объединяло раздел импорта с другими разделами PEфайла.
Поэтому, если раздел импорта располагается в памяти, доступной только для чте
ния, попытка перезаписи адреса функции приведет к нарушению доступа. Чтобы
избежать этой ошибки, я перед записью адреса функцииловушки устанавливаю
защиту виртуальной памяти в состояние разрешения чтения и записи. Вторая, чуть
более сложная проблема связана с невозможностью перехвата в некоторых слу
чаях импортируемых функций в Microsoft Windows Me. Очень многие спрашива
ют меня о перехвате функций, поэтому я решил реализовать его и для Windows
Me и рассказать, что происходит в этой ОС.
Работая с DeadlockDetection, вам хотелось бы иметь возможность перенаправ
ления функций работы с потоками при любом запуске своей программы, даже когда
она выполняется под управлением отладчика. Однако установка ловушек под управ
лением отладчика может представлять проблему, хотя на первый взгляд так не
кажется. Получив адрес функции при помощи GetProcAddress в Windows XP или
при выполнении программы в Windows Me вне отладчика, вы всегда сможете найти
этот адрес в разделе импорта. Но в Windows Me адрес, возвращаемый функцией

ГЛАВА 15

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

547

GetProcAddress в программе, выполняемой под управлением отладчика, отличает
ся от адреса, получаемого при выполнении вне отладчика. В первом случае GetProc
Address на самом деле возвращает отладочный шлюз (debug thunk) — специаль
ную оболочку для действительного вызова.
Отладочный шлюз нужен потому, что Windows Me не выполняет копирование
при записи (copyonwrite) для адресов, расположенных выше 2 Гб. Копирование
при записи предполагает, что при записи в страницу разделяемой памяти ОС делает
копию страницы и предоставляет ее процессу, выполняющему запись. Обычно
Windows Me и Windows XP следуют одинаковым правилам, и все работает отлич
но. Однако для разделяемой памяти, находящейся выше 2 Гб, где в Windows Me
загружаются все DLL системы, Windows Me не выполняет копирования при запи
си. Это значит, что при изменении памяти в DLL системы в результате установки
точки прерывания или исправления функции изменение произойдет для всех
процессов ОС, что вызовет ее крах, если измененная область будет использоваться
другим процессом. Поэтому Windows Me прилагает серьезные усилия, чтобы по
мешать вам исказить эту память.
Отладочный шлюз, возвращаемый GetProcAddress при выполнении под отлад
чиком, — это средство, при помощи которого Windows Me предотвращает попытки
отладки системных функций, расположенных выше 2 Гб. В целом отсутствие ко
пирования при записи большинство программистов волновать не должно; оно
представляет проблему только для тех, кто разрабатывает отладчики или желает
корректно перехватывать функции независимо от того, выполняется программа
под отладчиком или нет.
К счастью, получить действительный адрес импортируемой функции не так
сложно — просто для этого требуется чуть поработать, избегая при этом GetProc
Address. Структура IMAGE_IMPORT_DESCRIPTOR в PEфайле, которая содержит всю ин
формацию о функциях, импортируемых из конкретной DLL, имеет указатели на
два массива в исполняемом файле — таблицы адресов импортируемых функций
(import address table, IAT) (иногда их называют массивами данных шлюзов — thunk
data array). Первый указатель указывает на действительную IAT, который загруз
чик программ корректирует при загрузке исполняемого файла, второй — на ис
ходную IAT, содержащую адреса импортируемых функций и не изменяемую заг
рузчиком. Итак, для обнаружения действительного адреса импортируемой функ
ции нужно просто найти ее в исходной IAT; после этого надо записать адрес ло
вушки в соответствующий элемент действительной IAT, используемой програм
мой. Благодаря этому ловушка будет работать всегда независимо от того, где она
вызывается.
Всю работу, связанную с установкой ловушек, выполняет моя функция HookIm
portedFunctionsByName (табл. 153). Так как я хотел сделать перехват функций как
можно более общим, я реализовал возможность одновременного перехвата не
скольких функций, импортируемых из одной DLL. Как можно догадаться по име
ни, HookImportedFunctionsByName перехватывает только те функции, которые импор
тируются по имени. Для перехвата функций, экспортируемых по ординалу, я на
писал функцию HookOrdinalExport, но я не буду рассматривать ее в этой книге.

548

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Табл. 15-3. Описание параметров функции HookImportedFunctionsByName
Параметр

Описание

hModule

Модуль, в котором находятся перехватываемые импортируемые
функции.

szImportMod

Имя модуля импортируемых функций.

uiCount

Число перехватываемых функций. Этот параметр равен числу
элементов массивов paHookArray и paOrigFuncs.

paHookArray

Массив указателей на структуры дескрипторов функций, в котором
указываются перехватываемые функции. Он не обязан быть сортиро
ванным по порядку имен szFunc (хотя это было бы мудрым решением,
потому что в будущем я могу реализовать лучший алгоритм поиска).
Если конкретный указатель pProc имеет значение NULL,
HookImportedFunctionsByName пропускает этот элемент. Структура каждого
элемента в массиве paHookArray содержит имя перехватываемой функ
ции и указатель на новую функциюловушку. Чтобы вы в любой мо
мент могли установить/удалить ловушку, HookImportedFunctionsByName
возвращает все исходные адреса импортируемых функций.

paOrigFuncs

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

pdwHooked

Возвращает число перехваченных функций из массива paHookArray.

В листинге 152 показана функция HookImportedFunctionsByNameA, но в своем коде
вы будете вызывать HookImportedFunctionsByName: этот макрос выполняет отображение
между форматами ANSI и Unicode. Однако, поскольку все имена в разделе IAT
хранятся в формате ANSI, я реализовал и функцию HookImportedFunctionsByNameW,
которая просто преобразует соответствующие параметры в формат ANSI и вызы
вает HookImportedFunctionsByNameA.

Листинг 15-2. Функция HookImportedFunctionsByNameA из файла
HOOKIMPORTEDFUNCTIONBYNAME.CPP
BOOL BUGSUTIL_DLLINTERFACE __stdcall
HookImportedFunctionsByNameA ( HMODULE
hModule
,
LPCSTR
szImportMod ,
UINT
uiCount
,
LPHOOKFUNCDESC paHookArray ,
PROC *
paOrigFuncs ,
LPDWORD
pdwHooked
)
{
// Проверка параметров.
ASSERT ( FALSE == IsBadReadPtr ( hModule
,
sizeof ( IMAGE_DOS_HEADER ) ) ) ;
ASSERT ( FALSE == IsBadStringPtrA ( szImportMod , MAX_PATH ) ) ;
ASSERT ( 0 != uiCount ) ;
ASSERT ( NULL != paHookArray ) ;
ASSERT ( FALSE == IsBadReadPtr ( paHookArray ,
sizeof (HOOKFUNCDESC) * uiCount ));

ГЛАВА 15

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

549

// В отладочных компоновках выполняется глубокая проверка paHookArray.
#ifdef _DEBUG
if ( NULL != paOrigFuncs )
{
ASSERT ( FALSE == IsBadWritePtr ( paOrigFuncs ,
sizeof ( PROC ) * uiCount ) );
}
if ( NULL != pdwHooked )
{
ASSERT ( FALSE == IsBadWritePtr ( pdwHooked , sizeof ( UINT )));
}
// Проверка всех элементов массива перехватываемых функций.
{
for ( UINT i = 0 ; i < uiCount ; i++ )
{
ASSERT ( NULL != paHookArray[ i ].szFunc ) ;
ASSERT ( '\0' != *paHookArray[ i ].szFunc ) ;
// Если адрес функции не равен NULL, выполняется его проверка.
if ( NULL != paHookArray[ i ].pProc )
{
ASSERT ( FALSE == IsBadCodePtr ( paHookArray[i].pProc));
}
}
}
#endif
// Дополнительная проверка параметров и установка кода ошибки.
if ( ( 0
== uiCount
)
||
( NULL == szImportMod )
||
( TRUE == IsBadReadPtr ( paHookArray ,
sizeof ( HOOKFUNCDESC ) * uiCount ) ))
{
SetLastErrorEx ( ERROR_INVALID_PARAMETER , SLE_ERROR ) ;
return ( FALSE ) ;
}
if ( ( NULL != paOrigFuncs )
&&
( TRUE == IsBadWritePtr ( paOrigFuncs ,
sizeof ( PROC ) * uiCount ) ) )
{
SetLastErrorEx ( ERROR_INVALID_PARAMETER , SLE_ERROR ) ;
return ( FALSE ) ;
}
if ( ( NULL != pdwHooked )
&&
( TRUE == IsBadWritePtr ( pdwHooked , sizeof ( UINT ) ) ) )
{
SetLastErrorEx ( ERROR_INVALID_PARAMETER , SLE_ERROR ) ;
return ( FALSE ) ;
}
см. след. стр.

550

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// Здесь я проверяю, расположена ли данная системная DLL выше 2 Гб,
// в случае чего Windows 98 не позволит ее скорректировать.
if ( ( FALSE == IsNT ( ) ) && ( (DWORD_PTR)hModule >= 0x80000000 ) )
{
SetLastErrorEx ( ERROR_INVALID_HANDLE , SLE_ERROR ) ;
return ( FALSE ) ;
}
// СООБРАЖЕНИЯ ПО ПОВОДУ УЛУЧШЕНИЯ ПРОГРАММЫ
// Следует ли проверять каждый элемент массива
// перехватываемых фукнций в заключительных компоновках?
if ( NULL != paOrigFuncs )
{
// Присвоение всем элементам массива paOrigFuncs значения NULL.
memset ( paOrigFuncs , NULL , sizeof ( PROC ) * uiCount ) ;
}
if ( NULL != pdwHooked )
{
// Присвоение числу перехваченных функций значения 0.
*pdwHooked = 0 ;
}
// Получение специфического дескриптора импорта.
PIMAGE_IMPORT_DESCRIPTOR pImportDesc =
GetNamedImportDescriptor ( hModule , szImportMod );
if ( NULL == pImportDesc )
{
// Запрошенный модуль не был импортирован. Не возвращать ошибку.
return ( TRUE ) ;
}
//
//
//
//
if

ИСПРАВЛЕННАЯ ОШИБКА. Спасибо Аттиле Шепезвари (Attila Szepesvary)!
Проверка того, что первый шлюз и исходный первый шлюз
не равны NULL. Исходный первый шлюз может быть нулевым
дескриптором импорта, что вызвало бы крах этой функции.
( ( NULL == pImportDesc>OriginalFirstThunk ) ||
( NULL == pImportDesc>FirstThunk
)
)

{
// Я возвращаю TRUE, потому что это аналогично случаю,
// в котором запрошенный модуль не был импортирован.
// Все в порядке!
SetLastError ( ERROR_SUCCESS ) ;
return ( TRUE ) ;
}
//
//
//
//

Получение информации об исходном шлюзе для этой DLL.
Я не могу использовать информацию, хранимую
в pImportDesc>FirstThunk, так как загрузчик уже изменил
этот массив во время коррекции импортируемых функций.

ГЛАВА 15

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

// Исходный шлюз предоставляет мне доступ к именам функций.
PIMAGE_THUNK_DATA pOrigThunk =
MakePtr ( PIMAGE_THUNK_DATA
,
hModule
,
pImportDesc>OriginalFirstThunk )
// Получение указателя на массив pImportDesc>FirstThunk, при
// помощи которого выполняется действительный перехват функций.
PIMAGE_THUNK_DATA pRealThunk = MakePtr ( PIMAGE_THUNK_DATA
hModule
pImportDesc>FirstThunk

551

;

,
,
);

// Поиск перехватываемых функций.
while ( NULL != pOrigThunk>u1.Function )
{
// Выполняется поиск только тех функций, которые
// импортируются по имени, но не по значению ординала.
if ( IMAGE_ORDINAL_FLAG !=
( pOrigThunk>u1.Ordinal & IMAGE_ORDINAL_FLAG ))
{
// Изучение имени этой импортируемой функции.
PIMAGE_IMPORT_BY_NAME pByName ;
pByName = MakePtr ( PIMAGE_IMPORT_BY_NAME
,
hModule
,
pOrigThunk>u1.AddressOfData ) ;
// Если имя начинается с NULL, оно пропускается.
if ( '\0' == pByName>Name[ 0 ] )
{
// ИСПРАВЛЕННАЯ ОШИБКА (спасибо Аттиле Шепезвари!)
// Я забыл про увеличение указателей на шлюз!
pOrigThunk++ ;
pRealThunk++ ;
continue ;
}
// Этот флаг показывает, перехватываю ли я функцию.
BOOL bDoHook = FALSE ;
//
//
//
//
//
//
//
//
//
//

СООБРАЖЕНИЯ ПО ПОВОДУ УЛУЧШЕНИЯ ПРОГРАММЫ
Возможно, здесь следует реализовать двоичный поиск.
Я проверяю, есть ли имя этой импортируемой функции
в массиве перехватываемых функций. Возможно, paHookArray
следует держать отсортированным по именам функций, чтобы
можно было ускорить его просмотр при помощи двоичного
поиска. Однако передаваемый в эту функцию параметр
uiCount будет довольно небольшим, поэтому при поиске
каждой функции, импортируемой по szImportMod, вполне
допустимо просматривать весь массив paHookArray.
см. след. стр.

552

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

for ( UINT i = 0 ; i < uiCount ; i++ )
{
if ( ( paHookArray[i].szFunc[0] ==
pByName>Name[0] ) &&
( 0 == strcmpi ( paHookArray[i].szFunc ,
(char*)pByName>Name ) )
)
{
// Если адрес функции равен NULL, выполняется выход;
// в противном случае функция перехватывается.
if ( NULL != paHookArray[ i ].pProc )
{
bDoHook = TRUE ;
}
break ;
}
}
if ( TRUE == bDoHook )
{
// Я обнаружил функцию, которую нужно перехватить. Теперь,
// прежде чем перезаписать указатель на функцию, я должен
// изменить защиту памяти, разрешив запись в нее. Заметьте,
// что я выполняю запись в область действительного шлюза!
MEMORY_BASIC_INFORMATION mbi_thunk ;
VirtualQuery ( pRealThunk
,
&mbi_thunk
,
sizeof ( MEMORY_BASIC_INFORMATION ) ) ;
if ( FALSE == VirtualProtect ( mbi_thunk.BaseAddress ,
mbi_thunk.RegionSize ,
PAGE_READWRITE
,
&mbi_thunk.Protect
))
{
ASSERT ( !"VirtualProtect failed!" ) ;
SetLastErrorEx ( ERROR_INVALID_HANDLE , SLE_ERROR );
return ( FALSE ) ;
}
// Сохранение исходного адреса в случае надобности.
if ( NULL != paOrigFuncs )
{
paOrigFuncs[i] =
(PROC)((INT_PTR)pRealThunk>u1.Function) ;
}
// Перехват функции.
DWORD_PTR * pTemp = (DWORD_PTR*)&pRealThunk>u1.Function ;
*pTemp = (DWORD_PTR)(paHookArray[i].pProc);

ГЛАВА 15

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

553

DWORD dwOldProtect ;
// Возвращение параметра защиты в состояние,
// бывшее до перезаписи указателя на функцию.
VERIFY ( VirtualProtect ( mbi_thunk.BaseAddress
mbi_thunk.RegionSize
mbi_thunk.Protect
&dwOldProtect

,
,
,
) ) ;

if ( NULL != pdwHooked )
{
// Увеличение общего числа перехваченных функций.
*pdwHooked += 1 ;
}
}
}
// Увеличение указателей на обе таблицы.
pOrigThunk++ ;
pRealThunk++ ;
}
// Все OK!
SetLastError ( ERROR_SUCCESS ) ;
return ( TRUE ) ;
}
HookImportedFunctionsByName не должна быть слишком сложной для понимания.
После тщательной профилактической проверки каждого параметра я вызываю
вспомогательную функцию GetNamedImportDescriptor, выполняющую поиск IMAGE_IM
PORT_DESCRIPTOR для запрошенного модуля. Получив указатели на исходную и дей!
ствительную IAT, я просматриваю исходную IAT и изучаю каждую функцию, им!
портируемую по имени, чтобы узнать, есть ли она в списке paHookArray. Если фун!
кция имеется в списке перехватываемых функций, я просто разрешаю запись в
область памяти действительной IAT, записываю вместо адреса действительной
функции адрес ловушки и возвращаю защиту памяти в исходное состояние.
В исходный код BUGSLAYERUTIL.DLL я включил функцию блочного теста для Hook
ImportedFunctionsByName, которая поможет вам со всем разобраться, если вы не очень
внимательно следили за происходящим.
Теперь, когда вы представляете механизм перехвата импортируемых функций,
займемся реализацией остальной части DeadlockDetection.

Детали реализации
Одна из моих основных целей при реализации DeadlockDetection состояла в том,
чтобы сделать утилиту максимально ориентированной на использование данных
и таблиц. Поразмыслив о том, как выполняется перехват функций DLL, вы пой!
мете, что его механизм почти идентичен для всех функций, указанных в табл. 15!1.
Функция!ловушка вызывается, определяет, отслеживается ли ее класс функций,

554

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

вызывает действительную функцию и (если для этого класса включено протоко!
лирование) записывает информацию и выполняет возврат. Я должен был напи!
сать ряд похожих функций!ловушек и хотел сделать их как можно проще. Слож!
ные функции!ловушки — плодородная почва для ошибок, которые могут прокра!
сться в ваш код, даже когда вы пытаетесь все упростить. Чуть ниже я расскажу про
одну неприятную ошибку в коде DeadlockDetection в первом издании этой книги.
Лучше всего показать эту простоту, обсудив написание DLL DeadDetExt. Биб!
лиотека DeadDetExt должна иметь три экспортируемых функции. Роль первых двух,
DeadDetExtOpen и DeadDetExtClose, очевидна. Интерес представляет DeadDetProcessEvent,
вызываемая каждой функцией!ловушкой при наличии информации для записи.
DeadDetProcessEvent принимает единственный параметр — указатель на структуру
DDEVENTINFO:

typedef struct tagDDEVENTINFO
{
// Идентификатор, определяющий содержание оставшейся части структуры.
eFuncEnum
eFunc
;
// Индикатор предварительного или заключительного вызова.
ePrePostEnum ePrePost
;
// Адрес возврата. Он нужен для нахождения вызвавшей функции.
DWORD
dwAddr
;
// Идентификатор вызвавшего потока.
DWORD
dwThreadId
;
// Значение, возвращаемое при заключительных вызовах.
DWORD
dwRetValue
;
// Информация о параметрах. Приводите этот элемент к указателю
// на структуру, соответствующую функции, как описано ниже. При доступе
// к параметрам обращайтесь с ними, как со значениями только для чтения.
DWORD
dwParams
;
} DDEVENTINFO , * LPDDEVENTINFO ;
Весь вывод для какой!либо функции из листинга 15!1 основан на информа!
ции, содержащейся в структуре DDEVENTINFO. Большинство полей DDEVENTINFO гово!
рит само за себя, а вот dwParams требует пояснения. Это поле является на самом
деле указателем на параметры в том порядке, в котором они расположены в па!
мяти.
В главе 7 я рассказал о том, как параметры передаются в стек. Напомню, что
параметры функций с соглашениями вызова __stdcall и __cdecl передаются спра!
ва налево, а стек растет по направлению от старших адресов памяти к младшим.
Поле dwParams структуры DDEVENTINFO указывает на последний параметр в стеке, т. е.
слева направо. Чтобы обеспечить легкое преобразование dwParams, я прибегнул к
приведению типов.
В файле DEADLOCKDETECTION.H содержатся объявления typedef, описываю!
щие списки параметров каждой перехватываемой функции. Например, если бы
поле eFunc соответствовало значению eWaitForSingleObjectEx, то для получения
параметров нужно было бы привести тип dwParams к LPWAITFORSINGLEOBJECTEX_PARAMS.
Чтобы увидеть все это творческое приведение типов в действии, изучите код биб!
лиотеки TEXTFILEDDEXT.DLL (см. CD, прилагаемый к книге).

ГЛАВА 15

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

555

Хотя обработка вывода относительно проста, сбор информации может оказаться
сложным. Мне требовалось, чтобы DeadlockDetection перехватывала функции
синхронизации из табл. 15!1, но я не хотел, чтобы функции!ловушки изменяли
поведение действительных функций. Я также хотел получать параметры и возвра!
щаемые значения и с легкостью писать функции!ловушки на C/C++. Я провел за
отладчиком и дизассемблером немало времени, пока мне удалось сделать это
правильно.
Первоначально я сделал все функции!ловушки сквозными (pass!through func!
tion), чтобы они вызывали действительные функции непосредственно. Этот под!
ход работал отлично. Затем я поместил параметры функций и возвращаемые ими
значения в локальные переменные. Получение возвращаемого значения из дей!
ствительной функции оказалось простым, но из!за того, что я начал реализацию
DeadlockDetection на Visual C++ 6, у меня не было чистого способа получения
адресов возврата в моих функциях!ловушках C/C++. Visual C++ .NET поддержива!
ет внутреннюю (intrinsic) функцию _ReturnAddress, но в Visual C++ 6 такой возмож!
ности не было. Мне нужно было значение DWORD прямо перед текущим указателем
стека. Увы, в обычном C/C++ пролог функции уже выполнил бы все свои действия
к тому времени, когда я смог бы получить управление, и указатель стека имел бы
не то значение, которое мне было нужно.
Вы можете подумать, что указатель стека — это просто смещение, определяе!
мое числом локальных переменных, но это не всегда так. Компилятор Visual C++
выполняет великолепную оптимизацию, так что при различных конфигурациях
флагов оптимизации указатель стека может иметь разные значения. Так, когда вы
объявляете переменную как локальную, компилятор может оптимизировать ра!
боту с ней, сохранив в регистре, из!за чего она даже не появится в стеке.
Мне нужен был гарантированный способ получения указателя стека незави!
симо от параметров оптимизации. В этот момент я начал думать, почему бы не
объявить функции!ловушки как __declspec(naked) и не создать собственные про!
лог и эпилог? Это дало бы мне полный контроль над регистром ESP независимо
от параметров оптимизации. Кроме того, это облегчило бы и получение адреса
возврата и параметров, так как они находятся по смещениям ESP+04h и ESP+08h
соответственно. Помните, что мои пролог и эпилог не представляют собой ниче!
го сверхъестественного, поэтому я все же выполняю обычные команды PUSH EBP и
MOV EBP, ESP в прологе и MOV ESP, EBP и POP EBP в эпилоге.
Решив объявлять все функции!ловушки как __declspec(naked), я написал для
обработки пролога и эпилога два макроса: HOOKFN_PROLOG и HOOKFN_EPILOG. Кроме того,
я заблаговременно объявил в HOOKFN_PROLOG некоторые общие локальные переменные,
нужные всем функциям!ловушкам. В число этих переменных вошли значение
последней ошибки, dwLastError, и структура информации о событии, stEvtInfo,
передаваемая в DLL DeadDetExt. Переменная dwLastError — просто еще один при!
знак состояния, который мне нужно сохранять при перехвате функций.
При помощи функции SetLastError Windows API возвращает специальный код
ошибки, предоставляя более подробную информацию в случае неудачи функции.
Этот код ошибки может быть настоящим благословением, потому что он сооб!
щает о причине неудачи API!функции. Так, если GetLastError возвратит 122, вы будете
знать, что причиной ошибки стал недостаточный размер переданного в функцию

556

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

буфера. Все возвращаемые ОС коды ошибок указаны в файле WINERROR.H. Про!
блема с функциями!ловушками в том, что во время своего выполнения они могут
перезаписать значение последенй ошибки. Это может привести к катастрофе, если
значение последней ошибки используется вашей программой.
Если при вызове CreateEvent вы хотите узнать, был ли возвращаемый описатель
создан или просто открыт, вы также можете использовать код последней ошиб!
ки: если CreateEvent просто открыла описатель, код будет иметь значение ERROR_AL
READY_EXISTS. Одно из важнейших правил перехвата функций гласит, что вы не
можете изменять ожидаемое поведение функции, поэтому сразу же после вызова
действительной функции я должен был вызыать GetLastError, чтобы моя функция!
ловушка могла правильно установить код последней ошибки, возвращаемый дей!
ствительной функцией. Общее правило написания функций!ловушек таково: сразу
после вызова действительной функции нужно вызывать GetLastError, а непосред!
ственно перед выходом из ловушки устанавливать код ошибки при помощи Set
LastError.

Стандартный вопрос отладки
Если теперь доступна ReturnAddress, почему вы не использовали ее,
а пошли на все эти проблемы?
Когда пришло время обновить DeadlockDetection для второго издания этой
книги, я думал немного упростить свою жизнь и изменить макрос HOOK
FN_PROLOG, чтобы он использовал новую внутреннюю функцию _ReturnAddress.
Это значио бы, что я могу избавиться от объявлений naked, привести функ!
ции к более нормальному виду и не создавать собственные пролог и эпи!
лог. Однако существующие макросы дают мне одно большое преимущество:
я могу обращаться с параметрами, как с блоками памяти, и передавать их
прямо в функцию вывода. Если б я применял стандартные функции, мне
нужно было бы выполнять странное приведение типов для достижения того
же результата. Кроме того, у меня был самый весомый аргумент: имевший!
ся код работал очень хорошо, и мне не хотелось его переписывать. Поэто!
му я оставил функции с соглашением naked прежними.
В этот момент я подумал, что все, кроме тестирования, сделано. Увы, во время
первого теста я нашел ошибку: между вызовами ловушек я не сохранял регистры
ESI и EDI, потому что в документации к встроенному ассемблеру сказано, что их
сохранять не требуется. После решения этой проблемы казалось, что Deadlock!
Detection работает прекрасно. Однако когда я начал сравнивать регистры до, во
время и после вызовов функций, я заметил, что я не возвращаю значения, сохра!
няемые действительными функциями в EBX, ECX и EDX и, что еще хуже, в регистре
флагов. Хотя я не видел в этом никаких проблем и в документации говорилось,
что эти регистры сохранять не требуется, я все же был озабочен тем, что мои
функции!ловушки изменяли состояние приложения. Для сохранения значений
регистров после вызовов действительных функций я объявил структуру REGSTATE,
чтобы можно было восстанавливать регистры по возвращении из функции!ловуш!
ки. Для сохранения и восстановления регистров я создал два дополнительных

ГЛАВА 15

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

557

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

MOV DWORD PTR [EBP018h] , 00000002h
MOV DWORD PTR [EBP014h] , 00000002h
преобразовывались оптимизатором в:

PUSH
POP
MOV
MOV

002h
EBX
DWORD PTR [EBP01Ch] , EBX
DWORD PTR [EBP018h] , EBX

Легко увидеть, что во втором фрагменте команда POP EBX искажает значение реги!
стра. Чтобы помешать оптимизатору искажать регистры без моего ведома, я от!
ключил оптимизацию для всех функций!ловушек, поместив директиву:

#pragma optimize("", off )
в начало каждого файла. Кроме того, отключение оптимизации упростило отлад!
ку, потому что генерируемый компилятором неоптимизированный код очень похож
и в заключительных, и в отладочных компоновках.
В листинге 15!3 приведена заключительная версия внутреннего заголовочно!
го файла DD_FUNCS.H, в котором объявлены все специальные макросы для функ!
ций!ловушек. В комментарии в начале файла вы можете найти два примера функ!
ций!ловушек, поясняющих применение каждого специального макроса. Внима!
тельно изучите пример DDSimpTest, который можно найти на CD. Исследуйте
вызовы функций на языке ассемблера полностью, потому что это единственный
способ увидеть все выполняемые действия.

Листинг 15-3.

Файл DD_FUNCS.H

/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright © 19972003 John Robbins — All rights reserved.
———————————————————————————————————————————————————————————————————————
Прототипы для всех функцийловушек и кода пролога/эпилога
——————————————————————————————————————————————————————————————————————*/
#ifndef _DD_FUNCS_H
#define _DD_FUNCS_H
/*//////////////////////////////////////////////////////////////////////
см. след. стр.

558

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Все функцииловушки объявляются с соглашением __declspec(naked),
поэтому я должен сам написать пролог и эпилог. Я должен предоставить
собственные пролог и эпилог по нескольким причинам.
1. Функции, написанные на C, не позволяют контролировать использование
регистров и сохранение исходных регистров компилятором.
Отсутствие контроля над регистрами означает, что получить адрес
возврата почти невозможно. Для проекта DeadlockDetection адрес
возврата очень важен.
2. Я хотел передавать параметры в функцию обработки из DLL расширения,
не копируя при каждом вызове функции большие объемы данных.
3. Так как почти все функцииловушки ведут себя похожим образом, я
могу присвоить значения общим переменным, нужным во всех функциях.
4. Функцииловушки не должны изменять возвращаемые значения, в том
числе значение, возвращаемое GetLastError. Собственные пролог
и эпилог позволяют мне значительно упростить возвращение правильного
значения. Кроме того, я должен восстанавливать значения регистров
в то состояние, в каком они были после вызова действительной функции.
Базовая функцияловушка требует только двух
макросов: HOOKFN_STARTUP и HOOKFN_SHUTDOWN.
Как вы можете видеть, это здорово облегчает работу!
BOOL NAKEDDEF DD_InitializeCriticalSectionAndSpinCount (
LPCRITICAL_SECTION lpCriticalSection,
DWORD
dwSpinCount
)
{
HOOKFN_STARTUP ( eInitializeCriticalSectionAndSpinCount ,
DDOPT_CRITSEC
,
0
) ;
InitializeCriticalSectionAndSpinCount ( lpCriticalSection ,
dwSpinCount
) ;
HOOKFN_SHUTDOWN ( 2 , DDOPT_CRITSEC ) ;
}
Если надо выполнить специальную обработку и вы
не хотите делать чегото, что нельзя выполнить,
используя обычные макросы, вам помогут макросы:
HOOKFN_PROLOG
REAL_FUNC_PRE_CALL
REAL_FUNC_POST_CALL
HOOKFN_EPILOG
Пример функции, использующей указанные макросы:
HMODULE NAKEDDEF DD_LoadLibraryA ( LPCSTR lpLibFileName )
{
// Все локальные переменные должны быть объявлены

ГЛАВА 15

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

559

// до макроса HOOKFN_PROLOG. Он создает фактический
// пролог функции и автоматически определяет некоторые
// важные переменные, такие как stEvtInfo (DDEVENTINFO).
HOOKFN_PROLOG ( ) ;
// Перед вызовом действительной функции нужно указать
// макрос REAL_FUNC_PRE_CALL, чтобы регистры имели те
// же значения, что и при вызове функцииловушки.
REAL_FUNC_PRE_CALL ( ) ;
// Вызов действительной функции.
LoadLibraryA ( lpLibFileName ) ;
// Макрос для сохранения регистров после вызова действительной
// функции. Благодаря этому я могу присвоить регистрам нужные
// значения перед выходом из ловушки, сделав ее "невидимой".
REAL_FUNC_POST_CALL ( ) ;
// Операции, специфичные для ловушки LoadLibraryA.
if ( NULL != stEvtInfo.dwRetValue )
{
HookAllLoadedModules ( ) ;
}
// Этот макрос создает эпилог функции, восстанавливая
// регистры и стек. Значение, передаваемое макросу, — это
// число параметров, переданных в функцию. Вырезая и вставляя
// данный макрос в другие функции, будьте очень внимательны,
// чтобы не допустить ошибку "наследования при редактировании"!
HOOKFN_EPILOG ( 1 ) ;
}
//////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////
// Структура состояния регистров. Я использую эту структуру, чтобы
// гарантировать, что ВСЕ регистры по возвращении будут иметь такие
// же значения, какие были после выполнения действительной функции.
// Обратите внимание, что EBP и ESP обрабатываются во время пролога.
//////////////////////////////////////////////////////////////////////*/
typedef struct tag_REGSTATE
{
DWORD dwEAX ;
DWORD dwEBX ;
DWORD dwECX ;
DWORD dwEDX ;
DWORD dwEDI ;
DWORD dwESI ;
DWORD dwEFL ;
} REGSTATE , * PREGSTATE ;
/*//////////////////////////////////////////////////////////////////////
// Макросы для сохранения и восстановления ESI между
см. след. стр.

560

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// вызовами функций в отладочных компоновках.
//////////////////////////////////////////////////////////////////////*/
#ifdef _DEBUG
#define SAVE_ESI()
__asm PUSH ESI
#define RESTORE_ESI() __asm POP ESI
#else
#define SAVE_ESI()
#define RESTORE_ESI()
#endif
/*//////////////////////////////////////////////////////////////////////
// Общий пролог для всех функций DD_*.
//////////////////////////////////////////////////////////////////////*/
#define HOOKFN_PROLOG()
\
/* Все функцииловушки автоматически получают
*/\
/* три одинаковых локальных переменных.
*/\
DDEVENTINFO stEvtInfo ; /* Информация о событии для функции.
*/\
DWORD
dwLastError ; /* Значение последней ошибки.
*/\
REGSTATE
stRegState ; /* Состояние регистров, нужное для их
*/\
/* правильного восстановления.
*/\
{
\
__asm PUSH EBP
/* Всегда явно сохраняйте EBP.
*/\
__asm MOV EBP , ESP
/* Настройка кадра стека.
*/\
__asm MOV EAX , ESP
/* Получение указателя стека для подсчета*/\
/* адреса возврата и адреса параметров. */\
SAVE_ESI ( )
/* Сохранение ESI в отлад. компоновках. */\
__asm SUB ESP , __LOCAL_SIZE /* Место для локальных переменных.
*/\
__asm ADD EAX , 04h + 04h /* Нужно учесть команду PUSH EBP
*/\
/* и адрес возврата.
*/\
/* Сохранение начала параметров в стеке. */\
__asm MOV [stEvtInfo.dwParams] , EAX
\
__asm SUB EAX , 04h
/* Вернуться к адресу возврата.
*/\
__asm MOV EAX , [EAX]
/* Теперь EAX содержит адрес возврата. */\
/* Сохранение адреса возврата.
*/\
__asm MOV [stEvtInfo.dwAddr] , EAX
\
__asm MOV dwLastError , 0 /* Инициализация dwLastError.
*/\
/* Инициализация информации о событии. */\
__asm MOV [stEvtInfo.eFunc] , eUNINITIALIZEDFE
\
__asm MOV [stRegState.dwEDI] , EDI /* Сохранение двух регистров, */\
__asm MOV [stRegState.dwESI] , ESI /* которые нужно сохранять
*/\
/* между вызовами функций.
*/\
}
/*//////////////////////////////////////////////////////////////////////
// Общий эпилог для всех функций DD_*. INumParams — это число
// параметров функции, используемое для восстановления
// правильного состояния стека после вызова ловушки.
//////////////////////////////////////////////////////////////////////*/
#define HOOKFN_EPILOG(iNumParams)
\
{
\

ГЛАВА 15

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

SetLastError ( dwLastError ) ;
__asm ADD

ESP ,

__asm MOV
__asm MOV
__asm MOV
__asm MOV
__asm MOV
__asm MOV
__asm SAHF
__asm MOV
RESTORE_ESI

EBX
ECX
EDX
EDI
ESI
EAX

__asm MOV
__asm POP
__asm RET

,
,
,
,
,
,

/*
/*
__LOCAL_SIZE
/*
/*
[stRegState.dwEBX]/*
[stRegState.dwECX]/*
[stRegState.dwEDX]/*
[stRegState.dwEDI]/*
[stRegState.dwESI]
[stRegState.dwEFL]

EAX , [stRegState.dwEAX]
( )
/*
/*
ESP , EBP
/*
EBP
/*
iNumParams * 4
/*
/*

561

Установка кода последней
*/\
ошибки действительной фции. */\
Добавление к ESP размера
*/\
локальных переменных.
*/\
Восстановление всех регистров,*/\
чтобы этот вызов был
*/\
прозрачным для перехваченной */\
функции.
*/\
\
\
\
\
Восстановление ESI в
*/\
отладочных компоновках
*/\
Восстановление ESP.
*/\
Восстановление EBP.
*/\
Восстановление стека для
*/\
функций с соглашением stdcall.*/\

}
/*//////////////////////////////////////////////////////////////////////
// Макрос REAL_FUNC_PRE_CALL нужно размещать НЕПОСРЕДСТВЕННО
// *ПЕРЕД* ЛЮБЫМ вызовом действительной функции, обрабатываемым
// ловушкой. Этот макрос гарантирует, что регистры EDI и ESI будут
// иметь те же значения, что и при вызове функцииловушки.
//////////////////////////////////////////////////////////////////////*/
#define REAL_FUNC_PRE_CALL()
\
{
\
__asm MOV EDI , [stRegState.dwEDI]
/* Восстановление EDI.
*/\
__asm MOV ESI , [stRegState.dwESI]
/* Восстановление ESI.
*/\
}
/*//////////////////////////////////////////////////////////////////////
// Макрос REAL_FUNC_POST_CALL нужно размещать СРАЗУ ЖЕ *ПОСЛЕ*
// ЛЮБОГО вызова действительной функции, обрабатываемого ловушкой.
// Он сохраняет значения всех регистров после вызова действительной
// функции, чтобы эпилог функцииловушки мог вернуть те же значения,
// что и действительная функция.
//////////////////////////////////////////////////////////////////////*/
#define REAL_FUNC_POST_CALL()
\
{
\
__asm MOV [stRegState.dwEAX] , EAX /* Сохранение значения EAX.
*/\
__asm MOV [stRegState.dwEBX] , EBX /* Сохранение значения EBX.
*/\
__asm MOV [stRegState.dwECX] , ECX /* Сохранение значения ECX.
*/\
__asm MOV [stRegState.dwEDX] , EDX /* Сохранение значения EDX.
*/\
__asm MOV [stRegState.dwEDI] , EDI /* Сохранение значения EDI.
*/\
__asm MOV [stRegState.dwESI] , ESI /* Сохранение значения ESI.
*/\
__asm XOR EAX , EAX
/* Обнуление EAX.
*/\
__asm LAHF
/* Загрузка флагов в AH.
*/\
__asm MOV [stRegState.dwEFL] , EAX /* Сохранение флагов.
*/\
см. след. стр.

562

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

}
\
dwLastError = GetLastError ( ) ;
/* Сохр. кода последней ошибки.*/\
{
\
__asm MOV EAX , [stRegState.dwEAX] /* Восстановление EAX
*/\
/* Установка возвращаемого
*/\
/* значения.
*/\
__asm MOV [stEvtInfo.dwRetValue] , EAX
\
}
/*//////////////////////////////////////////////////////////////////////
// Удобный макрос для заполнения структуры информации о событии
//////////////////////////////////////////////////////////////////////*/
#define FILL_EVENTINFO(eFn)
\
stEvtInfo.eFunc
= eFn
;
\
stEvtInfo.ePrePost = ePostCall ;
\
stEvtInfo.dwThreadId = GetCurrentThreadId ( )
/*//////////////////////////////////////////////////////////////////////
// Макросы для второй версии программы, ЗНАЧИТЕЛЬНО
// облегчающие определение функцийловушек
//////////////////////////////////////////////////////////////////////*/
// Объявляйте его в начале каждой функцииловушки.
// eFunc
 значение перечисления, соответствующее функции.
// SynchClassType – значение флага DDOPT_*, указывающее на класс
//
обрабатываемой вами функции.
// bRecordPreCall – выполняет запись информации об этой функции.
#define HOOKFN_STARTUP(eFunc,SynchClassType,bRecordPreCall)
\
HOOKFN_PROLOG ( ) ;
\
if ( TRUE == DoLogging ( SynchClassType ) )
\
{
\
FILL_EVENTINFO ( eFunc ) ;
\
if ( TRUE == (int)bRecordPreCall )
\
{
\
stEvtInfo.ePrePost = ePreCall ;
\
ProcessEvent ( &stEvtInfo ) ;
\
}
\
}
\
REAL_FUNC_PRE_CALL ( ) ;
/*//////////////////////////////////////////////////////////////////////
// Макрос завершения функцииловушки.
// iNuMParams  число параметров, переданных функции.
// SynchClassType – класс функции синхронизации.
//////////////////////////////////////////////////////////////////////*/
#define HOOKFN_SHUTDOWN(iNumParams,SynchClass)
\
REAL_FUNC_POST_CALL ( ) ;
\
if ( TRUE == DoLogging ( SynchClass ) )
\
{
\
stEvtInfo.ePrePost = ePostCall ;
\
ProcessEvent ( &stEvtInfo ) ;
\

ГЛАВА 15

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

}
HOOKFN_EPILOG ( iNumParams ) ;

563

\

/*//////////////////////////////////////////////////////////////////////
// Объявление соглашения вызова для всех функций DD_*.
//////////////////////////////////////////////////////////////////////*/
#define NAKEDDEF __declspec(naked)
/*//////////////////////////////////////////////////////////////////////
// ВАЖНОЕ ПРИМЕЧАНИЕ! ВАЖНОЕ ПРИМЕЧАНИЕ!
// Следующие прототипы выглядят, как функции с соглашением __cdecl, но на
// самом деле это не так: они все __stdcall! Использование правильного
// соглашения вызова гарантируется моими прологом и эпилогом!
//////////////////////////////////////////////////////////////////////*/
////////////////////////////////////////////////////////////////////////
// Функции, перехват которых обязателен, иначе система не будет
// работать.
BOOL DD_FreeLibrary ( HMODULE hModule ) ;
VOID DD_FreeLibraryAndExitThread ( HMODULE hModule
,
DWORD dwExitCode ) ;
HMODULE DD_LoadLibraryA ( LPCSTR lpLibFileName ) ;
HMODULE DD_LoadLibraryW ( LPCWSTR lpLibFileName ) ;
HMODULE DD_LoadLibraryExA ( LPCSTR lpLibFileName ,
HANDLE hFile
,
DWORD dwFlags
) ;
HMODULE DD_LoadLibraryExW ( LPCWSTR lpLibFileName ,
HANDLE hFile
,
DWORD dwFlags
) ;
VOID DD_ExitProcess ( UINT uExitCode ) ;
FARPROC DD_GetProcAddress ( HMODULE hModule , LPCSTR lpProcName ) ;
////////////////////////////////////////////////////////////////////////
// Функции работы с потоками
HANDLE DD_CreateThread (LPSECURITY_ATTRIBUTES lpThreadAttributes ,
DWORD
dwStackSize
,
LPTHREAD_START_ROUTINE lpStartAddress
,
LPVOID
lpParameter
,
DWORD
dwCreationFlags
,
LPDWORD
lpThreadId
) ;
VOID DD_ExitThread ( DWORD dwExitCode ) ;
DWORD DD_SuspendThread ( HANDLE hThread ) ;
DWORD DD_ResumeThread ( HANDLE hThread ) ;
BOOL DD_TerminateThread ( HANDLE hThread , DWORD dwExitCode ) ;
// Ниже приведены функции работы с потоками стандартной библиотеки C.
// Они обрабатываются правильно, так как используют соглашение __cdecl.
uintptr_t
см. след. стр.

564

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

DD_beginthreadex ( void *
security
,
unsigned
stack_size
,
unsigned ( __stdcall *start_address )(
void *
arglist
,
unsigned
initflag
,
unsigned *
thrdaddr
) ;
uintptr_t
DD_beginthread ( void( __cdecl *start_address )( void * )
unsigned stack_size
void *
arglist
VOID DD_endthreadex ( unsigned retval ) ;
VOID DD_endthread ( void ) ;

void * ) ,

,
,
) ;

////////////////////////////////////////////////////////////////////////
// Функции ожидания и специальные функции
DWORD DD_WaitForSingleObject ( HANDLE hHandle
,
DWORD dwMilliseconds ) ;
DWORD DD_WaitForSingleObjectEx ( HANDLE hHandle
,
DWORD dwMilliseconds ,
BOOL bAlertable
) ;
DWORD DD_WaitForMultipleObjects( DWORD
nCount
,
CONST HANDLE * lpHandles
,
BOOL
bWaitAll
,
DWORD
dwMilliseconds ) ;
DWORD DD_WaitForMultipleObjectsEx( DWORD
nCount
,
CONST HANDLE * lpHandles
,
BOOL
bWaitAll
,
DWORD
dwMilliseconds ,
BOOL
bAlertable
) ;
DWORD DD_MsgWaitForMultipleObjects ( DWORD
nCount
,
LPHANDLE pHandles
,
BOOL
fWaitAll
,
DWORD
dwMilliseconds,
DWORD
dwWakeMask
) ;
DWORD DD_MsgWaitForMultipleObjectsEx ( DWORD
nCount
,
LPHANDLE pHandles
,
DWORD
dwMilliseconds ,
DWORD
dwWakeMask
,
DWORD
dwFlags
) ;
DWORD DD_SignalObjectAndWait ( HANDLE hObjectToSignal ,
HANDLE hObjectToWaitOn ,
DWORD dwMilliseconds ,
BOOL bAlertable
) ;
BOOL DD_CloseHandle ( HANDLE hObject ) ;
////////////////////////////////////////////////////////////////////////
// Функции работы с критическими секциями
VOID DD_InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
BOOL DD_InitializeCriticalSectionAndSpinCount (
LPCRITICAL_SECTION lpCriticalSection,

ГЛАВА 15

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

565

DWORD
dwSpinCount
);
VOID DD_DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection ) ;
VOID DD_EnterCriticalSection ( LPCRITICAL_SECTION lpCriticalSection ) ;
VOID DD_LeaveCriticalSection ( LPCRITICAL_SECTION lpCriticalSection ) ;
DWORD DD_SetCriticalSectionSpinCount (
LPCRITICAL_SECTION lpCriticalSection,
DWORD
dwSpinCount
);
BOOL DD_TryEnterCriticalSection ( LPCRITICAL_SECTION lpCriticalSection);
////////////////////////////////////////////////////////////////////////
// Функции работы с мьютексами
HANDLE DD_CreateMutexA ( LPSECURITY_ATTRIBUTES lpMutexAttributes ,
BOOL
bInitialOwner
,
LPCSTR
lpName
) ;
HANDLE DD_CreateMutexW ( LPSECURITY_ATTRIBUTES lpMutexAttributes ,
BOOL
bInitialOwner
,
LPCWSTR
lpName
) ;
HANDLE DD_OpenMutexA ( DWORD dwDesiredAccess ,
BOOL bInheritHandle ,
LPCSTR lpName
) ;
HANDLE DD_OpenMutexW ( DWORD dwDesiredAccess ,
BOOL
bInheritHandle ,
LPCWSTR lpName
) ;
BOOL DD_ReleaseMutex ( HANDLE hMutex ) ;
////////////////////////////////////////////////////////////////////////
// Функции работы с семафорами
HANDLE
DD_CreateSemaphoreA ( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes ,
LONG
lInitialCount
,
LONG
lMaximumCount
,
LPCSTR
lpName
);
HANDLE
DD_CreateSemaphoreW ( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes ,
LONG
lInitialCount
,
LONG
lMaximumCount
,
LPCWSTR
lpName
);
HANDLE DD_OpenSemaphoreA ( DWORD dwDesiredAccess ,
BOOL bInheritHandle ,
LPCSTR lpName
) ;
HANDLE DD_OpenSemaphoreW ( DWORD dwDesiredAccess ,
BOOL
bInheritHandle ,
LPCWSTR lpName
) ;
BOOL DD_ReleaseSemaphore ( HANDLE hSemaphore
,
LONG lReleaseCount ,
LPLONG lpPreviousCount ) ;
////////////////////////////////////////////////////////////////////////
// Функции работы с событиями
HANDLE DD_CreateEventA ( LPSECURITY_ATTRIBUTES lpEventAttributes ,
см. след. стр.

566

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

BOOL
bManualReset
BOOL
bInitialState
LPCSTR
lpName
HANDLE DD_CreateEventW ( LPSECURITY_ATTRIBUTES lpEventAttributes
BOOL
bManualReset
BOOL
bInitialState
LPCWSTR
lpName
HANDLE DD_OpenEventA ( DWORD dwDesiredAccess ,
BOOL bInheritHandle ,
LPCSTR lpName
) ;
HANDLE DD_OpenEventW ( DWORDdwDesiredAccess ,
BOOL
bInheritHandle ,
LPCWSTR lpName
) ;
BOOL DD_PulseEvent ( HANDLE hEvent ) ;
BOOL DD_ResetEvent ( HANDLE hEvent ) ;
BOOL DD_SetEvent ( HANDLE hEvent ) ;

,
,
) ;
,
,
,
) ;

#endif // _DD_FUNCS_H
В первой версии DeadlockDetection я допустил глупейшую ошибку «наследо!
вания при редактировании». Я создал функции!ловушки для LoadLibraryA и LoadLib
raryW и понял, что мне также нужны ловушки для LoadLibraryExA и LoadLibraryExW.
Как типичный программист, я вырезал функции LoadLibraryA/W, вставил их в нуж!
ное место кода и отредактировал с учетом особенностей LoadLibraryExA/W. Если вы
посмотрите на макрос HOOKFN_EPILOG, то увидите, что он принимает некоторое
значение, а именно число параметров функциии. Наверное, вы уже догадались,
что случилось: я забыл изменить это значение с 1 на 3, поэтому вызовы LoadLibrary
ExA/W удаляли из стека два лишних элемента и приводили к краху программ!
Изучив код всех функций!ловушек, я понял, что он по сути везде одинаков. Для
инкапсуляции общих действий я создал макросы HOOKFN_STARTUP и HOOKFN_SHUTDOWN.
Как видно по имени, макрос HOOKFN_STARTUP размещается в начале функции!ловушки
и заботится о прологе, а также выполняет необходимое протоколирование до
вызова действительной функции. Он принимает следующие параметры: перечис!
ление функции, флаг DDOPT_*, показывающий, к какой группе относится данная
функция, и булев флаг, выполняющий предварительное протоколирование, если
имеет значение TRUE. Предварительное протоколирование предназначено для
функций, которые потенциально могут вызывать блокировку, таких как WaitFor
SingleObject. Макрос HOOKFN_SHUTDOWN принимает число параметров функции и тот
же флаг DDOPT_*, что передается в HOOKFN_STARTUP. Конечно, чтобы не допустить ту
же ошибку, которую я сделал в случае ловушек LoadLibraryExA/W, я проверил число
параметров HOOKFN_SHUTDOWN.
Я хочу упомянуть еще несколько деталей. Во!первых, DeadlockDetection все!
гда активна в вашем приложении, даже если вы приостанавливаете протоколиро!
вание. Вместо того чтобы устанавливать и удалять ловушки, я оставляю функции
перехваченными и изучаю некоторые внутренние флаги, чтобы определить, как
ловушка должна себя вести. Поддержание всех функций в перехваченном состо!
янии упрощает переключение между разными протоколируемыми функциями в

ГЛАВА 15

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

567

период выполнения, но несколько снижает эффективность вашей программы.
Я чувствовал, что реализация перехвата и его отмены «на лету» привела бы к по!
явлению дополнительных ошибок.
Во!вторых, DeadlockDetection перехватывает функции при загрузке DLL в вашу
программу с помощью LoadLibrary. Однако DeadlockDetection может получить
контроль только после выполнения в DLL функции DllMain, поэтому, если какие!
то объекты синхронизации создаются или используются во время DllMain, Deadlock!
Detection может их упустить.
В!третьих, DeadlockDetection также перехватывает функции GetProcAddress и
ExitProcess. Перехват GetProcAddress полезен, если ваша программа или элемент
управления сторонней фирмы, который может привести к блокировке, вызывает
GetProcAddress для нахождения метода синхронизации в период выполнения.
Я перехватываю ExitProcess потому, что при завершении приложения мне нужно
отменить перехват функций и завершить DeadlockDetection, чтобы она не вызва!
ла крах или зависание вашей программы. Так как при завершении программы
контролировать порядок выгрузки DLL невозможно, вы с легкостью можете по!
пасть в ситуацию, когда DLL утилиты DeadlockDetection, такая как DeadDetExt,
выгружается до самой DeadlockDetection. К счастью, очень немногие программи!
сты обрабатывают несколько потоков после вызова ExitProcess.
Перехват ExitProcess реализован в файле DEADLOCKDETECTION.CPP. Из!за ог!
ромной важности правильного завершения DeadlockDetection я принудительно
перехватываю все вызовы ExitProcess даже в игнорируемых модулях. Это позво!
ляет исключить неожиданные ошибки, при которых функции синхронизации
остаются перехваченными после завершения DeadlockDetection.
Наконец, вместе с DeadlockDetection вы можете найти на CD несколько тесто!
вых программ. Все они включены в главное решение DeadlockDetectionTests и
компонуются вместе с DEADLOCKDETECTION.DLL. Они помогут вам понять работу
DeadlockDetection.

Что после DeadlockDetection?
DeadlockDetection — достаточно полная утилита, и я успешно применял ее для
отслеживания многих многопоточных блокировок. Однако, как всегда, я предла!
гаю вам обдумать возможности расширения DeadlockDetection, чтобы сделать ее
еще полезнее. Вот некоторые мои идеи по этому поводу.
쐽 Создайте отдельное приложение для работы с файлом DEADLOCKDETECTI!
ON.INI. Будет еще лучше, если оно позволит указывать DLL DeadDetExt и будет
проверять, что эта DLL экспортирует корректные функции.
쐽 Вы можете оптимизировать функции!ловушки, если они не выполняют про!
токолирования. В этом случае нужно копировать не все значения регистров.
쐽 На данный момент DeadlockDetection просто пропускает перехват некоторых
DLL, которые ей указаны. Было бы великолепно, если б вы разработали меха!
низм пропуска DLL с учетом выполняемой программы.

568

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Отладка: фронтовые очерки
Проблема фиксации транзакций при использовании пула объектов COM
Боевые действия
Мой хороший друг Питер Иерарди (Peter Ierardi) рассказал об одной инте!
ресной многопоточной ошибке. Он работал над крупным проектом DCOM,
включавшим многопоточную службу DCOM для координации транзакций
базы данных. Служба DCOM управлял транзакциями, создавая пул ориен!
тированных на базу данных внутрипроцессных COM!объектов, применяв!
шихся для записи и чтения данных из реляционной СУБД. Взаимодействие
компонентов осуществлялось при помощи Microsoft Message Queue Server
(MSMQ). Несмотря на явную фиксацию транзакций, данные, похоже, не за!
писывались в базу. Служба DCOM повторяла попытку от трех до пяти раз, и
только после этого данные появлялись, как по щучьему велению. Очевид!
но, лишние попытки снижали быстродействие приложения, а то, что дан!
ные не хотели записываться в базу данных, вызывал тревогу.

Исход
После нескольких тяжелых сеансов отладки Питер обнаружил, что служба
DCOM выполнял чтение/запись при помощи отдельных несинхронизиро!
ванных потоков. Чтение происходило до того, как отдельный экземпляр
COM!объекта базы записывал данные. Во время отладки это поведение было
далеко не очевидным, потому что отладчик навязывал правильный отсчет
времени и синхронизацию. В конце концов Питер обнаружил проблему,
правильно отметив объекты в журнале событий.

Полученный опыт
Как сказал Питер, эта ошибка помогла ему извлечь один очень важный урок:
работая над крупномасштабным распределенным приложением, нельзя
предполагать, что среда отладки правильно воспроизведет среду выполне!
ния заключительной версии программы. Он решил проблему, добавив нуж!
ный код синхронизации и включив взаимодействие компонентов (которые
первоначально взаимодействовали по отдельности при помощи MSMQ), в
транзакции записи в базу данных, чтобы сообщения отправлялись только
после фиксации транзакций.
Ошибка Питера состояла в том, что MSMQ выполнял операции чтения/
записи, как легко догадаться, гораздо быстрее, чем база данных. Хотя Пи!
тер и другие члены его группы тщательно проработали и спланировали все
многопоточные фрагменты, они все же не до конца осознавали, насколько
быстрее в реальном мире будут выполняться определенные операции, не
подвластные их приложению.

ГЛАВА 15

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

569

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

Г Л А В А

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

В главе 3 я рассказал про блочные тесты и объяснил, почему они так важны для
разработки высококачественного кода. Если вы работаете преимущественно над
внутренней логикой приложения, блочные тесты могут быть довольно просты. На
CD в каталоге BugslayerUtil\Tests вы можете найти все блочные тесты, использо!
ванные мной при разработке BUGSLAYERUTIL.DLL. Почти все они — консольные
приложения, прекрасно справляющиеся со своими обязанностями.
К сожалению, тестирование пользовательского интерфейса (UI) гораздо сложнее
независимо от того, является ли приложение «толстым клиентом» Microsoft .NET
или работает на базе браузера. В этой главе я расскажу про мою утилиту Tester,
которая поможет вам автоматизировать тестирование кода UI. По сравнению с
версией Tester, включенной в первое издание книги, новая утилита Tester уже при!
ближается к возможностям полного коммерческого средства регрессивного тес!
тирования. Tester на самом деле применяют очень многие группы разработчиков,
чем я весьма польщен. Она не только проще в работе многих коммерческих сис!
тем, но и гораздо дешевле.

Проклятие блочного тестирования: UI
Абсолютно уверен, что если разработчики программ для Microsoft Windows и
получают туннельный синдром, то это вызвано не написанием исходного кода, а
многократными нажатиями одних и тех же комбинаций клавиш при тестирова!
нии приложений. После пятитысячного нажатия Alt+F, O запястья сковываются
сильнее, чем арматура, залитая бетоном. При отсутствии средства, автоматизиру!
ющего доступ к различным функциям ваших приложений, обычно необходимо
следовать некоторому сценарию, чтобы гарантировать, что блочное тестирова!

ГЛАВА 16

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

571

ние проведено в достаточном объеме. Тестирование программ вручную при по!
мощи сценариев мгновенно надоедает, значительно повышая вероятность чело!
веческих ошибок.
Автоматизация блочного тестирования позволяет уменьшить объем работы с
клавиатурой и дает возможность быстрой проверки состояния кода. Очень жаль,
но аналога программы Recorder из состава Microsoft Windows 3.0 и 3.1 в 32!раз!
рядных ОС нет. Если вы не работали со старыми версиями Windows, я поясню
сказанное: Recorder записывала ваши манипуляции с мышью и клавиатурой в файл,
который позднее можно было воспроизвести, сымитировав физические события
мыши и клавиатуры. В настоящее время доступны некоторые программы сторонних
фирм, обеспечивающие автоматизацию работы с приложением и другие возмож!
ности (например, сравнение экранов с проверкой каждого пиксела и поддержку
базы данных о времени проведения тех или иных тестов), но я все равно хотел
разработать что!то более простое и дружественное к программистам. Так роди!
лась идея Tester.
Задумав утилиту автоматизации тестирования, я некоторое время размышлял
о том, какие именно возможности мне хотелось бы получить. Сначала я решил
разработать утилиту вроде Recorder. Во времена Windows 3.0 у меня был целый
набор REC!файлов для выполнения моих тестов. Однако Recorder имел большой
недостаток: он не поддерживал условные тесты. Если во время тестирования мое
приложение сообщало об ошибке, Recorder просто продолжал работу, проигры!
вая записанные нажатия клавиш клавиатуры и мыши и полностью игнорируя стра!
дания моей программы. Однажды благодаря Recorder я умудрился уничтожить
половину ОС: я тестировал собственное расширение WINFILE.EXE, и, когда в нем
возникла ошибка, Recorder проиграл команду удаления файлов для всего катало!
га System. Мое новое средство автоматизации тестирования непременно должно
было поддерживать конструкцию if...then...else.
Очевидно, что для этого мне нужен был некоторый вид языка. Разработка соб!
ственного языка тестирования казалась заманчивым интеллектуальным упражне!
нием, но вскоре я пришел к выводу, что я больше заинтересован в полезном от!
ладочном средстве, а не в проектировании языка и работе с YACC и FLEX. Я почти
сразу понял, что Tester нужно реализовать как объект COM: благодаря этому про!
граммисты могли бы создавать тесты на предпочтительном для них языке, а я мог
бы сосредоточиться на программировании функций регрессивного тестирования,
а не на разработке нового языка. Лично я предпочитаю создавать тесты на язы!
ках сценариев, таких как Microsoft Visual Basic Scripting Edition (VBScript) и Microsoft
JScript, потому что они не требуют компиляции. Однако различные реализации
механизма сценариев Microsoft Windows Scripting Host (WSH) имеют некоторые
ограничения, на которые я укажу ниже. Сейчас я хотел бы обсудить требования,
которыми я руководствовался при создании Tester.

Требования к Tester
Я хотел, чтобы Tester очень хорошо делал две вещи: записывал нажатые вами ком!
бинации клавиш и проигрывал их обратно вашему приложению, ускоряя прове!
дение блочного тестирования. Если вы когда!либо изучали коммерческие сред!

572

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

ства регрессивного тестирования, то знаете, насколько различные функции они
могут поддерживать: от простого управления окном до проверки самых сложных
и причудливых свойств окна. Я хотел сосредоточиться на потребностях разработ!
чиков во время блочного тестирования и сделать Tester простым в использова!
нии. Вот какими были основные требования к Tester.
1. Возможность управления им при помощи любого языка, поддерживающего
COM.
2. При получении строки нажатых клавиш в формате, используемом классом
System.Windows.Forms.SendKeys, Tester должен уметь проигрывать ее активному окну.
3. Tester должен поддерживать возможность нахождения любых окон верхнего
уровня или дочерних окон по их заголовку или классу.
4. При получении любого HWND Tester должен быть способен получить все свой!
ства окна.
5. Tester должен уведомлять пользовательский сценарий о создании/уничтоже!
нии конкретного окна, чтобы сценарий мог обработать потенциальные усло!
вия ошибки или выполнить дополнительную обработку окна. Tester не должен
ограничивать возможность расширения кода, позволяя разработчикам удов!
летворить любые свои потребности.
6. Tester должен уметь записывать нажатия клавиш в строки, совместимые с его
модулем воспроизведения.
7. Сохраняемые сценарии Tester должны быть полными, т. е. готовыми к запуску.
8. Пользователь должен иметь возможность редактирования автоматически сге!
нерированного сценария перед его сохранением.
9. Tester должен гарантировать правильность потока воспроизведения информа!
ции, присваивая фокус конкретным окнам, в том числе любым дочерним эле!
ментам управления.
Tester поддерживает практически полный набор функций, однако он, навер!
ное, не решит всех задач, поставленных перед вашим отделом контроля качества,
состоящим из 20 человек. Я просто хотел создать средство, которое позволило бы
нам, программистам, автоматизировать блочное тестирование. Думаю, Tester от!
вечает этим требованиям. Он очень помог мне при разработке WDBG, отладчика
с графическим пользовательским интерфейсом (GUI), описанного в главе 4. Са!
мый приятный аспект работы с Tester при создании WDBG заключался в том, что
он избавил меня от многих тысяч нажатий клавиш. Как видите, я уже добрался до
16 главы и все еще могу двигать пальцами!

Использование Tester
Работать с Tester относительно просто. Сначала я расскажу про объект Tester и его
использование в сценариях, а затем перейду к обсуждению записи сценариев при
помощи программы TESTREC.EXE. Разобравшись с объектом, который Tester пре!
доставляет вашим сценариям, вы сможете создавать их более эффективно.

ГЛАВА 16

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

573

Сценарии Tester
Принцип работы сценариев прост: вы создаете несколько объектов Tester, запус!
каете приложение или находите его основное окно, воспроизводите ему несколько
нажатий клавиш, проверяете результаты и завершаете сценарий. В листинге 16!1
представлен пример сценария VBScript, который запускает NOTEPAD.EXE, вводит
несколько строк текста и закрывает Блокнот (все примеры сценариев, приведен!
ные в этой главе, вы можете найти на CD).

Листинг 16-1. Сценарий MINIMAL.VBS поясняет работу с часто используемыми
объектами Tester
' Минимальный пример сценария Tester на VBScript. Он просто запускает
' Notepad, вводит несколько строк текста и закрывает Notepad.
' Получение системного объекта и объекта ввода.
Dim tSystem
Dim tInput
Dim tWin
Set tSystem = WScript.CreateObject ( "Tester.TSystem" )
Set tInput = WScript.CreateObject ( "Tester.TInput" )
' Запуск Notepad.
tSystem.Execute "NOTEPAD.EXE"
' Подождать 3 секунды.
tSystem.Sleep 3.0
' Попытка найти основное окно Notepad.
Set tWin = tSystem.FindTopTWindowByTitle ( "Untitled  Notepad" )
If ( tWin Is Nothing ) Then
MsgBox "Unable to find Notepad!"
WScript.Quit
End If
' Гарантия того, что Notepad работает в активном режиме.
tWin.SetForegroundTWindow
' Ввод чегонибудь.
tInput.PlayInput "Be all you can be!~~~"
' Еще раз.
tInput.PlayInput "Put on your boots and parachutes....~~~"
' И еще.
tInput.PlayInput "Silver wings upon their chests.....~~~"
' Подождать 3 секунды.
tSystem.Sleep 3.0
' Закрыть Notepad.
tInput.PlayInput "%FX"
tSystem.Sleep 2.0
tInput.PlayInput "{TAB}~"
' Сценарий выполнен!

574

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

В листинге 16!1 вы можете увидеть три объекта, чаще всего используемых Tester.
Объект TSystem позволяет находить окна верхнего уровня, запускать приложения
и приостанавливать тестирование. Объект TWindow, возвращаемый в листинге 16!1
методом FindTopTWindowByTitle, — главная рабочая лошадка и является оболочкой
для HWND, включающей все виды свойств окна, которые вам могут понадобиться.
Кроме того, TWindow умеет находить все дочерние окна, относящиеся к конкрет!
ному родительскому окну. Последний объект в листинге 16!1 — объект TInput,
поддерживающий единственный метод PlayInput для воспроизведения нажатий
клавиш окну с фокусом.
В листинге 16!2 представлен тест на языке VBScript, поясняющий работу с
объектом TNotify. Одна из самых сложных проблем при разработке сценариев
автоматизации тестирования связана с появлением неожиданного окна, такого как
информационное окно ASSERT. Объект TNotify позволяет предоставить обработчик
таких непредвиденных событий. Несложный сценарий, показанный в листинге
16!2, просто следит за любыми окнами, содержащими в заголовке слово «Notepad».
Скорее всего класс TNotify вам понадобится нечасто, но порой он по!настоящему
нужен.

Листинг 16-2. Сценарий HANDLERS.VBS демонстрирует
использование объекта TNotify
' Тест VBScript, иллюстрирующий обработку оконных уведомлений
' Константы, передаваемые
Const antDestroyWindow
Const antCreateWindow
Const antCreateAndDestroy

в
=
=
=

метод TNotify.AddNotification.
1
2
3

Const ansExactMatch
Const ansBeginMatch
Const ansAnyLocMatch

= 0
= 1
= 2

' Получение системного объекта и объекта ввода.
Dim tSystem
Dim tInput
Set tSystem = WScript.CreateObject ( "Tester.TSystem" )
Set tInput = WScript.CreateObject ( "Tester.TInput" )
' Переменная для объекта TNotify.
Dim Notifier
' Создание объекта TNotify.
Set Notifier = _
WScript.CreateObject ( "Tester.TNotify"
, _
"NotepadNotification"
)
' Добавление интересующих меня уведомлений. В данном случае мне
' нужны уведомления об уничтожении и создании окна. Все возможные
' комбинации уведомлений вы найдете в исходном коде TNotify.

ГЛАВА 16

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

575

Notifier.AddNotification antCreateAndDestroy , _
ansAnyLocMatch
, _
"Notepad"
' Запуск Notepad.
tSystem.Execute "NOTEPAD.EXE"
' Пауза на 1 секунду.
tSystem.Sleep 1.0
' Поскольку модель разделенных потоков небезопасна с точки зрения
' потоков, я использую в схеме уведомлений таймер. Однако сообщение
' может быть заблокировано, поскольку вся обработка ограничивается
' одним потоком. Эта функция позволяет вручную проверить уведомления
' о создании и уничтожении окна.
Notifier.CheckNotification
' Информационное окно процедуры NotepadNotification_CreateWindow
' вызывает блокировку, поэтому код завершения Notepad не будет
' выполнен, пока оно не будет закрыто.
tInput.PlayInput "%FX"
tSystem.Sleep 1.0
' Еще одна проверка уведомлений.
Notifier.CheckNotification
' Я даю TNotify шанс на перехват сообщения об уничтожении окна.
tSystem.Sleep 1.0
' Отключение уведомлений. Если при работе с WSH этого
' не сделать, объект не будет уничтожен, и уведомления
' в таблице уведомлений останутся в активном состоянии.
WScript.DisconnectObject Notifier
Set Notifier = Nothing
WScript.Quit

Sub NotepadNotificationCreateWindow ( tWin )
MsgBox ( "Notepad was created!!" )
End Sub
Sub NotepadNotificationDestroyWindow ( )
MsgBox ( "Notepad has gone away...." )
End Sub
Время от времени нужно вызывать метод CheckNotification объекта TNotify (при!
чины этого я объясню в разделе «Реализация Tester»). Периодический вызов ме!
тода CheckNotification гарантирует поступление уведомлений, даже если в выбранном

576

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

вами языке нет цикла сообщений. Листинг 16!2 иллюстрирует использование
информационных окон (message box) в процедурах уведомлений, однако вам,
вероятно, не захочется включать в реальные сценарии вызовы информационных
окон, потому что они могут вызвать проблемы, неожиданно изменяя окно с фо!
кусом.
Помните также, что я позволяю задать только ограниченное число уведомле!
ний — пять, поэтому вам не следует использовать TNotify для общих задач сцена!
риев, таких как ожидание появления окна сохранения файла. TNotify следует при!
менять только для обработки неожиданных окон. Вы можете определить свои
обработчики уведомлений и параметры поиска текста в заголовке окна так, что
будете получать уведомления для окон, в которых вы не заинтересованы. Скорее
всего вы будете получать нежелательные уведомления при использовании общих
строк, таких как «Notepad», и указании, что строка может находиться в любом месте
заголовка окна. Для избежания нежелательных уведомлений методу AddNotification
объекта TNotify надо передавать как можно более специфичную информацию.
Кроме того, в процедурах обработки события CreateWindow следует изучать полу!
чаемый объект TWindow, чтобы можно было проверить, то ли это окно, которое вам
нужно. В процедурах события DestroyWindow, обрабатывающих общие уведомления,
следует выполнять поиск открытых окон, чтобы гарантировать, что интересую!
щее вас окно больше не существует.
На CD есть и другие примеры, которые помогут вам лучше разобраться в ра!
боте с Tester. NPAD_TEST.VBS — это более полный тест VBScript, включающий не!
которые повторно используемые блоки. PAINTBRUSH.JS иллюстрирует воспроиз!
ведение манипуляций с мышью, не зависящее от разрешения экрана. Его выпол!
нение требует некоторого времени, однако результат того стоит. TesterTester — это
основной блочный тест для COM!объекта Tester. Эта программа написана на C# и
расположена в каталоге Tester\Tester\Tests\TesterTester. Изучив ее, вы получите
представление о том, как использовать Tester вместе с .NET. Кроме того, пример
TesterTester демонстрирует работу с объектом TWindows — коллекцией объектов
TWindow.
Хотя я предпочитаю писать свои блочные тесты на языках JScript и VBScript, я
понимаю, что иногда это довольно трудно. Языки сценариев не позволяют конт!
ролировать тип переменных и не включают редактор IntelliSense, такой как ре!
дактор C# в Visual Studio .NET, поэтому вам придется вернуться к старому стилю
отладки — «запустить и ошибиться». Языки сценариев нравятся мне в первую
очередь тем, что они не требуют компиляции тестов. Если вы работаете с гибкой
средой сборки программы, в которой легко создавать другие двоичные файлы в
дополнение к главному приложению, вы можете применять .NET, создавая тесты
вместе со сборкой своего приложения. Конечно, Tester не ограничивает вас про!
стейшими языками тестирования. Если вам удобней писать тесты на C или Microsoft
Macro Assembler (MASM) — пожалуйста.
Использовать объекты Tester довольно просто, однако реальная работа состо!
ит в планировании тестов. Ваши тесты должны быть максимально конкретными
и простыми. Когда я только приступал к автоматизации своих блочных тестов в
начале карьеры, я пытался включить в них побольше функций. Теперь каждый мой
сценарий тестирует только одну операцию. В качестве хорошего примера такого

ГЛАВА 16

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

577

сценария можно привести тест открытия файла. Для повторного использования
сценариев вы можете объединить их самыми разными способами. Например, как
только вы напишете сценарий открытия файла, вы сможете включить его в три
различных теста: тест открытия существующего файла, тест открытия несуществу!
ющего файла и тест открытия поврежденного файла. Как и при разработке обычных
программ, следует избегать включения в тесты жестко закодированных строк. Это
не только облегчит интернационализацию сценария, но и упростит его адапта!
цию к очередной версии системы меню и комбинаций управляющих клавиш
(accelerator).
При разработке сценариев Tester нужно предусмотреть и способы проверки
того, что сценарии на самом деле работают. Если вам нечем заняться, можете просто
сидеть и следить за их выполнением, сравнивая результаты каждого запуска. Од!
нако лучше было бы записывать основные состояния и точки сценария, чтобы
сравнение результатов можно было проводить автоматически. Если для выполне!
ния сценариев вы используете CSCRIPT.EXE, можете перенаправить вывод в файл
методом WScript.Echo. По завершении сценария вы можете сравнить разные вер!
сии полученных файлов утилитой нахождения различий версий (такой как WinDiff)
и узнать, корректно ли выполнился сценарий. Помните: записываемая информа!
ция должна быть нормализованной и не зависящей от деталей конкретного за!
пуска сценария. Так, если вы работаете над приложением, загружающим из сети
курсы акций, не следует записывать в файл время последнего обновления курса.
Что сказать об отладке сценариев Tester? Tester не включает собственного ин!
тегрированного отладчика, поэтому вам понадобятся другие средства отладки, до!
ступные для языка, на котором написан сценарий. Отлаживая сценарий, старай!
тесь не прерываться на вызове метода PlayInput объекта Tinput, потому что при
остановке на этом методе нажатия клавиш будут воспроизведены неправильному
окну. Для решения этой потенциальной проблемы я обычно перед каждым вызо!
вом PlayInput перемещаю окно, которому посылаю нажатия клавиш, на вершину
z!порядка, вызывая метод SetForegroundTWindow объекта TWindow. Это позволяет мне
прерваться на вызове SetForegroundTWindow и проверить состояние приложения, не
вызывая ошибок воспроизведения нажатий клавиш.

Запись сценариев
Теперь вы понимаете работу объектов Tester и знаете, как их вызывать из собствен!
ных сценариев, поэтому я хочу перейти к рассмотрению программы TESTREC.EXE,
которую вы будете применять для записи взаимодействия со своими приложени!
ями. Запустив TESTREC.EXE в первый раз, вы заметите, что это текстовый редак!
тор, который уже при запуске генерирует некоторый объем кода. По умолчанию
сценарии создаются на языке JScript, но чуть ниже я покажу, как изменить его на
VBScript. Для начала записи нужно нажать на панели инструментов кнопку Record
(запись) или клавиши Ctrl+R.
При записи сценария TESTREC.EXE минимизируется и изменяет заголовок на
«RECORDING!», давая вам знать о происходящем. Остановить запись можно несколь!
кими способами. Самый простой — сделать TESTREC.EXE активной программой,
нажав Alt+Tab или выбрав ее на панели задач. Запись сценария также остановит!
ся при нажатии Ctrl+Break или Ctrl+Alt+Delete; первый вариант упоминается в

578

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

документации к функциям!ловушкам, а второй позволяет принудительно завер!
шить все активные ловушки записи журнала (journaling hooks), при помощи ко!
торых TESTREC.EXE колдует.
Прежде чем приступить к записи своих многочисленных сценариев, вы долж!
ны составить некоторый план, чтобы полностью задействовать преимущества Tester.
Хотя Tester умеет обрабатывать и воспроизводить манипуляции с мышью, ваши
сценарии будут гораздо более надежными, если все действия вы будете выполнять
при помощи клавиатуры. Одно из достоинств Tester в том, что при записи сцена!
рия он внимательно следит за тем, какому окну принадлежит фокус. По умолча!
нию перед обработкой одиночных и двойных щелчков мыши Tester генерирует
код, устанавливающий фокус на окно верхнего уровня. Кроме того, при записи
действий с клавиатурой Tester отслеживает комбинацию Alt+Tab, также устанав!
ливая фокус.
Так как запись всех событий мыши может привести к получению просто ог!
ромного сценария, по умолчанию TESTREC.EXE записывает только одиночные,
двойные щелчки и перемещение курсора на каждые 50 пикселов при нажатой
клавише. Конечно, я позволяю точно указать параметры записи сценариев. Диа!
логовое окно Script Recording Options (параметры записи сценариев) программы
TESTREC.EXE, показанное на рис. 16!1, доступно при нажатии Ctrl+T или выборе
пункта Script Options (параметры сценария) из меню Scripts (сценарии). На ри!
сунке все пункты имеют значения по умолчанию.

Рис. 161.

Параметры записи сценариев Tester

В самой верхней части окна Script Recording Options вы можете выбрать язык
записи новых сценариев: JScript или VBScript. Установленный по умолчанию флажок
Record For Multiple Monitor Playback (запись сценария для воспроизведения на
нескольких мониторах), включает в сценарий вызовы метода TSystem.CheckVirtual
Resolution, настраивающие размер экрана для последующих записываемых дей!
ствий. Если этот флажок убрать, при нажатии кнопок мыши и доступе к точкам
вне основного монитора запись будет прерываться. Возможно, вам следует отклю!
чить эту функцию, если вы планируете запускать записанные сценарии на несколь!

ГЛАВА 16

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

579

ких компьютерах. Однако при записи сценариев, с которыми будете работать
только вы, лучше оставить этот флажок установленным, чтобы позже можно было
задействовать все удобства нескольких мониторов.
Если вы создаете сценарий, включающий интенсивную работу с мышью, и
желаете записать все движения мыши между нажатием и отпусканием ее клавиш,
задайте в поле Minimum Pixels To Drag Before Generating A MOVETO (минималь!
ное число пикселов, после которого при перетаскивании генерируется команда
MOVETO) значение 0. Если вы собираетесь записать много нажатий кнопок мыши
без передачи фокуса другим приложениям, снимите флажок Record Focus Changes
With Mouse Clicks And Double Clicks (записывать изменения фокуса при одиноч!
ных и двойных щелчках мыши). Благодаря этому TESTREC.EXE не будет генери!
ровать код, форсирующий установку фокуса при каждом щелчке, что сделает ваши
сценарии гораздо меньше.
При заданном флажке Do Child Focus Attempt In Scripts (выполнять попытку
установки фокуса на дочернем окне) в сценарий добавляется код, пытающийся
установить фокус на конкретном элементе управления или дочернем окне, в ко!
тором вы щелкаете. По умолчанию этот параметр отключен, так как я генерирую
команды установки фокуса на окне верхнего уровня. Такие приложения, как Notepad
имеют только одно дочернее окно, однако многие другие программы характери!
зуются глубоко вложенной иерархией окон, и отслеживание дочерних окон мо!
жет быть сложным, когда все родительские окна не имеют заголовков и уникаль!
ных классов. Изучите, например, иерархию окон редактора Visual Studio .NET при
помощи утилиты Spy++. Я обнаружил, что установка фокуса на окне верхнего уров!
ня перед генерированием кода нажатия кнопки мыши, как правило, работает от!
лично.
Наконец, параметр Seconds To Wait Before Inserting SLEEP Statements (число
секунд перед включением команд SLEEP) автоматически включает в сценарий паузы,
превышающие указанное значение в секундах. Обычно вы будете хотеть, чтобы
сценарии выполнялись максимально быстро, однако дополнительная пауза помо!
жет скоординировать сценарии.
Сценарии Tester поддерживают тот же формат записи и воспроизведения дан!
ных, что и класс .NET System.Windows.Forms.SendKeys, кроме параметра повторения
клавиш. Для обработки событий мыши я расширил формат, включив в него команду
повторения, а также модификаторы формата, необходимые для использования
вместе с ней клавиш Ctrl, Alt и Shift. Формат команд мыши, передаваемых методу
TInput.PlayInput, описан в табл. 16!1.

Табл. 16-1. Команды мыши, передаваемые методу TInput.PlayInput
Команда

Использование

MOVETO

{MOVETO x , y}

BTNDOWN

{BTNDOWN btn , x , y}

BTNUP

{BTNUP btn , x , y}

CLICK

{CLICK btn , x , y}

DBLCLICK

{DBLCLICK btn , x , y}

SHIFT DOWN

{SHIFT DOWN}

SHIFT UP

{SHIFT UP}

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

580

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Табл. 16-1. Команды мыши …
Команда

Использование

CTRL DOWN

{CTRL DOWN}

CTRL UP

{CTRL UP}

ALT DOWN

{ALT DOWN}

ALT UP

{ALT UP}

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

btn: LEFT, RIGHT, MIDDLE
x: координата экрана X
y: координата экрана Y

Есть некоторые функции мыши, которые я не смог реализовать. Во!первых, это
обработка колесика. Я использовал ловушку записи журнала для перехвата дей!
ствий с клавиатурой и мышью и смог получать сообщения о вращении колесика.
Но, увы, из!за ошибки в функции!ловушке нет возможности узнать направление
вращения колесика. Во!вторых, я не смог реализовать обработку новых клавиш
X1 и X2, имеющихся на мыши Microsoft Explorer. Соответствующие сообщения
WM_XBUTTON* содержат данные о нажатой клавише в старшем слове wParam. Так как
сообщение WM_MOUSEWHEEL хранит направление вращения колесика там же, но фун!
кция!ловушка эту информацию не получает, я сомневаюсь, чтобы в случае кноп!
ки X ситуация чем!нибудь отличалась.

Реализация Tester
Вы уже представляете, как записывать и воспроизводить при помощи Tester сцена!
рии автоматизации тестирования, поэтому я перейду к некоторым более существен!
ным вопросам его реализации. Просуммировав объем исходных и двоичных фай!
лов Tester, включая TESTER.DLL и TESTREC.EXE, вы увидите, что Tester — самая
крупная, а кроме того, и самая сложная утилита в этой книге из!за COM, рекур!
сивного синтаксического разбора и фоновых таймеров.

Уведомления и воспроизведение файлов в TESTER.DLL
В первом издании книги я реализовал TESTER.DLL на Visual Basic 6, потому что в
то время язык и среда разработки Visual Basic 6 пользовались при работе с COM
наибольшей популярностью. Однако я не хотел требовать от вас установки Visual
Basic 6 только для компиляции одной DLL для COM. Прежде всего я решил пере!
нести код TESTER.DLL на платформу .NET. Так как модуль воспроизведения собы!
тий клавиатуры был написана на C++, я решил, что будет проще переписать часть
Tester, реализованную на Visual Basic 6, на C++, задействовав при этом преимуще!
ства новой технологии программирования COM на базе атрибутов (attributed COM
programming).
В целом модель COM на базе атрибутов очень удобна, но мне потребовалось
некоторое время для обнаружения атрибута idl_quote, необходимого для поддер!
жки объявлений интерфейсов. Очень приятным сюрпризом при работе с COM на
базе атрибутов оказалась чистота комбинирования языков IDL/ODL и кода C++.
Кроме того, благодаря значительно улучшенным мастерам облегчилось добавле!

ГЛАВА 16

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

581

ние интерфейсов и их методов и свойств. Я хорошо помню, сколько времени ухо!
дило на решение проблем с мастерами в предыдущих версиях Visual Studio.
Когда я только задумывал утилиту автоматизированного воспроизведения, я
полагал, что могу использовать функцию SendKeys языка Visual Basic 6. Однако после
тестирования я обнаружил, что такая реализация неудовлетворительна, так как она
некорректно посылает события клавиатуры таким программам, как Microsoft
Outlook. Это означало, что мне нужно было написать собственную функцию, ко!
торая правильно посылала бы события клавиатуры и позволяла в будущем реали!
зовать воспроизведение событий мыши. К счастью, я натолкнулся на функцию
SendInput, поддерживаемую технологией Microsoft Active Accessibility (MSAA), и
заменил ею все предыдущие низкоуровневые функции обработки событий, такие
как keybd_event. Кроме того, функция SendInput помещает всю вводимую инфор!
мацию в поток ввода клавиатуры или мыши в виде непрерывного блока, гаранти!
руя, что вводимые вами данные не будут перемешаны с посторонней пользова!
тельской информацией. Эта возможность была особенно привлекательной для
Tester.
Как только я узнал, как правильно посылать нажатия клавиш, мне нужно было
разработать формат их ввода. Функция SendKeys языка Visual Basic 6 или класс
System.Windows.Forms.SendKeys платформы .NET уже предоставляли отличный фор!
мат ввода, поэтому я решил воспроизвести его в своей функции PlayInput. Я ис!
пользовал все, кроме кода повтора клавиш, и, как уже говорилось, расширил формат,
включив в него поддержку воспроизведения событий мыши. В коде синтаксичес!
кого разбора нет ничего особо интересного, но если вам захочется его изучить,
вы найдете его на CD в файле Tester\Tester\ParsePlayInputString.CPP. Если вам за!
хочется увидеть его в действии, можете проработать в отладчике программу Parse!
PlayKeysTest из каталога Tester\Tester\Tests\ParsePlayKeysTest. Как можно догадать!
ся по имени программы, это один из блочных тестов для Tester DLL.
Объекты TWindow, TWindows и TSystem просты, и вы сможете разобраться в их ра!
боте по исходному коду. Эти три класса являются по сути оболочками для соот!
ветствующих API!функций Windows. Единственный более!менее интересный ас!
пект их реализации заключался в написании кода, гарантирующего, что методы
TWindow.SetFocusTWindow и TSystem.SetSpecificFocus могут сделать окно активным. Для
этого они до установки фокуса выполняют присоединение к потоку вывода при
помощи API!функции AttachThreadInput.
С некоторыми интересными проблемами я столкнулся при написании класса
TNotify. Когда я только начал думать о том, что нужно для определения создания
или уничтожения окна с конкретным заголовком, я не ожидал, что создать такой
класс будет настолько трудно. Кроме того, я обнаружил, что уведомления о создании
окон невозможно сделать надежными, не приложив героических усилий.
Моя первая идея заключалась в реализации системной ловушки для приложе!
ний компьютерной профессиональной подготовки [computer!based training (CBT)
hook]. В документации SDK подразумевается, что ловушка CBT — лучший метод
перехвата событий создания и уничтожения окон. Я быстро написал пример, но
вскоре столкнулся с неприятностями. Когда моя ловушка получала уведомление
HCBT_CREATEWND, я не всегда мог узнать заголовок окна. По непродолжительном раз!

582

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

мышлении проблема начала обретать смысл: вероятно, ловушка CBT вызывалась
во время обработки сообщения WM_CREATE, и очень немногие окна имели в этот
момент установленные заголовки. Единственным типом окон, заголовки которых
я мог надежно получать при помощи уведомления HCBT_CREATEWND, были диалого!
вые окна. В то же время с уничтожением окон при использовании ловушки CBT
все было в порядке.
Просмотрев остальные типы ловушек, я расширил свой пример и попробовал
их в действии. Как я и подозревал, простое отслеживание WM_CREATE не обеспечи!
вало надежного получения заголовка окна. Один друг предложил мне наблюдать
только за сообщениями WM_SETTEXT, так как именно его в конечном счете исполь!
зуют для установки заголовка почти все окна. Конечно, если вы рисуете в некли!
ентской области окна или выполняете битовый перенос (bit blitting), вы не буде!
те использовать сообщение WM_SETTEXT. По ходу дела я заметил одну интересную
деталь: некоторые программы, в частности Microsoft Internet Explorer, посылают
сообщения WM_SETTEXT с одним и тем же текстом много раз подряд.
Поняв, что мне нужно следить за сообщениями WM_SETTEXT, я внимательней
рассмотрел типы ловушек, которые мог установить. В конце концов наилучшим
вариантом оказалась ловушка вызова оконной процедуры (WH_CALLWNDPROCRET). Она
позволяет с легкостью наблюдать за сообщениями WM_CREATE и WM_SETTEXT, а также
за сообщениями WM_DESTROY. Сначала я полагал, что с WM_DESTROY будут некоторые
проблемы, так как думал, что заголовок окна может быть уничтожен до получе!
ния этого сообщения. К счастью, оказалось, что заголовок окна корректен вплоть
до получения сообщения WM_NCDESTROY.
Рассмотрев плюсы и минусы обработки сообщений WM_SETTEXT только для тех
окон, которые еще не имеют заголовка, я решил поступить проще и обрабаты!
вать все сообщения WM_SETTEXT. Альтернативным вариантом могло бы быть созда!
ние конечного автомата для отслеживания созданных окон и времени установки
ими своих заголовков; это решение казалось безошибочным, однако в то же вре!
мя сложным в реализации. Недостаток обработки всех сообщений WM_SETTEXT в том,
что вы можете получить много уведомлений о создании одного окна. Например,
если вы установите обработчик TNotify для окон, содержащих в заголовке слово
«Notepad», вы будете получать уведомления не только при запуске NOTEPAD.EXE,
но и при каждом открытии им нового файла. В итоге я предпочел смириться с не
самой лучшей реализацией, но не проводить многие дни за отладкой «правиль!
ного» решения. Кроме того, написание ловушки охватывало реализацию итого!
вого класса TNotify только на четверть; остальные три четверти были посвящены
уведомлению пользователя о создании и уничтожении окон.
Выше я упоминал, что использование объекта TNotify связано с некоторыми
неудобствами: время от времени вы должны вызывать метод CheckNotification.
Необходимость периодического вызова CheckNotification объясняется тем, что Tester
поддерживает только модель разделенных потоков, которая не может быть мно!
гопоточной; так что мне нужен был механизм проверки создания и уничтожения
окон, работающий в том же потоке, что и оставшаяся часть Tester.
Рассмотрев некоторые аспекты механизма уведомлений, я ограничил требо!
вания следующими базовыми факторами.

ГЛАВА 16

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

583

쐽 Ловушка WH_CALLWNDPROCRET должна быть системной, поэтому ее необходимо
реализовать в ее собственной DLL.
쐽 Очевидно, что DLL приложения Tester не подходит на эту роль, так как я не хочу
размещать всю эту DLL и, соответственно, весь код COM в каждом адресном
пространстве на компьютере пользователя. Это значит, что DLL ловушки, на!
верное, должна устанавливать что!то вроде флага, который DLL приложения
Tester могла бы читать, узнавая об удовлетворении нужного условия.
쐽 Tester не может быть многопоточным, поэтому мне нужно выполнять всю об!
работку в одном потоке.
Первое следствие из этих требований заключалось в том, что функцию!ловушку
нужно было написать на C. Так как функция!ловушка загружается во все адрес!
ные пространства, ее DLL не может вызывать функции из TESTER.DLL, написан!
ные при помощиразделенных потоков COM. Поэтому мой код должен был пери!
одически проверять результаты работы ловушки.
Если вы писали программы для 16!разрядных ОС Windows, то знаете, что ка!
кая!нибудь фоновая обработка в однопоточной среде с невытесняющей много!
задачностью — прекрасная работа для API!функции SetTimer. Благодаря SetTimer
вы можете выполнять фоновую задачу, сохраняя приложение однопоточным. С этой
целью я включил в объект TNotify уведомление таймера, указывающее на созда!
ние или уничтожение интересующих меня окон.
Фоновая обработка при помощи TNotify интересна тем, что процедура тайме!
ра казалась решением на все случаи жизни, однако на самом деле она обычно
работает только при наличии TNotify. В зависимости от объема сценария и от того,
реализован ли в выбранном вами языке цикл сообщений, сообщение WM_TIMER может
до вас не добраться, поэтому вам нужно вызывать метод CheckNotification, кото!
рый также проверяет данные ловушки.
Все эти подробности могут казаться запутанными, но вы удивитесь, как мало
кода понадобилось для фактической реализации Tester. В листинге 16!3 показан
код функции!ловушки из файла TNOTIFYHLP.CPP. С точки зрения Tester, файл
TNOTIFY.CPP — это модуль, в котором находится процедура таймера и код COM,
необходимый для объекта. Класс TNotify имеет несколько методов C++, которые
объект TNotify может использовать для возбуждения событий и определения ин!
тересующих пользователя типов уведомлений. Один из интересных фрагментов
кода функции!ловушки — глобально разделяемый сегмент данных, .HOOKDATA, со!
держащий массив данных об уведомлениях. При изучении кода помните, что дан!
ные об уведомлениях глобальны, в то время как все остальные данные уникальны
для каждого процесса.

Листинг 16-3.

TNOTIFYHLP.CPP

/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright © 19972003 John Robbins — All rights reserved.
——————————————————————————————————————————————————————————————————————*/
#include "stdafx.h"
/*//////////////////////////////////////////////////////////////////////
см. след. стр.

584

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Определения и константы с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Максимальное число слотов уведомлений
static const int TOTAL_NOTIFY_SLOTS = 5 ;
// Имя мьютекса
static const LPCTSTR k_MUTEX_NAME = _T ( "TNotifyHlp_Mutex" ) ;
// Наибольшее время ожидания мьютекса
static const int k_WAITLIMIT = 5000 ;
// Здесь я определяю свою директиву TRACE, потому что не хочу
// размещать BugslayerUtil.DLL в каждом адресном пространстве.
#ifdef _DEBUG
#define TRACE ::OutputDebugString
#else
#define TRACE __noop
#endif
/*//////////////////////////////////////////////////////////////////////
// Объявления typedef с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Структура данных интересующего нас окна.
typedef struct tag_TNOTIFYITEM
{
// PID процесса, создавшего структуру
DWORD dwOwnerPID ;
// Тип уведомления
int
iNotifyType ;
// Параметр сравнения заголовка
int
iSearchType ;
// Описатель создаваемого окна
HWND
hWndCreate ;
// Флаг, указывающий на уничтожение окна
BOOL
bDestroy
;
// Строка заголовка
TCHAR szTitle [ MAX_PATH ] ;
} TNOTIFYITEM , * PTNOTIFYITEM ;
/*//////////////////////////////////////////////////////////////////////
// Глобальные переменные с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Эти данные **НЕ** являются общими для процессов,
// поэтому каждый процесс получает собственную их копию.
// HINSTANCE этого модуля. Установка глобальных системных
// ловушек должна выполняться при помощи DLL.
static HINSTANCE g_hInst = NULL ;
// Мьютекс, защищающий таблицу g_NotifyData
static HANDLE g_hMutex = NULL ;
// Описатель ловушки. Я не включил его в раздел общих

ГЛАВА 16

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

585

// данных, потому что при выполнении нескольких сценариев
// процессы могут устанавливать собственные ловушки.
static HHOOK g_hHook = NULL ;
// Число элементов, добавленных в таблицу этим процессом. Это
// число нужно для того, чтобы я знал, как обрабатывать ловушку.
static int g_iThisProcessItems = 0 ;
/*//////////////////////////////////////////////////////////////////////
// Прототипы функций с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
// Наша функцияловушка
LRESULT CALLBACK CallWndRetProcHook ( int
nCode ,
WPARAM wParam ,
LPARAM lParam ) ;
// Внутренняя функция проверки
static LONG_PTR __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate ) ;
/*//////////////////////////////////////////////////////////////////////
// Данные, общие для всех экземпляров ловушек
//////////////////////////////////////////////////////////////////////*/
#pragma data_seg ( ".HOOKDATA" )
// Таблица уведомлений
static TNOTIFYITEM g_shared_NotifyData [ TOTAL_NOTIFY_SLOTS ] =
{
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' } ,
{ 0 , 0 , 0 , NULL , 0 , '\0' }
} ;
// Счетчик использованных слотов уведомлений
static int g_shared_iUsedSlots = 0 ;
#pragma data_seg ( )
/*//////////////////////////////////////////////////////////////////////
// ЗДЕСЬ НАЧИНАЕТСЯ ВНЕШНЯЯ РЕАЛИЗАЦИЯ
//////////////////////////////////////////////////////////////////////*/
extern "C" BOOL WINAPI DllMain ( HINSTANCE hInst
,
DWORD
dwReason
,
LPVOID
/*lpReserved*/ )
{
#ifdef _DEBUG
BOOL bCHRet ;
#endif
BOOL bRet = TRUE ;
switch ( dwReason )
см. след. стр.

586

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

{
case DLL_PROCESS_ATTACH :
// Присвоение значения глобальному описателю модуля.
g_hInst = hInst ;
// Мне не нужны уведомления, связанные с потоками.
DisableThreadLibraryCalls ( g_hInst ) ;
// Создание мьютекса для данного процесса. Мьютекс
// создается, но его получение пока не выполняется.
g_hMutex = CreateMutex ( NULL , FALSE , k_MUTEX_NAME ) ;
if ( NULL == g_hMutex )
{
TRACE ( _T ( "Unable to create the mutex!\n" ) ) ;
// Если я не могу создать мьютекс, продолжение
// невозможно, и загрузка DLL завершилась неудачей.
bRet = FALSE ;
}
break ;
case DLL_PROCESS_DETACH :
//
//
//
if
{

Имеет ли этот процесс какиенибудь элементы
в массиве уведомлений? Если да, я их удаляю,
чтобы не оставлять осиротевшие элементы.
( 0 != g_iThisProcessItems )
DWORD dwProcID = GetCurrentProcessId ( ) ;
// В этом случае мне не нужно получать мьютекс,
// потому что только один поток может вызывать
// DLL по причине DLL_PROCESS_DETACH.
// Нахождение элементов, относящихся к этому процессу.
for ( INT_PTR i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )
{
if ( g_shared_NotifyData[i].dwOwnerPID == dwProcID )
{

#ifdef _DEBUG
TCHAR szBuff[ 50 ] ;
wsprintf ( szBuff ,
_T("DLL_PROCESS_DETACH removing : #%d\n"),
i ) ;
TRACE ( szBuff ) ;
#endif
// И их удаление.
RemoveNotifyTitle ( (HANDLE)i ) ;
}
}
}
// Закрытие описателя мьютекса.
#ifdef _DEBUG
bCHRet =

ГЛАВА 16

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

587

#endif
CloseHandle ( g_hMutex ) ;
#ifdef _DEBUG
if ( FALSE == bCHRet )
{
TRACE ( _T ( "!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;
TRACE ( _T ( "CloseHandle(g_hMutex) " ) \
_T ( "failed!!!!!!!!!!!!!!!!!!\n" ) ) ;
TRACE ( _T ( "!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;
}
#endif
break ;
default
:
break ;
}
return ( bRet ) ;
}

HANDLE TNOTIFYHLP_DLLINTERFACE __stdcall
AddNotifyTitle ( int
iNotifyType ,
int
iSearchType ,
LPCTSTR szString
)
{
// Проверка корректности типа уведомления.
if ( ( iNotifyType < ANTN_DESTROYWINDOW
) ||
( iNotifyType > ANTN_CREATEANDDESTROY ) )
{
TRACE (
_T( "AddNotify Title : iNotifyType is out of range!\n" ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Проверка корректности метода сравнения заголовка.
if ( ( iSearchType < ANTS_EXACTMATCH ) ||
( iSearchType > ANTS_ANYLOCMATCH ) )
{
TRACE (
_T( "AddNotify Title : iSearchType is out of range!\n" ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Проверка корректности заголовка.
if ( TRUE == IsBadStringPtr ( szString , MAX_PATH ) )
{
TRACE ( _T( "AddNotify Title : szString is invalid!\n" ) ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Ожидание получения мьютекса.
DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;
if ( WAIT_TIMEOUT == dwRet )
см. след. стр.

588

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

{
TRACE (_T( "AddNotifyTitle : Wait on mutex timed out!!\n"));
return ( INVALID_HANDLE_VALUE ) ;
}
// Если все слоты использованы, выполняется выход.
if ( TOTAL_NOTIFY_SLOTS == g_shared_iUsedSlots )
{
ReleaseMutex ( g_hMutex ) ;
return ( INVALID_HANDLE_VALUE ) ;
}
// Нахождение
for ( INT_PTR
{
if ( _T (
{
break
}
}

первого свободного слота.
i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )
'\0' ) == g_shared_NotifyData[ i ].szTitle[ 0 ] )
;

// Добавление данных.
g_shared_NotifyData[ i ].dwOwnerPID = GetCurrentProcessId ( ) ;
g_shared_NotifyData[ i ].iNotifyType = iNotifyType ;
g_shared_NotifyData[ i ].iSearchType = iSearchType ;
lstrcpy ( g_shared_NotifyData[ i ].szTitle , szString ) ;
// Увеличение счетчика использованных слотов.
g_shared_iUsedSlots++ ;
// Увеличение счетчика элементов этого процесса.
g_iThisProcessItems++ ;
TRACE ( _T( "AddNotifyTitle  Added a new item!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
// Если это первый запрос об уведомлениях, устанавливается ловушка.
if ( NULL == g_hHook )
{
g_hHook = SetWindowsHookEx ( WH_CALLWNDPROCRET ,
CallWndRetProcHook ,
g_hInst
,
0
) ;
#ifdef _DEBUG
if ( NULL == g_hHook )
{
TCHAR szBuff[ 50 ] ;
wsprintf ( szBuff ,
_T ( "SetWindowsHookEx failed!!!! (0x%08X)\n" ),

ГЛАВА 16

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

589

GetLastError ( ) ) ;
TRACE ( szBuff ) ;
}
#endif
}
return ( (HANDLE)i ) ;
}
void TNOTIFYHLP_DLLINTERFACE __stdcall
RemoveNotifyTitle ( HANDLE hItem )
{
// Проверка описателя.
INT_PTR i = (INT_PTR)hItem ;
if ( ( i < 0 ) || ( i > TOTAL_NOTIFY_SLOTS ) )
{
TRACE ( _T ( "RemoveNotifyTitle : Invalid handle!\n" ) ) ;
return ;
}
// Получение мьютекса.
DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;
if ( WAIT_TIMEOUT == dwRet )
{
TRACE ( _T ( "RemoveNotifyTitle : Wait on mutex timed out!\n"));
return ;
}
if ( 0 == g_shared_iUsedSlots )
{
TRACE ( _T ( "RemoveNotifyTitle : Attempting to remove when " )\
_T ( "no notification handles are set!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
return ;
}
//
//
//
//
//
if
{

Перед удалением чеголибо нужно убедиться в том, что индекс
указывает на элемент NotifyData, имеющий корректное значение.
Если бы я этого не делал, неоднократный вызов данной функции
с одним и тем же параметром приводил бы к сбою счетчика
использованных слотов.
( 0 == g_shared_NotifyData[ i ].dwOwnerPID )
TRACE ( _T ( "RemoveNotifyTitle : ") \
_T ( "Attempting to double remove!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
return ;

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

590

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// Удаление элемента
g_shared_NotifyData[
g_shared_NotifyData[
g_shared_NotifyData[
g_shared_NotifyData[
g_shared_NotifyData[
g_shared_NotifyData[

из массива.
i ].dwOwnerPID
i ].iNotifyType
i ].hWndCreate
i ].bDestroy
i ].iSearchType
i ].szTitle[ 0 ]

=
=
=
=
=
=

0 ;
0 ;
NULL ;
FALSE ;
0 ;
_T ( '\0' ) ;

// Уменьшение счетчика использованных слотов.
g_shared_iUsedSlots— ;
// Уменьшение счетчика элементов данного процесса.
g_iThisProcessItems— ;
TRACE ( _T ( "RemoveNotifyTitle  Removed an item!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
// Если это последний элемент данного
// процесса, ловушка процесса удаляется.
if ( ( 0 == g_iThisProcessItems ) && ( NULL != g_hHook ) )
{
if ( FALSE == UnhookWindowsHookEx ( g_hHook ) )
{
TRACE ( _T ( "UnhookWindowsHookEx failed!\n" ) ) ;
}
g_hHook = NULL ;
}
}
HWND TNOTIFYHLP_DLLINTERFACE __stdcall
CheckNotifyCreateTitle ( HANDLE hItem )
{
return ( (HWND)CheckNotifyItem ( hItem , TRUE ) ) ;
}
BOOL TNOTIFYHLP_DLLINTERFACE __stdcall
CheckNotifyDestroyTitle ( HANDLE hItem )
{
return ( (BOOL)CheckNotifyItem ( hItem , FALSE ) ) ;
}
/*//////////////////////////////////////////////////////////////////////
// ЗДЕСЬ НАЧИНАЕТСЯ ВНУТРЕННЯЯ РЕАЛИЗАЦИЯ
//////////////////////////////////////////////////////////////////////*/
static LONG_PTR __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate )
{
// Проверка описателя.

ГЛАВА 16

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

591

INT_PTR i = (INT_PTR)hItem ;
if ( ( i < 0 ) || ( i > TOTAL_NOTIFY_SLOTS ) )
{
TRACE ( _T ( "CheckNotifyItem : Invalid handle!\n" ) ) ;
return ( NULL ) ;
}
LONG_PTR lRet = 0 ;
// Получение мьютекса.
DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;
if ( WAIT_TIMEOUT == dwRet )
{
TRACE ( _T ( "CheckNotifyItem : Wait on mutex timed out!\n" ) );
return ( NULL ) ;
}
// Если все слоты пусты, делать нечего.
if ( 0 == g_shared_iUsedSlots )
{
ReleaseMutex ( g_hMutex ) ;
return ( NULL ) ;
}
// Проверка запрошенного элемента.
if ( TRUE == bCreate )
{
// Если HWND не равен NULL, выполняется возврат
// его значения и его обнуление в таблице.
if ( NULL != g_shared_NotifyData[ i ].hWndCreate )
{
lRet = (LONG_PTR)g_shared_NotifyData[ i ].hWndCreate ;
g_shared_NotifyData[ i ].hWndCreate = NULL ;
}
}
else
{
if ( FALSE != g_shared_NotifyData[ i ].bDestroy )
{
lRet = TRUE ;
g_shared_NotifyData[ i ].bDestroy = FALSE ;
}
}
ReleaseMutex ( g_hMutex ) ;
return ( lRet ) ;
}
static void __stdcall CheckTableMatch ( int

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

592

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

HWND
hWnd
LPCTSTR szTitle

,
)

{
// Получение мьютекса.
DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ;
if ( WAIT_TIMEOUT == dwRet )
{
TRACE ( _T ( "CheckTableMatch : Wait on mutex timed out!\n" ) );
return ;
}
// Таблица не должна быть пустой, но никогда
// нельзя быть ни в чем уверенным.
if ( 0 == g_shared_iUsedSlots )
{
ReleaseMutex ( g_hMutex ) ;
TRACE ( _T ( "CheckTableMatch called on an empty table!\n" ) ) ;
return ;
}

// Просмотр элементов таблицы.
for ( int i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ )
{
// Не пуст ли этот элемент и совпадают ли типы уведомлений?
if ( ( _T ( '\0' ) != g_shared_NotifyData[ i ].szTitle[ 0 ] ) &&
( g_shared_NotifyData[ i ].iNotifyType & iNotifyType ) )
{
BOOL bMatch = FALSE ;
// Сопоставление заголовка окна
// с аналогичным полем элемента таблицы.
switch ( g_shared_NotifyData[ i ].iSearchType )
{
case ANTS_EXACTMATCH
:
// Это просто.
if ( 0 == lstrcmp ( g_shared_NotifyData[i].szTitle ,
szTitle
) )
{
bMatch = TRUE ;
}
break ;
case ANTS_BEGINMATCH
:
if ( 0 ==
_tcsnccmp ( g_shared_NotifyData[i].szTitle ,
szTitle
,
_tcslen(g_shared_NotifyData[i].szTitle)))
{
bMatch = TRUE ;
}
break ;

ГЛАВА 16

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

593

case ANTS_ANYLOCMATCH :
if ( NULL != _tcsstr ( szTitle
,
g_shared_NotifyData[i].szTitle ))
{
bMatch = TRUE ;
}
break ;
default
:
TRACE ( _T ( "CheckTableMatch invalid " ) \
_T ( "search type!!!\n" ) ) ;
ReleaseMutex ( g_hMutex ) ;
return ;
break ;
}
// Ну, и каковы результаты?
if ( TRUE == bMatch )
{
// Если это уведомление об уничтожении окна,
// соответствующее поле получает значение "1".
if ( ANTN_DESTROYWINDOW == iNotifyType )
{
g_shared_NotifyData[ i ].bDestroy = TRUE ;
}
else
{
// Иначе устанавливается значение HWND.
g_shared_NotifyData[ i ].hWndCreate = hWnd ;
}
}
}
}
ReleaseMutex ( g_hMutex ) ;
}
LRESULT CALLBACK CallWndRetProcHook ( int
nCode ,
WPARAM wParam ,
LPARAM lParam )
{
// Буфер для хранения заголовка окна
TCHAR szBuff[ MAX_PATH ] ;
// Прежде чем чтолибо сделать, я всегда передаю сообщение
// следующей ловушке, чтобы не забыть сделать это потом.
// Передав сообщение, я могу спокойно заняться своими делами.
LRESULT lRet = CallNextHookEx ( g_hHook , nCode , wParam , lParam );
// В документации запрещается обрабатывать сообщение при
// отрицательных значениях параметра nCode. Не будем спорить.
if ( nCode < 0 )
{
см. след. стр.

594

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

return ( lRet ) ;
}
// Получение структуры сообщения. Интересно, зачем нужны три
// (или больше) различных структуры сообщений? Почему нельзя
// было использовать структуру MSG для всех сообщений/ловушек?
PCWPRETSTRUCT pMsg = (PCWPRETSTRUCT)lParam ;
// Нет заголовка — нет работы.
LONG lStyle = GetWindowLong ( pMsg>hwnd , GWL_STYLE ) ;
if ( WS_CAPTION != ( lStyle & WS_CAPTION ) )
{
return ( lRet ) ;
}
//
//
//
if
{

Сообщения WM_DESTROY используются и диалоговыми, и обычными
окнами. Нужно просто получить заголовок окна и проверить
наличие соответствующего элемента в таблице уведомлений.
( WM_DESTROY == pMsg>message )
if ( 0 != GetWindowText ( pMsg>hwnd , szBuff , MAX_PATH ) )
{
CheckTableMatch ( ANTN_DESTROYWINDOW , pMsg>hwnd , szBuff ) ;
}
return ( lRet ) ;

}
// С созданием окна все не так просто, как с его уничтожением.
//
//
if
{
#ifdef

Получение класса окна. Если это подлинное диалоговое
окно, мне нужно только сообщение WM_INITDIALOG.
( 0 == GetClassName ( pMsg>hwnd , szBuff , MAX_PATH ) )
_DEBUG
TCHAR szBuff[ 50 ] ;
wsprintf ( szBuff
,
_T ( "GetClassName failed for HWND : 0x%08X\n" ) ,
pMsg>hwnd
) ;
TRACE ( szBuff ) ;

#endif
// Продолжение не имеет смысла.
return ( lRet ) ;
}
if ( 0 == lstrcmpi ( szBuff , _T ( "#32770" ) ) )
{
// Мне нужно проверять только сообщение WM_INITDIALOG.
if ( WM_INITDIALOG == pMsg>message )
{
// Получение заголовка диалогового окна.
if ( 0 != GetWindowText ( pMsg>hwnd , szBuff , MAX_PATH ) )

ГЛАВА 16

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

595

{
CheckTableMatch ( ANTN_CREATEWINDOW ,
pMsg>hwnd
,
szBuff
) ;
}
}
return ( lRet ) ;
}
// Я разобрался с диалоговыми окнами. Теперь
// мне нужно позаботиться о других окнах.
if ( WM_CREATE == pMsg>message )
{
// Очень немногие окна устанавливают заголовок
// при обработке сообщения WM_CREATE. Однако некоторые
// поступают именно так, и они не используют WM_SETTEXT,
// поэтому я должен выполнить соответствующую проверку.
if ( 0 != GetWindowText ( pMsg>hwnd , szBuff , MAX_PATH ) )
{
CheckTableMatch ( ANTN_CREATEWINDOW ,
pMsg>hwnd
,
szBuff
) ;
}
}
else if ( WM_SETTEXT == pMsg>message )
{
// Я всегда обрабатываю WM_SETTEXT, поскольку именно так
// программы устанавливают заголовки. К сожалению, похоже,
// некоторые приложения, такие как Internet Explorer, вызывают
// WM_SETTEXT несколько раз с одним заголовком. Чтобы не усложнять
// эту функцию, я просто сообщаю WM_SETTEXT вместо поддержки
// странных, тяжелых в отладке структур данных, необходимых
// для слежения за окнами, которые уже вызывали WM_SETTEXT раньше.
if ( NULL != pMsg>lParam )
{
CheckTableMatch ( ANTN_CREATEWINDOW
,
pMsg>hwnd
,
(LPCTSTR)pMsg>lParam ) ;
}
}
return ( lRet ) ;
}
Некоторые аспекты реализации TNotify казались довольно сложными, поэто!
му я был приятно удивлен тем, как мало проблем я испытал на самом деле. Если
вы хотите усовершенствовать код функции!ловушки, знайте, что отлаживать сис!
темные ловушки очень непросто. Для этого лучше всего использовать удаленную
отладку (см. главу 5). Еще один способ отладки системных ловушек заключается
в отладке в стиле printf. Программа DebugView, которую можно загрузить с сайта

596

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

www.sysinternals.com, позволит вам видеть все вызовы OutputDebugString, указыва!
ющие на состояние вашей ловушки.

Реализация TESTREC.EXE
После создания Tester DLL мне нужно было разработать программу TESTREC.EXE,
которая записывала бы события клавиатуры и мыши. Если дело касается записи
вводимой информации, в ОС Windows, есть только один чистый способ сделать
это: использовать ловушку записи журнала. В ловушках записи журнала нет ни!
чего увлекательного, кроме проблемы правильной обработки сообщения WM_CAN

Начальное
состояние
Клавиша
отпущена

Любой ввод

Тип ввода

Нажата
VK_MENU
или VK_CONTROL

Menu
или Control
Нажата
VK_MENU

Нажата
VK_CONTROL
Клавиша
отпущена

Состояние
IsTabKey

Состояние
CheckBreak

Любой ввод

Любой ввод

Тип ввода
Нажата
обычная клавиша

Тип ввода

Клавиша нажата

Клавиша нажата

Клавиша

Клавиша

VK_TAB

VK_CANCEL

Состояние
табуляции

Клавиша
отпущена

Любая
другая клавиша

Прервать
запись

Любая
другая клавиша

Состояние
обычной
клавиши

Рис. 162. Конечные автоматы начала записи событий клавиатуры,
обработки клавиши Tab и комбинации Ctrl+Break

ГЛАВА 16

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

597

CELJOURNAL. Когда пользователь нажимает Ctrl+Alt+Delete, ОС завершает все актив!
ные ловушки записи журнала. Это очень грамотное решение, так как возможность
записи нажатий клавиш при вводе пароля открывала бы огромную брешь в сис!
теме безопасности. Чтобы скрыть детали реализации обработки WM_CANCELJOURNAL,
я написал фильтр, отслеживающий это сообщение. Все подробности работы фун!
кции!ловушки вы можете увидеть в файле HOOKCODE.CPP, находящемся в ката!
логе Tester\TestRec.

Обработка ввода с клавиатуры
Запись событий клавиатуры сводится главным образом к правильной обработке
нажатий клавиш Shift, Ctrl и Alt. Прежде чем я опишу некоторые аспекты борьбы
с нажатиями отдельных клавиш, изучите рисунки 16!2 — 16!4, на которых пред!
ставлен упрощенный граф всех состояний клавиатуры, обрабатываемых кодом
записи сценария.
Нормальное
Normal
состояние
State
Введенная клавиша

Клавиша
отпущена
Тип
сообщения

VK_MENU

Тип ввода

Клавиша
отпущена
VK_CANCEL

Клавиша нажата
Состояние
IsTabKey

Обработка

Рис. 163.

Тип
сообщения

Клавиша нажата
Любая другая клавиша

Тип ввода

Состояние
CheckBreak

VK_SHIFT

Запись
Shift + клавиша
нажата/отпущена

Конечный автомат нормальной обработки событий клавиатуры

Первая проблема записи событий клавиатуры заключается в получении их из
функции!ловушки в понятной человеку форме. Если вы никогда не испытывали
радость работы с виртуальными и скан!кодами, вы не знаете, что такое настоя!
щие трудности! Кроме того, я обнаружил, что некоторые данные, получаемые
ловушкой записи журнала довольно сильно отличались от того, что я ожидал.
Последний раз я работал с клавиатурой на этом уровне во времена MS!DOS
(похоже, я выдал свой возраст!). Поэтому я внес в проблему некоторые дополни!
тельные недоразумения. Например, когда я впервые ввел восклицательный знак,
я ожидал увидеть, что именно этот символ и поступит в функцию!ловушку. Одна!
ко вместо этого я получил символ Shift, за которым следовала единица. Именно
так восклицательный знак вводится с клавиатур US English. Однако я хотел, что!
бы все воспроизводимые мной последовательности нажатых клавиш были пре!

598

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

дельно понятны. Последовательность SendKeys «+1» с технической точки зрения
верна, но при этом нужно проделать некоторую умственную гимнастику, чтобы
понять, что на самом деле это символ «!».
Состояние
Alt Tab
Любое входное сообщение

VK_TAB or VK_SHIFT

Тип ввода

Клавиша

Любая
другая
клавиша

WM_SYSKEYDOWN

WM_SYSKEYUP

WM_SYSKEYDOWN

Игнорируем
Alt+Tab

Клавиша
VK_MENU

Игнорируем
Alt+Tab?

Да

Нет
Начальное
состояние

Определение
состояния фокуса

Прервать
запись

Рис. 164.

Да

В фокусе
при TestRec?

Нет

Конечный автомат обработки комбинации Alt+Tab

Чтобы программа TESTREC.EXE была максимально полезна, я должен был реа!
лизовать некоторый специальный механизм обработки, который позволил бы
сделать выводимые строки понятными. Проще говоря, я должен был проверить
состояние клавиатуры, узнать, нажата ли клавиша Shift, и, если да, преобразовать
символ в понятную форму. К счастью, для получения действительного символа у нас
есть API!функции GetKeyboardState и ToUnicode. Чтобы понять суть обработки нажатий
клавиш, изучите функцию CRecordingEngine::NormalKeyState из листинга 16!4.

Листинг 16-4.

CRecordingEngine::NormalKeyState

void CRecordingEngine :: NormalKeyState ( PEVENTMSG pMsg )
{
// Состояние, в которое будет выполнен переход

ГЛАВА 16

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

599

// после обработки поступившего сообщения.
eKeyStates eShiftToState = eNormalKey ;
UINT vkCode = LOBYTE ( pMsg>paramL ) ;
#ifdef _DEBUG
{
STATETRACE (_T("RECSTATE: Normal : ")) ;
if ( ( WM_KEYDOWN == pMsg>message
) ||
( WM_SYSKEYDOWN == pMsg>message ) )
{
STATETRACE ( _T( "KEYDOWN " ) ) ;
}
else
{
STATETRACE ( _T ( "KEYUP " ) ) ;
}
TCHAR szName [ 100 ] ;
GetKeyNameText ( pMsg>paramH message ) ||
( WM_SYSKEYDOWN == pMsg>message ) )
{
eShiftToState = eCheckBreak ;
STATETRACE ( _T ( "RECSTATE: Looking for BREAK key\n"));
}
else
{
m_cKeyBuff += _T( "{CTRL UP}" ) ;
m_iKeyBuffKeys++ ;
}
m_iKeyBuffKeys++ ;
break ;
case VK_MENU
:
if ( ( WM_KEYDOWN
== pMsg>message ) ||
см. след. стр.

600

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

( WM_SYSKEYDOWN == pMsg>message )

)

{
eShiftToState = eIsTabKey ;
STATETRACE (_T("RECSTATE: Looking for TAB key\n")) ;
}
else
{
m_cKeyBuff += _T( "{ALT UP}" ) ;
m_iKeyBuffKeys++ ;
}
m_iKeyBuffKeys++ ;
break ;
case VK_SHIFT :
if ( ( WM_KEYDOWN
== pMsg>message ) ||
( WM_SYSKEYDOWN == pMsg>message ) )
{
// При нажатии SHIFT этот блок выполняется только один раз!
if ( FALSE == m_bShiftDown )
{
// Нажата клавиша SHIFT, выполняется установка флагов.
m_bShiftDown = TRUE ;
m_bShiftDownInString = FALSE ;
}
}
else
{
// Если раньше я записал {SHIFT DOWN},
// мне нужно сопоставить его с {SHIFT UP}.
if ( TRUE == m_bShiftDownInString )
{
m_cKeyBuff += _T ( "{SHIFT UP}" ) ;
m_iKeyBuffKeys++ ;
m_bShiftDownInString = FALSE ;
}
// Клавиша SHIFT отпущена.
m_bShiftDown = FALSE ;
}
break ;
default :
// Это обычная клавиша.
// Если это сообщение не о нажатии, я ничего не делаю.
if ( ( WM_KEYDOWN
== pMsg>message ) ||
( WM_SYSKEYDOWN == pMsg>message ) )
{
//TRACE ( "vkCode = %04X\n" , vkCode ) ;
// Есть ли строка, соответствующая этому виртуальному коду?

ГЛАВА 16

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

601

if ( NULL != g_KeyStrings[ vkCode ].szString )
{
// Нажата ли клавиша SHIFT? Если да,
// перед записью обрабатываемой клавиши
// мне нужно записать {SHIFT DOWN}.
if ( ( TRUE == m_bShiftDown
) &&
( FALSE == m_bShiftDownInString ) )
{
m_cKeyBuff += _T ( "{SHIFT DOWN}" ) ;
m_iKeyBuffKeys++ ;
m_bShiftDownInString = TRUE ;
}
// Добавление клавиши в список.
m_cKeyBuff += g_KeyStrings[ vkCode ].szString ;
}
else
{
// Я должен преобразовать нажатую клавишу в ее
// символьный эквивалент. Для правильного
// преобразования таких последовательностей, как
// "{SHIFT DOWN}1{SHIFT UP}" в "!", мне нужно
// получить состояние клавиатуры и вызвать
// функцию ToAscii.
// Сначала нужно получить состояние клавиатуры.
BYTE byState [ 256 ] ;
GetKeyboardState ( byState ) ;
// А теперь выполнить преобразование.
TCHAR cConv[3] = { _T ( '\0' ) } ;
TCHAR cChar ;
#ifdef _UNICODE
int iRet = ToUnicode ( vkCode
pMsg7>paramH
byState
(LPWORD)&cConv
sizeof ( cConv ) /
sizeof ( TCHAR )
0

,
,
,
,
,
) ;

#else
int iRet = ToAscii ( vkCode
pMsg7>paramH
byState
(LPWORD)&cConv
0

,
,
,
,
) ;

#endif
if ( 2 == iRet )
{
см. след. стр.

602

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// Это национальный символ.
ASSERT ( !"I gotta handle this!" ) ;
}
// Если символ не был преобразован,
// cChar не используется!
if ( 0 == iRet )
{
cChar = (TCHAR)vkCode ;
}
else
{
// Прежде чем записать символ, мне нужно узнать,
// нажата ли клавиша CTRL. Если да, то функции
// ToAscii/ToUnicode возвращают управляющий
// код ASCII. Так как мне нужен символ, я должен
// выполнить некоторую дополнительную работу.
SHORT sCtrlDown =
GetAsyncKeyState ( VK_CONTROL ) ;
if ( 0xFFFF8000 == ( 0xFFFF8000 & sCtrlDown ))
{
// Клавиша CTRL нажата, поэтому мне нужно
// узнать состояние клавиш CAPSLOCK и SHIFT.
BOOL bCapsLock =
( 0xFFFF8000 == ( 0xFFFF8000 &
GetAsyncKeyState ( VK_CAPITAL)));
if ( TRUE == bCapsLock )
{
// Если нажаты клавиши CAPSLOCK и SHIFT,
// используем символ нижнего регистра.
if ( TRUE == m_bShiftDown )
{
// Запрещение предупреждения 'variable' : conversion from 'type' to 'type'
// of greater size (преобразование к типу, имеющему больший размер).
#pragma warning ( disable : 4312 )
cChar = (TCHAR)
CharLower ( (LPTSTR)vkCode );
#pragma warning ( default : 4312 )
}
else
{
// Символ верхнего регистра.
cChar = (TCHAR)vkCode ;
}
}
else
{
// Клавиша CAPSLOCK не нажата,
// поэтому проверяется только
// клавиша SHIFT.

ГЛАВА 16

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

603

if ( TRUE == m_bShiftDown )
{
cChar = (TCHAR)vkCode ;
}
else
{
// Запрещение предупреждения 'variable' : conversion from 'type' to 'type'
// of greater size (преобразование к типу, имеющему больший размер).
#pragma warning ( disable : 4312 )
cChar = (TCHAR)
CharLower ( (LPTSTR)vkCode );
#pragma warning ( default : 4312 )
}
}
}
else
{
// Клавиша CTRL не нажата, поэтому я могу
// сразу использовать преобразованную клавишу.
cChar = cConv[ 0 ] ;
}
}
switch ( cChar )
{
// Квадратные и фигурные скобки и тильды
// требуют особой обработки. Все остальные
// клавиши просто добавляются в буфер вывода.
case _T ( '[' ) :
m_cKeyBuff += _T ( "{[}" ) ;
break ;
case _T ( ']' ) :
m_cKeyBuff += _T ( "{]}" ) ;
break ;
case _T ( '~' ) :
m_cKeyBuff += _T ( "{~}" ) ;
break ;
case _T ( '{' ) :
m_cKeyBuff += _T ( "{{}" ) ;
break ;
case _T ( '}' ) :
m_cKeyBuff += _T ( "{}}" ) ;
break ;
default :
m_cKeyBuff += cChar ;
}
}
// Увеличение числа обработанных клавиш.
m_iKeyBuffKeys++ ;
см. след. стр.

604

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

if ( ( m_iKeyBuffKeys > 20
) ||
( m_cKeyBuff.Length ( ) > 50 ) )
{
DoKeyStrokes ( TRUE ) ;
}
}
break ;
}
// Установка состояния, в которое выполняется
// переход после обработки этой клавиши.
eCurrKeyState = eShiftToState ;
}
Специальным образом обрабатывается только комбинация Alt+Tab. Я мог за
писывать фактические нажатия клавиш Alt и Tab, однако это могло бы привести
к проблемам при следующем запуске сценария, так как мне нужно было бы раз
мещать окно приложения в том же месте zпорядка и иметь то же число запущенных
приложений. Поэтому вместо записи нажатий клавиш я перехожу в состояние
ожидания того, когда вы отпустите клавишу Alt, а при следующем вводе какойлибо
журнальной информации я определяю, какое приложение имеет фокус, и гене
рирую соответствующий код сценария.

Обработка ввода мыши
Когда я собрался реализовать поддержку мыши, я понастоящему удивился тому,
что мой первоначальный механизм обработки событий клавиатуры не позволял
добавить обработку событий мыши. Первоначальный вариант моего кода обра
ботки клавиатуры оптимизировал обработку Ctrl, Shift и Alt, гарантируя, что один
метод PlayInput включает полную команду, начиная с нажатия одной из клавиш
Ctrl, Shift или Alt и заканчивая ее отпусканием. Когда я стал думать о поддержке
событий мыши, я понял, что это может приводить к генерированию метода Play7
Input, включающего десятки тысяч символов! Даже для начала обработки ввода
мыши мне нужно было изменить код записи событий клавиатуры, чтобы он ге
нерировал специальные коды, такие как {ALT DOWN} и {ALT UP}. Это было нужно для
того, чтобы сгенерированные команды мог выполнять любой язык сценариев.
Позаботившись об обработке Ctrl, Alt и Shift, я должен был разобраться с оди
ночными, двойными щелчками и перетаскиванием. Очень интересно, что ловуш
ка записи журнала, при помощи которой я регистрирую события клавиатуры и
мыши, получает только сообщения WM_xBUTTONDOWN и WM_xBUTTONUP. Я был бы очень
рад получать сообщения WM_xBUTTONDBLCLK, так как это сделало бы мою жизнь го
раздо проще. Обработка событий мыши отчаянно требовала создания конечного
автомата, подобного тому, что я разработал для обработки событий клавиатуры.
На рис. 165 и 166 показан конечный автомат обработки ввода мыши, который я
реализовал в файлах RECORDINGENGINE.H/.CPP. Помните, что я должен был вы
полнять такое отслеживание состояния для каждой клавиши. Слоты 0 и 1 нужны
для слежения за предыдущим событием с целью сравнения.

ГЛАВА 16

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

605

Начальное
состояние
Кнопка
отпущена

Любой ввод

Тип ввода

Возврат к началу

Движение

Генерирование
события движения
(если установлен
параметр)

Нажатие

Возврат
к началу

Генерирование
события
движения

Сохранить событие
в слоте 0

Да

Возврат к состоянию
перетаскивания

Достаточное
расстояние?

Движение

Состояние
перетаскивания

Нет

Посмотреть
состояние
отпускания

Нет

Отпущена

Генерирование
события
отпускания

Любой ввод

Тип ввода

Движение

Достаточное
расстояние?

Да

Генерирование
события
нажатия

Генерирование
события
движения

Отпущена та же кнопка

Проверить
время

Время >
времени
DBL CLK

Генерирование
события
щелчка

Возврат к началу

Время <
времени DBL CLK
Сохранить событие
в слоте 1

Посмотреть
состояние
DBL CLK

Рис. 165.

Конечный автомат нормальной обработки событий мыши

606

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Посмотреть
состояние DBL CLK
Возврат к просмотру
состояния DBL CLK

Любой ввод

Тип ввода

Движение

Генерирование
события движения
(если установлен
параметр)

Время >
времени
DBL CLK

Генерирование
события щелчка
для слотов 0, 1

Посмотреть
состояние
отпускания

Нажата
та же кнопка

Проверить
время

Очистить
слоты 0, 1

Сохранить
событие в слоте 0

Время <
времени DBL CLK

Щелчок в
пределах двойного
щелчка?

Нет

Да
Генерирование
события
двойного щелчка

Очистить
слоты 0, 1

Начальное
состояние

Рис. 166.

Конечный автомат обработки двойного щелчка

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

ГЛАВА 16

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

607

стро передавал команды MOVETO, что вызывало переполнение входной очереди ОС
Windows и отбрасывание избыточных сообщений. Следовательно, я должен был
замедлить обработку сообщений мыши, обеспечив достаточное время для выпол
нения всех соответствующих событий. Так как для воспроизведения команд я
использовал функцию SendInput, я сначала подумал о том, чтобы задать время для
каждого события мыши в структуре INPUT, предоставив дополнительное время для
их обработки. Это не сработало, и я обнаружил, что задание достаточно долгого
времени переводит компьютер в режим энергосбережения, что в первый раз меня
довольно сильно удивило.
Тогда я попробовал другой способ. Я решил, что, если мой код записывает вво
димые команды в массив структур INPUT, указатель на который передается функ
ции SendInput, я могу изучать массив по одному элементу и делать дополнитель
ную паузу при обнаружении событий мыши. Длительность пауз я определил экс
периментально. После ряда проб я обнаружил, что лучше всего делать паузы на
25 миллисекунд до и после каждого события мыши. Это означает, что записан
ные сценарии будут воспроизводиться гораздо медленнее по сравнению с тем, когда
вы их записывали.

Что после Tester?
Как я уже говорил, Tester хорошо справляется с двумя вещами: с записью сцена
риев и воспроизведением записанных событий. Если у вас имеется необходимое
вдохновение, вы можете усовершенствовать Tester (равно как и все остальные
утилиты из этой книги). Вот некоторые возможные способы улучшения Tester:
쐽 Добавьте классыоболочки, такие как TListBox, TTreeControl и TRadioButton, что
бы вы могли проверять состояния и данные элементов управления. Такие классы
позволят проверять элементы управления и писать более сложные сценарии.
Возможно, для облегчения этой задачи понадобится изучить интерфейсы MSAA.
쐽 Реализуйте поддержку оперативного ввода проверочного и другого необходи
мого кода в сценарий во время его записи.
쐽 Сделайте программу TestRec более дружественной к пользователю во время
разработки сценариев и создайте систему помощи. Например, можно реали
зовать хранение сведений о поддерживаемых объектах Tester, что поможет
другим разработчикам писать собственные сценарии.
쐽 Реализуйте возможность записи сценариев на компилируемых языках, таких
как C# и Visual Basic .NET (или Microsoft Visual Object Assembler, если Microsoft
когданибудь его создаст).
쐽 В настоящее время TestRec и Tester поддерживают только клавиатуру US English.
Если хотите, вы можете сделать оба приложения понастоящему интернацио
нальными.

608

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

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

Г Л А В А

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

Даже после выхода первого издания этой книги я получал и получаю массу во
просов по поводу искажений и утечек памяти. Если бы программисты просто пре
кратили использовать память в своих программах, они избежали бы массы про
блем. Все так. А если бы мы прекратили дышать, мы никогда не страдали бы от
легочных болезней. Память — эликсир жизни программ C и C++, поэтому, если
вы и впрямь хотите чтото сделать с искажениями и утечками памяти, а не про
сто мечтать, чтобы они исчезли, нужно позаботиться об их проактивной обра
ботке. Первый шаг в этом направлении — изучение отладочной библиотеки C,
разработанной Microsoft.
Сложность отладки памяти имеет легендарный статус, и именно она была од
ним из главных факторов, побудивших Microsoft разработать платформу .NET.
Благодаря сборщику мусора CLR программисты могут избавиться от многих ас
пектов работы с памятью, что устраняет, наверное, около 50% ошибок, с которы
ми приходится сталкиваться в мире Microsoft Win32/Win64. Однако, если для ва
ших приложений очень важно быстродействие, вам еще долго придется писать
их на C++ и быть готовым к возможным ошибкам при работе с памятью.
C/C++программисты от таких проблем ничем не защищены. Эти языки обес
печивают почти полную свободу программирования, но при этом позволяют вам
не только выстрелить себе в ногу, но и полностью отстрелить ее даже при неболь
шой ошибке. К счастью, разработчики стандартной библиотеки C (C runtime, CRT
library) не оставили наши муки без внимания и создали миллион Интернетлет
назад удивительное средство — отладочную библиотеку CRT (debug CRT, DCRT
library), включенную в состав сред Microsoft, начиная с Visual C++ 4.

610

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Странно, но похоже, что многие C/C++программисты не подозревают о су
ществовании этой библиотеки. Ее таинственность объясняется тем, что по умол
чанию многие ее свойства отключены. Однако, как только вы установите соот
ветствующие флаги, вы сразу поймете, сколь богатые возможности от вас усколь
зали. В этой главе я сначала представлю библиотеку DCRT и опишу два ее расши
рения, MemDumperValidator и MemStress, которые предоставят вам еще более раз
витые возможности. Кратко рассмотрев DCRT, я расскажу о разных аспектах от
ладки памяти, такие как кучи ОС, отслеживание записи по случайным адресам и
использование бесплатных, но крайне полезных инструментов от Microsoft: Page
Heap и Application Verifier. Наконец, как я и обещал в главе 2, я опишу удивитель
ные ключи проверки ошибок в период выполнения (/RTCx) и ключ безопасности
(/GS), повышающие эффективность отладки памяти и безопасность программ,
написанных на Visual C++, на недостижимый раньше уровень.

Особенности стандартной отладочной
библиотеки C
Главное достоинство библиотеки DCRT — удивительные возможности слежения
за памятью куч. Она позволяет следить за всей памятью, выделяемой в отладоч
ных компоновках при помощи стандартных функций C/C++, таких как new, malloc
и calloc, а также за записью данных до начала выделенного блока памяти (under
write) и после его окончания (overwrite). Обо всех этих ошибках сообщает сама
DCRT посредством утверждений (assertion). DCRT также следит за утечками памя
ти, сообщая о них при завершении программы посредством функции Output7
DebugString, вывод которой появляется в окне Output отладчика. Если вы работа
ли над приложениями, использующими библиотеку Microsoft Foundation Class
(MFC), то сталкивались при завершении своих программ с отчетами об утечке
памяти; их посылала вам библиотека DCRT. MFC подключает некоторые ее функ
ции автоматически.
Другое полезное свойство библиотеки DCRT — ее подсистема сообщений (мы
с вами назвали бы ее трассировщиком), обеспечиваемая макросами _RPTn и RPTFn
и утверждениями. О поддержке библиотекой DCRT утверждений и их использо
вании я писал в главе 3. Как я говорил, утверждения DCRT очень полезны, но они
уничтожают значение последней ошибки, что может приводить к различному
поведению отладочных и заключительных компоновок. Я советую применять для
своих утверждений макрос SUPERASSERT, код которого включен в BUGSLAYERUTIL.DLL.
Еще одна приятная особенность библиотеки DCRT в том, что ее исходный код
поставляется вместе с компилятором. Список всех ее файлов см. в табл. 171. Если
при установке Microsoft Visual Studio .NET вы установили исходный код библио
теки CRT, что я очень рекомендую, то сможете найти весь исходный код библио
тек CRT и DCRT в подкаталоге \VC7\CRT\SRC.

ГЛАВА 17

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

611

Табл. 17-1. Исходные файлы стандартной отладочной библиотеки C
Исходный файл

Описание

DBGDEL.CPP

Определение глобального отладочного оператора delete.

DBGHEAP.C

Определения всех отладочных функций работы с кучами.

DBGHOOK.C

Заглушка функцииловушки выделения памяти.

DBGINT.H

Объявления внутренних данных и функций отладочной библиотеки.

DBGNEW.CPP

Определение глобального отладочного оператора new.

DBGRPT.C

Определения отладочных функций подсистемы сообщений.

CRTDBG.H

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

Стандартный вопрос отладки
Зачем мне стандартная отладочная библиотека C, если я использую
средство обнаружения ошибок наподобие BoundsChecker?
Такие инструменты обнаружения ошибок, как BoundsChecker компании
Compuware или Purify от Rational Software автоматически обрабатывают
запись данных до начала и после окончания выделенной памяти, а также
ее утечки. Если вы работаете с одним из этих средств, вам может казаться,
что использование библиотеки DCRT не стоит затрат времени и усилий.
С технической точки зрения это верно, однако, чтобы гарантировать на
хождение всех проблем с памятью, отладочную компоновку приложения
нужно всегда выполнять под управлением средства обнаружения ошибок.
Это должны делать не только вы и ваши коллеги по группе, но и, если вы
следовали моим советам, приведенным в главе 2, даже сотрудники отдела кон
троля качества. Не думаю, чтобы все люди были такими ответственными.
Библиотека DCRT подобна хорошей страховке от пожара или кражи. Все
мы надеемся, что такая страховка нам не понадобится, но порой она мо
жет спасти нас от разорения. Не упускайте ни одной возможности прове
рить данные своей программы. Библиотека DCRT не вызывает значитель
ного снижения быстродействия программ и в то же время может указать
на некоторые очень коварные ошибки. Вам следует использовать ее всегда,
даже если вы применяете все средства обнаружения ошибок в мире.

Использование стандартной отладочной
библиотеки C
Чтобы вы начали как можно раньше извлекать выгоду из слежения за памятью,
библиотеку DCRT нужно прежде всего подключить. Для этого в главный преком
пилированный заголовочный файл (или любой другой заголовочный файл, вклю
чаемый во все исходные файлы проекта) надо добавить такую строку, указав ее
перед всеми директивами #include:

#define _CRTDBG_MAP_ALLOC

612

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

После всех остальных заголовочных файлов нужно включить файл CRTDBG.H.
Благодаря определению _CRTDBG_MAP_ALLOC вызовы обычных функций выделения и
освобождения памяти будут перенаправляться их специальным версиям, записы
вающим при каждой такой операции сведения об исходном файле и номере строки.
После этого нужно включить средства проверки кучи, обеспечиваемые библио
текой DCRT. Как я уже упоминал, большинство из них по умолчанию отключено.
В документации утверждается, что они отключены для уменьшения объема кода
и повышения быстродействия программы. Конечно, для заключительных компо
новок это очень важно, но не забывайте, что назначение отладочных компоно
вок как раз в нахождении ошибок! Увеличение объема и снижение быстродей
ствия отладочных компоновок не играют большой роли. Поэтому без колебаний
включайте все средства библиотеки DCRT, которые, по вашему мнению, могут
пригодиться. Для их включения нужно передать функции _CrtSetDbgFlag набор
флагов, объединенных операцией ИЛИ (табл. 172).

Табл. 17-2. Флаги стандартной отладочнойбиблиотеки C
Флаг

Описание

_CRTDBG_ALLOC_MEM_DF

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

_CRTDBG_CHECK_ALWAYS_DF

Выполняет проверку всей памяти при каждом запросе об ее
выделении и освобождении. Установка этого флага позволя
ет обнаруживать запись данных вне блока памяти как мож
но раньше после возникновения ошибки.

_CRTDBG_CHECK_CRT_DF

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

_CRTDBG_DELAY_FREE_MEM_DF

После установки этого флага действительное освобождение
памяти не выполняется. Блоки продолжают храниться во
внутреннем списке кучи, но заполняются значениями 0xDD,
благодаря чему вы легко можете узнать освобожденную па
мять, изучая ее в отладчике. Этот флаг позволяет вам прове
рить свою программу в условиях нехватки памяти. Кроме
того, библиотека DCRT следит за тем, чтобы все ячейки
освобожденных блоков памяти оставались равными 0xDD,
что поможет вам обнаружить попытки повторного доступа
к ним. Устанавливайте этот флаг всегда, но помните, что
требования вашей программы к памяти при этом легко
могут удвоиться, потому что освобожденная память
не возвращается в кучу.

_CRTDBG_LEAK_CHECK_DF

Проверяет утечки памяти в конце программы. Установка
этого флага просто обязательна.

ГЛАВА 17

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

613

Включив в программу директивы #include и #define и вызвав _CrtSetDbgFlag, вы
получите полный доступ ко всем функциям библиотеки DCRT, что поможет кон
тролировать использование памяти и получать нужную информацию. Вы можете
вызывать эти функции в любой момент. Многие из них приспособлены для ис
пользования в утверждениях, так что вы можете свободно «рассыпать» их по сво
ему коду для раннего обнаружения проблем с памятью.

Ошибка в DCRT
Если вы следуете инструкциям предыдущего раздела, ваш исходный код будет похож
на листинг 171. В начале листинга вы видите определение _CRTDBG_MAP_ALLOC и
заголовочный файл CRTDBG.H, между которыми включаются все остальные заго
ловочные файлы. В самом начале main вызывается функция _CrtSetDbgFlag для на
стройки DCRT. Далее я выделяю три блока памяти: один при помощи malloc и два
при помощи new, — и все три случая вызывают утечку памяти.

Листинг 17-1. Подключение библиотеки DCRT и механизмов отслеживания
утечек памяти
// Эту директиву define нужно указывать до включения
// любых заголовочных файлов.
#define _CRTDBG_MAP_ALLOC
#include
#include
#include
#include
// Файл CRTDBG.H включается после всех остальных заголовочных файлов.
#include
void main ( void )
{
// Включение всех механизмов проверки кучи.
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF
|
_CRTDBG_CHECK_ALWAYS_DF
|
_CRTDBG_DELAY_FREE_MEM_DF |
_CRTDBG_LEAK_CHECK_DF
) ;
// Выделение памяти.
TCHAR * pNew = new TCHAR[ 200 ] ;
TCHAR * pNew2 = new TCHAR[ 200 ] ;
TCHAR * pMemLeak = (TCHAR*)malloc ( 100 ) ;
_tcscpy ( pNew , _T ( "New'd memory..." ) ) ;
_tcscpy ( pNew2 , _T ( "More new'd memory..." ) ) ;
_tcscpy ( pMemLeak , _T ( "Malloc'd memory..." ) ) ;
}
Так как в листинге 171 я включил проверку утечек памяти, то при выполне
нии программы вы увидите в окне Output нечто вроде:

614

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Detected memory leaks!
Dumping objects 7>
NewProblem.cpp(22) : {62} normal block at 0x002F2E58, 100 bytes long.
Data: 4E 00 65 00 77 00 27 00 64 00 20 00 6D 00 65 00
Object dump complete.
Формат вывода информации об утечках памяти очень удобен: двойной щел
чок выведенного номера строки вызовет автоматический переход к этой строке
в исходном файле. Первая утечка происходит в строке 22 файла NEWPROBLEM.CPP
(см. листинг 171), и двойной щелчок действительно переносит вас на строку,
выполняющую malloc. Однако, дважды щелкнув номер строки, указанной в спис
ке второй, вы попадете в файл CRTDBG.H (строка 692). Вы увидите вызов new, но
это определенно не мой исходный код. Так как в моей программе несколько вы
зовов new, можно сделать вывод, что все вызовы new отображаются как исходящие
из файла CRTDBG.H. Легко понять, что это не очень поможет при поиске утечек
памяти в большой программе!
Проблема заключается в объявлении new в файле CRTDBG.H:

inline void* __cdecl operator new[](size_t s)
{ return ::operator new[](s, _NORMAL_BLOCK, __FILE__, __LINE__); }
Если вы никогда не сталкивались с синтаксисом размещения (placement syntax)
оператора new, представленный фрагмент может поначалу показаться странным.
Оператор new — очень специфическая конструкция языка C++, так как он может
принимать самые разнообразные параметры. Видно, что во время вызова ::operator
new ему передаются три дополнительных параметра. Эта версия new определяется
в файле CRTDBG.H. Кажется, причина неприятностей неочевидна. Макросы __FILE__
и __LINE__ расширяются при компиляции в имя исходного файла и номер строки
в нем. Однако, как вы увидели, они расширяются не в ваши файлы и номера строк.
Проблема — в первом слове представленного фрагмента: inline. В заключитель
ных компоновках объявление функции inline означает, что компилятору реко
мендуется не вызывать функцию, а разместить в месте ее вызова сам код функ
ции. Но при этом нужно помнить, что в отладочных компоновках функции inline
не расширяются и рассматриваются как действительные функции. Соответствен
но макросы __FILE__ и __LINE__ в нашем случае расширяются в CRTDBG.H и 692.
Так как это накладывает на использование DCRT серьезные ограничения, я
должен был найти способ сделать так, чтобы все обнаруженные утечки указыва
ли на выделение памяти в истинном исходном коде. Сначала я хотел изменить
CRTDBG.H, но потом решил, что это не очень здорово: всетаки это системный
заголовочный файл. Поразмыслив, я в конце концов написал следующий макрос,
который нужно включать в прекомпилированный заголовочный файл сразу по
сле включения CRTDBG.H. Он просто преобразует любой вызов new в версию new
с синтаксисом размещения, принимающую дополнительные параметры.

ГЛАВА 17

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

615

#ifdef _DEBUG
#ifndef NEW_INLINE_WORKAROUND
#define NEW_INLINE_WORKAROUND new ( _NORMAL_BLOCK ,\
__FILE__ , __LINE__ )
#define new NEW_INLINE_WORKAROUND
#endif
#endif // _DEBUG
Наверное, внимательные читатели догадались, что в моем макросе NEW_INLINE_WOR7
KAROUND скрыта проблема: если у вас есть класс, определяющий оператор new, мой
макрос приведет к путанице объявлений. Скорее всего вам не приходится опре
делять оператор new в своих классах, но такие библиотеки, как MFC и STL, это точно
делают. Вся хитрость — в его определении только внутри прекомпилированного
заголовочного файла, во включении в этот файл только таких библиотек, как MFC
и STL, и в использовании улучшенных директив #pragma push_macro/#pragma pop_macro.
Точные рекомендации на этот счет см. в разделе «Стандартный вопрос отладки:
что включать в прекомпилированные заголовочные файлы?».
Если в вашу программу входят классы, определяющие оператор new, вы тоже
можете использовать макрос NEW_INLINE_WORKAROUND, если согласитесь добавить в класс
несколько директив и немного кода. В следующем фрагменте я привожу упрощен
ный пример класса, содержащего все необходимое для отличной работы NEW_IN7
LINE_WORKAROUND:

// Сохранение определения оператора new в стеке макроса.
#pragma push_macro ( "new" )
// Отмена определения new нужна для правильного объявления класса.
#ifdef new
#undef new
#endif
class TestClass
{
public :
// Синтаксис размещения new с прототипом, нужным
// для записи правильной информации об исходном
// файле и номере строки при выделении памяти.
#ifdef _DEBUG
// iSize
7 размер выделяемой памяти.
// iBlockType 7 тип блока DCRT.
// lpszFileName 7 имя исходного файла.
// nLine
7 номер строки в исходном файле.
static void * operator new ( size_t nSize
int
iBlockType
char * lpszFileName
int
nLine
{
// Любой нужный вам код.

,
,
,
)

// Действительное выделение памяти при помощи _malloc_dbg

616

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// и передача всех параметров для записи места этой операции.
return ( _malloc_dbg ( nSize
,
iBlockType
,
lpszFileName ,
nLine
) ) ;
}
#endif // _DEBUG
} ;
// Восстановление сохраненного значения
// макроса new (т. е. NEW_INLINE_WORKAROUND).
#pragma pop_macro ( "new" )
Своим существованием такое решение обязано новым директивам #pragma
push_macro и #pragma pop_macro, которые сохраняют текущее определение макроса
во внутреннем стеке компилятора и восстанавливают сохраненное значение со
ответственно. Вы должны будете заключать в них любой класс, в котором выпол
няется перегрузка оператора new, потому что директивы #pragma не могут быть
автоматизированы при помощи макросов. Дополнительный оператор new будет
вызываться макросом NEW_INLINE_WORKAROUND для изменения фактического выделе
ния памяти. Применять дополнительный оператор new немного неудобно, но так
вы хоть получите полные отчеты обо всех утечках памяти. Чтобы увидеть, как все
это работает, изучите проект FixedNewProblem на CD.

Стандартный вопрос отладки
Что включать в прекомпилированные заголовочные файлы?
Как я упоминал при обсуждении решения ошибки в DCRT, для получения
сообщений об утечках памяти, выделяемой при помощи оператора new, и
отображения верной информации о номере строки исходного кода, необ
ходимо наличие правильных прекомпилированных заголовочных файлов.
К тому же это не только облегчит отладку памяти, но и ускорит компиля
цию программ. Прекомпилированный заголовочный файл — это по сути
записанное на диск дерево грамматического разбора для файлов, указан
ных в файле .H (традиционно называемом STDAFX.H). Поэтому вы компи
лируете его только раз, а не при каждой компиляции файла .C/.CPP.
Вот правила создания прекомпилированного заголовочного файла.
1. Включайте в него все заголовочные файлы библиотек CRT/компилятора.
2. Если в директиве #include вы заключаете имя файла в угловые скобки,
имена ваших заголовочных файлов должны указываться в кавычках.
3. Включайте в него все заголовочные файлы сторонних фирм, такие как
файлы для BUGSLAYERUTIL.DLL.
4. Включайте в него все заголовочные файлы вашего проекта, которые не
изменялись более одногодвух месяцев.

ГЛАВА 17

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

617

Полезные функции DCRT
Одна из наиболее полезных функций библиотеки DCRT — _CrtCheckMemory — про
сматривает всю выделенную вами память и проверяет, не выполняете ли вы за
пись данных вне выделенных блоков и не используете ли вы ранее освобожден
ные блоки. Даже одна эта функция оправдывает использование всей библиотеки
DCRT. Один из великолепных методов обнаружения проблем с памятью состоит
в «разбрасывании» вызовов ASSERT ( _CrtCheckMemory ( ) ) ; по всему коду програм
мы. Так вы сможете находить ошибки записи данных вне выделенных блоков
памяти максимально близко к месту их возникновения.
Другой набор функций позволяет с легкостью проверять корректность любо
го блока памяти. Отладочные функции _CrtIsValidHeapPointer, _CrtIsMemoryBlock и
_CrtIsValidPointer прекрасно подходят для проверки параметров. Эти функции вместе
с _CrtCheckMemory обеспечивают великолепные возможности проверки памяти.
Еще один полезный набор функций библиотеки DCRT включает функции изу
чения состояния памяти: _CrtMemCheckpoint, _CrtMemDifference и _CrtMemDumpStatis7
tics. Благодаря им вы можете легко выполнять сравнение кучи до и после вызова
какойнибудь функции, определяя момент, когда чтото начинает работать не так.
Скажем, если вы используете стандартную библиотеку в группе, вы можете запи
сывать состояние кучи до и после вызовов библиотечных функций для обнару
жения утечек памяти и определения объема памяти, необходимого для конкрет
ной операции.
Сахарной глазурью на пирожном проверки памяти является возможность уста
новки ловушки, благодаря которой вы можете узнавать про каждый вызов функ
ций выделения и освобождения памяти. Если ловушка выделения памяти возвра
щает TRUE, выделение памяти можно продолжить, если же FALSE —выделение па
мяти завершается неудачей. Когда я впервые обнаружил эту функциональность, я
сразу же понял, что, приложив небольшие усилия, я получу инструменты тести
рования кода в понастоящему сложных граничных условиях, которые иначе было
бы очень сложно воспроизвести. Результатом этого является модуль MemStress из
состава BUGSLAYERUTIL.DLL. Он дает вам возможность принудительно вызывать
неудачи выделения памяти в ваших программах, про что я расскажу ниже.
И, наконец, вишенка на сахарной глазури: библиотека DCRT позволяет уста
навливать ловушку для функций записи дампа памяти и перечислять клиентские
блоки (выделенную вами память). Вы можете заменить функции дампа памяти,
используемые по умолчанию, собственными функциями, знающими о ваших дан
ных все. После этого вы сможете получать не таинственный дамп памяти по умол
чанию (который не только сложен в понимании, но и менее полезен), а точное
содержание блока памяти, отформатированное так, как вам угодно. MFC предо
ставляет для этого функцию Dump, но она работает только с классами, унаследо
ванными от CObject. Я уверен, что если мы с вами в чемто похожи, вы также не
можете смириться с написанием программ только при помощи MFC и хотели бы
получить более общие функции создания дампов памяти, охватывающие различ
ные типы кода.
Как можно догадаться, перечисление клиентских блоков позволяет перечис
лять выделенные вами блоки памяти. Опираясь на эту прекрасную возможность,

618

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

можно создать некоторые очень интересные утилиты. Так, в функциях MemDumper
Validator из библиотеки BUGSLAYERUTIL.DLL я при перечислении клиентских
блоков вызываю ловушки записи дампов, чтобы можно было создавать дампы и
выполнять проверку многих типов выделенной памяти за одно действие. Это очень
мощное средство, позволяющее реализовать глубокую проверку памяти, а не только
проверку записи данных вне выделенного блока. Под глубокой проверкой я по
нимаю алгоритм, который знает формат данных блока памяти и проверяет кор
ректность всех элементов блока с учетом их формата.

Выбор правильной стандартной отладочной
библиотеки C для вашего приложения
Некоторое замешательство по поводу использования библиотек CRT при созда
нии программ для Microsoft Windows связано с выбором правильной библиоте
ки. Существует шесть версий библиотеки CRT, подразделяющихся на две основ
ных категории: отладочные (DCRT) и заключительные (CRT). В каждой категории
имеется однопоточная статическая библиотека, многопоточная статическая биб
лиотека и многопоточная DLL.
При работе со статическими версиями библиотек CRT библиотечные функции
компонуются прямо в вашу программу; именно эти версии используются по умол
чанию для приложений, создаваемых без помощи мастеров MFC. Преимущество
этого подхода в том, что вам не придется поставлять вместе со своей програм
мой динамическую библиотеку CRT, а недостаток — в огромном увеличении объема
двоичных файлов и рабочего набора. Названия двух вариантов статической биб
лиотеки CRT — однопоточной и многопоточной — говорят сами за себя. Если вы
создаете DLL и хотите задействовать статическую библиотеку CRT, вам следует
выполнять компоновку только с ее многопоточной версией, иначе многопоточ
ные приложения не смогут работать с вашей DLL, так как однопоточные стати
ческие библиотеки CRT небезопасны с точки зрения потоков.
DLLверсии библиотек CRT — MSVCRT(D).DLL — позволяют импортировать биб
лиотечные функции CRT. Благодаря этим DLL вы можете уменьшить размер сво
их двоичных файлов, а значит, и рабочий набор программы. Поскольку другие
приложения будут загружать одни и те же DLL, ОС сможет предоставить несколь
ким процессам совместный доступ к страницам кода DLL, и вся система станет
работать быстрее. Однако этот вариант имеет и недостаток: вполне возможно, что
вам придется распространять еще одну DLL вместе со своей программой.
Чрезвычайно важно, чтобы вы выбрали какуюто одну версию библиотеки CRT
для всех двоичных файлов, загружаемых в адресное пространство своей основ
ной программы. Если некоторые ваши DLL будут обращаться к статической биб
лиотеке CRT, а другие — к динамической, вы не только израсходуете дополнитель
ное адресное пространство изза дублирования кода, но и создадите плодород
ную почву для одной из самых коварных ошибок памяти, на отладку которой могут
потребоваться месяцы. При выделении памяти в куче из одной DLL и освобожде
нии этой памяти во второй DLL, использующей другую версию библиотеки CRT,
ваша программа сможет с легкостью потерпеть крах, потому что освобождающая
память DLL не будет знать, откуда взялась выделенная память. Не относитесь к куче

ГЛАВА 17

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

619

с пренебрежением: одновременное выполнение разных версий библиотеки CRT
влечет за собой различную обработку памяти куч.
Я всегда использую динамические версии библиотек CRT и советую вам делать
то же самое. Выгода от уменьшения рабочего набора и сокращения основных
двоичных файлов перевешивает все прочие соображения. Очень редко — скажем,
при разработке игр, когда я уверен в том, что многопоточность мне не понадо
бится и когда чрезвычайную важность приобретает быстродействие, — я могу
рассмотреть применение однопоточных статических версий для избежания за
трат на механизмы многопоточной блокировки.
Для работы с динамическими версиями библиотек CRT я создал библиотеку
BUGSLAYERUTIL.DLL. Код расширений MemDumperValidator и MemStress, про ко
торые я рассказываю в этой главе, также хранится в BUGSLAYERUTIL.DLL. Эти модули
расширения тоже ожидают, что вы будете работать с их DLLверсиями. Однако,
если вы захотите использовать их в своем приложении не в виде DLL, вы можете
отобрать исходные файлы MEMDUMPERVALIDATOR.CPP, MEMDUMPERVALIDATOR.H,
MEMSTRESS.CPP, MEMSTRESSCONSTANTS.H и MEMSTRESS.H, изменить указанный
метод компоновки функций и включить их в свое приложение.
И еще одна деталь — она касается использования BUGSLAYERUTIL.DLL. В зави
симости от того, как вы выделяете память, вы можете столкнуться с замедлением
работы своей программы. Разрабатывая расширение MemDumperValidator, я хо
тел обеспечить всю полноту отслеживания и проверки памяти, для чего включил
в библиотеке DCRT все соответствующие флаги, в том числе _CRTDBG_CHECK_ALWAYS_DF,
который приказывает библиотеке DCRT просматривать и проверять все фрагменты
памяти кучи при каждом выделении и освобождении памяти. Если вы выделяете
в своей программе тысячи небольших блоков памяти, задержка будет очевидной,
однако это ясно укажет вам на желательность изменения алгоритма обработки
данных. Большое число выделений небольших фрагментов памяти плохо сказы
вается на быстродействии и требует исправления. Если вы не сможете изменить
код, что ж, отключите этот флаг, вызвав функцию _CrtSetDbgFlag.

Использование MemDumperValidator
Расширение MemDumperValidator здорово упрощает отладку памяти. Библиотека
DCRT по умолчанию сообщает об утечках памяти и записи данных вне выделен
ных блоков. Оба этих сообщения могут пригодиться, но, когда они выглядят сле
дующим образом, тип «вытекшей» памяти определить очень сложно:

Detected memory leaks
Dumping objects 7>
TestProc.cpp(104) : {596} normal block at 0x008CD5B0,
24 bytes long.
Data: < k
w k > 90 6B 8C 00 B0 DD 8C 00 00 00 80 77 90 6B 8C 00
Object dump complete.
Как я уже говорил, гораздо лучше было бы иметь дополнительную информа
цию — скажем, глубокая проверка памяти помогает обнаружить запись данных
по случайным адресам, что очень сложно в противном случае. Именно такую более
подробную отладочную информацию и предоставляет вам MemDumperValidator

620

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

в своих отчетах об утечках памяти, поддерживая к тому же ряд дополнительных
методов проверки памяти. А чем больше информации вы будете иметь при отладке,
тем быстрее вы ее выполните.
MemDumperValidator использует идентификаторы блоков памяти библиотеки
DCRT, позволяющие связать тип блока с набором функций, которым известно его
содержание. Всем блокам, выделяемым библиотекой DCRT, присваиваются иден
тификаторы (табл. 173). Тип блока передается как параметр в функци выделения
памяти библиотеки DCRT: _nh_malloc_dbg (new), _malloc_dbg (malloc), _calloc_dbg (calloc)
и _realloc_dbg (realloc).

Табл. 17-3. Идентификаторы блоков памяти
Идентификатор
блока

Описание

_NORMAL_BLOCK

Обычный вызов new, malloc или calloc приводит к созданию нор
мального блока. После определения _CRTDBG_MAP_ALLOC вся память в
куче выделяется по умолчанию в форме нормальных блоков, кото
рые при этом ассоциируются с номером выделившей память стро
ки и ее исходным файлом.

_CRT_BLOCK

Блоки памяти, выделяемые внутри многих функцияй стандартной
библиотеки, отмечаются как блоки CRT, чтобы их можно было об
рабатывать отдельно. Это позволяет не проверять их при поиске
утечек и других операциях проверки памяти. Ваше приложение
никогда не должно выделять, перераспределять или освобождать
блок типа CRT.

_CLIENT_BLOCK

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

define CLIENT_BLOCK_VALUE(x) \
(_CLIENT_BLOCK|(xpfnDump = pfnD ;
((LPBSMDVINFO)lpBSMDVINFO)7>pfnValidate = pfnV ;
AddClientDV ( lpBSMDVINFO ) ;
}

\
\
\
\
\
\
\
\

// Макросы, сответствующие функциям выделения памяти в стиле C.
// С ними будет легче работать, если создать для них оболочки,
// позволяющие не запоминать, какие структуры BSMDVINFO
// передавать в каждую функцию.
#define MEMDEBUG_MALLOC(lpBSMDVINFO , nSize)
\
_malloc_dbg ( nSize
,
\
((LPBSMDVINFO)lpBSMDVINFO)7>dwValue , \
__FILE__
,
\
__LINE__
)
#define MEMDEBUG_REALLOC(lpBSMDVINFO , pBlock , nSize)
\
_realloc_dbg( pBlock
,
\
nSize
,
\
((LPBSMDVINFO)lpBSMDVINFO)7>dwValue , \
__FILE__
,
\
__LINE__
)
#define MEMDEBUG_EXPAND(lpBSMDVINFO , pBlock , nSize )
\
_expand_dbg( pBlock
,
\
nSize
,
\
((LPBSMDVINFO)lpBSMDVINFO)7>dwValue , \
__FILE__
,
\
__LINE__
)
#define MEMDEBUG_FREE(lpBSMDVINFO , pBlock)
\
_free_dbg ( pBlock
,
\
((LPBSMDVINFO)lpBSMDVINFO)7>dwValue )
#define MEMDEBUG_MSIZE(lpBSMDVINFO , pBlock) \
_msize_dbg ( pBlock , ((LPBSMDVINFO)lpBSMDVINFO)7>dwValue )
// Макрос для вызова функции ValidateAllBlocks.
см. след. стр.

626

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

#define VALIDATEALLBLOCKS(x)
#else

ValidateAllBlocks ( x )

// Макрос _DEBUG не определен.

#ifdef __cplusplus
#define DECLARE_MEMDEBUG(classname)
#define IMPLEMENT_MEMDEBUG(classname)
#define MEMDEBUG_NEW new
#endif
// __cplusplus
#define INITIALIZE_MEMDEBUG(lpBSMDVINFO , pfnD , pfnV )
#define MEMDEBUG_MALLOC(lpBSMDVINFO , nSize) \
malloc ( nSize )
#define MEMDEBUG_REALLOC(lpBSMDVINFO , pBlock , nSize) \
realloc ( pBlock , nSize )
#define MEMDEBUG_EXPAND(lpBSMDVINFO , pBlock , nSize) \
_expand ( pBlock , nSize )
#define MEMDEBUG_FREE(lpBSMDVINFO , pBlock) \
free ( pBlock )
#define MEMDEBUG_MSIZE(lpBSMDVINFO , pBlock) \
_msize ( pBlock )
#define VALIDATEALLBLOCKS(x)
#endif

// _DEBUG

#ifdef __cplusplus
}
#endif
// __cplusplus
#endif

// _MEMDUMPERVALIDATOR_H

Использование MemDumperValidator в программах C++
К счастью, настроить класс C++ для его обработки MemDumperValidator’ом довольно
просто. Для этого нужно добавить перед объявлением нужного класса директиву
#pragma push_macro ("new") и отменить определение new. После объявления надо
восстановить определение new, применив директиву #pragma pop_macro ("new").
В объявлении класса C++ просто укажите макрос DECLARE_MEMDEBUG с именем клас
са в качестве параметра. Этот макрос чемто похож на «магические» макросы MFC:
он тоже расширяется в пару объявлений данных и метода. Изучая листинг 172,
вы заметите шесть встраиваемых функций для new и delete, обрабатывающих любой
тип вызова этих операторов с синтаксисом размещения. Если какойто из этих
операторов определен в вашем классе, извлеките код из расширенных операто
ров и поместите его в операторах вашего класса.
В файле реализации класса C++ нужно указать макрос IMPLEMENT_MEMDEBUG с именем
класса в качестве параметра. Этот макрос подготавливает статическую перемен
ную для вашего класса. Макросы DECLARE_MEMDEBUG и IMPLEMENT_MEMDEBUG расширяются

ГЛАВА 17

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

627

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

static void ClassDumper ( const void * pData ) ;
static void ClassValidator ( const void * pData,
const void * pContext ) ;
Параметр pData, одинаковый для обоих методов, — это указатель на блок па
мяти объекта. Для получения готового к использованию указателя вам нужно только
привести pData к типу указателя на ваш класс. Что бы вы ни делали при записи
дампа или проверке памяти, рассматривайте значение pData как супертолькодля
чтения, или вы с легкостью внесете в свой код столько ошибок, сколько собира
лись предотвратить. Второй параметр метода ClassValidator — pContext — это па
раметр контекста, передаваемый вами в функцию ValidateAllBlocks. Подробнее о
функции ValidateAllBlocks см. раздел «Глубокая проверка».
Что до реализации метода ClassDumper, то я могу дать лишь два совета. Вопер
вых, старайтесь использовать макросы _RPTn и _RPTFn библиотеки DCRT, чтобы ваш
форматированный вывод записывался в дамп там же, где и остальная информа
ция библиотеки DCRT. Вовторых, завершайте свой вывод комбинацией «возврат
каретки/перевод строки» (CR/LF), потому что макросы библиотеки DCRT не вы
полняют форматирования.
Подключение функций записи дампа и проверки памяти для класса C++ кажется
почти тривиальным. А структуры данных C, которые вам также хотелось бы запи
сывать в понятные и удобные дампы? Увы, их обработка требует большей работы.

Использование MemDumperValidator в программах C
Вы недоумеваете, зачем беспокоиться о поддержке C? Все просто: на этом языке
написана масса используемых мной и вами программ. Как хотите, но некоторые
из этих приложений и модулей тоже работают с памятью.
Чтобы использовать MemDumperValidator в программе C, нужно сначала объя
вить структуру BSMDVINFO для каждого типа памяти, который вы хотите проверять
и записывать в дамп. Макросы C++ объявляют методы записи дампа и проверки
памяти автоматически, однако в C коечто придется сделать самостоятельно. По
мните: все макросы, про которые я здесь говорю, должны получать указатель на
специфическую структуру BSMDVINFO.
Прототипы функций записи дампа и проверки памяти C аналогичны прото
типам методов C++ — нет только ключевого слова static. Как и при объявлении
уникальных структур BSMDVINFO для блоков памяти, реализацию всех функций за
писи дампа и проверки C можно поместить в один файл.
Прежде чем вы начнете выделять, записывать и проверять память в програм
мах C, вы должны сообщить расширению MemDumperValidator о подтипе клиен
тского блока и функциях записи дампа и проверки памяти. Эта информация пе

628

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

редается расширению MemDumperValidator при помощи макроса INITIALIZE_MEM7
DEBUG, который в качестве параметров принимает указатель на специфическую
структуру BSMDVINFO и указатели на функции записи дампа и проверки. Вам нужно
будет выполнять этот макрос перед выделением любого блока памяти соответству
ющего типа.
Наконец (и в этом смысле работать с памятью в C++ гораздо легче, чем в C),
для выделения, освобождения, перераспределения, расширения или получения
размера блока вы должны использовать набор макросов, передающих значение
блока нужной функции работы с памятью. Так, если ваша структура BSMDVINFO на
зывается stdvBlockInfo, нужно выделить блоки памяти в программе C:

MEMDEBUG_MALLOC ( &stdvBlockInfo , sizeof ( x ) ) ;
В конце листинга 172 содержатся все макросы для функций работы с памя
тью языка C. Можно запомнить структуры BSMDVINFO для каждого типа выделения
памяти, но это непрактично, поэтому для обработки структур BSMDVINFO лучше
написать макросыоболочки — тогда вам нужно передавать в свои макросыобо
лочки только обычные параметры функций работы с памятью.

Глубокая проверка
Польза записи дампов при помощи MemDumperValidator несомненна, тогда как
назначение метода проверки не столь очевидно, пусть даже он позволяет выпол
нять глубокую проверку блока памяти. Зачастую функция проверки может быть
даже пустой, если класс содержит только несколько переменныхстрок. И все же
функция проверки памяти все равно может оказаться бесценной, предоставляя
великолепные отладочные возможности. Одна из причин того, что я начал исполь
зовать глубокую проверку, заключалась в получении второго уровня проверки
данных для набора разработанных мной базовых классов. Хотя функция провер
ки не должна заменять вам старую добрую проверку параметров и вводимой ин
формации, она может предоставить дополнительное подтверждение корректно
сти данных. На основе глубокой проверки можно создать и вторую линию обо
роны против записи информации по случайным адресам памяти.
Самый очевидный способ использования функции проверки — контроль слож
ных структур данных после проведения над ними какихлибо операций. Я, напрмер,
както попал в сложную ситуацию, когда изза ограничений памяти мне нужно
было сделать так, чтобы две отдельных ссылающихся на себя структуры данных
работали с одними и теми же объектами, находящимися в выделенной памяти.
После заполнения структур большим набором данных я изучил при помощи фун
кции проверки памяти отдельные блоки кучи и убедился в правильных значени
ях ссылок. Конечно, я мог написать код просмотра каждой структуры данных, но
я знал, что любой написанный мной код будет потенциальным источником оши
бок. Функция проверки памяти позволила мне применить для изучения выделен
ных блоков уже протестированный код и проверить структуры данных с разных
позиций, так как блоки памяти располагались в порядке выделения, а не в отсор
тированном порядке.
Хотя в C настройка выделения памяти сложнее, чем в C++, применение функ
ции проверки памяти в обоих языках одинаково. Для этого нужно только вызвать

ГЛАВА 17

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

629

макрос VALIDATEALLBLOCKS. Он расширяется в отладочных компоновках в вызов фун
кции ValidateAllBlocks. Параметром макроса может быть любое значение, кото
рое вы хотите передать зарегистрированным функциям проверки памяти. Рань
ше я указывал через этот параметр глубину выполняемой функцией проверки.
Помните: ValidateAllBlocks передает это значение каждой зарегистрированной
функции проверки, поэтому вам нужно согласовать его между всеми членами вашей
группы.
Чтобы увидеть функции расширения MemDumperValidator в действии, изучи
те программу Dump (листинг 173; каталог BUGSLAYERUTIL\TESTS\DUMP на CD).
Dump демонстрирует все действия, нужные для использования расширения. Хотя
я не привел соответствующего кода, расширение MemDumperValidator хорошо
работает с MFC, так как MFC вызывает любую зарегистрированную клиентскую
функциюловушку записи дампа. MemDumperValidator позволяет вам получить в
свое распоряжение лучшее обоих миров!

Листинг 17-3.

DUMP.CPP

/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright © 199772003 John Robbins — All rights reserved.
——————————————————————————————————————————————————————————————————————*/
#include
#include
#include
#include
#include
#include "BugslayerUtil.h"
#pragma push_macro ( "new" )
#ifdef new
#undef new
#endif
class TestClass
{
public:
TestClass ( void )
{
_tcscpy ( m_szData , _T ( "TestClass constructor data!" ) ) ;
}
~TestClass ( void )
{
m_szData[ 0 ] = _T ( '\0' ) ;
}
// Объявления механизмов отладки памяти для классов C++.
DECLARE_MEMDEBUG ( TestClass ) ;
private
:
TCHAR m_szData[ 100 ] ;
см. след. стр.

630

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

} ;
#pragma pop_macro ( "new" )
// Этот макрос создает статическую структуру BSMDVINFO.
IMPLEMENT_MEMDEBUG ( TestClass ) ;
// Методы, которые вы должны реализовать
// для записи дампов и проверки памяти.
#ifdef _DEBUG
void TestClass::ClassDumper ( const void * pData )
{
TestClass * pClass = (TestClass*)pData ;
_RPT1 ( _CRT_WARN
,
" TestClass::ClassDumper : %S\n" ,
pClass7>m_szData
) ;
}
void TestClass::ClassValidator ( const void * pData ,
const void *
)
{
// Выполняйте здесь проверку данных.
TestClass * pClass = (TestClass*)pData ;
_RPT1 ( _CRT_WARN
,
" TestClass::ClassValidator : %S\n" ,
pClass7>m_szData
) ;
}
#endif
typedef struct tag_SimpleStruct
{
TCHAR szName[ 256 ] ;
TCHAR szRank[ 256 ] ;
} SimpleStruct ;
// Функции записи дампа и проверки памяти для простых строк.
void DumperOne ( const void * pData )
{
_RPT1 ( _CRT_WARN , " Data is : %S\n" , pData ) ;
}
void ValidatorOne ( const void * pData , const void * pContext )
{
// Выполняйте здесь проверку данных строки.
_RPT2 ( _CRT_WARN
,
" Validator called with : %s : 0x%08X\n" ,
pData
,
pContext
) ;
}
// Функции записи дампа и проверки памяти для структуры.
void DumperTwo ( const void * pData )

ГЛАВА 17

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

631

{
_RPT2 ( _CRT_WARN
,
" Data is Name : %S\n"
"
Rank : %S\n"
,
((SimpleStruct*)pData)7>szName ,
((SimpleStruct*)pData)7>szRank ) ;
}
void ValidatorTwo ( const void * pData , const void * /*pContext*/ )
{
// Выполняйте здесь проверку полей структур.
_RPT2 ( _CRT_WARN
,
" Validator called with :\n"
"
Data is Name : %s\n"
"
Rank : %s\n"
,
((SimpleStruct*)pData)7>szName ,
((SimpleStruct*)pData)7>szRank ) ;
}
// К сожалению, функции C нуждаются в собственных структурах
// BSMDVINFO. При работе над реальными программами вам следует
// определять эти структуры как внешние ссылки и создавать
// для макросов MEMDEBUG собственные макросы7оболочки.
static BSMDVINFO g_dvOne ;
static BSMDVINFO g_dvTwo ;
void main ( void )
{
_tprintf ( _T ( "At start of main\n" ) ) ;
// Инициализация механизмов отладки памяти
INITIALIZE_MEMDEBUG ( &g_dvOne , DumperOne
// Инициализация механизмов отладки памяти
INITIALIZE_MEMDEBUG ( &g_dvTwo , DumperTwo

для типа 1.
, ValidatorOne ) ;
для типа 2.
, ValidatorTwo ) ;

// Выделение памяти для объекта C++ при помощи
// оператора new, определенного в макросе MEMDEBUG.
TestClass * pstClass ;
pstClass = new TestClass ( ) ;
// Выделение памяти для двух типов C.
TCHAR * p = (TCHAR*)MEMDEBUG_MALLOC ( &g_dvOne , 20 ) ;
_tcscpy ( p , _T ( "VC VC" ) ) ;
SimpleStruct * pSt =
(SimpleStruct*)MEMDEBUG_MALLOC ( &g_dvTwo ,
sizeof ( SimpleStruct ) ) ;
_tcscpy ( pSt7>szName , _T ( "Pam" ) ) ;
_tcscpy ( pSt7>szRank , _T ( "CINC" ) ) ;
см. след. стр.

632

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// Проверка всех блоков списка.
VALIDATEALLBLOCKS ( NULL ) ;
_tprintf ( _T ( "At end of main\n" ) ) ;
// Дамп каждого блока создается при проверке утечек памяти.
}

Реализация MemDumperValidator
Реализация функций MemDumperValidator оказалась довольно простой. Первая
неожиданная проблема, с которой я должен был справиться, была в том, что в
библиотеке DCRT не был документирован способ получения значений блоков
памяти функциямиловушками. Функциямловушкам передается только указатель
на данные пользователя, а не на весь блок памяти, выделяемый библиотекой DCRT.
К счастью, в исходном коде библиотеки DCRT я нашел точный механизм выделе
ния блоков памяти. Каждый блок памяти выделяется как структура _CrtMemBlock7
Header, определенная в файле DBGINT.H.
В файле DBGINT.H есть еще макросы доступа к _CrtMemBlockHeader через указа
тель на данные пользователя и доступа к данным пользователя через указатель
_CrtMemBlockHeader. Чтобы получить эту информацию, я скопировал структуру
_CrtMemBlockHeader и макросы в заголовочный файл CRTDBG_INTERNALS.H (листинг
174). Создание копии определения структуры — не лучший метод, так как опре
деление может измениться, но тут все в порядке, поскольку структура _CrtMemBlock7
Header не изменялась в библиотеке DCRT, начиная с Visual C++ 4, и все же это не
значит, что она не изменится в будущих версиях Visual C++. Если вы собираетесь
применять MemDumperValidator, вы должны будете следить за появлением всех
пакетов обновлений и основных версий компилятора и проверять, не изменились
ли в них внутренние структуры данных.

Листинг 17-4.

CRTDBG_INTERNALS.H

/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright (c) 199772003 John Robbins — All rights reserved.
——————————————————————————————————————————————————————————————————————*/
#ifndef _CRTDBG_INTERNALS_H
#define _CRTDBG_INTERNALS_H
#define nNoMansLandSize 4
typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader * pBlockHeaderNext
struct _CrtMemBlockHeader * pBlockHeaderPrev
char *
szFileName
int
nLine

;
;
;
;

ГЛАВА 17

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

633

size_t
nDataSize
;
int
nBlockUse
;
long
lRequest
;
unsigned char
gap[nNoMansLandSize]
;
/* после чего располагаются:
* unsigned char
data[nDataSize];
* unsigned char
anotherGap[nNoMansLandSize];
*/
} _CrtMemBlockHeader;
#define pbData(pblock) ((unsigned char *) \
((_CrtMemBlockHeader *)pblock + 1))
#define pHdr(pbData) (((_CrtMemBlockHeader *)pbData)71)
#endif

// _CRTDBG_INTERNALS_H

Если вам удобнее работать с DBGINT.H напрямую, можете заменить определе
ние структуры в файле CRTDBG_INTERNALS.H директивой #include DBGINT.H. При
этом вам понадобится добавить выражение «$(VCInstallDir)VC7\CRT\SRC» в систем
ную переменную среды INCLUDE и список включаемых файлов, для доступа к ко
торому нужно открыть диалоговое окно Options (свойства), папку Projects (про
екты) и выбрать страницу свойств VC++ Directories (каталоги VC++). Не все про
граммисты устанавливают исходный код библиотеки CRT, хотя делать это следо
вало бы, поэтому я решил включить определение структуры непосредственно.
_CrtMemBlockHeader также позволяет извлечь более подробную информацию из
структур _CrtMemState, заполняемых функцией _CrtMemCheckpoint, потому что пер
вый элемент в _CrtMemState — указатель на _CrtMemBlockHeader. Надеюсь, в следую
щую версию библиотеки DCRT войдут настоящие функции доступа к информа
ции о блоке памяти.
Просматривая исходный код в файле MEMDUMPERVALIDATOR.CPP из проекта
BUGSLAYERUTIL.DLL (он находится на CD), вы заметите, что для внутреннего управ
ления памятью я использовал простые APIфункции семейства HeapCreate. Я сде
лал это, поскольку функции создания дампа и функцииловушки, применяемые
для работы с библиотекой DCRT, были бы реентерабельными при использовании
функций стандартной библиотеки. Заметьте, что я не имею в виду многопоточ
ную реентерабельность. Если бы моя ловушка выделяла память при помощи malloc,
она была бы реентерабельной, потому что ловушки вызываются при каждом вы
делении памяти.

Инициализация и завершение в программах C++
Завершив реализацию MemDumperValidator и начав его тестирование, я с удов
летворением отметил, что все работает так, как было запланировано. Однако при
рассмотрении всех способов, которыми программа может выделять память в куче,
я покрылся холодным потом. При выделении памяти статическими конструкто
рами могли возникнуть проблемы. Взглянув на первоначальный код MemDumper
Validator, я обнаружил серьезный пробел в своей логике.

634

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Иногда, хоть и не очень часто, память выделяется до достижения точки входа
в программу. Поэтому мне нужно было гарантировать, что нужные флаги устанав
ливаются функцией _CrtSetDbgFlag до любого выделения памяти.
Работая над MemDumperValidator, я ни в коем случае не хотел, чтобы вы вы
зывали до использования библиотеки некоторую функцию инициализации. Нам
хватает проблем со структурами BSMDVINFO при программировании на C. Я хотел
сделать MemDumperValidator как можно более автоматизированным, чтобы рабо
тать с ним было удобно большинству программистов.
К моей радости, замешательство не продлилось слишком долго, потому что я
вспомнил о директиве #pragma init_seg, благодаря которой можно управлять по
рядком инициализации и уничтожения статических значений. Эта директива может
принимать значения compiler, lib, user, section name и funcname. Важными являются
первые три.
Значение compiler зарезервировано для компиляторов Microsoft; все объекты
этой группы создаются первыми, а уничтожаются последними. Объекты, отмечен
ные как lib, создаются во вторую очередь, а уничтожаются предпоследними. На
конец, отмеченные как user создаются последними, а уничтожаются первыми.
Так как код MemDumperValidator должен инициализироваться до вашего кода,
я мог просто указать lib в директиве #pragma init_seg и покончить со всем этим.
Однако при создании своих библиотек вы также отмечаете их как сегменты lib
(и правильно), поэтому мне нужен был другой способ инициализации своего кода
в первую очередь. Чтобы справиться с этим непредвиденным обстоятельством, я
указываю в директиве #pragma init_seg значение compiler. Хотя при инициализа
ции сегментов всегда нужно следовать правилам, применение в отладочном коде
значения compiler вполне безопасно.
Описанная идея инициализации работает только в коде C++, поэтому в Mem
DumperValidator входит специальный статический класс AutoMatic, который про
сто вызывает функцию _CrtSetDbgFlag. Я вынужден был пойти на все это, так как
это единственный способ установки флагов DCRT до инициализации любых других
библиотек. Кроме того, как вы увидите ниже, для преодоления некоторых огра
ничений проверки утечек памяти, свойственнных библиотеке DCRT, я должен был
реализовать коекакие специфические действия в деструкторе класса. Пусть Mem
DumperValidator имеет интерфейс C, но я все равно воспользовался преимущества
ми C++ для инициализации этого расширения и своевременного приведения его
в рабочее состояние.

И куда же подевались все сообщения об утечках памяти?
Наконец, я справился со всеми проблемами инициализации и заставил MemDum
perValidator работать. Я был доволен всем за одним исключением: когда програм
ма, вызывавшая утечку памяти, завершала работу, я не видел красиво отформати
рованных данных, выводимых моими функциями записи дампов. Вместо этого
отображались стандартные старые дампы библиотеки DCRT. Я отследил «пропав
шие» отчеты об утечках памяти и с удивлением обнаружил, что функции завер
шения библиотеки DCRT вызывали _CrtSetDumpClient с параметром NULL, аннули
руя, таким образом, перед вызовом _CrtDumpMemoryLeaks мою ловушку записи дам

ГЛАВА 17

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

635

пов. Я огорчился, но скоро понял, что завершающую проверку утечек памяти я дол
жен был выполнять сам. Подходящее место для этого у меня уже имелось.
Выше я говорил, что для инициализации класса AutoMatic до вашего кода и вызова
его деструктора после вашего кода я использовал директиву #pragma init_seg(com7
piler), поэтому мне нужно было просто проверить в деструкторе утечку памяти
и отключить после этого флаг _CRTDBG_LEAK_CHECK_DF, чтобы библиотека DCRT не
генерировала собственный отчет. Этот подход имеет единственный недостаток:
при компоновке программы с ключом /NODEFAULTLIB вы должны гарантировать, что
выбранная вами библиотека CRT компонуется раньшеBUGSLAYERUTIL.LIB. Биб
лиотеки CRT не подчиняются директиве #pragma init_seg(compiler), вследствие чего
нет никакой гарантии, что данные BUGSLAYERUTIL.LIB будут инициализировать
ся первыми и уничтожаться последними, а значит, вы сами должны позаботиться
о правильном порядке компоновки.
Очищение всех установленных ловушек записи дампа библиотекой DCRT не
лишено смысла. Если бы ваша ловушка записи дампа использовала какието функ
ции CRT, такие как printf, она могла бы нарушить завершение вашей программы,
потому что во время вызова _CrtDumpMemoryLeaks библиотека находится в середине
процесса прекращения своей работы. Если вы следуете указанным правилам и
всегда компонуете свою программу сначала с библиотекой DCRT и только потом
со всеми остальными библиотеками, все будет в порядке, потому что функции
MemDumperValidator отключаются до завершения работы библиотеки DCRT. Тем
не менее для избежания проблем используйте в своих функциях записи дампов
только макросы _RPTn и _RPTFn, потому что _CrtDumpMemoryLeaks работает только с
этими макросами.

Использование MemStress
Пора добавить в вашу жизнь каплю стресса. Как хотите, но стресс может быть по
лезным. Увы, подвергнуть стрессу приложения Win32 сейчас гораздо сложнее, чем
раньше. Во времена 16разрядных ОС Windows мы могли выполнять наши при
ложения под управлением STRESS.EXE, полезной программы из SDK. Она позво
ляла вам мучить свое приложение всеми способами, в том числе отнимать у него
дисковое пространство или пространство кучи интерфейса графических устройств
(GDI) и расходовать описатели файлов. Даже ее значок был великолепен: слон,
идущий по канату.
Чтобы испытать приложения Win32 в стрессовых условиях, можно установить
ловушку для системы выделения памяти библиотеки DCRT и управлять выделением
памяти. Расширение MemStress дает вам возможность испытать выделение памя
ти на языке C или C++ (написание кода расходования дискового пространства я
оставил вам). Чтобы сделать MemStress простым в использовании, я написал при
помощи Windows Forms интерфейсную часть, позволяющую точно указать усло
вия, в которых вы хотели бы проверить свою программу.
Расширение MemStress позволяет форсировать неудачи выделения памяти,
опираясь на различные критерии: для всех выделений памяти, при каждом nом
выделении памяти, после выделения x байт, при запросе более y байт, для всех
выделений памяти из исходного файла и из конкретной строки исходного фай
ла. Кроме того, вы можете указать расширению MemStress выводить при каждом

636

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

выделении памяти окно, спрашивающее, хотите ли вы, чтобы это конкретное
выделение памяти завершилось неудачей, а также установить флаги библиотеки
DCRT, влияющие на выполнение вашего приложения. На CD находится програм
ма MemStressDemo; этот написанный при помощи MFC пример позволяет экспе
риментировать с параметрами пользовательского интерфейса MemStress и уви
деть соответствующие результаты, а еще выполняет функцию блочного теста для
MemStress.
Использовать расширение MemStress относительно просто. Для этого вы дол
жны включить в свою программу файл BUGSLAYERUTIL.H и вызвать макрос MEMST7
RESSINIT, передав ему имя своей программы. Для прекращения работы ловушки
выделения памяти служит макрос MEMSTRESSTERMINATE. Вы можете подключать и
останавливать ловушку при работе своей программы сколько угодно.
После компиляции своей программы запустите пользовательский интерфейс
MemStress, нажмите на кнопку Add Program (добавить программу) и введите то же
имя, что вы указали в макросе MEMSTRESSINIT. Выбрав условия ошибки, нажмите на
кнопку Save Program Settings (сохранение настроек программы) для сохранения
конфигурации в файле MEMSTRESS.INI. После этого вы можете запускать свою
программу и изучать ее поведение при неудачном выделении памяти.
К расширению MemStress следует относиться очень избирательно. Так, указав,
чтобы неудачей завершались все выделения блоков памяти, превышающих 100 байт,
и включив макрос MEMSTRESSINIT в функцию InitInstance своего приложения MFC,
вы скорее всего нарушите работу MFC, так как она не сможет инициализировать
ся. Самые лучшие результаты вы получите, если ограничите работу MemStress клю
чевыми областями своего кода, чтобы их можно было протестировать по отдель
ности.
Основная часть реализации MemStress касается чтения и обработки файла
MEMSTRESS.INI, в котором хранятся все параметры для отдельных программ.
С точки зрения библиотеки DCRT, особую важность представляет вызов функции
_CrtSetAllocHook при инициализации MemStress, потому что он устанавливает фук
нциюловушку выделения памяти AllocationHook. Если ловушка выделения памяти
возвращает TRUE, обработка запроса на выделение памяти может продолжаться.
Возвращая FALSE, ловушка выделения памяти может заставить библиотеку DCRT
провалить запрос на выделение памяти. Библиотека DCRT предъявляет к ловушке
выделения памяти только одно строгое требование: если тип блока, определяе
мый параметром nBlockUse, имеет значение _CRT_BLOCK, функцияловушка должна
возвращать TRUE, чтобы выделение могло увенчаться успехом.
Ловушка выделения памяти получает управление при вызове любого типа фун
кции выделения памяти. Эти типы, передаваемые ловушке в первом ее парамет
ре, могут иметь значения _HOOK_ALLOC, _HOOK_REALLOC и _HOOK_FREE. Если моя ловушка
AllocationHook получает тип _HOOK_FREE, я пропускаю весь код, определяющий ус
пешную или неудачную обработку запроса на выделение памяти. При получении
типов _HOOK_ALLOC и _HOOK_REALLOC моя функция AllocationHook выполняет ряд опе
раторов if, определяя, выполняется ли какоенибудь из условий неудачи. Если хотя
бы одно из условий выполняется, я возвращаю FALSE.

ГЛАВА 17

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

637

Интересные проблемы с MemStress
При тестировании MemStress на консольном примере все работало отлично, и я
был очень доволен. Однако, закончив работу над программой MemStressDemo,
основанной на MFC, я столкнулся с одной странной проблемой. Если я приказы
вал MemStress спрашивать меня, хочу ли я, чтобы выделение памяти провалилось,
я слышал несколько звуковых сигналов, и MemStressDemo прекращала свою ра
боту. Ошибка воспроизводилась при каждом запуске программы, но я никак не
мог найти ее причин, что стало меня не на шутку раздражать.
После нескольких запусков я, наконец, получил информационное окно, одна
ко оно находилось не в центре экрана, а в правом нижнем углу. Когда окна появ
ляются в правом нижнем углу экрана, вы можете быть почти уверены в том, что
столкнулись с ситуацией, в которой вызов APIфункции MessageBox почемуто стал
реентерабельным. Я предположил, что гдето в середине MessageBox вызывалась моя
ловушка выделения памяти. Для проверки этой гипотезы я установил точку пре
рывания на первой команде AllocationHook и «перешагнул» (step over) через вы
зов MessageBox. Все подтвердилось: отладчик остановился на точке прерывания.
Я приступил к изучению стека и увидел, что вызов APIфункции MessageBox
почемуто проходил через MFC. Пробираясь через код и наблюдая за тем, что
происходит, я попал в функцию _AfxActivationWndProc на строку, вызывавшую
CWnd::FromHandle. Этот вызов приводил к выделению памяти, нужной для того, чтобы
MFC могла создать CObject. Я был слегка удивлен тем, как я там оказался, однако
комментарии в коде гласили, что _AfxActivationWndProc служит для обработки ак
тивизации диалоговых окон и их закрашивания в серый цвет. MFC использует
ловушку приложений компьютерной профессиональной подготовки (computer
based training (CBT) hook) для перехвата создания окон в адресном пространстве
процесса. При создании нового окна — в моем случае простого информацион
ного окна (message box) — MFC создает подкласс окна с его собственной окон
ной процедурой.
Когда я понял суть проблемы, то пришел в еще большее замешательство, по
тому что не знал, как с ней справиться. Поскольку реентерабельность имела мес
то в одном потоке, я не мог использовать объект синхронизации, такой как сема
фор, потому что это привело бы к блокировке потока. Поразмыслив, я решил, что
мне нужен флаг рекурсии, указывающий на реентерабельность AllocationHook, но
он должен быть отдельным для каждого потока. У меня уже была критическая сек
ция, защищающая AllocationHook от многопоточной реентерабельности.
Сформулировав проблему таким образом, я понял, что мне нужна только пе
ременная в локальной памяти потока, которую я проверял бы в начале AllocationHook.
Если бы ее значение превышало 0, это указывало бы на реентерабельность Alloca7
tionHook во время обработки MessageBox, и в этом случае я должен был бы немед
ленно покидать функцию. Я быстро реализовал динамичное решение на основе
локальной памяти потока, и уровень моего беспокойства значительно снизился,
так как все начало работать так, как я и планировал.
Я думал, что теперь все будет в порядке, но не тут то было. При тестировании
кода, вызывавшего неудачу выделения памяти для конкретного файла и строки,
имя исходного файла имело значение NULL, а номер строки — 0. Я писал программу
MemStressDemo при помощи MFC и полагал, что она будет правильно использо

638

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

вать функции выделения памяти CRTDBG.H для передачи в них исходного файла
и номера строки. Увы, это было не так.
Я понял, что в начале STDAFX.H нужно указать определение _CRTDBG_MAP_ALLOC,
а в конце включить файл CRTDBG.H. Конечно, стоило мне скомпилировать такой
вариант программы, число ошибок, указывавших на переопределения и неожи
даные константы, меня просто сразило. Как я только ни пытался заставить при
ложение MFC скомпилироваться и использовать правильные версии функций
выделения памяти! Спустя некоторое время я обнаружил, что единственный при
емлемый вариант решения этой проблемы состоял в извлечении определений из
CRTDBG.H и включении их в файл STDAFX.H вручную. Чтобы в точности узнать,
что именно вам нужно делать, можете изучить файл STDAFX.H программы Mem
StressDemo.

Кучи операционной системы
Возможно, вы думали, что я уже рассказал про систему куч все, однако есть еще
один набор куч, про который вам непременно следует знать, — это кучи ОС. При
запуске приложения под управлением отладчика Windows включает проверку кучи
ОС. Это не отладочная куча стандартной библиотеки C — это куча Windows для
тех куч, что создаются при помощи APIфункции HeapCreate. Куча стандартной биб
лиотеки C — отдельная сущность. Кучи ОС интенсивно используются процесса
ми, например, для преобразования строк ANSI в формат Unicode при работе с
функциями ANSI, поэтому вы можете видеть информацию о кучах ОС при нор
мальных операциях, вот почему их так важно рассмотреть. Если вы подключаете
отладчик к своему приложению позднее, а не сразу начинаете выполнение про
граммы под отладчиком, вы не активизируете проверку куч ОС. Очень часто меня
спрашивают: «Вне отладчика моя программа работает отлично, но в отладчике она
вызывает исключение пользовательской точки прерывания. Почему моя программа
не работает?» Ответ: изза проверки куч ОС.
При включенной проверке куч ОС ваше приложение будет работать медлен
нее, потому что при вызове функций работы с кучей ОС будет выполнять соот
ветствующую проверку. В число примеров к этой книге я включил программу Heaper
(листинг 175), повреждающую кучу. Запустив Heaper в отладчике, вы увидите, что
она дважды вызывает DebugBreak на первой функции HeapFree. Будет также выведе
на информация об ошибке, пример которой приведен ниже. Да, вывод останав
ливается на буквах «of a» и не отображает размер блока, что могло бы быть весь
ма полезным. Если вы запустите эту программу вне отладчика, она проработает
до своего завершения безо всяких проблем.

HEAP[Heaper.exe]: Heap block at 00311E98 modified at 00311EAA past
requested size of a

Листинг 17-5.

HEAPER.CPP — пример повреждения кучи Windows

void main(void)
{
// Создание кучи операционной системы.
HANDLE hHeap = HeapCreate ( 0 , 128 , 0 ) ;

ГЛАВА 17

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

639

// Выделение памяти для 107байтового блока.
LPVOID pMem = HeapAlloc ( hHeap , 0 , 10 ) ;
// Запись 12 байт в 107байтовый блок (запись после конца блока).
memset ( pMem , 0xAC , 12 ) ;
// Выделение нового 207байтового блока.
LPVOID pMem2 = HeapAlloc ( hHeap , 0 , 20 ) ;
// Запись данных на 1 байт до начала второго блока.
char * pUnder = (char *)( (DWORD_PTR)pMem2 7 1 );
*pUnder = 'P' ;
// Освобождение первого блока. Этот вызов HeapFree приведет
// к срабатыванию точки прерывания в отладочном коде кучи ОС.
HeapFree ( hHeap , 0 , pMem ) ;
// Освобождение второго блока. Заметьте: этот
// вызов не приводит к сообщениям о проблеме.
HeapFree ( hHeap , 0 , pMem2 ) ;
// Освобождение несуществующего блока.
// Этот вызов также не приводит к проблемам.
HeapFree ( hHeap , 0 , (LPVOID)0x1 ) ;
HeapDestroy ( hHeap ) ;
}
Если вы используете собственные кучи ОС или хотите, чтобы приложение
включило проверку куч ОС при выполнении вне отладчика, вы можете установить
дополнительные флаги для получения более подробного диагностического вывода.
Небольшая утилита GFLAGS.EXE из пакета Debugging Tools for Windows поможет
установить некоторые глобальные флаги, которые Windows проверяет при пер
вом запуске приложения. На рис. 171 показаны параметры GFLAGS.EXE для HEA
PER.EXE, программы из листинга 175. Многие из параметров System Registry (си
стемный реестр) и Kernel Mode (режим ядра) глобальны, поэтому вам нужно быть
чрезвычайно внимательным при их изменении, так как это может оказать значи
тельное влияние на производительность системы или вообще нарушить ее рабо
ту. Изменять параметры Image File Options (настройки файла образа), показанные
на рис. 171, гораздо безопасней, потому что они ограничены только одним ис
полняемым файлом. Кстати, несмотря на всю полезность GFLAGS.EXE, для проверки
повреждений кучи можно использовать и средство Application Verifier, о котором
я расскажу ниже.
Наконец, чтобы завершить рассказ про GFLAGS.EXE, я хочу обратить ваше вни
мание на один очень полезный параметр Show Loader Snaps (показывать снимки
загрузчика). Если вы отметите этот флажок и запустите свою программу, вы уви
дите, где Windows загружает DLL и как она собирается настраивать импортируе

640

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

мые функции для вашего приложения, что называется деланием снимков (snapping).
Если вам нужно точно узнать, что делает загрузчик Windows при загрузке прило
жения (если у вас есть проблема), этот флажок следует отключить. Подробне о
снимках загрузчика см. раздел «Under the Hood» Мэтта Питрека (Matt Pietrek) в
«Microsoft Systems Journal» (1999, сентябрь).

Рис. 171.

Параметры GFLAGS.EXE для программы HEAPER.EXE

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

Обнаружение записи в неинициализированную память
Нет ничего хуже ошибки, которая происходит из ниоткуда и не соответствует
никакой ветви выполнения кода. Если вы попали в такую ситуацию, она объясня
ется скорее всего записью в неинициализированную память, также известной как
запись по случайному адресу (wild write). Причина таких ошибок таится в неини
циализированном указателе, который по воле случая указывает на корректную
область памяти. Обычно это происходит со стековыми указателями или, иначе
говоря, с локальными переменными. Так как при выполнении вашей программы
стек постоянно изменяется, ничто не сможет сказать вам, куда указывает неини
циализированный указатель, вот почему он может казаться случайным.
Когда нас просят помочь решить подобную коварную проблему, мы всегда
встречаем совершенно растерянных программистов, утверждающих, что попро
бовали буквально все способы ее обнаружения. Так как они уже проделали «все»,
они отчаянно желают узнать, что мы наколдуем. Я отвечаю на это, что мы соби
раемся применить запатентованный и зарегистрированный Магический Способ

ГЛАВА 17

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

641

Отладки Неинициализированной Памяти по Методике Ниндзя (MNUMDT). Все
собираются посмотреть на это и ерзают в креслах, ожидая чуда, особенно когда я
упоминаю, что MNUMDT сработает, только если мне помогут двое наименее опыт
ных разработчиков группы.
В этот момент ко мне подходят двое младших программистов, всем своим видом
показывающие, что намерены с достоинством нести свою ношу, и я описываю им
MNUMDT:
쐽 каждый из нас берет на себя по одной трети кода;
쐽 каждый должен внимательно прочитать каждую строку кода, отыскивая все
объявления указателей;
쐽 при нахождении объявления неинициализированного указателя мы инициа
лизируем его значением NULL;
쐽 при обнаружении каждого вызова выделения памяти не для классов мы добав
ляем после него вызов memset или ZeroMemory для обнуления памяти;
쐽 после любого освобождения памяти мы снова присваиваем указателю нулевое
значение;
쐽 при нахождении вызова memset или операции копирования строк мы должны
проверить, что каждая операция правильно подсчитывает размер блока памяти;
쐽 каждая переменная члена класса должна инициализироваться в конструкто
ре(ах).
Выслушивая правила MNUMDT, парни готовы выбежать из комнаты, но я не
даю им сделать этого. Если вы думаете, что это грубое насилие, вы абсолютно правы:
так и есть.
Я имел дело с тысячами ошибок, вызванных неинициализированными указа
телями, и знаю, что тут не поможет никакая отладка. Вы просто потратите время.
Отладка станет гораздо эффективнее, если перед ее началом выполнить только
что описанные мной действия. Очень вероятно, что проблему вызывает один из
указателей, которые вы проинициализируете. После этого программа не сможет
исказить память и продолжить свое выполнение, а тут же потерпит крах, пытаясь
записать данные по указателю NULL.
Некоторые думают, что это не сработает, но я могу привести сотни случаев,
когда программисты неделями пытались отыскать проблему и не получали ника
ких результатов. Когда они обращались к нам, мы находили проблему за день или
два. Иногда разработчики пытаются перехитрить сами себя, не желая прибегать
к такому грубому подходу, однако он просто великолепен в подобных ситуациях.
Почему я прибегаю к помощи наименее опытных программистов? Да потому, что
они менее высокомерны, чем старшие разработчики, и, находясь перед лицом всех
своих коллег, чрезвычайно ответственно подходят к возложенным на них задачам.

Нахождение записи данных после окончания блока
Запись данных после окончания блока памяти следует искать не только при по
мощи DCRT и таких средств, как BoundsChecker компании Compuware, но и при
тестировании программы. Увы, не все программисты относятся к тестированию
так же серьезно, как вы, поэтому вы все равно будете сталкиваться в их коде с этими
мерзкими ошибками. Кроме того, некоторые случаи записи данных по оконча

642

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

нии блока происходят только в наиболее напряженных условиях. Ситуация ухуд
шается еще и тем, что, когда такая ошибка происходит в заключительной компо
новке, в конце блока памяти нет пустого пространства, что может привести к
коварному искажению данных, которое неделями может оставаться незамечен
ным, пока не потерпит крах важное серверное приложение.
Один из недостатков проверки записи данных после блока памяти в DCRT в
том, что о них сообщается, только когда операция с памятью выполнит соответ
ствующую проверку. Было бы лучше, если б программа, допустившая такую ошибку,
тут же прекращала свою работу. Программисты Microsoft также желали получить
эту возможность, поэтому несколько лет назад они выпустили средство PageHeap.
PageHeap тесно взаимодействует с ОС и использует уникальный трюк для не
медленного обнаружения записи после выделенного блока. Когда вы выделяете
16 байт, программа PageHeap на самом деле выделяет 8 кб! Сначала она выделяет
4кбайтную страницу, наименьший блок памяти, к которому применяются права
доступа. PageHeap предоставляет права на чтение этой страницы и ее запись. Сразу
за страницей для чтения/записи PageHeap выделяет следующую страницу, отме
чая ее как не имеющую доступа. PageHeap выполняет некоторые манипуляции с
указателями и предоставляет вашей программе адрес, находящийся за 16 байт до
конца первой страницы. Таким образом, когда вы попытаетесь записать данные в
17ый байт, начиная от этого адреса, вы попадете на страницу с запрещенным
доступом и немедленно вызовете нарушение доступа. Формат памяти, выделяемой
PageHeap, показан на рис. 172.

Память для чтения/записи

Возвращаемый
16-байтный
блок

Память, не имеющая доступа

0x00310000

0x00310FF0

0x00311000

Рис. 172.

Память, выделяемая программой PageHeap

Как вы можете представить, PageHeap использует гораздо больше памяти, чем
нужно. Однако эта цена не имеет значения, если вы сможете найти запись дан
ных вне блока. Если вы работаете над крупным приложением, установите в тес
товый компьютер как можно больше памяти. Можете «позаимствовать» пару мо
дулей из компьютера своего начальника — все равно он этого не заметит.
Рассказывая о PageHeap, я обязательно должен упомянуть, что все возвращен
ные вам указатели выравниваются по границе 16 байт. Это значит, что если вы
выделяете 10 байт, то для попадания на страницу с запрещенным доступом вы
должны будете записать в память 7 дополнительных байт. Пусть это вас не сму
щает: при выходе за пределы блока памяти обычно выполняется запись не 1–2
байт, а довольно приличного их числа, поэтому на эффективности PageHeap это
не скажется. Это также значит, что PageHeap заслуживает внимания, только когда
вы работаете с заключительной компоновкой своей программы. Отладочная ком
поновка сама по себе включает дополнительное пространство до и после выде
ляемых блоков памяти, поэтому в таких случаях PageHeap ничего не обнаружит.

ГЛАВА 17

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

643

Пакет Application Compatibility Toolkit
Я мог бы привести вам огромное описание подключения PageHeap при помощи
странного средства командной строки, встроенного в GFLAGS, но есть способ
получше. Пакет Application Compatibility Toolkit (ACT) не только вносит функци
ональность PageHeap прямо в Visual Studio .NET, но и предоставляет некоторые
прекрасные средства обнаружения ошибок, про которые вам следует знать. ACT
предназначен в первую очередь для обеспечения выполнения программ под Micro
soft Windows XP/Server 2003, но он также включает программу Application Verifier
(AppVerifier). Онато нам и нужна.
Пакет ACT вы можете установить с CD, прилагаемого к книге, или загрузить
его последнюю версию с сайта http://www.microsoft.com/windowsxp/appexperience/
default.asp. В документации говорится, что AppVerifier из версии ACT 2.6, доступ
ной на момент написания этой книги, может работать под управлением Microsoft
Windows 2000 SP3 и более поздних версий ОС, но я смог запустить эту програм
му только на Windows XP/Server 2003. Я так и не добился правильной работы
AppVerifier с Windows 2000. Кроме того, некоторые из тестов и ошибок не выво
дят информации, хотя в документации утверждается обратное. В оставшейся ча
сти обсуждения я буду полагать, что вы выполняете AppVerifier под Windows XP/
Server 2003 с правами администратора (это необходимое условие).
AppVerifier включает отдельный исполняемый файл (APPVERIF.EXE) и надстройку
(VSAPPVERIF.DLL). Надстройка AppVerifier, включенная в ACT версии 2.6, интегри
руется в панель инструментов Debug среды Visual Studio .NET 2002, но не Visual
Studio .NET 2003. К счастью, благодаря опыту, полученному при работе с надстрой
ками в главе 9, я смог обнаружить, как заставить AppVerifier работать и во втором
случае. Если вы собираетесь использовать более позднюю версию AppVerifier,
вероятно, она интегрируется в Visual Studio .NET 2003 сама, так что вы можете
пропустить описание следующих действий.
Установив ACT, откройте командную строку и перейдите в подкаталог \Applications. Зарегистрируйте DLL надстройки AppVerifier
командой REGSVR32 VSAPPVERIF.DLL, чтобы внести в реестр информацию о компо
нентах COM. Далее вы должны сообщить об этой надстройке среде Visual Studio
.NET 2003. В каталоге AppVerifierAddIn на CD находится .REGфайл AppVerifier
AddInReg.reg. Чтобы выполнить его, дважды щелкните его значок в Windows Explorer
или введите в командной строке выражение REGEDIT AppVerifierAddInReg.REG.
Если вы беспокоитесь о том, может ли перенос надстройки, написанной для
предыдущей версии Visual Studio .NET, в более новую версию этой среды привес
ти к неприятностям, я отвечу, что это возможно. Если бы надстройка была напи
сана при помощи .NET, использование предыдущих версий CLR могло бы вызвать
проблемы. Однако AppVerifier написана только на C++, поэтому вы ничем не рис
куете. Я знаю это совершенно точно, так как я проверил VSAPPVERIF.DLL при по
мощи REGASM, и REGASM сообщил, что эта надстройка не является сборкой .NET.
Конечно, я все же протестировал VSAPPVERIF.DLL и проверил все ее параметры,
чтобы гарантировать полную безопасность. Запустив Visual Studio .NET с AppVerifier,
не имея прав администратора, вы увидите странное окно сообщения об ошибке —
«Installer Error» (ошибка программы установки) — с текстом «Error: insufficient

644

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

permissions to run this program. Administrator access needed» (ошибка: недостаточ
но прав для выполнения программы. Необходимы права администратора).
Как только вы установите ACT или зарегистрируете AppVerifier вручную, в па
нель инструментов Debug среды Visual Studio .NET будет добавлено несколько новых
кнопок, которых можно не заметить. Новый вид панели Debug см. на рис. 173.
Вам нужно будет настроить панель инструментов Debug так, чтобы она присут
ствовала не только при отладке, поэтому установить параметры надстройки AppVe
rifier можно лишь перед началом отладки. Один из главных принципов работы
AppVerifier состоит в том, что при возникновении проблемы она вызывает функ
цию DebugBreak, поэтому программу всегда следует выполнять под управлением
отладчика. Встроив надстройку в Visual Studio .NET, вы избавитесь от мучений,
связанных с Windows NT Symbolic Debugger (NTSD) или WinDBG.

Панель Debug

Рис. 173. Панель инструментов Debug среды Visual Studio .NET
после правильного конфигурирования надстройки AppVerifier
При работе с AppVerifier нужно прежде всего подключить один недокументи
рованный параметр, обеспечивающий более эффективное обнаружение ошибок
памяти. Загрузив проект в Visual Studio .NET, щелкните на панели Debug новую кноп
ку Options (параметры) и установите в диалоговом окне Options флажок Use Full
Page Heap (использовать кучу полных страниц), как показано на рис. 174. В самой
по себе программе APPVERIF.EXE этот параметр недоступен, но чем более тщатель
ную проверку вы выполняете, тем лучше. На рис. 174 вы можете заметить, что я
не устанавливал флажок Break In The Debugger After Each Logged Event (выходить
в отладчик после регистрации каждого события). Установив его, вы будете оста
навливаться буквально каждые 15 наносекунд, когда надстройка AppVerifier будет
сообщать о чемнибудь.

Рис. 174.

Диалоговое окно Options надстройки AppVerifier в Visual Studio .NET

ГЛАВА 17

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

645

Если щелкнуть на панели инструментов Debug кнопку Tests (тесты), появится
диалоговое окно AppVerifier Test Settings (параметры тестов AppVerifier), в кото
ром вы можете указать, какие тесты выполнять (табл. 174). Кроме того, я подключаю
все тесты, показанные на рис. 175. Пункты, указываемые в диалоговом окне AppVe
rifier Test Settings, относятся к отдельному процессу, поэтому их нужно устанав
ливать для каждого приложения, которое вы хотите выполнить под управлением
AppVerifier.

Табл. 17-4.

Описание тестов надстройки AppVerifier

Тест

Описание

Detect heap corruptions
(обнаружение повреждений
кучи)

Выполняет систематичную проверку кучи и добавля
ет после каждого выделенного блока памяти «защит
ную» страницу, нужную для обнаружения выхода
за пределы блока.

Check lock usage (проверка
механизмов блокировки)

Находит частые ошибки, связанные с критическими
секциями.

Detect invalid handle usage
(обнаружение неправильного
использования описателей)

В ACT 2.6 этот тест проверяет, не имеют ли описате
ли значения NULL/INVALID_HANDLE_VALUE и коррек
тен ли параметр TLS (локальная память потока).

Check for adequate stack
(проверка объема стека)

Проверяет наличие достаточного объема стека
в службах Win32. Вероятно, вам не придется исполь
зовать этот тест.

Log start and stop
(регистрация запуска
и прекращения работы
программы)

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

Checks system path usage
(проверка использования
системных путей)

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

Checks version handling
(проверка обработки версий)

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

Checks registry usage
(проверка использования
реестра)

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

Logs changes to Windows File
Protection files (регистрировать
изменение файлов Windows File
Protection)

Регистрирует попытки изменения файлов,
защищенных механизмом Windows File Protection.

Logs DirectX file checks
(регистрировать проверку
файлов DirectX)

Регистрирует все попытки приложения выполнить
проверку версий библиотек DirectX.

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

646

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Табл. 17-4. Описание тестов надстройки AppVerifier

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

Тест

Описание

Logs registry changes (регистри
ровать изменения реестра)

Записывает в XMLфайлв все изменения, вносимые
приложением в реестр.

Logs file system changes
(регистрировать изменения
файловой системы)

Записывает в XMLфайл все изменения, вносимые
приложением в файловую систему. Если программа
попытается записать файлы в папки %windir% или
Program Files, вы получите предупреждение.

Logs calls made to obsolete APIs
(регистрировать вызовы
устаревших APIфункций)

Регистрирует все вызовы APIфункций, отмеченгые
в Microsoft Platform SDK как устаревшие. При исполь
зовании этого теста изучайте в журнале только те
записи, в которых встречаются ваши двоичные фай
лы. ОС сама выполняет достаточно много вызовов
устаревших функций.

Logs installations of kernelmode
drivers (регистрировать установку
драйверов режима ядра)

Регистрирует все попытки программы установить
драйвер режима ядра.

Logs potential security issues
(регистрировать потенциальные
проблемы с безопасностью)

Определяет и регистрирует потенциальные пробле
мы с безопасностью при использовании дескрипто
ров безопасности DACL со значением NULL и вызо
вов APIфункций создания процесса.

RPC Checks (проверки RPC)

Определяет некорректное использование RPC.
Применяется только при работе с Windows
Server 2003. Вероятно, вам никогда не понадобится
включать этот тест.

Рис. 175.

Диалоговое окно AppVerifier Test Settings

Чтобы подключить AppVerifier для вашего приложения, нужно всего лишь на
жать кнопку Enable Verification (включить проверку). Определять тесты и параметры
нужно до нажатия Enable Verification, потому что после этого кнопки Tests и Options
становятся неактивными. В пакет ACT входит демонстрационная программа, но
ее действия непонятны, а исходного кода нет, поэтому мне пришлось быстро
написать небольшую программу AppVerifierDemo, которую вы можете использо
вать для изучения многих ошибок, которые находит AppVerifier. Чтобы увидеть
нахождение ошибок в действии откройте проект AppVerifierDemo (он есть на СD).

ГЛАВА 17

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

647

Некоторые из кнопок — например, двойная инициализация критической секции —
ошибок не вызывают: ACT 2.6 документирует эту ошибку, но на самом деле не
генерирует ее. Уделите особое внимание разделу PageHeap Errors (ошибки Page
Heap), который покажет вам, как прекрасно PageHeap справляется с обнаружени
ем выхода за пределы блока и других неприятных проблем с памятью.
Выполнив свое приложение пару раз, вы можете изучить журналы, сохранен
ные AppVerifier. Хотя большинство ошибок, обнаруженных надстройкой AppVerifier,
приводит к немедленному нарушению доступа, некоторые появляются только в
журнале, который открывается щелчком кнопки Log Files (файлы журналов) на
панели инструментов Debug. Изучая журналы, убедитесь, что установлен параметр
Show All (показывать все), иначе вы увидите не всю информацию.

Потрясающие ключи компилятора
Как я упоминал в главе 2, компилятор Microsoft Visual C++ .NET поддерживает новые
ключи, которые необходимо устанавливать всегда. В этом разделе я опишу клю
чи /RTCx и /GS и объясню, почему они так важны.

Ключи проверки ошибок в период выполнения
Даже если бы единственной новой функцией, добавленной в компилятор Visual
C++ .NET, были ключи /RTCx (RunTime Error Checks, проверка ошибок во время
выполнения), я бы всем говорил, что переход на эту версию просто обязателен.
Как можно догадаться по названию, четыре ключа RTC следят за вашим кодом и
вызывают отладчик, если при выполнении программы возникают определенные
ошибки. На рис. 176 показана ошибка, возникшая в результате того, что какой
то код выполнил запись после окончания локальной переменной. Как вы можете
увидеть по информационному окну, показана и искаженная локальная перемен
ная. К тому же рисунок ничего не говорит о том, что информационное окно по
является в конце функции, в которой произошла ошибка, благодаря чему поиск
и исправление проблемы становятся тривиальной задачей! Между прочим, клю
чи RTC допускается применять только в отладочных компоновках.

Рис. 176.

Сообщение об ошибке при установленном ключе /RTCs

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

648

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

неинициализированного указателя. Все локальные переменные получают значе
ния 0xCC, что соответствует коду операции INT 3 (точка прерывания).
Вовторых, ключ /RTCs выполняет проверку указателя стека при каждом вызо
ве функции, обнаруживая несоответствия соглашений вызова. Так, если функция
объявлена с соглашением __cdecl, но экспортируется как __stdcall, при возвращении
из функции __stdcall стек будет поврежден. Если вы уже давно следите за ключа
ми компиляторов, вы уже догадались, что первые два варианта проверки стека
ключом /RTCs аналогичны функции ключа /GZ в Microsoft Visual C++ 6.
К нашей всеобщей радости, Microsoft расширила ключ /RTCs, и теперь он так
же проверяет запись данных до начала и после границ многобайтовых локаль
ных переменных, таких как массивы. Он делает это, добавляя 4 байта к началу и
концу этих массивов и проверяя в конце функции, сохранили ли эти байты пер
воначальное значение 0xCC. Локальная проверка работает со всеми многобайто
выми локальными переменными, кроме тех, что требуют от компилятора добав
ления дополнительных байтов. Обычно дополнительные байты добавляются только
при использовании директивы __declspec(align), ключа /Zp, выравнивающего члены
структуры, или директивы #pragma pack(n).
Второй ключ, /RTCu, проверяет использование ваших переменных и выводит
предупреждение, если вы вызываете какуюнибудь из них, не проинициализиро
вав. Если вы много лет охотитесь на насекомых, важность этого ключа может быть
вызывать у вас удивление. Так как все ответственные читатели этой книги (и вы в
том числе) уже компилируют свой код на уровне диагностики 4 (/W4) и рассмат
ривают все предупреждения как ошибки (/WX), вы можете быть уверены в том, что
предупреждения компилятора C4700 и C4701 укажут вам на стопроцентное и ве
роятное использование неинициализированных переменных соответственно. Ну,
а благодаря ключу /RTCu об этих видах ошибок могут узнать и более легкомыс
ленные программисты. В связи с ключом /RTCu интересно отметить, что код, про
веряющий неинициализированные переменные, включается в программу, если
компилятор обнаруживает условие C4700 или C4701. Третий ключ, /RTC1, просто
объединяет ключи /RTCu и /RTCs.
Последний ключ, /RTCc, обнаруживает отбрасывание данных при операциях
присваивания — например, если вы пытаетесь присвоить значение 0x101 пере
менной типа char. Как и в случае ключа /RTCu, если вы компилируете программу с
ключами /W4 и /WX, отбрасывание данных приведет к ошибке C4244 во время ком
пиляции. При получении ошибки /RTCc вам нужно или применить к нужным вам
битам маску, или выполнить приведение типа. Поле Basic Runtime Checks (базо
вые виды проверки периода выполнения) диалогового окна Property Pages
(рис. 177) позволяет установить только /RTCu, /RTCs или /RTC1. Чтобы включить ключ
/RTCc, нужно выбрать значение в поле Smaller Type Check (проверка при преобра
зовании к меньшему типу), расположенному над полем Basic Runtime Checks.
Сначала я не мог понять, почему /RTCc не устанавливается ключом /RTC1. Одна
ко потом я понял, что /RTCc может сообщать об ошибках при абсолютно коррек
тном коде C, например:

char LoByte(int a)
{
return ((char)a) ;
}

ГЛАВА 17

Рис. 177.

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

649

Установка ключей /RTCx

Если бы ключ /RTCc входил в состав /RTC1, люди могли бы думать, что все клю
чи проверки ошибок в периода выполнения могут поднимать ложную тревогу. Тем
не менее я все равно всегда устанавливаю ключ /RTCc, так как хочу знать обо всех
потенциальных проблемах при выполнении своей программы.
Теперь обратимся к уведомлениям, которые вы получаете в случае обнаруже
ния ошибки. При выполнении ваших программ вне отладчика код проверки в
период выполнения выводит обычное диагностическое информационное окно
стандартной библиотеки C. Если вы пишете службы или модули, не имеющие
пользовательского интерфейса, вы должны будете перенаправить сообщение
информационного окна при помощи функции _CrtSetReportMode, передав ей в
качестве типа сообщения значение _CRT_ASSERT. Возможно, вы думали, что ключи
/RTCx имеют единственный стандартный механизм уведомления пользователя, но
это не так. При выполнении программы под отладчиком имеет место совершен
но иной механизм уведомлений.
Если вы посмотрите на диалоговое окно Exceptions (исключения) интегриро
ванной среды разработки Visual Studio .NET, вы можете заметить несколько но
вых классов исключений. В данный момент нам интересен класс Native RunTime
Checks (проверка в период выполнения). Открыв его в окне Exceptions, вы увиди
те четыре разных исключения, соответствующих ключам /RTCx. Наверное, вы уже
догадались, что, работая под управлением отладчика и сталкиваясь с проверкой в
период выполнения, ваша программа генерирует специальное исключение, что
бы отладчик мог его обработать.

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

650

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

собственный обработчик получает те же параметры, что и версия по умолчанию,
поэтому вы можете выводить сведения об ошибках, соответствующих им перемен
ных и другую информацию. Как видите, вы должны сами выбирать способ инфор
мирования пользователя. Код листинга 176 находится на CD в примере RTCHan
dling, так что вы можете изменять обработку ошибок, как вам будет угодно.

Листинг 17-6.

Создание собственных отчетов об ошибках /RTCx

/*/////////////////////////////////////////////////////////////////////
ФУНКЦИЯ: HandleRTCFailure
ОПИСАНИЕ:
Обработчик ошибок периода выполнения (Run Time Checking, RTC), работающий
при выполнении НЕ под отладчиком. При выполнении под управлением отладчика
эта функция игнорируется. Поэтому вы никак не можете ее отладить!
ПАРАМЕТРЫ:
iError 7 тип ошибки, сообщаемый функции _RTC_SetErrorType.
Обратите внимание, что я не использую этот параметр.
szFile 7 имя исходного файла, в котором произошла ошибка.
iLine
7 номер строки ошибки.
szModule – модуль ошибки.
szFormat – строка формата в стиле printf для переменного списка
параметров. Обратите внимание, что я использую этот
параметр только для получения их значений.
...
– "переменный" список параметров. Здесь могут передаваться
только два значения. Первое — идентификатор ошибки RTC:
1 = /RTCc
2 = /RTCs
3 = /RTCu
Второе — указатель на строку, выводимую отладчиком. Это
значение важно только для ключей /RTCs и /RTCu, так как
они показывают переменную, вызвавшую проблему.
ВОЗВРАЩАЕМЫЕ ЗНАЧЕНИЯ:
TRUE 7 после возврата из этой функции выполняется вызов _DebugBreak.
FALSE – выполнение продолжается.
/////////////////////////////////////////////////////////////////////*/
// Отключение проверок периода выполнения для этой функции,
// нужное для предотвращения ее рекурсивного вызова.
#pragma runtime_checks("", off)
// Критическая секция, защищающая функцию HandleRTCFailure
// от реентерабельности.
CRITICAL_SECTION g_csRTCLock ;
int HandleRTCFailure ( int
/*iError*/
const char * szFile
int
iLine
const char * szModule
const char * szFormat
...
{

,
,
,
,
,
)

ГЛАВА 17

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

651

// Нет реентерабельности кода!
EnterCriticalSection ( &g_csRTCLock ) ;
// Получение двух параметров переменного списка. Я полагаю,
// что в будущем будет добавлено множество проверок RTC.
va_list vl ;
va_start ( vl , szFormat ) ;
// Первый параметр — идентификатор ошибки.
_RTC_ErrorNumber RTCErrNum = va_arg ( vl , _RTC_ErrorNumber ) ;
// Второй — дополнительное описание ошибки.
char * szErrorVariableDesc = va_arg ( vl , char * ) ;
va_end ( vl ) ;
TCHAR szBuff [ 512 ] ;
// Получение описания ошибки по ее идентификатору.
const char *szErr = _RTC_GetErrDesc ( RTCErrNum ) ;
// Нужно убедиться,что szFile и szModule не равны NULL.
if ( NULL == szFile )
{
szFile = "Unknown File" ;
}
if ( NULL == szModule )
{
szModule = "Unknown Module" ;
}
// Если ошибка соответствует ключу /RTCs или /RTCu, я могу вывести
// полезную информацию, в том числе интересующую нас переменную!
if ( 1 != RTCErrNum )
{
_stprintf ( szBuff
,
_T ( "%S\n%S\nLine #%d\nFile:%S\nModule:%S" ) ,
szErr
,
szErrorVariableDesc
,
iLine
,
szFile
,
szModule
) ;
}
else
{
// Формирование строки.
_stprintf ( szBuff
,
_T ( "%S\nLine #%d\nFile:%S\nModule:%S" ) ,
szErr
,
см. след. стр.

652

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

iLine
szFile
szModule

,
,
) ;

}
int iRes = TRUE ;
if ( IDYES == MessageBox ( GetForegroundWindow ( )
,
szBuff
,
_T ( "Run Time Check Failure" ) ,
MB_YESNO | MB_ICONQUESTION
) )
{
// Возврат 1 означает, что по окончании этой функции
// будет вызвана функция DebugBreak.
iRes = 1 ;
}
else
{
iRes = 0 ;
}
// Выход из критической секции.
LeaveCriticalSection ( &g_csRTCLock ) ;
return ( iRes ) ;
}
#pragma runtime_checks("", restore)
Установка собственного обработчика ошибок тривиальна; для этого нужно
просто передать указатель на него в _RTC_SetErrorFunc. Есть несколько других фун
кций, помогающих обрабатывать ошибки в период выполнения. Первая, _RTC_Get6
ErrDesc, получает строку, описывающую конкретную ошибку. _RTC_NumErrors возвра
щает общее число ошибок, поддерживаемых текущими версиями компилятора и
стандартной библиотеки. Кроме того, есть еще и функция _RTC_SetErrorType, ко
торуя я нахожу немного опасной. Она позволяет отключить обработку ошибок для
отдельных или всех специфических проверок в период выполнения.
Так как проверки в период выполнения основаны на возможностях стандарт
ной библиотеки C, вы можете подумать, что, если ваша программа не использует
стандартную библиотеку C, вы утрачиваете все преимущества ключей RTC. Если
вам интересно, зачем вам когданибудь может понадобиться программировать без
стандартной библиотеки C, вспомните про ATL и макрос _ATL_MIN_CRT. Если вы не
используете стандартную библиотеку C, то при определенном макросе __MSVC_RUN6
TIME_CHECKS нужно вызвать функцию _RTC_Initialize. Вы также должны предоста
вить функцию _CRT_RTC_INIT, возвращающую указатель на ваш собственный обра
ботчик ошибок.
Когда я только начал писать собственные обработчики вывода, я столкнулся с
небольшой проблемой. Я не мог отладить собственный обработчик! Немного
подумав об этом, вы поймете причину. Как я уже сказал, код проверки в период
выполнения может определить, выполняете ли вы программу под управлением
отладчика, и вывести информацию или в отладчике, или при помощи обычного

ГЛАВА 17

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

653

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

Стандартный вопрос отладки
Как убедиться в том, что при обработке строк я не допустил ошибок?
Наверное, самым частым источником ошибок и проблем с безопасностью
является обработка старых добрых строк, оканчивающихся на 0. Пробле
ма заключается в определении этих функций в стандартной библиотеке C:
они никак не позволяют указать длину буфера. Так, функция strcpy прини
мает указатели на два буфера и вслепую копирует данные из буфера ввода
в буфер вывода, совершенно не предполагая, что буфер вывода может быть
вдвое меньшим. Это не только может привести к записи данных вне выде
ленного блока памяти, но и создает огромную брешь в защите, при помо
щи которой авторы вирусов перезаписывают в стеке адрес возврата для
передачи управленя на свой код.
Вы можете до потери сознания просматривать программу в поисках этих
ошибок и все же упустить их. К счастью, некоторые умные люди из Microsoft
поняли, что пора изменить положение дел. Новая библиотека STRSAFE при
звана обезопасить обработку строк. STRSAFE входит в Platform SDK за июль
2002 года и включена в Visual Studio .NET 2003.
Чтобы работать с этой библиотекой, нужно включить в прекомпилиро
ванный заголовок файл STRSAFE.H, который предоставит вам доступ ко
многим новым функциям, принимающим длину буфера вывода и гаранти
рующим, что число скопированных символов не превысит указанное вами.
Включеив STRSAFE.H, вы при первой же компиляции программы обнаружите,
что уязвимые к ошибкам функции начнут возмущаться и компиляция не
удастся, пока вы их не исправите.
Мне очень жаль, но STRSAFE появилась после того, как я написал почти
весь код для этой книги. Работая над своими проектами, вы поймете, что
настройка STRSAFE очень похожа на действия, нужные для преобразования
кода ANSI в формат Unicode. Это требует некоторого времени, но затраты
того стоят. Ко времени поступления этой книги в продажу или чуть позже
я переработаю все программы из нее и выложу их на вебсайте компании
Wintellect.

Ключ проверки безопасности буфера
Проверки в период выполнения очень полезны, но есть и еще один ключ, кото
рый также следует устанавливать всегда. Это ключ /GS, выполняющий проверку
безопасности буфера (Buffer Security Check). В отличие от ключей /RTCx его сле
дует устанавливать и для отладочных, и для заключительных компоновок. Ключ
/GS отслеживает адреса возврата из функции и предотвращает его перезапись, что
часто делают вирусы и троянские кони для передачи управления на свой код. Для

654

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

этого он резервирует в стеке дополнительное пространство перед адресом воз
врата. При входе в функцию код пролога сохраняет в этом месте результат вы
полнения операции ИСКЛЮЧАЮЩЕЕ ИЛИ над адресом возврата и специальным
значением (security cookie). Это значение подсчитывается во время загрузки мо
дулей, чем достигается его уникальность для отдельных модулей. При возврате из
функции специальная функция _security_check_cookie проверяет, не изменилось
ли сохраненное значение. При обнаружении различия выводится информационное
окно, и программа завершается. Если вы хотите посмотреть этот механизм в дей
ствии, изучите в исходном коде стандартной библиотеки C файлы SECCINIT.C,
SECCOOK.C и SECFAIL.C
Обеспечения безопасности ключу /GS мало, поэтому он еще оказывает нам
огромную помощь при отладке. Ключи /RTCx отслеживают множество ошибок, но
случайную перезапись адреса возврата они все же иногда пропускают. Благодаря
/GS вы получаете проверку таких ситуаций и в отладочных компоновках. Конеч
но, сотрудники Microsoft при разработке этого ключа не забывали про нас, по
этому вы можете заменить функцию вывода информационного окна по умолча
нию собственным обработчиком, вызвав функцию _set_security_error_handler. Если
вы повредите стек, ваш обработчик должен выполнять после записи ошибки вы
зов ExitProcess.

Резюме
Стандартная отладочная библиотека C предоставляет массу великолепных возмож
ностей, если, конечно, вы подключите их в своем приложении. Так как использо
вания памяти в программах C и C++ избежать невозможно, мы не должны упус
кать ни одного шанса облегчить решение проблем, которые обязательно при этом
возникнут. В этой главе я привел самую важную информацию о библиотеке DCRT
и представил две написанных мной утилиты, MemDumperValidator и MemStress,
которые помогут получить более подробную информацию о памяти, используе
мой вашими программами, и позволят оптимизировать тестирование программ
в стрессовых условиях.
Расширяемость библиотеки DCRT просто удивительна. Если вы всерьез зани
маетесь отладкой более года или двух, вы, возможно, уже разрабатывали в про
шлом чтото похожее на нее. Надеюсь, что я смог доказать вам мощь библиотеки
DCRT. Советую разработать другие утилиты и модули, которые облегчат отладку
проблем с памятью.
Мы также обсудили методы и инструменты обработки самых отвратительных
проблем с памятью: записи посредством неинициализированных указателей и
записи вне выделенных блоков памяти. Пусть применение грубой силы кажется
очень неэлегантным способом решения этих проблем, но только так вы получа
ете реальный шанс на успех. Когда дело доходит до записи данных после окон
чания блока памяти, их отслеживание поможет облегчить инструмент PageHeap
из состава AppVerifier, одного из приложений пакета Application Compatibility Toolkit.
Хотя AppVerifier не лишен недостатков, я уверен, что в будущем Microsoft их ис
правит. Наконец, огромную помощь в избавлении от ошибок оказывают новые
ключи компилятора: ключи проверки ошибок в период выполнения и ключ про
верки безопасности буферов.

Г Л А В А

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

В

се мы знаем, что быстродействие — основное требование, предъявляемое к сер
верным приложениям. От скорости их выполнения напрямую зависит масштаби
руемость программы. Мы создаем приложения, которые должны обрабатывать
тысячи и даже миллионы отдельных запросов, при этом любое дополнительное
действие может иметь огромные последствия. Что делает серверные приложения
еще «интереснее», так это то, что они в высокой степени многопоточны, изза чего
причины низкой производительности иногда найти очень трудно.
Писать серверные приложения трудно — отлаживать же трудней вдвое. При
поиске некоторых ошибок — особенно проблем с производительностью — в кли
ентских приложениях мы можем изучать их работу непосредственно. В случае
серверных приложений мы имеем дело с кодом, скрывающимся в самых потаен
ных уголках памяти, поэтому нам остается исследовать его только косвенно, по
времени отклика, а также при помощи отладчиков и таких средств, как PerfMon,
которые указывают только на отдельные аспекты общего состояния программы.
Вообщето все еще хуже: будьте готовы к тому, что никакие нетривиальные ошибки
никогда не обнаружат себя в удобной управляемой среде ваших систем контроля
качества, а покажутся только в джунглях готовой программы.
Для отладки серверных приложений у нас есть старое спасительное сред
ство — трассировка. Это единственный способ, позволяющий увидеть общую кар
тину, особенно при запуске готовой программы в реальных условиях. Мне дово

656

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

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

Фундаментальная проблема и ее решение
Самая крупная проблема с серверными приложениями объясняется тем, что нам,
людям, трудно представить себе множество вещей одновременно. Наш мозг орга
низован линейным образом. Чтобы облегчить отладку серверных приложений, мы
неосознанно организуем вывод трассировочной информации последовательно.
А вот большинство современных серверов являются многопроцессорными, и
многие приложения имеют до 20 и более потоков, так что многие из этих линей
ных процессов на самом деле выполняются параллельно. Пытаясь внести поря
док в кажущийся хаос, мы выводим трассировочные данные всего приложения в
одинединственный файл.
Если результаты трассировки многопоточной программы сохраняются в од
ном файле, вы попадаете в ситуацию, показанную на рис. 181. Именно линейная
регистрация трассировочных вызовов для всех потоков приводит к возникнове
нию узких мест (bottle neck1 ). Как я уже говорил в главе 15, вся суть разработки
многопоточных программ сводится к тому, чтобы потоки были максимально за
няты и как можно меньше простаивали.
Очевидно, что для создания самого быстрого средства трассировки в запад
ном мире требуется такой линейный способ вывода информации, который не влиял
бы на нормальный ход выполнения программы. Если б каждый поток выполнял
собственную трассировку, все было бы прекрасно. Это и делает утилита FastTrace:
она предоставляет каждому потоку отдельный файл вывода, что избавляет пото
ки от перехода в состояние ожидания или блокировки. После сохранения трас
сировочной информации нескольких потоков в файлах журнала вы можете объе
динить файлы и увидеть истинную последовательность трассировки. В разделе
«Реализация FastTrace» вы увидите, что ничего сложного в этом нет.

1

Буквально «бутылочное горлышко». — Прим. пер.

ГЛАВА 18

Поток 1
вызывает
трассировку

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

Поток 2
вызывает
трассировку

Поток 3
вызывает
трассировку

Поток 4
вызывает
трассировку

Поток 5
вызывает
трассировку

657

Поток 6
Поток n
вызывает . . . вызывает
трассировку
трассировку

Баба-а-ах!
Чертово
«бутылочное
горлышко»!
Единственная запись трассировки

Результаты трассировки

Рис. 181.

Типичная система трассировки серверных приложений

Использование FastTrace
Детали реализации FastTrace я скрыл, поэтому для ее использования вам нужно
только скомпоновать свое приложение с библиотекой FASTTRACE.DLL и вызывать
в нужных случаях одну из ее экспортируемых функций, названную, конечно же,
FastTrace. Вот ее прототип:

FASTTRACE_DLLINTERFACE void FastTrace ( LPCTSTR szFmt
...

,
) ;

Трассировочный вывод будет направлен в файлы, котрые хранятся в том же
каталоге, что и файл выполняющегося процесса. Имена файлов имеют формат:

__.FTL
Открыв какойнибудь файл, вы заметите, что информация хранится в двоич
ном виде. Каждая выводимая FastTrace строка сохраняется с соответствующим ей
номером; это нужно для соблюдения правильного порядка строк при объедине
нии файлов. Кроме того, каждая строка может содержать метку времени.
Чтобы сделать FastTrace простой и быстрой, я решил ограничить длину строк
80 символами. При выполнении отладочной компоновки на попытку записи бо
лее длинной строки вам укажет диагностическое сообщение SUPERASSERT. Если вы
хотите использовать более длинные строки, нужно просто переопределить зна
чение MAX_FASTTRACE_LEN в файле FastTrace.H и перекомпоновать FastTrace.
При помощи функции SetFastTraceOptions, принимающей указатель на струк
туру FASTTRACEOPTIONS, вы можете передать FastTrace три команды. Первая команда
позволяет подключить сквозную запись. Это удобно, если вы ищете ошибку и хотите
гарантировать, что вся трассировочная информация записывается в файл как
можно быстрее. Очевидно, что это замедляет быстродействие, поэтому следует
использовать данную функцию только при необходимости. Вторая команда под
ключает создание меток времени для каждой строки. По умолчанию их создание

658

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

отключено с целью небольшого повышения производительности. Вы можете вклю
чать/отключать его когда угодно. Наконец, последняя команда позволяет прика
зать FastTrace вызвать функцию отладочного вывода, прототип которой соответ
ствует функции OutputDebugString. По умолчанию FastTrace не вызывает OutputDebug6
String; как я объяснил в главе 4, эта функция генерирует исключение и будет за
медлять ваше приложение. Однако вам, вероятно, хотелось бы видеть эти исклю
чения при выполнении отладочных компоновок, поэтому я предоставляю и та
кую возможность.
Наконец, я реализовал две функции, предназначенные для управления трасси
ровкой. Первая из них — FlushFastTraceFiles — как можно догадаться по ее име
ни, записывает все буферы FastTrace на диск. Возможно, вам захочется создать фо
новый поток, следящий за работой серверного приложения и периодически «сбра
сывающий» трассировочную информацию на диск в те моменты, когда оно не
занято. Так вы сможете гарантировать, что нужная информация будет в файлах
даже при аварийном завершении приложения.
Вторая называется SnapFastTraceFiles. Серверные приложения не имеют точ
но определенной точки завершения, поэтому вам нужен способ, позволяющий
изучать трассировочную информацию в любое время. SnapFastTraceFiles закры
вает все открытые файлы трассировки и переименовывает их, чтобы можно было
определить соответствующие «снимки». Переименование файлов происходит
согласно такой схеме:

SNAP____.FTL
Номер снимка представляет собой уникальное для каждого процесса десятич
ное число, благодаря чему вы сможете увидеть всю цепь своих снимков. После
закрытия и переименования всех активных файлов трассировки SnapFastTraceFiles
снова открывает файлы для каждого потока, чтобы не пропустить никакой инфор
мации. Как и в случае с FlushFastTraceFiles, вам, вероятно, захочется реализовать
возможность вызова SnapFastTraceFiles в нужный вам момент. Помните: для защиты
внутренних структур данных и SnapFastTraceFiles, и FlushFastTraceFiles использу
ют критическую секцию, что приводит к временной блокировке всех трассиро
вочных вызовов.

Объединение журналов трассировки
Как я уже говорил, файлы журнала имеют двоичный формат, поэтому для изуче
ния отдельных файлов или их слияния с целью исследования линейного потока
трассировочных вызовов потребуется программа FastTraceLog.EXE. Для вывода
дампа журнала на экран нужно только передать ей в командной строке выраже
ние –d . В результате вы увидите порядковый номер,
дату/время (если было включено создание меток времени) и сохраненную в ука
занный момент строку трассировочной информации.
Слияние, или объединение файлов журнала чуть сложнее. Для этого FastTrace
Log.EXE нужно передать в командной строке выражение –c __. Например, выпол
нив тестовую программу FTSimpTest.EXE, вы увидите, что она генерирует файлы
трассировки с такими именами:

ГЛАВА 18

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

659

FTSimpTest_2720_0400.FTL
FTSimpTest_2720_1644.FTL
FTSimpTest_2720_2332.FTL
FTSimpTest_2720_2368.FTL
FTSimpTest_2720_2424.FTL
FTSimpTest_2720_2560.FTL
FTSimpTest_2720_2584.FTL
FTSimpTest_2720_2640.FTL
FTSimpTest_2720_2688.FTL
Для объединения этих файлов отдельного потока нужно выполнить команду:

FastTraceLog.exe 6c FTSimpTest_2720
В итоге вы получите текстовый файл — в нашем случае с именем FTSimp
Test_2720.TXT. Информация в текстовом файле очень похожа на дамп, и в приве
денном ниже фрагменте вы можете увидеть, что до последней строки создание
меток времени было включено, а перед ней отключено:

[0x0B3C 57 1/1/2003 17:52:47:205]
Hello from CThread 6> 3!
[0x0B50 58 1/1/2003 17:52:47:205]
Hello from CThread 6> 3!
[0x0B50 59 1/1/2003 17:52:47:486]
Hello from CThread 6> 4!
[0x0B20 60 1/1/2003 17:52:47:486]
Hello from CThread 6> 4!
[0x0B20 61 1/1/2003 17:52:47:767]
Hello from CThread 6> 5!
[0x0830 62]
THIS SHOULD BE THE LAST LINE!
В квадратных скобках выводится идентификатор сгенерировавшего сообщение
потока, порядковый номер и необязательная метка даты/времени записи сообще
ния, а на следующей строке — само трассировочное сообщение. Я с гордостью
сообщаю, что сделал метку даты/времени интернациональной, благодаря чему вы
увидите дату в том формате, к которому привыкли!

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

660

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

кретной строкой трассировочной информации. Сначала я хотел реализовать
поддержку строк различной длины, но потом решил, что не стоит замедлять трас
сировку, подсчитывая при каждой трассировочной команде число символов в
строке. Кроме того, читать строки переменной длины было бы гораздо сложнее.
Второе интересное решение было связано с форматом хранения меток вре
мени. Если вы когданибудь заглядывали в справочный раздел, описывающий их
хранение в ОС, то знаете, что количество форматов просто огромно. Изучив их
подробнее, я решил использовать формат FILETIME, потому что он занимает толь
ко 8 байт, в то время как формат SYSTEMTIME требует 16 байт. Кроме того, я рас
смотрел их обработку и обнаружил, что самый быстрый способ получения вре
мени обеспечивает функция GetSystemTimeAsFileTime.

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

Г Л А В А

19
Утилита Smooth Working Set

К

ак утверждалось в фильме «Годзилла» и как вы сами можете убедиться по объе
му спама в вашем почтовом ящике, «размер имеет значение». У всех разработчи
ков просто слюнки текут при мысли о более быстрых компьютерах с большим
объемом памяти, но даже в этом случае нам нужно беспокоиться о размере на
ших программ. Старая пословица «компактный код — хороший код» ничуть не
утратила своей актуальности в современном мире, в котором оперативная память
компьютеров разработчиков часто превышает 512 Мб, а серверов — 2 Гб. Если
объем памяти, имеющейся в вашем распоряжении, кажется вам бесконечным, это
не значит, что ее нужно использовать полностью!
После устранения явных проблем с производительностью программы, таких
с точностью до миллиардного разряда, на
как циклическое вычисление числа
ступает самое время позаботиться о минимизации рабочего набора. Рабочий
набор — это объем памяти, выделенной для выполняемых в текущий момент блоков
вашей программы. Чем меньше вы сделаете рабочий набор, тем быстрее будет
работать ваше приложение благодаря уменьшению числа ошибок страниц. Ошибка
страницы происходит при доступе к блоку вашей программы, который или не
находится в кэшпамяти (мягкая ошибка страницы), или не находится в памяти
вообще и должен быть загружен с жесткого диска (жесткая ошибка страницы). Как
мне однажды сказал один мудрый человек, «ошибки страниц способны испортить
вам весь день!».
Объем текущего рабочего набора своей программы вы можете увидеть в столбце
Mem Usage (память), выбрав в окне Task Manager (диспетчер задач) вкладку Processes
(процессы). Сведения о рабочем наборе показывают также многие другие диаг
ностические и информационные средства, такие как PerfMon. Убедившись, что ваши
алгоритмы экономно расходуют память, вам следует попытаться уменьшить объем
памяти, занимаемой самим кодом. В начале этой главы я подробнее опишу ошибки
страниц во время выполнения программы и объясню, почему они так нежелатель

π

662

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

ны. После этого, как вы уже, наверное, догадались, я представлю программу Smooth
Working Set (SWS), которая сделает оптимизацию рабочего набора тривиальной.

Оптимизация рабочего набора
Вероятно, вы всегда интуитивно понимали, пусть и не пытаясь вдаваться в под
робности, что при компиляции и компоновке двоичных файлов компоновщик
просто размещает в итоговом файле сначала функции первого OBJфайла, потом
второго и т. д. Иначе говоря, функции размещаются в порядке компоновки, а не в
порядке выполнения. Однако с данным процессом связана одна проблема: при этом
не принимается во внимание расположение чаще всего вызываемых функций.
На рис. 191 — пример того, что может произойти: ОС поддерживает только
шесть функций фиксированного размера на страницу памяти и в любой момент
времени может хранить в памяти только четыре страницы; кроме того, 10 вызы
ваемых функций — самые часто вызываемые функции программы.
При загрузке этой программы в память ОС, не долго думая, загружает четыре
первых страницы двоичного файла. В ходе выполнения первая функция вызыва
ет функцию 2, которая по стечению обстоятельств располагается на той же стра
нице. Однако функция 2 вызывает функцию 3, которая находится на странице 5.
Так как страницы 5 в памяти нет, происходит ошибка страницы. Теперь ОС долж
на выгрузить одну из загруженных страниц. Так как страница 4 не изменялась, ОС
решает выгрузить ее и загружает на ее место страницу 5. Теперь можно выпол
нить функцию 3. Увы, функция 3 вызывает функцию 4, находящуюся на только
что выгруженной странице 4, поэтому у нас происходит вторая ошибка страни
цы. ОС выгружает страницу 3, к которой дольше всего не было обращений, воз
вращает в память страницу 4 и выполняет функцию 4. Как вы можете видеть, пос
ле этого произойдет еще одна ошибка страницы, поскольку функция 4 вызывает
функцию 5 — только что выгруженную. Я мог бы продолжить этот процесс даль
ше, но уже и так ясно, что при ошибках страниц происходит очень много работы.
Главное, что обработка ошибок страниц требует массу времени. Программа на
рис. 191 вместо выполнения «отсиживается» в коде ОС. Если бы мы могли ука
зать компоновщику порядок размещения функций на страницах, мы избежали бы
многих дополнительных затрат, связанных с обработкой этих ошибок.
На рис. 192 показана та же программа после перемещения самых часто ис
пользуемых функций в начало двоичного файла. Вся программа занимает тот же
объем памяти (4 страницы), но благодаря объединению часто вызываемых фун
кций они не приводят ни к каким ошибкам страниц, в результате чего приложе
ние будет работать быстрее.
Процесс обнаружения и упорядочения наиболее часто вызываемых функций
называется оптимизацией рабочего набора и состоит из двух этапов. На первом
нужно определить, какие функции вызываются чаще всего, а на втором компо
новщику нужно задать порядок размещения функций в файле, чтобы все они были
расположены соответствующим образом.

ГЛАВА 19

Страница 1

Утилита Smooth Working Set

663

Страница 5
Fn 3

Запуск Fn

Fn 1

Fn 2

Fn 7

Страница 2

Страница 6
Fn 6

Fn 10

Страница 3

Страница 7

Fn 5

Fn 8

Страница 4

Страница 8

Fn 4
Fn 9

Рис. 191.

Неоптимизированная система

Вся работа, связанная с подсчетом числа вызовов функций, скрыта в SWS; под
робнее о необходимых для этого действиях см. раздел «Работа с SWS». Указать
компоновщику порядок размещения функций проще простого. Для этого LINK.EXE
поддерживает ключ командной строки /ORDER. В качестве параметра после ключа
/ORDER просто указывается текстовый файл, называемый файлом порядка, в кото
ром нужно привести список всех функций в желательном для вас порядке. Инте
ресно, что имена всех функций должны быть указаны в файле порядка в полном,
расширенном виде. Идея, лежащая в основе SWS, по сути заключается в создании
файла порядка для вашей программы.
Возможно, у тех из вас, кто давно работает с Microsoft Windows, гдето в со
знании сейчас замаячили буквы WST. WST означает Working Set Tuner (средство
настройки рабочего набора). Эту программу Microsoft распространяла вместе с
Platform SDK для… хм, для настройки рабочего набора. Однако в связи с этим вам
могут прийти в голову и три других факта: вопервых, утилиту WST было очень
сложно использовать, вовторых, в ней было несколько ошибок, и втретьих, она

664

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

больше не входит в состав Platform SDK. SWS — гораздо более простая в исполь
зовании замена WST.
Страница 1

Страница 5

Fn 1
Fn 2
Fn 3
Fn 4
Fn 5
Fn 6
Страница 2

Страница 6

Fn 7
Fn 8
Fn 9
Fn 10

Страница 3

Страница 7

Страница 4

Страница 8

Рис. 192.

Оптимизированная система

Если вам хочется больше узнать о настройке рабочего набора и получить пре
красное пособие по производительности Windows, попытайтесь найти четвертый
том книги «Microsoft Windows NT 3.51 Resource Kit» под названием «Optimizing
Windows NT» (оптимизация Windows NT) (Microsoft Press, 1995). Этот том, напи
санный Рассом Блейком (Russ Blake), представляет собой великолепное введение
в указанную тему. В группе разработчиков Microsoft Windows NT Расс отвечал за
настройку производительности системы. Эта книга раньше была в MSDN, но вне
запно исчезла. Утилиту WST также разрабатывала группа Расса, и в своей книге
он утверждает, что после использования программы оптимизации рабочего на
бора вы должны достигнуть его уменьшения на 35–50%. Каждый раз, когда ваше
приложение может сбросить такой вес, эту возможность определенно стоит рас
смотреть повнимательней!

ГЛАВА 19

Утилита Smooth Working Set

665

Стандартный вопрос отладки
Могу ли я проверить процесс, выполняемый на главном сервере,
на предмет утечки ресурсов без установки каких-либо программ?
Для выполнения быстрой проверки всегда подойдет PerfMon, однако нет ни
чего лучше, чем удивительный диспетчер задач. Открыв вкладку Processes,
вы увидите столбец, отображающий различную информацию о производи
тельности. Так как работа с ресурсами основана на дескрипторах, следует
отслеживать число дескрипторов процесса. Откройте вкладку Processes, вы
берите в меню View (вид) пункт Select Columns (выбрать столбцы) и уста
новите в диалоговом окне Select Columns (выбор столбцов) флажок Handle
Count (счетчик дескрипторов). Если вы хотите следить за объектами GDI,
установите также флажок GDI Objects (объекты GDI). Оба столбца, добав
ленных в диспетчер задач и отсортированных по дескрипторам, показаны
на рис. 193.

Рис. 193. Отображение дескрипторов и объектов GDI
в диспетчере задач
По умолчанию диспетчер задач автоматически обновляет информацию
каждые несколько секунд, но при этом вам нужно непрерывно следить за
числом дескрипторов и объектов GDI, чтобы увидеть, когда оно начинает
увеличиваться. Мне нравится приостанавливать обновление путем выбора
пункта Paused (приостановить) в подменю Update Speed (скорость обнов
ления) меню View. Благодаря этому я могу выполнить какуюлибо опера
см. след. стр.

666

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

цию, вернуться в диспетчер задач и нажать F5 для обновления экрана. На
блюдайте и за столбцом Mem Usage (память), потому что числу дескрипто
ров соответствует определенный объем памяти: если оба значения растут,
утечка ресурсов в самом разгаре.
Прежде чем описать работу с SWS, я должен вернуть вас на землю. Вопервых,
SWS — не панацея. Если до запуска SWS быстродействие вашей программы было
ужасным, таким оно и останется. Вовторых, оптимизацию рабочего набора сле
дует выполнять в самом конце работы над производительностью приложения, после
улучшения его алгоритмов и исправления всех найденных проблем с быстродей
ствием. Наконец, использовать SWS следует только в конце цикла разработки.
Максимальное преимущество SWS обеспечит, когда изменения кода приближаются
к минимуму.
SWS может и не понадобиться. Разрабатывая небольшую программу, откомпи
лированные файлы которой занимают не более 2–3 Мб, вы можете вообще не
заметить уменьшения рабочего набора. Максимальной выгоды от оптимизации
рабочего набора вы добьетесь, работая над более крупными приложениями, по
тому что чем больше страниц требуется программе, тем больше возможностей для
ее улучшения.

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

Настройка компиляндов SWS
Необходимость отдельной компиляции программы для использования SWS объ
ясняется тем, что в основе SWS лежат те же принципы, что и в WST. Хотя я мог
написать огромную, сложную и подверженную ошибкам программу для получе
ния информации о функциях вашего приложения «на лету», гораздо проще пре
доставить установку функцииловушки компилятору. Ключ /Gh (включить функ
циюловушку _penter) приказывает компилятору разместить в начале пролога всех
генерируемых функций вызов функции _penter. В SWS функция _penter уже реа
лизована; чтобы она могла продемонстрировать все свои магические возможно
сти, вы должны скомпоновать с ней свою программу.
Скомпилировать программу для использования с SWS обычно просто — для
этого надо только коечто сделать.
1. Прежде всего убедитесь в правильности параметров заключительной компо
новки своего проекта, так как эта конфигурация будет клонирована. Если вы

ГЛАВА 19

Утилита Smooth Working Set

667

установили при помощи надстройки SettingsMaster из главы 9 ключи компи
лятора и компоновщика, которые я рекомендовал в главе 2, у вас все в порядке.
2. Клонируйте конфигурацию, с которой вы хотите работать. Нажмите правой
кнопкой название решения в окне Solution Explorer (проводник решений) и
выберите в меню пункт Configuration Manager (менеджер конфигураций), после
чего появится одноименное диалоговое окно. Новый конфигурационный па
раметр скрывается в списке Active Solution Configuration (активная конфигу
рация решения). Доступ к нужному нам полю показан на рис. 194 (я
искал его довольно долго). Как правило, следует клонировать заключительные
компоновки. Открыв диалоговое окно New Solution Configuration (конфигура
ция нового решения), дважды убедитесь в том, что вы выбрали нужное значе
ние в списке Copy Settings From (копировать параметры из), потому что, если
вы будете наследовать решение от , ваша конфигурация компоновки
не будут перенесена. Называя новый проект, я предпочитаю указывать тип ком
поновки и буквы «SWS», в результате чего получается имя ReleaseSWS. Диало
говое окно New Solution Configuration с этими новыми параметрами показано
на рис. 195. Помните, что при создании новых решений каталог вывода и про
межуточный каталог изменяются.

Рис. 194. Выбор новой конфигурации при помощи
окна Configuration Manager

Рис. 195.

Создание новой конфигурации Release$SWS

3. Если вы пользуетесь утилитой SettingsMaster (как и следует делать), настройка
компилятора и компоновщика тривиальна. Нажмите кнопку SettingsMaster

668

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Custom Project Update (обновление проекта) и, когда появится приглашение,
выберите файл ReleaseSWS.XML. Это установит минимально необходимые
параметры. Чтобы использовать SWS с отладочной компоновкой, выберите файл
DebugSWS.XML (оба файла находятся в каталоге SettingsMaster\SettingsMaster).
4. Если вы не используете SettingsMaster (позор!), вам нужно установить для проекта
ReleaseSWS следующие параметры компилятора (CL.EXE) (см. по этому пово
ду главу 2):
쐽 /Zi (формат отладочной информации);
쐽 /Gy (включить компоновку на уровне функций);
쐽 /Gh (включить функциюловушку _penter); стандартного способа установки
этого ключа нет, поэтому вам придется выбрать в папке C/C++ пункт Com
mand Line (командная строка) и ввести его вручную.
Кроме того, для проекта ReleaseSWS нужно задать ключи компоновщика
(LINK.EXE):
쐽 /DEBUG (генерировать отладочную информацию);
쐽 /OPT:REF (оптимизация: удалять неиспользуемые функции).
5. Добавьте SWSDLL.LIB в список зависимостей (dependencies).
Чтобы сконфигурировать проект DebugSWS, установите ключ /Gh для компи
лятора CL.EXE и добавьте зависимость от SWSDLL.LIB.

Выполнение приложений вместе с SWS
После настройки и компиляции приложения наступит самый трудный для вас этап
работы с SWS — выполнение вашей программы. Вам придется самым тщательным
образом обдумать, каковы наиболее частые сценарии использования вашего при
ложения. Если вы оптимизируете готовую программу, вам, вероятно, следует по
сетить своих клиентов, чтобы увидеть, что они чаще всего с ней делают. Если же
вы работаете над новым приложением, обсудите это с сотрудниками службы мар
кетинга или представителями заказчика. Определив сценарии, их нужно будет
выполнить на разных машинах с различной загрузкой. Скорее всего вам захочет
ся сделать эти сценарии воспроизводимыми при помощи такой утилиты автома
тизации, как Tester из главы 16.
Запустив программу вместе с SWS, вы обнаружите два новых файла в каталоге,
в котором находится каждый двоичный файл, скомпилированный для SWS: .SWS и .1.SWS. Файл SWS без цифры — это базовый файл, со
держащий адреса и размеры каждой функции модуля, а также пространство для
счетчиков числа вызовов. При каждом запуске вашей программы этот файл ко
пируется в новый файл, чтобы избежать повторного просмотра символов. Файлы
с цифрой соответствуют запускам программы и содержат счетчики вызовов. При
каждом выполнении вашего специального откомпилированного двоичного фай
ла создается новый — .#.SWS.
Нужные компоненты SWS находятся в исполняемом файле SWS.EXE. Эта про
грамма позволяет просматривать отдельные SWSфайлы, создавать новые файлы
и выполнять финальную оптимизацию. Запустив SWS.EXE без всяких параметров
или с ключом 6?, вы увидите:

ГЛАВА 19

Утилита Smooth Working Set

669

SWS (Smooth Working Set) 2.0
John Robbins 6 Debugging Applications for Microsoft .NET and Microsoft Windows
Usage: SWS [6t ]|[6d ]|[6g ]|[6?] [6nologo]
6t 6 Tune the module's working set
(run from directory with .SWS file)
6d 6 Dump the raw data for the module or #.SWS file
6g 6 Generate the initial SWS file for the module
6?
6 Show this help screen
6nologo
6 Do not display the program information
Чтобы увидеть, какие именно функции вызываются, вы можете просматривать
все итоговые файлы .SWS; это позволяет гарантировать, что вы выполняете имен
но те функции, которые собирались. Вывод файла, соответствующего запуску ва
шей программы (.#.SWS), имеет формат:

Link time
Entry count
Image base
Image size
Module name

:
:
:
:
:

0x3E13849C
12
0x00400000
0x00007000
SimpleSWSTest.exe

Address
Count
Size Name
————— ———— —— ——
0x00401050
2
22 ?Bar@@YAXXZ
0x00401066
2
22 ?Baz@@YAXXZ
0x0040107C
2
22 ?Bop@@YAXXZ
0x00401092
4
10 ?Foo@@YAXXZ
0x0040109C
1
49 _wmain
0x004010CD
2
10 _YeOlCFunc
0x004011E0
0 422 _wmainCRTStartup
0x00401C50
0
63 __onexit
0x00401C90
0
24 _atexit
0x00401DC0
0
23 __setdefaultprecision
0x00401DE0
0
7 __matherr
0x00401DF0
0
7 __wsetargv
При запуске вашей программы, использующей функцию _penter утилиты SWS,
первоначальный файл .SWS генерируется автоматически. Однако это требует зна
чительного объема работы с символьной машиной DBGHELP.DLL, поэтому вы
сможете уменьшить время запуска, если будете предварительно генерировать SWS
файлы для модуля, указывая программе SWS.EXE ключ 6g.

Генерирование и использование файла порядка
После завершения всех запусков вашей программы вам нужно оптимизировать ее
и сгенерировать файл порядка, который будет передан компоновщику. Для этого
SWS.EXE предоставляет ключ командной строки 6t, после которого следует про
сто указать имя модуля оптимизируемого двоичного файла. В результате оптими
зации создается действительный файл порядка с расширением .PRF; я выбрал это
расширение потому, что такие файлы генерировала программа Working Set Tuner.

670

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

Если вам хочется увидеть, что происходит при создании файла порядка, т. е. сам
процесс «упаковки» и упорядочения функций, укажите SWS в командной строке
ключ 6v. Понять подробный вывод легко:

Verbose output turned on
Action 6> Tuning for : SimpleSWSTest
Initializing the symbol engine.
Loading the symbols
Processing : SimpleSWSTest.1.SWS
Processing : SimpleSWSTest.2.SWS
Processing : SimpleSWSTest.3.SWS
Processing : SimpleSWSTest.4.SWS
Processing : SimpleSWSTest.5.SWS
Order file output: SimpleSWSTest.PRF
Page Remaining (4086) (0010) : ( 20)
Page Remaining (4064) (0022) : ( 10)
Page Remaining (4042) (0022) : ( 10)
Page Remaining (4032) (0010) : ( 10)
Page Remaining (4010) (0022) : ( 10)
Page Remaining (3961) (0049) : (
5)

?Foo@@YAXXZ
?Bop@@YAXXZ
?Baz@@YAXXZ
_YeOlCFunc
?Bar@@YAXXZ
_wmain

Числа, которые вы видите после слов «Page Remaining», указывают на оставшееся
пространство страницы, размер функции и число ее вызовов.
При создании файла .PRF утилита SWS генерирует не просто текстовый лис
тинг функций в порядке убывания числа вызовов, так как это не учитывало бы
размера страницы ОС. Если функция пересекает границу страницы, то для ее
выполнения потребуется загрузить в память две страницы. SWS гарантирует, что
во всех случаях функция будет размещаться на одной странице, а также пытается
обеспечить максимальную заполненность каждой страницы. Именно поэтому вы
можете встретить в начале итогового файла порядка редко вызываемые функции.
Избегая пустот на страницах, вы снизите объем оперативной памяти, нужный вашей
программе.

Рис. 196. Указание файла порядка для отладочной компоновки при помощи
ключа /ORDER

ГЛАВА 19

Утилита Smooth Working Set

671

После создания файла порядка нужно просто указать его компоновщику при
помощи ключа /ORDER. Всегда сохраняйте файл порядка в том же каталоге, что и
файл проекта и исходные файлы. Ключ /ORDER имеет одну особенность, которая
часто вызывает замешательство: перед именем файла должен быть указан символ
@. Если вы не используете SettingsMaster, вы можете указать файл порядка для
двоичного файла своей заключительной компоновки, открыв окно Property Pages
(страницы свойств), папку Linker (компоновщик), страницу Optimization и введя
нужное выражение в поле Function Order (порядок функций) (рис. 196). При
использовании SettingsMaster просто нажмите кнопку SettingsMaster Custom Project
Update и выберите файл ReleaseOrderFile.XML.

Реализация SWS
Как это бывает со многими проектами, когда я впервые подумал о реализации SWS,
этопредставлялось довольно сложным делом, однако при написании кода все
оказалось проще. Я планировал сначала разобраться с функцией _penter, после это
го — с двоичными файлами и перечислением символов и, наконец, — с перио
дом выполнения и алгоритмом оптимизации.

Функция _penter
Должен признать, что до того, как я решил использовать функцию _penter при
помощи ключа компилятора /Gh, я хотел заставить SWS работать с немодифици
рованными двоичными файлами. Конечно, это возможно, но довольно затрудни
тельно: вопервых, чтобы гарантировать включение кода ловушки в первую оче
редь, мне нужно было написать отладчик, а вовторых, я должен был включить 5
байтовые переходы для всех функций, обнаруживаемых в таблице символов. У меня
был опыт разработки похожего кода для коммерческих программ, так что я знал,
насколько это сложно. В конце концов я счел специальную компоновку вполне
приемлемой, особенно когда у нас есть такие средства, как SettingsMaster, благо
даря которым добавить в конфигурацию компоновки SWS совсем легко.
Разработав установку ловушки, я приступил к рассмотрению действий, кото
рые нужно выполнять при каждом вызове _penter. Моя функция _penter и ее вспо
могательный код приведены в листинге 191. Как вы можете видеть, она исполь
зует соглашение вызова naked, поэтому я сам генерирую пролог и эпилог. Согла
шение naked требуется в документации к _penter; кроме того, это облегчает полу
чение адреса возврата из функции. К счастью, когда компилятор обещает, что он
сгенерирует вызов _penter до всех остальных команд, он держит свое слово! Ре
зультат установки ключа /Gh показан в дизассемблированном фрагменте, из ко
торого видно, что вызов _penter выполняется даже до команды PUSH EBP, одного
из элементов стандартного пролога функции:

Bar:
00401050:
00401055:
00401056:
00401058:
0040105D:

E8B7000000
55
8BEC
E8A8FFFFFF
3BEC

call
push
mov
call
cmp

_penter
ebp
ebp,esp
ILT+0(?Foo
ebp,esp

672

ЧАСТЬ IV

0040105F: E8AE000000
00401064: 5D
00401065: C3

Мощные средства и методы отладки неуправляемого кода

call
pop
ret

_chkesp
ebp

Немного пофантазировав, вы поймете, что ключ /Gh позволяет создать неко
торые другие интересные утилиты. Первая, что пришла мне в голову, — это ути
лита контроля производительности. Благодаря тому, что Microsoft уже предлага
ет нам ключ /GH (включить функциюловушку _pexit), использовать такое средство
будет гораздо проще, так как вы будете знать момент окончания функции. Сове
тую вам получше обдумать силу функций _penter и _pexit.

Стандартный вопрос отладки
Почему в окне Disassembly вызовы некоторых функций
начинаются с «ILT»?
В отрывке, сгенерированном ключом /Gh, вы видели вызов функции CALL
ILT+0(?Foo, что многих прогhраммистов приводит в недоумение. Подобные
вызовы свидетельствуют о магической компоновке с приращением в дей
ствии. Аббревиатура ILT означает Incremental Link Table (таблица компоновки
с приращением).
При создании отладочной компоновки компоновщик хочет скомпоно
вать двоичный файл как можно быстрее. Для этого он добавляет в каждую
функцию довольно много пустого места, благодаря чему при изменении
функции ему нужно только перезаписать функцию, не перемещая код в
двоичном файле. Между прочим, это пустое пространство заполняют коман
ды INT 3. Однако размер функции может превысить объем выделенного места.
В этом случае компоновщик должен переместить функцию в другое место
двоичного файла.
Если б каждая функция, вызывающая перемещенную функцию, делала
это при помощи ее действительного адреса, то при каждом перемещении
функции в результате новой компоновки компоновщик должен был бы
искать и обновлять все команды CALL. Поэтому для экономии времени ком
поновщик создает специальную таблицу компоновки с приращением, ко
торую использует для всех вызовов. Теперь при изменении функции ком
поновщик должен обновить в двоичном файле только одно выражение.
Таблица ILT показана в листинге 191.

@ILT+0(_wmain):
00401005 jmp
wmain (401070h)
@ILT+5(??_GCResString@@UAEPAXI@Z):
0040100A jmp
CResString::'scalar deleting destructor' (401B40h)
@ILT+10(?ParseCommandLine@@YAHHQAPAGAAUtag_CMDOPTS@@@Z):
0040100F jmp
ParseCommandLine (401439h)
@ILT+15(?ShowHelp@@YAXXZ):
00401014 jmp
ShowHelp (401644h)
@ILT+20(??1CResString@@UAE@XZ):
00401019 jmp
CResString::~CResString (401A00h)
@ILT+25(?LoadStringW@CResString@@QAEPBGI@Z):
0040101E jmp
CResString::LoadStringW (401A30h)

ГЛАВА 19

Утилита Smooth Working Set

673

@ILT+30(??2@YAPAXI@Z):
00401023 jmp
operator new (401B90h)
@ILT+35(??_GCResString@@UAEPAXI@Z):
00401028 jmp
CResString::'scalar deleting destructor' (401B40h)
@ILT+40(??0CResString@@QAE@PAUHINSTANCE__@@@Z):
0040102D jmp
CResString::CResString (401990h)
@ILT+45(??BCResString@@QBEPBGXZ):
00401032 jmp
CResString::operator unsigned short const * (401B20h)
Следовательно, команда CALL @ILT+15(?ShowHelp@@YAXXZ) на самом деле
вызывает переход к метке @ILT+15(?ShowHelp@@YAXXZ), которая представляет
собой переход к реальной команде.

Листинг 19-1.

PENTERHOOK.CPP

/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright © 199762003 John Robbins — All rights reserved.
——————————————————————————————————————————————————————————————————————*/
/*//////////////////////////////////////////////////////////////////////
Включение заголовочных файлов
//////////////////////////////////////////////////////////////////////*/
#include "stdafx.h"
#include "SWSDLL.h"
#include "SymbolEngine.h"
#include "VerboseMacros.h"
#include "ModuleItemArray.h"
/*//////////////////////////////////////////////////////////////////////
Определения, константы, объявления typedef
//////////////////////////////////////////////////////////////////////*/
// Описатель события, который функция _penter проверяет,
// чтобы узнать, не запрещена ли обработка.
static HANDLE g_hStartStopEvent = NULL ;
// Простой автоматический класс 6 я использую
// его для разного рода инициализации.
class CAutoMatic
{
public :
CAutoMatic ( void )
{
g_hStartStopEvent = ::CreateEvent ( NULL
,
TRUE
,
FALSE
,
SWS_STOPSTART_EVENT ) ;
ASSERT ( NULL != g_hStartStopEvent ) ;
}
~CAutoMatic ( void )
{
VERIFY ( ::CloseHandle ( g_hStartStopEvent ) ) ;
см. след. стр.

674

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

}
} ;
/*//////////////////////////////////////////////////////////////////////
Глобальные объекты с областью видимости файл
//////////////////////////////////////////////////////////////////////*/
// Автоматический класс.
static CAutoMatic g_cAuto ;
// Массив модулей.
static CModuleItemArray g_cModArray ;

/*//////////////////////////////////////////////////////////////////////
Прототипы функций
//////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////*/
extern "C" void SWSDLL_DLLINTERFACE __declspec(naked) _penter ( void )
{
DWORD_PTR dwCallerFunc ;
// Пролог функции.
__asm
{
PUSH EBP
MOV EBP , ESP
PUSH EAX
MOV EAX , ESP

// Создание стандартного кадра стека.

//
//
//
//

Сохранение EAX, так как он нужен
мне до сохранения всех регистров.
Загрузка текущего указателя стека
в регистр EAX.

SUB ESP , __LOCAL_SIZE

// Резервирование пространства
// для локальных переменных.

PUSHAD

// Сохранение значений всех
// регистров общего назначения.

// Теперь я могу вычислить адрес возврата.
ADD EAX , 04h + 04h
// Нужно учесть команды PUSH EBP
// и PUSH EAX.
MOV EAX , [EAX]
// Получение адреса возврата.
SUB EAX , 5
// Чтобы определить начало функции,
// надо вычесть 56байтовый переход,
// использованный для вызова _penter.
MOV [dwCallerFunc] , EAX // Сохранение нового адреса возврата.

ГЛАВА 19

Утилита Smooth Working Set

675

}
// Если событие начала/завершения находится
// в сигнальном состоянии, ничего не делаем.
if ( WAIT_TIMEOUT == WaitForSingleObject ( g_hStartStopEvent , 0 ))
{
// Выполняем всю работу.
g_cModArray.IncrementFunctionEntry ( dwCallerFunc ) ;
}
// Эпилог функции.
__asm
{
POPAD
// Восстановление всех регистров
// общего назначения.
ADD ESP , __LOCAL_SIZE

// Удаление пространства, выделенного
// для локальных переменных.

POP EAX

// Восстановление регистра EAX.

MOV ESP , EBP
POP EBP
RET

// Восстановление кадра стека.
// Возврат в вызвавшую функцию.

}
}

Формат файла .SWS и перечисление символов
Как показывает листинг 191, в _penter ничего удивительного. Все становится
интереснее, когда дело касается организации адресов функций. Так как мне нуж
но связать адрес с именем функции, то в некоторых местах программы я прибе
гаю к услугам своего старого друга — сервера символов DBGHELP.DLL. Однако про
смотр символов при помощи сервера символов — не самая быстрая операция, а
доступ к данным нужен при каждом вызове функции, поэтому я должен был най
ти компактный и быстрый способ его выполнения.
Размышляя об этом, я захотел упорядочить данные при помощи отсортиро
ванного массива всех адресов функций с соответствующими им счетчиками вы
зовов. В этом случае, получив адрес возврата в _penter, я мог бы просто выпол
нить для него быстрый двоичный поиск. Такое решение казалось относительно
простым, потому что оно требовало только перечисления символов модулей и
сортировки массива функций. Все данные для этого у меня имелись.
Я решил, что SWS подобно WST должна хранить счетчики вызовов для каждо
го запуска каждого модуля в отдельном файле данных. Я предпочел этот подход,
потому что он позволяет удалить информацию о конкретном запуске приложе
ния из объединенного набора данных, если она вам не нужна. WST использует для
наименования файлов формат ., но я хотел, чтобы
SWS поддерживала схему ..SWS, чтобы я мог в конеч

676

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

ном счете написать графическую программу, облегчающую объединение данных
обо всех запусках.
Выбрав способ обработки данных в период выполнения, я приступил к рас
смотрению создания файла порядка. Как я уже говорил, мне нужен был способ
объединения информации об отдельных запусках. Однако при размышлении над
фактическим созданием файла порядка я понял, что у меня нет некоторых дан
ных. Файл порядка должен содержать имена функций, а также их размеры, в то
время как я планировал хранить только адреса функций. Хотя я опять мог бы
использовать символьную машину при генерировании файла порядка, единствен
ный способ получения размера символа — перечисление всех символов модуля.
Так как я уже выполнял полное перечисление символов на первоначальных эта
пах генерирования данных, я решил, что мне следует просто добавить в файл
размеры функций. Я не нуждаюсь в хранении имен функций, потому что их все
гда можно узнать, загрузив PDBфайл для двоичного файла.
Если вы все же нашли книгу Расса Блейка «Optimizing Windows NT» и прочи
тали главу «Tuning the Working Set of Your Application» (настройка рабочего набо
ра программы), вас, вероятно, интересует, почему я ничего не говорю о наборах
битов и интервалах времени. Группа, работавшая над производительностью Win
dows NT, использовала при создании WST схему, в которой каждой функции со
ответствует один бит из набора. Каждые столькото секунд WST регистрирует при
помощи этого набора битов функции, выполненные за прошедший интервал вре
мени. Меня часто удивляет, почему они реализовали WST именно так. На первый
взгляд, набор битов позволяет сэкономить память, но при этом нужно помнить,
что должен быть реализован некоторый способ отображения битов и адресов
функций. Не думаю, что такая схема экономит намного больше памяти, чем мой
метод. Мне кажется, что программисты, работавшие над производительностью
Windows NT, использовали набор битов потому, что оптимизировали при помо
щи WST целую ОС. Я же, напротив, работаю с отдельными двоичными файлами,
так что это вопрос масштаба.
Разрабатывая структуры данных, я был озабочен одним моментом: при вызо
ве функции я просто хотел увеличивать счетчик. В многопоточных программах я
должен защищать это значение, чтобы в каждый конкретный момент времени им
мог манипулировать только один поток. Я хотел сделать SWS как можно более
быстрой, поэтому увеличение счетчика вызовов функций лучше всего выполнять
при помощи APIфункции InterlockedIncrement. Так как она использует аппарат
ный механизм блокировки (префикс команды LOCK), то гарантирует согласован
ность данных в многопоточных приложениях. Однако в Microsoft Win32 наиболь
шим числом, которое можно передать в InterlockedIncrement, является 32разряд
ное значение, в связи с чем возникает проблема с превышением 4 294 967 295 вы
зовов функций. Четыре миллиарда — много, но и этого может не хватить для не
которых циклов сообщений при долгом выполнении приложения.
Для решения этой проблемы программа WST в период выполнения только
записывала вызовы функций, а общее их число подсчитывалось постфактум, ког
да проще обрабатывать возможное переполнение. При настройке ОС вероятность
выполнения некоторых функций более 4 миллиардов раз довольно высока. Од

ГЛАВА 19

Утилита Smooth Working Set

677

нако, планируя оптимизировать программу при помощи с SWS, я рассматриваю
эту проблему в самом начале и провожу тестирование, чтобы определить, возможно
ли переполнение счетчика вызовов функций. Вероятность переполнения счетчика
функций в SWS можно уменьшить, потому что я оставил ловушки, позволяющие
начинать и прекращать сбор данных. Так что при каждом запуске приложения вы
можете генерировать данные, только когда они вам нужны.
В тех крайне редких случаях, когда функция выполняется более 4 миллиардов
раз, у вас есть два варианта. Вопервых, можно реализовать переменную счетчи
ка как 64разрядное целое, защитив ее при помощи объекта синхронизации, от
дельного для каждого модуля. Другой, более радикальный вариант — разработка
схемы, подобной алгоритму WST. Конечно, есть и еще один вариант: так как про
блема переполнения возможна только в Win32, можно реализовать SWS только
для Microsoft Win64. Даже за всю свою жизнь вы не сможете выполнить
18 446 744 073 709 551 615 вызовов функции.
Изучив листинг 192, вы увидите, что формат SWSфайла очень прост. Я быст
ро понял, что для обработки всех манипуляций с файлами мне нужен общий ба
зовый класс — CSWSFile, определенный в файлах SWSFILE.H и SWSFILE.CPP. По сути
этот класс — не более чем оболочка для обычных действий с файлами Win32.

Листинг 19-2.

FILEFORMAT.H

/*——————————————————————————————————————————————————————————————————————
Отладка приложений для Microsoft .NET и Microsoft Windows
Copyright © 199762003 John Robbins — All rights reserved.
——————————————————————————————————————————————————————————————————————*/
#ifndef _FILEFORMAT_H
#define _FILEFORMAT_H

/*//////////////////////////////////////////////////////////////////////
Директивы define и объявления структур
//////////////////////////////////////////////////////////////////////*/
// Сигнатура SWS6файла (SWS2).
#define SIG_SWSFILE '2SWS'
#define EXT_SWSFILE _T ( ".SWS" )
/*//////////////////////////////////////////////////////////////////////
Заголовок SWS6файла.
//////////////////////////////////////////////////////////////////////*/
typedef struct tag_SWSFILEHEADER
{
// Сигнатура файла. См. определения SIG_*, указанные выше.
DWORD dwSignature ;
// Время компоновки двоичного файла, ассоциированного с этим файлом.
DWORD dwLinkTime ;
// Адрес загрузки двоичного файла.
DWORD64 dwLoadAddr ;
// Размер образа.
DWORD dwImageSize ;
см. след. стр.

678

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

// Число записей в этом файле.
DWORD dwEntryCount ;
// Поле флагов.
DWORD dwFlags ;
// Имя модуля для этого файла.
TCHAR szModuleName[ MAX_PATH ] ;
DWORD dwPadding ;
} SWSFILEHEADER , * LPSWSFILEHEADER ;
/*//////////////////////////////////////////////////////////////////////
Тип записи SWS6файла.
//////////////////////////////////////////////////////////////////////*/
typedef struct tag_SWSENTRY
{
// Адрес функции.
DWORD64 dwFnAddr ;
// Размер функции.
DWORD dwSize ;
// Счетчик вызовов.
DWORD dwExecCount ;
} SWSENTRY , * LPSWSENTRY ;
#endif // _FILEFORMAT_H
В связи с форматом SWSфайла я хочу обратить ваше внимание на то, что я
храню в нем адрес загрузки двоичного файла. Сначала я хранил в нем только адреса
функций, но потом вспомнил о возможности перемещения двоичного файла в
памяти. В этой ситуации SWSDLL.DLL могла бы быть вызвана с какимлибо адре
сом, и у меня не было бы никакой записи для этого адреса в SWSфайлах, загру
женных для модуля. Хотя всем нам следует модифицировать базовые адреса на
ших DLL, иногда мы про это забываем, и я хотел, чтобы SWS правильно обраба
тывала такие случаи.
Некоторые проблемы у меня вызвало генерирование символов для первона
чального модуля SWS. Изза особенностей компоновки программ и генерирова
ния символов многие символы модуля не имеют в себе вызовов _penter. Скажем,
при компоновке программы со статической стандартной библиотекой C в вашем
модуле будет содержаться множество стандартных функций C. Чтобы ускорить
просмотр адресов утилитой SWS, я реализовал несколько способов сокращения
числа символов.
Функция обратного вызова для перечисления символов и некоторые мои по
пытки ограничения их числа приведены в листинге 193. Прежде всего я реали
зовал проверку того, имеет ли символ соответствующую информацию о номере
строки. Так как я полагал, что функции, содержащие в себе вызовы _penter, будут
скомпилированы правильно, с соблюдением всех вышеописанных этапов, я смог
безопасно избавиться от многих посторонних символов. Следующий шаг на пути
к устранению символов заключался в проверке, являются ли частью символов
специфические строки. Например, все символы, начинающиеся с _imp__, представ
ляют собой функции, импортируемые из других DLL. Еще две проверки я оставил

ГЛАВА 19

Утилита Smooth Working Set

679

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

Листинг 19-3.

Перечисление символов SWS

/*——————————————————————————————————————————————————————————————————————
ФУНКЦИЯ: SymEnumSyms
ОПИСАНИЕ:
Функция обратного вызова для перечисления символов. Эта функция только
добавляет данные в SWS6файлы и все.
ПАРАМЕТРЫ:
szSymbolName
6 имя символа.
ulSymbolAddress – адрес символа.
ulSymbolSize
6 размер символа в байтах.
pUserContext
6 файл SWS.
ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ:
TRUE 6 все отлично.
FALSE – при добавлении данных в файл возникла проблема.
——————————————————————————————————————————————————————————————————————*/
BOOL CALLBACK SymEnumSyms ( PSTR
szSymbolName
,
DWORD64 ulSymbolAddress ,
ULONG ulSymbolSize
,
PVOID pUserContext
)
{
LPENUMSYMCTX pCTX = (LPENUMSYMCTX)pUserContext ;
CImageHlp_Line cLine ;
DWORD dwDisp ;
if ( FALSE == g_cSym.SymGetLineFromAddr ( ulSymbolAddress ,
&dwDisp
,
&cLine
) )
{
// Если для символа не было обнаружено исходного файла
// и номера строки, игнорируем его.
return ( TRUE ) ;
}
//
//
//
//
//

Будущие улучшения для игнорирования конкретных символов:
1. Реализуйте проверку того, не находится ли файл
в списке игнорируемых файлов.
2. Проверяйте, относится ли адрес к разделу кода модуля.
Это позволит избежать добавления в итоговые файлы символов IAT.

// Есть ли этот символ в списке игнорируемых символов?
см. след. стр.

680

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

for ( int i = 0 ; i < IGNORE_CONTAINING_COUNT ; i++ )
{
if ( NULL != strstr ( szSymbolName
,
g_szIgnoreContaining[ i ] ) )
{
// Выход.
return ( TRUE ) ;
}
}
if ( NULL != pCTX6>pfnVerboseOutput )
{
#ifdef _WIN64
pCTX6>pfnVerboseOutput(_T(" Adding Symbol : 0x%016I64X %S\n"),
#else
pCTX6>pfnVerboseOutput(_T(" Adding Symbol : 0x%08X %S\n" ) ,
#endif
(DWORD_PTR)ulSymbolAddress
,
szSymbolName
);
}
if ( FALSE == pCTX6>pSWSFile6>AddData ( ulSymbolAddress ,
ulSymbolSize
,
0
) )
{
ASSERT ( !"Adding to SWS file failed!" ) ;
return ( FALSE ) ;
}
pCTX6>iAddedCount++ ;
return ( TRUE ) ;
}

Период выполнения и оптимизация
Одна проблема с символами в период выполнения была связана с тем, что сим
вольная машина не возвращает статические функции. Становясь подозрительным,
если я не находил в модуле адрес, я, как обычно, включал в программу вызовы 6–7
диагностических информационных окон. Сначала я несколько смутился тем, что
видел диагностические сообщения, так как в одной из моих тестовых программ
никакая функция не была объявлена статической. Взглянув на стек в отладчике, я
увидел символ с именем наподобие $E127. В функции имелся вызов _penter, и все
казалось правильным. Наконец я понял, что это функция, сгенерированная ком
пилятором, такая как конструктор копий. Хотя мне понастоящему нравится вы
полнять проверку ошибок в коде, я заметил, что в некоторых программах хвата
ло этих сгенерированных компилятором функций, поэтому я мог только сооб
щить о проблеме в отладочных компоновках при помощи TRACE.
Последний интересный аспект SWS — оптимизация модуля. Функция TuneModule
довольно объемна, поэтому в листинге 194 я привел только ее алгоритм. Как вы
можете увидеть, на каждой странице кода я размещаю как можно больше функ

ГЛАВА 19

Утилита Smooth Working Set

681

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

Листинг 19-4.

Алгоритм настройки SWS

// Алгоритм функции TuneModule.
BOOL TuneModule ( LPCTSTR szModule )
{
Сгенерировать имя SWS6файла вывода.
Скопировать базовый SWS6файл во временный файл.
Открыть временный SWS6файл.
Для каждого файла szModule.#.SWS в этом каталоге
{
Проверить, что время компоновки этого файла #.SWS
соответствует времени компоновки временного SWS6файла.
Для каждого адреса в этом файле #.SWS
{
Прибавить значение счетчика вызовов для этого адреса
к аналогичному счетчику во временном файле.
}
}
Получить размер страницы этого компьютера.
Пока не готово.
{
Найти первую запись во временном SWS6файле,
для которой указан адрес.
Если я проверил все адреса, но такой записи не нашел.
см. след. стр.

682

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

{
готово = TRUE
Прервать цикл.
}
Если счетчик вызовов для этой записи равен 0.
{
готово = TRUE
Прервать цикл.
}
Если размер этой записи меньше, чем оставшееся
пространство страницы.
{
Вывести имя функции этой записи в PRF6файл.
Обнулить адрес, чтобы я не использовал его еще раз.
Вычесть размер этой записи из размера оставшегося
пространства страницы.
}
Иначе, если размер этой записи больше, чем размер страницы
{
Просто записать адрес в PRF6файл,
так как я ничего не могу сделать.
Присвоить значению оставшегося объема страницы
размер всей страницы.
}
Иначе.
{
// Эта запись слишком велика для размещения
// на странице, поэтому выполняется поиск функции,
// лучше всего подходящей для этой страницы.
Для каждого элемента временного SWS6файла.
{
Если адрес имеет ненулевое значение.
{
Если наилучшее соответствие не найдено.
{
Обозначить эту запись как наилучшее соответствие
в целом.
Обозначить эту запись как наилучшее соответствие
по числу вызовов.
}
Иначе.
{
Если размер этой записи > размер наилучшего соотв6ия
{
Обозначить эту запись как наилучшее
соответствие в целом.
}

ГЛАВА 19

Утилита Smooth Working Set

683

Если счетчик вызовов для этой записи не равен 0.
{
Если размер этой записи > размер наилучшей
записи по числу вызовов.
{
Обозначить эту запись как наилучшее
соответствие по числу вызовов.
}
}
}
}
}
Если наилучшее соответствие по числу вызовов не найдено.
{
Считать наилучшей записью по числу вызовов
наилучшую запись в целом.
}
Вывести имя функции, наилучшей по числу вызовов в PRF6файл.
Присвоить значению оставшегося объема страницы размер всей
страницы.
}
}
Закрыть все временные файлы
}

Что после SWS?
Как я уже говорил, SWS обеспечивает довольно хорошую возможность оптимиза
ции программ. Если вам хочется сделать ее еще лучше, вот несколько полезных
советов.
쐽 Напишите программу начала и прекращения сбора данных. В функции _penter
я выполняю проверку того, находится ли событие в сигнальном состоянии. Вы
можете написать отдельную программу, которая будет генерировать событие,
управляющее сбором данных утилитой SWS. Просто создайте событие с име
нем SWS_Start_Stop_Event и генерируйте его, когда хотите прекратить накопле
ние данных.
쐽 Реализуйте упомянутые мной возможности исключения символов, чтобы их
число в ваших SWSфайлах было минимальным.
쐽 Если вы понастоящему честолюбивы, создайте для просмотра данных и оп
тимизации программу с графическим интерфейсом. Работать с ней будет го
раздо удобней, чем с утилитой, основанной на командной строке.

684

ЧАСТЬ IV

Мощные средства и методы отладки неуправляемого кода

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

Ч А С Т Ь

V

ПРИЛОЖЕНИЯ

ПРИЛОЖЕНИЕ

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

Я

надеюсь, что для облегчения отладки приложений вы будете включать в них
возможность создания минидампов (см. главу 13) и вам не придется изучать жур
налы Dr. Watson. Однако, если у вас есть готовое приложение или ваши клиенты
не могут высылать вам двоичные минидампы по электронной почте, Dr. Watson
всегда сможет указать вам на время и место возникновения проблемы.
Пожалуй, доктора Ватсона следовало бы назвать доктором Джекилом и мисте
ром Хайдом. В режиме доктора Джекила вы получаете информацию об ошибке
на машине пользователя, легко находите место проблемы и быстро ее исправля
ете. В режиме мистера Хайда вы получаете лишь еще один ни о чем не говоря
щий набор чисел. В этом приложении я объясню работу с журналами Dr. Watson,
что позволит вам реже встречаться с мистером Хайдом и чаще с доктором Дже
килом.
На следующих страницах я рассмотрю полный журнал Dr. Watson, объясняя
по ходу дела всю важную информацию (релевантные данные в конкретном раз
деле выделены полужирным начертанием). Этот журнал был создан в результате
одной из ошибок ранней версии WDBG.EXE — отладчика, написанного мной для
главы 4.
После знакомства с этой книгой ничто в журнале Dr. Watson не должно быть
для вас незнакомым. Что до различий между журналами Dr. Watson в Microsoft
Windows 2000, Windows XP и Windows Server 2003, то их немного. Однако, как вы
увидите ниже, версии Dr. Watson в двух последних ОС несколько лучше.
Для получения журналов запустите Dr. Watson (DRWTSN32.EXE). В списке Appli
cation Errors (ошибки приложения) вы увидите недавние ошибки. Если этот спи
сок пуст, возможно, Dr. Watson не задан в качестве отладчика по умолчанию. Что
бы сделать Dr. Watson отладчиком по умолчанию, запустите его с ключом –i, т. е.
введите выражение drwtsn32 –i. Для генерирования тестовой ошибки запустите

ПРИЛОЖЕНИE A

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

687

программу CrashTest.EXE из главы 13 и нажмите кнопку Crash Away (сгенериро
вать ошибку). Пользовательский интерфейс Dr. Watson изображен на рис. A1.

Рис. A1.

Пользовательский интерфейс Dr. Watson

Выберите в списке Application Errors интересующую вас ошибку и нажмите
кнопку View (показать), после чего появится диалоговое окно Log File Viewer (про
смотр журнала) (рис. A2). В Windows 2000 в окне Application Errors вы увидите
только номера ошибок приложений и их адреса. В Windows XP/Server 2003 вы
увидите еще и имя процесса. Скопируйте из окна Log File Viewer описание кон
кретной ошибки. Если вам хочется получить минидамп последней ошибки, пол
ный путь к нему указан в поле Crash Dump (аварийная копия памяти) окна Dr.
Watson.

Рис. A2.

Диалоговое окно Log File View утилиты Dr. Watson для Windows XP

688

ЧАСТЬ V

Приложения

Журналы Dr. Watson
Первый раздел журнала Dr. Watson имеет вид:

Исключение в приложении:
Прил.: (pid=1796)
Время: 1/2/2003 @ 13:42:56.208
Номер: c0000005 (нарушение прав доступа)
Заголовок содержит информацию о причине ошибки: в моем примере это исклю
чение в приложении. В случае некоторых ошибок номера исключений не всегда
преобразуются в понятную людям форму, такую как «нарушение прав доступа» для
исключения 0xC0000005. Все номера исключений вы можете узнать, отыскав в
файле WINNT.H строки STATUS_. Коды ошибок указаны в документации как значе
ния EXCEPTION_, возвращаемые функцией GetExceptionCode, однако реальные значе
ния определяются в директивах #define STATUS_. После преобразования кода ошибки
в значение EXCEPTION_ вы сможете просмотреть ее описание в документации к
GetExceptionCode.
Раздел System Information (сведения о системе) в объяснении не нуждается:

*——> Сведения о системе
0
8
132
160
156
208
220
364
424
472
504
528
576
592
836
904
912

Список задач State Dump for Thread Id 0xe14 (Копия памяти для потока 0xe14) 00410144 mov
ecx,[eax+0x4]
ds:0023:00000004=????????
00410147 cmp
dword ptr [ecx],0x80000003
0041014d jz WDBG!CWDBGProjDoc__HandleBreakpoint+0x90 (004101a0)
0041014f mov
[ebp60x14],esp
00410152 mov
[ebp60x18],ebp
00410155 mov
esi,esp
00410157 push
0x456070
0041015c push
0x45606c
00410161 mov
edx,[ebp60x18]
00410164 xor
eax,eax
00410166 push
eax
Dr. Watson отображает информацию о состоянии каждого потока, выполнявше
гося в процессе в момент ошибки. Состояния потока содержат всю информацию,
необходимую для обнаружения механизма и причин краха.
В разделе регистров указываются значения всех регистров в момент ошибки.
Особое внимание следует уделить регистру EIP, указателю команд. Для моего при
мера из Windows XP у меня имелись символы, поэтому вы можете видеть, какую
функцию выполнял этот поток в момент ошибки, но в большинстве журналов Dr.
Watson информации о символах не будет. Конечно, если Dr. Watson не сообщает
вам имя функции, это не проблема. Просто загрузите в программу CrashFinder из
главы 12 проект CrashFinder вашего приложения, введите адрес в поле Hexadecimal
Address(es) (шестнадцатеричные адреса) и нажмите кнопку Find (найти).
Поток из нашего фрагмента оказался потоком, вызвавшим ошибку. Об этом
свидетельствует только указатель FAULT> (СБОЙ>) в середине дизассемблиро
ванного листинга. Пару раз я видел журналы Dr. Watson, в которых не было ука
зателя FAULT>. Если вы не можете найти в журнале этот указатель, изучите со

ПРИЛОЖЕНИE A

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

691

стояние каждого потока и введите каждый адрес EIP в CrashFinder, чтобы узнать,
какую команду выполнял поток в момент ошибки.
Если вы читали главу 7, дизассемблированный листинг должен быть вам по
нятен. Новыми элементами будут только значения, показанные после команд. Чтобы
вы могли узнать, какие значения использовались командой, дизассемблер Dr. Watson
пытается просмотреть эффективный адрес ссылки на память. Адреса, начинаю
щиеся с букв ss, свидетельствуют о том, что происходил доступ к сегменту стека;
ds — к сегменту данных. В Windows XP/Server 2003 эффективный адрес будет указан
только после строки, на которую указывал регистр EIP в момент ошибки.
В журналах Dr. Watson из Windows 2000 эффективные адреса будут указаны
после каждой ассемблерной команды. Однако при этом гарантируется правиль
ность только того адреса, что указан в строке, на которой находился указатель
команд. Другие адреса могут быть неверны, так как значения, используемые коман
дой, могли измениться. Допустим, первая дизассемблированная команда в состо
янии потока ссылалась на память при помощи регистра EBX. Если ошибка случи
лась после выполнения еще 10 команд, то одна из промежуточных команд легко
могла изменить EBX. Однако, когда Dr. Watson в Windows 2000 дизассемблирует
программу, для преобразования эффективного адреса он использует текущее зна
чение EBX — то, которое имело место в момент ошибки. Поэтому эффективный
адрес, показанный в дизассемблированном коде, может быть неверным. Итак,
прежде чем поверить в значения эффективных адресов, убедитесь, что нужные
регистры не были изменены никакой командой.
Благодаря недавно приобретенным навыкам работы с ассемблером, вы долж
ны легко узнать, почему этот поток потерпел крах. Читая ассемблерный листинг
Dr. Watson (или отладчика), большинство программистов допускает серьезную
ошибку: они изучают его сверху вниз. Настоящая хитрость в том, чтобы начать
исследование с места ошибки и постепенно подниматься вверх в поисках коман
ды, присвоившей значения регистрам, использованным в команде, вызвавшей
ошибку.
В нашем случае поток потерпел крах на команде 00410144 MOV ECX, [EAX+0x4], при
которой регистр EAX имел значение 0. В Microsoft Windows все адреса, располо
женные ниже 64 кб, отмечены как не имеющие доступа, поэтому попытка чтения
памяти по адресу 0x00000004 — не лучшая идея. Итак, мы должны найти коман
ду, заносящую 0 в EAX. Поднявшись на одну строку, вы увидите команду MOV EAX,
[EBP+0xC]. Помните, что второй операнд, источник, помещается в первый операнд,
приемник (иначе говоря, помните про правило «от источника к приемнику»). Это
значит, что в EAX было скопировано значение, находившееся по адресу [EBP+0xC].
Следовательно, по адресу [EBP+0xC] располагался 0.
В этот момент вы должны вспомнить еще одну хитрость, которую я описал в
главе 7: «параметры располагаются по положительным смещениям»! Параметры
располагаются по положительным смещениям от регистра EBP, причем первый
находится по адресу [EBP+0x8], а каждый следующий отстоит от предыдущего на
4 байта. Так как 0xC на 4 байта больше, чем 0x8, я могу предположить, что ошиб
ка была вызвана тем, что второй параметр этой функции был равен NULL (наде
юсь, прочитав эти два абзаца, вы поняли, как важно знать ассемблер в достаточ
ном объеме для чтения журналов Dr. Watson!).

692

ЧАСТЬ V

Приложения

Ниже вы можете увидеть вторую часть состояния потока: раздел Stack Back Trace
(обратная трассировка стека) Заметьте: я вывожу имена функций на двух строках,
чтобы они помещались на странице. При помощи двух символов подчеркивания
(__) Dr. Watson отображает операцию разрешения области видимости (::).

*——> Stack Back Trace (Обратная трассировка стека) Обратная трассировка стека Копия необработанного стека