Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf792 Часть V. Структурная обработка исключений
EXCEPTION_RECORD содержит информацию об исключении, независимую от типа процессора, а CONTEXT — машинно-зависимую информацию об этом исключении. В структуре EXCEPTION_POWTERS всего два элемента — указатели на помещенные в стек структуры EXCEPTION_ RECORD и CONTEXT:
typedef Struct _EXCEPTION_P0INTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord;
} EXCEPTION_P0INTERS, *PEXCEPTION_POINTERS;
Чтобы получить эту информацию и использовать ее в программе, вызовите
GetExceptionInformation:
PEXCEPTION_POINTERS GetExceptionInformation();
PEXCEPTION_POINTERS GetExceptionInfornation();
Эта встраиваемая функция возвращает указатель на структуру EXCEPTION_POINTERS.
Самое важное в GetExceptionInformation то, что ее можно вызывать только в фильтре исключений и больше. Нигде, потому что структуры CONTEXT, EXCEPTION_RECORD и EXCEPTION_POINTERS существуют лишь во время обра-
ботки фильтра исключений. Когда управление переходит к обработчику исключений, эти данные в стеке разрушаются.
Если вам нужно получить доступ к информации об исключении из обработчика — сохраните структуру EXCEPTION_RECORD и/или CONTEXT (на которые указывают элементы структуры EXCEPTION_POINTERS) в объявленных вами переменных. Вот пример сохранения этих структур:
void FuncSkunk() {
//объявляем переменные, которые мы сможем потом использовать
//для сохранения информации об исключении (если оно произойдет)
EXCEPTION_RECORD SavedExceptRec; CONTEXT SavedContext;
…
__try {
…
}
__except ( SavedExceptRec =
*(GetExceptionInformation())->ExceptionRecord, SavedContext =
*(GetExceptionInformation())->ContextRecord, EXCEPTION_EXECUTE_HANDLER) {
//мы можем теперь использовать переменные SavedExceptRec
//и SavedContext в блоке обработчика исключений
Глава 24. Фильтры и обработчики исключений.docx 793
switch (SavedExceptRec.ExceptionCode) {
…
}
}
}
Вфильтре исключений применяется оператор-запятая (,) — мало кто из программистов знает о нем. Он указывает компилятору, что выражения, отделенные запятыми, следует выполнять слева направо. После вычисления всех выражений возвращается результат последнего из них — крайнего справа.
ВFuncSkunk сначала вычисляется выражение слева, что приводит к сохранению находящейся в стеке структуры EXCEPTION_RECORD в локальной переменной SavedExceptRec. Результат этого выражения является значением SavedExceptRec. Но он отбрасывается, и вычисляется выражение, расположенное правее. Это приводит к сохранению размещенной в стеке структуры CONTEXT в локальной переменной SavedContext. И снова результат — значение SavedContext — отбрасывается, и вычисляется третье выражение. Оно равно EXCEPTION_EXECUTE_HANDLER — это и будет результатом всего выражения в скобках.
Так как фильтр возвращает EXCEPTION_EXECUTE_HANDLER, выполняется код в блоке except. К этому моменту переменные SavedExceptRec и SavedContext уже инициализированы, и их можно использовать в данном блоке. Важно, чтобы переменные SavedExceptRec и SavedContext были объявлены вне блока try.
Вероятно, вы уже догадались, что элемент ExceptionRecord структуры EXCEPTION_POINTERS указывает на структуру EXCEPTION_RECORD:
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
Структура EXCEPTION_RECORD содержит подробную машиннонезависимую информацию о последнем исключении. Вот что представляют собой ее элементы.
■ExceptionCode — код исключения. Это информация, возвращаемая функцией
GetExceptionCode.
■ExceptionMags — флаги исключения. На данный момент определено только два значения: 0 (возобновляемое исключение) и EXCEPTION_NONCONTINUABLE (невозобновляемое исключение). Любая попытка возоб-
794 Часть V. Структурная обработка исключений
новить работу программы после невозобновляемого исключения генерирует исключение EXCEPTION_NONCONTINUABLE_EXCEPTION.
■ExceptionRecord — указатель на структуру EXCEPTION_RECORD, содержащую информацию о другом необработанном исключении. При обработке одного исключения может возникнуть другое. Например, код внутри фильтра исключений может попытаться выполнить деление на нуль. Когда возникает серия вложенных исключений, записи с информацией о них могут образовывать связанный список. Исключение будет вложенным, если оно генерируется при обработке фильтра. В отсутствие необработанных исключений ExceptionRecord равен NULL.
■ExceptionAddress — адрес машинной команды, при выполнении которой произошло исключение.
■NumberParameters — количество параметров, связанных с исключением (0-15). Это число заполненных элементов в Массиве ExceptionInformation. Почти для всех исключений значение этого элемента равно 0.
■ExceptionInformation — массив дополнительных аргументов, описывающих исключение. Почти для всех исключений элементы этого массива не определены.
Последние два элемента структуры EXCEPTION_RECORD сообщают фильтру дополнительную информацию об исключении. Сейчас такую информацию дает только один тип исключений: EXCEPTION_ACCESS_VIOLATION. Все остальные дают нулевое значение в элементе NumberParameters. Проверив его, вы узнаете, надо ли просматривать массив ExceptionInformation.
При исключении EXCEPTION_ACCESS_VIOLATION элемент ExceptionInformation[0] содержит флаг, указывающий тип операции, которая вызвала нарушение доступа. Если его значение равно 0, поток пытался читать недоступные ему данные; 1 — записывать данные по недоступному ему адресу. Элемент ExceptionInformation[1] определяет адрес недоступных данных. Когда защитный механизм Data Execution Prevention (DEP) обнаруживает попытку исполнения кода, хранящегося в странице, для которой исполнение запрещено, генерируется исключение и в ExceptionInformation[0] записывается 2 для IA-64 и 8 в противном случае.
Эта структура позволяет писать фильтры исключений, сообщающие значительный объем информации о работе программы. Можно создать, например, такой фильтр:
_try {
…
}
__except(ExpFltr(GetExceptionInformation())) {
…
}
Глава 24. Фильтры и обработчики исключений.docx 795
LONG ExpFltr (LPEXCEPTION_POINTERS pep) {
TCHAR szBufl300], *p;
PEXCEPTION_RECORD pER = pep->ExceptionRecord;
DWORD dwExceptionCode = pER->ExceptionCode;
StringCchPrintf(szBuf, _countof(szBuf), TEXT("Code = %x, Address = %p"),
dwExceptionCode, pER->ExceptionAddress);
// находим конец строки
p = _tcschr(szBuf, TEXT('0‟));
//я использовал оператор switch на тот случай, если Майкрософт
//в будущем добавит информацию для других исключений
switch (dwExceptionCode) {
case EXCEPTION_ACCESS_VIOLATION: StringCchPrintf(p, _countof(szBuf),
TEXT("\n Attempt to %s data at address %p"), pER->ExceptionInformation[0] ? TEXT("write") : TEXT("read"), pER->ExceptionInformation[1]);
break;
default:
break;
}
MessageBox(NULL, szBuf, TEXT("Exception"), MB_OK | MB_ICONEXCLAMATION);
return(EXCEPTION_CONTINUE_SEARCH);
}
Элемент ContextRecord структуры EXCEPTION_POTNTERS указывает на структуру CONTEXT (см. главу 7), содержимое которой зависит от типа процессора.
С помощью этой структуры, в основном содержащей по одному элементу для каждого регистра процессора, можно получить дополнительную информацию о возникшем исключении. Увы, это потребует написания машинно-зависимого кода, способного распознавать тип процессора и использовать подходящую для него структуру CONTEXT. При этом вам придется включить в код набор директив #ifdef для разных типов процессоров. Структуры CONTEXT для различных процессоров, поддерживаемых Windows, определены в заголовочном файле WinNT.h.
Программные исключения
До сих пор мы рассматривали обработку аппаратных исключений, когда процессор перехватывает некое событие и возбуждает исключение. Но вы можете и сами генерировать исключения. Это еще один способ для функции
796 Часть V. Структурная обработка исключений
сообщить о неудаче вызвавшему ее коду Традиционно функции, которые могут закончиться неудачно, возвращают некое особое значение — признак ошибки. При этом предполагается, что код, вызвавший функцию, проверяет, не вернула ли она это особое значение, и, если да, выполняет какие-то альтернативные операции. Как правило, вызывающая функция проводит в таких случаях соответствующую очистку и в свою очередь тоже возвращает код ошибки. Подобная передача кодов ошибок по цепочке вызовов резко усложняет написание и сопровождение кода.
Альтернативный подход заключается в том, что при неудачном вызове функции возбуждают исключения. Тогда написание и сопровождение кода становится гораздо проще, а программы работают намного быстрее. Последнее связано с тем, что та часть кода, которая отвечает за контроль ошибок, вступает в действие лишь при сбоях, т. е. в исключительных ситуациях.
К сожалению, большинство разработчиков не привыкло пользоваться исключениями для обработки ошибок. На то есть две причины. Во-первых, многие просто не знакомы с SEH. Если один разработчик создаст функцию, которая генерирует исключение, а другой не сумеет написать SEH-фрейм для перехвата этого исключения, его приложение при неудачном вызове функции будет завершено операционной системой.
Вторая причина, по которой разработчики избегают пользоваться SEH, — невозможность его переноса на другие операционные системы. Ведь компании нередко выпускают программные продукты, рассчитанные на несколько операционных систем, и, естественно, предпочитают работать с одной базой исходного кода для каждого продукта. А структурная обработка исключений — это технология, специфичная для Windows.
Если вы все же решились на уведомление об ошибках через исключения, я аплодирую этому решению и пишу этот раздел специально для вас. Давайте для начала посмотрим на семейство Heap-функций (HeapCreate, HeapAlloc и т. д.) Наверное, вы помните из главы 18, что они предлагают разработчику возможность выбора. Обычно, когда их вызовы заканчиваются неудачно, они возвращают NULL, сообщая об ошибке. Но вы можете передать флаг HEAP_GENERATE_EXCEPTIONS, и тогда при неудачном вызове Heap-функция не станет возвращать NULL; вместо этого она возбудит программное исключение STATUS_NO_MEMORY, перехватываемое с помощью SEH-фрейма.
Чтобы использовать это исключение, напишите код блока try так, будто выделение памяти всегда будет успешным; затем — в случае ошибки при выполнении данной операции — вы сможете либо обработать исключение в блоке except, либо заставить функцию провести очистку, дополнив блок try блоком finally. Очень удобно!
Программные исключения перехватываются точно так же, как и аппаратные. Иначе говоря, все, что я рассказывал об аппаратных исключениях, в полной мере относится и к программным исключениям.
В этом разделе основное внимание мы уделим тому, как возбуждать программные исключения в функциях при неудачных вызовах. В сущности, вы
Глава 24. Фильтры и обработчики исключений.docx 797
можете реализовать свои функции по аналогии с Heap-функциями: пусть вызывающий их код передает специальный флаг, который сообщает функциям способ уведомления об ошибках.
Возбудить программное исключение несложно — достаточно вызвать функ-
цию RaiseException:
VOID RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DW0RD nNumberOfArguments,
CONST ULONG_PTR *pArguments);
Ее первый параметр, dwExceptionCode, — значение, которое идентифицирует генерируемое исключение. HeapAlloc передает в нем STATUS_NO_MEMORY. Если вы определяете собственные идентификаторы исключений, придерживайтесь формата, применяемого для стандартных кодов ошибок в Windows (файл WinError.h). Не забудьте, что каждый такой код представляет собой значение типа DWORD; его поля описаны в таблице 24-1. Определяя собственные коды исключений, заполните все пять его полей:
■биты 31 и 30 должны содержать код степени «тяжести»;
■бит 29 устанавливается в 1 (0 зарезервирован для исключений, определяемых Майкрософт, вроде STATUS_NO_MEMORY для HeapAlloc);
■бит 28 должен быть равен 0;
■биты 27-16 должны указывать один из кодов подсистемы, предопределенных Майкрософт;
■биты 15-0 могут содержать произвольное значение, идентифицирующее ту часть вашего приложения, которая возбуждает исключение.
Второй параметр функции RaiseException — dwExceptionFlags — должен быть либо 0, либо EXCEPTION_NONCONTINUABLE. В принципе этот флаг указывает, может ли фильтр исключений вернуть EXCEPTION_CONTINUE_EXECUTION в ответ на данное исключение. Если вы передаете в этом параметре нулевое значение, фильтр может вернуть EXCEPTION_CONTINUE_EXECUTION. В нормальной ситуации это заставило бы поток снова выполнить машинную команду, вызвавшую программное исключение. Однако Майкрософт пошла на некоторые ухищрения, и поток возобновляет выполнение с оператора, следующего за вызовом RaiseException.
Но, передав функции RaiseException флаг EXCEPTION_NONCONTINUABLE,
вы сообщаете системе, что возобновить выполнение после данного исключения нельзя. Операционная система использует этот флаг, сигнализируя о критических (фатальных) ошибках. Например, HeapAlloc устанавливает этот флаг при возбуждении программного исключения STATUS_NO_MEMORY, чтобы указать системе: выполнение продолжить нельзя. Ведь если вся память занята, выделить в ней новый блок и продолжить выполнение программы не удастся.
798 Часть V. Структурная обработка исключений
Если возбуждается исключение EXCEPTION_NONCONTINUABLE, а фильтр все же возвращает EXCEPTION_CONTINUE_EXECUTION, система генерирует новое исключение EXCEPTION_NONCONTINUABLE_EXCEPTION.
При обработке программой одного исключения вполне вероятно возбуждение нового исключения. И смысл в этом есть. Раз уж мы остановились на этом месте, замечу, что нарушение доступа к памяти возможно и в блоке finally, и в фильтре исключений, и в обработчике исключений. Когда происходит нечто подобное, система создает список исключений. Помните функцию GetExceptionInformation? Она возвращает адрес структуры EXCEPTION_POINTERS. Ее элемент ExceptionRecord указывает на структуру EXCEPTION_RECORD, которая в свою очередь тоже содержит элемент ExceptionRecord. Он указывает на другую структуру EXCEPTION_RECORD, где содержится информация о предыдущем исключении.
Обычно система единовременно обрабатывает только одно исключение, и элемент ExceptionRecord равен NULL. Но если исключение возбуждается при обработке другого исключения, то в первую структуру EXCEPTION_RECORD помещается информация о последнем исключении, а ее элемент ExceptionRecord указывает на аналогичную структуру с аналогичными данными о предыдущем исключении. Если есть и другие необработанные исключения, можно продолжить просмотр этого связанного списка структур EXCEPTION_RECORD, чтобы определить, как обработать конкретное исключение.
Третий и четвертый параметры (nNumberOfArguments и pArguments) функции RaiseException позволяют передать дополнительные данные о генерируемом исключении. Обычно это не нужно, и в pArguments передается NULL; тогда RaiseException игнорирует параметр nNumberOfArguments. А если вы передаете дополнительные аргументы, nNumberOfArguments должен содержать число элементов в массиве типа ULONG_PTR, на который указывает pArguments. Значение nNumberOfArguments не может быть больше EXCEPTION_MAXIMUM_PARAMETERS (в файле WinNT.h этот идентификатор опре-
делен равным 15).
При обработке исключения написанный вами фильтр — чтобы узнать значе-
ния nNumberOfArguments и pArguments — может ссылаться на элементы NumberParameters и ExceptionInformation структуры EXCEPTION_RECORD.
Собственные программные исключения генерируют в приложениях по целому ряду причин. Например, чтобы посылать информационные сообщения в системный журнал событий. Как только какая-нибудь функция в вашей программе столкнется с той или иной проблемой, вы можете вызвать RaiseException; при этом обработчик исключений следует разместить выше по дереву вызовов, тогда — в зависимости от типа исключения — он будет либо заносить его в журнал событий, либо сообщать о нем пользователю. Вполне допустимо возбуждать программные исключения и для уведомления о внутренних фатальных ошибках в приложении.
Оглавление |
|
Г Л А В А 2 5 Необработанные исключения, векторная обработка исключений и |
|
исключения C++ ........................................................................................................................... |
799 |
Как работает функция UnhandledExceptionFilter................................................................. |
802 |
Взаимодействие UnhandledExceptionFilter с WER................................................... |
805 |
Отладка по запросу ................................................................................................................. |
808 |
Программа-пример Spreadsheet............................................................................................ |
811 |
Векторная обработка исключений и обработчики возобновления................................ |
823 |
Исключения C++ и структурные исключения..................................................................... |
825 |
Исключения и отладчик.......................................................................................................... |
827 |
Г Л А В А 2 5
Необработанные исключения, векторная обработка исключений и исключения C++
В предыдущей главе мы обсудили, что происходит, когда фильтр возвращает значение EXCEPTION_CONTINUE_SEARCH. Оно заставляет систему искать дополнительные фильтры исключений, продвигаясь вверх по дереву вызовов. А что будет, если все фильтры вернут EXCEPTION_CONTINUE_SEARCH? Тогда мы по-
лучим необработанное исключение (unhandled exception).
Windows поддерживает функцию SetUnhandledExceptionFilter, которая дает последний шанс на обработку исключения, прежде чем Windows объявит его необработанным:
PTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
PTOP_LEVEL_EXCEPTION_FILTER pTopLevelExceptionFilter);
Как правило, эта функция вызывается во время инициализации процесса. В результате при возникновении необработанного исключения в любом из потоков вашего процесса будет вызвана функция фильтра верхнего уровня, заданная параметром SetUnhandledExceptionFilter. Прототип функции фильтра выглядит следующим образом:
LONG WINAPI TopLevelUnhandledExceptionFilter(PEXCEPTION_POINTERS pException Info);
Функция фильтра может выполнять любые нужные вам действия, но возвращать она должна один из трех идентификаторов EXCEPTION_*, перечисленных в табл. 25-1. Учите, что состояние процесса при вызове этой функции может быть повреждено из-за переполнения стека, а также синхронизирующих объектов или блоков памяти в куче, оставшихся занятыми.