@Catmengi
Юный человек который учит язык Си.

Как виртуальные машины исполняют код и как правильно это реализовать?

Пишу свою машину на основе стека. Сообствено вопрос: моя машина хранит код щас примерно в таком формате
Instructions[max_programm_size][5]
Нулевой подмасив это инструкция.
Остальные от 1 до 4 это аргументы инструкции (если его (аргумента)нет то 0 или любое число т.к инструкция не работает с ним) такая реализация костыль или не костыль вот в чем вопрос? Если является то как сделать нормальную реализацию инструкций и их аргументов(желательно через одно число,но понятную нубу в C)? Пока что в VM есть:
Jump
If-else(одна инструкция)(потом разделю на if-else / if)
Add
Mult
Divide
Syscall(printf)
Plus
Minus
Push
Переменные через большой и интересный костыль(с одной особенностью ввиде того что можно создать несколько одинаковых по номеру переменных но с разным значение,но чтение/изменение видит только первую из них).
И сообственно сам не динамический стек(тупо массив с размером ввиде define).
Кому нужен код чтобы покопаться в нем,я могу его скинуть.
P.s за пунктуацию или ее отсутвие простите писал "наскоряк".
  • Вопрос задан
  • 153 просмотра
Пригласить эксперта
Ответы на вопрос 2
WNeZRoS
@WNeZRoS
Если вопрос при виртуальные машины по типу тех что используется для исполнения Java или C# кода, то инструкции в них гораздо проще. В CIL например, у большинства инструкций нет параметров, они просто берут нужные данные с конца стека. А когда параметры есть - он один: значение или токен переменной/функции.

Можно посмотреть как C# преобразуется в инструкции виртуальной машины на sharplab.io, сопоставить и понять как оно выполняется.

Например
IL_0000: ldarg 1 // кладёт в стек первый аргумент
IL_0001: ldarg 2 // кладёт в стек второй аргумент
IL_0002: sub     // достаёт из стека два последних значения и отнимает последний от пред последнего (т.е. будет arg.2 - arg.1), результат складывается в стек
IL_0003: brtrue.s IL_0011 // достаёт из стека значение, если оно не 0, переходит к инструкции IL_0011, если 0 исполнения идёт дальше на I_0005

IL_0005: ldstr "zero" // загружает в стек строку "zero"
IL_000a: call void System.Console::WriteLine(string) // вызывает вывод в консоль, из стека достаются N нужных значений для параметров (в данном случае 1), void функция ничего в стек не добавляет
IL_000f: br.s IL_001b // переход (jump) к IL_001b

IL_0011: ldstr "not zero" // загружает в стек строку "not zero"
IL_0016: call void System.Console::WriteLine(string) // вызывает вывод в консоль, из стека достаётся 1 значения для параметра 

IL_001b: ldstr "done" // загружает в стек строку ...
IL_0020: call void System.Console::WriteLine(string) // ...


Пример реализации простой виртуальной машины на основе примера выше

(Код написан так чтобы работал, правильнее писать более безопасно и читабельнее)

#include <stdio.h>

enum Opcode {
    OP_LOAD_ARGUMENT,
    OP_LOAD_STRING,
    OP_SUBTRACT,
    OP_GOTO,
    OP_GOTO_IF_TRUE,
    OP_CALL,
    OP_RETURN
};

union Argument {
    int value;
    const void* pointer;
};

struct Instruction { 
    Opcode opcode; 
    Argument arg; 
};

void run_vm(const Instruction* instructions, const Argument* args) {
    Argument stack[16];
    
    Argument* stack_pointer = &stack[0];
    int address = 0;

    while(true) {
        const Instruction instruction = instructions[address];
        address++;

        switch(instruction.opcode) {
            case OP_RETURN:
                return;
            
            case OP_LOAD_ARGUMENT:
                *stack_pointer = args[instruction.arg.value];
                stack_pointer++;
                break;

            case OP_LOAD_STRING:
                *stack_pointer = instruction.arg;
                stack_pointer++;
                break;

            case OP_SUBTRACT:
            {
                stack_pointer--;
                int b = stack_pointer->value;
                
                stack_pointer--;
                int a = stack_pointer->value;

                stack_pointer->value = a - b;
                stack_pointer++;
                break;
            }

            case OP_GOTO:
                address = instruction.arg.value;
                break;

            case OP_GOTO_IF_TRUE:
                stack_pointer--;
                if(stack_pointer->value != 0)
                    address = instruction.arg.value;
                break;

            case OP_CALL:
                void (* fn)(const void*) = (void (*)(const void*))instruction.arg.pointer;
                stack_pointer--;
                fn(stack_pointer->pointer);
                break;
        }
    }
}

void print(const char* text) { printf("%s\n", text); }

int main(int argc, char** argv) {
    const Instruction instructions[] = {
        /* 0 */ { OP_LOAD_ARGUMENT, { 0 } },
        /* 1 */ { OP_LOAD_ARGUMENT, { 1 } },
        /* 2 */ { OP_SUBTRACT, {} },
        /* 3 */ { OP_GOTO_IF_TRUE, { 0x7 } },
        /* 4 */ { OP_LOAD_STRING, { .pointer = "zero" } },
        /* 5 */ { OP_CALL, { .pointer = (void*)&print } }, // функции надо где-то регистрировать, чтобы знать сколько у них параметров и какого они типа
        /* 6 */ { OP_GOTO, { 0x9 } },
        /* 7 */ { OP_LOAD_STRING, { .pointer = "not zero" } },
        /* 8 */ { OP_CALL, { .pointer = (void*)&print } },
        /* 9 */ { OP_LOAD_STRING, { .pointer = "done" } },
        /* A */ { OP_CALL, { .pointer = (void*)&print } },
        /* B */ { OP_RETURN, {} }
    };

    const Argument args[] = { 
        { 100500 },
        { 777 }
     };

    run_vm(instructions, args);
    return 0;
}



Виртуальные машины для процессоров можно делать по такому же принципу, но они будут медленные и структура инструкций ограничена архитектурой процессора.
Ответ написан
Комментировать
@vadimr
Ну это нормальный известный вариант – хранить представление кода в тетрадах. В основном, правда, так делают в компиляторах на промежуточном этапе, а не в виртуальных машинах, так как трёхадресная структура команды приводит к лишним издержкам на декодирование и обращение к оперативной памяти. Но для демонстрации технологии потянет. Вы же не собираетесь, наверное, ставить рекорды производительности.

с одной особенностью ввиде того что можно создать несколько одинаковых по номеру переменных но с разным значение,но чтение/изменение видит только первую из них


Это размещение в памяти типа controlled, практиковалось в качестве возможного варианта в языке PL/I.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы