Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

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

.pdf
Скачиваний:
353
Добавлен:
13.08.2013
Размер:
3.3 Mб
Скачать

ГЛАВА 13 Обработчики ошибок

473

 

 

мне работать над их проблемой по вечерам. Меня это вполне устраивало,

ия сказал, что смогу работать над их ошибкой каждый день до полуночи, а он тут же отодвинул трубку от уха и прокричал кому то: «Он будет в Сиэт ле! Немедленно вылетайте!» Менеджер сказал мне, что отправил в аэропорт двух программистов с необходимым оборудованием. Когда я спросил его, что случится, если мы не решим проблему до того, как мне придется уехать из Сиэтла, он ответил: «Мы последуем за вами в Нью Хэмпшир». Ошибка ав томатически перешла в категорию суперсерьезных.

Следующим вечером я приехал в отель на встречу с этими программис тами и увидел двух человек, едва стоявших на ногах. Они работали над этой ошибкой примерно три недели подряд почти без перерыва. Увидев прило жение, я сразу покрылся холодным потом! Они разрабатывали модуль GINA (Graphical Identification and Authentication, — графическая идентификация

иаутентификация), предназначенный для регистрации на терминальном сервере при помощи смарт карты! Да уж, трудно представить более непри ятного приложения для отладки! Так как значительная часть программы выполнялась внутри LSASS.EXE, вы могли начать отладку, но любой щелчок вне отладчика блокировал компьютер. Чтобы сделать мою жизнь еще ин тересней, программисты повсюду использовали STL, поэтому программа не только содержала в себе ошибку, но и была очень тяжелой в понимании. Всем нам стоит помнить, что основное достоинство STL заключается не в повторно используемых структурах данных, а в гарантии занятости. Пони мание и сопровождение кода, написанного при помощи STL, доступно только его автору, что обеспечивает ему уверенность в завтрашнем дне и постоян ный источник дохода.

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

иуказали 10 или 12 мест, где они сталкивались с ошибками. Сначала я пред положил, что ошибка объясняется записью данных в неинициализирован ную память. Потратив несколько часов на изучение работы системы и при выкание к отладке их приложения, я попытался найти эти неинициализи рованные указатели. Пробираясь через дебри исходного кода, я обнаружил огромное число блоков catch (...). К концу первого вечера я сказал им уда лить все операторы catch (...), чтобы мы могли видеть искажение данных немедленно и начали сужать диапазон причин проблемы.

Исход

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

//catch ( ... )

//{

return ( FALSE ) ;

//}

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

474 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

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

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

Полученный опыт

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

Не используйте _set_se_translator

В первом издании этой книги я описал интересную API функцию _set_se_translator, которая волшебным образом преобразует ваши ошибки SEH в исключения C++. Для этого она вызывает определенную вами функцию, вызывающую throw для каж дого типа, который вы хотите использовать для преобразования. Сейчас я могу признаться, что тот мой совет оказался ошибочным, хотя в его основе и лежали добрые намерения. При использовании _set_se_translator вы быстро обнаружи те, что в заключительных компоновках она не работает.

Первая проблема с _set_se_translator в том, что она не является глобальной; область ее действия ограничена конкретным потоком. Это значит, что вам, веро ятно, придется переработать свою программу, чтобы гарантировать вызов _set_se_trans lator в начале каждого потока. Увы, это не всегда просто. Кроме того, если вы разрабатываете компонент, используемый другими, не контролируемыми вами процессами, _set_se_translator полностью запутает обработку исключений этих процессов, если они ожидают исключения SEH, а вместо этого получают исклю чения C++.

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

Асинхронные исключения имеют один недостаток: компилятор должен сгене рировать для каждой функции то, что называется кодом слежения за временем жизни объекта (object lifetime tracking code). Компилятор полагает, что исключе ние может быть сгенерировано любой командой, поэтому каждая функция, по мещающая объект C++ в стек, должна включать код, гарантирующий при возник

ГЛАВА 13 Обработчики ошибок

475

 

 

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

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

Ситуация ухудшается еще и тем, что в отладочных компоновках _set_se_translator работает правильно. Проблемы возникают только в заключительных компонов ках. Это объясняется тем, что в отладочных компоновках применяется синхрон ная обработка исключений вместо асинхронной, используемой по умолчанию в заключительных компоновках. Из за описанных мной проблем просмотрите свой код и убедитесь в том, что эта функция нигде не вызывается.

API-функция SetUnhandledExceptionFilter

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

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

Создание обработчиков ошибок обеспечивает API функция SetUnhandledExcep tionFilter. Удивительно, но эта возможность присутствует в Win32 со времен

476 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

Microsoft Windows NT 3.5, однако почти не упоминается в документации. На мо мент написания этой главы данная функция встречается в MSDN Online только восемь раз.

Надо сказать, что я нашел эту функцию очень эффективной. Просто взглянув на ее имя — SetUnhandledExceptionFilter, вы можете догадаться, что она делает. Она позволяет указать функцию фильтр необработанных исключений, которая будет вызываться при возникновении в процессе необработанного исключения. Един ственным параметром SetUnhandledExceptionFilter является указатель на функцию фильтр исключений, которая вызывается в заключительном блоке __except при ложения. Этот фильтр исключений может возвращать те же значения, что и лю бой другой фильтр исключений: EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_EXE CUTION или EXCEPTION_CONTINUE_SEARCH. Вы можете выполнять в фильтре исключений почти любые действия по их обработке, но при этом нужно помнить о перепол нении стека. Чтобы обезопасить себя, вам, вероятно, следует избегать вызовов функций стандартной библиотеки C, а также MFC. Я должен был предупредить вас об этих неприятностях, однако я могу гарантировать, что большинство ваших ошибок будет ошибками нарушения доступа, поэтому, реализуя в фильтре и об работчике исключений полную систему обработки ошибок, вы, скорее всего, не столкнетесь с какими либо проблемами, если будете проверять причину исклю чения и избегать вызовов функций при переполнении стека.

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

Используя SetUnhandledExceptionFilter, нужно кое о чем помнить. Во первых, вы не сможете отлаживать установленный фильтр необработанных исключений, применяя стандартные отладчики пользовательского режима. Это ограничение определенно имеет смысл, так как при выполнении программы под отладчиком ОС должна взять на себя управление заключительным фильтром исключений, чтобы сообщить отладчику правильную информацию о последней ошибке. Это затруд няет отладку заключительного обработчика ошибок. Одно из возможных реше ний данной проблемы — вызвать фильтр необработанных исключений из обыч ного фильтра исключений SEH. Пример такого подхода вы найдете в функции Baz из файла BugslayerUtil\Tests\CrashHandler\CrashHandler.CPP, который находится на CD.

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

ГЛАВА 13 Обработчики ошибок

477

 

 

Стандартный вопрос отладки

Что можно сделать с переполнением стека при бесконечной рекурсии?

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

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

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

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

Листинг 13-4. CRASHHANDLER.CPP

/*——————————————————————————————————————————————————————————————————————

Отладка приложений для Microsoft .NET и Microsoft Windows Copyright © 1997 2003 John Robbins — All rights reserved.

——————————————————————————————————————————————————————————————————————*/

#include "pch.h"

#include "BugslayerUtil.h" #include "CrashHandler.h"

// Внутренний заголовочный файл проекта #include "Internal.h"

/*////////////////////////////////////////////////////////////////////// // Определения с областью видимости файла

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

478 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

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

//Максимальный размер символов для модуля #define MAX_SYM_SIZE 512

#define BUFF_SIZE 2048 #define SYM_BUFF_SIZE 1024

//Константы формата строк. Чтобы избежать частого

//преобразования строк ANSI в формат UNICODE вручную,

//я делаю это с помощью функции wsprintf. Работая с ANSI,

//в строках формата нужно использовать %s, а не %S. #ifdef UNICODE

#define k_NAMEDISPFMT

_T ( " %S()+%04d byte(s)" )

#define k_NAMEFMT

_T ( " %S " )

#define k_FILELINEDISPFMT

_T ( " %S, line %04d+%04d byte(s)" )

#define k_FILELINEFMT

_T ( " %S, line %04d" )

#else

 

#define k_NAMEDISPFMT

_T ( " %s()+%04d byte(s)" )

#define k_NAMEFMT

_T ( " %s " )

#define k_FILELINEDISPFMT

_T ( " %s, line %04d+%04d byte(s)" )

#define k_FILELINEFMT

_T ( " %s, line %04d" )

#endif

 

#ifdef _WIN64

 

#define k_PARAMFMTSTRING

_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 ;

ГЛАВА 13 Обработчики ошибок

479

 

 

//Размер массива g_ahMod в элементах static UINT g_uiModCount = 0 ;

//Статический буфер, возвращаемый различными функциями,

//обеспечивает передачу данных без использования стека. static TCHAR g_szBuff [ BUFF_SIZE ] ;

//Статический буфер для просмотра символов

static BYTE g_stSymbol [ SYM_BUFF_SIZE ] ;

// Статическая структура, содержащая сведения

//об исходном файле и номере строки static IMAGEHLP_LINE64 g_stLine ;

//Структура кадра стека, используемая при анализе стека static STACKFRAME64 g_stFrame ;

//Флаг, указывающий на состояние инициализации символьной машины static BOOL g_bSymEngInit = FALSE ;

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

//структуру CONTEXT. Поэтому, если пользователь применял для

//записи минидампа структуру EXCEPTION_POINTERS, включающую

//указатель на CONTEXT, дамп получался некорректным. Теперь

//я сохраняю CONTEXT глобально, во многом как и кадр стека. static CONTEXT g_stContext ;

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

//Объявления функций с областью видимости файла

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

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

LONG __stdcall CrashHandlerExceptionFilter ( EXCEPTION_POINTERS *

 

pExPtrs

) ;

//Функция преобразования идентификатора

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

LPCTSTR ConvertSimpleException ( DWORD dwExcept ) ;

//Внутренняя функция, отвечающая за весь анализ стека

LPCTSTR __stdcall InternalGetStackTraceString ( DWORD dwOpts ) ;

//Функция, инициализирующая в случае надобности символьную машину void InitSymEng ( void ) ;

//Функция, очищающая в случае надобности символьную машину

void CleanupSymEng ( void ) ;

/*////////////////////////////////////////////////////////////////////// // Класс деструктора

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

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

480ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода

//См. примечание об автоматических классах в MEMDUMPERVALIDATOR.CPP.

//Отключение предупреждения "инициализаторы в области инициализации

//библиотеки" (initializers put in library initialization area)

#pragma warning (disable : 4073) #pragma init_seg(lib)

class CleanUpCrashHandler

{

public :

CleanUpCrashHandler ( void )

{

}

~CleanUpCrashHandler ( void )

{

// Имеются ли неосвобожденные блоки выделенной памяти? if ( NULL != g_ahMod )

{

VERIFY ( HeapFree ( GetProcessHeap ( ) ,

 

 

0

,

 

 

 

g_ahMod

 

) ) ;

g_ahMod = NULL

;

 

 

 

// ИСПРАВЛЕННАЯ ОШИБКА. Спасибо Геннадию

Майко (Gennady Mayko).

g_uiModCount =

0

;

 

 

}

if ( NULL != g_pfnOrigFilt )

{

// Восстановление первоначального фильтра необработанных исключений. SetUnhandledExceptionFilter ( g_pfnOrigFilt ) ;

g_pfnOrigFilt = NULL ;

}

}

} ;

// Статический класс

static CleanUpCrashHandler g_cBeforeAndAfter ;

/*////////////////////////////////////////////////////////////////////// // Реализация функций обработчика ошибок.

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

BOOL __stdcall SetCrashHandlerFilter ( PFNCHFILTFN pFn )

{

// Если pFn равен NULL, новый фильтр необработанных исключений удаляется. if ( NULL == pFn )

{

if ( NULL != g_pfnOrigFilt )

{

// Восстановление первоначального фильтра необработанных исключений. SetUnhandledExceptionFilter ( g_pfnOrigFilt ) ;

g_pfnOrigFilt = NULL ; if ( NULL != g_ahMod )

ГЛАВА 13 Обработчики ошибок

481

 

 

{

//ИСПРАВЛЕННАЯ ОШИБКА:

//Раньше я вызвал функцию "free" вместо "HeapFree." VERIFY ( HeapFree ( GetProcessHeap ( ) ,

 

0

,

 

g_ahMod

) ) ;

g_ahMod = NULL ;

 

 

// ИСПРАВЛЕННАЯ ОШИБКА. Спасибо Геннадию Майко.

g_uiModCount = 0

;

 

}

 

 

g_pfnCallBack = NULL ;

}

}

else

{

ASSERT ( FALSE == IsBadCodePtr ( (FARPROC)pFn ) ) ; if ( TRUE == IsBadCodePtr ( (FARPROC)pFn ) )

{

return ( FALSE ) ;

}

g_pfnCallBack = pFn ;

//Если новый обработчик ошибок еще не установлен, выполняется

//установка 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 |

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

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 ;

}

Соседние файлы в предмете Программирование на C++