Операции с памятью
Операционная система Windows оперирует тремя типами адресов:
Работа по программированию в режиме ядра всегда связана с тонкостями работы с памятью. В каком контексте работает программный поток, какого типа памятью он манипулирует (пользовательской или режима ядра), какого типа память (страничная или нестраничная) используется, если идет работа с памятью режима ядра, наконец, приемлем ли текущий уровень приоритета IRQL для доступа к данному типу памяти? Разумеется, все эти вопросы возникают, только лишь, если разрабатывается код режима ядра.
Адреса 4 гигабайтного виртуального пространства памяти 32-разрядных версий операционной системы Windows NT 5 (об отличиях для 64-разрядных версий было сказано в главе 4) делятся на 2 нижних гигабайта памяти пользовательского виртуального пространства, имеющего смысл только в контексте пользовательского приложения (процесса), которому оно выделено, и 2 верхних гигабайт системного виртуального пространства режима ядра. Системное адресное пространство доступно всем программным потокам режима ядра. (Иначе, как смогло бы работать программное обеспечение режима ядра собственно операционной системы?!) Все 4-х гигабайтное адресное пространство можно представить в виде книги с одной обложкой. Толстая обложка — это системное адресное пространство, тонкие бумажные листы — это виртуальные и автономные пользовательские адресные пространства.
Системное виртуальное пространство памяти режима ядра делится на диапазоны (обычная архитектура x86), представленные в таблице 7.1.
Адреса с 0xC0000000 по 0xC0800000 используются для хранения данных Менеджера памяти, который поддерживает механизм виртуальной памяти.
Диапазон с 0xFFBE0000 по 0xFFC00000 используются для хранения информации о страничном файле (файле подкачки), которая используется для сброса содержимого физической памяти в этот файл. (Методология crash дампа предусматривает создание crash dump файла из этого файла подкачки при следующей загрузке системы.)
Адреса с 0xE1000000 по 0xFFBE0000 занимают области странично и нестранично организованной памяти, что вместе составляет менее 500 мегабайт.
Какие трюки можно проделывать с виртуальной памятью? На этот не слишком конкретный вопрос существует ответ в виде вопроса: а для достижения чего именно?
Рассмотрим довольно незамысловатую ситуацию, предпосылки которой рассматривались ранее. Предположим, драйвер создал программный поток (вызовом PsCreateSystemThread), который в некоторой ситуации должен выполнять некоторую работу, например, по сигналу функции обработки IOCTL запросов — выполнить перенос данных в буфер, предоставленный пользовательским приложением. Предположим, что разработчик драйвера так задал IOCTL код (используя метод буферизации NEITHER), что в драйвер поступает пользовательский адрес буфера (значение меньше 0x80000000). Разработчик драйвера через внутренние переменные передает этот адрес программному потоку, который должен выполнить перенос, и... Наступает сбой системы.
Что случилось? Чтобы объяснить сложившуюся ситуацию и решить проблему, необходимо, прежде всего, согласиться с тем, что простые приемы пользовательского режима следует оставить пользовательскому режиму.
Когда драйверная функция обработки IOCTL запросов пользовательского режима (по вызову DeviceIoControl) получает адрес пользовательского буфера по методу буферизации NEITHER, то этот пользовательский виртуальный адрес имеет смысл в этой функции, поскольку она работает в контексте пользовательского программного потока и интерпретация пользовательского виртуального адреса не вызовет проблем.
Другое дело, если этот адрес окажется в распоряжении программного потока, созданного драйвером по вызову PsCreateSystemThread, где интерпретация данного адреса вызовет ошибку, поскольку, как указано в документации DDK, этот системный программный поток не имеет никакого пользовательского контекста. Это одна из проблем.
|
Таблица 7.1. Диапазоны памяти системного адресного пространства Windows 2000 |
Чтобы решить задачу в данной постановке необходимо, во-первых, создать список MDL (структуру, хранящую отображение блока виртуальной памяти на физическую память), зафиксировать пользовательский буфер в физической оперативной памяти и передать MDL список (или соответствующий виртуальный адрес системного адресного пространства) рабочему потоку. По выполнении работы, следует разблокировать страницы пользовательского буфера и освободить структуру MDL списка. Соответственно, при этом делаются вызовы системных функций: IoAllocateMdl, MmProbeAndLockPages, MmGetSystemAddressForMdl, MmUnlockPages и IoFreeMdl.
Вызов IoAllocateMdl создает структуру MDL списка для указанного виртуального адреса (пользовательского или системного адресного пространства) с указанной длиной.
Вызов MmProbeAndLockPages проверяет присутствие страниц в физической памяти, подгружает (для страничной памяти), если они отсутствуют, и производит их фиксацию (после чего данные страницы не могут быть сброшены в страничный файл на жестком диске). Для корректного вызова MmProbeAndLockPages
по поводу MDL, составленного для страничной памяти, программный поток должен работать на уровне IRQL ниже DISPATCH_LEVEL — чтобы позволить отработать системному коду, если страница окажется в страничном файле. (В случае, если исходный буфер находился бы в нестраничной памяти, то данный вызов можно было бы сделать с уровня IRQL равном DISPATCH_LEVEL или ниже.)
Функция MmGetSystemAddressForMdl возвращает виртуальный адрес, вычисленный из MDL списка, так, будто рассматриваемая область памяти находится в системном адресном пространстве, а именно — в нестраничном пуле. Этот адрес можно использовать в любом месте кода драйвера на любых уровнях IRQL, даже в процедуре обработки прерываний. Контекст выполнения для этого адреса не имеет никакого значения. (Справедливости ради, следует отметить, что перевод в системный адрес не является обязательной операцией, можно и далее использовать MDL список, что позволяют делать, в частности, вызовы нижних драйверов в стеке.) Вызов (точнее, макроопределение) MmGetSystemAddгessForMdl являeтcя устаревшим, и его следует использовать только в WDM драйверах, предназначенных для работы еще и в Windows 98. Использование макроопределения MmGetSystemAddressForMdlSafe
является предпочтительным. Оба эти макроопределения могут быть вызваны из кода, работающего на уровне IRQL не выше DISPATH_LEVEL.
Вызов MmUnlockPages отменяет фиксацию страниц в оперативной памяти, а вызов IoFreeMdl уничтожает структуру MDL списка.
Чтобы отследить ошибки, связанные с фиксацией блока виртуальной памяти в памяти оперативной, рекомендуется выполнять вызов MmProbeAndLockPages
внутри try-except блока, например:
__try { MmProbeAndLockPages( pMdl, UserMode, IoModifyAccess); } __except( EXCEPTION_EXECUTE_HANDLER) { pIrp->IoStatus.Status = STATUS_ACCESS_VIOLATION; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_CONFLICTING_ADDRESSES; }
На рассмотренном выше примере видно, что операционная система предоставляет достаточно инструментов для преобразования адресов пользовательского пространства в адреса системного пространства и физические адреса. Важно только корректно отслеживать, когда и что следует использовать.
Ниже приводятся более полные сведения о системных функциях, которые полезны для работы с виртуальной и физической памятью.