Abstract

A long time ago in a Galaxy far, far away...


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

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

Дизассемблер работает в 16/32/64битных режимах, поддерживаются общий набор инструкций Intel и AMD, наборы инструкций FPU, SSE1, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, SMX, Intel-VT. Ожидается поддержка 3DNow! и AMD VMX. Дизассемблер определяет принадлежность инструкции к какой-либо группе инструкций, ID инструкции, тестируемые, изменяемые, устанавливаемые, сбрасываемые флаги регистра EFLAGS, а также флаги, значение которых не определено. Отлавливаются все избыточные префиксы, поддерживаются полудокументированные и недокументированные инструкции, UNICODE, многопоточность, работа в Linux и Windows. Остальные возможности дизассемблера описываются ниже вместе с описанием входных и выходных данных.

man disasm
Главная выходной структурой является struct INSTRUCTION. Рассмотрим ее поля.
struct INSTRUCTION
struct INSTRUCTION
{
    uint64_t groups;
    uint16_t id;
    uint16_t flags;
    uint16_t prefixes;
    uint8_t  opcode_offset;

    struct OPERAND ops[3];
    struct DISPLACEMENT disp;

    uint8_t addrsize;
    uint8_t opsize;
    uint8_t modrm;
    uint8_t sib;
    uint8_t rex;

    uint8_t tested_flags;
    uint8_t modified_flags;
    uint8_t set_flags;
    uint8_t cleared_flags;
    uint8_t undefined_flags;

    unichar_t mnemonic[MAX_MNEMONIC_LEN];
};
После разбора дизассемблер предоставляет следующую информацию об инструкции:
uint64_t groups Группы, в которые входит инструкция. Например, GRP_GEN | GRP_ARITH обозначает, что группа относится к общим и арифметическим инструкциям.
uint16_t id ID инструкции. Инструкции, имеющие разные типы операндов, например 'call 0x401000' и 'call eax' имеют одинаковый ID. Это же касается и инструкций с мнемоникой, зависящей от размера операнда. Различать такие инструкции следует по полю opsize структуры INSTRUCTION. Список всех ID инструкций слишком большой, чтобы вставлять его сюда, его можно увидеть в файле "mediana.h"
uint16_t flags Флаги инструкции. Возможные значения:
INSTR_FLAG_MODRMИнструкция имеет байт MODRM.
INSTR_FLAG_SIBИнструкция имеет байт SIB.
INSTR_FLAG_SF_PREFIXESИнструкция содержит избыточные префиксы.
INSTR_FLAG_IOPLИнструкция является чувствительной к значению поля IOPL регистра EFLAGS.
INSTR_FLAG_RING0Инструкция выполняется только в привилегированном режиме (ring0).
INSTR_FLAG_SERIALСериализирующая инструкция.
INSTR_FLAG_UNDOCНедокументированная инструкция. Если этот бит установлен, значит инструкция отсутствует в таблицах Intel и/или AMD.
uint16_t prefixes Префиксы инструкции. Сюда попадают лишь те префиксы, которые оказывают действительное влияние на инструкцию. Возможные значения:
INSTR_PREFIX_CS
INSTR_PREFIX_DS
INSTR_PREFIX_ES
INSTR_PREFIX_SS
INSTR_PREFIX_FS
INSTR_PREFIX_GS
//Segment prefixes mask:
INSTR_PREFIX_SEG_MASK
INSTR_PREFIX_REPZ
INSTR_PREFIX_REPNZ
//Repeat prefixes mask:
INSTR_PREFIX_REP_MASK
INSTR_PREFIX_OPSIZE
INSTR_PREFIX_ADDRSIZE
INSTR_PREFIX_REX
//Operand size prefixes mask:
INSTR_PREFIX_SIZE_MASK
//LOCK prefix:
INSTR_PREFIX_LOCK
Думаю, что в дополнительных описаниях эти значения не нуждаются :).
uint8_t opcode_offset Смещение байта кода операции относительно начала инструкции.
struct OPERAND ops[3] Массив операндов инструкции. Структура OPERAND будет описана далее.
struct DISPLACEMENT disp Смещение в команде. Структура DISPLACEMENT будет описана далее. Т.к. у инструкции не может быть более одного смещения, то для экономии его значение хранится в структуре INSTRUCTION, хотя и относится к одному из операндов.
uint8_t addrsize Разрядность адреса инструкции. Константы:
#define ADDR_SIZE_16 0x2
#define ADDR_SIZE_32 0x4
#define ADDR_SIZE_64 0x8
uint8_t opsize Разрядность неявного операнда инструкции. Этот член используется только для инструкций, мнемоника которых зависит от размера неявного операнда, например pushfw/pushfd/pushfq. В остальных случаях оно равно нулю.
uint8_t modrm Значение байта MODRM. Наличие/отсутствие байта MORDM определяется флагами инструкции (см. выше).
uint8_t sib Значение байта SIB. Наличие/отсутствие байта SIB определяется флагами инструкции (см. выше).
uint8_t rex Значение префикса REX. Наличие/отсутствие префикса REX определяется установленным/сброшенным битов в prefixes (см. выше). Константы для полей префикса:
#define PREFIX_REX_W 0x8
#define PREFIX_REX_R 0x4
#define PREFIX_REX_X 0x2
#define PREFIX_REX_B 0x1
uint8_t tested_flags Флаги регистра EFLAGS, тестируемые инструкцией. Константы:
#define EFLAG_C 0x01
#define EFLAG_P 0x02
#define EFLAG_A 0x04
#define EFLAG_Z 0x08
#define EFLAG_S 0x10
#define EFLAG_I 0x20
#define EFLAG_D 0x40
#define EFLAG_O 0x80
Константы для флагов FPU:
#define FPU_FLAG0 0x01
#define FPU_FLAG1 0x02
#define FPU_FLAG2 0x04
#define FPU_FLAG3 0x08
uint8_t modified_flags Флаги регистра EFLAGS, модифицируемые инструкцией.
uint8_t set_flags Флаги регистра EFLAGS, устанавливаемые в единицу.
uint8_t cleared_flags Флаги регистра EFLAGS, сбрасываемые в ноль.
undefined_flags Флаги регистра EFLAGS, чье значение не определено.
unichar_t mnemonic[MAX_MNEMONIC_LEN] Мнемоника инструкции (кто бы мог подумать?..)

Теперь разберем остальные структуры, содержащиеся в INSTRUCTION. Следующая важная структура, в некотором смысле описывающее лицо дизассемблера -- struct OPERAND.

struct OPERAND
В структуре OPERAND поддерживается четыре типа операнда: регистр, память, "прямой адрес" и непосредственное значение. Каждый тип операнда описывается отдельной структурой, структуры в свою очередь объединены в union для экономии места. Кроме того, структура содержит два члена "size" и "flags". Описание всех членов структуры следует ниже.
struct OPERAND
{
	union
	{
		struct REG
		{
			uint8_t code;
			uint8_t type;
		} reg;

		struct IMM
		{
			union
			{
				uint8_t  imm8;
				uint16_t imm16;
				uint32_t imm32;
				uint64_t imm64;
			};
			uint8_t size;
			uint8_t offset;
		} imm;

		struct FAR_ADDR
		{
			union
			{
				struct FAR_ADDR32
				{
					uint16_t offset;
					uint16_t seg;
				} far_addr32;

				struct FAR_ADDR48
				{
					uint32_t offset;
					uint16_t seg;
				} far_addr48;
			} ;

			uint8_t offset;
		} far_addr;

		struct ADDR
		{
			uint8_t seg;
			uint8_t mod;
			uint8_t base;
			uint8_t index;
			uint8_t scale;
		} addr;
	} value;

	uint16_t size;
	uint8_t flags;
};
Члены "size" и "flags" описывают размер операнда и его флаги соответственно.
Более подробно о каждом типе операндов:
OPERAND_TYPE_REG
Регистр. Описывается структурой:
struct REG
{
	uint8_t code;
	uint8_t type;
} reg;
OPERAND_TYPE_MEM
Адрес. Описывается структурой:
struct ADDR
{
	uint8_t seg;
	uint8_t mod;
	uint8_t base;
	uint8_t index;
	uint8_t scale;
} addr;
OPERAND_TYPE_IMM
Непосредственный операнд. Описывается структурой:
struct IMM
{
	union
	{
		uint8_t  imm8;
		uint16_t imm16;
		uint32_t imm32;
		uint64_t imm64;
	};
	uint8_t size;
	uint8_t offset;
} imm;
OPERAND_TYPE_DIR
"Прямой" адрес. Эти "дальние" адреса размещаются в теле инструкции.
struct FAR_ADDR
{
	union
	{
		struct FAR_ADDR32
		{
			uint16_t offset;
			uint16_t seg;
		} far_addr32;

		struct FAR_ADDR48
		{
			uint32_t offset;
			uint16_t seg;
		} far_addr48;
	};
	uint8_t offset;
} far_addr;
Теперь вкратце разберем уже упоминавшуюся структуру DISPLACEMENT.

struct DISPLACEMENT
struct DISPLACEMENT
{
	uint8_t size;
	uint8_t offset;
	union VALUE
	{
		int16_t d16;
		int32_t d32;
		int64_t d64;
	} value;
};

Теперь, когда все основные структуры разобраны, можно перейти к функциям дизассемблера.
disassemble()
unsigned int disassemble(uint8_t                    *offset,  /* IN */
                         struct INSTRUCTION         *instr,   /* OUT */
                         struct DISASM_INOUT_PARAMS *params); /* IN/OUT */
Unicode
Поддержка Unicode осуществляется с помощью типа unichar_t. Этот тип, в зависимости от того, определен ли макрос UNICODE, будет объявлен как char/wchar_t.

Пример использования
#include <stdio.h>
#include <stdlib.h>
#include "mediana.h" //Главный заголовочный файл дизассемблера.

#define OUT_BUFF_SIZE 0x200
#define IN_BUFF_SIZE  14231285
#define SEEK_TO       0x0

int main(int argc, char **argv)
{
    uint8_t sf_prefixes[MAX_INSTRUCTION_LEN]; //Массив избыточных префиксов.
    unichar_t buff[OUT_BUFF_SIZE];            //Выходной буфер печати инструкции.
    struct INSTRUCTION instr;                 //Выходная инструкция.
    struct DISASM_INOUT_PARAMS params;        //Параметры дизассемблера.

    uint8_t *base, *ptr, *end;
    int reallen;
    unsigned int res;
    FILE *fp;

    params.arch = ARCH_ALL;                                                 //Включая все архитектуры.
    params.sf_prefixes = sf_prefixes;                                       //Подключение массива избыточных префиксов.
    params.mode = DISASSEMBLE_MODE_32;                                      //Режим дизассемблирования.
    params.options = DISASM_OPTION_APPLY_REL | DISASM_OPTION_OPTIMIZE_DISP; //Все опции.
    params.base = 0x00401000;                                               //Базовый адрес первой инструкции.

    base = malloc(IN_BUFF_SIZE);
    ptr = base;
    end = ptr + IN_BUFF_SIZE;


    fp = fopen("asm_com2.bin", "rb");
    fseek(fp, SEEK_TO, SEEK_SET);
    fread(base, IN_BUFF_SIZE, 1, fp);
    fclose(fp);


    while(ptr < end)
    {
        res = medi_disassemble(ptr, &instr, ¶ms); //Disassemble!
        if (params.errcode)
        {
            printf("%X: fail: %d, len: %d\n", ptr - base, params.errcode, res);
            if (res == 0)
                res++;
        }
        else
        {
            reallen = medi_dump(&instr, buff, OUT_BUFF_SIZE, DUMP_OPTION_IMM_UHEX | DUMP_OPTION_DISP_HEX); //Эта ф-ия будет описана ниже.
            if (reallen < OUT_BUFF_SIZE)
                buff[reallen] = 0;
            else
                buff[OUT_BUFF_SIZE - 1] = 0;

            printf("%X: %s\n", ptr - base, buff);
        }
        ptr += res;
        params.base += res; //Высчитываем базовый адрес следующей инструкции.
    }

    return 0;
}

Вывод результатов
Часто после того, как инструкция разобрана, ее необходимо вывести на экран или куда-либо еще. Дизассемблер имеет несколько функций для вывода текстовой информации об инструкции в буфер:
int dump(struct INSTRUCTION *instr,    /* IN */
         unichar_t          *buff,     /* OUT */
         int                 bufflen,  /* IN */
         int                 options); /* IN */

int dump_prefixes(struct INSTRUCTION *instr,    /* IN */
                  int                 options,  /* IN */
                  unichar_t          *buff,     /* OUT */
                  int                 bufflen); /* IN */

int dump_mnemonic(struct INSTRUCTION *instr,    /* IN */
                  unichar_t          *buff,     /* OUT */
                  int                 bufflen); /* IN */

int dump_operand(struct INSTRUCTION *instr,    /* IN */
                 int                 op_index, /* IN */
                 int                 options,  /* IN */
                 unichar_t          *buff,     /* OUT */
                 int                 bufflen); /* IN */

int dbg_dump(struct INSTRUCTION *instr,           /* IN */
             uint8_t            *sf_prefixes,     /* IN */
             int                 sf_prefixes_len, /* IN */
             unichar_t          *buff,            /* OUT */
             int                 len);            /* IN */
Функция dump выводит в буфер префиксы, мнемонику и операнды инструкции. Действие остальных функций понятно из названий. Отдельно стоит сказать про функцию dbg_dump: она выводит отладочную (наиболее полную) информацию об инструкции. В нее входят все префиксы, флаги и свойства инструкции, группы в которых она участвует, подробная информация операндах: их тип, значение и т.д.
Каждая функция принимает struct INSTRUCTION, а также выходной буфер (unichar_t *buff), его длину (int len) в символах и опции вывода непосредственных операндов и смещений. Дизассемблер заполняет буфер до тех пор, пока в нем есть место или пока не кончится текстовая информация об инструкции. Важное замечание: все функции возвращают требуемый для вывода инструкции размер буфера. Таким образом, если всегда можно узнать, какой объем буфера требуется для вывода информации об инструкции. Например, вы передали в функцию "dump" буфер длиной 10 символов. Если функция вернула значение 15, значит для вывода инструкции требуется буфер на пять символов больше.
Доступны четыре варианта вывода непосредственных операндов и смещений: знаковый шестнадцатеричный, беззнаковый шестнадцатеричный, знаковый десятичный и беззнаковый десятичный. Константы опций вывода:
#define DUMP_OPTION_IMM_HEX   0x01
#define DUMP_OPTION_IMM_UHEX  0x02
#define DUMP_OPTION_IMM_DEC   0x04
#define DUMP_OPTION_IMM_UDEC  0x08
#define DUMP_OPTION_IMM_MASK  0x0F

#define DUMP_OPTION_DISP_HEX  0x10
#define DUMP_OPTION_DISP_UHEX 0x20
#define DUMP_OPTION_DISP_DEC  0x40
#define DUMP_OPTION_DISP_UDEC 0x80
#define DUMP_OPTION_DISP_MASK 0xF0
Контроль компиляции
Для удобства контроля компиляции дизассемблера в него включен файл disasm_ctrl.h. На данный момент из него можно: Минусы дизассемблера
Несмотря на длительное время разработки, дизассемблер имеет несколько серьезных недостатков. Эти недостатки не относятся к мелким ошибкам, которые можно исправить, а носят скорее архитектурный характер. Надеюсь, что со временем эти недостатки будут устранены.

Немного мыслей о дизассемблировании
Во время написания дизассемблера я подглядывал в несколько существующих дизассемблеров в поисках хороших идей. Чаще всего это были libdisasm и дизассемблер из эмулятора Bochs. В этом параграфе я немного скажу, что думаю по поводу дизассемблирования и возможной архитектуры дизассемблера.
Мне очень понравилась идея табличного представления данных. Во-первых, поиск описателя инструкции по индексу кода операции в таблице происходит очень быстро, во-вторых, таблицы, в отличие от кода, позволяют довольно легко добавлять и изменять инструкции. В Bochs по каким-то причинам (м.б. для скорости) таблицы разделены на 32битную и 64битную версии. Мне такое разделение показалось избыточным, и я склонился к идее libdisasm. Как и libdisasm, мой дизассемблер описывает почти всю инструкцию одной записью в таблице. Такой подход имеет как плюсы, так и минусы. К плюсам можно отнести простоту редактирования инструкций, а также то, что большАя часть таблицы может попасть в кэш, таким образом ускорив дизассемблер. К минусам можно отнести избыточность и плохую расширияемость. Т.к. инструкция описывается одно записью, количество операндов жестко прописывается одновременно для всех инструкций и должно равняться макисмально возможному количеству операндов. В результате теряется память для инструкций, которые имеют меньше операндов. Это будет чувствоваться особо сильно если размер структуры OPERAND увеличится. Увеличивать максимальное число операндов тоже довольно сложно, т.к. придется увеличивать его сразу во всех таблицах. Вариант с ссылками (как в дизассемблере Bochs) кажется более гибким, но тоже имеет несколько недостатков. Редактировать такие таблицы вручную намного сложнее, а объем занимаемого места уменьшится незначительно, если уменьшится вообще. Подсчет для вероятных структур данных:
struct INTERNAL_OPERAND
{
    uint8_t type;
    uint8_t size;
};

struct OPERANDS
{
    struct INTERNAL_OPERAND operand[];
    uint8_t                 count;
};

struct OPCODE_DESCRIPTOR
{
    ...
    struct OPERANDS *operands;
    ...
};
В этом случае мы имеем 4 байта на указатель и, в случае отсутствия операндов, 7 байт для одного операнда, 9 байт для двух и 11 байт для трех операндов. Статистика по количеству операндов для моих таблиц:
Инструкций без операндов: 75
с одним операндом:        221
с двумя:                  807
и тремя:                  38
Получаем: 4 * 75 + 7 * 221 + 9 * 807 + 11 * 3 == 9 143 байт. В случае использования одной записи на инструкцию операнды всегда занимают 6 байт. Инструкций в моих таблицах 1 141, 6 * 1 141 == 6 846 байт.
Конечно, экономия 2 297 байт вряд ли покажется кому-то серьезной. Но все же, перед тем как принимать, решение лучше произвести небольшие подсчеты.

Отдельно хочется сказать про таблицы дизассемблера. Набивать таблицы руками, особенно до тех пор, пока формат таблиц не устоялся, очень тяжело. Есть смысл подыскать готовые таблицы (я взял таблицу в виде файла XML с http://ref.x86asm.net) и написать небольшой генератор, конвертирующий их в ваш формат. Изменять генератор намного проще и веселее, чем перебивать сотни инструкций руками. Особенно, если генератор написан, например, на С, а не на ассемблере, как в моем случае.

После того, как дизассемблер готов, его не лишне протестировать. В этом случае есть смысл либо написать генератор инструкций самому, либо взять готовый файл, содержащий инструкции во всех формах. Мне достался такой файл в готовом виде (кажется, его выкладывали на wasm.ru), скачать его можно здесь: mediana.sf.net. Файл содержит довольно много относительно новых инструкций, а также недокументированные инструкции и, видимо, уже несуществующие инструкции. Моя версия файла, в которой несуществующие инструкции заменены на nop, можно скачать здесь: mediana.sf.net.

Где скачать
Дизассемблер, и связанные с ним файлы, можно скачать здесь: Mediana.

Заключение
Это все, что я хотел сказать про дизассемблер. Надеюсь, что вам было интересно. Если у вас есть вопросы или вы нашли ошибку -- без стеснения пишите мне на почту (ящик указан в исходниках) или в журнал: http://mika0x65.livejournal.com

Благодарности