Основы разработки прикладных виртуальных драйверов

Часть 2. Взаимодействие драйвера и приложения

К.Г.Финогенов

Проблема взаимодействия драйвера с приложением Windows имеет два аспекта:

  1. Как вызвать из прикладной программы API-процедуру драйвера.
  2. Каким образом передать драйверу исходную информацию и получить результаты его работы.

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

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

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

Вызывать драйвер можно из приложения, написанного на любом языке, в частности на языке ассемблера. Однако приложения Windows на языке ассемблера оказываются чрезвычайно громоздкими; их составление требует от программиста не только понимания многочисленных внутренних алгоритмов системы Windows, но и освоения целого ряда специфических (и относительно редко используемых) средств и приемов программирования на языке ассемблера, без которых написать приложение Windows затруднительно. (Желающих ознакомиться с принципами составления на языке ассемблера прикладных программ для системы Windows адресую к 3-му изданию книги автора «Основы языка ассемблера (учебный курс)», М.: Радио и связь, 2001, которое в настоящее время готовится к печати.)

Гораздо проще написать приложение Windows на языке С (или С++). Разумеется, для разработки красивого современного приложения со сложным интерфейсом (меню, диалоговые окна, кнопки, курсоры, рисунки и пр.) необходимо достаточно свободно владеть всем огромным арсеналом изобразительных средств Windows, однако здесь мы ограничимся относительно простыми программами с минимумом интерфейсных деталей. Краткие пояснения помогут читателю понять суть дела. (Более основательно изучить программирование для Windows на языке С++ можно по книге автора «Прикладное программирование для Windows на Borland C++», М: Принтер, 1999.)

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

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

Готовить и отлаживать 16-разрядные приложения Windows проще всего в интегрированной среде Borland C++ версии 4.5. Для работы с 32-разрядными приложениями лучше использовать пакеты Borland C++ 5.0 или Microsoft Visual C++.

 

Текст приложения Windows, служащего для вызова виртуального драйвера, получения от него некоторой информации и вывода этой информации на экран:

#define STRICT        //(1)Более строгая проверка типов переменных  
#include <windows.h>  //(2)Два файла с определениями констант, макросами   
#include <windowsx.h> //(3)и прототипами функций Windows  
void GetAPIEntry();   //(4)Прототип функции  
FARPROC VxDEntry;     //(5)Объявление переменной типа FARPROC   
struct DESCR{         //(6)Описание структуры, повторяющей состав дескриптора  
  WORD lim;           //(7)Граница (биты 0...15)  
  WORD base_l;        //(8)База, биты 0...15  
  BYTE base_m;        //(9)База, биты 16...23  
  BYTE attr_1;        //(10)Байт атрибутов 1  
  BYTE attr_2;        //(11)Граница (биты 16...19) и атрибуты 2  
  BYTE base_h;        //(12)База, биты 24...31  
  }dscr;              //(13)Объявление переменной dscr типа структуры DESCR  
//Главная функция WinMain  
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){//(14)  
  char txt[160];      //(15)Объявление текстовой строки  
  GetAPIEntry();      //(16)Вызов функции GetAPIEntry,   
                      //получение точки входа в драйвер  
  _DI=OFFSETOF(&dscr);//(17)В драйвер будет передано смещение dscr  
  VxDEntry();         //(18)Вызов API-процедуры драйвера  
  wsprintf(txt,'Базовый линейный адрес = %X%X%Xh'//(19)Подготовка строки  
     '\nАтрибут 1 = %Xh   Атрибут 2 = %Xh',//с текстом и данными  
     '\nРазмер сегмента = %Xh=%d байт',//для вывода на экран  
     dscr.base_h,dscr.base_m,dscr.base_l,  
     dscr.attr_1,dscr.attr_2,++dscr.lim,dscr.lim);  
  MessageBox(NULL,txt,'Info',MB_ICONINFORMATION);//(20)Вывод сообщения  
  return 0;           //(21)Завершение главной функции  
  }                   //(22)Конец главной функции  
//Функция GetAPIEntry() получения точки входа в драйвер  
void GetAPIEntry(){   //(23)Описание функции GetAPIEntry  
  asm{                //(24)Вставка на языке ассемблера  
        mov AX,0x1684 //(25)Номер функции   
        mov BX,0x8000 //(26)Идентификатор драйвера  
        int 0x2F      //(27)Мультиплексное прерывание  
        mov word ptr VxDEntry,DI//(28)Смещение точки входа в драйвер  
        mov word ptr VxDEntry+2,ES//(29)Селектор точки входа в драйвер  
        }             //(30)Конец ассемблерной вставки  
  }                   //(31)Конец функции GetAPIEntry 

При запуске приложения Windows управление всегда передается главной функции WinMain(), которая, таким образом, должна присутствовать в любой программе. Помимо главной функции в программе может быть любое число вспомогательных функций-подпрограмм, вызываемых в тех или иных точках главной функции. В нашей программе имеется одна такая функция-подпрограмма — GetAPIEntry(), служащая для определения адреса точки входа в драйвер. Для того чтобы по ходу изложения было понятно, о каком объекте идет речь (о функции или о переменной), мы будем сопровождать обозначения функций парой круглых скобок (), подчеркивая тем самым, что этому объекту в принципе присущи параметры.

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

#define STRICT 

объявляет имя STRICT определенным (известным). В этом случае компилятор осуществляет более строгую проверку типов данных, используемых в программе, что дает возможность выявить и устранить многие ошибки еще на стадии компиляции. Для нашей программы, в которой используется минимальное число типов (BYTE, WORD, int и некоторые другие), эта проверка значения не имеет, однако в более сложных программах она весьма полезна.

В предложениях 2 и 3 к исходному тексту программы подсоединяются заголовочные файлы windows.h и windowsx.h, в которых содержатся описания производных типов данных, констант, макросов и прототипов функций Windows. Например, использованные в программе типы данных BYTE и WORD являются типами не классического языка С, а системы программирования для Windows. В языке С им соответствуют типы unsigned char и unsigned short int. Определения специфических типов Windows даны в файле windows.h. Там же можно найти определение использованного нами типа FARPROC как дальнего указателя на функцию, не требующую параметров и не возвращающую результат.

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

float Func(int,int); 

Для функций, определенных в прикладной программе, прототип указывается в начале программы, а чаще — в «персональном» заголовочном файле, принадлежащем этой программе. Прототипы же стандартных функций Windows, в частности функций WinMain() и MessageBox(), используемых в нашей программе, приведены в файле windows.h (а также в других включаемых файлах с расширением .h). Следовательно, включение в программу файла windows.h обязательно.

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

В предложении 4 определяется прототип прикладной функции GetAPIEntry(), которая служит для получения адреса точки входа в драйвер. Эта функция не требует никаких параметров и не возвращает результат (полученный ею адрес драйвера она запишет непосредственно в поля данных, в переменную VxDEntry). Отсутствие параметров обозначается в прототипе пустой парой скобок, а отсутствие возвращаемого результата — словом void перед именем функции. Определение прототипа, как и любой оператор языка С, заканчивается точкой с запятой (это правило не относится к операторам препроцессора).

В предложении 5 объявляется переменная VxDEntry типа FARPROC, то есть типа дальнего указателя на функцию. Действительно, API-процедура драйвера выступает в нашей программе в качестве вызываемой функции, причем она, безусловно, не входит в состав программы и находится в другом сегменте, поэтому ее адрес должен быть дальним. Оба объекта — и прототип функции, и переменная VxDEntry — должны быть указаны перед началом главной функции WinMain().

Предложения 6...13 описывают структуру, соответствующую составу дескриптора памяти. Эта структура не является необходимой (вместо нее можно было просто ввести в программу массив из 8 однобайтовых переменных), однако использование структурной переменной несколько повышает наглядность операций с отдельными полями дескриптора. Слово DESCR является придуманным нами типом этой (также придуманной нами) структуры, имя же конкретной переменной, для которой отводится место в памяти, указывается после заключительной фигурной скобки определения структуры. В данном случае эта переменная носит имя dscr. Стоит отметить, что в этом примере в одной конструкции языка объединяются и определение состава новой структуры DESCR, и объявление конкретной структурной переменной dscr. Часто эти действия разделяют, сначала определяя тип структуры и только далее, в нужном месте программы, объявляя одну или несколько структурных переменных этого типа.

Переменные, описанные перед именем главной функции программы, называются глобальными. Это означает, что к ним можно обращаться из любых точек программы, в частности, из ее внутренних функций (переменные, объявленные внутри функции, являются локальными; они доступны лишь из этой функции и не видны не только во внешних функциях, но даже во внутренних, вызываемых из данной функции). Полезно помнить, что глобальные переменные размещаются в сегменте данных программы, адресуемом через сегментный регистр DS, а под локальные переменные выделяется место на стеке и доступ к ним осуществляется через сегментный регистр SS. Правда, в большинстве случаев содержимое этих регистров совпадает; под все данные выделяется один сегмент памяти, в начале которого (в области меньших адресов) располагаются глобальные переменные, а в конце (в области больших адресов) — стек. Однако так бывает не всегда, и в нашем случае, где переменная dscr должна быть доступна драйверу, лучше определить ее как глобальную, чтобы компилятор разместил ее в сегменте, адресуемом через DS.

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

int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int nCmdShow) 

Здесь предполагается, что функция WinMain() будет использовать в процессе своего выполнения первый и последний из переданных ей системой параметров, а второй и третий — не будет.

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

В предложении 15 объявляется символьная строка — массив, состоящий из 160 переменных типа char. Впоследствии эта пока пустая строка будет заполнена упорядоченными данными из структуры dscr и использована для их отображения на экране.

С предложения 16 начинается собственно текст программы. Прежде всего вызовом

GetAPIEntry(); 

в переменную VxDEntry заносится полученный из Windows адрес точки входа в API-процедуру драйвера. Состав функции GetAPIEntry() и смысл возвращаемого ею значения мы рассмотрим чуть позже, а на этом этапе удовольствуемся полученным результатом.

Получив адрес точки входа в драйвер, мы сможем вызвать его API-процедуру, которая должна вернуть 8-байтовое значение дескриптора памяти. Получить данные из виртуального драйвера (как и передать ему исходные данные) можно разными способами. В настоящей программе через регистры DS:DI драйверу передается полный адрес структурной переменной dscr, а драйвер, получивший в процессе выполнения своей API-процедуры искомый дескриптор, записывает его значение непосредственно в переменную dscr приложения. Регистр DS уже содержит сегментный адрес сегмента данных программы; в предложении 17 в регистр DI с помощью макроса OFFSETOF заносится смещение структурной переменной dscr. В языке С предусмотрена возможность непосредственно обращаться ко всем регистрам общего назначения с помощью обозначений _AX, _AL, _DI и т.д. Следует обратить внимание на обозначение &ptr. Оператор &, находящийся перед именем переменной, обозначает ее адрес. Макрос OFFSETOF позволяет выделить младшую часть этого адреса, то есть смещение.

Подготовив для драйвера исходные данные, вызовем его API-процедуру. Это делается в предложении 18, где адрес точки входа в драйвер VxDEntry рассматривается как имя дальней функции. После возврата из драйвера переменная dscr заполнена искомыми данными, и нам остается лишь вывести на экран ее содержимое в разумном виде. Это действие выполняется в два этапа: сначала с помощью функции Windows wsprintf() значения полей переменной dscr преобразуются в символьную форму и помещаются в строку txt; затем эта строка выводится на экран в окно сообщения с помощью функции Windows MessageBox().

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

MessageBox(NULL,'Драйвер отработал','Контроль', MB_ICONINFORMATION);  

Если, однако, требуется вывести значения каких-либо числовых переменных, то прежде эти значения следует преобразовать в символьную форму, для чего служит функция wsprintf(). Для данной функции первый параметр является адресом строки-приемника, второй — адресом строки-источника (или самой строкой, как в нашем примере), третий — преобразуемой переменной (или списком переменных, если их несколько). В строке-источнике кроме текста могут присутствовать спецификации формата, определяющие, по какому формату будут преобразовываться в символьные строки, указанные в последнем параметре данные. Число указанных переменных (в нашем случае их 7: dscr.base_h, dscr.base_m, dscr.base_l, dscr.attr_1, dscr.attr_2 и два раза dscr.lim) должно совпадать с числом спецификаций формата. Все спецификации формата начинаются со знака %.

В нашем примере сначала из трех составляющих линейного адреса (dscr.base_h, dscr.base_m и dscr.base_l) формируется изображение одного длинного числа в шестнадцатеричной форме, далее выводятся оба атрибута сегмента и, наконец, его длина сначала в шестнадцатеричной форме (%Xh), а затем в десятеричной (%d). Напомним, что в дескрипторе указывается граница сегмента, которая на 1 байт меньше его длины; для вывода на экран длины сегмента переменная dscr.lim включена в список переменных функции wsprintf() с префиксным оператором ++, который сначала увеличивает значение переменной на 1 и лишь затем передает ее по назначению. Второе включение той же переменной в список (уже без префиксного оператора) позволяет вывести в окно сообщения ее значение еще раз в другом (в данном случае десятичном) формате.

Функция Windows MessageBox(), выведя на экран окно сообщения, ожидает реакции пользователя; приложение продолжит свою работу после того, как пользователь щелкнет по клавише ОК. В нашем примере вслед за вызовом функции MessageBox() стоит оператор return, который завершает работу приложения.

Рассмотрим теперь подпрограмму GetAPIEntry(), которая позволяет получить адрес точки входа в драйвер. Для взаимодействия с виртуальными драйверами в Windows предусмотрен ряд функций мультиплексного прерывания 2Fh. Например, с помощью функции 1681h можно сообщить драйверу о начале критической секции программы, а с помощью функции 1682h — о ее завершении. Для получения адреса API-процедуры используется функция 1684h: в регистре BX ей следует передать идентификационный номер нашего виртуального драйвера. Вызвать мультиплексное прерывание 2Fh (как и любое другое программное прерывание) можно с помощью функции C int86x, однако проще и нагляднее это сделать путем использования ассемблерной вставки. В функции GetAPIEntry после заполнения регистров AX и BX исходными значениями и после вызова прерывания 2Fh результат выполнения функции 1684h, возвращаемый в регистрах ES:DI, отправляется непосредственно в предусмотренную нами переменную VxDEntry.

Результат выполнения нашей программы (предположим, что мы уже написали и установили соответствующий ему виртуальный драйвер) приведен на рис. 1.

А теперь перейдем к тексту виртуального драйвера, работающего в паре с рассмотренной выше программой.

 

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

.386p  
.XLIST  
include vmm.inc  
.LIST  
Declare_Virtual_Device VMyD,1,0,VMyD_Control,8000h, \  
    Undefined_Init_Order,,API_Handler  
;Определим структуру descr, описывающую состав дескриптора  
descr   struc  
lim     dw 0  
base_l  dw 0  
base_m  db 0  
attr_1  db 0  
attr_2  db 0  
base_h  db 0  
descr   ends  
;==============Сегмент инициализации реального режима===============  
VxD_REAL_INIT_SEG  
;Процедура инициализации реального режима  
BeginProc VMyD_Real_Init  
        mov        AH,09h  
        mov        DX,offset msg  
        int        21h  
        mov        ax, Device_Load_Ok  
        xor        bx,bx  
        xor        si,si   
        xor        edx,edx  
        ret  
msg db 'Виртуальный драйвер VmyD загружен'  
EndProc VMyD_Real_Init  
VxD_REAL_INIT_ENDS  
;===============Сегмент данных=======================================  
VxD_DATA_SEG  
pdescr  df 0          ;Поле для псевдодескриптора  
d       descr <>      ;Поле для дескриптора  
VxD_DATA_ENDS  
;===============Сегмент защищенного режима===========================  
VxD_CODE_SEG  
;Управляющая процедура для обработки системных сообщений Windows  
BeginProc VMyD_Control  
        clc           ;Мы не обрабатываем   
        ret           ;сообщения Windows  
EndProc VMyD_Control  
;Процедура, вызываемая из приложения защищенного режима (приложения Windows)  
BeginProc API_Handler  
;Получим из GDT дескриптор LDT  
        sgdt        pdescr        ;(1)Линейный адрес и граница GDT  
        mov        ESI,dword ptr pdescr+2;(2)Линейный адрес GDT  
        sldt        DX        ;(3)EDX=Селектор LDT  
        mov        EAX,[EDX][ESI];(4)Извлечем из GDT 1-ю половину дескриптора LDT  
        mov        dword ptr d,EAX;(5)Отправим ее в d  
        mov        EAX,[EDX+4][ESI];(6)Извлечем из GDT 2-ю половину дескриптора LDT  
        mov        dword ptr d+4,EAX;(7)Отправим ее в d  
;Извлечем из дескриптора LDT линейный адрес LDT  
        mov        EDX,dword ptr d.base_l;(8)3 байт базы + атрибут 1  
        and        EDX,0FFFFFFh;(9)Уберем атрибут 1  
        movzx        EAX,byte ptr d.base_h;(10)Старший байт базы в EAX  
        shl        EAX,24        ;(11)Сдвинем его в старший байт EAX  
        or        EDX,EAX        ;(12)EDX=линейный адрес LDT  
;Получим из приложения селектор сегмента команд и найдем его дескриптор в LDT  
        movzx        ECX,[EBP.Client_CS];(13)Селектор из CS приложения  
        and        ECX,0FFFFFFF8h;(14)Уберем 3 младших бита  
        mov        EAX,[EDX][ECX];(15)Извлечем из GDT 1-ю половину дескриптора приложения  
        mov        dword ptr d,EAX;(16)Отправим ее в d  
        mov        EAX,[EDX+4][ECX];(17)Извлечем из GDT 2-ю половину дескриптора   
        mov        dword ptr d+4,EAX;(18)Отправим ее в d  
;Перешлем полученный дескриптор из d в приложение  
        Client_Ptr_Flat EDI,DS,DI;(19)EDI=линейный адрес переменной d приложения  
        mov        EAX,dword ptr d;(20)1-я половина дескриптора  
        mov        [EDI],EAX        ;(21)Отправим ее в приложение  
        mov        EAX,dword ptr d+4;(22)2-я половина дескриптора  
        mov        [EDI+4],EAX        ;(23)Отправим ее в приложение  
        ret           ;(24)Возврат через Windows в приложение  
EndProc API_Handler  
VxD_CODE_ENDS  
end VMyD_Real_Init  

Общая структура драйвера почти не отличается от той, что была рассмотрена в первой части этого цикла. В блоке описания драйвера указана лишь одна API-процедура — процедура защищенного режима, так как мы не собираемся вызывать драйвер из программ DOS. Эта единственная процедура для краткости названа просто API_Handler. В сегменте данных появились поля для данных, используемых драйвером в процессе его работы. Поле pdescr размером 6 байт служит для размещения в нем содержимого регистра процессора GDTR, в котором, как известно, хранятся линейный адрес и граница таблицы глобальных дескрипторов GDT. Второе поле d представляет собой структуру, полностью совпадающую со структурой dscr в приложении Windows и служащую для помещения в нее содержимого найденного дескриптора.

В одном из элементов глобальной таблицы дескрипторов GDT хранится системный дескриптор, описывающий локальную таблицу дескрипторов LDT данной задачи (рис. 2). Селектор этого дескриптора хранится в регистре LDTR и может быть прочитан оттуда командой sldt. Линейный адрес (вместе с границей) глобальной таблицы дескрипторов хранится в регистре GTDR, откуда его можно получить с помощью команды sgdt. Содержимое сегментного регистра CS нашего приложения представляет собой селектор, указывающий на тот дескриптор в составе LDT, который описывает сегмент команд приложения. В этом дескрипторе и находятся интересующие нас данные, в частности линейный адрес сегмента команд и его предел. Приведенные на рис. 2 значения относятся к конкретному сеансу и носят случайный характер, хотя некоторые характеристики этих значений отражают закономерности системы Windows. Так, таблица глобальных дескрипторов вместе со многими другими системными компонентами располагается в 4-м гигабайте линейного адресного пространства, который начинается с адреса C0000000h, а 16-разрядные прикладные программы — в 3-м гигабайте, с адреса 80000000h.

Вернемся к рассмотрению алгоритма API-процедуры виртуального драйвера (для удобства ссылок строки этой процедуры пронумерованы). В предложении 1 командой sgdt читается и запоминается в поле pdescr содержимое регистра GDTR. В байтах 0...1 этого регистра хранится значение границы GDT, в байтах 2...5 — базовый адрес. В предложении 2 базовый адрес извлекается из pdescr и запоминается в регистре ESI. Далее командой sldt в регистр DX заносится содержимое регистра LDTR, в котором хранится селектор LDT (в конкретном сеансе, показанном на рис. 2, он оказался равен 00F8h). Селектор представляет собой смещение в байтах соответствующего дескриптора от начала таблицы; поскольку каждый дескриптор занимает 8 байт, то дескриптор, описываемый селектором 00F8h, занимает F8h/8=1Fh=31-е место в таблице GDT (если отсчет элементов GDT начинать, как это и полагается, от 0).

Линейный адрес интересующего нас дескриптора равен сумме базового адреса GDT и относительного адреса дескриптора в указанной  таблице. В нашей программе эти составляющие находятся в регистрах ESI и EDX (при заполнении регистра DX командой sldt автоматически очищается старшая половина EDX). В предложениях 4...7 две половины дескриптора переносятся в поле данных d.

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

Интересно отметить, что таблица локальных дескрипторов, как и 16-разрядное приложение Windows, располагается в 3-м гигабайте линейных адресов и имеет довольно большой размер – 3000h байт = 12 Кбайт.

В дескрипторе LDT нас интересует только линейный базовый адрес, который придется складывать из его составляющих в байтах 7, 4, 3 и 2 дескриптора. В предложении 8 байты 5...2 дескриптора загружаются в регистр EDX, а в предложении 9 командой and в нем очищается старший байт (в который попал байт атрибутов 1). Далее старший байт базы загружается в регистр EAX, сдвигается влево до конца регистра и командой or дописывается в EDX, где уже находятся три младших байта базы.

Смещения в LDT к дескрипторам сегментов памяти приложения (сегментов команд, данных и стека) определяются значениями селекторов этих сегментов, хранящихся в сегментных регистрах приложения. Однако, как уже отмечалось ранее, при переходе в виртуальный драйвер регистры процессора теряют те значения, которые они имели в приложении. Для получения «прикладных» значений регистров необходимо воспользоваться структурой клиента. В нашем примере структура клиента используется для получения драйвером селектора сегмента команд из регистра CS; передача результирующих данных в приложение осуществляется другим способом.

В предложении 13 API-процедуры селектор сегмента команд загружается в регистр ECX (с обнулением старшей половины регистра). Однако значение селектора не равно смещению в таблице дескрипторов, так как в трех его младших битах содержится дополнительная информация: индикатор таблицы дескрипторов TI и запрошенный уровень привилегий RPL (рис. 4).

В нашем случае селектор сегмента команд имеет значение 239Fh; установленный бит 2 говорит о принадлежности этого селектора таблице локальных (а не глобальных) дескрипторов, а установленные биты 0 и 1 определяют уровень привилегий приложения, который, разумеется, равен 3. Для получения смещения следует обнулить три младших бита селектора, что и выполняется в предложении 14. К этому моменту в регистре EDX находится линейный адрес LDT, а в регистре ECX — смещение к интересующему нас дескриптору. В предложениях 15...18 искомый дескриптор сегмента команд приложения переносится в то же поле данных d, которое мы ранее использовали для хранения дескриптора LDT.

Нам осталось переслать 8-байтовое поле d в приложение. Для этого можно воспользоваться структурой клиента, занеся в 4 элемента этой структуры (например, в элементы Client_AX, Client_BX, Client_CX и Client_DX) четыре четверти дескриптора, а после возврата в приложение извлечь дескриптор по частям из регистров AX, BX, CX и DX. Однако мы поступим иначе, передав весь дескриптор непосредственно в поле dscr сегмента данных приложения. Это возможно сделать, так как виртуальный драйвер работает в плоской модели памяти и ему доступны все 4 Гбайт линейных адресов, в том числе и виртуальные адреса, назначенные системой приложению. Для перевода виртуального адреса в линейный в файле VMM.INC предусмотрен макрос Client_Ptr_Flat, в качестве параметров которого указывается 32-разрядный регистр-приемник, а также сегментный регистр и какой-либо регистр общего назначения, содержащие пару сегмент:смещение (для приложения реального режима) или селектор:смещение (для приложения защищенного режима).

Имея в виду это средство, перед вызовом драйвера мы занесли в регистр DI смещение переменной dscr; сегментный адрес сегмента данных приложения находится, естественно, в регистре DS. В предложении 19 API-процедуры драйвера в помощью макроса Client_Ptr_Flat в регистр EDI помещается линейный адрес переменной d, а в четырех последующих предложениях две половины дескриптора переносятся в поле dscr с использованием в качестве перевалочного пункта регистра EAX.

Команда ret, завершающая API-процедуру, передает управление через Windows в ту точку приложения, откуда был вызван драйвер.

Вернемся еще раз к макросу Client_Ptr_Flat. Каким образом он преобразует виртуальный адрес в линейный? Ведь макрос представляет собой просто некоторую последовательность директив или команд, имеющую имя и допускающую настройку под конкретные значения параметров. Однако никакими арифметическими или логическими операциями невозможно получить линейный адрес из величин селектор:смещение. Для определения линейного адреса по значению селектора необходимо обратиться к таблице локальных дескрипторов, найти в ней элемент, соответствующий данному селектору, извлечь из этого элемента составляющие базового линейного адреса сегмента и прибавить к нему смещение. Между прочим, именно эту операцию мы и выполняли в нашем примере виртуального драйвера. Следовательно, макрос Client_Ptr_Flat должен для образования линейного адреса обращаться к каким-то специальным системным средствам. Действительно, в тексте макроса имеется обращение к системной функции Map_Flat, которая извлекает из таблиц дескрипторов линейные адреса, выполняя, в сущности, те же операции, что и наша API-процедура. Для получения интересующего нас линейного адреса сегмента команд приложения вполне можно было бы воспользоваться этой функцией, существенно сократив размер API-процедуры. Правда, в этом случае мы не получили бы полной информации о составе дескриптора, а именно — значений границы и атрибутов.

КомпьютерПресс 4'2001


Наш канал на Youtube

1999 1 2 3 4 5 6 7 8 9 10 11 12
2000 1 2 3 4 5 6 7 8 9 10 11 12
2001 1 2 3 4 5 6 7 8 9 10 11 12
2002 1 2 3 4 5 6 7 8 9 10 11 12
2003 1 2 3 4 5 6 7 8 9 10 11 12
2004 1 2 3 4 5 6 7 8 9 10 11 12
2005 1 2 3 4 5 6 7 8 9 10 11 12
2006 1 2 3 4 5 6 7 8 9 10 11 12
2007 1 2 3 4 5 6 7 8 9 10 11 12
2008 1 2 3 4 5 6 7 8 9 10 11 12
2009 1 2 3 4 5 6 7 8 9 10 11 12
2010 1 2 3 4 5 6 7 8 9 10 11 12
2011 1 2 3 4 5 6 7 8 9 10 11 12
2012 1 2 3 4 5 6 7 8 9 10 11 12
2013 1 2 3 4 5 6 7 8 9 10 11 12
Популярные статьи
КомпьютерПресс использует