Функция UnhandledExceptionFilter находится в библиотеке kernel32.dll и присутствует в Windows, начиная с версии Windows 95/NT. Назначение этой функции, как видно из названия - обработка необработанных исключений и завершение приложения. В зависимости от того, как запущено приложение, UnhandledExceptionFilter ведет себя по-разному. Так, если приложение находится под отладкой, она передает управление отладчику, в противном случае она выводит диалоговое окно «Application Error». Значит, для обработки необработанных исключений, следует установить свой обработчик не на вершину списка обработчиков, как казалось раньше, а перед обработчиком Runtime библиотеки.
Установка обработчика верхнего уровня
Давайте немного отдохнем и суммируем все сказанное выше. SEH – это системный сервис, в котором унифицирован механизм обработки исключений, все обработчики текущего потока регистрируются в списке регистрации обработчиков исключений. Если в функции встречается конструкция __try … __except, то создается код, который регистрирует новый обработчик исключения и помещает информацию о нем в стек. Во время завершения функции (а точнее, после того, как управление вышло из секции __try), функция разрегистрирует обработчик. Значит, если к текущему моменту в стеке находится три функции, каждая из которых установила свой обработчик исключения, то в списке обработчиков исключения должно находиться по крайней мере три обработчика, а в стеке должны находиться три записи об обработчиках исключений. Информация о текущем обработчике доступна по адресу fs:[0]. Runtime-библиотека регистрирует свой обработчик исключений, который (если исключение не обрабатывается приложением) вызывает функцию UnhandledExceptionFilter, после чего приложение завершается с выводом диалогового окна «Application Error».
Теперь настало время написать код, который бы использовал сказанное выше и подтвердил правильность наших суждений. Давайте напишем простую функцию, которая бы пробегала по всем зарегистрированным обработчикам и выводила информацию о них на экран. Код функции приведен ниже:
void zWalkThroughSEH(){ _EXCEPTION_REGISTRATION * pVCExcRec; __asm mov eax, FS:[0] __asm mov [pVCExcRec], EAX// Перебираем блоки в связанном списке. 0xFFFFFFFF означаетконецсписка. printf("Exception Registration chain:\n"); while (0xFFFFFFFF != (unsigned)(UINT_PTR)pVCExcRec) { printf("\tCurrent SEH record: 0x%X\n\tPrev SEH Record: 0x%X\n\tHandler: 0x%X\n\n", pVCExcRec, pVCExcRec->prev, pVCExcRec->hander); pVCExcRec = (_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);}} |
Вызов эту функции после начала выполнения функции main покажет, что к моменту выполнения функции main в списке обработчиков уже зарегистрированы два обработчика. Текущий обработчик, как было показано раньше, установлен библиотекой Runtime. А вот последний установлен системой.
Exception Registration chain: Current SEH record: 0x12FFB0 Prev SEH Record: 0x12FFE0 Handler: 0x41123A Current SEH record: 0x12FFE0 Prev SEH Record: 0xFFFFFFFFHandler: 0x77E94809 |
Как вы, наверное, уже догадались, создавать свой обработчик и располагать его за системным нет никакого смысла, поскольку исключение будет обработано в runtime-библиотеке, и приложение будет завершено. Тогда я попытался создать свой обработчик и расположил информацию о нем перед обработчиком runtime-библиотеки, выделив для него место в динамической памяти, но увы, мое приложение просто было выгружено из памяти после возникновения исключения, а вставленный обработчик не был выполнен. Как оказалось, так делать нельзя потому, что все записи списка обработчиков исключений должны лежать в стеке, причем каждая следующая запись должна быть расположена выше предыдущей.
Итак, нельзя расположить информацию об обработчике перед информацией о runtime-обработчике. Но никто не мешает переписать значение поля hander обработчика runtime-библиотеки, установив его так, чтобы он указывал на нашу функцию. Код, который реализует это, приведен ниже.
void zHookUpSEHChain(SEHHandler handler){ _EXCEPTION_REGISTRATION * pVCExcRec; __asm mov eax, FS:[0] __asm mov [pVCExcRec], EAX// Перебираем блоки в связанном списке. 0xFFFFFFFF означает конец списка. while (0xFFFFFFFF != (unsigned)(UINT_PTR)pVCExcRec) { if ( (unsigned)(UINT_PTR)pVCExcRec->prev->prev == 0xFFFFFFFF) { defHandler = pVCExcRec->hander; pVCExcRec->hander = handler; break; } pVCExcRec = (_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);}} |
где
defHandler – статическая переменная, в которой сохраняется адрес предыдущего обработчика.
handler – наш обработчик исключения.
Разумеется, внимательный читатель уже заметил некоторую нелогичность в этих суждениях. Зачем пытаться зарегистрировать свой обработчик таким изощренным методом, если достаточно поместить свой блок __try __except в функции main? Дело в том, что при использовании MFC, ATL или какой-то иной библиотеки не имеется доступа к пользовательской точке входа, и, стало быть, нельзя установить свой обработчик.
Сейчас пришло время собрать воедино все сказанное выше и написать небольшую программу, иллюстрирующую способ установки обработчика. К статье прилагается файл ehSimple.cpp, в котором вы найдете код установки обработчика. Первый обработчик реализован в виде класса CatUnhandledExceptionFilter, объявленного следующим образом:
class CatUnhandledExceptionFilter{private: // SEHHandler oldHandler – переменная, в которую будет записан адрес //предыдущего обработчика исключения. Объявление типа SEHHandler // былоприведеновыше. static SEHHandler oldHandler; static void zHookUpSEHChain(SEHHandler handler); static int myHandler(PEXCEPTION_RECORD pEhRecors, PEXCEPTION_REGISTRATION pEhRegRecord, PCONTEXT pContext, void* pp);public: CatUnhandledExceptionFilter(); ~CatUnhandledExceptionFilter();}; |
static void zHookUpSEHChain(SEHHandler handler); – это функция для подмены обработчика исключений runtime-библиотеки. Код ее почти не отличается от предложенного ранее. Единственным изменением является переменная, в которой сохраняется адрес предыдущего обработчика.
static int myHandler(PEXCEPTION_RECORD pEhRecors, PEXCEPTION_REGISTRATION pEhRegRecord, PCONTEXT pContext, PEXCEPTION_RECORD pp); - это наш обработчик, который будет вызван в случае возникновения необработанного исключения.
int CatUnhandledExceptionFilter::myHandler(PEXCEPTION_RECORD pEhRecors, PEXCEPTION_REGISTRATION pEhRegRecord, PCONTEXT pContext, void* pp){ printf("*** In My Handler ***\n"); printf("Exception address: 0x%X\n", pEhRecors->ExceptionAddress); printf("Exception code: 0x%X\n", pEhRecors->ExceptionCode); return CatUnhandledExceptionFilter::oldHandler(pEhRecors, pEhRegRecord, pContext, pp);} |
В программе создается статический объект типа CatUnhandledExceptionFilter. Во время создания этого объекта в конструкторе вызывается функция подмены самого верхнего обработчика исключений. После того, как статические объекты приложения созданы, runtime передает управление функции main, в которой генерируется исключение по доступу к памяти, в результате чего управление переходит нашему обработчику исключения, который сейчас не делает ничего, а просто выводит информацию об исключении на экран и передает управление подмененному обработчику.
После возникновения необработанного исключения операционная система вызывает наш обработчик для обработки исключения и он, выполнив то, что ему нужно, передет управление дальше.
Недостатком приведенного выше кода является то, что он работоспособен только в однопоточных приложениях. Все это происходит потому, что указатель на вершину цепочки EXCEPTION_REGISTRATION находится в структуре TIB, которая хранит в себе информацию о текущем потоке, а значит, при вставке обработчика исключения с использованием значение FS:[0] мы установим обработчик только для одного потока.
Но что же делать в случае многопоточного приложения? Для этого, видимо, нужно проделать описанные действия для каждого потока. К счастью, этого делать не придется. В следующем разделе вы увидите, как обработать необработанные исключения во всех потоках.
Фильтр необработанных исключений приложения
Как уже упоминалось раньше, если ОС не нашла подходящего обработчика исключений, она вызывает функцию UnhandledExceptionFilter. Эта функция находится в kernel32.dll и имеется во всех версиях Windows. В заголовочных файлах SDK она объявлена следующим образом:
LONG UnhandledExceptionFilter(_EXCEPTION_POINTERS *ExceptionInfo); |
где ExceptionInfo – это указатель на структуру, которая описывает исключение и содержимое регистров во время возникновения исключения.
Назначение этой функции – показать диалоговое окно, сообщающее, что произошло необработанное исключение, и, в зависимости от того, находится приложение под отладкой или нет, передать управление отладчику или завершить поток. Еще одной функцией этого фильтра является вызов пользовательского обработчика необработанных исключений, который может быть установлен функцией SetUnhandledExceptionFilter.
Я сознательно не упоминал о функции SetUnhandledExceptionFilter, потому что ее использование имеет ряд важных недостатков, перекрывающих преимущества, предоставляемых ее использованием. Чтобы внести ясность, давайте рассмотрим ее код:
SetUnhandledExceptionFilter:77E7E5A1 mov ecx,dword ptr [esp+4] 77E7E5A5 mov eax,dword ptr ds:[77ED73B4h] 77E7E5AA mov dword ptr ds:[77ED73B4h],ecx 77E7E5B0 ret 4 |
Как видно из кода, функция берет адрес, переданный ей в качестве параметра, и помещает его в системную область памяти, возвращая предыдущее значение. Т.е. она замещает установленный ранее обработчик новым. Такое поведение говорит, что использование этой функции может привести к тому, что обработчик может быть переустановлен кем угодно.