Compare commits

..

17 Commits

Author SHA1 Message Date
Anton Lydike a28bf834ac runtime: add a basic stdlib and crt0 file 1 year ago
Anton Lydike 207cf918ef syscall: add partial support for mmap2 syscall 1 year ago
Anton Lydike c7e14a3b42 misc: annotations and dead code removal 1 year ago
Anton Lydike 7a4972d48f fix jalr instruction to take arguments in the form of rd, rs, imm 1 year ago
Sasha Lopoukhine 25d059da09
add some typing annotations (#20)
* add some typing annotations

* minor additions

* import Optional

* format with black

* review comments

---------

Co-authored-by: Anton Lydike <me@antonlydike.de>
2 years ago
Anton Lydike d6d3a18aa6 minor additions 2 years ago
Anton Lydike 1ea5bb2edc more filechecks? 2 years ago
Anton Lydike 2f6073b4df fix whitespace issues 2 years ago
Anton Lydike 189dc63ceb add lit filecheck 2 years ago
Anton Lydike 0c37be3c4d fix ci (pt3) ? 2 years ago
Anton Lydike a51681811f fix ci (pt2) ? 2 years ago
Anton Lydike 87968d08d9 fix ci? 2 years ago
Anton Lydike 94d01a97d9 add ci 2 years ago
Anton Lydike 448b19c144 add blame ignore commit 2 years ago
Anton Lydike 5515c7795c format black 2 years ago
Anton Lydike e1fbe4f11d fix testing infra 2 years ago
Anton Lydike dd77d1b387 minor cleanup 2 years ago

@ -0,0 +1,2 @@
# introduces black formatting
5515c7795cfd690d346aad10ce17b30acf914648

@ -0,0 +1,49 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: CI - Python-based Testing
on:
# Trigger the workflow on push or pull request,
# but only for the master branch
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10']
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Upgrade pip
run: |
pip install --upgrade pip
- name: Install the package locally
run: pip install -r requirements.txt -r requirements-dev.txt
- name: Test with pytest
run: |
pytest -W error
- name: Test with lit
run: |
lit -v test/filecheck
#- name: Execute lit tests
# run: |
# export PYTHONPATH=$(pwd)
# lit -v tests/filecheck/

@ -0,0 +1,34 @@
# This workflow check the format all files in the repository
# * It checks that all nonempty files have a newline at the end
# * It checks that there are no whitespaces at the end of lines
# * It checks that Python files are formatted with black
name: Code Formatting
on:
pull_request:
push:
branches: [master]
jobs:
code-formatting:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10']
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Upgrade pip
run: |
pip install --upgrade pip
- name: Run code formatting checks with pre-commit
uses: pre-commit/action@v3.0.0

@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install jupyterlite[all] libarchive-c build pyodide-build
- name: Build RiscEmu source distribution
run: |
cd riscemu
@ -42,7 +42,7 @@ jobs:
with:
path: pyodide
key: pyodide
- name: Clone pyodide if not cached
if: steps.cache-pyodide.outputs.cache-hit != 'true'
run: git clone https://github.com/pyodide/pyodide.git
@ -51,16 +51,16 @@ jobs:
# and do the necessary updates before building.
- name: Build custom Pyodide distribution
run: |
cd pyodide
git fetch --all
git checkout 0.22.0a3
python -m pip install -r requirements.txt
sudo apt update && sudo apt install f2c
rm -rf packages/riscemu
pyodide skeleton pypi riscemu
PYODIDE_PACKAGES="riscemu" make
- name: Build the JupyterLite site
@ -68,11 +68,11 @@ jobs:
mkdir content
cp riscemu/docs/* content -r
cp riscemu/examples content -r
rm -rf pyodide/pyodide
mkdir pyodide/pyodide
mv pyodide/dist pyodide/pyodide/pyodide
python -m jupyter lite build --contents content --pyodide pyodide/pyodide
- name: Upload artifact
@ -96,5 +96,3 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

@ -3,4 +3,4 @@
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
</component>

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (riscemu)" project-jdk-type="Python SDK" />
</project>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (riscemu)" project-jdk-type="Python SDK" />
</project>

@ -5,4 +5,4 @@
<module fileurl="file://$PROJECT_DIR$/.idea/riscemu.iml" filepath="$PROJECT_DIR$/.idea/riscemu.iml" />
</modules>
</component>
</project>
</project>

@ -13,4 +13,4 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
</module>

@ -3,4 +3,4 @@
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
</project>

@ -0,0 +1,12 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black

@ -25,7 +25,7 @@
- Fixed bug where wrong parts of section would be printed in mmu.dump()
- Removed tests for bind_twos_complement as the function is now redundant with the introduction of Int32
- Fixed address translation error for sections without symbols
- Changed verbosity level at which start and end of CPU are printed, added prints for start and stack loading
- Changed verbosity level at which start and end of CPU are printed, added prints for start and stack loading
## 2.0.2
@ -39,6 +39,6 @@
## 2.0.0
- Correct handling of 32 bit overflows and underflows
- Correct handling of 32 bit overflows and underflows
- Complete revamp of internal data structures
- Completely reworked how assembly is parsed

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

@ -10,7 +10,7 @@ This emulator contains:
* RISC-V Assembly loader
* Emulation for most parts of the basic RISC-V instruction set and the M and A extensions
* Naive memory emulator
* Basic implementation of some syscalls
* Basic implementation of some syscalls
* A debugging environment
## Installation:
@ -33,7 +33,7 @@ Program exited with code 0
If you want to run it from a python script, here is [an online demo](https://AntonLydike.github.io/riscemu/lab/index.html?path=PythonDemo.ipynb).
The [`read` syscall](docs/syscalls.md) defaults to readline behaviour. Reading "true chunks" (ignoring newlines) is currently not supported.
The [`read` syscall](docs/syscalls.md) defaults to readline behaviour. Reading "true chunks" (ignoring newlines) is currently not supported.
See the docs on [asembly](docs/assembly.md) for more detail on how to write assembly code for this emulator.
See the [list of implemented syscalls](docs/syscalls.md) for more details on how to syscall.
@ -71,10 +71,10 @@ disable_io Disallow reading/writing from stdin/stdout/stderr
--instruction-sets INSTRUCTION_SETS: (-is)
A list of comma separated instruction sets you want to load:
Currently implemented: RV32I, RV32M
```
```
If multiple files are specified, all are loaded into memeory, but only the last one is executed. This might be improved
later, maybe the `_init` section of each binary is executed before the main loop starts?
If multiple files are specified, all are loaded into memeory, but only the last one is executed. This might be improved
later, maybe the `_init` section of each binary is executed before the main loop starts?
If `stack_size` is greater than zero, a stack is allocated and initialized, with the `sp` register pointing to the end of the stack.
@ -90,17 +90,17 @@ See [docs/debugging.md](docs/debugging.md) for more info.
Check out the [documentation](https://riscemu.readthedocs.io/en/latest/riscemu.html).
## Accessing local documentation:
To generate your local documentation, first install everything in `sphinx-docs/requirements.txt`. Then run `./generate-docs.sh`, which will
To generate your local documentation, first install everything in `sphinx-docs/requirements.txt`. Then run `./generate-docs.sh`, which will
generate and make all doc files for you. Finally, you can open the docs locall by runnint `open sphinx-docs/build/html/index.html`.
## Resources:
* RISC-V Programmers Handbook: https://github.com/riscv-non-isa/riscv-asm-manual/blob/master/riscv-asm.md
* Pseudo ops: https://www.codetd.com/article/8981522
* detailed instruction definition: https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html#add
* RISC-V reference card: https://www.cl.cam.ac.uk/teaching/1617/ECAD+Arch/files/docs/RISCVGreenCardv8-20151013.pdf
## TODO:
* Correctly handle 12 and 20 bit immediate (currently not limited to bits at all)
* Add a cycle limit to the options and CPU to catch infinite loops
* Move away from `print` and use `logging.logger` instead
* Writer proper tests

@ -30,8 +30,8 @@ The following pseudo-ops are implemented as of yet:
* `.global <name>` mark symbol `<name>` as a global symbol. It is available from all loaded programs
* `.align <bytes>` currently a nop as cpu does not care about alignment as of now
## Sections:
Currently only these three sections are supported:
## Sections:
Currently only these three sections are supported:
* `data` read-write data (non-executable)
* `rodata` read-only data (non-executable)
* `text` executable data (read-only)

@ -37,7 +37,7 @@ uses the `ebreak` instruction to open the debugger. Let's run it and see what ha
[MMU] Successfully loaded: LoadedExecutable[examples/fibs.asm](base=0x00000100, size=72bytes, sections=data text, run_ptr=0x00000138)
[CPU] Started running from 0x00000138 (examples/fibs.asm)
Debug instruction encountered at 0x0000013C
>>>
>>>
```
In this interactive session, you have access to the cpu, registers, memory and syscall interface. You can look into everything,
@ -58,8 +58,8 @@ and most common tasks should have helper methods for them.
* `get(name)` get register content
* `set(name, val)` set register content
* `cpu`:
* The CPU has the `pc` attribute and `cycle` attribute. Others won't be useful in this context.
* The CPU has the `pc` attribute and `cycle` attribute. Others won't be useful in this context.
**Available helpers are:**
* `dump(regs | addr)` dumps either registers or memory address
@ -71,4 +71,4 @@ and most common tasks should have helper methods for them.
Example:
![debuggin the fibs program](debug-session.png)
![debuggin the fibs program](debug-session.png)

@ -1,19 +1,19 @@
# Internal Structure
## Loading assembly files:
In order to load an assembly file, you need to instantiate a CPU with the capabilities you want. Loading an assembly
In order to load an assembly file, you need to instantiate a CPU with the capabilities you want. Loading an assembly
file is the done in multiple steps:
* An `RiscVInput` is created, this represents the file internally
* An `RiscVTokenizer` is created by calling `cpu.get_tokenizer()`.
* The input is tokenized by calling `.tokenize()` on the tokenizer.
* The tokens can then be converted to an Executable, this will then
* The tokens can then be converted to an Executable, this will then
hold all the information such as name, sections, symbols, etc.
This is done by creating an `ExecutableParser(tk: RiscVTokenizer)`
and the calling `parse()`.
* Now you have a representation of the assembly file that can be loaded
into memory by calling `cpu.load(executable)`, this will internally
into memory by calling `cpu.load(executable)`, this will internally
construct a `LoadedExecutable`, which represents the actual memory
regions the executable contains (and some meta information such as
symbols).
@ -30,4 +30,3 @@ Creating a cpu with certain instruction sets is done by passing the CPU construc
```
cpu = CPU(config, [RV32I, RV32M])
```

@ -13,4 +13,3 @@ You can include the libraries by adding them as arguments (before your main asse
```
These libraries are no where near a stable state, so documentation will be scarce. Your best bet would be to `grep` for functionality. Sorry!

@ -7,7 +7,7 @@ Performing a syscall is quite simple:
; set syscall args:
addi a0, zero, 1 ; exit with code 1
; invode syscall handler
scall
scall
```
The global symbols (e.g. `SCALL_READ`) are loaded by default. If you specify the option `no_syscall_symbols`, they will be omitted.
@ -47,4 +47,4 @@ Requires flag `--scall-fs` to be set to True
# Extending these syscalls
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.
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.

@ -150,5 +150,3 @@ __memset_loop:
j __memset_loop
__memset_ret:
ret

@ -17,4 +17,4 @@ main:
print.uhex a0
// exit
li a7, 93
ecall
ecall

@ -35,4 +35,4 @@ cd sphinx-docs
make html
# xdg-open build/html/index.html
# xdg-open build/html/index.html

@ -1,4 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

@ -0,0 +1,5 @@
black==23.3.0
pre-commit==3.2.2
pytest==7.3.1
filecheck==0.0.23
lit==16.0.2

@ -30,7 +30,9 @@ class UserModeCPU(CPU):
It is initialized with a configuration and a list of instruction sets.
"""
def __init__(self, instruction_sets: List[Type['riscemu.InstructionSet']], conf: RunConfig):
def __init__(
self, instruction_sets: List[Type["riscemu.InstructionSet"]], conf: RunConfig
):
"""
Creates a CPU instance.
@ -49,12 +51,16 @@ class UserModeCPU(CPU):
syscall_symbols.update(self.mmu.global_symbols)
self.mmu.global_symbols.update(syscall_symbols)
def step(self, verbose=False):
def step(self, verbose: bool = False):
"""
Execute a single instruction, then return.
"""
if self.halted:
print(FMT_CPU + "[CPU] Program exited with code {}".format(self.exit_code) + FMT_NONE)
print(
FMT_CPU
+ "[CPU] Program exited with code {}".format(self.exit_code)
+ FMT_NONE
)
return
launch_debugger = False
@ -63,7 +69,9 @@ class UserModeCPU(CPU):
self.cycle += 1
ins = self.mmu.read_ins(self.pc)
if verbose:
print(FMT_CPU + " Running 0x{:08X}:{} {}".format(self.pc, FMT_NONE, ins))
print(
FMT_CPU + " Running 0x{:08X}:{} {}".format(self.pc, FMT_NONE, ins)
)
self.pc += self.INS_XLEN
self.run_instruction(ins)
except RiscemuBaseException as ex:
@ -72,25 +80,29 @@ class UserModeCPU(CPU):
if self.debugger_active:
raise ex
print(FMT_CPU + '[CPU] Debugger launch requested!' + FMT_NONE)
print(FMT_CPU + "[CPU] Debugger launch requested!" + FMT_NONE)
launch_debugger = True
else:
print(ex.message())
ex.print_stacktrace()
print(FMT_CPU + '[CPU] Halting due to exception!' + FMT_NONE)
print(FMT_CPU + "[CPU] Halting due to exception!" + FMT_NONE)
self.halted = True
if launch_debugger:
launch_debug_session(self)
def run(self, verbose=False):
def run(self, verbose: bool = False):
while not self.halted:
self.step(verbose)
if self.conf.verbosity > 0:
print(FMT_CPU + "[CPU] Program exited with code {}".format(self.exit_code) + FMT_NONE)
print(
FMT_CPU
+ "[CPU] Program exited with code {}".format(self.exit_code)
+ FMT_NONE
)
def setup_stack(self, stack_size=1024 * 4) -> bool:
def setup_stack(self, stack_size: int = 1024 * 4) -> bool:
"""
Create program stack and populate stack pointer
:param stack_size: the size of the required stack, defaults to 4Kib
@ -98,22 +110,26 @@ class UserModeCPU(CPU):
"""
stack_sec = BinaryDataMemorySection(
bytearray(stack_size),
'.stack',
".stack",
None, # FIXME: why does a binary data memory section require a context?
'',
0
"",
0,
)
if not self.mmu.load_section(stack_sec, fixed_position=False):
print(FMT_ERROR + "[CPU] Could not insert stack section!" + FMT_NONE)
return False
self.regs.set('sp', Int32(stack_sec.base + stack_sec.size))
self.regs.set("sp", Int32(stack_sec.base + stack_sec.size))
if self.conf.verbosity > 1:
print(FMT_CPU + "[CPU] Created stack of size {} at 0x{:x}".format(
stack_size, stack_sec.base
) + FMT_NONE)
print(
FMT_CPU
+ "[CPU] Created stack of size {} at 0x{:x}".format(
stack_size, stack_sec.base
)
+ FMT_NONE
)
return True

@ -5,15 +5,23 @@ from riscemu.types import MemorySection, MemoryFlags, T_RelativeAddress
class IOModule(MemorySection, ABC):
def __init__(self, name: str, flags: MemoryFlags, size: int, owner: str = 'system', base: int = 0):
def __init__(
self,
name: str,
flags: MemoryFlags,
size: int,
owner: str = "system",
base: int = 0,
):
super(IOModule, self).__init__(name, flags, size, base, owner, None)
def contains(self, addr, size: int = 0):
return self.base <= addr < self.base + self.size and \
self.base <= addr + size <= self.base + self.size
return (
self.base <= addr < self.base + self.size
and self.base <= addr + size <= self.base + self.size
)
def dump(self, start: T_RelativeAddress, end: Optional[T_RelativeAddress] = None, fmt: str = 'hex',
bytes_per_row: int = 16, rows: int = 10, group: int = 4):
def dump(self, *args, **kwargs):
print(self)
def __repr__(self):

@ -8,7 +8,9 @@ class TextIO(IOModule):
raise InstructionAccessFault(self.base + offset)
def __init__(self, base: int, buflen: int = 128):
super(TextIO, self).__init__('TextIO', MemoryFlags(False, False), buflen + 4, base=base)
super(TextIO, self).__init__(
"TextIO", MemoryFlags(False, False), buflen + 4, base=base
)
self.buff = bytearray(buflen)
self.current_line = ""
@ -23,15 +25,15 @@ class TextIO(IOModule):
self._print()
return
buff_start = addr - 4
self.buff[buff_start:buff_start + size] = data[0:size]
self.buff[buff_start : buff_start + size] = data[0:size]
def _print(self):
buff = self.buff
self.buff = bytearray(self.size)
if b'\x00' in buff:
buff = buff.split(b'\x00')[0]
text = buff.decode('ascii')
if '\n' in text:
if b"\x00" in buff:
buff = buff.split(b"\x00")[0]
text = buff.decode("ascii")
if "\n" in text:
lines = text.split("\n")
lines[0] = self.current_line + lines[0]
for line in lines[:-1]:

@ -8,8 +8,15 @@ from typing import Dict, List, Optional, Union
from .colors import *
from .helpers import align_addr
from .types import Instruction, MemorySection, MemoryFlags, T_AbsoluteAddress, \
Program, InstructionContext, Int32
from .types import (
Instruction,
MemorySection,
MemoryFlags,
T_AbsoluteAddress,
Program,
InstructionContext,
Int32,
)
from .types.exceptions import InvalidAllocationException, MemoryAccessException
@ -80,8 +87,14 @@ class MMU:
"""
sec = self.get_sec_containing(addr)
if sec is None:
print(FMT_MEM + "[MMU] Trying to read instruction form invalid region! (read at {}) ".format(addr)
+ "Have you forgotten an exit syscall or ret statement?" + FMT_NONE)
print(
FMT_MEM
+ "[MMU] Trying to read instruction form invalid region! (read at {}) ".format(
addr
)
+ "Have you forgotten an exit syscall or ret statement?"
+ FMT_NONE
)
raise RuntimeError("No next instruction available!")
return sec.read_ins(addr - sec.base)
@ -94,12 +107,19 @@ class MMU:
:return: The bytearray at addr
"""
if isinstance(addr, Int32):
breakpoint()
addr = addr.unsigned_value
sec = self.get_sec_containing(addr)
if sec is None:
print(FMT_MEM + "[MMU] Trying to read data form invalid region at 0x{:x}! ".format(addr) + FMT_NONE)
raise MemoryAccessException("region is non-initialized!", addr, size, 'read')
print(
FMT_MEM
+ "[MMU] Trying to read data form invalid region at 0x{:x}! ".format(
addr
)
+ FMT_NONE
)
raise MemoryAccessException(
"region is non-initialized!", addr, size, "read"
)
return sec.read(addr - sec.base, size)
def write(self, addr: int, size: int, data: bytearray):
@ -112,8 +132,16 @@ class MMU:
"""
sec = self.get_sec_containing(addr)
if sec is None:
print(FMT_MEM + '[MMU] Invalid write into non-initialized region at 0x{:08X}'.format(addr) + FMT_NONE)
raise MemoryAccessException("region is non-initialized!", addr, size, 'write')
print(
FMT_MEM
+ "[MMU] Invalid write into non-initialized region at 0x{:08X}".format(
addr
)
+ FMT_NONE
)
raise MemoryAccessException(
"region is non-initialized!", addr, size, "write"
)
return sec.write(addr - sec.base, size, data)
@ -127,7 +155,11 @@ class MMU:
"""
sec = self.get_sec_containing(addr)
if sec is None:
print(FMT_MEM + "[MMU] No section containing addr 0x{:08X}".format(addr) + FMT_NONE)
print(
FMT_MEM
+ "[MMU] No section containing addr 0x{:08X}".format(addr)
+ FMT_NONE
)
return
sec.dump(addr - sec.base, *args, **kwargs)
@ -139,10 +171,18 @@ class MMU:
"""
print(FMT_MEM + "[MMU] Lookup for symbol {}:".format(symb) + FMT_NONE)
if symb in self.global_symbols:
print(" Found global symbol {}: 0x{:X}".format(symb, self.global_symbols[symb]))
print(
" Found global symbol {}: 0x{:X}".format(
symb, self.global_symbols[symb]
)
)
for bin in self.programs:
if symb in bin.context.labels:
print(" Found local labels {}: 0x{:X} in {}".format(symb, bin.context.labels[symb], bin.name))
print(
" Found local labels {}: 0x{:X} in {}".format(
symb, bin.context.labels[symb], bin.name
)
)
def read_int(self, addr: int) -> Int32:
return Int32(self.read(addr, 4))
@ -155,17 +195,27 @@ class MMU:
bin = self.get_bin_containing(address)
secs = set(sec.name for sec in bin.sections) if bin else []
elf_markers = {
'__global_pointer$', '_fdata', '_etext', '_gp',
'_bss_start', '_bss_end', '_ftext', '_edata', '_end', '_fbss'
"__global_pointer$",
"_fdata",
"_etext",
"_gp",
"_bss_start",
"_bss_end",
"_ftext",
"_edata",
"_end",
"_fbss",
}
def key(x):
name, val = x
return address - val
best_fit = sorted(filter(lambda x: x[1] <= address, sec.context.labels.items()), key=key)
best_fit = sorted(
filter(lambda x: x[1] <= address, sec.context.labels.items()), key=key
)
best = ('', float('inf'))
best = ("", float("inf"))
for name, val in best_fit:
if address - val < best[1]:
best = (name, val)
@ -179,13 +229,14 @@ class MMU:
if not name:
return "{}:{} + 0x{:x} (0x{:x})".format(
sec.owner, sec.name,
address - sec.base, address
sec.owner, sec.name, address - sec.base, address
)
return str('{}:{} at {} (0x{:0x}) + 0x{:0x}'.format(
sec.owner, sec.name, name, val, address - val
))
return str(
"{}:{} at {} (0x{:0x}) + 0x{:0x}".format(
sec.owner, sec.name, name, val, address - val
)
)
def has_continous_free_region(self, start: int, end: int) -> bool:
# if we have no sections we are all good
@ -211,13 +262,24 @@ class MMU:
def load_program(self, program: Program, align_to: int = 4):
if program.base is not None:
if not self.has_continous_free_region(program.base, program.base + program.size):
print(FMT_MEM + "Cannot load program {} into desired space (0x{:0x}-0x{:0x}), area occupied.".format(
program.name, program.base, program.base + program.size
) + FMT_NONE)
raise InvalidAllocationException("Area occupied".format(
program.name, program.base, program.base + program.size
), program.name, program.size, MemoryFlags(False, True))
if not self.has_continous_free_region(
program.base, program.base + program.size
):
print(
FMT_MEM
+ "Cannot load program {} into desired space (0x{:0x}-0x{:0x}), area occupied.".format(
program.name, program.base, program.base + program.size
)
+ FMT_NONE
)
raise InvalidAllocationException(
"Area occupied".format(
program.name, program.base, program.base + program.size
),
program.name,
program.size,
MemoryFlags(False, True),
)
at_addr = program.base
else:
@ -245,7 +307,12 @@ class MMU:
self.sections.append(sec)
self._update_state()
else:
print(FMT_MEM + '[MMU] Cannot place section {} at {}, space is occupied!'.format(sec, sec.base))
print(
FMT_MEM
+ "[MMU] Cannot place section {} at {}, space is occupied!".format(
sec, sec.base
)
)
return False
else:
at_addr = align_addr(self.get_guaranteed_free_address(), 8)
@ -270,8 +337,7 @@ class MMU:
def __repr__(self):
return "{}(\n\t{}\n)".format(
self.__class__.__name__,
"\n\t".join(repr(x) for x in self.programs)
self.__class__.__name__, "\n\t".join(repr(x) for x in self.programs)
)
def context_for(self, addr: T_AbsoluteAddress) -> InstructionContext:

@ -8,8 +8,17 @@ This package aims at providing an all-round usable RISC-V emulator and debugger
It contains everything needed to run assembly files, so you don't need any custom compilers or toolchains
"""
from .types.exceptions import RiscemuBaseException, LaunchDebuggerException, InvalidSyscallException, LinkerException, \
ParseException, NumberFormatException, InvalidRegisterException, MemoryAccessException, OutOfMemoryException
from .types.exceptions import (
RiscemuBaseException,
LaunchDebuggerException,
InvalidSyscallException,
LinkerException,
ParseException,
NumberFormatException,
InvalidRegisterException,
MemoryAccessException,
OutOfMemoryException,
)
from .instructions import *
@ -25,4 +34,4 @@ from .parser import tokenize, parse_tokens, AssemblyFileLoader
__author__ = "Anton Lydike <Anton@Lydike.com>"
__copyright__ = "Copyright 2022 Anton Lydike"
__version__ = '2.0.5'
__version__ = "2.0.5"

@ -8,7 +8,7 @@ This file holds the logic for starting the emulator from the CLI
from riscemu import RiscemuBaseException, __copyright__, __version__
from riscemu.CPU import UserModeCPU
if __name__ == '__main__':
if __name__ == "__main__":
from .config import RunConfig
from .instructions import InstructionSetDict
from .colors import FMT_BOLD, FMT_MAGENTA
@ -18,11 +18,12 @@ if __name__ == '__main__':
all_ins_names = list(InstructionSetDict.keys())
if '--version' in sys.argv:
print("riscemu version {}\n{}\n\nAvailable ISA: {}".format(
__version__, __copyright__,
", ".join(InstructionSetDict.keys())
))
if "--version" in sys.argv:
print(
"riscemu version {}\n{}\n\nAvailable ISA: {}".format(
__version__, __copyright__, ", ".join(InstructionSetDict.keys())
)
)
sys.exit()
class OptionStringAction(argparse.Action):
@ -49,56 +50,105 @@ if __name__ == '__main__':
d = {}
if not self.omit_empty:
d.update(self.keys)
for x in values.split(','):
for x in values.split(","):
if x in self.keys:
d[x] = True
else:
raise ValueError('Invalid parameter supplied: ' + x)
raise ValueError("Invalid parameter supplied: " + x)
setattr(namespace, self.dest, d)
parser = argparse.ArgumentParser(
description="RISC-V Userspace parser and emulator",
prog="riscemu",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"files",
metavar="file.asm",
type=str,
nargs="+",
help="The assembly files to load, the last one will be run",
)
parser = argparse.ArgumentParser(description='RISC-V Userspace parser and emulator', prog='riscemu',
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('files', metavar='file.asm', type=str, nargs='+',
help='The assembly files to load, the last one will be run')
parser.add_argument('--options', '-o', action=OptionStringAction,
keys=('disable_debug', 'no_syscall_symbols', 'fail_on_ex', 'add_accept_imm', 'unlimited_regs'),
help="""Toggle options. Available options are:
parser.add_argument(
"--options",
"-o",
action=OptionStringAction,
keys=(
"disable_debug",
"no_syscall_symbols",
"fail_on_ex",
"add_accept_imm",
"unlimited_regs",
),
help="""Toggle options. Available options are:
disable_debug: Disable ebreak instructions
no_syscall_symbols: Don't add symbols for SCALL_EXIT and others
fail_on_ex: If set, exceptions won't trigger the debugger
add_accept_imm: Accept "add rd, rs, imm" instruction (instead of addi)
unlimited_regs: Allow an unlimited number of registers""")
unlimited_regs: Allow an unlimited number of registers""",
)
parser.add_argument('--syscall-opts', '-so', action=OptionStringAction,
keys=('fs_access', 'disable_input'))
parser.add_argument(
"--syscall-opts",
"-so",
action=OptionStringAction,
keys=("fs_access", "disable_input"),
)
parser.add_argument('--instruction-sets', '-is', action=OptionStringAction,
help="Instruction sets to load, available are: {}. All are enabled by default"
.format(", ".join(all_ins_names)), keys={k: True for k in all_ins_names}, omit_empty=True)
parser.add_argument(
"--instruction-sets",
"-is",
action=OptionStringAction,
help="Instruction sets to load, available are: {}. All are enabled by default".format(
", ".join(all_ins_names)
),
keys={k: True for k in all_ins_names},
omit_empty=True,
)
parser.add_argument('--stack_size', type=int, help='Stack size of loaded programs, defaults to 8MB', nargs='?')
parser.add_argument(
"--stack_size",
type=int,
help="Stack size of loaded programs, defaults to 8MB",
nargs="?",
)
parser.add_argument('-v', '--verbose', help="Verbosity level (can be used multiple times)", action='count',
default=0)
parser.add_argument(
"-v",
"--verbose",
help="Verbosity level (can be used multiple times)",
action="count",
default=0,
)
parser.add_argument('--interactive', help="Launch the interactive debugger instantly instead of loading any "
"programs", action='store_true')
parser.add_argument(
"--interactive",
help="Launch the interactive debugger instantly instead of loading any "
"programs",
action="store_true",
)
parser.add_argument(
"--ignore-exit-code",
help="Ignore exit code of the program and always return 0 if the program ran to completion.",
action="store_true",
default=False,
)
args = parser.parse_args()
# create a RunConfig from the cli args
cfg_dict = dict(
stack_size=args.stack_size,
debug_instruction=not args.options['disable_debug'],
include_scall_symbols=not args.options['no_syscall_symbols'],
debug_on_exception=not args.options['fail_on_ex'],
add_accept_imm=args.options['add_accept_imm'],
unlimited_registers=args.options['unlimited_regs'],
scall_fs=args.syscall_opts['fs_access'],
scall_input=not args.syscall_opts['disable_input'],
verbosity=args.verbose
debug_instruction=not args.options["disable_debug"],
include_scall_symbols=not args.options["no_syscall_symbols"],
debug_on_exception=not args.options["fail_on_ex"],
add_accept_imm=args.options["add_accept_imm"],
unlimited_registers=args.options["unlimited_regs"],
scall_fs=args.syscall_opts["fs_access"],
scall_input=not args.syscall_opts["disable_input"],
verbosity=args.verbose,
)
for k, v in dict(cfg_dict).items():
if v is None:
@ -106,15 +156,13 @@ unlimited_regs: Allow an unlimited number of registers""")
cfg = RunConfig(**cfg_dict)
if not hasattr(args, 'ins'):
setattr(args, 'ins', {k: True for k in all_ins_names})
if not hasattr(args, "ins"):
setattr(args, "ins", {k: True for k in all_ins_names})
FMT_PRINT = FMT_BOLD + FMT_MAGENTA
# parse required instruction sets
ins_to_load = [
InstructionSetDict[name] for name, b in args.ins.items() if b
]
ins_to_load = [InstructionSetDict[name] for name, b in args.ins.items() if b]
try:
cpu = UserModeCPU(ins_to_load, cfg)
@ -129,7 +177,7 @@ unlimited_regs: Allow an unlimited number of registers""")
# launch the last loaded program
cpu.launch(cpu.mmu.programs[-1], verbose=cfg.verbosity > 1)
sys.exit(cpu.exit_code)
sys.exit(cpu.exit_code if not args.ignore_exit_code else 0)
except RiscemuBaseException as e:
print("Error: {}".format(e.message()))

@ -6,10 +6,17 @@ from .colors import FMT_PARSE, FMT_NONE
from riscemu.types.exceptions import ParseException, ASSERT_LEN
from .helpers import parse_numeric_argument, align_addr, get_section_base_name
from .tokenizer import Token
from .types import Program, T_RelativeAddress, InstructionContext, Instruction, BinaryDataMemorySection, \
InstructionMemorySection, Int32
INSTRUCTION_SECTION_NAMES = ('.text', '.init', '.fini')
from .types import (
Program,
T_RelativeAddress,
InstructionContext,
Instruction,
BinaryDataMemorySection,
InstructionMemorySection,
Int32,
)
INSTRUCTION_SECTION_NAMES = (".text", ".init", ".fini")
"""
A tuple containing all section names which contain executable code (instead of data)
@ -47,8 +54,7 @@ class CurrentSection:
def __repr__(self):
return "{}(name={},data={},type={})".format(
self.__class__.__name__, self.name,
self.data, self.type.name
self.__class__.__name__, self.name, self.data, self.type.name
)
@ -72,12 +78,20 @@ class ParseContext:
if self.section.type == MemorySectionType.Data:
section = BinaryDataMemorySection(
self.section.data, self.section.name, self.context, self.program.name, self.section.base
self.section.data,
self.section.name,
self.context,
self.program.name,
self.section.base,
)
self.program.add_section(section)
elif self.section.type == MemorySectionType.Instructions:
section = InstructionMemorySection(
self.section.data, self.section.name, self.context, self.program.name, self.section.base
self.section.data,
self.section.name,
self.context,
self.program.name,
self.section.base,
)
self.program.add_section(section)
@ -89,7 +103,9 @@ class ParseContext:
self._finalize_section()
self.section = CurrentSection(name, type, base)
def add_label(self, name: str, value: int, is_global: bool = False, is_relative: bool = False):
def add_label(
self, name: str, value: int, is_global: bool = False, is_relative: bool = False
):
self.context.labels[name] = value
if is_global:
self.program.global_labels.add(name)
@ -109,10 +125,16 @@ class ParseContext:
def ASSERT_IN_SECTION_TYPE(context: ParseContext, type: MemorySectionType):
if context.section is None:
raise ParseException('Error, expected to be in {} section, but no section is present...'.format(type.name))
raise ParseException(
"Error, expected to be in {} section, but no section is present...".format(
type.name
)
)
if context.section.type != type:
raise ParseException(
'Error, expected to be in {} section, but currently in {}...'.format(type.name, context.section)
"Error, expected to be in {} section, but currently in {}...".format(
type.name, context.section
)
)
@ -174,7 +196,9 @@ class AssemblerDirectives:
cls.add_bytes(size, bytearray(size), context)
@classmethod
def add_bytes(cls, size: int, content: Union[None, int, bytearray], context: ParseContext):
def add_bytes(
cls, size: int, content: Union[None, int, bytearray], context: ParseContext
):
ASSERT_IN_SECTION_TYPE(context, MemorySectionType.Data)
if content is None:
@ -187,9 +211,9 @@ class AssemblerDirectives:
@classmethod
def add_text(cls, text: str, context: ParseContext, zero_terminate: bool = True):
# replace '\t' and '\n' escape sequences
text = text.replace('\\n', '\n').replace('\\t', '\t')
text = text.replace("\\n", "\n").replace("\\t", "\t")
encoded_bytes = bytearray(text.encode('ascii'))
encoded_bytes = bytearray(text.encode("ascii"))
if zero_terminate:
encoded_bytes += bytearray(1)
cls.add_bytes(len(encoded_bytes), encoded_bytes, context)
@ -197,24 +221,36 @@ class AssemblerDirectives:
@classmethod
def handle_instruction(cls, token: Token, args: Tuple[str], context: ParseContext):
op = token.value[1:]
if hasattr(cls, 'op_' + op):
getattr(cls, 'op_' + op)(token, args, context)
elif op in ('text', 'data', 'rodata', 'bss', 'sbss'):
if hasattr(cls, "op_" + op):
getattr(cls, "op_" + op)(token, args, context)
elif op in ("text", "data", "rodata", "bss", "sbss"):
cls.op_section(token, (token.value,), context)
elif op in ('string', 'asciiz', 'asciz', 'ascii'):
elif op in ("string", "asciiz", "asciz", "ascii"):
ASSERT_LEN(args, 1)
cls.add_text(args[0], context, op == 'ascii')
cls.add_text(args[0], context, op == "ascii")
elif op in DATA_OP_SIZES:
size = DATA_OP_SIZES[op]
for arg in args:
cls.add_bytes(size, parse_numeric_argument(arg), context)
else:
print(FMT_PARSE + "Unknown assembler directive: {} {} in {}".format(token, args, context) + FMT_NONE)
print(
FMT_PARSE
+ "Unknown assembler directive: {} {} in {}".format(
token, args, context
)
+ FMT_NONE
)
DATA_OP_SIZES = {
'byte': 1,
'2byte': 2, 'half': 2, 'short': 2,
'4byte': 4, 'word': 4, 'long': 4,
'8byte': 8, 'dword': 8, 'quad': 8,
"byte": 1,
"2byte": 2,
"half": 2,
"short": 2,
"4byte": 4,
"word": 4,
"long": 4,
"8byte": 8,
"dword": 8,
"quad": 8,
}

@ -6,18 +6,18 @@ SPDX-License-Identifier: MIT
# Colors
FMT_RED = '\033[31m'
FMT_ORANGE = '\033[33m'
FMT_GRAY = '\033[37m'
FMT_CYAN = '\033[36m'
FMT_GREEN = '\033[32m'
FMT_MAGENTA = '\033[35m'
FMT_BLUE = '\033[34m'
FMT_YELLOW = '\033[93m'
FMT_RED = "\033[31m"
FMT_ORANGE = "\033[33m"
FMT_GRAY = "\033[37m"
FMT_CYAN = "\033[36m"
FMT_GREEN = "\033[32m"
FMT_MAGENTA = "\033[35m"
FMT_BLUE = "\033[34m"
FMT_YELLOW = "\033[93m"
FMT_BOLD = '\033[1m'
FMT_NONE = '\033[0m'
FMT_UNDERLINE = '\033[4m'
FMT_BOLD = "\033[1m"
FMT_NONE = "\033[0m"
FMT_UNDERLINE = "\033[4m"
FMT_ERROR = FMT_RED + FMT_BOLD
@ -26,4 +26,4 @@ FMT_PARSE = FMT_CYAN + FMT_BOLD
FMT_CPU = FMT_BLUE + FMT_BOLD
FMT_SYSCALL = FMT_YELLOW + FMT_BOLD
FMT_DEBUG = FMT_MAGENTA + FMT_BOLD
FMT_CSR = FMT_ORANGE + FMT_BOLD
FMT_CSR = FMT_ORANGE + FMT_BOLD

@ -11,10 +11,10 @@ from .helpers import *
if typing.TYPE_CHECKING:
from riscemu import CPU, Registers
HIST_FILE = os.path.join(os.path.expanduser('~'), '.riscemu_history')
HIST_FILE = os.path.join(os.path.expanduser("~"), ".riscemu_history")
def launch_debug_session(cpu: 'CPU', prompt=""):
def launch_debug_session(cpu: "CPU", prompt=""):
if cpu.debugger_active:
return
import code
@ -39,7 +39,7 @@ def launch_debug_session(cpu: 'CPU', prompt=""):
mmu.dump(what, *args, **kwargs)
def dump_stack(*args, **kwargs):
mmu.dump(regs.get('sp'), *args, **kwargs)
mmu.dump(regs.get("sp"), *args, **kwargs)
def ins():
print("Current instruction at 0x{:08X}:".format(cpu.pc))
@ -51,11 +51,7 @@ def launch_debug_session(cpu: 'CPU', prompt=""):
return
context = mmu.context_for(cpu.pc)
ins = SimpleInstruction(
name,
tuple(args),
context,
cpu.pc)
ins = SimpleInstruction(name, tuple(args), context, cpu.pc)
print(FMT_DEBUG + "Running instruction {}".format(ins) + FMT_NONE)
cpu.run_instruction(ins)
@ -63,7 +59,7 @@ def launch_debug_session(cpu: 'CPU', prompt=""):
try:
cpu.run(verbose)
except LaunchDebuggerException:
print(FMT_DEBUG + 'Returning to debugger...')
print(FMT_DEBUG + "Returning to debugger...")
return
def step():
@ -92,4 +88,3 @@ def launch_debug_session(cpu: 'CPU', prompt=""):
finally:
cpu.debugger_active = False
readline.write_history_file(HIST_FILE)

@ -1,2 +1,2 @@
from .decoder import decode, RISCV_REGS
from .formatter import format_ins
from .formatter import format_ins

@ -1,4 +1,4 @@
if __name__ == '__main__':
if __name__ == "__main__":
import code
import readline
import rlcompleter
@ -14,4 +14,6 @@ if __name__ == '__main__':
readline.set_completer(rlcompleter.Completer(sess_vars).complete)
readline.set_completer(rlcompleter.Completer(sess_vars).complete)
readline.parse_and_bind("tab: complete")
code.InteractiveConsole(sess_vars).interact(banner="Interaktive decoding session started...", exitmsg="Closing...")
code.InteractiveConsole(sess_vars).interact(
banner="Interaktive decoding session started...", exitmsg="Closing..."
)

@ -5,13 +5,14 @@ from typing import Tuple, List
def print_ins(ins: int):
print(" f7 rs2 rs1 f3 rd op")
print(
f"0b{ins >> 25 :07b}_{(ins >> 20) & 0b11111:05b}_{(ins >> 15) & 0b11111:05b}_{(ins >> 12) & 0b111:03b}_{(ins >> 7) & 0b11111:05b}_{ins & 0b1111111:07b}");
f"0b{ins >> 25 :07b}_{(ins >> 20) & 0b11111:05b}_{(ins >> 15) & 0b11111:05b}_{(ins >> 12) & 0b111:03b}_{(ins >> 7) & 0b11111:05b}_{ins & 0b1111111:07b}"
)
STATIC_INSN: Dict[int, Tuple[str, List[int], int]] = {
0x00000013: ("nop", [], 0x00000013),
0x00008067: ("ret", [], 0x00008067),
0xfe010113: ("addi", [2, 2, -32], 0xfe010113),
0xFE010113: ("addi", [2, 2, -32], 0xFE010113),
0x02010113: ("addi", [2, 2, 32], 0x02010113),
0x00100073: ("ebreak", [], 0x00100073),
0x00000073: ("ecall", [], 0x00000073),
@ -23,7 +24,7 @@ STATIC_INSN: Dict[int, Tuple[str, List[int], int]] = {
def int_from_ins(insn: bytearray):
return int.from_bytes(insn, 'little')
return int.from_bytes(insn, "little")
def name_from_insn(ins: int):
@ -45,7 +46,7 @@ def name_from_insn(ins: int):
if isinstance(dec, str):
return dec
if opcode == 0x1c and fun3 == 0:
if opcode == 0x1C and fun3 == 0:
# we have ecall/ebreak
token = imm110(ins)
if token in dec:

@ -1,6 +1,7 @@
from typing import Dict, Callable, List, Union
from .regs import RISCV_REGS
def op(ins: int):
return (ins >> 2) & 31
@ -46,7 +47,12 @@ def imm_b(ins: int):
lower = rd(ins)
higher = funct7(ins)
num = (lower & 0b11110) + ((higher & 0b0111111) << 5) + ((lower & 1) << 11) + ((higher >> 6) << 12)
num = (
(lower & 0b11110)
+ ((higher & 0b0111111) << 5)
+ ((lower & 1) << 11)
+ ((higher >> 6) << 12)
)
return sign_extend(num, 13)
@ -56,10 +62,11 @@ def imm_u(ins: int):
def imm_j(ins: int):
return sign_extend(
(((ins >> 21) & 0b1111111111) << 1) +
(((ins >> 20) & 1) << 11) +
(((ins >> 12) & 0b11111111) << 12) +
(((ins >> 31) & 1) << 20), 21
(((ins >> 21) & 0b1111111111) << 1)
+ (((ins >> 20) & 1) << 11)
+ (((ins >> 12) & 0b11111111) << 12)
+ (((ins >> 31) & 1) << 20),
21,
)
@ -111,7 +118,7 @@ INSTRUCTION_ARGS_DECODER: Dict[int, Callable[[int], List[int]]] = {
0x0D: decode_u,
0x18: decode_b,
0x19: decode_i,
0x1b: decode_j,
0x1c: decode_i_unsigned,
0b1011: decode_r
0x1B: decode_j,
0x1C: decode_i_unsigned,
0b1011: decode_r,
}

@ -1,5 +1,15 @@
from .formats import INSTRUCTION_ARGS_DECODER, op, decode_i, decode_r, decode_u, decode_b, decode_j, decode_s, \
decode_i_shamt, decode_i_unsigned
from .formats import (
INSTRUCTION_ARGS_DECODER,
op,
decode_i,
decode_r,
decode_u,
decode_b,
decode_j,
decode_s,
decode_i_shamt,
decode_i_unsigned,
)
from .regs import RISCV_REGS
@ -9,9 +19,9 @@ def int_to_hex(num: int):
return f"0x{num:x}"
def format_ins(ins: int, name: str, fmt: str = 'int'):
def format_ins(ins: int, name: str, fmt: str = "int"):
opcode = op(ins)
if fmt == 'hex':
if fmt == "hex":
fmt = int_to_hex
else:
fmt = str
@ -20,7 +30,7 @@ def format_ins(ins: int, name: str, fmt: str = 'int'):
return f"{name} <unknown op>"
decoder = INSTRUCTION_ARGS_DECODER[opcode]
if name in ('ecall', 'ebreak', 'mret', 'sret', 'uret'):
if name in ("ecall", "ebreak", "mret", "sret", "uret"):
return name
if opcode in (0x8, 0x0):
r1, r2, imm = decoder(ins)

@ -4,7 +4,7 @@ from .formats import *
tbl = lambda: defaultdict(tbl)
RV32 = tbl()
RV32[0x1b] = "jal"
RV32[0x1B] = "jal"
RV32[0x0D] = "lui"
RV32[0x05] = "auipc"
RV32[0x19][0] = "jalr"
@ -36,26 +36,26 @@ RV32[0x08][0] = "sb"
RV32[0x08][1] = "sh"
RV32[0x08][2] = "sw"
RV32[0x1c][1] = "csrrw"
RV32[0x1c][2] = "csrrs"
RV32[0x1c][3] = "csrrc"
RV32[0x1c][5] = "csrrwi"
RV32[0x1c][6] = "csrrsi"
RV32[0x1c][7] = "csrrci"
RV32[0x1C][1] = "csrrw"
RV32[0x1C][2] = "csrrs"
RV32[0x1C][3] = "csrrc"
RV32[0x1C][5] = "csrrwi"
RV32[0x1C][6] = "csrrsi"
RV32[0x1C][7] = "csrrci"
RV32[0x1c][0][0] = "ecall"
RV32[0x1c][0][1] = "ebreak"
RV32[0x1C][0][0] = "ecall"
RV32[0x1C][0][1] = "ebreak"
RV32[0x0C][0][0] = "add"
RV32[0x0C][0][0] = "add"
RV32[0x0C][0][32] = "sub"
RV32[0x0C][1][0] = "sll"
RV32[0x0C][2][0] = "slt"
RV32[0x0C][3][0] = "sltu"
RV32[0x0C][4][0] = "xor"
RV32[0x0C][5][0] = "srl"
RV32[0x0C][1][0] = "sll"
RV32[0x0C][2][0] = "slt"
RV32[0x0C][3][0] = "sltu"
RV32[0x0C][4][0] = "xor"
RV32[0x0C][5][0] = "srl"
RV32[0x0C][5][32] = "sra"
RV32[0x0C][6][0] = "or"
RV32[0x0C][7][0] = "and"
RV32[0x0C][6][0] = "or"
RV32[0x0C][7][0] = "and"
# rv32m
RV32[0x0C][0][1] = "mul"

@ -1,6 +1,34 @@
RISCV_REGS = [
'zero', 'ra', 'sp', 'gp', 'tp', 't0', 't1', 't2',
's0', 's1', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7',
's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11',
't3', 't4', 't5', 't6'
"zero",
"ra",
"sp",
"gp",
"tp",
"t0",
"t1",
"t2",
"s0",
"s1",
"a0",
"a1",
"a2",
"a3",
"a4",
"a5",
"a6",
"a7",
"s2",
"s3",
"s4",
"s5",
"s6",
"s7",
"s8",
"s9",
"s10",
"s11",
"t3",
"t4",
"t5",
"t6",
]

@ -14,6 +14,8 @@ from .types.exceptions import *
def align_addr(addr: int, to_bytes: int = 8) -> int:
"""
align an address to `to_bytes` (meaning addr & to_bytes = 0)
This will increase the address
"""
return addr + (-addr % to_bytes)
@ -23,16 +25,19 @@ def parse_numeric_argument(arg: str) -> int:
parse hex or int strings
"""
try:
if arg.lower().startswith('0x'):
if arg.lower().startswith("0x"):
return int(arg, 16)
return int(arg)
except ValueError as ex:
raise ParseException('Invalid immediate argument \"{}\", maybe missing symbol?'.format(arg), (arg, ex))
raise ParseException(
'Invalid immediate argument "{}", maybe missing symbol?'.format(arg),
(arg, ex),
)
def create_chunks(my_list, chunk_size):
"""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]]"""
return [my_list[i:i + chunk_size] for i in range(0, len(my_list), chunk_size)]
return [my_list[i : i + chunk_size] for i in range(0, len(my_list), chunk_size)]
def apply_highlight(item, ind, hi_ind):
@ -45,26 +50,31 @@ def apply_highlight(item, ind, hi_ind):
def highlight_in_list(items, hi_ind, joiner=" "):
return joiner.join([apply_highlight(item, i, hi_ind) for i, item in enumerate(items)])
return joiner.join(
[apply_highlight(item, i, hi_ind) for i, item in enumerate(items)]
)
def format_bytes(byte_arr: bytearray, fmt: str, group: int = 1, highlight: int = -1):
"""Format byte array as per fmt. Group into groups of size `group`, and highlight index `highlight`."""
chunks = create_chunks(byte_arr, group)
if fmt == 'hex':
return highlight_in_list(['0x{}'.format(ch.hex()) for ch in chunks], highlight)
if fmt == 'int':
if fmt == "hex":
return highlight_in_list(["0x{}".format(ch.hex()) for ch in chunks], highlight)
if fmt == "int":
spc = str(ceil(log10(2 ** (group * 8 - 1))) + 1)
return highlight_in_list([('{:0' + spc + 'd}').format(Int32(ch)) for ch in chunks], highlight)
if fmt == 'uint':
return highlight_in_list(
[("{:0" + spc + "d}").format(Int32(ch)) for ch in chunks], highlight
)
if fmt == "uint":
spc = str(ceil(log10(2 ** (group * 8))))
return highlight_in_list([('{:0' + spc + 'd}').format(UInt32(ch)) for ch in chunks],
highlight)
if fmt == 'char':
return highlight_in_list(
[("{:0" + spc + "d}").format(UInt32(ch)) for ch in chunks], highlight
)
if fmt == "char":
return highlight_in_list((repr(chr(b))[1:-1] for b in byte_arr), highlight, "")
T = TypeVar('T')
T = TypeVar("T")
class Peekable(Generic[T], Iterator[T]):
@ -98,6 +108,10 @@ class Peekable(Generic[T], Iterator[T]):
def get_section_base_name(section_name: str) -> str:
if '.' not in section_name:
print(FMT_PARSE + f"Invalid section {section_name}, not starting with a dot!" + FMT_NONE)
return '.' + section_name.split('.')[1]
if "." not in section_name:
print(
FMT_PARSE
+ f"Invalid section {section_name}, not starting with a dot!"
+ FMT_NONE
)
return "." + section_name.split(".")[1]

@ -10,52 +10,52 @@ class RV32A(InstructionSet):
for this?
"""
def instruction_lr_w(self, ins: 'Instruction'):
def instruction_lr_w(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_sc_w(self, ins: 'Instruction'):
def instruction_sc_w(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_amoswap_w(self, ins: 'Instruction'):
def instruction_amoswap_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
if dest == 'zero':
if dest == "zero":
self.mmu.write(addr, val.to_bytes())
else:
old = Int32(self.mmu.read(addr, 4))
self.mmu.write(addr, val.to_bytes())
self.regs.set(dest, old)
def instruction_amoadd_w(self, ins: 'Instruction'):
def instruction_amoadd_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
old = Int32(self.mmu.read(addr, 4))
self.mmu.write(addr, (old + val).to_bytes(4))
self.regs.set(dest, old)
def instruction_amoand_w(self, ins: 'Instruction'):
def instruction_amoand_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
old = Int32(self.mmu.read(addr, 4))
self.mmu.write(addr, (old & val).to_bytes(4))
self.regs.set(dest, old)
def instruction_amoor_w(self, ins: 'Instruction'):
def instruction_amoor_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
old = Int32(self.mmu.read(addr, 4))
self.mmu.write(addr, (old | val).to_bytes(4))
self.regs.set(dest, old)
def instruction_amoxor_w(self, ins: 'Instruction'):
def instruction_amoxor_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
old = Int32(self.mmu.read(addr, 4))
self.mmu.write(addr, (old ^ val).to_bytes(4))
self.regs.set(dest, old)
def instruction_amomax_w(self, ins: 'Instruction'):
def instruction_amomax_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
old = Int32(self.mmu.read(addr, 4))
self.mmu.write(addr, max(old, val).to_bytes(4))
self.regs.set(dest, old)
def instruction_amomaxu_w(self, ins: 'Instruction'):
def instruction_amomaxu_w(self, ins: "Instruction"):
val: UInt32
dest, addr, val = self.parse_rd_rs_rs(ins, signed=False)
old = UInt32(self.mmu.read(addr, 4))
@ -63,13 +63,13 @@ class RV32A(InstructionSet):
self.mmu.write(addr, max(old, val).to_bytes())
self.regs.set(dest, old)
def instruction_amomin_w(self, ins: 'Instruction'):
def instruction_amomin_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
old = Int32(self.mmu.read(addr, 4))
self.mmu.write(addr, min(old, val).to_bytes(4))
self.regs.set(dest, old)
def instruction_amominu_w(self, ins: 'Instruction'):
def instruction_amominu_w(self, ins: "Instruction"):
val: UInt32
dest, addr, val = self.parse_rd_rs_rs(ins, signed=False)
old = UInt32(self.mmu.read(addr, 4))

@ -22,241 +22,186 @@ class RV32I(InstructionSet):
See https://maxvytech.com/images/RV32I-11-2018.pdf for a more detailed overview
"""
def instruction_lb(self, ins: 'Instruction'):
def instruction_lb(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.regs.set(rd, Int32.sign_extend(self.mmu.read(addr.unsigned_value, 1), 8))
def instruction_lh(self, ins: 'Instruction'):
def instruction_lh(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.regs.set(rd, Int32.sign_extend(self.mmu.read(addr.unsigned_value, 2), 16))
def instruction_lw(self, ins: 'Instruction'):
def instruction_lw(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.regs.set(rd, Int32(self.mmu.read(addr.unsigned_value, 4)))
def instruction_lbu(self, ins: 'Instruction'):
def instruction_lbu(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.regs.set(rd, Int32(self.mmu.read(addr.unsigned_value, 1)))
def instruction_lhu(self, ins: 'Instruction'):
def instruction_lhu(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.regs.set(rd, Int32(self.mmu.read(addr.unsigned_value, 2)))
def instruction_sb(self, ins: 'Instruction'):
def instruction_sb(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.mmu.write(addr.unsigned_value, 1, self.regs.get(rd).to_bytes(1))
def instruction_sh(self, ins: 'Instruction'):
def instruction_sh(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.mmu.write(addr.unsigned_value, 2, self.regs.get(rd).to_bytes(2))
def instruction_sw(self, ins: 'Instruction'):
def instruction_sw(self, ins: "Instruction"):
rd, addr = self.parse_mem_ins(ins)
self.mmu.write(addr.unsigned_value, 4, self.regs.get(rd).to_bytes(4))
def instruction_sll(self, ins: 'Instruction'):
def instruction_sll(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
dst = ins.get_reg(0)
src1 = ins.get_reg(1)
src2 = ins.get_reg(2)
self.regs.set(
dst,
self.regs.get(src1) << (self.regs.get(src2) & 0b11111)
)
self.regs.set(dst, self.regs.get(src1) << (self.regs.get(src2) & 0b11111))
def instruction_slli(self, ins: 'Instruction'):
def instruction_slli(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
dst = ins.get_reg(0)
src1 = ins.get_reg(1)
imm = ins.get_imm(2)
self.regs.set(
dst,
self.regs.get(src1) << (imm & 0b11111)
)
self.regs.set(dst, self.regs.get(src1) << (imm & 0b11111))
def instruction_srl(self, ins: 'Instruction'):
def instruction_srl(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
dst = ins.get_reg(0)
src1 = ins.get_reg(1)
src2 = ins.get_reg(2)
self.regs.set(
dst,
self.regs.get(src1).shift_right_logical(self.regs.get(src2) & 0b11111)
dst, self.regs.get(src1).shift_right_logical(self.regs.get(src2) & 0b11111)
)
def instruction_srli(self, ins: 'Instruction'):
def instruction_srli(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
dst = ins.get_reg(0)
src1 = ins.get_reg(1)
imm = ins.get_imm(2)
self.regs.set(
dst,
self.regs.get(src1).shift_right_logical(imm & 0b11111)
)
self.regs.set(dst, self.regs.get(src1).shift_right_logical(imm & 0b11111))
def instruction_sra(self, ins: 'Instruction'):
def instruction_sra(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
dst = ins.get_reg(0)
src1 = ins.get_reg(1)
src2 = ins.get_reg(2)
self.regs.set(
dst,
self.regs.get(src1) >> (self.regs.get(src2) & 0b11111)
)
self.regs.set(dst, self.regs.get(src1) >> (self.regs.get(src2) & 0b11111))
def instruction_srai(self, ins: 'Instruction'):
def instruction_srai(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
dst = ins.get_reg(0)
src1 = ins.get_reg(1)
imm = ins.get_imm(2)
self.regs.set(
dst,
self.regs.get(src1) >> (imm & 0b11111)
)
self.regs.set(dst, self.regs.get(src1) >> (imm & 0b11111))
def instruction_add(self, ins: 'Instruction'):
def instruction_add(self, ins: "Instruction"):
# FIXME: once configuration is figured out, add flag to support immediate arg in add instruction
dst, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
dst,
rs1 + rs2
)
self.regs.set(dst, rs1 + rs2)
def instruction_addi(self, ins: 'Instruction'):
def instruction_addi(self, ins: "Instruction"):
dst, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(
dst,
rs1 + imm
)
self.regs.set(dst, rs1 + imm)
def instruction_sub(self, ins: 'Instruction'):
def instruction_sub(self, ins: "Instruction"):
dst, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
dst,
rs1 - rs2
)
self.regs.set(dst, rs1 - rs2)
def instruction_lui(self, ins: 'Instruction'):
def instruction_lui(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
imm = UInt32(ins.get_imm(1) << 12)
self.regs.set(reg, Int32(imm))
def instruction_auipc(self, ins: 'Instruction'):
def instruction_auipc(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
imm = UInt32(ins.get_imm(1) << 12)
self.regs.set(reg, imm.signed() + self.pc)
def instruction_xor(self, ins: 'Instruction'):
def instruction_xor(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
rs1 ^ rs2
)
self.regs.set(rd, rs1 ^ rs2)
def instruction_xori(self, ins: 'Instruction'):
def instruction_xori(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(
rd,
rs1 ^ imm
)
self.regs.set(rd, rs1 ^ imm)
def instruction_or(self, ins: 'Instruction'):
def instruction_or(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
rs1 | rs2
)
self.regs.set(rd, rs1 | rs2)
def instruction_ori(self, ins: 'Instruction'):
def instruction_ori(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(
rd,
rs1 | imm
)
self.regs.set(rd, rs1 | imm)
def instruction_and(self, ins: 'Instruction'):
def instruction_and(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
rs1 & rs2
)
self.regs.set(rd, rs1 & rs2)
def instruction_andi(self, ins: 'Instruction'):
def instruction_andi(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(
rd,
rs1 & imm
)
self.regs.set(rd, rs1 & imm)
def instruction_slt(self, ins: 'Instruction'):
def instruction_slt(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
Int32(int(rs1 < rs2))
)
self.regs.set(rd, Int32(int(rs1 < rs2)))
def instruction_slti(self, ins: 'Instruction'):
def instruction_slti(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(
rd,
Int32(int(rs1 < imm))
)
self.regs.set(rd, Int32(int(rs1 < imm)))
def instruction_sltu(self, ins: 'Instruction'):
def instruction_sltu(self, ins: "Instruction"):
dst, rs1, rs2 = self.parse_rd_rs_rs(ins, signed=False)
self.regs.set(
dst,
Int32(int(rs1 < rs2))
)
self.regs.set(dst, Int32(int(rs1 < rs2)))
def instruction_sltiu(self, ins: 'Instruction'):
def instruction_sltiu(self, ins: "Instruction"):
dst, rs1, imm = self.parse_rd_rs_imm(ins, signed=False)
self.regs.set(
dst,
Int32(int(rs1 < imm))
)
self.regs.set(dst, Int32(int(rs1 < imm)))
def instruction_beq(self, ins: 'Instruction'):
def instruction_beq(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 == rs2:
self.pc = dst.unsigned_value
def instruction_bne(self, ins: 'Instruction'):
def instruction_bne(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 != rs2:
self.pc = dst.unsigned_value
def instruction_blt(self, ins: 'Instruction'):
def instruction_blt(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 < rs2:
self.pc = dst.unsigned_value
def instruction_bge(self, ins: 'Instruction'):
def instruction_bge(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 >= rs2:
self.pc = dst.unsigned_value
def instruction_bltu(self, ins: 'Instruction'):
def instruction_bltu(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins, signed=False)
if rs1 < rs2:
self.pc = dst.unsigned_value
def instruction_bgeu(self, ins: 'Instruction'):
def instruction_bgeu(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins, signed=False)
if rs1 >= rs2:
self.pc = dst.unsigned_value
# technically deprecated
def instruction_j(self, ins: 'Instruction'):
def instruction_j(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 1)
addr = ins.get_imm(0)
self.pc = addr
def instruction_jal(self, ins: 'Instruction'):
reg = 'ra' # default register is ra
def instruction_jal(self, ins: "Instruction"):
reg = "ra" # default register is ra
if len(ins.args) == 1:
addr = ins.get_imm(0)
else:
@ -266,7 +211,7 @@ class RV32I(InstructionSet):
self.regs.set(reg, Int32(self.pc))
self.pc = addr
def instruction_jalr(self, ins: 'Instruction'):
def instruction_jalr(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
base = ins.get_reg(1)
@ -274,51 +219,53 @@ class RV32I(InstructionSet):
self.regs.set(reg, Int32(self.pc))
self.pc = self.regs.get(base).unsigned_value + addr
def instruction_ret(self, ins: 'Instruction'):
def instruction_ret(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 0)
self.pc = self.regs.get('ra').value
self.pc = self.regs.get("ra").value
def instruction_ecall(self, ins: 'Instruction'):
def instruction_ecall(self, ins: "Instruction"):
self.instruction_scall(ins)
def instruction_ebreak(self, ins: 'Instruction'):
def instruction_ebreak(self, ins: "Instruction"):
self.instruction_sbreak(ins)
def instruction_scall(self, ins: 'Instruction'):
def instruction_scall(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 0)
if not isinstance(self.cpu, UserModeCPU):
# FIXME: add exception for syscall not supported or something
raise
syscall = Syscall(self.regs.get('a7'), self.cpu)
syscall = Syscall(self.regs.get("a7"), self.cpu)
self.cpu.syscall_int.handle_syscall(syscall)
def instruction_sbreak(self, ins: 'Instruction'):
def instruction_sbreak(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 0)
if self.cpu.conf.debug_instruction:
print(FMT_DEBUG + "Debug instruction encountered at 0x{:08X}".format(self.pc - 1) + FMT_NONE)
print(
FMT_DEBUG
+ "Debug instruction encountered at 0x{:08X}".format(self.pc - 1)
+ FMT_NONE
)
raise LaunchDebuggerException()
def instruction_nop(self, ins: 'Instruction'):
def instruction_nop(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 0)
pass
def instruction_li(self, ins: 'Instruction'):
def instruction_li(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
immediate = ins.get_imm(1)
self.regs.set(reg, Int32(immediate))
def instruction_la(self, ins: 'Instruction'):
def instruction_la(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
immediate = ins.get_imm(1)
self.regs.set(reg, Int32(immediate))
def instruction_mv(self, ins: 'Instruction'):
def instruction_mv(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
rd, rs = ins.get_reg(0), ins.get_reg(1)
self.regs.set(rd, self.regs.get(rs))

@ -12,50 +12,33 @@ class RV32M(InstructionSet):
"""
The RV32M Instruction set, containing multiplication and division instructions
"""
def instruction_mul(self, ins: 'Instruction'):
def instruction_mul(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
rs1 * rs2
)
self.regs.set(rd, rs1 * rs2)
def instruction_mulh(self, ins: 'Instruction'):
def instruction_mulh(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
(rs1 * rs2) >> 32
)
self.regs.set(rd, (rs1 * rs2) >> 32)
def instruction_mulhsu(self, ins: 'Instruction'):
def instruction_mulhsu(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_mulhu(self, ins: 'Instruction'):
def instruction_mulhu(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_div(self, ins: 'Instruction'):
def instruction_div(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
rs1 // rs2
)
self.regs.set(rd, rs1 // rs2)
def instruction_divu(self, ins: 'Instruction'):
def instruction_divu(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins, signed=False)
self.regs.set(
rd,
rs1 // rs2
)
self.regs.set(rd, rs1 // rs2)
def instruction_rem(self, ins: 'Instruction'):
def instruction_rem(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(
rd,
rs1 % rs2
)
self.regs.set(rd, rs1 % rs2)
def instruction_remu(self, ins: 'Instruction'):
def instruction_remu(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins, signed=False)
self.regs.set(
rd,
rs1 % rs2
)
self.regs.set(rd, rs1 % rs2)

@ -8,7 +8,11 @@ class RV_Debug(InstructionSet):
def instruction_print_uint(self, ins: Instruction):
reg = ins.get_reg(0)
print("register {} contains value {}".format(reg, self.regs.get(reg).unsigned_value))
print(
"register {} contains value {}".format(
reg, self.regs.get(reg).unsigned_value
)
)
def instruction_print_hex(self, ins: Instruction):
reg = ins.get_reg(0)
@ -16,4 +20,8 @@ class RV_Debug(InstructionSet):
def instruction_print_uhex(self, ins: Instruction):
reg = ins.get_reg(0)
print("register {} contains value {}".format(reg, hex(self.regs.get(reg).unsigned_value)))
print(
"register {} contains value {}".format(
reg, hex(self.regs.get(reg).unsigned_value)
)
)

@ -12,6 +12,4 @@ from .RV32I import RV32I
from .RV32A import RV32A
from .RV_Debug import RV_Debug
InstructionSetDict = {
v.__name__: v for v in [RV32I, RV32M, RV32A, RV_Debug]
}
InstructionSetDict = {v.__name__: v for v in [RV32I, RV32M, RV32A, RV_Debug]}

@ -23,23 +23,21 @@ class InstructionSet(ABC):
instructions containing a dot '.' should replace it with an underscore.
"""
def __init__(self, cpu: 'CPU'):
def __init__(self, cpu: "CPU"):
"""Create a new instance of the Instruction set. This requires access to a CPU, and grabs vertain things
from it such as access to the MMU and registers.
"""
self.name = self.__class__.__name__
self.cpu = cpu
def load(self) -> Dict[str, Callable[['Instruction'], None]]:
def load(self) -> Dict[str, Callable[["Instruction"], None]]:
"""
This is called by the CPU once it instantiates this instruction set
It returns a dictionary of all instructions in this instruction set,
pointing to the correct handler for it
"""
return {
name: ins for name, ins in self.get_instructions()
}
return {name: ins for name, ins in self.get_instructions()}
def get_instructions(self):
"""
@ -48,10 +46,10 @@ class InstructionSet(ABC):
converts underscores in names to dots
"""
for member in dir(self):
if member.startswith('instruction_'):
yield member[12:].replace('_', '.'), getattr(self, member)
if member.startswith("instruction_"):
yield member[12:].replace("_", "."), getattr(self, member)
def parse_mem_ins(self, ins: 'Instruction') -> Tuple[str, Int32]:
def parse_mem_ins(self, ins: "Instruction") -> Tuple[str, Int32]:
"""
parses both rd, rs, imm and rd, imm(rs) argument format and returns (rd, imm+rs1)
(so a register and address tuple for memory instructions)
@ -69,51 +67,69 @@ class InstructionSet(ABC):
rd = ins.get_reg(0)
return rd, rs + imm
def parse_rd_rs_rs(self, ins: 'Instruction', signed=True) -> Tuple[str, Int32, Int32]:
def parse_rd_rs_rs(
self, ins: "Instruction", signed=True
) -> Tuple[str, Int32, Int32]:
"""
Assumes the command is in <name> rd, rs1, rs2 format
Returns the name of rd, and the values in rs1 and rs2
"""
ASSERT_LEN(ins.args, 3)
if signed:
return ins.get_reg(0), \
Int32(self.get_reg_content(ins, 1)), \
Int32(self.get_reg_content(ins, 2))
return (
ins.get_reg(0),
Int32(self.get_reg_content(ins, 1)),
Int32(self.get_reg_content(ins, 2)),
)
else:
return ins.get_reg(0), \
UInt32(self.get_reg_content(ins, 1)), \
UInt32(self.get_reg_content(ins, 2))
return (
ins.get_reg(0),
UInt32(self.get_reg_content(ins, 1)),
UInt32(self.get_reg_content(ins, 2)),
)
def parse_rd_rs_imm(self, ins: 'Instruction', signed=True) -> Tuple[str, Int32, Int32]:
def parse_rd_rs_imm(
self, ins: "Instruction", signed=True
) -> Tuple[str, Int32, Int32]:
"""
Assumes the command is in <name> rd, rs, imm format
Returns the name of rd, the value in rs and the immediate imm
"""
ASSERT_LEN(ins.args, 3)
if signed:
return ins.get_reg(0), \
Int32(self.get_reg_content(ins, 1)), \
Int32(ins.get_imm(2))
return (
ins.get_reg(0),
Int32(self.get_reg_content(ins, 1)),
Int32(ins.get_imm(2)),
)
else:
return ins.get_reg(0), \
UInt32(self.get_reg_content(ins, 1)), \
UInt32(ins.get_imm(2))
return (
ins.get_reg(0),
UInt32(self.get_reg_content(ins, 1)),
UInt32(ins.get_imm(2)),
)
def parse_rs_rs_imm(self, ins: 'Instruction', signed=True) -> Tuple[Int32, Int32, Int32]:
def parse_rs_rs_imm(
self, ins: "Instruction", signed=True
) -> Tuple[Int32, Int32, Int32]:
"""
Assumes the command is in <name> rs1, rs2, imm format
Returns the values in rs1, rs2 and the immediate imm
"""
if signed:
return Int32(self.get_reg_content(ins, 0)), \
Int32(self.get_reg_content(ins, 1)), \
Int32(ins.get_imm(2))
return (
Int32(self.get_reg_content(ins, 0)),
Int32(self.get_reg_content(ins, 1)),
Int32(ins.get_imm(2)),
)
else:
return UInt32(self.get_reg_content(ins, 0)), \
UInt32(self.get_reg_content(ins, 1)), \
UInt32(ins.get_imm(2))
return (
UInt32(self.get_reg_content(ins, 0)),
UInt32(self.get_reg_content(ins, 1)),
UInt32(ins.get_imm(2)),
)
def get_reg_content(self, ins: 'Instruction', ind: int) -> Int32:
def get_reg_content(self, ins: "Instruction", ind: int) -> Int32:
"""
get the register name from ins and then return the register contents
"""
@ -140,6 +156,5 @@ class InstructionSet(ABC):
def __repr__(self):
return "InstructionSet[{}] with {} instructions".format(
self.__class__.__name__,
len(list(self.get_instructions()))
self.__class__.__name__, len(list(self.get_instructions()))
)

@ -3,21 +3,29 @@ import sys
from riscemu import RunConfig
from riscemu.types import InstructionMemorySection, SimpleInstruction, Program
if __name__ == '__main__':
if __name__ == "__main__":
from .CPU import UserModeCPU
from .instructions import InstructionSetDict
from .debug import launch_debug_session
cpu = UserModeCPU(list(InstructionSetDict.values()), RunConfig(verbosity=4))
program = Program('interactive session', base=0x100)
program = Program("interactive session", base=0x100)
context = program.context
program.add_section(InstructionMemorySection([
SimpleInstruction('ebreak', (), context, 0x100),
SimpleInstruction('addi', ('a0', 'zero', '0'), context, 0x104),
SimpleInstruction('addi', ('a7', 'zero', '93'), context, 0x108),
SimpleInstruction('scall', (), context, 0x10C),
], '.text', context, program.name, 0x100))
program.add_section(
InstructionMemorySection(
[
SimpleInstruction("ebreak", (), context, 0x100),
SimpleInstruction("addi", ("a0", "zero", "0"), context, 0x104),
SimpleInstruction("addi", ("a7", "zero", "93"), context, 0x108),
SimpleInstruction("scall", (), context, 0x10C),
],
".text",
context,
program.name,
0x100,
)
)
cpu.load_program(program)

@ -16,28 +16,32 @@ from .types.exceptions import ParseException
def parse_instruction(token: Token, args: Tuple[str], context: ParseContext):
if context.section is None:
context.new_section('.text', MemorySectionType.Instructions)
context.new_section(".text", MemorySectionType.Instructions)
if context.section.type != MemorySectionType.Instructions:
raise ParseException("{} {} encountered in invalid context: {}".format(token, args, context))
ins = SimpleInstruction(token.value, args, context.context, context.current_address())
raise ParseException(
"{} {} encountered in invalid context: {}".format(token, args, context)
)
ins = SimpleInstruction(
token.value, args, context.context, context.current_address()
)
context.section.data.append(ins)
def parse_label(token: Token, args: Tuple[str], context: ParseContext):
name = token.value[:-1]
if re.match(r'^\d+$', name):
if re.match(r"^\d+$", name):
# relative label:
context.context.numbered_labels[name].append(context.current_address())
else:
if name in context.context.labels:
print(FMT_PARSE + 'Warn: Symbol {} defined twice!'.format(name))
print(FMT_PARSE + "Warn: Symbol {} defined twice!".format(name))
context.add_label(name, context.current_address(), is_relative=True)
PARSERS: Dict[TokenType, Callable[[Token, Tuple[str], ParseContext], None]] = {
TokenType.PSEUDO_OP: AssemblerDirectives.handle_instruction,
TokenType.LABEL: parse_label,
TokenType.INSTRUCTION_NAME: parse_instruction
TokenType.INSTRUCTION_NAME: parse_instruction,
}
@ -58,7 +62,9 @@ def parse_tokens(name: str, tokens_iter: Iterable[Token]) -> Program:
return context.finalize()
def composite_tokenizer(tokens_iter: Iterable[Token]) -> Iterable[Tuple[Token, Tuple[str]]]:
def composite_tokenizer(
tokens_iter: Iterable[Token],
) -> Iterable[Tuple[Token, Tuple[str]]]:
"""
Convert an iterator over tokens into an iterator over tuples: (token, list(token))
@ -71,7 +77,11 @@ def composite_tokenizer(tokens_iter: Iterable[Token]) -> Iterable[Tuple[Token, T
while not tokens.is_empty():
token = next(tokens)
if token.type in (TokenType.PSEUDO_OP, TokenType.LABEL, TokenType.INSTRUCTION_NAME):
if token.type in (
TokenType.PSEUDO_OP,
TokenType.LABEL,
TokenType.INSTRUCTION_NAME,
):
yield token, tuple(take_arguments(tokens))
@ -106,8 +116,9 @@ class AssemblyFileLoader(ProgramLoader):
The AssemblyFileLoader loads .asm, .S and .s files by default, and acts as a weak fallback to all other filetypes.
"""
def parse(self) -> Program:
with open(self.source_path, 'r') as f:
with open(self.source_path, "r") as f:
return parse_tokens(self.filename, tokenize(f))
def parse_io(self, io):
@ -123,7 +134,7 @@ class AssemblyFileLoader(ProgramLoader):
:return:
"""
# gcc recognizes these line endings as assembly. So we will do too.
if source_path.split('.')[-1] in ('asm', 'S', 's'):
if source_path.split(".")[-1] in ("asm", "S", "s"):
return 1
return 0.01

@ -12,6 +12,7 @@ class CSR:
"""
This holds all Control and Status Registers (CSR)
"""
regs: Dict[int, UInt32]
"""
All Control and Status Registers are stored here
@ -52,7 +53,9 @@ class CSR:
return self.virtual_regs[addr]()
return self.regs[addr]
def set_listener(self, addr: Union[str, int], listener: Callable[[UInt32, UInt32], None]):
def set_listener(
self, addr: Union[str, int], listener: Callable[[UInt32, UInt32], None]
):
addr = self._name_to_addr(addr)
if addr is None:
print("unknown csr address name: {}".format(addr))
@ -73,11 +76,11 @@ class CSR:
"""
size = 2 if name in MSTATUS_LEN_2 else 1
off = MSTATUS_OFFSETS[name]
mask = (2 ** size - 1) << off
old_val = self.get('mstatus')
mask = (2**size - 1) << off
old_val = self.get("mstatus")
erased = old_val & (~mask)
new_val = erased | (val << off)
self.set('mstatus', new_val)
self.set("mstatus", new_val)
def get_mstatus(self, name) -> UInt32:
if not self.mstatus_cache_dirty and name in self.mstatus_cache:
@ -85,8 +88,8 @@ class CSR:
size = 2 if name in MSTATUS_LEN_2 else 1
off = MSTATUS_OFFSETS[name]
mask = (2 ** size - 1) << off
val = (self.get('mstatus') & mask) >> off
mask = (2**size - 1) << off
val = (self.get("mstatus") & mask) >> off
if self.mstatus_cache_dirty:
self.mstatus_cache = dict(name=val)
else:

@ -1,81 +1,81 @@
from typing import Dict, Tuple
MCAUSE_TRANSLATION: Dict[Tuple[int, int], str]= {
(1, 0): 'User software interrupt',
(1, 1): 'Supervisor software interrupt',
(1, 3): 'Machine software interrupt',
(1, 4): 'User timer interrupt',
(1, 5): 'Supervisor timer interrupt',
(1, 7): 'Machine timer interrupt',
(1, 8): 'User external interrupt',
(1, 9): 'Supervisor external interrupt',
(1, 11): 'Machine external interrupt',
(0, 0): 'Instruction address misaligned',
(0, 1): 'Instruction access fault',
(0, 2): 'Illegal instruction',
(0, 3): 'Breakpoint',
(0, 4): 'Load address misaligned',
(0, 5): 'Load access fault',
(0, 6): 'Store/AMO address misaligned',
(0, 7): 'Store/AMO access fault',
(0, 8): 'environment call from user mode',
(0, 9): 'environment call from supervisor mode',
(0, 11): 'environment call from machine mode',
(0, 12): 'Instruction page fault',
(0, 13): 'Load page fault',
(0, 15): 'Store/AMO page fault',
MCAUSE_TRANSLATION: Dict[Tuple[int, int], str] = {
(1, 0): "User software interrupt",
(1, 1): "Supervisor software interrupt",
(1, 3): "Machine software interrupt",
(1, 4): "User timer interrupt",
(1, 5): "Supervisor timer interrupt",
(1, 7): "Machine timer interrupt",
(1, 8): "User external interrupt",
(1, 9): "Supervisor external interrupt",
(1, 11): "Machine external interrupt",
(0, 0): "Instruction address misaligned",
(0, 1): "Instruction access fault",
(0, 2): "Illegal instruction",
(0, 3): "Breakpoint",
(0, 4): "Load address misaligned",
(0, 5): "Load access fault",
(0, 6): "Store/AMO address misaligned",
(0, 7): "Store/AMO access fault",
(0, 8): "environment call from user mode",
(0, 9): "environment call from supervisor mode",
(0, 11): "environment call from machine mode",
(0, 12): "Instruction page fault",
(0, 13): "Load page fault",
(0, 15): "Store/AMO page fault",
}
"""
Assigns tuple (interrupt bit, exception code) to their respective readable names
"""
MSTATUS_OFFSETS: Dict[str, int] = {
'uie': 0,
'sie': 1,
'mie': 3,
'upie': 4,
'spie': 5,
'mpie': 7,
'spp': 8,
'mpp': 11,
'fs': 13,
'xs': 15,
'mpriv': 17,
'sum': 18,
'mxr': 19,
'tvm': 20,
'tw': 21,
'tsr': 22,
'sd': 31
"uie": 0,
"sie": 1,
"mie": 3,
"upie": 4,
"spie": 5,
"mpie": 7,
"spp": 8,
"mpp": 11,
"fs": 13,
"xs": 15,
"mpriv": 17,
"sum": 18,
"mxr": 19,
"tvm": 20,
"tw": 21,
"tsr": 22,
"sd": 31,
}
"""
Offsets for all mstatus bits
"""
MSTATUS_LEN_2 = ('mpp', 'fs', 'xs')
MSTATUS_LEN_2 = ("mpp", "fs", "xs")
"""
All mstatus parts that have length 2. All other mstatus parts have length 1
"""
CSR_NAME_TO_ADDR: Dict[str, int] = {
'mstatus': 0x300,
'misa': 0x301,
'mie': 0x304,
'mtvec': 0x305,
'mepc': 0x341,
'mcause': 0x342,
'mtval': 0x343,
'mip': 0x344,
'mvendorid': 0xF11,
'marchid': 0xF12,
'mimpid': 0xF13,
'mhartid': 0xF14,
'time': 0xc01,
'timeh': 0xc81,
'halt': 0x789,
'mtimecmp': 0x780,
'mtimecmph': 0x781,
"mstatus": 0x300,
"misa": 0x301,
"mie": 0x304,
"mtvec": 0x305,
"mepc": 0x341,
"mcause": 0x342,
"mtval": 0x343,
"mip": 0x344,
"mvendorid": 0xF11,
"marchid": 0xF12,
"mimpid": 0xF13,
"mhartid": 0xF14,
"time": 0xC01,
"timeh": 0xC81,
"halt": 0x789,
"mtimecmp": 0x780,
"mtimecmph": 0x781,
}
"""
Translation for named registers
"""
"""

@ -11,7 +11,7 @@ if typing.TYPE_CHECKING:
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import Section, SymbolTableSection
INCLUDE_SEC = ('.text', '.stack', '.bss', '.sdata', '.sbss')
INCLUDE_SEC = (".text", ".stack", ".bss", ".sdata", ".sbss")
class ElfBinaryFileLoader(ProgramLoader):
@ -20,6 +20,7 @@ class ElfBinaryFileLoader(ProgramLoader):
This loader respects local and global symbols.
"""
program: Program
def __init__(self, source_path: str, options: T_ParserOpts):
@ -28,8 +29,8 @@ class ElfBinaryFileLoader(ProgramLoader):
@classmethod
def can_parse(cls, source_path: str) -> float:
with open(source_path, 'rb') as f:
if f.read(4) == b'\x7f\x45\x4c\x46':
with open(source_path, "rb") as f:
if f.read(4) == b"\x7f\x45\x4c\x46":
return 1
return 0
@ -42,23 +43,33 @@ class ElfBinaryFileLoader(ProgramLoader):
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import Section, SymbolTableSection
with open(self.source_path, 'rb') as f:
print(FMT_ELF + "[ElfLoader] Loading elf executable from: {}".format(self.source_path) + FMT_NONE)
with open(self.source_path, "rb") as f:
print(
FMT_ELF
+ "[ElfLoader] Loading elf executable from: {}".format(
self.source_path
)
+ FMT_NONE
)
self._read_elf(ELFFile(f))
except ImportError as e:
print(FMT_PARSE + "[ElfLoader] Cannot load elf files without PyElfTools package! You can install them "
"using pip install pyelftools!" + FMT_NONE)
print(
FMT_PARSE
+ "[ElfLoader] Cannot load elf files without PyElfTools package! You can install them "
"using pip install pyelftools!" + FMT_NONE
)
raise e
return self.program
def _read_elf(self, elf: 'ELFFile'):
if not elf.header.e_machine == 'EM_RISCV':
def _read_elf(self, elf: "ELFFile"):
if not elf.header.e_machine == "EM_RISCV":
raise InvalidElfException("Not a RISC-V elf file!")
if not elf.header.e_ident.EI_CLASS == 'ELFCLASS32':
if not elf.header.e_ident.EI_CLASS == "ELFCLASS32":
raise InvalidElfException("Only 32bit executables are supported!")
from elftools.elf.sections import SymbolTableSection
for sec in elf.iter_sections():
if isinstance(sec, SymbolTableSection):
self._parse_symtab(sec)
@ -69,18 +80,22 @@ class ElfBinaryFileLoader(ProgramLoader):
self._add_sec(self._lms_from_elf_sec(sec, self.filename))
def _lms_from_elf_sec(self, sec: 'Section', owner: str):
is_code = sec.name in ('.text',)
def _lms_from_elf_sec(self, sec: "Section", owner: str):
is_code = sec.name in (".text",)
data = bytearray(sec.data())
if len(data) < sec.data_size:
data += bytearray(len(data) - sec.data_size)
flags = MemoryFlags(is_code, is_code)
print(FMT_ELF + "[ElfLoader] Section {} at: {:X}".format(sec.name, sec.header.sh_addr) + FMT_NONE)
print(
FMT_ELF
+ "[ElfLoader] Section {} at: {:X}".format(sec.name, sec.header.sh_addr)
+ FMT_NONE
)
return ElfMemorySection(
data, sec.name, self.program.context, owner, sec.header.sh_addr, flags
)
def _parse_symtab(self, symtab: 'SymbolTableSection'):
def _parse_symtab(self, symtab: "SymbolTableSection"):
from elftools.elf.enums import ENUM_ST_VISIBILITY
for sym in symtab.iter_symbols():
@ -88,18 +103,26 @@ class ElfBinaryFileLoader(ProgramLoader):
continue
self.program.context.labels[sym.name] = sym.entry.st_value
# check if it has st_visibility bit set
if sym.entry.st_info.bind == 'STB_GLOBAL':
if sym.entry.st_info.bind == "STB_GLOBAL":
self.program.global_labels.add(sym.name)
print(FMT_PARSE + "LOADED GLOBAL SYMBOL {}: {}".format(sym.name, sym.entry.st_value) + FMT_NONE)
print(
FMT_PARSE
+ "LOADED GLOBAL SYMBOL {}: {}".format(sym.name, sym.entry.st_value)
+ FMT_NONE
)
def _add_sec(self, new_sec: 'ElfMemorySection'):
def _add_sec(self, new_sec: "ElfMemorySection"):
for sec in self.program.sections:
if sec.base < sec.end <= new_sec.base or sec.end > sec.base >= new_sec.end:
continue
else:
print(FMT_ELF + "[ElfLoader] Invalid elf layout: Two sections overlap: \n\t{}\n\t{}".format(
sec, new_sec
) + FMT_NONE)
print(
FMT_ELF
+ "[ElfLoader] Invalid elf layout: Two sections overlap: \n\t{}\n\t{}".format(
sec, new_sec
)
+ FMT_NONE
)
raise RuntimeError("Cannot load elf with overlapping sections!")
self.program.add_section(new_sec)

@ -42,10 +42,12 @@ class CpuTrap(BaseException):
priv: PrivModes
"""
The privilege level this trap targets
The privilege level this trap targets
"""
def __init__(self, code: int, mtval, type: CpuTrapType, priv: PrivModes = PrivModes.MACHINE):
def __init__(
self, code: int, mtval, type: CpuTrapType, priv: PrivModes = PrivModes.MACHINE
):
self.interrupt = 0 if type == CpuTrapType.EXCEPTION else 1
self.code = code
self.mtval = UInt32(mtval)
@ -63,7 +65,9 @@ class CpuTrap(BaseException):
name = "Reserved interrupt({}, {})".format(self.interrupt, self.code)
if (self.interrupt, self.code) in MCAUSE_TRANSLATION:
name = MCAUSE_TRANSLATION[(self.interrupt, self.code)] + "({}, {})".format(self.interrupt, self.code)
name = MCAUSE_TRANSLATION[(self.interrupt, self.code)] + "({}, {})".format(
self.interrupt, self.code
)
return "{} {{priv={}, type={}, mtval={:x}}} {}".format(
name, self.priv.name, self.type.name, self.mtval, self.message()
@ -74,7 +78,7 @@ class CpuTrap(BaseException):
class IllegalInstructionTrap(CpuTrap):
def __init__(self, ins: 'ElfInstruction'):
def __init__(self, ins: "ElfInstruction"):
super().__init__(2, ins.encoded, CpuTrapType.EXCEPTION)
@ -104,7 +108,9 @@ class InvalidElfException(RiscemuBaseException):
self.msg = msg
def message(self):
return FMT_PARSE + "{}(\"{}\")".format(self.__class__.__name__, self.msg) + FMT_NONE
return (
FMT_PARSE + '{}("{}")'.format(self.__class__.__name__, self.msg) + FMT_NONE
)
class LoadAccessFault(CpuTrap):
@ -117,8 +123,5 @@ class LoadAccessFault(CpuTrap):
def message(self):
return "(During {} at 0x{:08x} of size {}: {})".format(
self.op,
self.addr,
self.size,
self.msg
self.op, self.addr, self.size, self.msg
)

@ -14,10 +14,9 @@ from ..types import MemoryFlags, ProgramLoader, Program, T_ParserOpts
class MemoryImageLoader(ProgramLoader):
@classmethod
def can_parse(cls, source_path: str) -> float:
if source_path.split('.')[-1] == 'img':
if source_path.split(".")[-1] == "img":
return 1
return 0
@ -26,14 +25,14 @@ class MemoryImageLoader(ProgramLoader):
return argv, {}
def parse(self) -> Iterable[Program]:
if 'debug' not in self.options:
if "debug" not in self.options:
yield self.parse_no_debug()
return
with open(self.options.get('debug'), 'r') as debug_file:
with open(self.options.get("debug"), "r") as debug_file:
debug_info = MemoryImageDebugInfos.load(debug_file.read())
with open(self.source_path, 'rb') as source_file:
with open(self.source_path, "rb") as source_file:
data: bytearray = bytearray(source_file.read())
for name, sections in debug_info.sections.items():
@ -43,11 +42,15 @@ class MemoryImageLoader(ProgramLoader):
if program.base is None:
program.base = start
#in_code_sec = get_section_base_name(sec_name) in INSTRUCTION_SECTION_NAMES
# in_code_sec = get_section_base_name(sec_name) in INSTRUCTION_SECTION_NAMES
program.add_section(
ElfMemorySection(
data[start:start+size], sec_name, program.context,
name, start, MemoryFlags(False, True)
data[start : start + size],
sec_name,
program.context,
name,
start,
MemoryFlags(False, True),
)
)
@ -57,19 +60,27 @@ class MemoryImageLoader(ProgramLoader):
yield program
def parse_no_debug(self) -> Program:
print(FMT_PARSE + "[MemoryImageLoader] Warning: loading memory image without debug information!" + FMT_NONE)
print(
FMT_PARSE
+ "[MemoryImageLoader] Warning: loading memory image without debug information!"
+ FMT_NONE
)
with open(self.source_path, 'rb') as source_file:
with open(self.source_path, "rb") as source_file:
data: bytes = source_file.read()
p = Program(self.filename)
p.add_section(ElfMemorySection(
bytearray(data), '.text', p.context, p.name, 0, MemoryFlags(False, True)
))
p.add_section(
ElfMemorySection(
bytearray(data), ".text", p.context, p.name, 0, MemoryFlags(False, True)
)
)
return p
@classmethod
def instantiate(cls, source_path: str, options: T_ParserOpts) -> 'ProgramLoader':
if os.path.isfile(source_path + '.dbg'):
return MemoryImageLoader(source_path, dict(**options, debug=source_path + '.dbg'))
def instantiate(cls, source_path: str, options: T_ParserOpts) -> "ProgramLoader":
if os.path.isfile(source_path + ".dbg"):
return MemoryImageLoader(
source_path, dict(**options, debug=source_path + ".dbg")
)
return MemoryImageLoader(source_path, options)

@ -87,7 +87,11 @@ class PrivCPU(CPU):
if self.halted:
print()
print(FMT_CPU + "[CPU] System halted with code {}".format(self.exit_code) + FMT_NONE)
print(
FMT_CPU
+ "[CPU] System halted with code {}".format(self.exit_code)
+ FMT_NONE
)
sys.exit(self.exit_code)
elif launch_debug:
@ -96,55 +100,68 @@ class PrivCPU(CPU):
self.run(verbose)
else:
print()
print(FMT_CPU + "[CPU] System stopped without halting - perhaps you stopped the debugger?" + FMT_NONE)
print(
FMT_CPU
+ "[CPU] System stopped without halting - perhaps you stopped the debugger?"
+ FMT_NONE
)
def launch(self, program: Optional[Program] = None, verbose: bool = False):
print(FMT_CPU + '[CPU] Started running from 0x{:08X} ({})'.format(self.pc, "kernel") + FMT_NONE)
print(
FMT_CPU
+ "[CPU] Started running from 0x{:08X} ({})".format(self.pc, "kernel")
+ FMT_NONE
)
self._time_start = time.perf_counter_ns() // self.TIME_RESOLUTION_NS
self.run(self.conf.verbosity > 1 or verbose)
def load_program(self, program: Program):
if program.name == 'kernel':
if program.name == "kernel":
self.pc = program.entrypoint
super().load_program(program)
def _init_csr(self):
# set up CSR
self.csr = CSR()
self.csr.set('mhartid', UInt32(0)) # core id
self.csr.set("mhartid", UInt32(0)) # core id
# TODO: set correct value
self.csr.set('mimpid', UInt32(0)) # implementation id
self.csr.set("mimpid", UInt32(0)) # implementation id
# set mxl to 1 (32 bit) and set bits for i and m isa
self.csr.set('misa', UInt32((1 << 30) + (1 << 8) + (1 << 12))) # available ISA
self.csr.set("misa", UInt32((1 << 30) + (1 << 8) + (1 << 12))) # available ISA
# CSR write callbacks:
@self.csr.callback('halt')
@self.csr.callback("halt")
def halt(old: UInt32, new: UInt32):
if new != 0:
self.halted = True
self.exit_code = new.value
@self.csr.callback('mtimecmp')
@self.csr.callback("mtimecmp")
def mtimecmp(old: UInt32, new: UInt32):
self._time_timecmp = (self.csr.get('mtimecmph') << 32) + new
self._time_timecmp = (self.csr.get("mtimecmph") << 32) + new
self._time_interrupt_enabled = True
@self.csr.callback('mtimecmph')
@self.csr.callback("mtimecmph")
def mtimecmph(old: UInt32, new: UInt32):
self._time_timecmp = (new << 32) + self.csr.get('mtimecmp')
self._time_timecmp = (new << 32) + self.csr.get("mtimecmp")
self._time_interrupt_enabled = True
# virtual CSR registers:
@self.csr.virtual_register('time')
@self.csr.virtual_register("time")
def get_time():
return UInt32(time.perf_counter_ns() // self.TIME_RESOLUTION_NS - self._time_start)
return UInt32(
time.perf_counter_ns() // self.TIME_RESOLUTION_NS - self._time_start
)
@self.csr.virtual_register('timeh')
@self.csr.virtual_register("timeh")
def get_timeh():
return UInt32((time.perf_counter_ns() // self.TIME_RESOLUTION_NS - self._time_start) >> 32)
return UInt32(
(time.perf_counter_ns() // self.TIME_RESOLUTION_NS - self._time_start)
>> 32
)
# add minstret and mcycle counters
@ -160,17 +177,21 @@ class PrivCPU(CPU):
self._check_interrupt()
ins = self.mmu.read_ins(self.pc)
if verbose and (self.mode == PrivModes.USER or self.conf.verbosity > 4):
print(FMT_CPU + " Running 0x{:08X}:{} {}".format(self.pc, FMT_NONE, ins))
print(
FMT_CPU + " Running 0x{:08X}:{} {}".format(self.pc, FMT_NONE, ins)
)
self.run_instruction(ins)
self.pc += self.INS_XLEN
except CpuTrap as trap:
self._handle_trap(trap)
if trap.interrupt == 0 and not isinstance(trap, EcallTrap):
print(FMT_CPU + "[CPU] Trap {} encountered at {} (0x{:x})".format(
trap,
self.mmu.translate_address(self.pc),
self.pc
) + FMT_NONE)
print(
FMT_CPU
+ "[CPU] Trap {} encountered at {} (0x{:x})".format(
trap, self.mmu.translate_address(self.pc), self.pc
)
+ FMT_NONE
)
breakpoint()
if self.conf.debug_on_exception:
raise LaunchDebuggerException()
@ -179,12 +200,15 @@ class PrivCPU(CPU):
def _timer_step(self):
if not self._time_interrupt_enabled:
return
if self._time_timecmp <= (time.perf_counter_ns() // self.TIME_RESOLUTION_NS) - self._time_start:
if (
self._time_timecmp
<= (time.perf_counter_ns() // self.TIME_RESOLUTION_NS) - self._time_start
):
self.pending_traps.append(TimerInterrupt())
self._time_interrupt_enabled = False
def _check_interrupt(self):
if not (len(self.pending_traps) > 0 and self.csr.get_mstatus('mie')):
if not (len(self.pending_traps) > 0 and self.csr.get_mstatus("mie")):
return
# select best interrupt
# FIXME: actually select based on the official ranking
@ -194,24 +218,30 @@ class PrivCPU(CPU):
self.regs.dump_reg_a()
if trap.priv != PrivModes.MACHINE:
print(FMT_CPU + "[CPU] Trap not targeting machine mode encountered! - undefined behaviour!" + FMT_NONE)
print(
FMT_CPU
+ "[CPU] Trap not targeting machine mode encountered! - undefined behaviour!"
+ FMT_NONE
)
raise Exception("Undefined behaviour!")
if self.mode != PrivModes.USER:
print(FMT_CPU + "[CPU] Trap triggered outside of user mode?!" + FMT_NONE)
self.csr.set_mstatus('mpie', self.csr.get_mstatus('mie'))
self.csr.set_mstatus('mpp', self.mode.value)
self.csr.set_mstatus('mie', UInt32(0))
self.csr.set('mcause', trap.mcause)
self.csr.set('mepc', self.pc - self.INS_XLEN)
self.csr.set('mtval', trap.mtval)
self.csr.set_mstatus("mpie", self.csr.get_mstatus("mie"))
self.csr.set_mstatus("mpp", self.mode.value)
self.csr.set_mstatus("mie", UInt32(0))
self.csr.set("mcause", trap.mcause)
self.csr.set("mepc", self.pc - self.INS_XLEN)
self.csr.set("mtval", trap.mtval)
self.mode = trap.priv
mtvec = self.csr.get('mtvec')
mtvec = self.csr.get("mtvec")
if mtvec & 0b11 == 0:
self.pc = mtvec.value
if mtvec & 0b11 == 1:
self.pc = ((mtvec & 0b11111111111111111111111111111100) + (trap.code * 4)).value
self.pc = (
(mtvec & 0b11111111111111111111111111111100) + (trap.code * 4)
).value
self.record_perf_profile()
if len(self._perf_counters) > 100:
self.show_perf()
@ -232,7 +262,10 @@ class PrivCPU(CPU):
cycled = cycle
timed = time_ns
cps_list.append(cps)
print(" on average {:.0f} instructions/s".format(sum(cps_list) / len(cps_list)) + FMT_NONE)
print(
" on average {:.0f} instructions/s".format(sum(cps_list) / len(cps_list))
+ FMT_NONE
)
self._perf_counters = list()
def record_perf_profile(self):
@ -240,6 +273,4 @@ class PrivCPU(CPU):
@classmethod
def get_loaders(cls) -> typing.Iterable[Type[ProgramLoader]]:
return [
AssemblyFileLoader, MemoryImageLoader, ElfBinaryFileLoader
]
return [AssemblyFileLoader, MemoryImageLoader, ElfBinaryFileLoader]

@ -9,7 +9,6 @@ if typing.TYPE_CHECKING:
class PrivMMU(MMU):
def get_sec_containing(self, addr: T_AbsoluteAddress) -> MemorySection:
# try to get an existing section
existing_sec = super().get_sec_containing(addr)
@ -18,7 +17,9 @@ class PrivMMU(MMU):
return existing_sec
# get section preceding empty space at addr
sec_before = next((sec for sec in reversed(self.sections) if sec.end < addr), None)
sec_before = next(
(sec for sec in reversed(self.sections) if sec.end < addr), None
)
# get sec succeeding empty space at addr
sec_after = next((sec for sec in self.sections if sec.base > addr), None)
@ -31,7 +32,14 @@ class PrivMMU(MMU):
# end at the start of the next section, or address + 0xFFFF (aligned to 16 byte boundary)
end = min(next_sec_start, align_addr(addr + 0xFFFF, 16))
sec = ElfMemorySection(bytearray(end - start), '.empty', self.global_instruction_context(), '', start, MemoryFlags(False, True))
sec = ElfMemorySection(
bytearray(end - start),
".empty",
self.global_instruction_context(),
"",
start,
MemoryFlags(False, True),
)
self.sections.append(sec)
self._update_state()
@ -40,4 +48,4 @@ class PrivMMU(MMU):
def global_instruction_context(self) -> InstructionContext:
context = InstructionContext()
context.global_symbol_dict = self.global_symbols
return context
return context

@ -16,155 +16,159 @@ if typing.TYPE_CHECKING:
class PrivRV32I(RV32I):
cpu: 'PrivCPU'
cpu: "PrivCPU"
"""
This is an extension of RV32I, written for the PrivCPU class
"""
def instruction_csrrw(self, ins: 'Instruction'):
def instruction_csrrw(self, ins: "Instruction"):
rd, rs, csr_addr = self.parse_crs_ins(ins)
old_val = None
if rd != 'zero':
if rd != "zero":
self.cpu.csr.assert_can_read(self.cpu.mode, csr_addr)
old_val = self.cpu.csr.get(csr_addr)
if rs != 'zero':
if rs != "zero":
new_val = self.regs.get(rs)
self.cpu.csr.assert_can_write(self.cpu.mode, csr_addr)
self.cpu.csr.set(csr_addr, new_val)
if old_val is not None:
self.regs.set(rd, old_val)
def instruction_csrrs(self, ins: 'Instruction'):
def instruction_csrrs(self, ins: "Instruction"):
rd, rs, csr_addr = self.parse_crs_ins(ins)
if rs != 'zero':
if rs != "zero":
# oh no, this should not happen!
INS_NOT_IMPLEMENTED(ins)
if rd != 'zero':
if rd != "zero":
self.cpu.csr.assert_can_read(self.cpu.mode, csr_addr)
old_val = self.cpu.csr.get(csr_addr)
self.regs.set(rd, old_val)
def instruction_csrrc(self, ins: 'Instruction'):
def instruction_csrrc(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_csrrsi(self, ins: 'Instruction'):
def instruction_csrrsi(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_csrrwi(self, ins: 'Instruction'):
def instruction_csrrwi(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
rd, imm, addr = ins.get_reg(0), ins.get_imm(1), ins.get_imm(2)
if rd != 'zero':
if rd != "zero":
self.cpu.csr.assert_can_read(self.cpu.mode, addr)
old_val = self.cpu.csr.get(addr)
self.regs.set(rd, old_val)
self.cpu.csr.assert_can_write(self.cpu.mode, addr)
self.cpu.csr.set(addr, imm)
def instruction_csrrci(self, ins: 'Instruction'):
def instruction_csrrci(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_mret(self, ins: 'Instruction'):
def instruction_mret(self, ins: "Instruction"):
if self.cpu.mode != PrivModes.MACHINE:
print("MRET not inside machine level code!")
raise IllegalInstructionTrap(ins)
# retore mie
mpie = self.cpu.csr.get_mstatus('mpie')
self.cpu.csr.set_mstatus('mie', mpie)
mpie = self.cpu.csr.get_mstatus("mpie")
self.cpu.csr.set_mstatus("mie", mpie)
# restore priv
mpp = self.cpu.csr.get_mstatus('mpp')
mpp = self.cpu.csr.get_mstatus("mpp")
self.cpu.mode = PrivModes(mpp)
# restore pc
mepc = self.cpu.csr.get('mepc')
mepc = self.cpu.csr.get("mepc")
self.cpu.pc = (mepc - self.cpu.INS_XLEN).value
if self.cpu.conf.verbosity > 0:
sec = self.mmu.get_sec_containing(mepc.value)
if sec is not None:
print(FMT_CPU + "[CPU] returning to mode {} in {} (0x{:x})".format(
PrivModes(mpp).name,
self.mmu.translate_address(mepc),
mepc
) + FMT_NONE)
print(
FMT_CPU
+ "[CPU] returning to mode {} in {} (0x{:x})".format(
PrivModes(mpp).name, self.mmu.translate_address(mepc), mepc
)
+ FMT_NONE
)
if self.cpu.conf.verbosity > 1:
self.regs.dump_reg_a()
def instruction_uret(self, ins: 'Instruction'):
def instruction_uret(self, ins: "Instruction"):
raise IllegalInstructionTrap(ins)
def instruction_sret(self, ins: 'Instruction'):
def instruction_sret(self, ins: "Instruction"):
raise IllegalInstructionTrap(ins)
def instruction_scall(self, ins: 'Instruction'):
def instruction_scall(self, ins: "Instruction"):
"""
Overwrite the scall from userspace RV32I
"""
raise EcallTrap(self.cpu.mode)
def instruction_beq(self, ins: 'Instruction'):
def instruction_beq(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 == rs2:
self.pc += dst.value - 4
def instruction_bne(self, ins: 'Instruction'):
def instruction_bne(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 != rs2:
self.pc += dst.value - 4
def instruction_blt(self, ins: 'Instruction'):
def instruction_blt(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 < rs2:
self.pc += dst.value - 4
def instruction_bge(self, ins: 'Instruction'):
def instruction_bge(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 >= rs2:
self.pc += dst.value - 4
def instruction_bltu(self, ins: 'Instruction'):
def instruction_bltu(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins, signed=False)
if rs1 < rs2:
self.pc += dst.value - 4
def instruction_bgeu(self, ins: 'Instruction'):
def instruction_bgeu(self, ins: "Instruction"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins, signed=False)
if rs1 >= rs2:
self.pc += dst.value - 4
# technically deprecated
def instruction_j(self, ins: 'Instruction'):
def instruction_j(self, ins: "Instruction"):
raise NotImplementedError("Should never be reached!")
def instruction_jal(self, ins: 'Instruction'):
def instruction_jal(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
addr = ins.get_imm(1)
if reg == 'ra' and (
(self.cpu.mode == PrivModes.USER and self.cpu.conf.verbosity > 1) or
(self.cpu.conf.verbosity > 3)
if reg == "ra" and (
(self.cpu.mode == PrivModes.USER and self.cpu.conf.verbosity > 1)
or (self.cpu.conf.verbosity > 3)
):
print(FMT_CPU + 'Jumping from 0x{:x} to {} (0x{:x})'.format(
self.pc,
self.mmu.translate_address(self.pc + addr),
self.pc + addr
) + FMT_NONE)
print(
FMT_CPU
+ "Jumping from 0x{:x} to {} (0x{:x})".format(
self.pc, self.mmu.translate_address(self.pc + addr), self.pc + addr
)
+ FMT_NONE
)
self.regs.dump_reg_a()
self.regs.set(reg, Int32(self.pc))
self.pc += addr - 4
def instruction_jalr(self, ins: 'Instruction'):
def instruction_jalr(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
rd, rs, imm = self.parse_rd_rs_imm(ins)
self.regs.set(rd, Int32(self.pc))
self.pc = rs.value + imm.value - 4
def instruction_sbreak(self, ins: 'Instruction'):
def instruction_sbreak(self, ins: "Instruction"):
raise LaunchDebuggerException()
def parse_crs_ins(self, ins: 'Instruction'):
def parse_crs_ins(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 3)
return ins.get_reg(0), ins.get_reg(1), ins.get_imm(2)
def parse_mem_ins(self, ins: 'Instruction') -> Tuple[str, int]:
def parse_mem_ins(self, ins: "Instruction") -> Tuple[str, int]:
ASSERT_LEN(ins.args, 3)
addr = self.get_reg_content(ins, 1) + ins.get_imm(2)
reg = ins.get_reg(0)

@ -12,4 +12,4 @@ Syscalls will have to be intercepted by your assembly code.
The PrivCPU Implements the Risc-V M/U Model, meaning there is machine mode and user mode. No PMP or paging is available.
"""
"""

@ -4,27 +4,55 @@ from .PrivCPU import PrivCPU
import sys
if __name__ == '__main__':
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='RISC-V privileged architecture emulator', prog='riscemu')
parser = argparse.ArgumentParser(
description="RISC-V privileged architecture emulator", prog="riscemu"
)
parser.add_argument('source', type=str,
help='Compiled RISC-V ELF file or memory image containing compiled RISC-V ELF files', nargs='+')
parser.add_argument('--debug-exceptions', help='Launch the interactive debugger when an exception is generated',
action='store_true')
parser.add_argument(
"source",
type=str,
help="Compiled RISC-V ELF file or memory image containing compiled RISC-V ELF files",
nargs="+",
)
parser.add_argument(
"--debug-exceptions",
help="Launch the interactive debugger when an exception is generated",
action="store_true",
)
parser.add_argument('-v', '--verbose', help="Verbosity level (can be used multiple times)", action='count',
default=0)
parser.add_argument(
"-v",
"--verbose",
help="Verbosity level (can be used multiple times)",
action="count",
default=0,
)
parser.add_argument('--slowdown', help="Slow down the emulated CPU clock by a factor", type=float, default=1)
parser.add_argument(
"--slowdown",
help="Slow down the emulated CPU clock by a factor",
type=float,
default=1,
)
args = parser.parse_args()
cpu = PrivCPU(RunConfig(verbosity=args.verbose, debug_on_exception=args.debug_exceptions, slowdown=args.slowdown))
cpu = PrivCPU(
RunConfig(
verbosity=args.verbose,
debug_on_exception=args.debug_exceptions,
slowdown=args.slowdown,
)
)
for source_path in args.source:
loader = max((loader for loader in cpu.get_loaders()), key=lambda l: l.can_parse(source_path))
loader = max(
(loader for loader in cpu.get_loaders()),
key=lambda l: l.can_parse(source_path),
)
argv, opts = loader.get_options(sys.argv)
program = loader.instantiate(source_path, opts).parse()
if isinstance(program, Program):

@ -6,9 +6,19 @@ from typing import Tuple, Dict, Set
from riscemu.colors import FMT_NONE, FMT_PARSE
from riscemu.decoder import format_ins, RISCV_REGS, decode
from riscemu.priv.Exceptions import InstructionAccessFault, InstructionAddressMisalignedTrap, LoadAccessFault
from riscemu.types import Instruction, InstructionContext, T_RelativeAddress, MemoryFlags, T_AbsoluteAddress, \
BinaryDataMemorySection
from riscemu.priv.Exceptions import (
InstructionAccessFault,
InstructionAddressMisalignedTrap,
LoadAccessFault,
)
from riscemu.types import (
Instruction,
InstructionContext,
T_RelativeAddress,
MemoryFlags,
T_AbsoluteAddress,
BinaryDataMemorySection,
)
@dataclass(frozen=True)
@ -27,34 +37,45 @@ class ElfInstruction(Instruction):
return RISCV_REGS[self.args[num]]
def __repr__(self) -> str:
if self.name == 'jal' and self.args[0] == 0:
if self.name == "jal" and self.args[0] == 0:
return "j {}".format(self.args[1])
if self.name == 'addi' and self.args[2] == 0:
if self.name == "addi" and self.args[2] == 0:
return "mv {}, {}".format(self.get_reg(0), self.get_reg(1))
if self.name == 'addi' and self.args[1] == 0:
if self.name == "addi" and self.args[1] == 0:
return "li {}, {}".format(self.get_reg(0), self.args[2])
if self.name == 'ret' and len(self.args) == 0:
if self.name == "ret" and len(self.args) == 0:
return "ret"
return format_ins(self.encoded, self.name)
class ElfMemorySection(BinaryDataMemorySection):
def __init__(self, data: bytearray, name: str, context: InstructionContext, owner: str, base: int,
flags: MemoryFlags):
def __init__(
self,
data: bytearray,
name: str,
context: InstructionContext,
owner: str,
base: int,
flags: MemoryFlags,
):
super().__init__(data, name, context, owner, base=base, flags=flags)
self.read_ins = lru_cache(maxsize=self.size // 4)(self.read_ins)
def read_ins(self, offset):
if not self.flags.executable:
print(FMT_PARSE + "Reading instruction from non-executable memory!" + FMT_NONE)
print(
FMT_PARSE + "Reading instruction from non-executable memory!" + FMT_NONE
)
raise InstructionAccessFault(offset + self.base)
if offset % 4 != 0:
raise InstructionAddressMisalignedTrap(offset + self.base)
return ElfInstruction(*decode(self.data[offset:offset + 4]))
return ElfInstruction(*decode(self.data[offset : offset + 4]))
def write(self, offset: T_RelativeAddress, size: int, data: bytearray):
if self.flags.read_only:
raise LoadAccessFault('read-only section', offset + self.base, size, 'write')
raise LoadAccessFault(
"read-only section", offset + self.base, size, "write"
)
self.read_ins.cache_clear()
return super(ElfMemorySection, self).write(offset, size, data)
@ -64,7 +85,7 @@ class ElfMemorySection(BinaryDataMemorySection):
class MemoryImageDebugInfos:
VERSION = '1.0.0'
VERSION = "1.0.0"
"""
Schema version
"""
@ -89,12 +110,13 @@ class MemoryImageDebugInfos:
This dictionary contains the list of all global symbols of a given program
"""
def __init__(self,
sections: Dict[str, Dict[str, Tuple[int, int]]],
symbols: Dict[str, Dict[str, int]],
globals: Dict[str, Set[str]],
base: int = 0
):
def __init__(
self,
sections: Dict[str, Dict[str, Tuple[int, int]]],
symbols: Dict[str, Dict[str, int]],
globals: Dict[str, Set[str]],
base: int = 0,
):
self.sections = sections
self.symbols = symbols
self.globals = globals
@ -108,7 +130,9 @@ class MemoryImageDebugInfos:
return json.dumps(dict(obj), default=serialize)
if isinstance(obj, (set, tuple)):
return json.dumps(list(obj), default=serialize)
return "<<unserializable {}>>".format(getattr(obj, '__qualname__', '{unknown}'))
return "<<unserializable {}>>".format(
getattr(obj, "__qualname__", "{unknown}")
)
return json.dumps(
dict(
@ -116,22 +140,25 @@ class MemoryImageDebugInfos:
symbols=self.symbols,
globals=self.globals,
base=self.base,
VERSION=self.VERSION
VERSION=self.VERSION,
),
default=serialize
default=serialize,
)
@classmethod
def load(cls, serialized_str: str) -> 'MemoryImageDebugInfos':
def load(cls, serialized_str: str) -> "MemoryImageDebugInfos":
json_obj: dict = json.loads(serialized_str)
if 'VERSION' not in json_obj:
if "VERSION" not in json_obj:
raise RuntimeError("Unknown MemoryImageDebugInfo version!")
version: str = json_obj.pop('VERSION')
version: str = json_obj.pop("VERSION")
# compare major version
if version != cls.VERSION and version.split('.')[0] != cls.VERSION.split('.')[0]:
if (
version != cls.VERSION
and version.split(".")[0] != cls.VERSION.split(".")[0]
):
raise RuntimeError(
"Unknown MemoryImageDebugInfo version! This emulator expects version {}, debug info version {}".format(
cls.VERSION, version
@ -141,7 +168,7 @@ class MemoryImageDebugInfos:
return MemoryImageDebugInfos(**json_obj)
@classmethod
def builder(cls) -> 'MemoryImageDebugInfos':
def builder(cls) -> "MemoryImageDebugInfos":
return MemoryImageDebugInfos(
defaultdict(dict), defaultdict(dict), defaultdict(set)
)

@ -18,53 +18,110 @@ class Registers:
"""
valid_regs = {
'zero', 'ra', 'sp', 'gp', 'tp', 's0', 'fp',
't0', 't1', 't2', 't3', 't4', 't5', 't6',
's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11',
'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7',
'ft0', 'ft1', 'ft2', 'ft3', 'ft4', 'ft5', 'ft6', 'ft7',
'fs0', 'fs1', 'fs2', 'fs3', 'fs4', 'fs5', 'fs6', 'fs7', 'fs8', 'fs9', 'fs10', 'fs11',
'fa0', 'fa1', 'fa2', 'fa3', 'fa4', 'fa5', 'fa6', 'fa7'
"zero",
"ra",
"sp",
"gp",
"tp",
"s0",
"fp",
"t0",
"t1",
"t2",
"t3",
"t4",
"t5",
"t6",
"s1",
"s2",
"s3",
"s4",
"s5",
"s6",
"s7",
"s8",
"s9",
"s10",
"s11",
"a0",
"a1",
"a2",
"a3",
"a4",
"a5",
"a6",
"a7",
"ft0",
"ft1",
"ft2",
"ft3",
"ft4",
"ft5",
"ft6",
"ft7",
"fs0",
"fs1",
"fs2",
"fs3",
"fs4",
"fs5",
"fs6",
"fs7",
"fs8",
"fs9",
"fs10",
"fs11",
"fa0",
"fa1",
"fa2",
"fa3",
"fa4",
"fa5",
"fa6",
"fa7",
}
def __init__(self, infinite_regs: bool = False):
from .types import Int32
self.vals = defaultdict(lambda: Int32(0))
self.vals: defaultdict[str, Int32] = defaultdict(lambda: Int32(0))
self.last_set = None
self.last_read = None
self.infinite_regs = infinite_regs
def dump(self, full=False):
def dump(self, full: bool = False):
"""
Dump all registers to stdout
:param full: If True, floating point registers are dumped too
"""
named_regs = [self._reg_repr(reg) for reg in Registers.named_registers()]
lines = [[] for i in range(12)]
lines: list[list[str]] = [[] for _ in range(12)]
if not full:
regs = [('a', 8), ('s', 12), ('t', 7)]
regs = [("a", 8), ("s", 12), ("t", 7)]
else:
regs = [
('a', 8),
('s', 12),
('t', 7),
('ft', 8),
('fa', 8),
('fs', 12),
("a", 8),
("s", 12),
("t", 7),
("ft", 8),
("fa", 8),
("fs", 12),
]
for name, s in regs:
for i in range(12):
if i >= s:
lines[i].append(" " * 15)
else:
reg = '{}{}'.format(name, i)
reg = "{}{}".format(name, i)
lines[i].append(self._reg_repr(reg))
print("Registers[{},{}](".format(
FMT_ORANGE + FMT_UNDERLINE + 'read' + FMT_NONE,
FMT_ORANGE + FMT_BOLD + 'written' + FMT_NONE
))
print(
"Registers[{},{}](".format(
FMT_ORANGE + FMT_UNDERLINE + "read" + FMT_NONE,
FMT_ORANGE + FMT_BOLD + "written" + FMT_NONE,
)
)
if not full:
print("\t" + " ".join(named_regs[0:3]))
print("\t" + " ".join(named_regs[3:]))
@ -80,23 +137,26 @@ class Registers:
"""
Dump the a registers
"""
print("Registers[a]:" + " ".join(self._reg_repr('a{}'.format(i)) for i in range(8)))
print(
"Registers[a]:"
+ " ".join(self._reg_repr("a{}".format(i)) for i in range(8))
)
def _reg_repr(self, reg):
txt = '{:4}=0x{:08X}'.format(reg, self.get(reg, False))
if reg == 'fp':
reg = 's0'
def _reg_repr(self, reg: str):
txt = "{:4}=0x{:08X}".format(reg, self.get(reg, False))
if reg == "fp":
reg = "s0"
if reg == self.last_set:
return FMT_ORANGE + FMT_BOLD + txt + FMT_NONE
if reg == self.last_read:
return FMT_ORANGE + FMT_UNDERLINE + txt + FMT_NONE
if reg == 'zero':
if reg == "zero":
return txt
if self.get(reg, False) == 0 and reg not in Registers.named_registers():
return FMT_GRAY + txt + FMT_NONE
return txt
def set(self, reg, val: 'Int32', mark_set=True) -> bool:
def set(self, reg: str, val: "Int32", mark_set: bool = True) -> bool:
"""
Set a register content to val
:param reg: The register to set
@ -106,17 +166,20 @@ class Registers:
"""
from .types import Int32
# remove after refactoring is complete
if not isinstance(val, Int32):
raise RuntimeError("Setting register to non-Int32 value! Please refactor your code!")
raise RuntimeError(
"Setting register to non-Int32 value! Please refactor your code!"
)
if reg == 'zero':
if reg == "zero":
return False
# if reg not in Registers.all_registers():
# raise InvalidRegisterException(reg)
# replace fp register with s1, as these are the same register
if reg == 'fp':
reg = 's1'
if reg == "fp":
reg = "s1"
if mark_set:
self.last_set = reg
@ -126,7 +189,7 @@ class Registers:
self.vals[reg] = val.unsigned()
return True
def get(self, reg, mark_read=True) -> 'Int32':
def get(self, reg: str, mark_read: bool = True) -> "Int32":
"""
Retuns the contents of register reg
:param reg: The register name
@ -135,8 +198,8 @@ class Registers:
"""
# if reg not in Registers.all_registers():
# raise InvalidRegisterException(reg)
if reg == 'fp':
reg = 's0'
if reg == "fp":
reg = "s0"
if not self.infinite_regs and reg not in self.valid_regs:
raise RuntimeError("Invalid register: {}".format(reg))
@ -151,13 +214,69 @@ class Registers:
Return a list of all valid registers
:return: The list
"""
return ['zero', 'ra', 'sp', 'gp', 'tp', 's0', 'fp',
't0', 't1', 't2', 't3', 't4', 't5', 't6',
's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11',
'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7',
'ft0', 'ft1', 'ft2', 'ft3', 'ft4', 'ft5', 'ft6', 'ft7',
'fs0', 'fs1', 'fs2', 'fs3', 'fs4', 'fs5', 'fs6', 'fs7', 'fs8', 'fs9', 'fs10', 'fs11',
'fa0', 'fa1', 'fa2', 'fa3', 'fa4', 'fa5', 'fa6', 'fa7']
return [
"zero",
"ra",
"sp",
"gp",
"tp",
"s0",
"fp",
"t0",
"t1",
"t2",
"t3",
"t4",
"t5",
"t6",
"s1",
"s2",
"s3",
"s4",
"s5",
"s6",
"s7",
"s8",
"s9",
"s10",
"s11",
"a0",
"a1",
"a2",
"a3",
"a4",
"a5",
"a6",
"a7",
"ft0",
"ft1",
"ft2",
"ft3",
"ft4",
"ft5",
"ft6",
"ft7",
"fs0",
"fs1",
"fs2",
"fs3",
"fs4",
"fs5",
"fs6",
"fs7",
"fs8",
"fs9",
"fs10",
"fs11",
"fa0",
"fa1",
"fa2",
"fa3",
"fa4",
"fa5",
"fa6",
"fa7",
]
@staticmethod
def named_registers():
@ -165,4 +284,4 @@ class Registers:
Return all named registers
:return: The list
"""
return ['zero', 'ra', 'sp', 'gp', 'tp', 'fp']
return ["zero", "ra", "sp", "gp", "tp", "fp"]

@ -15,17 +15,17 @@ from .types import Int32, CPU
from .types.exceptions import InvalidSyscallException
SYSCALLS = {
63: 'read',
64: 'write',
93: 'exit',
192: 'mmap2',
1024: 'open',
1025: 'close',
63: "read",
64: "write",
93: "exit",
192: "mmap2",
1024: "open",
1025: "close",
}
"""
This dict contains a mapping for all available syscalls (code->name)
If you wish to add a syscall to the built-in system, you can extend this
If you wish to add a syscall to the built-in system, you can extend this
dictionary and implement a method with the same name on the SyscallInterface
class.
"""
@ -43,11 +43,11 @@ A set of additional symbols that are used by various syscalls.
"""
OPEN_MODES = {
0: 'rb',
1: 'wb',
2: 'r+b',
3: 'x',
4: 'ab',
0: "rb",
1: "wb",
2: "r+b",
3: "x",
4: "ab",
}
"""All available file open modes"""
@ -57,6 +57,7 @@ class Syscall:
"""
Represents a syscall
"""
id: int
"""The syscall number (e.g. 64 - write)"""
cpu: CPU
@ -67,12 +68,10 @@ class Syscall:
return SYSCALLS.get(self.id, "unknown")
def __repr__(self):
return "Syscall(id={}, name={})".format(
self.id, self.name
)
return "Syscall(id={}, name={})".format(self.id, self.name)
def ret(self, code: int | Int32):
self.cpu.regs.set('a0', Int32(code))
self.cpu.regs.set("a0", Int32(code))
def get_syscall_symbols():
@ -81,9 +80,7 @@ def get_syscall_symbols():
:return: dictionary of all syscall symbols (SCALL_<name> -> id)
"""
items: Dict[str, int] = {
('SCALL_' + name.upper()): num for num, name in SYSCALLS.items()
}
items: Dict[str, int] = {("SCALL_" + name.upper()): num for num, name in SYSCALLS.items()}
items.update(ADDITIONAL_SYMBOLS)
@ -94,16 +91,13 @@ class SyscallInterface:
"""
Handles syscalls
"""
open_files: Dict[int, IO]
next_open_handle: int
def handle_syscall(self, scall: Syscall):
self.next_open_handle = 3
self.open_files = {
0: sys.stdin,
1: sys.stdout,
2: sys.stderr
}
self.open_files = {0: sys.stdin, 1: sys.stdout, 2: sys.stderr}
if getattr(self, scall.name):
getattr(self, scall.name)(scall)
@ -115,21 +109,25 @@ class SyscallInterface:
read syscall (63): read from file no a0, into addr a1, at most a2 bytes
on return a0 will be the number of read bytes or -1 if an error occured
"""
fileno = scall.cpu.regs.get('a0').unsigned_value
addr = scall.cpu.regs.get('a1').unsigned_value
size = scall.cpu.regs.get('a2').unsigned_value
fileno = scall.cpu.regs.get("a0").unsigned_value
addr = scall.cpu.regs.get("a1").unsigned_value
size = scall.cpu.regs.get("a2").unsigned_value
if fileno not in self.open_files:
scall.ret(-1)
return
chars = self.open_files[fileno].readline(size)
try:
data = bytearray(chars, 'ascii')
data = bytearray(chars, "ascii")
scall.cpu.mmu.write(addr, len(data), data)
return scall.ret(len(data))
except UnicodeEncodeError:
print(FMT_SYSCALL + '[Syscall] read: UnicodeError - invalid input "{}"'.format(chars) + FMT_NONE)
print(
FMT_SYSCALL
+ '[Syscall] read: UnicodeError - invalid input "{}"'.format(chars)
+ FMT_NONE
)
return scall.ret(-1)
def write(self, scall: Syscall):
@ -137,19 +135,23 @@ class SyscallInterface:
write syscall (64): write a2 bytes from addr a1 into fileno a0
on return a0 will hold the number of bytes written or -1 if an error occured
"""
fileno = scall.cpu.regs.get('a0').unsigned_value
addr = scall.cpu.regs.get('a1').unsigned_value
size = scall.cpu.regs.get('a2').unsigned_value
fileno = scall.cpu.regs.get("a0").unsigned_value
addr = scall.cpu.regs.get("a1").unsigned_value
size = scall.cpu.regs.get("a2").unsigned_value
if fileno not in self.open_files:
return scall.ret(-1)
data = scall.cpu.mmu.read(addr, size)
if not isinstance(data, bytearray):
print(FMT_SYSCALL + '[Syscall] write: writing from .text region not supported.' + FMT_NONE)
print(
FMT_SYSCALL
+ "[Syscall] write: writing from .text region not supported."
+ FMT_NONE
)
return scall.ret(-1)
self.open_files[fileno].write(data.decode('ascii'))
self.open_files[fileno].write(data.decode("ascii"))
return scall.ret(size)
def open(self, scall: Syscall):
@ -168,19 +170,29 @@ class SyscallInterface:
"""
# FIXME: this should be toggleable in a global setting or something
if True:
print(FMT_SYSCALL + '[Syscall] open: opening files not supported without scall-fs flag!' + FMT_NONE)
print(
FMT_SYSCALL
+ "[Syscall] open: opening files not supported without scall-fs flag!"
+ FMT_NONE
)
return scall.ret(-1)
mode = scall.cpu.regs.get('a0').unsigned_value
addr = scall.cpu.regs.get('a1').unsigned_value
size = scall.cpu.regs.get('a2').unsigned_value
mode = scall.cpu.regs.get("a0").unsigned_value
addr = scall.cpu.regs.get("a1").unsigned_value
size = scall.cpu.regs.get("a2").unsigned_value
mode_st = OPEN_MODES.get(mode, )
mode_st = OPEN_MODES.get(
mode,
)
if mode_st == -1:
print(FMT_SYSCALL + '[Syscall] open: unknown opening mode {}!'.format(mode) + FMT_NONE)
print(
FMT_SYSCALL
+ "[Syscall] open: unknown opening mode {}!".format(mode)
+ FMT_NONE
)
return scall.ret(-1)
path = scall.cpu.mmu.read(addr, size).decode('ascii')
path = scall.cpu.mmu.read(addr, size).decode("ascii")
fileno = self.next_open_handle
self.next_open_handle += 1
@ -188,10 +200,18 @@ class SyscallInterface:
try:
self.open_files[fileno] = open(path, mode_st)
except OSError as err:
print(FMT_SYSCALL + '[Syscall] open: encountered error during {}!'.format(err.strerror) + FMT_NONE)
print(
FMT_SYSCALL
+ "[Syscall] open: encountered error during {}!".format(err.strerror)
+ FMT_NONE
)
return scall.ret(-1)
print(FMT_SYSCALL + '[Syscall] open: opened fd {} to {}!'.format(fileno, path) + FMT_NONE)
print(
FMT_SYSCALL
+ "[Syscall] open: opened fd {} to {}!".format(fileno, path)
+ FMT_NONE
)
return scall.ret(fileno)
def close(self, scall: Syscall):
@ -200,13 +220,17 @@ class SyscallInterface:
return -1 if an error was encountered, otherwise returns 0
"""
fileno = scall.cpu.regs.get('a0').unsigned_value
fileno = scall.cpu.regs.get("a0").unsigned_value
if fileno not in self.open_files:
print(FMT_SYSCALL + '[Syscall] close: unknown fileno {}!'.format(fileno) + FMT_NONE)
print(
FMT_SYSCALL
+ "[Syscall] close: unknown fileno {}!".format(fileno)
+ FMT_NONE
)
return scall.ret(-1)
self.open_files[fileno].close()
print(FMT_SYSCALL + '[Syscall] close: closed fd {}!'.format(fileno) + FMT_NONE)
print(FMT_SYSCALL + "[Syscall] close: closed fd {}!".format(fileno) + FMT_NONE)
del self.open_files[fileno]
return scall.ret(0)
@ -215,7 +239,7 @@ class SyscallInterface:
Exit syscall. Exits the system with status code a0
"""
scall.cpu.halted = True
scall.cpu.exit_code = scall.cpu.regs.get('a0').value
scall.cpu.exit_code = scall.cpu.regs.get("a0").value
def mmap2(self, scall: Syscall):
"""
@ -265,11 +289,5 @@ class SyscallInterface:
# if that didn't work, return error
return scall.ret(-1)
def __repr__(self):
return "{}(\n\tfiles={}\n)".format(
self.__class__.__name__,
self.open_files
)
return "{}(\n\tfiles={}\n)".format(self.__class__.__name__, self.open_files)

@ -12,9 +12,11 @@ from typing import List, Iterable
from riscemu.decoder import RISCV_REGS
from riscemu.types.exceptions import ParseException
LINE_COMMENT_STARTERS = ('#', ';', '//')
WHITESPACE_PATTERN = re.compile(r'\s+')
MEMORY_ADDRESS_PATTERN = re.compile(r'^(0[xX][A-f0-9]+|\d+|0b[0-1]+|[A-z0-9_-]+)\(([A-z]+[0-9]{0,2})\)$')
LINE_COMMENT_STARTERS = ("#", ";", "//")
WHITESPACE_PATTERN = re.compile(r"\s+")
MEMORY_ADDRESS_PATTERN = re.compile(
r"^(0[xX][A-f0-9]+|\d+|0b[0-1]+|[A-z0-9_-]+)\(([A-z]+[0-9]{0,2})\)$"
)
REGISTER_NAMES = RISCV_REGS
@ -34,22 +36,22 @@ class Token:
def __str__(self):
if self.type == TokenType.NEWLINE:
return '\\n'
return "\\n"
if self.type == TokenType.COMMA:
return ', '
return '{}({})'.format(self.type.name[0:3], self.value)
return ", "
return "{}({})".format(self.type.name[0:3], self.value)
NEWLINE = Token(TokenType.NEWLINE, '\n')
COMMA = Token(TokenType.COMMA, ',')
NEWLINE = Token(TokenType.NEWLINE, "\n")
COMMA = Token(TokenType.COMMA, ",")
def tokenize(input: Iterable[str]) -> Iterable[Token]:
for line in input:
for line_comment_start in LINE_COMMENT_STARTERS:
if line_comment_start in line:
line = line[:line.index(line_comment_start)]
line.strip(' \t\n')
line = line[: line.index(line_comment_start)]
line.strip(" \t\n")
if not line:
continue
@ -64,9 +66,9 @@ def parse_line(parts: List[str]) -> Iterable[Token]:
return ()
first_token = parts[0]
if first_token[0] == '.':
if first_token[0] == ".":
yield Token(TokenType.PSEUDO_OP, first_token)
elif first_token[-1] == ':':
elif first_token[-1] == ":":
yield Token(TokenType.LABEL, first_token)
yield from parse_line(parts[1:])
return
@ -74,14 +76,14 @@ def parse_line(parts: List[str]) -> Iterable[Token]:
yield Token(TokenType.INSTRUCTION_NAME, first_token)
for part in parts[1:]:
if part == ',':
if part == ",":
yield COMMA
continue
yield from parse_arg(part)
def parse_arg(arg: str) -> Iterable[Token]:
comma = arg[-1] == ','
comma = arg[-1] == ","
arg = arg[:-1] if comma else arg
mem_match_resul = re.match(MEMORY_ADDRESS_PATTERN, arg)
if mem_match_resul:
@ -98,7 +100,7 @@ def parse_arg(arg: str) -> Iterable[Token]:
def print_tokens(tokens: Iterable[Token]):
for token in tokens:
print(token, end='\n' if token == NEWLINE else '')
print(token, end="\n" if token == NEWLINE else "")
print("", flush=True, end="")
@ -123,7 +125,7 @@ def split_whitespace_respecting_quotes(line: str) -> Iterable[str]:
part = ""
continue
if c in ' \t\n':
if c in " \t\n":
if part:
yield part
part = ""

@ -1,4 +1,4 @@
from typing import Dict
from typing import Dict, Any
import re
# define some base type aliases so we can keep track of absolute and relative addresses
@ -6,9 +6,9 @@ T_RelativeAddress = int
T_AbsoluteAddress = int
# parser options are just dictionaries with arbitrary values
T_ParserOpts = Dict[str, any]
T_ParserOpts = Dict[str, Any]
NUMBER_SYMBOL_PATTERN = re.compile(r'^\d+[fb]$')
NUMBER_SYMBOL_PATTERN = re.compile(r"^\d+[fb]$")
# base classes
from .flags import MemoryFlags
@ -24,6 +24,16 @@ from .instruction_memory_section import InstructionMemorySection
from .binary_data_memory_section import BinaryDataMemorySection
# exceptions
from .exceptions import ParseException, NumberFormatException, MemoryAccessException, OutOfMemoryException, \
LinkerException, LaunchDebuggerException, RiscemuBaseException, InvalidRegisterException, \
InvalidAllocationException, InvalidSyscallException, UnimplementedInstruction
from .exceptions import (
ParseException,
NumberFormatException,
MemoryAccessException,
OutOfMemoryException,
LinkerException,
LaunchDebuggerException,
RiscemuBaseException,
InvalidRegisterException,
InvalidAllocationException,
InvalidSyscallException,
UnimplementedInstruction,
)

@ -1,29 +1,56 @@
from . import MemorySection, InstructionContext, MemoryFlags, T_RelativeAddress, Instruction
from typing import Optional
from . import (
MemorySection,
InstructionContext,
MemoryFlags,
T_RelativeAddress,
Instruction,
)
from ..types.exceptions import MemoryAccessException
class BinaryDataMemorySection(MemorySection):
def __init__(self, data: bytearray, name: str, context: InstructionContext, owner: str, base: int = 0, flags: MemoryFlags = None):
self.name = name
self.base = base
self.context = context
self.size = len(data)
self.flags = flags if flags is not None else MemoryFlags(False, False)
def __init__(
self,
data: bytearray,
name: str,
context: InstructionContext,
owner: str,
base: int = 0,
flags: Optional[MemoryFlags] = None,
):
super().__init__(
name,
flags if flags is not None else MemoryFlags(False, False),
len(data),
base,
owner,
context,
)
self.data = data
self.owner = owner
def read(self, offset: T_RelativeAddress, size: int) -> bytearray:
if offset + size > self.size:
raise MemoryAccessException("Out of bounds access in {}".format(self), offset, size, 'read')
return self.data[offset:offset + size]
raise MemoryAccessException(
"Out of bounds access in {}".format(self), offset, size, "read"
)
return self.data[offset : offset + size]
def write(self, offset: T_RelativeAddress, size: int, data: bytearray):
if offset + size > self.size:
raise MemoryAccessException("Out of bounds access in {}".format(self), offset, size, 'write')
raise MemoryAccessException(
"Out of bounds access in {}".format(self), offset, size, "write"
)
if len(data[0:size]) != size:
raise MemoryAccessException("Invalid write parameter sizing", offset, size, 'write')
self.data[offset:offset + size] = data[0:size]
raise MemoryAccessException(
"Invalid write parameter sizing", offset, size, "write"
)
self.data[offset : offset + size] = data[0:size]
def read_ins(self, offset: T_RelativeAddress) -> Instruction:
raise MemoryAccessException("Tried reading instruction on non-executable section {}".format(self),
offset, 4, 'instruction fetch')
raise MemoryAccessException(
"Tried reading instruction on non-executable section {}".format(self),
offset,
4,
"instruction fetch",
)

@ -7,8 +7,8 @@ from ..config import RunConfig
from ..colors import FMT_RED, FMT_NONE, FMT_ERROR, FMT_CPU
from . import T_AbsoluteAddress, Instruction, Program, ProgramLoader
if TYPE_CHECKING:
from .. import MMU
if typing.TYPE_CHECKING:
from ..MMU import MMU
from ..instructions import InstructionSet
@ -18,7 +18,7 @@ class CPU(ABC):
# housekeeping variables
regs: Registers
mmu: 'MMU'
mmu: "MMU"
pc: T_AbsoluteAddress
cycle: int
halted: bool
@ -28,12 +28,17 @@ class CPU(ABC):
# instruction information
instructions: Dict[str, Callable[[Instruction], None]]
instruction_sets: Set['InstructionSet']
instruction_sets: Set["InstructionSet"]
# configuration
conf: RunConfig
def __init__(self, mmu: 'MMU', instruction_sets: List[Type['InstructionSet']], conf: RunConfig):
def __init__(
self,
mmu: "MMU",
instruction_sets: List[Type["InstructionSet"]],
conf: RunConfig,
):
self.mmu = mmu
self.regs = Registers(conf.unlimited_registers)
self.conf = conf
@ -75,25 +80,31 @@ class CPU(ABC):
self.pc,
self.cycle,
self.halted,
" ".join(s.name for s in self.instruction_sets)
" ".join(s.name for s in self.instruction_sets),
)
@abstractmethod
def step(self, verbose=False):
def step(self, verbose: bool = False):
pass
@abstractmethod
def run(self, verbose=False):
def run(self, verbose: bool = False):
pass
def launch(self, program: Program, verbose: bool = False):
if program not in self.mmu.programs:
print(FMT_ERROR + '[CPU] Cannot launch program that\'s not loaded!' + FMT_NONE)
print(
FMT_ERROR + "[CPU] Cannot launch program that's not loaded!" + FMT_NONE
)
return
if self.conf.verbosity > 0:
print(FMT_CPU + "[CPU] Started running from {}".format(
self.mmu.translate_address(program.entrypoint)
) + FMT_NONE)
print(
FMT_CPU
+ "[CPU] Started running from {}".format(
self.mmu.translate_address(program.entrypoint)
)
+ FMT_NONE
)
print(program)
self.pc = program.entrypoint
self.run(verbose)

@ -14,63 +14,86 @@ if typing.TYPE_CHECKING:
class RiscemuBaseException(BaseException):
@abstractmethod
def message(self):
pass
def message(self) -> str:
raise NotImplemented
def print_stacktrace(self):
import traceback
traceback.print_exception(type(self), self, self.__traceback__)
# Parsing exceptions:
class ParseException(RiscemuBaseException):
def __init__(self, msg, data=None):
def __init__(self, msg: str, data=None):
super().__init__(msg, data)
self.msg = msg
self.data = data
def message(self):
return FMT_PARSE + "{}(\"{}\", data={})".format(self.__class__.__name__, self.msg, self.data) + FMT_NONE
return (
FMT_PARSE
+ '{}("{}", data={})'.format(self.__class__.__name__, self.msg, self.data)
+ FMT_NONE
)
def ASSERT_EQ(a1, a2):
if a1 != a2:
raise ParseException("ASSERTION_FAILED: Expected elements to be equal!", (a1, a2))
raise ParseException(
"ASSERTION_FAILED: Expected elements to be equal!", (a1, a2)
)
def ASSERT_LEN(a1, size):
if len(a1) != size:
raise ParseException("ASSERTION_FAILED: Expected {} to be of length {}".format(a1, size), (a1, size))
raise ParseException(
"ASSERTION_FAILED: Expected {} to be of length {}".format(a1, size),
(a1, size),
)
def ASSERT_NOT_NULL(a1):
if a1 is None:
raise ParseException("ASSERTION_FAILED: Expected {} to be non null".format(a1), (a1,))
raise ParseException(
"ASSERTION_FAILED: Expected {} to be non null".format(a1), (a1,)
)
def ASSERT_NOT_IN(a1, a2):
if a1 in a2:
raise ParseException("ASSERTION_FAILED: Expected {} to not be in {}".format(a1, a2), (a1, a2))
raise ParseException(
"ASSERTION_FAILED: Expected {} to not be in {}".format(a1, a2), (a1, a2)
)
def ASSERT_IN(a1, a2):
if a1 not in a2:
raise ParseException("ASSERTION_FAILED: Expected {} to not be in {}".format(a1, a2), (a1, a2))
raise ParseException(
"ASSERTION_FAILED: Expected {} to not be in {}".format(a1, a2), (a1, a2)
)
class LinkerException(RiscemuBaseException):
def __init__(self, msg, data):
def __init__(self, msg: str, data):
self.msg = msg
self.data = data
def message(self):
return FMT_PARSE + "{}(\"{}\", data={})".format(self.__class__.__name__, self.msg, self.data) + FMT_NONE
return (
FMT_PARSE
+ '{}("{}", data={})'.format(self.__class__.__name__, self.msg, self.data)
+ FMT_NONE
)
# MMU Exceptions
class MemoryAccessException(RiscemuBaseException):
def __init__(self, msg, addr, size, op):
def __init__(self, msg: str, addr, size, op):
super(MemoryAccessException, self).__init__()
self.msg = msg
self.addr = addr
@ -78,13 +101,13 @@ class MemoryAccessException(RiscemuBaseException):
self.op = op
def message(self):
return FMT_MEM + "{}(During {} at 0x{:08x} of size {}: {})".format(
self.__class__.__name__,
self.op,
self.addr,
self.size,
self.msg
) + FMT_NONE
return (
FMT_MEM
+ "{}(During {} at 0x{:08x} of size {}: {})".format(
self.__class__.__name__, self.op, self.addr, self.size, self.msg
)
+ FMT_NONE
)
class OutOfMemoryException(RiscemuBaseException):
@ -92,10 +115,13 @@ class OutOfMemoryException(RiscemuBaseException):
self.action = action
def message(self):
return FMT_MEM + '{}(Ran out of memory during {})'.format(
self.__class__.__name__,
self.action
) + FMT_NONE
return (
FMT_MEM
+ "{}(Ran out of memory during {})".format(
self.__class__.__name__, self.action
)
+ FMT_NONE
)
class InvalidAllocationException(RiscemuBaseException):
@ -106,28 +132,29 @@ class InvalidAllocationException(RiscemuBaseException):
self.flags = flags
def message(self):
return FMT_MEM + '{}[{}](name={}, size={}, flags={})'.format(
self.__class__.__name__,
self.msg,
self.name,
self.size,
self.flags
return FMT_MEM + "{}[{}](name={}, size={}, flags={})".format(
self.__class__.__name__, self.msg, self.name, self.size, self.flags
)
# CPU Exceptions
class UnimplementedInstruction(RiscemuBaseException):
def __init__(self, ins: 'Instruction', context = None):
def __init__(self, ins: "Instruction", context=None):
self.ins = ins
self.context = context
def message(self):
return FMT_CPU + "{}({}{})".format(
self.__class__.__name__,
repr(self.ins),
', context={}'.format(self.context) if self.context is not None else ''
) + FMT_NONE
return (
FMT_CPU
+ "{}({}{})".format(
self.__class__.__name__,
repr(self.ins),
", context={}".format(self.context) if self.context is not None else "",
)
+ FMT_NONE
)
class InvalidRegisterException(RiscemuBaseException):
@ -135,10 +162,11 @@ class InvalidRegisterException(RiscemuBaseException):
self.reg = reg
def message(self):
return FMT_CPU + "{}(Invalid register {})".format(
self.__class__.__name__,
self.reg
) + FMT_NONE
return (
FMT_CPU
+ "{}(Invalid register {})".format(self.__class__.__name__, self.reg)
+ FMT_NONE
)
class InvalidSyscallException(RiscemuBaseException):
@ -146,10 +174,11 @@ class InvalidSyscallException(RiscemuBaseException):
self.scall = scall
def message(self):
return FMT_SYSCALL + "{}(Invalid syscall: {})".format(
self.__class__.__name__,
self.scall
) + FMT_NONE
return (
FMT_SYSCALL
+ "{}(Invalid syscall: {})".format(self.__class__.__name__, self.scall)
+ FMT_NONE
)
def INS_NOT_IMPLEMENTED(ins):
@ -162,13 +191,10 @@ class NumberFormatException(RiscemuBaseException):
self.msg = msg
def message(self):
return "{}({})".format(
self.__class__.__name__,
self.msg
)
return "{}({})".format(self.__class__.__name__, self.msg)
# this exception is not printed and simply signals that an interactive debugging session is
class LaunchDebuggerException(RiscemuBaseException):
def message(self):
def message(self) -> str:
return ""

@ -8,6 +8,5 @@ class MemoryFlags:
def __repr__(self):
return "r{}{}".format(
'-' if self.read_only else 'w',
'x' if self.executable else '-'
"-" if self.read_only else "w", "x" if self.executable else "-"
)

@ -18,8 +18,8 @@ class InstructionContext:
numbered_labels: Dict[str, List[T_RelativeAddress]]
"""
This dictionary maps numbered labels (which can occur multiple times) to a list of (block-relative) addresses where
the label was placed
This dictionary maps numbered labels (which can occur multiple times) to a list of (block-relative) addresses where
the label was placed
"""
global_symbol_dict: Dict[str, T_AbsoluteAddress]
@ -33,21 +33,32 @@ class InstructionContext:
self.base_address = 0
self.global_symbol_dict = dict()
def resolve_label(self, symbol: str, address_at: Optional[T_RelativeAddress] = None) -> Optional[T_AbsoluteAddress]:
def resolve_label(
self, symbol: str, address_at: Optional[T_RelativeAddress] = None
) -> Optional[T_AbsoluteAddress]:
if NUMBER_SYMBOL_PATTERN.match(symbol):
if address_at is None:
raise ParseException("Cannot resolve relative symbol {} without an address!".format(symbol))
raise ParseException(
"Cannot resolve relative symbol {} without an address!".format(
symbol
)
)
direction = symbol[-1]
values = self.numbered_labels.get(symbol[:-1], [])
if direction == 'b':
return max((addr + self.base_address for addr in values if addr < address_at), default=None)
if direction == "b":
return max(
(addr + self.base_address for addr in values if addr < address_at),
default=None,
)
else:
return min((addr + self.base_address for addr in values if addr > address_at), default=None)
return min(
(addr + self.base_address for addr in values if addr > address_at),
default=None,
)
else:
# if it's not a local symbol, try the globals
if symbol not in self.labels:
return self.global_symbol_dict.get(symbol, None)
# otherwise return the local symbol
return self.labels.get(symbol, None)

@ -1,11 +1,24 @@
from typing import List
from . import MemorySection, Instruction, InstructionContext, MemoryFlags, T_RelativeAddress
from . import (
MemorySection,
Instruction,
InstructionContext,
MemoryFlags,
T_RelativeAddress,
)
from .exceptions import MemoryAccessException
class InstructionMemorySection(MemorySection):
def __init__(self, instructions: List[Instruction], name: str, context: InstructionContext, owner: str, base: int = 0):
def __init__(
self,
instructions: List[Instruction],
name: str,
context: InstructionContext,
owner: str,
base: int = 0,
):
self.name = name
self.base = base
self.context = context
@ -15,13 +28,27 @@ class InstructionMemorySection(MemorySection):
self.owner = owner
def read(self, offset: T_RelativeAddress, size: int) -> bytearray:
raise MemoryAccessException("Cannot read raw bytes from instruction section", self.base + offset, size, 'read')
raise MemoryAccessException(
"Cannot read raw bytes from instruction section",
self.base + offset,
size,
"read",
)
def write(self, offset: T_RelativeAddress, size: int, data: bytearray):
raise MemoryAccessException("Cannot write raw bytes to instruction section", self.base + offset, size, 'write')
raise MemoryAccessException(
"Cannot write raw bytes to instruction section",
self.base + offset,
size,
"write",
)
def read_ins(self, offset: T_RelativeAddress) -> Instruction:
if offset % 4 != 0:
raise MemoryAccessException("Unaligned instruction fetch!", self.base + offset, 4, 'instruction fetch')
raise MemoryAccessException(
"Unaligned instruction fetch!",
self.base + offset,
4,
"instruction fetch",
)
return self.instructions[offset // 4]

@ -1,4 +1,4 @@
from typing import Union
from typing import Any, Union
from ctypes import c_int32, c_uint32
@ -11,13 +11,18 @@ class Int32:
You can use it just like you would any other integer, just be careful when passing it
to functions which actually expect an integer and not a Int32.
"""
_type = c_int32
__slots__ = ('_val',)
__slots__ = ("_val",)
def __init__(self, val: Union[int, c_int32, c_uint32, 'Int32', bytes, bytearray] = 0):
def __init__(
self, val: Union[int, c_int32, c_uint32, "Int32", bytes, bytearray] = 0
):
if isinstance(val, (bytes, bytearray)):
signed = len(val) == 4 and self._type == c_int32
self._val = self.__class__._type(int.from_bytes(val, 'little', signed=signed))
self._val = self.__class__._type(
int.from_bytes(val, "little", signed=signed)
)
elif isinstance(val, self.__class__._type):
self._val = val
elif isinstance(val, (c_uint32, c_int32, Int32)):
@ -26,67 +31,71 @@ class Int32:
self._val = self.__class__._type(val)
else:
raise RuntimeError(
"Unknonw {} input type: {} ({})".format(self.__class__.__name__, type(val), val)
"Unknonw {} input type: {} ({})".format(
self.__class__.__name__, type(val), val
)
)
def __add__(self, other: Union['Int32', int]):
def __add__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value + other)
def __sub__(self, other: Union['Int32', int]):
def __sub__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value - other)
def __mul__(self, other: Union['Int32', int]):
def __mul__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value * other)
def __truediv__(self, other):
def __truediv__(self, other: Any):
return self // other
def __floordiv__(self, other):
def __floordiv__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.__class__(self.value // other)
def __mod__(self, other: Union['Int32', int]):
def __mod__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value % other)
def __and__(self, other: Union['Int32', int]):
def __and__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value & other)
def __or__(self, other: Union['Int32', int]):
def __or__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value | other)
def __xor__(self, other: Union['Int32', int]):
def __xor__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value ^ other)
def __lshift__(self, other: Union['Int32', int]):
def __lshift__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self.value << other)
def __rshift__(self, other: Union['Int32', int]):
def __rshift__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self.value >> other)
def __eq__(self, other: Union['Int32', int]):
if isinstance(other, Int32):
other = other.value
return self.value == other
def __eq__(self, other: object) -> bool:
if isinstance(other, int):
return self.value == other
elif isinstance(other, Int32):
return self.value == other.value
return False
def __neg__(self):
return self.__class__(-self._val.value)
@ -98,33 +107,33 @@ class Int32:
return self.to_bytes(4)
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, self.value)
return "{}({})".format(self.__class__.__name__, self.value)
def __str__(self):
return str(self.value)
def __format__(self, format_spec):
def __format__(self, format_spec: str):
return self.value.__format__(format_spec)
def __hash__(self):
return hash(self.value)
def __gt__(self, other):
def __gt__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value > other
def __lt__(self, other):
def __lt__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value < other
def __le__(self, other):
def __le__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value <= other
def __ge__(self, other):
def __ge__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value >= other
@ -132,38 +141,38 @@ class Int32:
def __bool__(self):
return bool(self.value)
def __cmp__(self, other):
def __cmp__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value.__cmp__(other)
# right handed binary operators
def __radd__(self, other):
def __radd__(self, other: Any):
return self + other
def __rsub__(self, other):
def __rsub__(self, other: Any):
return self.__class__(other) - self
def __rmul__(self, other):
def __rmul__(self, other: Any):
return self * other
def __rtruediv__(self, other):
def __rtruediv__(self, other: Any):
return self.__class__(other) // self
def __rfloordiv__(self, other):
def __rfloordiv__(self, other: Any):
return self.__class__(other) // self
def __rmod__(self, other):
def __rmod__(self, other: Any):
return self.__class__(other) % self
def __rand__(self, other):
def __rand__(self, other: Any):
return self.__class__(other) & self
def __ror__(self, other):
def __ror__(self, other: Any):
return self.__class__(other) | self
def __rxor__(self, other):
def __rxor__(self, other: Any):
return self.__class__(other) ^ self
@property
@ -174,7 +183,7 @@ class Int32:
"""
return self._val.value
def unsigned(self) -> 'UInt32':
def unsigned(self) -> "UInt32":
"""
Convert to an unsigned representation. See :class:Uint32
:return:
@ -188,9 +197,9 @@ class Int32:
:param bytes: The length of the bytearray
:return: A little-endian representation of the contained integer
"""
return bytearray(self.unsigned_value.to_bytes(4, 'little'))[0:bytes]
return bytearray(self.unsigned_value.to_bytes(4, "little"))[0:bytes]
def signed(self) -> 'Int32':
def signed(self) -> "Int32":
"""
Convert to a signed representation. See :class:Int32
:return:
@ -207,7 +216,7 @@ class Int32:
"""
return c_uint32(self.value).value
def shift_right_logical(self, ammount: Union['Int32', int]) -> 'Int32':
def shift_right_logical(self, ammount: Union["Int32", int]) -> "Int32":
"""
This function implements logical right shifts, meaning that the sign bit is shifted as well.
@ -237,12 +246,12 @@ class Int32:
:return: An instance of Int32, holding the sign-extended value
"""
if isinstance(data, (bytes, bytearray)):
data = int.from_bytes(data, 'little')
data = int.from_bytes(data, "little")
sign = data >> (bits - 1)
if sign > 1:
print("overflow in Int32.sext!")
if sign:
data = (data & (2 ** (bits - 1) - 1)) - 2**(bits-1)
data = (data & (2 ** (bits - 1) - 1)) - 2 ** (bits - 1)
return cls(data)
@ -250,9 +259,10 @@ class UInt32(Int32):
"""
An unsigned version of :class:Int32.
"""
_type = c_uint32
def unsigned(self) -> 'UInt32':
def unsigned(self) -> "UInt32":
"""
Return a new instance representing the same bytes, but signed
:return:
@ -263,11 +273,13 @@ class UInt32(Int32):
def unsigned_value(self) -> int:
return self._val.value
def shift_right_logical(self, ammount: Union['Int32', int]) -> 'UInt32':
def shift_right_logical(self, ammount: Union["Int32", int]) -> "UInt32":
"""
see :meth:`Int32.shift_right_logical <Int32.shift_right_logical>`
:param ammount: Number of positions to shift
:return: A new Int32 object representing the shifted value (keeps the signed-ness of the source)
"""
return self >> ammount
if isinstance(ammount, Int32):
ammount = ammount.value
return UInt32(self.value >> ammount)

@ -4,7 +4,14 @@ from typing import Optional
from ..colors import FMT_MEM, FMT_NONE, FMT_UNDERLINE, FMT_ORANGE, FMT_ERROR
from ..helpers import format_bytes
from . import MemoryFlags, T_AbsoluteAddress, InstructionContext, T_RelativeAddress, Instruction, Int32
from . import (
MemoryFlags,
T_AbsoluteAddress,
InstructionContext,
T_RelativeAddress,
Instruction,
Int32,
)
@dataclass
@ -32,8 +39,16 @@ class MemorySection(ABC):
def read_ins(self, offset: T_RelativeAddress) -> Instruction:
pass
def dump(self, start: T_RelativeAddress, end: Optional[T_RelativeAddress] = None, fmt: str = None,
bytes_per_row: int = None, rows: int = 10, group: int = None, highlight: int = None):
def dump(
self,
start: T_RelativeAddress,
end: Optional[T_RelativeAddress] = None,
fmt: Optional[str] = None,
bytes_per_row: Optional[int] = None,
rows: int = 10,
group: Optional[int] = None,
highlight: Optional[int] = None,
):
"""
Dump the section. If no end is given, the rows around start are printed and start is highlighted.
@ -54,11 +69,11 @@ class MemorySection(ABC):
if fmt is None:
if self.flags.executable and self.flags.read_only:
bytes_per_row = 4
fmt = 'asm'
fmt = "asm"
else:
fmt = 'hex'
fmt = "hex"
if fmt == 'char':
if fmt == "char":
if bytes_per_row is None:
bytes_per_row = 4
if group is None:
@ -70,50 +85,82 @@ class MemorySection(ABC):
if bytes_per_row is None:
bytes_per_row = 4
if fmt not in ('asm', 'hex', 'int', 'char'):
print(FMT_ERROR + '[MemorySection] Unknown format {}, known formats are {}'.format(
fmt, ", ".join(('asm', 'hex', 'int', 'char'))
) + FMT_NONE)
if fmt not in ("asm", "hex", "int", "char"):
print(
FMT_ERROR
+ "[MemorySection] Unknown format {}, known formats are {}".format(
fmt, ", ".join(("asm", "hex", "int", "char"))
)
+ FMT_NONE
)
if end is None:
end = min(start + (bytes_per_row * (rows // 2)), self.size)
highlight = start
start = max(0, start - (bytes_per_row * (rows // 2)))
if fmt == 'asm':
print(FMT_MEM + "{}, viewing {} instructions:".format(
self, (end - start) // 4
) + FMT_NONE)
if fmt == "asm":
print(
FMT_MEM
+ "{}, viewing {} instructions:".format(self, (end - start) // 4)
+ FMT_NONE
)
for addr in range(start, end, 4):
if addr == highlight:
print(FMT_UNDERLINE + FMT_ORANGE, end='')
print("0x{:04x}: {}{}".format(
self.base + addr, self.read_ins(addr), FMT_NONE
))
print(FMT_UNDERLINE + FMT_ORANGE, end="")
print(
"0x{:04x}: {}{}".format(
self.base + addr, self.read_ins(addr), FMT_NONE
)
)
else:
print(FMT_MEM + "{}, viewing {} bytes:".format(
self, (end - start)
) + FMT_NONE)
print(
FMT_MEM + "{}, viewing {} bytes:".format(self, (end - start)) + FMT_NONE
)
aligned_end = end - (end % bytes_per_row) if end % bytes_per_row != 0 else end
aligned_end = (
end - (end % bytes_per_row) if end % bytes_per_row != 0 else end
)
for addr in range(start, aligned_end, bytes_per_row):
hi_ind = (highlight - addr) // group if highlight is not None else -1
print("0x{:04x}: {}{}".format(
self.base + addr, format_bytes(self.read(addr, bytes_per_row), fmt, group, hi_ind), FMT_NONE
))
print(
"0x{:04x}: {}{}".format(
self.base + addr,
format_bytes(
self.read(addr, bytes_per_row), fmt, group, hi_ind
),
FMT_NONE,
)
)
if aligned_end != end:
hi_ind = (highlight - aligned_end) // group if highlight is not None else -1
print("0x{:04x}: {}{}".format(
self.base + aligned_end, format_bytes(
self.read(aligned_end, end % bytes_per_row), fmt, group, hi_ind
), FMT_NONE
))
def dump_all(self, *args, **kwargs):
self.dump(0, self.size, *args, **kwargs)
hi_ind = (
(highlight - aligned_end) // group if highlight is not None else -1
)
print(
"0x{:04x}: {}{}".format(
self.base + aligned_end,
format_bytes(
self.read(aligned_end, end % bytes_per_row),
fmt,
group,
hi_ind,
),
FMT_NONE,
)
)
def dump_all(
self,
fmt: Optional[str] = None,
bytes_per_row: Optional[int] = None,
rows: int = 10,
group: Optional[int] = None,
highlight: Optional[int] = None,
):
self.dump(0, self.size, fmt, bytes_per_row, rows, group, highlight)
def __repr__(self):
return "{}[{}] at 0x{:08X} (size={}bytes, flags={}, owner={})".format(
@ -122,5 +169,5 @@ class MemorySection(ABC):
self.base,
self.size,
self.flags,
self.owner
self.owner,
)

@ -14,6 +14,7 @@ class Program:
the offset in the program, and everything will be taken care of for you.
"""
name: str
context: InstructionContext
global_labels: Set[str]
@ -44,9 +45,13 @@ class Program:
if self.base is not None:
if sec.base < self.base:
print(
FMT_RED + FMT_BOLD + "WARNING: memory section {} in {} is placed before program base (0x{:x})".format(
FMT_RED
+ FMT_BOLD
+ "WARNING: memory section {} in {} is placed before program base (0x{:x})".format(
sec, self.name, self.base
) + FMT_NONE)
)
+ FMT_NONE
)
self.sections.append(sec)
# keep section list ordered
@ -54,18 +59,21 @@ class Program:
def __repr__(self):
return "{}(name={},sections={},base={})".format(
self.__class__.__name__, self.name, self.global_labels,
[s.name for s in self.sections], self.base
self.__class__.__name__,
self.name,
self.global_labels,
[s.name for s in self.sections],
self.base,
)
@property
def entrypoint(self):
if '_start' in self.context.labels:
return self.context.labels.get('_start')
if 'main' in self.context.labels:
return self.context.labels.get('main')
if "_start" in self.context.labels:
return self.context.labels.get("_start")
if "main" in self.context.labels:
return self.context.labels.get("main")
for sec in self.sections:
if get_section_base_name(sec.name) == '.text' and sec.flags.executable:
if get_section_base_name(sec.name) == ".text" and sec.flags.executable:
return sec.base
def loaded_trigger(self, at_addr: T_AbsoluteAddress):
@ -81,12 +89,17 @@ class Program:
"""
if self.is_loaded:
if at_addr != self.base:
raise RuntimeError("Program loaded twice at different addresses! This will probably break things!")
raise RuntimeError(
"Program loaded twice at different addresses! This will probably break things!"
)
return
if self.base is not None and self.base != at_addr:
print(FMT_MEM + 'WARNING: Program loaded at different address then expected! (loaded at {}, '
'but expects to be loaded at {})'.format(at_addr, self.base) + FMT_NONE)
print(
FMT_MEM
+ "WARNING: Program loaded at different address then expected! (loaded at {}, "
"but expects to be loaded at {})".format(at_addr, self.base) + FMT_NONE
)
# check if we are relocating
if self.base != at_addr:

@ -39,7 +39,7 @@ class ProgramLoader(ABC):
pass
@classmethod
def instantiate(cls, source_path: str, options: T_ParserOpts) -> 'ProgramLoader':
def instantiate(cls, source_path: str, options: T_ParserOpts) -> "ProgramLoader":
"""
Instantiate a loader for the given source file with the required arguments

@ -5,8 +5,13 @@ from ..helpers import parse_numeric_argument
class SimpleInstruction(Instruction):
def __init__(self, name: str, args: Union[Tuple[()], Tuple[str], Tuple[str, str], Tuple[str, str, str]],
context: InstructionContext, addr: T_RelativeAddress):
def __init__(
self,
name: str,
args: Union[Tuple[()], Tuple[str], Tuple[str, str], Tuple[str, str, str]],
context: InstructionContext,
addr: T_RelativeAddress,
):
self.context = context
self.name = name
self.args = args
@ -23,4 +28,3 @@ class SimpleInstruction(Instruction):
def get_reg(self, num: int) -> str:
return self.args[num]

@ -23,9 +23,14 @@ setuptools.setup(
"Operating System :: OS Independent",
],
package_dir={"": "."},
packages=["riscemu", "riscemu.decoder", "riscemu.instructions", "riscemu.IO", "riscemu.priv", "riscemu.types"],
packages=[
"riscemu",
"riscemu.decoder",
"riscemu.instructions",
"riscemu.IO",
"riscemu.priv",
"riscemu.types",
],
python_requires=">=3.6",
install_requires=[
"pyelftools~=0.27"
]
)
install_requires=["pyelftools~=0.27"],
)

@ -1,4 +1,4 @@
build
source/riscemu*.rst
source/modules.rst
source/help
source/help

@ -18,37 +18,34 @@ import subprocess
# import sys
# sys.path.insert(0, os.path.abspath('.'))
if os.getenv('READTHEDOCS', False) and not os.path.exists('riscemu.rst'):
subprocess.check_call(['../../generate-docs.sh', 'generate'])
if os.getenv("READTHEDOCS", False) and not os.path.exists("riscemu.rst"):
subprocess.check_call(["../../generate-docs.sh", "generate"])
# -- Project information -----------------------------------------------------
project = 'RiscEmu'
copyright = '2022, Anton Lydike'
author = 'Anton Lydike'
project = "RiscEmu"
copyright = "2022, Anton Lydike"
author = "Anton Lydike"
# The full version, including alpha/beta/rc tags
release = '2.0.0a2'
release = "2.0.0a2"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'recommonmark'
]
extensions = ["sphinx.ext.autodoc", "recommonmark"]
# autodoc options
autodoc_default_options = {
'members': True,
'member-order': 'bysource',
'special-members': '__init__',
"members": True,
"member-order": "bysource",
"special-members": "__init__",
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@ -58,10 +55,10 @@ templates_path = ['_templates']
from recommonmark.parser import CommonMarkParser
# The suffix of source filenames.
source_suffix = ['.rst', '.md']
source_suffix = [".rst", ".md"]
source_parsers = {
'.md': CommonMarkParser,
".md": CommonMarkParser,
}
# -- Options for HTML output -------------------------------------------------
@ -69,18 +66,18 @@ source_parsers = {
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
html_theme = "alabaster"
pygments_style = 'sphinx'
pygments_style = "sphinx"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
sys.path.insert(0, os.path.abspath('../../'))
sys.path.insert(0, os.path.abspath("../../"))
if os.getenv('READTHEDOCS', False):
if os.getenv("READTHEDOCS", False):
import sphinx_rtd_theme
extensions.append("sphinx_rtd_theme")

@ -59,4 +59,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

@ -1,3 +1,3 @@
from .test_tokenizer import *
from .test_helpers import *
from .test_integers import *
from .test_integers import *

@ -1,73 +0,0 @@
import contextlib
import os
from abc import abstractmethod
from tempfile import NamedTemporaryFile
from typing import Optional, Union, Tuple
from unittest import TestCase
from riscemu import CPU, UserModeCPU, InstructionSetDict, RunConfig
from riscemu.types import Program
class EndToEndTest(TestCase):
def __init__(self, cpu: Optional[CPU] = None):
super().__init__()
if cpu is None:
cpu = UserModeCPU(InstructionSetDict.values(), RunConfig())
self.cpu = cpu
@abstractmethod
def get_source(self) -> Tuple[str, Union[bytes, str, bytearray]]:
"""
This method returns the source code of the program
:return:
"""
pass
def test_run_program(self):
"""
Runs the program and verifies output
:return:
"""
with self.with_source_file() as names:
fname, orig_name = names
loader = self.cpu.get_best_loader_for(fname)
self.program = loader.instantiate(fname, loader.get_options([])).parse()
self._change_program_file_name(self.program, orig_name)
self.cpu.load_program(self.program)
self.after_program_load(self.program)
if isinstance(self.cpu, UserModeCPU):
self.cpu.setup_stack()
try:
self.cpu.launch(self.program)
except Exception as ex:
if self.is_exception_expected(ex):
pass
raise ex
@contextlib.contextmanager
def with_source_file(self):
name, content = self.get_source()
if isinstance(content, str):
f = NamedTemporaryFile('w', suffix=name, delete=False)
else:
f = NamedTemporaryFile('wb', suffix=name, delete=False)
f.write(content)
f.flush()
f.close()
try:
yield f.name, name
finally:
os.unlink(f.name)
def after_program_load(self, program):
pass
def is_exception_expected(self, ex: Exception) -> bool:
return False
def _change_program_file_name(self, program: Program, new_name: str):
program.name = new_name
for sec in program.sections:
sec.owner = new_name

@ -0,0 +1,2 @@
8.096814e-02 hello-world.asm
9.465098e-02 fibs.asm

@ -0,0 +1,23 @@
// RUN: python3 -m riscemu -v --ignore-exit-code %s || true | filecheck %s
.data
fibs: .space 1024
.text
main:
addi s1, zero, 0 // storage index
addi s2, zero, 1024 // last storage index
addi t0, zero, 1 // t0 = F_{i}
addi t1, zero, 1 // t1 = F_{i+1}
loop:
sw t0, fibs(s1) // save
add t2, t1, t0 // t2 = F_{i+2}
addi t0, t1, 0 // t0 = t1
addi t1, t2, 0 // t1 = t2
addi s1, s1, 4 // increment storage pointer
blt s1, s2, loop // loop as long as we did not reach array length
// exit gracefully
add a0, zero, t2
addi a7, zero, 93
scall // exit with code 0
// CHECK: [CPU] Program exited with code 1265227608

@ -0,0 +1,15 @@
// RUN: python3 -m riscemu -v %s | filecheck %s
.data
msg: .ascii "Hello world\n"
.text
addi a0, zero, 1 // print to stdout
addi a1, zero, msg // load msg address
addi a2, zero, 12 // write 12 bytes
addi a7, zero, SCALL_WRITE // write syscall code
scall
addi a0, zero, 0 // set exit code to 0
addi a7, zero, SCALL_EXIT // exit syscall code
scall
// CHECK: Hello world
// CHECK: [CPU] Program exited with code 0

@ -0,0 +1,9 @@
import lit.formats
import os
config.test_source_root = os.path.dirname(__file__)
xdsl_src = os.path.dirname(os.path.dirname(config.test_source_root))
config.name = "riscemu"
config.test_format = lit.formats.ShTest(preamble_commands=[f"cd {xdsl_src}"])
config.suffixes = ['.asm']

@ -1,7 +1,15 @@
from unittest import TestCase
from riscemu.helpers import *
class TestHelpers(TestCase):
pass
def test_align_address():
assert align_addr(3, 1) == 3
assert align_addr(3, 2) == 4
assert align_addr(3, 4) == 4
assert align_addr(3, 8) == 8
assert align_addr(8, 8) == 8
def test_parse_numeric():
assert parse_numeric_argument("13") == 13
assert parse_numeric_argument("0x100") == 256
assert parse_numeric_argument("-13") == -13

@ -1,19 +1,15 @@
from unittest import TestCase
from riscemu.types import Int32, UInt32
class TestTokenizer(TestCase):
def test_logical_right_shift(self):
a = Int32(100)
self.assertEqual(a.shift_right_logical(0), a)
self.assertEqual(a.shift_right_logical(10), 0)
self.assertEqual(a.shift_right_logical(1), 100>>1)
def test_logical_right_shift():
a = Int32(100)
assert a.shift_right_logical(0) == a
assert a.shift_right_logical(10) == 0
assert a.shift_right_logical(1) == 100 >> 1
a = Int32(-100)
self.assertEqual(a.shift_right_logical(0), a)
self.assertEqual(a.shift_right_logical(1), 2147483598)
self.assertEqual(a.shift_right_logical(10), 4194303)
self.assertEqual(a.shift_right_logical(31), 1)
self.assertEqual(a.shift_right_logical(32), 0)
a = Int32(-100)
assert a.shift_right_logical(0) == a
assert a.shift_right_logical(1) == 2147483598
assert a.shift_right_logical(10) == 4194303
assert a.shift_right_logical(31) == 1
assert a.shift_right_logical(32) == 0

@ -38,38 +38,44 @@ def get_arg_from_ins(ins: Instruction, num: int, cpu: CPU):
assert_ops = {
'==': assert_equals,
'!=': _not(assert_equals),
'in': assert_in,
'not_in': _not(assert_in),
"==": assert_equals,
"!=": _not(assert_equals),
"in": assert_in,
"not_in": _not(assert_in),
}
class TestIS(InstructionSet):
def __init__(self, cpu: 'CPU'):
print('[Test] loading testing ISA, this is only meant for running testcases and is not part of the RISC-V ISA!')
class Z_test(InstructionSet):
def __init__(self, cpu: "CPU"):
print(
"[Test] loading testing ISA, this is only meant for running testcases and is not part of the RISC-V ISA!"
)
self.failed = False
super().__init__(cpu)
def instruction_assert(self, ins: Instruction):
if len(ins.args) < 3:
print(FMT_ERROR + '[Test] Unknown assert statement: {}'.format(ins) + FMT_NONE)
print(
FMT_ERROR + "[Test] Unknown assert statement: {}".format(ins) + FMT_NONE
)
return
op = ins.args[1]
if op not in assert_ops:
print(FMT_ERROR + '[Test] Unknown operation statement: {} in {}'.format(op, ins) + FMT_NONE)
print(
FMT_ERROR
+ "[Test] Unknown operation statement: {} in {}".format(op, ins)
+ FMT_NONE
)
return
if assert_ops[op](ins, self.cpu):
print(FMT_SUCCESS + '[TestCase] 🟢 passed assertion {}'.format(ins))
print(FMT_SUCCESS + "[TestCase] 🟢 passed assertion {}".format(ins))
else:
print(FMT_ERROR + '[TestCase] 🔴 failed assertion {}'.format(ins))
print(FMT_ERROR + "[TestCase] 🔴 failed assertion {}".format(ins))
self.cpu.halted = True
self.failed = True
def instruction_fail(self, ins: Instruction):
print(FMT_ERROR + '[TestCase] 🔴 reached fail instruction! {}'.format(ins))
self.cpu.halted = True
self.failed = True
def assert_mem(self, ins: Instruction):
print(FMT_ERROR + "[TestCase] 🔴 reached fail instruction! {}".format(ins))
self.cpu.halted = True
self.failed = True

@ -1,7 +1,14 @@
from unittest import TestCase
from riscemu.tokenizer import tokenize, print_tokens, Token, TokenType, NEWLINE, COMMA, \
split_whitespace_respecting_quotes
from riscemu.tokenizer import (
tokenize,
print_tokens,
Token,
TokenType,
NEWLINE,
COMMA,
split_whitespace_respecting_quotes,
)
def ins(name: str) -> Token:
@ -21,41 +28,49 @@ def lbl(name: str) -> Token:
class TestTokenizer(TestCase):
def test_instructions(self):
program = [
'li a0, 144',
'divi a0, a0, 12',
'xori a1, a0, 12'
]
program = ["li a0, 144", "divi a0, a0, 12", "xori a1, a0, 12"]
tokens = [
ins('li'), arg('a0'), COMMA, arg('144'), NEWLINE,
ins('divi'), arg('a0'), COMMA, arg('a0'), COMMA, arg('12'), NEWLINE,
ins('xori'), arg('a1'), COMMA, arg('a0'), COMMA, arg('12'), NEWLINE,
ins("li"),
arg("a0"),
COMMA,
arg("144"),
NEWLINE,
ins("divi"),
arg("a0"),
COMMA,
arg("a0"),
COMMA,
arg("12"),
NEWLINE,
ins("xori"),
arg("a1"),
COMMA,
arg("a0"),
COMMA,
arg("12"),
NEWLINE,
]
self.assertEqual(list(tokenize(program)), tokens)
def test_comments(self):
parsed_res = [
ins('li'), arg('a0'), COMMA, arg('144'), NEWLINE
]
for c in ('#', '//', ';'):
lines = [
c + ' this is a comment',
'li a0, 144'
]
parsed_res = [ins("li"), arg("a0"), COMMA, arg("144"), NEWLINE]
for c in ("#", "//", ";"):
lines = [c + " this is a comment", "li a0, 144"]
self.assertEqual(list(tokenize(lines)), parsed_res)
def test_pseudo_ins(self):
parsed_res = [
Token(TokenType.PSEUDO_OP, '.section'), Token(TokenType.ARGUMENT, '.text'), NEWLINE,
Token(TokenType.PSEUDO_OP, '.type'), Token(TokenType.ARGUMENT, 'init'), COMMA,
Token(TokenType.ARGUMENT, '@function'), NEWLINE
]
input_program = [
'.section .text',
'.type init, @function'
Token(TokenType.PSEUDO_OP, ".section"),
Token(TokenType.ARGUMENT, ".text"),
NEWLINE,
Token(TokenType.PSEUDO_OP, ".type"),
Token(TokenType.ARGUMENT, "init"),
COMMA,
Token(TokenType.ARGUMENT, "@function"),
NEWLINE,
]
input_program = [".section .text", ".type init, @function"]
self.assertEqual(list(tokenize(input_program)), parsed_res)
def test_full_program(self):
@ -71,24 +86,40 @@ section:
sub s0, s0, s0
"""
tokens = [
op('.section'), arg('.text'), NEWLINE,
ins('addi'), arg('sp'), COMMA, arg('sp'), COMMA, arg('-32'), NEWLINE,
ins('sw'), arg('s0'), COMMA, arg('ra'), arg('0'), NEWLINE,
lbl('section:'), NEWLINE,
ins('sub'), arg('s0'), COMMA, arg('s0'), COMMA, arg('s0'), NEWLINE
op(".section"),
arg(".text"),
NEWLINE,
ins("addi"),
arg("sp"),
COMMA,
arg("sp"),
COMMA,
arg("-32"),
NEWLINE,
ins("sw"),
arg("s0"),
COMMA,
arg("ra"),
arg("0"),
NEWLINE,
lbl("section:"),
NEWLINE,
ins("sub"),
arg("s0"),
COMMA,
arg("s0"),
COMMA,
arg("s0"),
NEWLINE,
]
self.assertEqual(list(tokenize(program.splitlines())), tokens)
def test_split_whitespace_respecting_quotes_single(self):
self.assertEqual(
list(split_whitespace_respecting_quotes("test")), ["test"]
)
self.assertEqual(list(split_whitespace_respecting_quotes("test")), ["test"])
def test_split_whitespace_respecting_quotes_empty(self):
self.assertEqual(
list(split_whitespace_respecting_quotes("")), []
)
self.assertEqual(list(split_whitespace_respecting_quotes("")), [])
def test_split_whitespace_respecting_quotes_two_parts(self):
self.assertEqual(
@ -107,20 +138,24 @@ section:
def test_split_whitespace_respecting_quotes_quoted_then_normal(self):
self.assertEqual(
list(split_whitespace_respecting_quotes('"test 123" abc')), ["test 123", "abc"]
list(split_whitespace_respecting_quotes('"test 123" abc')),
["test 123", "abc"],
)
def test_split_whitespace_respecting_quotes_quoted_sorrounded(self):
self.assertEqual(
list(split_whitespace_respecting_quotes('hello "test 123" abc')), ["hello", "test 123", "abc"]
list(split_whitespace_respecting_quotes('hello "test 123" abc')),
["hello", "test 123", "abc"],
)
def test_split_whitespace_respecting_quotes_weird_spaces(self):
self.assertEqual(
list(split_whitespace_respecting_quotes('hello "test 123"\tabc')), ["hello", "test 123", "abc"]
list(split_whitespace_respecting_quotes('hello "test 123"\tabc')),
["hello", "test 123", "abc"],
)
def test_split_whitespace_respecting_quotes_quotes_no_spaces(self):
self.assertEqual(
list(split_whitespace_respecting_quotes('hello"test 123"abc')), ["hello", "test 123", "abc"]
list(split_whitespace_respecting_quotes('hello"test 123"abc')),
["hello", "test 123", "abc"],
)

@ -1,53 +0,0 @@
from riscemu import AssemblyFileLoader
from riscemu.colors import *
FMT_SUCCESS = FMT_GREEN + FMT_BOLD
def run_test(path: str):
from riscemu import CPU, UserModeCPU, RunConfig
from riscemu.instructions import InstructionSetDict
from test.test_isa import TestIS
import os
fname = os.path.basename(path)
ISAs = list(InstructionSetDict.values())
ISAs.append(TestIS)
cpu = UserModeCPU(ISAs, RunConfig())
try:
program = AssemblyFileLoader(path, {}).parse()
cpu.load_program(program)
cpu.launch(program)
except Exception as ex:
print(FMT_ERROR + '[Test] 🔴 failed with exception "{}" ({})'.format(ex, fname) + FMT_NONE)
raise ex
if cpu.halted:
for isa in cpu.instruction_sets:
if isinstance(isa, TestIS):
if not isa.failed:
print(FMT_SUCCESS + '[Test] 🟢 successful {}'.format(fname) + FMT_NONE)
return not isa.failed
return False
if __name__ == '__main__':
import os
import glob
successes = 0
failures = 0
ttl = 0
for path in glob.glob(f'{os.path.dirname(__file__)}/*.asm'):
print(FMT_BLUE + '[Test] running testcase ' + os.path.basename(path) + FMT_NONE)
ttl += 1
if run_test(path):
successes += 1
else:
failures += 1

@ -1,7 +0,0 @@
.data
data:
.word 0xFFFFFFFF, 0x0000FFFF, 0xFF00FF00, 0x7FFFFFFF
.text
ebreak

@ -1,20 +0,0 @@
.text
main:
addi a0, zero, main
addi a1, zero, main
addi t0, zero, 1000
assert a0, ==, 0x100
1:
addi a1, a1, 1
blt a1, t0, 1b
sub a1, a1, a0
j 1f
addi a1, zero, 0
fail
1:
assert a1, ==, 744
add a0, zero, a1 ; set exit code to a1
addi a7, zero, SCALL_EXIT ; exit syscall code
scall
fail
Loading…
Cancel
Save