Управление памятью в ядре Windows XP

Дата публикации 17 янв 2008

Управление памятью в ядре Windows XP — Архив WASM.RU

Для начала нечто вроде вступления. Статья рассчитана на тех, кто уже работал с памятью в режиме ядра и отличает MmProbeAndLockPages от MmMapLockedPagesSpecifyCache, а так же знаком с основами устройства управления памятью у процессора - каталоги страниц (PDE), таблицы страниц (PTE), исключение ошибки страницы (#PF). Для исправления первого упущения рекомендуется сначала прочитать соответствующие статьи Four-F (http://www.wasm.ru/series.php?sid=9, части 6 и 9), для исправления второго - статьи цикла BrokenSword "Процессор Intel в защищенном режиме" (http://www.wasm.ru/series.php?sid=20, части 6 и 7, кстати, в части 7 есть ошибка в картинке - вместо картинки для PDE 4M страниц представлена картинка для PDE 4K страниц).

I. Устройство PDE/PTE, невалидные PTE

Рассмотрим сначала как в Windows используются поля PTE, которые помечены Intel как доступные программному обеспечению операционной системы (Avail.) Эти три бита операционная система Windows использует следующим образом (структуры при выключенном и включенном PAE соответственно):

Код (Text):
  1. typedef struct _MMPTE_HARDWARE {
  2.     ULONG Valid : 1;
  3.     ULONG Write : 1;
  4.     ULONG Owner : 1;
  5.     ULONG WriteThrough : 1;
  6.     ULONG CacheDisable : 1;
  7.     ULONG Accessed : 1;
  8.     ULONG Dirty : 1;
  9.     ULONG LargePage : 1;
  10.     ULONG Global : 1;
  11.     ULONG CopyOnWrite : 1; // software field
  12.     ULONG Prototype : 1;   // software field
  13.     ULONG reserved : 1;    // software field
  14.     ULONG PageFrameNumber : 20;
  15. } MMPTE_HARDWARE, *PMMPTE_HARDWARE;
  16. <p>
  17. typedef struct _MMPTE_HARDWARE_PAE {
  18.     ULONGLONG Valid : 1;
  19.     ULONGLONG Write : 1;
  20.     ULONGLONG Owner : 1;
  21.     ULONGLONG WriteThrough : 1;
  22.     ULONGLONG CacheDisable : 1;
  23.     ULONGLONG Accessed : 1;
  24.     ULONGLONG Dirty : 1;
  25.     ULONGLONG LargePage : 1;
  26.     ULONGLONG Global : 1;
  27.     ULONGLONG CopyOnWrite : 1; // software field
  28.     ULONGLONG Prototype : 1;   // software field
  29.     ULONGLONG reserved0 : 1;  // software field
  30.     ULONGLONG PageFrameNumber : 24;
  31.     ULONGLONG reserved1 : 28;  // software field
  32. } MMPTE_HARDWARE_PAE, *PMMPTE_HARDWARE_PAE;

Комментариями помечены такие поля.

Поле CopyOnWrite означает, является ли страница копируемой при записи. Такие страницы с пользовательской стороны задаются атрибутом PAGE_WRITECOPY или PAGE_EXECUTE_WRITECOPY и означают, что процессу будет выделена личная копия страницы при попытке записи в неё. Остальные будут использовать публичную не модифицированную копию. Поле Prototype для валидного PTE означает, что это т.н. прототипный PTE, используемый для разделения памяти между процессами с помощью механизма проецированных в память файлов (Memory Mapped Files, MMF, см. документацию на Win32 API CreateFileMapping, OpenFileMapping, MapViewOfFile(Ex)) Поле reserved для валидного PTE не используется, для невалидного PTE этот бит называется Transition и установлен, когда PTE считается переходным.

Не буду рассказывать про аппаратное управление памятью и остальные поля структур PDE/PTE: об этом неплохо писали уже не один десяток раз. Последующее же повествование пойдет про формат тех PTE, которые использует Windows при флаге Valid = 0, или про недействительные (невалидные) PTE.

  • Paged out PTE (выгруженные PTE) - невалидный PTE, описывающий страницу, выгруженную в файл подкачки. По первому требованию она вновь будет считана и включена в рабочий набор. PTE описывается структурой

    Код (Text):
    1. typedef struct _MMPTE_SOFTWARE {
    2.     ULONG Valid : 1;
    3.     ULONG PageFileLow : 4;
    4.     ULONG Protection : 5;
    5.     ULONG Prototype : 1;
    6.     ULONG Transition : 1;
    7.     ULONG PageFileHigh : 20;
    8. } MMPTE_SOFTWARE;
    А при включенном PAE такой:

    Код (Text):
    1. typedef struct _MMPTE_SOFTWARE_PAE {
    2.     ULONGLONG Valid : 1;
    3.     ULONGLONG PageFileLow : 4;
    4.     ULONGLONG Protection : 5;
    5.     ULONGLONG Prototype : 1;
    6.     ULONGLONG Transition : 1;
    7.     ULONGLONG Unused : 20;
    8.     ULONGLONG PageFileHigh : 32;
    9. } MMPTE_SOFTWARE_PAE;
    При этом Valid = 0 PageFileLow содержит номер страничного файла (которых, как нетрудно догадаться, максимально может быть 16 штук) Protection, соответственно, атрибуты доступа на страницу, задается в виде констант MM_*:

    Код (Text):
    1. #define MM_ZERO_ACCESS         0  // this value is not used.
    2. #define MM_READONLY            1
    3. #define MM_EXECUTE             2
    4. #define MM_EXECUTE_READ        3
    5. #define MM_READWRITE           4  // bit 2 is set if this is writable.
    6. #define MM_WRITECOPY           5
    7. #define MM_EXECUTE_READWRITE   6
    8. #define MM_EXECUTE_WRITECOPY   7
    9. #define MM_NOCACHE             8
    10. #define MM_DECOMMIT         0x10
    11. #define MM_NOACCESS         MM_DECOMMIT|MM_NOCACHE
    Prototype = 0 Transition = 0 PageFileHigh - номер страницы в страничном файле (о страничном файле и подкачке далее)

  • Demand zero PTE (обнуляемые по требованию PTE) - невалидный PTE, описывающий страницу, которой еще нет в рабочем наборе, но по обращению она должна быть выделена либо из списка обнуленных страниц, либо из списка свободных страниц, обнулена и добавлена в рабочий набор. Описывается аналогично выгруженному PTE за тем исключением, что PageFileHigh = 0.
  • Prototype PTE (прототипные PTE) - невалидные PTE, которые описывают страницы, разделяемые несколькими процессами, например, спроецированные в память файлы. Точнее, такие PTE создаются в одном экземпляре и не включаются в списки PDE, а в списках PDE процесса располагаются следующие невалидные PTE, ссылающиеся на прототипные PTE, соответственно их версии для систем без PAE и с PAE:

    Код (Text):
    1. typedef struct _MMPTE_PROTOTYPE {
    2.     ULONG Valid : 1;
    3.     ULONG ProtoAddressLow : 7;
    4.     ULONG ReadOnly : 1;
    5.     ULONG WhichPool : 1;
    6.     ULONG Prototype : 1;
    7.     ULONG ProtoAddressHigh : 21;
    8. } MMPTE_PROTOTYPE;
    9. <p>
    10. typedef struct _MMPTE_PROTOTYPE_PAE {
    11.     ULONGLONG Valid : 1;
    12.     ULONGLONG Unused0: 7;
    13.     ULONGLONG ReadOnly : 1;
    14.     ULONGLONG Unused1: 1;
    15.     ULONGLONG Prototype : 1;
    16.     ULONGLONG Protection : 5;
    17.     ULONGLONG Unused: 16;
    18.     ULONGLONG ProtoAddress: 32;
    19. } MMPTE_PROTOTYPE_PAE;

    При этом: Valid = 0 ProtoAddress (ProtoAddressLow/ProtoAddressHigh) содержат ссылку на прототипный PTE, описывающий разделяемую страницу. Prototype = 1 Protection содержит атрибуты защиты страницы (MM_*) ReadOnly установлен, если страница должна только читаться. Игнорируется при загрузке образов в пространство сеанса - загрузчику позволяется писать в такие страницы в целях обработки импортов или расставления релоков. WhichPool назначение этого поля мне неизвестно..

  • Transition PTE (переходные PTE) - невалидные PTE, описывающие страницу, которая находится в списке Standby, Modified или ModifiedNoWrite страниц (об этих списках далее). При обращении страница возвращается в рабочий набор. Описываются следующими структурами:

    Код (Text):
    1. typedef struct _MMPTE_TRANSITION {
    2.     ULONG Valid : 1;
    3.     ULONG Write : 1;
    4.     ULONG Owner : 1;
    5.     ULONG WriteThrough : 1;
    6.     ULONG CacheDisable : 1;
    7.     ULONG Protection : 5;
    8.     ULONG Prototype : 1;
    9.     ULONG Transition : 1;
    10.     ULONG PageFrameNumber : 20;
    11. } MMPTE_TRANSITION;
    12. <p>
    13. typedef struct _MMPTE_TRANSITION_PAE {
    14.     ULONGLONG Valid : 1;
    15.     ULONGLONG Write : 1;
    16.     ULONGLONG Owner : 1;
    17.     ULONGLONG WriteThrough : 1;
    18.     ULONGLONG CacheDisable : 1;
    19.     ULONGLONG Protection : 5;
    20.     ULONGLONG Prototype : 1;
    21.     ULONGLONG Transition : 1;
    22.     ULONGLONG PageFrameNumber : 24;
    23.     ULONGLONG Unused : 28;
    24. } MMPTE_TRANSITION_PAE;

    При этом: Valid = 0 Prototype = 0 Transition = 1 Назначение остальных полей аналогично валидному PTE

II. Обработка ошибок страниц

Когда процессор сталкивается с невалидным PTE, генерируется исключение ошибки страницы (#PF, Page Fault). В Windows обработчик _KiTrap0E вызывает MmAccessFault() для обработки исключения, которая после некоторого числа проверок вызывает MiDispatchFault, если страница должна быть разрешена успешно.

MiDispatchFault вызывает одну из следующих функций для разрешения ошибки страницы:

  • MiResolveProtoPteFault вызывается при ошибке страницы на PTE c флагом Prototype=1 Она исследует прототипный PTE, на который указывает сбойный PTE и: 1. Если прототипный PTE тоже имеет флаг Prototype, значит это разделяемые страницы проецированного в память файла. Вызывается MiResolveMappedFileFault. 2. Если прототипный PTE имеет флаг Transition, то значит, что это переходный PTE, его страница находится в списке модифицированных или простаивающих страниц. Попала она туда в результате усечения рабочего набора. Вызывается MiResolveTransitionFault. 3. Если у прототипного PTE Transition==0 && Prototype==0 && PageFileHigh==0, то это demand-zero PTE. Вызывается MiResolveDemandZeroFault. 4. Если у прототипного PTE Transition==0 && Prototype==0 && PageFileHigh!=0, то страница выгружена в своп. Вызывается MiResolvePageFileFault.

  • MiResolveTransitionFault вызывается, когда сбойный PTE имеет флаг Transition=1, либо если он указывает на прототипный PTE, имеющий флаг Transition. Поскольку страницы в этом состоянии оказываются в результате усечения рабочего набора или других обстоятельств, когда понадобились физические страницы, то разрешение такой ошибки страницы должно заключаться в возвращении страницы в рабочий набор. Поскольку страница еще не выгружена на диск, то сделать это очень просто - нужно лишь записать валидный PTE на место недействительного. Например, в состояние Transition как раз и переводит страницы функция MmTrimAllSystemPagableMemory(0), но о ней подробнее далее в части статьи, посвященную подкачке.

  • MiResolveDemandZeroFault вызван при обработке ошибки страницы, обнуляемой по требованию. Если запрос был из пользовательского режима, тогда идет попытка выделения физической страницы из списка обнуленных страниц (о поддерживаемых списках физических страниц далее). Если это не удается, выделяется свободная страница и обнуляется. При запросе из режима ядра обнуление не форсируется при выделении страницы из списка свободных страниц. Для обнуления используются зарезервированные системные PTE или гиперпространство.
  • MiResolvePageFileFault вызывается при обработке ошибки страницы, которая была выгружена в файл подкачки. Инициируется операция чтения файла подкачки за счет возвращения статуса STATUS_ISSUE_PAGING_IO, страницы читаются из файла подкачки кластерами для снижения числа ошибок страниц. Когда MiDispatchFault получает статус STATUS_ISSUE_PAGING_IO, она выполняет операцию чтения страниц с помощью функции IoPageRead, которая производит создание обычного IRP для операции IRP_MJ_READ, но ставит в нем специальный флаг IRP_PAGING_IO. Страница выбирается из списка свободных или обнуленных страниц.
  • MiResolveMappedFileFault вызывается из MiResolveProtoPteFault когда Prototype==1 у прототипного PTE. Тогда PTE трактуется следующим образом (варианты без PAE и с PAE):

    Код (Text):
    1. typedef struct _MMPTE_SUBSECTION {
    2.     ULONG Valid : 1;
    3.     ULONG SubsectionAddressLow : 4;
    4.     ULONG Protection : 5;
    5.     ULONG Prototype : 1;
    6.     ULONG SubsectionAddressHigh : 20;
    7.     ULONG WhichPool : 1;
    8. } MMPTE_SUBSECTION;
    9. <p>
    10. typedef struct _MMPTE_SUBSECTION {
    11.     ULONGLONG Valid : 1;
    12.     ULONGLONG Unused0 : 4;
    13.     ULONGLONG Protection : 5;
    14.     ULONGLONG Prototype : 1;
    15.     ULONGLONG Unused1 : 21;
    16.     ULONGLONG SubsectionAddress : 32;
    17. } MMPTE_SUBSECTION;
    18.  

    Он содержит адрес объекта SUBSECTION, поддерживающего спроецированный файл. Например, в SUBSECTION::ControlArea->FilePointer хранится FILE_OBJECT файла.

III. Управление физической памятью Физическая память в системе описывается определенными структурами режима ядра. Они необходимы для поддержания списка свободных и занятых страниц, для удовлетворения аллокаций и других операций с памятью. Для начала рассмотрим, какие же основные части ядра отвечают за описание и распределение физической памяти системы. Первой структурой, на которую мы обратим внимание, будет MmPhysicalMemoryDescriptor, имеющую описание:

Код (Text):
  1. typedef struct _PHYSICAL_MEMORY_RUN {
  2.     PFN_NUMBER BasePage;
  3.     PFN_NUMBER PageCount;
  4. } PHYSICAL_MEMORY_RUN, *PPHYSICAL_MEMORY_RUN;
  5. <p>
  6. typedef struct _PHYSICAL_MEMORY_DESCRIPTOR {
  7.     ULONG NumberOfRuns;
  8.     PFN_NUMBER NumberOfPages;
  9.     PHYSICAL_MEMORY_RUN Run[1];
  10. } PHYSICAL_MEMORY_DESCRIPTOR, *PPHYSICAL_MEMORY_DESCRIPTOR;
  11. <p>
  12. PPHYSICAL_MEMORY_DESCRIPTOR MmPhysicalMemoryDescriptor;

Переменная ядра MmPhysicalMemoryDescriptor описывает всю доступную и пригодную для использования физическую память в системе и инициализируется при загрузке.

Ядро поддерживает шесть списков страниц (из восьми возможных состояний), в которых размещаются практически все физические страницы, разве что за исключением тех, что используются самим менеджером памяти. Списки страниц поддерживаются указателями u1.Flink и u2.Blink в структуре MMPFN (о ней далее). Это списки:

  • ZeroedPageList - Список обнуленных страниц, которые можно выдавать по запросу из пользовательского кода. В фоновом режиме работает поток MmZeroPageThread (в него переходит первичный поток KiSystemStartup после всей инициализации) и занимается обнулением свободных страниц с перемещением их в этот список. При запросе страницы пользовательским кодом это наиболее приоритетный список, откуда может быть вынута страница.
  • FreePageList - список свободных страниц. Они могут быть переданы пользоваелю после обнуления (поток либо сигналит событие MmZeroingPageEvent, тогда страницу обнуляет поток обнуления страниц MmZeroPageThread, либо в некоторых исключительных случаях обнуляет самостоятельно - например при обработке #PF на PTE типа demand-zero. В этом случае передача страницы потоку обнуления повлечет за собой дополнительные потери времени), в запросах от пользователя это второй по приоритету список после ZeroedPageList.
  • StandbyPageList - список простаивающих страниц. Эти страницы раньше входили в рабочий набор (процесса или системы), но в последствии были удалены из него. Страница не была изменена с последней записи на диск, PTE, ссылающийся на такую страницу, находится в переходном (transition) состоянии и страница может быть использована для удовлетворения запроса на выделение памяти, но после просмотра списков обнуленных и свободных страниц. ( В Windows 2003 есть 8 подсписков, поддерживающих простаивающие страницы, по приоритетам, они описаны в массиве MmStandbyPageListByPriority[]. В Windows XP и ниже список один)
  • ModifiedPageList - список модифицированных страниц, они тоже раньше входили в рабочий набор, но в последствии были удалены из него в результате сокращения рабочих наборов по какой-либо причине. Страницы были изменены с момента последней записи на диск и должны быть записаны в файл подкачки. PTE все еще ссылается на страницу, но недействителен и находится в состоянии transition.
  • ModifiedNoWritePageList - список модифицированных, но не записываемых страниц. Аналогично предыдущему, но страница не должна быть записана на диск.
  • BadPageList - список страниц, которые были помечены менеджером памяти плохими по каким-либо причинам. Они не должны быть использованы. Например, поток обнуления страниц временно помечает страницы плохими, когда ищет область из страниц, ждущих обнуления, чтобы они не были вдруг переданы какому-нибудь процессу по запросу выделения продолжительного региона памяти (MmAllocateContiguousMemory). PTE не должны ссылаться на такую страницу.
Состояние страниц, не являющиеся списками:
  • ActiveAndValid - страница активная и действительная, не входит ни в один список. Такие страницы являются частью рабочего набора или не входят ни в один рабочий набор и являются частью неподкачиваемой памяти системы. На них ссылаются действительные PTE.
  • TransitionPage - временное состояние страницы на время ожидания операции ввода-вывода.

Указатели на списки хранит переменная ядра MmPageLocationList[], содержимое которой объявлено следующим образом:

Код (Text):
  1. PMMPFNLIST MmPageLocationList[8] =
  2.  {
  3.   &MmZeroedPageListHead,
  4.   &MmFreePageListHead,
  5.   &MmStandbyPageListHead,
  6.   &MmModifiedPageListHead,
  7.   &MmModifiedNoWritePageListHead,
  8.   &MmBadPageListHead,
  9.   NULL,
  10.   NULL };
  11.  

Есть два важных потока, оперирующих списками страниц - поток обнуления страниц и поток записи модифицированных страниц.

  • Поток обнуления страниц. В него KiSystemStartup переходит после инициализации всех компонентов системы и запуска менеджера сессий smss. Он занимается тем, что в цикле ожидает события MmZeroingPageEvent. Когда оно наступает (а наступает оно при наличии в системе достаточного количества свободных страниц, чтобы поток обнуления мог их стереть), захватывается спин-блокировка базы данных фреймов (PFN Database), выделяется страница из списка свободных страниц, проецируется в гиперпространство и обнуляется, после чего включается в список обнуленных страниц и цикл повторяется.
  • Поток записи модифицированных страниц. После старта подсистемы управления памятью MmInitSystem() создает через PsCreateSystemThread поток MiModifiedPageWriter, который стартует второй вспомогательный поток MiMappedPageWriter, а сам переходит в MiModifiedPageWriterWorker. Основной функцией выгрузки страниц в своп-файл является MiGatherMappedPages, о выгрузке будет рассказано далее в следующей части статьи.

MmPfnDatabase. MmPfnDatabase - это массив структур MMPFN, описывающих каждую физическую страницу в системе. Это, пожалуй, второй по важности объект, после массивов PDE/PTE, которые поддерживают низкоуровневые операции с памятью. В списках PFN хранится информация о конкретной физической странице. Схематично MMPFN представляется в следующем виде (полное объявлеие прилагается к исходникам к статье, в том числе и для других версий ОС - Windows 2000, Windows 2003 Server):

Код (Text):
  1. typedef struct _MMPFN {
  2.     union {
  3.         PFN_NUMBER Flink;             // Used if (u3.e1.PageLocation < ActiveAndValid)
  4.         WSLE_NUMBER WsIndex;          // Used if (u3.e1.PageLocation == ActiveAndValid)
  5.         PKEVENT Event;                // Used if (u3.e1.PageLocation == TransitionPage)
  6.         NTSTATUS ReadStatus;          // Used if (u4.InPageError == 1)
  7.     } u1;
  8.     PMMPTE PteAddress;
  9.     union {
  10.         PFN_NUMBER Blink;             // Used if (u3.e1.PageLocation < ActiveAndValid)
  11.         ULONG ShareCount;             // Used if (u3.e1.PageLocation >= ActiveAndValid)
  12.         ULONG SecondaryColorFlink;    // Used if (u3.e1.PageLocation == FreePageList or == ZeroedPageList)
  13.     } u2;
  14.     union {
  15.         struct _MMPFNENTRY {
  16.             ULONG Modified : 1;
  17.             ULONG ReadInProgress : 1;
  18.             ULONG WriteInProgress : 1;
  19.             ULONG PrototypePte: 1;
  20.             ULONG PageColor : 3;
  21.             ULONG ParityError : 1;
  22.             ULONG PageLocation : 3;
  23.             ULONG RemovalRequested : 1;
  24.             ULONG CacheAttribute : 2;
  25.             ULONG Rom : 1;
  26.             ULONG LockCharged : 1;
  27.             ULONG ReferenceCount : 16;
  28.         } e1;
  29.         struct {
  30.             USHORT ShortFlags;
  31.             USHORT ReferenceCount;
  32.         } e2;
  33.     } u3;
  34.     MMPTE OriginalPte;
  35.     union {
  36.         ULONG EntireFrame;
  37.         struct {
  38.             ULONG PteFrame : 26;
  39.             ULONG InPageError : 1;
  40.             ULONG VerifierAllocation : 1;
  41.             ULONG AweAllocation : 1;
  42.             ULONG LockCharged : 1;
  43.             ULONG KernelStack : 1;
  44.             ULONG Reserved : 1;
  45.         };
  46.     } u4;
  47. } MMPFN, *PMMPFN;

Элементы u1.Flink / u2.Blink поддерживают связанность шести списков страниц, про которые говорилось выше, используются, когда u3.e1.PageLocation < ActiveAndValid. Если u3.e1.PageLocation >= ActiveAndValid, тогда второе объединение трактуется как u2.ShareCount и содержит счетчик числа пользователей - количество PTE, ссылающихся на эту страницу. Для страниц, содержащих массивы PTE, содержит число действительных PTE на странице. Если u3.e1.PageLocation == ActiveAndValid, u1 трактуется как u1.WsIndex - индекс страницы в рабочем наборе (или 0 если страница в неподкачиваемой области памяти). Если u3.e1.PageLocation == TransitionPage, u1 трактуется как u1.Event - адрес объекта "событие", на котором будет ожидать менеджер памяти для разрешения доступа на страницу. Если u4.InPageError == 1, то u1 трактуется как ReadStatus и содержит статус ошибки чтения.

ReferenceCount содержит счетчик ссылок действительных PTE на эту страницу или использования ее внутри менеджера памяти (например, во время записи страницы на диск, счетчик ссылок увеличивается на единицу). Он всегда >= ShareCount PteAddress содержит обратную ссылку на PTE, который указывает на эту физическую cтраницу. Младший бит означает, что PFN удаляется. OriginalPte содержит оригинальный PTE, используемый для восстановления его в случае выгрузки страницы. u4.PteFrame - номер PTE, поддерживающего страницу, где находится текущая структура MMPFN. Кроме того объединение u4 содержит еще и следующие дополнительные флаги:

  • InPageError - показывает, что при чтении страницы с диска произошла ошибка. u1.ReadStatus хранит статус этой ошибки.
  • VerifierAllocation устанавливается в единицу для аллокаций, защищаемых Driver Verifier.
  • AweAllocation устанавливается в единицу для Address Windowing Extension
  • Назначение поля LockCharged и одноименного поля MMPFNENTRY мне, к сожалению, не известно. Если кто знает - поделитесь.
  • KernelStack, видимо, устанавливается в единицу для страниц, принадлежащих стеку ядра.

Если страница находится в списке обнуленных или простаивающих страниц, второе объединение трактуется как указатель, связывающий списки обнуленных или свободных страниц по вторичному цвету (т.н. Secondary Color). Различие по цвету делается по следующей причине: количество цветов устанавливается в количество страниц, которые может вместить в себя кеш-память второго уровня процессора и различие делается, чтобы два соседних выделения памяти не использовали страницы одного цвета для правильного использования кеша.

Объединение u3, фактически, содержит флаги данного PFN. Рассмотрим что же они означают:

  • Modified. Установлен для подкачиваемых или спроецированных с диска страниц, что ее содержимое было изменено и должно быть сброшено на диск.
  • ReadInProgress, он же StartOfAllocation
  • WriteInProgress, он же EndOfAllocation
  • PrototypePte, он же LargeSessionAllocation Для неподкачиваемых системных адресов эти три поля трактуется как StartOfAllocation, EndOfAllocation и LargeSessionAllocation и обозначают следующее:
    • StartOfAllocation установлено в 1, если эта страница является началом неподкачиваемого пула.
    • EndOfAllocation установлено в 1, если эта страница является концом неподкачиваемого пула.
    • LargeSessionAllocation установлено в 1 для больших аллокаций в пространстве сессии.
    Для подкачиваемых адресов эти поля означают следующее:
    • ReadInProgress установлен, пока страница находится в процессе чтения с диска
    • WriteInProgress установлен, пока страница записывается на диск
    • PrototypePte установлен, когда PTE, который ссылается на эту PFN, является прототипным.
  • PageColor, он же иногда называемый Primary Page Color, или цвет страницы. Используется на некоторых платформах для равномерного распределения списков страниц (аллокации вида MiRemoveAnyPage выдаются страницу каждый раз из другого списка другого цвета и несколько списков, поддерживающих, например, свободные страницы, расходуются равномерно). В x86 и x64 используется всего один цвет страниц и это поле всегда равно нулю. Не путать с Secondary Color, который используется для равномерного распределения страниц по кешу второго уровня и используется в функциях MiRemoveZeroPage, MiRemoveAnyPage и др. Кроме простых списков свободных и обнуленных страниц так же поддерживаются списки свободных и обнуленных страниц по цвету - MmFreePagesByColor[список][SecondaryColor], где _список_ - это ZeroedPageList или FreePageList. Списки поддерживаются вместе с общими списками свободных и обнуленных страниц, при обнаружении несоответствия генерируется синий экран PFN_LIST_CORRUPT.
  • PageLocation - тип страницы (как раз один из восьми вышеперечисленных от ZeroedPageList до TransitionPage)
  • RemovalRequested - этим битом помечаются страницы, запрошенные к удалению. После уменьшения их счетчика ссылок до нуля, PTE станет недействительным переходным, а страница попадет в список плохих (BadPageList)
  • CacheAttribute - атрибут кеширования страницы. MmNonCached или MmCached.
  • Rom - новшество WinXP: физическая страница доступна только для чтения.
  • ParityError - на странице произошла ошибка честности

Лучше усвоить написанное поможет пример, содержащийся в приложении к статье. В примере драйвер, который показывает доступные Memory Runs и демонстрирует обращение с PDE/PTE/PFN. Код примера хорошо откомментирован и, с учетом материала статьи, не должен вызвать вопросов.

IV. Управление виртуальной памятью - файл подкачки

Однако размещать все данные постоянно в физической памяти невыгодно - к каким-то данным обращения происходят редко, к каким-то часто, к тому иногда требуются объемы памяти большие, чем доступно физической памяти в системе. Поэтому во всех современных ОС реализован механизм подкачки страниц. Называется он по-разному - выгрузка, подкачка, своп. В Windows этот механизм представляет собой часть менеджера памяти, управляющего подкачкой, и максимально до 16 различных страничных файлов (paging files в терминологии Windows). В Windows есть подкачиваемая и неподкачиваемая память, соответственно, они могут и не могут быть выгружены на диск. Подкачиваемую память в ядре можно выделить из пула подкачиваемой памяти, неподкачиваемую - соответственно из пула неподкачиваемой (для небольших аллокаций). В пользовательском режиме память обычно подкачиваемая, если только она не была заблокирована в рабочем наборе с помощью вызова VirtualLock. Страничные файлы в ядре Windows представлены переменной ядра MmPagingFile[MAX_PAGE_FILES] (максималное число страничных файлов, как можно было догадаться еще в самом начале по размеру поля номера страницы в страничном файле в 4 бита, составляет 16 штук). Каждый страничный файл в этом массиве представлен указателем на структуру вида:

Код (Text):
  1. typedef struct _MMPAGING_FILE {
  2.     PFN_NUMBER Size;
  3.     PFN_NUMBER MaximumSize;
  4.     PFN_NUMBER MinimumSize;
  5.     PFN_NUMBER FreeSpace;
  6.     PFN_NUMBER CurrentUsage;
  7.     PFN_NUMBER PeakUsage;
  8.     PFN_NUMBER Hint;
  9.     PFN_NUMBER HighestPage;
  10.     PVOID Entry[MM_PAGING_FILE_MDLS];
  11.     PRTL_BITMAP Bitmap;
  12.     PFILE_OBJECT File;
  13.     UNICODE_STRING PageFileName;
  14.     ULONG PageFileNumber;
  15.     BOOLEAN Extended;
  16.     BOOLEAN HintSetToZero;
  17.     BOOLEAN BootPartition;
  18.     HANDLE FileHandle;
  19. <p>
  20. } MMPAGING_FILE, *PMMPAGING_FILE;
  21.  
  • Size - текущий размер файла подкачки (стр.)
  • MaximumSize - максимальный размер файла подкачки (стр.)
  • MinimumSize - минимальный размер файла подкачки (стр.)
  • FreeSpace - число свободных страниц
  • CurrentUsage - число занятых страниц. Всегда верна формула Size = FreeSpace+CurrentUsage+1 (первая страница не используется)
  • PeakUsage - пиковая нагрузка на файл подкачки
  • Hint, HighestPage, HintSetToZero - [назначение неизвестно]
  • Entry - массив из двух указателей на блоки MMMOD_WRITER_MDL_ENTRY, используемые потоком записи модифицированных страниц.
  • Bitmap - битовая карта RTL_BITMAP занятости страниц в файле подкачки.
  • File - объект "файл" файловой системы, используемый для чтения/записи в файл подкачки
  • PageFileName - имя файла подкачки, например, \??\C:\pagefile.sys
  • PageFileNumber - номер файла подкачки
  • Extended - флаг, предположительно указывающий на то, расширялся ли файл подкачки когда-либо с момента создания
  • BootPartition - флаг, указывающий на то, располагается ли файл подкачки на загрузочном разделе. Если нет ни одного страничного файла, размещенного на загрузочном разделе, то во время BSoD аварийный дамп записываться не будет.
  • FileHandle - хендл файла подкачки.

В приложении к статье есть откомментированный пример с выводом полей структуры MmPagingFile[0] рабочей системы.

Когда системе нужна страница, а свободных страниц осталось мало, происходит усечение рабочих наборов процессов (оно происходит и по другим причинам, это лишь одна из них). Допустим, что усечение рабочих наборов было инициировано функцией MmTrimAllSystemPagableMemory(0). Во время усечения рабочих наборов, PTE страниц переводятся в состояние Transition, счетчик ссылок Pfn->u3.e2.ReferenceCount уменьшеается на 1 (это выполняет функция MiDecrementReferenceCount). Если счетчик ссылок достиг нуля, сами страницы заносятся в списки StandbyPageList или ModifiedPageList, в зависимости от Pfn->u3.e1.Modified. Страницы из списка StandbyPageList могут быть использованы сразу, как только потребуется - для этого достаточно лишь перевести PTE в состояние Paged-Out. Страницы из списка ModifiedPageList должны быть сперва записаны потоком записи модифицированных страниц на диск, а уж после чего они переводятся в StandbyPageList и могут быть использованы (за выгрузку отвечает функция MiGatherPagefilePages()). Псевдокод снятия страницы из рабочего набора (сильно обрезанный код MiEliminateWorkingSetEntry и вызываемых из нее функций):

Код (Text):
  1. TempPte = *PointerPte;
  2. PageFrameNumber = PointerPte->u.Hard.PageFrameNumber;
  3.  
  4. if( Pfn->u3.e1.PrototypePte == 0)
  5. {
  6.     //
  7.     // Приватная страница, сделать переходной.
  8.     //
  9.    
  10.     MI_ZERO_WSINDEX (Pfn);  // Pfn->u1.WsIndex = 0;
  11.    
  12.     //
  13.     // Следующий макрос делает это:
  14.     //
  15.     // TempPte.u.Soft.Valid = 0;
  16.     // TempPte.u.Soft.Transition = 1;
  17.     // TempPte.u.Soft.Prototype = 0;
  18.     // TempPte.u.Trans.Protection = PROTECT;
  19.     //
  20.    
  21.     MI_MAKE_VALID_PTE_TRANSITION (TempPte,
  22.                                   Pfn->OriginalPte.u.Soft.Protection);
  23.                                  
  24.     //
  25.     // Этот вызов на самом деле заменяет текущий PTE на TempPte и очищает буфера
  26.     // ассоциативной трансляции
  27.     //
  28.     // ( *PointerPte = TempPte );
  29.     //
  30.    
  31.     PreviousPte.u.Flush = KeFlushSingleTb(
  32.                                     Wsle[WorkingSetIndex].u1.VirtualAddress,
  33.                                     TRUE,
  34.                                     (Wsle == MmSystemCacheWsle),
  35.                                     &PointerPte->u.Hard,
  36.                                     TempPte.u.Flush);
  37.     //
  38.     // Декремент счетчика использования. Если он стал равен нулю, страница переводится в переходное состояние
  39.     // и уменьшается на единицу счетчик ссылок.
  40.     //
  41.    
  42.     // MiDecrementShareCount()
  43.     Pfn->u2.ShareCount -= 1;
  44.    
  45.     if( Pfn->u2.ShareCount == 0 )
  46.     {
  47.         if( Pfn->u3.e1.PrototypePte == 1 )
  48.         {
  49.             // ... Дополнительная обработка прототипных PTE ...
  50.         }
  51.        
  52.         Pfn->u3.e1.PageLocation = TransitionPage;
  53.        
  54.         //
  55.         // Уменьшаем на 1 счетчик ссылок. Если он тоже стал равен нулю, перемещаем
  56.         //  страницу в список модифицированных или простаивающих страниц, либо полностью удаляем
  57.         // (помещая в список плохих страниц) в зависимости от MI_IS_PFN_DELETED() и RemovalRequested.
  58.         //
  59.        
  60.         // MiDecrementReferenceCount()
  61.         Pfn->u3.e2.ReferenceCount -= 1;
  62.        
  63.         if( Pfn->u3.e2.ReferenceCount == 0 )
  64.         {
  65.             if( MI_IS_PFN_DELETED(Pfn) )
  66.             {
  67.                 // PTE больше не ссылаются на эту страницу. Переместить ее в список свободных либо удалить, если нужно.
  68.                
  69.                 MiReleasePageFileSpace (Pfn->OriginalPte);
  70.                
  71.                 if( Pfn->u3.e1.RemovalRequested == 1 )
  72.                 {
  73.                     // Страница помечена к удалению. Перемещаем ее в список плохих страниц. Она не будет использована,
  74.                     // пока кто-либо не удалит ее из этого списка.
  75.                    
  76.                     MiInsertPageInList (MmPageLocationList[BadPageList],
  77.                                         PageFrameNumber);
  78.                 }
  79.                 else
  80.                 {
  81.                     // Помещаем страницу в список свободных
  82.                     MiInsertPageInList (MmPageLocationList[FreePageList],
  83.                                         PageFrameNumber);
  84.                 }
  85.                 return;
  86.             }
  87.            
  88.             if( Pfn->u3.e1.Modified == 1 )
  89.             {
  90.                 // Страница модифицирована. Помещаем в список модифицированных страниц,
  91.                 // поток записи модифицированных страниц запишет ее на диск.
  92.                 MiInsertPageList (MmPageLocationList[ModfifiedPageList], PageFrameIndex);
  93.             }
  94.             else
  95.             {
  96.                 if (Pfn->u3.e1.RemovalRequested == 1)
  97.                 {
  98.                     // Удалить страницу, но оставить ее состояние как простаивающее.
  99.                     Pfn->u3.e1.Location = StandbyPageList;
  100.                    
  101.                     MiRestoreTransitionPte (PageFrameIndex);
  102.                     MiInsertPageInList (MmPageLocationList[BadPageList],
  103.                                         PageFrameNumber);
  104.                     return;
  105.                 }
  106.                
  107.                 // Помещаем страницу в список простаивающих страниц.
  108.                 if (!MmFrontOfList) {
  109.                     MiInsertPageInList (MmPageLocationList[StandbyPageList],
  110.                                         PageFrameNumber);
  111.                 } else {
  112.                     MiInsertStandbyListAtFront (PageFrameNumber);
  113.                 }
  114.             }
  115.         }
  116.     }
  117. }

В приложении к статье есть программа с исходными кодами для демонстрации усечения рабочих наборов из пользовательского режима с помощью вызова SetProcessWorkingSetSize(hProcess, -1, -1).

Напротив, когда поток обращается к странице, которая была удалена из рабочего набора, происходит ошибка страницы. К страничным файлам относятся два типа PTE: Transition и Paged-Out. Если страница была удалена из рабочего набора, но еще не была записана на диск или ей вообще не нужно быть записанной на диск и она ЕЩЕ НАХОДИТСЯ в физической памяти (состояние Transition PTE), то вызывается MiResolveTransitionFault() и PTE просто переводится в состояние Valid с соответствующей корректировкой MMPFN и удалением страницы из списка простаивающих или модифицированных страниц. Если страница уже была записана на диск, либо ей не нужно было быть записанной на диск и ее уже использовали для каких-то других целей (состояние Paged-Out PTE), то вызывается MiResolvePageFileFault() и инициируется операция чтения страницы из файла подкачки со снятием соответствующего бита в битовой карте. Псевдокод разрешения Transition Fault (обрезанный код MiResolveTransitionFault):

Код (Text):
  1. if( Pfn->u4.InPageError )
  2. {
  3.     return Pfn->u1.ReadStatus;  // #PF на странице, чтение которой не удалось.
  4. }
  5. if (Pfn->u3.e1.ReadInProgress)
  6. {
  7.     // Повторная ошибка страницы. Если снова у того же потока,
  8.     // то возвращается STATUS_MULTIPLE_FAULT_VIOLATION;
  9.     // Если у другого - тогда ожидаем завершения чтения.
  10. }
  11. <p>
  12. MiUnlinkPageFromList (Pfn);
  13. Pfn->u3.e2.ReferenceCount += 1;
  14. Pfn->u2.ShareCount += 1;
  15. Pfn->u3.e1.PageLocation = ActiveAndValid;
  16. <p>
  17. MI_MAKE_TRANSITION_PTE_VALID (TempPte, PointerPte);
  18. MI_WRITE_VALID_PTR (PointerPte, TempPte);
  19. <p>
  20. MiAddPageToWorkingSet (...);
  21.  

Псевдокод загрузки страницы с диска (обрезанный код MiResolvePageFileFault):

Код (Text):
  1. TempPte = *PointerPte;
  2. <p>
  3. // Подготовить параметры для чтения
  4. PageFileNumber = TempPte.u.Soft.PageFileLow;
  5. StartingOffset.QuadPart = TempPte.u.Soft.PageFileHigh << PAGE_SHIFT;
  6. FilePointer = MmPagingFile[PageFileNumber]->File;
  7. <p>
  8. // Выбрать свободную страницу
  9. PageColor = (PFN_NUMBER)((MmSystemPageColor++) & MmSecondaryColorMask);
  10. PageFrameIndex = MiRemoveAnyPage( PageColor );
  11. <p>
  12. // build MDL...
  13. <p>
  14. // Скорректировать ее запись в базе данных страниц
  15. Pfn = MI_PFN_ELEMENT (PageFrameIndex);
  16. Pfn->u1.Event = &Event;
  17. Pfn->PteAddress = PointerPte;
  18. Pfn->OriginalPte = *PointerPte;
  19. Pfn->u3.e2.ReferenceCount += 1;
  20. Pfn->u2.ShareCount = 0;
  21. Pfn->u3.e1.ReadInProgress = 1;
  22. Pfn->u4.InPageError = 0;
  23. if( !MI_IS_PAGE_TABLE_ARRESS(PointerPte) )  Pfn->u3.e1.PrototypePte = 1;
  24. Pfn->u4.PteFrame = MiGetPteAddress(PointerPte)->PageFrameNumber;
  25. <p>
  26. // Временно перевести страницу в Transition состояние на время чтения
  27. MI_MAKE_TRANSITION_PTE ( TempPte, ... );
  28. MI_WRITE_INVALID_PTE (PointerPte, TempPte);
  29. <p>
  30. // Прочитать страницу.
  31. Status = IoPageRead (FilePointer,
  32.                      Mdl,
  33.                      StartingOffset,
  34.                      &Event,
  35.                      &IoStatus);
  36.                      
  37. if( Status == STATUS_SUCCESS )
  38. {
  39.     MI_MAKE_TRANSITION_PTE_VALID (TempPte, PointerPte);
  40.     MI_WRITE_VALID_PTE (PointerPte, TempPte);
  41.    
  42.     MiAddValidPageToWorkingSet (...);
  43. }
  44.  

Рабочие наборы Рабочий набор по определению - это совокупность резидентных страниц процесса (системы). Существуют три вида рабочих наборов:

  • Рабочий набор процесса содержит резидентные страницы, принадлежащие процессу - код, данные процесса и все последующие аллокации пользовательского режима. Хранится в EPROCESS::Vm
  • Рабочий набор системы содержит резидентные подкачиваемые страницы системы. В него входят страницы подкачиваемого кода, данных ядра и драйверов устройств, системного кеша и пула подкачиваемой памяти. Указатель на него хранится в переменной ядра MmSystemCacheWs.
  • Рабочий набор сеанса содержит резидентные страницы сеанса, например, графической подсистемы Windows (win32k.sys). Указатель хранится в MmSessionSpace->Vm.

Когда системе нужны свободные страницы, инициируется операция усечения рабочих наборов - страницы отправляются в списки Standby или Modified, в зависимости от того, была ли запись в них, а PTE переводятся в состояние Transition. Когда страницы окончательно отбираются, то PTE переводятся в состояние Paged-Out (если это были страницы, выгружаемые в файл подкачки) или в Invalid, если это были страницы проецируемого файла. Когда процесс обращается к странице, то страница либо удаляется из списков Standy/Modified и становится ActiveAndValid, либо инициируется операция загрузки страницы с диска, если она была окончательно выгружена. Если памяти достаточно, процессу позволяется расширить свой рабочий набор и даже превысить максимум для загрузки страницы, иначе для загрузки страницы выгружается какая-то другая, то есть новая страница замещает старую. Имеется системный поток управления рабочими наборами или т.н. диспетчер баланса. Он ожидает на двух объектах KEVENT, первое из которых срабатывает по таймеру раз в секунду, а второе срабатывает, когда нужно изменить рабочие наборы. Диспетчер настройки баланса так же проверяет ассоциативные списки, регулируя из глубину для оптимальной производительности.

V. Ядерные функции управления памятью В этой части речь пойдет о некоторых полезных функциях управления памятью в режиме ядра. Слои функции управления памятью ядра можно разделить следующим образом от низшего уровня к высшему:

  • макросы MI_WRITE_VALID_PTE/MI_WRITE_INVALID_PTE
  • низкоуровневые функции MiResolve..Fault, MiDeletePte и другие функции работы с PDE/PTE, а так же функции работы с MMPFN и списками страниц - MiRemovePageByColor, MiRemoveAnyPage, MiRemoveZeroPage.
  • функции, предоставляемые драйверам для работы с физической памятью: MmAllocatePagesForMdl, MmFreePagesFromMdl, MmAllocateContiguousMemory.
  • функции, предоставляемые драйверам для работы с пулом: ExAllocatePoolWith..., ExFreePoolWith..., MmAllocateContiguousMemory (относится и к предыдущему слою и к этому)

Для пользовательской памяти дело обстоит немного по-другому:

  • макросы MI_WRITE_VALID_PTE/MI_WRITE_INVALID_PTE
  • функции работы с VAD и пользовательской памятью - MiAllocateVad, MiCheckForConflictingVad, и др.
  • функции работы с виртуальной памятью - NtAllocateVirtualMemory, NtFreeVirtualMemory, NtProtectVirtualMemory.

Описывать начнем их от низшего уровня к высшему, сначала для управления памятью ядра, затем пользовательской памятью.

Управление памятью режима ядра

1. макросы MI_WRITE_VALID_PTE/MI_WRITE_INVALID_PTE Эти макросы используются во всех функциях, которые как-либо затрагивают выделение или освобождение физической (в конечном итоге) памяти. Соответственно, они записывают действительный и недействительный PTE в таблицу страниц процесса.

2. Низкоуровневые функции для работы с PDE/PTE и списками физических страниц Все это, вообщем то, я уже описывал, когда рассказывал про списки страниц, MMPFN и другое, поэтому приведу лишь прототипы функций с кратким описанием их действий: PFN_NUMBER FASTCALL MiRemoveAnyPage(IN ULONG PageColor) ; // Выделяет физическую страницу заданного цвета (SecondaryColor) из списков свободных, обнуленных или простаивающих страниц. PFN_NUMBER FASTCALL MiRemoveZeroPage(IN ULONG PageColor) ; // Выделяет физическую страницу заданного цвета (SecondaryColor) из списка свободных страниц. VOID MiRemovePageByColor (IN PFN_NUMBER Page, IN ULONG Color); // Выделяет указанную страницу, удаляя ее из списка свободных страниц указанного цвета.

3. Функции, предоставляемые драйверам для работы с физической памятью PMDL MmAllocatePagesForMdl( IN PHYSICAL_ADDRESS LowAddress, IN PHYSICAL_ADDRESS HighAddress, IN PHYSICAL_ADDRESS SkipBytes, IN SIZE_T TotalBytes ); Эта функция выделяет физические страницы (не обязательно идущие подряд, как это делает, например, MmAllocateContiguousMemory), пробуя выделить страницы общим размером TotalBytes, начиная с физического адреса LowAddress и заканчивая HighAddress, "перешагивая" по SkipBytes. Просматриваются списки обнуленных, затем свободных страниц. Разумеется, страницы неподкачиваемые. Если страниц не хватает, функция старается выделить столько страниц, сколько возможно. Возвращаемое значение - Memory Descriptor List (MDL), описывающий выделенные страницы. Они должны быть освобождены соответствующим вызовом MmFreePagesFromMdl и ExFreePool для структуры MDL. Страницы НЕ спроецированы ни на какие виртуальные адреса, об этом должен позаботиться программист с помощью вызова MmMapLockedPages.

PVOID MmAllocateContiguousMemory( IN ULONG NumberOfBytes, IN PHYSICAL_ADDRESS HighestAcceptableAddress ); Функция выделяет физически непрерывную область физических страниц общим размером NumberOfBytes, не выше HighestAcceptableAddress, так же проецируя их в адресное пространство ядра. Сначала она пытается выделить страницы из неподкачиваемого пула, если его не хватает, она начинает просматривать списки свободных и обнуленных страниц, если и их не хватает, то она просматривает страницы из списка простаивающих страниц. Возвращает базовый адрес выделенного участка памяти. Память должна быть освобождена с помощью вызов MmFreeContiguousMemory.

4. функции, предоставляемые драйверам для работы с пулом Они подробно описаны в статье Four-F, поэтому останавливаться на этом я не буду.

Дополнительные функции управления памятью режима ядра Из функций контроля ядерной памяти следует, наверное, упомянуть про MmIsAddressValid и MmIsNonPagedSystemAddressValid. Функция MmIsAddressValid проверяет страницу памяти на то, возникнет ли ошибка страницы при доступе к этому адресу. То есть, другими словами, она проверяет тот факт, что страница уже сейчас находится в физической памяти. Следует отметить, что состояние transition,paged-out,prototype ее не волнуют, поэтому она может использоваться лишь для контроля адресов при высоких IRQL (>=DPC/Dispatch), поскольку при этих IRQL не разрешены ошибки страниц (а если встретится ошибка страницы, будет выброшен синий экран IRQL_NOT_LESS_OR_EQUAL). Если нужно проверять ядерные адреса на доступ при низком IRQL, то, насколько мне известно, нет документированных способов это сделать. Видимо, считается, что драйвер должен знать, какие у него адреса правильные, а какие нет и не пробовать обращаться по неправильным адресам. В приложении к статье имеется написанная мной функция MmIsAddressValidEx, которая проверяет адрес на корректность доступа при низком IRQL, учитывая, что PTE может находиться в недействительном состоянии, но ошибка страницы не вызовет синего экрана или исключения (в программном смысле). С учетом рассказанных мною структур недействительных PTE, разобраться в ее исходном коде будет нетрудно. Функция MmIsNonPagedSystemAddressValid, почему-то незаслуженно "выброшенная" разработчиками Windows и обозначенная как obsolete, на самом деле тоже полезна. Она на порядок проще, чем MmIsAddressValid (которую, кстати, и рекомендует использовать Microsoft), и всего лишь проверяет то, что переданный ей адрес принадлежит подкачиваемой или неподкачиваемой областям памяти ядра. Адрес не проверяется на корректность, но результат функции вовсе не эквивалентен MmIsAddressValid (в том смысле, что память может быть в пуле подкачиваемой памяти, но может быть как выгружена на диск так и загружена, поэтому возвращенное значение FALSE еще ничего не говорит о том, можно ли обращаться к этой памяти), поэтому я совершенно не понимаю, почему Microsoft сочли ее "obsolete" и не рекомендуют использовать, подсовывая взамен MmIsAddressValid. Использовать MmIsNonPagedSystemAddressValid мы будем, например, в функции вывода MMPFN в приложении, когда потребуется определить, принадлежит ли адрес подкачиваемому пулу (поля MMPFN, как Вы помните, различаются для подкачиваемого и неподкачиваемого пулов).

Управление пользовательской памятью Для начала стоит заметить, что для управления пользовательской памятью используется дополнительный механизм - Virtual Address Descriptors (VAD), которые описывают проекции секций, а так же выделения памяти через NtAllocateVirtualMemory (VirtualAlloc в Win32 API). Представлены эти VAD в виде дерева, указатель на вершину содержится в поле EPROCESS->VadRoot. Секции можно создавать и проецировать на пользовательские адреса с помощью NtCreateSection, NtMapViewOfSection (Win32API-аналоги у них: CreateFileMapping, MapViewOfFileEx). Адреса памяти могут резервироваться (reserve) и в последствии память по этим адреса может передаваться (commit) процессу. Этим заведует NtAllocateVirtualMemory.

Функции работы с VAD и пользовательской памятью Элемент VAD представлен следующей структурой:

Код (Text):
  1. typedef struct _MMVAD_FLAGS {
  2.     ULONG_PTR CommitCharge : COMMIT_SIZE;
  3.     ULONG_PTR PhysicalMapping : 1;
  4.     ULONG_PTR ImageMap : 1;
  5.     ULONG_PTR UserPhysicalPages : 1;
  6.     ULONG_PTR NoChange : 1;
  7.     ULONG_PTR WriteWatch : 1;
  8.     ULONG_PTR Protection : 5;
  9.     ULONG_PTR LargePages : 1;
  10.     ULONG_PTR MemCommit: 1;
  11.     ULONG_PTR PrivateMemory : 1;
  12. } MMVAD_FLAGS;
  13. <p>
  14. typedef struct _MMVAD_SHORT {
  15.     ULONG_PTR StartingVpn;
  16.     ULONG_PTR EndingVpn;
  17.     struct _MMVAD *Parent;
  18.     struct _MMVAD *LeftChild;
  19.     struct _MMVAD *RightChild;
  20.     union {
  21.         ULONG_PTR LongFlags;
  22.         MMVAD_FLAGS VadFlags;
  23.     } u;
  24. } MMVAD_SHORT, *PMMVAD_SHORT;

Так же есть структура MMVAD, аналогичная MMVAD_SHORT, но содержащая больше полей и используемая для проецированных файлов (дополнительные поля это PCONTROL_AREA, необходимая для поддержания спроецированных файлов и содержащая такие важные указатели, как PFILE_OBJECT и др; о проекциях файлов, как-нибудь в следующий раз: и так уже 50 килобайт вышло =\), а MMVAD_SHORT используется для пользовательских выделений памяти. Чтобы отличить, какой именно VAD представлен указателем, используется флаг u.VadFlags.PrivateMemory: если он установлен, то это "частная память", то есть обычное выделение памяти. Если сброшен - проекция файла. Поля StartingVpn и EndingVpn, соответственно, обозначают начальную и конечную виртуальную страницу (Virtual Page Number) описываемой области (для конвертирования виртуального адреса в номер страницы используется MI_VA_TO_VPN, который просто сдвигает виртуальный адрес на PAGE_SHIFT бит вправо). Поля Parent, LeftChild, RightChild используются для связи дескрипторов виртуальных адресов в дерево. u.VadFlags содержит некоторые полезные флаги, а именно:

  • CommitCharge. Это поле содержит количество фактически выделенных и переданных страниц процессу, если VAD описывает переданную память, или 0 если описывается зарезервированная память.
  • PhysicalMapping. Этот флаг показывает, что память на самом деле является проекцией физических страниц, созданной с помощью MmMapLockedPages при AccessMode == UserMode.
  • Флаг ImageMap показывает, что VAD описывает загруженный исполняемый модуль (с помощью LoadLibrary и других, в конечном итоге сводящихся к NtCreateSection с SEC_IMAGE).
  • UserPhysicalPages устанавливается при вызове NtAllocateVirtualMemory с MEM_PHYSICAL|MEM_RESERVE, используемом для выделения окна для физических страниц при испольщовании AWE (Address Windowing Extensions).
  • NoChange установлен, когда запрещено менять атрибуты доступа области, описываемой этим VAD. Чтобы создать такую область, используется флаг SEC_NO_CHANGE у NtCreateSection.
  • WriteWatch устанавливается при выделении памяти с флагом MEM_WRITE_WATCH, при этом создается битовая карта страниц, где впоследствии отмечается, на какие страницы была произведена запись. Эту информацию можно получить в последствии через Win32 API GetWriteWatch() и сбросить карту через ResetWriteWatch()
  • Protection - изначальные атрибуты доступа к памяти.
  • LargePages содержит 1 при использовании больших страниц через MEM_LARGE_PAGES. В Windows XP/2000 не поддерживается.
  • MemCommit содержит 1 если память была передана процессу.
  • PrivateMemory, как уже было сказано, отличает MMVAD от MMVAD_SHORT.

Функция MiAllocateVad выделяет VAD для процесса, резервируя переданные ей адреса, а функция MiCheckForConflictingVad (на самом деле макрос, раскрывающийся в вызов функции MiCheckForConflictingNode) проверяет, существуют ли VAD у процесса такие, что описываемая ими область памяти перекрывается с указанными виртуальными адресами. Если это так, возвращается VAD первой конфликтной области, иначе NULL. Функция используется при передаче памяти процессу в NtAllocateVirtualMemory для поиска VAD, соответствующего указанному адресу. Функция MiInsertVad добавляет VAD в дерево виртуальных дескрипторов адресов процесса и реорганизует его, если это нужно. Функция MiRemoveVad, соответственно, удаляет и освобождает VAD. Перейдем теперь к функциям, доступным пользовательскому коду и драйверам устройств для управления памятью.

Функция NtAllocateVirtualMemory производит следующие действия: 1) для резервирования адресов вызывается MiCheckForConflictingVad для проверки, не была ли эта область или какая-либо ее часть зарезервированы или использованы другой функцией для работы с памятью (например, проецированием секции) ранее. Если так - возвращает STATUS_CONFLICTING_ADDRESSES. Далее выделяется VAD функцией MiAllocateVad, заполняются соответствующие поля и VAD добавляется в дерево с помощью MiInsertVad. Если он описывает AWE-вид или включен WriteWatch, тогда еще вызывается MiPhysicalViewInserter. 2) для передачи адресов вызывается MiCheckForConflictingVad, но уже с целью найти соответствующий VAD, созданный при резервировании. Потом соответствующие страницы выставляются в таблице страниц как обнуляемые по требованию, а так же меняются атрибуты защиты, если это необходимо. NtFreeVirtualMemory производит обратные действия.

На этом я думаю, наконец-то (!), что статью можно завершить.

В приложении к статье можно найти:

  1. программу Working Sets для демонстрации усечения рабочих наборов.
  2. Функции ручной загрузки и выгрузки страницы в файл подкачки. Примечание: очень сырые! Поскольку страница не добавляется в рабочий набор и не удаляется из него, может быть синий экран MEMORY_MANAGEMENT или PFN_LIST_CORRUPT (для выгрузки и загрузки соответственно), поэтому экспериментировать на реальной системе я не советую. Лучше запускать только изучающий и анализирующий код, который не изменяет никаких параметров системы. Это функции MiPageOut и MiLoadBack (префиксы Mi я сам добавил для красоты :smile3:)
  3. Функция вывода в DbgPrint содержимого MMPFN. Это MiPrintPfnForPage.
  4. Функция MmIsAddressValidEx для расширенной проверки доступа к адресам при низком IRQL. Возвращает статус проверки - элемент перечисления

    Код (Text):
    1. enum VALIDITY_CHECK_STATUS {
    2.     VCS_INVALID = 0,     //  = 0 (FALSE)
    3.     VCS_VALID,           //-|
    4.     VCS_TRANSITION,      // |
    5.     VCS_PAGEDOUT,        // |-   > 0
    6.     VCS_DEMANDZERO,      // |
    7.     VCS_PROTOTYPE,       //-|
    8. };

    Так же может трактоваться как BOOLEAN, поскольку статус невалидной страницы 0, а все остальные больше нуля.

  5. Комплексный пример драйвера, демонстрирующий все эти функции (ручная загрузка и выгрузка закомментированы).

© Great


0 2.562
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532