Hier möchte ich einen Crashkurs über die Programmierung von virtuellen Maschinen beginnen. Auch ich habe bei der L1VM von Null an angefangen. Hier ist jetzt der Code von der ZeroVM für den Kurs. Auch hier fangen wir klein an. Hier das C Programm in Teilen Schritt für Schritt:

// zeroVM - VM crash course
// How to build a simple virtual machine
// Stefan Pietzonke 2019
//

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <signal.h>
#include <stdint.h>
#include <ctype.h>
#include <errno.h>
#include <string.h>

typedef unsigned char           U1;		/* UBYTE   */
typedef int16_t                 S2;     /* INT     */
typedef uint16_t                U2;     /* UINT    */
typedef int32_t                 S4;     /* LONGINT */

typedef long long               S8;     /* 64 bit long */
typedef double                  F8;     /* DOUBLE */

Am Beginn werden die benötigten Header mit “#include” eingefügt. Danach mit “typedef” werden die Variablentypen, die wir später brauchen werden, festgelegt. Das “U” in der Typbezeichnung steht für “unsigned” (U1). Also “U1” für eine Ganzzahl im Bereich von 0 - 255.

Das “S” beim Typ steht für signed, also eine Zahl die auch in den negativen Bereich reicht.

In der VM werden wir mit 64 Bit Genaugkeit rechnen.

#define DEBUG 1

#define EXE_NEXT(); ep = ep + eoffs; goto *jumpt[code[ep]];

/* opcodes */
#define PUSHB	0
#define PULLB	1

#define LOADB 	2
#define ADDI	3

#define PRINTI	4

#define PEXIT	5

//	space for code and data
unsigned char code[1024];
unsigned char data[1024];

Jetzt wird mit der Zeile “#define” ein Makro erstellt, das wir später für die Ausführung in der VM brauchen werden. Wir fügen das Makro später mit “EXE_NEXT();” in den Code ein!

Und es werden die internen Werte für die Opcodes (Befehle) der VM festgelegt. Dann werden noch die Variablen für Code und Daten erstellt.

int main (int ac, char *av[])
{
	S8 ep;				// execution pointer
	S8 eoffs = 0;		// number of bytes to skip to get next opcode

	S8 regi[256];			// integer registers 64 bit
	S8 arg1, arg2, arg3;	// opcode arguments

	S8 cpu_core = 0;

	printf ("zeroVM\n");

	// data bytes 23 and 42
	data[0] = 23;
	data[1] = 42;

	// code
	// load byte from data_size
	code[0] = LOADB;
	code[1] = 0;	// data ptr
	code[2] = 0;	// offset
	code[3] = 0;	// target register

	// load byte from data
	code[4] = LOADB;
	code[5] = 1;
	code[6] = 0;
	code[7] = 1;

	code[8] = ADDI;
	code[9] = 0;	// register 0: 23
	code[10] = 1;	// register 1: 42
	code[11] = 2;	// target register 2: = 65

	code[12] = PRINTI;
	code[13] = 2;	// register 2: 65

	code[14] = PEXIT;

	// execution bytecode pointer
	ep = 0;

Hier geht es mit der “Main” Funktion weiter. Es werden die 64 Bit Register deklariert. Die Zeilen:

data[0] = 23;
data[1] = 42;

Legen die Werte für unsere zwei Variablen fest. Es folgt der Bytecode für das Programm das wir ausführen möchten:

// load byte from data_size
code[0] = LOADB;
code[1] = 0;	// data ptr
code[2] = 0;	// offset
code[3] = 0;	// target register

Hier wird mit dem “LOADB” Opcode die Variable an der Adresse 0 in das Register 0 geladen. Also 23 steht jetzt in Register 0.

Der “ADDI” Befehl addiert die Register 0 und 1. Das Ergebnis wird in Register 2 gespeichert.

Mit “PRINTI” wird das Register 2 angezeigt. Und “PEXIT” beendet das Programm.

// jumptable for indirect threading execution
static void *jumpt[] =
{
	&&pushb, &&pullb, &&loadb, &&addi, &&printi, &&pexit
};

printf ("running test program...\n\n");

// fetch first bytecode
EXE_NEXT();

Jetzt kommt ein besonderer Teil: die Sprungtabelle der VM. Beim GCC und Clang C Compiler kann man mit Hilfe des “labels as values” Sprunglabels in Variablen speichern. Erinnerst Du dich noch an das Makro vom Beginn des Kurses?

#define EXE_NEXT(); ep = ep + eoffs; goto *jumpt[code[ep]];

Schauen wir uns das mal genauer an! Hier wird mit “ep = ep + eoffs” die neue Position im Code berechnet. Die “eoffs” Variable wird am Befehlsende gesetzt. Dazu kommen wir später. Mit “goto *jumpt[code[ep]];” wird die Adresse des nächsten Befehls angesprungen. Der ja in “code[ep]” festgelegt wurde.

Hier kommen nun die Befehle mit ihren Sprungmarken z.B. “addi:” am Anfang des Befehls:

pushb:
#if DEBUG
printf ("%lli PUSHB\n", cpu_core);
#endif
arg1 = regi[code[ep + 1]];
arg2 = regi[code[ep + 2]];
arg3 = code[ep + 3];

regi[arg3] = data[arg1 + arg2];

eoffs = 4;
EXE_NEXT();


pullb:
#if DEBUG
printf ("%lli PULLB\n", cpu_core);
#endif
arg1 = code[ep + 1];
arg2 = regi[code[ep + 2]];
arg3 = regi[code[ep + 3]];

data[arg2 + arg3] = regi[arg1];

eoffs = 4;
EXE_NEXT();


loadb:
#if DEBUG
printf ("%lli LOADB\n", cpu_core);
#endif
arg1 = code[ep + 1] 	// data address
arg2 = code[ep + 2];	// data offset
arg3 = code[ep + 3];	// register

regi[arg3] = data[arg1 + arg2];

eoffs = 4;
EXE_NEXT();


addi:
#if DEBUG
printf ("%lli ADDI\n", cpu_core);
#endif
arg1 = code[ep + 1];
arg2 = code[ep + 2];
arg3 = code[ep + 3];

regi[arg3] = regi[arg1] + regi[arg2];

eoffs = 4;
EXE_NEXT();


printi:
#if DEBUG
printf ("PRINTI\n");
#endif
arg1 = code[ep + 1];

printf ("%lli\n", regi[arg1]);

eoffs = 2;
EXE_NEXT();

pexit:
printf ("shutdown...\n");
exit (0);
}

Jeder der Befehle endet mit dem Makro, das zum nächsten Befehl springt. Außer “PEXIT”, dort wird die VM beendet.

So sieht jetzt unser Grundgerüst der ZeroVM aus.

Ich habe eine Aufgabe für euch: Ergänzt die VM um Befehle für die restlichen Grundrechenarten: Subtraktion, Multiplikation und Division.

Hier gibt es ein ZIP Archiv mit dem vollständigem Programm:

ZeroVM-01