oldi

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

Часть 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

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

Примечания к таблице:

  1. Адрес addr может задаваться одним смещением, если имеется в виду текущий сегмент команд или данных, или в форме seg:offs, если речь идет о другом сегменте (например, в виртуальном драйвере или в системном компоненте Windows).
  2. Спецификатор 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