Win32™ SEH изнутри (ч.3)

Дата публикации 11 авг 2005

Win32™ SEH изнутри (ч.3) — Архив WASM.RU

Раскрутка

(листинг)

Перед тем как погрузиться в код, осуществляющий раскрутку (unwinding), давайте кратко обсудим, что это собственно такое. Ранее я описал, как потенциальные обработчики исключений сохраняется в связанном списке, на который указывает первый DWORD в TIB (FS:[0]). Так как нужный обработчик исключения может находиться не в начале списка, необходим механизм для удаления всех обработчиков исключений находящихся в списке перед ним.

Как вы видели в Visual C++-функции __except_handler3, раскрутка реализуется с помощью RTL-функции __global_unwind2. Эта функция представляет собой очень тонкую обертку вокруг не документированной API-функции RtlUnwind:

__global_unwind2(void * pRegistFrame)
 {
     _RtlUnwind( pRegistFrame,
                 &__ret_label,
                 0, 0 );
     __ret_label:
 }

Хотя функция RtlUnwind является критически важным API для реализации SEH на уровне компилятора, она нигде не документирована. Хотя технически эта функция располагается в KERNEL32, в Windows NT KERNEL32.DLL переадресует ее вызов к NTDLL.DLL, который также имеет функцию RtlUnwind. На рис. 12 показан примерный псевдокод, который я набросал для неё.

void _RtlUnwind( PEXCEPTION_REGISTRATION pRegistrationFrame,
                  PVOID returnAddr,  // Не используется на процессорах i386+!
                  PEXCEPTION_RECORD pExcptRec,
                  DWORD _eax_value ) 
 {
     DWORD   stackUserBase;
     DWORD   stackUserTop;
     PEXCEPTION_RECORD pExcptRec;
     EXCEPTION_RECORD  exceptRec;    
     CONTEXT context;
 
     // Получаем границы стека из FS:[4] и FS:[8].
     RtlpGetStackLimits( &stackUserBase, &stackUserTop );
 
     if ( 0 == pExcptRec )   // Нормальный случай.
     {
         pExcptRec = &excptRec;
 
         pExcptRec->ExceptionFlags = 0;
         pExcptRec->ExceptionCode = STATUS_UNWIND;
         pExcptRec->ExceptionRecord = 0;
         // Получить адрес возврата из стека.
         pExcptRec->ExceptionAddress = RtlpGetReturnAddress();
         pExcptRec->ExceptionInformation[0] = 0;
     }
 
     if ( pRegistrationFrame )
         pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING;
     else
         pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND);
 
     context.ContextFlags =
         (CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS);
 
     RtlpCaptureContext( &context );
 
     context.Esp += 0x10;
     context.Eax = _eax_value;
 
     PEXCEPTION_REGISTRATION pExcptRegHead;
 
     pExcptRegHead = RtlpGetRegistrationHead();  // Восстанавливаем значение в FS:[0].
 
     // Начинаем перебор связанного списка структур EXCEPTION_REGISTRATION
     while ( -1 != pExcptRegHead )
     {
         EXCEPTION_RECORD excptRec2;
 
         if ( pExcptRegHead == pRegistrationFrame )
         {
             _NtContinue( &context, 0 );
         }
         else
         {
             // Если начало связанного списка структур EXCEPTION_REGISTRATION
             // находится в более старшем адресе, чем тот адрес,
             // который был передан в эту функцию в параметре pRegistrationFrame, 
             // считаем, что возникла проблема.
             if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) )
             {
                 // Чтобы сообщить ОС о возникшей ситуации, вызываем исключение.
                 excptRec2.ExceptionRecord = pExcptRec;
                 excptRec2.NumberParameters = 0;
                 excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
                 excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;    
 
                 _RtlRaiseException( &exceptRec2 );
             }
         }
 
         PVOID pStack = pExcptRegHead + 8; // 8==sizeof(EXCEPTION_REGISTRATION)
 
         if (    (stackUserBase <= pExcptRegHead ) // Удостоверяемся что
             &&  (stackUserTop >= pStack ) // адрес находящийся в pExcptRegHead
             &&  (0 == (pExcptRegHead & 3)) ) // выровнен на границе DWORD.
         {
             DWORD pNewRegistHead;
             DWORD retValue;
 
             retValue = RtlpExecutehandlerForUnwind(
                             pExcptRec, pExcptRegHead, &context,
                             &pNewRegistHead, pExceptRegHead->handler );
 
             if ( retValue != DISPOSITION_CONTINUE_SEARCH )
             {
                 if ( retValue != DISPOSITION_COLLIDED_UNWIND )
                 {
                     excptRec2.ExceptionRecord = pExcptRec;
              excptRec2.NumberParameters = 0;
                     excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION;
                     excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;    
 
                     RtlRaiseException( &excptRec2 );
                 }
                 else
                     pExcptRegHead = pNewRegistHead;
             }
 
             PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead;
             pExcptRegHead = pExcptRegHead->prev;
 
             RtlpUnlinkHandler( pCurrExcptReg );
         }
         else    // Адрес не выровнен!  Чтобы сообщить ОС о возникшей ситуации,
                 // вызываем исключение. 
         { 
             excptRec2.ExceptionRecord = pExcptRec;
             excptRec2.NumberParameters = 0;
             excptRec2.ExceptionCode = STATUS_BAD_STACK;
             excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;    
 
             RtlRaiseException( &excptRec2 );
         }
     }
 
     // Если управление дошло сюда, значит, мы достигли конца списка
     // EXCEPTION_REGISTRATION.
     // Это обычно не происходит.
 
     if ( -1 == pRegistrationFrame )
         NtContinue( &context, 0 );
     else
         NtRaiseException( pExcptRec, &context, 0 );
 
 }
 
 PEXCEPTION_REGISTRATION
 RtlpGetRegistrationHead( void )
 {
     return FS:[0];
 }
 
 _RtlpUnlinkHandler( PEXCEPTION_REGISTRATION pRegistrationFrame )
 {
     FS:[0] = pRegistrationFrame->prev;
 }
 
 void _RtlpCaptureContext( CONTEXT * pContext )
 {
     pContext->Eax = 0;
     pContext->Ecx = 0;
     pContext->Edx = 0;
     pContext->Ebx = 0;
     pContext->Esi = 0;
     pContext->Edi = 0;
     pContext->SegCs = CS;
     pContext->SegDs = DS;
     pContext->SegEs = ES;
     pContext->SegFs = FS;
     pContext->SegGs = GS;
     pContext->SegSs = SS;
     pContext->EFlags = flags; // __asm{ PUSHFD / pop [xxxxxxxx] }
     pContext->Eip = адрес следующей инструкции в защищенном коде
     (т.е. в том коде, где произошло исключение).
     pContext->Ebp = значение регистра EBP в защищенном коде
     (т.е. в том коде, где произошло исключение).
     pContext->Esp = Context.Ebp + 8
 }
Рисунок 12. Псевдокод RtlUnwind.

Хотя RtlUnwind выглядит внушительно, её не сложно понять, если быть внимательным. Работа этой API начинается с того, что она получает текущую верхнюю и нижнюю границу стека потока из FS:[4] и FS:[8]. Эти значения будут важны позже при проверке того, что все раскручиваемые кадры исключений находятся внутри области стека.

Далее RtlUnwind создает в стеке фиктивную структуру EXCEPTION_RECORD, и устанавливает в поле ExceptionCode значение STATUS_UNWIND. Также, устанавливается флажок EXCEPTION_UNWINDING в поле ExceptionFlags принадлежащем этой структуре. Позже указатель на эту структуру будет передаваться в качестве параметра каждому обработчику исключений. После этого, она вызывает функцию _RtlpCaptureContext, чтобы создать фиктивную структуру CONTEXT, которая также будет использоваться в качестве параметра при вызовах обработчиков исключений для раскрутки.

Остальная часть RtlUnwind занимается перебором элементов в связанном списке структур EXCEPTION_REGISTRATION. Её код, для каждого фрейма, вызывает функцию RtlpExecuteHandlerForUnwind, которую я рассмотрю позже. Именно эта функция вызывает обработчик исключений с установленным флажком EXCEPTION_UNWINDING. После каждого вызова, соответствующий фрейм исключения удаляется с помощью вызова функции RtlpUnlinkHandler.

RtlUnwind заканчивает раскрутку фреймов, после того как добирается до фрейма адрес которого был передан ему в первом параметре. В код, который я описал, включён код проверки обеспечивающий правильное выполнение. Если случится какая-нибудь неприятность делающая продолжение раскрутки невозможным, RtlUnwind вызовет исключение, указывающее тип проблемы, и такое исключение будет иметь установленный флажок EXCEPTION_NONCONTINUABLE. Если у возникшего исключения установлен этот флажок, система не даст процессу, в котором оно возникло, продолжить свою работу, уничтожив его.

Необработанные исключения

Ранее в статье, я откладывал полное описание API UnhandledExceptionFilter. Обычно вам не нужно вызывать эту API напрямую (хотя вы можете это сделать). В большинстве случаев, её вызывает код выражения-фильтра из дефолтного обработчика исключений находящегося в KERNEL32. Это показано в приведенном ранее псевдокоде функции BaseProcessStart.

На рис. 13 показан написанный мой псевдокод функции UnhandledExceptionFilter. Её начало выглядит немного странно (по крайней мере, мне так кажется). Если произошедшая ошибка - EXCEPTION_ACCESS_VIOLATION, код вызывает функцию _BasepCheckForReadOnlyResource. Хотя я не представил здесь псевдокод этой функции, тем не менее, я могу описать ее работу. Если исключение возникло, из-за попытки записи в раздел ресурсов (.rsrc) в EXE или DLL, функция _BasepCurrentTopLevelFilter изменяет атрибуты страницы, при обращении к которой произошло исключение (в нормальном состоянии эта страница имеет атрибут "Read Only"), тем самым, разрешая запись на эту страницу. Если происходит именно это, UnhandledExceptionFilter возвращает значение EXCEPTION_CONTINUE_EXECUTION, после чего происходит рестарт операции вызвавшей исключение.

UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *pExceptionPtrs )
 {
     PEXCEPTION_RECORD pExcptRec;
     DWORD currentESP;
     DWORD retValue;
     DWORD DEBUGPORT;
     DWORD   dwTemp2;
     DWORD   dwUseJustInTimeDebugger;
     CHAR    szDbgCmdFmt[256];   // Шаблон строки получаемой из ключа системного
                                 // реестра: AeDebug.
     CHAR    szDbgCmdLine[256];  // После заполнения, здесь будет находиться
                                 // командная строка для вызова отладчика.
     STARTUPINFO startupinfo;
     PROCESS_INFORMATION pi;
     HARDERR_STRUCT  harderr;    // ???
     BOOL fAeDebugAuto;
     TIB * pTib;                 // Блок информации потока (TIB)
 
     pExcptRec = pExceptionPtrs->ExceptionRecord;
 
     if (   (pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
         && (pExcptRec->ExceptionInformation[0]) )
     {
         retValue = 
             _BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]);
 
         if ( EXCEPTION_CONTINUE_EXECUTION == retValue )
             return EXCEPTION_CONTINUE_EXECUTION;
      }
 
     // Смотрим, выполняется ли этот процесс под управлением отладчика.
     retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort,
                                          &debugPort, sizeof(debugPort), 0 );
 
     if ( (retValue >= 0) && debugPort )     // Передаем исключение отладчику.
         return EXCEPTION_CONTINUE_SEARCH;
 
     // Вызывала ли пользовательская программа API-функцию
     // SetUnhandledExceptionFilter?  Если да, вызываем установленную
     // ей функцию обратного вызова (т.е. производим обратный вызов).
 
     if ( _BasepCurrentTopLevelFilter )
     {
         retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs );
 
         if ( EXCEPTION_EXECUTE_HANDLER == retValue )
             return EXCEPTION_EXECUTE_HANDLER;
         
         if ( EXCEPTION_CONTINUE_EXECUTION == retValue )
             return EXCEPTION_CONTINUE_EXECUTION;
 
         // Далее обрабатываются только EXCEPTION_CONTINUE_SEARCH.
     }
 
     // Вызывалась ли функция SetErrorMode с параметром SEM_NOGPFAULTERRORBOX?
     if ( 0 == (GetErrorMode() & SEM_NOGPFAULTERRORBOX) )
     {
         harderr.elem0 = pExcptRec->ExceptionCode;
         harderr.elem1 = pExcptRec->ExceptionAddress;
 
         if ( EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode )
             harderr.elem2 = pExcptRec->ExceptionInformation[2];
         else
             harderr.elem2 = pExcptRec->ExceptionInformation[0];
 
         dwTemp2 = 1;
         fAeDebugAuto = FALSE;
 
         harderr.elem3 = pExcptRec->ExceptionInformation[1];
 
         pTib = FS:[18h];
 
         DWORD someVal = pTib->pProcess->0xC;
 
         if ( pTib->threadID != someVal )
         {
             __try
 			{
                 char szDbgCmdFmt[256]
                 retValue = _GetProfileStringA( "AeDebug", "Debugger", 0,
                                      szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 );
 
                 if ( retValue )
                     dwTemp2 = 2;
 
                 char szAuto[8]
 
                 retValue = GetProfileStringA(   "AeDebug", "Auto", "0",
                                                 szAuto, sizeof(szAuto)-1 );
                 if ( retValue )
                     if ( 0 == strcmp( szAuto, "1" ) )
                         if ( 2 == dwTemp2 )
                             fAeDebugAuto = TRUE;
             }
             __except( EXCEPTION_EXECUTE_HANDLER )
             {
                 ESP = currentESP;
                 dwTemp2 = 1
                 fAeDebugAuto = FALSE;
             }
         }
 
         if ( FALSE == fAeDebugAuto )
         {
             retValue =  NtRaiseHardError(
                                 STATUS_UNHANDLED_EXCEPTION | 0x10000000,
                                 4, 0, &harderr,
                                 _BasepAlreadyHadHardError ? 1 : dwTemp2,
                                 &dwUseJustInTimeDebugger );
         }
         else
         {
             dwUseJustInTimeDebugger = 3;
             retValue = 0;
         }
 
         if (    retValue >= 0 
             &&  ( dwUseJustInTimeDebugger == 3)
             &&  ( !_BasepAlreadyHadHardError )
             &&  ( !_BaseRunningInServerProcess ) )
         {
             _BasepAlreadyHadHardError = 1;
 
             SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE };
 
             HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 );
 
             memset( &startupinfo, 0, sizeof(startupinfo) );
 
             sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent);
 
             startupinfo.cb = sizeof(startupinfo);
             startupinfo.lpDesktop = "Winsta0\Default"
 
             CsrIdentifyAlertableThread();   // ???
 
             retValue = CreateProcessA(
                             0,              // lpApplicationName
                             szDbgCmdLine,   // Коммандная строка
                             0, 0,           // атрибуты защиты процесса и потока
                             1,              // bInheritHandles
                             0, 0,           // флаги (влияющие на то, как именно
                                             // создается новый процесс) и блок
                                             // переменных окружения
                             0,              // текущая директория.
                             &statupinfo,    // STARTUPINFO
                             &pi );          // PROCESS_INFORMATION
 
             if ( retValue && hEvent )
             {
                 NtWaitForSingleObject( hEvent, 1, 0 );
                 return EXCEPTION_CONTINUE_SEARCH;
             }
         }
 
         if ( _BasepAlreadyHadHardError )
             NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode);
     }
 
     return EXCEPTION_EXECUTE_HANDLER;
 }
 
 LPTOP_LEVEL_EXCEPTION_FILTER
 SetUnhandledExceptionFilter(
     LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );   
 { 
     // Глобальная переменная _BasepCurrentTopLevelFilter находится в KERNEL32.DLL.
     LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter;
 
     // Устанавливаем новое значение.
     _BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter;
 
     return previous;    // Возвращаем старое значение.
 }
Рисунок 13. Псевдокод UnHandledExceptionFilter.

Следующая задача UnhandledExceptionFilter состоит в том, чтобы определить, выполняется ли процесс под управлением Win32-отладчика. То есть, был ли, при создании этого процесса, установлен флажок DEBUG_ONLY_THIS_PROCESS, или флажок DEBUG_PROCESS. UnhandledExceptionFilter использует для этого функцию NtQueryInformationProcess (эту функцию я описывал в этом месяце, в колонке Under the Hood). Если процесс действительно запущен под отладчиком, UnhandledExceptionFilter возвращает значение EXCEPTION_CONTINUE_SEARCH, которое заставляет ОС разбудить отладчик, и сообщить ему, что произошло исключение в отлаживаемой им программе.

Затем UnhandledExceptionFilter вызывает установленный пользователем фильтр необрабатываемых исключений, если он присутствует. Обычно, эта callback-функция отсутствует, но может быть установлена с помощью API-функции SetUnhandledExceptionFilter. Я не буду приводить здесь псевдокод для этой API. API UnhandledExceptionFilter просто помещает в глобальную переменную новый адрес пользовательской callback-функции, и возвращает адрес старой.

После подготовительных мероприятий, UnhandledExceptionFilter может переходить к своей основной задаче: информировать вас о вашей позорной ошибке в программе, с помощью стильного диалога Application Error. Существует два пути, следуя которым можно избежать появления этого диалога. Первый путь заключается в том, что процесс должен вызвать (до возникновения исключения) функцию SetErrorMode, указав ей флажок SEM_NOGPFAULTERRORBOX. Другой метод состоит в том, что значение параметра Auto, находящегося в разделе системного реестра AeDebug, должно быть равно единице. В этом случае, UnhandledExceptionFilter не показывает диалог Application Error, а вместо этого автоматически запускает отладчик, который определен в параметре Debugger, находящийся в разделе системного реестра AeDebug. Если вы знакомы с механизмом "just in time debugging" то знайте, что именно с помощью этого операционная система поддерживает данный механизм. Позже мы поговорим об этом более подробно.

В большинстве случаев, ни один из этих путей не используется, и UnhandledExceptionFilter вызывает функцию NtRaiseHardError, находящуюся в NTDLL.DLL. Именно эта функция показывает диалог Application Error. Этот диалог ждет, пока вы нажмете на нем какую-либо кнопку: или OK, чтобы завершить процесс, или Cancel, чтобы начать его отладку (лично мне кажется использование кнопки Cancel для запуска отладчика немного извращённым).

Если в диалоговом окне Application Error вы нажмете OK, UnhandledExceptionFilter вернет значение EXCEPTION_EXECUTE_HANDLER. Код, вызвавший UnhandledExceptionFilter, обычно реагирует на это, завершением своей работы (как вы видели в коде BaseProcessStart). Здесь приоткрывается один интересный момент. Большинство людей считает, что система завершает процесс, не обрабатывая исключение. На самом деле, правильнее будет сказать, что система делает так, чтобы необработанное исключение заставило процесс завершить самого себя.

Действительно интересный код в UnhandledExceptionFilter выполняется в том случае, если в диалоге Application Error вы выбираете Cancel, он обеспечивает прикрепление отладчика к процессу, вызвавшему исключение. Код сначала вызывает CreateEvent, чтобы создать объект Событие, через которое отладчик сообщит, что он подключился к сбойному процессу. Дескриптор этого объекта, наряду с идентификатором текущего процесса (process ID), передается в функцию sprintf, которая формирует командную строку, используемую для запуска отладчика. Как только всё готово, UnhandledExceptionFilter вызывает функцию CreateProcess, для запуска отладчика. Если выполнение CreateProcess пройдет нормально, код вызывает функцию NtWaitForSingleObject, и передает ей дескриптор объекта Событие, который был создан ранее. Этот вызов производит блокировку до тех пор, пока отладчик не просигналит через объект Событие, что его прикрепление к сбойному процессу прошло успешно. На данный момент в UnhandledExceptionFilter еще остались небольшие темные пятна, но самое важное я здесь осветил.

Инферно

Раз вы зашли так далеко, было бы несправедливо остановиться, не разобравшись во всем этом до конца. Я показал, как операционная система вызывает определяемую пользователем функцию, когда происходит исключение. Я показал, что обычно происходит внутри этих callback-функций, и как компиляторы используют их, для реализации _try и _catch. Я даже показал, что происходит, когда никто не обрабатывает исключение, и система должна делать очистку. Все, что осталось - это показать, где начинается работа обработчиков. Да, давайте погрузимся во внутренности системы, и посмотрим начальные этапы выполнения SEH.

На рис. 14 показан примерный, сделанный мной на скорую руку, псевдокод функции KiUserExceptionDispatcher и некоторых других, связанных с ней, функций. KiUserExceptionDispatcher находится в NTDLL.DLL, и именно она начинает выполняться первой при возникновении исключения. Если быть на 100 процентов точным, это не совсем так. Например, в Intel-архитектуре возникновение исключения вызывает передачу управления по вектору в обработчик находящийся в нулевом кольце защиты, т.е. в обработчик режима ядра (kernel mode). Обработчик определен соответствующим исключению элементом таблицы дескрипторов прерываний (она же - Interrupt Descriptor Table, или просто - IDT). Я собираюсь пропустить все, что касается кода уровня ядра, и притвориться, что, при возникновении исключения, CPU передает управление прямо в KiUserExceptionDispatcher.

KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
 {
     DWORD retValue;
 
     // Обратите внимание: Если исключение обработано, RtlDispatchException()
     // никогда не возвращает управления.
     if ( RtlDispatchException( pExceptRec, pContext ) )
         retValue = NtContinue( pContext, 0 );
     else
         retValue = NtRaiseException( pExceptRec, pContext, 0 );
 
     EXCEPTION_RECORD excptRec2;
 
     excptRec2.ExceptionCode = retValue;
     excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
     excptRec2.ExceptionRecord = pExcptRec;
     excptRec2.NumberParameters = 0;
 
     RtlRaiseException( &excptRec2 );
 }
 
 int RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
 {
     DWORD   stackUserBase;
     DWORD   stackUserTop;
     PEXCEPTION_REGISTRATION pRegistrationFrame;
     DWORD hLog;
 
     // Получаем границы стека из FS:[4] и FS:[8]
     RtlpGetStackLimits( &stackUserBase, &stackUserTop );
 
     pRegistrationFrame = RtlpGetRegistrationHead();
 
     while ( -1 != pRegistrationFrame )
     {
         PVOID justPastRegistrationFrame = &pRegistrationFrame + 8;
         if ( stackUserBase > justPastRegistrationFrame )
         {
             pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
             return DISPOSITION_DISMISS; // 0
         }
 
         if ( stackUsertop < justPastRegistrationFrame )
         {
             pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
             return DISPOSITION_DISMISS; // 0
         }
 
         if ( pRegistrationFrame & 3 )   // Удостоверяемся что стек выровнен
                                         // на границе DWORD.
         {
             pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
             return DISPOSITION_DISMISS; // 0
         }
 
         if ( someProcessFlag )
         {
             // Doesn't seem to do a whole heck of a lot.
             hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0,
                                             pRegistrationFrame, 0x10 );
         }
 
         DWORD retValue, dispatcherContext;
 
         retValue= RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame,
                                                  pContext, &dispatcherContext,
                                                  pRegistrationFrame->handler );
 
         // Doesn't seem to do a whole heck of a lot.
         if ( someProcessFlag )
             RtlpLogLastExceptionDisposition( hLog, retValue );
 
         if ( 0 == pRegistrationFrame )
         {
             pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL;   // Сбрасываем флажок.
         }
 
         EXCEPTION_RECORD excptRec2;
 
         DWORD yetAnotherValue = 0;
 
         if ( DISPOSITION_DISMISS == retValue )
         {
             if ( pExcptRec->ExceptionFlags & EH_NONCONTINUABLE )
             {
                 excptRec2.ExceptionRecord = pExcptRec;
                 excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION;
                 excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
                 excptRec2.NumberParameters = 0
                 RtlRaiseException( &excptRec2 );
             }
             else
                 return DISPOSITION_CONTINUE_SEARCH;
         }
         else if ( DISPOSITION_CONTINUE_SEARCH == retValue )
         {
         }
         else if ( DISPOSITION_NESTED_EXCEPTION == retValue )
         {
             pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND;
             if ( dispatcherContext > yetAnotherValue )
                 yetAnotherValue = dispatcherContext;
         }
         else    // DISPOSITION_COLLIDED_UNWIND
         {
             excptRec2.ExceptionRecord = pExcptRec;
             excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION;
             excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
             excptRec2.NumberParameters = 0
             RtlRaiseException( &excptRec2 );
         }
 
         pRegistrationFrame = pRegistrationFrame->prev; 
         // Перейти к предыдущему фрейму.
     }
 
     return DISPOSITION_DISMISS;
 }
 
 
 _RtlpExecuteHandlerForException:    // Обрабатываем исключение (в основном режиме).
     MOV     EDX,XXXXXXXX
     JMP     ExecuteHandler
 
 
 RtlpExecutehandlerForUnwind:        // Обрабатываем раскрутку (в режиме раскрутки).
     MOV     EDX,XXXXXXXX
 
 
 
 int ExecuteHandler( PEXCEPTION_RECORD pExcptRec
                     PEXCEPTION_REGISTRATION pExcptReg
                     CONTEXT * pContext
                     PVOID pDispatcherContext,
                     FARPROC handler ) 
                     // Действительный указатель на _except_handler()
 
     // Устанавливаем EXCEPTION_REGISTRATION, здесь в регистре EDX 
     // содержится адрес кода соответствующего обработчика,
     // который показан ниже.
     PUSH    EDX
     PUSH    FS:[0]
     MOV     FS:[0],ESP
 
     // Вызываем callback-функцию обработчика исключений.
     EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext );
 
     // Удаляем голый фрейм EXCEPTION_REGISTRATION 
     MOV     ESP,DWORD PTR FS:[00000000]
     POP     DWORD PTR FS:[00000000]
 
     return EAX;
 }
 
 Обработчик исключений для _RtlpExecuteHandlerForException:
 {
     // Если установлен флажок раскрутки, возвращаем значение
     // DISPOSITION_CONTINUE_SEARCH, в противном случае
     // назначаем pDispatcherContext, и возвращаем значение
     // DISPOSITION_NESTED_EXCEPTION.
 
     return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT
                 ? DISPOSITION_CONTINUE_SEARCH 
                 : *pDispatcherContext = pRegistrationFrame->scopetable,
                   DISPOSITION_NESTED_EXCEPTION;
 }
 
 Обработчик исключений для _RtlpExecuteHandlerForUnwind:
 {
     // Если установлен флажок раскрутки, возвращаем значение 
     // DISPOSITION_CONTINUE_SEARCH, в противном случае
     // назначаем pDispatcherContext, и возвращаем значение
     // DISPOSITION_COLLIDED_UNWIND.
 
     return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT
                 ? DISPOSITION_CONTINUE_SEARCH 
                 : *pDispatcherContext = pRegistrationFrame->scopetable,
                   DISPOSITION_COLLIDED_UNWIND;
 }
Рисунок 14. Псевдокод KiUserExceptionDispatcher.

Основа KiUserExceptionDispatcher - вызов функции RtlDispatchException. Эта функция начинает поиск любого зарегистрированного обработчика исключений. Если обработчик решил обработать исключение, функция RtlDispatchException не вернет управления. Если происходит возврат из RtlDispatchException, то есть два возможных пути: или вызывается NtContinue, который позволяет процессу продолжить выполнение, или вызвать другое исключение. На сей раз, исключение будет не устранимым (isn't continuable), и, следовательно, процесс должен будет завершиться.

Перейдем к RtlDispatchExceptionCode, именно здесь вы найдете код перебора SEH-фреймов, который я часто упоминал в этой статье. Функция получает указатель на связанный список EXCEPTION_REGISTRATIONs, и перебирает каждый его узел в поисках обработчика. Из-за возможности искажения стека, функция очень параноидальна. Перед вызовом обработчика указанного в каждой структуре EXCEPTION_REGISTRATION, код убеждается в том, что она (т.е. структура EXCEPTION_REGISTRATION) выровнена на границу DWORD, находится внутри стека потока, и располагается выше, чем предыдущая EXCEPTION_REGISTRATION.

RtlDispatchException не производит непосредственный вызов адреса указанного в структуре EXCEPTION_REGISTRATION. Вместо этого, для выполнения черновой работы, она вызывает RtlpExecuteHandlerForException. В зависимости от того, что происходит внутри RtlpExecuteHandlerForException, RtlDispatchException, или продолжает перебор SEH-фреймов, или инициирует другое исключение. Вторичное исключение указывает, что в обработчике исключений что-то пошло не так, как надо, и что выполнение процесса не может быть продолжено.

Код RtlpExecuteHandlerForException тесно связан с другой функцией, RtlpExecuteHandlerForUnwind. Вспомните, я уже упоминал эту функцию ранее, когда я описывал раскрутку. Обе этих "функции" просто загружают в регистр EDX разные значения перед передачей управления функции ExecuteHandler. Другими словами, RtlpExecuteHandlerForException и RtlpExecutehandlerForUnwind - это различные точки входа в функцию ExecuteHandler.

В ExecuteHandler происходит извлечение и вызов адреса взятого из поля handler структуры EXCEPTION_REGISTRATION. Может показаться странным, но вызов callback-функции исключения тоже обслуживается обработчиком SEH. Использование SEH внутри SEH выглядит немного забавным, но если немного подумать, причина становится ясной. Если обработчик вызывает другое исключение, ОС должна знать об этом. В зависимости от того, в каком режиме работал обработчик, во время возникновения исключения: в основном режиме, или в режиме раскрутки, ExecuteHandler возвращает, или значение DISPOSITION_NESTED_ EXCEPTION, или значение DISPOSITION_COLLIDED_UNWIND. Основное значение обоих кодов: "Красная тревога! Закрыть все сейчас же!" ("Red Alert! Shut everything down now!").

Если вы похожи на меня, вам трудно запомнить все функции, связанные с SEH. Также трудно запомнить, какая из них какую вызывает. Чтобы помочь себе, я составил диаграмму, показанную на рис. 15.

 KiUserExceptionDispatcher()
 
     RtlDispatchException()
 
         RtlpExecuteHandlerForException()
 
             ExecuteHandler() // Обычно идет в __except_handler3
 
 ---------
 
 __except_handler3()
 
     scopetable filter-expression()
 
     __global_unwind2()
 
         RtlUnwind()
 
             RtlpExecuteHandlerForUnwind()
 
     scopetable __except block()
Рисунок 15. Взаимосвязь функций поддерживающих SEH.

Теперь, зачем делается установка значения регистра EDX перед переходом в код ExecuteHandler? На самом деле, здесь все просто. В том случае, если в процессе работы обработчика установленного пользователем что-то пойдет не так, как надо, ExecuteHandler использует значение из регистра EDX как адрес обработчика исключений. Он помещает значение регистра EDX в стек в качестве поля Handler, принадлежащего минимальной структуре EXCEPTION_REGISTRATION. В сущности, ExecuteHandler использует базовый SEH так, как я показал в программах MYSEH и MYSEH2.

Заключение

SEH - замечательная особенность Win32. Благодаря высокоуровневой поддержке, которую, подобно Visual C++, обеспечивают компиляторы, средний программист может извлечь выгоду из его использования с относительно небольшими затратами на его изучение. Однако, на уровне операционной системы, это более сложная вещь, чем можно было бы подумать, доверившись документации по Win32.

К сожалению, до настоящего времени о SEH было написано очень мало, потому что почти все считают это чрезвычайно трудной темой. Отсутствие документации по подробностям системного уровня никому не помогает. В этой статье, я показал, что SEH уровня системы вращается вокруг относительно простой концепции функции обратного вызова (callback function). Если вы поймете природу обратного вызова, и будете в процессе изучения опираться на это понятие, то механизм SEH не будет для вас слишком сложен.

Из Microsoft Systems Journal. Январь 1997..

()

© Matt Pietrek / пер. Oleg_SK, SI

0 525
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532