[Previous] [Next]

Контекст выполнения программного кода

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

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

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

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

Контекст исключения или внутреннего прерывания (trap)

Запрос программного кода пользовательского режима на обслуживание фрагментом кода режима ядра обслуживается через использование внутренних прерываний ('trap', что переводится как 'ловушка', 'внутреннее прерывание'). Переход к выполнению процедуры режима ядра оформлен как вызванное этим приложением пользовательского режима программное исключение (exception, или внутреннее прерывание, trap). В данном случае, контекст следует отнести, скорее, к коду peжима ядра, нежели к коду пользовательского режима, вызвавшего исключение. Неуверенность толкования этой ситуации проистекает из того, что действие после генерации исключения разворачивается все-таки в режиме ядра. Однако память по адресам пользовательского приложения (каким либо образом попавшим в код драйвера) видится коду режима ядра точно так же, как она представлялась вызвавшему его потоку пользовательского режима.

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

Контекст прерывания

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

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

Код обслуживания прерывания может использовать области памяти, указатели на которые он может получить через внутренние переменные и ссылки собственно в драйвере. Однако следует внимательно следить за тем, чтобы используемые указатели или области памяти, на которые они указывают, не оказались в страничной памяти, поскольку это потенциально опасно и рано или поздно приведет к краху системы. В частности, следует аккуратно использовать директивы указаний компилятору типа #pragma code_seg("PAGE") и #pragma alloc_text("PAGE", MyFunctionName). При сомнениях, относительно корректности размещения кода в страничной памяти можно применять макроопределение PAGED_CODE(), которое выявит случаи входа в данный код с недопустимо высокими приоритетами (разумеется, только в отладочной версии драйвера).

Контекст программного потока режима ядра

Последний возможный вариант контекста выполнения кода драйвера состоит в том, что фрагмент кода выполняется в контексте 'отдельного потока программного кода режима ядра'.

Некоторые драйверы предпочитают создавать дополнительные программные потоки для выполнения специфических задач, связанных, например, с обслуживаем устройств, которые требуют последовательного опроса (polling), или при работе в условиях, которые продиктованы специфическими схемами организации ожидания. Эти потоки, выполняющиеся в режиме ядра, не так сильно отличаются от привычных потоков пользовательского режима, подробно описанных в книгах по Win32 программированию. Они выполняются в соответствии с решениями планировщика ядра и в соответствии с присвоенными им приоритетами. Объясняется это "простое", несмотря на режим ядра, поведение тем, что создаются такие потоки вызовом PsCreateSystemThread, который может быть сделан только из кода, работающего на уровне IRQL_PASSIVE_LEVEL. Соответственно, начинают работу они на этом же уровне IRQL среди приоритетов пользовательского режима.

Такой программный поток не имеет ни собственного TEB (Thread environment block), ни контекста пользовательского режима, хотя вполне возможно, что "ответвился" от потока, у которого такой контекст имелся. Новый программный поток работает только в режиме ядра. Он не может интерпретировать виртуальные адреса, полученные из приложений пользовательского режима — даже если он их как-то получит (например, через внутренние переменные драйвера).

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