Статья ASM – Распределение физ.памяти процессам Windows

В системной архитектуре отображение вирт.памяти на физическую носит таинственный характер. Предлагая материал на эту тему, 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 и подкидывает нам инфу жалкими крапалями. Однако общую картину зрительно уже можно будет сформировать:


Main_scheme.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 представлено ниже:


VAD.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                : <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 представлен уже знакомый нам диапазон памяти Start\EndVPN, а так-же указатели на Left\Right узлы дочернего уровня. Что касается атрибутов защиты данного региона, они спрятаны во-вложенных безымянных структурах 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           \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" в окне отладчика):


Код:
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: \TEMP\ASM\CODE\ModuleInfo.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_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() для перечисления флагов всех страниц в наборе. В практической части приводится пример их вызова.


WorkingSet.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 ;//<--- требует отлаживаемую ОС


4. MMU – блок управления памятью

Во-внутреннем блоке MMU процессора нас будет интересовать только "транслятор адреса" из виртуального в физический. Управляют этим блоком контрольные регистры CR0-CR4 процессора, и модельно-специфичный MSR.IA32_EFER (Extended Feature Enables Register, вкл.расширенных возможностей). Вот их описание из мануалов Intel том(3,4):


MSR_CR0.png


Если на машине и установлен 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):


MMU_x64.png


Как видим, из 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 явно перестарались с тех.доками на транслятор – один зоопарк имён чего только стоит: PML4\PML4E, PDP\PDPE, PDT\PDTE, PT\PTE. Такое разнообразие уже на взлёте сбивает нас с толку и механизм трансляции кажется чем-то сверх сложным. Но если учесть, что все записи в представленных блоках имеют одинаковый внутренний формат, то для лучшего понимания можно обозвать их просто уровнями "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):


CR3_PTE.png


Важно понять, что именно на уровне записей 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 (владелец, 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-Кбайтных страниц без РАЕ:


MMU_x32.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:


Код:
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 состояния фреймов, хотя всего их 8):


PFN_base.png


Расширение отладчика !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

;// макрос переводит из байт в 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,<'*** 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  'api\msvcrt.inc'
include  'api\kernel32.inc'
include  'api\user32.inc'

PFN_Size.png



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". На рис.ниже показано, как кодируется запрос:


IOCTL.png


Обратите внимание, что 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 всегда описывает только один буфер ввода-вывода. Вот как это выглядит графически:


MDL.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  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

;// Макрос переводит из байт в 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   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  'api\msvcrt.inc'
include  'api\kernel32.inc'
include  'api\user32.inc'
include  'api\psapi.inc'

MemInfo.png


В скрепке лежит инклуд Kernel32.inc с описанием всех используемых здесь структур. Как оказалось, в штатной поставке FASM'а имеется только оставшийся нам в наследство от Win-XP старый набор, поэтому я обновил его и советую заменить им устаревший инклуд по адресу: fasm\include\equates.


7. Постскриптум.

Такой вот получился "бутафорский ман" с мозговым штурмом..
В статье планировал лишь коротко рассмотреть основные моменты, но в работе диспетчера-памяти всё переплетено в клубок так, что если потянешь за одну нить, то автоматом всплывает другая, без объяснения которой в первой теряется смысл. Здесь вспоминается критика к фильму "Выживщий" с Лео в главной роли: -"Выживщим является тот, кто досмотрел фильм до конца".

В скрепку ложу два исполняемым файла для тестов, инклуд Kernel32.inc, а так-же лист из 1000+ поддерживаемых Win7 кодов IOCTL/FSCTL. Всем удачи, пока!
 

Вложения

  • MMU_VMM.zip
    22,4 КБ · Просмотры: 88

Mikl___

Green Team
01.09.2019
25
41
Замечательная статья! Браво!
Тимур, можно я твои статьи на WASM.IN перенесу? После технических неполадок на codeby.net стало как-то боязно... :(
 
  • Нравится
Реакции: Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
291
1 267
можно я твои статьи на WASM.IN перенесу? После технических неполадок на codeby.net стало как-то боязно
@Mikl___ сенкью за такую честь, но думаю обычных линков (которые уже на васме есть) будет вполне достаточно. Проблемы тут были временными + у сайта есть бэкап и зеркало на info.
 
  • Нравится
Реакции: Dzen и Mikl___

p1eioper

One Level
12.12.2020
5
1
Благодарю за просвящение. ты делаешь этот мир айти лучше
 
Мы в соцсетях: