Яндекс.Метрика

    Песочница

    Дизассемблер своими руками

         Знание структуры машинных команд уже много лет не является обязательным, для того, чтобы человек мог назвать себя программистом. Естественно так было не всегда. До появления первых ассемблеров программирование осуществлялось непосредственно в машинном коде. Каторжная работа, сопряженная с большим количеством ошибок. Современные ассемблеры позволяют (в разумной степени) абстрагироваться от железа, метода кодирования команд. Что уж говорить о компиляторах высокоуровневых языков. Они поражают сложностью своей реализации и той простотой, с которой программисту позволяется преобразовывать исходный код в последовательность машинных команд (причем преобразовывать, в достаточной степени, оптимально). От программиста требуется лишь знание любимого языка/ IDE. Знание того, во что преобразует компилятор исходный листинг вовсе не обязательно.
    Тем же, кому интересно взглянуть на краткое описание структуры кодирования машинных команд, пример реализации и исходный код дизассемблера для x86 архитектуры, добро пожаловать.

         Создание дизассемблера для x86 архитектуры является, хотя задачей и не особо сложной, но все, же довольно специфичной. От программиста требуются определенного рода знания – знания того, как микропроцессор распознает последовательность “байтиков” в машинном коде. Далеко не в каждом вузе можно получить такие знания в объеме достаточном для написания полнофункционального современного дизассемблера – приходится искать самому (как правило, на английском языке). Данный пост не претендует на полноту освещение проблемы создания дизассемблера, в нем лишь кратко рассказывается то, как был написан дизассемблер для x86 архитектуры, 32-разрядного режима исполнения команд. Так же хотелось бы отметить вероятность возможных неточностей при переводе некоторых понятий из официальной спецификации.

         Структура команд для intel x86

    Структура команды следующая:
    • Опциональные префиксы (каждый префикс имеет размер 1 байт)
    • Обязательный опкод команды (1 или 2 байта)
    • Mod_R/M – байтик, определяющий структуру операндов команды — опциональный.
    • Опциональные байты, занимаемые операндами команды (иногда разделено как один байт поля SIB[Scale, Index, Base], смещения и непосредственного значения).

    Префиксы

         Существуют следующие префиксы:
    Первые шесть изменяют сегментный регистр, используемый командой при обращении к ячейке памяти.
    • 0x26 – префикс замены сегмента ES
    • 0x2E – префикс замены сегмента CS
    • 0x36 – префикс замены сегмента SS
    • 0x3E – префикс замены сегмента BS
    • 0x64 – префикс замены сегмента FS
    • 0x65 – префикс замены сегмента GS

    • 0x0F – префикс дополнительных команд (иногда его не считают за настоящий префикс – в этом случае считается, что опкод команды состоит из двух байт, первый из которых 0x0F)

    • 0x66 – префикс переопределения размера операнда (к примеру, вместо регистра eax будет использоваться ax)
    • 0x67 – префикс переопределения размера адреса (см ниже)
    • 0x9B – префикс ожидания (WAIT)
    • 0xF0 – префикс блокировки (LOCK с его помощью реализуется синхронизация многопоточных приложений)
    • 0xF2 – префикс повторенья команды REPNZ – работа с последовательностями байт (строками)
    • 0xF3 – префикс повторенья команды REP – работа с последовательностями байт (строками)

    Каждый из этих префиксов меняет семантику и (или) структуру машинной инструкции (например, ее длину или выбор мнемоники).

         Опкоды команд.
    Опкод команды иногда один, иногда вмести с префиксом (ами) однозначно определяет мнемонику (название) команды. Команд много. И при усложнении современных микропроцессоров их количество не уменьшается – новые команды появляются, а устаревшие не исчезают (обратная совместимость). Список опкодов и команд ассоциированных с ними, как правило, можно скачать на официальных сайтах производителей микропроцессоров.

    Байт Mod_R/M состоит из следующих полей:

    • Mod – первые два бита (значение от 0 до 3)
    • R/M – следующие три бита (значение от 0 до 7)
    • Value of ModR/M – следующие три бита (значение от 0 до 7)

         Реализация:

    Для написания дизассемблера мы будем использовать следующую страничку: http://ref.x86asm.net/geek32.html.

         Мы видим несколько таблиц. В сущности, только эти таблицы и описание их полей нам и понадобятся, для написания дизассемблера. Конечно, дополнительно требуется способность к логическому рассуждению и свободное время.
         В первой таблице представлен список машинных команд, не содержащих префикс 0x0F. Во второй список команд содержащих этот префикс (большинство этих команд появились в микропроцессорах семейства “Pentium with MMX” или более поздних).
         Следующие три таблицы позволяют преобразовать байт Mod_R/M в последовательность операндов команды для 32-битного режима кодировки команд. Причем каждая последующая из этих трех таблиц уточняет разбор Mod_R/M байта частных случаев предыдущей таблицы.
         Последняя таблица позволяет преобразовать байт Mod_R/M в последовательность операндов команды для 16-битного режима кодировки команд. По умолчанию считается, что команда кодируется в 32-битном режиме. Для смены режима кодировки используется префикс переопределения размера адреса (0x67).

         Первое, что необходимо сделать, это перенести первые две таблицы в удобные для работы структуры данных. На том же сайте можно скачать xml-версии данных таблиц, и уже их преобразовать в красивые сишные структуры. Я же поступил иначе – загрузил html таблицы в Excel, и уже там, написав несложный скриптик на VBA, получил исходный сишный код, который, уже после ручных исправлений представлял собой требуемые структуры данных.

    Сам алгоритм дизассемблирования достаточно прост:

    • Собирается список префиксов, используемых в текущей машинной инструкции
    • Ищется в одной из двух таблиц соответствующее поле в зависимости от опкода, префиксов и поколения (модели) целевого (искомого) микропроцессора.
    • Найденная нами запись характеризуется списком полей такими как поколение (модель) микропроцессора, с которого появилась поддержка данной команды или, например, список флагов, которые данная команда может изменить. Нас же, в основном, интересуют лишь мнемоника (название) команды и список операндов. Проанализировав все операнды найденной и поля байта Mod_R/M, мы сможем узнать текстовое представление и длину команды.

         Количество операндов может колебаться от нуля до трех. Исходные таблицы содержат более сотни типов операндов. Некоторые операнды дублируются – у них различные названия, но последовательность действий обработки Mod_R/M байта (и возможно последующих байтов) у них одинакова.

         Для просмотра примера обработки различных операндов и примера дизассемблирования простейшей функции “Hello world” можно скачать исходный код дизассемблера для компилятора C++ Builder 6.

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