здесь, сейчас, этот момент - Несколько слов о дизассемблерах [entries|archive|friends|userinfo]
pr0mix

[ userinfo | ljr userinfo ]
[ archive | journal archive ]

Несколько слов о дизассемблерах [Feb. 9th, 2013|12:01 am]
Previous Entry Add to Memories Tell A Friend Next Entry
[Tags|, , ]

Re:)

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

Это своего рода справочная портянка для создания (простого) дизасма.

Предполагается, что тот, кто собирается прочитать сей текст, знает формат команд IA-32 (так как здесь он описан коротко). Если это не так, то сначала желательно просмотри ссылки в конце данного текста.

Итак, поехали.


Для начала определение:

Машинные Команды - это сформированные по определенным правилам последовательности байтов. Так вот, правила эти называют форматом команд, и формат этот в свою очередь состоит из нескольких полей, ОБЯЗАТЕЛЬНЫМ из которых является код операции (опкод). Остальные поля могут отсутствовать (тогда команда содержит только 1 байт - опкод). Формат команд IA-32:

ПРЕФИКС ОПКОД МОДРМ СИБ СМЕЩЕНИЕ НЕПОСРЕДСТВЕННЫЙ ОПЕРАНД

А вот, собственно, вариант дизассемблирования:

  1. Определение префиксов команды

  2. Опкод (разбор)

  3. Парсинг ModR/M

  4. Парсинг SIB

  5. Смещение и непосредственный операнд


Теперь по порядку.

1. ОПРЕДЕЛЕНИЕ ПРЕФИКСОВ КОМАНДЫ.

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

Префиксы делятся на 4 группы:

  • блокировки шины (LOCK - 0F0h) & повторения(REP или REPE/REPZ - 0F3h, REPNE/REPNZ - 0F2h)

  • замены сегмента (CS - 2Eh, SS - 36h, DS - 3Eh, ES - 26h, FS - 64h, GS - 65h)

  • размера оперенда (66h)

  • размера адреса (67h)


Максимальная длина команды равна 15 байтам. Все 15 байт префиксами быть не могут (почему? - вспоминаем про опкод:) - по ходу возникнет #UD (недействительный код операции). LOCK можно использовать только один раз и только с определенными командами. REP/E/NE - могут использоваться с командами обработки срок, могут быть частью команд из расширенного набора или вообще "быть не при делах". Разрешено многократное использование данных префиксов, однако это будет равносильно однократному + использоваться будет последний встреченный. 2 группа префиксов - по идее также используется последний встреченный. Здесь же: 2Eh и 3Eh (появились в Pentium 4 & Xeon) - еще и определяют (только с инструкциями Jcc):

  • 2Eh - переход выполняется;

  • 3Eh - переход не выполяется.


Префиксы 66h/67h изменяют разрядность операнда/адреса: 16 или 32 бита, а также могут быть частью команд из расширенного набора. При создании дизасма - быть внимательным к этим префиксам(блин, и к другим тоже). В общем, здесь все. Остальное - внимательное изучение доков от Intel.

2. ОПКОД (РАЗБОР).

Код оперции (опкод) - обязательный элемент, описывающий операцию, выполняемую командой. Может занимать 1,2,3 байта, а также и часть битов для некоторых машинных команд в байте ModR/M (о нем чуть ниже).

Опкод не имеет однозначной структуры. В зависимости от определенных команд может иметь от 1 до 3 элементов, уточняющих детали операции:

  • d - направление передачи данных

  • s - расширение непосредственного операнда

  • w - размер данных

  • reg - регистр



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

Теперь, собственно, о декодировании команд. Так вот, лучше всего декодировать по характеристикам опкода. Как это делается? Тут есть несколько вариков: можно искать опкод

  1. кучей if{} else{}, и если нашли - установить соответствующие флаги (характеристики) - это имеющиеся в данном опкоде поля, набор параметров и другие.

  2. switch (аналог if/else) - тогда их будет 2: 1-ый - для однобайтных опкодов, а 2-ой - для 0x0F, и также, при нахождении данного опкода выставлять соответствующие флаги. Но, по-моему, лучше вариант #3, а именно:

  3. соcтавить таблицы, содержащие Характеристики уже всех опкодов. Некоторые спрашивают, как же создаются эти самые таблицы и как узнать, какие из характеристик соответствуют какому опкоду? Так вот, таблицы (массивы) строятся так, чтобы опкод команды был ИНДЕКСОМ (индексом в массиве) элемента, описывающего данный опкод. Дизасм выбирает характеристики опкода из таблицы, после чего декодирует опкод уже по набору этих характеристик. Насчет характеристик: все находится в мануалах Intel (IA-32 Intel Architecture Software Developer's Manual Volume 2B Instruction Set Reference N-Z - где-то в конце книги Appendix A Opcode Map). Короче, приведу несколько примеров:

    Opcode       Instruction             Description
    88 /r          MOV r/m8,r8          Move r8 to r/m8
    C6 /0         MOV r/m8,imm8     Move imm8 to r/m8

    88 /r          MOV r/m8,r8          Move r8 to r/m8
    C6 /0         MOV r/m8,imm8     Move imm8 to r/m8


Вспоминаем, что означают условные знаки "/r", "rb","/0", "r8", "imm32", etc.

Обозначим, например, наличие ModR/M в команде как B_MODRM equ 01h, а наличие непосредственного однобайтного значения(imm8) как B_DATA8.

Так, для опкода 0x88 выставлено "/r" - это означает, что ModR/M задаёт оба операнда (регистр и r/m) - значит выставляем флаг B_MODRM. Улавливаешь мыслю? Еще пример: опкод 0xC6. Напомню: "/0" говорит о том, что поле Reg в ModR/M всегда равно 0. Видно, что здесь также присутствует ModR/M. Но, еще здесь есть "imm8" - значит опкод 0xC6 будет содержать следующие характеристики: B_MODRM+B_DATA8. И так далее.

3. РАЗБОР ModR/M.

Байт ModR/M несет информацию об операндах и режиме адресации и состоит из следующих полей:

  • mod

  • Reg/Opcode (обычно называют Reg)

  • R/M


Поле mod - определяет режим адресации и количество байтов поля СМЕЩЕНИЯ в команде. Поля Reg и R/M обычно служат для указания операндов. Следует уточнить, что в Reg для некоторых команд может кодироваться расширение опкода.

mod address

00b [reg] - если reg==5, то сразу за modrm идет 32-битное смещение - если reg==4, то сразу за modrm идет байт SIB

01b [reg+8-битное смещение] - если reg==4, то сразу за modrm следует байт SIB

10b [reg+32-битное смещение] - если reg==4, то сразу за modrm следует байт SIB

11b reg

С префиксом 0x67 32-битное смещение превратится в 16-битное. Также при наличии 0x67, если mod=0, rm=6 (110b), то за modrm идет 16-битное смещение.

4. РАЗБОР SIB (Scale-Index-Base).

Этот байт используется для расширения возможности адресации операндов. Его присутствие можно определить из байта ModR/M (смотри выше, как). SIB состоит из 3-х полей:

  • scale

  • Index

  • Base


С помощью байта SIB можно задавать выражения вида [reg1+reg2*scale+СМЕЩЕНИЕ]. Структура SIB:

Биты 6-7: scale

  • 00b - 1

  • 01b - 2

  • 10b - 4

  • 11b - 8


Биты 3-5: reg2 - если reg2 == 4, то reg2 не используется

Биты 0-2: reg1 - если reg1==5 и если mod в ModR/M равен 00, то за сразу SIB идет 32-битное смещение, иначе ebp

Важно: в 16-разрядном режиме (0x67) байта SIB нет!

5. СМЕЩЕНИЕ и НЕПОСРЕДСТВЕННЫЙ ОПЕРАНД.

Эти поля - 8, 16 или 32-разрядные числа. Разрядность определяется по характеристикам опкода + префиксам (0x67/0x66).

Также, хотелось бы черкануть пару слов о дизасмах в вирусах. Такую силу должен иметь каждый нормальный вирь:) Пока еще большинство прог (но не все!) юзают стандартные & fpu команды. Поэтому задумайся - так ли тебе нужны хери типа mmx, sse1/2/3? Дизассемблер должен быть быстр и предельно компактен - короче оптимизация во все щели. Но, как это часто случается, страдает либо морда, либо жопа. Компактен - не означает, что после написания движка к нему хер прикрутишь новые команды. Это важно. И естесственно, код должен быть базонезависимым.

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

Добавлю, что для написания своего дизасма в первую очередь нужно решить - для чего он тебе нужен? Ну а потом уже штудировать маны от Intel + сорцы других движков. Удачи!

P.S. Начать изучение сорсов можешь с моего простенького дизассемблера.

Используемые ссылки:

//pr0mix@mail.ru
LinkLeave a comment

Comments:
From:(Anonymous)
Date:May 8th, 2013 - 03:30 pm

Насчёт префиксов

(Link)
Вы пишете:
2Eh и 3Eh (появились в Pentium 4 & Xeon) - еще и определяют (только с инструкциями Jcc):
* 2Eh - переход выполняется;
* 3Eh - переход не выполяется.

afaik, эти префиксы не определяют выполнение перехода, а указывают процессору на наиболее вероятную ветку для перехода.
это используется при предсказывании ветвлений в процессе загрузки инструкций в конвейер
From:(Anonymous)
Date:December 3rd, 2013 - 05:32 pm
(Link)
Вот нормальная тема про дизасмы
http://z0mbie.daemonlab.org/bitmask.html
Плюс еще есть хардварный вариант Indy Clerk'а на фолтах