Реализация макроса 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)

Эксперимент 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *