В сборках GCC под windows (cygwin,mingw) из коробки нет удобного макроса __try{} __except{} для перехвата как программных (throw MyExc) так и системных (сигналы). Попробуем изобрести свой велосипед.
Вся статья в 3-х пунктах:
- Создаём try catch блок
- Оборачиваем его в SEH блок
- Когда 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)
Эксперимент 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"); } }
Если интересно что же такое __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 под капотом
ссылка на оригинал статьи https://habrahabr.ru/post/280304/
Добавить комментарий