Структурная обработка исключений (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).