Windows via c программирование

Рихтер Дж., Назар К. — Windows via C C++. Программирование на языке Visual C++ — 2009

Глава 14 . Исследование виртуальной памяти.docx 479

Табл. 14-4. (окончание)

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

параметре pwAddress . Принимает одно из значений: MEM_FREE,

MEM_RESERVE, MEM_IMAGE, MEM_MAPPED или MEM_PRIVATE.

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

VMQuery , используйте VirtualQuery или VirtualQueryEx .

Листинг файла VMQuery.cpp (рис. 14-3) показывает, как я получаю и обрабатываю данные, необходимые для инициализации элементов структуры

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

Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre

#include «..\CommonFiles\CmnHdr.h» /* см. приложение A. */ #include

// вспомогательная структура typedef struct <

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

// если > 0, в регионе содержится стек потока

// TRUE, если в регионе содержится стек потока

// глобальная статическая переменная, содержащая значение —

480 Часть III . Управление памятью

// памяти на данном типе процессора; инициализируется при первом

static DWORD gs_dwAllocGran = 0;

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

static BOOL VMQueryHelp(HANDLE hProcess, LPCVOID pvAddress, VMQUERY_HELP *pVMQHelp) <

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

BOOL bOk = (VirtualQueryEx(hProcess, pvAddress, &mbi, sizeof(mbi)) == sizeof(mbi));

return(bOk); // неверный адрес памяти, сообщаем об ошибке

// проходим по региону, начиная с его базового адреса

// (который никогда не изменяется)

PVOID pvRgnBaseAddress = mbi.AllocationBase;

// начинаем с первого блока в регионе

// (соответствующая переменная будет изменяться в цикле)

PVOID pvAddressBlk = pvRgnBaseAddress;

// запоминаем тип физической памяти, переданной данному блоку pVMQHelp->dwRgnStorage = mbi.Type;

// получаем информацию о текущем блоке

// не удалось получить информацию; прекращаем цикл

// проверяем, принадлежит ли текущий блок запрошенному региону if (mbi.AllocationBase != pvRgnBaseAddress)

break; // блок принадлежит следующему региону; прекращаем цикл

// блок принадлежит запрошенному региону

pVMQHelp->dwRgnBlocks++; // добавляем к региону еще один блок

Глава 14 . Исследование виртуальной памяти.docx 481

pVMQHelp->RgnSize += mbi.RegionSize; // увеличиваем счетчик блоков

// в этом регионе на 1

// если блок имеет флаг PAGE_GUARD, добавляем 1 к счетчику блоков

// делаем наиболее вероятное предположение о типе физической памяти,

// переданной данному блоку. Стопроцентной гарантии дать нельзя,

// потому что некоторые блоки могли быть преобразованы MEM_IMAGE

// в MEM_PRIVATE или из MEM_MAPPED в MEM_PRIVATE; MEM_PRIVATE в любой

// момент может быть замещен наMEM_IMAGE или MEM_MAPPED.

if (pVMQHelp->dwRgnStorage == MEM_PRIVATE) pVMQHelp->dwRgnStorage = mbi.Type;

// получаем адрес следующего блока

pvAddressBlk = (PVOID) ((PBYTE) pvAddressBlk + mbi.RegionSize);

// обследовать регион, думаем: не стек ли это?

// Windows Vista: да – если в регионе содержится хотя бы 1 блок

// с флагом PAGE_GUARD

pVMQHelp->bRgnIsAStack = (pVMQHelp->dwRgnGuardBlks > 0);

BOOL VMQuery(HANDLE hProcess, LPCVOID pvAddress, PVMQUERY pVMQ) <

if (gs_dwAllocGran == 0) <

// если это первый вызов, надо выяснить гранулярность

// выделения памяти в данной системе

SYSTEM_INFO sinf; GetSystemInfo(&sinf);

// получаем MEMORY_BASIC_INFORMATION для переданного адреса

BOOL bOk = (VirtualQueryEx(hProcess, pvAddress, &mbi, sizeof(mbi)) == sizeof(mbi));

482 Часть III . Управление памятью

return(bOk); // неверный адрес памяти, сообщаем об ошибке

// структура MEMORY_BASIC_INFORMATION содержит действительную

// информацию – пора заполнить элементы нашей структуры VMQUERY

// во-первых, заполним элементы, описывающие состояния блока;

// данные по региону получим позже

// свободный блок (незарезервирован-

case MEM_RESERVE: // зарезервированный блок, которому // не передана физическая память

pVMQ->pvBlkBaseAddress = mbi.BaseAddress; pVMQ->BlkSize = mbi.RegionSize;

// для блока, которому не передана физическая память,

// элемент mbi.Protect недействителен. Поэтому мы покажем,

// что зарезервированный блок унаследовал атрибут защиты

Читайте также:  Linux notify send the

// того региона, в котором он содержатся

pVMQ->dwBlkProtection = mbi.AllocationProtect; pVMQ->dwBlkStorage = MEM_RESERVE;

зарезервированный блок, которому

передана физическая память

pVMQ->pvBlkBaseAddress = mbi.BaseAddress; pVMQ->BlkSize = mbi.RegionSize; pVMQ->dwBlkProtection = mbi.Protect; pVMQ->dwBlkStorage = mbi.Type;

// теперь заполняем элементы, относящиеся к региону

VMQUERY_HELP VMQHelp; switch (mbi.State) <

case MEM_FREE: // свободный блок (незарезервированный) pVMQ->pvRgnBaseAddress = mbi.BaseAddress;

Глава 14 . Исследование виртуальной памяти.docx 483

// зарезервированный блок, которому

// не передана физическая память

// чтобы получить полную информацию по региону, нам придется

// пройти по всем его блокам

// зарезервированный блок, которому

// передана физическая память

// чтобы получить полную информацию по региону, нам придется

// пройти по всем его блокам

484 Часть III . Управление памятью

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

Карты виртуальной памяти, представленные в главе 13 в таблицах 13-2 и 13-3, созданы с помощью именно этой программы. Каждый элемент в списке — результат вызова моей функции VMQuery . Основной цикл программы VMMap (в функции Refresh ) выглядит так:

PV0ID pvAddress = NULL;

bOk = VMQuery(hProcess, pvAddress, &vmq); if (bOk) <

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

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

Глава 14 . Исследование виртуальной памяти.docx 485

ListBox_AddSt ring(hWndLB. szLine);

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

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

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

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

Если запустить VMMap на компьютере с Windows Vista после перезагрузки (или сравнить результаты программы на разных компьютерах с Vista), можно заметить, что разные DLL-библиотеки каждый раз загружаются по разным адресам. Так работает новая функция Windows под названием Address Space Layout Randomization (ASLR). Цель случайного выбора базовых адресов — затруднить хакерам поиск известных DLL в памяти, чтобы они не смогли использовать их в своих целях.

Например, хакеры часто используют переполнение буфера или стека, чтобы вызвать стандартную функцию из системной DLL В системе с ASLR у хакера только один шанс из 256 (а то и меньше) найти нужную ему фун-

486 Часть III . Управление памятью

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

При загрузке DLL ядро изменяет и выравнивает ее базовый адрес, после чего всем процессам передается уже измененный базовый адрес этой библиотеки. Это повышает эффективность использования памяти, поскольку освобождает от необходимости выравнивания адресов для каждого процесса по отдельности.

Примечание. В Visual Studio 2005 SP1 и выше вы можете использовать ASLR для своих DLL- и ЕХЕ-файлов, включив ключ /dynamicbase при компоновке. Я также рекомендую использовать этот ключ, если ваш модуль загружается по адресу, отличному от базового, — так процессы получат уже выровненный адрес вашей библиотеки, что повысит эффективность использования памяти.

Г Л А В А 1 5 Использование виртуальной памяти в приложениях .

Резервирование региона в адресном пространстве .

Передача памяти зарезервированному региону .

Резервирование региона с одновременной передачей физической памяти .491

В какой момент региону передают физическую память .

Возврат физической памяти и освобождение региона .

В какой момент физическую память возвращают системе .

Рихтер Дж., Назар К. — Windows via C C++. Программирование на языке Visual C++ — 2009

258 Часть II . Приступаем к работе

Читайте также:  Когда вышел windows phone

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

Microsoft повысила быстродействие критических секций, включив в них спинблокировку. Теперь, когда вы вызываете EnterCriticalSection , она выполняет заданное число циклов спин-блокировки, пытаясь получить доступ к ресурсу. И лишь в том случае, когда все попытки заканчиваются неудачно, функция переводит поток в режим ядра, где он будет находиться в состоянии ожидания.

Для использования спин-блокировки в критической секции нужно инициализировать счетчик циклов, вызвав:

Как и в InitializeCriticalSection , первый параметр этой функции — адрес структуры критической секции. Но во втором параметре, dwSpinCount , передается число циклов спин-блокировки при попытках получить доступ к ресурсу до перевода потока в состояние ожидания. Этот параметр может принимать значения от 0 до 0x00FFFFFF. Учтите, что на однопроцессорной машине значение параметра dwSpinCount игнорируется и считается равным 0. Дело в том, что применение спин-блокировки в такой системе бессмысленно: поток, владеющий ресурсом, не сможет освободить его, пока другой поток «крутится» в циклах спин-блокировки.

Вы можете изменить счетчик циклов спин-блокировки вызовом:

И в этой функции значение dwSpinCount на однопроцессорной машине игнорируется.

На мой взгляд, используя критические секции, вы должны всегда применять спин-блокировку — терять вам просто нечего. Могут возникнуть трудности в подборе значения dwSpinCount , но здесь нужно просто поэкспериментировать. Имейте в виду, что для критической секции, стоящей на страже динамической кучи вашего процесса, этот счетчик равен 4000.

Критические секции и обработка ошибок

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

Глава 8 . Синхронизация потоков в пользовательском режиме.docx 259

блок памяти для внутрисистемной отладочной информации. Если выделить память не удается, генерируется исключение STATUS_NO_MEMORY. Вы можете перехватить его, используя структурную обработку исключений (см. главы 23,24

Есть и другой, более простой способ решить эту проблему — перейти на но-

вую функцию InitializeCriticalSectionAndSpinCount . Она, тоже выделяя блок памя-

ти для отладочной информации, возвращает FALSE, если выделить память не удается.

В работе с критическими секциями может возникнуть еще одна проблема. Когда за доступ к критической секции конкурирует два и более потока, она использует объект ядра «событие». Поскольку такая конкуренция маловероятна, система не создает объект ядра «событие» до тех пор, пока он действительно не потребуется. Это экономит массу системных ресурсов — в большинстве критических секций конкуренция потоков никогда не возникает. Кстати, этот объект ядра освобождается только при вызове DeleteCriticalSection . Так что не забывайте вызывать эту функцию, закончив операции в критической секции.

Раньше (до Windows XP) в случаях, когда в условиях нехватки памяти конкуренция потоков за критическую секцию все же возникала, системе не удавалось создать нужный объект ядра. И тогда EnterCriticalSection генерировала исключение EXCEPTION_INVALID_HANDLE. Большинство разработчиков просто игнорирует вероятность такой ошибки, и не предусматривает для нее никакой обработки, поскольку она случается действительно очень редко. Но если вы хотите заранее подготовиться к такой ситуации, у вас есть две возможности.

Первая — использовать структурную обработку исключений и перехватывать ошибку. При этом вы либо отказываетесь от обращения к ресурсу, защищенному критической секцией, либо дожидаетесь появления свободной памяти, а затем по-

вторяете вызов EnterCriticalSection .

Вторая возможность заключается в том, что вы создаете критическую секцию вызовом InitializeCriticalSectionAndSpinCount , передавая параметр dwSpinCount с

установленным старшим битом. Тогда функция создает объект «событие» и сопоставляет его с критической секцией. Если создать объект не удается, она возвращает FALSE, и это позволяет корректнее обрабатывать такие ситуации. Но успешно созданный объект ядра «событие» гарантирует вам, что EnterCriticalSection выполнит свою задачу при любых обстоятельствах и никогда не вызовет исключение. (Всегда выделяя память под объекты ядра «событие», вы неэкономно расходуете системные ресурсы. Поэтому делать так следует лишь в нескольких случаях, а именно: если программа может рухнуть из-за неудачного завершения функции EnterCriticalSection , если вы уверены в конкуренции потоков при обращении к критической секции или если программа будет работать в условиях нехватки памяти.)

Читайте также:  Браузер грузит систему windows 10

В Windows XP появился новый тип объекта ядра «событие» — т. н. событие с ключом ( keyed event ). Оно предназначено как раз для решения

260 Часть II . Приступаем к работе

проблемы создания объектов в условиях нехватки памяти. Вместе с процессом операционная система всегда создает один такой объект, его легко найти с помо-

щью утилиты Process Explorer от Sysinternals (см. http://www.micwsoft.com/technet/sysinternals/utilities/ProcessExplorer.mspx; ищите объект \KernelObjects\CritSecOutOfMemoryEvent). Этот недокументированный объект ядра во всем похож на обычный объект «событие» за исключением одного. Он способен синхронизировать различные группы потоков, которые определяются и блокируются с помощью ключа (по сути, указателя). Если из-за нехватки памяти потоку, вошедшему в критическую секцию, не удается создать объект «событие», в качестве ключа используется адрес критической секции. Так что потоки, пытающиеся войти в данную конкретную критическую секцию, будут синхронизированы и, при необходимости, блокированы на этом объекте «событие с ключом».

У «тонкой» блокировки чтения и записи (slim reader-writer lock), называемой так-

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

Сначала следует объявить структуру SRWLOCK и инициализировать ее вызо-

VOID InitializeSRWLock(PSRWLOCK SRWLock);

Структура SRWLOCK определена как RTL_SRWLOCK в файле WinBase.h. В WinNT.h объявление этой структуры содержит лишь указатель, ссылающийся на что-то другое, незадокументированное и потому (в отличие от полей CRITICAL_SECTION) недоступное для использования в коде.

typedef struct _RTL_SRWLOCK < PVOID Ptr;

После инициализации SRWLock записывающий поток может попытаться получить монопольный доступ к ресурсу, защищенному SRWLock , вызвав функцию AcquireSRWLockExclusive и передав ей в качестве параметра адрес объекта

VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);

Глава 8 . Синхронизация потоков в пользовательском режиме.docx 261

После модификации ресурса блокировку освобождают вызовом ReleaseSRWLockExclusive с передачей в качестве параметра адреса объекта SRWLOCK:

VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);

В случае читающего потока все аналогично, только используются другие функции:

VOID AcquireSRWLockShared(PSRWLOCK SRWLock);

VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);

Вот и все. Функций для удаления и разрушения объектов SRWLOCK нет, поскольку система выполняет эти операции автоматически.

У SRWLock отсутствует ряд функций критической секции:

■ Вызовы типа TryEnter ( Shared / Exclusive ) SRWLock невозможны, поскольку вызовы функций вида AcquireSRWLock ( Shared /Exclusive) блокируют вызывающий поток, если у него уже есть блокировка.

■ Невозможно также рекурсивное получение SRWLOCK, то есть один поток не может получить сразу несколько блокировок для многократной записи, а затем освободить их, вызвав соответствующее число раз функцию ReleaseSRWLock *.

Впрочем, если вы смиритесь с этими ограничениями, то получите реальный

прирост производительности, заменив критические секции блокировками SRWLock . Чтобы убедиться в том, что эти механизмы сильно различаются по скорости работы, запустите на многопроцессорной машине проект 08UserSyncCompare, доступный на веб-сайте поддержки этой книги.

Эта простая программа порождает один, два или четыре потока, повторно исполняющих одну и ту же задачу с использованием различных механизмов синхронизации. Я прогнал все тесты на своем двухпроцессорном компьютере и записал затраченное время. Результаты показаны в табл. 8-2.

Табл. 8-2. Сравнение производительности различных механизмов синхронизации

Оцените статью