Este repositório contém a implementação de uma simples máquina virtual, juntamente com um driver que lerá um programa de um arquivo e o executará através dessa máquina virtual.
Além do intérprete da máquina virtual, você também encontrará:
Esta máquina virtual em particular é intencionalmente simples, mas apesar de ter sido implementada de maneira legível. ("Simplicidade" aqui significa que apoiamos apenas um pequeno número de instruções, e os registros que a CPU virtual possui pode armazenar strings e números inteiros, mas não valores de ponto flutuante.) Esta máquina virtual específica é baseada em registro, com dez registros que podem ser usados para armazenar strings ou valores inteiros.
Como o compilador e o decompilador estão escritos em Perl, eles não precisam de tratamento especial.
O interpretador, escrito em C, pode ser construído assim:
$ make
Isso gerará simple-vm e embedded a partir do conteúdo de SRC/.
Implementar uma máquina virtual básica, como esta, é um problema bem compreendido:
case longa.halt ou exit da instrução.As principais complicações da escrita de uma máquina virtual são:
goto repeat " NÃO " 0x10 0x03 0x00 ". Esta máquina virtual em particular contém apenas algumas primitivas, mas inclui o suporte para rótulos, loop, saltos condicionais, chamadas e retornos. Há também uma pilha que pode ser usada para armazenar valores temporários e usada para manuseio call / ret .
O manuseio de rótulos nessa implementação talvez seja digno de nota, porque muitas máquinas virtuais simples/demonstrações não as lidam.
Para apoiar o salto para rótulos que não foram necessariamente definidos, nosso compilador mantém uma lista em execução de todos os rótulos (ou seja, possíveis destinos de salto) e quando encontra uma instrução de salto, ou algo mais que se refere a um rótulo, ele gera um espaço reservado, como:
JUMP 0x0000x10 0x00 0x00 pois a instrução de salto é definida como 0x10 .Depois que a compilação estiver concluída, todos os alvos deveriam ter sido descobertos e o compilador é gratuito para atualizar os bytecodes gerados para preencher os destinos apropriados.
NOTA : Em nossas máquinas virtuais, todos os saltos são absolutos, para que pareçam "
JUMP 0x0123" em vez de "JUMP -4" ou "JUMP +30".
Nota : O mesmo se aplica a outras instruções que lidam com os rótulos, como armazenar o endereço de um rótulo, fazer uma chamada etc.
Esta máquina virtual é projetada principalmente como uma experiência de aprendizado, mas é construída com a idéia de incorporar em mente.
O binário padrão simple-vm , que lerá os códigos de um arquivo e os interpretará, tem menos de 40 mil de tamanho.
Como o processamento de códigos de operações binárias é tratado por meio de uma tabela de expedição, é trivialmente possível adicionar seus próprios códigos opcíficos ao sistema, o que permitiria executar programas pequenos compilados e eficientes que podem voltar ao seu aplicativo quando desejarem.
Há um exemplo de definição de um código de opção personalizado no arquivo src/embedded.c . Este exemplo define um código OPCODE personalizado 0xCD e executa um pequeno programa que usa esse código de operação para fins de demonstração:
$ ./embedded
[stdout] Register R01 => 16962 [Hex:4242]
Custom Handling Here
Our bytecode is 8 bytes long
Existem vários tipos de instrução implementados:
As instruções são bastante básicas, pois esse é apenas um brinquedo, mas adicionar novas não é difícil e as primitivas disponíveis são razoavelmente úteis como são.
A seguir, são apresentados exemplos de todas as instruções:
:test
:label
goto 0x44ff # Jump to the given address
goto label # Jump to the given label
jmpnz label # Jump to label if Zero-Flag is not set
jmpz label # Jump to label if Zero-Flag is set
store #1, 33 # store 33 in register 1
store #2, "Steve" # Store the string "Steve" in register 1.
store #1, #3 # register1 is set to the contents of register #3.
exit # Stop processing.
nop # Do nothing
print_int #3 # Print the (integer) contents of register 3
print_str #3 # Print the (string) contents of register 3
system #3 # Call the (string) command stored in register 3
add #1, #2, #3 # Add register 2 + register 3 contents, store in reg 1
sub #1, #2, #3 # sub register 2 + register 3 contents, store in reg 1
mul #1, #2, #3 # multiply register 2 + register 3 contents, store in reg 1
concat #1, #2,#3 # store concatenated strings from reg2 + reg3 in reg1.
dec #2 # Decrement the integer in register 2
inc #2 # Increment the integer in register 2
string2int #3 # Change register 3 to have a string from an int
is_integer #3 # Does the given register have an integer content?
int2string #3 # Change register 3 to have an int from a string
is_string #3 # Does the given register have a string-content?
cmp #3, #4 # Compare contents of reg 3 & 4, set the Z-flag.
cmp #3, 42 # Compare contents of reg 3 with the constant 42. sets z.
cmp #3, "Moi" # Compare contents of reg 3 with the constant string "Moi". sets z.
peek #1, #4 # Load register 1 with the contents of the address in #4.
poke #1, #4 # Set the address stored in register4 with the contents of reg1.
random #2 # Store a random integer in register #2.
push #1 # Store the contents of register #1 in the stack
pop #1 # Load register #1 with the contents of the stack.
call 0xFFEE # Call the given address.
call my_label # Call the defined label
ret # Return from a called-routine.
O programa a seguir produzirá infinitamente uma string:
:start
store #1, "I like loops"
print_str #1
goto start
Este programa inclui primeiro a string " I like loops " no Registro 1 e depois imprime que se registra, antes de voltar ao início do programa.
Para realmente compilar este programa no ByteCode Run:
$ ./compiler ./examples/simple.in
Isso produzirá um arquivo de saída cheio de códigos binários no arquivo simple.raw :
$ od -t x1 -An ./examples/simple.raw
01 01 0c 49 20 6c 69 6b 65 20 6c 6f 6f 70 73 03
01 06 00 00
Agora podemos executar essa série de opcodes:
./simple-vm ./examples/simple.raw
Se você deseja depurar a execução, execute:
DEBUG=1 ./simple-vm ./examples/simple.raw
Existem mais exemplos armazenados abaixo dos examples/ subdiretório neste repositório. Os exemplos de arquivos/quine.in fornecem um bom exemplo de vários recursos - ele produz seus próprios códigos de operações.
Se você deseja se destacar com a AFL, deve achar isso bastante direto:
afl , conforme as instruções.CC=afl-gcc no Makefile .Você pode compilar cada uma de nossas amostras agrupadas como assim:
cd examples/
for i in *.in; do ../compiler $i; done
cd ..
Isso compilará examples/*.in examples/*.raw . Coloque os que estão em um diretório e comece o seu docer:
mkdir samples/
cp examples/*.raw samples/
Agora você tem ./samples/ contendo apenas programas compilados. Você pode sofrer mutações/permitir esses exemplos sob o controle do Fuzzfer com uma linha de comando como esta:
export FUZZ=1
afl-fuzz -i ./samples/ -o results/ ./simple-vm @@ 16384
Nota : Aqui, limitamos cada uma de execução não mais que 16384 instruções. Isso garantirá que os programas não tenham loops infinitos.
Definimos a variável ambiental FUZZ como conter 1 apenas para desativar o uso da função system() . O que pode remover acidentalmente seu diretor de casa, formatar sua unidade ou me enviar uma doação!
Uma porta de Golang do compilador e intérprete de máquina virtual está disponível no repositório a seguir:
Além disso, você pode encontrar uma máquina virtual real dentro do mecanismo de script incorporado que escrevi, também para Golang. Nesse caso, uma linguagem de script é analisada e convertida em uma série de instruções de bytecode, que são executadas por uma máquina virtual. Semelhante a este projeto, mas as operações de bytecode são mais complexas e de alto nível:
Se você estiver interessado em compiladores e intérpretes, geralmente também poderá aproveitar esses outros projetos: