Наконец, сравнение списка DLL, загруженных в процесс, с аналогичным списком другого экземпляра той же программы в другой системе помогает выявить проблемы с конфигурацией DLL, например загрузку в процесс не той версии DLL.
 
Защита памяти
   Как уже говорилось в главе 1,Windows обеспечивает защиту памяти, предотвращая случайную или преднамеренную порчу пользовательскими процессами данных в адресном пространстве системы или других процессов. B Windows предусмотрено четыре основных способа защиты памяти.
   Во-первых, доступ ко всем общесистемным структурам данных и пулам памяти, используемым системными компонентами режима ядра, возможен лишь из режима ядра – у потоков пользовательского режима нет доступа к соответствующим страницам. Когда поток пользовательского режима пытается обратиться к одной из таких страниц, процессор генерирует исключение, и диспетчер памяти сообщает потоку о нарушении доступа.
 
    ПРИМЕЧАНИЕ Некоторые страницы в системном адресном пространстве Windows 95/98 и Windows Millennium Edition, напротив, доступны для записи из пользовательского режима, что позволяет сбойным приложениям портить важные системные структуры данных и вызывать крах системы.
 
   Во-вторых, у каждого процесса имеется индивидуальное закрытое адресное пространство, защищенное от доступа потоков других процессов. Исключение составляют те случаи, когда процесс разделяет какие-либо страницы с другими процессами или когда у другого процесса есть права на доступ к объекту «процесс» для чтения и/или записи, что позволяет ему использовать функции ReadProcessMemoryи WriteProcessMemory.Как только поток ссылается на какой-нибудь адрес, аппаратные средства поддержки виртуальной памяти совместно с диспетчером памяти перехватывают это обращение и транслируют виртуальный адрес в физический. Контролируя трансляцию виртуальных адресов, Windows гарантирует, что потоки одного процесса не получат несанкционированного доступа к страницам другого процесса.
   В-третьих, кроме косвенной защиты, обеспечиваемой трансляцией виртуальных адресов в физические, все процессоры, поддерживаемые Windows, предоставляют ту или иную форму аппаратной защиты памяти (например доступ для чтения и записи, только для чтения и т. д.); конкретные механизмы такой защиты зависят от архитектуры процессора. Скажем, страницы кода в адресном пространстве процесса помечаются атрибутом «только для чтения», что защищает их от изменения пользовательскими потоками.
   Атрибуты защиты памяти, определенные в Windows API, перечислены в таблице 7-3 (см. также документацию на функции VirtualProtect, VirtualProtectEx, VirtualOueryи VirtualQuervEx}.
 
    ПРИМЕЧАНИЕ Атрибут защиты «запрет на выполнение» (no execute protection) поддерживается Windows XP Service Pack 2 и Windows Server 2003 Service Pack 1 на процессорах с соответствующей аппаратной поддержкой (например, на x64-, IA64- и будущих x86-npoueccopax). B более ранних версиях Windows и на процессорах без поддержки атрибута защиты «запрет на выполнение», выполнение всегда разрешено. Более подробные сведения об этом атрибуте защиты см. в следующем разделе.
 
   Наконец, совместно используемые объекты «раздел» имеют стандартные для Windows списки контроля доступа (access control lists, ACL), проверяемые при попытках процессов открыть эти объекты. Таким образом, доступ к разделяемой памяти ограничен кругом процессов с соответствующими правами. Когда поток создает раздел для проецирования файла, в этом принимает участие и подсистема защиты. Для создания раздела поток должен иметь права хотя бы на чтение нижележащего объекта «файл», иначе операция закончится неудачно.
   После успешного открытия описателя раздела действия потока все равно зависят от описанных выше атрибутов защиты, реализуемых диспетчером памяти на основе аппаратной поддержки. Поток может менять атрибуты защиты виртуальных страниц раздела (на уровне отдельных страниц), если это не противоречит разрешениям, указанным в ACL для данного раздела. Так, диспетчер памяти позволит потоку сменить атрибут страниц общего раздела «только для чтения» на «копирование при записи», но запретит его изменение на атрибут «для чтения и записи». Копирование при записи разрешается потому, что не влияет на другие процессы, тоже использующие эти данные.
   Все эти механизмы защиты памяти вносят свой вклад в надежность, стабильность и устойчивость Windows к ошибкам и сбоям приложений.
 
Запрет на выполнение
   Хотя в API управления памятью в Windows всегда были определены биты защиты страницы, позволяющие указывать, может ли страница содержать исполняемый код, лишь с появлением Windows XP Service Pack 2 и Windows Server 2003 Service Pack 1 эта функциональность стала поддерживаться на процессорах с аппаратной защитой «запрет на выполнение», в том числе на всех процессорах AMD64 (AMD Athlon64, AMD Opteron), на некоторых чисто 32-разрядных процессорах AMD (отдельных AMD Sempron), на Intel IA64 и Intel Pentium 4 или Xeon с поддержкой EM64T (Intel Extended Memory 64 Technology).
   Эта защита, также называемая предотвращением выполнения данных (data execution prevention, DEP), означает, что попытка передачи управления какой-либо инструкции на странице, помеченной атрибутом «запрет на выполнение», приведет к нарушению доступа к памяти. Благодаря этому блокируются попытки определенных типов вирусов воспользоваться ошибками в операционной системе, которые иначе позволили бы выполнить код, размещенный на странице данных. Попытка выполнить код на странице, помеченной атрибутом «запрет на выполнение», в режиме ядра вызывает крах системы с кодом ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY. Если такая же попытка предпринимается в пользовательском режиме, то генерируется исключение STATUS_ACCESS_VIOLATION (0xc0000005); оно доставляется потоку, в котором была эта недопустимая ссылка. Если процесс выделяет память, которая должна быть исполняемой, то при вызове функций, отвечающих за выделение памяти, он обязан явно указать для соответствующих страниц флаг PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE или PAGE_EXECUTE_WRITECOPY.
   B б4-разрядных версиях Windows атрибут защиты «запрет на выполнение» всегда применяется ко всем б4-разрядным программам и драйверам устройств, и его нельзя отключить. Поддержка такой защиты для 32-разрядных программ зависит от конфигурационных параметров системы. B 64-разрядной Windows защита от выполнения применяется к стекам потоков (как режима ядра, так и пользовательского режима), к страницам пользовательского режима, не помеченным явно как исполняемые, к пулу подкачиваемой памяти ядра и к сеансовому пулу ядра (описание пулов памяти ядра см. в разделе «Системные пулы памяти»). Однако в 32-разрядной Windows защита от выполнения применяется только к стекам потоков и страницам пользовательского режима. Кроме того, когда в 32-разрядной Windows разрешена защита от выполнения, система автоматически загружается в PAE-режиме (переходя на использование РАЕ-ядра, \Windows\System32\Ntkrnlpa.exe). Описание PAE см. в разделе «Physical Address Extension (PAE)».
   Активизация защиты от выполнения для 32-разрядных программ зависит от ключа /NOEXECUTE= в Boot.ini. Эти настройки можно изменить и на вкладке Data Execution Prevention, которая открывается последовательным выбором My Computer, Properties, Advanced, Performance Settings (см. рис. 7-2.) Когда вы выбираете защиту от выполнения в диалоговом окне настройки DEP, файл Boot.ini модифицируется добавлением в него соответствующего ключа /NOEXECUTE. Список аргументов для этого ключа и их описание см. в таблице 7-4. 32-разрядные приложения, исключенные из защиты от выполнения, перечисляются в параметрах в разделе реестра HKLM\Software\Microsoft\Windows NT \CurrentVersion\AppCompatFlags\Layers; при этом в качестве имени параметра используется полный путь к исполняемому файлу, а в качестве его значения – «DisableNXShowUI».
    Рис. 7-2. Параметры Data Execution Protection
 
   B Windows XP (в 64- и 32-разрядных версиях) защита от выполнения для 32-разрядных программ по умолчанию применяется только к базовым исполняемым образам операционной системы Windows (/NOEXECUTE=OPTIN), чтобы не нарушить работу 32-разрядных приложений, которые могут полагаться на выполнение кода в страницах, не помеченных как исполняемые. B Windows Server 2003 такая защита по умолчанию распространяется на все 32-разрядные приложения (/NOEXECUTE=OPTOUT).
 
    ПРИМЕЧАНИЕ Чтобы получить полный список защищаемых программ, установите Windows Application Compatibility Toolkit (его можно скачать с microsoft.com) и запустите CompatibilityAdministrator Tool. Выберите System Database, Applications и Windows Components. B правой секции окна появится список защищенных исполняемых файлов.
 
 
Программный вариант DEP
   Поскольку большинство процессоров, на которых сегодня работает Windows, не поддерживает аппаратную защиту от выполнения, Windows XP Service Pack 2 и Windows Server 2003 Service Pack 1 (или выше) поддерживают ограниченный программный вариант DEP (data execution prevention). Одна из функций программного DEP – сужать возможности злоумышленников в использовании механизма обработки исключений в Windows. (Описание структурной обработки исключений см. в главе 3.) Если файлы образа программы опираются на безопасную структурную обработку исключений (новая функциональность компилятора Microsoft Visual C++ 2003), то, прежде чем передавать исключение, система проверяет, зарегистрирован ли обработчик этого исключения в таблице функций, которая помещается в файл образа. Если в файлах образа программы безопасная структурная обработка исключений не применяется, программный DEP проверяет, находится ли обработчик исключения в области памяти, помеченной как исполняемая, еще до передачи исключения.
 
Копирование при записи
   Защита страницы типа «копирование при записи» – механизм оптимизации, используемый диспетчером памяти для экономии физической памяти. Когда процесс проецирует копируемое при записи представление объекта «раздел» со страницами, доступными для чтения и записи, диспетчер памяти – вместо того чтобы создавать закрытую копию этих страниц в момент проецирования представления (как в операционной системе Hewlett Packard OpenVMS) – откладывает создание копии до тех пор, пока не закончится запись в них. Эта методика используется и всеми современными UNIX-системами. Ha рис. 7-3 показана ситуация, когда два процесса совместно используют три страницы, каждая из которых помечена как копируемая при записи, но ни один из процессов еще не пытался их модифицировать.
   Если поток любого из этих процессов что-то записывает на такую страницу, генерируется исключение, связанное с управлением памятью. Обнаружив, что запись ведется на страницу с атрибутом «копирование при записи», диспетчер памяти, вместо того чтобы сообщить о нарушении доступа, выделяет в физической памяти новую страницу, доступную для чтения и записи, копирует в нее содержимое исходной страницы, обновляет соответствующую информацию о страницах, проецируемых на данный процесс, и закрывает исключение. B результате команда, вызвавшая исключение, выполняется повторно, и операция записи проходит успешно. Ho, как показано на рис. 7-4, новая страница теперь является личной собственностью процесса, инициировавшего запись, и не видима другим процессам, совместно использующим страницу с атрибутом «копирование при записи». Каждый процесс, что-либо записывающий на эту разделяемую страницу, получает в свое распоряжение ее закрытую копию.
   Одно из применений копирования при записи – поддержка точек прерываний для отладчиков. Например, по умолчанию страницы кода доступны только для выполнения. Если программист при отладке программы устанавливает точку прерывания, отладчик должен добавить в код программы соответствующую команду. Для этого он сначала меняет атрибут защиты страницы на PAGE_EXECUTE_READWRITE, а затем модифицирует поток команд. Поскольку страница кода является частью проецируемого раздела, диспетчер памяти создает закрытую копию для процесса с установленной точкой прерывания, тогда как другие процессы по-прежнему используют исходную страницу кода.
   Копирование при записи может служить примером алгоритма отложенной оценки(lazy evaluation), который диспетчер памяти применяет при любой возможности. B таких алгоритмах операции, чреватые большими издержками, не выполняются до тех пор, пока не станут абсолютно необходимыми, – если операция так и не понадобится, никаких издержек вообще не будет.
   Подсистема POSIX использует преимущества копирования при записи в реализации функции fork.Как правило, если UNIX-приложения вызывают forkдля создания другого процесса, то первое, что делает новый процесс, – обращается к функции execдля повторной инициализации адресного пространства исполняемой программы. Вместо копирования всего адресного пространства при вызове forkновый процесс использует страницы родительского процесса, помечая их как копируемые при записи. Если дочерний процесс что-то записывает на эти страницы, он получает их закрытую копию. B ином случае оба процесса продолжают разделять страницы без копирования. Так или иначе диспетчер памяти копирует лишь те страницы, на которые процесс пытается что-то записать, а не все содержимое адресного пространства.
   Оценить частоту срабатывания механизма копирования при записи можно с помощью счетчика Memory: Write Copies/Sec (Память: Запись копий страниц/сек).
 
Диспетчер куч
   Многие приложения выделяют память небольшими блоками (менее 64 Кб – минимума, поддерживаемого функциями типа VirtuaLAlloc).Выделение столь большой области (64 Кб) для сравнительно малого блока весьма неоптимально с точки зрения использования памяти и производительности. Для устранения этой проблемы в Windows имеется компонент – диспетчер куч(heap manager), который управляет распределением памяти внутри больших областей, зарезервированных с помощью функций, вьщеляющих память в соответствии с гранулярностью страниц. Гранулярность выделения памяти в диспетчере куч сравнительно мала: 8 байтов в 32-разрядных системах и 16 байтов в 64-разрядных. Диспетчер куч обеспечивает оптимальное использование памяти и производительность при выделении таких небольших блоков памяти.
   Функции диспетчера куч локализованы в двух местах: в NtdlLdll и Ntoskrnl.exe. API-функции подсистем (вроде API-функций Windows-куч) вызывают функции из Ntdll, а компоненты исполнительной системы и драйверы устройств – из NtoskrnL Родные интерфейсы (функции с префиксом Rtl)доступны только внутренним компонентам Windows и драйверам устройств режима ядра. Документированный интерфейс Windows API для куч (функции с префиксом Heap) представляют собой тонкие оболочки, которые вызывают родные функции из NtdlLdll Кроме того, для поддержки устаревших Windows-приложений предназначены унаследованные API-функции (с префиксом Localили GlobaP).K наиболее часто используемым Windows-функциям куч относятся:
 
    (o) HeapCreateили HeapDestroy –соответственно создает или удаляет кучу. При создании кучи можно указать начальные размеры зарезервированной и переданной памяти;
    (o) HeapAlloc- выделяет блок памяти из кучи;
    (o) HeapFree- освобождает блок, ранее выделенный через HeapAlloc
    (o) HeapReAlloc –увеличивает или уменьшает размер уже выделенного блока;
    (o) HeapLockи HeapUnlock –управляют взаимным исключением (mutual exclusion) операций, связанных с обращением к куче;
    (o) HeapWalk- перечисляет записи и области в куче.
 
Типы куч
   У каждого процесса имеется минимум одна куча – куча, выделяемая процессу по умолчанию (default process heap). Куча по умолчанию создается в момент запуска процесса и никогда не удаляется в течение срока жизни этого процесса. По умолчанию она имеет размер 1 Мб, но ее начальный размер может быть увеличен, если в файле образа указано иное значение с помощью ключа /HEAP компоновщика. Однако этот объем памяти резервируется только для начала и по мере необходимости автоматически увеличивается (в файле образа можно указать и начальный размер переданной памяти).
   Куча по умолчанию может быть явно использована программой или неявно некоторыми внутренними Windows-функциями. Приложение запрашивает память из кучи процесса по умолчанию вызовом Windows-функции GetProcessHeap.Процесс может создавать дополнительные закрытые кучи вызовом HeapCreate.Когда куча больше не нужна, занимаемое ею виртуальное адресное пространство можно освободить, вызвав HeapDestroy.Массив всех куч поддерживается в каждом процессе, и поток может обращаться к ним через Windows-функцию GetProcessHeaps.
   Куча может быть создана в больших регионах памяти, зарезервированных через диспетчер памяти с помощью VirtualAllocили через объекты «файл, проецируемый в память», отображенные на адресное пространство процесса. Последний подход редко применяется на практике, но удобен в случаях, когда содержимое блоков нужно разделять между двумя процессами или между частями компонента, работающими в режиме ядра и в пользовательском режиме. B последнем случае действует ряд ограничений на вызов функций куч. Во-первых, внутренние структуры куч используют указатели и поэтому не допускают перемещения на другие адреса. Во-вторых, функции куч не поддерживают синхронизацию между несколькими процессами или между компонентом ядра и процессом пользовательского режима. Наконец, в случае кучи, разделяемой между кодом пользовательского режима и режима ядра проекция пользовательского режима должна быть только для чтения, чтобы исключить повреждение внутренних структур кучи кодом пользовательского режима.
 
Структура диспетчера кучи
   Как показано на рис. 7-5, диспетчер куч состоит из двух уровней: необязательного интерфейсного (front-end layer) и базового (core heap layer). Последний заключает в себе базовую функциональность, которая обеспечивает управление блоками внутри сегментов, управление сегментами, поддержку политик расширения кучи, передачу и возврат памяти, а также управление большими блоками.
   Необязательный интерфейсный уровень (только для куч пользовательского режима) размещается поверх базового уровня. Существует два типа интерфейсных уровней: ассоциативные списки (look-aside lists) и куча с малой фрагментацией (Low Fragmentation Heap, LFH). LFH доступна лишь в Windows XP и более поздних версиях Windows. Единовременно для каждой кучи можно использовать только один интерфейсный уровень.
 
Синхронизация доступа к куче
   Диспетчер куч по умолчанию поддерживает параллельный доступ из нескольких потоков. Однако, если процесс является однопоточным или использует внешний механизм синхронизации, он может отключить синхронизацию, поддерживаемую диспетчером куч, и тем самым избежать издержек, связанных с этим видом синхронизации. Для этого при создании кучи или при каждом запросе на выделение памяти такой процесс может указывать флаг HEAP_NO_SERIALIZE.
   Процесс также может блокировать всю кучу и запретить другим потокам выполнение операций, требующих согласования состояний между несколькими обращениями к куче. Например, перечисление блоков в куче с помощью Windows-функции HeapWalkтребует блокировки кучи, если над ней могут выполняться операции сразу несколькими потоками.
   Если синхронизация куч разрешена, для каждой кучи выделяется по одной блокировке, которая защищает все внутренние структуры кучи. B приложениях с большим числом потоков (особенно когда они выполняются в многопроцессорных системах) блокировка кучи может превратиться в точку интенсивной конкуренции. B таком случае производительность можно повысить за счет использования интерфейсного уровня.
 
Ассоциативные списки
   Это однонаправленные связанные списки (single linked lists), поддерживающие элементарные операции вроде заталкивания в список или выталкивания из него по принципу «последним пришел, первым вышел» (Last In, First Out, LIFO) без применения блокирующих алгоритмов. Упрощенная версия этих структур данных также доступна Windows-приложениям через функции InterlockedPopEntrySListи InterlockedPushEntrySList.Для каждой кучи создается 128 ассоциативных списков, которые удовлетворяют запросы на выделение блоков памяти размером до 1 Кб на 32-разрядных платформах и до 2 Кб на 64-разрядных.
   Ассоциативные списки обеспечивают гораздо более высокую производительность, чем при обычных запросах на выделение памяти, так как несколько потоков могут одновременно выполнять операции выделения и возврата памяти, не требуя применения глобальной для кучи блокировки. Кроме того, благодаря модели размещения LIFO и обращению к меньшему числу внутренних структур данных при каждой операции над кучей оптимизируется локальность кэша.
   Диспетчер куч поддерживает ряд блокировок в каждом ассоциативном списке и некоторые счетчики, помогающие независимо регулировать работу с каждым списком. Если поток запрашивает блок такого размера, которого нет в соответствующем ассоциативном списке, диспетчер куч переадресует этот вызов базовому уровню и обновит внутренний счетчик неудачных выделений, значение которого впоследствии будет использовано при принятии решений по оптимизации.
   Диспетчер куч создает ассоциативные списки автоматически при создании кучи, если только эта куча расширяемая и не включен отладочный режим. У некоторых приложений могут возникать проблемы совместимости из-за использования диспетчером куч ассоциативных списков. B таких случаях для корректной работы нужно указывать флаг DisableHeapLookasideв параметрах выполнения файлов образов унаследованных приложений. (Эти параметры можно задавать с помощью утилиты Imagecfg.exe из Windows 2000 Server Resource Kit, supplement 1.)
 
Куча с малой фрагментацией
   Многие приложения, выполняемые в Windows, используют сравнительно небольшие объемы памяти из куч (обычно менее одного мегабайта). Для этого класса приложений диспетчер куч применяет политику наибольшей подгонки (best-fit policy), которая помогает сохранять небольшим «отпечаток» каждого процесса в памяти. Однако такая стратегия не масштабируется для больших процессов и многопроцессорных машин. B этих случаях доступная память в куче может уменьшиться из-за ее фрагментации. B сценариях, где лишь блоки определенного размера часто используются параллельно разными потоками, выполняемыми на разных процессорах, производительность ухудшается. Дело в том, что нескольким процессорам нужно одновременно модифицировать один и тот же участок памяти (например, начало ассоциативного списка для блоков этого размера), а это приводит к объявлению недействительной соответствующей кэш-линии для других процессоров.
   Эти проблемы решаются применением кучи с малой фрагментацией (LFH), которая использует базовый уровень диспетчера куч и ассоциативные списки. B отличие от ситуации, в которой ассоциативные списки по умолчанию применяются как интерфейсные, если это разрешено другими параметрами куч, поддержка LFH включается, только когда приложение вызывает функцию HeapSetInformation.B случае больших куч значительная доля запросов на выделение обычно раскладывается на относительно небольшое число корзин (buckets) определенных размеров. Стратегия выделения памяти, применяемая LFH, заключается в оптимизации использования памяти для таких запросов за счет эффективной обработки блоков одного размера.
   Для устранения проблем с масштабируемостью LFH раскрывает часто используемые внутренние структуры в набор слотов, в два раза больший текущего количества процессоров в компьютере. Закрепление потоков за этими слотами выполняется LFH-компонентом, называемым диспетчером привязки(affinity manager). Изначально LFH использует для распределения памяти первый слот, но, как только возникает конкуренция при доступе к некоторым внутренним данным, переключает текущий поток на другой слот. И чем больше конкуренция, тем большее число слотов задействуется для потоков. Эти слоты создаются для корзины каждого размера, что также увеличивает локальность и сводит к минимуму общий расход памяти.
 
Средства отладки
   Диспетчер куч предоставляет несколько средств, помогающих обнаруживать ошибки.
 
    (o) Enable tail checking (включить проверку концевой части блока)B конец каждого блока помещается сигнатура, проверяемая при его освобождении. Если эта сигнатура полностью или частично уничтожается из-за переполнения буфера, куча сообщает о соответствующей ошибке.
    (o) Enable free checking (включить проверку свободных блоков)Свободный блок заполняется определенным шаблоном, который проверяется, когда диспетчеру куч нужен доступ к этому блоку. Если процесс продолжает записывать в блок после его освобождения, диспетчер куч обнаружит изменения в шаблоне и сообщит об ошибке.
    (o) Parameter checking (проверка параметров)Проверка параметров, передаваемых функциям куч.
    (o) Heap validation (проверка кучи)Вся куча проверяется при каждом обращении к ней.
    (o) Heap tagging and stack traces support (поддержка меток и трассировки стека)Это средство поддерживает задание меток для выделяемой памяти и/или перехват трассировок стека пользовательского режима при обращениях к куче, что помогает локализовать причину той или иной ошибки.
   Первые три средства включаются по умолчанию, если загрузчик обнаруживает, что процесс запущен под управлением отладчика. (Отладчик может переопределить такое поведение и выключить эти средства.) Средства отладки для куч могут быть заданы установкой различных отладочных флагов в заголовке образа через утилиту gflags (см. раздел «Глобальные флаги Windows» в главе 3) или командой