From cd5795bb744869866bec72c272572e23cb89af2f Mon Sep 17 00:00:00 2001 From: Anton Lydike Date: Fri, 18 Feb 2022 10:17:12 +0100 Subject: [PATCH] fixed priv start code, added tests --- riscemu/priv/ImageLoader.py | 2 +- riscemu/priv/PrivCPU.py | 13 +++-- riscemu/priv/__main__.py | 37 +++++++------- test/end_to_end/__init__.py | 0 test/end_to_end/end_to_end_test.py | 73 ++++++++++++++++++++++++++++ test/test_isa.py | 77 ++++++++++++++++++++++++++++++ test/testcases/__main__.py | 53 ++++++++++++++++++++ test/testcases/half-loads.asm | 7 +++ test/testcases/symbols.asm | 20 ++++++++ 9 files changed, 261 insertions(+), 21 deletions(-) create mode 100644 test/end_to_end/__init__.py create mode 100644 test/end_to_end/end_to_end_test.py create mode 100644 test/test_isa.py create mode 100644 test/testcases/__main__.py create mode 100644 test/testcases/half-loads.asm create mode 100644 test/testcases/symbols.asm diff --git a/riscemu/priv/ImageLoader.py b/riscemu/priv/ImageLoader.py index b711568..9ef86e6 100644 --- a/riscemu/priv/ImageLoader.py +++ b/riscemu/priv/ImageLoader.py @@ -17,7 +17,7 @@ class MemoryImageLoader(ProgramLoader): @classmethod def can_parse(cls, source_path: str) -> float: - if source_path.split('.')[-1] == '.img': + if source_path.split('.')[-1] == 'img': return 1 return 0 diff --git a/riscemu/priv/PrivCPU.py b/riscemu/priv/PrivCPU.py index a6d9c5a..483300e 100644 --- a/riscemu/priv/PrivCPU.py +++ b/riscemu/priv/PrivCPU.py @@ -11,6 +11,7 @@ from .CSR import CSR from .ElfLoader import ElfBinaryFileLoader from .Exceptions import * from .ImageLoader import MemoryImageLoader +from .PrivMMU import PrivMMU from .PrivRV32I import PrivRV32I from .privmodes import PrivModes from ..instructions import RV32A, RV32M @@ -45,7 +46,7 @@ class PrivCPU(CPU): """ def __init__(self, conf): - super().__init__(MMU(), [PrivRV32I, RV32M, RV32A], conf) + super().__init__(PrivMMU(), [PrivRV32I, RV32M, RV32A], conf) # start in machine mode self.mode: PrivModes = PrivModes.MACHINE @@ -90,11 +91,17 @@ class PrivCPU(CPU): print() print(FMT_CPU + "[CPU] System stopped without halting - perhaps you stopped the debugger?" + FMT_NONE) - def launch(self, program: Program, verbose: bool = False): + def launch(self, program: Optional[Program] = None, verbose: bool = False): print(FMT_CPU + '[CPU] Started running from 0x{:08X} ({})'.format(self.pc, "kernel") + FMT_NONE) self._time_start = time.perf_counter_ns() // self.TIME_RESOLUTION_NS + self.run(self.conf.verbosity > 1 or verbose) + def load_program(self, program: Program): + if program.name == 'kernel': + self.pc = program.entrypoint + super().load_program(program) + def _init_csr(self): # set up CSR self.csr = CSR() @@ -230,4 +237,4 @@ class PrivCPU(CPU): def get_loaders(cls) -> typing.Iterable[Type[ProgramLoader]]: return [ AssemblyFileLoader, MemoryImageLoader, ElfBinaryFileLoader - ] \ No newline at end of file + ] diff --git a/riscemu/priv/__main__.py b/riscemu/priv/__main__.py index 2363de4..bbdd1fb 100644 --- a/riscemu/priv/__main__.py +++ b/riscemu/priv/__main__.py @@ -1,3 +1,5 @@ +from riscemu import RunConfig +from riscemu.types import Program from .PrivCPU import PrivCPU from .ElfLoader import ElfBinaryFileLoader from .ImageLoader import MemoryImageLoader @@ -9,26 +11,27 @@ if __name__ == '__main__': parser = argparse.ArgumentParser(description='RISC-V privileged architecture emulator', prog='riscemu') - parser.add_argument('--kernel', type=str, help='Kernel elf loaded with user programs', nargs='?') - parser.add_argument('--image', type=str, help='Memory image containing kernel', nargs='?') - parser.add_argument('--debug-exceptions', help='Launch the interactive debugger when an exception is generated', action='store_true') + parser.add_argument('source', type=str, + help='Compiled RISC-V ELF file or memory image containing compiled RISC-V ELF files', nargs='+') + parser.add_argument('--debug-exceptions', help='Launch the interactive debugger when an exception is generated', + action='store_true') - parser.add_argument('-v', '--verbose', help="Verbosity level (can be used multiple times)", action='count', default=0) + parser.add_argument('-v', '--verbose', help="Verbosity level (can be used multiple times)", action='count', + default=0) args = parser.parse_args() - mmu = None - - if args.kernel is not None: - mmu = LoadedElfMMU(ElfExecutable(args.kernel)) - elif args.image is not None: - mmu = MemoryImageMMU(args.image) - - if mmu is None: - print("You must specify one of --kernel or --image for running in privilege mode!") - sys.exit(1) - - cpu = PrivCPU(RunConfig(verbosity=args.verbose, debug_on_exception=args.debug_exceptions), mmu) - cpu.run() + cpu = PrivCPU(RunConfig(verbosity=args.verbose, debug_on_exception=args.debug_exceptions)) + for source_path in args.source: + loader = max((loader for loader in cpu.get_loaders()), key=lambda l: l.can_parse(source_path)) + argv, opts = loader.get_options(sys.argv) + program = loader.instantiate(source_path, opts).parse() + if isinstance(program, Program): + cpu.load_program(program) + else: + program_iter = program + for program in program_iter: + cpu.load_program(program) + cpu.launch() diff --git a/test/end_to_end/__init__.py b/test/end_to_end/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/end_to_end/end_to_end_test.py b/test/end_to_end/end_to_end_test.py new file mode 100644 index 0000000..c1a4e90 --- /dev/null +++ b/test/end_to_end/end_to_end_test.py @@ -0,0 +1,73 @@ +import contextlib +import os +from abc import abstractmethod +from tempfile import NamedTemporaryFile +from typing import Optional, Union, Tuple +from unittest import TestCase + +from riscemu import CPU, UserModeCPU, InstructionSetDict, RunConfig +from riscemu.types import Program + + +class EndToEndTest(TestCase): + + def __init__(self, cpu: Optional[CPU] = None): + super().__init__() + if cpu is None: + cpu = UserModeCPU(InstructionSetDict.values(), RunConfig()) + self.cpu = cpu + + @abstractmethod + def get_source(self) -> Tuple[str, Union[bytes, str, bytearray]]: + """ + This method returns the source code of the program + :return: + """ + pass + + def test_run_program(self): + """ + Runs the program and verifies output + :return: + """ + with self.with_source_file() as names: + fname, orig_name = names + loader = self.cpu.get_best_loader_for(fname) + self.program = loader.instantiate(fname, loader.get_options([])).parse() + self._change_program_file_name(self.program, orig_name) + self.cpu.load_program(self.program) + self.after_program_load(self.program) + if isinstance(self.cpu, UserModeCPU): + self.cpu.setup_stack() + try: + self.cpu.launch(self.program) + except Exception as ex: + if self.is_exception_expected(ex): + pass + raise ex + + @contextlib.contextmanager + def with_source_file(self): + name, content = self.get_source() + if isinstance(content, str): + f = NamedTemporaryFile('w', suffix=name, delete=False) + else: + f = NamedTemporaryFile('wb', suffix=name, delete=False) + f.write(content) + f.flush() + f.close() + try: + yield f.name, name + finally: + os.unlink(f.name) + + def after_program_load(self, program): + pass + + def is_exception_expected(self, ex: Exception) -> bool: + return False + + def _change_program_file_name(self, program: Program, new_name: str): + program.name = new_name + for sec in program.sections: + sec.owner = new_name diff --git a/test/test_isa.py b/test/test_isa.py new file mode 100644 index 0000000..80a7a13 --- /dev/null +++ b/test/test_isa.py @@ -0,0 +1,77 @@ +from riscemu.colors import FMT_ERROR, FMT_NONE, FMT_BOLD, FMT_GREEN +from riscemu.exceptions import ASSERT_LEN +from riscemu.helpers import int_from_bytes +from riscemu.instructions import InstructionSet +from riscemu.types import Instruction, CPU +from riscemu.decoder import RISCV_REGS + +FMT_SUCCESS = FMT_GREEN + FMT_BOLD + + +def assert_equals(ins: Instruction, cpu: CPU): + a, b = (get_arg_from_ins(ins, i, cpu) for i in (0, 2)) + return a == b + + +def assert_equals_mem(ins: Instruction, cpu: CPU): + a, b = (get_arg_from_ins(ins, i, cpu) for i in (0, 2)) + a = cpu.mmu.read_int(a) + return a == b + + +def assert_in(ins: Instruction, cpu: CPU): + a = get_arg_from_ins(ins, 0, cpu) + others = [get_arg_from_ins(ins, i, cpu) for i in range(2, len(ins.args))] + return a in others + + +def _not(func): + def test(ins: Instruction, cpu: CPU): + return not func(ins, cpu) + + return test + + +def get_arg_from_ins(ins: Instruction, num: int, cpu: CPU): + a = ins.args[num] + if a in RISCV_REGS: + return cpu.regs.get(a) + return ins.get_imm(num) + + +assert_ops = { + '==': assert_equals, + '!=': _not(assert_equals), + 'in': assert_in, + 'not_in': _not(assert_in), +} + + +class TestIS(InstructionSet): + def __init__(self, cpu: 'CPU'): + print('[Test] loading testing ISA, this is only meant for running testcases and is not part of the RISC-V ISA!') + self.failed = False + super().__init__(cpu) + + def instruction_assert(self, ins: Instruction): + if len(ins.args) < 3: + print(FMT_ERROR + '[Test] Unknown assert statement: {}'.format(ins) + FMT_NONE) + return + op = ins.args[1] + if op not in assert_ops: + print(FMT_ERROR + '[Test] Unknown operation statement: {} in {}'.format(op, ins) + FMT_NONE) + return + + if assert_ops[op](ins, self.cpu): + print(FMT_SUCCESS + '[TestCase] 🟢 passed assertion {}'.format(ins)) + else: + print(FMT_ERROR + '[TestCase] 🔴 failed assertion {}'.format(ins)) + self.cpu.halted = True + self.failed = True + + def instruction_fail(self, ins: Instruction): + print(FMT_ERROR + '[TestCase] 🔴 reached fail instruction! {}'.format(ins)) + self.cpu.halted = True + self.failed = True + + def assert_mem(self, ins: Instruction): \ No newline at end of file diff --git a/test/testcases/__main__.py b/test/testcases/__main__.py new file mode 100644 index 0000000..2b8fd43 --- /dev/null +++ b/test/testcases/__main__.py @@ -0,0 +1,53 @@ +from riscemu import AssemblyFileLoader +from riscemu.colors import * + +FMT_SUCCESS = FMT_GREEN + FMT_BOLD + +def run_test(path: str): + from riscemu import CPU, UserModeCPU, RunConfig + from riscemu.instructions import InstructionSetDict + from test.test_isa import TestIS + import os + + fname = os.path.basename(path) + + ISAs = list(InstructionSetDict.values()) + ISAs.append(TestIS) + + cpu = UserModeCPU(ISAs, RunConfig()) + try: + program = AssemblyFileLoader(path, {}).parse() + cpu.load_program(program) + cpu.launch(program) + except Exception as ex: + print(FMT_ERROR + '[Test] 🔴 failed with exception "{}" ({})'.format(ex, fname) + FMT_NONE) + raise ex + + if cpu.halted: + for isa in cpu.instruction_sets: + if isinstance(isa, TestIS): + if not isa.failed: + print(FMT_SUCCESS + '[Test] 🟢 successful {}'.format(fname) + FMT_NONE) + return not isa.failed + return False + + +if __name__ == '__main__': + + import os + import glob + + successes = 0 + failures = 0 + ttl = 0 + + for path in glob.glob(f'{os.path.dirname(__file__)}/*.asm'): + print(FMT_BLUE + '[Test] running testcase ' + os.path.basename(path) + FMT_NONE) + ttl += 1 + if run_test(path): + successes += 1 + else: + failures += 1 + + + diff --git a/test/testcases/half-loads.asm b/test/testcases/half-loads.asm new file mode 100644 index 0000000..f0a2ce5 --- /dev/null +++ b/test/testcases/half-loads.asm @@ -0,0 +1,7 @@ +.data + +data: +.word 0xFFFFFFFF, 0x0000FFFF, 0xFF00FF00, 0x7FFFFFFF + +.text + ebreak diff --git a/test/testcases/symbols.asm b/test/testcases/symbols.asm new file mode 100644 index 0000000..107ab45 --- /dev/null +++ b/test/testcases/symbols.asm @@ -0,0 +1,20 @@ +.text + +main: + addi a0, zero, main + addi a1, zero, main + addi t0, zero, 1000 + assert a0, ==, 0x100 +1: + addi a1, a1, 1 + blt a1, t0, 1b + sub a1, a1, a0 + j 1f + addi a1, zero, 0 + fail +1: + assert a1, ==, 744 + add a0, zero, a1 ; set exit code to a1 + addi a7, zero, SCALL_EXIT ; exit syscall code + scall + fail \ No newline at end of file