hi hello how are you
welcome to chapman world
this is the first video in a short
series in which we're going to build a
virtual machine
now by virtual machine i don't mean the
kind of thing that you may be familiar
with from virtualization technologies
such as a hosted virtual server for
example
but instead the kind of virtual machine
that is more like an emulator
in fact the only reason it's not called
an emulator is because it doesn't
emulate any real hardware it'll emulate
some imaginary cpu that we're going to
invent
so with that clarification out of the
way in the first half of this video
we're going to look at a simplified
model of how a real computer works
and we'll then use that model in the
second half of this video
to build a virtual machine based on that
model
in pascal so let's go ahead and get
started
okay so we're going to take a look at
how a computer functions
internally and we're going to do this
using a very crude model of a computer
and the reason for the crude model is
modern computers
are very sophisticated machines but they
all
operate on same fundamental principles
as older machines and even back into
early clockwork computers
the the fundamental principles of how
they work remain the same
and so this model is intended to reflect
those fundamental principles to give you
an intuition
about how computers actually function
and
if you are then inspired to go on and
learn the idiosyncrasies of modern
processes
then i'm happy to encourage that but for
right now we just want to focus
on the basics so all computers will have
at least these three components they'll
have some memory
they'll have a cpu usually referred to
as a processor
and they'll have a clock of some kind
very often the clock is actually built
into the cpu so what do these components
do
well the memory is one of the most
important components of a computer
system
it is literally a chip that's on the
main board of your computer
or sometimes plugged in on external
boards into the main board of your
computer
so it's literally a little chip with
pins coming off of it
and internally it stores a big list of
numbers
so in my example here in my model i've
just got six numbers being stored in
location zero through five
but this is the device that we would
normally talk about
in gigabytes or megabytes or gigabytes
this is a device that could hold
billions of numbers in a big old list
and so
the reason for its importance is
everything the cpu does
is essentially a computation on numbers
so the cpu is going to be able to look
at the numbers in the memory and perform
calculations on them now the memory also
connects to
all of your external devices so your
keyboard
your mouse your screens your monitors
these are all connected via various
different types of circuitry
to some piece of memory and the reason
is let's say i press a key on the
keyboard
well that key press through the
circuitry that connects it to the memory
will generate a number
and it will store that number in memory
so my a key might be number one maybe
key might be number two
it will store those numbers in memory
the cpu because it's able to look at
what's in the memory can then see which
key i pressed
and the cpu's job might then be to put
that number
into the location that drives the pixels
on the screen
so that you can see which key was
pressed on your screen
so essentially everything happens by
putting data into and pulling data out
of
this memory circuitry now the cpu
is the component inside a computer that
does computation this is where all of
the work
happens and it is going to get its
instructions again from the memory
so the instructions or the program which
our cpu is going to run
it reads from this memory circuitry okay
so let's take a look at the cpu in more
detail
every cpu every modern computer has a
cpu
with a certain number of registers
now registers are internal memory
locations they are
special bits of memory that exist inside
the cpu
not in the memory circuitry and the
reason that they're in the cpu is the
cpu is going to use these
in special ways to perform operations
and we're going to take a look
at just how that happens in a moment so
these are little memory locations and
every modern computer will have some
form of program counter
it might be called something else and
you know different manufacturers name
things differently
uh it might have different details about
precisely how it works but fundamentally
every cpu will have a register called a
program counter
or that behaves as a program counter and
every cpu will have
one or more registers that behave as an
accumulator and again we're going to see
what that does
in just a moment so every cpu in a
modern computer
will have at least these two registers
but they'll usually have many many more
registers
i'm simplifying again for this model so
we'll just work with these two registers
and then we have a clock now the reason
we have a clock is
a cpu needs some mechanism to drive it
some
some instruction some signal saying go
do something
okay so the clock is going to send
pulses
at regular intervals into the cpu
instructing the cpu to do
something so let's start with our
machine uh
powered off and we flick the switch to
power it on electricity comes into the
circuitry
the memory typically in a modern
computer would be empty
or uninitialized data so
the memory is not useful to us at the
moment in most modern computers you'll
have
some hard-coded memory to get the
computer up and running and then you'll
load things into memory from the
disks or from a usb device or something
else
but in our example right here today
we're going to say that this memory
when you first turn it on is initialized
with this data so this just keeps our
model simple we're going to say that the
program is pre-loaded
uh in a modern computer of course it
wouldn't actually be pre-loaded
but this is what we have these numbers
right here our cpu when it's turned on
its program counter register at least
will be set to zero now the accumulator
might be
uninitialized it might be set to zero it
varies from processor to processor
but the program counter contains a zero
so what happens is the clock sends a
pulse
into the cpu and the cpu says ah the
clock's pulsed i've got to do something
so it needs an instruction and its
instructions are stored over here in the
memory
so it needs to signal the memory to say
hey give me an instruction
and the way it does that is it takes
this program counter value this
zero and it puts it on the address bus
the address bus is quite
literally wires connecting the cpu to
the memory
so in a 32-bit computer you'll have 32
of these wires in a 64-bit you'll have
64 of these wires typically
and so the cpu will translate this
number 0 into a binary value
that can be presented on those 32 or 64
wires
and it'll put that value into the
address bus and it'll then say
this read write line again this is just
another wire that connects the cpu to
the memory
i want to set this to a value meaning
read so the memory says okay you want
something from location zero in my list
of numbers
and you want to read it so you want me
to put this value
onto this the data bus the data bus
is again another 32 64 however many
wires
connecting the cpu to the memory so the
memory is going to say well you want
location zero that's got a number one in
it
so i'm going to take the number one and
i'm going to represent that on the wires
of the data bus
so there we are and the cpu gets its
number
zero one and what the cpu will do is it
will look
at a special hard-coded set of
instructions
it'll look at that number and say well
that number represents
a load instruction so that means the cpu
is going to load something
what does that mean well we're going to
load a value from the memory
into the accumulator register that's
what our load instruction is going to do
so the cpu now knows that it needs to
load something but
where does it get that something from
well it knows that the program counter
is at zero
and that the memory location that we
want to load is the very next location
so it'll update the address bus to a one
it'll leave this pin as a read
the memory says okay you want something
from location one
and you're reading it so i'm going to
put that on the data bus
well the number in location one is a
five so it puts that on our data bus
and the cpu reads that five and puts it
in the accumulator
okay so that is the whole load
instruction done
we read the instruction we got a zero
one and we said oh that's a load
instruction so
read the next piece of memory and copy
that into our accumulator
but now the cpu also needs to update its
program counter so it knows it started
at zero
that's this location here and it read an
instruction it knows that
load instruction needed one piece of
information from memory which is here
and we read that piece of information
and so the next instruction
is at location two so the program
counter will be updated
to two that's a whole instruction cycle
now the clock will pulse again to
trigger the next
instruction cycle there is a detail here
which i will pull out
modern processors don't generally
perform a whole instruction
a whole instruction cycle per clock tick
in fact it may be broken up it may be
that the first pulse is when it reads
the instruction
the second pulse is when it reads the
value into the accumulator
and so on we can break these operations
down into even smaller steps
but for the purposes of our model every
time the clock pulses
our cpu is going to perform one
instruction
so we've just done one instruction we've
loaded the value five into our
accumulator
the clock pulses again and the program
counter is now set at 2.
so we put the value 2 onto our address
bus
we leave the read write pin set to read
the memory says you want to read from
location 2
there's a number 3 in there so i'm going
to put that on the data bus
the cpu gets the three it looks up its
list of instructions
and it says oh three is an addition so
we're going to add
two numbers together which two numbers
are we gonna add well the first one
is the value in our accumulator we
already have that so we're gonna load
another number from the memory
to add to the accumulator so we'll
update our address bus now from
2 to 3 to get the next location in
memory
that has a value 2 in it because the
read pin is still set
we put a 2 on the data bus and we now
know in the cpu
we need to add the number two to our
accumulator so our accumulator becomes
seven at this point we've performed our
instructions so we need to update
our program counter so we started
location two in memory
and that gave us our instruction which
was the
addition we read the value of 2 which is
the value we wanted to add so we're now
at location 3 in memory
and so the next instruction that we're
going to read must come from location
4. so the program counter gets updated
to full and we wait for another clock
tick
the clock ticks again it sends a pulse
into the cpu
the cpu says okay i need an instruction
so i'm going to put the program counter
value on the address bus
at this point the memory sees a read
instruction for address location 4
it takes the value 2 and it puts that on
the data bus
we look up instruction 2 in our
instruction set and we find it's a save
instruction so we're going to save
something somewhere
and what we're actually going to do this
instruction will save the accumulator
to the next memory location so the first
thing the cpu needs to do is
update the memory location because
location 4 has the instruction so we're
going to update to
location 5. this time we're trying to
put data
into memory so it's going to change the
read write pin to a write
and then it's going to take the value
from the accumulator and put that
on the data bus so the memory says ah
you have said that you want to
write something into location five and
something you want to write is the
number seven
so it will update location five to
number seven
and that's it we've just looked at three
whole instruction cycles
for a program that adds five and two and
gives us the result of seven
now in a modern computer of course being
connected to
uh many external devices like screens
and keyboards and everything else
it will then have to do a lot more work
to take that result seven
and translated into pixels to be
displayed on the screen as the answer in
our calculator program
so you know it's got a lot more work to
do to finish
and there's a lot more memory space for
it to run through fortunately the clocks
in
modern computers run at three
and four maybe four and a half gigahertz
so that means that these pulse hundreds
of millions or billions of times per
second
so the cpu really is blistering through
instructions an incredibly high rate
there are
you know millions of instructions per
program that you might run
but essentially this is how a program
operates inside a computer
today we're going to look at an
additional feature that most or if not
all cpus will have
and that's called the stack so what is
the stack
well thinking back to our previous model
of a computer we have our cpu
and a bunch of memory a stack is an
ordered structure
for storing data in memory and it's
usually implemented using a register
called the stack pointer
now the stack pointer is a little bit
like the program counter that we had in
our previous model
in that it points to some location in
memory in this instance it's set to zero
so it's pointing at the zero location
now there are two operations that we can
perform on the stack
which are called push and pull and
they're usually implemented as
instructions that the cpu can perform
push and pull instructions
so push means adding data to the stack
and pull means fetching data from the
stack
so let's first see how we put data onto
the stack by performing a push
operation so we're going to store the
value of 5 into the stack from the cpu
it could be from any of the cpu's
registers
we want to put the value 5 into the
stack so we look at where the stack
pointer is pointing
it's currently pointing at the zero
location in memory and we put a 5 there
and then we update the stack pointer to
position
1. so the value of 5 has now been stored
in the memory we say that it's been
stored in our stack that's our push
operation now when we want to pull the
value back off of the stack we use the
pull
operation or the pop operation to get
that data back from the stack
and that's essentially doing a push in
reverse so we
move our stack pointer in the other
direction back to 0
and read the value 5 from that location
so that's how a stack functions
essentially we push values in and we
pull
values out so an important property of
the stack
is that when you pop or pull data from
it what you will actually get will be
the last thing that you pushed in
it's often compared to a stack of dishes
when you add a dish to the top of the
stack
you add it on the top and when you
remove you remove from the top
and so the one that you get when you
remove a dish from the stack is always
the last one that you placed into that
stack this is known in computer science
as a lifo
system or last in first out so let's
take a look at another example here
and in this example we're going to push
several pieces of data
into our stack and the pieces of data
we're going to push are the numbers
48 108 108 101
and 72. now these numbers are actually
codes which represent letters
in something called the ascii character
set and those letters are
o l l e h respectively
so let's go ahead and see that data
getting pushed into our stack
okay so we essentially put the number
into the memory location
updated the stack pointers to the next
position and
push the next number in so that's how we
push data into the stack
now we're going to pop that data back
off of the stack
so as you can see as we pop the data off
of the stack
because of this last in first out nature
of the stack
we're actually pulling out data in the
reverse order that we put it
in so when we pull data out of a stack
or pop data out of a stack as it's often
called
it's in reverse order to what we put in
now
in modern intel systems the stack
actually works
in the opposite direction the pointer
will start at some location high up in
memory
and as you push values it'll move the
pointer down towards the bottom of
memory
and then as you pull values back out of
the stack or pop values
it will increment that pointer back up
so it actually works in the reverse
order for
intel cpus but the functionality the
behavior is exactly the same
essentially you push values in you get
back out the last value that you pushed
in
and that's it that is the stack feature
of a cpu
okay so we have looked at a model of how
a computer functions internally
and where today we're going to write a
virtual machine based on that model
so i've opened up a new project here in
lazarus and i'm about to get started
uh working that model into a simulation
now the first thing we need to simulate
if you'll remember
the model had three components the clock
the cpu
and the memory and the first component
we need to simulate
is the clock now what does the clock do
well it just continues to send pulses it
basically introduces repetition so that
the cpu can
repeat its process over and over so
we're just going to use a very
straightforward repeat
now our repeat statement needs a
condition to end it otherwise this would
be an
infinite loop so we're going to say keep
running until not
still running in fact let's just call
this variable running
so keep running keep repeating until no
longer running
and we're going to initialize running to
true
running is true i'll learn to type i
promise
so this is our initialization
and this basically represents our clock
tick or clock cycle
in which we perform an instruction now
i'm going to put a read line
at the end of the program here just so
that when we run it the
anything we print out to the window can
be seen without the program disappearing
so this repeat until represents our
clock
when we start at repeat that is one
clock pulse
we get to this until and if running is
still true
we'll come back up to repeat and that is
our second clock pulse
okay so what did our cpu do within each
clock tick each clock cycle well first
it fetched an instruction from the
memory so we'll put in a fetch
then it ran that instruction so we'll
say execute
execute instruction and then
it updated its program counter
now the execute piece could perform uh
many sub operations for example it might
get a value from memory to compare with
the value in
a register or something like that but
for right now
we're just going to consider this one
section this execute
and keep it as simple as possible okay
so we have this running variable we need
to declare
so that's a boolean and we have no other
registers the first register we're going
to need if we're going to fetch an
instruction we need to know where to get
that instruction from
and we're informed by the program
counter where to get our instructions
from
so we need a program counter before we
can define a program counter
we also need to define what it's going
to point at because remember the program
counter is essentially a pointer
pointing to a location in the memory so
we're going to define a data type for
our instructions
so i'll say type tvm instruction
and we need to decide on a type for this
now i'm going to go for uint
16. 16 is 2 bytes 16 bits
and the reason for this is if i select
uint 8 then i would only have space for
256
instructions 0 to two five five and i
know from experience having written a
virtual machine
before that's not going to be enough
when we enhance this virtual machine out
now our model from the previous session
had only three instructions in its
instruction set it had load
save and addition we we are going to
want more than those
and that we're going to want more than
255 so i'm making this uh an n16
unit 16 that's going to give us 65
536 potential options for instructions
which i think will be more than enough
so if we have some memory buffer full of
these instructions plus other arbitrary
data
we need some way to point at a location
in that buffer so we'll just create a
pointer data type
that points to a vm instruction
and this is essentially what our program
counter will be
so we're going to create a program
counter
which is a pointer to a vm instruction
in our memory buffer okay so with that
done
we now need some memory to represent the
memory of the virtual machine
now i could allocate a large buffer here
but we don't really need a large buffer
for the minimal instructions that we're
going to cover in this session
instead what we need is just enough
space to store a program
and in order to store a program we need
to specify what the values are
that make that program up those are our
instruction
op codes so i'm going to create this
constant let's say op knob
this will be our first instruction and
op
halt will be our second instruction now
these instructions don't do anything
particularly interesting so you'll
remember from our model we had
load save and add well in this case
we're just going to have knop which
does nothing and halt which will end the
program
and we'll come back later and add more
interesting instructions in but for now
this is all we need to get our virtual
machine up and running and demonstrate
that we can make it work
and then we can come back and enhance it
as we see fit with these two
instructions then knob does nothing
halt exits the program how does halt
exit the program
well it will just set the running value
to false
which means that when the clock cycle
ends this test will be false
and we will repeat the loop sorry we
will not repeat the loop will drop out
so that will end our program
okay so let's set our program counter up
to point at something
well we need as i said a piece of memory
let's put that in
here so we're going to have c test
program now i'm going to reiterate one
more time
at this point this test program is going
to be an array
of 0 to 1 instructions so two
instructions
of type tvm instruction
which is actually 16 bits wide it's two
bytes
now when we enhance the machine out this
will be a buffer full of bytes
and we'll make the type byte but for the
moment it's convenient to put
the tvm instruction type in because we
can then write our program like this
we can just use the constants which we
defined above so i'm going to put an op
knob in there and i'm going to put an op
halt
in there so this is convenient because
the
values here that we want to put in are
16 bits wide and we're making our array
an array of 16 bit items when we create
a uh
as i said a more complete virtual
machine we'll make this a byte buffer
but we'll then need a special function
to take our
op codes and write them into that memory
so that we can construct a program
because if we write a 16-bit value into
a byte that's not going to work for our
program
okay so this c-test program then
represents the entire memory space of
our virtual machine and at the moment it
has a program in that
runs an instruction that does nothing
and then exits the program
so it's not going to do anything too
interesting but as i said it will get us
up and running so we need to initialize
our program counter to point at
that program program counter
so we want the zero element as i said
the program counter is always
initialized
to zero and that's the zero element of
our
memory space which is c test program so
we'll say
zero element of that array oops and
because
the program counter is pointing at an
instruction
we'll say the address of that element so
our program counter is now initialized
to point at the first instruction
in our program okay so how do we fetch
that instruction
well the vast majority of tutorials on
how to build a virtual machine that you
might come across
will tell you to use a case statement or
if they're written for a c based
language a switch
statement which is essentially the same
thing they'll tell you to use a case
statement
to determine which instruction we're
looking at and we
use the case statement on the program
counter
dereferenced so we're looking at what
that program counter is pointing at
so that's not a bad idea for teaching
purposes
but it's actually the worst option for
writing a virtual machine
we'll look in more detail at why case is
wrong for virtual machines later
but for right now just take it as read
that the case statement is actually a
bad idea
if you want your virtual virtual machine
to be performant and we're actually
going to optimize this out to make it
more performant later
right now it's going to serve our
purposes we want to look at what happens
if we get an
op knob well that should do a begin
and it should do nothing and we also
want to look at what happens if we do an
uphold
well that's going to set our running
property
to false okay so that's our execute
let's move my comment into the correct
place there that's how we execute the
instruction
and we now need to update our program
counter in fact
i'm going to space this out a little and
bring this back here
so this is our fetch we're looking at
the instruction we care about
this is our execute what we do for that
instruction and then we're going to
update our program counselor down here
now what i want to do is increment the
program program counter
by one instruction which as we've
already explained
is two bytes so i want to increase by
two bytes
well to make it uh maintainable code
to some degree we're going to say sizeof
tvm
instruction what this means is this is
going to return a
2 and the compiler will optimize this
down and literally put
two in place of this sizeof function so
it's not actually generate any code to
do this it's just a literal value
but if later we come back and decide
actually two bytes is not enough space
for our instruction set
we could change this to say uint32 and
this code will still compile it will
just insert
four instead of a two we don't have to
come and change the value
so we we have two bytes that we want to
increment our program counter by
program counter is equal to program
counter plus two now we also have
another problem here
the program counter is of type pointer
it's appointed to atvm instruction
the size of is going to return
essentially a cardinal value of
an integer value so these two types are
incompatible
we need to make them compatible so i'm
going to typecast my pointer
as a native uint so that's going to make
my
program counter an integer and my size
of oops
is also an integer but then we're trying
to assign it to a counter
so we need to wrap the whole thing in
another typecast
to typecast it back to a pointer
okay so the compiler is actually going
to get upset with us when we try to
compile this
and the reason is that the compiler is
looking for instances where we try to
use
integer values with pointers the reason
is that an integer value
in on most targets is 32 bits wide but
it's not 32 bit wide
on all targets and a pointer could be 32
bits wide on
say a 32-bit processor but it will be
64-bit wide on a 64-bit processor if
compiled in 64-bit
mode so effectively the size of a
pointer can change and the size of an
integer can change
but i'm using this special native uint
type which free pascal ensures is always
the same size as a pointer
which means that our pointer arithmetic
here will work just fine
the type casting might look a little
ugly and the compiler will get upset
because it thinks that we're using
integers and pointers in the same
statement
but it will work just fine and it's
correct it's okay
the only thing we might want to do is
put a cache around the size of as a
native uint
uh but that's actually not necessary i'm
sure so we'll just leave it as
is but when we build it as i said it's
going to give us these hints
so i'm going to put in some little
directives here hints
off and hints back
on so this is telling the compiler
we know what we're doing we're going to
leave this as it is okay
so that's updating our program counter
by two bytes now there's actually
another trick that we could do here if
we
knew for a fact that our vm instruction
is always going to be
two bytes then we could just do a shift
left
to increment the program counter but i'm
writing this code under the assumption
that that type
may change so i'm going to leave that as
is for the moment
and okay as we saw this program actually
compiles so let's
see if it does something it'll be nice
if this program would write out some
debug information
so we'll put some little write lens in
and we'll say that this
is start virtual
yep type that correctly uh type start
virtual program
and we'll put another one at the end for
end virtual program
and then in the middle here i'm actually
going to modify knob
to put a knob message out
so although the instructions should
actually do nothing for debug purposes
we're going to see a message come out
okay let's go ahead and run our virtual
machine and
we get a start virtual program followed
by a knob followed by an end virtual
program
now what this means is our program
counter started pointing at the knop
instruction
and we saw that we enter the program
start virtual program
and then execute that knob so we see
that message come out
and confirming that we reach the next
instruction
we end the program so what's happened is
when we've done the knop
we fall through to this program counter
being updated
we repeat our loop we get down to the
halt instruction because our program
count has now been updated
and it sets the running to false we
actually still calculate the next
program counter but it's not used
because we hit this
not running and end the virtual program
so congratulations we have a working
virtual machine
so now let's talk about how we can fix
what i was talking about with the
fetch case statement here now the
problem with this case statement
is the code that it generates when the
compiler compiles this program
so i'm going to go ahead and save my
project off i'm going to save
all i'll create a folder on my desktop
virtual machine one
okay so we've got that saved now when i
build it what's going to happen
let's do a build first so we can see
this it's going to generate
several files it generates ultimately
this executable
but this executable is built by linking
together
all of the object files that are
generated by the compiler or rather the
assembler
those object files of which we only have
one but there could be multiple
depending on the scale of the program
those object files are generated by an
assembler
and the assembler is fed an assembly
script from the compiler to tell it what
to build
the problem is we don't see that
assembler here so i'm going to go
and modify my options here under custom
options
i'm going to put in a hyphen a so i've
gone into project options
custom options hyphen a what that's
going to do when we build
is say when you generate the assembler
to build the object file
don't delete it leave it there because
we want to take a look at it
so i'm going to take a look at that
assembler file now
and i'll ask your forgiveness i'm
familiar with uh intel assembler format
i'm a little bit
confused sometimes by this format i
think is
gas the gnu assembler but
ultimately if we take a look through
here i'm seeing these fpc get output
okay
program counter starts here right what
i'm going to do is i'm going to build it
again without my debug messages just to
make the uh
the assembler a little bit cleaner to
read so i'm going to take these debugs
out
and we'll build it again
okay so what's going on here well when
we enter our main program the first
thing we do
is move a value into running so that's
this running equals true
and then we're coming in here to the
test program
and setting up our program counter so
we're loading the effective address
of the first element of our test program
constant
into the program counter so that's the
second line and then this
lj3 here this is where our repeat loop
begins this is the beginning of our
virtual clock cycle and the first thing
we do is
in the case statement here the program
counter we're looking for the value
at that program counter so what we're
doing in the assembler
is we're coming in here and we're saying
get me the
value at the program counter and put it
into
rax now rax is an intel processor
register
let's just call it register accumulator
x it's an accumulator style register
so we get the value of that program
counter what it's pointing to which is
on op
knob and that value is a zero so then
we're going to come
in here and we're going to say i want to
test that value
to see if it's zero and if it is we'll
jump to
lj six well lj6 is down here
lj6 because this is our knop instruction
there's no instruction here
so it falls through immediately to l6
which adds something to the program
counter
that's our program counter line here
being updated adding two bytes to the
pointer
then we hit ll7 at which point we
compare
the running value with it's doing a
boolean compare right comparing it with
zero and so if running is still true
it jumps back to our repeat statement at
sorry lj 3 there it is so it's going to
jump back so our actual loop
is from lj3 here down to
right here line 68 where we jump back
now i'm going to put some temporary code
in
to illustrate that so i'm going to put a
temporary variable in
and i'm going to set the temp variable
to
53280 at the top and then i'm going to
set it again
to 53281 at the bottom
so if i do a build
we can actually take a look at our
program now with these 532 8053281
these are markers for the start and end
of our loop so i'm going to take
all of the code between them copy that
out put it in a new file
and that will give me a line count now i
know that a line count
is not an accurate measure of how many
instructions are being run and therefore
is not an accurate measure of the
performance of this loop
but it is a rough indication we have 26
lines of code being generated in the
assembly file here
and this is the problem with the case
statement
it's a two-part problem the first part
of the problem is we're first testing to
see if this is a knob
and then we're testing to see if it's a
halt
right so we're doing two tests if the
instruction we want
is the halt instruction we still go
through the not
test it just fails and then we go on and
run the
halt test ideally
we want some piece of code that doesn't
run through
the not test at all there's no point
testing it for our halt instruction
we just want to go straight to the halt
instruction and the case statement
doesn't let us do that
the second part of the problem is
related to this
as our instruction set grows we're going
to want more instructions to make our
machine more useful
so i'm going to put a new one in alert
and we'll set that to 0
2. so this is our new instruction the
alert instruction
and i need to test for it in my case
statement so i'm going to go ahead and
put it on the end
up alert and for the moment i'm just
going to make it a knob so it does the
same as
knob does nothing but i've added that to
my case statement
and i'm actually just going to add it to
our test program here as well
and so our test program when we come
back to running it will actually
run one of these alert instructions so
let's build it again
and what we can see if we copy the code
from between
53280 and 53281
we previously had 26 lines of code
and we now have 29 lines of code the
reason again is we're testing to see if
the instruction is a knock so
let's assume that the instruction that
the program counter is pointing at
is an alert well in order to find out
whether it's an alert or not we're
testing is it a knob
no is it a halt no is it an alert
yes go do something so these lines of
code here for testing
if the uh instruction is an alert is a
knob or a halt the test failed they're
pointless lines of code but we have to
go through them anyway
to get to our alert test so how can we
fix this
to mean to make two enhancements okay
the first one is i don't want to test
every possible value of instruction
and the second is i don't want my code
to grow when i add new instructions to
my instruction set
let's see how we can change that and
this is where the program is going to
get a little more sophisticated but
it is a worthwhile change and i'll just
point out if this were
for example the a professional virtual
machine such as
the java vm or microsoft's il
or whatever other professional vm this
might be then the manufacturer would go
to
extreme lengths to make this loop as
small as possible
in fact most of the time i think it
would be written in assembler not in
c or pascal or whatever other language
it would be written directly in
assembler
and various optimizations would be used
such as
pre-fetching instruction data maybe
using a background thread
there'd be an awful lot of effort put
into keeping this loop small
the reason is really very simple as i've
just said we have to execute all of
these instructions for every
virtual machine instruction which means
that the more instructions we
execute the slower our virtual machine
will be and of course those professional
vm manufacturers don't want a slow
virtual machine they're in a competitive
world
so we're not going to try the same level
of
optimization as those manufacturers
would that's an extreme that i'm not
going to go to for the sake of this
video
certainly but what we will do is just
make our code a little better
by removing that case statement and as i
said aiming for the goals of not
letting this function grow when we add
instructions and not having to pass
through every option
so the first thing that we want to do is
uh we want to
modify what we do within our execute so
that our fetch can be more simple
so what i'm going to do is pull out the
knob halt and alert into their own
functions
now these functions need access to what
is effectively our state data so we'll
call this
state data for our cpu
the temporary variable is not but i'm
going to leave it in for the moment so
that we can
still look at it in the assembler code
so under our state data
i'm going to generate three new
procedures i want
knop handler which does nothing
i'll copy that and have an alert handler
which also does nothing
and then we'll put one more instruction
in which will be our halt
handler
okay and this is the reason i didn't
name them nop halt an alert
halt means something to pascal so i
didn't want to use that name
so it's halt handler not handler alert
handler what does halt do
it sets running to false
okay so not does nothing halt handler
says running to false alert does nothing
okay so they're the procedures that
we're going to call
but in order to call them we're still
using a case statement and that's not
good enough
what we want to do instead is to put
these uh functions into some kind of an
array
because then we just need to look up the
index of the function that we care about
so what i'm going to do is i'm going to
create two
new data types let's start here
so i'll create a tvm instruction handler
and this will be a procedure
with no parameters or anything so it's
just a procedure type
now what is a procedure type when it
compiles down this is a simple pointer
it's nothing more than a pointer so
we're going to create that type
and then we're going to create a t
instruction set
type so this is not our instruction set
this is just a list of constants that
allow us to
write instructions into our program this
is our instruction set
it's an array and we have zero to two
because we have three instructions
of tvm instruction handler
okay so this data type we need to
declare a variable for it
and i'm going to declare that down here
because it's not actually a part of our
state
data it's a part of the virtual machine
machinery mechanics so we're going to
create an instruction
set in here t instruction
set and so that is now an array of three
elements
and we need to populate them so in our
initialization code the very first thing
we do
up halt actually start with op nop
should be at uh not
handler now just as a pointer if you are
following following along
using delphi you won't need the at here
this is only because i'm in the fpc
object mode for the fpc compiler
so we're going to do the same thing for
let's do copy paste
and do the same thing for the op halt
instruction
we'll use the halt handler and the op
alert
we'll use the alert handler and another
piece of data that we're going to put
not in the state data
but in the mechanics machinery we'll
just label this as machinery
machinery of vm this is going to be
current instruction type tvm
instruction handler so what we do
in our fetch is we say well whatever the
number is in the program counter
the number is going to be let's take a
look at this a knob for the very first
instruction
so that's a value of zero so the value
of zero
in our array the value of op nop points
to the
knob handler so as long as the
instructions that we put
into this array have the same indices as
the values we use to access them
then we can look up the instruction that
we care about or the handler for it
from the instructions op code so down
here we're going to say that the
current instruction
is coming from our instruction set
at program counter pointer so the value
of the op code
which is uh our instruction and then to
execute it we can get rid of all three
of these lines all four of these lines
and just call current instruction
no parameters so we're going to look up
the instruction in our instruction set
array get the handler for it and call
that handler to perform the instruction
doing it this way means that a little
bit of point-to-math pointer arithmetic
is going to go on on this line
where we calculate the offset into the
array for the
item that we want and add that to the
pointer to the first element in the
array
that's what the compiler is going to
generate under the hood and we can
actually take a look at that in a moment
when we build it
and then we're going to call that
instruction then we're going to update
our program counter to point to the next
value
so when we come around again we're going
to assign to current instruction
the new value which in this case will be
alert and then
we'll run the alert instruction update
our program counter
come around again so effectively this
loop
should never need to change from this
point on this
is our clock cycle this piece of code
right here so that should never need to
change now
i'm kind of lying because when we
enhance this virtual machine later
i'm going to actually want to pass
parameters into these functions just one
parameter in fact
so there will be a small modification
which will affect this loop
but essentially it is done it's not
going to change
and what that means is that as we add
instructions to our program
they'll get added here in the
initialization
their handlers will be added their
initialization will be added
but the clock cycle doesn't need
anything changing
so the code generated for the clock
cycle will remain at a fixed
size so let's go ahead and build our
program now and see what that size is
if we come back into the code generated
for the virtual machine
find my little marker values uh actually
let's take a look at some of this code
as we look through it
so up here we're actually we've done
initialized units that's
free pascal doing its initialization and
then we're loading the effective address
of the knob handler
and putting it into our instruction set
array then we're loading the halt
handler's
address and putting that into the array
the alert handler address putting that
into the array so that's our
initialization code right here
we're then setting running to true which
is right here we're then setting the
program counter to the first item
in our c test program array so that's
getting our
program counter to point to the knob
instruction okay so i don't
fully understand how to read this piece
of assembler but effectively what that's
doing
is getting our instruction set and using
our index
uh our program index into that
instruction set
to load up the uh the current
instruction pointer here
so we're getting our current instruction
pointer we are calling that
current instruction uh and then we are
coming
after calling that we're coming straight
back out to update our program counter
that's this instruction right here
and then we're testing to see if we
should still be running and repeating
our loop okay
so that is effectively our loop the
pieces of code that would appear between
our markers is that piece of code right
there
so if i take a look at this this was 26
lines for the first iteration
when we added an instruction we got 29
lines our current loop is a mere 14
lines
and it should never get any longer okay
so
as we add of this i'm being unfair there
was a label line so this is 15
okay 15 instructions um so it will never
get any longer because
as we add instructions in we're just
adding pointers to the handlers into the
array
that this loop is looking up
so that's how we can prevent this
virtual machine from growing more and
more ugly
let's make sure it actually still
functions
so i'm going to keep this in
initialization and put my debug messages
back in
so i'm going to put a rightlane in here
starting
virtual program and i'm also going to
put the
ending virtual program back in at the
bottom
but because we've broken out the
handlers now we can treat them
individually
so we'll say rightland knop
in here oops we can put another line in
the
halt handler we'll say rightlane
setting running
to false halt
and then in the alert handler well let's
put in rightland
alert okay so
that's effectively what our program
should do is we'll hit this
starting virtual program our program
counter is pointing at a knob
so we will write out a knob
then we will and update our program
counter
our program counts as pointing at alert
so we'll write out an alert
then we'll update our program counter
it's now pointing at halt
so we'll write out a where is it a halt
setting program to false and then we
will exit the program and write
out ending virtual program let's go
ahead and run that now
sure enough we start the virtual program
write out a not
because we did nothing write out an
alert because we had something to say
uh setting running to false that's the
halt instruction
and end our virtual program so at this
point we have
a working virtual machine the code is
not the prettiest it could be and in
fact
it would be nice to abstract all of this
away behind classes and
interfaces but it works and we can now
start adding
instructions now from our model of a
computer we looked at
the program counter register and an
accumulator register
so in the next part i'm going to
abstract some of this code away behind
interfaces and start adding some
registers
now when we add registers such as the
accumulator register and
maybe several different general purpose
registers those registers can be used by
our instructions
so if we want to do addition we'll come
back and add an addition
instruction into our instruction set and
then we'll be able to use the
registers to perform that edition so
that's what we're going to take a look
at
in the next part for now though we have
a working virtual machine
and hopefully you have a basic
understanding of how a virtual machine
functions
like subscribe and hit the bell