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

Аварийные завершения и искажение данных

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


Что такое ошибки?

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


  • Короткие или недопустимые предельные сроки разработки

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


    Набор навыков

    И хорошие отладчики, и хорошие разработчики имеют серьезные навыки решения специфических для программного обеспечения задач. Эти навыки можно приобрести и оттачивать. Сильных отладчиков/разработчиков отличает от хороших то, что они не только умеют решать соответствующие проблемы, но и понимают, как части проекта соотносятся с проектом в целом.
    Ниже перечислены компоненты разработки, доскональное знание которых определяет мастерство программиста в отладке:
  • проект;
  • язык программирования;
  • технология;
  • операционная система;
  • особенности CPU.
  • Знание проекта
    Знание своего проекта — это первая линия защиты от ошибок интерфейса пользователя, а также ошибок логики и выполнения программы. Зная, как и где реализуются свойства в различных исходных файлах, можно быстро разобраться, кто что делает и для кого.
    К сожалению, из-за большого разнообразия проектов единственный способ их изучения — это чтение проектной документации, если она существует, и просмотр кода в отладчике. Если вы работаете с исходным кодом C++, то может оказаться полезным также взглянуть на файлы соответствующего браузера (программы просмотра исходных кодов C++). Некоторые компании, кроме того, производят инструменты, которые преобразуют существующий исходный код в UML-диаграммы. Даже плохо документированный исходный код лучше, чем совсем ничего, если он спасает вас от необходимости интерпретировать листинги дизассемблера.
    Знание языка программирования
    Знание языка (или языков) программирования, применяемого при разработке, — задача более трудная, чем это может показаться. Я понимаю под этим знание не только того, как на языке нужно программировать, но и того, что он "делает за сценой". Например, разработчики иногда забывают, что локальные переменные, которые являются объектами С++-классов, или перегруженные С++-операторы могут создавать временные элементы в стеке. И оператор назначения, выглядящий вполне невинно, может иметь дело с большим объемом исполняемого кода.
    Язык Microsoft Visual Basic также генерирует значительное количество кода. Многие ошибки, особенно связанные с проблемами производительности, являются результатом неправильного употребления языка, так что имеет смысл затратить некоторое время на изучение индивидуальных особенностей используемых вами языков программирования.

    Знание технологии

    Освоение применяемых при разработке технологий,— это первый большой шаг на пути устранения наиболее трудных ошибок. Например, если вы знаете, что делает СОМ при создании экземпляра СОМ-объекта и возврате его интерфейса, то вам понадобится намного меньше времени, чтобы проследить, почему потерпел неудачу специфический запрос интерфейса. То же самое происходит и при работе с библиотекой классов MFC (Microsoft Foundation Class). Имеет смысл представлять, как протекают сообщения в архитектуре документ/представление (document/view), на случай возникновения проблем с документом, получающим сообщение от операционной системы Windows. Говоря о знании применяемых технологий, я имею в виду, что необходимо иметь по крайней мере общее их понимание и, что еще важнее, точно знать, где, при необходимости, можно найти более детальную информацию.

    Знание операционной системы

    Знание операционной системы позволит устранить ошибку, а не пытаться просто что-то делать. Необходимо знать ответы на следующие вопросы, касающиеся используемой операционной системы: что такое библиотека динамической компоновки (DLL), как работает загрузчик изображений, как работает системный реестр. Часто самые тяжелые ошибки возникают тогда, когда программа обращается к операционной системе, и связано это с тем, что разработчик не знает разветвлений, создаваемых этим обращением, или передает неверные данные.

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


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

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

    Знание CPU

    И наконец, стать сильным отладчиком невозможно без знания центрального процессора. Эти знания необходимы для устранения большинства самых неприятных ошибок, с которыми приходится сталкиваться. Было бы здорово, если бы при аварийном завершении всегда был доступен исходный код, но большинство сбоев направляют пользователя прямо в окно дизассемблера. Несколько часов, потраченных на изучение ассемблера, могут сохранить бесчисленные часы при отладке, однако некоторые инженеры не знают языка ассемблера и не проявляют интереса к его изучению. Нет необходимости уметь написать целую программу на языке ассемблера (я и сам, наверное, не мог бы этого сделать), следует уметь прочитать такую программу. Некоторые сведения о языке ассемблера приводятся в главе 6.

    Недостаточная подготовленность разработчика

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



    Недостаточные обязательства по качеству

    Последняя причина существования ошибок в проектах, по моему мнению, наиболее серьезна. Любой разработчик скажет, что он выполняет обязательства по качеству. К сожалению, обязательства по качеству не всегда достаточно реальны. Стремясь к получению надежного и работоспособного продукта, необходимо уделить внимание всем компонентам разработки, а не только самым эффектным. Кроме того, из всех возможных алгоритмов следует выбрать наиболее простой и приложить максимум усилий, чтобы как можно лучше его протестировать. В конце концов, заказчик не покупает алгоритмы, а только высококачественные программные продукты. В основе ..спеха тех фирм-производителей программного обеспечения и индивидуальных разработчиков, которые заботятся о качестве, лежат следующие компоненты: тщательное предварительное планирование, персональная ответственность, жесткий контроль качества и превосходные коммуникативные способности. Многие решают крупные задачи разработки программного обеспечения (т. е. задачи планирования, программирования и т. д.), но только те, кто обращают внимание на детали, сдают продукты вовремя и : высоким качеством.
    Хорошим примером правильного подхода к решению проблемы качества является практика, принятая в NuMega при составлении сотрудниками ежегодных обзоров. Одна из ключевых частей обзора должна содержать запись о том, сколько ошибок зарегистрировано в продукте. Учет ошибок является жизненной частью поддержания качества программного продукта, никакая другая компания, в которой работал автор, даже не проверяла настолько очевидные вещи. Разработчики знают, где присутствуют ошибки, но для учета последних нужен стимул. В NuMega нашли такой подход. Узнав, что ошибки учитываются как часть полезной работы, разработчики регистрировали их независимо от того, насколько они были тривиальными. Благодаря этому соревнованию по регистрации ошибок лишь самые диковинные из них "проскользнули" в готовый продукт. Что более важно, это дает реалистическую картину того, в какой точке проекта разработчики находятся в любой заданный момент.

    Руководя проектом, автор установил правило, согласно которому каждый член команды должен был согласиться, что продукт готов к переходу на следующий этап разработки. Если хотя бы один человек чувствовал, что продукт не готов к этому, то переход не выполнялся. Исправлялись даже незначительные ошибки, а весь следующий день отводился на тестирование. Это не только гарантировало уверенность каждого члена команды в качестве продукта, но также помогало определить вклад каждого из них в конечный результат. Интересно, что никто никогда не пытался остановить выпуск из-за чьей-то ошибки (это всегда делал автор ошибки).

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

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

    Необдуманное программирование

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



    Неправильное понимание требований

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

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

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

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

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

    Несоответствие ожиданиям

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


    Низкая производительность

    Пользователи не любят, когда приложение замедляет работу, сталкиваясь с реальными данными. Неадекватное тестирование — корень всех ошибок низкой производительности (эффективности), однако хуже всего при разработке выглядит приложение, которое команда не сумела протестировать со всеми подходящими реальными значениями. Один проект, с которым я работал в NuMega — BoundsChecker 3.0 — содержал подобную ошибку в своей оригинальной технологии FinalCheck. Эта версия FinalCheck включала дополнительную отладочную и контекстную информацию непосредственно в исходный код, чтобы сообщения об ошибках, генерируемые BoundsChecker, были более содержательными. К сожалению, код программы FinalCheck не был достаточно протестирован на крупных реальных приложениях, прежде чем выпустили версию BoundsChecker 3.0. В результате, значительная часть пользователей не смогла применить это свойство. В последующих выпусках свойство FinalCheck было полностью переписано, но многие пользователи так никогда и не опробовали его из-за проблем производительности в первоначальной версии, хотя это было одно из наиболее мощных и полезных свойств продукта.
    Ошибки низкой производительности отлавливаются двумя способами. Во-первых, следует определить требования к производительности приложения с самого начала процесса разработки. Нужно уметь измерить производительность, чтобы иметь адекватное представление о ней. Важно сохранять основные значения производительности на постоянном уровне; если приложение снижает эти значения на 10% и более, нужно определить причины этого снижения и исправить ситуацию. Во-вторых, удостоверьтесь, что тестирование выполняется насколько возможно близко к реальным сценариям, и как можно раньше в цикле разработки.



    Ошибки и отладка

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



    Ошибки процесса разработки и их устранение

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


  • Освоение набора навыков

    Любое дело, связанное с технологией, необходимо изучать постоянно, чтобы держаться на высоком уровне, становиться лучше и продвигаться вперед. В приложении 2 перечислены ресурсы, которые помогли мне и, наверное, помогут вам стать более квалифицированным отладчиком.
    Помимо чтения книг и журналов по отладке, полезно также писать утилиты (любого вида). Лучший способ обучения — практическая работа. В данном случае это означает необходимость выполнять кодирование и отладку. Это позволит не только усовершенствовать мастерство кодирования и отладки, но, если рассматривать эти утилиты как реальные проекты (т. е. завершая их вовремя и с высоким качеством), научиться лучше планировать проект и оценивать график работ.
    Стимулом для разработки собственных утилит может служить то, что законченные утилиты являются превосходным средством демонстрации ваших профессиональных способностей в области разработки программного обеспечения, вполне заменяя так называемые "рабочие интервью" (деловые собеседования при приеме на работу). Немногие разработчики демонстрируют свои программы на подобных интервью, однако компании предпочитают преуспевающих в этом искусстве тем, которые не имеют таких навыков. Представление портфеля работ, сделанных в свободное время, показывает, что вы можете выполнять такую работу по-своему и имеете пристрастие к разработке программного обеспечения — и это сразу поместит вас в верхнюю часть списка соискателей.
    Другая практика, чрезвычайно полезная при более глубоком изучении языков, технологий и операционной системы, состоит в просмотре исходных текстов программ других разработчиков. Такие тексты без труда можно найти в Интернете. Выполняя различные программы под отладчиком, вы увидите, как другие специалисты работают над ошибками. Кроме того, если вам не придумать свою утилиту, можно просто добавить свойство к. одной из найденных утилит.
    Еще один способ, который я рекомендовал бы для более глубокого изучения технологий, операционной системы и CPU, состоит в выполнении обратной разработки (reverse engineering).
    Это поможет активно изучать язык ассемблера и продвинутые особенности отладчика. Полученных из главы 6 знаний о языке ассемблера должно быть достаточно, чтобы начать работу. Я не рекомендовал бы начинать полную обратную разработку с загрузчика операционной системы, — лучше заняться более простыми задачами. Для меня очень полезной оказалась работа с реализацией функции CoCreateinstanceEx.

    Чтение книг и журналов, написание утилит, обзор кодов других инженеров и выполнение обратной разработки — все это отличные способы улучшить навыки отладки. Еще один огромный ресурс — друзья и коллеги по работе. Не бойтесь спрашивать их, как они что-то сделали или как что-то работает, и они, если не находятся в цейтноте, будут рады помочь вам. Я уже говорил, что люблю, когда мне задают вопросы, потому что узнаю при этом больше, чем тот, кто эти вопросы задает! Кроме того, я постоянно читаю группы новостей по программированию, это отличное место для выяснения вопросов — можно получить очень хороший ответ, особенно от тех людей, которых в фирме Microsoft обозначили как MVPS (Most Valuable Professionals — наиболее ценные профессионалы).

    В модели DCOM одна из функций, предназначенных для создания удаленного объекта. — Ред.

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

    Теперь, коротко рассмотрев типы и происхождение ошибок, перейдем к процессу отладки. Многие начинают думать об отладке только тогда, когда на этапе кодирования сталкиваются с аварийным завершением. Думать же об этом следует с самого начала, на этапе разработки требований. Чем лучше спланирован проект, тем меньшее количество времени (и денег) будет затрачено на последующую его отладку.
    Как было сказано выше, "расползание" свойств может стать бедствием для проекта. В большинстве случаев незапланированные свойства вводят ошибки и наносят ущерб продукту. Однако это не означает, что в планы не могут быть внесены изменения. Иногда необходимо изменить свойства продукта или добавить новые, чтобы стал более конкурентоспособным и соответствовал запросам пользователя. Главное, перед изменением кода следует точно определить и спланировать все изменения. И имейте в виду, что добавление свойств затрагивает не только код, но воздействует и на тестирование, и на документацию. Существует правило, согласно которому время, необходимое для добавления или удаления свойства, растет экспоненциально по мере приближения к концу производственного цикла.
    В превосходной книге Стива Макконнелла "Искусство кодирования" (Steve McConnell. Code Complete. — Microsoft Press, 1993, pp. 25—26) говорится о стоимости исправления ошибки. Сценарий тот же, что и при добавлении или удалении свойств — на этапе разработки требований и планирования стоимость исправления ошибки невелика, однако в процессе дальнейшей работы она растет экспоненциально; то же самое происходит и со стоимостью отладки.
    Планирование отладки идет вместе с планированием тестирования. Во время планирования нужно искать способы ускорения и улучшения обоих процессов. Одна из самых лучших предосторожностей, которые вы можете предпринять, состоит в написании специальных программ сброса файловых данных (file data dumpers) и проверки достоверности (validators) для внутренних структур данных, а также для двоичных файлов, если это необходимо. Если приложение читает и пишет данные в двоичный файл, следует написать тестирующую программу, которая будет сбрасывать (dumps) данные в удобочитаемом формате в текстовый файл. Программа сброса (dumper) должна также проверять достоверность данных и все взаимозависимости в двоичном файле. Этот шаг сделает как тестирование, так и отладку более легкими.
    Правильное планирование отладки позволяет минимизировать время работы с отладчиком (в этом и состоит ваша цель). Может показаться, что в книге по отладке такой совет выглядит странно, но идея состоит в том, чтобы попробовать совсем избежать ошибок. Встройте такой отладочный код в свои приложения, чтобы именно он (а не отладчик) сообщал вам, где находятся ошибки. Вопросы, касающиеся отладочного кода, рассмотрены в главе 3.



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

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


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

    Перейдем, наконец, к обсуждению процесса отладки. Разработать процесс, действенный для всех ошибок, даже для "берущихся неизвестно откуда", было непросто. Но опираясь на собственный опыт и опыт коллег, я описал подход к отладке, которому интуитивно следуют все сильные разработчики; менее же опытные (или просто слабые) разработчики зачастую не находят его очевидным.
    Как вы увидите, этот процесс отладки не требует наличия семи пядей во лбу. Главное — систематически применять его. Рекомендуемый мной подход к отладке включает девять шагов.
  • Шаг 1. Дублируйте ошибку
  • Шаг 2. Опишите ошибку
  • Шаг 3. Всегда предполагайте, что ошибку допустили вы
  • Шаг 4. Разделяйте и преодолевайте
  • Шаг 5. Думайте творчески
  • Шаг 6. Используйте инструменты усиления отладки
  • Шаг 7. Начните интенсивную отладку
  • Шаг 8. Убедитесь, что ошибка исправлена
  • Шаг 9. Изучайте и делитесь с коллегами
  • В зависимости от ошибки, можно пропускать некоторые шаги целиком, если проблема и ее расположение полностью очевидны. Начинать всегда нужно с шага 1 и проходить через шаг 2. Решение может быть вычислено и ошибка исправлена на любом этапе между шагом 3 и шагом 7. После этого переходите к шагу 8, чтобы верифицировать и тестировать исправление. На рис 1.1 показаны шаги процесса отладки.
    Процесс отладки
    Рис. 1.1. Процесс отладки


    Противоречивые интерфейсы пользователя

    Противоречивые интерфейсы пользователя — это тип ошибок, которые хотя и не являются самыми серьезными, однако сильно раздражают. Одна из причин успеха системы Microsoft Windows заключается в том, что все Windows-приложения в общем случае ведут себя одинаково. Отклоняясь от стандарта Windows, приложение начинает мешать пользователю. В качестве примера этого нестандартного, раздражающего поведения можно привести клавиши управления поиском в программе Microsoft Outlook. Во всех других англоязычных Windows-приложениях нажатие комбинации клавиш + + приводит к открытию диалогового окна Find (Найти), служащего для поиска текста в активном окне. В Outlook, однако, нажатие + приводит к открытию сообщения. Даже после многих лет работы с Outlook я никак не могу запомнить, что найти текст в текущем открытом сообщении можно с помощью клавиши .
    Решить проблемы с противоречивыми интерфейсами пользователя можно, следуя рекомендациям в книге Microsoft Windows User Experience (Microsoft Press, 1999). В сети разработчиков фирмы Microsoft (Microsoft Developer Network — MSDN) появилась также предыдущая ее версия — The Windows Interface Guidelines for Software Design. Если ни одна из этих книг не содержит необходимых сведений, ищите приложение Microsoft, функции которого аналогичны тем, что вы пытаетесь реализовать, и следуйте его модели.



    Дублируйте ошибку

    Наиболее критический шаг в процессе отладки — первый, дублирование (повторение) ошибки. Это бывает трудно или даже невозможно сделать, но если не удается дублировать ошибку, то, вероятно, не удастся ее и устранить. При попытке дублирования могут потребоваться крайние меры. Однажды я столкнулся с ошибкой, которую не мог дублировать, просто запуская программу на выполнение. Я предположил, что эту ошибку могло вызывать состояние входных данных, поэтому запустил программу под отладчиком и вводил данные, нужные для дублирования, непосредственно в память. Это помогло. Если вы имеете дело с проблемой синхронизации, вам могут потребоваться такие шаги, как загрузка тех же задач, чтобы можно было дублировать состояние, в котором произошла ошибка.
    Продублировав ошибку с помощью одного набора шагов, оцените, нельзя ли сделать это как-нибудь еще. Некоторые ошибки можно получить только в одной ветви кода, а другие, — прогоняя программу через множество ветвей. Суть в том, чтобы попытаться посмотреть поведение программы во всех возможных ситуациях. Дублируя ошибку во многих ветвях программного алгоритма, можно намного лучше ощутить смысл данных и граничных условий, вызывающих проблемы. Кроме того, как известно, одни ошибки могут маскировать другие. Чем больше способов вы найдете, чтобы дублировать ошибку, тем лучше.
    Даже если не удалось дублировать ошибку, следует зарегистрировать ее в системе трассировки ошибок1. Таким образом, если другой разработчик отвечает за соответствующую секцию кода, он, по крайней мере, знает, что что-то неладно. Регистрируя ошибку, которую вы не можете воссоздать, нужно максимально описать ее, и эта информация в дальнейшем может позволить вам или кому-то другому решить проблему.
    Bug-tracking system (BTS) — программный продукт, предназначенный для осуществления контроля за всеми этапами жизненного цикла ошибок в ПО. — Ред.


    Опишите ошибку

    Вы должны быть способны описать ошибку как устно, так и в письменной форме. Столкнувшись с серьезной ошибкой, необходимо остановиться сразу после дублирования, и описать ее. В идеале это нужно делать в вашей системе прослеживания ошибок, даже если отладка ошибки лежит на вашей ответственности; и обсудить проблему тоже полезно. Часто это помогает исправить ошибку. Я не могу даже вспомнить, сколько раз вычислял ошибку, просто описывая ее кому-то.
    Этот "кто-то" даже не обязательно должен быть человеком. С этой ролью с успехом справляется мой кот, который помог мне устранить множество трудных ошибок, просто слушая, как я ему о них рассказывал. Если какие- то ошибки я и не смог исправить с его помощью, то по крайней мере попрактиковался в представлении их моим коллегам.
    Коммуникативные навыки очень важны, поскольку коллеги смогут помочь вам, только если вы способны достаточно ясно описать свои ошибки.



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

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


    Разделяйте и преодолевайте

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



    Думайте творчески

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


    Используйте инструменты усиления отладки

    Не поддается пониманию тот факт, что некоторые компании позволяют разработчикам тратить недели на поиск ошибки, расходуя тысячи долларов, тогда как инструменты повышения эффективности и покрытия кода (code coverage) помогли бы им найти текущую ошибку (и ошибки, с которыми они столкнутся в будущем) за считанные минуты.
    Прежде чем приступить к интенсивной отладке, я всегда запускаю программу под BoundsChecker/SmartCheck от NuMega (инструмент обнаружения ошибки), TrueTime (инструмент контроля эффективности) и TrueCoverage (инструмент покрытия кода). Другие компании, такие как Rational Software и MuTek Solutions, выпускают продукты с сопоставимыми функциональными. Применение дополнительных инструментов позволяет эффективнее расходовать время, чем при обычной работе с отладчиком.
    Рассмотрим кратко назначение инструментов перечисленных типов. Инструмент обнаружения ошибок ищет неправильные обращения к памяти, недействительные параметры системных API-функций и СОМ-интерфейсов, утечки памяти и ресурсов и многое другое. Инструмент контроля эффективности помогает проследить, где именно приложение работает медленно, причем, как правило, оказывается, что это совсем не то место, о котором мы думали. Инструмент покрытия кода показывает строки исходного кода, которые не осуществляются при выполнении программы. Эта информация полезна, потому что поиск ошибки целесообразно ограничить теми строками кода, которые выполняются.



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

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


    Убедитесь, что ошибка исправлена

    Если вы думаете, что окончательно исправили ошибку, то на следующем шаге отладки протестируйте исправление повторно, причем лучше несколько раз. Если ошибка находится в строке кода в изолированном модуле, вызываемом только один раз, тестирование исправления выполняется легко. Однако если исправление находится в корневом модуле, особенно в том, который управляет структурами данных, то следует соблюдать особую осторожность, — исправление может коренным образом повлиять на работу других частей проекта.
    При тестировании исправлений, особенно в критическом участке кода, нужно удостовериться, что оно работает со всеми состояниями данных — и "хорошими" и "плохими". Ничего нет хуже, чем исправление одной ошибки, которое вызывает две других. Если изменение вносится в критический модуль, то вся команда должна знать об этом. Только тогда станет возможной помощь коллег в обнаружении любых эффектов, наведенных этими изменениями.
    История отладочных войн
    Сражение
    Один из разработчиков, с которым автор работал в NuMega, думал, что он нашел большую ошибку в интегрированной среде разработки программ на Visual C++ (VC IDE1) фирмы NuMega, потому что VC IDE не работала на его машине.
    Для тех, кто незнаком с этой средой, приведем небольшую справку. Программные продукты NuMega интегрированы в VC IDE, благодаря чему окна, панели команд и меню NuMega являются частью интерфейса среды VC IDE.
    1VC IDE — Visual C++ Integrated Development Environment (Интегрированная Среда Разработки программ на языке Visual C++). Разработчики данной фирмы сокращенно называют эту систему VC IDE-интеграцией (VC IDE integration). — Пер.
    Результат
    Этот разработчик потратил часа два, исследуя "ошибку" с помощью SoftlCE. Через некоторое время, установив контрольные точки по всей операционной системе, он заметил, что при запуске VC IDE функция API CreateProcess называлась "\\R2D2\VCommon\MSDev98\Bin\MSDEV.EXE", а не "C:\VSCommon \MSDev98\Bin\MSDEV.EXE", как должно было быть.
    Другими словами, VC IDE выполнялась на машине \\R2D2\VCommon\MSDev98\Bin\MSDEV.EXE (вместо C:\VSCommon\MSDev98\Bin\MSDEV.EXE). Как это случилось?
    Разработчик только что получил новую машину и установил полную VC IDE для продуктов от NuMega. Чтобы установить ее быстрее, он скопировал связи (LNK-файлы) своего рабочего стола, которые были установлены без VC IDE, со своей старой машины на новую, перетаскивая их мышью. При перетаскивании LNK-файлов внутренние связи обновляются, чтобы отразить местоположение первоначальной связи. Поэтому VC IDE всегда запускалась из LNK-файла пиктограммы рабочего стола, который указывал на старую машину. Таким образом, он все время выполнял VC IDE своей старой машины.
    Урок
    Решая проблему отладки, разработчик пошел по неправильному пути: вместо попытки многократно дублировать проблему, он попытался просто внедриться в ядро отладки. Выше, в разделе "Шаг 1. Дублируйте ошибку" сказано, что следует попытаться дублировать ошибку несколькими способами, чтобы быть уверенными, что вы имеете дело действительно с ошибкой. Соответствующие рекомендации даны в разделе "Шаг 5. Думайте творчески" данной главы.

    Изучайте и разделяйте проблемы с коллегами

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


    Заключительный секрет процесса отладки

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



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

    Адреса загрузки DLL

    Когда приложение завершается аварийно, то большую помощь программисту оказывают некие вехи (т. е. указатели), позволяющие ему не заблудиться в отладчике.
    Первым важным указателем для аварийных сбоев является базовый адрес ваших динамических библиотек (DLL) и элементов управления ActiveX (OCX), который указывает, с какой ячейки памяти начинается отведенное им адресное пространство. Когда заказчик сообщает адрес аварийного завершения, необходимо быстро сузить его до первых двух или трех цифр адреса DLL, из которого он пришел. Конечно, трудно запомнить адреса всех системных DLL, но надо знать, по крайней мере, базовые адреса DLL своего проекта.
    Если все ваши DLL загружены в уникальные адреса, то имеется несколько хороших указателей, помогающих вести поиск аварийных остановов. Но что, вы думаете, случилось бы, если бы все DLL имели один и тот же адрес загрузки? Очевидно, что операционная система не отображает все DLL в одно и то же место памяти. Она должна перемещать любую входящую DLL, которая хочет занять уже заполненную память, в другое место. Тогда возникают проблемы, связанные с попытками вычислить, где какая DLL загружена. К сожалению, нет никакого способа узнать, что операционная система будет делать на различных машинах. Следовательно, программист понятия не имеет, откуда пришел аварийный останов, и ему придется потратить уйму времени на его поиск через отладчик.
    По умолчанию, для проектов, созданных с помощью соответствующего мастера, Visual Basic загружает DLL-библиотеки по адресу 0x11000000, a Visual C++ — по адресу 0x10000000. Держу пари, что сегодня по крайней мере половина DLL-библиотек в мире пытается загрузиться по одному из этих адресов. Изменение базового адреса для DLL называется перебазированием (rebasing), и это — простая операция, в которой указывается адрес загрузки, отличающийся от умалчиваемого.
    Прежде чем перейти к перебазированию, рассмотрим более легкий способ выяснить, имеются ли конфликты загрузки в ваших DLL. Получив следующее уведомление в окне Output отладчика Visual C++, следует немедленно остановиться и исправить адреса загрузки конфликтующих DLL.
    Удостоверьтесь, что вы исправляете адреса загрузки как для выпускных (финальных), так и для отладочных конфигураций.

    LDR: Dll xxx base 10000000 relocated due to collision with yyy

    (LDR: Dll xxx база 10000000 перемещена из-за конфликта с yyy)

    xxx и yyy в этом утверждении — имена DLL-библиотек, которые находятся в конфликте друг с другом.

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

    Существует два способа перебазирования DLL-библиотек в приложении. Первый метод использует утилиту REBASE.EXE, которая поставляется с набором разработчика Platform SDK. Утилита REBASE.EXE имеет много различных возможностей (опций), но лучший выбор состоит в ее вызове через командную строку с ключом /b, со стартовым базовым адресом и указанием в командной строке имен соответствующих DLL-файлов.

    Данные, представленные в табл. 2.1, взяты из документации Platform SDK и могут быть применены для перебазирования пользовательских DLL. Как видите, рекомендованный формат достаточно прост. Динамические библиотеки операционной системы загружаются в адреса от 0x70000000 до 0x78000000, поэтому следование рекомендациям табл 2.1 предохранит вас от конфликта с операционной системой.

    Таблица 2.1. Схема перебазирования DLL

    Первая буква имени DLL-файла

    Стартовый адрес

    А-С

    0x60000000

    D-F

    0x61000000

    G-I

    0x62000000

    J-L

    0x63000000

    М-О

    0x64000000

    P-R

    0x65000000

    S-U

    0x66000000

    V-X

    0x67000000

    Y-Z

    0x68000000

    Если в приложении имеются четыре DLL-файла: APPLE.DLL, DUMPLING.DLL, GINGER.DLL и GOOSEBERRIES.DLL, то, чтобы перебазировать все эти DLL-файлы, нужно запустить REBASE.EXE три раза.


    Следующие три команды показывают, как нужно запускать REBASE.EXE с этими DLL:

    REBASE /b 0x60000000 APPLE.DLL

    REBASE /b 0x61000000 DUMPLING.DLL

    REBASE /b 0x62000000 GINGER.DLL GOOSEBERRIES.DLL

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

    Другой метод перебазирования DLL состоит в спецификации адреса загрузки при компоновке DLL. В IDE Visual Basic установите адрес в поле DLL Base Address на вкладке Compile диалогового окна Project Properties. В Visual C++ укажите адрес в редактируемое поле Base Address, перейдя на вкладку Link диалогового окна Project Settings и выбрав там элемент Output в комбинированном списке Category. Visual C++ транслирует адрес, который вы вводите в поле Base Address в ключ /BASE компоновщика LINK.EXE.

    С помощью утилиты REBASE.EXE можно автоматически обрабатывать одновременную установку множественных адресов загрузки DLL. Однако при установке адреса загрузки во время компоновки следует быть немного осторожнее. Если адреса загрузки нескольких DLL-файлов установлены слишком близко друг к другу, то в окне Ouput появляется сообщение перераспределения загрузчика. Фокус в том, чтобы установить загрузочные адреса достаточно далеко друг от друга (чтобы не беспокоиться о них после того, как вы их установили).

    Для тех же DLL-файлов, что и в примере с REBASE.EXE, загрузочные адреса устанавливаются так:

    APPLE.DLL 0x60000000

    DUMPLING.DLL. 0x61000000

    GINGER.DLL 0x62000000

    GOOSEBERRIES.DLL 0x62100000

    Два файла — GINGER.DLL и GOOSEBERRIES.DLL - интересны потому, что их имена начинаются с одного и того же знака. Когда это случается, адреса загрузки дифференцируются по третьей старшей цифре.


    В случае добавления еще одного DLL-файла, имя которого начиналось бы с символа "G", его адрес загрузки был бы 0x62200000.

    Для того чтобы увидеть проект, в котором адреса загрузки установлены вручную, посмотрите на проект WDBG в разделе "WDBG: реальный отладчик" главы 4. Ключ /BASE позволяет также указать текстовый файл, содержащий адреса загрузки для каждого DLL в приложении (как это сделано в проекте WDBG).

    Хотя перебазировать DLL- и OCX-файлы может как метод, использующий REBASE.EXE, так и ручное перебазирование, но лучше следовать второму способу и перебазировать DLL-файлы вручную. Именно вручную были перебазированы DLL-файлы всех примеров на сопровождающем компакт-диске данной книги. Главное достоинство этого метода заключается в том, что устанавливаемый специфический адрес будет содержаться в МАР- файле. МАР- файл — это текстовый файл, который указывает, куда компоновщик помещает все символы и исходные строки программы. В выпускной конфигурации всегда следует создавать МАР- файлы, потому что они — единственное прямое текстовое представление символов, которое можно получить.

    МАР- файл содержит карту распределения глобальных символических имен (символов) конкретного приложения в памяти компьютера. Файл необязательный, он создается компоновщиком конкретной системы программирования (по запросу разработчика, через специальные ключи /MAP компоновщика) и имеет расширение .MAP. Описание состава, структуры и методики использования МАР-файлов для отладки приложений приводятся в главе 8. — Пер

    МАР- файлы окажутся особенно удобны в будущем, когда потребуется найти положение точки аварийного останова, а текущая версия отладчика не сможет прочитать старые символы. Если вместо ручного перебазирования DLL используется REBASE.EXE, то МАР -файл, созданный компоновщиком, будет содержать первоначальный базовый адрес, и нужно будет сделать некоторые арифметические вычисления, чтобы преобразовать адрес в МАР- файле в перебазированный адрес. В главе 8 МАР- файлы рассматриваются более подробно.


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

    Общий вопрос отладки

    Какие дополнительные параметры компилятора и компоновщика помогут мне с упреждающей отладкой?

    Параметры (ключи) компилятора и компоновщика помогают управлять выполнением и облегчают отладку приложений. Установки по умолчанию, которые обеспечивают мастера проектов Visual C++ для компилятора и компоновщика, годятся не на все случаи жизни. Поэтому некоторые из них приходится изменять.

    Параметры (ключи) компилятора CL.EXE

    Все ключи компилятора можно ввести с клавиатуры непосредственно в поле редактирования Project Options в нижней части вкладки C/C++ диалогового окна Project Settings.

    IP (препроцессорная обработка файла)

    Если у вас неприятности с макросами, ключ /р будет предварительно обрабатывать ваш исходный файл, расширяя все макросы, включая все include-файлы и посылая вывод в файл с тем же именем, но с расширением .1. Чтобы увидеть, как расширен ваш макрос, вы можете заглянуть в М-файл. Удостоверьтесь, что на диске имеете достаточно места, потому что М-файлы могут иметь объем в несколько мегабайт каждый. Если они слишком велики, то можно использовать ключ /ЕР (совместно с /р), чтобы подавить директивы #line, выводимые препроцессором. Директивы #line используются препроцессором для координации номеров строк и имен исходных файлов в файле препроцессора, так что компилятор может сообщать о расположении ошибок компиляции.

    /X (игнорировать стандартные пути)

    Получение правильной конфигурации приложения может иногда быть затруднено, если на машине разработчика установлено несколько компиляторов и SDK. Если этот ключ не указывается, то компилятор при вызове из МАК-файла будет использовать переменную среды INCLUDE. Для того чтобы точно управлять включением конкретных файлов заголовков, применяется ключ /х, заставляющий компилятор игнорировать переменную среды INCLUDE и искать файлы заголовков только в тех местах, которые явно указаны в ключе /I.


    /Zp (выравнивать члены структур)

    Разработчик не должен использовать этот флаг. Вместо того чтобы указывать в командной строке, как члены структуры должны быть выровнены в памяти, это следует сделать при помощи директивы ttpragma pack внутри конкретного файла заголовка. Источником ошибок является то, что команда разработчиков выполняла построение приложений, изначально установив ключ /Zp. Требуется много времени, чтобы найти такие ошибки.

    /GZ (отлавливать ошибки конфигурации версии в отладочной конфигурации)

    В Visual C++ 6 введено выдающееся отладочное свойство, при включении которого компилятор после вызовов функций автоматически инициализирует их локальные переменные и проверяет стек вызовов. Этот флаг включен по умолчанию для отладочных конфигураций, но можно также использовать его в конфигурациях версии. Если возникают неприятности с чтением неинициализированной памяти (wild reads), записью неинициализированной памяти (wild writes) или перезаписью памяти, создайте новую проектную конфигурацию, которая основана на конфигурации версии и добавьте данный ключ к параметрам компиляции. Просматривая локальные переменные, заполненные во время их создания значениями ОхСС, можно попытаться понять, что изменило их исходные значения в неподходящий момент.

    Кроме того, ключ /GZ будет генерировать код, который сохраняет текущий указатель стека перед косвенным вызовом функции (таким как вызов DLL-функции) и подтверждает, что указатель стека остается неизменным после вызова. Подтверждение правильности указателя стека предохраняет от одной из наиболее коварных ошибок описания, противоречащего соглашениям о вызовах. Эта ошибка происходит, когда вызываемая функция, специфицированная как _stdcall, неправильно объявлена со спецификатором _cdecl. Эти два спецификатора по-разному чистят стек, что позже приводит программу к аварийному сбою, если программист нарушает данное соглашение о вызовах.

    /О1 (минимизировать размер)

    По умолчанию проект, созданный с помощью мастера AppWizard библиотеки классов Microsoft Foundation Class (MFC), использует ключ /02 (максимизировать скорость) для построения конфигураций версии.


    Однако Microsoft строит все свои коммерческие приложения с ключом /01, который и следует указывать. В Microsoft нашли, что после выбора наилучшего алгоритма и записи плотного кода уход от страничных ошибок1может помочь значительно ускорить выполнение приложения.

    Страничные ошибки происходят, когда выполняющийся код переходит с одной страницы памяти (4 Кбайт для процессоров Intel x86) на следующую. Чтобы исправить страничную ошибку, операционная система должна прекратить выполнение вашей программы и разместить новую страницу в CPU. Если страничная ошибка — "мягкая" (т. е. страница уже находится в памяти), то издержки не слишком ужасны, но это, тем не менее, дополнительные издержки.

    Страничная ошибка (page fault) — ошибка из-за отсутствия страницы (ошибка, которая возникает в случае, когда процесс указывает на страницу виртуальной памяти, отсутствующую в рабочем наборе в главной памяти). — Пер.

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

    В дополнение к использованию ключа /01, следует обратить внимание на применение утилиты Working Set Tuner (WST) из Platform SDK. Утилита WST поможет упорядочить наиболее часто вызываемые функции в начале двоичного файла так, чтобы минимизировать рабочий набор (число страниц, хранящихся в памяти). С общими функциями в начале операционная система сможет выполнять свопинг ненужных страниц. Таким образом, приложение будет выполняться быстрее. Подробнее об использовании WST, см. февральскую колонку "Bugslayer" (1999) в Microsoft Systems Journal на MSDN.


    Ключи (параметры) компоновщика LINK.EXE

    Эти ключи можно ввести с клавиатуры прямо в поле редактирования Project

    Options в нижней части вкладки Link диалогового окна Project Settings.

    /MAP (генерировать МАР-файл)

    /MAPINFO:LINES (включать строчную информацию в МАР-файл)

    /MAPINFO:EXPORTS (включать экспортную информацию в МАР-файл)

    Эти ключи строят МАР-файл для связанного изображения. Следует всегда создавать МАР-файл, потому что это — единственный способ получить текстовую символическую информацию. Используйте все три ключа, чтобы гарантировать, что МАР-файл содержит наиболее полезную информацию.

    /NODEFAULTLIB (ignore libraries)

    Многие системные файлы заголовков включают записи #pragma comment (lib#, xxx), указывающие, с каким библиотечным файлом они связаны, где ххх — имя библиотеки. Ключ /NODEFAULTLIB сообщает, что компоновщик игнорирует директивы pragma. Этот ключ позволяет указать, с какими библиотеками надо держать связь и в каком порядке. Чтобы приложение имело связь с библиотеками, нужно указать каждую необходимую библиотеку в командной строке компоновщика, но по крайней мере нужно точно знать, какие библиотеки вы получаете и в каком порядке. Управление порядком, в котором связаны библиотеки, может быть достаточно важным, если один и тот же символ включен более чем в одну библиотеку, что может привести к ошибкам, которые очень трудно найти.

    /ORDER (разместить функции по порядку)

    После того как выполнена утилита WST, ключ /ORDER позволяет указать файл, который содержит упорядоченный список функций. Ключ /ORDER отменит инкре-ментную компоновку, так что указывайте его только в конфигурациях версии.

    /PDBTYPE:CON (Объедините PDB-файлы)

    Всегда указывайте ключ /PDBTYPEICON для всех конфигураций (как для выпускных, так и для отладочных). Для проектов Visual C++ этот параметр по

    умолчанию не включен. Ключ /PDBTYPE:CON объединяет всю отладочную информацию модуля в единый PDB-файл. Наличие единственного PDB-файла существенно облегчает отладку одного и того же двоичного кода несколькими пользователями, а также упрощает архивирование отладочной информации.


    /VERBOSE (печатать сообщения о ходе процесса)

    /VERBOSE:LIB (печатать только сообщения о найденных библиотеках)

    Если возникают затруднения с компоновкой, то эти сообщения могут показать, какие символы компоновщик ищет и где он их находит. Вывод может оказаться довольно громоздким, но покажет, где имеются проблемы, связанные с построением приложения. Я использовал ключи /VERBOSE и /VERBOSE : LIB, получив случайный сбой из-за того, что вызываемая функция выглядела (на уровне языка ассемблера) как-то не так, как должна была выглядеть, по моему представлению. Оказалось, что я имел две функции с идентичными сигнатурами, но различными реализациями в двух разных библиотеках, и компоновщик находил не ту, которая была нужна.

    /WARN:3

    Вообще-то этот ключ не нужен все время, но пару раз в течение жизни проекта программисту надо посмотреть, на какие библиотеки он фактически ссылается. Включив параметр /WARN:3, вы будете получать сообщения о том, имеются ли ссылки на библиотеки, переданные компоновщику LINK.EXE. Лично мне нравится точно знать, с какими библиотеками я связан, и я удаляю из списка компоновщика те библиотеки, на которые нет ссылок.

    Частые построения и интенсивные тесты обязательны

    Две наиболее важных части инфраструктуры — это система построения продукта (build system) и набор интенсивных тестов (smoke-tests) Система построения — это то, что выполняет и компонует продукт, а набор smoke-тестов включает тесты, которые выполняют программу и подтверждают, что она работает.


    Частые построения

    В идеале, следует строить приложение один раз в день. Конечно, размеры некоторых проектов настолько велики, что ежедневные построения не оставляют достаточно времени для его полного тестирования. Для таких больших и сложных проектов необходимо разработать схему, по которой построения будут выполняться так часто, как это возможно.
    При построении продукта следует строить как отладочную, так и выпускную версии одновременно. Как будет показано позже в этой главе, отладочные построения имеют решающее значение. Прерывание процесса построения нужно трактовать как ошибку. Если разработчики регистрируют код, который не откомпилирован, они должны заплатить некоторый вид штрафа за право ошибиться, например, в форме доставки пончиков и публичного признания в преступлении. Если в команде нет специального инженера, ответственного за построение версии продукта, то можно наказать виновника, сделав его ответственным за процедуры построения, пока не придет черед следующего нарушителя.
    Наилучшей практикой для ежедневных построений можно считать уведомление команды через электронную почту об окончании этого процесса. С учетом того, что построение автоматически происходит по ночам, первое сообщение, которое каждый член команды может отыскать утром, — это указание на то, потерпел ли этот процесс неудачу; если это так, то команда может предпринимать немедленные действия, чтобы исправить ситуацию.
    Чтобы избежать проблем с построением продукта, каждый член команды должен иметь одни и те же версии всех инструментов и частей системы построения. Как я упомянул ранее, чтобы усилить эту практику, некоторые команды любят хранить систему построения в подсистеме управления версией. Если у членов команды работают различные версии инструментов, включая уровни пакетов обслуживания, то вы получите море ошибок в построенном продукте. Если нет веской причины, чтобы кто-то использовал иную версию компилятора, никакой разработчик не должен выполнять его модернизацию (upgrade) самовольно.
    Ваша система построения продукта будет извлекать самые последние главные исходные файлы (master sources) из системы управления версией каждый раз, когда будет выполняться процесс построения (в идеале, разработчики должны также получать эти данные каждый день).
    Нет ничего хуже, чем проводить время, пытаясь решить неприятную проблему только для того, чтобы выяснить, что она связана со старшей версией файла на машине разработчика. Другое преимущество частого получения исходных данных разработчиками состоит в том, что оно позволяет осуществлять непрерывное построение. Благодаря частому извлечению, любая проблема с главным построением (master build) продукта автоматически становится проблемой для локального построения (local build) каждого разработчика. Когда прерываются ежедневные построения, то раздражаются менеджеры, а разработчики не любят, когда прерывается их локальное построение. Зная, что прерывание главного построения означает прерывание построения всех индивидуальных разработчиков, каждый вынужден ограничиться чистым кодом в главном источнике.

    Общий вопрос отладки

    Когда следует заморозить модернизацию (upgrade) компилятора и других инструментов?

    Как только завершена разработка ведущих свойств продукта (что также известно как разработка версии beta 1), вы определенно не должны модернизировать никакие инструменты. Изменяя код, нельзя позволить себе риск новой схемы оптимизации компилятора, независимо от того, насколько хорошо она обдумана. Ко времени, когда получена версия beta 1, уже выполнено некоторое существенное тестирование, и в случае изменения инструментов вы будете вынуждены повторно начать тестирование — с нуля.



    Планируйте время для построения систем отладки

    Начиная планировать проект, предусмотрите в нем время для построения систем отладки. С самого начала следует решить, как вы будете реализовывать аварийные обработчики (этой теме посвящена глава 9), разгрузчики файлов данных и другие инструменты, которые понадобятся, чтобы помочь воспроизвести проблемы, сообщенные в полях отчетов об ошибках. Целесообразно обращаться с системами обработки ошибок как со свойством продукта — это позволяет коллегам в компании видеть, как вы подходите к профилактике обработки ошибок.
    При планировании системы отладки нужно определить превентивную политику отладки. Первое и наиболее трудное — определить то, как возвращать состояния ошибок в проект. Следует выбрать один способ отладки и его придерживаться. Однажды я столкнулся с проектом (к счастью, я в нем не участвовал), в котором было три различных способа возврата ошибок: возврат значений, исключения setjmp/longjmp и через глобальную переменную ошибки, подобную переменной errno из исполнительной библиотеки языка С. Разработчики этого проекта сталкивались с очень большими трудностями при прослеживании ошибок через границы подсистем.
    К сожалению, я не могу рекомендовать какой-либо конкретный способ возврата ошибок как общий для всех ситуаций, потому что разработка в Windows слишком зависит от технологий и чужих компонентов. Такие технологии, как Component Object Model (COM — модель компонентного объекта), предписывают стандарт возврата ошибок. В общем случае, я предпочитаю СОМ-подход, при котором проверяется возвращаемое значение, а не выбрасывается объект типа С++- исключений. Некоторые С++ - разработчики могут со мной и не согласиться, но мои симпатии всегда на стороне простоты и ясности (что очевидно присуще СОМ-подходу).



    Постоянно отслеживайте изменения

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



    Проектируйте облегченную диагностическую систему для выпускных конфигураций

    Больше всего я ненавижу ошибки, которые случаются только на машинах одного или двух пользователей. Все остальные пользователи весело работают с вашим продуктом, но с машинами одного или двух происходит что-то уникальное, и понять это почти невозможно. Конечно всегда можно попросить пользователь отправить вам "плохо ведущую себя" машину, но эта стратегия не всегда практична. Если клиент находится в Карибском бассейне, вы могли бы добровольно отправиться туда и решить проблему. Никто не слышал, однако, чтобы компании так подходили к решению проблем качества их продукции. Не слышно ничего также и о толпах разработчиков, добровольно отправляющихся за Северный Полярный Круг, чтобы решить проблему.
    Если сбойная ситуация воспроизводится только на одной или двух машинах, необходим способ просмотра потока выполнения программы на этих машинах. Многие разработчики уже прослеживают поток выполнения через файлы регистрации и запись в журнал событий, но я хочу подчеркнуть важность этого журнала для решения проблем. Возможности решения проблем регистрации потока выполнения резко возрастают, если вся команда подходит к прослеживанию такого потока организованно.
    При регистрации информации особенно важно следование шаблону. Унифицированный формат способствует облегчению синтаксического анализа файла. В частности, можно автоматизировать чтение такого журнала при помощи Peri-сценария, выделяющего важные элементы.
    Вообще необходимо регистрировать все (ну почти все), что связано с проектами, но, как минимум, следует точно регистрировать отказы и аварийные ситуации. Нужно также попытаться ухватить логический смысл действий программы. Например, если программа выполняет файловые операции, то не стоит регистрировать такие мелкие детали, как "переход к смещению 23 в файле...", но желательно регистрировать открытие и закрытие файла. Тогда, если последний вход в журнале оказался: "Preparing to open D:\Foo BAR.DAT" (Подготовка к открытию D:\Foo\BAR.DAT), то почти наверняка файл BAR.DAT испорчен.

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

    Для регистрации в коде C++ я применяю макрос следующего типа (обратите внимание, что G_isLogging — глобальная переменная, которую могут видеть все модули). Наличие глобальной переменной позволяет сэкономить на вызове функции.

    //Visual C++ макрос для регистрации событий

    #define LOGGING(x) \

    if ( TRUE == G_IsLogging) \

    { \

    Logginglnfo ( x); \

    }

    Для Visual Basic, поскольку в этом языке нет макросов, я только проверяю глобальную переменную вручную. Читателю предоставляется возможность написать простое встраиваемое дополнение к IDE Visual Basic, которое позволяло бы с помощью кнопки добавлять все, кроме строк, для передачи в функцию регистрации.

    Visual Basic. Пример вызова функции регистрации

    i£ ( 1 = G_IsLogging) Then

    Logginglnfo ( "Подготовка к открытию" & sFile)

    End If

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

    Рассматривайте предупреждения как возможные ошибки

    Поскольку Visual Basic намного более чувствителен к ошибкам компиляции, чем C++, то все, что компилятор сообщает вам, является ошибками. Как знает каждый, кто компилировал какую-нибудь программу, более сложную чем "Hello, World!", C++ — значительно более свободный язык, который позволяет избежать многих неприятностей. Подобно Visual Basic, Visual C++ имеет некоторые трудные ошибки, аварийно завершающие компиляцию. Такие ошибки, как С2037 ("left of 'operator' specifies undefined struct/union 'identifier1— "слева от 'операция' указан неопределенный 'идентификатор' struct/union"), означают, что компилятор не может продолжать выполнение. Visual C++ отличается от Visual Basic в первую очередь тем, что первый может (кроме ошибок) выдавать также предупреждения.
    Эти предупреждения, в общем случае, означают, что некоторая часть кода неоднозначна, но компилятор сделает обоснованное предположение о правильном значении. Превосходным примером является предупреждение типа С4244 ("'conversion' conversion from 'typel' to 'type2', possible loss of data" — '"преобразование1преобразование из 'тип Г в 'тип2', возможная потеря данных"), которое всегда выводится при преобразованиях типов со знаком и без знака. Многие думают, что предупреждения являются только предупреждениями, но я считаю, что любое предупреждение — это то же самое, что ошибка, и что нужно трактовать его как ошибку— увидев предупреждение компилятора, следует остановиться и исправить текст программы, сделав его однозначным для компилятора.
    Те, кто изучал конструкцию компилятора, в частности синтаксический анализатор, вероятно, знают, что синтаксический анализ очень труден, особенно в столь сложном языке, как C++. Компилятор Visual C++ выдает предупреждения, сообщая, что код является неоднозначным, и пытается предположить, что программист имеет в виду. Позволять инструменту делать предположения за человека — это верный способ ввести ошибки. Борясь с ошибкой, необходимо прежде всего убедиться, что код компилируется без предупреждений.

    Проекты по умолчанию, создаваемые мастерами Visual C++, выдают предупреждения 3-го уровня, которые соответствуют ключу /из компилятора CL.EXE. Следующий уровень предупреждений — 4 (ключ /W4), и можно даже заставить компилятор трактовать все предупреждения как ошибки (ключ /их). Все эти уровни легко установить в диалоговом окне Project Settings интегрированной среды разработки (Integrated Development Environment — IDE) Visual C++. Уровень предупреждений устанавливается в комбинированном поле Warning Level на вкладке C/C++, General Category, а ключ /wx — с помощью флажка Warnings As Errors, расположенного ниже поля Warning Level.

    Можно почти доказать утверждение: "Все проектные конструкции должны компилироваться с предупреждениями уровня 4, и все предупреждения следует трактовать как ошибки", но действительность заставляет смягчить это требование. Во-первых, некоторые общие файлы заголовков не будут компилироваться с ключами /wх и /w4. Сам компилятор имеет пару ненужных информационных предупреждений, которые он трактует как реальные предупреждения, поэтому ключ /wх остановит компиляцию. Стандартная библиотека шаблонов (Standard Template Library — STL), поставляемая вместе с Visual C++, генерирует много предупреждений 4-го уровня. Некоторые проблемы порождает также применение шаблонов с компилятором. К счастью, программист может работать, не думая о большинстве этих проблем и предупреждений.

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


    Хотя целью и должно быть избавление от большинства предупреждений путем написания лучшего кода, но можно также выключать определенные ошибки с помощью директивы препроцессора #pragma warning, которая, кроме того, позволяет управлять уровнем ошибок вокруг определенных заголовков.

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

    #pragma warning ( push, 3)

    #include "IDoNotCompileAtWarning4.h"

    #pragma warning ( pop)

    С помощью директивы #pragma warning также можно выключать индивидуальные предупреждения. Эта директива оказывается полезной, если при использовании неименованной структуры или объединения получена ошибка С4201 ("nonstandard extension used: nameless struct/union" — "использовано нестандартное расширение: неименованная структура/объединение") с предупреждениями уровня 4. Чтобы выключить эти предупреждения, используется директива #pragma warning, как показано в следующем коде. Заметьте, что в нем прокомментировано выключение предупреждения и приведено соответствующее предупреждение. Выключая индивидуальные предупреждения, убедитесь, что ограничили область действия директивы #pragma warning определенной секцией кода. Если разместить директиву слишком высоко, то можно замаскировать другие проблемы в программе.

    // Выключить предупреждение

    // "nonstandard extension used: nameless struct/union"

    // потому что я не пишу переносимый код

    tpragma warning ( disable : 4201)

    struct S

    {

    float y;

    struct

    Это окно открывает команда Build из меню Build интегрированной среды разработки. — Пер.

    {

    int a;

    int b;

    int c;

    };.

    } *p_s;


    // Включить предупреждение обратно.

    #pragma warning ( default : 4201)

    Если STL не используется, то данная схема работает хорошо, а если да, то она может работать, а может и не работать. Прежде чем понижать уровень ошибок с помощью директивы #pragma warning ( push, 3), обязательно пытайтесь получить STL-заголовки, чтобы компилировать на 4-ом уровне предупреждений. При этом, может быть, придется выключить некоторые дополнительные индивидуальные предупреждения, но надо стремиться сохранять 4-й уровень предупреждений, если это возможно. Для пары проектов я так и не получил компилированного кода без предупреждений, как бы ни "вылизывал" программу. В этих случаях я понизил глобальный уровень предупреждений до 3. Даже тогда, однако, я сохранил включенным режим "Warnings As Errors" (ключ /wx).

    Суть в том, что следует пытаться компилировать с самым высоким возможным уровнем предупреждений и трактовать все предупреждения как ошибки , с самого начала работы над проектом. Впервые повысив уровень предупреждений проекта, вы, вероятно, будете удивлены количеством полученных предупреждений. Просмотрите код и исправьте каждое из них. Вероятно, простое исправление предупреждений устранит ошибку или две. Для тех, кто думает, что компиляция программы с ключами /W4 и /wx невозможна, есть иное доказательство: весь код примеров на сопровождающем эту книгу компакт-диске скомпилирован с установкой обоих флагов для всех конфигураций.

    В этой главе описаны важные

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

    Системы отслеживания ошибок

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



    Системы управления версией

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


    Smoke-тесты

    Если вы не знакомы с этим термином, то smoke-тест ("дымовой" тест) — это тест, который проверяет основные функциональные возможности продукта. Термин пришел из электронной промышленности. В некоторой точке жизненного цикла изделия инженеры-электронщики включают его в сеть, чтобы видеть, дымится ли оно (буквально). Если оно не дымится или, в худшем случае, — загорается, они считают это успехом. В большинстве ситуаций с программным обеспечением, smoke-тест — это просто прогон программы, показывающий, что она выполняется и поэтому достаточно хороша для серьезного тестирования. Smoke-тест — это измерительный прибор общего состояния кода.
    Smoke-тест — это просто контрольный список элементов, которыми программа может управлять. Начните с малого: установите приложение, запустите и затем закройте его. По мере продвижения через цикл разработки smoke-тест должен дорасти до проверки новых свойств продукта. В лучшем случае smoke-тест должен содержать по крайней мере один тест для каждого свойства и главного компонента изделия. В магазине информационных технологий (ITshop) это означает тестирование каждого из главных свойств, которые разработчик обещал менеджеру по информатизации (СЮ1) и клиенту. Имейте в виду, что с помощью smoke-теста не нужно исчерпывающе проверять каждую ветвь кода в программе, но его следует использовать, чтобы судить, работоспособно ли приложение в целом. Как только программа пройдет smoke-тест, инженеры по качеству могут начать трудную работу, пытаясь прервать (или даже разрушить) программу.
    Важный компонент smoke-теста — некоторый вид эталонного теста производительности2. Многие забывают включать его в тест и расплачиваются позже, в цикле разработки. Если эталонный тест установлен для определения, например, длительности выполнения последней версии продукта, то можно определить как отказ, если текущее выполнение на 10 или более процентов превышает значение, полученное по эталонному тесту. Просто удивительно, какое вредное воздействие на производительность может оказать маленькое изменение в безобидном, на первый взгляд, месте программы.
    Контролируя производительность по всему циклу разработки, вы можете решать проблемы производительности прежде, чем они выйдут из-под контроля.

    Идеальная ситуация для smoke-теста — та, в которой программа автоматизирована так, что она может выполняться, не требуя какого-либо взаимодействия с пользователем. Инструмент, который применяется для автоматизации ввода и операций в приложении, называется инструментом регрессивного тестирования (regression-testing tool). К сожалению, не всегда можно автоматизировать каждое свойство. На рынке много хороших инструментов регрессивного тестирования, и если вы работаете с большим, сложным приложением и можете назначить кого-то для поддержки smoke-тестов, следует рассмотреть покупку такого инструмента. Некоторые из этих инструментов перечислены в приложении 2.

    СIO — сокр. от Chief Information Officer (менеджер по информатизации — руководитель, отвечающий за развитие информационных технологий в рамках фирмы). — Пер.

    Тест, определяющий сравнительные характеристики производительности продукта. — Пер.

    Если вас устраивает просто посылка нескольких клавишных команд приложению, то можете взглянуть на тестирующую систему, которая рассмотрена в главе 13.

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

    Создайте программу установки

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



    Специалисты по качеству должны тестировать отладочные конфигурации

    Проблема, вообще говоря, состоит в том, что только разработчики извлекают выгоду из диагностики. Чтобы эффективнее помогать в решении проблем отладки, инженеры по качеству должны использовать также и отладочную конфигурацию.
    В начальных стадиях цикла разработки продукта инженеры по качеству должны чередовать работу с отладочной и выпускной конфигурациями. По мере развития продукта они должны постепенно все больше концентрироваться на выпускной конфигурации. Пока не достигнута отметка alpha-версии продукта, в которой достаточно реализованных свойств для демонстрации продукта заказчикам, инженеры по качеству должны использовать отладочную конфигурацию от двух до трех дней в неделю. Когда вы подойдете к работе с версией beta 1, они должны ограничиться (в работе с отладочной конфигурацией) двумя днями в неделю. После перехода к версии beta 2, когда все свойства и главные ошибки установлены, они должны ограничить работу с отладочной конфигурацией одним днем в неделю. После перехода к работе с версией-кандидатом (release candidate) они должны сосредоточиться исключительно на выпускной конфигурации.


    Стройте все конфигурации продукта с символами отладки

    Некоторые из моих рекомендаций по системе отладки почти бесспорны. Первая рекомендация, на которой я настаиваю уже много лет: выполняйте сборку всех конфигураций (builds) программного продукта, включая конфигурации выпуска (версии), с полными символами отладки. Символы отладки — это данные, которые позволяют отладчику показывать исходную и компилированную информацию кодовых строк, имена переменных и информацию о типах данных. Я не приветствую отладку двоичной конечной версии (релиза) полностью на уровне языка ассемблера — мне больше нравится экономить время.
    Конечно, отладка релиза с помощью символов имеет свои недостатки. Например, оптимизированный код, производимый компилятором, не всегда будет соответствовать потоку выполнения в исходном коде, так что вы можете обнаружить, что пошаговое выполнение кода выпуска несколько труднее, чем продвижение через код отладки. Еще одна проблема, возникающая иногда в конечных версиях, такова: компилятор оптимизирует стек регистров таким образом, что не виден полный стек вызовов (тогда как в прямой отладочной конфигурации он мог бы быть виден). Кроме того, после добавления символов отладки к двоичному коду объем кода увеличивается. Однако это увеличение незначительно по сравнению с возможностью быстро устранять ошибки.
    Включать символы отладки в выпускную (выходную) конфигурацию нетрудно. В Microsoft Visual Basic на вкладке Compile диалогового окна Project Properties включите флажок Create Symbolic Debug Info. Для проектов Microsoft Visual C++ потребуется два шага. Первый шаг — установка для компилятора (CL.EXE) режима размещения символов отладки в OBJ-фай-лах. В диалоговом окне Project Settings выберите элемент Win32 Release комбинированного поля Settings For, чтобы изменять только выпускные конфигурации. На вкладке C/C++ General Category комбинированного поля Debug Info выберите элемент Program Database. Эта установка добавит переключатель /zi к вашим компиляциям. Удостоверьтесь, что не выбран элемент Program Database For Edit And Continue (/ZI) — это привело бы к добавлению всех разновидностей наполнителей и другой информации в двоичные файлы так, чтобы во время отладки вы могли редактировать исходный код.

    Второй шаг для проектов Visual C++ — генерация символов отладки компоновщиком LINK.EXE. Для этого выберите элемент Release Win32 в комбинированном поле Settings For, и на вкладке Link General Category установите флажок Generate Debug Info. Эта установка включает ключ отладки (/DEBUG) компоновщика, который необходим для конструкций отладки. Вы также должны ввести с клавиатуры строку /OPT:REF в редактируемое поле Project Options на вкладке Link. Использование ключа отладки /DEBUG компоновщика автоматически предписывает ему вводить все функции (независимо от того, будут ли на них ссылаться или нет), которые необходимы для отладочных конфигураций. Ключ /OPT:REF предписывает компоновщику вводить только функции, которые программа вызывает непосредственно. Если этот ключ не добавлен, то версия приложения будет также содержать функции, которые никогда не вызываются, что намного увеличит ее размер.

    Хотя можно подумать, что включение отладочных символов сделает обратную разработку (reverse engineering) вашего приложения более легкой задачей, но, на самом деле, это не так. Если в проекте выбрана установка Program Database (PDB), то все символы отладки сохраняются в отдельных PDB-файлах, которые генерирует программа. Поскольку разработчики" не отправляют эти файлы заказчикам, дополнительные символы отладки нисколько не облегчат обратную разработку приложения.

    После того как будут построены выпускные конфигурации с полными PDB-файлами, нужно сохранить в безопасном месте PDB-файлы и все двоичные файлы, отправляемые заказчикам. Утеря PDB-файлов вынудит вас вернуться к отладке на уровне языка ассемблера. Рассматривайте PDB-файлы как распределенные двоичные данные.

    Управление изменениями

    Учет изменений жизненно необходим, однако наличие хорошей системы трассировки ошибок не означает, что разработчики могут произвольно изменять главные исходные файлы. Такой карт-бланш сделал бы весь учет бессмысленным. Идея состоит в том, чтобы управлять изменениями периода разработки, ограничиваясь лишь определенными типами изменений в некоторых стадиях проектирования, так чтобы можно было ежедневно оценивать состояние главных исходных файлов. Лучшая идея управления изменениями, о которой я когда-либо слышал, принадлежит моему другу Стиву Маньяну (Steve Munyan) и заключается в том, что вся разработка разделяется на три периода. В первом, который Стив назвал зеленым, допускаются любые изменения в главных исходных файлах ("горит зеленый свет"). Самые ранние части проекта обычно создаются именно в этот период, когда команда работает над новыми свойствами.
    Второй период — желтый — наступает, когда продукт находится на этапе исправления ошибок или приближается к "замораживанию" кода. Допускаются только изменения, связанные с исправлением ошибок. Добавление новых свойств или другие изменения не разрешаются. Перед регистрацией исправления ошибки ее должен одобрить технический руководитель или менеджер разработки. Разработчик, выполняющий исправление ошибки, должен описать как сам процесс исправления, так и то, на что он влияет. По существу, этот процесс является мини-обзором кода для каждого отдельного исправления ошибки. В некоторых командах, где я работал, "желтый" период начинался с первого дня, потому что разработчикам нравилось составлять обзоры кода на этой стадии. Одобрять изменения мог любой другой разработчик. Такой подход позволял отлавливать массу ошибок прежде, чем окончательно формировался код главных исходных файлов.
    На последнем этапе загорается красный свет, начинается этап замораживания кода и для любого изменения кода требуется одобрение менеджера разработки. Будучи таким менеджером, я даже ужесточил ограничение, разрешив доступ "только для чтения" (read-only) к файлам, составляющим проект. На этом этапе разработчики думают: "Это всего лишь маленькое изменение, которое исправляет ошибку и ничему не повредит". Благое намерение может заставить целую команду возобновить тестирование с самого начала.
    Руководитель проекта должен неукоснительно проводить в жизнь ограничения этого этапа. Если продукт имеет воспроизводимый аварийный отказ или в нем происходит разрушение данных, решение о внесении изменений принимается, по существу, автоматически. В большинстве случаев, однако, необходимость исправления специфической ошибки менее очевидна. Для того чтобы в этой ситуации принять правильное решение, необходимо, по моему мнению, ответить на следующие вопросы:
  • Сколько людей затрагивает эта проблема?
  • Вносятся изменения в ядро или в периферийную часть продукта?
  • Какие части приложения потребуется повторно протестировать?


  • Важность использования меток

    Одной из наиболее важных команд, применяемых в системе управления версией, является команда метки (label command). В системе Microsoft Visual SourceSafe метку называют "label" (метка), в MKS Source Integrity — контрольной точкой (checkpoint), а в PVCS Version Manager — меткой версии (version label). Различные системы управления версиями, как мы только что видели, могут ссылаться на команду метки различными способами, но независимо от того, как она называется, метка отмечает некоторый набор главных источников. Метка позволяет отыскать и извлечь отмеченную ею версию главных исходных файлов в будущем. Если при отметке вы сделаете ошибку, то можете навсегда потерять возможность идентифицировать главные исходные файлы, используемые индивидуальной версией, а следовательно, обнаружить, почему специфическая версия завершается аварийно.
    Решая вопрос о том, что нужно помечать, я всегда следовал следующим трем правилам:
    1. Помечайте все внутренние опорные точки.
    2. Помечайте любую конфигурацию продукта, которая посылается кому-то вне команды.
    3. Помечайте любые ежедневные версии проекта Во всех случаях я использую следующую схему:
    <имя-проекта> <Отметка/Причина> <Дата>
    поэтому имена меток становятся описательными. Что касается первых двух правил применительно к рассмотренным выше этапам разработки, то по окончании второго и третьего (желтого и красного) этапов применение меток обязательно.
    О третьем правиле установки меток многие забывают. Инженеры по качеству обычно работают с ежедневными построениями так, что когда они сообщают о проблеме, она относится к специфической версии главных источников. Поскольку разработчики могут изменять код быстро, желательно, чтобы они имели возможность достаточно просто возвращаться к точной версии файлов, в которых им нужно воспроизвести ошибку.
    Общий вопрос отладки
    Что делать, если возникают трудности с воспроизведением конфигураций для программистов, не входящих в команду?
    Компилируя проект для кого-то, кто не входит в команду, необходимо сделать полную копию каталога сборки проекта на компакт-диск или ленту. Эта копия должна включать все исходные файлы, промежуточные файлы, файлы символов и окончательный вывод (final output). Кроме того, в нее также следует включить комплект установки, который был послан заказчику.
    Даже сделав все возможное, чтобы сохранить специфическую конфигурацию в системе управления версией, нельзя быть уверенным, что в результате повторной сборки проекта будут получены двоичные файлы, идентичные исходным. Архивированием полного дерева конфигураций можно отлаживать проблемы пользователя точно с теми же двоичными данными, которые были отосланы.


    Выбор подходящих систем

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


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

    Блочное тестирование

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

    Дополнительной выгодой от тестирования проекта является то, что появляется возможность быстро находить и решать проблемы, что, в свою очередь, делает код повторно-используемым и расширяемым. Поскольку возможность многократного использования — священна для программирования, то любые усилия, потраченные на это, оправданы. Мне удалось удачно решить эту проблему при разработке обработчика аварийных сбоев, описанного в главе 9. Тестируя блоки в Windows 98, я заметил, что API-функция syminitiaiize из символьной машины DBGHELP.DLL перестала автоматически загружать символы всех модулей в процессах, как она это делала в Windows 2000. Предположив, что для решения этой задачи в Windows 98 нужна совсем другая утилита, я разработал функцию Bsusyminitialize. С ее помощью я выполнил все блочное тестирование программы обработчика аварийных сбоев, завершив тем самым эту разработку универсальным решением проблемы многократного использования.

    Во время кодирования следует постоянно выполнять блочные тесты. По моим представлениям, в отдельном функциональном блоке содержится приблизительно 50 строк кода. Каждый раз, добавляя или изменяя свойство, я повторно запускаю блочный тест, чтобы видеть, не испортил ли я что-нибудь. Неожиданности следует сводить к минимуму. Настоятельно рекомендую выполнять блочные тесты до регистрации кода в главных источниках (master sources). Некоторые организации используют специальные тесты, называемые регистрационными тестами, которые нужно выполнять перед регистрацией кода. Такие тесты значительно уменьшают число аварий при построениях программ и smoke-тестах.

    Ключом к большинству эффективных блочных тестов является понятие покрытия (охвата) кода (code coverage).


    Даже если вы не усвоите ничего в данной главе, кроме этого понятия, буду рассматривать это как успех. Покрытие кода — это просто процентная доля реально выполняемых операторов в вашем модуле. Если, например, в модуле закодировано 100 выполняемых операторов, а реально после запуска программы выполняется только 85 из них, то речь идет о 85%-ном покрытии кода. При этом предполагается, что любой, невыполненный оператор является потенциальным источником сбоя.

    Статистику покрытия кода можно получить двумя способами. Первый способ довольно трудоемок. Он предполагает использование отладчика и установку точек прерывания на каждом исполняемом операторе тестируемого модуля. Когда во время отладочного прогона модуль выполнит очередной оператор, очистите его точку прерывания. Продолжайте выполнение программы до тех пор, пока не будут очищены все точки прерываний (в этом случае покрытие кода считается 100%). Легче подсчитывать процент покрытия при помощи специальных программных инструментов независимых поставщиков, например, TrueCoverage от NuMega или Visual PureCoverage фирмы Rational.

    Лично я не регистрирую свой код в главных источниках, пока не добьюсь выполнения, по крайней мере, от 85 до 90 процентов его операторов. Полагаю, что многие читатели застонут прямо сейчас. Действительно, чтобы получить высокий процент кодового покрытия, нужно потратить много времени. Иногда для этого нужно провести гораздо больше тестов, чем обычно, и это также может потребовать много времени. Получив наилучшее покрытие, следует запустить приложение в отладчике и попытаться так изменить соответствующие переменные, чтобы выполнились и те ветви кода, которые иным способом выполнить трудно. Цель, однако, состоит в написании надежного кода и, по моему мнению, покрытие кода блочными тестами — это почти единственный способ ее достижения.

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

    Следуя рекомендациям этой главы, можно получить в конце разработки довольно эффективные блочные тесты, но работа на этом не заканчивается. Мои блочные тесты можно найти в каталоге ..\SourceCode\BugslayerUtil \Tests на сопровождающем компакт-диске. Я храню их как часть свой кодовой базы, так что коллеги могут легко их отыскать. Когда я вношу изменения в эту базу, то могу легко проверить, не нарушил ли я что-нибудь. Настоятельно рекомендую всем разработчикам установить свои тесты в систему управления версией. Хотя большинство блочных тестов самодокументировано, удостоверьтесь, что вы комментируете все ключевые предположения так, чтобы другие могли быстрее ознакомиться с вашими тестами.

    Как и что следует проверять с помощью утверждений

    Проверять нужно все — любое условие, потому что это может пригодиться для исправления ошибок в будущем. Не переживайте, что включение слишком большого количества утверждений будет препятствовать работе программы — утверждения обычно активны только в отладочных построениях, а создаваемые ими возможности поиска ошибок более чем оправдывают небольшое падение эффективности программы.
    Подчеркну, что операторы утверждений не должны изменять те переменные или состояния, которые они контролируют. Считайте, что все данные, которые проверяются в утверждениях, имеют атрибут "только для чтения". Поскольку утверждения активны только в отладочных построениях, то любая попытка модифицировать данные с их помощью приведет к изменению поведения программы в отладочных и выпускных конфигурациях, и проследить эти различия будет чрезвычайно трудно.
    Правила использования утверждений
    Первое правило: в одном операторе утверждения следует проверять только один элемент. Если в одном утверждении проверяется несколько условий, то нет никакой возможности узнать, какое из условий явилось причиной неудачи всего утверждения. В следующем примере показано два способа проверки одной и той же функции с помощью оператора ASSERT. Хотя утверждение в первой функции и будет отлавливать неправильный параметр, оно не сообщит, какое условие привело к неудаче (т. е. останется неизвестным, какой из трех параметров является неправильным).
    // Неправильный способ записи утверждения. Какой параметр был
    // неправильным?
    BOOL GetPathltem ( int i, LPTSTR szltem, int iLen)
    {
    ASSERT ( ( i > 0 ) &&
    ( NULL != szltem ) &&
    ( ( iLen > 0) && ( iLen < MAX_PATH) ) &&
    ( FALSE = IsBadWriteStringPtr ( szltem, iLen)));
    }
    // Подходящий способ. Каждый параметр проверяется
    // индивидуально, так что вы можете видеть, какой из них неправильный.
    BOOL GetPathltem ( int i, LPTSTR szltem, int iLen)
    {
    ASSERT ( i > 0);
    ASSERT ( NULL != szltem);
    ASSERT ( ( iLen > 0) && ( iLen < MAX_PATH));

    ASSERT ( FALSE == IsBadWriteStringPtr ( szltem, iLen));

    }

    Следует всегда стремиться к полной проверке условия. Например, если контролируемая функция в качестве параметра принимает указатель на функцию, и вы просто проверяете этот параметр на равенство значению NULL

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

    // Пример проверки только части ошибочного условия

    BOOL EnumerateListltems ( PFNELCALLBACK pfnCallback)

    {

    ASSERT ( NULL != pfnCallback);

    }

    Для полной проверки корректности указателя, в операторе ASSERT можно также использовать API-функцию isBadCodePtr:

    // Пример полной проверки ошибочного условия

    BOOL EnumerateListltems (PFNELCALLBACK pfnCallback)

    {

    ASSERT ( FALSE = IsBadCodePtr ( pfnCallback));

    }

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

    Пример неправильного определения аргумента оператора утверждения:

    отрицательное значение nCount не будет обнаружено функцией утверждения.

    Function UpdateListEntries(ByVal nCount As Integer) as Integer

    Debug.Assert nCount

    .

    .

    .

    End Function

    ' Здесь аргумент вызова определен так (nCount>0) , что Assert-функция

    ' правильно отреагирует на отрицательное значение nCount

    Function UpdateListEntries(ByVal nCount As Integer) as Integer

    Debug.Assert nCount > 0

    .

    .

    .

    End Function

    В первом примере, по существу, проверяются все значения nCount, не равные нулю, так что неправильные значения параметра (nCount <= 0) не будут обнаружены функцией Assert. Во втором условие проверки сформулировано точнее (nCount > 0), так что утверждение, во-первых, оказывается самодокументированным (т.


    к. в выражении nCount > 0 явно указано условие проверки) и, во-вторых, функция утверждения должным образом реагирует на все "неправильные" (<= 0) значения параметра nCount.

    Когда функция UpdateListEntries получает в качестве параметра неположительное значение, то функция-утверждение Debug.Assert обнаруживает этот факт при оценке выражения nCount > 0 и выводит на экран панель сообщений ASSERTION FAILURE. — Пер.

    В языках С и C++ имеются специальные проверочные функции, приведенные в табл. 3.1 и помогающие создавать достаточно описательные утверждения. Эти функции можно вызывать и из программ на Visual Basic, но делать этого не нужно из-за проблем с указателями.

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

    Функция

    Описание

    GetOb j ectType

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

    IsBadCodePtr

    Проверяет правильность указателя памяти

    IsBadReadPtr

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

    IsBadStringPt

    Проверяет, может ли строчный указатель читать символы до NULL-терминатора (или указанное максимальное число символов)

    IsBadWritePtr

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

    IsWindow

    Проверяет, является ли HWND-параметр правильным окном

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

    Visual Basic имеет свой набор функций, помогающих проверять достоверность специфических условий Visual Basic. Все эти функции перечислены в табл 3.2. Если разработчик, следуя общепринятой практике программирования на языке Visual Basic, не использует спецификатор variants и явно определяет спецификаторы ByVal и ByRef для параметров, то ему нет необходимости так часто проверять достоверность типов переменных. Если же вы не придерживаетесь подобной практики, то, по крайней мере, получаете некоторый набор хороших средств для выполнения такой проверки.

    Таблица 3.2. Справочные функции для описательных утверждений Visual Basic

    Однажды мой друг Фрэнсис Полин

    Однажды мой друг Фрэнсис Полин (Francois Poulin), работавший в службе сопровождения, явился на работу с нагрудной табличкой, которая гласила: "Пиши программы так, будто всякий, кто сопровождает твою программу — полный психопат, который знает, где ты живешь!" Фрэнсис никакой не психопат, но он предложил хорошую идею. Разработчик может, конечно, считать, что его программа — идеал ясности и совершенно понятна без всяких комментариев, но на самом деле она столь же плоха для тех, кто ее сопровождает, как сплошные строки команд ассемблера. Вспоминайте табличку Фрэнсиса каждый раз, когда пишете программу.
    Работа инженеров-разработчиков преследует две цели: создать решение для пользователя и сделать его удобным для поддержки в будущем. Единственный способ сделать код удобным для поддержки состоит в том, чтобы комментировать его. "Комментировать" не означает просто записывать комментарии, которые разъясняют, что именно делает программа; имеется в виду документирование предположений, подходов к решению задачи и причин для их выбора. Необходимо также отслеживать соответствие комментариев тексту программы.
    Рекомендую следующий подход к комментированию:
  • каждая функция или метод нуждаются в одном-двух предложениях комментариев, которые сообщают:
  • • что подпрограмма делает;
    • какие она использует предположения;
    • что она ожидает получить через каждый входной параметр;
    • что будет содержать каждый выходной параметр при успехе и отказе;
    • каждое возможное возвращаемое значение.
  • каждая часть функции, которая не полностью очевидна из кода, нуждается в одном или двух предложениях комментариев, которые объясняют, что она делает;
  • любой интересный алгоритм заслуживает полного описания;
  • любые нетривиальные ошибки, которые вы исправили в коде, должны быть прокомментированы с указанием номера ошибки и описанием сути исправления;
  • хорошо расположенные операторы трассировки и утверждений, а также удачные соглашения об именах могут служить отличными комментариями и обеспечивать поддерживающий контекст для кода;
  • комментируйте так, будто вы собрались сопровождать код в течение пяти лет;
  • если, просматривая код функции, вы произносите фразы типа: "Это — хакерская штучка" или "Это — действительно хитрый материал", то, вероятно, нужно переписывать функцию, а не комментировать ее.
  • Различие между серьезным, профессиональным разработчиком и тем, кто играет в него, выявляет надлежащая и полная документация в коде. Дональд Кнут (Donald Knuth) однажды заметил, что хорошо написанную программу нужно уметь читать так же, как хорошо написанную книгу. Хотя я не представляю себя лежащим на ковре у камелька с копией исходного кода программы ТеХ, но я полностью согласен с мнением доктора Кнута.
    Рекомендую изучить главу 19 "Self-Documenting Code" феноменальной книги Стива МакКоннелла (Steve McConnell) Code Complete (Microsoft Press, 1993). Читая ее, вы увидите, как я учился писать комментарии. Если вы комментируете правильно, то даже если сопровождающий программист окажется психопатом, не сомневайтесь — вы будете в безопасности.

    Обзор операторов утверждений для Visual C++ и Visual Basic

    В этом разделе приводится краткий обзор и обсуждение различных операторов утверждений, которые используются в языках Visual C++ и Visual Basic. Хотя следует отметить, что вместо них можно создавать и собственные несложные операторы-утверждения, подобные макросу ASSERT, который использован во всех предыдущих примерах.
    Макросы assert, _ASSERTw _ASSERTE
    Макрос assert исполнительной (run-time) библиотеки языка С определен стандартом ANSI С. Эта версия переносима на все С-компиляторы и платформы и определена во включаемом файле ASSERT.H. При сбое консольных Windows-приложений макрос утверждения assert посылает свое сообщение в стандартный поток вывода ошибок stderr. Если речь идет о Windows-приложении с графическим интерфейсом пользователя (GUI), то при его сбое assert выводит свое сообщение на экран в форме панели сообщений ASSERTION FAILURE... (см. рис. 3.1).
    Другой тип операторов утверждений исполнительной С-библиотеки специфичен для Windows. Это макросы _ASSERT и __ASSERTE, которые определены в файле CRTDBG.H. Единственное различие между ними в том, что _ASSERTE выводит в свою выходную панель также и выражение, получаемое через аргумент вызова. Отслеживать это выражение настолько важно, особенно когда программу тестируют специальные инженеры1, что следует всегда использовать именно макрос _ASSERTE, а не _ASSERT. Оба макроса являются частью чрезвычайно полезной отладочной С-библиотеки времени выполнения (подробное описание DCRT-библиотеки приведено в главе 15).
    Test engineers — тестирующие инженеры. — Пер.
    История отладочной войны Исчезновение файлов и потоков
    Сражение
    При работе с одной из версий программы BoundsChecker фирмы NuMega мы встретились с невероятно трудной проблемой случайных сбоев, которые было почти невозможно дублировать. Единственной зацепкой было то, что дескрипторы файлов и потоков иногда становились неправильными, приводя к беспорядочному закрытию файлов и срыву синхронизации потоков. Разработчиков интерфейса пользователя (U ^-разработчиков) также преследовали случайные сбои, но только при выполнении под отладчиком.
    Эти проблемы мучили нас во время разработки и, наконец, настал момент, когда вся команда бросила работу и принялась за исправление этих ошибок.

    UI — User Interface, интерфейс пользователя. — Пер.

    Подробнее оба отладочных процесса и их терминология поясняются в следующей главе. — Пер.

    Результат

    Меня буквально смешали с грязью, потому что оказалось, что проблема возникла из-за моей ошибки. В приложении BoundsChecker я отвечал за цикл отладки, и при этом использовалась отладочная библиотека Windows (Windows Debugging API), которая стартует один отладочный процесс (debugger) и управляет другим3 (debuggee), а также отвечает на события отладки, которые генерирует второй процесс. Будучи добросовестным программистом, я видел, что функция WaitForDebugEvent возвращала значения дескриптора некоторых уведомлений о событиях. Например, когда процесс стартовал под отладчиком, тот получал структуру, которая содержала дескриптор процесса и начальный поток для этого процесса.

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

    Однако, к великому моему огорчению, я не прочитал интересное замечание в документации по отладочному API, в котором говорится, что отладочный АР! сам закрывает любые дескрипторы, которые он генерирует. Происходило же следующее: я держал некоторые дескрипторы, полученные из отладочного API, до тех пор, пока в них нуждался, однако закрывал я их после того, как отладочный API их уже закрыл.

    Чтобы понять, как эта ситуация привела к проблеме, нужно знать, что когда дескриптор закрывается, операционная система помечает его как "доступный". Операционная система Windows NT 4, которую мы использовали в то время, является особенно агрессивной относительно повторного использования значений дескрипторов (Windows' 2000 демонстрирует такое же агрессивное поведение).


    Пользовательский интерфейс нашей программы был в значительной степени многопоточным и открывал много файлов, все время создавая новые дескрипторы. Поскольку отладочный API закрывал мои дескрипторы, а операционная система повторно использовала их, то иногда Ul-участки программы получали один из дескрипторов, которые хранились у меня, но когда позже я закрывал свои копии дескрипторов, то фактически закрывал и Ul-потоки, и дескрипторы файлов!

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

    Урок

    Можно было избежать этой проблемы, прочитав упомянутую выше заметку в документации отладочного API. Кроме того (и это— большой урок), я понял, что нужно всегда проверять значения, возвращаемые в cioseHandle. Хотя в такой ситуации мало что можно сделать, но когда вы закрываете неверный дескриптор, то операционная система хотя бы выдает сообщение о соответствующих неполадках, на которое нужно обратить внимание.

    Замечу, что если вы пытаетесь повторно закрыть дескриптор или передать некорректное значение в CioseHandle, выполняясь под отладчиком, то Windows NT 4 и Windows 2000 выводят следующее сообщение: "Invalid Handle exception (0x00000008)" (исключение (0x00000008) "Недействительный дескриптор"). Получив такое сообщение, можно остановить выполнение и попытаться выяснить, почему оно появилось.

    Хотя макросы assert, _ASSERT и _ASSERTE удобны в работе и бесплатны, они имеют несколько недостатков. С макросом assert связаны две проблемы.

    Во-первых, имя файла в его выходном сообщении усекается до 60 символов, так что иногда при завершении программы вы понятия не имеете, какой файл вызвал макрос утверждения. Вторая проблема возникает при работе с проектом, который не использует пользовательского интерфейса, например со службой Windows 2000 или внепроцессным СОМ-сервером.


    Когда макрос assert направляет свой вывод в стандартный выходной поток ошибок (stderr), то его можно легко пропустить. А если assert пытается направить свой вывод в панель сообщений, то консольное приложение повиснет, пытаясь закрыть эту панель, т. к. не использует UI-интерфейса и не может выводить на экран никаких окон.

    С другой стороны, макросы исполнительной С-библиотеки, по умолчанию направляющие вывод утверждений на панель сообщений, позволяют переадресовывать его в файл или к API-функции outputoebugstring, вызывая функцию __CrtsetReportMode. Однако все операторы утверждений, поставляемые компанией Microsoft, имеют один фатальный недостаток: они изменяют состояние системы (неизменность состояния является кардинальным правилом, которое утверждения не могут нарушать). Вызов утверждений с побочными эффектами едва ли не хуже, чем полный отказ от использования утверждений.

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

    //Послать сообщение окну. Если время такой посылки истекает (тайм-аут),

    // то другой поток зависает, так что нужно прервать данный поток.

    //Напоминаем, что единственный способ проверить, произошел ли сбой

    //функции SendMessageTimeout, состоит в том, чтобы проверить функцию

    // GetLastError. Если функция возвратила 0 и последняя ошибка есть 0,

    //то SendMessageTimeout выполнила тайм-аут.

    _ASSERTE ( NULL != pDataPacket)

    if ( NULL == pDataPacket)

    return ( ERR_INVALID_DATA);

    }

    LRESULT IRes = SendMessageTimeout ( hUIWnd,

    WM_USER_NEEDNEXTPACKET,

    0

    (LPARAM)pDataPacket ,

    SMTO_BLOCK ,

    10000

    &pdwRes ) ;

    _ASSERTE ( FALSE != IRes);

    if ( 0 == IRes)

    {

    // Получить значение последней ошибки.

    DWORD dwLastErr = GetLastError ();

    if ( 0 == dwLastErr)

    {

    // UI висит или нет достаточно быстрой обработки данных.

    return ( ERR_UI_IS_HUNG);

    }

    // Если ошибка в чем-то еще, то существует проблема

    //с данными, посылаемыми через параметр.


    return ( ERR_INVALID_DATA);

    }

    return ( ERR_SUCCESS);

    .

    .

    .

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

    _ASSERTE ( FALSE != IRes)

    (т. е. при вызове макроса с указанным параметром) выводится панель сообщения, а значение последней ошибки становится равным 0, так что в отладочных построениях UI-поток всегда кажется зависшим, тогда как в выпускных построениях бывают ситуации, в которых параметры, пересылаемые в функцию sendMessageTimeout, оказываются некорректными. Кому-то подобная ситуация может и не казаться такой уж проблемной, но мой собственный опыт был иным — с этой ситуацией оказались связанными две ошибки, на отслеживание и исправление которых пришлось потратить много времени. К счастью, представленное ниже утверждение SUPERASSERT позволяет решить эту проблему.

    Макросы ASSERT_KINDOFw 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 или на класс,


    // производньм от CFrameWnd.

    .

    .

    .

    Второй MFC-макрос-утверждение — ASSERT_VALID — сводится к вызову функции AfxAssertvaiidObject, которая подтверждает корректность указателя класса, производного от cobject. После подтверждения корректности указателя, макрос ASSERT_VALID вызывает объектный метод Assertvaiid. Для проверки внутренних структур данных в производных классах этот метод можно переопределять. Метод Assertvaiid считается сильным средством для выполнения глубоких проверок, поэтому нужно переопределять его для всех ключевых классов приложения.

    Оператор Debug.Assert

    С одной стороны, жизнь Visual Basic-программистов намного легче, чем у программистов C/C++, потому что Visual Basic не требует обширных проверок корректности типов параметров и указателей (до тех пор, пока не используются параметры типа variant). Однако, с другой стороны, правильно организовать профилактическое программирование средствами языка Visual Basic довольно трудно. В Visual Basic имеется всего один встроенный оператор утверждения — Debug.Assert, хотя у него и существует целых четыре версии.

    Это — хорошие новости. Но есть и плохие: Debug.Assert нельзя использовать, когда вы действительно в нем нуждаетесь, т. е. при отладке компилированного кода. Думаю, что создатели Visual Basic сделали большую ошибку,

    не разрешив функции Debug.Assert компилироваться в родной (native) код. Оператор Debug.Assert доступен только при выполнении внутри интегрированной среды разработки (IDE) Visual Basic. Когда функция утверждения терпит неудачу при отладке, то программист попадает в окно IDE на строку Debug.Assert. Хотя Debug.Assert активна только в IDE, желательно использовать ее в максимально возможной степени, чтобы контролировать все проблемы заранее, еще на уровне исходного кода.

    Для себя я разрешил все проблемы с Debug.Assert, когда просматривал книгу Advanced Visual Basic 6.0 (2nd ed., Microsoft Press, 1998) компании The Mandelbrot Set, основанной в Англии. Для этой книги Марк Пирс (Mark Реагс) написал замечательную дополнительную программу для Visual Basic, называемую Assertion Sourcerer.


    Она одна стоит целой книги (и остальная часть книги тоже превосходна). Эта программа автоматически отслеживает предложения Debug.Assert в исходной программе и помещает после них вызов реального оператора утверждения. Она также вычисляет имя исходного файла и номер строки, в которых была обнаружена проблема. Дополнительно к размещению реальных операторов утверждений в исходном коде, программа Assertion Sourcerer еще и убирает их, когда работа с ними заканчивается!

    Программу Марка Пирса можно легко расширить на поиск предложений Debug.Print и вставку после них реальных операторов трассировки. В листинге 3-2 показан исходный код авторского файла VBASSERTANDTRACE.BAS, который содержит реализации всех тех реальных операторов утверждений и трассировки, о которых только что шла речь. Для обработки утверждений в нем используется макрос SUPERASSERT, обсуждению которого посвящен следующий раздел.

    Листинг 3-2. Файл VBASSERTANDTRACE.BAS

    Attribute VB_Name = "VBAssertAndTrace"

    '''''''''''''''''''''''''''''''''''''''''''''

    ' Copyright (с) 1999-2000 John Robbins — All rights reserved.

    ' "Debugging Applications" (Microsoft Press)

    '

    ' Чтобы использовать этот файл:

    ' Не обязательно (но настоятельно!) рекомендуется:

    ' использовать подключаемый Visual Basic-модуль Assertion

    ' Sourcerer Марка Пирса (Mark Pearce) из

    ' "Advanced Microsoft Visual Basic 6.0" (2nd ed).

    ' Он будет отлавливать все предложения Debug.Assert

    ' программы и помещать под каждым таким предложением

    ' обращение к BugAssert, так что в компилированный Visual Basic-код

    ' будут вставлены реальные операторы утверждений.

    ' В работе с Debug.Assert придерживайтесь следующих правил:

    ' 1. Компилируйте BUGSLAYERUTIL.DLL, потому что этот файл

    ' использует несколько экспортированных функций.

    ' 2. Разместите операторы Debug.Assert в исходном коде программы.

    ' 3. Когда вы будете готовы компилировать программу, используйте

    ' подключаемый модуль Марка Пирса, чтобы добавить обращения к


    ' BugAssert.

    ' 4. Добавьте данный файл в ваш проект.

    ' 5. Компилируйте свой проект и понаблюдайте за утверждениями.

    ' Можно также вызывать различные функции библиотеки

    ' BUGSLAYERUTIL.DLL, чтобы установить различные опции и выходные

    ' дескрипторы.

    '''''''''''''''''''''''''''''''''''''''''''

    Option Explicit

    ' Объявить все функции BUGSLAYERUTIL.DLL, которые этот модуль

    ' может вызывать.

    Public Declare Sub DiagOutputVB Lib "BugslayerUtil" _

    (ByVal sMsg As String)

    Public Declare Function DiagAssertVB Lib "BugslayerUtil" _

    (ByVal dwOverrideOpts As Long, _

    ByVal bAllowHalts As Long,

    _ ByVal sMsg As String) _

    As Long

    Public Declare Function AddDiagAssertModule Lib "BugslayerUtil" _

    (ByVal hMod As Long) _

    As Long

    Public Declare Function SetDiagAssertFile Lib "BugslayerUtil" _

    (ByVal hFile As Long) _

    As Long

    Public Declare Function SetDiagAssertOptions Lib "BugslayerUtil" _

    (ByVal dwOpts As Long) _

    As Long

    Public Declare Function SetDiagOutputFile Lib "BugslayerUtil" _

    (ByVal dwOpts As Long) _

    As Long

    Private Declare Function GetModuleFileName Lib "kerne!32" _

    Alias "GetModuleFileNameA" _

    (ByVal hModule As Long, _

    ByVal IpFileName As String, _

    ByVal nSize As Long) _

    As Long Public Declare Sub DebugBreak Lib "kerne!32" ()

    ' Авторский макрос TRACE. Его можно использовать для вызова любого

    ' другого макроса. Кроме того, программа Assertion Sourcerer расширена

    ' для добавления TRACE-операторов (после предложений Debug.Print)

    Public Sub TRACE(ByVal sMsg As String)

    DiagOutputVB sMsg End Sub

    ' Функция BugAssert, вставленная с помощью

    ' Assertion Sourcerer

    Public Sub BugAssert(ByVal vntiExpression As Variant, sMsg As String)

    CallAssert vntiExpression, 0, sMsg

    End Sub

    ' Подпрограмма SUPERASSERT.

    Public Sub SUPERASSERT{ByVal vntiExpression As Variant, sMsg As String)


    CallAssert vntiExpression, 7, sMsg

    End Sub

    Private Sub CallAssert{ByVal vntiExpression As Variant, _

    ByVal iOpt As Long,

    _ sMsg As String)

    If (vntiExpression) Then

    Exit Sub Else

    ' Следующий флажок используется, чтобы определить, вызывалась ли

    ' уже функция InDesign. Вызывать эту функцию повторно нет

    ' необходимости.

    Static bCheckedDesign As Boolean 'False по умолчанию.

    ' Флажок, разрешающий остановки, я пересылаю в DiagAssertVB.

    ' Если этот флажок установлен в 1, то DiagAssertVB разрешит

    ' останавливать приложение. Если этот флажок установлен в 0,

    ' приложение выполняется в VB IDE так, что DiagAssertVB не будет

    ' разрешать остановки. Если пользователь запускается

    ' внутри VB IDE, то прерывание довольно опасно и может погубить

    ' весь ваш дневной труд!

    Static lAllowHalts As Long

    ' Вызвать InDesign только раз.

    If (False = bCheckedDesign) Then

    If (True = InDesign()) Then

    lAllowHalts = 0

    Else

    lAllowHalts = I

    End If

    bCheckedDesign = True

    End If

    Dim IRet As Long

    IRet = DiagAssertVB(iOpt, lAllowHalts, sMsg)

    If (I = IRet) Then

    ' Пользователь .хочет прервать выполнение. Однако

    ' прерывание не разрешается, если выполнение

    ' происходит внутри VB IDE.

    If (1 = lAllowHalts) Then

    DebugBreak

    End If

    End If

    End If

    End Sub

    '''''''''''''''''''''''''''''''''''''''''

    ' Эта замечательная функция взята из превосходной главы Пита Морриса "On

    ' Error GoTo To Hell" (с.25,26 в "Advanced Microsoft Visual Basic 6.0")

    ' InDesign позволяет проверять, выполняетесь ли вы в VB IDE. Я благодарен

    ' Питу, разрешившему мне использовать эту функцию!

    '''''''''''''''''''''''''''''''''''''''

    Public Function InDesign() As Boolean

    ' Я оставлю только один комментарий Пита — он превосходен.

    ' Только для этого и нужен Debug.Assert!

    Static nCallCount As Integer

    Static bRet As Boolean ' По умолчанию этот флажок False.

    nCallCount = nCallCount + 1

    Select Case nCallCount

    Case 1: ' Первый вход (выполнение Debug.Assert)

    Debug.Assert InDesign() Case 2: ' Второй вход, когда Debug.Assert уже выполнен

    bRet = True

    End Select

    ' Если был вызван Debug.Assert, возвратить True,

    ' чтобы предотвратить ловушку.

    InDesign = bRet

    ' Переустановить счетчик для будущих вызовов.

    nCallCount = 0

    End Function

    Операторы утверждений

    Большинство читателей, конечно, знают, что такое утверждение, потому что это наиболее важный инструмент профилактического программирования в арсенале отладки. Для тех, кто не знаком с термином, приведем краткое определение: утверждение — это специальный программный оператор, который проверяет (в определенной точке программы) истинность некоторого условия.
    Термин assertion (англ.) является логическим утверждением (или суждением), проверяющим некоторые (в данном случае программные) условия на истинность (true) или ложность (false). — Пер.
    Если контролируемое утверждением условие ложно (имеет значение false), то говорят, что проверка окончилась неудачей. Утверждения следует использовать в дополнение к нормальной проверке ошибок. Традиционно, утверждения определяются в программе, в виде специальных функций или макросов1, которые выполняются только в отладочных построениях. В случае ложности контролируемого утверждением условия, оператор утверждения посылает на экран монитора сообщение, в котором указывается, проверка какого условия оказалась неуспешной (т. е. в момент проверки условие имело значение false). Я расширяю определение утверждения, включая в него условно компилируемый код, проверяющий условия и предположения, которые являются слишком сложными для обработки в обычном операторе утверждения. Утверждения — ключевой компонент профилактического программирования, потому что они помогают разработчикам и тестирующим инженерам определять не только то, что ошибки присутствуют, но также и почему они происходят.
    Если вы что-то и слышали про операторы утверждений или даже изредка используете их в своих программах, вполне возможно, что с методикой их эффективного использования вы знакомы еще недостаточно. Сколько бы утверждений программист ни включал в программу, их никогда не может быть слишком много.
    Если программа содержит достаточное количество операторов утверждений, то при первых же признаках неблагополучия они выведут на экран монитора (через панели сообщений) значительную часть той информации, которая нужна для диагностики проблемы. Хорошо сконструированный оператор утверждения сообщит, где и почему проверяемое условие стало ложным. Кроме того, он позволит войти в отладчик сразу же после того, как будет обнаружена несостоятельность проверяемого условия, что позволит отслеживать состояние программы прямо в точках обнаружения неисправностей.
    Еще один плюс от использования большого количества утверждений состоит в том, что они служат в качестве дополнительной документации для исходного кода программы (хотя и не могут совсем заменить комментарии).
    Обычно с именами Assert — для функций, или ASSERT — для макросов (см. примеры этого раздела). — Пер


    Описание Функции isArray Проверяет

    BOOL CheckDriveFreeSpace ( LPCTSTR szDrive)
    {
    ULARGE_INTEGER ulgAvail;
    ULARGE_INTEGER ulgNumBytes;
    ULARGE INTEGER ulgFree;
    if ( FALSE == GetDiskFreeSpaceEx ( szDrive ,
    &ulgAvail ,
    &ulgNumBytes ,
    &ulgFree ))
    {
    ASSERT ( FALSE);
    return ( FALSE);
    }
    }
    Здесь использован обычный макрос ASSERT, но в нем не специфицировано проверяемое условие. Панель сообщения данного утверждения показывала только, что проверяемое условие имеет значение FALSE, так что от него было мало пользы. При вызове функции утверждения нужно пытаться получать (через панель сообщения) как можно больше информации относительно ее неудачного завершения.
    Мой друг Дейв Анджел (Dave Angel) указал мне, что в операторе ASSERT языков С и C++ можно использовать логическую операцию NOT (!), а в качестве ее операнда — строку символов. Эта комбинация позволяет выводить более информативные сообщения, из которых, по крайней мере, можно почерпнуть какую-то идею относительно того, в чем же заключалась ошибка (без просмотра исходного кода). Следующий пример показывает надлежащий способ контроля ложного условия. К сожалению, уловка Дейва Анджела не работает в Visual Basic.
    // Надлежащее использование утверждения
    BOOL CheckDriveFreeSpace ( LPCTSTR szDrive)
    {
    ULARGE_INTEGER ulgAvail;
    ULARGE_INTEGER ulgNuinBytes;
    ULARGE INTEGER ulgFree;
    if ( FALSE = GetDiskFreeSpaceEx ( szDrive ,
    &ulgAvail ,
    &ulgNumBytes,
    &ulgFree ))
    {
    ASSERT ( !"GetDiskFreeSpaceEx failed!");
    return ( FALSE);
    }
    }
    Можно усовершенствовать прием Дейва, используя для формирования проверочного условия логическую операцию AND (&&). В следующем примере показано, как можно добавить к тексту обычного ASSERT-сообщения дополнительное уточняющее сообщение:
    BOOL AddToDataTree ( PTREENODE pNode)
    {
    ASSERT ( ( FALSE == IsBadReadPtr
    ( pNode, sizeof ( TREENODE))) &&
    "Invalid parameter!");

    .
    .
    .
    }
    Что проверяют операторы утверждений
    Теперь посмотрим, что необходимо проверять с помощью операторов утверждений. Судя по предыдущим примерам, прежде всего нужно проверять те данные, которые поступают в функцию через аргументы ее вызовов из других программ. Существует опасность, что таким образом в функцию будут направляться некорректные данные (например, данные некорректных для этой функции типов или значений). Выполняя проверки соответствующих параметров, операторы утверждений значительно облегчают процесс отладки и реализуют идею профилактического программирования.
    Ниже показан исходный код одной из ключевых функций (stopDebugging) простого отладчика, описанного в главе 4, в которой для контроля параметра использован макрооператор ASSERT. Обратите внимание, что в теле функции сначала выполняется оператор утверждения (ASSERT) и сразу за ним — обработка реальной ошибки. Напомним, что операторы утверждений лишь контролируют правильность параметров, да и то лишь на этапах отладки, и никоим образом не заменяют нормальной обработки ошибок.
    BOOL DEBUGINTERFACE_DLLINTERFACE _stdcall
    StopDebugging ( LPHANDLE IpDebugSyncEvents)
    {
    ASSERT ( FALSE ==
    IsBadWritePtr ( IpDebugSyncEvents,
    sizeof ( HANDLE) * NUM_DEBUGEVENTS));
    if ( TRUE == IsBadWritePtr ( IpDebugSyncEvents,
    sizeof ( HANDLE) * NUM_DEBUGEVENTS))
    {
    SetLastError ( ERROR_INVALID_PARAMETER);
    return ( FALSE);
    }
    // Сигнал потоку отладки с именем события его закрытия.
    VERIFY ( SetEvent ( IpDebugSyncEvents[ CLOSEDEBUGGER ]));
    return ( TRUE);
    }
    Параметры внутренних private-функций программного модуля не всегда нуждаются в контроле с помощью утверждений. Он необходим только в том случае, если речь идет о внешнем вызове внутренней функции. Более того, если параметр, используемый внешним вызовом, однажды уже прошел через проверку оператором утверждения (на этапе отладки, например), то нет необходимости повторять такие проверки в отлаженном (рабочем) варианте программы.


    Это значит, что из отлаженного варианта все операторы утверждений можно спокойно удалить. Однако на этапе отладки иногда полезно использовать сплошные ASSERT-проверки — для всех параметров всех внутренних функций модуля. Возможно, это позволит отловить некоторые внутренние ошибки в модуле.
    При выборе параметров для проверок с помощью утверждений полезно придерживаться некоторой средней линии поведения и проверять не все, а лишь наиболее "опасные" для устойчивой работы модуля параметры внутренних вызовов. Правильный отбор таких параметров возможен лишь после приобретения достаточных навыков в разработке программ. Только накопив определенный опыт программирования, вы почувствуете, в каких точках программы можно столкнуться с проблемами, и сможете отобрать внутренние параметры для проверок с помощью утверждений.
    Другими объектами ASSERT-контроля являются возвращаемые значения функций. ASSERT-оператор проверяет корректность возвращаемого значения непосредственно перед его возвратом в вызывающую программу. Некоторые разработчики предпочитают проверять с помощью операторов утверждений почти каждое возвращаемое значение. В листинге 3-1 приводится определение функции startDebugging (из отладчика, описанного в главе 4), использующее операторы утверждений для проверки корректности возвращаемых значений. Если в функции вычисляется некорректное значение, то оператор утверждения выводит на экран предупреждающее сообщение.
    Листинг 3-1. Примеры ASSERT-проверок возвращаемых значений ;
    HANDLE DEBUGINTERFACE_DLLINTERFACE _stdcall
    StartDebugging ( LPCTSTR szDebuggee ,
    LPCTSTR szCmdLine ,
    LPDWORD IpPID ,
    CDebugBaseUser * pUserClass ,
    LPHANDLE IpDebugSyncEvents )
    {
    // ASSERT-проверки параметров.
    ASSERT ( FALSE == IsBadStringPtr ( szDebuggee, MAX__PATH));
    ASSERT ( FALSE == IsBadStringPtr ( szCmdLine, MAX_PATH));
    ASSERT ( FALSE == IsBadWritePtr ( IpPID, sizeof ( DWORD)));
    ASSERT ( FALSE == IsBadReadPtr ( pUserClass,


    sizeof ( CDebugBaseUser *)));
    ASSERT' ( FALSE == IsBadWritePtr ( IpDebugSyncEvents,
    sizeof ( HANDLE) *
    NUM_DEBUGEVENTS));
    // Обычные проверки параметров.
    if ( ( TRUE == IsBadStringPtr ( szDebuggee, MAX_PATH) ) ||
    ( TRUE •== IsBadStringPtr ( szCmdLine, MAX_PATH) ) ||
    ( TRUE — IsBadWritePtr ( IpPID, sizeof ( DWORD) )) ||
    ( TRUE == IsBadReadPtr ( pUserClass,
    sizeof ( CDebugBaseUser *))) ||
    ( TRUE == IsBadWritePtr ( IpDebugSyncEvents,
    sizeof ( HANDLE) *
    NUM_DEBUGEVENTS) ) )
    {
    SetLastError ( ERROR_INVALID_PARAMETER);
    return ( INVALID_HANDLE_VALUE);
    }
    // Обработка начального уведомления о том, что данная
    // функция будет ждать, пока не начнет выполняться отладочный поток HANDLE hStartAck;
    // Строка, используемая для начального уведомления TCHAR szStartAck [ МАХ_РАТН ];
    // Загрузить строку начального уведомления,
    if ( 0 == LoadString ( GetDllHandle () ,
    IDS_DBGEVENTINIT ,
    szStartAck ,
    sizeof ( szStartAck) ))
    {
    ASSERT ( !"LoadString IDS_DBGEVENTINIT failed!");
    return ( INVALID_HANDLE_VALUE);
    }
    // Создать событие начального уведомления.
    hStartAck = CreateEvent ( NULL , // Безопасность по умолчанию
    TRUE , // Событие ручной переустановки FALSE ,
    // Сигнал Initial state = Not szStartAck);
    // Имя события ASSERT ( FALSE != hStartAck);
    if ( FALSE == hStartAck)
    {
    TRACE ( "StartDebugging : Unable to create Start Ack event\n");
    return ( INVALID_HANDLE_VALUE);
    }
    // Связать параметры.
    THREADPARAMS StParams;
    stParams.lpPID = IpPID;
    stParams.pUserClass = pUserClass;
    stParams.szDebuggee = szDebuggee;
    stParams.szCmdLine = szCmdLine ;
    // Дескриптор для потока отладки HANDLE hDbgThread;
    // Попытка создать поток.
    hDbgThread = (HANDLE)_beginthread ( DebugThread, 0, sstParams);
    ASSERT ( NULL != hDbgThread);
    if ( NULL == hDbgThread)
    {
    TRACE ( "StartDebugging : _beginthread failed\n");


    VERIFY ( CloseHandle ( hStartAck));
    return ( INVALID_HANDLE_VALUE);
    }
    // Ждать, пока поток отладки не стабилизируется, и запустить
    ::WaitForSingleObject ( hStartAck, INFINITE);
    // Освободить дескриптор уведомления.
    VERIFY ( CloseHandle ( hStartAck));
    // Проверить, выполняется ли еще поток отладки. Если нет,
    // отладка, вероятно, не может стартовать.
    DWORD dwExitCode = ~STILL_ACTIVE;
    if ( FALSE == GetExitCodeThread ( hDbgThread, SdwExitCode))
    {
    ASSERT ( !"GetExitCodeThread failed!");
    return ( INVALID_HANDLE_VALUE);
    }
    ASSERT ( STILL_ACTIVE = dwExitCode);
    if ( STILL_ACTIVE != dwExitCode)
    {
    TRACE ( "StartDebugging : GetExitCodeThread failedXn");
    return ( INVALID_HANDLE_VALUE);
    }
    // Создать события синхронизации, чтобы главный поток мог
    // сообщать циклу отладки, что делать.
    BOOL bCreateDbgSyncEvts =
    CreateDebugSyncEvents ( IpDebugSyncEvents, *lpPID);
    ASSERT ( TRUE = bCreateDbgSyncEvts);
    if ( FALSE = bCreateDbgSyncEvts)
    {
    // Это — серьезная проблема. Есть выполняющийся поток отладки,
    //но нет возможности создавать события синхронизации, которые
    // нужны потоку пользовательского интерфейса для управления
    // потоком отладки. Здесь можно только завершить поток отладки и
    // выполнить возврат. Больше сделать ничего нельзя.
    TRACE ( "StartDebugging : CreateDebugSyncEvents failedW) ;
    VERIFY ( TerminateThread ( hDbgThread, (DWORD)-1));
    return ( INVALID_HANDLE_VALUE);
    }
    return ( hDbgThread);
    }
    И, наконец, операторы утверждений используются в том случае, когда возникает необходимость проверить некоторое предположение. Например, если в спецификациях функции говорится о том, что она требует 3 Мбайт дискового пространства, то нужно проверить это предположение с помощью оператора утверждения. Другой пример: если функция получает (через аргумент вызова) массив указателей на определенную структуру данных, то необходимо проверить данные этой структуры и подтвердить правильность каждого индивидуального элемента.


    В обоих этих случаях, как и при проверке большинства других предположений, нет возможности проверять предположение с помощью обычных функций или макросов. В этих ситуациях следует использовать технику условной компиляции, которая, как указывалось ранее, должна стать частью комплекта инструментов для проверки утверждений. Поскольку код, который выполняется во время условной компиляции, работает на "живых" данных, нужно предпринять дополнительные меры предосторожности, гарантирующие неизменность состояния программы. В программах на Microsoft Visual C++ и Visual Basic я предпочитаю, если возможно, реализовывать эти типы утверждений в виде отдельных функций. Таким способом можно защитить от изменений любые локальные переменные внутри исходной функции. Кроме того, условно компилированные функции утверждений могут свободно использовать окно Watch (об этом мы поговорим в главе 5, где речь пойдет об отладчике Visual C++). Следующий пример показывает условно компилированную функцию-утверждение ValidatePointerArray, которая выполняет глубокие проверки корректности на массивах данных.
    #ifdef _DEBUG
    void VaiidatePointerArray ( STDATA * pData, int iCount)
    {
    // Сначала проверить буфер массива.
    ASSERT ( FALSE == IsBadReadPtr ( pData,
    iCount * sizeof ( STDATA *)));
    for ( int i = 0; i < iCount; i++)
    {
    ASSERT ( pData[ i ].bFlags < DF_HIGHVAL);
    ASSERT { FALSE == IsBadStringPtr ( pDataf i ].pszName,
    MAX_PATH));
    }
    }
    #endif
    void PlotDataltems ( STDATA * pData, int iCount)
    #ifdef _DEBUG
    VaiidatePointerArray ( pData, iCount);
    #endif
    }
    Макрос VERIFY
    Прежде чем двигаться дальше, поговорим о макросе VERIFY, который использовался при разработке библиотеки классов Microsoft Foundation Classes MFC). В отладочных построениях этот макрос ведет себя так же, как обычное утверждение: если условие установлено в 0, то VERIFY открывает панель с предупреждающим сообщением. Однако, в отличие от обычного утверждения, в выпускной конфигурации параметр этого макроса остается в исходном коде и считается нормальной частью программной процедуры.


    В сущности, VERIFY можно рассматривать как нормальное утверждение с побочными эффектами, которые сохраняются и в выпускных конфигурациях программы. Строго говоря, в утверждениях любого типа нельзя использовать условия, вызывающие какие-либо побочные эффекты. Все-таки в одной ситуации макрос VERIFY полезен: когда имеется функция, возвращающая ошибочное значение, которое нельзя проверить другим способом. Например, если вызывается функция ResetEvent, чтобы очистить дескриптор свободного события, и вызов терпит неудачу, то мало что можно сделать. Вот почему большинство программистов вызывает ResetEvent и никогда не проверяет возвращаемое значение ни в отладочных, ни в выпускных построениях. Если поместить вызов в макрос VERIFY, то, по крайней мере, можно будет получить уведомление во время отладочных построений, что что-то не так. Конечно, можно достичь тех же результатов, используя ASSERT, но VERIFY избавляет от необходимости создавать новую переменную только для того, чтобы сохранять и проверять возвращаемое значение. Такая переменная, вероятно, использовалась бы только в отладочных построениях.
    Многие MFC-программисты, вероятно, применяют макрос VERIFY просто по привычке. Однако в большинстве случаев вместо этого нужно проверять возвращаемое значение. Хорошим примером использования VERIFY является метод cstring: :Loadstring, который загружает строки ресурса. Такой способ хорош в отладочных построениях, потому что если Loadstring завершается неудачно, то макрос VERIFY предупреждает об этом. Однако если сбой Loadstring происходит в выпускном построении, то приходится заканчивать работу с неинициализированной переменной. В лучшем случае здесь можно получить незаполненную строку, но, скорее всего, задача будет завершаться аварийно. Мораль этой истории заключается в том, что возвращаемые значения нужно проверять. Если же вы собираетесь использовать макрос VERIFY, то нужно всегда задаваться вопросом, не приведет ли отказ от проверки возвращаемого значения к каким-нибудь проблемам в выпускном построении?


    Программа SUPERASSERT

    Рассмотрев проблемы, возникающие с системными утверждениями, покажем, как можно усовершенствовать оператор утверждения, чтобы существенно расширить выводимую им информацию о причинах возникновения проблемы. На рис. 3.1 показан пример панели сообщений программы SUPERASSERT. Поля Program, File и Line самоочевидны. Интерес представляют те, что следуют за полем Last Error.
    В SUPERASSERT значения последних ошибок переводятся в их текстовые представления. При сбоях функций API просматривать сообщения об ошибках в текстовой форме чрезвычайно полезно, т. к. можно сразу же увидеть, почему соответствующие функции завершились неудачно, и быстрее начинать отладку. Например, если функция GetModuieFileName завершается потому, что размер входного буфера недостаточен, SUPERASSERT установит значение последней ошибки равным 122, что соответствует строчному значению ERROR_INSUFFICIENT_BUFFER (ошибка недостаточного размера буфера) из WINERROR.H. Увидев текст "The data area passed to a system call is too small" (область данных, переданная системному вызову, слишком мала), вы будете точно знать, что это за проблема и как ее следует решать.
    Программа SUPERASSERT
    Рис. 3.1. Пример панели сообщений программы SUPERASSERT
    Кроме того, если вы посмотрите на строку Last Error на рис. 3.1, то увидите, что это не стандартное Windows-сообщение об ошибке. Если вы устанавливаете собственные значения последней ошибки (что я и рекомендую делать), то для трансляции таких сообщений можно добавить в программу SUPERASSERT собственный модуль ресурсов сообщений. Чтобы получить дополнительную информацию об использовании собственных ресурсов сообщений, просмотрите в MSDN тему "Message Compiler" (Компилятор сообщений). Существует и дополнительный стимул для применения таких ресурсов: с их помощью гораздо легче осуществлять интернационализацию приложения.
    Чрезвычайно полезна часть панели, расположенная ниже строки Last Error. Это — трасса стека. Она показывает путь к оператору утверждения. SUPERASSERT старается показывать как можно больше информации в сообщениях утверждений, чтобы не нужно было собирать ту же информацию с помощью отладчика.

    Вот еще одно интересное свойство SUPERASSERT: можно отказаться от открытия панели его сообщений. Поначалу это может показаться контрпродуктивным, но я ручаюсь, что это не так! Если вы следовали рекомендациям главы 2 и начинали тестирование отладочных конструкций с помощью инструмента регрессивного тестирования (regression-testing tool), то знаете, что управление такими панелями (со случайными сообщениями утверждений) почти невозможно. Из-за подобных проблем инженерам, тестирующим ПО, не очень нравится возиться с отладочными конструкциями. Работая же с программой SUPERASSERT, можно указать, чтобы вывод направлялся в функцию OutputDebugsString, дескриптор файла или и туда, и туда. Такая гибкость позволяет управлять кодом, получать всю обширную информацию утверждений, и иметь возможность автоматизировать отладочные построения. Кроме того, такое утверждение будет работать и в тех случаях, когда приложение не содержит интерфейса пользователя.

    Пользоваться программой SUPERASSERT довольно легко. При работе в среде С и C++ для этого нужно только включить файл заголовка BUGSLAYERUTIL.H и установить связь с библиотекой BUGSLAYERUTIL.LIB. В листинге 3-3 показан файл DIAGASSERT.H, который содержит все макросы и функции и автоматически включается в заголовочный файл BUGSLAYERUTIL.H.

    Листинг 3-3. DIAGASSERT.H (включенный в BUGSLAYERUTIL.H)

    / - - - - - - - - - - - - - - - - - - - - - -

    "Debugging Applications" (Microsoft Press)

    Copyright (с) 1999-2000 John Robbins — All Rights Reserved.

    /- - - - - - - - - - - - - - - - - - - -

    #fndef _DIAGASSERT_H

    #define _DIAGASSERT_H

    #ifdef _cplusplus

    extern "C" {

    #endif //_cplusplus

    #include

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

    Директивы препроцессора #define

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

    // Основной материал должен быть доступен как для выпускных, так и для

    // отладочных построений. // Использовать глобальные флажки утверждений


    #define DA_USEDEFAULTS OxOOOO

    // Включает показ утверждений в панели сообщений (по умолчанию).

    #define DA_SHOWMSGBOX OxOOOl

    // Включает показ утверждений как через OutputDebugString (по умолчанию).

    // the default.

    #define DA_SHOWODS 0x0002

    // Показывает трассу стека в утверждении. Выключен по умолчанию в

    // макросе ASSERT и включен в макросе SUPERASSERT.

    ifdefine DA_SHOWSTACKTRACE 0x0004

    /*- - - - - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : SetDiagAssertOptions

    ОПИСАНИЕ :

    Устанавливает глобальные режимы для нормального макроса ASSERT.

    ПАРАМЕТРЫ :

    dwOpts — флажок новых режимов

    ВОЗВРАЩАЕТ :

    Предыдущие режимы

    - - - - - - - - - - - - - - - - - - - - - */

    DWORD BUGSUTIL_DLLINTERFACE _stdcall

    SetDiagAssertOptions ( DWORD dwOpts);

    /*- - - - - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : SetDiagAssertFile

    ОПИСАНИЕ :

    Устанавливает дескриптор файла, в который будут записываться данные любого утверждения. Чтобы отключить регистрацию, вызывайте эту функцию с параметром INVALID_HANDLE_VALUE. Набор режимов из SetDiagAssertOptions еще применим; эта функция позволяет регистрировать assertion-информацию в файле.

    В дескрипторе файла не делается никаких проверок ошибок

    или записей в него.

    ПАРАМЕТРЫ :

    hFile — дескриптор файла

    ВОЗВРАЩАЕТ :

    Дескриптор предыдущего файла

    - - - - - - - - - - - - - - - - - - - - -* /

    HANDLE BUGSUTILJ3LLINTERFACE _stdcall

    SetDiagAssertFile ( HANDLE hFile);

    /* - - - - - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : AddDiagAssertModule

    DISCUSSION :

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

    выбираться строки ошибок

    ОПИСАНИЕ :

    hMod — добавляемый модуль

    ВОЗВРАЩАЕТ :

    TRUE - модуль был добавлен.

    FALSE - внутренняя таблица заполнена.

    - - - - - - - - - - - - - - - - - - - - -*/

    BOOL BUGSUTIL_DLLINTERFACE _stdcall

    AddDiagAssertModule ( HMODULE hMod);

    /*- - - - - - - - - - - - - - - - - - - -


    ФУНКЦИЯ : DiagAssert

    ОПИСАНИЕ :

    Функция утверждения для программ на С и C++

    ПАРАМЕТРЫ :

    dwOverrideOpts — DA_* режимы для переопределения глобальных умолчаний для этого вызова в DiagAssert

    szMsg — сообщение для показа в панели сообщений

    szFile — файл, который показывается в утверждении

    dwLine — номер строки, в которой имеется утверждение

    ВОЗВРАЩАЕТ :

    FALSE — игнорировать утверждение.

    TRUE — запустить DebugBreak.

    - - - - - - - - - - - - - - - - - - - - - -* /

    BOOL BUGSUTIL_DLLINTERFACE _stdcall

    DiagAssertA ( DWORD dwOverrideOpts ,

    LPCSTR szMsg ,

    LPCSTR szFile DWORD dwLine );

    BOOL BUGSUTILJDLLINTERFACE _stdcall

    DiagAssertW ( DWORD dwOverrideOpts ,

    LPCWSTR szMsg ,

    LPCSTR szFile

    DWORD dwLine ) ;

    #ifdef UNICODE

    #define DiagAssert DiagAssertW ttelse

    #define DiagAssert DiagAssertA

    #endif

    /*- - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : DiagAssertVB

    ОПИСАНИЕ :

    Функция утверждения для Visual Basic-программ.

    ПАРАМЕТРЫ

    dwOverrideOpts — DA_* режимы для переопределения глобальных умолчаний

    для этого вызова в DiagAssert

    bAllowHalts — Если TRUE, то не показывает кнопки Retry и Ignore

    szMsg — Выводимое сообщение. За форматирование строки

    ответственна сторона Visual Basic

    ВОЗВРАЩАЕТ :

    FALSE — игнорировать утверждение.

    TRUE - запустит DebugBreak.

    - - - - - - - - - - - - - - - - - - - */

    BOOL BUGSUTILJDLLINTERFACE _stdcall

    DiagAssertVB ( DWORD dwOverrideOpts,

    BOOL bAllowHalts,

    LPCSTR szMsg);

    /*- - - - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : SetDiagOutputFile

    ОПИСАНИЕ :

    Устанавливает дескриптор файла, куда будут (по желанию) записаны любые trace-операторы. Чтобы выключить регистрацию, вызовите эту функцию с параметром INVALID_HANDLE_VALUE.

    Не делается никаких проверок ошибок для дескриптора файла или каких-либо записей в него.

    ПАРАМЕТРЫ :

    hFile — дескриптор файла

    ВОЗВРАЩАЕТ :

    Дескриптор предыдущего файла


    - - - - - - - - - - - - - - - - - */

    HANDLE BUGSUTIL_DLLINTERFACE _stdcall

    SetDiagOutputFile ( HANDLE hFile);

    /*- - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : DiagOutput

    ОПИСАНИЕ :

    Обеспечивает подпрограмму трассировки для посылки строк через

    OutputDebugString

    ПАРАМЕТРЫ :

    szFmt — форматная строка

    ... — параметры, которые будут расширены в szFmt

    ВОЗВРАЩАЕТ :

    Нет.

    - - - - - - - - - - - - - - - - - - */

    void BUGSUTIL_DLLINTERFACE

    DiagOutputA ( LPCSTR szFtat, ...);

    void BUGSUTIL_DLLINTERFACE

    DiagOutputW ( LPCWSTR szFmt, ...);

    #ifdef UNICODE

    #define DiagOutput DiagOutputW

    #else

    idefine DiagOutput DiagOutputA

    #endif

    /*- - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : DiagOutputVB

    ОПИСАНИЕ :

    Обеспечивает подпрограмму трассировки для посылки строк через

    OutputDebugString для Visual Basic-программ

    ПАРАМЕТРЫ :

    szMsg — строка сообщения

    ВОЗВРАЩАЕТ :

    нет.

    - - - - - - - - - - - - - - - - - - - - - -*/

    void BUGSUTIL_DLLINTERFACE _stdcall

    DiagOutputVB ( LPCSTR szMsg);

    /*/////////////////////////////////////

    Директивы #undef

    ////////////////////////////////////////*/

    #ifdef ASSERT

    #undef ASSERT

    #endif

    #ifdef assert

    #undef assert

    #endif

    #ifdef VERIFY

    #undef VERIFY

    #endif

    #ifdef TRACE

    3undef TRACE

    #endif

    #ifdef TRACED

    #undef TRACED

    #endif

    #ifdef TRACE1

    #undef TRACE1

    #endif

    #ifdef TRACE2

    #undef TRACE2

    #endif

    #ifdef TRACE3

    #undef TRACE3

    #endif

    /*////////////////////////////////////

    _DEBUG определен

    ///////////////////////////////////////*/

    #ifdef _DEBUG

    /*//////////////////////////////////////////////

    Директивы #define

    /////////////////////////////////////////////*/

    // Различные глобальные режимы, которые могут быть установлены

    // в SetDiagAssertOptions. Если любой из этих режимов пересылается

    //в DiagAssert в первом параметре, то это значение будет переопределять


    // глобальные установки.

    // Макрос assert используется ASSERT и SUPERASSERT.

    // Выключить "conditional expression is constant" ("условное выражение

    // является константой") из-за того, что while(0).

    // Нужно сделать это выключение глобально, потому что при расширении

    // макроса происходит ошибка компиляции.

    #pragma warning ( disable : 4127)

    #ifdef PORTABLE_BUGSLAYERUTIL

    #define ASSERTMACRO(a,x) \

    do \

    { \

    if ( !(x) &&\

    DiagAssert ( a, _T ( #x), _FILE_, _LINE_) ) \

    { \

    DebugBreak () ; \

    } \


    } while (0)

    #else //!PORTABLE_BUGSLAYERUTIL

    #define ASSERTMACRO(a,x) \

    do \

    { \

    if ( !(x) &&\

    DiagAssert ( a, _T ( Ix), _FILE_, _LINE_) . ) \

    { \

    _asm int 3 \

    } \

    } while (0)

    #endif // PORTABLE_BUGSLAYERUTIL

    // Нормальное утверждение. Оно использует умолчания модуля.

    #define ASSERT(x) ASSERTMACRO(DA_OSEDEFAULTS, x)

    // Выполнить assert.

    #define assert ASSERT // Доверяй, но проверяй!


    #define VERIFY(x) ASSERT(x)

    // Полный assert со всеми украшениями

    #define SUPERASSERT(x) ASSERTMACRO ( DA_SHOWSTACKTRACE | \

    DA_SHOWMSGBOX | \

    DA_SHOWODS , \

    x ,)

    // Макрос режимов

    #define SETDIAGASSERTOPTIONS(x) SetDiagAssertOptions(x)

    // Добавить макрос модуля

    #define ADDDIAGASSERTMODULE(x) AddDiagAssertModule(x)

    // Макрос трассировки TRACE

    #ifdef _cplusplus

    #define TRACE ::DiagOutput

    #endif

    #define TRACED(sz) DiagOutput(_T("Is"), _T(sz))

    #define TRACEl(sz, pi) DiagOutput(_T(sz), pi)

    #define TRACE2(sz, pi, p2) DiagOutput(_T(sz), pi, p2)

    #define TRACE3(sz, pi, p2, p3) DiagOutput(_T(sz), pi, p2, p3)

    #else // !_DEBUG

    /*/////////////////////////////////////////

    _DEBUG !!HE!! определен

    //////////////////////////////////////////*/

    #define ASSERTMACRO(a,x)

    #define ASSERT(x)

    #define VERIFY(x) ((void)(x))

    #define SUPERASSERT(x)

    #define SETDIAGASSERTOPTIONS(x)

    #define ADDDIAGASSERTMODULE(x)

    #ifdef _cplusplus

    //inline void TraceOutput(LPCTSTR, ...) { }

    #define TRACE (void)0

    #endif

    #define TRACED(fmt)

    #define TRACE1(fmt,argl)

    #define TRACE2(fmt,argl,arg2)

    #define TRACE3(fmt,argl,arg2,arg3)

    #endif // _DEBUG

    #ifdef _cplusplus

    }

    #endif //_cplusplus

    #endif // _DIAGASSERT_H

    С помощью программы SUPERASSERT можно автоматически переадресовывать все вызовы ASSERT и assert к своим функциям. Макросы _ASSERT и __ASSERTE не переадресовываются, чтобы не мешать другим работать с отладочной библиотекой времени выполнения. Не затрагиваются также макросы Visual Basic ASSERT_KINDOF и ASSERT_VALID. Для программ на Visual Basic нужно только включать в проект файл VBASSERTANDTRACE.BAS.

    Используя макрос или функцию SUPERASSERT, вы автоматически получите трассу стека. Для макроса ASSERT трассировка стека по умолчанию выключена, т.


    к. нецелесообразно вносить издержки, связанные с трассировкой стека, в общие утверждения. Однако, при желании использовать трассировку стека, можно легко включить ее, установив соответствующие опции с помощью макроса SETDIAGASSERTOPTIONS или функции setoiagAssertcptions и передав в ASSERT битовый флажок DA_SHOWSTACKTRACE. SUPERASSERT лучше применять там, где не ожидается серьезных проблем. Вряд ли он будет хорошо работать, скажем, в блоке исключения. В нормальных же ситуациях прекрасно работает и макрос ASSERT.

    Общий вопрос отладки

    Почему константы всегда помещаются в левой части условных операций?

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

    if ( INVALID_HANDLE_VALUE == hFile)

    вместо

    if ( hFile == INVALID_ HANDLE_VALUE)

    Этот стиль используется для того, чтобы избежать ошибок. В первой версии можно легко пропустить один из знаков равенства, что приведет к ошибке во время компиляции. Вторая же версия может и не выдавать предупреждения (что зависит от его уровня), но будет изменять значение переменной. Как в C/C++, так и в Visual Basic, при попытке назначать значение константе будет выдаваться ошибка компилятора. Если когда-либо вам приходилось прослеживать ошибку, включающую случайное назначение, то вы знаете, насколько труден поиск ошибок такого типа.

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

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

    Трассировка

    Утверждения, возможно, наилучший прием профилактического программирования, но операторы трассировки, если их правильно использовать совместно с утверждениями, на самом деле позволяют отлаживать приложения без отладчика. Подобно утверждениям, макросы TRACE (для С и С ++) и Debug.Print (для Visual Basic) применяются и в выпускных компиляциях, причем их можно использовать в исходном тексте столько раз, сколько нужно. Некоторые программисты воспринимают операторы трассировки как средства отладки priritf-стиля. Не стоит недооценивать мощь этой методики, потому что большинство приложений было отлажено прежде, чем были изобретены диалоговые отладчики.
    Определение возможного объема трассировки всегда было проблемой, особенно если речь идет о работе в команде. Хотя каждый разработчик трассирует понемногу, общий объем таких операторов может быстро стать огромным. Операторы трассировки, как минимум, должны ассоциироваться со всеми ключевыми структурами данных и ветвями программы. Они настолько полезны, что можно размещать в приложениях любое количество подобных операторов. В главе 14 описана программа LIMODS, позволяющая ограничить применение операторов трассировки только теми исходными файлами, за которыми интересно наблюдать.
    Операторы трассировки могут разрешить почти все проблемы, но имеют два недостатка.
    Первый состоит в том, что при вызове операторы трассировки обычно преобразуют выполнение приложения в последовательную форму. Это означает, что, когда вы используете такие операторы, быстродействующее многопоточное приложение может выполняться совершенно иным способом, потому что потоки блокируются и планируются вокруг операторов трассировки. Если вы правильно разрабатывали многопоточный код (например так, как описывается в главе 12), то никаких проблем возникать не должно. Однако известны случаи, когда код, который работает внутри отладчика или с большим количеством операторов трассировки, не выполняется вне отладчика или в выпускном режиме.
    Второе ограничение состоит в том, что из-за проблемы сериализации (преобразования в последовательную форму) излишне большое количество операторов трассировки очень сильно замедлить выполнение вашей отладочной конструкции.
    Если имеет место такая ситуация, значит вы слишком увлекаетесь трассировкой, и необходимо ограничить количество соответствующих операций. Поэтому старайтесь не помещать операторы трассировки внутри затяжных циклов.

    Планируя стратегию трассировки, вся команда тратит некоторое время на обдумывание методики форматирования соответствующих операторов. Если все члены команды используют аналогичный формат, то поиск информации с помощью §гер1-утилит или написания простых синтаксических анализаторов для регистрационных журналов — довольно простая задача. Я предпочитаю использовать формат "функция: оператор трассировки". Начиная формат с имени функции, можно легко отыскивать только те функции, которые нужно видеть. Будьте осторожны, чтобы не слишком усложнять этот формат. Если он будет слишком сложным, то разработчики не смогут его запомнить и поэтому не будут использовать.

    Обычно операторы трассировки видны только в окне Output отладчика. Однако свободно распространяемая на www.sysinternals.com утилита DebugView/Enterprise Edition Марка Руссиновича (Mark Russinovich) позволяет видеть операторы трассировки даже тогда, когда приложение выполняется вне отладчика. Я всегда запускаю эту программу. Просматривая операторы трассировки, можно видеть, что происходит в приложениях. Утилита DebugView/Enterprise Edition особенно полезна при работе с мультипроцессными СОМ-приложениями, потому что можно видеть все межпроцессные взаимодействия в одном месте. Другая область, в которой эта утилита оказывает неоценимую помощь, — это СОМ-приложения, выполняющиеся в контекстах, которыми разработчик не может управлять — такие, например, как Microsoft Internet Information Services (US).

    Речь идет о программах поиска строк в текстовых файлах. Название происходит от соответствующей команды UNIX (grep). В UNIX используется также поисковая команда find, которая ищет файлы в каталогах. — Пер.

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

    Автоматический запуск приложений в отладчике

    Самыми трудными для отладки типами приложений являются те приложения, которые запускаются другим процессом. В эту категорию нападают службы Windows 2000 и внепроцессные (out-of-process) СОМ-серверы (СОМ out-of-process servers). Чтобы заставить отладчик прикрепиться к процессу, во многих случаях можно использовать API-функцию DebugBreak. В двух случаях, однако, эта функция работать не будет. Во-первых, она иногда не работает со службами Windows 2000. Если нужно отладить запуск службы, то вызов DebugBreak присоединит отладчик, но к тому времени, когда отладчик запустится, может быть исчерпан интервал тайм-аута службы, и Windows 2000 остановит ее. Во-вторых, DebugBreak не будет работать, когда нужно отлаживать внепроцессный СОМ-сервер. Если вы вызовете DebugBreak, обработчик СОМ-ошибок отловит исключение точки прерывания и завершит СОМ-сервер. К счастью, Windows 2000 позволяет указать, что приложение должно стартовать в отладчике. Это свойство позволяет начать отладку прямо с первой инструкции. Однако, прежде чем вы включите это свойство для службы Windows 2000, удостоверьтесь, что в этой службе сконфигурирована возможность взаимодействия с рабочим столом Windows 2000.
    Свойство запуска с отладчиком можно включить двумя способами. Самый легкий — запустить утилиту GFLAGS.EXE и выбрать переключатель Image File Options (см. рис. 4.1). После ввода в редактируемое поле Image File Name имени двоичного файла программы установите флажок Debugger в группе Image Debugger Options) и введите в редактируемое поле рядом с этим флажком полный путь к отладчику.
    Более трудный способ: нужно вручную установить необходимые параметры з подходящие разделы реестра (с помощью редактора RegEdit). В разделе
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NTXCurrent Version\Image Tile Execution Options
    создайте подраздел, имя которого совпадает с именем файла вашего приложения. Например, если имя приложения — FOO.EXE, то имя подраздела реестра тоже FOO.EXE. В этом подразделе создайте новый строковый параметр с именем Debugger. В диалоговом окне Изменение строкового параметра введите с клавиатуры полное (вместе с путем к каталогу) имя файла выбранного вами отладчика. Если вы указали GFLAGS.EXE и установили некоторые глобальные опции, то сможете заметить в ключе вашего приложения строчное значение GiobaiFiag.
    Теперь при запуске вашего приложения автоматически загружается и запускается и отладчик. Опции командной строки для отладчика также можно определить в строковом параметре Debugger (вслед за именем программы отладчика). Например, для того чтобы использовать отладчик WinDBG и автоматически инициировать отладку, как только стартует WinDBG, нужно в диалоговом окне изменения строкового параметра Debugger ввести значение d:\platform sdk\bin\windbg.exe -g.


    Быстрые клавиши прерываний

    Иногда нужно быстро войти в отладчик. Если вы отлаживаете консольные приложения, то нажатие клавиш + или + вызовет специальное исключение (с именем DBG_CONTROL_C) . Это исключение переведет вас прямо в отладчик и позволит стартовать отладку.
    Полезным свойством как Windows 2000, так и Windows NT 4 является возможность в любое время переходить в отладчик также и в GUI-приложениях. При выполнении под отладчиком нажатие клавиши приводит (по умолчанию) к вызову функции DebugBreak. Интересный аспект обработки этой клавиши заключается в том, что, даже если вы используете ее как акселератор или как-то иначе обрабатываете сообщения клавиатуры для этой клавиши, она все еще будет подключать вас к отладчику.
    В Windows NT 4 быстрая клавиша прерывания назначена по умолчанию, однако в Windows 2000 вы можете сами определить, какую клавишу следует использовать для этих целей. Для чего в разделе реестра
    HKEY_LOCAL_MACHINE\SOFTWARE\ Microsoft\WindowsNT\CurrentVersion\AeDebug
    установите для параметра userDebuggerHotKey значение кода клавиши (VK_*). Например, чтобы использовать клавишу для подключения к отладчику, следует установить значение UserDebuggerHotKey равным 0x91. Изменения вступают в силу после перезагрузки компьютера.



    Чтение и запись памяти

    Чтение из памяти подчиненного отладчика довольно простая операция. Выполняет ее функция ReadProcessMemory. Когда основной отладчик запускает подчиненный, то он имеет полный доступ к подчиненному, потому что дескриптор процесса, возвращенный событием отладки CREATE_PROCESS_DEBUG_EVENT, содержит спецификаторы доступа PROCESS_VM_READ и PROCESS_VM_WRITE. Если ваш отладчик прикрепляется к процессу с помощью функции DebugActiveProcess, то, чтобы получить дескриптор подчиненного отладчика, нужно вызвать функцию openProcess и указать при этом доступ как для чтения, так и для записи.
    Прежде чем рассказывать о записи в память подчиненного отладчика, нужно кратко объяснить важную концепцию "копирование-при-записи" (сору-on-write) Когда Windows загружает выполняемый файл, она разделяет между различными использующими его процессами так много отображаемых страниц памяти этого двоичного файла, насколько это возможно. Если один из этих процессов выполняется под отладчиком, и одна из этих страниц имеет записанную в нее точку прерывания, то очевидно, что эта точка прерывания не может быть представлена во всех процессах, разделяющих эту страницу. Как только какой-нибудь процесс, работающий вне отладчика, выполнит этот код, он завершится аварийно с исключением EXCEPTION_BREAKPOINT. Чтобы не допустить такой ситуации, операционная система, видя, что страница изменена для конкретного процесса, делает копию этой страницы, которая является частной (private) для процесса, записавшего в нее точку прерывания. Таким образом, как только процесс пишет в страницу памяти, операционная система ее копирует.
    Запись в память подчиненного отладчика почти столь же проста, как и чтение из нее. Однако, поскольку страницы памяти, в которые будет произведена запись, могут быть помечены атрибутом "только-для-чтения", то, чтобы получить текущие характеристики защиты страницы, нужно сначала вызвать функцию virtualQueryEx. Имея эти характеристики, можно использовать API-функцию virtualProtectEx, чтобы установить для страницы параметр защиты PAGE_EXECUTE_READWRITE, что позволит вам писать в нее, а Windows сможет подготовиться к выполнению операций "копирование-при-записи".
    По завершении записи в память нужно установить защиту страницы в исходное состояние. Если этого не сделать, подчиненный отладчик может случайно записать страницу (и преуспеть в этом, тогда как должен был потерпеть неудачу). Если бы, например, первоначальным параметром защиты страницы было "только-чтение", то случайная запись подчиненного отладчика привела бы к нарушению доступа. Без восстановления защиты страницы (от записи) случайная запись не сгенерирует соответствующего исключения, и вы будете иметь случай, когда выполнение под отладчиком отличается от выполнения вне его.

    Интересная деталь отладочного API Win32: когда появляется событие OUTPUT_DEBUG_STRING_EVENT, то ответственным за получение строки для вывода является основной отладчик. Информация, переданная отладчику, включает расположение и длину строки. Получив это сообщение, отладчик читает память вне подчиненного отладчика. В главе 3 упоминалось, что при выполнении под отладчиком операторы трассировки могут легко изменить поведение приложения. Поскольку все потоки в приложении останавливаются, когда цикл отладки обрабатывает событие, вызов функции OutputDebugString в подчиненном отладчике означает, что все потоки стоят. Листинг 4-3 показывает, как WDBG обрабатывает событие OUTPUT_DEBUG_STRING_EVENT. Заметьте, что функция DBG_ReadProcessMemory является функцией-оболочкой вокруг ReadProcessMemory из LOCALASSIST.DLL.

    Листинг 4-3.OutputDebugStringEvent изPROCESSDEBUGEVENTS.CPP

    static

    DWORD OutputDebugStringEvent ( CDebugBaseUser * pUserClass ,

    LPDEBUGGEEINFO pData ,

    DWORD dwProcessId,

    DWORD dwThreadld ,

    OUTPUT_DEBUG_STRING_INFO & stODSI )

    {

    TCHAR szBuff[ 512 ];

    HANDLE hProc = pData->GetProcessHandle (); DWORD dwRead;

    // Читать память.

    BOOL bRet = DBG_ReadProcessMemory( hProc ,

    stODSI.lpDebugStringData ,

    szBuff, min ( sizeof ( szBuff) ,

    stODSI.nDebugStringLength),

    &dwRead ) ;

    ASSERT ( TRUE == bRet);

    if ( TRUE == bRet)

    {

    // Строку всегда завершает NULL .

    szBuff [ dwRead + 1 ] = _T ( '\0');

    // Преобразовать символы CR/LF .

    pUserClass->ConvertCRLF ( szBuff, sizeof ( szBuff));

    // Послать ковертированную строку в класс пользователя.

    pUserClass->OutputDebugStringEvent ( dwProcessId,

    dwThreadld , szBuff );

    }

    return ( DBG_CONTINUE);

    }

    Интересная проблема разработки WDBG

    Вообще у меня было мало неприятностей при разработке WDBG. Однако настало время обсудить одну, на мой взгляд, довольно интересную проблему. При работе с отладчиком Visual C++ окно Output показывает полные пути к загруженным программным модулям. Поскольку требовалось снабдить WDBG максимальным набором функциональных возможностей, в нем продублирована эта функция отладчика Visual C++. Но сделать это оказалось непросто.
    Приведенное ниже определение структуры LOAD_DLL_DEBUG_INFO (она передается в отладчик при получении уведомлений LOAD_DLL_DEBUG_EVENT) содержит поле ipimageName, которое, по всей вероятности, должно хранить имя загружаемого модуля. Это так и есть, но ни одна из операционных систем Win32 никогда правильно не заполняет это поле при его считывании в программу.
    typedef struct _LOAD_DLL_DEBUG_INFO
    {
    HANDLE hFile;
    LPVOID IpBaseOfDll;
    DWORD dwDebuglnfoFileOffset;
    DWORD nDebuglnfoSize;
    LPVOID IpimageName;
    WORD fUnicode;
    } LOAD_DLL_DEBUG_INFO;
    Поскольку при получении уведомления LOAD_DLL_DEBUG_EVENT, образно говоря, модуль загружается в символьную машину DBGHELP.DLL, то мне казалось, что после загрузки модуля (в память) легко можно отыскать его полное имя. API-функция SymGetModuieinfo получает (через соответствующий параметр) показанную ниже структуру IMAGEHLP_MODULE, где имеется место для полного имени модуля (см. поле ModuleName[32]).
    На самом деле все, по-видимому, наоборот: это символьная машина загружает символьную информацию модуля в соответствующий символьный файл (в данном случае — в DBG-файл). — Пер
    typedef struct _IMAGEHLP_MODULE {
    DWORD SizeOfStruct;
    DWORD BaseOfImage;
    DWORD ImageSize;
    DWORD TimeDateStamp;
    DWORD Checksum;
    DWORD NumSyms;
    SYM_TYPE SymType;
    CHAR ModuleName[32];
    CHAR ImageName[256] ;
    CHAR LoadedlmageName[256];
    } IMAGEHLP_MODULE, *PIMAGEHLP_MODULE;
    Странная вещь: когда функция SymGetModuieinfo возвращает символьную информацию модуля, то вместо имени модуля либо возвращается имя символьного DBG-файла, либо ничего не возвращается (т.
    е. имя модуля в возвращаемой информации полностью пропускается). Такое поведение может показаться удивительным, но только на первый взгляд. Когда была получена • структура LOAD_DLL_DEBUG_INFO, ее первый член (типа hFile) был правильным, и тогда была вызвана функция SymLoadModuie с дескриптором того же типа (hFile). Поскольку я никогда не загружал в символьную машину DBGHELP.DLL полное имя файла, она просто заглядывала в открытый файл, обозначенный дескриптором hFile, находила в нем отладочную информацию и считывала ее. У символьной машины никогда не было необходимости знать полное имя файла.

    Получить же требовалось полное имя загруженного модуля. Сначала я думал, что мог бы использовать сам дескриптор файла, чтобы получить доступ к экспортной секции модуля и сообщить найденное там имя модуля. Кроме того, модуль мог быть переименован, и его имя в экспортной секции было бы неправильным. Это мог быть ЕХЕ- или DLL-модуль, не содержащий списка экспортируемых модулей. И даже если бы как-то удалось найти правильное имя модуля, его полное имя (путь) было бы недоступно.

    Затем я предположил, что где-то должна быть API-функция, которая будет получать (через аргумент вызова) значение дескриптора файла и возвращать полное имя открытого файла. Обнаружив, что в библиотеках операционной системы такой функции нет, я проверил несколько недокументированных значений, которые работали, но не полностью. Тогда я начал поиск с помощью функций из набора Tool Help и файла PSAPI.DLL, потому что оба этих средства сообщают информацию о модулях, загруженных в процесс. Функции Tool Help в Windows 98 работали нормально, в Windows NT 4 происходил сбой функций PSAPI.DLL, а в Windows 2000 функции Tool Help тяжело подвешивали отладчик. Сами функции Tool Help не были испорчены, но они пробуют стартовать новый поток в адресном пространстве подчиненного отладчика с помощью вызова CreateRemoteThread. Поскольку подчиненный отладчик был полностью остановлен в WDBG, функции Tool Help будут висеть, пока подчиненный отладчик повторно не стартует.


    После переключения на PSAPI.DLL в Windows 2000 вместо зависания происходил сбой функций Tool Help, как это было в Windows NT 4.

    Используя подход к решению проблем, который был намечен еще в главе 1, я приступил к формулировке некоторой гипотезы, пытающейся объяснить проблему. Внимательное изучение функции GetModuleFilenameEx из PSAPI.DLL помогло понять, почему она не работала, когда я ее вызывал. Уведомление LOAD_DLL_DEBUG_EVENT сообщало мне, что DLL только собиралась загружаться в адресное пространство, а не то что DLL уже загружена. Поскольку память не была Отображена для хранения DLL, функция GetModuleFilenameEx из PSAPI.DLL терпела неудачу. Когда я выполнял пошаговый проход памяти этой функции на уровне языка ассемблера, то казалось, что она выглядит как список отображенной памяти, который операционная система поддерживает для каждого процесса.

    Локализовав источник проблемы, нужно было только выяснить, когда операционная система полностью отображала модуль в памяти. Вероятно, можно было предпринять чрезвычайные меры, чтобы получить эту информацию, например, выполнив обратную разработку загрузчика образа в NTDLL.DLL и установив там точку прерывания. К счастью, нашлось немного более простое решение, которое не вызывало остановов на каждом релизе пакета обслуживания операционной системы. Оказалось, что загрузочную информацию модуля нужно просто поставить в очередь и время от времени проверять ее. pulseModuieNotification — это авторская функция, которая управляет деталями проверки загрузочной информации модуля; ее реализацию (исходный код) можно найти в файле MODULENOTIFICATION.CPP на сопровождающем компакт-диске. Если вы просмотрите исходный код функции DebugThread в DEBUGTHREAD.CPP на сопровождающем компакт-диске, то увидите, что функция PulseModuieNotification вызывается на каждом шаге цикла отладки, и каждый раз, когда завершается интервал ожидания (тайм-аут) функции WaitForDebugEvent.

    Общий вопрос отладки

    Почему я не могу входить в системные функции или устанавливать точки прерывания в системной памяти Windows 98?


    Если вы когда- нибудь пробовали во время отладки входить внутрь (step into) некоторых системных функций операционной системы Windows 98, то могли убедиться, что отладчик не позволяет это делать. С другой стороны, Windows 2000 позволяет входить в любую точку процесса пользовательского режима. Дело в том, что Windows 2000 реализует "копирование-при-записи" во всей памяти, тогда как Windows 98 делает это только для адресов ниже 2 Гбайт.

    Напомним, что "копирование-при-записи" позволяет процессам иметь свои собственные частные копии страниц отображенной памяти, когда они (процессы) или отладчик пишут на странице. В Windows 98 все процессы разделяют адресное пространство над 2 Гбайт. Поскольку Windows 98 не реализует "копирование-при-записи" для этих адресов, то если бы Windows 98 разрешала вам установить точку прерывания в разделяемой памяти, первый же процесс, который выполнил бы этот адрес, вызвал бы исключение точки прерывания. Поскольку этот процесс, вероятно, не выполняется под отладчиком, то он закончился бы с исключением точки прерывания. Хотя некоторые системные DLL, такие как сомсть32. DLL, загружаются ниже 2 Гбайт, главные системные DLL, такие как KERNEL32.DLL и USER32.DLL, загружаются выше 2 Гбайт. Это означает, что если у вас нет корневого отладчика, выполняющегося в Windows 98, то вы не можете входить в них с отладчиком пользовательского режима.

    Итак, вы хотите написать собственный отладчик?

    Просто удивительно, сколько программистов пытаются писать собственные отладчики. Понимаю, почему они хотят это делать, т. к. как сам прожил жизнь автора отладчиков. Мы заинтересовались компьютерами и программным обеспечением в первую очередь потому, что хотели знать, как они работают, а отладчики — это волшебное зеркало, которое позволяет видеть все, что с ними связано. Написание WDBG преследовало цель получить, наконец, полный пример отладчика, доступный не только для разработчиков и позволяющий видеть, как отладчики работают.
    Первый шаг, который нужно сделать после рассмотрения WDBG, — получить превосходную книгу (Джонатан Розенберг "Как работают отладчики"). Хотя там не представлен исходный код отладчика, это — замечательное введение и обсуждение реальных проблем, с которыми программист столкнется при написании отладчика.
    Необходимо детально познакомиться с форматом РЕ и конкретным CPU, на котором вы работаете. Сопровождающий компакт-диск содержит PECOFF.DOC, самую последнюю спецификацию РЕ-файлов от Microsoft. Подробнее можно изучать CPU по руководствам Intel CPU, доступным на www.intel.com.
    Прежде чем заняться полным отладчиком, вы должны, вероятно, написать дизассемблер. Написание дизассемблера позволит не только подробнее изучить CPU, но и получить код, который можно будет использовать в отладчике. Дизассемблер в WDBG — это код "только-для-чтения" (read-only). Другими словами, только разработчик, который его написал, может его читать. Стремитесь делать ваш дизассемблер расширяемым и удобным для сопровождения. Я написал в прошлом довольно много программ на языке ассемблера, но действительно изучил язык ассемблера, только создав собственный дизассемблер.
    Написание дизассемблера лучше всего начать со справочных руководств фирмы Intel. Они содержат всю необходимую информацию по командам и кодам операций, а также полную карту кодов операций, которую нужно знать для включения соответствующих чисел в команды. Исходный код нескольких дизассемблеров можно найти в Интернете. Прежде чем приступить собственно к написанию, следует изучить исходные коды нескольких дизассемблеров, чтобы получить основные идеи и посмотреть, как другие специалисты справлялись с этой задачей.
    Как уже говорилось, символьная машина DBGHELP.DLL достаточна для некоторых превосходных вспомогательных отладочных утилит, но не достаточна для реального отладчика. Вы можете всегда заняться обратной разработкой формата PDB-файлов, а мы все можем надеяться, что Microsoft когда-нибудь откроет доступ к PDB-файлам.


    На первый взгляд, отладчик для


    На первый взгляд, отладчик для 32-разрядных ОС Windows (Win32) — это простая программа, к которой предъявляются всего два требования. Во-первых, отладчик должен передать функции createProcess (через параметр dwCreationFlags) специальный флажок DEBUG_ONLY_THIS_PROCESS. Этот флажок сообщает операционной системе, что вызывающий поток вошел в цикл отладки, чтобы управлять процессом, который он запускает. Если отладчик может управлять множеством процессов, порождаемых первоначальным подчиненным процессом, то вместо флажка DEBUG_ONLY_THIS_PROCESS в :reateProcess будет пересылаться флажок DEBUG_PROCESS.

    Таким образом, отладочный API Win32 организует базовый и подчиненный отладчики в отдельных процессах, что делает операционные системы Win32 намного более устойчивыми при отладке. Даже если подчиненный отладчик выполняет неконтролируемые записи (wild memory writes) в память, это не приведет к аварии базового отладчика. (Отладчики в 16-разрядных операционных системах Windows и Macintosh восприимчивы к повреждениям подчиненного отладчика, потому что как базовый, так и подчиненный отладчики выполняются в одном и том же контексте процесса.)

    Второе требование заключается в том, что после запуска подчиненного отладчика базовый должен войти в цикл, вызывающий API-функцию v/aitForDebugEvent, чтобы ждать получения отладочного уведомления. Закончив обрабатывать конкретное событие отладки, он вызывает функцию :ontinueDebugEvent. Знайте, что только поток, который вызывает функцию :reateProcess со специальными флажками создания отладки, может вызывать отладочные API-функции. Следующий псевдокод показывает, как немного нужно, чтобы создать простейший Win32-oxnafl4HK:

    void main ( void)

    CreateProcess ( ..., DEBUG_ONLY_THIS_PROCESS, ...);

    while ( 1 == WaitForDebugEvent ( ...))

    {

    if ( EXIT_PROCESS)

    {

    break;

    }

    ContinueDebugEvent ( ...);

    }

    }

    Заметим, что для создания минимального варианта 32-разрядного Win-отладчика не требуется ни многопоточности, ни интерфейса пользователя, ни чего-либо еще.
    Реальный отладочный API Win32 ориентирован на то, чтобы цикл отладки находился в отдельном потоке.

    Пока базовый отладчик находится в цикле отладки, он получает различные уведомления о том, что в подчиненном отладчике имели место некоторые события. Следующая структура (с именем DEBUG_EVENT), которая заполнена функцией WaitForDebugEvent, содержит всю интересную информацию о событии отладки.

    typedef struct _DEBUG_EVENT {

    DWORD dwDebugEventCode;

    DWORD dwProcessId;

    DWORD dwThreadld;

    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;

    RIP_INFO Riplnfo;

    } u;

    } DEBUG_EVENT

    Ниже приведено полное описание индивидуальных событий отладки.

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

    Структура DEBUG_EVENT содержит структуру CREATE_PROCESS_DEBUG_INFO.

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

    Дескриптор процесса имеет доступ PROCESS_VM_READ и PROCESS_VM_WRITE. Если отладчику открыты эти типы доступа к потоку, он может читать и писать в память процесса, используя функции ReadProcessMemory и WriteProcessMemory.

    Дескриптор файла образа процесса имеет доступ GENERIC_READ и открывается для разделенного чтения (read-sharing).

    Дескриптор потока инициализации процесса имеет доступ к потокам THREAD_GET_CONTEXT, THREAD_SET_CONTEXT и THREAD_SUSPEND_RESUME.


    ЕСЛИ отладчик имеет эти типы доступа к потоку, он может читать (из) и писать В регистры потока, используя функции GetThreadContext и SetThreadContext, а также может приостанавливать и возобновлять поток, используя функции ResumeThread и SuspendThread.

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

    Структура DEBUG_EVENT содержит структуру CREATE_THREAD_DEBUG_INFO. Эта структура включает дескриптор нового потока и стартовый адрес потока. Дескриптор имеет доступ к потоку THREAD_GET_CONTEXT, THKEAD_SET_ CONTEXT и THREAD_SUSPEND_RESUME. Если отладчик имеет эти типы доступа к потоку, он может читать (из) и писать в регистры потока, используя функции GetThreadContext и SetThreadContext, и может приостанавливать и возобновлять поток, используя функции ResumeThread и SuspendThread.

  • EXCEPTION_DEBUG_EVENT
  • Этот событие генерируется всякий раз, когда в отлаживаемом процессе происходит исключение. Возможные исключения включают попытку доступа к недоступной памяти, выполнение инструкций точки прерывания, попытку деления на 0 или любое другое исключение, упомянутое в теме "Structured Exception Handling" (Обработка структурированных исключений) в Platform SDK.

    Структура DEBUG_EVENT содержит структуру EXCEPTION_DEBUG_INFO. Эта структура описывает исключение, которое послужило причиной события отладки.

    Кроме условий стандартных исключений, во время отладки консольного процесса может возникнуть дополнительный код исключения. Ядро генерирует код исключения DBG_CONTROL_C, когда вводом в консольный процесс является результат нажатия клавиш +. Этот процесс обрабатывает сигналы от + и отлаживается. Данный код исключения не предполагается обрабатывать приложениями. Приложение никогда не должно использовать для него обработчик исключения.


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

    Если процесс не отлажен или если отладчику пересылается необработанное исключение DBG_CONTROL_C, то отыскивается список функций обработчика консольного приложения. (Дополнительную информацию о функциях обработчика консольного процесса можно найти в документации MSDN для функции SetConsoleCtrlHandler.)

  • EXIT_PROCESS_DEBUG_EVENT
  • Это событие генерируется всякий раз, когда происходит выход из последнего потока в отлаживаемом процессе. Оно происходит сразу же после того, как ядро разгружает DLL процесса и обновляет код выхода из него.

    Структура DEBUG_EVENT содержит структуру EXIT_PROCESS_DEBUG_INFO, которая специфицирует код выхода.

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

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

    Структура DEBUG_EVENT содержит структуру EXIT_THREAD_DEBUG_INFO, которая специфицирует код выхода.

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

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

  • LOAD_DLL_DEBUG_EVENT
  • Это событие генерируется всякий раз, когда отлаживаемый процесс загружает DLL. Оно происходит, когда системный загрузчик разрешает связи с DLL или когда отлаженный процесс использует функцию LoadLibrary. Это отладочное событие вызывается каждый раз, когда в адресное пространство загружается DLL. Если счетчик ссылок на DLL уменьшается до 0, DLL выгружается.


    При следующей загрузке DLL это событие будет сгенерировано снова.

    Структура DEBUG_EVENTсодержит структуру LOAD_DLL_DEBUG_INFO. Эта структура включает дескриптор недавно загруженной DLL, базовый адрес DLL, и другую информацию, которая описывает DLL.

    Как правило, при получении этого события отладчик загружает таблицу символов, связанную с DLL.

  • OUTPUT_DEBUG_STRING_EVENT
  • Данное событие генерируется, когда отлаживаемый процесс использует функцию OutputDebugString.

    Структура DEBUG_EVENT содержит структуру OUTPUT_DEBUG_STRING_INFO. Эта структура указывает адрес, длину и формат строки отладки.

  • UNLOAD_DLL_DEBUG_EVENT
  • Это событие генерируется всякий раз, когда отлаживаемый процесс разгружает DLL, используя функцию FreeLibrary. Это отладочное событие происходит только тогда, когда DLL последний раз разгружается из адресного пространства процесса (т. е. когда счетчик использования DLL становится равным 0).

    Структура DEBUG_EVENT содержит структуру UNLOAD_DLL_DEBUG_INFO. Эта структура указывает базовый адрес DLL в адресном пространстве процесса, который разгружает DLL.

    Как правило, при получении этого отладочного события отладчик разгружает таблицу символов, связанную с DLL.

    Когда происходит выход из процесса, ядро автоматически разгружает все DLL процесса, но не генерирует отладочное событие UNLOAD_DLL_DEBUG _EVENT.

  • RIP__INFO
  • Это событие генерируется только контролируемой сборкой Windows 98 и используется, чтобы сообщить об условиях ошибок, таких, например, как закрытие неправильных дескрипторов.

    Когда отладчик обрабатывает события отладки, возвращаемые функцией :.TaitForDebugEvent, он имеет полный контроль над подчиненным отладчиком, потому что операционная система останавливает все потоки в этом отладчике, и не будет планировать их до тех пор, пока не будет вызвана функция continueoebugEvent. Если отладчик должен читать или писать в адресном пространстве подчиненного отладчика, он может использовать функции ReadProcessMemory и WriteProcessMemory.


    Если память имеет атрибут "только-для-чтения", то можно вызвать функцию virtuaiProtect, чтобы повторно установить уровни защиты. Если базовый отладчик модифицирует код подчиненного отладчика через обращение к функции WriteProcessMemory, он должен вызвать функцию FlushinstructionCache, чтобы очистить кэш команд памяти. Если не вызвать FlushinstructionCache, то ваши изменения, в принципе, могут работать, но если память, которую вы изменили в настоящий момент, находится в кэше CPU, этого может и не быть. Вызов FlushinstructionCache особенно важен для мультипроцессорных машин. Если базовому отладчику нужно получить или установить текущий контекст подчиненного отладчика или регистры CPU, то он может вызвать функцию GetThreadContext или SetThreadContext.

    Единственным отладочным событием Win32, которое нуждается в специальной обработке, является точка прерывания загрузчика. После того как операционная система посылает начальные уведомления CREATE_PROCESS_DEBUG_VENT и LOAD_DLL_DEBUG_EVENT для неявно загружаемых модулей, бвзовый отладчик получает уведомление EXCEPTION_DEBUG_EVENT. Это отладочное событие является точкой прерывания загрузчика (loader breakpoint). Подчиненный отладчик выполняет ее, потому что уведомление CREATE_PROCESS_DEBUG_EVENT указывает только на то, что процесс был загружен, а не на то, что он был выполнен. Базовый же отладчик в этот момент впервые узнает о том, что подчиненный отладчик действительно выполняется. В реальных (real-world) отладчиках инициализация главных структур данных (например, таблицы символов) выполняется во время процесса создания, и отладчик стартует,показывая код дизассемблера или делая необходимые модификации подчиненного отладчика в точке прерывания загрузчика.

    1 Речь, видимо, идет об отладчиках систем программирования Visual Basic, Visual C++ и пр. — Пер

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


    Дополнительная обработка, необходимая для первой точки прерывания (и для всех точек прерывания вообще), зависит от CPU. Для семейства Intel Pentium отладчик должен продолжать обработку, вызывая функцию ContinueDebugEvent и передавая ей флажок DBG_CONTINUE, чтобы подчиненный отладчик возобновил выполнение.

    В листинге 4-2 показан "минимальный отладчик" MinDBG. Он обрабатывает все события отладки и должным образом выполняет дочерний отладочный процесс. При выполнении MinDBG обратите внимание, что обработчики событий отладки реально не показывают никакой интересной информации, такой, например, как имена исполняемых файлов и DLL. Нужно совсем немного поработать, чтобы превратить этот "минимальный" отладчик в "реальный".

    Листинг 4-2. MINDBG.CPP

    /*- - - - - - - - - - - - - - - - - - - - - - - -

    Программы самого простого в мире отладчика для Win32

    - - - - - - - - - - - - - - - - - - - - - - - - - */

    /*//////////////////////////////////////////////////////////////

    Обычные директивы #include

    //////////////////////////////////////////////////////////////*/

    #include "stdafx.h"

    /*///////////////////////////////////////////////////

    Прототипы

    ////////////////////////////////////////////////////////*/

    // Shows the minimal help.

    void ShowHelp ( void);

    // Display-функции

    void DisplayCreateProcessEvent ( CREATE_PROCESS_DEBUG_INFO & stCPDI);

    void DisplayCreateThreadEvent ( CREATE_THREAD_DEBUG_INFO & stCTDI);

    void DisplayExitThreadEvent ( EXIT_THREAD_DEBUG_INFO & stETDI);

    void DisplayExitProcessEvent ( EXIT_PROCESS_DEBUG_INFO & stEPDI);

    void DisplayDllLoadEvent ( 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);

    /*////////////////////////////////////////////////////////////

    Точка входа!


    /////////////////////////////////////////////////////////////*/

    void main ( int argc, char * argv[ ])

    // Проверка наличия аргумента командной строки.

    if ( 1 == argc)

    {

    ShowHelp ();

    return;

    }

    // Конкатенация параметров командной строки.

    TCHAR szCmdLine[ МАХ_РАТН ];

    szCmdLine[ 0 ] = '\0';

    for ( int i = 1; i < argc; i++)

    { strcat ( szCmdLine, argv[ i ]);

    if ( i < argc)

    {

    strcat ( szCmdLine, " ");

    }

    }

    // Попытка стартовать процесс подчиненного отладчика.

    // Вызов функции выглядит как нормальный вызов CreateProcess,

    //за исключением флажка специального режима

    // запуска DEBUG_ONLY_THIS_PROCESS.

    STARTUPINFO stStartlnfo ;

    PROCESS_INFORMATION stProcessInfo ;

    memset ( sstStartlnfo , NULL, sizeof ( STARTUPINFO ));

    memset ( SstProcessInfo, NULL, sizeof ( PROCESS_INFORMATION));

    stStartlnfo.cb = sizeof ( STARTUPINFO);

    BOOL bRet = CreateProcess ( NULL ,

    szCmdLine ,

    NULL

    NULL ,

    FALSE ,

    CREATE_NEW_CONSOLE |

    DEBUG__ONLY_THIS_PROCESS,

    NULL ,

    NULL ,

    &stStartlnfo ,

    &stProcessInfo ) ;

    // Посмотреть, стартовал ли процесс подчиненного отладчика.

    if ( FALSE == bRet)

    {

    printf ( "Unable to start %s\n", szCmdLine);

    return;

    }

    // Подчиненный отладчик стартовал, войдем в цикл отладки.

    DEBUG_EVENT stDE

    BOOL bSeenlnitialBP = FALSE ;

    BOOL bContinue = TRUE ;

    HANDLE hProcess = INVALID_HANDLE_VALUE;

    DWORD dwContinueStatus

    // Входим в цикл while.

    while ( TRUE == bContinue)

    {

    // Пауза, пока не придет уведомление о событии отладки.

    bContinue = WaitForDebugEvent ( &stDE, INFINITE);

    // Обработать конкретные отладочные события. Из-за того что

    // MinDBG является минимальным отладчиком, он обрабатывает

    // только некоторые события.

    switch ( stDE.dwDebugEventCode)

    {

    case CREATE_PROCESS_DEBUG_EVENT :

    {

    DisplayCreateProcessEvent ( stDE.u.CreateProcessInfo);

    // Сохранить информацию дескриптора, необходимую

    // для дальнейшего использования


    . hProcess = stDE.u.CreateProcessInfo.hProcess;

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case 'EXIT_PROCESS_DEBUG_EVENT :

    {

    DisplayExitProcessEvent ( stDE.u.ExitProcess);

    bContinue = FALSE;

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case LOAD_DLL_DEBUG_EVENT :

    {

    DisplayDllLoadEvent ( stDE.u.LoadDll);

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case UNLOAD_DLL_DEBUG_EVENT :

    {

    DisplayDllUnLoadEvent ( stDE.u.UnloadDll);

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case CREATE_THREAD_DEBUG_EVENT :

    {

    DisplayCreateThreadEvent ( stDE.u.CreateThread);

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case EXIT_THREAD_DEBUG_EVENT :

    {

    DisplayExitThreadEvent ( stDE.u.ExitThread);

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case OUTPUT_DEBUG_STRING_EVENT :

    {

    DisplayODSEvent ( hProcess, stDE.u.DebugString);

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case RIPR_VENT :

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    case EXCEPTION_DEBUG_EVENT :

    {

    DisplayExceptionEvent ( stDE.u.Exception);

    // Единственным исключением, с которым следует

    // обращаться по-особому, является начальная

    // точка прерывания, которую обеспечивает загрузчик.

    switch ( stDE.u.Exception.ExceptionRecord.ExceptionCode)

    {

    case EXCEPTION_BREAKPOINT :

    {

    // Если возникает исключение точки прерывания

    // и оно замечается впервые, то продолжаем;

    // иначе, передаем исключение подчиненному

    // отладчику

    if ( FALSE == bSeenlnitialBP)

    {

    bSeenlnitialBP = TRUE;

    dwContinueStatus = DBG_CONTINUE;

    }

    else {

    // Хьюстон, у нас проблема!

    dwContinueStatus =

    DBG_EXCEPTION_NOT_HANDLED;

    }

    }

    break;

    // Просто передать любые другие исключения

    // подчиненному отладчику,

    default :

    {

    dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;

    }

    break;

    }

    }

    break;

    // Для любых других событий просто продолжить,


    default :

    {

    dwContinueStatus = DBG_CONTINUE;

    }

    break;

    }

    // Перейти к операционной системе.

    ContinueDebugEvent ( stDE.dwProcessId,

    stDE.dwThreadld ,

    dwContinueStatus );

    }

    }

    /*/////////////////////////////////////////////////////////

    /////////////////////////////////////////////////////////*/

    void ShowHelp ( void)

    {

    printf ( "MinDBG "

    "\n");

    }

    void DisplayCreateProcessEvent ( CREATE_PROCESS_DEBUG_INFO & stCPDI)

    {

    printf ( " Create Process Event :\n");

    printf (." hFile : Ox%08X\n",

    stCPDI.hFile );

    printf ( " hProcess : 0x%08X\n",

    stCPDI.hProcess );

    printf ( " hThread : 0x%08X\n",

    stCPDI.hThread);

    printf (" lpBaseOfImage :0x%08X\n",

    stCPDI.lpBaseOfImage);

    printf("dwDebugInfoFileOffset: 0x%08X\n",

    stCPDI.dwDebugInfoFileOffset);

    printf("nDebugInfoSize: 0x%08X\n",

    stCPDI.nDebugInfoSize);

    printf ( " IpThreadLocalBase : Ox%08X\n",

    stCPDI.IpThreadLocalBase );


    printf ( " IpStartAddress : Ox%08X\n",

    stCPDI.IpStartAddress ) ;

    printf ( " IpImageName : Ox%08X\n",

    stCPDI.IpImageName );

    printf ( " fUnicode : Ox%08X\n",

    stCPDI.fUnicode );

    }

    void DisplayCreateThreadEvent ( CREATE_THREAD_DEBUG_INFO & stCTDI)

    {

    printf ( "Create Thread Event :\n");

    printf ( " hThread : Ox%08X\n",

    stCTDI.hThread );

    printf ( " IpThreadLocalBase : Ox%08X\n",

    stCTDI.IpThreadLocalBase );

    printf ( " IpStartAddress : Ox%08X\n",

    stCTDI.IpStartAddress );

    }

    void DisplayExitThreadEvent ( EXIT_THREAD_DEBUG_INFO & stETDI)

    {

    printf ( "Exit Thread Event :\n");

    printf ( " dwExitCode : Ox%08X\n",

    stETDI.dwExitCode );

    }

    void DisplayExitPrpcessEvent ( EXIT_PROCESS_DEBUG_INFO & stEPDI)


    {

    printf ( " Exit Process Event :\n");

    printf ( " dwExitCode ' : Ox%08X\n",

    stEPDI.dwExitCode );

    }

    void DisplayDllLoadEvent ( LOAD_DLL_DEBUG_INFO & stLDDI)

    {

    printf ( "DLL Load Event :\n");

    printf ( " hFile : Ox%08X\n",

    stLDDI.hFile );

    printf ( " IpBaseOfDll : Ox%08X\n",

    stLDDI.IpBaseOfDll );

    printf ( " dwDebuglnfoFileOffset : Ox%08X\n",

    stLDDI.dwDebuglnfoFileOffset );

    printf ( " nDebuglnfoSize : Ox%08X\n",

    stLDDI.nDebuglnfoSize );

    printf ( " IpImageName : Ox%08X\n",

    stLDDI.IpImageName );

    printf ( " fUnicode : Ox%08X\n",

    stLDDI.fUnicode );

    }

    void DisplayDllUnLoadEvent ( UNLOAD_DLL_DEBUG_INFO & stULDDI)

    {

    printf ( "DLL Unload Event :\n");

    printf ( " IpBaseOfDll : Ox%08X\n",

    stULDDI.IpBaseOfDll );

    }

    void DisplayODSEvent { HANDLE hProcess,

    OUTPUT_DEBUG STRING INFO & stODSI )

    {

    printf ( "OutputDebugString Event :\n");

    printf ( " IpDebugStringData : Ox%08X\n",

    stODSI.IpDebugStringData );

    printf ( " fUnicode : Ox%08X\n",

    stODSI.fUnicode );

    printf ( " nDebugStringLength : Ox%08X\n",

    stODSI.nDebugStringLength );

    printf ( " String :\n"); char szBuff[ 512 ];

    if ( stODSI.nDebugStringLength > 512)

    {

    return;

    }

    DWORD dwRead;

    BOOL bRet;

    bRet = ReadProcessMemory ( hProcess

    stODSI.IpDebugStringData ,

    szBuff ,

    stODSI.nDebugStringLength ,

    SdwRead );

    printf ( "%s", szBuff);

    }

    void DisplayExceptionEvent ( EXCEPTION_DEBUG INFO & stEDI)

    {

    printf ( "Exception Event :\n");

    printf ( " dwFirstChance : Ox%08X\n",

    stEDI.dwFirstChance );

    printf ( " ExceptionCode : Ox%08X\n",

    stEDI.ExceptionRecord.ExceptionCode );

    printf ( " ExceptionFlags : Ox%08X\n",

    stEDI.ExceptionRecord.ExceptionFlags );

    printf ( " ExceptionRecord : Ox%08X\n",

    stEDI.ExceptionRecord.ExceptionRecord );

    printf ( " ExceptionAddress : Ox%08X\n",

    stEDI.ExceptionRecord.ExceptionAddress );

    printf ( " NumberParameters : Ox%08X\n",

    stEDI.ExceptionRecord.NumberParameters );

    }

    Операции Step Into, Step Over и Step Out

    Теперь, после описания точек прерывания и символьных машин, можно объяснить, как отладчики реализуют три замечательные операции — Step Into, Step Over и Step Out. Они не реализованы в WDBG, потому что я хотел сконцентрироваться на основных частях отладчика. Эти функции работают в рамках двух специальных представлений1 отлаживаемой программы, которые позволяют отслеживать текущую выполняемую строку или команду.
    Все эти операции (Step Into, Step Over и Step Out) работают с одноразовыми точками прерывания, которые, как вы помните из предыдущих разделов, являются точками прерывания, сбрасываемыми отладчиком после того, как они срабатывают. При обсуждении пункта меню Debug Break (см. выше) был рассмотрен другой случай, в котором отладчик использует одноразовые точки прерывания для остановки обработки.
    Операция Step Into работает по-разному, в зависимости от того, на каком уровне выполняется отладка: на исходном или на уровне дизассемблирования. При отладке на исходном уровне отладчик должен ориентироваться на одноразовые точки прерывания, потому что одна строка языка высокого уровня переводится в одну или большее количество строк языка ассемблера. При переводе CPU в пошаговый режим будет происходить пошаговое выполнение индивидуальных машинных команд, а не строк исходного кода.
    На исходном уровне отладчик знает, на какой исходной строке вы находитесь. Когда выполняется команда отладчика Step Into, то для нахождения адреса следующей выполняемой строки отладчик использует символьную машину. Отладчик выполнит частичное дизассемблирование по адресу следующей строки, чтобы видеть, является ли эта строка командой вызова. Если строка — команда вызова, то отладчик установит одноразовую точку прерывания на первом же адресе функции, которую собирается вызывать подчиненный отладчик. Если адрес следующий строки — не команда вызова, то отладчик там и устанавливает точку прерывания one-shot. Затем отладчик разблокирует подчиненный отладчик, чтобы тот выполнился до только что установленной точки прерывания.
    Когда эта точка прерывания сработает, отладчик заменит код операции в точке ее размещения (в памяти) и освободит связанную с ней память. Если пользователь работает на уровне дизассемблирования, то реализовать Step Into намного легче, потому что отладчик будет просто переводить CPU в режим пошагового выполнения.

    Операция Step Over похожа на Step Into в том, что отладчик должен отыскивать следующую строку в символьной машине и dsgjkyznm частичное дизассемблирование по адресу строки. Различие их в том, что для Step Over (если строка является вызовом) отладчик также будет устанавливать точку прерывания one-shot, но после инструкции вызова.

    Операция Step Out, в некотором смысле, является самой простой из трех. Когда пользователь выбирает команду Step Out, отладчик проходит стек, чтобы найти адрес возврата для текущей функции и устанавливает по этому адресу точку прерывания one-shot.

    Source view (представление в виде строк исходного кода) и disassembly view (представление в виде кодов дизассемблера в окне Disassembly). — Пер.

    Обработка операций Step Into, Step Over и Step Out кажется довольно простой, но имеется одна особенность, которую следует рассмотреть. Что делать, если (в отладчике, создаваемом для управления этими операциями) уже установлены точки прерывания one-shot для этих операций, а перед ними срабатывает регулярная точка прерывания? Как разработчик отладчика, вы имеете две возможности. Первая — оставить только точки прерывания one-shot (чтобы только они и срабатывали). Другая возможность — удалять точки прерывания one-shot, когда отладчик уведомляет вас о том, что сработала регулярная точка прерывания. Отладчик Visual C++ использует последнюю возможность.

    Отладчики режима ядра

    Отладчики режима ядра находятся между CPU и операционной системой. Это означает, что, когда вы останавливаете отладчик режима ядра, операционная система также полностью останавливается. Нетрудно сообразить, что переход операционной системы к резкому останову полезен, когда вы работаете с таймером и над проблемами синхронизации. Все-таки, за исключением одного отладчика, о котором будет рассказано ниже (в разделе "Отладчик SoftlCE" данной главы), нельзя отлаживать код пользовательского режима с помощью отладчиков режима ядра.
    Отладчиков режима ядра не так много. Вот некоторые из них: Windows 80386 Debugger (WDEB386), Kernel Debugger (1386KD), WinDBG и SoftlCE. Каждый из этих отладчиков кратко описан в следующих разделах.
    Отладчик WDEB386
    WDEB386 — это отладчик режима ядра Windows 98, распространяемый в составе Platform SDK. Этот отладчик полезен только для разработчиков, пишущих драйверы виртуальных устройств Windows 98 (VxD). Подобно большинству отладчиков режима ядра для операционных систем Windows, отладчик WDEB386 требует для работы две машины и нуль-модемный кабель. Две машины необходимы потому, что часть отладчика, которая выполняется на целевой машине, имеет ограниченный доступ к ее аппаратным средствам, так что он посылает свой вывод и получает команды от другой машины.
    Отладчик WDEB386 имеет интересную историю. Он начинался как внутренний фоновый инструмент Microsoft в эпоху Windows 3.0. Его было трудно использовать, и он не имел достаточной поддержки для отладки исходного кода и других приятных свойств, которыми нас испортили отладчики Visual C++ и Visual Basic.
    "Точечные" (DOT) команды — наиболее важная особенность WDEB386. Через прерывание INT 41 можно расширять WDEB386 с целью добавления команд. Эта расширяемость позволяет авторам VxD-драйверов создавать заказные отладочные команды, которые дают им свободный доступ к информации в их виртуальных устройствах. Отладочная версия Windows 98 поддерживает множество DOT-команд, которые позволяют наблюдать точное состояние операционной системы в любой точке процесса отладки.

    Отладчик I386KD

    Windows 2000 отличается от Windows 98 тем, что реально действующая часть отладчика режима ядра является частью NTOSKRNL.EXE — файла главного ядра операционной системы Windows 2000. Этот отладчик доступен как в свободных (выпускных), так и в проверенных (отладочных) конфигурациях операционной системы. Чтобы включить отладку в режиме ядра, установите параметр загрузчика /DEBUG в BOOT.INI и, дополнительно, опцию загрузчика /DEBUGPORT, если необходимо установить значение коммуникационного порта отладчика режима ядра, отличающееся от умалчиваемого (СОМ1). I386KD выполняется на своей собственной машине и сообщается с машиной Windows 2000 через кабель нуль-модема.

    Отладчик режима ядра NTOSKRNL.EXE делает только то, что достаточно для управления CPU, так чтобы операционная система могла быть отлажена. Большая часть отладочной работы — обработка символов, расширенные точки прерывания и дизассемблирование — выполняется на стороне 1386KD. Одно время Windows NT 4 Device Driver Kit (DDK) документировал протокол, используемый в кабеле нуль-модема. Однако Microsoft больше его не документирует.

    Мощь 1386KD очевидна, если посмотреть на все команды, которые он предлагает для доступа к внутреннему состоянию Windows 2000. Знание механизма работы драйверов устройств в Windows 2000 поможет программисту следить за выводом многих команд. Не смотря на всю свою мощь, i386KD почти никогда не применяется, потому что это консольное приложение, которое очень утомительно использовать для отладок исходного уровня.

    Отладчик Win DBG

    WinDBG — это отладчик, который поставляется в составе Platform SDK. Можно также загрузить его с http://msdn.microsoft.com/developer/sdVdebidt.asp. Это гибридный отладчик, который может работать как в режиме ядра, так и в режиме пользователя, но WinDBG не позволяет отлаживать программы пользовательского режима и режима ядра одновременно. Для отладки в режиме ядра WinDBG предоставляет всю мощь отладчика i386KD, но с более легким в использовании внешним GUI-интерфейсом, который значительно упрощает отладку на уровне исходного кода.


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

    Как отладчик пользовательского режима WinDBG великолепен, и я настоятельно рекомендую установить его, если вы этого еще не сделали. WinDBG — более мощный отладчик, по сравнению с отладчиком Visual C++, в том отношении, что он показывает намного больше информации в процессе отладки. Однако за эту мощь надо платить: WinDBG труднее использовать, чем отладчик Visual C++. Тем не менее я посоветовал бы потратить время на изучение WinDBG. Это может окупиться намного более быстрым исправлением ошибок, чем с помощью отладчика Visual C++. Лично я провожу, в среднем, приблизительно 70% времени в отладчике Visual C++, а остальное — в WinDBG.

    При первом запуске WinDBG можно заметить, что у него имеется специальное окно команд (Command). Подобно отладчику Visual C++, WinDBG выполняет отладку на уровне исходного кода. Однако реальная мощь WinDBG проявляется в интерфейсе окна его команд. Почувствовав удобство различных команд, вы вскоре обнаружите, что с помощью окна Command можно выполнять отладку намного быстрее, чем с помощью только GUI-интерфейса.

    Возможности окна команд WinDBG возрастают, если добавить в него свои собственные команды, называемые WinDBG-расширениями. В то время как отладчик Visual C++ не очень гибок при остановке отлаживаемого процесса, в WinDBG имеется полный набор API-функций, позволяющих использовать все функциональные возможности отладчика, включая дизассемблер, символьную машину и машину трассировки стека. Дополнительную информацию о WinDBG-расширениях можно найти в разделе "Debugger Extension" (Расширение отладчика) на MSDN.

    В некоторых ситуациях лучше использовать WinDBG, а не отладчик Visual C++, потому что WinDBG поддерживает более мощный набор точек прерывания. Благодаря окну команд можно связывать команды с точкой прерывания. Это позволяет вывести отладку на совершенно новый уровень. Например, отлаживая программный модуль и выполняя многочисленные вызовы, очень удобно видеть значения каждого вызова без остановки приложения.


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

    В дополнение к лучшей расширяемости, чем у отладчика Visual C++, WinDBG имеет еще одно свойство, которое обязательно нужно рассмотреть, если приложение выполняется в Windows 2000 или Windows NT 4: WinDBG может читать файлы дампов пользовательского режима, созданные программой Dr. Watson. Это означает, что можно загружать в отладчик точное состояние программы во время аварийного прерывания, подробно его просмотреть и проанализировать. Дополнительную информацию о том, что нужно для установки этого свойства, можно найти в колонках "Bugslayer" журнала "Microsoft Systems Journal" ж декабрь 1999 и январь 2000.

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

    Отладчик SoftICE

    SoftICE — коммерческий отладчик режима ядра фирмы Compuware NuMega. Это единственный известный мне коммерческий отладчик подобного типа, а также единственный отладчик режима ядра, который может работать на одной машине. Однако, в отличие от других отладчиков этого режима, SoftICE превосходно выполняет работу по отладке программ пользовательского режима. Как упоминалось ранее, отладчики режима ядра находятся между CPU и операционной системой. При отладке программы пользовательского режима SoftICE также находится между CPU и операционной системой и, таким образом, останавливает в случае необходимости всю операционную систему целиком.

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


    Но зададим такой вопрос: " Что случится, если нужно отладить код, работающий с таймером?" Если вы используете такую API-функцию, как SendMessageTimeout, то можете легко попасть в ситуацию тайм-аута, когда выполняете пошаговый проход другого потока с помощью типового GUI-отладчика. Работая с SoftICE, можно "шагать" везде где угодно, потому что таймер, с которым имеет дело SendMessageTimeout, не будет выполняться, пока вы работаете под SoftICE. SoftICE — единственный отладчик, который позволяет эффективно отлаживать многопоточные приложения. Сам факт, что SoftICE останавливает всю операционную систему, когда он активен, означает, что решение таймерных проблем становится гораздо более легким.

    Другим преимуществом размещения SoftICE между CPU и операционной системой является то, что отладка межпроцессных взаимодействий становится очень легкой. Если вы занимаетесь СОМ-программированием с множеством внепроцессных служб, то можете легко устанавливать точки прерывания во всех процессах и выполнять пошаговые переходы между ними. Наконец, если нужно переходить от режима пользователя в режим ядра или обратно, то SoftICE делает такие переключения тривиально.

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

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


    Хотя в отладчиках 1386KD и WinDBG довольно много таких команд, в SoftICE их намного больше. В SoftICE можно просматривать почти все — от состояния всех событий синхронизации до полной HWND-информации и расширенной информации о любом потоке в системе.

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

    Общий вопрос отладки

    Как изменить отладчик по умолчанию, который будет использовать операционная система при аварийном сбое?

    В Windows 2000 сведения о том, что нужно вызывать для отладки приложения, завершившегося аварийно, содержатся под ключом реестра

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion \AeDebug

    В Windows 98 эта информация "прописана" в секции [AeDebug] файла WIN.INI. Если под указанным ключом (или в секции) нет никаких параметров, Windows 2000 сообщает только адрес аварийного сбоя. Если сбой произошел из-за нарушения доступа, Windows 2000 сообщает также адрес той области

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

    Под указанным ключом (Windows 2000) или в секции [AeDebug] (Windows 98) возможно размещение трех строчных параметров:

    • Auto

    • Debugger

    • UserDebuggerHotKey

    Если значение параметра Auto установить в 0, то операционная система будет генерировать стандартную диалоговую панель аварийного сбоя и активизирует в ней кнопку Cancel (Windows 2000) или Debug (Windows 98) на тот случай, если вы сами захотите присоединить отладчик. Если параметр Auto установить в 1, то отладчик стартует автоматически. Параметр Debugger указывает отладчик, который операционная система запустит на сбойном приложении.


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

    Раздел реестра AeDebug можно устанавливать вручную, но только утилита Dr. Watson (ее версия в Windows 2000), WinDBG и отладчик Visual C++ позволяют устанавливать в нем различные значения параметров. Dr. Watson и WinDBG используют ключ командной строки -I, который определяет их в качестве отладчика по умолчанию. Чтобы установить отладчик Visual C++ в качестве отладчика, который будет вызывать операционная система, включите флажок Just-In-Time Debugging на вкладке Debug диалогового окна Options (которое открывает команда ToolsjOptions... в IDE Microsoft Visual C++).

    Если заглянуть в раздел реестра AeDebug, то можно увидеть, что значение, которое введено для параметра Debugger, выглядит точно так же, как строка, передаваемая в API-функцию wsprintf:

    drwtsn32 -p %d -e %d -g

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

    Отладчики режима пользователя

    Отладчики режима пользователя предназначены для отладки любого приложения, выполняющегося в режиме пользователя, включая любые GUI-программы, а также такие не совсем обычные приложения, как службы (services) Windows 2000. В общем случае, отладчики этого типа поддерживают графический интерфейс пользователя (GUI1). Главный признак таких отладчиков — они используют отладочный интерфейс прикладного программирования (отладочный API) Win32. Поскольку операционная система помечает подчиненный отладчик как "выполняющийся в специальном режиме", то, чтобы выяснить, выполняется ли ваш процесс под отладчиком, можно использовать API-функцию IsDebuggerPresent.
    При поставке отладочного API Win32 подразумевается следующее соглашение: раз процесс выполняется под отладочным API (и делает его, таким образом, подчиненным процессом), основной отладчик не может отделиться от данного процесса. Эти симбиозные отношения означают, что если основной отладчик завершает работу, то завершается также и подчиненный отладчик. Основной отладчик ограничивается отладкой только подчиненного отладчика и любых порожденных им процессов (если основной отладчик поддерживает процессы-потомки).
    GUI — Graphical User Interface. — Пер.
    Для интерпретируемых языков и исполнительных (run-time) систем, которые используют подход виртуальной машины, полную отладочную среду обеспечивают сами виртуальные машины, и они не используют отладочный API Win32. Вот некоторые примеры таких типов сред: виртуальные Java-машины (JVM) фирм Microsoft или Sun, среда сценариев для Web-приложений фирмы Microsoft, и интерпретатор р-кода в системе Microsoft Visual Basic.
    Мы доберемся до отладки в Visual Basic в главе 7, но знайте, что интерфейс р-кода Visual Basic не документирован. Не будем вникать в отладочные интерфейсы Java и сценариев, эти темы выходят за рамки данной книги. Дополнительную информацию по отладке и профилированию Microsoft JVM ищите в разделе "Debugging and Profiling Java Applications" (Отладка и профилирование приложений Java) на MSDN. Набор таких интерфейсов весьма богат и разнообразен и позволяет полностью управлять работой JVM. Информацию о написании отладчика сценария можно найти в разделе MSDN "Active Script Debugging API Objects" (Активные объекты отладочных API-сценариев). Подобно JVM, объекты отладчика сценариев обеспечивают богатый интерфейс для сценариев доступа и документирования.
    Отладочный API Win32 использует удивительно много программ. К ним относятся: отладчик Visual C++, который подробно рассматривается в главах 5 и 6; отладчик Windows (WinDBG), который обсуждается в следующем разделе (посвященном отладчику режима ядра); программа BoundsChecker фирмы Compuware NuMega; программа Platform SDK HeapWalker; программа Platform SDK Depends; отладчики Borland Delphi и C++ Builder, а также символический отладчик NT Symbolic Debugger (NTSD). Я уверен, что их намного больше.


    Windows 2000 содержит набор специальных


    Windows 2000 содержит набор специальных API-функций, предназначенных для использования в отладчиках. Кроме того, эта операционная система обладает рядом свойств, весьма полезных для поиска проблемных приложений. Некоторые из них мало знакомы разработчикам.

    Когда приложение стартует под отладчиком,


    Когда приложение стартует под отладчиком, Windows 2000 включает проверку специальной отладочной области динамического распределения памяти (debug heap) операционной системы. Эта область не совпадает с отладочной heap-памятью С-библиотеки времени выполнения. Код этой области создается с помощью специальной API-функции — HeapCreate. О динамической области ("куче") С-библиотеки времени выполнения речь пойдет в главе 15. Поскольку отладочные heap-области используются процессами Windows 2000 довольно широко, с их информацией приходится сталкиваться часто, вот почему так важно поподробнее ознакомиться с ними. Если вы подключаете отладчик к приложению, а не стартуете приложение под отладчиком, то проверка отладочной heap-области в операционной системе Windows 2000 активизирована не будет.

    Если проверка отладочной динамической области включена, то приложение будет выполняться немного медленнее, потому что когда в приложении вызывается функция HeapFree, то приходится дополнительно проверять корректность "кучи". В листинге 4-1 показан пример программы, которая портит память. Если эта программа выполняется под отладчиком, то нетрудно заметить, что функция DebugBreak вызывается дважды (на первом же вызове функции HeapFree). Ниже показан вывод, по которому видно, что при работе с heap-областью возникли некоторые проблемы.

    HEAP[Heaper.exe]: Heap block at 00441E98 modified at 00441EAA past

    requested size of a

    HEAP[Heaper.exe]: Invalid Address specified to

    RtlFreeHeapt 440000, 441eaO)

    Если эта программа выполняется вне отладчика, то она завершается без сообщений о каких-либо проблемах.

    Если вы используете в Windows 2000 свои собственные2 heap-области, то для того чтобы получить более полный диагностический вывод, можно включить несколько дополнительных флажков. Для этого в Platform SDK включена небольшая утилита GFLAGS.EXE. С ее помощью можно установить несколько глобальных флажков, проверяемых операционной системой при первом запуске приложения. Установки, выполненные этой утилитой для файла HEAPER.EXE, показаны на рис. 4.1.
    Heap - "куча", область динамически распределяемой памяти. — Пер.

    Что вполне возможно, т. к. не только отладчики, но и любые приложения могут вызывать функцию HeapCreate. — Пер.

    Этот набор инструментов вы можете найти на сопровождающем CD. — Пер.

    Это загрузочный модуль, частью которого является функция, показанная в листинге 4-1. — Пер.

    Многие опции кнопок System Registry и Kernel Mode переключателя Destination окна Global Flags являются глобальными. Нужно проявлять особую осторожность при их установке, потому что они могут оказать решающее влияние на производительность системы. Установка переключателя Destination в положение Image File Options намного безопаснее, потому что все установки влияют только на один модуль (имя которого указано в соседнем поле Image File Name).

    Когда приложение стартует под отладчиком,
    Рис. 4.1. Вывод программы GFLAGS.EXE

    Листинг 4-1. Пример разрушения heap-области Windows 2000

    void main(void)

    // Создать heap-область операционной системы.

    HANDLE hHeap = HeapCreate ( 0, 128, 0) ;

    // Распределить память для блока размером в 10 байтов.

    LPVOID pMem = HeapAlloc ( hHeap, 0, 10);

    // Записать 12 байт в 10-байтовый блок (переполнение heap-области).

    memset ( pMem, OxAC, 12);

    // Распределить новый блок размером 20 байт.

    LPVOID pMem2 = HeapAlloc ( hHeap, 0, 20);

    // Записать 1 байт во второй блок.

    char * pUnder = (char *)( (DWORD)pMem2 - 1);

    *pUnder = 'P';

    // Освободить первый блок. Это обращение к HeapFree будет

    // инициировать точку прерывания в коде отладочной heap-области

    // операционной системы.

    HeapFree ( hHeap, 0, pMem);

    // Освободить второй блок. Заметим, что этот вызов не будет

    // выдавать соообщения о неполадке

    HeapFree ( hHeap, 0, pMem2);

    // Освободить фиктивный блок. Заметим, что этот вызов не будет

    // выдавать сообщения о неполадке

    HeapFree ( hHeap, О, (LPVOID)Oxl); HeapDestroy ( hHeap);

    }

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


    PAGEHEAP: process 0x490 created debug heap 00430000

    (flags 0xl, 50, 25, 0, 0)

    PAGEHEAP: process 0x490 created debug heap 00CF0000

    (flags Oxl, 50, 25, 0,- 0)

    PAGEHEAP: process 0x490 created debug heap 01600000

    (flags Oxl, 50, 25, 0, 0)

    PAGEHEAP: Tail fill corruption detected:

    Allocation at 0x01606FF0

    Requested size 0x0000000A

    Allocated size 0x00000010

    Corruption at Ox01606FFA

    PAGEHEAP: Attempt to reference block which is not allocated

    Содержимое листинга объясняют названия флажков, установленных панелью Global Flags.

    Обсуждая программу GFLAGS.EXE, я хочу указать на одну очень полезную опцию — Show Loader Snaps. Если вы установите этот флажок и выполните приложение, то увидите то, что называют снимком (snap) приложения, в котором видно, где Windows 2000 загружает DLL-файлы и как она начинает организацию импорта. Если необходимо точно видеть, что делает загрузчик Windows 2000 при загрузке приложения (особенно в том случае, когда в нем обнаружена проблема), то включение этой опции может оказаться весьма полезным мероприятием. Дополнительную информацию по снимкам загрузчика можно получить в колонке Мэта Пьетрека "Under the Hood" в сентябрьском выпуске Microsoft Systems Journal за 1999 год.

    Таблицы символов, символьные машины и проход стека

    Реальное мастерство в написании отладчиков опирается на символьные машины (т. е. на программный код, который манипулирует таблицами символов). Отладка на традиционном уровне языка ассемблера интересна в течение первой пары минут работы, но быстро надоедает. Таблицы символов (symbol tables), называемые также символами отладки (debugging symbols) — это то, что превращает шестнадцатеричные числа в строки, имена функций и имена переменных исходных файлов. Таблицы символов содержат, кроме того, информацию о типах, которую использует ваша программа. Эта информация позволяет отладчику отображать на экране "сырые" данные как структуры и переменные, которые вы определили в своей программе. Иметь дело с современными таблицами символов трудно, потому что наиболее часто используемый их формат — PDB (Program DataBase — База данных программы), не документирован, и владельцы прав не планируют его документировать. К счастью, можно получить по крайней мере частичный доступ к таблицам символов.
    Форматы символов отладки
    Прежде чем углубляться в обсуждение организации доступа к таблицам символов, приведем обзор различных форматов отладочных символов. Думаю, что даже опытные программисты слабо разбираются в этих форматах, так что поговорим об этом подробнее.
    SYM — самый старый формат, который применялся во времена MS-DOS и 16-разрядных Windows. В настоящее время SYM-формат употребляется только для отладочных символов в Windows 98. SYM-формат применяется лишь потому, что большая часть основного ядра операционной системы все еще остается 16-разрядным кодом. Единственный отладчик, активно использующий символы этого формата, — это WDEB386.
    COFF (Common Object File Format — общий формат объектных файлов) :дин из первых форматов таблиц символов, который был представлен в Windows NT 3.1 (первой версии Windows NT). Команда разработчиков Windows NT имела опыт в разработке операционной системы и хотела загружать Windows NT с помощью некоторых существующих инструментов. Формат COFF является частью большой спецификации, которой пользовались различные поставщики UNIX-систем, пытавшиеся создать общие форматы двоичных файлов.
    Хотя в WINNT.H находится полная спецификация COFF-символов, инструменты Microsoft генерируют только некоторые еe части — public-функции и глобальные переменные. В Microsoft привыкли поддерживать исходную и строчную информацию, но постепенно отошли от формата COFF в пользу более современных форматов символьных таблиц.
    Формат С7 или CodeView появился еще во времена MS-DOS как часть системы программирования Microsoft C/C++ версии 7. Возможно, вы слышали название "CodeView". Это имя старого отладчика Microsoft. Формат С7 был модифицирован, чтобы поддерживать операционные системы Win32, и тетерь можно генерировать этот формат, запуская компилятор CL.EXE из командной строки с ключом /Z7 или выбирая пункт С7 Compatible1 из раскрывающегося списка Debug Info на вкладке C/C++ диалогового окна Project Settings.
    Здесь и далее речь идет об интерфейсе с IDE Microsoft Visul C++. — Пер
    Чтобы включить параметр компоновщика /PDB:NONE, на вкладке Link диалогового окна Project Settings выключите в категории Customize флажок Use Program Database. Отладчики WinDBG и Visual C++ поддерживают как полную отладку на уровне исходного кода, так и построчную отладку в формате С7. Компоновщик добавляет символьную информацию к двоичному файлу уже после того, как он выполнит компоновку, потому отладочные символы формата С7 содержатся внутри выполняемого модуля. Добавление отладочных символов более чем вдвое увеличивает объем двоичного файла. Для того чтобы узнать, содержит ли двоичный файл отладочную информацию формата С7, нужно открыть его в шестнадцатеричном редакторе и, переместившись к его концу, просмотреть содержимое последних строк. Если будет обнаружена строка с последовательностью символов "NB11", значит файл содержит отладочную информацию формата С7.
    Спецификацию С7 можно найти на MSDN в разделе "VC5.0 Symbolic Debug Information". Спецификация перечисляет только необработанную байтовую структуру и определения типов. Тем, кто захочет увидеть фактические определения типов на С, рекомендуем просмотреть исходный код программы Dr.


    Watson на компакт-дисках MSDN. В каталоге \ Include этой программы имеется несколько файлов заголовков старого формата С7. Хотя эти файлы значительно устарели, они могут дать некоторое представление о том, на что похожи эти структуры.
    При желании, конечно, можно использовать формат С7, но лучше этого не делать. Отказаться от использования формата С7 нужно по двум причинам. Во-первых, он автоматически выключает инкрементную компоновку, из-за чего катастрофически увеличивается время компоновки. Во-вторых, значительно возрастает размер двоичных файлов. Можно убрать символьную информацию с помощью программы REBASE.EXE, но существуют такие форматы (например, PDB), которые удаляют ее автоматически.
    PDB — наиболее общий из используемых сегодня символьных форматов, поддерживает как Visual C++, так и Visual Basic. В отличие от формата С7, PDB-символы сохраняются в отдельном файле или файлах, в зависимости от того, как приложение скомпоновано. По умолчанию, загрузочный файл Visual C++ 6 компонуется с ключом /PDBTYPE:SEPT, который помещает информацию о типах в файл VC60.PDB, а сами символы — в файл <имя-двоичного-файла>.РОВ. Отделение информации о типах от символов отладки ускоряет компоновку и требует меньше места на диске. Однако в документации указано, что если вы строите двоичный файл, который могли бы j отлаживать другие, то, чтобы вся информация о типах и символы отладки были сведены в единый PDB-файл, нужно указать ключ /PDBTYPE:CON. К счастью, Visual Basic автоматически использует этот ключ.
    Чтобы посмотреть, содержит ли двоичный PDB-файл символьную информацию, откройте его в шестнадцатеричном редакторе и перейдите к концу файла. Вы увидите маркер отладочной информации. Если маркер начинает>:я с символов "NB10" и заканчивается полным путем к PDB-файлу, построенному во время компоновки, то двоичный файл включает PDB-символы. Отладочный формат PDB внутренне напоминает формат С7. Однако компания Microsoft оптимизировала этот формат для инкрементной компоновки.


    К сожалению, интерфейсы низкого уровня с PDB-файлами являются собственностью фирмы Microsoft и не опубликованы.
    DBG — файлы этого формата уникальны в том смысле, что, в отличие от файлов других символьных форматов, их создает не компоновщик. В них хранятся отладочные символы форматов COFF или С7. В DBG-файлах применяются структуры, определенные файловым форматом РЕ (Portable Executable), который использует все выполняемые файлы в операционных системах Win32. DBG-файлы создаются с помощью утилиты REBASE.EXE, которая извлекает из программного модуля отладочную информацию форматов COFF или С7 и помещает ее в DBG-файл. Нет никакой необходимости выполнять REBASE.EXE для модуля, который был построен с использованием PDB-файлов, потому что символы уже отделены от модуля. Желающим изучить методику создания DBG-файлов следует прочитать MSDN-доку-ментацию по REBASE.EXE. Microsoft распространяет отладочные символы операционной системы в DBG-файлах, а в Windows 2000 также включены и PDB-файлы. Не надейтесь, что отладочные символы операционной системы включают все, что нужно, скажем, для ее обратного проектирования. Учтите, что DBG-файлы содержат только общую и глобальную информацию. Однако их использование может значительно облегчить вашу работу с информацией окна Disassembly.
    Если вы заинтересовались символьными машинами и начинаете исследовать возможности их программирования, то рано или поздно встретите символы еще одного типа — ОМАР. Символы этого типа появляются только в некоторых приложениях Microsoft. Их можно иногда встретить при получении дампа символьной информации с помощью утилиты DUMPBIN.EXE, запускаемой с параметром /SYMBOLS. (DUMPBIN.EXE распространяется вместе с системой программирования Visual C++.) Символьный формат ОМАР совершенно не документирован. Как уже говорилось, Microsoft использует специальный внутренний инструмент, который реорганизует компилированный двоичный файл так, чтобы поместить наиболее часто вызываемый код в начало файла. ОМАР-символы имеют какое-то отношение к тем отладочным символам, которые принимают во внимание этот послекомпоновочный шаг.


    Подобную оптимизацию выполняет и программа Working Set Tuner (WST), поставляемая вместе с Platform SDK. WST работает на функциональном уровне, не проникая внутрь функций, тогда как инструмент Microsoft опускается до так называемого уровня базовых блоков. В следующем кодовом фрагменте базовый блок обозначен стрелками:
    if ( TRUE = blsError)
    { <- Начало базового блока.
    // Обработка ошибок.
    } <- Конец базового блока.
    Инструмент Microsoft перемещает обработчик ошибки в конец двоичного файла, так что только наиболее общий код размещается в его начале. ОМАР-символы оказываются некоторой разновидностью адресных записей (fixup1) для главных символов, потому что инструмент Microsoft манипулирует двоичным файлом уже после того, как тот был построен.
    Доступ к символьной информации
    Для доступа к символьной информации можно использовать символьную машину DBGHELP.DLL фирмы Microsoft. DBGHELP.DLL может читать символьные форматы PDB, COFF и С7. В прошлом символьная машина была в IMAGEHLP.DLL, но Microsoft умудрился вывести ее из ядра системы и поместил в DLL, которую было легче обновлять. Для работы с программами, которые использовали символьную машину, встроенную в IMAGEHLP.DLL, в него все еще включены списки экспортируемых функций символьной машины (symbol engine exports). Новая IMAGEHLP.DLL пересылает соответствующие функции в DBGHELP.DLL. Во время написания этой книги MSDN-документация символьной машины еще оставалась частью библиотеки IMAGEHLP.DLL.
    Символьная машина DBGHELP.DLL преобразует адрес в имя ближайшей общей функции или глобальной переменной. Она может также работать и наоборот, отыскивая адрес по имени конкретной функции. Наконец, она может отыскивать имя исходного файла и номер строки для конкретного адреса. Символьная машина DBGHELP.DLL не поддерживает ни поиск параметров или локальных переменных, ни оценку их типов. Как будет показано позже, используя только эти ограниченные функциональные возможности, можно построить некоторые превосходные утилиты, оказывающие неоценимую помощь при поиске многочисленных проблем в приложениях.


    В WDBG был использован простой класс-оболочка (язык C++), который показан (файл SYMBOLENGINE.H) в листинге 4-7. Первоначально этот класс был написан как часть библиотеки BUGSLAYERUTIL.DLL. Это значительно урезанный вариант API символьной машины DBGHELP.DLL, но он обеспечивает и некоторые дополнительные возможности для решения проблем, с которыми приходится сталкиваться в старых версиях символьной машины IMAGEHLP.DLL. Исходный код SYMBOLENGINE.H представлен на тот случай, если придется использовать этот класс со старыми символьными машинами IMAGEHLP.DLL.
    Fixup — адресная запись (запись, генерируемая ассемблером для редактора связей в отношении каждого адреса, который не может быть определен). — Пер.
    Листнг4-7.Файл SYMBOLENGINE.H
    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - -
    "Debugging Applications" (Microsoft Press)
    Copyright (c) 1997-2000 John Robbins — All rights reserved.
    - - - - - - - -- - - - - - - - - - - - - - - - - - - - - -
    Этот класс - сокращенный вариант символьной машины DBGHELP.DLL. Эн охватывает только те функции, которые имеют уникальное значение HANDLE-дескриптора. Остальные функции символьной машины DBGHELP.DLL являются глобальными, поэтому они не включены в этот класс.
    - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - -
    Макроопределения компиляции:
    DO_NOT_WORK_AROUND_SRCLINE_BUG —
    При определении этой константы класс не будет работать с ошибкой SymGetLineFromAddr, когда поиски PDB fMile терпят неудачу уже после первого поиска.
    USE_BUGSLAYERUTIL —
    При определении этой константы данный класс будет применять другой метод инициализации символьной машины — BSUSymlnitialize из BUGSLAYERUTIL.DLL. Кроме того, флажок захвата процесса начнет работать для всех 32-разрядных Windows-систем ОС. При использовании этого определения кроме SYMBOLENGINE.Н нужно также включать и файл BUGSLAYERUTIL.H.
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
    #ifndef _SYMBOLENGINE_H


    #define _SYMBOLENGINE_H
    // Можно включить либо IMAGEHLP.DLL, либо DBGHELP.DLL.
    # include "imagehlp.h"
    #include
    // Включайте эти директивы в случае, если пользователь забывает
    // про компоновку соответствующих библиотек
    #pragma comment (lib, "dbghelp. lib")
    #pragma comment (lib, "version, lib")
    // Грандиозная идея создания классов-оболочек на структурах, которые
    // имеют размерные поля, пришла от коллеги-журналиста из MSJ Поля
    // (Paul DiLascia). Спасибо, Поль!;
    // Я не включаю в класс константу IMAGEHLP_SYMBOL, потому что это
    // структура переменного размера.
    // Класс-оболочка IMAGEHLP_MODULE
    struct CImageHlp_Module : public IMAGEHLP_MODULE
    {
    CImageHlp_Module ()
    {
    memset ( this, NULL, sizeof ( IMAGEHLP_MODULE));
    SizeOfStruct = sizeof ( IMAGEHLP_MODULE);
    }
    };
    // Класс-оболочка IMAGEHLP_LINE
    struct CImageHlp_Line : public IMAGEHLP_LINE
    {
    CImageHlp_Line ()
    {
    memset ( this, NULL, sizeof ( IMAGEHLP_LINE));
    SizeOfStruct = sizeof ( IMAGEHLP_LINE);
    }
    };
    // Класс символьной машины class CSymbolEngine
    {
    /*- - - - - - - - - - - - - - - - - - - - - - - - - -
    Public-конструктор и деструктор
    - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - */
    public :
    // Чтобы использовать этот класс, вызовите метод Symlnitialize для
    // инициализации символьной машины и затем применяйте другие методы
    // вместо соответствующих функций из DBGHELP.DLL
    CSymbolEngine ( void)
    {
    }
    virtual -CSymbolEngine ( void)
    {
    }
    / *- - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Вспомогательные информационные public-функции
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
    public :
    // Возвращает версию используемого файла DBGHELP.DLL.
    // Чтобы преобразовать возвращаемые значения в читаемый формат,
    // применяется функция:
    // wsprintf ( szVer ,
    // _Т ( "%d.%02d.%d.%d"),
    // HIWORD ( dwMS)
    // LOWORD ( dwMS)
    // HIWORD ( dwLS)
    // LOWORD ( dwLS) );


    // Параметр szVer будет содержать строку вида: 5.00.1878.1
    BOOL GetlmageHlpVersion ( DWORD & dwMS, DWORD & dwLS)
    {
    return( GetlnMemoryFileVersion ( _T ( "DBGHELP.DLL"),
    dwMS ,
    dwLS ) ) ;
    }
    BOOL GetDbgHelpVersion ( DWORD & dwMS, DWORD & dwLS)
    {
    return( GetlnMemoryFileVersion ( __T ( "DBGHELP.DLL"),
    dwMS ,
    dwLS ) ) ;
    }
    // Возвращает версию DLL-файлов, читающих PDB.
    BOOL GetPDBReaderVersion ( DWORD & dwMS, DWORD & dwLS)
    {
    // Первым проверяется файл MSDBI.DLL.
    if ( TRUE == GetlnMemoryFileVersion ( _T ( "MSDBI.DLL"),
    dwMS ,
    dwLS ) )
    {
    return ( TRUE);
    }
    else if.( TRUE == GetlnMemoryFileVersion ( _T ( "MSPDB60.DLL"),
    dwMS
    dwLS ) )
    {
    return ( TRUE);
    }
    // Теперь пришла очередь проверить MSPDB50.DLL.
    return ( GetlnMemoryFileVersion ( _T ( "MSPDB50.DLL"),
    dwMS
    dwLS ) ) ;
    }
    // Рабочая функция, используемая двумя предшествующими функциями.
    BOOL GetlnMemoryFileVersion ( LPCTSTR szFile,
    DWORD & dwMS ,
    DWORD & dwLS )
    {
    HMODULE hlnstlH = GetModuleHandle ( szFile);
    // Получить полное имя файла загруженной версии
    TCHAR sz!mageHlp[ MAX_PATH ];
    GetModuleFileName ( hlnst-IH, szImageHlp, MAX_PATH);
    dwMS = 0;
    dwLS = 0;
    // Получить размер информации о версии.
    DWORD dwVerlnfoHandle;
    DWORD dwVerSize;
    dwVerSize = GetFileVersionlnfoSize ( szImageHlp ,
    SdwVerlnfoHandle );
    if ( 0 == dwVerSize)
    {
    return ( FALSE);
    }
    // Получили размер информации о версии, теперь получим
    // саму информацию.
    LPVOID IpData = (LPVOID)new TCHAR [ dwVerSize ];
    if ( FALSE == GetFileVersionlnfo ( szImageHlp ,
    dwVerlnfoHandle , dwVerSize , IpData ))
    {
    delete [] IpData; return ( FALSE);
    }
    VS_FIXEDFILEINFO * IpVerlnfo;
    UINT uiLen;
    BOOL bRet = VerQueryValue ( IpData ,
    _T ( "\\")
    (LPVOID*)SlpVerlnfo, &uiLen ) ;


    if ( TRUE == bRet)
    {
    dwMS = lpVerInfo->dwFileVersionMS;
    dwLS = lpVer!nfo->dwFileVersionLS;
    }
    delete [] IpData; return ( bRet);
    }
    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Public-методы инициализации и чистки
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
    public :
    BOOL Symlnitialize ( IN HANDLE hProcess ,
    IN LPSTR UserSearchPath,
    IN BOOL flnvadeProcess )
    {
    m_hProcess = hProcess;
    return ( ::Symlnitialize ( hProcess ,
    UserSearchPath, fInvadeProcess ));
    }
    #ifdef USE_BUGSLAYERUTIL
    BOOL BSUSymlnitialize ( DWORD dwPID ,
    HANDLE hProcess ,
    PSTR UserSearchPath,
    BOOL flnvadeProcess )
    {
    m_hProcess = hProcess;
    return ( ::BSUSymlnitialize ( dwPID ,
    hProcess , UserSearchPath, flnvadeProcess ));
    }
    #endif // USE_BUGSLAYERUTIL
    BOOL SymCleanup ( void)
    {
    return ( ::SymCleanup ( m_hProcess)) ;
    }
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Public-методы манипуляций с модулями
    - - - - - - - - - - - - - - - - - - - - - - - - - - * /
    public :
    BOOL SymEnumerateModules ( IN PSYM_ENUMMODULES_CALLBACK
    EnumModulesCallback,
    IN PVOID UserContext)
    {
    return ( ::SymEnumerateModules ( m_hProcess ,
    EnumModulesCallback ,
    UserContext )) ;
    }
    BOOL SymLoadModule { IN HANDLE hFile ,
    IN PSTR ImageName ,
    IN PSTR ModuleName ,
    IN DWORD BaseOfDll ,
    IN DWORD SizeOfDll )
    {
    return ( ::SymLoadModule ( m_hProcess ,
    hFile
    ImageName ,
    ModuleName ,
    BaseOfDll SizeOfDll ));
    }
    BOOL EnumerateLoadedModules ( IN PENUMLOADED_MODULES_CALLBACK
    EnumLoadedModulesCallback,
    IN PVOID UserContext )
    {
    return ( ::EnumerateLoadedModules ( m_hProcess ,
    EnumLoadedModulesCallback,
    UserContext ));
    }
    BOOL SymUnloadModule ( IN DWORD BaseOfDll)
    {
    return ( ::SymUnloadModule ( m_hProcess, BaseOfDll));
    }
    BOOL SymGetModulelnfo ( IN DWORD dwAddr


    OUT PIMAGEHLP__MODULE Modulelnfo )
    {
    return ( ::SymGetModulelnfo ( m_hProcess ,
    dwAddr ,
    Modulelnfo ));
    }
    DWORD SymGetModuleBase ( IN DWORD dwAddr)
    {
    return ( ::SymGetModuleBase ( m_hProcess, dwAddr));
    }
    /*- - - - - - - - - - - - - - - - - - - - - - - - - -
    Public-методы манипуляций с символами
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
    public :
    BOOL SymEnumerateSymbols ( IN DWORD BaseOfDll,
    IN PSYM_ENUMSYMBOLS_CALLBACK
    EnumSymbolsCallback,
    IN PVOID UserContext)
    {
    return ( ::SymEnumerateSymbols ( m_hProcess ,
    BaseOfDll
    EnumSymbolsCallback,
    UserContext ));
    }
    BOOL SymGetSymFromAddr ( IN DWORD dwAddr ,
    OUT PDWORD pdwDisplacement,
    OUT PIMAGEHLP_SYMBOL Symbol )
    {
    return ( ::SymGetSymFromAddr ( m_hProcess ,
    dwAddr ,
    pdwDisplacement ,
    Symbol ));
    }
    BOOL SymGetSymFromName ( IN LPSTR Name ,
    OUT PIMAGEHLP_SYMBOL Symbol )
    {
    return ( ::SymGetSymFromName ( m_hProcess,
    Name ,
    Symbol }};
    }
    BOOL SymGetSymNext ( IN OUT PIMAGEHLP_SYMBOL Symbol)
    {
    return ( ::SymGetSymNext ( m_hProcess, Symbol));
    }
    BOOL SymGetSymPrev ( IN OUT PIMAGEHLP_SYMBOL Symbol)
    {
    return ( ::SymGetSymPrev ( m_hProcess, Symbol));
    }
    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Public-метод манипуляций с исходной строкой
    - - - -- - - - - - - - - - - - - - - - - - - - - - - - - */
    public :
    BOOL SymGetLineFromAddr ( IN DWORD dwAddr ,
    OUT PDWORD pdwDisplacement, OUT PIMAGEHLP_LINE Line )
    {
    # ifde f DO_NOT_WORK_AROUND_SRCLINE_BUG
    // Просто передайте значения, возвращенные main-функцией
    return ( ::SymGetLineFromAddr ( m_hProcess ,
    dwAddr
    , pdwDisplacement,
    Line ) ) ;
    #else
    // Проблема в том, что символьная машина находит только те адреса
    // исходных строк (после первого поиска), которые попадают точно
    //на нулевые смещения. Чтобы найти строку и возвратить


    // подходящее смещение, я возвращаюсь назад на 100 байт.
    DWORD dwTempDis = 0;
    while ( FALSE == ::SymGetLineFromAddr ( m_hProcess ,
    dwAddr — dwTempDis ,
    pdwDisplacement ,
    Line ) )
    }
    dwTempDis += 1;
    if ( 100 == dwTempDis)
    {
    return ( FALSE);
    }
    }
    if (0 != dwTempDis)
    {
    *pdwDisplacement = dwTempDis;
    }
    return { TRUE);
    #endif // DO_NOT_WORK_AROUND_SRCLINE_BUG
    }
    BOOL SymGetLineFromName ( IN LPSTR ModuleName ,
    IN LPSTR FileName ,
    IN DWORD dwLineNumber ,
    OUT PLONG plDisplacement ,
    IN OUT PIMAGEHLP_LINE Line )
    {
    return ( ::SymGetLineFromName ( m_hProcess ,
    ModuleName ,
    FileName ,
    dwLineNumber ,
    plDisplacement ,
    Line ) ) ;
    }
    BOOL SymGetLineNext ( IN OUT PIMAGEHLP_LINE Line)
    {
    return ( ::SymGetLineNext ( m_hProcess, Line));
    }
    BOOL SymGetLinePrev ( IN OUT PIMAGEHLP_LINE Line)
    {
    return ( ::SymGetLinePrev ( m_hProcess, Line));
    }
    BOOL SymMatchFileName ( IN LPSTR FileName ,
    IN LPSTR Match ,
    OUT LPSTR * FileNameStop ,
    OUT LPSTR * MatchStop )
    {
    return ( ::SymMatchFileName ( FileName ,
    Match ,
    FileNameStop ,
    MatchStop ));
    }
    /*- - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -
    Разные public-члены
    - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -*/
    public :
    LPVOID SymFunctionTableAccess ( DWORD AddrBase)
    {
    return ( ::SymFunctionTableAccess ( m_hProcess, AddrBase));
    }
    BOOL SymGetSearchPath ( OUT LPSTR SearchPath ,
    IN DWORD SearchPathLength )
    {
    return ( ::SymGetSearchPath ( m_hProcess ,
    SearchPath ,
    SearchPathLength ));
    }
    BOOL SymSetSearchPath ( IN LPSTR SearchPath)
    {
    return ( ::SymSetSearchPath ( m_hProcess, SearchPath));
    }
    BOOL SymRegisterCallback ( IN PSYMBOL_REGISTERED_CALLBACK
    CallbackFunction,
    IN PVOID UserContext )
    {
    return ( ::SymRegisterCallback ( m_hProcess ,


    CallbackFunction ,
    UserContext ));
    }
    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Защищенные члены данных
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
    protected :
    // Уникальное значение, которое будет использоваться для этого
    // экземпляра символьной машины. Это значение не должно быть
    // значением актуального процесса, а просто — уникальным значением.
    HANDLE m_hProcess ;
    };
    ttendif // _SYMBOLENGINE_H
    Перед появлением Windows 2000 получить рабочий вариант символьной машины, поддерживаемой фирмой Microsoft, было не так легко. Главная причина трудностей состояла в том, что символьная машина была включена в состав системного файла IMAGEHLP.DLL, который использовали многие программы. Заменить ее новым вариантом внутри загруженного системного файла было невозможно, а получить более новую версию — сложно. Теперь, когда DBGHELP.DLL больше не является системной библиотекой, ее гораздо легче обновлять. Для этого нужно просто следить, чтобы на вашей машине всегда была установлена последняя версия Platform SDK. Ее всегда можно загрузить с сайта www.microsoft.com или получить как часть подписки MSDN. Все исходные коды в этой книге ориентированы на DBGHELP.DLL, поэтому нужно следить за тем, чтобы DBGHELP.DLL была установлена и путь к ее каталогу указан в переменной окружения PATH.
    Установка DBGHELP.DLL — только часть дела, ведь для того чтобы загружать символьные файлы, нужно гарантировать их доступность для символьной машины. В случае DBG-файлов символьная машина DBGHELP.DLL будет искать их в следующих местах:
  • текущий рабочий каталог приложения, использующего DBGHELP.DLL (а не подчиненный отладчик!);
  • переменная среды _NT_SYMBOL_PATH;
  • переменная среды _NT_ALT_SYMBOL_PATH;
  • переменная среды SYSTEMROOT.
  • Каталоги, на которые указывают переменные среды, должны быть организованы определенным образом. Например, если приложение состоит из ЕХЕ-файла и пары DLL, расположенных в каталоге C:\MyFiles, то под этим каталогом нужно создать следующую структуру подкаталогов:


  • C:\MyFiles
  • C:\MyFiles\Symbols
  • C:\MyFiles\Symbols\Exe
  • C:\MyFiles\Symbols\Dll
  • Два последних подкаталога предназначены для размещения соответствующих DBG-файлов приложения.
    Единственное различие при работе с PDB-файлами состоит в том, что символьная машина DBGHELP.DLL будет отыскивать PDB-файлы в первичном каталоге приложения и пробовать загружать PDB из этого каталога. Если символьная машина DBGHELP.DLL не сможет загрузить PDB-файлы из этого каталога, то она будет пытаться искать и загружать их так же, как DBG-файлы (т. е. из тех же подкаталогов, которые должны были быть созданы для хранения файлов отладочных символов).
    То есть для каждого типа рабочих файлов приложения создается отдельный подкаталог для DBG-файлов. — Пер.
    Где хранятся как двоичные файлы приложения, так и соответствующие PDB-файлы, которые были созданы компоновщиком на этапе отладочного построения. — Пер.
    Прохождение стека
    К счастью для всех нас, нет необходимости писать собственный код для прохода стека. В DBGHELP.DLL определена специальная API-функция stackwalk, которая берет на себя все заботы по работе со стеком. WDBG использует ее точно так же, как это делает отладчик Visual C++. Единственная неприятность — нет подробной документации по структуре STACKFRAME. В листинге 4-8 показаны только те поля этой структуры, которые должны быть заполнены. Функция stackwalk так хорошо заботится обо всех деталях, что вы можете и не знать, что при оптимизированном коде проход по стеку может быть довольно трудной задачей. Причина этих трудностей заключается в том, что для некоторых функций компилятор может выполнять оптимизацию вдали от области стека, т. е. от того места, где выталкиваются его элементы. Компиляторы Visual C++ и Visual Basic довольно агрессивны, когда они выполняют оптимизацию, и если они могут использовать стековый регистр как рабочий, то они будут это делать. Чтобы облегчать работу со стеком в таких ситуациях, компилятор генерирует то, что называется данными FPO (Frame Pointer Omission).


    FPO-данные — это таблица, которую функция stackwalk использует для вычислений, связанных с обработкой тех функций, которые пропускают нормальную область стека. Мы рассматриваем FPO-данные еще и потому, что ссылки на них иногда встречаются в MSDN и в различных отладчиках. Можно подробнее познакомиться со структурой FPO-данных в файле WINNT.H.
    Листинг 4-8. InitializeStackFrameWithGontext из i386CPUHELP.C
    BOOL CPUHELP_DLLINTERFACE _stdcall
    InitializeStackFrameWithContext ( STACKFRAME * pStack,
    CONTEXT * pCtx)
    {
    ASSERT ( FALSE == IsBadReadPtr ( pCtx, sizeof ( CONTEXT)));
    ASSERT ( FALSE == IsBadWritePtr ( pStack, sizeof ( STACKFRAME))
    } ;
    if ( ( TRUE == IsBadReadPtr ( pCtx, sizeof ( CONTEXT))) ||
    ( TRUE == IsBadWritePtr ( pStack, sizeof ( STACKFRAME))))
    {
    return ( FALSE);
    }
    pStack->AddrPC.Offset = pCtx->Eip;
    pStack->AddrPC.Mode = AddrModeFlat ;
    pStack->AddrStack.Offset = pCtx->Esp;
    pStack->AddrStack.Mode = AddrModeFlat ;
    pStack->AddrFrame.Offset = pCtx->Ebp;
    pStack->AddrFrame.Mode = AddrModeFlat ;
    return ( TRUE);
    }

    Типы Windows-отладчиков

    Если вы хоть немного программировали для Windows, то, вероятно, слышали о различных типах отладчиков, которые можно при этом использовать. В мире Windows доступны два типа отладчиков: отладчики режима пользователя (user-mode debuggers) и отладчики режима ядра (kernel-mode debuggers).
    Большинству разработчиков больше знакомы отладчики пользовательского режима. Не удивительно, что отладчики этого режима предназначены для отладки приложений пользовательского режима (user-mode applications). Главный пример отладчика режима пользователя — отладчик Microsoft Visual C++. Отладчики режима ядра, как следует из их названия, — это такие отладчики, которые позволяют отлаживать ядро операционной системы. Они используются главным образом теми, кто пишет (и отлаживает, конечно) драйверы устройств.



    Точки прерывания и пошаговый проход

    Большинство программистов не понимают, что отладчики широко используют точки прерывания "за сценой", чтобы позволить основному отладчику управлять подчиненным. Хотя можно устанавливать точки прерывания не напрямую, отладчик будет их устанавливать, позволяя управлять такими задачами, как пошаговый проход через (stepping over) вызванную функцию. Отладчик также использует точки прерывания, когда необходимо выполнить программу до указанной строки исходного файла и остановиться. Наконец, отладчик устанавливает точки прерывания, чтобы перейти в подчиненный отладчик по команде (например, через выбор пункта меню Debug Break в WDBG).
    >Концепция установки точки прерывания довольно проста. Все, что нужно сделать — это получить адрес памяти, где требуется установить точку прерывания, сохранить код машинной команды (его значение), расположенный в этом месте, и записать по этому адресу инструкцию точки прерывания. В семействе Intel Pentium мнемоника инструкции точки прерывания выглядит как INT з, а код операции — ОхСС, так что нужно сохранить только единственный байт по адресу, где вы устанавливаете точку прерывания. Другие CPU, такие как Intel Merced, имеют иные размеры кода операции, поэтому придется сохранять больше данных по этому адресу.
    В листинге 4-4 показан код функции SetBreakpoint. Читая этот код, имейте в виду, что функции DBG_* принадлежат библиотеке LOCALASSIST.DLL и помогают изолировать различные подпрограммы манипуляции с процессом, облегчая добавление к WDBG функций удаленной отладки. Функция SetBreakpoint иллюстрирует обработку (описанную ранее в этой главе), необходимую для изменения защиты памяти при записи в нее.
    Листинг 4-4. Функция SetBreakepoint из 1386CPUHELP.C
    int CPUHELP_DLLINTERFACE _stdcall
    SetBreakpoint ( PDEBUGPACKET dp ,
    ULONG ulAddr ,
    OPCODE * pOpCode )
    {
    DWORD dwReadWrite = 0;
    BYTE bTempOp = BREAK_OPCODE;
    BOOL bReadMem;
    BOOL bWriteMem;
    BOOL bFlush;
    MEMORY_BASIC_INFORMATION mbi;
    DWORD dwOldProtect;
    ASSERT ( FALSE == IsBadReadPtr ( dp, sizeof ( DEBUGPACKET))) ;

    ASSERT ( FALSE == IsBadWritePtr ( pOpCode, sizeof ( OPCODE)));

    if ( ( TRUE == IsBadReadPtr ( dp, sizeof ( DEBUGPACKET))) ||

    ( TRUE == IsBadWritePtr ( pOpCode, sizeof ( OPCODE))) )

    {

    TRACE0 ( "SetBreakpoint : invalid parameters\n!");

    return ( FALSE);

    }

    // Если текущая операционная система Windows 98 и адрес

    // больше 2 Гбайт, то просто выполните возврат,

    if ( ( FALSE = IsNT ()) && ( ulAddr >= 0x80000000))

    {

    return ( FALSE);

    }

    // Читать код операции по определенному адресу.

    bReadMem = DBG_ReadProcessMemory ( dp->hProcess ,

    (LPCVOID)ulAddr, SbTempOp , sizeof ( BYTE), SdwReadWrite ) ;

    ASSERT ( FALSE != bReadMem);

    ASSERT ( sizeof ( BYTE) = dwReadWrite);

    if ( ( FALSE = bReadMem ) ||

    ( sizeof ( BYTE) != dwReadWrite))

    {

    return ( FALSE);

    }

    // Готова ли эта новая точка прерывания переписать

    // код операции существующей точки прерывания?

    if ( BREAKJDPCODE = bTempOp)

    {

    return ( -1);

    }

    // Получить страничные свойства для подчиненного отладчика.

    DBG_VirtualQueryEx ( dp->hProcess ,

    (LPCVOID)ulAddr,

    &mbi ,

    sizeof ( MEMORY_BASIC_INFORMATION) );

    // Перевести подчиненный отладчик в режим

    // "копирование-при записи" для страниц памяти,

    if ( FALSE == DBG_VirtualProtectEx ( dp->hProcess ,

    mbi.BaseAddress ,

    mbi.RegionSize ,

    PAGE_EXECUTE_READWRITE,

    &mbi.Protect ) )

    {

    ASSERT ( ! "VirtualProtectEx .failed!!");

    return ( FALSE);

    }

    // Сохранить код заменяемой операции.

    *pOpCode = (void*)bTempOp;

    bТеmрОр = BREAK_DPCODE;

    dwReadWrite = 0;

    // Код операции был сохранен, так что теперь

    // нужно установить точку прерывания.

    bWriteMem = DBG_WriteProcessMemory ( dp->hProcess ,

    (LPVOID)ulAddr ,

    (LPVOID)SbTempOp,

    sizeof ( BYTE) ,

    sdwReadWrite );

    ASSERT ( FALSE != bWriteMem);


    ASSERT ( sizeof ( BYTE) == dwReadWrite);

    if ( ( FALSE == bWriteMem ) ||

    ( sizeof ( BYTE) != dwReadWrite))

    {

    return ( FALSE);

    }

    // Вернуть защиту к состоянию, которое предшествовало

    // установке точки прерывания

    // Change the protection back to what it was before

    // I blasted thebreakpoint in.

    VERIFY ( DBG_VirtualProtectEx ( dp->hProcess ,

    mbi. BaseAddress,

    mbi.RegionSize ,

    mbi.Protect ,

    SdwOldProtect ));

    // Сбросить кэш инструкций в случае, если эта память была в кэше CPU

    bFlush = DBG_FlushInstructionCache ( dp->hProcess ,

    (LPCVOID)ulAddr,

    sizeof ( BYTE) );

    ASSERT ( TRUE = bFlush);

    return ( TRUE);

    }

    После установки точки прерывания CPU выполнит ее и сообщит отладчику, что произошло исключение EXCEPTION_BREAKPOINT (0x80000003) — здесь-то и появляется проблема. Если это регулярная точка прерывания, то отладчик определит место ее размещения и покажет его пользователю. После того как пользователь решает .продолжить выполнение, отладчик должен проделать некоторую работу, чтобы восстановить состояние программы. Точка прерывания переписала часть памяти, поэтому если вы, как автор отладчика, просто позволите процессу продолжаться, то выполните неправильную кодовую последовательность, и подчиненный отладчик, вероятно, завершится аварийно. Поэтому следует передвинуть указатель текущей инструкции назад, к адресу точки прерывания и заменить ее кодом операции, который был сохранен при установке этой точки. После восстановления кода операции можно продолжать выполнение.

    Вопрос: как переустанавливать точку прерывания, чтобы иметь возможность повторно останавливаться в этом месте? Если CPU поддерживает пошаговое выполнение, переустановка точки прерывания тривиальна. В пошаговом режиме CPU выполняет единственную инструкцию и генерирует другой тип исключения — EXCEPTION_SINGLE_STEP (0x80000004). К счастью, все CPU, на которых выполняются 32-разрядные Windows, поддерживают пошаговое выполнение.


    Для перехода в режим пошагового выполнения процессоров Intel Pentium требуется установить (в единичное состояние) бит 8 регистра флагов. Справочное руководство Intel называет его битом ловушки — Trap Rag (TF или флагом трассировки). В листинге 4-5 приведена функция Setsingiestep и действия, необходимые для установки бита TF. После замены точки прерывания исходным кодом операции отладчик отмечает в своем внутреннем состоянии, что он ожидает пошагового выполнения, устанавливает в CPU соответствующий режим и затем продолжает процесс.

    Листинг 4-5.Функция SetSingleStep из 1386CPUHELP.C

    BOOL CPUHELP_DLLIMNTERFACE _stdcall

    SetSingleStep ( PDEBUGPACKET dp)

    {

    BOOL bSetContext;

    ASSERT ( FALSE == IsBadReadPtr ( dp, sizeof ( DEBUGPACKET)));

    if ( TRUE = IsBadReadPtr ( dp, sizeof ( DEBUGPACKET)))

    {

    TRACED ( "SetSingleStep : invalid parameters\n!");

    return ( FALSE);

    }

    // Для i386, просто установить TF-бит.

    dp->context.EFlags |= TF_BIT;

    bSetContext = DBG_SetThreadContext ( dp->hThread,

    &dp->context);

    ASSERT ( FALSE != bSetContext);

    return ( bSetContext);

    }

    После того как основной отладчик разблокирует процесс, вызывая функцию ContinueDebugEvent, этот процесс после каждого выполнения отдельной инструкции немедленно генерирует пошаговое исключение. Чтобы удостовериться, что это было ожидаемое пошаговое исключение, отладчик проверяет свое внутреннее состояние. Поскольку отладчик ожидал такое исключение, т "знает", что точка прерывания должна быть переустановлена. На каждом отдельном шаге этого процесса указатель команд продвигается в позицию, предшествующую исходной точке прерывания. Поэтому отладчик может устанавливать код операции точки прерывания обратно в ее исходное положение. Каждый раз, когда происходит исключение типа EXCEPTION_ SINGLE_STEP, операционная система автоматически сбрасывает бит TF, так что нет никакой необходимости сбрасывать его с помощью отладчика. После установки точки прерывания основной отладчик разблокирует подчиненный, и тот продолжает выполняться.


    Всю обработку точки прерывания реализует метод CWDBGProjDOC :: .-andieBreakpoint, который можно найти в файле WDBGPROJDOC.CPP на сопровождающем компакт-диске. Сами точки прерывания определены в файлах BREAKPOINTS и BREAKPOINT.CPP. Эти файлы содержат пару классов, которые обрабатывают точки прерывания различных стилей. Диалоговое окно WDBG Breakpoints позволяет устанавливать точки прерывания при выполнении подчиненного отладчика точно так же, как это делается в отладчике Visual C++. Способность устанавливать точки прерывания "на лету" означает, что необходимо тщательно сохранять след состояния вторичного отладчика и состояния точек прерывания. Подробности обработки включения и выключения точек прерывания в зависимости от состояния подчиненного отладчика можно найти в описании метода CBreakpointsDig::OnOk в файле BREAKPOINTSDLG.CPP на сопровождающем компакт-диске.

    Одно из наиболее изящных свойств, реализованных в WDBG, связано с пунктом меню Debug Break. Речь идет о том, что пока выполняется подчиненный отладчик, можно в любое время быстро войти в основной отладчик.

    Точки прерывания, устанавливаемые при реализации пункта Debug Break, несколько отличаются от тех, что использует WDBG. Такие точки называют одноразовыми (one-shot) точками прерывания, потому что они удаляются сразу же, как только срабатывают. Получение набора таких точек прерывания представляет некоторый интерес. Полное представление можно получить, проанализировав функцию CWDBGProj Doc: : OnDebugBreak из WDBGPROJDOC.CPP, а здесь приведем лишь некоторые поучительные подробности. В листинге 4-6 показана функция CWDBGProj Doc:: OnDebugBreak из WDBGPROJDOC.CPP. Дополнительные сведения об одноразовых точках прерывания приведены далее в разделе "Операции Step Into, Step Over u Step Out" этой главы.

    Листинг 4-5. Обработка Debug Breake в WDBGPROJDOC.CPP

    void CWDBGProjDoc :: OnDebugBreak ()

    {

    ASSERT ( m_vDbgThreads.size () > 0) ;

    // Идея здесь состоит в том, чтобы приостановить все потоки


    // подчиненного отладчика и установить указатель текущей инструкции

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

    // гарантировать, что по крайней мере один из потоков будет

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

    // при которой установка точки прерывания на каждом потоке не будет

    // работать, происходит, когда приложение "висит". Поскольку в

    // обороте нет потоков, точки прерывания никогда не вызываются.

    // Чтобы выполнить работу в такой тупиковой ситуации, я был вынужден

    // использовать следующий алгоритм:'

    // 1. Установить точки прерывания с помощью данной функции.

    // 2. Установить флажок состояния, указывающий, что я ожидаю

    // на точке прерывания Debug Break.

    // 3. Установить фоновый таймер на ожидание точки прерывания.

    // 4. Если одна из точек прерывания исчезает, сбросить таймер.

    // Все хорошо!

    // 5. Если таймер сбрасывается, то приложение "висит".

    // 6. После таймера установить указатель инструкции одного из

    // потоков на другой адрес и поместить точку прерывания по этому

    // адресу.

    // 7. Рестартовать поток.

    // 8. Когда эти специальные точки прерывания сработают, очистить

    // точку прерывания и переустановить указатель команды

    // обратно в первоначальное положение.

    // Повысим приоритет этого потока так,

    // чтобы пройти через установку этих точек прерывания как можно

    // быстрее и предохранить любой поток подчиненного отладчика от

    // планирования.

    HANDLE hThisThread = GetCurrentThread () ;

    int iOldPriority = GetThreadPriority ( hThisThread);

    SetThreadPriority ( hThisThread, THREAD_BASE_PRIORITY_LOWRT);

    HANDLE hProc = GetDebuggeeProcessHandle ();

    DBGTHREADVECT::iterator i; for ( i = m_vDbgThreads.begin ();

    i != m_vDbgThreads.end () ;

    i++ )

    {

    // Приостановить этот поток. Если он уже имеет счетчик

    // приостановок, меня это, на самом деле, не беспокоит. Именно

    // поэтому точки прерывания и устанавливались на каждом потоке

    // подчиненного отладчика.


    Я нахожу активный поток

    //в конечном счете случайно.

    DBG_SuspendThread ( i->m_hThread);

    // Поток приостановлен, можно получить контекст.

    CONTEXT ctx;

    ctx.ContextFlags = CONTEXT_FULL;

    // Поскольку, если используется ASSERT, приоритет этого потока

    // установлен в реальном масштабе времени, и компьютер может

    // "висеть" на панели сообщения, поэтому в if-операторе можно

    // указать ошибку только с помощью оператора трассировки.

    if ( FALSE != DBG_GetThreadContext ( i->m_hThread, &ctx))

    {

    // Найти адрес, который указатель команд собирается

    // выполнить. Это адрес, где будет устанавливаться

    // точка прерывания.

    DWORD dwAddr = ReturnlnstructionPointer ( &ctx);

    COneShotBP cBP;

    // Установить точку прерывания.

    cBP.SetBreakpointLocation ( dwAddr);

    // Активизировать ее.

    if ( TRUE == cBP.ArmBreakpoint ( hProc))

    {

    // Добавить эту точку прерывания к списку Debug Break,

    // только если точка прерывания была успешно

    // активизирована. Подчиненный отладчик легко мог бы

    // иметь множественные потоки, связанные с одной и той же

    // командой, но я хочу установить на этот адрес

    // только одну точку прерывания. m_aDebugBreakBPs.Add ( cBP);

    }

    }

    else

    {

    TRACE ( "GetThreadContext failed! Last Error = Ox%08X\n",

    GetLastError ());

    #ifdef _DEBUG

    // Поскольку функция GetThreadContext потерпела неудачу,

    // вероятно, следует посмотреть, что случилось. Поэтому

    // войдем в отладчик, выполняющий отладку отладчика WDBG.

    // Даже притом, что поток WDBG выполняется на уровне

    // приоритетов реального масштаба времени, вызов DebugBreak

    // немедленно удаляет этот поток из планировщика операционной

    // системы, поэтому его приоритет снижается. DebugBreak ();

    #endif

    }

    }

    // Все потоки имеют установленные точки прерывания. Теперь будем

    // всех их рестартовать и отправлять каждому поточное сообщение.

    // Причина для отправки таких сообщений проста.


    Если подчиненный

    // отладчик прореагирует на сообщения или другую обработку, он будет

    // немедленно прерван. Однако, если он просто простаивает в цикле

    // сообщений, необходимо вынудить его к действию.

    // Поскольку имеется идентификатор (ID) потока, будем просто посылать

    // потоку сообщение WM_NULL. Предполагается, что это простенькое

    // сообщение, так что оно не должно испортить подчиненный отладчик.

    // Если поток не имеет очереди сообщений, эта функция просто потерпит

    // неудачу для такого потока, не причинив никакого вреда,

    for ( i = m_vDbgThreads.begin () ;

    i!= m_vDbgThreads.end () ;

    i++ )

    {

    // Пусть этот поток продолжит выполнение

    //до очередной точки прерывания

    . DBG_ResumeThread ( i->ro_hThread);

    PostThreadMessage ( i->m_dwTID, WM_NULL, 0, 0);

    }

    // Теперь понизить приоритет до старого значения.

    SetThreadPriority ( hThisThread, iOldPriority);

    }

    Для того чтобы остановить подчиненный отладчик, нужно умудриться "втиснуть" точку прерывания в поток команд CPU так, чтобы можно было останавливаться в отладчике. Если поток выполняется, то подобраться к известной точке можно при помощи API-функции suspendThread, приостанавливающей его. Затем, вызвав API-функцию GetThreadContext, определить указатель текущей команды. Имея такой указатель, можно вернуться к установке простых точек прерывания. Установив точку прерывания, нужно вызвать API-функцию ResumeThread, чтобы разрешить потоку продолжать выполнение и сделать так, чтобы он натолкнулся на эту точку.

    Хотя вмешаться в отладчик довольно просто, нужно подумать еще о паре проблем. Первая состоит в том, что ваша точка прерывания может не сработать. Если подчиненный отладчик обрабатывает сообщение или делает некоторую другую работу, он будет прерван. Однако, если подчиненный отладчик, находясь в таком состоянии, ожидает прибытия сообщения, точка прерывания не будет срабатывать, пока подчиненный отладчик не получит сообщение.


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

    Чтобы гарантировать, что подчиненный отладчик достигнет точки прерывания, нужно послать ему сообщение. Если все, что вы имеете, это дескриптор потока, выданный отладочным API, то непонятно, как превратить этот дескриптор в соответствующий дескриптор окна (HWND)? К сожалению, сделать это нельзя. Однако, имея дескриптор потока, всегда можно вызвать функцию PostThreadMessage, которая отправит сообщение в очередь поточных сообщений. Поскольку обработка HWND-сообщения накладывается на вершину очереди поточных сообщений, вызов PostThreadMessage сделает точно то, что нужно.

    Остается понять, какое сообщение следует отправить? Нельзя отправлять сообщение, которое могло бы заставить подчиненный отладчик делать какую-нибудь реальную обработку, разрешая, таким образом, основному отладчику изменять поведение подчиненного отладчика. Например, отправка сообщения WM_CREATE, вероятно, не была бы хорошей идеей. К счастью существует более подходящее сообщение — WM_NULL, которое вы, вероятно, используете как средство отладки при изменении сообщений. Отправка сообщения WM_NULL с помощью PostThreadMessage не приносит никакого вреда, даже если поток не имеет очереди сообщений, а приложение является консольным. Поскольку консольные приложения всегда находятся в состоянии выполнения, даже если ожидают клавишную команду, установка точки прерывания в текущей выполняющейся команде вызовет прерывание.

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


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

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

    Пока алгоритм прерывания многопоточных приложений звучит разумно. Однако, чтобы сделать пункт Debug Break полностью работающим, необходимо решить еще одну, последнюю проблему. Если установлен весь набор точек прерывания во всех потоках и эти потоки возобновляются, то все еще возможна ситуация, в которой не будет происходить прерываний. Устанавливая точки прерывания, вы полагаетесь на выполнение, по крайней мере, одного из потоков, чтобы вызвать исключение точки прерывания. А что случится, если процесс находится в ситуации тупика? Ничего не случится — никакие потоки не выполняются, и ваши тщательно размещенные точки прерывания никогда не вызовут исключения.

    Для того чтобы учесть возможность тупиковой ситуации, нужно установить таймер, отметив момент добавления прерывания. По истечении определенного времени (отладчик Visual C+ использует 3 секунды), нужно предпринять некоторое решительное действие. Когда время пункта Debug Break заканчивается, нужно сначала установить один из указателей поточной команды на другой адрес, затем — точку прерывания в этом новом адресе и рестартовать поток. Когда эти специальные точки прерывания сработают, необходимо сместить указатель поточной команды назад, в его первоначальное положение.В WDBG антитупиковая обработка не реализована, эта возможность оставлена читателям в качеств упражнения в функции GWDBGProjDoc::OnDebugBreak (файл WDBGPROJDOC.CPP на сопровождающем компакт-диске). Полная инфраструктура для управления антитупиковой обработкой расположена там же и, вероятно, потребуется не больше пары часов для ее заполнения. Завершив ее реализацию, вы будете хорошо понимать, как работает WDBG.

    WDBG: что делать дальше?

    После установки отладчик WDBG работает так, как было задумано. Однако его можно улучшить по многим направлениям. Ниже перечислены некоторые идеи усовершенствования WDBG. Если вы займетесь расширением WDBG, сообщите мне об этом. Кроме того, как уже говорилось в главе!, замечательной иллюстрацией профессиональных способностей программиста являются примеры собственного кода (весьма полезные, скажем, и для рабочих интервью). Если вы добавляете какое-нибудь существенное свойство к WDBG, то нужно показать это!
  • Можно заняться интерфейсом пользователя (UI) WDBG-отладчика. Первое усовершенствование, которое можно осуществить — улучшить реализацию этого интерфейса. Интерфейс уже содержит всю необходимую информацию; вы должны только спроектировать лучшие способы ее представления.
  • Сам WDBG поддерживает только простые, позиционные точки прерывания (location breakpoints). С помощью BREAKPOINT.H и BREAKPOINT.CPP можно добавить интересные дополнительные виды точек прерывания, такие как точки прерывания со счетчиком пропусков (skip count breakpoints) или точки прерывания выражений (expression breakpoints), для которых прерывание происходит, только если помеченное ими выражение истинно. Удостоверьтесь, что вы получаете новые точки прерывания с помощью функции CLocationBp (благодаря которой вы получаете се-\риализованный код и не должны ничего изменять в WDBG).
  • Вы должны быть способны без больших усилий расширить WDBG, чтобы поддержать отладку множественных процессов (multiple process debug->ging). Большинство интерфейсов построены для работы по схеме идентификации процесса, так что нужно только проследить, с каким процессом вы работаете во время уведомления об отладке.
  • Интерфейс WDBG построен так, чтобы позволить быстро перейти к удаленной отладке и различным CPU, оставив работу главной части интерфейса примерно такой же. Напишите динамические библиотеки удаленной отладки и расширьте WDBG так, чтобы позволить пользователю выбирать, где выполнять отладку: на местной или на удаленной машине.
  • Наконец, чтобы сделать WDBG-отладчик действительно полезным, вы всегда можете написать лучший дизассемблер и символьную машину для отладочных символов формата С7!


  • WDBG: реальный отладчик

    Мне кажется, лучший способ показать, как работает отладчик — написать его, что и сделано в этом разделе. Хотя WDBG не может заменять отладчик Visual C++, но он, конечно, умеет многое, что полагается уметь отладчику. На рис. 4.2 показан WDBG, отлаживающий программу Microsoft Word. На рисунке Word остановлен в точке прерывания, которую я установил на функции GetProcAddress. Окно Memory, в верхнем правом углу, показывает второй параметр, который Word пересылает данному экземпляру GetProcAddress (строка PhevCreateFilelnfo). Рис. 4.2 демонстрирует большую часть возможностей этого отладчика, включая показ регистров, просмотр стеков вызова и кода дизассемблера, показ загруженных модулей и выполняющихся потоков. Кроме того, WDBG поддерживает точки прерывания, перечисление символов и прерывание приложений для остановки отладчика (эти возможности не видны на рис. 4.2, но станут очевидными при первом же запуске WDBG).
    WDBG: реальный отладчик
    Рис. 4.2. Отладчик WDBG в действии
    В целом, WDBG — хороший образец отладчика. Однако, глядя на интерфейс пользователя (UI) в WDBG, можно заметить, что я не потратил много времени на создание интерфейса пользователя. Фактически, все окна в WDBG построены по стандарту многодокументного интерфейса (Multiple-Document Interface — MDI) и относятся к редактируемым элементам управления. Это было сделано умышленно: я сохранил простой интерфейс пользователя, потому что не хотел, чтобы его детали отвлекали вас от сущности кода отладчика. Пользовательский интерфейс WDBG написан с использованием библиотеки классов MFC, поэтому попытки улучшить интерфейс не должны вызвать затруднений.
    Прежде чем приступать к изучению специфических особенностей отладки, рассмотрим WDBG подробнее. В табл. 4.1 описаны все главные подсистемы WDBG. Одним из моих намерений при создании WDBG было определение нейтрального интерфейса между пользовательским интерфейсом и циклом отладки. Для того чтобы WDBG.EXE поддерживал удаленную отладку через сеть, следовало бы с помощью нейтрального интерфейса просто заменить локальные отладочные DLL на сетевые.

    Таблица 4.1. Главные подсистемы WDBG

    Подсистема

    Описание

    WDBG.EXE

    Содержит весь Ul-код. Дополнительно он заботится об обработке всех точек прерывания. Большая часть этих действий отладчика запрограммирована в файле WDBGPROJDOC.CPP

    LOCALDEBUG.DLL

    Реализует цикл отладки. Поскольку я хотел обеспечить повторное использование этого отладочного цикла, код пользователя (в данном случае это WDBG.EXE) передает в цикл отладки С++-класс, производный от класса CDebugBaseUser (который определен в DEBUGINTERFACE.H). Когда происходит какое-нибудь отладочное событие, цикл отладки вызыва- " ется в этот класс. За всю синхронизацию ответственны классы пользователей. Файлы WDBGUSER.H и WDBGUSER.CPP содержат координирующий класс для WDBG.EXE. WDBG.EXE использует простую синхронизацию (типа SendMessage). Другими словами, поток отладки посылает сообщение Ш-потоку и блокируется, пока не произойдет возврат из Ill-потока. Если отладочное событие требует ввода пользователя, поток отладки блокируется после посылки сообщения о событии синхронизации. Как только Ill-поток начинает обработку команды Go, он устанавливает событие синхронизации, и поток отладки снова начинает выполняться

    LOCALASSIST.DLL

    Этот простой модуль только оболочка API-функций, манипулирующих с памятью подчиненного отладчика и регистрами. Используя интерфейс, определенный в этом модуле, WDBG.EXE и I386CPUHELP.DLL могут немедленно перейти к управлению удаленной отладкой, просто заменив этот модуль на сетевой

    I386CPUHELP.DLL

    Это вспомогательный модуль для процессора IA32 (Pentium). Хотя этот модуль специфичен для процессоров Pentium, его интерфейс, определенный в CPUHELP.H, не зависит от CPU. Если бы вы захотели пренести WDBG на другой процессор, то это единственный модуль, который пришлось бы заменить. Дизассемблер в этом модуле взят из программы Dr. Watson, которая поставляется в составе Platform SDK. Хотя дизассемблер работает, но он нуждается в обновлении, чтобы поддержать последние варианты CPU Pentium



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

    Быстрое прерывание на произвольной функции

    При установке точек прерывания существует много интересных возможностей, связанных со способностью отладчика Visual C++ оценивать выражения. Если известно имя функции, на которой нужно выполнить прерывание, то вместо утомительного поиска по всему исходному коду достаточно ввести это имя в редактируемое поле Break at. Если в одном из загруженных модулей существует соответствующий символ, то отладчик поместит точку прерывания на первую инструкцию этой функции. Если программа остановлена в отладчике, а имя введенной функции неправильно, то отладчик выведет панель с сообщением. При вводе имени С++-функции нужно вводить также и квалификатор соответствующего класса. Например, чтобы организовать прерывание на методе Опок класса coiaiog из библиотеки MFC, следует ввести coialog: :0nOk.
    Отладчик достаточно развит, чтобы учитывать перегруженные (overloaded) члены классов, и он подсказывает имена конкретных версий перегруженных функций в специальном окне. Например, если для установки точки прерывания в MFC-приложении ввести в поле Break at имя cstring: :cstring как имя функции, на которой следует установить точку прерывания, то отладчик не будет знать, какая версия конструктора класса cstring интересует программиста и подскажет их в списке Symbols диалогового окна Resolve Ambiguity, показанного на рис. 5.2. В этом окне перечисляются (в расширенном синтаксисе точек прерывания) восемь конструкторов cstring, из которых и нужно выбрать подходящую версию.
    Быстрое прерывание на произвольной функции
    Рис. 5.2. Диалоговое окно Resolve Ambiguity
    Самый легкий способ устанавливать точки прерывания на сложных функциях, таких как operator-функции классов, состоит в том, чтобы напечатать достаточно информации для показа в диалоговом окне Resolve Ambiguity. Например, MFC-класс cstring имеет перегруженные операторы назначения, и чтобы увидеть их список в окне Resolve Ambiguity, нужно напечатать в поле Break at cstring: :operator= (operator-функция класса cstring). Чтобы выполнить прямую установку точки прерывания, в поле Break at можно также указать параметры функции (если они известны). Например, вместо CString: :operator= можно напечатать CString: :operator=(const char *) И вполне обойтись без диалогового окна Resolve Ambiguity.



    Команда Set Next Statement

    Одним из самых известных скрытых свойств отладчика является команда Set Next Statement. Она доступна как в окне исходного кода, так и в окне Disassembly — по контекстному меню (открываемому щелчком правой кнопки мыши), но только тогда, когда выполняется отладка. Команда Set Next Statement позволяет изменять указатель инструкции (команды), т. е. устанавливать его на различные места программы. То же самое можно сделать и прямой установкой регистра EIP. Изменение точки выполнения программы — фантастическая отладочная техника, особенно если во время блочного тестирования требуется протестировать собственные обработчики ошибок.
    Имейте в виду, если не проявить чрезвычайную осторожность, то изменение указателя инструкции может легко привести к аварийному завершению программы. Если программа выполняется в режиме отладки, то использование Set Next Statement не приводит к большим неприятностям. Однако в оптимизированном режиме финальной сборки безопаснее всего выполнять команду Set Next Statement только в окне Disassembly. Компилятор будет перемещать код так, что исходные строки могут и не выполняться линейно. Кроме того, нужно знать, создает ли ваш код временные переменные в стеке во время использования Set Next Statement. В главе 6 последняя ситуация рассмотрена более подробно.
    Если гипотеза состоит в том, что обнаруженная ошибка могла быть в некоторой ветви кода, надо установить точку прерывания в отладчике перед подозрительной функцией или функциями. Затем проверить данные и параметры, входящие в функции и выполнить пошаговый проход через эти функции (step over). Если проблема не дублируется, выполните команду Set Next Statement, чтобы вернуть точку выполнения назад, к точке прерывания, и измените данные, входящие в функции. Эта тактика позволит протестировать несколько гипотез в одной отладочной сессии, экономя таким образом время. Нетрудно видеть, что невозможно применять эту технику во всех случаях, потому что, как только вы выполняете некоторый код в своей программе, его повторное выполнение может нарушить ее состояние. Set Next Statement лучше всего работает на коде, который не слишком сильно изменяет свое состояние.

    Как упоминалось ранее, команда Set Next Statement удобна во время блочного тестирования. Например, она полезна, когда требуется тестировать обработчики ошибок. Скажем, в if-операторе необходимо проверить, что случится, если условие не выполнится. Все, что нужно сделать в этом случае, — это позволить условию выполниться и использовать команду Set Next Statement, чтобы переместить точку выполнения вниз, до ветви с отказом. В дополнение к Set Next Statement, одноразовую (one-shot) точку прерывания позволяет устанавливать пункт меню Run To Cursor, доступный также в меню Debug. При тестировании я также изредка использую Run To Cursor. Превосходный пример применения Set Next Statement для тестировании или отладке — заполнение структур данных (особенно списков и массивов). Пусть в некотором коде заполняется структура данных и затем эта структура добавляется к связному списку. Чтобы добавить к этому списку несколько дополнительных элементов, можно выполнить Set Next Statement (так что можно будет видеть, как выполняется обработка). Такое использование Set Next Statement особенно удобно, когда при отладке нужно установить режим трудно дублируемых данных.



    Модификаторы позиционных точек прерывания

    Мы рассмотрели вопросы правильного размещения экспортируемых функций, и теперь читатель должен уверенно устанавливать позиционную (location) точку прерывания в любом месте приложения. Роль позиционных точек прерывания в отладочном процессе довольно значительна, но, как было указано в начале данной главы, к ним можно также добавить некоторый "интеллект" с целью повышения эффективности работы отладчика. Носителями этих "интеллектуальных" возможностей являются специальные модификаторы позиционных точек прерывания: счетчики пропусков, условные выражения и изменения переменных.
    Счетчики пропусков
    Самый простой модификатор позиционной точки прерывания (Tll) — это счетчик пропусков (skip count). Он указывает отладчику, что нужно вставить точку прерывания, но пропускать ее выполнение (т. е. не приостанавливать приложение в этой точке) указанное количество раз. С помощью этого модификатора легко решается проблема прерывания на нужной итерации цикла.
    Добавлять счетчик пропусков к простой точке прерывания довольно легко. Сначала установите обычную позиционную точку прерывания и раскройте диалоговое окно Breakpoints. Выделите позиционную точку прерывания в списке Breakpoints, и щелкните кнопку Condition. Затем в нижнем редактируемом поле диалогового окна Breakpoint Condition введите нужное число пропусков точки прерывания.
    Когда отладчик, наконец, остановит выполнение приложения в такой точке, он сообщит, сколько раз выполнялся помеченный им оператор. Если есть цикл, в котором имеет место аварийный останов, но неизвестно, на какой итерации это происходит, то нужно добавить позиционную точку прерывания со счетчиком пропусков к строке какого-нибудь оператора внутри цикла. Значение счетчика пропусков должно быть больше, чем общее число итераций цикла. Когда программа завершится аварийно, откройте диалоговое окно Breakpoints и посмотрите на точку прерывания, обозначенную в списке Breakpoints1. После строки описателя точки прерывания располагается остаток счетчика пропусков (в круглых скобках).
    Знание различных псевдорегистров, которые обеспечивают доступ к регистровым и специальным значениям, а также творческий подход — вот два секрета применения условных выражений. Например, хотя отладчик Visual C++ не имеет явного метода для установки позиционной точки прерывания, которая срабатывает только в определенном потоке под Windows 2000, но если установить выражение @TIВ=линейный адрес TIB, то прерывание произойдет только на указанном потоке. Первый шаг заключается в том, чтобы ввести псевдорегистр @ТIB в окно Watch и найти линейный адрес информационного блока потока, на котором требуется выполнить прерывание. Чтобы активизировать поток, который надо проверить, можно использовать диалоговое окно Threads отладчика. Если, например, поток содержит в псевдорегистре @Т1В адрес, равный Ox7FFDEOOO, то выражение выглядит так: @TIB == Ox7FFDEOOO. Для Windows 98 нужно просмотреть регистр FS, который является уникальным для каждого потока, и задать выражение @FS == значение конкретного потока (thread specific value).

    Для того чтобы выполнить прерывание, основанное на конкретном коде последней ошибки (last error code), можно использовать псевдорегистр @ERR. Например, чтобы выполнить прерывание после API-вызова, который, согласно документации Platform SDK, может сгенерировать последнюю ошибку ERROR_FILE_NOT_FOUND, выражение должно выглядеть так: @ERR==2. Числовое значение ошибки с идентификатором ERROR_FILE_NOT_FOUND можно найти в файле WINERROR.H. Все псевдорегистры перечислены в табл. 5.1.

    Наконец, из-за того что нельзя вызывать функции в таких выражениях, прерывание на строке с конкретным (строчным) значением затруднительно.

    TIB — Thread Information Block, информационный блок потока. — Пер.

    В этом случае просто установите выражение, которое проверяет каждый символ, например, так:

    (szBuff[0] = 'Р')-&& (szBuff [l] = 'a'j&& (SzBuff [2] == 'm').

    Другой способ использования условных выражений в прерываниях: можно комбинировать их со счетчиком пропусков.


    Такой прием позволяет выполнить прерывание, когда n-ое значение выражения станет равно true (где п — исходное значение счетчика пропусков).

    Таблица 5.1. Выражения и псевдорегистры окна Watch

    Псевдорегистр

    Описание

    ©ERR

    Значение последней ошибки; то же значение возвращается API-функцией GetLastError

    ©TIB

    Информационный блок текущего потока; необходим, потому что отладчик не обрабатывает формат FS:0

    ©CLK

    Недокументированный регистр часов; используется только в окне Watch

    @ЕАХ, @ЕВХ, ©ЁСХ, @EDX, ©ESI, ©EDI, ©EIP, ©ESP, ©EBP, ©EFL

    Регистры Intel CPU

    @CS, @DS, @ES, @SS, @FS, ©GS

    Сегментные регистры Intel CPU

    ©STO, ©ST1, @ST2, ©ST3, ©ST4, ©ST5, ©ST6, ©ST7

    Регистры чисел с плавающей точкой Intel CPU

    Изменения переменных

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

    Добавление этого модификатора происходит точно так же, как добавление других модификаторов позиционных точек прерывания — с использованием диалогового окна Breakpoint Condition для установки необходимых условных параметров. Единственное недоразумение может возникнуть из-за того, что редактируемое поле (Enter the expression to be evaluated), в которое вводится наблюдаемая переменная, является одновременно и полем для ввода условного выражения.


    В среднем редактируемом поле диалогового окна Breakpoint Condition (с именем Enter the number of elements to watch in an array or structure) пользователь сообщает отладчику, сколько элементов массива или области памяти надо наблюдать. Если значение, на котором требуется выполнить прерывание, является разыменованием указателя, например, *pMyData, то в это поле нужно ввести число наблюдаемых байт.

    Чтобы открыть эту панель, нужно установить на подходящем исходном операторе простую позиционную точку прерывания и, не изменяя позиции курсора ввода, ввести команду Edit|Breakpoints, Затем в открывшемся диалоговом окне Breakpoints ввести в поле Break at вкладки Location номер строки (после символа "точка"), в которой установлена данная точка прерывания (например, .47), и нажать кнопку Condition (под полем Break at). — Пер.

    Если же ввести в поле Enter the expression to be evaluated сам указатель pMyData (без звездочки), то прерывание произойдет, когда изменится сам указатель, указывающий на другую область памяти. — Пер.

    Окно Watch

    Окно Watch занимает высокое место в списке важных свойств отладчика Visual C++. Что делает окно Watch чрезвычайно популярным — так это его разносторонность. Можно по-разному работать с этим окном, собирая информацию о приложении. Самая впечатляющая возможность заключается в том, что оно позволяет легко изменять" значение переменной, редактируя его в правой стороне окна. Вспомните обсуждение синтаксиса для выражений точек прерывания. В окне Watch используется такая же оценка выражений, поэтому и синтаксис расширенных точек прерывания, и правила составления выражений, и псевдорегистры могут использоваться и в окне Watch.
    Форматирование данных и оценка выражений
    Чтобы приобрести опыт манипулирования окном Watch, необходимо запомнить форматирующие символы, приведенные в табл. 5.3 и 5.4, которые получены из документации Visual C++ на MSDN. Форматный символ записывают через запятую после переменной. Наиболее полезный спецификатор формата для СОМ-программирования — hr. Если разместить в окне Watch выражение @ЕАХ,hr, то при переходе через вызов СОМ-метода можно увидеть результат вызова в понятной форме (@ЕАХ — это регистр Intel CPU, в котором хранятся возвращаемые значения). Применение форматных спецификаторов позволяет легко управлять видом данных и экономить огромное количество времени на их интерпретацию.
    Таблица 5.3. Символы форматирования для переменных окна Watch
    Символ

    Описание формата

    Пример

    Вид на экране

    d, i

    Десятичное целое со знаком

    (int)OxFOOOF065,d

    -268373915

    u

    Десятичное целое без знака

    0x0065, u

    101

    о

    Восьмеричное целое без знака

    OxF065,o

    0170145

    х, X

    Шестнадцатеричное целое

    61541, X

    OxOOOOF065

    1 , h

    Префикс long или short для d, i, u, о, х, X

    0x00406042, hx

    OxOc22

    £

    Плавающая точка со знаком

    3./2.,f

    1.500000

    е

    Научная нотация со знаком

    3./2,е

    1.500000e+000

    g

    Плавающая точка или научная нотация со знаком, в зависимости от того, какой формат короче

    3./2,g

    1.5

    с

    Одиночный символ

    0x0065, с

    'e1

    s

    Строка

    szHiWorld, s

    "Hello world"

    su

    Строка Unicode

    szWHiWorld, su

    "Hello world"

    st

    Строка Unicode или ANSI, в зависимости от установки в AUTOEXP.DAT





    hr

    HRESULT или Win32 код ошибки

    0x00000000, hr

    S_OK

    we

    .Флажок класса Windows

    0x00000040, we

    WC_
    DEFAULTCHAR (Заметим, что хотя этот формат и докуме
    нтирован он не работает в Visual C++ 6)

    wm

    Номера Windows-сообщений

    0x0010, wm

    WM_CLOSE


    Таблица 5.4. Символы форматирования для дампов памяти окна Watch

    Символ

    Описание формата

    Пример

    Вид на экране



    64 ASCII символы

    Ox0012ffac,ma

    0x001 2ffac .4.0.".OW&.

    .1W&.0.:W.1 .."1.JO&.12.

    ."1..0y.1

    m

    16 байт (шестнад-цатеричных), за которыми следует 16 ASCII символов

    Ox0012ffac,m

    0x001 2ffac

    ЬЗ 34 cb 00 84 30 94 80 ff 22 8а 30 57 26 00 00 .4...0...".OW&..

    rob

    16 байт (шестнад-цатеричных), за которыми следует 16 ASCII символов

    Ox0012ffac,mb

    0x001 2ffac ЬЗ 34 cb 00 84 30 94 80 ff 22 8a 30 57 26 00 00 A..O...".OW&..

    mw

    8 слов

    Ox0012ffac,mw

    0x001 2ffac 34b3 OOcb 3084 8094 22ff 308a 2657 0000

    md

    4 двойных слова

    Ox0012ffac,md

    0x001 2ffac ООсЬ34ЬЗ 80943084 308a22ff 00002657

    mq

    4 четверных слова

    Ox0012ffac,mq

    0x001 2ffac 8094308400cb34b3 00002657308a22ff

    mu

    2-байтовые символы (Unicode)

    Ox0012ffac,mu

    0x001 2ffac

    34ЬЗ OOcb 3084 8094 22ff 308a 2657 0000 7 99797

    #

    (Не документирован). Расширяет указатель (на область памяти) на указанное число значений

    pCharArray, 10

    Расширенный массив из 10 символов (использующий расширители + /-)

    Недокументированный спецификатор формата позволяет расширить область памяти, указателя на указанное число значений. Если имеется указатель на массив из 10 длинных целых чисел (long int), окно Watch покажет только первое значение. Чтобы видеть весь массив, поместите за переменной количество значений, .которое хотите наблюдать. Например, указатель с расширителем (pLong, 10) покажет расширяемый массив из 10 элементов.


    Если имеется большой массив, то можно сместить указатель к его середине, а в расширении указать то количество значений, которое требуется отобразить. Например, переменная с форматом (pBigArray+100) ,20 показывает в окне Watch 20 элементов со смещением 99. При этом имеется ошибка: значения индекса всегда начинаются с 0, независимо от позиции первого отображенного элемента от начала массива. В примере с pBigArray первый индекс, показанный как.0, является 100-м элементом массива, второй — 101-м элементом и т. д.

    Кроме широких возможностей форматирования данных по желанию разработчика, окно Watch позволяет выполнять приведение типов переменных и показывать их в любой нужной ему форме. Например, чтобы получить смещения указателя, можно использовать выражения BY, WO и DW. Разрешены также адресная операция (&) и операция указателя (*). Обе они позволяют получать значения адресов памяти и наблюдать результаты операций приведения типов. В окне Watch можно явно указывать контекст переменной, если применять спецификаторы контекста, описанные выше в разделе "Синтаксис расширенных точек прерывания и позиционные точки прерывания" этой главы. Наконец, все форматы и спецификаторы, используемые в окне Watch, работают также и в окне QuickWatch.

    Нетрудно заметить, что окно Watch предоставляет гораздо больше возможностей, чем простой просмотр статических переменных. Фактически, это средство оценки выражений, в котором можно проверять любые условные операторы. Во время тестирования элементов своих программ я очень широко использую окно Watch для проверки операторов if и других условных операторов. Для этого необходимо поместить в окно Watch индивидуальные переменные условного оператора, а вслед за ними — сам условный оператор. Это позволяет видеть как значения каждой переменной, так и результаты условной оценки. Для того чтобы изменить оценку условного оператора, можно сначала изменять значения индивидуальных переменных и прослеживать вывод в окне Watch. Из-за того что окно Watch так хорошо управляет выражениями, программисту не нужно для несложных вычислений открывать программу Калькулятор.


    Простые вычисления можно выполнять прямо в окне Watch.

    Таймирование кода в окне Watch

    Еще один изящный прием — применение окна Watch для наблюдения за временем выполнения участков программы. В качестве элементарного таймера может служить недокументированный псевдорегистр @CLK. Во многих случаях нужно только грубое представление временного интервала между двумя точками программы, и @CLK помогает уточнить время выполнения между двумя точками прерывания. Имейте в виду, что это время включает накладные расходы отладчика. Весь фокус заключается в том, чтобы ввести (в окно Watch) два элемента наблюдения @CLK: первый — просто "@CLK" и второй — "@CLK=0". Второй элемент сбросит таймер (установит на 0) после того, как выполнение возобновится. Поскольку время измеряется в микросекундах, а я предпочитаю — в миллисекундах, то устанавливаю первый @CLK в формате @CLK/1000,d. Форматный символ ,d введен для того, чтобы показать таймер в виде десятичного числа, если в окне Watch предварительно установлен шестнадцатеричный формат отображения (Hexadecimal Display). Хотя таймер @CLK и недостаточно совершенен, но для приближенных подсчетов он вполне пригоден.

    Вызов функций в окне Watch

    И последнее: окно Watch наделено способностью выполнять функции внутри отладчика. Можно задать вопрос: "А зачем это нужно?" А для того, чтобы полностью настроить отладочную среду на требования пользователя. Например, вместо десятиминутного просмотра 10 различных структур данных (чтобы убедиться в их однородности), можно написать специальную функцию, которая проверяет данные, и затем вызывать ее прямо из окна Watch, когда это необходимо (например, когда отладчик останавливает приложение).

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


    При вызове функции в окне Watch ей можно передавать параметры, а это позволяет создавать функции-шаблоны, работающие на различных типах данных. Окно Watch можно представлять себе как ограниченное окно Immediate из IDE Microsoft Visual Basic.

    Если отладочная функция не имеет параметров, не забывайте использовать при ее вызове круглые скобки с пустым списком параметров, чтобы указать отладчику Visual C++, что вызывается функция, а просматривается значение переменной. Например, если речь идет об отладочной функции void MyMemCheck (), надо вызвать ее в окне Watch в формате MyMemCheck (). Если же отладочная функция получает параметры, просто передайте их ей, как будто вызываете обычную функцию1. Если отладочная функция возвращает значение, то оно будет выведено в правой части окна Watch.

    При вызове отладочных функций в окне Watch вы встретитесь с рядом ограничений. Эти ограничения не вызывают никаких трудностей, если придерживаться нескольких правил. Первое правило: пока функция находится в окне Watch, она может выполняться только в однопоточном контексте. Если имеется многопоточная программа, необходимо ввести отладочную функцию в окно Watch, проверить результаты и затем немедленно удалить ее из окна. Если отладочная функция выполнится в другом потоке (отличающемся от первого), то второй поток немедленно завершится. Второе правило: отладочная функция должна выполняться меньше чем за 20 секунд; если за это время она выдаст исключение, то отладчик завершит всю программу. Последнее правило: такая функция должна только читать память и проверять данные. Если возникает проблема, просто вызовите функцию OutputDebugstring или printf. Причем неизвестно, что может случиться, если программист начнет изменять память или вызывать API-функции Windows.

    То есть укажите в круглых скобках вслед за именем функции список соответствующих аргументов. — Пер.

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


    Это случается при следующих условиях:

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

    Автоматическое расширение собственных типов

    Хотя документация Visual C++ только упоминает эту тему, программист может создавать собственные типы, автоматически расширяемые в окне Watch, так же, как и в окнах QuickWatch и DataTips. Вы, вероятно, видели некоторые общие типы, такие как cobject и RECT, расширенные в окне Watch. Так вот, можно легко организовать дело таким образом, чтобы выгоду из расширяемости окна Watch извлекали ваши собственные типы. Весь фокус заключается в текстовом файле AUTOEXP.DAT из подкаталога Microsoft Visual Studio\Common\MSDev98\Bin. Просто добавьте в конец файла вход для своих собственных типов.


    В качестве примера рассмотрим добавление входа с авторасширением для структуры PROCESS_INFORMATION, которая посылается в API-функцию createProcess. Первый шаг состоит в проверке того, что отладчик Visual C++ распознает в качестве типа. В примере программы переменная PROCESS_INFORMATION помещена в окно Watch, на ней выполнен щелчок правой кнопкой мыши и выбран пункт Properties контекстного меню. В диалоговом окне Program Variable Properties в качестве метки (имени) типа указан идентификатор _PROCESS_INFORMATION, который, если посмотреть на определение структуры, приведенное ниже, соответствует метке структуры.

    typedef struct _PROCESS_INFORMATION {

    HANDLE hProcess;

    HANDLE hThread;

    DWORD dwProcessId;

    DWORD dwThreadld;

    } PROCESS_INFORMATION

    Документация в AUTOEXP.DAT говорит, что формат для авторасширяемого входа таков:

    тип = [текст]<член[,формат]>....

    В табл. 5.5 показаны значения каждого поля этого формата. Обратите внимание, что в авторасширении можно показать несколько членов.

    Таблица 5.5. Входы авторасширений в AUTOEXP.DAT

    Поле

    Описание

    Тип

    Имя типа. Для шаблонных типов, за этим полем может следовать поле со звездочкой "<*>", чтобы охватить все производные типы

    Текст

    Любой литеральный текст. Эта поле, в общем случае, есть имя члена или его короткая версия

    Член

    Фактический член данных, который будет показан в окне Watch. Этим полем может быть выражение (так, если нужно добавить некоторые смещения к различным указателям, то в вычисление можно включить смещения). Работают также операции приведения типов

    Формат

    Дополнительные спецификаторы формата для членов-переменных. Это те же спецификаторы, что и форматирующие символы, показанные в табл. 5.3

    Если с помощью структуры PROCESS_INFORMATION требуется просмотреть значения hProcess и hThread, правило авторасширения должно выглядеть так:

    _PROCESS_INFORMATION=hProcess= hThread=

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

    В файле AUTOEXP.DAT можно увидеть один специальный форматирующий код— <,t>. Этот код просит отладчик разместить в качестве имени типа имя максимального (по уровню) производного типа. Например, если имеется базовый класс А с производным классом В, и только А имеет правило авторасширения, то авторасширением для переменной типа В будет имя класса В, за которым следует правило авторасширения для класса А. Формат <, t> очень полезен для прямой поддержки классов.

    Отладка компилированного кода Visual Basic

    Отладка компилированного кода Visual Basic включает проверку и отладку приложения в форме р-кода (перед его компиляцией). Отладка компилированного кода Visual Basic не столь проста, как отладка С-кода, потому что отладочная информация, которую генерирует Visual Basic, не содержит достаточных сведений о типах. Следовательно, отладчик не может расшифровывать различные объекты. Прежде чем обращаться к отладчику Visual C++, нужно использовать инструмент независимого поставщика, такой как программа SmartCheck фирмы Compuware NuMega, потому что это намного облегчает отладку в Visual Basic. Программа SmartCheck знает, как конвертировать сложные сообщения об ошибках Visual Basic в такие условия, которые точно выводят на проблему, так что не нужно использовать отладчик Visual C++. SmartCheck может также воспроизводить полный поток приложения Visual Вазютак, что пользователь может видеть, как выполняется его программа. В случае с компилированным кодом можно упростить отладку, дополнительно использовав условно компилированные вызовы функции Outputoebugstring. Во многих случаях оказалось легче отлаживать компилированный Visual Basic код на уровне языка ассемблера. Допускаю, что это звучит дико, но это действительно так. Материал главы 6 поможет читателю совершенствоваться в навыках работы на языке ассемблера.
    Чтобы приготовить файлы Visual Basic к отладке, нужно сначала (во время компиляции) сгенерировать PDB-файлы. Можно установить этот режим на вкладке Compile диалогового окна Project Properties. На ней также устанавливается режим компилятора No Optimizations. Только не забудьте возвратиться в режим оптимизации, когда начнете построение коммерческой версии.
    Для того чтобы можно было видеть локальные переменные стандартного типа в окне Variables Visual C++, перейдите на вкладку Locals. Visual Basic использует много временных переменных, поэтому на вкладке показано много переменных типа unnamed_vari. Если прокрутить экран вниз к основанию окна, то можно увидеть локальные переменные.

    Начав работать с Visual Basic, я был озадачен, когда получил случайное исключение "Floating-point inexact result" (Неточный результат операции с плавающей точкой). Программа не выполняла никаких действий с плавающей точкой, так что я понятия не имел, откуда могло взяться это сообщение. Проделав некоторую поисковую работу, я обнаружил, что в Visual Basic реализована собственная версия структурированной обработки исключений (SEH). К сожалению, она использует в качестве одного из исключений значение EXCEPTION_FLT_INEXACT_RESULT, и когда исключение не обработано, появляется ложное сообщение об исключении.

    Одна уловка, которую используют некоторое мои коллеги, особенно когда они тестируют компилированный элемент управления ActiveX, состоит в выполнении полной среды Visual Basic под отладчиком Visual C++. Этот прием позволяет отлаживать элемент управления ActiveX и отводит программе на р-коде роль тестового приспособления. Способность переключаться между тестовой программой на р-коде и компилированным компонентом позволяет легче видеть обе стороны проблемы.

    Расширенные точки прерывания

    Установка точки прерывания на исходной строке в отладчике Visual C++ (для проектной конфигурации Win32 Debug или Win32 Unicode Debug) довольно проста: загрузите исходный файл, поместите курсор на строку, где требуется остановить выполнение, переместите указатель мыши на кнопку Insert/Remove Breakpoint и щелкните левой кнопкой мыши.
    Эта кнопка находится в панелях команд Buid и BuildMiniBar Visual C++. — Пер
    В терминах отладчика этот процесс называется установкой или размещением точки прерывания в определенной позиции1 (строке) исходного или дизассемблерного кода. Во время отладочного прогона отладчик дойдет до этой точки, выполнит операцию прерывания и остановит дальнейшее выполнение программы.
    На заре компьютеризации точек прерывания просто не существовало. Единственная "стратегия" для обнаружения ошибок состояла в том, чтобы выполнять программу до тех пор, пока она не завершится аварийно, и затем, страница за страницей, пробираться через распечатки шестнадцатеричных дампов памяти, отыскивая проблему. Единственными отладчиками в те мрачные для отладки времена были операторы трассировки и вера в Бога. В эпоху отладочного Ренессанса, наступившую после появления языков более высокого уровня, разработчики смогли устанавливать точки прерывания, но должны были выполнять отладку только на уровне языка ассемблера. Языки более высокого уровня еще не имели никаких средств для просмотра локальных переменных или наблюдения за программой в исходной форме. Эра современной отладки началась тогда, когда языки программирования развились в более сложные инструменты, а разработчики стали устанавливать точки прерывания на строках исходного кода и просматривать переменные на дисплее, который интерпретировал значения переменных в точном соответствии с их типом. Такие простые позиционные точки прерывания (location breakpoint) до сих пор остаются чрезвычайно мощным средством отладки: они позволяют решить 99,46 процентов отладочных проблем.
    Тем не менее, работа с такого рода точками прерывания очень быстро может стать утомительной.
    Что будет, если установить точку прерывания на строке внутри цикла for, который выполняется 10 000 раз, а ошибка обнаружится на последней итерации? Во-первых, устанут руки (придется 10 000 раз выполнить команду Go), а во-вторых, нахождение ошибочной итерации займет много времени. Было бы хорошо иметь некоторый способ сообщить отладчику, что точка прерывания не должна срабатывать 9 999 раз перед остановкой.

    В связи с этим точки прерывания подобного типа будем называть позиционными точками прерывания (location breakpoint). Это самый простой из нескольких типов прерываний, с которыми работает Microsoft Visual С. — Пер.

    То есть расположенные в определенной позиции исходного кода. — Пер.

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

  • пропустить точку прерывания (перед ее выполнением) определенное количество раз;
  • выполнить прерывание, когда отмеченное этой точкой выражение примет значение true;
  • выполнить прерывание, когда изменится переменная или адрес памяти.
  • Расширенные возможности точек прерывания, в конце концов, серьезно усовершенствовали отладчики, позволяя разработчикам за считанные минуты делать то, на что при использовании простых позиционных точек прерывания тратились часы.

    Синтаксис расширенных точек прерывания и позиционные точки прерывания

    Перед обсуждением возможностей расширенных точек прерывания потратим немного времени на рассмотрение их синтаксиса. Это важно, потому что в диалоговом окне Breakpoints показан список описателей вставленных в код точек прерывания в соответствующем формате. К счастью, синтаксис таких описателей довольно прост. В листинге 5-1 (раздел "Точки прерывания глобальных выражений и условные точки прерывания" данной главы) приведена программа AdvancedBP, которая демонстрирует каждый тип расширенных точек прерывания. Проект этой программы находится на сопровождающем компакт-диске, его можно открыть в Visual Studio и обращаться к нему при работе с этим разделом.
    Синтаксис описателя расширенной точки прерывания состоит из двух частей. В первой части размещается набор строк в определенном формате (контекст), а во второй — условие, задающее позицию точки прерывания в исходном коде (обычно номер строки), а также выражение, переменную или Windows-сообщение, с которыми связывается это прерывание. Контекст можно представлять себе примерно как область действия (scope) идентификатора переменной при программировании. Контекст указывает отладчику точное место расположения точки прерывания.
    В полном формате контекста перечисляются (через запятую в указанном порядке) имена функции, исходного файла и двоичного модуля:
    {[функция], [исходный файл], [двоичный модуль]}
    Загрузочный или исполняемый модуль приложения.. Все элементы контекста не обязательны. При пропусках элементов запятые сохраняются. — Пер.
    Чтобы установить точку прерывания, нужно определить достаточное количество контекстной информации. В простой позиционной точке прерывания отладчику надо только имя исходного файла. Читателю, вероятно, знаком простой формат позиционной точки прерывания в диалоговом окне Breakpoints. Если установить такую точку прерывания на строке, например, 20 файла TEST.CPP, то в диалоговом окне Breakpoints будет описана так:
    {,TEST.CPP,}.20
    См. список Breakpoints в нижней части окна Breakpoints, открываемого командой Edit|Breakpoints.
    — Пер.
    Возможность указать контекст для позиционной точки прерывания позволяет решать особенно неприятный тип проблем отладки. Рассмотрим следующий случай: исходный файл с диагностической функцией CheckMyMem используется двумя библиотеками динамической компоновки — A.DLL и B.DLL и включен в них с помощью статической компоновки. Если соблюдаются принципы профилактического программирования, то имеют место многократные вызовы этой функции из обеих DLL. Однако только при вызове из В.DLL часто происходят случайные аварийные остановы. Если установить стандартную позиционную точку прерывания в исходном коде функции checkMyMem (ее синтаксис в этом случае выглядит как {,CHECKMYMEM.CPP, } .27), то она будет срабатывать в обеих DLL, даже если вы хотите, чтобы вызовы делались только из В.DLL. Если нужно, чтобы точка прерывания срабатывала только в B.DLL, то придется использовать контекст точки прерывания в следующей форме: {, CHECKMYMEM.CPP, в.DLL}.27. Синтаксис контекста можно ввести с клавиатуры непосредственно в редактируемое поле Break At на вкладке Location диалогового окна Breakpoints (открываемого командой Edit|Breakpoints или нажатием клавиш + +) отладчика Visual C++; однако легче использовать диалоговое окно Advanced Breakpoint, показанное на рис. 5.1. Для его открытия нажмите кнопку со стрелкой справа от редактируемого поля Break at (в панели Breakpoints), чтобы раскрыть меню. В этом меню выберите пункт Advanced и затем в поля группы Context окна Advanced Breakpoint введите информацию для контекстной части синтаксиса точки прерывания.
    Реальная мощь расширенных точек прерывания сосредоточена во второй части их синтаксиса, где указывается расположение (позиция) точки прерывания в исходном коде, выражение, переменная или Windows-сообщение, связанные с точкой прерывания. Синтаксис позиционных точек прерывания довольно прост -г- после фигурных скобок контекста указывается десятичный номер строки, на которой располагается точка прерывания в исходном коде (номер отделяется от контекста десятичной точкой).Однако можно выполнять прерывание с помощью других типов позиций. Например, если необходимо выполнить прерывание по известному абсолютному адресу (а не по номеру строки), то нужно просто ввести этот адрес в поле Break at диалогового окна Breakpoints. Например, при написании данного раздела использовалась упомянутая выше программа AdvancedBP. Точка входа данного приложения (mainCRTStartup) расположена по адресу 0x401210 — это и есть абсолютный адрес, который задавался для точки прерывания. Напомним, что при вводе шестнадцатеричного адреса нужно указывать префикс Ох.
    Синтаксис расширенных точек прерывания и позиционные точки прерывания
    Рис. 5.1. Спецификация контекста точки прерывания в диалоговом окне Advanced Breakpoint

    Советы и специальные приемы

    Рассмотрим некоторые специальные приемы, которые позволяют собирать как можно больше информации об отлаживаемом приложении.


    Точки прерывания глобальных выражений и условные точки прерывания

    До этого момента мы говорили о единственном типе точек прерывания — о позиционных точках прерывания (location breakpoints) и их модификациях. При рассмотрении синтаксиса точек прерывания упоминались три других типа таких точек (в дополнение к позиционным): точки прерывания выражений (expression breakpoints), точки прерывания переменных (variable breakpoints) и точки прерывания Windows-сообщений (Windows message breakpoints). Точки прерывания выражений и переменных подобны соответствующим модификаторам позиционных точек прерывания, за исключением того, что по области своего действия (scope) они глобальны. Причем для них действуют те же правила, что и для модифицированных позиционных точек. На latel CPU оба этих типа точек прерывания будут пытаться использовать аппаратную точку прерывания через один из специальных отладочных регистров CPU.
    Отладочные регистры контролируют адрес и 1, 2 или 4 байта памяти по этому адресу. Если ТП, связанная с выражениями или изменениями данных, конструируется таким образом, чтобы отладчик мог сохранять их в одном из отладочных регистров, то программа сможет выполняться с высокой скоростью до тех пор, пока не произойдут изменения в соответствующем

    Точки прерывания на системных или экспортируемых функциях

    Размещение точки прерывания на первой инструкции функции — очень мощная техника. Однако попытка установить точку прерывания на функции, которую программа импортирует из DLL, ни к чему не приведет. Со стороны отладчика здесь все в порядке, нужно только дать ему некоторую контекстную информацию о том, где он может найти функцию. Кроме того, важна еще одна небольшая деталь: имя функции зависит от того, загружены ли отладочные символы DLL. Имейте в виду, что точку прерывания на системных DLL-функциях можно устанавливать только в Microsoft Windows 2000. Недостаток защиты для "копирования-при-записи" (обсуждавшийся в главе 4) и есть та причина, по которой нельзя устанавливать точки прерывания на системных функциях Windows 98, которые загружены выше 2 Гбайтной границы памяти. Чтобы заставить эту технику работать в Windows 2000, нужно использовать формат COFF (см. главу 4) и включить в отладчике загрузку экспорта. Для этого, работая в IDE Visual C++, убедитесь, что на вкладке Debug диалогового окна Options (открываемого командой Tools|Options) установлен флажок Load COFF & Exports.
    Чтобы показать, как устанавливаются точки прерывания на системной DLL, установим такую точку на функцию LoadLibrary из KERNEL32.DLL. Поскольку уже известно, как нужно устанавливать контекст для позиционной точки прерывания, то ясно, что первая часть контекста записывается как {,,KERNEL32.DLL} и идентифицирует модуль функции. Отладчик Visual C++ придерживается иерархического подхода к символической информации, при котором более полные наборы символов имеют приоритет над менее полными. Так, файлы программных баз данных (PDB), которые включают всю возможную информацию — номера исходных строк, имена функций, переменных и типов1, всегда имеют приоритет над COFF/DBG-файлами, которые содержат только символические имена общих (public) функций2. COFF/DBG-файлы имеют приоритет над экспортируемыми именами, которые являются разновидностью псевдосимволов. Для того чтобы подтвердить, что отладчик загружает символы для DLL, нужно контролировать вкладку Debug окна Output.
    Если в окне Output выводится сообщение "Loaded symbols for 'DLL name" (Загружены символы для 'имя DLL'), значит имеются полные символы для этой DLL. Наоборот, если выводится "Loaded 'DLL name, no matching symbolic information found" (Загружена 'имя DLL', соответствующая символьная информация не найдена) или "Loaded exports for 'DLL name " (Загружены эксперты для 'имя DLL"), значит символы не были загружены.

    Здесь обсуждается тема отладочных символов, поэтому вспомним, что отладочные символы Windows 2000 нужно всегда устанавливать. Хотя они и не помогут реализовать полную обратную разработку операционной системы, потому что содержат символические имена только для общих (public) компонентов. Но если символы загружены, то вы, по крайней мере, будете видеть, в каких функциях вы находитесь при просмотре стека или окна Disassembly. Причем запомните, нужно обновлять символы операционной системы каждый раз, когда устанавливается очередной пакет обслуживания (Service Pack) операционной системы. Отладочные символы для Windows 2000 находятся на компакт-диске Customer Support Diagnostics (Диагностика поддержки клиента). Visual Studio для Windows NT 4 включает программу Windows NT Symbols Setup, которая устанавливает соответствующие символы.

    Это и есть полный набор символов. — Пер.

    И поэтому содержат неполный набор символов. — Пер.

    Окно Output открывает в нижней части окна Microsoft Visual C++ команда VievjOutput или +<2>. - Пер.

    Если отладочные символы не загружены, то в качестве строки для установки точки прерывания нужно использовать имя, экспортированное из DLL. Можно проверить это имя, запустив утилиту DUMPBIN с ключом /EXPORTS:

    DOMPBIN /EXPORTS DLL-Name

    Если вы выполните DUMPBIN для KERNEL32.DLL, то увидите не функцию LoadLibrary, а две функции с похожими именами — LoadLibraryA и LoadLibraryw. Суффиксы указывают на набор применяемых данной функцией символов: суффикс А происходит от ANSI (American National Standards Institute), a w — от wide (или Unicode).


    Windows 2000 использует кодировку Unicode для интернационализации. Если программа компилировалась с параметром UNICODE, то нужно выбрать версию LoadLibraryw, а если нет, то LoadLibraryA. Однако функция LoadLibraryA это просто оболочка, которая распределяет память для конвертирования ANSI-строки в Unicode и вызывает LoadLibraryw, поэтому технически можно использовать также и LoadLibraryw. Если наверняка известно, что программа собирается вызывать только одну из этих функций, то можно просто установить точку прерывания на этой функции. Если такой уверенности нет, установите точки прерывания сразу на двух функциях. Если символы не загружены, то синтаксис точки прерывания для прерывания на функции LoadLibrary выглядит либо так: (,,KERNEL32.DLL}LoadLibraryA, либо так: {,,KERNEL32.DLL)LoadLibraryw.

    Если приложение ориентировано только на Windows 2000, то нужно везде использовать Unicode. Это обеспечит значительное повышение производительности. Мэтт Пьетрек в декабрьской (за 1997 год) колонке "Under the Hood" в Microsoft Systems Journal сообщил, что ANSI-оболочки обеспечивают значительный рост производительности. Кроме того, поддержка Unicode облегчает интернационализацию программы.

    Если символы загружены, то необходимо выполнить некоторые вычисления, чтобы согласовать декорированные имена символов. Для этого нужно только знать соглашение о вызове экспортируемой функции и прототип функции. Подробности соглашений о вызовах приведены в главе 6. Ниже показан прототип функции LoadLibrary из WINBASE.H с некоторыми макросами, расширенными для ясности:

    _declspec (dllimport)

    HMODULE

    _stdcall

    LoadLibraryA(

    LPCSTR IpLibFileName

    );

    Макрос WINBASEAPI расширяется в стандартное соглашение о вызовах _stdcall, которое, между прочим, является соглашением о вызовах для всех системных API-функций. Стандартные вызовы функций декорируются префиксом с символом подчеркивания и суффиксом с символом "@", за которым следует число байт, помещенных в стек.


    К счастью, вычислить это число довольно просто: оно равно сумме байт, отводимых в стеке для всех параметров. Для семейства CPU Intel Pentium нужно просто сосчитать число параметров и умножить его на 4. Для функции LoadLibrary, которая имеет один параметр, окончательное декорированное имя выгладит так: _LoadLibraryA@4. Вот еще два примера, которые показывают, как должны выглядеть окончательные декорированные имена функций:

  • _createProcessA040 — для функции CreateProcess, которая имеет 10 параметров;
  • _TlsAiioc@o — для функции TisAiioc, которая не имеет параметров.
  • Даже если функция не имеет параметров, нужно придерживаться формата "@#" (как мы только что видели, декорированное имя функции TISAIIOC имеет вид maiito:_TlsAiioc@o). Когда символы не загружаются, ANSI- и Unicode-условия все еще применимы. Если символы загружены, синтаксис прерывания на LoadLibrary выглядит или так: {, ,KERNEL32.DLL}_LoadLibraryA@4,

    Точки прерывания Windows-сообщений

    Точки прерывания Windows-сообщений (Windows message breakpoints) останавливают отладочный прогон приложения, когда оконная процедура этого приложения получает определенное Windows-сообщение. Точки прерывания этого типа устанавливаются на вкладке Messages диалогового окна Breakpoints (рис. 5.5). Для программ, разработанных средствами SDK1 языка С, установка этой точки прерывания достаточно проста, потому что когда выполняется подчиненный отладчик, раскрывающийся список поля Break at WndProc уже содержит имя нужной оконной процедуры. Для установки таких ТП нужно:
    1. Выбрать из списка Break at WndProc оконную процедуру.
    2. Выбрать из списка Set one breakpoint for each message to watch идентификатор того сообщения, при получении которого должно произойти прерывание.
    3. Нажать кнопку ОК.
    Если в списке Set one breakpoint for each message to watch нет подходящего идентификатора, его следует ввести в это поле вручную.
    В настоящее время в Windows-программировании широко используются библиотеки С++-классов, такие как MFC, позволяющие напрямую работать с точками прерывания Windows-сообщений. Если установить ТП этого типа на главной оконной процедуре MFC: {, ,MFC42D.DLL}AfxWndProc, то она будет работать. Однако такая установка позволяет останавливать приложение только на сообщениях главного окна приложения. Если нужно выполнять прерывания на частных Windows-сообщениях, обрабатываемых в оконных процедурах дочерних оконных классов (производных от класса cwnd), то придется перенести эту ТП из AfxWndProc на оконную процедуру дочернего окна.
    1 SDK (Software Development Kit) — инструментальная система разработки программ. — Пер.
    Если нужно выполнить прерывание на общем сообщении, то лучше использовать условную позиционную точку прерывания, устанавливая ее в MFC-методе cwnd: :WindowProc. Для этого сначала нужно найти значение this-ука-зателя интересующего вас класса, а затем отыскать в файле WINUSER.H значение Windows-сообщения, на котором требуется остановить программу. С помощью этих двух значений нужно установить позиционную точку прерывания в cwnd: :WindowProc.
    При написании данного раздела автор отлаживал программы пакета Visual C++ 6.0 Service Pack 3, и чтобы выполнять прерывание на сообщении WM_PAINT только от своего класса, он установил расширенную позиционную точку прерывания со следующим описателем:

    {,WINCORE.CPP,}.1584 when (this=Ox0012FE74)&&(message==OxF)

    Точки прерывания Windows-сообщений
    Рис. 5.5. Вкладка Messages для установки точек прерывания от Windows-сообщений

    При использовании точки прерывания с выражением "this. ..&&.. .message" нужно проявлять осторожность, потому что указатель this, вероятно, изменяется в зависимости от способа выделения памяти вашему классу и от изменений, вносимых в отлаживаемый код. Если же необходимо быстро установить точку прерывания, то всегда можно добавить метод обработки (с помощью мастера классов Class Wizard) и установить на нем простую (не расширенную) позиционную точку прерывания.

    В этой главе много внимания уделено расширенным точкам прерывания. В листинге программы AdvancedBP показаны все типы точек прерывания, кроме прерываний от Windows-сообщений. Синтаксис точек прерывания каждого типа показан в виде строчных комментариев в исходном коде программы (что позволяет видеть соответствующие описатели без открытия диалогового окна Breakpoints). Настоятельно советую всем разработчикам попрактиковаться в установке расширенных точек прерывания в собственных программах. Поразительно, как много полезного можно узнать при этом и об отладчике, и о собственном проекте!

    Общий вопрос отладки

    Почему точки прерывания исчезают или перескакивают с места на место?

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

    One or more breakpoints are not positioned on valid lines.


    These breakpoints have been moved to the next valid line.

    (Одна или несколько точек прерывания не были позиционированы на правильной строке. Эти точки прерывания перемещены к следующей правильной строке).

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

    Если отладчик вообще не может установить точку прерывания, вы получите сообщение:

    One or more breakpoints cannot be set and have been disabled. Execution will stop at the beginning of the program.

    (Одна или несколько точек прерывания не могут быть установлены и были отключены. Выполнение остановится в начале программы.)

    Нажав кнопку ОК, чтобы закрыть панель сообщения, нужно открыть диалоговое окно Breakpoints и отыскать точки прерывания со сброшенными флажками. Они-то и доставили неприятности отладчику.

    Наиболее общей причиной отказа в установке точек прерывания является попытка установить точку прерывания в явно загруженную DLL — т. е. в DLL, загруженную явным обращением к API-функции LoadLibrary. Все DLL-файлы модели компонентных объектов (СОМ) загружаются явно, так что эта проблема возникает с раздражающей частотой. Отладчик должен установить все точки прерывания еще при запуске, поэтому в начале отладки нужно будет вручную добавить к списку дополнительных DLL явно загруженные DLL (чтобы заставить отладчик загрузить символы этих DLL). В диалоговом окне Project Settings на вкладке Debug выберите пункт Additional DLLs в списке Category. В списке Modules добавьте все DLL, которые проект может когда-либо загружать.

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

    Эту панель можно открыть командой Project|Settings из Microsoft Visual C++.— Пер.

    Удаленная отладка

    Теперь, когда читатель хорошо осведомлен относительно нюансов расширенных точек прерывания, обратимся к одной из скрытых возможностей отладчика Visual C++ — удаленной отладке. Удаленная отладка означает, что ваша программа и крошечная отладочная заглушка выполняются на одной (удаленной) машине, а отладчик — на другой (локальной) машине, причем связь поддерживается через протоколы TCP/IP1. Хотя с помощью отладчика Visual C++ можно многое сделать и на одиночной машине, иногда разумнее выбрать двухмашинный (удаленный) вариант отладки. Удаленную отладку полезно применять, столкнувшись с любой из следующих ситуаций (которые могут сложиться под влиянием отладчика с полным графическим интерфейсом пользователя — GUI-отладчика):
  • при отладке критического кода активизации окна;
  • при отладке графических программ;
  • если нужно освободить как можно больше памяти для приложения;
  • при выполнении отладки в Windows 98;
  • если программист не хочет радикально изменять конфигурацию машины, устанавливая полную интегрированную среду разработки Visual C++.
  • Самая плохая ситуация — первая (отладка критического кода активизации окна). На отдельной машине пошаговая отладка через Windows-сообщение WM_SETFOCUS и аналогичные процессы невозможна, потому что в пошаговом режиме программа теряет фокус отладчика. GUI-отладчики это, конечно, великолепно, но принцип неопределенности Гейзенберга справедлив и применительно к отладчикам! Перемещение отладчика на отдельную машину позволяет как отладчику, так и вашей программе иметь свой собственный фокус, аккуратно обходя эту проблему.
    Вторая ситуация, в которой удаленная отладка может дать значительную экономию времени — когда нужно отлаживать код графической программы. Хотя в простых случаях в системе с единственным монитором можно разделить отладчик и соответствующее приложение по разным углам экрана, бывают ситуации, когда отладчик нужно отправить на другую машину. Если приложению постоянно необходима большая часть экрана, или речь идет об отладке игрового приложения, использующего технологию Microsoft DirectX2, то без удаленной отладки не обойтись.

    TCP/IP — Transmission Control Protocol/Internet Protocol (Протокол Управления Передачей/Протокол Интернета). — Пер.

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

    Третья ситуация — более тонкая и заслуживает отдельного внимания. Полный GUI-отладчик занимает значительное количество ресурсов. Если вам нужны вся мощь и комфорт отладчика Visual C++, значит приложение должно отказаться от некоторой части памяти, чтобы отладчик мог выполняться. В разработках реальных приложений отладочные построения могут достигать огромных размеров. Удаленная отладка позволит освободить часть памяти для выполнения приложения. Удаленная отладка особенно полезна, когда речь идет о тестировании на машинах с минимальными ресурсами.

    Четвертая ситуация, в которой уместна удаленная отладка: при отладке программ, выполняющихся под управлением операционной системы Windows 98. Удаленная отладка позволяет отлаживать программы одной операционной системы, находясь в другой операционной системе. Кроме того, оказалось что отладчик Visual C++ лучше работает в Windows 2000, поэтому следует предпочесть Windows 2000 на локальной машине, где работает отладчик, a Windows 98 — на удаленной, где находится отлаживаемое приложение.

    Наконец, заключительная ситуация: отладка выполняется на машине, на которой у приложения, по вашему мнению, могут возникнуть проблемы, связанные с версией системных DLL. Установка полной IDE для Visual C++ на рассматриваемой системе может привести к обновлению различных системных DLL, таким образом уничтожая шанс дублирования и исправления ошибки. Решить проблему можно копированием всего нескольких файлов, необходимых для работы удаленной части отладчика, на удаленную машину.

    Подробное объяснение методики удаленной отладки приводится в документации Visual Studio. Желающие могут обратиться к теме библиотеки MSDN "Debugging Remote Applications" (Отладка удаленных приложений).


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

    Таблица 5.2. Файлы, необходимые для удаленной отладки, и их размещение в Visual C++

    Файлы

    Размещение в установленном Visual C++

    MSVCMON.EXE, TLNOT.DLL, DM.DLL, MSDIS110.DLL

    \MSDEV98\BIN

    MSVCRT.DLL, MSVCP60.DLL

    %SYSTEMROOT%\System32

    PSAPI.DLL (только Windows 2000)

    %SYSTEMROOT%\System32

    Для успешной удаленной отладки нужно, во-первых, установить две системы так, чтобы локальная машина, на которой работает отладчик, могла отыскивать те же двоичные файлы, которые выполняет удаленная машина. Как минимум, нужно иметь общий (shared) доступ ко всем дисководам удаленной машины, потому что локальная машина будет нуждаться в доступе к согласованным двоичным файлам. Размещение отлаживаемой программы зависит от ее сложности: она может размещаться на локальной машине, а выполняться на удаленной; она может находиться на обеих машинах или только на удаленной машине. Если это стандартная ЕХЕ-программа, то удаленная машина может, вероятно, выполнять ее вне локальной машины. Однако в современном мире новейших СОМ-технологий иногда непросто принять решение о том, где следует выполнять программу. Лично я размещал программу на идентичных дисках и каталогах на обеих машинах. Во-вторых, прежде чем начать удаленную отладку, следует гарантировать, что имеется полностью и правильно установленная программа, готовая к выполнению на удаленной машине (куда бы вы ни решили поместить ее).

    MSVCMON.EXE — это отладочная заглушка, которая выполняется на удаленной машине. Она имеет кнопку Settings, которая должна обеспечивать возможность задания сетевого имени локальной машины, но эта функция не действует, т. к. MSVCMON.EXE почему-то игнорирует установки.


    Запустив MSVCMON.EXE, убедитесь, что ваш отладочный проект установлен правильно на локальной машине. Введите полный путь и имя двоичного (исполняемого) файла удаленной машины в редактируемое поле Remote Executable Path and File Name на вкладке Debug диалогового окна Project Settings. Когда вы заставляете локальный отладчик выполнять отладку удаленно, он фактически не стартует процесс отладки. Его стартует MSVCMON.EXE, поэтому необходимо сообщить этой программе, где следует искать двоичный файл.

    После того как проект установлен и нажата кнопка Go, отладка, в значительной степени, выполняется так же, как обычно. Единственное отличие состоит в том, что локальный отладчик будет иногда подсказывать расположение согласованных DLL (вот почему необходимо разделение всех дисков удаленного компьютера — всегда должна быть возможность найти согласованные двоичные файлы, чтобы при встрече с какой-либо проблемой можно было взглянуть на ее источник). Хотя специальный флажок позволяет отключить согласование DLL, делать это не следует. Проблема состоит в том, что раз вы выключили согласование DLL, не существует никакого способа включить его обратно иначе, как удалив ОРТ-файл проекта. Кроме того, в ОРТ-файле проекта сохраняются пути к удаленно согласованным двоичным файлам, так что если позже вы захотите присоединиться к другой машине для удаленной отладки того же проекта, то единственным способом переустановки положения двоичного файла будет удаление и ОРТ-файла проекта. Имейте в виду, что удаление этого файла приведет к потере всех точек прерывания и компоновок экрана.

    Можно комбинировать приложение дистанционного управления, например, pcAnywhere фирмы Symantec, с удаленной отладкой, получая возможность отлаживать свое приложение на машине пользователя. Мне приходилось делать это в корпоративных сетях интранет, но не в Интернете. Конечно, это может привести к некоторым проблемам безопасности, но все же неплохо иметь такое средство в своем арсенале. Вполне возможна ситуация, при которой машины пользователя и разработчика находятся на разных континентах, а проблему надо устранить в один день!

    И, наконец, два последних не очень существенных замечания относительно удаленной отладки. Во-первых, закончив удаленную отладку, не забудьте установить в IDE Visual C++ режим локальной отладки. Установка удаленной отладки глобальна для всех проектов, хотя, по моему мнению, это должна быть раздельная установка по проектам. Во-вторых, если речь идет о сетевом приложении или о системе, которая чувствительна к сетевому трафику, то удаленная отладка использует TCP/IP, поэтому отладчик может помешать вам. В любой из этих ситуаций следует предпочесть отладчик WinDBG, который поддерживает удаленную отладку, используя последовательные порты и нуль-модемный кабель.

    Условии или в данных. Если же

    на вкладке Data диалогового окна Breakpoints) фактические адреса выражений и данных. В программе AdvancedBP, показанной в листинге 5-1, требовалось установить точку прерывания глобального выражения, которая должна срабатывать, если "первый символ в имени глобальной переменной g_szGiobai изменится на "G". Для этого пришлось найти адрес переменной g_szciobai (он оказался равным 0x00404594) и установить (в поле Enter the expression to be evaluated на вкладке Data диалогового окна Breakpoints) точку прерывания выражения в формате:
    Mchar*) Ox00404594='G'
    Однако при пошаговом просмотре каждой инструкции будет показано неправильное выражение:
    WO (0x00404594) =='G'
    Мне так и не удалось найти подходящей точки прерывания глобального выражения, потому что очень трудно подыскать такое выражение, которое примет отладчик. Намного удобнее в этом случае воспользоваться точкой прерывания глобальной переменной.
    /*00З*/ ttinclude
    /*004*/ void LocBPFunc ( void)
    /*005*/ { // {,AdvancedBP.cpp,}.6
    /*006V printf ( "Hello from LocBPFuncW) ;
    /*007*/ }
    /*008*/ void SkipLocBPFunc ( void)
    /*009*/ { // {,AdvancedBP.cpp,}.12 skip 99 times(s)
    /*010*/ for ( int i = 0; i < 100; i++)
    /*011*/ {
    /*012*/ printf ( "SkipLocBPFunc iteration = %d\n", i);
    /*013*/ }
    /*014*/ }
    /*015*/ void ExprTrueLocBP ( void)
    /*016*/ { // {,AdvancedBP.cpp, } .20 when 'j=8'
    /*017*/ int j = 0;
    /*018*/ for ( int i = 0; i < 10; i++)
    /*019*/ {
    /*020*/ j = i * 2;
    /*021*/ }
    /*022*/ }
    /*023*/ void DataChangeLocBP ( void)
    /*024*/.{ // {,AdvancedBP.cpp,}.26 когда изменяется szBuff[5](length:1)
    /*025*/ char szBuff[ 10 ];
    /*026*/ strcpy ( szBuff, "String!");
    /*027*/ }
    /*028*/ char g_szGlobal[ 10 ];
    /*029*/ int g_ilnt = 0;
    /*030*/ void main ( void)
    /*031*/ { // 0x401210 -> ТП-адрес точки входа в jmainCRTStartup
    /*032*/ LocBPFunc ();
    /*0ЗЗ*/ SkipLocBPFunc ();
    /*034*/ ExprTrueLocBP ();
    /*035*/ DataChangeLocBP ();
    /*036*/
    /*037*/ //{,,KERNEL32.DLL}_LoadLibraryA@4 <- с отладочными символами

    /*038*/ //{,,KERNEL32.DLL}LoadLibrary <- без отладочных символов

    /*039*/ LoadLibrary ( "KERNEL32.DLL");

    /*040*/

    /*041*/ // (char)Ox00404594=='G' <- ТП глобального выражения.

    /*042*/ strcpy ( g_szGlobal, "Global!");

    /*043*/

    /*044*/ // (long)Ох4045АО <- ТП глобальной переменной.

    /*045*/ g_ilnt = 0x42;

    /*046*/

    /*047*/ printf ( "Done!\n");

    На рис. 5. 4 приведено диалоговое окно команды Edit|Breakpoints, в нижней части которого показан список Breakpoints, содержащий описания всех установленных в программе AdvancedBP точек прерывания. Флажки в левой части каждого описателя позволяют управлять активностью соответствующих ТП: включение флажка активизирует ТП, а выключение деактивизирует ее (что вынуждает отладчик не выполнять останов программы на этой ТП во время отладочного прохода).

    Подобно точке прерывания глобального выражения, точка прерывания глобальной переменной работает наилучшим образом, если при ее установке задать шестнадцатеричный адрес, выполнить явное приведение типа адреса к длинному указателю и указать число элементов наблюдения равным 1. Когда содержимое памяти по этому адресу изменяется, отладчик останавливается. Например, в программе AdvancedBP можно модифицировать выражение *(char*)Ox00404594=='G' так, чтобы прерывание происходило тогда, когда содержимое ячейки памяти по этому адресу изменяется (а не просто содержит код символа "G"). Для этого нужно выполнить приведение адреса к типу long, Т.е. установить ТП по адресу "* (long*) (0x00404594) " (СМ.комментарий в строке 044). Чтобы задействовать отладочные регистры, в общем случае требуется одна или две попытки установить такие ТП с правильными выражениями. Еще раз отметим, что при приведении адреса к типу long не следует устанавливать (в поле Enter the number of elements to watch in an array or structure на вкладке Data диалогового окна Breakpoints) число элементов наблюдения больше 1. Значение в этом поле указывает, что контролируется запись в область памяти размером в двойное слово (DWORD), а из-за того что аппаратные регистры отладки не могут обрабатывать ссылки, превосходящие по размеру двойное слово, устанавливается пошаговый метод проверки памяти.


    Условии или в данных. Если же
    Рис. 5.4. Список описателей точек прерывания программы AdvancedBP с флажками активизации/деактивизации ТП

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

    Точки прерывания глобальных переменных — превосходное средство для поиска наиболее сложных видов ошибок, связанных с неконтролируемой записью в память (wild writes). Как указывалось в главе 2, это происходит, когда при записи в память используется неинициализированный указатель, что вызывает непредсказуемую модификацию данных в памяти.

    Чтобы открыть эту панель, нужно установить на подходящем исходном операторе простую позиционную точку прерывания и, не изменяя позиции курсора ввода, ввести команду Edit|Breakpoints, Затем в открывшемся диалоговом окне Breakpoints ввести в поле Break at вкладки Location номер строки (после символа "точка"), в которой установлена данная точка прерывания (например, .47), и нажать кнопку Condition (под полем Break at). — Пер.

    Если же ввести в поле Enter the expression to be evaluated сам указатель pMyData (без звездочки), то прерывание произойдет, когда изменится сам указатель, указывающий на другую область памяти. — Пер.

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

    Установка точек прерывания

    Может показаться, что о точках прерывания уже все рассказано, но добавим следующее. Каждый знает, что можно размещать точки прерывания на строке исходного кода и в окне Disassembly, но знаете ли вы, что допускается также установка их в окне Call Stack (т. е. в стеке вызовов)? Когда нужно выйти из глубоко вложенной цепочки вызовов, то установка точки прерывания в окне Call Stack — очень полезная техника. Для установки точки прерывания в окне Call Stack выполните правый щелчок по функции, на которой требуется приостановить приложение, и в раскрывшемся контекстном меню выберите пункт Insert/Remove Breakpoint (останов выполнится сразу же, как только произойдет возврат к этой функции).
    Вместо удаления точек прерывания после тяжелого сеанса отладки, лучше просто выключить их. Выключать точки прерывания можно либо щелкнув правой кнопкой мыши на каждой из них в окне исходного кода и выбрав команду Disable Breakpoint, либо перейдя в диалоговое окно Breakpoints и выключая флажки рядом с точками прерывания в списке Breakpoints в нижней части этой панели. Оставляйте точки прерывания в коде, чтобы обеспечить быстрый возврат приложения к состоянию, в котором оно находилось, когда возник сбой. Конечно, надо обновлять любые точки прерывания, которые могли измениться, перед тем как начать новый сеанс отладки. После | проверки решения можно удалить все точки прерывания.
    Для гарантированного получения всего набора точек прерывания нажмите кнопку Step Into, чтобы загрузить и запустить подчиненный отладчик прежде, чем будете устанавливать любые точки прерывания, отличающиеся от простых позиционных точек. Отладчик может проверять точки прерывания и показывать диалоговое окно Resolve Ambiguity только тогда, когда активен подчиненный отладчик.



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

    Циклы


  • LOOP цикл по счетчику ЕСХ
  • Нельзя слишком сосредотачиваться на инструкции LOOP, потому что компиляторы Microsoft генерируют их не так много. Однако в некоторых частях ядра операционной системы (которые выглядят так, как если бы разработчики Microsoft написали их на языке ассемблера) их иногда можно увидеть. Применять инструкцию LOOP довольно просто. Установите ЕСХ равным числу шагов цикла, и затем выполните блок кода. Сразу после блока кода разместите инструкцию LOOP. Если ЕСХ не равен нулю, то она выполнит декремент ЕСХ и затем перейдет к вершине блока. Когда ЕСХ достигает нуля, инструкция LOOP пропускается (и выполняется следующая за ней инструкция).
    Большинство циклов, представленных ниже, являются комбинациями условных и абсолютных переходов. Во многих отношениях эти циклы выглядят как оператор if, код которого рассмотрен в предыдущем фрагменте, за исключением того, что основанием if-блока является инструкция JMP, выполняющая обратный переход к вершине блока. Следующий пример содержит код цикла.
    void LoopingExample ( int q )
    {
    // С-код:
    // for ( ; q < 10 ; q++ )
    // {
    // printf ( "q = %d\n" , q ) ;
    // }
    char szEmt[] = "q = %d\n" ;
    _asm
    {
    JMP LE_CompareStep // При первом проходе выполнить
    // прямую проверку 10.
    LE_IncrementStep:
    INC q // Инкремент q.
    LE_CompareStep:
    CMP q , OAh // Сравнить q с 10.
    JGE LE_End // Если q >= 10, эта функция выполнена.
    MOV ЕСХ , DWORD PTR [q] // Переместить значение q в ЕСХ.
    PUSH ЕСХ // Поместить значение в стек.
    LEA ЕСХ , szFmt // Получить форматную строку.
    PUSH ЕСХ // Поместить форматную строку в стек.
    CALL DWORD PTR [printf] // Напечатать текущую итерацию.
    ADD ESP , 8 // Очистить стек.
    JMP LE_IncrementStep // Инкремент q, и начать сначала.
    LE_End: // Цикл выполнен.
    }
    }


    Дополнительные инструкции

    Инструкции, описанные в этом разделе, выполняют манипуляции сданными и указателями, сравнение и проверку, переходы и ветвления, циклы и манипуляции со строками.


    Доступ через регистр FS

    Регистру FS в операционных системах Win32 отведена специальная роль: в нем хранится указатель на блок информации потока (Thread Information Block — TIB). TIB называют также блоком среды потока (Thread Environment Block — ТЕВ). TIB содержит все специфические данные, которые позволяют операционной системе выполнять прямой доступ к потоку. Эти специфические поточные данные включают все цепочки структурированной обработки исключений (SEH), локальное хранилище потока и другую необходимую внутреннюю информацию. Подробные сведения о SEH-цепочках можно найти в главе 9. Пример с локальным хранилищем потока рассмотрен в главе 15 при обсуждении Memstress-расширений.
    Блок TIB хранится в специальном сегменте памяти, и когда операционной системе нужен доступ к TIB, она переводит содержимое регистра FS и смещение в нормальный линейный адрес. Инструкция, обращающаяся к регистру FS, может реализовать одну из следующих операций: создание или уничтожение SEH-кадра, обращение к блоку TIB или к локальному хранилищу потока.



    Доступ к параметрам, глобальным и локальным переменным

    Теперь рассмотрим доступ к переменным. Глобальные переменные — самые легкие для доступа, потому что при этом обращение к памяти происходит по фиксированному адресу. Если имеется символьная информация для адреса конкретного модуля, то возможно получение для просмотра и имени глобальной переменной. Следующий пример показывает, как выполнить доступ к глобальной переменной через встроенный ассемблер. Во встроенном ассемблере переменные могут выступать либо как источник, либо как приемник, в зависимости от инструкции. В комментариях внимание читателя обращается на то, что может показывать окно Disassembly для данной операции в зависимости от того, загружены ли символы.
    int g_iVal = 0 ;
    void AccessGlobalMemory ( void )
    _asm
    {
    // Установить в глобальной переменной значение 48,059.
    MOV g_iVal , OBBBBh
    // Если символы загружены, окно Disassembly покажет
    // MOV DWORD PTR [g_iVal (00403060)],OBBBBh.
    // Если символы не загружены, окно Disassembly покажет
    // MOV DWORD PTR [00403060],OBBBBh.
    }
    }
    Если для функции определены стандартные стековые кадры, то параметры имеют положительные смещения от регистра ЕВР. В том случае, если за время жизни функции значение ЕВР не изменяется, параметры появляются в тех же самых положительных смещениях, потому что прежде чем вызывать процедуру, параметры помещаются в стек. Следующий код показывает доступ к параметрам.
    void AccessParameter ( int iParam )
    {
    _asm
    {
    // Переместить значение iParam value в регистр ЕАХ.
    MOV ЕАХ , iParam
    // Если символы загружены, окно Disassembly будет показывать
    // MOV ЕАХ,DWORD PTR [iParam].
    // Если символы не загружены, окно Disassembly будет показывать
    // MOV ЕАХ,DWORD PTR [ЕВР+8].
    }
    }
    Если при отладке оптимизированного кода отображаются ссылки, которые имеют положительное смещение от регистра стека ESP, значит это функция, которая имеет FPO-данные. Поскольку во время жизни функции содержимое ESP может изменяться, то работа с параметрами немного затрудняется.
    При работе с оптимизированным кодом нужно сохранять след элементов, помещаемых в стек, потому что ссылка [ESP+20H] в этой функции может быть такой же, как и предыдущая [ESP+SH]. В процессе отладки, при выполнении пошагового прохода через операции языка ассемблера оптимизированного кода, всегда можно заметить, где расположены параметры. Если используются стандартные кадры, локальные переменные имеют отрицательные смещения от ЕВР. Как показано в предыдущем разделе, инструкция SUB резервирует место в стеке. Следующий код содержит пример установки нового значения в локальной переменной:

    void AccessLocalVariable ( void )

    {

    int iLocal ;

    _asm

    {

    // Установить в локальную переменную значение 23.

    MOV iLocal ,'017h

    // Если символы загружены, окно Disassembly покажет

    // MOV DWORD PTR [iLocal],017h.

    // Если символы не загружены, окно Disassembly покажет

    // MOV [EBP-4],017h.

    }

    }

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

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

    void AccessLocalsAndParamsExample ( int * pParaml , int * pParam2 }

    {

    int iLocal1 = 3 ;

    int iLocal2 = 0x42 ;

    iLocal1 = *pParaml ;


    iLocal2 = *pParam2 ;

    }

    // Дизассемблерный код AccessLocalsAndParamsExample

    //с адресами стандартного пролога функции

    00401097 PUSH EBP

    00401098 MOV EBP , ESP

    0040109A SUB ESP , 8

    // int iLocal1 = 3 ;

    0040109D MOV DWORD PTR [EBP-8h] , 3

    // int iLocal2 = 0x42 ;

    004010A4 MOV DWORD PTR [EBP-4h] , 42h

    // iLocal1 = *pParaml ;

    004010AB MOV EAX , DWORD PTR [EBP+8h]

    004010AE MOV ECX , DWORD PTR [EAX]

    004010BO MOV DWORD PTR [EBP-08h] , ECX

    // iLocal2 = *pParam2 ;

    004010B3 MOV EDX , DWORD PTR [EBP+OCh]

    004010B6 MOV EAX , DWORD PTR [EDX]

    004010B8 MOV DWORD PTR [EBP-4h] , EAX

    // Стандартный эпилог функции

    004010BB MOV ESP , EBP

    004010BD POP EBP

    004010BE RET

    }

    Если точка прерывания устанавливается в начале функции AccessLocalsAndParamsExample (по адресу 0x00401097), то будут отображены

    значения стека и регистров, как показано на рис. 6.2.

    Доступ к параметрам, глобальным и локальным переменным
    Рис. 6.2. Стек перед прологом функции AccessLocalsAndParamsExample

    Первые три инструкции языка ассемблера в AccessLocalsAndParamsExaraple составляют пролог функции. После выполнения пролога устанавливаются указатели стека (ESP) и базы (ЕВР), доступ к параметрам выполняется через положительные смещения от ЕВР, а к локальным переменным — через отрицательные смещения от ЕВР. На рис. 6.3 показаны значения указателей стека и базы после выполнения каждой инструкции пролога.

    Доступ к параметрам, глобальным и локальным переменным
    Рис. 6.3. Стек в течение и после выполнения пролога функции AccessLocalsAndParamsExample

    Endians

    Термин "Endianness" описывает свойство CPU, которое определяет порядок хранения частей многобайтовых данных в памяти. Для Intel CPU это свойство обозначают как "Little Endian", что означает, что младший байт (т. е. конец) многобайтового значения хранится в памяти первым. Например, значение 0x1234 хранится в памяти как 0x34 0x12. Важно помнить об этом при просмотре памяти в отладчике. Чтобы получить правильные значения, нужно выполнить преобразование самостоятельно. Если окно Memory используется для просмотра одного из узлов связного списка, и следующим значением указателя является 0x12345678, то в окне это значение будет показано в байтовом формате как 0x78 0x56 0x34 0x12.
    Для любопытных: термин "Endian" пришел из "Путешествий Гулливера" Джонатана Свифта, а компьютерное его значение — из RFC-запроса Дэнни Коена (Danny Cohen, 1980) относительно упорядочивания байтов.

    Формат инструкции и адресация памяти


    Все инструкции Intel CPU имеют следующий основной формат:
    [префикс] инструкция [операнды]
    Префикс присутствует только в некоторых строчных инструкциях. Эти ситуации рассмотрены ниже, в разделе "Манипуляции со строками" данной главы. Формат операндов определяет направление действия операции. В инструкциях с двойным операндом источник указывается во втором операнде, а пункт назначения — в первом, так что операнды читаются (и передают данные) справа налево.
    Инструкция с единственным операндом:
    XXX источник
    Инструкция с двумя операндами (разделяемыми запятой):
    XXX получатель, источник
    Операнд-источник может быть регистром, обращением (ссылкой) к памяти или непосредственным (жестко закодированным) значением. Операнд-получатель может быть регистром или обращением к памяти. Intel CPU не допускают, чтобы и источник, и получатель были обращениями к памяти.
    Обращения к памяти — это такие операнды, которые помещаются в квадратные скобки. Например, обращение к памяти [0040l29Ah] означает "получить значение, размещенное в ячейке памяти Ох0040129А". С помощью суффикса b в языке ассемблера указывается шестнадцатеричное число. Запись [0040i29Ah] — означает то же самое, что и доступ через указатель к целому числу в языке С (*pivai)- Обращения к памяти могут храниться в регистрах. Например, [ЕАХ] означает "получить значение, хранящееся в памяти по адресу, указанному в регистре ЕАХ". Другое часто используемое обращение к памяти указывает адрес, добавляя (шестнадцатеричное) смещение к значению регистра. Например, [ЕАХ+ОСh] означает "добавить смещение ОхС к адресу, хранящемуся в ЕАХ, и получить значение, хранящееся в памяти по этому адресу".
    Эту операцию в С называют "разыменованием указателя", т. е. извлечением из памяти значения по адресу, хранящемуся в указателе. — Пер
    Допустимы также обращения к памяти, включающие вычисление адреса по содержимому нескольких регистров (например,[ЕAX+ЕBX*2]), но такая форма оказывается довольно сложной для приложений.

    Часто встречается обращение, которому предшествует спецификатор размера указателя, дифференцирующий размеры обращений к памяти. Размеры указателя специфицируются как BYTE PTR, WORD PTR и DWORD PTR (для ссылок размером в байт, слово и двойное слово, соответственно). Можно также представлять их, как приведение типов в C++. Если дизассемблер не указывает размера указателя, то он принимается равным двойному слову.

    Иногда применяется прямое обращение к памяти в инструкции, т. е. в нем можно видеть непосредственный адрес соответствующего участка памяти. Например, обращение [ЕВХ] — это просто адрес памяти, содержащийся в регистре ЕВХ, и, чтобы просмотреть его содержимое (т. е. содержащийся в нем адрес), можно просто открыть окно Memory и ввести значение ЕВХ. Однако в других случаях невозможно вычислить ссылку без выполнения сложного шестнадцатеричного умножения. К счастью, окно Registers показывает, на какую память собирается сослаться инструкция.

    Обратите внимание на строку "0012F988 = 0012F9D4" в нижней части рис. 6.1, отображающую эффективный адрес. Текущая инструкция (располагающаяся в данном случае по адресу Ox5F42D8B8) ссылается на адрес Ox0012F988 (левая сторона строки). Правая часть строки — это значение Ox0012F9D4, располагающееся по адресу Ox0012F988. Эффективный адрес в окне Registers показывают только те инструкции, которые выполняют обращение к памяти. Поскольку CPU x86 допускают лишь один операнд с обращением к памяти, то, прослеживая эффективный адрес, можно увидеть, к какой ячейке памяти вы собираетесь выполнить доступ и какое значение в ней расположено.

    Если доступ к памяти неправомерен, то CPU генерирует исключение (типа "General Protection Fault (GPF — общая ошибка защиты)" или "ошибка страницы"). GPF указывает, что приложение пыталось получить доступ к памяти, к которой оно доступа не имело. Ошибка страницы свидетельствует о попытке получить доступ к позиции памяти, которой не существует. Просматривая строку ассемблера, на которой происходит аварийный останов, обратите внимание на ту ее часть, где находится ссылка на память.Из нее можно узнать, какие значения были недействительными. Например, если этот ссылочный операнд выглядит как [ЕАХ], то нужно просмотреть значение регистра ЕАХ в окне Registers. Если ЕАХ содержит недействительный адрес, необходимо стартовать обратное сканирование листинга ассемблера, чтобы увидеть, какая инструкция устанавливает в ЕАХ неправильное значение. Учтите, для того чтобы найти эту инструкцию, может потребоваться обратный проход через несколько вызовов. Далее (в разделе "Окна Memory и Disassembly" этой главы) показано, как можно пройти стек вручную.

    Инструкции, которые нужно знать

    Существует много различных инструкций для Intel CPU; справочный раздел с описанием набора инструкций для Intel Pentium Pro содержит 467 страниц. Это не означает, конечно, что этих инструкций 467; такой объем занимает их описание. К счастью, многие из инструкций не применяются в программах пользовательского режима, так что вам они не нужны. Здесь рассмотрены только инструкции, которые часто используются, и ситуации, в которых они обычно бывают нужны. При этом сначала описывается пара инструкций, а затем демонстрируются сценарии, в которых они применяются.


    Инструкции переходов и ветвлений

  • JMP абсолютный переход
  • Как указано в названии, JMP передает управление по абсолютному адресу.
  • JE переход, если равно
  • JL переход, если меньше чем
  • JG переход, если больше чем
  • JNE переход, если не равно
  • JGE переход, если больше или равно
  • OLE переход, если меньше или равно
  • От инструкций СМР и TEST немного пользы, если программист не имеет возможности воздействовать на их результаты. Условные переходы позволяют выполнять соответствующие ветвления программы. Показанные выше инструкции — это наиболее общие условные переходы, с которыми вы встретитесь в окне Disassembly, хотя всего существует более трех десятков (точнее — 31) различных условных переходов, многие из которых выполняют те же самые действия за исключением того, что в мнемонике используется слово "NOT". Например, инструкция JLE (переход, если меньше или равно) имеет тот же код операции, что JNG (переход, если не больше чем). Работая с другим дизассемблером (не из отладчика Visual C++), можно увидеть иные инструкции. Чтобы расшифровывать все инструкции переходов, можно найти jcc-коды в руководствах Intel.
    В следующем примере инструкции условных переходов расположены в том же порядке, как в табл. 6.4. Один из условных переходов немедленно следует за инструкциями СМР и TEST. Оптимизированный код может содержать несколько инструкций, разбросанных между проверкой и переходом, но эти инструкции никогда не изменяют флажков.
    Просматривая код дизассемблера, можно заметить, что условная проверка вообще-то противоположна той, что была введена в исходный код. Пример показан в первой секции следующего кода.
    void JumpExamples ( int i )
    {
    // Здесь показан оператор С-кода. Заметьте, что условие записано как
    // "i > 0", но компилятор генерирует противоположное условие.
    Код

    // ассемблера, который показан в следующей секции, похож на тот,

    // что генерирует компилятор.

    // Разные методы оптимизации генерируют различный код.

    // if ( i > 0 )

    // {

    // printf ( "i > 0\n" ) ;

    // }

    char szGreaterThan[] = "i > 0\n" ;

    _asm

    {

    CMP i , 0 // Сравнить i с 0 вычитанием (i - 0).

    JLE JE_LessThanOne // Если i меньше чем или равно 0, перейти

    // к метке.

    PUSH i // Поместить параметр в стек.

    LEA ЕАХ , szGreaterThan // Поместить в стек форматную строку.

    PUSH ЕАХ CALL DWORD PTR [printf] // Вызвать printf. Заметьте, что printf,

    // вероятно, приходит из DLL, потому что

    // вызов выполняется через указатель.

    ADD ESP ,8 // для printf действует соглашение _cdecl,

    поэтому

    // нужно очистить стек в вызывающей

    // программе.

    JE_LessThanOne: //Во встроенном ассемблере можно

    // перейти к любой С-метке.

    }

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

    // Взять абсолютное значение параметра и снова проверить.

    // С-код:

    // int у = abs ( i ) ;

    // if. ( у >=5 )

    // {

    // printf ( "abs(i) >= 5\n" ) ;

    // }

    // else

    // {

    // printf ( "abs(i) < 5\n" ) ;

    // }

    char szAbsGTEFive [] = "abs(i) >= 5\n" ;

    char szAbsLTFive[] = "abs(i) < 5\n" ;

    _asm

    {

    MOV EBX , i // Переместить значение i в ЕВХ.

    СМР ЕВХ , 0 // Сравнить ЕВХ с 0 (ЕВХ - 0).

    JG JE_PosNum // Если результат больше 0, то ЕВХ

    // содержит положительное значение.

    NEG ЕВХ // Преобразовать отрицательное в положительное.

    JE_PosNum:

    СМР ЕВХ , 5 // Сравнить ЕВХ с 5 (ЕВХ - 5).

    JL JE_LessThan5 // Переход, если меньше 5.

    LEA ЕАХ , szAbsGTEFive // Получить в ЕАХ указатель на правильную

    // форматную строку.

    JMP JE_DoPrintf // Перейти к вызову printf.

    JE_LessThan5:

    LEA ЕАХ , szAbsLTFive // Получить в ЕАХ указатель на правильную

    // форматную строку.

    JE_DoPrintf:

    PUSH ЕАХ // Поместить строку в стек.


    CALL DWORD PTR [printf] // Напечатать ее.

    ADD ESP , 4 .. // Восстановить стек.

    }

    }

    Нетрудно видеть, что результат в первом примере правилен. Идея состоит в" том, что выгоднее проверить противоположное условие и затем выполнить переход, чем сначала выполнить переход, проверить условие внутри оператора if и потом перейти обратно.

  • переход, если выше
  • JBE переход, если ниже или равно
  • JC переход, если есть перенос
  • JNC переход, если нет переноса
  • JNZ переход, если не О
  • JZ переход, если О
  • Эти инструкции условных переходов не столь обычны как те, что были перечислены выше, но их можно увидеть в окне Disassembly. Необходимо разбираться в их условиях интуитивно, по имени перехода.

    Изучайте ASM-файлы

    Чтобы увидеть смешанный код — исходный и ассемблера, нужно с помощью Visual C++ сгенерировать ассемблерные листинги для исходных файлов. Если вы укажете ключ /FAS в редактируемое поле Project Options на вкладке C++ диалогового окна Project Settings, то компилятор сгенерирует ASM-файл для каждого исходного файла. Можно не генерировать ASM-файлы при каждом построении, но они могут быть поучительны, позволяя видеть генерируемый компилятором код. При наличии ASM-файлов не требуется каждый раз запускать приложение, когда возникает необходимость просмотреть программу в кодах языка ассемблера.
    Сгенерированные файлы почти готовы для компиляции с помощью макроассемблера Microsoft (Microsoft Macro Assembler — MASM), но они могут быть довольно сложными для чтения. Многие файлы состоят из директив MASM, но главные части файлов показывают С-код вместе с кодом языка ассемблера под каждой С-конструкцией. После усвоения материала этой главы читатели не должны испытывать никаких проблем при чтении ASM-файлов.


    Код мусора

    Когда в результате аварийного останова вы оказываетесь в окне Disassembly, необходимо определить, действительно ли там отображается реальный код (это иногда довольно трудно). Вот некоторые советы:
  • оказалось, что для просмотра кодов инструкций ассемблера полезно включение режима Code Bytes (в контекстном меню окна Disassembly);
  • если в окне Disassembly отображается ряд идентичных инструкций ADD BYTE PTR [EAX], AL, это не есть правильный код ассемблера. Вы видите ряд нулей;
  • если отображаются символы, но добавленные к ним смещения — очень большие числа, в общем случае превосходящие 0x1000, то вы, вероятно, вне секции кода. Однако очень большие числа могут также означать, что отлаживается модуль, который не содержит доступных частных (private) символов;
  • если вы видите группу инструкций, которые не описаны в этой главе, значит вы, вероятно, видите данные;
  • если дизассемблер Visual C++ не может дизассемблировать инструкцию, то в качестве кода операции он показывает "???".



  • Команда Set Next Statement окна Disassembly доступна в контекстном меню (открываемом правым щелчком мыши) и позволяет изменить EIP (регистр указателя инструкций), при помощи перевода указателя на следующую позицию исполнения. В окнах с исходными кодами команда Set Next Statement допускает некоторую "небрежность", но следует быть очень осторожным с этой командой в окне Disassembly.
    Чтобы избежать аварийных остановов при переустановках EIP, необходимо обратить внимание на стек. Произвольные размещения в стеке и извлечения из него могут привести к аварийному завершению программы.
    Например, для того чтобы повторно выполнить функцию без немедленного аварийного останова, необходимо обеспечить такое изменение выполнения, чтобы стек оставался сбалансированным. Ниже приводится двукратное обращение к функции, расположенной по адресу 0x00401005.
    00401032 PUSH EBP
    00401033 MOV EBP , ESP
    00401035 PUSH 404410h
    0040103А CALL 00401005h
    0040103F ADD ESP , 4
    00401042 POP EBP
    00401043 RET
    Дважды проходя через дизассемблирование, необходимо удостовериться, что при выполнении инструкции ADD по адресу 0x0040103F не нарушает баланс стека. Как было показано ранее при обсуждении различных соглашений о вызовах, данный фрагмент ассемблерного кода показывает обращение к _cded-функции (потому что инструкция ADD расположена прямо после ее вызова). Чтобы повторно выполнять функцию, следует установить указатель инструкций на 0x00401035, гарантируя, что операция PUSH выполнится должным образом.


    Манипуляции с данными

  • AND логическое И
  • OR логическое ИЛИ-(включающее)
  • Инструкции AND и OR выполняют поразрядные операции, которые должны быть знакомы каждому, потому что они являются основой для манипуляции с разрядами.
  • NOT отрицание с поразрядным дополнением до 1
  • NEG отрицание с поразрядным дополнением до 2
  • Инструкции NOT и NEG иногда вызывают некоторое замешательство, потому что по виду они похожи, но, конечно, не выполняют одну и ту же операцию. Инструкция NOT — поразрядная операция, которая устанавливает каждую двоичную 1 в 0 и каждый двоичный 0 в 1. Инструкция NEG выполняет вычитание операнда из 0. Следующий фрагмент кода показывает различия между этими двумя инструкциями:
    void NOTExample { void )
    {
    _asm {
    MOV EAX , OFFh
    MOV EBX , 1
    NOT EAX // EAX теперь содержит OFFFFFFOOh.
    NOT EBX // ЕВХ теперь содержит OFFFFFFFEh.
    }
    }
    void NEGExample ( void ) (
    _asm
    {
    MOV EAX , OFFh MOV EBX , 1
    NEG EAX // EAX теперь содержит OFFFFFFOlh ( 0 - OFFh ).
    NEC EBX // EBX теперь содержит OFFFFFFFFh ( 0 - 1 ).
    }
    }
  • XOR логическое ИЛИ (исключающее)
  • Инструкция XOR — это самый быстрый способ обнулить значение. XOR с двумя операндами установит каждый разряд в 1, если одинаковые разряды в каждом операнде различны. Если все разряды одинаковы, то результат равен 0. Поскольку операция
    XOR ЕАХ,ЕАХ
    выполняется быстрее, чем
    MOV EAX,0
    (потому что первая занимает меньшее количество машинных тактов), компиляторы Microsoft используют XOR для обнуления регистров.
  • INC инкремент (увеличение) на 1
  • DEC декремент (уменьшение) на 1
  • Эти инструкции очень просты, и можно понять, что они делают, прямо из их названий. Компилятор часто использует эти инструкции при оптимизации некоторых кодовых последовательностей, потому что каждый из них выполняется за один цикл часов.
    Кроме того, эти инструкции отображаются прямо в целочисленные арифметические С-операции ++ и - -

  • SHL сдвиг влево, умножение на 2
  • SHR сдвиг вправо, деление на 2
  • При двоичных манипуляциях поразрядные сдвиги выполняются быстрее, чем соответствующие инструкции умножения и деления в CPU x86. Эти инструкции родственны поразрядным С-операциям " и ", соответственно.

  • DIV беззнаковое деление
  • MUL беззнаковое умножение
  • Эти простые с виду инструкции фактически немного странны. Обе инструкции выполняют беззнаковые действия на регистре ЕАХ. Но вывод неявно использует регистр EDX. Старшие байты двойного слова и большие по величине множители помещаются в регистр EDX. Инструкция DIV сохраняет остаток в EDX, а частное — в ЕАХ. Обе инструкции оперируют значением из ЕАХ, используя в качестве второго операнда только значения из регистра или из памяти.

  • IDIV деление со знаком
  • IUML умножение со знаком
  • Эти инструкции подобны инструкциям DIV и MUL, за исключением того, что они обращаются с операндами как со знаковыми значениями. Старшие байты двойного слова и большие по величине множители помещаются в регистр EDX. Инструкция IDIV сохраняет остаток в EDX, а частное — в ЕАХ. Обе инструкции оперируют значением из ЕАХ, используя в качестве второго операнда только значения из регистра или из памяти. Инструкция IMUL иногда имеет три операнда. Первый операнд — целевой (в нем сохраняется результат), а два последних — исходные операнды. В наборе инструкций х86 IMUL — единственная инструкция с тремя операндами.

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

  • MOVSX пересылка с расширением знакового разряда
  • MDVZX пересылка с расширением нулем
  • Эти две инструкции копируют значения меньшего размера в значения большего размера и указывают, как в больших значениях заполняются старшие разряды. MOVSX указывает, что знаковое значение исходного операнда будет расширено на все верхние разряды регистра-приемника (т. е. регистра результата). MOVZX заполняет, верхние разряды регистра-приемника нулями. Эти две инструкции предназначены для наблюдений за значениями при прослеживании ошибок в знаках.

    Манипуляции с указателями

  • LEA загрузить эффективный адрес
  • LEA загружает целевой регистр адресом исходного операнда. Следующий фрагмент кода содержит два примера с инструкцией LEA. Первый показывает, как следует назначать адрес целому указателю, а второй — как извлекать адрес локального символьного массива с помощью инструкции LEA и передавать адрес как параметр API-функции GetwindowsDirectory.
    void LEAExamples ( void )
    {
    int * pint ;
    int iVal ;
    // Следующие инструкции эквивалентны С-коду
    // pint = siVal ;.
    _asm
    {
    LEA EAX , iVal
    MOV [pint] , EAX
    }
    ///////////////////////////////////////////////////////////////////
    char szBuff [ MAX_PATH ] ;
    // Другой пример доступа к указателю через LEA.
    // Эта последовательность инструкций идентична С-коду
    // GetWindowsDirectory ( szBuff , МАХ_РАТН ) ;.
    _asm
    {
    PUSH 104h // Поместить МАХ_РАТН в стек как
    // второй параметр.
    LEA ЕСХ , szBuff // Получить адрес szBuff.
    PUSH ECX // Поместить адрес szBuff в стек как
    // первый параметр.
    CALL DWORD PTR [GetWindowsDirectory]
    }
    }


    Манипуляции со стеком

  • PUSH поместить слово или двойное слово в стек
  • POP извлечь значение из стека
  • Intel CPU широко используют стек. Другие CPU, которые имеют намного больше-регистров, могут через них передавать параметры, но Intel CPU передают большинство параметров через стек. Стек начинается в старших адресах памяти и растет вниз. Обе эти инструкции неявно изменяют регистр ESP, который указывает текущую вершину стека. После операции PUSH значение регистра ESP уменьшается, после операции POP — увеличивается.
    В стек можно помещать значения регистров, адреса ячеек памяти или жестко закодированные числа. Извлеченный элемент стека обычно перемещается в регистр. Ключевой характеристикой стека CPU является организация его данных в виде очереди LIFO (Last In, First Out). Если в стек помещаются значения трех регистров, то извлекать их нужно в обратном порядке, как это делает функция PushPop:
    void PushPop ( void )
    {
    _asm
    {
    // Сохранить значения регистров ЕАХ, ЕСХ и EDX. ,
    PUSH ЕАХ
    PUSH ЕСХ
    PUSH EDX
    // Здесь нужно выполнить некоторые операции, которые могут
    // изменить значения указанных регистров.
    // Восстановить ранее сохраненные регистры. Обратите внимание,
    // что они удаляются из стека в LIFO-порядке.
    POP EDX .
    POP ECX
    POP EAX
    }
    }
    Хотя существуют гораздо более эффективные способы обмена значений, инструкции PUSH и POP позволяют обменять значения регистров. Обмен происходит, когда программист изменяет порядок pop-инструкций:
    void SwapRegistersWithPushAndPop ( void )
    {
    _asm
    {
    // Обменять значения регистров EAX и ЕВХ , используя стек.
    PUSH EAX
    PUSH EBX
    POP EAX
    POP EBX
    }
    }
  • PUSHAD поместить в стек значения всех регистров общего назначения
  • POPAD извлечь из стека значения всех регистров общего назначения
  • С этими инструкциями можно иногда столкнуться при отладке системного кода. Они эквивалентны длинным цепочкам PUSH-инструкций для сохранения всех общих регистров и pop-инструкций — для их восстановления.


    Манипуляции со строками

    Intel CPU обладают большими возможностями для манипуляций со строками, т. е. поддерживают обработку больших участков памяти в отдельной инструкции. Все рассмотренные здесь строчные инструкции имеют несколько мнемоник, которые можно найти в справочных руководствах Intel. Однако окно Disassembly в Visual C++ всегда дизассемблирует строчные инструкции только в те формы, которые показаны ниже. Все эти инструкции могут работать на областях памяти размером в байт, слово и двойное слово.
  • MOVS перемещают данные из строки в строку
  • Инструкция MOVS перемещает адрес памяти из регистра ESI в регистр EDI. Эта инструкция работает только на значениях, на которые указывают ESI и EDI. Инструкцию MOVS можно представить себе как реализацию С-функции memcpy. Окно Disassembly из Visual C++ всегда показывает размер операции со спецификатором размера, так сразу видно, сколько памяти было перемещено. После того как перемещение заканчивается, регистры ESI и EDI инкрементируются или декрементируются, в зависимости от флажка направления (DF) в регистре EFLAGS (отображаемого как поле UP в окне Registers Visual C++). Если поле UP равно 0, то регистры инкрементируются, а если равно 1, регистры декрементируются. Величина инкремента и декремента зависит от размера памяти, с которой работает операция: 1 — для байт, 2 — для слов и 4 — для двойных слов.
  • SCAS сканировать строку
  • Инструкция SCAS сравнивает значение по адресу памяти, указанному в регистре EDI, со значением в регистрах AL, АХ или ЕАХ (в зависимости от требуемого размера). Результаты сравнения устанавливают значения различных флажков в регистре EFLAGS. Установки флажков — те же, что показаны в табл. 6.4. Если вы сканируете строку в поисках NULL-терминатора (пустого указателя), то инструкцию SCAS можно использовать, чтобы дублировать возможности С-функции strien. Подобно инструкции MOVS, SCAS выполняет автоинкремент или автодекремент регистра EDI.
  • STOS сохранить строку
  • Инструкция STOS сохраняет значение регистров AL, АХ или ЕАХ (в зависимости от требуемого размера) по адресу, указанному регистром EDI.
    Инструкция STOS похожа на С- функцию memset. Подобно инструкциям MOVS и SCAS, инструкция STOS автоинкрементирует или автодекрементирует регистр EDI.

  • CMPS сравнить строки
  • Инструкция CMPS сравнивает два строчных значения и устанавливает соответствующие флажки в EFLAGS. Тогда как SCAS выполняет сравнения символов в единственной строке, CMPS проходит символы в двух строках. Инструкция CMPS похожа на С-функцию memcmp. Подобно остальным строчным манипуляторам, инструкция CMPS сравнивает значения различных размеров, а также выполняет автоинкремент и автодекремент указателей обеих строк.

  • ВЕР повторять по счетчику в EСХ
  • REPE повторять, пока равно или счетчик ЕСХ не станет равен 0
  • REPNE повторять, пока не равно или счетчик ЕСХ не станет равен 0
  • Строчные инструкции, хотя и удобны, но не много стоят, когда манипулируют элементом только один раз. Префиксы повторения позволяют выполнять строчные инструкции заданное (в ЕСХ) количество раз или пока не будет выполнено указанное условие. При пошаговом проходе окна Disassembly командой Step Into выполнение такой инструкции повторяется необходимое число раз. Если же при этом используется команда Step Over, то такая инструкция не выполняется повторно. При отладке можно с помощью команды Step Into проверять строки в регистрах ESI или EDI. Другой прием: при поиске аварийного останова в строчной инструкции с префиксом повторения нужно взглянуть на регистр ЕСХ, чтобы увидеть, на какой итерации случился останов.

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

    void MemCPY ( char * szSrc , char * szDest , int iLen )

    {

    _asm

    {

    MOV ESI , szSrc // Установить исходную строку.


    MOV EDI , szDest // Установить целевую строку ng.

    MOV ЕСХ , iLen // Установить длину копирования.

    // Копировать немедленно!

    REP MOVS BYTE PTR [EDI] , BYTE PTR [ESI]

    }

    }

    int StrLEN (char * szSrc )

    {

    int iReturn ; _asm

    {

    XOR EAX , EAX // Обнулить ЕАХ.

    MOV EDI , szSrc // Поместить проверяемую строку в EDI.

    MOV ECX , 0FFFFFFFFh // Максимальное число проверяемых

    // символов.

    REPNE SCAS BYTE PTR [EDI] // Сравнивать, пока ЕСХ не станет

    // равным 0 или не будет найден конец

    // строки .

    СМР ЕСХ ,0 // Если ЕСХ равен 0, то

    JE StrLEN_NoNull // в строке не был найден NULL.

    NOT ECX // Преобразовать ЕСХ в положительное число,

    // поскольку он был просчитан.

    DEC ЕСХ ' // Подсчет для Попадания на NULL.

    MOV EAX , ЕСХ // Возврат счета.

    JMP StrLen_Done .// Возврат. StrLENJNoNull:

    MOV EAX , OFFFFFFFFh // Поскольку NULL не был найден,

    И возвратить -1.

    StrLEN_Done:

    }

    _asm MOV iReturn , EAX ;

    return ( iReturn ) ;

    }

    void MemSET ( char * szDest , irit iVal , int iLen )

    { _asm

    {

    MOV EAX , iVal // EAX содержит полное значение.

    MOV EDI , szDest // Переместить строку в EDI.

    MOV ECX , iLen // Переместить счет в ЕСХ.

    REP STOS BYTE PTR [EDI] // Заполнить память.

    }

    }

    int MemCMP ( char * szMeml , char * szMem2 , int iLen )

    {

    int iReturn ;

    _asm

    {

    MOV ESI , szMeml // ESI содержит первый блок памяти.

    MOV EDI , szMem2 // EDI содержит второй блок памяти.

    MOV ECX , iLen // Максимальное число байт для сравнения

    // Сравнить блоки памяти.

    REPE CMPS BYTE PTR.

    [ESI], BYTE PTR [EDI]

    JL MemCMP_LessThan // Если szSrc < szDest

    JG MemCMP_GreaterThan // Если szSrc > szDest

    // Блоки памяти равны.

    XOR EAX', EAX // Возвратить 0.

    JMP MemCMP_Done

    MemCMP_Les sThan:

    MOV EAX , 0FFFFFFFFh // Возвратить -1.

    JMP MemCMP_Done

    MemCMP_GreaterThan:

    MOV EAX , 1 // Возвратить 1.

    JMP MemCMP_Done

    XemCMP_Done:

    }

    _asm MOV iReturn ,

    EAX return ( iReturn ) ;

    }

    Навигация

    К счастью, окно Disassembly предлагает несколько эффективных способов навигации в подчиненном отладчике.
    Первый путь для достижения определенной позиции в подчиненном отладчике — через диалоговое окно Go To, открываемое командой Go To меню Edit или нажатием клавиш +. Если адрес перехода известен, то его можно ввести в поле Enter address expression и переходить в нужное место в коде прямо по этому адресу. Диалоговое окно Go To поддерживает также интерпретацию символов и контекстной информации, поэтому можно переходить к областям памяти, даже не зная точного адреса.
    Например, если имеются символы, загруженные для KERNEL32.DLL, и требуется перейти к коду функции LoadLibrary в окне Disassembly, то для этого следует ввести в диалоговом окне Go То строку
    {,, kerne!32}_LoadLibraryA@4
    Весьма полезна еще одна известная возможность, которую поддерживает окно Disassembly — буксировка мышью ("drag-and-drop"). Если вы работаете через секцию языка ассемблера, и нужно быстро проверить, где в памяти выполняется операция, то можно выделить адрес и перетащить его. После отпускания кнопки мыши окно Disassembly автоматически переходит к этому адресу.
    Для того чтобы вернуться обратно туда, где находится указатель инструкции, просто сделайте правый щелчок мышью в окне Disassembly и введите команду Show Next Statement. Эта команда также доступна в окнах исходного кода.


    Общая последовательность: вход и выход из функции

    Большинство функций Windows и в пользовательских программах выполняют вход и выход в одной и той же манере. Для входа в функцию устанавливается пролог, а для выхода — эпилог (компилятор генерирует их автоматически). При установке пролога код получает доступ к локальным переменным и параметрам функции. Объекты доступа называются кадром стека (stack frame). Хотя CPU x86 явно не определяет никакой схемы стекового кадра, операционным системам легче всего использовать для хранения указателя на этот кадр регистр ЕВР (этому способствует конструкция CPU и формат некоторых его инструкций).
    _asm
    {
    // Установка стандартного пролога.
    PUSH EBP // Сохранить содержимое регистра стекового кадра.
    MOV EBP , ESP // Установить в ЕВР адрес стекового кадра локальной
    // функции.
    SUB ESP , 20h // Отвести в стеке 0x20 байт для локальных
    // переменных. Инструкция SUB появляется, только
    // если функция имеет локальные переменные.
    }
    Эта последовательность является общей как для отладочных, так и для выпускных построений (финальных версий). Однако в некоторых функциях выпускных построений можно увидеть группу инструкций, помещенных между PUSH и MOV. CPU с множественными конвейерами (например, из семейства Pentium) могут расшифровывать сразу несколько инструкций одновременно, и чтобы воспользоваться этим преимуществом, оптимизатор попытается установить поток инструкций.
    В зависимости от режима оптимизации, выбранного при компиляции кода, можно также иметь функции, которые не используют регистр ЕВР в качестве указателя кадра стека. Эти процедуры обладают тем, что называют FPO1-данными. В окне дизассемблера код такой функции выглядит так, как будто она только что начала манипулировать данными. Как можно идентифицировать одну из таких функций, будет показано ниже, в разделе "Доступ к параметрам, локальным и глобальным переменным" этой главы.
    Следующий общий (для всех функций) эпилог отменяет действия пролога. Такой эпилог можно увидеть в большинстве в отладочных конструкций.
    Этот эпилог соответствует указанному выше прологу.

    _asm

    {

    // Стандартный демонтаж эпилога

    MOV ESP , ЕВР // Восстановить стековое значение.

    POP EBP // Восстановить сохраненное значение регистра -.

    // стекового кадра.

    }

    В выпускных построениях инструкция LEAVE выполняется быстрее, чем последовательности MOV/POP, так что в их эпилогах можно найти только инструкцию LEAVE. Она идентична последовательности MOV/POP. В отладочных построениях компиляторы по умолчанию используют последовательности MOV/POP. Интересно, что CPU х86 для установки пролога имеют соответствующую инструкцию — ENTER, но она медленнее, чем последовательность PUSH/MOV/ADD, так что компиляторы ее не применяют.

    Выбор компиляторами способа генерации кода во многом зависит от того, как оптимизирована программа — по скорости или по размеру. Если установлена оптимизация по размеру, как было настоятельно рекомендовано в главе 2, большинство функций преимущественно используют стандартные стековые кадры. Оптимизация по скорости приводит к более сложной FPO-генерации.

    FPO (Frame Pointer Omission — пропуск указателя кадра). — Пер.

    Общие конструкции языка ассемблера

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


    Окна Memory и Disassembly

    Окна Memory и Disassembly имеют симбиозные отношения. Пытаясь определить, что делает последовательность операций языка ассемблера в окне Disassembly, надо держать окно Memory открытым, чтобы можно было видеть и адреса, и значения. Инструкции языка ассемблера работают в памяти, а память воздействует на выполнение этих инструкций. Окна Disassembly и Memory вместе позволяют наблюдать динамику этих взаимоотношений. Само по себе, окно Memory — просто море чисел, особенно когда происходит аварийный отказ. Однако, комбинируя эти два окна, можно начать вычисления, связанные некоторыми неприятными проблемами аварийных отказов. Совместное использование этих окон наиболее важно при отладке оптимизированного кода, когда прохождение стека отладчика затруднено. Чтобы разрешить аварийную ситуацию, необходимо пройти стек вручную. На первом этапе прохождения стека нужно знать, по каким адресам памяти загружены ваши двоичные файлы. В отладчике Visual C++ 6 добавлено диалоговое окно Module List, отображающее все двоичные файлы, загруженные вашей программой. Оно показывает также имя модуля, путь к модулю в дереве каталогов, порядок загрузки и, самое важное, диапазон адресов загрузки модуля. Поскольку это окно является модальным, лучше записать имена модулей и их загрузочные адреса, потому что эта информация понадобится в будущем неоднократно. Сравнивая элементы стека со списком диапазонов адресов, можно получить некоторое представление о том, какие элементы адресованы в ваших модулях.
    Просмотрев диапазоны адресов загрузки, откройте окна Memory и Disassembly. В окне Memory введите в поле Address регистр стека ESP и покажите значения в формате двойного слова, щелкая правой кнопкой мыши в пределах окна и выбирая команду Long Hex Format в контекстном меню. Используя либо свой список адресов загрузки, либо диалоговое окно Module List, начните просмотр чисел в окне Memory слева направо и сверху вниз.
    Для проверки числа на принадлежность одному из ваших загруженных модулей, выберите его и перетащите в окно Disassembly.
    В нем будут отображены инструкции языка ассемблера по этому адресу, а если в приложение включена полная отладочная информация, то вы сможете увидеть вызы вающую функцию.

    Если регистр ESP не содержит ничего, что напоминало бы адрес модуля, выведите дамп регистра ЕВР в окно Memory и выполните те же действия. Освоившись с языком ассемблера, вы сможете просматривать дизассемблерный код, окружающий адрес аварийного останова. Изучение "криминальной" аварийной ситуации позволит вам понять, где бы мог быть расположен адрес возврата — в ESP или в ЕВР.

    Поговорим немного сначала о достоинствах, а затем и о недостатках окна Memory. Во-первых, окно Memory — единственное место, где можно просматривать символьные строки, превышающие 255 знаков. Кроме того, окно Memory позволяет просматривать любую переменную или участок памяти.

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

    История отладочной войны

    Что может быть не так в функции GlobalUnlock?

    Ведь она просто разыменовывает указатель.

    Сражение

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


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

    Результат

    Во-первых, не было уверенности, что кто-то еще использует функции дескрип-торной памяти (handle-based memory) GlobalAlloc, GlobalLock, GlobalFree и

    GlobalUnlock) в \Л/1п32-програм|"ировании. Однако после просмотра в коде дизассемблера сторонних управляющих элементов стало понятно, что их автор, очевидно, перенес их из 16-разрядной кодовой базы. Первая гипотеза состояла в том, что эти элементы неправильно взаимодействовали с API-функциями де-скрипторной памяти.

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

    В некоторой точке после закрытия диалогового окна Print, мы заметили, что стартовавшая функция GlobalAlloc возвращала значения дескриптора, которые завершались нечетными цифрами, например, 5. Поскольку дескрипторная память в Win32 нуждается в разыменовании указателя, чтобы преобразовать дескриптор в значения памяти, я сразу же понял, что наткнулся на критическую ситуацию. Каждое распределение памяти в Win32 должно заканчиваться шест-надцатеричными цифрами 0, 4, 8 или С, потому что все указатели должны быть выровнены на двойное слово. Значения же дескрипторов, выходящие из GlobalAlloc, были явно (и довольно значительно) искажены.

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


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

    Проверка исходного кода показала, что приложение было полным 32-разрядным Windows-приложением и ничего не делало с дескрипторной памятью. Тогда мы проверили функцию печати — ее код выглядел безупречно.

    Было решено сузить область дублирования аварии. После нескольких прогонов оказалось, что для аварийного останова нужно было лишь открыть диалоговое окно Print и изменить ориентацию печатной страницы, а после закрытия окна Print надо было просто повторно открыть его. Аварийный останов происходил вскоре после второго закрытия окна. Ориентация страницы, вероятно, просто изменяла байт где-то в памяти, что и служило причиной аварии.

    Хотя при начальном чтении код выглядел безупречно, я просматривал каждую строку в обратном порядке и перепроверял ее по документации MSDN. Через 10 минут ошибка была найдена. Команда сохраняла структуру данных PRINTDLG, используемую для инициализации диалогового окна Print, с помощью API-функции PrintDlg. Третье поле в этой структуре — hDevMode — является значением дескрипторной памяти, которую выделяет диалоговое окно Print. Ошибка состояла в том, что разработчики использовали это значение памяти как регулярный указатель и должным образом не разыменовывали дескриптор или вызывали функцию GlobalLock для дескриптора. Изменяя значения в структуре DEVMODE, на самом деле они выполняли запись в глобальную таблицу дескрипторов процесса. Эта таблица является участком памяти, в котором хранятся все распределения динамической памяти дескрипторов. При случайной записи в глобальную таблицу дескрипторов обращение к GlobalAlloc использует неправильные смещения, а значения, вычисленные по такой таблице, приводили к тому, что функция GlobalAlloc возвращала некорректные указатели.


    Уроки

    Урок первый — нужно тщательно читать документацию. Если в документации говорится, что структура данных есть "перемещаемый глобальный объект памяти", то память предназначена для дескрипторов, и необходимо должным образом разыменовывать этот дескриптор памяти или использовать на нем функцию GlobalLock. Хотя Windows 3.1 сильно устарела, некоторые 16-разрядные компоненты все еще входят в Win32 API, и следует обращать на них внимание.

    Урок второй — глобальная таблица дескрипторов хранится в перезаписываемой памяти. Лично я считаю, что такая важная структура операционной системы должна храниться в памяти "только-для-чтения". Почему Microsoft не защитил эту память? Могу предположить, что дело в следующем. Технически память дескрипторов используется только для обратной совместимости, и 32-разрядные Windows-приложения должны бы были использовать специфические для Win32 типы памяти. Защита глобальной таблицы дескрипторов потребовала бы двух переключений контекста (из режима пользователя в режим ядра) на каждом вызове функции дескрипторной памяти. Поскольку эти контекстные переключения очень дороги (в смысле времени их обработки), можно понять, почему Microsoft не защитил глобальную таблицу дескрипторов от записи.

    Урок последний — мы потратили слишком много времени, сосредоточившись на стороннем элементе управления. Всего мне потребовалось около семи часов, чтобы найти ошибку. Однако тот факт, что ошибка могла дублироваться только при открытии диалогового окна Print, которая пришла из кода приложения, должен был предупредить меня, что проблема была "ближе к дому" (а не где-то на стороне).



    Окно Disassembly

    Теперь, немного познакомившись с языком ассемблера, рассмотрим свойства окна Disassembly отладчика Visual C++, облегчающие выполнение отладочных работ, и способы уменьшения времени, проводимого в окне Disassembly.


    Основы CPU

    Довольно продолжительное время мы живем в окружении набора команд процессоров компании Intel, уходящего корнями в CPU 8086, который Intel впервые выпустил в 1978 году. Во времена MS-DOS и 16-разрядной операционной системы Windows язык ассемблера казался немного странным и трудным (из-за методики работы CPU с памятью — через 64 Кбайтные блоки, называемые сегментами). К счастью, сегодня иметь дело с языком ассемблера намного легче, потому что в Microsoft Windows 98 и 2000 CPU имеет прямой доступ к полному адресному пространству.
    Язык ассемблера, который я представлю в этой главе, имеет дело с основным набором 32-разрядных команд, совместимых со всеми CPU архитектуры х86 от компаний Intel и AMD (Advanced Micro Devices). Продвинутые свойства процессоров Intel Pentium, например, ММХ в обычной практике не применяются, потому что Windows использует относительно немного таких свойств. Здесь мы не будем углубляться в действительно неприятные аспекты форматов инструкций языка ассемблера, такие как байты ModR/M и SIB, которые указывают способ доступа к памяти. Не будут рассмотрены также инструкции с плавающей точкой. Операции на модуле с плавающей точкой Intel CPU (FPU) подобны обычным инструкциям. Главные их отличия состоят в том, что FPU имеет собственный набор регистров, а инструкции с плавающей точкой используют стековую архитектуру регистров.
    Напомним один ключевой момент — CPU x86 обладают большой гибкостью и реализуют много способов выполнения аналогичных операций. К счастью, компиляторы Microsoft проделывают большую работу по выбору самого быстрого способа выполнения операции и многократному использованию этой конструкции везде, где она применима, распознавая, таким образом, в какой секции кода ее легче выполнить. В данной главе описаны наиболее широко используемые инструкции, которые можно встретить в программах на языке ассемблера. Если этот материал вдохновит читателя на более подробное изучение семейства CPU компании Intel — а я надеюсь, что это так и будет — ему придется загрузить PDF1-файлы трехтомного руководства разработчиков архитектуры программного обеспечения компании Intel ("Intel Architecture Software Developer's Manual") с узла www.intel.com. Intel даже предлагает эти руководства в свободно распространяемой книжной форме.
    1 PDF — Portable Document Format. Формат переносимых (мобильных) документов компании Adobe. — Пер.



    Полный пример

    Представив все важные части языка Intel-ассемблера, обратимся к полному примеру одной из API-функций операционных систем Win32. В листинге 6.2 показан полностью прокомментированный дизассемблерный код функции IstrcpyA из библиотеки KERNEL32.DLL пакета обслуживания Service Pack 4 операционной системы Windows NT 4. Функция IstrcpyA копирует одну строку в другую. Эту функция выбрана потому, что она показывает понемногу все, что обсуждалось до сих пор в этой главе, а также потому, что цель этой функции легко понять. Комментарии, выделенные точками с запятой, делают их настолько подробными, насколько это возможно.
    Листинг 6-2. IstrcpyA- полный пример на языке ассемблера
    ; Прототип функции:
    ; LPTSTR Istrcpy ( LPTSTR IpStringl , LPCTSTR lpString2 )
    IstrcpyA:
    ; Начать подготовку к установке SEH-кадра.
    77F127E6: MOV EAX , ES:[00000000h]
    ; Установить регулярный кадр стека.
    77F127EC: PUSH EBP
    77F127ED: MOV EBP , ESP
    ; Продолжить установку SEH-кадра.
    77F127EF: PUSH OFFh
    77F127F1: PUSH 77F3CD48h
    77F127F6: PUSH _except_handler3
    77F127FB: PUSH EAX
    77F127FC: MOV DWORD PTR FS:[00000000h] , ESP
    ; Сохранить 12 байтов для локальных переменных.
    77F12803: SUB ESP , 00Ch
    ; Сохранить значения регистра, которые будут разрушены
    ; как часть этой функции
    77F12806: PUSH EBX
    77F12807: PUSH ESI
    77F12808: PUSH EDI
    Сохранить текущую вершину стека в локальной переменной.
    Эта строка - также часть установки SEH.
    7F12809: MOV DWORD PTR [EBP-018h] , ESP
    Инициализировать эту локальную переменную к 0. Эта строка указывает,
    что функция входит в блок _try.
    77F1280C: MOV DWORD PTR [EBP-004h] , 00000000h
    Первый шаг после установки должен получить длину строки
    копирования. Строка копирования - второй параметр.
    Переместить второй параметр (строку, которая будет скопирована) в EDI.
    77F12813: MOV EDI , DWORD PTR [EBP+OOCh]
    Istrcpy будет просматривать 4,294,967,295 байтов до NULL-терминатора.
    EDX используется позже со значением -1, а здесь он инициализируется.
    Помните, что REPNE SCAS использует регистр ЕСХ как счетчик цикла.

    77F12816: MOV EDX , FFFFFFFFh

    77F1281B: MOV ЕСХ , EDX

    ; Обнуление EAX, так что SCAS будет искать символ NULL.

    77F1281D: SUB EAX , EAX ; Поиск символа NULL.

    77F1281F: REPNE SCAS BYTE PTR [EDI]

    ; Поскольку ЕСХ считает в обратном направлении, переключить все биты так,

    ; чтобы длина строки оказалась в ЕСХ. Эта длина включает символ NULL.

    77F12821: NOT ЕСХ

    ;Поскольку REPNE SCAS инкрементирует также и EDI, вычесть длину строки

    ;из EDI так, чтобы EDI указывал обратно, на начало строки.

    77F12823: SUB EDI , ЕСХ

    Держать длину строки в ЕАХ.

    77F12825: MOV ЕАХ , ЕСХ

    ;Переместить второй параметр в ESI, т.к. ESI является исходным

    ;операндом строчных инструкций.

    77F12827: MOV ESI , EDI

    ;Переместить первый параметр (целевую строку) в EDI.

    77F12829: MOV EDI , DWORD PTR [EBP+008h]

    ;Длина строки была подсчитана в байтах. Делите длину строки на 4,

    ;чтобы получить число двойных слов (DWORDS). Если число символов

    ;нечетное, REPE MOVS не будет копировать их всех.

    ;Любые остающиеся байты

    ;копируются прямо после REPE MOVS.

    77F1282C: shr ЕСХ , 002h

    ;Копировать строку второго параметра в строку первого.

    77F1282F: REPE MOVS DWORD PTR [EDI] , DWORD PTR [ESI]

    ;Переместить сохраненную длину строки в ЕСХ.

    77F12831: MOV ЕСХ , ЕАХ

    ;Операция AND счетчика с числом 3, чтобы получить

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

    77F12833: AND ЕСХ , 00Зh

    ;Копировать остающиеся байты из строки в строку.

    77F12836: REPE MOVS BYTE PTR [EDI] , BYTE PTR [ESI]

    Istrcpy возвращает первый параметр, поэтому переместить

    ;возвращаемое значение в ЕАХ

    77F12838: MOV ЕАХ , DWORD PTR [EBP+008h]

    ;Установить локальную переменную в -1, указывая, что функция

    ;покидает этот блок try/except.

    77F1283B: MOV DWORD PTR [EBP-004h] , EDX

    ;Функция завершена; выйдите и переместитесь домой.

    77F1283E: JMP 77F12852h

    ;Если вы просмотрите эту функцию, то заметите, что фактически нет

    ;инструкции, которая переходит или ветвится по адресу 0x77F12840.


    Этот

    ; адрес является частью фильтра исключений кадра SEH. Фильтр исключений -

    ;это часть кода, которая сообщает возврату из SEH, что нужно делать.

    ;Здесь фильтр исключений эквивалентен функции

    ;_except (EXCEPTION_EXECUTE_HANDLER). Код возврата должен выполнить

    ;обработчик, который расположен справа от инструкции RET. Подробные

    ;сведения о фильтрах исключений можно найти в MSDN

    ;или в книге Джеффри Рихтера

    ;"Программирование приложений для Microsoft Windows" (Jeffrey Richter,

    ;"Programming Applications for Microsoft Windows".-

    ;Microsoft Press, 1999)

    77F12840: MOV EAX , 00000001h

    77F12845: RET

    ; Следующие три инструкции — блок исключения для функции.

    ; Восстановить стек, сохраненный ранее.

    77F12846: MOV ESP , DWORD PTR [EBP-018h]

    ; Установить локальную переменную в -1, указывая, что функция

    ; покидает этот блок try/except.

    77F12849: MOV DWORD PTR [EBP-004h] , FFFFFFFFh

    ; Установить 0 в качестве неуспешного возвращаемого значения.

    77F12850: XOR ЕАХ , ЕАХ

    ; Получить предыдущий SEH-кадр.

    77F12852: MOV ECX , DWORD PTR [EBP-010h]

    ; Восстановить предварительно сохраненный в стеке EDI.

    77F12855: POP EDI

    ; Отменить SHE-кадр.

    "7F12856: MOV DWORD PTR FS: [00000000h] , ECX

    ; Восстановить предварительно сохраненный в стеке ESI.

    77F1285D: POP ESI

    ; Восстановить EBI, предварительно сохраненный в стеке.

    77F1285E: POP EBX

    ; Отменить установку нормального стекового кадра

    77F1285F: MOV ESP , ЕВР

    77F12861: POP EBP

    ; Возврат в вызывающую программу и очистка 8 байтов стека.

    ; Istrcpy is a __sdtcall function.

    77F12862: RET 00008h

    Пример соглашений о вызовах

    В листинге 6-1 показан пример всех соглашений о вызовах из окна Disassembly отладчика Visual C++. В нем объединены все инструкции, рассмотренные до настоящего момента, и соглашения о вызовах. Исходный код примера (CALLING.CPP) находится на сопровождающем компакт-диске.
    Для облегчения просмотра код листинга 6-1 имеет отладочную структуру; кроме того, код фактически ничего не делает. Каждая функция просто вызывается с подходящим соглашением о вызове. Обратите особое внимание на то, как размещены параметры в функциях, и как очищается стек. Чтобы сделать листинг более легким для чтения, между функциями вставлены инструкции NOP.
    Листинг 6-1 Пример соглашений о вызовах
    6: // Строки , передаваемые каждой функции -
    7: static char * g_szStdCall = "_stdcall";
    8: static char * g_szCdeclCall = "_cdecl";
    9: static char * g_szFastCall = "_fastcall" ;
    10: static char * g_szNakedCall = "_naked" ;
    11:
    12: // extern "С" отключает всю декорацию имен C++ .
    13: extern "С"
    14: {
    15: .
    16: // _cdecl-функция
    17: void CDeclFunction { char * szString ,
    18: unsigned long ulLong ,
    19: char chChar ) ;
    20:
    21: // stdcall-функция
    22: void _stdcall StdCallFunction ( char * szString ,
    23: unsigned long ulLong ,
    24: char chChar ) ;
    25: // _fastcall-функция
    26: void _fastcall FastCallFunction ( char * szString ,
    27: unsigned long ulLong ,
    28: char chChar ) ;
    29:
    30: /'/ "Голая" функция. Нет спецификатора ни для определения,
    31: //ни для декларирования функции.
    32: int NakedCallFunction ( char * szString ,
    33: unsigned long ulLong ,
    34: ' char chChar ) ;
    35: }
    36:
    37: void main ( void )
    38: {
    00401000 55 push ebp
    00401001 8B EC mov ebp,esp
    00401003 53 push ebx
    00401004 56 push esi
    00401005 57 push edi
    39: // Вызвать каждую функцию для генерации кода. Я разделяю
    40: // каждую функцию парой NOP-байтов, чтобы облегчить чтение
    41: // кода дизассемблера
    42: _asm NOP _asm NOP
    00401006 90 nор

    00401007 90 nор
    43: CDeclFunction ( g_szCdeclCall , 1 , 'а' ) ;
    00401008 6А 61 push 61h
    0040100A 6A 01 push 1
    0040100C Al 14 30 40 00 mov eax,[g_szCdeclCall (00403014)]
    00401011 50 push eax
    00401012 E8 45 00 00 00 call CDeclFunction (0040105с)
    00401017 83 C4 ОС add esp,OCh
    44: _asm NOP _asm NOP
    0040101A 90 nор
    0040101B 90 nор
    45: StdCallFunction ( g_szStdCall , 2 , 'b' ) ;
    0040101C 6A 62 push 62h
    0040101E 6A 02 push 2
    00401020 8B OD 10 30 40 00 mov ecx,dword ptr
    [g_szStdCall (00403010)]
    00401026 51 push ecx
    00401027 E8 3D 00 00 00 call StdCallFunction (00401069)
    46: _asm NOP _asm NOP
    0040102C 90 nор
    0040102D 90 nор
    47: FastCallFunction ( g_szFastCall , 3 , 'c' ) ;
    0040102E 6A 63 push 63h
    00401030 BA 03 00 00 00 mov edx,3
    00401035 8B OD 18 30 40 00 mov ecx,dword ptr
    [g_szFastCall (00403018)]
    0040103В Е8 38 00 00 00 call FastCallFunction (00401078)
    48: _asm NOP _asm NOP
    00401040 90 nор
    00401041 90 nор
    49: NakedCallFunction ( g_szNakedCall , 4 , 'd' ) ;
    00401042 6A 64 , push 64h
    00401044 6A 04 push 4
    00401046 8B 15 1C 30 40 00 mov edx,dword ptr
    [g-_szNakedCall (0040301с)]
    0040104C 52 push edx
    0040104D E8 40 00 00 00 call NakedCallFunction (00401092)
    00401052 83 C4 ОС add esp,OCh
    50: _asm NOP _asm NOP
    00401055 90 nор
    00401056 90 nор
    51:
    52: }
    00401057 5F pop edi
    00401058 5E pop esi
    00401059 5B pop ebx
    0040105A 5D pop ebp
    0040105В СЗ ret
    53:
    54: void CDeclFunction ( char * szString ,
    55: unsigned long ulLong ,
    56: char chChar )
    57: {
    0040105C 55 push ebp
    0040105D 8B EC mov ebp,esp
    0040105F 53 push ebx
    00401060 56 push esi
    00401061 57 push edi 58: _asm NOP _asm NOP
    00401062 90 nор
    00401063 90 nор
    59: }
    00401064 5F pop edi
    00401065 5E pop esi
    00401066 5B pop ebx
    00401067 5D pop ebp
    00401068 C3 ret
    60:
    61: void _stdcall StdCallFunction ( char * szString ,
    62: unsigned long ulLong ,
    63: char chChar )
    64: {
    00401069 55 push ' ebp
    0040106A-8B EC mov ebp,esp
    0040106C 53 push ebx


    0040106D 56 push esi
    0040.106E 57 push edi
    65: _asm NOP _asm NOP
    0040106F 90 nор
    00401070 90 nор
    66: }
    00401071 5F pop edi
    00401072 5E pop esi
    00401073 5B pop ebx
    00401074 5D pop ebp
    00401075 C2 ОС 00 ret OCh
    67:
    68: void _fastcall FastCallFunction ( char * szString ,
    69: unsigned long ulLong ,
    70: char chChar )
    71: {
    00401078 55 push ebp
    00401079 8B EC mov ebp,esp
    0040107B 83 EC 08 sub , esp,8
    0040107E 53 push ebx
    0040107F 56 push esi
    00401080 57 push edi
    00401081 89 55 F8 mov dword ptr [ebp-8],edx
    00401084 89 4D FC mov dword ptr [ebp-4],ecx
    72: _asm NOP _asm NOP
    00401087 90 nор
    00401088 90 nор
    73: }
    00401089 5F pop edi
    0040108A 5E pop esi
    0040108В 5В pop ebx
    0040108C 8В Е5 , mov esp,ebp
    0040108Е 5D pop ebp
    0040108F C2 04 00 ret 4
    74:
    75: _declspec(naked) int NakedCa11Function ( char * szString ,
    76: unsigned long ulLong ,
    77: . char chChar )
    78: {
    00401092 90 nор
    00401093 9.0 nор
    79: _asm NOP _asm NOP
    80: // Голые функции должны явно выполнять возврат.
    81: _asm RET
    00401094 СЗ ret

    Просмотр параметров в стеке

    В главе 5 показано, как устанавливать точки прерывания на системных и экспортируемых функциях. Одна из главных причин для установки точек прерывания на этих функциях — необходимость просматривать параметры, которые передаются в данную функцию.
    С тех пор как Visual Basic 5 позволил получать "родной" код (native code), я хотел посмотреть, как работает "родная" компиляция. Каталог Visual Basic включал файлы LINK.EXE и С2.ЕХЕ. Эти две программы являются также частью Visual C++, и было любопытно посмотреть, как их использует Visual Basic, как вообще работает компиляция. Как можно понять из названия, LINK.EXE связывает объектные файлы и производит выполняемый двоичный файл. С2.ЕХЕ довольно сложен. В системе Visual C++ файл С2.ЕХЕ — это генератор кода, который производит машинный код.
    Из интегрированной среды разработки (IDE) Visual C++ я открыл VB6.EXE как программу для отладки. Поскольку были загружены символы, нужно было установить точку прерывания на {,, kernel32}_CreateProcessA@40. Запустив Visual Basic, я создал простой проект, установил свойства 'Проекта для создания "родного" кода и выбрал команду File|Make из IDE Visual Basic. Точка прерывания на _CreateProcessA@4о,обеспечивает управление отладчика при запуске или С2.ЕХЕ, или LINK.EXE.
    В Windows 2000 RC2 точка прерывания на _CreateProcessA@40 останавливает отладчик на адресе 0x77E8D7E6, когда инструкция, подлежащая выполнению (PUSH EBP), устанавливает стандартный кадр стека. Поскольку точка останова находится на первой инструкции функции createProcess, вершина стека содержит параметры и адрес возврата. Затем при помощи команды View|Debug Wmdows|Memory я открыл окно Memory и ввел в поле Address строку ESP, которая является именем регистра указателя стека, чтобы просмотреть содержимое стека.
    По умолчанию данные в окне Memory отображаются в байтовом формате. Поиск при этом может быть довольно утомительным. Щелчок правой кнопкой мыши в окне Memory позволяет выбрать формат: byte, short hex (2 байта или WORD) и long hex (4 байта или DWORD).

    На рис. 6.4 показан стек в окне Memory отладчика в начале точки прерывания на функции CreateProcess. Первое значение — адрес возврата для инструкции OxFB6B3F6J 10 следующих — Параметры фуyrwbb CreateProcess (СМ. табл. 6.5). Параметры функции CreateProcess занимают 40 байт, а каждый параметр имеет длину 4 байта. Стек растет от старших адресов памяти к младшим, а параметры помещаются в стек справа налево, поэтому параметры появляются в окне Memory в том же порядке, как в определении функции.

    Просмотр параметров в стеке
    Рис. 6.4. Стек в окне Memory отладчика Visual C++

    Просматривать индивидуальные значения первых двух параметров можно двумя способами. Первый состоит в том, чтобы использовать окно Memory, переключая его в байтовый формат и рассматривая конкретный адрес. Второй, более легкий способ, состоит в том, чтобы буксировать (мышью) адрес, который требуется рассмотреть, в окно Watch. В окне Watch для просмотра адреса следует использовать оператор приведения типов. Например, чтобы просмотреть параметр ipAppiicationName в примере, надо поместить в окно Watch строку
    0х0012ЕАС4 "c:\vb\C2.EXE"

    0х0012ЕВС4 "С@ -11 "e:\temp\VB815574

    -f "c:\junk\vb\Forml.frm _W 3 _Gy _G5

    -GS4096 _dos _Z1

    -Fo"c:\junk\vb\Forml.OBJ" _Zi _QIfdiv

    -ML _basic"

    Таблица 6.5. Параметры, которые VB6.EXE передает В функцию CreateProcess

    Значение

    Тип

    Параметр

    0x001 2ЕАС4

    LPCTSTR

    IpApplicationName

    0x0012EBC4

    LPTSTR

    IpCommandLine

    0x00000000

    LPSECURITY_ATTRIBUTES

    IpProcessAttributes

    0x00000000

    LPSECURITY_ATTRIBUTES

    IpThreadAttributes

    0x00000001

    BOOL

    blnheritHandles

    0x08000000

    DWORD

    dwCreationFlags

    0x00000000

    LPVOID

    IpEnvi r onment

    0x00000000

    LPCTSTR

    IpCurrentDirectory

    0x001 2EA3C

    LPSTARTUPINFO

    IpStartupInfo

    0x001 2EC60

    LPPROCESS_INFORMATION

    IpProcessInformation

    Получение предшествующих параметров не составило труда, потому что функция была остановлена на первой инструкции, прежде чем она поместила в стек дополнительные элементы. Для проверки параметров в середине функции нужно проделать немного больше работы. Например, найти положительные смещения относительно ЕВР. Иногда же лучше просто открыть окно Memory и начать просмотр.

    Регистры и окно Watch

    Окно Watch отладчика Visual C++ "знает", как нужно декодировать мнемонику всех регистров в их значения. Поэтому, просматривая, например, инструкцию, манипулирующую со строками, можно ввести в окно Watch строку (char*)@EDi, чтобы просматривать данные в формате, который легче читать.


    Регистры

    Именно через регистры передается приложению каждый бит данных, который оно обрабатывает, и знание роли каждого регистра поможет вам распознать неудачный участок кода. CPU x86 имеют восемь регистров общего назначения (ЕАХ, ЕВХ, ЕСХ, EDX, ESI, EDI, ESP и ЕВР), шесть сегментных регистров (CS, DS, ES, SS, FS и GS), указатель инструкций (команд) EIP и регистр флагов (EFLAGS). Есть и другие регистры, такие как регистры отладки и управляющие машинные регистры, но это регистры специального назначения, и вы не встретитесь с ними при нормальной отладке в режиме пользователя. Все регистры общего назначения, перечисленные в табл. 6.1, являются 32-разрядными. Заметьте, что некоторые из них допускают мнемонические обозначения для доступа к различным частям полного 32-разрядного регистра. Единственный сегментный регистр, представляющий интерес для данной главы, — это регистр FS, который содержит блок информации потока (TIB), содержащий описание текущего выполняемого потока. Используются и другие сегментные регистры, но операционная система конфигурирует их таким образом, что они оказываются прозрачными по отношению к нормальной операции. Указатель инструкции содержит адрес текущей выполняющийся инструкции.
    Таблица 6.1. Регистры общего назначения
    32-разрядный регистр
    16-разрядный доступ
    Доступ к младшему байту (биты 0-7)
    Доступ к старшему байту (биты 8-1 5)
    Специфика использования
    ЕАХ
    АХ
    AL
    АН
    Здесь хранятся возвращаемые значения целых функций
    ЕВХ
    ВХ
    BL
    ВН
    Здесь хранится базовый адрес объекта в памяти
    ЕСХ
    СХ
    CL
    СН
    Эти регистры используются счетчиками инструкций циклов
    EDX
    DX
    DL
    DH
    Здесь хранятся 32 старших бита 64-битных значений
    ESI
    SI


    Здесь хранится исходный адрес инструкций перемещения или сравнения в памяти
    EDI
    DI


    Здесь хранится целевой адрес инструкций перемещения или сравнения в памяти
    ESP
    SP


    Указатель стека. Этот регистр изменяется неявно при вызове функции, возврате из функции, отведении места в стеке для локальных переменных и очистке стека

    EBP

    ВР





    Указатель база/кадр. Этот регистр содержит адрес стекового кадра для процедуры

    Регистр флагов EFLAGS содержит флажки состояний и флажки управления. Биты EFLAGS устанавливаются различными инструкциями, чтобы указать результат их выполнения. Например, бит ZF (Zero Flag — нулевой флажок) равен 1, если результатом выполнения инструкции является нуль (0). В главе 4 описан перевод CPU в пошаговый режим (single-step mode), который включал установку бита TF (Trap Rag— флажок трассировки) в регистре EFLAGS. На рис. 6.1 приведено окно регистров Registers отладчика Visual C++. Окно Registers показывает регистр EFLAGS как EFL. Заметьте, что регистры с плавающей точкой в окне Registers не отображены. Их можно скрыть щелчком правой кнопки мыши в окне Registers и сбросом пункта Floating-Point Registers в раскрывшемся контекстном меню.

    В табл. 6.2 описаны флажки, показанные в окне Registers. К сожалению, документация Visual C++ не раскрывает значения этих флажков, а мнемоника, применяемая для них в системе Visual C++, не соответствует мнемонике Intel, так что при ссылках на документацию Intel нужна соответствующая трансляция.

    Регистры
    Рис. 6.1. Окно Registers из Visual C++

    С окном Registers связана одна незначительная особенность: при обновлении флажков цвет значения регистра EFL не изменяется (в отличие от обычных регистров, значения которых при обновлениях выделяются другим цветом). Но необходимость просматривать индивидуальные значения флажков возникает довольно редко. Для облегчения разметки изменяющихся флажков можно сделать следующее: нажать кнопку New Text File (в панели Standard) и открыть новый временный файл. Затем скопировать (в буфер обмена) существующие флажки из окна Registers и вставить их в окно временного текста, чтобы сравнить их значения до и после изменения.

    Таблица 6.2. Значения флажков окна Registers

    Флажок окна Registers

    Значение

    Мнемоника в руководстве Intel

    Примечания

    OV

    Overflow Flag (флажок переполнения)

    OF

    Устанавливается в 1, если операция закончилось целым переполнением или потерей значимости

    UP

    Direction Flag (флажок направления)

    DF

    Устанавливается в 1, если строчная инструкция выполняет обработку в направлении от самого высокого до самого низкого адреса (автодекремент). 0 означает, что строчная инструкция выполняет обработку в противоположном направлении: от самого низкого до самого высокого адреса (автоприращение)

    El

    Interrupt Enable Flag (флажок разрешения прерываний)

    IF

    Устанавливается в 1, если прерывания разрешены. Этот флажок будет всегда включен (1) в пользовательском режиме отладчика

    PL

    Sign Flag (флажок знака)

    SF

    Отображает самый старший (знаковый) бит результата инструкции. Устанавливается в 0 для положительных значений или в 1 — для отрицательных

    ZR

    Zero Flag (нулевой флажок)

    ZF

    Устанавливается в 1, если результатом инструкции является 0 (нуль). Этот флажок важен для инструкций сравнения

    AC

    Auxiliary Carry Flag (вспомогательный флажок переноса)

    AF

    Устанавливается в 1, если двоично-десятичная (BCD) операция генерирует служебный перенос или заем старшего разряда

    PE

    Parity Flag ' (флажок четности)

    PF

    Устанавливается в 1, если наименее значимый (младший) байт результата содержит четное число бит, установленных в 1

    CY

    Carry Flag (флажок переноса)

    CF

    Устанавливается в 1, если арифметическая операция генерирует перенос или заем из наиболее значащего (знакового) бита результата. Устанавливается в 1 также при переполнении в операциях целочисленной арифметики без знака

    Еще одно важное свойство окна Registers — в нем можно редактировать значения регистров. Для этого нужно поместить курсор на первую цифру изменяемого числа (справа от знака равенства для соответствующего регистра) и ввести новое значение.

    Самые общие простые инструкции

  • MOV переместить
  • Инструкция MOV применяется чаще всего потому, что она предоставляет возможность перемещать данные из одного места в другое. Только что было показано, как обменять значения двух регистров, используя лишь инструкции PUSH. Теперь вы увидите, как выполнить тот же самый обмен с помощью команды MOV.
    SwapRegisters ( void )
    { _asm
    {
    // Регистр EAX используется как временное хранилище.
    // Обмен значений регистров ЕСХ и ЕВХ.
    MOV ЕАХ , ЕСХ
    MOV ECX , EBX
    MOV EBX , EAX
    }
  • SUB вычитание
  • Инструкция SUB реализует операцию вычитания. Значение исходного операнда (второго) вычитается из значения целевого (первого) операнда, а результат сохраняется в целевом операнде.
  • ADD сложение
  • Инструкция ADD добавляет значение исходного операнда (второго) к значению целевого операнда (первого) и сохраняет результат в целевом операнде.
  • INT 3 точка прерывания
  • Инструкция INT 3 — это команда прерывания для Intel CPU. Компиляторы Microsoft используют эту инструкцию как заполнитель между функциями в файле. Подобное заполнение поддерживает выравнивание РЕ-секций (Portable Executable sections), базирующееся на ключе /ALIGN компоновщика (по умолчанию такое выравнивание производится по границе 4 Кбайтовых областей).
  • LEAVE выход из процедуры высокого уровня
  • Инструкция LEAVE восстанавливает состояние CPU при выходе из функции. Подробнее она рассмотрена в следующем разделе.


    Соглашения о вызовах

    Прежде чем перейти к другим инструкциям, сделаем краткий обзор соглашений о вызовах. Несколько инструкций, представленных в предыдущем разделе, могут обеспечить первоклассную отладку. Однако, чтобы показать, как нужно расшифровывать окно Disassembly, необходимо связать воедино процедуру вызова и соглашения о вызовах.
    Соглашение о вызовах указывает, как нужно пересылать параметры в функцию и как происходит очистка стека, когда выполняется возврат из функции. Соглашение о вызовах диктует программист, который кодирует функцию. Этому соглашению должен следовать каждый, кто вызывает эту функцию. CPU не диктует каких-либо специальных соглашений о вызовах. Понимание этих соглашений облегчает отыскание параметров в окне Memory и определение потока операций языка ассемблера в окне Disassembly.
    Существует всего пять соглашений о вызовах, но только три из них — наиболее общие: стандартный вызов (__stdcall), С-декларация (__cdecl) и this-вызов. Стандартный вызов и С-декларацию программист может указывать самостоятельно, а this-вызов применяется в С-программе автоматически, когда он задает способ передачи этого указателя. Два других соглашения о вызовах — это быстрый вызов (_fastcalll) и так называемые "голые" (naked) соглашения о вызовах. По умолчанию операционные системы Win32 не используют соглашение быстрого вызова в коде пользовательского режима, потому что оно не переносимо на другие CPU. Соглашение о "голых" вызовах применяется для программирования драйверов виртуальных устройств (VxD) и в тех случаях, когда программист хочет самостоятельно управлять прологом и эпилогом (об этом будет рассказано в главах, 12 к 14).
    В табл. 6.3 перечисляются все соглашения о вызовах. Вспомним описание схемы декорирования1 имен для установки точек прерывания на системных функциях из главы 5. Из табл. 6.3 ясно, что схему декорирования имен диктует соглашение о вызовах.
    "Декорированное" имя в C++ — это генерируемая компилятором строка, содержащая, кроме собственно имени, символы, используемые компилятором или компоновщиком для получения информации о типе.
    — Пер.

    Читатель, никогда не встречавшийся с соглашениями о вызовах, может задаться вопросом: почему существуют различные их типы?. Различия между вызовами _cdecl и _stdcall довольно тонкие. При стандартном вызове вызываемая функция очищает стек, поэтому она должна точно "знать" количество ожидаемых параметров. В связи с этим функция стандартного вызова не может иметь переменного числа аргументов (как, например, printf). Поскольку для функций _cdeci стек очищает вызывающая программа, функции с переменным числом аргументов допустимы. Стандартный вызов используется по умолчанию для системных функций Win32, а также для функций языка Visual Basic.

    Таблица 6.3. Соглашения о вызовах

    Соглашение о вызове

    Передача параметров

    Поддержка стека

    Декорирование имен

    Замечания

    _cdecl

    Справа налево

    Аргументы из стека удаляет вызывающая программа. Это единственное соглашение о вызовах, которое допускает переменные списки аргументов

    Символ подчеркивания в качестве префикса перед именами функций, как в_Роо

    Используется по умолчанию для функций С и C++

    _ stdcall

    Справа налево

    Свои собственные аргументы из стека удаляет сама вызванная функция

    Символ подчеркивания в качестве префикса перед именами функций и суффикс @ , за которым следует десятичное число байт в списке аргументов, как в_Роо@12

    Используется почти всеми системными функциями и, по умолчанию, внутренними функциями Visual Basic

    _ fastcall

    Два первых DWORD-параметра передаются в регистрах ЕСХ и EDX; остальные передаются справа налево

    Аргументы из стека удаляет вызывающая функция

    Префикс @ перед именем и суффикс @ после него, за которым следует десятичное число байт в списке аргументов, как в ®Foo@12

    Применяется только в Intel CPU. Это соглашение о вызовах используется по умолчанию для компиляторов Borland Delphi

    this

    Справа налево. Параметр this передается в регистре ЕСХ

    Аргументы из стека удаляет вызывающая функция

    Нет

    Используется автомат-ски методами классов C++, если не указан стандартный вызов. Все СОМ-методы объявляются со стандартным вызовом




    naked

    Справа налево

    Аргументы из стека удаляет вызывающая функция

    Нет

    Используются драйверами виртуальных устройств VxD и когда программист нуждается в собственном прологе и эпилоге



    и специальные приемы, которые могут

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

    Создание или уничтожение SEH-кадра

    Первые инструкции после установки кадра стека часто похожи на следующий фрагмент, который является стандартным кодом, начинающим _try-блок. Первый узел в цепочке SEH-обработчиков расположен в TIB со смещением 0. В показанном ниже фрагменте кода дизассемблера компилятор помещает в стек значение данных и указатель на функцию _except_ handlers. Первая инструкция MOV обращается к TIB. Смещение 0 указывает, что узел добавляется к вершине цепочки исключений. Две последних инструкции показывают, каким образом код перемещает фактический узел в цепочку
    PUSH 004060d0
    PUSH 004014a0
    MOV EAX , FS:[00000000]
    PUSH EAX
    MOV DWORD PTR FS:[0] , ESP
    Хотя этот пример довольно прост и понятен, компилятор не всегда производит такой аккуратный код. Иногда он расширяет область создания SEH-кадра. В зависимости от флажков генерации кода и оптимизации, компилятор перемещает окружающие инструкции так, чтобы усилить преимущества конвейерной обработки CPU. Следующий пример дизассемблерного кода, в котором загружены символы библиотеки KERNEL32.DLL, демонстрирует запуск функции IsBadReadPtr из Microsoft Windows NT 4.
    MOV EAX , FS:[00000000h]
    PUSH EBP
    MOV EBP , ESP
    PUSH 0FFh
    PUSH 77F3DlE8h
    PUSH _except_handler3
    PUSH EAX
    MOV EAX , [BaseStaticServerData];
    MOV DWORD PTR FS:[0000000h] , ESP
    Как показывает следующий фрагмент кода, уничтожение SEH-кадра намного проще, чем его создание. Основной момент, который нужно запомнить, это то, что запись FS: [0] означает доступ к SEH.
    MOV ЕСХ , DWORD PTR [EBP-lOh] .
    MOV DWORD PTR FS:[0] , ECX
    Организация доступа к TIB
    Значение в FS:[18] является линейным адресом структуры TIB. В следующем фрагменте кода реализация GetCurrentThreadid (из Windows 2000) получает сначала линейный адрес TIB-блока и затем, в позиции со смещением 0x24 (в Т1В-блоке) — фактический идентификатор (ID) потока.
    GetCurrentThreadid:
    MOV EAX , FS:[00000018h]
    MOV EAX , DWORD PTR [EAX+024h]
    RET
    Доступ к локальному хранилищу потока
    Локальное хранилище потока (Thread Local Storage — TLS) — это механизм Win32, который позволяет каждому потоку (в многопоточной ситуации) иметь свой собственный экземпляр глобальных переменных. Указатель на массив локального хранилища потока размещается в структуре TIB со смещением 0х2С. Следующий фрагмент кода дизассемблера показывает, как получить доступ к указателю локального хранилища потока.
    MOV ЕСХ , DWORD PTR FS:[2Ch]
    MOV EDX , DWORD PTR [ECX+EAX*4]


    Сравнение и проверка

  • СМР сравнить два операнда
  • Инструкция СМР сравнивает первый и второй операнды, вычитая второй из первого, сбрасывая результаты и устанавливая соответствующие флаги в регистре EFLAGS. Можно представлять инструкцию СМР как условную часть С-оператора if. В табл. 6.4 приведены различные флаги и значения, которым они соответствуют при выполнении инструкции СМР.
    Таблица 6.4. Результирующие значения и установки флажков инструкции СМР
    Результат (сравнения первого операнда со вторым)
    Установки флажков регистра EFLAGS
    Установки флажков руководства Intel
    Равно
    Меньше чем
    ZR = 1
    PL != OV
    ZF=1
    SF != OF
    Больше чем
    ZR = 0 and PL = 0V
    ZF = 0 и SF = OF
    Не равно
    ZR = 0
    ZF = 0
    Больше чем или равно
    PL = OV
    SF = OF
    Меньше чем или равно
    ZR = 1 или PL != 0V
    ZF = 1 или SF != OF
  • TEST логическое сравнение
  • Инструкция TEST выполняет поразрядную операцию "логическое И" над своими операндами и устанавливает флажки PL, ZR и РЕ (SF, ZF и PF для руководств Intel) соответственно. Инструкция TEST проверяет, было ли установлено разрядное значение.


    Ссылки на структуры и классы

    Поскольку структуры и классы играют значительную роль в Windows-разработках, потратим некоторое время на обсуждение проблем доступа к соответствующей памяти. Хотя со структурами и классами удобно иметь дело на языках высокого уровня, на уровне языка ассемблера они, вообще говоря, не существуют. На языках высокого уровня структура и класс — это просто краткие способы указывать смещения в "блобе" памяти.
    "Блоб" (Binary Large Object — BLOB). Во-первых, этим термином разработчики баз данных обозначают любой произвольный битовый блок, который следует сохранить в БД, например картинку или звуковой файл. Существенно, что BLOB является объектом, который не может быть интерпретирован средствами СУБД. Во-вторых, употребляется в значении глагола (to blob) и в этом случае переводится как "послать огромный e-mail", обычно в ответ на нанесенное серьезное оскорбление. Часто используется в качестве умеренной угрозы. Например: "If that program crashes again, I'm going to BLOB the core dump to you" (Если эта программа опять "грохнется", я тебе вышлю дамп ядра по мэйлу). См. Файл Жаргона (The Jargon File). -Ред.
    Компиляторы, в основном, размещают память для пользовательских структур и классов так, как указано в спецификациях. Иногда компилятор дополняет поля заполнителями, чтобы сохранять их на естественных границах памяти, которые для CPU x86 кратны 4 или 8 байтам.
    Ссылки на структуры и классы обозначаются регистром и смещением памяти. В структуре Mystruct, приведенной ниже, комментарии справа показывают смещение каждого элемента от начала структуры. За определением MyStruct расположены различные способы доступа к полям структуры.
    typedef struct tag_MyStruct
    {
    DWORD dwFirst ; // 0-байтное смещение
    char szBuff[ 256 ] ; // 4-байтное смещение
    int iVal ; // 260-байтное смещение
    } MyStruct , * PMyStruct ;
    void FillStruct ( PMyStruct pSt )
    {
    char szName[] = "Pam\n" ;
    _asm
    {
    MOV EAX , pSt // Поместить pSt в EAX.
    Ниже используются прямые

    // смещения на языке ассемблера, чтобы показать,

    // на что они похожи в дизассемблере.

    // Встроенный ассемблер позволяет применять

    // нормальные ссылки формата ..

    // С-код: pSt->dwFirst = 23 ;

    MOV DWORD PTR [EAX] , 17h.

    // С-код: pSt->iVal = 0x33 ;

    MOV DWORD PTR [EAX + 0104h] , 0x33

    // С-код: strcpy ( pSt->szBuff , szName ) ;

    LEA ECX , szName // Поместить szName в стек.

    PUSH ECX .

    LEA ECX , [EAX + 4] //. Получить доступ к полю szBuff.

    PUSH ECX

    CALL strcpy

    ADD ESP ,8 // strcpy есть _cdecl функция.

    // С-код: pSt->szBuff[ 1 ] = 'A' ;

    MOV BYTE PTR [EAX + 5] , 41h

    // С-код: printf ( pSt->szBuff ) ;

    MOV EAX , pSt // Получить pSt обратно. EAX был разрушен

    // при обращении к strcpy.

    LEA ECX , [EAX + 4]

    PUSH ECX

    CALL DWORD PTR [printf]

    ADD ESP , 4 // printf есть _cdecl-функция.

    }

    }

    Встроенный ассемблер Visual C++

    Прежде чем перейти к изучению инструкций языка ассемблера, поговорим немного о встроенном ассемблере Visual C++. Подобно большинству "профессиональных компиляторов C++, компилятор Visual C++ позволяет внедрить инструкции языка ассемблера прямо в строки исходного кода С и C++. Хотя в общем случае использование языка встроенного ассемблера не рекомендуется, потому что он ограничивает мобильность кода, иногда это единственный способ выполнить задачу. В главах 12 и 14 будет показано, как можно заполучить в программу импортированные функции, используя язык встроенного ассемблера.
    В начале говорилось, что не обязательно знать, как писать программы на языке ассемблера, и здесь нет противоречия. Применять встроенный ассемблер — не то же самое, что писать на ассемблере полную программу — код C/C++ обеспечивает инфраструктуру приложения. Встроенный ассемблер облегчает понимание примеров и позволяет экспериментировать с инструкциями, наблюдая за их действием.
    Представим первую инструкцию:
  • NOP нет операции
  • NOP — это инструкция, которая ничего не делает. Компилятор иногда использует NOP как заполнитель внутри функций, чтобы держать функции, выровненными на подходящие границы памяти.
    Строка встроенного ассемблера начинается с ключевого слова _asm, за которым следует инструкция языка ассемблера, которую нужно выполнить (или несколько инструкций в фигурных скобках). Две следующие подпрограммы функционально эквивалентны и показывают формат инструкций встроенного ассемблера:
    void NOPFuncOne ( void )
    {
    _Asm NOP
    _Asm NOP
    }
    void NOPFUncTwo ( void )
    {
    _Asm
    {
    NOP
    NOP
    }
    }
    В этой главе встроенный ассемблер используется для иллюстрации операций доступа к параметрам и переменным. Все рассмотренные ниже примеры с языком ассемблера содержатся в программе ASMer на сопровождающем компакт-диске.


    Вызов и возврат из процедур

    CALL вызов процедуры
    RET возврат из процедуры
    Теперь, поговорив о том, как выглядят процедуры, посмотрим, как нужно их вызывать и возвращаться из них. Инструкция CALL проста. Она неявно помещает адрес возврата в стек, так что, остановившись на первой инструкции вызванной процедуры и посмотрев на ESP, на вершине стека можно увидеть адрес возврата.
    Операндом инструкции CALL может быть почти все, и в окне Disassembly отображаются вызовы, в параметрах которых указаны регистры, ссылки на память, параметры и глобальные смещения. Если в качестве параметра CALL выступает указатель с адресной ссылкой на память, то, для того чтобы точно увидеть процедуру, которую вы собираетесь вызвать, можно использовать поле эффективного адреса в окне Registers.
    Вызов локальной функции будет прямым обращением по определенному адресу. Однако чаще можно видеть вызовы, выполняющиеся через указатели, которые, в общем случае, являются обращениями к импортированным функциям через таблицу адресов импорта (Import Address Table — IAT). Если загружены символы двоичного файла, через который выполняется пошаговый проход, то вы увидите нечто вроде первой инструкции CALL, показанной ниже в примере функции CallSomeFunctions. Этот код указывает, что вызов выполняется через IAT (префикс _imp_ является страшной тайной!). Пример функции CallSomeFunctions также показывает, как нужно вызвать локальную функцию.
    void CaiiSomeFunctions ( void }
    {
    _asm
    {
    // Вызвать импортированную функцию GetLastError, у которой нет
    // параметров. Регистр ЕАХ будет содержать возвращаемое значение.
    // Это вызов через IAT, т. е. вызов через указатель.
    CALL DWORD PTR [GetLastError]
    // Если символы загружены, окно Disassembly покажет
    // CALL DWORD PTR [_imp__GetLastError@0 (00402000)].
    // Если символы не загружены, окно Disassembly покажет
    // CALL DWORD PTR [00402000].
    ////////////////////////////////////////////////////////////////
    // Вызвать функцию внутри этого файла.
    CALL NOPFuncOne
    // Если символы загружены, окно Disassembly покажет
    // CALL NOPFuncOne (00401000).
    // Если символы не загружены, окно Disassembly покажет
    // CALL 00401000.
    }
    }
    Инструкция RET выполняет возврат в вызывающую функцию, используя адрес на вершине стека (без какой бы то ни было его проверки). Нетрудно представить, что испорченный стек может выполнить возврат в любую точку приложения. За инструкцией RET иногда следует фиксированное число, которое определяет, сколько байт нужно извлечь из стека, чтобы учесть все параметры, помещенные в стек и переданные функции
    .


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

    Добавьте кнопку Run To Cursor к панели инструментов Debug

    Подобно отладчику Visual C++, отладчик Visual Basic позволяет выполнять программу до позиции курсора. По умолчанию команда Run To Cursor назначается сочетанию клавиш +, но многим нравится при отладке использовать мышь, а этой замечательной команды нет на панели инструментов Debug. На вкладке Command диалогового окна Customize выберите категорию Debug и перетащите кнопку Run To Cursor на панель инструментов Debug.
    В Visual Basic 6 можно заметить, что если создать кнопку Run To Cursor на панели инструментов, то в окне всплывающей подсказки для пункта меню Run To Cursor отображается текст "Step To Cursor" (Шаг к курсору). Просто игнорируйте эту подсказку (т. к. кнопка все еще выполняет команду Run To Cursor). Вы просто видите "маленькую ошибку".



    Групповые проекты как способ перехода к отладке

    В Visual Basic 5 была введена концепция группового проекта. Групповой проект позволяет разместить все ActiveX-компоненты вместе с их тестовым окружением и главный ЕХЕ-файл (при условии, что его программа написана на языке Visual Basic) в одном проекте. Если все компоненты помещены в один проект, становится возможным пошаговое выполнение ЕХЕ-файла с проходом через все элементы управления и библиотеки DLL. В качестве примера просмотрите файл TESTER.VBG (часть кода из главы 13) на сопровождающем компакт-диске.


    Окна отладчика Visual Basic

    До сих пор мы говорили главным образом об ограничениях VB-отладчика, а не о его сильных сторонах. Но, как сказано в начале этой главы, этот отладчик имеет несколько свойств, которые разработчикам нравятся. Для начинающих отладчик Visual Basic намного проще, чем отладчик Visual C++. Он также включает несколько превосходных поддерживающих окон, которые делают отладку довольно простым занятием. В этом разделе рассмотрены три окна VB-отладчика: Locals, Immediate и Watch.


    Окно Immediate

    По моему мнению, окно Immediate Visual Basic изумительно. Хотелось бы, чтобы все отладчики имели такие развитые встроенные возможности отладки. Окно Immediate поддерживает подчиненную отладку и, по существу, является мини-интерпретатором Basic, позволяя выполнять фрагменты кода.
    Окно Immediate показывает вывод трассы операторов Debug.Print. Имейте в виду, однако, что это окно обеспечивает отображение лишь 200 строк, поэтому может потребоваться прокрутить строки трассировки (по экрану), для того чтобы прочитать их. К сожалению, нельзя очистить окно Immediate программным способом, чтобы гарантировать просмотр важных операторов трассировки. Надеемся, что в следующей версии Visual Basic будет предложен метод Debug, clear.
    Главное преимущество окна Immediate состоит в том, что в нем можно изменять значения переменных и вызывать подпрограммы прямо в приложении. Чтобы увидеть значение конкретной переменной программы, скажем, frmFoo.x, нужно использовать символ ? или оператор Print (т. е. для вывода значения нужно напечатать: ? FrmFoo.x). Прелесть окна Immediate в том, что в него встроены все замечательные IntelliSense-функции Microsoft. Например, если ввести правильное имя объекта, то IntelliSense покажет на экране методы и свойства этого объекта.
    Технология IntelliSense известна также под названием Automatic Statement Completion (автоматическое завершение операторов). — Пер.
    Для того чтобы изменить значение переменной, просто напечатайте строку кода Visual Basic в окне Immediate — точно так же, как если бы требовалось присвоить значение этой переменной в окне исходного кода. Окно Immediate "знает" все о свойствах "только-для-чтения" (read-only) и синтаксисе языка и известит вас через панель сообщения о том, что какое-либо действие окончилось неудачей.
    Один удачный прием, доступный в окне Immediate, позволяет создать приспособление для быстрого тестирования. Например, если вы разрабатываете класс, то можете протестировать его в окне Immediate сразу же, как только напечатаете его код в исходном редакторе.
    Если этот класс имеет имя clcMyclass и содержит метод с именем DoSomethingMagical, то можете ввести (строка за строкой) в окно Watch следующий код:

    Set x = New clsMyClass

    х.DoSomethingMagicai

    Set x = Nothing

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

    Кроме того, окно Immediate поддерживает вызов специальных отладочных функций. Вспомнив правила для ограничений на вызов отладочных функций из окна Watch Visual C++ (см. раздел "Вызов функций в окне Watch" главы 5), вы высоко оцените простоту использования окна Immediate. Единственное ограничение на вызов функций в окне Immediate — функция должна существовать в программе. Трудно сказать, какое свойство интенсивной отладки использовать легче.

    Хотя окно Immediate позволяет "одним махом перепрыгнуть небоскреб", оно не позволяет писать полноценное приложение. Первое ограничение: в окне Immediate нельзя писать функции. Второе: окно Immediate выполняет за один раз только одну строку кода. Некоторые структуры управления, такие как циклы For...Next, требуют записи нескольких операторов. Для этого предназначена специальная операция ":", позволяющая располагать операторы в одной строке. Ниже приведен пример записи цикла For.. .Next в окне Immediate:

    For i = 1 to UBound(a) : ? a(i) :'Next i

    Окно Locals

    Окно Locals довольно простое, тем не менее обращу внимание читателя на три ключевых момента. Во-первых, в отличие от окна Watch отладчика Visual C++, окно Locals в Visual Basic не требует, чтобы программист тратил силы на приведение типов и прочие хитрости, чтобы значения отображались в соответствующем формате. Во-вторых, наиболее важная переменная, показанная в окне Locals, является Me-объектом. Подобно указателю this языка C++, Me-объект является родовой конструкцией, которая полностью описывает текущий объект и его свойства.
    И, наконец, последнее: иногда можно изменять локальные переменные в этом окне, выбрав переменную и щелкнув (кнопкой мыши, конечно) на поле Value. Если Visual Basic позволит изменять переменные, то в поле Value будет разрешено редактирование текста. Visual Basic не позволяет изменять объекты и некоторые переменные типа variant. Например, нельзя изменять какое-либо из свойств элементов управления в коллекциях форм Controls. Однако если в форме имеются переменные с действующими типами элементов управления, такими как CommandButton, то изменение свойств этих элементов через переменные допустимо. В тех случаях, когда нельзя изменять значение переменной в окне Locals, можно воспользоваться окном Immediate.


    Для того чтобы редактировать результат буксировки в окне Watch, просто выполните щелчок правой кнопкой мыши в окне Watch и выберите команду Edit Watch всплывающего (контекстного) меню.

    Прежде чем переходить к следующему разделу, упомянем еще два аспекта окна Watch. Первый — можно редактировать значения щелчком мыши на поле Value. Так же, как и в окне Locals, если IDE Visual Basic разрешает редактирование, то вы можете изменять значения. Выражения можно также редактировать прямо по месту.

    Мне еще нравится помещать в окно Watch специальные значения, такие, например, как Err.Description. Это позволяет следить за любыми ошибочными значениями, с которыми можно столкнуться, если "перешагивать" через функции. Если отлаживаемое приложение получает также параметры из командной строки, я помещаю в окно Watch значение command, чтобы получить возможность быстро проверять различные параметры командной строки, которые приложение могло бы использовать (без необходимости их предварительной установки на вкладке Make диалогового окна Project Properties). К сожалению, как было сказано в начале данной главы, невероятно раздражающее свойство среды VB IDE заключается в том, что она "забывает" все тщательно построенные наблюдения и точки прерывания, как только вы ее покидаете (так что приходится повторно вводить их, начиная каждый новый сеанс отладки).



    Отключите режим Compile On Demand

    VB IDE пытается сделать отладку быстрее, компилируя только часть приложения, достаточную для передачи в отладчик. Хотя идея выглядит нормально, проблема состоит в том, что в ходе отладки приложения можно "нарваться" на секцию кода, которая не была откомпилирована, и сеанс отладки будет неожиданно остановлен с сообщением о синтаксической ошибке. К тому же, это может произойти в тот момент, когда та единственная ошибка, которая сводила вас с ума, уже почти дублирована. Согласитесь, это будет неприятно.
    Для того чтобы избежать каких-либо синтаксических сюрпризов, начинайте сеанс отладки командой Start With Fall Compile, Я даже добавил кнопку этой команды в панель инструментов Debug. Также выключайте режим Compile On Demand на вкладке General диалогового окна Options. Если вы работаете с большим приложением, то это увеличит время начального запуска отладки, но, по крайней мере, застрахует от обнаружения синтаксической ошибки в середине сеанса отладки.


    Отладка и реальность

    Как было сказано выше (в разделе "Р-код Visual Basic" этой главы), Visual Basic может отлаживать объекты только на уровне интерпретируемого р-ко-да. В современном мире СОМ+-технологий и компонентных служб (Component Services) можно выполнять отладку объектов с помощью отладчика Visual Basic, но большая часть документации содержит настойчивые рекомендации выполнять отладку объектов Visual Basic на компилируемом уровне средствами отладчика Visual C++.
    Для приложений компонентных служб1 я следовал рекомендациям, перечисленным в файле READMEVB.HTM системы Visual Basic 6, установленной как часть Visual Studio. Раздел "Building and Debugging MTS Components in Visual Basic 6" (Построение и отладка МТ82-компонентов в Visual Basic 6) этого документа точно сообщает, что нужно делать, чтобы отладить приложения. Сведения об отладке приложений СОМ+ можно найти в MSDN в следующих темах из Platform SDK: "Debugging Components Written in Visual Basic" (Отладка компонентов, написанных на Visual Basic) и "СОМ+ Visual Basic Debugging Support Contrasted with MTS" (Поддержка Visual Basic-отладки компонентов COM+ в сопоставлении с компонентами MTS).
    Приложения компонентных служб (от англ. Component Services applications). — Пер.
    MTS — Microsoft Transaction Server. — Пер.
    Чтобы полностью отладить какую-нибудь компонентную службу или СОМ-объекты, я просто компилирую объекты и использую либо отладчик SoftICE, либо отладчик Visual C++. Отладка компилируемого кода Visual Basic нелегка, но задачу можно упростить, увеличив количество операторов трассировки, для того чтобы просматривать все переменные и другую необходимую информацию. Итак, лучше все-таки отлаживать компонентные службы и приложения СОМ+ как компилируемые приложения (вместо работы в условиях ограничений VB-отладчика).


    Перехват ошибок: режимы Break In или Break On

    Теперь вернемся к режимам перехвата различных ошибок. VB IDE может выполняться в трех режимах: режиме проектирования (когда выполняется кодирование), режиме выполнения (когда приложение выполняется под отладчиком) и режиме прерывания (когда приложение останавливается отладчиком).
    Когда приложение наталкивается на позиционную точку прерывания, IDE автоматически переходит в режим прерывания. Однако, когда приложение генерирует ошибку, IDE перейдет (или не перейдет) в режим прерывания, в зависимости от установки трех следующих режимов:
  • Break On All Errors (Прерывать на всех ошибках);
  • Break On Unhandled Errors (Прерывать на необработанных ошибках);
  • Break In Class Module (Прерывать в модуле класса).
  • Когда я только начинал программировать на Visual Basic, то долго не мог получить класс, выдававший такую ошибку времени выполнения, которая обрабатывалась бы в режиме прерывания. После изрядных мучений, наконец, было найдено "секретное" меню, которое позволяло устанавливать эти режимы отлавливания ошибок.
    Режим перехвата ошибок можно устанавливать на вкладке General диалогового окна Option (открываемого командой Tools|Options). По умолчанию Visual Basic устанавливает режим Break In Class Module. Можно также изменять режим перехвата ошибок "на лету". Для этого нужно открыть (правым щелчком мыши в исходном окне) контекстное меню, выделить пункт Toggle и в раскрывшемся подменю включить нужный режим перехвата ошибок.


    Последствия работы с отладчиком

    Напомню, что в разделе "MinDBG: простой отладчик для Win32" главы 4, рассказано о том, насколько более устойчива отладка в 32-разрядных верcиях Windows, по сравнению с 16-разрядными, потому что в первом случае подчиненный отладчик находится вне адресного пространства основного отладчика. VB-отладчик понимает только интерпретируемый р-код, поэтому в результате подчиненный VB-отладчик выполняется в том же самом адресном пространстве, что и основной. Большинству разработчиков на Visual ) Basic известно, что VB не присущи проблемы, аналогичные тем, которые \ порождает применение указателей в С-программах, но ошибочный компо нент, загружаемый приложением VB, может привести к аварийному завершению VB IDE и потере части проекта.
    Следующие три конкретных правила должны помочь читателю разумно использовать VB-отладчик:
  • будьте крайне осторожны при подклассификации или использовании операции AddressOf;
  • в общем случае, во время отладки считайте, что исходный код имеет атрибут "только-для-чтения" (read-only);
  • при отладке не пользуйтесь остальной частью IDE.
  • Подклассификация (subclassing) в программировании для Windows — это метод, позволяющий приложению перехватывать и обрабатывать сообщения, посланные некоторому окну, прежде чем оно сможет обработать его. — Пер.
    Будьте крайне осторожны при подклассификации или использовании оператора AddressOf
    Большинство VB-приложений прекрасно выполняются под отладчиком. Однако если приложение подклассифицирует элементы управления Windows или вы используете операцию AddressOf, чтобы переслать одну из ваших подпрограмм как перехватчик обратных вызовов или процедуру таймера, то следует соблюдать особую осторожность, потому что приложение выполняется в том же самом адресном пространстве, что и VB-отладчик. Отлаживая приложение, необходимо учитывать, что обратные вызовы и таймеры могут все еще выполняться после того, как приложение будет остановлено, в результате приводя к аварийному завершению IDE.

    Если выполняется подклассификация оконной процедуры в режиме перерывания внутри VB-отладчика, то после получения сообщения окном, которое вы подклассифицировали, IDE завершится аварийно. К счастью, из-за того что подклассификация окон — такая общая операция, Microsoft обеспечивает решение с помощью утилиты DBGWPROC.DLL. Эта DLL позволяет подклассифицировать окна во время работы с VB-отладчиком. DBGWPROC.DLL можно найти по адресу Если оператор AddressOf используется для передачи одной из подпрограмм приложения функциям обработки прерываний, обратных вызовов или таймера операционной системы, то никакой помощи, подобной DBGWPROC.DLL, ждать не приходится. При желании, конечно, можно запустить приложение под VB-отладчиком. Однако всегда следует выполнять приложение (до завершения) таким образом, чтобы любые обработчики прерываний и обратные вызовы не выполнялись. Если для остановки приложения используется команда End меню Run или комбинация клавиш +, то могут возникнуть ситуации, в которых ваша процедура не находится больше в памяти, поэтому IDE завершится аварийно.

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

    В общем случае, во время отладки считайте, что исходный код имеет атрибут "только-для-чтения" (read-only)

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

    Чтобы автоматически сохранять все исходные файлы при запуске приложения в отладчике, нужно установить специальный режим VB IDE — Save Changes.


    Для этого следует установить флажок Save Changes на вкладке Environment диалогового окна Options. Если исходный код исправляется "на лету" (on the fly), то рекомендуется сохранять изменения как можно чаще (на случай, если VB-отладчик завершится аварийно). Лично я не делаю каких-либо изменений во время выполнения приложения в отладчике и никому не советую. Я полагаю, что, находясь в отладчике, нужно выполнять отладку, а не редактирование. Ведь добавить ошибку во время редактирования легче, чем устранить ее в ходе кропотливой отладки.

    При отладке не пользуйтесь остальной частью IDE

    Поскольку всю работу по управлению приложением во время отладки выполняет IDE, то могут возникнуть некоторые проблемы, если попытаться в IDE получить доступ к свойствам приложения, не связанным с отладкой. Например, если приложение использует уведомления таймера для фоновой обработки, и вы раскроете какое-нибудь модальное окно, скажем диалоговое окно Open Project или Options, то таймерная процедура приложения может перестать получать сообщения таймера.

    При работе в VB-отладчике рекомендуется использовать его отладочные окна. Если в приложении есть специализированный код (таймеры или немодальные диалоговые окна), то любое дополнительное взаимодействие с IDE может привести к тому, что приложение будет получать сообщения, которые не являются нормальной частью прикладного процесса. Вследствие того, что окна приложения являются частью той же самой очереди поточных сообщений, что и от IDE, в некоторых случаях работа отладчика будет затруднена из-за тесного взаимодействия между основным и подчиненным отладчиком. Наихудший случай — необходимость отладки сообщений нажатия клавиш мыши или клавиатуры. В этих ситуациях для отладки кода можно использовать только операторы Debug. Print.

    Р-код Visual Basic

    Опытные разработчики знают все о р-коде языка Visual Basic (VB), но необходимо, чтобы каждый читатель совершенно точно понимал, что происходит, когда выполняется VB-приложение, и представлял себе последствия отладки VB-приложений. Наряду с другими аспектами отладки, понимание работы отладчика может чрезвычайно помочь в этом. Для того чтобы установить систему определенных понятий, начнем с небольшого урока истории.


    Режим Break In Class Module

    Этот режим предназначен для отладки СОМ-серверов. Выбирая этот режим перехвата ошибок, вы сообщаете отладчику, что хотите рассматривать все ошибки в модулях классов так, как если бы не было никаких обработчиков ошибок. Хотя режим Break In Class Modules позволяет получать ошибки от СОМ-серверов, пользовательские обработчики ошибок игнорируются по-прежнему.


    Режим Break On All Errors

    Как подсказывает само название режима (прерывать на всех ошибках), всякий раз, когда VB-отладчик сталкивается с ошибкой времени выполнения, он останавливается и открывает диалоговое окно, показанное на рис. 7.1. Прерывание будет вызывать любая ошибка, даже если в программе имеется обработчик ошибок. Вообще говоря, этот режим перехвата ошибок не так полезен и я использую его, только если мне нужно увидеть, где возникла конкретная ошибка и когда она происходит в функции (не обращая внимания ни на какие обработчики ошибок).
    Режим Break On All Errors
    Рис. 7.1. Диалоговое окно ошибки времени выполнения Visual Basic
    По существу, при установке режима перехвата ошибок Break On All Errors выключаются все обработчики ошибок, встроенные в отлаживаемое приложение. Выполнение прерванного на ошибке приложения может быть продолжено путем перетаскивания стрелочного указателя с текущей выполняемой строки (на которой возникла ошибка) к следующей за ней выполнимой строке или с помощью команды Set Next Statement контекстного меню. Единственная проблема, возникающая в результате изменения строки выполнения, состоит в том, что Visual Basic не будет передавать вам фактическую ошибку для обработки.


    Режим Break On Unhandled Errors

    Судя по названию, режим Break On Unhandled Errors вынудит отладчик перейти в режим прерывания, если возникла ошибка, а обработчика для нее в программе нет. Этот режим прекрасно работает почти во всех ситуациях и я установил его в качестве режима перехвата ошибок по умолчанию. К сожалению, если отлаживаются системы СОМ-серверов (как внутриза-дачных (in-process), так и внезадачных (out-of-process)), то прерывания на ошибках модулей классов выполняться не будут. При отладке таких систем поступают иначе: возникшие ошибки упаковываются и передаются обратно клиенту как нормальные СОМ-ошибки. Для того чтобы выполнить прерывание в СОМ-серверах, нужно установить режим перехвата ошибок Break In Class Modules.



    Как обычно, закончим главу некоторыми

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

    истории р-кода

    Visual Basic 1, представленный в 1991 году, был многообещающим прогрессом в средствах программирования. В начале 90-х, разработки для Microsoft Windows необходимо было писать на языке С средствами SDK, что было, вообще говоря, довольно сложно и требовало больших усилий. Visual Basic обещал и, в большинстве случаев, предоставлял возможность "рисовать" интерфейс пользователя (UI) при помощи инструмента, получившего название WYSIWYG (What-You-See-Is-What-You-Get — "что -вы -видите, то -вы -и получаете"), и легкий способ связывать код с событиями пользователя, такими как щелчок кнопкой мыши. Необычным свойством в Visual Basic I было то, что он компилировал окончательное приложение не в "родной" выполняемый код, а в форму, называемую р-кодом.
    Термин р-код (p-code — сокращение от packed code, переводится как "упакованный код") происходит от названия метода сокращения объема памяти для хранения двоичных файлов (размер р-кода намного меньше, чем код языка ассемблера для процессоров Intel x86). При "компиляции" приложения в Visual Basic 1 создается регулярный выполняемый код, но это только оболочка для р-кода. Когда запускается "компилированный" двоичный файл (с этого начинается так называемое "время выполнения" (run-time) программы), по определенному смещению в памяти отыскивается р-код и начинается его выполнение. Известно, что вместе с приложением должна быть загружена большая библиотека динамической компоновки (DLL) VBRUN100.DLL, которая содержит интерпретатор р-кода.
    DLL иногда называют также библиотеками времени выполнения (run-time library). — Пер.
    Интерпретатор р-кода — стековая машина, которая транслирует специальные коды операций в операции CPU. Во времена MS-DOS и 16-разрядных Windows компания Microsoft предлагала иную форму р-кода, генерировавшуюся в системах программирования C/C++ 7.x. В действительности, первоначальные 16-разрядные версии продуктов Microsoft Word, Excel и PowerPoint широко использовали р-код этой системы в своих UI-кодах, чтобы достичь компромисса "размер памяти/производительность" и обеспечить выполнение этих приложений в операционных системах с ограниченной памятью (например, выпускавшихся тогда Windows 3 и 3.1).
    С идеей реализации р-кода в системах C/C++ 7.x, которая, вероятно, близка к реализации р-кода в Visual Basic, можно ознакомиться в библиотеке MSDN, найдя тему "Microsoft P-Code Technology" (Технология р-кода компании Microsoft).
    Все версии Visual Basic от 1-й до 4-й генерировали только р-код. Самая большая проблема в его применении состояла в том, что он был медленнее, чем исполняемый двоичный. Старая шутка гласила, что вы общались с Visual Basic на расстоянии в целую милю (потому что приложение работало очень медленно, т. к. использовало трехмерные элементы управления для всех текстовых кнопок). Компания Microsoft никак не могла повлиять на выбор разработчиками элементов управления, но учла просьбу "номер один" разработчиков Visual Basic, требовавших возможности компилировать исполняемые файлы.
    Начиная с Visual Basic 5, выпущенного в 1997 году, компания Microsoft добавила специальный режим, который поддерживал такую компиляцию. Разработчики Microsoft также переписали исполнительную (run-time) систему Visual Basic, сделав ее быстрее. Результатом обоих изменений было фантастическое повышение производительности приложений Visual Basic. Единственный недостаток заключался в том, что Microsoft сделала лишь точечную прививку свойства родной компиляции к дереву свойств системы Visual Basic, а не полностью интегрировала компиляцию и отладку со средой VB IDE. После компиляции приложения в исполняемый код программисту предоставлялась "полная свобода" в его отладке. Поскольку двоичные файлы Visual Basic не имеют достаточного количества отладочных символов, что обсуждалось в разделе "Отладка компилированного кода Visual Basic" главы 5, разработчик почти вынужден отлаживать программу на необработанном языке ассемблера.
    Самый большой недостаток компиляции в исполняемый код заключается в том, что VB IDE понимает только интерпретируемый р-код. Таким образом, VB-отладчик имеет дело с р-кодом, а это совсем не то, что получается в результате — исполняемый компилированный код.С точки зрения отладки и тестирования ситуация неприятная, — даже отдавая предпочтение компиляции в двоичный р-код, вы не отлаживаете точно те биты, которые посылаете заказчикам. К сожалению, ничего не остается, как мучиться с отладкой компилируемого двоичного VB-файла под отладчиком Visual C++. Остается надеяться, что в будущей версии Visual Basic или Microsoft Visual Studio эта проблема будет исправлена, и приложения, отлаженные разработчиками, будут точно соответствовать тому, что получат заказчики.

    Заключительные предложения по перехвату ошибок

    Как можно понять из этого обсуждения, перехват ошибок в VB-отладчике вряд ли желателен. Если вы хотите останавливаться на ошибках, то вы можете это делать, но за счет способности "переступать" через обработчики ошибок. Единственный реальный обходной путь состоит в том, чтобы устанавливать точки прерывания на всех обработчиках ошибок, которые требуется выполнить в пошаговом режиме, и задать режим перехвата ошибок Break On Unhandled Errors. В чем мы действительно нуждаемся, так это в способе останавливать выполнение при возникновении ошибок, но обрабатывать их при помощи нормальных приемов.
    Остается только надеяться, что команда VB-разработчиков сжалится над теми, кто хочет отлаживать свои приложения полностью и предложит для этого удобные, эффективные инструменты. Искусственные ограничения, наложенные на перехват ошибок в VB-отладчике, серьезно затрудняют отладку приложений.
    Поскольку проблемы перехвата ошибок связаны непосредственно с конструкциями On Error GOTO в языке Visual Basic, следует обратить особое внимание на использование обработчиков ошибок в программах. Следующий раздел этой главы я собирался посвятить надлежащей обработке ошибок, но Пит Моррис (Peet Morris) сообщил, что соответствующую тему он обсуждает в главе 1 (названной так: "On Error GoTo Hell") книги Advanced Microsoft Visual Basic 6.0, The Mandelbrot Set, 2nd ed., Microsoft Press, 1998. Если вы даже только раздумываете о разработках на Visual Basic, эту главу нужно прочитать обязательно.



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

    Что делать дальше с утилитой CrashFinder?

    Теперь, немного разобравшись с тем, как работает CrashFinder, поговорим о том, как расширить его функциональные возможности. Хотя CrashFinder — полноценное приложение, далее предложено несколько дополнений, которые сделают его мощнее и намного более легким для использования. Если вы хотите получать больше информации о двоичных образах, добавьте в CrashFinder следующие свойства:
  • автоматическое добавление зависимых DLL. В настоящее время добавление каждого двоичного образа в CrashFinder-проект приходится выполнять вручную. Было бы намного лучше, если бы при создании нового проекта CrashFinder загружал в новый проект нужный ЕХЕ-файл (по подсказке пользователя), и затем автоматически добавлял к нему все зависимые DLL-файлы. Конечно, это свойство не обеспечило бы поиска динамически загружаемых DLL (т. к. они загружаются обращением к функции LoadLibrary), но все же привело бы к экономии какого-то времени за счет автоматического добавления индивидуальных двоичных файлов;
  • отображение дополнительной информации в информационной панели. Класс CBinaryimage позволяет выводить гораздо больше информации (кроме той символьной информации, которая выводится в настоящее время в правой панели программы). С помощью метода GetAdditionaiinfo можно добавить, например, файлы заголовков, а также списки импортированных и экспортируемых функций;
  • вставка списков DLL для автоматического добавления их к проекту. Окно Output отладчика выводит списки всех DLL, которые загружает приложение. Можно расширить CrashFinder так, чтобы позволить пользователю вставлять текст из окна Output в документальную панель программы CrashFinder и выполнять сканирование этого текста в поисках имен DLL.



  • Использование утилиты CrashFinder

    Как видите, чтение МАР-файла не слишком затруднительно. Оно скорее утомительно, но это, конечно, не должно касаться других членов команды (инженеров по качеству, персонала технической поддержки и даже менеджеров). Чтобы облегчить и их жизнь, я решил сделать утилиту CrashFinder пригодной к употреблению всеми членами команды — от разработчиков до инженеров службы поддержки (включая, конечно, и специалистов по тестированию). Я старался, чтобы все аварийные отчеты включили по возможности максимум информации об ошибках. Если соблюдается процедура создания соответствующих отладочных символов, описанная в главе 2, то применение CrashFinder не вызовет затруднений.
    При использовании программы CrashFinder в рабочих группах необходимо особенно внимательно относиться к обеспечению доступности двоичных образов1 и связанных с ними PDB-файлов, потому что CrashFinder не хранит никакой другой информации о приложении, кроме путей к двоичным образам. CrashFinder сохраняет только имена двоичных файлов, так что один и тот же проект, работающий с утилитой CrashFinder, можно применять повсюду в цикле производства. Если бы CrashFinder хранил более детальную информацию о приложении, такую, например, как таблицы символов, то, вероятно, пришлось бы создавать CrashFinder-проект для каждого построения продукта. Если следовать этой рекомендации и разрешать свободный доступ к двоичным и PDB-файлам при аварийном завершении приложения, то специалистам по тестированию или поддержке останется лишь запускать CrashFinder, чтобы добавить к отчету об ошибках соответствующую информацию. Как всем известно, чем больше информации о проблеме доступно разработчику, тем легче ему решить эту проблему.
    Здесь и далее под двоичными образами (binary images) автор понимает, по-видимому, любые двоичные файлы конкретного приложения — EXE, DLL, OCX и т. д. — Пер.
    Вероятно, для конкретного приложения придется создать несколько CrashFinder-проектов. Если системные DLL являются частью CrashFinder-проекта, то для каждой операционной системы, которую вы поддерживаете, нужно будет создавать отдельные проекты.
    Придется также создать CrashFinder- проект для каждой версии приложения, посылаемой специалистам по тестированию, не входящим в команду разработчиков, и отдельно для каждой такой версии хранить двоичные и PDB-файлы.

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

    Двоичный образ добавляется к CrashFinder-'Проекту командой Add Image меню Edit. Добавляя двоичные образы, имейте в виду, что CrashFinder воспринимает только один ЕХЕ-файл в каждом проекте. Если приложение включает несколько ЕХЕ-файлов, необходимо создать отдельный CrashFinder-проект для каждого из них. Поскольку CrashFinder является МDI-приложением, то можно открыть отдельные проекты для каждого ЕХЕ-файла (с целью определения соответствующих аварийных позиций). Когда вы добавляете DLL-файлы, CrashFinder проверяет отсутствие конфликтов в адресах загрузки с любыми другими DLL, уже находящимися в проекте. Если CrashFinder обнаруживает конфликт, то позволит изменить адрес загрузки конфликтующей DLL только для текущего экземпляра CrashFinder-проекта. Такой режим удобен, когда вы, имея CrashFinder-проект для отладочной конфигурации, забываете перебазировать свои DLL: Как указано в главе 2, нужно всегда устанавливать базовые адреса для всех DLL приложения.

    MDI — Multiple-Document Interface (многодокументный интерфейс). — Пер.

    Использование утилиты CrashFinder
    Рис. 8.2. Интерфейс пользователя утилиты CrashFinder

    Если приложение изменится через какое-то время, то можно удалить ненужные двоичные образы, выбрав команду Remove Image меню Edit. Адрес загрузки двоичного образа можно изменять с помощью команды Image Properties меню Edit.


    Целесообразно также добавлять в дерево системные DLL, которые используются проектом. Это дает возможность локализовать проблему, когда программа аварийно завершается в одном из них. Как уже говорилось в главе 5, наличие установленных отладочных символов в Windows 2000 иногда очень помогает при пошаговом выполнении кода дизассемблера системного модуля. Теперь есть даже более серьезная причина для установки отладочных символов в Windows 2000 — утилита CrashFinder может их использовать для того, чтобы предоставить разработчику возможность отыскивать аварии даже в системных модулях.

    RaiSon d'etre утилиты CrashFinder состоит в том, чтобы преобразовать аварийный адрес в имя функции, имя исходного файла и номер строки. Выполнение команды Find Crash меню Edit открывает диалоговое окно Find Crash, показанное на рис. 8.3. Для каждого аварийного адреса, который требуется отыскать, нужно лишь ввести шестнадцатеричный адрес в редактируемое поле Hexadecimal Address и нажать кнопку Find.

    Raison d'etre — разумное основание (франц.). — Пер.

    Использование утилиты CrashFinder
    Рис. 8.3. Поиск позиции аварийного останова с помощью утилиты CrashFinder

    В нижней части диалогового окна Find Crash отображена информация, относящаяся к последнему найденному адресу. Большинство полей здесь самоочевидно и не требует объяснения. Поле Fn Displacement показывает смещение адреса ошибки от начала функции в байтах, а поле Source Displacement — смещение адреса ошибки от начала ближайшей исходной строки. Помните, что отдельная исходная строка может порождать несколько инструкций языка ассемблера, особенно, если вызовы функций являются частью списка параметров. Имейте в виду, что при использовании программы CrashFinder нельзя отыскивать адреса, которые не являются адресами правильных (исполняемых) инструкций. Если в программе на C++ очищается указатель this, то это может вызвать аварийный останов в адресе 0x00000001. К счастью, такие типы ошибок не столь распространены как обычные ошибки нарушения доступа к памяти, которые можно легко найти с помощью утилиты CrashFinder.

    Основные моменты реализации

    Сама программа CrashFinder является непосредственным MFC-приложением (т. е. напрямую использует библиотеку классов Microsoft Foundation Class), поэтому большая ее часть должна быть хорошо узнаваемой. Чтобы облегчить дальнейшее расширение программы CrashFinder (это можно сделать, следуя рекомендациям, приведенным в разделе "Что делать дальше с утилитой CrashFinder?" в конце этой главы), укажу на три ключевых момента и поясню основные особенности их реализации. Во-первых, речь пойдет о символьной машине, которую использует CrashFinder, во-вторых, мы рассмотрим, где выполняется основная работа в программе CrashFinder, и, наконец, опишем архитектуру данных этой программы.
    CrashFinder использует символьную машину DBGHELP.DLL, описанную в главе 4. Единственной интересной деталью является то, что необходимо заставить эту машину загружать весь исходный файл и информацию о номерах строк, пересылая флажок SYMOPT_LOAD_LINES в функцию symsetoptions. Символьная машина DBGHELP.DLL по умолчанию не загружает исходный файл и информацию о номерах строк, так что об этом должен позаботиться программист.
    Еще одна особенность реализации программы CrashFinder — вся работа, по существу, выполняется в документальном классе ccrashFinderDoc. Он содержит класс csyinboiEngine, осуществляет поиск всех символов и управляет их представлением (видом). Его ключевая функция— ccrashFinderDoc:: LoadAndShowimage — показана в листинге 8-2. Эта функция подтверждает правильность двоичного образа, проверяет его на наличие элементов проекта, имеющих конфликтующие адреса загрузки, загружает символы и вставляет образ в конец дерева. Она вызывается и при добавлении двоичного образа к проекту, и при открытии проекта. Перекладывая управление этими рутинными операциями на функцию CcrashFinderDoc: :LoadAndShowimage, разработчик может быть уверен, что основная логика программы CrashFinder всегда находится в одном месте, и что проект хранит только имена двоичных образов, а не копирует всю таблицу символов.

    Листинг 8-2. Функция CcrashFinderDoc: :LoadAndShowimage

    BOOL CcrashFinderDoc :: LoadAndShowimage ( CBinaryImage * plmage,

    BOOL bModifiesDoc)

    {

    // Проверить предположения за пределами функции.

    ASSERT ( this);

    ASSERT ( NULL != m_pcTreeControl);

    // Строка, которая может использоваться для любых сообщений

    CString sMsg ;

    // Состояние для графики дерева

    int iState = STATEJSIOTVALID;

    // Переменная для булевского возвращаемого значения

    BOOL bRet ;

    // Убедиться, что параметр — в порядке.

    ASSERT ( NULL != plmage);

    if ( NULL == plmage)

    {

    // Ничего не может случиться с плохим указателем,

    return ( FALSE);

    }

    // Проверить, правилен ли этот образ. Если это так, убедиться,

    // что его еще нет в списке и что он не имеет

    // конфликтующего адреса загрузки. Если это не так, все равно

    // добавить его, т. к. нехорошо просто выбрасывать данные

    // пользователя.

    // Если образ плох, я просто показываю его с неправильным растром

    // и не загружаю в символьную машину,

    if ( TRUE == p!mage->IsValidImage ())

    {

    // Здесь сканируются элементы массива данных, чтобы отыскать

    // три проблемных условия:

    // 1. Двоичный образ уже есть в списке. Если это так, то возможен

    // только преждевременный выход.

    // 2. Двоичный образ будет загружен в адрес, который уже

    // есть в списке. Если это так, открываем диалоговое окно

    // Properties для двоичного образа, чтобы перед его

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

    // 3. Проект уже включает исполняемый (ЕХЕ-)образ, и plmage тоже

    // является исполняемым.

    // Для начала я всегда оптимистично предполагаю, что данные в

    // plmage правильны.

    BOOL bValid = TRUE;

    int iCount = m_cDataArray.GetSize ();

    for ( int 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 ())

    {

    // Сообщить пользователю!!

    sMsg.FormatMessage ( IDS_DUPLICATEFILE,

    pTemp->GetFullName () );

    AfxMessageBox ( sMsg);

    return ( FALSE);

    }

    // Если текущее изображение из структуры данных неправильно,

    // то проверим дублирование имен, как это было только что |

    // показано, но адреса загрузки и характеристики ЕХЕ-образа

    // проверить трудно. Если рТетр — неправильный,

    //то следует пропустить эти проверки..

    // Это может привести к проблемам, но т. к. рТетр отмечен

    //в списке как неправильный, то переустановка

    // свойств становится проблемой пользователя.

    if ( TRUE == pTemp.->IsValidIinage ( FALSE) )

    {

    // Проверить, что в проект не добавлены два ЕХЕ-файла.

    if ( 0 == ( IMAGE_FILE_DLL &

    pTemp->GetCharacteristics ()))

    {

    if ( 0 = { IMAGE_FILE_DLL &

    pImage->GetCharacteristics ()))

    {

    // Сообщить пользователю!!

    SMsg.FormatMessage ( IDS_EXEALREADYINPROJECT,

    p!mage->GetFullName (), pTemp->GetFullName () );

    AfxMessageBox ( sMsg);

    // Попытка загрузить два образа, помеченных как

    // "ЕХЕ", будет автоматически отбрасывать данные

    // для plmage. 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 (


    pImage->GetLoadAddress(),

    iIndex ) )

    {

    sMsg.FormatMessage

    ( IDS_DUPLICATELOADADDRFINAL,

    p!mage->GetFullName () ,

    ((CBinaryImage*)m_cDataArray[iIndex])->GetFullName());

    AfxMessageBox ( sMsg);

    // Данные в pImage неправильные, поэтому

    // двигаемся дальше и выходим из цикла.

    bValid = FALSE;

    break;

    }

    }

    else

    {

    // Данные в plmage неправильные, поэтому

    // двигаемся дальше и выходим из цикла.

    bValid = FALSE;

    break;

    }

    }

    }

    }

    if ( TRUE = bValid)

    {

    // Этот образ хорош (по крайней мере, по отношению

    //к загруженным символам).

    iState = STATE_VALIDATED;

    }

    else

    {

    iState = STATE_NOTVALID;

    }

    }

    else

    {

    // Этот образ неправильный.

    iState = STATE_NOTVALID;

    }

    if ( STATE_VALIDATED = iState)

    {

    // Попытка загрузить этот образ в символьную машину.

    bRet =

    m_cSymEng.SymLoadModule(NULL ,

    (PSTR)(LPCSTR)pImage->GetFullName(),

    NULL

    pImage->GetLoadAddress () ,

    0 );

    // Наблюдение закончено. SymLoadModule возвращает адрес загрузки

    // образа, неравный TRUE.

    ASSERT ( FALSE != bRet);

    if ( FALSE == bRet)

    {

    TRACE ( "m_cSymEng.SymLoadModule failed!!\n");

    iState = STATE_NOTVALID;

    }

    else

    {

    iState ь STATE_VALIDATED;

    }

    }

    // Установить значение "Extra Data" для plmage в состояние загрузки

    // отладочных символов i

    f ( STATEJVALIDATED == iState)

    {

    pImage->SetExtraData ( TRUE);

    }

    else

    {

    pImage->SetExtraData ( FALSE);

    }

    // Поместить этот элемент в массив.

    m_cDataArray.Add ( plmage);

    // Добавлен ли элемент модификации документа?

    if ( TRUE == bModifiesDoc)

    {

    SetModifiedFlag ();

    }

    CCrashFinderApp * pApp = (CCrashFinderApp*)AfxGetApp ();

    ASSERT ( NULL != pApp);

    // Поместить строку в дерево.

    HTREEITEM hltem =


    m_pcTreeControl->Insert!tem ( pApp->ShowFullPaths ()

    ? pImage->GetFullName ()

    : pImage->GetName () ,

    iState ,

    iState );

    ASSERT ( NULL != hltem);

    // Поместить указатель на образ в данные элемента. Этот указатель

    // облегчает обновление символьной информации модуля

    // всякий раз, когда его вид претерпевает изменения.

    bRet = m_pcTreeControl->SetItemData ( hltem, (DWORD)plmage);

    ASSERT ( bRet);

    // Форсировать выбор элемента.

    bRet = m_pcTreeControl->SelectItem ( hltem);

    // Bee OK, Jumpmaster!

    return ( bRet);

    }

    И, наконец, опишем архитектуру данных программы CrashFinder. Ее главная структура данных — это просто массив классов cbinaryimage. Каждый класс cbinaryimage представляет отдельный двоичный образ, добавляемый к проекту, и обслуживает информацию о таких деталях этого образа, как адрес загрузки, двоичные свойства и имя. Документ1 добавляет объект cbinaryimage (двоичный образ) к массиву главных данных и помещает значение соответствующего указателя в слот данных узлового элемента дерева. При выборке элемента в представлении дерева двоичных файлов (в левой панели окна программы CrashFinder) выбранный узел пересылается назад в документ, так чтобы документ смог получить объект cbinaryimage и просмотреть его символьную информацию (предъявив ее пользователю в правой панели окна программы CrashFinder).

    Точнее — объект класса CcrashFinderDoc. — Пер.

    Поиск функции, исходного файла и номера строки

    Алгоритм для извлечения функции, исходного файла и номера строки из МАР-файла прост, но выполняется с применением шестнадцатеричной арифметики. Пусть, например, аварийный останов произошел по адресу 0x03901099 в модуле MAPDLL.DLL, показанном в листинге 8-1.
    Прежде всего, нужно заглянуть в МАР-файлы своего проекта и найти файл, который содержит аварийный адрес. Сначала посмотрите на предпочтительный адрес загрузки и последний адрес в секции общих функций. Адрес ошибки должен находиться между этими значениями, если это не так, значит перед нами "неправильный" МАР-файл.
    Чтобы отыскать функцию (или ближайшую общую функцию, если ошибка произошла в статической С-функции), найдите в колонке Rva+Base первый адрес, который превышает аварийный. Тогда предыдущий вход в МАР-файле и есть та функция, которая вызвала аварийный останов. Например, в листинге 8-1 первым адресом функции, превосходящим аварийный (0x3901099), является адрес Ox39010F6, следовательно, ошибку вызвала функция ?MapDLLHappyFunc@@YAPADPAD@z. Имя функции, которое начинается со знака вопроса, является декорированным именем (decorated name) языка C++. Чтобы транслировать это имя, передайте его как параметр команд- | ной строке программы UNDNAME.EXE из Platform SDK. Например, ?MapDLLHappyFunc@@YApADPAD@z переводится этой программой в обычное имя MapDLLHappyFunc, о чем, вероятно, можно было догадаться, просто взглянув на декорированное имя. Другие декорированные имена C++ расшифровы вать труднее, особенно когда используются перегруженные функции. Номер строки вычисляется по следующей формуле:
    (crash address) — (preferred load address) — 0x1000
    Напомним, что адреса указываются как смещение от начала первой секции кода, что и учитывает эта формула. Вероятно, нетрудно догадаться, почему вычитается предположительный адрес загрузки, но зачем вычитать еще и 0x1000? Адрес ошибки представляет собой смещение от начала секции кода, но эта секция не является первой частью двоичного файла. Его первой частью является РЕ заголовок, длина которого равна 0x1000 байт.

    РЕ-файл — от англ. Portable Executable, переносимый (на другую платформу) исполняемый файл. — Пер.

    Не знаю точно, почему компоновщик генерирует МАР-файлы, которые требуют этого добавочного вычисления. Разработчики утилиты LINK.EXE ввели колонку Rva+Base недавно, и не вполне понятно, почему они просто не установили в этой колонке и номер строки тоже.

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

    0x03901099 - 0x03900000 - 0x1000 = 0x99

    В листинге 8-1 самый близкий, но не превышающий расчетное значение адреса 0001:00000099 номер строки равен 38 (с адресом 0001:00000096), т. е. речь идет о строке 38 в файле MAPDLL.CPP.

    Рассматривая МАР-файл для модуля, написанного на языке Visual Basic, следует знать, что номера строк, о которых сообщает МАР-файл (и утилита CrashFinder), не соответствуют номерам строк, которые отображает редактор Visual Basic. Компилируемые двоичные файлы принимают во внимание полный заголовок в верхней части исходного файла, который Visual Basic скрывает от просмотра. Чтобы найти строку, о которой сообщает компилятор, нужно открыть VB-файл в текстовом редакторе, таком как редактор Visual C++, и перейти к строкам, перечисленным в соответствующем списке МАР-файла.

    Содержимое МАР-файла

    Пример МАР-файла показан в листинге 8-1. Верхняя часть МАР-файла содержит имя модуля, метку даты/времени (timestamp), указывающую, когда LINK.EXE скомпоновал модуль и предпочтительный адрес загрузки. После заголовка расположена информация, которая показывает, какие секции присоединил компоновщик из различных OBJ- и LIB-файлов.
    Далее следует информация об общих (public) функциях. Обратите внимание на эту часть. Если в программе есть С-функции, объявленные как статические (static), то они не будут показаны в МАР-файле. К счастью, номера строк все еще будут отражать статические функции.
    Важными данными для общих функций являются их имена и информация в колонке Rva+Base, в которой указываются их стартовые адреса. За секцией общих функций следует строчная информация. Строки отображаются следующим образом:
    10 0001:00000030
    Первое число — номер строки, а второе представляет собой смещение от начала секции кода, в которой находится эта строка. Это выглядит довольно путано, но позже мы рассмотрим способ преобразования адреса в имя исходного файла и номер строки в нем.
    Если модуль содержит экспортируемые функции, то они перечислены в заключительной секции МАР-файла. Можно получить эту же информацию, запустив утилиту DUMPBIN с параметром .
    Листинг 8-1.Пример MAP-файла
    MapDLL
    Timestamp is 37f41936 (Thu Sep 30 22:15:18 1999)
    Preferred load address is 03900000
    Start Length Name Class
    0001:00000000 00001421H .text CODE
    0002:00000000 0000012cH .rdata DATA
    0002:00000130 00000193H .edata DATA
    0003:00000000 00000104H .CRT$XCA DATA

    0003:00000104 00000104H .CRT$XCZ DATA

    0003:00000208 00000104H .CRT$XIA DATA

    0003:0000030с 00000104Н .CRT$XIZ DATA

    0003:00000410 00000176H .data DATA

    0003:00000588 00000030H .bss DATA

    0004:00000000 00000014H .idata$2 DATA

    0004:00000014 00000014H .idata$3 DATA

    0004:00000028 00000050H .idata$4 DATA

    0004:00000078 00000050H .idata$5 DATA

    0004:000000c8 00000179H .idata$6 DATA

    Address Publics by Value Rva+Base Lib:0bject

    0001:00000030 _DllMain@12 03901030 f MapDLL.obj

    0001:0000004с ?MapDLLFunction@@YAHXZ 0390104с f MapDLL.obj

    0001:00000076 ?MapDLLHappyFunc@@YAPADPAD@Z 03901076 f MapDLL.obj

    0001:000000f6 _printf 039010f6 f MSVCRTD:MSVCRTD.dll

    0001:000000fc _chkesp 039010fc f MSVCRTD:MSVCRTD.dll

    0001:00000110 _CRT_INIT@12 03901110 f MSVCRTD:crtdll.obj

    0001:00000220 _DllMainCRTStartup@12 03901220 f MSVCRTD:crtdll.obj

    0001:00000314 _free_dbg 03901314 f MSVCRTD:MSVCRTD.dll


    0001:0000031a _initterm 0390131a f MSVCRTD:MSVCRTD.dll

    0001:00000320 _onexit 03901320 f

    MSVCRTD:atonexit.obj

    0001:00000360 __atexit 03901360 f

    MSVCRTD:atonexit.obj

    0001:00000378 _malloc_dbg 03901378 f MSVCRTD:MSVCRTD.dll

    0001:0000037e __dllonexit 0390137e f MSVCRTD:MSVCRTD.dll

    0002:0000001c ??_C@_08JKC@crtdll?4c?$AA@ 0390301с MSVCRTDrcrtdll.obj

    0003:00000000 __xc_a 03904000

    MSVCRTD:cinitexe.obj

    0003:00000104 __xc_z 03904104

    MSVCRTD:cinitexe.obj

    0003:00000208 __xi_a 03904208

    MSVCRTD:cinitexe.obj

    0003:0000030с __xi_z 0390430с

    MSVCRTD:cinitexe.obj

    0003:0000058с _adjust_fdiv 0390458с

    0003:00000598 __onexitend 03904598

    0003:000005a8 __onexitbegin 039045a8

    0003:000005ac _pRawDllMain 039045ac

    0004: 00000000 _IMPORT_DESCRIPTOR_MSVCRTD 03905000 MSVCRTD:MSVCRTD.dll


    0004:00000014 _NULL_IMPORT_DESCRIPTOR 03905014 MSVCRTD:MSVCRTD.dll

    0004:00000078 __imp__malloc_dbg 03905078 MSVCRTD:MSVCRTD.dll

    0004:0000007с _imp chkesp 0390507с MSVCRTD:MSVCRTD.dll

    0004:00000080 imp free_dbg 03905080 MSVCRTD:MSVCRTD.dll

    0004:00000084 _imp initterm 03905084 MSVCRTD:MSVCRTD.dll

    0004:00000088 _imp_jjrintf 03905088 MSVCRTD:MSVCRTD.dll

    0004:0000008с _imp adjust_fdiv 0390508с MSVCRTD:MSVCRTD.dll

    0004:00000090 _imp___dllonexit 03905090 MSVCRTD:MSVCRTD.dll

    0004:00000094 _imp onexit 93905094 MSVCRTD:MSVCRTD.dll

    0004:00000098 \177MSVCRTD_NULL_THUNK_DATA 03905098 MSVCRTD:MSVCRTD.dll

    entry point at 0001:00000220

    Line numbers for .\Debug\MapDLL.obj(D:\MapFile\MapDLL\MapDLL.cpp)

    segment '. text

    10 0001:00000030 12 0001:0000003b 19 0001:00000041 20 0001:00000046' 24 0001:0000004c 25 0001:00000050 26 0001:00000067 27 0001:0000006c 35 0001:00000076 36 0001:0000007a 37 0001:0000007f 38 0001:00000096 39 0001:0000009c 40 0001:0000009f 30 0001:000000a9 31 0001:OOOOOOad 32 0001:000000c4

    Line numbers for g:\vc\LIB\MSVCRTD.lib(atonexit.c) segment .text

    84 0001:00000320 89 0001:00000324 98 0001:0000035b 103 0001:00000360

    104 0001:00000363 105 0001:00000376

    Line numbers for g:\vc\LIB\MSVCRTD.lib(crtdll.c) segment .text

    135 0001:00000110 140 0001:00000114 141 0001:0000011a 142


    0001:00000123

    143 0001:00000130 147 0001:00000132 156 0001:00000139 164

    0001:00000147

    170 0001:0000014d 175 0001:00000175 177 0001:0000017с 179

    0001:00000187

    184 0001:00000193 189 0001:OOOOOlaS 192 0001:000001b4 219 0001:OOOOOlbc

    220 0001:000001c5 222 0001:000001cd 227 0001:OOOOOlel 228 0001:000001e9

    236 0001:000001ee 238 0001:00000202 242 0001:0000020c 243 0001:00,000211

    251 0001:00000220 252 0001:00000224 258 0001:0000022b 259 0001:0000023a

    261 0001:00000241 263 0001:0000024d 264 0001:00000256 266 0001:0000026b

    267 0001:00000271 269 0001:00000285 270 0001:0000028b 273 0001:0000028f

    276 0001:000002a3 284 0001:000002af 287 0001:000002be 289 0001:000002ca

    290 0001:000002df 292 0001:000002e6 293 0001:000002f5 296 0001:ОООООЗОа

    297 0001:0000030d '

    Exports

    ordinal name

    1 ?MapDLLFunction@@YAHXZ (int_cdecl MapDLLFunction(void))

    2 ?MapDLLHappyFunc@@YAPADPAD@Z (char * _cdecl MapDLLHappyFunc(char *))

    Создание и чтение МАР-файла

    Многие не понимают, зачем создавать МАР-файлы в финальных построениях. Очень просто: потому что МАР-файлы являются единственным текстовым представлением глобальных символов программы, информации об ее исходном файле и о номерах строк в этом файле. Работать с утилитой CrashFinder намного проще, чем расшифровывать МАР-файлы, но зато для чтения последних не требуется (для получения той же самой информации) программа поддержки и наличие всех необходимых двоичных файлов программы (DLL, OCX и т. д.). Поверьте, если когда-нибудь в будущем вам потребуется вычислять, где произошла авария в старых версиях вашей программы, то нужную информацию удастся найти только в соответствующих МАР-файлах.
    Можно создавать МАР-файлы для модулей, компилируемых как в Microsoft Visual C++, так и в Microsoft Visual Basic. В Visual C++ для этого нужно на вкладке Link диалогового окна Project Settings в редактируемой области Project Options допечатать ключи компоновщика /MAPINFO: EXPORTS и /MAPINFO:LINES. В списке Category следует выбрать элемент Debug и включить флажок Generate mapfile.
    Если вы работаете с реальным проектом, то двоичные файлы, вероятно, направляются в собственный выходной каталог. По умолчанию компоновщик записывает МАР-файл в тот же каталог, что и промежуточные файлы, поэтому следует указать, что МАР-файл направляется в каталог вывода двоичных файлов. В редактируемом поле Mapfile name можно ввести $ (OUTDIR) \<проект>.МАР, где <проект> — имя конкретного проекта, $ (OOTDIR; — это макрос программы NMAKE.EXE, который система построения будет замещать реальным выходным каталогом. На рис. 8.1 показаны окончательные установки МАР-файла для проекта MapDLL, который включен в сопровождающий компакт-диск.
    Создание МАР-файла для VB-модуля .включает установку тех же флагов, но иным, довольно интересным способом. Visual Basic использует тот же самый компоновщик (LINK.EXE), что и Visual C++, и некоторые ключи его командной строки можно устанавливать через переменную окружения LINK.
    Если задать для нее значение " /MAP :<проект>.МАР/мдргаго: EXPORTS /MAPINFO:LINES", то Visual Basic будет генерировать МАР-файл на шаге компоновки процесса компиляции. Определив значение указанной переменной в окне Command Prompt, необходимо из этого же окна запустить и Visual Basic (чтобы переменная LINK находилась в зоне видимости программы VB6.EXE).

    Возможно, в повседневной работе МАР-файлы и не нужны, но они могут понадобиться в будущем. Утилита CrashFinder и отладчик полагаются на таблицы символов и символьную машину для их чтения. Не забывайте регулярно сохранять РDВ файлы, что же касается таблиц символов, то они изменяются часто, и разработчик не имеет никакого контроля над их форматами. Например, те, кто обновлял версию 5 Microsoft Visual Studio (до версии 6), наверняка заметили, что инструменты типа CrashFinder переставали работать с программами, компилированными в новой версии (Visual Studio 6). Дело в том, что компания Microsoft изменила формат таблиц символов (и делает это регулярно). В этой ситуации МАР-файлы — единственное ваше спасение.

    PDB — Program Data Base (база данных программы). — Пер.

    Создание и чтение МАР-файла
    Рис. 8.1. Установки МАР-файла в диалоговом окне Project Settings

    Будь вы даже лет через пять разработчиком, пользующимся Windows 2005 и Visual Studio 11 Service Pack 6, можно поручиться, что найдутся заказчики, которые будут выполнять программы, созданные вами в далеком 1999 году. Когда они вызовут вас по тревоге и сообщат адрес ошибки, то неизвестно, сколько времени уйдет на поиски компакт-дисков с Visual Studio 6, необходимой для чтения сохраненных когда-то PDB-файлов. А при наличии МАР-файлов проблему можно будет найти за пять минут.

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

    API-функция SetUnhandledExceptionFilter

    В программах на C++ имеется возможность обрабатывать аварии, защищая секции кода, в которых, по вашему мнению, может произойти тяжелый аварийный останов. Однако, как известно, аварии обыкновенно никогда не происходят там, где мы их ожидаем. К сожалению, когда пользователи узнают об аварии в своей программе, они видят только диалоговое окно Application Error и затем, возможно, программа Dr. Watson передает им немного дополнительной информации для разрешения возникшей проблемы. Как я уже говорил, можно разработать свои собственные диалоговые окна, чтобы получать информацию, которая действительно нужна вам для объяснения аварии. Этого можно добиться, устанавливая с помощью API-функции SetUnhandledExceptionFilter специальные типы обработчиков, называемых фильтрами необрабатываемых исключений. Мы уже ссылались на них как на обработчики аварий. Удивительно, что эти функциональные возможности были реализованы в Win32 начиная еще с Microsoft Windows NT 3.5, но они почти не документированы. В июльском (1999) выпуске MSDN эта функция была упомянута только в девяти темах.
    По моему опыту, обработчики аварий имеют превосходные отладочные возможности. В одном из моих проектов в случае аварии, в дополнение к открытию диалогового окна с номером нашей технической поддержки, записывалась в файл вся информация, включая состояние системы пользователя и главных объектов программы, и было известно (вплоть до уровня классов), какие объекты были активны и что они содержали. Я зарегистрировал едва ли не всю информацию о состоянии программы. Вместе с сообщением об аварии была фактически дублирована проблема пользователя. Что это как не профилактическая отладка!
    Само собой разумеется, что я нахожу функцию SetUnhandledExceptionFilter довольно мощным инструментом. Просто взглянув на название функции (SetUnhandledExceptionFilter), можно, вероятно, догадаться, что она делает. Один ее параметр — указатель на функцию, которая вызывается в заключительном _except-блоке приложения. Эта функция возвращает те же значения, что и любой другой фильтр исключений: EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_EXECUTION или EXCEPTION_CONTINUE_SEARCH.
    В этой фильтр- функции можно выполнять любую обработку исключений, но, как говорилось выше при обсуждении С++-функции _set_se_transiator, нужно соблюдать осторожность, исключая возможность переполнения стеков. Чтобы обезопасить себя, следует избегать вызовов любых библиотек времени выполнения, а также MFC. Если вы пишете собственную функцию фильтра исключений на языке Visual Basic, то проявляйте сверхосторожность во всем, что касается доступа из исполнительной библиотеки Visual Basic. Я обязан предупредить об этих неприятностях, но могу вас уверить, что подавляющее большинство аварий происходит из-за нарушения доступа и не должно быть каких-либо проблем, если вы включите полную систему обработки аварий в свою фильтр-функцию (при условии, что сначала вы будете проверять причину исключения и, кроме того, предпримете меры, чтобы избегать вызовов функций при переполнении стека).

    Фильтр исключений тоже получает указатель на структуру EXCEPTION_POINTERS. В листинге 9-5 представлено несколько подпрограмм, которые транслируют эту структуру. Поскольку каждая компания имеет различные потребности в обработчиках аварий, читателю представляется возможность написать собственный аварийный обработчик.

    Следует иметь в виду две проблемы, возникающие при использовании setunhandiedExceptionFiiter. Первая: нельзя применять стандартные отладчики пользовательского режима для отладки любого фильтра необрабатываемых исключений, который вы устанавливаете. Это — известная ошибка. В статье Q173652 в Knowledge Base говорится, что под отладчиком фильтр необрабатываемых исключений не вызывается. Эта ошибка может быть немного болезненной, но в программе на C++ для отладки своего фильтра необрабатываемых исключений можно использовать следующий обходной путь: нужно вызывать его из штатного SEH-фильтра исключений. Соответствующий пример можно найти в функции Baz программы CHJTESTS.CPP, которая является частью исходного кода, поставляемого с этой книгой.

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

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

    В динамической библиотеке BUGSLAYERUTIL.DLL мной реализован CrashHandler API, с помощью которого можно ограничить свой обработчик аварий конкретным модулем или модулями. Способ, с помощью которого это делается, состоит в том, что все исключения проходят через установленный фильтр необрабатываемых исключений. При вызове этого фильтра проверяется модуль, из которого пришло исключение. Если исключение пришло от одного из предписанных модулей, вызывается пользовательский обработчик аварий, но если оно приходит от другого модуля (не от предписанного), вызывается фильтр необрабатываемых исключений, который я заменил. Вызов замененного фильтра исключений означает, что многие модули могут использовать CrashHandler API, не мешая друг другу. Все функции CrashHandler API показаны в листинге 9-5.
    Листинг 9-5. CrashHandler.СPP
    /*- - - - - - - - - - - - - - - - - - - -
    "Debugging Applications" (Microsoft Press)
    Copyright (с) 1997-2000 John Robbing — All rights reserved.
    УСЛОВНАЯ КОМПИЛЯЦИЯ:
    WORK_AROUND_SRCLINE_BUG —
    Определить данный символ для работы с ошибкой SymGetLineFromAddr; после первого поиска вызывает сбой в поисках PDB-файла. Эта ошибка исправлена в DBGHELP.DLL, но есть обходной путь для пользователей, которым может потребоваться старая версия IMAGEHLP.DLL.
    - - - - - - - - - - - - - - - - - - - - - - - - -*/
    #include "pch.h"
    #include "BugslayerUtil.h"
    #include "CrashHandler.h"
    // Внутренний файл заголовков проекта
    #include "Internal.h"
    /*////////////////////////////////////
    Определения области видимости файла
    ////////////////////////////////////////////////*
    Максимальный размер символов, обрабатываемых в файле
    #define MAX_SYM_SIZE 256
    #define BUFF_SIZE 1024
    #define SYM_BUFF_SIZE 512
    /*///////////////////////////////////////////////////
    Глобальные переменные области видимости файла
    ////////////////////////////////////////////////////*
    // Фильтр необработанных исключений заказчика (обработчик аварий)

    static PFNCHFILTFN g_pfnCallBack = NULL;

    // Оригинальный фильтр необработанных исключений

    static LPTOP_LEVEL_EXCEPTION_FILTER g_pfnOrigFilt = NULL;

    // Массив модулей для ограниченного обработчика аварий

    static HMODULE * g_ahMod = NULL;

    // Размер массива g_ahMod (число элементов)

    static UINT g_uiModCount = 0;

    // Статический буфер, возвращаемый различными функциями. Этот буфер

    // позволяет передавать данные без использования стека,

    static TCHAR g_szBuff [ BUFF_SIZE ];

    // Буфер поиска статических символов

    static BYTE g_stSymbol [ SYM_BUFF_SIZE ];

    // Структура статического исходного файла и номера строки

    static IMAGEHLP_LINE g_stLine;

    // Кадр стека, используемый для его проходов

    static STACKFRAME g_stFrame;

    // Флажки, указывающие, что символьная машина была инициализирована

    static BOOL g_bSymEngInit = FALSE;

    /*////////////////////////////////////////////////////

    Объявление функций области видимости файла

    /////////////////////////////////////////////////////*/

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

    LONG _stdcall CrashHandlerExceptionFilter ( EXCEPTION_POINTERS *

    pExPtrs );

    // Конвертирует простое исключение в строчное значение

    LPCTSTR ConvertSimpleException ( DWORD dwExcept);

    // Внутренняя функция, которая выполняет все проходы по стеку

    LPCTSTR _stdcall

    InternalGetStackTraceString ( DWORD dwOpts ,

    EXCEPTION_POINTERS * pExPtrs);

    // Внутренняя функция SymGetLineFromAddr

    BOOL InternalSymGetLineFromAddr ( IN HANDLE hProcess ,

    IN DWORD dwAddr ,

    OUT PDWORD pdwDisplacement,

    OUT PIMAGEHLP_LINE Line , );

    // Инициализирует символьную машину, если это необходимо void InitSymEng ( void);

    // Очищает символьную машину, если это необходимо

    void CleanupSymEng ( void);

    /*/////////////////////////////////////////////////

    Класс деструктора

    //////////////////////////////////////////////////*/

    // См. Примечание в MEMDUMPVALIDATOR.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;

    }

    if ( NULL != g_pfnOrigFilt)

    {

    У/ Восстановить оригинальный фильтр необрабатываемых

    // исключений.

    SetUnhandledExceptionFilter ( g_pfnOrigFilt);

    }

    }

    };

    // Статический класс

    static CleanUpCrashHandler g_cBeforeAndAfter;

    /*/////////////////////////////////////////////////

    Инициализация функции обработчика аварий

    ////////////////////////////////////////////////*/

    BOOL _stdcall SetCrashHandlerFilter ( PFNCHFILTFN pFn)

    {

    // NULL-параметр "отцепляет" обратный вызов.

    if { NULL == pFn)

    {

    if ( NULL != g_pfnOrigFilt)

    {

    // Восстановить оригинальный фильтр необрабатываемых

    // исключений.

    SetUnhandledExceptionFilter ( g_pfnOrigFilt);

    g_pfnOrigFilt = NULL;

    if ( NULL ! = g_ahMod)

    {

    free ( g_ahMod);

    g_ahMod = NULL;

    }

    g_pfnCallBack = NULL;

    }

    }

    else

    {

    ASSERT ( FALSE == IsBadCodePtr ( (FARPROC)pFn));

    if ( TRUE •== IsBadCodePtr { (FARPROC)pFn))

    {

    return ( FALSE);

    }

    g_pfnCallBack = pFn;

    // Если обработчик аварии заказчика уже используется,

    // активизировать CrashHandlerExceptionFilter и сохранить

    // оригинальный фильтр необрабатываемых исключений.

    if ( 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 |

    HEAP_GENERATE_EXCEPTIONS ,

    ( sizeof ( HMODULE) * ( g_uiModCount+l)) );

    ASSERT ( NULL != phTemp);

    if ( NULL = phTemp)

    {

    TRACE0 ( "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;

    }.

    if ( uiSize < g_uiModCount)

    {

    iRet = GLMA_BUFFTOOSMALL;

    _leave;

    }

    CopyMemory ( pahMod ,

    g_ahMod ,

    sizeof ( HMODULE) * g_uiModCount);

    iRet = GLMA_SUCCESS;

    }

    _except ( EXCEPTION_EXECUTE_HANDLER)

    {

    iRet = GLMA_FAILURE;

    }

    return ( iRet);

    }

    LONG _stdcall GrashHandlerExceptionFilter ( EXCEPTION_POINTERS* pExPtrs) {


    LONG IRet = EXCEPTION_CONTINUE_SEARCH;

    // Если возбуждено исключение EXCEPTION_STACK_OVERFLOW (переполнение

    // стека исключений), то мало что можно сделать, потому что стек

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

    // Хотя я не рекомендую это делать, но можно попробовать

    // управлять регистром стека так, чтобы освободить

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

    //Я избрал безопасный путь и выполняю здесь несколько обращений //к OutputDebugString. Существует риск двойной ошибки, но

    // благодаря тому, что OutputDebugString очень мало использует

    // стек (примерно 8-16 байт), это — стоящий шаг.

    // Ваши пользователи могут также загрузить программу

    // DebugView/Enterprise Edition

    // Марка Руссиновича (Mark Russinovich) с узла www.sysinternals.com.

    // Единственная проблема

    // состоит в том, что я не могу даже удостовериться, что

    //в стеке достаточно места для преобразования указателя команд.

    //К счастью, EXCEPTION_STACK_OVERFLOW не случается очень часто.

    // Заметьте, что я еще вызываю ваш обработчик аварий.

    // Кроме того, в случае если переполненный стек разрушает ваш

    // обработчик аварий, здесь выполняется протоколирование.

    if ( EXCEPTION_STACK_OVERFLOW ==

    pExPtrs->ExceptionRecord->ExceptionCode)

    {

    OutputDebugString ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");

    OutputDebugString ( "EXCEPTION_STACK_OVERFLOW occurred\n"); OutputDebugString ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");

    }

    _try

    {

    if ( NULL != g_pfnCallBack)

    {

    // Здесь должна быть инициализирована символьная машина,

    // так чтобы можно было отыскать информацию базового модуля

    // об адресе аварии, а заодно и привести символьную машину


    //в состояние готовности.

    InitSymEng ();

    // Проверить список g_ahMod.

    BOOL bCalllt = FALSE;

    if ( 0 == g_uiModCount)

    {

    bCalllt = TRUE;

    }

    else {

    HINSTANCE hBaseAddr = (HINSTANCE)

    SymGetModuleBase ((HANDLE)GetCurrentProcessId (), (DWORD)pExPtrs->

    ExceptionRecord->

    ExceptionAddress);

    if ( NULL != hBaseAddr)

    {

    for ( UINT i = 0; i < g__uiModCount; i ++)

    {

    if ( hBaseAddr == g_ahMod[ i ])

    {

    bCalllt = TRUE; break;

    }

    }

    }

    }

    if ( TRUE == bCalllt)

    {

    // Проверить перед вызовом обработчика аварии, что он все еще

    // существует в памяти.

    // Пользователь может забыть зарегистрироваться,

    //и обработчик аварии оказывается недействительным, потому что

    // он не загружен. Однако, если какая-нибудь другая функция

    // загружена в тот же самый адрес, мало что можно сделать.

    ASSERT ( FALSE == IsBadCodePtr( (FARPROC)g_pfnCallBack));

    if ( FALSE == IsBadCodePtr ( (FARPROC)g_pfnCallBack))

    {

    IRet = g_pfnCallBack ( pExPtrs);

    }

    }

    else

    {

    // Вызвать предыдущий фильтр, но только после того,

    // как он закончит работу. Я просто немного параноик!:)

    ASSERT ( FALSE == IsBadCodePtr ( (FARPROC) g__pfnOrigFilt) ) ;

    if ( FALSE == IsBadCodePtr ( (FARPROC)g_pfnOrigFiIt))

    {

    IRet = g_pfnOrigFilt ( pExPtrs);

    }

    }

    CleanupSymEng ();

    }

    }

    _except ( EXCEPTION_EXECUTE_HANDLER)

    {

    IRet = EXCEPTION_CONTINUE_SEARCH;

    }

    return ( IRet);

    }

    /*/////////////////////////////////////////////

    Реализация функций-трансляторов EXCEPTION_POINTER-структyp

    //////////////////////////////////////////////*/

    LPCTSTR _stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs) {

    ASSERT ( FALSE == IsBadReadPtr ( pExPtrs,

    sizeof ( EXCEPTION_POINTERS)));

    if ( TRUE = IsBadReadPtr ( pExPtrs,

    sizeof ( EXCEPTION_POINTERS)))

    {

    TRACEO ( "Bad parameter to GetFaultReasonA\n");


    return ( NULL);

    }

    // Переменная, которая содержит возвращаемое значение

    LPCTSTR szRet;

    _try

    {

    // Инициализировать символьную машину в случае, если она

    //не инициализирована.

    InitSymEng ();

    // Текущая позиция в буфере

    int iCurr = 0;

    // Временное хранилище значения. Оно позволяет свести

    // использование стека к минимуму.

    DWORD dwTemp;

    iCurr += BSUGetModuleBaseName ( GetCurrentProcess (),

    NULL

    , g_szBuff ,

    BUFF_SIZE );

    iCurr += wsprintf ( g_szBuff + iCurr, _T ( " caused an "));

    dwTemp = (DWORD)

    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 )

    * sizeof ( TCHAR));

    }

    ASSERT ( iCurr < ( BUFF_SIZE - MAX_PATH));

    iCurr += wsprintf ( g_szBuff + i.Curr, _T ( " in module ") ) ;

    dwTemp =

    SymGetModuleBase ( (HANDLE)GetCurrentProcessId (),

    (DWORD)pExPtrs->ExceptionRecord->

    ExceptionAddress);

    ASSERT ( NULL != dwTemp);

    if ( NULL == dwTemp)

    {

    iCurr += wsprintf ( g_szBuff + iCurr, _T ( ""));

    }

    else

    {

    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_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&g_stSymbol;

    FillMemory ( pSym, NULL, SYM_BUFF_SIZE);

    pSym->SizeOfStruct = sizeof ( IMAGEHLP_SYMBOL);

    pSym->MaxNameLength = SYM_BUFF_SIZE - sizeof ( IMAGEHLP_SYMBOL);

    DWORD dwDisp;

    if ( TRUE ==

    SymGetSymFromAddr ( (HANDLE)GetCurrentProcessId () ,

    (DWORD)pExPtrs->ExceptionRecord->

    ExceptionAddress ,

    SdwDisp ,

    pSym ) )

    {

    iCurr += wsprintf ( g_szBuff + iCurr, _T ( ", "));

    // Копировать не больше символьной информации, чем

    // позволяет память.

    dwTemp = Istrlen ( pSym->Name);

    // Удостовериться, что достаточно места для самого длинного

    // символа и смещения.

    if ( (int)dwTemp > ( ( BUFF_SIZE _ iCurr) -

    ( MAX_SYM_SIZE +50) ))

    {

    Istrcpyn ( g_szBuff + iCurr ,

    pSym->Name ,

    BUFF_SIZE - iCurr - 1 );

    // Теперь можно выйти

    szRet = g_szBuff; _leave;

    }

    else

    {

    if ( dwDisp > 0)

    {

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T ( "%s()+%04d byte(s)"),

    pSym->Name ,

    dwDisp );

    }

    else

    {

    iCurr += wsprintf ( g_szBuff + iCurr,

    _T ( "%s ") pSym->Name ) ;

    }

    }

    }

    else

    {

    // Если символ не найден, то источник и номер строки

    // тоже не найдены, поэтому выходим сейчас.

    szRet = g_szBuff;

    _leave;

    }

    ASSERT ( iCurr < ( BUFF_SIZE _ 200));

    // Поиск исходного файла и номера строки.

    ZeroMemory ( &g_stLine, sizeof ( IMAGEHLP_LINE));

    g_stLine.SizeOfStruct = sizeof ( IMAGEHLP_LINE);

    if ( TRUE ==

    InternalSymGetLineFromAddr ( (HANDLE)

    GetCurrentProcessId () ,

    (DWORD)pExPtrs->

    ExceptionRecord->

    ExceptionAddress,

    SdwDisp ,

    &g_stLine ))

    {

    iCurr += wsprintf ( g_szBuff + iCurr, _T ( ", "));


    // Копировать не больше информации об исходном файле

    //и номере строки, чем позволяет память.

    dwTemp = Istrlen ( g_stLine.FileName);

    if ( (int)dwTemp > ( BUFF_SIZE - iCurr

    _ MAX_PATH _ 50 ))

    {

    Istrcpyn ( g_szBuff + iCurr ,

    g_stLine.FileName ,

    BUFF_SIZE - iCurr - 1);

    // Теперь можно выйти

    szRet = g_szBuff;

    _leave;

    }

    else

    {

    if ( dwDisp > 0)

    {

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T("%s, line %04d+%04d byte(s)"),

    g_stLine.FileName ,

    g_stLine. LineNumber ,

    dwDisp ) ;

    }

    else

    {

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T ( "%s, line %04d"),

    g_stLine.FileName ,

    g_stLine.LineNumber);

    }

    }

    }

    szRet = g_szBuff;

    }

    _except ( EXCEPTION_EXECUTE_HANDLER)

    {

    ASSERT ( !"Crashed in GetFaultReason");

    szRet = NULL;

    }

    return ( szRet);

    }

    BOOL _stdcall GetFaultReasonVB ( EXCEPTION_POINTERS * pExPtrs,

    LPTSTR szBuff , UINT uiSize )

    {

    ASSERT ( FALSE == IsBadWritePtr ( szBuff, uiSize));

    if ( TRUE == IsBadWritePtr ( szBuff, uiSize))

    {

    return ( FALSE);

    }

    LPCTSTR szRet;

    __try

    {

    szRet = GetFaultReason ( pExPtrs);

    ASSERT ( NULL != szRet);

    if ( NULL == szRet)

    {

    _leave;

    }

    Istrcpyn ( szBuff ,

    szRet ,

    min ( (UINT)Istrlen ( szRet) + 1, uiSize));

    }

    _except ( EXCEPTION_EXECUTE_HANDLER)

    {

    szRet = NULL;

    }

    return ( NULL != szRet);

    }

    LPCTSTR BUGSUTIL_DLLINTERFACE _stdcall

    GetFirstStackTraceString ( DWORD dwOpts ,

    EXCEPTION_POINTERS * pExPtrs)

    {

    // Все проверки ошибок выполняются в функции

    // InternalGetStackTraceString

    // Инициализировать структуру STACKFRAME .

    ZeroMemory ( &g_stFrame, sizeof ( STACKFRAME));

    #ifdef _X86_

    g_stFrame.AddrPC.Offset = pExPtrs->ContextRecord->Eip;

    g_stFrame.AddrPC.Mode = AddrModeFlat ;


    g_stFrame.AddrStack.Offset = pExPtrs->ContextRecord->Esp;

    g_stFrame.AddrStack.Mode = AddrModeFlat ;

    g_stFrame.AddrFrame.Offset = pExPtrs->ContextRecord->Ebp;

    g_stFrame.AddrFrame.Mode = AddrModeFlat ;

    #else

    g_stFrame.AddrPC.Offset = (DWORD)pExPtrs->ContextRecord->Fir;

    g_stFrame.AddrPC.Mode = AddrModeFlat;

    g_stFrame.AddrReturn.Offset =

    (DWORD)pExPtrs->ContextRecord->IntRa;

    g_stFrame.AddrReturn.Mode = AddrModeFlat;

    g_stFrame.AddrStack.Offset =

    (DWORD)pExPtrs->ContextRecord->IntSp;

    g_stFrame.AddrStack.Mode = AddrModeFlat;

    g_stFrame.AddrFrame.Offset -

    (DWORD)pExPtrs->ContextRecord->IntFp;

    g_stFrame.AddrFrame.Mode = AddrModeFlat;

    #endif

    return ( InternalGetStackTraceString ( dwOpts, pExPtrs));

    }

    LPCTSTR BUGSUTIL_DLLINTERFACE _stdcall

    GetNextStackTraceString ( DWORD dwOpts ,

    EXCEPTION_POINTERS * pExPtrs)

    {

    // Все проверки ошибок — в InternalGetStackTraceString.

    // Предполагается, что GetFirstStackTraceString уже

    // инициализировала информацию в кадре стека,

    return ( InternalGetStackTraceString ( dwOpts, pExPtrs));

    }

    BOOL _stdcall CH__ReadProcessMemory ( HANDLE ,

    LPCVOID IpBaseAddress ,

    LPVOID IpBuffer ,

    DWORD nSize ,

    LPDWORD IpNumberOfBytesRead )

    {

    return ( ReadProcessMemory ( GetCurrentProcess (),

    IpBaseAddress ,

    IpBuffer ,

    nSize ,

    IpNumberOfBytesRead ));

    }

    // Внутренняя функция, которая выполняет все проходы по стеку

    LPCTSTR _stdcall

    InternalGetStackTraceString ( DWORD dwOpts ,

    EXCEPTION_POINTERS * pExPtrs )

    {

    ASSERT ( FALSE == IsBadReadPtr ( pExPtrs

    sizeof ( EXCEPTION_POINTERS)));

    if ( TRUE == IsBadReadPtr ( pExPtrs ,

    sizeof ( EXCEPTION_POINTERS)))

    {

    TRACED ( "GetStackTraceString — invalid pExPtrs!\n");

    return ( NULL);

    }

    // Возвращаемое значение LPCTSTR szRet;

    // Временная переменная для общего пользования.


    Эта переменная

    // сохраняет область стека. DWORD dwTemp;

    // Базовый адрес модуля. Я отыскиваю его прямо после прохода

    // по стеку, чтобы убедиться, что стек правильный.

    DWORD dwModBase;

    _try

    {

    // Инициализировать символьную машину в случае, если она

    //не инициализирована.

    InitSymEng ();

    #ifdef _WIN64

    #define CH_MACHINE IMAGE_FILE_MACHINE_IA64

    #else

    #define CH_MACHINE IMAGE_FILE_MACHINE_I386

    #endif

    // Замечание: Если функции исходных файлов и номеров строк

    // используются, StackWalk может вызвать останов по нарушению

    // доступа.

    BOOL bSWRet = StackWalk ( CH_MACHINE ,

    (HANDLE)GetCurrentProcessId () ,

    GetCurrentThread () ,

    &g_stFrame ,

    pExPtrs->ContextRecord ,

    (PREAD_PROCESS_MEMORY_ROUTINE)

    CH_ReadProcessMemory ,

    SymFunctionTableAccess ,

    SymGetModuleBase ,

    NULL ) ;

    if ( ( FALSE = bSWKet) 11(0= g_stFrame.AddrFrame.Offset))

    {

    szRet = NULL;

    _leave;

    }

    // Прежде чем все подсчитывать,

    //я должен перепроверить, что адрес, возвращенный

    // из StackWalk, действительно существует. Бывает,

    // что StackWalk, возвращает TRUE, но адрес не принадлежит

    // модулю процесса.

    dwModBase = SymGetModuleBase ( (HANDLE)GetCurrentProcessId (),

    g_stFrame.AddrPC.Offset );

    if ( 0 == dwModBase)

    {

    szRet = NULL;

    _leave;

    }

    int iCurr = 0;

    // Как минимум получить адрес,

    #ifdef _WIN64

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T ( "Ox%016X") ,

    g_stFrame.AddrPC.Offset );

    #else

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T ( "%04X:%08X")

    pExPtrs->ContextReeord->SegCs, g_stFrame.AddrPC.Offset );

    #endif

    // Вывести параметры?

    if ( GSTSO_PARAMS == ( dwOpts & GSTSO_PARAMS))

    {

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T ( " ( Ox%08X Ox%08X "\

    "Ox%08X Ox%08X)"),

    g_stFrame.Params[ 0 ] ,


    g_stFrame.Params[ 1 ] ,

    g_stFrame.Params[ 2 ] ,

    g_stFrame.Params[ 3 ] );

    }

    // Вывести имя модуля.

    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));

    DWORD dwDisp;

    // Вывести имя символа?

    if ( GSTSO_SYMBOL == ( dwOpts & GSTSO_SYMBOL))

    {

    // Начать поиск адреса исключения.

    PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)Sg_stSymbol;

    ZeroMemory ( pSym, SYM_BUFF_SIZE);

    pSym->SizeOfStruct = sizeof ( IMAGEHLP_SYMBOL);

    pSym->MaxNameLength = SYM_BUFF_SIZE -

    sizeof ( IMAGEHLP_SYMBOL);

    if ( TRUE ==

    SymGetSymFromAddr ( (HANDLE)GetCurrentProcessId () ,

    g_stFrame.AddrPC.Offset ,

    sdwDisp ,

    pSym ))

    {

    iCurr += wsprintf ( g_szBuff + iCurr, _T ( ", "));

    // Копировать не больше символьной информации, чем

    // позволяет память.

    dwTeitip = Istrlen ( pSym->Name) ;

    if ( dwTeitip > (DWORD) ( BUFF_SIZE - iCurr _

    ( MAX_SYM_SIZE + 50)))

    {

    Istrcpyn ( g_szBuff + iCurr ,

    pSym->Name ,

    BUFF_SIZE - iCurr - 1 );

    // Теперь можно выйти

    szRet = g_szBuff;

    _leave;

    }

    else

    {

    if ( dwDisp > 0)

    {

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T( "%s()+%04d byte(s)"),

    pSym->Name ,

    dwDisp ) ;

    }

    else

    {

    iCurr += wsprintf ( g_szBuff + iCurr,

    _T ( "%s")

    pSym->Name ) ;

    }

    }

    }

    else

    {

    // Если символ не был найден, то исходный файл и номер

    // строки тоже не будут найдены, поэтому выйти сейчас.

    szRet = g_szBuff;

    _leave;

    }

    }

    ASSERT ( iCurr < ( BUFF_SIZE - MAX_PATH));


    // Вывести информацию исходного файла и номера строки?

    if ( GSTSO_SRCLINE == ( dwOpts & GSTSO_SRCLINE))

    {

    ZeroMemory ( &g_stLine, sizeof ( IMAGEHLP_LINE));

    g_stLine.SizeOfStruct = sizeof ( IMAGEHLP_LINE);

    if ( TRUE ==

    InternalSymGetLineFromAddr ( (HANDLE)

    GetCurrentProcessId (),

    g_stFrame.AddrPC.Offset ,

    SdwDisp ,

    &g_stLine ))

    {

    iCurr += wsprintf ( g_szBuff + iCurr, _T ( ", "));

    // Копировать не больше информации об исходном файле и

    // номере строки, чем позволяет память.

    dwTemp = Istrlen ( g_stLine.FileName);

    if ( dwTerap > (DWORD)( BUFF_SIZE - iCurr

    _ ( MAX_PATH +50 ))) {

    Istrcpyn ( g_szBuff + iCurr ,

    g_stLine.FileName ,

    BUFF_SIZE - iCurr - 1 );

    // Теперь можно выйти

    szRet = g_szBuff;

    _leave;

    }

    else

    {

    if { dwDisp > 0)

    {

    iCurr += wsprintf(g_szBuff + iCurr ,

    _T("%s, line %04d+%04d byte(s)"),

    g_stLine.FileName ,

    g_s tLine.LineNumbe r ,

    dwDisp ) ;

    }

    else

    {

    iCurr += wsprintf ( g_szBuff + iCurr ,

    _T ( "%s, line %04d") ,

    g_stLine.FileName ,

    g_stLine.LineNumber );

    }

    }

    }

    }

    szRet = g_szBuff;

    }

    _except ( EXCEPTION_EXECUTE_HANDLER)

    {

    ASSERT ( !"Crashed in InternalGetStackTraceString");

    szRet = NULL;

    }

    return ( szRet);

    }

    BOOL _stdcall

    GetFirstStackTraceStringVB ( DWORD dwOpts ,

    EXCEPTION_POINTERS * pExPtrs,

    LPTSTR szBuff ,

    UINT uiSize )

    {

    ASSERT ( FALSE == IsBadWritePtr ( szBuff, uiSize));

    if ( TRUE = IsBadWritePtr ( szBuff, uiSize))

    {

    return ( FALSE);

    }

    LPCTSTR szRet;

    _try

    {

    szRet = GetNextStackTraceString ( dwOpts, pExPtrs);

    if ( NULL == szRet)

    {

    _leave;

    }

    Istrcpyn ( szBuff , szRet ,

    min ( (UINT)lstrlen ( szRet) + I, uiSize));

    }

    _except ( EXCEPTION_EXECUTE_HANDLER)

    {

    szRet = NULL;


    }

    return ( NULL != szRet);

    }

    LPCTSTR _stdcall GetRegisterString ( EXCEPTION_POINTERS * pExPtrs) {

    // Проверить параметр.

    ASSERT ( FALSE = IsBadReadPtr ( pExPtrs ,

    sizeof ( EXCEPTION_POINTERS)));

    if { TRUE = IsBadReadPtr ( pExPtrs ,

    sizeof ( EXCEPTION_POINTERS)))

    {

    TRACED ( "GetRegisterString - invalid pExPtrs!\n");

    return ( NULL);

    }

    #ifdef _WIN64

    ASSERT ( !"IA64 is not supported (YET!) ");

    #else

    // Этот вызов помещает в стек 48 байт, что может стать проблемой,

    // если он переполнен,

    wsprintf ( g_szBuff,

    _Т ( "ЕАХ=%08Х ЕВХ=%08Х ЕСХ=%08Х EDX=%08X ESI=%08X\n"\

    "EDI=%08X EBP=%08X ESP=%08X EIP=%08X FLG=%08X\n"\

    "CS=%04X DS=%04X SS=%04X ES=%04X "\

    "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 ,

    pExPtrs->ContextRecord->Eip ,

    pExPtrs->ContextRecord->EFlags ,

    pExPtrs->ContextRecord->SegCs ,

    pExPtrs->ContextRecord->SegDs ,

    pExPtrs->ContextRecord->SegSs ,

    pExPtrs->ContextRecord->SegEs ,

    pExPtrs->ContextRecord->SegFs ,

    pExPtrs->ContextRecord->SegGs );

    #endif

    return ( g_szBuff);

    }

    BOOL _stdcall GetRegisterStringVB ( EXCEPTION_POINTERS * pExPtrs,

    LPTSTR szBuff , UINT uiSize )

    {

    ASSERT ( FALSE == IsBadWritePtr ( szBuff, uiSize));

    if ( TRUE == IsBadWritePtr ( szBuff, uiSize))

    {

    return ( FALSE);

    }

    LPCTSTR szRet;

    _try

    {

    szRet = GetRegisterString ( pExPtrs);

    if ( NULL = szRet)

    {

    _leave;

    }

    Istrcpyn ( szBuff , szRet ,

    min ( (UINT)Istrlen ( szRet) + 1, uiSize));

    }

    _except ( EXCEPTION_EXECUTE_HANDLER) {

    szRet = NULL;

    }

    return ( NULL != szRet);

    }


    LPCTSTR ConvertSimpleException ( DWORD dwExcept)

    {

    switch ( dwExcept)

    {

    case EXCEPTION_ACCESS_VIOLATION :

    return ( _T ( "EXCEPTION_ACCESS_VIOLATION"));

    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_BOUNDSJEXCEEDED

    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;

    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;

    default :

    return ( NULL);

    break;

    }

    }

    BOOL InternalSymGetLineFromAddr ( IN HANDLE hProcess ,

    IN DWORD dwAddr ,

    OUT PDWORD pdwDisplacement,

    OUT PIMAGEHLP_LINE Line )

    {

    #ifdef WORK_AROUND_SRCLINE_BUG

    // Проблема заключается в том, что символьная машина находит

    // только те адреса исходных строк (после первого поиска), которые

    // попадают точно на нулевое смещение. Сместимся назад на 100 байт,

    // чтобы найти строку, и вернем подходящее смещение.

    DWORD dwTempDis = 0;

    while ( FALSE == SymGetLineFromAddr ( hProcess ,

    dwAddr -

    dwTempDis ,

    pdwDisplacement,

    Line ) )

    {

    dwTempDis += 1;

    if ( 100 == dwTempDis)

    {

    return ( FALSE);

    }

    }

    // Строка найдена и информация исходной строки корректна,

    // поэтому нужно изменить смещение, если требуется

    // выполнить обратный поиск, чтобы найти исходную строку,

    if ( 0 != dwTempDis)

    {

    *pdwDisplacement = dwTempDis;

    }

    return ( TRUE);

    #else // WORK_AROUND_SRCLINE_BUG

    return ( SymGetLineFromAddr ( hProcess ,

    dwAddr ,

    pdwDisplacement ,

    Line ));

    #endif

    }

    // Инициализировать символьную машину, если необходимо

    void InitSymEng ( void)

    {

    if ( FALSE == g_bSymEngInit)

    {

    // Установить символьную машину.

    DWORD dwOpts = SymGetOptions ();

    // Включить загрузку строки и отложенную загрузку.

    SymSetOptions ( dwOpts |

    SYMOPT_DEFERRED_LOADS |

    SYMOPT_LOAD_LINES );


    // Форсировать флажок захватывающего процесса, независимо от

    // того, в какой операционной системе мы находимся.

    HANDLE hPID = (HANDLE)GetCurrentProcessId ();

    VERIFY ( BSUSymlnitialize ( (DWORD)hPID,

    hPID ,

    NULL ,

    TRUE ));

    g_bSymEngInit = TRUE;

    }

    }

    // Очистить символьную информацию, если необходимо,

    void CleanupSymEng ( void) CrashHandler

    {

    if ( TRUE == g_bSymEngInit)

    {

    VERIFY ( SymCleanup ( (HANDLE)GetCurrentProcessId ()));

    g_bSymEngInit = FALSE;

    }

    }

    Чтобы установить свою функцию фильтра, просто вызовите функцию SetCrashHandierFiiter, которая сохраняет вашу функцию фильтра в статической переменной И вызывает функцию SetUnhandledExceptionFilter, чтобы установить реальный фильтр Исключений CrashHandlerExceptionFilter. Если вы не добавляете какие-то модули, которые ограничивают фильтрацию исключений, то CrashHandlerExceptionFilter будет всегда вызывать ваш фильтр, независимо от того, в каком модуле произошел тяжелый останов. Если никакие модули не были добавлены, то вызов вашего фильтра исключений соответствует проекту, поэтому, чтобы установить заключительную обработку исключений, надо использовать только один API-вызов. Лучше всего сразу же вызвать функцию SetCrashHandierFiiter и убедиться, что она вызывается с NULL-аргументом непосредственно перед разгрузкой вашего фильтра, что позволит удалить вашу функцию фильтра с помощью рассмотренного обработчика аварий.

    Функция AddCrashHandierLimitModuie вызывается там, где вы добавляете модуль, чтобы ограничить обработку аварий. Все, что требуется передать данной функции — это дескриптор добавляемого модуля (через ее единственный параметр HMODULE hMod). Если нужно ограничить обработку аварий для нескольких модулей, просто вызовите AddCrashHandierLimitModuie для каждого модуля. Память для массива дескрипторов модулей распределяется из кучи главного процесса.

    Если посмотреть на различные функции в листинге 9-5, то можно заметить, что в них нет вызовов каких-либо функций исполнительной (run-time) библиотеки C++.


    Поскольку подпрограммы обработчика аварий вызываются только в экстраординарных ситуациях, нельзя быть уверенным, что функции этой библиотеки находятся в устойчивом состоянии. Чтобы очистить любую распределенную мною память, используется автоматический статический класс, деструктор которого вызывается, когда библиотека BUGSLAYERUTIL.DLL не загружена. Я также включил функции GetLimitModuleCount И GetLimitModulesArray, Позволяющие получить размер ограничивающего модуля и копию массива таких модулей. Написание функции RemoveCrashHandlerLimitModule Предоставляется читателю.

    Интересным аспектом реализации CRASHHANDLER.CPP является обработка инициализации символьной машины DBGHELP.DLL. Поскольку код обработчика аварий может быть вызван в любой момент, нужно было, чтобы во время аварии загружались все модули процесса. Об этом автоматически позаботится функция Syminitiaiize, если ее третий параметр (finvadeProcess) установить в значение TRUE. К сожалению, захват процесса и загрузка всех модулей будет работать только в Windows 2000, но не в Windows 98. Чтобы обеспечить такое же поведение и в Windows 98, следует использовать функцию Bsusyminitialize из BUGSLAYERUTIL.DLL, которая разыскивает все необходимые модули и загружает их по одному.

    Комбинирование обработки SEH- и С++-исключений

    Как было сказано выше, существует возможность сочетания обработки SEH- и С++-исключений. В результате программист получает возможность использовать в своем коде только обработку исключений языка C++. Функция _set_se_transiator исполнительной библиотеки языка C++ позволяет установить специальную транслирующую функцию, которая будет вызываться, когда случается структурированное исключение, так что вы можете возбуждать только исключения C++. Мощь этой функции не видна с первого взгляда. Следующий отрывок кода показывает, что должна делать функция-транслятор:
    void SEHToCPPException ( UINT uiEx,
    EXCEPTION_POINTERS * pExp)
    {
    // CSEHException — это класс, производный от MFC-класса CException
    throw CSEHException ( uiEx, pExp);
    }
    Первый параметр — это SEH-код, возвращаемый при вызове функции GetExceptionCode, а второй — состояние исключения из вызова функции GetExceptionlnformation.
    При использовании функции _set_se_transiator необходимо отлавливать классы исключений, возбуждаемых этой функцией только в том случае, если ожидается авария. Например, если вы разрешаете пользователям расширять приложение с помощью DLL, то для обработки потенциальных аварий вызовы соответствующих DLL можно разместить в блоках try.. .catch. Однако, если в ходе нормальной обработки происходит тяжелая SEH-авария, следует завершить выполнение приложения. В одной из своих программ я случайно обработал нарушение доступа вместо обычного аварийного останова. В результате, вместо того чтобы просто оставить пользовательские данные в покое, я стер несколько файлов.
    Причина, по которой необходимо соблюдать осторожность, обрабатывая тяжелые SEH-исключения как исключения языка C++, заключается в том, что процесс находится в нестабильном состоянии. В ходе обработки вы можете открывать диалоговые окна и записывать аварийную информацию в файл. Однако следует помнить, что стек может быть переполнен, так что места для вызова большого количества функций может быть явно недостаточно. Поскольку код исключения вам передает функция-транслятор, то необходимо проверить, не имеет ли он значение EXCEPTION_STACK_OVERFLOW, и плавно снизить интенсивность обработки ошибок, если места в стеке недостаточно.

    Как видно по предшествующему фрагменту кода (который переводит SEH-исключение в исключение языка C++), вы можете возбуждать исключения любого типа (класса), какие пожелаете. Реализация класса исключений тривиальна; интерес представляет лишь та ее часть, где выполняется перевод информации EXCEPTION_POINTERS в удобочитаемую форму. Но перед подробным описанием этого кода рассмотрим кратко асинхронную и синхронную обработку исключений в языке C++.

    Асинхронная и синхронная обработка исключений в C++

    Используя обработку исключений языка C++ необходимо понимать различие между асинхронной и синхронной обработкой исключений. К сожалению, термины асинхронная и синхронная не вполне адекватно описывают различие между этими двумя типами обработки исключений в C++. Реальное различие между асинхронной и синхронной обработкой исключений в C++ сводится к следующему: компилятор генерирует код обработки исключений для программы в зависимости от того, как, по его предположению, будут возбуждаться исключения.

    При асинхронной обработке исключений в C++ компилятор предполагает, что каждая инструкция может генерировать исключение, и что код должен быть готов обрабатывать исключения где угодно. В Visual C++ 5 модель обработки исключений по умолчанию была асинхронной. Проблема с асинхронной обработкой исключений заключается в том, что компилятор должен прослеживать время жизни объектов и быть готовым к "раскрутке" исключений в любой точке кода. Весь дополнительно сгенерированный код может быть достаточно объемным (и при этом нередко бесполезным).

    При синхронной обработке исключений, которая используется по умолчанию в Visual C++ 6, компилятор ожидает, что программа возбуждает исключения только с помощью явного оператора throw. Таким образом, компилятор не должен прослеживать продолжительность жизни объекта и не должен генерировать код, необходимый для обработки "раскручивания", если время жизни объекта не перекрывает вызова функции или оператора throw. Понятия асинхронности и синхронности в данном контексте можно сформулировать примерно так: "Асинхронность имеет место тогда, когда все функции программы отслеживают время жизни объектов, а синхронность — когда лишь некоторые функции отслеживают это время".


    Влияние ключевого умалчиваемого параметра компилятора на синхронную обработку исключений заключается в том, что в финальных построениях (релизах) Программы Тщательно сконструированная функция _set_se_translator никогда не вызывается, код не отлавливает транслированных исключений, а приложение завершается аварийно (как и положено нормальному приложению!). По умолчанию ключ /GX отображается в /EHSC (синхронные исключения), поэтому, чтобы включить асинхронные исключения, необходимо явно указать ключ /ЕНa (асинхронные исключения). К счастью, разработчик не должен активизировать асинхронные исключения на уровне всего проекта — можно без проблем компилировать разные исходные файлы с различной обработкой исключений и связывать их вместе.

    Если требуется компилировать программу с асинхронной обработкой исключений (с ключом /ЕНa), но без издержек на отслеживание времени жизни объектов на функциях, которые не возбуждают исключений, то для объявления или определения таких функций можно использовать спецификатор _decispec(nothrow). Хотя нужны дополнительные затраты, чтобы вручную вставлять в программу эти спецификаторы, но вы получите дополнительные выгоды от применения функции _set_se_translator и более "плотного" кода.

    Листинг 9-4 демонстрирует программу, использующую функцию _set_se_ translator, которая не работает, если компилируется как финальное построение с умалчиваемыми (т. е. синхронными) исключениями. Для использования асинхронных исключений эта программа должна компилироваться с ключом /ЕНа. Таким образом, если нужно гарантировать повсеместное применение функции _set_se_transiator, включая функции, расположенные вне классов, то необходимо выполнять компиляцию с ключом /ЕНа и смириться с большим объемом дополнительного кода. Для работы с синхронными исключениями (что особенно полезно, если программа на C++ использует классы MFC) следует применять класс, выброшенный функцией _set_se_transiator, только в методах (функциях-членах) используемых классов.

    Листинг 9-4.


    Пример, в котором синхронные исключения не работают

    // Компилировать как выпуск (версию) Win32, используя ключ /GX,

    // чтобы убедиться, что функция-транслятор не будет вызываться.

    // (Ключ /GX отображается в /EHsc.) Чтобы заставить эту программу

    // работать в финальных построениях, компилируйте ее с ключом /ЕНа.

    #include "stdafx.h"

    class CSEHError

    {

    public :

    CSEHError ( void)

    {

    m_uiErrCode = 0;

    }

    CSEHError ( unsigned int u)

    {

    m_uiErrCode = u;

    }

    ~CSEHError ( void)

    {

    }

    unsigned int m_uiErrCode;

    };

    void TransFunc ( unsigned int u, EXCEPTION_POINTERS * pEP)

    {

    printf ( "In TransFuncXn");

    throw CSEHError ( u);

    }

    void GrungyFunc ( char * p)

    {.

    *P = 'p';

    printf ( "This output should never be seen!\n");

    }

    void DoBadThings ( void)

    {

    try

    {

    GrungyFunc ( (char*)0xl);

    }

    catch ( CSEHError e)

    {

    printf ( "Got an exception! -> Ox%08X\n", e.m_uiErrCode);

    }

    }

    int main ( int argc, char* argv[])

    {

    _set_se_translator ( TransFunc);

    DoBadThings ();

    return 0;

    }

    Обработка исключений средствами языка C++

    Поскольку обработка исключений является частью спецификации языка C++, она, вероятно, больше знакома большинству программистов, чем SEH-обработка. Ключевые слова для организации обработки исключений в языке C++ — это try и catch. Ключевое слово throw позволяет начать раскрутку исключения. Тогда как коды ошибок SEH-обработчиков представлены одиночным целым числом без знака, оператор catch языка C++ может обрабатывать любой тип переменных, включая классы. Если вы выводите свои классы обработки ошибок из общего базового класса, то можете обрабатывать почти любую ошибку в своей программе. Именно этот иерархический подход к обработке ошибок применяется в библиотеке классов MFC (Microsoft Foundation Class) при работе с базовым классом CException. В листинге 9-3 показана обработка исключений C++ при чтении заголовка файла MFC-класса CFiie.
    Листинг 9-3.Пример обработчика исключений язака С++
    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:rendOfFile == e->m_cause)
    {
    e->Delete(); bRet = false;
    }
    else {
    // Оператор throw выбрасывет исключение, которое
    // передается в этот catch-блок.
    throw;
    }
    }
    return ( bRet);
    }
    При использовании обработки исключений языка C++ нужно иметь в виду следующие ее недостатки. Во-первых, аварии приложений не обрабатываются автоматически. (Позже мы рассмотрим работу с этим ограничением.) Во-вторых, обработка исключений в C++ не бесплатна. Компилятор может выполнять большую работу, устанавливая и удаляя блоки try и catch, даже если вы никогда не возбуждаете никаких исключений, так что если ваш код чувствителен к производительности, вы не можете позволить себе такие большие издержки. Хотя эти случаи редки, но они происходят. Если вы плохо знакомы с обработкой исключений в C++, то MSDN — самое подходящее место, чтобы начать ее изучение.


    Структурированная обработка исключений (SEH) и обработка исключений в C++

    Ускорение обработки исключений (exeption handling) — довольно сложная задача, потому что в C++ используется два основных типа обработки исключений: структурированная обработка исключений (Structured Exception Handling — SEH), которую обеспечивает операционная система, и обработка исключений средствами языка C++. Выбирать, какой тип обработки исключений и когда использовать, достаточно сложно, причем этому выбору не помогает даже то, что многие считают оба типа взаимозаменяемыми. Дело в том, что каждый тип обработки исключений использует совершенно различный подход. Думаю, многих сбивает с толку то, что можно комбинировать оба типа. В следующих разделах описаны сходства и различия этих двух типов обработки исключений. Рассмотрены, кроме того, некоторые варианты их совместного использования.


    Структурированная обработка исключений

    Структурированную обработку исключений (SEH) обеспечивает операционная система. Она напрямую связана с такими авариями, как нарушение доступа. SEH-обработка не зависит от языка и в программах C/C++ обычно реализуется парами ключевых слов _try/_except и _try/_finally. Методика использования пары _try/_except такова: сначала нужно установить свой код внутри блока _try, затем определить, как следует обрабатывать исключения в блоке _except (который называют также обработчиком исключений (exception handler)). В паре _try/_finally блок _finally (именуемый также обработчиком завершения (termination handler)) гарантирует, что данная секция кода будет всегда выполняться после выхода из функции (в которой размещается пара _try/_except), даже если код в блоке _try завершится преждевременно.
    Листинг 9-1 содержит типичную функцию с SEH-обработкой. Блок _except выглядит почти как вызов функции, но в круглых скобках указывается значение специального выражения, называемого фильтром исключения (exception filter). В листинге 9-1 значение фильтра исключения (EXCEPTION_ EXECUTE_HANDLER) указывает на то, что код в блоке except должен выполняться каждый раз, когда внутри блока try происходит какое-нибудь (любое) исключение. Два других возможных значения фильтра исключения — EXCEPTION_CONTINUE_EXECUTION (позволяет проигнорировать исключение ) и EXCEPTION_CONTINUE_SEARCH (передает исключение через цепочку вызовов следующему блоку except). Можно сделать выражение фильтра исключений настолько простым или сложным, насколько это необходимо, что позволяет разработчику планировать только те исключения, в обработке которых он заинтересован.
    Листинг 9-1. Пример SEH-обработчика
    void Foo ( void) {
    __try
    {
    _try
    {
    // Выполняемый код (любой).
    }
    _except ( EXCEPTION_EXECUTE_HANDLER)
    {
    // Этот блок будет выполняться, если в коде блока try
    // возникает нарушение доступа или любой другой фатальный
    // останов. Данный код также называют обработчиком исключения.
    }
    }

    _finally

    {

    // Этот блок будет выполняться независимо от того, вызывает

    // функция аварийный останов или нет. Здесь должен быть размещен

    // код обязательной чистки.

    }

    }

    Процесс поиска и выполнения обработчика исключений иногда называют раскруткой исключения (unwinding the exception). Обработчики исключений хранятся во внутреннем стеке. По мере роста цепочки вызовов функций обработчик исключений каждой новой функции (если он существует) помещается в этот внутренний стек. Когда происходит исключение, операционная система находит стек обработчика исключений потока и начинает вызывать обработчики исключений до тех пор, пока один из них не укажет, что именно он будет обрабатывать исключение. Как только исключение пройдет весь стек в поисках "своего" обработчика исключений, операционная система очищает стек вызовов и выполняет любые обработчики завершения, которые она находит. Если "раскручивание" продолжается до конца стека обработчиков исключений, то раскрывается диалоговое окно Application Error.

    Ваш обработчик исключений может определять значение исключения с помощью специальной функции GetExceptionCode, которая вызывается только в фильтрах исключений. Если бы вы, например, писали математический пакет, то могли бы иметь обработчик исключений, который обрабатывает попытку "деления на нуль" и возвращает значение NaN (не число). Пример такого обработчика исключений показан в листинге 9-2. Фильтр исключений вызывает функцию GetExceptionCode, и если исключение есть "деление на нуль", то обработчик исключений выполняется. Если происходит любое другое исключение, то EXCEPTION_CONTINUE_SEARCH сообщает операционной системе, что нужно выполнять следующий блок _except (вверх по цепочке вызовов).

    Листинг 9-2. Пример SEH-обработчика с обработкой фильтра исключения

    long IntegerDivide ( long x, long у)

    {

    long IRet; _try

    {

    IRet = x / y;

    }

    _except ( EXCEPTION_INT_DIVIDE_BY_ZERO ==


    GetExceptionCode ()

    ? EXCEPTION_EXECUTE_HANDLER

    : EXCEPTION_CONTTNUE_SEARCH

    )

    {

    IRet = NaN;

    }

    return ( IRet);

    }

    Если фильтр исключений должен быть более сложным, то в этом качестве можно даже вызвать одну из собственных функций (если она указывает, как нужно обрабатывать исключение, и возвращает одно из правильных значений фильтра исключений). В дополнение к вызову специальной функции GetExceptionCode, можно также вызывать функцию GetExceptionlnformation (в выражении фильтра исключений). Данная функция возвращает указатель на структуру EXCEPTION_POINTERS, которая полностью описывает причину аварии и состояние CPU в этот момент. Можно догадаться, что структура EXCEPTION_POINTERS пригодится позже в настоящей главе.

    SEH-обработка не ограничивается только обработкой аварий. Программист может также создавать собственные исключения с помощью API-функции RaiseException. Большинство разработчиков не использует эту функцию, но она предлагает возможность быстрого выхода из глубоко вложенных условных операторов. Техника выхода из условных операторов с помощью функции RaiseException более корректна, чем применение старых функций setjmp и longjmp (из исполнительной библиотеки С).

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

    Тем, кто захочет изучить SEH-обработчики подробнее, можно рекомендовать две ссылки (кроме просмотра MSDN). Лучший краткий обзор SEH-обработчиков приведен в книге Джеффри Рихтера (Jeffrey Richter, Programming Applications for Microsoft Windows, Microsoft Press, 1999). Если вы интересуетесь фактической реализацией SEH-обработчиков, найдите статью Мэтта Пиетрека — Matt Pietrek, "A Crash Course on the Depths of Win32 Structured Exception Handling" в январском номере журнала Microsoft Systems Journal за 1997 год.

    Трансляция структур EXCEPTION_POINTERS

    Рассмотрев создание собственных обработчиков исключений и аварий, поговорим о структурах EXCEPTION_POINTERS, которые пересылаются некоторым функциям. Поскольку в этих структурах хранится вся интересная информация об авариях, я разработал набор функций, которые можно вызывать, чтобы перевести эту информацию в удобочитаемую форму. С помощью этих функций выполняется визуализация информации (для пользователя). Все эти функции можно найти в листинге 9-5.
    Я пытался построить эти функции настолько просто, насколько это возможно. Все, что нужно с ними делать — это пересылать в них структуры EXCEPTION_POINTERS. Каждая функция возвращает указатель на строковую константу, которая содержит текст. Просматривая код, можно заметить, что каждая функция в этом наборе имеет "напарницу", имя которой заканчивается символами "VB1". Я пытался придумать способ, позволяющий использовать для VB-вариантов функций те же статические буферы, что и для С-функций. Были выбраны статические буферы, потому что функции обработки структур EXCEPTION_POINTERS будут использоваться в аварийной ситуации, а когда вызываются функции обработчика аварий, нельзя полагаться на распределенную память или использовать слишком много места в стеке. К сожалению, я не смог придумать ничего иного, кроме создания в Visual Basic своего собственного строчного буфера, который необходимо пересылать в соответствующие функции. В С-версиях, из-за того что можно возвращать буферы статических строк непосредственно, я просто хотел сделать эти функции более легкими для использования. При вызове функций аварийных обработчиков из Visual Basic предварительно объявите в программе глобальную строчную переменную, чтобы к моменту вызова память была доступна.
    То есть это вариант соответствующей функции для Visual Basic. — Пер.
    Функция GetRegisterString просто возвращает строку форматированного регистра. Функция GetFauitReason немного интереснее — она возвращает полное, описание проблемы. Возвращаемая строка показывает процесс, причину исключения, модуль, который вызвал исключение, адрес исключения и, если символьная информация доступна — функцию, исходный файл и номер строки, где произошел аварийный останов:

    CH_TESTS. EXE caused an EXCEPTION_ACCESS_VIOLATION in module CH_TESTS.EXE at 001B:004010FB, Baz()+0064 bytes, CHJTests.cpp, line 0060+0003 bytes

    Наиболее интересные фуункции — GetFirstStackTraceString и GetNextstackTracestring. Как указано в их именах, они позволяют продвигаться по стеку. Как и в случае с API-функциями FindFirstFiie и FindNextFile, Можно сначала вызвать GetFirstStackTraceString И затем продолжить проход всего стека, вызывая GetNextstackTracestring до тех пор, пока она не возвратит FALSE. В дополнение к структуре EXCEPTION_POINTERS, эти функции получают параметр флагов режимов, который позволяет управлять количеством информации, отображаемой в результирующей строке. Следующая строка показывает вывод, когда все опции (флажки) этого параметра включены:

    001В:004018АА (0x00000001 Ox008COF90 Ox008C0200 Ox77F8FE94) CH_TESTS.EXE, main()+1857 bytes, CHJTests.срр, line 0341+0007 bytes

    Значения в круглых скобках — это четыре первых возможных параметра функции. В табл. 9.1 приведены символические идентификаторы флагов режимов и содержание строки вывода для каждого из них.

    Таблица 9.1. Режимы функций GetFirststackTraceString И GetNextStackTraceString

    Режим

    Вывод

    0

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

    GSTSO_PARAMS

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

    GSTSO MODULE

    Имя модуля

    GSTSO SYMBOL

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

    GSTSO_SRCLINE

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

    Трансляция структур EXCEPTION_POINTERS


    Рис. 9.1. Диалоговое окно программы CrashTest

    Чтобы продемонстрировать эти функции в действии, я включил в сопровождающий CD два примера тестовых программ. Первая, CH_TEST, написана на C/C++, а вторая, CrashTest, — на Visual Basic. По этим двум программам можно получить довольно хорошее представление о том, как использовать все рассмотренные здесь функции. На рис. 9.1 показано диалоговое окно программы CrashTest с информацией аварийного сбоя.

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

    Использование API-функций

    Службы обладают рядом уникальных свойств, и от вас потребуются некоторые усилия, чтобы к ним приспособиться. Во-первых, точка входа, которая используется в службах — main или winMain — не имеет особого значения. Поскольку служба не программирует пользовательского интерфейса, то вы можете использовать как одну, так и другую.
    UPS — Uninterruptible Power Supply. — Пер.
    С этих функций начинается выполнение Windows-приложений. Функция main является консольной точкой входа, a WinMain — точкой входа графического интерфейса пользователя (GUI). - Пер.
    Внутри функций main и winMain прежде всего нужно обратиться к API-функции startServiceCtrlDispatcher. При этом ей следует передать структуру SERVICE_TABLE_ENTRY, в которой нужно указать имя и главную точку входа службы. Диспетчер управления службами (Service Control Manager — SCM), который запускает все службы и с которым, в конечном счете, общается StartServiceCtrlDispatcher (для того чтобы установить вашу службу), является средством операционной системы, которое, судя по его названию, управляет всеми службами. Если служба не вызывает функцию StartServiceCtrlDispatcher в течение 2 минут (для Windows NT 4) или 30 секунд (для Windows 2000) после своего запуска, то SCM завершает эту службу. Как будет показано чуть позже, этот лимит времени может определенным образом влиять на запуск отладчика.
    Как только выполняется обращение к SCM-менеджеру, он порождает поток для вызова точки входа вашей службы. С этой точкой входа связано одно жесткое Требование: она должна вызвать функцию RegisterServiceCtrlHandler в течение 1 секунды после старта службы. Если вызов не последует в течение 1 секунды, SCM подумает, что служба потерпела неудачу, но не завершитее. ЕСЛИ служба, В Конечном счете, Вызывает RegisterServiceCtrlHandler, то она будет выполняться нормально. На первый взгляд, было бы логично, если бы диспетчер SCM завершил службу, заподозрив, что она закончилась неудачей, на самом деле он этого не делает. Преимуществом такого поведения является то, что значительно облегчается отладка, поскольку служба продолжает выполняться.

    Функция RegisterServiceCtrlHandler получает еще и другой указатель — на функцию, называемую функцией обработчика (handler function). SCM обращается к этой функции для того, чтобы управлять производительностью службы на таких операциях, как остановка, приостановка (пауза) или продолжение выполнения.

    Когда служба переходит в состояния старта, остановки и приостановки, она связывается с SCM через API-функцию setservicestatus. Большинство служб должны просто вызвать setservicestatus и указать основное состояние, в которое они переходят, так что в этой API-функции нет ничего необычного.

    Некоторые детали, связанные с API-функциями, опущены, но в основном обращения к StartServiceCtrlDispatcher, RegisterServiceCtrlHandler И setservicestatus — это все, что операционная система требует от вашей службы для ее запуска и выполнения. Заметьте, что ничего не сказано о требованиях к протоколам обмена, с помощью которых служба связывается пользовательским интерфейсом контроллера, который вы пишете. К счастью, службы имеют доступ ко всем регулярным API-функциям Windows, поэтому возможно использование файлов, отображаемых в память, почтовых ячеек и именованных каналов. Службы предоставляют все возможности для организации нормальных связей между процессами. Наиболее трудной проблемой, связанной со службами, является безопасность.

    Обеспечение безопасности

    Если не указано иное, службы запускаются под специальной учетной записью System. Поскольку в Windows 2000 защита всех объектов выполняется на пользовательском уровне, системный учет ведется для отдельной машины, а не для сети в целом. Следовательно, процесс, выполняемый под учетной записью System, не имеет доступа к сетевым ресурсам. Во время разработки многих служб, таких как упомянутая выше служба управления источником бесперебойного питания, проблемы безопасности могут никогда не возникать. Но если, например, попытаться разделить ресурс отображаемой памяти между вашей службой и клиентским UI-приложением, а защита не установлена правильно, то в клиентских приложениях вы столкнетесь с ошибками типа "access denied" (доступ запрещен).
    К сожалению, никакая отладка не решает до конца проблем безопасности. Подход к проблемам безопасности при программировании как служб, так и клиентских приложений должен быть одинаково серьезным. Полное описание программирования всех аспектов безопасности (security programming) в Windows 2000 заняло бы целую книгу, поэтому будьте готовы потратить некоторое время на планирование программирования безопасности с самого начала разработки приложения. Для первого ознакомления с вопросами безопасности программных служб я настоятельно рекомендую прочитать статью Фрэнка Кима "Why Do Certain Win32 Technologies Misbehave in Windows NT Services?" (Почему некоторые Win32 технологии плохо ведут себя в службах Windows NT?) в мартовском выпуске журнала Microsoft Systems Journal за 1998 год (http://www.microsoft.com/MSJ/0398/service2.htm). Другой превосходный источник — колонка Кейта Брауна (Keith Browns) "Security Briefs" (Коротко о безопасности) в журнале Microsoft Systems Journal.
    Те из вас, кому приходилось заниматься этими проблемами, могут подумать, что вместо возни с разработкой служб гораздо проще запустить программу SRVANY.EXE из Windows 2000 Resource Kit, которая позволяет преобразовать любое приложение в службу, и покончить с этим делом. Это иногда действительно помогает, но в упомянутой статье Кима рассмотрены несколько причин, по которым SRVANY.EXE не годится для этого.
    Теперь вернемся к основному вопросу этой главы: как отлаживать службы?



    Основы программных служб

    Вот три основных характеристики программной службы:
  • служба выполняется все время, даже когда на компьютере никто не зарегистрирован или когда он запускается впервые;
  • служба не имеет пользовательского интерфейса;
  • служба может управляться и контролироваться как локальными, так и удаленными клиентами.
  • Принимая решение о том, нужно ли писать приложение как службу или как нормальное приложение пользовательского режима, спросите себя, отвечает ли задача разработки, которую вы пытаетесь решить, этим трем требованиям? Если — да, то следует рассматривать приложение как службу. В этом случае необходимо хорошо понимать механизм работы службы. Если вы захотите изучить службы подробнее, просмотрите статью Джеффри Рихтера (Jeffrey Richter) "Design a Windows NT Service to Exploit Special Operating System Facilities" (Проектирование служб Windows NT для использования специальных возможностей операционных систем) в октябрьском (за 1997 год) журнале Microsoft Systems Journal (на MSDN).
    Подходящим опытом написания именно службы (а не обычного приложения) является разработка программного обеспечения (ПО), которое должно контролировать источник бесперебойного электропитания (UPS1) для компьютера. ПО UPS должно контролировать сообщения аппаратуры UPS об отказе электропитания. Кроме того, когда питание пропадает, это ПО должно инициировать управляемое завершение работы компьютера. Очевидно, что если ПО UPS не выполняется все время (первый критерий для решения, должно ли приложение быть службой), то завершения не будет, и компьютер остановится только тогда, когда в аппаратуре UPS иссякнет батарейное питание. ПО UPS действительно не нуждается в интерфейсе пользователя (второй критерий), потому что от него требуется только выполнение в фоновом режиме и управление аппаратурой UPS. Наконец, если речь идет об аппаратуре UPS, используемых в распределенных центрах данных, то системные администраторы определенно захотят проверить состояние аппаратуры удаленных UPS (третий критерий).
    Пока все это звучит достаточно просто. Теперь поговорим о том, как службы работают. Сначала рассмотрим специфические API-функции, которые нужно вызывать, чтобы превратить нормальный процесс пользовательского режима в службу.


    Отладка базовых служб

    Проверив общую логику, можно начинать отладку кода для его выполнения как службы. Начальная отладка должна выполняться на той системе, все управление которой находится в руках разработчика. В идеале рядом с ней следует установить вторую машину, причем версия и параметры операционной системы (Windows) на этой машине должны моделировать клиентскую машину, на которой будет выполняться данная служба. Если целью отладки основного кода была проверка базовой логики службы, то целью предварительной отладки службы является реорганизация базового кода в специфический код службы. Для выполнения отладки первого варианта кода службы следует:
  • включить свойство Allow Service To Interact With Desktop (Разрешить службе взаимодействовать с рабочим столом);
  • установить идентификатор (имя) службы;
  • присоединить средства отладки к службе;
  • отладить код запуска.
  • При обсуждении каждой задачи мы будем, по мере необходимости, рассматривать конкретные проблемы, важные для различных технологий.
    Включение режима Allow Service To Interact With Desktop
    Независимо от того, какой тип службы вы отлаживаете, необходимо установить флажок Allow Service To Interact With Desktop на вкладке Log On диалогового окна Properties отлаживаемой службы. Хотя у службы не должно быть никаких элементов интерфейса пользователя, наличие панелей с сообщениями от утверждений, которые позволяют контролировать службу через отладчик, весьма желательно. Эти панели, объединенные с чрезвычайно полезным регистрационным кодом (например, кодом, который ATL-библиотека выдает для записи в журнал регистрации событий), могут значительно облегчить отладку служб.
    На начальных этапах разработки я включаю в службу код, открывающий диалоговое окно ASSERTION FAILURE (SUPERASSERT), что позволяет быстро оценивать общее состояние службы (более подробная информация о программе SUPERASSERT приведена в главе 3). Однако после нескольких прогонов службы я изменяю параметры сообщений, направляя их через операторы трассировки.

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

    Установка идентификатора службы

    Чтобы избежать проблем безопасности при попытке запуска службы, можно установить ее идентификатор. По умолчанию все службы выполняются под учетной записью LocalSystem. Однако имеется возможность установить службу для запуска под учетной записью пользователя с правами Администратора.

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

    Присоединение отладчика к службе

    Если служба стартует успешно, отладка обычно не составляет особого труда. Все, что нужно сделать — это присоединить к ней процесс отладчика Microsoft Visual C++. Для того чтобы прикрепить к службе активный процесс отладчика Visual C++:

    1. Запустите утилиту MSDEV.EXE.

    2. Выберите пункт Start Debug меню Build и в раскрывшемся подменю выберите команду Attach To Process....

    3. Установите флажок Show System Processes (Отображать все служебные процессы).


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

    Альтернативный метод присоединения отладчика: нужно вызвать API-функцию DebugBreak и, когда раскроется диалоговое окно Application Error, нажать кнопку Cancel, а затем выполнять отладку как обычно. Имейте в виду, что если вы создаете СОМ+-службу, то нужно вызывать DebugBreak вне любого СОМ-метода или обращений к свойствам. Если вы этого не сделаете, то СОМ-служба будет поглощать исключения точек прерывания, генерируемые функцией DebugBreak, и вы никогда не добьетесь присоединения отладчика. Кроме того, не следует вызывать DebugBreak в коде начального запуска службы (причины изложены ниже в разделе "Отладка кода запуска" данной главы).

    Все другие средства присоединения отладчика (к службе) должны использовать Диспетчер задач (Task Manager) операционной системы. Загрузите Диспетчер задач, выберите вкладку Processes, щелкните правой кнопкой мыши на процессе, который хотите отладить, и выберите пункт Debug в раскрывшемся контекстном меню. В этом случае операционная система облегчает присоединение отладчика.

    Если пункт Debug контекстного меню Диспетчера задач не активен, не волнуйтесь, — вы просто видите работу службы безопасности Windows 2000. Только пользователям, с правами администратора на локальной машине позволено присоединять отладчик к службам. Если программисты вашей компании обычно входят в систему под учетной записью домена, следует добавить эту учетную запись в группу Administrators на каждой машине.

    US ISAPI фильтры и расширения

    В версии 5 Internet Information Services (US 5) изменены программы, в которых выполняются ISAPI-фильтры и расширения. В предыдущих версиях IIS все фильтры и расширения запускались внутри INETINFO.EXE (главной службы IIS). В IIS 5 расширения выполняются в DLLHOST.EXE вследствие использования новой объединенной внепроцессной модели1расширений. ISAPI-фильтры все еще выполняются внутри IIS-процесса INETINFO.EXE. Новая модель делает IIS-службы намного более устойчивыми и, согласно Microsoft, намного более расширяемыми.


    Единственная проблема отладки состоит в том, что вы можете не знать, под каким процессом из DLLHOST.EXE выполняется ваше расширение.

    От англ, pooled out-of-process model. — Пер. •,

    Новая объединенная модель расширений применяется только к Web-сайтам, созданным после обновления (upgrade) IIS до версии 5. В результате обновления существующие совместно используемые Web-ресурсы будут выполнять расширения точно так же, как и версия IIS 4. Если ваше расширение обрабатывает свои собственные пулы потоков или использует любую форму функции RevertioSeif, то нужно установить это расширение для выполнения в адресном пространстве IIS. Сведения об установке расширений можно найти в MSDN в разделе "Pooled Out-of-Process Model for ISAPI" (Объединенная внепроцессная модель для ISAPI).

    В документации IIS также говорится, что расширения, выполняющиеся внутри IIS, необходимо устанавливать так, чтобы иметь возможность их отладки. Единственный нюанс, связанный с изменением места выполнения расширения, заключается в том, что при этом они должны по-прежнему поддерживать объединенную внепроцеесную модель. Будучи убежденным сторонником отладки, продемонстрирую некий трюк, позволяющий отлаживать расширения, выполняемые под управлением DLLHOST.EXE.

    Прежде чем говорить об использовании отладчика, выясним, как следует вычислять тот процесс, который выполняет ваш фильтр или расширение (потому что одновременно будет выполняться множество экземпляров диспетчера внепроцессного пула расширений DLLHOST.EXE). Сначала нужно загрузить свободно распространяемую утилиту HandleEx для Windows NT, с Web-сайта www.sysinternals.com Марка Руссиновича (Mark Russinovich) и Брюса Когсвелла (Bruce Cogswell). Эта утилита показывает дескрипторы уже открытых процессов и, что более важно, какие DLL в какой процесс загружены. Чтобы найти DLL с помощью HandleEx, нажмите клавишу , и введите имя DLL-файла в поле DLL substring диалогового окна HandleEx Search. Затем нажмите кнопку Search, и HandleEx выведет список имен и идентификаторы процессов (PID), которые загружают вашу DLL.


    На рис. 10. 1 показано окно поиска утилиты HandleEx, сообщающее, в каком процессе выполняется расширение SIMPLE.DLL.

    Отладка базовых служб
    Рис. 10.1. Поиск расширения, выполняющегося в диспетчере внепроцессного пула (dllhost.exe), входящего в состав служб IIS 5

    Получив значение PID, можно с помощью команды Attach To Process присоединить к процессу отладчик Visual C++. Имейте в виду, что, прикрепив к процессу отладчик, необходимо, чтобы он выполнялся даже тогда, когда отладка конкретной DLL будет закончена. Если завершить отладчик, то остановится и отлаживаемый процесс.

    Поскольку вы ищете загружаемую DLL, то, очевидно, должны удостовериться, что она загружается, прежде чем ее можно будет отлаживать. Фильтры выполняются внутри INETINFO.EXE, поэтому нельзя присоединять отладчик до запуска службы IIS (попытка отладить инициализацию службы до запуска IIS завершится неудачей). А в случае расширений можно отлаживать и инициализацию, если вы достаточно изобретательны. Идея состоит в том, чтобы создать фиктивное расширение, которое приведет к загрузке IIS при соединении с Web-сайтом при помощи MS Internet Explorer, который заставит IIS-службы запустить DLLHOST.EXE. После того как вы найдете PID нового DLLHOST.EXE, можно присоединить отладчик и установить точку прерывания на функцию LdrpRuninitiaiizeRoutines, чтобы получить прямой доступ к точке входа расширения (ollMain). Мэт Пьетрек (Matt Pietrek) точно разъясняет, что нужно делать, чтобы установить эту точку прерывания в своей колонке "Under the Hood" в сентябрьском (за 1999 год) номере журнала Microsoft Systems Journal. После установки точки прерывания можно загружать реальное расширение с помощью Internet Explorer и отлаживать инициализацию службы.

    Отладка кода запуска

    Самая тяжелая часть отладки служб — отладка кода запуска. Диспетчер SCM ждет только 2 минуты под Windows NT 4 или 30 секунд под Windows 2000

    Перед запуском службы И вызывает функцию StartServiceCtrlDispatcher, чтобы указать, что служба выполняется нормально.


    Это время можно потратить на пошаговый прогон своего кода с просмотром переменных.

    Единственный "чистый" способ отладки кода запуска службы состоит в том, чтобы использовать операторы трассировки и отладчик Visual C++. Кроме того, операторы службы можно просматривать с помощью утилиты DebugView/Enterprise Edition Марка Руссиновича, рассмотренной в главе 3. К счастью, код запуска службы обычно проще, чем ее основной код, поэтому отладка с помощью операторов трассировки не слишком болезненна.

    Значения тайм-аутов SCM могут создать трудности для служб, которые не способны быстро стартовать. Медленная часть аппаратуры или природа службы иногда диктуют длительное время запуска. Решить эти проблемы можно при помощи полей dwCheckPoint и dwwaitHint структуры SERVICE_STATUS, которая передается в функцию SetServiceStatus.

    Когда служба стартует, можно сообщить диспетчеру SCM, что вы входите в состояние SERVICE_START_PENDING (ожидание_запуска_службы), ввести рекомендуемое время ожидания (в миллисекундах) в поле dwwaitHint и установить поле dwCheckPoint в 0, чтобы SCM не использовал тайм-аут по умолчанию. Если на запуск службы требуется больше времени, то можно повторять вызов setservicestatus столько раз, сколько необходимо, увеличивая значение поля dwCheckPoint перед каждым следующим вызовом.

    Кроме того, SCM добавляет записи в журнал регистрации событий (event log), объясняя, почему он не может стартовать конкретную службу. Чтобы просмотреть эту запись, запустите утилиту Event Viewer и поищите в колонке Source запись "Service Control Manager". Если журнал регистрации событий используется также и для упрощенной трассировки, то у вас должна появиться возможность разрешать многие проблемы запуска служб. Убедитесь, что параметры служб установлены так, чтобы ваша служба запускалась после службы Event Log.

    Кривая обучения (learning curve) определяет скорость изучения программного продукта. Чем круче эта кривая, тем меньше период его изучения (обучения).


    — Пер.

    Реальная отладка

    Ограничения GUI- отладчиков делают отладку служб интересным, но достаточно сложным испытанием для разработчиков. Все же отлаживать службы с помощью отладчиков, поставляемых вместе с Microsoft Visual Studio или Platform SDK, можно.

    Если вы разрабатываете службы, которые предъявляют усложненные требо- , вания к обработке (выполняя, например, довольно трудные для Win32 процессы синхронизации или разделения памяти) или загружают DLL в службы типа IIS, то можете воспользоваться отладчиком SoftICE компании Compuware NuMega. SoftICE выполняется между CPU и операционной системой и может значительно облегчить отладочные процедуры кода пользовательского режима, например, запуск служб или межпроцессные связи, что не всегда позволяют делать GUI-отладчики. Работая с SoftICE, вы просто загружаете исходный код своего модуля, устанавливаете точку прерывания и, независимо от того, как или где ваш модуль загружается в память, можете сосредоточиться на отладке, не беспокоясь о проблемах, связанных с GUI-отладчиками. Представленный на сопровождающем компакт-диске SoftICE имеет намного более крутую кривую обучения1, чем отладчик Visual C++, но если вы разрабатываете службы и DLL, которые в них загружаются, то в итоге сэкономите много времени на его изучении.

    Отладка основного кода

    Прежде чем рассматривать приложение как службу, его нужно выполнить и протестировать, как стандартный исполняемый код пользовательского режима (до тех пор, пока не будет отлажен весь основной код). Как только это будет сделано, можно начинать работать над проблемами, специфическими для служб.
    Основной код следует отлаживать на одной машине, работая под учетной записью разработчика (т. е. как основной код службы, так и любой клиентский код должны находиться на одной и той же машине). Отладив логику программы, можно переходить к специфическим особенностям служб, например, к проблемам их безопасности и инициализации.
    Службы СОМ+
    Если вы компилируете СОМ+-службу с библиотекой активных шаблонов (Active Template Library — ATL) — такую, например, как утилита TraceSrv из главы11, то не нужно предпринимать никаких мер защиты. По умолчанию ATL выполняется как обычная программа пользовательского режима до тех пор, пока вы не зарегистрируете свое приложение, запустив его из командной строки с ключом Service.
    Фильтры и расширения ISAPI
    Экспортируемые функции, которые необходимо предусмотреть для фильтров и расширений ISAPI (Internet Server API) довольно просты, и вы легко сможете написать тестовую оболочку, которая действует как фиктивная система информационных служб Internet (Internet Information Services — US). В такой управляемой среде можно протестировать все основные алгоритмы службы, выполнив, таким образом, их полную отладку до того, как служба будет запущена под соответствующим сервером информационных служб Internet (IIS).
    IIS-сервер — сервер информационных служб Интернета (его называют также Web-сервером), он является частью соответствующих служб. — Пер.
    Exchange Server
    Можно также создавать приложения служб обмена (Exchange service applications), которые выполняются как консольные приложения, если использовать вспомогательные функции из WINWRAP.LIB. Запуск службы с параметром notserv (он должен быть первым в списке параметров) приведет к тому, что она будет выполняться как нормальный процесс.


    Отладка служб

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


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

    Первые проблемы с TraceSrv

    Первая проблема обнаружилась после того, как я установил TraceSrv, запустил ее и присоединил к ней несколько клиентских процессов. В проектных требованиях указывалось, что все клиенты должны использовать один и тот же экземпляр TraceSrv. Во время тестирования обнаружилось, однако, что каждый процесс, который соединялся с TraceSrv, получал собственную копию интерфейса iirace, так что у программы просмотра трассы не было никакой возможности увидеть вывод из присоединенных процессов.
    Эта проблема поставила меня в тупик, потому что я не думал, что будет так трудно создать интерфейс с единственным экземпляром программы. Промучившись целый день, я был готов переопределить метод iciassFactory:: Createinstance и заставить его всегда возвращать один и тот же интерфейс itrace. Это изменило бы ожидаемое поведение Createinstance, но, по крайней мере, позволило бы иметь только один экземпляр интерфейса. К счастью, разбираясь в коде ATL, я наткнулся на класс ccomciassFactorySingieton, который, как говорит документация, предназначен для создания единственного экземпляра метода — как раз то, что мне было нужно. Этот класс обрабатывается макросом DECLARE_CLASSFACTORY_SINGLETON (CTrace), определенным в TRACE.H. Итак, данная ошибка была вызвана моим незнанием ATL.
    Однажды, в самом начале использования TraceSrv, я заметил, что класс ccomBSTR выполнял все распределения и освобождения памяти почти при каждом вызове метода. Разработав класс CFastBSTR, я полагал, что тестирование будет делом нетрудным. Однако при проверке различных сценариев я получил утверждение (в конце CTrace: :ProcessTrace), которое можно найти в заключительной части листинга 11-2. В TraceSrv применяется макрос ASSERT, рассмотренный в главе 3, а из-за того что TraceSrv спроектирована как служба, я вызывал функцию setoiagAssertoptions и выключал отображение панели сообщений.
    Я получал это сообщение, когда выполнял TraceSrv без присоединенной программы просмотра трассы. Просматривая код функции Fire_TraceEvent, который был сгенерирован командой Implement Connection Point в IDE, я заметил кое-что очень интересное.
    Оригинальный код функции Fire_TraceEvent приведен в листинге 11-3. Будьте внимательны при просмотре кода и попробуйте найти ошибку.

    Листинг 11-3. Функция Fire_ TraceEvent с ошибкой

    HRESULT Fire_TraceEvent( BSTR bstrText )

    {

    CComVariant varResult;

    Т* рТ = static_cast( this );

    int nConnectionlndex;

    CComVariant* pvars = new CComVariant[1];

    int nConnections = m_vec.GetSize( );

    for ( nConnectionlndex = 0;

    nConnectionlndex < nConnections; nConnection!ndex++ )

    {

    pT->Lock();

    CComPtr sp = m_vec.GetAt( nConnectionlndex );

    pT->Unlock( );

    IDispatch* pDispatch = reinterpret_cast( sp.p );

    if (pDispatch != NULL)

    {

    VariantClear( SvarResult );

    pvars[0] = bstrText;

    DISPPARAMS disp = { pvars, NULL, 1, 0 };

    pDispatch->Invoke( 0xl,

    IID_NULL,

    LOCALE_USER_DEFAULT,

    DISPATCH_METHOD,

    &disp, SvarResult,

    NULL, NULL );

    }

    }

    delete[] pvars;

    return varResult.scode;

    }

    Имейте в виду, что утверждение срабатывало только тогда, когда программа просмотра к TraceSrv не присоединялась. Внимательно посмотрев на Fire_rraceEvent, вы увидите, что цикл for никогда не выполняется, если программа просмотра не присоединена. Однако сгенерированный код возвращает varResuit.scode, который инициализируется только внутри цикла for. Следовательно, когда программа просмотра не присоединена, функция возвращает неинициализированное значение. В отладочных построениях функция Fire_rraceEvent возвращала значение Охсссссссс (символ-заполнитель, который при компиляции с ключом /GZ помещается в локальные переменные).

    Решение проблемы неинициализированной переменной было довольно простым. Я переименовал файл, который генерировала команда Implement Connection Point, (TRACESRVCP.H) в CORRECTEDTRACESRVCP.H и после объявления переменной varResult установил varResuit.scode равным s ок. Хотя использование неинициализированных переменных в практике программирования не рекомендуется, но теперь, по крайней мере, разработчики Visual C++ возвращают результаты вызовов IDispatch:: invoke.В предыдущих версиях Visual C++ это было невозможно. Как только я решил эту маленькую проблему, TraceSrv стал выполняться довольно хорошо.

    Прежде чем завершить эту главу, рассмотрим программу TraceView, обеспечивающую безопасность в Win32, и вызовы TraceSrv из пользовательского кода.

    Применение TraceSrv

    Работать с программой TraceSrv довольно легко. Код на сопровождающем компакт-диске вызывает TraceSrv из программ, написанных на трех различных языках. В листинге 11-4 приведен пример вызова TraceSrv из программы на VBScript, который показывает, насколько легко использовать TraceSrv. Пример на C++ более интересен в том отношении, что прежде чем обращаться к TraceSrv, нужно конвертировать строки трассы в строки типа BSTR. Программа DCOMTEST.CPP показана в листинге 11-5.
    RFC — Remote Procedure Call, удаленный вызов процедуры (средство передачи сообщений, которое позволяет распределенному приложению вызывать сервис различных компьютеров в сети; обеспечивает процедурно-ориентированный подход в работе с сетью; применяется в распределенных объектных технологиях, таких как DCOM, CORBA, Java RMI. — Пер
    Листинг 11-4. Вызов TraceSrv из VBScript

    Листинг 11-5. Вызов TraceSrv из программы на C++ (DCOMtESf .СРР)
    void main ( void )
    {
    HRESULT hr ;
    ITrace * IpTrace ;
    lUnknown * IpUnknown ;
    // Инициализировать библиотеки СОМ+.
    hr = CoInitializeEx ( NULL , COINIT_APARTMENTTHREADED ) ;
    if ( FAILED ( hr ) )
    {
    printf ( "Unable to initialize COM+\n" ) ;
    return ;
    }
    hr = CoCreatelnstance ( CLSIDJTrace ,
    NULL , CLSCTX_SERVER , IID_IUnknown , (LPVOID*)SlpUnknown } ;
    if ( FAILED ( hr ) )
    {
    LPVOID IpMsgBuf;
    FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER |
    FORMAT_MESSAGE__FROM_SYSTEM |
    FORMAT_MESSAGE_IGNORE__INSERTS,
    NULL,
    hr,
    MAKELANGID ( LANG_NEUTRAL, SUBLANG__DEFAULT ),

    (LPTSTR) SlpMsgBuf,

    0,

    NULL ) ;

    printf ( " CoCreatelnstanceEx failed: Ox%08X\n" , hr ) ;

    printf ( "FormatMessage returned: %s\n" , IpMsgBuf ) ;

    return ;

    }

    hr = lpUnknown->Query!nterface ( IID_ITrace ,

    (LPVOID*)&lpTrace ) ;

    lpUnknown->Release ( ) ;

    if ( FAILED ( hr } )

    {

    LPVOID IpMsgBuf;

    FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER |

    FORMAT_MESSAGE_FROM_SYSTEM |

    FORMAT_MESSAGE_IGNORE_INSERTS, NULL,

    hr,

    MAKELANGID ( LANG_NEUTRAL, SUBLANG_DEFAULT ),

    (LPTSTR) SlpMsgBuf, 0,

    NULL );

    printf ( "Querylnterface failed: Ox%08X\n" , hr ) ;

    printf { "FormatMessage returned: %s\n" , IpMsgBuf ) ;

    return ;

    }

    OLECHAR * pszTemp ;

    pszTemp = SysAllocString ( OLESTR ( "Hello from a C++ program!!!" ) ); ,

    lpTrace->Trace ( pszTemp ) ;

    SysFreeString ( pszTemp ) ;

    lpTrace->Release ( ) ;

    CoUninitialize ( ) ;

    }

    Программа TraceView и безопасность

    Программа TraceSrv полезна сама по себе, но утилита просмотра, которая отображает на экране операторы трассировки, на самом деле улучшает ее. Я написал TraceView на языке Visual Basic, потому что это было довольно просто сделать. Если посмотреть на ее исходный код, то можно убедиться, что ничего особенного в нем нет.
    Я попытался сделать TraceView немного более полезной, чем простой редактируемый элемент управления, добавив к ней панель инструментов, панель состояния, поддержку сохранения и восстановления позиции окна, сохранение файлов, поиск вперед и назад, и реализовал для окна возможность всегда оставаться наверху. Чтобы облегчить локализацию программы, все строки сохраняются в файле ресурса. Не буду углубляться в загрузку строк ресурса, но отмечу, что пришлось модифицировать сгенерированную функцию LoadResStrings (переименованную В LoadFormResStrings) так, чтобы она уведомляла пользователя о том, какие элементы ресурса не были загружены. Сначала TraceView работала прекрасно. Однако в ходе проверки различных способов присоединения TraceView к TraceSrv были выявлены некоторые проблемы. Если TraceView и TraceSrv располагались на одной машине, то Trace View могла соединяться с TraceSrv только тогда, когда она выполнялась как служба или как локальный сервер. TraceView могла также соединяться должным образом с TraceSrv, если TraceSrv выполнялась на другой машине как локальный сервер, использующий СОМ+-технологию. Однако когда я пробовал соединять TraceView с TraceSrv, выполняющейся на другой машине как СОМ+-служба, это всегда приводило к отказу, сопровождаемому VB-сообщением об ошибке "Run-time error -2147023071 (80070721) Automation Error" (Ошибка времени выполнения -2147023071 (80070721) Ошибка автоматизации). В файле WINERROR.H идентификатор (ID) этой ошибки выглядит так: RPC_S_SEC_PKG_ERROR, "A ' security package specific error occurred" (Произошла специфическая ошибка пакета защиты).
    Этот идентификатор был мне незнаком, а попытавшись найти его в MSDN, я узнал только, что он определен в WINERROR.H и внесен в список приложений с системными ошибками.
    Провозившись с этой проблемой несколько дней, я обнаружил, что мог бы добиться соединения VB-программы с удаленной службой TraceSrv лишь в том случае, если бы в объявлении Trace-объекта не использовал ключевое слово withEvents. Указывая ключевое слово withEvents, я всегда получал ошибку RPC_S_SEC_PKG_ERROR и пребывал в недоумении, пока один из друзей не указал мне, что неправильно установлена защита (security) службы TraceSrv.

    Вернувшись "к своим баранам" и еще раз посмотрев, что же происходит, я начал кое-что понимать. Ключевое слово withEvents устанавливает интерфейс iconnectionPoint, который сервер использует для вызова клиента — это, по существу, обратный вызов. Чтобы выполнить обратный вызов клиента, сервер должен иметь корректные полномочия доступа. Когда TraceSrv выполняется на той же машине, что и TraceView, то, не зависимо от того, запущена ли она как локальный сервер или как служба, TraceSrv выполняется под тем же пользовательским идентификатором, что и TraceView. Выполнение TraceSrv на одной машине в качестве удаленного СОМ+-сервера, a TraceView — на другой, было успешным потому, что мне повезло. На обеих машинах, работавших под Windows NT Workstation без контроллера домена, я был зарегистрирован в как "John" с одним тем же паролем. Согласно статье Q158508 Knowledge Base ("Часто задаваемые вопросы по СОМ-безопасности") операционная система Windows NT Workstation "возвращается к режиму согласования имен и паролей учетных записей". Если на двух машинах, работающих под Windows NT Workstation, используются одни и те же ASCII-имена, и учетные записи имеют одни те же пароли, то средства защиты DCOM и других ресурсов NT (например, файловой системы) должны работать так, как если бы вы были действительно зарегистрированы ш этих двух машинах с одной и той же учетной записью".

    Когда я, зарегистрировавшись на удаленной машине как "Bob", запускал на ней TraceSrv как удаленный сервер, и пытался соединять с ним TraceView на машине клиента, зарегистрировавшись на ней, как "John", я получал ошибку RPC_S_SEC_PKG_ERROR.


    В моем случае выполнение TraceSrv как удаленного сервера на отдельной машине не принимало во внимание каких-либо измеIнений в соединениях.

    Запуск удаленного сервера с надлежащей защитой — довольно простая задача: нужно только при входе в систему зарегистрироваться в качестве пользтователя, имеющего сетевые права. Однако для служб Win32 решение этой задачи требует немного больших усилий. По умолчанию такие службы не имеют никаких полномочий безопасности, поэтому TraceSrv и вызывала соответствующую ошибку всякий раз, когда она пыталась что-то делать с интерфейсом iconnectionPoint, который она получала через параметр вызова. Необходимо было сделать так, чтобы клиент сообщил СОМ+-службам уровень безопасности, который они должны разрешать своим собственным интерфейсам. Уровень безопасности для клиентских интерфейсов определяется с помощью функции CoInitializeSecurity, которая должна вызываться немедленно после того, как ваше приложение вызывает Coinitiaiize. В программе TraceView, которая написана на языке Visual Basic, функция CoInitializeSecurity не будет работать. Если вывзов CoInitializeSecurity будет первым в функции sub Main, то вы получите код ошибки 0x80010119 (RPC_E_ TOO_LATE), что означает: "Security must be initialized before any interfaces are marshaled or unmarshaled. It cannot be changed once initialized." (Защита должна быть инициализирована перед маршализацией1или демаршализацией любого интерфейса. После инициализации она не может быть изменена.) Нетрудно видеть, что Visual Basic выполняет маршалинг намного раньше того, как пользовательский код получает вызов.

    Маршалинг — передача данных через границы процесса. Здесь речь идет о передаче параметров и возврате результатов при передаче вызова в другое адресное пространство. — Пер.

    Обойти это ограничение Visual Basic можно двумя способами. Первый — 1 запустить DCOMCNFG и установить для свойства Default Authentication Level на I вкладке Default Properties значение None. Такое решение подходит для небольшой замкнутой домашней сети, но для крупных реальных сетей разработки это не самое лучшее решение.


    Второй подход более приемлем и безопасен: на машине, которая будет выполнять программу TraceSrv, зарегистрируйте ее как службу, запустите Панель управления, и щелкните на значке Services. Выберите элемент TraceSrv (в списке служб) и нажмите кнопку Start Service, а также кнопку Properties, чтобы отобразить диалоговое окно Properties программы TraceSrv. На вкладке Log On в переключателе Log On As выберите переключатель This Account и введите с клавиатуры имя пользователя и пароль для учетной записи, под которой предполагается выполнять TraceSrv. Теперь служба будет способна получить необходимую ей защиту от известной в сети учетной записи. Как указывается в статье из "COM Security Frequently Asked Questions" (Часто задаваемые вопросы по СОМ-безопасности): "Учетная запись Localsystem является локально очень привилегированной... Однако ее владелец не имеет сетевых привилегий и не может покинуть машину через любые защищенные механизмы NT, включая файловую систему, именованные каналы, DCOM или безопасные RPC1-вызовы". Как только я получил службу, запускающуюся под надлежащей учетной записью, TraceView заработала прекрасно. Для работы с сервером домена можно предусмотреть создание специальной учетной записи, которую можно использовать для запуска инструментов, подобных TraceSrv. Например, можно организовать специальную учетную запись Build, которую ваши сборочные (build) машины будут использовать для отправки почты.

    TraceSrv и DCOMCNFG

    Для того чтобы разрешить доступ к TraceSrv через сеть, не следует запускать ее как службу, (т. к. после запуска TraceSrv работает как удаленный СОМ+-сервер). Для отладки такая гибкость очень удобна, потому что, запустив TraceSrv в отладчике, можно наблюдать за присоединением к ней клиентов, а при необходимости выполнять и отладку. Мне удобнее всего было, когда и клиентское приложение, и TraceSrv выполнялись под отладчиками на своих машинах. Когда какое-то из приложений достигает точки прерывания, следует приостановить и другое (чтобы избежать любых возможных проблем с тайм-аутами). Я всегда компилировал Visual Basic-клиент до уровня "родного" кода и запускал его под отладчиком Visual C++. Эта тактика гарантировала, что по достижении клиентом точки прерывания он "намертво" останавливается в отладчике. Причина, по которой нельзя остановить клиент, выполняющийся под VB-отладчиком, заключается в том, что с TraceSvr на самом деле соединяется не отлаживаемое приложение, а сам VB-отладчик.
    Для того чтобы использовать TraceSrv через сеть, нужно выполнить программу DCOMCNFG.EXE, корректно прописывающую данные о ней в реестр. Первое, что нужно сделать — выполнить для своей машины установку СОМ+-СВОЙСТБ по умолчанию. Перед изменением СОМ+-свойств необходимо проверить с сетевым администратором умалчиваемые СОМ+-свойства в сетевом окружении всей фирмы. Если вы работаете в небольшой сети и обладаете привилегиями суперпользователя, то можно применить установки, перечисленные в табл. 11.1 и, наилучшим образом работавшие на всех машинах, на которых я тестировал TraceSrv.
    Таблица 11.1. Умалчиваемые установки DCOMCNFG
    На вкладке Default Properties в DCOMCNFG
    Enable Distributed COM Установлен (флажок)
    On This Computer
    Default Authentication Level Connect
    Default Impersonation Level Identify
    На вкладке Default Security в DCOMCNFG.EXE
    Default Access Permissions Everyone (Bee) Allow Access (Доступ разрешен)
    INTERACTIVE Allow Access
    NETWORK Allow Access
    SYSTEM Allow Access
    Default Launch Permissions Administrators Allow Launch
    (Администраторы) (Запуск разрешен)
    Everyone Allow Launch
    INTERACTIVE Allow Launch
    NETWORK Allow Launch
    SYSTEM Allow Launch
    Default Configuration Administrators Full Control Permissions
    (Полное управление)
    CREATOR OWNER Full Control
    Everyone Read (Чтение)
    INTERACTIVE Special Access (Специальный
    доступ. Включены все значеия
    за исключением Create Link,
    Write DAC и Write Owner)
    SYSTEM Full Control

    После регистрации TraceSrv ( либо как части построения, либо с ключом -RegServer командной строки) запустите DCOMCNFG, выберите TraceSrv (или Trace Class в Windows 98) и нажмите кнопку Properties. Я изменял установки только на вкладке Location. Для того чтобы TraceSrv выполнялась только на локальной машине, установите флажок Run Application On This Computer и сбросьте другие. Если требуется выполнять TraceSrv только на другой машине, то включите флажок опции Run Application On The Following Computer и укажите сервер. (Обратите внимание, что утилита DCOMCNFG позволит поместить на панель имя текущего компьютера, но тогда она не будет создавать сервер.) Чтобы избежать многих неприятностей, дважды проверьте, что все опции на вкладке Security установлены для использования по умолчанию.

    Вообще-то не рекомендуется изменять установки в DCOMCNFG — параметры безопасности (security) и идентичности (identity), но если вы это сделали и TraceSrv больше не запускается, выполните утилиту из командной строки с ключом -unRegServer — реестр будет очищен, и можно регистрировать TraceSrv заново (см. выше). Автоматическая регистрация (registration) и "выписка" (unregistration) — удобные свойства ATL.

    Теперь вы знаете, как проектировалась, строилась и устанавливалась TraceSrv, и, вероятно, думаете, что эта тема исчерпана. Сначала я тоже так думал, но затем, когда уже начал работать с TraceSrv, обнаружил некоторые действительно неприятные ошибки.

    Требования к TraceSrv

    Сначала сформулируем цели проектирования TraceSrv, потому что это вообще лучший способ понять, что именно должна выполнять любая программа. Итак:
    1. TraceSrv должна быть совместима с общими языками программирования, включая, как минимум, C++, Visual Basic, Borland Delphi, Visual Basic for Applications, Java, Jscript и VBScript.
    2. TraceSrv должна быть очень проста для использования внутри языка программирования.
    3. TraceSrv должна всегда выполняться так, чтобы любое приложение могло в любой момент соединяться с ней.
    4. Операторы трассировки программы, которая выполняется на нескольких машинах, должны направлять свои результаты в один каталог (файл).
    5. Приложения просмотра трасс (trace viewer applications) должны видеть строки трасс от нескольких машин одновременно.
    6. Должны быть доступны следующие операции обработки необязательных параметров (опций) трассировочных строк:
    • добавление (в строку трассировки) префикса со временем получения строки;
    • добавление префикса с номером строки;
    • добавление префикса с идентификатором (ID) процесса, который послал строку трассировки;
    • добавление в конец записи трассы символов возврата каретки и перевода строки, если необходимо;
    • посылать предложения трассировки через основной отладчик, где выполняется процесс TraceSrv.
    7. Если хотя бы один из параметров TraceSrv, перечисленных в п. 6 изменяется, все активные программы просмотра трасс должны быть уведомлены, для того чтобы все эти программы (даже на других машинах) были скоординированы с текущими опциями.
    На первый взгляд, требования к TraceSrv могут показаться чрезмерно завышенными из-за необходимости многоязычного программирования и работы в сети. Я предполагал, что можно переадресовать многоязычную поддержку простой динамически компонуемой библиотеке (DLL), которую мог бы загружать кто угодно. Поскольку я — прежде всего системный программист, а не Web-разработчик, то сказалось незнание языков VBScript и Java. В частности, при ближайшем знакомстве с VBScript я понял, что никакие хакер-ские трюки не заставят VBScript напрямую вызывать DLL.
    Свет, наконец,

    забрезжил в конце тоннеля, когда выяснилось, что VBScript поддерживает функцию createObject; мне как раз был необходим СОМ-объект, a VBScript способен прекрасно его использовать. Поскольку СОМ-объект работает почти на всех языках, было решено сделать TraceSrv простым СОМ-объектом.

    СОМ-технология легко решает проблему сетевого программирования. Имеется свободно распространяемый расширенный вариант СОМ-технологии — СОМ+, который поддерживает "непрерывное выполнение", потому что СОМ+-серверы могут выполняться как службы Microsoft Win32. Если применяется служба автоматического запуска, то СОМ-объект всегда готов к использованию.

    Моя первая схватка с СОМ+-службами (известными в свое время как службы DCOM2) произошла в далекие времена альфа-версии Microsoft Windows NT 4 и была довольно неприятной. Мало того что нужно было написать службы (не самая легкая работа), но пришлось также изрядно повозиться с СОМ-объектами (перед их подключением). К счастью, всю черную работу по написанию СОМ+-служб выполняет библиотека активных шаблонов (ATL3), которая поставляется с Microsoft Visual C++ 6 и даже включает мастер, помогающий сгенерировать код.

    Как только было обозначено главное направление разработки, возникла необходимость в определении интерфейса для TraceSrv. Главный интерфейс TraceSrv (itrace) определен в IDL-файле4TRACESRV.IDL, показанном в листинге 11-1. Для передачи в TraceSrv операторов трассировки я применяю метод Trace интерфейса iTrace, а чтобы приспособиться к разнообразию языков, установил специальный строчный тип BSTR (см. параметр bstrText в описании операторов трассировки).

    COM+ технология сочетает использование модели компонентных объектов (СОМ) и сервера транзакций корпорации Microsoft (MTS — Microsoft Transaction Server). — Пер.

    DCOM (Distributed Component Object Model) — распределенная модель компонентных объектов. — Пер.

    3ATL — ActiveX Template Library. — Пер.

    IDL-файл — специальный файл, автоматически генерируемый мастерами ATL при создании СОМ-компонента.


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

    Листинг 11-1.TRACERV.IDL

    /*- - - - - - - - - - - - - - - - - - - - - - - - - -

    "Debugging Applications" (Microsoft Press)

    Copyright (c) 1997-2000 John Robbins — All rights reserved.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    import "oaidl.idl";

    import "ocidl.idl";

    [

    object ,

    uuid ( 4D42AOOC-7774-11D3-9F57-OOC04FA34F2C ) ,

    dual ,

    helpstring ( "ITrace Interface" ) ,

    pointer_default ( unique )

    ]

    interface ITrace : IDispatch

    {

    [ id ( 1 ) ,

    helpstring ( "method Trace" ) ]

    HRESULT Trace ( [ in ] BSTR bstrText ) ;

    [ id ( 2 ) ,

    helpstring ( "method FullTrace" ) ]

    HRESULT FullTrace ( [ in ] BSTR bstrText , [ in ] long dwPID ) ;

    [ propget, id ( 3 ) ,

    helpstring ( "property ShowTimeStamps" ) ]

    HRESULT ShowTimeStamps ( [ out, retval ] VARIANT_BOOL *pVal ) ;

    [ propput, id ( 3 ) ,

    helpstring ( "property ShowTimeStamps" ) ]

    HRESULT ShowTimeStamps ( [ in ] VARIANT_BOOL newVal ) ;

    [ propget,

    id ( 4 ) ,

    helpstring ( "property ShowTraceAsODS" ) ]

    HRESULT ShowTraceAsODS ( [ out, retval ] VARIANT_BOOL *pVal ) ;

    [ propput,

    id ( 4 ) ,

    helpstring ( "property ShowTraceAsODS" ) ]

    HRESULT ShowTraceAsODS ( [ in ] VARIANT_BOOL newVal ) ;

    [ propget,

    id ( 5 ) ,

    helpstring ( "property ShowItemNumber" ) ]

    HRESULT ShowItemNumber ( [ out, retval ] VARIANT_BOOL *pVal ) ;

    [ propput,

    id ( 5 ) ,

    helpstring ( "property ShowItemNumber" ) ]

    HRESULT ShowItemNumber ( [ in ] VARIANT_BOOL newVal ) ;

    [ propget,

    id ( 6 ) ,

    helpstring ( "property ShowPID" ). ]

    HRESULT ShowPID ( [ out, retval ] VARIANT_BOOL *pVal ) ;


    [ propput,

    id ( 6 ) ,

    helpstring ( "property ShowPID" ) ]

    HRESULT ShowPID ( [ in ] VARIANTJ30OL newVal ) ;

    [ propget,

    id ( 7 ) ,

    helpstring ( "property AddCRLF" ) ]

    HRESULT AddCRLF ( [ out, retval ] VARIANT_BOOL *pVal ) ;

    [ propput,

    id ( 7 ) ,

    helpstring ( "property AddCRLF" ) ]

    HRESULT AddCRLF ( [ in ] VARIANT_BOOL newVal ) ;

    } ;

    [

    uuid ( 4D42AOOO-7774-11D3-9F57-OOC04FA34F2C ) ,

    version ( 1.0 ) ,

    helpstring ( "TraceSrv 1.0 Type Library" ) ]

    library TRACESRVLib

    {

    importlib ( "stdole32.tlb" ) ;

    importlib ( "stdole2.tlb" ) ;

    [

    uuid ( 4D42AOOE-7774-11D3-9F57-OOC04FA34F2C ) ,

    helpstring ( "_ITraceEvents Interface" )

    ]

    dispinterface _ITraceEvents

    {

    properties: methods:

    [ id ( 1 ) ,

    helpstring ( "method TraceEvent" ) ] HRESULT TraceEvent ( BSTR bstrText ) ;

    [ id ( 2 ) ,

    helpstring ( "method ChangeShowTimeStamps" ) ]

    HRESULT ChangeShowTimeStamps ( VARIANT_BOOL bNewVal ) ;

    [ id ( 3 ) ,

    helpstring ( "method ChangeShowTraceAsODS" ) ]

    HRESULT ChangeShowTraceAsODS ( VARIANT_BOOL bNewVal ) ;

    [ id ( 4 ) ,

    helpstring ( "method ChangeShowItemNumber" ) ]

    HRESULT ChangeShowItemNumber ( VARIANT_BOOL bNewVal ) ;

    [ id ( 5 ) ,

    helpstring ( "method ChangeShowPID" ) ]

    HRESULT ChangeShowPID ( VARIANT_BOOL bNewVal ) ;

    [ id ( 6 ) ,

    helpstring ( "method ChangeAddCRLF" ) }

    HRESULT ChangeAddCRLF ( VARIANT__BOOL bNewVal ) ;

    } ;

    [

    uuid ( 4D42AOOD-7774-11D3-9F57-OOC04FA34F2C ) ,

    helpstring ( "Trace Class" )

    ]

    coclass Trace

    {

    [ default ] interface ITrace ;

    [ default, source ] dispinterface _ITraceEvents ;

    } ;

    } ;

    Для того чтобы написать программу просмотра операторов трассировки, нужно просто обрабатывать события интерфейса iTraceEvents.


    В интерфейсе ITrace определены свойства1TraceSrv, которые реализуют перечисленные выше (в п. 6 списка требований) параметры операторов трассировки (на тот случай, если приложение, использующее TraceSrv, захочет изменить их). Когда свойство программы TraceSrv изменяется, она генерирует событие, которое должна обработать специальная программа просмотра трассы — TraceView. Эта программа (она рассмотрена чуть ниже) показывает, как следует обрабатывать каждое событие, которое генерирует TraceSrv.

    Мастер AppWizard (создающий СОМ-приложения средствами ATL) строит почти 90% кода СОМ+-службы. Мне пришлось написать только интерфейс TraceSrv и обработчики. Большая часть этих кодов находится в файлах TraceSrvTRACE.H и TRACE.CPP на сопровождающем компакт-диске. Они, в основном, выполняют установку и получение свойств и запуск событий. Единственная неординарная функция CTrасе: :ProcessTrace (обрабатывающая строки трассы) показана в листинге 11-2.

    Здесь речь идет о том, что атрибуты propput и propget IDL-файла информируют некоторые языки (типа Visual Basic), что с указанным в них методом нужно обращаться, как со свойством. — Пер.

    Листинг 11-2. Функция CTrасе: :ProcessTrace

    HRESULT CTrace :: ProcessTrace ( BSTR bstrText , long dwPID)

    {

    // Все перепроверяйте и ничему не верьте!

    ASSERT ( this ) ;

    ASSERT ( NULL != bstrText ) ;

    // Длина входной строки. Длина вычисляется после того, как

    // проверен указатель.

    int ilnputLen = 0 ;

    if ( NULL == bstrText )

    {

    return ( Error ( IDS_NULLSTRINGPASSED ,

    GUID_NULL ,

    E_INVALIDARG ) ) ;

    }

    // bstrText содержит некоторый указатель.

    // Удостовериться, что указатель содержит правильное значение.

    ASSERT ( FALSE = IsBadReadPtr ( bstrText , sizeof ( BSTR ) ) ) ;

    ASSERT ( L';\0'; != *bstrText );

    if ( ( TRUE == IsBadReadPtr ( bstrText , sizeof ( BSTR ) ) ) ||

    ( L';\0'; == *bstrText ) )

    {

    return ( Error ( IDS_INVALIDSTRING , GUID_NULL

    E_INVALIDARG ) ) ;


    }

    // Теперь, когда указатель проверен, получить длину

    // входной строки (в символах).

    iInputLen = IstrlenW ( bstrText ) ;

    // Вычислить максимальное число байт, необходимых для

    // входной строки.

    UINT uiSize = ( ilnputLen * sizeof ( OLECHAR ) ) +

    k_SIZE_FULLFORMATBYTES ;

    // Захватить объект lock, чтобы защитить класс m_cOutput.

    // Grab the lock to protect the m_cOutput class.

    ObjectLock lock ( this ) ;

    // Если это первое обращение к ProcessTrace (m_lBuffSize - 0),

    //то этот if-блок служит исходной точкой распределения памяти,

    if ( uiSize >= m_cOutput.BufferSize ( ) )

    {

    // Удалить существующий буфер и распределить больший.

    m_cOutput.Free ( ) ;

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

    . // Это делается для того, чтобы выполнять распределение памяти

    //не так часто. Это компромисс между неиспользуемой

    // дополнительной памятью и временем на непрерывные распределения.

    // Умножение размера буфера на 2 гарантирует также, что

    //сохраняется четный размер памяти. Программа работает

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

    UINT uiAllocSize = uiSize * 2 ;

    // Убедитесь, что получен минимальный размер буфера.

    // Минимальный размер буфера 2 Кбайт, так что в большинстве

    // случаев код в этом if-блоке выполняется только однажды.

    if ( k_MIN_TRACE_BUFF_SIZE > uiAllocSize )

    {

    uiAllocSize = k_MIN_TRACE_BUFF_SIZE ;

    }

    OLECHAR * pTemp = m_cOutput.Allocate ( uiAllocSize ) ;

    ASSERT ( NULL != pTemp ) ;

    if ( NULL == pTemp )

    {

    return ( Error ( IDSJXJTOFMEMORY ,

    GUID_NULL , EJDUTOFMEMORY ) ) ;

    }

    }

    // Все проверено, теперь можно начать реальную работу.

    // Увеличить на 1 итоговый счетчик.

    m_dwCurrCount++ ;

    if ( 100000 == m_dwCurrCount )

    {

    m_dwCurrCount = 0 ;

    }

    // Установить указатель маркера в начало буфера вьвода


    OLECHAR * pCurr = m__cOutput.GetDataBuffer ( ) ;

    if ( -1 = m_vbShowItemNumber )

    {

    pCurr += wsprintfW ( pCurr , L"%05d " , m_dwCurrCount ) ;

    }

    if ( -1 == m_vbShowTimeStamps )

    {

    // Показать метку времени в формате местного пользователя.

    // (здесь сервер, а не в клиент!). Я устанавливаю метку

    // в 24-часовом формате.

    int iLen = GetTimeFormatW ( LOCALE_USER_DEFAULT ,

    LOCALE_NOUSEROVERRIDE |

    TIME_FORCE24HOURFORMAT |

    TIME_NOTIMEMARKER ,

    NULL

    NULL ,

    pCurr ,

    k_SIZE_TIME ) ; ASSERT ( 0 != iLen ) ;

    // Переместить указатель, но не забыть о наличии

    // NULL-символа в конце строки.

    pCurr 4= ( iLen - I ) ;

    11 GetTimeFormat не добавляет дополнительного пробела,

    // поэтому добавляем его сейчас.

    *pCurr = L' ' ;

    pCurr++ ;

    }

    if ( -1 == m_vbShowPID )

    {

    pCurr += wsprintfW ( pCurr , L"[%04X] " , dwPID ) ;

    }

    // Теперь поместите в буфер фактическое сообщение и копируйте

    // NULL-терминатор в конец строки.

    IstrcpynW ( pCurr , bstrText , IlnputLeri + 1 ) ;

    // Переместить pCurr, чтобы указать на NULL-терминатор.

    pCurr += ilnputLen ;

    // Проверить, не нужны ли символы CRLF в конце строки,

    if ( -1 == m_vbAddCRLF )

    {

    if ( ( L';\xOD'; != *( pCurr _ 2 ) ) ||

    ( L';\xOA'; != *( pCurr _ 1 ) ) )

    {

    *( pCurr ) = L';\xOD;;

    *( pCurr + 1 } = L';\xOA'; ;

    pCurr += 2 ;

    *pCurr = YL';\0'; ;

    }

    }

    // Предполагается ли получить снимок для отладчика режима ядра?

    if ( -1 == m_vbShowTraceAsODS )

    {

    OutputDebugStringW ( (OLECHAR*) m_cOutput ) ;

    }

    // Подсчитать длину строки.

    m_cOutput.GetStringByteLength ( ) ;

    // Вывод сообщения о результате трассировки.

    #ifdef _DEBUG

    HRESULT hr =

    #endif

    Fire_TraceEvent ( m_cOutput ) ;

    #ifdef _DEBUG

    if ( ! SUCCEEDED ('hr ) )

    {

    ASSERT ( SUCCEEDED ( hr ) ) ;

    TRACE ( Т ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;


    TRACE ( _T ( "TraceSrv FireJTraceEvent failed!!\n" ) ) ;

    TRACE ( Т ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ) ;

    }

    #endif

    return ( S_OK ) ;

    }

    В целом, реализация TraceSvr довольно проста. Команда Implement Connection Point меню ClassView делает обработку кода интерфейса IconnectionPoint очень приятной. По сравнению с ATL Proxy Generator из Microsoft Visual C++ 5, эта команда значительно усовершенствована.

    Обработке строк типа BSTR было уделено много внимания. Поскольку имелись в виду сценарии, в которых количество операторов трассировки должно увеличиваться весьма интенсивно, то хотелось удостовериться, что строки Обрабатывались максимально быстро. Функция СТгасе: : ProcessTrace в TRACE.CPP выполняет много манипуляций со строками, особенно если учитывать различные элементы, которые могут быть размещены в начале и конце заключительного строчного вывода программы TraceSrv. Первоначально для строчных манипуляций был предназначен класс CComBSTR. Но в результате пошаговой трассировки выяснилось, что почти для каждого метода и оператора в классе он каждый раз выделял или освобождал память с помощью функций Sysxxxstring. Хотя в некоторых приложениях использование ccomBSTR совершенно законно, в программах (таких как TraceSrv), манипулирующих строками, это может привести к снижению реальной производительности.

    Для того чтобы ускорить обработку строк, я написал простой класс с именем CFastBSTR, который обрабатывает тип BSTR напрямую. Класс находится в файле FASTBSTR.H. Единственной его работой является выделение памяти для одиночного буфера данных и варьирование ведущего размера (DWORD) функции GetstringByteLength. Может показаться, что я должен был неминуемо увязнуть в семантике автоматизации типа BSTR, но в этом случае увеличение производительности было важнее, чем консервативное программирование. Если такой подход кажется вам неудобным, то код в CFastBSTR нетрудно изменить, чтобы использовать обычные функции Sysxxxstring.


    Нужно указать еще на одну деталь: рабочее пространство проекта имеет четыре различные конфигурации для сборки приложений — отладочную (debug) и выпускную (release) для многобайтовых символов, и две аналогичных — для символов Unicode. Многобайтовые конфигурации позволяют регистрировать TraceSrv на машинах с Windows 98. Как указано в главе 5, если вы нацеливаетесь исключительно на Windows 2000, то следует компилировать программы, ориентируясь на полноценную работу с Unicode. Поскольку я проектировал TraceSrv как службу Windows 2000, которая определенно не будет выполняться под Windows 98, то версию, устанавливаемую на серверной машине, нужно компилировать в одной из Unicode-конфигураций.

    Теперь, познакомившись с кодом TraceSrv, рассмотрим особенности работы с готовой утилитой TraceSrv. Проект, созданный в Visual C++ 6, который находится на сопровождающем компакт-диске, сгенерирован в основном с помощью мастера COM AppWizard библиотеки активных шаблонов (ATL), так что последний шаг построения должен регистрировать TraceSrv. Все регистрируемые компоненты TraceSrv являются частью свободно распространяемого ATL-кода, но сама программа TraceSrv регистрируется только как локальный ЕХЕ-сервер. TraceSrv не будет выполняться как служба Win32, если не указать в командной строке ключ -service. Можно было сделать регистрацию службы частью процедуры построения, но я выбрал не это, потому что отладка служб Win32 без отладчика режима ядра (типа SoftICE) не проста. Кроме того, если вы находитесь в середине цикла "исправление-компиляция-отладка", то выход в режим командной строки (т. е. на командный процессор) и выполнение команды net stop tracesrv только для того, чтобы заставить конструкцию работать — настоящая пытка. После того как вы осуществили достаточное количество сеансов отладки и тестирования, запуская TraceSrv как локальный сервер, его можно зарегистрировать и запустить как службу.

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

    Что делать дальше с DeadlockDetection?

    DeadlockDetection — довольно полная утилита, и я успешно применял ее для прослеживания нескольких многопоточных блокировок. Как всегда, однако, читателю рекомендуется искать пути расширения DeadlockDetection, чтобы сделать ее более полезной. Вот некоторые из возможных идей:
  • создайте автономное приложение для манипуляций с файлом DEADLOCKDETECTION.INI. Ваша программа могла бы даже улучшиться, если бы она разрешила вам устанавливать DeadDetExt DLL и проверяла, что выбранный DeadDetExt DLL экспортировал правильные функции;
  • можно было бы улучшить оптимизацию функций подключения, если бы они не выполняли никакой регистрации. В этом случае не все значения регистров нужно копировать;
  • сейчас DeadlockDetection просто пропускает подключение тех DLL, которые ей известны. Хорошо бы разработать программный механизм, определяющий, какие DLL следует пропускать.
  • История отладочной войны
    Незавершенные транзакции с объединенными СОМ-объектами Сражение
    Питер Иерарди (Peter lerardi) рассказал мне об интересной многопоточной ошибке, с которой он столкнулся. Он работал над большим DCOM-проектом,который использовал многопоточную DCOM-службу для координации транзакций базы данных. DCOM-служба управляла транзакциями, создавая пул внут-рипроцессных централизованных СОМ-объектов, которые использовались, чтобы записывать и считывать данные из реляционной системы управления базами данных (RDBMS). Межкомпонентная связь выполнялась через сервер очереди сообщений Microsoft (Microsoft Message Queue (MSMQ) Server). Несмотря на то, что выполнялась явная фиксация транзакций, данные в базу данных не записывались. Однако после 3—5-кратных повторных попыток записи, данные, как по мановению волшебной палочки, наконец появлялись в базе. Очевидно, чрезмерные повторения уменьшали производительность приложения и тот факт, что данные не записывались в базу, был причиной для беспокойства.
    Результат
    После нескольких тяжелых сеансов отладки Питер нашел, что DCOM-служба выполняла чтение и запись на отдельных, несинхронизированных потоках.
    Чтение происходило прежде, чем отдельный экземпляр СОМ-объекта базы данных записывал данные. Такое поведение не было очевидным во время отладочных сессий, потому что отладчик форсировал надлежащее таймирование и синхронизацию. Он в конечном счете решил проблему с помощью маркирования экземпляров объекта в Event Log (журнале регистрации событий).

    Урок

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

    Что касается ошибки Питера, то она объясняется очень просто: циклы операций чтения/записи MSMQ выполнялись намного быстрее, чем в базе данных. Хотя Питер и его команда тщательно планировали и прорабатывали многопо-точность в своем проекте, им все еще не доставало некоторого исходного понимания того, насколько быстрее будут выполняться некоторые операции вне их проекта, в реальном мире.

    DeadlockDetection и проблемы высокоуровневого проектирования

    Необходимо было найти способ реализации DeadlockDetection, который позволил бы выполнить все предыдущие требования. Во-первых, необходимо было определить, какие нужны функции для организации такого управления программой, который дал бы возможность сообщать пользователю полную трассу блокировки. В табл. 12.1 перечислены (и сгруппированы по типам) все функции, которые, по моему мнению, были нужны для организации управления в программе DeadlockDetection.
    Обдумывая, как собрать информацию, необходимую для удовлетворения первых четырех требований, я понял, что надо перехватывать (или подключаться по адресу1) функции, указанные в табл 12.1, для того чтобы регистрировать (записывать) получение и освобождение объектов синхронизации. Подключение функций через адрес — нетривиальная задача (реализация соответствующего кода рассмотрена в разделе "Подключение импортированных функций" чуть ниже). Такое подключение накладывает на DeadlockDetection следующее ограничение: ее код должен постоянно храниться в DLL, потому что такие подключения применяются только к адресному пространству, в котором он создан. Это ограничение означает, что пользователь должен загрузить DLL с программой DeadlockDetection в ее адресное пространство. Это требование не слишком жестко по сравнению с получаемыми от него преимуществами. Как и DLL, утилита будет легко объединяться с программой пользователя (см. условие, указанное в п. 7 списка требований предыдущего раздела).
    Сбор информации, удовлетворяющей требованиям 1—4, является прямым следствием выбора особого подхода "с подключением функций в процессе выполнения программы" (in-process function hooking approach). Такой подход означает, что каждая многопоточная функция и функция синхронизации будет вызываться прямо в процессе выполнения DeadlockDetection вместе со всей необходимой информацией.
    Таблица 12.1. Функции, мониторинг которых осуществляет DeadlockDetection
    Тип Функция
    Функции, относящиеся к потокам
    CreateThread ExitThread SuspendThread ResumeThread TerminateThread
    Функции критической секции
    Initial izeCr it icalSection
    InitializeCriticalSectionAndSpinCount
    DeleteCr it icalSection
    EnterCr it icalSection
    LeaveCr it icalSection
    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 SignalOb j ectAndWait

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

    CloseHandle ExitProcess GetProcAddress




    Требование минимального вмешательства DeadlockDetection в программу пользователя (п. 5 списка требований) довольно трудно удовлетворить. Рассчитывая на то, что разработчик должен знать, какие типы объектов синхронизации используются в его программе, я сгруппировал типы объектов так, чтобы можно было указывать лишь те функции, которые требуется подключить. Например, если проверяются только блокировки на мьютексах, то можно обрабатывать только мьютекс-функции.

    Работая с DeadlockDetection, можно указать "на лету", какой набор функций объектов синхронизации надо наблюдать. Вы можете также включать и выключать DeadlockDetection столько раз, сколько необходимо. Можно даже назначить для приложения клавишу быстрого вызова или специальный пункт меню, который подключает всю DeadlockDetection-систему целиком. Реализация этих узких возможностей удовлетворяет требованиям п. 5 и помогает реализовать требование п. 7.

    Что касается требования п. 6 — сделать обработку вывода настолько расширяемой, насколько возможно, то пользователю предоставляется возможность самостоятельно оформлять вывод. Организуя раздельное хранение кода основных подключений и кода обработки вывода, удалось достичь большей степени повторного использования кода, потому что единственную изменяемую часть кода — выводную — намного легче разрабатывать, чем его ядро. Я называю выводящие части кода расширениями утилиты DeadlockDetection (или еще короче — DeadDetExi). DeadDetExt это просто DLL, которая содержит несколько экспортируемых функций. DeadlockDetection отыскивает и вызывает эти функции, когда в них возникает необходимость.

    Настало время объяснить, как можно использовать DeadlockDetection. Понимая рассмотренные требования и возможности применения этой утилиты, легче разобраться и в ее реализации.

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

    Первым шагом в использовании утилиты DeadlockDetection является размещение файла ее инициализации DEADLOCKDETECTION.DLL и соответствующего файла расширения DeadDetExt DLL в том же каталоге, в котором находится сама программа. Файл инициализации — это простой INI-файл, который, как минимум, должен указывать имя файла расширения (в нашем случае DeadDetExt), чтобы загружать его. В следующем примере показан файл инициализации DEADLOCKDETECTION.INI, который загружает файл TEXTFILEDDEXT.DLL
    [Initialization]
    Единственное обязательное значение — имя файла DeadDetExt,
    который будет обрабатывать вывод
    ExtDll = "TextFileDDExt.dll"
    Если StartlnDllMain равен 1, DeadlockDetection будет
    инициализирована в своей функции DllMain, чтобы регистрация
    могла начинаться как можно раньше.
    StartlnDllMain = О
    Если StartlnDllMain равно 1, InitialOpts указывает
    начальные режимы для DeadlockDetection. Это значение
    является комбинацией DDOPT_*-флажков.
    InitialOpts = 0
    Как можно видеть по некоторым установкам, инициализация DeadlockDetection возможна только в том случае, если в ней уже вызвана функция LoadLibrary. Хорошей идеей профилактической (упреждающей) отладки было создание "закулисной" инициализации приложения, которая вызывает LoadLibrary для DLL с указанным именем, если приложение обнаруживает специальный ключ реестра или переменную окружения. Этот альтернативный подход к инициализации приложения означал бы, что условная компиляция не нужна, а пользователю предоставлены средства прямого получения DLL-файлов в свое адресное пространство. Конечно, все это предполагает, что все DLL, которые загружаются таким способом, достаточно "разумны", чтобы совершенно самостоятельно инициализироваться в своих DiiMain-секциях и не требовать вызова никаких других экспортируемых функций в DLL.
    Если вместо использования INI-файла ваш код устанавливает параметры инициализации, то нужно включить в приложение файл заголовков DEADLOCKDETECTION.H и компоновать его с библиотечным файлом DEADLOCKDETECTION.LIB.
    Для того чтобы инициализировать DeadlockDetection самостоятельно, надо лишь вызвать в подходящий момент функцию openDeadiockDetection, которая принимает единственный параметр — режимы начальных отчетов (initial reporting options). Все DDOPT_*-флажки перечислены в табл. 12.2. Чтобы получить возможность записывать (регистрировать) всю ключевую информацию об объектах синхронизации, функцию OpenDeadiockDetection необходимо вызвать перед тем, как приложение начнет создание потоков.

    Таблица 12.2. Режимы отчетов DeadlockDetection

    Флажок

    Пределы регистрации

    DDOPT_THREADS

    Функции, относящиеся к потокам

    DDOPT CRITSEC

    Функции критической секции

    DDOPT_MUTEX

    Функции мьютекса

    DDOPT SEMAPHORE

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

    DDOPT_EVENT

    Функции событий

    DDOPT ALL

    Все подключаемые функции

    В любой точке программы можно изменять режимы отчетов, вызывая функцию SetDeadiockDetectionOptions. Эта функция принимает тот же набор флажков (объединенных операцией OR), что и функция OpenDeadiockDetection. Чтобы получить текущий режим отчетов, нужно вызвать функцию GetDeadlockDetectionOptions. Во время выполнения программы можно изменять текущий режим сколько угодно раз. Если нужно приостановить или возобновить регистрацию, вызывайте функцию ResumeDeadlockDetection ИЛИ SuspendDeadlockDetection.

    Наряду с исходным кодом DeadlockDetection, сопровождающий компакт-диск содержит и DLL-файл расширения DeadDetExt (TEXTFILEDDEXT.DLL). Это относительно простое расширение записывает всю информацию в текстовый файл. Когда DeadlockDetection выполняется с TEXTFILEDDEXT.DLL, расширение создает текстовый файл в том же каталоге, где находится выполняемая программа. Текстовый файл использует имя выполняемого файла с расширением .DD. Например, если выполняется SIMPTEST.EXE, то в результате будет создан файл SIMPTEST.DD. Листинг 12-1 показывает пример вывода из TEXTFILEDDEXT.DLL.

    Листинг 12-1. Вывод DeadlockDetection с использованием TEXTFILEDDEXT.DLL


    TID Ret Addr C/R Ret Value Function & Params

    0x000000F? [Ox004011AC] (R) 0x00000000 InitializeCriticalSection

    0x00403110

    0x000000F7 [0x00401106] (R) 0x00000290 CreateEventA 0x00000000, 1, 0,

    0x004030F0 [The event name]

    0x000000F? [Ox004011E9] (R) 0x00000294 CreateThread 0x00000000,

    0x00000000, 0x00401000,

    0x00000000, 0x00000000,

    0x0012FF68

    0x000000F7 [0x0040120C] (R) 0x00000298 CreateThread 0x00000000,

    0x00000000, 0x004010BC,

    0x00000000, 0x00000000,

    0x0012FF68

    0x000000FV [0x00401223] (C) EnterCriticalSection 0x00403110

    0x000000F7 [0x00401223] (R) 0x00000000 EnterCriticalSection 0x00403110 0x000000F? [0x00401238] (C) WaitForSingleObject 0x00000290,

    INFINITE

    0x000000FF [Oxl020B973] (C) EnterCriticalSection 0xl025CE90

    0x000000FF [Oxl020B973] (R) 0x00000000 EnterCriticalSection 0xl025CE90

    0x00000l0C [Ox004010F3] (R) 0x000002A4 OpenEventA 0x001F0003, 0,

    0x004030BC

    [The event name]

    Заметьте, что информация о функции и параметрах (в столбце Function & Params) в листинге 12-1 не поместилась в одной строке и поэтому перенесена на следующие строки (в этом же столбце). Информация выводится на экран в следующем порядке:

    1. Идентификатор (ID) выполняющегося потока.

    2. Адрес возврата, указывающий, какая из ваших функций вызвала функцию синхронизации. Используя утилиту CrashFinder, рассмотренную в главе 8, можно просмотреть адреса возвратов и узнать, как вы входили в ситуации блокировки.

    3. Индикатор вызова/возврата (Call/Return — C/R), помогающий идентифицировать действия, которые происходят до и после конкретной функции.

    4. Возвращаемое значение функции, если программа возвращает функции отчетов.

    5. Имя функции синхронизации.

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

    Если запускаемое приложение блокируется, то для того чтобы увидеть последний вызванный элемент синхронизации, аннулируйте процесс и просмотрите файл вывода.TEXTFILEDDEXT.DLL сохраняет последнее файловое обновление, сбрасывая буферы файлов каждый раз, когда вызываются функции WaitFor*, EnterCriticalSection И TryEnterCriticalSection.

    Предостережение

    Если включить полную регистрацию (всех функций), то можно довольно долго генерировать чрезвычайно большой файл. Используя Visual С++-приложение MTGDI, я генерировал текстовый файл объемом в 11 Мбайт за одну или две минуты, если создавал пару потоков.

    Основные моменты реализации

    Одной из первичных целей реализации DeadlockDetection была организация управления этой утилитой как с помощью данных, так и с помощью таблиц. Если немного отстраниться и посмотреть на то, чем занимается DLL при обработке подключения, то нетрудно заметить, что для каждой функции из табл. 12.1 обработка почти идентична. Подключаемая функция вызывается, определяет, ее ли класс функций контролируется, вызывает реальную функцию и (если регистрация для этого класса включена) регистрирует информацию, после чего выполняет возврат в вызывающую программу. Надо было написать набор аналогичных функций подключения, причем сделать их как можно более простыми. Сложные функции подключения — идеальная питательная почва для размножения ошибок.
    Чтобы показать эту простоту, поговорим о том, как была написана DeadDetExt DLL. DeadDetExt DLL должна иметь три экспортируемых функций. Две первых — DeadDetExtOpen И DeadDetExtClose — самоочевидны. Интересна функция DeadDet, которую вызывает каждая функция подключения, когда имеется информация для записи. DeadDetProcessEvent принимает единственный параметр — указатель на структуру DDEVENTINFO:
    typedef struct tagDDEVENTINFO
    {
    // Идентификатор, который указывает, что содержит остальная часть
    // этой структуры
    eFuncEnum eFunc ;
    // Индикатор pre- или post-вызова
    ePrePostEnum ePrePost ;
    // Адрес возврата. Этот адрес помогает в нахождении вызова
    // функции.
    DWORD dwAddr;
    // ID вызывающего потока
    DWORD dwThreadld;
    // Возвращаемое значение для post-вызовов.
    DWORD dwRetValue ;
    // Информация параметра. Преобразовать тип этой информации
    //в подходящую структуру для функции (как описано ниже).
    // При доступе к параметрам трактовать их как read-only
    // (только-для-чтения).
    DWORD dwParams
    } DDEVENTINFO-, * LPDDEVENTINFO;
    Полный вывод для единственной функции, которая появляется в листинге 12-1, формируется информацией структуры DDEVENTINFO. Большинство полей в ней самоочевидны, но поле dwParams нуждается в специальном упоминании.
    Листинг 12-3. DD_FUNCS.H

    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - -

    "Debugging Applications" (Microsoft Press)

    Copyright (c) 1997-2000 John Robbins — All rights reserved.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    Прототипы для всех функций подключения и код пролога/эпилога

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    #ifndef J3D_FUNCS_H

    #define _DD_FUNCS__H

    /*//////////////////////////////////////////////////////////////

    Все функции подключения имеют спецификатор _declspec (naked), так что я должен обеспечить пролог и эпилог. Заказной пролог и эпилог необходим по нескольким причинам:

    1. Функции, написанные на С, не имеют никакого контроля над тем, какие регистры используются или когда компилятор сохраняет исходные значения регистров. Отсутствие контроля над регистрами означает, что получение адреса возврата почти невозможно. Для проекта DeadlockDetection адрес возврата имеет решающее значение.

    2. Я также хотел передавать параметры в обрабатывающую функцию расширяющей DLL без необходимости копировать большие объемы данных при каждом вызове функции.

    3. Поскольку почти все функции подключения ведут себя одинаково, я установил общие переменные, необходимые во всех функциях.

    4. Функции подключения не могут изменять возвращаемых значений

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

    Основная функция подключения напоминает следующую функцию:

    HANDLE NAKEDDEF DD_OpenEventA ( DWORD dwDesiredAccess,

    BOOL blnheritHandle ,

    LPCSTR IpNarne )

    {

    // Любые локальные переменные функции должны быть специфицированы

    // перед рабочим кодом.


    // HOOKFN_PROLOG должен быть специфицирован сразу же после

    // локальных переменных.

    HOOKFN_PROLOG ();

    // Включена ли регистрация типа функции?

    if ( TRUE == DoLogging ( DDOPT_EVENT))

    {

    // Используйте макрос FILL_EVENTINFO, чтобы заполнить переменную

    // stEvtlnfo, которая объявлена в макросе HOOKFN_PROLOG. Все

    // функции подключения автоматически имеют некоторые локальные

    // переменные, помогающие стандартизировать их код.

    FILL_EVENTINFO ( eOpenEventA);

    // НЕОБХОДИМО вызвать макрос REAL_FUNC_PRE_CALL перед вызовом

    // реальной функции, или если регистры ESI и EDI не будут

    // сохраняться во время вызова.

    REAL_FUNC_PRE_CALL ();

    // Вызвать реальную функцию. Возвращаемое значение, сохраняемое

    //в ЕАХ, сохраняется как часть обработки REAL_FUNC_POST_CALL.

    OpenEventA ( dwDesiredAccess, blnheritHandle , IpName );

    //Вы должны назвать макрос REAL_FUNC_POST_CALL после вызова

    // реальной функции. Значения регистров и последней ошибки

    // сохраняются как часть REAL_FUNC_POST_CALL.

    REAL_FUNC_POST_CALL ();

    // Вызвать код регистрации для регистрации события.

    ProcessEvent ( sstEvtlnfo);

    }

    else

    {

    // См. комментарии выше. Предложение else обрабатывает случай,

    // когда функция не зарегистрирована.

    REAL_FUNC_PRE_CALL ();

    OpenEventA ( dwDesiredAccess, blnheritHandle , IpName );

    REAL_FUNC_POST_CALL ();

    }

    // HOOKFN_EPILOG — последний макрос в функции. Его параметр — это

    // число параметров функции, так что стек будет очищен правильно.

    // Макрос HOOKFN_EPILOG заботится также об установке во всех

    // регистрах тех же значений, которые возвратила реальная

    // функция.

    HOOKFN_EPILOG ( 3) ;

    }

    //////////////////////////////////////////////////////////////////*/

    /*//////////////////////////////////////////////////////////////

    Структура состояния регистра. Я использую эту структуру, чтобы гарантировать, что ВСЕ регистры возвращаются точно в том состоянии, в каком они прибыли из реальной функции.


    Заметьте, что регистры ЕВР и ESP обрабатываются как часть пролога.

    ////////////////////////////////////////////////*/

    typedef struct tag__REGSTATE

    {

    DWORD dwEAX;

    DWORD dwEBX;

    DWORD dwECX;

    DWORD dwEDX;

    DWORD dwEDI;

    DWORD dwESI;

    DWORD dwEFL;

    } REGSTATE, * PREGSTATE;

    /*//////////////////////////////////////////////////////////

    Общий код пролога для всех DD_*-функций

    //////////////////////////////////////////////////////////*/

    #define HOOKFN_PROLOG() \

    /* Все функции подключения автоматически получают три одинаковые */\

    /* переменные. */\

    DDEVENTINFO stEvtlnfo ; /* Информация о событиях для функции */\

    DWORD dwLastError; /* Значение последней ошибки */\

    REGSTATE stRegState ; /* Состояние регистра */\

    { \

    _asm PUSH EBP /* ЕВР всегда сохраняйте явно. */\

    _asm MOV EBP, ESP /* Переместить стек */\

    _asm MOV EAX, ESP /* Получить указатель стека для вычисления /\

    /* адреса возврата и параметров. */\

    _asm SUB ESP, _LOCAL_SIZE /* Сохранить пространство для локальных*/\

    / * переменных * / \

    _asm ADD EAX, 04h + 04h /* Счет для PUSH EBP и адреса возврата. */\

    /* Сохранить начало параметров в стеке. */\

    _asm MOV [stEvtlnfo.dwParams], EAX \

    _asm SUB EAX, 04h /* Вернуться к адресу возврата. */\

    _asm MOV EAX, [EAX] /* EAX теперь содержит адрес возврата. */\

    /* Сохранить адрес возврата. */\

    _asm MOV [stEvtlnfo.dwAddr], EAX. \

    _asm MOV dwLastError, 0 /* Инициализировать dwLastError. */\

    /* Инициализировать информацию событий. */\

    _asm MOV [stEvtlnfo.eFunc], eUNINITIALIZEDFE \

    _asm MOV [stRegState.dwEDI], EDI /* Сохранить два регистра /\

    _asm MOV [stRegState.dwESI], ESI /* на время вызовов функций. */\

    }

    /*///////////////////////////////////////////////////////////

    Общий код эпилога для всех 00_*-функций. INumParams -число параметров функции, которая используется для восстановления стека после вызова подключения.


    //////////////////////////////////////////////////////////*/

    #define HOOKFN_EPILOG(iNumParams) \

    { \

    SetLastError ( dwLastError); /* Установить значение последней */\

    /* ошибки реальной функции. */\

    _asm ADD ESP, _LOCAL_SIZE /* Добавить размер локальных */\

    /* переменных. */\

    _asm MOV EBX, [StRegState.dwEBX]/* Восстановить все регистры так, */\

    _asm MOV ECX, [stRegState.dwECX]/* чтобы данный вызов выглядел */\

    _asm MOV EDX, [stRegState.dwEDX]/* идентично перехваченной */\

    _asm MOV EDI, [stRegState.dwEDI]/*функции. */\

    _asm MOV ESI, [StRegState.dwESI] \

    _asm MOV EAX, [StRegState.dwEFL] \

    _asm SAHF . \

    _asm MOV EAX, [stRegState.dwEAX] \

    _asm MOV ESP, EBP /* Передвинуть назад ESP. */\

    _asm POP EBP /* Восстановить сохраненный EBP. */\

    _asm RET iNumParams * 4 /* 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 /* Сохранить значение ЕВХ. */\

    _asm MOV [stRegState.dwECX], ECX /* Сохранить значение ЕСХ. */\

    _asm MOV [stRegState.dwEDX], EDX /* Сохранить значение EDX. */\

    _asm MOV [stRegState.dwEDI], EDI /* Сохранить значение EDI. */\

    _asm MOV [stRegState.dwESI], ESI /* Сохранить значение ESI. */\

    _asm XOR . EAX, EAX /* Обнулить ЕАХ. */\

    _asm LAHF /* в АН.*/\

    _asm MOV [stRegState.dwEFL], EAX /* Загрузить значение флажков. */\

    } \

    dwLastError = GetLastError (); /* Сохранить значение последней */\

    /* ошибки. */\

    { \

    _asm MOV EAX, [stRegState.dwEAX] /* Восстановить ЕАХ */\

    /* к его исходному значению. */\

    /* Установить возвращаемое */\

    /* значение для информации. */\

    _asm MOV [stEvtlnfo.dwRetValue] , ЕАХ \

    }

    /*////////////////////////////////////////////////////////////

    Удобный макрос для заполнения информационной структуры события

    /////////////////////////////////////////////////////////////////*/

    #define FILL__EVENTINFO(eFn) \

    stEvtlnfo.eFunc = eFn ; \

    stEvtlnfo.ePrePost = ePostCall; \

    stEvtlnfo.dwThreadld = GetCurrentThreadld ()

    /*///////////////////////////////////////////////////////////////

    Объявление для всех DD_*-определений

    ////////////////////////////////////////////////////////////////*/

    #define NAKEDDEF _declspec(naked)

    /*///////////////////////////////////////////////////////////////

    БОЛЬШОЕ ПРИМЕЧАНИЕ

    Все следующие прототипы похожи на cdecl-функции. Но это не так — все они stdcall-функции! Заказной пролог и эпилог гарантируют, что используется правильное соглашение о вызовах!

    ////////////////////////////////////////////////////////////////*/

    ////////////////////////////////////////////////////////////////*/

    // Обязательные функции, которые должны быть перехвачены, чтобы выполнить

    // системную работу

    HMODULE DD_LoadLibraryA ( LPCSTR IpLibFileName);


    HMODULE DD_LoadLibraryW ( LPCWSTR IpLibFileName);

    HMODULE DD_LoadLibraryExA ( LPCSTR IpLibFileName,

    HANDLE hFile ,

    DWORD dwFlags );

    HMODULE DD_LoadLibraryExW ( LPCWSTR IpLibFileName,

    HANDLE hFile , DWORD dwFlags );

    VOID DD_ExitPr

    FARPROC DD_GetProcAddress ( HMODULE hModule, LPCSTR IpProcName);

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

    Функции, специфические для потоков

    HANDLE DD_CreateThread (LPSECURITY_ATTRIBUTES IpThreadAttributes ,

    DWORD dwStackSize ,

    LPTHREAD_START_ROUTINE IpStartAddress ,

    LPVOID IpParameter ,

    DWORD dwCreationFlags ,

    LPDWORD IpThreadld );

    VOID DD_ExitThread ( DWORD dwExitCode);

    DWORD DD_SuspendThread ( HANDLE hThread);

    DWORD DD_ResumeThread ( HANDLE hThread);

    BOOL DDjrerminateThread ( HANDLE hThread, DWORD dwExitCode);

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

    Ожидание и специальные функции

    DWORD DD_WaitForSingleObject ( HANDLE hHandle ,

    DWORD dwMilliseconds );

    DWORD DD_WaitForSingleObjectEx ( HANDLE hHandle ,

    DWORD dwMilliseconds,

    BOOL bAlertable );

    DWORD DD_WaitForMultipleObjects( DWORD nCount ,

    CONST HANDLE * IpHandles ,

    BOOL bWaitAll ,

    DWORD dwMilliseconds );

    DWORD DD_WaitForMultipleObjectsEx( DWORD nCount ,

    CONST HANDLE * IpHandles ,

    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 IpCriticalSection);


    BOOL DD_InitializeCriticalSectionAndSpinCount (

    LPCRITICAL_SECTION IpCriticalSection,

    DWORD dwSpinCount );

    VOID DD_DeleteCriticalSection(LPCRITICAL_SECTION IpCriticalSection);

    VOID DD_EnterCriticalSection ( LPCRITICAL_SECTION IpCriticalSection);

    VOID DD_LeaveCriticalSection ( LPCRITICAL_SECTION IpCriticalSection);

    DWORD DD__SetCriticalSectionSpinCount (

    LPCRITICAL_SECTION IpCriticalSection,

    DWORD dwSpinCount );

    BOOL DD_TryEnterCriticalSection ( LPCRITICAL_SECTION IpCriticalSection);

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

    // Функции мьютекса

    HANDLE DD_CreateMutexA ( LPSECURITY_ATTRIBUTES IpMutexAttributes,

    BOOL blnitialOwner ,

    LPCSTR IpName ) ;

    HANDLE DD_CreateMutexW ( LPSECURITY_ATTRIBUTES IpMutexAttributes,

    BOOL blnitialOwner ,

    LPCWSTR IpName );

    HANDLE DD_OpenMutexA ( DWORD dwDesiredAccess,

    BOOL blnheritHandle ,

    LPCSTR IpName ) ;

    HANDLE DD_OpenMutexW ( DWORD dwDesiredAccess,

    BOOL blnheritHandle ,

    LPCWSTR IpName );

    BOOL DD_ReleaseMutex ( HANDLE hMutex);

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

    // Функции семафора

    DD_CreateSemaphoreA ( LPSECURITY_ATTRIBUTES IpSemaphoreAttributes,

    LONG UnitialCount ,

    LONG IMaximumCount ,

    LPCSTR IpName );

    HANDLE

    DD_CreateSemaphoreW ( LPSECURITY_ATTRIBUTES IpSemaphoreAttributes,

    LONG UnitialCount ,

    LONG IMaximumCount ,

    LPCWSTR IpName );

    HANDLE DD_OpenSemaphoreA ( DWORD dwDesiredAccess,

    BOOL blnheritHandle ,

    LPCSTR IpNaroe );

    HANDLE DD_OpenSemaphoreW ( DWORD dwDesiredAccess,

    BOOL blnheritHandle ,

    LPCWSTR IpNarae );

    BOOL DD_ReleaseSemaphore ( HANDLE hSemaphore ,

    LONG IReleaseCount ,

    LPLONG IpPreviousCount );

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

    // Функции событий

    HANDLE DD__CreateEventA ( LPSECURITY_ATTRIBUTES IpEventAttributes,

    BOOL bManualReset ,

    BOOL blnitialState ,


    LPCSTR IpName );

    HANDLE DD_CreateEventW ( LPSECURITY_ATTRIBUTES IpEventAttributes,

    BOOL bManualReset ,

    BOOL blnitialState ,

    LPCWSTR IpName );

    HANDLE DD_OpenEventA ( DWORD dwDesiredAccess,

    BOOL blnheritHandle ,

    LPCSTR IpName );

    HANDLE DDjOpenEventW ( DWORD dwDesiredAccess,

    BOOL blnheritHandle , LPCWSTR IpName );

    BOOL DD_PulseEvent ( HANDLE hEvent);

    BOOL DD_ResetEvent ( HANDLE hEvent);

    BOOL DD_SetEvent ( HANDLE hEvent);

    #endif // _DD_FUNCS_H

    Сделаю несколько коротких замечаний относительно утилиты DeadlockDetection. Во-первых, DeadlpckDetection всегда активна в приложении, даже если вы откладываете ее регистрацию. Вместо динамических подключений и отключений, я оставляю функции подключенными, а для того чтобы определить, как подключение должно себя вести, просматриваю некоторые внутренние флажки. Сохранение всех функций подключенными облегчает переключение регистрации различных функций во время выполнения, но это добавляет и некоторые издержки. Было интуитивно ясно, что организация подключений и отключений "на лету" привела бы к увеличению количества ошибок в коде DeadlockDetection.

    Во-вторых, DeadlockDetection подключает функции вне DLL, когда подключается к программе через функцию LoadLibrary. Однако она может получить управление только после того, как выполнилась функция DllMain этой DLL, поэтому если какие-нибудь объекты синхронизации создаются или используются во время выполнения DllMain, то DeadlockDetection может пропустить их.

    В-третьих, DeadlockDetection подключает также функции GetProcAddress и ExitProcess. Подключение GetProcAddress происходит в том случае, если ваша программа вызывает GetProcAddress, чтобы найти (во время выполнения) метод синхронизации.

    Я подключаю ExitProcess, потому что, когда приложение заканчивается, необходимо отключить и завершить DeadlockDetection так, чтобы она не завершилась аварийно или не подвесила вашу программу. Поскольку не существует никакого способа управлять порядком разгрузки DLL во время завершения программы, можно легко попасть в ситуации, в которых DLL, связанная с DeadlockDetection (например, DeadDetExt), была бы разгружена раньше самой DeadlockDetection.К счастью, очень немногие разработчики организуют основное многопоточное управление после того, как приложение вызывает функцию ExitProcess.

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

    Отказ от многопоточной организации

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


    Подключение импортированных функций

    Существует много способов подключения вызовов функций в программу. Довольно сложный способ заключается в следующем: нужно отыскать все CALL-инструкции вызовов и заменить указанный в них адрес одним из своих собственных. Этот подход сложен и чреват ошибками. К счастью, в случае DeadlockDetection функции, которые надо подключать, являются импортированными и их намного легче обрабатывать, чем CALL-инструкции.
    Импортированной называют функцию, которая приходит из DLL. Например, когда программа вызывает функцию outputoebugstring, то она обращается к функции, которая постоянно находится в KERNEL32.DLL. Только начав заниматься программированием для Microsoft Win32, я думал, что вызов импортированной функции ведет себя точно так же, как вызов любой другой функции: инструкция CALL или инструкция ветвления передает управление на нужный адрес и начинает выполнять импортированную функцию. Единственным различием могло бы быть то, что в случае импортированной функции программный загрузчик операционной системы должен был бы пробежаться через выполняемый файл и подправить адреса вызовов так, чтобы учесть адрес загруженной в память DLL. Когда же я посмотрел, как на самом деле организовано обращение к импортированной функции, то был поражен простотой и красотой ее реализации.
    Проблемы станут очевидными, если вспомнить, сколько существует API-функций, и как легко можно вызывать одни и те же функции много раз и в любой точке программы. Если бы загрузчик должен был находить и заменять каждое обращение к OutputDebugstring, например, то загрузка программы длилась бы вечно. Даже если бы компоновщик генерировал полную таблицу, указывающую, где каждое обращение к OutputDebugstring имело место в коде, огромное количество циклов и записей в памяти сделали бы время загрузки мучительно медленным.
    Как же загрузчик сообщает вашему приложению, где можно найти импортированную функцию? Решение дьявольски остроумно. Подумав, куда направлены обращения к OutputDebugstring, вы скоро поймете, что каждое обращение должно направляться по одному и тому же адресу — адресу загрузки OutputDebugstring в памяти.
    Конечно, ваше приложение не может знать этот адрес заранее, поэтому все вызовы OutputDebugstring направляются через единственный косвенный адрес. Когда программный загрузчик загружает выполняемый файл и связанные с ним DLL-файлы в память, загрузчик устанавливает этот косвенный адрес так, чтобы он соответствовал окончательному адресу загрузки OutputDebugstring. Компилятор заставляет эту косвенную адресацию работать, генерируя переход к косвенному адресу каждый раз, когда код вызывает импортированную функцию. Этот косвенный адрес сохраняется в секции импорта (.idata1) выполняемого файла. Если импорт выполняется через объявление _declspec(dllimport), то вместо косвенного перехода код выполняет косвенное обращение, экономя, таким образом, пару инструкций на вызове функции.

    Idata — первый символ в этом имени (i) от англ, import (импорт). — Пер.

    При подключении импортированной функции выполняются следующие операции: поиск секции импорта выполняемого файла, поиск адреса конкретной функции, которую вы хотите подключить, и затем запись адреса подключаемой функции на свое место (в коде). Хотя поиск и замена адресов функций могут показаться довольно трудоемким занятием, но тут уж ничего не поделать — так организован РЕ2-формат файлов в Win32.

    РЕ - Portable Executable. - Пер.

    В главе 10 своей превосходной книги "Секреты системного программирования Windows 95" (Matt Pietrek. System Programming Secrets, — IDG Books, 1995) Мэт Пьетрек описывает метод подключения импортированных функций. Код Мэта просто отыскивает секцию импорта модуля и, используя значение, возвращаемое из вызова функции GetProcAddress, организует циклический просмотр списка импортированных функций. Обнаружив нужную функцию, он перезаписывает адрес подключаемой функции на исходный адрес импортированной функции.

    После выхода книги Мэта в этой методике произошло два небольших изменения. Во-первых, когда Мэт писал книгу, большинство разработчиков не объединяло секцию импорта с другими РЕ-секциями. Поэтому, если секция импорта находится в памяти, защищенной от записи (с атрибутом доступа "read-only"), то запись адреса подключения вызывает нарушение доступа.


    Я решил эту проблему простой переустановкой значения атрибута доступа к виртуальной памяти на "read-write" (перед записью адреса подключаемой функции). Вторая проблема, справиться с которой немного труднее, появляется из-за того, что при работе под Windows 98 иногда не удается подключить импортированные функции.

    При использовании DeadlockDetection желательно иметь возможность переадресовать поточные функции во время выполнения приложения, даже когда оно выполняется под отладчиком. Хотя можно предположить, что подключение функций при работе под отладчиком не должно вызывать особых проблем, это не так. При выполнении программы под Windows 2000 или под Windows 98 вне отладчика, когда вы вызываете GetProcAddress, чтобы найти адрес функции, и затем просматриваете секцию импорта, отыскивая этот адрес в ее списке, вы всегда будете находить данный адрес. Если же программа выполняется под отладчиком в Windows 98, то вызов GetProcAddress возвращает другой адрес — не тот, что при выполнении без отладчика. В этом случае GetProcAddress возвращает адрес "отладочного переходника" (debug thunk1) — специальной оболочки вокруг реального вызова.

    Thunk — "переходник" (небольшая секция кода, выполняющая преобразование (напр., типов) или обеспечивающая вызов 32-разрядного кода из 16-разрядного и наоборот). Здесь речь идет о специальном отладочном переходнике, который используется при отладке приложения. — Пер.

    Как сказано в главе 4, в операционной системе Windows 98 не реализовано "копирование-при-записи". Отладочный переходник, возвращаемый при выполнении под отладчиком, — это то средство, с помощью которого Windows 98 предохраняет отладчики от попытки входа в системные функции, расположенные выше 2 Гбайтной отметки памяти. В целом, отсутствие "копирования-при-записи" — небольшая проблема для большинства разработчиков. Она важна только для тех, кто пишет отладчики или хочет правильно подключать функции независимо от того, выполняются они под отладчиком или нет.


    К счастью, получение реального адреса для импортированной функции не слишком сложная задача — требуется только немного больше работы, и нужно избегать вызовов функции GetProcAddress. Структура IMAGE_IMPORT_ DESCRIPTOR РЕ-файла, которая содержит всю информацию о функциях, импортированных из конкретной DLL, имеет указатели на два массива в выполняемом файле. Эти массивы называются таблицами адресов импорта (Import Address Tables — IAT) или, иногда, массивами данных переходников (thunk data arrays). Первый указатель ссылается на реальную IAT, которую программный загрузчик устанавливает, когда загружает выполняемый файл.

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

    Листинг 12-2 показывает функцию HookimportedFunctionsByName, предназначенную для организации подключения импорта. В табл. 12.3 показаны и описаны все параметры этой функции. Желая сделать подключение максимально обобщенным, я побеспокоился о том, чтобы разрешить подключение множества функций, одновременно импортируемых из одной и той же DLL. Как видно из названия этой функции (HookimportedFunctionsByName), она подключает только функции, импортируемые по имени. В главе 14 обсуждается подключение функций, импортируемых по порядковому номеру (которое используется в утилите LIMODS).

    Таблица 12.3. Описания параметров функции HookimportedFunctionsByName

    Параметр

    Описание

    hModule

    Модуль, в котором будет подключен импорт

    szImportMod

    Имя модуля, чьи функции импортируются

    Count

    Количество подключаемых функций. Этот параметр указывает размер массивов paHookArray И paOrigFuncs

    paHookArray

    Массив структур дескрипторов функций, который перечисляет, какие функции нужно подключать. Массив не должен быть упорядочен по szFunc-именам (хотя разумно хранить массив отсортированным в порядке имен функций, потому что в будущем можно было бы лучше организовать поиск). Кроме того, если конкретный рРгос-указатель равен NULL (пустой), то HookimportedFunctionsByName пропускает этот элемент. Структура каждого элемента в paHookArray проста: имя подключаемой функции и указатель на процедуру нового подключения. Поскольку у вас может появиться желание подключать или не подключать функции, HookimportedFunctionsByName возвращает все исходные адреса импортируемых функций

    paOrigFuncs

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

    NULL

    pdwHooked

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




    ЛИСТИНГ 12-2. Функция HookImportedFunctionsByNaine из файла

    HOOKIMPORTEDFUNCTIONSBYNAME.CPP .

    BOOL BUGSUTIL_DLLINTERFACE _stdcall

    HooklmportedFunctionsByName ( HMODULE hModule ,

    LPCSTR szImportMod,

    UINT uiCount

    LPHOOKFUNCDESCA paHookArray,

    PROC * paOrigFuncs,

    LPDWORD pdwHooked )

    {

    // Проверить параметры.

    ASSERT ( FALSE == IsBadReadPtr ( hModule

    sizeof ( IMAGE_DOS_HEADER) ));

    ASSERT ( FALSE == IsBadStringPtr ( szImportMod, MAX_PATH));

    ASSERT ( 0 != uiCount);

    ASSERT ( NULL != paHookArray);

    ASSERT ( FALSE == IsBadReadPtr ( paHookArray,

    sizeof (HOOKFUNCDESC) * uiCount));

    // В отладочных построениях выполнить тщательную проверку 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);

    // If the function address isn't NULL, it is validated.

    if ( NULL != paHookArray[ i ].pProc)

    {

    ASSERT ( FALSE == IsBadCodePtr ( paHookArray[i].pProc));

    }

    }

    }

    #endif

    // Выполнить проверку ошибок параметров.

    if ( ( 0 == uiCount ) | |

    ( NULL == szIinportMod ) | |

    ( 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);

    }

    // Это системная DLL, которую Windows 98 не

    // разрешает загружать (из-за того, что адрес загрузки >2 Гбайт)?

    if ( ( FALSE == IsNT .()) && ( (DWORD)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 plmportDesc =

    GetNamedlmportDescriptor ( hModule, szImportMod);

    if ( NULL == plmportDesc)

    {

    // Затребованный модуль не был импортирован.

    // Ошибку не возвращать.

    return ( TRUE);

    }

    // Получить информацию об исходных переходниках для этого DLL.

    // Невозможно использовать информацию переходников, хранящуюся в

    // p!mportDesc->FirstThunk, т. к. загрузчик уже изменил этот массив

    // при установке всех импортов. Исходный переходник обеспечивает

    // доступ к именам функций.

    PIMAGE_THUNK_DATA pOrigThunk =

    MakePtr ( PIMAGE_THUNK_DATA

    hModule ,

    plmportDesc-XDriginalFirstThunk );

    // Получить массив p!mportDesc->FirstThunk, в котором я буду

    // выполнять подключения и всю черную работу.

    PIMAGE_THUNK_DATA pRealThunk = MakePtr { PIMAGE_THUNK_DATA

    hModule , pImportDesc->FirstThunk );

    // Цикл поиска подключаемых функций,

    while ( NULL != pOrigThunk->ul.Function)

    {

    // Искать только функции, которые импортируются по имени,

    // а не те, что импортируются по порядковому номеру.


    if ( IMAGE_ORDINAL_FLAG !=

    ( pOrigThunk->ul.Ordinal & IMAGE_ORDINAL_FLAG))

    {

    // Поиск имени данной импортируемой функции.

    PIMAGE_IMPORT_BY_NAME pByName;

    pByName = MakePtr ( PIMAGE_IMPORT_BY_NAME,

    hModule ,

    pOrigThunk->ul.AddressOfData );

    // Если имя начинается с NULL, то пропустить элемент,

    if ( '\0' == pByName->Name[ 0 ])

    {

    continue;

    }

    // Определить, подключилась ли функция

    BOOL bDoHook = FALSE;

    // Здесь можно рассмотреть возможность двоичного поиска.

    // Посмотреть, есть ли имя импортируемой функции в массиве

    // подключения. Обдумайте требования к paHookArray для

    // сортировки по именам функций, что позволило бы использо-

    // вать двоичный поиск, который увеличил бы скорость поиска.

    // Однако размер параметра uiCount, получаемого этой

    // функцией, должен быть скорее небольшим для успешного

    // поиска по всему массиву paHookArray для каждой функции,

    // импортированной модулем szImportMod.

    for ( DINT 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}

    {

    // Функция для подключения найдена. Теперь нужно

    // изменить защиту памяти на "read-write" (для записи),

    // прежде чем перезаписывать указатели функций. Заметьте,

    // что в реальную область переходников запись

    //не производится!

    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)pRealThunk->ul.Function;

    }

    // Microsoft имеет два различных определения

    // РIМАСЕ_ТНЦМК_ОАТА-полей для будущей поддержки Win64.

    // Используем самый последний набор заголовков

    // из W2K RC2 Platform SDK, и заставим иметь с ними дело

    // заголовки из Visual C++ 6 Service Pack 3.

    // Подключить функцию.

    DWORD * pTemp = (DWORD*)&pRealThunk->ul.Function;

    *pTemp = (DWORD)(paHookArray[i].pProc);

    DWORD dwOldProtect;

    // Изменить защиту обратно к тому состоянию, которое

    // было перед переписыванием указателя функции.

    VERIFY ( VirtualProtect ( mbi_thunk.BaseAddress,

    mbi_thunk.RegionSize , mbi_thunk.Protect , SdwOldProtect )) ;

    if ( NULL != pdwHooked)

    {

    // Инкремент общего количества подключенных функций.

    *pdwHooked += 1;

    }

    }

    }

    // Инкремент обеих таблиц. pOrigThunk++; pRealThunk++;

    }

    // Все OK, Jumpmaster!

    SetLastError ( ERROR_SUCCESS);

    return ( TRUE);

    }

    Уточнить принципы работы HooklmportedFunctionsByName не так уж трудно. После выполнения обычной для практики профилактической отладки полной проверки (макросами утверждений) каждого параметра вызывается вспомогательная функция GetNamedimportDescriptor, чтобы найти структуру IMAGE_IMPORT_DESCRIPTOR для требуемого модуля. После получения указателей на исходную и реальную IAT-таблицы выполняется циклический просмотр исходной IAT-таблицы, проверяется каждая импортированная по имени функция, присутствует ли она в списке подключения paHookArray. Если функция находится в этом списке, то устанавливается атрибут доступа PAGE_READWRITE для памяти реальной IAT-таблицы, чтобы благополучно записать в нее адрес подключения, затем этот адрес заносится в ячейку указателя соответствующей реальной функции и защита памяти восстанавливается в ее исходное состояние.Если вы не совсем разобрались в том, что происходит, воспользуйтесь функцией блочного тестирования для пошагового выполнения HookimportedFunctionsByName (эта функция включена в исходный код BUGSLAYERUTIL.DLL на сопровождающем компакт-диске).

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

    Постоянный просмотр кода

    Если приложение действительно нуждается в мультипоточности, то приходится тратить много времени на полный просмотр его кода. Данный прием заключается в том, чтобы для подобных обзоров прикрепить по одному человеку на каждый поток и на каждый объект синхронизации в коде. Во многих отношениях обзор кода в многопоточном программировании — действительно "многопоточный" обзор.
    Просматривая код, представьте себе, что каждый поток выполняется с приоритетом реального времени на своем собственном CPU. Каждый "владелец потока" просматривает код, обращая внимание только на специфический код, который, как предполагается, его поток выполняет. Когда этот человек готов запросить объект синхронизации, "владелец объекта синхронизации" буквально ходит по пятам "владельца потока". Когда "владелец потока" освобождает этот объект, "владелец объекта синхронизации" отходит в нейтральное положение. В дополнение к представителям потока и объектов, нужно иметь несколько разработчиков, контролирующих общую поточную активность, чтобы они имели доступ к потоку программы и помогали определять точки, в которых блокируются различные потоки.
    Работая с обзором кода, имейте в виду, что операционная система имеет свои собственные объекты синхронизации, которые она применяет к вашему процессу, и эти объекты также могут вызвать блокировки. Критическая секция процесса, объясненная ниже в примечании "История отладочной войны. Блокировка не имеет смысла" данной главы, и печально известные 16-разрядные мьютексы из Windows 98 являются объектами синхронизации, которые операционная система использует в вашем процессе. Если не проявлять осторожности, то причиной блокировок может также быть и обработка сообщений. Если поток А — это UI-поток, ожидающий критическую секцию, в настоящее время принадлежащую потоку В, и если поток В посылает сообщение HWND-объекту в поток А с помощью функции SendMessage, то произойдет блокировка. Убедитесь, что контролировали эти действия во время обзора кода.
    Мьютекс (miitex object) — специальный синхронизирующий объект в межпроцессном взаимодействии, подающий сигнал, когда он не захвачен каким-либо потоком. — Пер.



    Реализация DeadlockDetection

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


    Реализация потоков мелкими дискретными частями

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


    Рекомендации и приемы работы с многопоточностью

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


  • Синхронизация потоков на самом низком уровне

    Помещайте методы синхронизации на самом низком уровне кода. Если в программе нужна критическая секция для защиты части данных, то необходимо помещать функции EnterCriticalSection И LeaveCriticalSection лишь вокруг данных фактического доступа. Это размещение гарантирует, что действительно защищается только тот элемент, который требуется защищать, и ничего более. Ограничение области видимости объектов синхронизации — лучшая защита от случайных блокировок. Одной из самых неприятных блокировок, с которыми я когда-либо встречался, был захват синхронизации объекта двумя указанными выше функциями.


    Тестирование на мультипроцессорных машинах

    Многопоточное приложение предъявляет к тестированию намного более высокие требования, чем однопоточное. Самая важная рекомендация для тестирования многопоточного приложения — необходимо выполнять тестирование на мультипроцессорных системах. При этом не имеется в виду простое выполнение приложения по частям; речь идет о непрерывном тестировании программы во всех возможных сценариях. Даже если приложение отлично выполняется на однопроцессорных машинах, мультипроцессорная машина выявляет такие блокировки, которые никогда не казались возможными.
    Лучший подход к этому виду тестирования состоит в том, чтобы иметь в команде специальных разработчиков, выполняющих приложение на мультипроцессорных машинах каждый день. Если вы менеджер и не имеете никакой мультипроцессорной машины в своем распоряжении, прекращайте чтение прямо сейчас и немедленно снабдите половину разработчиков и QA-испытателей мультипроцессорными машинами! Если вы — разработчик без мультипроцессорной машины, покажите эту главу своему менеджеру и потребуйте для работы надлежащее оборудование!


    Требования утилиты DeadlockDetection

    Обратите внимание, что в предыдущем разделе не высказывалось никаких предположений относительно того, что нужно делать, когда неожиданная блокировка парализует вашу программу. Предложены были скорее профилактические меры, с помощью которых можно попробовать, в первую очередь, избежать блокировок, а не предписания для их исправления при возникновении. В этом разделе показано, что исправление блокировок задача нелегкая, даже с учетом применения отладчика, и что почти всегда нужна некоторая дополнительная помощь. Такую помощь и предоставляет утилита DeadlockDetection.
    Вот список основных требований, учтенных при разработке DeadlockDetection:
    1. Утилита должна точно показывать, где в коде пользователя происходит блокировка. Инструмент, который только сообщает, что функция EnterCriticaiSection блокирована, не очень помогает. Действительно эффективный инструмент должен возвращаться обратно к адресу, и, следовательно, к исходному файлу и номеру строки, где произошла блокировка, чтобы имелась возможность ее быстро исправить.
    2. Утилита должна показывать, какой объект синхронизации стал причиной блокировки.
    3. Утилита должна показывать, какая Windows-функция блокирована и какие параметры ей переданы. Это помогает увидеть как значения тайм-аута, так и значения параметров, переданных в функцию.
    4. Утилита должна определить, какой поток вызвал блокировку.
    5. Утилита должна быть достаточно "легковесной", т. е. как можно меньше вмешиваться в работу программы пользователя.
    6. Обработка вывода данных, собранных утилитой, должна быть расширяемой. Информация, собранная в системе обнаружения блокировки, может быть обработана многими способами, и утилита должна позволять другим разработчикам (не только вам) расширять эту информацию.
    7. Утилита должна легко объединяться с программами пользователя.
    Имейте в виду, что утилиты типа DeadlockDetection определенно воздействуют на поведение приложения, которое они наблюдают. Утилита DeadlockDetection сама может привести к блокировкам ваших программ, потому что работа, которую она делает, чтобы собирать информацию, замедляет выполнение потоков.
    Я определил это поведение чуть ли не как свойство, потому что как только вы вызываете блокировку кода, считайте, что вы уже идентифицировали ошибку, а это — первый шаг к ее исправлению. Кроме того, всегда лучше самому находить ошибки, чем ждать, пока это сделают заказчики.

    История отладочной войны Блокировка

    Сражение

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

    Продукт, над которым они работали, имел интересную архитектуру и был в значительной степени многопоточным. Блокировка, с которой они столкнулись, происходила только в определенное время, и это всегда случалось в середине последовательности загрузок из библиотеки динамической компоновки (DLL). Программа попадала в блокировку, когда вызывалась функция WaitForSingleObject, проверяющая способность потока создавать некоторые разделяемые объекты.

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

    Результат

    Всегда с удовольствием вспоминаю эту ситуацию, потому что это был один из тех немногих случаев, когда уже через 5 минут после запуска отладчика я стал похож на героя. Как только команда дублировала блокировку, я быстро взглянул на окно Call Stack и заметил, что программа ожидала на дескрипторе потока внутри функции DllMain. Когда загружается некоторая DLL, эта функция, являясь частью архитектуры DLL, стартует другой поток и затем немедленно вызывает функцию WaitForSingleObject из объекта события подтверждения приема, чтобы гарантировать, что порожденный поток способен должным образом инициализировать некоторые важные разделяемые объекты перед продолжением остальной части обработки в DllMain.

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


    Операционная система использует эту секцию, чтобы синхронизировать различные действия, которые случаются "за сценой" процесса. Одной из ситуаций, в которых используется критическая секция, является сериализация1выполнения DllMain для следующих четырех случаев ее вызова: DLL_PROCESS_ATTACH (присоединение DLL-процесса), DLL_THREAD_ATTACH (присоединение DLL-потока), DLL_THREAD_ DETACH (отсоединение DLL-потока) и DLL_PROCESS_DETACH (отсоединение DLL-процесса). Причину обращения к DliMain указывает ее второй параметр.

    Сериализация — преобразование в последовательную форму. — Пер.

    В приложении, над которым работала команда, запрос к LoadLibrary заставил операционную систему захватить критическую секцию процесса для того, чтобы вызывать DliMain для случая DLL_PROCESS_ATTACH. Затем функция DliMain порождала второй поток. Всякий раз, когда процесс порождает новый поток, операционная система захватывает критическую секцию процесса так, чтобы она могла вызывать функцию DliMain каждой загружаемой DLL для случая DLLJTHREAEKATTACH. В этой конкретной программе второй поток блокировался, потому что критическую секцию процесса содержал первый поток. К сожалению, первый поток затем вызывал функцию WaitForSingleObject, чтобы гарантировать, что второй поток способен должным образом инициализировать некоторые разделяемые объекты. Поскольку второй поток был блокирован на критической секции процесса, удерживаемой первым потоком, а первый поток блокирован при ожидании второго потока, результатом была обычная взаимоблокировка.

    Урок

    Очевидный урок таков: необходимо избегать любых вызовов ожидающих (Wait*-) функций внутри DliMain. Однако проблемы с критической секцией процесса касаются не только Юа11:*-функций. Операционная система использует критическую секцию процесса и при вызове других фунций (CreateProcess, GetModuleFileName, GetProcAddress, LoadLibrary И FreeLibrary), так ЧТО не нужно вызывать любую из этих функций в DliMain. Поскольку DliMain обзаводится критической секцией процесса, то она может одновременно выполнять только один поток.

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

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

    Бич блочного тестирования: интерфейсы пользователя

    Я твердо убежден, что разработчики Microsoft получают туннельный синдром запястья не от того, что им приходится вводить исходный код с клавиатуры, а от многократного нажатия одних и тех же комбинаций клавиш при тестировании своих приложений. После 5000-го нажатия +, <О> запястья зажаты плотнее, чем арматура в бетоне. Без инструмента автоматизации задач, имеющих доступ к различным свойствам ваших приложений, вообще приходится следовать некоторому сценарию, чтобы гарантировать выполнение блочного тестирования в достаточном объеме. Тестирование со сценариями — чрезвычайно скучная процедура, которая оставляет множество лазеек для человеческих ошибок.
    Автоматизация блочных тестов означает уменьшение количества ввода с клавиатуры и уменьшение затрат времени на проверку состояния кода. К сожалению, приложение Recorder, которое обычно поставлялось с Windows 3.x, не включается в состав 32-разрядных операционных систем. Recorder обеспечивал запись действий пользователя (с мышью и клавиатурой) и последующее их воспроизведение, как если бы они были событиями физической мыши и клавиатуры (правда на его возможности воспроизведения перемещений мыши были наложены очень неудобные ограничения). Сейчас доступны продукты независимых поставщиков, предназначенные для автоматизации приложений и других работ (например, полной проверки каждого пиксела при сравнении экранов, поддержки служебных баз данных, связанных с тестированием, и т. д.), но требовалось создать нечто более удобное и предназначенное специально для инженеров-разработчиков. Так родилась идея этого приложения.
    Когда я решил создать автоматизированную утилиту, то потратил некоторое время на уточнение того, что же можно ожидать от такого инструмента. Прежде всего, я подумал о разработке утилиты, похожей на старое приложение Recorder. В далекие времена Windows 3.x имелся полный набор REC-файлов для проведения тестирования. Однако у этого приложения был большой недостаток — оно не могло выполнять условные тесты.
    Если во время тестирования приложение сообщало об ошибке, то Recorder шел по самому легкому пути — он просто воспроизводил зарегистрированные нажатия клавиш и щелчки мыши, полностью забывая о бедствии приложения. Однажды я стер половину операционной системы, потому что тестировал расширение WINFILE.EXE, и когда в нем возникли ошибки, Recorder выполнил последовательность удаления файлов для всего каталога \System. Новый автоматизированный инструмент определенно должен поддерживать конструкцию

    if. . .then. . .else.

    Чтобы включить в тесты условные конструкции, необходимо было использовать некоторый вид языка. Разработка собственного языка тестирования была бы увлекательным интеллектуальным упражнением, но вскоре я решил, что больше заинтересован в написании полезного инструмента отладки, чем в проектировании языка и возне с компиляторами YACC и FLEX. Потребовалось всего две секунды, чтобы понять, что надо написать Tester как СОМ-объект — таким образом, разработчики смогут использовать эту утилиту, а для написания тестов выбрать любой язык; я при этом мог сконцентрироваться на программировании свойств регрессивного тестирования (regression-testing) утилиты вместо проектирования нового языка. В качестве языков тестирования были выбраны языки сценариев типа Microsoft Visual Basic Scripting Edition (VBScript) и Java Script (JScript), потому что сценарии тестирования не требуют компиляции. Однако различные реализации машин сценариев Scripting Host (WSH) имеют несколько ограничений, которые будут рассмотрены чуть позже. Пока же поговорим о требованиях, которые привели меня к созданию утилиты Tester.

    Что делать дальше с утилитой Tester?

    Как уже говорилось, Tester делает хорошо только одну вещь: воспроизводит нажатия клавиш. Как и для всех утилит в этой книге, мы приветствуем поиск путей усовершенствований Tester'a, если вы, конечно, имеете склонность к этому. Вот некоторые возможные усовершенствования, которые можно попытаться реализовать:
  • добавьте классы-оболочки, такие как TListBox, TTreeControl и TRadioButton, чтобы получить возможность проверять различные состояния и содержимое элементов управления (чтобы они содержали надлежащие данные). С помощью этих классов можно начать проверку элементов управления и составление более сложных сценариев;
  • добавьте приложение, записывающее нажатия клавиш (recorder application). Попытайтесь сделать его расширяемым, чтобы при добавлении дополнительных классов-оболочек можно было с его помощью генерировать улучшенные сценарии;
  • исследуйте способы добавления к Tester'y ввода мыши. Можно было бы попытаться записывать расположение указателя мыши в процентных долях его смещения от краев экрана. Единственная проблема здесь состоит в том, что, изменяя разрешение экрана, следует пропускать элемент управления, на котором выполняется щелчок. Другая идея состоит в том, что нужно сначала добавить все классы-оболочки и затем использовать только метод click для щелчков на классах-оболочках. Тонкость здесь состоит в том, что нужно гарантировать, что все типы окон будут заключены в оболочки специфических элементов управления утилиты Tester.



  • Применение утилиты Tester

    Использовать Tester довольно просто. Следует создать пару Tester-объектов, стартовать или отыскать главное окно приложения, выполнить для него несколько нажатий клавиш, проверить результаты и закончить работу. Листинг 13-1 содержит пример VBScript-теста, который запускает стандартную программу Блокнот (NOTEPAD.EXE), вводит несколько строк текста и закрывает ее.

    Листинг 13-1. Использование общих Tester-объектов
    ' Минимальный пример работы с VBScript Tester. В нем просто запускается
    ' Блокнот, вводится несколько строк текста и Блокнот закрывается.
    ' Создать объекты системы и ввода.
    Dim tSystem
    Dim tInput
    Dim twin
    Set tSystem = "Script.CreateObject ( "Tester.TSystem")
    Set tInput = WScript.CreateObject ( "Tester.TInput")
    'Запустить Блокнот.
    tSystem.Execute "NOTEPAD.EXE"
    ' Ждать 200 миллисекунд.
    tSystem.Pause 200
    ' Попытка найти главное окно программы Блокнот.
    Set twin = tSystem.FindTopWindowByTitle ( "Untitled - Notepad")
    If ( twin Is Nothing) Then
    MsgBox "Unable to find Notepad!"
    WScript.Quit
    End If
    ' Убедиться, что Блокнот выполняется в фоновом режиме.
    twin.SetForegroundTWindow
    ' Напечатать (ввести) первую строку.
    tlnput.PlayKeys "Be all you can be!~ ~ ~"
    ' Повторите ввод.
    tlnput.PlayKeys "Put on your boots and parachutes....~ ~ ~"
    ' Ввести третью строку.
    tlnput.PlayKeys "Silver wings upon their chests.....~ ~ ~"
    ' Подождать 1 секунду.
    tSystem.Pause 1000
    ' Закончить Блокнот.
    tlnput.PlayKeys "%FX"
    tSystem.Pause 50
    tlnput.PlayKeys "{TAB}~"
    ' Сценарий выполнен!
    В листинге 13-1 показано три объекта, которые Tester использует наиболее часто. Объект TSystem позволяет находить родительские окна, запускает приложения и приостанавливает тестирование. Объект TWindow, который возвращается функцией FindTopWindowByritie в листинге 13-1, является главной "рабочей лошадкой". Это — оболочка вокруг объекта HWND (дескриптора окна), содержащего полный набор свойств окна.
    Дополнительно, TWindow позволяет перечислить все дочерние окна, которые принадлежат конкретному родителю. Последний объект в этом листинге — Tlnput, который поддерживает единственный метод PlayKeys, направляющий клавишные команды окну, имеющему фокус.

    В листинге 13-2 показан объект TNotify, используемый в VBScript-тесте. При разработке сценариев автоматизации один из наиболее трудных случаев для обработки — когда неожиданно раскрывается окно, например, панель сообщений от утверждений. Объект TNotify делает его моментальный снимок, обеспечивая тем самым, аварийный обработчик для таких событий. Простой сценарий, приведенный в листинге 13-2, отыскивает окна с заголовком "Блокнот".

    Листинг 13-2. Использование TNotify в VBScript

    ' VBScript-тест для отображения обработчиков оконных уведомлений

    ' Константы для подпрограммы TNotify.AddNotification . Если бы я

    ' использовал Visual Basic 6, то определил бы здесь константы типа enum .

    Const antDestroyWindow = 1

    Const antCreateWindow = 2

    Const antCreateAndDestroy = 3

    Const ansExactMatch = 0

    Const ansBeginMatch = 1

    Const ansAnyLocMatch = 2

    ' Создать объекты tSystem и tInput.

    Dim tSystem

    Dim tInput

    Set tSystem = WScript.CreateObject ( "Tester.TSystem")

    Set tlnput = WScript.CreateObject ( "Tester.Tlnput")

    ' Переменная объекта TNotify

    Dim Notifier

    ' Создать объект TNotify.

    Set Notifier =

    WScript.CreateObject ( "Tester.TNotify" ,' _

    "NotepadNotification_" )

    ' Добавить нужные уведомления. В этой демонстрации используются два

    ' уведомления — window destroy (ликвидация окна) и

    'window create (создание окна). Все возможные комбинации уведомлений

    'см. исходный код TNotify .

    Notifier.AddNotification antCreateAndDestroy, _

    ansAnyLocMatch ,

    _ "Notepad"

    ' Запуск программы Блокнот.

    tSystem.Execute "NOTEPAD.EXE"

    ' Ожидать полсекунды.

    tSystem.Pause 500

    ' Из-за того что Visual Basic не является потокобезопасным языком,


    ' Because Visual Basic isn't thread-safe

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

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

    ' в единственном потоке. Эта функция позволяет вручную проверять

    ' состояния window create и window destroy.

    Notifier.CheckNotification

    ' Панель сообщений в процедуре событий NotepadNotification_CreateWindow

    ' блокирована, поэтому код закрытия Блокнота не будет выполняться до тех

    'пор, пока панель сообщений не будет очищена,

    tInput.PlayKeys "%FX"

    tSystem.Pause 50

    tlnput.PlayKeys "{TAB}-"

    ' Снова проверить уведомление.

    Notifier.CheckNotification

    ' Дать TNotify шанс перехватить сообщение о ликвидации окна.

    tSystem.Pause 100

    ' Отсоединить уведомления. Если вы не сделаете этого в WSH, деструктор

    ' класса никогда не получит вызова, так что уведомление останется

    ' активным в таблице уведомлений.

    WScript.DisconnectObject Notifier

    Set Notifier = Nothing

    Sub NotepadNotificationCreateWindow ( twin)

    MsgBox ( "Notepad was created!")

    End Sub

    Sub NotepadNotificationDestroyWindow ()

    MsgBox ( "Notepad has gone away....")

    End Sub

    Время от времени необходимо вызывать метод TNotify checkNotification. (причины изложены чуть позже, в разделе "Реализация утилиты Tester" данной главы). Периодический вызов метода checkNotification гарантирует, что сообщения уведомлений могут проходить, несмотря на то, что в языке, который вы выбрали, может отсутствовать цикл сообщений. Код листинга 13-2 показывает, как нужно использовать панель сообщений в процедурах уведомлений о событиях, хотя применение панели сообщений в реальных сценариях нежелательно, потому что это может вызвать проблемы, неожиданно изменяя окно, обладающее фокусом.

    Имейте также в виду, что количество уведомлений ограничено пятью, поэтому нельзя применять TNotify для общих задач сценария, таких как ожидание появления диалогового окна File Save.


    TNotify следует использовать только для неожидаемых окон. В зависимости от того, как установлены обработчики уведомлений и как они отыскивают указанный текст в заголовке окна, можно получать уведомления и для таких окон, которые вас, возможно, не интересуют. Наиболее вероятно получение нежелательных уведомлений, когда используется родовая строка, такая как "Блокнот", и указано, что строка может появляться в любом месте заголовка окна. Чтобы избежать нежелательных уведомлений, следует при вызове метода TNotify AddNotification как можно точнее специфицировать уведомления. Процедуры обработки событий CreateWindow также должны просматривать передаваемые им TWindow-объекты, чтобы можно было проверить, что это нужное вам окно. Для процедур обработки событий DestroyWindow, которые обрабатывают родовые уведомления, следует просматривать открытые окна, чтобы гарантировать, что окно, которое вам больше не нужно, не существует.

    Речь, по-видимому, идет об имени основного, родительского окна приложения, способного порождать дочерние окна. — Пер.

    Наряду с исходным кодом на сопровождающем компакт-диске вы найдете два других примера использования утилиты Tester. Первый пример, NPAD_TEST.VBS — это более полный VBScript-тест, который включает несколько повторно используемых подпрограмм. Второй — ТТ (или Tester Tester'a) — является главным блочным тестом программы Tester, и вы можете получить доступ к нему с помощью группового файла проекта. TESTER.VBG.TT — это VB-приложение, из которого можно почерпнуть основную идею применения Tester'a в программировании на Visual Basic. Дополнительно, в этих двух примерах показан объект TWindows, который является коллекцией, содержащей объекты TWindow.

    Хотя я частично применяю VBScript для блочного тестирования, я понимаю, что правильная его работа весьма сомнительна. VBScript-переменные не типизированы и для VBScript нет редактора, аналогичного редактору Visual Basic, так что при отладке приходится возвращается к старому методу проб и ошибок.


    Главная причина привлекательности языка VBScript заключается в том, что не надо компилировать тесты. Если бы вы работали в настраиваемом отладочном окружении, позволяющем легко добавлять двоичные файлы к главному приложению, то рассмотреть и применение языка Visual Basic, благодаря чему можно было бы строить тесты аналогично тому, как вы строите свое приложение. Конечно, утилита Tester не ограничивает разработчика самыми легкими языками, если вам больше нравится язык С или макроассемблер (MASM), то вы вполне можете работать с ними вместо VBScript.

    Работа с объектами в Tester'e довольно проста, а серьезное внимание следует уделить планированию тестов, добиваясь их максимальной простоты и целенаправленности. Не пытайтесь возложить на них слишком много работы. Желательно, чтобы каждый сценарий тестировал отдельную операцию. Можно, например, ограничить сценарий посылкой клавиатурной последовательности в открытый файл. Старайтесь обеспечить повторное использование сценариев. Так, сценарий для открытия файла можно применять в трех различных тестах: тесте работоспособности открываемого файла, тесте неработоспособности открываемого файла и тесте поврежденное™ информации в открываемом файле. Как и в нормальной разработке, следует по возможности избегать прямого включения строковых ресурсов в текст программы, а размещать их в файлах ресурсов. Это не только упростит локализацию сценария, но поможет также при изменении системы меню и клавиш быстрого вызова.

    Проектируя сценарий Tester'a, полезно задать себе вопрос: "Как проверить, что сценарий выполнен?". Лучшая идея, вероятно, состоит в том, чтобы регистрировать в сценарии состояние приложения в ключевых точках, обеспечивая автоматическое сравнение выводов последовательных прогонов. Если вы используете машину сценариев Windows (WSH-файл CSCRIPT.EXE), то можете, вызвав функцию wscript.Echo, переадресовывать вывод в файл. После того как сценарий закончит работу, можно проанализировать такой файл при помощи утилиты регистрации различий (типа WinDiff); если утилита обнаружит какие-то различия, то можно проверить правильность выполнения сценария.Имейте в виду, что следует нормализовать информацию, отбрасывая специфические детали выполнения. Например, разрабатывая приложение, которое загружает биржевые сводки, не нужно включать в вывод время последнего обновления цен.

    Как быть с отладкой сценариев Tester'a? Поскольку Tester не имеет своего собственного интегрированного отладчика, a Visual Basic — имеет, то необходимо проявлять осторожность, чтобы не остановить отладчик на вызове метода Tinput.piayKeys. Если отладчик там остановится, то нажатия клавиш пойдут, очевидно, не к тому окну. Чтобы справиться с этой потенциальной проблемой, перед каждым вызовом piayKeys я устанавливаю окно, к которому посылаю нажатия клавиш, поверх всех остальных окон (вызывая метод TWindow.SetForegroundTWindow). Таким образом, можно прерваться на вызове SetForegroundTwindow, проверить состояние приложения и после этого направлять нажатия клавиш к правильному окну.

    Реализация утилиты Tester

    Ознакомившись с основными идеями применения Tester'a, рассмотрим некоторые важные моментам его реализации. Сначала для реализации Tester'a я использовал C++ и библиотеку активных шаблонов (Active Template Library — ATL), но затем понял, что намного лучшим выбором был бы Visual Basic. Многое из того, что предполагалось реализовать в Tester'e, не представляло особой сложности, а работу надо было выполнить побыстрее. Поэтому был сделан выбор в пользу Visual Basic, хотя, как вы увидите позже, иногда приходилось прибегать к некоторым уловкам, чтобы заставить его работать.
    Первым я начал реализовывать объект класса Tinput, который является ответственным за весь ввод, посылаемый другому окну. Первоначально я думал, что обработка ввода с клавиатуры будет делом простым, — я предполагал строить оболочку вокруг VB-функции SendKeys. Этот подход прекрасно работал, когда коды отдельных клавиш посылались к Блокноту, но когда потребовалось посылать клавиши к Microsoft Outlook 2000, обнаружилось, что некоторые из них до него не доходили. Я никак не мог заставить работать SendKeys и в конце концов был вынужден реализовать собственную функцию, которую назвал PiayKeys. Раздумывая, что же следует включить в эту функцию, я заметил, что Microsoft Windows 98 и 2000 имеют изящную новую функцию — Sendinput. Эта функция также реализована в Windows NT 4, Service Pack 3 (и выше). Функция Sendinput является частью библиотеки Microsoft Active Accessibility (MSAA) и заменяет все предыдущие функции событий низкого уровня, такие как keybd_event. Функция Sendinput обрабатывает клавиатуру, мышь и события аппаратных компонентов. Она также размещает всю информацию ввода от клавиатуры или мыши в специальный входной поток (в виде непрерывного блока), гарантируя тем самым, что этот ввод не будет перемешиваться с любым посторонним пользовательским вводом. Эти функциональные возможности были особенно привлекательны для Tester'a. Быстрое тестирование показало, что функция Sendinput работала также и при посылке ввода в Outlook 2000.

    После изучения правил корректной пересылки нажатий клавиш в приложения необходимо было разработать формат клавишного ввода. Поскольку оператор Visual Basic sendKeys обеспечивает удачный формат ввода, я решил просто дублировать его для функции piayKeys и оставил все, кроме кода повторения клавиши. Функция, анализирующая код клавиши, не содержит ничего особенного, — если вы заинтересуетесь этим вопросом, то загляните в каталог SourceCode\Tester\TInputHlp на сопровождающем компакт-диске. Начав работу с объектом Tinput, я все еще намеревался писать Tester на C++ и написал весь код лексического анализа в виде С++-библиотек динамической компоновки (DLL). Метод Tinput. PiayKeys — просто VB-обо-лочка для вызова этой DLL.

    Объекты TWindow, Twindows и TSystem довольно просты и должны быть понятны после чтения их исходного кода. Эти три класса реализованы на Visual Basic и являются просто оболочками вокруг некоторых API-функций Windows. Создавая класс TNotify, я столкнулся с некоторыми интересными препятствиями. Задумавшись о том, как он будет определять, было ли окно с конкретным заголовком создано или разрушено, я не предполагал, что создать такой класс будет довольно трудно. Мало того, что работа была нелегкой, но оказалось также, что и уведомления о создании окна не могут быть сделаны "ошибкоустойчивыми" без героических усилий.

    Первая идея состояла в том, чтобы реализовать общесистемный обработчик сообщений на основе системы СВ1. Мне показалось, что в SDK-документации говорилось о том, что СВТ-обработчик сообщений является наилучшим методом для определения моментов создания и разрушения окон. Меня подхлестнула быстрая разработка примера, но скоро я наткнулся на препятствие. Когда мой обработчик получал уведомление HCBT_CREATEWND, я не смог согласованно восстановить заголовок окна. После некоторого размышления я предположил, что СВТ-обработчик, вероятно, вызывается как часть обработки сообщения WM_CREATE, и очень немногие окна к этому моменту установили свои заголовки.


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

    СВТ — computer-based training, компьютерное обучение. — Пер

    Просмотрев все остальные типы обработчиков, я включил их в свой пример. Как я и подозревал, перехват лишь сообщения WM_CREATE не позволил мне получить заголовок. Один из друзей посоветовал перехватывать сообщения WM_SETTEXT. В конечном счете, чтобы установить заголовок в строке заголовка, почти каждое окно использует сообщение WM_SETTEXT. Конечно, если в приложении создается собственный (неклиентский) рисунок и происходит обмен данными с видеопамятью, то сообщение WM_SETTEXT не годится. Было замечено еще одно интересное обстоятельство: некоторые программы, в частности браузер Microsoft Internet Explorer, много раз последовательно отправляли сообщения WM_SETTEXT с одним и тем же текстом.

    Выяснив, что нужно перехватывать сообщения WM_SETTEXT (а не WM_CREATE), я внимательнее рассмотрел различные обработчики, которые можно было использовать. В итоге выбор пал на перехват вызова оконной процедуры WH_CALLWNDPROCRET. Это позволило легко отслеживать оба сообщения — и WM_CREATE, и WM_SETTEXT. Можно также наблюдать сообщения WM_DESTROY. Сначала я ожидал некоторых неприятностей от сообщения WM_DESTROY, т. к. думал, что заголовок окна мог быть освобожден к тому моменту, когда обнаруживается это сообщение. К счастью, заголовок окна имеет силу, пока не получено сообщение WM_NCDESTROY.

    Рассмотрев все "за" и "против" обработки сообщений WM_SETTEXT лишь для окон, которые еще не имели заголовка, я решил идти только вперед и обрабатывать все WM_SЕТТЕХТ-сообщения. Альтернативой было бы написание машины состояний (state machine), чтобы сохранять след созданных окон и времени установки их заголовков. Однако казалось, что такое решение чревато ошибками и его трудно реализовать. Препятствием к обработке всех WM_SЕТТЕХT-сообщений было то обстоятельство, что можно получать многократные уведомления о создании для одного и того же окна.


    Например, если вы устанавливаете TNotify- обработчик для окон, заголовки которых содержат подстроку "Блокнот", то вы получите уведомление при запуске NOTEPAD.EXE, но вы также получали бы уведомление каждый раз, когда NOTEPAD.EXE открывало бы новый файл. В конце концов я почувствовал, что было бы лучше принять "менее-чем-оптимальную" реализацию, чем тратить много дней на отладку "правильного" решения. К тому же, написание собственно обработчика занимала лишь около четверти полной реализации окончательного TNotify-класса; другие три четверти кода должны были информировать пользователя о создании или разрушении окна.

    Я принял решение реализовать Tester на Visual Basic прежде, чем написал класс TNotify. Ранее упоминалось, что использование Tnotify, — не полностью автоматическая операция, и что время от времени необходимо вызывать метод checkNotification. Причина заключается в том, что приложение Visual Basic не может быть многопоточным, а требовалось проверять статус окна, было ли оно создано или разрушено и продолжает ли использовать тот же поток, в котором выполнялась остальная часть TESTER.DLL.

    Составив некоторое представление о механизмах уведомления, я сформулировал следующие основные требования к реализации:

  • обработчик процедуры WH_CALLWNDPROCRET должен быть общесистемным, поэтому его нужно реализовать в отдельной DLL;
  • очевидно, что Tester.DLL (т. е. сама утилита Tester) не может быть такой DLL, потому что перенос всей DLL VB-Tester'a и, в свою очередь, MSVBM60.DLL в адресное пространство каждого потока на компьютере пользователя. Это условие означает, что обрабатывающая DLL, вероятно, должна установить флаг или что-то еще, что может прочитать DLL Tester'a, чтобы знать, что условие выполнено;
  • Tester не может быть многопоточным, поэтому вся обработка должна выполняться в одном и том же потоке.
  • Первое уточнение основных требований: функция-обработчик должна быть написана на языке С. Поскольку эта функция загружается во все адресные пространства, сама DLL не может вызывать какие-либо функции из TESTER.DLL, написанной на Visual Basic.


    Следовательно, мой Visual Basic- код должен периодически проверять результаты данных, сгенерированных обработчиком.

    Те, кто разрабатывал 16-разрядные Windows-приложения, знают, что поиск способа организации любой фоновой обработки в однопоточной среде без прерываний всегда приводил к API-функции setTimer. С ее помощью можно было организовать фоновую обработку, сохраняя однопоточность приложения. Поэтому я установил уведомление таймера, как часть класса TNotify, чтобы определять, когда были созданы или разрушены окна, которые требовалось контролировать.

    Решение в виде процедуры таймера очень похоже на ответ, но, на самом деле, оно только отчасти работает в TNotify. В зависимости от длины сценария и от того, реализует ли выбранный вами язык цикл сообщений, сообщение WM_TIMER может и не проходить, и придется вызвать метод checkNotification, который проверяет также и данные обработчика. Реализуя автоматическую проверку, я пробовал устанавливать метод TSystem. Pause, чтобы вызывать функцию DoEvents по истечении указанного (в TSystem. Pause) интервала времени. К сожалению, применение DoEvents в TSystem.pause перенесло основные проблемы производительности на сценарии, поэтому мне пришлось просто просить пользователей время от времени вызывать метод CheckNotification.

    Все эти подробности реализации могут показаться довольно запутанными, но вы удивитесь, увидев, насколько компактным получился Tester. Листинг 13-3 содержит код функции-обработчика из TNOTIFYHLP.CPP. На стороне Tester'a файл TNOTIFY. BAS — это модуль, в котором постоянно хранится процедура таймера, а фактический класс реализован в файле TNOTIFY.CLS. Класс TNotify имеет пару скрытых методов и свойств, к которым модуль TNotify может обращаться, чтобы возбуждать события и определять, какие типы уведомлений хочет получать пользователь. Интересная часть кода подключения — это глобальный совместно используемый сегмент данных (.HOOKDATA), который содержит массив данных уведомлений. При просмотре кода имейте в виду, что данные уведомления глобальны, но вся остальная часть данных находится "внутри процесса".


    Листинг 13-3.TNOTIFY.СРР

    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    "Debugging Applications" (Microsoft Press)

    Copyright (c) 1997-2000 John Robbins — All rights reserved.

    Главный файл для TNotifyHlp.dll

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * /

    #include

    #include

    #include "TNotifyHlp.h"

    /*///////////////////////////////////////////////////////////////

    Определения и константы файловой области видимости

    ////////////////////////////////////////////////////////////////*/

    // Максимальное количество слотов уведомлений

    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 (void)0

    #endif

    /*//////////////////////////////////////////////////////////////

    Определение типов файловой области видимости

    //////////////////////////////////////////////////////////////*/

    // Структура для поиска индивидуального окна

    typedef struct tagJTNOTIFYITEM

    {

    // PID процесса, который создал этот процесс

    DWORD dwOwnerPID ;

    // Тип уведомления

    int iNotifyType;

    // Параметр поиска

    int iSearchType;

    // Дескриптор для создаваемого HWND-объекта

    HWND hWndCreate ;

    // Булевская переменная

    BOOL bDestroy ;

    // Строка заголовка

    TCHAR szTitle [ МАХ_РАТН ];

    } TNOTIFYITEM, * PTNOTIFYITEM;

    /*///////////////////////////////////////////////////////////////

    Глобальные переменные файловой области видимости

    ////////////////////////////////////////////////////////////////*/

    // Эти данные НЕ разделяются между процессами, поэтому каждый


    // процесс получает собственную копию.

    // HINSTANCE- объект данного модуля. Установка глобальных обработчиков

    // системы требует DLL.

    static HINSTANCE gjnlnst = NULL;

    // Мьютекс, который защищает таблицу g_NotifyData

    static HANDLE g_hMutex = NULL;

    // Обработчик перехвата. Этот дескриптор не сохраняется в разделяемой

    // секции, т. к. множественные экземпляры могут устанавливать обработчик

    // при выполнении множественных сценариев.

    static HHOOK g_hHook = NULL;

    // Количество элементов, добавляемых этим процессом,

    // позволяет определить, как следует обрабатывать подключение,

    static int g_iThisProcess!tems = 0;

    /*/////////////////////////////////////////////////////////////////

    Прототипы файловой области видимости

    ////////////////////////////////////////////////////////////////*/

    // Наш обработчик

    LRESULT CALLBACK CallWndRetProcHook ( int nCode ,

    WPARAM wParam,

    LPARAM IParam );

    // Внутренняя проверочная функция

    static LONG _stdcall CheckNotifyltem ( HANDLE hltem, 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 hlnst ,

    DWORD dwReason ,


    LPVOID /*lpReserved*/)

    {

    #ifdef _DEBUG

    BOOL bCHRet;

    #endif

    BOOL bRet = TRUE;

    switch ( dwReason)

    {

    case DLL_PROCESS_ATTACH :

    // Установить экземпляр глобального модуля.

    g_hlnst = hlnst;

    // Нам не нужны поточные уведомления.

    DisableThreadLibraryCalls ( g_hlnst);

    // Создать мьютекс для этого процесса. Здесь мьютекс создан,

    //но еще не присвоен.

    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 :

    // Проверить, имеет ли этот процесс какие-то элементы в

    // массиве уведомлений. Если имеет, удалить их, чтобы

    // избежать образования потерянных (orphan) элементов.

    if (0 != g_iThisProcess!tems)

    {

    DWORD dwProcID = GetCurrentProcessId ();

    // Здесь не нужно захватывать мьютекс, потому что

    // при наличии сообщения DLL_PROCESS_DETACH всегда будет

    // вызываться только одиночный поток.

    // Цикл по таблице уведомлений.

    for ( int 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 =

    #endif

    CloseHandle ( g_hMutex);

    #ifdef _DEBUG

    if ( FALSE == bCHRet)

    {

    TRACE ( "!!!!!!!!!!!!!!!!!!!!!!!!\n");

    TRACE { "CloseHandle(gJiMutex) "

    "failed!!!!!!!!!!!!!!!!!!\n");

    TRACE ( "!!!!!!!!!!!!!!!!!!!!!!!!\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 ( "AddNotify Title : iNotifyType is out of range!\n");

    return ( INVALID_HANDLE_VALUE);

    }

    // Убедитесь, что диапазон типов поиска корректен,

    if ( ( iSearchType < ANTS_EXACTMATCH ) ||

    ( iSearchType > ANTS_ANYLOCMATCH) )

    {

    TRACE ( "AddNotify Title : iSearchType is out of range!\n");

    return ( INVALID_HANDLE_VALUE);

    }

    // Убедитесь, что строка правильная,

    if ( TRUE == IsBadStringPtr ( szString, MAX_PATH))

    {

    TRACE ( "AddNotify Title : szString is invalid!\n");

    return ( INVALID_HANDLE_VALUE);

    }

    // Ждать получения мьютекса.

    DWORD dwRet = WaitForSingleObject ( g_hMutex, k_WAITLIMIT);

    if ( WAIT_TIMEOUT == dwRet)

    {

    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 i = 0; i < TOTAL_NOTIFY_SLOTS; i++)

    {

    if ( _T ( '\0') == g_shared_NotifyData[ i ].szTitle[ 0 ])

    {

    break;

    }

    }

    // Добавить эти данные.

    g_shared_NotifyData[ i ].dwOwnerPID = GetCurrentProcessId ();

    g_shared_NotifyData[ i ].iNotifyType = iNotifyType;

    g__shared_NotifyData[ i ].iSearchType = iSearchType;

    Istrcpy ( g_shared__Notif yData [ i J.szTitle, szString);

    // Увеличить главный счетчик

    . g_shared_iUsedSlots++;

    // Увеличить счетчик этого процесса.

    g_iThis Process Items++;

    TRACE ( "AddNotifyTitle - Added a new item!\n");

    ReleaseMutex ( g_hMutex);


    // Если это первый запрос уведомления, активизировать подключение.

    if ( NULL = g_hHook)

    {

    g_hHook = SetWindowsHookEx ( WH_CALLWNDPROCRET ,

    CallWndRetProcHook, g_hlnst , 0 );

    #ifdef _DEBUG

    if ( NULL == g_hHook)

    {

    char szBuff[ 50 ];

    wsprintf ( szBuff,

    _T ( "SetWindowsHookEx failed!!!! (Ox%08X)\n"), GetLastError ());

    TRACE ( szBuff);

    }

    #endif

    }

    return ( (HANDLE)!);

    }

    void TNOTIFYHLP_DLLINTERFACE _stdcall

    RemoveNotifyTitle ( HANDLE hltem)

    {

    // Проверить значение.

    int i = (int)hltem;

    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 "

    "no notification handles are set!\n"));

    ReleaseMutex ( g_hMutex);

    return;

    }

    // Перед удалением чего-то, удостоверьтесь, что этот индекс указывает

    // на вход NotifyData, который содержит правильное значение. Без

    // проверки можно вызывать эту функцию с тем же значением

    // несколько раз, что запортит индексы используемых слотов.

    if ( 0 == g_shared_NotifyData[ i ].dwOwnerPID)

    {

    TRACE ( "RemoveNotifyTitle : Attempting to double remove!\n");

    ReleaseMutex ( g_hMutex);

    return;

    }

    // Удалить этот элемент из массива.

    g_shared_NotifyData[ i ].dwOwnerPID =0;

    g_shared_NotifyData[ i ].iNotifyType = 0;

    g_share.d_NotifyData [ i ] .hWndCreate = NULL;

    g_shared_NotifyData[ i ].bDestroy = FALSE;

    g_shared_NotifyData[ i ].iSearchType = 0;

    g_shared_NotifyData[ i ].szTitle[ 0 ] = _T ( '\0');

    // Декремент главного счетчика элементов.


    g_shared_iUsedSlots- -;

    // Декремент счетчика элементов этого процесса.

    g_iThisProcessItems- -;

    TRACE ( _Т ( "RemoveNotifyTitle - Removed an item!\n"));

    ReleaseMutex ( g_hMutex);

    // Если это последний элемент данного процесса, завершить

    // его обработку.

    if ( ( 0 == g_iThisProcess!tems) && ( NULL != g_hHook))

    {

    if ( FALSE = UnhookWindowsHookEx ( g_hHook))

    {

    TRACE ( _T ( "UnhookWindowsHookEx failed!\n"));

    }

    g_hHook = NULL;

    }

    } HWND TNOTIFYHLP_DLLINTERFACE _stdcall

    CheckNotifyCreateTitle ( HANDLE hltem)

    {

    return ( (HWND)CheckNotifyltem ( hltem, TRUE));

    }

    BOOL TNOTIFYHLP_DLLINTERFACE _stdcall

    CheckNotifyDestroyTitle ( HANDLE hltem)

    {

    return ( (BOOL)CheckNotifyltem ( hltem, FALSE));

    }

    /*///////////////////////////////////////////////////////////////

    ЗДЕСЬ НАЧИНАЕТСЯ ВНУТРЕННЯЯ РЕАЛИЗАЦИЯ

    ////////////////////////////////////////////////////////////////*/

    static LONG _stdcall CheckNotifyltem { HANDLE hltem, BOOL bCreate)

    {

    // Проверить значение.

    int i = (int)hltem;

    if ( ( i < 0) || ( i > TOTAL_NOTIFY_SLOTS))

    {

    TRACE ( _T ( "CheckNotifyltem : Invalid handle!\n"));

    return ( NULL);

    }

    LONG IRet = 0;

    // Получить мьютекс.

    DWORD dwRet = WaitForSingleObject ( g_hMutex, k_WAITLIMIT);

    if ( WAIT_TIMEOUT == dwRet)

    {

    TRACE ( _T ( "CheckNotifyltem : 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)

    {

    IRet = (LONG)g_shared_NotifyData[ i ].hWndCreate;

    g_shared_NotifyData[ i ].hWndCreate = NULL;

    }

    }

    else

    {

    if ( FALSE != g_shared_NotifyData[ i ].bDestroy)


    {

    IRet = TRUE;

    g_shared_NotifyData[ i ].bDestroy = FALSE;

    }

    }

    ReleaseMutex ( g_hMutex);

    return ( IRet);

    }

    static void _stdcall CheckTableMatch ( int iNotifyType,

    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- ( ( _Т ( '\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 = Istrcmp ( g_shared_NotifyData[i].szTitle,

    szTitle ))

    {

    bMatch = TRUE;

    }

    break;

    case ANTS_BEGINMATCH :

    if ( 0 ==

    _tcsnccmp ( g_shared_NotifyData[i].szTitle,

    szTitle , strlen(g_shared_NotifyData[i].szTitle)))

    {

    bMatch = TRUE;

    }

    break; case ANTS_ANYLOCMATCH :

    if ( NULL != _tcsstr ( szTitle

    g_shared_NotifyData[i].szTitle))

    {

    bMatch = TRUE;

    }

    break;

    default :

    TRACE ( _T ( "CheckTableMatch invalid "\ "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 IParam )

    {

    // Буфер для хранения заголовка окна

    TCHAR szBuff[ МАХ_РАТН ];

    // Всегда передавать сообщение следующему обработчику, прежде чем

    // приниматься за какую-нибудь обработку.

    LRESULT IRet = CallNextHookEx ( gJnHook, nCode, wParam, IParam);

    // The docs say never to mess around with a negative code, so I don't.

    // Документация просит никогда не оставлять без внимания

    // отрицательные значения, вот я и не оставляю.

    if ( nCode < 0)

    {

    return ( IRet);

    }

    // Получить структуру сообщения. Почему там три (или больше)

    // различных структуры сообщений? Где ошибка в

    // последовательном использовании запаса ole-сообщений для всех

    // обработчиков сообщений/процессов?

    PCWPRETSTRUCT pMsg = (PCWPRETSTRUCT)IParam;

    // Нет заголовка, нет работы

    LONG IStyle = GetWindowLong ( pMsg->hwnd, GWL_STYLE);

    if ( WS_CAPTION != ( IStyle & WS_CAPTION)) {

    return ( IRet);

    }

    // Сообщения WM_DESTROY прекрасно подходят и для диалоговых,

    // и для нормальных окон. Только получите заголовок и проверьте

    // согласование.

    if ( WM_DESTROY == pMsg->message)

    {

    if (0 != GetWindowText ( pMsg->hwnd, szBuff, MAX_PATH))

    {

    CheckTableMatch ( ANTN_DESTROYWINDOW, pMsg->hwnd, szBuff);

    }

    return ( IRet);

    }

    // Создание окна сложнее, чем его уничтожение.

    // Получить класс окна. Если это на самом деле диалоговое окно,

    //то нам нужно только сообщение WM_INITDIALOG.

    if ( 0 == GetClassName ( pMsg->hwnd, szBuff, MAX_PATH))

    {

    #iifdef _DEBUG

    TCHAR szBuff[ 50 ];

    wsprintf ( szBuff ,

    _T ( "GetClassName failed for HWND : 0x%08X\n"),

    pMsg->hwnd ) ;


    TRACE ( szBuff);

    #endif

    // He так уж много точек, куда можно перейти

    return ( IRet);

    }

    if ( 0 == Istrcmpi ( szBuff, _T ( "#32770")))

    {

    // Единственное сообщение, которое нужно проверить,

    // - ЭТО WM_INITDIALOG.

    if ( WM_INITDIALOG == pMsg->message)

    {

    // Получить заголовок диалогового окна.

    if (0 != GetWindowText ( pMsg->hwnd, szBuff, MAX_PATH))

    {

    CheckTableMatch ( ANTN_CREATEWINDOW,

    pMsg->hwnd ,

    szBuff );

    }

    }

    return ( IRet);

    }

    //

    // Далее речь пойдет о настоящих диалоговых окнах.

    // Решаем, что делать с текущими окнами,

    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 ( IRet);

    }

    Хотя реализация TNotify была довольно трудноразрешимой задачей, я все-таки остался доволен, что при этом почти не испытал неприятностей. Если собираетесь расширить код обработчика, то уже знаете, что отладка общесистемных обработчиков — непростая задача.


    Для ее выполнения вполне годится отладчик Microsoft Visual C++, но я никогда не пытался этого делать. Я использую только отладчик SoftlCE. Существует еще один способ отладки общесистемных подключений. Чтобы воспользоваться им, следует обратиться к так называемому printf-стилю отладки. Применение утилиты DBGVIEW позволяет наблюдать все вызовы функции OutputDebugString и видеть, таким образом, состояние обработчика.

    Я столкнулся с одной раздражающей проблемой при разработке Tester'a, которая появлялась только в Windows 98. Весь тест-код прекрасно работал в Windows NT 4 и Windows 2000, но в Windows 98 невозможно было заполнить коллекцию TWindows. Я проверял правильность дескриптора HWND окна, передаваемого в метод Add, с помощью функции iswindow. Беглое чтение документации подсказало, что iswindow возвращает значение типа BOOL. Я ошибочно предположил, что TRUE соответствовало положительным значениям, а FALSE — отрицательным. А в условных конструкциях, я использовал выражение в форме 1 = iswindow (hWndT), которая, очевидно, не работала. Как можно предположить, различные операционные системы не возвращают одни и те же значения. Этот "маленький", но досадный промах весьма поучителен.

    Требования, предъявляемые к утилите Tester

    Главное требование заключалось в том, чтобы Tester сосредоточился на одной задаче, но выполнял ее хорошо, а именно: автоматизировал нажатия клавиш в ходе тестирования приложения, обеспечивая ускорение блочного теста. Те, кто работал с коммерческими инструментами регрессивного тестирования, несомненно знают, какую дикую гонку они могут устраивать — от простого управления окном на экране до проверки любых самых сложных и экзотических данных о наиболее тонких свойствах окна. Необходимо было сконцентрировать Tester на потребностях разработчика во время блочного тестирования максимально упростить его использование.
    Вот основные требования к утилите Tester:
    1. Ею можно управлять через любой язык, который поддерживает модель компонентных объектов (СОМ).
    2. Получив входную последовательность нажатия клавиш, в том же формате, который использует VB-функция sendKeys, Tester должен воспроизвести ее в активном окне.
    3. Tester может находить любое высокоуровневое или дочернее окно по его заголовку или классу.
    4. По заданному произвольному дескриптору HWND Tester может получить все свойства соответствующего окна.
    5. Tester должен уведомлять сценарий пользователя о создании или ликвидации конкретного окна, так чтобы сценарий мог обрабатывать состояния потенциальных ошибок или выполнять улучшенную обработку окна.
    6. Tester должен поддерживать совместное использование сценариев с любым разработчиком команды.
    7. Tester допускает расширения своего кода в любом интересующем разработчиков направлении.
    Нетрудно заметить, что в этом списке ничего не сказано о действиях с мышью. Зарегистрировать ввод мыши через журнал относительно легко, но, выбрав этот метод, не забудьте об одной маленькой, но важной детали: воспроизведение "мышиных" действий должно выполняться при том же разрешении экрана, при котором они были записаны. Не многие коллективы разработчиков могут соблюдать это требование. Если разрешение экрана при описании действий с мышью задано жестко, то совместное использование сценария становится практически невозможным. Другая проблема состоит в том, что сценарий прерывается, если переместить управление в UI даже на пиксел или два. Сценарий, записанный после того, как UI "замораживается", оказывается слишком хрупким. Фокус в том, что адекватное тестирование невозможно, пока UI "заморожен".
    Tester, вероятно, не является подходящим решением для отдела с 20 специалистами по проверке качества (Quality Assurance — QA). Я намеревался создать инструмент, пригодный для автоматизации блочного тестирования в группе, состоящей из нескольких разработчиков. Думаю, что к настоящему моменту Tester удовлетворяет данному требованию. Эта утилита применялась при разработке GUI-отладчика WDBG, рассмотренного в главе 4, и уберегла меня от тысяч нажатий клавиш — так что я могу еще двигать запястьями!


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

    Что делать дальше с LIMODS?

    Работать с очередной версией LIMODS (скажем, с версией 1.1) приятно, но всегда имеется возможность для ее усовершенствования. Посоветовал бы читателю добавить следующие свойства (и придумать совершенно новые):
  • LIMODS.EXE одновременно просматривает только один LOM-файл. Было бы очень хорошо, если бы утилита LIMODS.EXE могла поддерживать множественные LOM-файлы, используя LOP-файл (проект LIMODS), позволяющий просматривать и манипулировать LOM-файлами всего проекта в целом. Кроме того, нет необходимости усиленно работать с интерфейсом управления отметками дерева, который я использовал — с помощью множественных LOM-файлов можно найти лучший способ работы с данными проекта;
  • перед выполнением приложения в LIMODS.EXE версии 1.1 нужно указывать список файлов, предложения трассировки которых требуется увидеть, причем длина этого списка остается фиксированной во время выполнения данного приложения. Было бы намного полезнее организовать некоторого рода канал связи между выполняющимися экземплярами LIMODSDLL.DLL и LIMODS.EXE, чтобы сохранение LOM-файла автоматически обновляло все экземпляры LIMODSDLL.DLL, которые были подключены к этому модулю;
  • спроектируйте новую, более привлекательную пиктограмму для LIMODS. Мои художественные способности не простираются дальше названий двух-трех первичных цветов. Нужно, чтобы пиктограмма утилиты LIMODS.EXE сразу обращала на себя внимание и однозначно идентифицировала программу.



  • Что подключает LIMODSDLL.DLL

    Когда LIMODSDLL.DLL стартует, он перехватывает ключевые импортированные функции трассировки во всех модулях процессов. Для данной версии LIMODS этими функциями являются OutputDebugStringA и OutputDebugStringW ИЗ KERNEL32.DLL, DiagOutputA DiagOutputW ИЗ BUGSLAYERUTIL.DLL, _CrtDbgReport из MSVCRTD.DLL и AfxTrace из MFC42(U)D.DLL. Кроме того, я подключил семейство функций LoadLibrary, что позволяет получать своевременную информацию о загрузке в адресное пространство дополнительных модулей.
    Для того чтобы LIMODS могла работать с Visual Basic, потребовалось также подключить функцию GetProcAddress, чтобы обеспечить возвращение соответствующей функции, когда MSVBVM60.DLL пытается получить функцию OutputDebugStringA. О функциях подключения говорилось в главе 12 и, казалось бы, эта тема исчерпана. Однако при подключении функций DiagOutputA, DiagOutputW и AfxTrace возникают некоторые уникальные проблемы. Во-первых, эти функции имеют спецификатор вызова _cdeci (а не _stdcaii), и в главе 12 показано, как нужно их подключать. Кроме того, AfxTrace экспортируется по порядковому значению (а не по имени).


    Исключение исходных файлов из LOM-файлов

    Программа GENLIMODS.EXE обладает исключительной способностью ограничивать размер генерируемых ею LOM-файлов за счет включения в них лишь тех исходных файлов, которые содержат предложения трассировки. Конечно, было бы интересно увидеть в сгенерированном коде половину библиотеки стандартных шаблонов (Standard Template Library — STL), но эти файлы не содержат предложений трассировки, они только увеличивают размер памяти, занимаемый LIMODSDLL.DLL, и замедляют процесс генерации LOM-файлов. GENLIMODS.EXE отыскивает два файла — SYSINCL.DAT и MSVCINCL.DAT. Visual C++ использует эти файлы для исключения файлов из проверки зависимостей. Файл SYSINCL.DAT — это просто список файлов, содержащихся в каталогах \Include системы программирования (\Include и \MFC\Include). Необязательный, пользовательский файл MSVCINCL.DAT может содержать любой список файлов заголовков, для которых не планируется использовать проверку зависимостей. В дополнение к этим файлам, GENLIMODS.EXE ищет в каталогах, указанных в переменной PATH, файл LIMODSINCL.DAT, содержащий любые дополнительные файлы, которые надо исключить из LOM-файлов. Например, для того чтобы исключить из LOM-файлов файлы исполнительной библиотеки языка С, можно поместить их в файл LIMODSINCL.DAT. На сопровождающем компакт-диске в каталоге \SourceCode\LIMODS есть версия файла LIMODSINCL.DAT, которая исключает из LOM-файлов все исходные файлы исполнительной библиотеки языка С.



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

    Перед дальнейшим чтением нужно установить LIMODS (исходный код LIMODS находится на сопровождающем компакт-диске) и откомпилировать полный LIMODS-проект. После компиляции скопируйте файл параметров LIMODS (LIMODS.INI) в переменную среды %SYSTEMROOT% или каталог Windows. По умолчанию LIMODS разместит свои файлы данных (*.LOM) в том же каталоге, в котором находится каждый модуль, загруженный во время активности LIMODS. Если установить параметр LOMDirectory в секции [LIMODS] файла LIMODS.INI, то программа GENLIMODS.EXE разместит все сгенерированные LOM-файлы в одном каталоге. После установки файла параметров нужно поместить двоичные файлы LIMODSDLL.DLL, BUGSLAYERUTIL.DLL, GENLIMODS.EXE и LIMODS.EXE в каталог, который должен быть предварительно указан в переменной среды PATH.



    LOM-файлы

    Просматривая листинг 14.2, нетрудно заметить, что формат LOM-файлов, генерируемых программой GENLIMODS.EXE, в точности соответствует формату файлов *.INI. В первой секции ([Module info]) хранится главная информация того модуля, который использовался при построении LOM-файла (включая имя модуля, его базовый адрес и метку даты/времени). Когда LIMODSDLL.DLL просматривает модуль в памяти, он проверяет его по этой части LOM-файла; если метка даты/времени модуля отличается от соответствующей метки LOM-файла в LIMODSDLL.DLL, то программа GENLIMODS.EXE генерирует новый LOM-файл для этого модуля. Я сохраняю базовый адрес модуля для того, чтобы LIMODSDLL.DLL мог повторно, "на лету" вычислять диапазоны адресов (в случае перемещения модуля загрузчиком образа). LIMODSDLL.DLL также сообщит пользователю (через вызов функции outputoebugstring), что модуль был перемещен.
    Листинг 14-2. Пример LOM-файла
    [Module Info]
    DateTimeStamp=380b75e8
    BaseAddress=400000
    ModuleName=LIMODS.exe
    [Ranges]
    RangeCount=11
    Range0=0x004017D0|0x00401C8E|0 ID:\Book\SourceCode\LIMODS\About.cpp
    Rangel=0x00401EF0|0x00402313|0 ID:\Book\SourceCode\LIMODS\BigIcon.CPP
    Range2=0x00402430|0x00402A5E|0|D:\Book\SourceCode\LIMODS\LIMODS.cpp
    Range3=0x00402D60| 0x00403727111D:\Book\SourceCode\LIMODS\LIMODSDoc.cpp
    Range4=0x004044B0 0x0040480010|D:\Book\SourceCode\LIMODS\LIMODSOptions.cpp
    Range5=0x00404950I 0x00405823|0 ID:\Book\SourceCode\LIMODS\LIMODSView.cpp
    Range6=0x00405D70|0x00405DB0|0 ID:\Book\SourceCode\LIMODS\LIMODSDoc.h
    Range7=0x00406150|0x0040752110 ID:\Book\SourceCode\LIMODS\LOMFile.cpp
    Range8=0x00408D00|0x004090FF|0|D:\Book\SourceCode\LIMODS\MainFrm.cpp
    Range9=0x00409270 I 0x00409516|0 ID:\Book\SourceCode\LIMODS\OptionsDialog.cpp
    RangelO=0x0040A0A0|0x0040A140I 0 Iappmodul.cpp
    [Sources]
    Source0=0|D:\Book\SourceCode\LIMODS\About.cpp
    Sourcel=0|D:\Book\SourceCode\LIMODS\BigIcon.CPP
    Source2=0|D:\Book\SourceCode\LIMODS\LIMODS.cpp
    Source3=lID:\Book\SourceCode\LIMODS\LIMODSDoc.cpp

    Source4=0|D:\Book\SourceCode\LIMODS\LIMODSOptions.cpp

    Source5=0ID:\Book\SourceCode\LIMODS\LIMODSView.cpp

    Source6=0|D:\Book\SourceCode\LIMODS\LIMODSDoc.h

    Source7=0|D:\Book\SourceCode\LIMODS\LOMFile.cpp

    Source8=0|D:\Book\SourceCode\LIMODS\MainFrm.cpp

    Source9=0|D:\Book\SourceCode\LIMODS\OptionsDialog.cpp

    Sourcel0=0|appmodul.cpp

    SourceCount=l1

    В секции [Ranges] указаны ( в определенном формате) диапазоны адресов исходных файлов. Именно эту секцию, прежде всего, и использует LIMODSDLL.DLL, чтобы определить, какие предложения трассировки показывать и когда их показывать. Поля каждой записи этой секции расположены в следующем порядке: адрес начала диапазона, адрес конца диапазона, булевское значение (флажок) показа трассы и имя исходного файла. Секция [Sources] используется LIMODS.EXE, чтобы показать имена исходных файлов в их полной форме (с именем диска и полным путем в дереве каталогов). Первоначально формат INI-файла был выбран, чтобы облегчить исходное тестирование и скрыть его в специальном классе доступа в файлах LOMFILE.H и LOMFILE.CPP. В дальнейшем выяснилось, что производительность LIMODS.EXE вполне приемлема, поэтому я так и не перешел "к другому формату.

    Обработка _сdесl-подключений

    Как показано в главе 12, _stdcail-функции легко подключать, потому что такая функция сама чистит стек; для _cdeci-функций стеки чистит вызывающая функция.
    _stdcaii _cdeci — спецификаторы соглашений о вызовах функций в языках C/C++ (см. табл. 6.3 главы 6). — Пер
    Кроме того, функции DiagOutputA, DiagOutputW И AfxTrace, имеют параметры переменной длины, так что перехватить их намного труднее. Само подключение — такое же, как и для экспортируемых _stdcaii-функций, но обработка _cdeci-функций должна быть совсем другой. В LIMODSDLL.DLL требовалось, чтобы функция подключения захватила адрес возврата и определила, является ли он диапазоном адресов, в котором пользователь хочет видеть предложения трассировки. После проверки источника функция трассировки либо выполняется, либо игнорируется, после чего выполняется возврат в вызывающую. Для _stdcaii-функций эта обработка очень проста. Можно напрямую вызвать функцию трассировки и возвратиться прямо из функции подключения в вызывающую функцию, потому стек очищается внутри функции подключения. Для _cdeci-функций нужно вернуть стек обратно в первоначальное состояние и затем, если необходимо выполнить функцию трассировки, перейти к ней (а не вызвать ее!).
    Листинг 14-3. cdecl-функция подключения с расширенным макросом
    VOID NAKEDDEF LIMODS_DiagOutputA ( void)
    {
    // Содержит адрес возврата вызывающей функции
    DWORD_PTR dwRet;
    // Содержит сохраненный регистр ESI, поэтому отладочные построения
    // Visual C++ 6 работают. (ESI использует функцию chkesp,
    // вставляемую ключом компилятора /GZ.)
    DWORD_PTR dwESI;
    _asm PUSH EBP /* Установить стандартный кадр. */
    _asm MOV EBP, ESP
    _asm SUB ESP, _LOCAL_SIZE /* Сохранить место для локальных */
    /* переменных. */
    _asm MOV EAX, EBP /* EBP указатели на исходный стек.*/
    _asm ADD EAX, 4 /* Счетчик для PUSH EBP. */
    _asm MOV EAX, [EAX] /* Получить адрес возврата. */
    _asm MOV [dwRet], EAX /* Сохранить адрес возврата. */
    _asm MOV [dwESI], ESI /* Сохранить ESI, чтобы в отладочных*/

    /* построениях работала chkesp. */

    // Вызвать функцию, которая определяет, предназначен ли данный адрес

    // для показа. После этого вызова возвращаемое значение находится в

    // ЕАХ и затем проверяется. Возврат TRUE означает выполнение функции

    // трассировки, a FALSE — пропуск функции трассировки.

    ChecklfAddressIsOn ( dwRet);

    _asm MOV ESI, [dwESI] /* Восстановить ESI. */

    _asm ADD ESP, _LOCAL_SIZE /* Исключить область локальных

    /* переменных. */

    _asm MOV ESP, EBP /* Восстановить стандартный кадр. */

    _asm POP EBP

    // Здесь и начинается функция! Четыре предшествующих строки

    // ассемблерного кода восстанавливают стек точно до того состояния,

    //в котором он был до входа в эту функцию, поэтому теперь можно

    // перейти к функции трассировки. Функция pReadDiagOutputA содержит

    // адрес функции трассировки, который я получил во время инициализации.

    _asm TEST ЕАХ, ЕАХ /* Проверить ЕАХ на 0. */

    _asm JZ IblDiagOutputA /* Если ЕАХ содержит 0, просто

    /* выполнить возврат.*/

    _asm JMP pReadDiagOutputA /* Сделано! JUMP выполняет возврат */

    /* в вызывающую, а не в эту функцию. */

    IblDiagOutputA:

    /* Пропущенный TRACE! Просто выполнить */

    _asm RET /* возврат в вызывающую функцию. */

    }

    В листинге 14-3 показана функция подключения с расширенным макросом, которая подключает функцию DiagOutputA из BUGSLAYERUTIL.DLL. Чтобы облегчить повторное использование общих подпрограмм языка ассемблера, таких как _cdeci-код пролога, в LIMODSDLL.CPP определены несколько макросов языка ассемблера (для использования в функциях подключения). Настоятельно рекомендую читателям выполнить пошаговый проход этих макросов в окне Disassembly отладчика Visual C++, чтобы наблюдать каждую инструкцию в действии.

    Общие проблемы реализации

    Разобравшись с подключением экспорта по порядковым значениям, я больше не сталкивался со сколько-нибудь серьезными проблемами при реализации LIMODS в целом. Одно интересное свойство, реализованное в LIMODS.EXE, — это автоматическая отметка элементов управления видом дерева (см. рис. 14.1). Если установить или снять отметку корневого узла в представлении дерева (имя модуля), автоматически выполняется установка (или снятие) отметок всех дочерних узлов (исходных файлов). Для выполнения подобной работы нужно было организовать уведомление о переключении метки. Все это реализовано в файле LIMODSVIEW.CPP, который можно найти на сопровождающем компакт-диске.
    Самая большая проблема, возникшая при реализации LIMODS, была связана с библиотекой стандартных шаблонов (STL). Я допускаю, что разработчики STL умнее меня, но, тем не менее, я не ожидал что, Visual С++-код STL окажется настолько непроницаемым. Одна расшифровка ошибок компиляции заняла очень много времени. Кроме того, было чрезвычайно сложно разобраться, в какой части кода произошел отказ, или как что-то в нем работает. В соответствии с рекомендациями главы 2, я использовал предупреждения компилятора 4-го уровня и рассматривал все предупреждения как ошибки, и при компиляции кода STL был завален предупреждениями С4786 ("'identifier': identifier was truncated to '255' characters in the debug information" — '"идентификатор1: в отладочной информации был усечен до '255' символов") для любого класса из шаблонов STL, который имеет более двух символов в своем имени.
    Секрет предупреждений С4786 состоит в том, что отключать их надо через директиву #pragma warning перед включением любых файлов заголовков STL. Кроме того, методика, использующая директиву #pragma warning, работает лучше, если в главный предварительно компилированный заголовочный файл включить только STL-заголовки, а обычные предупреждения в предварительно компилированном заголовке отключите раз и навсегда. Хотя при этом и пришлось немного повозиться, но все же удалось сэкономить время, используя STL вместо реализации своих собственных возрастающих массивов и классов отображений (map classes).

    Что касается последней проблемы, с которой я столкнулся, то не могу сказать, была ли это ошибка в компиляторе или недопонимание с моей стороны. В LIMODSDLL.DLL применяется статический массив HOOKFUNCDESC, содержащий указатели реальных функций для DiagOutputA и DiagOutputw из BUGSLAYERUTIL.DLL. В функциях подключения используется указатель реальной функции вне структуры как пункт назначения для оператора перехода. Проблема заключалась в том, что ссылка на второй элемент в массиве создавала недействительную ссылку. Исходная строка встроенного ассемблера

    JMP g_stBugslayerUtilRealFuncs [0] .рРгос

    генерировала строку кода языка ассемблера

    JMP g_stBugslayerUtilRealFuncs+4h

    которая была правильной. Однако исходная строка, которая сослалась на второй элемент в структуре:

    JMP g_stBugslayerUtilRealFuncs[l].pProc

    генерировала

    JMP g_stBugslayerUtilRealFuncs+5h,

    тогда как я думал, что она должна генерировать

    JMP g_stBugslayerUtilRealFuncs+OCh

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

    JMP g_stBugslayerUtilRealFuncs[0x8].рРгос

    Это частная проблема, и она может возникнуть, только если разработчик добавит к LIMODSDLL.DLL собственные специальные функции трассировки. Добавляя такие функции, используйте в качестве примеров таблицы из BUGSLAYERUTIL.DLL.

    Определение диапазонов исходного кода

    Читатель, вероятно, не очень удивится тому, что здесь снова используется символьная машина DBGHELP.DLL (подробнее она описана в главе 4). Я полагал, что ее функции, работающие с именами исходных файлов и номерами строк исходного кода, позволят отыскивать адреса первой и последней строк рабочих кодов конкретного исходного файла (эту пару адресов я и называю диапазоном адресов). Вооружившись диапазоном адресов и методикой, которая обсуждалась в главе 12, можно было бы подключить функцию OutputDebugString и по адресу возврата определить, находится ли она в диапазоне адресов того исходного файла, из которого пользователь хочет получать предложения трассировки. Хотя этот подход теоретически довольно прост, но для его практической реализации мне пришлось изрядно потрудиться.
    Не существует специальной API-функции, которая перечисляет диапазоны адресов исходных файлов, но я полагал, что смог бы обойтись функцией перечисления символов SymEnumerateSymbols. С помощью этой функции Я хотел извлечь первый символ, затем (находясь уже внутри собственной функции перечисления символов) переместиться обратно к началу исходного файла (с помощью функции SymGetLinePrev) и затем перейти к его концу с помощью функции symGetLineNext. В случае простых тестов функция SymEnumerateSymbols работала великолепно, но когда я провел аналогичное тестирование программы GENLIMODS.EXE, то заметил, что диапазоны исходного кода, полученные с помощью SymEnumerateSymbols, не соответствовали тому, что показывал дизассемблер при тестировании GENLIMODS.EXE. Казалось, что при моей методике пропускаются целые секции исходного файла.
    Когда я вручную вычислил диапазоны, они получились похожими на перечисленные в табл. 14.1. Проблема возникала из-за того, что функции SymGetLineNext И SymGetLinePrev перечисляют только смежные (непрерывные) диапазоны. Из табл 14.1 видно, что исходные файлы со встроенными (inline) функциями находятся между первой и второй частями GENLIMODS.CPP. Я быстро понял, что это не ошибка, а скорее следствие работы компилятора.
    Непонимание наблюдалось с моей стороны: я сосредотачивал свое внимание на исходном файле, когда на самом деле нужно было в первую очередь думать о диапазонах адресов.

    Таблица 14.1. Примеры диапазонов адресов GENLIMODS.EXE

    Начало

    Конец

    Исходный файл

    0x00401900

    0x0040 1A8A

    COMMANDLINE.CPP

    0x00401000

    Ox00402F1F

    GENLIMODS.CPP

    0x00403450

    0x00403774

    RESSTRING.H

    0x00403700

    0x00403700

    GENLIMODS.H

    0x00403060

    Ox004040F9

    SYMBOLENGINE.H

    0x00404690

    Ox004046AC

    GENLIMODS.CPP

    0x00407080

    Ox0040852E

    LOMFILE.CPP

    0x00409050

    Ox0040A532

    READIGNOREFILES.CPP

    Ox0040C800

    0x00400894

    VERBOSE.CPP



    Подключение функций, экспортируемых по порядковому значению

    Должен честно сказать, что почти не поддерживал функций подключения, экспортируемых по порядковому значению, потому что подобная попытка весьма чревата ошибками (из-за того, что разные версии MFC DLL используют различные порядковые значения). Однако если абстрагироваться от проблем, связанных с версиями, то процесс подключения по порядковому значению почти идентичен подключению по имени. Сравните функцию HookordinaiExport, показанную в листинге 14-4, с функцией HookimportedFunctionsByName, рассмотренной в главе 12, и вы увидите, что обе функции выполняют много одинаковых действий.
    Листинг 14-4. Функция HookordinaiExport
    BOOL BUGSUTILJ3LLINTERFACE _stdcall
    HookordinaiExport ( HMODULE hModule ,
    LPCTSTR szImportMod,
    DWORD dwOrdinal ,
    PROC pHookFunc ,
    PROC * ppOrigAddr )
    {
    // Проверить параметры с помощью утверждений.
    ASSERT ( NULL != hModule);
    ASSERT ( FALSE == IsBadStringPtr ( szImportMod, MAX_PATH));
    ASSERT ( 0 != dwOrdinal);
    ASSERT ( FALSE = IsBadCodePtr ( pHookFunc));
    // Выполнить проверку ошибок для параметров.
    if ( ( NULL == hModule ' ) | |
    ( TRUE == IsBadStringPtr ( szImportMod, MAX_PATH)) ||
    ( 0 == dwOrdinal ) I I ( TRUE == IsBadCodePtr ( pHookFunc) ) )
    {
    SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);
    return ( FALSE);
    }
    if ( NULL != ppOrigAddr)
    {
    ASSERT ( FALSE ==
    IsBadWritePtr ( ppOrigAddr, sizeof ( PROG)));
    if ( TRUE == IsBadWritePtr ( ppOrigAddr, sizeof ( PROC)))
    {
    SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);
    return ( FALSE);
    }
    }
    // Получить конкретный дескриптор импорта.
    PIMAGE_IMPORT_DESCRIPTOR plmportDesc =
    GetNamedlmportDescriptor ( hModule, szImportMod);
    if ( NULL == plmportDesc)
    {
    // Запрошенный модуль не был импортирован. Не возвращать ошибку,
    return ( TRUE);
    }
    // Получить информацию об исходных переходниках для этого DLL
    . // Невозможно использовать информацию переходников, хранящуюся в

    // pImportDesc->FirstThunk, т. к. загрузчик уже изменил этот массив

    // при установке всех импортов. Исходный переходник обеспечивает

    // доступ к именам функций.

    PIMAGE_THUNK_DATA pOrigThunk =

    MakePtr ( PIMAGE_THUNK_DATA

    hModule ,

    pImportDesc->OriginalFirstThunk );

    // Получить массив p!mportDesc->FirstThunk, в котором будут

    // выполняться подключения и вся черная работа.

    PIMAGE_THUNK_DATA pRealThunk = MakePtr ( PIMAGE_THUNK_DATA ,

    hModule ,

    pImportDesc->FirstThunk );

    // Флажок будет устанавливаться из переходника,

    // что облегчает его поиск.

    DWORD dwCompareOrdinal = IMAGE JDRDINAL_FLAG | dwOrdinal;

    // Цикл поиска подключаемых функций.

    while ( NULL != pOrigThunk->ul.Function)

    {

    // Отыскивать только функции, которые импортируются по

    // порядковому значению, а не по имени,

    if ( IMAGE__ORDINAL_FLAG ==

    ( pOrigThunk->ul.Ordinal & IMAGE_ORDINAL_FLAG))

    {

    // Найдена ли функция подключения?

    if ( dwCompareOrdinal == pOrigThunk->ul.Ordinal) .

    {

    // Функция для подключения найдена. Теперь нужно

    // изменить защиту памяти на "read-write" (для записи),

    // прежде чем перезаписывать указатели функций. Заметьте,

    // что ничего не записывается в реальную область

    // переходников!

    MEMORY_BASIC__IN FORMATION mbi_thunk ;

    VirtualQuery ( pRealThunk ,

    &mbi_thunk ,

    sizeof ( MEMORY_BASIC_INFORMATION) );

    if ( FALSE == VirtualProtect ( mbi_thunk.BaseAddress,

    rabi_thunk.RegionSize ,

    PAGE_READWRITE ,

    &mbi_thunk.Protect ))

    {

    ASSERT ( !"VirtualProtect failed!");

    // Здесь приходится фиксировать неуспешное

    // выполнение функции (возвращая FALSE),

    // предварительно указав причину ошибки.

    SetLastErrorEx ( ERROR__INVALID_PARAMETER,

    SLE^ERROR );

    return ( FALSE);

    }

    // Сохранить исходные адреса, если требуется

    if ( NULL != ppOrigAddr)


    {

    *ppOrigAddr = (PROC)pRealThunk->ul.Function;

    }

    // Microsoft имеет два различных определения

    // РIМАСЕ_ТШ№С_ОАТА-полей для будущей поддержки Win64

    // Будет использован самый последний набор заголовков

    //из W2K RC2 Platform SDK, с которыми будут иметь дело

    // заголовки из Visual C++ 6 Service Pack 3.

    // Подключить функцию (DWORD*)SpRealThunk->ul.Function;

    *pTemp = (DWORD)(pHookFunc);

    DWORD dwOldProtect;

    // Изменить защиту обратно к тому состоянию, которое

    // предшествовало переписыванию указателя функции.

    VERIFY ( VirtualProtect ( mbi_thunk.BaseAddress,

    mbi_thunk.RegionSize ,

    mbi_thunk.Protect ,

    sdwOldProtect ));

    // Жизнь прекрасна!

    SetLastError ( ERROR_SUCCESS);

    return ( TRUE);

    }

    }

    // Инкремент обеих таблиц. pOrigThunk++; pRealThunk++;

    }

    // Ничего не было подключено. Технически это не ошибка. Это просто

    // означает, что модуль импортирован, а функция — нет.

    SetLastError ( ERROR_SDCCESS);

    return ( FALSE);

    }

    Для реализации обработки AfxTrace без ее подключения, пришлось бы просматривать стек при каждом вызове, чтобы вернуться к реальному вызову функции OutputDebugstring. Дополнительная работа на каждом вызове была бы медленнее, по сравнению с прямым подключением AfxTrace. Кроме того, если бы я игнорировал AfxTrace, то утилита LIMODS была бы, в основном, бесполезна для MFC-программистов. И, наконец, я предпочел создавать утилиту LIMODS настолько полной, насколько это возможно, причем я был вынужден дважды проверять версии MFC DLL.

    Поиск решения

    Путь к решению проблемы ограничения предложений трассировки был очень извилистым. Первой мыслью было применение условной компиляции, чтобы каждый исходный файл имел связанную с ним директиву #define. Чтобы увидеть предложения трассировки для конкретного файла или набора файлов, нужно просто вставить в исходный код директиву #define и выполнить компиляцию. Такой подход, конечно, работал бы, но повторная компиляция программы только для включения или выключения предложений трассировки в исходном файле все-таки не представлялась достаточно эффективной.
    Другим возможным решением было включение в программу уникального макроса трассировки. Пытаясь решить проблему слишком большого количества трассировок в огромной команде разработчиков, я опробовал и такой подход. Каждая "подкоманда" получала уникальный макрос трассировки для своей специфической секции проекта. Подобный подход реализует библиотека классов MFC (Microsoft Foundation Class), внутренняя трассировка которой включается с помощью специальной программы TRACER.EXE. Предложения внутренней трассировки MFC проверяют глобальную переменную-флажок, и если бит, назначенный подсистеме, установлен, в окне Output появляются предложения трассировки. В моей большой команде подход с уникальным макросом некоторое время работал, но постепенно перестал применяться, потому что MFC-мастера (wizards) сами генерировали специальный трассировочный макрос, и разработчики забывали использовать свой уникальный макрос. Чтобы заставить разработчиков использовать правильный макрос, я пробовал бороться с этой проблемой, отменяя определение макроса TRACE в исходных файлах (с помощью директивы #undef), но обнаружил, что наличие макросов, устанавливаемых и мастером, и еще кем-то, сильно раздражает разработчика. Другая проблема состоит в том, что этот макрос не так просто расширить. В общем, нужно тратить довольно много времени на предварительный перенос макроса трассировки с проекта на проект. Становится также проблемой поддержка таких макросов при изменении архитектуры программы и перемещении кода из одной подсистемы в другую. Кроме того, подход с уникальным макросом не работает с программами на Visual Basic.
    Осознав вышеизложенное, я попытался обдумать проблему с совершенно другой точки зрения и понял, что на самом деле нужно было найти способ связывать вызов функции трассировки с конкретным исходным файлом, из которого он пришел. Надо было также найти способ решить, действительно ли проходил (или не проходил) этот вызов, и наконец, способ переключать вывод предложений трассировки от исходных файлов и от модулей. С такой формулировкой проблемы я и подошел к ее решению.
    В отличие от других глав этой книги, проблемы, описанные в предыдущем разделе, я решал независимо друг от друга. Чтобы построить систему, которая ограничивает количество предложений трассировки, нужно только связать индивидуальные решения вместе. Кажется, мне удалось достичь заветной цели повторного использования кода (см. ниже раздел "Реализация LIMODS" данной главы).



    Работа с MFC

    Если планируется применять LIMODS с приложением, которое использует либо MFC42D.DLL, либо MFC42UD.DLL, нужен дополнительный шаг для ее установки. К сожалению, существует по крайней мере 1001 версия этих двух очень важных DLL. LIMODS должна знать, какое порядковое значение соответствует экспортируемой функции AfxTrace из каждого DLL-файла MFC. Хотя можно предположить, что AfxTrace будет всегда иметь одинаковое порядковое значение, независимо от того, в какой DLL она находится, на самом деле это не так. Дистрибутивный LIMODS.INI содержит информацию о файлах MFC42(U)D.DLL, которые используются с Visual C++ 5 без Service Pack, Visual C++ 6 без Service Pack, Visual C++ 6 с Service Pack 1 (SP1), Visual C++ 6 с Service Pack 2 (SP2) и Visual C++ 6 Service Pack 3 (SP3).
    Если установленная версия Visual C++ отличается от перечисленных в LIMODS.INI (включая любые последующие выпуски Service Pack), то придется проделать некоторую дополнительную работу, чтобы гарантировать получение утилитой LIMODS правильного экспорта из MFC42D.DLL и MFC42UD.DLL. Информацию о версии библиотечного файла %SYSTEMROOT%\System32\MFC42(U)D.DLL можно получить, если щелкнуть правой кнопкой мыши на этом файле в Проводнике Windows и выбрать пункт Свойства в раскрывшемся контекстном меню. Затем следует перейти на вкладку Версия диалоговой панели Свойства. Первый пункт этой вкладки (Версия файла) и является искомым номером версии. Например, версия файла MFC42D.DLL, которую использует Visual C++ 6.0 SP3, имеет номер 6.00.8447.0.
    Затем нужно перейти в подкаталог \MFC\SRC\Intel каталога Visual C++. Там можно найти DEF-файлы, которые использовались для компоновки MFC. Имена DEF-файлов соответствуют именам двоичных файлов. Например, MFC42D.DEF является DEF-файлом для MFC42D.DLL. Откройте соответствующий DEF-файл и отыщите текст ?AfxTrace@@YAXPBpzz для MFC42D.DLL. Для MFC42UD.DLL найдите текст ?AfxTrace@@YAXPBGZZ. Эта важная строка будет похожа на следующую:
    ?AfxTrace@@YAXPBDZZ @ 1179 NONAME
    Число после знака @ — порядковое значение экспортируемой функции AfxTrace.
    Запомните это число: его нужно будет ввести в файл LIMODS.INI.

    Откройте свою копию LIMODS.INI. Для MFC42D.DLL ищите секцию [MFC42D.DLL Hack-0-Rama], а для MFC42UD.DLL— секцию [MFC42UD.DLLHack-o-Rama]. Примерный вид секции для MFC42D.DLL показан ниже (секция для MFC42UD.DLL выглядит примерно так же):

    [MFC42D.DLL Hack-0-Rama]

    VerCount=3

    ; VC 6.0 SP3

    VerO=6.00.8447.0,1179 ;

    VC 6.0 SP1 and SP2.

    Verl=6.00.8267.0,1179 ;

    VC 6.0 NO SERVICE PACKS

    Ver2=6.00.8168.0,1179

    Параметры verN определяют номера версий и их порядковые значения. Первое число — номер версии MFC-файла, а второе — порядковое значение функции AfxTrace. Добавьте1 версию вашего MFC-файла и порядковое значение функции AfxTrace В конец секции [MFC42D.DLL Hack-0-Rama] ИЛИ [MFC42UD.DLL Hack-0-Rama]. Например, если вы работаете с Visual C++ 5 без Service Pack, то добавьте строку ver3=4.21.7022,1253 к предшествующему примеру (для MFC42D.DLL). Нужно также увеличить на 1 значение счетчика vercount (vercount=4). Для MFC42UD.DLL в Visual C++ 5 без Service Pack строка была бы такой:

    Ver3=4.21.7022,1256.

    Если вы модифицируете исходный код MFC и строите собственную MFC42(U)D.DLL, то можно проверять информацию порядкового значения точно так же, как описано выше. Лучше, однако, не изменять исходный код MFC и создавать собственную версию MFC (если, конечно, вы не испытываете желания вручную обновлять каждое исправление MFC-ошибки в своих версиях исходных файлов).

    Эту операцию нужно выполнять только в том случае, когда в списке версий нет параметра ParN для вашей версии системы программирования. — Пер.

    LIMODS проверяет версию MFC-файла, когда MFC42(U)D.DLL загружается в память. Если в LIMODS.INI нет соответствующей версии файла, то будет выведено (в окне Output) сообщение об ошибке, сопровождаемое несколькими звуковыми сигналами. Если компилировать и выполнить программу тестирования LIMODS (\SourceCode\LIMODS\TestLIMODS), поставляемую на сопровождающем компакт-диске, то можно быстро выяснить, правильно ли сконфигурирована утилита LIMODS.

    Реализация LIMODS

    Реализация LIMODS оказалась весьма интересной. Например, чтобы заставить работать символьную машину DBGHELP.DLL, пришлось прибегнуть к некоторым хитростям, но действительный интерес представляет разработка _cdeci-функций, подключающих импортируемые функции, и подключение импортируемых функций по их порядковым номерам.


    Требования к LIMODS

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


    Выбор исходных файлов для трассировки

    Все файлы *.LOM для модулей, компилированных с символами отладки, будут генерироваться во время первого же выполнения приложения с загруженной библиотекой LIMODSDLL.DLL, так что при выполнении приложения возникнет небольшая пауза. LIMODS сохраняет LOM-файлы неизменными вплоть до даты изменения программных модулей, причем отслеживание этих изменений и модификация соответствующих ШМ-файлов выполняются автоматически. При желании генерацию LOM-файлов можно сделать частью процесса нормального построения приложения, используя программу GENLIMODS.EXE. Чтобы просмотреть описание параметров GENLIMODS.EXE, нужно запустить ее в командной строке без параметров. После того как LOM-файлы загружены, просто запустите LIMODS.EXE и выберите, какие исходные файлы и из каких модулей вы хотите просматривать на предмет трассировки. На рис. 14.1 показан LIMODS.EXE в действии. Установите флажки тех исходных файлов, чьи предложения трассировки необходимо просматривать.
    Выбор исходных файлов для трассировки
    Рис. 14.1. LIMODS.EXE в действии


    Вызов LIMODS из кода

    Вложив столько усилий в гарантии правильного использования MFC DLL, можно слегка разочароваться, узнав, что утилита LIMODS заработает, если добавить в исходный код С/С++-приложения всего лишь одну строку:
    LoadLibrary ( "LIMODSDLL.DLL");
    Основная часть работы LIMODSDLL.DLL сосредоточена в ее функции DllMain, так что никаких других функций вызывать не нужно. Полагаю, что добавление одной строки кода — не слишком большая цена за услуги LIMODS.
    Если разрабатываются Visual Basic-программы, то заставить работать LIMODS в их коде немного сложнее, — но не очень. Сначала нужно копировать файлы LIMODS.CLS и INDESIGNMOD.BAS из каталога \SourceCode \LIMODS\VB сопровождающего компакт-диска и добавить их к вашему проекту. Затем нужно создать глобальный экземпляр класса CISLIMODS, показанного в листинге 14-1. Я бы рекомендовал именовать глобальную переменную экземпляра как CLIMODS. Класс CISLIMODS содержит только один метод; его имя Trace, а его входные параметры — такие же, как у метода Debug.Print. Нет никакой возможности перехватить внутренний объект Debug.Print, поэтому, чтобы утилита LIMODS работала в приложении Visual Basic, это приложение нужно компилировать. Если запускать такую программу из IDE, то метод Trace класса CISLIMODS преобразуется в вызов метода Debug. Print, вот почему предложения трассировки можно будет увидеть в окне Immediate.
    Кроме того, в проекте нужно определить режим условной компиляции LIMODS=-1, что позволит при компиляции получать необходимые методы класса. Если этого не сделать, то будут получены пустые версии. Условная компиляция позволяет избежать дополнительных расходов на применение LIMODS в тех случаях, когда она не нужна, а расходы на пустые функции можно исключить, окружив все обращения к объекту ClsLIMODS директивами условной компиляции.
    Листинг 14-1. LIMDOS.CLS VERSION 1.0 CLASS
    BEGIN
    MultiUse = -1 'True
    Persistable = 0 'NotPersistable
    DataBindingBehavior = 0 'vbNone
    DataSourceBehavior = 0 'vbNone
    MTSTransactionMode = 0 'NotAnMTSObject

    END

    Attribute VB_Name = "clsLIMODS"

    Attribute VB_GlobalNameSpace = False

    Attribute VB_Creatable = True

    Attribute VB_PredeclaredId = False

    Attribute VB_Exposed = False

    ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

    'Copyright (c) 1997-2000 John Robbins — All rights reserved.

    ' "Debugging Applications" (Microsoft Press)

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

    ' Вспомогательный класс LIMODS для разработки приложений Visual Basic

    ' 1. Включить этот файл класса в Visual Basic-проект и создать в нем

    ' глобальный экземпляр этого класса. ( Я назвал экземплярную

    ' переменную "cLIMODS".)

    ' 2. Чтобы отправлять отсюда предложения трассировки, нужно просто

    ' вызвать метод cLIMODS.Trace.

    ' 3. LIMODS активен только в компилированном Visual Basic-приложении.

    ' Если выполнять объект этого класса под отладчиком VB-IDE, то

    ' предложения трассировки будут посылаться с помощью регулярного

    ' оператора трассировки Debug.Print.

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

    Option Explicit

    Private Declare Function LoadLibrary Lib "kerne!32" _

    Alias "LoadLibraryA" _

    (ByVal IpLibFileName As String) As Long

    Private Declare Sub OutputDebugString Lib "kerne!32" _

    Alias "OutputDebugStringA" _

    (ByVal IpOutputString As String)

    Private Declare Function GetModuleFileName Lib "kerne!32" _

    Alias "GetModuleFileNameA"

    _ (ByVal hModule As Long, _

    ByVal IpFileName As String, _

    ByVal nSize As Long) As Long

    Private Declare Function GetModuleHandle Lib "kerne!32" _

    Alias "GetModuleHandleA" _

    (ByVal IpModuleName As String) As Long

    Private m_Is!nIDE As Boolean

    #If LIMODS Then

    Private Sub Class_Initialize()

    Dim blsInlDE As Boolean

    blsInlDE = InDesign()

    ' If blsInlDE is False, the main module isn't the Visual Basic IDE,

    ' so I can load LIMODSDLL.DLL.

    If (False = blsInlDE) Then

    LoadLibrary "LIMODSDLL.DLL"

    m_Is!nIDE = False Else

    m_IsInIDE = True

    End If

    End Sub

    #End If

    #If LIMODS Then

    Public Sub Trace(sOut As Variant)

    If (True = m_Is!nIDE) Then

    Debug.Print sOut

    Else

    Dim s As String

    s = sOut

    OutputDebugString s

    End If

    End Sub

    #Else ' LIMODS is *not* conditionally defined.

    Public Sub Trace(sOut As Variant)

    End Sub

    #End If

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

    Глубокие проверки корректности

    Та часть расширения MemDumperValidator, которая имеет дело с выдачей дампов, бесспорно полезна, но можно задать вопрос: "Зачем вообще нужен метод проверки корректности, даже если он позволяет осуществлять "глубокую" проверку блока памяти?" Если класс содержит всего пару строчных переменных, то во многих случаях функция проверки корректности может быть "пустой". Но даже в этом случае такая функция может оказаться бесценной для разработчика, потому что она обеспечивает его превосходными отладочными возможностями. Одной из целей, которой я стремился достичь, начиная пользоваться глубокой проверкой корректности памяти, было обеспечение второго уровня проверки корректности данных на наборе разработанных мной базовых классов. Функция проверки корректности не должна заменять обычных проверок параметров и вводимых данных, но может повысить степень уверенности в правильности этих данных. Глубокая проверка корректности может также быть второй линией защиты против "непредсказуемых" (wild) записей.
    Лучше всего применять функции проверки корректности для двойной проверки сложных структур данных после того, как на них были выполнены некоторые операции. Например, однажды я попал в довольно сложную ситуацию, когда две отдельные рекурсивные структуры данных (self-referential data structures) использовали (по соображениям экономии памяти) одни и те же распределенные объекты. Заполнив эти структуры большими наборами данных, я с помощью функции проверки корректности просматривал индивидуальные блоки кучи и проверял корректность ссылок. Можно было написать обстоятельную программу для просмотра каждой структуры данных, но я знал, что любая такая программа стала бы новым рассадником ошибок. А функция проверки корректности позволила "проскакивать" через распределенные блоки, используя уже протестированный код, и проверять структуры данных, начиная с различных позиций, потому что память была выстроена в порядке распределения, а не в отсортированном порядке.
    Операции распределения памяти языка С более сложны, чем в языке C++, но функции проверки корректности памяти в обоих языках используются одинаково. Все что нужно делать — это вызывать макрос VALIDATEALLBLOCKS. В отладочных построениях этот макрос расширяется до вызова подпрограммы vaiidateAHBiocks. В качестве параметра она использует любое значение, которое вы хотите передать функциям проверки корректности, зарегистрированным вместе с библиотекой. Раньше при помощи этого параметра я определял глубину проверок корректности, выполняемых функцией. Имейте в виду, что validateAHBlocks пересылает данное значение каждой зарегистрированной подпрограмме проверки корректности, чтобы можно было координировать эти значения в команде разработчиков.

    Чтобы понять, как работают функции расширения MemDumperValidator, просмотрите программу с именем Dump, показанную в листинге 15-2. Dump — это "пустая", незаполненная программа, в которой показано все,, что нужно для использования этого расширения. Я не привожу пример кода, но MemDumperValidator хорошо работает и с MFC, потому что MFC будет вызывать любые предварительно зарегистрированные функции подключения клиентских дампов.

    Листинг 15-2. DUMP.CPP

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    "Debugging Applications" (Microsoft Press)

    Copyright (c) 1997-2000 John Robbins — All rights reserved.

    - - - - - - - - - - - - - - - - - - - - - - - - - -*/

    #include

    #include

    #include

    #include

    #include

    #include "BugslayerUtil.h"

    class TestClass

    {

    public:

    TestClass ( void)

    {

    strcpy ( m_szData, "TestClass constructor data!");

    }

    TestClass ( void)

    {

    m_szData[ 0 ] = '\0';

    }

    // Объявление средств отладки памяти для классов языка C++

    DECLARE_MEMDEBUG ( TestClass);

    private :

    char m_szData[ 100 ];

    };

    // Этот макрос устанавливает статическую структуру DVINFO .


    IMPLEMENT_MEMDEBUG ( TestClass);

    Listing 15-2DUMP.CPP *

    // Методы выдачи дампов и проверки корректности блоков памяти

    #ifdef _DEBUG

    void TestClass::ClassDumper ( const void * pData)

    {

    TestClass * pClass = (TestClass*JpData;

    _RPT1 ( _CRT_WARN,

    " TestClass::ClassDumper : %s\n", pClass->m_szData);

    }

    void TestClass::ClassValidator ( const void * pData ,

    const void * )

    {

    // Проверка корректности данных.

    TestClass * pClass = (TestClass*)pData;

    _RPT1 ( _CRT_WARN ,

    " TestClass::ClassValidator : %s\n",

    pClass->m_szData );

    }

    #endif

    typedef struct tag_SimpleStruct

    {

    char szNamef 256 ]; char 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 : Ox%08X\n",

    pData, pContext);

    }

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

    void DumperTwo ( const void * pData)

    {

    _RPT2 ( _CRT_WARN

    " Data is Name : %s\n"

    " Rank : %s\n" ,

    ((SimpleStruct*)pData)->szName ,

    ((SimpleStruct*)pData)->szRank );

    }

    void ValidatorTwo ( const void * pData, const void * pContext)

    {

    // Проверка корректности структур.

    _RPT2 ( _CRT_WARN , " Val%dator called with :\n"

    " Data is Name : %s\n"

    " Rank : %s\n" ,

    ((SimpleStruct*)pData)->szName ,

    ((SimpleStruct*)pData)->szRank );

    }

    // К сожалению, функции языка С используют собственные структуры

    // DVINFO. Эти структуры необходимо определять как внешние ссылки, а


    // для макроса MEMDEBUG нужно создавать собственный макрос-оболочку,

    static DVINFO g_dvOne;

    static DVINFO g_dvTwo;

    void main ( void)

    {

    cout " "At start of main\n";

    // Инициализация отладки памяти для типа One.

    INITIALIZE_MEMDEBUG ( &g_dvOne, DumperOne, ValidatorOne) ;

    // Инициализация отладки памяти для типа Two.

    INITIALIZE_MEMDEBUG ( Sg_dvTwo, DumperTwo, ValidatorTwo) ;

    // Распределить память для класса с новым MEMDEBUG.

    TestClass * pstClass;

    //pstClass = MEMDEBUG_NEW TestClass;

    pstClass = new TestClass;

    // Распределить память для двух типов языка С.

    char * р = (char*)MEMDEBUG_MALLOC '( &g_dvOne, 10);

    strcpy ( р, "VC VC");

    SimpleStruct * pSt =

    (SimpleStruct*)MEMDEBUG_MALLOC ( Sg_dvTwo,

    sizeof ( SimpleStruct));

    strcpy ( pSt->szName, "Pam");

    strcpy ( pSt->szRank, "CINC");

    // Проверить корректность всех блоков в списке.

    VALIDATEALLBLOCKS ( NULL);

    cout " "At end of main\n";

    // Дамп каждого блока будет выведен как часть проверки утечки памяти

    }

    Инициализация и завершение в C++

    Закончив реализацию расширения MemDumperValidator и начав его тестировать, я убедился, что расширение работало так, как было запланировано. Однако, обдумывая все способы, с помощью которых программа может распределять память в куче, и просматривая код MemDumperValidator, я обнаружил явный пробел в его логике: отсутствовали статические конструкторы, распределяющие память.
    Хотя большинство разработчиков не очень часто используют этот прием, в некоторых случаях память распределяется перед точкой входа приложения.
    При моем подходе к разработке MemDumperValidator проблема состояла в следующем: нужно было гарантировать, что установки подходящих флажков для функции _CrtsetDbgFiag будут выполняться прежде, чем произойдут какие-либо распределения памяти.
    И, наконец, последний момент, который надо подчеркнуть в связи с расширением MemDumperValidator: перед началом использования DCRT-биб-лиотеки не забывайте вызывать какую-нибудь, хотя бы самую простую, функцию инициализации. В С-программах не очень удобно иметь дело со структурами DVINFO. Я хотел сделать MemDumperValidator настолько автоматическим, насколько это возможно, чтобы применение его разработчиками не вызывало каких-либо затруднений.
    К счастью, существует директива #pragma init_seg, позволяющая управлять инициализацией и порядком ликвидации статически объявленных значений. Директиве #pragma init_seg можно передать один из следующих параметров, которые, no-существу, специфицируют тип инициализируемого значения: compiler, lib, user, section name И funcname. Наиболее важными ЯВЛЯЮТСЯ три первых параметра.
    Параметр compiler зарезервирован для компилятора Microsoft. Любые объекты, маркированные меткой compiler, создаются первыми, а разрушаются последними. Объекты, маркированные как lib, создаются после и разрушаются перед объектами compiler, а объекты, маркированные меткой user, строятся последними, а разрушаются первыми.
    Напомним, что в языке C++ созданием и разрушением объектов в динамической памяти занимаются специальные методы — конструкторы и деструкторы.
    Первые занимаются выделением памяти и инициализацией объекта, а вторые — освобождением выделенной объекту памяти, т. е. его ликвидацией, разрушением. — Пер

    Поскольку код расширения MemDumperValidator должен быть инициализирован перед тем, как будет инициализирован ваш код, можно просто передать lib как параметр директиве #pragma init_seg и на этом закончить работу. Однако если вы создаете библиотеки и маркируете их в виде lib-сегментов (как и должно быть) и хотите использовать мой код, то его нужно инициализировать перед инициализацией вашего кода. Для этого данной директиве надо передать параметр: #pragma init_seg (compiler). Хотя нужно всегда следовать правилам, приводящим к правильной инициализации кодовых сегментов, применение параметра compiler в отладочном коде оказывается достаточно безопасным занятием.

    Поскольку идеи инициализации работают только в кодах языка C++, в MemDumperValidator включен специальный статический класс (с именем AutoMatic), который просто вызывает функцию _CrtSetDbgFiag. Приходится идти на все эти ухищрения только потому, что это единственный способ установить DCRT-флажки перед инициализацией других библиотек. Кроме того, чтобы обойти некоторые ограничения при проверке утечек памяти в DCRT-библиотеке, необходимо выполнять некоторую специальную обработку при разрушении объектов соответствующего класса. Даже если бы MemDumperValidator имел только С-интерфейс, то всегда можно было бы использовать преимущества языка C++ для его установки и запуска, чтобы он всегда был готов для вызова.

    Интересная "стрессовая" проблема

    Первоначально, при работе с примером небольшой консольной программы, MemStress вела себя великолепно. Однако, завершая работу над программой MemStressDemo (которая использует библиотеку классов MFC), я обратил внимание на следующую проблему. Если в режиме диалога с MemStress пользователь (через панель сообщений) требовал, чтобы распределения терпели неудачу, то раздавался ряд звуковых сигналов, и программа MemStressDemo переставала работать. Поскольку ситуация устойчиво воспроизводилась (иначе говоря, я имел возможность дублировать эту проблему), то серьезный стресс возник уже у меня самого, потому что я никак не мог уловить, в чем суть этой проблемы.
    После нескольких прогонов, наконец, удалось вновь открыть панель сообщения. Но, вместо того чтобы находиться в центре экрана, она открылась в его нижнем правом углу. Такое поведение панелей сообщений позволяет с достаточной долей уверенности считать, что вызов API-функции MessageBox каким-то образом стал реентерабельным (повторно входимым). Мне показалось, что подключение распределения происходило где-то в середине вызова функции MessageBox. Для проверки гипотезы я установил точку прерывания на первой инструкции функции AiiocationHook, как раз перед вызовом функции MessageBox. Отладчик, как и следовало ожидать, остановился на точке прерывания.
    Просмотр стека показал, что прямое обращение к API-функции MessageBox почему-то проходило через код MFC. Трассировка этого кода привела меня внутрь функции _AfxActivationWndProc в строку, которая вызывала метод CWND::FromHandie, выполняющий распределения памяти для того, чтобы MFC могла создавать объекты типа (класса) cobject. Я был немного озадачен — как я там оказался? Но комментарий в коде указал, что функция _AfxActivationWndProc используется для управления активизацией и создает "серые" (неактивные) диалоговые панели. MFC использует СВТ-подключение, чтобы перехватывать создание окна в пространстве процесса. Когда новое окно создается (в моем случае это простая панель сообщения — message box), MFC создает подчиненное окно со своей собственной оконной процедурой.

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

    Такая формулировка проблемы позволила понять, что нужна только переменная в локальной памяти потока, обеспечивающая доступ к функции AiiocationHook. Если ее значение больше 0, значит функция AiiocationHook была повторно вызвана в процессе выполнения функции MessageBox, и нужно только освободить эту функцию. Я реализовал специальное решение, и все заработало так, как и было запланировано.

    СВТ — Computer-Based Training, машинное обучение. — Пер.

    Автор называет это решение "quick dynamic thread local storage solution" быстрым решением для динамической локальной памяти потока. — Пер.

    Использование DCRT-библиотеки

    Первым шагом к применению DCRT-библиотеки является включение ее в проект, что позволяет немедленно начать извлекать выгоду из мониторинга памяти. Для этого перед директивами #inciude главного файла проекта (или любого другого файла вашего проекта) необходимо добавить следующую строку:
    #define _CRTDBG_MAP_ALLOC
    Потом к имеющимся в программе заголовочным файлам следует добавить файл CRTDBG.H. Определение _CRTDBG_MAP_ALLOC будет переадресовывать обращения к обычным функциям распределения и освобождения памяти к специальным версиям этих функций, которые будут записывать в отчет имя и номер строки исходного файла каждой операции распределения или освобождения памяти.
    Затем необходимо включить являющийся частью DCRT-библиотеки код управления кучей. Как говорилось в начале этой главы, большинство свойств DCRT-библиотеки по умолчанию выключено. Документация утверждает, что это сделано для того, чтобы сохранять небольшой размер кода и увеличивать скорость его выполнения. Хотя размер и скорость могут быть важны для сборки релиза (release build, выпускного построения), основной целью отладочного построения (debug build) является поиск ошибок! При отладке увеличенный размер и уменьшенная скорость выполнения приложения не имеют особого значения. Поэтому без колебаний включайте все свойства, которые, по вашему мнению, могут оказаться полезными. Функция _CrtsetDbgFiag принимает набор флагов, перечисленных в табл. 15.2. Чтобы включать различные режимы библиотеки DCRT, их можно объединять друг с другом операцией ок.
    После сборки приложения с указанными выше директивами #inciude и #define и вызова функции _crtsetDbgFlag обеспечен полный доступ к библиотеке DCRT, многочисленные функции которой помогут управлять использованием памяти и получать соответствующие отчеты. Эти функции можно вызывать в любой точке приложения, а многие из них использовать внутри утверждений, что позволяет отлавливать проблемы памяти вблизи источника.
    Одна из наиболее полезных функций DCRT — _CrtcheckMemory.
    Она просматривает всю распределенную память и проверяет, не записывались ли какие-нибудь данные в начало или в конец блока, а также не перераспределялись ли предварительно освобожденные блоки памяти. Только из-за одной этой функции применение библиотеки DCRT оправдано!

    Другой набор функций позволяет проверять корректность данных любой области памяти. Функции _CrtIsValidHeapPointer, _CrtIsMemoryBlock И _crtisVaiidPointer удобно использовать в качестве отладочных параметров функций проверки корректности. Вместе с _crtcheckMemory эти функции являются превосходными средствами проверки памяти.

    Таблица 15.2. Флажки библиотеки DCRT

    Флажок

    Описание

    _CRTDBG_ALLOC_MEM_DF

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

    _CRTDBG_CHECK_ALWAYS_DF

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

    _CRTDBG_CHECK_CRT_DF

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

    CRTDBG_DELAY_FREE_MEM_DF

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



    (прод.)

    программу с предельными требованиями к памяти, без реального ее освобождения. Кроме того, по заполненности списка значениями OxDD библиотека DCRT будет проверять, не пытались ли вы снова получить доступ к освобожденному блоку памяти. Нужно всегда включать этот флажок, но имейте в виду, что при этом требования вашей программы к памяти легко могут удвоиться, потому что освобожденная память не восстанавливается за счет кучи

    _CRTDBG_LEAK_CHECK_DF

    Проверять утечки памяти в конце программы. Включение этого чрезвычайно полезного флажка обязательно




    Обратите внимание еще на одну полезную группу функций в DCRT — это функции состояния памяти _CrtMemCheckpoint, _CrtMemDifference И _CrtMemDumpStatistics. Чтобы увидеть различные неполадки при работе кучи, эти функции полезно выполнять перед и после операцией сравнения областей кучи. Например, если используется обычный (не отладочный) вариант CRT-библиотеки, то можно сделать предварительные и последующие дампы кучи при вызове функций, сообщающих об утечке памяти или о размере памяти, используемой некоторой операцией.

    Библиотека DCRT позволяет подключаться к потоку функций распределения и освобождения памяти, что помогает проследить каждый вызов этих функций. Если функция подключения к этому потоку возвращает значение TRUE, то распределение может продолжаться. Если же эта функция возвращает FALSE, то это означает, что был сбой в процессе распределения. Впервые обнаружив эти возможности, я подумал, что мог бы без особых усилий получить средства тестирования кода при некоторых действительно неприятных граничных условиях (которые в иной ситуации будет очень трудно дублировать). Результат можно увидеть в приложении MemStress (входящем в состав библиотеки BUGSLAYERUTIL.DLL). Эта программа, по существу, расширяет DCRT-библиотеку и позволяет форсировать отказы в процедурах распределения памяти (это приложение будет представлено в конце данной главы).

    Кроме того, библиотека DCRT позволяет подключать функции дампов памяти и перечислять клиентские блоки (т. е. память, выделенную программе). Можно также заменять штатные функции дампов памяти собственными, которые "знают" все о ваших данных. Теперь, вместо просмотра загадочной выгруженной памяти (т. е. дампа), которую вы получаете по умолчанию (и которая, кроме того, что ее трудно расшифровывать, не так уж и полезна), можно получить точную информацию о содержимом блока памяти и вывести ее в удобном формате. Для этой цели в MFC имеется функция Dump, но она работает только с классами, производными от cobject. Если же вы (как и я) программируете не только в MFC, то вам нужны более общие функции дампов, приспособленные к различным типам кодов.

    Свойство перечисления клиентских блоков, судя по названию, позволяет перечислять выделенные блоки памяти. Эта очень полезное свойство поможет создавать некоторые интересные утилиты. Например, в функциях MemDumperVaiidator из BUGSLAYERUTIL.DLL, я вызываю обработчики дампов из функции перечисления клиентских блоков, так что перечисление может выполнять дамп и проверку корректности многих типов распределенной памяти в одной операции. Это позволяет выполнять более глубокую проверку содержимого памяти (по сравнению с проверкой поверхности записей underwrites и overwrites). Под глубокой проверкой корректности я понимаю специальный алгоритм, которому известны форматы данных в блоке памяти и который, опираясь на знание этих форматов, гарантирует, что каждый элемент данных является корректным.

    Использование MemDumperValidator в приложениях C++

    К счастью, для того чтобы MemDumperValidator заработал в приложениях C++, нужно выполнить относительно простую операцию — определить особый класс для работы с расширением MemDumperValidator. В объявлении этого класса нужно определить макрос DECLARE_MEMDEBUG с именем класса в качестве параметра. Этот макрос немного похож на макрос MFC, который расширяется в пары объявлений — для данных и методов. При просмотре листинга 15-1 можно найти определения трех встроенных функций — new, delete и new, содержащих информацию об имени исходного файла и номере строки соответствующего вызова. Если в вашем классе определен любой из этих операторов, то необходимо извлечь их код из соответствующих операторов расширения и поместить его в операторы вашего класса.
    В файле реализации вашего С++класса нужно использовать макрос IMPLEMENT_MEMDEBUG (и снова с именем класса в качестве параметра), который определяет статическую переменную этого класса. Макросы DECLARE
    _MEMDEBUG И IMPLEMENT_MEMDEBUG работают только в отладочных построениях, поэтому их не следует обрамлять директивами условной компиляции.
    После определения обоих макросов (и правильного их размещения в исходном коде), необходимо реализовать два метода, которые будут выполнять фактический дамп и проверку корректности блоков памяти. Прототипы этих методов выглядят так:
    static void ClassDumper ( const void * pData);
    static void ClassValidator ( const void * pData,
    const void * pContext);
    Очевидно, что эти методы следует разместить между директивами условной компиляции, для того чтобы они не компилировались в выпускных построениях.
    Параметр pData в обоих методах — это указатель на блок памяти с экземпляром (объектом) данного класса. Для получения работоспособного указателя следует выполнить явное приведение типа значения, хранящегося в pData (т. е. адреса объекта), к типу класса. Во всех операциях, связанных с выдачей дампов и проверкой корректности (памяти), значения параметра pData следует использовать только для чтения, иначе в код может быть введено множество ошибок, которые придется устранять.
    Второй параметр метода ClassValidator - pContext — это указатель на контекст, который вы передаете в первый вызов функции ValidateAliBlocks. Подробнее об этой функции рассказано чуть ниже, в разделе "Глубокие проверки корректности" данной главы.

    Приведу две рекомендации по реализации метода ClassDumper. Во-первых, для того чтобы вывод форматированного дампа выполнялся в том же месте, что и вывод остальной части DCRT-библиотеки, нужно использовать макросы _RPTn и _RFTFn. Во-вторых, вывод дампов нужно заканчивать комбинацией символов CR/LF (Carriage Return/Line Feed — Возврат Каретки/Перевод Строки), потому что макросы DCRT-библиотеки не выполняют никакого форматирования.

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

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

    Расширение MemDumperValidator из DCRT-библиотеки значительно облегчает отладку памяти. По умолчанию DCRT-библиотека выдает сообщения об утечках памяти и проверках корректности тех блоков памяти, в которых оба типа записей (с начала — underwrites или с конца блока — overwrites) не подвергались разрушению. Оба отчета могут быть очень полезны, но если отчет об утечках памяти выглядит так, как показано ниже, то довольно трудно точно определить, в памяти какого типа произошла утечка:
    Detected memory leaks
    Dumping objects ->
    с:\vc\INCLUDE\crtdbg.h(552) : (596} normal block at Ox008CD5BO,
    24 bytes long.
    Data: < k w k > 90 6B 8C 00 BO DD 8C 00 00 00 80 77 90 6В 8С 00
    Object dump complete.
    Как говорилось ранее, средств проверки корректности памяти, заданных по умолчанию, может оказаться недостаточно. Для отлавливания некоторых специальных видов записей в память необходима более глубокая проверка корректности (иначе вы вообще не сможете перехватывать такие записи). Дополнительные проверки корректности и информация отчетов об утечках памяти, обеспечиваемые расширением MemDumperValidator, особенно полезны во время отладки. Чем больше информации получено во время отладки, тем меньше времени она занимает.
    Расширение MemDumperValidator использует в своей работе идентификаторы блоков памяти DCRT-библиотеки, что позволяет ему ассоциировать тип блока со специфическим набором функций работы с памятью, которым что-то известно о содержимом соответствующего блока. Каждому блоку памяти, распределенному через библиотеку DCRT, назначается специальный идентификатор, как показано в табл. 15.3. Типы блоков являются параметрами следующих функций распределения памяти библиотеки DCRT: _nh_maiioc_ dbg (new), _malloc_dbg (malloc), _calloc_dbg (calloc) И _realloc_dbg (realloc).
    Таблица 15.3. Идентификаторы блоков памяти
    Идентификатор блока Описание
    _NORMAL_BLOCK

    Вызов обычной функции распределения (new, malloc или calloc) создает нормальные блоки. Определение
    #define _CRTDBG_MAP_ALLOC
    приводит к тому, что все распределения кучи по умолчанию являются нормальными блоками и связывают с блоком памяти имя исходного файла и номер строки, содержащей вызов соответствующей функции распределения
    _CRT_BLOCK
    Блоки памяти, распределяемые многими функциями библиотеки времени выполнения, помечаются как CRT-блоки, поэтому они могут быть обработаны отдельно (иначе, чем блоки других типов). В частности, появляется возможность исключить такие блоки из процедур обнаружения утечек памяти и других операций проверок корректности. Ваше приложение никогда не должно распределять, перераспределять или освобождать блоки подобного типа
    CLIENT BLOCK
    Если нужно, чтобы приложение выполняло специальную трассировку блоков распределенной памяти, то можно вызывать особые отладочные функции распределения, передавая им в качестве параметра специальное значение CLIENT BLOCK VALUE (см. ниже вызов heap alloc dbg после директивы #define). Можно прослеживать подтипы клиентских блоков, помещая 1 6-разрядное значение в 16 верхних разрядов значения блока, как показано ниже:
    #define CLIENT_BLOCK_VALUE(x) \
    (_CLIENT_BLOCK | (x"16) )
    heap alloc dbg ( 10,
    CLIENT BLOCK VALUE ( OxA) ,
    _ FILE _ ,
    _ LINE _ ) ;
    Для дампов блоков памяти этого типа (т. е. памяти, зарегистрированной в форме клиентских блоков) приложение может обеспечить функцию-обработчик (через функцию CrtSetDumpClient). Функция-обработчик будет вызываться всякий раз, когда функции DCRT-библиотеки потребуется выполнить дамп клиент-

    (прод.)

    ского блока. Кроме того, имеется специальная функция (__CrtDoForAlldientObjects), которая позволяет получить список всех клиентских блоков, распределенных к моменту ее вызова. MFC использует данный идентификатор для всех классов производных от класса cobject. Расширение MemDumperValidator тоже использует описанный механизм

    FREE BLOCK

    Вызов подпрограммы освобождения памяти обычно удаляет память из списков отладочной кучи. Однако если при обращении к функции CrtSetDbgFlag вы устанавливаете флажок CRTDBG DELAY FREE MEM DF, то память не освобождается, а выравнивается влево и заполняется символами OxDD

    IGNORE BLOCK

    Если вы временно отключаете трассировку DCRT-библиотеки, то любые распределения, выполненные после этого, будут помечаться как блоки Ignore

    Расширение MemDumperValidator работает следующим образом: после установки подходящего класса (в языке C++) или типов данных (в языке С), DCR-библиотека будет вызывать MemDumperValidator всякий раз, когда нужно выполнить дамп блока памяти. Расширение просматривает значения данных в блоке и если находит подходящую дамп-функцию, то вызывает ее для выполнения дампа памяти. Аналогичным образом работает и та часть расширения, которая проверяет корректность данных в блоках памяти. Процесс проверки корректности отличается от процесса выдачи дампа только в том, что DCRT-библиотека вызывает не функции дампа, а функции проверки корректности данных соответствующего блока памяти.

    Описать MemDumperValidator не так уж сложно, но вот заставить его работать — немного сложнее. В листинге 15.1 показан заголовочный файл MEMDUMPERVALIDATOR.H, который выполняет основную часть работ по инициализации. Включая в программу файла BUGSLAYERUTIL.H, вы автоматически включаете и MEMDUMPERVALIDATOR.H.

    Листинг 15-1, JWEMDUMPERVAL1DATOR.H

    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - -

    "Debugging Applications" (Microsoft Press)

    Copyright (с) 1997-2000 John Robbins — All rights reserved.


    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    #ifndef _MEMDUMPERVALIDATOR_H

    #define _MEMDUMPERVALIDATOR_H

    // He включайте этот файл напрямую; вместо него включайте BUGSLAYER.H

    #ifndef _BUGSLAYERUTIL_H

    #error "Include BUGSLAYERUTIL.H instead of this file directly!"

    #endif // _BUGSLAYERUTIL_H

    // Включить заголовочный файл CRTDBG.H.

    #include "MSJDBG.h"

    #ifdef _cplusplus

    extern "C" {

    #endif // _ _cplusplus

    // Эту библиотеку можно использовать только в отладочных построениях.

    #ifdef _DEBUG

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

    // Директивы typedef для функций вьдачи дампов и проверки корректности

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

    // Функция выдачи дампов памяти. Единственный параметр этой функции -

    // указатель на блок памяти. Эта функция выводит данные блока памяти

    // одним из нескольких доступных ей способов, но, для того чтобы быть

    // состоятельной, она использует механизм формирования отчетов,

    // которым пользуется остальная часть DCRT-библиотеки.

    typedef void (*PFNMEMDUMPER)(const void *);

    // Функция проверки корректности (validating function).

    //Ее первый параметр — блок памяти,

    // корректность которого нужно проверить, а второй — контекстная

    // информация, пересылаемая в функцию ValidateAllBlocks function.

    typedef void (*PFNMEMVALIDATOR)(const void *, const void *);

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

    // Полезный макрос

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

    // Макрос, используемый для установки значения подтипа Client-блока.

    // Использование этого макроса — единственное санкционированное средство

    // установки значения поля dwValue в структуре DVINFO (см. ниже).

    tdefine CLIENT_BLOCK_VALUE(x) (_CLIENT_BLOCK|(x"16))

    // Макрос для выбора подтипа

    Idefine CLIENT_BLOCK_SUBTYPE(х) ((х " 16) & 0xFFFF)

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


    // Заголовок, используемый для инициализации функций дампа и проверки

    // корректности Client-блока специфического подтипа

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

    typedef struct tag_DVINFO

    {

    // Значение подтипа Client-блоков. Это значение должно быть

    // установлено с помощью определенного выше макроса.

    // CLIENT_BLOCK_VALUE. Чтобы выяснить, как расширение назначает

    // это число, см. функцию AddClientDV.

    unsigned long dwValue ;

    // Указатель на функцию дампа

    PFNMEMDUMPER pfnDump

    // Указатель на функцию проверки корректности

    PFNMEMVALIDATOR pfnValidate;

    } DVINFO, * LPDVINFO;

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : AddClientDV

    ОБСУЖДЕНИЕ :

    Добавляет в список функции дампа и проверки корректности

    Client-блока. Если поле dwValue в структуре DVINFO равно О,

    то назначается следующее значение из списка. Возвращаемое значение

    должно всегда пересылаться в функцию _malloc_dbg в качестве

    значения Client-блока.

    Если значение подтипа устанавливается с помощью макроса

    CLIENT_BLOCK__VALUE, то его можно использовать в качестве значения,

    передаваемого в функцию _malloc_dbg.

    Заметим, что соответствующей функции удаления не существует.

    Почему возникает риск введения ошибок в отладочный код? Проблема

    производительности отходит на задний план, когда речь заходит

    о поиске ошибок.

    ПАРАМЕТРЫ :

    IpDVInfo — Указатель на структуру DVINFO

    ВОЗВРАЩАЕТ :

    1 — функции дампа и проверки корректности клиентского блока были

    успешно добавлены.

    0 — функции дампа и проверки корректности клиентского блока не могут

    быть добавлены.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    int BUGSUTIL_DLLINTERFACE _stdcall AddClientDV (LPDVINFO IpDVInfo);

    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    ФУНКЦИЯ : ValidateAllBlocks

    ОБСУЖДЕНИЕ :

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


    Кроме

    того, просматривает все Client-блоки и вызывает специальные функции

    проверки корректности для различных подтипов этих блоков.

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

    ПАРАМЕТРЫ :

    pContext — Контекстная информация, которая будет передаваться

    в каждую функцию проверки корректности.

    ВОЗВРАЩАЕТ :

    Ничего.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/

    void BUGSOTIL_DLLINTERFACE _stdcall

    ValidateAllBlocks ( void * pContext);

    #ifdef _cplusplus

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

    // Макросы вспомогательных классов C++

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

    // Объявите этот макрос в своем классе как обычный MFC-макрос,

    #define DECLAREJYEMDEBUG(classname)

    public :

    static DVINFO m_stDVInfo;

    static void ClassDumper ( const void * pData);

    static void ClassValidator ( const void * pData,

    const void * pContext);

    static void * operator new ( size_t nSize)

    {

    if ( 0 == m_stDVInfo.dwValue)

    {

    m_stDVTnfо.pfnDump = classname::ClassDumper;

    m_stDVInfo.pfnValidate = classname::ClassValidator;

    AddClientDV ( &m_stDVInfo);

    }

    return ( _malloc_dbg ( nSize

    (int)m_stDVlnfо.dwValue,

    _FILE_ ,

    _LINE_ ) ) ;

    }

    static void * operator new ( size_t nSize ,

    char * IpszFileName,

    int nLine )

    {

    if ( 0 = m_stDVInfo.dwValue)

    {

    m_stDVInfo.pfnDump = classname::ClassDumper;

    m_stDVInfo.pfnValidate = classname::ClassValidator;

    AddClientDV ( &m_stDVInfo);

    }

    return ( _malloc_dbg ( nSize

    (int)m_stDVInfо.dwValue,

    IpszFileName ,

    nLine )) ;

    }

    static void operator delete ( void * pData)

    {

    _free_dbg ( pData, (int)m_stDVInfo.dwValue);

    }

    // Объявите этот макрос в начале своего СРР-файла.

    #define IMPLEMENT_MEMDEBUG(classname)

    DVINFO classname::m_stDVInfо = { 0, 0, 0 }

    // Макрос для отладочных распределений памяти.


    Если определено

    // символическое имя DEBUG_NEW, то этот макрос использовать нельзя.

    #ifdef DEBUG_NEW

    tdefine MEMDEBUG_NEW DEBUG_NEW

    #else

    #define MEMDEBUG_NEW new ( _FILE_, _LINE_)

    #endif

    #endif // идентификатор _cplusplus определен

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

    // Вспомогательные С-макросы

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

    // Используйте этот макрос для распределения памяти в С-стиле.

    // Единственной проблемой при этом является необходимость работы со

    // структурой DVINFO.

    Idefine INITIALIZE_MEMDEBUG(IpDVInfo, pfnD, pfnV)

    {

    ASSERT ( FALSE == IsBadWritePtr ( IpDVInfo,

    sizeof ( DVINFO)));

    ((LPDVINFO)IpDVInfo)->dwValue = 0;

    ((LPDVINFO)IpDVInfo)->pfnDump = pfnD;

    ((LPDVINFO)IpDVInfo)->pfnValidate = pfnV;

    AddClientDV ( IpDVInfo);

    }

    // Макросы, которые преобразуют функции распределения памяти С-формата

    //в более удобную для применения форму. Он избавляет вас от

    // запоминания и кодирования различных значений блока DVINFO,

    // используемых в функциях работы с памятью.

    #define MEMDEBUG_MALLOC(IpDVInfo, nSize) \

    _malloc_dbg ( nSize , \

    ((LPDVINFO)IpDVInfo)->dwValue, \

    _FILE_ , \

    _LINE_ )

    #define MEMDEBUG_REALLOC(IpDVInfo, pBlock, nSize) \

    __realloc_dbg ( pBlock , \

    nSize , \

    ((LPDVINFO)IpDVInfo)->dwValue , \

    _FILE_ , \

    _LINE_ )

    #define MEMDEBUG_EXPAND(IpDVInfo, pBlock, nSize) \

    _expand_dbg( pBlock , \

    nSize , \

    ' ((LPDVINFO)IpDVInfo)->dwValue , \

    _FILE_ , \

    _LINE_ )

    #define MEMDEBUG_FREE(lpDVInfo, pBlock) \

    _free_dbg ( pBlock , \

    ((LPDVINFO)IpDVInfo)->dwValue)

    #define MEMDEBUG_MSIZE(IpDVInfo, pBlock} \

    _msize_dbg ( pBlock, ((LPDVINFO)IpDVInfo)->dwValue)

    // Макрос для вызова функции ValidateAllBlocks

    #define VALIDATEALLBLOCKS(x) ValidateAllBlocks ( x)

    #else // _DEBUG не определен.

    #ifdef _cplusplus

    #define DECLARE_MEMDEBUG(classname)

    #define IMPLEMENT_MEMDEBUG(classname)

    #define MEMDEBUG_NEW new

    #endif // _cplusplus

    #define INITIALIZE_MEMDEBUG(IpDVInfo, pfnD, pfnV)

    #define MEMDEBUG_MALLOC(IpDVInfo, nSize) \

    malloc ( nSize)

    #define MEMDEBUG_REALLOC(IpDVInfo, pBlock, nSize) \

    realloc ( pBlock, nSize)

    #define MEMDEBUG_EXPAND(IpDVInfo, pBlock, nSize) \

    _expand ( pBlock, nSize)

    #define MEMDEBUG_FREE(IpDVInfo, pBlock) \

    free ( pBlock) ttdefine MEMDEBUG_MSIZE(IpDVInfo, pBlock) \

    _msize ( pBlock)

    #define VALIDATEALLBLOCKS(x)

    #endif // _DEBUG ttifdef _cplusplus

    }

    #endif // _cplusplus

    #endif // _MEMDUMPERVALIDATOR_H

    Использование утилиты MemStress

    Теперь самое время добавить немного стресса. Как ни странно, но он может играть и положительную роль. К сожалению, довести до этого состояния 32-разрядные приложения Windows в наши дни намного труднее, чем раньше. Приложения прежних 16-разрядных Windows (да еще и современной 16-разрядной подсистемы Windows 98) можно выполнять под управлением изящной программы STRESS.EXE, которая поставляется в составе SDK и позволяет вводить в приложение различного рода искажения. Например, такие, которые заставляют ее буквально "пожирать" дисковое пространство и динамическую память (heap) интерфейса графических устройств (Graphics Device Interface — GDI), а также усиленно использовать файловые дескрипторы. Программа имеет подходящую пиктограмму — изображение слона, идущего по натянутому канату.
    Чтобы подвергнуть "стрессу" 32-разрядные приложения Windows, можно подключиться к системе распределения памяти DCRT-библиотеки и контролировать результаты (успехи или неудачи) ее операций. MemStress обеспечивает средства для усиленного распределения памяти в С/С++-приложениях. (Оставлю на усмотрение читателя написание кода, "съедающего" дисковую память.) Чтобы облегчить применение MemStress, я написал интерфейс (на языке Visual Basic), позволяющий точно задать условия отказов.
    MemStress обеспечивает принудительное создание отказов в распределениях (памяти), основанных на различных критериях: для всех распределений, для каждого n-ого распределения, после распределения определенного числа байт, по запросам через каждые n байт, для всех распределений вне исходного файла и на конкретной строке в исходном файле. Кроме того, можно заставить MemStress выдавать на любом запросе распределения подсказку в виде панели сообщения, в которой спрашивается, хотите ли вы получить отказ в данном конкретном распределении. Можно также устанавливать флажки DCRT-библиотеки, которые желательным образом влияли бы на программу. MFC-программа MemStressDemo (проект которой поставляется на сопровождающем компакт-диске) позволяет экспериментировать с установкой различных опций пользовательского интерфейса (UI) MemStress и просматривать соответствующие результаты.

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

    Скомпилировав свою программу, запустите MemStress UI, нажмите кнопку Add Program и введите (с клавиатуры) то же самое имя, которое указано в макросе MEMSTRESSINIT. После выбора желательных опций отказов нажмите кнопку Save Settings For This Program, чтобы сохранить установки в файле MEMSTRESS.INI. Теперь можно запускать программу и следить за ее поведением при отказах в распределении памяти.

    Применять MemStress следует аккуратно. Например, если потребовать, чтобы терпели неудачу все распределения, превышающие 100 байт, при условии, что в функции initinstance вашего MFC-приложения имеется макрос MEMSTRESSINIT, то можно будет заподозрить MFC в неспособности инициализировать свои объекты. Лучшие результаты вы получите, если ограничите область действия MemStress тестированием отдельных ключевых областей программы.

    Большая часть кода MemStress занимается чтением и обработкой файла MEMSTRESS.INI, в котором хранятся все установки для индивидуальных программ. С точки зрения DCRT-библиотеки, наиболее важной функцией является обращение к функции _CrtSetAilocHook во время инициализации MemStress, потому что для обработки распределений памяти этот вызов устанавливает функцию AllocationHook. Если эта функция возвращает TRUE, то запрос распределения продолжается, а возврат FALSE означает, что DCRT-библиотека отклонила этот запрос. К обработке распределения со стороны DCRT-библиотеки имеется только одно твердое требование: если параметр nBiockUse задает тип блока _CRT_BLOCK, то hook-функция должна возвратить значение TRUE, что позволит распределять блоки этого типа.

    Обработка распределения выполняется для функций распределения любого типа. Тип функции указан в ее первом параметре, для которого допустимы следующие значения: _HOOK_ALLOC, _HOOK_REALLOC и _HOOK_FREE).Если в своей функции обработки распределения AllocationHook указать тип _HOOK_FREE, то будет пропущен весь код, который определяет, должен ли запрос памяти передаваться или отклоняться. Для типов _HOOK_ALLOC и _HOOK_REALLOC функция AiiocationHook выполняет ряд if-операторов, чтобы определить, встретилось ли какое-нибудь из условий отказа в распределении. Если условие отказа встретилось, функция возвращает FALSE.

    Куда направляются отчеты об утечках памяти?

    Решив проблему инициализации, удалось, наконец, получить работающее расширение MemDumperValidator. И все работало хорошо — за исключением того, что функции, выдающие дампы при обнаружении утечек памяти, по завершении программы не генерировали удобно отформатированного вывода. Дампы памяти оказывались стандартными дампами DCRT-биб-лиотеки. Прослеживание "потерявшихся" отчетов об утечках показало, что функции завершения DCRT-библиотеки вызывают функцию _crtsetDumpclient с параметром NULL (пустой указатель), очищая, таким образом, подключение дампа перед вызовом функции _crtDumpMemoryLeaks. Стало понятно, что нужно просто самому выполнить заключительную проверку утечки памяти. К счастью, у меня нашлось подходящее место, чтобы выполнить эту операцию.
    Общий Вопрос Отладки
    Зачем нужна отладочная DCRT-библиотека, если используется инструмент обнаружения ошибок типа утилиты BoundsChecker?
    Инструменты обнаружения ошибок, такие как BoundsChecker от Compuware NuMega и Purify от Rational Software автоматически обрабатывают записи, перезаписи (underwrites и overwrites) и утечки памяти. Если вы используете один из этих инструментов, то, вероятно, думаете, что на DCRT-библиотеку не стоит тратить время и усилия. Технически это так, но, чтобы гарантированно разрешить все проблемы с памятью, необходимо запускать приложение под управлением инструмента обнаружения ошибок каждый раз, когда кто-то из команды разработчиков выполняет отладочное построение (debug build) приложения. Таким партнером по отладке может быть кто угодно — вы сами, ваши коллеги разработчики и, если вы следовали рекомендациям из главы 2, то даже сотрудники отдела контроля качества.
    Использование DCRT-библиотеки подобно наличию хорошего страхования от пожара или грабежа. Все надеются, что им никогда не понадобится страховка, но, застраховавшись, можно предохранить себя от многих неприятностей. Проверку корректности данных в приложении необходимо выполнять при любой возможности. DCRT-библиотека не наносит большого ущерба производительности приложения, но помогает находить некоторые серьезные ошибки.
    Надо всегда работать с этой библиотекой, какими бы дополнительными инструментами отладки вы ни пользовались.

    Поскольку директива #pragma init_seg(compiler) уже использовалась для предварительной инициализации класса AutoMatic (и вызова деструктора после выполнения самого последнего оператора приложения), то в этой же точке исходного кода нужно было выполнять и проверку утечки памяти, а затем выключать флажок _CRTDBG_LEAK_CHECK_DF, чтобы DCRT-библиотека не выводила собственных отчетов. Единственное предостережение — при использовании этого подхода необходимо удостовериться, что выбранная CRT-библиотека включается перед файлом BUGSLAYERUTIL.LIB (если компоновка выполняется с ключом /NODEFAULTLIB). При компоновке файла BUGSLAYERUTIL.LIB может оказаться, что CRT-библиотеки не зависят от соответствующих директив #pragma init_seg(compiler), гарантирующих, что данные этих библиотек инициализируются первыми, а разрушаются последними, так что поддерживать правильный порядок в размещении этих директив программист должен самостоятельно.

    Здесь речь идет о размещении указанной директивы перед исходным кодом отлаживаемого приложения. — Пер.

    Если внимательно подумать, то установка режима чистки обработчиков функций вывода дампов DCRT-библиотекой имеет определенный смысл. Если бы ваш обработчик использовал какую-нибудь функцию из CRT-библиотеки (скажем, printf), то он мог бы завершить вашу программу аварийно, потому что, когда вызывается функция _crtDumpMemoryLeaks, CRT-библиотека находится где-то в середине процесса завершения своей работы. Надо соблюдать правила и всегда компоновать DCRT-библиотеку перед компоновкой любых других библиотек, т. к. функции расширения MemDumperValidator завершаются прежде, чем заканчивает работу DCRT-библиотека. Чтобы обойти подобные проблемы, применяйте в дамп-функциях только макросы _RPTn и _RPTFH, потому что их использует и функция _CrtDumpMemoryLeaks.

    Применение MemDumperValidator в приложениях С

    Можно задать вопрос — почему я занимаюсь поддержкой языка С? Ответ прост: многие программные продукты, с которыми мы работаем, еще используют С-коды. И, хотите верьте, хотите — нет, некоторые из этих приложений и модулей тоже располагаются в памяти.
    Чтобы использовать MemDumperValidator в С-приложении, необходимо придерживаться определенной последовательностью действий. Во-первых, нужно объявить DVINFO-структуру для каждого типа памяти, который вы хотите контролировать через вывод дампа и проверку корректности. В C++ соответствующий макрос автоматически объявляет методы дампов и проверок корректности, но в С следует самостоятельно выполнить некоторую дополнительную работу. Имейте в виду, что все макросы, о которых здесь идет речь, используют указатель на конкретные DVINFO-структуры. Прототипы С-функций вывода дампов и проверки корректности — те же, что и у соответствующих методов в C++, за исключением того, что ключевое слово static в них не используется. Объявления DVINFO-структур для специфических блоков памяти и определения всех С-функций вывода дампов и проверки корректности удобно размещать в объединенном файле. Перед началом конкретных операций работы с памятью1нужно сообщить расширению MemDumperValidator о подтипах используемых клиентских блоков и их функциях вывода дампов и проверки корректности. Всю эту информацию следует передавать в MemDumperValidator с помощью макроса INITIALIZE_MEMDEBUG, который принимает в качестве параметров' соответствующую DVINFO-структуру, функцию вывода дампа и функцию проверки корректности. Этот макрос нужно выполнять перед началом распределения блоков памяти соответствующего типа.
    К операциям работы с памятью относятся следующие операции, выполняемые над блоками памяти: выделение/освобождение/повторное выделение (ailocation/free/reallocation), дамп (dump), проверка корректности содержимого (validation), получение и расширение размера блока (get/expand block size). — Пер
    Для выполнения операций обработки блоков памяти в С нужно использовать полный набор макросов, которые передают значения блоков памяти соответствующим функциям обработки. Например, если в программе определена DVINFO-структура stdvBiockinfo, то распределения блоков памяти в С-программе следует выполнять с помощью следующего кода:
    MEMDEBUG_MALLOC(&stdvBlockInfo, sizeof (х) ) ;
    Все макросы для С-функций, работающих с памятью, можно найти в заключительной части листинга 15-1. Запоминать DVINFO-структуры для каждого типа распределения памяти хотя и возможно, но не очень практично, поэтому для обработки различных DVINFO-структур можно определять специальные макросы-оболочки; таким макросам нужно передавать параметры обычных функций работы с памятью.


    Реализация MemDumperValidator

    Реализация функций расширения MemDumperValidator не очень сложна. Первая неожиданная проблема, с которой мне пришлось столкнуться, состояла в том, что DCRT-библиотека не документирует способ, с помощью которого hook-функции получают значение блока памяти. Этим функциям передается только указатель на данные пользователя, а не весь блок памяти, который распределяет DCRT-библиотека. К счастью, наличие исходного кода DCRT-библиотеки дало возможность точно видеть, как библиотека распределяет блоки памяти. Все блоки памяти распределяются в виде структуры _GrtMemBiockHeader, которая определена в файле DBGINT.H. Кроме того, в файле DBGINT.H определены макросы, обеспечивающие доступ к полям структуры _GrtMemBiockHeader по указателю данных пользователя и, наоборот, доступ к данным пользователя по указателю этой структуры. Чтобы иметь возможность получать информацию заголовков, я скопировал структуру _CrtMemBiockHeader и макрос доступа в файл заголовков CRTDBG_INTERNALS.H (показанный в листинге 15-3). Полагаться на копию определения структуры, когда это определение может изменяться — не очень хорошая практика, но в данном случае это работает, потому что структура _crtMemBiockHeader DCRT-библиотеки не изменялась в версиях Visual C++ от 4-й до 6-й. Однако это не означает, что данная структура не будет изменена в будущей версии Visual C++. При использовании расширения MemDumperValidator нужно быстро проверять каждый пакет обслуживания и главный релиз компилятора (major release of the compiler), чтобы видеть, изменились ли их внутренние структуры.
    Для того чтобы непосредственно использовать DBGINT.H, можно заменить определение структуры в файле CRTDBG_INTERNALS.H директивой #indude DBGINT.H. В этом случае нужно будет также добавить каталог \CRT\SRC как к главной переменной окружения INCLUDE, так и в список каталогов Include-файлов на вкладке Directories диалогового окна Options из IDE Visual C++. Поскольку не каждый разработчик устанавливает исходный код CRT-библиотеки, а многие не упоминают об этой структуре в файле README.TXT, то было принято решение использовать прямое включение определения этой структуры.

    Листинг 15-3.CRTDBG_INTERNALS.H

    /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    "Debugging Applications" (Microsoft Press)

    Copyright (c) 1997-2000 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 ;

    size_t nDataSize ;

    int nBlockUse ;

    long IRequest ;

    unsigned char gap[nNoMansLandSize] ;

    /* followed by:

    * unsigned char data[nDataSize];

    * unsigned char anotherGap[nNoMansLandSize];

    */

    } _CrtMemBlockHeader;

    #define pbData(pblock) ((unsigned char *) \

    ((_CrtMemBlockHeader *)pblock + 1) )

    tfdefine pHdr(pbData) (((_CrtMemBlockHeader *)pbData)-l)

    #endif // _CRTDBG_INTERNALS_H

    Можно также использовать определение структуры _CrtMemBiockHeader, чтобы получить дополнительную информацию из структур _crtMemstate, возвращаемых функцией _crtMemCheckpoint, потому что первым элементом в этой структуре является указатель на _crtMemBiockHeader. Надеюсь, что будущая версия DCRT-библиотеки обеспечит реальные функции доступа для получения информации о блоках памяти.

    Просматривая поставляемый на сопровождающем компакт-диске исходный код файла MEMDUMPERVALIDATOR.CPP, являющегося частью проекта BUGSLAYERUTIL.DLL, можно заметить, что для внутреннего управления памятью применяются API-функции семейства HeapCreate, напрямую работающие с кучей. Причина в том, что функции дампов и hook-функции, используемые с DCRT-библиотекой, будут вызываться повторно, если применяются подпрограммы стандартной библиотеки. Имейте в виду, что мы не говорим о многопоточных повторных входах. Если код обработчика распределяет память с помощью обращения к функции malloc, то он будет введен повторно, потому что hook-функции вызываются при каждом распределении памяти.

    Свойства библиотеки DCRT

    Главной причиной популярности DCRT-библиотеки в том, что она поддерживает мониторинг кучи (heap). В отладочных версиях можно проследить всю память, которая распределяется при помощи стандартных С/С++-функций, таких как new, maiioc и calloc. Монитор кучи проверяет как записи (underwrites), которые программа помещает в начало выделенного блока памяти, так и перезаписи (overwrites), размещаемые за концом блока. Этот механизм контролирует также и утечки памяти в приложении (и выдает соответствующие отчеты). Те, кто писал программы с использованием библиотеки MFC, вероятно, знакомы с отчетами об утечке памяти, генерируемыми при завершении приложения частью библиотеки DCRT, которую MFC автоматически включает в приложение.
    Overwrite — обычно обозначает перезапись или наложение одной записи поверх другой. По определению автора (program writes past the end of block memory), в перезапись вкладывается несколько иной смысл: запись за концом блока памяти, что, по-видимому, означает выход записываемых данных за пределы выделенного им блока памяти. — Пер
    Подсистема отчетов DCRT формирует трассу памяти (с помощью макросов __RPTn и RPTFn, а также поддержки утверждений). Поддержка таких утверждений библиотекой DCRT описана в главе 3, где рассмотрены также некоторые проблемы, связанные с их использованием. Напомним, что утверждения библиотеки DCRT — это довольно сильное средство, но они разрушают значение последней ошибки, что приводит к различному поведению отладочных и выпускных построений. Я настоятельно рекомендую пользоваться утверждениями программы SUPERASSERT, которая является частью BUGSLAYERUTIL.DLL.
    Полезной особенностью DCRT-библиотеки является включение ее исходного кода в компилятор. В табл. 15.1 перечислены все файлы, входящие в состав DCRT-библиотеки. Если исходный код библиотеки CRT выполняется во время установки Microsoft Visual Studio (что я настоятельно рекомендую делать), то все исходные коды библиотек CRT и DCRT можно найти в каталоге ..\Microsoft Visual Studio\VC98\CRT\SRC.

    Таблица 15.1. Исходные файлы DCRT-библиотеки.
    Исходный файл
    Описание
    DBGDEL.CPP
    Глобальная отладочная операция delete
    DBGHEAP.C
    Все функции обработки кучи
    DBGHOOK.C
    Функция подключения заглушки распределения памяти
    DBGINT.H
    Внутренние отладочные заголовки и функции
    DBGNEW.CPP
    Глобальная отладочная операция new
    DBGRPT.C
    Функции отладочных сообщений
    CRTDBG.H
    Файл заголовков, который нужно включать в приложение, пользующееся DCRT-библиотекой. Этот файл находится в стандартном каталоге \lnclude


    Выбор правильного варианта CRT-библиотеки

    Некоторая неразбериха вокруг применения CRT-библиотек при разработке приложений Windows связана с тем, что необходимо решать, какую библиотеку следует использовать. Существует шесть версий этой библиотеки, которые можно разделить на две категории: отладочную (DCRT) и выпускную (CRT). В каждую категорию входит однопоточная статическая библиотека, многопоточная статическая библиотека и многопоточная библиотека динамической компоновки (DLL).
    Статические версии CRT-библиотек включают библиотечные функции прямо в приложение. Эти версии используются по умолчанию для не-МРС-приложений, которые собираются с помощью мастеров. Преимущество статических версий заключается в том, что DLL таких библиотек не нужно отправлять заказчику вместе с разработанным продуктом. Недостатком же является то, что сильно возрастает размер двоичных файлов, так что рабочая конфигурация приложения становится довольно большой. Однопоточный и многопоточный варианты статической библиотеки CRT не требуют объяснений. Если вы создаете DLL и хотите использовать статическую CRT-библиотеку, то нужно выполнять компоновку только с многопоточной версией этой библиотеки (в противном случае приложение не сможет использовать вашу DLL, потому что однопоточные статические CRT-библиотеки не являются потокобезопасными).
    DLL-версии библиотек CRT с именами MSVCRT(D).DLL позволяют импортировать их функции. Большим преимуществом этих DLL является то, что размер двоичных файлов радикально уменьшается, значительно сокращая, таким образом, рабочий набор приложения.
    Поскольку другие приложения загружают одну и ту же DLL-версию библиотеки CRT, операционная система может разделять таблицы страниц кодовых секций DLL между процессами, так что вся система будет выполняться быстрее. К недостатком таких DLL-версий следует отнести то, что приложения могут быть ориентированы на другие варианты DLL, поэтому вместе с ними придется распространять и соответствующие варианты DLL.
    Для приложений чрезвычайно важен выбор одной версии CRT-библиотеки, используемой всеми двоичными файлами, которые загружаются в адресное пространство их главной программы.
    Если приложение работает с несколькими DLL, каждая из которых использует свою статическую CRT-библиотеку, то мало того что увеличивается адресное пространство за счет дублирования кода, но также повышается риск введения одной из наиболее коварных ошибок, которую можно отслеживать месяцами. Если вы распределяете область кучи в одной DLL, а пытаетесь освобождать ее в другой, использующей иную версию библиотеки CRT, то легко можно вызвать аварийное завершение, потому что DLL, освобождающая память не знает, откуда взялась распределяемая память. Было бы ошибкой полагать, что в данном случае мы имеем дело с обычной кучей, т. к. наличие различных версий CRT-библиотеки, выполняющихся одновременно, означает, что имеется много версий кода управления кучей.

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

    В утилите BUGSLAYERUTIL.DLL используются DLL-версии библиотек CRT. Кроме того, в BUGSLAYERUTIL.DLL включено два расширения DCRT-библиотеки: MemDumperValidator и MemStress, которые рассмотрены ниже в этой главе. Ожидается, что вы пользуетесь DLL-версиями этих расширений. Однако если вы хотите, чтобы приложение работало с модулями ЕХЕ, а не их DLL-версиями, то нужно просто взять исходные файлы MEMDUMPERVALIDATOR.CPP, MEMDUMPERVALIDATOR.H, а также MEMSTRESS.CPP, MEMSTRESSCONSTANTS.H и MEMSTRESS.H, изменить компоновку указанных функций и поместить их в приложение.

    Хочу обратить ваше внимание на одну дополнительную деталь применения BUGSLAYERUTIL.DLL. Можно наблюдать некоторое замедление приложения в зависимости от того, как распределяется память.Чтобы разрешить полное прослеживание и проверку корректности памяти, я устанавливаю в расширении MemDumperValidator все подходящие флажки DCRT-библио-теки, включая _CRTDBG_CHECK_ALWAYS_DF. Установка этого флажка приводит к тому, что каждый раз, когда вы распределяете или освобождаете кучу, DCRT-библиотека просматривает каждый ее участок и проверяет корректность данных. Если в приложении происходят тысячи распределений небольших областей памяти, то его выполнение ощутимо замедлится. Если замедление недопустимо, то возможны два решения. Первое — перед выполнением распределений памяти нужно сбросить флажок _CRTDBG_CHECK _ALWAYS_DF (обратившись к функции _GrtsetDbgFiag). Второе — проверить алгоритм и посмотреть, нужны ли вообще распределения небольших участков памяти, потому что даже без проверки отладочной кучи операции выделения памяти чрезвычайно замедляют приложение

    .

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

    Первая секция журнала выглядит так:


    Первая секция журнала выглядит так:

    Microsoft (R) Windows 2000 (ТМ) Version 5.00 DrWtsn32

    Copyright (С) 1985-1999 Microsoft Corp. All rights reserved.

    Application exception occurred:

    App: (pid=252)

    When: 9/4/1999 @ 16:43:56.173

    Exception number: cOOOOOOS (access violation)

    Информация заголовка сообщает причину аварийного останова. В данном случае — это исключительная ситуация (исключение), возникшая в приложении. Номера исключений для некоторых аварийных ситуаций невозможно перевести в удобочитаемое описание, например, такое, как показано в последней строке нашего заголовка ("access violation — нарушение доступа" для исключения с (шестнадцатеричным) номером С0000005). Все возможные значения номеров исключений можно найти в препроцессорных директивах #define STATUS_ файла WINNT.H. Значения, отмеченные в документации, как данные типа EXCEPTION_, возвращает функция GetExceptionCode, но реальные величины находятся в директивах #define STATUS_. Функция GetExceptionCode преобразует номер исключения в ЕХСЕРТION_-значение, а по ее документации можно найти более подробное описание данного сбоя.

    Секция system information (сведения о системе) самоочевидна и не требует пояснений:

    * - - -> System Information < - - -*

    Computer Name: PLATO

    User Name: John

    Number of Processors: 1

    Processor Type: x86 Family 6 Model 6 Stepping 10

    Windows 2000 Version: 5.0

    Current Build: 2128

    Service Pack: None

    Current Type: Uniprocessor Free

    Registered Organization: Enter your company name here

    Registered Owner: John Robbins

    Секция Task List (список задач) выглядит примерно так:

    * - - - > Task List < - - -*

    0 Idle.ёхе

    8 System.exe

    132 smss.exe

    160 csrss.exe

    156 winlogon.exe

    208 services.exe

    220 lsass.exe

    364 svchost.exe

    424 svchost.exe

    472 spoolsv.exe

    504 MWMDMSVC.exe

    528 MWSSW32.exe

    576 regsvc.exe

    592 MSTask.exe

    836 Explorer.exe

    904 tp4mon.exe

    912 tphkmgr.exe

    920 4nt.exe
    940 taskmgr.exe

    956 tponscr.exe

    268 msdev.exe

    252 WDBG.exe

    828 NOTEPAD.exe

    416 drwtsn32.exe

    0 _Total.exe

    В этой секции показан список процессов, которые выполнялись во время сбоя. К сожалению, Windows 2000 не показывает информацию о версиях, включенных в этот список файлов, поэтому в случае необходимости нужно получить эту информацию у пользователя. Числа слева от имен программных файлов — это десятичные идентификаторы процессов (PID — Program ID).

    Следующий список содержит адреса загрузки всех модулей, находившихся в адресном пространстве во время аварии:

    (00400000 - 0042D000) D:\Dev\Book\CD\SourceCode\Output\WDBG.pdb

    (77F80000 - 77FF9000) E:\WINNT\symbols\dll\ntdll.dbg

    (60000000 - 6001А000) D:\Dev\Book\CD\SourceCode\Output\BugslayerUtil.pdb

    (77Е80000 - 77F35000) E:\WINNT\symbols\dll\kernel32.dbg

    (77Е10000 - 77Е74000) E:\WINNT\symbols\dll\user32.dbg

    (77F40000 - 77F7C000) E:\WINNT\symbols\dll\gdi32.dbg

    (72950000 - 72967000) E:\WINNT\symbols\dll\dbghelp.dbg

    (78000000 - 78046000)

    • (77DB0000 - 77Е07000) Е:\WINNT\symbols\dll\advapi32.dbg

    (77D30000 - 77DA2000) E:\WINNT\symbols\dll\rpcrt4.dbg

    (10200000 - 10264000)

    (63100000 - 63108000) D:\Dev\Book\CD\SourceCode\Output\LocalAssist.pdb

    (62000000 - 6202В000) D:\Dev\Book\CD\SourceCode\Output\i386CPUHelp.pdb

    (63000000 - 63010000) D:\Dev\Book\CD\SourceCode\Output\LocalDebug.pdb

    (5F400000 - 5F4E5000)

    (77ВЗ0000 - 77ВВА000) E:\WINNT\symbols\dll\comctl32.dbg

    (775А0000 - 777DE000) E:\WINNT\symbols\dll\shell32.dbg

    (77С50000 - 77С9А000) E:\WlNNT\symbols\dll\shlwapi.dbg

    (76В20000 - 76В5Е000) E:\WINNT\syitibols\dll\comdlg32.dbg

    (77АЗ0000 - 77В24000) E:\WINNT\symbols\dll\ole32.dbg

    (77990000 - 77А24000) E:\WINNT\symbols\dll\oleaut32.dbg

    (77СА0000 - 77D25000)

    (77850000 - 7788В000)

    (770В0000 - 770D3000)

    (6В6Е0000 - 6B6FC000) E:\WINNT\symbols\dll\msdbi.dbg


    (68ED0000 - 68EDB000) E:\WINNT\symbols\dll\psapi.dbg

    Числа в круглых скобках в левой части этого списка указывают начальный и максимальный адреса загрузки каждого модуля. Намного полезнее было бы показать в этой секции фактические имена модулей, загруженных в адресное пространство. К сожалению, как видно по именам, расположенным справа от адресов загрузки, здесь показаны только каталоги, в которые Dr. Watson загрузил файлы отладочных символов соответствующего модуля. Незаполненные входы означают, что никакой отладочной информации для данного модуля загружено не было.

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

    Ниже показана первая часть дампа состояния потока (с идентификационным номером Ox2fc):

    State Dump for Thread Id 0x2fс

    eax=00000000 ebx=7ffdfOOO ecx=008c67cO edx=0000033c esi=00134c78 edi=0012fd74

    eip=0040bd2d esp=0012fb98 ebp=0012fbc4 iopl=0 nv up ei pi nz na pe nc

    cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202

    function: CWDBGProjDoc::HandleBreakpoint

    0040bdll push esi

    0040bdl2 push edi

    0040bdl3 mov eax,Oxcccccccc

    0040bdl8 mov [ebp+OxeO],eax ss:00b4dl9a=????????

    0040bdlb mov [ebp+0xe4],eax ss:00b4dl9a=????????

    0040bdle mov [ebp+0xe8],eax ss:00b4dl9a=????????

    0040bd21 mov [ebp+Oxec],eax ss:00b4dl9a=????????

    0040bd24 mov [ebp+Oxf0],eax ss:00b4dl9a=????????

    0040bd27 mov [ebp+OxeO],ecx ss:00b4dl9a=????????

    0040bd2a mov eax,[ebp+Oxc] ss:00b4dl9a=????????

    FAULT ->004Obd2d mov ecx,[eax+0x4] ds:00ald5d6=????????

    0040bd30 cmp dword ptr [ecx],0x80000003 ds:008c67cO=0041b714

    0040bd36 jz CArray::SetSize+Ox25d (0041485d)

    0040bd38 mov esi,esp


    0040bd3a push 0x382

    0040bd3f push 0x420030

    0040bd44 push 0x420064

    0040bd49 push 0x0

    0040bd4b call dword ptr [_imp_DiagAssertA (00423ad4)]

    0040bd51 cmp esi,esp

    0040bd53 call _chkesp (00416b5a)

    0040bd58 test eax,eax

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

    В регистровой части дампа показано содержимое всех регистров во время сбоя. Важно следить за регистром EIP (указателем машинных команд). В данном примере используются отладочные символы, так что можно видеть, какую функцию выполнял этот поток во время сбоя, однако довольно часто журналы этой программы не могут пользоваться отладочными символами1. Нет ничего страшного, если в журнале нет имени функции. В этом случае нужно загрузить для своего приложения проект программы CrashFinder, рассмотренной в главе 8, нажать клавиши + и ввести в открывшееся диалоговое окно Find EIP-адрес этого потока.

    Тогда вместо имени функции в журнале указывается значение . — Пер.

    Так уж случилось, что сбой произошел в этом потоке. Единственным индикатором этого события в журнале является указатель FAULT-> в середине кода дизассемблера. Иногда этот указатель не выводится. Тогда, для того чтобы определить, что происходило с потоком во время сбоя, нужно просмотреть (по журналу) состояние каждого потока и ввести соответствующий EIP-адрес в CrashFinder.

    Код дизассемблера рассмотрен в главе 6. Новыми элементами являются значения, показанные справа от команд. Дизассемблер программы Dr. Watson пытается найти самый эффективный адрес для ссылки на команду. Справа от команды можно увидеть, с каким еще адресным значением работала команда. Метка ss: указывает на адреса сегмента стека, а ds: — на адреса сегмента данных.

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


    Например, пусть первая команда, дизассемблированная в дампе состояния потока, использовала (в качестве аргумента) ссылку на ячейку памяти из регистра ЕВХ. Если сбой дала команда, расположенная на 10 инструкций позже, то одна из промежуточных команд может легко изменить содержимое ЕВХ. Однако когда Dr. Watson выполняет дизассемблирование, то для эффективного преобразования адреса он использует текущее значение ЕВХ (т. е. то, которое этот регистр содержал во время сбоя). По этой причине эффективный адрес, показанный в коде дизассемблера дампа состояния потока, может быть неправильным. Для того чтобы убедиться, что в дампах журнала отображены корректные адреса, тщательно проверьте, не могли ли какие-нибудь команды, предшествующие сбою, изменить значения регистров.

    Можно вычислить, что в данном потоке произошел сбой, потому что команда:

    Ox0040BD2D MOV ECX,[ЕАХ+Ох4]

    пыталась получить доступ к ПУСТОМУ (NULL) указателю в ЕАХ (ЕАХ=00000000). Посмотрев внимательно на команду в предшествующей строке:

    MOV ЕАХ, [ЕВР+ОхС],

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

    Ниже показана вторая часть дампа состояния потока, которая названа stack Back Trace (обратная трассировка стека) (). Заметьте, что длинные значения столбца Function Name (и только они) там, где они не поместились в строке, перенесены на следующую строку.

    *——> Stack Back Trace <——*

    FramePtr ReturnAd Paramtl Param#2 Param#3 Param#4 Function Name

    0012FBC4 0040BCB5 0000033C 00000000 80000003 008C67CO

    !CWDBGProj Doc::HandleBreakpoint

    0012FBEO 00405A9C 0000033C 010DFCOC 008C68BO 0012FCF4

    !CWDBGProj Doc::HandleExceptionEvent


    0012FBF4 5F42F3AC 0000033C 010DFCOC 0012FD74 00134C78

    !CDocNotifyWnd::HandleExceptionEvent

    0012FCF4 5F42ECE8 00000502 0000033C 010DFCOC 0012FD10 !0rdinal4118 0012FD14 5F42C889 00000502 0000033C 010DFCOC 77F86-618 !0rdinal5076 0012FD88 5F42CD25 008C68BO 000602A4 00000502 0000033C !0rdinall045 0012FDB4 5F4905FD 000602A4 00000502 0000033C 010DFCOC !0rdinalll92 0012FDE4 77E135F8 000602A4 00000502 0000033C 010DFCOC !0rdinalll93 0012FE04 77E15FE8 5F4905B3 000602A4 00000502 0000033C

    user32!UserCallWinProc

    0012FE20 77E1600E 004A9B70 00000502 0000033C 010DFCOC

    user32!DispatchClientMessage (FPO: Non-FPO

    [5,1,0])

    0012FE48 77F9D8B7 0012FE58 00000018 004A9B70 00000502

    user32!_fnDWORD (FPO: Non-FPO [1,4,0])

    0012FE68 77E15FB5 77E17BD9 00422250 00000000 00000000

    ntdlliKiUserCallbackDiSpatcher (FPO: [0,0,0])

    0012FE90 5F4396F8 00422250 00000000 00000000 00000000

    user32!DispatchClientMessage (FPO: Non-FPO [5,1,0])

    0012FEB8 5F438E1D 77F86618 77F81A9B 7FFDFOOO 00000001 !0rdinal4239 0012FEDC 5F439AD4 00422218 0012FF08 5F43366E 77F86618 !0rdinal4409 0012FEE8 5F43366E 77F86618 77F81A9B 7FFDFOOO 00422218 !0rdinal4408 0012FF08 00417028 00400000 00000000 00133A73 00000001 !Ordinalll90 0012FF20 00416E53 00400000 00000000 00133A73 00000001 'WinMain 0012FFCO 77E9BC52 77F86618 77F81A9B 7FFDFOOO C0000005 !WinMainCRTStartup

    0012FFFO 00000000 00416CAO 00000000 OOOOOOC8 00000100


    kernel32!BaseProcessStart (FPO: Non-FPO [1,8,3])

    Данный пример журнала использует отладочные символы (вследствие чего в колонке Function Name отображены имена всех вызванных к моменту сбоя функций), однако, в пользовательском журнале этого, вероятно, не будет1. В колонке ReturnAd перечисляются адреса возврата функций, находящихся в стеке вызовов. Если журнал пользователя не содержит отладочных символов, то нужно загрузить каждый адрес из колонки ReturnAd в программу CrashFinder (чтобы вычислить последовательность вызовов функций, которая привела к сбою).

    Тогда вместо имени функции в журнале указывается значение . — Пер.

    Колонки Paramtl, #2, #3 и #4 показывают четыре первых возможных параметра функций в стеке вызовов. При высоко оптимизированном выпуском построении и без отладочных символов показанные здесь значения, вероятно, некорректны. Однако их можно использовать в качестве отправной точки для ручной прокрутки кода.

    Имена функций показаны в формате <модуль>! "функциях Функции с именами ! Ordinal#N являются функциями порядкового экспорта. Если у вас нет исходного кода DLL, которая выполняет экспорт функций по порядковым номерам (а не по именам), то вам просто не повезло. Однако, вследствие того, что библиотека классов MFC (Microsoft Foundation Classes) поставляется с исходными кодами, порядковые значения MFC-функций найти можно. Например, для программы WDBG известно, что библиотека MFC42D.DLL загружается по адресу Ox5F400000, так что есть возможность отыскать порядковые номера ее функций, потому что все MFC-функции экспортируются по своим порядковым номерам через DEF-файл компоновщика.

    Существует одно предварительное условие для преобразования порядковых номеров MFC-функций в их имена: необходимо точно знать версию MFC DLL на аварийной машине. На моей машине, называемой \\PLATO, имелась MFC42D.DLL из системы Visual C++ 6 Service Pack 3.


    Ниже приводится последовательность действий для преобразования порядковых номеров функций в их имена:

    1. Откройте каталог \VC98\MFC\SRC\Intel системы Visual C++.

    2. Выберите подходящий DEF-файл для того MFC-файла, который собираетесь посмотреть. Например, DEF-файлом для MFC42D.DLL является MFC42D.DEF.

    3. Ищите порядковый номер. Чтобы найти имя функции с порядковым номером Ordinal4118 из предыдущего стека, я поискал бы в файле MFC42D.DEF строку с номером 4118. Она выглядит так:

    ? OnWndMsg@CWnd@@MAEHIIJPAJ @Z 4118 NONAME.

    Имя, расположенное слева от подстроки "@ 4118 NONAME", является расширенным (декорированным) именем функции, экспортируемой по порядковому номеру. Чтобы преобразовать декорированное имя в обычное, используйте программу UNDNAME.EXE, которая поставляется в составе набора инструментальных средств Platform SDK. Для функции с порядковым номером 4118, обычно, именем функции является CWND::OnWndMsg.

    Третья, заключительная часть дампа состояния потока называется Raw stack Dump (дамп необработанного стека) и выглядит так:

    *———> Raw Stack Dump <———*

    0012fb98 74 fd 12 00 78 4c 13 00-00 fO fd 7f cO 67 8c 00 t...xL....g..

    0012fba8 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................

    0012fbb8 e8 fc 12 00 2e 8a 41 00-ff ff ff ff eO fb 12 00 .....A......

    0012fbc8 b5 be. 40 00 3c 03 00 00-00 00 00 00 03 00 00 80 ..@.<.......

    0012fbd8 cO 67 8c 00 cc cc cc cc-f4 fb 12 00 9c 5a 40 00 .g........Z@.

    0012fbe8 3c 03 00 00 Oc fc Od 01-bO 68 8c 00 f4 fc 12 00 <......h.....

    0012fbf8 ac f3 42 5f 3c 03 00 00-Oc fc Od 01 74 fd 12 00 ..B_<....t...

    0012fc08, 78 4c 13 00 00 fO fd 7f-09 00 00 00 bO 68 8c 00 xL.......h..

    0012fcl8 04 00 00 00 00 00 00 00-00 00 8c 00 50 00 8c 00 .........P...

    0012fc28 50 00 8c 00 01 00 00 00-ae 05 12 00 70 2d 00 00 P........p-..

    0012fc38 01 00 00 00 fa 00 00 00-8c fc 12 00 8b 48 fb 77 ..........H.w

    0012fc48 00 00 8c 00 00 00 9f 01-01 00 00 00 80 fc 12 00 ............

    0012fc58 7c fc 12 00 84 fc 12 00-00 00 00 00 00 00 00 00 | ...........

    0012fc68 00 00 8c 00 00 00 8c 00-90 fc 12 00 d5 19 49 5f ...........I_

    0012fc78 04 00 00 00 18 66 f8 77-9b la f8 77 a4 fc 12 00 ...f.w...w...

    0012fc88 d5 19 49 5f 04 00 00 00-18 66 f8 77 9b la f8 77 ..!_...f.w..w

    0012fc98 00 fO fd 7f 48 el 4c 5f-48 4d 13 00 cc fc 12 00 ....H.L_HM...

    0012fca8 45 16 49 5f cc fc 12 00-d5 19 49 5f d4 fc 12 00 E.I_...I_....

    0012fcb8 d.8- fc 12 00 d5 19 49 5f-04 00 00 00 18 66 f8 77 ... I_.. f.

    0012fcc8 50 22 42 00 00 00 00 00-Oa 00 00 00 00 a5 41 00 P"B........A.

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

    в которой Windows 98 превосходит


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

    Большая часть аварийной информации, регистрируемой в журнале программы Dr. Watson, одинакова для этих операционных систем, но в Windows 98 ее намного больше. Например, программа Dr. Watson из Windows 98 дизассемблирует каждый элемент в стеке вызовов. Чтобы пояснить дополнительную информацию, с которой вы можете встретиться в журнале программы Dr. Watson, рассмотрим секцию Details этого журнала.

    Верхняя часть секции Details выглядит так:

    *———> Details <———*

    Command line: "g:\Dev\Book\CD\SourceCode\Output\WDBG.exe"

    Trap Oe 0000 - Invalid page fault

    eax=00000000 ebx=0065fba6 есх=ООЬ938ЬО edx=fffdcla5 esi=005301c0

    edi=0065fae4

    eip=0040bd2d esp=0065f908 ebp=0065f934 — — — nv up El pi nz na PE nc

    cs=0167 ss=016f ds=016f es=016f fs=38d7 gs=382f

    WDBG.EXE:.text+0xad2d:

    >0167:0040bd2d 8b4804 mov ecx,dword ptr [eax+04]

    В начале секции показана полная командная строка, за которой следует описание ошибки, похожее на описание исключения в Windows 2000. После описания ошибки выводится секция регистров (в том же формате, что и в Windows 2000), а за ней — команда, которая вызвала сбой.

    Далее Windows 98 выводит часть своего 16-разрядного "наследия" — таблицу сегментных регистров, где показаны значения соответствующих селекторов. Дополнительную информацию можно получить в руководствах по процессорам Intel.

    sel type base lim/bot

    ---- ---- ------ --------

    cs 0167 r-x- 00000000 ffffffff

    ss 016f rw-e 00000000 OOOOd7aO

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

    CPU и аппаратные средства

    П Andrew S. Tanenbaum.Structured Computer Organization. — 4th ed., Prentice-Hall, 1998
    Превосходное введение в архитектуру компьютера. Эта книга заполнена информацией, которой я пользуюсь для решения каждодневных проблем отладки. Книга содержит некоторые типографские и технические ошибки, но если вы сумеете обойти их, то получите удовлетворение от ее чтения.
  • Справочные руководства по Intel CPU
  • Intel свободно (бесплатно) распространяет руководства для своих процессоров. Если вы выполняете серьезную отладку, то эти руководства чрезвычайно полезны и информативны. PDF-файлы компании Adobe можно загрузить с узла компании Intel:
    developer.intel.com/design/litcentr/index.htm
  • Hans-Peter Messmer. The Indispensable PC Hardware Book. — 3rd ed., Addison-Wesley, 1997
  • Самое удачное обсуждение аппаратных средств PC. Эта книга бесценна, когда необходимо организовать взаимодействие с аппаратными средствами PC.


    Инструментальные средства

    В разговоре о программных инструментах, я не могу оставаться сторонним наблюдателем. Во-первых, в течение четырех лет я работал в NuMega и входил в команды, которые разрабатывали некоторые из ведущих рыночных программных инструментов. Во-вторых, я все время их использую. Приведенный ниже список поставщиков этих программ предоставлен исключительно для ссылок и не означает поддержки какой-либо компании или продукта. Перечисленные инструменты связаны с отладкой и автоматизированным тестированием.
  • Компания Compuware NuMega (www.numega.com)
  • • BoundsChecker
    Автоматическое обнаружение ошибок времени выполнения и диагностика для программ на языках Visual C++, Borland Delphi и Borland C++ Builder (для всех типов операционных систем Windows).
    • TraeTime
    Автоматический анализ производительности для программ на языках Visual C++, Visual Basic и Java (для всех типов операционных систем Windows).
    • TrueCoverage
    Автоматический анализ покрытия кода для программ на языках Visual C++, Visual Basic и Java (для всех типов операционных систем Windows).
    • CodeReview
    Автоматический анализ исходного кода для программ на языках Visual C++, Visual Basic и Java (для всех типов операционных систем Windows).
    • SmartCheck
    Автоматическое обнаружение ошибок времени выполнения и диагностика для программ на языках Visual C++, Visual Basic и Java (для всех типов операционных систем Windows).
  • FailSafe
  • Автоматическая обработка ошибок и восстановление для программ на языкeVisual Basic (для всех типов операционных систем Windows).
    • JCheck
    Визуальный анализ потоков и событий для программ на языке Java (для всех типов операционных систем Windows).
    • SoftICE
    Усовершенствованный отладчик Windows для отладки программ как в пользовательском режиме, так и в режиме ядра для всех типов операционных систем Windows.
  • Компания Rational Software (www.rational.com)
  • • Purify
    Автоматическое обнаружение ошибок времени выполнения и диагностика для Visual С++-программ (только для Windows NT и Windows 2000).
    • PureCoverage
    Автоматический анализ покрытия кода для программ на языках Visual C++, Visual Basic и Java (только для Windows NT и Windows 2000).
    • Quantify
    Автоматический анализ производительности для программ на языках Visual C++, Visual Basic и Java (только для Windows NT и Windows 2000).
    • Rational Robot
    Инструмент регрессивного тестирования для всех типов операционных систем Windows.
    • Visual Test
    Инструмент регрессивного тестирования для всех типов операционных систем Windows.
    • Компания Mutek Software (www.mutek.com)
    • BugTrapper
    Перехват ошибки и "посмертная" отладка для программ на языках Visual C++ и Visual Basic (для всех типов операционных систем Windows).


    Языки

  • Brian W. Kernighan and Dennis Ritchie.The С Programming Languag. — 2nd ed., Prentice Hall, 1988 (Есть перевод: Керниган Б., Ритчи Д., Фьюэр А.Язык программирования СИ. — М., Финансы и статистика, 1985. —Пер.)
  • Эта книга является наиболее полным руководством по программированию на С и прекрасно написанной технической книгой, лучшей из всех, которые я когда-либо читал.
  • Neill Graham.Learning C++. — McGraw-Hill, 1991
  • Я использовал эту книгу при изучении C++. В отличие от других объемных книг по C++ она написана коротко, в хорошем стиле и точно.
  • The Mandelbrot Set.Advanced Microsoft Visual Basic 6.0. — 2nd ed., Microsoft Press, 1998
  • Превосходная книга по Visual Basic, которая освещает многие реальные проблемы. Первую главу —"On Error GoTo Hell" Пита Морриса, и шестую —"Оставаясь под контролем" Марка Пирса должны читать все программисты на Visual Basic.
  • Mark Nelson. C++Programmer's Guide to the Standard Template Library. — IDG Books, 1995
  • Мое отношение к библиотеке стандартных шаблонов (Standard Template Library — STL) весьма неоднозначно. К счастью, в книге Нельсона приводится много примеров и пояснений наиболее ярких моментов использования этой библиотеки.


    Книги

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


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

  • Brian Kernighan and Rob Pike. The Practice of Programming. — Addison-Wesley, 1999
  • В этой книге приводится замечательное обсуждение разработки, отладки и тестирования программ.
  • Steve Maguire.Writing Solid Code. — Microsoft Press, 1993
  • Книга посвящена, главным образом, программированию на языке С, содержит превосходные советы по определениям интерфейса и обходу неприятных проблем языка.
  • Rex Black.Managing the Testing Process. — Microsoft Press, 1999
  • Для того чтобы лучше разрабатывать программы, нужно знать, как улучшить их тестирование. Эта превосходная книга поможет вам улучшить методику разработки и способ взаимодействия со специалистами по контролю качества.
  • Jonathan В. Rosenberg.How Debuggers Work. — John Wiley & Sons, 1996
  • Эта книга — превосходное введение в работу отладчиков и проблемы их проектирования.


    Разработка программного обеспечения

    Steve McConnell.Code Complete. — Microsoft Press, 1993 Это лучшая книга по конструированию программного обеспечения, которую я когда-либо читал. Каждый разработчик должен иметь собственный экземпляр этой книги и читать его от корки до корки каждый год.
  • Steve McConnell.Rapid Development. — Microsoft Press, 1996
  • Эта книга научила меня, как нужно управлять командой разработчиков и планированием проектов.
  • Steve Maguire.Debugging the Development Process. — Microsoft Press, 1994
  • Эта книга является введением в методики, с помощью которых компания Microsoft разрабатывает программное обеспечение. Из-за того что компания Microsoft — наиболее успешная программистская компания на планете, она должна все делать правильно. Вы можете многому научиться из этой книги.
  • Jim McCarthy.Dynamics of Software Development. — Microsoft Press, 1995
  • Здесь изложен очень интересный взгляд на разработку программного обеспечения (с точки зрения менеджера) — с прослеживанием графиков поставки больших программных продуктов. Правила, которые предлагает Джим Маккарти превосходны, потому что они все исходят из практического опыта, а не только из академического теоретизирования.


    Web-сайты

    В моей папке \Favorites обозревателя Microsoft Internet Explorer только три сайта для разработчиков:
  • MSDN Online (msdn.microsoft.com)
  • MSDN — это первое место, куда нужно обращаться за информацией по Windows. К MSDN Online можно обращаться как по этому адресу, так и через поддерживаемые компанией Microsoft журналыMicrosoft Systems Journal (MSJ) иMicrosoft Internet Developer (MIND). Имейте в виду, что MSDN — полумаркетинговая организация, поэтому в ее материалах иногда встречается навязчивая реклама.
  • Sysinternals (http:// www.sysinternals.com/)
  • Марк Руссинович (Mark Russinovich) и Брюс Когсвелл (Bryce Cogswell) владеют одним из лучших наборов отладочных утилит, включая Regmon, Filemon, DebugView HandleEx и много других. Многие утилиты приходят с полными исходными текстами и все бесплатны! Я проверяю этот сайт по крайней мере один раз в неделю.
  • MVPS.org (www.mvps.org)
  • Это Web-сайт участников сетевой телеконференции, называющих себя Most Valuable Professional (MVP) newsgroup. MVP — это разработчики и пользователи, которые являются экспертами в специфических областях и помогают различным группам новостей отвечать на вопросы по технологиям и программным продуктам компании Microsoft.
    Вот некоторые другие сайты, которые могут вас заинтересовать:
  • CodeGuru (www.codeguru.com)
  • Информационный сайт, посвященный преимущественно системе программирования Visual C++.
  • Experts Exchange (www.experts-exchange.com)
  • Технический информационный обменный сайт, который охватывает многие тематические разделы Windows.
  • Dr. Dobb's Journal Microprocessor Resources (www.x86.org)
  • Здесь можно найти все, что нужно знать о семействе процессоров х86
  • . Архивы различных почтовых адресатов (discuss.microsoft.com/archives)
  • Архивы некоторых полезных почтовых адресатов, таких как ATL и DCOM.

    Windows и Windows-технологии

  • Charles Petzold.Programming Windows. — 5th ed., Microsoft Press, 1999
  • Одна эта книга научит вас всему, что нужно знать о работе Windows-программ, причем на самом высоком уровне. Многие проблемы можно решить только в том случае, если хорошо понимать, как обрабатываются сообщения, как работает интерфейс графических устройств (Graphics Device Interface — GDI) и другие объекты на уровне SDK.
  • Jeffrey Richter.Programming Applications for Microsoft Windows. — 4th ed., Microsoft Press, 1999
  • Эта книга охватывает все важнейшие аспекты разработки \¥т32-при-ложений. Главы по DLL, потокам, синхронизации и структурированной обработке исключений — наилучшие из доступных описаний, и все они имеют прямое отношение к более быстрой и эффективной отладке ваших программ.
  • Jeff Prosise.Programming Windows with MFC. — 2nd ed., Microsoft Press, 1999
  • Эта книга — наиболее полное руководство по программированию с использованием библиотеки классов MFC. Если вы используете MFC, то должны иметь эту книгу.
  • Paul Dilascia.Windows++. — Addison-Wesley, 1992
  • В отличие от многих книг по проектированию, здесь приводится сильная и жизнеспособная библиотека С++-классов для Windows.
  • Don Box.Essential COM. — Addison-Wesley, 1998
  • В окне Disassembly, COM'-интерфейс является просто указателем на массив указателей, что позволяет легко понять смысл СОМ-модели. СОМ-модель, однако, гораздо сложнее простого указателя. Она играет фундаментальную роль в программировании, так что если вы хотите иметь шанс корректно разрабатывать и отлаживать свои программы, то должны понимать, как эта модель работает. Книга Бокса может послужить хорошим началом на пути изучения СОМ-технологии.
    COM — Component Object Model, модель компонентных объектов. —Пер.
  • David A. Solomon.Inside Windows NT. — 2nd ed., Microsoft Press, 1998
  • В этой книге представлен широкий официальный обзор ядра Windows NT. Хотя она непосредственно адресована тем, кто пишет драйверы устройств, вам она поможет уловить смысл того, как совмещаются различные части этой операционной системы.
  • Matt Pietrek.Windows 95 System Programming Secrets. — IDG Books, 1995
  • Эта книга не переиздавалась, но нужно постараться найти ее копию. При попытке отладить что-то большее, чем простое нарушение доступа в Windows 95/98, вы испытываете чувство глубокого разочарования главным образом потому, что Windows 95/98 являются гибридными 16/32-разрядными операционными системами. Однако многие разделы книги, такие как глава о переносимом исполняемом (Portable Executable — РЕ) формате файлов, применимы также и к Windows 2000.
  • Brent Rector and Chris Sells.ATI Internals. — Addison-Wesley, 1999
  • Библиотека активных шаблонов (ATL) помогает строить самые компактные и самые быстрые СОМ-объекты. Эту книгу нужно иметь для того, чтобы полностью использовать все возможности ATL.


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

    Модификаторы позиционных точек прерывания

    После установки регулярной позиционной точки прерывания можно уточнить (модифицировать) способ прерывания в этой точке. Чтобы установить условие модификации, нужно выделить позиционную точку прерывания в диалоговом окне Breakpoints и нажать кнопку Condition, чтобы открыть диалоговое окно Breakpoint Condition.
    Тип модификатора и его вид в окнеBreakpoints
    Описание
    Счетчик пропусков
    at '{,WINMAIN.CPP} .24, ' skip five times (5)'
    Пропустить указанное число точек прерывания. Ввести в редактируемое поле Enter The Number Of Times To Skip Before Stopping число пропускаемых точек прерывания. Установив в счетчике пропусков число, превосходящее максимальное количество итераций цикла, можно узнать, сколько раз сработала точка прерывания перед сбоем в цикле. После сбоя в цикле откройте окно Breakpoints — количество пропусков будет выведено в нижней части окна
    Выполнить прерывание, когда условное выражение примет значение TRUE.
    at 4,WINMAIN.CPP} .24, when Ч=°=3'
    Ввести оцениваемое выражение в поле Enter The Expression To Be Evaluated. Выражение не должно содержать вызовов функций или макросов. Допускаются операции сравнения в стиле языка С. При проверке строчных значений проверяйте отдельные символы, связав их операциями AND
    Выполнить прерывание, когда изменится значение переменной
    at MfWINMAIN.CPP) .24, when %szBuffer' (length: 5) changes
    Ввести идентификатор переменной в поле Enter The Expression To Be Evaluated. Для массивов или структур ввести число элементов в поле Enter The Number Of Elements To Watch In Array Or Structure. Размер элемента базируется на размере введенной переменной. Прерывания будут вызывать только те записи данных, которые изменяют переменную


    Позиционные точки прерывания

    Все позиционные точки прерывания (location breakpoints) устанавливаются вручную в редактируемом поле Break at на вкладке Location диалогового окна Breakpoints.
    Тип позиционной точки и ее вид в окнеBreakpoints
    Описание
    Исходная строка
    at M/TEST.CPP,} .20'
    Нажать кнопку Insert/Remove breakpoint, когда курсор установлен на строке исходного кода, на которой нужно вызвать прерывание
    Первая инструкция в функции
    at 'Cdialog: :0n0k'
    Ввести имя функции. Если отладчик выполняется, он может подсказать подходящее имя в диалоговом окне Resolve Ambiguity
    Любой адрес
    at '0x417013'
    Ввести шестнадцатеричный адрес, на котором нужно выполнить прерывание
    Прерывание на экспортируемой системной функции (отладочные символы модуля загружены) at '{, ,KERNEL32.DLL} _LoadLibraryA@4'
    Ввести контекстную информацию и вставить информацию двоичного модуля. Для экспортируемой функции ввести имя с символом подчеркивания, знаком @ и числом параметров, умноженным на 4
    Прерывание на экспортируемой системной функции (отладочные символы модуля не загружены)at {, , KERNEL32 . DLL}_LoadLibraryA'
    Ввести контекстную информацию, вставить информацию двоичного модуля и ввести экспортируемое имя



    Точки прерывания глобальных выражений и условные точки прерывания

    Точки прерывания этого типа (global expression and conditional breakpoints) устанавливаются в поле Enter the expression to be evaluated на вкладке Data диалогового окна Breakpoints. Они используют отладочные регистры Intel CPU. Хотя таких регистров четыре, можно устанавливать только две точки данного типа. При этом лучше использовать шестнадцатеричные адреса, приведенные к типам short, word или double word. Отладчик принимает такую точку, но у него почему-либо может отсутствовать доступ к отладочному регистру (тогда он перейдет в пошаговый режим, проверяя адреса памяти на каждой ассемблерной команде).
    Тип модификатора и его вид в окнеBreakpoints
    Описание
    Глобальное выражение
    at y*(char*)Ox4287C8'
    Точка прерывания (ТП) этого типа срабатывает при изменении содержимого памяти по указанному адресу
    Глобальное выражение
    at '*(char*)Ox4287C8=='G"
    ТП этого типа срабатывает при записи определенного значения (например, символа 'G', см. слева) по указанному адресу



    Точки прерывания сообщений

    Точки прерывания сообщений (message breakpoints) особенно полезны при программировании в среде SDK. При работе с библиотекой классов MFC очень полезна установка такой точки на общей для всех процессов функции AfxWndProc. Можно также применять позиционную ТП с модификатором в виде условного выражения. Например, чтобы получить прерывание на сообщении WM_PAINT для конкретного класса, нужно ввести выражение
    {,WINCORE.CPP,}.1584 when (this==Ox0012EFE74)&&(message==OxF)

    

        Программирование: Языки - Технологии - Разработка