Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6269
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

820 Часть V. Структурная обработка исключений

// наш глобальный фильтр необработанных исключений для экземпляров этого

класса

static LONG WINAPI UnhandledExceptionFilter(PEXCEPTION_POINTERS pep);

};

///////////////////////////////////////////////////////////////////////////////

//заголовок связанного списка объектов template <class TYPE>

CVMArray<TYPE>* CVMArray<TYPE>::sm_pHead = NULL;

//адрес предыдущего фильтра необработанных исключений template <class TYPE>

PTOP_LEVEL_EXCEPTION_FILTER CVMArray<TYPE>::sm_pfnUnhandledExceptionFilterPrev;

///////////////////////////////////////////////////////////////////////////////

template <class TYPE> CVMArray<TYPE>::CVMArray(DWORD dwReserveElements) {

if (sm_pHead == NULL) {

//устанавливаем наш глобальный фильтр необработанных исключений

//при создании первого экземпляра класса

sm_pfnUnhandledExceptionFilterPrev = SetUnhandledExceptionFilter(UnhandledExceptionFilter);

}

m_pNext = sm_pHead; // следующий узел был вверху списка sm_pHead = this; // сейчас вверху списка находится этот узел

m_cbReserve = sizeof(TYPE) * dwReserveElements;

// резервируем регион для всего массива

m_pArray = (TYPE*) VirtualAlloc(NULL, m_cbReserve, MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE);

chASSERT(m_pArray != NULL);

}

///////////////////////////////////////////////////////////////////////////////

template <class TYPE> CVMArray<TYPE>::~CVMArray() {

// освобождаем регион массива (возвращаем всю переданную ему память)

VirtualFree(m_pArray, 0, MEM_RELEASE);

Глава 25. Необработанные исключения, векторная обработка исключений и исключения

C++.docx 821

// удаляем этот объект из связанного списка

CVMArray* p = sm_pHead;

if (p == this) { // удаляем верхний узел sm_pHead = p->m_pNext;

} else {

BOOL bFound = FALSE;

// проходим по списку сверху и модифицируем указатели

for (; !bFound && (p->m_pNext != NULL); p = p->m_pNext) { if (p->m_pNext == this) {

// узел, указывающий на нас, должен указывать на следующий узел p->m_pNext = p->m_pNext->m_pNext;

break;

}

}

chASSERT(bFound);

}

}

/////////////////////////////////////////////////////////////////////

//предлагаемый по умолчанию механизм обработки нарушений доступа,

//возникающих при попытках передачи физической памяти

template <class TYPE>

LONG CVMArray<TYPE>::OnAccessViolation(PVOID pvAddrTouched,

BOOL bAttemptedRead, PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful) { BOOL bCommittedStorage = FALSE; // считаем, что передать память не удалось

do {

//пытаемся передать физическую память bCommittedStorage = (NULL != VirtualAlloc(pvAddrTouched,

sizeof(TYPE), MEM_COMMIT, PAGE_READWRITE));

//если передать память не удается, предлагаем пользователю освободить

//память

if (!bCommittedStorage && bRetryUntilSuccessful) { MessageBox(NULL,

TEXT("Please close some other applications and Press OK."), TEXT("Insufficient Memory Available"), MB_ICONWARNING | MB_OK);

}

} while (!bCommittedStorage && bRetryUntilSuccessful);

822Часть V. Структурная обработка исключений

//если память передана, пытаемся возобновить выполнение программы;

//в ином случае активизируем обработчик

return(bCommittedStorage

? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_EXECUTE_HANDLER);

}

///////////////////////////////////////////////////////////////////////////////

// фильтр, связываемый с отдельным объектом класса CVMArray template <class TYPE>

LONG CVMArray<TYPE>::ExceptionFilter(PEXCEPTION_POINTERS pep, BOOL bRetryUntilSuccessful) {

//пo умолчанию пытаемся использовать другой фильтр (самый

//безопасный путь)

LONG lDisposition = EXCEPTION_CONTINUE_SEARCH;

// мы обрабатываем только нарушение доступа

if (pep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) return(lDisposition);

// получаем адрес и информацию о попытке чтения/записи

PVOID pvAddrTouched = (PVOID) pep->ExceptionRecord->ExceptionInformation[1]; BOOL bAttemptedRead = (pep->ExceptionRecord->ExceptionInformation[0] == 0);

//попадает ли полученный адрес в регион,

//зарезервированный для этого VMArray? if ((m_pArray <= pvAddrTouched) &&

(pvAddrTouched < ((PBYTE) m_pArray + m_cbReserve))) {

//была попытка записи в наш массив, пытаемся исправить ситуацию lDisposition = OnAccessViolation(pvAddrTouched, bAttemptedRead,

pep, bRetryUntilSuccessful);

}

return(lDisposition);

}

///////////////////////////////////////////////////////////////////////////////

// фильтр, связываемый со всеми объектами класса CVMArray template <class TYPE>

LONG WINAPI CVMArray<TYPE>::UnhandledExceptionFilter(PEXCEPTION_POINTERS pep) {

Глава 25. Необработанные исключения, векторная обработка исключений и исключения

C++.docx 823

//по умолчанию пытаемся использовать другой фильтр (самый безопасный путь)

LONG lDisposition = EXCEPTION_CONTINUE_SEARCH;

//мы обрабатываем только нарушение доступа

if (pep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {

// проходим все узлы связанного описка

for (CVMArray* p = sm_pHead; p != NULL; p = p->m_pNext) {

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

//Примечание: исключение НАДО обработать, иначе процесс будет за-

крыт!

lDisposition = p->ExceptionFilter(pep, TRUE);

//если подходящий узел найден и он обработал исключение,

//выходим из цикла

if (lDisposition != EXCEPTION_CONTINUE_SEARCH) break;

}

}

//если ни один узел не смог устранить проблему,

//пытаемся использовать предыдущий фильтр исключений if (lDisposition == EXCEPTION_CONTINUE_SEARCH)

lDisposition = sm_pfnUnhandledExceptionFilterPrev(pep);

return(lDisposition);

}

//////////////////////////////// End of File //////////////////////////////////

Векторная обработка исключений и обработчики возобновления

Работа механизма SEH, о котором шла речь в главах 23 и 24, основана на фреймах. То есть, каждый раз, когда поток входит в блок (или фрейм) try, к связанному списку фреймов добавляется узел. Если возникает исключение, система просматривает занесенные в связанный список фреймы. Просмотр начинается с фрейма, в который поток вошел последним и заканчивается первым фреймом. При этом система ищет обработчики, указанные в блоках catch, связанных с фреймами try. Обнаружив обработчик в блоке catch, система снова просматривает связанный список, исполняя код в блоках finally. После завершения раскрутки либо после нормального (без исключений) завершения кода в блоке try фреймы удаляются из связанного списка.

824 Часть V. Структурная обработка исключений

Windows также поддерживает механизм векторной обработки исключений (vectored exception handling, VEH), работающий совместно с SEH. Вместо конструкций языка программирования в этом механизме используются функции, которые регистрируются и вызываются каждый раз, когда возникает исключение, либо при обнаружении исключений, «просочившихся» сквозь стандартные механизмы SEH.

Функция AddVectoredExceptionHandler отвечает за регистрацию обработчика исключения (exception handler), который добавляется во внутренний список функций, вызываемых при возникновении исключения в любом из потоков процесса:

PVOID AddVectoredExceptionHandler (

ULONG bFirstlnTheList,

PVECTORED_EXCEPTION_HANDLER pfnHandler);

Параметр pfnHandler — это указатель на векторный обработчик исключения. Функция, которая играет эту роль, должна соответствовать следующей сигнатуре:

LONG WINAPI ExceptionHandler(struct _EXCEPTION_POINTERS* pExceptionlnfo);

Если параметр bFirstInTheList равен 0, то функция, заданная параметром pfnHandler, добавляется к концу внутреннего списка. В противном случае bFirstInTheList заданная функция добавляется в начало внутреннего списка. Когда возникает исключение, функции из списка VEH вызываются поочередно (но до вызова любых фильтров SEH). Если вызов устранил проблему, он должен вернуть EXCEPTION_CONTINUE_EXECUTION, чтобы система возобновила исполнение, начав с повтора команды, исходно вызвавшей исключение; в этом случае SEHфильтры так и не вступают в игру. Если же вызванный обработчик не справился с ошибкой, он должен вернуть EXCEPTION_CONTINUE_SEARCH, чтобы другие обработчики, внесенные в список, получили возможность обработать это исключение. Если все VEH-обработчики вернули EXCEPTION_CONTINUE_SEARCH, начинается обработка исключения с помощью SEH-фильтров. Заметьте, что VEHфильтрам запрещено возвращать EXCEPTION_EXECUTE_HANDLER.

Ранее установленный VEH-обработчик исключений можно удалить из внутреннего списка вызовом следующей функции:

ULONG RemoveVectoredExceptionHandler (PVOID pHandler);

Параметр pHandler задает описатель ранее установленной функцииобработчика, возвращаемый функцией AddVectoredExceptionHandler.

Примечание. Рекомендую также изучить статью Мэтта Петрека (Matt Pietrek), посвященную реализации перехвата API-вызовов внутри процессов с применением точек прерывания («Under the Hood: New Vectored Exception

Handling in Windows XP», см. http://msdn.microsoft.com/msdn/mag/issues/01/09/hood/).

Этот метод отличается от показанного мной в главе 22.

Глава 25. Необработанные исключения, векторная обработка исключений и исключения

C++.docx 825

VEH-обработчики не только обрабатывают исключения прежде, чем это сделают SEH, но и позволяют уведомить разработчика о возникновении необработанных исключений. Для этого необходимо зарегистрировать обработчик возобновления (continue handler) вызовом следующей функции:

PVOID AddVectoredContinueHandler (

ULONG bFirstlnThelist,

PVECTORED_EXCEPTION_HANDLER pfnHandler);

Если параметр bErstInTheList равен 0, переданная в параметре pfnHandler функция добавляется к концу внутреннего списка обработчиков возобновления; в противном случае эта функция добавляется в начал списка. Когда возникает необработанное исключение, функции-обработчики возобновления из списка вызываются поочередно. В частности, это происходите, когда глобальный фильтр необработанных исключений, установленный вызовом SetUnhandl edExceptionFilter, возвращает EXCEPTION_CONTINUE_SEARCH. Функция-обработчик возобновления может, вернув EXCEPTION_CONTINUE_EXECUTION, отменить исполнение остальных функций в списке обработчиков возобновления и заставить систему повторно выполнить команду, вызвавшую исключение. Обработчик исключения также может вернуть значение EXCEPTION_CONTINUE_SEARCH, и тогда остальные функции-обработчики будут вызваны.

Чтобы удалить из списка ранее установленную функцию-обработчик возобновления, вызовите следующую функцию:

ULONG RemoveVectoredContinueHandler (PVOID pHandler);

Параметр pHandler задает описатель ранее установленной функцииобработчика, возвращенный функцией AddVectoredContinueHandler.

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

Исключения C++ и структурные исключения

Разработчики часто спрашивают меня, что лучше использовать: SEH или исключения C++. Ответ на этот вопрос вы найдете здесь.

Для начала позвольте напомнить, что SEH механизм операционной системы, доступный в любом языке программирования, а исключения С++ поддерживаются только в C++. Создавая приложение на C++, вы должны использовать средства именно этого языка, а не SEH. Причина в том, что исключения C++ - часть самого языка и его компилятор автоматически создает код, который вызывает деструкторы объектов и тем самым обеспечивает корректную очистку ресурсов.

Однако вы должны иметь в виду, что компилятор Microsoft Visual С++ реализует обработку исключений C++ на основе SEH операционной системы. Напри-

мер, когда вы создаете С++-блок try, компилятор генерирует SEH-блок

__try.

С++-блок

catch

становится

SEH-фильтром

исключений,

а

826 Часть V. Структурная обработка исключений

код блока catch — кодом SEH-блока __except. По сути, обрабатывая С++- оператор throw, компилятор генерирует вызов Windows-функции RaiseException, и значение переменной, указанной в throw, передается этой функции как дополнительный аргумент.

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

void ChunkyFunky() {

void ChunkyFunky() {

try {

__try {

// тело блока try

// тело блока try

throw 5;

RaiseException(Code=0xE06D7363,

 

Flag=EXCEPTION_NONCONTINUABLE,

 

Args=5);

}

}

catch (int x) {

__except ((ArgType == Integer) ?

 

EXCEPTION_EXECUTE_HANDLER :

 

EXCEPTION_C0NTINUE_SEARCH) {

// тело блока catch

// тело блока catch

}

}

}

}

Обратите внимание на ряд интересных особенностей этого кода. Во-первых, RaiseException вызывается с кодом исключения 0xE06D7363. Это код программного исключения, выбранный разработчиками Visual C++ на случай генерации (throwing) исключений C++. Вы можете сами в этом убедиться, преобразовав этот код в AsCII-символы: в результате получится «.msc».

Заметьте также, что при генерации исключения C++ всегда используется флаг EXCEPTION_NONCONTINUABLE. Исключения C++ не разрешают возобновлять выполнение программы, и возврат EXCEPTION_CONTINUE_EXECUTION

фильтром, диагностирующим исключения C++, был бы ошибкой. Если вы посмотрите на фильтр __except в функции справа, то увидите, что он возвращает только EXCEPTION_EXECUTE_HANDLER или EXCEPTION_CONTINUE_

SEARCH.

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

Последнее, на что я хочу обратить ваше внимание, — фильтр __except. Он служит для сравнения типа данных переменной throw с типом переменной, используемой С++-оператором catch. Если эти переменные одного типа, фильтр возвращает EXCEPTION_EXECUTE_HANDLER, заставляя систе-

Глава 25. Необработанные исключения, векторная обработка исключений и исключения

C++.docx 827

му исполнить код в блоке catch. В противном случае фильтр возвращает EXCEPTION_CONTINUE_SEARCH, и система проверяет фильтры catch, расположенные выше в дереве вызовов.

Примечание. Так как исключения C++ реализуются через SEH, оба эти механизма можно использовать в одной программе. Например, я предпочитаю передавать физическую память при исключениях, вызываемых нарушениями доступа. Хотя C++ вообще не поддерживает этот тип обработки исключений (с возобновлением выполнения), он позволяет применять SEH в тех местах программы, где это нужно, и ваш фильтр __except может возвращать

EXCEPTION_CONTINUE_EXECUTION. Ну а в остальных частях исходно-

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

C++.

Исключения и отладчик

Отладчик Visual Studio обладает фантастическими возможностями по отладке исключений. Когда один из потоков процесса генерирует исключение, операционная система тут же уведомляет подключенный к процессу отладчик (если он имеется). Такое уведомление называется уведомлением первого шанса (first-chance notification). Обычно отладчик, реагируя на это уведомление, заставляет поток начать поиск фильтра исключения. Если все фильтры исключений вернут EXCEPTION_CONTINUE_SEARCH, операционная система направляет отладчику другое уведомление — т.н. уведомление последнего шанса (last-chance notification). Эти два типа уведомлений дают разработчикам больше контроля над отладкой и обработкой исключений.

При настройке отдельных решений в Visual Studio окно Exceptions (см. ниже) позволяет настроить реакцию отладчика на уведомления первого шанса

828 Часть V. Структурная обработка исключений

Видно, что исключения в этом окне рассортированы по категориям, включая Win32 Exceptions, в которой находятся все системные исключения. Для каждого исключения отображается 32-разрядный код, текстовое описание и действие отладчика при получении уведомления первого шанса (флажок Thrown) и уведомления последнего шанса (флажок User-Unhandled). Учтите, что последний параметр применим только к исключениям CLR. В показанном выше окне я выбрал исключение, возникающее при нарушении доступа, и задал остановку как действие, которое отладчик выполняет при генерации этого исключении. Теперь, как только в одном из потоков отлаживаемого процесса возникнет нарушение доступа, отладчик получит уведомление первого шанса и покажет сообщение следующего вида:

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

Если щелкнуть в нем No, сбойный поток попробует повторить команду, вызвавшую исключение. В большинстве случаев это приведет к повторной генерации исключения, и тогда проку от этого варианта немного. Но в случае исключений, генерированных функцией RaiseException, выбор кнопки No позволит потоку продолжить исполнение, как ни в чем не бывало. Это особенно удобно для отладки программ на C++, генерирующих исключения с помощью С++-оператора throw (подробнее обработка исключений C++ освещалась в следующем разделе).

Щелчок кнопки Yes позволит сбойному потоку приступить к поиску фильтров исключений. Если найденный фильтр исключений возвращает EXCEPTION_EXECUTE_HANDLER или EXCEPTION_CONTINUE_XECUTION,

Глава 25. Необработанные исключения, векторная обработка исключений и исключения

C++.docx 829

все хорошо, и поток продолжает исполнение. Если же все фильтры вернут EXCEPTION_CONTINUE_SEARCH, отладчик получит уведомление последнего шанса и откроет такое окно:

Теперь остается лишь два варианта: отладка приложения либо его завершения. Описанные выше события происходят, если настроить отладчик для остановки исполнения при возникновении исключения (что я и сделал, пометив флажок Thrown). Однако для большинства исключений этот флажок по умолчанию снят. В этом случае отладчик, получив уведомление первого шанса, просто показывает

уведомление в своем окне Output:

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

Соседние файлы в предмете Программирование на C++