Ассемблер
Язык ассемблера подчиняется правилу «В одной строке — одна инструкция».
Инструкция или оператор (англ. statement) — наименьшая автономная часть языка программирования; команда или набор команд. Программа обычно представляет собой последовательность инструкций.
Источник: Оператор (программирование) — Википедия
Пустые строки, комментарии, как и лишние tab'ы или пробелы, игнорируются.
В header'е op.h
есть константа COMMENT_CHAR
. Она определяет какой символ, обозначает начало комментария.
В предоставленном файле это октоторп — #
.
То есть в качестве комментария будет восприниматься всё между символом #
и концом строки.
Комментарий может располагаться в любой части файла.
Пример #1:
# UNIT Factory
# is a programming school
Пример #2:
ld %0, r2 # And it is located in UNIT City
В предоставленном архиве vm_champs.tar
по пути champs/examples
можно найти файл bee_gees.s
с кодом чемпиона, который оригинальная программа asm
транслирует в байт-код без ошибок.
В коде этого чемпиона встречаются два вида комментариев:
- стандартный, который был рассмотрен выше;
- альтернативный, о котором нет информации в subject'e.
Этот альтернативный вид от стандартного и описанного в subject'е отличается лишь символом начала комментария. Вместо октоторпа (#
) здесь выступает ;
.
Пример использования комментария данного вида:
sti r1, %:live, %1 ; UNIT City is placed in Kyiv, Ukraine
Этот вид комментария не описывается в subject'е, но поддерживается оригинальным транслятором. Поэтому мы, скорее всего, не обязаны его обрабатывать.
Но все же добавим его поддержку в наш проект. Для этого в заголовочный файл op.h
будет внесена следующая строчка:
# define ALT_COMMENT_CHAR ';'
Это будет вторым (первое — приведение к Norm'е) и последним изменением, которое мы внесем в файл
op.h
.
В файле с кодом чемпиона должно быть определено его имя. Для этого в ассемблере существует команда, имя которой определено в константе NAME_CMD_STRING
. В предоставленном файле op.h
это .name
.
То есть после команды .name
должна следовать строка с именем нашего чемпиона:
.name "Batman"
Длина строки не должна превышать число заданное в константе PROG_NAME_LENGTH
. В предоставленном файле оно равно 128
.
Кстати, в качестве имени чемпиона также может выступать пустая строка:
.name ""
Но полное отсутствие строки это уже ошибка:
.name
Также в файле с расширением .s
должен быть определен комментарий чемпиона.
Команда, которая поможет это сделать, содержится в константе COMMENT_CMD_STRING
файла op.h
. В предоставленном файле это .comment
.
Длина строки-комментария ограничена константой COMMENT_LENGTH
. В предоставленном файле op.h
её значение равно 2048
.
По своей сути команда .comment
очень похожа на .name
и ведет себя аналогичным образом в случаях с пустой строкой и в случаях с её полным отсутствием.
В некоторых файлах с расширением .s
, которые были предоставлены нам в качестве примера, встречалась такая команда как .extend
.
Эта команда, как и любые другие отличные от .name
и .comment
, не описана в subject'e и оригинальным транслятором определяется как ошибочная.
Таким же образом с подобными командами будем обращаться и мы.
Исполняемый код чемпиона состоит из инструкций.
Для языка ассемблера действует правило «Одна строка — одна инструкция». И символом окончания инструкции в этом языке является перевод строки. То есть вместо привычного в языке С символа ;
здесь выступает символ \n
.
Опираясь на это правило мы должны помнить, что даже после последней инструкции должен следовать перевод строки. Иначе, asm
выведет сообщение об ошибке.
Каждая инструкция состоит из нескольких компонентов:
Метка состоит из символов, которые были определены в константе LABEL_CHARS
. В файле-примере это abcdefghijklmnopqrstuvwxyz_0123456789
.
То есть в составе метки не могут присутствовать символы, которые не указаны в LABEL_CHARS
.
А за самой меткой должен следовать символ определенный в константе LABEL_CHAR
. В файле-примере это символ :
.
Метка указывает на операцию, которая следует прямо за ней. Именно на одну операцию, а не на их блок.
.name "Batman"
.comment "This city needs me"
loop:
sti r1, %:live, %1 # <-- На эту операцию указывает метка loop
live:
live %0 # <-- На эту операцию указывает метка live
ld %0, r2 # <-- А на эту операцию никакая метка не указывает
zjmp %:loop
Задача меток состоит в том, чтобы упростить нашу жизнь, а точнее процесс написания кода.
Чтобы в полной мере осознать их роль, давайте представим мир без меток.
Как мы знаем, написанный на языке ассемблера код чемпиона после работы программы-транслятора превратится в множество байтов, представленных в шестнадцатеричной системе исчисления. И именно такой байт-код будет выполнять виртуальная машина.
Допустим, нам нужно организовать цикл, в котором бы раз за разом выполнялась операция live
. Для этого у нас есть операция zjmp
, которая может перебросить нас на N-ое количество байтов вперед или назад.
В рассматриваемом случае нам нужно после каждой итерации цикла снова возвращаться на операцию live
. Вот только сколько это байтов назад? Чтобы это выяснить нужно узнать сколько байтов в байт-коде займет код операции и её аргумент.
Как мы узнаем позже, код операции live
занимает 1 байт, а её единственному аргументу необходимо 4 байта.
Получается нам нужно вернуться на 5 байтов назад:
live %1
zjmp %-5
Не так уж сложно, но все же подобные расчеты занимают время. И гораздо проще было бы просто написать «перейти на операцию live
». Для этого и существуют метки.
Мы просто создаем метку на нужную нам операцию live
и передаем её операции zjmp
:
loop: live %1
zjmp %:loop
С точки зрения трансляции кода на ассемблере в байт-код оба примера абсолютно идентичны. Во время работы программа asm
высчитает на сколько байтов назад указывает метка loop
и подменит её на число -5
.
Так, что для итогового результата совершенно не важно, что было использовано. Вот только писать код с использованием меток гораздо удобнее.
Для записи метки существует несколько подходов:
marker:
live %0
marker:
live %0
marker: live %0
Все приведенные выше примеры для программы-транслятора означают одно и тоже.
Поэтому можно выбрать любой из представленных вариантов.
Также возможна такая форма записи:
marker:
label:
live %0
Это означает, что и метка marker
, и метка label
указывают на одну и ту же операцию.
А может сложиться ситуация, когда у метки нет операции, на которую она могла бы указывать:
marker:
# Конец файла
В таком случае метка указывает на место, сразу за исполняемым кодом чемпиона.
Главное, чтобы в конце линии, на которой она расположена, находился \n
. Иначе транслятор сообщит об ошибке.
Язык ассемблера имеет определенный набор из 16 операций. Каждая из которых принимает от одного до трех аргументов.
Информация об имени операции, её коде, а также об аргументах, которые она принимает, приведена в предоставленном заданием файле op.c
.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
1 | live |
T_DIR |
— | — |
2 | ld |
T_DIR / T_IND
|
T_REG |
— |
3 | st |
T_REG |
T_REG / T_IND
|
— |
4 | add |
T_REG |
T_REG |
T_REG |
5 | sub |
T_REG |
T_REG |
T_REG |
6 | and |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
7 | or |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
8 | xor |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
9 | zjmp |
T_DIR |
— | — |
10 | ldi |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
T_REG |
11 | sti |
T_REG |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
12 | fork |
T_DIR |
— | — |
13 | lld |
T_DIR / T_IND
|
T_REG |
— |
14 | lldi |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
T_REG |
15 | lfork |
T_DIR |
— | — |
16 | aff |
T_REG |
— | — |
Разобраться какова роль каждой операции и как она интерпретирует аргументы разных типов это самая важная задача для понимания основ проекта «Corewar».
Каждый аргумент соответствует одному из трех типов:
Регистр это такая переменная, где мы можем хранить какие-либо данные. Размер этой переменной в октетах обозначен в константе REG_SIZE
, которая в файле-примере op.h
инициализирована значением 4
.
Октет в информатике — восемь двоичных разрядов. В русском языке октет обычно называют байтом.
Источник: Октет (информатика) — Википедия
Количество регистров ограничено числом указанным в константе REG_NUMBER
. В примере — 16
.
То есть доступные нам регистры это r1
, r2
, r3
... r16
.
Значения регистров
Во время запуска виртуальной машины все регистры, кроме
r1
, будут инициализированы нулями.В
r1
будет записан номер игрока-чемпиона. Только со знаком минус.Данный номер уникален в рамках игры и нужен операции
live
, чтобы сообщить, что конкретный игрок жив.То есть каретка, которая будет размещена на начале кода игрока под номером
2
, получит значениеr1
равным-2
.В случае, если операция
live
будет выполнена с аргументом-2
, виртуальная машина засчитает, что данный игрок жив:live %-2
Прямой аргумент состоит из двух частей: символ, который задан в константе DIRECT_CHAR
(%
) + число или метка, которые представляют прямое значение.
В случае, если речь идет о метке, то перед её именем также должен быть указан символ из переменной LABEL_CHAR
(:
):
sti r1, %:marker, %1
Что такое прямое и непрямое значение?
Чтобы осознать разницу между прямым и непрямым значением, стоит рассмотреть один очень простой пример.
Представим, что у нас есть число
5
. В своем прямом значении оно представляет самого себя. То есть число5
это число5
.Но в непрямом значении это уже не число, а относительный адрес, который указывает на 5 байтов вперед.
Метка в прямом и непрямом значении
Если с числами в прямом и непрямом значении все понятно, то как быть с метками? В чем тут разница?
Все довольно просто. Как мы знаем, данные операции будет выполнять виртуальная машина. И именно для нее важно какой аргумент был получен. Прямой или непрямой?
Вот только до виртуальной машины метки попросту не дойдут. На этапе трансляции в байт-код они все будут заменены на свои числовые эквиваленты.
Поэтому метки это те же числа. Только записанные в другой форме
Процесс замены меток на числа описан в главе «Зачем нужны метки?».
Аргументом данного типа может быть либо число, либо метка, которые представляют непрямое значение.
Если в качестве аргумента типа T_IND
выступает число, то никаких дополнительных символов не нужно:
ld 5, r7
Если таким аргументом является метка, то перед её именем должен быть указан символ из переменной LABEL_CHAR
(:
):
ld :label, r7
Для того, чтобы отделить один аргумент от другого в рамках одной операции, в ассемблере используется специальный символ-разделитель. Он определяется константой препроцессора SEPARATOR_CHAR
и в файле-примере op.h
это символ ,
:
ld 21, r7
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
1 | live |
T_DIR |
— | — |
Описание
На операцию live
возложены две функции:
-
Она засчитывает, что каретка, которая выполняет операцию
live
, жива. -
Если указанный в качестве аргумента операции
live
номер совпадает с номером игрока, то она засчитывает, что это игрок жив. Например, если значение аргумента равно-2
, значит игрок с номером2
жив.
Что такое каретка?
Подробное объяснение этого термина будет дано в разделе «Виртуальная машина», поскольку каретка относится именно к этой части проекта «Corewar».
Но поскольку базовые знания об этом термине являются необходимым условием для понимания работы операций, коротко рассмотрим, что же это такое.
Каретка это процесс, выполняющий операцию, на которой стоит.
Допустим мы запускаем виртуальную машину с тремя игроками-чемпионами, которым предстоит сражаться за победу.
Значит, в памяти виртуальной машины будут размещены исполняемые коды чемпионов. И на начало каждого из участков памяти будет помещена каретка.
3 чемпиона. 3 участка памяти с размещенными на них исполняемыми кодами. 3 каретки.
Каждая каретка содержит в себе несколько важных элементов:
PC
(Program Counter)Переменная, которая содержит позицию каретки.
- Регистры
Те самые регистры, количество которых определяет константа
REG_NUMBER
.
- Флаг
carry
Специальная переменная, которая влияет на работу функции
zjmp
и может принимать одно из двух значений:1
или0
(true
илиfalse
).
- Номер цикла, в котором данной кареткой в последний раз выполнялась операция
live
Эта информация нужна, чтобы определить жива ли каретка.
На самом деле каретка содержит в себе гораздо больше элементов, но они будут рассмотрены позже.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
2 | ld |
T_DIR / T_IND
|
T_REG |
— |
Описание
Задача этой операции состоит в загрузке значения в регистр. Но её поведение отличается в зависимости от типа первого аргумента:
- Аргумент #1 —
T_DIR
Если тип первого аргумента это T_DIR
, то число переданное в качестве аргумента будет воспринято «как есть».
Задачи операции:
-
Записать полученное число в регистр, который был передан в качестве второго аргумента.
-
Если в регистр записали число
0
, то установить значениеcarry
в1
. Если было записано не нулевое значение, то установитьcarry
в0
.
- Аргумент #1 —
T_IND
Если тип первого аргумента это T_IND
, то в данном случае число представляет собой адрес.
В случае получения аргумента такого типа он обрезается по модулю — <ПЕРВЫЙ_АРГУМЕНТ> % IDX_MOD
.
Что такое
IDX_MOD
?
IDX_MOD
это еще одна константа из файлаop.h
. Её значение определяется с помощью выражения(MEM_SIZE / 8)
, гдеMEM_SIZE
определяет объем памяти в байтах. В виртуальной машине на участке памяти размером сMEM_SIZE
и будут биться чемпионы.Так для чего же нужна константа
IDX_MOD
? Она нужна для того, чтобы каретка не могла прыгать в памяти на слишком большие расстояния. В файле-примере константаMEM_SIZE
была инициализирована значением(4 * 1024)
. Поэтому значениеIDX_MOD
в этом случае соответствует512
.Получается, что каретка не сможет за один прыжок переместиться дальше, чем на 512 байт.
После того, как аргумент типа T_IND
был усечен по модулю, полученное значение используется как относительный адрес — на сколько байтов вперед или назад относительно текущего местоположения каретки находится нужная нам позиция.
Задача операции ld
:
-
Определить адрес — текущая позиция +
<ПЕРВЫЙ_АРГУМЕНТ> % IDX_MOD
. -
С полученного адреса необходимо считать 4 байта.
-
Записать считанное число в регистр, который был передан в качестве второго параметра.
-
Если в регистр записали число
0
, то устанавливаем значениеcarry
в1
. Если было записано не нулевое значение, то устанавливаемcarry
в0
.
Почему мы считываем именно 4 байта?
Как мы знаем размер каждого регистра составляет 4 байта. А точнее то количество байт, которое определено в файле
op.h
константойREG_SIZE
.В том же файле определен размер аргумента типа
T_DIR
:# define REG_SIZE 4 # define DIR_SIZE REG_SIZE
И он тоже составляет 4 байта.
Мы переходим по адресу, который задан с помощью аргумента типа
T_IND
, чтобы считать значение. Считать число «как оно есть». То есть получить число типаT_DIR
. И это самое число мы должны записать в регистр. Для того, чтобы запись прошла успешно и считанное число поместилось в регистр, размеры этого числа и размеры регистра должны быть совместимы.Также во время разбора следующих операций станет известно, что мы можем не только считывать значение и записывать его в регистр, но и проводить обратное действие — выгружать значение из регистра по адресу.
Поэтому размеры числа и регистра должны быть совместимы в обоих направлениях. В этом случае единственным возможным решением является сделать количество байт, которые мы считываем (или на которое записываем) равным количеству байт, которое мы можем сохранить в регистре.
В общем, мы считываем столько байт, сколько может вместить регистр.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
3 | st |
T_REG |
T_REG / T_IND
|
— |
Описание
Эта операция записывает значение из регистра, который был передан как первый параметр. А вот куда данная операция его записывает, зависит от типа второго аргумента:
- Аргумент #2 —
T_REG
Если второй аргумент соответствует типу T_REG
, то значение записывается в регистр.
Например, в данном случае значение из регистра номер 7 записывается в регистр с номером 11:
st r7, r11
- Аргумент #2 —
T_IND
Как мы помним аргументы типа T_IND
это об относительных адресах. Поэтому в данном случае порядок работы операции st
такой:
-
Усечь значение второго аргумента по модулю
IDX_MOD
. -
Определить адрес — текущая позиция +
<ВТОРОЙ_АРГУМЕНТ> % IDX_MOD
-
Записать значение из регистра, который был передан в качестве первого аргумента, в память по полученному адресу.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
4 | add |
T_REG |
T_REG |
T_REG |
Описание
К счастью у данной операции все аргументы имеют один и тот же тип. Поэтому с ней все просто.
Задача операции add
:
-
Просуммировать значение из регистра, который был передан как первый аргумент, с значением регистра, который был передан как второй аргумент.
-
Записать полученный результат в регистр, который был передан как третий аргумент.
-
Если полученная сумма, которую мы записали в третий аргумент была равна нулю, то устанавливаем
carry
в1
. А если сумма была не нулевой — в0
.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
5 | sub |
T_REG |
T_REG |
T_REG |
Описание
В данной операции также нет неоднозначности относительно аргументов.
Её задачи:
-
От значения регистра, переданного в качестве первого аргумента, отнять значение регистра, который был передан в качестве второго аргумента.
-
Полученный результат записать в регистр, который был передан в качестве третьего аргумента.
-
Если записанный результат был равен нулю, то значение
carry
сделать равным1
. Если результат был не нулевой, то сделать равным0
.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
6 | and |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
Описание
and
выполняет операцию «побитовое И» для значений первых двух аргументов и записывает полученный результат в регистр, переданный в качестве третьего аргумента.
Если записанный результат был равен нулю, то значение carry
нужно установить равным 1
. Если результат был не нулевой, то — равным 0
.
Поскольку первый и второй аргумент могут быть одного из трех типов, мы рассмотрим как получить значение каждого из них:
- Аргумент #1 / Аргумент #2 —
T_REG
В данном случае значение берется из регистра, переданного в качестве аргумента.
- Аргумент #1 / Аргумент #2 —
T_DIR
В этом случае используется переданное в качестве аргумента числовое значение.
- Аргумент #1 / Аргумент #2 —
T_IND
Если тип аргумента T_IND
, то необходимо установить адрес, с которого будет считано 4 байта.
Адрес определяется следующим образом — текущая позиция + <АРГУМЕНТ> % IDX_MOD
.
Считанное по этому адресу 4-байтовое число и будет требуемым значением.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
7 | or |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
Описание
По своей сути эта операция полностью аналогична операции and
. Только в данном случае «побитовое И» заменяется на «побитовое ИЛИ».
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
8 | xor |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
Описание
По своей сути эта операция полностью аналогична операции and
. Только в данном случае «побитовое И» заменяется на «побитовое исключающее ИЛИ».
Как работает «побитовое исключающее ИЛИ» (XOR) ?
A B A ^ B 0 0 0 0 1 1 1 0 1 1 1 0
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
9 | zjmp |
T_DIR |
— | — |
Описание
Эта та самая функция, на работу которой влияет значение флага carry
.
Если оно равно 1
, то функция обновляет значение PC
на адрес — текущая позиция + <ПЕРВЫЙ_АРГУМЕНТ> % IDX_MOD
.
То есть zjmp
устанавливает куда должна переместиться каретка для выполнения следующей операции. Это позволяет нам перепрыгивать в памяти на нужную позицию, а не выполнять всё по порядку.
Если значение carry
равно нулю, перемещение не выполняется.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
10 | ldi |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
T_REG |
Описание
Данная операция записывает значение в регистр, который был ей передан как третий параметр. Значением, которая она записывает, являются 4 байта. Эти 4 байта она считывает по адресу, который формируется по следующему принципу: текущая позиция + (<ЗНАЧЕНИЕ_ПЕРВОГО_АРГУМЕНТА> + <ЗНАЧЕНИЕ_ВТОРОГО_АРГУМЕНТА>) % IDX_MOD
.
Поскольку операция может принимать разные типы первого и второго аргументов, рассмотрим способ получения значения для каждого типа:
- Аргумент #1 / Аргумент #2 —
T_REG
Значение содержится в регистре, который был передан в качестве параметра.
- Аргумент #1 / Аргумент #2 —
T_DIR
В данном случае у нас аргумент уже содержит свое значение.
- Аргумент #1 —
T_IND
Чтобы получить значение этого аргумента, нужно считать 4 байта по адресу — текущая позиция + <ПЕРВЫЙ_АРГУМЕНТ> % IDX_MOD
.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
11 | sti |
T_REG |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
Описание
Эта операция записывает значение регистра, переданного в качестве первого параметра, по адресу — текущая позиция + (<ЗНАЧЕНИЕ_ВТОРОГО_АРГУМЕНТА> + <ЗНАЧЕНИЕ_ТРЕТЕГО_АРГУМЕНТА>) % IDX_MOD
.
Как получить значение для каждого типа аргумента описано выше.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
12 | fork |
T_DIR |
— | — |
Описание
Операция fork
делает копию каретки. И эту копию размещает по адресу <ПЕРВЫЙ_АРГУМЕНТ> % IDX_MOD
.
Какие данные копируются?
Значения всех регистров
Значение
carry
Номер цикла, в котором в последний раз выполнялась операция
live
И кое-что еще, но об этом позже.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
13 | lld |
T_DIR / T_IND
|
T_REG |
— |
Описание
Данная операция ведет себя практически так же как и операция ld
. То есть записывает значение, полученное из первого аргумента, в регистр, переданный как второй аргумент.
Единственное различие между этими двумя операциями состоит в применении усечения по модулю.
Если первый аргумент имеет тип T_IND
, то в этой операции мы будем считывать 4 байта значения по адресу — текущая позиция + <ПЕРВЫЙ_АРГУМЕНТ>
. Не применяя усечение по модулю.
Проблемы оригинальной виртуальной машины
Оригинальная виртуальная машина
corewar
, к сожалению, работает неправильно. И считывает 2 байта, а не 4. Возможно подобный баг объясняется теми же строками, что и проблемы в предоставленных файлах:... we might have mistaken a bottle of water for a bottle of vodka.
Для аргумента типа T_DIR
ничего не меняется.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
14 | lldi |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
T_REG |
Описание
По своей сути данная операция аналогична операции ldi
.
Она записывает значение в регистр, который был передан ей как третий параметр. Значением, которая эта операция записывает, являются считанные 4 байта.
Они считываются по адресу, который формируется по следующему принципу: текущая позиция + (<ЗНАЧЕНИЕ_ПЕРВОГО_АРГУМЕНТА> + <ЗНАЧЕНИЕ_ВТОРОГО_АРГУМЕНТА>)
.
В отличие от операции ldi
в этом случае при формировании адреса не нужно делать усечение по модулю IDX_MOD
.
Для аргументов типа
T_IND
все остается по старомуЕсли в качестве первого или второго аргумента мы получаем аргумент типа
T_IND
, то все так же считываем 4 байта значения по адресу — текущая позиция +<АРГУМЕНТ> % IDX_MOD
. Здесь усечение по модулю сохраняется.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
15 | lfork |
T_DIR |
— | — |
Описание
По своей сути эта операция аналогична операции fork
.
За исключением того факта, что новая каретка в этом случае создается по адресу — текущая позиция + <ПЕРВЫЙ_АРГУМЕНТ>
. В операции lfork
усечение по модулю делать не нужно.
Код операции | Имя операции | Аргумент #1 | Аргумент #2 | Аргумент #3 |
---|---|---|---|---|
16 | aff |
T_REG |
— | — |
Описание
Эта операция берет значение из регистра, который был передан в качестве единственного аргумента. Приводит его к типу char
. И выводит на экран как ASCII-символ.
В тексте задания работа операции aff описывается следующими строками:
aff: The opcode is
10
in the hexadecimal. There is an argument’s coding byte, even if it’s a bit silly because there is only 1 argument that is a registry, which is a registry, and its content is interpreted by the character’s ASCII value to display on the standard output. The code is modulo256
.Но на самом деле выполнять дополнительное усечение по модулю
256
нет необходимости.Ведь результат работы этих двух расчетов будет идентичен:
(char)(value % 256)
(char)(value)
Режим отображения вывода
aff
в оригинальномcorewar
В оригинальной виртуальной машине
corewar
по умолчанию режим отображения вывода операцииaff
выключен. Чтобы его включить нужно воспользоваться флагом-a
.
Код | Имя | Аргумент #1 | Аргумент #2 | Аргумент #3 | Изменяет carry
|
Описание |
---|---|---|---|---|---|---|
1 | live |
T_DIR |
— | — | Нет | alive |
2 | ld |
T_DIR / T_IND
|
T_REG |
— | Да | load |
3 | st |
T_REG |
T_REG / T_IND
|
— | Нет | store |
4 | add |
T_REG |
T_REG |
T_REG |
Да | addition |
5 | sub |
T_REG |
T_REG |
T_REG |
Да | subtraction |
6 | and |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
Да | bitwise AND (& ) |
7 | or |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
Да | bitwise OR (| ) |
8 | xor |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR / T_IND
|
T_REG |
Да | bitwise XOR (^ ) |
9 | zjmp |
T_DIR |
— | — | Нет | jump if non-zero |
10 | ldi |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
T_REG |
Нет | load index |
11 | sti |
T_REG |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
Нет | store index |
12 | fork |
T_DIR |
— | — | Нет | fork |
13 | lld |
T_DIR / T_IND
|
T_REG |
— | Да | long load |
14 | lldi |
T_REG / T_DIR / T_IND
|
T_REG / T_DIR
|
T_REG |
Да | long load index |
15 | lfork |
T_DIR |
— | — | Нет | long fork |
16 | aff |
T_REG |
— | — | Нет | aff |
Но это еще не всё, что необходимо знать об операциях.
Есть еще один важный параметр — циклы до исполнения.
Это количество циклов, которое каретка должна подождать перед тем, как начать выполнять операцию.
Примером, оказавшись на операции fork
следующие 800 циклов она проведет в ожидании. А для операции ld
ожидание составит всего 5 циклов.
Данный параметр был введен для создания игровой механики, в которой самые эффективные и полезные функции имеют наибольшую стоимость.
Код | Имя | Циклы до исполнения |
---|---|---|
1 | live |
10 |
2 | ld |
5 |
3 | st |
5 |
4 | add |
10 |
5 | sub |
10 |
6 | and |
6 |
7 | or |
6 |
8 | xor |
6 |
9 | zjmp |
20 |
10 | ldi |
25 |
11 | sti |
25 |
12 | fork |
800 |
13 | lld |
10 |
14 | lldi |
50 |
15 | lfork |
1000 |
16 | aff |
2 |
Полную таблицу операций можно просмотреть на сервисе Google Sheets.