Структурная обработка исключений (SEH).

У каждого потока регистр FS указывает на свой Информационный Блок Потока (TIB). В первом двойном слове этого блока содержится адрес начала цепочки обработчиков исключений. Звенья этой цепочки выглядят примерно так :

Первое двойное слово (+0) Адрес следующего звена в цепочке
Второе двойное слово (+4) Адрес обработчика исключений
Третье и последующие Неважно (определяется обработчиком исключений)

Значение 0FFFFFFFFh (-1) в качестве адреса следующего звена обозначает, что это последнее звено в цепочке.

Когда происходит исключение, система берёт адрес цепочки из fs:[0], и вызывает первый обработчик в цепочке. Он может обработать это исключение , а может и нет. Тогда система находит в цепочке адрес следующего обработчика, и вызывает его. Так продолжается до тех пор, пока один из них обработает это исключение, или не закончится цепочка обработчиков.

Если цепочка закончилась, а исключение никто не обработал, то система просто завершает процесс.

Процедура обработчика должна возвратить в регистре eax одним из четырёх чисел :
ExceptionContinueExecution = 0,
ExceptionContinueSearch = 1,
ExceptionNestedException = 2,
ExceptionCollidedUnwind =3.

ExceptionContinueExecution - продолжить выполнение программы. Если процедура вернёт это значение, то, значит обработчик узнал это исключение, изменил приводящие к ошибке параметры, и теперь программа может работать спокойно. Система загружает контекст потока и продолжает выполнение программы.

ExceptionContinueSearch - искать следующий обработчик. Обработчик не может исправить это исключение, поэтому, пусть система запускает другой, вышестоящий обработчик.

ExceptionNestedException,
ExceptionCollidedUnwind - не знаю, что обозначает.

Если обработчик возвратит в eax какое-то другое значение, то генерируется ещё одно, вложенное исключение EXCEPTION_INVALID_DISPOSITION.

Процедура обработчика исключения получает четыре параметра, и вызывается она по C-соглашению (параметры в стеке в обратном порядке, стек очищает вызывающая процедура). Все четыре параметра являются указателями. Давайте подробнее их рассотрим.

1й параметр - pExcept

Указатель на структуру EXCEPTION_RECORD, содержащую информацию об исключении

EXCEPTION_RECORD	struc
	ExceptionCode		dd	?
	ExceptionFlags		dd	?
	pExceptionRecord		dd	?
	ExceptionAddress		dd	?
	NumberParameters		dd	?
	ExceptionInformation	dd	15 dup(?)
EXCEPTION_RECORD	ends

ExceptionCode - код исключения

ExceptionFlags - флаги :
EXCEPTION_NONCONTINUABLE (1) - единственный документированный Microsoft флаг. Если он установлен, то обработчик не может вернуть ExceptionContinueExecution (продолжить выполнение потока). Не представляю, что это может быть за ошибка, после которой невозможно восстановиться (сгорел жёсткий диск?). Если он всё же вернёт это значение, то генерируется исключение EXCEPTION_NONCONTINUABLE_EXCEPTION. Но обычно обработчики его игнорируют (а что толку..).

pExceptionRecord - если это вложенное исключение, указывает на структуру EXCEPTION_RECORD предыдущего необработанного исключения. Если нет, то содержит ноль.

ExceptionAddress - линейный адрес инструкции, вызвавшей исключение.

NumberParameters - число дополнительных параметров в структуре (0-15). Почти у всех исключений параметров нет (содержит 0).

ExceptionInformation - дополнительные параметры. До 15 двойных слов.

Дополнительные параметры получает только исключение EXCEPTION_ACCESS_VIOLATION (2 параметра). При этом первый параметр содержит 0, если поток пытался читать по недоступному адресу, 1 - если писать. Второй параметр содержит этот адрес.

2й параметр - pContext

Указатель на структуру CONTEXT, содержащую значения регистров процессора в момент исключения

CONTEXT	struc
	ContextFlags	dd	?	;Флаги, нужны для API-функций
	iDr0		dd	?	;Отладочные регистры
	iDr1		dd	?
	iDr2		dd	?
	iDr3		dd	?
	iDr6		dd	?
	iDr7		dd	?
	FLOATING_SAVE_AREA FloatSave	;+1Ch - Область сохранения FPU
	regGs		dd	?	;+8Ch
	regFs		dd	?	;+90h
	regEs		dd	?	;+94h
	regDs		dd	?	;+98h
	regEdi		dd	?	;+9Ch
	regEsi		dd	?	;+0A0h
	regEbx		dd	?	;+0A4h
	regEdx		dd	?	;+0A8h
	regEcx		dd	?	;+0ACh
	regEax		dd	?	;+0B0h
	regEbp		dd	?	;+0B4h
	regEip		dd	?	;+0B8h
	regCs		dd	?	;+0BCh
	regFlag		dd	?	;+0C0h - Регистр eflags
	regEsp		dd	?	;+0C4h
	regSs		dd	?	;+0C8h
	ExtendedeRegisters	db	512 dup(?)	;Зависят от типа процессора
CONTEXT	ends

Эта структура определена в windows.inc Обработчик может поменять любой регистр, и продолжить выполнение.

3й параметр - pFrame

Указатель на узел цепочки обработчиков, из которого вызван этот обработчик. Адрес в точности равен адресу сформированного нами звена цепочки.

4й параметр - pDispatch

Не знаю, на что он указывает

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

Вот как выглядит скелет обработчика на ассемблере. Напомню, что процедура, как это принято в Windows, должна сохранять содержимое регистров ebx, ebp, esi и edi.

pDispatch		equ[esp+10h]
pFrame 		equ[esp+0Ch]
pContext 		equ[esp+8]
pExcept 		equ[esp+4]

ExceptHandler:	mov	eax, pExcept
		test	[eax+4], EXCEPTION_NONCONTINUABLE	;Флаги исключения
		jnz	next_handler
		...		;Обрабатываем исключение
		mov	eax, ExceptionContinueExecution
		ret
next_handler:	mov	eax, ExceptionContinueSearch
		ret

Как установить обработчик исключений?

Установить его очень просто: нам нужно 8 байт памяти для звена цепочки, и пара команд для его формирования. Обычно память берётся из стека 2мя командами push. Звено формируем из адреса начала цепочки (хранится в fs:[0]) - он будет указателем на следующее звеном, и адреса нашего обработчика. Адрес нашего звена нужно поместить в fs:[0], и всё - обработчик установлен.

assume	fs:nothing			;Нужно для компилятора
	push	offset ExceptHandler	;Адрес нашего обработчика в [esp+4]
	push	fs:[0]			;Адрес следующего звена в [esp+0]
	mov	fs:[0], esp		;esp - указатель на наше звено - новое начало цепочки

Убрать его ещё проще (если esp остаётся неизменным) :

	pop	fs:[0]			;Восстановить предыдущее начало цепочки
	add	esp, 4			;Выкинуть из стека адрес обработчика (можно pop eax)

Давайте рассмотрим простой пример:

assume	fs:nothing
		push	offset ExceptHandler	;Адрес обработчика
		push	fs:[0]			;Следующее звено
		mov	fs:[0], esp		;Устанавливаем обработчик

		xor	eax, eax		;Обнуляем eax
		mov	eax, [eax]		;Обращаемся по нулевому адресу
;Пред. команда вызовет исключение EXCEPTION_ACCESS_VIOLATION,
;и управление передаётся обработчику
		;Если здесь есть код, он не будет выполняться
FromHandler:	pop	fs:[0]			;Убираем обработчик
		pop	eax			;Очищаем стек

pDispatch		equ[esp+10h]
pFrame			equ[esp+0Ch]
pContext 		equ[esp+8]
pExcept 		equ[esp+4]

ExceptHandler:	mov	eax, pContext		;Указатель на контекст потока
;Устанавливаем новое значение eip
		mov	(CONTEXT ptr [eax]).regEip, offset FromHandler
;Можно mov [eax+0B8h], offset FromHandler, если больше нравится
		mov	eax, ExceptionContinueExecution	;Продолжить выполнение
		ret					; с нового места

В этом примере обработчик продолжает выполнение с нового, безопасного места. Это достигается записью нового значения регистра eip в контекст потока. Так как при генерации исключения мы не меняли регистр esp, то он остаётся постоянным, и его можно не восстанавливать.

А почему именно стек?

Собирался сказать, что не обязательно использовать стек, можно сделать звено и в секции данных, но не буду этого делать. Оказывается, Windows проверяет, находится ли звено в стеке потока, и если нет, отказывается вызывать обработчик (верхняя и нижняя границы стека находятся в fs:[4] и fs:[8]). С чем связано такое ограничение, я не знаю - наверное, для большего контроля целостности цепочки.

Обязательно ли возвращать управление?

Обработчик исключения возвращать управление Windows не обязан - он может делать всё, что захочет.

strcpy:		push	offset Handler	;Установим наш обработчик
		push	fs:[0]
		mov	fs:[0], esp
@@:		lodsb			;Если esi или edi - неправильные указатели,
		stosb			;произойдёт исключение, и обработчик всё
		cmp	al, 0		;исправит
		jne	@B
EndOfCopy:	pop	fs:[0]		;В любом случае убираем обработчик
		pop	eax
		ret

pFrame	equ	[esp+0Ch]	;pFrame равно тому, что мы поместили в fs:[0]
Handler:	mov	esp, pFrame	;Нужно восстановить наш стек
		jmp	EndOfCopy	;Как будто ничего и не произошло

Стандартный обработчик

Помните, я говорил, что при необработанном исключении процесс тихо умирает? А как же окошко "Программа выполнила недопустимую операцию... и т.п.", спросите вы? Дело в том, что исключение как раз обрабатывается - посмотрите под отладчиком на цепочку обработчиков новой программы. Windows при создании каждого потока создаёт звено цепочки с указателем на стандартный обработчик - именно он будет последним в цепочке, и ему будут передаваться все необработанные нами исключения. Он вызывает функцию UnhandledExceptionFilter, которая и рисует это дурацкое окошко. Если вы нажмёте "Закрыть", то стандартный обработчик просто вернёт ExceptionContinueSearch системе, а та убьет процесс. Избавиться от стандартного обработчика просто - нужно разрушить созданную системой цепочку (mov dword ptr fs:[0], -1).

Сайт управляется системой uCoz