Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf
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.
