Разбираемся с EXCEPTION_CONTINUE_EXECUTION

Механизм структурированной обработки исключений (Structured Exception Handling, SEH) позволяет вернуться к инструкции, сгенерировавшей исключение и попробовать выполнить ее заново. Для этого в блок __except нужно передать значение EXCEPTION_CONTINUE_EXECUTION. Важно помнить, что возврат происходит к ассемблерной инструкции, а не инструкции высокоуровневого языка.

Рассмотрим пример 32-битного приложения, в котором происходит деление на 0:

DWORD a = 0, b = 1, res = 0;

__try
{
	res = b / a;
	printf("res = %d\n", res);
}

__except (CustomFilter(&a, GetExceptionInformation())){ }

Взглянем на ассемблерный код:

a54895e5335ed232817002bd6b43411e.png

Исследуя код в отладчике, можно увидеть, что исключение генерируется командой div. В первом аргументе этой инструкции, находящемся в регистре EAX, находится делимое, а делитель (второй аргумент) берется из памяти (dword ptr [a]). Таким образом, для «исправления» исключения нужно изменить значение, хранящееся по адресу dword ptr [a], т.е. в переменной a.

Наша функция-фильтр исключений будет принимать первым аргументом адрес региона памяти (переменной a), значение в котором нужно изменить. Далее функция разыменовывает указатель и помещает в заданный регион памяти ненулевое значение:

DWORD WINAPI CustomFilter(PVOID Arg, PEXCEPTION_POINTERS ExPtrs)
{
	// Only for x86, NOT x64!
	if (ExPtrs->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
	{
		DWORD* ptr = (DWORD*)Arg;
		(*ptr)++;

		printf("Argument with zero value has been incremented\n");
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	return EXCEPTION_CONTINUE_SEARCH;
}

Как видно, после наших исправлений, вычисления произведены корректно и выполнена следующая за делением инструкция — вывод результата на экран:

2702dae57d4062daf1c4735ad76ebfc2.png

Теперь скомпилируем наш пример для x64 и запустим:

a95d7f8cb76683e0b1b20455a8ba4bb9.png

Теперь наша программа входит в бесконечный цикл. В чем же дело?

 Взглянем на ассемблерный код блока __try:

0bed6187e95c159d8ea3b8beb73834b5.png

Теперь второй аргумент инструкции div хранится в регистре ECX. Поэтому, даже изменив значение в памяти (переменную a), значение в регистре ECX остается нетронутым. Поэтому, когда из обработчика исключения произойдет возврат к инструкции div, снова будет произведено деление на нуль, сгенерировано исключение, а потом будет вызван наш обработчик исключения. Таким образом, получили бесконечный цикл.

Для «исправления» исключения необходимо изменить значение в регистре ECX, а потом заново выполнить инструкцию div. К счастью, сделать это довольно просто. Обработчик исключения восстанавливает контекст, который был за момент генерации исключения, а затем повторно выполняет инструкцию, вызвавшую исключение. Контекст хранит, в том числе, и значения регистров. Контекст потока хранится в структуре CONTEXT, указатель на которую хранится в структуре EXCEPTION_POINTERS, которая может быть получена в блоке __except при помощи макроса GetExceptionInformation. GetExceptionInformation может быть вызвана только в фильтре исключений. В структуре CONTEXT имеется поле Rcx, в котором сохранено значение регистра RCX, младшей частью которого является регистр ECX. Данное значение будет восстановлено при повторном выполнении инструкции, сгенерировавшей исключение.

Тогда функция CustomFilter должна быть модифицирована следующим образом:

DWORD WINAPI CustomFilter(PVOID Arg, PEXCEPTION_POINTERS ExPtrs)
{
	// Only for x64, NOT x86!
	if (ExPtrs->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
	{
		ExPtrs->ContextRecord->Rcx = 1;

		printf("Argument with zero value has been incremented\n");
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	return EXCEPTION_CONTINUE_SEARCH;
}

Теперь приложение не входит в бесконечный цикл, корректно производит вычисления и выполняет следующую за делением инструкцию — вывод результата на экран:

537be4fc194c6981e51bc07105e026d8.png

Для того, чтобы функция CustomFilter работала для обоих разрядностей (x86 и x64) можно воспользоваться директивами условной компиляции:

DWORD WINAPI CustomFilter(PVOID Arg, PEXCEPTION_POINTERS ExPtrs)
{
	if (ExPtrs->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
	{
		#if _WIN64
		ExPtrs->ContextRecord->Rcx = 1;
		
		#elif _WIN32
		DWORD* ptr = (DWORD*)Arg;
		(*ptr)++;

		#endif

		printf("Argument with zero value has been incremented\n");
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	return EXCEPTION_CONTINUE_SEARCH;
}

Применяя аналогичный подход, реализуем обработку ошибок обращения по нулевому указателю. Код исключения, возникающего при попытке разыменования нулевого указателя — EXCEPTION_ACCESS_VIOLATION. Однако, данное исключение может возникнуть и в других случаях, например, обращение по невалидному адресу. Мы же рассмотрим простейший случай. Для отличия обращения по нулевому адресу от других исключительных ситуаций нужно выполнить дополнительные проверки.

Код, генерирующий исключение:

BYTE* mem = NULL;

__try
{
	mem[0] = 'A';

	printf("mem = %s\n", mem);

	HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, mem);
}

__except(CustomFilter2(&mem, GetExceptionInformation())) { }

Разыменование нулевого указателя произойдет в строке:

mem[0] = 'A';

Данный код преобразуется в следующие инструкции:

b5998a284e3162a4d678123760da9fc3.png

Адрес, записанный в переменную mem, помещается в регистр EDX. В регистр ECX помещается индекс, точнее смещение в байтах, кратное размеру элементов массива, относительно начала региона памяти. Код 41h — шестнадцатеричное значение кода символа «A» в таблице ASCII. Исключение нарушения доступа будет сгенерировано инструкцией

mov byte ptr [edx+ecx], 41h

Таким образом, для «исправления» нам необходимо поместить в регистр EDX адрес корректного региона памяти.

На x64 код, генерирующий исключение выглядит так:

de7a660939c9ad6cefdf63c7715f9691.png

Здесь смещение хранится в регистре RAX, а адрес региона памяти — в регистре RCX. Для «исправления» адрес корректного региона памяти должен быть помещен в регистр RCX.

В обработчике исключения выделим в Heap регион памяти и запишем туда данные — символы слова «Hello». Адрес данного региона должен быть сохранен не только в соответствующем регистре в контексте, но и в переменной mem, для того, чтобы иметь возможность освободит выделенную память.

Код функции CustomFilter2, работающей для обоих разрядностей (x86 и x64):

DWORD WINAPI CustomFilter2(PVOID Arg, PEXCEPTION_POINTERS ExPtrs)
{
	if (ExPtrs->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
	{
		BYTE** ptr = (BYTE**)Arg;

		*ptr = (BYTE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);

		#if _WIN64
		ExPtrs->ContextRecord->Rcx = (ULONG_PTR)*ptr;

		#elif _WIN32
		ExPtrs->ContextRecord->Edx = (DWORD)*ptr;

		#endif

		(*ptr)[0] = 'H';
		(*ptr)[1] = 'e';
		(*ptr)[2] = 'l';
		(*ptr)[3] = 'l';
		(*ptr)[4] = 'o';

		printf("Memory has been allocated\n");
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	return EXCEPTION_CONTINUE_SEARCH;
}

После запуска приложения видим, что после генерации исключения происходит «исправление» — выделение региона памяти и запись туда символов «Hello», после этого вновь выполняется инструкция, сгенерировавшая исключение, в результате чего первый символ заменяется на «A». Далее происходит освобождение выделенной памяти.

7a295298b916220de8419ed457db1b51.png

А что еще можно сделать? Можно вернуться не к машинной инструкции, сгенерировавшей исключение, а к одной из предыдущих. Например, к инструкции, помещающей значение делителя из памяти в регистр. Адрес следующей для выполнения инструкции хранится в регистре EIP (x86) или RIP (x64). Следовательно, значение этого регистра хранится в поле Eip (x86) или Rip (x64) структуры CONTEXT. Также адрес инструкции, сгенерировавшей исключение хранится в поле ExceptionAddress вложенной в EXCEPTION_POINTERS структуры EXCEPTION_RECORD.

Рассмотрим уже известный пример с делением на нуль:

DWORD a = 0, b = 1, res = 0;

__try
{
	res = b / a;
	printf("res = %d\n", res);
}

__except (CustomFilter3(&a, GetExceptionInformation())){ }

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

х86:

6b3a6c32ac508e1059565bc0a9797e13.png

Как мы уже говорили ранее, значение делителя в x86 коде берется непосредственно из памяти. Поэтому изменять значение регистра EIP нам не нужно. Нужно только изменить значение переменной a, что мы уже и делали в предыдущем примере.

х64:

f830428e5dea9124cdfad9120abe0b6f.png

А вот с x64 кодом ситуация интереснее. Значение переменной a берется из памяти и помещается в регистр EAX:

mov eax, dword ptr [a]

А далее это значение помещается в стэк по адресу [rbp+174h]:

mov dword ptr [rbp+174h], eax

Далее значение, хранящееся по этому адресу, помещается в регистр ECX:

mov ecx, dword ptr [rbp+174h]

И, наконец, значение в регистре ECX используется в качестве делителя в команде div:

div eax, ecx

Следовательно, после изменения значения переменной a, нужно вернутся к инструкции, загружающей значение a из памяти, т.е. к:

mov eax, dword ptr [a]

Рассчитаем разницу адресов между командой, вызвавшей исключение (div) и инструкцией, считывающей значение a из памяти:

Delta = 0x7FF7A16D1992 – 0x7FF7A16D197E = 0x14 = 20

Таким образом, для повторного считывания значения a из памяти необходимо из значения регистра RIP вычесть 20.

Код функции-фильтра CustomFilter3:

DWORD WINAPI CustomFilter3(PVOID Arg, PEXCEPTION_POINTERS ExPtrs)
{
	if (ExPtrs->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) 
  {
		DWORD* ptr = (DWORD*)Arg;
		(*ptr)++;

		#if _WIN64
		ExPtrs->ContextRecord->Rip -= 20;
		#endif

		printf("Argument with zero value has been reinitialized\n");
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	return EXCEPTION_CONTINUE_SEARCH;
}

При работе нашего приложения снова получаем исправленный результат:

2fb3b1f1ad1752a6001afd9f20b9a012.png

Как видим, структурированная обработка исключений очень мощный и полезный механизм, который не только позволяет «отловить» исключение, но и исправить данные, которые привели к его генерации.

© Habrahabr.ru