Источник: http://wasm.ru/print.php?article=mem_management

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

Для начала нечто вроде вступления. Статья рассчитана на тех, кто уже работал с памятью в режиме ядра и отличает 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 соответственно):

typedef struct _MMPTE_HARDWARE {
    ULONG Valid : 1;
    ULONG Write : 1;
    ULONG Owner : 1;
    ULONG WriteThrough : 1;
    ULONG CacheDisable : 1;
    ULONG Accessed : 1;
    ULONG Dirty : 1;
    ULONG LargePage : 1;
    ULONG Global : 1;
    ULONG CopyOnWrite : 1; // software field
    ULONG Prototype : 1;   // software field
    ULONG reserved : 1;    // software field
    ULONG PageFrameNumber : 20;
} MMPTE_HARDWARE, *PMMPTE_HARDWARE;

typedef struct _MMPTE_HARDWARE_PAE { ULONGLONG Valid : 1; ULONGLONG Write : 1; ULONGLONG Owner : 1; ULONGLONG WriteThrough : 1; ULONGLONG CacheDisable : 1; ULONGLONG Accessed : 1; ULONGLONG Dirty : 1; ULONGLONG LargePage : 1; ULONGLONG Global : 1; ULONGLONG CopyOnWrite : 1; // software field ULONGLONG Prototype : 1; // software field ULONGLONG reserved0 : 1; // software field ULONGLONG PageFrameNumber : 24; ULONGLONG reserved1 : 28; // software field } 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.

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

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

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

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

typedef struct _PHYSICAL_MEMORY_RUN {
    PFN_NUMBER BasePage;
    PFN_NUMBER PageCount;
} PHYSICAL_MEMORY_RUN, *PPHYSICAL_MEMORY_RUN;

typedef struct _PHYSICAL_MEMORY_DESCRIPTOR { ULONG NumberOfRuns; PFN_NUMBER NumberOfPages; PHYSICAL_MEMORY_RUN Run[1]; } PHYSICAL_MEMORY_DESCRIPTOR, *PPHYSICAL_MEMORY_DESCRIPTOR;

PPHYSICAL_MEMORY_DESCRIPTOR MmPhysicalMemoryDescriptor;

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

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

Состояние страниц, не являющиеся списками:

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

PMMPFNLIST MmPageLocationList[8] =
 {
  &MmZeroedPageListHead,
  &MmFreePageListHead,
  &MmStandbyPageListHead,
  &MmModifiedPageListHead,
  &MmModifiedNoWritePageListHead,
  &MmBadPageListHead,
  NULL,
  NULL };

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

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

typedef struct _MMPFN {
    union {
        PFN_NUMBER Flink;             // Used if (u3.e1.PageLocation < ActiveAndValid)
        WSLE_NUMBER WsIndex;          // Used if (u3.e1.PageLocation == ActiveAndValid)
        PKEVENT Event;                // Used if (u3.e1.PageLocation == TransitionPage)
		NTSTATUS ReadStatus;          // Used if (u4.InPageError == 1)
    } u1;
    PMMPTE PteAddress;
    union {
        PFN_NUMBER Blink;             // Used if (u3.e1.PageLocation < ActiveAndValid)
        ULONG ShareCount;             // Used if (u3.e1.PageLocation >= ActiveAndValid)
        ULONG SecondaryColorFlink;    // Used if (u3.e1.PageLocation == FreePageList or == ZeroedPageList)
    } u2;
    union {
	    struct _MMPFNENTRY {
		    ULONG Modified : 1;
		    ULONG ReadInProgress : 1;
		    ULONG WriteInProgress : 1;
		    ULONG PrototypePte: 1;
		    ULONG PageColor : 3;
		    ULONG ParityError : 1;
		    ULONG PageLocation : 3;
		    ULONG RemovalRequested : 1;
			ULONG CacheAttribute : 2;
		    ULONG Rom : 1;
			ULONG LockCharged : 1;
		    ULONG ReferenceCount : 16;
        } e1;
        struct {
            USHORT ShortFlags;
            USHORT ReferenceCount;
        } e2;
    } u3;
    MMPTE OriginalPte;
    union {
		ULONG EntireFrame;
		struct {
			ULONG PteFrame : 26;
			ULONG InPageError : 1;
			ULONG VerifierAllocation : 1;
			ULONG AweAllocation : 1;
			ULONG LockCharged : 1;
			ULONG KernelStack : 1;
			ULONG Reserved : 1;
		};
	} u4;
} 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 содержит еще и следующие дополнительные флаги:

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

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

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

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

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

typedef struct _MMPAGING_FILE {
    PFN_NUMBER Size;
    PFN_NUMBER MaximumSize;
    PFN_NUMBER MinimumSize;
    PFN_NUMBER FreeSpace;
    PFN_NUMBER CurrentUsage;
    PFN_NUMBER PeakUsage;
    PFN_NUMBER Hint;
    PFN_NUMBER HighestPage;
    PVOID Entry[MM_PAGING_FILE_MDLS];
    PRTL_BITMAP Bitmap;
    PFILE_OBJECT File;
    UNICODE_STRING PageFileName;
    ULONG PageFileNumber;
    BOOLEAN Extended;
    BOOLEAN HintSetToZero;
	BOOLEAN BootPartition;
	HANDLE FileHandle;

} MMPAGING_FILE, *PMMPAGING_FILE;

В приложении к статье есть откомментированный пример с выводом полей структуры 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 и вызываемых из нее функций):

TempPte = *PointerPte;
PageFrameNumber = PointerPte->u.Hard.PageFrameNumber;

if( Pfn->u3.e1.PrototypePte == 0)
{
	//
	// Приватная страница, сделать переходной.
	//
	
	MI_ZERO_WSINDEX (Pfn);  // Pfn->u1.WsIndex = 0;
	
	//
	// Следующий макрос делает это:
	//
	// TempPte.u.Soft.Valid = 0;
	// TempPte.u.Soft.Transition = 1;
	// TempPte.u.Soft.Prototype = 0;
	// TempPte.u.Trans.Protection = PROTECT;
	//
	
	MI_MAKE_VALID_PTE_TRANSITION (TempPte,
								  Pfn->OriginalPte.u.Soft.Protection);
								  
	//
	// Этот вызов на самом деле заменяет текущий PTE на TempPte и очищает буфера
	// ассоциативной трансляции
	//
	// ( *PointerPte = TempPte );
	//
	
	PreviousPte.u.Flush = KeFlushSingleTb(
									Wsle[WorkingSetIndex].u1.VirtualAddress,
									TRUE,
									(Wsle == MmSystemCacheWsle),
									&PointerPte->u.Hard,
									TempPte.u.Flush);
	//
	// Декремент счетчика использования. Если он стал равен нулю, страница переводится в переходное состояние
	// и уменьшается на единицу счетчик ссылок.
	//
	
	// MiDecrementShareCount()
	Pfn->u2.ShareCount -= 1;
	
	if( Pfn->u2.ShareCount == 0 )
	{
		if( Pfn->u3.e1.PrototypePte == 1 )
		{
			// ... Дополнительная обработка прототипных PTE ...
		}
		
		Pfn->u3.e1.PageLocation = TransitionPage;
		
		//
		// Уменьшаем на 1 счетчик ссылок. Если он тоже стал равен нулю, перемещаем
		//  страницу в список модифицированных или простаивающих страниц, либо полностью удаляем
		// (помещая в список плохих страниц) в зависимости от MI_IS_PFN_DELETED() и RemovalRequested.
		//
		
		// MiDecrementReferenceCount()
		Pfn->u3.e2.ReferenceCount -= 1;
		
		if( Pfn->u3.e2.ReferenceCount == 0 )
		{
			if( MI_IS_PFN_DELETED(Pfn) )
			{
				// PTE больше не ссылаются на эту страницу. Переместить ее в список свободных либо удалить, если нужно.
				
				MiReleasePageFileSpace (Pfn->OriginalPte);
				
				if( Pfn->u3.e1.RemovalRequested == 1 )
				{
					// Страница помечена к удалению. Перемещаем ее в список плохих страниц. Она не будет использована,
					// пока кто-либо не удалит ее из этого списка.
					
					MiInsertPageInList (MmPageLocationList[BadPageList],
										PageFrameNumber);
				}
				else
				{
					// Помещаем страницу в список свободных
					MiInsertPageInList (MmPageLocationList[FreePageList],
										PageFrameNumber);
				}
				return;
			}
			
			if( Pfn->u3.e1.Modified == 1 )
			{
				// Страница модифицирована. Помещаем в список модифицированных страниц,
				// поток записи модифицированных страниц запишет ее на диск.
				MiInsertPageList (MmPageLocationList[ModfifiedPageList], PageFrameIndex);
			}
			else
			{
				if (Pfn->u3.e1.RemovalRequested == 1)
				{
					// Удалить страницу, но оставить ее состояние как простаивающее.
					Pfn->u3.e1.Location = StandbyPageList;
					
					MiRestoreTransitionPte (PageFrameIndex);
					MiInsertPageInList (MmPageLocationList[BadPageList],
										PageFrameNumber);
					return;
				}
				
				// Помещаем страницу в список простаивающих страниц.
				if (!MmFrontOfList) {
					MiInsertPageInList (MmPageLocationList[StandbyPageList],
										PageFrameNumber);
				} else {
					MiInsertStandbyListAtFront (PageFrameNumber);
				}
			}
		}
	}
}

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

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

if( Pfn->u4.InPageError )
{
	return Pfn->u1.ReadStatus;  // #PF на странице, чтение которой не удалось.
}
if (Pfn->u3.e1.ReadInProgress)
{
	// Повторная ошибка страницы. Если снова у того же потока,
	// то возвращается STATUS_MULTIPLE_FAULT_VIOLATION;
	// Если у другого - тогда ожидаем завершения чтения.
}

MiUnlinkPageFromList (Pfn); Pfn->u3.e2.ReferenceCount += 1; Pfn->u2.ShareCount += 1; Pfn->u3.e1.PageLocation = ActiveAndValid;

MI_MAKE_TRANSITION_PTE_VALID (TempPte, PointerPte); MI_WRITE_VALID_PTR (PointerPte, TempPte);

MiAddPageToWorkingSet (...);

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

TempPte = *PointerPte;

// Подготовить параметры для чтения PageFileNumber = TempPte.u.Soft.PageFileLow; StartingOffset.QuadPart = TempPte.u.Soft.PageFileHigh << PAGE_SHIFT; FilePointer = MmPagingFile[PageFileNumber]->File;

// Выбрать свободную страницу PageColor = (PFN_NUMBER)((MmSystemPageColor++) & MmSecondaryColorMask); PageFrameIndex = MiRemoveAnyPage( PageColor );

// build MDL...

// Скорректировать ее запись в базе данных страниц Pfn = MI_PFN_ELEMENT (PageFrameIndex); Pfn->u1.Event = &Event; Pfn->PteAddress = PointerPte; Pfn->OriginalPte = *PointerPte; Pfn->u3.e2.ReferenceCount += 1; Pfn->u2.ShareCount = 0; Pfn->u3.e1.ReadInProgress = 1; Pfn->u4.InPageError = 0; if( !MI_IS_PAGE_TABLE_ARRESS(PointerPte) ) Pfn->u3.e1.PrototypePte = 1; Pfn->u4.PteFrame = MiGetPteAddress(PointerPte)->PageFrameNumber;

// Временно перевести страницу в Transition состояние на время чтения MI_MAKE_TRANSITION_PTE ( TempPte, ... ); MI_WRITE_INVALID_PTE (PointerPte, TempPte);

// Прочитать страницу. Status = IoPageRead (FilePointer, Mdl, StartingOffset, &Event, &IoStatus); if( Status == STATUS_SUCCESS ) { MI_MAKE_TRANSITION_PTE_VALID (TempPte, PointerPte); MI_WRITE_VALID_PTE (PointerPte, TempPte); MiAddValidPageToWorkingSet (...); }

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

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

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

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

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

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

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 представлен следующей структурой:

typedef struct _MMVAD_FLAGS {
    ULONG_PTR CommitCharge : COMMIT_SIZE;
    ULONG_PTR PhysicalMapping : 1;
    ULONG_PTR ImageMap : 1;
    ULONG_PTR UserPhysicalPages : 1;
    ULONG_PTR NoChange : 1;
    ULONG_PTR WriteWatch : 1;
    ULONG_PTR Protection : 5;
    ULONG_PTR LargePages : 1;
    ULONG_PTR MemCommit: 1;
    ULONG_PTR PrivateMemory : 1;
} MMVAD_FLAGS;

typedef struct _MMVAD_SHORT { ULONG_PTR StartingVpn; ULONG_PTR EndingVpn; struct _MMVAD *Parent; struct _MMVAD *LeftChild; struct _MMVAD *RightChild; union { ULONG_PTR LongFlags; MMVAD_FLAGS VadFlags; } u; } 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 содержит некоторые полезные флаги, а именно:

Функция 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 я сам добавил для красоты :))
  3. Функция вывода в DbgPrint содержимого MMPFN. Это MiPrintPfnForPage.
  4. Функция MmIsAddressValidEx для расширенной проверки доступа к адресам при низком IRQL. Возвращает статус проверки - элемент перечисления

    enum VALIDITY_CHECK_STATUS {
    	VCS_INVALID = 0,     //  = 0 (FALSE)
    	VCS_VALID,           //-|
    	VCS_TRANSITION,      // |
    	VCS_PAGEDOUT,        // |-   > 0
    	VCS_DEMANDZERO,      // |
    	VCS_PROTOTYPE,       //-|
    };

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

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

  [C] Great

© 2002-2007 wasm.ru - all rights reserved and reversed