Мои эксперименты с bytecode виртуальной машиной
Решил поиграться с написанием собственной виртуальной машины для выполнения bytecode. Но делать какой-нибудь очередной эмулятор CHIP-8 или GameBoy показалось неинтересным, поэтому замахнулся на более масштабный проект: аналог DOSboxа. Но не под x86, а под под выдуманную 32-битную архитектуру (как потом узнал, это называется «fantasy console»), которая похожа на так недокументированный "unreal mode" в 386 процессоре, но с рядом отличий: фиксированный размер команды (16 бит), четыре не перекрывающихся и неизменяемых сегмента (данные, стек, видеопамять и буферный сегмент для ввода/вывода), 16, а не 8 регистров (как в x86_64), и высокоуровневые интерфейсы для работы с файлами, сетью и звуком через syscall, а также раздельные стеки для данных и вызовов (адресов возврата), причём стек вызовов в память ВМ вообще не отображается (на мой взгляд, это поможет защититься от атак через buffer overflow).
Замысел был такой: сделать ВМ, под которую можно писать такие же компактные и простые TUI или GUI программы, как это было в DOS, но при этом с использованием 32-битной адресации. В общем, всё то, о чём я мечтал в школьные годы, когда впервые столкнулся с protected mode. А ещё, в идеале, сделать эту ВМ такой же переносимой, как DOSBox, так как на мой взгляд, именно DOSbox — лучшее решение в плане переносимости, когда нужно сделать что-то, что работает абсолютно везде, включая экзотику типа MenuetOS и HaikuOS (и вроде даже под малоизвестную HelenOS он тоже есть).
Писать решил на чистом C, и сегодня набросал базовую версию, которая позволяет выполнить несколько команд — погонять данные между регистрами и памятью, просуммировать и выйти. Ничего особо сложного в этом не было, кроме того, что в какой-то момент вместо & по привычке написал && и потом больше часа потратил на отладку, и в процессе ещё узнал, что под Linux всё довольно плохо с GUI-отладчиками: найти что-то похожее на привычный Tubro Debugger сложно, а то, что есть — это обёртки над gdb разной степени кривизны. Ну и в процессе понял, что лучше бы всё-таки оформил код в виде класса на C++, код бы чище получился — не нужно было бы всё время таскать из функции в функцию указатель на состояние вирт. машины.
Но вот дальше стали возникать вопросы, ответить на которые оказалось не так-то просто.
Первый — как загружать непосредственные значения (immediate values) в регистры, если длина команды — 16 бит, а регистра — 32. Не придумал ничего лучше, чем делать это побайтово. Но и тут всё оказалось непросто: если во втором байте команды лежит само значение, 2 бита уходит на указание номера байта и 4 — номера регистра, то получается, что команда MOV регистр,значение съедает сразу четверть пространства команд! А если учесть, что есть ещё MOV сегмент:[регистр1],регистр2 и MOV регистр1,сегмент:[регистр2], где номер сегмента тоже указывается в первом байте, это забирает ещё 8 значений из пространства команд. И это я ещё всякие битовые и арифметические операции собирался делать только в режиме регистр-регистр! Можно, конечно, сделать так: раз уж у нас RISC-подобная архитектура, то всегда загружать такие значения данные в какой-то конкретный (например, последний, R15), а потом уже делать MOV в другие.
Второй — это нужно ли предусматривать возможность обращения к сокращённым регистрам (наподобие AX, AH, AL в x86). С одной стороны, если сделать, это ещё сильно сократит пространство команд, с другой — без таких операций многое усложнится. Есть можно, конечно, сделать отдельную команду, которая позволит устанавливать маску или длину регистра (либо на 1 следующий такт, либо постоянно, тут тоже надо подумать), но это кажется костылём и путём к повторению неудачных решений из x86.
Третий тесно связан со вторым — это вопрос memory alignment: нужно ли делать возможность обращаться к памяти побайтно, а не только по адресам, кратным четырём, и что делать при попытке считать 32-битное слово с нарушением выравнивания.
Четвёртый — это нужны ли аналоги строковых операций из x86 (всякие MOVSB, STOSB и т.п.) или лучше сделать вызов через syscall.
Пятый — это как быть с обработкой событий: то ли предусматривать аналог таблицы прерываний, которые вызываются по их наступлению, то ли предусмотреть вызов для получения события через syscall. То ли вообще смешанный вариант: сделать возможность маскирования, и если прерывание замаскировано, информация о нём уходит в список событий, доступных через syscall, если нет — вызывается обработчик из таблицы прерываний.
Но в любом случае, опыт написания собственной ВМ — очень интересный!
Ребята, давайте жить спокойно!
У вас нет прав для отправки сообщений в эту тему.
