[Previous] [Next]

Управление размещением кода драйвера в памяти

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

Определение размещения при компиляции

Для того чтобы драйверные процедуры оказались после загрузки в памяти определенного типа, можно использовать директивы указания компилятору #pragma.

#pragma code_seg("INIT")
<программный текст>
#pragma code_seg()

#pragma code_seg("PAGE")
<программный текст>
#pragma code_seg() 

Здесь первая строка вводит программный код категории INIT. Этот код, подобно сгоревшей ступени ракеты, растворится в небытии сразу по окончании инициализации драйвера (работы драйверной процедуры DriverEntry). Традиции такого кода восходят еще ко временам операционной системы DOS, когда малый размер драйвера был его важнейшим достоинством.

Директива '#pragma code_seg()' восстанавливает правила по умолчанию.

Директива '#pragma code_seg("PAGE")' обеспечивает размещение кода в областях странично организованной памяти.

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

Аналогичным образом директивы компилятора применимы и к сегментам (секциям) данных, см. два примера ниже.

#pragma data_seg("INIT")
<описание переменных>
#pragma data_seg()

#pragma data_seg("PAGE")
<описание переменных>
#pragma data_seg() 

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

#ifdef ALLOC_PRAGMA
#pragma alloc_text( "INIT", DriverEntry )
#pragma alloc_text( "PAGE", MyUnloadProcedure )
#endif 

В приведенном выше фрагменте процедура DriverEntry будет отнесена к категории INIT, а процедура MyUnloadProcedure будет размещена в станичной памяти.

Динамическое перемещение кода драйвера в страничную память

Наконец, третий способ перемещения кода драйвера в страничную память является динамическим и происходит под управлением самого драйвера. Весь программный код драйвера, обычно размещенный в области кода операционной системы (начинающейся с адреса 0x80000000), можно пометить как "страничный" и переместить в странично организованную память системным вызовом MmPageEntireDriver, которому в качестве параметра передается любой адрес или указатель на любую процедуру в составе кода драйвера, например, AddDevice (если эта процедура присутствует). Обратное действие выполняет вызов MmResetDriverPaging, который восстанавливает статус всех секций кода драйвера, данный им при компиляции, и все секции, которые были созданы как нестраничные, фиксируются в оперативной памяти.

Вызовы MmPageEntireDriver и MmResetDriverPaging следует выполнять только в коде, работающем на уровне IRQL равном PASSIVE_LEVEL.

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

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

Соответственно, если пользователь прекращает работу с драйвером вызовом CloseHandle (в результате чего вызывается драйверная процедура, обслуживающая запрос IRP_MJ_CLOSE), то драйвер может выполнить перевод всего своего кода в область страничной памяти, после чего он, возможно, окажется сброшенным на диск. Разумеется, имеет смысл проверить предварительно, сколько раз был получен доступ к драйверу (возможно, имеются еще пользовательские приложения, имеющие открытый дескриптор). Если драйвер подключен к прерыванию, следует отключиться от него. Обратные действия (восстановления прежнего состояния драйверных процедур) можно проделать в процедуре, обслуживающей IRP_MJ_CREATE, то есть когда пользовательское приложение пытается получить доступ к драйверу и получить соответствующий дескриптор вызовом пользовательского режима CreateFile.

Контроль за количеством обращений к драйверу можно вести при помощи системных вызовов InterlockedIncrement и InterlockedDecrement, обеспечивающих безопасное обращение к счетчику, который можно разместить, например, в расширении структуры устройства (device extension), состав которой полностью зависит от воли разработчика драйвера.

Следует также помнить, что после старта драйвера секции, отнесенные при компиляции к типу INIT перестают существовать, поэтому использовать их в операциях по перемещению из/в страничную память нельзя.

Фиксация страничных секций кода и данных в оперативной памяти

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

Фиксация секций страничной памяти данных и кода выполняется при помощи вызовов MmLockPagableDataSection и MmLockPagableCodeSection соответственно. В качестве параметра этим функциям передается адрес, указывающий внутрь интересующей области — адрес элемента данных или адрес интересующей процедуры. Возвращаемые этими вызовами дескрипторы следует сохранить — они потребуются для последующего системного вызова MmUnlockPagableImageSection, используемого для восстановления прежнего статуса соответствующих областей виртуальной памяти (уменьшения числа ссылок на такие области).

Применение вызовов MmLockPagableDataSection/MmLockPagableCodeSection повторно к одной и той же области является расточительным приемом. Поэтому, если драйвер вынужден использовать эти вызовы повторно, то следует использовать функцию MmLockPagableSectionByHandle, используя первоначально полученный дескриптор. В противном случае, рекомендуется вести учет вызовов функций MmLockPagableDataSection/MmLockPagableCodeSection, чтобы не выполнять лишней работы.

Для того чтобы программно контролировать отдельный набор данных, можно поместить эти данные автономно, как показано ниже:

#pragma data_seg("MY_DATA")
<описание переменных>
#pragma data_seg()

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

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

Все упомянутые выше вызовы следует выполнять только в коде, работающем на уровне IRQL равном PASSIVE_LEVEL.

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

Если программный код размещен в страничной памяти, то в отладочной версии драйвера можно организовать проверку, всегда ли он вызывается на должном уровне привилегий. Для странично размещенного кода этот уровень IRQL должен быть ниже DISPATCH_LEVEL.

Для выполнения этой работы сконструировано специальное макроопределение 'PAGED_CODE();', которое проверяет текущий уровень IRQL (не превышает ли он APC_LEVEL) и генерирует отладочное сообщение при помощи вызовов KdPrint и RtlAssert, которое можно наблюдать, например, в окне программы DebugView, если она к тому моменту запущена.

Кстати, сам факт существования макроопределения PAGED_CODE говорит о том, что использование станичной памяти на высоких уровнях IRQL является некорректным не само по себе (иногда оно может "сойти с рук"). Причина в том, что данное стечение обстоятельств является потенциальной угрозой правилам системы: она откажется обслуживать ситуацию, когда данная страничная память окажется на диске, а уровень IRQL будет столь же высок.

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