| #!/usr/bin/env python3 |
| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| '''A simulator that runs one instruction at a time, reading from a REPL |
| |
| The input language is simple (and intended to be generated by another program). |
| Input should appear with one command per line. |
| |
| The valid commands are as follows. All arguments are shown here as <argname>. |
| The integer arguments are read with Python's int() function, so should be |
| prefixed with "0x" if they are hexadecimal. |
| |
| start_operation Start an execution or DMEM/IMEM secure wipe |
| |
| step Run one instruction. Print trace information to |
| stdout. |
| |
| load_elf <path> Load the ELF file at <path>, replacing current |
| contents of DMEM and IMEM. |
| |
| add_loop_warp <addr> <from> <to> |
| |
| Add a loop warp to the simulation. This will |
| trigger at address <addr> and will jump from |
| iteration <from> to iteration <to>. |
| |
| clear_loop_warps Clear any loop warp rules |
| |
| load_d <path> Replace the current contents of DMEM with <path> |
| (read as an array of 32-bit little-endian words) |
| |
| load_i <path> Replace the current contents of IMEM with <path> |
| (read as an array of 32-bit little-endian words) |
| |
| dump_d <path> Write the current contents of DMEM to <path> (same |
| format as for load). |
| |
| print_regs Write the hex contents of all registers to stdout |
| |
| edn_rnd_step Send 32b RND Data to the model. |
| |
| edn_rnd_cdc_done Finish the RND data write process by signalling RTL |
| is also finished processing 32b packages from EDN. |
| |
| edn_urnd_step Send 32b URND seed data to the model. |
| |
| edn_urnd_cdc_done Finish the URND resseding process by signalling RTL |
| is also finished processing 32b packages from EDN |
| and set the seed. |
| |
| edn_flush Flush EDN data from model because of reset signal |
| in EDN clock domain |
| |
| otp_key_cdc_done Lowers the request flag for any external secure |
| wipe operation. Gets called when we acknowledge |
| incoming scrambling key in RTL. |
| |
| invalidate_imem Mark all of IMEM as having invalid ECC checksums |
| |
| invalidate_dmem Mark all of DMEM as having invalid ECC checksums |
| |
| set_keymgr_value Send keymgr data to the model. |
| |
| step_crc Step CRC function with 48 bits of data. No actual |
| change of state (this is pure, but handled in |
| Python to simplify verification). |
| |
| send_err_escalation React to an injected error. |
| |
| set_software_errs_fatal Set software_errs_fatal bit. |
| ''' |
| |
| import binascii |
| import sys |
| from typing import List, Optional |
| |
| from sim.decode import decode_file |
| from sim.load_elf import load_elf |
| from sim.sim import OTBNSim |
| |
| |
| def read_word(arg_name: str, word_data: str, bits: int) -> int: |
| '''Try to read an unsigned word of the specified bit length''' |
| try: |
| value = int(word_data, 0) |
| except ValueError: |
| raise ValueError('Failed to read {!r} as an integer for <{}> argument.' |
| .format(word_data, arg_name)) from None |
| |
| if value < 0 or value >> bits: |
| raise ValueError('<{}> argument is {!r}: ' |
| 'not representable in {!r} bits.' |
| .format(arg_name, word_data, bits)) |
| |
| return value |
| |
| |
| def end_command() -> None: |
| '''Print a single '.' to stdout and flush, ending the output for command''' |
| print('.') |
| sys.stdout.flush() |
| |
| |
| def check_arg_count(cmd: str, cnt: int, args: List[str]) -> None: |
| if len(args) != cnt: |
| if cnt == 0: |
| txt_cnt = 'no arguments' |
| elif cnt == 1: |
| txt_cnt = 'exactly one argument' |
| else: |
| txt_cnt = f'exactly {cnt} arguments' |
| |
| raise ValueError(f'{cmd} expects {txt_cnt} arguments. Got {args}.') |
| |
| |
| def on_start_operation(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('start_operation', 1, args) |
| command = args[0] |
| |
| if command == 'Execute': |
| print('START') |
| sim.start(collect_stats=False) |
| elif command == 'DmemWipe': |
| sim.start_mem_wipe(False) |
| elif command == 'ImemWipe': |
| sim.start_mem_wipe(True) |
| else: |
| raise ValueError(f'Invalid command for start_operation: {command}.') |
| |
| return None |
| |
| |
| def on_step(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Step one instruction''' |
| check_arg_count('step', 0, args) |
| |
| pc = sim.state.pc |
| assert 0 == pc & 3 |
| |
| was_wiping = sim.state.wiping() |
| |
| insn, changes = sim.step(verbose=False) |
| if insn is not None: |
| hdr = insn.rtl_trace(pc) # type: Optional[str] |
| elif was_wiping: |
| # The trailing space is a bit naff but matches the behaviour in the RTL |
| # tracer, where it's rather difficult to change. |
| hdr = 'U ' if sim.state.wiping() else 'V ' |
| elif sim.state.executing(): |
| hdr = 'STALL' |
| else: |
| hdr = None |
| |
| rtl_changes = [] |
| for c in changes: |
| rt = c.rtl_trace() |
| if rt is not None: |
| rtl_changes.append(rt) |
| |
| # This is a bit of a hack. Very occasionally, we'll see traced changes when |
| # there's not actually an instruction in flight. For example, this happens |
| # if there's a RND request still pending when an operation stops. In this |
| # case, we might see a change where we drop the REQ signal after the secure |
| # wipe has finished. Rather than define a special "do-nothing" trace entry |
| # format for this situation, we cheat and use STALL. |
| if hdr is None and rtl_changes: |
| hdr = 'STALL' |
| |
| if hdr is not None: |
| print(hdr) |
| for rt in rtl_changes: |
| print(rt) |
| |
| return None |
| |
| |
| def on_load_elf(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Load contents of ELF at path given by only argument''' |
| check_arg_count('load_elf', 1, args) |
| |
| path = args[0] |
| |
| print('LOAD_ELF {!r}'.format(path)) |
| load_elf(sim, path) |
| |
| return None |
| |
| |
| def on_add_loop_warp(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Add a loop warp to the simulation''' |
| check_arg_count('add_loop_warp', 3, args) |
| |
| try: |
| addr = int(args[0], 0) |
| if addr < 0: |
| raise ValueError('addr is negative') |
| from_cnt = int(args[1], 0) |
| if from_cnt < 0: |
| raise ValueError('from_cnt is negative') |
| to_cnt = int(args[2], 0) |
| if to_cnt < 0: |
| raise ValueError('to_cnt is negative') |
| except ValueError as err: |
| raise ValueError('Bad argument to add_loop_warp: {}' |
| .format(err)) from None |
| |
| print('ADD_LOOP_WARP {:#x} {} {}'.format(addr, from_cnt, to_cnt)) |
| sim.add_loop_warp(addr, from_cnt, to_cnt) |
| |
| return None |
| |
| |
| def on_clear_loop_warps(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Run until ecall or error''' |
| check_arg_count('clear_loop_warps', 0, args) |
| |
| sim.loop_warps = {} |
| |
| return None |
| |
| |
| def on_load_d(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Load contents of data memory from file at path given by only argument''' |
| check_arg_count('load_d', 1, args) |
| |
| path = args[0] |
| |
| print('LOAD_D {!r}'.format(path)) |
| with open(path, 'rb') as handle: |
| sim.load_data(handle.read(), has_validity=True) |
| |
| return None |
| |
| |
| def on_load_i(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Load contents of insn memory from file at path given by only argument''' |
| check_arg_count('load_i', 1, args) |
| |
| path = args[0] |
| |
| print('LOAD_I {!r}'.format(path)) |
| sim.load_program(decode_file(0, path)) |
| |
| return None |
| |
| |
| def on_dump_d(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Dump contents of data memory to file at path given by only argument''' |
| check_arg_count('dump_d', 1, args) |
| |
| path = args[0] |
| |
| print('DUMP_D {!r}'.format(path)) |
| |
| with open(path, 'wb') as handle: |
| handle.write(sim.state.dmem.dump_le_words()) |
| |
| return None |
| |
| |
| def on_print_regs(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Print registers to stdout''' |
| check_arg_count('print_regs', 0, args) |
| |
| print('PRINT_REGS') |
| for idx, value in enumerate(sim.state.gprs.peek_unsigned_values()): |
| print(' x{:<2} = 0x{:08x}'.format(idx, value)) |
| for idx, value in enumerate(sim.state.wdrs.peek_unsigned_values()): |
| print(' w{:<2} = 0x{:064x}'.format(idx, value)) |
| |
| return None |
| |
| |
| def on_print_call_stack(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| '''Print call stack to stdout. First element is the bottom of the stack''' |
| check_arg_count('print_call_stack', 0, args) |
| |
| print('PRINT_CALL_STACK') |
| for value in sim.state.peek_call_stack(): |
| print('0x{:08x}'.format(value)) |
| |
| return None |
| |
| |
| def on_reset(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('reset', 0, args) |
| return OTBNSim() |
| |
| |
| def on_edn_rnd_step(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('edn_rnd_step', 2, args) |
| edn_rnd_data = read_word('edn_rnd_step', args[0], 32) |
| fips_err = read_word('fips_err', args[1], 1) |
| sim.state.edn_rnd_step(edn_rnd_data, bool(fips_err)) |
| return None |
| |
| |
| def on_edn_urnd_step(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('edn_urnd_step', 1, args) |
| edn_urnd_data = read_word('edn_urnd_step', args[0], 32) |
| sim.state.edn_urnd_step(edn_urnd_data) |
| return None |
| |
| |
| def on_edn_flush(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('edn_flush', 0, args) |
| sim.state.edn_flush() |
| return None |
| |
| |
| def on_edn_urnd_cdc_done(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('urnd_cdc_done', 0, args) |
| sim.state.urnd_completed() |
| return None |
| |
| |
| def on_edn_rnd_cdc_done(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('edn_rnd_cdc_done', 0, args) |
| sim.state.rnd_completed() |
| return None |
| |
| |
| def on_invalidate_imem(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('invalidate_imem', 0, args) |
| |
| sim.state.invalidate_imem() |
| return None |
| |
| |
| def on_invalidate_dmem(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('invalidate_dmem', 0, args) |
| |
| sim.state.dmem.empty_dmem() |
| return None |
| |
| |
| def on_set_software_errs_fatal(sim: OTBNSim, |
| args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('set_software_errs_fatal', 1, args) |
| new_val = read_word('error', args[0], 1) |
| assert new_val in [0, 1] |
| sim.state.software_errs_fatal = new_val != 0 |
| |
| return None |
| |
| |
| def on_set_keymgr_value(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('set_keymgr_value', 3, args) |
| key0 = read_word('key0', args[0], 384) |
| key1 = read_word('key1', args[1], 384) |
| valid = read_word('valid', args[2], 1) == 1 |
| sim.state.wsrs.set_sideload_keys(key0 if valid else None, |
| key1 if valid else None) |
| |
| return None |
| |
| |
| def on_step_crc(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('step_crc', 2, args) |
| |
| item = read_word('item', args[0], 48) |
| state = read_word('state', args[1], 32) |
| |
| new_state = binascii.crc32(item.to_bytes(6, 'little'), state) |
| print(f'! otbn.LOAD_CHECKSUM: 0x{new_state:08x}') |
| |
| return None |
| |
| |
| def on_send_err_escalation(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('send_err_escalation', 1, args) |
| err_val = read_word('err_val', args[0], 32) |
| sim.send_err_escalation(err_val) |
| return None |
| |
| |
| def on_otp_cdc_done(sim: OTBNSim, args: List[str]) -> Optional[OTBNSim]: |
| check_arg_count('otp_key_cdc_done', 0, args) |
| |
| sim.on_otp_cdc_done() |
| return None |
| |
| |
| _HANDLERS = { |
| 'start_operation': on_start_operation, |
| 'otp_key_cdc_done': on_otp_cdc_done, |
| 'step': on_step, |
| 'load_elf': on_load_elf, |
| 'add_loop_warp': on_add_loop_warp, |
| 'clear_loop_warps': on_clear_loop_warps, |
| 'load_d': on_load_d, |
| 'load_i': on_load_i, |
| 'dump_d': on_dump_d, |
| 'print_regs': on_print_regs, |
| 'print_call_stack': on_print_call_stack, |
| 'reset': on_reset, |
| 'edn_rnd_step': on_edn_rnd_step, |
| 'edn_urnd_step': on_edn_urnd_step, |
| 'edn_rnd_cdc_done': on_edn_rnd_cdc_done, |
| 'edn_urnd_cdc_done': on_edn_urnd_cdc_done, |
| 'edn_flush': on_edn_flush, |
| 'invalidate_imem': on_invalidate_imem, |
| 'invalidate_dmem': on_invalidate_dmem, |
| 'set_keymgr_value': on_set_keymgr_value, |
| 'step_crc': on_step_crc, |
| 'send_err_escalation': on_send_err_escalation, |
| 'set_software_errs_fatal': on_set_software_errs_fatal |
| } |
| |
| |
| def on_input(sim: OTBNSim, line: str) -> Optional[OTBNSim]: |
| '''Process an input command''' |
| words = line.split() |
| |
| # Just ignore empty lines |
| if not words: |
| return None |
| |
| verb = words[0] |
| handler = _HANDLERS.get(verb) |
| if handler is None: |
| raise RuntimeError('Unknown command: {!r}'.format(verb)) |
| |
| ret = handler(sim, words[1:]) |
| print('.') |
| sys.stdout.flush() |
| |
| return ret |
| |
| |
| def main() -> int: |
| sim = OTBNSim() |
| try: |
| for line in sys.stdin: |
| ret = on_input(sim, line) |
| if ret is not None: |
| sim = ret |
| |
| except KeyboardInterrupt: |
| print("Received shutdown request, ending OTBN simulation.") |
| return 0 |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |