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

         

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 ( "<UNKNOWN>")); 

}

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 — самое подходящее место, чтобы начать ее изучение.



Эта глава посвящена обработчикам программных



Эта глава посвящена обработчикам программных аварий, которые разделяются на обработчики исключений и фильтры необработанных исключений. Оба типа аварийных обработчиков позволяют получать большое количество информации об авариях приложения. Фильтры необработанных исключений работают как с Visual C++, так и с Visual Basic, а обработка исключений — только с Visual C++.
Иногда путают C++- и SEH-исключения. Первые обеспечены спецификациями языка C++, тогда как SEH-исключения — операционной системой. Эти два вида обработки исключений совершенно различны. К счастью, можно комбинировать C++- и SHE-исключения с помощью функции _set_se_translator исполнительной библиотеки C++. Необходимо убедиться, что вы понимаете различия между асинхронной и синхронной обработкой исключений в C++. Проще всего сформулировать эти различия примерно так: "Асинхронность имеет место тогда, когда все функции программы отслеживают время жизни объектов, а синхронность — когда только некоторые функции отслеживают это время".
Важную роль в организации работы аварийных обработчиков играет функция SetunhandiedExceptionFiiter, которая позволяет установить окончательный SEH-фильтр исключений. Окончательный фильтр исключений позволяет получать управление непосредственно перед тем, как будет раскрыто диалоговое окно Application Error, чтобы можно было записать в него всю информацию о причинах аварии. Представленный в конце раздела код (CrashHandler) позволит облегчить установку фильтров необработанных исключений и выполнит всю достаточно трудную работу по трансляции аварийной информации таким образом, чтобы вы могли сконцентрироваться на ее отображении и на особенно ответственных участках своего приложения.

Структурированная обработка исключений


Структурированную обработку исключений (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 год.



Структурированная обработка исключений (SEH) и обработка исключений в C++


Ускорение обработки исключений (exeption handling) — довольно сложная задача, потому что в C++ используется два основных типа обработки исключений: структурированная обработка исключений (Structured Exception Handling — SEH), которую обеспечивает операционная система, и обработка исключений средствами языка C++. Выбирать, какой тип обработки исключений и когда использовать, достаточно сложно, причем этому выбору не помогает даже то, что многие считают оба типа взаимозаменяемыми. Дело в том, что каждый тип обработки исключений использует совершенно различный подход. Думаю, многих сбивает с толку то, что можно комбинировать оба типа. В следующих разделах описаны сходства и различия этих двух типов обработки исключений. Рассмотрены, кроме того, некоторые варианты их совместного использования.



Трансляция структур 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

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

 


Рис. 9.1. Диалоговое окно программы CrashTest

Чтобы продемонстрировать эти функции в действии, я включил в сопровождающий CD два примера тестовых программ. Первая, CH_TEST, написана на C/C++, а вторая, CrashTest, — на Visual Basic. По этим двум программам можно получить довольно хорошее представление о том, как использовать все рассмотренные здесь функции. На рис. 9.1 показано диалоговое окно программы CrashTest с информацией аварийного сбоя.