Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdfГЛАВА 12 Нахождение файла и строки ошибки по ее адресу |
463 |
|
|
Реализуйте возможность вставки (pasting) списков DLL, чтобы их можно было добавлять в проект автоматически. В окне отладчика Output выводится спи сок всех загружаемых приложением DLL, поэтому было бы удобно, если б CrashFinder позволял вставлять этот текст и умел отыскивать в нем имена DLL.
Добавьте в CrashFinder поддержку аварийных дампов памяти. Тогда CrashFinder мог бы сопоставлять ошибку с аварийным дампом и определять все ее обсто ятельства.
Резюме
В этой главе я рассказал, как найти расположение ошибки в исходном коде, имея только ее адрес. Первый метод нахождения исходного файла и номера строки конкретной ошибки — изучение MAP файла. MAP файлы — это единственное средство представления информации о символах в текстовом виде, и вам следует создавать их для каждой заключительной компоновки вашего приложения. Вто рой метод преобразования адреса ошибки в информацию об исходном файле, имени функции и номере строки обеспечивает утилита CrashFinder. CrashFinder выполняет это преобразование самостоятельно, позволяя получить максимально подробную информацию об ошибке. CrashFinder проще в использовании, чем MAP файлы, но это не избавляет вас от необходимости их создания, потому что фор маты файлов символов иногда изменяются. Когда в вашу дверь постучит призрак одного из ваших старых приложений, ничто не спасет вашу душу — только MAP файлы.
Г Л А В А
13
Обработчики ошибок
Ни для кого не секрет, что пользователи ненавидят диалоговое окно сообщения об ошибке, появляющееся при аварийном завершении приложения. Если вы чи таете эту книгу, это означает, что вы делаете все возможное для профилактики ошибок. Однако все мы знаем, что ошибки происходят даже в самых лучших про граммах, и к ним нужно быть готовым.
Как было бы хорошо, если бы вместо раздражающего пользователей сообще ния об ошибке появлялось дружественное к ним окно, которое описывало бы проблему и спрашивало его о том, что он делал в момент ошибки! А еще лучше, если б это любезное и вежливое окно записывало не только обычную информа цию об адресе ошибки и стеке вызовов, предоставляемую такими утилитами, как Dr. Watson, но и внутреннее состояние приложения, позволяющее узнать состоя ние выполнения программы и ее данных в момент ошибки! И разве не было бы уж совсем великолепно, если б диалоговое окно автоматически отсылало вам информацию об ошибке по электронной почте и сохраняло отчет об ошибке в вашей системе отслеживания ошибок?
Обработчики ошибок могут сделать эти мечты реальностью, обеспечив вас всей этой полезной информацией. Обработчиками ошибок я называю и обработчики исключений, и фильтры необработанных исключений. Если вы работали с язы ком C++, обработчики исключений должны быть вам известны. Вероятно, вам хуже знакомы фильтры необработанных исключений, которые представляют собой интересные функции, позволяющие получить контроль прямо перед появлением отвратительного окна с сообщением об ошибке. Обработчики исключений име ются только в C++, в то время как фильтры необработанных исключений можно использовать и в C, и в C++.
В этой главе я приведу код, который вы можете включать в свои приложения для получения такой информации, как значения регистров и стеки вызовов. Кро
ГЛАВА 13 Обработчики ошибок |
465 |
|
|
ме того, он скроет значительную часть грязной работы по сбору этих данных, благодаря чему вы сможете сосредоточиться на информации, специфичной для вашего приложения, и смягчении отрицательных эмоций своих пользователей. Я также расскажу о том, как извлечь максимальную выгоду из прекрасной API фун кции MiniDumpWriteDump, чтобы получать минидампы всегда, когда они нужны. Од нако, прежде чем перейти к рассмотрению этого кода, я опишу типы обработки исключений в ОС Microsoft Win32.
Структурная обработка исключений против обработки исключений C++
Изучение обработки исключений осложняется тем, что C++ поддерживает два основных ее типа: структурную обработку исключений (structured exception han dling, SEH), предоставляемую ОС, и обработку исключений C++, входящую в со став языка. Многие считают оба типа обработки исключений одинаковыми. Могу вас заверить, что в основе этих двух типов обработки исключений лежат совер шенно разные подходы. Общая их черта в том, что исключения обоих типов пред назначены для применения в исключительных ситуациях, а не при нормальном выполнении программы. По моему, многих людей путают слухи, что оба типа исключений можно комбинировать. Ниже я коснусь различий и сходств между этими типами обработки исключений, а также расскажу про то, как избежать крупнейшего источника связанных с ними ошибок.
Структурная обработка исключений
SEH обеспечивается ОС и предназначена для непосредственной обработки таких ошибок, как нарушение доступа. SEH не привязана к конкретному языку; в про граммах C и C++ она обычно реализуется в виде пар __try/__except и __try/__finally. Использование пары __try/__except заключается в размещении некоторого кода внутри блока __try и описании обработки возникающих в нем исключений в блоке __except (также называемом обработчиком исключений). Входящий в состав пары __try/__finally блок __finally (его еще называют обработчиком завершения) вы полняется при выходе из функции всегда, даже при преждевременном заверше нии блока __try, что позволяет гарантировать освобождение ресурсов.
Типичная функция с SEH представлена в листинге 13 1. Блок __except выгля дит почти так же, как вызов функции, только в его скобках указано значение спе циального выражения, называемого фильтром исключений. Фильтр исключений имеет значение EXCEPTION_EXECUTE_HANDLER, которое указывает на то, что блок __except должен выполняться каждый раз при возникновении любого исключения в бло ке __try. Возможны еще два значения фильтра исключений: EXCEPTION_CONTINUE_EXE CUTION позволяет проигнорировать исключение, а EXCEPTION_CONTINUE_SEARCH пере дает исключение по цепи вызовов следующему блоку __except. Вы можете делать фильтр исключений сколь угодно простым или сложным, обрабатывая только те исключения, которые желаете.
466 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Листинг 13-1. Пример обработчика SEH
void Foo ( void )
{
__try
{
__try
{
// Какие нибудь действия.
}
__except ( EXCEPTION_EXECUTE_HANDLER )
{
//Этот блок будет выполнен, если код в блоке __try
//вызовет нарушение доступа или другую ошибку.
//Блок __except называют также обработчиком исключений.
}
}
__finally
{
//Этот блок будет выполнен независимо от того,
//вызвала ли функция ошибку. Выполняйте здесь
//все действия, необходимые для освобождения ресурсов.
}
}
Процесс поиска и выполнения обработчика исключения иногда называют развертыванием (unwinding) исключения. Обработчики исключений хранятся во внутреннем стеке; по мере роста цепи вызовов функций в этот внутренний стек помещаются обработчики исключений (если они есть) всех новых функций. При возникновении исключения ОС находит стек обработчиков исключений данно го потока и начинает вызывать их, пока какой нибудь из них не согласится обра ботать данное исключение. По мере прохождения исключения через стек обра ботчиков ОС очищает стек вызовов и выполняет все обработчики завершений, которые ей при этом встречаются. Если развертывание достигает конца стека обработчиков исключений, появляется окно сообщения об ошибке или загружа ется JIT отладчик.
Обработчик исключений может определить значение исключения посредством специальной функции GetExceptionCode, которая может вызываться только в фильтрах исключений. При работе над математической программой вы, например, могли бы установить обработчик исключений, который обрабатывал бы попытки деле ния на 0 и возвращал значение NaN (not a number — не число). Пример такого об работчика см. в листинге 13 2. Фильтр исключений вызывает GetExceptionCode, и, если исключение вызвано делением на 0, выполняется обработчик. Любому дру гому исключению соответствует значение EXCEPTION_CONTINUE_SEARCH, приказывающее ОС выполнить следующий блок __except в цепи вызовов.
ГЛАВА 13 Обработчики ошибок |
467 |
|
|
Листинг 13-2. Пример обработчика SEH, включающего обработку фильтра исключений
""""long IntegerDivide ( long x , long y )
{
long lRet ;
__try
{
lRet = x / y ;
}
__except ( EXCEPTION_INT_DIVIDE_BY_ZERO == GetExceptionCode ( )
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH
)
{
lRet = NaN ;
}
return ( lRet ) ;
}
Фильтром исключений может быть даже вызов вашей собственной функции, если только она определяет обработку исключения, возвращая одно из коррект ных значений фильтра исключений. Кроме специальной функции GetExceptionCode, в выражении фильтра исключений можно вызывать функцию GetExceptionInfor mation. Она возвращает указатель на структуру EXCEPTION_POINTERS, которая полно стью описывает причину ошибки и состояние процессора в момент ее возник новения. Как вы уже догадались, структуру EXCEPTION_POINTERS я использую ниже.
Возможности SEH не ограничиваются обработкой ошибок. Вы можете опре делять собственные исключения, используя API функцию RaiseException. Большин ство программистов ее не применяет, хотя она обеспечивает один из способов быстрого выхода из глубоко вложенных условных выражений. Этот способ бо лее корректен, чем старый метод, основанный на использовании функций стан дартной библиотеки setjmp и longjmp.
Чтобы грамотно работать с SEH, вам нужно знать о двух ее ограничениях. Первое не очень существенно: число кодов ваших ошибок ограничено одним беззнако вым целым. Вторая проблема серьезнее: SEH плохо сочетается с C++, потому что исключения C++ на самом деле реализованы при помощи SEH, и беспорядочное комбинирование обоих типов исключений вызывает недовольство компилятора. Причина конфликта в том, что при развертывании SEH деструкторы объектов C++, созданных в стеке, не вызываются. В конструкторах объектов C++ часто выпол няется самая разнообразная инициализация, например, выделение памяти для внутренних структур данных, поэтому пропуск деструкторов может приводить к утечкам памяти и другим проблемам.
Если вы хотите лучше изучить основы SEH, то, кроме Microsoft Developer Network (MSDN), я рекомендую еще два источника. Самый лучший обзор SEH можно най ти в книге Джеффри Рихтера (Jeffrey Richter. Programming Applications for Microsoft
468 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Windows. — Microsoft Press, 1999). Если вас интересует действительная реализа ция SEH, прочитайте статью Мэтта Питрека (Matt Pietrek) «A Crash Course on the Depths of Win32 Structured Exception Handling» («Тонкости структурной обработ ки исключений Win32») в «Microsoft Systems Journal» в январе 1997 г.
Я хочу также упомянуть про одно усовершенствование SEH, появившееся в Microsoft Windows XP/Server 2003, — векторную обработку исключений. При обыч ной SEH способа глобального уведомления об исключениях нет. Обычно в повсед невной работе векторная обработка исключений не нужна, однако она предо ставляет одну очень полезную возможность — получение первого и последнего уведомления обо всех SEH исключениях приложения. Как только я узнал, что Microsoft добавила в ОС векторную обработку исключений, у меня сразу же со зрел план программы мониторинга SEH исключений, позволяющей следить за генерируемыми приложением исключениями, не запуская его под управлением отладчика.
Для получения векторных исключений просто вызовите функцию AddVectored ExceptionHandler, вторым параметром которой является указатель на функцию, которая будет вызываться при возникновении в вашей программе любого перво го случая исключения (first chance exception). Первый параметр — булево значе ние, показывающее, как вы хотите получать уведомления: до или после нормаль ного развертывания цепи исключений. При исключении ваша функция обратно го вызова получит указатель на структуру EXCEPTION_POINTERS, описывающую исклю чение. Как вы уже поняли, эта информация делает получение исключений триви альным.
На CD вы найдете проект XPExceptMon, иллюстрирующий использование век торных исключений, записывая все исключения, возникающие в вашей програм ме. Вся работа по установке и удалению ловушки векторных исключений выпол няется в функции DllMain библиотеки XPExceptMon.DLL, поэтому задействовать ее в своих приложениях вам будет очень просто. Я хотел просто продемонстриро вать работу с векторными исключениями, поэтому все, что делает XPExceptMon, заключается в записи типа и адреса исключения в текстовый файл. Чтобы попрак тиковаться с сервером символов DBGHELP.DLL, можете добавить в XPExceptMon просмотр функций и анализ стека.
Если вам хотелось бы получать уведомления об исключениях для более ран них версий Windows, вам повезло. Юджин Гершник (Eugene Gershnik) написал отличную статью «Visual C++ Exception Handling Instrumentation» («Обработка ис ключений в Visual C++»), опубликованную в декабрьском номере «Windows Develo per Magazine» за 2002 год. Юджин не только показывает, как устанавливать ловушки для исключений, но и отлично описывает саму обработку исключений.
Обработка исключений C++
Обработка исключений C++ входит в состав языка C++, поэтому большинству программистов она скорее всего известна лучше, чем SEH. Для обработки исклю чений C++ используются ключевые слова try и catch. Ключевое слово throw позволяет инициировать развертывание исключений. В то время как число кодов ошибок SEH ограничено одним беззнаковым целым, обработчик catch языка C++ может
ГЛАВА 13 Обработчики ошибок |
469 |
|
|
перехватывать любые типы переменных, включая классы. Если вы выполните наследование классов обработки ошибок от общего базового класса, вы сможете обрабатывать почти любые ошибки. Именно такой иерархический подход к об работке ошибок реализован в библиотеке Microsoft Foundation Class (MFC) с ба зовым классом CException. Обработка исключений C++ показана в листинге 13 3, где выполняется чтение объекта класса CFile библиотеки MFC.
Листинг 13-3. Пример обработчика исключений C++
BOOL ReadFileHeader ( CFile * pFile , LPHEADERINFO pHeader )
{
ASSERT |
( FALSE |
== IsBadReadPtr ( pFile , sizeof ( CFile * ) ) ) ; |
||
ASSERT |
( FALSE |
== IsBadReadPtr ( pHeader , |
|
|
|
|
sizeof ( LPHEADERINFO ) ) ) ; |
|
|
if ( ( |
TRUE == |
IsBadReadPtr ( pFile , sizeof ( CFile * ) ) ) || |
|
|
( |
TRUE == |
IsBadReadPtr ( pHeader , |
|
|
|
|
sizeof ( LPHEADERINFO ) |
) ) |
) |
{ |
|
|
|
|
return ( FALSE ) ;
}
BOOL bRet ;
try
{
pFile >Read ( pHeader , sizeof ( HEADERINFO ) ) ; bRet = TRUE ;
}
catch ( CFileException * e )
{
//Если заголовочный файл не был прочитан из за обнаружения
//преждевременного конца файла, исключение обрабатывается,
//в противном случае развертывание продолжается.
if ( CFileException::endOfFile == e >m_cause ) |
{ |
e >Delete(); |
|
bRet = false; |
|
} |
|
else |
|
{ |
|
//Само по себе ключевое слово throw генерирует то же
//исключение, что было передано в этот блок catch. throw ;
}
}
return ( bRet ) ;
}
Обработка исключений C++ имеет некоторые недостатки, о которых следует помнить. Во первых, ошибки вашей программы не обрабатываются автоматически. Во вторых, обработка исключений C++ не так уж и грациозна. Даже если ваша программа никогда не генерирует исключений, компилятор проделает много
470 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
работы, устанавливая и удаляя блоки try и catch, что может оказаться слишком на кладно при высоких требованиях к быстродействию программы. Если вы новичок в обработке исключений C++, для начала их изучения прекрасно подойдет MSDN.
Избегайте использования обработки исключений C++
Обработка исключений C++ — один из самых частых вопросов, с которыми я сталкиваюсь как консультант. При разработке Windows приложений программи сты тратят на решение проблем с исключениями C++ больше времени, чем на все прочие ошибки (за исключением искажений памяти). Опираясь на большой опыт разрешения многих ужасных ситуаций, я рекомендую избегать обработки исклю чений C++, потому что это бесконечно облегчит вашу жизнь и упростит отладку программ.
Первая проблема обработки исключений C++ в том, что ее нельзя считать чистым свойством языка. Очень часто она кажется чужеродной и незавершенной. Отсутствие стандартного класса ANSI, содержащего информацию об исключении, означает, что у нас нет согласованного способа обработки общих ошибок. Кое кто из вас, возможно, считает, что стандартным механизмом для обработки всех исключений является конструкция catch (...), но ниже вы увидите, насколько она небезопасна.
Обработка исключений C++ принадлежит к числу технологий, великолепных
стеоретической точки зрения, но приводящих к проблемам, если вы пишете что нибудь посерьезнее, чем «Hello World!». Я часто сталкиваюсь с абсолютно безум ными ситуациями, когда один из членов группы так влюбляется в исключения C++, что начиняет ими свой код в невообразимом количестве. Это заставляет работать
сисключениями C++ и всех остальных членов группы, даже если далеко не все из них до конца понимают тонкости проектирования и использования исключений. При этом кто либо из программистов неизбежно забывает про перехват какого нибудь случайного неожиданного исключения, и приложение терпит крах. Кро ме того, программы, в которых для сообщения об ошибках используются и воз вращаемые значения, и исключения C++, практически не поддаются модерниза ции и сопровождению, вследствие чего многие компании предпочитают отказаться от существующего кода и начать разработку программы с самого начала, значи тельно увеличивая расходы.
Многим программистам исключения C++ нравятся тем, что они позволяют отказаться от проверки возвращаемых из функций значений. Однако этот аргу мент не только ошибочен, но и служит оправданием плохого стиля программи рования. Если у одного из членов вашей группы постоянно возникают проблемы
спроверкой возвращаемых значений, проведите соответствующую консультацию. Если и после этого он не будет выполнять проверку возвращаемых значений, смело увольняйте его — он просто отлынивает от работы.
До этого момента я обсуждал вопросы проектирования исключений C++ и управления ими. Но не стоит также забывать, что с ними связано много дополни тельных затрат. Для создания блоков try и catch нужно проделать большую рабо ту, что заметно ухудшает быстродействие программы, даже если вы редко (если вообще) генерируете исключения.
ГЛАВА 13 Обработчики ошибок |
471 |
|
|
Кроме того, в компиляторах Microsoft исключения C++ реализованы при по мощи SEH, а это означает, что каждый оператор throw вызывает функцию Raise Exception. В этом нет ничего плохого, но каждый throw вызывает всеми любимое переключение в режим ядра. Само по себе переключение выполняется очень быстро, но манипуляции с вашим исключением в режиме ядра требуют огром ных затрат. В разделе «Советы и уловки» главы 7 я рассказал о способе контроля исключений C++, который сможет точнее указать вам на эти затраты.
Похоже, иногда разработчики забывают о цене исключений C++. Однажды меня наняла одна компания, чтобы я решил проблему с производительностью програм мы. Приступив к работе, я никак не мог понять, почему функция _except_handler3, выполняющаяся при обработке исключений, вызывается столько раз. При проверке кода я понял, что вместо испытанной конструкции switch...case кто то исполь зовал обработку исключений C++. Чтобы ускорить приложение, компании при шлось переписать значительную часть кода этого программиста просто для воз врата значений перечисления. На мой вопрос, почему он решил прибегнуть к об работке исключений C++, он ответил, что думал, что оператор throw просто изме няет указатель команд. Итак, исключения C++ допустимы только в тех програм мах, быстродействие которых не имеет никакого значения.
Никогда, ни за что, НИ В КОЕМ СЛУЧАЕ не используйте catch ( ... )
Мне очень нравится конструкция catch (...), потому что она весьма благоприят но сказывается на моем банковском счете, вызывая больше ошибок, чем вы мо жете себе представить. С ней связаны две огромных проблемы. Первая заключа ется в ее спецификации согласно стандарту ANSI. Многоточие означает, что блок catch перехватывает любой тип исключений.
Однако из за отсутствия в catch какой либо переменной вы не можете узнать, как вы очутились в этом блоке и почему. Это, например, означает, что исключе ние может быть вызвано присвоением случайного значения указателю команд. Вследствие невозможности узнать тип и причину исключения единственное бе зопасное и благоразумное действие, которое вы можете предпринять, — завер шить приложение. Некоторым из вас, возможно, покажется, что это слишком ра дикальное решение, но лучшего я предложить не могу.
Вторая проблема связана с реализацией catch (...). Многие программисты не осознают, что в стандартной библиотеке C для Windows блок catch (...) перехва тывает не только исключения C++, но и исключения SEH! Вы не только не будете знать, как вы оказались в блоке catch, но и сможете очутиться в нем из за нару шения доступа к памяти или какой нибудь другой аппаратной ошибки. В этом случае вы можете не только заблудиться, но и столкнуться с совершенно неста бильным поведением программы в блоке catch, поэтому самым лучшим решени ем будет немедленное завершение процесса.
Меня просто поражает, как часто программисты, и довольно опытные в том числе, пишут что нибудь вроде:
BOOL DoSomeWork ( void )
{
BOOL bRet = TRUE ;
try
{
472 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
// ...Какие нибудь действия...
}
catch ( ... )
{
// ВНИМАНИЕ! ЭТОТ БЛОК ПУСТ!
}
return ( bRet ) ;
}
Если в такой ситуации произойдет нарушение доступа, оно будет перехвачено блоком catch (...), но вы об этом даже не узнаете. Минут через двадцать ваша про грамма потерпит крах, однако о его причине вы не будете иметь абсолютно ни какого представления, потому что стек вызовов не будет иметь причинно след ственного отношения. Вам останется только удивляться проблеме. Опираясь на свой богатый опыт, я утверждаю, что главной причиной непонятных ошибок яв ляется catch (...). Гораздо лучше допустить аварийное завершение приложения, потому что тогда у вас хотя бы будет неплохой шанс обнаружения ошибки. При использовании catch (...) вероятность ее обнаружения снижается до 5% и ниже.
Если вы еще не догадались, я сам скажу, что советую вам удалить все блоки catch (...) из ваших программ. Фактически я ожидаю, что вы сделаете это сразу, иначе вы скоро обратитесь ко мне за услугами и поможете мне выплатить очередной взнос за автомобиль.
Отладка: фронтовые очерки
О вреде catch (...)
Боевые действия
Однажды я ехал в аэропорт, собираясь вылететь к клиенту. Тут мне позво нил руководитель отделения и сказал, что мы получили отчаянный — про сто безумный — запрос о проведении консультации. Помогать обезумевшим людям — наша работа, поэтому я решил узнать, что случилось. Поднявший трубку менеджер не просто обезумел — он был на грани инфаркта! Он сказал, что сотрудники его компании бьются над решением абсолютно непонят ной ошибки, задерживающей выпуск программы. Он также сказал, что это еще не самое страшное, и добавил, что если ошибка не будет решена и программа не будет закончена, его компания обанкротится. Более 10 про граммистов уже работали над этой ошибкой три недели подряд, но безре зультатно. В то время моя жизнь была не особо увлекательной по сравне нию с работой на предыдущих должностях, поэтому возможность спасти чью то компанию определенно привлекла мой интерес. Этот человек спро сил меня, насколько быстро я смогу добраться к ним. Я ответил, что выле таю в Сиэтл на неделю, поэтому не смогу заняться их проблемой до окон чания этого срока. Мы недавно основали компанию Wintellect, и у нас не было свободных сотрудников.
В этот момент менеджер начал говорить гораздо громче (ладно, что греха таить, он начал орать), утверждая, что не может столько ждать. Он спро сил, буду ли я занят в Сиэтле постоянно. Это было не так, и он предложил
