Глава 1. Обзор внутреннего устройства Windows
Содержание
Данная глава описывает наиболее важные понятия в самой внутренней работе Windows. Некоторые из этих тем будут подробнее описаны позднее в этой книге, где они теснее связаны с самой темой. Убедитесь что вы разобрались с описанными в данной главе понятиями, так как они составляют сами основы построения любого драйвера и даже кода нижнего уровня пользовательского режима.
В этой главе:
-
Процессы
-
Виртуальная память
-
Потоки
-
Системные службы
-
Архитектура системы
-
Дескрипторы и объекты
Процесс это некий объект герметизации и управления, который представляет запущенный экземпляр программы. Довольно часто употребляемый термин "выполняется процесс" является неточным. процессы не работают - процессы управляют. Тем что исполняет код и технически выполняется выступают потоки. С точки зрения верхнего уровня процесс обладает следующим:
-
Некой исполняемой программой, которая содержит его изначальный код и те данные, которые используются для исполнения ода внутри данного процесса. Это так для большинства процессов, однако некие особенные не обладают каким- то образом исполнения (создаваемого непосредственно самим ядром).
-
Частым виртуальным адресным пространством, применяемым для выделения памяти для любых целей данного кода, необходимых данному процессу.
-
Каким- то маркером доступа (носящим название первичного маркера), который представляет объект, хранящий содержимое контекста безопасности данного процесса (если поток не применяет иной маркер, применяя заимствование прав - impersonation).
-
Частную таблицу дескрипторов для исполняемых объектов, таких как события, семафоры и файлы.
-
Один или более потоков исполнения. Обычный процесс режима пользователя создаётся с одним потоком (выполнение классической функции
main/WinMain
). Некий процесс режима пользователя без потоков в большинстве случаев бесполезен и в обычных обстоятельствах будет уничтожаться самим ядром.
Эти элементы процесса представлены на рисунке 1-1.
Процесс уникально идентифицируется его идентификатором процесса (Process ID), который остаётся уникальным на протяжении
существования самого процесса ядра. После его уничтожения, тот же самый идентификатор может повторно применяться для новых
процессов. Важно осознавать, что сам по себе исполняемый файл это не уникальный идентификатор процесса. Например, может
иметься пять экземпляров notepad.exe
, исполняемых одновременно. Каждый из
таких экземпляров Notepad
обладает своими собственным адресным пространством,
потоками, таблицей дескрипторов, идентификатором процесса и тому подобным. Все эти пять процессов пользуются одним и тем же
файлом образа (notepad.exe
) в качестве своих первоначального кода и данных.
Рисунок 1-2 отображает снимок экрана таблицы подробностей Диспетчера задач, отображающей
пять экземпляров Notepad.exe
, причём каждый со своими собственными атрибутами.
Всякий процесс обладает своим собственным виртуальным, частным, линейным адресным пространством. Это адресное пространство
стартует пустым (или почти пустым, поскольку сам исполняемый образ и NtDll.Dll
первыми ставятся в соответствие, а за ними следуют дополнительные подсистемы DLL). Как только начинается выполнение основного
(первого) потока, вероятно, будет выделена память, загружены дополнительные DLL и т.д.. Это адресное пространство является
частным, что означает, что прочие процессы не способны получать к нему прямой доступ. Значение диапазона адресного пространства
начинается с нуля (технически говоря, самые первые и последние 64k его адресного пространства не могут быть фиксированы) и
доходит до максимума, который зависит от "разрядности" (32 или 64 бита) процесса и "разрядности" его
операционной системы следующим образом:
-
Для 32- битных процессов в 32- битных системах Windows, по умолчанию величина размера адресного пространства составляет 2ГБ.
-
Для 32- битных процессов в 32- битных системах Windows, которые пользуются установкой увеличенного виртуального адресного пространства пользователя, может настраиваться на обладание до 3ГБ адресного пространства на процесс. Для получения расширенного адресного пространства, тот исполняемый файл, из которого был создан данный процесс, должен был помечен флагом компоновщика
LARGEADDRESSAWARE
в своём PE заголовке. Если бы это было не так, он всё равно был бы ограничен до 2ГБ. -
Для 64- битных процессов (естественно, в 64- битных системах Windows), величиной размера адресного пространства является 8ГБ (для Windows 8 и более ранних) или 128ТБ (для Windows 8.1 и последующих).
-
Для 32- битных процессов в 64- битных системах Windows, когда в их заголовке PE исполняемый образ исполняемого файла обладает флагом
LARGEADDRESSAWARE
, величина адресного пространства равна 4ГБ. В противном случае значением размера остаётся 2ГБ.
Замечание | |
---|---|
Такое требование установки флага |
Всякий процесс обладает своим собственным адресным пространством, что превращает адреса процесса в относительные, вместо того,
чтобы они были абсолютными. Например, когда мы пробуем определить что пребывает по адресу 0x20000
,
значения адреса самого по себе не достаточно; должен быть определён тот процесс, к которому относится данный адрес.
Такая память сама по себе носит название виртуальной, что означает, что нет непосредственной взаимосвязи между неким адресом и значением точного местоположения в котором его можно найти в физической памяти (Оперативной памяти, ОЗУ - RAM). Буфер внутри процесса может сопоставляться с физической памятью или временно пребывать в неком файле (например, в файле подкачки страниц). Сам термин виртуальный соотносится с тем фактом, что с точки зрения исполнения нет необходимости знать находится ли та память, к которой необходимо получать доступ в ОЗУ, ЦПУ осуществит преобразование преобразования виртуального значения в физическое перед доступом к самим данным. Когда такая память не является резидентной (на что указывает флаг в соответствующей записи таблицы трансляции), само ЦПУ возбуждает исключительную ситуацию отказа страницы, что заставляет имеющийся дескриптор отказов страниц диспетчера памяти извлекать данные из своего соответствующего файла (когда это и в самом деле допустимый отказ страницы), скопировать их в ОЗУ, внести необходимые изменения в его записи таблицы страниц, которые отражают соответствие его буфера и выдаёт указание своему ЦПУ повторить попытку. Рисунок 1-3 отображает такое концептуальное отображение виртуальной памяти в физическую для двух процессов.
Единица управления памяти носит название страницы. Всякий связанный с памятью атрибут всегда пребывает на уровне детализации страницы, скажем, её защита или состояние. Размер страницы определяется типом ЦПУ (а в некоторых процессорах может настраиваться), и в любом случае диспетчер памяти обязан следовать соответствию. Обычный (порой носящий название малого) размер страницы составляет 4кБ во всех поддерживаемых Windows архитектурах.
Помимо обычного (малого) размера страницы, Windows также поддерживает большие страницы. Значение размера большой страницы составляет 2МБ (x86/x64/ARM64) или 4МБ (ARM). Это основано на применении Записи каталога страниц (PDE, Page Directory Entry) для сопоставления значения большой страницы без применения таблицы страниц. В результате это приводит к более быстрой трансляции, но что ещё более важно, к лучшему применению Буфера быстрого преобразования адреса (TLB, Translation Lookaside Buffer) - кэша только что транслированных страниц, поддерживаемого самим ЦПУ.
В случае большой страницы, одна запись TLB отображает гораздо больше памяти чем при маленькой странице.
Замечание | |
---|---|
Основным недостатком больших страниц является необходимость наличия достаточной непрерывной памяти в ОЗУ, что может приводить к отказу когда память ограничена или сильно фрагментирована. К тому же большие страницы всегда не обладают страничной организацией и способны применять лишь защиту чтения/ записи. |
В Windows 10 и Server 2016, а также в последующих версиях, поддерживаются гигантские (huge) страницы с размером в 1ГБ. Они применяются автоматически для больших страниц при выделении по крайней мере размера в 1ГБ, причём такой размер может располагаться в ОЗУ как непрерывный.
Каждая страница в виртуальной памяти способна пребывать в одном из трёх состояний:
-
Свободна (Free) - данная страница ни коим образом не распределена; здесь нет ничего. Все попытки доступа к такой странице вызвала бы исключительную ситуацию нарушения доступа. Большинство страниц во вновь созданном процессе Свободны.
-
Фиксирована (Committed) - это обратно к Свободной; выделенная страница, к которой возможен успешный доступ (при условии атрибутов защиты без конфликтов; скажем, запись в доступную исключительно для чтения страницы вызывает нарушение прав доступа). Фиксированным страницам установлено соответствие в ОЗУ или неком файле (например, в файле подкачки страниц).
-
Зарезервирована (Reserved) - данная страница не Фиксирована, однако её диапазон адреса зарезервирован для возможной Фиксации в будущем. С точки зрения ЦПУ, это то же самое что и Свободна - всякая попытка доступа возбуждает некую исключительную ситуацию нарушения доступа. Тем не менее, новые попытки доступа с применением функции
VirtualAlloc
(илиNtAllocateVirtualMemory
, связанного собственного API), в которой не указан конкретный адрес, не будут выделяться в Зарезервированной области. Классическим примером использования Зарезервированной памяти для поддержки непрерывного виртуального адресного пространства при сохранении применения Фиксированной памяти описан далее в этой главе в разделе Стеки потоков.
Нижняя часть имеющегося адресного пространства предназначена для использования процессами режима пользователя. Во время исполнения конкретного потока связанная с ней адресное пространство процесса отображается от нуля до своего верхнего предела, как это описано в нашем предыдущем разделе. Тем не менее, сама операционная система также должна Где- то пребывать - и это Где- то собственно верхний диапазон поддерживаемых самой системой адресов, а именно:
-
В 32- битной системе, запущенной без настройки увеличения пространства виртуального адреса пространства, сама операционная система располагается в самых верхних 2ГБ виртуального адресного пространства, с адреса
0x80000000
по0xFFFFFFFF
. -
В 32- битной системе, запущенной с настройкой увеличения пространства виртуального адреса пространства, сама операционная система пребывает в оставшемся адресном пространстве. Например, когда эта система настроена на адресное пространство пользовательского пространства адресов в 3ГБ на процесс (максимально), сама ОС занимает верхний 1 ГБ (с адреса
0xC0000000
по0xFFFFFFFF
). Тот компонент, который больше всего страдает от такого сокращения адресного пространства - это кэш её файловой системы. -
В 64- битных системах под управлением Windows 8, Server 2012 и более ранних, сама ОС получает верхние 8ТБ виртуального адресного пространства.
-
В 64- битных системах под управлением Windows 8.1, Server 2012 R2 и последующих версиях, сама ОС получает самые верхние 128ТБ виртуального адресного пространства.
Рисунок 1-4 отображает схему своей виртуальной памяти для двух "крайних" ситуаций: 32- битного процесса в 32- битной системе (слева) и 64- битного процесса в 64- битной системе (справа).
Системное пространство не зависит от процессов - в конце концов, это одна и та же система, одно и то же ядро, один и те же драйверы, которые обслуживают все процессы в этой системе (исключением выступает некая системная память, которая выделяется для на основе выделения под каждый сеанс, но это не существенно для данного обсуждения). Из этого следует, что любой адрес в системном пространстве является абсолютным, а не относительным, поскольку он "выглядит" одинаково в контексте любого процесса. Естественно, фактический доступ из режима пользователя к системному пространству имеет результатом исключительную ситуацию нарушения доступа.
Системное пространство это то место, в котором после запуска пребывает само ядро, Уровень аппаратной абстракции (HAL, Hardware Abstraction Layer), а также драйверы ядра. Таким образом, драйверы ядра автоматически защищены от непосредственного доступа их режима пользователя. Это также означает, что они способны оказывать воздействие на всю систему. Например, в случае утечки памяти в драйвере ядра, такая память не будет высвобождаться даже после выгрузки такого драйвера. С другой стороны, процессы пользовательского режима никогда не смогут допускать утечек за пределами своего жизненного цикла. Само ядро отвечает за закрытие и освобождение всех частных или умерших процессов (все дескрипторы закрываются и вся частная память освобождается).
Действительными исполняющими код субъектами выступают потоки. Поток содержится внутри какого- то процесса, причём пользуется теми ресурсами, которые выставляются его процессом для выполнения работы (таких как виртуальная память и дескрипторы для объектов ядра). Наиболее важные сведения, которыми обладают потоки, таковы:
-
Текущий режим доступа, либо пользователя, либо ядра.
-
Контекст выполнения, включая регистры процессора и состояние исполнения.
-
Один или два стека, применяемых для выделения локальных переменных и управления вызовами.
-
Массив Локальной памяти потока (TLS, Thread Local Storage), который предоставляет некий способ хранения частных данных потока с единообразной семантикой доступа.
-
Базовый приоритет и текущий (динамический) приоритет.
-
Сродство с процессором, указывающее на каких процессорах допускается выполнять этот поток.
Наиболее распространёнными состояниями потока могут быть такими:
-
Исполняемый (Running) - выполняемый в настоящий момент код в (логическом) процессоре.
-
Готов (Ready) - дожидающийся распределения под исполнение по той причине что все уместные процессоры заняты или не доступны.
-
Ожидающий (Waiting) - дожидающийся возникновения некоторого события перед обработкой. После того как это событие происходит, данный поток переходит в состояние Готов.
Рисунок 1-5 отображает диаграмму переходов состояний для этих состояний. Значения чисел в скобках указывают значения номеров состояния, которые могут просматриваться такими инструментами как Performance Monitor. Обратите внимание, что состояние Готов обладает родственным {одного уровня} состоянием Отложенной готовности (Deferred Ready), которое аналогично и присутствует для минимизации внутреннего блокирования.
Рисунок 1-5
Распространённые состояния потоков
preemption quantum end - завершение кванта вытеснения
voluntary switch - добровольное переключение
Каждый поток обладает неким стеком, который он применяет при исполнении, используемый для хранения локальных переменных, передаваемых в функции переменных (в некоторых случаях), а также куда возвращаются адреса, сохраняемые перед вызовами функции. Поток обладает по крайней мере одним располагаемым в пространстве системы (ядра) стеком,причём он достаточно небольшой (по умолчанию 12кБ в 32- разрядных системах и 24кБ в 64- битных системах). Поток режима пользователя обладает вторым стеком в диапазоне адресного пространства пользователя своего процесса и является значительно более крупным (по умолчанию он способен расти до 1МБ). Некий образец с тремя потоками режима пользователя и их стеками показан на Рисунке 1-6. На этом рисунке потоки 1 и 2 пребывают в процессе A, а поток 3 находится в процессе B.
Стек самого ядра всегда располагается в оперативной памяти пока его поток находится в состояниях Исполняемого и Готового. Основная причина для этого слабо уловима и будет обсуждаться позднее в данной главе. С другой стороны, стек режима пользователя может выгружаться, как и всякая прочая память режима пользователя.
Имеющийся стек режима пользователя обрабатывается иначе, нежели стек самого режима ядра в плане своего размера. Он
стартует с определённого объёма фиксированной памяти (которая может быть уменьшена вплоть до одной страницы), в то время как
его следующая страница фиксируется при помощи атрибута PAGE_GUARD
. Всё остальное
адресное пространство памяти этого стека зарезервировано, тем самым нет пустых трат памяти. Основная мысль состоит в увеличении
такого стека на тот случай, если коду его потока потребуется больше места в стеке.
Когда данному потоку требуется больше пространства стека, он получил бы доступ к необходимой охраняемой странице, что
вызывает исключительную ситуацию защиты страницы (page-guard). Затем диспетчер памяти снимает защиту охраны и фиксирует некую
дополнительную страницу, помечая её атрибутом PAGE_GUARD
. Таким образом, по мере
необходимости растёт этот стек, при этом предотвращая заблаговременную фиксацию всей памяти стека. Рисунок 1-7 отображает
данную схему.
Замечание | |
---|---|
С технической точки зрения, в большинстве случаев, Windows пользуется 3 охраняемыми страницами вместо одной. |
Значения размеров стека потока режима пользователя определяется следующим образом:
-
Сам образ исполняемого файла обладает фиксированным стеком и зарезервированными значениями в своём заголовке Portable Executable (PE, формата загружаемого кода). Он принимается в качестве устанавливаемого по умолчанию, когда поток не определяет альтернативных значений. Это всегда применяется для самого первого потока в его процессе.
-
Когда поток создаётся при помощи
CreateThread
(или аналогичных функций), вызывающая сторона способна определять необходимый ей размер стека, либо заранее фиксированный размер, либо резервируемый размер (но не оба), в зависимости от предоставляемого в этой функции флага; задание нуля определяет значения по умолчанию, установленные в его заголовке PE.
Замечание | |
---|---|
Достаточно любопытно, что функции |
Приложениям необходимо выполнять различные операции, которые не являются чисто вычислительными, такие как выделение памяти, открытие файлов, создание потоков и т.п.. Эти действия в конечном итоге могут быть выполнены лишь кодом, работающем в режиме ядра. Так как же код режима пользователя способен выполнять такие операции?
Давайте воспользуемся распространённым (простым) примером: выполняющий процесс Notepad
пользователь при помощи меню File / Open
открывает некий файл. Код
Notepad
отвечает вызовом документированной функции API Windows
CreateFile
. CreateFile
задокументирована как
реализуемая в kernel32.Dll
, одной из DLL подсистемы Windows. Данная функция всё ещё
выполняется в режиме пользователя, поэтому нет способа чтобы она непосредственно открыла файл. После некоторой проверки ошибок,
она вызывает NtCreateFile
, функцию, реализованную в NTDLL.dll
,
основополагающую DLL, которая реализует те API, которые носят название Родного API
(Native API) и это самый нижний уровень кода, который всё ещё пребывает в режиме пользователя. Данная функция, (которая
задокументирована в Windows Driver Kit для разработчиков драйвера устройства) и именно она выполняет переход в режим ядра.
Перед таким реальным переходом она помещает число, носящее название номера службы, в регистр ЦПУ (для архитектур Intel/ AMD
EAX
). Затем она выдаёт специальную инструкцию ЦПУ (syscall
в x64 или sysenter
в x86), что и осуществляет реальный переход в режим ядра с
одновременным безусловным переходом к некой предопределённой процедуре с названием диспетчера
системных служб.
Этот диспетчер системных служб, в свою очередь, использует значение из своего регистра EAX
в качестве индекса для Таблицы диспетчера системных служб (SSDT, System Service
Dispatch Table). При помощи данной таблицы соответствующий код выполняет безусловный переход к самой необходимой системной
службе (системный вызов). Для нашего примера с Notepad, соответствующая запись SSDT
будет указывать на функцию NtCreateFile
, реализуемую диспетчером ввода/ вывода своего
ядра. Обратите внимание, что эта функция обладает тем же самым названием, что и функция из NTDLL.dll
,
а также обладает теми же самыми параметрами. На стороне самого ядра пребывает её реальная реализация. После завершения этого
системного вызова, данный поток возвращается в режим пользователя для выполнения тех инструкций, которые следуют за
sysenter/syscall
. Данная последовательность вызовов отображается на Рисунке 1-8.
Рисунок 1-9 показывает общую архитектуру Windows, составляемую из компонентов режима пользователя и режима ядра.
Вот быстрая сводка по появляющимся на Рисунке 1-9 именованным блокам:
- User processes (процессы пользователя)
Это обычные, основанные на файлах образов, процессы, исполняемые в своей системе, например, экземпляры
Notepad.exe
,cmd.exe
,explorer.exe
и тому подобного.- Subsystem DLLs (библиотеки подсистем)
DLL подсистем это Динамически подключаемая библиотека (DLL, dynamic link libraries), которая реализует весь API некой подсистемы. Подсистема выступает неким конкретным представлением тех возможностей, которые выставляются самим ядром. С технической точки зрения, начиная с Windows 8.1, существует лишь одна единственная подсистема - Windjws Subsystem. Все DLL подсистемы включают в свой состав известные файлы, такие как
kernel32.dll
,user32.dll
,gdi32.dll
,advapi32.dll
,combase.dll
и многие прочие. Они содержат в себе в основном официально документированные API Windows.- NTDLL.DLL
Общесистемная библиотека DLL, реализующая собственный (родной, native) API Windows. Это самый нижний уровень кода, который всё ещё пребывает в режиме пользователя. Его самая важная роль состоит в переходе в режим ядра для активации системных вызовов. NTDLL к тому же реализует Диспетчер кучи, Загрузчик образов и некоторую часть пула потоков своего режима пользователя.
- Service processes (Служебные процессы)
Служебные процессы это обычные процессы Windows, которые взаимодействуют с Диспетчером управления служб (SCM, Service Control Manager, реализуемом в
services.exe
) и делают возможным некое управление их временем жизни. Такой SCM способен запускать, останавливать, ставить на паузу, возобновлять и отправлять прочие сообщения в службы. Обычно службы выполняются под одной из специальных учётных записей - локальной системы (local system), сетевой службы (network service) или локальной службы (local service).- Executive (Супервизор)
Супервизор это самый верхний уровень
NtOskrnl.exe
(собственно "ядра"). Он размещает большую часть кода, который пребывает в режиме ядра. В основном он содержит разнообразные "диспетчеры": Диспетчер объектов, Диспетчер памяти, Диспетчер ввода/ вывода, Диспетчер автоматических подключений (Plug & Play), Диспетчер питания, Диспетчер конфигурации и тому подобные. Он намного больше нижнего уровня Ядра.- Kernel (Ядро)
Собственно уровень Ядра реализует наиболее основополагающие и чувствительные относительно времени исполнения части кода ОС режима ядра. Он содержит планирование потоков, оперативное управление (диспетчеризацию) прерываний и исключительных ситуаций, а также реализацию разнообразных примитивов ядра, таких как взаимные исключения (mutex, mutual exclusion) и семафоры. С целью эффективности и получения непосредственного доступа специфичным для ЦПУ деталям, к часть кода ядра написана на особом для ЦПУ машинном языке.
- Device Drivers (Драйверы устройств)
Драйверы устройств это загружаемые модули ядра. Их код выполняется в режиме ядра, а потому обладает полной мощностью своего ядра. Данная книга посвящена написанию определённых типов драйверов ядра.
- Win32k.sys
Это определённый компонент режима ядра подсистемы Windows. По- существу, это некий модуль ядра (драйвер), который обрабатывает собственно часть интерфейса пользователя Windows и классические API Интерфейса графических устройств (GDI, Graphics Device Interface). Это означает, что данным компонентом обрабатываются все операции с окнами (
CreateWindowEx
,GetMessage
,PostMessage
и т.п.). Остальная часть всей системы практически ничего не знает об интерфейсе пользователя (UI).- Hardware Abstraction Layer (HAL) (Уровень абстрагирования от оборудования)
HAL это программный уровень абстракции над всем оборудованием, непосредственно примыкающему к ЦПУ. Он позволяет драйверам устройств применять API, который не требует детализации и специфических знаний, таких как Контроллеры прерываний или Контроллеры DMA. Естественно, данный уровень в основном полезен для драйверов устройств, написанных для обработки аппаратных средств.
- System Processes (Процессы системы)
Процессы системы это общий (зонтичный) термин, применяемый для описания процессов, которые "просто там" обычно находятся, осуществляя свою работу там, где обычно эти процессы не взаимодействуют непосредственно. Тем не менее, они важны, а некоторые даже критически важны для благополучия всей системы. Прекращение некоторых из них является фатальным и вызывает крушение системы. Некоторые из процессов системы являются собственными (native) процессами, что означает, что они применяют исключительно собственный API (тот API, который реализуется NTDLL). Примеры процессов системы включают в свой состав
Smss.exe
,Lsass.exe
,Winlogon.exe
иServices.exe
.- Subsystem Process (Процесс подсистемы)
Процесс подсистемы Windows, запускающий образ
Csrss.exe
, может быть представлен как вспомогательный для своего ядра с целью управления процессами, запускаемыми под управлением самой подсистемы Windows. Это критически важный процесс, что подразумевает, что при его уничтожении ваша система упадёт. Для каждого сеанса имеется один экземплярCsrss.exe
, а потому в стандартной системе будут существовать два экземпляра - один для сеанса0
и один для сеанса зарегистрированного пользователя (обычно1
). ХотяCsrss.exe
это определённый "диспетчер" самой подсистемы Windows (единственного оставшегося в наши дни), его значение выходит за рамки только этой роли.- Hyper-V Hypervisor (Гипервизор Hyper-V)
Гипервизор Hyper-V присутствует в системах Windows 10 и в Server 2016 (и последующих версиях) когда они поддерживают Безопасность на основе виртуализации (VBS, Virtualization Based Security). VBS предоставляет некий дополнительный уровень безопасности, на котором сама обычная ОС выступает какой- то виртуальной машиной, управляемой Hyper-V. Определены два различных Виртуальных уровня доверия (VTL, Virtual Trust Levels), где VTL
0
составлен из известных нам обычных режимов пользователя, ядра, а VTL1
составлен из безопасного ядра и Режима изолированного пользователя (IUM, Isolated User Mode). VBS выходит за рамки данной книги. Для получения дополнительных сведений обратитесь к книге Windows Internals и/ или в документации Microsoft.
Замечание | |
---|---|
Windows 10 версии 1607 представил Windows Subsystem for Linux (WSL). Хотя это может и выглядеть как ещё одна подсистема, вроде старых подсистем POSIX и OS/2, поддерживаемых Windows, это вовсе не так. Эти старые подсистемы были способны выполнять прикладные приложения POSIX и OS/2, когда они были скомпилированы при помощи компилятора Windows для применения формата PE и системных вызовов Windows. WSL, с другой стороны, не имеет такого требования. Имеющиеся исполняемые файлы из Linux (хранящиеся в формате ELF) могут исполняться как- есть в Windows, без какой бы то ни было повторной компиляции. Чтобы нечто подобное заработало, был создан новый тип процесса- процесс Pico совместно с поставщиком Pico. Если кратко, Pico - это пустое адресное пространство (минимальный процесс), которое применяется для процессов WSL, в котором каждый системный вызов (системный вызов Linux) должен быть перехвачен и переведён в эквивалент системного вызова (вызовов) системы Windows с применением этого поставщика Pico (некого драйвера устройства). В самой машине Windows имеется установленным реальный Linux (его часть режима пользователя). Выше приведено описание WSL версии 1. Начиная с Windows 10 версии 2004, Windows поддерживает новую версию WSL, носящую название WSL 2. WSL 2 более не основывается на процессе Pico. Вместо этого она базируется на некой гибридной технологии виртуальной машины, которая позволяет устанавливать полную систему Linux (включая собственно ядра Linux), но всё ещё видеть и совместно использовать имеющиеся ресурсы машины Windows, например, её файловую систему. WSL 2 быстрее WSL 1 и решает некоторые критические моменты, которые не работали как положено в WSL 1, благодаря реальной обработке ядром Linux системных вызовов Linux {Прим. пер.: подробнее в нашем переводе Linux подсистема Windows (WSL) для профессионалов Хайдена Барнса, 2021, Apress}. |
Для применения процессами режима пользователя,самим ядром и драйверами режима ядра имеющееся ядро Windows выставляет различные типы объектов. Экземпляры этих типов являются структурами данных в пространстве системы, причём создаваемыми Диспетчером объектов (частью Супервизора) при при запросов на осуществление этого кодом режима пользователя или режима ядра. Объекты обладают подсчётом ссылок на них - только когда самая последняя ссылка на соответствующий объект высвобождается, такой объект будет уничтожен и освобождён из памяти.
Поскольку эти экземпляры объектов располагаются в системном пространстве, к ним не может выполняться непосредственный
доступ из режима пользователя. Режим пользователя должен применять механизм косвенного доступа, носящего название дескрипторов
(handles). Дескриптор это некий индекс для какой- то записи в таблице, сопровождаемой процессом на основе процесса,
причём хранимой в пространстве ядра, который указывает на какой- то объект ядра, располагающийся в системном пространстве. Для
создания/ открытия объектов и выполнения выборки дескрипторов обратно в эти объекты существуют различные функции
Create*
и Open*
. Например, функция режима
пользователя CreateMutex
позволяет создавать или открывать некую взаимную блокировку
(зависящую от того имеется ли название такого объекта и существует ли он). В случае успеха, такая функция возвращает некий
дескриптор такого объекта. Возвращение значения нуля означает не верный дескриптор (и отказ в вызове функции). Соответствующая
функция OpenMutex
, с другой стороны, выполняет попытку открытия дескриптора во
взаимном исключении с названием. Когда такое взаимное исключение с названием не существует, эта функция завершается неудачей
и возвращает null (0
).
Код ядра (и драйвера) может пользоваться либо дескриптором, либо непосредственным указателем на некий объект. Конкретный
выбор обычно основывается на том API, который желает вызвать соответствующий код. В некоторых ситуациях выдаваемый режимом
пользователя в надлежащий драйвер дескриптор должен приводиться к указателю при помощи функции
ObReferenceObjectByHandle
. Подробности этого мы обсудим в этой главе позднее.
Замечание | |
---|---|
Большинство возвращают при отказе null (ноль), однако для некоторых это не так. Наиболее примечательная,
функция |
Предостережение | |
---|---|
Значения дескриптора кратны 4, причём самым первым допустимым дескриптором является 4; Ноль никогда не является допустимым значением дескриптора. |
Код режима ядра способен применять дескрипторы при создании/ открытии объектов, однако он также может применять
непосредственные указатели на объекты ядра. Как правило, это осуществляется при его запросе определённым API. Код ядра может
получить некий указатель на объект определённого допустимого дескриптора при помощи функции
ObReferenceObjectByHandle
. В случае успеха, такая ссылка инкрементально увеличивает
счётчик данного объекта, а потому нет никакой опасности в том, что когда удерживающий этот дескриптор клиент режима
пользователя примет решение закрыть его в то время как код ядра содержит указатель на объект, который теперь будет содержать
висячий указатель. Такой объект безопасен для доступа вне зависимости от самого держателя дескриптора до тех пор, пока сам
код ядра не вызовет ObDerefenceObject
, который уменьшит значение счётчика ссылок;
если код ядра пропустил такой вызов, это является утечкой ресурса, которая будет устранена лишь при следующем запуске
системы.
Все объекты пересчитываются по ссылкам на них. Сам диспетчер объектов поддерживает счётчик дескрипторов и общий счётчик ссылок для объектов. Как только некий объект не нужен, его клиент обязан закрыть соответствующий дескриптор (когда для доступа к объекту применялся дескриптор), либо разыменовать этот объект (если клиент ядра пользуется указателем). С этого момента соответствующий код обязан рассматривать свой дескриптор/ указатель как недействительный. Когда счётчик ссылок объекта достигает нуля, Диспетчер объектов уничтожит его.
Всякий объект указывает на некий тип объекта, который содержит сведения по самому этому типу, что означает, что для каждого типа объекта существует единственный тип. Они также выставляются в качестве экспортируемых глобальных переменных ядра, некоторые из которых определяются в заголовках самого ядра и необходимы при определённых обстоятельствах, как мы это обнаружим в последующих главах.
Некоторые типы объектов способны обладать названиями. Эти названия могут применяться для открытия объекта по имени при
помощи подходящей функции Open
. Обратите внимание, что не все объекты
обладают именами; например, процессы и потоки не имеют названий - у них имеются идентификаторы. Именно поэтому функции
OpenProcess
и OpenThread
вместо базирующегося
на строке названия требуют идентификатора (некого числа) процесса/ потока. Другой чудной вариант объекта, у которого нет
имени, это файл. Собственно название файла это не имя его объекта - это разные понятия.
Совет | |
---|---|
Представляется, что у потоков также имеется название (начиная с Windows 10), которое можно устанавливать при помощи
при помощи API режима пользователя |
Вызов функции Create
с названием из кода режима пользователя создаёт объект
с таким названием, однако когда он уже имеется, он просто открывает этот существующий объект. В таком последнем случае, вызов
GetLastError
возвращает ERROR_ALREADY_EXISTS
, что
указывает на то, что это не новый объект и возвращаемый дескриптор это всё ещё иной дескриптор на имеющийся объект.
Такое предоставляемое в функции Create
в действительности не значение
окончательного названия объекта. Перед ним стоит \Sessions\x\BaseNamedObjects\
,
где x это идентификатор значения сеанса вызывающей стороны. Когда такое значение сеанса ноль, Перед данным именем стоит
\BaseNamedObjects\
. Когда вызывающая сторона оказывается в AppContainer (обычно
это процесс Универсальной платформы Windows - Universal Windows Platform), тогда предваряемая строка является более сложной и
состоит из уникального идентификатора безопасности (SID) AppContainer:
Sessions\x\AppContainerNamedObjects\{AppContainerSID}
.
Всё сказанное выше означает, что имена объектов зависят от сеанса (а в случае AppContainer - зависят от пакета). Когда некий
объект должен разделяться по сеансам, он может создаваться в сеансе 0
при помощи
подготовки значения имени объекта с Global\
; например, создание взаимного
исключения при помощи функции CreateMutex
с названием
Global\MyMutex
, создаст его под
\BaseNamedObjects
. Обратите внимание на то, что AppContainer не обладают
силой применения пространства имён объектов сеанса 0
.
Данная иерархия может просматриваться при помощи инструмента Sysinternals WinObj
(запускаемом с повышенными правами), как это показано на рисунке 1-10.
Отображаемое на Рисунке 1-10 представление это пространство имён Диспетчера объектов, составленного из иерархии именованных
объектов. Вся эта структура целиком хранится в памяти, а манипуляции с ней по мере необходимости выполняются Диспетчером
объектов (частью Супервизора). Обратите внимание на то, что объекты без названий не являются частью данной структуры, что
подразумевает, что те объекты, которые наблюдаются в WinObj
, не составляют все
имеющиеся объекты, а вместо этого, все те объекты, которые были созданы с именами.
Всякий процесс обладает частной таблицей дескрипторов для объектов ядра (вне зависимости от того имеют они названия или нет),
которую можно просматривать при помощи инструментов Sysyinternals Process Explorer
или Handles
. На Рисунке 1-11 показан снимок экрана для Process Explorer,
отображающего некоторые процессы. Отображаемые в представлении дескрипторов столбцы по умолчанию это лишь тип объекта и
название. Однако, как это показано на Рисунке 1-11, имеются и иные столбцы.
По умолчанию, Process Explorer отображает дескрипторы лишь для объектов, обладающих именами (согласно с определением имени в
Process Explorer
, которое мы вскоре обсудим). Для просмотра всех дескрипторов
в процессе, в меню Process Explorer View
выберите
Show Unnamed Handles and Mappings
.
Различные представленные столбцы в таком представлении дескрипторов предоставляют дополнительные сведения для каждого дескриптора. Само значение дескриптора и его тип объекта являются самостоятельно поясняющими. Значение имени столбца слегка коварно. Он отображает истинные имена объектов для Взаимных исключений (мутирующих), Семафоров, Событий, Разделов, Портов ALPC, Заданий, Таймеров, Каталогов (Каталогов Диспетчера объектов, не каталогов файловой системы) и прочих, менее применяемых типов. Тем не менее, иные, показаны с именем, которое имеет отличное от истинного значение именованного объекта:
-
Для объектов процессов и потоков значение имени отображается как их уникальный идентификатор.
-
Для объектов Файлов, показывается значение имени файла (или имени устройства), на которое указывает данный объект файла. Это не то же самое что и название объекта, поскольку не существует возможности получения дескриптора на объект файла по значению имени файла - может быть создан лишь новый объект файла, который обладает доступом к тому же самому лежащему в основе файлу или устройству (в предположении что для самого первоначального объекта файла настройки совместного применения допускают это).
-
Для названий объектов Ключа (Реестра) отображаются значения с его путём и именем ключа реестра. Это не название, по той же самой причине, что и для объектов файлов.
-
Для имён маркеров (token) объектов отображаются значения с именем пользователя, хранимого в этом маркере.
Столбец Access
в представлении дескрипторов
Process Explorer
отображает значение маски доступа, которая применяется для
открытия или создания такого дескриптора. Эта маска доступа выступает ключом к тому, какие операции допускается выполнять с
этим конкретным дескриптором. Например, когда код клиента желает прекратить некий процесс, он обязан сначала вызвать функцию
OpenProcess
для получения дескриптора к такому требуемому процессу с маской доступа
(по крайней мере) PROCESS_TERMINATE
, в противном случае с таким дескриптором не будет
возможности прекращения его процесса. Если такой вызов успешен, тогда вызов
TerminateProcess
обязательно будет успешным.
Вот пример режима пользователя для прекращения процесса с заданным идентификатором процесса:
bool KillProcess(DWORD pid) {
//
// открытие достаточно мощного дескриптора для данного процесса
//
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (!hProcess)
return false;
//
// теперь уничтожаем его с неким произвольным кодом выхода
//
BOOL success = TerminateProcess(hProcess, 1);
//
// закрываем свой дескриптор
//
CloseHandle(hProcess);
return success != FALSE;
}
Столбец Decoded Access
предоставляет текстовое пояснение значение маски
доступа (для некоторых типов объектов), упрощая идентификацию точного доступа, допустимого для определённого дескриптора.
Двойной клик по записи дескриптора (или правый клик с выбором Properties
)
отображает некоторые свойства данного объекта. Рисунок 1-12 показывает снимок экрана образца свойств объекта события.
Совет | |
---|---|
Обратите внимание, что показанный на Рисунке 1-12 диалог для свойств приводимого объекта отличается от значения дескриптора. Иными словами, просмотр свойств объекта из любого дескриптора, который указывает на тот же самый объект, отображает те же самые свойства. |
Свойства на Рисунке 1-12 содержат значение имени объекта (когда оно имеется), его тип, краткое описание , его адрес в памяти
ядра, значение номеров открытых дескрипторов, а также некоторые специфичные для объекта сведения, например, значение
состояния и тип самого отображаемого объекта события. Обратите внимание, что показанное значение
References
не указывает фактическое количество остающихся ссылок на объект
(что имело место вплоть до Windows 8.1). Верный способ обнаружить настоящий счётчик ссылок состоит в применении команды
!trueref
, как это показано тут:
lkd> !object 0xFFFFA08F948AC0B0
Object: ffffa08f948ac0b0 Type: (ffffa08f684df140) Event
ObjectHeader: ffffa08f948ac080 (new version)
HandleCount: 2 PointerCount: 65535
Directory Object: ffff90839b63a700 Name: ShellDesktopSwitchEvent
lkd> !trueref ffffa08f948ac0b0
ffffa08f948ac0b0: HandleCount: 2 PointerCount: 65535 RealPointerCount: 3
В последующих главах мы внимательнее познакомимся со значениями атрибутов объектов и самим отладчиком ядра.
В своей следующей главе мы начнём писать очень простой драйвер чтобы показать и применить многие их тех инструментов, которые потребуются нам позднее в данной книге.