Основы разработки прикладных виртуальных драйверов
Часть 3. Системный отладчик SoftICE и отладка виртуальных драйверов
Для исследований операционной системы Windows и, в частности, для отладки виртуальных драйверов требуется специальный отладчик, работающий на нулевом уровне привилегий в плоском адресном пространстве. Одним из таких отладчиков является инструментальный пакет NuMega SoftICE от Compuware Corporation. Отладчик SoftICE позволяет изучать работу программ, действующих на любом уровне привилегий, выводить на экран поля данных и коды системных компонентов Windows, находить соответствие между виртуальными, линейными и физическими адресами, устанавливать в отлаживаемой программе (в том числе в виртуальном драйвере) точки останова на определенных адресах, а также по аппаратным и программным прерываниям, выводить на печать фрагменты кодов прикладных и системных программ.
Отладчик SoftICE поступает к потребителю в виде дистрибутива и требует установки в операционной системе, причем для систем Windows 95/98 или Windows NT и установка, и использование отладчика осуществляются с некоторыми различиями. В развернутом виде пакет SoftICE занимает около 16 Мбайт дискового пространства и помимо основных и вспомогательных программ содержит весьма подробный интерактивный справочник.
Пользователь взаимодействует в основном с двумя программами, входящими в пакет SoftICE: WINICE.EXE и LOADER32.EXE. Первая программа устанавливается резидентно в процессе загрузки Windows; вторая — вызывается пользователем по мере необходимости. Для загрузки резидентной части SoftICE в файл AUTOEXEC.BAT следует включить строку G:\SOFTICE\WINICE.EXE в предположении, что пакет SoftICE находится в каталоге SoftICE диска G.
Запустить отладчик можно двумя способами. Первый заключается в активизации отладчика без связи с какой-либо программой; он осуществляется нажатием комбинации клавиш <Ctrl>+D. В таком режиме можно, например, просматривать содержимое таблиц дескрипторов и таблиц трансляции, изучать характеристики действующих виртуальных машин, исследовать содержимое физической памяти. Для выхода из отладчика следует ввести команду отладчика Х или еще раз <Ctrl>+D.
Второй способ заключается в запуске отлаживаемой программы под управлением отладчика. Для этого следует запустить программу LOAD32.EXE. На экране появится начальный кадр SoftICE (рис. 1), в котором надо сначала открыть файл с отлаживаемой программой с помощью самой левой кнопки, а затем загрузить его нажатием второй слева кнопки. Другие кнопки, как и меню, служат для настройки режимов работы SoftICE. Все эти операции подробно описаны в интерактивном справочнике, входящем в состав пакета; здесь мы отметим только одну полезную возможность.
С помощью пункта меню EditØ SoftICE Initialization Settings можно изменить некоторые настройки отладчика, в частности строку инициализации, определяющую исходный режим работы отладчика. По умолчанию строка инициализации состоит из одной команды X, оканчивающейся точкой с запятой. Эта команда автоматически завершает работу отладчика при его первой инициализации в процессе загрузки Windows. Если удалить команду X из строки инициализации, то при перезагрузке Windows еще до загрузки виртуальных драйверов управление будет передано отладчику.
По умолчанию рабочий кадр с программой содержит всего 25 строк текста, при этом на экран не выводятся коды команд. Поскольку при отладке программы желательно иметь на экране максимум информации, строку инициализации полезно сформировать следующим образом:
LINES=60;CODE ON;X;
В этом случае на экран будут выводиться 60 строк текста, а команды отлаживаемой программы (так же как и команды драйвера или программных составляющих Windows) — сопровождаться их кодами.
После загрузки отлаживаемой программы на экран выводится рабочий кадр отладчика с текстом отлаживаемой программы. Приведенный на рис. 2 кадр получен после загрузки программы, описанной во второй статье этого цикла (КомпьютерПресс № 4’2001), и останова ее на команде дальнего вызова виртуального драйвера (2A07:08BF в данном случае).
В верхней части кадра располагается окно регистров; средняя часть отведена под программные коды; нижняя — представляет собой окно команд. В рассматриваемом примере после загрузки программы была введена команда G 08BF — выполнения программы до указанного адреса, а затем команда LDT 2A07 — вывода элемента локальной таблицы дескрипторов, соответствующего селектору сегмента команд (который в данном сеансе оказался равен 2A07h).
Дальнейшее управление отладчиком осуществляется с помощью команд, вводимых с клавиатуры. Полный список команд с пояснениями можно найти в интерактивном справочнике; в таблице приведен перечень наиболее важных команд отладчика, иллюстрирующий его возможности.
Наиболее часто используемые команды отладчика SoftICE
Команда |
Назначение |
---|---|
BPX addr |
Установка точки останова по адресу addr |
BPINT intr |
Установка асинхронной точки останова по прерыванию с номером intr. Прерывание может быть как программным, так и аппаратным |
BL |
Вывод информации о точках останова с их номерами |
BC n |
Удаление точки останова с номером n |
BC * |
Удаление всех точек останова |
Dsize addr |
Вывод в нижнюю часть кадра содержимого ячеек виртуальной памяти — начиная с адреса addr |
PEEKsize addr |
Вывод в нижнюю часть кадра содержимого ячейки физической памяти по адресу addr |
G addr |
Выполнение программы до точки с адресом addr |
<F7> |
Выполнение программы до положения курсора |
<F8> |
Выполнение одной команды программы. Вход внутрь подпрограмм, выполнение циклов по шагам |
<F10> |
Выполнение одной команды программы. Подпрограммы и циклы выполняются как одна команда |
X |
Выполнение программы до конца с возвратом в начальный кадр SoftICE. Нажатием кнопки загрузки файла можно повторно запустить текущую программу сначала |
HBOOT |
Перезагрузка компьютера. Используется в случае зависания отладчика, что происходит (при наличии ошибок в отлаживаемой программе) довольно часто, например, при отказах страниц |
<PrintScreen> |
Вывод содержимого экрана на принтер (принтер должен иметь драйвер для работы в DOS) |
LINESn |
Установка размера экрана (в числе строк). Параметр n может принимать значения 25, 43, 50, 60 |
CODE ON/OFF |
Включение/выключение вывода на экран машинных кодов команд программы |
U addr |
Деассемблирование — вывод на экран содержимого программных кодов начиная с адреса addr |
GDT sel |
Вывод содержимого GDT. При указании селектора sel вывод одного дескриптора, соответствующего этому селектору |
LDT sel |
Вывод содержимого LDT. При указании селектора sel вывод одного дескриптора, соответствующего этому селектору |
IDT intr |
Вывод содержимого IDT. При указании номера вектора intr вывод одного дескриптора, соответствующего этому вектору |
TSS |
Вывод содержимого сегмента состояния задачи |
CPU |
Вывод содержимого регистров процессора и информации о процессоре |
VM |
Вывод характерных данных о действующих виртуальных машинах |
PHYS paddr |
Вывод всех линейных адресов, отображаемых на указанный физический адрес paddr |
I port |
Ввод данного из порта port |
O port |
Вывод данного в порт port |
R |
Возврат в отладчик после невосстановимого сбоя (например, после отказа страницы) |
Примечания к таблице:
- Адрес addr может задаваться одним смещением, если имеется в виду текущий сегмент команд или данных, или в форме seg:offs, если речь идет о другом сегменте (например, в виртуальном драйвере или в системном компоненте Windows).
- Спецификатор size указывает на формат вывода: по байтам (B), словам (W), двойным словам (D).
С помощью отладчика SoftICE нетрудно проследить механизм взаимодействия приложения с виртуальным драйвером. Рассмотрим некоторые детали этого механизма.
Как видно из рис. 2, команда перехода в виртуальный драйвер выглядит в машинных кодах следующим образом:
CS:08BF call far[0284]
В двойное слово по адресу DS:0284h, которое соответствует переменной FARPROC VxDEntry нашего приложения Windows, в ходе выполнения программы было записано то, что мы называли адресом API-процедуры виртуального драйвера. В действительности это совсем не так. При просмотре (с помощью SoftICE или обычного отладчика из пакета Borland C++) содержимого ячейки 0284h видим адрес вроде 003Bh:03ECh. Заглянув в сегмент с селектором 3Bh, мы увидим, что в нем нет ничего, кроме длинной последовательности команд int 30h, предоставляющих стандартный для Windows механизм перехода из 16-разрядного приложения защищенного режима в 32-разрядную программу уровня 0. Этот переход осуществляется через дескриптор 30h таблицы дескрипторов прерываний IDT, описывающий некоторую процедуру со смещением, например C0001B04h, в том самом сегменте с селектором 28h, в котором работает VMM. Переход с помощью команды int 30h из приложения уровня 3 в исполняемый сегмент уровня 0 возможен потому, что — хотя при загрузке в CS селектора 0028h текущий уровень привилегий CPL оказывается равен 0, что и обусловливает работу программ VMM на самом внутреннем кольце защиты — шлюз IDT содержит значение DPL=3, отчего этот шлюз оказывается доступен прикладным программам кольца 3.
Программный фрагмент VMM, на который происходит переход через вектор 30h, выполняет ряд системных проверок и настроек. В частности, выполняется процедура VMM под названием Simulate_Far_Ret, которая снимает со стека приложения два слова адреса возврата, помещенные туда командой call far, и копирует их в ячейки структуры клиента Client_CS и Client_IP. Это приводит при передаче управления из VMM в текущую виртуальную машину к «эмуляции дальнего возврата», то есть к переходу по этому адресу, как если бы программа, вызванная приложением командой call far, выполнила команду ret far.
Выполнив все эти операции, VMM командой дальнего перехода jmp far передает управление на точку входа в API-процедуру нашего драйвера, который, таким образом, продолжает работу на нулевом уровне защиты при значениях сегментных регистров CS=28h и DS=SS=ES=FS=GS=30h.
Виртуальный драйвер, выполнив свои функции, командой ret возвращает управление VMM. Тот производит некоторые завершающие действия и активизирует текущую виртуальную машину, в которой выполняется наше приложение Windows. В этот момент и происходит эмуляция дальнего возврата с передачей управления назад в прикладную программу. Все эти действия VMM по обеспечению перехода в драйвер и возврата в программу (фактически системные издержки), содержат около 100 команд. Такова цена использования в прикладной программе стандартных системных средств.
В примере предыдущей статьи цикла, на который мы уже ссылались, вывод на экран системной информации, полученной с помощью виртуального драйвера, выполнялся приложением Windows. В ряде случаев (главным образом в процессе отладки нового драйвера или при исследовании его работы), лучше не передавать данные из виртуального драйвера в вызывающее приложение, а вывести их из драйвера непосредственно на экран. Приведем пример драйвера, который при вызове его из приложения защищенного режима выводит на экран характерные данные виртуальной машины: номер виртуальной машины, ее дескриптор, а также адрес структуры клиента.
Виртуальный драйвер, выводящий на экран сообщение с некоторыми данными:
.386p .XLIST include vmm.inc include shell.inc ;Поддержка средств вывода сообщений .LIST Declare_Virtual_Device VMyD,1,0,VMyD_Control,8000h, \ Undefined_Init_Order,,API_Handler ;======================= VxD_REAL_INIT_SEG BeginProc VMyD_Real_Init ;Текст процедуры инициализации реального режима (см. статью 2 этого цикла) EndProc VMyD_Real_Init VxD_REAL_INIT_ENDS ;====================== VxD_DATA_SEG Caption db 'Message #1',0 Mesg db 'VM ID=' VMID db '**** ',13,10 db 'VM Handler=' VMHandle db '******** ',13,10 db 'Client Struct=' ClSt db '********',0 VxD_DATA_ENDS ;====================== VxD_CODE_SEG BeginProc VMyD_Control clc ret EndProc VMyD_Control ;------------------------- ;API-процедура защищенного режима BeginProc API_Handler ;Получим и выведем номер VM mov EAX,[EBX].CB_VMID;Получили номер VM (фактически в AX) mov ESI,offset32 VMID;Адрес строки call Word_ascii ;Преобразуем в символьную форму ;Получим и выведем дескриптор VM mov EAX,EBX ;Получили дескриптор VM mov ESI,offset32 VMH+4;Адрес строки call Word_ascii ;Преобразуем в символьную форму shr EAX,16 ;Сдвинем старшую половину данного в AX mov ESI,offset32 VMH;Адрес строки call Word_ascii ;Получим и выведем адрес структуры клиента mov EAX,[EBX].CB_Client_Pointer mov ESI,offset32 ClSt+4;Адрес строки call Word_ascii ;Преобразуем в символьную форму shr EAX,16 ;Сдвинем старшую половину данного в AX mov ESI,offset32 ClSt;Адрес строки call Word_ascii ;Преобразуем в символьную форму call Info ;Выведем всю строку на экран ret EndProc API_Handler ;--------------------------- ;Процедура вызова функции вывода сообщения BeginProc Info mov EAX,MB_OK ;Константа формата сообщения mov ECX,offset32 Mesg;Адрес выводимой строки mov EDI,offset32 Caption;Заголовок сообщения драйвера VxdCall SHELL_Sysmodal_Message ret EndProc Info ;---------------------------- BeginProc Word_ascii push AX ;Сохраним исходное данное and AX,0F000h ;Выделим старшую четверку битов shr AX,12 ;Сдвиг вправо до начала регистра call Bin_ascii ;Преобразуем в символ mov byte ptr [ESI],AL;Отправим его в строку inc ESI ;Сдвиг по строке символов pop AX ;Вернем в AX исходное данное push AX ;Сохраним его and AX,0F00h ;Выделим следующую четверку битов shr AX,8 ;Сдвиг вправо до начала регистра call Bin_ascii ;Преобразуем в символ mov byte ptr [ESI],AL;Отправим его в строку inc ESI ;Сдвиг по строке символов pop AX ;Вернем в AX исходное данное push AX ;Сохраним его and AX,0F0h ;Выделим следующую четверку битов shr AX,4 ;Сдвиг вправо до начала регистра call Bin_ascii ;Преобразуем в символ mov byte ptr [ESI],AL; Отправим его в строку inc ESI ;Сдвиг по строке символов pop AX ;Вернем в AX исходное данное and AX,0Fh ;Выделим младшую четверку битов call Bin_ascii ;Преобразуем в символ mov byte ptr [ESI],AL;Отправим его в строку ret EndProc Word_ascii ;----------------------------- BeginProc Bin_ascii cmp AL,9 ;Цифра > 9? ja letter ;Да, на преобразование в символы A...F add AL,30h ;Нет, преобразуем в символ 0...9 jmp ok ;И на выход letter: add AL,37h ;Преобразуем в символы A...F ok: ret EndProc Bin_ascii VxD_CODE_ENDS end VMyD_Real_Init
Вывод сообщения из драйвера на экран осуществляется функцией SHELL_Sysmodal_Message, вызов которой в рассматриваемом примере оформлен в виде процедуры Info. Функция SHELL_Sysmodal_Message вызывается с помощью макроса VxDCall, определенного в файле VMM.INC. В качестве параметров функции указывается адрес символьной строки, выводимой на экран (в регистре ECX), а также адрес строки с заголовком сообщения (в EDI). Функция требует наличия в регистре EBX дескриптора виртуальной машины, однако при вызове драйвера из приложения этот дескриптор заносится в EBX автоматически, и о нем можно не заботиться. В регистре EAX с помощью различных символьных констант можно указать формат сообщения. Все эти константы определены в файле SHELL.INC, который необходимо подключить к исходному тексту драйвера директивой include. Константа MB_OK, использованная в примере, предполагает продолжение работы драйвера (в частности, возврат в вызвавшее драйвер приложение) после нажатия пользователем клавиши <Enter>; при указании других констант можно организовать диалог драйвера с пользователем. Например, при использовании в качестве параметра константы MB_YESNO в сообщение драйвера включается указание на альтернативное продолжение по нажатии клавиш Y (Да) или N (Нет). Реакция драйвера на ту или иную команду предусматривается в его тексте таким образом:
BeginProc Info mov EAX,MB_YESNO ;Константа формата сообщения mov ECX,offset32 Mesg;Адрес выводимой строки mov EDI,offset32 Caption;Заголовок сообщения драйвера VxdCall SHELL_Sysmodal_Message cmp EAX,IDYES je yyy ;Переход при нажатии клавиши Y jmp nnn ;Переход при нажатии клавиши N ret EndProc Info
Строка Mesg, составляющая содержание выводимого на экран сообщения, для удобства заполнения ее конкретными данными разбита с помощью последовательности директив db на несколько символьных полей. Тем полям, в которые предполагается поместить данные, присвоены имена (VMID, VMHandle и ClSt). Начальное заполнение этих полей знаком «*» (звездочка) может помочь в отладке программы драйвера.
Для преобразования данных, представленных в виде двоичных чисел, в символьные строки в состав драйвера включены две подпрограммы. Процедура Bin_ascii преобразует четверку битов в символ шестнадцатеричной системы счисления; процедура Word_ascii преобразует двухбайтовое число, находящееся в регистре AX, в символьную форму (с использованием для преобразования каждой четверки битов подпрограммы Bin_ascii) и заносит его в память по адресу, находящемуся в регистре ESI. Обе эти процедуры оформлены с помощью макросов BeginProc и EndProc.
Дескриптор виртуальной машины, который мы хотим вывести на экран, при вызове драйвера помещается системой в регистр EBX, а остальные интересующие нас данные входят в состав управляющего блока, адресом которого как раз и является дескриптор VM. Все эти данные имеют определенные символические обозначения (см. первую статью этого цикла: КомпьютерПресс №3’2001). Заполнение строки Mesg данными в символьной форме начинается в программе с идентификатора (номера) VM. Этот номер формально занимает в блоке управления двойное слово, однако фактически находится в младшей половине этого слова. После переноса номера VM из структуры, адресуемой через EBX, в регистр EAX в ESI загружается адрес поля VMID в строке Mesg и вызывается процедура Word_ascii, выполняющая преобразование номера VM в символьную форму и занесение результата преобразования в четыре байта поля VMID.
Далее в регистр EAX загружается дескриптор VM (из регистра EBX), в регистр ESI — адрес VMH+4 правой четверки байтов поля VMH, после чего вызовом процедуры Word_ascii (которая работает с регистром AX, то есть с младшей половиной дескриптора) эта младшая половина помещается на свое место в символьной строке. Старшая половина дескриптора командой shr сдвигается в регистр AX, преобразуется в символы и заполняет левую четверку байтов поля VMH, в результате чего вы наблюдаем на экране 32-битовое число в привычном для нас виде (старшие цифры слева, младшие — справа).
Аналогично выполняется преобразование адреса структуры клиента. Последней командой процедуры API_Handler вызывается подпрограмма Info, которая выводит сформированную строку Mesg на экран. Сообщение драйвера выглядит приблизительно так, как показано на рис. 3.
Для того чтобы процедураAPI_Handler начала выполняться, ее следует вызывать из приложения Windows. Возможный текст такого приложения приведен ниже. Оно имеет, можно сказать, вырожденный характер, так как в нем не формируется никаких окон или других изобразительных элементов Windows, а только определяется адрес драйвера и выполняется его вызов, и затем для контроля хода программы на экран выводится окно сообщения с текстом «Драйвер вызван».
Приложение Windows для вызова виртуального драйвера:
#define STRICT #include <windows.h> #include <windowsx.h> void GetAPIEntry(); FARPROC VxDEntry; int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){ GetAPIEntry(); VxDEntry(); MessageBox(NULL,"Драйвер вызван","Info",MB_ICONINFORMATION); return 0; } void GetAPIEntry(){ asm{ mov AX,0x1684 mov BX,0x8000 int 0x2F mov word ptr VxDEntry,DI mov word ptr VxDEntry+2,ES } }
КомпьютерПресс 5'2001