Первое, что вам нужно, это что-то вроде этого файла . Это база данных команд для процессоров 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
. Отправьте его в модуль вывода вместе с примечанием о том, что ни один из байтов не является ссылками на память (модуль вывода может знать, если они это делают).
Повторите для каждой инструкции. Следите за метками, чтобы вы знали, что вставлять, когда на них есть ссылки. Добавьте средства для макросов и директив, которые передаются в модули вывода ваших объектных файлов. И это в основном так, как работает ассемблер.
$ 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
... да, ты совершенно прав. :)На практике ассемблер обычно не создает непосредственно некоторый двоичный исполняемый файл , но некоторые объектные файлы (которые будут переданы позднее компоновщику ). Однако есть исключения (вы можете использовать некоторые ассемблеры для непосредственного создания некоторого двоичного исполняемого файла; они редки).
Во-первых, обратите внимание, что многие ассемблеры сегодня являются бесплатными программами. Поэтому скачайте и скомпилируйте на свой компьютер исходный код 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. Ваш вопрос настолько широк, что вам нужно прочитать о нем несколько книг. Я дал некоторые (очень неполные) ссылки. Вы должны найти больше из них.
источник