Куайн для uxn

Куайн/квайн/quine [1] это программа, которая в результате своей работы даёт точную копию своего исходного кода. Не знаю писал ли кто-нибудь их для виртуальной машины uxn [2], но мне захотелось создать свой вариант.

UPD 2025-09-03: Для виртуальной машины uxn есть куайн от авторов: https://wiki.xxiivv.com/site/uxntal_labyrinth.html

Сразу оговорюсь, что у меня часто начинает закипать мозг, когда я анализирую чужие куайны. Скорее всего моё решение не будет сильно оптимальным. Но зато, надеюсь, будет достаточно простым и наглядным.

Введение

В качестве инструментария я использую утилиты uxnasm и uxncli из репозитория ~rabbits/uxn [3]. Утилита uxnasm служит для превращения ассемблерных tal-файлов в бинарные rom-файлы, а uxncli запускает собранные rom-файлы. Соответственно, наша задача получить такой tal-файл, который после компиляции в rom-файл (и его запуска) выдаст в консоль собственный исходный код.

У tal-файлов есть одна интересная особенность: помимо мнемоник инструкций и синтаксиса меток в tal-файле можно напрямую писать байт-код. Шестнадцатеричные байты или шестнадцатеричные двухбайтовые слова. То есть (буквально) весь исходник может состоять из байткода.

Возьмём для примера простенькую программу, выводящую символ a (текст в круглых скобках это игнорируемые комментарии):

LIT "a ( инструкция LIT помещает значение байта, 
расположенного в следующей за инструкцией ячейке памяти, в стек )
LIT 18 ( заносим в стек адрес устройства консольного вывода,
подробности о Console/write в [4] )
DEO ( инструкция вывода в устройство, 
которая считает заданные параметры со стека )

И всё это можно записать в tal-файле байткодом:

80 61
80 18
17

Где: 80 это байткод инструкции LIT, 61 это шестнадцатеричные представление символа a, а 17 это инструкция DEO.

Если мы скомпилируем оба листинга, то собранные rom файлы будут идентичны. Это, фактически, означает что для дизассемблирования uxn достаточно написать дампер шестнадцатеричных байт. Распечатав каждый байт своей программы в шестнадцатеричном представлении мы получим валидный tal-файл (то есть файл исходного кода uxn), который можно будет собрать и запустить. В этом и заключается моя идея простого куайна для uxn.

Вариант 1: побайтовый дамп себя

Идея, думаю, понятна: нужно просто реализовать дамп каждого байта прямо из памяти. Вначале я буду писать генератор, используя обычные мнемоники команд. Затем, после одной итерации (компиляции rom-файла и генерации с его помощью нового tal-файла), я получу куайн.

В качестве основы кода форматирования байта в шестнадцатеричное представление я взял функции @print-byte-hex и @print-nibble-hex из uxn/projects/library/helpers.tal [5]. Сейчас этот код удалён из свежей версии репозитория. Не знаю почему, возможно перенесен куда-то. Вокруг вызова этой функции я написал цикл, который извлекает очередной байт из памяти (начиная с 0x0100) и вызывает функцию печати шестнадцатеричного представления.

Генератор в моём случае выглядит так:

    ( rom-файл загружается по адресу 0x0100
    поэтому начинаем именно с него )
    #0100

@detal/next
    DUP #31 ( тут захардкожен размер скопилированного rom-файла )
    NEQ ?{ BRK } ( если дошли до конца, то выходим )
    INC2k SWP2 LDA ( берем текущий адрес,
    сохраняем в стеке адрес следующего байта памяти и
    загружаем байт по текущему адресу )
    ( печатаем шестнадцатеричное представление текущего байта,
    печатаем пробел (0x20) и переходим к следующей итерации )
    ,print-byte-hex JSR #2018 DEO !detal/next

@print-byte-hex
    DUP #04 SFT ,print-nibble-hex JSR
    #0f AND
    ( jump to print-nibble-hex )

@print-nibble-hex
    #30 ADD DUP #39 GTH #27 MUL ADD #18 DEO
    JMP2r

В результате запуска получаем tal-файл куайна:

a0 01 00 06 80 31 09 20 00 01 00 a1 24 14 80 07 0e a0 20 18 17 40 ff eb 06 80 04 1f 80 03 0e 80 0f 1c 80 30 18 06 80 39 0a 80 27 1a 18 80 18 17 6c 

Если перенаправить это в файл q1.tal, то можно удостовериться в том, что файл действительно является куайном для uxn:

$ uxnasm q1.tal q1.rom && uxncli q1.rom > q2.tal && sha256 q1.tal q2.tal
Assembled q1.rom in 49 bytes(0.08% used), 0 labels, 0 macros.
SHA256 (q1.tal) = 2c8b15d3feb50cbae73797b17ae171be6ab14c0f1e26914449410aa3e988506b
SHA256 (q2.tal) = 2c8b15d3feb50cbae73797b17ae171be6ab14c0f1e26914449410aa3e988506b

Это и есть первый вариант куайна, tal-файл которого занимает 147 байт, а скомпилированный rom-файл 49 байт.

Вариант 2: уменьшаем tal-файл

В первом варианте на каждый байт мы тратим 3 символа: два символа шестнадцатеричного представления байта и пробел. Но синтаксис tal допускает не только байты, но и двухбайтовые слова. Получается пять символов на два байта вместо шести. Но придётся немного "раздуть" код:

    #0100

@detal/next
    DUP #36 ( rom file size ) NEQ ?{ BRK }
    INC2k INC2 SWP2 LDA2 SWP ( теперь каждую итерацию адрес нужно инкрементировать дважды,
    а загружать нужно двухбайтовое слово. Сначала выводится старший байт, 
    поэтому нужно перемешать загруженные байты )
    ,print-byte-hex JSR
    ,print-byte-hex JSR
    #2018 DEO
    !detal/next

@print-byte-hex
    DUP #04 SFT ,print-nibble-hex JSR
    #0f AND
    ( jump to print-nibble-hex )

@print-nibble-hex
    #30 ADD DUP #39 GTH #27 MUL ADD #18 DEO
    JMP2r

Такой генератор порождает более короткий вариант куайна (q1.tal):

a001 0006 8036 0920 0001 00a1 2124 3404 800a 0e80 070e a020 1817 40ff e606 8004 1f80 030e 800f 1c80 3018 0680 390a 8027 1a18 8018 176c
$ uxnasm q1.tal q1.rom && uxncli q1.rom > q2.tal && sha256 q1.tal q1.tal
SHA256 (q1.tal) = 7b5148adf104f6b4c62396a747a5c1d8e100c5fc5b8fba21e7ede6bc61bc84b6
SHA256 (q2.tal) = 7b5148adf104f6b4c62396a747a5c1d8e100c5fc5b8fba21e7ede6bc61bc84b6

Естественно, что таким образом уменьшается только размер tal-файла (135 байт), а размер скомпилированного rom-файла немного подрос (54 байта).

Ссылки

[1] Куайн (Википедия)
[2] 100R - uxn
[3] Varvara Ordinator, written in C99(SDL2)
[4] Varvara is a computer system for Uxn
[5] uxn/projects/library/helpers.tal