[Из песочницы] Реализация макроса try для gcc под win32

В сборках GCC под windows (cygwin, mingw) из коробки нет удобного макроса __try{} __except{} для перехвата как программных (throw MyExc) так и системных (сигналы). Попробуем изобрести свой велосипед.

Вся статья в 3-х пунктах:

  1. Создаём try catch блок
  2. Оборачиваем его в SEH блок
  3. Когда SEH поймает исключение, бросаем программное исключение


Если заинтересовал, добро пожаловать под кат.

Немного теории


Исключение
Исключение это некое событие, исключительная ситуация, произошедшая в ходе выполнения программы. Это может быть, например деление на ноль, обращение к недопустимому адресу или переполнение стека. В общем случае, обработка исключений — это реакция программы на возникшее событие. Стоит учитывать, что исключения могут быть сгенерированны программно.Путь исключения в Windows
При недопустимых действиях, происходит
прерывание процессора, которое обрабатывает операционная система. Если исключение произошло в контексте приложения пользователя, то ядро Windows, осуществив необходимые действия, передаёт управление потоку, в котором произошло исключение для его дальнейшей обработки. Однако, поток продолжает своё выполнение не с места возникновения исключения, а со специальной функции — диспетчера исключений KiUserExceptionDispatcher (NTDLL.DLL). Диспетчеру передаётся вся необходимая информация о месте исключения и его характере. Это структуры EXCEPTION_RECORD и CONTEXT.

KiUserExceptionDispatcher загружает цепочку обработчиков исключений (об этом позже) и поочерёдно вызывает их, пока исключение не будет обработано.

SEH в windows
SEH (Structured Exception Handling) механизм обработки исключений в windows. Представляет собой цепочку из структур EXCEPTION_REGISTRATION, расположенных в стеке потока.

   typedef struct _EXCEPTION_REGISTRATION {
        struct _EXCEPTION_REGISTRATION *prev; // указатель на EXCEPTION_REGISTRATION предыдущего обработчика в цепочке
        PEXCEPTION_ROUTINE handler; // указатель на функцию-обработчик
    } EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;


В Win32 указатель на последнюю EXCEPTION_REGISTRATION находится в TIB (Thread Information Block). Дальнейшие описание структур и способов доступа к ним будут касаться только Win32.

typedef struct _NT_TIB32 {
      DWORD ExceptionList;
      DWORD StackBase;
      DWORD StackLimit;
      DWORD SubSystemTib;
      DWORD FiberData;
      DWORD ArbitraryUserPointer;
      DWORD Self;
    } NT_TIB32,*PNT_TIB32;


Первый DWORD в TIB’е — указывает на EXCEPTION_REGISTRATION текущего потока. На TIB текущего потока указывает регистр FS. Таким образом, по адресу FS:[0] можно найти указатель на структуру EXCEPTION_REGISTRATION.

Итак, начнём!

Много практики


Полную версию исходников можно посмотреть на bitbucket.
Проект сделан в Netbeans 8.1.
Для ассемблерного кода я использую intel синтаксис, т.к. он мне привычнее.По этому в gcc для сборки нужен ключ -masm=intel.Эксперимент 1

EXCEPTION_DISPOSITION __cdecl except_handler(
                        PEXCEPTION_RECORD pException,
                        PEXCEPTION_REGISTRATION pEstablisherFrame,
                        PCONTEXT pContext,
                        PEXCEPTION_REGISTRATION *pDispatcherContext) {
                        
                        printf("EXCEPTION_RECORD(%p):\n"
                                " Address=%p\n"
                                " Code=%lx\n"
                                pException,
                                pException->ExceptionAddress,
                                pException->ExceptionCode);

}
        void ex_1() {
                //размещаем в стеке структуру EXCEPTION_REGISTRATION
                EXCEPTION_REGISTRATION seh_ex_reg = EXCEPTION_REGISTRATION();
                //получаем из fs:[0] адресс последнего обработчика исключений
                int seh_prev_addr;
                asm ("mov %0,fs:[0];" : "=r" (seh_prev_addr) :);
                seh_ex_reg.prev = (_EXCEPTION_REGISTRATION_RECORD*) seh_prev_addr;
                seh_ex_reg.handler = (PEXCEPTION_ROUTINE) & except_handler;
                //записываем в fs:[0] адресс новой структуры
                asm volatile("mov fs:[0], %0;"::"r"(&seh_ex_reg) :);

                *(char *) 0 = 0; //генерируем аппаратное исключение
                // востанавливаем обработчик
                asm volatile("mov fs:[0], %0;"::"r"(seh_ex_reg.prev) :);
                
        }



Выполняем, смотрим результат:

EXCEPTION_RECORD (0028f994):
Address=00401d1b EIP инструкции где произошло исключение
Code=c0000005 STATUS_ACCESS_VIOLATION ((DWORD)0xC0000005)


a0888ac916874c67baf8618feecd5c0d.pngЭксперимент 2
Оборачиваем код внутри ex_1 в try{}catch{} и пробуем просто бросить исключение из except_handler:

EXCEPTION_RECORD (0028f994):
Address=00401d7e
Code=c0000005
terminate called after throwing an instance of 'test: SEH_EXCEPT'


Закономерный результат.

Смотрим во что превращается try…catch в gcc, смотрим в ассемблерный код, курим мануалы.

 void throw_seh() {
                throw SEH_EXCEPT();
        }

        void ex_2() {
                NOP;
                try {
                        printf("try1\n");
                        throw SEH_EXCEPT();
                } catch (...) {
                        printf("catch1\n");
                }
                NOP;
                try {
                        printf("try2\n");
                        throw_seh();
                } catch (...) {
                        printf("catch2\n");
                }
        }


0a8dc159a346492f80dc630861d334a3.png

Если интересно что же такое __cxa_allocate_exception и __cxa_throw и рекомендую прочитать цикл статей «С++ exception handling под капотом или как же работают исключения в C++».

Идея: бросать исключения будем не из except_handler, а из синтетической функции, в которую «будет происходить» call, вместо инструкции вызвавшей ошибку.

Финальный вариант

Код
struct SEH_EXCEPTION {
                PVOID address;
                DWORD code;
        };

        void __stdcall landing_throw_unwinder(PVOID exceptionAddress, DWORD exceptionCode) {
                SEH_EXCEPTION ex = SEH_EXCEPTION();
                ex.address = exceptionAddress;
                ex.code = exceptionCode;
                throw ex;
        }

        EXCEPTION_DISPOSITION __cdecl except_handler(
                        PEXCEPTION_RECORD pException,
                        PEXCEPTION_REGISTRATION pEstablisherFrame,
                        PCONTEXT pContext,
                        PEXCEPTION_REGISTRATION *pDispatcherContext) {

                DWORD pLanding = (DWORD) & landing_throw_unwinder;

                //имитация call
                // push параметр DWORD exceptionCode
                pContext->Esp = pContext->Esp - 4;
                *(DWORD *) (pContext->Esp) = pException->ExceptionCode;
                // push параметр exceptionAddress
                pContext->Esp = pContext->Esp - 4;
                *(PVOID *) (pContext->Esp) = pException->ExceptionAddress;
                // push адресс возврата
                pContext->Esp = pContext->Esp - 4;
                *(int *) (pContext->Esp) = pContext->Eip;
                pContext->Eip = pLanding;
                //продолжаем выполнение программы
                return ExceptionContinueExecution;
        }

        /**
         * не даёт компилятору вырезать try{..}catch{...} из за отсутсвия методов бросающих исключение
         * вынуждает компилятор заполнить структуру для перехвата исключения и указать catchIndex
         * вызов метода будет выглядеть так
         * mov[esp+20],index
         * call __throw_magic_link
         *(push eip; jmp __throw_magic_link)
         */
        __attribute__((noinline, stdcall)) void __throw_magic_link() {
                int test;
                asm volatile ("mov %0,1;" : "=r" (test)); //чтобы gcc не вырезал не используемый throw
                if (test > 0) {
                        return;
                }
                throw SEH_EXCEPTION();
        }

        void ex_4() {

                EXCEPTION_REGISTRATION __seh_ex_reg = EXCEPTION_REGISTRATION();
                try {
                        //заполняем новую EXCEPTION_REGISTRATION, пишем её в  fs:[0]
                        int __seh_prev_addr;
                        asm ( "mov %0,fs:[0];" : "=r" (__seh_prev_addr) :);
                        __seh_ex_reg . prev = (_EXCEPTION_REGISTRATION_RECORD *) __seh_prev_addr;
                        __seh_ex_reg . handler = (PEXCEPTION_ROUTINE) & seh::except_handler;
                        asm volatile ( "mov fs:[0], %0;" ::"r" (& __seh_ex_reg) :);
                        //извлекаем из структуры в стеке номер предыдущего catch блока
                        int catchIndex;
                        asm volatile ( "mov %0,[esp+0x20];" : "=r" (catchIndex) :);
                        //"волшебный" метод который "может" бросить исключение
                        //не даёт компилятору вырезать try{..}catch{...} из за отсутсвия методов бросающих исключение
                        //и заставляет заполнить catchIndex
                        seh::__throw_magic_link();
                        {
                                *(char *) 0 = 0;
                        }
                        //не было исключения, востанавливаем catchIndex, нужно для корреткной работы вложенных блоков
                        asm volatile ( "mov [esp+0x20],%0;" ::"r" (catchIndex) :);
                        //востанавливаем предыдущий обработчик исключений
                        asm volatile ( "mov fs:[0], %0;" ::"r" (__seh_ex_reg . prev) :);
                } catch (SEH_EXCEPTION) {
                        //востанавливаем предыдущий обработчик исключений
                        asm volatile ( "mov fs:[0], %0;" ::"r" (__seh_ex_reg . prev) :);
                        printf("except1!\n");
                }

        }



В except_handler выполняем переход в функцию которая бросает исключение, за счёт правки CONTEXT:

DWORD pLanding = (DWORD) & landing_throw_unwinder;
// push адресс возврата
pContext->Esp = pContext->Esp - 4;
*(int *) (pContext->Esp) = pContext->Eip;
pContext->Eip = pLanding;
//продолжаем выполнение программы
return ExceptionContinueExecution;


После установки SEH, внутри блока try добавляем вызов специальной функции __throw_magic_link, которая, по мнению компилятора, может бросить исключение. Это не даст компилятору вырезать наш try…catch блок как не используемый. Чтобы не было проблем при работе вложенных блоков, запоминаем и восстанавливаем catchIndex.Макрос

Код
#undef __try
#define __try \
                                if (bool _try = true) {\
                                        EXCEPTION_REGISTRATION __seh_ex_reg = EXCEPTION_REGISTRATION();/*размещаем в стеке структуру EXCEPTION_REGISTRATION*/\
                                        try {\
                                                int __seh_prev_addr;\
                                                asm ("mov %0,fs:[0];" : "=r" (__seh_prev_addr) :);\
                                                __seh_ex_reg.prev = (_EXCEPTION_REGISTRATION_RECORD*) __seh_prev_addr;\
                                                __seh_ex_reg.handler = (PEXCEPTION_ROUTINE) & seh::except_handler;\
                                                asm volatile("mov fs:[0], %0;"::"r"(&__seh_ex_reg) :);\
                                                int catchIndex; asm volatile ("mov %0,[esp+0x20];" : "=r" (catchIndex) :);/*индекс catch блока*/\
                                                seh::__throw_magic_link();\
                                                /*begin try bloc*/

#define __except_line(filter, line )\
                                                asm volatile ("mov [esp+0x20],%0;" ::"r" (catchIndex) :);;\
                                                asm volatile("mov fs:[0], %0;"::"r"(__seh_ex_reg.prev) :);\
                                        } catch (filter) {\
                                                asm volatile("mov fs:[0], %0;"::"r"(__seh_ex_reg.prev) :);\
                                                _try = false;\
                                                goto __seh_catch_ ## line;\
                                        }\
                                } else\
                                        __seh_catch_ ## line:\
                                        if (!_try)\
                                        /*begin catch bloc*/

#define __except_line__wrap(filter, line ) __except_line(filter,line)

#undef __except
#define __except(filter) __except_line__wrap(filter,__LINE__)

#define __exceptSEH __except_line__wrap(SEH_EXCEPTION,__LINE__)

#endif



Итак, концепт работает, теперь нужно написать макрос для удобного использования, шаблон такой:

//макрос __try:
if (bool _try = true) { //чтобы ограничить область видимости переменных и  использовать else
    EXCEPTION_REGISTRATION __seh_ex_reg; //перед try чтобы был виден в catch
    try {
        //установка seh сопутствующие действия
//конец макроса __try:
        {
        //пользовательский код
        }
//макрос __except:
        //восстановление seh
    }catch (filter) {
        //восстановление seh
        _try = false;
        goto seh_label;
    }\
} else
seh_label:
if (!_try)
//конец макроса __except:
        {
        //пользовательский код
        }

//Пример использования:
__try{
        throw_test();
}
__except{
        printf("except1!\n");
}


Данный код является в большей мере разминкой для ума, нежили готовым бизнес решением, и писался за несколько вечеров из за желания поковыряться в ассемблерном коде. Спасибо за внимания, буду благодарен за критику.

Исходники

Статьи по теме:

Win32 SEH изнутри
С++ exception handling под капотом

© Habrahabr.ru