Кодим дисассемблер своими руками
автор: Great
I. Intro
Эта статья предназначена для тех, кто знает знает язык ассемблера. Если ты его не знаешь - не огорчайся, есть хороший учебник по ассемблеру В.Юрова, его можно найти практически в любом книжном магазине. Я расскажу про структуру машинных команд в архитектуре IA-32, для закрепления знаний и навыком мы вручную проассемблируем/дисассемблируем несколько команд и накодим простенький дисассемблер. Для упрощения мы реализуем дисассемб**цию только целочисленных команд реального режима. Статья написана для тех, кто знаком с языком Си (в программе используются некоторые конструкции С++, но ООП не используется, знаний Си будет достаточно).
II. Структура машинных команд IA-32
В общем случае машинная команда состоит из следующих полей:
1. Одобайтовые префиксы (одновременно до 4-х штук). Префиксы ставятся перед командой для изменения ее действия. Префиксы есть следующие:
- префиксы повторения. Ставятся перед командой для повторения команды определенное число раз (REP) или до наступления (или, наоборот, до прекращения) какого-то условия (REPE/REPNE).
- префикс размера операнда. Ставится перед командой для замены действующего размера операнда на 16/32 бит. Опкод - 66h
- префикс размера адреса. Аналогично предыдущему, но для размера адреса. Опкод - 67h
- префикс замены сегмента. Используется для явного указания сегментной составляющей адреса для следующей команды. Например:
CS: MOV EAX, DWORD PTR [100]
Без префикса в EAX окажется число по адресу DS:[100], с префиксом CS: адрес будет CS:[100]. Опкоды: CS=2e, DS=3e, ES=26h, SS=36, FS=64, GS=65.
- префикс блокировки шины (LOCK). Используется для синхронизаций действий процессора и сопроцессора. Опкод:f0h
2. Код операции (1 байт, если первый байт=0f, то 2 байта. Иногда 0f пречисляют к префиксам, но я склонен относить его к коду операции)
3. байт mod r/m, кодирующий формат операндов команды. Имеет такую структуру:
Биты 7-6 -- поле mod. Кодирует тип адресации
Биты 5-3 -- поле reg/КОП. Кодирует второй регистр команды (под первым регистром понимаем регистр, участвующий в адресации), либо пордолжает код операции, размер которого составляет тогда в совокупности 11 байт.
Биты 2-0 -- поле r/m. Кодирует первый(-е) регистр(-ы), участвующий(-е) в адресации
Для упрощения нашей жизни есть специальные таблицы со всеми возможными значениями байта mod r/m.
4. байт sib (scale, index, base). Кодирует 32-разрядную косвенную адресацию
5. Смещение в команде
6. Непосредственный операнд
Все числа по законам процессора Intel записываются в памяти в обратном порядке следования байт. То есть, число 12345678h будет записано как 78 56 34 12.
Приведу пример команды:
Здесь цифрами обозначено:
1 - префикс замены сегмента 2e (CS
2 - опкод C7 (MOV)
3 - байт mod r/m
4 - смещение в команде (5f4e, записанное в обратном порядке следования байт)
5 - операнд (1234, в обратном порядке)
Ну вот вроде теперь все ясно насчет структуры машинной команды и мы перейдем к написанию дисассемблера.
III. Кодинг
Я уже написал скелет дисассемблера (сорцы ты найдешь в приложении к статье).
Я лишь объясню тебе принцип действия и назначение каждой функции. Итак, начнем с осмотра файлов:
interface.cpp - интерфейс. Содержит функцию main() и прочую чушь - открытие входного файла, чтение и проч.
disasm.cpp - тут самое главное. Функция disasm()
disasm_help.cpp - вспомогательные функции дисассемблера
disasm.h - хидер. Прототипы функций и объявления статических переменных (таблиц).
В интерфейсе ничего интересного нет, разберешься без меня. Теперь дисассемблер.
Думаю, стоит пояснить назначение каждой функции:
- void disasm(unsigned char*,int);
основная функция. Выводит дисассемблерный листинг. Первый параметр - указатель на расположенный в памяти код, второй - длина кода
- void parse_mod_rm(unsigned char mod_rm, unsigned int *mod, unsigned int *reg, unsigned int *rm);
извлекает из байта mod r/m соотв. поля и заносит их по адресам, переданным в виде параметров mod, reg, rm.
- char* get_address_by_mod_rm(unsigned int mod, unsigned int rm, int *offsetsize, int regsize, int *isaddress);
ты самая функция, которая интерпретирует значение байта mod r/m. Передаваемые параметры:
mod, rm - значения соотв. полей, извлеченные функцией parse_mod_rm()
*offsetsize - адрес переменной, куда будет записан размер смещения
regsize - текущий размер операнда (бит). Допустимые значения - 8, 16, 32
*isaddress - адрес переменной, куда будет записано, является ли возвращенная строка адресом или нет (0 - нет, 1 - является).
Функция ищет соотв. значение в таблице (закодированной с оригинальных таблиц байта mod r/m) и возвращает результат в виде строки
Назначение переменных:
int chopsize=0, // флаг размера операнда (CHange OPerand SIZE)
chaddrsize=0; // флаг размера адреса (CHange ADDRess SIZE)
unsigned int mod=0,reg=0,rm=0; // значения полей байта mod r/m, извлекаемые parse_mod_rm()
int offsetsize=0; // размер смещения
char* adr=0; // принимает строку, возвращаемую get_address_by_mod_rm(). Может содержать адрес, но не обязан. Зависит от значения полей mod и rm.
int isaddress=0; // является ли строка в предыдущей переменной адресом. (0-нет, 1-да)
После этого рассмотрим как все эти функции применяются вместе.
Префиксы:
case 0x66:
chopsize=1;
ip++;
goto switch__;
break;
тут все просто. Устанавливаем флаг и переходим к свитчу снова (замечу, что нельзя просто пройти далее, сделав break - тогда следующая команда будет на новой строке, а эта останется пуста - мы ведь ничего не выводим).
Однобайтовые команды:
case 0x37:
printf("AAA\n");
break;
Думаю, и так все ясно =). Пояснений не даю.
Двухбайтовые:
case 0xd5:
printf("AAD");
ip++;
if(*ip==0x0a)
printf("\n");
else printf(" %x\n", *ip);
break;
Это чуток сложней предыдущего. Дело в том, что команда принимает аргумент - основание системы счисления для пересчета BCD-чисел. Но если он равен 10 (0a), он обычно не выводится. Поэтому от его значения мы решаем - выводить операнд или нет.
case 0xeb:
ip++;
printf("JMP %x\n", *ip+1);
break;
Команда ближнего безусловного перехода (JMP NEAR). Адрес задается одним байтом.
Теперь пример сложной команды - MOV.
case 0x8b: // команда mov r/m16/32, r/m16/32
printf("MOV "); // выводим мнемонику
ip++; // переходим к следующему байту
parse_mod_rm(*ip, &mod, ®, &rm); // анализируем байт mod r/m
if(chopsize) // не установлен ли префикс размера операнда? Если да:
{
adr=get_address_by_mod_rm(mod, rm, &offsetsize, 32, &isaddress); // получаем адрес по полю mod r/m
if(isaddress) // выводим операнды
printf("%s,[%s]\n", regs32[reg], adr); // команда вида mov регистр, [регистр]
else
printf("%s,%s\n", regs32[reg], adr); // команда вида mov регистр, регистр
chopsize=0; // сбрасываем флаг размера операнда
}
else // ... если нет:
{
adr=get_address_by_mod_rm(mod, rm, &offsetsize, 16, &isaddress);
if(isaddress)
printf("%s,[%s]\n", regs16[reg], adr); // команда вида mov регистр, [регистр]
else
printf("%s,%s\n", regs16[reg], adr); // команда вида mov регистр, регистр
}
break;
Сначала мы выводим мнемонику команды, чтоб понять, что это именно MOV, а не PUSH и не IRET. Потом мы парсим байт mod r/m и, в зависимости от установленного флага размера операнда (устанавливается при анализе соотв. префикса) выводим операнды.
Теперь тебе все должно быть понятно. Естественно, я не стал писать полноценный дисассемблер. Это лишь скелет.
Дописать еще несколько команд предлагаю в качастве домашнего задания. Приведу описание нескольких команд, не реализованных в моем дисассемблере:
- INT ;генерация прерывания
опкод: CD XX, XX - номер прерывания.
- PUSH reg ;заталкивание в стек регистра общего назначения
опкод: (50+rd), rd - регистр общего назначения из таблицы:
0 - EAX
1 - ECX
2 - EDX
3 - EBX
4 - ESP
5 - EBP
6 - ESI
7 - EDI
На этом все, удачного компилирования =)
Прикрепленные файлы - сорцы дисассемблера
автор: Great
I. Intro
Эта статья предназначена для тех, кто знает знает язык ассемблера. Если ты его не знаешь - не огорчайся, есть хороший учебник по ассемблеру В.Юрова, его можно найти практически в любом книжном магазине. Я расскажу про структуру машинных команд в архитектуре IA-32, для закрепления знаний и навыком мы вручную проассемблируем/дисассемблируем несколько команд и накодим простенький дисассемблер. Для упрощения мы реализуем дисассемб**цию только целочисленных команд реального режима. Статья написана для тех, кто знаком с языком Си (в программе используются некоторые конструкции С++, но ООП не используется, знаний Си будет достаточно).
II. Структура машинных команд IA-32
В общем случае машинная команда состоит из следующих полей:
1. Одобайтовые префиксы (одновременно до 4-х штук). Префиксы ставятся перед командой для изменения ее действия. Префиксы есть следующие:
- префиксы повторения. Ставятся перед командой для повторения команды определенное число раз (REP) или до наступления (или, наоборот, до прекращения) какого-то условия (REPE/REPNE).
- префикс размера операнда. Ставится перед командой для замены действующего размера операнда на 16/32 бит. Опкод - 66h
- префикс размера адреса. Аналогично предыдущему, но для размера адреса. Опкод - 67h
- префикс замены сегмента. Используется для явного указания сегментной составляющей адреса для следующей команды. Например:
CS: MOV EAX, DWORD PTR [100]
Без префикса в EAX окажется число по адресу DS:[100], с префиксом CS: адрес будет CS:[100]. Опкоды: CS=2e, DS=3e, ES=26h, SS=36, FS=64, GS=65.
- префикс блокировки шины (LOCK). Используется для синхронизаций действий процессора и сопроцессора. Опкод:f0h
2. Код операции (1 байт, если первый байт=0f, то 2 байта. Иногда 0f пречисляют к префиксам, но я склонен относить его к коду операции)
3. байт mod r/m, кодирующий формат операндов команды. Имеет такую структуру:
Биты 7-6 -- поле mod. Кодирует тип адресации
Биты 5-3 -- поле reg/КОП. Кодирует второй регистр команды (под первым регистром понимаем регистр, участвующий в адресации), либо пордолжает код операции, размер которого составляет тогда в совокупности 11 байт.
Биты 2-0 -- поле r/m. Кодирует первый(-е) регистр(-ы), участвующий(-е) в адресации
Для упрощения нашей жизни есть специальные таблицы со всеми возможными значениями байта mod r/m.
4. байт sib (scale, index, base). Кодирует 32-разрядную косвенную адресацию
5. Смещение в команде
6. Непосредственный операнд
Все числа по законам процессора Intel записываются в памяти в обратном порядке следования байт. То есть, число 12345678h будет записано как 78 56 34 12.
Приведу пример команды:
Код:
2E C786 4E5F 3412 mov word cs:[bp+0x5f4e],0x1234
^ ^ ^ ^ ^
1 2 3 4 5
1 - префикс замены сегмента 2e (CS
2 - опкод C7 (MOV)
3 - байт mod r/m
4 - смещение в команде (5f4e, записанное в обратном порядке следования байт)
5 - операнд (1234, в обратном порядке)
Ну вот вроде теперь все ясно насчет структуры машинной команды и мы перейдем к написанию дисассемблера.
III. Кодинг
Я уже написал скелет дисассемблера (сорцы ты найдешь в приложении к статье).
Я лишь объясню тебе принцип действия и назначение каждой функции. Итак, начнем с осмотра файлов:
interface.cpp - интерфейс. Содержит функцию main() и прочую чушь - открытие входного файла, чтение и проч.
disasm.cpp - тут самое главное. Функция disasm()
disasm_help.cpp - вспомогательные функции дисассемблера
disasm.h - хидер. Прототипы функций и объявления статических переменных (таблиц).
В интерфейсе ничего интересного нет, разберешься без меня. Теперь дисассемблер.
Думаю, стоит пояснить назначение каждой функции:
- void disasm(unsigned char*,int);
основная функция. Выводит дисассемблерный листинг. Первый параметр - указатель на расположенный в памяти код, второй - длина кода
- void parse_mod_rm(unsigned char mod_rm, unsigned int *mod, unsigned int *reg, unsigned int *rm);
извлекает из байта mod r/m соотв. поля и заносит их по адресам, переданным в виде параметров mod, reg, rm.
- char* get_address_by_mod_rm(unsigned int mod, unsigned int rm, int *offsetsize, int regsize, int *isaddress);
ты самая функция, которая интерпретирует значение байта mod r/m. Передаваемые параметры:
mod, rm - значения соотв. полей, извлеченные функцией parse_mod_rm()
*offsetsize - адрес переменной, куда будет записан размер смещения
regsize - текущий размер операнда (бит). Допустимые значения - 8, 16, 32
*isaddress - адрес переменной, куда будет записано, является ли возвращенная строка адресом или нет (0 - нет, 1 - является).
Функция ищет соотв. значение в таблице (закодированной с оригинальных таблиц байта mod r/m) и возвращает результат в виде строки
Назначение переменных:
int chopsize=0, // флаг размера операнда (CHange OPerand SIZE)
chaddrsize=0; // флаг размера адреса (CHange ADDRess SIZE)
unsigned int mod=0,reg=0,rm=0; // значения полей байта mod r/m, извлекаемые parse_mod_rm()
int offsetsize=0; // размер смещения
char* adr=0; // принимает строку, возвращаемую get_address_by_mod_rm(). Может содержать адрес, но не обязан. Зависит от значения полей mod и rm.
int isaddress=0; // является ли строка в предыдущей переменной адресом. (0-нет, 1-да)
После этого рассмотрим как все эти функции применяются вместе.
Префиксы:
case 0x66:
chopsize=1;
ip++;
goto switch__;
break;
тут все просто. Устанавливаем флаг и переходим к свитчу снова (замечу, что нельзя просто пройти далее, сделав break - тогда следующая команда будет на новой строке, а эта останется пуста - мы ведь ничего не выводим).
Однобайтовые команды:
case 0x37:
printf("AAA\n");
break;
Думаю, и так все ясно =). Пояснений не даю.
Двухбайтовые:
case 0xd5:
printf("AAD");
ip++;
if(*ip==0x0a)
printf("\n");
else printf(" %x\n", *ip);
break;
Это чуток сложней предыдущего. Дело в том, что команда принимает аргумент - основание системы счисления для пересчета BCD-чисел. Но если он равен 10 (0a), он обычно не выводится. Поэтому от его значения мы решаем - выводить операнд или нет.
case 0xeb:
ip++;
printf("JMP %x\n", *ip+1);
break;
Команда ближнего безусловного перехода (JMP NEAR). Адрес задается одним байтом.
Теперь пример сложной команды - MOV.
case 0x8b: // команда mov r/m16/32, r/m16/32
printf("MOV "); // выводим мнемонику
ip++; // переходим к следующему байту
parse_mod_rm(*ip, &mod, ®, &rm); // анализируем байт mod r/m
if(chopsize) // не установлен ли префикс размера операнда? Если да:
{
adr=get_address_by_mod_rm(mod, rm, &offsetsize, 32, &isaddress); // получаем адрес по полю mod r/m
if(isaddress) // выводим операнды
printf("%s,[%s]\n", regs32[reg], adr); // команда вида mov регистр, [регистр]
else
printf("%s,%s\n", regs32[reg], adr); // команда вида mov регистр, регистр
chopsize=0; // сбрасываем флаг размера операнда
}
else // ... если нет:
{
adr=get_address_by_mod_rm(mod, rm, &offsetsize, 16, &isaddress);
if(isaddress)
printf("%s,[%s]\n", regs16[reg], adr); // команда вида mov регистр, [регистр]
else
printf("%s,%s\n", regs16[reg], adr); // команда вида mov регистр, регистр
}
break;
Сначала мы выводим мнемонику команды, чтоб понять, что это именно MOV, а не PUSH и не IRET. Потом мы парсим байт mod r/m и, в зависимости от установленного флага размера операнда (устанавливается при анализе соотв. префикса) выводим операнды.
Теперь тебе все должно быть понятно. Естественно, я не стал писать полноценный дисассемблер. Это лишь скелет.
Дописать еще несколько команд предлагаю в качастве домашнего задания. Приведу описание нескольких команд, не реализованных в моем дисассемблере:
- INT ;генерация прерывания
опкод: CD XX, XX - номер прерывания.
- PUSH reg ;заталкивание в стек регистра общего назначения
опкод: (50+rd), rd - регистр общего назначения из таблицы:
0 - EAX
1 - ECX
2 - EDX
3 - EBX
4 - ESP
5 - EBP
6 - ESI
7 - EDI
На этом все, удачного компилирования =)
Прикрепленные файлы - сорцы дисассемблера