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

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

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

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

в очередной раз увидел бы самое известное диалоговое окно Application has stopped working (см. главу 25). Стоит его закрыть — завершится и приложение. Если бы процесс завершился (из-за неправильного доступа к памяти), семафор остался бы занят — соответственно и ожидающие его потоки не получили бы процессорное время. Но вызов ReleaseSemaphore в блоке finally гарантирует освобождение семафора, даже если нарушение доступа к памяти происходит в какой-то другой функции.

Однако, начиная с Windows Vista, разработчики должны явно защищать свои блоки try/finally, гарантируя исполнение блока finally в случае исключения, подробнее см. ниже, а также в следующей главе.

В прежних вызовах Windows исполнение блока finally гарантировалось не для всех типов исключений. Так, если в Windows XP возникало исключение из-за переполнения стека в блоке try, вероятность вызова finally была небольшой, поскольку коду службы WER, функционирующему внутри сбойного процесса, просто не хватит места в стеке, и процесс «молча» завершится. Аналогично, не гарантируется исполнение обработчика завершения в случае исключения, сгенерированного при повреждении SEH-цепочки. То же верно для исключения, возникшего при работе фильтра исключений. Чтобы избежать этого, придерживайтесь простого правила: код в блоках catch и finally должен выполнять минимум действий, иначе процесс просто завершится и никакие блоки finally больше не будут исполнены. По этой причине служба WER в Windows Vista работает в отдельном процессе (см. главу 25).

Раз обработчик завершения — такое мощное средство, способное перехватывать завершение программы из-за неправильного доступа к памяти, можно смело рассчитывать и на то, что оно также перехватит комбинации setjump/longjump и элементарные операторы типа break и continue.

Проверьте себя: FuncaDoodleDoo

Посмотрим, отгадаете ли вы, что именно возвращает следующая функция:

DWORD FuncaDoodleDoo() {

DWORD dwTemp = 0;

while (dwTemp < 10) {

__try {

if (dwTemp == 2) continue;

if (dwTemp == 3) break;

}

__finally { dwTemp++;

}

Глава 23. Обработчики завершения.docx 755

dwTemp++;

}

dwTemp += 10; return(dwTemp);

}

Проанализируем эту функцию шаг за шагом. Сначала dwTemp приравнивается 0. Код в блоке try выполняется, но ни одно из условий в операторах if не дает TRUE, и поток управления естественным образом переходит в блок finally, где dwTemp увеличивается до 1. Затем инструкция после блока finally снова увеличивает значение dwTemp, приравнивая его 2.

На следующей итерации цикла dwTemp равно 2, поэтому выполняется оператор continue в блоке try. Без обработчика завершения, вызывающего принудительное выполнение блока finally перед выходом из try, управление было бы передано непосредственно в начало цикла while, значение dwTemp больше бы не менялось — и мы в бесконечном цикле! В присутствии же обработчика завершения система обнаруживает, что оператор continue приводит к преждевременному выходу из try, и передает управление блоку finally. Значение dwTemp в нем увеличивается до 3, но код за этим блоком не выполняется, так как управление снова передается оператору continue, и мы вновь в начале цикла.

Теперь обрабатываем третий проход цикла. На этот раз значение выражения в первом if равно FALSE, а во втором — TRUE. Система снова перехватывает нашу попытку прервать выполнение блока try и обращается к коду finally. Значение dwTemp увеличивается до 4. Так как выполнен оператор break, выполнение возобновляется после тела цикла. Поэтому код, расположенный за блоком finally (но в теле цикла), не выполняется. Код, расположенный за телом цикла, добавляет 10 к значению dwTemp, что дает в итоге 14, — это и есть результат вызова функции. Даже не стану убеждать вас никогда не писать такой код, как в FuncaDoodleDoo. Я-то включил continue и break в середину кода, только чтобы продемонстрировать поведение обработчика завершения.

Хотя обработчик завершения справляется с большинством ситуаций, в которых выход из блока try был бы преждевременным, он не может вызвать выполнение блока finally при завершении потока или процесса. Вызов ExitThread или ExitProcess сразу завершит поток или процесс — без выполнения какого-либо кода в блоке finally. То же самое будет, если ваш поток или процесс погибнут из-за того, что некая программа вызвала TenninateThread или TerminateProcess. Некоторые функции библиотеки C (вроде abort), в свою очередь вызывающие ExitProcess, тоже исключают выполнение блока finally. Раз вы нс можете запретить другой программе завершение какого-либо из своих потоков или процессов, так хоть сами не делайте преждевременных вызовов ExitThread и ExitProcess.

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

Funcenstein4

Рассмотрим еще один сценарий обработки завершения.

DWORD Funcenstein4() { DWORD dwTemp;

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

...

__try {

//2. Запрашиваем разрешение на доступ

//к защищенным данным, а затем используем их

WaitForSingleObject(g_hSem, INFINITE);

g_dwProtectedData = 5; dwTemp = g_dwProtectedData;

// возвращаем новое значение return(dwTemp);

}

__finally {

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

ReleaseSemaphore(g_hSem, 1, NULL); return(103);

}

//продолжаем что-то делать - этот код

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

dwTemp = 9; return(dwTemp);

}

Блок try в Funcenstein4 пытается вернуть значение переменной dwTemp (5) функции, вызвавшей Funcenstein4. Как мы уже отметили при обсуждении Funcenstein2, попытка преждевременного возврата из блока try приводит к генерации кода, который записывает возвращаемое значение во временную переменную, созданную компилятором. Затем выполняется код в блоке finally. Кстати, в этом варианте Funcenstein2 я добавил в блок finally оператор return. Вопрос: что вернет Funcenstein4 — 5 или 103? Ответ: 103, так как оператор return в блоке finally приведет к записи значения 103 в ту же временную переменную, в которую занесено значение 5. По завершении блока finally текущее значение временной переменной (103) возвращается функции, вызвавшей Funcenstein4.

Итак, обработчики завершения, весьма эффективные при преждевременном выходе из блока try, могут дать нежелательные результаты именно потому, что предотвращают досрочный выход из блока try. Лучше всего избегать любых операторов, способных вызвать преждевременный выход из блока try обработчика завершения. А в идеале — удалить все операторы return, continue,

Глава 23. Обработчики завершения.docx 757

break, goto (и им подобные) как из блоков try, так и из блоков finally. Тогда компилятор сгенерирует код и более компактный (перехватывать преждевременные выходы из блоков try не понадобится), и более быстрый (на локальную раскрутку потребуется меньше машинных команд). Да и читать ваш код будет гораздо легче.

Funcarama1

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

BOOL Funcarama1() {

HANDLE hFile = INVALID_HANDLE_VALUE;

PV0ID pvBuf = NULL;

DWORD dwNumBytesRead;

BOOL bOk;

hFile = CreateFile(TEXT("SOMEDATA.DAT"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

if (hFile == INVALID.HANOLE.VALUE) { return(FALSE);

}

pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (pvBuf == NULL) {

CloseHandle(hFile);

return(FALSE);

}

bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (!bOk || (dwNumBytesRead == 0)) {

VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile);

return(FALSE);

)

//что-то делаем с данными

...

//очистка всех ресурсов

VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile);

return(TRUE);

}

Проверки ошибок в функции Funcarama1 затрудняют чтение ее текста, что усложняет ее понимание, сопровождение и модификацию.

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

Funcarama2

Конечно, можно переписать Funcarama1 так, чтобы она стала яснее:

BOOL Funcararoa2() {

HANDLE hFile = INVALID_HANDLE_VALUE;

PVOID pvBuf = NULL;

DWORD dwNumBytesRead;

BOOL bOk, bSuccess = FALSE;

hFile = CreateFile(TEXT("SOHEDATA.DAT"), GENERIC_READ, FILE_SHARE_READ,

NULL, OPEN_EXISTING, 0, NULL);

if (hFile != INVALID_HANDLE_VALUE) {

pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (pvBuf != NULL) {

bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (bOk && (dwNumBytesRead != 0)) {

// что-то делаем с данными

...

bSuccess = TRUE;

}

VirtualFree(pvBuf, MEM_RELEASE | NEM_DECOMMIT);

}

CloseHandle(hFile);

}

return(bSuccess);

}

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

Funcarama3

Перепишем-ка еще раз первый вариант (Funcarama1), задействовав преимущества обработки завершения:

DWORD Funcarama3() {

// Внимание: инициализируйте все переменные, предполагая худшее

HANDLE hFile = INVALID_HANDLE_VALUE; PV0ID pvBuf = NULL;

__try {

DWORD dwNumBytesRead; B00L bOk;

Глава 23. Обработчики завершения.docx 759

hFile = CreateFile(TEXT("SOMEDATA. DAT"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

if (hFile == INVALID_HANDLE_VALUE) { return(FALSE);

}

pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PA6E_READWRITE); if (pvBuf == NULL) {

return(FALSE);

}

bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (!bOk || (dwNuffiBytesRead != 1024)) {

return(FALSE);

}

// что-то делаем с данными

...

}

__finally {

// очистка всех ресурсов if (pvBuf != NULL)

VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE)

CloseHandle(hFile);

}

// продолжаем что-то делать return(TRUE);

}

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

Funcarama4: последний рубеж

Настоящая проблема в Funcarama3 — расплата за изящество. Я уже говорил: избегайте по возможности операторов return внутри блока try.

Чтобы облегчить последнюю задачу, Майкрософт ввела еще одно ключевое слово в свой компилятор С++: leave. Вот новая версия (Funcarama4), построенная на применении нового ключевого слова:

DWORD Funcarama4() {

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

//Внимание: инициализируйте все переменные, предполагая худшее

HANDLE hFile = INVALID_HANDLE_VALUE; PVOID pvBuf = NULL;

//предполагаем, что выполнение функции будет неудачным

BOOL bFunctionOk = FALSE;

__try {

DWORD dwNumBytesRead; BOOL bOk;

hFile = CreateFile(TEXT("SOHEDATA.DAT"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

if (hFile == INVALID_HANDLE_VALUE) { __leave;

}

pvBuf = VirtualAlloc(NULL. 1024, MEM_COMMIT, PAGE_READWRITE);

if (pvBuf == NULL) { __leave;

}

bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL); if (!bOk || (dwNumBytesRead » 0)) {

__leave;

}

//что-то делаем с данными

...

//функция выполнена успешно bFunctionOk = TRUE;

}

__finally {

// очистка всех ресурсов if (pvBuf != NULL)

VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE)

CloseHandle(hFile);

}

// продолжаем что-то делать return(bFunctionOk);

}

Ключевое слово __leave в блоке try вызывает переход в конец этого блока. Можете рассматривать это как переход на закрывающую фигурную скобку блока try. И никаких неприятностей это не сулит, потому что выход из блока

Глава 23. Обработчики завершения.docx 761

try и вход в блок finally происходит естественным образом. Правда, нужно ввести дополнительную булеву переменную fFunctionOk, сообщающую о завершении функции: удачно оно или нет. Но это дает минимальные издержки.

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

И еще о блоке finally

Пока нам с вами удалось четко выделить только два сценария, которые приводят к выполнению блока finally:

нормальная передача управления от блока try блоку finally;

локальная раскрутка — преждевременный выход из блока try (из-за операторов goto, longjump, continue, break, return и т. д.), вызывающий принудительную пе-

редачу управления блоку finally.

Третий сценарий — глобальная раскрутка (global unwind) — протекает не

столь выраженно. Вспомним Funcfurter1. Ее блок try содержал вызов функции Funcinator. При неверном доступе к памяти в Funcinator глобальная раскрутка приводила к выполнению блока finally в Funcfurter1. Но подробнее о глобальной раскрутке мы поговорим в следующих главах.

Выполнение кода в блоке finally всегда начинается в результате возникновения одной из этих трех ситуаций. Чтобы определить, какая из них вызвала выполнение блока finally, вызовите встраиваемую функцию1 AbnormalTermination:

BOOL AbnormalTermination();

Примечание. Встраиваемые функции (intrinsic function) — это особые функции, распознаваемые компилятором. Обнаружив вызов такой функции, компилятор, вместо того, чтобы, как обычно, генерировать код для вызова, встраивает код этой функции. Примером встраиваемой функции может быть тетсру (если для компилятора задан ключ /Oi). Обнаружив вызов тетсру, компилятор встраивает эту функцию прямо в код тетсру вместо, собственно, ее вызова. Как правило, это повышает быстродействие, но увеличивает размер кода.

Встраиваемая функция AbnormalTermination отличается от тетсру тем, что существует только во встроенном состоянии, в библиотеке С/С++ этой функции нет.

Ее можно вызвать только из блока finally; она возвращает булево значение, которое сообщает, был ли преждевременный выход из блока try, связан-

1

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

ного с данным блоком finally. Иначе говоря, если управление естественным образом передано из try в finally, AbnormalTermination возвращает FALSE. А если выход был преждевременным — обычно либо из-за локальной раскрутки, вызванной оператором goto, return, break или continue, либо из-за глобальной раскрутки, вызванной нарушением доступа к памяти, — то вызов AbnormalTermination дает TRUE. Но, когда она возвращает TRUE, различить, вызвано выполнение блока finally глобальной или локальной раскруткой, нельзя. Впрочем, это не проблема, так как вы должны избегать кода, приводящего к локальной раскрутке.

Funcfurter2

Следующий фрагмент демонстрирует использование встраиваемой функции AbnormalTermination:

DWORD Funcfurter2() {

DWORD dwTemp;

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

...

__try {

//2. Запрашиваем разрешение на доступ

//к защищенным данным, а затем используем их

WaitForSingleObject(g_hSem, INFINITE);

dwTemp = Funcinator(g_dwProtectedData);

}

__finally {

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

ReleaseSemaphore(g_hSem, 1, NULL);

if (!AbnormalTermination()) {

//в блоке try не было ошибок – управление

//передано в блок finally естественным образом

...

}else {

//что-то вызвало исключение, и, так как в блоке try

//нет кода, который мог бы вызвать преждевременный

//выход, блок finally выполняется из-за глобальной

//раскрутки

//если бы в блоке try был оператор goto, мы бы

//не узнали, как попали сюда

...

}

}

Глава 23. Обработчики завершения.docx 763

// 4. Продолжаем что-то делать return(dwTemp);

}

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

Упрощается обработка ошибок — очистка гарантируется и проводится в одном месте.

Улучшается восприятие текста программ.

Облегчается сопровождение кода.

Удается добиться минимальных издержек по скорости и размеру кода — при условии правильного применения обработчиков.

Программа-пример SEHTerm

Эта программа (23-SEHTerm.exe), демонстрирует обработчики завершения. Файлы исходного кода и ресурсов этой программы находятся в каталоге 23-SEHTerm внутри архива, доступного на сайте поддержки этой книги.

После запуска SEHTerm ее первичный поток входит в блок try. Из него открывается следующее окно.

В этом окне предлагается обратиться к памяти по недопустимому адресу. (Большинство приложений не столь тактично — они обращаются по недопустимым адресам, никого не спрашивая.) Давайте обсудим, что случится, если вы щелкнете кнопку Yes. В этом случае поток попытается записать значение 5 по нулевому адресу памяти. Запись по нулевому адресу всегда вызывает исключение, связанное с нарушением доступа. А когда поток возбуждает такое исключение, Windows выводит окно, показанное ниже.

Заметьте, что это сообщение говорит об успешном исполнении кода блоке try. Если закрыть сообщение, поток покинет блок finally и покажет следующее сообщение:

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