Как компилятор C++ реализует обработку исключений - Реализация

ОГЛАВЛЕНИЕ

 

Реализация

Этот раздел рассматривает три темы, не изложенные выше:

a) Установка обработчика исключений.
b) Блок Catch, повторно выбрасывающий исключение или выбрасывающий новое исключение.
c) Поддержка обработки исключений по потокам.

Инструкции по компоновке смотрите в файле Readme.txt в файлах исходников. Они также содержат демо проект.

Первая задача – установить библиотеку обработки исключений, или, иначе говоря, заменить библиотеку, предоставленную VC++. Из вышесказанного ясно, что VC++ предоставляет функцию __CxxFrameHandler, которая является точкой входа для всех исключений. Для каждой функции компилятор создает процедуру обработки исключений, которая вызывается, если исключение возникает внутри функции. Эта процедура передает указатель funcinfo функции __CxxFrameHandler.

Функция install_my_handler() вставляет в начало __CxxFrameHandler код, который переходит на my_exc_handler(). Но __CxxFrameHandler хранится в неизменяемой кодовой странице. Любая попытка записи в нее вызвала бы нарушение прав доступа. Поэтому первый шаг – изменить доступ страницы на чтение-запись при помощи функции VirtualProtectEx, предоставленной Windows API (интерфейс программирования приложений). После записи в память восстанавливается старая защита страницы. Функция записывает содержимое структуры jmp_instr в начало __CxxFrameHandler.

//install_my_handler.cpp

#include <windows.h>
#include "install_my_handler.h"

//обработчик исключений C++ по умолчанию
extern "C"
EXCEPTION_DISPOSITION  __CxxFrameHandler(
     struct _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     );

namespace
{
    char cpp_handler_instructions[5];
    bool saved_handler_instructions = false;
}

namespace my_handler
{
    //Обработчик исключений, заменяющий обработчик C++ по умолчанию.
    EXCEPTION_DISPOSITION my_exc_handler(
         struct _EXCEPTION_RECORD *ExceptionRecord,
         void * EstablisherFrame,
         struct _CONTEXT *ContextRecord,
         void * DispatcherContext
         ) throw();

#pragma pack(1)
    struct jmp_instr
    {
        unsigned char jmp;
        DWORD         offset;
    };
#pragma pack()
   
    bool WriteMemory(void * loc, void * buffer, int size)
    {
        HANDLE hProcess = GetCurrentProcess();
       
        //меняем защиту страниц, содержащих область памяти
        //[loc, loc+size], на чтение-запись
        DWORD old_protection;
       
        BOOL ret;
        ret = VirtualProtectEx(hProcess, loc, size,
                         PAGE_READWRITE, &old_protection);
        if(ret == FALSE)
            return false;

        ret = WriteProcessMemory(hProcess, loc, buffer, size, NULL);
      
        //восстанавливаем старую защиту
        DWORD o2;
        VirtualProtectEx(hProcess, loc, size, old_protection, &o2);

        return (ret == TRUE);
    }

    bool ReadMemory(void *loc, void *buffer, DWORD size)
    {
        HANDLE hProcess = GetCurrentProcess();
        DWORD bytes_read = 0;
        BOOL ret;
        ret = ReadProcessMemory(hProcess, loc, buffer, size, &bytes_read);
        return (ret == TRUE && bytes_read == size);
    }

    bool install_my_handler()
    {
        void * my_hdlr = my_exc_handler;
        void * cpp_hdlr = __CxxFrameHandler;

        jmp_instr jmp_my_hdlr;
        jmp_my_hdlr.jmp = 0xE9;
        //фактически вычисляется смещение от __CxxFrameHandler+5
        //так как команда jmp имеет длину 5 байт.
        jmp_my_hdlr.offset = reinterpret_cast<char*>(my_hdlr) -
                    (reinterpret_cast<char*>(cpp_hdlr) + 5);
       
        if(!saved_handler_instructions)
        {
            if(!ReadMemory(cpp_hdlr, cpp_handler_instructions,
                        sizeof(cpp_handler_instructions)))
                return false;
            saved_handler_instructions = true;
        }

        return WriteMemory(cpp_hdlr, &jmp_my_hdlr, sizeof(jmp_my_hdlr));
    }

    bool restore_cpp_handler()
    {
        if(!saved_handler_instructions)
            return false;
        else
        {
            void *loc = __CxxFrameHandler;
            return WriteMemory(loc, cpp_handler_instructions,
                           sizeof(cpp_handler_instructions));
        }
    }
}

Диретива #pragma pack(1) в определении структуры jmp_instr говорит компилятору расположить члены структуры без какого-либо заполнения пробелами свободного места между ними. Без этой директивы размер этой структуры 8 байтов. Размер равен 5 байтам, когда эта директива определена.

Возвращаясь к обработке исключений, когда обработчик исключений вызывает блок catch, блок catch может выбросить исключение повторно или выбросить совершенно новое исключение. Если блок catch выбрасывает новое исключение, то обработчик исключений должен уничтожить предыдущее исключение перед продвижением вперед. Если блок catch решает выбросить исключение повторно, обработчик исключений должен распространить текущее исключение. В этот момент обработчик исключений должен разобраться с двумя вопросами: как обработчик исключений узнает, что исключение возникло внутри блока catch, и как он будет следить за старым? Перед тем, как обработчик вызовет блок catch, он сохраняет текущее исключение в объекте exception_storage и регистрирует специальный обработчик исключений, catch_block_protector. Объект exception_storage доступен путем вызова функции get_exception_storage():

exception_storage* p = get_exception_storage();
p->set(pexc, pexc_info);
register catch_block_protector
call catch block
//....

Если исключение (повторно) выброшено из блока catch, управление переходит к catch_block_protector. Теперь эта функция может извлечь предыдущее исключение из объекта exception_storage и уничтожить его, если блок catch выбросил новое исключение. Если блок catch повторно выбросил исключение (что можно выяснить при помощи просмотра первых двух записей массива ExceptionInformation, оба равны нулю. Смотрите код ниже), то обработчик должен распространить текущее исключение, скопировав его в массив ExceptionInformation. Следующий фрагмент показывает функцию catch_block_protector().

 //-------------------------------------------------------------------
// Если этот обработчик вызван, то исключение было (повторно) выброшено из блока catch.
// Обработчик исключения (my_handler) регистрирует этот
// обработчик перед вызовом блока catch. Его задача – определить,
// выбросил ли блок catch новое исключение и повторно выбросил исключение. Если
// блок catch выбросил новое исключение, он должен уничтожить
// объект предыдущего исключения, который был передан блоку catch. Если
// блок catch повторно выбросил исключение, то этот обработчик должен извлечь
// исходное исключение и сохранить его в ExceptionRecord, чтобы
// обработчики исключений могли использовать его.
//-------------------------------------------------------------------
EXCEPTION_DISPOSITION  catch_block_protector(
     _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     ) throw()
{
    EXCEPTION_REGISTRATION *pFrame;
    pFrame = reinterpret_cast<EXCEPTION_REGISTRATION*>
   
    (EstablisherFrame);if(!(ExceptionRecord->ExceptionFlags & ( 
          _EXCEPTION_UNWINDING | _EXCEPTION_EXIT_UNWIND)))
    {
        void *pcur_exc = 0, *pprev_exc = 0;
        const excpt_info *pexc_info = 0, *pprev_excinfo = 0;
        exception_storage *p =
        get_exception_storage();  pprev_exc=
        p->get_exception();  pprev_excinfo=
        p->get_exception_info();p->set(0, 0);
        bool cpp_exc = ExceptionRecord->ExceptionCode == MS_CPP_EXC;
        get_exception(ExceptionRecord, &pcur_exc);
        get_excpt_info(ExceptionRecord, &pexc_info);
        if(cpp_exc && 0 == pcur_exc && 0 ==   pexc_info)
        //повторный выброс исключения
            {ExceptionRecord->ExceptionInformation[1] =
                reinterpret_cast<DWORD>
            (pprev_exc);ExceptionRecord->ExceptionInformation[2] =
                reinterpret_cast<DWORD>(pprev_excinfo);
        }
        else
        {
            exception_helper::destroy(pprev_exc, pprev_excinfo);
        }
    }
    return ExceptionContinueSearch;
}


Рассмотрим одну возможную реализацию функции get_exception_storage():

exception_storage* get_exception_storage()
{
    static exception_storage es;
    return &es;
}

Эта была бы прекрасная реализация, за исключением мира многопоточности. Рассмотрим случай, когда более одного потока захватывают этот объект и пытаются сохранить в нем объект исключения. Это будет ужасно. Каждый поток имеет свой собственный стек и собственную цепочку обработки исключений. Нужно отдельно взятый объект exception_storage для потока. Каждый поток имеет свой собственный объект, который создается, когда поток начинает свое существование, и разрушается, когда потк заканчивается. Windows предоставляет локальную память потока для этой цели. Локальная память потока позволяет каждому объекту иметь свою собственную закрытую копию объекта, доступную через глобальный ключ. С этой целью предоставляются функции TLSGetValue() и TLSSetValue().

Файл Excptstorage.cpp определяет фукцию get_exception_storage(). Этот файл скомпонован как DLL. Это связано с тем, что дает нам возможность знать всякий раз, когда поток создан или уничтожен. Каждый раз, когда поток создан или уничтожен, Windows вызывает каждую функцию DllMain() из DLL (которая загружена в адресное пространство этого процесса). Эта функция вызывается в поток, который был создан. Это позволяет инициализировать отдельные данные потока, объект exception_storage в данном случае.

//excptstorage.cpp

#include "excptstorage.h"
#include <windows.h>

namespace
{
    DWORD dwstorage;
}

namespace my_handler
{
    __declspec(dllexport) exception_storage* get_exception_storage() throw()
    {
        void *p = TlsGetValue(dwstorage);
        return reinterpret_cast<exception_storage*>(p);
    }
}


BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    using my_handler::exception_storage;
    exception_storage *p;
    switch(ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        //Для первого основного потока этот вариант содержит неявный
        //DLL_THREAD_ATTACH, поэтому здесь нет DLL_THREAD_ATTACH для
        // первого основного потока.
        dwstorage = TlsAlloc();
        if(-1 == dwstorage)
            return FALSE;
        p = new exception_storage();
        TlsSetValue(dwstorage, p);
        break;
    case DLL_THREAD_ATTACH:
        p = new exception_storage();
        TlsSetValue(dwstorage, p);
        break;
    case DLL_THREAD_DETACH:
        p = my_handler::get_exception_storage();
        delete p;
        break;
    case DLL_PROCESS_DETACH:
        p = my_handler::get_exception_storage();
        delete p;
        break;
    }
    return TRUE;
}