
Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdf

474 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Полусонные программисты просто забыли закомментировать один опе ратор return. Я закомментировал его и запустил программу. Она потерпела крах почти сразу. При втором запуске она завершилась там же, и это был первый раз, когда разработчики увидели согласованную ошибку. Третья ошибка в том же месте показалась всем благословением, и я начал иссле довать все, что происходит со стеком.
Тщательно изучив код, мы обнаружили ошибку всего за пару часов. В документации требовалось, чтобы один буфер, передаваемый другой фун кции, имел размер 250 символов. Кто то из программистов передавал в качестве буфера локальную переменную и написал 25 вместо 250. Как только мы исправили опечатку, приложение заработало совершенно нормально!
Полученный опыт
Урок прост: не используйте catch (...)! В данном конкретном случае ком пании пришлось потратить недели труда (и огромные деньги) в поисках легкой ошибки, которую нельзя было воспроизвести из за catch (...).
Не используйте _set_se_translator
В первом издании этой книги я описал интересную API функцию _set_se_translator, которая волшебным образом преобразует ваши ошибки SEH в исключения C++. Для этого она вызывает определенную вами функцию, вызывающую throw для каж дого типа, который вы хотите использовать для преобразования. Сейчас я могу признаться, что тот мой совет оказался ошибочным, хотя в его основе и лежали добрые намерения. При использовании _set_se_translator вы быстро обнаружи те, что в заключительных компоновках она не работает.
Первая проблема с _set_se_translator в том, что она не является глобальной; область ее действия ограничена конкретным потоком. Это значит, что вам, веро ятно, придется переработать свою программу, чтобы гарантировать вызов _set_se_trans lator в начале каждого потока. Увы, это не всегда просто. Кроме того, если вы разрабатываете компонент, используемый другими, не контролируемыми вами процессами, _set_se_translator полностью запутает обработку исключений этих процессов, если они ожидают исключения SEH, а вместо этого получают исклю чения C++.
Более серьезная проблема касается скрытых деталей реализации обработки исключений C++. Обработка исключений C++ может быть реализована двумя спо собами: асинхронным и синхронным. При асинхронном режиме генератор кода предполагает, что исключение может быть сгенерировано любой командой, при синхронном исключения генерируются явно только оператором throw. Различия между асинхронной и синхронной обработкой исключений не кажутся такими уж большими, но на самом деле это именно так.
Асинхронные исключения имеют один недостаток: компилятор должен сгене рировать для каждой функции то, что называется кодом слежения за временем жизни объекта (object lifetime tracking code). Компилятор полагает, что исключе ние может быть сгенерировано любой командой, поэтому каждая функция, по мещающая объект C++ в стек, должна включать код, гарантирующий при возник
ГЛАВА 13 Обработчики ошибок |
475 |
|
|
новении исключения вызов деструкторов каждого объекта. А так как предполага ется, что исключения — редкие или почти невозможные события, неиспользуе мый код слежения за временем жизни объектов приводит к значительному сни жению быстродействия программы.
Синхронная обработка исключений решает эту проблему, генерируя код сле жения за временем жизни объекта, только когда метод в дереве вызовов включа ет явный throw. Фактически синхронные исключения настолько хороши, что именно этот тип исключений используется компилятором. Однако компилятор предпо лагает, что исключения происходят только в результате явного throw в стеке вы зовов, в то время как функция преобразования выполняет throw, который нахо дится вне нормального потока выполнения программы и, таким образом, являет ся асинхронным. Из за этого ваш тщательно разработанный класс оболочки ис ключений C++ никогда не будет обработан, и ваше приложение все равно потер пит крах. Чтобы лучше изучить различия между асинхронной и синхронной об работкой исключений, включите асинхронный режим, добавив в командную строку компилятора ключ /EHa и удалив ключи /GX и /EHs.
Ситуация ухудшается еще и тем, что в отладочных компоновках _set_se_translator работает правильно. Проблемы возникают только в заключительных компонов ках. Это объясняется тем, что в отладочных компоновках применяется синхрон ная обработка исключений вместо асинхронной, используемой по умолчанию в заключительных компоновках. Из за описанных мной проблем просмотрите свой код и убедитесь в том, что эта функция нигде не вызывается.
API-функция SetUnhandledExceptionFilter
Ошибки имеют привычку никогда не происходить там, где вы их ожидаете. Увы, когда пользователи сталкиваются с ошибкой вашей программы, они просто ви дят диалоговое окно сообщения об ошибке; возможно, Dr. Watson предоставит им некоторую информацию, которую они смогут послать вам, чтобы облегчить по иск проблемы. Как я уже говорил, для получения информации, действительно нужной для исправления ошибок, вы можете разработать собственные диалого вые окна и обработчики. Я всегда называю эти обработчики исключений вместе с соответствующими им фильтрами исключений обработчиками ошибок.
Судя по моему опыту, обработчики ошибок значительно облегчают отладку. Во многих проектах мы получали контроль сразу же после краха приложения, запи сывали всю информацию об ошибке (включая состояние системы пользователя) в файл, и, если проект был клиентским приложением, выводили диалоговое окно с телефонным номером службы поддержки. В некоторых случаях мы реализовы вали возможность циклического изучения основных объектов программы, что позволяло нам опускаться до уровня классов и регистрировать активные объек ты и состояние их данных. Можно сказать, что записываемая нами информация о состоянии программы была чуть ли не избыточной. Такие отчеты об ошибках предоставляли нам 90% ый шанс воспроизведения проблемы пользователя. Если это не проактивная отладка, то я не знаю, что это такое!
Создание обработчиков ошибок обеспечивает API функция SetUnhandledExcep tionFilter. Удивительно, но эта возможность присутствует в Win32 со времен

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


478 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
//////////////////////////////////////////////////////////////////////*/
//Максимальный размер символов для модуля #define MAX_SYM_SIZE 512
#define BUFF_SIZE 2048 #define SYM_BUFF_SIZE 1024
//Константы формата строк. Чтобы избежать частого
//преобразования строк ANSI в формат UNICODE вручную,
//я делаю это с помощью функции wsprintf. Работая с ANSI,
//в строках формата нужно использовать %s, а не %S. #ifdef UNICODE
#define k_NAMEDISPFMT |
_T ( " %S()+%04d byte(s)" ) |
#define k_NAMEFMT |
_T ( " %S " ) |
#define k_FILELINEDISPFMT |
_T ( " %S, line %04d+%04d byte(s)" ) |
#define k_FILELINEFMT |
_T ( " %S, line %04d" ) |
#else |
|
#define k_NAMEDISPFMT |
_T ( " %s()+%04d byte(s)" ) |
#define k_NAMEFMT |
_T ( " %s " ) |
#define k_FILELINEDISPFMT |
_T ( " %s, line %04d+%04d byte(s)" ) |
#define k_FILELINEFMT |
_T ( " %s, line %04d" ) |
#endif |
|
#ifdef _WIN64 |
|
#define k_PARAMFMTSTRING |
_T ( " (0x%016X 0x%016X 0x%016X 0x%016X)" ) |
#else |
|
#define k_PARAMFMTSTRING |
_T ( " (0x%08X 0x%08X 0x%08X 0x%08X)" ) |
#endif |
|
// Определение типа компьютера. #ifdef _X86_
#define CH_MACHINE IMAGE_FILE_MACHINE_I386 #elif _AMD64_
#define CH_MACHINE IMAGE_FILE_MACHINE_AMD64 #elif _IA64_
#define CH_MACHINE IMAGE_FILE_MACHINE_IA64 #else
#pragma FORCE COMPILE ABORT! #endif
/*//////////////////////////////////////////////////////////////////////
//Глобальные переменные с областью видимости файла
//////////////////////////////////////////////////////////////////////*/
//Новый фильтр необработанных исключений (обработчик ошибок)
static PFNCHFILTFN g_pfnCallBack = NULL ;
// Первоначальный фильтр необработанных исключений
static LPTOP_LEVEL_EXCEPTION_FILTER g_pfnOrigFilt = NULL ;
// Массив модулей, ограничивающих применение обработчика ошибок static HMODULE * g_ahMod = NULL ;


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


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