
- •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

пользователь новое суммирование, и функция Sum переполнила бы стек, а соответствующее исключение не было бы возбуждено, Вместо этого возникло бы исключение «нарушение доступа", и корректно обработать эту ситуацию уже не удалось бы.
И последнее, почему я использую отдельный поток: физическую память, отведен ную под его стек, можно освободить. Рассмотрим такой сценарий: пользователь про сит функцию Sum вычислить сумму целых чисел от 0 до 30 000. Это требует передачи региону стека весьма ощутимого объема памяти. Затем пользователь проводит не
сколько операций суммирования — максимум до 5000 И окажется, что стеку передан порядочный объем памяти, который больше не используется А ведь эта физическая память выделяется из страничною файла Так что лучше бы освободить се и вернуть системе И поскольку программа завершает поток SumThreadFunc, система автомати чески освобождает физическую память, переданную региону стека
ГЛАВА 17 Проецируемые в память файлы
Операции с файлами — это то, что рапо или поздно приходится делать практичес ки во всех программах, и всегда это вызывает массу проблем. Должно ли приложение просто открыть файл, считать и закрыть его, или открыть, считать фрагмент в буфер и перезаписать его в другую часть файла? В Windows многие из этих проблем реша ются очень изящно — с помощью проецируемых в память файлов (memory-mapped files)
Как и виртуальная память, проецируемые файлы позволяют резервировать реги он адресного пространства и передавать ему физическую память. Различие между этими механизмами состоит в том, что в последнем случае физическая память не выделяется из страничного файла, а берется из файла, уже находящегося на диске. Как только файл спроецирован в память, к нему можно обращаться так, будто он цели ком в нее загружен.
Проецируемые файлы применяются для:
загрузки и выполнения EXE- и DLL-файлов Это позволяет существенно эконо мить как на размере страничного файла, так и на времени, необходимом для подготовки приложения к выполнению, доступа к файлу данных, размещенному на диске Это позволяет обойтись без
операций файлового ввода-вывода и буферизации его содержимого, разделения данных между несколькими процессами, выполняемыми па одной
машине (В Windows есть и другие методы для совместного доступа разных процессов к одним данным — но все они так или иначе реализованы на осно ве проецируемых в память файлов.)
Эти области применения проецируемых файлов мы и рассмотрим в данной главе.
Проецирование в память EXE- и DLL-файлов
При вызове из потока функции CreateProcess система действует так:
1.Отыскивает ЕХЕ-файл, указанный при вызове CreateProcess. Если файл не най ден, новый процесс не создастся, а функция возвращает FALSE.
2.Создает новый объект ядра «процесс»
3.Создает адресное пространство нового процесса
4.Резервирует регион адресного пространства — такой, чтобы в него поместил ся данный ЕХЕ-файл Желательное расположение этого региона указывается внут ри самого ЕХЕ-файла По умолчанию базовый адрес ЕХЕ-файла — 0x00400000 (в 64разрядном приложении под управлением 64-разрядпой Windows 2000 этот адрес может быть другим). При создании исполняемого файла приложе ния базовый адрес может быть изменен через параметр компоновщика /BASE.
5.Отмечает, что физическая память, связанная с зарезервированным регионом, — ЕХЕ-файл на диске, а нс страничный файл.
Спроецировав ЕХЕ-файл на адресное пространство процесса, система обращает ся к разделу ЕХЕ-файла со списком DLL, содержащих необходимые программе функ ции. После этого система, вызывая LoadLibrary, поочередно загружает указанные (а при необходимости и дополнительные) DLL-модули. Всякий раз, когда для загрузки DLL вызывается LoadLibrary, система выполняет действия, аналогичные описанным выще в пп. 4 и 5:
1.Резервирует регион адресного пространства - такой, чтобы в него мог поме ститься заданный DLL-файл Желательное расположение этого региона указы вается внутри самого DLL-файла. По умолчанию Microsoft Visual C++ присваи вает DLLмодулям базовый адрес 0x10000000 (в 64-разрядной DLL под управ лением 64разрядной Windows 2000 этот адрес может быть другим). При ком поновке DLL это значение можно изменить с помощью параметра /BASE. У всех стандартных системных DLL, поставляемых с Windows, разные базовые здре ca, чтобы не допустить их перекрытия при загрузке в одно адресное простран ство
2.Если зарезервировать регион по желательному для DLL базовому адресу не удается (из-за того, что он слишком мал либо занят каким-то еще EXEили DLL файлом), система пытается найти другой регион. Но по двум причинам такая ситуация весьма неприятна. Во-первых, если в DLL нет информации о возмож ной переадресации (relocation information), загрузка может вообще не полу читься. (Такую информацию можно удалить из DLL при компоновке с парамет ром /FIXED. Это уменьшит размер DLL-файла, но тогда модуль должен грузить ся только по указанному базовому адресу) Во-вторых, системе приходится выполнять модификацию адресов (relocations) внутри DLL. В Windowы 98 эта операция осуществляется по мере подкачки сграниц в оперативную память. Но в Windows 2000 на это уходит дополнительная физическая память, выделяе мая из страничного файла, да и загрузка такой DLL займет больше времени.
3.Отмечает, что физическая память, связанная с зарезервированным регионом, — DLL-файл на диске, а не страничный файл. Если Windows 2000 пришлось вы полнять модификацию адресов из-за того, что DLL не удалось загрузить по желательному базовому адресу, она запоминает, что часть физической памяти для DLL связана со страничным файлом.
Если система почему-либо не свяжет ЕХЕ-файл с необходимыми сму DLL, на эк ране появится соответствующее сообщение, а адресное пространство процесса и объект «процесс" будут освобождены При этом CreateProcess вернет FALSE; прояснить причину сбоя поможет функция GetLastError.

После увязки EXE- и DLL-файлов с адресным пространством процесса начинает исполняться стартовый код EXE-файла. Подкачку страниц, буферизацию и кэширо вание система берет на себя. Например, если код в ЕХЕ-файле переходит к команде, не загруженной в память, возникает ошибка. Обнаружив ее, система перекачивает нужную страницу кода из образа файла на страницу оперативной памяти. Затем ото бражает страницу оперативной памяти на должный участок адресного пространства процесса, тем самым позволяя потоку продолжить выполнение кода Все эти опера ции скрыты от приложения и периодически повторяются при каждой попытке про цесса обратиться к коду или данным, отсутствующим в оперативной памяти.
Статические данные не разделяются несколькими экземплярами EXE или DLL
Когда Вы создаете новый процесс для уже выполняемого приложения, система про сти открывает другое проецируемое в память представление (view) объекта "проек ция файла" (file-mapping object), идентифицирующего образ исполняемого файла, и создает новые объекты "процесс" и «поток» (для первичного потока) Этим объектам присваиваются идентификаторы процесса и потока. С помощью проецируемых в память файлов несколько одновременно выполняемых экземпляров приложения мо жет совместно использовать один и тот же код, загруженный в оперативную память. Здесь возникает небольшая проблема. Процессы используют линейное (flat) ад ресное пространство. При компиляции и компоновке программы весь ее код и дан ные объединяются в нечто, так сказать, большое и цельное Данные, конечно, отделе ны от кода, но только в том смысле, что они расположены вслед за кодом в ЕХЕ-фай ле<snoska На самом деле содержимое файла разбито на отдельные разделы (sections). Код находится в одном разделе, а глобальные переменные — в другом Разделы выравниваются по грани цам страниц Приложение определяет размер страницы через функцию GetSystemInfo. В EXEили DLLфлйле раздел кода обычно предшествует разделу данных.
>. Вот упрощенная иллюстрация того, как код и данные приложения загружаются в виртуальную память, а затем отображаются на адресное пространство процесса:
Теперь допустим, что запущен второй экземпляр программы. Система просто-на просто проецирует страницы виртуальной памяти, содержащие код и данные файла, на адресное пространство второго экземпляра приложения:

Если один экземпляр приложения модифицирует какие-либо глобальные перемен ные, размещенные на странице данных, содержимое памяти изменяется для всех эк земпляров этого приложения. Такое изменение могло бы привести к катастрофичес ким последствиям и поэтому недопустимо.
Система предотвращает подобные ситуации, применяя механизм копирования при записи. Всякий раз, когда программа пытается записывать что-то в файл, спрое цированный в память, система перехватывает эту попытку, выделяет новый блок па мяти, копирует в него нужную программе страницу и после этого разрешает запись в новый блок памяти. Благодаря этому работа остальных экземпляров программы пе нарушается. Вот что получится, когда первый экземпляр программы попытается из менить какую-нибудь глобальную переменную на второй странице данных:
Система выделяет новую страницу и копирует на нее содержимое страницы дан ных 2. Адресное пространство первого экземпляра изменяется так, чтобы отобразить новую страницу данных на тот же участок, что и исходную. Теперь процесс может изменить глобальную переменную, не затрагивая данные другого экземпляра .
Аналогичная цепочка событий происходит и при отладке приложения. Например, запустив несколько экземпляров программы, Вы хотите отладить только один из них. Вызвав отладчик, Вы ставите в строке исходного кода точку прерывания. Отладчик модифицирует Ваш код, заменяя одну из команд на языке ассемблера другой — зас тавляющей активизировать сам отладчик. И здесь Вы сталкиваетесь с той же пробле мой. После модификации кода все экземпляры программы, доходя до исполнения измененной команды, приводили бы к его активизации. Чтобы этого избежать, сис тема вновь использует копирование при записи. Обнаружив попытку отладчика из менить код, она

выделяет новый блок памяти, копирует туда нужную страницу и по зволяет отладчику модифицировать код на этой копии.
WINDOWS 98
При загрузке процесса система просматривает все страницы образа файла. Физическая память из страничного файла передается сразу только тем стра ницам, которые должны быть защищены атрибутом копирования при записи. При обращении к такому участку образа файла в память загружается соответ ствующая страница. Если cc модификации не происходит, она может быть выгружена из памяти и при необходимости загружена вновь. Если же страни ца файла модифицируется, система перекачивает ее на одну из ранее передан ных страниц в страничном файле.
Поведение Windows 2000 и Windows 98 в подобных случаях одинаково, кроме ситуации, когда в память загружено два экземпляра одного модуля и никаких данных не изменено. Тогда процессы под управлением Windows 2000 могут совместно использовать данные, а в Windows 98 каждый процесс полу чает свою копию этих данных. Но если в память загружен лишь один экземп ляр модуля или же данные были модифицированы (что чаще всего и бывает), Windows 2000 и Windows 98 ведут себя одинаково.
Статические данные разделяются несколькими экземплярами
EXE или DLL
По умолчанию для большей безопасности глобальные и статические данные нс разделя ются несколькими проекциями одного и того же EXE или DLL. Но иногда удобнее, чтобы несколько проекций EXE разделяли единственный экземпляр переменной. Например, в Windows не так-то просто определить, запущено ли несколько экземп ляров приложения. Если бы у Вас была переменная, доступная всем экземплярам при ложения, она могла бы отражать число этих экземпляров Тогда при запуске нового экземпляра приложения его поток просто проверил бы значение глобальной пере менной (обновленное другим экземпляром приложения) и, будь оно больше 1, сооб щил бы пользователю, что запустить можно лишь один экземпляр; после чего эта копия приложения была бы завершена.
В этом разделе мы рассмотрим метод, обеспечивающий совместное использова ние переменных всеми экземплярами EXE или DLL. Но сначала Вам понадобятся кое какие базовые сведения.
Любой образ EXEили DLL-файла состоит из группы разделов. По соглашению имя каждого стандартного раздела начинается с точки Например, при компиляции про граммы весь код помещается в раздел .text, неинициализированные данные - в раз дел .bss, а инициализированные — в раздел .data.
С каждым разделом связана одна из комбинаций атрибутов, перечисленных в сле дующей таблице.
Атрибут |
Описание |
|
|
READ |
Разрешает чтение из раздела |
|
|
WRITE |
Разрешает запись в раздел |
|
|
EXECUTЕ |
Содержимое раздела можно исполнять |
|
|
SHARED |
Раздел доступен нескольким экземплярам |
|
приложения (этот атрибут отклю чает механизм |
|
|

копирования при записи)
Запустив утилиту DumpBin из Microsoft Visual Studio (c ключом /Headers), Вы уви дите список разделов в файле образа EXE или DLL Пример такого списка, показан ный ниже, относится к ЕХЕ-файлу.
SECTION HEADER #1 text name 11A70 virtual size 1000 virtual address 12000 size of raw data 1000 file pointer to raw data
0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 60000020 flags Code Execute Read
SECTION HEADER #2 rdata name
1F6 virtual size
13000 virtual address 1000 size of raw data 13000 file pointer to raw data
0 file poinLer lo relocation tabie 0 file pointer to line numbers 0 number ot relocations 0 number of line numbers 40000040 flags
Initialized Data Read Only
SECTION HEADER #3 .data name
560 virtual size 14000 virtual address 1000 size of raw data 14000 file pointer to raw data
0 filc pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags
Initialized Data Read Write
SECTION HtADER #4 .idata name
58D virtual size 15000 virtual address 1000 size of raw data 15000 file pointer to raw data
0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags
Initialized Data Read Write
SECTION HEADER #5 .didat name
7A2 vi rtual size 16000 virtual address 1000 size of raw data 16000 file pointer to raw data
0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags
Initialized Data Read Write

SECTION HEADER #6 .reloc name
26D virtual size 17000 virtual address 1000 size of raw data 17000 file pointer to raw data
0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 42000040 flags
Initialized Data Discardable Read Only
Summary
1000 data 1000 didat 1000 idata 1000 rdata 1000 .reloc 12000 text
Некоторые из часто встречающихся разделов перечислены в таблице ниже
Имя раздела |
Описание |
|
|
|
|
bss |
Неинициализированные данные |
|
|
|
|
CRT |
Неизменяемые данные библиотеки С |
|
|
|
|
data |
Инициализированные данные |
|
|
|
|
.debug |
Отладочная информация |
|
|
|
|
.didat |
Таблица имен для отложенного импорта (delay imported names table) |
|
|
|
|
edata |
Таблица экспортируемых имен |
|
|
|
|
idata |
Таблица импортируемых имен |
|
|
|
|
.rdata |
Неизменяемые данные периода выполнения |
|
|
|
|
.reloc |
Настроечная информация — таблица переадресации (relocation table) |
|
|
|
|
.rsrc |
Ресурсы |
|
|
|
|
.text |
Код ЕХЕ или DLL |
|
|
|
|
.tls |
Локальная память потока |
|
|
|
|
.xdata |
Таблица для обработки исключений |
|
|
|
Кроме стандартных разделов, генерируемых компилятором и компоновщиком, можно создавать свои разделы в EXEили DLL-файле, используя директиву компи лятора:
#pragma data_seg("имя_раздела")
Например, можно создать раздел Shared, в котором содержится единственная пе ременная типа LONG:
#pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_seg()
Обрабатывая этот код, компилятор создаст раздел Shared и поместит в него все инициализированные переменные, встретившиеся после директивы #pragma. В нашем примере в этом разделе находится переменная g_lInstanceCount. Директива #pragma data_seg() сообщает компилятору, чти следующие за ней переменные нужно вновь помещать в стандартный раздел данных, а нс в Shared. Важно помнить, что компиля тор помещает в новый раздел только инициализированные переменные. Если из пре дыдущего фрагмента кода исключить инициализацию переменной, она будет вклю чена в другой раздел:
#pragma data_seg("Shared") LONG g_lInslanceCount; #pragma data_seg()
Однако в компиляторе Microsoft Visual C++ 6.0 предусмотрен спецификатор allo cate, который позволяет помещать неинициализированные данные в любой раздел. Взгляните на этот код:
//создаем раздел Shared и заставляем компилятор
//поместить в него инициализированные данные
#pragma data_seg("Shared")
//инициализированная переменная, по умолчанию помещается в раздел Shared
int а = 0;
//неинициализированная переменная, по умолчанию помещается в другой раздел
int b;
//сообщаем компилятору прекратить включение инициализированных данных
//в раздел Shared
#pragma data_seg()
//инициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int с = 0;
//неинициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int d;
//инициализированная переменная, по умолчанию помещается в другой раздел
int e = 0;
//неинициализированная переменная, по умолчанию помещается в другой раздел
int f;
Чтобы спецификатор allocate работал корректно, сначала должен быть создан соответствующий раздел. Так что, убрав из предыдущего фрагмента кода первую стро ку #pragma data_seg, Вы нс смогли бы его скомпилировать.
Чаще всего переменные помещают в собственные разделы, намереваясь сделать их разделяемыми между несколькими проекциями EXE или DLL. По умолчанию каж дая проекция получает свой набор переменных. Но можно сгруппировать в отдель ном разделе переменные, которые должны быть доступны всем проекциям EXE или DLL; тогда система не станет создавать новые экземпляры этих переменных для каж дой проекции EXE или DLL.
Чтобы переменные стали разделяемыми, одного указания компилятору выделить их в какой-то раздел мало. Надо также сообщить компоновщику, что переменные в
этом разделе должны быть общими Для этого предназначен ключ /SECTION компоновщика