Pull to refresh

Реализация макроса try для gcc под win32

Reading time 7 min
Views 8.4K
В сборках 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 под капотом
Tags:
Hubs:
+13
Comments 2
Comments Comments 2

Articles