Este repositorio contiene la implementación para una máquina virtual simple , junto con un controlador que leerá un programa de un archivo y lo ejecutará a través de esa máquina virtual.
Además del intérprete de la máquina virtual también encontrará:
Esta máquina virtual en particular es intencionalmente simple, pero a pesar de eso, con suerte se implementa de manera legible. ("Simplicity" aquí significa que solo admitimos un pequeño número de instrucciones, y los registros de la CPU virtual poseen pueden almacenar cadenas e enteros, pero no valores de punto flotante). Esta máquina virtual particular está basada en registros, con diez registros que pueden usarse para almacenar cadenas o valores enteros.
Debido a que el compilador y el descompilador están escritos en Perl, no necesitan un tratamiento especial.
El interpretador, escrito en C, se puede construir así:
$ make
Esto generará simple-vm e embedded a partir del contenido de Src/.
Implementar una máquina virtual básica, como esta, es un problema bastante bien entendido:
case largo.halt o exit de instrucción.Las principales complicaciones de escribir una máquina virtual son:
goto repeat " no " 0x10 0x03 0x00 ". Esta máquina virtual particular contiene solo unas pocas primitivas, pero incluye el soporte de etiquetas, bucle, saltos condicionales, llamadas y devoluciones. También hay una pila que se puede utilizar para almacenar valores temporales y utilizar para el manejo call / ret .
El manejo de etiquetas en esta implementación es quizás digna de mención, porque muchas máquinas virtuales simples/de demostración no las manejan en absoluto.
Para admitir saltar a las etiquetas que no se han definido necesariamente, pero nuestro compilador mantiene una lista en ejecución de todas las etiquetas (es decir, posibles destinos de salto) y cuando se encuentra con una instrucción de salto, o algo más que se refiere a una etiqueta, genera un consejo de positional, como:
JUMP 0x0000x10 0x00 0x00 ya que la instrucción de salto se define como 0x10 .Después de completar la compilación, todos los objetivos deberían haberse descubierto y el compilador es libre de actualizar los bytecodes generados para completar los destinos apropiados.
Nota : En nuestras máquinas virtuales, todos los saltos son absolutos, por lo que pueden parecer "
JUMP 0x0123" en lugar de "JUMP -4" o "JUMP +30".
Nota : Lo mismo se aplica para otras instrucciones que manejan etiquetas, como almacenar la dirección de una etiqueta, hacer una llamada, etc.
Esta máquina virtual está diseñada principalmente como una experiencia de aprendizaje, pero está construida con la idea de incrustar en mente.
El binario simple-vm estándar, que leerá los códigos de operación de un archivo e interpretará los de menos de 40k de tamaño.
Debido a que el procesamiento de los códigos de operación binarios se maneja a través de una tabla de despacho, es trivialmente posible que agregue sus propios códigos de operación específicos de la aplicación al sistema que le permitiría ejecutar pequeños programas compilados y eficientes que pueden volver a llamar a su aplicación cuando lo deseen.
Hay un ejemplo de definir un código de operación personalizado en el archivo src/embedded.c . Este ejemplo define un código de operación personalizado 0xCD y ejecuta un pequeño programa que utiliza ese código de operación para fines de demostración:
$ ./embedded
[stdout] Register R01 => 16962 [Hex:4242]
Custom Handling Here
Our bytecode is 8 bytes long
Hay varios tipos de instrucciones implementados:
Las instrucciones son bastante básicas, ya que esto es solo un juguete, pero agregar otras nuevas no es difícil y las primitivas disponibles son razonablemente útiles.
Los siguientes son ejemplos de todas las instrucciones:
: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.
El siguiente programa simplemente emitirá sin cesar una cadena:
:start
store #1, "I like loops"
print_str #1
goto start
Este programa primero almacena la cadena " I like loops " en el Registro 1, luego imprime ese registro, antes de volver al inicio del programa.
Para compilar este programa en la ejecución de Bytecode:
$ ./compiler ./examples/simple.in
Esto producirá un archivo de salida lleno de opcodes binarios en el archivo 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
Ahora podemos ejecutar esa serie de códigos de operación:
./simple-vm ./examples/simple.raw
Si desea depurar la ejecución, ejecute:
DEBUG=1 ./simple-vm ./examples/simple.raw
Hay más ejemplos almacenados debajo de los examples/ subdirectorio en este repositorio. El archivo ejemplos/quine.in proporciona un buen ejemplo de varias características: genera sus propios códigos de operación.
Si desea probar con Fuzz con AFL, debería encontrarlo bastante directo:
afl en sí, según las instrucciones.CC=afl-gcc en el Makefile .Puede compilar cada una de nuestras muestras agrupadas como así:
cd examples/
for i in *.in; do ../compiler $i; done
cd ..
Esto compilará examples/*.in en examples/*.raw . Coloquelos en un directorio y comience su fuzzer:
mkdir samples/
cp examples/*.raw samples/
Ahora tienes ./samples/ contiene solo programas compilados. Luego puede mutar/permitir esos ejemplos bajo el control del fuzzer con una línea de comandos como esta:
export FUZZ=1
afl-fuzz -i ./samples/ -o results/ ./simple-vm @@ 16384
Nota : Aquí coronamos cada una ejecución para ejecutar no más de 16384 instrucciones. Esto asegurará que los programas no tengan bucles infinitos en ellos.
Establecemos la FUZZ de la variable ambiental para contener 1 únicamente para deshabilitar el uso de la función system() . ¡Lo que podría eliminar accidentalmente su directorio doméstico, formatear su impulso o enviarme una donación!
Un puerto de Golang del compilador e intérprete de máquina virtual está disponible en el siguiente repositorio:
Además de eso, puede encontrar una máquina virtual real dentro del motor de secuencias de comandos incrustado que escribí, también para Golang. En ese caso, un lenguaje de secuencias de comandos se analiza y se convierte en una serie de instrucciones de Bytecode, que son ejecutadas por una máquina virtual. Similar a este proyecto, pero las operaciones de Bytecode son más complejas y de alto nivel:
Si está interesado en compiladores e intérpretes, generalmente también puede disfrutar de estos otros proyectos: