Writing a VM Part Two

Filed under vms on December 06, 2022

When we last left off, we had a basic machine that could add two numbers. This time, we’ll add a new register to act as a set of flags to convey the machine status, instructions to allow us to set and reset these flags and a few other useful arithmetic instructions.

The New Architecture

We’re going to add a new register, which isn’t a large change

The new SR register

This will act not as register that holds a number, like the other registers in the CPU, but as a set of 32 binary flags we can use as indicators for things like errors and interrupts. Each bit in the register will be one flag, meaning we can mask the register with the flag value in order to get the value.

The register will be addressed with the number 0xC.

We’ll start with a few obvious flags for now:

  • 0x1 - This value will indicate a halt
  • 0x2 - Arithmetic overflows
  • 0x4 - Arithmetic underflows
  • 0x8 - Divide by zero

The new instructions

  • SUB - Subtract I2 from I1 and store the result in D
  • MUL - Multiply I1 by I2 and store the result in D
  • DIV - Divide I1 by I2 and store the result in D
  • STAT - Get the value of the I1th bit in the status register (SR) and store it in D
  • SET - Set the value of the I1th bit to I2

The coding

The new register

This is pretty straightforward, same as the old one. First, replace __reserved9 with SR in register.go

//...
	__reserved8
	SR
	PC
//...

And then make sure it exists in the map when we call NewRegisterBank:

func NewRegisterBank() *RegisterBank {
	return &RegisterBank{
		registerMap: map[uint8]*Register{
//...
			SR: {0x00},
//...
    }
  }
}

And we’ll add some more constants, so we can easily refer to our status flags in code

const (
	STATUS_HALT = 1 << iota
	STATUS_OVERFLOW
	STATUS_UNDERFLOW
	STATUS_DIVIDE_BY_ZERO
	STATUS_MEMORY_ERROR
)

The new instructions

Let’s start with arithmetic operations. We’ll also update our ADD operation to set the overflow if it needs to

func (c *CPU) add(i1, i2 *Register) {
	i1Val := uint64(i1.Value)
	i2Val := uint64(i2.Value)
	sum := i1Val + i2Val
	if sum > 0xFFFFFFFF {
		sr, err := c.registers.GetRegister(SR)
		// We'll panic here because if the status register doesn't work then our machine may as well crash
		if err != nil {
			panic(err)
		}
		sr.Value = sr.Value | STATUS_OVERFLOW
	}
	i2.Value = uint32(sum & 0xFFFFFFFF)
}

Similarly, SUB will do an underflow check since we technically don’t really support signed operations yet

func (c *CPU) sub(i1, i2 *Register) {
	i1Val := int64(i1.Value)
	i2Val := int64(i2.Value)
	diff := i1Val - i2Val
	if diff < 0 {
		sr, err := c.registers.GetRegister(SR)
		if err != nil {
			panic(err)
		}
		sr.Value = sr.Value | STATUS_UNDERFLOW
		diff = diff + 0xFFFFFFFF
	}
	i2.Value = uint32(diff & 0xFFFFFFFF)
}

MUL will check overflow

func (c *CPU) mul(i1, i2 *Register) {
	i1Val := int64(i1.value)
	i2Val := int64(i2.value)

	product := i1Val * i2Val

	if product > 0xFFFFFFFF {
		sr, err := c.registers.GetRegister(SR)
		if err != nil {
			panic(err)
		}
		sr.value = sr.value | STATUS_OVERFLOW
	}
	i2.value = uint32(product & 0xFFFFFFFF)
}

And finally, DIV will check the divide by zero status

func (c *CPU) div(i1, i2 *Register) {
	i1Val := i1.value
	i2Val := i2.value

	if i2Val == 0 {
		sr, err := c.registers.GetRegister(SR)
		if err != nil {
			panic(err)
		}
		sr.value = sr.value | STATUS_DIVIDE_BY_ZERO
		return
	}

	i2.value = i1Val / i2Val
}

Next, we’ll add in the instructions to get and set status flags

func (c *CPU) stat(i1, i2 *Register) {
	var bit uint32 = 1 << (i1.value - 1)
	sr, err := c.registers.GetRegister(SR)
	if err != nil {
		panic(err)
	}
	// Should be either 0 or 1
	i2.value = (sr.value & bit) / bit
}

func (c *CPU) set(i1, i2 *Register) {
	sr, err := c.registers.GetRegister(SR)
	if err != nil {
		panic(err)
	}

	var bit uint32 = 1 << (i1.value - 1)
	if i2.value > 0 {
		sr.value = sr.value | bit
	} else {
		sr.value = sr.value ^ bit
	}
}

And finally putting the new instructions in the opcode switch statement

switch opcode {
	//...
	case SUB:
		c.sub(i1, i2)
	case MUL:
		c.mul(i1, i2)
	case DIV:
		c.div(i1, i2)
	case STAT:
		c.stat(i1, i2)
	case SET:
		c.set(i1, i2)
	}

Retrofitting the new halt flag

Previously, we used the CPU.Halted boolean to track if our machine was halted. We’ll refactor a little to use that status flag instead, starting with our HALT operation

func (c *CPU) halt(_, _ *Register) {
	sr, err := c.registers.GetRegister(SR)
	if err != nil {
		panic(err)
	}
	sr.value = sr.value | STATUS_HALT
}

Next, we’ll fix up our Tick function to use that flag

func (c *CPU) Tick() error {
	sr, err := c.registers.GetRegister(SR)
	if err != nil {
		return err
	}
	if sr.value&STATUS_HALT > 0 {
		return fmt.Errorf("cannot tick on a Halted machine")
	}
	//...
}

Tidying up

As a kind of capstone, we’ll clean up the few spots that set Halted to true on a memory error, instead setting the memory error flag.

func (c *CPU) read(i1, i2 *Register) {
	val, err := c.bus.Read(i1.Value)
	if err != nil {
		sr, err := c.registers.GetRegister(SR)
		if err != nil {
			panic(err)
		}
		sr.Value = sr.Value | STATUS_MEMORY_ERROR
		return
	}
	i2.Value = val
}

func (c *CPU) write(i1, i2 *Register) {
	err := c.bus.Write(i2.Value, i1.Value)
	if err != nil {
		sr, err := c.registers.GetRegister(SR)
		if err != nil {
			panic(err)
		}
		sr.Value = sr.Value | STATUS_MEMORY_ERROR
	}
}

Conclusion

We have a machine that can tell us a little about itself now, on top of being able to perform some more simple arithmetic. Next up, we’ll implement some flow control to make loops and function calls possible. Code for this blog is available here


Stephen Gream

Written by Stephen Gream who lives and works in Melbourne, Australia. You should follow him on Minds