XT: 8086 дизасемблер
Передмова
У попередній статті про InfoSec редактор BE ми розглянули просте завдання написати термінальний дизасемблер використовуючи існуючі бібліотеки дизасемблювання. У цій статті ми покажемо як написати таку бібліотеку самим.
Ця вправа може стати лабораторною роботою, або курсовою роботою по курсу "КВ-123-17: Мікропроцесорні архітектури" для студентів першого курсу, які тільки знайомляться з існуючими архітектурами. Зазвичай у педагогічному процесі вибираються прості мікропроцесорні архітектури (такі як MIPS) або спеціалізовані для викладання (такі як RISC-V). Так чи інакше, працюючи у цій області, вам доведеться зіткнутися зі всіма архітектурами.
У якості асемблера, або мови програмування на якій пропонується виконувати завдання ми вибрали NASM, який вже використали для побудови термінального дизасемблера. Заодно буде з чим порівнювати, так як ndisasm з поставки nasm займає у бінарному вигляді 1.5 мегабайта. Ми ж даємо обмеження у цьому завданні на дизасемблер — 64 кілобайта.
Щоб вернути x86 архітектуру в академічне русло, ми обмежемо набір інструкцій системою команд мікропроцесора 8086, яка розширяє систему команд 8-бітних процесорів 8085 за допомогою додаткового байта MOD_R/M і збільшує таким чином розмір команди до 6 байт (довжина конвеєру).
Вступ
У курсі "КВ-123-16 Основи схемотехніки" для студентів першого курсу ви знайомитеся з чого складається елементна база 2-бітних мікропроцесорів Intel 3000, та як збирати такі системи самому. У якості підручника ми використовуємо "Цифрові ЕОМ: Практикум". Самофалов, Корнійчук, Тарасенко, Жабін. Київ, "Вища школа", 1990. Однак після 2-бітних, де ви самі програмували матрицю мікрокоду для команд, 4-бітних та 8-бітних процесорів, тільки 16-бітні процесори відкривають нову епоху сучасних процесорів Intel. Починаючи з цієї архітектури далі усі подальші процесори компанії Intel були сумісними між собою, тож вивчивши архітектуру 8086 ви зрозумієте контекст розвитку EM64T.
Рік | Код | Бітність | Опис | Чіпсет |
---|---|---|---|---|
1970 | 3000 | 2 | Intel 3000 | DIY |
1971 | 4000 | 4 | Intel 4000 | MCS-4 |
1974 | ZX | 8 | Intel 8085/8080 | MCS-85 |
1976 | XT | 16 | Intel 8086/8088 | MCS-86 |
1982 | AT | 16 | Intel 80286 | PS/2 Model 30 |
1985 | IA-32 | 32 | Intel 80386 | PS/2 Model 50 |
1989 | IA-32 | 32 | Intel 80486 | PS/2 Model 70 |
1993 | IA-32 | 32 | Intel Pentium (P5/P54C) | Triton |
1995 | IA-32 | 32 | Intel Pentium Pro/II/III | 82443BX |
2000 | EM64T | 64 | Intel Pentium 4 | 82865P |
2006 | EM64T | 64 | Intel Core | Q35 |
2008 | EM64T | 64 | Intel Atom | |
2011 | AVX | 128 | Intel Core (Sandy Bridge) | Z77, X99 |
2013 | AVX-2 | 256 | Intel Core (Haswell) | Z97 |
2017 | AVX-512 | 512 | Intel Phi, Core (Skylake) | Z370, X299 |
2023 | AMX | 1024 | Intel Core (Sapphire Rapids) | W790 |
Мотивація
Особливо важливий це курс для тих, хто на третьому році навчання буде слухати курси на кафедрі мовного забезпечення КА-121, такі як "КА-121-04 Верифікація програмного забезпечення", "КА-121-03 Верифікація мікропроцесорних архітектур", та "КА-121-05 Верифікація об`єктного коду".
Корисним цей курс буде для всіх, хто хоче навчитися вивчити асемблер у повному обсязі, так як ми пишемо асемблер та/або дизасеблер. У залежності від технічної орієнтації студентів, можна взяти альтернативний курс розробки LISP інтерпретатора на асемблері, але це вимагає знання програми другого року навчання.
Анотація
TL;DR — Дизасемблер на асемблері для Mac за 1 день на 672 байта для 45 опкодів, де використовується байт MOD_R/M (00, 01, 02, 03, 08, 09, 0A, 0B, 10, 11, 12, 13, 18, 19, 1A, 1B, 20, 21, 22, 23, 29, 29, 2A, 2B, 30, 31, 32, 33, 3C, 3D, 3E, 3F, 88, 89, 8A, 8B, 8D, 80, 81, 84, 85, 86, 87, C4, C5), тобто одразу для половини найскладніших мнемонік. Інші мнемоніки простіші та нагадуються 8085 асемблер.
Специфікація на архітектуру опкодів
Система команд 8086 розділяється на наступні категорії (загалом 89 мнемонік + 4 сегментних префікси): 1) інструкції пересилки даних (14): MOV, PUSH, POP, XCHG, IN, OUT, XLAT, LEA, LDS, LES, LAHF, SAHF, PUSHF, POPF; 2) арифметичні інструкції (20): ADD, ADC, INC, AAA, BAA, SUB, SSB, DEC, NEG, CMP, AAS, DAS, MUL, IMUL, AAM, DIV, IDIV, AAD, CBW, CWD; 3) логічні інструкції (11): NOT, SHL, SHR, ROL, ROR, RCL, RCR, AND, TEST, OR, XOR; 4) маніпуляція з послідовностями (6): REP, MOVS, CMPS, SCAS, LODS, STOS; 5) управління потоком виконання (27): CALL, JMP, RET, INT, INT 3, INTO, IRET, JE, LJ, JLE, JB, JBE, JP, JO, JS, JNE, JNL, JNLE, JNB, JUNBE, JNP, JNO, JNS, LOOP, LOOPZ, LOOPNZ, JCXZ; 6) управління станом процесору (11): CLC, CMC, STC, CLD, STD, CLI, STI, HLT, WAIT, ESC, LOCK.
Тут ми використовуємо виключно синтаксичну специфікацію, тобто визначення на формат бінарної серіалізації певної програми певної архітектури. На курсі формальної верифікації будуть використовуватися семантичні формальні моделі для кожної інструкції які будуть у сутності формальними програмами на MLTT які відповідають псевдокоду для кожної інструкції з документації Intel SDM. В цих специфікацях детально буде показано які біти яких регістрів змінюються та що відбувається з шиною пам'яті та даних. Зараз же ми соредимося виключно на серіалазації та компактифікації дизасемблера, тобто на синтаксичній специфікації.
Легенда
Загальний формат інструкції
Таблиця мнемонік
Таблиця мнемонік буде вміщувати 136 рядків, однак ми звузимо завдання до лобораторної роботи для для першого рядка, це одразу 4 варіанта інструкції ADD.
Асемблер nasm
Ставимо популярний x86 асемблер з підтримкою усіх розширень Intel: MMX, SSE, SSE2, SSE3, SSE4.1 SSE4.2, SSE5, KNI, AVX, AVX-2, AVX-512, AES, AMX, ADX, TSX, MPX, FMA, VNNI, GFNI, SGX, CET, PKU. Ми будемо працювати в x86-64 режимі, з Mach-O вихідним форматом.
Нуль-програма.
Асемблювання, лінковка та запуск.
Таблиці
Почнемо практикум. Ми будемо робити діспатч по першому байту інструкції ігноруючи поки префікси, якшо це префікс ми будемо його просто запам`ятовувати у якості змінної в контексті. Будемо робити демонстрацію для перших чотирьох опкодів 00 01 02 03, але покажемо як заповнюється таблиця основного діспатча. Тут число 7 означає кількість послідовних комбінаторів мотузок які викликаються в процесі парса інструкції та друкують символи на термінал. Вже можна зробити покращення виділивши сімки в окрему область вірівняну по границі байту. Але тут для простоти вирівняємо і адресу мікрокоду діспатчу і кількість його елементів по 8-байтовій границі.
Кожна адреса з таблиці опкодів містить вирівняну по границі слова послідовність кожен елемент якої 16-бітне число, старший (по адресам) байт якого містить параметр, а молодший номер функції-комбінатора в таблиці функцій ropes), який друкує певну частину візуалізації інструкції (назву, операнд, дужки та навіть line feed).
0 — парсер mod_rm байт (преамбула);
1 — друк назви операції з таблиці символьних мнемонік;
2 — друк відкриття дужки та початку адресації;
3 — друк відносної адреси, якщо така використовується;
4 — друк закриття дужки;
5 — друк коми;
6 — друк регістру;
7 — друк A.
Для байткодів 02 та 03 де порядок аргументів звороній, алгоритм побудови зображення інструкції буде видозміненим: (0,1,6,5,2,3,4,7). У випадку коли mod=11 такий алгоритм повинен бути наступним: (0,1,6,5,6,7).
Таблиця функцій
Для побудови дизасемблера для mod r/m-байтових інструкцій достатньо наступного набору комбінаторів:
У якості прикладу розмістимо в пам'яті секції .data наступнs інструкції. Для простоти ми неправильно оброблямо mod r/m байт, та трактуємо поле mod без виключення коли mod=11, тобто трактуємо це як відностну трьохбайтову адресу (4 та 6 інструкції).
Допоміжні символьні таблиці
Синоніми для syscall операційної системи MacOS.
Передача параметрів
Основні вимоги до функції: мінімізація використання операцій зі стеком, використовувати передачу параметрів через регістри, мінімізація звертань до шини, бампінг короткими версіями інструкцій (бажано мінімальною довжиною 2 байти), функції-мотузочки (ropes) довжиною у розмір конвеєра.
Бібліотека функцій
Rope #0. Функція обробки байту MOD-R/M
Перед початком виконання після головного діспатчу ми попадаємо в парсер mod r/m байта, де ми завантажуємо адресу поточної інструкції в rsi (джерело для дизасемблера) та адресу таблиці опкодів в rdi (ціль дизасемблювання). У цій функції ми заглядаємо у попередній байт [rsi-1] чим вносимо цей парсер до класу L(1). Попередній байт (опкод інструкці) ми зберігаємо в регістрі BL, а байт r/m в регістрі AL. Використовуючи зсув BL на 4 ми визначаємо позицію а таблиці опкодів та використовуємо її у відносній адресації [rdi+rbx] щоб визначити адресу фукнції-мотузки у яку небхідно модифікувати параметри для розпізнання на наступних кроках. В даному випадку йдеться про дві функції мотузки from_reg і to_reg, які модифікують аргументи в таблицях inst0 та inst1 або таблицях inst2 та inst3. Ці функції записують в байт-аргумент фукнці-мотузки наступні параметри: 1) для фукнкції регістру — номер регістру з байту mod r/m у молодих 4-бітах AL та розрядність інструкції у 5-му біті AL (береться з байткоду), 2) для r/m відображення — значення mod з байту яке індексує вигляд адресації у масиві rm, 3) для функції відображення відносної адреси — кількість байт які небходно прочитати з потоку інструкцій (значення поля mod). Тому кожна з фунції 7 та 8 пише в кожні з цих трох місць, по суті готує майбутні параметри для лінійного проходження. Після виконання функція збільшує адресу поточної інструкції.
Rope #1. Функція друку операції
Rope #2. Функція друку адресації памʼяті
Rope #3. Функція друку шістнадцяткового числа
Rope #4. Функція друку дужки
Rope #5. Функція друку коми
Rope #6. Функція друку регістра
Rope #7. Функція-патч для регістрових вихідних операндів
Rope #8. Функція-патч для регістрових вхідних операндів
Головна програма
У головній програмі ми вимушені скористатися стеком для приховування регістра rdx та rdi у процесі виконання функцій мотузок. Саме в цій функції ми передаємо однобайтові паратери функціям через регістр AL, а регістр rsi в циклі завжди виставляється заново в адресу мотузок для відносної адресації по [rsi+rbx], де rbx=8*i, де i — номер функції мотузки в таблиці функцій. Після виходу з функції мотузки ми беремо то що вернулося в rax (використовуємо як вихідний результат функції), та збільшуємо на це число адресу наступної функції мотузки rdi (тобто використовуємо результат для обчислення адреси де знаходиться адреса наступної функції-мотузки). Регістр rdx використовуємо як лічильник циклу (в даному випадку ми крутимо число 7), та кожен раз декриментуємо його. Адресу display.ptr використовуємо як лінільник дизасембльованих інструкцій.
Виконання
[:0x3d2f] [d]
DYI PC XT
На курсі "КВ-123-16 Схемотехніка" вивчається як будувати XT комп`ютери та збирати їх самому, на прикладі платформи PC/104. Тут показана повна елементна база для побудови ХТ комп`ютера для максимального та мінімального режимів.
№ | Чіп | Опис |
---|---|---|
1 | 2142 | 1024 x 4-bit SRAM |
2 | 2764 | 8K EPROM |
3 | 6264 | 8K SRAM |
4 | 8086 | 16-bit bus CPU |
5 | 8087 | 16-bit bus MPU |
6 | 8088 | 8-bit bus CPU |
7 | 8089 | 16-bit bus MPU |
8 | 8155 | 256 x 8-bit SRAM, I/O, Timer |
9 | 8212 | 8-bit I/O port |
A | 8214 | PICU |
A | 8254 | PIC 8086 |
B | 8257 | DMA |
C | 8282 | 8-bit latch |
D | 8284 | Clock generator |
E | 8286 | Transeiver |
F | 8288 | Bus controller |
G | 8289 | Bus arbiter |
H | 74LS244 | Octal buffer |
I | 74LS245 | Bi-directional octal buffer |
J | 74LS373 | 8-bit latch |
Приклад компоновки процесору 8088 у мінімальному режимі
Код
Репозиторій проекту github.com/5HT/xt-dasm.
Рекомендована література
[1]. Rector, Alexy. 8086 Book. Berkley. 1980.
[2]. Brey. The Intel Microprocessors. 2009.
[3]. Mathur. 8086: Architecture, Programming and Interfacing. Delhi. 2011.
[4]. Intel Mnemonics. 1980.
Альтернативний практикум на асемблері
Розробка дизасемблера ARM або RISC-V на вибір.
Розробка LISP інтерпретатора.
Наступні кроки
Імітаційне моделювання (розробка емулятора) та математичне моделювання (доведення коректності бінарного коду мікропроцесора). Нас цікавитимуть такі моделі, які враховують мультиагентний доступ до шини та операції LOCK.