Г Л А В А 24 Фильтры и обработчики исключений

Исключение — это событие, которого Вы не ожидали. В хорошо написанной про грамме не предполагается попыток обращения по неверному адресу или деления на нуль И все же такие ошибки случаются За псрехват попыток обращения по неверно му адресу и деления на нуль отвечает центральный процессор, возбуждающий исклю чения в ответ на эти ошибки. Исключение, возбужденное процессором, называется аппаратным (hardware exception) Далее мы увидим, что операционная система и прикладные программы способны возбуждать собственные исключения — программ ные (software exceptions).

При возникновении аппаратного или программного исключения операционная система дает Вашему приложению шанс определить его тип и самостоятельно обра ботать Синтаксис обработчика исключений таков:

__try {

// защищенный блок

}

__except (фильтр исключений) {

// обработчик исключений

}

Обратите внимание на ключевое слово _ except За блоком try всегда должен сле довать либо блок finaly, либо блок except. Для данного блока try нельзя указать одно временно и блок finaly, и блок except: к тому же за try не может следовать несколько блок finaly или except Однако try-finally можно вложить в try-except, и наоборот.

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

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

Funcmeister1

Вот более конкретный пример блока try-except

DWORD Funcmeister1()
{

DWORD dwTemp

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

...

__try
{

// 2 Выполняем какую-то операцию
dwTemp = 0;

}

__except (EXCEPTION_EXECUTE HANDLER)
{

// обрабатываем исключение этит код никогда не выполняется
...

}

// 3 Продолжаем что то делать return(dwTemp)

}

В блоке try функции Funcmetsterl мы просто присваиваем 0 переменной dwTemp Такая операция не приведет к исключению, и поэтому код в блоке except никогда не выполняется Обратите внимание на такую особенность конструкция try-finally ведет себя иначе После того как переменной dwTemp присваивается 0, следующим испол няемым оператором оказывается return

Хотя ставить операторы return, goto, continue и break в блоке try обработчика за вершения настоятельно не рекомендуется, их применение в этом блоке не приводит к снижению быстродействия кода или к увеличению сго размера Использование этих операторов в блоке try, связанном с блоком except, не вызовет таких неприятностей, как локальная раскрутка

Funcmeister2

Попробуем модифицировать нашу функцию и посмотрим, что будет

DWORD Funcmeister2()
{

DWORD dwTemp = 0;

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

...

__try
{

// 2 Выполняем какую-то операцию

dwTemp = 5 / dwTemp;
// генерирурт исключение

dwTemp += 10;
// никогда не выполняется

}

__except ( /* 3 Проверяем фильтр */ EXCEPTION_EXECUTE_HANDLER)
{

// 4. Обрабатываем исключение

MessageBeep(0)

...

}

// 5 Продолжаем что-то делать

return(dwТemp); }

rihter23-7.jpg

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

Инструкция внутри блока try функции Funcmeister2 пытается поделить 5 на 0. Перехватив это событие, процессор возбуждает аппаратное исключение Тогда опе рационная система ищст начало блока except и проверяет выражение, указанное в качестве фильтра исключении, оно должно дать один из трех идентификаторов, оп ределенных в заголовочном Windows-файле Exept.h

Идентификатор

Значение

EXCEPTION_EXECUTE_HANDLER

1

EXCEPTION_CONTINUE_SEARCH

0

EXCEPTION_CONTINUE_EXECUTION

-1

Далее мы обсудим, как эти идентификаторы изменяют выполнение потока. Читая следующие разделы, посматривайте на блок-схему на рис. 24-1, которая иллюстриру ет операции, выполняемые системой после генерации исключения

EXCEPTION_EXECUTE_HANDLER

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

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

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

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

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

malloc(5 / dwTemp);

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

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

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

dwTemp = 2;

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

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

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

Допустим, Вы хотите создать отказоустойчивое приложение, которое должно рабо тать 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-фрейм. Если вызов strcfiy приходит успешно, RobustStrCpy просто возвращает управление. Если же strcpy генерирует нарушение доступа, фильтр исключений возвращает значение EXCEP TION_EXECIITE_HANDLER, которое заставляет поток выполнить код обработчика. В функции RobublStrCpy обработчик не делает ровным счетом ничего, и опягь Robust StrCpy просто возвращает управление. Но она никогда не приведет к аварийному за вершению процесса1

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

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}
{

// здесь ничего не делаем

}

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

return(nHowManyTokens);

}

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

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

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

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

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

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

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

PBYTE RobustMemDup(PBYTE pbSrc, size_t cb)
{

PBYTE pbDup = NULL;

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

__try
{

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

memcpy(pbDup, pbSrc, cb);

}

__except (EXCEPTION_EXECUTE_HANDLER)
{

free(pbDup);

pbDup = NULL;

}

return(pbDup);

}

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

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

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

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

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

rihter23-8.jpg

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

void FuncOSTimpy1()
{

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

...

__try
{

// 2 Вызываем другую функцию
FuncORen1();

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

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

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

MessageBox(....);

}

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

}

void FuncORen1()
{

DWORD dwTemp = 0;

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

...

__try
{

// 4. Запрашиваем разрешение на доступ к защищенным данным
WaitForSingleObject(g_nSem, INFINITE);

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

g_dwProtectedData = 5 / dwTemp;

}

__finally
{

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

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

// Даем и другим попользоваться защищенными данными
ReleaseSemaphore(g_hScm, 1, NULL);

}

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

...

}

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

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

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

Мощь SEH по-настоящему проявляется, когда система выполняет код finally в Func ORen1. Из-за его выполнения семафор освобождается, и поэтомудругой поток полу чает возможность продолжить работу. Если бы вызов ReleaseSemapbore в блоке finally отсутствовал, семафор никогда бы не освободился.

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

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

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

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

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

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. Ho этот блок содержит оператор return. Он заставляет систему прекра тить раскрутку, и FuncPheasant фактически завершается возвратом в FuncFish, кото рая выводит сообщение на экран Затем FuncFish возвращает управление FuncMonkey, и та вызывает MessageBox.

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

Microsoft намеренно вложила в SEH такую логику Иногда всдь нужно прекратить раскрутку и продолжить выполнение программы. Хотя в большинстве случаев так все же не делают А значит, будьте внимательны и избслайте операторов return в блоках finally.

EXCEPTION_CONTINUE_EXECUTION

Давайте приглядимся к юму, как фильтр исключений получает один из трсх иденти фикаторов, определенных в файле Excpt.h В Funcmeister2 идентификатор EXCEP TION_EXECUTE_HANDLER «зашит» (простоты ради) в код самого фильтра, но Вы могли бы вызывать там функцию, которая определяла бы нужный идентификатор Взгляните:

char g_szBuffer[100];

void FunclinRoosevfilt1()
{

int x = 0;

Char *pchBuffer = NULL;

__try
{

*pchBuffer = 'J';

x = 5 / x;

}

__except (OilFilter1(&pchBuffer))
{

MessageBox(NULL, "An exception occurred", NULL, MB_OK);

}

MessageBox(NULL, Function completed , NULL, MB_OK),

}

LONG OilFilter1(char **ppchBuffer}
{

if (*ppchBuffer == NULL)
{

*ppchBuffer = g_szBuffer;

return(FXCEPTION_CONTINUE EXECUTION);

}

return(EXCEPTION_EXECUTE_HANDLER);

}

В первый раз проблема возникает, когда мы пытаемся поместить J в буфер, на который указывает pchBuffer К сожалению, мы не определили pchBuffer как указатель на наш глобальный буфер g_szBuffer — вместо этою он указывает на NULL. Процес сор генерирует исключение и вычисляет выражение в фильтре исключений в блоке except, связанном с блоком try, в котором и произошло исключение. В блоке cxcept адрес переменной pchBuffer передается функции OilFilter1,

Получая управление, OilFilter1 проверяет, не равен ли *ppchBuffer значению NULL, и, если да, устанавливает его так, чтобы он указывал на глобальный буфер g_szBuffer. Тогда фильтр возвращает EXCEPTION_CONTINUE_EXECUTION. Обнаружив гакое зна чение выражения в фильтре, система возвращается к инструкции, вызвавшей исклю чение, и пытается выполнить ее снова. IIa этот раз все проходит успешно, и J будет записана в первый байт буфера g_szBuffer.

Когда выполнение кода продолжится, мы опять столкнемся с проблемой в блоке try — теперь это деление на нуль. И вновь система вычислит выражение фильтра ис

ключений. На этот раз *ppchBuffer не равен NULL, и поэтому OilFttterl вернет EXCEP TION_EXECUTE_HANDLER, что подскажет системе выполнить код в блоке excepf, и на экране появится окно с сообщением oб исключении.

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

Будьте осторожны с EXCEPTION_CONTINUE_EXECUTION

Будет ли удачной попытка исправить ситуацию в только что рассмотренной функ ции и заставить систему продолжить выполнение программы, зависит от типа про цессора, от того, как компилятор генерирует машинные команды при трансляции операторов С/С++, и от параметров, заданных компилятору

Компилятор мог сгенерировать две машинные команды для оператора *pchBuffer = 'J'; которые выглядят так:

MOV EAX, [pchBuffer] // адрес помещается в регистр EAX

MOV [EAX], 'J' // символ J записывается по адресу из регистра LAX

Последняя команда и возбудила бы исключение. Фильтр исключений, перехватив его, исправил бы значение pchBuffer и указал бы системе повторить эту команду. Но проблема в том, что содержимое регистра не изменится так, чтобы отразить новое значение pchBuffer, и поэтому повторение команды снова приведет к исключению. Вот и бесконечный цикл!

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

EXCEPTION_CONTINUE_EXECUTION всегда срабатывает лишь в одной ситуации: при передаче памяти зарезервированному региону. О том, как зарезервировать боль шую область адресного пространства, а потом передавать ей память лишь по мере необходимости, я рассказывал в главе 15 Соответствующий алгоритм демонстриро вала программа-пример VMAlloc. На основе механизма SEH то же самое можно было бы реализовать гораздо эффективнее (и не пришлось бы все время вызывать функ цию VirtualAtloc).

В главе l6 мы говорили о стеках потоков, В частности, я показал, как система ре зервирует для стека потока регион адресного пространства размером 1 Мб и как она автоматически передает ему новую память по мере разрастании стека. С этой целью система создает SEH-фрейм. Когда поток пытается задействовать несуществующую часть стека, генерируется исключение. Системный фильтр определяет, что исключе ние возникло из-за попытки обращения к адресному пространству, зарезервирован ному под стек, вызывает функцию VirtualAlloc для передачи дополнительной памяти стеку потока и возвращает EXCEPTION_CONTINUE_EXECUTION. После этого машин ная команда, пытавшаяся обратиться к несуществующей части стека, благополучно выполняется, и поток продолжает свою работу.

Механизмы использования виртуальной памяти в сочетании со структурной об работкой исключений позволяют создавать невероятно «шустрые* приложения Про грамма-пример Spreadsheet в следующей главе продемонстрирует, как на основе SEH

эффективно реализовать управление памятью в электронной таблице. Этот код вы полняется чрезвычайно быстро.

EXCEPTION_CONTINUE_SEARCH

Приведенные до сих пор примеры были ну просто детскими Чтобы немного встрях нуться, добавим вызов функции:

char g_szBuffer[100];

void FunclinRoosevelt2()
{

char *pchBuffer = NULL;

__try
{

FuncAtude2(pchBuffer);

}

__except (OilFilter2(&pchBuffer))
{

MessageBox(...);

}

}

void FuncAtude2(char *sz)
{

*sz = 0;
}

LONG OilFilter2(char **ppchBuffer)
{

if (*ppchBuffer == NULL)
{

*ppchBuffer = g_szBuffer;
return(EXCEPTION_CONTINUE_EXECUTION);

}

return(EXCEPTION_EXECUTE HANDLER);

}

При выполнении FunclinRoosevelt2 вызывается FuncAtude2, которой передается NULL. Последняя приводит к исключению. Как и раньше, система проверяет выраже ние в фильтре исключений, связанном с последним исполняемым блоком try. В на шем примере это блок try в FunclinRoosevelt2, поэтому для оценки выражения в филь тре исключений система вызываег OilFilter2 (хотя исключение возникло в FuncAtude2).

Замесим ситуацию еще круче, добавив другой блок try-except

char g_szBuffer[100];

void FunclinHoosevelt3()
{

char *pchBuffer = NULL;

__try
{

FuncAtude3(pchBuffer);

}

__except (OilFilter3(&pch8uffer))
{

Message8ox(...);

}

}

void FuncAtude3(char *sz)
{

__try
{

*sz = 0;

}

__except (EXCEPTION_CONTINUE_SEARCH)
{

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

...

}

}

LONG OilFilter3(Utar **ppchBuffer)
{

if (*ppchBuffer == NULL)
{

*ppchBuffer = g_szBuffer;

return(EXCEPTION CONTINUE_EXECUTION);

}

return(EXCEPTIQN_EXECUTE_HANDLER);

}

Теперь, когда FuncAtude3 пытается занести 0 по адресу NULL, по-прежнему возбуж дается исключение, но в работу вступает фильтр исключений из FuncAtude3. Значе ние этого очень простого фильтра — EXCEPTIUN_CONTINUE_SEARCH. Данный иден тификатор указывает системе перейти к предыдущему блоку tty, которому соответ ствует блок except, и обработать его фильтр.

Так как фильтр в FuncAtude3 дает EXCEPTION_CONTINUE_SEARCH, система пере ходит к предыдущему блоку try (в функции FunclinRoOsevelt3) и вычисляет eго фильтр OilFilter3. Обнаружив, что значение pchBuffer равно NULL, OilFilter3 меняет его так, чтобы оно указывало на глобальный буфер, и сообщает системе возобновить выпол нение с инструкции, вызвавшей исключение Это позволяет выполнить код в блоке try функции FuncAtude3, но, увы, локальная переменная sz в этой функции не измене на, и возникает новое исключение Опять бесконечный цикл!

Заметьте, я сказал, что система переходит к последнему исполнявшемуся блоку try, которому соответствует блок except, и проверяет его фильтр Это значит, что система пропускает при просмотре цепочки блоков любые блоки try, которым соответствуют блоки finally (а не except). Причина этого очевидна, в блоках finally нет фильтров ис ключений, а потому и проверять в них нечего. Если бы в последнем примере Func Atude3 содержала вместо except, система начала бы проверять фильтры исключений с OilFilter3 в FunclinRroosevelt3

Дополнительную информацию об EXCEPTION_CONTINUE_SEARCH см. в главе 25.

Функция GetExceptionCode

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

__try
{

x = 0;

У = 4 / x;

}

__except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ? EXCEPTlON_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{

// обработка деления иа нуль

}

Встраиваемая функция GetExceptionCode возвращает идентификатор типа исклю чения.

DWORD GotExceptionCode();

Ниже приведен список всех предопределенных идентификаторов исключений с пояснением их смысла (информация взята из докуметации Platform SDK) Эти иден тификаторы содержатся в заголовочном файле WinBase.h. Я сгруппировал исключе ния по категориям.

Исключения, связанные с памятью

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

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

Исключения, связанные с операциями над целыми числами

Исключения, связанные с операциями над вещественными числами

Встраиваемую функцию GetExceptionCode можно вызвать только из фильтра ис ключений (между скобками, которые следуют за _except) или из обработчика исклю чений. Скажем, такой код вполне допустим:

__try
{

У = 0;

x = 4 / у;

}

_except
{

{(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) || (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEAHCH)
{

switch (GetExceptionCode())
{

case EXCEPTION_ACCESS_VIOLATION:

// обработка нарушения доступа к памяти
...
break;

case EXCEPTION_INT_DIVIDE_BY_ZERO:

// обработка деления целого числа на нуль
...
break;

}

}

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

__try
{

У = 0;

x = 4 / у;

}

__except (CoffeeFilter())
{

// обрабогка исключения
...

}

LONG CoffeeFilter(void)
{

// ошибка при компиляции: недопустимый вызов GetExceptionCode

return((GetExceptionCode() == EXCFPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);

}

Нужного эффекта можно добиться, переписав код так:

__try
{

y = 0;
x = 4 / у;

}

__except (CoffeeFi]ter(GetExceptionCode()))
{

// обработка исключения
...

}

LONG CoffeeFilter(DWORD dwExceptionGode)
{

return((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);

}

Коды исключений формируются по тем же правилам, что и коды ошибок, опре деленные в файле WinError.h. Каждое значение типа UWORD разбивается на поля, как показано в таблице 24-1.

Биты

31-30

29

28

27-16

15-0

Содержимое

Код степени "тяжести" (severity)

Кем определен — Microsoft или пользователем

Зарезервирован

Код подсистемы (facility code)

Код исключения

Значение

0 = успех 1 = информация 2 = предупреждение 3 = ошибка

0 = Microsoft 1 = пользователь

Должен быть 0 (см таблицу ниже)

Определяется Microsoft

Определяется Microsoft или пользовате лем

Таблица 24-1, Поля кода ошибки

На сегодняшний день определены такие коды подсистемы.

Код подсистемы

Значение

Код подсистемы

Значение

FACILITY_NULL

0

FACILITY_CONTROL

10

FACILITY_RPC

1

FACILITY_CERT

11

FACILITY_DISPATCH

2

FACILITY_INTERNET

12

FACILITY_STORAGE

3

FACILITY_MEDIASERVER

13

FACILITY_ITF

4

FACILITY_MSMQ

11

FACILITY_WIN32

7

FACILITY_SETUPAPI

15

FACILITY_WINDOWS

8

FACILITY_SCARD

16

FACILITY_SECURITY

9

FACILITY_COMPLUS

17

Разберем на части, например, код исключения EXCEPTION_ACCESS_VIOLATlON. Если Вы посмотрите его значение в файле WinBase.h, то увидите, что оно равно 0xC0000005:

С 0 0 0 0 0 0 5 (в шестнадцатеричном виде) 1100 0000 0000 0000 0000 0000 0000 0101 (в двоичном виде)

Биты 30 и 31 установлены в 1, указывая, что нарушение доступа является ошиб кой (поток не может продолжить выполнение) Бит 29 равен 0, а это значит, что дан ный код определен Microsoft. Бит 28 равен 0, так как зарезервирован на будущее. Биты 16-27 равны 0, сообщая код подсистемы FACILITY_NULL (нарушение доступа может произойти в любой подсистеме операционной системы, а нс в какой-то одной). Биты 0-15 дают значение 5, которое означает лишь то, что Microsoft присвоила исключе нию, связанному с нарушением доступа, код 5.

Функция GetExceptionlnformation

Когда возникает исключение, операционная система заталкивает в стек соответству ющего потока структуры EXCEPTION_RECORD, CONTEXT и EXCEPTION_POINTERS

EXCEPTlON_RECORD содержит информацию об исключении, независимую от типа процессора, a CONTEXT — машинно-зависимую информацию об этом исключении В структуре EXCEPTIONPOINTERS всего два элемента — указатели на помещенные в стек структуры EXCEPTlON_RECORD и CONTEXT.

typedef struct _EXCEPTION_POINTERS
{

PEXCEPTION_RECORD ExceptionRecord;

PCONTEXT ConlexlRecofd;

} EXCEPTTON_POINTERS, *PEXCEPTION_POINTERS;

Чтобы получить эту информацию и использовать ее в программе, вызовите GetEx ceptionInformatton-.

PEXCEPTION_POINTERS GetExceptionInformation();

Эта встраиваемая функция возвращает' указатель на структуру EXCEPTION_POINTERS.

Самое важное в GetExceptionInformatton то, что ее можно вызывать только в филь тре исключений и больше нигде, потому что структуры CONTEXT, EXCEPTION_RE CORD и EXCEPTION_POINTERS существуют лишь во время обработки фильтра исклю чений. Когда управление переходит к обработчику исключений, эти данные в стеке разрушаются.

Если Вам нужно получить доступ к информации об исключении из обработчика, сохраните струкчуру EXCEPTION_RECORD и/или CONTEXT (на которые указывают элементы структуры EXCEPTIONPOINTERS) в объявленных Вами переменных Вот пример сохранения этих структур:

void FuncSkunk()
{

// объявляем переменные, которые мы сможем потом использовать
// для сохранения информации об исключении (если оно произойдет)
EXCEPTION_RECORD SavedExceptRec;
CONTEXT SavedContext;

...

__try
{

...

}

__except
(SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord;
SavedContext = *(GetExceptionInformation())->ContextRecord;
EXCEPTION_EXECUTE_HANDIER)
{

// мы можем теперь использовать переменные SavedExceptRec
// и SavedContext в блоке обработчика исключений
switch (SavedExceptRec ExceptionCode)
{

...

}

}

...

}

В фильтре исключений применяется оператор-запятая (,) — мало кто из програм мистов знает о нсм. Он указывает компилятору, что выражения, отделенные запяты ми, следует выполнять слева направо После вычисления всех выражений возвраща ется результат последнего из них — крайнего справа.

В FuncSkunk сначала вычисляется выражение слева, что приводит к сохранению находящейся в стеке структуры EXCEPTION_RECORD в локальной переменной Saved ExceptRec. Результат этого выражения является значением SavedExceptRec IIo он от брасывается, и вычисляется выражение, расположенное правее Это приводит к со хранению размещенной в стеке структуры CONTEXT в локальной переменной Saved-

Context. И снова результат — значение SavedContexl — отбрасывается, и вычисляется третье выражение. Оно равно 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 ExcepLionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NurrberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

} EXCEPTION_RECORD;

Структура EXCEPTIONRECORD содержит подробную машинно-независимую ин формацию о последнем исключении. Вот что представляют собой ec элементы.

Последние два элемента структуры EXCEPTION_RECORD сообщают фильтру до полнительную информацию об исключении. Сейчас такую информацию дает только один тип исключений EXCEPTION_ACCESS_VIOLATION. Все остальные дают нулевое значение в элементе NumberParameters. Проверив его, Вы узнаете, надо ли присмат ривать массив ExceptionInformation.

При исключении EXCEPTION_ACCESS_VIOLATION эелемент ExceplionInformation[0] содержит флаг, указывающий тип операции, которая вызвала нарушение доступа. Если его значение равно 0, поток пытался читать недоступные ему данные; Т — записы вать данные по недоступному ему адресу. Элемент ExceptionInformation[l] определяет адрес недоступных данных.

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

__try
{

...

}

__except (ExpFltr(GetExceptionInformation()->ExceptionRecord))
{

...

}

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 | MB_ICONEXCLAMATION);

return(EXCEPTION_CONTINUE_SEARCH); }

Элемент ContextRecord структуры EXCEPTION_POINTERS указывает на структуру CONTEXT (см. главу 7), содержимое которой зависит от типа процессора.

С помощью этой структуры, в основном содержащей по одному элементу для каж дого регистра процессора, можно получить дополнительную информацию о возник шем исключении. Увы, это потребует написания машинно-зависимого кода, способ ного распознавать тип процессора и использовать подходящую для пего структуру CONTEXT. При этом Вам придется включить в код набор директив #ifdef для разных типов процессоров. Структуры CONTEXT для различных процессоров, поддерживае мых Windows, определены в заголовочном файле WinNT.h,

Программные исключения

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

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

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

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

Если Вы все же решились на уведомление об ошибках через исключения, я аппло дирую этому решению и пишу этот раздел специально для Вас. Давайте для начала посмотрим на семейство Неар-функции (HeapCreate, НеарАllос и т. д.). Наверное, Вы помните из главы 18, что они предлагают разработчику возможность выбора. Обыч но, когда их вызовы заканчиваются неудачно, они возвращают NULL, сообщая об ошибке. Но Вы можете передать флаг HEAP_GENERATE_EXCEPTIONS, и тогда при не удачном вызове Неар-функция не станет возвращать NULL; вместо этого она возбу дит программное исключение STATUS_NO_MEMORY, перехватываемое с помощью SEH-фрейма.

Чтобы использовать это исключение, напишите код блока try так, будто выделе ние памяти всегда будет успешным; затем — в случае ошибки при выполнении дан ной операции Вы сможете либо обработать исключение в блоке except, либо зас тавить функцию провести очистку, дополнив блок try блоком finally. Очень удобно!

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

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

Возбудить программное исключение несложно — достаточно вызвать функцию RaiseException:

VOTD RaiseException( OWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, CONST ULONG_PTR *pArguments);

Ее первый параметр, dwExceptionCode, — значение, которое идентифицирует ге нерируемое исключение. HeapAlloc передает в нем STATUS_NO_MEMORY. Если Вы определяете собственные идентификаторы исключений, придерживайтесь формата, применяемого для стандартных кодов ошибок в Windows (файл WinError.h) Не забудь те, что каждый такой код представляет собой значение типа DWORD, его поля описа ны в таблице 24-1. Определяя собственные коды исключений, заполните все пять его полей

Второй параметр функции RaiseException — dwExceptionFlags — должен быть либо 0, либо EXCEPTION_NONCONTINUABLE. В принципе этот флаг указывает, может ли фильтр исключений вернуть EXCEPTION_CONTINUE_EXECUTION в ответ на дан ное исключение. Если Вы передаете в этом параметре нулевое значение, фильтр мо жет вернуть EXCEPTION_CONTlNTJE_EXECUTION В нормальной ситуации это заста вило бы поток снова выполнить машинную команду, вызвавшую программное исклю чение Однако Microsoft пошла на некоторые ухищрения, и поток возобновляет вы полнение с оператора, следующего за вызовом RaiseExcepiton

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

Если возбуждается исключение EXCEPTION_NONCONTINUABLE, а фильтр все же возвращает EXCEPTION_CONTINUE EXECUTION, система генерирует новое исключе ние EXCEPTION_NONCONTINUABLE_EXCEPTION.

При обработке программой одного исключения вполне вероятно возбуждение нового исключения И смысл в этом есть. Раз уж мы остановились на этом месте, за мечу, что нарушение доступа к памяти возможно и в блоке finally, и в фильтре исклю чений, и в обработчике исключений. Когда происхидит нечто подобное, система со здает список исключений. Помните функцию GetExceptionlnformation? Она возвращает адрес структуры EXCEPTION_POINTERS Ее элемент ExceptionRecord указывает на струк туру EXCEPTION_RECORD, которая в свою очередь тоже содержит элемент Exception-

Record. Он указывает на другую структуру EXCEPTION_RECORD, где содержится ин формация о предыдущем исключении.

Обычно система единовременно обрабатывает только одно исключение, и эле мент ExceptionRecord равен NULL. Ho если исключение возбуждается при обработке другого исключения, то в первую структуру EXCEPTION_RECORD помещается инфор мация о последнем исключении, а ее элемент ExecptionRecord указывает на аналогич ную структуру с аналогичными данными о предыдущем исключении. Если есть и дру гие необработанные исключения, можно продолжить просмотр этого связанного списка структур EXCEPTION_RECORD, чтобы определить, как обработать конкретное исключение.

Третий и четвертый параметры (nNumberOfArguments и pArguments) функции Raise Exception позволяют передать дополнительные данные о генерируемом исключении Обычно это не нужно, и pArguments передается NULL; тогда RaiseException игнори рует параметр nNumberOfArguments. А если Вы передаете дополнительные аргументы, nNumberOfArguments должен содержать число элементов в массиве типа ULONGPTR, на который указываетр pArguments. Значение nNumberOfArguments не может быть боль ше EXCEPTION_MAXIMUM_PARAMETERS (в файле WinNT.h этот идентификатор опре делен равным 15).

При обработке исключения написанный Вами фильтр — чтобы узнать значения nNumberOfArguments и pArguments — может ссылаться на элементы NumberParameters и Exceptionlnformation структуры EXCEPTION_RECORD

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