Marylin
18.11.2021, 00:27
В системной архитектуре отображение вирт.памяти на физическую носит таинственный характер. Предлагая материал на эту тему, MSDN забывает о логической нити – по большому счёту у них тонны безсвязных отрывков. В результате сеть заполонили статьи о диспетчерезации памяти, однако инфа в них сильно разнится, поскольку каждый из авторов трактует оригинальные доки по своему. Приняв это во-внимание, в данной статье был выбрал формат "беседа с новичком", чтобы рассмотреть такие вопросы как: задачи MMU и VMM, область применения списков VAD и MDL, назначение базы PFN, состав рабочих страниц WorkingSet, способы трансляции адресов, и многое другое. Из оружий ближнего боя понадобится отладчик WinDbg, и том(3) мануалов Intel"System Programming Guide", как внушающий доверие источник информации.
Оглавление:
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 и имеют префиксы Mm\Mi_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 использует для вкл/откл этого рульного механизма. Всех, кого интересует данная фишка, копайте докуIntel том(4). В системе имеется альтернатива аппаратным 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 и подкидывает нам инфу жалкими крапалями. Однако общую картину зрительно уже можно будет сформировать:
https://forum.antichat.xyz/attachments/4913888/img_507289e698.png
Значит у каждого процесса своё древо 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 представлено ниже:
https://forum.antichat.xyz/attachments/4913888/img_b86751009e.png
Обратите внимание на поле 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 :
+0x004 LeftChild : 0x891a0eb8 _MMVAD
+0x008 RightChild : 0x89a9e398 _MMVAD
+0x00c StartingVpn : 0x400
+0x010 EndingVpn : 0x403
+0x014 u :
+0x018 PushLock : _EX_PUSH_LOCK
+0x01c u5 :
+0x020 u2 :
+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 представлен уже знакомый нам диапазон памяти Start\EndVPN, а так-же указатели на Left\Right узлы дочернего уровня. Что касается атрибутов защиты данного региона, они спрятаны во-вложенных безымянных структурах
u..u5
(union). Чтобы раскрыть их, достаточно указать имя поля в структуре, и поставить в конце точку:
Код:
lkd> dt _MMVAD 86688b20 u.
;//------------------------
nt!_MMVAD
+0x000 u1 :
+0x000 Balance : 0y00
+0x000 Parent : 0x891025c0 _MMVAD ;// 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:
Вышеизложенный подход просмотра 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 \Windows\System32\locale.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 \TEMP\ASM\CODE\ModuleInfo.EXE
89ac7608 ( 3) 759a0 759ea 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\KernelBase.dll
89acc8b0 ( 2) 761f0 7629b 8 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\msvcrt.dll
891821e8 ( 3) 774a0 77574 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\kernel32.dll
89a9e398 ( 1) 77830 77971 10 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
890f18b8 ( 4) 77a00 77a04 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\psapi.dll
89b0b870 ( 3) 77a90 77a90 0 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\apisetschema.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" в окне отладчика):
Для программного доступа к структурам 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_COMMIT\FREE\RESERVE)
Protect dd
0
;
// Защита доступа (см.AllocaProtect)
Type dd
0
;
// Тип памяти в регионе (MEM_MAPPED\PRIVATE\IMAGE)
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() для перечисления флагов всех страниц в наборе. В практической части приводится пример их вызова.
https://forum.antichat.xyz/attachments/4913888/img_c646c4fcba.png
Если процесс пытается обратиться к странице, которой нет на данный момент в его наборе (и соответственно в 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 ;// dt -v _MMPTE u.
;//---------------------------------
struct _MMPTE, 1 elements, 0x8 bytes
+0x000 u : union , 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 ;// 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 ;// !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 (владелец, user\kernel)
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-Кбайтных страниц без РАЕ:
https://forum.antichat.xyz/attachments/4913888/img_cd2751e80a.png
Здесь видно, что из-за ограниченного кол-ва бит, ни о каких 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:
2. Теперь передаём команде
!vtop
полученные на этапе(1) значения "DirBase", и во-втором аргументе любой вирт.адрес, например
0х00401100
:
Значит система для тестов у меня 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":
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 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
:
Реальные эксперименты в отладчике позволяют толковать спецификацию с практической точки зрения, ведь только пощупав объект руками можно сделать о нём выводы. На данный момент мы знаем, что размер одной структуры MMPFN может быть равен 24, 28 или 48-байт. Кол-во структур напрямую зависит от размера установленной в системе физ.памяти ОЗУ. Выходит, что простой арифметикой можно вычислить размер всей базы PFN на текущей машине, что демонстрирует код ниже:
C-подобный:
format pe console
include
'win32ax.inc'
entry start
;
//----------
.
data
sysInfo SYSTEM_INFO
;
// макрос переводит из байт в M\Kbyte
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
,
;
//---- Получить размер страницы\фрейма
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
,
,
esi
;
//---- Запросить реальный размер установленной ОЗУ
invoke GetPhysicallyInstalledSystemMemory
,
buff
push dword
[
buff
+
4
]
dword
[
buff
]
FpuDiv fpuRes1
,
kByte
finit
cinvoke printf
,
,
\
dword
[
fpuRes1
]
,
dword
[
fpuRes1
+
4
]
,
[
sysInfo
.
dwPageSize
]
;
//---- Имеем размер памяти ОЗУ и размер страницы.
;
//---- Вычисляем общее кол-во фреймов PFN
push dword
[
buff
+
4
]
dword
[
buff
]
FpuDiv fpuRes1
,
pageSize
fistp
[
fpuRes2
]
cinvoke printf
,
,
\
dword
[
fpuRes1
]
,
dword
[
fpuRes1
+
4
]
,
\
dword
[
fpuRes2
]
,
dword
[
fpuRes2
+
4
]
,
[
pfnSize
]
;
//---- Всего PFN * размер одной структуры = Размер базы PFN
fld qword
[
fpuRes1
]
fimul
[
pfnSize
]
fidiv
[
mByte
]
fstp
[
fpuRes2
]
cinvoke printf
,
,
\
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
'api\msvcrt.inc'
include
'api\kernel32.inc'
include
'api\user32.inc'
https://forum.antichat.xyz/attachments/4913888/img_f98adfd8f9.png
5.MDL – Memory DescriptorList
Структура 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
;
// dt -v -b _IRP
;//--------------------------------
struct _IRP, 21 elements, 0x70 bytes
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 MdlAddress : Ptr32 to ;//AssociatedIrp.SystemBuffer
. Буфф является общим для входа и выхода – дров принимает данные из этого буфера, а затем передаёт в него-же.[/FONT]
• 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 всегда описывает только один буфер ввода-вывода. Вот как это выглядит графически:
https://forum.antichat.xyz/attachments/4913888/img_504869d0b6.png
Здесь, в вирт.памяти представлено всего три страницы, а буфер 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
001
h
,
pNA
,
002
h
,
pRO
,
004
h
,
pRW
,
008
h
,
pWC
,
010
h
,
pEx
,
020
h
,
pER
dd
040
h
,
pERW
,
080
h
,
pEWC
,
104
h
,
pG
,
200
h
,
pNC
,
400
h
,
pWCN
dd
0
,
mRes
,
1000
h
,
mC
,
2000
h
,
mRes
,
4000
h
,
mDec
,
8000
h
,
mRel
,
10000
h
,
mFree
dd
20000
h
,
mPriv
,
30000
h
,
mCr
,
40000
h
,
mMap
,
80000
h
,
mRst
,
100000
h
,
mTd
,
1000000
h
,
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
;
// Макрос переводит из байт в M\Kbyte
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
10000
h
hProcess dd
0
hModule dd
0
buff db
0
;
//----------
.
code
start
:
invoke SetConsoleTitle
,
;
//---- Получить дескрипторы и заполнить структуры
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
,
,
buff
invoke K32GetModuleInformation
,
-
1
,
[
hModule
]
,
mInfo
,
sizeof
.
MODULEINFO
cinvoke printf
,
,
\
[
mInfo
.
lpBaseOfDll
]
,
[
mInfo
.
EntryPoint
]
,
[
mInfo
.
SizeOfImage
]
;
//--------------------------------
invoke GetPhysicallyInstalledSystemMemory
,
buff
push dword
[
buff
+
4
]
dword
[
buff
]
FpuDiv fpuRes1
,
kByte
cinvoke printf
,
,
\
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
,
,
\
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
,
,
\
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
,
,
\
[
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
,
,
\
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
,
@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
,
,
\
[
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
'api\msvcrt.inc'
include
'api\kernel32.inc'
include
'api\user32.inc'
include
'api\psapi.inc'
https://forum.antichat.xyz/attachments/4913888/img_0f42ead66c.png
В скрепке лежит инклуд Kernel32.inc с описанием всех используемых здесь структур. Как оказалось, в штатной поставке FASM'а имеется только оставшийся нам в наследство от Win-XP старый набор, поэтому я обновил его и советую заменить им устаревший инклуд по адресу: fasm\include\equates.
7. Постскриптум.
Такой вот получился "бутафорский ман" с мозговым штурмом..
В статье планировал лишь коротко рассмотреть основные моменты, но в работе диспетчера-памяти всё переплетено в клубок так, что если потянешь за одну нить, то автоматом всплывает другая, без объяснения которой в первой теряется смысл. Здесь вспоминается критика к фильму "Выживщий" с Лео в главной роли: -"Выживщим является тот, кто досмотрел фильм до конца".
В скрепку ложу два исполняемым файла для тестов, инклуд Kernel32.inc, а так-же лист из 1000+ поддерживаемых Win7 кодов IOCTL/FSCTL. Всем удачи, пока!
Оглавление:
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 и имеют префиксы Mm\Mi_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 использует для вкл/откл этого рульного механизма. Всех, кого интересует данная фишка, копайте докуIntel том(4). В системе имеется альтернатива аппаратным 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 и подкидывает нам инфу жалкими крапалями. Однако общую картину зрительно уже можно будет сформировать:
https://forum.antichat.xyz/attachments/4913888/img_507289e698.png
Значит у каждого процесса своё древо 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 представлено ниже:
https://forum.antichat.xyz/attachments/4913888/img_b86751009e.png
Обратите внимание на поле 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 :
+0x004 LeftChild : 0x891a0eb8 _MMVAD
+0x008 RightChild : 0x89a9e398 _MMVAD
+0x00c StartingVpn : 0x400
+0x010 EndingVpn : 0x403
+0x014 u :
+0x018 PushLock : _EX_PUSH_LOCK
+0x01c u5 :
+0x020 u2 :
+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 представлен уже знакомый нам диапазон памяти Start\EndVPN, а так-же указатели на Left\Right узлы дочернего уровня. Что касается атрибутов защиты данного региона, они спрятаны во-вложенных безымянных структурах
u..u5
(union). Чтобы раскрыть их, достаточно указать имя поля в структуре, и поставить в конце точку:
Код:
lkd> dt _MMVAD 86688b20 u.
;//------------------------
nt!_MMVAD
+0x000 u1 :
+0x000 Balance : 0y00
+0x000 Parent : 0x891025c0 _MMVAD ;// 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:
Вышеизложенный подход просмотра 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 \Windows\System32\locale.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 \TEMP\ASM\CODE\ModuleInfo.EXE
89ac7608 ( 3) 759a0 759ea 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\KernelBase.dll
89acc8b0 ( 2) 761f0 7629b 8 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\msvcrt.dll
891821e8 ( 3) 774a0 77574 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\kernel32.dll
89a9e398 ( 1) 77830 77971 10 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
890f18b8 ( 4) 77a00 77a04 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\psapi.dll
89b0b870 ( 3) 77a90 77a90 0 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\apisetschema.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" в окне отладчика):
Для программного доступа к структурам 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_COMMIT\FREE\RESERVE)
Protect dd
0
;
// Защита доступа (см.AllocaProtect)
Type dd
0
;
// Тип памяти в регионе (MEM_MAPPED\PRIVATE\IMAGE)
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() для перечисления флагов всех страниц в наборе. В практической части приводится пример их вызова.
https://forum.antichat.xyz/attachments/4913888/img_c646c4fcba.png
Если процесс пытается обратиться к странице, которой нет на данный момент в его наборе (и соответственно в 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 ;// dt -v _MMPTE u.
;//---------------------------------
struct _MMPTE, 1 elements, 0x8 bytes
+0x000 u : union , 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 ;// 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 ;// !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 (владелец, user\kernel)
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-Кбайтных страниц без РАЕ:
https://forum.antichat.xyz/attachments/4913888/img_cd2751e80a.png
Здесь видно, что из-за ограниченного кол-ва бит, ни о каких 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:
2. Теперь передаём команде
!vtop
полученные на этапе(1) значения "DirBase", и во-втором аргументе любой вирт.адрес, например
0х00401100
:
Значит система для тестов у меня 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":
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 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
:
Реальные эксперименты в отладчике позволяют толковать спецификацию с практической точки зрения, ведь только пощупав объект руками можно сделать о нём выводы. На данный момент мы знаем, что размер одной структуры MMPFN может быть равен 24, 28 или 48-байт. Кол-во структур напрямую зависит от размера установленной в системе физ.памяти ОЗУ. Выходит, что простой арифметикой можно вычислить размер всей базы PFN на текущей машине, что демонстрирует код ниже:
C-подобный:
format pe console
include
'win32ax.inc'
entry start
;
//----------
.
data
sysInfo SYSTEM_INFO
;
// макрос переводит из байт в M\Kbyte
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
,
;
//---- Получить размер страницы\фрейма
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
,
,
esi
;
//---- Запросить реальный размер установленной ОЗУ
invoke GetPhysicallyInstalledSystemMemory
,
buff
push dword
[
buff
+
4
]
dword
[
buff
]
FpuDiv fpuRes1
,
kByte
finit
cinvoke printf
,
,
\
dword
[
fpuRes1
]
,
dword
[
fpuRes1
+
4
]
,
[
sysInfo
.
dwPageSize
]
;
//---- Имеем размер памяти ОЗУ и размер страницы.
;
//---- Вычисляем общее кол-во фреймов PFN
push dword
[
buff
+
4
]
dword
[
buff
]
FpuDiv fpuRes1
,
pageSize
fistp
[
fpuRes2
]
cinvoke printf
,
,
\
dword
[
fpuRes1
]
,
dword
[
fpuRes1
+
4
]
,
\
dword
[
fpuRes2
]
,
dword
[
fpuRes2
+
4
]
,
[
pfnSize
]
;
//---- Всего PFN * размер одной структуры = Размер базы PFN
fld qword
[
fpuRes1
]
fimul
[
pfnSize
]
fidiv
[
mByte
]
fstp
[
fpuRes2
]
cinvoke printf
,
,
\
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
'api\msvcrt.inc'
include
'api\kernel32.inc'
include
'api\user32.inc'
https://forum.antichat.xyz/attachments/4913888/img_f98adfd8f9.png
5.MDL – Memory DescriptorList
Структура 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
;
// dt -v -b _IRP
;//--------------------------------
struct _IRP, 21 elements, 0x70 bytes
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 MdlAddress : Ptr32 to ;//AssociatedIrp.SystemBuffer
. Буфф является общим для входа и выхода – дров принимает данные из этого буфера, а затем передаёт в него-же.[/FONT]
• 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 всегда описывает только один буфер ввода-вывода. Вот как это выглядит графически:
https://forum.antichat.xyz/attachments/4913888/img_504869d0b6.png
Здесь, в вирт.памяти представлено всего три страницы, а буфер 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
001
h
,
pNA
,
002
h
,
pRO
,
004
h
,
pRW
,
008
h
,
pWC
,
010
h
,
pEx
,
020
h
,
pER
dd
040
h
,
pERW
,
080
h
,
pEWC
,
104
h
,
pG
,
200
h
,
pNC
,
400
h
,
pWCN
dd
0
,
mRes
,
1000
h
,
mC
,
2000
h
,
mRes
,
4000
h
,
mDec
,
8000
h
,
mRel
,
10000
h
,
mFree
dd
20000
h
,
mPriv
,
30000
h
,
mCr
,
40000
h
,
mMap
,
80000
h
,
mRst
,
100000
h
,
mTd
,
1000000
h
,
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
;
// Макрос переводит из байт в M\Kbyte
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
10000
h
hProcess dd
0
hModule dd
0
buff db
0
;
//----------
.
code
start
:
invoke SetConsoleTitle
,
;
//---- Получить дескрипторы и заполнить структуры
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
,
,
buff
invoke K32GetModuleInformation
,
-
1
,
[
hModule
]
,
mInfo
,
sizeof
.
MODULEINFO
cinvoke printf
,
,
\
[
mInfo
.
lpBaseOfDll
]
,
[
mInfo
.
EntryPoint
]
,
[
mInfo
.
SizeOfImage
]
;
//--------------------------------
invoke GetPhysicallyInstalledSystemMemory
,
buff
push dword
[
buff
+
4
]
dword
[
buff
]
FpuDiv fpuRes1
,
kByte
cinvoke printf
,
,
\
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
,
,
\
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
,
,
\
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
,
,
\
[
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
,
,
\
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
,
@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
,
,
\
[
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
'api\msvcrt.inc'
include
'api\kernel32.inc'
include
'api\user32.inc'
include
'api\psapi.inc'
https://forum.antichat.xyz/attachments/4913888/img_0f42ead66c.png
В скрепке лежит инклуд Kernel32.inc с описанием всех используемых здесь структур. Как оказалось, в штатной поставке FASM'а имеется только оставшийся нам в наследство от Win-XP старый набор, поэтому я обновил его и советую заменить им устаревший инклуд по адресу: fasm\include\equates.
7. Постскриптум.
Такой вот получился "бутафорский ман" с мозговым штурмом..
В статье планировал лишь коротко рассмотреть основные моменты, но в работе диспетчера-памяти всё переплетено в клубок так, что если потянешь за одну нить, то автоматом всплывает другая, без объяснения которой в первой теряется смысл. Здесь вспоминается критика к фильму "Выживщий" с Лео в главной роли: -"Выживщим является тот, кто досмотрел фильм до конца".
В скрепку ложу два исполняемым файла для тестов, инклуд Kernel32.inc, а так-же лист из 1000+ поддерживаемых Win7 кодов IOCTL/FSCTL. Всем удачи, пока!