Обработка user-mode исключений в Windows

Дата публикации 12 июн 2010

Обработка user-mode исключений в Windows — Архив WASM.RU

ОБРАБОТКА USER-MODE ИСКЛЮЧЕНИЙ В ОС WINDOWS
-------------------------------------------

1. Исключения – общие сведения.
 1.1. От исключения к его обработке.
 1.2. Диспетчер исключений - KiUserExceptionDispatcher.
 1.3. Несколько слов о списке обработчиков исключений.
 1.4. RtlDispatchException.
 1.5. Nested - вложенные исключения.
2. Обработка исключений.
 2.1. Низкоуровневая обработка исключений.
 2.2. Структурная обработка исключений с MSVC++.
  2.2.1. Языковые расширения – блоки __try, __finally, __except
  2.2.2. Как это работает?
  2.2.3. Обработчик исключений __except_handler3.
  2.2.4. Unwinding - раскрутка.
3. Заключение.
4. Список литературы.

1. Исключения – общие сведения.

Исключение представляет собой некое событие, исключительную ситуацию, возникшую в ходе выполнения программы. Это может быть, например деление на ноль или выполнение привилегированной инструкции без соответствующих прав. В общем случае, обработка исключений представляет собой процесс реакции программы на возникшее событие с его целью обработки. Необходимо отметить, что исключения возникают не только в ходе выполнения недопустимых инструкций, неверных действий типа деления на ноль или обращения к несуществующему адресу памяти, а также и в случае их программной генерации, например при помощи RaiseException. Вообще, в первую очередь, исключения принято делить на программные и аппаратные [1,2].

1.1. От исключения к его обработке.

Вообще, обработка исключений начинается в ядре операционной системы. Так, каждому типу исключений поставлен в соответствие свой обработчик, получающий управление в случае возникновения определённого события. Осуществив необходимые действия, ядро Windows передаёт управление потоку, в котором произошло исключение для его дальнейшей обработки. Однако, поток продолжает своё выполнение не с места возникновения исключения, а со строго определённого места - пользовательского диспетчера исключений KiUserExceptionDispatcher, располагающегося в библиотеке NTDLL.DLL. В качестве информации, необходимой для корректной обработки исключения, потоку передаётся вся необходимая информация о месте исключения и его характеристиках -- для этого, в стек потока заносятся структуры EXCEPTION_RECORD и CONTEXT, а указатели на эти структуры передаются в качестве аргументов непосредственно KiUserExceptionDispatcher [3].

Структура EXCEPTION_RECORD представляет собой блок информации, описывающий исключение, возникшее в ходе выполнения потока. Её формат документирован и представлен ниже.

    typedef struct _EXCEPTION_RECORD {
        DWORD                    ExceptionCode;
        DWORD                    ExceptionFlags;
        struct _EXCEPTION_RECORD *ExceptionRecord;
        PVOID                    ExceptionAddress;
        DWORD                    NumberParameters;
        ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD, *PEXCEPTION_RECORD;

Здесь, ExceptionCode однозначно характеризует исключение. Например, широко известны следующие коды исключений:

    EXCEPTION_ACCESS_VIOLATION            - Попытка чтения/записи области памяти, без соответствующих разрешений.
    EXCEPTION_BREAKPOINT                  - Достигнута точка останова.
    EXCEPTION_ILLEGAL_INSTRUCTION         - Попытка выполнения невалидной инструкции.
    EXCEPTION_IN_PAGE_ERROR               - Попытка обращения к отсутствующей странице, если её невозможно загрузить.
    EXCEPTION_INT_DIVIDE_BY_ZERO          - Попытка целочисленного деления на ноль.
    EXCEPTION_NONCONTINUABLE_EXCEPTION    - Попытка возобновления выполнения потока, после "не продолжаемого" исключения.
    EXCEPTION_PRIV_INSTRUCTION            - Попытка выполнения привилегированной инструкции.
    EXCEPTION_SINGLE_STEP                 - В процессе трассировки была выполнена одна инструкция.
    EXCEPTION_STACK_OVERFLOW              - Потоком исчерпан стек.

Поле ExceptionFlags предназначено для определения того, является ли данное исключение продолжаемым. В случае, если это не так, будет установлен флаг EXCEPTION_NONCONTINUABLE. Попытка возобновить выполнение потока после возникновения не продолжаемого исключения приведёт к возникновению исключения с кодом EXCEPTION_NONCONTINUABLE_EXCEPTION.

ExceptionRecord служит для организации цепочки из структур EXCEPTION_RECORD. Это свойство используется в случае вложенных исключений, когда исключения происходят во время их обработки. Подробнее об этом будет рассказано в п.1.2.3.

Поле ExceptionAddress содержит информацию о месте возникновения исключения.

В поле NumberParameters содержится количество параметров, ассоциированных с данным исключением и хранящихся в массиве ExceptionInformation. Это позволяет передавать обработчикам дополнительную информацию об исключении. Например, в случае с EXCEPTION_ACCESS_VIOLATION, в первом элементе данного массива передаётся флаг операции (чтение/запись), а второй элемент содержит виртуальный адрес, к которому производилось обращение.

Вторая структура, передаваемая пользовательскому диспетчеру исключений, представляет собой структуру контекста потока (CONTEXT) и описывает полное состояние потока на момент возникновения исключения.

Таким образом, KiUserModeDispatcher получает полную информацию, характеризующую возникшее исключение и необходимую для осуществления процесса его обработки.

1.2. Диспетчер исключений - KiUserExceptionDispatcher.

Как было отмечено, KiUserExceptionDispatcher является функцией библиотеки NTDLL.DLL, отображаемой на адресное пространство каждого процесса, и служит точкой входа для обработки исключений в режиме пользователя. Это не совсем обычная функция, так как управление ей передаётся непосредственно из ядра. Поэтому, её начало выглядит следующим не вполне обычным образом:

    KiUserExceptionDispatcher:
        .text:77F299E8      cld
        .text:77F299E9      mov     ecx, [esp+4]
        .text:77F299ED      mov     ebx, [esp+0]
        .text:77F299F0      push    ecx
        .text:77F299F1      push    ebx
        .text:77F299F2      call    RtlDispatchException
        .text:77F299F7      or      al, al
        .text:77F299F9      ...

Здесь, для начала выполняется команда cld, затем из стека извлекаются параметры -- указатели на структуры EXCPETION_RECORD и CONTEXT, которые затем заталкиваются в стек, для передачи управления не экспортируемой функции RtlDispatchException. Общий же код (псевдокод, здесь и далее) функции KiUserExceptionDispatcher такой:

    VOID KiUserExceptionDispatcher(PEXCEPTION_RECORD pException, PCONTEXT pContext)
    {
        DWORD dwResult;

        if (RtlDispatchException(pException, pContext) == FALSE) {
            dwResult = NtRaiseException(pException, pContext, 0);
        } else {
            dwResult = NtContinue(ContextRecord, 0);
        }
        
        EXCEPTION_RECORD Exception2;
        
        Exception2.ExceptionCode = dwResult;
        Exception2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
        Exception2.ExceptionRecord = pException;
        Exception2.NumberParameters = 0;

        RtlRaiseException(&Exception2);

        /* Сюда мы не попадаем */
    }

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

Таким образом, в случае успеха обработки возникшего исключения, поток продолжит своё выполнение. Иначе, будет генерироваться новое исключение, характеризующее проблему, возникшую в ходе выполнения функции RtlDispatchException. Интересная информация, касающаяся о работе диспетчера исключений содержится в [4].

1.3. Несколько слов о списке обработчиков исключений.

Перед тем, как продолжить описание работы пользовательского диспетчера исключений, необходимо сказать несколько слов о том, каким же образом в ОС Windows организована поддержка обработки исключений. Так, для каждого выполняющегося в системе потока, определён некий уникальный блок информации Thread Information Block (TIB), расположенный в начале сегмента, адресуемого регистром FS. Помимо прочего, данная структура хранит указатель на вершину списка зарегистрированных обработчиков исключений, каждый из которых описывается структурой EXCEPTION_REGISTRATION:

    typedef struct _EXCEPTION_REGISTRATION {
        struct _EXCEPTION_REGISTRATION *Next;                   // Пусть будет Next, мне так удобнее, ok?
        PVOID Handler;
    } EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;

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

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

    ; Создание в стеке структуры EXCEPTION_REGISTRATION.
    push    OFFSET MyExceptionHandler
    push    fs:[0]
    ; Модификация указателя списка обработчиков
    mov     DWORD PTR fs:[0], esp

Удаление верхнего обработчика также не представляет проблем:

    ; Изьятие обработчика
    pop     fs:[0]
    add     esp, 4

Важным моментом здесь является тот факт, что для каждого потока по адресу FS:[0] доступен только верхний элемент списка обработчиков. Хотя, на практике, это не является сколь-либо серьёзным затруднением. Последним элементом списка является элемент с полем Next равным -1. При создании потока операционная система добавляет обработчик исключения по умолчанию, указывающий на функцию kernel32!UnhandledExceptionFilter.

В данном случае, обработчик исключения MyExceptionHandler -- это специальная функция, с декларированным прототипом и перечнем возвращаемых значений:

    typedef enum _EXCEPTION_DISPOSITION {
        ExceptionContinueExecution  = 0,    // Продолжить выполнение потока с места возникновения исключения.
        ExceptionContinueSearch = 1,        // Продолжить поиск обработчика исключения.
        ExceptionNestedException = 2,       // Вложенное исключение.
        ExceptionCollidedUnwind = 3         // Перекрывающаяся раскрутка.
    } EXCEPTION_DISPOSITION;

    __cdecl \
    EXCEPTION_DISPOSITION \
    MyExceptionHandler(PEXCEPTION_RECORD pRecord, \
                       PVOID pEstablisherFrame, \
                       PCONTEXT pContext, \
                       PVOID pDispatcherContext);

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

1.2. Диспетчер исключений - KiUserExceptionDispatcher.

Эта не экспортируемая функция выполняет всю основную работу по обработке исключений. Ниже я приведу её код, полученный для Windows 7. Итак,

    BOOLEAN \
    RtlDispatchException(PEXCEPTION_RECORD pException, PCONTEXT pContext)
    {
        BOOLEAN bfResult, bfValidateChain;
        DWORD dwLowLimit, dwHighLimit, dwExecuteFlags;
        EXCEPTION_REGISTRATION pRegFrame, pIntRegFrame, pDispContextFrame;
        
        bfResult = 0;
        
        if (RtlCallVectoredExceptionHandlers(pException, pContext) == TRUE) {
            bfResult = TRUE;
        } else {
            // Определяем границы стека потока
            RtlpGetStackLimits(&dwLowLimit, &dwHighLimit);
                
            pRegFrame = RtlpGetRegistrationHead();
                
            dwExecuteFlags = 0;
            bfValidateChain = TRUE;

            // Если НЕ доступна информация о DEP или выключена опция DisableExceptionChainValidation,
            // осуществить проверку валидности...
            if ((NtQueryInformationProcess(NtCurrentProcess(), ProcessExecuteFlags, \
                &dwExecuteFlags, sizeof(dwExecuteFlags), NULL) < 0) || \
                ((dwExecuteFlags & DisableExceptionChainValidation) == 0))
                {
                    DWORD dwHandler;

                    // Проверяем валидность всех зарегистрированных обработчиков
                    while ((DWORD)pRegFrame != -1) {
                        // Запись pRegFrame должна лежать в стеке с выравниванием на 4 байта
                        if ((pRegFrame < dwLowLimit) || ((pRegFrame + 8) > dwHighLimit) || (pRegFrame & 3)) {
                            goto exception_stack_invalid;
                        }
                                
                        dwHandler = pRegFrame->Handler;

                        // Обработчик НЕ должен располагаться в стеке
                        if ((dwHandler >= dwLowLimit) && (dwHandler < dwHighLimit)) {
                            goto exception_stack_invalid;
                        }

                        pRegFrame = pRegFrame->Next;
                    }

                    // RtlExceptionAttached = 0x0200
                    if (NtCurrentTeb()->SameTebFlags & RtlExceptionAttached) {
                        // Проверка последнего обработчика в цепочке
                        if (dwHandler != FinalExceptionHandler) {
                            goto exception_stack_invalid;
                        }
                    }
                } else {
                    bfValidateChain = FALSE;
                }

            pRegFrame = RtlpGetRegistrationHead();
            pIntRegFrame = 0;
                
            // Проходимся по всему списку
            while ((DWORD)pRegFrame != -1) {
                // Если необходимо, проверять правильность расположения pRegFrame и pRegFrame->Handler
                if (bfValidateChain == FALSE) {
                    if ((pRegFrame < dwLowLimit) || ((pRegFrame + 8) > dwHighLimit) || (pRegFrame & 3)) {
                        goto exception_stack_invalid;
                    }
                                
                    if ((pRegFrame->Handler >= dwLowLimit) || (pRegFrame->Handler < dwHighLimit)) {
                        goto exception_stack_invalid;
                    }
                }
                        
                // Проверить валидность pRegFrame->Handler
                if (RtlIsValidHandler(pRegFrame->Handler, dwExecuteFlags) == FALSE) {
                    goto exception_stack_invalid;
                }

                DWORD dwResult = RtlpExecuteHandlerForException(pException, pRegFrame, \
                    pContext, &pDispContextFrame, pRegFrame->Handler);
                        
                // Проверяем, вернулись ли мы к обработке фрейма, вызвавшего вложенное исключение?
                if (pRegFrame == pIntRegFrame) {
                    pException->ExceptionFlags &= ~EXCEPTION_NESTED_CALL;
                    pIntRegFrame = 0;
                }

                EXCEPTION_RECORD Exception2;
                        
                switch (dwResult) {
                    // Продожить выполнение потока
                    case ExceptionContinueExecution:
                        if (pException->ExceptionFlags & EXCEPTION_NONCONTINUABLE) {
                            Exception2.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION;
                            Exception2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                            Exception2.ExceptionRecord = pException;
                            Exception2.NumberParameters = 0;
                            RtlRaiseException(&Exception2);
                        } else {
                            bfResult = TRUE;
                            goto return_continue_veh;
                        }
                        break;
                    // Продолжить поиск обработчика
                    case ExceptionContinueSearch:
                        if (pException->ExceptionFlags & EXCEPTION_STACK_INVALID) {
                            goto return_continue_veh;
                        }
                        break;
                    // Вложенное исключение
                    case ExceptionNestedException:
                        // В процессе обработки исключения произошло вложенное исключение,
                        // поэтому необходимо сохранить указатель на прерванный фрейм для
                        // его последующей обработки (pIntRegFrame)
                        pException->ExceptionFlags |= EXCEPTION_NESTED_CALL;
                        if (pDispContextFrame > pIntRegFrame) {
                            pIntRegFrame = pDispContextFrame;
                        }
                        break;
                    // Любое другое значение
                    default:
                        Exception2.ExceptionCode = STATUS_INVALID_DISPOSITION;
                        Exception2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                        Exception2.ExceptionRecord = pException;
                        Exception2.NumberParameters = 0;
                        RtlRaiseException(&Exception2);
                        break;
                    }
                }
        }
    return_continue_veh:
        RtlCallVectoredContinueHandlers(pException, pContext);

        return bfResult;

    exception_stack_invalid:
        pException->ExceptionFlags |= EXCEPTION_STACK_INVALID;
        goto return_continue_veh;      
    }    

Основной задачей, которую выполняет эта функция, является задача последовательного обхода списка зарегистрированных обработчиков исключений с целью вызова этих обработчиков для обработки данного исключения. Напомню, что выполнение потока будет продолжено в том единственном случае, если результатом работы RtlDispatchException будет TRUE. Как видно, такое возможно только в случае получения ExceptionContinueExecution. Но обо всём по порядку...

Первое и последнне, что всегда делает функция -- это вызов обработчиков VEH (Vectored Exception Handler): RtlCallVectoredExceptionHandlers и RtlCallVectoredContinueHandlers (VEH появился в Windows XP, на ранних версиях ОС Windows его не было) [5]. Причём, результат обработки VEH влияет на дальнейшую обработку. Так, в случае, если RtlCallVectoredExceptionHandlers возвращает истинное значение, RtlDispatchException завершается с успехом, предварительно вызвав RtlCallVectoredContinueHandlers.

Если в ходе выполнения RtlCallVectoredExceptionHandlers ни один из векторных обработчиков не смог обработать исключение, управление переходит к основной части функциии RtlDispatchException, где в первую очередь осуществляется проверка валидности зарегистрированных обработчиков исключений. Причём, обработчик считается валидным, если структура EXCEPTION_REGISTRATION, которой он принадлежит находится в стеке, выравнена на границу 4 байта, а сам обработчик располагается за пределами стека. Если проверка валидности проводилась до основного цикла обхода обработчиков, то флаг bfValidateChain устанавливается равным TRUE и повторная проверка не осуществляется.

Далее следует основной цикл поиска необходимого обработчика. В самом его начале также присутствует код проверки фрейма на валидность, однако он будет исполнен только в том случае, если предыдущая проверка не проводилась. Однако, помимо этого, здесь дополнительно осуществляется вызов функции RtlIsValidHandler, которая также дополнительно проверяет его корректность непосредственно перед вызовом.

Вызов обработчика происходит при помощи функции RtlpExecuteHandlerForException, являющейся обёрткой к функции ExceptionHandler. Ниже приведён код этих функций:

    DWORD \
    RtlpExecuteHandlerForException(PEXCEPTION_RECORD pException, \
                                   PEXCEPTION_REGISTRATION pEstablisherFrame, \
                                   PCONTEXT pContext, \
                                   PEXCEPTION_REGISTRATION pDispatcherContext, \
                                   PEXCEPTION_ROUTINE pExceptionRoutine)
    {
        __asm {
            mov edx, OFFSET ExceptionHandler
            jmp ExecuteHandler
        }
    }

    DWORD \
    ExecuteHandler(PEXCEPTION_RECORD pException, \
                   PEXCEPTION_REGISTRATION pEstablisherFrame, \
                   PCONTEXT pContext, \
                   PEXCEPTION_REGISTRATION pDispatcherContext, \
                   PEXCEPTION_ROUTINE pExceptionRoutine)
    {
        DWORD dwResult;

        __asm {
            push pEstablisherFrame          // Тут сохраняем указатель на текущий фрейм
            push edx                        // См. RtlpExecuteHandlerForException
            push fs:[0]
            mov fs:[0], esp
        }

        dwResult = pExceptionRoutune(pException, pEstablisherFrame, pContext, pDispatcherContext);

        __asm {
            mov esp, fs:[0]
            pop fs:[0]
        }
        
        return dwResult;
    }
    
    DWORD \
    ExceptionHandler(PEXCEPTION_RECORD pException, \
                     PEXCEPTION_REGISTRATION pEstablisherFrame, \
                     PCONTEXT pContext, \
                     PEXCEPTION_REGISTRATION * pDispatcherContext)
    {
        if (pException->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) {
            return ExceptionContinueSearch;
        }
        
        // Тут лежит указатель на прерванный фрейм (см. ExecuteHandler)
        pDispatcherContext[0] = (LPVOID)&pEstablisherFrame[1];

        return ExceptionNestedException;
    }

Как видно, RtlpExecuteHandlerForException просто осуществляет переход к началу ExecuteHandler, установив в регистре EDX адрес функции ExceptionHandler. Как будет показано далее, функция ExecuteHandler является общей как для процесса поиска обработчика исключений, так и для процесса глобальной раскрутки фреймов (global unwinding). Как бы то ни было, её основной задачей является вызов указанного в параметре pExceptionRoutine обработчика исключения и возвращение результата его работы. Однако, существует вероятность того, что в процессе обработки исключения, в самом обработчике может возникнуть искючение, обработка которого также должна быть осуществлена. Для этого, на время вызова pExceptionRoutine происходит установка дополнительно обработчика исключений. Это позволяет получить управление в случае возникновения вложенных (nested) исключений, о которых будет рассказано далее.

Результат выполнения обработчика исключений возвращается в функцию RtlDispatchException, где происходит проверка полученного значения. Так, поток будет продолжен в случае получения кода ExceptionContinueExecution, если вызванным обработчиком не был установлен флаг EXCEPTION_NONCONTINUABLE, иначе будет сгенерировано исключение EXCEPTION_NONCONTINUABLE_EXCEPTION и процесс будет завершён системой. Процесс также будет завершён, если обнаружена какая либо некорректность (код EXCEPTION_STACK_INVALID) или возвращённое значение больше 2 (ExceptionNestedException).

Подводя итог анализу работы функции RtlDispatchException, можно резюмировать следующее: данная функция, являясь диспетчером исключений в режиме пользователя, производит последовательный обход списка зарегистрированных для вызвавшего исключение потока обработчиков исключений, проверяя их валидность и осуществляя их вызов с целью анализа полученного результата. Единственным случаем, когда будет возобновлено исполнение потока, будет являться возвращение обработчиком значения ExceptionContinueExecution (при условии отсутствия флага EXCEPTION_NONCONTINUABLE). Иначе, будет продолжен обход списка обработчиков (ExceptionContinueSearch) или же будет генерироваться исключение, с соответствующим кодом.

1.5. Nested - вложенные исключения.

Как было отмечено, в процессе обработки исключений, при вызове обработчика исключения, может также возникнуть исключительная ситуация, требующая обработки. Процесс обработки такого исключения будет точно таким же, как и предыдущего (и вообще, всех остальных): ядром будет сохранено состояние потока и управление вновь получит KiUserExceptionDispatcher. Однако теперь, ввиду небольшого шаманства в функциях RtlpExecuteHandlerForException и ExecuteHandler, будет выполнен обработчик вложенных исключений ExceptionHandler. Результат его работы будет зависеть от того, в каком режиме он вызван -- обработки исключений или раскрутки. Для раскрутки будет возвращено значение ExceptionContinueSearch, иначе ExceptionNestedException. В последнем случае, по указателю pDispatcherContext будет записано значение указателя фрейма, в ходе обработки которого произошло это исключение, заботливо сохранённое на стеке в самом начале функции ExecuteHandler, выше структуры EXCEPTION_REGISTRATION. Иллюстрация, приведённая ниже, поясняет механизм обработки вложенных исключений.

Рисунок 1. Обработка вложенных (nested) исключений.

Итак, если возникает вложенное исключение, оно обрабатывается специальным обработчиком -- ExceptionHandler, который определяет фрейм, в котором произошло это исключение и возвращает его через указатель pDispatcherContext. Определив, что обрабатываемое исключение является вложенным, RtlDispatchException устанавливает pIntRegFrame равным полученному указателю на прерванный фрейм, а также поднимает в описателе исключения флаг EXCEPTION_NESTED_CALL. После этого, продолжается нормальный обход обработчиков, а когда очередной фрейм будет равен фрейму, указанному в pIntRegFrame, то есть тому самому, прерванному исключением, флаг EXCEPTION_NESTED_CALL будет снят.

Такая организация обработки процесса исключений позволяет обрабатывать исключения, возникающие в обработчиках исключений. Причём, количество вложенных исключений ограничено в принципе только размером стека. Каждое из вложенных исключений также должно быть обработано. В результате обработки, как и ранее, происходит либо возобновление выполнения потока с места возникновения исключения, либо генерация нового исключения. Так, если обработка исключения в обработчике исключения завершается с результатом ExceptionContinueExecution, управление получит код обработчика, вызвавшего это исключение. То есть, будет сделан "шаг назад".

В приведённом выше примере, в потоке Thread возникает исключение Exception1 . В процессе его обработки, в обработчике h1() возникает вложенное исключение Exception2. Снова происходит попытка обработать исключение, но в процессе обработки в обработчике h2() также возникает исключение Exception3, которое в свою очередь должно быть обработано. В этой схеме, возврат к потоку Thread возможен лишь в том случае, если будет восстановлен каждый из сохранённых контекстов: контекст #3 восстановит обработчик h2(), контекст #2 -- обработчик h1(), контекст #1 -- основной поток.

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

В этом разделе описываются вопросы обработки исключений. Будет рассмотрена низкоуровневая обработка исключений и структурная обработка исключений (SEH) с использованием MSVC++, а также описаны механизмы, используемые в ходе этого процесса.

2.1. Низкоуровневая обработка исключений.

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

    .386
    .model flat

    include windows.inc
    
    include kernel32.inc
    includelib kernel32.lib

    .data
        szMessageOk db "INT 3 has been passed!", 0
        szTitleOk db ":smile3:", 0

    .code

    MyHandler PROC C uses ebx esi edi,
                    pException:PEXCEPTION_RECORD, \
                    pRegFrame:PEXCEPTION_REGISTRATION, \
                    pContext:PCONTEXT, \
                    pDispContext:PEXCEPTION_REGISTRATION
    ; function body
        mov     ebx, pException
        cmp     [ebx][EXCEPTION_RECORD.ExceptionFlags], EXCEPTION_BREAKPOINT
        je      @@ContinueExecute

    @@ContinueSearch:
        mov     eax, ExceptionContinueSearch
        ret
    
    @@ContinueExecute:
        mov     esi, pContext
        ; Подправим адрес следующей инструкции (EIP)
        add     [esi][CONTEXT.regEip], 1
        mov     eax, ExceptionContinueExecution
        ret
    MyHandler ENDP

    MainProc PROC
        ; Установим обработчик
        push    OFFSET MyHandler
        push    fs:[0]
        mov     fs:[0], esp
        
        ; Сгенерируем исключение (размер инструкции -- 1 байт, опкод -- 0xCC)
        int     3
        
        ; Выведем сообщение, о том, что инструкция пройдена
        invoke MessageBoxA, NULL, szMessageOk, szTitleOk, MB_OK

        ; Снимем обработчик
        pop     fs:[0]
        add     esp, 4
        
        ; Завершим процесс
        invoke  ExitProcess, 0
    MainProc ENDP

        END MainProc

2.2. Структурная обработка исключений с MSVC++.

Структурная обработка исключений, SEH (Structed Exception Handling) -- механизм обработки программных и аппаратных исключений в ОС Windows, позволяющий программистам контролировать обработку исключений, а также являющийся отладочным средством (c) wikipedia. На самом деле, как будет показано далее, SEH является скорее надстройкой над системной обработкой исключений. SEH достаточно хорошо рассмотрен в [6].

2.2.1. Языковые расширения – блоки __try, __finally, __except.

Для облегчения жизни программистам, в компиляторе MSVC++ введены дополнительные возможности, не предусмотренные стандартами C и C++ -- блоки __try, __finally и __except. Назначение данных блоков сводится к одному -- обеспечить возможность удобной и простой обработки исключений, максимально упростив при этом разработку программ. Как видно из примера низкоуровневой обработки исключений, программисту необходимо заботиться не только о написании обработчика исключений, но также и о его установке и т.д.

В основе структурной обработки исключений лежит принцип организации неких "защищённых" участков кода, в пределах которых отлавливаются и обрабатываются все исключения. Такие блоки описываются при помощи связок __try // __except или __try // __finally. Отдельное использование каждого из этих блоков не допускается. Конструкция __try // __except позволяет осуществить обработку исключений, возникших в ходе выполнения кода, находящегося в __try-блоке. Конструкция __try // __finally позволяет обеспечить выполнения кода, заключённого в блок __finally, в случае выхода за пределы блока __try, независимо от причины.

    __try {
        // Защищаемый код
    } __finally {
        // Терминальный обработчик (очистка ресурсов и т.д.)
    }
    
    __try {
        // Защищаемый код
    } __except ( filter-expression ) {
        // Обработчик исключения
    }

Допускается любая вложенность блоков. В конструкции с __except, выражение-фильтр определяет, будет ли выполнен код обработчика. В реализации фильтра есть некоторые ограничения, связанные с тем, что функции GetExceptionInformation и GetExceptionCode можно использовать только в выражении, стоящем в скобках блока __except( ). Причина этого будет раскрыта далее. Подробнее об использовании __try // __finally, __try // __except можно посмотреть в MSDN [xxx].

2.2.2. Как это работает?

А как же всё это работает? Для того, чтобы разобраться в этом вопросе, необходимо рассмотреть низкоуровневую реализацию блоков __try, __finally и __except. На самом деле, всё достаточно просто. SEH, как расширение системного механизма обработки исключений, позволяет пользователю формировать список обработчиков исключений, доступных в пределах верхнего __try блока. При этом, такой список является "локальным" и, как будет показано далее, обслуживается специальным обработчиком исключений. Говоря локальный, я подразумеваю в первую очередь ограниченность действия этого списка (пределы __try-блока), а так же то, что существует и глобальный список. Так вот, глобальным списком исключений является доступный через FS:[0] и уже хорошо знакомый список обработчиков исключений. Использование локального списка обработчиков исключений, обслуживаемого неким специальным обработчиком, очень удобно и позволяет реализовать заложенный в __try, __finally и __except смысл.

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

    Пролог:
        push    SizeOfLocalVariables + 8
        push    OFFSET ScopeTable
        call    __SEH_prolog
        ...
    Эпилог:
        ...
        call    __SEH_epilog
        retn

Как видно, в прологе используется некая функция __SEH_prolog, в эпилоге -- __SEH_epilog. Эти функции являются частью MSVC RunTime библиотеки (например, MSVCR71.DLL) и линкуются либо динамически (как и показано), либо статически. Вообще, вся поддержка SEH реализована в указанной библиотеке. Основной задачей, которую выполняют эти функции, является настройка стекового фрейма, а также установка специального обработчика исключений -- __except_handler3, отвечающего за обработку исключений, возникших во время работы функции. На самом деле, этот обработчик исключений будет пытаться найти подходящий обработчик из локального списка, о котором я упоминал чуть выше. Но об этом позже...

Итак, __SEH_prolog устанавливает обработчик исключений и формирует стековый фрейм функции. Однако, помимо этого, происходит ещё одно важное действие -- в стеке создаётся специальный SEH-фрейм, описывающий состояние SEH для данной функции. Структура SEH-фрейма недокументирована, но значения её полей вполне себе известны:

    typedef _SEH_FRAME {
        DWORD StackPointerValue;
        PEXCEPTION_POINTERS ExceptionPointers;
        EXCEPTION_REGISTRATION RegFrame;
        DWORD ScopeTable;
        DWORD TryLevel;
        DWORD StackFrameBase;
    } SEH_FRAME, *PSEH_FRAME;

Данная структура содержит в себе поле RegFrame, использующееся для регистрации обработчика исключений (в __SEH_prolog). Таким образом, так как поле RegFrame входит в состав структуры SEH_FRAME, по адресу структуры EXCEPTION_REGISTRATION всегда можно определить базовый адрес структуры SEH_FRAME. Это важное обстоятельство используется в обработчике __except_handler3 при обработке исключений. Ниже приведено состояние стека, после возвращения из __SEH_prolog:

            Смещение
            --------
            S = SizeOfLocalVariables
            
    ESP =>  -(40 + S)   Адрес возврата из __SEH_prolog
            -(36 + S)   EDI
            -(32 + S)   ESI
            -(28 + S)   EBX
            -(24 + S)   Локальные переменные функции
                        struct SEH_FRAME {
            -24             DWORD StackPointerValue;
            -20             PEXCEPTION_POINTERS ExceptionPointers;
                            struct EXCEPTION_REGISTRATION RegFrame {
            -16                 struct EXCEPTION_REGISTRATION * Next;
            -12                 LPVOID Handler;
                            };
            -08             DWORD ScopeTable;
            -04             DWORD TryLevel;
    EBP =>   00             DWORD StackFrameBase;
                        };
            +04         Адрес возврата из функции
            +08         Аргументы функции
                        ...

В структуре SEH_FRAME, в поле StackFrameBase содержится значение регистра EBP, в поле StackPointerValue -- значение регистра ESP. Это позволяет получит доступ к внутренним переменным функции из любого контекста, что особенно важно в момент обработки исключений, когда текущим контекстом является контекст диспетчера исключений. Поля ScopeTable, TryLevel и ExceptionPointers относятся непосредственно к обработке исключений и будут рассмотрены далее...

Ниже приведён код функций __SEH_prolog и __SEH_epilog, взятых мной из библиотеки MSVCR71.DLL для Windows 7.

    _SEH_prolog PROC C
        ASSUME  fs:nothing

        push    OFFSET _except_handler3     ; RegFrame.Handler = _except_handler3
        mov     eax, DWORD PTR fs:[0]
        push    eax                         ; RegFrame.Next = FS:[0]
        mov     eax, [esp + 16]             ; SizeOfLocalVariblaes + 8
        mov     [esp + 16], ebp             ; StackFrameBase = EBP
        lea     ebp, [esp + 16]             ; EBP = указатель на StackFrameBase
        sub     esp, eax                    ; Зарезервировать место под локальные переменные
        push    ebx                         ;
        push    esi                         ; Сохраняем регистры EBX, ESI, EDI
        push    edi                         ;
        mov     eax, [ebp - 8]              ; EAX = адрес возврата из __SEH_prolog
        mov     [ebp - 24], esp             ; StackPointerValue = ESP
        push    eax                         ; Это нужно для RET'а, чтобы парвильно вернуться
        mov     eax, [ebp - 4]              ; EAX = OFFSET ScopeTable
        mov     DWORD PTR [ebp - 4], -1     ; TryLevel (-1, так как внутрь __try-блока ещё не входили)
        mov     [ebp - 8], eax              ; ScopeTable = OFFSET ScopeTable
        lea     eax, [ebp - 16]             ; Адрес структуры RegFrame
        mov     fs:[0], eax                 ; Установка обработчика исключений, входящего в SEH_FRAME

        ret
    _SEH_prolog ENDP

    _SEH_epilog PROC C
        ASSUME  fs:nothing

        mov     ecx, [ebp - 16]             ; Снятие обработчика исключений
        mov     fs:[0], ecx                 ; FS:[0] =  RegFrame.Next
        pop     ecx                         ; ECX = адрес возврата из __SEH_epilog
        pop     edi                         ;
        pop     esi                         ; Восстанавливаем регистры EDI, ESI, EBX
        pop     ebx                         ;
        mov     esp, ebp                    ; Восстанавливаем указатель стека (ESP)
        pop     ebp                         ; Восстанавливаем (EBP)
        push    ecx                         ; Заносим в стек адрес возврата для RET

        ret
    _SEH_epilog ENDP    

Теперь, имея представление о том, какие подготовительные действия совершаются в каждой использующей SEH функции, можно двигаться дальше. В процессе компиляции, когда компилятор встречает __try блок, он создаёт для каждого такого блока специальную структуру -- SCOPETABLE_ENTRY, описывающую данный блок. Далее, эти структуры компонуются в специальные таблицы (ScopeTable), описывающие иерархию __try-блоков. Структура элемента такой таблицы приведена ниже:

    typedef _SCOPETABLE_ENTRY {
        DWORD EnclosingLevel;
        DWORD FilterProc;
        DWORD HandlerProc;
    } SCOPETABLE_ENTRY, *PSCOPETABLE_ENTRY;

Здесь, EnclosingLevel определяет уровень вложенности __try-блока, причём значение -1 соответствует самому верхнему блоку. FilterProc -- адрес функции-фильтра для блока __except, HandlerProc -- адрес обработчика исключения или соответствующего терминального обработчика. Ниже приведена иллюстрация построения таблицы ScopeTable в соответствии с заданной структурой блоков.

Рисунок 2. Построение таблицы ScopeTable.

Как видно, в таблице три элемента, что точно соответствует количеству __try-блоков. Структура таблицы такова, что единственным отличительным признаком __except-блока от __finally будет наличие фильтра. Именно таким образом, по значению поля FilterProc, система определяет с каким блоком имеет дело.

А вот что получается при дизассемблировании программы, использующей показанную структуру __try-блоков:

    int main()
    {
        DWORD dwValue;

        __try {
            dwValue = 0xABADF00D; // TryLevel = 0
            __try {
                dwValue = 0xABADC0DE; // TryLevel = 1
                __try {
                    dwValue = 0xDEADBEEF;  // TryLevel = 2
                }
                __except (2) {
                }
            }
            __finally {
                // TryLevel = 0, так как вышли из второго блока!
            }
        }
        __except (1) {
        }
        
        return dwValue;
    }
    .text:00401000 _main           proc near
    .text:00401000 dwValue         = dword ptr -1Ch
    .text:00401000                 push    0Ch                                  ; 12 = 8 + 4
    .text:00401002                 push    offset ScopeTable
    .text:00401007                 call    __SEH_prolog
    .text:0040100C                 and     dword ptr [ebp-4], 0                 ; TryLevel = 0, вошли в первый блок
    .text:00401010                 mov     dword ptr [ebp-1Ch], 0ABADF00Dh
    .text:00401017                 xor     eax, eax
    .text:00401019                 inc     eax
    .text:0040101A                 mov     [ebp-4], eax                         ; TryLevel = 1, вошли во второй блок
    .text:0040101D                 mov     dword ptr [ebp-1Ch], 0ABADC0DEh
    .text:00401024                 mov     dword ptr [ebp-4], 2                 ; TryLevel = 2, вошли в третий блок
    .text:0040102B                 mov     dword ptr [ebp-1Ch], 0DEADBEEFh
    .text:00401032                 mov     [ebp-4], eax                         ; TryLevel = 1, вышли из третьего блока
    .text:00401035                 jmp     short loc_401045
    .text:00401037                                                              ; except-filter-code-2
    .text:00401037 $LN15:                                                       ; DATA XREF: .rdata:004020CC
    .text:00401037                 push    2
    .text:00401039                 pop     eax
    .text:0040103A                 retn
    .text:0040103B                                                              ; except-handler-code-2
    .text:0040103B $LN16:                                                       ; DATA XREF: .rdata:004020D0
    .text:0040103B                 mov     esp, [ebp-18h]                       ; Восстановим ESP
    .text:0040103E                 mov     dword ptr [ebp-4], 1                 ; TryLevel = 1, вышли из третьего блока
    .text:00401045 loc_401045:
    .text:00401045                 and     dword ptr [ebp-4], 0                 ; TryLevel = 0, вышли из второго блока
    .text:00401049                 call    $LN11                                ; Вызываем финальный обработчик
    .text:0040104E                 jmp     short $LN14
    .text:00401050                                                              ; finally-handler-code-1
    .text:00401050 $LN11:                                                       ; DATA XREF: .rdata:004020C4
    .text:00401050                 retn
    .text:00401051                 jmp     short $LN14
    .text:00401053                                                              ; except-filter-code-1
    .text:00401053 $LN7:                                                        ; DATA XREF: .rdata:004020B4
    .text:00401053                 xor     eax, eax
    .text:00401055                 inc     eax
    .text:00401056                 retn
    .text:00401057                                                              ; except-handler-code-1
    .text:00401057 $LN8:                                                        ; DATA XREF: .rdata:stru_4020B0
    .text:00401057                 mov     esp, [ebp-18h]                       ; Восстановим ESP
    .text:0040105A $LN14:
    .text:0040105A                 or      dword ptr [ebp-4], -1                ; TryLevel = -1, вышли из всех блоков
    .text:0040105E                 mov     eax, [ebp-dwValue]
    .text:00401061                 call    __SEH_epilog
    .text:00401066                 retn
    .text:00401066 _main           endp

    .rdata:004020B0 ScopeTable     dd -1                                        ; EnclosingLevel = -1, (try-except, первый блок)
    .rdata:004020B4                dd offset $LN7                               ;  except-filter-code-1
    .rdata:004020B8                dd offset $LN8                               ;  except-handler-code-1
    .rdata:004020BC                dd 0                                         ; EnclosingLevel = 0 (try-finally, второй блок)
    .rdata:004020C0                dd 0                                         ;  NULL
    .rdata:004020C4                dd offset $LN11                              ;  finally-handler-code-1
    .rdata:004020C8                dd 1                                         ; EnclosingLevel = 1 (try-except, третий блок)
    .rdata:004020CC                dd offset $LN15                              ;  except-filter-code-2
    .rdata:004020D0                dd offset $LN16                              ;  except-handler-code-2

Надеюсь, теперь понятно, каким образом компилятор MSVC++ реализует поддержку SEH. В приведённом выше фрагменте дизассемблирования программы, стоит обратить внимание на реализацию фильтров и обработчиков. Так как предполагается, что те и другие могут быть вызваны в контексте, отличном от контекста потока, то первое, что делает обработчик -- восстанавливает значение регистра ESP из SEH-фрейма (SEH_FRAME.StackPointerValue, 0x18), адресуемого регистром EBP. Да, важный момент. Регистр EBP предполагается валидным, то есть содержащим верное значение. За это отвечает обработчик исключения __except_handler3. Его работа будет рассмотрена далее.

Завершая начатое в данной части, стоит подвести итог сказанному. Итак, поддержка SEH на уровне компилятора состоит из двух частей: организации специальных пролога и эпилога в функциях, использующих SEH, с целью создания специального SEH-фрейма, хранящего необходимую информацию, а также создании специальных Scope-таблиц, содержащих информацию о __try-блоках и их взаимной иерархии. В процессе выполнения пролога функции, происходит регистрация специального обработчика исключений (глобального), который отвечает за поиск необходимых (локальных) обработчиков, связанных с SEH-фреймом с помощью указателя на таблицу ScopeTable. Таким образом, в случае вложенного вызова функций, использующих SEH, в системе будет зарегистрировано столько обработчиков, сколько было вызвано функций, и каждый из них будет связан с соответствующим своей функции SEH-фреймом.

2.2.3. Обработчик исключений __except_handler3.

Являясь по сути обычным обработчиком исключений, функция __except_handler3 имеет стандартный для обработчиков исключений прототип. Ниже я приведу код этой функции, полученный мной из MSVCR71.DLL для Windows 7.

    #define REG_TO_SEH(x) (PSEH_FRAME)((DWORD)x - 8)
    
    __cdecl \
    EXCEPTION_DISPOSITION \
    __exception_handler3(PEXCEPTION_RECORD pException, \
                         PEXCEPTION_REGISTRATION  pEstablisherFrame, \
                         PCONTEXT pContext, \
                         PEXCEPTION_REGISTRATION *pDispatcherContext)
    {
        PSEH_FRAME pSehFrame;
        EXCEPTION_POINTERS ExceptionPointers;

        // pEstablisherFrame - это на самом деле указатель на структуру
        // EXCEPTION_REGISTRATION, входящую в состав SEH_FRAME. Поэтому,
        // получить адрес SEH_FRAME легко.
        pSehFrame = REG_TO_SEH(pEstablisherFrame);

        // Проверим, а не возникло ли исключение в процессе раскрутки?
        if (pException->ExceptionCode & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND))
        {
            // Сохраним EBP, адресующий локальный стек
            push_ebp();
            // Сменим стековый фрейм
            move_ebp(pSehFrame->StackFrameBase);
            // Выполним раскрутку всей локальную цепочки
            __local_unwind2(pEstablisherFrame, -1);
            // Восстановим EBP
            pop_ebp();
            
            return ExceptionContinueSearch;
        }
        
        pSehFrame->ExceptionPointers = &ExceptionPointers;
        ExceptionPointers.ContextRecord = pContext;
        ExceptionPointers.ExceptionRecord = pException;

        // Начинаем поиск и текущего TryLevel'а
        TryLevel = pSehFrame->TryLevel;

        while (TryLevel != -1)
        {
            // Определяем тип блока. Если указан фильтр, то это except -- работаем.
            if (pSehFrame->ScopeTable[TryLevel].FilterProc)
            {
                DWORD dwResult;
                
                // Сохраним EBP, адресующий локальный стек
                push_ebp();
                // Сменим стековый фрейм
                move_ebp(pSehFrame->StackFrameBase);
                // Вызываем функцию-фильтр, с настроенным через EBP стековым фреймом
                dwResult = pSehFrame->ScopeTable[TryLevel].FilterProc();
                // Восстановим EBP
                pop_ebp();
                
                // EXCEPTION_CONTINUE_EXECUTION (-1)
                if (dwResult < 0)
                {
                    return ExceptionContinueExecution;
                }
                
                // EXCEPTION_EXECUTE_HANDLER (1)
                if (dwResult > 0)
                {
                   
                    // Выполним раскрутку глобальной цепочки до pEstablisherFrame
                    __global_unwind2(pEstablisherFrame);
                    // Сменим стековый фрейм
                    move_ebp(pSehFrame->StackFrameBase);
                    // Выполним раскрутку локальной цесочки до TryLevel
                    __local_unwind2(pEstablisherFrame, TryLevel);
                    // Понижаем уровень вложенности
                    pSehFrame->TryLevel = pSehFrame->ScopeTable[TryLevel].EnclosingLevel;
                    // Вызываем функцию-обработчик
                    pSehFrame->ScopeTable[TryLevel].HandlerProc();

                    // Сюда мы больше не возвращаемся!
                }
                
                // EXCEPTION_CONTINUE_SEARCH (0)
            }
            
            TryLevel = pSehFrame->ScopeTable[TryLevel].EnclosingLevel;
        }
        
        return ExceptionContinueSearch;
    }

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

Результат выполнения функции-фильтра служит для определения необходимого действия: продолжения выполнения потока (EXCEPTION_CONTINUE_EXECUTION), осуществления вызова соответствующего данному __try-блоку обработчика (EXCEPTION_EXECUTE_HANDLER) или же продолжению поиска подходящего обработчика (EXCEPTION_CONTINUE_SEARCH). И, в случае, если определена необходимость вызова соответствующего данному __try-блоку обработчика, происходит раскрутка, смена стекового фрейма и его непосредственный вызов, причём возвращение управления не происходит. Иначе, после перебора всех локальных обработчиков, функция __exceute_handler3 вернёт управление диспетчеру исключений RtlDispatchException с результатом ExceptionContinueSearch, говорящем о том, что процесс поиска обработчика исключения должен быть продолжен.

Возвращаясь к дизассемблерному листингу, ещё раз отмечу тот факт, что в случае с фильтрами не происходит восстановления регистра ESP, так предполагается, что функция-фильтр всегда возвращается. Помимо этого, хочу отметить хитрую особенность функций-макросов GetExceptionCode и GetExceptionInformation. Будучи макросами, они разворачиваются до следующих конструкций:

    LPEXCEPTION_POINTERS GetExceptionInformation(void):
        mov eax, DWORD PTR [ebp - 14]   ; SEH_FRAME.ExceptionPointers

    DWORD GetExceptionCode(void):
        mov eax, DWORD PTR [ebp - 14]   ; SEH_FRAME.ExceptionPointers
        mov ecx, [eax]                  ; EXCEPTION_POINTERS.ExceptionRecord
        mov ecx, [ecx]                  ; EXCEPTION_RECORD.ExceptionCode

Как видно, результат выполнения этих участков кода очень зависит от содержимого регистра EBP. Поэтому, использование обозначенных функций допускается только в выражении фильтра, а также в самом обработчике. Если же, к примеру, фильтр реализован в виде функции, то вызов данных функций внутри функции-фильтра будет ошибкой, ведь скорее всего её фреймовый стек, адресуемый EBP, уже отличается от фреймового стека вызвавшей её функции.

2.2.4. Unwinding - раскрутка.

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

    void do_something_strange()         // SEH_FRAME3
    {
        char * data = NULL;
        
        __try {
            data = malloc(6*10^23);     // Здесь, скорее всего ничего не дадут...
            data[0] = 0;                // А здесь, будет исключение :smile3:
        }
        __finally {
            free(data);
        }
    }

    void do_some_job()                  // SEH_FRAME2
    {
        char * data = NULL;

        __try {
            data = malloc(1024);
            data[0] = 0;

            do_something_strange();
        }
        __finally {
            free(data);
        }
    }
    
    void do_the_job()                   // SEH_FRAME1
    {
        char * data = NULL;
        __try {
            data = malloc(1024);
            __try {
                do_some_job();
            }
            __finally {
                free(data);
            }
        }
        __except (EXCEPTION_EXECUTE_HANDLER) {
        }
    }

В данном примере в функции do_something_stupid скорее всего возникнет исключение, которое должно быть обработано. В соответствии с рассмотренным механизмом обработки исключений, диспетчером исключений RtlDispatchException будет осуществлён обход всех зарегистрированных для потока обработчиков с целью предоставления им возможности заняться его обработкой. Так, в какой-то момент управление получит обработчик __execute_handler3. Начнётся поиск подходящего обработчика среди локальных обработчиков, связанных с фреймом SEH_FRAME3 через ScopeTable. Но так как в контексте данного фрейма нет ни одного __try // __except блока, управление вернётся к диспетчеру и он продолжит перебор. Так, будет вызван __execute_handler3 для SEH_FRAME2, а затем и для SEH_FRAME1. В этом фрейме присутствует обработчик исключения, фильтр которого вернёт значение EXCEPTION_EXECUTE_HANDLER и будет выполнен код этого обработчика (см. работу функции __execute_handler3). После этого, управление получит код функции do_the_job, следующий за __try // __except блоком. Необходимость освобождения ресурсов требует дополнительных действий, ведь в соответствии с логикой, заложенной в концепцию структурной обработки исключений, в процессе обработки исключения, должны были быть выполнены все __finally-блоки и, тем самым, освобождены используемые ресурсы.

Процесс повторного обхода фреймов с целью вызова финальных обработчиков называется раскруткой. Раскрутка бывает двух видов -- глобальная и локальная. Глобальная раскрутка "раскручивает" фреймы обработчиков исключений (EXCEPTION_REGISTRATION), локальная раскрутка раскручивает локальные фреймы глобального фрейма, доступные через таблицу ScopeTable. Таким образом, а данном случае, в соответствии с логикой работы __execute_handler3, перед передачей управления обработчику исключений, обнаруженному в функции do_the_job, будет сперва осуществлена глобальная раскрутка фреймов функций SEH_FRAME3 и SEH_FRAME2, а затем локальная раскрутка фрейма SEH_FRAME1. Таким образом, будет гарантировано исполнение всех финальных блоков, для которых происходил вход в соответствующий __try-блок.

Механизм глобальной раскрутки представлен в функцией __global_unwind2, являющейся обёрткой к функции библиотеки NTDLL.DLL RtlUnwind. Код этой функции, полученный для Windows 7 представлен ниже:


    __global_unwind2 PROC C uses ebx esi edi ebp, \
                     pRegFrame:PEXECPTION_REGISTRATION
    ; -- function code
        push    0
        push    0
        push    OFFSET @@return
        push    pRegFrame
        call    RtlUnwind
    @@return:
        ret
    __global_unwind2 ENDP

    VOID RtlUnwind(PEXCEPTION_REGISTRATION pRegFrameToken, PVOID, PEXCEPTION_RECORD pException, DWORD dwEaxValue)
    {
        DWORD dwLowLimit, dwHighLimit;
        EXCEPTION_RECORD ExceptionRecord;
        EXCEPTION_RECORD ExceptionRecord2;
        PEXCEPTION_REGISTRATION pRegFrame, pDispContextFrame;
        CONTEXT FakeContext;
            
        // Определим границы стека потока
        RtlpGetStackLimits(&dwLowLimit, dwHighLimit);
            
        if (pException == NULL) {
            pException = &ExceptionRecord;
            ExceptionRecord.ExceptionCode = STATUS_UNWIND;
            ExceptionRecord.ExceptionFlags = 0;
            ExceptionRecord.ExceptionRecord = 0;
            ExceptionRecord.ExceptionAddress = get_return_address(); // [ebp + 4]
            ExceptionRecord.NumberParameters = 0;
        }

        if (pRegFrameToken) {
            pException->ExceptionFlags |= EXCEPTION_UNWINDING;
        } else {
            pException->ExceptionFlags |= (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND);
        }
            
        // Создадим поддельный контекст для того, чтобы иметь возможность
        // продолжить выполнение функцией NtContinue, после вызова которой
        // управление получит код функции __global_unwind2 (метка @@return).
        FakeContext.ContextFlags = 0x10007; // (CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)
        RtlpCaptureContext(&FakeContext);
        // Установим RegEax
        FakeContext.RegEax = dwEaxValue;
        // Скорректируем RegEsp, чтобы после NtContinue всё выглядело так, как
        // будто бы ничего и не было :smile3:
        FakeContext.RegEsp = FakeContext.regEsp + 16;
            
        pRegFrame = RtlpGetRegistrationHead();
        while (pRegFrame != -1) {
            if (pRegFrame == pRegFrameToken) {
                // Продолжить выполнение потока с места вызова __global_unwind2
                NtContinue(&FakeContext, 0);
            } else {
                // Проверить корректность расположения фреймов --
                // фрейм pRegFrameToke должен быть выше всех...
                if (pRegFrame && (pRegFrameToken < pRegFrame)) {
                    goto status_invalid_unwind_target;
                }
            }

            // Проверка валидности фрейма каждого pRegFrame
            if ((pRegFrame < dwLowLimit) || ((pRegFrame + 8) > dwHighLimit) || (pRegFrame & 3) || \
                ((pRegFrame->Handler >= dwLowLimit) && (pRegFrame->Handler < dwHighLimit))) {
                    goto status_bad_stack;
            }
            // Дополнительная проверка валидности расположения обработчика
            if (RtlIsValidHandler(pRegFrame->Handler, 0) == FALSE) {
                goto status_bad_stack;
            }

            DWORD dwResult= RtlpExecuteHandlerForUnwind(pException, pRegFrame, \
                &FakeContext, &pDispContextFrame, pRegFrame->Handler);

            switch (dwResult) {
                // Продолжаем поиск обработчика
                case ExceptionContinueSearch:
                    break;
                // В процессе раскрутки произошло исключение, приведшее к вложенной раскрутке
                case ExceptionCollidedUnwind:
                    pRegFrame = pDispContextFrame;
                    break;
                // Всё остальное
                default:
                    goto status_invalid_disposition;
                    break;
            }

            PEXCEPTION_REGISTRATION pRegFrameTmp = pRegFrame;

            pRegFrame = pRegFrame->Next;

            RtlpUnlinkHandler(pRegFrameTmp);
        }

        // Вся цепочка ракручена, возвращаем управление
        if (pRegFrame == -1) {
            NtContinue(&FakeContext, 0);
        }

        // Иначе -- исключение
        NtRaiseException(pException, &FakeContext, 0);        

        // Сюда мы никогда не попадём
        return;

    status_invalid_unwind_target:
        ExceptionRecord2.NumberParameters = 0;
        ExceptionRecord2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
        ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
        ExceptionRecord2.ExceptionRecord = pException;
        RtlRaiseException(&ExceptionRecord2);
    status_invalid_disposition:
        ExceptionRecord2.NumberParameters = 0;
        ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
        ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
        ExceptionRecord2.ExceptionRecord = pException;
        RtlRaiseException(&ExceptionRecord2);        
    status_bad_stack:
        ExceptionRecord2.NumberParameters = 0;
        ExceptionRecord2.ExceptionCode = STATUS_BAD_STACK;
        ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
        ExceptionRecord2.ExceptionRecord = pException;
        RtlRaiseException(&ExceptionRecord2);
    }

    __declspec(naked) \
    VOID RtlpCaptureContext(PCONTEXT pContext)
    {
            pContext->RegEax = 0;
            pContext->RegEcx = 0;
            pContext->RegEdx = 0;
            pContext->RegEbx = 0;
            pContext->RegEsi = 0;
            pContext->RegEdi = 0;

            pContext->SegCs = CS;
            pContext->SegDs = DS;
            pContext->SegEs = ES;
            pContext->SegFs = FS;
            pContext->SegGs = GS;
            pContext->SegSs = SS;

            pContext->RegEip = get_return_address(); // [ebp + 4]
            pContext->RegEbp = get_ebp_value(); // [ebp + 0]
            pContext->RegEsp = get_esp_value(); // ebp + 8

            pContext->EFlags = get_eflags();
            pContext->ContextFlags = 0x10007;
    }

Код приведённой функции RtlUnwind довльно прост. Основное, что она делает, это осуществляет проход по всем обработчикам исключений с целью вызова соответствующих им обработчиков. Как нетрудно заметить, этот вызов осуществляется схожим образом с вызовом обработчиков в функции RtlDispatchException. Здесь для этого используется функция RtlpExecuteHandlerForUnwind, код которой будет дан ниже. Итак, в RtlUnwind осуществляется обход обработчиков исключений с целью передачи им управления. Но для того, чтобы вызванный обработчик мог определить цель своего вызова (раскрутка), в описателе исключения устанавливается флаг EXCEPTION_UNWINDING, а вслучае завершающей раскрутки, ещё и флаг EXCEPTION_EXIT_UNWIND. Сама же раскрутка осуществляется до фрейма pRegFrameToken, переданного в качестве аргумента.

Возвращаясь к функции __execute_handler3, теперь становятся понятными её действия, в случае определения факта раскрутки. Так, единственное что она делает, это вызывает функцию локальной раскрутки __local_unwind2, параметрами которой служат текущий фрейм и TryLevel = -1, соответствующий выполнению полной локальной раскрутки. После этого, функиця RtlUnwind переходит с следующему фрейму обработчика исключений, а обработанный только что фрейм удаляет с вершины списка (FS:[0]).

Ещё одним важным моментом при выполнении раскрутки является то, что в ходе вызова обработчика также допускается возникновение исключений. Для их обработки используется точно такой же способ, как и при обработке вложенных исключений. Вызов соответствующего данному фрейму обработчика "охраняется" временно устанавливаемым обработчиком UnwindHandler, действия которого сводятся к тому, что в случае возникновения исключения во время раскрутки он сигнализирует об этом диспетчеру исключений, возвратив значение ExceptionCollidedUnwind и установив pDispatcherContext, после чего диспетчер исключений продолжает раскрутку с нового фрейма.

    DWORD \
    RtlpExecuteHandlerForUnwind(PEXCEPTION_RECORD pException, \
                                PEXCEPTION_REGISTRATION pEstablisherFrame, \
                                PCONTEXT pContext, \
                                PEXCEPTION_REGISTRATION * pDispatcherContext, \
                                PEXCEPTION_ROUTINE ExceptionRoutine)
    {
        __asm {
            mov edx, OFFSET UnwindHandler
            jmp ExecuteHandler
        }
    }
    
    DWORD \
    UnwindHandler(PEXCEPTION_RECORD pException, \
                  PEXCEPTION_REGISTRATION pEstablisherFrame, \
                  PCONTEXT pContext, \
                  PEXCEPTION_REGISTRATION * pDispatcherContext)
    {
        if (pException->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) {
            // Тут лежит указатель на прерванный фрейм (см. ExecuteHandler)
            pDispatcherContext[0] = (PEXCEPTION_REGISTRATION)&pEstablisherFrame[1];

            return ExceptionCollidedUnwind;
        }
        
        return ExceptionContinueSearch;
    }

Таким образом, смыслом глобальной раскрутки является обработка всех фреймов обработчиков исключений с целью передачи управления каждому из них с указанием причины вызова -- раскрутки. После этого, будет продолжена обработка следующего фрейма в цепочке фреймов, а данный фрейм будет успешно отлинкован. Вызванный в режиме раскрутки обработчик исключений __except_handler3 осуществляет полную локальную раскрутку данного фрейма, вызывая __local_unwind2. Ниже приведён код этой функции.

    // Данная функция уже выполняется с EBP, настроенным на фрейм обработчика,
    // так что перед его вызовом смена EBP не нужна (см. __except_handler3).
    __cdecl VOID __local_unwind(PEXCEPTION_REGISTRATION pRegFrame, DWORD TryLevelNeeded)
    {
        PSEH_FRAME pSehFrame = REG_TO_SEH(pRegFrame);

        __asm {
            push OFFSET __unwind_handler2
            push fs:[0]
            mov fs:[0], esp
        }
        
        DWORD TryLevel = pSehFrame->TryLevel;

        while (TryLevel != -1) {
            if (TryLevel == TryLevelNeeded) {
                break;
            }
            
            // Понизить уровень вложенности
            pSehFrame->TryLevel = pSehFrame->ScopeTable[TryLevel].EnclosingLevel;
            
            // Выполнить финальный обработчик блока __try // __finally
            if (pSehFrame->ScopeTable[TryLevel].FilterProc == NULL) {
                pSehFrame->ScopeTable[TryLevel].FilterProc();
            }
        }

        __asm {
            pop fs:[0]
            add esp, 4
        }
    }
    
    __cdecl \
    EXCEPTION_DISPOSITION \
    __unwind_handler2(PEXCEPTION_RECORD pException, \
                      PEXCEPTION_REGISTRATION pEstablisherFrame, \
                      PCONTEXT pContext, \
                      PEXCEPTION_REGISTRATION  *pDispatcherContext)
    {
        if (pException->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) {
            // Тут лежит указатель на прерванный фрейм (см. ExecuteHandler)
            pDispatcherContext[0] = (PEXCEPTION_REGISTRATION)&pEstablisherFrame[1];

            return ExceptionCollidedUnwind;
        }
        
        return ExceptionContinueSearch;
    }

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

Рисунок 3. Иллюстрация процесса раскрутки.

3. Заключение.

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

--
Любое использование данного материала в целях, отличных от образовательных, в
том числе его копирование и перепечатка, разрешаются только с согласия автора.

      
      Copyright © 2010 Матвейчиков Илья, matvejchikov(at)gmail.com.

4. Список литературы.

  1. Обработка исключений
  2. Exception handling
  3. Exception Records
  4. Модель вызова диспетчера исключений
  5. Vectored Exception Handling
  6. Цикл статей SEH-изнутри
  7. Ошибки в пользовательском режиме
© Матвейчиков Илья

1 5.707
archive

archive
New Member

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