Як ми переходимо від складання до машинного коду (генерація коду)


16

Чи є простий спосіб візуалізувати крок між збіркою коду до машинного коду?

Наприклад, якщо у блокноті ви відкриваєте про двійковий файл, ви бачите текстово відформатоване зображення машинного коду. Я припускаю, що кожен байт (символ), який ви бачите, є відповідним символом ascii для його бінарного значення?

Але як ми переходимо від складання до бінарного, що відбувається за лаштунками ??

Відповіді:


28

Перегляньте документацію щодо набору інструкцій, і ви знайдете записи, подібні до цієї, з мікроконтролера pic для кожної інструкції:

Приклад інструкції addlw

Рядок "кодування" говорить про те, як виглядає ця інструкція у двійковій формі. У цьому випадку вона завжди починається з 5 одиниць, потім біт небайдужих (який може бути або один, або нуль), а потім "k" стенду для того, який ви додаєте.

Перші кілька бітів називаються "опкодом", є унікальними для кожної інструкції. Процесор в основному дивиться на опкод, щоб побачити, що це інструкція, тоді він знає розшифрувати "k" s як число, яке потрібно додати.

Це нудно, але не так складно кодувати і декодувати. У мене був клас нижчого класу, де нам довелося це робити вручну на іспитах.

Щоб зробити фактично повний виконуваний файл, вам також потрібно виконати такі дії, як розподілити пам'ять, обчислити зміщення гілки та ввести її у такий формат, як ELF , залежно від вашої операційної системи.


10

Опкоди монтажу здебільшого мають відповідність один на один основним машинним інструкціям. Отже, все, що вам потрібно зробити, - це визначити кожен код коду мовою складання, зіставити його у відповідній машинній інструкції та записати машинну інструкцію у файл разом із відповідними параметрами (якщо такі є). Потім ви повторите процес для кожного додаткового коду у вихідному файлі.

Звичайно, для створення виконавчого файлу, який належним чином завантажується та запускається в операційній системі, потрібно більше, ніж більшість порядних асемблерів мають додаткові можливості, крім простого відображення опкодів до інструкцій на машині (наприклад, макросів, наприклад).


7

Перше, що вам потрібно - це щось на кшталт цього файлу . Це база даних інструкцій для процесорів x86, яку використовує асемблер NASM (яку я допоміг написати, хоча не частини, які фактично перекладають інструкції). Дозволяє вибрати довільну лінію з бази даних:

ADD   rm32,imm8    [mi:    hle o32 83 /0 ib,s]      386,LOCK

Це означає, що вона описує інструкцію ADD . Існує кілька варіантів цієї інструкції, і конкретний, який тут описаний, - це варіант, який приймає або 32-бітний регістр, або адресу пам'яті і додає негайне 8-бітове значення (тобто константа, безпосередньо включена в інструкцію). Приклад інструкції по збірці, яка використовувала б цю версію:

add eax, 42

Тепер вам потрібно взяти текст і проаналізувати його в окремих інструкціях та операндах. Для вищевказаної інструкції це, ймовірно, призведе до структури, яка містить інструкцію,ADD та масив операндів (посилання на регістр EAXта значення 42). Коли ви маєте цю структуру, ви переходите до бази даних інструкцій і знаходите рядок, який відповідає як імені інструкції, так і типам операндів. Якщо ви не знайдете відповідності, це помилка, яку потрібно представити користувачеві ("незаконна комбінація опкоду та операндів" або подібне - це звичайний текст).

Як тільки ми отримаємо рядок із бази даних, ми подивимось на третій стовпчик, який для цієї інструкції:

[mi:    hle o32 83 /0 ib,s] 

Це набір інструкцій, що описують, як генерувати необхідну інструкцію машинного коду:

  • Це miопис операндів: один modr/mоперанд (регістр або пам'ять) (це означає, що нам потрібно буде додатиmodr/m байт до кінця інструкції, про який ми підемо пізніше) і один негайний інструктаж (який буде використовуватись в описі інструкції).
  • Далі є hle . Це визначає, як ми обробляємо префікс "замок". Ми не використовували "замок", тому ігноруємо його.
  • Далі є o32. Це говорить нам про те, що якщо ми збираємо код для 16-бітного формату виводу, інструкція потребує префікса переопределення розміру операнду. Якби ми виробляли 16-розрядний висновок, ми створили би префікс зараз (0x66 ), але я припускаю, що ми його не продовжуємо.
  • Далі є 83. Це буквальний байт у шістнадцятковій кількості. Виводимо його.
  • Далі є /0. Це вказує деякі додаткові біти, які нам знадобляться в байті modr / m, і змушує нас його генерувати. modr/mБайт використовується для регістрів кодують або посилання непрямих пам'яті. У нас є єдиний такий операнд, реєстр. Реєстр має номер, який вказаний в іншому файлі даних :

    eax     REG_EAX         reg32           0
  • Ми перевіряємо, чи відповідає reg32узгоджений розмір інструкції з вихідної бази даних (вона це робить). Номер 0реєстру. modr/mБайт являє собою структуру даних , зазначена з допомогою процесора, який виглядає наступним чином :

     (most significant bit)
     2 bits       mod    - 00 => indirect, e.g. [eax]
                           01 => indirect plus byte offset
                           10 => indirect plus word offset
                           11 => register
     3 bits       reg    - identifies register
     3 bits       rm     - identifies second register or additional data
     (least significant bit)
  • Оскільки ми працюємо з реєстром, modполе є 0b11.

  • regПоле номер регістра , який ми використовуємо,0b000
  • Оскільки в цій інструкції є лише один регістр, нам потрібно rmщось заповнити . Ось для чого були вказані додаткові дані /0, тому ми поміщаємо їх у rmполе,0b000 .
  • Тому modr/mбайт є 0b11000000або0xC0 . Ми виводимо це.
  • Далі є ib,s. Це визначає підписаний негайний байт. Ми дивимося на операндів і зазначаємо, що у нас є негайне значення. Перетворюємо його в підписаний байт і виводимо його ( 42=> 0x2A).

Повна інструкція зібраний тому: 0x83 0xC0 0x2A. Надішліть його на свій вихідний модуль разом із зауваженням, що жоден з байтів не містить посилань на пам'ять (модуль виведення, можливо, повинен знати, чи є)

Повторіть кожну інструкцію. Слідкуйте за мітками, щоб ви знали, що потрібно вставити, коли на них посилаються. Додайте засоби для макросів та директив, які передаються до модулів виведення об'єктного файлу. І це в основному, як працює асемблер.


1
Дякую. Чудове пояснення, але чи не слід це "0x83 0xC0 0x2A", а не "0x83 0xB0 0x2A", тому що 0b11000000 = 0xC0
Камран

@Kamran - $ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003... так, ти абсолютно прав. :)
Жуль

2

На практиці асемблер зазвичай не виробляє безпосередньо деякі двійковий виконуваний файл , але деякі об'єктний файл (який буде подаватися пізніше в линкер ). Однак є винятки (ви можете використовувати деякі асемблери, щоб безпосередньо створити якийсь двійковий виконуваний файл; вони рідкі).

По-перше, зауважте, що сьогодні багато асемблерів є безкоштовними програмами. Тож завантажте та складіть на свій комп'ютер вихідний код GNU як (частина бінутів ) та nasm . Потім вивчіть їх вихідний код. BTW, я рекомендую використовувати для цієї мети Linux (це дуже зручна для розробників та вільна до програмного забезпечення ОС).

Об'єктний файл, створений асемблером, містить, зокрема, сегмент коду та інструкції щодо переміщення . Він організований у добре задокументованому файловому форматі, який залежить від операційної системи. У Linux цей формат (використовується для об'єктних файлів, спільних бібліотек, основних дампів та виконуваних файлів) є ELF . Пізніше цей об’єктний файл вводиться в лінкер (який, нарешті, створює виконуваний файл). Переміщення визначаються ABI (наприклад, x86-64 ABI ). Докладніше читайте книгу Левенів "Лінери та навантажувачі" .

Сегмент коду в такому об’єктному файлі містить машинний код з отворами (заповнюється, за допомогою інформації про переселення, лінкером). (Переміщається) машинний код, згенерований асемблером, очевидно, специфічний для архітектури набору інструкцій . У x86 або x86-64 (використовуються в більшості ноутбуків або настільних процесорів) ІСАС є дуже складним в деталях. Але для навчальних цілей було винайдено спрощений підмножина, зване y86 або y86-64. Прочитайте слайди на них. Інші відповіді на це питання також трохи пояснюють це. Можливо, ви хочете прочитати хорошу книгу про архітектуру комп’ютерів .

Більшість асемблерів працюють у два проходи , другий - випромінюючи переміщення або коригуючи частину виходу першого проходу. Зараз вони використовують звичні методи розбору (тому читайте, можливо, Книгу Драконів ).

Як виконується виконуваним файлом ядром ОС (наприклад, як працює execveсистемний виклик в Linux) - це інше (і складне) питання. Зазвичай він встановлює деякий віртуальний адресний простірпроцесі виконання цього execve (2) ...), а потім повторно ініціалізує внутрішній стан процесу (включаючи регістри в режимі користувача). Динамічний компонувальник -such , як ld-linux.so (8) на Linux так може бути залучений під час виконання. Прочитайте хорошу книгу, наприклад, Операційна система: Три легких п’єси . OSDEV вікі також дає корисну інформацію.

PS. Ваше запитання настільки широке, що вам потрібно прочитати кілька книг про нього. Я дав кілька (дуже неповних) посилань. Вам слід знайти їх більше.


1
Що стосується форматів файлів об'єктів, для початківців я рекомендую переглянути формат RDOFF, створений NASM. Це було навмисно розроблено таким чином, щоб бути максимально простим і реально можливим і все ще працювати в різних ситуаціях. Джерело NASM включає в себе лінкер і завантажувач формату. (Повне розкриття інформації - я все це розробив і написав)
Жюль,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.