Обработка исключений Win32 для программистов на ассемблере — Архив WASM.RU

Все статьи

Обработка исключений Win32 для программистов на ассемблере — Архив WASM.RU

Оригинал статьи может быть найден тут: http://www.jorgon.freeserve.co.uk/ExceptFrame.htm

Исходные тексты

1. Введение
2. Обработка исключений на практике
3. Установка простого обработчика исключений
4. Раскрутка стека
5. Информация, передаваемая обработчикам
6. Восстановление после устранения причины исключения
7. Продолжение выполнения после вызова финального обработчика
8. Пошаговое выполнение, получаемое с помощью установки trap flag из обработчика
9. Обработка исключений в многопоточных приложениях
10. Демонстрационная программа Except.exe

1. Введение

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

Рис. 1

или его более сложного аналога в Windows NT.

Что дает обработка исключений...

Суть механизма обработки исключений (известного как: "Структурная Обработка Исключений" ("Structured Exception Handling" или "SEH")) состоит в том, что ваше приложение, во время своего выполнения, может установить некоторое количество процедур обратного вызова (callback routines), называемых "обработчиками исключений" (“exception handlers”) и затем, при возникновении какого-либо исключения, система вызовет одну из этих процедур, чтобы дать приложению возможность самому его обработать. Это позволило бы рассчитывать, что обработчик исключений сможет сам устранить причину возникновения исключения, и даст приложению возможность продолжить свое выполнение, либо с кода вызвавшего исключение, либо с "безопасного места" ("safe place") в коде так, словно ничто не случилось. При этом предупреждающее сообщение о закрытии программы выдано не будет, и пользователь о возникшем исключении ничего не узнает. В процессе обработки исключения обработчику, может понадобиться сделать следующее: закрыть описатели (handle), закрыть временные файлы, освободить контексты устройств, освободить области памяти, проинформировать другие потоки, затем произвести раскрутку стека или закрыть проблемный поток. В течение этого процесса обработчик исключений может создавать отчет о том, что он делает и сохранять его в файле для более позднего анализа.

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

Запланированные исключения

Windows SDK предлагает и другое использование механизма SEH. Например, использование его для мониторинга использования памяти. Идея заключается в том, что если вы обращаетесь к еще не переданной области памяти, то происходит исключение. Вы перехватываете его, и производите передачу памяти. Это можно сделать, перехватив исключение, вызванное нарушением доступа к памяти [номер исключения: 0C0000005h], которое произошло бы, если бы ваш код попробовал обратиться (чтение / запись) к памяти, которая не была передана.

Другой вариант организации мониторинга использования памяти с помощью SEH основывается на использовании т.н. сторожевых страниц (guard page). Для его реализации нужно при вызове функции VirtualAlloc для передачи памяти, или последующим вызовом функции VirtualProtect установить флажок сторожевой страницы (guard page). В этом случае, при обращении (чтение / запись) к охраняемой области памяти происходит исключение сторожевой страницы (guard page exception) [080000001h], после чего флажок сторожевой страницы (guard page flag) будет сброшен (released). Таким образом, обработчик исключений информируется относительно требуемых объемов памяти и, в случае необходимости, может снова установить этот флажок.

Этот метод широко используется в системе, например: потоку требуется больше стекового пространства, чем ему передано на данный момент, тогда объем стека увеличивается автоматически.

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

Этот способ будет работать, даже если ту же самую область памяти использует несколько потоков, так как указанная переменная может совместно использоваться всеми потоками. В этом случае, обработка исключений: 0C0000005h, может использоваться как резерв на тот случай, если ваш код станет работать неправильно.

Ограничения механизма: обработки исключений...

Кроме деления на ноль [код исключения: 0C0000094h], которого можно легко избежать защитным кодированием, самый распространенный тип исключения - попытка обратиться к памяти по недоступному адресу (illegal memory address) [0C0000005h]. Есть несколько путей, приводящих к его возникновению. Например:

  • неправильное значение индексного регистра при адресации памяти;
  • неожиданные бесконечные циклы, содержащие обращения к памяти;
  • при дисбалансе операций PUSH и POP, возврат из вызванной процедуры происходит на неопределенный адрес памяти;
  • непредвиденное повреждение во входных файлах данных.

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

Другие возможные причины отказа программы

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

  • Недостаток системных ресурсов;
  • Бесконечный цикл, который не содержит обращений к памяти;

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

Фатальные исключения

Некоторые ошибки настолько фатальны, что система не может даже вызвать ваш обработчик исключений. В этом случае, если пользователю повезет: появится сообщение о завершении работы системы, в противном случае будет показан т.н. “синий экран смерти”, сигнализирующий о возникновении "фатальной" ошибки. В большинстве случаев это происходит в результате полного аварийного отказа системы, и для продолжения работы требуется перезагрузка компьютера. К счастью, в Win32 трудно столкнуться с такими ошибками, но это все еще возможно.

... и где обработка исключений действительно дает выигрыш

Потратив некоторое время на рассмотрение того, чего обработка исключений сделать не может, давайте рассмотрим примеры, где ее применение наиболее эффективно:

  • В процессе разработки программы, перехват и информирование об ошибках можно использовать как еще один способ отладочного контроля;
  • При использовании кода, написанного другими людьми, которому нельзя полностью доверять;
  • При обращении (чтение / запись) к области памяти, которая может быть перемещена без предупреждения. Например, во время исследования системных областей памяти (которыми управляет сама система) или областей памяти, которые могут быть закрыты другими процессами и/или потоками;
  • Использование указателей на файлы, которые могут быть разрушены или иметь неопределенный формат. Здесь обработка исключений была бы более эффективна, чем применение API-функций: IsBadReadPtr или IsBadWritePtr, для проверки каждого указателя перед его использованием;
  • При непредвиденных ошибках.

2. Обработка исключений на практике

Последовательность действий Win32 при возникновении исключения

Чтобы понять, что ваша программа может и\или должна сделать в процессе обработки исключений, вы должны знать, как функционирует система при возникновении исключения. Если вы этого не знаете, то не сможете, в полной мере, понять то, что здесь изложено. Однако ниже приведена последовательность действий, которые предпринимает Win32 при возникновении исключения. Знание этих действий поможет вам понять изложенное здесь. Вот эти шаги:

  1. Windows решает, хочет ли он послать это исключение обработчику исключений программы. Если это так, и если программа отлаживается, Windows уведомит отладчик о возникновении исключения, приостанавливая программу и посылая EXCEPTION_DEBUG_EVENT (значение 1h) отладчику (это первое предупреждение);
  2. Если программа не отлаживается или если отладчик не обработает исключение, система посылает исключение вашему внутрипоточному обработчику исключений (если он установлен). Внутрипоточный обработчик устанавливается во время выполнения программы путем изменения значения первого двойного слова в TIB (Thread Information Block), адрес которого - FS:[0];
  3. Внутрипоточный обработчик исключений может попытаться сам обработать исключение, или не делать этого, оставляя это дело для обработчиков находящихся далее по цепочке (если таковые имеются);
  4. В конечном счете, если ни один из внутрипоточных обработчиков не обработал исключение, и если программа отлаживается, система снова приостановит программу и уведомит об этом отладчик (это второе и последнее предупреждение);
  5. Если программа не отлаживается или если отладчик снова не обработает исключение, система вызовет ваш финальный обработчик (если он установлен). Финальный обработчик устанавливается приложением во время своего выполнения , с помощью функции API: SetUnhandledExceptionFilter;
  6. Если ваш финальный обработчик не обработает исключение, то после того, как он отработает, будет вызван системный финальный обработчик. Он может вывести сообщение о закрытии программы. В зависимости от параметров настройки в системном реестре, это диалог может дать пользователю шанс подключить к программе отладчик. Если никакой отладчик не может быть подключен или если отладчик не может помочь, программа будет обречена, т.к. система вызовет функцию ExitProcess, для принудительного завершения программы;
  7. Тем не менее, прежде чем окончательно закрыть программу система производит "финальную раскрутку" стека потока вызвавшего исключение.

Примечание переводчика
В ring3 любой эксепшн начинается в KiUserExceptionDispatcher (экспортируется из ntdll.dll). Именно отсюда он идет в SEH (если таковой есть) или в UnhandledExceptionFilter (аналогично). Похучив это место можно не только анализировать все исключения программы, но и фильтровать свои вперед программы.
(С) ASMax / http://reng.ru/board, 03.09.2002

Преимущества использования ассемблера для обработки исключений

Win32 обеспечивает только каркас для обработки исключений, предоставляя для этого несколько API. Так что большая часть кода, необходимого для обработки исключений должна быть написана вами самостоятельно.

"C"-программисты могут использовать различные надстройки, предоставляемые им их компиляторами, путем включения в свой исходный код их инструкций, типа: _try, _except, _finally, _catch и _throw.

Один из реальных недостатков кода, созданного компилятором состоит в том, что компилятор может чрезвычайно раздуть в размерах выходной exe-файл.

Также, большинство "C"- программистов понятия не имеет, какой код создан компилятором, для обработки исключений, а это еще один - реальный недостаток, потому что для эффективной обработки исключения, вы нуждаетесь в гибкости, понимании и управляемости. Это так, потому что исключения могут быть перехвачены и обработаны различными способами и на различных независимых уровнях в вашем коде. Используя ассемблер, вы можете создавать малый по размеру, надежный и гибкий код, который вы можете плотно подогнать к вашему собственному приложению.

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

Информация по обработке исключений на низком уровне трудна для понимания, и поэтому содержимое Win32 Software Development Kit (SDK) концентрируют внимание на том, как для этой цели использовать "C"-инструкции компилятора, а не на проблемах формирования программы, используя непосредственно Win32 каркас (Win32 framework).

Информация для этой статьи была получена, с помощью тестовой программы, отладчика, и дизассемблированного кода, созданного компилятором "C". Прилагающаяся программа: Except.exe, демонстрирует описанные здесь методы.

3. Установка простого обработчика исключений

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

Два типа обработчиков исключений

Как было сказано выше, существует два типа обработчиков исключений.

Тип 1 – финальный обработчик исключений

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

Установка финального обработчика исключений

Обычно, он устанавливается в первичном потоке, как можно ближе к точке входа программы, вызовом API-функции SetUnhandledExceptionFilter. Таким образом, он защищает всю программу от этой точки и до конца. Нет никакой необходимости удалять обработчик перед завершением, т.к. это автоматически сделает сама Windows.

Пример:

MAIN:	;Точка входа в программу.
PUSH OFFSET FINAL_HANDLER 
CALL SetUnhandledExceptionFilter 
; ... 
; ...	;Код защищенный финальным обработчиком.
; ... 
CALL ExitProcess 
;**************** 
FINAL_HANDLER: 
; ...                                                        
; ... 	;Код, обеспечивающий корректный выход.
; ...                                                        
MOV EAX,1 ;Возможные возвращаемые значения и их описание:
	;eax=-1 перезагрузить контекст и продолжить выполнение программы.
	;eax= 1 не показывать сообщение о предстоящем закрытии программы.
	;eax= 0 показывать сообщение о предстоящем закрытии программы.
RET

Не пытайтесь сформировать цепочку из финальных обработчиков исключений

В любой момент времени, в процессе может быть только один, определенный приложением, финальный обработчик исключений. Если ваш код вызывает во второй раз функцию SetUnhandledExceptionFilter, то адрес финального обработчика исключений просто меняется на новое значение, а предыдущий теряется.

Тип 2 – внутрипоточный обработчик исключений

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

Значение в FS - 16-разрядный селектор, который указывает на "Блок информации Потока" ("Thread Information Block", TIB), - структуру, которая содержит важную информацию о каждом потоке. Самое первое двойное слово (DWORD) в TIB указывает на структуру, которую мы будем называть - структурой "ERR".

Структура "ERR" состоит как минимум из двух двойных слов, как показано на следующей схеме:

1st dword +0 Указатель на следующую структуру ERR (расположенную выше по цепочке).
2nd dword +4 Указатель на имеющийся обработчик исключений.

Установка внутрипоточного обработчика исключений

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

Пример:

PUSH OFFSET HANDLER
PUSH FS:[0]	;Адрес следующей структуры ERR.
MOV FS:[0],ESP 	;Помещаем в FS:[0] адрес только что созданной структуры ERR.
... 
... 	        ;Здесь находится защищенный обработчиком код.
... 
POP FS:[0]	;Восстанавливаем в FS:[0] адрес предыдущей ERR, которая 
		;до этого была следующей в цепочке вверх после текущей ERR.
		;Тем самым, мы удаляем текущий обработчик исключений. 
ADD ESP, 4h	; Очищаем стек от остатков ненужной нам более структуры ERR.
RET 
;*********************** 
HANDLER: 
... 
...		;Здесь находится код обработчика исключений.
... 
MOV EAX,1	;Возможные возвращаемые значения и их описание:
		;eax=1 исключение не было обработано (система вызовет 
		;следующий по цепочке внутрипоточный обработчик);
		;eax=0 перезагрузить контекст и продолжить выполнение
		;программы.
RET

Сцепление в цепочку внутрипоточных обработчиков исключений

В вышеупомянутом коде мы можем видеть, что 2-ое двойное слово структуры ERR, которое содержит адрес вашего обработчика, помещено в стек первым, после чего в стек помещено 1-ое двойное слово, содержащее адрес следующей структуры ERR, командой PUSH FS:[0]. Предположим что код, который был тогда защищен этим обработчиком, вызывает другие функции, которые нуждаются в собственной индивидуальной защите. Тогда вы можете создать другую структуру ERR и обработчик, чтобы защитить тот код точно таким же способом. Это называется: формированием цепочки (которая представляет собой односторонний связанный список). Практически это означает, что, когда происходит исключение, система будет обходить цепочку обработчиков, взывая первым тот обработчик исключений, который установлен последним перед тем кодом, который вызвал исключение. Если тот обработчик не обработает это исключением (при этом он должен вернуть EAX=1), то система вызывает следующий обработчик, находящийся выше по цепочке. Так как каждая структура ERR содержит адрес следующего обработчика выше по цепочке, то этим способом можно установить любое количество этих обработчиков. Каждый обработчик мог бы принять меры или обработать определенные для него типы исключений в зависимости от того, что находится в пределах его видимости, в вашем коде. Для хранения структур ERR используется стек, так что избегайте их случайной перезаписи. Однако, при необходимости, вы можете использовать для хранения структур ERR и другие части памяти (т.е. использовать для этого стек не обязательно, а просто удобно).

Примечание переводчика
По поводу последнего утверждения Edmond [HI-TECH] сделал следующее замечание: На самом деле ЭТО НЕ СОВСЕМ ТАК!!!!!!! Код ядра, который управляет SEH, проверяет где находится структура ERR. Если эта структура находится не в стеке - он вызывает аварийное исключение.

4. Раскрутка стека

В этой части мы собираемся рассмотреть раскрутку стека (stack unwinds), потому что это не должно больше оставаться тайной! "Раскрутка стека" звучит очень пугающе, но практически - это просто вызов всех внутрипоточных обработчиков исключений, локальные данные которых находятся в стеке ниже, чем локальные данные внутрипоточного обработчика производящего раскрутку (или вообще всех внутрипоточных обработчиков в цепочке в том случае, если раскрутку производит финальный обработчик) и затем (вероятно) выполнение программы продолжится в пределах другого стекового фрейма. Другими словами программа готовится к игнорированию содержимого стека между этими двумя позициями.

Допустим, у вас есть цепочка внутрипоточных обработчиков, установленного как на этой схеме, где функция A вызывает функцию B которая, в свою очередь, вызывает функцию C:

Рис. 2

Тогда содержимое стека будет выглядеть приблизительно так:

****
Третий стековый фрейм Использование стека функцией C.
Структура ERR от обработчика 3.
Локальные данные функции C.
Второй стековый фрейм Адрес возврата из функции C.
Использование стека функцией B.
Структура ERR от обработчика 2.
Локальные данные функции B.
Первый стековый фрейм Адрес возврата из функции B.
Использование стека функцией A.
Структура ERR от обработчика 1.
Локальные данные функции A.
Локальные данные функции A.
****

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

Теперь предположим, что в функции C происходит исключение. После этого, как мы знаем, система начнет обход цепочки обработчиков. Сначала будет вызван обработчик 3. Допустим, что обработчик 3 не смог обработать это исключение (он вернул EAX=1), тогда будет вызван обработчик 2. Допустим, что обработчик 2 тоже отказался обработать это исключение (он вернул EAX=1), тогда вызывается обработчик 1. Если Обработчик 1 берется за обработку исключения, ему, возможно, потребуется произвести очистку использованных локальных данных в стековых фреймах созданных функциями B и C. Это можно сделать, вызвав раскрутку. При этом просто повторяется обход цепочки обработчиков: обработчик 3 -> обработчик 2 -> обработчик 1.

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

Пример:

  • Этот тип обхода цепочки обработчиков инициируется вашим обработчиком, а не системой;
  • Флажок исключения в EXCEPTION_RECORD должен быть установлен в 2h (EH_UNWINDING). Это сигнализирует внутрипоточному обработчику, что произошел вызов из другого обработчика, расположенного выше по цепочке и предназначенный для того, чтобы текущий обработчик мог очистить используемые локальные данные. Обработчик не должен пытаться сделать что-то еще, кроме этого и должен возвратить EAX=1;
  • Обход обработчиков заканчивается на обработчике, который непосредственно предшествует вызывающему обход обработчику. Например (см. предыдущую схему), если раскрутку инициирует обработчик 1 - последним в процессе раскрутки будет вызван обработчик 2. Нет никакой необходимости в том, чтобы обработчик 1 вызывал самого себя, т.к. он имеет непосредственный доступ к своим локальным данным и может их удалить.

Вы сможете увидеть ниже ("Обеспечение доступа к локальным данным"), как в процессе обхода обработчиков обработчик сможет найти локальные данные.

Как произвести раскрутку

Обработчик может инициализировать раскрутку, используя API RtlUnwind, или, как мы увидим позже, он может легко сделать это сам, используя для этого написанный вами код. Этот API можно вызвать следующим образом:

PUSH Return value 
PUSH pExceptionRecord 
PUSH OFFSET CodeLabel 
PUSH LastStackFrame 
CALL RtlUnwind 

Где:

Return value - указывает на возвращаемое после раскрутки значение (вам оно, скорее всего, не понадобится);

pExceptionRecord - указатель на структуру EXCEPTION_RECORD, которая является одной из тех структур, которые передаются обработчику, при возникновении исключения;

CodeLabel - адрес инструкции, с которой должно продолжиться выполнение после проведения раскрутки и обычно он находится сразу за инструкцией вызвавшей раскрутку посредством вызова RtlUnwind. Если этот адрес не определен API, по идее, возвращается нормальным способом, однако SDK предлагает использовать этот адрес для улучшения безопасности этого типа API;

LastStackFrame - stack frame, на котором процесс раскрутки должен завершиться. Обычно это адрес в стеке структуры ERR, содержащей адрес обработчика, который инициализирует раскрутку.

Примечание
В отличие от других API, RtlUnwind не гарантирует сохранения значений регистров EBX, ESI или EDI - если вы используете их в своем коде, то вы должны сохранить их значение перед помещением в стек первого параметра функции и восстановить сразу после CodeLabel.

Собственный код раскрутки

Следующий код моделирует процесс раскрутки (здесь EBX содержит адрес посланной обработчику структуры EXCEPTION_RECORD):

MOV D[EBX+4],2h     ;Делаем флаг исключения EH_UNWINDING.
MOV EDI,FS:[0]      ;Получаем адрес первого внутрипоточного обработчика.
>L2: 
CMP D[EDI],-1       ;Смотрим, является ли он последним?
JZ >L3              ;Если да, то завершаемся.
PUSH EDI,EBX        ;Сохраняем на стеке адреса структур ERR и EXCEPTION_RECORD.
CALL [EDI+4]        ;Вызываем обработчик для выполнения кода очистки.
ADD ESP,8h          ;Удаляем из стека два сохраненных там параметра. 
MOV EDI,[EDI]       ;Получаем указатель на следующую структуру ERR,
JMP L2              ;и если она не последняя, то отрабатываем ее.
L3:                 ;Метка окончания работы.

Здесь каждый обработчик вызывают в свою очередь со значением 2h в ExceptionFlag, пока не будет достигнут последний обработчик (система имеет значение: -1, в последней структуре ERR). Показанный выше код не проверяет искажение значений в [EDI] и в [EDI+4]. Первый - адрес стека и может быть проверен, для гарантии, что он ниже базы стека потока, указанного в FS:[8] и выше вершины стека потока, указанного в FS:[4]. Второй - адрес кода и может быть проверен, для гарантии, что он находится между двумя метками в коде, одной в начале вашего кода и другой в конце него. Еще вы можете проверить, могут ли адреса [EDI] и [EDI+4] читаться, вызывая API IsBadReadPtr.

Раскрутка из финального обработчика

Раскрутка стека может быть инициирована не только из внутрипоточного обработчика. Это можно сделать и из финального обработчика, вызывая функцию RtlUnwind, или свой собственный код раскрутки и затем возвращая EAX=-1 (См. "Продолжение выполнения после вызова финального обработчика").

Заключительная раскрутка перед завершением

Если финальный обработчик установлен, и возвращает EAX=0 или EAX=1, система принудительно завершит процесс. Однако, перед окончательным завершением происходит кое-что интересное. Система производит финальную раскрутку (final unwind), возвращаясь на самый первый обработчик в цепочке (то есть обработчик, охраняющий код, в котором произошло исключение). Это дает вашему обработчику самую последнюю возможность выполнить, необходимый внутри каждого стекового фрейма, очищающий код. Вы сможете увидеть, что возникновение этой финальной раскрутки прозрачно в том случае, если вы установили сопроводительную демонстрационную программу Except.exe и, позволив исключению дойти до финального обработчика, нажмете там F3 или F5.

5. Информация, передаваемая обработчикам

Очевидно, что обработчикам должен передаваться достаточный объем информации для того, чтобы они смогли попытаться устранить причину исключения, создавать файлы регистрации ошибок, или выдать сообщение пользователю. Как мы увидим, система передает эту информацию через стек, при вызове обработчика. Также, вы можете передать обработчикам вашу собственную информацию, расширяя для этого структуру ERR.

Информация, передаваемая финальному обработчику

Финальный обработчик документирован в Windows Software Development Kit ("SDK") как API "UnhandledExceptionFilter". Он получает только один параметр, который является указателем на структуру EXCEPTION_POINTERS. Вот описание этой структуры:

EXCEPTION_POINTERS +0 Указатель на структуру: EXCEPTION_RECORD
+4 Указатель на структуру: CONTEXT

Структура EXCEPTION_RECORD имеет следующие поля:

EXCEPTION_RECORD +0 ExceptionCode
+4 ExceptionFlag
+8 NestedExceptionRecord
+0C ExceptionAddress
+10 NumberParameters
+14 AdditionalData

Где:

ExceptionCode - содержит тип возникшего исключения. В заголовочных файлах из SDK определено множество типов исключений. Вот лишь некоторые из тех, с которыми вы можете столкнуться на практике:

C0000005h – Поток пытался считать или записать по виртуальному адресу, не имея на то необходимых прав. Это самое распространенное исключение;

C0000094h – Поток пытался поделить число целого типа на делитель того же типа, равный 0;

C00000FDh – Стек, отведенный потоку, исчерпан;

80000001h – Поток пытался обратиться к странице памяти с атрибутом защиты FAGE_GUARD.

Страница становится доступной, и генерируется данное исключение

Исключения, связанные с обработкой самих исключений:

C0000025h – Обработчик исключений вернул в регистре EAX значение 0, в ответ на невозобновляемое исключение (noncontinuable exception). – Обработчик не должен пытаться обрабатывать его;

C0000026h – Код исключения используемый системой во время обработки исключения. Этот код может использоваться, если система столкнулась с неожиданным возвращенным значением от обработчика. Этот код также используется в том случае, если при вызове функции RtlUnwind, не указанна структура EXCEPTION_RECORD.

Исключения, связанные с отладкой:

80000003h – Точка прерывания, инициированная командой INT3;

80000004h – Трассировочная ловушка или другой механизм пошагового исполнения команд подал сигнал о выполнении одной команды.

Биты 31-30 29 28 27-16 15-0
Содержимое Код степени "тяжести" (severity) Кем определен — Microsoft или пользователем Зарезервирован Код подсистемы (facility code) Код исключения
Значение 0 = успех 1 = информация 2 = предупреждение 3 = ошибка 0 = Microsoft 1 = for user Должен быть 0 (см. таблицу ниже) Определяется Microsoft Определяется Microsoft или пользователем

Типичный собственный код исключения, посланный посредством функции RaiseException мог бы быть: E0000100h (ошибка, приложение, code=100h).

Код определенный пользователем - исключение с таким кодом ваше приложение может отправить самому себе, вызвав API-функцию: RaiseException. Это быстрый способ вызова кода завершения находящегося непосредственно в вашем обработчике, если это потребуется;

Exception flag - используется для управления работой обработчика. Может иметь следующие значения:

0 – Исключение, допускающее возобновление работы (может быть обработано);

1 – Исключение, не допускающее возобновление работы (не может быть обработано);

2 – раскрутка стека – не пытайтесь это обрабатывать.

Примечание переводчика
Здесь, вероятно, нужно было бы пояснить, что, собственно говоря, тут имеется в виду под понятиями: ‘Может быть обработано’ и ‘Не может быть обработано’. Так вот, речь здесь идет только о том, какие значения может вернуть обработчик, а не о том, может ли обработчик выполнить при этом какие-либо действия или нет. Таким образом, если исключение не может быть обработано, то обработчик не должен возвращать значение 0 (0 – перезагрузить контекст и продолжить выполнение программы). Это необходимо для того, чтобы система вызвала следующий по цепочке обработчик.

Nested exception record - если обработчик вызван для обработки исключения, которое произошло во время обработки другого исключения, то указывает на другую структуру EXCEPTION_RECORD, которая относится к тому исключению, обработка которого была аварийно прервана;

Exception address – адрес кода вызвавшего исключение;

NumberParameters – количество двойных слов находящихся в поле Additional information;

Additional information – массив элементов (DWORD) содержащих дополнительную информацию. Здесь может быть, или информация, непосредственно переданная приложением, при вызове функции RaiseException, или, если код произошедшего исключения - C0000005h, здесь будет находиться следующая информация:

1st dword - 0= нарушение прав доступа произошло при попытке чтения, 1= нарушение прав доступа произошло при попытке записи;

2nd dword – адрес, при попытке доступа к которому, произошло нарушение.

Вторая часть структуры EXCEPTION_POINTERS, передаваемой финальному обработчику, указывает на CONTEXT, - процессорно-зависимую структуру, которая содержит значения всех регистров на момент возникновения исключения. Заголовочный файл WINNT.H содержит описание структуры CONTEXT для различных типов процессоров. Ваша программа может узнать тип используемого процессора, вызвав API-функцию: GetSystemInfo. Для процессоров архитектуры IA32 (Intel 386+) структура CONTEXT определена следующим образом:

+0 context flags 
(used when calling GetThreadContext) 
DEBUG REGISTERS 
+4 debug register #0 
+8 debug register #1 
+C debug register #2 
+10 debug register #3 
+14 debug register #6 
+18 debug register #7 
FLOATING POINT / MMX registers 
+1C ControlWord 
+20 StatusWord 
+24 TagWord 
+28 ErrorOffset 
+2C ErrorSelector 
+30 DataOffset 
+34 DataSelector 
+38 FP registers x 8 (10 bytes each) 
+88 Cr0NpxState 
SEGMENT REGISTERS 
+8C gs register 
+90 fs register 
+94 es register 
+98 ds register 
ORDINARY REGISTERS 
+9C edi register 
+A0 esi register 
+A4 ebx register 
+A8 edx register 
+AC ecx register 
+B0 eax register 
CONTROL REGISTERS 
+B4 ebp register 
+B8 eip register 
+BC cs register 
+C0 eflags register 
+C4 esp register 
+C8 ss register

Информация, передаваемая внутрипоточным обработчикам

При вызове внутрипоточного обработчика, ему в качестве параметров передаются указатели на три структуры данных. Эти указатели располагаются в стеке следующим образом:

ESP+4 Указатель на структуру: EXCEPTION_RECORD
ESP+8 Указатель на принадлежащую обработчику структуру ERR.
ESP+C Указатель на структуру: CONTEXT
ESP+10 Param

Примечание
В отличие от обычного способа вызова CALLBACK-процедур принятого в Windows, при вызове внутрипоточного обработчика используется C-конвенция. Поэтому, после того как процедура обработчика отработает, вызывающий код должен сам удалить переданные ей параметры из стека. Подтверждение этому можно найти, если изучить тот код в Kernel32, который производит вызов обработчика:

PUSH Param, CONTEXT, ERR, EXCEPTION_RECORD 
CALL HANDLER 
ADD ESP, 10h 
Описание значения информации содержащейся в параметре с именем Param, я нигде не нашел.

Примечание переводчика
Вот формальное объявление четвертого параметра (указанного выше как Param): void * DispatcherContext.

Структуры EXCEPTION_RECORD и CONTEXT были описаны выше.

Структура ERR - это та самая структура, которую вы создавали на стеке, когда устанавливали обработчик. Она должна содержать указатель на следующую структуру ERR, и адрес кода установленного обработчика (см. выше "Установка простого обработчика исключений"). Указатель на структуру ERR переданный внутрипоточному обработчику, указывает на вершину этой структуры. Это обстоятельство позволяет увеличивать размер структуры ERR для того, чтобы передавать обработчику дополнительную информацию.

Ниже на схеме показан пример содержимого этой структуры. При вызове обработчика адрес вершины этой структуры передается во втором параметре (т.е. находится по адресу [ESP+8]):

ERR +00h Указатель на следующую структуру: ERR.
+04h Адрес точки входа в код обработчика.
+08h Адрес кода 'безопасного места' ('safe-place') для обработчика.
+0Ch Информация для обработчика.
+10h Область для флажков.
+14h Значение регистра EBP в безопасном месте.

Как мы увидим ниже (см. "Продолжение выполнения программы с безопасного места"), поля в +8 и +14 могут использоваться обработчиком, для возобновления выполнения программы после исключения.

Обеспечение доступа к локальным данным

Давайте теперь получше рассмотрим расположение структуры ERR на стеке относительно стекового фрейма, который вполне может содержать локальные данные. Это важно, т.к. обработчику вполне может понадобиться обратиться к этим локальным данным, чтобы правильно очистить их, удалив из стека. Ниже показан типичный код, который может использоваться для установки внутрипоточного обработчика в функции, которая имеет локальные переменные:

MYFUNCTION:           ;Точка входа в функцию.
PUSH EBP              ;Сохраняем значение регистра EBP.
MOV EBP,ESP           ;Используем регистр EBP как указатель на стековый фрейм.
SUB ESP,40h           ;Выделяем на стеке пространство размером в 16 двойных
                      ;слов для локальных данных процедуры, в диапазоне адресов:
                      ;от [EBP-4] до [EBP-40h].
;*** local data now at  
;*** install handler    ;Здесь на стеке создается структура ERR текущей
                        ;процедуры:
PUSH EBP                ;ERR+14h Сохраняем значение регистра EBP (значение EBP в
                        ;        безопасном месте).
PUSH 0                  ;ERR+10h Область для флажков.
PUSH 0                  ;ERR+0Ch Информация для обработчика.
PUSH OFFSET SAFE_PLACE  ;ERR+08h Адрес кода 'безопасного места'.
PUSH OFFSET HANDLER     ;ERR+04h Адрес обработчика. 
PUSH FS:[0]             ;ERR+00h Сохранить адрес следующей ERR расположенной
                        ;        выше по цепочке.
MOV FS:[0],ESP          ;Указываем на, только что созданную на стеке,
                        ;структуру ERR.
... 
...                     ;Здесь расположен защищаемый код.
... 
JMP >L10                ;Если нет никакого исключения, нормально завершаемся.
SAFE_PLACE:             ;Обработчик устанавливает значения регистров EIP/ESP/EBP
                        ;для этого места.
L10: 
POP FS:[0]              ;Удаляем, из цепочки обработчиков, обработчик текущей
                        ;процедуры путем восстановления на вершине цепочки
                        ;предыдущего обработчика.
MOV ESP,EBP 
POP EBP 
RET 
;***************** 
HANDLER: 
RET

При использовании этого кода, на момент вызова обработчика стек имеет состояние, показанное ниже на схеме, а [ESP+08] указывает на вершину структуры ERR (т.е. на ERR+00h):

ERR +00h Указатель на следующую структуру: ERR.
ERR +04h Адрес точки входа в код обработчика.
ERR +08h Адрес кода 'безопасного места' ('safe-place') для обработчика.
ERR +0Ch Информация для обработчика.
ERR +10h Пространство для флагов.
ERR +14h Значение регистра EBP в безопасном месте.
ERR+18h Локальные данные.
ERR+1Ch Локальные данные.
ERR+20h Локальные данные.
Другие локальные данные.

Как видите, обработчик способен найти адреса локальных переменных процедуры, используя передаваемый ему указатель на структуру ERR. Это возможно благодаря тому, что ему известны размер структуры ERR, и расположение локальных переменных на стеке. Также обратите внимание, что в выше приведенном примере, для доступа к локальным переменным процедуры можно использовать значение, указанное в поле: “Значение регистра EBP в безопасном месте”.

6. Восстановление после устранения причины исключения

Продолжение выполнения программы с безопасного места

Выбор безопасного места

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

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

Другая важная проблема, которую нужно иметь в виду заключается в том, как упростить получение правильных значений в регистрах EIP, ESP и EBP нужных для безопасного места. Как мы позже увидим, это не так уж и трудно.

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

Пример получения безопасного места

Для примера, давайте снова посмотрим на приведенный выше код в функции MYFUNCTION. Как вы видите, в коде есть метка: "SAFE_PLACE". Это как раз то самое место, с которого можно было бы безопасно продолжить выполнение программы, после того как обработчик, выполнил бы всю необходимую очистку.

Значения этих трех регистров должны быть установлены по следующим причинам:

ESP - чтобы операция POP FS:[0] могла выполнить правильное действие, а также, чтобы в случае необходимости, можно было бы получить из стека и другие значения;

EBP – чтобы гарантированно иметь доступ к локальным переменным из обработчика. А также для того, чтобы можно было перед возвратом из MYFUNCTION восстановить в регистре ESP правильное значение;

EIP – чтобы выполнение программы продолжилось с безопасного места.

Как вы видите, функция обработчика может получить любое из этих значений. Правильное значение регистра ESP - это адрес вершины структуры ERR (адрес вершины структуры ERR, при вызове обработчика, передается в [ESP+8h]). Правильное значение регистра EBP доступно из [ERR+14h], т.к. оно было сохранено на стеке, при создании структуры ERR. Правильное значение регистра EIP – это адрес кода безопасного места, его можно взять из [ERR+8h].

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

HANDLER: 
     PUSH EBP 
     MOV EBP, ESP        ;** now [EBP+08h]=pointer to EXCEPTION_RECORD
                         ;**     [EBP+0Ch]=pointer to ERR structure 
                         ;**     [EBP+10h]=pointer to CONTEXT
     PUSH EBX, EDI, ESI  ;Сохраняем значения тех регистров, которые требуются
                         ;для работы Windows.
     MOV EBX, [EBP+08h]  ;Теперь регистр EBX содержит адрес структуры
                         ;EXCEPTION_RECORD.
     TEST D[EBX+4], 01h  ;Смотрим, позволяет ли данное исключение продолжить
                         ;выполнение программы.
     JNZ >L5             ;Если нет, то мы не должны его обрабатывать.
     TEST D[EBX+4], 02h  ;Смотрим, не установлен ли флаг EH_UNWINDING (для
                         ;раскрутки).
     JZ >L2              ;Если нет, то продолжаем обычную работу.
     ... 
     ...                 ;Код, используемый для очистки стека при раскрутке.
     ... 
     JMP >L5             ;Должен вернуть: 1, для перехода на следующий
                         ;обработчик в цепочке.
L2: 
     PUSH 0              ;Возвращаемое значение (не используется).
     PUSH [EBP+08h]      ;Указатель на структуру EXCEPTION_RECORD, переданную в
                         ;обработчик.
     PUSH OFFSET UN23    ;Адрес кода, куда функция RtlUnwind должна вернуть
                         ;управление.
     PUSH [EBP+0Ch]      ;Указатель на структуру ERR, переданную в
                         ;обработчик.
     CALL RtlUnwind 
UN23: 
     MOV ESI, [EBP+10h]  ;Теперь регистр ESI содержит адрес структуры 
                         ;CONTEXT.
     MOV EDX, [EBP+0Ch]  ;Получаем указатель на структуру ERR.
     MOV [ESI+0C4h] ,EDX ;Используем его в качестве нового значения 
                         ;регистра ESP.
     MOV EAX, [EDX+08h]  ;Получаем адрес безопасного места из структуры ERR.
     MOV [ESI+B8h] ,EAX  ;Вставляем его как новое значение регистра EIP.
     MOV EAX, [EDX+14h]  ;Получаем значение регистра EBP в безопасном месте из
                         ;структуры ERR.
     MOV [ESI+0B4h] ,EAX ;Вставляем его как новое значение регистра EBP.
     XOR EAX, EAX        ;Сигнализируем о том, что система должна перезагрузить 
                         ;контекст & продолжить выполнение программы (в данном
                         ;случае, с безопасного места) – возвращаем eax=0.
     JMP >L6                             
L5: 
     MOV EAX, 1          ;Сигнализируем о том, что система должна вызвать 
                         ;следующий по цепочке обработчик – возвращаем eax=1.
L6:                      ;Обычное завершение (никакие параметры не
                         ;используются).
     POP ESI, EDI, EBX               
     MOV ESP, EBP 
     POP EBP 
     RET

Устранение причины исключения

В приведенном выше примере вы видели, что для того, чтобы выполнение программы продолжилось с безопасного места, в контексте были изменены значения регистров EIP, EBP и ESP. Этот же метод (замена значений регистров в контексте) можно использовать для того, чтобы устранить причину возникновения исключения, и продолжить выполнение программы, начиная с того кода, который вызвал исключение.

Очевидным примером этому является деление на ноль, которое может быть устранено обработчиком, путем замены значения делителя на 1, и затем возвращающим в регистре EAX значение 0 (если это внутрипоточный обработчик) для того, чтобы заставить систему перезагрузить контекст, и продолжить выполнение с инструкции вызвавшей исключение. В случае нарушений доступа к памяти (memory violations), вы можете использовать тот факт, что адрес, по которому произошло нарушение, передается во втором двойном слове дополнительного информационного поля структуры EXCEPTION_RECORD. Обработчик может использовать это значение для того, чтобы вызвать функцию VirtualAlloc для передачи дополнительной памяти, начиная с указанного места. Если сделать это удалось, обработчик может перезагрузить контекст (неизменённый), и возвратить EAX=0 (если это внутрипоточный обработчик), чтобы продолжить выполнение программы с кода инструкции вызвавшей исключение.

7. Продолжение выполнения после вызова финального обработчика

При желании, вы можете обработать исключение в финальном обработчике. Вспомните, в начале статьи я говорил, что система вызывает финальный обработчик, когда хочет сообщить процессу о том, что собирается закрыть его принудительно. И это действительно так. Возвращаемые в EAX значения у финального обработчика отличаются от таковых у внутрипоточного обработчика. Если возвращается - EAX=1, то система принудительно завершает процесс, не показывая системное сообщение об аварийном закрытии программы, и если EAX=0, то система тоже принудительно завершает процесс, но при этом показывает это сообщение.

Однако, есть также третий код возврата, EAX=-1, который должным образом описан в SDK как "EXCEPTION_CONTINUE_EXECUTION". Эффект от него аналогичен эффекту от возвращения EAX=0 из внутрипоточного обработчика, то есть он заставляет систему перезагрузить CONTEXT в процессор, и продолжить выполнение с адреса загруженного в регистр EIP из контекста. Конечно же, перед возвратом в систему, финальный обработчик тоже может изменить значения полей в структуре CONTEXT, как это делает внутрипоточный обработчик. Таким образом, финальный обработчик может помочь программе оправиться от исключения, продолжив выполнение программы в подходящем безопасном месте, или же попробовать устранить причину исключения.

Вы должны чётко понимать, что отказ от внутрипоточных обработчиков в пользу использования финального обработчика для обработки всех возможных исключений приводит к уменьшению гибкости механизма обработки исключений. Это происходит по следующим причинам:

  1. Вы не можете делать финальные обработчики вложенными. В любой момент времени, вы можете иметь в своем коде только один зарегистрированный финальный обработчик установленный с помощью функции SetUnhandledExceptionFilter. При желании, вы можете изменять адрес финального обработчика, для различных частей выполняющегося кода. Функция SetUnhandledExceptionFilter возвращает адрес заменяемого финального обработчика, так что вы можете использовать его следующим образом:
    PUSH OFFSET FINAL_HANDLER 
    CALL SetUnhandledExceptionFilter 
    PUSH EAX                          ;Сохраним адрес предыдущего обработчика.
    ... 
    ...                               ;Здесь располагается защищаемый код.
    ...  
    CALL SetUnhandledExceptionFilter  ;Восстанавливаем предыдущий обработчик.
    
    Обратите внимание на то, что во время второго запроса к SetUnhandledExceptionFilter адрес предыдущего обработчика находится уже на стеке из-за более ранней команды PUSH EAX;
  2. Другая трудность с использованием финального обработчика заключается в том, что передаваемая ему информация ограничивается структурами EXCEPTION_RECORD и CONTEXT. Поэтому вы должны будете сохранить адрес кода безопасного места, и значений регистров ESP и EBP в том самом безопасном месте, в статической памяти. Это легко может быть сделано во время выполнения. Например, когда обрабатывается сообщение: WM_COMMAND, в процессе работы оконной процедуры:
    PROCESS_COMMAND:         ;Вызывается при uMsg=111h (WM_COMMAND).
    MOV EBPSAFE_PLACE, EBP   ;Сохранить значение регистра EBP в безопасном месте.
    MOV ESPSAFE_PLACE, ESP   ;Сохранить значение регистра ESP в безопасном месте.
    ... 
    ...                      ;Это защищенный код.
    ... 
    SAFE_PLACE:              ;Метка кода для безопасного места.
    XOR EAX, EAX             ;Возвращаем EAX=0 - сообщение обработано.
    RET
    
    В вышеупомянутом примере, чтобы помочь программе оправиться от исключения, продолжив ее выполнение в безопасном месте, обработчик может вставить значения EBPSAFE_PLACE в CONTEXT +0B4h (EBP), ESPSAFE_PLACE в CONTEXT +0C4h (ESP), и адрес безопасного места в CONTEXT +0B8h (EIP), и затем, при возврате, возвратить EAX=-1.
  3. Обратите внимание, что в процессе принудительной раскрутки стека системой из-за аварийного завершения программы, вызываются только внутрипоточные обработчики проблемного потока (если они установлены), и не вызывается финальный обработчик. Если бы в проблемном потоке не было установлено никаких внутрипоточных обработчиков, финальный обработчик должен был бы сам, непосредственно перед возвращением в систему, производить всю очистку стека.

8. Пошаговое выполнение, получаемое с помощью установки trap flag из обработчика

На время разработки программы, вы можете сделать для нее простой пошаговый тестер, используя тот факт, что обработчик может перед возвратом в систему устанавливать флажок trap flag в регистровом контексте. Вы можете сделать так, чтобы обработчик отобразил результаты на экране, или сформировал их дамп в файле. Это может быть полезно, если вы подозреваете, что результаты изменяются, когда программа выполняется под управлением отладчика, или если вы хотите быстро увидеть, как определенная часть кода реагирует на различные входные данные. Вставьте следующий код туда, откуда вы хотите начать выполнение в пошаговом режиме:

MOV SSCOUNT,5 
INT 3 

SSCOUNT – символьное имя данных (data symbol), и указывает на количество шагов, которые обработчик должен сделать перед возвращением к нормальной работе. Инструкция INT 3 возбуждает исключение: 80000003, и таким образом вызывает ваш обработчик. Код в вашей разрабатываемой программе должен быть защищен внутрипоточным обработчиком, содержащим код подобный этому:

SS_HANDLER: 
PUSH EBP 
MOV EBP,ESP 
PUSH EBX,EDI,ESI     ;Сохраняем значения тех регистров, которые требуются для
                     ;работы Windows.
MOV EBX,[EBP+8]      ;Теперь регистр EBX содержит адрес структуры
                     ;EXCEPTION_RECORD.

TEST D[EBX+4], 01h   ;Смотрим, позволяет ли данное исключение продолжить
                     ;выполнение программы.
JNZ >L14             ;Если нет, то мы не должны его обрабатывать.

TEST D[EBX+4],02h    ;Смотрим, не установлен ли флаг EH_UNWINDING (для
                     ;раскрутки).
JNZ >L14             ;Если да, то мы не должны его обрабатывать.
MOV ESI,[EBP+10h]    ;Теперь регистр ESI содержит адрес структуры
                     ;CONTEXT.
MOV EAX,[EBX]        ;Получаем ExceptionCode.
CMP EAX,80000004h    ;Смотрим, не вызвано ли исключение установленным флагом
                     ;trap flag.
JZ >L10              ;Если да, то обрабатываем его.
CMP EAX,80000003h    ;Смотрим, не вызвано ли исключение командой INT3,
                     ;включающей пошаговый режим.
JNZ >L14             ;Если нет, то мы не должны его обрабатывать.
L10: 
DEC SSCOUNT          ;Остановка после того как выполнено указанное кол-во шагов.
JZ >L12 
OR D[ESI+0C0h],100h  ;Установить в контексте trap flag.
L12: 
... 
...                  ;Код, отображающий результаты на экране.
... 
XOR EAX,EAX          ;EAX=0 перезагрузить контекст, и продолжить выполнение.
JMP >L17 
L14: 
MOV EAX,1            ;EAX=1 система должна вызвать следующий обработчик.
L17: 
POP ESI,EDI,EBX 
MOV ESP,EBP 
POP EBP 
RET

Здесь первый вызов обработчика инициирован инструкцией INT 3 (система резко воспротивилась моей попытке использовать инструкцию INT 1). При получении этого исключения, которое может происходить только в фрагменте кода, вставленном в тестируемый код, обработчик устанавливает trap flag в контексте перед возвращением. Это служит причиной возникновения исключения: 80000004, которое возникает после выполнения следующей команды, и приводит к вызову нашего обработчика. Обратите внимание, что при этом исключении, EIP - уже указывает на следующую команду, то есть на следующую команду после INT3, или на следующую команду, после команды выполненной при установленном флажке trap flag. Для того чтобы продолжить выполнение программы в пошаговом режиме нужно, перед возвращением в систему, снова установить trap flag.

* Thanks to G.W.Wilhelm, Jr of IBM for this idea

9. Обработка исключений в многопоточных приложениях

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

Вот известные мне нюансы, применения предоставленного системой механизма обработки исключений в контексте многопоточного приложения:

  1. В любой взятый момент, в процессе может быть зарегистрированным только один финальный обработчик. Если какой-либо из потоков процесса вызовет функцию SetUnhandledExceptionFilter, то она просто заменит предыдущий финальный обработчик (если он был) на новый, т.е. не существует цепочки для финальных обработчиков, подобной цепочки для внутрипоточных обработчиков. Поэтому самый простой, и вероятно самый лучший способ использовать финальный обработчик в многопоточном приложении заключается в установке его в первичном потоке, как можно ближе к точке входа программы;
  2. Система вызовет финальный обработчик в том случае, если процесс должен быть завершен, независимо от того, какой из его потоков вызвал исключение;
  3. В процессе финальной раскрутки (перед самым завершением процесса) вызываются только те внутрипоточные обработчики, которые установлены в потоке, вызвавшем исключение. Даже в том случае, если в процессе есть другие (не имеющие отношения к возникшему исключению) потоки которые имеют свои окна и очереди сообщений - система не предупредит их о том, что собирается принудительно завершить процесс. Не существует специальных сообщений, которые бы передавались им при этом, и которые бы отличались от обычных сообщений вызванных потерей фокуса другими окнами;
  4. Следовательно, другие (не имеющие отношения к возникшему исключению) потоки не могут рассчитывать, что система произведет их финальную раскрутку. Таким образом, они останутся не осведомленными о предстоящем принудительном завершении процесса;
  5. В том случае, если другие (не имеющие отношения к возникшему исключению) потоки тоже нуждаются в очистке перед завершением процесса, то вам нужно информировать их из финального обработчика. Финальный обработчик должен будет, перед возвращением в систему подождать пока все эти потоки закончат свою очистку;
  6. Способ, которым не имеющие отношения к возникшему исключению потоки информируются о предстоящем завершении процесса, полностью зависит от структуры вашего кода. Если такой поток имеет окно и цикл сообщений, то финальный обработчик может использовать функцию SendMessage, для того чтобы послать ему определенное в приложении сообщение (код сообщения должен быть >= 400h - WM_USER), чтобы он мог завершиться корректно. Если у потока нет ни одного окна и соответственно цикла сообщений, финальный обработчик может установить какую-нибудь общедоступную переменную играющую роль флажка, которая время от времени опрашивается другими потоками. В качестве альтернативы, вы можете использовать функцию SetThreadContext, чтобы принудительно заставить поток выполнить некоторый код завершения, путем засылки в регистр EIP адреса начала этого кода. Этот метод не сработал бы, если поток находился бы, в тот момент, в какой-либо ожидающей API-функции, например, ждал бы возврата управления от функции GetMessage. В этом случае, вам нужно послать сообщение для того, чтобы быть полностью уверенным, что система вернула потоку управление, и что новый контекст был установлен;
  7. Функция RaiseException будет работать только в вызвавшем ее потоке, так что она не может использоваться для того, чтобы связаться с другим потоком с целью заставить его выполнить код его собственного обработчика исключений;
  8. Каким образом финальный обработчик может узнать о том, что он может продолжать свою работу после того, как другие потоки были проинформированы о возникшей ситуации? Функция SendMessage не вернет управление, пока поток-получатель не вернется из своей оконной процедуры, и, следовательно, финальный обработчик будет этого ждать. В качестве альтернативы, финальный обработчик может опрашивать флажок, ожидая ответа от другого потока означающего что, этот поток закончил очистку (обратите внимание, что в цикле опроса вы должны вызывать API-функцию Sleep, чтобы избежать злоупотребления выделяемым системным временем). Или еще лучше, если финальный обработчик будет ждать, завершения другого потока (это может быть реализовано с помощью API-функции WaitForSingleObject или WaitForMultipleObjects, если есть несколько таких потоков). В качестве альтернативы можно использовать API-функции для работы с объектом Событие или Семафор;
  9. Для примера того, как эти процедуры могли бы работать на практике предположим, что имеется рабочий поток, задачей которого является реорганизация базы данных, и последующая запись ее на диск. Эта задача может быть уже наполовину выполнена, когда в первичном потоке возникает исключение, которое активизирует ваш финальный обработчик. Здесь вы могли или заставить вторичный поток прерывать выполнение его задания, вызвать свою раскрутку, и корректно завершиться, откатившись на первоначальные данные на диске, или же вы могли бы разрешить ему завершать свою задачу, по окончании которой он бы дал знать об этом обработчику, чтобы обработчик мог вернуться в систему. После вызова вашего обработчика, вы были бы должны прекратить прием рабочим потоком дальнейших подобных заданий. Это может быть достигнуто обработчиком, путем установки флажка, который проверяется рабочим потоком перед тем, как он начнет выполнение любого задания, или используя API-функции для работы с объектом Событие;

Если связь между потоками затруднена, есть другой способ, заключающийся в том, что один поток может получить доступ к стеку другого потока, и таким образом вызвать его раскрутку. Этот способ использует тот факт, что, хотя каждый поток имеет свой собственный стек, память, зарезервированная для этого стека, находится непосредственно в пределах адресного пространства процесса. Вы можете сами проверить это, если понаблюдаете за функционированием многопоточного приложения с помощью отладчика. Когда вы будете переключаться между потоками, значения регистров ESP и EBP будут изменяться, но адреса указанные в них, в любом случае, будут находиться в пределах адресного пространства процесса. Значение регистра FS, в разных потоках, также будет отличаться, и указывать на поточно-зависимый TIB. Для того, чтобы один поток смог обратиться к стеку другого, и вызвать его раскрутку, он должен выполнить следующие шаги:

a) После создания, каждый поток записывает в сопоставленную с ним статическую переменную значение его регистра FS;

b) Перед завершением, каждый поток обнуляет значение, сопоставленной с ним, статической переменной;

с) Обработчик, который должен раскрутить другие потоки, должен в свою очередь взять все эти статические переменные, и для тех, которые имеют ненулевое значение (т.е. потоки, за которыми они закреплены, на момент возникновения исключения - выполнялись), вызвать закрепленные за ними обработчики потоков с флагом исключения 2 (EH_UNWINDING), при этом, пользовательский флажок должен иметь значение 400h, чтобы сигнализировать, что внутрипоточный обработчик вызван из вашего финального обработчика. Вы не можете вызвать внутрипоточный обработчик из другого потока, используя функцию RtlUnwind (которая является поточно-зависимой), но это может быть сделано, если использовать следующий код (где регистр EBX содержит адрес структуры EXCEPTION_RECORD):

MOV D[EBX+4],402h  ;Устанавливает значение в exception flag на EH_UNWINDING + 400h.
L1: 
PUSH ES 
MOV AX,FS_VALUE    ;Получаем значение регистра FS из раскручиваемого потока.
MOV ES,AX 
MOV EDI,ES:[0]     ;Получаем адрес первого внутрипоточного обработчика.
POP ES 
L2: 
CMP D[EDI],-1      ;Смотрим, является ли он последним.
JZ >L3             ;Если да, то завершаемся.
PUSH EDI,EBX       ;Сохраняем на стеке адреса структур ERR и EXCEPTION_RECORD.
CALL [EDI+4]       ;Вызываем обработчик для выполнения кода очистки.
ADD ESP,8h         ;Удаляем из стека два, сохраненных там ранее, параметра.
MOV EDI,[EDI]      ;Получаем указатель на следующую структуру ERR
JMP L2             ;и, если она не последняя, обрабатываем ее таким же образом.
L3:                ;Метка кода финиша.
;Выполнить следующую итерацию цикла с переходом на метку: L1, с новым FS_VALUE,
;пока не будут обработаны все нужные потоки. 

ПЗдесь вы видите, как читается информация из TIB каждого не имеющего отношения к возникшему исключению потока посредством использования регистра ES, которому временно присваивают значение, полученное из регистра FS в соответствующем потоке.

Вместо того, чтобы использовать регистр FS для поиска TIB, вы могли бы использовать для этого следующий код, чтобы получить 32-разрядный линейный адрес. В этом коде LDT_ENTRY - структура из двух двойных слов, значение в регистре AX считается 16-разрядным значением селектора (FS_VALUE) которое будет преобразовано, и hThread - это любой валидный описатель (хэндл) потока:

AND EAX, 0FFFFh 
PUSH OFFSET LDT_ENTRY, EAX, hThread 
CALL GetThreadSelectorEntry 
OR EAX, EAX                 ;Смотрим потерпели ли мы неудачу.
JZ >L300                    ;Если да, то возвращаем нулевое значение.
MOV EAX, OFFSET LDT_ENTRY 
MOV DH, [EAX+7]             ;Получаем старшую часть базы.
MOV DL, [EAX+4]             ;Получаем среднюю часть базы.
SHL EDX, 16D                ;Сдвигаем младшее слово регистра EDX в старшее
MOV DX, [EAX+2]             ;и получаем младшую часть базы.
OR EDX, EDX                 ;Сейчас EDX содержит 32-разрядный линейный адрес.
L300:                       ;В случае успеха, возвращаем ненулевое значение.

Причина того, почему важно (использованием флажка 400h) сообщить вызванному обработчику, что он вызван из другого потока, состоит в том, что вызываемый поток все еще выполняется, т.к. исключение произошло в другом потоке. В этих обстоятельствах, обработчик вполне может захотеть приостановить поток для того, чтобы задача очистки могла быть возложена на вызываемый поток. Тогда, перед вызовом функции ResumeThread, не имеющий отношения к возникшему исключению поток направили бы в безопасное место. Все это должно быть сделано, перед тем как финальный обработчик вернется в систему, потому что по возвращению система, применив грубую силу, просто завершит все потоки принадлежащие процессу.

* Thanks to G.W.Wilhelm, Jr of IBM for this idea

10. Демонстрационная программа Except.exe

Except.exe - это демонстрационная программа предназначенная для того, чтобы показать на практике все представленные выше объяснения по обработке исключений, для программистов на ассемблере.

Также представлен исходный код для Except.exe (файлы Except.asm и Except.rc). Есть модальный диалог в качестве основного окна, и, установленный на ранней стадии, финальный обработчик исключений. После нажатия кнопки: "Cause Exception", вызывается диалоговая процедура для обработки соответствующего сообщения, далее из нее вызывается вторая процедура, а из второй вызывается третья, причем третья процедура провоцирует исключение, имеющее тот тип, который выбран переключателями в диалоговом окне. Поскольку выполнение проходит через код всех трех процедур, создаются три внутрипоточных обработчика исключений.

Рис. 3

Причина, породившая исключение или устраняется на месте, если это возможно, или программа оправляется в выбранном обработчике, начиная выполняться с безопасного места. Вы можете сделать одно из двух: выйти, нажав F3 или F5, или нажав F7, заставить финальный обработчик попытаться помочь программе оправиться после исключения.

Вы можете следить за событиями, по мере их возникновения, по сообщениям от обработчиков отображаемых в окне списка. Между сообщениями делается небольшая задержка для того, чтобы вам было легче заметить ту последовательность, в которой они происходят. Для того, чтобы вспомнить их последовательность вы можете просто пролистать список сообщений.

Перед самым завершением программы происходит кое-что интересное. Система вызывает финальную раскрутку с установленным в exception flag значением 2h. Сообщения, посланные окну списка, еще более замедляются, потому что программа скоро должна завершится!

Вы увидите, что тот же самый тип раскрутки, происходит в том случае, если вы укажете, что выполнение должно продолжиться в "безопасном месте" или, если нажмете F7, в финальном обработчике. Эта раскрутка, инициализируется самим обработчиком.

Copyright © Джереми Гордон 1996-2000

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

перевод Oleg_SK (2005).

2002-2013 (c) wasm.ru