[otbn] Teach ISS to handle bad address errors

This catches out-of-range or misaligned accesses to dmem and bad PCs
for jump/branch destinations.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/otbnsim/sim/alert.py b/hw/ip/otbn/dv/otbnsim/sim/alert.py
index 449db83..f399bf2 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/alert.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/alert.py
@@ -24,7 +24,7 @@
     value that should be written to the ERR_CODE external register.
 
     '''
-    # Subclasses should override this class field
+    # Subclasses should override this class field or the error_code method
     err_code = None  # type: Optional[int]
 
     def error_code(self) -> int:
@@ -32,6 +32,27 @@
         return self.err_code
 
 
+class BadAddrError(Alert):
+    '''Raised when loading or storing or setting PC with a bad address'''
+
+    def __init__(self, operation: str, addr: int, what: str):
+        assert operation in ['pc',
+                             'narrow load', 'narrow store',
+                             'wide load', 'wide store']
+        self.operation = operation
+        self.addr = addr
+        self.what = what
+
+    def error_code(self) -> int:
+        return (ERR_CODE_BAD_INSN_ADDR
+                if self.operation == 'fetch'
+                else ERR_CODE_BAD_DATA_ADDR)
+
+    def __str__(self) -> str:
+        return ('Bad {} address of {:#08x}: {}.'
+                .format(self.operation, self.addr, self.what))
+
+
 class LoopError(Alert):
     '''Raised when doing something wrong with a LOOP/LOOPI'''
 
diff --git a/hw/ip/otbn/dv/otbnsim/sim/dmem.py b/hw/ip/otbn/dv/otbnsim/sim/dmem.py
index 6358668..39b327a 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/dmem.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/dmem.py
@@ -7,6 +7,7 @@
 
 from shared.mem_layout import get_memory_layout
 
+from .alert import BadAddrError
 from .trace import Trace
 
 
@@ -139,13 +140,34 @@
 
     def load_u256(self, addr: int) -> int:
         '''Read a u256 little-endian value from an aligned address'''
-        assert 0 == addr % 32
-        return self.data[addr // 32]
+        assert addr >= 0
+
+        if addr & 31:
+            raise BadAddrError('wide load', addr,
+                               'address is not 32-byte aligned')
+
+        word_addr = addr // 32
+
+        if word_addr >= len(self.data):
+            raise BadAddrError('wide load', addr,
+                               'address is above the top of dmem')
+
+        return self.data[word_addr]
 
     def store_u256(self, addr: int, value: int) -> None:
         '''Write a u256 little-endian value to an aligned address'''
-        assert 0 == addr % 32
+        assert addr >= 0
         assert 0 <= value < (1 << 256)
+
+        if addr & 31:
+            raise BadAddrError('wide store', addr,
+                               'address is not 32-byte aligned')
+
+        word_addr = addr // 32
+        if word_addr >= len(self.data):
+            raise BadAddrError('wide store', addr,
+                               'address is above the top of dmem')
+
         self.trace.append(TraceDmemStore(addr, value, True))
 
     def load_u32(self, addr: int) -> int:
@@ -155,8 +177,13 @@
         32-bit integer.
 
         '''
-        assert 0 == addr % 4
-        assert addr < 32 * len(self.data)
+        assert addr >= 0
+        if addr & 3:
+            raise BadAddrError('narrow load', addr,
+                               'address is not 4-byte aligned')
+        if (addr + 31) // 32 >= len(self.data):
+            raise BadAddrError('narrow load', addr,
+                               'address is above the top of dmem')
 
         idx32 = addr // 4
         idxW = idx32 // 8
@@ -170,9 +197,16 @@
         addr should be 4-byte aligned.
 
         '''
-        assert 0 == addr % 4
-        assert addr < 32 * len(self.data)
+        assert addr >= 0
         assert 0 <= value <= (1 << 32) - 1
+
+        if addr & 3:
+            raise BadAddrError('narrow load', addr,
+                               'address is not 4-byte aligned')
+        if (addr + 31) // 32 >= len(self.data):
+            raise BadAddrError('narrow load', addr,
+                               'address is above the top of dmem')
+
         self.trace.append(TraceDmemStore(addr, value, False))
 
     def changes(self) -> Sequence[Trace]:
diff --git a/hw/ip/otbn/dv/otbnsim/sim/state.py b/hw/ip/otbn/dv/otbnsim/sim/state.py
index c591f12..1d93d82 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/state.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/state.py
@@ -4,7 +4,9 @@
 
 from typing import List, Optional, Tuple
 
-from .alert import LoopError
+from shared.mem_layout import get_memory_layout
+
+from .alert import BadAddrError, LoopError
 from .csr import CSRFile
 from .dmem import Dmem
 from .ext_regs import OTBNExtRegs
@@ -143,6 +145,10 @@
 
         self.pc = 0
         self.pc_next = None  # type: Optional[int]
+
+        _, imem_size = get_memory_layout()['IMEM']
+        self.imem_size = imem_size
+
         self.dmem = Dmem()
 
         # Stall cycle support: if an instruction causes one or more stall
@@ -318,8 +324,32 @@
         '''Run before running an instruction'''
         self.loop_stack.check_insn(self.pc, insn_affects_control)
 
+    def check_jump_dest(self) -> None:
+        '''Check whether self.pc_next is a valid jump/branch target
+
+        If not, raises a BadAddrError.
+
+        '''
+        if self.pc_next is None:
+            return
+
+        # The PC should always be non-negative (it's an error in the simulator
+        # if that's come unstuck)
+        assert 0 <= self.pc_next
+
+        # Check the new PC is word-aligned
+        if self.pc_next & 3:
+            raise BadAddrError('pc', self.pc_next,
+                               'address is not 4-byte aligned')
+
+        # Check the new PC lies in instruction memory
+        if self.pc_next >= self.imem_size:
+            raise BadAddrError('pc', self.pc_next,
+                               'address lies above the top of imem')
+
     def post_insn(self) -> None:
         '''Update state after running an instruction but before commit'''
+        self.check_jump_dest()
         self.loop_step()
 
     def read_csr(self, idx: int) -> int: