
Продолжим эксперименты с байт-кодом. Это продолжение статьи о байт-машине на ассемблере, вот первая часть.
Вообще, я планировал во второй части сделать интерпретатор форта, а в третьей — компилятор форта для этой байт-машины. Но объем, который получался для статьи, оказался очень велик. Что бы сделать интерпретатор, надо расширить ядро (набор байт-команд), и реализовать: переменные, парсинг строк, ввод строк, словари, поиск по словарям… Ну и должен работать хотя бы вывод чисел. В результате, я решил разбить статью об интерпретаторе на две. Поэтому, в этой статье мы расширим ядро, определимся с переменными, сделаем вывод чисел. Дальше примерный план такой: 3-я часть — интерпретатор, 4-я — компилятор. И, конечно же, тесты быстродействия. Они будут в 4-й или 5-й статье. Эти статьи будут уже после нового года.
А кто еще не испугался страшного ассемблера и байт-кода — добро пожаловать под кат! :)
Для начала исправим ошибки. Зададим файлу расширение .s, как это принято для GAS (спасибо mistergrim). Затем, заменим int 0x80 на syscall и используем 64-битные регистры (спасибо qw1). В начале я не внимательно прочитал описание вызова и исправил только регистры… и получил Segmentation fault. Оказывается, для syscall поменялось все, в том числе и номера вызовов. sys_write для syscall имеет номер 1, а sys_exit — 60. В итоге, команды bad, type и bye приобрели такой вид:
b_bad = 0x00 bcmd_bad: mov rax, 1 # системный вызов № 1 - sys_write mov rdi, 1 # поток № 1 - stdout mov rsi, offset msg_bad_byte # указатель на выводимую строку mov rdx, msg_bad_byte_len # длина строки syscall # вызов ядра mov rax, 60 # системный вызов № 1 - sys_exit mov rbx, 1 # выход с кодом 1 syscall # вызов ядра b_bye = 0x01 bcmd_bye: mov rax, 1 # системный вызов № 1 - sys_write mov rdi, 1 # поток № 1 - stdout mov rsi, offset msg_bye # указатель на выводимую строку mov rdx, msg_bye_len # длина строки syscall # вызов ядра mov rax, 60 # системный вызов № 60 - sys_exit mov rdi, 0 # выход с кодом 0 syscall # вызов ядра b_type = 0x80 bcmd_type: mov rax, 1 # системный вызов № 1 - sys_write mov rdi, 1 # поток № 1 - stdout pop rdx pop rsi push r8 syscall # вызов ядра pop r8 jmp _next
И еще один момент. Совершенно справедливо написали в комментариях к прошлой статье berez и fpauk, что, если использовать в байт-коде адреса процессора, то байт-код зависит от платформы. А в том примере адрес строки для «Hello, world!», был задан в байт-коде по значению (командой lit64). Конечно же, так делать не нужно. Но это был самый простой способ проверить байт-машину. Больше я так делать не буду, а адреса переменных буду получать другими средствами: в частности, командой var (об этом чуть позже).
Разминка
А сейчас, в качестве разминки, сделаем все основные целочисленные арифметические операции (+, -, *, /, mod, /mod, abs). Они нам понадобятся.
Код настолько прост, что привожу его в спойлере без комментариев.
Арифметика
b_add = 0x21 bcmd_add: pop rax add [rsp], rax jmp _next b_sub = 0x22 bcmd_sub: pop rax sub [rsp], rax jmp _next b_mul = 0x23 bcmd_mul: pop rax pop rbx imul rbx push rax jmp _next b_div = 0x24 bcmd_div: pop rbx pop rax cqo idiv rbx push rax jmp _next b_mod = 0x25 bcmd_mod: pop rbx pop rax cqo idiv rbx push rdx jmp _next b_divmod = 0x26 bcmd_divmod: pop rbx pop rax cqo idiv rbx push rdx push rax jmp _next b_abs = 0x27 bcmd_abs: mov rax, [rsp] or rax, rax jge _next neg rax mov [rsp], rax jmp _next
Традиционно, в форте к обычным арифметическим и стековым операциям добавляют операции двойной точности. Слова для таких операций обычно начинаются с символа «2»: 2DUP, 2SWAP и т.д. Но у нас стандартная арифметика уже 64 разряда, и 128 мы сегодня точно делать не будем :)
Дальше добавим основные стековые операции (drop, swap, root, -root, over, pick, roll).
Стековые операции
b_drop = 0x31 bcmd_drop: add rsp, 8 jmp _next b_swap = 0x32 bcmd_swap: pop rax pop rbx push rax push rbx jmp _next b_rot = 0x33 bcmd_rot: pop rax pop rbx pop rcx push rbx push rax push rcx jmp _next b_mrot = 0x34 bcmd_mrot: pop rcx pop rbx pop rax push rcx push rax push rbx jmp _next b_over = 0x35 bcmd_over: push [rsp + 8] jmp _next b_pick = 0x36 bcmd_pick: pop rcx push [rsp + 8*rcx] jmp _next b_roll = 0x37 bcmd_roll: pop rcx mov rbx, [rsp + 8*rcx] roll1: mov rax, [rsp + 8*rcx - 8] mov [rsp + 8*rcx], rax dec rcx jnz roll1 push rbx jmp _next
И еще сделаем команды чтения и записи в память (фортовские слова @ и !). А так же их аналоги на другую разрядность.
Чтение и запись в память
b_get = 0x40 bcmd_get: pop rcx push [rcx] jmp _next b_set = 0x41 bcmd_set: pop rcx pop rax mov [rcx], rax jmp _next b_get8 = 0x42 bcmd_get8: pop rcx movsx rax, byte ptr [rcx] push rax jmp _next b_set8 = 0x43 bcmd_set8: pop rcx pop rax mov [rcx], al jmp _next b_get16 = 0x44 bcmd_get16: pop rcx movsx rax, word ptr [rcx] push rax jmp _next b_set16 = 0x45 bcmd_set16: pop rcx pop rax mov [rcx], ax jmp _next b_get32 = 0x46 bcmd_get32: pop rcx movsx rax, dword ptr [rcx] push rax jmp _next b_set32 = 0x47 bcmd_set32: pop rcx pop rax mov [rcx], eax jmp _next
Нам еще могут понадобиться команды сравнения, сделаем и их.
Команды сравнения
# 0= b_zeq = 0x50 bcmd_zeq: pop rax or rax, rax jnz rfalse rtrue: push -1 jmp _next rfalse: push 0 jmp _next # 0< b_zlt = 0x51 bcmd_zlt: pop rax or rax, rax jl rtrue push 0 jmp _next # 0> b_zgt = 0x52 bcmd_zgt: pop rax or rax, rax jg rtrue push 0 jmp _next # = b_eq = 0x53 bcmd_eq: pop rbx pop rax cmp rax, rbx jz rtrue push 0 jmp _next # < b_lt = 0x54 bcmd_lt: pop rbx pop rax cmp rax, rbx jl rtrue push 0 jmp _next # > b_gt = 0x55 bcmd_gt: pop rbx pop rax cmp rax, rbx jg rtrue push 0 jmp _next # <= b_lteq = 0x56 bcmd_lteq: pop rbx pop rax cmp rax, rbx jle rtrue push 0 jmp _next # >= b_gteq = 0x57 bcmd_gteq: pop rbx pop rax cmp rax, rbx jge rtrue push 0 jmp _next
Тестировать операции не будем. Главное, что бы ассемблер не выдал при компиляции ошибок. Отладка будет в процессе их использования.
Еще сразу сделаем слово depth (глубина стека). Для этого, при старте, сохраним начальные значения стека данных и стека возвратов. Эти значения могут еще пригодиться и при рестарте системы.
init_stack: .quad 0 init_rstack: .quad 0 _start: mov rbp, rsp sub rbp, stack_size lea r8, start mov init_stack, rsp mov init_rstack, rbp jmp _next b_depth = 0x38 bcmd_depth: mov rax, init_stack sub rax, rsp shr rax, 3 push rax jmp _next
Вывод чисел
Ну что же, разминка кончилась, и придется немного попотеть. Научим нашу систему выводить числа. Для вывода чисел в форте используется слово "." (точка). Сделаем это так, как делается в стандартных реализациях форта, с использованием слов <#, hold, #, #s, #>, base. Придется реализовать все эти слова. Для формирования числа используется буфер и указатель на формируемый символ, это будут слова holdbuf и holdpoint.
Итак, нам потребуются вот такие слова:
- holdbuf — буфер для формирования представления числа, формирование происходит с конца
- holdpoint — адрес на последний выведенный символ (в holdbuf)
- <# — начало формирования числа; устанавливает holdpoint на байт, за последним байтом holdbuf
- hold — уменьшает holdpoint на 1 и по полученному адресу сохраняет символ со стека в буфер
- # — делит слово на вершине стека на основание системы счисления, остаток от деления переводит в символ и сохраняет в буфер с помощью hold
- #s — преобразует все слово; фактически вызывает слово # в цикле, пока на стеке не останется 0
- #> — завершение преобразования; помещает в стек начало сформированной строки и ее длину
Все слова сделаем на байт-коде, но для начала разберемся с переменными.
Переменные
А вот тут будет немного фортовской магии. Дело в том, что в форте переменная — это слово. При исполнении этого слова, в стеке оказывается адрес ячейки памяти, хранящее значение переменной. По этому адресу можно читать или писать. Например, что бы записать в переменную A значение 12345, нужно выполнить такие команды: «12345 A !». В этом примере, в стек помещается 12345, потом переменная A кладет свой адрес, а слово "!" снимает со стека два значения и записывает 12345 по адресу переменной A. В типичных реализациях форта (с прямым шитым кодом), переменные представляют собой команду микропроцессора CALL с адресом _next, после которой зарезервировано место для хранения значения переменной. При исполнении такого слова микропроцессор передает управление на _next и кладет в стек адрес возврата (по RSP). Но в форте стек микропроцессора — арифметический, и возвращаться никуда не будем. В результате этого, выполнение продолжается, а в стеке — адрес переменной. И все это одной процессорной командой! На ассемблере это выглядело бы так:
call _next # улетели на _next, а в стек попал адрес возврата, где лежит 12345 .quad 12345
Но у нас байт-код, и мы не можем использовать этот механизм! Я не сразу сообразил, как можно сделать подобный механизм на байт-коде. Но, если рассуждать логически, ни что не мешает реализовать что-то очень похожее. Просто надо учитывать, что это будет не команда процессора, а байт-код, точнее, «подпрограмма» на байт-коде. Вот постановка задачи:
- это байт-код, при передаче управления на который следует сразу же возврат из него
- после возврата, в арифметическом стеке должен остаться адрес, где храниться значение переменной
У нас есть байт-команда exit. Сделаем слово на байт-коде, содержащее одну-единственную команду exit. Тогда эта команда будет осуществлять возврат из него. Остается сделать такую же команду, которая дополнительно положит в стек адрес следующего байта (регистр R8). Сделаем это в виде дополнительной точки входа в exit, что бы сэкономить на переходе:
b_var0 = 0x28 bcmd_var0: push r8 b_exit = 0x17 bcmd_exit: mov r8, [rbp] add rbp, 8 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8]
Теперь переменная base будет выглядеть так:
base: .byte b_var0 .quad 10
Кстати, а почему именно var0, а не просто var? Дело в том, что будут другие команды для определения более продвинутых слов, которые содержат данные. Подробнее я расскажу в следующих статьях.
Теперь у нас все готово, что бы сделать вывод чисел. Начнем!
Слова base, holdbuf, holdpoint
Как будут устроены переменные, уже определились. Поэтому, слова base, holdbuf, holdpoint получаются такими:
base: .byte b_var0 .quad 10 holdbuf_len = 70 holdbuf: .byte b_var0 .space holdbuf_len holdpoint: .byte b_var0 .quad 0
Размер буфера holdbuf выбран 70. Максимальное количество разрядов числа — 64 (это если выбрать двоичную систему). Еще сделан запас в несколько символов, что бы поместить, например, знак числа и пробел после него. На переполнение буфера сделаем проверку, но пока не будем помещать лишние символы в буфер. Потом можно будет сделать другую диагностику.
hold
Теперь можно сделать слово hold. На форте его код выглядит так:
: hold holdpoint @ 1- dup holdbuf > if drop drop else dup holdpoint ! c! then ;
Для тех, кто видит форт впервые, разберу подробно код. Для последующих слов делать этого не буду.
В начале идет слово для определения новых слов и имя нового слова: ": hold". После этого идет код, который завершается словом ";". Разберем код слова. Приведу команду и состояние стека после исполнения команды. Перед вызовом слова на стеке находиться код символа, который помещается в буфер (обозначено <символ>). Далее получается так:
holdpoint <символ> <адрес переменной holdpoint> @ <символ> <содержимое переменной holdpoint> 1- <символ> <содержимое переменной holdpoint минус 1> dup <символ> <содержимое переменной holdpoint минус 1> <содержимое переменной holdpoint минус 1> holdbuf <символ> <содержимое переменной holdpoint минус 1> <содержимое переменной holdpoint минус 1> <начало буфера holdbuf> > <символ> <содержимое переменной holdpoint минус 1> <истина, если содержимое переменной holdpoint минус 1 больше начала буфера holdbuf>
После этого находится команда if, которая компилируется в условный переход на последовательность команд между else и then. Условный переход снимает со стека результат сравнения и выполняет переход, если на стеке была ложь. Если перехода не было, то выполняется ветвь между if и else, в которой две комманды drop, которые убирают символ и адрес. В противном случае выполнение продолжается. Слово "!" сохраняет новое значение в holdpoint (со стека снимается адрес и значение). А слово «c!» записывает символ в буфер, это байт-команда set8 (со стека снимается адрес и значение символа).
dup <символ> <содержимое переменной holdpoint минус 1> <содержимое переменной holdpoint минус 1> holdpoint <символ> <содержимое переменной holdpoint минус 1> <содержимое переменной holdpoint минус 1> <адрес переменной holdpoint> ! <символ> <содержимое переменной holdpoint минус 1> c! все, символ записан, а стек пустой! :)
Вот сколько действий делает эта короткая последовательность комманд! Да, форт лаконичен. А теперь включаем ручной «компилятор» в голове :) И компилируем все это в байт-код:
hold: .byte b_call8 .byte holdpoint - . - 1 # holdpoint .byte b_get # @ .byte b_wm # 1- .byte b_dup # dup .byte b_call8 .byte holdbuf - . - 1 # holdbuf .byte b_gt # > .byte b_qbranch8 # if .byte 0f - . .byte b_drop # drop .byte b_drop # drop .byte b_branch8 # команда перехода на возврат (после then) .byte 1f - . 0: .byte b_dup # dup .byte b_call8 .byte holdpoint - . - 1 # holdpoint .byte b_set # ! .byte b_set8 # c! 1: .byte b_exit # ;
Здесь я использовал локальные метки (0 и 1). К этим меткам можно обращаться по специальным именам. Например, к метке 0 можно обращаться по именам 0f или 0b. Это означает ссылку на ближайшую метку 0 (вперед или назад). Довольно удобно для меток, которые используются локально, что бы не придумывать различные имена.
Слово #
Сделаем слово #. На форте его код будет выглядеть так:
: # base /mod swap dup 10 < if c″ 0 + else 10 - c″ A + then hold ;
Условие тут применяется для проверки: меньше ли полученная цифра десяти? Если меньше, используются цифры 0-9, иначе — символы, начиная с «A». Это позволит работать и с шестнадцатиричной системой счисления. Последовательность c″ 0 кладет на стек код символа 0. Включаем «компилятор»:
conv: .byte b_call16 .word base - . - 2 # base .byte b_get # @ .byte b_divmod # /mod .byte b_swap # swap .byte b_dup # dup .byte b_lit8 .byte 10 # 10 .byte b_lt # < .byte b_qnbranch8 # if .byte 0f - . .byte b_lit8 .byte '0' # c″ 0 .byte b_add # + .byte b_branch8 # else .byte 1f - . 0: .byte b_lit8 .byte 'A' # c″ A .byte b_add # + 1: .byte b_call16 .word hold - . - 2 # hold .byte b_exit # ;
Слово <#
Слово <# совсем простое:
: <# holdbuf 70 + holdpoint ! ;
Байт-код:
conv_start: .byte b_call16 .word holdbuf - . - 2 .byte b_lit8 .byte holdbuf_len .byte b_add .byte b_call16 .word holdpoint - . - 2 .byte b_set .byte b_exit
Слово #>
Слово #> для завершения преобразования выглядит так:
: #> holdpoint @ holdbuf 70 + over - ;
Байт-код:
conv_end: .byte b_call16 .word holdpoint - . - 2 .byte b_get .byte b_call16 .word holdbuf - . - 2 .byte b_lit8 .byte holdbuf_len .byte b_add .byte b_over .byte b_sub .byte b_exit
Слово #s
И, наконец, слово #s:
: #s do # dup 0= until ;
Байт-код:
conv_s: .byte b_call8 .byte conv - . - 1 .byte b_dup .byte b_qbranch8 .byte conv_s - . .byte b_exit
Кто внимательный, заметит здесь небольшое несоответствие байт-кода и кода форта :)
Все готово
Теперь ничто не помешает сделать слово ".", которое выводит число:
: . <# #s drop #> type ;
Байт-код:
dot: .byte b_call8 .byte conv_start - . - 1 .byte b_call8 .byte conv_s - . - 1 .byte b_drop .byte b_call8 .byte conv_end - . - 1 .byte b_type .byte b_exit
Сделаем тестовый байт-код, который проверяет нашу точку:
start: .byte b_lit16 .word 1234 .byte b_call16 .word dot - . - 2 .byte b_bye
Конечно, заработало все не сразу. Но, после отладки, был получен такой результат:
$ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth 1234bye!
Косяк виден сразу. После числа форт должен выводить пробел. Добавим после вызова conv_start (<#) команды 32 hold.
Еще сделаем вывод знака. В начале добавим dup abs, а в конце проверим знак оставленной копии и поместим минус, если число отрицательное (0< if c″ – hold then). В результате, слово "." приобретает такой вид:
: . dup abs <# 32 hold #s drop #> 0< if c″ - hold then type ;
Байт-код:
dot: .byte b_dup .byte b_abs .byte b_call8 .byte conv_start - . - 1 .byte b_lit8 .byte ' ' .byte b_call16 .word hold - . - 2 .byte b_call8 .byte conv_s - . - 1 .byte b_drop .byte b_zlt .byte b_qnbranch8 .byte 1f - . .byte b_lit8 .byte '-' .byte b_call16 .word hold - . - 2 1: .byte b_call8 .byte conv_end - . - 1 .byte b_type .byte b_exit
В стартовой последовательности байт-команд поставим отрицательное число и проверим:
$ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth -1234 bye!
Вывод чисел есть!
Полный исходник
.intel_syntax noprefix stack_size = 1024 .section .data init_stack: .quad 0 init_rstack: .quad 0 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 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_qnbranch8, bcmd_qnbranch16,bcmd_bad, bcmd_exit # 0x10 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_add, bcmd_sub, bcmd_mul, bcmd_div, bcmd_mod, bcmd_divmod, bcmd_abs # 0x20 .quad bcmd_var0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_dup, bcmd_drop, bcmd_swap, bcmd_rot, bcmd_mrot, bcmd_over, bcmd_pick, bcmd_roll # 0x30 .quad bcmd_depth, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_get, bcmd_set, bcmd_get8, bcmd_set8, bcmd_get16, bcmd_set16, bcmd_get32, bcmd_set32 # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_zeq, bcmd_zlt, bcmd_zgt, bcmd_eq, bcmd_lt, bcmd_gt, bcmd_lteq, bcmd_gteq #0x50 .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_lit16 .word -1234 .byte b_call16 .word dot - . - 2 .byte b_bye base: .byte b_var0 .quad 10 holdbuf_len = 70 holdbuf: .byte b_var0 .space holdbuf_len holdpoint: .byte b_var0 .quad 0 # : hold holdpoint @ 1- dup holdbuf > if drop drop else dup holdpoint ! c! then ; hold: .byte b_call8 .byte holdpoint - . - 1 # holdpoint .byte b_get # @ .byte b_wm # 1- .byte b_dup # dup .byte b_call8 .byte holdbuf - . - 1 # holdbuf .byte b_gt # > .byte b_qbranch8 # if .byte 0f - . .byte b_drop # drop .byte b_drop # drop .byte b_branch8 # команда перехода на возврат (после then) .byte 1f - . 0: .byte b_dup # dup .byte b_call8 .byte holdpoint - . - 1 # holdpoint .byte b_set # ! .byte b_set8 # c! 1: .byte b_exit # ; # : # base /mod swap dup 10 < if c" 0 + else 10 - c" A + then hold ; conv: .byte b_call16 .word base - . - 2 # base .byte b_get # @ .byte b_divmod # /mod .byte b_swap # swap .byte b_dup # dup .byte b_lit8 .byte 10 # 10 .byte b_lt # < .byte b_qnbranch8 # if .byte 0f - . .byte b_lit8 .byte '0' # c" 0 .byte b_add # + .byte b_branch8 # else .byte 1f - . 0: .byte b_lit8 .byte '?' # c" A .byte b_add # + 1: .byte b_call16 .word hold - . - 2 # hold .byte b_exit # ; # : <# holdbuf 70 + holdpoint ! ; conv_start: .byte b_call16 .word holdbuf - . - 2 .byte b_lit8 .byte holdbuf_len .byte b_add .byte b_call16 .word holdpoint - . - 2 .byte b_set .byte b_exit # : #s do # dup 0=until ; conv_s: .byte b_call8 .byte conv - . - 1 .byte b_dup .byte b_qbranch8 .byte conv_s - . .byte b_exit # : #> holdpoint @ holdbuf 70 + over - ; conv_end: .byte b_call16 .word holdpoint - . - 2 .byte b_get .byte b_call16 .word holdbuf - . - 2 .byte b_lit8 .byte holdbuf_len .byte b_add .byte b_over .byte b_sub .byte b_exit dot: .byte b_dup .byte b_abs .byte b_call8 .byte conv_start - . - 1 .byte b_lit8 .byte ' ' .byte b_call16 .word hold - . - 2 .byte b_call8 .byte conv_s - . - 1 .byte b_drop .byte b_zlt .byte b_qnbranch8 .byte 1f - . .byte b_lit8 .byte '-' .byte b_call16 .word hold - . - 2 1: .byte b_call8 .byte conv_end - . - 1 .byte b_type .byte b_exit .section .text .global _start # точка входа в программу _start: mov rbp, rsp sub rbp, stack_size lea r8, start mov init_stack, rsp mov init_rstack, rbp jmp _next b_var0 = 0x28 bcmd_var0: push r8 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 = 0x30 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_add = 0x21 bcmd_add: pop rax add [rsp], rax jmp _next b_sub = 0x22 bcmd_sub: pop rax sub [rsp], rax jmp _next b_mul = 0x23 bcmd_mul: pop rax pop rbx imul rbx push rax jmp _next b_div = 0x24 bcmd_div: pop rbx pop rax cqo idiv rbx push rax jmp _next b_mod = 0x25 bcmd_mod: pop rbx pop rax cqo idiv rbx push rdx jmp _next b_divmod = 0x26 bcmd_divmod: pop rbx pop rax cqo idiv rbx push rdx push rax jmp _next b_abs = 0x27 bcmd_abs: mov rax, [rsp] or rax, rax jge _next neg rax mov [rsp], rax jmp _next b_drop = 0x31 bcmd_drop: add rsp, 8 jmp _next b_swap = 0x32 bcmd_swap: pop rax pop rbx push rax push rbx jmp _next b_rot = 0x33 bcmd_rot: pop rax pop rbx pop rcx push rbx push rax push rcx jmp _next b_mrot = 0x34 bcmd_mrot: pop rcx pop rbx pop rax push rcx push rax push rbx jmp _next b_over = 0x35 bcmd_over: push [rsp + 8] jmp _next b_pick = 0x36 bcmd_pick: pop rcx push [rsp + 8*rcx] jmp _next b_roll = 0x37 bcmd_roll: pop rcx mov rbx, [rsp + 8*rcx] roll1: mov rax, [rsp + 8*rcx - 8] mov [rsp + 8*rcx], rax dec rcx jnz roll1 push rbx jmp _next b_depth = 0x38 bcmd_depth: mov rax, init_stack sub rax, rsp shr rax, 3 push rax jmp _next b_get = 0x40 bcmd_get: pop rcx push [rcx] jmp _next b_set = 0x41 bcmd_set: pop rcx pop rax mov [rcx], rax jmp _next b_get8 = 0x42 bcmd_get8: pop rcx movsx rax, byte ptr [rcx] push rax jmp _next b_set8 = 0x43 bcmd_set8: pop rcx pop rax mov [rcx], al jmp _next b_get16 = 0x44 bcmd_get16: pop rcx movsx rax, word ptr [rcx] push rax jmp _next b_set16 = 0x45 bcmd_set16: pop rcx pop rax mov [rcx], ax jmp _next b_get32 = 0x46 bcmd_get32: pop rcx movsx rax, dword ptr [rcx] push rax jmp _next b_set32 = 0x47 bcmd_set32: pop rcx pop rax mov [rcx], eax jmp _next # 0= b_zeq = 0x50 bcmd_zeq: pop rax or rax, rax jnz rfalse rtrue: push -1 jmp _next rfalse: push 0 jmp _next # 0< b_zlt = 0x51 bcmd_zlt: pop rax or rax, rax jl rtrue push 0 jmp _next # 0> b_zgt = 0x52 bcmd_zgt: pop rax or rax, rax jg rtrue push 0 jmp _next # = b_eq = 0x53 bcmd_eq: pop rbx pop rax cmp rax, rbx jz rtrue push 0 jmp _next # < b_lt = 0x54 bcmd_lt: pop rbx pop rax cmp rax, rbx jl rtrue push 0 jmp _next # > b_gt = 0x55 bcmd_gt: pop rbx pop rax cmp rax, rbx jg rtrue push 0 jmp _next # <= b_lteq = 0x56 bcmd_lteq: pop rbx pop rax cmp rax, rbx jle rtrue push 0 jmp _next # >= b_gteq = 0x57 bcmd_gteq: pop rbx pop rax cmp rax, rbx jge rtrue push 0 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_qnbranch8 = 0x14 bcmd_qnbranch8: pop rax or rax, rax jz bcmd_branch8 inc r8 jmp _next b_qnbranch16 = 0x15 bcmd_qnbranch16:pop rax or rax, rax jz bcmd_branch16 add r8, 2 jmp _next b_bad = 0x00 bcmd_bad: mov rax, 1 # системный вызов № 1 - sys_write mov rdi, 1 # поток № 1 — stdout mov rsi, offset msg_bad_byte # указатель на выводимую строку mov rdx, msg_bad_byte_len # длина строки syscall # вызов ядра mov rax, 60 # системный вызов № 1 - sys_exit mov rbx, 1 # выход с кодом 1 syscall # вызов ядра b_bye = 0x01 bcmd_bye: mov rax, 1 # системный вызов № 1 - sys_write mov rdi, 1 # поток № 1 — stdout mov rsi, offset msg_bye # указатель на выводимую строку mov rdx, msg_bye_len # длина строки syscall # вызов ядра mov rax, 60 # системный вызов № 60 - sys_exit mov rdi, 0 # выход с кодом 0 syscall # вызов ядра b_type = 0x80 bcmd_type: mov rax, 1 # системный вызов № 1 - sys_write mov rdi, 1 # поток № 1 - stdout pop rdx pop rsi push r8 syscall # вызов ядра pop r8 jmp _next
Итог
Теперь у нас есть довольно приличное ядро байт-команд: все основные арифметические, стековые операции, операции сравнения, работа с памятью, переменные. Так же, уже есть вывод чисел, полностью реализованный на байт-коде. Все готово, что бы сделать интерпретатор, чем и займемся в следующей статье!
Всех с наступающим Новым годом!
Критика — приветствуется! :)
Продолжение: Байт-машина для форта (и не только) по-индейски (часть 3)
