[otbn,dv] Misaligned load/store snippet Misaligned load-store generates valid but misaligned address which results with an error. Signed-off-by: Canberk Topal <ctopal@lowrisc.org> Co-authored-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/rig/rig/configs/base.yml b/hw/ip/otbn/dv/rig/rig/configs/base.yml index 9c590b8..3695435 100644 --- a/hw/ip/otbn/dv/rig/rig/configs/base.yml +++ b/hw/ip/otbn/dv/rig/rig/configs/base.yml
@@ -25,3 +25,4 @@ BadInsn: 1 BadGiantLoop: 1 BadZeroLoop: 1 + MisalignedLoadStore: 1
diff --git a/hw/ip/otbn/dv/rig/rig/gens/misaligned_load_store.py b/hw/ip/otbn/dv/rig/rig/gens/misaligned_load_store.py new file mode 100644 index 0000000..8ce69ee --- /dev/null +++ b/hw/ip/otbn/dv/rig/rig/gens/misaligned_load_store.py
@@ -0,0 +1,284 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +import random +from typing import Optional + +from shared.operand import ImmOperandType, RegOperandType, OptionOperandType +from shared.insn_yaml import Insn, InsnsFile + +from ..config import Config +from ..model import Model +from ..program import ProgInsn, Program + +from ..snippet import ProgSnippet +from ..snippet_gen import GenCont, GenRet, SnippetGen + + +class MisalignedLoadStore(SnippetGen): + '''A snippet generator that generates random load/store instructions + + Generated instructions are misaligned, therefore they end the + program. + + ''' + + ends_program = True + + def __init__(self, cfg: Config, insns_file: InsnsFile) -> None: + super().__init__() + + self.insns = [] + self.weights = [] + + self.sw = self._get_named_insn(insns_file, 'sw') + # SW Instruction Checks + # SW has three operands: grs1, offset, grs2 + if not (len(self.sw.operands) == 3 and + isinstance(self.sw.operands[0].op_type, RegOperandType) and + self.sw.operands[0].op_type.reg_type == 'gpr' and + not self.sw.operands[0].op_type.is_dest() and + isinstance(self.sw.operands[1].op_type, ImmOperandType) and + isinstance(self.sw.operands[2].op_type, RegOperandType) and + self.sw.operands[2].op_type.reg_type == 'gpr' and + not self.sw.operands[2].op_type.is_dest()): + raise RuntimeError('SW instruction from instructions file is ' + 'not the shape expected by the BadLoadStore' + 'generator.') + + self.lw = self._get_named_insn(insns_file, 'lw') + + # LW has three operands: grd, offset, grs + if not (len(self.lw.operands) == 3 and + isinstance(self.lw.operands[0].op_type, RegOperandType) and + self.lw.operands[0].op_type.reg_type == 'gpr' and + self.lw.operands[0].op_type.is_dest() and + isinstance(self.lw.operands[1].op_type, ImmOperandType) and + isinstance(self.lw.operands[2].op_type, RegOperandType) and + self.lw.operands[2].op_type.reg_type == 'gpr' and + not self.lw.operands[2].op_type.is_dest()): + raise RuntimeError('LW instruction from instructions file is ' + 'not the shape expected by the BadLoadStore' + 'generator.') + + # BN.LID Instruction Checks + self.bnlid = self._get_named_insn(insns_file, 'bn.lid') + + # bn.lid expects the operands: grd, grs1, offset, grs1_inc, grd_inc + if len(self.bnlid.operands) != 5: + raise RuntimeError('Unexpected number of operands for bn.lid') + + if not (isinstance(self.bnlid.operands[0].op_type, RegOperandType) and + (self.bnlid.operands[0].op_type.reg_type == 'gpr' and + not self.bnlid.operands[0].op_type.is_dest() and + isinstance(self.bnlid.operands[1].op_type, RegOperandType) and + self.bnlid.operands[1].op_type.reg_type == 'gpr' and + not self.bnlid.operands[1].op_type.is_dest() and + isinstance(self.bnlid.operands[2].op_type, ImmOperandType) and + self.bnlid.operands[2].op_type.signed and + isinstance(self.bnlid.operands[3].op_type, OptionOperandType) and + isinstance(self.bnlid.operands[4].op_type, OptionOperandType))): + raise RuntimeError('BN.LID instruction from instructions file is ' + 'not the shape expected by the BadLoadStore' + 'generator.') + + # BN.SID Instruction Checks + self.bnsid = self._get_named_insn(insns_file, 'bn.sid') + + # bn.sid expects the operands: grs1, grs2, offset, grs1_inc, + # grs2_inc + if len(self.bnsid.operands) != 5: + raise RuntimeError('Unexpected number of operands for bn.sid') + + if not (isinstance(self.bnsid.operands[0].op_type, RegOperandType) and + self.bnsid.operands[0].op_type.reg_type == 'gpr' and + not self.bnsid.operands[0].op_type.is_dest() and + isinstance(self.bnsid.operands[1].op_type, RegOperandType) and + self.bnsid.operands[1].op_type.reg_type == 'gpr' and + not self.bnsid.operands[1].op_type.is_dest() and + isinstance(self.bnsid.operands[2].op_type, ImmOperandType) and + self.bnsid.operands[2].op_type.signed and + isinstance(self.bnsid.operands[3].op_type, OptionOperandType) and + isinstance(self.bnsid.operands[4].op_type, OptionOperandType)): + raise RuntimeError('BN.LID instruction from instructions file is ' + 'not the shape expected by the BadLoadStore' + 'generator.') + + for insn in [self.sw, self.bnsid, self.lw, self.bnlid]: + weight = cfg.insn_weights.get(insn.mnemonic) + if weight > 0: + self.weights.append(weight) + self.insns.append(insn) + + # Check that at least one instruction has a positive weight + assert len(self.insns) == len(self.weights) + if not self.weights: + self.disabled = True + + def gen(self, + cont: GenCont, + model: Model, + program: Program) -> Optional[GenRet]: + + weights = self.weights + prog_insn = None + while prog_insn is None: + idx = random.choices(range(len(self.insns)), weights=weights)[0] + + # If all 4 instructions are not successful, give up. + if not weights[idx]: + return None + + # Try to fill out the instruction. On failure, clear the weight for + # this index and go around again. + prog_insn = self.fill_insn(self.insns[idx], model) + if prog_insn is None: + weights[idx] = 0 + continue + + weights = self.weights.copy() + snippet = ProgSnippet(model.pc, [prog_insn]) + snippet.insert_into_program(program) + + return (snippet, True, model) + + def fill_insn(self, insn: Insn, model: Model) -> Optional[ProgInsn]: + '''Try to pick one of BN.XID or XW instructions + + ''' + + # Special-case BN load/store instructions by mnemonic. These use + # complicated indirect addressing, so it's probably more sensible to + # give them special code. + if insn.mnemonic in ['bn.lid', 'bn.sid']: + return self._fill_bn_xid(insn, model) + if insn.mnemonic in ['lw', 'sw']: + return self._fill_xw(insn, model) + + return None + + def _fill_xw(self, insn: Insn, model: Model) -> Optional[ProgInsn]: + + grd_op_type = self.lw.operands[0].op_type + imm_op_type = self.lw.operands[1].op_type + + # Determine the offset range for XW + offset_rng = imm_op_type.get_op_val_range(model.pc) + assert offset_rng is not None + + # Max/Min Offsets for XW (-2048, 2047) + min_offset, max_offset = offset_rng + + # Get known registers + known_regs = model.regs_with_known_vals('gpr') + + base = [] + for reg_idx, reg_val in known_regs: + val = reg_val - (1 << 32) if reg_val >> 31 else reg_val + if 1 - max_offset <= val < model.dmem_size - min_offset: + base.append((reg_idx, val)) + + # We always have x0 as an eligible register for misaligned load/stores + assert base + + # Pick a random register among known registers that is constrained above + idx, value = random.choice(base) + + op_val_grs1 = idx + assert op_val_grs1 is not None + + imm_val = None + for _ in range(50): + # Pick a random offset value. We will change it later for + # guaranteeing misaligned addresses. + imm_val = random.randrange(min_offset, max_offset) + addr = imm_val + value + if (addr % 4) and (0 <= addr < model.dmem_size): + break + # The loop body above should succeed 75% of the time, so we should only + # get here without an imm_val one time in 2^200. + assert imm_val is not None + + offset_val = imm_op_type.op_val_to_enc_val(imm_val, model.pc) + assert offset_val is not None + + if insn.mnemonic == 'lw': + # Pick grd randomly. Since we can write to any register (including x0) + # this should always succeed. + op_val_grd = model.pick_operand_value(grd_op_type) + assert op_val_grd is not None + + op_val = [op_val_grd, offset_val, op_val_grs1] + + elif insn.mnemonic == 'sw': + # Any known register is okay for grs2 operand since it will be + # guaranteed to have a misaligned address to store the value from. + op_val_grs2 = random.choice(known_regs)[0] + + op_val = [op_val_grs2, offset_val, op_val_grs1] + + return ProgInsn(insn, op_val, ('dmem', 4096)) + + def _fill_bn_xid(self, insn: Insn, model: Model) -> Optional[ProgInsn]: + '''Fill out a BN.LID or BN.SID instruction''' + + bn_imm_op_type = self.bnlid.operands[2].op_type + + # Determine the offset range for BN.XID + bn_offset_rng = bn_imm_op_type.get_op_val_range(model.pc) + assert bn_offset_rng is not None + + # Max/Min Offsets for BN.XID (-512 * 32, 511 * 32) + min_offset, max_offset = bn_offset_rng + + # Get known registers + known_regs = model.regs_with_known_vals('gpr') + + base = [] + + # Get misaligned registers which can easily be in valid address range + for reg_idx, reg_val in known_regs: + val = reg_val - (1 << 32) if reg_val >> 31 else reg_val + if (((1 - max_offset <= val < model.dmem_size - min_offset) and + val % 32)): + base.append((reg_idx, val)) + + if not base: + return None + + # Pick a random register among known registers for initialization + idx, value = random.choice(base) + + for _ in range(50): + # Pick a random offset value. We will change it later for + # guaranteeing valid misaligned addresses. + bn_imm_val = random.randrange(min_offset, max_offset, 32) + addr = bn_imm_val + value + if (addr % 32) and (0 <= addr < model.dmem_size): + break + + bn_offset_val = bn_imm_op_type.op_val_to_enc_val(bn_imm_val, model.pc) + assert bn_offset_val is not None + + # Get the chosen base register index as grs1 operand. + op_val_grs1 = idx + assert op_val_grs1 is not None + + if insn.mnemonic == 'bn.lid': + # Pick grd randomly. Since we can write to any register (including x0) + # this should always succeed. + op_val_grd = model.pick_operand_value(self.bnlid.operands[0].op_type) + assert op_val_grd is not None + + op_val = [op_val_grd, op_val_grs1, bn_offset_val, 0, 0] + + elif insn.mnemonic == 'bn.sid': + # Any known register is okay for grs2 operand since it will be + # guaranteed to have an out of bounds address to store the value from. + op_val_grs2 = random.choice(known_regs)[0] + assert op_val_grs2 is not None + + op_val = [op_val_grs1, op_val_grs2, bn_offset_val, 0, 0] + + return ProgInsn(insn, op_val, ('dmem', 4096))
diff --git a/hw/ip/otbn/dv/rig/rig/snippet_gens.py b/hw/ip/otbn/dv/rig/rig/snippet_gens.py index 1647370..838bc1a 100644 --- a/hw/ip/otbn/dv/rig/rig/snippet_gens.py +++ b/hw/ip/otbn/dv/rig/rig/snippet_gens.py
@@ -32,6 +32,7 @@ from .gens.bad_giant_loop import BadGiantLoop from .gens.bad_load_store import BadLoadStore from .gens.bad_zero_loop import BadZeroLoop +from .gens.misaligned_load_store import MisalignedLoadStore class SnippetGens: @@ -56,7 +57,8 @@ BadInsn, BadGiantLoop, BadLoadStore, - BadZeroLoop + BadZeroLoop, + MisalignedLoadStore ] def __init__(self, cfg: Config, insns_file: InsnsFile) -> None: