Данное окно отображает текущее состояние сопроцессора.
Окно ссылок на строки
Рисунок 4.4.4. Окно ссылок на строки.
Как видно из рисунка, можно скопировать в буфер выбранную строку или все строки.
Окно стека
Окно стека.
Показывает текущее состояние стека. Причем первая вызванная функция будет находиться на дне стека.
Окно точек останова
Окно точек останова.
Здесь содержится информация обо всех установленных в программе точках останова. Здесь же можно добавить или установить точки останова.
Окончательный вариант программы
В старом Си для того, чтобы для хранения переменных компилятор использовал регистры, переменные следует объявлять как AUTO.
Операции с текстом
Операции с текстом.
Строки дизассемблированного текста могут быть выделены и скопированы в буфер либо напечатаны. Выделение строки осуществляется щелком левой кнопки мыши, когда курсор мыши расположен в крайнем левом положении. Для выделения группы строк дополнительно используется клавиша Shift. Выделенный фрагмент копируется специальной кнопкой, которая "загорается", когда фрагмент существует, либо отправляется на печатающее устройство.
Оператор switch или оператор выбора
2. Оператор switch или оператор выбора.
Оператор switch весьма часто употребляется в функциях окон. Хорошее знание его ассемблерной структуры поможет Вам легче отыскивать эти функции в море ассемблерного кода.
switch(i) { case 1: ... ... ... break; case 3: ... ... ... break; case 5: ... ... ... break; }
А вот соответствующий данной структуре ассемблерный код.
Структура, как видите, интересная. Такой подход позволяет наилучшим образом оптимизировать проверку большого количества условий. В действительности оператор выбора может кодироваться и другим способом. Вот еще один возможный вариант представления оператора выбора:
CMP EAX,10 JE L1 CMP EAX,5 JE L2 CMP EAX,11 JE L3 ...
Описание заголовка РЕ
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
Слово-сигнатура, определяющее состояние отображенного файла. Определены следующие значения:
Старший номер версии использовавшегося при создании модуля компоновщика. Десятичный вид.
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 - можно писать.
Страницы образов секций. Здесь мы изучим некоторые секции.
Оптимизация кода
6. Оптимизация кода.
Выше мы пытались восстановить исходный текст программы по ассемблерному коду. Насколько это удалось, можно выяснить, сравнив получившийся текст с исходным. Следует заметить, что мы ошиблись: в исходном тексте стоит цикл while, а в нашем do. Однако, если откомпилировать получившуюся программу, она будет работать, так же как и исходная. Причина отличия двух текстов программ (весьма простых, кстати) заключается в том, что транслятор, кроме всего прочего, производит еще оптимизацию кода. Результат оптимизации - принципиальная невозможность восстановить исходный текст по исполняемому коду. Можно, однако, получить программный текст, правильно описывающий алгоритм исполнения. Можно просто понять то, что делает данный код, чем мы в данной главе и занимаемся.
Рассмотрим небольшую и весьма тривиальную программу на Си.
Оказывается, здесь тоже имеются резервы. Можно построить проверку условия так, чтобы количество переходов было наименьшим. Т.о. можно добиться того, чтобы данный фрагмент программы работал несколько быстрее. Предположим, у Вас имеется следующий фрагмент.
Код становится и быстрее, и короче, вот только разобраться в нем становится сложнее. На этом мы оставляем вопрос оптимизации. Всех интересующихся могу отослать к книге [14].
Основной целью отладчика Turbo Debugger является отладка программы
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 не позволяет исправлять исполняемые модули. Но случаются и ситуации, когда очевидно, что данный участок программы (или процедура) выдает ошибочное значение, а на то, чтобы найти ошибку, у Вас не хватает времени - участок достаточно сложен. Иногда неправильное значение выходной информации возникает, лишь при редком сочетании входных параметров. В этом случае может быть применен простой прием: между указанным участком и остальной частью программы вставляется несколько строк, проверяющих выходную информацию и исправляющих ее, если нужно. Такой прием часто оказывается незаменим при доработке чужой программы очень большого объема, когда понять логику чужого алгоритма (да еще содержащего ошибку) бывает просто невозможно.
Отладчик фирмы Borland
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-приложений.
Рассмотрим команды отладчика и возможности отладчика.
Отладчик SoftIce
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.
Отладочная информация ( debug$S debug$T)
Отладочная информация (.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
Смещение к отладочным данным в файле.
Переменные и константы
Переменные и константы.
Современные компиляторы довольно эффективно оптимизируют исходный код, поэтому не всегда просто разобраться, где какая переменная работает. В первую очередь это касается того, что для хранения части переменных, насколько это возможно, компилятор использует регистры. Если ему не хватит регистров, он начнет использовать память.
Для примера я взял простую консольную программу, написанную на Borland C++. В текстовом варианте программа занимает полтора десятка строк, тогда как ЕХЕ-файл имеет размер более 50 Кб. Впрочем, размер исполняемых файлов давно уже никого не удивляет. Интересно другое: корректно справился с задачей, т.е. корректно выявил точку входа - метку _main, только один дизассемблер - IDA PRO. Т.е., конечно, реально работающий участок программы дизассемблировали все, но выявить, как происходит переход на участок, смог только упомянутый мной дизассемблер. Приятно также и то, что аккуратно была распознана функция _printf. На Рисунок 4.5.1 показан фрагмент дизассемблированной программы, соответствующей основной процедуре main. С другой стороны, в данном случае нет никаких видимых возможностей быстрого поиска данного фрагмента в отладчике. Осюда наглядно можно понять полезность совместного использования отладчика и дизассемблера.
Рисунок 4.4.6. Первое информационное окно отладчика.
Отмечу, что если Вы подсоединяетесь к процессу, расположенному в памяти, то при выходе из отладчика процесс также будет выгружен из памяти, что может привести к неправильной работе операционной системы.
Поговорим теперь об отладке программ
II
Поговорим теперь об отладке программ, написанных на языке высокого уровня. Если при трансляции в исполняемом модуле была сохранена отладочная информация, то Turbo Debugger будет работать и с языком высокого уровня. Рассмотрим, например, простую консольную программу, демонстрирующую пузырьковую сортировку. Программа располагается на Рисунок 4.3.6.
Поиск нужного места в программе
Поиск нужного места в программе.
Часто требуется найти в дизассемблированном коде место, соответствующее месту исполняемой программы. Наиболее эффективно это можно сделать следующим образом. Загружаем в отладчик данный модуль. Запускаем его, доходим до нужного места и нажимаем кнопку "Terminate". В результате подсвеченная строка в дизассемблированном коде окажется как раз в нужном месте. Нужно только иметь в виду, что некоторые программы делают изменения, которые потом продолжают действовать. К таковым относятся, в частности, горячие клавиши.
Для примера я взял программу FILES.EXE. Эта поисковая программа очень удобна для поиска информации в локальной сети. Выберем, например, одно из диалоговых окон данной программы и поставим цель найти функцию этого диалогового окна. Сделаем это двумя способами: с помощью отладочных средств программы W32Dasm и с помощью отладчика SoftIce. На Рисунок 4.6.6. показаны несколько открытых диалоговых окон программы. Последнее диалоговое окно с заголовком "Название сетевого компьютера" и будет объектом нашего внимания.
Пример динамического драйвера
Рисунок 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.
Пример дизассемблирования программы с помощью самого мощного дизассемблера IDA PRO (под Windows)
Рисунок 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.
Пример командного файла IDA PRO
Рисунок 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. Еще одна приятная особенность дизассемблера - он создает ассемблерный файл, с которым затем можно работать уже в текстовом режиме.
Пример окна Turbo Debugger отображающего иерархию классов
Рисунок 4.3.3. Пример окна Turbo Debugger, отображающего иерархию классов.
Пример простой консольной программы
Рисунок 4.3.6. Пример простой консольной программы.
На Рисунок 4.3.5 показано окно отладчика Turbo Debugger с данной программой. В нижней части рисунка расположено окно Watches. В этом окне отслеживается значение элементов массива а[]. Обратившись к окну CPU, можно увидеть, как алгоритм пузырьковой сортировки, реализованный на языке Си, преобразуется в ассемблерный код.
Пример задания двух локальных массивов Взят из отладчика IDA PRO
Рисунок 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.
Программа W32Dasm (Windows Disassembler) представляет собой симбиоз довольно мощного дизассемблера и отладчика. Версия 8.93 программы, наиболее распространенная в настоящее время, может работать не только с РЕ-модулями, но и DOS-, NE-, -LE- модулями. Я намерен довольно полно описать работу с этой программой.
Программа загружающая использующая и выгружающая динамический драйвер
Рисунок 4.7.4. Программа, загружающая, использующая и выгружающая динамический драйвер.
Прокомментируем программу на Рисунок 4.7.4. Главное здесь - разобрать работу функции DeviceIoControl. Вот параметры этой функции:
1-й параметр, дескриптор драйвера, полученный через функцию CreateFile. 2-й параметр, номер необходимой Вам операции. 3-й параметр, адрес данных для драйвера. 4-й параметр, длина данных. 5-й параметр, буфер, куда драйвер поместит свои данные. 6-й параметр, длина буфера. 7-й параметр, адрес переменной, куда будет занесено количество байтов, помещенных в буфер драйвером. 8-й параметр, адрес OVERLAPPED-структуры.
Как видите, при вызове мы передаем указатель на строку MES1.
Теперь, я думаю. Вы легко сможете разобраться в программе загрузки. Переходим к самому драйверу. Драйвер выполняет весьма простую функцию. При вызове его сервиса он выводит на экран сообщение. Причем текст сообщения передается вызывающей программой. При вызове функции DeviceIoControl с дескриптором драйвера, на драйвер приходит сообщение w32_deviceIoControl. При этом регистр EBX содержит дескриптор виртуальной машины, ESI указывает на структуру, содержимое которой мы сейчас разберем. При этом надо иметь в виду, что при загрузке драйвера на него тоже приходит это же сообщение, и мы его должны также обработать. Разберем теперь структуру, на которую указывает регистр ESI.
; сегмент данных 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
Работа с динамическими библиотеками
Работа с динамическими библиотеками.
Для отладки динамической библиотеки можно поступить следующим образом. Загрузить в отладчик программу, которая обращается к динамической библиотеке. Затем обратиться к списку используемых динамических библиотек. Возможно, для работы с данной динамической библиотекой Вам понадобится запустить программу и выполнить какую-либо ее функцию. Дважды щелкнув по нужной библиотеке, Вы получите дизассемблированный код данной библиотеки в окне дизассемблера и возможность работать с кодом библиотеки.
Ресурсы
Ресурсы.
В начале дизассемблированного текста также описаны и ресурсы, точнее два основных ресурса-меню и диалоговое окно. Со списком этих ресурсов можно работать и в специальных окнах, получаемых с помощью пунктов меню программы "Refs\Menu References" и "Refs\Dialog references". Строковые ресурсы можно увидеть в уже упомянутом окне просмотра перечня строковых ссылок (см. Рисунок 4.4.4). Остальные ресурсы данной версии программы, к сожалению, не выделяются.
Секция экспорта ( edata) Общая структура
Секция экспорта (.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 строку с именем экспортируемой функции. Никто не говорит, что они должны в файле идти друг за другом последовательно, хотя так построено большинство файлов. Надо отметить, что имена экспорта чувствительны к регистру. Отметим особенность загрузчика - при связывании, если адрес функции находится в секции экспорта, на самом деле по указанному адресу лежит строка, переадресующая к другой библиотеке, экспортирующей данную функцию (с указанием библиотеки и самой функции). Это называется - передача экспорта.
Секция ресурсов ( rdata)
Секция ресурсов (.rdata)
Ресурсы представляют собой многоуровневое двоичное дерево. Их структура позволяет содержать до 2^31 уровней, однако реально используется только 3: самый верхний есть Type, затем Nam и затем Language (тип, имя, язык). Перемещения по иерархии каталогов ресурсов похожи на перемещения по каталогам жесткого диска. Типичное представление ресурсного участка в файлах:
Каталог ресурсов
Resources Directory Table
Данные ресурсов
Resources Data
Скорость или объем
Скорость или объем.
Это весьма важный вопрос, когда дело касается микропроцессора.
Например, команда MOV EAX,EBX выполняется быстрее команды XCHG EAX,EBX, но длиннее ее. Зная такую особенность, можно либо сокращать объем, либо увеличивать скорость программы. Особенно часто используется замена такой операции, как MUL, другими командами, в частности SHL. Это может значительно увеличить скорость выполнения программы и увеличить ее объем. Интересно, что арифметические действия можно производить и с помощью команды LEA (см. Приложение 2), и современные трансляторы уже взяли это на вооружение. Так что команда MUL не так часто может встретиться в оттранслированном коде, как об этом можно подумать, исходя из текста программы. Вообще, свойство команд надо исследовать, и иногда узнаешь довольно интересные вещи. Например, нахождение остатка от деления числа без знака на 4 производится следующим образом: AND EAX,0003h - не правда ли, оригинально?
Содержимое файла VXD MAP
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.
Сообщение об истечении времени работы программы
Рисунок 4.6.3. Сообщение об истечении времени работы программы.
Таким образом, следует решить две проблемы:
Устранить весьма раздражающую задержку. Сделать так, чтобы программа работала при любом количестве запусков.
Рисунок 4.6.2 являет собой явный "прокол" авторов программы. Дело в том, что окно и все его содержимое можно спрятать в ресурсы. Но когда на том же окне появляется новая запись - это уже программный код. Итак, запускаем W32Dasm и считываем туда программу All Screen. Запускаем окно SDR (String Data Reference), ищем строку Shareware Delay, дважды щелкаем по ней и, закрыв его, оказываемся в нужном месте программы. Вот этот фрагмент (Рисунок 4.6.4.).
Рисунок 4.1.1. Структура ЕХЕ-программы для MS DOS.
Более подробно разбор заголовка DOS-программы можно найти в [1]. Добавлю только, что сразу за таблицей перемещения начинается исполняемая часть модуля. Таблица же перемещения используется для того, чтобы при загрузке настроить адреса. Но это лишь в том случае, если в программе используются адреса сегментов. В противном случае таблица перемещения не содержит элементов, и исполняемый код начинается сразу за форматированной частью заголовка. Перейдем теперь к общей структуре РЕ-модуля.
Структура элемента массива настроек
Структура элемента массива настроек
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
?
Структура содержащая значения
.
58 При написании главы были использованы материалы с сайта http://win32asm.cjb.net
Таблица директория импорта
Таблица директория импорта
Каталог импорта
Import Directory Table
Таблица ссылок на имена
LookUp Table
Таблица имен
Hint-Name Table
Таблица адресов импорта
Import Address Table
Таблица настроек адресов
Таблица настроек адресов
Если исполняемый файл не может быть отображен по адресу, который указал компоновщик, то загрузчик производит настройку модуля, используя данные из секции .reloc. Поправки сводятся к перечню тех мест в отображенном файле, где нужно прибавить некоторую величину.
Формирование данных базовых поправок выглядит следующим образом. Поправки упаковываются сериями смежных кусков различной длины. Каждый кусок описывает поправки для одной четырехбайтовой страницы отображения и начинается со следующей структуры:
Смещение
Длина поля
Название поля
Описание поля
00h
DWORD
Page RVA
Относительный адрес страницы применения.
04h
DWORD
Block Size
Размер блока настроек (с заголовком). Эта величина используется для вычисления количества настроек.
08h
WORD
TypeOffset Record
Массив записей настроек, их переменное количество.
Для наложения настройки необходимо вычислить 32-битную разницу ("Дельта") между желаемой базой загрузки и действительной. Если образ программы загружен в требуемое место, то эта разница равна нулю и никакой настройки не требуется. Каждый блок настроек должен начинаться на DWORD-границе, для выравнивания блока можно пользоваться нулями. При настройке необходимую позицию в блоке вычисляют как сумму относительного адреса страницы и базового адреса загруженной программы.
Таблица разделов потоков
Таблица разделов потоков
Смещение
Длина поля
Название поля
Описание поля
00h
DWORD
Start Data Block VA
Виртуальный адрес начала блока данных цепочки.
04h
DWORD
End Data Block VA
Виртуальный адрес конца блока данных цепочки.
08h
DWORD
Index VA
Виртуальный адрес индексной переменной, используемой для доступа к локальному блоку данных цепочки.
0Ch
DWORD
CallBack Table VA
Виртуальный адрес таблицы обратных вызовов. Локальные обратные вызовы - массив виртуальных адресов функций, которые будут вызваны загрузчиком после создания цепочки (нити) и после ее завершения. Последний вход имеет нулевое значение и указывает на конец таблицы.
Текст программы можно увидеть на экране
Текст программы можно увидеть на экране,
обратившись к пункту меню Module. При этом программа должна быть оттранслирована с опциями, обеспечивающими сохранение в исполняемом модуле отладочной информации. К сожалению, речь идет только о продуктах фирмы Borland: C++, Delphi, Assembler. Отладочная информация других фирм Turbo Debugger не распознает. В дальнейшем мы рассмотрим пример отладки программы, написанной на Borland C++.
Точки останова
Точки останова.
В дизассемблированном тексте можно установить точки останова. Для этого следует перейти к нужной строке и воспользоваться клавишей F2 или использовать левую кнопку мыши при нажатой клавише Ctrl. Установка точки останова в окне дизассемблера тут же отражается в информационном окне и в управляющем окне - у отмеченной команды появляется префикс BP*. Удалить точку останова можно тем же способом, что и при установке. Точку останова можно сделать также неактивной. Для этого нужно обратиться к информационному окну и списку точек останова. Выбрав нужный адрес, щелкните по нему правой кнопкой. При этом "звездочка" у данной точки останова исчезнет, а строка в окне дизассемблера из желтой станет зеленой.
Условные конструкции
1. Условные конструкции.
Неполная условная конструкция.
if (простое условие) { ... ... ... }
если условие простое, то оно, разумеется, заменяется следующей возможной последовательностью
Что, конечно, равносильно одному составному условию, связанному союзом "И". Союз "ИЛИ", как известно, заменяется проверкой условий в блоке "ELSE".
Вход в таблицу ресурсов (Resource Entry Item)
Вход в таблицу ресурсов (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 используется и устанавливается в ноль.
Вид загрузчика SoftIce (LOADER EXE) Окно настройки запуска модуля в отладчике Softice
Рисунок 4.4.11. Окно настройки запуска модуля в отладчике Softice.
Внешний вид программы HIEW EXE
Программа на Рисунок 4.2.3 проста и корректна. Представьте теперь, что при отладке Вы случайно изменили одну команду: вместо JE поставили JNE. В результате поспе трансляции программа перестала работать. Можно исправить ее, не прибегая к ассемблерному тексту? Конечно. Для этого в начале ее следует дизассемблировать, найти ошибку, а потом воспользоваться программой HIEW.EXE. Вообще говоря, можно ограничиться только программой HIEW, так как она вполне корректно дизассемблирует. Однако мы нарочно проведем исправление в два этапа.
Дизассемблируем модуль при помощи программы DUMPBIN.EXE. Вот дизассемблированный текст программы (Рисунок 4.2.4).
Для начала работы с исполняемым модулем достаточно выбрать нужный файл в меню "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
Вот как выглядит ассемблерный код
Рисунок 4.5.5.
Вот как выглядит ассемблерный код, полученный с помощью транслятора Borland C++ 5.0.
Быстрый переход к точке останова можно произвести, выбрав ее из списка (информационное окно) и сделав двойной щелчок мышью. Наконец можно установить точки останова на определенные события, такие как загрузка и выгрузка динамической библиотеки, создание и удаление потока и т.д. Все это делается при помощи установки соответствующего флага в информационном окне.
VxDдрайвер
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 содержит огромное количество макросов, и нам не избежать пользоваться ими. Это позволит материал, который я хочу изложить, вместить в одну главу.
Выполнение команды с переходом через процедуру
Выполнение команды с переходом через процедуру.
Данная команда выполняется по нажатию клавиши F8. Отличается от предыдущей команды тем, что не происходит вход в процедуру, но все инструкции в процедуре выполняются автоматически.
Выполнение с задержкой
Выполнение с задержкой.
Можно заставить отладчик выполнять программу с задержкой после каждой команды (Animation) - команда Animate меню Run.
При выполнении любой из перечисленных команд также могут быть заданы опции командной строки - Run/Arguments...
Рассмотрим теперь окна, которые используются при отладке в данном отладчике. С некоторыми из них мы уже познакомились, но все равно повторимся. Показать на экране то или иное окно можно посредством меню View. Мы опишем часть наиболее важных окон.
Выполнить единичную команду
Выполнить единичную команду.
Команда выполняется по нажатию клавиши F7. Если мы находимся в окне с текстом программы, то выполняется оператор соответствующего языка. Если мы находимся в окне CPU, то выполняется инструкция микропроцессора.
Выполнить процедуру
Выполнить процедуру.
Если курсор находится на команде вызова процедуры, то можно выполнить лишь одну эту процедуру командой Alt+F8.
Выполнить программу до строки где находится курсор
Выполнить программу до строки, где находится курсор.
Команда выполняется при помощи клавиши F4 или посредством меню. Смысл данной команды заключается в том, что, заинтересовавшись каким-либо местом программы, Вы можете выполнить программу до данной строки и посмотреть результат выполнения. Фактически это та же точка останова, но работает более оперативно. Кроме того, можно заставить выполняться программу до данного адреса (Alt+F9).
Вызов импортируемой функции
Рисунок 4.1.4. Вызов импортируемой функции.
Схема вызова импортируемых функций из РЕ-модуля изображена на Рисунок 4.1.4, который с некоторыми изменениями взят из книги [2]. Смысл данного рисунка заключается в следующем. При компоновке все вызовы API-функций преобразуются к вызову типа CALL Адрес1. При этом адрес, также как и вызов, находится в секции кода (.text). По адресу же стоит команда JMP DWORD PTR [Адрес2]. [Адрес2] находится в секции .idata (импорта) и содержит двойное слово — адрес функции в динамической библиотеке. Современные компиляторы содержат директивы, позволяющие вместо двух вызовов (CALL и JMP) генерировать один CALL [Адрес2].
Загрузка программ для отладки
Загрузка программ для отладки.
Загрузить модуль для отладки можно двумя способами. С помощью пункта "Debug\Load Process" загружается для отладки уже дизассемблированный модуль. Пункт же "Debug\Attach to an Active Process" позволяет "подсоединяться" и отлаживать процесс, находящийся в памяти. После загрузки отладчика на экране появляются два окна. Первое окно информационное (Рисунок 4.4.6), в документации оно называется "нижним левым окном отладчика". Второе окно управляющее (Рисунок 4.4.7), называемое в документации "нижним правым окном отладчика".
+++++++++++++++++++ IMPORTED FUNCTIONS ++++++++++++++++++ Number of Imported Modules = 7 (decimal)
Для загрузки приложения в отладчик в пакете SoftIce имеется специальная программа LOADER32.EXE. Она используется для загрузки 32-битных приложений. Для загрузки 16-битных приложений используются утилиты, расположенные в подкаталоге Util16.
Итак, вернемся к программе LOADER32.EXE. Вид загрузчика показан на Рисунок 4.4.10. Последовательность действий для загрузки программы в отладчик следующая: открыть модуль, транслировать модуль - преобразование отладочной информации в NMS-файл, загрузить модуль. Если загрузчик разобрался с отладочной информацией, то в окне отладчика появится программный текст, в противном случае Вам придется работать с обычным дизассемблированным текстом. После загрузки модуля, вы можете настроить запуск модуля в отладчике, воспользовавшись пунктом меню Module\Setting (Рисунок 4.4.11). Настройка достаточно очевидна, поэтому мы не будем разъяснять смысл пунктов данного окна.
Запуск отлаживаемого приложения
Запуск отлаживаемого приложения.
Выполняется по клавише F9 или по команде RUN из меню RUN. Программа выполняется в полном объеме, если только не указана точка останова (breakpoint) на строку в окне с текстом программы или окне CPU. Точка останова устанавливается при помощи клавиши F2. Открыв окно Watch (View/ Watches), Вы можете установить там слежение за переменными, которые Вам интересны. В этом случае точки останова окажутся очень полезными.