ГЛАВА 14 Исследование виртуальной памяти

В предыдущей главе мы выяснили, как система управляет виртуальной памятью, как

процесс получает свое адресное пространство и что оно собой представляет А сей час мы перейдем от теории к практике и рассмотрим некоторые Windows-функции, сообщающие о состоянии системной памяти и виртуального адресного пространства в том или ином процессе

Системная информация

Многие параметры операционной системы (размер страницы, гранулярность выде ления памяти и др) зависят от используемого в компьютере процессора Поэтому нельзя жестко «зашивать» их значения в исходный код пpoгpaмм Эту информацию надо считывать в момент инициализации процесса с помощью функции GetSystemfnfo

VOID GetSystemInfo(LPSYSTEM_INFO psinf);

Вы должны передать в GetSystemInfo адрес структуры SYSTEM_INFO, и функция инициализирует элементы этой структуры

typedef struct _SYSTEM_INFO
{

union
{

DWORD dwOemIdж
// не используйте этот элемент он устарел

struct
{

WORD wProcessorArchitecture;
WORD wRescrved;

};

};

DWORD dwPageSize;
LPVOID lpMinimurnApplirationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRpvi4inn;

} SYSTEM INFO *LPSYSlEM_INFO;

При загрузке система определяет значения элементов этой структуры, для кон

кретной системы их значения постоянны Функция GetSystemInfo предусмотрена спе циально для того, чтобы и приложения могли получать эту информацию Из всех элементов структуры лишь четыре имеют отношение к памяти Они описаны в сле дующей таблице.

Элемент

Описание

dwPageStze

Размер страницы памяти. На процессорах x86 это значение ра но 4096, а на процессорах Alpha — 8192 байтам.

IpMinimumApplicationAddress

Минимальный адрес памяти доступного адресного пространен для каждого процесса. В Windows 98 это значение равно 4 194 304, или 0x00400000, поскольку нижние 4 Мб адресного пространства каждого процесса недоступны. В Windows 2000 это значение равно 65 536, или 0x00010000, так как в этой сие теме резервируются лишь первые 64 Кб адресного пространст каждого процесса

lpMaximwnApplicationAddress

Максимальный адрес памяти доступного адресного простран ства, отведенного в "личное пользование" каждому процессу. В Windows 98 этот адрес равен 2 147 483 647, или 0x7FFFFFFF, так как верхние 2 Гб занимают общие файлы, проецируемые в память, и разделяемый код операционной системы. В Windows 2000 этот адрес соответствует началу раздела для кода и данных режима ядра за вычетом 64 Кб.

dwAllocationGranularity

Гранулярность резервирования регионов адресного простран ства На момент написания книги это значение составляет 64 для всех платформ Windows

Остальные элементы этой структуры показаны в таблице ниже.

Элемент

Описание

dwOemld

Устарел, больше не используется

wReserved

Зарезервирован на будущее; пока не используется

dwNumberOfProcessors

Число процессоров в компьютере

dwActiveProcessorMask

Битовая маска, которая сообщает, какие процессоры активны (выполняют потоки)

dwProcessorType

Используется только в Windows 98; сообщает тип процессора, например Intel 386, 486 или Pentium

wProcessorArchitecture

Используется только в Windows 2000; сообщает тип архитектуры процессора, например Intel, Alpha, 64-разрядный Intel или 64-разрядный Alpha

wProcessorLevel

Используется только в Windows 2000; сообщает дополнительные подробности об архитектуре процессора, например Intel Pentium Pro или Pentium II

wProcessorRevision

Используется только в Windows 2000; сообщает дополнительные подробности об уровне данной архитектуры процессора

Программа-пример Syslnfo

Эта программа, «14 SysInfo.exe» (см. листинг на рис. 14-1), весьма проста; она вызыва ет функцию GetSystemInfo и выводит на экран информацию, возвращенную в струк туре SYSTEM_INFO. Файлы исходного кода и ресурсов этой программы находятся в каталоге 14-SysInfo на компакт-диске, прилагаемом к книге Диалоговые окна с ре зультатами выполнения программы SysInfo на разных процессорных платформах показаны ниже.

rihter14-1.jpg

Windows 98 на процессоре x86 32-разрядная Windows 2000 на процессоре x86

rihter14-2.jpg

32-разрядная Windows2000 64 разрядная Windows 2000 на процессореА1рhа на процессоре Alpha

Syslnfo

 

Статус виртуальной памяти

Windows-функция GlobalMemoryStatus позволяет отслеживать текущее состояние па мяти

VOID GlobalMemoryStatus(LPMEMORYSTATUS pmst);

На мой взгляд, она названа крайне неудачно, имя GlobalMemorySlatus подразуме вает, что функция каким-то образом связана с глобальными кучами в 16-разрядной Windows Мне кажется, что лучше было бы назвать функцию GlobalMemoryStatus по другому — скажем, VirtualMemoryStatus.

При вызове функции GlobaUdemoryStatus Вы должны передать адрес структуры MEMORYSTATUS. Вот эта структура:

typedef struct _MEMORYSTATUS
{

DWORD dwLength;
DWORD dwMemoryLoad;
SIZE_T dwTotalPhys;
SIZE_T dwAvailPhys;
SIZE_T dwTotalPageFile;
SIZE_T dwAvailPageFile;
SIZE_T dwTotalVirtual;
SIZE_T dwAvailVirtual;

} MEMORYSTATUS, *LPMEMORYSTATUS;

Перед вызовом GlobalMemoryStatus надо записать в элемент dwLength размер струк туры в байтах. Такой принцип вызова функции дает возможность Microsoft расширять эту структуру в будущих версиях Windows, не нарушая работу существующих прило жений После вызова GlobalMemoryStatus инициализирует остальные элементы струк туры и возвращает управление. Назначение элементов этой структуры Вы узнаете из следующего раздела, в котором рассматривается программа-пример VMStat.

Если Вы полагаете, что Ваше приложение будет работать на машинах с объемом оперативной памяти более 4 Гб или файлом подкачки более 4 Гб, используйте новую функцию GlobalMemoryStatusEx:

BOOL GlobalHemoryStatusEx(LPMEHORYSTATUSEX pmst);

Вы должны передать ей адрес новой структуры MEMORYSTATUSEX:

typedef struct _MEMORYSTATUSEX
{

DWORD dwLength;
DWORD dwMemoryLoad;
DWORDLONG ullTotalPhys;
DWORDLONG ullAvailPhys;
DWORDLONG ullTotalPageFile;
DWORDLONG ullAvaiIPageFile;
DWORDLONG ullTotalVirtual;
DWORDLONfi uUAvailVirtual;
DWOHDLONG ullAvailExtendedVirtual;

} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;

Эта структура идентична первоначальной структуре MEMORYSTATUS с одним ис ключением все ее элементы имеют размер по 64 бита, что позволяет оперировать со значениями, превышающими 4 Гб. Последний элемент, ullAvailExtendedVirtual, указы вает размер незарезервированной памяти в самой большой области памячи виртуаль ного адресного пространства вызывающего процесса. Этот элемент имеет смысл толь ко для процессоров определенных архитектур при определенных конфигурациях,

Программа-пример VMStat

Эта программа, «14 VMStat.exe» (см. листинг на рис. 14-2), выводит на экран окно с результатами вызова GlobalMemoryStatus Информация в окне обновляется каждую

секунду, так что VMStat вполне пригодна для мониторинга памяти в системе. Файлы

исходного кода и ресурсов этой программы находятся в каталоге 14-VMStat на ком пакт-диске, прилагаемом к книге. Окно этой программы после запуска в Windows 2000 на машине с процессором Intel Pentium II и 128 Мб оперативной памяти показано ниже.

rihter14-3.jpg

Элемент dwMemoryLoad (показываемый как Memory Load) позволяет оценить, на сколько занята подсистема управления памятью. Это число может быть любым в ди апазоне от 0 до 100 В Windows 98 и Windows 2000 алгоритмы, используемые для его подсчета, различны. Кроме того, в будущих версиях операционных систем этот алго ритм почти наверняка придется модифицировать. Но, честно говоря, на практике от значения этого элемента толку немного

Элемент dwTotalPhys (показываемый как TotalPhys) отражает общий объем физи ческой (оперативной) памяти в байтах. На данной машине с Pentium II и 128 Мб опе ративной памяти его значение составляет 133 677 056, что на 540 672 байта меньше 128 Мб. Причина, по которой GlobalMemoryStatus не сообщает о полных 128 Мб, кро ется в том, что система при загрузке резервирует небольшой участок оперативной памяти, недоступный даже ядру. Этот участок никогда не сбрасывается на диск А эле мент dwAvailPhys (показываемый как AvailPhys) дает число байтов свободной физи ческой памяти.

Элемент dwTotalPageFile (показываемый как TotalPagcFile) сообщает максимальное количество байтов, которое может содержаться в страничном файле (файлах) на жестком диске (дисках). Хотя VMStat показывает, что текущий размер страничного файла составляет 318 574 592 байта, система может варьировать его по своему усмот рению Элемент dwAvailPageFile (покапываемый как AvailPageFile) подсказывает, что в данный момент 233 046 0l6 байтов в страничном файле свободно и может быть пе редано любому процессу.

Элемент dwTotalVirtual (показываемый как TotalVirtual) отражает общее количе ство байтов, отведенных под закрытое адресное пространство процесса. Значение 2 147 352 576 ровно на 128 Кб меньше 2 Гб. Два раздела недоступного адресного про странства — от 0x00000000 до 0x0000FFFF и от 0x7FFF0000 до 0x7FFFFFFF — как раз и составляют эту разницу в 128 Кб. Запустив VMStat в Windows 98, Вы увидите, что значение этого элемента поменялось на 2 143 289 344 (2 Гб за вычетом 4 Мб). Разница в 4 Мб возникает из-за того, что Windows 98 блокирует нижний раздел от 0x00000000 до 0x003FFFFF (размером в 4 Мб).

И, наконец, dwAvailVirtual (показываемый как AvailVirtual) — единственный элемент структуры, специфичный для конкретного процесса, вызывающего GlobalMemoryStatus (остальные элементы относятся исключительно к самой системе и не зависят от того, какой именно процесс вызывает эту функцию). При подсчете значения dwAvaiWirtual функция суммирует размеры вссх свободных регионов в адресном пространстве вы зывающего процесса. В данном случае его значение говорит о том, что в распоряже нии программы VMStat имеется 2 136 846 336 байтов свободного адресного простран ства. Вычтя из значения dwTotalVirtual величину dwAvailVirtual, Вы получите 10 506 240 байтов — такой объем памяти VMStat зарезервировала в своем виртуальном адресном

пространстве Отдельного элемента, который сообщал бы количество физической памяти, используемой процессом в данный момент, не предусмотрено

VMStat

 

Определение состояния адресного пространства

В Windows имеется функция, позволяющая запрашивать определенную информацию об участке памяти по заданному адресу (в пределах адресного пространства вызыва ющего процесса): размер, тип памяти и атрибуты защиты. В частности, с ее помощью программа VMMap (ее листинг см. на рис. 14-4) выводит карты виртуальной памяти, с которыми мы познакомились в главе 13- Вот эта функция:

DWORD VirtualQuery( LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength);

Парная ей функция, VirtualQueryEx, сообщает ту же информацию о памяти, но в другом процессе:

DWORD VirtualQueryEx( HANDLE hProcess, LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength);

Эти функции идентичны с тем исключением, что VirtualQueryEx принимает опи сатель процесса, об адресном пространстве которого Вы хотите получить информа цию Чаще всего функцией VirtualQueryEx пользуются отладчики и системные утили ты — остальные приложения обращаются к VirtitalQuery. При вызове VirtualQitery(Ex) параметр pvAddress должен содержать адрес виртуальной памяти, о которой Вы хо тите получить информацию Параметр ртbi — это адрес структуры MEMORY_BA SIC_INFORMATION, которую надо создать перед вызовом функции. Данная структура определена в файле WinNT.h так

typedef struct _MFMORY_BASIC_INFORMATION
{

PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORO State;
DWORD Protect;
DWORD Type;

} MEMORY_BASIC_INFORMATION, PMEMORY_BASIC_INFORMATION;

Параметр dwLength задает размер структуры MEMORY_BASIC_INFORMATION. Фун кция VirtualQuery(Ex) возвращает число байтов, скопированных в буфер.

Используя адрес, указанный Вами в параметре pvAddress, функция VirtualQuery(Ex) заполняет структуру информацией о диапазоне смежных страниц, имеющих одина ковые состояние, атрибуты защиты и тип. Описание элементов структуры приведено в таблице ниже

Элемент

Описание

BaseAddress

Сообщает то же значение, что и параметр pvAddress, но округленное до ближайшего меньшею адреса, кратного размеру страницы

AllocationBase

Идентифицирует базовый адрес региона, включающего в себя адрес, указанный в параметре pvAddress

AllocationProtect

Идентифицирует атрибут защиты, присвоенный региону при его резервировании

RegionSize

Сообщаем суммарный размер (в байтах) группы страниц, которые на чинаются с базового адреса BaseAddress и имеют тс же атрибуты защи ты, состояние и тип, что и страница, расположенная по адресу, укачан ному в параметре pvAddress

State

Сообщает состояние (MEM_FRFF, MFM_RFSFRVE или MEM_COMMIT) всех смежных страниц, которые имеют те же атрибуты защиты, состо яние и тип, что и страница, расположенная по адресу, указанному в параметре pvAddress.

При MEM_FREE элементы AllocationBase, AllocationProtect, Protect и Туре содержат неопределенные значения, а при MEM_RESERVE неопреде ленное значение содержит элемент Protect.

Protect

Идентифицирует атрибут защиты (PAGE *) всех смежных страниц, ко торые имеют те же трибуты защиты, состояние и тип, что и страница, расположенная по адресу, указанному в параметре pvAddress

Type

Идентифицируем тип физической памяти (MEM_IMAGE, MEM_MAPPED или MEM PRIVATE), связанной с группой смежных страниц, которые имеют те же атрибуты защиты, состояние и тип, что и страница, рас положенная по адресу, указанному в пара метре pvAddress В Windows 98 этот элемент всегда дает MFM_PRIVATE

Функция VMQuery

Начиная изучать архитектуру памяти в Windows, я пользовался функцией VirtualQuery как «поводырем». Если Вы читали первое издание моей книги, то заметите, что про грамма VMMap была гораздо проще ее нынешней версии, представленной в следую щем разделе. Прежняя была построена на очень простом цикле, из которого перио дически вызывалась функция VirtualQuery, и для каждого вызова я формировал одпу строку, содержавшую элементы структуры MEMORY_BASIC__INFORMATION. Изучая полученные дампы и сверяясь с документацией из SDK (в то время весьма неудачной), я пытался разобраться в архитектуре подсистемы управления памятью. Что ж, с тех пор я многому научился и теперь знаю, что функция VirtualQuery и структура MEMO RY_BASIC_INFORMATION не дают полной картины

Проблема в том, чю в MEMORY_BASIC_INFORMATION возвращается отнюдь не вся информация, имеющаяся в распоряжении системы. Если Вам нужны простейшие дан ные о состоянии памяти по конкретному адресу, VirtualQuery действительно незаме нима. Она отлично работает, если Вас интересует, передана ли по этому адресу фи зическая память и доступен ли он для операций чтения или записи. Но попробуйте e ее помощью узнать общий размер зарезервированного региона и количество блоков в нем или выяснить, не содержит ли этот регион стек потока, — ничего не выйдет

Чтобы получать более полную информацию о памяти, я создал собственную фун кцию и назвал ее VMQuery.

BOOL VMQuery( HANDLE hProcess, PVOID pvAddress, PVMQUERY pVMQ);

По аналогии с VirtualQueryEx она принимает в hProcess описатель процесса, в pvAddress — адрес памяти, а в pVMQ — указатель на структуру, заполняемую самой функцией. Структура VMQUERY (тоже определенная мной) представляет собой вот что.

typedef struct
{

// информация о регионе
PVOID pvRgnBaseAddress;
DWORD dwRgnProtection;

// PAGE_*
SIZE_T RgnSize;
DWORD dwRgnStorage;

// MEM_* Free. Irnage, Mapped, Private
DWORD dwRgnBlocks;
DWORD dwRgnGuardBlks; // если > 0, регион содержит стек потока
BOOL tRqnlsAStack; // TRUE, если регион содержит стек потока

// информация о блоке
PVOID pvBlkBaseAddress;
DWORD dwBlkProtection;

// PAGE_*
SIZE_T BlkSize;

DWORD dwBlkStorage;

// MEM_* Free, Reserve, Image, Mapped, Private

} VMQUERY, *PVMQUERY;

С первого взгляда заметно, что моя структура VMQUERY содержит куда больше информации, чем MEMORY_BASIC_INFORMATION Она разбита (условно, конечно) на две части: в одной — информация и регионе, в другой — информация о блоке (адрес которого указан в параметре pvAddress). Элементы этой структуры описываются в следующейтаблице.

Элемент

Описание

pvRgnBaseAddress

Идентифицирует базовый адрес региона виртуального адресного про странства, включающего адрес, указанный в параметре pvAddress

dwRgnProtection

Сообщает атрибут защиты, присвоенный региону при его резервиро вании.

RgnSize

Указывает размер (в байтах) зарезернириванного о региона.

dwRgnStorage

Идентифицирует тип физической памяти, используемой группой бло ков данного peгиона: MEM_FREE, MEM_IMAGE, MEM_MAPPED или MEM PRIVATE. Поскольку Windows 98 не различает типы памяти, в этой операционной системе данный элемент содержит либо MEM_FREE, либо MEM_PRIVATE

dwRgnBlocks

Содержит значение — число блоков в указанном регионе

dwRgnGuardBlks

Указывает число блоков с установленным флагом атрибутов защиты PAGE GUARD. Обычно это значение либо 0, либо 1. Если оно равно 1, то регион скорее всего зарезервирован под стек потока В Windows 98 этот элемент всегда равен 0

fRgnIsAStack

Сообщает, есть ли в данном регионе стек потока Результат определя ется на основе взвешенной оценки, так как невозможно дать стопро центной гарантии тому, что в регионе содержится стек.

pvBlkBaseAddress

Идентифицирует базовый адрес блока, включающего адрес, указанный в параметре pvAddress,

dwBlkProtection

Идентифицирует атрибут защиты блока, включающего адрес, указан ный в параметре pvAddress.

BlkSize

Содержит значение — размер блока (в байтах), включающего адрес, указанный в параметре pvAddress.

dwBlkStorage

Идентифицирует содержимое блока, включающего адрес, указанный в параметре pvAddress. Принимает одно из значений: MEM FREE, MEM_RESERVE, MEM_IMAGE, MEM_MAPPED или MEM_PRIVATE. В Windows 98 этот элемент никогда не содержит значения MEM IMAGE и MEM MAPPED

Чтобы получить всю эту информацию, VMQuery, естественно, приходится выпол нять гораздо болыше операций (в том числе многократно вызывать VirtualQueryEx), а потому она работает значительно медленнее VirtualQueryEx. Так что Вы должны все тщательно взвесить, прежде чем остановить свой выбор па одной из этих функций Если Вам не нужна дополнительная информация, возвращаемая VMQuery, используй те VirtualQuery или VirtualQueryEx.

Листинг файла VMQuerycpp (рис. 14-3) показывает, как я получаю и обрабатываю данные, необходимые для инициализации элементов структуры VMQUERY. (Файлы VMQuery.cpp и VMQueryh содержатся в каталоге 14-VMMap на компакт-диске, прила гаемом к книге.) Чтобы не объяснять подробности обработки данных «на пальцах», я снабдил тексты программ массой комментариев, вольно разбросанных но всему коду.

VMQuery

 

Программа-пример VMMap

Эта программа, «14 VMMap.exe» (см. листинг на рис 14-4), просматривает свое адрес ное пространство и показывает содержащиеся в нем регионы и блоки, присутствую щие в регионах Файлы исходного кода и ресурсов этой программы находятся в ка талоге 14-VMMap па компакт-диске, прилагаемом к книге. После запуска VMMap на

экране появляется следующее окно.

rihter14-4.jpg

Карты виртуальной памяти, представленные в главе 13 в таблицах 13-2, 13-3 и 13-4, созданы с помощью именно этой программы

Каждый злемент в списке — результат вызова моей функции VMQuery. Основний цикл программы VMMap (в функции Refresb) выглядит так:

BOOL fOk = TRUE;
PVOID pvAddress = NULL;

...

while (fOk)
{

VMQUERY vmq;

fOk = VMQuery(hProcess, pvAddress, &vmq);

if (fOk)
{

// формируем строку для вывида на экран

// и добавляем ее в окно списка

TCHAR szLine[1024];

ConstructRgnInfoLine(hProcess. &vmq, szLine, sizeof(szLine});

LisTBox_AddString(hwndLB, szLine);

if (fExpandRegions)
{

for (DWORD dwBlock = 0; f0k && (dwBlock < vmq.dwRgnBlocks); dwBlock++)
{

ConstructBlkInfoLine(&vmq, szLine, sizeof(szLine));
ListBox_AddString(hwndLB, szLine);

// получаем адрес следующего региона
pvAddress = ((PBYTE) pvAddress + vmq BlkSize);

if (dwBlock < vmq dwRgnBlocks - 1)
{

// нельзя запрашивать информацию о памяти за последним блоком
fOk = VMQuery(liProcess, pvAddress, &vmq);

}

}

}

// получаем адрес следующего региона

pvAddress = ((PBYTE) vmq pvRgnBaseAddress + vmq.RgnSize);

}

}

Этот цикл начинает работу с виртуального адреса NULL и заканчивается, когда VMQuery возвращает FALSE, что указывает на невозможность дальнейшего просмотра адресного пространства процесса На каждой итерации цикла вызывается функция ConstructRgnlnfoLine; она заполняет символьный буфер информацией о регионе. По том эти данные вносятся в список.

В основной цикл вложен еще один цикл — он позволяет получать информацию о каждом блоке текущего региона. На каждой итерации из данного цикла вызывается функция ConstructBlklnfoLine, заполняющая символьный буфер информацией о бло ках региона. Эти данные тоже добавляются к списку. В общем, с помощью функции VMQuery просматривить адресное пространство процесса очень легко.

VMMap