Как перейти от сборки к машинному коду (генерация кода)

16

Есть ли простой способ визуализировать шаг между сборкой кода в машинный код?

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

Но как нам перейти от сборки к бинарному, что происходит за кулисами?

user12979
источник

Ответы:

28

Посмотрите документацию по набору инструкций, и вы найдете записи, подобные этой, из микроконтроллера pic для каждой инструкции:

пример инструкции addlw

Строка «encoding» сообщает, как выглядит эта инструкция в двоичном виде. В этом случае он всегда начинается с 5 единиц, затем бит безразличия (который может быть либо единицей, либо нулем), а затем "k" обозначает добавляемый вами литерал.

Первые несколько битов называются «кодом операции», они уникальны для каждой инструкции. Процессор в основном смотрит на код операции, чтобы увидеть, что это за инструкция, затем он знает, как декодировать «k» как число, которое нужно добавить.

Это утомительно, но не так сложно кодировать и декодировать. У меня был класс старшекурсников, где мы должны были делать это вручную на экзаменах.

Чтобы на самом деле создать полный исполняемый файл, вам также необходимо выполнить такие вещи, как выделение памяти, вычисление смещений ветвей и перевод в формат, подобный 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 как (часть binutils ), так и из nasm . Затем изучите их исходный код. Кстати, я рекомендую использовать Linux для этой цели (это очень удобная для разработчиков и свободная от программного обеспечения ОС).

Объектный файл, созданный ассемблером, содержит, в частности, сегмент кода и инструкции по перемещению . Он организован в хорошо документированном формате, который зависит от операционной системы. В Linux этот формат (используемый для объектных файлов, общих библиотек, дампов ядра и исполняемых файлов) - это ELF . Этот объектный файл позже вводится в компоновщик (который в итоге создает исполняемый файл). Перемещения определяются ABI (например, x86-64 ABI ). Читайте Левина книга Linkers и погрузчики для более.

Сегмент кода в таком объектном файле содержит машинный код с отверстиями (заполняется с помощью информации о перемещении компоновщиком). (Перемещаемый) машинный код, сгенерированный ассемблером, очевидно, специфичен для архитектуры набора команд . В x86 или x86-64 (используются в большинстве ноутбуков или настольных процессоров) ИСАС является очень сложным в деталях. Но для целей обучения было изобретено упрощенное подмножество, называемое y86 или y86-64. Читайте слайды на них. Другие ответы на этот вопрос также объясняют немного этого. Вы можете прочитать хорошую книгу по компьютерной архитектуре .

Большинство ассемблеров работают в два прохода , второй производит перемещение или корректирует некоторые выходные данные первого прохода. Теперь они используют обычные методы разбора (так что, возможно, прочитайте «Книгу дракона» ).

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

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

Василий Старынкевич
источник
1
Что касается форматов объектных файлов, я бы порекомендовал взглянуть на формат RDOFF, созданный NASM. Это было сделано специально, чтобы быть настолько простым, насколько это реально возможно, и все же работать в самых разных ситуациях. Источник NASM включает в себя компоновщик и загрузчик для формата. (Полное раскрытие - я разработал и написал все это)
Жюль