Да-да, именно «байт» и именно по индейски (не по индийски). Начну по порядку. В последнее время тут, на Хабре, стали появляться статьи о байт-коде. А когда-то давным-давно я развлекался тем, что писал форт-системы. Конечно, на ассемблере. Они были 16-ти разрядными. На x86-64 никогда не программировал. Даже с 32 поиграться не удалось. Вот и пришла такая мысль — а почему бы нет? Почему бы не замутить 64х разрядный форт, да ещё с байт-кодом? Да еще и на Linux, где я тоже ничего системного не писал.
У меня есть домашний сервер с Linux. В общем, я немного погуглил и узнал, что ассемблер на Linux называется GAS, а команда as. Подключаюсь по SSH к серверу, набираю as — есть! Он у меня уже установлен. Ещё нужен компоновщик, набираю ld — есть! Вот так, и попробуем написать что-нибудь интересное на ассемблере. Без цивилизации, только лес, как у настоящих индейцев :) Без среды разработки, только командная строка и Midnight Commander. Редактор будет Nano, который висит у меня на F4 в mc. Как там поет группа «Ноль»? Настоящему индейцу нужно только одного… Что еще нужно настоящему индейцу? Конечно, отладчик. Набираем gdb — есть! Ну что же, нажмем Shift+F4, и вперед!
Архитектура
Для начала, определимся с архитектурой. С разрядностью уже определились, 64 бита. В классических реализациях форта сегмент данных и кода совпадает. Но, мы попробуем сделать правильно. У нас код будет только в сегменте кода, данные — в сегменте данных. В результате этого получиться ядро для платформы и полностью платформонезависимый байт-код.
Попробуем сделать максимально быструю стековую байт-машину (но без JIT). Значит, у нас будет таблица, содержащая 256 адресов — по одному для каждой байт-команды. Меньше никак — лишняя проверка, это уже 1-2 команды процессора. А нам нужно быстро, без компромиссов.
Стеки
Обычно, в реализациях форта стек возвратов процессора (*SP) используют как стек данных, а стек возвратов реализуют другими средствами. Действительно, у нас машина будет стековая, и основная работа идет на стеке данных. Поэтому, сделаем так же — RSP будет стек данных. Ну а стек возвратов пусть будет RBP, который так же, по умолчанию, работает с сегментом стека. Таким образом, у нас будет три сегмента памяти: сегмент кода, сегмент данных и сегмент стеков (в нем будет и стек данных, и стек возвратов).
Регистры
Захожу в описание регистров x86-64, и упс! Тут есть еще целых 8 дополнительных регистров общего назначения (R8 — R16), по сравнению с режимами разрядности 32 или 16. Неплохо…
Уже определились, что будут нужны RSP и RBP. Еще нужен указатель (счетчик) команд байт-кода. Из операций по этому регистру нужно только чтение памяти. Основные регистры (RAX, RBX, RCX, RDX, RSI, RDI) более гибки, универсальны, с ними существует множество специальных команд. Они нам пригодятся для различных задач, а для счетчика команд байт-кода возьмем один из новых для меня регистров, пусть это будет R8.
Начнем?
У меня нет опыта программирования под Linux на ассемблере. Поэтому, для начала, найдем готовый «Hello, world», что бы понять, как программа стартует и выводит текст. Неожиданно для меня, я нашел варианты со странным синтаксисом, где даже источник и приемник переставлены местами. Как оказалось, это AT&T-синтаксис, и на нем, в основном, и пишут под Linux. Но, поддерживается и другой вариант синтаксиса, он называется Intel-синтаксис. Подумав, я решил использовать все же его. Ну что ж, пишем в начале .intel_syntax noprefix.
Скомпилируем и исполним «Hello, world», что бы убедиться, что все работает. Путем чтения хелпа и экспериментов я стал использовать для компиляции такую команду:
$ as fort.asm -o fort.o -g -ahlsm >list.txt
Тут ключ -o указывает файл результата, ключ -g предписывает генерировать отладочную информацию, а ключ -ahlsm задает формат листинга. А вывод сохраняю в листинг, в нем можно увидеть много полезного. Признаюсь, в начале работы я не делал листинг, и даже не указывал ключ -g. Ключ -g я стал использовать после первого применения отладчика, а делать листинг стал после того, как в коде появились макросы :)
После этого применяем компоновщик, но тут уже проще некуда:
$ ld forth.o -o forth
Ну и запускаем!
$ ./forth
Hello, world!
Работает.
.intel_syntax noprefix
.section .data
msg:
.ascii "Hello, world!\n"
len = . - msg # символу len присваивается длина строки
.section .text
.global _start # точка входа в программу
_start:
mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, OFFSET FLAT:msg # указатель на выводимую строку
mov edx, len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
xor ebx, ebx # выход с кодом 0
int 0x80 # вызов ядра
Кстати, чуть позже узнал, что в x86-64 для системного вызова правильнее использовать syscall, а не int 0x80. Вызов 0x80 считается для этой архитектуры устаревшим, хотя поддерживается.
Начало положено, а теперь…
Поехали!
Что бы была хоть какая-то конкретика, напишем код одной байт-команды. Пусть, это будет фортовское слово «0», кладущее 0 на вершину стека:
bcmd_num0: push 0
jmp _next
В момент выполнения этой команды, R8 уже указывает на следующую байт-команду. Надо ее прочитать, увеличить R8, определить по коду байт-команды исполнимый адрес, и передать на него управление.
Но… какой разрядности будет таблица адресов байт-команд? Тут мне пришлось изрядно покопаться в новой для меня системе команд x86-64. Увы, я не нашел команд, которые позволяют перейти по смещению в памяти. Значит, либо вычислять адрес, либо адрес будет готовый — 64 бита. Вычислять нам некогда, значит — 64 бита. В этом случае размер таблицы будет 256 * 8 = 4096 байт. Ну и закодируем, наконец, вызов _next:
_next: movzx rcx, byte ptr [r8]
inc r8
jmp [bcmd + rcx*8] # bcmd - таблица адресов байт-команд
Неплохо, как мне кажется… Всего три команды процессора, при переходе от одной байт-команды до другой.
На самом деле, мне не так просто дались эти команды. Пришлось снова копаться в системе комманд 0x86-64 и найти новую для меня команду MOVZX. Фактически, эта команда конвертирует значение размера 8, 16 или 32 бита в регистр 64 бита. Есть два варианта этой команды: беззнаковый, где старшие разряды дополняются нулями, и знаковый — MOVSX. В знаковом варианте расширяется знак, то есть, для положительных чисел в старшие разряды пойдут нули, а для отрицательных — единицы. Этот вариант еще нам пригодится для байт-команды lit.
Кстати, действительно ли этот вариант самый быстрый? Возможно, кто-то предложит еще быстрее?
Ну что ж, у нас теперь есть байт-машина, которая умеет бежать по последовательности байт-команд и выполнять их. Надо ее испытать в деле, заставить выполнить хотя бы одну команду. Только вот какую? Ноль в стек? Но тут даже не узнать результат, если не смотреть стек под отладчиком… Но если программа стартовала, ее можно завершить :)
Напишем команду bye, которая завершает выполнение программы и пишет об этом, тем более, у нас есть «Hellow, world!».
bcmd_bye: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bye # указатель на выводимую строку
mov edx, msg_bye_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 0 # выход с кодом 0
int 0x80 # вызов ядра
Остается дело за малым — создать таблицу адресов байт-комманд, инициализировать регистры, и запустить байт-машину. Так… в таблице 256 значений, а команд две. Что в остальные ячейки?
В остальных будет недопустимый код операции. Но, проверку на него делать нельзя, это лишние команды, у нас сейчас три, а с проверкой станет пять. Значит, сделаем такую команду-заглушку — плохая команда. Заполним вначале ей всю таблицу, а потом начнем занимать ячейки полезными командами. Пусть у плохой команды будет код 0x00, у команды bye — 0x01, а у '0' будет код 0x02, раз уже она написана. Плохая команда пока будет делать то же самое, что и bye, только с другим кодом завершения и текстом (положу в спойлер, почти то же, что и bye):
bcmd_bad: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bad_byte # указатель на выводимую строку
mov edx, msg_bad_byte_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 1 # выход с кодом 1
int 0x80 # вызов ядра
bcmd:
.quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
- Адреса для исполнения байт-команд будут начинаться на bcmd_
- Сами коды команд будут храниться в переменных, начинающихся с b_
Таким образом, тело байт-программы будет таким:
start:
.byte b_bye
Объявим размер стека данных, как stack_size. Пусть будет пока 1024. При инициализации, сделаем RBP = RSP — stack_size.
.intel_syntax noprefix
stack_size = 1024
.section .data
msg_bad_byte:
.ascii "Bad byte code!\n"
msg_bad_byte_len = . - msg_bad_byte # символу len присваевается длина строки
msg_bye:
.ascii "bye!\n"
msg_bye_len = . - msg_bye
msg_hello:
.ascii "Hello, world!\n"
msg_hello_len = . - msg_hello
bcmd:
.quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
start:
.byte b_bye
.section .text
.global _start # точка входа в программу
_start: mov rbp, rsp
sub rbp, stack_size
lea r8, start
jmp _next
_next: movzx rcx, byte ptr [r8]
inc r8
jmp [bcmd + rcx*8]
b_bad = 0x00
bcmd_bad: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bad_byte # указатель на выводимую строку
mov edx, msg_bad_byte_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 1 # выход с кодом 1
int 0x80 # вызов ядра
b_bye = 0x01
bcmd_bye: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bye # указатель на выводимую строку
mov edx, msg_bye_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 0 # выход с кодом 0
int 0x80 # вызов ядра
b_num0 = 0x02
bcmd_num0: push 0
jmp _next
Компилируем, запускаем:
$ as fort.asm -o fort.o -g -ahlsm >list.txt
$ ld forth.o -o forth
$ ./forth
bye!
Работает! Запустилась наша первая программа на байт-коде из одного байта :)
Конечно, так будет, если все сделано правильно. А если нет, то результат, скорее всего, будет такой:
$ ./forth
Ошибка сегментирования
Конечно, возможны и другие варианты, но с таким я сталкивался наиболее часто. И нам понадобиться отладчик.
$ gdb ./forth
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./forth...done.
(gdb)
Далее, вводя команды, производим отладку. Мне хватило часа, что бы найти несколько нужных команд и научиться использовать их для отладки. Вот они:
b <метка> — поставить точку останова
l <метка> — посмотреть исходный код
r — старт или рестарт программы
i r — посмотреть состояние регистров процессора
s — шаг
Кстати, помним, что компилировать программу надо с ключом -g? Иначе, метки и исходный код будут недоступны. В этом случае можно будет отлаживать только по дизассемблированному коду и использовать адреса в памяти. Мы, конечно, индейцы, но не до такой же степени...
Но как-то совсем мало делает программа. Мы ей только «Привет», а она сразу «Пока!». Давайте сделаем настоящий «Hello, world!» на байт-коде. Для этого положим на стек адрес и длину строки, затем выполним команду, выводящую строку, а после команду bye. Что бы сделать все это, потребуются новые команды: type для вывода строки, и lit для того, что бы положить адрес и длину строки. В начале напишем type, пусть ее код будет 0x80. Нам, снова, понадобиться тот кусочек кода с вызовом sys_write:
b_type = 0x80
bcmd_type: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
pop rdx
pop rcx
push r8
int 0x80 # вызов ядра
pop r8
jmp _next
Здесь мы адрес и длину строки берем из стека данных командами POP. Вызов int 0x80 может менять регистр R8, поэтому сохраняем его. Раньше мы так не делали, потому что программа завершалась. На содержимое этих регистров было плевать. Теперь же это обычная байт-команда, после которой продолжается выполнение байт-кода, и надо вести себя хорошо.
Теперь напишем команду lit. Это будет первая наша команда с параметрами. После байта с кодом этой команды, будут идти байты, содержащие число, которое она положит в стек. Сразу возникает вопрос — а какая разрядность тут нужна? Что бы можно было положить любое число, нужно 64 бита. Но, каждый раз команда будет занимать 9 байт, что бы положить одно число? Так мы теряем компактность, одно из главных свойств байт-кода, да и фортовского кода тоже…
Выход простой — сделаем несколько команд для разной разрядности. Это будут lit8, lit16, lit32 и lit64. Для маленьких чисел будем использовать lit8 и lit16, для боьших — lit32 и lit64. Наиболее часто используются маленькие числа, и для них будет самая короткая команда, которая занимает два байта. Неплохо!.. Коды этих команд сделаем 0x08 — 0x0B.
b_lit8 = 0x08
bcmd_lit8: movsx rax, byte ptr [r8]
inc r8
push rax
jmp _next
b_lit16 = 0x09
bcmd_lit16: movsx rax, word ptr [r8]
add r8, 2
push rax
jmp _next
b_lit32 = 0x0A
bcmd_lit32: movsx rax, dword ptr [r8]
add r8, 4
push rax
jmp _next
b_lit64 = 0x0B
bcmd_lit64: mov rax, [r8]
add r8, 8
push rax
jmp _next
Тут используем команду MOVSX — это знаковый вариант уже известной нам команды MOVZX. R8 у нас счетчик байт-команд. Загружаем по нему значение нужного размера, передвигаем его на следующую команду, а сконвертированное до 64 бит значение кладем в стек.
Не забудем занести адреса новых команд в таблицу на нужные позиции.
Вот и все готово, что бы написать свою первую программу «Hello, world!» на нашем байт-коде. Поработаем компилятором! :)
start:
.byte b_lit64
.quad msg_hello
.byte b_lit8
.byte msg_hello_len
.byte b_type
.byte b_bye
Используем две разных команды lit: lit64, что бы положить в стек адрес строки, и lit8, с помощью которой помещаем в стек длину. Дальше выполняем еще две байт-команды: type и bye.
Компилируем, запускаем:
$ as fort.asm -o fort.o -g -ahlsm >list.txt
$ ld forth.o -o forth
$ ./forth
Hello, world!
bye!
Заработал наш байт-код! Именно такой результат должен быть, если все нормально.
.intel_syntax noprefix
stack_size = 1024
.section .data
msg_bad_byte:
.ascii "Bad byte code!\n"
msg_bad_byte_len = . - msg_bad_byte # символу len присвавается длина строки
msg_bye:
.ascii "bye!\n"
msg_bye_len = . - msg_bye
msg_hello:
.ascii "Hello, world!\n"
msg_hello_len = . - msg_hello
bcmd:
.quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00
.quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
start:
.byte b_lit64
.quad msg_hello
.byte b_lit8
.byte msg_hello_len
.byte b_type
.byte b_bye
.section .text
.global _start # точка входа в программу
_start: mov rbp, rsp
sub rbp, stack_size
lea r8, start
jmp _next
_next: movzx rcx, byte ptr [r8]
inc r8
jmp [bcmd + rcx*8]
b_bad = 0x00
bcmd_bad: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bad_byte # указатель на выводимую строку
mov edx, msg_bad_byte_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 1 # выход с кодом 1
int 0x80 # вызов ядра
b_bye = 0x01
bcmd_bye: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bye # указатель на выводимую строку
mov edx, msg_bye_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 0 # выход с кодом 0
int 0x80 # вызов ядра
b_num0 = 0x02
bcmd_num0: push 0
jmp _next
b_lit8 = 0x08
bcmd_lit8: movsx rax, byte ptr [r8]
inc r8
push rax
jmp _next
b_lit16 = 0x09
bcmd_lit16: movsx rax, word ptr [r8]
add r8, 2
push rax
jmp _next
b_lit32 = 0x0A
bcmd_lit32: movsx rax, dword ptr [r8]
add r8, 4
push rax
jmp _next
b_lit64 = 0x0B
bcmd_lit64: mov rax, [r8]
add r8, 8
push rax
jmp _next
b_type = 0x80
bcmd_type: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
pop rdx
pop rcx
push r8
int 0x80 # вызов ядра
pop r8
jmp _next
Но возможности еще пока очень примитивны, нельзя сделать условие, цикл.
Как нельзя? Можно, все в наших руках! Сделаем-ка мы вывод этой строчки в цикле, 10 раз. Для этого потребуется команда условного перехода, а так же немного стековой арифметики: команда, уменьшающая значение на стеке на 1 (на форте «1-») и команда дублирования вершины («dup»).
С арифметикой все просто, даже не буду комментировать:
b_dup = 0x18
bcmd_dup: push [rsp]
jmp _next
b_wm = 0x20
bcmd_wm: decq [rsp]
jmp _next
Теперь условный переход. Для начала, сделаем задачу попроще — безусловный переход. Понятно, что нужно просто изменить значение регистра R8. Первое, что приходит в голову — это будет байт-команда, за которой лежит параметр — адрес перехода 64 бита. Опять девять байт. А нужны ли нам эти девять байт? Переходы обычно происходят на короткие расстояния, часто в пределах нескольких сотен байт. Значит, будем использовать не адрес, а смещение!
Разрядность? Во многих случаях хватит 8 бит (127 вперед/назад), но иногда этого будет мало. Поэтому поступим так же, как с командой lit, сделаем два варианта — 8 и 16 разрядов, коды команд будут 0x10 и 0x11:
b_branch8 = 0x10
bcmd_branch8: movsx rax, byte ptr [r8]
add r8, rax
jmp _next
b_branch16 = 0x11
bcmd_branch16: movsx rax, word ptr [r8]
add r8, rax
jmp _next
Теперь условный переход реализовать совсем просто. Если на стеке 0, идем в _next, а если нет — переходим в команду branch!b_qbranch8 = 0x12
bcmd_qbranch8: pop rax
or rax, rax
jnz bcmd_branch8
inc r8
jmp _next
b_qbranch16 = 0x13
bcmd_qbranch16: pop rax
or rax, rax
jnz bcmd_branch16
add r8, 2
jmp _next
Теперь у нас есть все, что бы сделать цикл:start:
.byte b_lit8
.byte 10 #счетчик повторений
#тело цикла
m0: .byte b_lit64
.quad msg_hello
.byte b_lit8
.byte msg_hello_len
.byte b_type
.byte b_wm
.byte b_dup
.byte b_qbranch8
.byte m0 - .
.byte b_bye
Первые две команды — кладем на стек счетчик цикла. Далее выводим строку Hello. Затем вычитаем 1 из счетчика, дублируем его и выполняем (или не выполняем) переход. Команда дублирования нужна потому, что команда условного перехода забирает значение с вершины стека. Переход тут используется восьмиразрядный, так как расстояние всего несколько байт.
Помещаем адреса новых команд в таблицу, компилируем и выполняем.
$ as fort.asm -o fort.o -g -ahlsm >list.txt
$ ld forth.o -o forth
$ ./forth
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
bye!
Отлично, условия и циклы мы уже делать можем!
.intel_syntax noprefix
stack_size = 1024
.section .data
msg_bad_byte:
.ascii "Bad byte code!\n"
msg_bad_byte_len = . - msg_bad_byte # символу len присваевается длина строки
msg_bye:
.ascii "bye!\n"
msg_bye_len = . - msg_bye
msg_hello:
.ascii "Hello, world!\n"
msg_hello_len = . - msg_hello
bcmd:
.quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00
.quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10
.quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
start:
.byte b_lit8
.byte 10 #счетчик повторений
#тело цикла
m0: .byte b_lit64
.quad msg_hello
.byte b_lit8
.byte msg_hello_len
.byte b_type
.byte b_wm
.byte b_dup
.byte b_qbranch8
.byte m0 - .
.byte b_bye
.section .text
.global _start # точка входа в программу
_start: mov rbp, rsp
sub rbp, stack_size
lea r8, start
jmp _next
_next: movzx rcx, byte ptr [r8]
inc r8
jmp [bcmd + rcx*8]
b_bad = 0x00
bcmd_bad: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bad_byte # указатель на выводимую строку
mov edx, msg_bad_byte_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 1 # выход с кодом 1
int 0x80 # вызов ядра
b_bye = 0x01
bcmd_bye: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bye # указатель на выводимую строку
mov edx, msg_bye_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 0 # выход с кодом 0
int 0x80 # вызов ядра
b_num0 = 0x02
bcmd_num0: push 0
jmp _next
b_lit8 = 0x08
bcmd_lit8: movsx rax, byte ptr [r8]
inc r8
push rax
jmp _next
b_lit16 = 0x09
bcmd_lit16: movsx rax, word ptr [r8]
add r8, 2
push rax
jmp _next
b_lit32 = 0x0A
bcmd_lit32: movsx rax, dword ptr [r8]
add r8, 4
push rax
jmp _next
b_lit64 = 0x0B
bcmd_lit64: mov rax, [r8]
add r8, 8
push rax
jmp _next
b_type = 0x80
bcmd_type: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
pop rdx
pop rcx
push r8
int 0x80 # вызов ядра
pop r8
jmp _next
b_dup = 0x18
bcmd_dup: push [rsp]
jmp _next
b_wm = 0x20
bcmd_wm: decq [rsp]
jmp _next
b_branch8 = 0x10
bcmd_branch8: movsx rax, byte ptr [r8]
add r8, rax
jmp _next
b_branch16 = 0x11
bcmd_branch16: movsx rax, word ptr [r8]
add r8, rax
jmp _next
b_qbranch8 = 0x12
bcmd_qbranch8: pop rax
or rax, rax
jnz bcmd_branch8
inc r8
jmp _next
b_qbranch16 = 0x13
bcmd_qbranch16: pop rax
or rax, rax
jnz bcmd_branch16
add r8, 2
jmp _next
Но до завершенной байт-машины не хватает еще одной очень важной функции. Мы не можем вызвать из одного байт-кода другой. У нас нет того, что называют подпрограммами, процедурами, и т.д. А в форте без этого мы не сможем использовать в одних словах другие, отличные от слов ядра.
Доведем работу до конца. Тут впервые нам потребуется стек возвратов. Команды нужны две — команда вызова и команда возврата (call и exit).
Команда call, в принципе, делает то же самое, что и branch — передает управление на другой кусочек байт-кода. Но, в отличие от branch, нужно еще сохранить адрес возврата в стеке возвратов, что бы можно было вернуться и продолжить выполнение. Есть и еще отличие — такие вызовы могут происходить на гораздо большие расстояния. Поэтому сделаем команду call по подобию branch, но в трех вариантах — 8, 16 и 32 бита.
b_call8 = 0x0C
bcmd_call8: movsx rax, byte ptr [r8]
sub rbp, 8
inc r8
mov [rbp], r8
add r8, rax
jmp _next
b_call16 = 0x0D
bcmd_call16: movsx rax, word ptr [r8]
sub rbp, 8
add r8, 2
mov [rbp], r8
add r8, rax
jmp _next
b_call32 = 0x0E
bcmd_call32: movsx rax, dword ptr [r8]
sub rbp, 8
add r8, 4
mov [rbp], r8
add r8, rax
jmp _next
Как видим, здесь, в отличие от переходов, добавлены 3 команды. Одна из них переставляет R8 на следующую байт-команду, а оставшиеся две сохраняют полученное значение в стеке возвратов. Кстати, тут я старался не ставить зависимые друг от друга команды процессора рядом, что бы конвеер процессора мог выполнять команды параллельно. Но насколько это дает эффект — не знаю. При желании, потом можно будет проверить на тестах.
Надо иметь ввиду, что формирование аргумента для команды call несколько отличается, нежели чем для branch. Для branch смещение вычисляется как разница между адресом перехода и адресом байта, следующего за байт-командой. А для команды call это разница между адресом перехода и адресом следующей команды. Зачем это нужно? Так получается меньше команд процессора.
Теперь команда возврата. Собственно, ее дело лишь восстановить R8 из стека возвратов и передать дальше управление байт-машине:
b_exit = 0x1F
bcmd_exit: mov r8, [rbp]
add rbp, 8
jmp _next
Эти команды будут применяться очень часто, и оптимизировать их нужно по максимуму. Байт-команда exit занимает три машинных команды. Можно ли тут что-то сократить? Оказывается, можно! Можно просто убрать команду перехода :)
Для этого разместим ее над точкой входа байт-машины _next:
b_exit = 0x1F
bcmd_exit: mov r8, [rbp]
add rbp, 8
_next: movzx rcx, byte ptr [r8]
inc r8
jmp [bcmd + rcx*8]
Кстати, наиболее важные и часто используемые команды (например, такие как call) нужно разместить ближе к байт-машине, что бы компилятор мог сформировать команду короткого перехода. Это хорошо видно в листинге. Вот пример.
262 0084 490FBE00 bcmd_lit8: movsx rax, byte ptr [r8]
263 0088 49FFC0 inc r8
264 008b 50 push rax
265 008c EB90 jmp _next
266
267 b_lit16 = 0x09
268 008e 490FBF00 bcmd_lit16: movsx rax, word ptr [r8]
269 0092 4983C002 add r8, 2
270 0096 50 push rax
271 0097 EB85 jmp _next
272
273 b_lit32 = 0x0A
274 0099 496300 bcmd_lit32: movsx rax, dword ptr [r8]
275 009c 4983C004 add r8, 4
276 00a0 50 push rax
277 00a1 E978FFFF jmp _next
277 FF
278
Тут в строке 265 и 271 команда jmp занимает по 2 байта, а в строке 277 та же команда компилируется уже в 5 байт, так как расстояние перехода превысило возможное для короткой команды.
Поэтому байт-команды, такие, как bad, bye, type переставим дальше, а такие, как call, branch, lit — ближе. К сожалению, в переход 127 байт уместить можно не так много.
Новые команды добавим в таблицу адресов команд, согласно их кодам.
Итак, у нас теперь есть вызов и возврат, протестируем их! Для этого печать строки выделим в отдельную процедуру, и будем вызывать ее в цикле дважды. А число повторений цикла уменьшим до трех.
start:
.byte b_lit8
.byte 3 #счетчик повторений
#тело цикла
m0: .byte b_call16
.word sub_hello - . - 2
.byte b_call16
.word sub_hello - . - 2
.byte b_wm
.byte b_dup
.byte b_qbranch8
.byte m0 - .
.byte b_bye
sub_hello:
.byte b_lit64
.quad msg_hello
.byte b_lit8
.byte msg_hello_len
.byte b_type
.byte b_exit
Тут можно было использовать и call8, но я решил применить call16, как наиболее вероятно используемую. Значение 2 вычитается из-за особенности вычисления адреса для байт-команды call, о которой я писал. Для call8 тут будет вычитаться 1, для call32, соответственно, 4.
Компилируем и вызываем:
$ as forth.asm -o forth.o -g -ahlsm>list.txt
$ ld forth.o -o forth
$ ./forth
Hello, world!
Bad byte code!
Упс… как говориться, что-то пошло не так :) Ну что ж, запускаем GDB и смотрим что там твориться. Точку останова я поставил сразу на bcmd_exit, так как видно, что вызов sub_hello проходит, и тело процедуры исполняется… запустил… и в точку останова программа не попала. Сразу возникло подозрение на код байт-команды. И, действительно, причина была в нем. b_exit я присвоил значение 0x1f, а сам адрес разместил в ячейке таблицы номер 0x17. Ну что же, исправлю значение b_exit на 0x17 и пробуем снова выполнить:
$ as forth.asm -o forth.o -g -ahlsm>list.txt
$ ld forth.o -o forth
$ ./forth
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
bye!
Ровно шесть раз здоровается, и один раз прощаеться. Как и должно быть :)
.intel_syntax noprefix
stack_size = 1024
.section .data
msg_bad_byte:
.ascii "Bad byte code!\n"
msg_bad_byte_len = . - msg_bad_byte # символу len присваевается длина строки
msg_bye:
.ascii "bye!\n"
msg_bye_len = . - msg_bye
msg_hello:
.ascii "Hello, world!\n"
msg_hello_len = . - msg_hello
bcmd:
.quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00
.quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_call8, bcmd_call16, bcmd_call32, bcmd_bad
.quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_exit # 0x10
.quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
.quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad
start:
.byte b_lit8
.byte 3 #счетчик повторений
#тело цикла
m0: .byte b_call16
.word sub_hello - . - 2
.byte b_call16
.word sub_hello - . - 2
.byte b_wm
.byte b_dup
.byte b_qbranch8
.byte m0 - .
.byte b_bye
sub_hello:
.byte b_lit64
.quad msg_hello
.byte b_lit8
.byte msg_hello_len
.byte b_type
.byte b_exit
.section .text
.global _start # точка входа в программу
_start: mov rbp, rsp
sub rbp, stack_size
lea r8, start
jmp _next
b_exit = 0x17
bcmd_exit: mov r8, [rbp]
add rbp, 8
_next: movzx rcx, byte ptr [r8]
inc r8
jmp [bcmd + rcx*8]
b_num0 = 0x02
bcmd_num0: push 0
jmp _next
b_lit8 = 0x08
bcmd_lit8: movsx rax, byte ptr [r8]
inc r8
push rax
jmp _next
b_lit16 = 0x09
bcmd_lit16: movsx rax, word ptr [r8]
add r8, 2
push rax
jmp _next
b_call8 = 0x0C
bcmd_call8: movsx rax, byte ptr [r8]
sub rbp, 8
inc r8
mov [rbp], r8
add r8, rax
jmp _next
b_call16 = 0x0D
bcmd_call16: movsx rax, word ptr [r8]
sub rbp, 8
add r8, 2
mov [rbp], r8
add r8, rax
jmp _next
b_call32 = 0x0E
bcmd_call32: movsx rax, dword ptr [r8]
sub rbp, 8
add r8, 4
mov [rbp], r8
add r8, rax
jmp _next
b_lit32 = 0x0A
bcmd_lit32: movsx rax, dword ptr [r8]
add r8, 4
push rax
jmp _next
b_lit64 = 0x0B
bcmd_lit64: mov rax, [r8]
add r8, 8
push rax
jmp _next
b_dup = 0x18
bcmd_dup: push [rsp]
jmp _next
b_wm = 0x20
bcmd_wm: decq [rsp]
jmp _next
b_branch8 = 0x10
bcmd_branch8: movsx rax, byte ptr [r8]
add r8, rax
jmp _next
b_branch16 = 0x11
bcmd_branch16: movsx rax, word ptr [r8]
add r8, rax
jmp _next
b_qbranch8 = 0x12
bcmd_qbranch8: pop rax
or rax, rax
jnz bcmd_branch8
inc r8
jmp _next
b_qbranch16 = 0x13
bcmd_qbranch16: pop rax
or rax, rax
jnz bcmd_branch16
add r8, 2
jmp _next
b_bad = 0x00
bcmd_bad: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bad_byte # указатель на выводимую строку
mov edx, msg_bad_byte_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 1 # выход с кодом 1
int 0x80 # вызов ядра
b_bye = 0x01
bcmd_bye: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, offset msg_bye # указатель на выводимую строку
mov edx, msg_bye_len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
mov ebx, 0 # выход с кодом 0
int 0x80 # вызов ядра
b_type = 0x80
bcmd_type: mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
pop rdx
pop rcx
push r8
int 0x80 # вызов ядра
pop r8
jmp _next
Что в итоге
Мы сделали и протестировали законченную и довольно быструю 64-разрядную стековую байт-машину. По скорости, возможно, эта байт-машина будет одной из самых быстрых в своем классе (стековая байт-машина без JIT). Она умеет выполнять команды последовательно, делать условные и безусловные переходы, вызывать процедуры, возвращаться из них. В то же время используемый байт-код обладает достаточной компактностью. В основном, байт-команды занимают 1-3 байта, больше — это очень редко (только числа большого размера, и очень дальние вызовы процедур). Так же набросан небольшой набор байт-команд, который легко расширять. Допустим, все основные команды работы со стеком (drop, swap, over, root и т.д. можно написать минут за 20, столько же уйдет на арифметические целочисленные команды).
Еще важный момент. Байт-код, в отличие от классического прямого шитого кода форта, не содержит машинных команд, поэтому он может переноситься без перекомпиляции на другую платформу. Достаточно один раз переписать ядро на систему команд нового процессора, и сделать это можно весьма быстро.
Текущий вариант байт-машины не специфичен для какого-то конкретного языка. Но я хочу сделать на ней реализацию языка Форт потому, что у меня есть опыт работы с ним, и компилятор для него можно сделать очень быстро.
Если есть к этому интерес, на основе этой машины, в следующей статье, я сделаю ввод-вывод строк и чисел, форт-словарь, и интерпретатор. Можно будет «потрогать» команды руками. Ну а в третей статье сделаем компилятор, и получиться почти законченная форт-система. Затем можно будет написать и скомпилировать какие-либо стандартные алгоритмы и сравнить быстродействие с другими языками и системами. Можно использовать, например, решето Эратосфена, и подобные.
Интересно еще по экспериментировать с вариантами. Например, сделать таблицу команд 16-ти разрядной, и посмотреть, как это повлияет на быстродействие. Еще можно точку входа _next превратить в макрос, в этом случае машинный код каждой байт-команды увеличиться в размере на две команды (минус переход и плюс три команды из _next). То есть, в конце будет не переход на _next, а содержимое самой точки _next (это 14 байт). Интересно узнать, как это повлияет на быстродействие. Еще можно попробовать сделать оптимизацию, используя регистры. Например, стандартный цикл со счетчиком в форте хранит счетчик в стеке возвратов. Можно сделать регистровый вариант и так же по тестировать.
Еще можно сделать компилятор выражений, записанных в классической форме (например, A = 5 + (B + C * 4) ).
В общем, есть простор для экспериментов! :)
Комментарии (22)
berez
03.12.2018 22:08Байт-код — это прекрасно. Осталось выяснить, при чем тут форт. Вы как-то планируете добавить интерпретатор юзерского ввода и компиляцию его в шитый код?
kuza2000 Автор
03.12.2018 23:06+1Да, об этом я написал в заключительной части статьи. Хочу сделать интерпретатор и компилятор форта. Только не в шитый код, а в байт-код этой машины.
mxms
04.12.2018 01:21Кстати, хороший вопрос что будет быстрее работать — один из вариантов шитого кода или ваш байткод.
kuza2000 Автор
04.12.2018 08:40Самому интересно. В теории, шитый год должен быть быстрее, так как там только одно чтение из памяти на команду (из кода). В байт-коде два — из кода, и из таблицы адресов. Потом можно будет сравнить с другими форт-системами, но, там другие детали реализации, другая разрядность…
FForth
05.12.2018 13:07Есть такой учебный проект Форта (Forthress) преподователя Igor Zhirkov из ITMO
"Low-Level Programming: C, Assembly, and Program Execution on Intel x86-64 Architecture".
сделанного на NASM.
P.S. И тоже вопрос, почему не FASM для реализации?
На FASM сделано ядро операционной системы KolibriOS (и Форт к ней тоже уже к ней портировали c базиса российской Форт-системы SPF4)
В теги к статье можно добавить "Forth" (Форт) "Ненормальное программирование" :)
mistergrim
03.12.2018 23:47Маленькое замечание: раз уж у вас и Linux, и AS, и LD, расширение исходников лучше указывать .s, а не .asm.
mxms
04.12.2018 01:05Форт это прекрасно. Ждём новых статей.
FForth
05.12.2018 13:16[url=https://archive.org/search.php?query=subject%3A%22FORTH+%28Computer+program+language%29%22]Некоторый kнижный архив по Форт на webarxive[/url]
johnfound
04.12.2018 11:48Ассемблер, это прекрасно! Ждем новых статей.
Хотя, выбор AS несколько смущает. Хоть в intel синтакс и на том спасибо. Но для полностью ассемблерных проектов все-таки FASM или NASM лучше будут. AS, скорее всего бакенд компиляторов. Для людей не очень удобный.
potan
04.12.2018 12:57Если уж писать на ассемблере, то вершину стека нужно в регистре хранить.
kuza2000 Автор
04.12.2018 13:18Не совсем понял идею. Сейчас указатель на вершину стека данных хранится в RSP, стека возвратов в RBP.
gatoazul
04.12.2018 16:25+1Вершину стека держать в регистре, допустим, в EAX. RSP указывает на настоящий стек. Полный стек состоит из двух частей.
Упрощает и стековые, и арифметические операции, и добавляет скорости.
NEGATE neg RAX
SWAP xchg EAX, [ESP]
DROP mov [ESP], EAX; add ESP,4
и т.д.kuza2000 Автор
04.12.2018 16:43Понятно. Держать одно верхнее значение в регистре, остальное в памяти. Можно попробовать, протестировать производительность.
Вообще, я пытался придумать, как держать несколько значений с вершины в регистрах… но несколько точно не стоит, слишком много движений при стековых операциях получается. Одно — может быть…
Может несколько уменьшиться количество обращений к памяти, а вот команд процессора — наверняка возрастет. Надо тестировать.potan
05.12.2018 08:22Количество команд, как правило, сокращается. На те же действия исчезает одна команда загрузки из стека и одна команда помещения в стек.
Jarik65535
04.12.2018 13:09+2Сам делал нечто подобное. Хотя мой интерпретатор/транслятор написан на C, для кроссплатформенности, и может быть как 32 битным, так и 64. Словарь формируется скриптом на python. Основной же особенностью является то, что форт представляет собой модуль ядра линукс. Разрабатывался для того, чтобы можно было изменять регистры и работать с железом напрямую. Есть возможность вызова функций ядра, однако, callback'и пока не реализованы.
github.com/y-salnikov/kforthmxms
04.12.2018 18:23Словарь формируется скриптом на python.
Ну это какое святотатство. Форт должен быть написан только на Форт.
Jarik65535
04.12.2018 18:32Так он и написан на форте, просто этот форт интерпретируется на python. Это делается только на этапе сборки модуля, после запуска пополнять словарь можно только исключительно средствами Форта.
qw1
04.12.2018 14:04Что бы можно было положить любое число, нужно 64 бита. Но, каждый раз команда будет занимать 5 байт, что бы положить одно число?
это будет байт-команда, за которой лежит параметр — адрес перехода 64 бита. Опять пять байт
Девять же, а не пять?
mov ecx, offset msg_bye # указатель на выводимую строку
Повезло, что старшие 32 бита нули, но лучше ECX заменить на RCX (это копипаста 32-битного кода?)kuza2000 Автор
04.12.2018 14:10+1Точно, девять! Все мыслю 32-мя битами :) Исправил.
И с ECX косяк, исправлю в исходнике.
Спасибо!
oisee
Про синтаксис.
А есть синтаксис где вместо MOV используется LD? =)
(В своё время так привык на Z80, что на целую букву больше писать было странно...)
или просто делаешь #DEFINE LD MOV?