What is Virtual Machine(VM)
For those who never had an Idea about it, here is a short excrept from wikipedia
In computing, a “virtual machine” (VM) is the virtualization or emulation of a computer system. Virtual machines are based on computer architectures and provide the functionality of a physical computer.
Thus, a Virtual Machine is basically a software which emulates the functionality of a computer system. It basically translates the instructions of another computer architecture to run on the actual host. This was very helpful in many ways:
- They help in running softwares which are only native to a specific processor (eg: running x86 on ARM).
- Helps in making old softwares usable (eg: running old DOS games on a Windows 10/Android).
- Helps in Easy Backup and Isolation which are very helpful during software testing.
Why Ada?
Ada is a Programming language created for making reliable, efficient and safe for use in Mission critical systems. It is created for use in Defense and Aerospace technology. But now, it’s usage is getting low due to the advent of programming language such as Python, Java, etc…
Also, I made this VM using Ada to give it a try. Why not Ada? being such a type-safe language, it has all the capabilities to create a virtual machine
The SUBLEQ VM
Virtual Machine softwares, basically take a program written in assembly instruction of another architecture and interprets it to run on the actual architecture. So, to implement a VM, we need to choose a architecture amongst the following:
- CISC: Complex Instruction Set Computer, they have a large fleet of instruction to implement(eg: Intel’s x86).
- RISC: Reduced Instruction Set Computer, have less no. of instructions to implement(eg: ARM, IBM’s PowerPC).
- OISC: One Instruction Set computer, has only one instruction to implement.
The OISC architecture is also known as URISC(Ultimate RISC) architecture. This is still a theoretical concept, but implemented virtually. Such a instruction used in real-life would definitely create highly powerful, fast systems.
There are many instructions which support this architecture. One such instruction is SUBLEQ(SUbtract and Branch if Less than or EQual to zero). It corresponds to this C program:
Mem[dest] = Mem[dest] - Mem[src]
if (Mem[dest] <= 0) goto branch
Where src
, dest
and branch
are arguments of the SUBLEQ instruction. Also, we need to note that:
- If
src = -1
, then the program should input a character and save it inMem[dest]
. - If
dest = -1
, then the program should output a character stored inMem[src]
.
This architecture is choosen to build a simple, but fast VM which you can make it within 15 minutes(includes logical understanding of program).
Prerequisites
To make this VM, you just need a Ada compiler(I used GNAT Ada compiler) and basic knowledge about Ada programming langauge.
Setup
Just cd
in your project folder and create a directory for the project by running the following commands
$ mkdir memoria
$ cd memoria
In the above commands, we create a directory to store our VM(I’m calling it Memoria) and make it as our
current directory. Now create a file named vm.adb
and open it using your favourite editor.
Package Imports and Declarations
Let us start the program by importing necessary packages.
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Command_Line; use Ada.Command_Line;
with Ada.Integer_Text_IO;
procedure VM is
package IntIO renames Ada.Integer_Text_IO;
type Code_Arr is array (0..255) of Integer;
Program : File_Type;
Num, Mindex : Integer := 0;
Memory : Code_Arr := (others => 0);
The first 3 lines of this code is simple. It just imports Ada.Text_IO
, Ada.Integer_Text_IO
for IO
operations and Ada.Command_Line
for accessing command-line arguments. Also, we rename Ada.Integer_Text_IO
into IntIO
to create distinction between Ada.Text_IO
’s functions.
Then we declare a type Code_Arr
which is a simple array and it has 256 locations(i.e., 32 byte). The
variable Program
, Num
and Mindex
are created to load our program from file to program memory(i.e., into
Memory
variable)
Making the VM
The SUBLEQ VM is internally created as a Procedure:
procedure Memoria (code: in out Code_Arr) is
IP, NextIP : Integer := 0;
function Src return Integer is (code(IP));
function Dest return Integer is (code(IP + 1));
function Branch return Integer is (code(IP + 2));
We create a procedure for our VM and declare the following:
- Variables
IP
to store the pointer(i.e., location) of current instruction in memory andNextIP
to store the upcoming instruction pointer. - Functions
Src
,Dest
andBranch
corresponds to the 3 instructions of SUBLEQ
begin
while IP >= 0 loop
NextIP := NextIP + 3;
As we begin our function, we create a while loop which runs until IP
is a whole number(which means to exit
the program we need to change IP
to -1
). Also, we increment the NextIP
variable by 3
.
if Src = -1 then
declare
Ch: Character;
begin
Get(Ch);
code(Dest) := Character'Pos(Ch);
end;
The above code handles input(i.e., If src = -1 case), it inputs a cheracter and store it in variable Ch
temporarily and sets memory location pointed by Dest
to ascii value of Ch
.
elsif Dest = -1 then
Put(Character'Val(code(Src)));
This code is self-explanatory, It outputs the character for its ascii value stored in memory pointed by Src
.
else
code(Dest) := code(Dest) - code(Src);
if code(Dest) <= 0 then
NextIP := Branch;
end if;
end if;
IP := NextIP;
end loop;
exception
when others => Put_Line("Memoria Program Execution Error");
end Memoria;
Here, we check for our actual condition which we need to implement the SUBLEQ instruction. If it is true, we
change the value of NextIP
to the Branch
pointer and finally assign it to the current IP
. Also, we
include a exception
to catch up error thrown while running any specific SUBLEQ program.
By the end of this procedure, you’ve a functional SUBLEQ VM(but it needs some interface with real os).
Reading stored Programs
Now, we’re ready with a VM. But we need to access the program files to execute it. Thus, we need to read the program file and load it into our memory.
begin
if Argument_Count < 1 then
Put_Line("Memoria VM needs atleast one CMDline argument");
Put_Line("Usage: ./memoria <fname>");
else
Open(Program, In_File, Argument(1));
while not End_Of_File(Program) loop
IntIO.Get(Program, Num);
Memory(Mindex) := Num;
Mindex := Mindex + 1;
end loop;
Close(Program);
Memoria(Memory);
end if;
end VM;
This is our main function(which gets executed when running the executable). In the first couple of lines, we include the code to handle if argument haven’t supplied by the user. Then, we do the following:
Open
the file provided by the user at the first argument(Argument(1)
) withIn_File
(read) functionality.- Read the file until EOF and read every integer seperated by whitespace and store it temporarily in
Num
. - Load each
Num
into Memory pointed byMindex
. - Increment the memory index pointed by
Mindex
by 1. Close
theProgram
file and pass the memory to ourMemoria
function to execute it.
Now we have a fully functional VM which can execute programs stored in file.
Running “Hello, world!”
The first program to test in our VM is to print "Hello, world!"
on to the screen. For that I’ve a premade hello_world program. Just download it, and pass the file as a commandline argument to the program.
To run the VM, execute the following commands
$ gnatmake vm.adb -o memoria
$ ./memoria hello.txt
This command would execute the "Hello, world!"
program which is stored in hello.txt
file.
Mission accomplished
Yay! now you’ve made a VM in just 62 lines of Ada. You can check out the source of this project here. This VM can execute more complex commands using multiple SUBLEQ instructions. If you’re interested in this topic and want to expand more, there is a lot of resources out there on the internet. I’ve also personally wrote a More complex VM and also a CHIP-8 emulator.
If you’ve liked this article, star my memoria github repository. If you need to suggest any changes, feel free to create an Issue at my repo.
Thanks for Reading!