Издание четвертое windows ® для профессионалов создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows



Pdf просмотр
страница61/68
Дата28.11.2016
Размер3.57 Mb.
Просмотров12596
Скачиваний0
1   ...   57   58   59   60   61   62   63   64   ...   68
Ч АС Т Ь V
СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ
Context. И снова результат — значение SavedContext — отбрасывается, и вычисляется третье выражение. Оно равно EXCEPTION_EXECUTE_HANDLER — это и будет результатом всего выражения в скобках.
Так как фильтр возвращает EXCEPTION_EXECUTE_HANDLER, выполняется код в блоке
except. К этому моменту переменные SavedExceptRec и SavedContext уже инициализированы, и их можно использовать в данном блоке. Важно, чтобы переменные
SavedExceptRec и SavedContext были объявлены вне блока try.
Вероятно, Вы уже догадались, что элемент
ExceptionRecord структуры EXCEP
TION_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 содержит подробную машинно независимую информацию о последнем исключении. Вот что представляют собой ее элементы.
í
ExceptionCode — код исключения. Это информация, возвращаемая функцией
GetExceptionCode.
í
ExceptionFlags — флаги исключения. На данный момент определено только два значения 0 (возобновляемое исключение) и EXCEPTION_NONCONTINUABLE
(невозобновляемое исключение. Любая попытка возобновить работу программы после невозобновляемого исключения генерирует исключение EXCEP
TION_NONCONTINUABLE_EXCEPTION.
í
ExceptionRecord — указатель на структуру EXCEPTION_RECORD, содержащую информацию о другом необработанном исключении. При обработке одного исключения может возникнуть другое. Например, код внутри фильтра исключений может попытаться выполнить деление на нуль. Когда возникает серия вложенных исключений, записи с информацией о них могут образовывать связанный список. Исключение будет вложенным, если оно генерируется при обработке фильтра. В отсутствие необработанных исключений
ExceptionRecord
равен NULL.
í
ExceptionAddress — адрес машинной команды, при выполнении которой произошло исключение.
í
NumberParameters — количество параметров, связанных с исключением (Это число заполненных элементов в массиве
ExceptionInformation. Почти для всех исключений значение этого элемента равно 0.
í
ExceptionInformation — массив дополнительных аргументов, описывающих исключение. Почти для всех исключений элементы этого массива не определены.
Последние два элемента структуры EXCEPTION_RECORD сообщают фильтру дополнительную информацию об исключении. Сейчас такую информацию дает только один тип исключений EXCEPTION_ACCESS_VIOLATION. Все остальные дают нулевое значение в элементе
NumberParameters. Проверив его, Вы узнаете, надо ли просматривать массив
ExceptionInformation.

619
Г ЛАВА 24
Фильтры и обработчики исключений
При исключении EXCEPTION_ACCESS_VIOLATION элемент
ExceptionInformation[0]
содержит флаг, указывающий тип операции, которая вызвала нарушение доступа. Если его значение равно 0, поток пытался читать недоступные ему данные 1 — записывать данные по недоступному ему адресу. Элемент
ExceptionInformation[1] определяет адрес недоступных данных.
Эта структура позволяет писать фильтры исключений, сообщающие значительный объем информации о работе программы. Можно создать, например, такой фильтр {
M
}
__except (ExpFltr(GetExceptionInformation() >ExceptionRecord)) {
M
}
LONG ExpFltr(PEXCEPTION_RECORD pER) {
char szBuf[300], *p;
DWORD dwExceptionCode = pER >ExceptionCode;
sprintf(szBuf, "Code = %x, Address = %p",
dwExceptionCode, pER >ExceptionAddress);
// находим конец строки p = strchr(szBuf, 0);
// я использовал оператор switch на тот случай, если Microsoft
// в будущем добавит информацию для других исключений switch (dwExceptionCode) {
case EXCEPTION_ACCESS_VIOLATION:
sprintf(p, "Attempt to %s data at address %p",
pER >ExceptionInformation[0] ? "write" : "read",
pER >ExceptionInformation[1]);
break;
default;
break;
}
MessageBox(NULL, szBuf, "Exception", MB_OK | Элемент
ContextRecord структуры EXCEPTION_POINTERS указывает на структуру (см. главу 7), содержимое которой зависит от типа процессора.
С помощью этой структуры, в основном содержащей по одному элементу для каждого регистра процессора, можно получить дополнительную информацию о возникшем исключении. Увы, это потребует написания машинно зависимого кода, способного распознавать тип процессора и использовать подходящую для него структуру. При этом Вам придется включить в код набор директив
#ifdef для разных типов процессоров. Структуры CONTEXT для различных процессоров, поддерживаемых, определены в заголовочном файле WinNT.h.

620
Ч АС Т Ь V
СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ
Программные исключения
До сих пор мы рассматривали обработку аппаратных исключений, когда процессор перехватывает некое событие и возбуждает исключение. Но Вы можете и сами генерировать исключения. Это еще один способ для функции сообщить о неудаче вызвавшему ее коду. Традиционно функции, которые могут закончиться неудачно, возвращают некое особое значение — признак ошибки. При этом предполагается, что код, вызвавший функцию, проверяет, не вернула ли она это особое значение, и, если да, выполняет какие то альтернативные операции. Как правило, вызывающая функция проводит в таких случаях соответствующую очистку ив свою очередь тоже возвращает код ошибки. Подобная передача кодов ошибок по цепочке вызовов резко усложняет написание и сопровождение кода.
Альтернативный подход заключается в том, что при неудачном вызове функции возбуждают исключения. Тогда написание и сопровождение кода становится гораздо проще, а программы работают намного быстрее. Последнее связано стем, что та часть кода, которая отвечает за контроль ошибок, вступает в действие лишь при сбоях, т. е.
в исключительных ситуациях.
К сожалению, большинство разработчиков не привыкло пользоваться исключениями для обработки ошибок. На то есть две причины. Во первых, многие просто незнакомы с SEH. Если один разработчик создаст функцию, которая генерирует исключение, а другой не сумеет написать SEH фрейм для перехвата этого исключения, его приложение при неудачном вызове функции будет завершено операционной системой.
Вторая причина, по которой разработчики избегают пользоваться SEH, — невозможность его переноса на другие операционные системы. Ведь компании нередко выпускают программные продукты, рассчитанные на несколько операционных систем, и, естественно, предпочитают работать с одной базой исходного кода для каждого продукта. А структурная обработка исключений — это технология, специфичная для Если Вы все же решились на уведомление об ошибках через исключения, я аппло дирую этому решению и пишу этот раздел специально для Вас. Давайте для начала посмотрим на семейство
Heap функций (HeapCreate, HeapAlloc и т. д. Наверное, Вы помните из главы 18, что они предлагают разработчику возможность выбора. Обычно, когда их вызовы заканчиваются неудачно, они возвращают NULL, сообщая об ошибке. Но Вы можете передать флаги тогда при неудачном вызове
Heap функция не станет возвращать NULL; вместо этого она возбудит программное исключение STATUS_NO_MEMORY, перехватываемое с помощью фрейма.
Чтобы использовать это исключение, напишите код блока
try так, будто выделение памяти всегда будет успешным затем — в случае ошибки при выполнении данной операции — Вы сможете либо обработать исключение в блоке
except, либо заставить функцию провести очистку, дополнив блок
try блоком finally. Очень удобно!
Программные исключения перехватываются точно также, как и аппаратные. Иначе говоря, все, что я рассказывал об аппаратных исключениях, в полной мере относится и к программным исключениям.
В этом разделе основное внимание мы уделим тому, как возбуждать программные исключения в функциях при неудачных вызовах. В сущности, Вы можете реализовать свои функции по аналогии с
Heap функциями: пусть вызывающий их код передает специальный флаг, который сообщает функциям способ уведомления об ошибках.

621
Г ЛАВА 24
Фильтры и обработчики исключений
Возбудить программное исключение несложно — достаточно вызвать функцию
RaiseException:
VOID RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
CONST ULONG_PTR Ее первый параметр,
dwExceptionCode, — значение, которое идентифицирует генерируемое исключение.
HeapAlloc передает в нем STATUS_NO_MEMORY. Если Вы определяете собственные идентификаторы исключений, придерживайтесь формата,
применяемого для стандартных кодов ошибок в Windows (файл WinError.h). Не забудьте, что каждый такой код представляет собой значение типа DWORD; его поля описаны в таблице 24 1. Определяя собственные коды исключений, заполните все пять его полей:
í
биты 31 и 30 должны содержать код степени «тяжести»;
í
бит 29 устанавливается в 1 (0 зарезервирован для исключений, определяемых, вроде STATUS_NO_MEMORY для
HeapAlloc);
í
бит 28 должен быть равен биты 27–16 должны указывать один из кодов подсистемы, предопределенных
Microsoft;
í
биты 15–0 могут содержать произвольное значение, идентифицирующее ту часть Вашего приложения, которая возбуждает исключение.
Второй параметр функции
RaiseExceptiondwExceptionFlags — должен быть либо 0, либо EXCEPTION_NONCONTINUABLE. В принципе этот флаг указывает, может ли фильтр исключений вернуть EXCEPTION_CONTINUE_EXECUTION в ответ на данное исключение. Если Вы передаете в этом параметре нулевое значение, фильтр может вернуть EXCEPTION_CONTINUE_EXECUTION. В нормальной ситуации это заставило бы поток снова выполнить машинную команду, вызвавшую программное исключение. Однако Microsoft пошла на некоторые ухищрения, и поток возобновляет выполнение с оператора, следующего за вызовом
RaiseException.
Но, передав функции
RaiseException флаг EXCEPTION_NONCONTINUABLE, Вы сообщаете системе, что возобновить выполнение после данного исключения нельзя.
Операционная система использует этот флаг, сигнализируя о критических (фатальных) ошибках. Например,
HeapAlloc устанавливает этот флаг при возбуждении программного исключения STATUS_NO_MEMORY, чтобы указать системе выполнение продолжить нельзя. Ведь если вся память занята, выделить в ней новый блоки продолжить выполнение программы не удастся.
Если возбуждается исключение EXCEPTION_NONCONTINUABLE, а фильтр все же возвращает EXCEPTION_CONTINUE_EXECUTION, система генерирует новое исключение При обработке программой одного исключения вполне вероятно возбуждение нового исключения. И смысл в этом есть. Раз уж мы остановились на этом месте, замечу, что нарушение доступа к памяти возможно ив блоке
finally, ив фильтре исключений, ив обработчике исключений. Когда происходит нечто подобное, система создает список исключений. Помните функцию
GetExceptionInformation? Она возвращает адрес структуры EXCEPTION_POINTERS. Ее элемент
ExceptionRecord указывает на структуру, которая в свою очередь тоже содержит элемент
Exception

622
Ч АС Т Ь V
СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ
Record. Он указывает на другую структуру EXCEPTION_RECORD, где содержится информация о предыдущем исключении.
Обычно система единовременно обрабатывает только одно исключение, и элемент равен NULL. Но если исключение возбуждается при обработке другого исключения, тов первую структуру EXCEPTION_RECORD помещается информация о последнем исключении, а ее элемент
ExceptionRecord указывает на аналогичную структуру с аналогичными данными о предыдущем исключении. Если есть и другие необработанные исключения, можно продолжить просмотр этого связанного списка структур EXCEPTION_RECORD, чтобы определить, как обработать конкретное исключение.
Третий и четвертый параметры (
nNumberOfArguments и pArguments) функции Raise
Exception позволяют передать дополнительные данные о генерируемом исключении.
Обычно это ненужно, ив передается NULL; тогда RaiseException игнорирует параметр
nNumberOfArguments. А если Вы передаете дополнительные аргументы,
nNumberOfArguments должен содержать число элементов в массиве типа на который указывает
pArguments. Значение nNumberOfArguments не может быть больше (в файле WinNT.h этот идентификатор определен равным При обработке исключения написанный Вами фильтр — чтобы узнать значения
nNumberOfArguments и pArguments — может ссылаться на элементы NumberParameters
и
ExceptionInformation структуры Собственные программные исключения генерируют в приложениях по целому ряду причин. Например, чтобы посылать информационные сообщения в системный журнал событий. Как только какая нибудь функция в Вашей программе столкнется стой или иной проблемой, Вы можете вызвать
RaiseException; при этом обработчик исключений следует разместить выше по дереву вызовов, тогда — в зависимости от типа исключения — он будет либо заносить его в журнал событий, либо сообщать о нем пользователю. Вполне допустимо возбуждать программные исключения и для уведомления о внутренних фатальных ошибках в приложении.

623
Г ЛАВА 5
Необработанные исключения и исключения В предыдущей главе мы обсудили, что происходит, когда фильтр возвращает значение. Оно заставляет систему искать дополнительные фильтры исключений, продвигаясь вверх по дереву вызовов. А что будет, если все фильтры вернут EXCEPTION_CONTINUE_SEARCH? Тогда мы получим
необработанное
исключение (unhandled Как Вы помните из главы 6, выполнение потока начинается с функции
BaseProcess
Start или BaseThreadStart в Kernel32.dll. Единственная разница между этими функциями в том, что первая используется для запуска первичного потока процесса, авто рая — для запуска остальных потоков процесса BaseProcessStart(PPROCESS_START_ROUTINE pfnStartAddr) {
__try {
ExitThread((pfnStartAddr)());
}
__except (UnhandledExceptionFilter(GetExceptionInformation())) {
ExitProcess(GetExceptionCode());
}
// Примечание сюда мы никогда не попадем BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
__try {
ExitThread((pfnStartAddr)(pvParam));
}
__except (UnhandledExceptionFilter(GetExceptionInformation())) {
ExitProcess(GetExceptionCode());
}
// Примечание сюда мы никогда не попадем
}
Обратите внимание, что обе функции содержат SEH фрейм: поток запускается из блока
try. Если поток возбудит исключение, в ответ на которое все Ваши фильтры вернут, будет вызвана особая функция фильтра, предоставляемая операционной системой UnhandledExceptionFilter(PEXCEPTION_POINTERS Она выводит окно, указывающее на то, что поток в процессе вызвал необрабаты ваемое им исключение, и предлагает либо закрыть процесс, либо начать его отладку.
В Windows 98 это окно выглядит следующим образом.

624
Ч АС Т Ь V
СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ
А в Windows 2000 оно имеет другой вид.
В Windows 2000 первая часть текста в этом окне подсказывает тип исключения и адрес вызвавшей его инструкции в адресном пространстве процесса. У меня окно появилось из за нарушения доступа к памяти, поэтому система сообщила адрес, по которому произошла ошибка, и тип доступа к памяти — чтение.
UnhandledException
Filter получает эту информацию из элемента ExceptionInformation структуры EXCEP
TION_RECORD, инициализированной для этого исключения.
В данном окне можно сделать одно из двух. Во первых, щелкнуть кнопку OK, и тогда
UnhandledExceptionFilter вернет EXCEPTION_EXECUTE_HANDLER. Это приведет к глобальной раскрутке и соответственно к выполнению всех имеющихся блоков
finally, а затем и к выполнению обработчика вили. Оба обработчика вызывают
ExitProcess, поэтому то Ваш процесс и закрывается. Причем кодом завершения процесса становится код исключения. Кроме того, процесс закрывается его же потоком, а не операционной системой А это означает, что Вы можете вмешаться вход завершения своего процесса.
Во вторых, Вы можете щелкнуть кнопку Cancel (сбываются самые смелые мечты программистов. В этом случае
UnhandledExceptionFilter попытается запустить отладчики подключить его к процессу. Тогда Вы сможете просматривать состояние глобальных, локальных и статических переменных, расставлять точки прерывания, пе резапускать процесс и вообще делать все, что делается при отладке процесса.
Но самое главное, что сбой в программе можно исследовать в момент его возникновения. В большинстве других операционных систем для отладки процесса сначала запускается отладчик. При генерации исключения в процессе, выполняемом в любой из таких систем, этот процесс надо завершить, запустить отладчики прогнать программу уже под отладчиком. Проблема, правда, в том, что ошибку надо сначала воспроизвести лишь потом можно попытаться ее исправить. А кто знает, какие значения были у переменных, когда Вы впервые заметили ошибку Поэтому найти ее та

625
Г ЛАВА 25
Необработанные исключения и исключения C++
ким способом гораздо труднее. Возможность динамически подключать отладчик к уже запущенному процессу — одно из лучших качеств В этой книге рассматривается разработка приложений, работающих только в пользовательском режиме. Но, наверное, Вас интересует, что происходит, когда необработанное исключение возникает в потоке, выполняемом в режиме ядра. Так вот, исключения в режиме ядра обрабатываются также, как и исключения пользовательского режима. Если низкоуровневая функция для работы с виртуальной памятью возбуждает исключение, система проверяет, есть ли фильтр режима ядра, готовый обработать это исключение. Если такого фильтра нет, оно остается необработанным. В этом случае необработанное исключение окажется в операционной системе или (что вероятнее) в драйвере устройства, а не в приложении. А это уже серьезно!
Так как дальнейшая работа системы после необработанного исключения в режиме ядра небезопасна, Windows не вызывает
UnhandledExceptionFilter. Вместо этого появляется так называемый синий экран смерти экран переключается в текстовый режим, окрашивается в синий фон, выводится информация о модуле, вызвавшем необработанное исключение, и система останавливается. Вам следует записать эту информацию и отправить ее вили поставщику драйвера устройства. Прежде чем продолжить работу, придется перезагрузить машину при этом все несохраненные данные теряются.
Отладка по запросу
Windows позволяет подключать отладчик к любому процессу в любой момент времени эта функциональность называется отладкой по запросу (just in time В этом разделе я расскажу, как она работает. Щелкнув кнопку Cancel, Вы сообщаете функции
UnhandledExceptionFilter о том, что хотите начать отладку процесса.
Для активизации отладчика
UnhandledExceptionFilter просматривает раздел реестра Если Вы установили Visual Studio, то содержащийся в этом разделе параметр Debug ger имеет следующее значение Files\Microsoft Visual Studio\Common\MSDev98\Bin\msdev.exe"
p %ld e В Windows 98 соответствующие значения хранятся не в реестре, а в файле
Win.ini.
Строка, приведенная выше, сообщает системе, какой отладчик надо запустить (в данном случае — MSDev.exe). Естественно, Вы можете изменить это значение, указав другой отладчик
. UnhandledExceptionFilter передает отладчику два параметра в командной строке. Первый — это идентификатор процесса, который нужно отладить, а второй — наследуемое событие со сбросом вручную, которое создается функцией
UnhandledExceptionFilter в занятом состоянии. Отладчик должен распознавать ключи
p и e как идентификатор процесса и описатель события.
Сформировав командную строку из идентификатора процесса и описателя события запускает отладчик вызовом CreateProcess. Отладчик про

626
Ч АС Т Ь V
СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ
веряет аргументы в командной строке и, обнаружив ключ
p, подключается к соответствующему процессу вызовом
DebugActiveProcess:
BOOL DebugActiveProcess(DWORD После этого система начинает уведомлять отладчик о состоянии отлаживаемого процесса, сообщая, например, сколько в нем потоков и какие DLL спроецированы на его адресное пространство. На сбор этих данных отладчику нужно какое то время, в течение которого поток
UnhandledExceptionFilter должен находиться в режиме ожидания. Для этого функция вызывает
WaitForSingleObject и передает описатель созданного ею события со сбросом вручную. Как Вы помните, оно было создано в занятом состоянии, поэтому поток отлаживаемого процесса немедленно приостанавливается и ждет освобождения этого события.
Закончив инициализацию, отладчик вновь проверяет командную строку — на этот раз он ищет ключ
e. Найдя его, отладчик считывает описатель события и вызывает
SetEvent. Он может напрямую использовать этот наследуемый описатель, поскольку процесс отладчика является дочерним по отношению к отлаживаемому процессу,
который и породил его, вызвав
UnhandledExceptionFilter.
Переход события в свободное состояние пробуждает поток отлаживаемого процесса, ион передает отладчику информацию о необработанном исключении. Получив эти данные, отладчик загружает соответствующий файл исходного кода и переходит к команде, которая вызвала исключение. Вот это действительно круто!
Кстати, совсем необязательно дожидаться исключения, чтобы начать отладку.
Отладчик можно подключить в любой момент командой «MSDEV p
PID», где PID идентификатор отлаживаемого процесса. Task Manager в Windows 2000 еще больше упрощает эту задачу. Открыв вкладку Process, Вы можете щелкнуть строку с нужным процессом правой кнопкой мыши и выбрать из контекстного меню команду В ответ Task Manager обратится к только что рассмотренному разделу реестра ивы зовет
CreateProcess, передав ей идентификатор выбранного процесса. Но вместо описателя события Task Manager передаст 0.
Отключение вывода сообщений об исключении
Иногда нужно, чтобы окно с сообщением об исключении не появлялось на экране, например, в готовом программном продукте. Ведь если такое окно появится, пользователь может случайно перейти в режим отладки Вашей программы. Стоит ему только щелкнуть кнопку Cancel, ион шагнет на незнакомую и страшную территорию попадет в отладчик. Поэтому предусмотрено несколько способов, позволяющих избежать появления этого окна на экране.
Принудительное завершение процесса
Запретить функции
UnhandledExceptionFilter вывод окна с сообщением об исключении можно вызовом
SetErrorMode с передачей идентификатора SEM_NOGPFAULT
ERRORBOX:
UINT SetErrorMode(UINT Тогда
UnhandledExceptionFilter, вызванная для обработки исключения, немедленно вернет EXCEPTION_EXECUTE_HANDLER, что приведет к глобальной раскрутке и выполнению обработчика вили, который закроет процесс.

627
Г ЛАВА 25
Необработанные исключения и исключения Лично мне этот способ не нравится, так как пользователь не получает никакого предупреждения — приложение просто исчезает.
Создание оболочки вокруг функции потока
Другой способ состоит в том, что Вы помещаете входную функцию первичного потока или wWinMain) в блок try except. Фильтр исключений должен всегда возвращать EXCEPTION_EXECUTE_HANDLER, чтобы исключение действительно обрабатывалось это предотвратит вызов
UnhandledExceptionFilter.
В обработчике исключений Вы выводите на экран диалоговое окно с какой нибудь диагностической информацией. Пользователь может скопировать эту информацию и передать ее в службу технической поддержки Вашего приложения, что поможет выявить источник проблем. Это диалоговое окно надо разработать так, чтобы пользователь мог завершить приложение, ноне отлаживать.
Этому способу присущ один недостаток он позволяет перехватывать только те исключения, которые возникают в первичном потоке. Если исключение происходит в любом другом потоке процесса, система вызывает функцию
UnhandledExceptionFilter.
Чтобы вывернуться из этой ситуации, придется также включить блоки
try except во входные функции всех вторичных потоков Вашего процесса.



Поделитесь с Вашими друзьями:
1   ...   57   58   59   60   61   62   63   64   ...   68


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал