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

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

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

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

Рис. 24-1. Так система обрабатывает исключения.

Глава 24. Фильтры и обработчики исключений.docx 773

EXCEPTION_EXECUTE_HANDLER

Фильтр исключений в Funcmeister2 определен как EXCEPTION_EXECUTE_ HANDLER. Это значение сообщает системе в основном вот что: «Я вижу это исключение; так и знал, что оно где-нибудь произойдет; у меня есть код для его обработки, и я хочу его сейчас выполнить». В этот момент система проводит глобальную раскрутку (о ней — немного позже), а затем управление передается коду внутри блока except (коду обработчика исключений). После его выполнения система считает исключение обработанным и разрешает программе продолжить работу. Этот механизм позволяет Windows-приложениям перехватывать ошибки, обрабатывать их и продолжать выполнение — пользователь даже не узнает, что была какая-то ошибка.

Но вот откуда возобновится выполнение? Поразмыслив, можно представить несколько вариантов.

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

Внашем случае нормальное выполнение можно продолжить, но Funcmeister2

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

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

ние в Funcmeister2, строкой:

malloc(5 / dwTemp);

Компилятор сгенерирует для нее машинные команды, которые выполняют деление, результат помещают в стек и вызывают malloc. Если попытка деления привела к ошибке, дальнейшее (корректное) выполнение кода невозможно. Система должна поместить что-то в стек, иначе он будет разрушен.

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

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

dwTemp = 2;

Тогда вы вполне могли бы возобновить выполнение с возбудившей исключение команды. На этот раз вы поделили бы 5 на 2, и программа спокой-

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

но продолжила бы свою работу. Иначе говоря, вы что-то меняете и заставляете систему повторить выполнение команды, возбудившей исключение. Но, применяя такой прием, нужно иметь в виду некоторые тонкости (о них — чуть позже).

Третий, и последний, вариант — приложение возобновляет выполнение с инструкции, следующей за блоком except. Именно так и происходит, когда фильтр исключений определен как EXCEPTION_EXECUTE_HANDLER. По окончании выполнения кода в блоке except управление передается на первую строку за этим блоком.

Некоторые полезные примеры

Допустим, вы хотите создать отказоустойчивое приложение, которое должно работать 24 часа в сутки и 7 дней в неделю. В наше время, когда программное обеспечение настолько усложнилось и подвержено влиянию множества непредсказуемых факторов, мне кажется, что без SEH просто нельзя создать действительно надежное приложение. Возьмем элементарный пример: функцию strcpy из библиотеки С:

char* strcpy(

char* strDestination, const char* strSource);

Крошечная, давно известная и очень простая функция, да? Разве она может вызвать завершение процесса? Ну, если в каком-нибудь из параметров будет передан NULL (или любой другой недопустимый адрес), strcpy приведет к нарушению доступа, и весь процесс будет закрыт.

Создание абсолютно надежной функции strcpy возможно только при использовании SEH:

char* RobustStrCpy(char* strDestination, const char* strSource) {

_try {

strcpy(strDestination, strSource);

}

__except (EXCEPTION_EXECUTE_HANDLER) { // здесь ничего не делаем

}

return(strDestination);

}

Все, что делает эта функция, — помещает вызов strcpy в SEH-фрейм. Если вызов strcpy проходит успешно, RobustStrCpy просто возвращает управление. Если же strcpy генерирует нарушение доступа, фильтр исключений возвращает значе-

ние

EXCEPTION_EXECUTE_HANDLER, которое заставляет поток выполнить

код

обработчика. В функции RobustStrCpy обработчик не делает

Глава 24. Фильтры и обработчики исключений.docx 775

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

Поскольку вы не знаете, как реализована функция strcpy, вы также не имеете представления, какие исключения могут возникнуть в ходе ее исполнения. В описании функции упомянут лишь случай, когда параметр strDestination равен NULL или недопустим. А что, если адрес правильный, но буфер для хранения strSource, но размер буфера слишком мал? Если блок памяти, на который указывает strDestination, входит в блок большего размера, его содержимое может быть повреждено функцией strcpy. При этом также возможно нарушение доступа. Однако во время обработки исключения работа поврежденного процесса продолжается, что впоследствии может привести к краху, причины которого будет трудно установить, либо к возникновению «дыры» в защите. Мораль проста: обрабатывайте только те исключения, последствия которых вы в состоянии устранить, но не забывайте и про другие меры защиты от повреждения данных и брешей в защите (подробнее о безопасных строковых функциях см. в главе 2).

Рассмотрим другой пример. Вот функция, которая сообщает число отделенных пробелами лексем в строке:

int RobustHowManyToken(const char* str) {

int nHowManyTokens = -1;

//

значение, равное -1, сообщает о неудаче

char* strTemp = NULL;

//

предполагаем худшее

__try {

// создаем временный буфер

strTemp = (char*) malloc(strlen(str) + 1);

//копируем исходную строку во временный буфер strcpy(strTemp, str);

//получаем первую лексему

char* pszToken = strtok(strTemp, “ ”);

// перечисляем все лексемы

for (; pszToken != NULL; pszToken = strtok(NULL, " ")) nHowManyTokens++;

nHowManyTokens++;

// добавляем 1, так как мы начали с -1

}

__except (EXCEPTION_EXECUTE_HANDLER) { // здесь ничего не делаем

}

// удаляем временный буфер (гарантированная операция)

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

free(strTemp);

return(nHowManyTokens);

}

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

Благодаря SEH эта обманчиво простая функция справляется с любыми неожиданностями. Давайте посмотрим, как она работает в некоторых ситуациях.

Во-первых, если ей передается NULL (или любой другой недопустимый адрес), переменная nHowManyTokens сохраняет исходное значение -1. Вызов strien внутри блока try приводит к нарушению доступа. Тогда управление передается фильтру исключений, а от него — блоку except, который ничего не делает. После блока except вызывается free, чтобы удалить временный буфер в памяти. Однако он не был создан, и в данной ситуации мы вызываем/гее с передачей ей NULL Стандарт ANSI С допускает вызов free с передачей NULL, в каковом случае эта функция просто возвращает управление, так что ошибки здесь нет. В итоге RobustHowManyToken возвращает значение -1, сообщая о неудаче, и аварийного завершения процесса не происходит.

Во-вторых, если функция получает корректный адрес, но вызов malloc (внутри блока try) заканчивается неудачно и дает NULL, то обращение к strcpy опять приводит к нарушению доступа. Вновь активизируется фильтр исключений, выполняется блок except (который ничего не делает), вызывается free с передачей NULL (из-за чего она тоже ничего не делает), и RobustHowManyToken возвращает -1, сообщая о неудаче. Аварийного завершения процесса не происходит.

Наконец, допустим, что функции передан корректный адрес и вызов malloc прошел успешно. Тогда преуспеет и остальной код, а в переменную nHowManyTokens будет записано число лексем в строке. В этом случае выражение в фильтре исключений (в конце блока try) не оценивается, код в блоке except не выполняется, временный буфер нормально удаляется, и nHowManyTokens сообщает количество лексем в строке.

Функция RobustHowManyToken демонстрирует, как обеспечить гарантированную очистку ресурса, не прибегая к try-finally. Также гарантируется выполнение любого кода, расположенного за обработчиком исключения (если, конечно, функция не возвращает управление из блока try, но таких вещей вы должны избегать).

А теперь рассмотрим последний, особенно полезный пример использования SEH. Вот функция, которая дублирует блок памяти:

PBYTE RobustHemDup(PBYTE pbSrc, 8ize_t cb) {

pbDup = NULL;

// заранее предполагаем неудачу

Глава 24. Фильтры и обработчики исключений.docx 777

_try {

// создаем буфер для дублированного блока памяти pbDup = (PBYTE) malloc(cb);

memcpy(pbDup, pbSrc, cb);

}

__except (EXCEPTION_EXECUTE_HANDLER) { free(pbDup);

pbDup = NULL;

}

return(pbDup);

}

Эта функция создает буфер в памяти и копирует в него байты из исходного блока. Затем она возвращает адрес этого дубликата (или NULL, если вызов закончился неудачно). Предполагается, что буфер освобождается вызывающей функцией — когда необходимость в нем отпадает. Это первый пример, где в блоке except понадобится какой-то код. Давайте проанализируем работу этой функции в различных ситуациях.

Если в параметре pbSrc передается некорректный адрес или если вызов malloc завершается неудачно (и дает NULL), тетсру возбуждает нарушение доступа. А это приводит к выполнению фильтра, который передает управление блоку except. Код в блоке except освобождает буфер памяти и устанавливает pbDup в NULL, чтобы вызвавший эту функцию поток узнал о ее неудачном завершении. (Не забудьте, что стандарт ANSI С допускает передачу NULL функции free.)

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

Глобальная раскрутка

Когда фильтр исключений возвращает EXCEPTION_EXECUTE_HANDLER, системе приходится проводить глобальную раскрутку. Она приводит к продолжению обработки всех незавершенных блоков try-finally, выполнение которых началось вслед за блоком try-except, обрабатывающим данное исключение. Блок-схема на рис. 24-2 поясняет, как система осуществляет глобальную раскрутку. Посматривайте на эту схему, когда будете читать мои пояснения к следующему примеру.

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

Рис. 24-2. Так система проводит глобальную раскрутку

void FuncOStimpy1() {

// 1. Что-то делаем здесь

_try {

//2. Вызываем другую функцию

FuncORen1();

//этот код никогда не выполняется

}

__except ( /* 6. Проверяем фильтр исключений. */ EXCEPTION_EXECUTE_HANDLER) {

// 8. После раскрутки выполняется этот обработчик

Глава 24. Фильтры и обработчики исключений.docx 779

MessageBox(…);

}

// 9. Исключение обработано - продолжаем выполнение

}

void FuncORen1() { DWORD dwTemp = 0;

// 3. Что-то делаем здесь

__try {

//4. Запрашиваем разрешение на доступ к защищенным данным

WaitForSingleObJect(g_hSem, INFINITE);

//5. Изменяем данные, и здесь генерируется исключение

g_dwProtectedData = 5 / dwTemp;

}

__finally {

//7. Происходит глобальная раскрутка, так как

//фильтр возвращает EXCEPTION_EXECUTE_HANDLER

//Даем и другим попользоваться защищенными данными

ReleaseSemaphore(g_hSem, 1, NULL);

}

// сюда мы никогда не попадем

}

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

FuncOStimpy1 начинает выполнение со входа в свой блок try и вызова FuncORen1. Последняя тоже начинает со входа в свой блок try и ждет освобождения семафора. Завладев им, она пытается изменить значение глобальной переменной g_dwProtectedData. Деление на нуль возбуждает исключение. Система, перехватив управление, ищет блок try, которому соответствует блок except. Поскольку блоку try функции FuncORen1 соответствует блок finally, система продолжает поиск и находит блок try в FuncOStimpy1, которому как раз и соответствует блок except.

Тогда система проверяет значение фильтра исключений в блоке except функции FuncOStimpy1. Обнаружив, что оно — EXCEPTION_EXECUTE_ HANDLER, система начинает глобальную раскрутку с блока finally в функции FuncORen1. Заметьте: раскрутка происходит до выполнения кода из

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

блока except в FuncOStimpy1. Осуществляя глобальную раскрутку, система возвращается к последнему незавершенному блоку try и ищет теперь блоки try, которым соответствуют блоки finally. В нашем случае блок finпаllу находится в функции FuncORen1. Мощь SEH по-настоящему проявляется, когда система выполняет код finally в FuncORen1. Из-за его выполнения семафор освобождается, и поэтому другой поток получает возможность продолжить работу. Если бы вызов ReleaseSemaphore в блоке finally отсутствовал, семафор никогда бы не освободился.

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

Вот так и работает структурная обработка исключений. Вообще-то, SHE — штука весьма трудная для понимания: выполнение вашего кода вмешивается операционная система. Код больше не выполняется последовательно, сверху вниз; система устанавливает свой порядок — сложный, но все же предсказуемый. Поэтому, следуя блок-схемам на рис. 24-1 и 24-2, вы сможете уверенно применять

SEH.

Чтобы лучше разобраться в порядке выполнения кода, посмотрим на происходящее под другим углом зрения. Возвращая EXCEPTION_EXECUTE_HANDLER, фильтр сообщает операционной системе, что регистр указателя команд данного потока должен быть установлен на код внутри блока except. Однако этот регистр указывал на код внутри блока try функции FuncORen1. А из главы 23 вы должны помнить, что всякий раз, когда поток выходит из блока try, соответствующего блоку finally, обязательно выполняется код в этом блоке finally. Глобальная раскрутка как раз и является тем механизмом, который гарантирует соблюдение этого правила при любом исключении.

Внимание! Одно из новшеств Windows Vista состоит в следующем. Если в вашем коде нет внешнего блока try/except (EXCEPTION_EXECUTE_ HANDLER) и в одном из внутренних блоков try/finally возникает исключение, процесс просто закрывается без глобальной раскрутки, код из внутренних блоков finally при этом не исполняется. В прежних версиях Windows перед остановкой процесса выполнялась глобальная раскрутка, что давало шансы на исполнение блоков finally. Подробнее о необработанных исключениях см. в следующей главе.

Остановка глобальной раскрутки

Глобальную раскрутку, осуществляемую системой, можно остановить, если в блок finally включить оператор return. Взгляните:

Глава 24. Фильтры и обработчики исключений.docx 781

void FuncMonkey() { __try {

FuncFish();

}

__except (EXCEPTION_EXECUTE_HANDLER) { MessageBeep(0);

}

MessageBox(…);

}

void FuncFish() { FuncPheasant();

MessageBox(…);

}

void FuncPheasant() {

__try {

strcpy(NULL, NULL);

}

__finally { return;

}

}

При вызове strcpy в блоке try функции FuncPheasant из-за нарушения доступа к памяти генерируется исключение. Как только это происходит, система начинает просматривать код, пытаясь найти фильтр, способный обработать данное исключение. Обнаружив, что фильтр в FuncMonkey готов обработать его, система приступает к глобальной раскрутке. Она начинается с выполнения кода в блоке finally функции FuncPheasant. Но этот блок содержит оператор return. Он заставляет систему прекратить раскрутку, и FuncPheasant фактически завершается возвратом в FuncFish, которая выводит сообщение на экран. Затем FuncFish возвращает управление FuncMonkey, и та вызывает MessageBox.

Заметьте: код блока except в FuncMonkey никогда не вызовет MessageBeep. Оператор return в блоке finally функции FuncPheasant заставит систему вообще прекратить раскрутку, и поэтому выполнение продолжится так, будто ничего не произошло.

Избегайте операторов return в блоках finally. Чтобы помочь в выявлении таких ситуаций, компилятор С++ генерирует предупреждение C4532:

„return‟ : jump out of _finally block has undefined behavior during termination handling.

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