- •WINDOWS
- •Джеффри Рихтер
- •ЧАCTЬ I МАТЕРИАЛЫ ДЛЯ ОБЯЗАТЕЛЬНОГО ЧТЕНИЯ
- •ГЛАВА 1. Обработка ошибок
- •Вы тоже можете это сделать
- •Программа-пример ErrorShow
- •ГЛАВА 2 Unicode
- •Наборы символов
- •Одно- и двухбайтовые наборы символов
- •Unicode: набор широких символов
- •Почему Unicode?
- •Windows 2000 и Unicode
- •Windows 98 и Unicode
- •Windows CE и Unicode
- •В чью пользу счет?
- •Unicode и СОМ
- •Как писать программу с использованием Unicode
- •Unicode и библиотека С
- •Типы данных, определенные в Windows для Unicode
- •Unicode- и ANSI-функции в Windows
- •Строковые функции Windows
- •Ресурсы
- •Текстовые файлы
- •Перекодировка строк из Unicode в ANSI и обратно
- •ГЛАВА 3 Объекты ядра
- •Что такое объект ядра
- •Учет пользователей объектов ядра
- •Защита
- •Таблица описателей объектов ядра
- •Создание объекта ядра
- •Закрытие объекта ядра
- •Совместное использование объектов ядра несколькими процессами
- •Наследование описателя объекта
- •Изменение флагов описателя
- •Именованные объекты
- •Пространства имен Terminal Server
- •Дублирование описателей объектов
- •ЧАСТЬ II НАЧИНАЕМ РАБОТАТЬ
- •ГЛАВА 4 Процессы
- •Ваше первое Windows-приложение
- •Описатель экземпляра процесса
- •Описатель предыдущего экземпляра процесса
- •Командная строка процесса
- •Переменные окружения
- •Привязка к процессорам
- •Режим обработки ошибок
- •Текущие диск и каталог для процесса
- •Текущие каталоги для процесса
- •Определение версии системы
- •Функция CreateProcess
- •Параметры pszApplicationName и pszCommandLine
- •Параметры psaProcess, psaThread и blnheritHandles
- •Параметр fdwCreate
- •Параметр pvEnvironment
- •Параметр pszCurDir
- •Параметр psiStartlnfo
- •Параметр ppiProclnfo
- •Завершение процесса
- •Возврат управления входной функцией первичного потока
- •Функция ExitProcess
- •Функция TerminateProcess
- •Когда все потоки процесса уходят
- •Что происходит при завершении процесса
- •Дочерние процессы
- •Запуск обособленных дочерних процессов
- •Перечисление процессов, выполняемых в системе
- •Программа-пример Processlnfo
- •ГЛАВА 5 Задания
- •Определение ограничений, налагаемых на процессы в задании
- •Включение процесса в задание
- •Завершение всех процессов в задании
- •Получение статистической информации о задании
- •Уведомления заданий
- •Программа-пример JobLab
- •ГЛАВА 6 Базовые сведения о потоках
- •В каких случаях потоки создаются
- •И в каких случаях потоки не создаются
- •Ваша первая функция потока
- •Функция CreateThread
- •Параметр psa
- •Параметр cbStack
- •Параметры pfnStartAddr и pvParam
- •Параметр fdwCreate
- •Параметр pdwThreadlD
- •Завершение потока
- •Возврат управления функцией потока
- •Функция ExitThread
- •Функция TerminateThread
- •Если завершается процесс
- •Что происходит при завершении потока
- •Кое-что о внутреннем устройстве потока
- •Некоторые соображения по библиотеке С/С++
- •Ой, вместо _beginthreadex я по ошибке вызвал CreateThread
- •Библиотечные функции, которые лучше не вызывать
- •Как узнать о себе
- •Преобразование псевдоописателя в настоящий описатель
- •ГЛАВА 7 Планирование потоков, приоритет и привязка к процессорам
- •Приостановка и возобновление потоков
- •Приостановка и возобновление процессов
- •Функция Sleep
- •Переключение потоков
- •Определение периодов выполнения потока
- •Структура CONTEXT
- •Приоритеты потоков
- •Абстрагирование приоритетов
- •Программирование приоритетов
- •Динамическое изменение уровня приоритета потока
- •Подстройка планировщика для активного процесса
- •Программа-пример Scheduling Lab
- •Привязка потоков к процессорам
- •ГЛАВА 8 Синхронизация потоков в пользовательском режиме
- •Кэш-линии
- •Более сложные методы синхронизации потоков
- •Худшее, что можно сделать
- •Критические секции
- •Критические секции: важное дополнение
- •Критические секции и спин-блокировка
- •Критические секции и обработка ошибок
- •Несколько полезных приемов
- •Не занимайте критические секции надолго
- •ГЛАВА 9 Синхронизация потоков с использованием объектов ядра
- •Wait-функции
- •Побочные эффекты успешного ожидания
- •События
- •Программа-пример Handshake
- •Ожидаемые таймеры
- •Ожидаемые таймеры и АРС-очередь
- •И еще кое-что о таймерах
- •Семафоры
- •Мьютексы
- •Отказ от объекта-мьютекса
- •Мьютексы и критические секции
- •Программа-пример Queue
- •Сводная таблица объектов, используемых для синхронизации потоков
- •Другие функции, применяемые в синхронизации потоков
- •Асинхронный ввод-вывод на устройствах
- •Функция WaitForlnputldle
- •Функция MsgWaitForMultipleObjects(Ex)
- •Функция WaitForDebugEvent
- •Функция SignalObjectAndWait
- •ГЛАВА 10 Полезные средства для синхронизации потоков
- •Реализация критической секции: объект-оптекс
- •Программа-пример Optex
- •Создание инверсных семафоров и типов данных, безопасных в многопоточной среде
- •Программа-пример lnterlockedType
- •Синхронизация в сценарии "один писатель/группа читателей"
- •Программа-пример SWMRG
- •Реализация функции WaitForMultipleExpressions
- •Программа-пример WaitForMultExp
- •ГЛАВА 11 Пулы потоков
- •Сценарий 1: асинхронный вызов функций
- •Сценарий 2: вызов функций через определенные интервалы времени
- •Программа-пример TimedMsgBox
- •Сценарий 3: вызов функций при освобождении отдельных объектов ядра
- •Сценарий 4; вызов функций по завершении запросов на асинхронный ввод-вывод
- •ГЛАВА 12 Волокна
- •Работа с волокнами
- •Программа-пример Counter
- •ЧАСТЬ III УПРАВЛЕНИЕ ПАМЯТЬЮ
- •Виртуальное адресное пространство процесса
- •Как адресное пространство разбивается на разделы
- •Увеличение раздела для кода и данных пользовательского режима до 3 Гб на процессорах x86 (только Windows 2000)
- •Закрытый раздел размером 64 Кб (только Windows 2000)
- •Раздел для общих MMF (только Windows 98)
- •Регионы в адресном пространстве
- •Передача региону физической памяти
- •Физическая память и страничный файл
- •Физическая память в страничном файле не хранится
- •Атрибуты защиты
- •Защита типа «копирование при записи»
- •Специальные флаги атрибутов защиты
- •Подводя итоги
- •Блоки внутри регионов
- •Особенности адресного пространства в Windows 98
- •Выравнивание данных
- •ГЛАВА 14 Исследование виртуальной памяти
- •Системная информация
- •Программа-пример Syslnfo
- •Статус виртуальной памяти
- •Программа-пример VMStat
- •Определение состояния адресного пространства
- •Функция VMQuery
- •Программа-пример VMMap
- •ГЛАВА 15 Использование виртуальной памяти в приложениях
- •Резервирование региона в адресном пространстве
- •Передача памяти зарезервированному региону
- •Резервирование региона с одновременной передачей физической памяти
- •В какой момент региону передают физическую память
- •Возврат физической памяти и освобождение региона
- •В какой момент физическую память возвращают системе
- •Программа-пример VMAIloc
- •Изменение атрибутов защиты
- •Сброс содержимого физической памяти
- •Программа-пример MemReset
- •Механизм Address Windowing Extensions (только Windows 2000)
- •Программа-пример AWE
- •ГЛАВА 16 Стек потока
- •Стек потока в Windows 98
- •Функция из библиотеки С/С++ для контроля стека
- •Программа-пример Summation
- •ГЛАВА 17 Проецируемые в память файлы
- •Проецирование в память EXE- и DLL-файлов
- •Статические данные не разделяются несколькими экземплярами EXE или DLL
- •Программа-пример Applnst
- •Файлы данных, проецируемые в память
- •Метод 1: один файл, один буфер
- •Метод 2: два файла, один буфер
- •Метод 3: один файл, два буфера
- •Метод 4: один файл и никаких буферов
- •Использование проецируемых в память файлов
- •Этап1: создание или открытие объекта ядра «файл»
- •Этап 2: создание объекта ядра «проекция файла»
- •Этап 3: проецирование файловых данных на адресное пространство процесса
- •Этап 4: отключение файла данных от адресного пространства процесса
- •Этапы 5 и 6: закрытие объектов «проекция файла» и «файл»
- •Программа-пример FileRev
- •Обработка больших файлов
- •Проецируемые файлы и когерентность
- •Базовый адрес файла, проецируемого в память
- •Особенности проецирования файлов на разных платформах
- •Совместный доступ процессов к данным через механизм проецирования
- •Файлы, проецируемые на физическую память из страничного файла
- •Программа-пример MMFShare
- •Частичная передача физической памяти проецируемым файлам
- •Программа-пример MMFSparse
- •ГЛАВА 18 Динамически распределяемая память
- •Стандартная куча процесса
- •Дополнительные кучи в процессе
- •Защита компонентов
- •Более эффективное управление памятью
- •Локальный доступ
- •Исключение издержек, связанных с синхронизацией потоков
- •Быстрое освобождение всей памяти в куче
- •Создание дополнительной кучи
- •Выделение блока памяти из кучи
- •Изменение размера блока
- •Определение размера блока
- •Освобождение блока
- •Уничтожение кучи
- •Использование куч в программах на С++
- •Другие функции управления кучами
- •ЧАСТЬ IV ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
- •ГЛАВА 19 DLL: основы
- •DLL и адресное пространство процесса
- •Общая картина
- •Создание DLL-модуля
- •Что такое экспорт
- •Создание DLL для использования с другими средствами разработки (отличными от Visual C++)
- •Создание ЕХЕ-модуля
- •Что такое импорт
- •Выполнение ЕХЕ-модуля
- •ГЛАВА 20 DLL: более сложные методы программирования
- •Явная загрузка DLL и связывание идентификаторов
- •Явная загрузка DLL
- •Явная выгрузка DLL
- •Явное подключение экспортируемого идентификатора
- •Функция входа/выхода
- •Уведомление DLL_PROCESS_ATTACH
- •Уведомление DLL_PROCESS_DETACH
- •Уведомление DLL_THREAD_ATTACH
- •Уведомление DLL_THREAD_DETACH
- •Как система упорядочивает вызовы DIIMain
- •Функция DllMain и библиотека С/С++
- •Отложенная загрузка DLL
- •Программа-пример DelayLoadApp
- •Переадресация вызовов функций
- •Известные DLL
- •Перенаправление DLL
- •Модификация базовых адресов модулей
- •Связывание модулей
- •ГЛАВА 21 Локальная память потока
- •Динамическая локальная память потока
- •Использование динамической TLS
- •Статическая локальная память потока
- •Пример внедрения DLL
- •Внедрение DLL c использованием реестра
- •Внедрение DLL с помощью ловушек
- •Утилита для сохранения позиций элементов на рабочем столе
- •Внедрение DLL с помощью удаленных потоков
- •Программа-пример lnjLib
- •Библиотека lmgWalk.dll
- •Внедрение троянской DLL
- •Внедрение DLL как отладчика
- •Внедрение кода в среде Windows 98 через проецируемый в память файл
- •Внедрение кода через функцию CreateProcess
- •Перехват API-вызовов: пример
- •Перехват API-вызовов подменой кода
- •Перехват API-вызовов с использованием раздела импорта
- •Программа-пример LastMsgBoxlnfo
- •ЧАСТЬ V СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ
- •ГЛАВА 23 Обработчики завершения
- •Примеры использования обработчиков завершения
- •Funcenstein1
- •Funcenstein2
- •Funcenstein3
- •Funcfurter1
- •Проверьте себя: FuncaDoodleDoo
- •Funcenstein4
- •Funcarama1
- •Funcarama2
- •Funcarama3
- •Funcarama4: последний рубеж
- •И еще о блоке finally
- •Funcfurter2
- •Программа-пример SEHTerm
- •ГЛАВА 24 Фильтры и обработчики исключений
- •Примеры использования фильтров и обработчиков исключений
- •Funcmeister1
- •Funcmeister2
- •EXCEPTION_EXECUTE_HANDLER
- •Некоторые полезные примеры
- •Глобальная раскрутка
- •Остановка глобальной раскрутки
- •EXCEPTION_CONTINUE_EXECUTION
- •Будьте осторожны с EXCEPTION_CONTINUE_EXECUTION
- •EXCEPTION_CONTINUE_SEARCH
- •Функция GetExceptionCode
- •Функция GetExceptionlnformation
- •Программные исключения
- •ГЛАВА 25 Необработанные исключения и исключения С++
- •Отладка по запросу
- •Отключение вывода сообщений об исключении
- •Принудительное завершение процесса
- •Создание оболочки вокруг функции потока
- •Создание оболочки вокруг всех функций потоков
- •Автоматический вызов отладчика
- •Явный вызов функции UnhandledExceptionFilter
- •Функция UnhandledExceptionFilter изнутри
- •Исключения и отладчик
- •Программа-пример Spreadsheet
- •Исключения С++ и структурные исключения
- •Перехват структурных исключений в С++
- •ЧАСТЬ VI ОПЕРАЦИИ С ОКНАМИ
- •ГЛАВА 26 Оконные сообщения
- •Очередь сообщений потока
- •Посылка асинхронных сообщений в очередь потока
- •Посылка синхронных сообщений окну
- •Пробуждение потока
- •Флаги состояния очереди
- •Алгоритм выборки сообщений из очереди потока
- •Пробуждение потока с использованием объектов ядра или флагов состояния очереди
- •Передача данных через сообщения
- •Программа-пример CopyData
- •ГЛАВА 27 Модель аппаратного ввода и локальное состояние ввода
- •Поток необработанного ввода
- •Локальное состояние ввода
- •Ввод с клавиатуры и фокус
- •Управление курсором мыши
- •Подключение к очередям виртуального ввода и переменным локального состояния ввода
- •Программа-пример LISLab
- •Программа-пример LISWatch
исключении, оно должно дать один из трех идентификаторов, оп ределенных в заголовочном 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);
