Основы разработки прикладных виртуальных драйверов
Часть 2. Взаимодействие драйвера и приложения
Проблема взаимодействия драйвера с приложением Windows имеет два аспекта:
- Как вызвать из прикладной программы API-процедуру драйвера.
- Каким образом передать драйверу исходную информацию и получить результаты его работы.
Следует иметь в виду, что взаимодействие с драйвером приложений 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