В сборках 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=%pn"
" Code=%lxn"
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("try1n");
throw SEH_EXCEPT();
} catch (...) {
printf("catch1n");
}
NOP;
try {
printf("try2n");
throw_seh();
} catch (...) {
printf("catch2n");
}
}
Если интересно что же такое __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 под капотом
Автор: ExIngus