Cамоучитель по Assembler

         

Часть IV. Отладка, анализ кода программ, драйверы

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

Глава 1. Структура исполняемых модулей

Исполняемым форматом в Windows является формат PE. Сокращение PE означает Portable Executable, т.е. переносимый исполняемый формат. Этот формат имеют как ЕХЕ-файлы, так и динамические библиотеки. Важно, что сейчас фирма Microsoft ввела "новый" формат и для объектных модулей - это COFF-формат (COFF - Common Object File Format), который, однако, на поверку оказался, в сущности, все тем же PE-форматом. Заметим в этой связи, что фирма Borland по-прежнему работает с объектными файлами, имеющими структуру OMF (Object Module Format). Старый NE-формат (NE - New Executable), используемый старой операционной системой Windows и рассчитанный на сегментную структуру памяти, ушел в небытие. Кроме того, есть еще формат VXD-драйверов - LE-формат (Linear Executable, т.е. линейный исполняемый). Этого формата мы коснемся, когда будем говорить о драйверах в операционной системе Windows. Таким образом, данная глава будет посвящена разбору структуры исполняемых РЕ-модулей. Поскольку в состав исполняемого РЕ-модуля входит и DOS-программа (STUB), мы начнем наше рассмотрение со структуры DOS-программ. Наше рассмотрение будет кратким, и мы воспользуемся таблицей из книги [1].


Структура ЕХЕ-программ для MS DOS

Смещение Длина Название Комментарий
+0 2 MZ подпись, признак ЕХЕ-программы
+2 2 PartPag длина неполной последней страницы
+4 2 PageCnt длина в страницах (512 б), включая заголовок и последнюю страницу
+6 2 ReloCnt число элементов в таблице перемещения
+8 2 HdrSize длина заголовка в параграфах
+0AН 2 MinMem минимум требуемой памяти за концом программы
+0CH 2 MaxMem максимум требуемой памяти за концом программы
+0EН 2 ReloSS сегментный адрес стека
+10H 2 EXESp значение регистра SP
+12Н 2 ChkSum контрольная сумма
+14Н 2 ExeIP значение регистра IP
+16H 2 ReloCS сегментный адрес кодового сегмента
+18H 2 TablOff смещение в файле первого элемента таблицы перемещения
+1АН 2 Overlay номер оверлея, 0 для главного модуля
* Конец форматированной порции заголовка **
+1СН
** Начало таблицы перемещения (возможно с 1СН) **
+? 4*? смещ. сегмент ... смещ. сегмент

Рис. 4.1.1. Структура ЕХЕ-программы для MS DOS.

Более подробно разбор заголовка DOS-программы можно найти в [1]. Добавлю только, что сразу за таблицей перемещения начинается исполняемая часть модуля. Таблица же перемещения используется для того, чтобы при загрузке настроить адреса. Но это лишь в том случае, если в программе используются адреса сегментов. В противном случае таблица перемещения не содержит элементов, и исполняемый код начинается сразу за форматированной частью заголовка. Перейдем теперь к общей структуре РЕ-модуля.

I

Общая структура РЕ-модуля. Начало заголовка ЕХЕ-файлов в WIN32 представляет собой небольшую DOS-программу, основное предназначение которой заключается в том, чтобы при запуске в операционной системе MS DOS сделать сообщение о том, что данный модуль не предназначен для работы в MS DOS. Программа LINK.EXE (TLINK32.EXE) устанавливает свой вариант DOS-программы. Однако при желании Вы всегда можете поставить свою программу-заглушку (stub в переводе заглушка).

Рассмотрим общую структуру РЕ-заголовка.

СмещениеКомментарий
00HОбычный DOS-заголовок.
1CHЧетыре байта до выравнивания до 20Н байт, т.е. выравнивание до двух параграфов.
20HИнформация о программе, обычно отсутствующая.
3CHСмещение 32-битного РЕ-заголовка.
40HТаблица перемещения для программы-заглушки. У стандартных заглушек таблица, разумеется, пуста. Не смотря на это TablOff должен показывать именно сюда.
40H+??Здесь начинается тело самой заглушки, которая начинается за таблицей перемещения. Естественно, в стандартных заглушках нет ничего, кроме сообщения о невозможности запуска программы. Однако заглушка может иметь и весьма разрушительные свойства.
??Здесь начинается собственно PE-заголовок. Сюда показывает содержимое четырех байт по адресу 3CH. Начало должно быть выровнено по 8-байтной границе.
??Таблица описаний секций файлов (Object Table).
??Остальная информация: coff-символы, отладочная информация, таблица импорта и таблица экспорта, ресурсы и т.д. Данный раздел называется Image Pages, т.е. страницы образов.

Рис. 4.1.2. Общая структура РЕ-заголовка.

00000: 4D 5A 0A 00 02 00 00 00 | 04 00 0F 00 FF FF 00 00 MZ
00010: C0 00 00 00 00 00 00 00 | 40 00 00 00 00 00 00 00
00020: 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
00030: 00 00 00 00 00 00 00 00 | 00 00 00 00 80 00 00 00
00040: B4 09 BA 10 00 OE IF CD | 21 B8 01 4C CD 21 90 90
00050: 54 68 69 73 20 69 73 20 | 61 20 57 69 6E 33 32 20
00060: 70 72 6F 67 72 61 6D 2E | 0D OA 24 00 00 00 00 00
00070: 00 00 00 00 00 00 00 00 | 00 00 00 00 00 00 00 00
00080: 50 45 00 00 4C 01 03 00 | 39 30 00 00 00 00 00 00 PE
00090: 00 00 00 00 E0 00 OE 03 | 0B 01 02 34 00 30 00 00
000A0: 00 10 00 00 00 60 00 00 | 30 96 00 00 00 70 00 00
000B0: 00 A0 00 00 00 00 40 00 | 00 10 00 00 00 02 00 00

Рис. 4.1.3. Фрагмент РЕ-заголовка.

На рис. 4.1.3 показан фрагмент PE-заголовка. Обратите внимание, что по смещению 3CH действительно находится адрес начала основного заголовка (символы PE).

II

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

СмещениеДлина поляНазвание поляОписание поля
00h DWORD Signature Bytes Сигнатура. Первые два байта "PE" 4550Н. Еще два байта обязательно должны быть равны нулю.
04h WORD CPU Type Данное поле указывает на процессор, который следует предпочесть при запуске программы. Вот возможное значение этого поля:
0000h - неизвестный процессор.
014Ch - i386
014Dh - i486
014Eh - i586
0162h - MIPS Mark I (R2000, R3000)
0163h - MIPS Mark II (R6000)
0166h - MIPS Mark III (R4000)
Чаще всего данное поле указывает на процессор 386.
06h WORD Num of Objects Поле указывает на число реальных входов в Object Table (см. таб. ниже).
08h DWORD Time/Date Stamp Дата и время, которые устанавливаются при компоновке программы.
0Ch DWORD Pointer to COFF table Дополнительный указатель, определяющий местонахождение отладочной COFF-таблицы в файлах. Это поле используется только в OBJ-файлах и РЕ-файлах, содержащих отладочную COFF-информацию.
10h DWORD COFF table size Количество символов в COFF-таблице.
14h WORD NT Header Size Размер заголовка РЕ-файла, начиная с поля Magic - таким образом, общий размер заголовка РЕ-файла составляет NT Header Size + 18h.
16h WORD Flags Указывает на предназначение программы. Значение флагов:
0000h - это программа;
0001h - файл не содержит перемещений и таблицы перемещаемых элементов;
0002h - образ в файле можно запускать. Если этот бит не установлен, то это обычно указывает на ошибку, обнаруженную на этапе линковки, или же на то, что код был инкрементально отлинкован (инкрементальная линковка - частичная линковка кода при изменении участка программы, а не тотальная перекомпиляция проекта);
0200h - загружать в память фиксированно. Указывает на то, что программу можно грузить только по адресу, записанному в Image Base, если это невозможно, то такой файл лучше вообще не запускать.
2000h - это библиотека.
18h WORD Magic Слово-сигнатура, определяющее состояние отображенного файла. Определены следующие значения:
107Н - отображение ПЗУ.
10BH - нормально исполняемое отображение.
1Ah BYTE Link Major Старший номер версии использовавшегося при создании модуля компоновщика. Десятичный вид.
1Bh BYTE Link Minor Младший номер версии использовавшегося при создании модуля компоновщика. Десятичный вид.
1Ch DWORD Size of Code Размер именно программного кода в файле. KERNEL использует это значение для фактического отведения памяти под загружаемую программу. Установка этого значения слишком маленьким приведет к выдаче сообщения о нехватке памяти. Обычно большинство модулей имеют только одну программную секцию .text.
20h DWORD Size of Init Data Размер секции инициализированных данных, очевидно, не используется в Windows 95, но используется в Windows NT. Назначение аналогично приведенному выше.
24h DWORD Size of UnInit Data Размер секции неинициализированных данных. Неинициализированные данные обычно содержатся в секции .bss. Данная секция не занимает на диске никакого места, но при загрузке модуля в память загрузчик отводит под нее память.
28h DWORD Entry point RVA Адрес относительно Image Base, no которому передается управление при запуске программы или адрес инициализации/завершения библиотеки.
2Ch DWORD Base of Code Адрес секции относительно базового адреса (40000Н), содержащей программный код. Этот адрес обычно равен 1000Н для компоновщика Microsoft и 10000H для компоновщика Borland.
30h DWORD Base of Data Адрес относительно базового (40000H), с которого начинаются секции данных файла. Секции данных обычно идут последними в памяти, после заголовка РЕ и программных секций.
34h DWORD Image Base При создании компоновщик помещает сюда адрес, куда будет отображен исполняемый файл в памяти. Если загрузчик отобразит файл именно по этому адресу, то дополнительной настройки не потребуется.
38h DWORD Object align Выравнивание программных секций. После отображения в память каждая секция будет обязательно начинаться с виртуального адреса, кратного данной величине.
3Ch DWORD File align В случае РЕ-файла исходные данные, которые входят в состав каждой секции, будут обязательно начинаться с адреса, кратного данной величине. Значение по умолчанию составляет 200Н.
40h WORD OS Major Старший номер версии операционной системы, необходимый для запуска программы.
42h WORD OS Minor Младший номер версии операционной системы.
44h WORD USER Major Пользовательский номер версии, задается пользователем при линковке программы. Старшая часть.
46h WORD USER Minor Пользовательский номер версии, младшая часть.
48h WORD SubSys Major Старший номер версии подсистемы.
4Ah WORD SubSys Minor Младший номер версии подсистемы. Типичное значение версии 4.0, что означает Windows 95.
4Ch DWORD Reserved Зарезервировано.
50h DWORD Image Size Представляет общий размер всех частей отображения, находящихся под контролем загрузчика. Эта величина равна размеру области памяти, начиная с базового адреса отображения и заканчивая адресом конца последней секции. Адрес конца секции выровнен на ближайшую верхнюю границу секции.
54h DWORD Header Size Общий размер всех заголовков: DOS Stub + РЕ Header + Object Table
58h DWORD File CheckSum Контрольная сумма всего файла. Как и в операционной системе MS DOS, ее никто не контролирует, а программа редактирования связей устанавливает ее в 0. Предполагалось ее рассчитывать как инверсию суммы всех байтов файла.
5Ch WORD Subsystem Операционная подсистема, необходимая для запуска данного файла. Вот значения этого поля:
1 - подсистема не требуется (NATIVE).
2 - запускается в подсистеме Windows GUI.
3 - запускается в подсистеме Windows character (терминальное или консольное приложение).
5 - запускается в подсистеме OS/2.
7 - запускается в подсистеме Posix.
5Eh WORD DLL Flags Указывает на специальные потребности при загрузке, начиная с операционной системы NT 3.5. Устарел и не используется.
60h DWORD Stack Reserve Size Память, требуемая для стека приложения. Память резервируется, но выделяется только Stack Commit Size байтов. Следующая страница является охранной. Когда приложение достигает этой страницы, то она становится доступной, а следующая страница - охранной, и так до достижения нижней границы, после чего Windows 95 убивает программу.
64h DWORD Stack Commit Size Объем памяти, отводимой для стека немедленно после загрузки.
68h DWORD Heap Reserve Size Максимально возможный размер локальной кучи.
6Ch DWORD Heap Comit Size Отводимый размер кучи при загрузке.
70h DWORD Loader Flags Начиная с Windows NT 3.5 объявлено неиспользуемым, назначение неясно, но в целом связано с поддержкой отладки.
74h DWORD Num of RVA and Sizes Указывает размер массива VA/Size, который следует ниже, данное поле зарезервирована под будущие расширения формата. В данный момент его значение всегда равно 10h.
78h DWORD Export Table RVA Относительный адрес (относительно базового адреса) таблицы экспорта.
7Ch DWORD Export Data Size Размер таблицы экспорта.
80h DWORD Import Table RVA Относительный адрес (относительно базового адреса) таблицы импорта.
84h DWORD Import Data Size Размер таблицы импорта.
88h DWORD Resource Table RVA Относительный адрес (относительно базового адреса) таблицы ресурсов.
8Ch DWORD Resource Data Size Размер таблицы ресурсов.
90h DWORD Exception Table RVA Относительный адрес таблицы исключений.
94h DWORD Exception Data Size Размер таблицы исключений.
98h DWORD Security Table RVA Адрес таблицы безопасности. По-видимому, не используется.
9Ch DWORD Security Data Size Размер таблицы безопасности.
A0h DWORD Fix Up's Table RVA Относительный адрес таблицы настроек.
A4h DWORD Fix Up's Data Size Размер таблицы настроек.
A8h DWORD Debug Table RVA Относительный адрес таблицы отладочной информации.
ACh DWORD Debug Data Size Размер таблицы отладочной информации.
B0h DWORD Image Description RVA Относительный адрес строки описания модуля.
B4h DWORD Description Data Size Размер строки описания модуля.
B8h DWORD Machine Specific RVA Адрес таблицы значений, специфичных для микропроцессора.
BCh DWORD Machine Data Size Размер таблицы значений, специфичных для микропроцессора.
C0h DWORD TLS RVA Указатель на локальную область данных цепочек.
C4h DWORD TLS Data Size Размер области данных цепочек.
C8h DWORD Load Config RVA Предназначение неизвестно.
CCh DWORD Load Config Data Size Предназначение неизвестно.
D0h 08h Reserved Зарезервировано.
D8h DWORD IAT RVA Используется в NT. В Windows 95, судя по всему, нет.
DCh DWORD IAT Data Size Размер описанного поля.
E0h 08h Reserved Зарезервировано.
E8h 08h Reserved Зарезервировано.
F0h 08h Reserved Зарезервировано.

Между заголовком РЕ и данными для секций расположена таблица секций. Вот элемент этой таблицы.

Элемент таблицы секций содержит полную базу данных об одной секции.

Смещение Длина поля Название поля Описание поля
00h 08h Object Name Имя объекта, остаток заполнен нулями. Если имя объекта имеет длину 8 символов, то заключительного 0 нет. Вот несколько возможных имен:
.text - исполняемый код общего назначения.
CODE - исполняемый код, помещаемый компоновщиками фирмы BORLAND.
.icode - переходники (jump'ы), помещаемые сюда старой версией TLINK32.
.data - инициализированные данные, помещаются компоновщиком фирмы Microsoft.
DATA - инициализированные данные, помещаемые сюда компоновщиком TLINK32.
.bss - неинициализированные глобальные и статические переменные.
.CRT - еще одна секция для хранения инициализированных данных.
.rsrc - секция для хранения ресурсов.
.idata - секция импорта.
.edata - секция экспорта.
.reloc - секция настроек. Данная информация может понадобиться загрузчику, если он не сможет загрузить модуль по базовому адресу.
.tls - данные для запуска цепочек.
.rdata - данная секция в основном содержит отладочную информацию.
.debug$s и .debug$t - данные секции есть только в COFF-объектных файлах. Они содержат информацию о символах Code View и их типах.
.drective - в данной секции содержится текст программ для компоновки. Данная секция есть только в объектных файлах. Секции, содержащие символ $. Такие секции обрабатываются особым образом. Компоновщик объединяет все секции, имеющие одинаковые символы в имени до символа $. Именем получившейся секции считается то, что стоит перед указанным символом.
08h DWORD Virtual Size Виртуальный размер секции - именно столько памяти будет отведено под секцию. Если Virtual Size превышает Physical Size, то разница заполняется нулями, так определяются секции неинициализированных данных (Physical Size = 0).
0Ch DWORD Section RVA Размещение секции в памяти, ее виртуальный адрес относительно Image Base. Позиция каждой секции выровнена на границу Object align (степень 2 от 512 до 256М включительно, по умолчанию 64К), секции упакованы вплотную друг к другу, впрочем, можно это не соблюдать. Для объектных файлов поле не имеет смысла.
10h DWORD Physical Size Размер секции (ее инициализированной части) в файле кратно полю File align в заголовке РЕ Header, должно быть меньше или равно Virtual Size. Для объектных файлов это поле содержит точный размер секции, сгенерированный компилятором или ассемблером. Другими словами, для объектных файлов оно эквивалентно полю Virtual Size.
14h DWORD Physical Offset Физическое смещение относительно начала ЕХЕ-файла, выровнено на границу File align поля заголовка РЕ Header. Смещение используется загрузчиком для поиска.
18h DWORD Pointer to Linenumber Файловое смещение таблицы номеров строк. Используется для объектных файлов.
1Ch WORD Number of Relocations Количество перемещений в таблице поправок. Используется только для объектных файлов.
1Eh WORD Number of Linenumbers Количество номеров строк в таблице номеров строк для данной секции. Используется для объектных файлов.
20h 08h Reserved Зарезервировано для объектных файлов.
28h DWORD Object Flags Битовые флаги секции:
00000004h - используется для кода с 16-битными смещениями.
00000020h - секция кода.
00000040h - секция инициализированных данных.
00000080h - секция неинициализированных данных.
00000200h - комментарии или любой другой тип информации.
00000400h - оверлейная секция.
00000800h - не будет являться частью образа программы.
00001000h - общие данные.
00500000h - выравнивание по умолчанию, если не указано иное.
02000000h - может быть выгружен из памяти.
04000000h - не кэшируется.
08000000h - не подвергается страничному преобразованию.
10000000h - разделяемый.
20000000h - выполнимый.
40000000h - можно читать.
80000000h - можно писать.

Страницы образов секций. Здесь мы изучим некоторые секции.

Секция экспорта (.edata). Общая структура

1   Таблица собственно экспорта   Export Directory Table
2   Адресная таблица   Export Address Table
3   Таблица указателей на имена   Export Name Table Pointers
4   Таблица номеров   Export Ordinal Table
5   Таблица самих имен   Export Name Table

1. Таблица экспорта

Смещение Длина поля Название поля Описание поля
00h DWORD Flags Зарезервировано, должно быть равно нулю.
04h DWORD Time/Date Stamp Время и дата создания экспортных данных.
08h WORD Major Version Старший номер версии таблицы экспорта. Не используется.
0Ah DWORD Minor Version Младший номер версии таблицы экспорта, также не используется.
0Ch DWORD Name RVA Относительный адрес строки, указывающей на имя нашей библиотеки.
10h DWORD Ordinal Base Начальный номер экспорта, для функций, экспортируемых данным модулем.
14h DWORD Num of Functions Количество функций экспортируемых данным модулем, является числом элементов массива Address Table (см.ниже).
18h DWORD Num of Name Pointers Число указателей на имена, обычно равно числу функций, но это не так, если у нас есть функции, экспортируемые только по номеру.
1Ch DWORD Address Table RVA Указатель на таблицу относительных адресов экспортируемых функций.
20h DWORD Name Pointers RVA Указатель на таблицу указателей на имена экспортируемых функций данного модуля.
24h DWORD Ordinal Table RVA Указатель на таблицу номеров экспорта, данный массив по индексам параллелен Name Pointers, элементами являются слова.

2. Таблица адресов экспорта. Эта структура данных содержит адреса экспортируемых функций (их точки входа) в формате DWORD (по 4 байта на элемент). Для доступа к данным используется номер функции с коррекцией на базу номеров (Ordinal Base).

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

4. Таблица номеров. Данная структура совместно с Name Table Pointers формирует два параллельных массива, разделенных для облегчения к ним доступа индексированием на родные для процессора данные (слова, двойные слова, но не сложные структуры). Данный массив содержит номера экспорта, которые в общем случае являются индексами в Address Table экспорта (за вычетом базы Ordinal Base). Элементами данного массива являются слова (2 байта).

5. Таблица имен экспорта. Эта таблица содержит необязательные (по мнению Microsoft) имена экспортируемых функций. Данный массив используется совместно с Name Table Pointers и Ordinal Table для обеспечения связывания загрузчиком импорта/экспорта по имени. Механизм описывался выше. Каждый элемент являет собой ASCIIZ строку с именем экспортируемой функции. Никто не говорит, что они должны в файле идти друг за другом последовательно, хотя так построено большинство файлов. Надо отметить, что имена экспорта чувствительны к регистру. Отметим особенность загрузчика - при связывании, если адрес функции находится в секции экспорта, на самом деле по указанному адресу лежит строка, переадресующая к другой библиотеке, экспортирующей данную функцию (с указанием библиотеки и самой функции). Это называется - передача экспорта.

Секция импорта (.idata)

Рис. 4.1.4. Вызов импортируемой функции.

Схема вызова импортируемых функций из РЕ-модуля изображена на Рис. 4.1.4, который с некоторыми изменениями взят из книги [2]. Смысл данного рисунка заключается в следующем. При компоновке все вызовы API-функций преобразуются к вызову типа CALL Адрес1. При этом адрес, также как и вызов, находится в секции кода (.text). По адресу же стоит команда JMP DWORD PTR [Адрес2]. [Адрес2] находится в секции .idata (импорта) и содержит двойное слово — адрес функции в динамической библиотеке. Современные компиляторы содержат директивы, позволяющие вместо двух вызовов (CALL и JMP) генерировать один CALL [Адрес2].

Таблица директория импорта

Каталог импорта   Import Directory Table
Таблица ссылок на имена   LookUp Table
Таблица имен   Hint-Name Table
Таблица адресов импорта   Import Address Table

Формат входа в каталог импорта

Смещение Длина поля Название поля Описание поля
00h DWORD Import LookUp Содержит ссылку на таблицу относительных адресов (относительно базового адреса), указывающих на соответствующие имена импортируемой функции, или непосредственно номер импортируемого входа.
04h DWORD Time/Date Stamp Отметка о времени создания, часто содержит ноль.
08h DWORD Forward Chain Связано с возможностью передачи экспорта в другие библиотеки. Обычно равно 0FFFFFFFFh.
0Ch DWORD Name RVA Ссылка на библиотеку импорта в виде ASCII строки с нулем на конце. Например, KERNEL32.DLL или USER32.DLL.
10h DWORD Addres Table RVA Ссылка на таблицу адресов импорта, заполняется системой при связывании.

Таблица просмотра импорта или таблица имен сервисов. Ссылка из поля Import LookUp на массив, содержащий ссылки на таблицу просмотра импорта. При импортировании по номеру старший бит элемента массива равен 1.

ТипСодержимое
Word   Номер функции
Hint   ASCII имя функции

Таблица адресов импорта. Данная таблица принимает в себя информацию после связывания загрузчиком импорта из внешних библиотек, она завершается нулевым элементом.

Локальная область данных цепочек (потоков)

Локальная область данных цепочек, это специальный, протяженный блок данных. Каждая цепочка получит собственный блок при создании.

Таблица разделов цепочек   TLS Directory Table
Данные цепочек   TLS Data
Индексные переменные   Index Variables
Адреса обратных вызовов   CallBack

Таблица разделов потоков

Смещение Длина поля Название поля Описание поля
00h DWORD Start Data Block VA Виртуальный адрес начала блока данных цепочки.
04h DWORD End Data Block VA Виртуальный адрес конца блока данных цепочки.
08h DWORD Index VA Виртуальный адрес индексной переменной, используемой для доступа к локальному блоку данных цепочки.
0Ch DWORD CallBack Table VA Виртуальный адрес таблицы обратных вызовов. Локальные обратные вызовы - массив виртуальных адресов функций, которые будут вызваны загрузчиком после создания цепочки (нити) и после ее завершения. Последний вход имеет нулевое значение и указывает на конец таблицы.

Секция ресурсов (.rdata)

Ресурсы представляют собой многоуровневое двоичное дерево. Их структура позволяет содержать до 2^31 уровней, однако реально используется только 3: самый верхний есть Type, затем Nam и затем Language (тип, имя, язык). Перемещения по иерархии каталогов ресурсов похожи на перемещения по каталогам жесткого диска. Типичное представление ресурсного участка в файлах:

Каталог ресурсов   Resources Directory Table
Данные ресурсов   Resources Data

Каталог ресурсов (Resource Directory Table)

Смещение Длина поля Название поля Описание поля
00h DWORD Flags Пока не используются, должны быть сброшены в ноль.
04h DWORD Time/Date Stamp Дата и время создания ресурсов от ресурсного компилятора.
08h WORD Major Version Старшая часть версии ресурсов. Обычно равно нулю.
0Ah WORD Minor Version Младшая часть версии ресурсов. Обычно равно нулю.
0Ch WORD Name Entry Количество входов в таблицу имен (элементов массива) ресурсов. Таблица располагается в самом начале массива входов и содержит строковые имена, ассоциируемые с ресурсами.
0Eh WORD ID_Num Entry Количество элементов массива, использующих целые ID.

За каталогом ресурсов сразу следует массив переменной длины, содержащий ресурсные входы. Name Entry содержит число ресурсных входов, имеющих имена (связанные с каждым входом). Имена нечувствительны к регистру и расположены в порядке возрастания. ID_Num Entry определяет число входов имеющих в качестве имени 32-битовый идентификатор. Эти входы так же отсортированы по возрастанию. Данная структура позволяет получать быстрый доступ к ресурсам по имени или по идентификатору, но для отдельно взятого ресурса поддерживается только одна из форм поиска. Что согласуется с синтаксисом .RC и .RES файлов. Каждый вход в таблицу ресурсов имеет следующий формат.

Вход в таблицу ресурсов (Resource Entry Item)

Смещение Длина поля Название поля Описание поля
00h DWORD Name RVA or Res ID Поле содержит либо идентификатор ресурса, либо указатель на его имя в таблице имен ресурсов.
04h DWORD Data Entry RVA or SubDirectory RVA Указывает либо на данные, либо на еще одну таблицу входов ресурсов. Старший бит поля, сброшенный в ноль, говорит, что поле указывает на данные.

Каждый пункт данных (Resource Entry Item) имеет следующий формат:

Смещение Длина поля Название поля Описание поля
00h DWORD Data RVA Указатель на реально расположенные данные относительно Image Base.
04h DWORD Size Размер ресурсных данных.
08h DWORD CodePage Кодовая страница.
0Ch DWORD Reserved He используется и устанавливается в ноль.

Таблица настроек адресов

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

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

Смещение Длина поля Название поля Описание поля
00h DWORD Page RVA Относительный адрес страницы применения.
04h DWORD Block Size Размер блока настроек (с заголовком). Эта величина используется для вычисления количества настроек.
08h WORD TypeOffset Record Массив записей настроек, их переменное количество.

Для наложения настройки необходимо вычислить 32-битную разницу ("Дельта") между желаемой базой загрузки и действительной. Если образ программы загружен в требуемое место, то эта разница равна нулю и никакой настройки не требуется. Каждый блок настроек должен начинаться на DWORD-границе, для выравнивания блока можно пользоваться нулями. При настройке необходимую позицию в блоке вычисляют как сумму относительного адреса страницы и базового адреса загруженной программы.

Структура элемента массива настроек

15...12
Type
11...0
Offset
Биты слова, Type указывает на тип настройки, a Offset на ее смещение внутри 4-килобайтной страницы.

Перечислим возможные типы поправок.

0h Адрес абсолютный и никаких изменений производить не требуется.
1h Добавить старшие 16 битов "Дельты" к 16-битовому полю, находящемуся по смещению Offset. 16-битовое поле представляет старшие биты 32-битового слова.
2h Добавить младшие 16 битов "Дельты" по смещению Offset. 16-битовое поле представляет младшую половину 32-битового слова. Данная запись настройки присутствует только на RISC машине, когда Object align не является по умолчанию 64К.
3h Прибавляет 32-битовое "Дельта" к 32-битовому значению.
4h Настройка требует полного 32-битового значения. Старшие 16-бит берутся по адресу Offset, а младшие в следующем элементе TypeOffset. Все это объединяется в знаковую переменную, затем добавляется 32-битовое "Дельта" и DWORD 8000h. Старшие 16 бит получившегося значения сохраняются по адресу Offset в 16-битовом поле.
5h ?

Отладочная информация (.debug$S, .debug$T)

Здесь помещается структура отладочного каталога, создаваемого любыми компоновщиками. Другая отладочная информация зависит от транслятора. Структуру COFF-отладочной информации можно посмотреть в книге [2].

Отладочный каталог

Смещение Длина поля Название поля Описание поля
00h DWORD Debug Flags Флаги, по-видимому, не используются и устанавливаются в нулевое значение.
04h DWORD Time/Date Stamp Дата и время создания отладочной информации.
08h WORD Major Version Старший номер версии отладочной информации.
0Ah WORD Minor Version Младший номер версии отладочной информации.
0Ch DWORD Debug Type Тип информации для отладчика. Вот эти типы:
0000h - UNKNOWN/BORLAND;
0001h - COFF таблица символов;
0002h - Code View таблица символов;
0003h - FPO таблица символов;
0004h - MISC;
0005h - EXCEPTION;
0006h - FIXUP.
10h DWORD Data Size Размер в байтах данных для отладки без размера заголовка.
14h DWORD Data RVA Относительный адрес расположения отладочных данных в памяти.
18h DWORD Data Seek Смещение к отладочным данным в файле.


Глава 2. Обзор отладчиков и дизассемблеров

В этой главе мы рассмотрим отладочные и дизассемблирующие программы, кроме трех наиболее известных, о которых пойдет речь в последующих двух главах.

I

Утилиты фирмы Microsoft

EDITBIN.EXE

Название программы многообещающе, но в действительности программу нельзя назвать редактором. Основное ее предназначение - конвертировать OMF-формат объектных файлов в COFF-формат. Кроме того, данная утилита позволяет менять некоторые другие атрибуты исполняемых и объектных модулей. Если в командной строке данной программы указать имя объектного модуля, то, в случае если модуль будет в OMF-формате, он будет преобразован в COFF-формат. Рассмотрим ключи данной программы, которые можно применять как к исполняемым, так и к объектным модулям.

/BIND - позволяет указать пути к динамическим библиотекам, которые используют данный исполняемый модуль. Например,
EDITBIN /BIND:PATH=c:\edit;d:\dll EDIT.EXE.

/HEAP - изменяет размер кучи в байтах. Например,
EDITBIN /HEAP:100000,100000 (см. Опции программы LINK.EXE).

/LARGEADDRESSAWARE - указывает, что приложение оперирует адресами, большими 2 гигабайт.

/NOLOGO - подавляет вывод информации о программе.

/REBASE - устанавливает базовый адрес модуля. По умолчанию для исполняемого модуля базовый адрес равен 400000Н, для динамической библиотеки - 10000000H.

/RELEASE - устанавливает контрольную сумму в заголовке исполняемого модуля.

/SECTION - изменяет атрибуты секций исполняемого модуля. Полный формат опции /SECTION:name[=newname][,attributes][,alignment]
Значение атрибутов

АтрибутЗначение
c   code
d   discardable
е   executable
i   initialized data
k   cached virtual memory
m   link remove
o   link info
p   paged virtual memory
r   read
s   shared
u   uninitialized data
w   write

Значение опции выравнивания
1   1
2   2
4   4
8   8
p   16
t   32
s   64
x   no alignment

/STACK - изменяет значение требуемого для загружаемого модуля стека.
Например: EDITBIN /STACK:10000,10000 EDIT.EXE

/SUBSYSTEM - переопределяет подсистему, в которой работает данная программа.
Например, если программа оттранслирована с опцией /SUBSYSTEM:WINDOWS, можно изменить установку, не перекомпилируя ее. EDITBIN /SUBSYSTEM:CONSOLE EDIT.EXE.

/SWAPRUN - устанавливает для исполняемого модуля атрибут "помещать модуль в SWAP-файл".

/VERSION - устанавливает версию для исполняемого модуля.

/WS (/WS:AGGRESSIVE) - ycтaнaвливает атрибут AGGRESSIVE, который используется операционной системой Windows NT и Windows 2000.

Утилита весьма полезна для быстрого изменения атрибутов исполняемых и объектных модулей.

DUMPBIN.EXE

Программа используется для исследования загружаемых и объектных модулей COFF-формата и выводит информацию в текущую консоль. Ключи данной программы:

/ALL - выводит всю доступную информацию о модуле, кроме ассемблерного кода.

/ARCH - выводит содержимое секции .arch заголовка модуля.

/ARCHIVEMEMBERS - выводит минимальную информацию о элементах объектной библиотеки.

/DEPENDENTS - выводит имена динамических библиотек, откуда данным модулем импортируются функции.

/DIRECTIVES - выводит содержимое секции .drective, создаваемой компилятором (только для объектных модулей).

/DISASM - дизассемблирует содержимое секций модуля с использованием и символьной информации, если она присутствует там.

/EXPORTS - выдает все экспортируемые модулем имена.

/HEADER - выдает заголовки модуля и всех его секций. В случае объектной библиотеки выдает заголовки всех составляющих ее объектных модулей.

/IMPORTS - выдает все имена, импортируемые данным модулем.

/LINENUMBERS - выдает номера строк объектного модуля, если таковые имеются.

/LINKERMEMBER[:{1|2}] - выдает все имена в объектной библиотеке, определяемые как public.

/LINKERMEMBER:1 - в порядке следования объектных модулей в библиотеке.

/LINKERMEMBER:2 - вначале выдает смещение и индекс объектных модулей, а затем список имен в алфавитном порядке для каждого модуля.

/LINKERMEMBER - сочетание ключей 1 и 2.

/OUT - определяет, что вывод осуществляется не в консоль, а в файл (/OUT:ED.TXT).

/PDATA - выдает содержимое таблиц исключения.

/RAWDATA - выдает дамп каждой секции файла. Разновидности данного ключа: /RAWDATA:BYTE, /RAWDATA:SHORTS, /RAWDATA:LONGS, /RAWDATA:NONE, /RAWDATA:,number. Здесь number определяет ширину строк.

/SUMMARY - выдает минимальную информацию о секциях.

/SYMBOLS - выдает таблицу символов COFF-файла.

Рассматриваемая программа является весьма мощным средством дизассемблирования. Пусть программа называется prog.asm. Выполним трансляцию программы следующим образом.

ml /с /coff /Zi /Zd prog.asm
link /debug /subsystem:windows prog.obj53

При этом, кроме исполняемого модуля prog.exe, появляется еще и файл prog.pdb, содержащий отладочную информацию.

Выполним теперь команду DUMPBIN /DISASM /OUT:PROG.TXT PROG.EXE. В результате получим практически исходный ассемблерный код. Часть этого кода представлена на Рис. 4.2.1.

_START:
0040101С: 6A00 push 0
0040101E: E843020000 call _GetModuleHandleA@4
00401023: A344404000 mov [00404044],eax
00401028: C7051C404000 mov dword ptr ds:[40401Ch],4003h
03400000
00401032: C70520404000 mov dword ptr ds:[404020h],offset
@ILT+0(_WNDPROC@0)
05104000
0040103C: C70524404000 mov dword ptr ds:[404024h],0
00000000
00401046: C70528404000 mov dword ptr ds:[404028h],0
00000000
00401050: A144404000 mov eax,[00404044]
00401055: A32C404000 mov [0040402C],eax
0040105A: 68007F0000 push 7F00h
0040105F: 6A00 push 0
00401061: E8D6010000 call _LoadIconA@8
00401066: A330404000 mov [00404030],eax
0040106B: 68037F0000 push 7F03h
00401070: 6A00 push 0
00401072: E8BF010000 call _LoadCursorA@8

Рис. 4.2.1. Часть дизассемблированиого кода.

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

_LoadIconA@8:
0040123С: FF2510514000 jmp dword ptr [_imp_LoadIconA@8]

В действительности, когда мы вызываем, например, функцию _LoadIconA@8, то на самом деле происходит переход на адрес, где стоит команда

jmp dword ptr [_imp_LoadIconA@8].

По этой причине в своих программах вместо объявления LoadIcon@8, можно объявлять _imp_LoadIconA@8, что несколько увеличит производительность Вашей программы. Ранее мы не говорили о такой возможности по двум причинам: Повышение производительности здесь минимально. Транслятор TASM32 делает все несколько иначе.

Более подробно данный вопрос рассматривается в Главе 4.1.


53 Здесь, разумеется, мы рассматриваем только средства MASM.

II

Утилиты других производителей

DUMPPE.EXE

Данная программа рассматривалась нами в гл. 1.1. Она во многом похожа на предыдущую программу DUMPBIN.EXE, но более удобна, хотя и обладает несколько меньшими возможностями.

HIEW.EXE

Данная программа широко известна в среде программистов, скажем так, хакерского направления. Название программы происходит от фразы "Hacker's View". Основная задача, которую выполняет данная программа - просматривать и редактировать загружаемые модули. Причем просмотр и редактирование допускается в трех вариантах: двоичный, текстовый и ассемблерный. Хороших дизассемблирующих программ создано довольно много, а вот программ, подобных данной, можно по пальцам перечесть.

Интерфейс программы весьма напоминает интерфейс редакторов таких программ, как FAR или Norton Commander (см. Рис. 4.2.2.). Все команды осуществляются при помощи функциональных клавиш с использованием клавиш "Alt" и "Ctrl". Например, нажимая клавишу F4, вы получаете возможность выбрать способ представления двоичного файла: текстовый, ассемблерный или двоичный. Нажимая клавишу F3 (при условии, если Вы находитесь в двоичном или ассемблерном просмотре), Вы получаете возможность редактировать файл. Если же, находясь в ассемблерном просмотре, Вы после F3 нажмете еще и F2, то сможете редактировать машинную команду в символьном виде. Мы не будем далее останавливаться на командах данной программы, поскольку они просты, очевидны и могут быть получены просто по F1, а перейдем сразу к простому примеру использования данной программы, хотя пример тематически и относится к материалу Гл. 4.6. Чтобы слишком не загромождать рассмотрение возьмем простую консольную программу.

Рис. 4.2.2. Внешний вид программы HIEW.EXE

Ниже (Рис. 4.2.3) представлена простая консольная программа, выводящая на экран текстовую строку.

.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_OUTPUT_HANDLE equ -11
INVALID_HANDLE_VALUE equ -1
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN WriteConsoleA@20:NEAR
EXTERN ExitProcess@4:NEAR
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
BUF DB "Строка для вывода",0
LENS DWORD ? ; количество выведенных символов
HANDL DWORD ?
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE вывода
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
CMP EAX,INVALID_HANDLE_VALUE
JNE _EX
MOV HANDL,EAX
; вывод строки
PUSH 0
PUSH OFFSET LENS
PUSH 17
PUSH OFFSET BUF
PUSH HANDL
CALL WriteConsoleA@20
_EX:
PUSH 0
CALL ExitProcess@4
_TEXT ENDS
END START

Рис. 4.2.3. Консольная программа.

Программа на Рис. 4.2.3 проста и корректна. Представьте теперь, что при отладке Вы случайно изменили одну команду: вместо JE поставили JNE. В результате поспе трансляции программа перестала работать. Можно исправить ее, не прибегая к ассемблерному тексту? Конечно. Для этого в начале ее следует дизассемблировать, найти ошибку, а потом воспользоваться программой HIEW.EXE. Вообще говоря, можно ограничиться только программой HIEW, так как она вполне корректно дизассемблирует. Однако мы нарочно проведем исправление в два этапа.

Дизассемблируем модуль при помощи программы DUMPBIN.EXE. Вот дизассемблированный текст программы (Рис. 4.2.4).

Dump of file cons1.exe
File Type: EXECUTABLE IMAGE
00401000: 6A F5 push 0F5h
00401002: E8 2B 00 00 00 call 00401032
00401007: 83 F8 FF cmp eax,0FFFFFFFFh
0040100A: 75 1E jne 0040102A
0040100C: A3 16 30 40 00 mov [00403016],eax
00401011: 6A 00 push 0
00401013: 68 12 30 40 00 push 403012h
00401018: 6A 11 push 11h
0040101A: 68 00 30 40 00 push 403000h
0040101F: FF 35 16 30 40 00 push dword ptr ds:[00403016h]
00401025: E8 0E 00 00 00 call 00401038
0040102A: 6A 00 push 0
0040102C: E8 0D 00 00 00 call 0040103E
00401031: CC int 3
00401032: FF 25 08 20 40 00 jmp dword ptr ds:[00402008h]
00401038: FF 25 00 20 40 00 jmp dword ptr ds:[00402000h]
0040103E: FF 25 04 20 40 00 jmp dword ptr ds:[00402004h]

Рис. 4.2.4. Дизассемблированный код программы на Рис. 4.2.3.

По дизассемблированному коду легко обнаружить ошибку. Кстати, команду cmp eax,0FFh надо, естественно, понимать как cmp eax,0FFFFFFFFh. Запомним нужный код 83F8FF. Запускаем программу HIEW.EXE, нажимаем клавишу F7 и ищем нужное сочетание. Далее клавиша F3, затем F2 и далее - заменяем команду JNE на JE. Клавиша F9 закрепляет изменение. В результате мы исправили программу, не прибегая к ее перетрансляции.

DEWIN.EXE

Программа работает в командном режиме, но по сравнению, например, с DUMPBIN.EXE обладает рядом достоинств. Главное из этих достоинств - это распознавание языков высокого уровня. Кроме того, Вы сами можете писать скрипт-процедуры на предлагаемом макроязыке.

IDA PRO

Один из самых мощных дизассемблеров. Возможности настолько велики, что многие программисты считают его всемогущим. Работая над текстом дизассемблируемой программы. Вы можете называть своими именами метки и процедуры, давать свои комментарии так, что дизассемблированный текст становится в конце концов ясным и понятным. Все сохраняется в специальную базу и при последующем запуске, естественно, восстанавливаются. Внешний вид дизассемблера IDA PRO показан на Рис. 4.2.4. Мы дизассемблировали одну из наших старых программ. Обратите, кстати, внимание на ссылку offset WNDPROC. Название WNDPROC дано уже нами в процессе анализа кода программы. Но рассмотрим все по порядку.

Рис. 4.2.4. Пример дизассемблирования программы с помощью самого мощного дизассемблера IDA PRO (под Windows).

Рассмотрим некоторые возможности этого дизассемблера.

1. Переименование процедур и меток в программе. При дизассемблировании IDA PRO дает, разумеется, свои названия процедурам и меткам. Вы можете ввести свои названия, тем самым сделав программу более понятной. Все изменения, сделанные в тексте, сохраняются в специальной базе и могут быть восстановлены при повторном запуске.

2. Распознавание библиотечных и API-функций (см. Рис. 4.2.4). Дизассемблер не просто распознает эти функции, но и комментирует параметры этих функций.

3. При помощи контекстного меню или двойного щелчка Вы можете перейти по команде JMP или команде CALL в указанное место программы и так продолжать осуществлять переходы любое количество раз. Возвратиться на любое количество шагов можно, используя кнопку "стрелка" на панели инструментов.

4. При помощи Shift+Ins, Ins, а так же пунктов меню "EDIT" в любом месте программы можно записать комментарий. Комментарий, как и введенные названия меток, запоминается в базе данных программы. Но это еще не самое приятное. В комментарии может присутствовать адрес строки программы или имя метки. Если сделать двойной щелчок по адресу или метке, то мы как раз и очутимся на этом месте.

5. Сворачивание и разворачивание процедур. При помощи клавиши "-" на дополнительной клавиатуре можно свернуть процедуру, а при помощи "+" развернуть процедуру. Представление процедур в свернутом виде позволяет представить программу в более компактном и более понятном виде.

6. IDA PRO весьма аккуратно распознает не только код, но и данные. На Рис. 4.2.5 показана дизассемблированная часть нашей программы, содержащей данные.

7. Создание и выполнение командных файлов. Язык командных файлов очень близок к языку Си. У меня нет намерения рассказывать о языке, который использует IDA PRO, приведу только один такой командный файл, содержащийся в пакете IDA PRO.

Рис. 4.2.5. Часть программы, содержащей данные, дизассемблированная при помощи IDA PRO.

//
// This example shows how to get list of functions.
//
#include <idc.idc>
static main() {
auto ea,x;
for (ea=NextFunction(0); ea != BADADDR; ea=NextFunction(ea))
{
Message("Function at %081X:%s",ea,GetFunctionName(ea));
x = GetFunctionFlags(ea);
if ( x & FUNC_NORET ) Message(" Noret");
if ( x & FUNC_FAR ) Message(" Far");
Message("\n") ;
}
ea = ChooseFunction("Please choose a function");
Message("The user chose function at %081X\n",ea);
}

Рис. 4.2.6. Пример командного файла IDA PRO.

Прокомментируем программу на Рис. 4.2.6. Как легко догадаться, организация цикла и условные конструкции имеют в точности тот же синтаксис, что и в языке Си. Главное здесь - разобрать смысл используемых библиотечных функций. Легко видеть, что функция Message просто выводит строку в окно сообщений, которое находится под основным окном. Функция ChooseFunction вызывает окно, которое вызывается также из меню: "Jump to Function". Функция GetFunctionFlags возвращает информацию об указанной функции. Наконец функция NextFunction осуществляет переход на следующую функцию. Она возвращает адрес функции. Аргументом же ее является адрес функции, от которой осуществляется переход на следующую функцию. Оставляю Вам изучение командного языка, поддерживаемого IDA PRO, который представлен в помощи программы.

Теоретически можно написать любую сколь угодно сложную программу по анализу дизассемблированного кода.

8. Программа IDA PRO осуществляет дизассемблирование модулей самых различных форматов: .OBJ, .EXE, .DLL, VXD, .ZIP, .NLM и др.

9. Функциональность IDA PRO может быть значительно усилена посредством подключаемых модулей - plugin'ов. Подключаемые модули пишутся на языке C++ и имеют структуру РЕ-модулей. Подключение модулей осуществляется через горячие клавиши или через пункты меню Edit\Plugins. Подключаемые модули находятся в специальном каталоге Plugins, где находится и файл конфигурации, где указаны эти модули.

10. Еще одна приятная особенность дизассемблера - он создает ассемблерный файл, с которым затем можно работать уже в текстовом режиме.



Глава 3. Введение в Turbo Debugger

I

Отладчик фирмы Borland - это весьма мощное средство отладки программ. Этот отладчик разрабатывался еще для DOS-программирования и ориентирован в основном на языки фирмы Borland. Существенно то, что отладчик позволяет отлаживать программу как на уровне дизассемблированных команд микропроцессора, так и на символьном уровне, т.е. с использованием текста программы. В последнем случае требуется, чтобы при трансляции в исполняемый модуль была помещена отладочная информация.

Рассмотрим, например, программу на Рис. 3.4.1. Эта программа выводит в окно типы устройств (список доступных дисков). При трансляции с помощью TASM32 добавим также ключ /zi, а при компоновке (TLINK.32.EXE) ключ -v. В этом случае в исполняемом модуле будет сохранена информация, необходимая для символьной отладки.

Тут важно иметь в виду, что для символьной отладки, т.е. с использованием текста программы, необходим не только сам исполняемый модуль и отладочная информация в нем, но и сам программный текст. Дело в том, что в исполняемом модуле хранится информация, позволяющая сопоставлять машинные коды программ и текст программы. На Рис. 4.3.1 представлено окно отладчика с загруженной отладочной информацией программы DRIV.ASM (Рис. 3.4.1). Отладчик позволяет выполнять программу в пошаговом режиме (подробнее о режимах выполнения программы см. далее в главе), видя одновременно текст программы и дизассемблированный текст, и передвигаясь по нему, видеть результат каждого шага.

На Рис. 4.3.1 представлены три наиболее часто используемых окна отладчика: окно с текстом программы (модульное окно), окно CPU, где хранится дизассемблированный текст, а также текущая информация о регистрах и флагах, и окно сообщений, которое очень важно при отладке оконных GUI-приложений.

Рассмотрим команды отладчика и возможности отладчика.

Запуск отлаживаемого приложения. Выполняется по клавише F9 или по команде RUN из меню RUN. Программа выполняется в полном объеме, если только не указана точка останова (breakpoint) на строку в окне с текстом программы или окне CPU. Точка останова устанавливается при помощи клавиши F2. Открыв окно Watch (View/ Watches), Вы можете установить там слежение за переменными, которые Вам интересны. В этом случае точки останова окажутся очень полезными.

Выполнить программу до строки, где находится курсор. Команда выполняется при помощи клавиши F4 или посредством меню. Смысл данной команды заключается в том, что, заинтересовавшись каким-либо местом программы, Вы можете выполнить программу до данной строки и посмотреть результат выполнения. Фактически это та же точка останова, но работает более оперативно. Кроме того, можно заставить выполняться программу до данного адреса (Alt+F9).

Выполнить единичную команду. Команда выполняется по нажатию клавиши F7. Если мы находимся в окне с текстом программы, то выполняется оператор соответствующего языка. Если мы находимся в окне CPU, то выполняется инструкция микропроцессора.

Рис. 4.3.1. Окна Turbo Debugger.

Выполнение команды с переходом через процедуру. Данная команда выполняется по нажатию клавиши F8. Отличается от предыдущей команды тем, что не происходит вход в процедуру, но все инструкции в процедуре выполняются автоматически.

Выполнить процедуру. Если курсор находится на команде вызова процедуры, то можно выполнить лишь одну эту процедуру командой Alt+F8.

Выполнение с задержкой. Можно заставить отладчик выполнять программу с задержкой после каждой команды (Animation) - команда Animate меню Run.

При выполнении любой из перечисленных команд также могут быть заданы опции командной строки - Run/Arguments...

Рассмотрим теперь окна, которые используются при отладке в данном отладчике. С некоторыми из них мы уже познакомились, но все равно повторимся. Показать на экране то или иное окно можно посредством меню View. Мы опишем часть наиболее важных окон.

Текст программы можно увидеть на экране, обратившись к пункту меню Module. При этом программа должна быть оттранслирована с опциями, обеспечивающими сохранение в исполняемом модуле отладочной информации. К сожалению, речь идет только о продуктах фирмы Borland: C++, Delphi, Assembler. Отладочная информация других фирм Turbo Debugger не распознает. В дальнейшем мы рассмотрим пример отладки программы, написанной на Borland C++.

Если отладочную информацию в исполняемом модуле Turbo Debugger не распознает, Вам при отладке придется использовать окно CPU. Окно разделено на пять частей (см. Рис. 4.3.2 против часовой стрелки):
1. Инструкции микропроцессора.
2. Сегмент данных.
3. Сегмент стека.
4. Состояние регистров.
5. Основные флаги.

Рис. 4.3.2. Окно CPU отладчика Turbo Debugger.

Окно слежения за переменными. Окно открывается через View/Watches. Щелкнув правой кнопкой мыши, мы можем по желанию добавить в окно переменные, за значением которых мы хотим наблюдать. При пошаговом выполнении программы, либо при выполнении с точками останова можно на каждом этапе контролировать значение переменных.

Окно стека. Показывает текущее состояние стека. Причем первая вызванная функция будет находиться на дне стека.

Окно точек останова. Здесь содержится информация обо всех установленных в программе точках останова. Здесь же можно добавить или установить точки останова.

Окно регистрации (Log). В нем хранится информация о происходящих в отладчике событиях.

Окно переменных. Отображает все переменные, доступные в данном месте.

Окно файлов. В этом окне можно просмотреть двоичный файл и исправить его при необходимости.

Окно отображения памяти. Дает возможность построчного просмотра памяти.

Окно сопроцессора. Данное окно отображает текущее состояние сопроцессора.

Окно истории выполнения (Execution History). Окно содержит историю выполнения программы.

Окно иерархии (Hierarchy). Выводит на дисплей иерархическое дерево для всех объектов и всех типах классов, используемых в текущем модуле. Пример окна, отображающего иерархию классов объектов, см. на Рис. 4.3.3.

Рис. 4.3.3. Пример окна Turbo Debugger, отображающего иерархию классов.

Окно потоков. Данное окно содержит информацию обо всех потоках, работающих в данный момент в программе.

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

Рис. 4.3.4. Пример содержимого окна сообщений

Окно буфера обмена. С помощью данного окна можно следить за содержимым буфера обмена при выполнении программы.

II

Поговорим теперь об отладке программ, написанных на языке высокого уровня. Если при трансляции в исполняемом модуле была сохранена отладочная информация, то Turbo Debugger будет работать и с языком высокого уровня. Рассмотрим, например, простую консольную программу, демонстрирующую пузырьковую сортировку. Программа располагается на Рис. 4.3.6.

Puc. 4.3.5. Программа, расположенная на Рис. 4.3.6 в окне отладчика.

#include <windows.h>
#include <stdio.h>
void sort(DWORD a[], DWORD );
void main()
{
DWORD a[10];
DWORD j,p,k,r;
randomize();
for(j=0; j<10; j++) a[j]=random(10) ;
// сортировка
for(k=9; k>l; k--)
{
r=0;
for(j=0; j<k; j++)
{
if(a[j]>a[j+1]){ p=a[j+1] ;
a[j+1]=a[j] ;
a[j]=p; r=1;
};
}
if(r==0) break;
}
for(j=0; j<10; j++) printf("%lu\n",a[j]);
ExitProcess(0);
}

Рис. 4.3.6. Пример простой консольной программы.

На Рис. 4.3.5 показано окно отладчика Turbo Debugger с данной программой. В нижней части рисунка расположено окно Watches. В этом окне отслеживается значение элементов массива а[]. Обратившись к окну CPU, можно увидеть, как алгоритм пузырьковой сортировки, реализованный на языке Си, преобразуется в ассемблерный код.

III

Основной целью отладчика Turbo Debugger является отладка программы, имеющей отладочную информацию. Дело в том, что хотя отладчик и дизассемблирует программу, и мы можем видеть дизассемблированный код в окне CPU, такие дизассемблеры, как W32Dasm и IDA Pro намного превосходят в этой части Turbo Debugger. Отладчик же Ice, работая в нулевом кольце, также более удобен для анализа исполняемых модулей. Отладчик Turbo Debugger занимает свою нишу в отладке, но здесь он незаменим и очень удобен. В данном разделе мы намерены рассмотреть некоторые вопросы методики отладки.

Вообще процесс отладки можно разделить на четыре этапа:

    Обнаружение ошибки. Обнаружение ошибок в программе связано с тестированием программы и ее эксплуатацией. Поиск ее местонахождения. Данный этап часто оказывается самым трудоемким, и именно здесь может пригодиться использование отладчика. Профессиональные программисты знают, что в сложных алгоритмах найти ошибку путем простого анализа текста программы бывает очень сложно. Можно, конечно, выводить промежуточные результаты, но для этого может понадобиться слишком много таких выводов. Хороший отладчик в этом случае незаменим, поскольку может позволить отслеживать значение переменных на каждом шаге. Определение причины ошибки. Отбрасывая тот случай, когда причиной ошибки является неправильный алгоритм, рассмотрим типичные ассемблерные ошибки.
      Ошибка в порядке следования операндов. Например, MOV EAX,EBX вместо MOV EBX,EAX. При использовании рекурсивных алгоритмов или слишком большой вложенности вызовов процедур может быть исчерпан стек. При вызове процедур может быть испорчено содержимое того или иного регистра. Неосвобождение стека при выходе из процедуры. Неправильное использование условных переходов - JA вместо JNA и т.п. Часто при организации циклических алгоритмов программисты ошибаются относительно последних значений переменных. Неправильная установка флага направления. Ошибка при определении границ переменных и массивов. Эти ошибки часто приводят к тому, что портится содержимое и других переменных. Неправильное преобразование из одного типа операнда к другому. Например, после загрузки MOV AL,BL используется EAX и забывается об обнулении старших байтов регистра EAX.
    Исправление ошибки. Если найденная ошибка проста и очевидна для Вас, то исправить ее не составит большого труда. Кстати, как Вы, наверное, уже поняли, Turbo Debugger не позволяет исправлять исполняемые модули. Но случаются и ситуации, когда очевидно, что данный участок программы (или процедура) выдает ошибочное значение, а на то, чтобы найти ошибку, у Вас не хватает времени - участок достаточно сложен. Иногда неправильное значение выходной информации возникает, лишь при редком сочетании входных параметров. В этом случае может быть применен простой прием: между указанным участком и остальной частью программы вставляется несколько строк, проверяющих выходную информацию и исправляющих ее, если нужно. Такой прием часто оказывается незаменим при доработке чужой программы очень большого объема, когда понять логику чужого алгоритма (да еще содержащего ошибку) бывает просто невозможно.


Глава 4. Описание работы с дизассемблером W32Dasm и отладчиком ICE

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

I

Программа W32Dasm (Windows Disassembler) представляет собой симбиоз довольно мощного дизассемблера и отладчика. Версия 8.93 программы, наиболее распространенная в настоящее время, может работать не только с РЕ-модулями, но и DOS-, NE-, -LE- модулями. Я намерен довольно полно описать работу с этой программой.

Начало работы. Внешний вид программы показан на Рис. 4.4.1. Меню дополняется панелью инструментов, элементы которой активизируются в зависимости от ситуации.

Рис. 4.4.1. Внешний вид программы W32Dasm.

Как уже было сказано, программа является дизассемблером и отладчиком в одном лице. Это отражено также в двух пунктах меню: "Disassembler" и "Debugger". Соответственно, имеются отдельные настройки для дизассемблера и отладчика. Для дизассемблера имеется всего три опции, касающиеся анализа перекрестных ссылок в условных переходах, безусловных переходах и вызовах процедур. По умолчанию все три опции установлены. Отмена этих опций нежелательна, т.к. снижает информативность дизассемблированного текста. В принципе, это может понадобиться при дизассемблировании очень большой программы, так это несколько ускоряет процесс анализа.

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

Рис. 4.4.2. Опции отлпдчика

Для начала работы с исполняемым модулем достаточно выбрать нужный файл в меню "Disassembler\0pen File...". После этого программа производит анализ модуля и выдает дизассемблированный текст, а также весьма полную информацию о секциях модуля54. W32Dasm весьма корректно распознает API-функции и комментирует их (см. Рис. 4.4.3).

После работы с модулем можно создать проект работы при помощи пункта "Disassembler\Save Disassembler...". По умолчанию проект сохраняется в подкаталог "wpjfiles", который расположен в рабочем каталоге W32Dasm и состоит из двух файлов: с расширением "alf" - дизассемблированный текст, с расширением "wpj" - собственно сам проект. При повторном запуске можно открывать уже не модуль, а проект с помощью пункта "Project\0pen...".

* Possible Reference to String Resource ID=00001: "! >>1I5=85"
|
:00401013 6A01 push 00000001
:00401015 FF3518304000 push dword ptr [00403018]
* Reference To: USER32.LoadStringA, Ord:01A8h
|
:0040101B E8AE000000 Call 004010CE

Puc. 4.4.3. Фрагмент дизассемблированного текста.

Передвижение по дизассемблированному тексту. При передвижении по тексту текущая строка подсвечивается другим цветом, при этом особо выделяются переходы и вызовы процедур. Передвижение облегчается также с помощью пункта меню "Goto":
Goto Code Start - переход на начало листинга;
Goto Program Entry Point - переход на точку входа программы, наиболее важный пункт меню;
Goto Page - переход на страницу с заданным номером, по умолчанию число строк на странице составляет пятьдесят;
Goto Code Location - переход по заданному адресу, в случае отсутствия адреса учитывается диапазон и близость к другим адресам.

Другим способом передвижения по дизассемблированному тексту является пункт "Search" - поиск. Здесь нет никаких отличий от подобных команд других программ.

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

Кроме того, те адреса, куда производится переход, содержат список адресов, откуда производятся переходы. Подсветив строку, где расположен адрес, и дважды щелкнув правой кнопкой мыши по этому адресу, мы перейдем к соответствующей строке.

Данные. Есть несколько вариантов работы с данными.

Во-первых, имеется пункт меню "HexData\Hex Display of Data...", где можно просмотреть содержимое сегментов данных в шестнадцатеричном и строковом варианте. Кроме того, сам код программы также можно просматривать в шестнадцатеричном виде. Для этого используется пункт "HexData\Hex Display of Code...".

Во-вторых, имеется пункт меню "Refs\String Data References". Это весьма мощное и полезное средство. При выборе этого пункта появляется список строк, на которые имеются ссылки в тексте программы. Во всяком случае, это то, что сумел определить дизассемблер при анализе программы. Выбрав нужную строку, можно двойным щелчком перенестись в соответствующее место программы. Если ссылок на данную строку несколько, то, продолжая делать двойные щелчки, мы будем переходить во все нужные места программы. На Рис. 4.4.4 изображено окно ссылок на строковые типы данных.

Рис. 4.4.4. Окно ссылок на строки.

Как видно из рисунка, можно скопировать в буфер выбранную строку или все строки.

Импортированные и экспортированные функции. Список импортированных функций и модулей находится в начале дизассемблированного текста (см. Рис. 4.4.5). Кроме того, список импортированных функций можно получить из меню "Functions\Imports". Выбрав нужную функцию в списке, двойным щелчком можно получить все места программы, где вызывается эта функция. Экспортированные функции также можно получить в соответствующем окне, выбрав пункт "Functions\Exports".

Ресурсы. В начале дизассемблированного текста также описаны и ресурсы, точнее два основных ресурса-меню и диалоговое окно. Со списком этих ресурсов можно работать и в специальных окнах, получаемых с помощью пунктов меню программы "Refs\Menu References" и "Refs\Dialog references". Строковые ресурсы можно увидеть в уже упомянутом окне просмотра перечня строковых ссылок (см. Рис. 4.4.4). Остальные ресурсы данной версии программы, к сожалению, не выделяются.

Операции с текстом. Строки дизассемблированного текста могут быть выделены и скопированы в буфер либо напечатаны. Выделение строки осуществляется щелком левой кнопки мыши, когда курсор мыши расположен в крайнем левом положении. Для выделения группы строк дополнительно используется клавиша Shift. Выделенный фрагмент копируется специальной кнопкой, которая "загорается", когда фрагмент существует, либо отправляется на печатающее устройство.

Загрузка программ для отладки. Загрузить модуль для отладки можно двумя способами. С помощью пункта "Debug\Load Process" загружается для отладки уже дизассемблированный модуль. Пункт же "Debug\Attach to an Active Process" позволяет "подсоединяться" и отлаживать процесс, находящийся в памяти. После загрузки отладчика на экране появляются два окна. Первое окно информационное (Рис. 4.4.6), в документации оно называется "нижним левым окном отладчика". Второе окно управляющее (Рис. 4.4.7), называемое в документации "нижним правым окном отладчика".

+++++++++++++++++++ IMPORTED FUNCTIONS ++++++++++++++++++
Number of Imported Modules = 7 (decimal)
Import Module 001: ADVAPI32.dll
Import Module 002: KERNEL32.dll
Import Module 003: MPR.dll
Import Module 004: COMCTL32.dll
Import Module 005: GDI32.dll
Import Module 006: SHELL32.dll
Import Module 007: USER32.dll
+++++++++++++++++++ IMPORT MODULE DETAILS +++++++++++++++
Import Module 001: ADVAPI32.dll
Addr:000D9660 hint(0000) Name: RegCloseKey
Addr:000D966B hint(0000) Name: RegOpenKeyExA
Addr:000D967B hint(0000) Name: KegQueryValueExA
Addr:000D9692 hint(0000) Name: RegSetValueBxA
Import Module 002: KERNEL32.dll

Рис. 4.4.5. Фрагмент списка импортированных модулей и функций.

Информационное окно содержит несколько окон-списков: содержимое регистров микропроцессора, значение флагов микропроцессора, точки останова, содержимое сегментных регистров, история трассировок, история событий, базовые адреса, два дисплея данных. Ниже мы объясним также значения кнопок этого окна.

Обратимся теперь к управляющему окну. Кнопка "Run" запускает загруженную в отладчик программу, кнопка "Pause" приостанавливает работу программы, кнопка "Terminate" останавливает выполнение программы и выгружает ее из отладчика. Кнопки "Step Over" и "Step Into" используются для пошагового исполнения программы. Первая кнопка, выполняя инструкции, "перескакивает" код процедур и цепочечные команды с повторением, вторая кнопка выполняет все инструкции последовательно. Кроме того, имеются кнопки "AutoStep Over" и "AutoStep Into" для автоматического пошагового выполнения программы. В случае API-функций даже использование "Step Into" не приведет к пошаговому выполнению кода функции в силу того, что код функции не доступен для пользовательских программ. Очень удобно, что при пошаговом выполнении происходит передвижение не только в окне отладчика, но и в окне дизассемблера.

Рис. 4.4.6. Первое информационное окно отладчика.

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

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

Точки останова. В дизассемблированном тексте можно установить точки останова. Для этого следует перейти к нужной строке и воспользоваться клавишей F2 или использовать левую кнопку мыши при нажатой клавише Ctrl. Установка точки останова в окне дизассемблера тут же отражается в информационном окне и в управляющем окне - у отмеченной команды появляется префикс BP*. Удалить точку останова можно тем же способом, что и при установке. Точку останова можно сделать также неактивной. Для этого нужно обратиться к информационному окну и списку точек останова. Выбрав нужный адрес, щелкните по нему правой кнопкой. При этом "звездочка" у данной точки останова исчезнет, а строка в окне дизассемблера из желтой станет зеленой.

Рис. 4.4.7. Второе управляющее окно отладчика.

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

Модификация кода, данных и регистров. Отладчик позволяет модифицировать загруженный в него код. Сделать это можно, обратившись к кнопке "Patch Code" в управляющем окне (см. Рис. 4.4.8). Важно отметить, что модификации подвергается только код, загруженный в отладчик, а не дизассемблированный текст. Найдя нужное место в отлаживаемом коде, и модифицировав его, вы можете тут же проверить результат модификации, запустив программу. Если модификация оказалась правильной, можно приступать уже к модификации самого модуля.

Для модификации регистров и ячеек памяти исполняемого процесса, существует специальная кнопка "Modify Data" на панели информационного окна. Окно расположено на Рис. 4.4.9. Окно несколько загромождено элементами, но, присмотревшись, Вы поймете, что все элементы на своем месте. В верхней части окна расположены текущие значения основных флагов микропроцессора, которые Вы можете изменить. Для того чтобы модифицировать содержимое регистра или ячейку памяти следует вначале установить модифицирующую величину - "Enter Value". Далее следует выбрать нужный регистр и нажать кнопку слева от нее. Чтобы установить старое значение, следует нажать кнопку "R" справа от регистра. Чтобы изменить содержимое ячейки памяти, следует вначале записать адрес ячейки в поле "Mem Lock", а затем воспользоваться кнопкой "Mem". Другие операции, предоставляемые данным окном, также достаточно очевидны.

Рис. 4.4.8. Окно модификации отлаживаемого кода.

Отладчик позволяет выдавать дополнительную информацию о выполняемых API-функциях. Чтобы воспользоваться этим, необходимо сделать следующее. В управляющем окне установите флаги: "Enable Documented API Detail", "Stop Auto On API". Далее запустите программу на выполнение клавишей F5. При прохождении API-функции будет производиться остановка и на экране будет появляться окно с информацией о данной функции,

Поиск нужного места в программе. Часто требуется найти в дизассемблированном коде место, соответствующее месту исполняемой программы. Наиболее эффективно это можно сделать следующим образом. Загружаем в отладчик данный модуль. Запускаем его, доходим до нужного места и нажимаем кнопку "Terminate". В результате подсвеченная строка в дизассемблированном коде окажется как раз в нужном месте. Нужно только иметь в виду, что некоторые программы делают изменения, которые потом продолжают действовать. К таковым относятся, в частности, горячие клавиши.

Рис. 4.4.9. Окно модификации регистров и ячеек памяти. Дополнительные возможности для работы с API.

К использованию программы W32Dasm мы еще вернемся в последующих главах.


54 Хотя W32Dasm работает с разного типа модулями, мы рассматриваем только модули формата РЕ.


II

Отладчик SoftIce (версия 4.05) или просто Ice рассчитан для работы в Windows 9x и Windows NT. 55. Отладчик состоит из собственно отладчика (в английском варианте это "kernel-mode debugger", что можно перевести как "отладчик на уровне ядра"), кроме этого, в пакет SoftIce входит еще символьный загрузчик для загрузки в отладчик исполняемых модулей. Загрузчик позволяет прочитать отладочную информацию для продуктов фирмы Microsoft и Borland.

Итак, что дает отладка при помощи SoftIce? символьная и обычная отладка 32-битных приложений; отладка драйверов для Windows NT и для Windows 9x, отладка 16-битных приложений для MS DOS и Windows, отладка внутренних программ операционной системы; установка обычных точек останова на команду, стоящую по определенному адресу; установка точек останова на операции чтения/записи в память, чтения/записи в порты ввода-вывода; установка точек останова на сообщения Windows; установка условных точек останова, т.е. точек останова, срабатывающих при выполнении определенного условия; получение внутренней информации операционной системы; возможность использования отладчика на удаленной машине и др.

Отладчик SoftIce имеет разные исполнения для Windows 9x и для Windows NT. В первом случае он представляет собой VXD-драйвер и запускается из autoexec.bat (программа WINICE.EXE). В Windows NT он представляет собой драйвер уровня ядра - NTICE.SYS.

Начальная установка. Установка пакета достаточно проста. Основной вопрос, который возникает при установке - выбор видеоадаптера и мыши. Что касается видеоадаптера, то обычно рекомендуют выбрать стандартный VGA-адаптер, и с видеосистемой не будет никаких проблем. Отладчик SoftIce предполагает использование мыши для управления. Однако мышь является второстепенным средством управления, и можно обойтись без нее.

После установки и перезапуска Вам, скорее всего, понадобится еще некоторая настройка файла WINICE.DAT. Вот основные рекомендации.

    Строка PHYSMB=32 определяет реальный объем физической памяти в мегабайтах. Укажите реальный объем памяти на Вашем компьютере. Строка INIT= - определяет оконные установки. Лично мне нравятся установки, определяемые строкой:INIT="SET FONT 1; SET ORIGIN 30 30; LINES 65; WIDTH 90; WR; WF; WD 4; WL; WC 30; X;". Впрочем, Вы сами сможете подобрать параметры по Вашему вкусу. Но главное заключается в том, что, открыв окно отладчика, можно оперативно изменить существующие установки. В конце файла установок стоят строки типа ;EXP=c:\windows\system\kernel32.dll - снимите с них комментарий. Это необходимо сделать, чтобы отладчик распознавал импортируемые приложением функции стандартных динамических библиотек. В Windows 9x SoftIce при инсталляции помещает в конце файла AUTOEXEC.BAT строку типа C:\ICE\WINICE.EXE. При желании Вы всегда можете поставить комментарий на эту строку.

Загрузка программы для отладки. Для загрузки приложения в отладчик в пакете SoftIce имеется специальная программа LOADER32.EXE. Она используется для загрузки 32-битных приложений. Для загрузки 16-битных приложений используются утилиты, расположенные в подкаталоге Util16.

Итак, вернемся к программе LOADER32.EXE. Вид загрузчика показан на Рис. 4.4.10. Последовательность действий для загрузки программы в отладчик следующая: открыть модуль, транслировать модуль - преобразование отладочной информации в NMS-файл, загрузить модуль. Если загрузчик разобрался с отладочной информацией, то в окне отладчика появится программный текст, в противном случае Вам придется работать с обычным дизассемблированным текстом. После загрузки модуля, вы можете настроить запуск модуля в отладчике, воспользовавшись пунктом меню Module\Setting (Рис. 4.4.11). Настройка достаточно очевидна, поэтому мы не будем разъяснять смысл пунктов данного окна.

Рис. 4.4.10. Вид загрузчика SoftIce (LOADER32.EXE).

Рис. 4.4.11. Окно настройки запуска модуля в отладчике Softice.

Рис. 4.4.12. Окно отладчика SoftIce.

Обзор команд отладчика

    Вызов отладчика. Вызов отладчика осуществляется клавишами Ctrl+D, повторное нажатие этих клавиш приводит к закрытию отладчика. Закрытие окна отладчика можно осуществить и клавишей F5. Помощь. Для получения списка всех команд наберите в командном окне команду "h". Для получения информации по конкретной команде наберите "h <команда>". Нижняя часть командного окна - панель помощи. В ней появляются подсказки. Например, если Вы введете букву "w" (на эту букву начинаются оконные команды) на панели появится список команд, начинающихся на "w". Работа с окнами. При вызове отладчика на экране появляется его окно (см. Рис. 4.4.12.), разделенное на несколько окон, каждое из которых несет на себе определенную информационную нагрузку. Количество и размер этих окон определяется начальной установкой (см. начало), но может меняться по нашему желанию. Для ввода команд существует командное окно. Обычно курсор находится именно в командном окне. Каждое окно имеет свое мнемоническое обозначение, например "c" - окно кода, "d" - окно данных, "r" - окно регистров, "f" - окно сопроцессора, "w" - окно наблюдения за переменными, "s" - окно стека, "l" - окно локальных переменных, "x" - окно регистров Pentium III. В часть окон можно перейти командой Alt+"мнемоническое обозначение окна". Перейти можно в каждое такое окно, выяснить это, как Вы понимаете, достаточно просто. Обратный переход осуществляется той же командой. Для создания соответствующего окна, если оно отсутствует, используются команды, в которых первым идет символ "w", вторым символом является символ, обозначающий окно. Например, если у Вас нет на экране окна данных, то создать его можно командой wd, та же команда удалит окно данных из перечня показываемых окон. Высота окна определяется параметром данной команды, например wc 20. Запомните еще несколько полезных команд. Ctrl+"стрелка вверх-вниз" - скроллинг окна кода, Alt+"стрелка вверх-вниз" - скроллинг окна данных, Alt+Ctrl "стрелка вверх-вниз, вправо-влево" - передвижение самого окна отладчика. Просмотр кода и данных. Один способ передвижения, а значит и просмотра, Вы уже знаете: скроллинг окна кода осуществляется клавишами Ctrl+<стрелка вниз, стрелка вверх> (Alt для окна данных). Наиболее удобной является команда "U". Общий формат этой команды: U [address [length]] | [name]. Address - адрес непосредственный или определяемый через регистр, length - число выводимых байт, name - осуществлять скроллинг, пока не встретится данное имя. Например, u ebx -20 - вывести инструкции, начиная с адреса за 20 байт до адреса CS:EBX. Пустая команда u выводит коды, начиная с CS:EIP. Если вторым параметром идет l, то вывод осуществляется в командное окно. Аналогично для окна данных работает команда D, формат которой имеет вид: D [address], [length]. Например, d 100, 100 или d eax. Положим, видя команду типа MOV EAX,[EBX-10], мы можем посмотреть область данных, откуда берется значение EAX: d ebx-10. Просмотр - модификация регистров. Переход к окну регистров, как уже было сказано, может быть осуществлено командой Alt+r. Находясь в окне. Вы можете вместе с тем менять содержимое регистров. К аналогичному результату можно прийти, просто выполнив в командной строке команду "R". Возможна также команда вида r reg=знач. Например, r еах=10. Ключ -d позволяет выводить содержимое регистров в командное окно. Команда r есх переводит курсор прямо к нужному регистру, a r fl - прямо к регистру флагов. Команда вида r fl=o+a-p — устанавливает флаги "o" и "a" и сбрасывает флаг "p". Трассировка кода. Горячие клавиши: F8 - выполнение инструкции с заходом в процедуру, F10 - выполнение инструкции с обходом процедуры, F7 - выполнить инструкции до текущей команды (если Вы находитесь в окне кода). Нажатие клавиши F7 эквивалентно команде HERE. Клавиша F12 - выполнять код, пока не встретится команда RET (команда P с параметром RET). F11 - вернуться к последней выполненной команде CALL. В командной строке Вы можете использовать команду T, которая имеет следующий формат: Т [=address] [count]. Здесь address - адрес, с которого начинается выполнение, count - количество шагов. Точки останова. Самое мощное и удобное средство отладки программы SoftIce.
      Обычные точки останова. Список точек останова можно просмотреть при помощи команды BL. Каждой точке останова ставится в соответствие номер, который Вы увидите в списке. Удалить точку останова из списка можно командой ВС <номер или список>. Все точки останова можно удалить командой ВС *. Находясь в окне кода, Вы можете поставить точку останова, нажав клавишу F9, строка при этом будет подсвечена. Повторное нажатие клавиши F9 снимает точку останова с текущей строки. Команда BD снимает точку останова, но не удаляет ее из списка, команда BE вновь активизирует точку останова. Поставить точку останова можно также командой bpx addess. Точки останова на функции API. Например, bpx MessageBoxA поставит точку останова на любой вызов данной функции. Для того чтобы проверить, распознает отладчик функции данной группы, выполните команду exp MessageBox, при этом в командное окно будет выведен соответствующий список. Точки останова с условием. Общий формат команды bpx имеет вид: BPX [address] [if expression] [do "command1; command2.."]. Таким образом, точка останова сработает только при выполнении условия. При этом будет выполнена последовательность команд отладчика. Например, возможно такое выражение: BPX eip if eax=10 do "db bx". Точки останова на сообщения. Точку останова на сообщения можно поставить при помощи команды bmsg. При этом надо знать дескриптор окна. Но на это есть специальная команда hwnd, которая дает список окон, их дескрипторов и адресов их процедур. Например, BMSG 0b0f wm_destroywindow означает установить точку останова на сообщение WM_DESTROYWTNDOW, приходящее на окно с дескриптором 0b0fh. Поскольку сообщения представляют собой определенные целые числа, можно через пробел указывать диапазон сообщений. Кроме того, точка останова может быть дополнена условием (if) и набором команда (do).
    Другие команды. Команд в SoftIce неисчислимое количество, точнее около 200. Вы можете посмотреть описание их в документации, которая прилагается к пакету, либо выполнив в командной строке команду h(F1). Поражает обилие команд для получения системной информации. Думаю, что во всех этих возможностях Вы разберетесь сами.

Примеры использования программ W32Dasm и SoftIce вы найдете в последующих главах.

55 Автору не известно о возможностях использования отладчика в Windows 2000 либо новых версиях, обладающих такими возможностями.








Глава 5. Основы анализа кода программ

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

I

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

Для примера я взял простую консольную программу, написанную на Borland C++. В текстовом варианте программа занимает полтора десятка строк, тогда как ЕХЕ-файл имеет размер более 50 Кб. Впрочем, размер исполняемых файлов давно уже никого не удивляет. Интересно другое: корректно справился с задачей, т.е. корректно выявил точку входа - метку _main, только один дизассемблер - IDA PRO. Т.е., конечно, реально работающий участок программы дизассемблировали все, но выявить, как происходит переход на участок, смог только упомянутый мной дизассемблер. Приятно также и то, что аккуратно была распознана функция _printf. На Рис. 4.5.1 показан фрагмент дизассемблированной программы, соответствующей основной процедуре main. С другой стороны, в данном случае нет никаких видимых возможностей быстрого поиска данного фрагмента в отладчике. Осюда наглядно можно понять полезность совместного использования отладчика и дизассемблера.

CODE 00401108 _main proc near ; DATA XREF: DATA:0040B044
CODE 00401108
CODE 00401108 argc = dword ptr 8
CODE 00401108 argv = dword ptr 0Ch
CODE 00401108 envp = dword ptr 10h
CODE 00401108
CODE 00401108 push ebp
CODE 00401109 mov ebp, esp
CODE 0040110B push ebx
CODE 0040110C mov edx, offset unk_40D42C
CODE 00401111 xor eax, eax
CODE 00401113
CODE 00401113 loc_401113: ; CODE XREF: _main+22
CODE 00401113 mov ecx, 1Fh
CODE 00401118 sub ecx, eax
CODE 0040111A mov ebx, ds:off_40B074
CODE 00401120 mov cl, [ebx+ecx]
CODE 00401123 mov [edx+eax], cl
CODE 00401126 inc eax
CODE 00401127 cmp eax, 21h
CODE 0040112A jl short loc_401113
CODE 0040112C mov byte ptr [edx+20h], 0
CODE 00401130 push edx ; char
CODE 00401131 push offset aS ;_va_args
CODE 00401136 call _printf
CODE 0040113B add esp, 8
CODE 0040113E pop ebx
CODE 0040113F pop ebp
CODE 00401140 retn
CODE 00401140 _main endp

Рис. 4.5.1. Функция main консольного приложения. Дизассемблер с программы на Си, IDA PRO.

Попытаемся теперь понять, какая программа на Си была исходным источником данного фрагмента. Начнем с рассмотрения стандартных структур. Собственно, налицо только одна структура - цикл. Команда

CODE:0040112A jl short loc_401113
и является ключевой в организации цикла. Команда же inc eax, очевидно, инкрементирует параметр цикла. Т.о. в EAX хранится некая переменная, играющая роль параметра цикла. Назовем эту переменную i. Вышесказанное подтверждается и наличием команды xor eax,eax перед началом цикла. Команда эта, разумеется, эквивалентна просто i=0. Команда inc eax означает просто i++. Попытаемся выявить еще переменные. Обратим внимание на команду
CODE:0040110C mov edx, offset unk_40D42C
Команда весьма примечательна, т.к. в регистр EDX помещается некий адрес, адрес чего-то. Проследим далее, как используется регистр EDX. Команда
CODE:00401123 mov [edx+eax], cl
а также отсутствие в цикле других команд с регистром EDX убеждает нас, что EDX играет роль указателя на строку, массив или запись. Это нам предстоит сейчас выяснить. Тут примечательны две команды:
CODE:0040112C mov byte ptr [edx+20h],0
и
CODE:00401130 push edx ; char

Первая команда убеждает нас, что EDX указывает на строку, т.к. именно строка должна содержать в конце символ 0. Вторая команда является посылкой второго для функции printf. Исходя из этого, а также комментария IDA PRO (она раньше нас поняла, что это такое), заключаем, что EDX являет собой указатель на некую строку. Заметьте, что мы не обратились к просмотру блока данных, что, несомненно, ускорило бы заключение. Обозначим этот указатель как s1. В этой связи выражение [edx+eax] можно трактовать как s1[i] или как *(s1+i). Рассмотрим теперь команду

CODE:0040111A mov ebx, ds:off_40B074

несомненно, означающую, что и регистр EBX также представляет собой указатель на строку (будем обозначать ее s2), подтверждением этому также служат последующие строки, означающие переброску символов из строки s2 в строку s1. Вот об этом следует поговорить подробнее. Что означает последовательность команд

CODE:00401113 mov ecx, 1Fh
CODE:00401118 sub ecx, eax

Значить она может только одно: на каждом шаге цикла в ECX будут находиться числа от 1FH (31) до 0 (последним значением регистра EAX, то бишь переменной i, участвующим в команде sub ecx,eax будет 1FH). Поскольку в формировании содержимого регистра ECX участвует еще команда mov cx,1FH, логично предположить, что в регистре ECX перед операцией перекачки символов из одной строки в другую всегда будет находиться число 1FH-i или 31-i. Выражение [ebx+ecx] тогда будет эквивалентно s2[31-i] или *(s2+31-i). В результате имеем, что строки

CODE:00401120 mov cl, [ebx+ecx]
CODE:00401123 mov [edx+eax], cl
по сути, можно заменить на s1[i]=s2[31-i]. Думаю, что теперь мы готовы рассмотреть целый фрагмент:
CODE 00401111 xor eax, eax
CODE 00401113
CODE 00401113 loc_401113: ; CODE XREF: _main+22
CODE 00401113 mov ecx, 1Fh
CODE 00401118 sub ecx, eax
CODE 0040111A mov ebx, ds:off_40B074
CODE 00401120 mov cl, [ebx+ecx]
CODE 00401123 mov [edx+eax], cl
CODE 00401126 inc eax
CODE 00401127 cmp eax, 21h
CODE 0040112A jl short loc_401113

Надеюсь, что не ошибусь, написав на языке Си следующий фрагмент:

i=0;
do {
s1[i]=s2[31-i] ;
i++;
} while(i>0x20)

Все бы хорошо, но не понятным остается наличие в цикле строки

CODE:0040111A mov ebx, ds:off_40B074

Почему бы ни поместить, ее, например, перед циклом. Ну что ж, оставим это на совести компилятора. Что касается того, что цикл этот должен быть обязательно циклом DO, то это совсем не обязательно. В данном случае количество шагов цикла задано явно, и можно сделать один условный переход в конце цикла. Другими словами, приведенная выше структура могла быть результатом оптимизационного преобразования цикла WHILE.

Имеется и еще один вопрос: где хранятся строки s1 и s2? Здесь можно разобраться достаточно быстро.

main является самой обычной процедурой, и если бы строки являлись локальными переменными, то для них была бы зарезервирована область в стеке процедуры путем вставки команды SUB ESP,N (или ADD ESP,-N).

Таким образом, строковые переменные являются глобальными.

Интересно, что другие переменные, хотя они тоже локальные, хранятся в регистрах, в полном соответствии с принципом, что по мере возможности переменные следует хранить в регистрах. 56

Итак, окончательный результат наших изысканий может быть представлен на Рис. 4.5.2.

char s1[32];
char * s2="абвгдежзийклмнопрстуфхцчшщыьъэюя";
void main()
{
int i;
i=0;
do {
s1[i]=s2[31-i];
i++;
} while(i>32);
s1[32]=0;
printf("%s\n",s1);
}

Рис. 4.5.2. Окончательный вариант программы на Си.

Ну, уж чтобы совсем покончить с рассматриваемым примером, отмечу, что переменная s1 является неинициализированной, а переменная s2 - инициализированной. Инициализированные переменные хранятся в секции DATA, неинициализированные - в BSS (компилятор Borland C++).

56 В старом Си для того, чтобы для хранения переменных компилятор использовал регистры, переменные следует объявлять как AUTO.

II

В данном разделе мы рассмотрим некоторые классические структуры языка Си и их отображение в язык ассемблера.

1. Условные конструкции.

Неполная условная конструкция.

if (простое условие)
{
...
...
...
}
если условие простое, то оно, разумеется, заменяется следующей возможной последовательностью
CMP EAX,1
JNZ L1
...
...
...
L1:

Полная условная конструкция.

if (простое условие)
{
...
...
...
} else
{
...
...
...
}
CMP EAX,1
JNZ L1
...
...
...
JMP L2
L1:
...
...
...
L2:

Вложенные условные конструкции.

Здесь все достаточно очевидно.

CMP EAX,1
JNZ L1
CMP EBX,2
JNZ L1
...
...
...
L1:

Что, конечно, равносильно одному составному условию, связанному союзом "И". Союз "ИЛИ", как известно, заменяется проверкой условий в блоке "ELSE".

2. Оператор switch или оператор выбора.

Оператор switch весьма часто употребляется в функциях окон. Хорошее знание его ассемблерной структуры поможет Вам легче отыскивать эти функции в море ассемблерного кода.

switch(i)
{
case 1:
...
...
...
break;
case 3:
...
...
...
break;
case 5:
...
...
...
break;
}

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

DEC EAX
JZ L1
SUB EAX,2
JZ L2
SUB EAX,2
JZ L3
JMP L4
L1:
...
...
...
JMP L4
L2:
...
...
...
JMP L4
L3:
...
...
...
L4:

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

CMP EAX,10
JE L1
CMP EAX,5
JE L2
CMP EAX,11
JE L3
...

3. Циклы.

С организацией цикла в языке Си мы уже познакомились в этой главе. Отмечу две наиболее часто практикуемые структуры.

1-я структура.

L1:
...
...
...
CMP EAX,10
JL L1

2-я структура.

L2:
CMP EAX,10
JA L1
...
...
...
JMP L2
L1:

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

JMP L2
L1:
...
...
...
CMP EAX,10
JL L1
L2:

Как по мановению волшебной палочки, в последней структуре цикл "ДО" превращается в цикл "ПОКА".

В своем рассмотрении я не упомянул цикл "FOR" - этого "монстра" языка Си, но, в принципе, это все тот же цикл "ПОКА".

4. Локальные переменные.

Чаще всего нам приходится работать с локальными переменными. С глобальными переменными мы уже встречались, они хранятся во вполне определенных секциях. Хороший дизассемблер их легко локализует. С локальными переменными часто разбираться гораздо сложнее. Особенно это касается записей или массивов. Хороший дизассемблер значительно упростит задачу. Рассмотрим, например, следующий фрагмент, взятый из IDA PRO.

CODE 00401108 _main proc near ; DATA XREF: DATA:0040B044
CODE 00401108
CODE 00401108 var_54 = dword ptr -54h
CODE 00401108 var_28 = byte ptr -28h
CODE 00401108 argc = dword ptr 8
CODE 00401108 argv = dword ptr 0Ch
CODE 00401108 envp = dword ptr 10h
CODE 00401108
CODE 00401108 push ebp
CODE 00401109 mov ebp, esp
CODE 0040110B add esp, 0FFFFFFACh
CODE 0040110E push ebx
CODE 0040110F xor ebx, ebx

Рис. 4.5.3. Пример задания двух локальных массивов. Взят из отладчика IDA PRO.

Взгляните внимательно на Рис. 4.5.3. Как видите, отладчик нам все прописал. Две переменные var_54 и var_28 являются, несомненно, массивами типа DWORD. Причем если на первый отводится 28h байт, т.е. 40 байт или 10 элементов массива, то на второй 54h-28h=2CH=44 или 11 элементов массива. И всего, следовательно, под локальные переменные зарезервировано 84 байта. А что означает команда add esp,0FFFFFFACH ? Но нас не обманешь! 0-0FFFFFFACH = 54h, что в десятичном исчислении и есть 84. В связи с массивами хотелось бы упомянуть, что в Си изначально практиковалось два способа доступа к элементам массива: посредством индексов и посредством указателей. Другими словами, можно было написать: a[i]=10 и *(a+i)=10. Причем вторая запись оказывалась более эффективной (см. по этому поводу книгу [1], гл. 15.). Разумеется, делать это можно и сейчас, но обе записи теперь в Borland C++ и Microsoft C++ 6.0 приводят к совершенно одинаковым ассемблерным эквивалентам. Что весьма отрадно - значит, компиляторы действительно развиваются. Кстати, весьма поучительным было бы сравнение ассемблерного кода, производимого разными компиляторами. Мы не будем этим заниматься, замечу только, что мое весьма поверхностное сравнение компиляторов Borland C++ 5.0 и Visual C++ 6.0 привело к выводу, что средства, разработанные фирмой Microsoft, несколько более эффективно оптимизируют транслируемый код.

Но вернемся опять к фрагменту на Рис. 4.5.3. Посмотрим, как выглядит начало функции main в дизассемблере W32Dasm.

:00401108 55 push ebp
:00401109 8ВЕС mov ebp, esp
:0040110B 83C4AC add esp, FFFFFFAC
:0040110E 53 push ebx
:0040110F 33DB xor ebx, ebx

Рис. 4.5.4. Как выглядит фрагмент программы, представленный на Рис. 4.5.3 в окне дизассемблера W32Dasm.

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

5. Функции и процедуры.

Функции и процедуры идентифицируются достаточно просто. Структуру вызова и внутреннюю часть процедур Вы уже хорошо знаете. Остается только напомнить некоторые положения. Вызов процедуры:

PUSH par1
PUSH par2
PUSH par3
CALL 232343

Здесь все достаточно просто. Главное - распознать параметры и понять порядок помещения их в стек. Надо также иметь в виду, что существует протокол передачи параметров через регистры (см. главу 3.7). После вызова процедуры может стоять команда очистки стека ADD ESP,N.

Внутренняя часть процедуры также нами неоднократно разбиралась (см. Гл. 1.2, 3.7). Думаю, что она достаточно Вами узнаваема, и мы не будем здесь на этом останавливаться.

Не забывайте, что функции возвращают результат через регистр EAX. Это может помочь Вам быстро разобраться в назначении функции.

6. Оптимизация кода.

Выше мы пытались восстановить исходный текст программы по ассемблерному коду. Насколько это удалось, можно выяснить, сравнив получившийся текст с исходным. Следует заметить, что мы ошиблись: в исходном тексте стоит цикл while, а в нашем do. Однако, если откомпилировать получившуюся программу, она будет работать, так же как и исходная. Причина отличия двух текстов программ (весьма простых, кстати) заключается в том, что транслятор, кроме всего прочего, производит еще оптимизацию кода. Результат оптимизации - принципиальная невозможность восстановить исходный текст по исполняемому коду. Можно, однако, получить программный текст, правильно описывающий алгоритм исполнения. Можно просто понять то, что делает данный код, чем мы в данной главе и занимаемся.

Рассмотрим небольшую и весьма тривиальную программу на Си.

void main ()
{
int i;
char a[10];
char b[11];
for(i=0; i<10; i++)
{
a[i]='a';
*(b+i)='a';
printf("%c %c\n",a[i],b[i]);
}
ExitProcess(0);
}

Рис. 4.5.5.

Вот как выглядит ассемблерный код, полученный с помощью транслятора Borland C++ 5.0.

.text 00401108 _main proc near ; DATA XREF: .data:0040A0B8
.text 00401108
.text 00401108 var_18 = byte ptr -18h
.text 00401108 var_C = byte ptr -0Ch
.text 00401108 argc = dword ptr 8
.text 00401108 argv = dword ptr 0Ch
.text 00401108 envp = dword ptr 10h
.text 00401108
.text 00401108 push ebp
.text 00401109 mov ebp, esp
.text 0040110B add esp, 0FFFFFFE8h
.text 0040110E push ebx
.text 0040110F xor ebx, ebx
.text 00401111
.text 00401111 loc_401111: ; CODE XREF: _main+30
.text 00401111 mov [ebp+ebx+var_C], 61h
.text 00401116 mov [ebp+ebx+var_18], 61h
.text 0040111B movsx eax, [ebp+ebx+var_18]
.text 00401120 push eax
.text 00401121 movsx edx, [ebp+ebx+var_C]
.text 00401126 push edx ; char
.text 00401127 push offset aCC ; _va_args
.text 0040112C call _printf
.text 00401131 add esp, 0Ch
.text 00401134 inc ebx
.text 00401135 cmp ebx, 0Ah
.text 00401138 jl short loc_401111
.text 0040113A push 0 ; uExitCode
.text 0040113C call ExitProcess
.text 00401141 pop ebx
.text 00401142 mov esp, ebp
.text 00401144 pop ebp
.text 00401145 retn
.text 00401145 _main endp

Рис. 4.5.6. Дизассемблированный текст программы (Рис. 4.5.5). Транслятор Borland C++ 5.0.

Кстати, функция ExitProcess(0) введена в текст программы для быстрого поиска нужного фрагмента в отладчике или дизассемблированном коде. Сразу видно, что оптимизацией здесь и не пахло. По такому тексту достаточно просто восстановить исходную Си-программу. А вот код, оптимизированный транслятором Visual C++ 6.0 (Рис. 4.5.7).

.text 00401000 sub_401000 proc near ; CODE XREF: start+AF
.text 00401000 push esi
.text 00401001 mov esi, 0Ah
.text 00401006
.text 00401006 loc_401006: ; CODE XREF: sub_401000+18
.text 00401006 push 61h
.text 00401008 push 61h
.text 0040100A push offset aCC ; "%c %c\n"
.text 0040100F call sub_401030 ;printf
.text 00401014 add esp, 0Ch
.text 00401017 dec esi
.text 00401018 jnz short loc_401006
.text 0040101A push 0 ; uExitCode
.text 0040101C call ds:ExitProcess
.text 00401022 pop esi
.text 00401023 retn
.text 00401023 sub_401000 endp

Рис. 4.5.7. Дизассемблированный текст программы (Рис. 4.5.5). Транслятор Visual C++ 6.0.

Я думаю, что текст на Рис. 4.5.7 удивит Вас. Однако, что же здесь удивительного? Взгляните на текст Си-программы. Заданные нами два символьных массива - абсолютно ни к чему. Транслятор Visual C++ очень точно это подметил и изменил код так, что, как ни старайся, текст исходной программы восстановить не удасться. Конечно, такая оптимизация оказалась возможной только потому, что наши массивы используются в ограниченной области.

Рассмотрим далее некоторые способы оптимизации, которые могут пригодиться нам и при написании программ на ассемблере.

Скорость или объем. Это весьма важный вопрос, когда дело касается микропроцессора.

Например, команда MOV EAX,EBX выполняется быстрее команды XCHG EAX,EBX, но длиннее ее. Зная такую особенность, можно либо сокращать объем, либо увеличивать скорость программы. Особенно часто используется замена такой операции, как MUL, другими командами, в частности SHL. Это может значительно увеличить скорость выполнения программы и увеличить ее объем. Интересно, что арифметические действия можно производить и с помощью команды LEA (см. Приложение 2), и современные трансляторы уже взяли это на вооружение. Так что команда MUL не так часто может встретиться в оттранслированном коде, как об этом можно подумать, исходя из текста программы. Вообще, свойство команд надо исследовать, и иногда узнаешь довольно интересные вещи. Например, нахождение остатка от деления числа без знака на 4 производится следующим образом: AND EAX,0003h - не правда ли, оригинально?

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

...
CMP EAX, 100
JB L1
Фрагмент 1
JMP L2
L1:
Фрагмент 2
L2:

Мы знаем, что содержимое EAX чаще всего оказывается меньше 100. Следовательно, фрагмент можно заменить на следующий.

...
CMP EAX,100
JNB L1
Фрагмент 2
JMP L2
L1:
Фрагмент 1
L2:

Оптимизация вызовов процедур. Рассмотрим следующий фрагмент.

P1 PROC
...
...
...
CALL P2
RET
P1 ENDP
...
...
...
P2 PROC
...
...
...
RET
P2 ENDP

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

Р1 PROC
...
...
...
JMP P2
Р1 ENDP
...
...
...
P2 PROC
...
...
...
RET
P2 ENDP

Код становится и быстрее, и короче, вот только разобраться в нем становится сложнее. На этом мы оставляем вопрос оптимизации. Всех интересующихся могу отослать к книге [14].

6. Объектное программирование. Объектное программирование в значительной степени усложняет получаемый исполняемый код. Мы рассмотрим один простой пример. На Рис. 4.5.7 представлена программа на C++.

#include <windows.h>
#include <stdio.h>
class stroka {
public:
char с[200];
char g[100];
stroka() { strcpy(c,"Privet"); strcpy(g,"Privet");}
int strcp();
};
stroka::stri()
{
strcpy(c,g);
return 0;
}
main()
{
stroka * s = new stroka;
s->strcp();
printf("%s\n",s->c);
delete s;
}

Рис. 4.5.7. Программа на Си++, использующая объекты.

Ниже на Рис. 4.5.8. представлен дизассемблированный код процедуры main.

CODE 00401122 _main proc near ; DATA XREF: DATA:0040C044
CODE 00401122
CODE 00401122 var_28 = dword ptr -28h
CODE 00401122 var_18 = word ptr -18h
CODE 00401122 dest = dword ptr -4
CODE 00401122 argc = dword ptr 8
CODE 00401122 argv = dword ptr 0Ch
CODE 00401122 envp = dword ptr 10h
CODE 00401122
CODE 00401122 push ebp
CODE 00401123 mov ebp, esp
CODE 00401125 add esp, 0FFFFFFD8h
CODE 00401128 push ebx
CODE 00401129 mov eax, offset stru_40C084
CODE 0040112E call @_InitExceptBlockLDTC
CODE 00401133 push 12Ch
CODE 00401138 call unknown_libname_8
CODE 0040113D pop ecx
CODE 0040113E mov [ebp+dest], eax
CODE 00401141 test eax, eax
CODE 00401143 jz short loc_40117D
CODE 00401145 mov [ebp+var_18 ], 14h
CODE 0040114B push offset aPrivet ; src
CODE 00401150 push [ebp+dest] ; dest
CODE 00401153 call _strcpy
CODE 00401158 add esp, 8
CODE 0040115B push offset aPrivet_0 ; src
CODE 00401160 mov edx, [ebp+dest]
CODE 00401163 add edx, 0C8h
CODE 00401169 push edx ; dest
CODE 0040116A call _strcpy
CODE 0040116F add esp, 8
CODE 00401172 mov [ebp+var_18], 8
CODE 00401178 mov ebx, [ebp+dest]
CODE 0040117B jmp short loc_401180
CODE 0040117D ; ------------------------------------
CODE 0040117D
CODE 0040117D loc_40117D: ; CODE XREF: _main+21
CODE 0040117D mov ebx, [ebp+dest]
CODE 00401180
CODE 00401180 loc_401180: ; CODE XREF: _main+59
CODE 00401180 push ebx ; dest
CODE 00401181 call sub_401108
CODE 00401186 pop ecx
CODE 00401187 push ebx ; char
CODE 00401188 push offset aS ; _va_args
CODE 0040118D call _printf
CODE 00401192 add esp, 8
CODE 00401195 push ebx ; block
CODE 00401196 call @$bdele$qpv ; operator delete(void *)
CODE 0040119B pop ecx
CODE 0040119C mov eax, [ebp+var_28]
CODE 0040119F mov large fs:0, eax
CODE 004011A5 xor eax, eax
CODE 004011A7 pop ebx
CODE 004011A8 mov esp, ebp
CODE 004011AA pop ebp
CODE 004011AB retn
CODE 004011AB _main endp

Рис. 4.5.8. Дизассемблированный код функции main с Рис. 4.5.7. Дизассемблер IDA PRO.

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

    Оператор new сводится к выполнению библиотечной процедуры: unknown_libname_8. Последняя процедура выделяет память для свойств экземпляра объекта (300 байт). Конструктор хранится и выполняется непосредственно в теле программы. Это связано с тем, что и сам конструктор определен в тексте класса. Для эксперимента попробуйте вынести текст конструктора в отдельную функцию, и Вы увидите, что конструктор будет вызываться из main, как обычная функция. Процедуру @_InitExceptBlockLDTC вставляет транслятор для поддержки так называемых исключений (Exception). Вы можете удалить информацию для использования исключений, несколько сократив получившийся код, но тогда Вы не сможете использовать операторы исключений, такие как try или catch. При вызове какого-либо метода в стек всегда помещается, по крайней мере, один параметр - указатель на экземпляр объекта.

Приведу теперь фрагмент той же дизассемблированной программы, но с использованием опции -x, что для транслятора Borland C++ означает - не использовать исключения. Как видите, текст программы оказался значительно проще.

.text 00401122 _main proc near ; DATA XREF: .data:0040B0C8
.text 00401122
.text 00401122 argc = dword ptr 8
.text 00401122 argv = dword ptr 0Ch
.text 00401122 envp = dword ptr 10h
.text 00401122
.text 00401122 push ebp
.text 00401123 mov ebp, esp
.text 00401125 push ebx
.text 00401126 push 12Ch
.text 0040112B call unknown_libname_8
.text 00401130 pop ecx
.text 00401131 mov ebx, eax
.text 00401133 test eax, eax
.text 00401135 jz short loc_40115D
.text 00401137 push offset aPrivet ; src
.text 0040113C push ebx ; dest
.text 0040113D call _strcpy
.text 00401142 add esp, 8
.text 00401145 push offset aPrivet_0 ; src
.text 0040114A lea edx, [ebx+0C8h]
.text 00401150 push edx ; dest
.text 00401151 call _strcpy
.text 00401156 add esp, 8
.text 00401159 mov ecx, ebx
.text 0040115B jmp short loc_40115F
.text 0040115D ;-----------------———-—----—-—---———
.text 0040115D
.text 0040115D loc_40115D: ; CODE XREF: _main+13
.text 0040115D mov ecx, ebx
.text 0040115F
.text 0040115F loc_40115F: ; CODE XREF: _main+39
.text 0040115F mov ebx, ecx
.text 00401161 push ebx ; dest
.text 00401162 call sub_401108
.text 00401167 pop ecx
.text 00401168 push ebx ; char
.text 00401169 push offset aS ; _va_args
.text 0040116E call _printf
.text 00401173 add esp, 8
.text 00401176 push ebx ; handle
.text 00401177 call _rtl_close
.text 0040117C pop ecx
.text 0040117D xor eax, eax
.text 0040117F pop ebx
.text 00401180 pop ebp
.text 00401181 retn
.text 00401181 _main endp

Рис. 4.5.8. Дизассемблированный код функции main с Рис. 4.5.7, при трансляции использована опция -x (исключить исключения, Borland C++). Дизассемблер IDA PRO.










Глава 6. Исправление исполняемых модулей

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

I

Сейчас мы рассмотрим простой57 пример, демонстрирующий некоторые приемы такого типа работы. Задача, которую ставим перед собой, не так сложна, и решить ее можно, воспользовавшись только дизассемблером W32Dasm.

Данная программа (Allscreen - программа, с помощью которой можно "снимать" окна и отдельные части экрана) попала ко мне как Shareware Release. Программа написана на Delphi, но мы увидим, что решить поставленную задачу можно, и не зная, на чем написана программа. При запуске ее на экране появляется окно, изображенное на Рис. 4.6.1. Ближе познакомившись с предметом, Вы убедитесь, что чаще всего приходится искать место в программе, соответствующее какому-либо визуальному эффекту: открытие окна, закрытие окна, вывод текста и т.п.

Рис. 4.6.1. Окно, появляющееся при запуске программы Allscreen.

При выборе кнопки "Accept" возникает задержка секунд в шесть (см. Рис. 4.6.2.). Далее программа работает нормально.

Рис. 4.6.2. Окно задержки.

После 15-ти запусков появляется окно Рис. 4.6.3 и происходит выход из программы.

Рис. 4.6.3. Сообщение об истечении времени работы программы.

Таким образом, следует решить две проблемы:

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

Рис. 4.6.2 являет собой явный "прокол" авторов программы. Дело в том, что окно и все его содержимое можно спрятать в ресурсы. Но когда на том же окне появляется новая запись - это уже программный код. Итак, запускаем W32Dasm и считываем туда программу All Screen. Запускаем окно SDR (String Data Reference), ищем строку Shareware Delay, дважды щелкаем по ней и, закрыв его, оказываемся в нужном месте программы. Вот этот фрагмент (Рис. 4.6.4.).

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:004420BC(C)
|
:00442123 33D2 xor edx, edx
:00442125 8B83B0010000 mov eax, dword ptr [ebx+000001B0]
:0044212B E8541DFDFF call 00413E84
:00442130 33D2 xor edx, edx
:00442132 8B83B4010000 mov eax, dword ptr [ebx+000001B4]
:00442138 E8471DFDFF call 00413E84
:0044213D 33D2 xor edx, edx
:0044213F 8B83B8010000 mov eax, dword ptr [ebx+000001B8]
:00442145 E83A1DFDFF call 00413E84
:0044214A BA50000000 mov edx, 00000050
:0044214F 8B83BC010000 mov eax, dword ptr [ebx+000001BC]
:00442155 E8D618FDFF call 00413A30
* Possible StringData Ref from Code Obj ->"Shareware Delay"
:0044215A BAA8214400 mov edx, 004421A8
:0044215F 8B83BC010000 mov eax, dword ptr [ebx+000001BC]
:00442165 E8EE1DFDFF call 00413F58
:0044216A 33D2 xor edx, edx
:0044216C 8B83C0010000 mov eax, dword ptr [ebx+000001C0]
:00442172 E80D1DFDFF call 00413E84
:00442177 33D2 xor edx, edx
:00442179 8B83C4010000 mov eax, dword ptr [ebx+000001C4]
:0044217F E8001DFDFF call 00413E84
:00442184 33D2 xor edx, edx
:00442186 8B83C8010000 mov eax, dword ptr [ebx+000001C8]
:0044218C E8F31CFDFF call 00413E84
:00442191 8B83CC010000 mov eax, dword ptr [ebx+000001CC]
:00442197 E8E8D4FFFF call 0043F684
:0044219C 5B pop ebx
:0044219D C3 ret

Рис. 4.6.4. Фрагмент кода, осуществляющего, в частности, задержку.

Я сразу взял несколько больше кода, захватив и несколько верхних строк. По сути дела, перед нами вся процедура задержки. Нет смысла пытаться понять, что означает та или иная команда call, хотя легко сообразить, что, например, call 00413E84 убирает строку с экрана.

Для того чтобы решить проблему задержки, достаточно "выключить" этот фрагмент из программы. Проще всего это можно сделать, поставив в начало команды pop ebx / ret, используя такой редактор, как HIEW. В результате задержка исчезает.

Перейдем теперь ко второй проблеме — ограничение на количество запусков. Уже из самого вида окна ясно, что оно формируется в самой программе. Следовательно, опять можно попытаться найти текст, который изображается на экране, в самой программе.

:00443326 8ВС0 mov eax, eax
:00443328 53 push ebx
:00443329 8BD8 mov ebx, eax
:0044332B 803DEC56440001 cmp byte ptr [004456EC], 01
:00443332 7546 jne 0044337A
:00443334 A124564400 mov eax, dword ptr [00445624]
:00443339 E84E2CFEFF call 00425F8C
:0044333E A1D8564400 mov eax, dword ptr [004456D8]
:00443343 E87816FEFF call 004249CO
:00443348 FF05F0564400 inc dword ptr [004456F0]
:0044334E C605EC56440000 mov byte ptr [004456EC], 00
:00443355 833DF05644000F cmp dword ptr [004456FO], 0000000F
:0044335C 7E1C jle 0044337A
:0044335E 6AOO push 00000000
:00443360 668BODB0334400 mov cx, word ptr [004433B0]
:00443367 B202 mov dl, 02
* Possible StringData Ref from Code Obj ->"This Software Has Been Used Over"
|
:00443369 B8BC334400 mov eax, 004433BC
:0044336E E8BDAEFEFF call 0042E230
:00443373 8BC3 mov eax, ebx
:00443375 E84214FEFF call 004247BC
* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
|:00443332(C), :0044335C(C)
:0044337A 33D2 xor edx, edx
:0044337C 8B83F4010000 mov eax, dword ptr [ebx+000001F4]
:00443382 E8A52DFFFF call 0043612C
:00443387 33D2 xor edx, edx
:00443389 8B83F8010000 mov eax, dword ptr [ebx+000001F8]
:0044338F E8982DFFFF call 0043612C
:00443394 33D2 xor edx, edx
:00443396 8B83FC010000 mov eax, dword ptr [ebx+000001FC]
:0044339C E88B2DFFFF call 0043612C
:004433A1 33D2 xor edx, edx
:004433A3 8B8314020000 mov eax, dword ptr [ebx+00000214]
:004433A9 E87E2DFFFF call 0043612C
:004433AE 5B pop ebx
:004433AF C3 ret

Рис. 4.6.5. Фрагмент кода проверки количества запусков.

Опять мы представляем весь необходимый фрагмент. Найденной выше ссылкой на искомую строку легко обнаруживаем "подозрительные" команды:

cmp dword ptr [004456F0], 0000000F
jle 0044337A

Вспомним, что программа перестает работать как раз после пятнадцати запусков. Проще всего исправить ситуацию, "забив" фрагмент программы с 0044335E по 00443375 командами NOP (90), используя редактор HIEW.

II

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

Для примера я взял программу FILES.EXE. Эта поисковая программа очень удобна для поиска информации в локальной сети. Выберем, например, одно из диалоговых окон данной программы и поставим цель найти функцию этого диалогового окна. Сделаем это двумя способами: с помощью отладочных средств программы W32Dasm и с помощью отладчика SoftIce. На Рис. 4.6.6. показаны несколько открытых диалоговых окон программы. Последнее диалоговое окно с заголовком "Название сетевого компьютера" и будет объектом нашего внимания.

Рис. 4.6.6. Будем искать функцию последнего диалогового окна.

И так обратимся вначале к программе W32Dasm. Имеется последовательность шагов поиска:

    Загрузим туда программу FILES.EXE. Затем обратимся к пункту Debug\Load Process и выходим в отладчик. Запускаем программу при помощи клавиши F9 и выходим на нужное окно. Теперь решающий момент. Нажимаем клавишу "Terminate Process" и снова оказываемся в окне дизассемблера. Глянув чуть выше, Вы обнаружите вызов API-функции DialogBoxParam. На Рис. 4.6.7 изображен нужный фрагмент.

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:004156BE(C)
|
:004156CA FF7508 push [ebp+08]
:004156CD E8369EFFFF call 0040F508
:004156D2 59 pop ecx
:004156D3 6A00 push 00000000
:004156D5 68019C4000 push 00409C01
:004156DA FF7508 push [ebp+08]
* Possible StringData Ref from Data Obj ->"DIAL3000"
|
:004156DD 6896904300 push 00439096
:004156E2 FF3548CA4300 push dword ptr [0043CA48]
* Reference To: USER32.DialogBoxParamA, Ord:0000h
|
:004156E8 E85FOD0200 call 0043644C
:004156ED 8A153C324D00 mov dl, byte ptr [004D323C]

Рис. 4.6.7. Фрагмент с вызовом функции DialogBoxParam.

Вспомним параметры функции DialogBoxParam. Четвертый параметр как раз представляет собой адрес процедуры окна. Итак, получаем адрес 00409C01. Обратимся к этому адресу (см. Рис. 4.6.8).

:00409C01 55 push ebp
:00409C02 8BEC mov ebp, esp
:00409C04 8B450C mov eax, dword ptr [ebp+0C]
:00409C07 2D10010000 sub eax, 00000110
:00409COC 7413 je 00409C21
:00409COE 48 dec eax
:00409COF OF84EB000000 je 00409D00
:00409C15 2D01020000 sub eax, 00000201
:00409C1A 7444 je 00409C60
:00409C1C E969010000 jmp 00409D8A

Рис. 4.6.7. Начало функции окна.

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

Поставим теперь себе цель, найти ту же самую функцию, но отладчиком SoftIce.

    Загружаем загрузчиком LOADER32.EXE программу FILES.EXE в отладчик SoftIce. Далее Ctrl-D, и мы имеем запущенную программу. Запускаем диалоговые окна, кроме последнего диалогового окна. Перед тем как вызвать последнее диалоговое окно, снова войдем в отладчик и установим точку останова на функцию DialogBoxParamA:
    ВРХ DialogBoxParamA
    
    Снова входим в программу, запускаем нужное диалоговое окно, при этом происходит автоматический выход в отладчик: сработала точка останова. Далее нажмем клавишу F11, при этом окажемся в точке вызова либо сразу, либо после закрытия диалогового окна (опять происходит выход в отладчик). Посмотрев на вызов функции DialogBoxParam, мы легко определим адрес процедуры окна. Разумеется, он совпадает с тем, который мы нашли ранее, с помощью отладчика программы W32Dasm (см. Рис. 4.6.7).

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

ВРХ EIP IF(EBP+8==N)
здесь N - найденный ранее дескриптор окна. После этого остается только проделать что-то с окном, чтобы на функцию его пришло какое-нибудь сообщение.
57 Простой с точки зрения возможных проблем, возникающих при исправлении исполняемых модулей.


Глава 7. Структура и написание драйверов .VXD

Последнюю главу нашей книги я посящаю написанию виртуальных драйверов. Сокращение VxD следует понимать как Virtual Device Driver, "x" относится к слову Device (имеется в виду любое устройство). Вопрос несколько устаревает, так как в Windows 98 принята на вооружение несколько иная концепция драйверов устройств, а Windows NT никогда не поддерживала VxD-драйверы, а использует модель, которая называется kernel mode (режим ядра). Все же программирование VxD остается актуальным по сей день, и знать основные положения должен каждый программирующий на языке ассемблера. В данной главе, отходя от нашей обычной практики, мы будем интенсивно использовать макроопределения, содержащиеся во включаемых (inc) файлах пакета DDK. Тем самым нам удастся поместить весь материал в одну главу. Кроме того, данная глава ориентирована на работу с MASM32.

I

Для программирования VxD-драйверов нам понадобятся файлы VMM.INC, SHELL.INC, VCOND.INC и др., которые можно найти в пакете DDK (Device Development Kit), свободно распространяемом фирмой Microsoft.

При загрузке Windows программа WIN.COM загружает драйвер VMM32.VXD, называемый Менеджером Виртуальной Машины (Virtual Machine Manager), который инициализирует другие VxD-драйверы. Затем VMM переключает процессор в защищенный режим и инициализирует системную виртуальную машину. Кроме уже сказанного, VMM обеспечивает сервис для других драйверов VXD. При запуске DOS-приложения для него выделяется виртуальная машина, так что приложению "кажется", что оно работает с настоящей машиной. При запуске обычных 32-битных приложений они работают в системной виртуальной машине. Приложения, работающие в разных виртуальных машинах, не подозревают о существовании других виртуальных машин. Одним из главных назначений виртуальных драйверов является обеспечение бесконфликтного коллективного доступа к физической аппаратуре для всех одновременно работающих виртуальных машин. Еще одна задача, которую приходится решать виртуальным драйверам, - это осуществление взаимодействия системной виртуальной машиной с другими виртуальными машинами.

Следует отметить, что в Windows существуют и так называемые обычные драйверы, имеющие расширение DRV и структуру DLL-библиотеки, которые экспортируют API-функции для работы с некоторыми внешними устройствами (например, видеоадаптером). Эти драйверы получают доступ к внешним устройствам не напрямую, а посредством VxD-драйверов. Поскольку драйверы VxD работают в нулевом кольце защиты, они имеют доступ к любому участку памяти и напрямую, посредством портов ввод-вывода, обращаются к внешним устройствам.

Все виртуальные драйверы делятся на два класса - статические и динамические. Статические драйверы загружаются во время загрузки системы и остаются в памяти до выхода из системы. Статические драйверы существовали еще в Windows 3.x. Динамические драйверы могут загружаться и выгружаться системой по мере необходимости. Они в основном используются для обслуживания устройств "Plug and Play" и загружаются менеджером конфигурации. Загрузить динамический виртуальный драйвер можно и из обычного приложения при помощи обычных функций работы с файлами.

Имеется три механизма взаимодействия виртуальных драйверов друг с другом.

    Управляющие сообщения. Эти сообщения посылает виртуальным драйверам менеджер виртуальной машины. Драйверы также могут обмениваться информацией посредством таких сообщений. Это очень напоминает сообщения Windows и взаимодействия приложений друг с другом и операционной системой через эти сообщения. Функции обратного вызова. Виртуальный драйвер может позволить вызов функции обратного вызова другому драйверу. Виртуальные драйверы и менеджер виртуальной машины могут экспортировать определенные функции для вызова из других виртуальных драйверов. Для вызова функции необходимо знать уникальный номер виртуального драйвера, который экспортирует данную функцию и номер этой функции.

Формат виртуальных драйверов называется LE-форматом, сокращенно от "linear executable". Данный формат поддерживает наличие как 16-битного, так и 32-битного кода. Это актуально для статических виртуальных драйверов, которые часть работы (инициализация) выполняют в реальном (незащищенном) режиме. В Windows NT драйверы грузятся в защищенном режиме, по этой причине данный формат в этой операционной системе не используется.

Код и данные в файле LE-формата располагаются в сегментах. Ниже мы опишем возможные классы сегментов.

LCODE - код или данные, заключенные в этом коде, не могут сбрасываться системой на диск (paging).

PCODE - код может временно помещаться на диск.

PDATE - аналогично предыдущему, но здесь хранятся данные.

ICODE - здесь располагается код инициализации, после инициализации сегмент удаляется из памяти.

DBOCODE - используется при запуске драйвера "под отладчиком".

SCODE - статические код и данные. Всегда остается в памяти, даже если драйвер выгружается.

RCODE — содержит 16-битный код для инициализации в реальном режиме.

16ICODE - 16-битный код инициализации в защищенном режиме.

MCODE - содержит строки сообщений.

Перечисленные классы сегментов не задаются непосредственно в тексте программы. Сегменты и классы объявляются в DEF-файле. Файл vmm.inc содержит огромное количество макросов, и нам не избежать пользоваться ими. Это позволит материал, который я хочу изложить, вместить в одну главу.

II

Начнем с содержимого DEF-файла. Содержимое показано на Рис. 4.7. Здесь перечислены сегменты на все случаи жизни. Вам нет необходимости использовать все определенные здесь сегменты. Т.о. образом данный файл можно использовать при создании любого виртуального драйвера. Сегменты, принадлежащие к одному классу, после компоновки объединяются в один сегмент. Менять следует только первую строку, где задается имя драйвера. Отметим, что имя драйвера следует задавать заглавными буквами. Кроме того, в первой строке можно задать тип драйвера. По умолчанию этот тип статический. Если бы мы записали строку VXD VXD1 DYNAMIC, то компоновщик создал бы динамический виртуальный драйвер.

VXD VXD1
SEGMENTS
_LPTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LTEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LDATA CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TEXT CLASS 'LCODE' PRELOAD NONDISCARDABLE
_DATA CLASS 'LCODE' PRELOAD NONDISCARDABLE
CONST CLASS 'LCODE' PRELOAD NONDISCARDABLE
_TLS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_BSS CLASS 'LCODE' PRELOAD NONDISCARDABLE
_LMGTABLE CLASS 'MCODE' PRELOAD NONDISCARDABLE IOPL
_LMSGDATA CLASS 'MCODE' PRELOAD NONDISCARDABLE IOPL
_IMSGTABLE CLASS 'MCODE' PRELOAD DISCARDABLE IOPL
_IMSGDATA CLASS 'MCODE' PRELOAD DISCARDABLE IOPL
_ITEXT CLASS 'ICODE' DISCARDABLE
_IDATA CLASS 'ICODE' DISCARDABLE
_PTEXT CLASS 'PCODE' NONDISCARDABLE
_PMSGTABLE CLASS 'MCODE' NONDISCARDABLE IOPL
_PMSGDATA CLASS 'MCODE' NONDISCARDABLE IOPL
_PDATA CLASS 'PDATA' NONDISCARDABLE SHARED
_STEXT CLASS 'SCODE' RESIDENT
_SDATA CLASS 'SCODE' RESIDENT
_DBOSTART CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBOCODE CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_DBODATA CLASS 'DBOCODE' PRELOAD NONDISCARDABLE CONFORMING
_16ICODE CLASS '16ICODE' PRELOAD DISCARDABLE
_RCODE CLASS 'RCODE'
EXPORTS
VXD1_DDB @1

Рис. 4.7.1. Файл VXD.DEF используемый для компоновки виртуального драйвера.

В конце файла указывается единственная экспортируемая переменная — блок описания устройства. DDB - Device Descriptor Block. В этот блок, состоящий из 22 полей, который определен в файле vmm.inc, содержит информацию о виртуальном драйвере (см. ниже).

В файле vmm.inc определены макроимена для всех записанных выше сегментов. Например, для сегмента _LTEXT задано имя VxD_LOCKED_CODE SEG, а для _RCODE имя VxD_REAL_INIT_SEG. Мы будем использовать эти имена в своих программах.

Обратимся теперь к трансляции виртуальных драйверов. Ниже записаны строки, в результате выполнения которых может быть получен виртуальный драйвер.

ml /coff /c /Сх /DMASM6 /DBLD_COFF /DIS_32 vxd1.asm
link /vxd /def:vxd.def vxd1.obj

Константы MASM6, BLD_COFF, IS_32 используются операторами условной трансляции, которые заданы в файлах vmm.inc и vcond.inc. Отметим, что если Вы используете DEF-файл в том виде, как мы его определили на Рис. 4.7.1, то при компоновке будут появляться сообщения об отсутствии той или иной секции, что смело можно проигнорировать.

Для того чтобы выполнить какие-либо действия, нам часто придется использовать макроопределения из файла vmm.inc. Познакомимся с некоторыми из них.

Во-первых, конечно, это Declare_Virtual_Device. Этот макрос заполняет структуру DDB, несколько облегчая нашу задачу. Вот его полный формат (саму структуру макроса, довольно сложную, можно изучить в файле vmm.inc):

Declare_Virtual_Device Name, MajorVer, MinorVer, CtrlProc,
DeviceID, InitOrder, V86Proc, PMProc, RefData

Разберем параметры.
Name - имя виртуального драйвера. Должно совпадать с именем, определенным в DEF-файле.
MajorVer, MinorVer - младшая и старшая части версии драйвера.
CtrlProc - имя управляющей процедуры драйвера. Эта процедура принимает и обрабатывает сообщения, приходящие на драйвер. Принято формировать имя процедуры из двух частей: имя драйвера и добавка Control. Например, если имя нашего драйвера будет VXD1, то имя процедуры следует взять VXD1_Control.
DeviceID - 16-битный уникальный идентификатор виртуального драйвера. Необходимо указывать в том случае, когда виртуальный драйвер будет предоставлять свой сервис другим драйверам. Кроме того, идентификатор может понадобиться, если ваш драйвер собирается работать в реальном режиме.
InitOrder - порядок загрузки драйвера. Это всего лишь номер. Драйверы, имеющие меньший номер, загружаются в первую очередь. Имеет смысл для статических драйверов.
V86Proc, PMProc - специфицируют адреса функций, которые будет экспортировать драйвер для DOS- и обычных приложений. Если экспортирование не предполагается, параметры следует опустить.
Ref_Data - ссылка на данные, используемые супервизором ввода-вывода. Обычно опускается.

Макросы Begin_control_dispatch и End_control_dispatch используются для определения управляющей процедуры. Выглядит это следующим образом.

Begin control_dispatch VXD1
Control_Dispatch message, function
End_control_dispatch VXD1

Макрос Control_Dispatch определяет, какие сообщения какими функциями будут обрабатываться. Например:

Begin_control_dispatch VXD1
Control_Dispatch INIT_COMPLETE, INIT
End_control_dispatch VXD1

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

.386P
include vmm.inc
include vcond.inc
DECLARE_VIRTUAL_DEVICE VXD1,1,0, VXD1_Control, \
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
Begin_control_dispatch VXD1
Control_Dispatch INIT_COMPLETE, INIT
End_control_dispatch VXD1
VxD_LOCKED_CODE_SEG
BeginProc INIT
EndProc INIT
VxD_LOCKED_CODE_ENDS
end

Рис. 4.7.2. "Скелет" виртуального драйвера.

Оттранслируем драйвер на Рис. 4.7.2 согласно ранее указанному алгоритму. Только при компоновке добавьте ключ /MAP, в результате, кроме драйвера VXD1.VXD, в каталоге появится файл VXD1.MAP. Содержимое этого файла мы поместили на Рис. 4.7.3.

VXD1

Timestamp is 3bb5ad7a (Sat Sep 29 17:16:10 2001) Preferred load address is 00400000 Start Length Name Class 0001:00000000 00000050H _LDATA CODE 0001:00000050 00000007H _LTEXT CODE Address Publics by Value Rva+Base Lib:Object 0001:00000000 VXD1_DDB 00401000 vxd1.obj 0001:00000050 VXD1_Control 00401050 f vxd1.obj 0001:00000057 INIT 00401057 f vxd1.obj entry point at 0000:00000000 Static symbols

Рис. 4.7.3. Содержимое файла VXD1.MAP.

Просматривая MAP-файл, обратите внимание, что в объектном файле определень два сегмента: _LDATA и _LTEXT, относящиеся к одному классу.

В заключение отмечу, что вместо стандартного name proc/name endp мы пользуемся макросами BeginProc и EndProc, определение которых также можно обнаружить в VMM.INC.

Виртуальные драйверы могут предоставлять свои функции для использование другими драйверами. Другими словами, виртуальные драйверы экспортируют свои функции. Вызов экспортируемой функции представляет шестибайтную величину:

int 20h
DD 00110002H

При этом 11Н - идентификатор виртуального драйвера, 02Н - номер сервиса (индекс в таблице сервисов). Мы не будем непосредственно писать вызов в таком виде, а будем пользоваться макросами VMMCall и VxDCall. Первый - для вызова сервисов VMM, а второй - для сервисов других виртуальных драйверов.

III

Приступим теперь к написанию простого, но работающего статического виртуального драйвера. Такой драйвер представлен на Рис. 4.7.3. Ниже мы обсудем этот драйвер, а также построение статических виртуальных драйверов.

.386P
include vmm.inc
include shell.inc
include vcond.inc
; заполнить структуру DDB
DECLARE_VIRTUAL_DEVICE VXD2,1,0, VXD2_Control, \
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
; объявить принимаемые сообщения и процедуры,
; которые их обрабатывают
Begin_control_dispatch VXD2
Control_Dispatch Create_VM, OnVMCreate
Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch VXD2
; сегмент для хранения сообщений
VxD_PAGEABLE_DATA_SEG
MsgTitle db "Сообщение драйвера VXD",0
VMCreated db "Создается виртуальная машина",0
VMDestroyed db "Уничтожается виртуальная машина",0
VMFocus db "Смена фокуса виртуальной машины"
VxD_PAGEABLE_DATA_ENDS
; сегмент, содержащий код
VxD_PAGEABLE_CODE_SEG
; процедура - реакция на создание виртуальной машины
BeginProc OnVMCreate
; здесь можно вставить код - реакцию на
; открытие виртуальной машины
MOV ECX,OFFSET VMCreated
CALL MES
RET
EndProc OnVMCreate
; процедура - реакция на закрытие виртуальной машины
BeginProc OnVMClose
; здесь можно вставить код - реакцию на
; закрытие виртуальной машины
MOV ECX,OFFSET VMDestroyed
CALL MES
RET
EndProc OnVMClose
; процедура, выводящая сообщение
MES PROC
; получить дескриптор системной виртуальной машины
VMMCall Get_sys_vm_handle
; дескриптор возвращается в EBX
; в EAX - флаг сообщения
MOV EAX,MB_OK ; адрес заголовка сообщения
MOV EDI, OFFSET MsgTitle
; адрес CallBack функции, в данном случае NULL
XOR ESI,ESI
; ссылка на данные для CallBack-функции
XOR EDX,EDX
; сервисная функция VXD - окно сообщения
VxDCall SHELL_Message
MES ENDP
VxD_PAGEABLE_CODE_ENDS
end

Рис. 4.7.3. Пример простого статического виртуального драйвера.

Как мы уже указывали, статический драйвер загружается при загрузке системы и остается в памяти до завершения работы операционной системы. Удобнее всего загрузить драйвер, указав строку device=имя_драйвера в секции [386enh] файла SYSTEM.INI. Можно также использовать системный реестр: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD\key\StaticVxD=pathname.

Первое, однако, более удобно, т.к. в случае ошибки в VXD-драйвере можно исключить его запуск, отредактировав файл SYSTEM.INI в MS DOS.

При установке виртуальных драйверов VMM посылает драйверам следующие три сообщения в порядке списка.

SysCriticalInit - посылается при переключении в защищенный режим, но до разрешения прерываний.

Device_Init - посылается после разрешения прерываний. Именно это сообщение чаще всего используется виртуальными драйверами для проведения начальной инициализации.

Init_Complete - последнее сообщение, посылаемое виртуальным драйверам при загрузке системы.

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

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

System_Exit2 - сообщение посылается перед выгрузкой системы. Микропроцессор находится еще в защищенном режиме.

Sys_Critical_Exit2 - следующее сообщение перед выгрузкой системы.

Device_Reboot_Notify2 - сообщают виртуальным драйверам, что система "собирается" выгружаться. Прерывания еще доступны.

Crit_Reboot_Notify2 - аналогичное предыдущему сообщению, но прерывания недоступны.

Обратимся теперь к программе на Рис. 4.7.3. Этот драйвер выводит сообщение при активизации виртуальной машины (например, создание консоли или просто запуск DOS-приложения) и при ее деактивизации. В драйвере мы использовали две сервисные функции: получить дескриптор системной виртуальной машины и вывести сообщение. Рассмотрим эти функции:

Get_sys_vm_handle - получить дескриптор системной виртуальной машины. Причем дескриптор возвращается в регистре EBX.

SHELL_Message — вывести сообщение. Параметры хранятся в регистрах: EBX — дескриптор виртуальной машины. EAX - флаг сообщения, например MB_OK. ECX - 32-битный адрес строки-сообщения. EDI - 32-битный адрес строки-заголовка. ESI - адрес функции - реакции на действие пользователя. Если функции нет, тогда 0. EDX - адрес данных, которые будут посылаться функции.

И еще, при выходе драйвер должен очищать флаг переноса. В нашем случае очистка флага переноса обусловлена правильным выполнением функции SHELL_Message.

IV

В последнем разделе главы мы рассмотрим динамические виртуальные драйверы. Существует три способа загрузки виртуальных динамических драйверов. Поместить драйвер в каталог \SYSTEM\IOSUBSYS. Драйверы из этого директория загружаются супервизором ввода- вывода. Использовать сервис VxDLDR. Эту сервисную функцию можно вызывать только из виртуальных драйверов. Использовать функцию CreateFile.

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

    Открываем драйвер с помощью функции CreateFile. При удачном открытии функция возвращает идентификатор, который затем используется при вызове функций экспортируемых данным драйвером. Используем функции динамического драйвера посредством вызова функции API DeviceIoControl. Закрываем драйвер с помощью CloseHandle, при этом он автоматически выгружается из памяти.

Перейдем теперь к программе загрузки динамического драйвера. Программа показана на Рис. 4.7.4. Она загружает драйвер msg.vxd и вызывает сервис 3 драйвера.

; файл FILES1.ASM
.386P
; плоская модель
.MODEL FLAT, stdcall
; константы
STD_INPUT_HANDLE equ -10
FILE_FLAG_DELETE_ON_CLOSE equ 4000000h
; прототипы внешних процедур
EXTERN GetStdHandle@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetCommandLineA@0:NEAR
EXTERN CreateFileA@28:NEAR
EXTERN CloseHandle@4:NEAR
EXTERN MessageBoxA@16:NEAR
EXTERN ReadConsoleA@20:NEAR
EXTERN DeviceIoControl@32:NEAR
;
;------------------------------------------------
;директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;------------------------------------------------
;
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
HANDL DWORD ?
HFILE DWORD ?
BUF DB "\\.\msg.vxd",0
CAP DB "Окно сообщения",0
MES DB "Ошибка загрузки драйвера",0
BUFER DB 20 DUP (0)
LENS DWORD ? ; количество выведенных символов
MES1 DB "Вызов сервиса OK!",0
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить HANDLE ввода
PUSH STD_INPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
; открыть файл
PUSH 0
PUSH FILE_FLAG_DELETE_ON_CLOSE
PUSH 0
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET BUF
CALL CreateFileA@28
CMP EAX,-1
JE _ERR
MOV HFILE,EAX
; вызов сервиса VXD
PUSH 0
PUSH 0
PUSH 0
PUSH 0
PUSH 18
PUSH OFFSET MES1
PUSH 3 ; номер сервиса
PUSH HFILE
CALL DeviceIoControl@32
; ждать клавиши ENTER
PUSH 0
PUSH OFFSET LENS
PUSH 200
PUSH OFFSET BUFER
PUSH HANDL
CALL ReadConsoleA@20
; закрыть и выгрузить драйвер
PUSH HFILE
CALL CloseHandle@4
_EXIT:
; конец работы программы
PUSH 0
CALL ExitProcess@4
_ERR:
PUSH 0 ; MB_OK
PUSH OFFSET CAP
PUSH OFFSET MES
PUSH 0 ; дескриптор окна
CALL MessageBoxA@16
JMP _EXIT
_TEXT ENDS
END START

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

Прокомментируем программу на Рис. 4.7.4. Главное здесь - разобрать работу функции DeviceIoControl. Вот параметры этой функции: 1-й параметр, дескриптор драйвера, полученный через функцию CreateFile. 2-й параметр, номер необходимой Вам операции. 3-й параметр, адрес данных для драйвера. 4-й параметр, длина данных. 5-й параметр, буфер, куда драйвер поместит свои данные. 6-й параметр, длина буфера. 7-й параметр, адрес переменной, куда будет занесено количество байтов, помещенных в буфер драйвером. 8-й параметр, адрес OVERLAPPED-структуры.

Как видите, при вызове мы передаем указатель на строку MES1.

Теперь, я думаю. Вы легко сможете разобраться в программе загрузки. Переходим к самому драйверу. Драйвер выполняет весьма простую функцию. При вызове его сервиса он выводит на экран сообщение. Причем текст сообщения передается вызывающей программой. При вызове функции DeviceIoControl с дескриптором драйвера, на драйвер приходит сообщение w32_deviceIoControl. При этом регистр EBX содержит дескриптор виртуальной машины, ESI указывает на структуру, содержимое которой мы сейчас разберем. При этом надо иметь в виду, что при загрузке драйвера на него тоже приходит это же сообщение, и мы его должны также обработать. Разберем теперь структуру, на которую указывает регистр ESI.

DIOCParams STRUC
Internal1 DD ?
VMHandle DD ?
Internal2 DD ?
dwIoControlCode DD ?
lpvInBuffer DD ?
cbInBuffer DD ?
lpvOutBuffer DD ?
cbOutBuffer DD ?
lpcbBytesReturned DD ?
lpoOverlapped DD ?
hDevice DD ?
tagProcess DD ?
DIOCParams ENDS

Опишем поле структуры:
Internal1 - указатель на структуру Client_Reg_Struc, определяющую регистры вызывающего приложения (см. Рис. 4.7.6. и пояснения к нему).
VMHandle - дескриптор виртуальной машины.
Internal2 - указатель на блок DDB.
dwIoControlCode - номер необходимой операции.
lpvInBuffer - указатель на буфер, содержащий информацию вызывающей программы.
cbInBuffer - количество байт, пересылаемых в буфере.
lpvOutBuffer - указатель на буфер, в который драйвер может поместить информацию для вызывающей программы.
cbOutBuffer - количество байт в буфере.
lpcbBytesReturned - количество возвращаемых байт.
lpoOverlapped - указатель на OverLapped-структуру.
hDevice - дескриптор драйвера, возвращаемый функцией CreateFile.
tagProcess - признак процесса.

.386P
include vmm.inc
include vcond.inc
include vwin32.inc
include shell.inc
DECLARE_VIRTUAL_DEVICE MSG,1,0, MSG_Control,\
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
Begin_control_dispatch MSG
; будем обрабатывать сообщение w32_Device!oControl
; процедурой PROC1
Control_Dispatch w32_DeviceIoControl, PROC1
End_control_dispatch MSG
; сегмент данных
VxD_PAGEABLE_DATA_SEG
CAP1 DB "Окно сообщения",0
MES1 DB 50 DUP (0)
VxD_PAGEABLE_DATA_ENDS
; сегмент кода
VxD_PAGEABLE_CODE_SEG
BeginProc PROC1
CMP DWORD PTR [ESI]+12,DIOC_Open
JNE L1
XOR EAX,EAX
JMP _EXIT
L1:
CMP DWORD PTR [ESI]+12,3
JNZ _EXIT
; длина строки
MOV EDI,DWORD PTR [ESI]+16
VMMCall _lstrlen, <EDI>
; копировать в буфер
INC EAX ; длина
VMMCall _lstrcpyn,<OFFSET MES1,EDI,EAX>
; вызвать функцию SHELL_Message
MOV ECX,OFFSET MES1 ; DWORD PTR [ESI]+14
MOV EDI,OFFSET CAP1
MOV EAX,MB_OK + MB_ICONEXCLAMATION
VMMCall Get_Sys_VM_Handle
; адрес CaliBack функции, в данном случае NULL
XOR ESI,ESI
; ссылка на данные для CallBack-функции
XOR EDX,EDX
VxDCall SHELL_Message
XOR EAX,EAX
_EXIT:
RET
EndProc PROC1
VxD_PAGEABLE_CODE_ENDS
end

Рис. 4.7.5. Пример динамического драйвера.

Прокомментируем программу на Рис. 4.7.5.

    Как я уже говорил, при загрузке драйвера на него приходит сообщение w32_DeviceIoControl, при этом на структуру указывает регистр ESI. При этом поле dwIoControlCode будет содержать число DIOC_Open, в действительности равное просто 0. Поле dwIoControlCode находится по смещению ESI+12. Убедившись, что там содержится 0, мы возвращаем управление, обнулив предварительно EAX (это необходимо). При вызове драйвера из программы мы используем номер 3. Убедившись, что в поле dwIoControlCode содержится 3, мы таким образом должны сделать то, что ждет от нас программа. Собственно задача драйвера - вывести сообщение со строкой, полученной от программы. Известен адрес строки и ее длина. Чтобы продемонстрировать некоторые функции VXD-сервиса, мы еще раз определяем длину строки и копируем ее в буфер, подготовленный в теле драйвера. Наконец вывод сообщения и возвращение управления с обнулением содержимого EAX.

Сделаем теперь некоторые пояснения к структуре Client_Reg_Struc.

Client_Reg_Struc STRUC
Client_EDI DD ?
Client_ESI DD ?
Client_EBP DD ?
Client_res0 DD ?
Client_EBX DD ?
Client_EDX DD ?
Client_ECX DD ?
Client_EAX DD ?
Client_Error DD ?
Client_EIP DD ?
Client_CS DW ?
Client_res1 DW ?
Client_EFlags DD ?
Client_ESP DD ?
Client_SS DW ?
Client_res2 DW ?
Client_ES DW ?
Client_res3 DW ?
Client_DS DW ?
Client_res4 DW ?
Client_FS DW ?
Client_res5 DW ?
Client_GS DW ?
Client_res6 DW ?
Client_Alt_EIP DD ?
Client_Alt_CS DW ?
Client_res7 DW ?
Client_Alt_EFlags DD ?
Client_Alt_ESP DD ?
Client_Alt_SS DW ?
Client_res8 DW ?
Client_Alt_ES DW ?
Client_res9 DW ?
Client_Alt_DS DW ?
Client_res10 DW ?
Client_Alt_FS DW ?
Client_res11 DW ?
Client_Alt_GS DW ?
Client_res12 DW ?
Client_Reg_Struc ENDS

Рис. 4.7.6. Структура, содержащая значения регистров вызывающего приложения.

Пояснение к Рис. 4.7.6. Структура содержит три типа полей:
Client_resX - зарезервированные поля.
Client_XXX - регистры программы, запускаемой в виртуальной машине.
Client_Alt_XXX - регистры 32-битной программы, запускаемой в системной виртуальной машине.

На этом мы заканчиваем рассмотрение виртуальных драйверов58.

58 При написании главы были использованы материалы с сайта http://win32asm.cjb.net