Skip to content

Из ассемблера в байт код

VBrazhnik edited this page Oct 10, 2019 · 3 revisions

Структура файла

Чтобы разобраться каким правилам подчиняется процесс трансляции исходного кода на языке ассемблера в байт-код, нужно рассмотреть структуру файла с расширением .cor.

Для этого мы транслируем данного чемпиона с помощью предоставленной заданием программы asm:

.name       "Batman"
.comment    "This city needs me"

loop:
        sti r1, %:live, %1
live:
        live %0
        ld %0, r2
        zjmp %:loop

Полученный в результате работы файл будет иметь следующую структуру:

Структура файла с байт-кодом

Magic header

Первые 4 байта в файле занимает «магическое число».

Оно определено с помощью константы COREWAR_EXEC_MAGIC, значение которой в файле-примере op.h равно 0xea83f3.

Что такое magic header и зачем он нужен?

Magic header это такое специальное число, задача которого — сообщать, что данный файл является бинарным.

Если в файле не будет такого «сообщения», он может быть интерпретирован как текстовый.

Champion name

Следующие 128 байт в файле занимает имя чемпиона. Почему 128? Это значение константы PROG_NAME_LENGTH, которая определяет максимальную длину строки с именем.

Имя нашего чемпиона гораздо короче. Но в байт-коде оно всё равно занимает все 128 байт.

Потому, что по правилам трансляции, если длина строки с именем меньше установленного ограничения, то недостающие символы компенсируются нулевыми байтами.

В общем, каждый символ имени превращается в ASCII-код размером в 1 байт, записанный в шестнадцатеричной системе исчисления:

Символ B a t m a n
ASCII-код 0x42 0x61 0x74 0x6d 0x61 0x6e

А вместо недостающих символов мы записываем нулевые байты.

NULL

Следующие 4 байта в структуре файла отведены для некой контрольной точки — четырех нулевых октетов.

Никакой информационной нагрузки они не несут. Их задача состоит в том, чтобы просто находиться в нужном месте.

Champion exec code size

Эти 4 байта содержат довольно важную информацию — размер исполняемого кода чемпиона в байтах.

Как мы помним, виртуальная машина должна удостовериться, что размер исходного кода не превышает ограничение заданное в константе CHAMP_MAX_SIZE. В предоставленном файле op.h оно равно 682.

Champion comment

Следующие 2048 байтов заняты комментарием чемпиона. И по своей сути эта часть полностью аналогична части «Champion name».

Правда за исключением того, что теперь ограничение на максимальную длину устанавливает константа COMMENT_LENGTH.

NULL

И снова 4 нулевых октета.

Champion exec code

Последнюю часть файла занимает исполняемый код чемпиона.

В отличие от имени или комментария, нулевыми байтами он не дополняется.

Кодирование операций

Для того, чтобы разобраться как работает кодирование операций нам понадобятся две таблицы.

Таблица операций

В первую очередь нам нужна обновленная таблица операций с колонками «Код типов аргументов» и «Размер T_DIR».

Код Имя Аргумент #1 Аргумент #2 Аргумент #3 Код типов аргументов Размер T_DIR
0x01 live T_DIR Нет 4
0x02 ld T_DIR / T_IND T_REG Есть 4
0x03 st T_REG T_REG / T_IND Есть 4
0x04 add T_REG T_REG T_REG Есть 4
0x05 sub T_REG T_REG T_REG Есть 4
0x06 and T_REG / T_DIR / T_IND T_REG / T_DIR / T_IND T_REG Есть 4
0x07 or T_REG / T_DIR / T_IND T_REG / T_DIR / T_IND T_REG Есть 4
0x08 xor T_REG / T_DIR / T_IND T_REG / T_DIR / T_IND T_REG Есть 4
0x09 zjmp T_DIR Нет 2
0x0a ldi T_REG / T_DIR / T_IND T_REG / T_DIR T_REG Есть 2
0x0b sti T_REG T_REG / T_DIR / T_IND T_REG / T_DIR Есть 2
0x0c fork T_DIR Нет 2
0x0d lld T_DIR / T_IND T_REG Есть 4
0x0e lldi T_REG / T_DIR / T_IND T_REG / T_DIR T_REG Есть 2
0x0f lfork T_DIR Нет 2
0x10 aff T_REG Есть 4

Зачем нужен «Размер T_DIR» для операций, которые не принимают аргументы этого типа?

На данном этапе это действительно бесполезные сведения. Но они будут необходимы во время работы виртуальной машины.

Что и будет рассмотрено в соответствующем разделе.

Полную таблицу операций можно просмотреть на сервисе Google Sheets.

Таблица аргументов

Вторая необходимая нам таблица содержит информацию о кодах типов аргументов и их размерах.

Тип Знак Код Размер
T_REG r 01 1 байт
T_DIR % 10 Размер T_DIR
T_IND 11 2 байта

Полную таблицу аргументов можно просмотреть на сервисе Google Sheets.

О регистрах и его размерах

Важно различать две характеристики размера касающиеся регистров.

Название регистра (r1, r2...) в байт-коде занимает 1 байт. Но сам регистр вмещает в себя 4 байта, как указано в константе REG_SIZE.

О размерах аргументов типа T_DIR

Как можно было увидеть в таблице операций, размер аргументов типа T_DIR не фиксирован и зависит от операции. Но в файле op.h есть константа препроцессора, которая говорит, что размер T_DIR равен размеру регистра — 4 байтам:

# define IND_SIZE     2
# define REG_SIZE     4
# define DIR_SIZE     REG_SIZE

В связи с этим возникает вопрос «А где здесь логика?».

Логика здесь есть, но довольно призрачная.

Как мы уже говорили, для записи числа типа T_DIR в регистр и для выгрузки этого числа в память размеры регистров и аргументов типа T_DIR должны совпадать.

С этим утверждением все хорошо. Оно корректно. Если посмотреть на операции, которые загружают значение в регистр, для них размер T_DIR равен 4.

Также размер T_DIR равен 4 для операции live.

Что касается операций, для которых размер T_DIR равен 2, то в этих случаях аргумент данного типа принимает участие только в формировании адреса. И для такой задачи 4 байта являются избыточными. Ведь объем памяти составляет всего 4096 байт, как указано в константе MEM_SIZE. И любое число-адрес, превышающее это ограничение будет урезано по модулю MEM_SIZE, если это еще не было сделано с помощью IDX_MOD.

В данной ситуации аргумент типа T_DIR играет роль относительного адреса. То есть аргумента типа T_IND. И поэтому его размер равен IND_SIZE (2 байта).

Алгоритм кодирования

Каждая операция представленная в байт-коде имеет следующую структуру:

  1. Код операции — 1 байт
  2. Код типов аргументов (Нужен не для всех операций) — 1 байт
  3. Аргументы

Код типов аргументов

Как было указано выше в структуре закодированной операции может отсутствовать второй компонент — код типов аргументов.

Его наличие зависит от конкретной операции. Если она принимает всего один аргумент и его тип однозначно определен как T_DIR, то код с информацией о типах аргументов не нужен. Для всех остальных операций этот компонент обязателен.

Проверить нужен ли этот код для конкретной операции можно в столбце «Код типов аргументов» таблицы операций.

Давайте рассмотрим как мы кодируем инструкции исполняемого кода:

loop:
        sti r1, %:live, %1
live:
        live %0
        ld %0, r2
        zjmp %:loop

Инструкция #1

Первая инструкция, которую необходимо транслировать это:

loop:
        sti r1, %:live, %1

Для начала установим размеры каждого компонента.

Код операции Код типов аргументов Аргумент #1 Аргумент #2 Аргумент #3
1 байт 1 байт 1 байт 2 байта 2 байта

Теперь давайте рассмотрим, как мы узнаём байт-код каждой части инструкции.

Код операции

Код каждой операции указан в таблице операций. Для sti он равен 0x0b.

Код типов аргументов

Для того, чтобы сформировать этот код, нужно представить 1 байт в двоичной системе исчисления. Первые два бита слева займет код типа аргумента #1. Следующие два отойдут коду типа второго аргумента. И так далее. Последняя четвертая пара всегда будет равна 00.

Коды для каждого типа указаны в таблице аргументов.

Аргумент #1 Аргумент #2 Аргумент #3 Итоговый код
T_REG T_DIR T_DIR
01 10 10 00 0x68

Аргумент типа T_REG

В этом случае в шестнадцатеричный код переводится номер регистра. Для регистра r1 это 0x01.

Аргумент-метка типа T_DIR

Как нам уже известно метка должна превратится в число, которое содержит относительный адрес в байтах.

Поскольку метка live указывает на следующую инструкцию, а размер в байтах текущей инструкции нам уже известен, можно легко посчитать необходимое расстояние — 7 байтов.

Полученное число-адрес должно будет разместиться на 2 байтах — 0x0007.

Аргумент-число типа T_DIR

В этом случае все еще проще. Нужно просто записать число, полученное в десятичной системе исчисления, в виде шестнадцатеричного байт-кода — 0x0001.

Итоговый байт-код инструкции — 0b 68 01 0007 0001.

Инструкция #2

Для следующей инструкции все аналогично:

live:
        live %0

Единственное существенное изменение состоит в том, что для данной операции не нужен код типов аргументов:

Код операции Аргумент #1
1 байт 4 байта

Итоговый байт-код для второй инструкции — 01 00000000.

Инструкция #3

Третья инструкция:

        ld %0, r2
Код операции Код типов аргументов Аргумент #1 Аргумент #2
1 байт 1 байт 4 байта 1 байт

Здесь также нет никаких сюрпризов — 02 90 00000000 02.

Инструкция #4

        zjmp %:loop

Код операции zjmp равен 0x09.

Код типов аргументов для этой операции не нужен.

Код операции Аргумент #1
1 байт 2 байта

Метка loop указывает на 19 байтов назад. Вот только как нам представить в шестнадцатеричной системе число -19?

Для этого нужно записать число 19 в двоичной системе исчисления в прямом коде:

0000 0000 0001 0011

Из числа 19, записанного в прямом коде, мы сможем получить число -19 в дополнительном коде.

Именно в дополнительном коде принято представлять целые отрицательные числа в компьютерах.

Для того, чтобы получить дополнительный код числа -19, мы должны выполнить следующие действия:

  1. Инвертировать все разряды

То есть изменить единицу на ноль, и наоборот:

1111 1111 1110 1100
  1. Добавить единицу к числу
1111 1111 1110 1101

Готово. Вот мы и получили число -19 в дополнительном коде.

Переведем его из двоичной в шестнадцатеричную систему — 0xffed.

Итоговый байт-код для инструкции #5 — 09 ffed.

Итог

Весь исполняемый код рассматриваемого чемпиона будет выглядеть так:

0b68 0100 0700 0101 0000 0000 0290 0000
0000 0209 ffed