Цель работы: получение практических навыков по использованию Win32 API для исследования памяти Windows
Типы памяти
На рисунке ниже представлена взаимосвязь виртуального адресного пространства процесса с физической и внешней памятью.
Физическая память
Физическая память (Physical memory) — это реальные микросхемы RAM, установленные в компьютере. Каждый байт физической памяти имеет физический адрес (Physical Address), который представляет собой число от нуля до числа на единицу меньшего, чем количество байтов физической памяти. Например, ПК с установленными 64 Мб RAM, имеет физические адреса &Н00000000-&Н04000000 в шестнадцатеричной системе счисления, что в десятичной системе будет 0-67 108 863.
Физическая память (в отличие от файла подкачки и виртуальной памяти) является исполняемой (Executable), то есть памятью, из которой можно читать и в которую центральный процессор может посредством системы команд записывать данные.
Виртуальная память
Виртуальная память (Virtual Memory) — это просто набор чисел, о которых говорят как о виртуальных адресах. Программист может использовать виртуальные адреса, но Windows не способна по этим адресам непосредственно обращаться к данным, поскольку такой адрес не является адресом реального физического запоминающего устройства, как в случае физических адресов и адресов файла подкачки. Для того чтобы код с виртуальными адресами можно было выполнить, такие адреса должны быть отображены на физические адреса, по которым действительно могут храниться коды и данные. Эту операцию выполняет диспетчер виртуальной памяти (Virtual Memory Manager — VMM). Операционная система Windows обозначает некоторые области виртуальной памяти как области, к которым можно обратиться из программ пользовательского режима. Все остальные области указываются как зарезервированные. Какие области памяти доступны, а какие зарезервированы, зависит от версии операционной системы (Windows 9x или Windows NT).
Страничные блоки памяти
Как известно, наименьший адресуемый блок памяти — байт. Однако самым маленьким блоком памяти, которым оперирует Windows VMM, является страница (Page) памяти, называемая также страничным блоком (Page Frame) памяти. На компьютерах с процессорами Intel объем страничного блока равен 4 Кб.
Память файла подкачки
Страничный файл (Pagefile), который называется также файлом подкачки (Swap File) в Windows находится на жестком диске. Он используется для хранения данных и программ точно так же, как и физическая память, но его объем обычно превышает объем физической памяти. Windows использует файл подкачки (или файлы, их может быть несколько) для хранения информации, которая не помещается в RAM, производя, если нужно, обмен страниц между файлом подкачки и RAM.
Таким образом, диапазон виртуальных адресов скорее согласуется с адресами в файле подкачки, чем с адресами физической памяти. Когда такое согласование достигается, говорят, что виртуальные адреса спроецированы (Backed) на файл подкачки, или являются проецируемыми на файл подкачки (Pagefile—Backed).
Набор виртуальных адресов может проецироваться на физическую намять, файл подкачки или любой файл.
Файлы, отображаемые в память
В лабораторной работе ЛАБ_ОС-2 обсуждались файлы, отображаемые в память, там же был приведен пример отображения файла. Любой файл применяется для проецирования виртуальной памяти так же, как для этих целей используется файл подкачки. Фактически единственное назначение файла подкачки — проецирование (Backing) виртуальной памяти.
Поэтому файлы, проецируемые в память подобным образом, называются отображаемыми в память (Memory—Mapped). На предыдущем рисунке изображены именно такие файлы. Соответствующие виртуальные страницы являются спроецированными на файл (File—Backed).
Функция CreateFileMapping объявляется так:
Function CreateFileMapping(hFile: THandle; lpFileMappingAttributes: PSecurityAttributes; flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: DWORD; lpName: PChar): THandle; stdcall;
Function CreateFileMapping; external kernel32 name ‘CreateFileMappingA’;
Она создает объект «отображение файла» (File—Mapping Object), используя дескриптор открытого файла, и возвращает дескриптор этого объекта. Дескриптор может использоваться с функцией MapViewOf File, отображающей файл в виртуальную память:
Function MapViewOfFile(hFileMappingObject: THandle; dwDesiredAccess: DWORD; dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap: DWORD): Pointer; stdcall;
Function MapViewOfFile; external kernel32 name ‘MapViewOfFile’;
Начальный адрес объекта «отображение файла» в виртуальной памяти возвращает функция MapViewOfFile. Можно также сказать, что представление проецируется на файл с дескриптором HFile.
Если параметр HFile, передаваемый функции CreateFileMapping, установлен в -1, то объект «отображение файла» (любые представления, созданные на основе этого объекта) проецируем на файл подкачки, а не на заданный файл.
Совместно используемая физическая память
О физической памяти говорят, что она совместно используется (shared), если она отображается на виртуальное адресное пространство нескольких процессов, хотя виртуальные адреса в каждом процессе могут отличаться. Следующий рисунок иллюстрирует это утверждение.
Если файл, такой как DLL, находится в совместно используемой физической памяти, то о нем можно говорить как о совместно используемом.
Одно из преимуществ файлов, отображаемых в память, заключается в том, что их легко использовать совместно. Присвоение имени объекту «отображение файла» делает возможным совместное использование файла несколькими процессами. В этом случае его содержимое отображено на совместно используемую физическую память (см. рис. ниже). Возможно также совместное пользование содержимого файла подкачки с помощью механизма отображения файла.
В частности, можно создать такой объект, проецируемый на файл подкачки,
Просто установив параметр hFile функции CreateFileMapping в -1.
Адресное пространство процесса
Каждый процесс Win32 получает виртуальное адресное пространство (virtual address space), называемое также адресным пространством, или пространством процесса (process space), объем которого равен 4 Гб. Таким образом, код процесса может ссылаться на адреса с &Н00000000 по &HFFFFFFFF (или с 0 по 232 — 1 = 4 294 967 295 в десятичной системе счисления). Конечно, так как виртуальные адреса — это просто числа, заявление о том, что каждый процесс получает свое собственное виртуальное адресное пространство, выглядит довольно бессмысленным. (Это все равно, что сказать, что каждый человек получает свой собственный диапазон возраста от 0 до 150).
Тем не менее, это утверждение должно означать, что Windows не видит ни какой взаимосвязи в том, что и процесс А, и процесс В используют один и тот же виртуальный адрес, например &Н40000000. В частности, Windows может сопоставить (или не сопоставить) виртуальным адресам каждого процесса разные физические адреса.
Использование адресного пространства в Windows 9X
На рисунке показана общая схема использования адресного пространства процесса в Windows 9x.
Область А
Как следует из рисунка, Windows 9х резервирует область А, объем которой всего лишь 4 Кб, для того же, что и Windows NT первые 64 Кб памяти, — с целью предупреждения о нулевых указателях. Эта область защищена, и попытка обращения к ней из программы пользовательского режима приводит к ошибке нарушения доступа.
Область В
Данная область памяти используется для поддержания совместимости с приложениями DOS и 16-разрядными приложениями Windows. Несмотря на потенциальную доступность, она не должна использоваться для программирования.
Область С
Область С — это адресное пространство, используемое прикладными программами и их DLL. Здесь размещаются также и модули Windows. Например, если приложению требуется управляющий элемент OCX, его модуль будет находиться в этой области.
Область D
Windows 9х отображает системные DLL Win32 (KERNEL32.DLL, USER32.DLL и т. д.) в это адресное пространство. Данные файлы используются совместно, то есть несколько процессов могут обращаться к единственной копии такого файла в физической памяти.
Область D доступна для программ пользовательского режима (однако размещать их здесь не рекомендуется).
Область Е
Данная область также содержит совместно используемые файлы Windows, такие как исполнительная система Windows и ядро, драйверы виртуальных устройств, файловая система, программы управления памятью.
Она также доступна для программ пользовательского режима.
Распределение виртуальной памяти
Каждая страница виртуального адресного пространства может находиться в одном из трех состояний:
· Reserved (зарезервирована) — страница зарезервирована для использования;
· Committed (передана) — для данной виртуальной страницы выделена физическая память в файле подкачки или в файле, отображаемом в память;
· Free (свободна) — данная страница не зарезервирована и не передана, и поэтому в данный момент она недоступна для процесса.
Виртуальная память может быть зарезервирована или передана с помощью вызова API-функции VirtualAlloc:
LPVOID VirtualAlloc(
LPVOID IpAddress, //Адрес резервируемой или выделяемой области.
DWORD dwSise, //Объем области.
DWORD flAllocationType, // Тип распределения.
DWORD flProtect // Тип защиты от доступа.
);
Параметр flAllocationType может принимать значения следующих констант (помимо других);
· MEM_RESERVE — параметр, резервирующий область виртуального адресного пространства процесса без выделения физической памяти. Тем не менее, память может быть выделена при следующем вызове этой же функции;
· МЕМ_СОММIТ — параметр, выделяющий физическую память в оперативной памяти или в файле подкачки на диске для заданного зарезервированною набора страниц.
Эти две константы могут объединяться для того, чтобы зарезервировать и выделить память одной операцией.
Разделение процедур резервирования и передачи памяти имеет некоторые преимущества. Например, резервирование памяти является очень полезной процедурой с точки зрения практичности. Если приложению требуется большой объем памяти, можно зарезервировать всю память, а выделить только ту часть, которая нужна в данный момент, раздвигая, таким образом, временные рамки более трудоемкой операции выделения физической памяти.
Windows тоже использует этот подход, когда выделяет память под стек каждого вновь создаваемого потока. Система резервирует 1 Мб виртуальной памяти под стек каждого потока, но выделяет первоначально только две страницы (8 Кб).
Защита памяти
Параметр flProtect функции virtualAlloc используется для задания типа защиты от доступа, соответствующего вновь выделенной (committed) виртуальной памяти (это не относится к резервируемой памяти). Существуют следующие методы защиты:
· PAGE_READONLY присваивает доступ «только для чтения» выделенной виртуальной памяти;
· PAGE_READWRITE назначает доступ «чтение-запись» выделенной виртуальной памяти;
· PAGE_WRITECOPY устанавливает доступ «запись копированием» (сору-оnwrite) выделенной виртуальной памяти.
· PAGE_EXECUTE разрешает доступ «выполнение» выделенной виртуальной памяти. Тем не менее, любая попытка чтения — записи этой памяти приведет к нарушению доступа;
· PAGE_EXECUTE_READ назначает доступ «выполнение» и «чтение»;
· PAGE_EXECUTE_READWRITE разрешает доступ «выполнение», «чтение» и «запись»;
· PAGE_EXECUTE_WRITECOPY присваивает доступ «выполнение», «чтение» и «запись копированием»;
· PAGE_NOACCESS запрещает все виды доступа к выделенной виртуальной памяти.
Любые из этих значений, за исключением PAGE_NOACCESS, могут комбинироваться при помощи логического оператора OR со следующими двумя флагами:
· PAGE_GUARD определяет помеченные страницы как защищенные (guard page). При любой попытке обращения к защищенной странице система возбуждает исключительную ситуацию STATUS_GUARD_PAGE и снимает с данной страницы статус защищенной. Таким образом, защищенные страницы предупреждают только о первом обращении к ним;
· PAGE_NOCACHES запрещает кэширование выделенной памяти.
Следует объяснить, что такое доступ «запись копированием». Допустим, некоторая страница физической памяти совместно используется двумя процессами. Если она помечена как «только для чтения», то два процесса без проблем могу совместно пользоваться этой страницей. Однако возможны ситуации, когда каждому процессу требуется разрешить запись в эту память, но без воздействия и другой процесс. После установки защиты «запись копированием» при попытке записи в совместно используемую страницу система создаст ее копию специально для процесса, которому нужно осуществить запись. Таким образом, данная страница перестает быть совместно используемой, а представление ее данных в других процессах остается неизменным.
Необходимо отметить, что атрибуты защиты страницы могут быть изменены с помощью API-функции Virtual Protect.
Гранулярность при распределении памяти
Если параметр IpAddress не является кратным 64 Кб, то система округляет указанный адрес в меньшую сторону до ближайшего числа, кратного 64 Кб. Windows всегда выравнивает начальный адрес виртуальной памяти на границу гранулярности распределения (allocation granularily), которая является числом кратным 64 Кб (при использовании процессоров Intel). Другими словами, начальный адрес любого блока зарезервированной памяти представляет собой число кратное 64 Кб.
Кроме того, объем выделяемой памяти всегда кратен объему системной страницы, то есть 4 Кб. Поэтому функция VirtualAlloc будет округлять любое запрашиваемое количество байтов в большую сторону до ближайшего числа, кратного объему страницы.
Дескриптор виртуальных адресов
Система отслеживает, какие из виртуальных страниц являются зарезервированными, при помощи структуры, называемой дескриптором виртуальных адресов (Virtual Address Descriptor — VAD). Другого способа определения не существует.
Пример использования функции GlobalMemoryStatus
API-функция GlobalMemoryStatus, записывающаяся таким образом:
В Delphi:
Procedure GlobalMemoryStatus(var lpBuffer: TMemoryStatus); stdcall;
Procedure GlobalMemoryStatus; external kernel32 name ‘GlobalMemoryStatus’;
Выводит множество данных, имеющих отношение к памяти, в составе следующей структуры:
Struct_MEMORYSTATUS {
DWORD dwLength; // Размер структуры MEMORYSTATUS.
DWORD dwMernoryLoad; // Процент используемой памяти.
DWORD dwTotalPhys; // Количество байтов физической памяти.
DWORD dwАvailPhys; // Количество свободных байтов физической памяти.
DWORD dwTotalPageFile; // Размер в байтах файла подкачки.
DWORD dwAvailPageFile; // Количество свободных байтов файла подкачки.
DWORD dwTotalVirtual; // Количество байтов адресного пространства,
// доступного пользователю.
DWORD dwAvailvirtual; // Количество свободных байтов памяти,
// доступных пользователю.
End Type
В Delphi:
_MEMORYSTATUS = record
dwLength: DWORD;
dwMemoryLoad: DWORD;
dwTotalPhys: DWORD;
dwAvailPhys: DWORD;
dwTotalPageFile: DWORD;
dwAvailPageFile: DWORD;
dwTotalVirtual: DWORD;
dwAvailVirtual: DWORD;
end;
Управление виртуальной памятью
Рассмотрим, как диспетчер виртуальной памяти Windows преобразует адреса виртуальной памяти и физические.
Преобразование виртуальных адресов в физические: попадание
На рисунке показан процесс преобразования при отображении виртуальных адресов в физические. Он называется попаданием в (физическую) страницу (Page Hit).
Все виртуальные адреса делятся на три части. Самая левая часть (биты 22-31) содержит индекс каталога страниц процесса. Windows поддерживает отдельный каталог страниц для каждого процесса. Его адрес хранится в одном из регистров центрального процессора, который называется CR3. (В операцию переключения задач входит переведение CR3 в состояние, когда он указывает на каталог страниц процесса, на который осуществляя переключение.) Каталог страниц одержит 1024 четырехбайтовых элемента.
Windows поддерживает для каждого процесса совокупность таблиц страниц (page table). Каждый элемент каталога страниц содержит уникальный номер. Поэтому Windows поддерживает до 1024 таблиц страниц. ( В действительности таблицы страниц создаются только при попытке обращения к данным или коду конкретному виртуальному адресу, а не когда выделяется виртуальная память).
Следующая часть виртуального адреса (биты 12-21) используется в качестве индекса в таблице страниц, соответствующей выбранному элементу каталога страниц. Каждый элемент таблицы, соответствующий указанному индексу, содержит в 20 старших разрядах номер страничного блока, который задает конкретный страничный блок физической памяти.
Третья, и последняя, часть виртуального адреса (биты 0-11) представляет собой смещение в данном страничном блоке. Сочетание номера страничного блока и смещения дают в совокупности адрес физической памяти.
Так как каждая таблица страниц состоит из 1024 элементов и количество таблиц равно 1024, общее количество страничных блоков, которое можно определить, таким образом, будет 1024 х 1024 = 210 х 210 = 220. Так как каждый страничный блок имеет объем 4 Кб = 4 х 210 байт, то теоретический предел физического адресного пространства будет 4 х 230 = 4 Гб.
У этой довольно сложной схемы преобразования есть несколько важных преимуществ. Одно из них — очень небольшой объем страничных блоков, которые легко могут быть размещены в памяти. Гораздо легче найти непрерывный блок памяти размером 4 Кб, чем, скажем, 64 Кб.
Но основное преимущество заключается в том, что адреса виртуальной памяти двух процессов могут быть сознательно преобразованы в разные или в одни и те же физические адреса.
Предположим, что Process1 и Process2 обращаются в программе к одному и тому же виртуальному адресу. При преобразовании виртуальных адресов в физические для каждого из процессов используются их собственные каталоги страниц. Поэтому, хотя индексы в каталогах страниц одинаковы и в том, и в другом случаях, они все же представляют собой индексы из разных каталогов. Таким способом VMM может гарантировать, что виртуальные адреса каждого процесса будут преобразованы в разные физические адреса.
С другой стороны, VMM может также дать гарантию, что виртуальные адреса двух процессов, независимо от того являются ли они одинаковыми или нет, будут преобразованы в один и тот же физический адрес. Один из способов добиться этого — установить соответствующий элемент в обоих каталогах страниц на одну и ту же таблицу страниц и, следовательно, на один и тот же страничный блок. Таким образом, процессы могут совместно использовать физическую память.
Каталог и таблицы системных страниц
Нужно также упомянуть, что Windows поддерживает каталог системных страниц (System Page Directory) для работы с виртуальной памятью, зарезервированной Windows, так же, как и соответствующую совокупность таблиц системных страниц.
Совместно используемые страницы
Ситуация с совместно используемой физической памятью является значительно более сложной, не будем углубляться в детали, а отметим только, что VMM использует концепцию, называемую прототипированием элементов таблицы страниц. Идея заключается в том, что обычные элементы таблицы каждого из совместно использующих память процессов указывают не на физическую память, а на общий прототип элемента таблицы страниц. А тот, в свою очередь, может ссылаться на совместно используемую физическую память.
Рабочие наборы
Каждая страница виртуального адресного пространства процесса объемом 4 Гб существует в одном из трех состояний — свободном (free), зарезервированном (reserved) или переданном (committed). Теперь можно также сказать, что каждая переданная страница (Committed Page) является или действительной, или недействительной. Совокупность действительных страниц, то есть спроецированных на физическую память, называют рабочим наборам (Working Set) процесса. Рабочий набор постоянно меняется по мере того, как страницы подкачиваются в память или выполняется обратное действие.
Системный рабочий набор (System Working Set) характеризует виртуальные страницы системной памяти, которые в данный момент отображены на физическую память.
Размер рабочего набора процесса ограничен теми установками, которые определяет Windows в зависимости от объема физической памяти. Эти значения приведены в следующей таблице.
Модель памяти |
Объем памяти |
Минимальный размер рабочего набора процесса |
Максимальный размер рабочего набора процесса |
Small Medium Large |
<=19Мб 20-32 Мб >= 33 Мб |
20 страниц (80 Кб) 30 страниц(120Кб) 50 страниц (200 Кб) |
45 страниц (180 Кб) 145 страниц (580 Кб) 345страниц(1380Кб) |
Эти пределы могут быть изменены с помощью API-функции
SetProcessWorkingSetSize:
BOOL SetProcessWorkingSetSize(
HANDLE hProcess, // Открытый дескриптор интересующего процесса.
DWORD dwMinimumWorkingSetSize,
// Задает мин. размер рабочего набора в байтах.
DWORD dwMaximumWorkingSetSize
// Задает максимальный размер рабочего набора в байтах.
);
Присвоение каждому из двух параметров размера значения -1 приведет к тому, что функция сожмет размер рабочего набора до 0 и тем самым временно удалит данный процесс из физической памяти.
Действительный размер рабочего набора процесса может изменяться во времени, так как Windows увеличивает рабочий набор, если замечает, что у процесса большое количество страничных промахов.
Пределы размера системного рабочего набора приведены в следующей таблице.
Модель памяти |
Объем памяти |
Минимальный размер рабочего набора процесса |
Максимальный размер рабочего набора процесса |
Small Medium Large |
<=19Мб 20-32 Мб >= 32 Мб |
388страниц(1,5 Мб) 688 страниц (2,7 Мб) 1188 страниц (4,6 Мб) |
500 страниц (2,0 Мб) 1150 страниц (4,5 Мб) 2050 страниц (8 Мб) |
База данных страничных блоков
Windows фиксирует coстояние каждой физической страницы памяти в структуре данных, называемой базой данных страничных блоков (Page Frame Database). Каждая физическая страница может находиться в одном из восьми различных стояний:
· активная, или действительная (Active, Valid). Страница в текущий момент отображается на виртуальную память, входя, таким образом, в рабочий набор страниц;
· переходная (Transition). Страница в процессе перехода к активному состоянию;
· резервная (Standby). Страница только что вышла из состояния «активная», но осталась неизменной;
· измененная (Modified). Страница вышла из состояния «активная». Ее содержание, пока она находилась в указанном состоянии, было изменено, но еще не записано на диск;
· измененная незаписанная (Modified No Write). Страница находится в состоянии «измененная», но особо помечена как страница, содержимое которой не сброшено на диск. Используется драйверами файловой системы Windows;
· свободная (Free). Страница свободна, но содержит произвольные записи и, следовательно, не может использоваться процессом;
· обнуленная (Zeroed). Страница свободна и инициализирована нулями потоком нулевой страницы. Может быть выделена процессу;
· плохая (Bad). В странице были отмечены ошибки четности или какие-то другие аппаратные ошибки, поэтому она не должна использоваться.
Кучи памяти в 32-разрядной Windows
При создании процесса Windows назначает ему кучу по умолчанию (Default Heap), то есть изначально резервирует область виртуальной памяти объемом 1 Мб. Тем не менее, при необходимости система будет регулировать размер кучи, которая используется самой Windows для различных целей.
API-функция GetProcessHeap используется для получения дескриптора кучи. При помощи функции HeapCreatЕ, возвращающей дескриптор кучи, программист может создавать дополнительные кучи.
Есть несколько причин создавать дополнительные кучи вместо того, чтобы использовать кучу по умолчанию. Например, те кучи, которые предназначены для конкретных задач, часто оказываются более эффективными. Кроме того, ошибки записи данных в кучу, память для которой выделена из специализированной кучи, не затронут данных других куч. Наконец, выделение памяти из специализированной кучи в общем случае будет означать, что данные в памяти упакованы более плотно друг к другу, а это может уменьшить потребность в загрузке страниц из файлa подкачки. Следует также упомянуть, что доступ к куче упорядочен (Serialized), то есть система заставляет каждый поток, пытающийся обратиться к памяти кучи, дожидаться своей очереди, пока другие потоки не закончат производимые операции. Следовательно, только один поток в каждый момент времени может выделять или освобождать память кучи во избежание неприятных конфликтов.
16-разрядная Windows поддерживает и глобальную, и локальную кучи. Соответственно в данной системе реализованы функции GlobalAlloc и LocalAlloc. Они выполняются, но не очень эффективны, поэтому следует избегать их применения в Win32. Однако их все-таки приходится использовать для некоторых целей, таких как создание окна просмотра буфера обмена.
Функции работы с кучей
Для работы с кучами используются следующие функции:
· GetProcessHeap возвращает дескриптор кучи процесса по умолчанию;
· GetProcessHeaps возвращает список дескрипторов всех куч, используемых в данный момент процессом;
· HeapAlloc выделяет блок памяти из заданной кучи;
· HeapCompact дефрагментирует кучу, объединяя свободные блоки. Может также освобождать неиспользуемые страницы памяти кучи;
· HeapCreatE создает новую кучу в адресном пространстве процесса;
· HeapDestroy удаляет заданную кучу;
· HeapFree Освобождает предварительно выделенные блоки памяти кучи;
· HeapLock блокирует кучу, при использовании данной функции только один поток имеет к ней доступ. Другие потоки, запрашивающие доступ, переводятся в состояние ожидания до тех пор, пока поток, владеющий кучей, не разблокирует ее. Это одна из форм синхронизации потоков, то есть тот прием, которым система реализует упорядоченность доступа;
· HeapReAlloc перераспределяет блоки памяти кучи. Используется для изменения размера блока;
· Heapsize возвращает размер выделенного блока памяти кучи;
· HeapUnlock разблокирует кучу, которая до этого была заблокирована функцией HeapLock;
· HeapValidate проверяет пригодность кучи (или отдельного ее блока), если имеются ли какие-либо повреждения;
· HeapWalk позволяет программисту просматривать содержимое кучи. Обычно используется при отладке.
Отображения виртуальной памяти
Функция Win32 API VirtualQuery может использоваться для получения информации о состоянии адресов виртуальной памяти. Синтаксис ее таков:
DWORD VirtualQuery(
LPCVOID IpAddress, // Адрес области.
PMEMORY_BASICONFORMATION IpBuffer, // Адрес информационного буфера
DWORD DwLength // Размер буфера
);
Используется также функция VirtualQueryEx, расширенная версия VirtualQuery, которая позволяет получать информацию о внешних виртуальных адресных пространствах:
DWORD VirtualQueryEx(
HANDLE hProcess // Дескриптор процесса
LPCVOID IpAddress, // Адрес области
MEMORY_BASIC_INFORMATION IpBuffer, // Адрес информационного буфера
DWORD DwLength // Размер буфера
);
Параметр HProcess — это дескриптор процесса. Параметр IpAddress — это начальный адрес для записи результирующих данных, который будет округляться в меньшую сторону до ближайшего кратного размеру страницы (4 Кб). Обе функции возвращают информацию в следующую структуру.
Struct MEMORY_BASlC_INFORMATION {
PVOID BaseAdciress; // Базовый адрес области
PVOID AllocationBase; // Базовый адрес выделенной области
DWORD AllocationProtect; // Первоначальная защита от доступа
DWORD RegionSize; // Размер области в байтах
DWORD State; // Передана зарезервирована, свободна
DWORD Protect; // Текущая защита от доступа
DWORD Type; // Тип страниц
}
Чтобы понять принцип действия членов этой структуры, необходимо знать о назначении данной функции. Чтобы сделать определение более понятным, назовем страницу, которой принадлежит адрес IpAddress, заданной (Specified) Следующий рисунок поможет разобраться в новой терминологии.
Функция VirtualQueryEx всегда заполняет следующие члены структуры
MEMORY_BASIC_INFORMATION:
· BaseAddress, которая возвращает базовый адрес заданной страницы;
· RegionSize, представляющая собой количество байтов от начала заданной страницы до вершины заданной области.
Если страница, содержащая адрес IpAddress, свободна (не зарезервирована и не передана), член структуры Stаte содержит символьную константу MEM_FREE. Остальные члены (кроме BaseAddress и RegionSize) не имеют значения.
Если страница, содержащая адрес IpAddress, не свободна, функция определяет выделенную область (allocation region), то есть область виртуальной памяти, которая включает заданную страницу и была, первоначально выделена с помощью вызова функции VirtualAlloc.
Начиная с базового адреса заданной страницы, функция последовательно просматривает все страницы выделенной области, проверяя, совпадают ли их типы выделения (Allocation Type) и защиты (Protection Type) с аналогичными типами заданной страницы. Совокупность всех совпадающих упорядоченных страниц представляет собой заданную область. К ней относятся значения структуры MEMORY_BASIC_INFORMATION. Cтраница считается совпадающей с заданной страницей, если она удовлетворяет двум следующим условиям:
· страница имеет тот же тип выделения, что и первоначальная страница, в соответствии со следующими значениями флага: MEM_COMMIT, MEM_RESERVE, MEM_FREE, MEM_PRIVATE, MEM_MAPPED Или MEM_IMAGE;
· страница имеет тот же тип зашиты, что и первоначальная страница, в соответствии со следующими значениями флага: PAGE_READONLY, PAGE_READWRITE, PAGE_NOACCESS, PAGE_WRITECOPY, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD Или PAGE_NOCACHE.
Рассмотрим остальные члены структуры MEMORY_BASIC_INFORMATION:
· AllocationBase — базовый адрес выделенной области;
· AllocationProtect — первоначальный тип защиты выделенной области;
· State — одно из трех значений: MEM_FREE, MEM_RESERVE или МЕМ_СОММIТ. Относится к заданной области;
· Protect — текущий тип защиты заданной области;
· Туре — одно из трех значений: MEM_IMAGE, MEM_MAPPED Или MEM_PRIVATE. Относится к заданной области. Эти константы имеют следующий смысл: MEM_IMAGE указывает, что область отображена на файл образа задачи (Image File), то есть на загрузочный; MEM_MAPPED указывает, что область отображена на не загрузочный отображаемый в память файл (например, файл данных); MEM__PRIVATE указывает, что область используется одним процессом, а не совместно.
СОДЕРЖАНИЕ ОТЧЕТА
8. Наименование лабораторной работы, ее цель.
9. Разработанное программное обеспечение для приложения, которое:
· выдает информацию, получаемую при использовании API GlobalMemoryStatus. При выводе информации использовать диаграммы.
· Составляет карту виртуальной памяти для любого процесса.
10. Примеры разработанных приложений (результаты и тексты программ).
Лабораторная
работа № 4
Цель работы:
получение практических навыков по
использованию Win32
API
для исследования памяти Windows
Типы памяти
На рис. 2 представлена
взаимосвязь виртуального адресного
пространства процесса с физической и
внешней памятью.
Рис. 2
Физическая память
Физическая память
— это реальные микросхемы RAM,
установленные в компьютере. Каждый байт
физической памяти имеет физический
адрес, который представляет собой число
от нуля до числа на единицу меньшего,
чем количество байтов физической памяти.
Например, ПК с установленными 64 Мб RAM,
имеет физические адреса &Н00000000-&Н04000000
в шестнадцатеричной системе счисления,
что в десятичной системе будет 0-67 108
863.
Физическая память
(в отличие от файла подкачки и виртуальной
памяти) является исполняемой, то есть
памятью, из которой можно читать и в
которую центральный процессор может
посредством системы команд записывать
данные.
Виртуальная память
Виртуальная память
(virtual
memory)
— это просто набор чисел, о которых
говорят как о виртуальных адресах.
Программист может использовать
виртуальные адреса, но Windows
не способна по этим адресам непосредственно
обращаться к данным, поскольку такой
адрес не является адресом реального
физического запоминающего устройства,
как в случае физических адресов и адресов
файла подкачки. Для того чтобы код с
виртуальными адресами можно было
выполнить, такие адреса должны быть
отображены на физические адреса, по
которым действительно могут храниться
коды и данные. Эту операцию выполняет
диспетчер виртуальной памяти (Virtual
Memory
Manager
— VMM).
Операционная система Windows
обозначает некоторые области виртуальной
памяти как области, к которым можно
обратиться из программ пользовательского
режима. Все остальные области указываются
как зарезервированные. Какие области
памяти доступны, а какие зарезервированы,
зависит от версии операционной системы
(Windows
9x
или Windows
NT).
Страничные блоки памяти
Как известно,
наименьший адресуемый блок памяти —
байт. Однако самым маленьким блоком
памяти, которым оперирует Windows
VMM,
является страница памяти, называемая
также страничным блоком памяти. На
компьютерах с процессорами Intel
объем страничного блока равен 4 Кб.
Память файла подкачки
Страничный файл,
который называется также файлом подкачки,
в Windows
находится на жестком диске. Он используется
для хранения данных и программ точно
так же, как и физическая память, но его
объем обычно превышает объем физической
памяти. Windows
использует файл подкачки (или файлы, их
может быть несколько) для хранения
информации, которая не помещается в
RAM,
производя, если нужно, обмен страниц
между файлом подкачки и RAM.
Таким образом,
диапазон виртуальных адресов скорее
согласуется с адресами в файле подкачки,
чем с адресами физической памяти. Когда
такое согласование достигается, говорят,
что виртуальные адреса спроецированы
на файл подкачки, или являются
проецируемыми на файл подкачки.
Набор виртуальных
адресов может проецироваться на
физическую память, файл подкачки или
любой файл.
Соседние файлы в папке Задания к лабам
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
Пост создан для публикации одной единственной картинки (кликабельна). На мой взгляд, она как нельзя лучше передаёт атмосферу того как устроена память в компьютере. Я изобразил её на 3-4 курсе института на A4 при объяснении положения дел товарищу. Так она мне тогда понравилась, что дал себе обещание нарисовать её в электронном виде.
Исходник в формате VSD (Microsoft Visio) лежит у меня в DropBox. Если имеются уточнения, поправки, комментарии — прошу под кат.
Отношение между ВАП и ВП:
Образ загружается в виртуальную память системы (ВП) единожды и проецируется в те виртуальные адресные пространства (ВАП), в которых он востребован. Образ EXE или DLL идентифицируется уникальным путём к файлу. Соответственно, если к одному и тому же файлу ведут несколько путей, можно произвести множественную загрузку одного и того же модуля. В системе имеется набор DLL, которые подгружаются во все программы, такие как kernel32.dll и прочие…
Отношение между ВП и ФП:
Физическая память (ФП) состоит из страниц. Любой модуль EXE или DLL занимает некоторое число таких страниц. При загрузке системой модулей для них ищутся свободные страницы, в которые загружается образ файла. Модули проецируются в виртуальную память и загружаются в физическую по мере необходимости средствами ОС. Также проецирование можно использовать для пользовательских файлов (также называется маппинг файлов — mapping).
Отношение между ФП и железом:
Операционная система может по своему усмотрению перемещать страницы физической памяти в файл подкачки и обратно. При обращении исполняемого кода к странице, находящейся в файле подкачки, производится выгрузка страницы в ОЗУ. Если к странице долгое время не осуществлялся доступ и возникла потребность в памяти, ОС может перемещение страницы в файл подкачки.
В системной архитектуре отображение вирт.памяти на физическую носит таинственный характер. Предлагая материал на эту тему, MSDN забывает о логической нити – по большому счёту у них тонны безсвязных отрывков. В результате сеть заполонили статьи о диспетчерезации памяти, однако инфа в них сильно разнится, поскольку каждый из авторов трактует оригинальные доки по своему. Приняв это во-внимание, в данной статье был выбрал формат «беседа с новичком», чтобы рассмотреть такие вопросы как: задачи MMU и VMM, область применения списков VAD и MDL, назначение базы PFN, состав рабочих страниц WorkingSet, способы трансляции адресов, и многое другое. Из оружий ближнего боя понадобится отладчик WinDbg, и том(3) мануалов Intel
Ссылка скрыта от гостей
, как внушающий доверие источник информации.
Оглавление:
1. FAQ – часто задаваемые вопросы;
2. Древо VAD процесса (Virtual Address Descriptor);
3. WorkingSet – набор рабочих страниц;
4. MMU – блок управления физической памятью;
5. База данных PFN (Page Frame Number);
6. Списки MDL (Memory Descriptor List);
7. Практика – сбор информации;
8. Постскриптум.
1. FAQ – Frequently Asked Questions
Чтобы далее не возникало лишних вопросов, начнём с определений часто встречающихся в технической литературе терминов. Практика показывает, что многие рерайтеры не обременяют себя даже чтением официальных доков от Intel и Microsoft, и как результат «сущности» получают совсем другие имена. На входе в тему фундамент очень важен, иначе продвигаясь дальше, заинтересованный читатель будет петлять траекторией пьяного гонщика, то и дело обращаясь к манам в поисках истины. Не зря говорят, как корабль назовёшь, так он и поплывёт. Вот основные моменты, на которые делается ставка в данной статье.
…::: Примечание :::…
Всё пространство физ.памяти ОЗУ почекано на 4-Кбайтные фреймы, которые называют ещё кадрами.
Термин «Frame» ввели, чтобы не путать страницу «Page» вирт.памяти, с физической. Таким образом,
если мы говорим «страничный фрейм», значит речь идёт о физ.памяти, а если просто «страница» – подразумеваем виртуальную.
В идеале фрейм, страница, и кластер жёсткого диска должны быть одинаковых размеров, и в большинстве случаях это 4096-байт.
1.1. Какая разница между блоком
MMU и диспетчером памяти?
Часто MMU обзывают диспетчером, что не соответствует действительности. MMU – это аппаратный блок управления памятью «Memory Management Unit», и находится он внутри процессора. Помимо прочего, содержит в себе транслятор адресов, а так-же небольшой кэш в виде буфера TLB «Translation Lookaside Buffer». Если-же говорить о диспетчере памяти, в доках он числится как VMM «Virtual Memory Manager» (не путать с Virtual Machine Manager) и это не аппаратный, а программный модуль ОС. Функции Kernel-API диспетчера прописаны в файле ядра Ntoskrnl.exe и имеют префиксы MmMi_xx(). Сервисы VMM занимают добрую половину всего функционала ядра, что говорит об особой их важности.
1.2. Зачем нужны регистры MTRR?
Регистры MTRR процессора входят в состав MSR (Model Specific Registers) и означают «Memory Type Range Registers» – регистры диапазонов типа памяти. Если транслятор в MMU озадачен адресацией страничных фреймов, то 23 регистра MTRR задают атрибуты кэширования этим фреймам. Жонглируя битами MTRR процессор на аппаратном уровне может определить до 96 областей памяти, с одним из пяти типом кэша: UnCacheable, WriteProtect, WriteBack, WriteThrough, WriteCombining. Бит[11] регистра IA32_MTRR_Def_Type
системный BIOS использует для вкл/откл этого рульного механизма. Всех, кого интересует данная фишка, копайте доку
Ссылка скрыта от гостей
. В системе имеется альтернатива аппаратным MTRR под названием РАТ, или «Page Attribute Table». Она не имеет уже ограничений на кол-во подконтрольных блоков памяти, т.к. работает на уровне записей РТЕ каждой из страниц (PageTableEntry).
1.3. Что такое системный кэш?
Не нужно путать кэши L1,2,3 процессора с системным кэшем Win – это два разных субъекта, хотя и придерживаются одной веры. Диспетчер кэша состоит из набора ядерных API, обеспечивающих кэширование файлов NTFS. Такой подход на порядок увеличивает скорость операций ввода-вывода при работе с дисковыми файлами. В дефолте кеш всегда включён, но в избирательном порядке механизм можно усыпить при открытии файловых объектов чз CreateFile() (см.параметр Flags&Attributes). К примеру флаг «NO_BUFFERING» даёт постановку диспетчеру вообще не буферизоватькэшировать содержимое данного файла в памяти, а флаг «WRITE_THROUGH» предписывает сквозную запись изменённых файлов и в активный кэш, и сразу на диск. Это отнимает больше времени, зато получаем согласованность данных на диске и в памяти.
На моей Win-7 системный кэш загребает у ядра порядка 600 MБ. Он состоит из т.н. «слотов» размером по 256K, каждый слот описывает своя структура VACB (Virtual Address Control Block). Функции диспетчера-кэша имеют префикс Сc_хх() (Cache control). Чтобы просмотреть его содержимое, можно потянуть за расширение !filecache
отладчика WinDbg. Во-втором томе издания(6) М.Руссиновича кэшу посвящена целая глава, а мнение MSDN на этот счёт лежит по следующим линкам:
Ссылка скрыта от гостей
, и сразу
Ссылка скрыта от гостей
файлов.
1.4. Каково назначение списков MDL?
Memory Descriptor List (или список дескрипторов памяти) представляет собой одноимённую структуру в ядерном пространстве, для отображения фрейма на страницу. Диспетчер заносит в структуру MDL адреса страниц только в двух случаях – когда устройства DMA запрашивают прямой доступ к физ.памяти (Direct Memory Access), или-же функция DeviceIoControl() просит драйвер вернуть ей какие-нибудь данные, передавая этому драйверу вирт.адрес своего приёмного буфера. Позже мы заглянем внутрь этой структуры.
1.5. Какую роль играет древо VAD?
Всякий процесс требует определённого кол-ва страниц вирт.памяти. В ядре имеется структура VAD (Virtual Address Descriptor, дескриптор вирт.адреса), которая описывает один непрерывный регион памяти с одинаковыми атрибутами. Например сотню идентичных по характеру последовательных страниц, описывает всего одна запись VAD. В силу того, что процессу требуется память с различными флагами защиты (чтение, запись, исполнение), то получаем несколько структур VAD. Для комфортного доступа и сортировки адресов, диспетчер собирает все принадлежащие данному процессу VAD’ы, в двоичное древо. Если учесть, что у каждого процесса своя вирт.память, то соответственно и своё древо VAD.
1.6. В чём смысл набора «WorkingSet«?
Прописанные в VAD страницы представляют «рабочий набор» процесса, что инглише звучит как «WorkingSet». Это ещё один клиент диспетчера, поскольку страницы могут находиться в различном состоянии типа: не тронутая с атрибутом READ-ONLY, модифицированная WRITE, только-что выделеннаячистая, зарезервированная, обнулённая после модификации, отсутствующая и т.д. VMM обязан следить за состоянием всех страниц в наборе, и при обнаружении проблем принимать соответствующие меры.
1.7. Что хранится в базе данных PFN?
PFN берёт начало от «Physical Frame Number», т.е. просто номер физ.фрейма. Специальный поток диспетчера включает свой радар и в непрерывном режиме следит, в каком из состояний находится конкретно взятый фрейм – варианты: занят, свободен, расшаренный, кэшируемый, можно-ли сбрасывать его в файл-подкачки (Paged, Non-Paged) и прочее. Для этого, с каждым фреймом связывается 28-байтная структура _MMPFN
, но поскольку фрейм у памяти не один, все структуры собираются в системную «базу PFN». Более того имеются и прототипы PFN (prototype), с помощью которых диспетчер открывает доступ к фреймам всем желающим – это т.н. расшаренные фреймы, например для отображения Ntdll и Kernel32.dll сразу во-все пользовательские процессы.
Пробежавшись по макушкам терминов посмотрим на схему ниже, где представлена логическая связь между основными структурами VMM. Ясно, что этот рисунок не отражает всей палитры, ведь полностью охватить хозяйство вирт.памяти в одном скрине просто нереально. Поэтому MSDN и подкидывает нам инфу жалкими крапалями. Однако общую картину зрительно уже можно будет сформировать:
Значит у каждого процесса своё древо VAD, где хранятся адреса его регионов памяти. Далее страницы попадают в «котёл» WorkingSet для фильтрации их по назначению. Когда декодер-инструкций процессора обнаруживает запрос к ОЗУ, он передаёт адрес в блок MMU, чтобы транслятор преобразовал его в физический. Вирт.адреса одинаковы у всех процессов, но благодаря базе PFN они указывают на разные фреймы памяти.
Чтобы процесс(А) не считал данные процесса(В), диспетчеру нужно сменить каталог-страниц процесса «PageDirectoryTable», внутри которого имеются записи PDTE – этим занимается планировщик Scheduler, при переключении с одного потока на другой. Адрес каталога-страниц верхнего уровня РDТ каждого из процессов, система запоминает в его ядерной структуре _KPROCESS
, от куда он считывается шедулером в регистр CR3 (PDBR, Page Directory Base Register).
Теперь сфокусируем своё внимание на системных таблицах и посмотрим, информацию какого рода хранит в них диспетчер. Пробираться по тёмным переулкам памяти будем таким маршрутом, как это указано на схеме выше, т.е. сверху вниз.
2. Древо VAD процесса – Virtual Address Descriptor
В отличие от планировщика, который выбирает потоки Thread для исполнения и пропускает между ног процессы, диспетчер наоборот полностью концентрируется на процессах и не подозревает о существовании потоков, ведь именно процессы (а не потоки) владеют адресным пространством. Когда программа запускается на исполнение, загрузчик образов в Ntdll.dll считывает её РЕ-заголовок (сколько секций, какого размера и т.д.), и на основании этого выделяет процессу вирт.память. При этом диспетчер создаёт сразу несколько дескрипторов VAD, в которых прописаны диапазоны отображаемых адресов. Таким образом, адресное пространство процесса полностью определяется списком его VAD.
В каждой структуре VAD хранится вирт.адрес первой и последней страницы в данном регионе памяти (Start и EndVPN), а если в нём отображается какой-нибудь файл, то и полный путь до него. Чтобы поиск отдельных VAD в списке был эффективным, все они выстраиваются в виде бинарного AVL-древа, которое имеет корень «VadRoot» и разветвляющиеся вниз узлы «VadNode». Формат деревьев AVL такой, что слева от узла всегда будут находиться VAD с меньшим от родителя стартовым адресом, а справа – большим. Такой подход позволяет с лёгкостью сортировать страницы по возрастанию или убыванию. Графическое представление древа типа AVL представлено ниже:
Обратите внимание на поле StartVPN (VirtualPageNumber) в структурах VAD.
Во-первых, значение в левом узле всегда будет меньше чем у родителя, а в правом больше. Во-вторых, поскольку регионы памяти выравниваются на границу 4К (размер одной страницы), то для экономии в VAD указываются только старшие 20-бит адреса, а младшие 12 отброшены, т.к. зарезервированы под смещение внутри выбранного пейджа (2^12=4096). То-есть чтобы получить полный вирт.адрес, нужно дополнять значения всех адресов тремя hex-нулями справа. Тогда получается, что корневой VAD описывает регион из трёх страниц с адресами от 0x00400000 до 0x00403000, и при инициализации система назначила ему атрибуты Exe+Write+Copy (полный доступ). Позже, в записях РТЕ ненужные атрибуты для конкретных страниц снимаются.
В нёдрах ядра NT структура VAD числится как _MMVAD
, так-что запустив отладчик WinDbg можно просмотреть её содержимое. Только для начала нужно узнать, по какому адресу лежит корень древа конкретно нашего процесса. Для этого, запустим какую-нибудь свою прожку и не закрывая её, в отладчике потянем за расширение !process 0 0
, чтобы он показал нам карту нашего процесса (у меня это ModuleInfo.exe):
Код:
lkd> !process 0 0 ModuleInfo.exe
;//------------------------------
PROCESS 89102348 SessionId: 1 Cid: 0ed4 Peb: 7ffd3000 ParentCid: 0da0
DirBase: 5f590d00 ObjectTable: b2f14598 HandleCount: 7
Image: ModuleInfo.EXE
--> VadRoot 86688b20. Vads 21. Clone 0. Private 60. Modified 50. Locked 0.
В последней строке лога видим линк на корень древа VadRoot, и теперь можно просмотреть его структуру:
Код:
lkd> dt _MMVAD 86688b20
;//----------------------
nt!_MMVAD
+0x000 u1 : <unnamed-tag>
+0x004 LeftChild : 0x891a0eb8 _MMVAD
+0x008 RightChild : 0x89a9e398 _MMVAD
+0x00c StartingVpn : 0x400
+0x010 EndingVpn : 0x403
+0x014 u : <unnamed-tag>
+0x018 PushLock : _EX_PUSH_LOCK
+0x01c u5 : <unnamed-tag>
+0x020 u2 : <unnamed-tag>
+0x024 Subsection : 0x891d09d8 _SUBSECTION
+0x024 MappedSubsection : 0x891d09d8 _MSUBSECTION
+0x028 FirstPrototypePte : 0x93ee1840 _MMPTE
+0x02c LastContiguousPte : 0xfffffffc _MMPTE
+0x030 ViewLinks : _LIST_ENTRY [ 0x891d09d0 - 0x891d09d0 ]
+0x038 VadsProcess : 0x89102348 _EPROCESS
Значит в каждой структуре VAD представлен уже знакомый нам диапазон памяти StartEndVPN, а так-же указатели на LeftRight узлы дочернего уровня. Что касается атрибутов защиты данного региона, они спрятаны во-вложенных безымянных структурах u..u5
(union). Чтобы раскрыть их, достаточно указать имя поля в структуре, и поставить в конце точку:
Код:
lkd> dt _MMVAD 86688b20 u.
;//------------------------
nt!_MMVAD
+0x000 u1 :
+0x000 Balance : 0y00
+0x000 Parent : 0x891025c0 _MMVAD ;//<--- линк на родителя
+0x014 u :
+0x000 LongFlags : 0x7200002
+0x000 VadFlags : _MMVAD_FLAGS
+0x01c u5 :
+0x000 LongFlags3 : 0
+0x000 VadFlags3 : _MMVAD_FLAGS3
+0x020 u2 :
+0x000 LongFlags2 : 0x40000000
+0x000 VadFlags2 : _MMVAD_FLAGS2
Поле «Parent» в структуре u1
хранит указатель на родительский VAD,
а непосредственно нужные нам флаги зарыты ещё глубже, в структурах _MMVAD_FLAGS
– пробираемся к ним аналогичным образом:
Код:
lkd> dt _MMVAD 86688b20 u.VadFlags.
;//--------------------------------
+0x000 u1 :
+0x014 u :
+0x000 VadFlags :
+0x000 CommitCharge : 0y010 (0x2)
+0x000 NoChange : 0y0
+0x000 VadType : 0y010 (0x2)
+0x000 MemCommit : 0y0
+0x000 Protection : 0y00111 (0x7)
+0x000 Spare : 0y00
+0x000 PrivateMemory : 0y0
А вот и атрибуты-защиты «Protection» моего корневого VAD (0y0 является двоичным представлением).
Не знаю, может и прописаны значения этих флагов где-нибудь в сишных хидерах, но мне было проще нагуглить.
«CommitCharge» указывает число зафиксированных страниц в регионе, т.е. уже привязанных к физ.адресам. Если CommitCharge=0, значит память только зарезервирована, но не связана с фреймами. VAD может описывать как память процесса, так и выделенную, например, девайсам память – вот перечисления типов VAD:
C-подобный:
;// VAD type flags
VadNone = 0 ;//
VadDevicePhysicalMem = 1 ;// Память принадлежит физ.устройству (см.DMA,MDL)
VadImageMap = 2 ;// <--- Наш клиент! (память образов программ)
VadAwe = 3 ;// Address Windowing Extensions (расширение адресного окна)
VadWriteWatch = 4 ;// Страницы с отслеживанием записи
VadLargePages = 5 ;// Большие пейджи 2Мb, 4Мb, или 1Gb (для серверов)
;// VAD protection flags
MM_ZERO_ACCESS = 0
MM_READONLY = 1
MM_EXECUTE = 2
MM_EXECUTE_READ = 3
MM_READ_WRITE = 4
MM_WRITE_COPY = 5
MM_EXECUTE_READ_WRITE = 6
MM_EXECUTE_WRITE_COPY = 7 ;//<---
Вышеизложенный подход просмотра VAD представляет практический интерес, чтобы обозначить расположение и состав структур при программировании драйверов (кстати VadRoot хранится в структуре EPROCESS). Он абсолютно не пригоден для визуального просмотра всего древа с высоты птичьего полёта. Для этого WinDbg имеет спец.расширение !vad
, которое выводит лог в более приглядном виде. Достаточно взять адрес корня «VadRoot» и вскормить его отладчику:
Код:
lkd> !vad 86688b20
;//---------------------
VAD level start end commit
891b7478 ( 3) 10 1f 0 Mapped READWRITE Pagefile-backed section
9411b640 ( 4) 20 2f 0 Mapped READWRITE Pagefile-backed section
89a43408 ( 2) 30 6f 3 Private READWRITE
891d17a8 ( 3) 70 73 0 Mapped READONLY Pagefile-backed section
891a0eb8 ( 1) 80 80 1 Private READWRITE
867ae7b8 ( 4) 90 f6 0 Mapped READONLY WindowsSystem32locale.nls
89aaa2f0 ( 3) 100 1ff 14 Private READWRITE
89afdbc8 ( 4) 200 203 4 Private EXECUTE_READ
8a17f5e0 ( 2) 230 23f 6 Private READWRITE
a8632ea8 ( 3) 3e0 3ef 5 Private READWRITE
86688b20 ( 0) 400 403 2 Mapped Exe EXECUTE_WRITECOPY TEMPASMCODEModuleInfo.EXE
89ac7608 ( 3) 759a0 759ea 3 Mapped Exe EXECUTE_WRITECOPY WindowsSystem32KernelBase.dll
89acc8b0 ( 2) 761f0 7629b 8 Mapped Exe EXECUTE_WRITECOPY WindowsSystem32msvcrt.dll
891821e8 ( 3) 774a0 77574 2 Mapped Exe EXECUTE_WRITECOPY WindowsSystem32kernel32.dll
89a9e398 ( 1) 77830 77971 10 Mapped Exe EXECUTE_WRITECOPY WindowsSystem32ntdll.dll
890f18b8 ( 4) 77a00 77a04 2 Mapped Exe EXECUTE_WRITECOPY WindowsSystem32psapi.dll
89b0b870 ( 3) 77a90 77a90 0 Mapped Exe EXECUTE_WRITECOPY WindowsSystem32apisetschema.dll
86785f88 ( 4) 7f6f0 7f7ef 0 Mapped READONLY Pagefile-backed section
941854c8 ( 2) 7ffb0 7ffd2 0 Mapped READONLY Pagefile-backed section
867c1600 ( 3) 7ffd3 7ffd3 1 Private READWRITE
9403d420 ( 4) 7ffdf 7ffdf 1 Private READWRITE
Total VADs: 21 Average level: 2 Maximum depth: 4
Здесь видно, что в древе моего процесса имеется всего 21-структура VAD, с макс.уровнем(4) и средним(2).
В первом столбце лежит адрес структуры, а во-втором её уровень Level в глобальном древе. Нуль – это корень VadRoot, а дальше отладчик отсортировал древо по возрастанию адресов StartVPN. Например на левом узле уровня(1) занял позицию VAD=891a0eb8
(диапазон памяти с меньшими адресами, в данном случае одна страница 0x80000), а на правом уровня(1) VAD=89a9e398
. Не забыл отладчик и про атрибуты, которые завершают картину. Если-же мы хотим просмотреть детали конкретного взятого VAD из всегоэтого древа, можно указать его адрес с аргументом(1) (см.справку WinDbg, команда «.hh !vad» в окне отладчика):
Код:
lkd> !vad 86688b20 1
;//---------------------
VAD @ 86688b20
Start VPN 400 End VPN 403 Control Area 891d0988
FirstProtoPte 93ee1840 LastPte fffffffc Commit Charge 2 (2.)
Secured.Flink 0 Blink 0 Banked/Extend 0
File Offset 0
ImageMap ViewShare EXECUTE_WRITECOPY
ControlArea @ 891d0988
Segment 93ee1810 Flink 00000000 Blink 867aa9c4
Section Ref 1 Pfn Ref 4 Mapped Views 1
User Ref 2 WaitForDel 0 Flush Count 0
File Object 890f3e68 ModWriteCount 0 System Views 0
WritableRefs 0
Flags (a0) Image File
File: TEMPASMCODEModuleInfo.EXE
Segment @ 93ee1810
ControlArea 891d0988 BasedAddress 00400000
Total Ptes 4
Segment Size 4000 Committed 0
Image Commit 1 Image Info 93ee1860
ProtoPtes 93ee1840 Flags (20000) ProtectionMask
Для программного доступа к структурам VAD нужен драйвер, поскольку корень древа лежит в вирт.пространстве ядра выше 0х80000000 (верхняя половина х32). Однако кое-что в режиме ReadOnly можно получить и из пользовательского уровня через VirtualQuery(). Вот её прототип:
C-подобный:
DWORD VirtualQuery
lpAddress dd 0 ;// In. линк на переменную с базой запрашиваемых страниц
lpBuffer dd 0 ;// Out. линк на структуру "MEMORY_BASIC_INFORMATION"
dwLength dd 0 ;// In. sizeof.MEMORY_BASIC_INFORMATION
;//---------------------------------------------------------------
struct MEMORY_BASIC_INFORMATION
BaseAddress dd 0 ;// База страницы внутри региона
AllocationBase dd 0 ;// База региона
AllocaProtect dd 0 ;// Флаг защиты при выделении региона (PAGE_xx)
RegionSize dd 0 ;// Размер региона, в котором все страницы имеют одинаковые атрибуты
State dd 0 ;// Состояние памяти (MEM_COMMITFREERESERVE)
Protect dd 0 ;// Защита доступа (см.AllocaProtect)
Type dd 0 ;// Тип памяти в регионе (MEM_MAPPEDPRIVATEIMAGE)
ends
Посмотрите на поля структуры – ничего не напоминает? Всё тот-же «StartVPN» из VAD (AllocationBase), размер региона и атрибуты защиты. Чтобы обойти всё древо, эту функцию нужно вызывать в цикле, на каждой итерации которого прибавлять к аргументу «lpAddress» значение поля «RegionSize». Цикл продолжаем до тех пор, пока не упрёмся в потолок вирт.памяти, который возвращает GetNativeSystemInfo().
…:: Примечание ::…
Во-всей линейке Win система всегда резервирует снизу и сверху по 64К для отлова неверных указателей,
поэтому на х32 мин.адресом процесса будет 0х00010000, а максимальным 0х7FFF0000. Буферы 64К оставлены на случай,
когда мы забываем передать аргумент какой-нибудь функции Win-API, и система подставляет вместо него Null-указатель.
В результате запрос попадает в зону сигнального буфера и получаем ошибку AccessViolation = 0xC0000005.
Благодаря VAD, выделение даже больших объёмов памяти не представляет проблему для диспетчера. Например та-же VirtualAlloc() просто добавляет ещё одну запись в древо VAD процесса, а реверсивная ей операция VirtualFree() тупо удаляет определённый узел из него.
3. WorkingSet – набор рабочих страниц процесса
Память – это разделяемый системный ресурс, а потому требует чёткого управления. Надзор ложится на плечи диспетчера вирт.памяти VMM. Он должен следить за тем, какие регионы свободны, выделять их процессам и освобождать, когда процесс завершает свою работу. Под определение WorkingSet попадают не регионы памяти, а набор отдельных страниц, которые в настоящий момент видны процессу во-фреймах памяти. Такие страницы называют ещё резидентными и доступны они приложению, не вызывая исключения PageFault (#PF, ошибка страницы). Когда система испытывает нехватку ОЗУ, кол-во пейджей в наборе влияет на процесс сброса их в файл-подкачки Pagefile.sys – эта процедура известна как обрезка набора, или «WorkingSet trim».
Диспетчер ведёт несколько списков-состояния страниц (см.рис.ниже), которые используют пользовательские процессы и сама ОС. Здесь диапазон «WorkingSetSize» представляет текущий размер набора, а Peak (пик) – макс.возможный для данной системы. В составе Kernel32.dll имеется функция K32GetProcessMemoryInfo() для вывода инфы о размере раб.набора указанного в аргументе процесса, а так-же K32QueryWorkingSet() для перечисления флагов всех страниц в наборе. В практической части приводится пример их вызова.
Если процесс пытается обратиться к странице, которой нет на данный момент в его наборе (и соответственно в VAD), блок MMU генерит аппаратное исключение #PF, и диспетчер подкачивает отсутствующую страницу с диска. Если-же процесс освобождает пейджи при помощи VirtualFree(), менеджер убирает их из списка WorkingSet, а если страница была изменена посредством записи, помещает её в «ModifiedPageList», и далее в отстойник «Standby». Страницы ExecuteRead, как правило, относятся к классу немодифицируемых, так-что после освобождения, они из набора прямиком отправляются в отстойник.
Менеджер ведёт ещё 2 списка: свободных страниц «FreePage», и пустых «ZeroPage». В список свободных помещаются пейджи, которые освободились после окончания процесса, а в лист пустых сбрасываются страницы, которые забил нулями специальный поток менеджера «ZeroPageThread» при помощи функции RtlZeroMemory(). В системном диспетчере-задач TaskMan (Ctrl+Alt+Del), на вкладке «Быстродействие» есть информация о рабочем наборе ОС:
• Доступно – это сумма объёмов: отстойника + пустых + свободных страниц (вход в набор, см.рис.выше);
• Кэшированно – сумма: отстойника + рабочих страниц.
Отладчик WinDbg имеет расширение !memusage
для вывода дампа рабочих страниц всей системы, и каждого приложения в отдельности. Правда для последнего случая нужно подцепить к дебагеру клиентскую ОС (например на вир.машине), а в локальном режиме(lkd) он выводит только общий лист, как показано в примере ниже:
Код:
lkd> !memusage
;//------------------
loading PFN database
loading (100% complete)..
Compiling memory usage data (99% Complete).
Zeroed: 0 ( 0 kb)
Free: 21654 ( 86616 kb)
Standby: 133179 ( 532716 kb)
Modified: 21004 ( 84016 kb)
ModifiedNoWrite: 6 ( 24 kb)
Active/Valid: 214243 ( 856972 kb)
Transition: 395 ( 1580 kb)
Bad: 237 ( 948 kb)
Unknown: 0 ( 0 kb)
TOTAL: 390481 (1561924 kb)
Building kernel map
Finished building kernel map
Unable to get control area: pfn 8ebc7d88 83c03b64 ;//<--- требует отлаживаемую ОС
4. MMU – блок управления памятью
Во-внутреннем блоке MMU процессора нас будет интересовать только «транслятор адреса» из виртуального в физический. Управляют этим блоком контрольные регистры CR0-CR4
процессора, и модельно-специфичный MSR.IA32_EFER
(Extended Feature Enables Register, вкл.расширенных возможностей). Вот их описание из мануалов Intel том(3,4):
Если на машине и установлен 64-битный процессор, после ребута он всё-равно будет находиться в режиме х32 до тех пор, пока BIOS или загрузчик Win не установит в единицу бит(8) в регистре IA32_EFER.LME
(LongModeEnable). Следующий бит(10) доступен только для чтения, и служит просто индикатором режима х64, вкл или выкл. Транслятор в MMU может работать в одном из 4-х режимах, которые определяют размеры страниц – это в дефолте 4К, а дальше 2М, 4М и 1Gb пейджи. Состояние битов[4:5] регистра CR4, и бита IA32_EFER.LME
напрямую влияет на переключение этих режимов. Вот как выглядит схема трансляции в х64 при активном бите(LME):
Как видим, из 64 бит используются только 48, что позволяет адресовать 2^48=256 ТБ вирт.памяти.
Транслятор сериализует 48-бит на 4 части, оставляя младшуюпятую под офсет на конкретный байт внутри фрейма. Кол-во разрядов в младшей части определяет охватываемое указателем пространство, что соответствует размерам вирт.страниц и физ.фреймов. В большинстве случаях это 12-бит, которыми можно полностью адресовать один 4-Кбайтный пейдж. Для остальных режимов транслятора, поле Offset расширяется в такой последовательности (зовите на помощь калькулятор в инж.виде):
• 12-бит (2^12) = 4.096 байт на страницу (дефолт 4КБ);
• 21-бит (2^21) = 2.097.152 = 2 Мбайт страница, при этом уровень L1 транслятора диспетчер игнорирует;
• 22-бит (2^22) = 4.194.304 = 4 Мбайт страница, L1 игнор и нужно взвести бит[4] в регистре CR4;
• 30-бит (2^30) = 1.073.741.824 = 1 ГБайт страница, игнор уровней L1+L2, и нужно взвести бит[8] в IA32_EFER.LME.
Обратите внимание, что чем больше размеры страниц, тем меньше для них требуется служебных структур – это в разы сокращает время поиска записей. С другой стороны, большие страницы напоминают сборище гопников, которые бесцеремонно «отжимают» у физ.памяти огромные фреймы, без особой необходимости. Как результат, диспетчеру приходится потеснять страницы соседних процессов, сбрасывая их в своп и возвращая обратно, при переключении потоков – это приводит к жутким тормозам. Таким образом, использование отличных от дефолта страниц оправдано только на серверных системах, где физ.памяти предостаточно и инфа крутится большими массивами.
Инженеры Intel явно перестарались с тех.доками на транслятор – один зоопарк имён чего только стоит: PML4PML4E, PDPPDPE, PDTPDTE, PTPTE. Такое разнообразие уже на взлёте сбивает нас с толку и механизм трансляции кажется чем-то сверх сложным. Но если учесть, что все записи в представленных блоках имеют одинаковый внутренний формат, то для лучшего понимания можно обозвать их просто уровнями «Level».
В практическом-же плане, реализация транслятора есть ничто-иное, как древо вложенных папок (директорий). Родительской для всех является L4, а её вирт.адрес хранится в регистре CR3. У каждого процесса имеется своя корневая папка: внутри неё лежат 512 записей, каждая из которых является указателем на одну из 512-ти вложенных папок уровня(L3) и т.д. То-есть получаем одну корневую папку PМL4, а дальше.. 512 папок PDP, 512 папок PDT, и 512 таблиц PT. Число 512 привязано здесь к разрядности полей адреса 2^9=512. Теперь, если взять произведение всех записей Entry и умножить его на размер страницы, получим макс.адрес виртуальной памяти 64-битного процесса:
(512*512*512*512)*4096 = 281.474.976.710.656 = 256 ТБ, или 2^48.
4.1. Записи PTE в таблице страниц
Наибольший интерес во-всей этой кухне представляют записи «PageTableEntry» в последнем уровне L1. Выше упоминалось, что с переходом на большие страницы транслятор игнорирует уровни L1,2, отдавая биты их адреса на растерзание офсету. В каждой записи имеется информационный бит(0) под кличкой «Present». Когда диспетчер читает записи, он в первую очередь проверяет этот бит, и если он сброшен в нуль, игнорирует уровень. Поскольку формат всех записей Entry одинаков, отсутствие уровня никак не влияет на механизм трансляции в целом – просто запись (например PDE) указывает сразу на фрейм PFN, минуя таблицу РageТable. Назначение и позиции флагов во-всех записях представлены ниже (здесь М = MaxPhysicalAddress):
Важно понять, что именно на уровне записей PTE виртуальный адрес превращается в номер-фрейма PFN физической памяти (см.биты 47:12). В пространстве ядра, записи PTE описывает одноимённая структура _MMPTE
, а значит можно просмотреть её в отладчике. Она имеет одно безымянное поле(u), которое необходимо раскрыть. Кстати команда отладчика dt
(DisplayType) имеет пару интересных ключей: -b
рекурсивно раскрывает все вложенные структуры, а –v
возвращает кол-во элементов и размеры структур:
Код:
lkd> dt -v _MMPTE u.
;//---------------------------------
struct _MMPTE, 1 elements, 0x8 bytes
+0x000 u : union <unnamed-tag>, 10 elements, 0x8 bytes
+0x000 Long : Uint8B
+0x000 VolatileLong : Uint8B
+0x000 HighLow : struct _MMPTE_HIGHLOW, 2 elements, 0x8 bytes
+0x000 Hard : struct _MMPTE_HARDWARE, 14 elements, 0x8 bytes
+0x000 Proto : struct _MMPTE_PROTOTYPE, 8 elements, 0x8 bytes
+0x000 Soft : struct _MMPTE_SOFTWARE, 10 elements, 0x8 bytes
+0x000 TimeStamp : struct _MMPTE_TIMESTAMP, 9 elements, 0x8 bytes
+0x000 Trans : struct _MMPTE_TRANSITION, 10 elements, 0x8 bytes
+0x000 Subsect : struct _MMPTE_SUBSECTION, 7 elements, 0x8 bytes
+0x000 List : struct _MMPTE_LIST, 9 elements, 0x8 bytes
Как видим, MMPTE является контейнером для 8-ми вложенных структур – для нас интересны только три из них:
• Hard – описывает типичный фрейм в физ.памяти,
• Proto – прототип PFN для расшаренных фреймов,
• Soft – фрейм выгружен в файл-подкачки на диск.
Чтобы определить тип записи PTE, диспетчер проверяет в каждой из этих структур бит в позиции нуль «Valid». Он может быть выставлен в единицу только в одной из структур, а в остальных будет сброшен – так диспетчер понимает, с фреймом какого типа имеет дело:
Код:
lkd> dt _MMPTE u.Hard.
;//-------------------
+0x000 u :
+0x000 Hard :
+0x000 Valid : Pos 0, 1 Bit ;//<--- бит принадлежности записи
+0x000 Dirty1 : Pos 1, 1 Bit
+0x000 Owner : Pos 2, 1 Bit
+0x000 WriteThrough : Pos 3, 1 Bit
+0x000 CacheDisable : Pos 4, 1 Bit
+0x000 Accessed : Pos 5, 1 Bit
+0x000 Dirty : Pos 6, 1 Bit
+0x000 LargePage : Pos 7, 1 Bit ;//<--- большая страница!
+0x000 Global : Pos 8, 1 Bit
+0x000 CopyOnWrite : Pos 9, 1 Bit
+0x000 Unused : Pos 10, 1 Bit
+0x000 Write : Pos 11, 1 Bit
+0x000 PFN : Pos 12, 26 Bits ;//<--- номер фрейма
+0x000 reserved1 : Pos 38, 26 Bits
В записи следующего типа, которая описывает сброшенный в файл-подкачки фрейм имеется два поля под указатель на PageFile.sys. Здесь нужно учитывать, что системы класса Win могут иметь макс.16 файлов подкачки (хотя на практике используется только один), поэтому 4-битное поле «PageFileLow» в структуре MMPTE.Soft
хранит номер файла, а 32-битное «PageFileHigh» – индекс выгруженного фрейма в нём:
Код:
lkd> dt _MMPTE u.Soft.
nt!_MMPTE
+0x000 u :
+0x000 Soft :
+0x000 Valid : Pos 0, 1 Bit
+0x000 Unused0 : Pos 1, 3 Bits
+0x000 SwizzleBit : Pos 4, 1 Bit
+0x000 Protection : Pos 5, 5 Bits
+0x000 Prototype : Pos 10, 1 Bit
+0x000 Transition : Pos 11, 1 Bit
+0x000 PageFileLow : Pos 12, 4 Bits ;//<--- номер файла
+0x000 InStore : Pos 16, 1 Bit
+0x000 Unused1 : Pos 17, 15 Bits
+0x000 PageFileHigh : Pos 32, 32 Bits ;//<--- индекс фрейма
Для просмотра записей-страниц отладчик имеет специальное расширение !pte
. В качестве аргумента, команда ожидает вирт.адрес страницы, а из выхлопной трубы выдаёт нам детали связанного с ней фрейма. В данном случае я передаю наименьший, доступный пользовательскому приложению адрес 0x00010000.
Код:
lkd> !pte 10000
VA 00010000
PDE at C0600000 PTE at C0000080
contains 0000000017D3E867 contains 8000000024492947
pfn 17d3e ---DA--UWEV pfn 24492 -G-D---UW-V
;//---------------------------------------------------------
;//------------- Флаги защиты [ ---DA—UWEV ] ---------------
;//---------------------------------------------------------
0x200 C - Copy on Write.
0x100 G - Global.
0x080 L - Large page (большой фрейм, флаг только в PDE)
0x040 D - Dirty (изменённый)
0x020 A - Accessed (был доступ)
0x010 N - Cache disabled (некэшируемый)
0x008 T - Write-through (сквозная запись на диск)
0x004 U K Owner (владелец, userkernel)
0x002 W R Writeable или Read-only
0x001 V - Valid (вилидная запись)
E - Execute page. Для платформ без аппаратного бита NX, всегда отображается буква E.
Обратите внимание на лог команды !pte
. Можно сделать вывод, что транслятор моего «Dual-Core E5200» работает в 2-уровневом режиме: PageDirectory(L2) и PageTable(L1). Такой модели придерживаются 32-битные системы без расширения РАЕ (см.рис.ниже), а если РАЕ включён битом[5] в регистре CR4, то в модель подключается ещё и уровень PageDirectoryPointer(L3). Вот как выглядит х32 транслятор 4-Кбайтных страниц без РАЕ:
Здесь видно, что из-за ограниченного кол-ва бит, ни о каких 1GB-страницах не может быть и речи. Зато если расширить Offset до 22-бит, можно охватить 4МБ фрейм. При этом макс.адресом в системе будет по-прежнему 4GB. Но тогда как удаётся в режиме РАЕ адресовать память 2^36=64GB? Здесь инженеры нашли оригинальное решение – они просто расширили сами записи РТЕ в таблице PageTable до 64-бит, хотя в режиме без РАЕ эти записи имеют размер 32-бит.
В арсенале WinDbg есть расширение !vtop
(VirtualToPhysical). Если вскормить ему вирт.адрес, получим значения записей(Entry) всех уровней транслятора, а так-же связанный с виртуальным, физический адрес. Проведём небольшой эксперимент по такому алго..
1. Запрашиваем дамп активных процессов системы !process 0 0
, и возьмём из них два произвольных. В поле «DirBase» будет лежать указатель на корневой каталог транслятора – в моём случае это 0x5f5af400
для процесса ModuleInfo.exe, и 0x5f5afd00
для FoxitReader.exe:
Код:
lkd> !process 0 0
;//-----------------------------
**** NT ACTIVE PROCESS DUMP ****
PROCESS 891b8030 SessionId: 1 Cid: 0e08 Peb: 7ffdb000 ParentCid: 0a70
---> DirBase: 5f5af400 ObjectTable: b4392c28 HandleCount: 7.
Image: ModuleInfo.EXE
PROCESS 8909aa20 SessionId: 1 Cid: 0eb4 Peb: 7ffdc000 ParentCid: 0a70
---> DirBase: 5f5afd00 ObjectTable: b3dd6790 HandleCount: 177.
Image: FoxitReader.exe
2. Теперь передаём команде !vtop
полученные на этапе(1) значения «DirBase», и во-втором аргументе любой вирт.адрес, например 0х00401100
:
Код:
lkd> !vtop 5f5af400 401100 ;//<--- запрос записей-каталога моего процесса ModuleInfo.exe
;//------------------------
X86VtoP: Virt 00401100, pagedir 5f5af400
X86VtoP: PAE PDPE 5f5af400 - 0000000023f4b801
X86VtoP: PAE PDE 23f4b010 - 0000000023c80867
X86VtoP: PAE PTE 23c80008 - 800000003202e947
X86VtoP: PAE Mapped phys 3202e100
Virtual address 00401100 translates to physical address 3202e100.
lkd> !vtop 5f5afd00 401100 ;//<--- записи FoxitReader.exe
;//------------------------
X86VtoP: Virt 00401100, pagedir 5f5afd00
X86VtoP: PAE PDPE 5f5afd00 - 0000000039801801
X86VtoP: PAE PDE 39801010 - 0000000050a99867
X86VtoP: PAE PTE 50a99008 - 0000200039565886
X86VtoP: PAE Mapped phys 39565100
Virtual address 00401100 translates to physical address 39565100.
Значит система для тестов у меня Win7-x32 с включённым РАЕ, а потому в логе пестрят напоминания об этом. Модель транслятора 3-х уровневая. Записи Entry на всех уровнях имеют размер 64-бит, что позволяет адресовать в режиме РАЕ пространство свыше 4Gb (в режиме без РАЕ записи размером 4-байт).
Вирт.адресу 0х00401100
моего процесса соответствует физ.адрес 0x3202e100
, а к такому-же адресу процесса Foxit привязан уже другой физ.адрес 0x39565100
. По этой причине, процесс(А) не может прочитать данные процесса(В). Физ.адрес получаем из записи РТЕ последнего уровня(L1), и если разделить его на 1000h
(размер 4К фрейма), получим PFN или порядковый номер страничного фрейма «Physical Frame Number».
Обратите внимание на значение РТЕ моего лога = 0х800000003202e947
.
Младшие 12-бит 947h
являются здесь атрибутами фрейма (см.формат записи РТЕ в табл.выше), поэтому диспетчер запоминает и сбрасывает их в нуль, получая таким образом базу 4К-фрейма в физ.памяти. Теперь из вирт.адреса берётся 12-битный офсет (в данном случае 100h), и складывается с базой. После такой арифметики, получаем физ.адрес 0x3202e100
.
Расширение отладчика !dc
показывает дамп памяти, ожидая на входе физ.адрес (обычный dc требует вирт.адрес). Так сложились звёзды, что 0х00401100
указывает в моей прожке на секцию-данных, где имеется массив текстовых строк – вот он собственной персоной (см.код в практической части ниже). Если-бы я передал в !vtop
адрес 0х00400000
, получил-бы дамп РЕ-заголовка, с сигнатурой «MZ»:
Код:
lkd> !dc 3202e100 ;//<--- вирт.адрес 0х00401100
;//----------------
#3202e100 43455845 5f455455 44414552 47415000 EXECUTE_READ.PAG
#3202e110 58455f45 54554345 45525f45 575f4441 E_EXECUTE_READ_W
#3202e120 45544952 47415000 58455f45 54554345 RITE.PAGE_EXECUT
#3202e130 52575f45 5f455449 59504f43 47415000 E_WRITE_COPY.PAG
#3202e140 55475f45 00445241 45474150 434f4e5f E_GUARD.PAGE_NOC
#3202e150 45484341 47415000 52575f45 43455449 ACHE.PAGE_WRITEC
#3202e160 49424d4f 4d00454e 435f4d45 494d4d4f OMBINE.MEM_COMMI
#3202e170 454d0054 45525f4d 56524553 4d004445 T.MEM_RESERVED.M
4.2. База данных PFN
Теперь проведём инвентаризацию базы PFN.
Мы не согрешим против истины заявив, что «база страничных фреймов» является ключевой фигурой во-всём механизме трансляции! Если подвести черту под вышесказанным, то процент участия MMU в этом деле стремится к нулю – аппаратный транслятор определяет лишь план действий диспетчеру вирт.памяти, который подстраиваясь под MMU должен создать соответствующее число каталогов и заполнить таблицу-трансляции, записями РТЕ. То-есть без привлечения средств диспетчера, транслятор в MMU ничего из себя не представляет.
При включении машины, диспетчер запрашивает у BIOS объём реально установленной физ.памяти ОЗУ, и разделив это значение на 4096-байт, получает общее кол-во фреймов в системе. Теперь, для каждого из них диспетчер создаёт индивидуальную запись – в ядре она числится как структура _MMPFN
. Её размер зависит от режима работы процессора: на системах х32 без РАЕ это 24-байта, для х32.РАЕ = 28-байт, а на х64 все 48-байт. Таким образом, чем больше физ.ОЗУ, тем больше имеем структур, которые собираются в глобальную базу PFN. Указатель на базу лежит в переменной ядра nt!MmPfnDatabase, прочитать её можно командой отладчика ?poi
(pointer value):
Код:
lkd> ?poi nt!MmPfnDatabase
;//-------------------------
Evaluate expression: -2084569088 = 0x83c00000 <--- Адрес базы PFN (Win7.x32)
Из предыдущего лога мы выяснили, что физ.адрес страниц извлекается из записей РТЕ (PageTableEntry). Но от куда он туда попадает? (ведь как мин.нужны тычинка и пестик). А копируется физ.адрес в РТЕ, как-раз из базы PFN. Более того, поскольку размер структуры PFN=28-байт, а размер PTE=8, то запись РТЕ является вложенной в MMPFN, т.е. полностью хранится в поле «OriginalPte» последней. Важно понять, что таблицы РТЕ транслятора заполняются после создания диспетчером базы PFN:
Код:
lkd> dt -v _MMPFN OriginalPte.u.
;//------------------------------
struct _MMPFN, 10 elements, 0x1c bytes ;//<--- Размер структуры 1Ch=28
+0x010 OriginalPte : struct _MMPTE, 1 elements, 0x8 bytes ;//<---- Оригинальная запись РТЕ в структуре MMPFN
+0x000 u : union <unnamed-tag>, 10 elements, 0x8 bytes
+0x000 Long : Uint8B
+0x000 VolatileLong : Uint8B
+0x000 HighLow : struct _MMPTE_HIGHLOW, 2 elements, 0x8 bytes
+0x000 Hard : struct _MMPTE_HARDWARE, 14 elements, 0x8 bytes
+0x000 Proto : struct _MMPTE_PROTOTYPE, 8 elements, 0x8 bytes
+0x000 Soft : struct _MMPTE_SOFTWARE, 10 elements, 0x8 bytes
+0x000 TimeStamp : struct _MMPTE_TIMESTAMP, 9 elements, 0x8 bytes
+0x000 Trans : struct _MMPTE_TRANSITION, 10 elements, 0x8 bytes
+0x000 Subsect : struct _MMPTE_SUBSECTION, 7 elements, 0x8 bytes
+0x000 List : struct _MMPTE_LIST, 9 elements, 0x8 bytes
База имеет свой формат, в котором применяется алгоритм «связывания в цепочку» LIST_ENTRY. Для его реализации, в структурах MMPFN используются два поля: это указатель Flink (Forward, вперёд на сл.структуру) и Blink (Backward, назад на предыдущую). Когда в базе имеется много структур, но с содержимым разного типа, такая схема позволяет с лёгкостью находить их в глобальном массиве (не нужно чекать каждую). Для базы PFN это как-раз то, что доктор прописал, т.к. фреймы могут находиться в разном состоянии в зависимости от того, в каком из листов набора WorkingSet лежит вирт.страница. Флаг её расположения указывается в 3-битном поле «PegeLocation» структуры PFN, с уже знакомыми нам вариантами: FreeList, Zero, Standby, Modified и прочие. На рис.ниже представлена схема такой связи (здесь я указал только 4 состояния фреймов, хотя всего их :
Расширение отладчика !pfn
с аргументом в виде номера фрейма, возвращает информацию о соответствующей структуре MMPFN. Дебагер интересуют значения только основных полей, среди которых будет Flink и вирт.адрес записи РТЕ. В данном случае я передают номер PFN=0x22e4
, и получаю адрес привязанной к данному фрейму структуры 0х83С3D0F0
(напомню, что база у меня начинается с адреса 0х83С00000):
Код:
lkd> !pfn 22e4
;//-----------------------------------
PFN 000022E4 at address 83C3D0F0
Flink 89A999E8 Share count 00000008 PteAddress C0603018
Reference count 0001 Сolor 0 Priority 0
Restore pte 200000000080 Containing page 0022E4 Active M
Cached Modified
В следующем логе MMPFN я убрал всё лишнее, и оставил только интересующие нас поля.
Обратите внимание на расшифровку «CacheAttribute» и «PageLocation» – как видим они совпадают с выхлопом расширения !pfn
:
Код:
lkd> dt -b -v _MMPFN 83C3D0F0
;//-----------------------------------
struct _MMPFN, 10 elements, 0x1c bytes
+0x000 u1.Flink : 0x89a999e8
+0x008 PteAddress : 0xc0603018
+0x00c u3.e1 : struct _MMPFNENTRY, 11 elements, 0x2 bytes
+0x000 PageLocation : Bitfield 0y110
+0x000 WriteInProgress : Bitfield 0y0
+0x000 Modified : Bitfield 0y1
+0x000 ReadInProgress : Bitfield 0y0
+0x000 CacheAttribute : Bitfield 0y01
+0x001 Priority : Bitfield 0y000
+0x001 Rom : Bitfield 0y0
+0x001 InPageError : Bitfield 0y0
+0x001 KernelStack : Bitfield 0y0
+0x001 RemovalRequested : Bitfield 0y0
+0x001 ParityError : Bitfield 0y0
+0x010 OriginalPte : struct _MMPTE, 1 elements, 0x8 bytes
+0x018 u4.PteFrame : Bitfield 0y0000000000010001011100100 (0x22e4)
;//---------------------------------------------------------------------------------------------------
;// PageLocation 3-бита:
;//------------------------
Zeroed = 000
Free = 001
Standby = 010
Modified = 011
ModNoWrt = 100
Bad = 101
Active = 110 <----
Trans = 111
;//------------------------
;// CacheAttribute 2-бита:
;//------------------------
NonCached = 00
Cached = 01 <---
WriteComb = 10
NotMapped = 11
Реальные эксперименты в отладчике позволяют толковать спецификацию с практической точки зрения, ведь только пощупав объект руками можно сделать о нём выводы. На данный момент мы знаем, что размер одной структуры MMPFN может быть равен 24, 28 или 48-байт. Кол-во структур напрямую зависит от размера установленной в системе физ.памяти ОЗУ. Выходит, что простой арифметикой можно вычислить размер всей базы PFN на текущей машине, что демонстрирует код ниже:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//----------
.data
sysInfo SYSTEM_INFO
;// макрос переводит из байт в MKbyte
macro FpuDiv [pAddr, pSize]
{ fild qword[esp] ;// ST0 = аргументы из стека
fidiv [pSize] ;// разделить на аргумент М или Кбайт
fst [pAddr] ;// сохранить в переменной
add esp,8 } ;// очистить аргументы
align 16
kByte dd 1024
mByte dd 1024*1024
fpuRes1 dq 0
fpuRes2 dq 0
isWow dd 0
pageSize dd 0
pfnSize dd 48 ;// x64=48, x32PAE=28, x32=24
x86_64 db 'x86.64',0
x86_32pae db 'x86.32 PAE',0
x86_32 db 'x86.32 Non PAE',0
buff db 0
;//----------
.code
start: invoke SetConsoleTitle,<'*** Memory PFN Info ***',0>
;//---- Получить размер страницыфрейма
invoke GetNativeSystemInfo,sysInfo
mov eax,[sysInfo.dwPageSize]
shr eax,10
mov [pageSize],eax ;// в КБ
;//---- Проверить систему на 64-бит (WOW64)
invoke GetCurrentProcess
invoke IsWow64Process,eax,isWow
mov esi,x86_64
cmp [isWow],1
jz @next
;//---- Проверить на режим РАЕ (только х32)
invoke IsProcessorFeaturePresent,PF_PAE_ENABLED ;// константа =9
or eax,eax
jz @f
mov [pfnSize],28 ;// размер структуры _MMPFN при РАЕ
mov esi,x86_32pae
jmp @next
@@: mov [pfnSize],24
mov esi,x86_32
@next: cinvoke printf,<10,' System CPU.......: %s',0>,esi
;//---- Запросить реальный размер установленной ОЗУ
invoke GetPhysicallyInstalledSystemMemory,buff
push dword[buff+4] dword[buff]
FpuDiv fpuRes1, kByte
finit
cinvoke printf,<10,' DDR-SDRAM size...: %.0f MB',
10,' Phy frame size...: %d Byte',0>,
dword[fpuRes1],dword[fpuRes1+4],[sysInfo.dwPageSize]
;//---- Имеем размер памяти ОЗУ и размер страницы.
;//---- Вычисляем общее кол-во фреймов PFN
push dword[buff+4] dword[buff]
FpuDiv fpuRes1, pageSize
fistp [fpuRes2]
cinvoke printf,<10,' Total PFNs.......: %.0f = 0x%I64x',
10,' MMPFN struct size: %d Byte',0>,
dword[fpuRes1],dword[fpuRes1+4],
dword[fpuRes2],dword[fpuRes2+4],[pfnSize]
;//---- Всего PFN * размер одной структуры = Размер базы PFN
fld qword[fpuRes1]
fimul [pfnSize]
fidiv [mByte]
fstp [fpuRes2]
cinvoke printf,<10,' PFN Database size: %.1f MB',0>,
dword[fpuRes2],dword[fpuRes2+4]
cinvoke _getch
cinvoke exit,0
;//---------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
include 'apimsvcrt.inc'
include 'apikernel32.inc'
include 'apiuser32.inc'
5. MDL – Memory Descriptor List
Структура MDL используется ядром исключительно при операциях прямого обращения к памяти DMA (Direct Memory Access, чтение/запись без участия процессора). Как-правило, каналы DMA используют только физ.устройства типа: накопители ATA/ATAPI (харды и DVD-ROM), девайсы USB, Audio/Video, LAN и прочие, т.е. все высокоскоростные. Для их поддержки, ещё во-времена динозавров в чипсет был включён спец.процессор с ограниченными возможностями DMAC, который называют ещё «Slave DMA Controller».
Ведомым Slave его обозвали потому, что в наше время DMA-контролёры уже встраиваются непосредственно в сами устройства так, что они могут захватывать шину-памяти по своей инициативе, не привлекая к этому делу вечно перегруженный DMAC – этот механизм известен как «BusMastering». Однако нужно учитывать, что шина у памяти одна, а потому в любой момент доступ к ней будет иметь кто-то один: или CPU (благодаря своему кэшу он редко обращается к памяти), или устройство DMA, ..но не оба сразу.
Чтобы организовать запрос на операцию DMA, пользовательское приложение должно сначала получить дескриптор нужного устройства, а потом передать драйверу этого устройства, адрес промежуточного буфера. Доступные на чтение/запись девайсы относятся к файловым объектам системы, так-что дескрипторы получаем через CreateFile(), а взаимодействуем с их драйверами через DeviceIoControl():
C-подобный:
BOOL DeviceIoControl()
In.HANDLE hDevice ;// дескриптор устройства
In.DWORD dwIoControlCode ;// IOCTL = код операции
In.LPVOID lpInBuffer ;//<-- буфер для передачи данных драйверу
In.DWORD nInBufferSize ;// ....размер буфера
Out.LPVOID lpOutBuffer ;//<-- буфер для приёма данных от драйвера
In.DWORD nOutBufferSize ;// ....размер буфера
Out.LPDWORD lpBytesReturned ;// сюда получим результат приёма/передачи в байтах
In.LPOVERLAPPED lpOverlapped ;// линк на структуру "Overlapped" при асинх.операциях
Секрет использования ядром записи MDL кроется внутри 32-битного кода IOCTL, в котором 2-мл.бита указывают на «метод передачи буфера». Когда мы вызываем эту функцию, диспетчер ввода-вывода собирает все наши аргументы в пакет IRP (I/O Request Packet), и в таком виде передаёт его драйверу. Приняв пакет, дров проверяет 2-мл.бита кода операции IOCTL, и создаёт структуру MDL только в том случае, если методом передачи являются «IN/OUT_DIRECT». На рис.ниже показано, как кодируется запрос:
Обратите внимание, что 2-битная маска кода может быть только у четырёх hex-чисел мл.тетрады. Теперь, если посмотреть на лист всех IOCTL (см.скрепку в подвале), то по маске 01b
и 10b
можно отфильтровать из них только запросы DMA. Например коду 0х000b0191
соответствует «IOCTL_HID_SET_FEATURE», и т.к. у него в последней тетраде единица, значит команда передаёт буфер методом IN_DIRECT. Обнаружив сей факт, диспетчер-памяти создаст для данного запроса структуру MDL, куда поместит информацию о буфере ввода-вывода.
Код:
;//---- METHOD_IN_DIRECT: 1,5,9,D -------
;//--------------------------------------
000b0191 = IOCTL_HID_SET_FEATURE
000b0195 = IOCTL_HID_SET_OUTPUT_REPORT
001b0011 = IOCTL_SCSI_EXECUTE_IN
001b0501 = IOCTL_SCSI_MINIPORT_IDENTIFY
001b0505 = IOCTL_SCSI_MINIPORT_DISABLE_SMART
001b0509 = IOCTL_SCSI_MINIPORT_EXECUTE_OFFLINE_DIAGS
001b0521 = IOCTL_SCSI_MINIPORT_NOT_CLUSTER_CAPABLE
00210021 = IOCTL_TDI_SEND_DATAGRAM
00210029 = IOCTL_TDI_SET_INFORMATION
0022021d = IOCTL_1394_CLASS
0f608015 = IOCTL_IR_TRANSMIT
;//---- METHOD_OUT_DIRECT: 2,6,A,E ------
;//--------------------------------------
0002403e = IOCTL_CDROM_RAW_READ
000b0192 = IOCTL_HID_GET_FEATURE
000b019a = IOCTL_GET_PHYSICAL_DESCRIPTOR
000b019e = IOCTL_HID_GET_HARDWARE_ID
000b01a2 = IOCTL_HID_GET_INPUT_REPORT
000b01ba = IOCTL_HID_GET_MANUFACTURER_STRING
000b01be = IOCTL_HID_GET_PRODUCT_STRING
000b01c2 = IOCTL_HID_GET_SERIALNUMBER_STRING
000b01e2 = IOCTL_HID_GET_INDEXED_STRING
000b01e6 = IOCTL_HID_GET_MS_GENRE_DESCRIPTOR
00170002 = IOCTL_NDIS_QUERY_GLOBAL_STATS
00170006 = IOCTL_NDIS_QUERY_ALL_STATS
0017000e = IOCTL_NDIS_QUERY_SELECTED_STATS
0017001e = IOCTL_NDIS_GET_LOG_DATA
00190012 = IOCTL_SCSISCAN_CMD
00190016 = IOCTL_SCSISCAN_LOCKDEVICE
0019001a = IOCTL_SCSISCAN_UNLOCKDEVICE
00190022 = IOCTL_SCSISCAN_GET_INFO
001b0012 = IOCTL_SCSI_EXECUTE_OUT
001b0502 = IOCTL_SCSI_MINIPORT_READ_SMART_ATTRIBS
001b0506 = IOCTL_SCSI_MINIPORT_RETURN_STATUS
001b050a = IOCTL_SCSI_MINIPORT_ENABLE_DISABLE_AUTO_OFFLINE
00210012 = IOCTL_TDI_QUERY_INFORMATION
00210016 = IOCTL_TDI_RECEIVE
0021001a = IOCTL_TDI_RECEIVE_DATAGRAM
00210036 = IOCTL_TDI_ACTION
0056402e = IOCTL_VOLUME_READ_PLEX
00564052 = IOCTL_VOLUME_QUERY_ALLOCATION_HINT
0f60401a = IOCTL_IR_RECEIVE
0f604022 = IOCTL_IR_PRIORITY_RECEIVE
0009411e = FSCTL_READ_FROM_PLEX
001440f2 = FSCTL_SRV_COPYCHUNK
001480f2 = FSCTL_SRV_COPYCHUNK_WRITE
Для каждого из методов передачи, в структуре пакета IRP отведено своё поле, куда в соответствии с маской диспетчер сохраняет адрес промежуточного буфера. Во всех случаях, его размер указывается в поле Parameters.DeviceIoControl
ещё одной структуры драйвера _IO_STACK_LOCATION
. Можно запросить эти структуры у отладчика и ознакомиться с указанными полями (здесь я отсеил лишнее):
Код:
lkd> dt -v -b _IRP
;//--------------------------------
struct _IRP, 21 elements, 0x70 bytes
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 MdlAddress : Ptr32 to ;//<--- адрес буфера для "METHOD_IN/OUT_DIRECT"
+0x008 Flags : Uint4B
+0x00c AssociatedIrp : union, 3 elements, 0x4 bytes
+0x000 SystemBuffer : Ptr32 to ;//<--- METHOD_BUFFERED
+0x03c UserBuffer : Ptr32 to ;//<--- METHOD_NEITHER
• METHOD_BUFFERED
Для этого типа передачи, пакеты IRP предоставляют указатель на буфер в Irp–>AssociatedIrp.SystemBuffer
. Буфф является общим для входа и выхода – дров принимает данные из этого буфера, а затем передаёт в него-же.
• METHOD_IN_DIRECT + OUT_DIRECT
Здесь линк на буфер лежит так-же в Irp–>AssociatedIrp.SystemBuffer
, но прицепом имеется ещё и указатель на структуру MDL, в поле Irp–>MdlAddress
. Буферы для приёма и передачи раздельны. Ниже мы положим их под скальпель и рассмотрим в деталях.
• METHOD_NEITHER
Диспетчер ввода-вывода не предоставляет в ядре никаких системных буферов! В Irp–>UserBuffer
лежит вирт.адрес выходного буфера пользователя (напомню, что разговор идёт от лица драйвера), а в поле Parameters.DeviceIoControl.Type3InputBuffer
структуры IO_STACK_LOCATION – адрес входного, которые были указаны при вызове DeviceIoControl().
Теперь посмотрим на структуру MDL, она имеет размер 28-байт (на системах х32) и всего 8 элементов:
Код:
lkd> dt -v _MDL
;//--------------------------------
struct _MDL, 8 elements, 0x1c bytes
+0x000 Next : Ptr32 to struct _MDL, 8 elements, 0x1c bytes
+0x004 Size : Int2B
+0x006 MdlFlags : Int2B
+0x008 Process : Ptr32 to struct _EPROCESS, 144 elements, 0x2e0 bytes
+0x00c MappedSystemVa : Ptr32 to Void
+0x010 StartVa : Ptr32 to Void
+0x014 ByteCount : Uint4B
+0x018 ByteOffset : Uint4B
1. Next – это линк на сл.структуру MDL, если они связываются в цепочку (используется редко);
2. Size – размер этой структуры (чтобы различать х32 от х64);
3. MdlFlags – флаги листа (см.ниже);
4. Process – указатель на процесс, которому принадлежит данная структура MDL;
5. MappedSystemVa – линк на буфер, если он отображается в виртуальное (не физ) пространство ядра;
6. StartVa – база вирт.страницы, которая выделена пользователем под буфер;
7. ByteCount – размер отображаемого в MDL буфера;
8. ByteOffset – смещение буфера от начала (базы) вирт.страницы StartVA.
Структуру MDL нужно рассматривать как заголовок, сразу после которого следует массив PFN физ.памяти. Если пользовательский буфер меньше 4К-страницы, после заголовка будет всего один указатель на PFN, для отображения этого буфера в свободный фрейм памяти. Поскольку вирт.память всегда линейна, а выделенные для неё фреймы могут идти в разнобой, то когда буфер юзера больше одной страницы, диспетчеру-памяти приходится выделять несколько разбросанных по физ.памяти фреймов, и связывать их в цепочку. В этом случае после заголовка MDL будут лежать уже несколько линков на выделенные PFN. Важно понять, что всякий лист MDL всегда описывает только один буфер ввода-вывода. Вот как это выглядит графически:
Здесь, в вирт.памяти представлено всего три страницы, а буфер DMA находится внутри второй. Его смещение от начала страницы указывается в поле «ByteOffset», а размер в «ByteCount». Пунктирные линии будут действительны только при размере буфа больше 4К-страницы.
Ещё однимважным моментом является то, как VMM выделяет физ.фреймы для буфера. Ключевым событием в этом алго является вызов функции MmProbeAndLockPages(), которая намертво блокирует выделенные фреймы так, что они становятся не выгружаемыми в файл-подкачки (NonPagedPool). При этом в MDL взводится флаг «PAGES_LOCKED». Фреймы остаются закреплёнными вплоть до окончания операции прямого обращения к памяти DMA, после чего драйвер должен освободить их посредством MmUnlockPages() + ExFreePool().
6. Практика – сбор информации
На финишной прямой соберём основные моменты статьи в приложение, которое будет использовать сл.функции Win32-API:
• GetNativeSystemInfo() – возвращает в структуру «SYSTEM_INFO» размер страницы и пр.инфу;
• GetPerformanceInfo() – структура «PERFORMANCE_INFORMATION», где можно найти счётчики использования памяти;
• K32GetModuleInformation() – в структуру «MODULEINFO» сбрасывает инфу о РЕ-заголовке (база/размер образа, и точка-входа ЕР);
• GetPhysicallyInstalledSystemMemory() – появилась начиная с Win7 и возвращает QWORD с реальным размером DDR-SDRAM в КБ;
• GlobalMemoryStatusEx() – в структуре «MEMORYSTATUS_EX» можно будет найти инфу о вирт.памяти процесса;
• GetProcessWorkingSetSize() – в переменных инфа о макс/мин рабочего набора процесса WorkingSet;
• K32GetProcessMemoryInfo() – в структуре «PROCESS_MEMORY_COUNTERS_EX» лежат различные квоты памяти;
• VirtualQuery() – в цикле позволит создать всю карту-памяти процесса MemoryMap (обходит древо VAD).
Последняя функция из этого списка возвращает двоичные «флаги состояния» регионов памяти и атрибутов их защиты. Чтобы вывести их в более дружелюбном нам текстовом виде, я создал таблицу соответствий. Поскольку приложение х32, а в коде имеются 64-бит поля, то удобно использовать макросы с операциями FPU. Вот пример:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//----------
.data
memTable dd 001h,pNA,002h,pRO,004h,pRW,008h,pWC,010h,pEx,020h,pER
dd 040h,pERW,080h,pEWC,104h,pG,200h,pNC,400h,pWCN
dd 0,mRes,1000h,mC,2000h,mRes,4000h,mDec,8000h,mRel,10000h,mFree
dd 20000h,mPriv,30000h,mCr,40000h,mMap,80000h,mRst,100000h,mTd,1000000h,mIm
tblSize = ($-memTable)/8
;// Page access/protect flags
pNA db 'NO_ACCESS',0
pRO db 'READONLY',0
pRW db 'READWRITE',0
pWC db 'WRITECOPY',0
pEx db 'EXECUTE',0
pER db 'EXECUTE_READ',0
pERW db 'EXECUTE_READWRITE',0
pEWC db 'EXECUTE_WRITECOPY',0
pG db 'PAGE_GUARD',0
pNC db 'PAGE_NOCACHE',0
pWCN db 'WRITECOMBINE',0
;// Memory allocation flags
mC db 'COMMIT',0
mRes db 'RESERVED',0
mDec db 'DECOMMIT',0
mRel db 'RELEASE',0
mFree db 'FREE',0
mPriv db 'PRIVATE',0
mMap db 'MAPPED',0
mRst db 'RESET',0
mTd db 'TOP_DOWN',0
mIm db 'IMAGE',0
mCr db 'COMMIT + RESERVE',0
Unk db 'Combine',0
;//-------------------------
align 16
perfInfo PERFORMANCE_INFORMATION
sysInfo SYSTEM_INFO
mStat MEMORYSTATUS_EX
mInfo MODULEINFO
mBasic MEMORY_BASIC_INFORMATION
mCount PROCESS_MEMORY_COUNTERS_EX
;// Макрос переводит из байт в MKbyte
macro FpuDiv [pAddr, pSize]
{ fild qword[esp]
fidiv [pSize]
fstp [pAddr]
add esp,8 }
;// Переводит из страниц в Kbyte
macro FpuMul [pAddr1, pSize1]
{ fild qword[esp]
fimul [pSize1]
fstp [pAddr1]
add esp,8 }
;// Переводит из Kb в Mb
macro FpuK2M [pAddr2]
{ fld [pAddr2]
fidiv [kByte]
fstp [pAddr2] }
;// Возвращает в ESI указатель на строку для VirtualAlloc()
macro GetAttr [Attr,pTable,pSize]
{ local @found
mov esi,pTable
mov ecx,pSize
@@: lodsd
cmp eax,Attr
je @found
add esi,4
loop @b
mov esi,Unk
jmp @f
@found: mov esi,[esi]
@@: }
workMin dd 0
workMax dd 0
kByte dd 1024
mByte dd 1024*1024
pageSize dd 4096/1024
align 16
fpuRes1 dq 0
fpuRes2 dq 0
fpuRes3 dq 0
fpuRes4 dq 0
fpuRes5 dq 0
fpuRes6 dq 0
fpuRes7 dq 0
fpuRes8 dq 0
pAddress dd 10000h
hProcess dd 0
hModule dd 0
buff db 0
;//----------
.code
start: invoke SetConsoleTitle,<'*** Process Memory Information ***',0>
;//---- Получить дескрипторы и заполнить структуры
invoke GetModuleHandle,0
mov [hModule],eax
invoke OpenProcess,PROCESS_QUERY_INFORMATION,0,
invoke GetCurrentProcessId
mov [hProcess],eax
invoke GetNativeSystemInfo,sysInfo
invoke GetPerformanceInfo,perfInfo,sizeof.PERFORMANCE_INFORMATION
;//---- Собираем инфу..
invoke GetModuleFileName,0,buff,256
cinvoke printf,<10,' Module **************',
10,' Name.............: %s',0>,buff
invoke K32GetModuleInformation,-1,[hModule],mInfo,sizeof.MODULEINFO
cinvoke printf,<10,' Base address.....: 0x%08x',
10,' EntryPoint.......: 0x%08x',
10,' Image size.......: %u byte',0>,
[mInfo.lpBaseOfDll],[mInfo.EntryPoint],[mInfo.SizeOfImage]
;//--------------------------------
invoke GetPhysicallyInstalledSystemMemory,buff
push dword[buff+4] dword[buff]
FpuDiv fpuRes1, kByte
cinvoke printf,<10,' Physical Memory *****',
10,' DDR-SDRAM size...: %7.1f Mb',0>,
dword[fpuRes1],dword[fpuRes1+4]
;//--------------------------------
invoke GlobalMemoryStatusEx,mStat
push dword[mStat.dqTotalPhys+4] dword[mStat.dqTotalPhys]
FpuDiv fpuRes1, mByte
push dword[mStat.dqAvailPhys+4] dword[mStat.dqAvailPhys]
FpuDiv fpuRes2, mByte
push dword[mStat.dqTotalPageFile+4] dword[mStat.dqTotalPageFile]
FpuDiv fpuRes3, mByte
push dword[mStat.dqAvailPageFile+4] dword[mStat.dqAvailPageFile]
FpuDiv fpuRes4, mByte
push dword[mStat.dqTotalVirtual+4] dword[mStat.dqTotalVirtual]
FpuDiv fpuRes5, mByte
push dword[mStat.dqAvailVirtual+4] dword[mStat.dqAvailVirtual]
FpuDiv fpuRes6, mByte
cinvoke printf,<10,' Alocated system..: %7.1f Mb',
10,' Free.............: %7.1f Mb',
10,' Loaded...........: %5d.0 %%',
10,' Virtual Memory ******',
10,' Total PageFile...: %7.1f Mb',
10,' Free PageFile...: %7.1f Mb',
10,' Total Virtual....: %7.1f Mb',
10,' Free Virtual....: %7.1f Mb',
10,' Page Size........: %5d byte',0>,
dword[fpuRes1],dword[fpuRes1+4],dword[fpuRes2],dword[fpuRes2+4],
[mStat.dwMemoryLoad],
dword[fpuRes3],dword[fpuRes3+4],dword[fpuRes4],dword[fpuRes4+4],
dword[fpuRes5],dword[fpuRes5+4],dword[fpuRes6],dword[fpuRes6+4],
[sysInfo.dwPageSize]
;//--------------------------------
push 0 [perfInfo.KernelTotal]
FpuMul fpuRes1, pageSize
FpuK2M fpuRes1
push 0 [perfInfo.KernelPaged]
FpuMul fpuRes2, pageSize
FpuK2M fpuRes2
push 0 [perfInfo.KernelNonpaged]
FpuMul fpuRes3, pageSize
FpuK2M fpuRes3
push 0 [perfInfo.SystemCache]
FpuMul fpuRes4, pageSize
FpuK2M fpuRes4
cinvoke printf,<10,' Kernel Memory *******',
10,' Total used.......: %7.1f Mb = %-7d frames',
10,' Paged memory.....: %7.1f Mb = %-7d frames',
10,' Non paged memory.: %7.1f Mb = %-7d frames',
10,' System cache.....: %7.1f Mb = %-7d pages',0>,
dword[fpuRes1],dword[fpuRes1+4],[perfInfo.KernelTotal],
dword[fpuRes2],dword[fpuRes2+4],[perfInfo.KernelPaged],
dword[fpuRes3],dword[fpuRes3+4],[perfInfo.KernelNonpaged],
dword[fpuRes4],dword[fpuRes4+4],[perfInfo.SystemCache]
;//--------------------------------
invoke GetProcessWorkingSetSize,[hProcess],workMin,workMax
shr [workMin],10
shr [workMax],10
mov eax,[workMin]
mov ebx,[workMax]
shr eax,2
shr ebx,2
cinvoke printf,<10,' Process Virtual *****',
10,' Working Set min..: %5u.0 Kb = %-3d pages',
10,' Working Set max..: %5u.0 Kb = %-3d pages',0>,
[workMin],eax,[workMax],ebx
;//--------------------------------
invoke K32GetProcessMemoryInfo,-1,mCount,sizeof.PROCESS_MEMORY_COUNTERS_EX
push 0 [mCount.PeakWorkingSetSize]
FpuDiv fpuRes1, kByte
push 0 [mCount.WorkingSetSize]
FpuDiv fpuRes2, kByte
push 0 [mCount.QuotaPeakPagedPoolUsage]
FpuDiv fpuRes3, kByte
push 0 [mCount.QuotaPagedPoolUsage]
FpuDiv fpuRes4, kByte
push 0 [mCount.QuotaPeakNonPagedPoolUsage]
FpuDiv fpuRes5, kByte
push 0 [mCount.QuotaNonPagedPoolUsage]
FpuDiv fpuRes6, kByte
push 0 [mCount.PagefileUsage]
FpuDiv fpuRes7, kByte
mov eax,[mCount.PeakWorkingSetSize]
mov ebx,[mCount.WorkingSetSize]
shr eax,12
shr ebx,12
cinvoke printf,<10,' Peak Working Set.: %7.1f Kb = %-3d pages',
10,' Working Set.: %7.1f Kb = %-3d pages',
10,' Peak Paged Pool..: %7.2f Kb',
10,' Paged Pool..: %7.2f Kb',
10,' Peak Non Paged...: %7.2f Kb',
10,' Non Paged...: %7.2f Kb',
10,' Peak Pagefile....: %7.2f Kb',10,
10,' PageFault exception..: %4u',10,0>,
dword[fpuRes1],dword[fpuRes1+4],eax,
dword[fpuRes2],dword[fpuRes2+4],ebx,
dword[fpuRes3],dword[fpuRes3+4],dword[fpuRes4],dword[fpuRes4+4],
dword[fpuRes5],dword[fpuRes5+4],dword[fpuRes6],dword[fpuRes6+4],
dword[fpuRes7],dword[fpuRes7+4],[mCount.PageFaultCount]
;//---- Карта памяти процесса --------------
cinvoke printf,<10,' Process Memory Map (VAD tree) *******',
10,' *************************************',0>
@map: invoke VirtualQuery,[pAddress],mBasic,sizeof.MEMORY_BASIC_INFORMATION
or eax,eax
je @err
mov ebx,[mBasic.Protect]
or ebx,ebx
jnz @f
mov ebx,[mBasic.AllocaProtect]
@@: GetAttr ebx,memTable,tblSize
push esi
mov ebx,[mBasic.State]
GetAttr ebx,memTable,tblSize
push esi
pop eax ebx
mov edx,[pAddress]
mov ecx,[mBasic.RegionSize]
add edx,ecx
shr ecx,10
mov esi,ecx
shr esi,2
cinvoke printf,<10,' %08x - %08x | %-9s %-15s |%8d Kb |%7d pages |',0>,
[pAddress],edx,eax,ebx,ecx,esi
@err: mov eax,[mBasic.RegionSize]
add [pAddress],eax ;//<---- Переход к сл.региону!
cmp [pAddress],0x7fff0000 ;//<---- Проверить потолок юзера
jb @map
cinvoke _getch
cinvoke exit,0
;//---------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',
user32,'user32.dll',psapi,'psapi.dll'
include 'apimsvcrt.inc'
include 'apikernel32.inc'
include 'apiuser32.inc'
include 'apipsapi.inc'
В скрепке лежит инклуд Kernel32.inc с описанием всех используемых здесь структур. Как оказалось, в штатной поставке FASM’а имеется только оставшийся нам в наследство от Win-XP старый набор, поэтому я обновил его и советую заменить им устаревший инклуд по адресу: fasmincludeequates.
7. Постскриптум.
Такой вот получился «бутафорский ман» с мозговым штурмом..
В статье планировал лишь коротко рассмотреть основные моменты, но в работе диспетчера-памяти всё переплетено в клубок так, что если потянешь за одну нить, то автоматом всплывает другая, без объяснения которой в первой теряется смысл. Здесь вспоминается критика к фильму «Выживщий» с Лео в главной роли: -«Выживщим является тот, кто досмотрел фильм до конца».
В скрепку ложу два исполняемым файла для тестов, инклуд Kernel32.inc, а так-же лист из 1000+ поддерживаемых Win7 кодов IOCTL/FSCTL. Всем удачи, пока!
В прошлый раз мы рассмотрели вопросы использования глобальных переменных. Прежде, чем двинуться дальше, нужно дать небольшой вводный курс по памяти в Windows.
Любая вещь в вашей программе занимает «память компьютера». Это может быть строка, число, открытый файл, запись, объект, форма и даже сам код. Даже хотя вы явно никого не просили выделять память, она всё равно выделена автоматически — либо компилятором, либо операционной системой.
Адресное пространство и все, все, все…
Кратко говоря, память программы может рассматриваться как один очень-очень длинный ряд байтов. Байт — это единица измерения количества информации, в мире Delphi и Windows он равен восьми битам и может хранить одно из 256
различных значений (от 0
до 255
). На память можно смотреть как на массив байт. Что именно содержат эти байты — зависит от того, как интерпретировать их содержимое, т.е. от того, как их используют. Значение 97 может означать число 97
, или же ANSI букву 'a'
. Если вы рассматриваете вместе несколько байт, то вы можете хранить и большие значения. Например, в 2-х байтах вы можете хранить одно из 256*256 = 65536
различных значений, две ANSI буквы 'ab'
или Unicode букву 'a'
— и т.д.
Чтобы обратиться к конкретному байту в памяти (адресовать его), можно присвоить каждому байту номер, пронумеровав их целыми положительными числами, включив ноль за начало отсчёта. Индекс байта в этом огромном массиве и называется его адресом, а весь массив целиком — памятью программы. Диапазон адресов от 0
до максимума называется адресным пространством программы. А максимум (длина) массива называется размером адресного пространства.
(примечание: ну, на самом деле, есть тысяча и один способ адресовать память, но в рамках современного мира и этой статьи мы ограничимся только этим способом).
Адресное пространство (вернее, его размер) определяет способность программы работать с данными. Чем оно больше — тем с большим количеством данных программа сможет работать (в один момент времени). Если у программы заканчивается свободное место в адресном пространстве (т.е. все адреса в нём выделены под какие-то объекты в программе) — то у программы заканчивается память (out of memory).
Как адресное пространство соотносится с вашим исходным кодом
С точки зрения языка высокого уровня (Паскаль) все вещи в вашей программе характеризуются именем (идентификатором), типом («сколько памяти выделять») и семантикой («что с этим можно делать»). Например, целое число занимает 4
байта и их можно читать, писать, складывать и т.п. И число A — это не то же самое, что число B. Строки же занимают переменный объём памяти, их, к примеру, можно соединять и редактировать. И так далее.
Но на уровне машинного языка, железа и операционной системы все они характеризуются только местоположением, размером (в байтах) и атрибутами доступа. Местоположение — это адрес объекта. К примеру, число A может иметь адрес 1234
, а число B — 1238
. И поэтому это два разных числа — потому что у них разный адрес, т.е. они лежат в разных местах. Атрибут доступа является упрощённой «семантикой», которая определяет то, что можно делать с памятью. А таких вещей всего три: читать, писать и выполнять. Последнее означает исполнение машинного кода. Тут нужно пояснить, что ваши данные (числа, строки, формы и т.п.) находятся в одном «контейнере» (том самом «массиве памяти из байт») вместе с кодом программы — .exe файлом. Иными словами, код рассматривается наравне с данными, а чтобы их отличать и служат атрибуты доступа.
Можно увидеть, как понятия языка высокого уровня («имя», «тип» и «семантика») проецируются в понятия низкого уровня («адрес», «размер» и «атрибуты доступа»).
Древний мир
В давние времена память программы была тождественно равна оперативной памяти машины (т.н. ОЗУ или RAM — Random Access Memory). Иными словами, размер адресного пространства программы был равен размеру установленной оперативной памяти. Вот, установлено на вашей машине две планки памяти по 64
Кб — значит, у вашей программы есть 128
Кб памяти. Ну, за вычетом той памяти, что уже занята, конечно же. Адрес объекта программы был равен адресу физической ячейке оперативной памяти (физическому адресу). И если у вас заканчивалось место в ОЗУ, то у вас заканчивалась память в программе.
Конечно, такой способ хотя и весьма прост, имеет две проблемы:
- Память программы ограничена оперативной памятью. А раньше эта память была дорогой и её было очень мало.
- Если нужно запустить две программы, то они будут работать «в одной песочнице»: и первая и вторая программа будут размещать свои данные в одном месте — оперативной памяти. И если первая программа по ошибке запишет что-то в данные второй, то… ой.
Виртуальная память и виртуальное адресное пространство
Поэтому в современном мире используется совершенно другая схема: во-первых, память программы теперь больше не тождественна оперативной памяти. Теперь программа работает исключительно с так называемой «виртуальной памятью». Виртуальная память — это имитация реальной памяти. Она позволяет каждой программе:
- считать, что установлено максимальное теоретически возможное количество оперативной памяти;
- считать, что она является единственной программой, запущенной на машине.
Иными словами, адресное пространство программы более не ограничено размером физической памяти (так называют оперативную память компьютера, чтобы специально указать на её отличие от виртуальной памяти) — адресное пространство имеет теперь максимально возможный размер. К примеру, если для адресации используются 32-битные указатели (4 байта), то размер адресного пространства равен 2^32 = 4'294'967'296
байт. Т.е. 4 миллиарда (если угодно: биллионов) или 4 Гб. А размерность адресного пространства — равна 32
.
В связи с новомодным «переходом на 64
бита» нужно упомянуть, что этот переход заключается в замене 4
-байтных (32
-битных) указателей на 8-байтные (64-битные) — что увеличивает размер адресного пространства программы аж до 2^64 = 18'446'744'073'709'551'616
байт. Т.е. 18
с лишним квинтиллионов байт или 16
Эб (эксабайт) для краткости. Соответственно, 32
-битный указатель может быть любым числом от 0
до 4'294'967'296
(от $00000000
до $FFFFFFFF
). 64
-разрядный указатель может варьироваться от $00000000'00000000
до $FFFFFFFF'FFFFFFFF
.
А из второго пункта следует, что 4
Гб или 16
Эб есть у каждой программы. Т.е. каждой программе отводится своё личное закрытое адресное пространство. Такая изолированность означает, что программа А в своем адресном пространстве может хранить какую-то запись данных по адресу $12345678
, и одновременно у программы В по тому же адресу $12345678
(но уже в его адресном пространстве) может находиться совершенно иная запись данных. Если программа A попробует прочитать данные по адресу $12345678
, то она получит доступ к своей записи (записи программы A), а не данным программы B. Но если к адресу $12345678
обратится программа B, то она получит свою запись, а не запись программы А. Иными словами, программа A не может обратиться в памяти (адресному пространству) программы B и наоборот.
Таким образом, при использовании виртуальной памяти упрощается программирование, так как программисту больше не нужно учитывать ограниченность памяти, или согласовывать использование памяти с другими приложениями. Для программы выглядит доступным и непрерывным всё допустимое адресное пространство, вне зависимости от наличия в компьютере соответствующего объема ОЗУ. Если программы выделяют в их адресных пространствах больше памяти, чем есть в системе физической памяти, то часть памяти из ОЗУ переносится на диск («винчестер») — в т.н. файл подкачки (его ещё называют страничным файлом, page file, SWAP-файлом или «свопом»). Когда программа обращается к своим данным, которые были выгружены на диск, то операционная система автоматически загрузит данные из файла подкачки в ОЗУ. И всё это происходит под капотом — т.е. совершенно незаметно для программы. С точки зрения программы, ей кажется, что она работает с 4
Гб или 16
Эб RAM.
Применение механизма виртуальной памяти позволяет:
- упростить адресацию памяти программами;
- рационально управлять оперативной памятью компьютера (хранить в ней только активно используемые области памяти);
- изолировать программы друг от друга (программа полагает, что монопольно владеет всей памятью).
А теперь, пока вы не перевозбудились от колоссального объема адресного пространства, предоставляемого вашей программе: вспомните, что оно — виртуальное, а не физическое. Другими словами, (виртуальное) адресное пространство — всего лишь диапазон адресов памяти. Конечно, нехватка памяти теперь не происходит, когда заканчивается свободное место в оперативной памяти. И на машине с 256
Мб ОЗУ, любая программа может выделить, скажем, один кусок в 512
Мб памяти. Конечно же, это не означает, что вы можете выделить аж 16
эксабайт — ведь реальный размер ограничен размером диска. И не факт, что в системе будет диск на 16
эксабайт. Тем не менее, это значительно лучше, чем просто 256
Мб оперативной памяти, установленные на вашем «старичке».
(примечание: по непонятной мне причине, некоторые люди не верят в тот простой факт, что программа может спокойно выделить больше памяти, чем установлено физической памяти в системе; звучит как сюжет для разрушителей легенд (MythBusters)).
Чем чаще системе приходится копировать данные из оперативной памяти в файл подкачки и наоборот, тем больше нагрузка на жесткий диск и тем медленнее работает операционная система (при этом может получиться так, что операционная система будет тратить всё свое время на подкачку памяти, вместо выполнения программ). Поэтому, добавив компьютеру оперативной памяти, вы снизите частоту обращения к жёсткому диску и, тем самым, увеличите общую производительность системы. Кстати, во многих случаях увеличение оперативной памяти дает больший выигрыш в производительности, чем замена старого процессора на новый. А с падением цен на память уже не проблема собрать систему с 16
или 32
Гб оперативной памяти по доступной цене.
Факты о виртуальном адресном пространстве
Хотя в самом начале мы рассматривали память программы (адресное пространство) как один непрерывный однородный блок, сейчас настало время сделать уточнение, что я вам наврал: таковым он не является. Адресное пространство, хотя действительно однородно и непрерывно более чем на 99%
, но в нём есть несколько специальных областей. Я не буду подробно разбирать их все, скажу только о самых важных.
Во-первых, это область для отлова нулевых указателей. Это, определённо, самая важная специальная часть адресного пространства. Начинается она в нуле и заканчивается на адресе 65'535
. Т.е. имеет размер в 64
Кб и расположена в диапазоне $00000000-$0000FFFF
— самом начале адресного пространства. Специальна эта область тем, что она всегда заблокирована: в ней нельзя выделить память, а любое обращение по этим адресам всегда возбуждает исключение access violation (примечание: это не единственная причина возбуждения access violation). Эта область сделана исключительно для нашего удобства. Как вы узнаете потом (или уже знаете), нулевой указатель nil
по числовому значению равен 0
. И если вы случайно (по ошибке) обратитесь к нулевому указателю — то эта область поможет вам возбудить исключение и поймать вашу ошибку.
А что такого особенного в числе 65'535
? Ну, 64
Кб — это гранулярность выделения памяти. Гранулярность выделения памяти определяет, блоками каких размеров вы можете оперировать при выделении и освобождении памяти. Т.е. гранулярность выделения памяти в 64
Кб означает, что вы можете выделять только блоки памяти, размер которых кратен 64
Кб. Зачем так делается? Ну, если вы попробуете вести учёт «выделенности» каждого байта в программе, то размер управляющих структур у вас превысит размер самих данных. Поэтому память выделяют «кластерами». Иными словами, если вы хотите расположить область в начале адресного пространства, то вы не можете выделить меньше, чем 64
Кб. А больше? Больше — можно. Например, 64 + 64 = 128
Кб. Но большого смысла в этом нет.
Почему гранулярность выделения памяти равна именно 64
Кб, а не, скажем, 8
Кб? Ну, на это есть исторические причины.
(примечание: полностью аналогичный блок расположен на границе 2
Гб — но уже по совершенно другим причинам).
Далее, что вам ещё нужно знать про виртуальное адресное пространство — оно доступно вам не полностью. Грубо говоря, в виртуальном адресном пространстве каждой программы сосуществуют сама программа и операционная система. Та часть, где работает ваша программа (и о котором мы говорили всё это время выше), называется разделом для кода и данных пользовательского режима (user mode). Та часть, где работает операционная система, называется разделом для кода и данных режима ядра (kernel mode). Обе эти части находятся в едином адресном пространстве программы.
Чем они отличаются? Про пользовательский раздел мы уже много чего сказали: он свой у каждой программы и это полностью ваш раздел — делайте что хотите. Раздел ядра является здесь особенным в двух моментах: во-первых, у вашей программы нет к нему никакого доступа. Вообще и в принципе это невозможно. Там орудует только операционная система, но не вы. Если вы попробуете обратиться к памяти в этом разделе, то получите просто access violation. Во-вторых, особенность раздела в том, что он разделяется между всеми программами. Да, вот так: пользовательская часть у каждого адресного пространства своя, но часть ядра — одна и та же, общая. По сути, раздел ядра является «адресным пространством режима ядра».
Какой размер имеют эти две части адресного пространства? Ну, если мы говорим про 32
-разрядную программу, то пользовательский раздел занимает от 2
до 4
Гб (по умолчанию — 2
Гб). Соответственно, режим ядра занимает от 0
до 2
Гб (ибо суммарно должно быть 4
Гб). Конечно же, это за вычетом уже упоминаемых специальных областей. Итого: по умолчанию адресное пространство 32
-разрядной программы делится пополам. Половина — вам, и половина — операционной системе.
(примечание: 0 Гб под режим ядра — это специальный особый случай, достижимый только при запуске 32-битной программы на 64-битной машине. В обычных условиях граница между разделами может двигаться от 2 до 3 Гб).
Если говорить совсем точно, то раздел для ваших данных в случае 32
-х бит имеет диапазон $0000FFFF-$7FFEFFFF
(или $BFFFFFFF
в максимуме на 3 Гб, с дыркой на 64
Кб в районе 2
Гб), а раздел режима ядра — $80000000-$FFFFFFFF
(или $C0000000-$FFFFFFFF
в максимуме для user mode). В случае 64
-разрядной программы ситуация будет несколько иная. На сегодняшний день в Windows соотношение выглядит так: user mode — $00000000'00010000-$000003FF'FFFEFFFF
(8
Тб); kernel mode — $00000400'00000000-$FFFFFFFF'FFFFFFFF
. Ну, это всё ещё недостаточно точно, ведь, на самом деле, режим ядра в случае 64
-х бит использует только максимум несколько сотен Гб, оставляя большую часть адресного пространства попросту неиспользуемой. Т.е. у нас в дополнение к двум областям (user mode и kernel mode) появляется ещё и третья: зарезервированная область. Которую, впрочем, со стороны user mode удобно считать частью kernel mode. Сделано это по той простой причине, что 64
-битное адресное пространство настолько огромно, что user mode и kernel mode выглядели бы в нём тонюсенькими полосочками, вздумай бы вы изобразить их графически и в масштабе. А если место просто зарезервировано, то и не нужно делать для него управляющих данных. Даже 8
Тб памяти для user mode — это очень много. Если бы вы выделяли мегабайт памяти в секунду, у вас бы ушло три месяца, чтобы исчерпать такое адресное пространство.
Это что касается изолированности одной программы от других и от операционной системы. Внутри программы её модули (exe, DLL, bpl) друг от друга, вообще говоря, никак не изолированы. Однако на практике граница всё же появляется, но связана она с языковыми различиями и особенностью управления памятью в разных языках программирования. Но это разговор для другого раза.
Если вы забудете всё то, что я тут говорил, то вот факт, который вы должны вынести из этого обсуждения: размер памяти программы ограничен 2 Гб (32-битная программа) или 8 Тб (64-битная программа), либо суммарным размером оперативной памяти и файлом подкачки — смотря что меньше, а что больше. Т.е. на практике вы получаете «out of memory» только когда превышаете размер в 2 Гб.
Операции, производимые с виртуальной памятью
Ну, вполне очевидно, что прежде чем использовать память, вы должны её выделить (commit), а после окончания работы — освободить (release). Конечно, вам не обязательно делать это в явном виде — как я уже сказал, часто за вас это делает кто-то другой автоматически. Но об этом в следующий раз.
Помимо двух операций (выделения и освобождения памяти) существует и третья операция — резервирование (reserve) памяти. Смысл её заключается в том, что под зарезервированную виртуальную память не выделяется никакой реальной памяти (будь то оперативная память или файл подкачки), но при этом память считается занятой, как если бы она была выделена. Позднее, вы можете выделить реальную память этому зарезервированному блоку (полностью или частями).
Зачем нужна такая операция? Ну, предположим, вам нужен непрерывный блок памяти для работы (к примеру, чтобы обработать его за один проход одним циклом), но вы выделяете память не сразу а частями — по мере надобности. Вы не можете просто выделить первый блок, а потом — второй: ведь тогда нет гарантии, что они будут идти друг за другом. Вот поэтому и придумали операцию резервирования: вы резервируете достаточно большой регион. Это — «бесплатно». Потом вы выделяете в нём реальную память. Обе цели достигнуты: вы и выделяете память по мере необходимости (а не сразу целиком), и вы получаете свою непрерывную область памяти.
Кстати, все три операции выполняются функциями VirtualAlloc
и VirtualFree
. Не забудьте только, что мы говорили про гранулярность выделения памяти в 64
Кб.
И снова: какое это имеет отношение к Delphi?
Ну, почти самое прямое. Ведь программа на Delphi должна выделять и освобождать память. Это значит, что ей нужно вызывать функции VirtualAlloc
и VirtualFree
. А выделять память она будет в своём (виртуальном) адресном пространстве — причём, только в пользовательской его части.
Операции с памятью в Delphi проводятся через функции GetMem
и FreeMem
. Конечно же, кроме этих функций в Delphi существует и много других — но они являются лишь обёртками или переходниками к GetMem
и FreeMem
. Эти обёртки (например: AllocMem
, New
, Dispose
, SetLength
и т.п.) предоставляют дополнительную функциональность и удобство (кстати, в системе тоже есть обёртки к вызовам VirtualAlloc
и VirtualFree
). В некоторых случаях, эти вызовы и вовсе скрыты и происходят автоматически под капотом языка. Например, при сложении двух строк:
var S1, S2, S3: String; begin S1 := S2 + S3;
вы не видите вызов GetMem
, но он здесь есть.
Зачем нужны «свои» подпрограммы управления памятью? Почему нельзя просто использовать VirtualAlloc
и VirtualFree
? Ну, Delphi тут не уникальна — большинство языков используют т.н. менеджеры памяти — это код, который в Delphi стоит за вызовами GetMem
и FreeMem
, который служит переходником к VirtualAlloc
и VirtualFree
. А делается это по причине всё той же гранулярности выделения в 64
Кб. Т.е. если вы создаёте 100
объектов по, скажем, 12
байт, то вместо двух килобайт (12
б * 100 = 1.2
Кб + служебные данные менеджера памяти) вы занимаете уже почти 6.5
Мб (64 * 100 = 6'400
Кб) — на несколько порядков больше! Использовали бы вы VirtualAlloc — вы бы очень быстро исчерпали свободную память. Менеджер памяти же «упаковывает» несколько запросов на выделение памяти в один блок.
(примечание: «упаковка» ни в коем случае не означает «сжатие» или «кодирование» — это просто размещение нескольких маленьких кусочков памяти в одном 64 Кб блоке).
Заметьте, что операции резервирования памяти у Delphi нет, т.к. подобная операции не имеет большого смысла при «упаковке» запросов менеджером памяти. Для работы с резервированием используются функции операционной системы.
Продолжение следует…
Вот и все базовые сведения про устройство памяти в Windows, которые вам нужно знать для начала. В следующий раз мы более близко посмотрим на то, как архитектура памяти соотносится с переменными в ваших программах.
См. также: Архитектура памяти в Windows: мифы и легенды (spin-off).
Читать далее: Адресное пространство под микроскопом.