Программирование драйверов Windows

       

Операции с памятью


Операционная система Windows оперирует тремя типами адресов:

  • Виртуальные адреса, которые транслируются в физические адреса перед доступом к области памяти.
  • Физические адреса, которые реально указывают в область физической памяти. Следует отметить, что по этим адресам содержимое памяти всегда появляется на шине доступа к памяти, независимо от того, реально ли она присутствует в ОЗУ, или содержится внутри обслуживаемых драйвером устройств.
  • Логические адреса. Этот тип описывает специальные адреса, используемые уровнем HAL при общении с устройствами. Соответственно, уровень HAL и отвечает за операции с этими адресами.
  • Работа по программированию в режиме ядра всегда связана с тонкостями работы с памятью. В каком контексте работает программный поток, какого типа памятью он манипулирует (пользовательской или режима ядра), какого типа память (страничная или нестраничная) используется, если идет работа с памятью режима ядра, наконец, приемлем ли текущий уровень приоритета IRQL для доступа к данному типу памяти? Разумеется, все эти вопросы возникают, только лишь, если разрабатывается код режима ядра.

    Адреса 4 гигабайтного виртуального пространства памяти 32-разрядных версий операционной системы Windows NT 5 (об отличиях для 64-разрядных версий было сказано в главе 4) делятся на 2 нижних гигабайта памяти пользовательского виртуального пространства, имеющего смысл только в контексте пользовательского приложения (процесса), которому оно выделено, и 2 верхних гигабайт системного виртуального пространства режима ядра. Системное адресное пространство доступно всем программным потокам режима ядра. (Иначе, как смогло бы работать программное обеспечение режима ядра собственно операционной системы?!) Все 4-х гигабайтное адресное пространство можно представить в виде книги с одной обложкой. Толстая обложка &#8212 это системное адресное пространство, тонкие бумажные листы &#8212 это виртуальные и автономные пользовательские адресные пространства.

    Системное виртуальное пространство памяти режима ядра делится на диапазоны (обычная архитектура x86), представленные в таблице 7.1.


    Адреса с 0xC0000000 по 0xC0800000 используются для хранения данных Менеджера памяти, который поддерживает механизм виртуальной памяти.

    Диапазон с 0xFFBE0000 по 0xFFC00000 используются для хранения информации о страничном файле (файле подкачки), которая используется для сброса содержимого физической памяти в этот файл. (Методология crash дампа предусматривает создание crash dump файла из этого файла подкачки при следующей загрузке системы.)

    Адреса с 0xE1000000 по 0xFFBE0000 занимают области странично и нестранично организованной памяти, что вместе составляет менее 500 мегабайт.

    Какие трюки можно проделывать с виртуальной памятью? На этот не слишком конкретный вопрос существует ответ в виде вопроса: а для достижения чего именно?

    Рассмотрим довольно незамысловатую ситуацию, предпосылки которой рассматривались ранее. Предположим, драйвер создал программный поток (вызовом PsCreateSystemThread), который в некоторой ситуации должен выполнять некоторую работу, например, по сигналу функции обработки IOCTL запросов &#8212 выполнить перенос данных в буфер, предоставленный пользовательским приложением. Предположим, что разработчик драйвера так задал IOCTL код (используя метод буферизации NEITHER), что в драйвер поступает пользовательский адрес буфера (значение меньше 0x80000000). Разработчик драйвера через внутренние переменные передает этот адрес программному потоку, который должен выполнить перенос, и... Наступает сбой системы.



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

    Когда драйверная функция обработки IOCTL запросов пользовательского режима (по вызову DeviceIoControl) получает адрес пользовательского буфера по методу буферизации NEITHER, то этот пользовательский виртуальный адрес имеет смысл в этой функции, поскольку она работает в контексте пользовательского программного потока и интерпретация пользовательского виртуального адреса не вызовет проблем.


    Другое дело, если этот адрес окажется в распоряжении программного потока, созданного драйвером по вызову PsCreateSystemThread, где интерпретация данного адреса вызовет ошибку, поскольку, как указано в документации DDK, этот системный программный поток не имеет никакого пользовательского контекста. Это одна из проблем.

    HAL 0xFFFFFFFF
    ...
    Информация CRASH DUMP 0xFFC00000
    ...
    Нестраничный пул 0xFFBE0000
    ...
    Страничный пул ...
     0xE1000000
    Файловый кэш  ...
     0xC1000000
    Пространство файлового кэш-менеджера  ...
     0xC0C00000
    Не используется  ...
     0xC0800000
    Зарезервировано  ...
     0xC0400000
    Элементы директории страниц (PDE)  ...
     0xC0300000
    Элементы таблицы страниц (РТЕ)  ...
     0xC0000000
    Не используется  ...
     0xA3000000
    Memory Mapped files  
     
    Копия операционной системы  ...
     0x80000000
    Таблица 7.1. Диапазоны памяти системного адресного пространства Windows 2000
    Вторая возможная проблема состоит в том, что к моменту обращения созданного программного потока к пользовательскому буферу, рассматриваемое пользовательское пространство вполне может оказаться в страничном файле на жестком диске, и (если поток повысил свой приоритет) неминуемо случится сбой системы, поскольку операционная система запрещает обработку таких ситуаций на высоких уровнях IRQL.

    Чтобы решить задачу в данной постановке необходимо, во-первых, создать список MDL (структуру, хранящую отображение блока виртуальной памяти на физическую память), зафиксировать пользовательский буфер в физической оперативной памяти и передать MDL список (или соответствующий виртуальный адрес системного адресного пространства) рабочему потоку. По выполнении работы, следует разблокировать страницы пользовательского буфера и освободить структуру MDL списка. Соответственно, при этом делаются вызовы системных функций: IoAllocateMdl, MmProbeAndLockPages, MmGetSystemAddressForMdl, MmUnlockPages и IoFreeMdl.



    Вызов IoAllocateMdl создает структуру MDL списка для указанного виртуального адреса (пользовательского или системного адресного пространства) с указанной длиной.

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

    по поводу MDL, составленного для страничной памяти, программный поток должен работать на уровне IRQL ниже DISPATCH_LEVEL &#8212 чтобы позволить отработать системному коду, если страница окажется в страничном файле. (В случае, если исходный буфер находился бы в нестраничной памяти, то данный вызов можно было бы сделать с уровня IRQL равном DISPATCH_LEVEL или ниже.)

    Функция MmGetSystemAddressForMdl возвращает виртуальный адрес, вычисленный из MDL списка, так, будто рассматриваемая область памяти находится в системном адресном пространстве, а именно &#8212 в нестраничном пуле. Этот адрес можно использовать в любом месте кода драйвера на любых уровнях 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-&#62IoStatus.Status = STATUS_ACCESS_VIOLATION; pIrp-&#62IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_CONFLICTING_ADDRESSES; }

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

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


    Содержание раздела