├── riscemu ├── py.typed ├── IO │ ├── __init__.py │ ├── IOModule.py │ └── TextIO.py ├── tools │ └── riscemu ├── core │ ├── privmodes.py │ ├── flags.py │ ├── platform.py │ ├── rtclock.py │ ├── instruction_memory_section.py │ ├── simple_instruction.py │ ├── csr_constants.py │ ├── binary_data_memory_section.py │ ├── instruction.py │ ├── instruction_context.py │ ├── program_loader.py │ ├── __init__.py │ ├── traps.py │ ├── program.py │ ├── cpu.py │ ├── exceptions.py │ └── usermode_cpu.py ├── decoder │ ├── __init__.py │ ├── regs.py │ ├── __main__.py │ ├── formatter.py │ ├── instruction_table.py │ ├── formats.py │ └── decoder.py ├── libc │ ├── crt0.s │ ├── README.md │ ├── stdlib.s │ └── string.s ├── __init__.py ├── priv │ ├── __init__.py │ ├── PrivMMU.py │ ├── __main__.py │ ├── ImageLoader.py │ ├── ElfLoader.py │ └── CSR.py ├── colors.py ├── __main__.py ├── instructions │ ├── __init__.py │ ├── RV32M.py │ ├── RV_Debug.py │ ├── RV32A.py │ ├── Zicsr.py │ ├── RV32D.py │ ├── instruction_set.py │ └── RV32F.py ├── config.py ├── interactive.py ├── debug.py ├── helpers.py └── tokenizer.py ├── snitch ├── __init__.py ├── __main__.py ├── xssr.py ├── regs.py └── frep.py ├── sphinx-docs ├── requirements.txt ├── .gitignore ├── Makefile ├── make.bat └── source │ ├── index.rst │ └── conf.py ├── test ├── filecheck │ ├── .gitignore │ ├── lit.cfg │ ├── p2align.asm │ ├── hello-world.asm │ ├── hello-world-register-indices.asm │ ├── snitch │ │ ├── frep_only.asm │ │ ├── ssr_only.asm │ │ └── ssr_frep.asm │ ├── rv32m-conv.asm │ ├── fibs.asm │ ├── csr.asm │ ├── rv32f-conv.asm │ └── libc │ │ └── test-string.s ├── __init__.py ├── test_helpers.py ├── test_mstatus.py ├── test_regs.py ├── test_float_impl.py ├── test_integers.py ├── test_instruction.py ├── test_isa.py ├── test_RV32F.py └── test_tokenizer.py ├── docs ├── debug-session.png ├── libraries.md ├── PythonDemo.ipynb ├── internal-structure.md ├── assembly.md ├── syscalls.md └── debugging.md ├── .git-blame-ignored-commits ├── .gitignore ├── examples ├── mul-float.asm ├── static-data.asm ├── hello-world.asm ├── snitch_simple.asm ├── fibs.asm ├── malloc.asm └── estimate-cpu-freq.asm ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .github ├── workflows │ ├── pythonpublish.yml │ ├── code-formatting.yml │ └── ci-pytest.yml └── pull_request_template.md ├── generate-docs.sh ├── pyproject.toml ├── LICENSE ├── CHANGELOG.md └── README.md /riscemu/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /snitch/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /riscemu/IO/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sphinx-docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | recommonmark 3 | -------------------------------------------------------------------------------- /test/filecheck/.gitignore: -------------------------------------------------------------------------------- 1 | .lit_test_times.txt 2 | Output 3 | -------------------------------------------------------------------------------- /riscemu/tools/riscemu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | python3 -m riscemu "$@" 3 | -------------------------------------------------------------------------------- /sphinx-docs/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | source/riscemu*.rst 3 | source/modules.rst 4 | source/help 5 | -------------------------------------------------------------------------------- /docs/debug-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AntonLydike/riscemu/HEAD/docs/debug-session.png -------------------------------------------------------------------------------- /.git-blame-ignored-commits: -------------------------------------------------------------------------------- 1 | # introduces black formatting 2 | 5515c7795cfd690d346aad10ce17b30acf914648 3 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_tokenizer import * 2 | from .test_helpers import * 3 | from .test_integers import * 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .mypy_cache 3 | .pytest_cache 4 | 5 | /venv* 6 | /dist 7 | /riscemu.egg-info 8 | /build 9 | .idea 10 | -------------------------------------------------------------------------------- /riscemu/core/privmodes.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class PrivModes(IntEnum): 5 | USER = 0 6 | SUPER = 1 7 | HYPER = 2 8 | MACHINE = 3 9 | -------------------------------------------------------------------------------- /riscemu/decoder/__init__.py: -------------------------------------------------------------------------------- 1 | from .decoder import decode, RISCV_REGS 2 | from .formatter import format_ins 3 | 4 | __all__ = [ 5 | decode, 6 | RISCV_REGS, 7 | format_ins, 8 | ] 9 | -------------------------------------------------------------------------------- /examples/mul-float.asm: -------------------------------------------------------------------------------- 1 | main: 2 | li a1, 1084227584 3 | li a2, 1082130432 4 | fcvt.s.wu ft0, a1 5 | fcvt.s.wu ft1, a2 6 | fmul.s ft6, ft0, ft1 7 | print.float ft6 8 | // exit gracefully 9 | addi a0, zero, 0 10 | addi a7, zero, 93 11 | scall // exit with code 0 12 | -------------------------------------------------------------------------------- /riscemu/core/flags.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(frozen=True) 5 | class MemoryFlags: 6 | read_only: bool 7 | executable: bool 8 | 9 | def __repr__(self): 10 | return "r{}{}".format( 11 | "-" if self.read_only else "w", "x" if self.executable else "-" 12 | ) 13 | -------------------------------------------------------------------------------- /test/filecheck/lit.cfg: -------------------------------------------------------------------------------- 1 | import lit.formats 2 | import os 3 | 4 | config.test_source_root = os.path.dirname(__file__) 5 | xdsl_src = os.path.dirname(os.path.dirname(config.test_source_root)) 6 | 7 | config.name = "riscemu" 8 | config.test_format = lit.formats.ShTest(preamble_commands=[f"cd {xdsl_src}"]) 9 | config.suffixes = ['.asm', '.s'] 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - repo: https://github.com/psf/black 10 | rev: 23.3.0 11 | hooks: 12 | - id: black 13 | -------------------------------------------------------------------------------- /riscemu/libc/crt0.s: -------------------------------------------------------------------------------- 1 | // A minimal crt0.s that works along the stdlib.s file provided to give 2 | // some resemblance of a functioning compilation target :) 3 | // 4 | // Copyright (c) 2023 Anton Lydike 5 | // SPDX-License-Identifier: MIT 6 | 7 | .text 8 | 9 | .globl _start 10 | _start: 11 | // TODO: read argc, argv from a0, a1 12 | // maybe even find envptr? 13 | jal main 14 | jal exit 15 | -------------------------------------------------------------------------------- /test/filecheck/p2align.asm: -------------------------------------------------------------------------------- 1 | // RUN: riscemu -o libc %s | filecheck %s 2 | .data 3 | .space 8 4 | 5 | .globl main 6 | .text 7 | 8 | text_start: 9 | .p2align 8 10 | main: 11 | la a0, main 12 | la a1, text_start 13 | sub a0, a0, a1 14 | print a0 15 | // align to 2**8 bytes, so 256 16 | // we have 8 bytes of padding at the front, so we should see 248 bytes between text_start and main 17 | // CHECK: register a0 contains value 248 18 | li a0, 0 19 | ret 20 | -------------------------------------------------------------------------------- /test/test_helpers.py: -------------------------------------------------------------------------------- 1 | from riscemu.helpers import * 2 | 3 | 4 | def test_align_address(): 5 | assert align_addr(3, 1) == 3 6 | assert align_addr(3, 2) == 4 7 | assert align_addr(3, 4) == 4 8 | assert align_addr(3, 8) == 8 9 | assert align_addr(8, 8) == 8 10 | 11 | 12 | def test_parse_numeric(): 13 | assert parse_numeric_argument("13") == 13 14 | assert parse_numeric_argument("0x100") == 256 15 | assert parse_numeric_argument("-13") == -13 16 | -------------------------------------------------------------------------------- /riscemu/decoder/regs.py: -------------------------------------------------------------------------------- 1 | RISCV_REGS = [ 2 | "zero", 3 | "ra", 4 | "sp", 5 | "gp", 6 | "tp", 7 | "t0", 8 | "t1", 9 | "t2", 10 | "s0", 11 | "s1", 12 | "a0", 13 | "a1", 14 | "a2", 15 | "a3", 16 | "a4", 17 | "a5", 18 | "a6", 19 | "a7", 20 | "s2", 21 | "s3", 22 | "s4", 23 | "s5", 24 | "s6", 25 | "s7", 26 | "s8", 27 | "s9", 28 | "s10", 29 | "s11", 30 | "t3", 31 | "t4", 32 | "t5", 33 | "t6", 34 | ] 35 | -------------------------------------------------------------------------------- /examples/static-data.asm: -------------------------------------------------------------------------------- 1 | .data 2 | my_data: 3 | .word 0x11223344, 0x55667788, 0x9900aabb, 0xccddeeff 4 | 5 | .text 6 | main: 7 | // load base address into t0 8 | la t0, my_data 9 | // begin loading words and printing them 10 | lw a0, 0(t0) 11 | print.uhex a0 12 | lw a0, 4(t0) 13 | print.uhex a0 14 | lw a0, 8(t0) 15 | print.uhex a0 16 | lw a0, 12(t0) 17 | print.uhex a0 18 | // exit 19 | li a7, 93 20 | ecall 21 | -------------------------------------------------------------------------------- /riscemu/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | This package aims at providing an all-round usable RISC-V emulator and debugger 7 | 8 | It contains everything needed to run assembly files, so you don't need any custom compilers or toolchains 9 | """ 10 | 11 | # to read package version: 12 | import importlib.metadata 13 | 14 | __author__ = "Anton Lydike " 15 | __copyright__ = "Copyright 2023 Anton Lydike" 16 | __version__ = importlib.metadata.version(__name__) 17 | -------------------------------------------------------------------------------- /riscemu/priv/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | The priv Module holds everything necessary for emulating privileged risc-v assembly 7 | 8 | 9 | Running priv is only preferable to the normal userspace emulator, if you actually want to emulate the whole system. 10 | 11 | Syscalls will have to be intercepted by your assembly code. 12 | 13 | 14 | The PrivCPU Implements the Risc-V M/U Model, meaning there is machine mode and user mode. No PMP or paging is available. 15 | """ 16 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | builder: html 11 | configuration: sphinx-docs/source/conf.py 12 | 13 | # Optionally set the version of Python and requirements required to build your docs 14 | python: 15 | version: "3.7" 16 | system_packages: true 17 | install: 18 | - requirements: sphinx-docs/requirements.txt 19 | -------------------------------------------------------------------------------- /examples/hello-world.asm: -------------------------------------------------------------------------------- 1 | ; hello-world.asm 2 | ; print "hello world" to stdout and exit 3 | .data 4 | msg: .ascii "Hello world\n" 5 | .text 6 | addi a0, zero, 1 ; print to stdout 7 | addi a1, zero, msg ; load msg address 8 | addi a2, zero, 12 ; write 12 bytes 9 | addi a7, zero, SCALL_WRITE ; write syscall code 10 | scall 11 | addi a0, zero, 0 ; set exit code to 0 12 | addi a7, zero, SCALL_EXIT ; exit syscall code 13 | scall 14 | -------------------------------------------------------------------------------- /riscemu/core/platform.py: -------------------------------------------------------------------------------- 1 | from .cpu import CPU 2 | from .memory_section import MemorySection 3 | 4 | from typing import List 5 | 6 | 7 | class Platform: 8 | """ 9 | This is a wrapper around a given hardware configuration. 10 | """ 11 | 12 | harts: List[CPU] 13 | 14 | memory: List[MemorySection] 15 | 16 | def __init__(self): 17 | pass 18 | 19 | def step(self): 20 | performed_step = False 21 | 22 | for cpu in self.harts: 23 | if not cpu.halted: 24 | cpu.step(cpu.conf.verbosity > 1) 25 | performed_step = True 26 | 27 | return performed_step 28 | -------------------------------------------------------------------------------- /test/filecheck/hello-world.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m riscemu -v %s | filecheck %s 2 | .data 3 | msg: .ascii "Hello world\n" 4 | .text 5 | addi a0, zero, 1 // print to stdout 6 | addi a1, zero, msg // load msg address 7 | addi a2, zero, 12 // write 12 bytes 8 | addi a7, zero, SCALL_WRITE // write syscall code 9 | scall 10 | addi a0, zero, 0 // set exit code to 0 11 | addi a7, zero, SCALL_EXIT // exit syscall code 12 | scall 13 | 14 | // CHECK: Hello world 15 | // CHECK: [CPU] Program exited with code 0 16 | -------------------------------------------------------------------------------- /test/filecheck/hello-world-register-indices.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m riscemu -v %s | filecheck %s 2 | .data 3 | msg: .ascii "Hello world\n" 4 | .text 5 | addi x10, x0, 1 ; print to stdout 6 | addi x11, x0, msg ; load msg address 7 | addi x12, x0, 12 ; write 12 bytes 8 | addi x17, x0, SCALL_WRITE ; write syscall code 9 | scall 10 | addi x10, x0, 0 ; set exit code to 0 11 | addi x17, x0, SCALL_EXIT ; exit syscall code 12 | scall 13 | 14 | // CHECK: Hello world 15 | // CHECK: [CPU] Program exited with code 0 16 | -------------------------------------------------------------------------------- /riscemu/decoder/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | import code 3 | import readline 4 | import rlcompleter 5 | 6 | from .decoder import * 7 | from .formats import * 8 | from .instruction_table import * 9 | from .regs import RISCV_REGS 10 | 11 | sess_vars = globals() 12 | sess_vars.update(locals()) 13 | 14 | readline.set_completer(rlcompleter.Completer(sess_vars).complete) 15 | readline.set_completer(rlcompleter.Completer(sess_vars).complete) 16 | readline.parse_and_bind("tab: complete") 17 | code.InteractiveConsole(sess_vars).interact( 18 | banner="Interactive decoding session started...", exitmsg="Closing..." 19 | ) 20 | -------------------------------------------------------------------------------- /test/filecheck/snitch/frep_only.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m snitch %s -o libc -v | filecheck %s 2 | 3 | .text 4 | .globl main 5 | main: 6 | // load constants 7 | li t0, 0 8 | fcvt.s.w ft0, t0 9 | li t0, 1 10 | fcvt.s.w ft1, t0 11 | 12 | // repeat 100 times 13 | li t0, 99 14 | frep.i t0, 1, 0, 0 15 | fadd.s ft0, ft0, ft1 // add one 16 | 17 | // print result to stdout 18 | printf "100 * 1 = {:f32}", ft0 19 | // CHECK: 100 * 1 = 100.0 20 | // return 0 21 | li a0, 0 22 | ret 23 | 24 | // CHECK-NEXT: [CPU] Program exited with code 0 25 | -------------------------------------------------------------------------------- /riscemu/colors.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | 7 | # Colors 8 | 9 | FMT_RED = "\033[31m" 10 | FMT_ORANGE = "\033[33m" 11 | FMT_GRAY = "\033[37m" 12 | FMT_CYAN = "\033[36m" 13 | FMT_GREEN = "\033[32m" 14 | FMT_MAGENTA = "\033[35m" 15 | FMT_BLUE = "\033[34m" 16 | FMT_YELLOW = "\033[93m" 17 | 18 | FMT_BOLD = "\033[1m" 19 | FMT_NONE = "\033[0m" 20 | FMT_UNDERLINE = "\033[4m" 21 | 22 | FMT_ERROR = FMT_RED + FMT_BOLD 23 | 24 | FMT_MEM = FMT_CYAN + FMT_BOLD 25 | FMT_PARSE = FMT_CYAN + FMT_BOLD 26 | FMT_CPU = FMT_BLUE + FMT_BOLD 27 | FMT_SYSCALL = FMT_YELLOW + FMT_BOLD 28 | FMT_DEBUG = FMT_MAGENTA + FMT_BOLD 29 | FMT_CSR = FMT_ORANGE + FMT_BOLD 30 | -------------------------------------------------------------------------------- /riscemu/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | This file holds the logic for starting the emulator from the CLI 7 | """ 8 | import sys 9 | 10 | from .core import RiscemuBaseException 11 | from .riscemu_main import RiscemuMain 12 | 13 | 14 | def main(): 15 | try: 16 | main = RiscemuMain() 17 | main.run_from_cli(sys.argv[1:]) 18 | sys.exit(main.cpu.exit_code if not main.cfg.ignore_exit_code else 0) 19 | 20 | except RiscemuBaseException as e: 21 | print("Error: {}".format(e.message())) 22 | e.print_stacktrace() 23 | 24 | sys.exit(-1) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /sphinx-docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /test/test_mstatus.py: -------------------------------------------------------------------------------- 1 | from riscemu.core.csr import MStatusRegister 2 | 3 | 4 | def test_mstatus_bits(): 5 | status = MStatusRegister() 6 | 7 | status.mpie = 1 8 | 9 | assert "{:032b}".format(int(status.state)) == "00000000000000000000000010000000" 10 | 11 | status.mpie = 0 12 | 13 | assert "{:032b}".format(int(status.state)) == "00000000000000000000000000000000" 14 | 15 | status.mpp = 3 16 | 17 | assert "{:032b}".format(int(status.state)) == "00000000000000000001100000000000" 18 | 19 | status.sd = 1 20 | 21 | assert "{:032b}".format(int(status.state)) == "10000000000000000001100000000000" 22 | 23 | status.mpp = 1 24 | 25 | assert "{:032b}".format(int(status.state)) == "10000000000000000000100000000000" 26 | -------------------------------------------------------------------------------- /snitch/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | This file holds the logic for starting the emulator from the CLI 7 | """ 8 | import sys 9 | 10 | from .xssr import Xssr_pseudo 11 | from .frep import FrepEnabledCpu, Xfrep 12 | from riscemu.riscemu_main import RiscemuMain 13 | 14 | 15 | class SnitchMain(RiscemuMain): 16 | def instantiate_cpu(self): 17 | self.cpu = FrepEnabledCpu(self.selected_ins_sets, self.cfg) 18 | self.configure_cpu() 19 | 20 | def register_all_isas(self): 21 | super().register_all_isas() 22 | self.available_ins_sets.update({"Xssr": Xssr_pseudo, "Xfrep": Xfrep}) 23 | 24 | 25 | if __name__ == "__main__": 26 | SnitchMain().run_from_cli(sys.argv) 27 | -------------------------------------------------------------------------------- /riscemu/libc/README.md: -------------------------------------------------------------------------------- 1 | # RiscEmu LibC 2 | 3 | This is a very basic implementation of libc in risc-v assembly, meant specifically for the riscemu emulator. 4 | 5 | This is currently very incomplete, only a handful of methods are implemented, and most of them pretty basic. 6 | 7 | ## Contents: 8 | 9 | ### `stdlib.s` 10 | 11 | Basic implementations of: 12 | 13 | - `malloc`/`free` (that leaks memory) 14 | - `rand`/`srand` (using xorshift) 15 | - `exit`/`atexit` (supporting up to 8 exit handlers) 16 | 17 | ### `string.s` 18 | 19 | Somewhat nice implementations of: 20 | 21 | - `strlen` 22 | - `strncpy` 23 | - `strcpy` 24 | - `memchr` 25 | - `memset` (very basic byte-by-byte copy) 26 | 27 | ## Correctness: 28 | 29 | This library is only lightly tested, so be careful and report bugs when you find them! 30 | -------------------------------------------------------------------------------- /riscemu/core/rtclock.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from typing import Optional 3 | 4 | from . import UInt32 5 | 6 | 7 | class RTClock: 8 | """ 9 | Represents a realtime clock. Can be set up to start at a certain time t0, and 10 | has a certain tickrate. 11 | """ 12 | 13 | tickrate: int 14 | t0: float 15 | 16 | def __init__(self, tickrate: int, t0: Optional[float] = None): 17 | if t0 is None: 18 | self.t0 = time() 19 | self.tickrate = tickrate 20 | 21 | def get_low32(self, *args) -> UInt32: 22 | return UInt32(int((time() - self.t0) * self.tickrate)) 23 | 24 | def get_hi32(self, *args) -> UInt32: 25 | return UInt32(int((time() - self.t0) * self.tickrate) >> 32) 26 | 27 | def get_as_int(self): 28 | return time() * self.tickrate 29 | -------------------------------------------------------------------------------- /riscemu/instructions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | This package holds all instruction sets, available to the processor 7 | """ 8 | 9 | from .instruction_set import InstructionSet, Instruction 10 | from .RV32M import RV32M 11 | from .RV32I import RV32I 12 | from .RV32A import RV32A 13 | from .RV32F import RV32F 14 | from .RV32D import RV32D 15 | from .RV_Debug import RV_Debug 16 | from .Zicsr import Zicsr 17 | 18 | InstructionSetDict = { 19 | v.__name__: v for v in [RV32I, RV32M, RV32A, RV32F, RV32D, Zicsr, RV_Debug] 20 | } 21 | 22 | __all__ = [ 23 | "Instruction", 24 | "InstructionSet", 25 | "InstructionSetDict", 26 | "RV32I", 27 | "RV32M", 28 | "RV32A", 29 | "RV32F", 30 | "RV32D", 31 | "Zicsr", 32 | "RV_Debug", 33 | ] 34 | -------------------------------------------------------------------------------- /riscemu/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021-2022 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | 7 | from dataclasses import dataclass 8 | 9 | 10 | @dataclass(frozen=True, init=True) 11 | class RunConfig: 12 | stack_size: int = 8 * 1024 * 64 # for 8KB stack 13 | include_scall_symbols: bool = True 14 | add_accept_imm: bool = False 15 | # debugging 16 | debug_instruction: bool = True 17 | debug_on_exception: bool = True 18 | # allowed syscalls 19 | scall_input: bool = True 20 | scall_fs: bool = False 21 | verbosity: int = 0 22 | slowdown: float = 1 23 | unlimited_registers: bool = False 24 | flen: int = 64 25 | # runtime config 26 | use_libc: bool = False 27 | ignore_exit_code: bool = False 28 | # csr stuff: 29 | # frequency of the real-time clock 30 | rtclock_tickrate: int = 32768 31 | -------------------------------------------------------------------------------- /docs/libraries.md: -------------------------------------------------------------------------------- 1 | # Included libraries 2 | 3 | I've started to implement some sort of standard library, following closely to [GNU's glibc](https://www.gnu.org/software/libc/). 4 | 5 | You can include the libraries by adding them as arguments (before your main assembly file): 6 | 7 | ``` 8 | > python3 -m riscemu examples/lib/libstring.asm example.asm 9 | [MMU] Successfully loaded: LoadedExecutable[examples/lib/libstring.asm](base=0x00000100, size=64bytes, sections=text, run_ptr=0x00000100) 10 | [MMU] Successfully loaded: LoadedExecutable[example.asm](base=0x00000140, size=168bytes, sections=data text, run_ptr=0x000001D0) 11 | [CPU] Allocated 524288 bytes of stack 12 | [CPU] Started running from 0x000001D0 (example.asm) 13 | ``` 14 | 15 | These libraries are no where near a stable state, so documentation will be scarce. Your best bet would be to `grep` for functionality. Sorry! 16 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | environment: publishing 11 | permissions: 12 | # IMPORTANT: this permission is mandatory for trusted publishing 13 | id-token: write 14 | steps: 15 | # clone repo and set up python 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.10" 21 | # install build package and build dist 22 | - name: Build distribution 23 | run: >- 24 | python3 -m pip install poetry && 25 | poetry build 26 | # retrieve your distributions here 27 | - name: Publish package distributions to PyPI 28 | uses: pypa/gh-action-pypi-publish@release/v1 29 | -------------------------------------------------------------------------------- /test/filecheck/rv32m-conv.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m riscemu -v %s -o libc | filecheck %s 2 | 3 | .text 4 | 5 | .globl main 6 | main: 7 | // test mulh 8 | li a1, -1 9 | mulh a0, a1, a1 10 | print a0 11 | // CHECK: register a0 contains value 0 12 | li a2, 2 13 | mulh a0, a1, a2 14 | print.uhex a0 15 | // CHECK-NEXT: register a0 contains value 0xffffffff 16 | 17 | // test mulhu 18 | mulhu a0, a1, a2 19 | print a0 20 | // CHECK: register a0 contains value 1 21 | 22 | mulhu a0, a1, a1 23 | print.uhex a0 24 | // CHECK-NEXT: register a0 contains value 0xfffffffe 25 | 26 | // test mulhsu 27 | mulhsu a0, a1, a2 28 | print.uhex a0 29 | // CHECK: register a0 contains value 0xffffffff 30 | 31 | mulhsu a0, a2, a1 32 | print a0 33 | // CHECK-NEXT: register a0 contains value 1 34 | 35 | li a0, 0 36 | ret 37 | // CHECK-NEXT: [CPU] Program exited with code 0 38 | -------------------------------------------------------------------------------- /.github/workflows/code-formatting.yml: -------------------------------------------------------------------------------- 1 | # This workflow check the format all files in the repository 2 | # * It checks that all nonempty files have a newline at the end 3 | # * It checks that there are no whitespaces at the end of lines 4 | # * It checks that Python files are formatted with black 5 | 6 | name: Code Formatting 7 | 8 | on: 9 | pull_request: 10 | push: 11 | branches: [master] 12 | 13 | jobs: 14 | code-formatting: 15 | 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: ['3.10'] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Upgrade pip 30 | run: | 31 | pip install --upgrade pip 32 | 33 | - name: Run code formatting checks with pre-commit 34 | uses: pre-commit/action@v3.0.0 35 | -------------------------------------------------------------------------------- /examples/snitch_simple.asm: -------------------------------------------------------------------------------- 1 | .data 2 | 3 | vec0: 4 | .word 0x0, 0x3f800000, 0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000, 0x40e00000, 0x41000000, 0x41100000 5 | vec1: 6 | .word 0x0, 0x3f800000, 0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000, 0x40e00000, 0x41000000, 0x41100000 7 | dest: 8 | .space 40 9 | 10 | .text 11 | .globl main 12 | 13 | main: 14 | // ssr config 15 | ssr.configure 0, 10, 4 16 | ssr.configure 1, 10, 4 17 | ssr.configure 2, 10, 4 18 | 19 | la a0, vec0 20 | ssr.read a0, 0, 0 21 | 22 | la a0, vec1 23 | ssr.read a0, 1, 0 24 | 25 | la a0, dest 26 | ssr.write a0, 2, 0 27 | 28 | ssr.enable 29 | 30 | // set up loop 31 | li a0, 10 32 | loop: 33 | fadd.s ft2, ft0, ft1 34 | 35 | addi a0, a0, -1 36 | bne a0, zero, loop 37 | 38 | // end of loop: 39 | ssr.disable 40 | 41 | ret 42 | -------------------------------------------------------------------------------- /sphinx-docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /riscemu/IO/IOModule.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Optional 3 | 4 | from riscemu.core import MemorySection, MemoryFlags, T_RelativeAddress 5 | 6 | 7 | class IOModule(MemorySection, ABC): 8 | def __init__( 9 | self, 10 | name: str, 11 | flags: MemoryFlags, 12 | size: int, 13 | owner: str = "system", 14 | base: int = 0, 15 | ): 16 | super(IOModule, self).__init__(name, flags, size, base, owner, None) 17 | 18 | def contains(self, addr, size: int = 0): 19 | return ( 20 | self.base <= addr < self.base + self.size 21 | and self.base <= addr + size <= self.base + self.size 22 | ) 23 | 24 | def dump(self, *args, **kwargs): 25 | print(self) 26 | 27 | def __repr__(self): 28 | return "{}[{}] at 0x{:0X} (size={}bytes, flags={})".format( 29 | self.__class__.__name__, self.name, self.base, self.size, self.flags 30 | ) 31 | -------------------------------------------------------------------------------- /test/test_regs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from riscemu.core.registers import Registers 4 | from riscemu.core import Float32 5 | 6 | 7 | def test_float_regs(): 8 | r = Registers() 9 | # uninitialized register is zero 10 | assert r.get_f("fs0") == 0 11 | # get/set 12 | val = Float32(3.14) 13 | r.set_f("fs0", val) 14 | assert r.get_f("fs0") == val 15 | 16 | 17 | def test_float_regs_flen64(): 18 | r = Registers(flen=64) 19 | # uninitialized register is zero 20 | assert r.get_f("fs0") == 0 21 | # get/set 22 | val = Float32(3.14) 23 | r.set_f("fs0", val) 24 | assert Float32.bitcast(r.get_f("fs0")) == val 25 | 26 | 27 | def test_unlimited_regs_works(): 28 | r = Registers(infinite_regs=True) 29 | r.get("infinite") 30 | r.get_f("finfinite") 31 | 32 | 33 | def test_unknown_reg_fails(): 34 | r = Registers(infinite_regs=False) 35 | with pytest.raises(RuntimeError, match="Invalid register: az1"): 36 | r.get("az1") 37 | -------------------------------------------------------------------------------- /examples/fibs.asm: -------------------------------------------------------------------------------- 1 | // Example program (c) by Anton Lydike 2 | // this calculates the fibonacci sequence and stores it in ram 3 | 4 | .data 5 | fibs: .space 56 6 | 7 | .text 8 | // make main global so it can be picked up by the crt0.s 9 | .globl main 10 | main: 11 | addi s1, zero, 0 // storage index 12 | addi s2, zero, 56 // last storage index 13 | addi t0, zero, 1 // t0 = F_{i} 14 | addi t1, zero, 1 // t1 = F_{i+1} 15 | loop: 16 | sw t0, fibs(s1) // save 17 | add t2, t1, t0 // t2 = F_{i+2} 18 | addi t0, t1, 0 // t0 = t1 19 | addi t1, t2, 0 // t1 = t2 20 | addi s1, s1, 4 // increment storage pointer 21 | blt s1, s2, loop // loop as long as we did not reach array length 22 | // exit gracefully 23 | addi a0, zero, 0 24 | addi a7, zero, 93 25 | scall // exit with code 0 26 | -------------------------------------------------------------------------------- /generate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # super hacky script to generate documentation when developing and for readthedocs 3 | 4 | echo "Generating docs!" 5 | 6 | 7 | if ! command -v 'sphinx-apidoc'; then 8 | source venv/bin/activate 9 | pip install -r sphinx-docs/requirements.txt 10 | fi 11 | 12 | 13 | 14 | if [[ $1 == 'generate' ]]; then 15 | 16 | # delete old help folder 17 | rm -rf help 18 | cp -r ../../docs help 19 | 20 | PYTHONPATH=../../ sphinx-apidoc -e -f -o . ../../riscemu ../../riscemu/colors.py ../../riscemu/__main__.py 21 | echo "only generating, not building..." 22 | rm ./modules.rst 23 | exit 0 24 | fi 25 | 26 | # delete old help folder 27 | rm -rf sphinx-docs/source/help 28 | cp -r docs sphinx-docs/source/help 29 | 30 | PYTHONPATH=. sphinx-apidoc -e -f -o sphinx-docs/source riscemu riscemu/colors.py riscemu/__main__.py 31 | 32 | rm sphinx-docs/source/modules.rst 33 | 34 | cd sphinx-docs 35 | 36 | make html 37 | 38 | # xdg-open build/html/index.html 39 | -------------------------------------------------------------------------------- /test/filecheck/fibs.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m riscemu -v -o ignore_exit_code,libc %s | filecheck %s 2 | .data 3 | fibs: .space 1024 4 | 5 | .text 6 | // make main global so it can be picked up by the crt0.s 7 | .globl main 8 | main: 9 | addi s1, zero, 0 // storage index 10 | addi s2, zero, 1024 // last storage index 11 | addi t0, zero, 1 // t0 = F_{i} 12 | addi t1, zero, 1 // t1 = F_{i+1} 13 | loop: 14 | sw t0, fibs(s1) // save 15 | add t2, t1, t0 // t2 = F_{i+2} 16 | addi t0, t1, 0 // t0 = t1 17 | addi t1, t2, 0 // t1 = t2 18 | addi s1, s1, 4 // increment storage pointer 19 | blt s1, s2, loop // loop as long as we did not reach array length 20 | ebreak 21 | // exit gracefully 22 | add a0, zero, t2 23 | addi a7, zero, 93 24 | scall // exit with code fibs(n) & 2^32 25 | 26 | // CHECK: [CPU] Program exited with code 1265227608 27 | -------------------------------------------------------------------------------- /test/filecheck/csr.asm: -------------------------------------------------------------------------------- 1 | .text 2 | 3 | .globl main 4 | main: 5 | // check that less that 1000 ticks passed since start 6 | csrrs a0, zero, time 7 | li a1, 1000 8 | bge a0, a1, fail 9 | print a0, "time passed since launch: {1} ticks" 10 | // CHECK: time passed since launch: 11 | // check that timeh is empty 12 | csrrs a0, zero, timeh 13 | bne a0, zero, fail 14 | // check that some ammount of cycles has passed so far: 15 | csrrs a0, zero, cycle 16 | beq a0, zero, fail 17 | print a0, "cycles passed: {1}" 18 | // CHECK-NEXT: cycles passed: 19 | // check that cycleh is zero 20 | csrrs a0, zero, cycleh 21 | bne a0, zero, fail 22 | // check instret and instreth 23 | csrrs a0, zero, instret 24 | beq a0, zero, fail 25 | print a0, "instret is: {1}" 26 | // CHECK-NEXT: instret is: 27 | csrrs a0, zero, instreth 28 | bne a0, zero, fail 29 | // CHECK-NEXT: Success! 30 | print a0, "Success!" 31 | ret 32 | 33 | fail: 34 | li a0, -1 35 | print a0, "Failure!" 36 | 37 | ret 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "riscemu" 3 | version = "2.2.7" 4 | description = "A basic RISC-V emulator" 5 | authors = ["Anton Lydike "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/antonlydike/riscemu" 9 | repository = "https://github.com/antonlydike/riscemu" 10 | keywords = ["RISC-V"] 11 | # classifiers 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | ] 17 | 18 | [tool.poetry.scripts] 19 | riscemu = "riscemu.__main__:main" 20 | 21 | [tool.poetry.urls] 22 | "Bug Tracker" = "https://github.com/antonlydike/riscemu/issues" 23 | 24 | [tool.poetry.dependencies] 25 | python = "^3.8" 26 | pyelftools = "^0.30" 27 | importlib-resources = "^6.1.0" 28 | 29 | [tool.poetry.group.dev.dependencies] 30 | black = "^23.7.0" 31 | pytest = "^7.4.0" 32 | filecheck = "^0.0.23" 33 | lit = "^16.0.6" 34 | pre-commit = "^3.3.3" 35 | psutil = "^5.9.5" 36 | 37 | [build-system] 38 | requires = ["poetry-core"] 39 | build-backend = "poetry.core.masonry.api" 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Anton Lydike 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /riscemu/interactive.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from riscemu.config import RunConfig 4 | from riscemu.core import InstructionMemorySection, SimpleInstruction, Program 5 | 6 | if __name__ == "__main__": 7 | from core.usermode_cpu import UserModeCPU 8 | from .instructions import InstructionSetDict 9 | 10 | cpu = UserModeCPU(list(InstructionSetDict.values()), RunConfig(verbosity=4)) 11 | 12 | program = Program("interactive session", base=0x100) 13 | context = program.context 14 | program.add_section( 15 | InstructionMemorySection( 16 | [ 17 | SimpleInstruction("ebreak", (), context, 0x100), 18 | SimpleInstruction("addi", ("a0", "zero", "0"), context, 0x104), 19 | SimpleInstruction("addi", ("a7", "zero", "93"), context, 0x108), 20 | SimpleInstruction("scall", (), context, 0x10C), 21 | ], 22 | ".text", 23 | context, 24 | program.name, 25 | 0x100, 26 | ) 27 | ) 28 | 29 | cpu.load_program(program) 30 | 31 | cpu.setup_stack() 32 | 33 | cpu.launch() 34 | 35 | sys.exit(cpu.exit_code) 36 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thank you for opening a PR to riscemu, your contribution is highly appreciated! 2 | 3 | We've put together two checklists below to make sure the review and merge process goes as smoothly as possible. Please make sure that you complete the writing checklist before opening the PR. Please feel free to delete it once you completed it. 4 | 5 | The pre-merge checklist is here so that you can see roughly what is expected of a PR to be merged. Not every step is always required, but checking all the boxes makes it almost certain that your PR will be merged swiftly. Feel free to leave it in the PR text, or delete it, it's up to you! 6 | 7 | Good luck with the PR! 8 | 9 | 10 | **PR Writing Checklist:** 11 | 12 | - [ ] If I'm referencing an Issue, I put the issue number in the PR name 13 | - [ ] I wrote a paragraph explaining my changes and the motivation 14 | - [ ] If this PR is not ready yet for review, I mark it as draft 15 | - [ ] Delete this checklist before opening the PR 16 | 17 | 18 | **Pre-Merge Checklist:** 19 | 20 | - [ ] I added an entry to the `CHANGELOG.md` 21 | - [ ] I added a test that covers my change 22 | - [ ] I ran the pre-commit formatting hooks 23 | - [ ] CI Passes 24 | -------------------------------------------------------------------------------- /.github/workflows/ci-pytest.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: CI - Python-based Testing 5 | 6 | on: 7 | # Trigger the workflow on push or pull request, 8 | # but only for the master branch 9 | push: 10 | branches: 11 | - master 12 | pull_request: 13 | branches: 14 | - master 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | python-version: ['3.8','3.10'] 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install poetry 33 | run: | 34 | pip install poetry 35 | 36 | - name: Install the package locally 37 | run: poetry install 38 | 39 | - name: Test with pytest 40 | run: | 41 | poetry run pytest -W error 42 | - name: Test with lit 43 | run: | 44 | poetry run lit --timeout=5 -v test/filecheck 45 | -------------------------------------------------------------------------------- /test/filecheck/rv32f-conv.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m riscemu -v %s -o libc | filecheck %s 2 | 3 | .text 4 | 5 | .globl main 6 | main: 7 | // test fcvt.s.wu 8 | li a1, -2 9 | fcvt.s.wu fa0, a1 10 | print.float.s fa0 11 | // CHECK: register fa0 contains value 4294967296.0 12 | li a1, 2 13 | fcvt.s.wu fa0, a1 14 | print.float fa0 15 | // CHECK-NEXT: register fa0 contains value 2.0 16 | 17 | // test fcvt.s.w 18 | li a1, -2 19 | fcvt.s.w fa0, a1 20 | print.float.s fa0 21 | // CHECK: register fa0 contains value -2.0 22 | li a1, 2 23 | fcvt.s.w fa0, a1 24 | print.float.s fa0 25 | // CHECK-NEXT: register fa0 contains value 2.0 26 | 27 | // test fmv.s.x 28 | li a1, 2 29 | fcvt.s.w fa0, a1 30 | fmv.x.w a1, fa0 31 | print a1 32 | // CHECK-NEXT: register a1 contains value 1073741824 33 | li a1, -2 34 | fcvt.s.w fa0, a1 35 | fmv.x.w a1, fa0 36 | print a1 37 | // CHECK-NEXT: register a1 contains value -1073741824 38 | 39 | // test fmv.w.x 40 | li a1, 1073741824 41 | fmv.w.x fa0, a1 42 | print.float.s fa0 43 | // CHECK-NEXT: register fa0 contains value 2.0 44 | li a1, -1073741824 45 | fmv.w.x fa0, a1 46 | print.float.s fa0 47 | // CHECK-NEXT: register fa0 contains value -2.0 48 | 49 | ret 50 | // CHECK-NEXT: [CPU] Program exited with code 0 51 | -------------------------------------------------------------------------------- /test/test_float_impl.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from riscemu.core import Float32, Float64 4 | 5 | # pi encoded as a 32bit little endian float 6 | PI_BYTES_LE = b"\xdb\x0fI@" 7 | 8 | 9 | def test_float_serialization(): 10 | assert Float32(PI_BYTES_LE) == Float32(math.pi) 11 | assert Float32(math.pi).bytes == PI_BYTES_LE 12 | 13 | 14 | def test_float_bitcast(): 15 | f32_pi = Float32(math.pi) 16 | f64_pi32 = Float64.bitcast(f32_pi) 17 | assert f32_pi.bytes == Float32.bitcast(f64_pi32).bytes 18 | 19 | f64_pi = Float64(math.pi) 20 | f32_pi64 = Float32.bitcast(f64_pi) 21 | assert f64_pi.bytes[-4:] == f32_pi64.bytes 22 | assert Float64.bitcast(f32_pi64).bytes[:4] == b"\x00\x00\x00\x00" 23 | 24 | 25 | def test_random_float_ops32(): 26 | val = Float32(5) 27 | assert val**2 == 25 28 | assert val // 2 == 2 29 | assert val * 3 == 15 30 | assert val - 2 == 3 31 | assert val * val == 25 32 | assert Float32(9) ** 0.5 == 3 33 | 34 | 35 | def test_random_float_ops64(): 36 | val = Float64(5) 37 | assert val**2 == 25 38 | assert val // 2 == 2 39 | assert val * 3 == 15 40 | assert val - 2 == 3 41 | assert val * val == 25 42 | assert Float64(9) ** 0.5 == 3 43 | 44 | 45 | def test_float_from_raw_bytes_conversion(): 46 | assert Float32.from_bytes(b"\x00\x00\xa0@") == Float32(5.0) 47 | -------------------------------------------------------------------------------- /examples/malloc.asm: -------------------------------------------------------------------------------- 1 | // example of a simple memory allocation 2 | // we use the mmap2 syscall for this 3 | 4 | .text 5 | // call mmap2 6 | li a0, 0 // addr = 0, let OS choose address 7 | li a1, 4096 // size 8 | li a2, 3 // PROT_READ | PROT_WRITE 9 | li a3, 5 // MAP_PRIVATE | MAP_ANONYMOUS 10 | li a7, SCALL_MMAP2 11 | ecall // invoke syscall 12 | 13 | li t0, -1 // exit if unsuccessful 14 | beq a0, t0, _exit 15 | 16 | // print address 17 | print.uhex a0 18 | # we can look at the state of the mmu here: 19 | ebreak 20 | # > mmu.sections 21 | # InstructionMemorySection[.text] at 0x00000100 22 | # BinaryDataMemorySection[.stack] at 0x00000170 23 | # BinaryDataMemorySection[.data.runtime-allocated] at 0x00080170 24 | 25 | sw t0, 144(a0) 26 | sw t0, 0(a0) 27 | sw t0, 8(a0) 28 | sw t0, 16(a0) 29 | sw t0, 32(a0) 30 | sw t0, 64(a0) 31 | sw t0, 128(a0) 32 | sw t0, 256(a0) 33 | sw t0, 512(a0) 34 | sw t0, 1024(a0) 35 | sw t0, 2048(a0) 36 | sw t0, 4000(a0) 37 | 38 | lw t1, 128(a0) 39 | print.uhex t0 40 | ebreak 41 | 42 | _exit: 43 | li a7, 93 44 | ecall 45 | -------------------------------------------------------------------------------- /docs/PythonDemo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Using RiscEmu from Python\n", 8 | "\n", 9 | "Here is how you can run some assembly through RiscEmu from Python.\n", 10 | "\n", 11 | "This example is using [this fibonacci assembly](examples/fibs.asm)." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from riscemu import RunConfig, UserModeCPU, RV32I, RV32M, AssemblyFileLoader\n", 21 | "\n", 22 | "cfg = RunConfig(debug_instruction=False, verbosity=50)\n", 23 | "cpu = UserModeCPU((RV32I, RV32M), cfg)\n", 24 | "\n", 25 | "loader = AssemblyFileLoader.instantiate('examples/fibs.asm', [])\n", 26 | "cpu.load_program(loader.parse())\n", 27 | "\n", 28 | "cpu.launch(cpu.mmu.programs[-1], True)" 29 | ] 30 | } 31 | ], 32 | "metadata": { 33 | "kernelspec": { 34 | "display_name": "Python 3 (ipykernel)", 35 | "language": "python", 36 | "name": "python3" 37 | }, 38 | "language_info": { 39 | "codemirror_mode": { 40 | "name": "ipython", 41 | "version": 3 42 | }, 43 | "file_extension": ".py", 44 | "mimetype": "text/x-python", 45 | "name": "python", 46 | "nbconvert_exporter": "python", 47 | "pygments_lexer": "ipython3", 48 | "version": "3.10.8" 49 | } 50 | }, 51 | "nbformat": 4, 52 | "nbformat_minor": 4 53 | } 54 | -------------------------------------------------------------------------------- /riscemu/decoder/formatter.py: -------------------------------------------------------------------------------- 1 | from .formats import ( 2 | INSTRUCTION_ARGS_DECODER, 3 | op, 4 | decode_i, 5 | decode_r, 6 | decode_u, 7 | decode_b, 8 | decode_j, 9 | decode_s, 10 | decode_i_shamt, 11 | decode_i_unsigned, 12 | ) 13 | from .regs import RISCV_REGS 14 | 15 | 16 | def int_to_hex(num: int): 17 | if num < 0: 18 | return f"-0x{-num:x}" 19 | return f"0x{num:x}" 20 | 21 | 22 | def format_ins(ins: int, name: str, fmt: str = "int"): 23 | opcode = op(ins) 24 | if fmt == "hex": 25 | fmt = int_to_hex 26 | else: 27 | fmt = str 28 | 29 | if opcode not in INSTRUCTION_ARGS_DECODER: 30 | return f"{name} " 31 | 32 | decoder = INSTRUCTION_ARGS_DECODER[opcode] 33 | if name in ("ecall", "ebreak", "mret", "sret", "uret"): 34 | return name 35 | if opcode in (0x8, 0x0): 36 | r1, r2, imm = decoder(ins) 37 | return f"{name:<7} {RISCV_REGS[r1]}, {imm}({RISCV_REGS[r2]})" 38 | elif decoder in (decode_i, decode_i_unsigned, decode_b, decode_i_shamt, decode_s): 39 | r1, r2, imm = decoder(ins) 40 | r1, r2 = RISCV_REGS[r1], RISCV_REGS[r2] 41 | return f"{name:<7} {r1}, {r2}, {fmt(imm)}" 42 | elif decoder in (decode_r,): 43 | rd, rs1, rs2 = [RISCV_REGS[x] for x in decoder(ins)] 44 | return f"{name:<7} {rd}, {rs1}, {rs2}" 45 | elif decoder in (decode_j, decode_u): 46 | r1, imm = decoder(ins) 47 | return f"{name:<7} {RISCV_REGS[r1]}, {fmt(imm)}" 48 | 49 | return f"{name} " 50 | -------------------------------------------------------------------------------- /snitch/xssr.py: -------------------------------------------------------------------------------- 1 | from riscemu.instructions.instruction_set import InstructionSet, Instruction 2 | 3 | from .regs import StreamingRegs, StreamDef, StreamMode 4 | 5 | 6 | class Xssr_pseudo(InstructionSet): 7 | def instruction_ssr_enable(self, ins: Instruction): 8 | self._stream.enabled = True 9 | 10 | def instruction_ssr_disable(self, ins: Instruction): 11 | self._stream.enabled = False 12 | 13 | def instruction_ssr_configure(self, ins: Instruction): 14 | dm = ins.get_imm(0).abs_value.value 15 | bound = ins.get_imm(1).abs_value.value 16 | stride = ins.get_imm(2).abs_value.value 17 | 18 | self._stream.dm_by_id[dm].bound = bound 19 | self._stream.dm_by_id[dm].stride = stride 20 | 21 | def instruction_ssr_read(self, ins: Instruction): 22 | base_pointer = ins.get_reg(0) 23 | dm = ins.get_imm(1).abs_value.value 24 | dim = ins.get_imm(2).abs_value.value 25 | 26 | self._stream.dm_by_id[dm].base = self.regs.get(base_pointer).value 27 | self._stream.dm_by_id[dm].dim = dim 28 | self._stream.dm_by_id[dm].mode = StreamMode.READ 29 | 30 | def instruction_ssr_write(self, ins: Instruction): 31 | base_pointer = ins.get_reg(0) 32 | dm = ins.get_imm(1).abs_value.value 33 | dim = ins.get_imm(2).abs_value.value 34 | 35 | self._stream.dm_by_id[dm].base = self.regs.get(base_pointer).value 36 | self._stream.dm_by_id[dm].dim = dim 37 | self._stream.dm_by_id[dm].mode = StreamMode.WRITE 38 | 39 | @property 40 | def _stream(self) -> StreamingRegs: 41 | return self.cpu.regs # type: ignore 42 | -------------------------------------------------------------------------------- /docs/internal-structure.md: -------------------------------------------------------------------------------- 1 | # Internal Structure 2 | 3 | ## Loading assembly files: 4 | In order to load an assembly file, you need to instantiate a CPU with the capabilities you want. Loading an assembly 5 | file is the done in multiple steps: 6 | 7 | 8 | * An `RiscVInput` is created, this represents the file internally 9 | * An `RiscVTokenizer` is created by calling `cpu.get_tokenizer()`. 10 | * The input is tokenized by calling `.tokenize()` on the tokenizer. 11 | * The tokens can then be converted to an Executable, this will then 12 | hold all the information such as name, sections, symbols, etc. 13 | This is done by creating an `ExecutableParser(tk: RiscVTokenizer)` 14 | and the calling `parse()`. 15 | * Now you have a representation of the assembly file that can be loaded 16 | into memory by calling `cpu.load(executable)`, this will internally 17 | construct a `LoadedExecutable`, which represents the actual memory 18 | regions the executable contains (and some meta information such as 19 | symbols). 20 | * You can load as many executables as you want into memory. If you want 21 | to run one, you pass it to `run_loaded(loaded_bin)` method of the cpu. 22 | 23 | You shouldn't have to do this manually, as the `riscemu/__main__.py` has all the necessary code. 24 | 25 | ## Instruction sets 26 | Each instruction set is in a separate file in `riscemu/instructions/`. All instruction sets have to inherit from the 27 | `InstructionSet` class that sets up all the relevant helpers and loading code. 28 | 29 | Creating a cpu with certain instruction sets is done by passing the CPU constructor a list of instruction set classes: 30 | ``` 31 | cpu = CPU(config, [RV32I, RV32M]) 32 | ``` 33 | -------------------------------------------------------------------------------- /riscemu/instructions/RV32M.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | 7 | from .instruction_set import * 8 | 9 | 10 | class RV32M(InstructionSet): 11 | """ 12 | The RV32M Instruction set, containing multiplication and division instructions 13 | """ 14 | 15 | def instruction_mul(self, ins: "Instruction"): 16 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins) 17 | self.regs.set(rd, rs1 * rs2) 18 | 19 | def instruction_mulh(self, ins: "Instruction"): 20 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins) 21 | self.regs.set(rd, Int32((rs1.signed().value * rs2.signed().value) >> 32)) 22 | 23 | def instruction_mulhsu(self, ins: "Instruction"): 24 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins) 25 | self.regs.set(rd, Int32((rs1.signed().value * rs2.unsigned_value) >> 32)) 26 | 27 | def instruction_mulhu(self, ins: "Instruction"): 28 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins) 29 | self.regs.set(rd, UInt32((rs1.unsigned_value * rs2.unsigned_value) >> 32)) 30 | 31 | def instruction_div(self, ins: "Instruction"): 32 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins) 33 | self.regs.set(rd, rs1 // rs2) 34 | 35 | def instruction_divu(self, ins: "Instruction"): 36 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins, signed=False) 37 | self.regs.set(rd, rs1 // rs2) 38 | 39 | def instruction_rem(self, ins: "Instruction"): 40 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins) 41 | self.regs.set(rd, rs1 % rs2) 42 | 43 | def instruction_remu(self, ins: "Instruction"): 44 | rd, rs1, rs2 = self.parse_rd_rs_rs(ins, signed=False) 45 | self.regs.set(rd, rs1 % rs2) 46 | -------------------------------------------------------------------------------- /test/test_integers.py: -------------------------------------------------------------------------------- 1 | from riscemu.core import Int32, UInt32 2 | import pytest 3 | 4 | 5 | def test_logical_right_shift(): 6 | a = Int32(100) 7 | assert a.shift_right_logical(0) == a 8 | assert a.shift_right_logical(10) == 0 9 | assert a.shift_right_logical(1) == 100 >> 1 10 | 11 | a = Int32(-100) 12 | assert a.shift_right_logical(0) == a 13 | assert a.shift_right_logical(1) == 2147483598 14 | assert a.shift_right_logical(10) == 4194303 15 | assert a.shift_right_logical(31) == 1 16 | assert a.shift_right_logical(32) == 0 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "val,expected_int", 21 | ( 22 | ( 23 | (0.0, 0), 24 | (-1.0, -1), 25 | (3.14159, 3), 26 | (float("NaN"), Int32.MIN_VALUE), 27 | (float("-inf"), Int32.MIN_VALUE), 28 | (float("inf"), Int32.MAX_VALUE), 29 | (1.0e100, Int32.MAX_VALUE), 30 | (-1.0e100, Int32.MIN_VALUE), 31 | ) 32 | ), 33 | ) 34 | def test_float_to_int_conversion(val: float, expected_int: int): 35 | assert Int32.from_float(val) == expected_int 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "val,expected_int", 40 | ( 41 | ( 42 | (0.0, 0), 43 | (3.14159, 3), 44 | (float("NaN"), UInt32.MIN_VALUE), 45 | (float("-inf"), UInt32.MIN_VALUE), 46 | (float("inf"), UInt32.MAX_VALUE), 47 | (1.0e100, UInt32.MAX_VALUE), 48 | (-1.0e100, UInt32.MIN_VALUE), 49 | (-1.0, UInt32.MIN_VALUE), 50 | ) 51 | ), 52 | ) 53 | def test_float_to_uint_conversion(val: float, expected_int: int): 54 | assert UInt32.from_float(val) == expected_int 55 | -------------------------------------------------------------------------------- /docs/assembly.md: -------------------------------------------------------------------------------- 1 | # Assembly 2 | 3 | Assembly tokenization should be working completely. It knows what instructions the CPU implementation supports and parses based on them. 4 | 5 | 6 | ## Instruction sets: 7 | * RV32I 8 | * Loads/Stores: `lb, lh, lw, lbu, lhu, sw, sh, sb` (supported arg format is either `rd, imm(reg)` or `rd, reg, imm`) 9 | * Branch statements: `beq, bne, blt, bge, bltu, bgeu` 10 | * Jumps `j, jal, jalr, ret` 11 | * Basic arithmetic: `add, addi, sub, lui, auipc` 12 | * Shifts: `sll, slli, srl, srli, sra, srai` 13 | * Syscall/Debugging:`scall, ecall, sbreak, ebreak` (both `s` and `e` version are the same instruction) 14 | * Compares: `slt, sltu, slti, sltiu` 15 | * Logical: `and, or, xor, andi, ori, xori` 16 | * Not implemented: `fence, fence.i, rdcycle, rdcycleh, rdtime, rdtimeh, rdinstret, rdinstreth` 17 | * RV32M 18 | * Multiplication: `mul, mulh`, not implemented yet are `mulhsu, mulhu` 19 | * Division: `div, divu, rem, remu` 20 | 21 | 22 | 23 | ## Pseudo-ops 24 | The following pseudo-ops are implemented as of yet: 25 | * `.space ` reverse bytes of zero 26 | * `.ascii 'text'` put text into memory 27 | * `.asciiz 'text'` put text into memory (null terminated) 28 | * `.section .` same as `.`, see sections 29 | * `.set , ` to create a const symbol with a given value 30 | * `.global ` mark symbol `` as a global symbol. It is available from all loaded programs 31 | * `.align ` currently a nop as cpu does not care about alignment as of now 32 | 33 | ## Sections: 34 | Currently only these three sections are supported: 35 | * `data` read-write data (non-executable) 36 | * `rodata` read-only data (non-executable) 37 | * `text` executable data (read-only) 38 | -------------------------------------------------------------------------------- /riscemu/IO/TextIO.py: -------------------------------------------------------------------------------- 1 | from .IOModule import IOModule 2 | from core.traps import InstructionAccessFault 3 | from ..core import T_RelativeAddress, Instruction, MemoryFlags, Int32 4 | 5 | 6 | class TextIO(IOModule): 7 | def read_ins(self, offset: T_RelativeAddress) -> Instruction: 8 | raise InstructionAccessFault(self.base + offset) 9 | 10 | def __init__(self, base: int, buflen: int = 128): 11 | super(TextIO, self).__init__( 12 | "TextIO", MemoryFlags(False, False), buflen + 4, base=base 13 | ) 14 | self.buff = bytearray(buflen) 15 | self.current_line = "" 16 | 17 | def read(self, addr: int, size: int) -> bytearray: 18 | raise InstructionAccessFault(self.base + addr) 19 | 20 | def write(self, addr: int, size: int, data: bytearray): 21 | if addr == 0: 22 | if size > 4: 23 | raise InstructionAccessFault(addr) 24 | if Int32(data) != 0: 25 | self._print() 26 | return 27 | buff_start = addr - 4 28 | self.buff[buff_start : buff_start + size] = data[0:size] 29 | 30 | def _print(self): 31 | buff = self.buff 32 | self.buff = bytearray(self.size) 33 | if b"\x00" in buff: 34 | buff = buff.split(b"\x00")[0] 35 | text = buff.decode("ascii") 36 | if "\n" in text: 37 | lines = text.split("\n") 38 | lines[0] = self.current_line + lines[0] 39 | for line in lines[:-1]: 40 | self._present(line) 41 | self.current_line = lines[-1] 42 | else: 43 | self.current_line += text 44 | 45 | def _present(self, text: str): 46 | print("[TextIO:{:x}] {}".format(self.base, text)) 47 | -------------------------------------------------------------------------------- /riscemu/core/instruction_memory_section.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from . import ( 4 | MemorySection, 5 | Instruction, 6 | InstructionContext, 7 | MemoryFlags, 8 | T_RelativeAddress, 9 | ) 10 | from .exceptions import MemoryAccessException 11 | 12 | 13 | class InstructionMemorySection(MemorySection): 14 | def __init__( 15 | self, 16 | instructions: List[Instruction], 17 | name: str, 18 | context: InstructionContext, 19 | owner: str, 20 | base: int = 0, 21 | ): 22 | self.name = name 23 | self.base = base 24 | self.context = context 25 | self.size = len(instructions) * 4 26 | self.flags = MemoryFlags(True, True) 27 | self.instructions = instructions 28 | self.owner = owner 29 | 30 | def read(self, offset: T_RelativeAddress, size: int) -> bytearray: 31 | raise MemoryAccessException( 32 | "Cannot read raw bytes from instruction section", 33 | self.base + offset, 34 | size, 35 | "read", 36 | ) 37 | 38 | def write(self, offset: T_RelativeAddress, size: int, data: bytearray): 39 | raise MemoryAccessException( 40 | "Cannot write raw bytes to instruction section", 41 | self.base + offset, 42 | size, 43 | "write", 44 | ) 45 | 46 | def read_ins(self, offset: T_RelativeAddress) -> Instruction: 47 | if offset % 4 != 0: 48 | raise MemoryAccessException( 49 | "Unaligned instruction fetch!", 50 | self.base + offset, 51 | 4, 52 | "instruction fetch", 53 | ) 54 | return self.instructions[offset // 4] 55 | -------------------------------------------------------------------------------- /test/test_instruction.py: -------------------------------------------------------------------------------- 1 | from riscemu.core import InstructionContext, SimpleInstruction, NumberFormatException 2 | import pytest 3 | 4 | 5 | def test_int_and_hex_immediates(): 6 | ctx = InstructionContext() 7 | 8 | ins = SimpleInstruction("addi", ("a0", "a1", "100"), ctx, 0x100) 9 | ins_hex = SimpleInstruction("addi", ("a0", "a1", "0x10"), ctx, 0x100) 10 | 11 | assert ins.get_reg(0) == "a0" 12 | assert ins.get_reg(1) == "a1" 13 | assert ins.get_imm(2).abs_value == 100 14 | assert ins.get_imm(2).pcrel_value == 100 - 0x100 15 | 16 | assert ins_hex.get_imm(2).abs_value == 0x10 17 | assert ins_hex.get_imm(2).pcrel_value == 0x10 - 0x100 18 | 19 | 20 | def test_label_immediates(): 21 | ctx = InstructionContext() 22 | ctx.labels["test"] = 100 23 | 24 | ins = SimpleInstruction("addi", ("a0", "a1", "test"), ctx, 0x100) 25 | 26 | assert ins.get_reg(0) == "a0" 27 | assert ins.get_reg(1) == "a1" 28 | assert ins.get_imm(2).abs_value == 100 29 | assert ins.get_imm(2).pcrel_value == 100 - 0x100 30 | 31 | 32 | def test_numerical_labels(): 33 | ctx = InstructionContext() 34 | ctx.numbered_labels["1"] = [0x100 - 4, 0x100 + 16] 35 | 36 | ins = SimpleInstruction("addi", ("a0", "a1", "1b"), ctx, 0x100) 37 | 38 | assert ins.get_reg(0) == "a0" 39 | assert ins.get_reg(1) == "a1" 40 | assert ins.get_imm(2).abs_value == 0x100 - 4 41 | assert ins.get_imm(2).pcrel_value == -4 42 | 43 | 44 | def test_invalid_immediate_val(): 45 | ctx = InstructionContext() 46 | ctx.labels["test"] = 100 47 | 48 | ins = SimpleInstruction("addi", ("a0", "a1", "test2"), ctx, 0x100) 49 | 50 | with pytest.raises( 51 | NumberFormatException, match="test2 is neither a number now a known symbol" 52 | ): 53 | ins.get_imm(2) 54 | -------------------------------------------------------------------------------- /riscemu/priv/PrivMMU.py: -------------------------------------------------------------------------------- 1 | from .types import ElfMemorySection 2 | from core.mmu import * 3 | 4 | import typing 5 | 6 | if typing.TYPE_CHECKING: 7 | pass 8 | 9 | 10 | class PrivMMU(MMU): 11 | def get_sec_containing(self, addr: T_AbsoluteAddress) -> MemorySection: 12 | # try to get an existing section 13 | existing_sec = super().get_sec_containing(addr) 14 | 15 | if existing_sec is not None: 16 | return existing_sec 17 | 18 | # get section preceding empty space at addr 19 | sec_before = next( 20 | (sec for sec in reversed(self.sections) if sec.end < addr), None 21 | ) 22 | # get sec succeeding empty space at addr 23 | sec_after = next((sec for sec in self.sections if sec.base > addr), None) 24 | 25 | # calc start and end of "free" space 26 | prev_sec_end = 0 if sec_before is None else sec_before.end 27 | next_sec_start = 0x7FFFFFFF if sec_after is None else sec_after.base 28 | 29 | # start at the end of the prev section, or current address - 0xFFFF (aligned to 16 byte boundary) 30 | start = max(prev_sec_end, align_addr(addr - 0xFFFF, 16)) 31 | # end at the start of the next section, or address + 0xFFFF (aligned to 16 byte boundary) 32 | end = min(next_sec_start, align_addr(addr + 0xFFFF, 16)) 33 | 34 | sec = ElfMemorySection( 35 | bytearray(end - start), 36 | ".empty", 37 | self.global_instruction_context(), 38 | "", 39 | start, 40 | MemoryFlags(False, True), 41 | ) 42 | self.sections.append(sec) 43 | self._update_state() 44 | 45 | return sec 46 | 47 | def global_instruction_context(self) -> InstructionContext: 48 | context = InstructionContext() 49 | context.global_symbol_dict = self.global_symbols 50 | return context 51 | -------------------------------------------------------------------------------- /riscemu/core/simple_instruction.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Union, Tuple 3 | from functools import lru_cache 4 | 5 | from . import ( 6 | Instruction, 7 | T_RelativeAddress, 8 | InstructionContext, 9 | Immediate, 10 | NumberFormatException, 11 | ) 12 | from ..helpers import parse_numeric_argument 13 | 14 | _NUM_LABEL_RE = re.compile(r"[0-9][fb]") 15 | _INT_IMM_RE = re.compile(r"[+-]?([0-9]+|0x[A-Fa-f0-9]+)") 16 | 17 | 18 | class SimpleInstruction(Instruction): 19 | def __init__( 20 | self, 21 | name: str, 22 | args: Union[Tuple[()], Tuple[str], Tuple[str, str], Tuple[str, str, str]], 23 | context: InstructionContext, 24 | addr: T_RelativeAddress, 25 | ): 26 | self.context = context 27 | self.name = name 28 | self.args = args 29 | self._addr = addr 30 | 31 | @property 32 | def addr(self) -> int: 33 | return self._addr + self.context.base_address 34 | 35 | @lru_cache(maxsize=None) 36 | def get_imm(self, num: int) -> Immediate: 37 | token = self.args[num] 38 | 39 | if _INT_IMM_RE.fullmatch(token): 40 | value = parse_numeric_argument(token) 41 | return Immediate(abs_value=value, pcrel_value=value - self.addr) 42 | 43 | # resolve label correctly 44 | if _NUM_LABEL_RE.fullmatch(token): 45 | value = self.context.resolve_numerical_label(token, self.addr) 46 | else: 47 | value = self.context.resolve_label(token) 48 | 49 | # TODO: make it raise a nice error instead 50 | if value is None: 51 | raise NumberFormatException( 52 | "{} is neither a number now a known symbol".format(token) 53 | ) 54 | return Immediate(abs_value=value, pcrel_value=value - self.addr) 55 | 56 | def get_reg(self, num: int) -> str: 57 | return self.args[num] 58 | -------------------------------------------------------------------------------- /sphinx-docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. RiscEmu documentation master file, created by 2 | sphinx-quickstart on Thu Apr 22 14:39:35 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | RiscEmu Documentation! 7 | ====================== 8 | 9 | Check this project out on github_. 10 | 11 | .. _github: https://github.com/antonlydike/riscemu 12 | 13 | Help: 14 | ===== 15 | 16 | .. toctree:: 17 | :maxdepth: 1 18 | :glob: 19 | 20 | help/* 21 | 22 | 23 | Package structure: 24 | ================== 25 | 26 | .. toctree:: 27 | :maxdepth: 5 28 | 29 | riscemu 30 | 31 | 32 | Other links: 33 | ============ 34 | 35 | * :ref:`genindex` 36 | * :ref:`modindex` 37 | * :ref:`search` 38 | 39 | 40 | License: 41 | ======== 42 | MIT License 43 | 44 | Copyright (c) 2021 Anton Lydike 45 | 46 | Permission is hereby granted, free of charge, to any person obtaining a copy 47 | of this software and associated documentation files (the "Software"), to deal 48 | in the Software without restriction, including without limitation the rights 49 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 50 | copies of the Software, and to permit persons to whom the Software is 51 | furnished to do so, subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all 54 | copies or substantial portions of the Software. 55 | 56 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 57 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 58 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 59 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 60 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 61 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 62 | SOFTWARE. 63 | -------------------------------------------------------------------------------- /riscemu/core/csr_constants.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Tuple 2 | 3 | MCAUSE_TRANSLATION: Dict[Tuple[int, int], str] = { 4 | (1, 0): "User software interrupt", 5 | (1, 1): "Supervisor software interrupt", 6 | (1, 3): "Machine software interrupt", 7 | (1, 4): "User timer interrupt", 8 | (1, 5): "Supervisor timer interrupt", 9 | (1, 7): "Machine timer interrupt", 10 | (1, 8): "User external interrupt", 11 | (1, 9): "Supervisor external interrupt", 12 | (1, 11): "Machine external interrupt", 13 | (0, 0): "Instruction address misaligned", 14 | (0, 1): "Instruction access fault", 15 | (0, 2): "Illegal instruction", 16 | (0, 3): "Breakpoint", 17 | (0, 4): "Load address misaligned", 18 | (0, 5): "Load access fault", 19 | (0, 6): "Store/AMO address misaligned", 20 | (0, 7): "Store/AMO access fault", 21 | (0, 8): "environment call from user mode", 22 | (0, 9): "environment call from supervisor mode", 23 | (0, 11): "environment call from machine mode", 24 | (0, 12): "Instruction page fault", 25 | (0, 13): "Load page fault", 26 | (0, 15): "Store/AMO page fault", 27 | } 28 | """ 29 | Assigns tuple (interrupt bit, exception code) to their respective readable names 30 | """ 31 | 32 | CSR_NAME_TO_ADDR: Dict[str, int] = { 33 | "fflags": 0x001, 34 | "frm": 0x002, 35 | "fcsr": 0x003, 36 | "mstatus": 0x300, 37 | "misa": 0x301, 38 | "mie": 0x304, 39 | "mtvec": 0x305, 40 | "mepc": 0x341, 41 | "mcause": 0x342, 42 | "mtval": 0x343, 43 | "mip": 0x344, 44 | "mtimecmp": 0x780, 45 | "mtimecmph": 0x781, 46 | "halt": 0x789, 47 | "cycle": 0xC00, 48 | "time": 0xC01, 49 | "instret": 0xC02, 50 | "cycleh": 0xC80, 51 | "timeh": 0xC81, 52 | "instreth": 0xC82, 53 | "mvendorid": 0xF11, 54 | "marchid": 0xF12, 55 | "mimpid": 0xF13, 56 | "mhartid": 0xF14, 57 | } 58 | """ 59 | Translation for named registers 60 | """ 61 | -------------------------------------------------------------------------------- /test/filecheck/snitch/ssr_only.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m snitch %s -o libc -v --flen 32 | filecheck %s 2 | 3 | .data 4 | 5 | vec0: 6 | .word 0x0, 0x3f800000, 0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000, 0x40e00000, 0x41000000, 0x41100000 7 | vec1: 8 | .word 0x0, 0x3f800000, 0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000, 0x40e00000, 0x41000000, 0x41100000 9 | dest: 10 | .space 40 11 | 12 | .text 13 | .globl main 14 | 15 | main: 16 | // ssr config 17 | ssr.configure 0, 10, 4 18 | ssr.configure 1, 10, 4 19 | ssr.configure 2, 10, 4 20 | 21 | la a0, vec0 22 | ssr.read a0, 0, 0 23 | 24 | la a0, vec1 25 | ssr.read a0, 1, 0 26 | 27 | la a0, dest 28 | ssr.write a0, 2, 0 29 | 30 | ssr.enable 31 | 32 | // set up loop 33 | li a0, 10 34 | loop: 35 | fadd.s ft2, ft0, ft1 36 | 37 | addi a0, a0, -1 38 | bne a0, zero, loop 39 | 40 | // end of loop: 41 | ssr.disable 42 | 43 | // check values were written correctly: 44 | la t0, vec0 45 | la t1, vec1 46 | la t2, dest 47 | li a0, 36 48 | loop2: 49 | add s0, t0, a0 50 | add s1, t1, a0 51 | add s2, t2, a0 52 | 53 | // load vec0, vec1 and dest elements 54 | flw ft0, 0(s0) 55 | flw ft1, 0(s1) 56 | flw ft2, 0(s2) 57 | 58 | // assert ft2 == ft1 + ft2 59 | fadd.s ft3, ft1, ft0 60 | feq.s s0, ft2, ft3 61 | beq zero, s0, fail 62 | 63 | addi a0, a0, -4 64 | bne a0, zero, loop2 65 | 66 | ret 67 | 68 | fail: 69 | printf "failed {} + {} != {} (at {})", ft0, ft1, ft2, a0 70 | li a0, -1 71 | ret 72 | 73 | // CHECK: [CPU] Program exited with code 0 74 | -------------------------------------------------------------------------------- /riscemu/core/binary_data_memory_section.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from . import ( 3 | MemorySection, 4 | InstructionContext, 5 | MemoryFlags, 6 | T_RelativeAddress, 7 | Instruction, 8 | ) 9 | from ..core.exceptions import MemoryAccessException 10 | 11 | 12 | class BinaryDataMemorySection(MemorySection): 13 | def __init__( 14 | self, 15 | data: bytearray, 16 | name: str, 17 | context: InstructionContext, 18 | owner: str, 19 | base: int = 0, 20 | flags: Optional[MemoryFlags] = None, 21 | ): 22 | super().__init__( 23 | name, 24 | flags if flags is not None else MemoryFlags(False, False), 25 | len(data), 26 | base, 27 | owner, 28 | context, 29 | ) 30 | self.data = data 31 | 32 | def read(self, offset: T_RelativeAddress, size: int) -> bytearray: 33 | if offset + size > self.size: 34 | raise MemoryAccessException( 35 | "Out of bounds access in {}".format(self), offset, size, "read" 36 | ) 37 | return self.data[offset : offset + size] 38 | 39 | def write(self, offset: T_RelativeAddress, size: int, data: bytearray): 40 | if offset + size > self.size: 41 | raise MemoryAccessException( 42 | "Out of bounds access in {}".format(self), offset, size, "write" 43 | ) 44 | if len(data[0:size]) != size: 45 | raise MemoryAccessException( 46 | "Invalid write parameter sizing", offset, size, "write" 47 | ) 48 | self.data[offset : offset + size] = data[0:size] 49 | 50 | def read_ins(self, offset: T_RelativeAddress) -> Instruction: 51 | raise MemoryAccessException( 52 | "Tried reading instruction on non-executable section {}".format(self), 53 | offset, 54 | 4, 55 | "instruction fetch", 56 | ) 57 | -------------------------------------------------------------------------------- /riscemu/priv/__main__.py: -------------------------------------------------------------------------------- 1 | from riscemu import RunConfig 2 | from riscemu.core import Program 3 | from .PrivCPU import PrivCPU 4 | 5 | import sys 6 | 7 | if __name__ == "__main__": 8 | import argparse 9 | 10 | parser = argparse.ArgumentParser( 11 | description="RISC-V privileged architecture emulator", prog="riscemu" 12 | ) 13 | 14 | parser.add_argument( 15 | "source", 16 | type=str, 17 | help="Compiled RISC-V ELF file or memory image containing compiled RISC-V ELF files", 18 | nargs="+", 19 | ) 20 | parser.add_argument( 21 | "--debug-exceptions", 22 | help="Launch the interactive debugger when an exception is generated", 23 | action="store_true", 24 | ) 25 | 26 | parser.add_argument( 27 | "-v", 28 | "--verbose", 29 | help="Verbosity level (can be used multiple times)", 30 | action="count", 31 | default=0, 32 | ) 33 | 34 | parser.add_argument( 35 | "--slowdown", 36 | help="Slow down the emulated CPU clock by a factor", 37 | type=float, 38 | default=1, 39 | ) 40 | 41 | args = parser.parse_args() 42 | 43 | cpu = PrivCPU( 44 | RunConfig( 45 | verbosity=args.verbose, 46 | debug_on_exception=args.debug_exceptions, 47 | slowdown=args.slowdown, 48 | ) 49 | ) 50 | 51 | for source_path in args.source: 52 | loader = max( 53 | (loader for loader in cpu.get_loaders()), 54 | key=lambda l: l.can_parse(source_path), 55 | ) 56 | argv, opts = loader.get_options(sys.argv) 57 | program = loader.instantiate(source_path, opts).parse() 58 | if isinstance(program, Program): 59 | cpu.load_program(program) 60 | else: 61 | program_iter = program 62 | for program in program_iter: 63 | cpu.load_program(program) 64 | 65 | cpu.launch(verbose=args.verbose > 4) 66 | -------------------------------------------------------------------------------- /docs/syscalls.md: -------------------------------------------------------------------------------- 1 | # Syscalls 2 | 3 | Performing a syscall is quite simple: 4 | ```risc-v asm 5 | ; set syscall code: 6 | addi a7, zero, 93 ; or SCALL_EXIT if syscall symbols are mapped 7 | ; set syscall args: 8 | addi a0, zero, 1 ; exit with code 1 9 | ; invode syscall handler 10 | scall 11 | ``` 12 | 13 | The global symbols (e.g. `SCALL_READ`) are loaded by default. If you specify the option `no_syscall_symbols`, they will be omitted. 14 | 15 | 16 | ## Read (63) `SCALL_READ` 17 | * `a0`: source file descriptor 18 | * `a1`: addr at which to write the input 19 | * `a2`: number of bytes to read (at most) 20 | * `return in a0`: number of bytes read or -1 21 | 22 | ## Write (64) `SCALL_WRITE` 23 | * `a0`: target file descriptor 24 | * `a1`: addr at which the data to be written is located 25 | * `a2`: number of bytes to write 26 | * `return in a0`: number of bytes written or -1 27 | 28 | ## Exit (93) `SCALL_EXIT` 29 | * `a0`: exit code 30 | 31 | ## Open (1024) `SCALL_OPEN` 32 | * `a0`: open mode: 33 | - `0`: read 34 | - `1`: write (truncate) 35 | - `2`: read/write (no truncate) 36 | - `3`: only create 37 | - `4`: append 38 | * `a1`: addr where path is stored 39 | * `a2`: length of path 40 | * `return in a0`: file descriptor of opened file or -1 41 | 42 | Requires flag `--scall-fs` to be set to True 43 | 44 | ## Close (1025) `SCALL_CLOSE` 45 | * `a0`: file descriptor to close 46 | * `return in a0`: 0 if closed correctly or -1 47 | 48 | # Extending these syscalls 49 | 50 | You can implement your own syscall by adding its code to the `SYSCALLS` dict in the [riscemu/syscalls.py](../riscemu/syscall.py) file, creating a mapping of a syscall code to a name, and then implementing that syscall name in the SyscallInterface class further down that same file. Each syscall method should have the same signature: `read(self, scall: Syscall)`. The `Syscall` object gives you access to the cpu, through which you can access registers and memory. You can look at the `read` or `write` syscalls for further examples. 51 | -------------------------------------------------------------------------------- /riscemu/core/instruction.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Union 3 | from .int32 import Int32 4 | 5 | 6 | class Immediate: 7 | """ 8 | This class solves a problem of ambiguity when interpreting assembly code. 9 | 10 | Let's look at the following four cases (assuming 1b is 16 bytes back): 11 | a) beq a0, a1, 1b // conditional jump 16 bytes back 12 | b) beq a0, a1, -16 // conditional jump 16 bytes back 13 | c) addi a0, a1, 1b // subtract (pc - 16) from a1 14 | d) addi a0, a1, -16 // subtract 16 from a1 15 | 16 | We want a and b to behave the same, but c and d not to. 17 | 18 | The Immediate class solves this problem, by giving each instruction two ways of 19 | interpreting a given immediate. It can either get the "absolute value" of the 20 | immediate, or it's relative value to the PC. 21 | 22 | In this case, the beq instruction would interpret both as PC relative values, 23 | while addi would treat them as absolute values. 24 | """ 25 | 26 | abs_value: Int32 27 | pcrel_value: Int32 28 | 29 | __slots__ = ["abs_value", "pcrel_value"] 30 | 31 | def __init__(self, abs_value: Union[int, Int32], pcrel_value: Union[int, Int32]): 32 | self.abs_value = Int32(abs_value) 33 | self.pcrel_value = Int32(pcrel_value) 34 | 35 | 36 | class Instruction(ABC): 37 | name: str 38 | args: tuple 39 | 40 | @abstractmethod 41 | def get_imm(self, num: int) -> Immediate: 42 | """ 43 | parse and get immediate argument 44 | """ 45 | pass 46 | 47 | @abstractmethod 48 | def get_reg(self, num: int) -> str: 49 | """ 50 | parse and get an register argument 51 | """ 52 | pass 53 | 54 | def __repr__(self): 55 | return "{} {}".format(self.name, ", ".join(self.args)) 56 | 57 | 58 | class InstructionWithEncoding(ABC): 59 | """ 60 | Mixin for instructions that have encodings 61 | """ 62 | 63 | @property 64 | @abstractmethod 65 | def encoding(self) -> int: 66 | raise NotImplementedError() 67 | -------------------------------------------------------------------------------- /riscemu/core/instruction_context.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Dict, List, Optional 3 | 4 | from .exceptions import ParseException 5 | from ..core import T_AbsoluteAddress, T_RelativeAddress, NUMBER_SYMBOL_PATTERN 6 | 7 | 8 | class InstructionContext: 9 | base_address: T_AbsoluteAddress 10 | """ 11 | The address where the instruction block is placed 12 | """ 13 | 14 | labels: Dict[str, T_RelativeAddress] 15 | """ 16 | This dictionary maps all labels to their relative position of the instruction block 17 | """ 18 | 19 | numbered_labels: Dict[str, List[T_RelativeAddress]] 20 | """ 21 | This dictionary maps numbered labels (which can occur multiple times) to a list of (block-relative) addresses where 22 | the label was placed 23 | """ 24 | 25 | global_symbol_dict: Dict[str, T_AbsoluteAddress] 26 | """ 27 | A reference to the MMU's global symbol dictionary for access to global symbols 28 | """ 29 | 30 | def __init__(self): 31 | self.labels = dict() 32 | self.numbered_labels = defaultdict(list) 33 | self.base_address = 0 34 | self.global_symbol_dict = dict() 35 | 36 | def resolve_numerical_label( 37 | self, symbol: str, address_at: int 38 | ) -> Optional[T_AbsoluteAddress]: 39 | direction = symbol[-1] 40 | values = self.numbered_labels.get(symbol[:-1], []) 41 | if direction == "b": 42 | return max( 43 | (addr + self.base_address for addr in values if addr < address_at), 44 | default=None, 45 | ) 46 | else: 47 | return min( 48 | (addr + self.base_address for addr in values if addr > address_at), 49 | default=None, 50 | ) 51 | 52 | def resolve_label(self, symbol: str) -> Optional[T_AbsoluteAddress]: 53 | # if it's not a local symbol, try the globals 54 | if symbol not in self.labels: 55 | return self.global_symbol_dict.get(symbol, None) 56 | # otherwise return the local symbol 57 | return self.labels.get(symbol, None) 58 | -------------------------------------------------------------------------------- /riscemu/instructions/RV_Debug.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from .instruction_set import InstructionSet, Instruction 4 | from ..core import BaseFloat, Int32, Float32 5 | 6 | 7 | class RV_Debug(InstructionSet): 8 | def instruction_print(self, ins: Instruction): 9 | reg = ins.get_reg(0) 10 | if len(ins.args) == 2: 11 | msg = ins.args[1] 12 | print(msg.format(reg, self.regs.get(reg))) 13 | else: 14 | print("register {} contains value {}".format(reg, self.regs.get(reg))) 15 | 16 | def instruction_printf(self, ins: Instruction): 17 | fmt_str = ins.args[0] 18 | regs = tuple(self.smart_get_reg(x) for x in ins.args[1:]) 19 | print(fmt_str.format(*regs)) 20 | 21 | def instruction_print_float(self, ins: Instruction): 22 | reg = ins.get_reg(0) 23 | print("register {} contains value {}".format(reg, self.regs.get_f(reg).value)) 24 | 25 | def instruction_print_float_s(self, ins: Instruction): 26 | reg = ins.get_reg(0) 27 | print( 28 | "register {} contains value {}".format( 29 | reg, Float32.bitcast(self.regs.get_f(reg)).value 30 | ) 31 | ) 32 | 33 | def instruction_print_uint(self, ins: Instruction): 34 | reg = ins.get_reg(0) 35 | print( 36 | "register {} contains value {}".format( 37 | reg, self.regs.get(reg).unsigned_value 38 | ) 39 | ) 40 | 41 | def instruction_print_hex(self, ins: Instruction): 42 | reg = ins.get_reg(0) 43 | print( 44 | "register {} contains value {}".format(reg, hex(self.regs.get(reg).value)) 45 | ) 46 | 47 | def instruction_print_uhex(self, ins: Instruction): 48 | reg = ins.get_reg(0) 49 | print( 50 | "register {} contains value {}".format( 51 | reg, hex(self.regs.get(reg).unsigned_value) 52 | ) 53 | ) 54 | 55 | def smart_get_reg(self, reg_name: str) -> Union[Int32, BaseFloat]: 56 | if reg_name[0] == "f" or reg_name in self.regs.float_vals: 57 | return self.regs.get_f(reg_name) 58 | return self.regs.get(reg_name) 59 | -------------------------------------------------------------------------------- /test/filecheck/snitch/ssr_frep.asm: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m snitch %s -o libc -v --flen 32 | filecheck %s 2 | 3 | .data 4 | 5 | vec0: 6 | .word 0x0, 0x3f800000, 0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000, 0x40e00000, 0x41000000, 0x41100000 7 | vec1: 8 | .word 0x0, 0x3f800000, 0x40000000, 0x40400000, 0x40800000, 0x40a00000, 0x40c00000, 0x40e00000, 0x41000000, 0x41100000 9 | dest: 10 | .space 40 11 | expected: 12 | .word 0x0, 0x3e800000, 0x3f800000, 0x40100000, 0x40800000, 0x40c80000, 0x41100000, 0x41440000, 0x41800000, 0x41a20000 13 | 14 | .text 15 | .globl main 16 | 17 | main: 18 | // ssr config 19 | ssr.configure 0, 10, 4 20 | ssr.configure 1, 10, 4 21 | ssr.configure 2, 10, 4 22 | 23 | // ft0 streams from vec0 24 | la a0, vec0 25 | ssr.read a0, 0, 0 26 | 27 | // ft1 streams from vec1 28 | la a0, vec1 29 | ssr.read a0, 1, 0 30 | 31 | // ft2 streams to dest 32 | la a0, dest 33 | ssr.write a0, 2, 0 34 | 35 | li a0, 9 36 | // some constant to divide by 37 | li t0, 4 38 | fcvt.s.w ft3, t0 39 | ssr.enable 40 | 41 | frep.o a0, 2, 0, 0 42 | fmul.s ft4, ft0, ft1 // ft3 = vec0[i] * vec1[i] 43 | fdiv.s ft2, ft4, ft3 // dest[i] = ft3 / 4 44 | 45 | // stop ssr 46 | ssr.disable 47 | 48 | // check values were written correctly: 49 | la t0, dest 50 | la t1, expected 51 | li a0, 36 52 | loop: 53 | add s0, t0, a0 54 | add s1, t1, a0 55 | 56 | // load vec0, vec1 and dest elements 57 | flw ft0, 0(s0) 58 | flw ft1, 0(s1) 59 | 60 | // assert ft0 == ft1 (expected[i] == dest[i]) 61 | feq.s s0, ft0, ft1 62 | beq zero, s0, fail 63 | 64 | addi a0, a0, -4 65 | bge a0, zero loop 66 | 67 | li a0, 0 68 | ret 69 | 70 | fail: 71 | printf "Assertion failure: {} != {} (at {})", ft0, ft1, a0 72 | li a0, -1 73 | ret 74 | 75 | // CHECK: [CPU] Program exited with code 0 76 | -------------------------------------------------------------------------------- /riscemu/decoder/instruction_table.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from .formats import * 3 | 4 | tbl = lambda: defaultdict(tbl) 5 | 6 | RV32 = tbl() 7 | RV32[0x1B] = "jal" 8 | RV32[0x0D] = "lui" 9 | RV32[0x05] = "auipc" 10 | RV32[0x19][0] = "jalr" 11 | 12 | RV32[0x04][0] = "addi" 13 | RV32[0x04][1] = "slli" 14 | RV32[0x04][2] = "slti" 15 | RV32[0x04][3] = "sltiu" 16 | RV32[0x04][4] = "xori" 17 | RV32[0x04][5][0x00] = "srli" 18 | RV32[0x04][5][0x20] = "srai" 19 | RV32[0x04][6] = "ori" 20 | RV32[0x04][7] = "andi" 21 | 22 | RV32[0x18][0] = "beq" 23 | RV32[0x18][1] = "bne" 24 | RV32[0x18][4] = "blt" 25 | RV32[0x18][5] = "bge" 26 | RV32[0x18][6] = "bltu" 27 | RV32[0x18][7] = "bgeu" 28 | 29 | RV32[0x00][0] = "lb" 30 | RV32[0x00][1] = "lh" 31 | RV32[0x00][2] = "lw" 32 | RV32[0x00][4] = "lbu" 33 | RV32[0x00][5] = "lhu" 34 | 35 | RV32[0x08][0] = "sb" 36 | RV32[0x08][1] = "sh" 37 | RV32[0x08][2] = "sw" 38 | 39 | RV32[0x1C][1] = "csrrw" 40 | RV32[0x1C][2] = "csrrs" 41 | RV32[0x1C][3] = "csrrc" 42 | RV32[0x1C][5] = "csrrwi" 43 | RV32[0x1C][6] = "csrrsi" 44 | RV32[0x1C][7] = "csrrci" 45 | 46 | RV32[0x1C][0][0] = "ecall" 47 | RV32[0x1C][0][1] = "ebreak" 48 | 49 | RV32[0x0C][0][0] = "add" 50 | RV32[0x0C][0][32] = "sub" 51 | RV32[0x0C][1][0] = "sll" 52 | RV32[0x0C][2][0] = "slt" 53 | RV32[0x0C][3][0] = "sltu" 54 | RV32[0x0C][4][0] = "xor" 55 | RV32[0x0C][5][0] = "srl" 56 | RV32[0x0C][5][32] = "sra" 57 | RV32[0x0C][6][0] = "or" 58 | RV32[0x0C][7][0] = "and" 59 | 60 | # rv32m 61 | RV32[0x0C][0][1] = "mul" 62 | RV32[0x0C][1][1] = "mulh" 63 | RV32[0x0C][2][1] = "mulhsu" 64 | RV32[0x0C][3][1] = "mulhu" 65 | RV32[0x0C][4][1] = "div" 66 | RV32[0x0C][5][1] = "divu" 67 | RV32[0x0C][6][1] = "rem" 68 | RV32[0x0C][7][1] = "remu" 69 | 70 | # rv32a 71 | RV32[0b1011][0b10][0b00010] = "lr.w" 72 | RV32[0b1011][0b10][0b00011] = "sc.w" 73 | RV32[0b1011][0b10][0b00001] = "amoswap.w" 74 | RV32[0b1011][0b10][0b00000] = "amoadd.w" 75 | RV32[0b1011][0b10][0b00100] = "amoxor.w" 76 | RV32[0b1011][0b10][0b01100] = "amoand.w" 77 | RV32[0b1011][0b10][0b01000] = "amoor.w" 78 | RV32[0b1011][0b10][0b10000] = "amomin.w" 79 | RV32[0b1011][0b10][0b10100] = "amomax.w" 80 | RV32[0b1011][0b10][0b11000] = "amominu.w" 81 | RV32[0b1011][0b10][0b11100] = "amomaxu.w" 82 | -------------------------------------------------------------------------------- /riscemu/core/program_loader.py: -------------------------------------------------------------------------------- 1 | import os 2 | from abc import abstractmethod, ABC 3 | from typing import Union, Iterator, List, ClassVar 4 | from io import IOBase 5 | 6 | from . import T_ParserOpts, Program 7 | 8 | 9 | class ProgramLoader(ABC): 10 | """ 11 | A program loader is always specific to a given source file. It is a place to store 12 | all state concerning the parsing and loading of that specific source file, including 13 | options. 14 | """ 15 | 16 | is_binary: ClassVar[bool] 17 | 18 | def __init__(self, source_name: str, source: IOBase, options: T_ParserOpts): 19 | self.source_name = source_name 20 | self.source = source 21 | self.options = options 22 | self.filename = os.path.split(self.source_name)[-1] 23 | self.__post_init__() 24 | 25 | def __post_init__(self): 26 | pass 27 | 28 | @classmethod 29 | @abstractmethod 30 | def can_parse(cls, source_name: str) -> float: 31 | """ 32 | Return confidence that the file located at source_path 33 | should be parsed and loaded by this loader 34 | :param source_name: the name of the input 35 | :return: the confidence that this file belongs to this parser in [0,1] 36 | """ 37 | pass 38 | 39 | @classmethod 40 | @abstractmethod 41 | def get_options(cls, argv: List[str]) -> [List[str], T_ParserOpts]: 42 | """ 43 | parse command line args into an options dictionary 44 | 45 | :param argv: the command line args list 46 | :return: all remaining command line args and the parser options object 47 | """ 48 | pass 49 | 50 | @classmethod 51 | def instantiate( 52 | cls, source_name: str, source: IOBase, options: T_ParserOpts 53 | ) -> "ProgramLoader": 54 | """ 55 | Instantiate a loader for the given source file with the required arguments 56 | 57 | :param source_name: the path to the source file 58 | :param source: IO Object representing the source 59 | :param options: the parsed options (guaranteed to come from this classes get_options method). 60 | :return: An instance of a ProgramLoader for the specified source 61 | """ 62 | return cls(source_name, source, options) 63 | 64 | @abstractmethod 65 | def parse(self) -> Union[Program, Iterator[Program]]: 66 | """ 67 | 68 | :return: 69 | """ 70 | pass 71 | -------------------------------------------------------------------------------- /riscemu/debug.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | import os.path 7 | 8 | from .core import SimpleInstruction 9 | from .helpers import * 10 | 11 | if typing.TYPE_CHECKING: 12 | pass 13 | 14 | HIST_FILE = os.path.join(os.path.expanduser("~"), ".riscemu_history") 15 | 16 | 17 | def launch_debug_session(cpu: "CPU", prompt=""): 18 | if cpu.debugger_active: 19 | return 20 | import code 21 | import readline 22 | import rlcompleter 23 | 24 | # set the active debug flag 25 | cpu.debugger_active = True 26 | 27 | # setup some aliases: 28 | registers = cpu.regs 29 | regs = cpu.regs 30 | memory = cpu.mmu 31 | mem = cpu.mmu 32 | mmu = cpu.mmu 33 | 34 | # setup helper functions: 35 | def dump(what, *args, **kwargs): 36 | if what == regs: 37 | regs.dump(*args, **kwargs) 38 | else: 39 | mmu.dump(what, *args, **kwargs) 40 | 41 | def dump_stack(*args, **kwargs): 42 | mmu.dump(regs.get("sp"), *args, **kwargs) 43 | 44 | def ins(): 45 | print("Current instruction at 0x{:08X}:".format(cpu.pc)) 46 | return mmu.read_ins(cpu.pc) 47 | 48 | def run_ins(name, *args: str): 49 | if len(args) > 3: 50 | print("Invalid arg count!") 51 | return 52 | context = mmu.context_for(cpu.pc) 53 | 54 | ins = SimpleInstruction(name, tuple(args), context, cpu.pc) 55 | print(FMT_DEBUG + "Running instruction {}".format(ins) + FMT_NONE) 56 | cpu.run_instruction(ins) 57 | 58 | def cont(verbose=False): 59 | try: 60 | cpu.run(verbose) 61 | except LaunchDebuggerException: 62 | print(FMT_DEBUG + "Returning to debugger...") 63 | return 64 | 65 | def step(): 66 | try: 67 | cpu.step(verbose=True) 68 | except LaunchDebuggerException: 69 | return 70 | 71 | # collect all variables 72 | sess_vars = globals() 73 | sess_vars.update(locals()) 74 | 75 | # add tab completion 76 | readline.set_completer(rlcompleter.Completer(sess_vars).complete) 77 | readline.parse_and_bind("tab: complete") 78 | if os.path.exists(HIST_FILE): 79 | readline.read_history_file(HIST_FILE) 80 | 81 | relaunch_debugger = False 82 | 83 | try: 84 | code.InteractiveConsole(sess_vars).interact( 85 | banner=FMT_DEBUG + prompt + FMT_NONE, 86 | exitmsg="Exiting debugger", 87 | ) 88 | finally: 89 | cpu.debugger_active = False 90 | readline.write_history_file(HIST_FILE) 91 | -------------------------------------------------------------------------------- /test/test_isa.py: -------------------------------------------------------------------------------- 1 | from riscemu.colors import FMT_ERROR, FMT_NONE, FMT_BOLD, FMT_GREEN 2 | from riscemu.instructions import InstructionSet 3 | from riscemu.core import Instruction, CPU 4 | from riscemu.decoder import RISCV_REGS 5 | 6 | FMT_SUCCESS = FMT_GREEN + FMT_BOLD 7 | 8 | 9 | def assert_equals(ins: Instruction, cpu: CPU): 10 | a, b = (get_arg_from_ins(ins, i, cpu) for i in (0, 2)) 11 | return a == b 12 | 13 | 14 | def assert_equals_mem(ins: Instruction, cpu: CPU): 15 | a, b = (get_arg_from_ins(ins, i, cpu) for i in (0, 2)) 16 | a = cpu.mmu.read_int(a) 17 | return a == b 18 | 19 | 20 | def assert_in(ins: Instruction, cpu: CPU): 21 | a = get_arg_from_ins(ins, 0, cpu) 22 | others = [get_arg_from_ins(ins, i, cpu) for i in range(2, len(ins.args))] 23 | return a in others 24 | 25 | 26 | def _not(func): 27 | def test(ins: Instruction, cpu: CPU): 28 | return not func(ins, cpu) 29 | 30 | return test 31 | 32 | 33 | def get_arg_from_ins(ins: Instruction, num: int, cpu: CPU): 34 | a = ins.args[num] 35 | if a in RISCV_REGS: 36 | return cpu.regs.get(a) 37 | return ins.get_imm(num) 38 | 39 | 40 | assert_ops = { 41 | "==": assert_equals, 42 | "!=": _not(assert_equals), 43 | "in": assert_in, 44 | "not_in": _not(assert_in), 45 | } 46 | 47 | 48 | class Z_test(InstructionSet): 49 | def __init__(self, cpu: "CPU"): 50 | print( 51 | "[Test] loading testing ISA, this is only meant for running testcases and is not part of the RISC-V ISA!" 52 | ) 53 | self.failed = False 54 | super().__init__(cpu) 55 | 56 | def instruction_assert(self, ins: Instruction): 57 | if len(ins.args) < 3: 58 | print( 59 | FMT_ERROR + "[Test] Unknown assert statement: {}".format(ins) + FMT_NONE 60 | ) 61 | return 62 | op = ins.args[1] 63 | if op not in assert_ops: 64 | print( 65 | FMT_ERROR 66 | + "[Test] Unknown operation statement: {} in {}".format(op, ins) 67 | + FMT_NONE 68 | ) 69 | return 70 | 71 | if assert_ops[op](ins, self.cpu): 72 | print(FMT_SUCCESS + "[TestCase] 🟢 passed assertion {}".format(ins)) 73 | else: 74 | print(FMT_ERROR + "[TestCase] 🔴 failed assertion {}".format(ins)) 75 | self.cpu.halted = True 76 | self.failed = True 77 | 78 | def instruction_fail(self, ins: Instruction): 79 | print(FMT_ERROR + "[TestCase] 🔴 reached fail instruction! {}".format(ins)) 80 | self.cpu.halted = True 81 | self.failed = True 82 | -------------------------------------------------------------------------------- /riscemu/core/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | import re 3 | 4 | # define some base type aliases so we can keep track of absolute and relative addresses 5 | T_RelativeAddress = int 6 | T_AbsoluteAddress = int 7 | 8 | # parser options are just dictionaries with arbitrary values 9 | T_ParserOpts = Dict[str, Any] 10 | 11 | NUMBER_SYMBOL_PATTERN = re.compile(r"^\d+[fb]$") 12 | 13 | # exceptions 14 | from .exceptions import ( 15 | ParseException, 16 | NumberFormatException, 17 | MemoryAccessException, 18 | OutOfMemoryException, 19 | LinkerException, 20 | LaunchDebuggerException, 21 | RiscemuBaseException, 22 | InvalidRegisterException, 23 | InvalidAllocationException, 24 | InvalidSyscallException, 25 | UnimplementedInstruction, 26 | INS_NOT_IMPLEMENTED, 27 | ) 28 | 29 | # base classes 30 | from .flags import MemoryFlags 31 | from .int32 import UInt32, Int32 32 | from .float import BaseFloat, Float32, Float64 33 | from .rtclock import RTClock 34 | from .instruction import Instruction, Immediate, InstructionWithEncoding 35 | from .instruction_context import InstructionContext 36 | from .memory_section import MemorySection 37 | from .program import Program 38 | from .program_loader import ProgramLoader 39 | from .privmodes import PrivModes 40 | from .mmu import MMU 41 | from .csr import CSR 42 | from .registers import Registers 43 | from .cpu import CPU 44 | from .simple_instruction import SimpleInstruction 45 | from .instruction_memory_section import InstructionMemorySection 46 | from .binary_data_memory_section import BinaryDataMemorySection 47 | from .usermode_cpu import UserModeCPU 48 | 49 | __all__ = [ 50 | "T_RelativeAddress", 51 | "T_AbsoluteAddress", 52 | "T_ParserOpts", 53 | "NUMBER_SYMBOL_PATTERN", 54 | "ParseException", 55 | "NumberFormatException", 56 | "MemoryAccessException", 57 | "OutOfMemoryException", 58 | "LinkerException", 59 | "LaunchDebuggerException", 60 | "RiscemuBaseException", 61 | "InvalidRegisterException", 62 | "InvalidAllocationException", 63 | "InvalidSyscallException", 64 | "UnimplementedInstruction", 65 | "INS_NOT_IMPLEMENTED", 66 | "MemoryFlags", 67 | "UInt32", 68 | "Int32", 69 | "BaseFloat", 70 | "Float32", 71 | "Float64", 72 | "RTClock", 73 | "Instruction", 74 | "Immediate", 75 | "InstructionWithEncoding", 76 | "InstructionContext", 77 | "MemorySection", 78 | "Program", 79 | "ProgramLoader", 80 | "PrivModes", 81 | "MMU", 82 | "CSR", 83 | "Registers", 84 | "CPU", 85 | "SimpleInstruction", 86 | "InstructionMemorySection", 87 | "BinaryDataMemorySection", 88 | "UserModeCPU", 89 | ] 90 | -------------------------------------------------------------------------------- /snitch/regs.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Tuple 2 | from riscemu.core import Registers, MMU, BaseFloat 3 | 4 | from dataclasses import dataclass 5 | 6 | from enum import Enum 7 | 8 | 9 | class StreamMode(Enum): 10 | READ = 1 11 | WRITE = 2 12 | # READWRITE = 3 13 | 14 | 15 | @dataclass 16 | class StreamDef: 17 | base: int = 0 18 | """ 19 | Base address to read from 20 | """ 21 | bound: int = 0 22 | """ 23 | How many elements are in here 24 | """ 25 | stride: int = 0 26 | """ 27 | stride = register width 28 | """ 29 | mode: StreamMode = StreamMode.READ 30 | """ 31 | Differentiate between read/write 32 | """ 33 | dim: int = 0 34 | """ 35 | Supports nested loops 36 | """ 37 | # internal: 38 | pos: int = 0 39 | """ 40 | Next element in stream 41 | """ 42 | 43 | 44 | class StreamingRegs(Registers): 45 | mem: MMU 46 | dm_by_id: List[StreamDef] 47 | streams: Dict[str, StreamDef] 48 | enabled: bool 49 | 50 | def __init__( 51 | self, 52 | mem: MMU, 53 | xssr_regs: Tuple[str] = ("ft0", "ft1", "ft2"), 54 | infinite_regs: bool = False, 55 | flen: int = 64, 56 | ): 57 | self.mem = mem 58 | self.enabled = False 59 | self.streams = dict() 60 | self.dm_by_id = [] 61 | for reg in xssr_regs: 62 | stream_def = StreamDef() 63 | self.dm_by_id.append(stream_def) 64 | self.streams[reg] = stream_def 65 | super().__init__(infinite_regs=infinite_regs, flen=flen) 66 | 67 | def get_f(self, reg) -> "BaseFloat": 68 | if not self.enabled or reg not in self.streams: 69 | return super().get_f(reg) 70 | 71 | # do the streaming stuff: 72 | stream = self.streams[reg] 73 | # TODO: Implement other modes 74 | assert stream.mode is StreamMode.READ 75 | # TODO: Check overflow 76 | # TODO: repetition 77 | addr = stream.base + (stream.pos * stream.stride) 78 | val = self._float_type(self.mem.read(addr, self.flen // 8)) 79 | # increment pos 80 | stream.pos += 1 81 | return val 82 | 83 | def set_f(self, reg, val: "BaseFloat") -> bool: 84 | if not self.enabled or reg not in self.streams: 85 | return super().set_f(reg, val) 86 | 87 | stream = self.streams[reg] 88 | assert stream.mode is StreamMode.WRITE 89 | 90 | addr = stream.base + (stream.pos * stream.stride) 91 | data = val.bytes 92 | self.mem.write(addr + (self.flen // 8) - len(data), len(data), bytearray(data)) 93 | 94 | stream.pos += 1 95 | return True 96 | -------------------------------------------------------------------------------- /riscemu/decoder/formats.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Callable, List, Union 2 | from .regs import RISCV_REGS 3 | 4 | 5 | def op(ins: int): 6 | return (ins >> 2) & 31 7 | 8 | 9 | def rd(ins: int): 10 | return (ins >> 7) & 31 11 | 12 | 13 | def funct3(ins: int): 14 | return (ins >> 12) & 7 15 | 16 | 17 | def rs1(ins: int): 18 | return (ins >> 15) & 31 19 | 20 | 21 | def rs2(ins: int): 22 | return (ins >> 20) & 31 23 | 24 | 25 | def funct7(ins: int): 26 | return ins >> 25 27 | 28 | 29 | def imm110(ins: int): 30 | return ins >> 20 31 | 32 | 33 | def imm3112(ins: int): 34 | return ins >> 12 35 | 36 | 37 | def imm_i(ins: int): 38 | return sign_extend(imm110(ins), 12) 39 | 40 | 41 | def imm_s(ins: int): 42 | num = (funct7(ins) << 5) + rd(ins) 43 | return sign_extend(num, 12) 44 | 45 | 46 | def imm_b(ins: int): 47 | lower = rd(ins) 48 | higher = funct7(ins) 49 | 50 | num = ( 51 | (lower & 0b11110) 52 | + ((higher & 0b0111111) << 5) 53 | + ((lower & 1) << 11) 54 | + ((higher >> 6) << 12) 55 | ) 56 | return sign_extend(num, 13) 57 | 58 | 59 | def imm_u(ins: int): 60 | return sign_extend(imm3112(ins), 20) 61 | 62 | 63 | def imm_j(ins: int): 64 | return sign_extend( 65 | (((ins >> 21) & 0b1111111111) << 1) 66 | + (((ins >> 20) & 1) << 11) 67 | + (((ins >> 12) & 0b11111111) << 12) 68 | + (((ins >> 31) & 1) << 20), 69 | 21, 70 | ) 71 | 72 | 73 | def sign_extend(num, bits): 74 | sign_mask = 1 << (bits - 1) 75 | return (num & (sign_mask - 1)) - (num & sign_mask) 76 | 77 | 78 | def decode_i(ins: int) -> List[int]: 79 | return [rd(ins), rs1(ins), imm_i(ins)] 80 | 81 | 82 | def decode_b(ins: int) -> List[int]: 83 | return [rs1(ins), rs2(ins), imm_b(ins)] 84 | 85 | 86 | def decode_u(ins: int) -> List[int]: 87 | return [rd(ins), imm_u(ins)] 88 | 89 | 90 | def decode_r(ins: int) -> List[int]: 91 | return [rd(ins), rs1(ins), rs2(ins)] 92 | 93 | 94 | def decode_s(ins: int) -> List[int]: 95 | return [rs2(ins), rs1(ins), imm_s(ins)] 96 | 97 | 98 | def decode_j(ins: int) -> List[int]: 99 | return [rd(ins), imm_j(ins)] 100 | 101 | 102 | def decode_i_shamt(ins: int) -> List[int]: 103 | if funct3(ins) in (1, 5): 104 | return [rd(ins), rs1(ins), rs2(ins)] 105 | return decode_i(ins) 106 | 107 | 108 | def decode_i_unsigned(ins: int) -> List[int]: 109 | return [rd(ins), rs1(ins), imm110(ins)] 110 | 111 | 112 | INSTRUCTION_ARGS_DECODER: Dict[int, Callable[[int], List[int]]] = { 113 | 0x00: decode_i, 114 | 0x04: decode_i_shamt, 115 | 0x05: decode_u, 116 | 0x08: decode_s, 117 | 0x0C: decode_r, 118 | 0x0D: decode_u, 119 | 0x18: decode_b, 120 | 0x19: decode_i, 121 | 0x1B: decode_j, 122 | 0x1C: decode_i_unsigned, 123 | 0b1011: decode_r, 124 | } 125 | -------------------------------------------------------------------------------- /sphinx-docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | 5 | # Configuration file for the Sphinx documentation builder. 6 | # 7 | # This file only contains a selection of the most common options. For a full 8 | # list see the documentation: 9 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 10 | 11 | # -- Path setup -------------------------------------------------------------- 12 | 13 | # If extensions (or modules to document with autodoc) are in another directory, 14 | # add these directories to sys.path here. If the directory is relative to the 15 | # documentation root, use os.path.abspath to make it absolute, like shown here. 16 | # 17 | # import os 18 | # import sys 19 | # sys.path.insert(0, os.path.abspath('.')) 20 | 21 | if os.getenv("READTHEDOCS", False) and not os.path.exists("riscemu.rst"): 22 | subprocess.check_call(["../../generate-docs.sh", "generate"]) 23 | 24 | # -- Project information ----------------------------------------------------- 25 | 26 | project = "RiscEmu" 27 | copyright = "2022, Anton Lydike" 28 | author = "Anton Lydike" 29 | 30 | # The full version, including alpha/beta/rc tags 31 | release = "2.0.0a2" 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = ["sphinx.ext.autodoc", "recommonmark"] 39 | 40 | # autodoc options 41 | autodoc_default_options = { 42 | "members": True, 43 | "member-order": "bysource", 44 | "special-members": "__init__", 45 | } 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ["_templates"] 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This pattern also affects html_static_path and html_extra_path. 53 | # Markdown support 54 | 55 | from recommonmark.parser import CommonMarkParser 56 | 57 | # The suffix of source filenames. 58 | source_suffix = [".rst", ".md"] 59 | 60 | source_parsers = { 61 | ".md": CommonMarkParser, 62 | } 63 | 64 | # -- Options for HTML output ------------------------------------------------- 65 | 66 | # The theme to use for HTML and HTML Help pages. See the documentation for 67 | # a list of builtin themes. 68 | # 69 | html_theme = "alabaster" 70 | 71 | pygments_style = "sphinx" 72 | 73 | # Add any paths that contain custom static files (such as style sheets) here, 74 | # relative to this directory. They are copied after the builtin static files, 75 | # so a file named "default.css" will overwrite the builtin "default.css". 76 | html_static_path = ["_static"] 77 | 78 | sys.path.insert(0, os.path.abspath("../../")) 79 | 80 | if os.getenv("READTHEDOCS", False): 81 | import sphinx_rtd_theme 82 | 83 | extensions.append("sphinx_rtd_theme") 84 | 85 | html_theme = "sphinx_rtd_theme" 86 | -------------------------------------------------------------------------------- /riscemu/decoder/decoder.py: -------------------------------------------------------------------------------- 1 | from .instruction_table import * 2 | from typing import Tuple, List 3 | 4 | 5 | def print_ins(ins: int): 6 | print(" f7 rs2 rs1 f3 rd op") 7 | print( 8 | f"0b{ins >> 25 :07b}_{(ins >> 20) & 0b11111:05b}_{(ins >> 15) & 0b11111:05b}_{(ins >> 12) & 0b111:03b}_{(ins >> 7) & 0b11111:05b}_{ins & 0b1111111:07b}" 9 | ) 10 | 11 | 12 | STATIC_INSN: Dict[int, Tuple[str, List[int], int]] = { 13 | 0x00000013: ("nop", [], 0x00000013), 14 | 0x00008067: ("ret", [], 0x00008067), 15 | 0xFE010113: ("addi", [2, 2, -32], 0xFE010113), 16 | 0x02010113: ("addi", [2, 2, 32], 0x02010113), 17 | 0x00100073: ("ebreak", [], 0x00100073), 18 | 0x00000073: ("ecall", [], 0x00000073), 19 | 0x30200073: ("mret", [], 0x30200073), 20 | 0x00200073: ("uret", [], 0x00200073), 21 | 0x10200073: ("sret", [], 0x10200073), 22 | 0x10500073: ("wfi", [], 0x10500073), 23 | } 24 | 25 | 26 | def int_from_ins(insn: bytearray): 27 | return int.from_bytes(insn, "little") 28 | 29 | 30 | def name_from_insn(ins: int): 31 | opcode = op(ins) 32 | if opcode not in RV32: 33 | print_ins(ins) 34 | raise RuntimeError(f"Invalid opcode: {opcode:0x} in insn {ins:x}") 35 | dec = RV32[opcode] 36 | 37 | if isinstance(dec, str): 38 | return dec 39 | 40 | fun3 = funct3(ins) 41 | if fun3 not in dec: 42 | print_ins(ins) 43 | raise RuntimeError(f"Invalid funct3: {fun3:0x} in insn {ins:x}") 44 | 45 | dec = dec[fun3] 46 | if isinstance(dec, str): 47 | return dec 48 | 49 | if opcode == 0x1C and fun3 == 0: 50 | # we have ecall/ebreak 51 | token = imm110(ins) 52 | if token in dec: 53 | return dec[token] 54 | print_ins(ins) 55 | raise RuntimeError(f"Invalid instruction in ebreak/ecall region: {ins:x}") 56 | 57 | fun7 = funct7(ins) 58 | if opcode == 0b1011 and fun3 == 0b10: 59 | # ignore the two aq/lr bits located in the fun7 block 60 | # riscemu has no memory reordering, therefore we don't need to look at these bits ever 61 | fun7 = fun7 >> 2 62 | 63 | if fun7 in dec: 64 | if opcode == 0x0C or (opcode == 0x04 and fun3 == 5): 65 | dec = dec[fun7] 66 | return dec 67 | print("unknown instruction?!") 68 | return dec[fun7] 69 | 70 | print_ins(ins) 71 | raise RuntimeError(f"Invalid instruction: {ins:x}") 72 | 73 | 74 | def decode(ins: Union[bytearray, bytes]) -> Tuple[str, List[int], int]: 75 | insn = int_from_ins(ins) 76 | 77 | if insn & 3 != 3: 78 | print_ins(insn) 79 | raise RuntimeError("Not a RV32 instruction!") 80 | 81 | if insn in STATIC_INSN: 82 | return STATIC_INSN[insn] 83 | 84 | opcode = op(insn) 85 | if opcode not in INSTRUCTION_ARGS_DECODER: 86 | print_ins(insn) 87 | raise RuntimeError("No instruction decoder found for instruction") 88 | 89 | return name_from_insn(insn), INSTRUCTION_ARGS_DECODER[opcode](insn), insn 90 | -------------------------------------------------------------------------------- /riscemu/priv/ImageLoader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Loads a memory image with debug information into memory 3 | """ 4 | 5 | import os.path 6 | from io import IOBase 7 | from typing import List, Iterable 8 | 9 | from .ElfLoader import ElfMemorySection 10 | from .types import MemoryImageDebugInfos 11 | from ..assembler import INSTRUCTION_SECTION_NAMES 12 | from ..colors import FMT_NONE, FMT_PARSE 13 | from ..core import MemoryFlags, ProgramLoader, Program, T_ParserOpts 14 | 15 | 16 | class MemoryImageLoader(ProgramLoader): 17 | is_binary = True 18 | 19 | @classmethod 20 | def can_parse(cls, source_name: str) -> float: 21 | if source_name.split(".")[-1] == "img": 22 | return 1 23 | return 0 24 | 25 | @classmethod 26 | def get_options(cls, argv: List[str]) -> [List[str], T_ParserOpts]: 27 | return argv, {} 28 | 29 | def parse(self) -> Iterable[Program]: 30 | if "debug" not in self.options: 31 | yield self.parse_no_debug() 32 | return 33 | 34 | with open(self.options.get("debug"), "r") as debug_file: 35 | debug_info = MemoryImageDebugInfos.load(debug_file.read()) 36 | 37 | with self.source as source_file: 38 | data: bytearray = bytearray(source_file.read()) 39 | 40 | for name, sections in debug_info.sections.items(): 41 | program = Program(name) 42 | 43 | for sec_name, (start, size) in sections.items(): 44 | if program.base is None: 45 | program.base = start 46 | 47 | # in_code_sec = get_section_base_name(sec_name) in INSTRUCTION_SECTION_NAMES 48 | program.add_section( 49 | ElfMemorySection( 50 | data[start : start + size], 51 | sec_name, 52 | program.context, 53 | name, 54 | start, 55 | MemoryFlags(False, True), 56 | ) 57 | ) 58 | 59 | program.context.labels.update(debug_info.symbols.get(name, dict())) 60 | program.global_labels.update(debug_info.globals.get(name, set())) 61 | 62 | yield program 63 | 64 | def parse_no_debug(self) -> Program: 65 | print( 66 | FMT_PARSE 67 | + "[MemoryImageLoader] Warning: loading memory image without debug information!" 68 | + FMT_NONE 69 | ) 70 | 71 | with self.source as source_file: 72 | data: bytes = source_file.read() 73 | 74 | p = Program(self.filename) 75 | p.add_section( 76 | ElfMemorySection( 77 | bytearray(data), ".text", p.context, p.name, 0, MemoryFlags(False, True) 78 | ) 79 | ) 80 | return p 81 | 82 | @classmethod 83 | def instantiate( 84 | cls, source_name: str, source: IOBase, options: T_ParserOpts 85 | ) -> "ProgramLoader": 86 | if os.path.isfile(source_name + ".dbg"): 87 | return MemoryImageLoader( 88 | source_name, source, dict(**options, debug=source_name + ".dbg") 89 | ) 90 | return MemoryImageLoader(source_name, source, options) 91 | -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | # Using the debugger 2 | 3 | You are launched into the debugger either by an `ebreak/sbreak` instruction, or when an exception occurs while running executing instructions. 4 | 5 | Consider the example program `examples/fibs.asm`: 6 | 7 | ```asm riscv-asm 8 | .data 9 | fibs: .space 56 10 | 11 | .text 12 | main: 13 | addi s1, zero, 0 ; storage index 14 | addi s2, zero, 56 ; last storage index 15 | addi t0, zero, 1 ; t0 = F_{i} 16 | addi t1, zero, 1 ; t1 = F_{i+1} 17 | ebreak ; launch debugger 18 | loop: 19 | sw t0, fibs(s1) ; save 20 | add t2, t1, t0 ; t2 = F_{i+2} 21 | addi t0, t1, 0 ; t0 = t1 22 | addi t1, t2, 0 ; t1 = t2 23 | addi s1, s1, 4 ; increment storage pointer 24 | blt s1, s2, loop ; loop as long as we did not reach array length 25 | ebreak ; launch debugger 26 | ; exit gracefully 27 | addi a0, zero, 0 28 | addi a7, zero, 93 29 | scall ; exit with code 0 30 | ``` 31 | 32 | This calculates the fibonacci sequence and stores it in memory at `fibs`. before and after it calculated all fibonacci numbers, it 33 | uses the `ebreak` instruction to open the debugger. Let's run it and see what happens: 34 | 35 | ``` 36 | > python -m riscemu examples/fibs.asm 37 | [MMU] Successfully loaded: LoadedExecutable[examples/fibs.asm](base=0x00000100, size=72bytes, sections=data text, run_ptr=0x00000138) 38 | [CPU] Started running from 0x00000138 (examples/fibs.asm) 39 | Debug instruction encountered at 0x0000013C 40 | >>> 41 | ``` 42 | 43 | In this interactive session, you have access to the cpu, registers, memory and syscall interface. You can look into everything, 44 | and most common tasks should have helper methods for them. 45 | 46 | **Available objects are:** 47 | 48 | * `mem`: (aka `mmu` or `memory`) 49 | * `dump(address, fmt='hex', max_rows=10, group=4, bytes_per_row=16, all=False`: 50 | Dumps the memory at `address`, in at most `max_rows` rows, each containing `bytes_per_row` bytes grouped 51 | into groups of `group` bytes. They can be printed as: 52 | * `hex`: hexadecimal, unsigned 53 | * `int`: converted to integers 54 | * `uint`: converted to unsigned integers 55 | * `symbol(name)`: Lookup all symbols named `name` 56 | * `reg`: (aka `regs` or `registers`) 57 | * `dump(full=False)` dumps all integer registers (unless `all` is true, then all registers are printed) 58 | * `get(name)` get register content 59 | * `set(name, val)` set register content 60 | * `cpu`: 61 | * The CPU has the `pc` attribute and `cycle` attribute. Others won't be useful in this context. 62 | 63 | **Available helpers are:** 64 | 65 | * `dump(regs | addr)` dumps either registers or memory address 66 | * `cont(verbose=False)` continue execution (verbose prints each executed instruction) 67 | * `step()` run the next instruction 68 | * `ins()` get current instruction (this reference is mutable, if you want to edit your code on the fly) 69 | * `run_ins(name, *args)` Run an instruction in the current context. Symbols, jumping, etc are supported! 70 | 71 | 72 | Example: 73 | 74 | ![debugging the fibs program](debug-session.png) 75 | -------------------------------------------------------------------------------- /test/filecheck/libc/test-string.s: -------------------------------------------------------------------------------- 1 | // RUN: python3 -m riscemu -v %s -o libc | filecheck %s 2 | 3 | .data 4 | 5 | data: 6 | .byte 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 7 | .byte 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 8 | 9 | dest: 10 | .byte 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB 11 | .byte 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB 12 | 13 | small_str: 14 | .string "test" 15 | 16 | .text 17 | 18 | .globl main 19 | main: 20 | // test that strlen(data) == 15 21 | addi sp, sp, -4 22 | sw ra, 0(sp) 23 | la a0, data 24 | // call strlen(data) 25 | jal strlen 26 | li t0, 15 27 | bne a0, t0, _fail 28 | 29 | // now memcpy strlen(data)+1 bytes from data to dest 30 | la a0, dest 31 | la a1, data 32 | li a2, 16 33 | // call strncpy(dest, data, 16) 34 | jal strncpy 35 | la a1, dest 36 | // fail because strncpy should return pointer to dest 37 | bne a0, a1, _fail 38 | // check that dest and data are the same 39 | jal check_data_dest_is_same 40 | la a0, dest 41 | li a1, 0x11 42 | li a2, 16 43 | // test that memset(dest) workds 44 | // call memset(dest, 0x11, 16) 45 | jal memset 46 | // check that all of dest is 0x11111111 47 | li t1, 0x11111111 48 | la a0, dest 49 | lw t0, 0(a0) 50 | bne t0, t1, _fail 51 | lw t0, 1(a0) 52 | bne t0, t1, _fail 53 | lw t0, 2(a0) 54 | bne t0, t1, _fail 55 | lw t0, 3(a0) 56 | bne t0, t1, _fail 57 | // test memchr 58 | // test memchr 59 | la a0, data 60 | li a1, 0x55 61 | li a2, 16 62 | // memchr(data, 0x55, 16) 63 | jal memchr 64 | la t0, data 65 | addi t0, t0, 4 66 | // fail if a0 != data+4 67 | bne a0, t0, _fail 68 | la a0, data 69 | li a1, 0x12 70 | li a2, 16 71 | // memchr(data, 0x12, 16) 72 | jal memchr 73 | // check that result is NULL 74 | bne a0, zero, _fail 75 | // test strcpy 76 | la a0, dest 77 | la a1, small_str 78 | // call strcpy(dest, small_str) 79 | jal strcpy 80 | la t0, dest 81 | lw t1, 0(a0) 82 | // ascii for "tset", as risc-v is little endian 83 | li t2, 0x74736574 84 | bne t1, t2, _fail 85 | 86 | // return to exit() wrapper 87 | lw ra, 0(sp) 88 | addi sp, sp, 4 89 | li a0, 0 90 | ret 91 | 92 | _fail: 93 | ebreak 94 | // fail the test run 95 | li a0, -1 96 | jal exit 97 | 98 | 99 | check_data_dest_is_same: 100 | la a0, data 101 | la a1, dest 102 | li a2, 4 103 | 1: 104 | lw t0, 0(a0) 105 | lw t1, 0(a1) 106 | bne t0, t1, _fail 107 | addi a0, a0, 4 108 | addi a1, a1, 4 109 | addi a2, a2, -1 110 | blt zero, a2, 1b 111 | ret 112 | 113 | 114 | //CHECK: [CPU] Program exited with code 0 115 | -------------------------------------------------------------------------------- /riscemu/instructions/RV32A.py: -------------------------------------------------------------------------------- 1 | from .instruction_set import InstructionSet, Instruction 2 | from riscemu.core.exceptions import INS_NOT_IMPLEMENTED 3 | from ..core import Int32, UInt32 4 | 5 | 6 | class RV32A(InstructionSet): 7 | """ 8 | The RV32A instruction set. Currently, load-reserved and store conditionally are not supported 9 | due to limitations in the way the MMU is implemented. Maybe a later implementation will add support 10 | for this? 11 | """ 12 | 13 | def instruction_lr_w(self, ins: "Instruction"): 14 | INS_NOT_IMPLEMENTED(ins) 15 | 16 | def instruction_sc_w(self, ins: "Instruction"): 17 | INS_NOT_IMPLEMENTED(ins) 18 | 19 | def instruction_amoswap_w(self, ins: "Instruction"): 20 | dest, addr, val = self.parse_rd_rs_rs(ins) 21 | if dest == "zero": 22 | self.mmu.write(addr.unsigned_value, val.to_bytes()) 23 | else: 24 | old = Int32(self.mmu.read(addr.unsigned_value, 4)) 25 | self.mmu.write(addr.unsigned_value, val.to_bytes()) 26 | self.regs.set(dest, old) 27 | 28 | def instruction_amoadd_w(self, ins: "Instruction"): 29 | dest, addr, val = self.parse_rd_rs_rs(ins) 30 | old = Int32(self.mmu.read(addr.unsigned_value, 4)) 31 | self.mmu.write(addr.unsigned_value, (old + val).to_bytes(4)) 32 | self.regs.set(dest, old) 33 | 34 | def instruction_amoand_w(self, ins: "Instruction"): 35 | dest, addr, val = self.parse_rd_rs_rs(ins) 36 | old = Int32(self.mmu.read(addr.unsigned_value, 4)) 37 | self.mmu.write(addr.unsigned_value, (old & val).to_bytes(4)) 38 | self.regs.set(dest, old) 39 | 40 | def instruction_amoor_w(self, ins: "Instruction"): 41 | dest, addr, val = self.parse_rd_rs_rs(ins) 42 | old = Int32(self.mmu.read(addr.unsigned_value, 4)) 43 | self.mmu.write(addr.unsigned_value, (old | val).to_bytes(4)) 44 | self.regs.set(dest, old) 45 | 46 | def instruction_amoxor_w(self, ins: "Instruction"): 47 | dest, addr, val = self.parse_rd_rs_rs(ins) 48 | old = Int32(self.mmu.read(addr.unsigned_value, 4)) 49 | self.mmu.write(addr.unsigned_value, (old ^ val).to_bytes(4)) 50 | self.regs.set(dest, old) 51 | 52 | def instruction_amomax_w(self, ins: "Instruction"): 53 | dest, addr, val = self.parse_rd_rs_rs(ins) 54 | old = Int32(self.mmu.read(addr.unsigned_value, 4)) 55 | self.mmu.write(addr.unsigned_value, max(old, val).to_bytes(4)) 56 | self.regs.set(dest, old) 57 | 58 | def instruction_amomaxu_w(self, ins: "Instruction"): 59 | val: UInt32 60 | dest, addr, val = self.parse_rd_rs_rs(ins) 61 | old = UInt32(self.mmu.read(addr.unsigned_value, 4)) 62 | 63 | self.mmu.write(addr.unsigned_value, max(old, val.unsigned()).to_bytes()) 64 | self.regs.set(dest, old) 65 | 66 | def instruction_amomin_w(self, ins: "Instruction"): 67 | dest, addr, val = self.parse_rd_rs_rs(ins) 68 | old = Int32(self.mmu.read(addr.unsigned_value, 4)) 69 | self.mmu.write(addr.unsigned_value, min(old, val).to_bytes(4)) 70 | self.regs.set(dest, old) 71 | 72 | def instruction_amominu_w(self, ins: "Instruction"): 73 | val: UInt32 74 | dest, addr, val = self.parse_rd_rs_rs(ins) 75 | old = UInt32(self.mmu.read(addr.unsigned_value, 4)) 76 | 77 | self.mmu.write(addr.unsigned_value, min(old, val.unsigned()).to_bytes(4)) 78 | self.regs.set(dest, old) 79 | -------------------------------------------------------------------------------- /riscemu/instructions/Zicsr.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from .instruction_set import InstructionSet, Instruction 4 | from ..core import Int32, UInt32 5 | from ..core.csr_constants import CSR_NAME_TO_ADDR 6 | from ..helpers import parse_numeric_argument 7 | 8 | 9 | class Zicsr(InstructionSet): 10 | def instruction_csrrw(self, ins: Instruction): 11 | rd, new_val, csr = self._parse_csr_ins(ins) 12 | if rd != "zero": 13 | self.regs.set(rd, self.cpu.csr.get(csr)) 14 | self.cpu.csr.set(csr, new_val) 15 | 16 | def instruction_csrrwi(self, ins: Instruction): 17 | rd, new_val, csr = self.parse_csr_imm_ins(ins) 18 | if rd != "zero": 19 | self.regs.set(rd, self.cpu.csr.get(csr)) 20 | self.cpu.csr.set(csr, new_val) 21 | 22 | def instruction_csrrs(self, ins: Instruction): 23 | rd, bitmask, csr = self._parse_csr_ins(ins) 24 | val = self.cpu.csr.get(csr) 25 | if rd != "zero": 26 | self.regs.set(rd, val) 27 | if bitmask != 0: 28 | self.cpu.csr.set(csr, val | bitmask) 29 | 30 | def instruction_csrrsi(self, ins: Instruction): 31 | rd, bitmask, csr = self.parse_csr_imm_ins(ins) 32 | val = self.cpu.csr.get(csr) 33 | if rd != "zero": 34 | self.regs.set(rd, val) 35 | if bitmask != 0: 36 | self.cpu.csr.set(csr, val | bitmask) 37 | 38 | def instruction_csrrc(self, ins: Instruction): 39 | rd, bitmask, csr = self._parse_csr_ins(ins) 40 | val = self.cpu.csr.get(csr) 41 | if rd != "zero": 42 | self.regs.set(rd, val) 43 | if bitmask != 0: 44 | self.cpu.csr.set(csr, val & ~bitmask) 45 | 46 | def instruction_csrrci(self, ins: Instruction): 47 | rd, bitmask, csr = self.parse_csr_imm_ins(ins) 48 | val = self.cpu.csr.get(csr) 49 | if rd != "zero": 50 | self.regs.set(rd, val) 51 | if bitmask != 0: 52 | self.cpu.csr.set(csr, val & ~bitmask) 53 | 54 | def instruction_rdtime(self, ins: Instruction): 55 | rd = ins.get_reg(0) 56 | self.regs.set(rd, self.cpu.rtclock.get_low32()) 57 | 58 | def instruction_rdtimeh(self, ins: Instruction): 59 | rd = ins.get_reg(0) 60 | self.regs.set(rd, self.cpu.rtclock.get_hi32()) 61 | 62 | # FIXME: rdclycle[h] and rdinstret[h] are not provided yet 63 | 64 | def _parse_csr_ins(self, ins: Instruction) -> Tuple[str, UInt32, int]: 65 | assert len(ins.args) == 3 66 | rd = ins.get_reg(0) 67 | rs = self.regs.get(ins.get_reg(1)).unsigned() 68 | 69 | return rd, rs, self._ins_get_csr_addr(ins) 70 | 71 | def parse_csr_imm_ins(self, ins: Instruction) -> Tuple[str, UInt32, int]: 72 | assert len(ins.args) == 3 73 | rd = ins.get_reg(0) 74 | # make sure we only have 5 bit immediate values here 75 | rs = ins.get_imm(1).abs_value.unsigned() 76 | if rs > 0b11111: 77 | print(f"Warning! more than 5bit immediate value in csr ins {ins}!") 78 | rs = rs & 0b11111 79 | 80 | return rd, rs, self._ins_get_csr_addr(ins) 81 | 82 | @staticmethod 83 | def _ins_get_csr_addr(ins: Instruction) -> int: 84 | # TODO: somehow handle elf instructions at some point? 85 | if isinstance(ins.args[2], str) and ins.args[2].lower() in CSR_NAME_TO_ADDR: 86 | return CSR_NAME_TO_ADDR[ins.args[2].lower()] 87 | 88 | return ins.get_imm(2).abs_value.unsigned_value 89 | -------------------------------------------------------------------------------- /riscemu/core/traps.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from ..colors import FMT_PARSE, FMT_NONE 3 | from .privmodes import PrivModes 4 | from .csr_constants import MCAUSE_TRANSLATION 5 | from .exceptions import RiscemuBaseException 6 | from .int32 import UInt32 7 | from .instruction import InstructionWithEncoding 8 | 9 | 10 | class CpuTrapType(Enum): 11 | TIMER = 1 12 | SOFTWARE = 2 13 | EXTERNAL = 3 14 | EXCEPTION = 4 15 | 16 | 17 | class CpuTrap(BaseException): 18 | code: int 19 | """ 20 | 31-bit value encoding the exception code in the mstatus register 21 | """ 22 | interrupt: int 23 | """ 24 | The isInterrupt bit in the mstatus register 25 | """ 26 | 27 | mtval: UInt32 28 | """ 29 | contents of the mtval register 30 | """ 31 | 32 | type: CpuTrapType 33 | """ 34 | The type (timer, external, software) of the trap 35 | """ 36 | 37 | priv: PrivModes 38 | """ 39 | The privilege level this trap targets 40 | """ 41 | 42 | def __init__( 43 | self, code: int, mtval, type: CpuTrapType, priv: PrivModes = PrivModes.MACHINE 44 | ): 45 | self.interrupt = 0 if type == CpuTrapType.EXCEPTION else 1 46 | self.code = code 47 | self.mtval = UInt32(mtval) 48 | self.priv = priv 49 | self.type = type 50 | 51 | @property 52 | def mcause(self): 53 | return (self.interrupt << 31) + self.code 54 | 55 | def message(self) -> str: 56 | return "" 57 | 58 | def __repr__(self): 59 | name = "Reserved interrupt({}, {})".format(self.interrupt, self.code) 60 | 61 | if (self.interrupt, self.code) in MCAUSE_TRANSLATION: 62 | name = MCAUSE_TRANSLATION[(self.interrupt, self.code)] + "({}, {})".format( 63 | self.interrupt, self.code 64 | ) 65 | 66 | return "{} {{priv={}, type={}, mtval={:x}}} {}".format( 67 | name, self.priv.name, self.type.name, self.mtval, self.message() 68 | ) 69 | 70 | def __str__(self): 71 | return self.__repr__() 72 | 73 | 74 | class IllegalInstructionTrap(CpuTrap): 75 | def __init__(self, ins: InstructionWithEncoding): 76 | super().__init__(2, ins.encoding, CpuTrapType.EXCEPTION) 77 | 78 | 79 | class InstructionAddressMisalignedTrap(CpuTrap): 80 | def __init__(self, addr: int): 81 | super().__init__(0, addr, CpuTrapType.EXCEPTION) 82 | 83 | 84 | class InstructionAccessFault(CpuTrap): 85 | def __init__(self, addr: int): 86 | super().__init__(1, addr, CpuTrapType.EXCEPTION) 87 | 88 | 89 | class TimerInterrupt(CpuTrap): 90 | def __init__(self): 91 | super().__init__(7, 0, CpuTrapType.TIMER) 92 | 93 | 94 | class EcallTrap(CpuTrap): 95 | def __init__(self, mode: PrivModes): 96 | super().__init__(mode.value + 8, 0, CpuTrapType.EXCEPTION) 97 | 98 | 99 | class InvalidElfException(RiscemuBaseException): 100 | def __init__(self, msg: str): 101 | super().__init__() 102 | self.msg = msg 103 | 104 | def message(self): 105 | return ( 106 | FMT_PARSE + '{}("{}")'.format(self.__class__.__name__, self.msg) + FMT_NONE 107 | ) 108 | 109 | 110 | class LoadAccessFault(CpuTrap): 111 | def __init__(self, msg, addr, size, op): 112 | super(LoadAccessFault, self).__init__(5, addr, CpuTrapType.EXCEPTION) 113 | self.msg = msg 114 | self.addr = addr 115 | self.size = size 116 | self.op = op 117 | 118 | def message(self): 119 | return "(During {} at 0x{:08x} of size {}: {})".format( 120 | self.op, self.addr, self.size, self.msg 121 | ) 122 | -------------------------------------------------------------------------------- /test/test_RV32F.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from riscemu.instructions.RV32F import RV32F 4 | from riscemu.core import CPU, Float32, Int32, SimpleInstruction, Registers, BaseFloat 5 | 6 | 7 | def is_close(a0: Union[float, int, BaseFloat], a1: Union[float, int, BaseFloat]): 8 | """ 9 | Compares if two numbers are close to 7 digits. 10 | This should be close enough to catch any real erros but ignore 11 | floating point rounding issues. 12 | """ 13 | diff = abs(float(a0 - a1)) 14 | mag = max(abs(float(a0)), abs(float(a1))) 15 | return (mag / 1e7) > diff 16 | 17 | 18 | class MockInstruction(SimpleInstruction): 19 | ... 20 | 21 | 22 | class MockRegisters(Registers): 23 | ... 24 | 25 | 26 | class MockCPU(CPU): 27 | def __init__(self, flen: int = 32): 28 | self.regs = MockRegisters(True, flen) 29 | 30 | def run(self, verbose: bool = False): 31 | assert False 32 | 33 | def step(self, verbose: bool = False): 34 | assert False 35 | 36 | 37 | def test_fcvt_instructions(): 38 | cpu = MockCPU() 39 | 40 | ins = MockInstruction("fcvt.s.w", ("fa0", "a0"), None, None) 41 | cpu.regs.set("a0", Int32(42)) 42 | RV32F(cpu).instruction_fcvt_s_w(ins) 43 | assert 42.0 == cpu.regs.get_f("fa0") 44 | 45 | ins = MockInstruction("fcvt.w.s", ("a1", "fa1"), None, None) 46 | cpu.regs.set_f("fa1", Float32(42.0)) 47 | RV32F(cpu).instruction_fcvt_w_s(ins) 48 | assert Int32(42) == cpu.regs.get("a1") 49 | 50 | 51 | def test_single_precision_on_flen64(): 52 | cpu = MockCPU(flen=64) 53 | 54 | cpu.regs.set_f("ft0", Float32(100)) 55 | cpu.regs.set_f("ft1", Float32(3)) 56 | # instruction doing ft2 <- ft1 ft2 57 | 58 | ins = MockInstruction("", ("ft2", "ft0", "ft1"), None, None) 59 | 60 | # div 61 | RV32F(cpu).base_fdiv(ins) 62 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 / 3)) 63 | 64 | # multiplication 65 | RV32F(cpu).base_fmul(ins) 66 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 * 3)) 67 | 68 | # fadd 69 | RV32F(cpu).base_fadd(ins) 70 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 + 3)) 71 | 72 | # fsub 73 | RV32F(cpu).base_fsub(ins) 74 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 - 3)) 75 | 76 | # fmin 77 | RV32F(cpu).base_fmin(ins) 78 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), min(100.0, 3)) 79 | 80 | # fmax 81 | RV32F(cpu).base_fmax(ins) 82 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), max(100.0, 3)) 83 | 84 | 85 | def test_single_precision_on_flen32(): 86 | cpu = MockCPU(flen=32) 87 | 88 | cpu.regs.set_f("ft0", Float32(100)) 89 | cpu.regs.set_f("ft1", Float32(3)) 90 | # instruction doing ft2 <- ft1 ft2 91 | 92 | ins = MockInstruction("", ("ft2", "ft0", "ft1"), None, None) 93 | 94 | # div 95 | RV32F(cpu).base_fdiv(ins) 96 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 / 3)) 97 | 98 | # multiplication 99 | RV32F(cpu).base_fmul(ins) 100 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 * 3)) 101 | 102 | # fadd 103 | RV32F(cpu).base_fadd(ins) 104 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 + 3)) 105 | 106 | # fsub 107 | RV32F(cpu).base_fsub(ins) 108 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), (100.0 - 3)) 109 | 110 | # fmin 111 | RV32F(cpu).base_fmin(ins) 112 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), min(100.0, 3)) 113 | 114 | # fmax 115 | RV32F(cpu).base_fmax(ins) 116 | assert is_close(Float32.bitcast(cpu.regs.get_f("ft2")), max(100.0, 3)) 117 | -------------------------------------------------------------------------------- /riscemu/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | 7 | from math import log10, ceil 8 | from typing import Iterable, Iterator, TypeVar, Generic, List, Optional 9 | 10 | from .core import Int32, UInt32 11 | from .core.exceptions import * 12 | 13 | 14 | def align_addr(addr: int, to_bytes: int = 8) -> int: 15 | """ 16 | align an address to `to_bytes` (meaning addr & to_bytes = 0) 17 | 18 | This will increase the address 19 | """ 20 | return addr + (-addr % to_bytes) 21 | 22 | 23 | def parse_numeric_argument(arg: str) -> int: 24 | """ 25 | parse hex or int strings 26 | """ 27 | try: 28 | if arg.lower().startswith("0x"): 29 | return int(arg, 16) 30 | return int(arg) 31 | except ValueError as ex: 32 | raise ParseException( 33 | 'Invalid immediate argument "{}", maybe missing symbol?'.format(arg), 34 | (arg, ex), 35 | ) 36 | 37 | 38 | def create_chunks(my_list, chunk_size): 39 | """Split a list like [a,b,c,d,e,f,g,h,i,j,k,l,m] into e.g. [[a,b,c,d],[e,f,g,h],[i,j,k,l],[m]]""" 40 | return [my_list[i : i + chunk_size] for i in range(0, len(my_list), chunk_size)] 41 | 42 | 43 | def apply_highlight(item, ind, hi_ind): 44 | """ 45 | applies some highlight such as underline to item if ind == hi_ind 46 | """ 47 | if ind == hi_ind: 48 | return FMT_UNDERLINE + FMT_ORANGE + item + FMT_NONE 49 | return item 50 | 51 | 52 | def highlight_in_list(items, hi_ind, joiner=" "): 53 | return joiner.join( 54 | [apply_highlight(item, i, hi_ind) for i, item in enumerate(items)] 55 | ) 56 | 57 | 58 | def format_bytes(byte_arr: bytearray, fmt: str, group: int = 1, highlight: int = -1): 59 | """Format byte array as per fmt. Group into groups of size `group`, and highlight index `highlight`.""" 60 | chunks = create_chunks(byte_arr, group) 61 | if fmt == "hex": 62 | return highlight_in_list(["0x{}".format(ch.hex()) for ch in chunks], highlight) 63 | if fmt == "int": 64 | spc = str(ceil(log10(2 ** (group * 8 - 1))) + 1) 65 | return highlight_in_list( 66 | [("{:0" + spc + "d}").format(Int32(ch)) for ch in chunks], highlight 67 | ) 68 | if fmt == "uint": 69 | spc = str(ceil(log10(2 ** (group * 8)))) 70 | return highlight_in_list( 71 | [("{:0" + spc + "d}").format(UInt32(ch)) for ch in chunks], highlight 72 | ) 73 | if fmt == "char": 74 | return highlight_in_list((repr(chr(b))[1:-1] for b in byte_arr), highlight, "") 75 | 76 | 77 | T = TypeVar("T") 78 | 79 | 80 | class Peekable(Generic[T], Iterator[T]): 81 | def __init__(self, iterable: Iterable[T]): 82 | self.iterable = iter(iterable) 83 | self.cache: List[T] = list() 84 | 85 | def __iter__(self) -> Iterator[T]: 86 | return self 87 | 88 | def __next__(self) -> T: 89 | if self.cache: 90 | return self.cache.pop() 91 | return next(self.iterable) 92 | 93 | def peek(self) -> Optional[T]: 94 | try: 95 | if self.cache: 96 | return self.cache[0] 97 | pop = next(self.iterable) 98 | self.cache.append(pop) 99 | return pop 100 | except StopIteration: 101 | return None 102 | 103 | def push_back(self, item: T): 104 | self.cache = [item] + self.cache 105 | 106 | def is_empty(self) -> bool: 107 | return self.peek() is None 108 | 109 | 110 | def get_section_base_name(section_name: str) -> str: 111 | if "." not in section_name: 112 | print( 113 | FMT_PARSE 114 | + f"Invalid section {section_name}, not starting with a dot!" 115 | + FMT_NONE 116 | ) 117 | return "." + section_name.split(".")[1] 118 | -------------------------------------------------------------------------------- /riscemu/tokenizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | 7 | import re 8 | from dataclasses import dataclass 9 | from enum import Enum, auto 10 | from typing import List, Iterable 11 | 12 | from riscemu.decoder import RISCV_REGS 13 | from riscemu.core.exceptions import ParseException 14 | 15 | LINE_COMMENT_STARTERS = ("#", ";", "//") 16 | WHITESPACE_PATTERN = re.compile(r"\s+") 17 | MEMORY_ADDRESS_PATTERN = re.compile( 18 | r"^(0[xX][A-f0-9]+|\d+|0b[0-1]+|[A-z0-9_-]+)\(([A-z]+[0-9]*)\)$" 19 | ) 20 | REGISTER_NAMES = RISCV_REGS 21 | 22 | 23 | class TokenType(Enum): 24 | COMMA = auto() 25 | ARGUMENT = auto() 26 | PSEUDO_OP = auto() 27 | INSTRUCTION_NAME = auto() 28 | NEWLINE = auto() 29 | LABEL = auto() 30 | 31 | 32 | @dataclass(frozen=True) 33 | class Token: 34 | type: TokenType 35 | value: str 36 | 37 | def __str__(self): 38 | if self.type == TokenType.NEWLINE: 39 | return "\\n" 40 | if self.type == TokenType.COMMA: 41 | return ", " 42 | return "{}({})".format(self.type.name[0:3], self.value) 43 | 44 | 45 | NEWLINE = Token(TokenType.NEWLINE, "\n") 46 | COMMA = Token(TokenType.COMMA, ",") 47 | 48 | 49 | def tokenize(input: Iterable[str]) -> Iterable[Token]: 50 | for line in input: 51 | for line_comment_start in LINE_COMMENT_STARTERS: 52 | if line_comment_start in line: 53 | line = line[: line.index(line_comment_start)] 54 | line.strip(" \t\n") 55 | if not line: 56 | continue 57 | 58 | parts = list(part for part in split_whitespace_respecting_quotes(line) if part) 59 | 60 | yield from parse_line(parts) 61 | yield NEWLINE 62 | 63 | 64 | def parse_line(parts: List[str]) -> Iterable[Token]: 65 | if len(parts) == 0: 66 | return () 67 | first_token = parts[0] 68 | 69 | if first_token[0] == ".": 70 | yield Token(TokenType.PSEUDO_OP, first_token) 71 | elif first_token[-1] == ":": 72 | yield Token(TokenType.LABEL, first_token) 73 | yield from parse_line(parts[1:]) 74 | return 75 | else: 76 | yield Token(TokenType.INSTRUCTION_NAME, first_token) 77 | 78 | for part in parts[1:]: 79 | if part == ",": 80 | yield COMMA 81 | continue 82 | yield from parse_arg(part) 83 | 84 | 85 | def parse_arg(arg: str) -> Iterable[Token]: 86 | comma = arg[-1] == "," 87 | arg = arg[:-1] if comma else arg 88 | mem_match_result = re.match(MEMORY_ADDRESS_PATTERN, arg) 89 | if mem_match_result: 90 | register = mem_match_result.group(2).lower() 91 | immediate = mem_match_result.group(1) 92 | yield Token(TokenType.ARGUMENT, register) 93 | yield Token(TokenType.ARGUMENT, immediate) 94 | else: 95 | yield Token(TokenType.ARGUMENT, arg) 96 | if comma: 97 | yield COMMA 98 | 99 | 100 | def print_tokens(tokens: Iterable[Token]): 101 | for token in tokens: 102 | print(token, end="\n" if token == NEWLINE else "") 103 | print("", flush=True, end="") 104 | 105 | 106 | def split_whitespace_respecting_quotes(line: str) -> Iterable[str]: 107 | quote = "" 108 | part = "" 109 | for c in line: 110 | if c == quote: 111 | yield part 112 | part = "" 113 | quote = "" 114 | continue 115 | 116 | if quote != "": 117 | part += c 118 | continue 119 | 120 | if c in "\"'": 121 | if part: 122 | yield part 123 | quote = c 124 | part = "" 125 | continue 126 | 127 | if c in " \t\n": 128 | if part: 129 | yield part 130 | part = "" 131 | continue 132 | 133 | part += c 134 | 135 | if part: 136 | yield part 137 | -------------------------------------------------------------------------------- /snitch/frep.py: -------------------------------------------------------------------------------- 1 | from typing import List, Type, Union, Set, Literal 2 | 3 | from riscemu.colors import FMT_CPU, FMT_NONE 4 | from riscemu.config import RunConfig 5 | from riscemu.core import UserModeCPU 6 | from riscemu.instructions import InstructionSet, Instruction, RV32F, RV32D 7 | 8 | from dataclasses import dataclass 9 | 10 | from snitch.regs import StreamingRegs 11 | 12 | 13 | @dataclass(frozen=True) 14 | class FrepState: 15 | rep_count: int 16 | ins_count: int 17 | mode: Literal["inner", "outer"] 18 | 19 | 20 | class FrepEnabledCpu(UserModeCPU): 21 | repeat: Union[FrepState, None] 22 | allowed_ins: Set[str] 23 | 24 | def __init__(self, instruction_sets: List[Type["InstructionSet"]], conf: RunConfig): 25 | super().__init__(instruction_sets, conf) 26 | self.regs = StreamingRegs( 27 | mem=self.mmu, infinite_regs=conf.unlimited_registers, flen=conf.flen 28 | ) 29 | self.repeats = None 30 | # only floating point instructions are allowed inside an frep! 31 | self.allowed_ins = set(x for x, y in RV32F(self).get_instructions()) 32 | if conf.flen > 32: 33 | self.allowed_ins.union(x for x, y in RV32D(self).get_instructions()) 34 | 35 | def step(self, verbose: bool = False): 36 | if self.repeats is None: 37 | super().step(verbose=verbose) 38 | return 39 | # get the spec 40 | spec: FrepState = self.repeats 41 | self.repeats = None 42 | 43 | instructions = [ 44 | self.mmu.read_ins(self.pc + i * self.INS_XLEN) 45 | for i in range(spec.ins_count) 46 | ] 47 | 48 | for ins in instructions: 49 | if ins.name not in self.allowed_ins: 50 | # TODO: wrap in a nicer error type 51 | raise RuntimeError( 52 | "Forbidden instruction inside frep loop: {}".format(ins) 53 | ) 54 | 55 | if verbose: 56 | print( 57 | FMT_CPU 58 | + "┌────── floating point repetition ({}) {} times".format( 59 | spec.mode, spec.rep_count + 1 60 | ) 61 | ) 62 | for i, ins in enumerate(instructions): 63 | print( 64 | FMT_CPU 65 | + "│ 0x{:08X}:{} {}".format( 66 | self.pc + i * self.INS_XLEN, FMT_NONE, ins 67 | ) 68 | ) 69 | print(FMT_CPU + "└────── end of floating point repetition" + FMT_NONE) 70 | 71 | pc = self.pc 72 | if spec.mode == "outer": 73 | for _ in range(spec.rep_count + 1): 74 | for ins in instructions: 75 | self.run_instruction(ins) 76 | elif spec.mode == "inner": 77 | for ins in instructions: 78 | for _ in range(spec.rep_count + 1): 79 | self.run_instruction(ins) 80 | else: 81 | raise RuntimeError(f"Unknown frep mode: {spec.mode}") 82 | self.cycle += (spec.rep_count + 1) * spec.ins_count 83 | self.pc = pc + (spec.ins_count * self.INS_XLEN) 84 | 85 | 86 | class Xfrep(InstructionSet): 87 | def instruction_frep_o(self, ins: Instruction): 88 | self.frep(ins, "outer") 89 | 90 | def instruction_frep_i(self, ins: Instruction): 91 | self.frep(ins, "inner") 92 | 93 | def frep(self, ins: Instruction, mode: Literal["inner", "outer"]): 94 | assert isinstance(self.cpu, FrepEnabledCpu) 95 | assert len(ins.args) == 4 96 | assert ins.get_imm(2).abs_value.value == 0, "staggering not supported yet" 97 | assert ins.get_imm(3).abs_value.value == 0, "staggering not supported yet" 98 | 99 | self.cpu.repeats = FrepState( 100 | rep_count=self.regs.get(ins.get_reg(0)).unsigned_value, 101 | ins_count=ins.get_imm(1).abs_value.value, 102 | mode=mode, 103 | ) 104 | -------------------------------------------------------------------------------- /riscemu/instructions/RV32D.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2023 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | This file contains copious amounts of docstrings that were all taken 7 | from https://msyksphinz-self.github.io/riscv-isadoc/html/rvfd.html 8 | (all the docstrings on the instruction methods documenting the opcodes 9 | and their function) 10 | """ 11 | from typing import Tuple 12 | 13 | from .instruction_set import InstructionSet, Instruction 14 | from .float_base import FloatArithBase 15 | from riscemu.core import INS_NOT_IMPLEMENTED, Float32, Int32, UInt32, Float64 16 | 17 | 18 | class RV32D(FloatArithBase[Float64]): 19 | flen = 64 20 | _float_cls = Float64 21 | 22 | def instruction_fcvt_d_w(self, ins: Instruction): 23 | """ 24 | +-----+-----+-----+-----+-----+-----+-----+---+ 25 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 26 | +-----+-----+-----+-----+-----+-----+-----+---+ 27 | |11010|01 |00000|rs1 |rm |rd |10100|11 | 28 | +-----+-----+-----+-----+-----+-----+-----+---+ 29 | 30 | :Format: 31 | | fcvt.d.w rd,rs1 32 | 33 | :Description: 34 | | Converts a 32-bit signed integer, in integer register rs1 into a double-precision floating-point number in floating-point register rd. 35 | 36 | :Implementation: 37 | | x[rd] = sext(s32_{f64}(f[rs1])) 38 | """ 39 | rd, rs = self.parse_rd_rs(ins) 40 | self.regs.set_f(rd, Float64(self.regs.get(rs).value)) 41 | 42 | def instruction_fcvt_d_wu(self, ins: Instruction): 43 | """ 44 | +-----+-----+-----+-----+-----+-----+-----+---+ 45 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 46 | +-----+-----+-----+-----+-----+-----+-----+---+ 47 | |11010|01 |00001|rs1 |rm |rd |10100|11 | 48 | +-----+-----+-----+-----+-----+-----+-----+---+ 49 | 50 | :Format: 51 | | fcvt.d.wu rd,rs1 52 | 53 | :Description: 54 | | Converts a 32-bit unsigned integer, in integer register rs1 into a double-precision floating-point number in floating-point register rd. 55 | 56 | :Implementation: 57 | | f[rd] = f64_{u32}(x[rs1]) 58 | """ 59 | rd, rs = self.parse_rd_rs(ins) 60 | self.regs.set_f(rd, Float64(self.regs.get(rs).unsigned_value)) 61 | 62 | def instruction_fcvt_w_d(self, ins: Instruction): 63 | """ 64 | +-----+-----+-----+-----+-----+-----+-----+---+ 65 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 66 | +-----+-----+-----+-----+-----+-----+-----+---+ 67 | |11000|01 |00000|rs1 |rm |rd |10100|11 | 68 | +-----+-----+-----+-----+-----+-----+-----+---+ 69 | 70 | :Format: 71 | | fcvt.w.d rd,rs1 72 | 73 | :Description: 74 | | Converts a double-precision floating-point number in floating-point register rs1 to a signed 32-bit integer, in integer register rd. 75 | 76 | :Implementation: 77 | | x[rd] = sext(s32_{f64}(f[rs1])) 78 | """ 79 | rd, rs = self.parse_rd_rs(ins) 80 | self.regs.set(rd, Int32.from_float(self.regs.get_f(rs).value)) 81 | 82 | def instruction_fcvt_wu_d(self, ins: Instruction): 83 | """ 84 | +-----+-----+-----+-----+-----+-----+-----+---+ 85 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 86 | +-----+-----+-----+-----+-----+-----+-----+---+ 87 | |11000|01 |00001|rs1 |rm |rd |10100|11 | 88 | +-----+-----+-----+-----+-----+-----+-----+---+ 89 | 90 | :Format: 91 | | fcvt.wu.d rd,rs1 92 | 93 | :Description: 94 | | Converts a double-precision floating-point number in floating-point register rs1 to a unsigned 32-bit integer, in integer register rd. 95 | 96 | :Implementation: 97 | | x[rd] = sext(u32f64(f[rs1])) 98 | """ 99 | rd, rs = self.parse_rd_rs(ins) 100 | self.regs.set(rd, UInt32.from_float(self.regs.get_f(rs).value)) 101 | -------------------------------------------------------------------------------- /examples/estimate-cpu-freq.asm: -------------------------------------------------------------------------------- 1 | .data 2 | // 16 words of data for benchmarking loads/stores 3 | buf0: 4 | .word 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 5 | 6 | .text 7 | .globl main 8 | 9 | main: 10 | mv s5, ra 11 | 12 | // warmup 13 | printf "warmup" 14 | li a0, 1000 15 | jal nop_loop 16 | 17 | la a1, nop_loop 18 | li a0, 1000 19 | printf "Measuring nops:" 20 | jal measure_loop 21 | li a0, 10000 22 | jal measure_loop 23 | 24 | la a1, arith_loop 25 | li a0, 1000 26 | printf "Measuring addi:" 27 | jal measure_loop 28 | li a0, 10000 29 | jal measure_loop 30 | 31 | la a1, memory_loop 32 | la a2, buf0 33 | li a0, 1000 34 | printf "Measuring load/stores:" 35 | jal measure_loop 36 | li a0, 10000 37 | jal measure_loop 38 | 39 | mv ra, s5 40 | ret 41 | 42 | 43 | // rtclock tickrate is 32768 44 | // execute bench at addr a1, a0 times 45 | measure_loop: 46 | mv s4, ra 47 | csrrs s0, zero, cycle 48 | csrrs s2, zero, time 49 | jalr ra, a1, 0 50 | csrrs s1, zero, cycle 51 | csrrs s3, zero, time 52 | sub s0, s1, s0 53 | sub s1, s3, s2 54 | fcvt.s.w ft0, s1 55 | li t1, 32768 // cpu tickrate 56 | fcvt.s.w ft1, t1 57 | fdiv.s ft0, ft0, ft1 // ft0 = seconds of execution time 58 | fcvt.s.w ft1, s0 // ft1 = number of ins executed 59 | fdiv.s ft2, ft1, ft0 // ft2 = ins/second 60 | li t0, 1000 61 | fcvt.s.w ft1, t0 // ft1 = 1k 62 | fdiv.s ft2, ft2, ft1 // ft2 = kins/sec 63 | 64 | printf "executed {} instructions in {:.4f32} seconds ({:.2f32}ki/s)", s0, ft0, ft2 65 | mv ra, s4 66 | ret 67 | 68 | // first loop, executes a0*32 nop + a0 addi + a0 beq instructions (a0 > 1) 69 | nop_loop: 70 | nop 71 | nop 72 | nop 73 | nop 74 | nop 75 | nop 76 | nop 77 | nop 78 | nop 79 | nop 80 | nop 81 | nop 82 | nop 83 | nop 84 | nop 85 | nop 86 | nop 87 | nop 88 | nop 89 | nop 90 | nop 91 | nop 92 | nop 93 | nop 94 | nop 95 | nop 96 | nop 97 | nop 98 | nop 99 | nop 100 | nop 101 | nop 102 | addi a0, a0, -1 103 | blt zero, a0, nop_loop 104 | ret 105 | 106 | // second loop, executes a0*16 load/store pairs + a0 addi + a0 beq instructions (a0 > 1) 107 | memory_loop: 108 | lw t0, 0(a2) 109 | sw t0, 0(a2) 110 | lw t0, 4(a2) 111 | sw t0, 4(a2) 112 | lw t0, 8(a2) 113 | sw t0, 8(a2) 114 | lw t0, 12(a2) 115 | sw t0, 12(a2) 116 | lw t0, 16(a2) 117 | sw t0, 16(a2) 118 | lw t0, 20(a2) 119 | sw t0, 20(a2) 120 | lw t0, 24(a2) 121 | sw t0, 24(a2) 122 | lw t0, 28(a2) 123 | sw t0, 28(a2) 124 | lw t0, 32(a2) 125 | sw t0, 32(a2) 126 | lw t0, 36(a2) 127 | sw t0, 36(a2) 128 | lw t0, 40(a2) 129 | sw t0, 40(a2) 130 | lw t0, 44(a2) 131 | sw t0, 44(a2) 132 | lw t0, 48(a2) 133 | sw t0, 48(a2) 134 | lw t0, 52(a2) 135 | sw t0, 52(a2) 136 | lw t0, 56(a2) 137 | sw t0, 56(a2) 138 | lw t0, 60(a2) 139 | sw t0, 60(a2) 140 | addi a0, a0, -1 141 | bge a0, zero, nop_loop 142 | ret 143 | 144 | // third loop, executes a0*32 addi + a0 addi + a0 beq instructions (a0 > 1) 145 | arith_loop: 146 | addi t0, a0, 1234 147 | addi t0, a0, 1234 148 | addi t0, a0, 1234 149 | addi t0, a0, 1234 150 | addi t0, a0, 1234 151 | addi t0, a0, 1234 152 | addi t0, a0, 1234 153 | addi t0, a0, 1234 154 | addi t0, a0, 1234 155 | addi t0, a0, 1234 156 | addi t0, a0, 1234 157 | addi t0, a0, 1234 158 | addi t0, a0, 1234 159 | addi t0, a0, 1234 160 | addi t0, a0, 1234 161 | addi t0, a0, 1234 162 | addi t0, a0, 1234 163 | addi t0, a0, 1234 164 | addi t0, a0, 1234 165 | addi t0, a0, 1234 166 | addi t0, a0, 1234 167 | addi t0, a0, 1234 168 | addi t0, a0, 1234 169 | addi t0, a0, 1234 170 | addi t0, a0, 1234 171 | addi t0, a0, 1234 172 | addi t0, a0, 1234 173 | addi t0, a0, 1234 174 | addi t0, a0, 1234 175 | addi t0, a0, 1234 176 | addi t0, a0, 1234 177 | addi t0, a0, 1234 178 | addi a0, a0, -1 179 | blt zero, a0, nop_loop 180 | ret 181 | -------------------------------------------------------------------------------- /riscemu/core/program.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Set 2 | 3 | from ..colors import FMT_RED, FMT_BOLD, FMT_NONE, FMT_MEM 4 | from ..helpers import get_section_base_name 5 | from . import InstructionContext, T_AbsoluteAddress, MemorySection 6 | 7 | 8 | class Program: 9 | """ 10 | This represents a collection of sections which together form an executable program 11 | 12 | When you want to create a program which can be located anywhere in memory, set base to None, 13 | this signals the other components, that this is relocatable. Set the base of each section to 14 | the offset in the program, and everything will be taken care of for you. 15 | 16 | """ 17 | 18 | name: str 19 | context: InstructionContext 20 | global_labels: Set[str] 21 | relative_labels: Set[str] 22 | sections: List[MemorySection] 23 | base: Optional[T_AbsoluteAddress] 24 | is_loaded: bool 25 | 26 | @property 27 | def size(self): 28 | if len(self.sections) == 0: 29 | return 0 30 | if self.base is None: 31 | return self.sections[-1].base + self.sections[-1].size 32 | return (self.sections[-1].base - self.base) + self.sections[-1].size 33 | 34 | def __init__(self, name: str, base: Optional[int] = None): 35 | self.name = name 36 | self.context = InstructionContext() 37 | self.sections = [] 38 | self.global_labels = set() 39 | self.relative_labels = set() 40 | self.base = base 41 | self.is_loaded = False 42 | 43 | def add_section(self, sec: MemorySection): 44 | # print a warning when a section is located before the programs base 45 | if self.base is not None: 46 | if sec.base < self.base: 47 | print( 48 | FMT_RED 49 | + FMT_BOLD 50 | + "WARNING: memory section {} in {} is placed before program base (0x{:x})".format( 51 | sec, self.name, self.base 52 | ) 53 | + FMT_NONE 54 | ) 55 | 56 | self.sections.append(sec) 57 | # keep section list ordered 58 | self.sections.sort(key=lambda section: section.base) 59 | 60 | def __repr__(self): 61 | return "{}(name={},sections={},base={})".format( 62 | self.__class__.__name__, 63 | self.name, 64 | self.global_labels, 65 | [s.name for s in self.sections], 66 | self.base, 67 | ) 68 | 69 | @property 70 | def entrypoint(self): 71 | if "_start" in self.context.labels: 72 | return self.context.labels.get("_start") 73 | if "main" in self.context.labels: 74 | return self.context.labels.get("main") 75 | for sec in self.sections: 76 | if get_section_base_name(sec.name) == ".text" and sec.flags.executable: 77 | return sec.base 78 | 79 | def loaded_trigger(self, at_addr: T_AbsoluteAddress): 80 | """ 81 | This trigger is called when the binary is loaded and its final address in memory is determined 82 | 83 | This will do a small sanity check to prevent programs loading twice, or at addresses they don't 84 | expect to be loaded. 85 | 86 | Then it will finalize all relative symbols defined in it to point to the correct addresses. 87 | 88 | :param at_addr: the address where the program will be located 89 | """ 90 | if self.is_loaded: 91 | if at_addr != self.base: 92 | raise RuntimeError( 93 | "Program loaded twice at different addresses! This will probably break things!" 94 | ) 95 | return 96 | 97 | if self.base is not None and self.base != at_addr: 98 | print( 99 | FMT_MEM 100 | + "WARNING: Program loaded at different address then expected! (loaded at {}, " 101 | "but expects to be loaded at {})".format(at_addr, self.base) + FMT_NONE 102 | ) 103 | 104 | # check if we are relocating 105 | if self.base != at_addr: 106 | offset = at_addr if self.base is None else at_addr - self.base 107 | 108 | # move all sections by the offset 109 | for sec in self.sections: 110 | sec.base += offset 111 | 112 | # move all relative symbols by the offset 113 | for name in self.relative_labels: 114 | self.context.labels[name] += offset 115 | 116 | self.base = at_addr 117 | self.context.base_address = at_addr 118 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.2.7 4 | 5 | - BugFix: Fix `malloc` implementation from being just wrong to being right (I think?) 6 | - BugFix: Fix `MMU.translate_address` to actually return the best match (wow!) 7 | - Feature: The instruction trace now contains register and symbol values starting at verbosity level 3 8 | - BugFix: RVDebug got better at finding out if a float or int register was meant 9 | 10 | ## 2.2.6 11 | 12 | - Feature: Canonicalize register names when parsing, converting e.g. `x0 -> zero` or `fp -> s0`. 13 | - Feature: Added support for `fcvt.d.w[u]` and `fcvt.w[u].d` instructions 14 | - BugFix: Fixed that registers were treated as UInt32s instead of Int32 (this may have caused subtle bugs before) 15 | - Feature: Added the remainder of the `M` extension 16 | - BugFix: Fixed a bug in the overflow behavior of `mulh` 17 | - BugFix: Fix faulty length assertion in `jalr` 18 | 19 | ## 2.2.5 20 | 21 | - BugFix: Fix missed import in core.simple_instruction 22 | 23 | ## 2.2.4 24 | 25 | - BugFix: Found and added some missing floating point registers (`ft8` to `ft11`) 26 | - Feature: Add frep support to the snitch emulation 27 | - Feature: Add support for 64-bit floats to the snitch Xssr emulation 28 | 29 | ## 2.2.3 30 | 31 | - Feature: Adding support for 64 bit floating point operations 32 | - BugFix: Fix a bug where `-o libc` would fail with packaged versions of riscemu 33 | - BugFix: Fix `__all__` to now properly work (use name strings instead of values) 34 | 35 | ## 2.2.2 36 | 37 | - Dev: Add `__all__` to `riscemu.{core,instructions,decoder}` modules to make pyright in other projects happy 38 | - Perf: very minor fix related to not converting values twice when loaded from memory 39 | 40 | ## 2.2.1 41 | 42 | Version bump to re-trigger CI run. 43 | 44 | ## 2.2.0 45 | 46 | - Feature: Added Zicsr extension and with that support for CSRs 47 | - Feature: Starting to add support for Snitch architecture (Xssr) 48 | - Feature: Add support for `.p2align` assembler directive 49 | - Rework: Improve handling of immediates, so that `beq a0, a1, 1b` and `beq a0, a1, -16` can both can be handled correctly. 50 | - BugFix: Fix some more errors in the RV32F implementation 51 | - Dev: Move to poetry for project development environment 52 | - Dev: Module refactoring, core datastructures now mostly live inside riscemu.core 53 | - Perf: Improved performance by around 1.8x 54 | 55 | ## 2.1.1 56 | 57 | - Bugfix: Fix some errors in the RV32F implementation (thanks @adutilleul) 58 | - Bugfix: Fix how floats are printed in the register view (thanks @KGrykiel) 59 | - Bugfix: Fix missing support for infinite registers in load/store ins (thanks @superlopuh) 60 | 61 | ## 2.1.0 62 | 63 | - Added a very basic libc containing a `crt0.s`, and a few functions 64 | such as `malloc`, `rand`, and `memcpy`. 65 | - Added a subset of the `mmap2` syscall (code 192) to allocate new memory 66 | - Refactored the launching code to improve using riscemu from code 67 | - Added an option to start with the provided libc: `-o libc` 68 | - Added floating point support (enabled by default). The RV32F extension is now available 69 | 70 | ## 2.0.5 71 | 72 | - Added unlimited register mode with `-o unlimited_regs` 73 | 74 | ## 2.0.4 75 | 76 | - Bugfix: fix a sign issue in instruction parsing for `rd, rs, rs` format 77 | - Bugfix: respect `conf.debug_instruction` setting 78 | 79 | ## 2.0.3 - 2022-04-18 80 | 81 | - Syscalls: cleaned up formatting and added instructions for extensions 82 | - Parser: fixed error when labels where used outside of sections 83 | - Cleaned up and improved memory dumping code 84 | - Fixed a bug with hex literal recognition in instruction parse code 85 | - Fixed bug where wrong parts of section would be printed in mmu.dump() 86 | - Removed tests for bind_twos_complement as the function is now redundant with the introduction of Int32 87 | - Fixed address translation error for sections without symbols 88 | - Changed verbosity level at which start and end of CPU are printed, added prints for start and stack loading 89 | 90 | ## 2.0.2 91 | 92 | - Added implicit declaration of .text section when a file starts with assembly instructions without declaring a section first 93 | - Fixed a regression where the cpu's exit code would no longer be the exit code of the emulator. Now the emulator exits with the cpu's exit code 94 | - Added the changelog 95 | 96 | ## 2.0.1 97 | 98 | - Fixed type annotations in parser code that prevented running unprivileged code 99 | 100 | ## 2.0.0 101 | 102 | - Correct handling of 32 bit overflows and underflows 103 | - Complete revamp of internal data structures 104 | - Completely reworked how assembly is parsed 105 | -------------------------------------------------------------------------------- /riscemu/priv/ElfLoader.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from io import IOBase, RawIOBase 3 | from typing import List 4 | 5 | from core.traps import * 6 | from .types import ElfMemorySection 7 | from ..helpers import FMT_PARSE, FMT_NONE, FMT_GREEN, FMT_BOLD 8 | from ..core import MemoryFlags, Program, ProgramLoader, T_ParserOpts 9 | 10 | FMT_ELF = FMT_GREEN + FMT_BOLD 11 | 12 | if typing.TYPE_CHECKING: 13 | from elftools.elf.elffile import ELFFile 14 | from elftools.elf.sections import Section, SymbolTableSection 15 | 16 | INCLUDE_SEC = (".text", ".stack", ".bss", ".sdata", ".sbss") 17 | 18 | 19 | class ElfBinaryFileLoader(ProgramLoader): 20 | """ 21 | Loads compiled elf binaries (checks for the magic sequence 7f45 4c46) 22 | 23 | This loader respects local and global symbols. 24 | """ 25 | 26 | is_binary = True 27 | 28 | program: Program 29 | 30 | def __post_init__(self): 31 | self.program = Program(self.filename) 32 | 33 | @classmethod 34 | def can_parse(cls, source_name: str) -> float: 35 | if source_name == "-" or not os.path.isfile(source_name): 36 | return 0 37 | 38 | with open(source_name, "rb") as f: 39 | if f.read(4) == b"\x7f\x45\x4c\x46": 40 | return 1 41 | return 0 42 | 43 | @classmethod 44 | def get_options(cls, argv: List[str]) -> [List[str], T_ParserOpts]: 45 | return argv, {} 46 | 47 | def parse(self) -> Program: 48 | try: 49 | from elftools.elf.elffile import ELFFile 50 | from elftools.elf.sections import Section, SymbolTableSection 51 | 52 | self._read_elf(ELFFile(self.source)) 53 | except ImportError as e: 54 | print( 55 | FMT_PARSE 56 | + "[ElfLoader] Cannot load elf files without PyElfTools package! You can install them " 57 | "using pip install pyelftools!" + FMT_NONE 58 | ) 59 | raise e 60 | 61 | return self.program 62 | 63 | def _read_elf(self, elf: "ELFFile"): 64 | if not elf.header.e_machine == "EM_RISCV": 65 | raise InvalidElfException("Not a RISC-V elf file!") 66 | if not elf.header.e_ident.EI_CLASS == "ELFCLASS32": 67 | raise InvalidElfException("Only 32bit executables are supported!") 68 | 69 | from elftools.elf.sections import SymbolTableSection 70 | 71 | for sec in elf.iter_sections(): 72 | if isinstance(sec, SymbolTableSection): 73 | self._parse_symtab(sec) 74 | continue 75 | 76 | if sec.name not in INCLUDE_SEC: 77 | continue 78 | 79 | self._add_sec(self._lms_from_elf_sec(sec, self.filename)) 80 | 81 | def _lms_from_elf_sec(self, sec: "Section", owner: str): 82 | is_code = sec.name in (".text",) 83 | data = bytearray(sec.data()) 84 | if len(data) < sec.data_size: 85 | data += bytearray(len(data) - sec.data_size) 86 | flags = MemoryFlags(is_code, is_code) 87 | print( 88 | FMT_ELF 89 | + "[ElfLoader] Section {} at: {:X}".format(sec.name, sec.header.sh_addr) 90 | + FMT_NONE 91 | ) 92 | return ElfMemorySection( 93 | data, sec.name, self.program.context, owner, sec.header.sh_addr, flags 94 | ) 95 | 96 | def _parse_symtab(self, symtab: "SymbolTableSection"): 97 | for sym in symtab.iter_symbols(): 98 | if not sym.name: 99 | continue 100 | self.program.context.labels[sym.name] = sym.entry.st_value 101 | # check if it has st_visibility bit set 102 | if sym.entry.st_info.bind == "STB_GLOBAL": 103 | self.program.global_labels.add(sym.name) 104 | print( 105 | FMT_PARSE 106 | + "LOADED GLOBAL SYMBOL {}: {}".format(sym.name, sym.entry.st_value) 107 | + FMT_NONE 108 | ) 109 | 110 | def _add_sec(self, new_sec: "ElfMemorySection"): 111 | for sec in self.program.sections: 112 | if sec.base < sec.end <= new_sec.base or sec.end > sec.base >= new_sec.end: 113 | continue 114 | else: 115 | print( 116 | FMT_ELF 117 | + "[ElfLoader] Invalid elf layout: Two sections overlap: \n\t{}\n\t{}".format( 118 | sec, new_sec 119 | ) 120 | + FMT_NONE 121 | ) 122 | raise RuntimeError("Cannot load elf with overlapping sections!") 123 | 124 | self.program.add_section(new_sec) 125 | -------------------------------------------------------------------------------- /riscemu/instructions/instruction_set.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | 7 | from typing import Tuple, Callable, Dict, Union, Iterable 8 | 9 | from abc import ABC 10 | 11 | from ..core.exceptions import ASSERT_LEN 12 | from ..core import Instruction, Int32, UInt32, Immediate, CPU, Registers 13 | 14 | 15 | class InstructionSet(ABC): 16 | """ 17 | Represents a collection of instructions 18 | 19 | Each instruction set has to inherit from this class. Each instruction should be it's own method: 20 | 21 | instruction_(self, ins: LoadedInstruction) 22 | 23 | instructions containing a dot '.' should replace it with an underscore. 24 | """ 25 | 26 | def __init__(self, cpu: "CPU"): 27 | """Create a new instance of the Instruction set. This requires access to a CPU, and grabs vertain things 28 | from it such as access to the MMU and registers. 29 | """ 30 | self.name = self.__class__.__name__ 31 | self.cpu = cpu 32 | 33 | def load(self) -> Dict[str, Callable[["Instruction"], None]]: 34 | """ 35 | This is called by the CPU once it instantiates this instruction set 36 | 37 | It returns a dictionary of all instructions in this instruction set, 38 | pointing to the correct handler for it 39 | """ 40 | return {name: ins for name, ins in self.get_instructions()} 41 | 42 | def get_instructions(self) -> Iterable[Tuple[str, Callable[[Instruction], None]]]: 43 | """ 44 | Returns a list of all valid instruction names included in this instruction set 45 | 46 | converts underscores in names to dots 47 | """ 48 | for member in dir(self): 49 | if member.startswith("instruction_"): 50 | yield member[12:].replace("_", "."), getattr(self, member) 51 | 52 | def parse_mem_ins(self, ins: "Instruction") -> Tuple[str, UInt32]: 53 | """ 54 | parses rd, imm(rs) argument format and returns (rd, imm+rs1) 55 | (so a register and address tuple for memory instructions) 56 | """ 57 | assert len(ins.args) == 3 58 | # handle rd, rs1, imm 59 | rd = ins.get_reg(0) 60 | rs = self.regs.get(ins.get_reg(1)).unsigned() 61 | imm = ins.get_imm(2) 62 | return rd, rs + imm.abs_value.unsigned() 63 | 64 | def parse_rd_rs_rs( 65 | self, ins: "Instruction", signed=True 66 | ) -> Tuple[str, Int32, Int32]: 67 | """ 68 | Assumes the command is in rd, rs1, rs2 format 69 | Returns the name of rd, and the values in rs1 and rs2 70 | """ 71 | ASSERT_LEN(ins.args, 3) 72 | if signed: 73 | return ( 74 | ins.get_reg(0), 75 | Int32(self.regs.get(ins.get_reg(1))), 76 | Int32(self.regs.get(ins.get_reg(2))), 77 | ) 78 | else: 79 | return ( 80 | ins.get_reg(0), 81 | UInt32(self.regs.get(ins.get_reg(1))), 82 | UInt32(self.regs.get(ins.get_reg(2))), 83 | ) 84 | 85 | def parse_rd_rs_imm(self, ins: "Instruction") -> Tuple[str, Int32, Immediate]: 86 | """ 87 | Assumes the command is in rd, rs, imm format 88 | Returns the name of rd, the value in rs and the immediate imm 89 | """ 90 | return ( 91 | ins.get_reg(0), 92 | Int32(self.regs.get(ins.get_reg(1))), 93 | ins.get_imm(2), 94 | ) 95 | 96 | def parse_rs_rs_imm(self, ins: "Instruction") -> Tuple[Int32, Int32, Immediate]: 97 | """ 98 | Assumes the command is in rs1, rs2, imm format 99 | Returns the values in rs1, rs2 and the immediate imm 100 | """ 101 | return ( 102 | Int32(self.regs.get(ins.get_reg(0))), 103 | Int32(self.regs.get(ins.get_reg(1))), 104 | ins.get_imm(2), 105 | ) 106 | 107 | def get_reg_content(self, ins: "Instruction", ind: int) -> Int32: 108 | """ 109 | get the register name from ins and then return the register contents 110 | """ 111 | return self.regs.get(ins.get_reg(ind)) 112 | 113 | @property 114 | def pc(self) -> int: 115 | """ 116 | shorthand for self.cpu.pc 117 | """ 118 | return self.cpu.pc 119 | 120 | @pc.setter 121 | def pc(self, val: Union[int, Int32]): 122 | if isinstance(val, Int32): 123 | val = val.unsigned_value 124 | self.cpu.pc = val 125 | 126 | @property 127 | def mmu(self): 128 | return self.cpu.mmu 129 | 130 | @property 131 | def regs(self) -> Registers: 132 | return self.cpu.regs 133 | 134 | def __repr__(self): 135 | return "InstructionSet[{}] with {} instructions".format( 136 | self.__class__.__name__, len(list(self.get_instructions())) 137 | ) 138 | -------------------------------------------------------------------------------- /riscemu/priv/CSR.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union, Callable, Optional 2 | from collections import defaultdict 3 | from core.privmodes import PrivModes 4 | from core.traps import InstructionAccessFault 5 | from ..colors import FMT_CSR, FMT_NONE 6 | 7 | from core.csr_constants import CSR_NAME_TO_ADDR, MSTATUS_LEN_2, MSTATUS_OFFSETS 8 | from ..core import UInt32 9 | 10 | 11 | class CSR: 12 | """ 13 | This holds all Control and Status Registers (CSR) 14 | """ 15 | 16 | regs: Dict[int, UInt32] 17 | """ 18 | All Control and Status Registers are stored here 19 | """ 20 | 21 | virtual_regs: Dict[int, Callable[[], UInt32]] 22 | """ 23 | list of virtual CSR registers, with values computed on read 24 | """ 25 | 26 | listeners: Dict[int, Callable[[UInt32, UInt32], None]] 27 | 28 | mstatus_cache: Dict[str, UInt32] 29 | mstatus_cache_dirty = True 30 | 31 | def __init__(self): 32 | self.regs = defaultdict(lambda: UInt32(0)) 33 | self.listeners = defaultdict(lambda: (lambda x, y: None)) 34 | self.virtual_regs = dict() 35 | self.mstatus_cache = dict() 36 | # TODO: implement write masks (bitmasks which control writeable bits in registers 37 | 38 | def set(self, addr: Union[str, int], val: Union[int, UInt32]): 39 | addr = self._name_to_addr(addr) 40 | if addr is None: 41 | return 42 | val = UInt32(val) 43 | self.listeners[addr](self.regs[addr], val) 44 | if addr == 0x300: 45 | self.mstatus_cache_dirty = True 46 | self.regs[addr] = val 47 | 48 | def get(self, addr: Union[str, int]) -> UInt32: 49 | addr = self._name_to_addr(addr) 50 | if addr is None: 51 | raise RuntimeError(f"Invalid CSR name: {addr}!") 52 | if addr in self.virtual_regs: 53 | return self.virtual_regs[addr]() 54 | return self.regs[addr] 55 | 56 | def set_listener( 57 | self, addr: Union[str, int], listener: Callable[[UInt32, UInt32], None] 58 | ): 59 | addr = self._name_to_addr(addr) 60 | if addr is None: 61 | print("unknown csr address name: {}".format(addr)) 62 | return 63 | self.listeners[addr] = listener 64 | 65 | # mstatus properties 66 | def set_mstatus(self, name: str, val: UInt32): 67 | """ 68 | Set mstatus bits using this helper. mstatus is a 32 bit register, holding various machine status flags 69 | Setting them by hand is super painful, so this helper allows you to set specific bits. 70 | 71 | Please make sure your supplied value has the correct width! 72 | 73 | :param name: 74 | :param val: 75 | :return: 76 | """ 77 | size = 2 if name in MSTATUS_LEN_2 else 1 78 | off = MSTATUS_OFFSETS[name] 79 | mask = (2**size - 1) << off 80 | old_val = self.get("mstatus") 81 | erased = old_val & (~mask) 82 | new_val = erased | (val << off) 83 | self.set("mstatus", new_val) 84 | 85 | def get_mstatus(self, name) -> UInt32: 86 | if not self.mstatus_cache_dirty and name in self.mstatus_cache: 87 | return self.mstatus_cache[name] 88 | 89 | size = 2 if name in MSTATUS_LEN_2 else 1 90 | off = MSTATUS_OFFSETS[name] 91 | mask = (2**size - 1) << off 92 | val = (self.get("mstatus") & mask) >> off 93 | if self.mstatus_cache_dirty: 94 | self.mstatus_cache = dict(name=val) 95 | else: 96 | self.mstatus_cache[name] = val 97 | return val 98 | 99 | def callback(self, addr: Union[str, int]): 100 | def inner(func: Callable[[UInt32, UInt32], None]): 101 | self.set_listener(addr, func) 102 | return func 103 | 104 | return inner 105 | 106 | def assert_can_read(self, mode: PrivModes, addr: int): 107 | if (addr >> 8) & 3 > mode.value: 108 | raise InstructionAccessFault(addr) 109 | 110 | def assert_can_write(self, mode: PrivModes, addr: int): 111 | if (addr >> 8) & 3 > mode.value or addr >> 10 == 11: 112 | raise InstructionAccessFault(addr) 113 | 114 | def _name_to_addr(self, addr: Union[str, int]) -> Optional[int]: 115 | if isinstance(addr, str): 116 | if addr not in CSR_NAME_TO_ADDR: 117 | print("Unknown CSR register {}".format(addr)) 118 | return None 119 | return CSR_NAME_TO_ADDR[addr] 120 | return addr 121 | 122 | def virtual_register(self, addr: Union[str, int]): 123 | addr = self._name_to_addr(addr) 124 | if addr is None: 125 | print("unknown csr address name: {}".format(addr)) 126 | 127 | def inner(func: Callable[[], UInt32]): 128 | self.virtual_regs[addr] = func 129 | return func 130 | 131 | return inner 132 | 133 | def dump_mstatus(self): 134 | print(FMT_CSR + "[CSR] dumping mstatus:") 135 | i = 0 136 | for name in MSTATUS_OFFSETS: 137 | print(" {:<5} {}".format(name, self.get_mstatus(name)), end="") 138 | if i % 6 == 5: 139 | print() 140 | i += 1 141 | print(FMT_NONE) 142 | -------------------------------------------------------------------------------- /test/test_tokenizer.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from riscemu.tokenizer import ( 4 | tokenize, 5 | print_tokens, 6 | Token, 7 | TokenType, 8 | NEWLINE, 9 | COMMA, 10 | split_whitespace_respecting_quotes, 11 | ) 12 | 13 | 14 | def ins(name: str) -> Token: 15 | return Token(TokenType.INSTRUCTION_NAME, name) 16 | 17 | 18 | def arg(name: str) -> Token: 19 | return Token(TokenType.ARGUMENT, name) 20 | 21 | 22 | def op(name: str) -> Token: 23 | return Token(TokenType.PSEUDO_OP, name) 24 | 25 | 26 | def lbl(name: str) -> Token: 27 | return Token(TokenType.LABEL, name) 28 | 29 | 30 | class TestTokenizer(TestCase): 31 | def test_instructions(self): 32 | program = ["li a0, 144", "divi a0, a0, 12", "xori a1, a0, 12"] 33 | tokens = [ 34 | ins("li"), 35 | arg("a0"), 36 | COMMA, 37 | arg("144"), 38 | NEWLINE, 39 | ins("divi"), 40 | arg("a0"), 41 | COMMA, 42 | arg("a0"), 43 | COMMA, 44 | arg("12"), 45 | NEWLINE, 46 | ins("xori"), 47 | arg("a1"), 48 | COMMA, 49 | arg("a0"), 50 | COMMA, 51 | arg("12"), 52 | NEWLINE, 53 | ] 54 | self.assertEqual(list(tokenize(program)), tokens) 55 | 56 | def test_comments(self): 57 | parsed_res = [ins("li"), arg("a0"), COMMA, arg("144"), NEWLINE] 58 | for c in ("#", "//", ";"): 59 | lines = [c + " this is a comment", "li a0, 144"] 60 | self.assertEqual(list(tokenize(lines)), parsed_res) 61 | 62 | def test_pseudo_ins(self): 63 | parsed_res = [ 64 | Token(TokenType.PSEUDO_OP, ".section"), 65 | Token(TokenType.ARGUMENT, ".text"), 66 | NEWLINE, 67 | Token(TokenType.PSEUDO_OP, ".type"), 68 | Token(TokenType.ARGUMENT, "init"), 69 | COMMA, 70 | Token(TokenType.ARGUMENT, "@function"), 71 | NEWLINE, 72 | ] 73 | input_program = [".section .text", ".type init, @function"] 74 | self.assertEqual(list(tokenize(input_program)), parsed_res) 75 | 76 | def test_full_program(self): 77 | program = """ 78 | # a hashtag comment 79 | 80 | ; semicolon comment followed by an empty line 81 | .section .text 82 | // double slash comment 83 | addi sp, sp, -32 84 | sw s0, 0(ra) 85 | section: 86 | sub s0, s0, s0 87 | """ 88 | tokens = [ 89 | op(".section"), 90 | arg(".text"), 91 | NEWLINE, 92 | ins("addi"), 93 | arg("sp"), 94 | COMMA, 95 | arg("sp"), 96 | COMMA, 97 | arg("-32"), 98 | NEWLINE, 99 | ins("sw"), 100 | arg("s0"), 101 | COMMA, 102 | arg("ra"), 103 | arg("0"), 104 | NEWLINE, 105 | lbl("section:"), 106 | NEWLINE, 107 | ins("sub"), 108 | arg("s0"), 109 | COMMA, 110 | arg("s0"), 111 | COMMA, 112 | arg("s0"), 113 | NEWLINE, 114 | ] 115 | 116 | self.assertEqual(list(tokenize(program.splitlines())), tokens) 117 | 118 | def test_split_whitespace_respecting_quotes_single(self): 119 | self.assertEqual(list(split_whitespace_respecting_quotes("test")), ["test"]) 120 | 121 | def test_split_whitespace_respecting_quotes_empty(self): 122 | self.assertEqual(list(split_whitespace_respecting_quotes("")), []) 123 | 124 | def test_split_whitespace_respecting_quotes_two_parts(self): 125 | self.assertEqual( 126 | list(split_whitespace_respecting_quotes("test 123")), ["test", "123"] 127 | ) 128 | 129 | def test_split_whitespace_respecting_quotes_whole_quoted(self): 130 | self.assertEqual( 131 | list(split_whitespace_respecting_quotes("'test 123'")), ["test 123"] 132 | ) 133 | 134 | def test_split_whitespace_respecting_quotes_double_quotes(self): 135 | self.assertEqual( 136 | list(split_whitespace_respecting_quotes('"test 123"')), ["test 123"] 137 | ) 138 | 139 | def test_split_whitespace_respecting_quotes_quoted_then_normal(self): 140 | self.assertEqual( 141 | list(split_whitespace_respecting_quotes('"test 123" abc')), 142 | ["test 123", "abc"], 143 | ) 144 | 145 | def test_split_whitespace_respecting_quotes_quoted_sorrounded(self): 146 | self.assertEqual( 147 | list(split_whitespace_respecting_quotes('hello "test 123" abc')), 148 | ["hello", "test 123", "abc"], 149 | ) 150 | 151 | def test_split_whitespace_respecting_quotes_weird_spaces(self): 152 | self.assertEqual( 153 | list(split_whitespace_respecting_quotes('hello "test 123"\tabc')), 154 | ["hello", "test 123", "abc"], 155 | ) 156 | 157 | def test_split_whitespace_respecting_quotes_quotes_no_spaces(self): 158 | self.assertEqual( 159 | list(split_whitespace_respecting_quotes('hello"test 123"abc')), 160 | ["hello", "test 123", "abc"], 161 | ) 162 | -------------------------------------------------------------------------------- /riscemu/core/cpu.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Type, Callable, Set, Dict, TYPE_CHECKING 3 | 4 | from ..config import RunConfig 5 | from ..colors import FMT_NONE, FMT_CPU 6 | from . import ( 7 | T_AbsoluteAddress, 8 | Instruction, 9 | Program, 10 | PrivModes, 11 | MMU, 12 | UInt32, 13 | Registers, 14 | CSR, 15 | RTClock, 16 | csr_constants, 17 | ) 18 | 19 | if TYPE_CHECKING: 20 | # from core.mmu import MMU 21 | from ..instructions import InstructionSet 22 | 23 | 24 | class CPU(ABC): 25 | # static cpu configuration 26 | INS_XLEN: int = 4 27 | 28 | # housekeeping variables 29 | regs: Registers 30 | mmu: "MMU" 31 | pc: T_AbsoluteAddress 32 | cycle: int 33 | halted: bool 34 | 35 | # debugging context 36 | debugger_active: bool 37 | 38 | # instruction information 39 | instructions: Dict[str, Callable[[Instruction], None]] 40 | instruction_sets: Set["InstructionSet"] 41 | 42 | # configuration 43 | conf: RunConfig 44 | 45 | # control and status things: 46 | hart_id: int 47 | mode: PrivModes 48 | csr: CSR 49 | rtclock: RTClock 50 | 51 | def __init__( 52 | self, 53 | mmu: "MMU", 54 | instruction_sets: List[Type["InstructionSet"]], 55 | conf: RunConfig, 56 | ): 57 | self.mmu = mmu 58 | self.regs = Registers(conf.unlimited_registers, conf.flen) 59 | self.conf = conf 60 | 61 | self.instruction_sets = set() 62 | self.instructions = dict() 63 | 64 | for set_class in instruction_sets: 65 | ins_set = set_class(self) 66 | self.instructions.update(ins_set.load()) 67 | self.instruction_sets.add(ins_set) 68 | 69 | self.halted = False 70 | self.cycle = 0 71 | self.pc = 0 72 | self.hart_id = 0 73 | self.debugger_active = False 74 | self.csr = CSR() 75 | self.rtclock = RTClock(conf.rtclock_tickrate) 76 | 77 | def run_instruction(self, ins: Instruction): 78 | """ 79 | Execute a single instruction 80 | 81 | :param ins: The instruction to execute 82 | """ 83 | try: 84 | self.instructions[ins.name](ins) 85 | except KeyError as ex: 86 | raise RuntimeError("Unknown instruction: {}".format(ins)) from ex 87 | 88 | def load_program(self, program: Program): 89 | self.mmu.load_program(program) 90 | 91 | def __repr__(self): 92 | """ 93 | Returns a representation of the CPU and some of its state. 94 | """ 95 | return "{}(pc=0x{:08X}, cycle={}, halted={} instructions={})".format( 96 | self.__class__.__name__, 97 | self.pc, 98 | self.cycle, 99 | self.halted, 100 | " ".join(s.name for s in self.instruction_sets), 101 | ) 102 | 103 | @abstractmethod 104 | def step(self, verbose: bool = False): 105 | pass 106 | 107 | @abstractmethod 108 | def run(self, verbose: bool = False): 109 | pass 110 | 111 | def initialize_registers(self): 112 | # set a0 to the hartid 113 | self.regs.set("a0", UInt32(self.hart_id)) 114 | 115 | def launch(self, verbose: bool = False): 116 | entrypoint = self.mmu.find_entrypoint() 117 | 118 | if entrypoint is None: 119 | entrypoint = self.mmu.programs[0].entrypoint 120 | 121 | self.initialize_registers() 122 | self.setup_csr() 123 | 124 | if self.conf.verbosity > 0: 125 | print( 126 | FMT_CPU 127 | + "[CPU] Started running from {}".format( 128 | self.mmu.translate_address(entrypoint) 129 | ) 130 | + FMT_NONE 131 | ) 132 | self.pc = entrypoint 133 | self.run(verbose) 134 | 135 | @property 136 | def sections(self): 137 | return self.mmu.sections 138 | 139 | @property 140 | def programs(self): 141 | return self.mmu.programs 142 | 143 | def setup_csr(self): 144 | """ 145 | Set up standard CSR registers, can be hooked into when subclassing to provide 146 | more information. 147 | """ 148 | 149 | self.csr.set(CSR.name_to_addr("mhartid"), UInt32(self.hart_id)) 150 | self.csr.register_callback( 151 | CSR.name_to_addr("time"), 152 | getter=self.rtclock.get_low32, 153 | ) 154 | self.csr.register_callback( 155 | CSR.name_to_addr("timeh"), 156 | getter=self.rtclock.get_hi32, 157 | ) 158 | self.csr.register_callback( 159 | CSR.name_to_addr("instret"), 160 | getter=(lambda csr, _: UInt32(self.cycle)), 161 | ) 162 | self.csr.register_callback( 163 | CSR.name_to_addr("instreth"), 164 | getter=(lambda csr, _: UInt32(self.cycle >> 32)), 165 | ) 166 | self.csr.register_callback( 167 | CSR.name_to_addr("cycle"), 168 | getter=(lambda csr, _: UInt32(self.cycle)), 169 | ) 170 | self.csr.register_callback( 171 | CSR.name_to_addr("cycleh"), 172 | getter=(lambda csr, _: UInt32(self.cycle >> 32)), 173 | ) 174 | -------------------------------------------------------------------------------- /riscemu/libc/stdlib.s: -------------------------------------------------------------------------------- 1 | // A very basic implementation of a stdlib.h but in assembly. 2 | // should(tm) work with riscemu. 3 | // 4 | // Copyright (c) 2023 Anton Lydike 5 | // SPDX-License-Identifier: MIT 6 | 7 | .data 8 | 9 | _rand_seed: 10 | .word 0x76767676 11 | _atexit_calls: 12 | // leave room for 8 atexit handlers here for now 13 | .word 0x00, 0x00, 0x00, 0x00 14 | .word 0x00, 0x00, 0x00, 0x00 15 | _atexit_count: 16 | .word 0x00 17 | 18 | _malloc_base_ptr: 19 | // first word is a pointer to some space 20 | // second word is the offset inside that space 21 | // space is always MALLOC_PAGE_SIZE bytes 22 | .word 0x00, 0x00 23 | .equ MALLOC_PAGE_SIZE 4069 24 | 25 | .text 26 | 27 | // malloc/free 28 | 29 | .globl malloc 30 | .globl free 31 | 32 | // malloc(size_t size) 33 | malloc: 34 | // set aside size in s0 35 | sw s0, -4(sp) 36 | mv s0, a0 37 | la t0, _malloc_base_ptr 38 | lw t1, 0(t0) 39 | beq t1, zero, _malloc_init 40 | _malloc_post_init: 41 | // if we are here, we always have 42 | // t0 = (&_malloc_base_ptr) 43 | // t1 = *(&_malloc_base_ptr) 44 | // now we load 45 | // t2 = base_ptr_offset 46 | lw t2, 4(t0) 47 | // add allocated size to offset 48 | add t2, t2, s0 49 | // check for overflow 50 | li t4, MALLOC_PAGE_SIZE 51 | bge t2, t4, _malloc_fail 52 | // save the new offset 53 | sw t2, 4(t0) 54 | // calculate base_ptr + offset 55 | add a0, t2, t1 56 | // return that 57 | lw s0, -4(sp) 58 | ret 59 | 60 | _malloc_init: 61 | // call mmap2() 62 | li a0, 0 // addr = 0, let OS choose address 63 | li a1, 4096 // size 64 | li a2, 3 // PROT_READ | PROT_WRITE 65 | li a3, 5 // MAP_PRIVATE | MAP_ANONYMOUS 66 | li a7, SCALL_MMAP2 67 | ecall // invoke syscall 68 | // check for error code 69 | li t2, -1 70 | beq a0, t2, _malloc_fail 71 | // if succeeded, load &_malloc_base_ptr 72 | // put value of _malloc_base_ptr into t1 73 | mv t1, a0 74 | // save base ptr to _malloc_base_ptr 75 | sw t1, 0(t0) 76 | // jump to post_init 77 | j _malloc_post_init 78 | _malloc_fail: 79 | li a0, 0 80 | ret 81 | 82 | // free is a nop, that's valid, but not very good^^ 83 | free: 84 | ret 85 | 86 | 87 | 88 | // exit, atexit 89 | .globl exit 90 | .globl atexit 91 | 92 | // we can happily use saved registers here because we don't care at all if we 93 | // destroy the calling registers. This is __noreturn__ anyways! 94 | // register layout: 95 | // s0 = &_atexit_count 96 | // s2 = &_atexit_calls 97 | // s1 = updated value of atexit 98 | // s3 = exit code 99 | exit: 100 | // save exit code to s3 101 | mv s3, a0 102 | _exit_start: 103 | la s0, _atexit_count // s0 = &_atexit_count 104 | lw s1, 0(s0) // s1 = *(&_atexit_count) 105 | // exit if no atexit() calls remain 106 | beq s1, zero, _exit 107 | // decrement 108 | addi s1, s1, -4 // s1-- 109 | // save decremented value 110 | sw s1, 0(s0) // _atexit_count = s1 111 | li s2, _atexit_calls 112 | add s1, s1, s2 // s1 = &_atexit_calls + (s1) 113 | lw s1, 0(s1) // s1 = *s1 114 | la ra, _exit_start // set ra up to point to exit 115 | jalr zero, s1, 0 // jump to address in s1 116 | // jalr will call the other function, which will then return back 117 | // to the beginning of exit. 118 | _exit: 119 | mv a0, s3 120 | li a7, 93 121 | ecall 122 | 123 | // atexit a0 = funcptr 124 | atexit: 125 | sw t0, -4(sp) 126 | sw t2, -8(sp) 127 | // load _atexit_count 128 | la t0, _atexit_count 129 | lw t2, 0(t0) 130 | // if >= 8, fail 131 | li t1, 8 132 | bge t2, t1, _atexit_fail 133 | // increment atexit_count by 4 (one word) 134 | addi t2, t2, 4 135 | sw t2, 0(t0) 136 | // load address of _atexit_calls 137 | la t0, _atexit_calls 138 | // add new _atexit_count to _atexit_calls 139 | add t0, t0, t2 140 | sw a0, -4(t0) 141 | li a0, 0 142 | lw t0, -4(sp) 143 | lw t2, -8(sp) 144 | ret 145 | 146 | _atexit_fail: 147 | li a0, -1 148 | lw s0, -4(sp) 149 | lw s1, -8(sp) 150 | ret 151 | 152 | 153 | 154 | 155 | 156 | 157 | // rand, srand 158 | 159 | .globl rand 160 | .globl srand 161 | 162 | // simple xorshift rand implementation 163 | rand: 164 | // load seed 165 | la t1, _rand_seed 166 | lw a0, 0(t1) 167 | // three rounds of shifts: 168 | sll a0, t0, 13 // x ^= x << 13; 169 | srl a0, t0, 17 // x ^= x >> 17; 170 | sll a0, t0, 5 // x ^= x << 5; 171 | sw a0, 0(t1) 172 | ret 173 | 174 | srand: 175 | la t1, _rand_seed 176 | sw a0, 0(t1) 177 | ret 178 | -------------------------------------------------------------------------------- /riscemu/libc/string.s: -------------------------------------------------------------------------------- 1 | // string operations in RISC-V Assembly 2 | // 3 | // Copyright (c) 2023 Anton Lydike 4 | // SPDX-License-Identifier: MIT 5 | 6 | // Create NullPtr constant 7 | .equ NULL, 0x00 8 | .global NULL 9 | 10 | 11 | .global strlen 12 | // size_t libstr_strlen(char* str) 13 | // return the length of str 14 | 15 | 16 | .global strncpy 17 | // char *strncpy(char *dest, const char *src, size_t n) 18 | // copy n bytes from source into dest. If source ends before n bytes, the rest is filled with null-bytes 19 | // returns pointer to dest 20 | 21 | 22 | .global strcpy 23 | // char *strncpy(char *dest, const char *src) 24 | // copy string src into dest, including null terminator 25 | // returns pointer to dest 26 | 27 | 28 | .global memchr 29 | // void *memchr(const void *str, char c, size_t n) 30 | // search vor the first occurrence of c in str 31 | // returns a pointer to the first occurrence, or NULL 32 | 33 | 34 | .global memset 35 | // void *memset(void *str, char c, size_t n) 36 | // copies the character c to the first n characters of str. 37 | 38 | 39 | // missing implementations 40 | //.global memcmp 41 | //.global memcpy 42 | //.global strcat 43 | 44 | 45 | .text 46 | strlen: 47 | // size_t strlen(char* str) 48 | // push s1, s2 to the stack 49 | sw s1, sp, -4 50 | sw s2, sp, -8 51 | // since no subroutines are called, we don't need to increment or decrement the sp 52 | addi s2, zero, -1 // length (since null byte is counted by this method, we return len - 1) 53 | __strlen_loop: 54 | lb s1, a0, 0 // read character 55 | addi s2, s2, 1 // increment number bytes read 56 | addi a0, a0, 1 57 | bne s1, zero, __strlen_loop 58 | // we are done, set return value in a0 59 | add a0, zero, s2 60 | // pop s1, s2, from stack 61 | lw s1, sp, -4 62 | lw s2, sp, -8 63 | ret 64 | 65 | strncpy: 66 | // char *strncpy(char *dest, const char *src, size_t n) 67 | // copy size bytes from source to dest 68 | sw s1, sp, -4 // push s1 to the stack 69 | sw s2, sp, -8 // push s1 to the stack 70 | add s1, a0, zero // save dest pointer for return 71 | __strncpy_loop: 72 | beq a2, zero, __strncpy_end 73 | // copy byte 74 | lb s2, a1, 0 // read first byte from src 75 | sb s2, a0, 0 // write first byte to dest 76 | // increment pointers 77 | addi a0, a0, 1 78 | addi a1, a1, 1 79 | // one less byte to copy 80 | addi a2, a2, -1 81 | // if we read the terminating byte, jump to fill code 82 | beq s2, zero, __strncpy_fill 83 | // otherwise continue copying 84 | j __strncpy_loop 85 | __strncpy_fill: 86 | // fill remaining space with 0 bytes 87 | // if no bytes left, stop filling 88 | beq a2, zero, __strncpy_end 89 | sb zero, a0, 0 90 | addi a0, a0, 1 91 | addi a2, a2, -1 92 | j __strncpy_fill 93 | __strncpy_end: 94 | // set return value 95 | add a0, zero, s1 96 | // pop s1, s2 from stack 97 | lw s1, sp, -4 98 | lw s2, sp, -8 99 | ret 100 | 101 | 102 | strcpy: 103 | // char *strcpy(char *dest, const char *src) 104 | sw s1, sp, -4 // push s1 to the stack 105 | sw s2, sp, -8 // push s1 to the stack 106 | add s1, a0, zero // save dest pointer for return 107 | __strcpy_loop: 108 | // copy byte 109 | lb s2, a1, 0 // read first byte from src 110 | sb s2, a0, 0 // write first byte to dest 111 | // increment pointers 112 | addi a0, a0, 1 113 | addi a1, a1, 1 114 | bne s2, zero, __strcpy_loop 115 | // we are done copying, return 116 | // set return value 117 | add a0, zero, s1 118 | // pop s1, s2 from stack 119 | lw s1, sp, -4 120 | lw s2, sp, -8 121 | ret 122 | 123 | memchr: 124 | // void *memchr(const void *str, char c, size_t n) 125 | sw s1, sp, -4 // push s1 to the stack 126 | andi a1, a1, 0xff // trim a1 to be byte-sized 127 | __memchr_loop: 128 | beq a2, zero, __memchr_ret_null 129 | lb s1, a0, 0 130 | addi a0, a0, 1 // let a0 point to the next byte 131 | addi a2, a2, -1 // decrement bytes to copy by 1 132 | bne s1, a1, __memchr_loop 133 | // return pointer to prev byte (as the prev byte actually matched a1) 134 | addi a0, a0, -1 135 | // pop s1, from stack 136 | lw s1, sp, -4 137 | ret 138 | __memchr_ret_null: 139 | // nothing found, return nullptr 140 | addi a0, zero, NULL 141 | lw s1, sp, -4 142 | ret 143 | 144 | 145 | memset: 146 | // void *memset(void *str, char c, size_t n) 147 | __memset_loop: 148 | beq a2, zero, __memset_ret 149 | sb a1, a0, 0 150 | addi a0, a0, 1 151 | addi a2, a2, -1 152 | j __memset_loop 153 | __memset_ret: 154 | ret 155 | -------------------------------------------------------------------------------- /riscemu/instructions/RV32F.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2023 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | This file contains copious amounts of docstrings that were all taken 7 | from https://msyksphinz-self.github.io/riscv-isadoc/html/rvfd.html 8 | (all the docstrings on the instruction methods documenting the opcodes 9 | and their function) 10 | """ 11 | from .instruction_set import Instruction 12 | from .float_base import FloatArithBase 13 | from riscemu.core import Float32, Int32, UInt32 14 | 15 | 16 | class RV32F(FloatArithBase[Float32]): 17 | flen = 32 18 | _float_cls = Float32 19 | 20 | def instruction_fcvt_w_s(self, ins: Instruction): 21 | """ 22 | +-----+-----+-----+-----+-----+-----+-----+---+ 23 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 24 | +-----+-----+-----+-----+-----+-----+-----+---+ 25 | |11000|00 |00000|rs1 |rm |rd |10100|11 | 26 | +-----+-----+-----+-----+-----+-----+-----+---+ 27 | 28 | :Format: 29 | | fcvt.w.s rd,rs1 30 | 31 | :Description: 32 | | Convert a floating-point number in floating-point register rs1 to a signed 32-bit in integer register rd. 33 | 34 | :Implementation: 35 | | x[rd] = sext(s32_{f32}(f[rs1])) 36 | """ 37 | rd, rs = self.parse_rd_rs(ins) 38 | self.regs.set(rd, Int32.from_float(self.regs.get_f(rs).value)) 39 | 40 | def instruction_fcvt_wu_s(self, ins: Instruction): 41 | """ 42 | +-----+-----+-----+-----+-----+-----+-----+---+ 43 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 44 | +-----+-----+-----+-----+-----+-----+-----+---+ 45 | |11000|00 |00001|rs1 |rm |rd |10100|11 | 46 | +-----+-----+-----+-----+-----+-----+-----+---+ 47 | 48 | :Format: 49 | | fcvt.wu.s rd,rs1 50 | 51 | :Description: 52 | | Convert a floating-point number in floating-point register rs1 to a signed 32-bit in unsigned integer register rd. 53 | 54 | :Implementation: 55 | | x[rd] = sext(u32_{f32}(f[rs1])) 56 | """ 57 | rd, rs = self.parse_rd_rs(ins) 58 | self.regs.set(rd, UInt32.from_float((self.regs.get_f(rs).value))) 59 | 60 | def instruction_fmv_x_w(self, ins: Instruction): 61 | """ 62 | +-----+-----+-----+-----+-----+-----+-----+---+ 63 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 64 | +-----+-----+-----+-----+-----+-----+-----+---+ 65 | |11100|00 |00000|rs1 |000 |rd |10100|11 | 66 | +-----+-----+-----+-----+-----+-----+-----+---+ 67 | 68 | :Format: 69 | | fmv.x.w rd,rs1 70 | 71 | :Description: 72 | | Move the single-precision value in floating-point register rs1 represented in IEEE 754-2008 encoding to the lower 32 bits of integer register rd. 73 | 74 | :Implementation: 75 | | x[rd] = sext(f[rs1][31:0]) 76 | """ 77 | rd, rs = self.parse_rd_rs(ins) 78 | self.regs.set(rd, UInt32(self.regs.get_f(rs).bytes[-4:])) 79 | 80 | def instruction_fcvt_s_w(self, ins: Instruction): 81 | """ 82 | +-----+-----+-----+-----+-----+-----+-----+---+ 83 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 84 | +-----+-----+-----+-----+-----+-----+-----+---+ 85 | |11010|00 |00000|rs1 |rm |rd |10100|11 | 86 | +-----+-----+-----+-----+-----+-----+-----+---+ 87 | 88 | :Format: 89 | | fcvt.s.w rd,rs1 90 | 91 | :Description: 92 | | Converts a 32-bit signed integer, in integer register rs1 into a floating-point number in floating-point register rd. 93 | 94 | :Implementation: 95 | | f[rd] = f32_{s32}(x[rs1]) 96 | """ 97 | rd, rs = self.parse_rd_rs(ins) 98 | self.regs.set_f(rd, Float32(self.regs.get(rs).value)) 99 | 100 | def instruction_fcvt_s_wu(self, ins: Instruction): 101 | """ 102 | +-----+-----+-----+-----+-----+-----+-----+---+ 103 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 104 | +-----+-----+-----+-----+-----+-----+-----+---+ 105 | |11010|00 |00001|rs1 |rm |rd |10100|11 | 106 | +-----+-----+-----+-----+-----+-----+-----+---+ 107 | 108 | :Format: 109 | | fcvt.s.wu rd,rs1 110 | 111 | :Description: 112 | | Converts a 32-bit unsigned integer, in integer register rs1 into a floating-point number in floating-point register rd. 113 | 114 | :Implementation: 115 | | f[rd] = f32_{u32}(x[rs1]) 116 | """ 117 | rd, rs = self.parse_rd_rs(ins) 118 | self.regs.set_f(rd, Float32(self.regs.get(rs).unsigned_value)) 119 | 120 | def instruction_fmv_w_x(self, ins: Instruction): 121 | """ 122 | +-----+-----+-----+-----+-----+-----+-----+---+ 123 | |31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0| 124 | +-----+-----+-----+-----+-----+-----+-----+---+ 125 | |11110|00 |00000|rs1 |000 |rd |10100|11 | 126 | +-----+-----+-----+-----+-----+-----+-----+---+ 127 | 128 | 129 | 130 | :Format: 131 | | fmv.w.x rd,rs1 132 | 133 | :Description: 134 | | Move the single-precision value encoded in IEEE 754-2008 standard encoding from the lower 32 bits of integer register rs1 to the floating-point register rd. 135 | 136 | :Implementation: 137 | | f[rd] = x[rs1][31:0] 138 | """ 139 | rd, rs = self.parse_rd_rs(ins) 140 | self.regs.set_f(rd, Float32.from_bytes(self.regs.get(rs).to_bytes())) 141 | -------------------------------------------------------------------------------- /riscemu/core/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | """ 6 | 7 | from abc import abstractmethod 8 | from ..colors import * 9 | import typing 10 | 11 | if typing.TYPE_CHECKING: 12 | from . import Instruction 13 | 14 | 15 | class RiscemuBaseException(BaseException): 16 | @abstractmethod 17 | def message(self) -> str: 18 | raise NotImplemented 19 | 20 | def print_stacktrace(self): 21 | import traceback 22 | 23 | traceback.print_exception(type(self), self, self.__traceback__) 24 | 25 | 26 | # Parsing exceptions: 27 | 28 | 29 | class ParseException(RiscemuBaseException): 30 | def __init__(self, msg: str, data=None): 31 | super().__init__(msg, data) 32 | self.msg = msg 33 | self.data = data 34 | 35 | def message(self): 36 | return ( 37 | FMT_PARSE 38 | + '{}("{}", data={})'.format(self.__class__.__name__, self.msg, self.data) 39 | + FMT_NONE 40 | ) 41 | 42 | 43 | def ASSERT_EQ(a1, a2): 44 | if a1 != a2: 45 | raise ParseException( 46 | "ASSERTION_FAILED: Expected elements to be equal!", (a1, a2) 47 | ) 48 | 49 | 50 | def ASSERT_LEN(a1, size): 51 | if len(a1) != size: 52 | raise ParseException( 53 | "ASSERTION_FAILED: Expected {} to be of length {}".format(a1, size), 54 | (a1, size), 55 | ) 56 | 57 | 58 | def ASSERT_NOT_NULL(a1): 59 | if a1 is None: 60 | raise ParseException( 61 | "ASSERTION_FAILED: Expected {} to be non null".format(a1), (a1,) 62 | ) 63 | 64 | 65 | def ASSERT_NOT_IN(a1, a2): 66 | if a1 in a2: 67 | raise ParseException( 68 | "ASSERTION_FAILED: Expected {} to not be in {}".format(a1, a2), (a1, a2) 69 | ) 70 | 71 | 72 | def ASSERT_IN(a1, a2): 73 | if a1 not in a2: 74 | raise ParseException( 75 | "ASSERTION_FAILED: Expected {} to not be in {}".format(a1, a2), (a1, a2) 76 | ) 77 | 78 | 79 | class LinkerException(RiscemuBaseException): 80 | def __init__(self, msg: str, data): 81 | self.msg = msg 82 | self.data = data 83 | 84 | def message(self): 85 | return ( 86 | FMT_PARSE 87 | + '{}("{}", data={})'.format(self.__class__.__name__, self.msg, self.data) 88 | + FMT_NONE 89 | ) 90 | 91 | 92 | # MMU Exceptions 93 | 94 | 95 | class MemoryAccessException(RiscemuBaseException): 96 | def __init__(self, msg: str, addr, size, op): 97 | super(MemoryAccessException, self).__init__() 98 | self.msg = msg 99 | self.addr = addr 100 | self.size = size 101 | self.op = op 102 | 103 | def message(self): 104 | return ( 105 | FMT_MEM 106 | + "{}(During {} at 0x{:08x} of size {}: {})".format( 107 | self.__class__.__name__, self.op, self.addr, self.size, self.msg 108 | ) 109 | + FMT_NONE 110 | ) 111 | 112 | 113 | class OutOfMemoryException(RiscemuBaseException): 114 | def __init__(self, action): 115 | self.action = action 116 | 117 | def message(self): 118 | return ( 119 | FMT_MEM 120 | + "{}(Ran out of memory during {})".format( 121 | self.__class__.__name__, self.action 122 | ) 123 | + FMT_NONE 124 | ) 125 | 126 | 127 | class InvalidAllocationException(RiscemuBaseException): 128 | def __init__(self, msg, name, size, flags): 129 | self.msg = msg 130 | self.name = name 131 | self.size = size 132 | self.flags = flags 133 | 134 | def message(self): 135 | return FMT_MEM + "{}[{}](name={}, size={}, flags={})".format( 136 | self.__class__.__name__, self.msg, self.name, self.size, self.flags 137 | ) 138 | 139 | 140 | # CPU Exceptions 141 | 142 | 143 | class UnimplementedInstruction(RiscemuBaseException): 144 | def __init__(self, ins: "Instruction", context=None): 145 | self.ins = ins 146 | self.context = context 147 | 148 | def message(self): 149 | return ( 150 | FMT_CPU 151 | + "{}({}{})".format( 152 | self.__class__.__name__, 153 | repr(self.ins), 154 | ", context={}".format(self.context) if self.context is not None else "", 155 | ) 156 | + FMT_NONE 157 | ) 158 | 159 | 160 | class InvalidRegisterException(RiscemuBaseException): 161 | def __init__(self, reg): 162 | self.reg = reg 163 | 164 | def message(self): 165 | return ( 166 | FMT_CPU 167 | + "{}(Invalid register {})".format(self.__class__.__name__, self.reg) 168 | + FMT_NONE 169 | ) 170 | 171 | 172 | class InvalidSyscallException(RiscemuBaseException): 173 | def __init__(self, scall): 174 | self.scall = scall 175 | 176 | def message(self): 177 | return ( 178 | FMT_SYSCALL 179 | + "{}(Invalid syscall: {})".format(self.__class__.__name__, self.scall) 180 | + FMT_NONE 181 | ) 182 | 183 | 184 | def INS_NOT_IMPLEMENTED(ins): 185 | raise UnimplementedInstruction(ins) 186 | 187 | 188 | class NumberFormatException(RiscemuBaseException): 189 | def __init__(self, msg): 190 | super().__init__(msg) 191 | self.msg = msg 192 | 193 | def message(self): 194 | return "{}({})".format(self.__class__.__name__, self.msg) 195 | 196 | 197 | # this exception is not printed and simply signals that an interactive debugging session is 198 | class LaunchDebuggerException(RiscemuBaseException): 199 | def message(self) -> str: 200 | return "" 201 | -------------------------------------------------------------------------------- /riscemu/core/usermode_cpu.py: -------------------------------------------------------------------------------- 1 | """ 2 | RiscEmu (c) 2021-2022 Anton Lydike 3 | 4 | SPDX-License-Identifier: MIT 5 | 6 | This file contains the CPU logic (not the individual instruction sets). See instructions/instruction_set.py for more info 7 | on them. 8 | """ 9 | import typing 10 | from typing import List, Type 11 | 12 | from ..config import RunConfig 13 | from ..colors import FMT_CPU, FMT_NONE, FMT_ERROR, FMT_GRAY, FMT_CYAN 14 | from ..debug import launch_debug_session 15 | from ..syscall import SyscallInterface, get_syscall_symbols 16 | from . import ( 17 | CPU, 18 | Int32, 19 | BinaryDataMemorySection, 20 | MMU, 21 | RiscemuBaseException, 22 | LaunchDebuggerException, 23 | PrivModes, 24 | Instruction, 25 | SimpleInstruction, 26 | ) 27 | 28 | if typing.TYPE_CHECKING: 29 | from ..instructions import InstructionSet 30 | 31 | 32 | class UserModeCPU(CPU): 33 | """ 34 | This class represents a single CPU. It holds references to it's mmu, registers and syscall interrupt handler. 35 | 36 | It is initialized with a configuration and a list of instruction sets. 37 | """ 38 | 39 | def __init__(self, instruction_sets: List[Type["InstructionSet"]], conf: RunConfig): 40 | """ 41 | Creates a CPU instance. 42 | 43 | :param instruction_sets: A list of instruction set classes. These must inherit from the InstructionSet class 44 | """ 45 | # setup CPU states 46 | super().__init__(MMU(), instruction_sets, conf) 47 | 48 | self.exit_code = 0 49 | 50 | # setup syscall interface 51 | self.syscall_int = SyscallInterface() 52 | 53 | # add global syscall symbols, but don't overwrite any user-defined symbols 54 | syscall_symbols = get_syscall_symbols() 55 | syscall_symbols.update(self.mmu.global_symbols) 56 | self.mmu.global_symbols.update(syscall_symbols) 57 | self.mode = PrivModes.USER 58 | 59 | def step(self, verbose: bool = False): 60 | """ 61 | Execute a single instruction, then return. 62 | """ 63 | launch_debugger = False 64 | 65 | try: 66 | self.cycle += 1 67 | ins = self.mmu.read_ins(self.pc) 68 | if verbose: 69 | if self.conf.verbosity > 2: 70 | ins_str = self._format_ins(ins) 71 | else: 72 | ins_str = str(ins) 73 | print(FMT_CPU + " 0x{:08X}:{} {}".format(self.pc, FMT_NONE, ins_str)) 74 | self.pc += self.INS_XLEN 75 | self.run_instruction(ins) 76 | except RiscemuBaseException as ex: 77 | if isinstance(ex, LaunchDebuggerException): 78 | # if the debugger is active, raise the exception to 79 | if self.debugger_active: 80 | raise ex 81 | 82 | print(FMT_CPU + "[CPU] Debugger launch requested!" + FMT_NONE) 83 | launch_debugger = True 84 | else: 85 | print(ex.message()) 86 | ex.print_stacktrace() 87 | print(FMT_CPU + "[CPU] Halting due to exception!" + FMT_NONE) 88 | self.halted = True 89 | 90 | if launch_debugger: 91 | launch_debug_session(self) 92 | 93 | def run(self, verbose: bool = False): 94 | while not self.halted: 95 | self.step(verbose) 96 | 97 | if self.conf.verbosity > 0: 98 | print( 99 | FMT_CPU 100 | + "[CPU] Program exited with code {}".format(self.exit_code) 101 | + FMT_NONE 102 | ) 103 | 104 | def setup_stack(self, stack_size: int = 1024 * 4) -> bool: 105 | """ 106 | Create program stack and populate stack pointer 107 | :param stack_size: the size of the required stack, defaults to 4Kib 108 | :return: 109 | """ 110 | stack_sec = BinaryDataMemorySection( 111 | bytearray(stack_size), 112 | ".stack", 113 | None, # FIXME: why does a binary data memory section require a context? 114 | "", 115 | 0, 116 | ) 117 | 118 | if not self.mmu.load_section(stack_sec, fixed_position=False): 119 | print(FMT_ERROR + "[CPU] Could not insert stack section!" + FMT_NONE) 120 | return False 121 | 122 | self.regs.set("sp", Int32(stack_sec.base + stack_sec.size)) 123 | 124 | if self.conf.verbosity > 1: 125 | print( 126 | FMT_CPU 127 | + "[CPU] Created stack of size {} at 0x{:x}".format( 128 | stack_size, stack_sec.base 129 | ) 130 | + FMT_NONE 131 | ) 132 | 133 | return True 134 | 135 | def _format_arg(self, arg: str, ins: Instruction) -> str: 136 | if arg in self.regs.vals or arg in self.regs.valid_regs: 137 | return "{}{}=0x{:x}{}".format( 138 | arg, FMT_GRAY, self.regs.get(arg, False), FMT_NONE 139 | ) 140 | elif arg in self.regs.float_vals or arg in self.regs.float_regs: 141 | return "{}{}={}{}".format(arg, FMT_GRAY, self.regs.get_f(arg), FMT_NONE) 142 | elif isinstance(ins, SimpleInstruction): 143 | val = ins.context.resolve_label(arg) 144 | if val is None: 145 | val = ins.context.resolve_numerical_label(arg, ins.addr) 146 | if val is None: 147 | return FMT_CYAN + arg + FMT_NONE 148 | return "{}{}=0x{:x}{}".format(arg, FMT_GRAY, val, FMT_NONE) 149 | 150 | def _format_ins(self, ins: Instruction) -> str: 151 | args = ", ".join(self._format_arg(arg, ins) for arg in ins.args) 152 | return "{}\t{}".format(ins.name, args) 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RiscEmu - RISC-V (userspace) emulator in python 2 | 3 | [![Documentation Status](https://readthedocs.org/projects/riscemu/badge/?version=latest)](https://riscemu.readthedocs.io/en/latest/?badge=latest) 4 | 5 | Implementing a basic RISC-V emulator, aimed at being easily extendable. Check out the docs at [readthedocs](https://riscemu.readthedocs.io/en/latest/index.html) 6 | or [riscemu.datenvorr.at](https://riscemu.datenvorr.at/index.html). 7 | 8 | This emulator contains: 9 | * RISC-V Assembly parser 10 | * RISC-V Assembly loader 11 | * Emulation for most parts of the basic RISC-V instruction set and the M and A extensions 12 | * Naive memory emulator 13 | * Basic implementation of some syscalls 14 | * A debugging environment 15 | 16 | ## Installation: 17 | 18 | ```bash 19 | $ pip install riscemu 20 | ``` 21 | 22 | ## Running simple Assembly: 23 | A couple of basic assembly programs are provided inside `examples/`, such as [`hello-world.asm`](examples/hello-world.asm). 24 | 25 | You can run it by typing `python -m riscemu examples/hello-world.asm`. It will produce output similar to: 26 | ``` 27 | [MMU] Successfully loaded: LoadedExecutable[examples/hello-world.asm](base=0x00000100, size=24bytes, sections=data text, run_ptr=0x00000110) 28 | [CPU] Started running from 0x00000110 (examples/hello-world.asm) 29 | Hello world 30 | 31 | Program exited with code 0 32 | ``` 33 | 34 | The [`read` syscall](docs/syscalls.md) defaults to readline behaviour. Reading "true chunks" (ignoring newlines) is currently not supported. 35 | 36 | See the docs on [assembly](docs/assembly.md) for more detail on how to write assembly code for this emulator. 37 | See the [list of implemented syscalls](docs/syscalls.md) for more details on how to syscall. 38 | 39 | Currently, symbols (such as `main` or `loop`) are looked-up at runtime. This allows for better debugging, I believe. 40 | 41 | Basic IO should work, as open, read, write and close are supported for stdin/stdout/stderr and even arbitrary file paths (if enabled) 42 | 43 | When trying to run an assembly program, the emulator first tries to find a symbol named `_start`, then a symbol named `main`. if both 44 | symbols were not found in the file, it simply starts at the beginning of the `.text` segment. 45 | 46 | ## Using the CLI: 47 | *Current CLI is not final, options may change frequently until a stable version is reached* 48 | 49 | This is how the interface is used: 50 | 51 | ``` 52 | usage: riscemu [-h] [--options OPTIONS] [--syscall-opts SYSCALL_OPTS] [--instruction-sets INSTRUCTION_SETS] [--stack_size stack-size] file.asm [file.asm ...] 53 | 54 | 55 | 56 | OPTIONS and SYSCALL_OPTIONS is a list of comma-separated flags that will be enabled 57 | 58 | --options OPTIONS: (-o) 59 | disable_debug Disable the ebreak and sbreak instructions 60 | no_syscall_symbols Don't make syscall symbols globally available 61 | fail_on_ex Do not launch an interactive debugger when the CPU loop catches an exception 62 | add_accept_imm accept "add rd, rs, imm" instructions, even though they are not standard 63 | 64 | --syscall-opts SYSCALL_OPTS: (-so) 65 | Options to control syscall behaviour 66 | fs_access Allow access to the filesystem 67 | disable_io Disallow reading/writing from stdin/stdout/stderr 68 | 69 | --instruction-sets INSTRUCTION_SETS: (-is) 70 | A list of comma separated instruction sets you want to load: 71 | Currently implemented: RV32I, RV32M 72 | ``` 73 | 74 | If multiple files are specified, all are loaded into memory, but only the last one is executed. This might be improved 75 | later, maybe the `_init` section of each binary is executed before the main loop starts? 76 | 77 | If `stack_size` is greater than zero, a stack is allocated and initialized, with the `sp` register pointing to the end of the stack. 78 | 79 | 80 | ## Debugging 81 | Debugging is done using the `ebreak` (formerly `sbreak`) instruction, which will launch a debugging session if encountered. 82 | See [docs/debugging.md](docs/debugging.md) for more info. 83 | 84 | ![debugging the fibs program](docs/debug-session.png) 85 | 86 | 87 | ## The source code: 88 | Check out the [documentation](https://riscemu.readthedocs.io/en/latest/riscemu.html). 89 | 90 | ## Accessing local documentation: 91 | To generate your local documentation, first install everything in `sphinx-docs/requirements.txt`. Then run `./generate-docs.sh`, which will 92 | generate and make all doc files for you. Finally, you can open the docs locall by running `open sphinx-docs/build/html/index.html`. 93 | 94 | ## Resources: 95 | * RISC-V Programmers Handbook: https://github.com/riscv-non-isa/riscv-asm-manual/blob/master/riscv-asm.md 96 | * Pseudo ops: https://www.codetd.com/article/8981522 97 | * detailed instruction definition: https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#add 98 | * RISC-V reference card: https://www.cl.cam.ac.uk/teaching/1617/ECAD+Arch/files/docs/RISCVGreenCardv8-20151013.pdf 99 | 100 | ## TODO: 101 | * Correctly handle 12 and 20 bit immediate (currently not limited to bits at all) 102 | * Add a cycle limit to the options and CPU to catch infinite loops 103 | * Move away from `print` and use `logging.logger` instead 104 | * Writer proper tests 105 | 106 | 107 | ## How To Release: 108 | 109 | Create a new commit that: 110 | 1. Changes the "Upcoming" heading to the new versions number 111 | 2. Increments the version in the pyproject.toml to the next version 112 | 113 | Commit this, and tag it with `v`. Push the commit and the tag: 114 | ```bash 115 | git push 116 | git push origin "v" 117 | ``` 118 | 119 | On GitHub, [draft a new release](https://github.com/AntonLydike/riscemu/releases/new), and 120 | then approve the workflow run [here](https://github.com/AntonLydike/riscemu/actions). 121 | --------------------------------------------------------------------------------