Compare commits

..

310 Commits
main ... master

Author SHA1 Message Date
Anton Lydike f8b979b08a fix release workflow 1 year ago
Anton Lydike d102376212 release 2.1.1 1 year ago
KGrykiel 3b89387e0a
Debugger fix (#32)
fixed an issue where debugger had an error when trying to dump registers. It tried to use get() method instead of get_f() method. Also changed format of floating point registers to show up as floats instead of Hex.
1 year ago
Sasha Lopoukhine 07265f26c9
allow for infinite registers in sw/lw instructions (#31) 1 year ago
Alban Dutilleul 801b165e70
fix various semantic mismatchs in RV32F (#27) 1 year ago
Alban Dutilleul 7e34855ad1
format using black (#28) 1 year ago
Anton Lydike 26d7f65cc1 add deployment github workflow 1 year ago
Anton Lydike 2ec134f612 bump version to 2.1.0 1 year ago
Anton Lydike be90879f86
Add support for floats (#22)
Adding a `Float32` datatype is necessary, since python makes no guarantees to the bitwidth of `float` (it's often a double)

Also adding the `RV32F` extension with most operations implemented, and support for floating point registers.
1 year ago
Sasha Lopoukhine 5a23804ad8
add py.typed file for riscemu to declare itself as a typed python package (#21)
* add py.typed to setup.py package_data

* create py.typed file

* move py.typed
1 year ago
Sasha Lopoukhine a217705d1f add a couple of type annotations in parser.py 1 year ago
Anton Lydike d508e01a6b misc: test python 3.8 in CI, add cfg arg to RiscemuRunner 1 year ago
Anton Lydike 47a9b12263 misc: improve typing and structure 1 year ago
Anton Lydike 1c2dad94e2 main: major overhaul of how __main__ works 1 year ago
Anton Lydike 8ac4a56c08 libc: add tests and fix a bunch of bugs in string.s 1 year ago
Anton Lydike 41d17daeaf syscall: reformat and fix exit() for signed integers 1 year ago
Anton Lydike 283bb1ae14 core: refactor how launch() works in preperation for libc integration 1 year ago
Anton Lydike 270c3e7090 assembler: fix bug with zero-termination of strings 1 year ago
Anton Lydike 86250157b7 regs: add __repr__ 1 year ago
Anton Lydike dfc0ed7862 updated .idea 1 year ago
Anton Lydike d9058a0ca0 add lit gitignore 1 year ago
Anton Lydike 1bb0770061 remove lit artifact 1 year ago
Anton Lydike 1bd754953c runtime: move start of libc into separate folder 1 year ago
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
Anton Lydike 1b26497e4c base: add debug instructions 2 years ago
Anton Lydike b5ebe13528 release 2.0.5 2 years ago
Anton Lydike 51d23a1630 updated .idea files 2 years ago
Anton Lydike 636e06f243 add an unlimited register mode 2 years ago
Anton Lydike 86063d64d7 version update: 2.0.4 2 years ago
Anton Lydike 5caf0d604d rv32i: respect conf.debug_instruction setting 2 years ago
Anton Lydike 36e8c9c9ce misc: fixed a typo in a docstring 2 years ago
Emilien Bauer f7e7c41034
Deploy RiscEmu in a JupyterLite distribution. (#18)
* Try and deploy a RiscEmu JupyterLite distribution.

* Add examples to the JupyterLite deployment.

* Add some demo notebooks and link to it in Readme.

Co-authored-by: Emilien Bauer <bauer.emilien@gmail.com>
2 years ago
K-W-Li 1d65b236f4
Fix sign issue in parse_rd_rs_rs (#16) 2 years ago
Anton Lydike be0591ed25 release of 2.0.3 3 years ago
Anton Lydike d353353748 fixed link in syscall docs 3 years ago
Anton Lydike ce5b01d463 Parser: fixed error when labels where used outside of sections 3 years ago
Anton Lydike 204d2c9a5b Syscalls: cleaned up formatting and added instructions for extensions 3 years ago
Anton Lydike 10a3c4201e cleaned up and improved memory dumping code 3 years ago
Anton Lydike 5a07770427 fixed a bug with hex literal recognition 3 years ago
Anton Lydike 39e759ae9b fixed bug where wrong parts of section would be printed in mmu.dump() 3 years ago
Anton Lydike 65903189a2 removed tests for bind_twos_complement 3 years ago
Anton Lydike 629786931e fixed address translation error for sections without symbols 3 years ago
Anton Lydike bb770ab43c Logging: Changed verbosity levels at which things are logged for the CPU 3 years ago
Anton Lydike 8dea2036c5 added changelog - release of 2.0.2 3 years ago
Anton Lydike 83d8412fff add /build to idea excluded dirs 3 years ago
Anton Lydike 0c39a9eefb fixes #12 - exit program with code from cpu.exit_code 3 years ago
Anton Lydike 761799c17a Parser: fixes #13 - implicit start of text section when parsing assembly
When an assembly file starts with instructions without explicitly
declaring any section beforehand, a .text section will be created
implicitly.
3 years ago
Anton Lydike baf4485143 release of 2.0.1 3 years ago
Anton Lydike 4d2d65a10d fixed type annotations type in parsers 3 years ago
Anton Lydike d18843a88b release of 2.0.0 3 years ago
Anton Lydike 4b77ce05a7
Merge pull request #5 from AntonLydike/assembly-parser-rework
- Completely revamped parsing of readable assembly
 - Completely revamped internal types
 - Added hard and correct 32 bit integer handling in registers
3 years ago
Anton Lydike fe4b3efb6f fixes #10 - fixed how preconfigured memory is handled 3 years ago
Anton Lydike 663721b306 pre-release 2.0.0a4 3 years ago
Anton Lydike 4004c5ee6d squashing bugs related to Int32 wrapping and sign extension 3 years ago
Anton Lydike c2b6385523 version 2.0.0a3 3 years ago
Anton Lydike b7f1365155 ported syscalls to Int32 usage and removed unecessary prints 3 years ago
Anton Lydike fa4a9b92f3 fixed imports in types/instruction_memory_section 3 years ago
Anton Lydike 57f827ba6a updated version to 2.0.0a2 3 years ago
Anton Lydike 61540dfcb7 [docs] improved documentation build 3 years ago
Anton Lydike b5e20ed39b added docstrings to Int32 and UInt32 classes 3 years ago
Anton Lydike 4ca475da69 improved the MMU.translate_address function 3 years ago
Anton Lydike cc3df91fd1 [restructured] moved more types and exceptions to riscemu.types 3 years ago
Anton Lydike bc26ed3a02 [restructured] moved all simple type definitions into riscemu.types 3 years ago
Anton Lydike 254410e9cc [priv] fixed error in halt csr 3 years ago
Anton Lydike 26d0a165f7 [priv] added --slowdown flag to slow down emulated clock speed 3 years ago
Anton Lydike cadccaef00 [priv] fixed printing for mret, sret and uret 3 years ago
Anton Lydike 71093fe72f Maor round of bugfixes and incremental improvements
- fixed errors in TextIO and IOModule
 - moved to Int32 and UInt32 based arithmetic
 - added a lot of end-to-end and other tests
3 years ago
Anton Lydike cd5795bb74 fixed priv start code, added tests 3 years ago
Anton Lydike 4f1c73df9e various small bugfixes 3 years ago
Anton Lydike 881f4004ed fixed removed argparse line in riscemu.__init__.py 3 years ago
Anton Lydike 6fa3558f6c added interactive mode, fixed some bugs 3 years ago
Anton Lydike 3d2619c258 created a better output for reads/writes outside of known regions 3 years ago
Anton Lydike 185ae8b94e added config and better loading code to CPU base 3 years ago
Anton Lydike 2880a59dbb fixed ascii escape sequences and section address calculation 3 years ago
Anton Lydike 7904a4dae8 added verbosity control to user mode emulator 3 years ago
Anton Lydike b396e0c5eb user mode emulator finally working again 3 years ago
Anton Lydike 5538034f8b started with base type overhaul 3 years ago
Anton Lydike 0488a9d6bc finished basic RISC-V parser 3 years ago
Anton Lydike dc4dca6fea [wip] almost done with the rework of the parser and internal data structure representation of programs 3 years ago
Anton Lydike 84562de98f added tests for tokenizer 3 years ago
Anton Lydike d5a4acef67 tokenizer reimplemented 3 years ago
Anton Lydike 52e189c226 fixed missing newline at the end of the file 3 years ago
Anton Lydike b317974dcc made sure register values adhere to correct 32bit two's complement standard - fixes #4 3 years ago
Anton Lydike a0259707b2 Released v1.0.0 to PyPi 3 years ago
Anton Lydike e65775774a extended userspace RV32I with li, la and mv instruction 3 years ago
Anton Lydike e9c11e9a41 added correct instruction printing 3 years ago
Anton Lydike 0b34aea520
Merge pull request #1 from AntonLydike/kernel-mode
Adding limited privileged emulation using the `riscemu.priv` module
3 years ago
Anton Lydike 7ab3f8361d code cleanup to increase visibility 3 years ago
Anton Lydike d09b7a5cb1 overhaul of debugging info printing 3 years ago
Anton Lydike d0c5abe845 added a whole lot of debugging info for privileged emulation 3 years ago
Anton Lydike 3d4d36bfe4 moved dependency on pyelftools into scoped function where it's used to reduce the number of dependencies required overall 3 years ago
Anton Lydike 0c96a87dcb added RV32A extension, only missing LR.W and SC.W 3 years ago
Anton Lydike 3033eb9985 tranlsation from absolute addressed to symbol-relative names for debugging 3 years ago
Anton Lydike ca71e196c2 added verbose flag and improved verbose output 3 years ago
Anton Lydike f2d07f90b5 priv: added __main__ script to module which correctly configures the cpu depending on --kernel or --image options 3 years ago
Anton Lydike 0651eabe18 fixed how ecalls are represented and handled 3 years ago
Anton Lydike 684c858300 added support for IO modules 3 years ago
Anton Lydike df9e610d14 forgot to commit image loader code 3 years ago
Anton Lydike 1f03449694 added memory image support to priv emulator 3 years ago
Anton Lydike 4c352d8567 [MMU] caching last used code section 3 years ago
Anton Lydike e8685af328 [PrivMMU] cleaned up file formatting 3 years ago
Anton Lydike 3d07c97a52 [PrivCPU] improved step function performance by checking time every tenth cycle 3 years ago
Anton Lydike 60a2a8d546 [CSR] adding cache to mstatus register 3 years ago
Anton Lydike 6b4f38d030 [ElfLoader] added cache for already decoded instructions 3 years ago
Anton Lydike 05c17bc029 [PrivCPU] fixed debugger skipping over ebreak instructions 3 years ago
Anton Lydike baa1f24eb7 [CpuTraps] fixed formatting for mcause registers 3 years ago
Anton Lydike 777717ed2e [PrivRV32I] fixed csrrw instruction to correctly switch register contents 3 years ago
Anton Lydike c7b3693740 [Regsietrs] ensuring register values are 32bit 3 years ago
Anton Lydike cc598c0910 [PrivCPU] changed timer compare to lower equals to trigger exactly on time 3 years ago
Anton Lydike affaa60d22 [PrivCPU] adding performance counter 3 years ago
Anton Lydike 48ce44993b [CSR] Adding dump_mstatus method to csr 3 years ago
Anton Lydike 639f91b192 [decoder] removed sign extension for CSR type instructions 3 years ago
Anton Lydike c25b9f2343 [PrivCPU] implemented CPU interrupt handling context switch 3 years ago
Anton Lydike 4c7f3ffe67 [PrivCPU] fixed perf-counter not comparing against shifted time 3 years ago
Anton Lydike c2002cd46d [PrivCPU] fixed naming for csr mtimecmp callback function 3 years ago
Anton Lydike 5b2b12507d [PrivRV32I] added half od csrrs instruction (reading only) 3 years ago
Anton Lydike 052ad56310 [CSR] fixed call to enum value member 3 years ago
Anton Lydike d9e5d78f87 [Registers] removed info when writing to zero register 3 years ago
Anton Lydike 79d913baaf [decoder] fixed formatting in print_ins function 3 years ago
Anton Lydike 9278235e44 [decoder] fixed botched j immediate decoding 3 years ago
Anton Lydike 6351f1e84d [PrivRV32I] fixed bug with blt backwards jumps missing by one 3 years ago
Anton Lydike f14bd2b983 [PrivCPU, PrivRV32I] fixed bug where ebreaks where missed during debugging 4 years ago
Anton Lydike c1110b9ce3 [ElfLoader] better formatting for jump and load/store instructions 4 years ago
Anton Lydike 37910018b9 [PrivRV32I] finally correct parsing of load/store instruction args 4 years ago
Anton Lydike e4537f86d9 [PrivRV32I] implemented csrrwi instruction 4 years ago
Anton Lydike c770cc05cf [Priv Exceptions] added __str__ as __repr__ alias to CpuTrap to correctly format exceptions when printed 4 years ago
Anton Lydike 3e4920f5d9 [decoder] fixed bug when decoding add/sub instruction 4 years ago
Anton Lydike 849d5f4fc3 [decoder, ElfLoader] decoing an instruction now returns all args as int 4 years ago
Anton Lydike f9b0bac245 [Priv Exceptions] fixed constructor typo in TimerInterrupt 4 years ago
Anton Lydike 9424390b65 [decoder] Added mret, sret, uret, wfi instruction decoding support 4 years ago
Anton Lydike 198d14d5fb [Priv Exceptions] added __repr__ to CpuTrap class 4 years ago
Anton Lydike ca3b4099d4 [Priv] moved CSR constants to a separate file 4 years ago
Anton Lydike 79369889f4 [CSR] fixed method naming for _addr_to_name (now _name_to_addr) 4 years ago
Anton Lydike de261c4c43 [Priv] overhauled instruction architecture 4 years ago
Anton Lydike c963fe3989 [Priv] small fixes for overlooked things 4 years ago
Anton Lydike 85af9b992f [PrivCPU] overhaul of instruction cycle, adding more CSR interaction 4 years ago
Anton Lydike 7239212729 [CSR] adding virtual csr registers 4 years ago
Anton Lydike 6653ef7e7c [CPU] set correct MISA 4 years ago
Anton Lydike a1f29b9d97 [CPU] cleaned up constructor 4 years ago
Anton Lydike 49b59cd46a [CSR] added read/write checks and unified name to addr resuloution 4 years ago
Anton Lydike 291f44a192 [CSR] unknown csr names now fail without exception 4 years ago
Anton Lydike c4cd83701f [CSR, PrivCPU] Added csr callback registration through decorator 4 years ago
Anton Lydike 504407c0d9 [CSR] adding callbacks to each csr block 4 years ago
Anton Lydike db2b0b314b [PrivCPU, PrivRV32I] fix for relative jumps and branches 4 years ago
Anton Lydike 6bd5cd1598 [ElfLoader] better formatting for load and save instructions 4 years ago
Anton Lydike ed6912a060 [ElfLoader] added bounds check to elf loader and casting binary data to bytearray 4 years ago
Anton Lydike 55be71dcc3 [CSR] added time and timeh csr codes 4 years ago
Anton Lydike 3a79bfdada [ElfLoader] also loading .sdata and .sbss sections now 4 years ago
Anton Lydike 3f11cd84ca [decoder] fixed error with decoding slli type instructions 4 years ago
Anton Lydike f3959be843 [decoder] now returning instruction number as third return value 4 years ago
Anton Lydike 0475d8d384 [CPU] added instruction XLEN attribute to CPU class to support multiple instruction lengths 4 years ago
Anton Lydike c9a136d595 [instructions] fixed error in auipc command 4 years ago
Anton Lydike ee0aac30c4 [instructions] moved regs and mmu to properties to work with janky PrivCPU 4 years ago
Anton Lydike 1bdf2e6efe [mmu] fixed typo in docstring 4 years ago
Anton Lydike c48a5efee3 [cpu] fixed formatting to include cpu class extensions 4 years ago
Anton Lydike 15da68995c [priv] module now able to load and execute elf binaries 4 years ago
Anton Lydike a4735db388 Added a decoder module which can deocde some RV32I/M instructions
Some of them even correctly O.o
4 years ago
Anton Lydike 483a3f2416 Priv: [wip] implementing privileged architecture 4 years ago
Anton Lydike a2e206eaee renamed CPU.__run -> CPU._run, it's now overwriteable by subclasses 4 years ago
Anton Lydike e45655e4c0 fixed some syscall docs 4 years ago
Anton Lydike 1abdc79e5e added additional logging to invalid mmu data access, needs a better exception 4 years ago
Anton Lydike fa22d76f13 Added libstring and documentation for the general library 4 years ago
Anton Lydike 6bb0ad3793 Added libstring and documentation for the general library 4 years ago
Anton Lydike a645e6259a Added more debugger documentation 4 years ago
Anton Lydike 462639ade7 Added run_ins method to debugger to run an instruction 4 years ago
Anton Lydike 5d484f08cf Minor fixes like imports and edge-case handling 4 years ago
Anton Lydike f45a37e705 Added MMU.get_bin_containing 4 years ago
Anton Lydike ff5ba9a7ef made CPU.run_instruction public 4 years ago
Anton Lydike da895f00cd added stack field to cpu 4 years ago
Anton Lydike f646bf1f1d added readthedocs badge 4 years ago
Anton Lydike 6436f8930a fixed stack docs in README 4 years ago
Anton Lydike d72f83d19c derp: wrong url to readthedocs in readme 4 years ago
Anton Lydike db0b9c26c3 updated documentation style in .idea config 4 years ago
Anton Lydike 905c2adcf5 added docs links to README 4 years ago
Anton Lydike f5f41f4e18 fixed errorneous license text in headers 4 years ago
Anton Lydike a276638f57 fixed docs folder not included in readthedocs build [second try] 4 years ago
Anton Lydike e8870420d3 fixed docs folder not included in readthedocs build 4 years ago
Anton Lydike 40559f00b4 Added instruction on how to build docs locally to README 4 years ago
Anton Lydike 0b6f8a05d8 Added markdown docs from docs folder to readthedocs output 4 years ago
Anton Lydike 242af5c7a3 implemented mmu.allocate_region and reworked how the stack works 4 years ago
Anton Lydike bc8c061c6d removed stack pref pseudo-op in preperation for real stack impl 4 years ago
Anton Lydike a52506a17f updated docs for new logging 4 years ago
Anton Lydike 2cd407aa79 added readthedocs link to readme 4 years ago
Anton Lydike 4ff9be4061 brainfart when configuring readthedocs theme 4 years ago
Anton Lydike 2810fb53a2 using correct readthedocs theme? 4 years ago
Anton Lydike f5afd0a8be more readthedocs fixes? 4 years ago
Anton Lydike 317d106b9c lots of readthedocs debugging 4 years ago
Anton Lydike 0574766a81 lots more documentation cleanup 4 years ago
Anton Lydike fdcb3a71be added readthedocs 4 years ago
Anton Lydike 819d57e3c8 preparations for sphinx documentation generation 4 years ago
Anton Lydike 6e6ce90e9a added lots more documentation and copyright notices 4 years ago
Anton Lydike 2a68f16e99 added lots of documentation in pydoc style 4 years ago
Anton Lydike 8c1714116e more info in readme to symbols and prgram startup 4 years ago
Anton Lydike a1fe631844 improved debugging output on early exit and reformatted cpu file 4 years ago
Anton Lydike 21b974cfbd prettier debug output (colorized) 4 years ago
Anton Lydike 41f5dd0730 better ascii dumps 4 years ago
Anton Lydike ebfb3a0112 better formatted print outputs 4 years ago
Anton Lydike 18b5ea0570 added launch message where cpu starts execution 4 years ago
Anton Lydike 44ae0bac77 read syscall now behaves like readline instead of read 4 years ago
Anton Lydike 0d9960c01a added add_accept_imm option flag to allow add rd, rs, imm instructions 4 years ago
Anton Lydike 5da0f8b0fa fixed failure to launch with missing --instruction-sets flag 4 years ago
Anton Lydike 9afbd03733 added .word pseudo op 4 years ago
Anton Lydike daed3a0205 added proper cpu exit handling 4 years ago
Anton Lydike 30d3e0ab59 updated readme formatting 4 years ago
Anton Lydike cd17c65ce7 Added instruction set selector to CLI 4 years ago
Anton Lydike 8b1cbd97ec added heap/stack support to todo section in readme 4 years ago
Anton Lydike b24a83d27c rewrote parts of instruction set docs 4 years ago
Anton Lydike 22a577da59 fixed formatting in verbose cpu output 4 years ago
Anton Lydike 785af6b747 added more documentation about debuggin tools 4 years ago
Anton Lydike 39d5212d3d added stepping functionality to the debugger 4 years ago
Anton Lydike 521cb73ad6 better dumping code in debugger 4 years ago
Anton Lydike 318b62431d catching invalid immediate value now 4 years ago
Anton Lydike 7d095991a2 added light documentation for internal structure 4 years ago
Anton Lydike 97525bf8ab fixed cpu __repr__ method 4 years ago
Anton Lydike 9cd0fcb7e8 added url to risc-v instruction definition to README 4 years ago
Anton Lydike b7c0f39aaa added documentation for RV32M 4 years ago
Anton Lydike 7aa67cd4e1 improved instruction parsing in RV32M 4 years ago
Anton Lydike d8f46c781c finished the RV32I instruction set 4 years ago
Anton Lydike 1abeab6f2d added parse_rs_rs_imm method to InstructionSet, used for branch instructions 4 years ago
Anton Lydike d40c80cb81 added unsigned options to instruction parsing 4 years ago
Anton Lydike 1957e11f62 added unified instruction parsing 4 years ago
Anton Lydike 2d378f2e0a implemented remu, rem, divu div and mul in RV32M 4 years ago
Anton Lydike 157589548d unified instruction parsing code 4 years ago
Anton Lydike d3fe6cb1a9 fixed read syscall shadowing of len 4 years ago
Anton Lydike 38cb3f7669 renamed RVM to RV32M 4 years ago
Anton Lydike 7f3fb9b141 fixed bgeu, bltu and bge in RV32I 4 years ago
Anton Lydike 07f097202d added lui instruction to RV32I 4 years ago
Anton Lydike 4c50b8bf06 added .space pseudo op, currently does nothing 4 years ago
Anton Lydike c6b18dd152 fixed hex check in parse number code 4 years ago
Anton Lydike d8d53da774 added scaffolding form RVM instruction set 4 years ago
Anton Lydike 7dcbd59d34 removed old unused run.py 4 years ago
Anton Lydike 18a9e5e223 updated todo in README 4 years ago
Anton Lydike 3ce42079d4 refactored instruction sets to be modular 4 years ago
Anton Lydike c20ab4cfb1 added more debugging info to README 4 years ago
Anton Lydike e42ec6a331 merged assembly and cpu docs 4 years ago
Anton Lydike a7cedc1cd2 fixed bad link in README 4 years ago
Anton Lydike 91a12fd2a8 fixed tokenizer hanging on unknown instruction 4 years ago
Anton Lydike a3ab418858 fixed formatting in Registers.dump() 4 years ago
Anton Lydike bf4bcfe388 added debugging info 4 years ago
Anton Lydike cd08cd4f70 added syscall interface to debug variables 4 years ago
Anton Lydike bf4c705297 added symbol lookup helper MMU.symbol(name) 4 years ago
Anton Lydike ce59d10a3b fixed range selection for LoadedMemorySection.dump 4 years ago
Anton Lydike c2cbb4653c updated readme and docs 4 years ago
Anton Lydike db6d21b734 now officially MIT licensed 4 years ago
Anton Lydike 4135ffdb32 syscall symbols now work as global symbols 4 years ago
Anton Lydike 8b4ba276a8 fixed saving reference to global_symbol_table in executable loader 4 years ago
Anton Lydike a1b9cf7f22 provide global syscall symbols if flag is set 4 years ago
Anton Lydike 11568ec9b0 improved arguments, dropped support for no-color output 4 years ago
Anton Lydike 2c5655d65a fixed registers keeping track of last accessed register 4 years ago
Anton Lydike 7cb29c5b89 added escape character deocoding for ascii and asciiz pseudo-ops 4 years ago
Anton Lydike 3bcabfbf78 fixed mmu bounds check for read 4 years ago
Anton Lydike fc22d4b6a7 added better colors to MMU output 4 years ago
Anton Lydike 09844c8d38 fixed isinstance check in write syscall 4 years ago
Anton Lydike d6e6856431 changed output color for syscall logs 4 years ago
Anton Lydike 45b82a3fa9 support for loading multiple executables 4 years ago
Anton Lydike cb48d66400 stack allocation now fixed 4 years ago
Anton Lydike 2b87bbe270 reformat Exceptions.py 4 years ago
Anton Lydike 8fc519ee86 added global symbol support! 4 years ago
Anton Lydike 9aaaf7313f added pseudo-op .set name val, and .global symb 4 years ago
Anton Lydike db8cc9b00c made instruction methods private to cpu class so not visible from debugger 4 years ago
Anton Lydike c3faaaed02 better formatting in memory dump 4 years ago
Anton Lydike f43c33b0d7 added __repr__ to MemoryFlags 4 years ago
Anton Lydike c4a28c9d1b added __repr__ to MMU and LoadedMemeorySection 4 years ago
Anton Lydike 4b26ab6774 added __repr__ to CPU class 4 years ago
Anton Lydike d56dca3ff4 added better debugging environment 4 years ago
Anton Lydike baaaa881bc colored exception messages 4 years ago
Anton Lydike 34a44860e0 added name field to executables for easy debugging 4 years ago
Anton Lydike 64507a4e7e added mmu dump to dump arbitrary section 4 years ago
Anton Lydike 30bcded998 beautiful section dumps 4 years ago
Anton Lydike 7f59ac9bca better interative shell 4 years ago
Anton Lydike 4272ae6d28 readme rework 4 years ago
Anton Lydike 8cf381ec61 added basic tests 4 years ago
Anton Lydike 8548891511 added assembly docs 4 years ago
Anton Lydike 9540a78e3e added CPU docs 4 years ago
Anton Lydike 8d39d79032 made module runnable 4 years ago
Anton Lydike 20db1e02ab implemented instructions sll, slli, srl, srli, sra, srai 4 years ago
Anton Lydike 7d09cb209f reworked memory instruction parsing 4 years ago
Anton Lydike 3c0e357ca0 minor bugfixes and missing members corrected 4 years ago
Anton Lydike dd79c11b3b implemented instructions and, or, slt, sltu 4 years ago
Anton Lydike f8e595b46e added instruction xor 4 years ago
Anton Lydike 99de083894 minor instruction fixes such as added assertions 4 years ago
Anton Lydike 5bdd866472 implemented instruction j, added cpu cycle counter 4 years ago
Anton Lydike 97d86108e8 added ebreak/scall aliases to sbreak/scall and replaced dbg with ebreak instruction 4 years ago
Anton Lydike 0aa42d0d1c implemented instructions beq, bne, blt, bge, bltu, bgeu, improved unsigned number handling 4 years ago
Anton Lydike 88c0b77a16 implemented instructions jal, jalr, j, ret, nop 4 years ago
Anton Lydike 5a722c8cf1 implemented syscalls open, read, write, close, exit 4 years ago
Anton Lydike a483db65c7 more work on syscalls 4 years ago
Anton Lydike a69cc7d346 moved registers out of CPU file 4 years ago
Anton Lydike feaf432645 fixed config dataclass 4 years ago
Anton Lydike 9710ed9b8b added Executable.__repr__ and added return self to ExecutableParser.parse 4 years ago
Anton Lydike 93ff8d7186 added unsigned option for int_{from,to}_bytes 4 years ago
Anton Lydike 4159d1609b added run config 4 years ago
Anton Lydike 6bc939572b parsing and simple running works somewhat 4 years ago
Anton Lydike da4ae7c4c1 parsing of tokenized asm into MemorySections works 4 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.8','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

@ -0,0 +1,98 @@
name: Deploy JupiterLite Page
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
steps:
- name: Checkout RiscEmu
uses: actions/checkout@v3
with:
path: riscemu
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install jupyterlite[all] libarchive-c build pyodide-build
- name: Build RiscEmu source distribution
run: |
cd riscemu
python setup.py sdist
# Pyodide is cached, so cloned only if not present in the cache, otherwise
# just checked out to whatever desired version and partially rebuilt.
- name: Restore cached Pyodide tree
id: cache-pyodide
uses: actions/cache@v3
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
# Clean the xDSL and FrozenList package folders, generate their skeletons
# 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
run: |
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
uses: actions/upload-pages-artifact@v1
with:
path: ./_output
deploy:
needs: build
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

@ -0,0 +1,22 @@
name: Upload to PyPI
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
environment: publishing
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
# install build package and build dist
- name: Build distribution
run: >-
python3 -m pip install build --user &&
python3 -m build --sdist --wheel --outdir dist/
# retrieve your distributions here
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

8
.gitignore vendored

@ -1,2 +1,8 @@
venv
__pycache__
.mypy_cache
.pytest_cache
/venv*
/dist
/riscemu.egg-info
/build

@ -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" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (riscemu)" project-jdk-type="Python SDK" />
</project>

@ -1,12 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/riscemu" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/.mypy_cache" />
<excludeFolder url="file://$MODULE_DIR$/riscemu.egg-info" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (riscemu)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

@ -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

@ -0,0 +1,18 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
builder: html
configuration: sphinx-docs/source/conf.py
# Optionally set the version of Python and requirements required to build your docs
python:
version: "3.7"
system_packages: true
install:
- requirements: sphinx-docs/requirements.txt

@ -0,0 +1,52 @@
# Changelog
## 2.1.1
- Bugfix: Fix some errors in the RV32F implementation (thanks @adutilleul)
- Bugfix: Fix how floats are printed in the register view (thanks @KGrykiel)
- Bugfix: Fix missing support for infinite registers in load/store ins (thanks @superlopuh)
## 2.1.0
- Added a very basic libc containing a `crt0.s`, and a few functions
such as `malloc`, `rand`, and `memcpy`.
- Added a subset of the `mmap2` syscall (code 192) to allocate new memory
- Refactored the launching code to improve using riscemu from code
- Added an option to start with the provided libc: `-o libc`
- Added floating point support (enabled by default). The RV32F extension is now available
## 2.0.5
- Added unlimited register mode with `-o unlimited_regs`
## 2.0.4
- Bugfix: fix a sign issue in instruction parsing for `rd, rs, rs` format
- Bugfix: respect `conf.debug_instruction` setting
## 2.0.3 - 2022-04-18
- Syscalls: cleaned up formatting and added instructions for extensions
- Parser: fixed error when labels where used outside of sections
- Cleaned up and improved memory dumping code
- Fixed a bug with hex literal recognition in instruction parse code
- 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
## 2.0.2
- Added implicit declaration of .text section when a file starts with assembly instructions without declaring a section first
- Fixed a regression where the cpu's exit code would no longer be the exit code of the emulator. Now the emulator exits with the cpu's exit code
- Added the changelog
## 2.0.1
- Fixed type annotations in parser code that prevented running unprivileged code
## 2.0.0
- Correct handling of 32 bit overflows and underflows
- Complete revamp of internal data structures
- Completely reworked how assembly is parsed

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2022 Anton Lydike
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
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.

@ -1,42 +1,106 @@
# RiscV (userspace) emulator in python
# RiscEmu - RISC-V (userspace) emulator in python
Implementing a basic RISC-V emulator, aimed at being easily extendable.
[![Documentation Status](https://readthedocs.org/projects/riscemu/badge/?version=latest)](https://riscemu.readthedocs.io/en/latest/?badge=latest)
Currently supported (but not implemented) instructions:
Implementing a basic RISC-V emulator, aimed at being easily extendable. Check out the docs at [readthedocs](https://riscemu.readthedocs.io/en/latest/index.html)
or [riscemu.datenvorr.at](https://riscemu.datenvorr.at/index.html).
````
lb, lh, lw, lbu, lhu, sb, sh, sw, sll, slli, srl, srli, sra,
srai, add, addi, sub, lui, auipc, xor, xori, or, ori, and,
andi, slt, slti, sltu, sltiu, beq, bne, blt, bge, bltu, bgeu,
j, jr, jal, jalr, ret, scall, break, nop
````
This emulator contains:
* RISC-V Assembly parser
* 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
* A debugging environment
Current register implementation (should be all standard userspace registers):
## Installation:
```bash
$ pip install riscemu
```
Registers[read,written](
zero=0x00000000 ra =0x00000000 sp =0x00000000 gp =0x00000000 tp =0x00000000 fp =0x00000000
--------------- --------------- --------------- --------------- --------------- ---------------
a0 =0x00000000 s0 =0x00000000 t0 =0x00000000 ft0 =0x00000000 fa0 =0x00000000 fs0 =0x00000000
a1 =0x00000000 s1 =0x00000000 t1 =0x00000000 ft1 =0x00000000 fa1 =0x00000000 fs1 =0x00000000
a2 =0x00000000 s2 =0x00000000 t2 =0x00000000 ft2 =0x00000000 fa2 =0x00000000 fs2 =0x00000000
a3 =0x00000000 s3 =0x00000000 t3 =0x00000000 ft3 =0x00000000 fa3 =0x00000000 fs3 =0x00000000
a4 =0x00000000 s4 =0x00000000 t4 =0x00000000 ft4 =0x00000000 fa4 =0x00000000 fs4 =0x00000000
a5 =0x00000000 s5 =0x00000000 t5 =0x00000000 ft5 =0x00000000 fa5 =0x00000000 fs5 =0x00000000
a6 =0x00000000 s6 =0x00000000 t6 =0x00000000 ft6 =0x00000000 fa6 =0x00000000 fs6 =0x00000000
a7 =0x00000000 s7 =0x00000000 ft7 =0x00000000 fa7 =0x00000000 fs7 =0x00000000
s8 =0x00000000 fs8 =0x00000000
s9 =0x00000000 fs9 =0x00000000
s10 =0x00000000 fs10=0x00000000
s11 =0x00000000 fs11=0x00000000
)
## Running simple Assembly:
A couple of basic assembly programs are provided inside `examples/`, such as [`hello-world.asm`](examples/hello-world.asm).
You can run it by typing `python -m riscemu examples/hello-world.asm`. It will produce output similar to:
```
[MMU] Successfully loaded: LoadedExecutable[examples/hello-world.asm](base=0x00000100, size=24bytes, sections=data text, run_ptr=0x00000110)
[CPU] Started running from 0x00000110 (examples/hello-world.asm)
Hello world
Current pseudo ops:
Program exited with code 0
```
.align, .ascii, .asciiz, .byte, .data, .double, .extern,
.float, .globl, .half, .kdata, .ktext, .set, .space, .text, .word
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.
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.
Currently, symbols (such as `main` or `loop`) are looked-up at runtime. This allows for better debugging, I believe.
Basic IO should work, as open, read, write and close are supported for stdin/stdout/stderr and even aribtrary file paths (if enabled)
When trying to run an assembly program, the emulator first tries to find a symbol named `_start`, then a symbol named `main`. if both
symbols were not found in the file, it simply starts at the beginning of the `.text` segment.
## Using the CLI:
*Current CLI is not final, options may change frequently until a stable version is reached*
This is how the interface is used:
```
usage: riscemu [-h] [--options OPTIONS] [--syscall-opts SYSCALL_OPTS] [--instruction-sets INSTRUCTION_SETS] [--stack_size stack-size] file.asm [file.asm ...]
OPTIONS and SYSCALL_OPTIONS is a list of comma-separated flags that will be enabled
--options OPTIONS: (-o)
disable_debug Disable the ebreak and sbreak instructions
no_syscall_symbols Don't make syscall symbols globally available
fail_on_ex Do not launch an interactive debugger when the CPU loop catches an exception
add_accept_imm accept "add rd, rs, imm" instructions, even though they are not standard
--syscall-opts SYSCALL_OPTS: (-so)
Options to control syscall behaviour
fs_access Allow access to the filesystem
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 `stack_size` is greater than zero, a stack is allocated and initialized, with the `sp` register pointing to the end of the stack.
## Debugging
Debugging is done using the `ebreak` (formerly `sbreak`) instruction, which will launch a debugging session if encountered.
See [docs/debugging.md](docs/debugging.md) for more info.
![debuggin the fibs program](docs/debug-session.png)
## The source code:
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
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

@ -0,0 +1,53 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Using RiscEmu from Python\n",
"\n",
"Here is how you can run some assembly through RiscEmu from Python.\n",
"\n",
"This example is using [this fibonacci assembly](examples/fibs.asm)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from riscemu import RunConfig, UserModeCPU, RV32I, RV32M, AssemblyFileLoader\n",
"\n",
"cfg = RunConfig(debug_instruction=False, verbosity=50)\n",
"cpu = UserModeCPU((RV32I, RV32M), cfg)\n",
"\n",
"loader = AssemblyFileLoader.instantiate('examples/fibs.asm', [])\n",
"cpu.load_program(loader.parse())\n",
"\n",
"cpu.launch(cpu.mmu.programs[-1], True)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.8"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

@ -0,0 +1,37 @@
# Assembly
Assembly tokenization should be working completely. It knows what instructions the CPU implementation supports and parses based on them.
## Instruction sets:
* RV32I
* Loads/Stores: `lb, lh, lw, lbu, lhu, sw, sh, sb` (supported arg format is either `rd, imm(reg)` or `rd, reg, imm`)
* Branch statements: `beq, bne, blt, bge, bltu, bgeu`
* Jumps `j, jal, jalr, ret`
* Basic arithmetic: `add, addi, sub, lui, auipc`
* Shifts: `sll, slli, srl, srli, sra, srai`
* Syscall/Debugging:`scall, ecall, sbreak, ebreak` (both `s` and `e` version are the same instruction)
* Compares: `slt, sltu, slti, sltiu`
* Logical: `and, or, xor, andi, ori, xori`
* Not implemented: `fence, fence.i, rdcycle, rdcycleh, rdtime, rdtimeh, rdinstret, rdinstreth`
* RV32M
* Multiplication: `mul, mulh`, not implemented yet are `mulhsu, mulhu`
* Division: `div, divu, rem, remu`
## Pseudo-ops
The following pseudo-ops are implemented as of yet:
* `.space <len>` reverse <len> bytes of zero
* `.ascii 'text'` put text into memory
* `.asciiz 'text'` put text into memory (null terminated)
* `.section .<name>` same as `.<name>`, see sections
* `.set <name>, <value>` to create a const symbol with a given value
* `.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:
* `data` read-write data (non-executable)
* `rodata` read-only data (non-executable)
* `text` executable data (read-only)

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

@ -0,0 +1,74 @@
# Using the debugger
You are launched into the debugger either by an `ebreak/sbreak` instruction, or when an exception occurs while running executing instructions.
Consider the example programm `examples/fibs.asm`:
```asm riscv-asm
.data
fibs: .space 56
.text
main:
addi s1, zero, 0 ; storage index
addi s2, zero, 56 ; last storage index
addi t0, zero, 1 ; t0 = F_{i}
addi t1, zero, 1 ; t1 = F_{i+1}
ebreak ; launch debugger
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
ebreak ; launch debugger
; exit gracefully
addi a0, zero, 0
addi a7, zero, 93
scall ; exit with code 0
```
This calculates the fibonacci sequence and stores it in memory at `fibs`. before and after it calculated all fibonacci numbers, it
uses the `ebreak` instruction to open the debugger. Let's run it and see what happens:
```
> python -m riscemu examples/fibs.asm
[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,
and most common tasks should have helper methods for them.
**Available objects are:**
* `mem`: (aka `mmu` or `memory`)
* `dump(address, fmt='hex', max_rows=10, group=4, bytes_per_row=16, all=False`:
Dumps the memory at `address`, in at most `max_rows` rows, each containing `bytes_per_row` bytes grouped
into groups of `group` bytes. They can be printed as:
* `hex`: hexadecimal, unsigned
* `int`: converted to integers
* `uint`: converted to unsigned integers
* `symbol(name)`: Lookup all symbols named `name`
* `reg`: (aka `regs` or `registers`)
* `dump(full=False)` dumps all integer registers (unless `all` is true, then all registers are printed)
* `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.
**Available helpers are:**
* `dump(regs | addr)` dumps either registers or memory address
* `cont(verbose=False)` continue execution (verbose prints each executed instruction)
* `step()` run the next instruction
* `ins()` get current instruction (this reference is mutable, if you want to edit your code on the fly)
* `run_ins(name, *args)` Run an instruction in the current context. Symbols, jumping, etc are supported!
Example:
![debuggin the fibs program](debug-session.png)

@ -0,0 +1,32 @@
# 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
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
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
construct a `LoadedExecutable`, which represents the actual memory
regions the executable contains (and some meta information such as
symbols).
* You can load as many executables as you want into memory. If you want
to run one, you pass it to `run_loaded(loaded_bin)` method of the cpu.
You shouldn't have to do this manually, as the `riscemu/__main__.py` has all the necessary code.
## Instruction sets
Each instruction set is in a separate file in `riscemu/instructions/`. All instruction sets have to inherit from the
`InstructionSet` class that sets up all the relevant helpers and loading code.
Creating a cpu with certain instruction sets is done by passing the CPU constructor a list of instruction set classes:
```
cpu = CPU(config, [RV32I, RV32M])
```

@ -0,0 +1,15 @@
# Included libraries
I've started to implement some sort of standard library, following closely to [GNU's glibc](https://www.gnu.org/software/libc/).
You can include the libraries by adding them as arguments (before your main assembly file):
```
> python3 -m riscemu examples/lib/libstring.asm example.asm
[MMU] Successfully loaded: LoadedExecutable[examples/lib/libstring.asm](base=0x00000100, size=64bytes, sections=text, run_ptr=0x00000100)
[MMU] Successfully loaded: LoadedExecutable[example.asm](base=0x00000140, size=168bytes, sections=data text, run_ptr=0x000001D0)
[CPU] Allocated 524288 bytes of stack
[CPU] Started running from 0x000001D0 (example.asm)
```
These libraries are no where near a stable state, so documentation will be scarce. Your best bet would be to `grep` for functionality. Sorry!

@ -0,0 +1,50 @@
# Syscalls
Performing a syscall is quite simple:
```risc-v asm
; set syscall code:
addi a7, zero, 93 ; or SCALL_EXIT if syscall symbols are mapped
; set syscall args:
addi a0, zero, 1 ; exit with code 1
; invode syscall handler
scall
```
The global symbols (e.g. `SCALL_READ`) are loaded by default. If you specify the option `no_syscall_symbols`, they will be omitted.
## Read (63) `SCALL_READ`
* `a0`: source file descriptor
* `a1`: addr at which to write the input
* `a2`: number of bytes to read (at most)
* `return in a0`: number of bytes read or -1
## Write (64) `SCALL_WRITE`
* `a0`: target file descriptor
* `a1`: addr at which the data to be written is located
* `a2`: number of bytes to write
* `return in a0`: number of bytes written or -1
## Exit (93) `SCALL_EXIT`
* `a0`: exit code
## Open (1024) `SCALL_OPEN`
* `a0`: open mode:
- `0`: read
- `1`: write (truncate)
- `2`: read/write (no truncate)
- `3`: only create
- `4`: append
* `a1`: addr where path is stored
* `a2`: length of path
* `return in a0`: file descriptor of opened file or -1
Requires flag `--scall-fs` to be set to True
## Close (1025) `SCALL_CLOSE`
* `a0`: file descriptor to close
* `return in a0`: 0 if closed correctly or -1
# 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.

@ -0,0 +1,23 @@
// Example program (c) by Anton Lydike
// this calculates the fibonacci sequence and stores it in ram
.data
fibs: .space 56
.text
main:
addi s1, zero, 0 // storage index
addi s2, zero, 56 // 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
addi a0, zero, 0
addi a7, zero, 93
scall // exit with code 0

@ -0,0 +1,13 @@
; hello-world.asm
; print "hello world" to stdout and exit
.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

@ -0,0 +1,44 @@
// example of a simple memory allocation
// we use the mmap2 syscall for this
.text
// call mmap2
li a0, 0 // addr = 0, let OS choose address
li a1, 4096 // size
li a2, 3 // PROT_READ | PROT_WRITE
li a3, 5 // MAP_PRIVATE | MAP_ANONYMOUS
li a7, SCALL_MMAP2
ecall // invoke syscall
li t0, -1 // exit if unsuccessful
beq a0, t0, _exit
// print address
print.uhex a0
# we can look at the state of the mmu here:
ebreak
# > mmu.sections
# InstructionMemorySection[.text] at 0x00000100
# BinaryDataMemorySection[.stack] at 0x00000170
# BinaryDataMemorySection[.data.runtime-allocated] at 0x00080170
sw t0, 144(a0)
sw t0, 0(a0)
sw t0, 8(a0)
sw t0, 16(a0)
sw t0, 32(a0)
sw t0, 64(a0)
sw t0, 128(a0)
sw t0, 256(a0)
sw t0, 512(a0)
sw t0, 1024(a0)
sw t0, 2048(a0)
sw t0, 4000(a0)
lw t1, 128(a0)
print.uhex t0
ebreak
_exit:
li a7, 93
ecall

@ -0,0 +1,11 @@
main:
li a1, 1084227584
li a2, 1082130432
fcvt.s.wu ft0, a1
fcvt.s.wu ft1, a2
fmul.s ft6, ft0, ft1
print.float ft6
// exit gracefully
addi a0, zero, 0
addi a7, zero, 93
scall // exit with code 0

@ -0,0 +1,20 @@
.data
my_data:
.word 0x11223344, 0x55667788, 0x9900aabb, 0xccddeeff
.text
main:
// load base address into t0
la t0, my_data
// begin loading words and printing them
lw a0, 0(t0)
print.uhex a0
lw a0, 4(t0)
print.uhex a0
lw a0, 8(t0)
print.uhex a0
lw a0, 12(t0)
print.uhex a0
// exit
li a7, 93
ecall

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# super hacky script to generate documentation when developing and for readthedocs
echo "Generating docs!"
if ! command -v 'sphinx-apidoc'; then
source venv/bin/activate
pip install -r sphinx-docs/requirements.txt
fi
if [[ $1 == 'generate' ]]; then
# delete old help folder
rm -rf help
cp -r ../../docs help
PYTHONPATH=../../ sphinx-apidoc -e -f -o . ../../riscemu ../../riscemu/colors.py ../../riscemu/__main__.py
echo "only generating, not building..."
rm ./modules.rst
exit 0
fi
# delete old help folder
rm -rf sphinx-docs/source/help
cp -r docs sphinx-docs/source/help
PYTHONPATH=. sphinx-apidoc -e -f -o sphinx-docs/source riscemu riscemu/colors.py riscemu/__main__.py
rm sphinx-docs/source/modules.rst
cd sphinx-docs
make html
# xdg-open build/html/index.html

@ -0,0 +1,29 @@
# RiscEmu LibC
This is a very basic implementation of libc in risc-v assembly, meant specifically for the riscemu emulator.
This is currently very incomplete, only a handful of methods are implemented, and most of them pretty basic.
## Contents:
### `stdlib.s`
Basic implementations of:
- `malloc`/`free` (that leaks memory)
- `rand`/`srand` (using xorshift)
- `exit`/`atexit` (supporting up to 8 exit handlers)
### `string.s`
Somewhat nice implementations of:
- `strlen`
- `strncpy`
- `strcpy`
- `memchr`
- `memset` (very basic byte-by-byte copy)
## Correctness:
This library is only lightly tested, so be careful and report bugs when you find them!

@ -0,0 +1,14 @@
// A minimal crt0.s that works along the stdlib.s file provided to give
// some resemblance of a functioning compilation target :)
//
// Copyright (c) 2023 Anton Lydike
// SPDX-License-Identifier: MIT
.text
.globl _start
_start:
// TODO: read argc, argv from a0, a1
// maybe even find envptr?
jal main
jal exit

@ -0,0 +1,178 @@
// A very basic implementation of a stdlib.h but in assembly.
// should(tm) work with riscemu.
//
// Copyright (c) 2023 Anton Lydike
// SPDX-License-Identifier: MIT
.data
_rand_seed:
.word 0x76767676
_atexit_calls:
// leave room for 8 atexit handlers here for now
.word 0x00, 0x00, 0x00, 0x00
.word 0x00, 0x00, 0x00, 0x00
_atexit_count:
.word 0x00
_malloc_base_ptr:
// first word is a pointer to some space
// second word is the offset inside that space
// space is always MALLOC_PAGE_SIZE bytes
.word 0x00, 0x00
.equ MALLOC_PAGE_SIZE 4069
.text
// malloc/free
.globl malloc
.globl free
// malloc(size_t size)
malloc:
// set aside size in s0
sw s0, -4(sp)
mv a0, s0
la t0, _malloc_base_ptr
lw t1, 0(t0)
beq t1, zero, _malloc_init
_malloc_post_init:
// if we are here, we always have
// t0 = (&_malloc_base_ptr)
// t1 = *(&_malloc_base_ptr)
// new we load
// t2 = base_ptr_offset
lw t2, 4(t0)
// add allocated size to offset
add t2, t2, s0
// check for overflow
li t4, MALLOC_PAGE_SIZE
bge t2, t4, _malloc_fail
// save the new offset
sw t2, 4(t0)
// calculate base_ptr + offset
add a0, t2, t1
// return that
lw s0, -4(sp)
ret
_malloc_init:
// call mmap2()
li a0, 0 // addr = 0, let OS choose address
li a1, 4096 // size
li a2, 3 // PROT_READ | PROT_WRITE
li a3, 5 // MAP_PRIVATE | MAP_ANONYMOUS
li a7, SCALL_MMAP2
ecall // invoke syscall
// check for error code
li t0, -1
beq a0, t0, _malloc_fail
// if succeeded, load &_malloc_base_ptr
la t0, _malloc_base_ptr
// put value of _malloc_base_ptr into t1
mv a0, t1
// save base ptr to _malloc_base_ptr
sw t1, 0(t0)
// jump to post_init
j _malloc_post_init
_malloc_fail:
li a0, 0
ret
// free is a nop, that's valid, but not very good^^
free:
ret
// exit, atexit
.globl exit
.globl atexit
// we can happily use saved registers here because we don't care at all if we
// destroy the calling registers. This is __noreturn__ anyways!
// register layout:
// s0 = &_atexit_count
// s2 = &_atexit_calls
// s1 = updated value of atexit
// s3 = exit code
exit:
// save exit code to s3
mv s3, a0
_exit_start:
la s0, _atexit_count // s0 = &_atexit_count
lw s1, 0(s0) // s1 = *(&_atexit_count)
// exit if no atexit() calls remain
beq s1, zero, _exit
// decrement
addi s1, s1, -4 // s1--
// save decremented value
sw s1, 0(s0) // _atexit_count = s1
li s2, _atexit_calls
add s1, s1, s2 // s1 = &_atexit_calls + (s1)
lw s1, 0(s1) // s1 = *s1
la ra, _exit_start // set ra up to point to exit
jalr zero, s1, 0 // jump to address in s1
// jalr will call the other function, which will then return back
// to the beginning of exit.
_exit:
mv a0, s3
li a7, 93
ecall
// atexit a0 = funcptr
atexit:
sw t0, -4(sp)
sw t2, -8(sp)
// load _atexit_count
la t0, _atexit_count
lw t2, 0(t0)
// if >= 8, fail
li t1, 8
bge t2, t1, _atexit_fail
// increment atexit_count by 4 (one word)
addi t2, t2, 4
sw t2, 0(t0)
// load address of _atexit_calls
la t0, _atexit_calls
// add new _atexit_count to _atexit_calls
add t0, t0, t2
sw a0, -4(t0)
li a0, 0
lw t0, -4(sp)
lw t2, -8(sp)
ret
_atexit_fail:
li a0, -1
lw s0, -4(sp)
lw s1, -8(sp)
ret
// rand, srand
.globl rand
.globl srand
// simple xorshift rand implementation
rand:
// load seed
la t1, _rand_seed
lw a0, 0(t1)
// three rounds of shifts:
sll a0, t0, 13 // x ^= x << 13;
srl a0, t0, 17 // x ^= x >> 17;
sll a0, t0, 5 // x ^= x << 5;
sw a0, 0(t1)
ret
srand:
la t1, _rand_seed
sw a0, 0(t1)
ret

@ -0,0 +1,154 @@
// string operations in RISC-V Assembly
//
// Copyright (c) 2023 Anton Lydike
// SPDX-License-Identifier: MIT
// Create NullPtr constant
.equ NULL, 0x00
.global NULL
.global strlen
// size_t libstr_strlen(char* str)
// return the length of str
.global strncpy
// char *strncpy(char *dest, const char *src, size_t n)
// copy n bytes from source into dest. If source ends before n bytes, the rest is filled with null-bytes
// returns pointer to dest
.global strcpy
// char *strncpy(char *dest, const char *src)
// copy string src into dest, including null terminator
// returns pointer to dest
.global memchr
// void *memchr(const void *str, char c, size_t n)
// search vor the first occurance of c in str
// returns a pointer to the first occurance, or NULL
.global memset
// void *memset(void *str, char c, size_t n)
// copies the character c to the first n characters of str.
// missing implementations
//.global memcmp
//.global memcpy
//.global strcat
.text
strlen:
// size_t strlen(char* str)
// push s1, s2 to the stack
sw s1, sp, -4
sw s2, sp, -8
// since no subroutines are called, we don't need to increment or decrement the sp
addi s2, zero, -1 // length (since null byte is counted by this method, we return len - 1)
__strlen_loop:
lb s1, a0, 0 // read character
addi s2, s2, 1 // increment number bytes read
addi a0, a0, 1
bne s1, zero, __strlen_loop
// we are done, set return value in a0
add a0, zero, s2
// pop s1, s2, from stack
lw s1, sp, -4
lw s2, sp, -8
ret
strncpy:
// char *strncpy(char *dest, const char *src, size_t n)
// copy size bytes from source to dest
sw s1, sp, -4 // push s1 to the stack
sw s2, sp, -8 // push s1 to the stack
add s1, a0, zero // save dest pointer for return
__strncpy_loop:
beq a2, zero, __strncpy_end
// copy byte
lb s2, a1, 0 // read first byte from src
sb s2, a0, 0 // write first byte to dest
// increment pointers
addi a0, a0, 1
addi a1, a1, 1
// one less byte to copy
addi a2, a2, -1
// if we read the terminating byte, jump to fill code
beq s2, zero, __strncpy_fill
// otherwise continue copying
j __strncpy_loop
__strncpy_fill:
// fill remaining space with 0 bytes
// if no bytes left, stop filling
beq a2, zero, __strncpy_end
sb zero, a0, 0
addi a0, a0, 1
addi a2, a2, -1
j __strncpy_fill
__strncpy_end:
// set return value
add a0, zero, s1
// pop s1, s2 from stack
lw s1, sp, -4
lw s2, sp, -8
ret
strcpy:
// char *strcpy(char *dest, const char *src)
sw s1, sp, -4 // push s1 to the stack
sw s2, sp, -8 // push s1 to the stack
add s1, a0, zero // save dest pointer for return
__strcpy_loop:
// copy byte
lb s2, a1, 0 // read first byte from src
sb s2, a0, 0 // write first byte to dest
// increment pointers
addi a0, a0, 1
addi a1, a1, 1
bne s2, zero, __strcpy_loop
// we are done copying, return
// set return value
add a0, zero, s1
// pop s1, s2 from stack
lw s1, sp, -4
lw s2, sp, -8
ret
memchr:
// void *memchr(const void *str, char c, size_t n)
sw s1, sp, -4 // push s1 to the stack
andi a1, a1, 0xff // trim a1 to be byte-sized
__memchr_loop:
beq a2, zero, __memchr_ret_null
lb s1, a0, 0
addi a0, a0, 1 // let a0 point to the next byte
addi a2, a2, -1 // decrement bytes to copy by 1
bne s1, a1, __memchr_loop
// return pointer to prev byte (as the prev byte actually matched a1)
addi a0, a0, -1
// pop s1, from stack
lw s1, sp, -4
ret
__memchr_ret_null:
// nothing found, return nullptr
addi a0, zero, NULL
lw s1, sp, -4
ret
memset:
// void *memset(void *str, char c, size_t n)
__memset_loop:
beq a2, zero, __memset_ret
sb a1, a0, 0
addi a0, a0, 1
addi a2, a2, -1
j __memset_loop
__memset_ret:
ret

@ -0,0 +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

@ -0,0 +1 @@
pyelftools~=0.27

@ -1,256 +1,138 @@
from dataclasses import dataclass
from collections import defaultdict
COLOR = True
FMT_ORANGE = '\033[33m'
FMT_GRAY = '\033[37m'
FMT_BOLD = '\033[1m'
FMT_NONE = '\033[0m'
FMT_UNDERLINE = '\033[4m'
class Registers:
def __init__(self, cpu: 'CPU'):
self.cpu = cpu
self.vals = defaultdict(lambda: 0)
self.last_mod = 'ft0'
self.last_access = 'a3'
def dump(self, small=False):
named_regs = [self.reg_repr(reg) for reg in Registers.named_registers()]
lines = [[] for i in range(12)]
if small:
regs = [('a', 8), ('s', 12), ('t', 7)]
else:
regs = [
('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)
lines[i].append(self.reg_repr(reg))
print("Registers[{},{}](".format(
FMT_ORANGE + FMT_UNDERLINE + 'read' + FMT_NONE,
FMT_ORANGE + FMT_BOLD + 'written' + FMT_NONE
))
if small:
print("\t" + " ".join(named_regs[0:3]))
print("\t" + " ".join(named_regs[3:]))
print("\t" + "--------------- " * 3)
else:
print("\t" + " ".join(named_regs))
print("\t" + "--------------- " * 6)
for line in lines:
print("\t" + " ".join(line))
print(")")
def reg_repr(self, reg):
txt = '{:4}=0x{:08X}'.format(reg, self.get(reg))
if reg == 'fp':
reg = 's0'
if reg == self.last_mod:
return FMT_ORANGE + FMT_BOLD + txt + FMT_NONE
if reg == self.last_access:
return FMT_ORANGE + FMT_UNDERLINE + txt + FMT_NONE
if reg == 'zero':
return txt
if self.get(reg) == 0 and reg not in Registers.named_registers():
return FMT_GRAY + txt + FMT_NONE
return txt
def set(self, reg, val):
if reg == 'zero':
print("[Registers.set] trying to set read-only register: {}".format(reg))
return False
if reg not in Registers.all_registers():
print("[Registers.set] invalid register name: {}".format(reg))
"""
RiscEmu (c) 2021-2022 Anton Lydike
SPDX-License-Identifier: MIT
This file contains the CPU logic (not the individual instruction sets). See instructions/instruction_set.py for more info
on them.
"""
import typing
from typing import List, Type
import riscemu
from .config import RunConfig
from .MMU import MMU
from .colors import FMT_CPU, FMT_NONE, FMT_ERROR
from .debug import launch_debug_session
from .types.exceptions import RiscemuBaseException, LaunchDebuggerException
from .syscall import SyscallInterface, get_syscall_symbols
from .types import CPU, ProgramLoader, Int32, BinaryDataMemorySection
from .parser import AssemblyFileLoader
if typing.TYPE_CHECKING:
from .instructions.instruction_set import InstructionSet
class UserModeCPU(CPU):
"""
This class represents a single CPU. It holds references to it's mmu, registers and syscall interrupt handler.
It is initialized with a configuration and a list of instruction sets.
"""
def __init__(
self, instruction_sets: List[Type["riscemu.InstructionSet"]], conf: RunConfig
):
"""
Creates a CPU instance.
:param instruction_sets: A list of instruction set classes. These must inherit from the InstructionSet class
"""
# setup CPU states
super().__init__(MMU(), instruction_sets, conf)
self.exit_code = 0
# setup syscall interface
self.syscall_int = SyscallInterface()
# add global syscall symbols, but don't overwrite any user-defined symbols
syscall_symbols = get_syscall_symbols()
syscall_symbols.update(self.mmu.global_symbols)
self.mmu.global_symbols.update(syscall_symbols)
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
)
return
launch_debugger = False
try:
self.cycle += 1
ins = self.mmu.read_ins(self.pc)
if verbose:
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:
if isinstance(ex, LaunchDebuggerException):
# if the debugger is active, raise the exception to
if self.debugger_active:
raise ex
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)
self.halted = True
if launch_debugger:
launch_debug_session(self)
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
)
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
:return:
"""
stack_sec = BinaryDataMemorySection(
bytearray(stack_size),
".stack",
None, # FIXME: why does a binary data memory section require a context?
"",
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
# replace fp register with s1, as these are the same register
if reg == 'fp':
reg = 's1'
self.last_mod = reg
setattr(self, reg, val)
def get(self, reg):
if not reg in Registers.all_registers():
print("[Registers.get] invalid register name: {}".format(reg))
return 0
if reg == 'fp':
reg = 's0'
return self.vals[reg]
@staticmethod
def all_registers():
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():
return ['zero', 'ra', 'sp', 'gp', 'tp', 'fp']
class CPU:
def instruction_lb(self, instruction):
pass
def instruction_lh(self, instruction):
pass
def instruction_lw(self, instruction):
pass
def instruction_lbu(self, instruction):
pass
def instruction_lhu(self, instruction):
pass
def instruction_sb(self, instruction):
pass
def instruction_sh(self, instruction):
pass
def instruction_sw(self, instruction):
pass
def instruction_sll(self, instruction):
pass
def instruction_slli(self, instruction):
pass
def instruction_srl(self, instruction):
pass
def instruction_srli(self, instruction):
pass
def instruction_sra(self, instruction):
pass
def instruction_srai(self, instruction):
pass
def instruction_add(self, instruction):
pass
def instruction_addi(self, instruction):
pass
def instruction_sub(self, instruction):
pass
def instruction_lui(self, instruction):
pass
def instruction_auipc(self, instruction):
pass
def instruction_xor(self, instruction):
pass
def instruction_xori(self, instruction):
pass
def instruction_or(self, instruction):
pass
def instruction_ori(self, instruction):
pass
def instruction_and(self, instruction):
pass
def instruction_andi(self, instruction):
pass
def instruction_slt(self, instruction):
pass
def instruction_slti(self, instruction):
pass
def instruction_sltu(self, instruction):
pass
def instruction_sltiu(self, instruction):
pass
def instruction_beq(self, instruction):
pass
def instruction_bne(self, instruction):
pass
def instruction_blt(self, instruction):
pass
def instruction_bge(self, instruction):
pass
def instruction_bltu(self, instruction):
pass
def instruction_bgeu(self, instruction):
pass
def instruction_j(self, instruction):
pass
def instruction_jr(self, instruction):
pass
def instruction_jal(self, instruction):
pass
def instruction_jalr(self, instruction):
pass
def instruction_ret(self, instruction):
pass
def instruction_scall(self, instruction):
pass
def instruction_break(self, instruction):
pass
def instruction_nop(self, instruction):
pass
@staticmethod
def all_instructions():
for method in vars(CPU):
if method.startswith('instruction_'):
yield method[12:]
@dataclass(frozen=True)
class Syscall:
id: int
registers: Registers
self.regs.set("sp", Int32(stack_sec.base + stack_sec.size))
class SyscallInterface:
def handle_syscall(self, scall: Syscall):
pass
if self.conf.verbosity > 1:
print(
FMT_CPU
+ "[CPU] Created stack of size {} at 0x{:x}".format(
stack_size, stack_sec.base
)
+ FMT_NONE
)
return True
a = Registers(None)
a.dump()
@classmethod
def get_loaders(cls) -> typing.Iterable[Type[ProgramLoader]]:
return [AssemblyFileLoader]

@ -0,0 +1,30 @@
from abc import ABC
from typing import Optional
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,
):
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
)
def dump(self, *args, **kwargs):
print(self)
def __repr__(self):
return "{}[{}] at 0x{:0X} (size={}bytes, flags={})".format(
self.__class__.__name__, self.name, self.base, self.size, self.flags
)

@ -0,0 +1,46 @@
from .IOModule import IOModule
from ..priv.Exceptions import InstructionAccessFault
from ..types import T_RelativeAddress, Instruction, MemoryFlags, Int32
class TextIO(IOModule):
def read_ins(self, offset: T_RelativeAddress) -> Instruction:
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
)
self.buff = bytearray(buflen)
self.current_line = ""
def read(self, addr: int, size: int) -> bytearray:
raise InstructionAccessFault(self.base + addr)
def write(self, addr: int, size: int, data: bytearray):
if addr == 0:
if size > 4:
raise InstructionAccessFault(addr)
if Int32(data) != 0:
self._print()
return
buff_start = addr - 4
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:
lines = text.split("\n")
lines[0] = self.current_line + lines[0]
for line in lines[:-1]:
self._present(line)
self.current_line = lines[-1]
else:
self.current_line += text
def _present(self, text: str):
print("[TextIO:{:x}] {}".format(self.base, text))

@ -0,0 +1,363 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
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,
Float32,
)
from .types.exceptions import InvalidAllocationException, MemoryAccessException
class MMU:
"""
The MemoryManagementUnit. This provides a unified interface for reading/writing data from/to memory.
It also provides various translations for addresses.
"""
max_size = 0xFFFFFFFF
"""
The maximum size of the memory in bytes
"""
max_alloc_size = 8 * 1024 * 1024 * 64
"""
No single allocation can be bigger than 64 MB
"""
sections: List[MemorySection]
"""
A list of all loaded memory sections
"""
programs: List[Program]
"""
A list of all loaded programs
"""
global_symbols: Dict[str, int]
"""
The global symbol table
"""
def __init__(self):
"""
Create a new MMU
"""
self.programs = list()
self.sections = list()
self.global_symbols = dict()
def get_sec_containing(self, addr: T_AbsoluteAddress) -> Optional[MemorySection]:
"""
Returns the section that contains the address addr
:param addr: the Address to look for
:return: The LoadedMemorySection or None
"""
for sec in self.sections:
if sec.base <= addr < sec.base + sec.size:
return sec
return None
def get_program_at_addr(self, addr: T_AbsoluteAddress) -> Optional[Program]:
for program in self.programs:
if program.base <= addr < program.base + program.size:
return program
return None
def read_ins(self, addr: T_AbsoluteAddress) -> Instruction:
"""
Read a single instruction located at addr
:param addr: The location
:return: The Instruction
"""
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
)
raise RuntimeError("No next instruction available!")
return sec.read_ins(addr - sec.base)
def read(self, addr: Union[int, Int32], size: int) -> bytearray:
"""
Read size bytes of memory at addr
:param addr: The addres at which to start reading
:param size: The number of bytes to read
:return: The bytearray at addr
"""
if isinstance(addr, Int32):
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"
)
return sec.read(addr - sec.base, size)
def write(self, addr: int, size: int, data: bytearray):
"""
Write bytes into memory
:param addr: The address at which to write
:param size: The number of bytes to write
:param data: The bytearray to write (only first size bytes are written)
"""
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"
)
return sec.write(addr - sec.base, size, data)
def dump(self, addr, *args, **kwargs):
"""
Dumpy the memory contents
:param addr: The address at which to dump
:param args: args for the dump function of the loaded memory section
:param kwargs: kwargs for the dump function of the loaded memory section
"""
sec = self.get_sec_containing(addr)
if sec is None:
print(
FMT_MEM
+ "[MMU] No section containing addr 0x{:08X}".format(addr)
+ FMT_NONE
)
return
sec.dump(addr - sec.base, *args, **kwargs)
def label(self, symb: str):
"""
Look up the symbol symb in all local symbol tables (and the global one)
:param symb: The symbol name to look up
"""
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]
)
)
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
)
)
def read_int(self, addr: int) -> Int32:
return Int32(self.read(addr, 4))
def read_float(self, addr: int) -> Float32:
return Float32(self.read(addr, 4))
def translate_address(self, address: T_AbsoluteAddress) -> str:
sec = self.get_sec_containing(address)
if not sec:
return "unknown at 0x{:0x}".format(address)
bin = self.get_program_at_addr(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",
}
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 = ("", float("inf"))
for name, val in best_fit:
if address - val < best[1]:
best = (name, val)
if address - val == best[1]:
if best[0] in elf_markers:
best = (name, val)
elif best[0] in secs and name not in elf_markers:
best = (name, val)
name, val = best
if not name:
return "{}:{} + 0x{:x} (0x{:x})".format(
sec.owner, sec.name, address - sec.base, address
)
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
if len(self.sections) == 0:
return True
# if the last section is located before the start we are also good
if start >= self.sections[-1].base + self.sections[-1].size:
return True
for sec in self.sections:
# skip all sections that end before the required start point
if sec.base + sec.size <= start:
continue
# we now have the first section that doesn't end **before** the start point
# if this section starts after the specified end, we are good
if sec.base >= end:
return True
# otherwise we can't continue
return False
# if all sections end before the requested start we are good
# technically we shouldn't ever reach this point, but better safe than sorry
return True
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),
)
at_addr = program.base
else:
at_addr = align_addr(self.get_guaranteed_free_address(), align_to)
# trigger the load event to set all addresses in the binary
program.loaded_trigger(at_addr)
# add program and sections to internal state
self.programs.append(program)
self.sections += program.sections
self._update_state()
# load all global symbols from program
self.global_symbols.update(
{key: program.context.labels[key] for key in program.global_labels}
)
# inject reference to global symbol table into program context
# FIXME: this is pretty unclean and should probably be solved in a better way in the future
program.context.global_symbol_dict = self.global_symbols
def load_section(self, sec: MemorySection, fixed_position: bool = False) -> bool:
if fixed_position:
if self.has_continous_free_region(sec.base, sec.base + sec.size):
self.sections.append(sec)
self._update_state()
else:
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)
sec.base = at_addr
self.sections.append(sec)
self._update_state()
return True
def _update_state(self):
"""
Called whenever a section or program is added to keep the list of programs and sections consistent
:return:
"""
self.programs.sort(key=lambda bin: bin.base)
self.sections.sort(key=lambda sec: sec.base)
def get_guaranteed_free_address(self) -> T_AbsoluteAddress:
if len(self.sections) == 0:
return 0x100
else:
return self.sections[-1].base + self.sections[-1].size
def __repr__(self):
return "{}(\n\t{}\n)".format(
self.__class__.__name__, "\n\t".join(repr(x) for x in self.programs)
)
def context_for(self, addr: T_AbsoluteAddress) -> InstructionContext:
sec = self.get_sec_containing(addr)
if sec is not None:
return sec.context
return InstructionContext()
def find_entrypoint(self) -> Optional[int]:
# try to find the global entrypoint
if "_start" in self.global_symbols:
return self.global_symbols["_start"]
# otherwise find a main (that's not necessarily global)
for p in self.programs:
if "main" in p.context.labels:
return p.context.resolve_label("main")
return None

@ -1,2 +1,37 @@
from .CPU import CPU, Registers, Syscall, SyscallInterface
from .tokenizer import RiscVToken, RiscVInput, RiscVTokenizer, RiscVInstructionToken, RiscVSymbolToken, RiscVPseudoOpToken
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
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 .instructions import *
from .MMU import MMU
from .registers import Registers
from .syscall import SyscallInterface, Syscall
from .CPU import CPU, UserModeCPU
from .debug import launch_debug_session
from .config import RunConfig
from .parser import tokenize, parse_tokens, AssemblyFileLoader
__author__ = "Anton Lydike <Anton@Lydike.com>"
__copyright__ = "Copyright 2023 Anton Lydike"
__version__ = "2.1.1"

@ -0,0 +1,22 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
This file holds the logic for starting the emulator from the CLI
"""
import sys
from riscemu import RiscemuBaseException
from riscemu.riscemu_main import RiscemuMain
try:
main = RiscemuMain()
main.run_from_cli(sys.argv[1:])
sys.exit(main.cpu.exit_code if not main.cfg.ignore_exit_code else 0)
except RiscemuBaseException as e:
print("Error: {}".format(e.message()))
e.print_stacktrace()
sys.exit(-1)

@ -0,0 +1,256 @@
from enum import Enum, auto
from typing import List
from typing import Optional, Tuple, Union
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")
"""
A tuple containing all section names which contain executable code (instead of data)
The first segment of each segment (first segment of ".text.main" is ".text") is checked
against this list to determine the type of it.
"""
class MemorySectionType(Enum):
Data = auto()
Instructions = auto()
class CurrentSection:
name: str
data: Union[List[Instruction], bytearray]
type: MemorySectionType
base: int
def __init__(self, name: str, type: MemorySectionType, base: int = 0):
self.name = name
self.type = type
self.base = base
if self.type == MemorySectionType.Data:
self.data = bytearray()
elif self.type == MemorySectionType.Instructions:
self.data = list()
else:
raise ParseException("Unknown section type: {}".format(type))
def current_address(self) -> T_RelativeAddress:
if self.type == MemorySectionType.Data:
return len(self.data) + self.base
return len(self.data) * 4 + self.base
def __repr__(self):
return "{}(name={},data={},type={})".format(
self.__class__.__name__, self.name, self.data, self.type.name
)
class ParseContext:
section: Optional[CurrentSection]
context: InstructionContext
program: Program
def __init__(self, name: str):
self.program = Program(name)
self.context = self.program.context
self.section = None
def finalize(self) -> Program:
self._finalize_section()
return self.program
def _finalize_section(self):
if self.section is None:
return
if self.section.type == MemorySectionType.Data:
section = BinaryDataMemorySection(
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.program.add_section(section)
self.section = None
def new_section(self, name: str, type: MemorySectionType, alignment: int = 4):
base = align_addr(self.current_address(), alignment)
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
):
self.context.labels[name] = value
if is_global:
self.program.global_labels.add(name)
if is_relative:
self.program.relative_labels.add(name)
def current_address(self):
if self.section:
return self.section.current_address()
return self.program.base if self.program.base is not None else 0
def __repr__(self):
return "{}(\n\tsetion={},\n\tprogram={}\n)".format(
self.__class__.__name__, self.section, self.program
)
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
)
)
if context.section.type != type:
raise ParseException(
"Error, expected to be in {} section, but currently in {}...".format(
type.name, context.section
)
)
class AssemblerDirectives:
"""
This class represents a collection of all assembler directives as documented by
https://github.com/riscv-non-isa/riscv-asm-manual/blob/master/riscv-asm.md#pseudo-ops
All class methods prefixed with op_ are directly used as assembler directives.
"""
@classmethod
def op_align(cls, token: Token, args: Tuple[str], context: ParseContext):
ASSERT_LEN(args, 1)
ASSERT_IN_SECTION_TYPE(context, MemorySectionType.Data)
align_to = parse_numeric_argument(args[0])
current_mod = context.current_address() % align_to
if current_mod == 0:
return
context.section.data += bytearray(align_to - current_mod)
@classmethod
def op_section(cls, token: Token, args: Tuple[str], context: ParseContext):
ASSERT_LEN(args, 1)
if get_section_base_name(args[0]) in INSTRUCTION_SECTION_NAMES:
context.new_section(args[0], MemorySectionType.Instructions)
else:
context.new_section(args[0], MemorySectionType.Data)
@classmethod
def op_globl(cls, token: Token, args: Tuple[str], context: ParseContext):
ASSERT_LEN(args, 1)
context.program.global_labels.add(args[0])
@classmethod
def op_global(cls, token: Token, args: Tuple[str], context: ParseContext):
cls.op_globl(token, args, context)
@classmethod
def op_equ(cls, token: Token, args: Tuple[str], context: ParseContext):
ASSERT_LEN(args, 2)
name = args[0]
value = parse_numeric_argument(args[1])
context.context.labels[name] = value
@classmethod
def op_space(cls, token: Token, args: Tuple[str], context: ParseContext):
ASSERT_LEN(args, 1)
ASSERT_IN_SECTION_TYPE(context, MemorySectionType.Data)
size = parse_numeric_argument(args[0])
cls.add_bytes(size, None, context)
@classmethod
def op_zero(cls, token: Token, args: Tuple[str], context: ParseContext):
ASSERT_LEN(args, 1)
ASSERT_IN_SECTION_TYPE(context, MemorySectionType.Data)
size = parse_numeric_argument(args[0])
cls.add_bytes(size, bytearray(size), context)
@classmethod
def add_bytes(
cls, size: int, content: Union[None, int, bytearray], context: ParseContext
):
ASSERT_IN_SECTION_TYPE(context, MemorySectionType.Data)
if content is None:
content = bytearray(size)
if isinstance(content, int):
content = Int32(content).to_bytes(size)
context.section.data += content
@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")
encoded_bytes = bytearray(text.encode("ascii"))
if zero_terminate:
encoded_bytes += bytearray(1)
cls.add_bytes(len(encoded_bytes), encoded_bytes, context)
@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"):
cls.op_section(token, (token.value,), context)
elif op in ("string", "asciiz", "asciz", "ascii"):
ASSERT_LEN(args, 1)
cls.add_text(args[0], context, zero_terminate=(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
)
DATA_OP_SIZES = {
"byte": 1,
"2byte": 2,
"half": 2,
"short": 2,
"4byte": 4,
"word": 4,
"long": 4,
"8byte": 8,
"dword": 8,
"quad": 8,
}

@ -0,0 +1,29 @@
"""
RiscEmu (c) 2021 Anton Lydike
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_BOLD = "\033[1m"
FMT_NONE = "\033[0m"
FMT_UNDERLINE = "\033[4m"
FMT_ERROR = FMT_RED + FMT_BOLD
FMT_MEM = FMT_CYAN + FMT_BOLD
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

@ -0,0 +1,26 @@
"""
RiscEmu (c) 2021-2022 Anton Lydike
SPDX-License-Identifier: MIT
"""
from dataclasses import dataclass
@dataclass(frozen=True, init=True)
class RunConfig:
stack_size: int = 8 * 1024 * 64 # for 8KB stack
include_scall_symbols: bool = True
add_accept_imm: bool = False
# debugging
debug_instruction: bool = True
debug_on_exception: bool = True
# allowed syscalls
scall_input: bool = True
scall_fs: bool = False
verbosity: int = 0
slowdown: float = 1
unlimited_registers: bool = False
# runtime config
use_libc: bool = False
ignore_exit_code: bool = False

@ -0,0 +1,90 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
import os.path
from .types import SimpleInstruction
from .helpers import *
if typing.TYPE_CHECKING:
from riscemu import CPU, Registers
HIST_FILE = os.path.join(os.path.expanduser("~"), ".riscemu_history")
def launch_debug_session(cpu: "CPU", prompt=""):
if cpu.debugger_active:
return
import code
import readline
import rlcompleter
# set the active debug flag
cpu.debugger_active = True
# setup some aliases:
registers = cpu.regs
regs = cpu.regs
memory = cpu.mmu
mem = cpu.mmu
mmu = cpu.mmu
# setup helper functions:
def dump(what, *args, **kwargs):
if what == regs:
regs.dump(*args, **kwargs)
else:
mmu.dump(what, *args, **kwargs)
def dump_stack(*args, **kwargs):
mmu.dump(regs.get("sp"), *args, **kwargs)
def ins():
print("Current instruction at 0x{:08X}:".format(cpu.pc))
return mmu.read_ins(cpu.pc)
def run_ins(name, *args: str):
if len(args) > 3:
print("Invalid arg count!")
return
context = mmu.context_for(cpu.pc)
ins = SimpleInstruction(name, tuple(args), context, cpu.pc)
print(FMT_DEBUG + "Running instruction {}".format(ins) + FMT_NONE)
cpu.run_instruction(ins)
def cont(verbose=False):
try:
cpu.run(verbose)
except LaunchDebuggerException:
print(FMT_DEBUG + "Returning to debugger...")
return
def step():
try:
cpu.step()
except LaunchDebuggerException:
return
# collect all variables
sess_vars = globals()
sess_vars.update(locals())
# add tab completion
readline.set_completer(rlcompleter.Completer(sess_vars).complete)
readline.parse_and_bind("tab: complete")
if os.path.exists(HIST_FILE):
readline.read_history_file(HIST_FILE)
relaunch_debugger = False
try:
code.InteractiveConsole(sess_vars).interact(
banner=FMT_DEBUG + prompt + FMT_NONE,
exitmsg="Exiting debugger",
)
finally:
cpu.debugger_active = False
readline.write_history_file(HIST_FILE)

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

@ -0,0 +1,19 @@
if __name__ == "__main__":
import code
import readline
import rlcompleter
from .decoder import *
from .formats import *
from .instruction_table import *
from .regs import RISCV_REGS
sess_vars = globals()
sess_vars.update(locals())
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..."
)

@ -0,0 +1,89 @@
from .instruction_table import *
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}"
)
STATIC_INSN: Dict[int, Tuple[str, List[int], int]] = {
0x00000013: ("nop", [], 0x00000013),
0x00008067: ("ret", [], 0x00008067),
0xFE010113: ("addi", [2, 2, -32], 0xFE010113),
0x02010113: ("addi", [2, 2, 32], 0x02010113),
0x00100073: ("ebreak", [], 0x00100073),
0x00000073: ("ecall", [], 0x00000073),
0x30200073: ("mret", [], 0x30200073),
0x00200073: ("uret", [], 0x00200073),
0x10200073: ("sret", [], 0x10200073),
0x10500073: ("wfi", [], 0x10500073),
}
def int_from_ins(insn: bytearray):
return int.from_bytes(insn, "little")
def name_from_insn(ins: int):
opcode = op(ins)
if opcode not in RV32:
print_ins(ins)
raise RuntimeError(f"Invalid opcode: {opcode:0x} in insn {ins:x}")
dec = RV32[opcode]
if isinstance(dec, str):
return dec
fun3 = funct3(ins)
if fun3 not in dec:
print_ins(ins)
raise RuntimeError(f"Invalid funct3: {fun3:0x} in insn {ins:x}")
dec = dec[fun3]
if isinstance(dec, str):
return dec
if opcode == 0x1C and fun3 == 0:
# we have ecall/ebreak
token = imm110(ins)
if token in dec:
return dec[token]
print_ins(ins)
raise RuntimeError(f"Invalid instruction in ebreak/ecall region: {ins:x}")
fun7 = funct7(ins)
if opcode == 0b1011 and fun3 == 0b10:
# ignore the two aq/lr bits located in the fun7 block
# riscemu has no memory reordering, therefore we don't need to look at these bits ever
fun7 = fun7 >> 2
if fun7 in dec:
if opcode == 0x0C or (opcode == 0x04 and fun3 == 5):
dec = dec[fun7]
return dec
print("unknown instruction?!")
return dec[fun7]
print_ins(ins)
raise RuntimeError(f"Invalid instruction: {ins:x}")
def decode(ins: Union[bytearray, bytes]) -> Tuple[str, List[int], int]:
insn = int_from_ins(ins)
if insn & 3 != 3:
print_ins(insn)
raise RuntimeError("Not a RV32 instruction!")
if insn in STATIC_INSN:
return STATIC_INSN[insn]
opcode = op(insn)
if opcode not in INSTRUCTION_ARGS_DECODER:
print_ins(insn)
raise RuntimeError("No instruction decoder found for instruction")
return name_from_insn(insn), INSTRUCTION_ARGS_DECODER[opcode](insn), insn

@ -0,0 +1,124 @@
from typing import Dict, Callable, List, Union
from .regs import RISCV_REGS
def op(ins: int):
return (ins >> 2) & 31
def rd(ins: int):
return (ins >> 7) & 31
def funct3(ins: int):
return (ins >> 12) & 7
def rs1(ins: int):
return (ins >> 15) & 31
def rs2(ins: int):
return (ins >> 20) & 31
def funct7(ins: int):
return ins >> 25
def imm110(ins: int):
return ins >> 20
def imm3112(ins: int):
return ins >> 12
def imm_i(ins: int):
return sign_extend(imm110(ins), 12)
def imm_s(ins: int):
num = (funct7(ins) << 5) + rd(ins)
return sign_extend(num, 12)
def imm_b(ins: int):
lower = rd(ins)
higher = funct7(ins)
num = (
(lower & 0b11110)
+ ((higher & 0b0111111) << 5)
+ ((lower & 1) << 11)
+ ((higher >> 6) << 12)
)
return sign_extend(num, 13)
def imm_u(ins: int):
return sign_extend(imm3112(ins), 20)
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,
)
def sign_extend(num, bits):
sign_mask = 1 << (bits - 1)
return (num & (sign_mask - 1)) - (num & sign_mask)
def decode_i(ins: int) -> List[int]:
return [rd(ins), rs1(ins), imm_i(ins)]
def decode_b(ins: int) -> List[int]:
return [rs1(ins), rs2(ins), imm_b(ins)]
def decode_u(ins: int) -> List[int]:
return [rd(ins), imm_u(ins)]
def decode_r(ins: int) -> List[int]:
return [rd(ins), rs1(ins), rs2(ins)]
def decode_s(ins: int) -> List[int]:
return [rs2(ins), rs1(ins), imm_s(ins)]
def decode_j(ins: int) -> List[int]:
return [rd(ins), imm_j(ins)]
def decode_i_shamt(ins: int) -> List[int]:
if funct3(ins) in (1, 5):
return [rd(ins), rs1(ins), rs2(ins)]
return decode_i(ins)
def decode_i_unsigned(ins: int) -> List[int]:
return [rd(ins), rs1(ins), imm110(ins)]
INSTRUCTION_ARGS_DECODER: Dict[int, Callable[[int], List[int]]] = {
0x00: decode_i,
0x04: decode_i_shamt,
0x05: decode_u,
0x08: decode_s,
0x0C: decode_r,
0x0D: decode_u,
0x18: decode_b,
0x19: decode_i,
0x1B: decode_j,
0x1C: decode_i_unsigned,
0b1011: decode_r,
}

@ -0,0 +1,49 @@
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
def int_to_hex(num: int):
if num < 0:
return f"-0x{-num:x}"
return f"0x{num:x}"
def format_ins(ins: int, name: str, fmt: str = "int"):
opcode = op(ins)
if fmt == "hex":
fmt = int_to_hex
else:
fmt = str
if opcode not in INSTRUCTION_ARGS_DECODER:
return f"{name} <unknown op>"
decoder = INSTRUCTION_ARGS_DECODER[opcode]
if name in ("ecall", "ebreak", "mret", "sret", "uret"):
return name
if opcode in (0x8, 0x0):
r1, r2, imm = decoder(ins)
return f"{name:<7} {RISCV_REGS[r1]}, {imm}({RISCV_REGS[r2]})"
elif decoder in (decode_i, decode_i_unsigned, decode_b, decode_i_shamt, decode_s):
r1, r2, imm = decoder(ins)
r1, r2 = RISCV_REGS[r1], RISCV_REGS[r2]
return f"{name:<7} {r1}, {r2}, {fmt(imm)}"
elif decoder in (decode_r,):
rd, rs1, rs2 = [RISCV_REGS[x] for x in decoder(ins)]
return f"{name:<7} {rd}, {rs1}, {rs2}"
elif decoder in (decode_j, decode_u):
r1, imm = decoder(ins)
return f"{name:<7} {RISCV_REGS[r1]}, {fmt(imm)}"
return f"{name} <unknown decoder>"

@ -0,0 +1,81 @@
from collections import defaultdict
from .formats import *
tbl = lambda: defaultdict(tbl)
RV32 = tbl()
RV32[0x1B] = "jal"
RV32[0x0D] = "lui"
RV32[0x05] = "auipc"
RV32[0x19][0] = "jalr"
RV32[0x04][0] = "addi"
RV32[0x04][1] = "slli"
RV32[0x04][2] = "slti"
RV32[0x04][3] = "sltiu"
RV32[0x04][4] = "xori"
RV32[0x04][5][0x00] = "srli"
RV32[0x04][5][0x20] = "srai"
RV32[0x04][6] = "ori"
RV32[0x04][7] = "andi"
RV32[0x18][0] = "beq"
RV32[0x18][1] = "bne"
RV32[0x18][4] = "blt"
RV32[0x18][5] = "bge"
RV32[0x18][6] = "bltu"
RV32[0x18][7] = "bgeu"
RV32[0x00][0] = "lb"
RV32[0x00][1] = "lh"
RV32[0x00][2] = "lw"
RV32[0x00][4] = "lbu"
RV32[0x00][5] = "lhu"
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][0][0] = "ecall"
RV32[0x1C][0][1] = "ebreak"
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][5][32] = "sra"
RV32[0x0C][6][0] = "or"
RV32[0x0C][7][0] = "and"
# rv32m
RV32[0x0C][0][1] = "mul"
RV32[0x0C][1][1] = "mulh"
RV32[0x0C][2][1] = "mulhsu"
RV32[0x0C][3][1] = "mulhu"
RV32[0x0C][4][1] = "div"
RV32[0x0C][5][1] = "divu"
RV32[0x0C][6][1] = "rem"
RV32[0x0C][7][1] = "remu"
# rv32a
RV32[0b1011][0b10][0b00010] = "lr.w"
RV32[0b1011][0b10][0b00011] = "sc.w"
RV32[0b1011][0b10][0b00001] = "amoswap.w"
RV32[0b1011][0b10][0b00000] = "amoadd.w"
RV32[0b1011][0b10][0b00100] = "amoxor.w"
RV32[0b1011][0b10][0b01100] = "amoand.w"
RV32[0b1011][0b10][0b01000] = "amoor.w"
RV32[0b1011][0b10][0b10000] = "amomin.w"
RV32[0b1011][0b10][0b10100] = "amomax.w"
RV32[0b1011][0b10][0b11000] = "amominu.w"
RV32[0b1011][0b10][0b11100] = "amomaxu.w"

@ -0,0 +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",
]

@ -0,0 +1,117 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
from math import log10, ceil
from typing import Iterable, Iterator, TypeVar, Generic, List, Optional
from .types import Int32, UInt32
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)
def parse_numeric_argument(arg: str) -> int:
"""
parse hex or int strings
"""
try:
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),
)
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)]
def apply_highlight(item, ind, hi_ind):
"""
applies some hightlight such as underline to item if ind == hi_ind
"""
if ind == hi_ind:
return FMT_UNDERLINE + FMT_ORANGE + item + FMT_NONE
return item
def highlight_in_list(items, hi_ind, joiner=" "):
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":
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":
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((repr(chr(b))[1:-1] for b in byte_arr), highlight, "")
T = TypeVar("T")
class Peekable(Generic[T], Iterator[T]):
def __init__(self, iterable: Iterable[T]):
self.iterable = iter(iterable)
self.cache: List[T] = list()
def __iter__(self) -> Iterator[T]:
return self
def __next__(self) -> T:
if self.cache:
return self.cache.pop()
return next(self.iterable)
def peek(self) -> Optional[T]:
try:
if self.cache:
return self.cache[0]
pop = next(self.iterable)
self.cache.append(pop)
return pop
except StopIteration:
return None
def push_back(self, item: T):
self.cache = [item] + self.cache
def is_empty(self) -> bool:
return self.peek() is None
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]

@ -0,0 +1,78 @@
from .instruction_set import InstructionSet, Instruction
from riscemu.types.exceptions import INS_NOT_IMPLEMENTED
from ..types import Int32, UInt32
class RV32A(InstructionSet):
"""
The RV32A instruction set. Currently, load-reserved and store conditionally are not supported
due to limitations in the way the MMU is implemented. Maybe a later implementation will add support
for this?
"""
def instruction_lr_w(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_sc_w(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_amoswap_w(self, ins: "Instruction"):
dest, addr, val = self.parse_rd_rs_rs(ins)
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"):
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"):
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"):
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"):
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"):
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"):
val: UInt32
dest, addr, val = self.parse_rd_rs_rs(ins, signed=False)
old = UInt32(self.mmu.read(addr, 4))
self.mmu.write(addr, max(old, val).to_bytes())
self.regs.set(dest, old)
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"):
val: UInt32
dest, addr, val = self.parse_rd_rs_rs(ins, signed=False)
old = UInt32(self.mmu.read(addr, 4))
self.mmu.write(addr, min(old, val).to_bytes(4))
self.regs.set(dest, old)

@ -0,0 +1,609 @@
"""
RiscEmu (c) 2023 Anton Lydike
SPDX-License-Identifier: MIT
This file contains copious amounts of docstrings that were all taken
from https://msyksphinz-self.github.io/riscv-isadoc/html/rvfd.html
(all the docstrings on the instruction methods documenting the opcodes
and their function)
"""
from typing import Tuple
from .instruction_set import InstructionSet, Instruction
from riscemu.types import INS_NOT_IMPLEMENTED, Float32, Int32, UInt32
class RV32F(InstructionSet):
def instruction_fmadd_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|rs3 |00 |rs2 |rs1 |rm |rd |10000|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fmadd.s rd,rs1,rs2,rs3
:Description:
| Perform single-precision fused multiply addition.
:Implementation:
| f[rd] = f[rs1]×f[rs2]+f[rs3]
"""
rd, rs1, rs2, rs3 = self.parse_rd_rs_rs_rs(ins)
self.regs.set_f(rd, rs1 * rs2 + rs3)
def instruction_fmsub_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|rs3 |00 |rs2 |rs1 |rm |rd |10001|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fmsub.s rd,rs1,rs2,rs3
:Description:
| Perform single-precision fused multiply addition.
:Implementation:
| f[rd] = f[rs1]×f[rs2]-f[rs3]
"""
rd, rs1, rs2, rs3 = self.parse_rd_rs_rs_rs(ins)
self.regs.set_f(rd, rs1 * rs2 - rs3)
def instruction_fnmsub_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|rs3 |00 |rs2 |rs1 |rm |rd |10010|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fnmsub.s rd,rs1,rs2,rs3
:Description:
| Perform single-precision fused multiply addition.
:Implementation:
| f[rd] = -f[rs1]×f[rs2]+f[rs3]
"""
rd, rs1, rs2, rs3 = self.parse_rd_rs_rs_rs(ins)
self.regs.set_f(rd, -rs1 * rs2 + rs3)
def instruction_fnmadd_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|rs3 |00 |rs2 |rs1 |rm |rd |10011|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fnmadd.s rd,rs1,rs2,rs3
:Description:
| Perform single-precision fused multiply addition.
:Implementation:
| f[rd] = -f[rs1]×f[rs2]-f[rs3]
"""
rd, rs1, rs2, rs3 = self.parse_rd_rs_rs_rs(ins)
self.regs.set_f(rd, -rs1 * rs2 - rs3)
def instruction_fadd_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00000|00 |rs2 |rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fadd.s rd,rs1,rs2
:Description:
| Perform single-precision floating-point addition.
:Implementation:
| f[rd] = f[rs1] + f[rs2]
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set_f(
rd,
rs1 + rs2,
)
def instruction_fsub_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00001|00 |rs2 |rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fsub.s rd,rs1,rs2
:Description:
| Perform single-precision floating-point substraction.
:Implementation:
| f[rd] = f[rs1] - f[rs2]
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set_f(
rd,
rs1 - rs2,
)
def instruction_fmul_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00010|00 |rs2 |rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fmul.s rd,rs1,rs2
:Description:
| Perform single-precision floating-point multiplication.
:Implementation:
| f[rd] = f[rs1] × f[rs2]
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set_f(
rd,
rs1 * rs2,
)
def instruction_fdiv_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00011|00 |rs2 |rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fdiv.s rd,rs1,rs2
:Description:
| Perform single-precision floating-point division.
:Implementation:
| f[rd] = f[rs1] / f[rs2]
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set_f(
rd,
rs1 / rs2,
)
def instruction_fsqrt_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|01011|00 |00000|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fsqrt.s rd,rs1
:Description:
| Perform single-precision square root.
:Implementation:
| f[rd] = sqrt(f[rs1])
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set_f(rd, self.regs.get_f(rs) ** 0.5)
def instruction_fsgnj_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00100|00 |rs2 |rs1 |000 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fsgnj.s rd,rs1,rs2
:Description:
| Produce a result that takes all bits except the sign bit from rs1.
| The results sign bit is rs2s sign bit.
:Implementation:
| f[rd] = {f[rs2][31], f[rs1][30:0]}
"""
INS_NOT_IMPLEMENTED(ins)
def instruction_fsgnjn_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00100|00 |rs2 |rs1 |001 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fsgnjn.s rd,rs1,rs2
:Description:
| Produce a result that takes all bits except the sign bit from rs1.
| The results sign bit is opposite of rs2s sign bit.
:Implementation:
| f[rd] = {~f[rs2][31], f[rs1][30:0]}
"""
INS_NOT_IMPLEMENTED(ins)
def instruction_fsgnjx_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00100|00 |rs2 |rs1 |010 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fsgnjx.s rd,rs1,rs2
:Description:
| Produce a result that takes all bits except the sign bit from rs1.
| The results sign bit is XOR of sign bit of rs1 and rs2.
:Implementation:
| f[rd] = {f[rs1][31] ^ f[rs2][31], f[rs1][30:0]}
"""
INS_NOT_IMPLEMENTED(ins)
def instruction_fmin_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00101|00 |rs2 |rs1 |000 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fmin.s rd,rs1,rs2
:Description:
| Write the smaller of single precision data in rs1 and rs2 to rd.
:Implementation:
| f[rd] = min(f[rs1], f[rs2])
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set_f(
rd,
Float32(
min(
rs1.value,
rs2.value,
)
),
)
def instruction_fmax_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|00101|00 |rs2 |rs1 |001 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fmax.s rd,rs1,rs2
:Description:
| Write the larger of single precision data in rs1 and rs2 to rd.
:Implementation:
| f[rd] = max(f[rs1], f[rs2])
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set_f(
rd,
Float32(
max(
rs1.value,
rs2.value,
)
),
)
def instruction_fcvt_w_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11000|00 |00000|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.w.s rd,rs1
:Description:
| Convert a floating-point number in floating-point register rs1 to a signed 32-bit in integer register rd.
:Implementation:
| x[rd] = sext(s32_{f32}(f[rs1]))
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set(rd, Int32(self.regs.get_f(rs).bytes))
def instruction_fcvt_wu_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11000|00 |00001|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.wu.s rd,rs1
:Description:
| Convert a floating-point number in floating-point register rs1 to a signed 32-bit in unsigned integer register rd.
:Implementation:
| x[rd] = sext(u32_{f32}(f[rs1]))
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set(rd, UInt32(self.regs.get_f(rs).bytes))
def instruction_fmv_x_w(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11100|00 |00000|rs1 |000 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fmv.x.w rd,rs1
:Description:
| Move the single-precision value in floating-point register rs1 represented in IEEE 754-2008 encoding to the lower 32 bits of integer register rd.
:Implementation:
| x[rd] = sext(f[rs1][31:0])
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set(rd, UInt32(self.regs.get_f(rs).bits))
def instruction_feq_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|10100|00 |rs2 |rs1 |010 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| feq.s rd,rs1,rs2
:Description:
| Performs a quiet equal comparison between floating-point registers rs1 and rs2 and record the Boolean result in integer register rd.
| Only signaling NaN inputs cause an Invalid Operation exception.
| The result is 0 if either operand is NaN.
:Implementation:
| x[rd] = f[rs1] == f[rs2]
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, Int32(rs1 == rs2))
def instruction_flt_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|10100|00 |rs2 |rs1 |001 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| flt.s rd,rs1,rs2
:Description:
| Performs a quiet less comparison between floating-point registers rs1 and rs2 and record the Boolean result in integer register rd.
| Only signaling NaN inputs cause an Invalid Operation exception.
| The result is 0 if either operand is NaN.
:Implementation:
| x[rd] = f[rs1] < f[rs2]
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, Int32(rs1 < rs2))
def instruction_fle_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|10100|00 |rs2 |rs1 |000 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fle.s rd,rs1,rs2
:Description:
| Performs a quiet less or equal comparison between floating-point registers rs1 and rs2 and record the Boolean result in integer register rd.
| Only signaling NaN inputs cause an Invalid Operation exception.
| The result is 0 if either operand is NaN.
:Implementation:
| x[rd] = f[rs1] <= f[rs2]
"""
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, Int32(rs1 <= rs2))
def instruction_fclass_s(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11100|00 |00000|rs1 |001 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fclass.s rd,rs1
:Description:
| Examines the value in floating-point register rs1 and writes to integer register rd a 10-bit mask that indicates the class of the floating-point number.
| The format of the mask is described in [classify table]_.
| The corresponding bit in rd will be set if the property is true and clear otherwise.
| All other bits in rd are cleared. Note that exactly one bit in rd will be set.
:Implementation:
| x[rd] = classifys(f[rs1])
"""
INS_NOT_IMPLEMENTED(ins)
def instruction_fcvt_s_w(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11010|00 |00000|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.s.w rd,rs1
:Description:
| Converts a 32-bit signed integer, in integer register rs1 into a floating-point number in floating-point register rd.
:Implementation:
| f[rd] = f32_{s32}(x[rs1])
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set_f(rd, Float32.from_bytes(self.regs.get(rs).signed().value))
def instruction_fcvt_s_wu(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11010|00 |00001|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.s.wu rd,rs1
:Description:
| Converts a 32-bit unsigned integer, in integer register rs1 into a floating-point number in floating-point register rd.
:Implementation:
| f[rd] = f32_{u32}(x[rs1])
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set_f(rd, Float32.from_bytes(self.regs.get(rs).unsigned_value))
def instruction_fmv_w_x(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11110|00 |00000|rs1 |000 |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fmv.w.x rd,rs1
:Description:
| Move the single-precision value encoded in IEEE 754-2008 standard encoding from the lower 32 bits of integer register rs1 to the floating-point register rd.
:Implementation:
| f[rd] = x[rs1][31:0]
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set_f(rd, Float32.from_bytes(self.regs.get(rs).unsigned_value))
def instruction_flw(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|imm[11:0] |rs1 |010 |rd |00001|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| flw rd,offset(rs1)
:Description:
| Load a single-precision floating-point value from memory into floating-point register rd.
:Implementation:
| f[rd] = M[x[rs1] + sext(offset)][31:0]
"""
rd, addr = self.parse_mem_ins(ins)
self.regs.set_f(rd, self.mmu.read_float(addr.value))
def instruction_fsw(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+--------+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+--------+-----+---+
|imm[11:5] |rs2 |rs1 |010 |imm[4:0]|01001|11 |
+-----+-----+-----+-----+-----+--------+-----+---+
:Format:
| fsw rs2,offset(rs1)
:Description:
| Store a single-precision value from floating-point register rs2 to memory.
:Implementation:
| M[x[rs1] + sext(offset)] = f[rs2][31:0]
"""
rs, addr = self.parse_mem_ins(ins)
val = self.regs.get_f(rs)
self.mmu.write(addr.value, 4, bytearray(val.bytes))
def parse_rd_rs(self, ins: Instruction) -> Tuple[str, str]:
assert len(ins.args) == 2
return ins.get_reg(0), ins.get_reg(1)
def parse_rd_rs_rs(self, ins: Instruction) -> Tuple[str, Float32, Float32]:
assert len(ins.args) == 3
return (
ins.get_reg(0),
self.regs.get_f(ins.get_reg(1)),
self.regs.get_f(ins.get_reg(2)),
)
def parse_rd_rs_rs_rs(
self, ins: Instruction
) -> Tuple[str, Float32, Float32, Float32]:
assert len(ins.args) == 4
return (
ins.get_reg(0),
self.regs.get_f(ins.get_reg(1)),
self.regs.get_f(ins.get_reg(2)),
self.regs.get_f(ins.get_reg(3)),
)

@ -0,0 +1,271 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
from .instruction_set import *
from ..CPU import UserModeCPU
from ..colors import FMT_DEBUG, FMT_NONE
from riscemu.types.exceptions import LaunchDebuggerException
from ..syscall import Syscall
from ..types import Instruction, Int32, UInt32
class RV32I(InstructionSet):
"""
The RV32I instruction set. Some instructions are missing, such as
fence, fence.i, rdcycle, rdcycleh, rdtime, rdtimeh, rdinstret, rdinstreth
All atomic read/writes are also not implemented yet
See https://maxvytech.com/images/RV32I-11-2018.pdf for a more detailed overview
"""
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"):
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"):
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"):
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"):
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"):
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"):
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"):
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"):
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))
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))
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)
)
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))
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))
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))
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)
def instruction_addi(self, ins: "Instruction"):
dst, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(dst, rs1 + imm)
def instruction_sub(self, ins: "Instruction"):
dst, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(dst, rs1 - rs2)
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"):
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"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, rs1 ^ rs2)
def instruction_xori(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(rd, rs1 ^ imm)
def instruction_or(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, rs1 | rs2)
def instruction_ori(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(rd, rs1 | imm)
def instruction_and(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, rs1 & rs2)
def instruction_andi(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(rd, rs1 & imm)
def instruction_slt(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, Int32(int(rs1 < rs2)))
def instruction_slti(self, ins: "Instruction"):
rd, rs1, imm = self.parse_rd_rs_imm(ins)
self.regs.set(rd, Int32(int(rs1 < imm)))
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)))
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)))
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"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 != rs2:
self.pc = dst.unsigned_value
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"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 >= rs2:
self.pc = dst.unsigned_value
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"):
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"):
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
if len(ins.args) == 1:
addr = ins.get_imm(0)
else:
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
addr = ins.get_imm(1)
self.regs.set(reg, Int32(self.pc))
self.pc = addr
def instruction_jalr(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 2)
reg = ins.get_reg(0)
base = ins.get_reg(1)
addr = ins.get_imm(2)
self.regs.set(reg, Int32(self.pc))
self.pc = self.regs.get(base).unsigned_value + addr
def instruction_ret(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 0)
self.pc = self.regs.get("ra").value
def instruction_ecall(self, ins: "Instruction"):
self.instruction_scall(ins)
def instruction_ebreak(self, ins: "Instruction"):
self.instruction_sbreak(ins)
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)
self.cpu.syscall_int.handle_syscall(syscall)
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
)
raise LaunchDebuggerException()
def instruction_nop(self, ins: "Instruction"):
ASSERT_LEN(ins.args, 0)
pass
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"):
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"):
ASSERT_LEN(ins.args, 2)
rd, rs = ins.get_reg(0), ins.get_reg(1)
self.regs.set(rd, self.regs.get(rs))

@ -0,0 +1,44 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
from .instruction_set import *
from riscemu.types.exceptions import INS_NOT_IMPLEMENTED
class RV32M(InstructionSet):
"""
The RV32M Instruction set, containing multiplication and division instructions
"""
def instruction_mul(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, rs1 * rs2)
def instruction_mulh(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, (rs1 * rs2) >> 32)
def instruction_mulhsu(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_mulhu(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
def instruction_div(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, rs1 // rs2)
def instruction_divu(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins, signed=False)
self.regs.set(rd, rs1 // rs2)
def instruction_rem(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins)
self.regs.set(rd, rs1 % rs2)
def instruction_remu(self, ins: "Instruction"):
rd, rs1, rs2 = self.parse_rd_rs_rs(ins, signed=False)
self.regs.set(rd, rs1 % rs2)

@ -0,0 +1,33 @@
from .instruction_set import InstructionSet, Instruction
class RV_Debug(InstructionSet):
def instruction_print(self, ins: Instruction):
reg = ins.get_reg(0)
print("register {} contains value {}".format(reg, self.regs.get(reg)))
def instruction_print_float(self, ins: Instruction):
reg = ins.get_reg(0)
print("register {} contains value {}".format(reg, self.regs.get_f(reg).value))
def instruction_print_uint(self, ins: Instruction):
reg = ins.get_reg(0)
print(
"register {} contains value {}".format(
reg, self.regs.get(reg).unsigned_value
)
)
def instruction_print_hex(self, ins: Instruction):
reg = ins.get_reg(0)
print(
"register {} contains value {}".format(reg, hex(self.regs.get(reg).value))
)
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)
)
)

@ -0,0 +1,16 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
This package holds all instruction sets, available to the processor
"""
from .instruction_set import InstructionSet, Instruction
from .RV32M import RV32M
from .RV32I import RV32I
from .RV32A import RV32A
from .RV32F import RV32F
from .RV_Debug import RV_Debug
InstructionSetDict = {v.__name__: v for v in [RV32I, RV32M, RV32A, RV32F, RV_Debug]}

@ -0,0 +1,160 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
from typing import Tuple, Callable, Dict
from abc import ABC
from ..CPU import CPU
from riscemu.types.exceptions import ASSERT_LEN, ASSERT_IN
from ..types import Instruction, Int32, UInt32
class InstructionSet(ABC):
"""
Represents a collection of instructions
Each instruction set has to inherit from this class. Each instruction should be it's own method:
instruction_<name>(self, ins: LoadedInstruction)
instructions containing a dot '.' should replace it with an underscore.
"""
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]]:
"""
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()}
def get_instructions(self):
"""
Returns a list of all valid instruction names included in this instruction set
converts underscores in names to dots
"""
for member in dir(self):
if member.startswith("instruction_"):
yield member[12:].replace("_", "."), getattr(self, member)
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)
"""
if len(ins.args) == 3:
# handle rd, rs1, imm
rs = self.get_reg_content(ins, 1)
imm = ins.get_imm(2)
else:
# handle rd, imm(rs1)
ASSERT_LEN(ins.args, 2)
ASSERT_IN("(", ins.args[1])
imm, rs_name = ins.get_imm_reg(1)
rs = self.regs.get(rs_name)
rd = ins.get_reg(0)
return rd, rs + imm
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)),
)
else:
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]:
"""
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)),
)
else:
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]:
"""
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)),
)
else:
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:
"""
get the register name from ins and then return the register contents
"""
return self.regs.get(ins.get_reg(ind))
@property
def pc(self):
"""
shorthand for self.cpu.pc
"""
return self.cpu.pc
@pc.setter
def pc(self, val):
self.cpu.pc = val
@property
def mmu(self):
return self.cpu.mmu
@property
def regs(self):
return self.cpu.regs
def __repr__(self):
return "InstructionSet[{}] with {} instructions".format(
self.__class__.__name__, len(list(self.get_instructions()))
)

@ -0,0 +1,36 @@
import sys
from riscemu import RunConfig
from riscemu.types import InstructionMemorySection, SimpleInstruction, Program
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)
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,
)
)
cpu.load_program(program)
cpu.setup_stack()
cpu.launch()
sys.exit(cpu.exit_code)

@ -1,3 +0,0 @@
from .CPU import *
from .tokenizer import *

@ -0,0 +1,143 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
import re
from typing import Dict, Tuple, Iterable, Callable, List
from .assembler import MemorySectionType, ParseContext, AssemblerDirectives
from .colors import FMT_PARSE
from .helpers import Peekable
from .tokenizer import Token, TokenType, tokenize
from .types import Program, T_ParserOpts, ProgramLoader, SimpleInstruction
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)
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()
)
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):
# 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))
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,
}
def parse_tokens(name: str, tokens_iter: Iterable[Token]) -> Program:
"""
Convert a token stream into a parsed program
:param name: the programs name
:param tokens_iter: the programs content, tokenized
:return: a parsed program
"""
context = ParseContext(name)
for token, args in composite_tokenizer(Peekable[Token](tokens_iter)):
if token.type not in PARSERS:
raise ParseException("Unexpected token type: {}, {}".format(token, args))
PARSERS[token.type](token, args, context)
return context.finalize()
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))
The first token ist either a pseudo_op, label, or instruction name. The token list are all remaining tokens before
a newline is encountered
:param tokens_iter: An iterator over tokens
:return: An iterator over a slightly more structured representation of the tokens
"""
tokens: Peekable[Token] = Peekable[Token](tokens_iter)
while not tokens.is_empty():
token = next(tokens)
if token.type in (
TokenType.PSEUDO_OP,
TokenType.LABEL,
TokenType.INSTRUCTION_NAME,
):
yield token, tuple(take_arguments(tokens))
def take_arguments(tokens: Peekable[Token]) -> Iterable[str]:
"""
Consumes (argument comma)* and yields argument.value until newline is reached
If an argument is not followed by either a newline or a comma, a parse exception is raised
The newline at the end is consumed
:param tokens: A Peekable iterator over some Tokens
"""
while True:
if tokens.peek().type == TokenType.ARGUMENT:
yield next(tokens).value
elif tokens.peek().type == TokenType.NEWLINE:
next(tokens)
break
elif tokens.peek().type == TokenType.COMMA:
next(tokens)
else:
break
# raise ParseException("Expected newline, instead got {}".format(tokens.peek()))
class AssemblyFileLoader(ProgramLoader):
"""
This class loads assembly files written by hand. It understands some assembler directives and supports most
pseudo instructions. It does very little verification of source correctness.
It also supports numbered jump targets and properly supports local and global scope (.globl assembly directive)
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:
return parse_tokens(self.filename, tokenize(f))
def parse_io(self, io: Iterable[str]):
return parse_tokens(self.filename, tokenize(io))
@classmethod
def can_parse(cls, source_path: str) -> float:
"""
It also acts as a weak fallback if no other loaders want to take the file.
:param source_path: the path to the source file
:return:
"""
# gcc recognizes these line endings as assembly. So we will do too.
if source_path.split(".")[-1] in ("asm", "S", "s"):
return 1
return 0.01
@classmethod
def get_options(cls, argv: List[str]) -> Tuple[List[str], T_ParserOpts]:
return argv, {}

@ -0,0 +1,141 @@
from typing import Dict, Union, Callable, Optional
from collections import defaultdict
from .privmodes import PrivModes
from .Exceptions import InstructionAccessFault
from ..colors import FMT_CSR, FMT_NONE
from .CSRConsts import CSR_NAME_TO_ADDR, MSTATUS_LEN_2, MSTATUS_OFFSETS
from ..types import UInt32
class CSR:
"""
This holds all Control and Status Registers (CSR)
"""
regs: Dict[int, UInt32]
"""
All Control and Status Registers are stored here
"""
virtual_regs: Dict[int, Callable[[], UInt32]]
"""
list of virtual CSR registers, with values computed on read
"""
listeners: Dict[int, Callable[[UInt32, UInt32], None]]
mstatus_cache: Dict[str, UInt32]
mstatus_cache_dirty = True
def __init__(self):
self.regs = defaultdict(lambda: UInt32(0))
self.listeners = defaultdict(lambda: (lambda x, y: None))
self.virtual_regs = dict()
self.mstatus_cache = dict()
# TODO: implement write masks (bitmasks which control writeable bits in registers
def set(self, addr: Union[str, int], val: Union[int, UInt32]):
addr = self._name_to_addr(addr)
if addr is None:
return
val = UInt32(val)
self.listeners[addr](self.regs[addr], val)
if addr == 0x300:
self.mstatus_cache_dirty = True
self.regs[addr] = val
def get(self, addr: Union[str, int]) -> UInt32:
addr = self._name_to_addr(addr)
if addr is None:
raise RuntimeError(f"Invalid CSR name: {addr}!")
if addr in self.virtual_regs:
return self.virtual_regs[addr]()
return self.regs[addr]
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))
return
self.listeners[addr] = listener
# mstatus properties
def set_mstatus(self, name: str, val: UInt32):
"""
Set mstatus bits using this helper. mstatus is a 32 bit register, holding various machine status flags
Setting them by hand is super painful, so this helper allows you to set specific bits.
Please make sure your supplied value has the correct width!
:param name:
:param val:
:return:
"""
size = 2 if name in MSTATUS_LEN_2 else 1
off = MSTATUS_OFFSETS[name]
mask = (2**size - 1) << off
old_val = self.get("mstatus")
erased = old_val & (~mask)
new_val = erased | (val << off)
self.set("mstatus", new_val)
def get_mstatus(self, name) -> UInt32:
if not self.mstatus_cache_dirty and name in self.mstatus_cache:
return self.mstatus_cache[name]
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
if self.mstatus_cache_dirty:
self.mstatus_cache = dict(name=val)
else:
self.mstatus_cache[name] = val
return val
def callback(self, addr: Union[str, int]):
def inner(func: Callable[[UInt32, UInt32], None]):
self.set_listener(addr, func)
return func
return inner
def assert_can_read(self, mode: PrivModes, addr: int):
if (addr >> 8) & 3 > mode.value:
raise InstructionAccessFault(addr)
def assert_can_write(self, mode: PrivModes, addr: int):
if (addr >> 8) & 3 > mode.value or addr >> 10 == 11:
raise InstructionAccessFault(addr)
def _name_to_addr(self, addr: Union[str, int]) -> Optional[int]:
if isinstance(addr, str):
if addr not in CSR_NAME_TO_ADDR:
print("Unknown CSR register {}".format(addr))
return None
return CSR_NAME_TO_ADDR[addr]
return addr
def virtual_register(self, addr: Union[str, int]):
addr = self._name_to_addr(addr)
if addr is None:
print("unknown csr address name: {}".format(addr))
def inner(func: Callable[[], UInt32]):
self.virtual_regs[addr] = func
return func
return inner
def dump_mstatus(self):
print(FMT_CSR + "[CSR] dumping mstatus:")
i = 0
for name in MSTATUS_OFFSETS:
print(" {:<5} {}".format(name, self.get_mstatus(name)), end="")
if i % 6 == 5:
print()
i += 1
print(FMT_NONE)

@ -0,0 +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",
}
"""
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,
}
"""
Offsets for all mstatus bits
"""
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,
}
"""
Translation for named registers
"""

@ -0,0 +1,128 @@
from typing import List
from .Exceptions import *
from .types import ElfMemorySection
from ..helpers import FMT_PARSE, FMT_NONE, FMT_GREEN, FMT_BOLD
from ..types import MemoryFlags, Program, ProgramLoader, T_ParserOpts
FMT_ELF = FMT_GREEN + FMT_BOLD
if typing.TYPE_CHECKING:
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import Section, SymbolTableSection
INCLUDE_SEC = (".text", ".stack", ".bss", ".sdata", ".sbss")
class ElfBinaryFileLoader(ProgramLoader):
"""
Loads compiled elf binaries (checks for the magic sequence 7f45 4c46)
This loader respects local and global symbols.
"""
program: Program
def __init__(self, source_path: str, options: T_ParserOpts):
super().__init__(source_path, options)
self.program = Program(self.filename)
@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":
return 1
return 0
@classmethod
def get_options(cls, argv: List[str]) -> [List[str], T_ParserOpts]:
return argv, {}
def parse(self) -> Program:
try:
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
)
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
)
raise e
return self.program
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":
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)
continue
if sec.name not in INCLUDE_SEC:
continue
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",)
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
)
return ElfMemorySection(
data, sec.name, self.program.context, owner, sec.header.sh_addr, flags
)
def _parse_symtab(self, symtab: "SymbolTableSection"):
from elftools.elf.enums import ENUM_ST_VISIBILITY
for sym in symtab.iter_symbols():
if not sym.name:
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":
self.program.global_labels.add(sym.name)
print(
FMT_PARSE
+ "LOADED GLOBAL SYMBOL {}: {}".format(sym.name, sym.entry.st_value)
+ FMT_NONE
)
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
)
raise RuntimeError("Cannot load elf with overlapping sections!")
self.program.add_section(new_sec)

@ -0,0 +1,127 @@
from typing import Optional, NewType
from enum import Enum
from .privmodes import PrivModes
from .CSRConsts import MCAUSE_TRANSLATION
import typing
from .. import RiscemuBaseException
from ..colors import FMT_PARSE, FMT_NONE
from ..types import UInt32
if typing.TYPE_CHECKING:
from .ElfLoader import ElfInstruction
class CpuTrapType(Enum):
TIMER = 1
SOFTWARE = 2
EXTERNAL = 3
EXCEPTION = 4
class CpuTrap(BaseException):
code: int
"""
31-bit value encoding the exception code in the mstatus register
"""
interrupt: int
"""
The isInterrupt bit in the mstatus register
"""
mtval: UInt32
"""
contents of the mtval register
"""
type: CpuTrapType
"""
The type (timer, external, software) of the trap
"""
priv: PrivModes
"""
The privilege level this trap targets
"""
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)
self.priv = priv
self.type = type
@property
def mcause(self):
return (self.interrupt << 31) + self.code
def message(self) -> str:
return ""
def __repr__(self):
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
)
return "{} {{priv={}, type={}, mtval={:x}}} {}".format(
name, self.priv.name, self.type.name, self.mtval, self.message()
)
def __str__(self):
return self.__repr__()
class IllegalInstructionTrap(CpuTrap):
def __init__(self, ins: "ElfInstruction"):
super().__init__(2, ins.encoded, CpuTrapType.EXCEPTION)
class InstructionAddressMisalignedTrap(CpuTrap):
def __init__(self, addr: int):
super().__init__(0, addr, CpuTrapType.EXCEPTION)
class InstructionAccessFault(CpuTrap):
def __init__(self, addr: int):
super().__init__(1, addr, CpuTrapType.EXCEPTION)
class TimerInterrupt(CpuTrap):
def __init__(self):
super().__init__(7, 0, CpuTrapType.TIMER)
class EcallTrap(CpuTrap):
def __init__(self, mode: PrivModes):
super().__init__(mode.value + 8, 0, CpuTrapType.EXCEPTION)
class InvalidElfException(RiscemuBaseException):
def __init__(self, msg: str):
super().__init__()
self.msg = msg
def message(self):
return (
FMT_PARSE + '{}("{}")'.format(self.__class__.__name__, self.msg) + FMT_NONE
)
class LoadAccessFault(CpuTrap):
def __init__(self, msg, addr, size, op):
super(LoadAccessFault, self).__init__(5, addr, CpuTrapType.EXCEPTION)
self.msg = msg
self.addr = addr
self.size = size
self.op = op
def message(self):
return "(During {} at 0x{:08x} of size {}: {})".format(
self.op, self.addr, self.size, self.msg
)

@ -0,0 +1,86 @@
"""
Laods a memory image with debug information into memory
"""
import os.path
from typing import List, Iterable
from .ElfLoader import ElfMemorySection
from .types import MemoryImageDebugInfos
from ..assembler import INSTRUCTION_SECTION_NAMES
from ..colors import FMT_NONE, FMT_PARSE
from ..helpers import get_section_base_name
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":
return 1
return 0
@classmethod
def get_options(cls, argv: List[str]) -> [List[str], T_ParserOpts]:
return argv, {}
def parse(self) -> Iterable[Program]:
if "debug" not in self.options:
yield self.parse_no_debug()
return
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:
data: bytearray = bytearray(source_file.read())
for name, sections in debug_info.sections.items():
program = Program(name)
for sec_name, (start, size) in sections.items():
if program.base is None:
program.base = start
# 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),
)
)
program.context.labels.update(debug_info.symbols.get(name, dict()))
program.global_labels.update(debug_info.globals.get(name, set()))
yield program
def parse_no_debug(self) -> Program:
print(
FMT_PARSE
+ "[MemoryImageLoader] Warning: loading memory image without debug information!"
+ FMT_NONE
)
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)
)
)
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")
)
return MemoryImageLoader(source_path, options)

@ -0,0 +1,276 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
import sys
import time
from riscemu.CPU import *
from .CSR import CSR
from .ElfLoader import ElfBinaryFileLoader
from .Exceptions import *
from .ImageLoader import MemoryImageLoader
from .PrivMMU import PrivMMU
from .PrivRV32I import PrivRV32I
from .privmodes import PrivModes
from ..IO.TextIO import TextIO
from ..instructions import RV32A, RV32M
from ..types import Program, UInt32
if typing.TYPE_CHECKING:
from riscemu.instructions.instruction_set import InstructionSet
class PrivCPU(CPU):
"""
This is a CPU that has different modes, instruction sets and registers.
It should support M and U Mode, but no U-Mode Traps.
This is meant to emulate whole operating systems.
"""
csr: CSR
"""
Reference to the control and status registers
"""
TIME_RESOLUTION_NS: int = 10000000
"""
controls the resolution of the time csr register (in nanoseconds)
"""
pending_traps: List[CpuTrap]
"""
A list of traps which are pending to be handled
"""
def __init__(self, conf):
super().__init__(PrivMMU(), [PrivRV32I, RV32M, RV32A], conf)
# start in machine mode
self.mode: PrivModes = PrivModes.MACHINE
self.pending_traps: List[CpuTrap] = list()
self.exit_code = 0
self._time_start = 0
self._time_timecmp = UInt32(0)
self._time_interrupt_enabled = False
# performance counters
self._perf_counters = list()
# add TextIO
io = TextIO(0xFF0000, 64)
self.mmu.load_section(io, True)
# init csr
self._init_csr()
self.TIME_RESOLUTION_NS = int(self.TIME_RESOLUTION_NS * conf.slowdown)
def run(self, verbose=False):
if self.pc <= 0:
return False
launch_debug = False
try:
while not self.halted:
self.step(verbose)
except RiscemuBaseException as ex:
if isinstance(ex, LaunchDebuggerException):
launch_debug = True
self.pc += self.INS_XLEN
if self.halted:
print()
print(
FMT_CPU
+ "[CPU] System halted with code {}".format(self.exit_code)
+ FMT_NONE
)
sys.exit(self.exit_code)
elif launch_debug:
launch_debug_session(self)
if not self.debugger_active:
self.run(verbose)
else:
print()
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
)
self._time_start = time.perf_counter_ns() // self.TIME_RESOLUTION_NS
self.run(self.conf.verbosity > 1 or verbose)
def load_program(self, program: Program):
if program.name == "kernel":
self.pc = program.entrypoint
super().load_program(program)
def _init_csr(self):
# set up CSR
self.csr = CSR()
self.csr.set("mhartid", UInt32(0)) # core id
# TODO: set correct value
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
# CSR write callbacks:
@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")
def mtimecmp(old: UInt32, new: UInt32):
self._time_timecmp = (self.csr.get("mtimecmph") << 32) + new
self._time_interrupt_enabled = True
@self.csr.callback("mtimecmph")
def mtimecmph(old: UInt32, new: UInt32):
self._time_timecmp = (new << 32) + self.csr.get("mtimecmp")
self._time_interrupt_enabled = True
# virtual CSR registers:
@self.csr.virtual_register("time")
def get_time():
return UInt32(
time.perf_counter_ns() // self.TIME_RESOLUTION_NS - self._time_start
)
@self.csr.virtual_register("timeh")
def get_timeh():
return UInt32(
(time.perf_counter_ns() // self.TIME_RESOLUTION_NS - self._time_start)
>> 32
)
# add minstret and mcycle counters
def _handle_trap(self, trap: CpuTrap):
# implement trap handling!
self.pending_traps.append(trap)
def step(self, verbose=True):
try:
self.cycle += 1
if self.cycle % 20 == 0:
self._timer_step()
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)
)
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
)
breakpoint()
if self.conf.debug_on_exception:
raise LaunchDebuggerException()
self.pc += self.INS_XLEN
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
):
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")):
return
# select best interrupt
# FIXME: actually select based on the official ranking
trap = self.pending_traps.pop() # use the most recent trap
if self.conf.verbosity > 0:
print(FMT_CPU + "[CPU] taking trap {}!".format(trap) + FMT_NONE)
self.regs.dump_reg_a()
if trap.priv != PrivModes.MACHINE:
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.mode = trap.priv
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.record_perf_profile()
if len(self._perf_counters) > 100:
self.show_perf()
def show_perf(self):
timed = 0
cycled = 0
cps_list = list()
print(FMT_CPU + "[CPU] Performance overview:")
for time_ns, cycle in self._perf_counters:
if cycled == 0:
cycled = cycle
timed = time_ns
continue
cps = (cycle - cycled) / (time_ns - timed) * 1000000000
cycled = cycle
timed = time_ns
cps_list.append(cps)
print(
" on average {:.0f} instructions/s".format(sum(cps_list) / len(cps_list))
+ FMT_NONE
)
self._perf_counters = list()
def record_perf_profile(self):
self._perf_counters.append((time.perf_counter_ns(), self.cycle))
@classmethod
def get_loaders(cls) -> typing.Iterable[Type[ProgramLoader]]:
return [AssemblyFileLoader, MemoryImageLoader, ElfBinaryFileLoader]

@ -0,0 +1,51 @@
from .types import ElfMemorySection
from ..MMU import *
from abc import abstractmethod
import typing
if typing.TYPE_CHECKING:
from .PrivCPU import PrivCPU
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)
if existing_sec is not None:
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
)
# get sec succeeding empty space at addr
sec_after = next((sec for sec in self.sections if sec.base > addr), None)
# calc start end end of "free" space
prev_sec_end = 0 if sec_before is None else sec_before.end
next_sec_start = 0x7FFFFFFF if sec_after is None else sec_after.base
# start at the end of the prev section, or current address - 0xFFFF (aligned to 16 byte boundary)
start = max(prev_sec_end, align_addr(addr - 0xFFFF, 16))
# 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),
)
self.sections.append(sec)
self._update_state()
return sec
def global_instruction_context(self) -> InstructionContext:
context = InstructionContext()
context.global_symbol_dict = self.global_symbols
return context

@ -0,0 +1,175 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
from ..instructions.RV32I import *
from riscemu.types.exceptions import INS_NOT_IMPLEMENTED
from .Exceptions import *
from .privmodes import PrivModes
from ..colors import FMT_CPU, FMT_NONE
import typing
if typing.TYPE_CHECKING:
from riscemu.priv.PrivCPU import PrivCPU
class PrivRV32I(RV32I):
cpu: "PrivCPU"
"""
This is an extension of RV32I, written for the PrivCPU class
"""
def instruction_csrrw(self, ins: "Instruction"):
rd, rs, csr_addr = self.parse_crs_ins(ins)
old_val = None
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":
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"):
rd, rs, csr_addr = self.parse_crs_ins(ins)
if rs != "zero":
# oh no, this should not happen!
INS_NOT_IMPLEMENTED(ins)
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"):
INS_NOT_IMPLEMENTED(ins)
def instruction_csrrsi(self, ins: "Instruction"):
INS_NOT_IMPLEMENTED(ins)
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":
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"):
INS_NOT_IMPLEMENTED(ins)
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)
# restore priv
mpp = self.cpu.csr.get_mstatus("mpp")
self.cpu.mode = PrivModes(mpp)
# restore pc
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
)
if self.cpu.conf.verbosity > 1:
self.regs.dump_reg_a()
def instruction_uret(self, ins: "Instruction"):
raise IllegalInstructionTrap(ins)
def instruction_sret(self, ins: "Instruction"):
raise IllegalInstructionTrap(ins)
def instruction_scall(self, ins: "Instruction"):
"""
Overwrite the scall from userspace RV32I
"""
raise EcallTrap(self.cpu.mode)
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"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 != rs2:
self.pc += dst.value - 4
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"):
rs1, rs2, dst = self.parse_rs_rs_imm(ins)
if rs1 >= rs2:
self.pc += dst.value - 4
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"):
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"):
raise NotImplementedError("Should never be reached!")
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)
):
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"):
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"):
raise LaunchDebuggerException()
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]:
ASSERT_LEN(ins.args, 3)
addr = self.get_reg_content(ins, 1) + ins.get_imm(2)
reg = ins.get_reg(0)
return reg, addr

@ -0,0 +1,15 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
The priv Module holds everything necessary for emulating privileged risc-v assembly
Running priv is only preferable to the normal userspace emulator, if you actually want to emulate the whole system.
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.
"""

@ -0,0 +1,65 @@
from riscemu import RunConfig
from riscemu.types import Program
from .PrivCPU import PrivCPU
import sys
if __name__ == "__main__":
import argparse
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(
"-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,
)
args = parser.parse_args()
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),
)
argv, opts = loader.get_options(sys.argv)
program = loader.instantiate(source_path, opts).parse()
if isinstance(program, Program):
cpu.load_program(program)
else:
program_iter = program
for program in program_iter:
cpu.load_program(program)
cpu.launch(verbose=args.verbose > 4)

@ -0,0 +1,7 @@
from enum import IntEnum
class PrivModes(IntEnum):
USER = 0
SUPER = 1
MACHINE = 3

@ -0,0 +1,174 @@
import json
from collections import defaultdict
from dataclasses import dataclass
from functools import lru_cache
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,
)
@dataclass(frozen=True)
class ElfInstruction(Instruction):
name: str
args: Tuple[int]
encoded: int
def get_imm(self, num: int) -> int:
return self.args[num]
def get_imm_reg(self, num: int) -> Tuple[int, int]:
return self.args[-1], self.args[-2]
def get_reg(self, num: int) -> str:
return RISCV_REGS[self.args[num]]
def __repr__(self) -> str:
if self.name == "jal" and self.args[0] == 0:
return "j {}".format(self.args[1])
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:
return "li {}, {}".format(self.get_reg(0), self.args[2])
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,
):
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
)
raise InstructionAccessFault(offset + self.base)
if offset % 4 != 0:
raise InstructionAddressMisalignedTrap(offset + self.base)
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"
)
self.read_ins.cache_clear()
return super(ElfMemorySection, self).write(offset, size, data)
@property
def end(self):
return self.size + self.base
class MemoryImageDebugInfos:
VERSION = "1.0.0"
"""
Schema version
"""
base: T_AbsoluteAddress = 0
"""
The base address where the image starts. Defaults to zero.
"""
sections: Dict[str, Dict[str, Tuple[int, int]]]
"""
This dictionary maps a program and section to (start address, section length)
"""
symbols: Dict[str, Dict[str, int]]
"""
This dictionary maps a program and a symbol to a value
"""
globals: Dict[str, Set[str]]
"""
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,
):
self.sections = sections
self.symbols = symbols
self.globals = globals
for name in globals:
globals[name] = set(globals[name])
self.base = base
def serialize(self) -> str:
def serialize(obj: any) -> str:
if isinstance(obj, defaultdict):
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 json.dumps(
dict(
sections=self.sections,
symbols=self.symbols,
globals=self.globals,
base=self.base,
VERSION=self.VERSION,
),
default=serialize,
)
@classmethod
def load(cls, serialized_str: str) -> "MemoryImageDebugInfos":
json_obj: dict = json.loads(serialized_str)
if "VERSION" not in json_obj:
raise RuntimeError("Unknown MemoryImageDebugInfo version!")
version: str = json_obj.pop("VERSION")
# compare major version
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
)
)
return MemoryImageDebugInfos(**json_obj)
@classmethod
def builder(cls) -> "MemoryImageDebugInfos":
return MemoryImageDebugInfos(
defaultdict(dict), defaultdict(dict), defaultdict(set)
)

@ -0,0 +1,248 @@
"""
RiscEmu (c) 2023 Anton Lydike
SPDX-License-Identifier: MIT
"""
from collections import defaultdict
from typing import Union
from .helpers import *
from .types import Int32, Float32
class Registers:
"""
Represents a bunch of 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",
}
float_regs = {
"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):
self.vals: dict[str, Int32] = defaultdict(UInt32)
self.float_vals: dict[str, Float32] = defaultdict(Float32)
self.last_set = None
self.last_read = None
self.infinite_regs = infinite_regs
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: list[list[str]] = [[] for _ in range(12)]
if not full:
regs = [("a", 8), ("s", 12), ("t", 7)]
else:
regs = [
("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)
lines[i].append(self._reg_repr(reg))
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:]))
print("\t" + "--------------- " * 3)
else:
print("\t" + " ".join(named_regs))
print("\t" + "--------------- " * 6)
for line in lines:
print("\t" + " ".join(line))
print(")")
def dump_reg_a(self):
"""
Dump the a registers
"""
print(
"Registers[a]:"
+ " ".join(self._reg_repr("a{}".format(i)) for i in range(8))
)
def _reg_repr(self, reg: str, name_len=4, fmt="08X"):
if reg in self.float_regs:
txt = "{:{}}={: .3e}".format(reg, name_len, self.get_f(reg, False))
else:
txt = "{:{}}=0x{:{}}".format(reg, name_len, self.get(reg, False), fmt)
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":
return txt
should_grayscale_int = (
reg in self.valid_regs
and self.get(reg, False) == 0
and reg not in Registers.named_registers()
)
should_grayscale_float = reg in self.float_regs and self.get_f(reg, False) == 0
if should_grayscale_int or should_grayscale_float:
return FMT_GRAY + txt + FMT_NONE
return txt
def set(self, reg: str, val: "Int32", mark_set: bool = True) -> bool:
"""
Set a register content to val
:param reg: The register to set
:param val: The new value
:param mark_set: If True, marks this register as "last accessed" (only used internally)
:return: If the operation was successful
"""
# remove after refactoring is complete
if not isinstance(val, Int32):
raise RuntimeError(
"Setting register to non-Int32 value! Please refactor your code!"
)
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 mark_set:
self.last_set = reg
if not self.infinite_regs and reg not in self.valid_regs:
raise RuntimeError("Invalid register: {}".format(reg))
self.vals[reg] = val.unsigned()
return True
def get(self, reg: str, mark_read: bool = True) -> "Int32":
"""
Retuns the contents of register reg
:param reg: The register name
:param mark_read: If the register should be markes as "last read" (only used internally)
:return: The contents of register reg
"""
# if reg not in Registers.all_registers():
# raise InvalidRegisterException(reg)
if reg == "fp":
reg = "s0"
if not self.infinite_regs and reg not in self.valid_regs:
raise RuntimeError("Invalid register: {}".format(reg))
if mark_read:
self.last_read = reg
return self.vals[reg]
def get_f(self, reg: str, mark_read: bool = True) -> "Float32":
if not self.infinite_regs and reg not in self.float_regs:
raise RuntimeError("Invalid float register: {}".format(reg))
if mark_read:
self.last_read = reg
return self.float_vals[reg]
def set_f(self, reg: str, val: Union[float, "Float32"]):
if not self.infinite_regs and reg not in self.float_regs:
raise RuntimeError("Invalid float register: {}".format(reg))
self.float_vals[reg] = Float32(val)
@staticmethod
def named_registers():
"""
Return all named registers
:return: The list
"""
return ["zero", "ra", "sp", "gp", "tp", "fp"]
def __repr__(self):
return "<Registers[{}]{}>".format(
"float" if self.supports_floats else "nofloat",
"{"
+ ", ".join(self._reg_repr("a{}".format(i), 2, "0x") for i in range(8))
+ "}",
)

@ -0,0 +1,284 @@
import argparse
import glob
import os
import sys
from typing import Type, Dict, List, Optional
from riscemu import AssemblyFileLoader, __version__, __copyright__
from riscemu.types import CPU, ProgramLoader, Program
from riscemu.instructions import InstructionSet, InstructionSetDict
from riscemu.config import RunConfig
from riscemu.CPU import UserModeCPU
class RiscemuMain:
"""
This represents the riscemu API exposed to other programs for better
interoperability.
"""
available_ins_sets: Dict[str, Type[InstructionSet]]
available_file_loaders: List[Type[ProgramLoader]]
cfg: Optional[RunConfig]
cpu: Optional[CPU]
input_files: List[str]
selected_ins_sets: List[Type[InstructionSet]]
def __init__(self, cfg: Optional[RunConfig] = None):
self.available_ins_sets = dict()
self.selected_ins_sets = []
self.available_file_loaders = []
self.cfg: Optional[RunConfig] = cfg
self.cpu: Optional[CPU] = None
self.input_files = []
self.selected_ins_sets = []
def instantiate_cpu(self):
self.cpu = UserModeCPU(self.selected_ins_sets, self.cfg)
self.configure_cpu()
def configure_cpu(self):
assert self.cfg is not None
if isinstance(self.cpu, UserModeCPU) and self.cfg.stack_size != 0:
self.cpu.setup_stack(self.cfg.stack_size)
def register_all_arguments(self, parser: argparse.ArgumentParser):
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",
"libc",
"ignore_exit_code",
),
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
libc: Load a libc-like runtime (for malloc, etc.)
ignore_exit_code: Don't exit with the programs exit code.""",
)
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(self.available_ins_sets)
),
keys={k: True for k in self.available_ins_sets},
omit_empty=True,
)
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(
"--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,
)
def register_all_isas(self):
self.available_ins_sets.update(InstructionSetDict)
def register_all_program_loaders(self):
self.available_file_loaders.append(AssemblyFileLoader)
def parse_argv(self, argv: List[str]):
parser = argparse.ArgumentParser(
description="RISC-V Userspace emulator",
prog="riscemu",
formatter_class=argparse.RawTextHelpFormatter,
)
if "--version" in argv:
print(
"riscemu version {}\n{}\n\nAvailable ISA: {}".format(
__version__, __copyright__, ", ".join(self.available_ins_sets)
)
)
sys.exit()
self.register_all_arguments(parser)
# parse argv
args = parser.parse_args(argv)
# add ins
if not hasattr(args, "ins"):
setattr(args, "ins", {k: True for k in self.available_ins_sets})
# create RunConfig
self.cfg = self.create_config(args)
# set input files
self.input_files = args.files
# get selected ins sets
self.selected_ins_sets = list(
self.available_ins_sets[name]
for name, selected in args.ins.items()
if selected
)
# if use_libc is given, attach libc to path
if self.cfg.use_libc:
self.add_libc_to_input_files()
def add_libc_to_input_files(self):
"""
This adds the provided riscemu libc to the programs runtime.
"""
libc_path = os.path.join(
os.path.dirname(__file__),
"..",
"libc",
"*.s",
)
for path in glob.iglob(libc_path):
if path not in self.input_files:
self.input_files.append(path)
def create_config(self, args: argparse.Namespace) -> RunConfig:
# 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,
use_libc=args.options["libc"],
ignore_exit_code=args.options["ignore_exit_code"],
)
for k, v in dict(cfg_dict).items():
if v is None:
del cfg_dict[k]
return RunConfig(**cfg_dict)
def load_programs(self):
for path in self.input_files:
for loader in self.available_file_loaders:
if not loader.can_parse(path):
continue
programs = loader.instantiate(path, {}).parse()
if isinstance(programs, Program):
programs = [programs]
for p in programs:
self.cpu.mmu.load_program(p)
def run_from_cli(self, argv: List[str]):
# register everything
self.register_all_isas()
self.register_all_program_loaders()
# parse argv and set up cpu
self.parse_argv(argv)
self.instantiate_cpu()
self.load_programs()
# run the program
self.cpu.launch(self.cfg.verbosity > 1)
def run(self):
"""
This assumes that these values were set externally:
- available_file_loaders: A list of available file loaders.
Can be set using .register_all_program_loaders()
- cfg: The RunConfig object. Can be directly assigned to the attribute
- input_files: A list of assembly files to load.
- selected_ins_sets: A list of instruction sets the CPU should support.
"""
assert self.cfg is not None, "self.cfg must be set before calling run()"
assert self.selected_ins_sets, "self.selected_ins_sets cannot be empty"
assert self.input_files, "self.input_files cannot be empty"
if self.cfg.use_libc:
self.add_libc_to_input_files()
self.instantiate_cpu()
self.load_programs()
# run the program
self.cpu.launch(self.cfg.verbosity > 1)
class OptionStringAction(argparse.Action):
def __init__(self, option_strings, dest, keys=None, omit_empty=False, **kwargs):
if keys is None:
raise ValueError('must define "keys" argument')
if isinstance(keys, dict):
keys_d = keys
elif isinstance(keys, (list, tuple)):
keys_d = {}
for k in keys:
if isinstance(k, tuple):
k, v = k
else:
v = False
keys_d[k] = v
else:
keys_d = dict()
super().__init__(option_strings, dest, default=keys_d, **kwargs)
self.keys = keys_d
self.omit_empty = omit_empty
def __call__(self, parser, namespace, values, option_string=None):
d = {}
if not self.omit_empty:
d.update(self.keys)
for x in values.split(","):
if x in self.keys:
d[x] = True
else:
raise ValueError("Invalid parameter supplied: " + x)
setattr(namespace, self.dest, d)

@ -0,0 +1,295 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
import sys
from dataclasses import dataclass
from math import log2, ceil
from typing import Dict, IO, Union
from .types import BinaryDataMemorySection, MemoryFlags
from .colors import FMT_SYSCALL, FMT_NONE
from .types import Int32, CPU
from .types.exceptions import InvalidSyscallException
SYSCALLS = {
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
dictionary and implement a method with the same name on the SyscallInterface
class.
"""
ADDITIONAL_SYMBOLS = {
"MAP_PRIVATE": 1 << 0,
"MAP_SHARED": 1 << 1,
"MAP_ANON": 1 << 2,
"MAP_ANONYMOUS": 1 << 2,
"PROT_READ": 1 << 0,
"PROT_WRITE": 1 << 1,
}
"""
A set of additional symbols that are used by various syscalls.
"""
OPEN_MODES = {
0: "rb",
1: "wb",
2: "r+b",
3: "x",
4: "ab",
}
"""All available file open modes"""
@dataclass(frozen=True)
class Syscall:
"""
Represents a syscall
"""
id: int
"""The syscall number (e.g. 64 - write)"""
cpu: CPU
"""The CPU object that created the syscall"""
@property
def name(self):
return SYSCALLS.get(self.id, "unknown")
def __repr__(self):
return "Syscall(id={}, name={})".format(self.id, self.name)
def ret(self, code: Union[int, Int32]):
self.cpu.regs.set("a0", Int32(code))
def get_syscall_symbols():
"""
Generate global syscall symbols (such as SCALL_READ, SCALL_EXIT etc)
:return: dictionary of all syscall symbols (SCALL_<name> -> id)
"""
items: Dict[str, int] = {
("SCALL_" + name.upper()): num for num, name in SYSCALLS.items()
}
items.update(ADDITIONAL_SYMBOLS)
return items
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}
if getattr(self, scall.name):
getattr(self, scall.name)(scall)
else:
raise InvalidSyscallException(scall)
def read(self, scall: Syscall):
"""
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
if fileno not in self.open_files:
scall.ret(-1)
return
chars = self.open_files[fileno].readline(size)
try:
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
)
return scall.ret(-1)
def write(self, scall: Syscall):
"""
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
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
)
return scall.ret(-1)
self.open_files[fileno].write(data.decode("ascii"))
return scall.ret(size)
def open(self, scall: Syscall):
"""
open syscall (1024): read path of a2 bytes from addr a1, in mode a0
returns the file no in a0
modes:
- 0: read
- 1: write (truncate)
- 2: read/write (no truncate)
- 3: only create
- 4: append
Requires running with flag scall-fs
"""
# 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
)
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_st = OPEN_MODES.get(
mode,
)
if mode_st == -1:
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")
fileno = self.next_open_handle
self.next_open_handle += 1
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
)
return scall.ret(-1)
print(
FMT_SYSCALL
+ "[Syscall] open: opened fd {} to {}!".format(fileno, path)
+ FMT_NONE
)
return scall.ret(fileno)
def close(self, scall: Syscall):
"""
close syscall (1025): closes file no a0
return -1 if an error was encountered, otherwise returns 0
"""
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
)
return scall.ret(-1)
self.open_files[fileno].close()
print(FMT_SYSCALL + "[Syscall] close: closed fd {}!".format(fileno) + FMT_NONE)
del self.open_files[fileno]
return scall.ret(0)
def exit(self, scall: Syscall):
"""
Exit syscall. Exits the system with status code a0
"""
scall.cpu.halted = True
scall.cpu.exit_code = scall.cpu.regs.get("a0").signed().value
def mmap2(self, scall: Syscall):
"""
mmap2 syscall:
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
Only supported modes:
addr = <any>
prot = either PROT_READ or PROT_READ | PROT_WRITE
flags = MAP_PRIVATE | MAP_ANONYMOUS
fd = <ignored>
off_t = <ignored>
"""
addr = scall.cpu.regs.get("a0").unsigned_value
size = scall.cpu.regs.get("a1").unsigned_value
prot = scall.cpu.regs.get("a2").unsigned_value
flags = scall.cpu.regs.get("a3").unsigned_value
# error out if prot is not 1 or 3:
# 1 = PROT_READ
# 3 = PROT_READ | PROT_WRITE
if prot != 1 and prot != 3:
return scall.ret(-1)
# round size up to multiple of 4096
size = 4096 * ceil(size / 4096)
section = BinaryDataMemorySection(
bytearray(size),
".data.runtime-allocated",
None,
"system",
base=addr,
flags=MemoryFlags(read_only=prot != 3, executable=False),
)
# try to insert section
if scall.cpu.mmu.load_section(section, addr != 0):
return scall.ret(section.base)
# if that failed, and we tried to force an address,
# try again at any address
elif addr != 0:
section.base = 0
if scall.cpu.mmu.load_section(section):
return scall.ret(section.base)
# 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)

@ -1,299 +1,136 @@
import re
from enum import IntEnum
from typing import List
from . import CPU, Registers
REGISTERS = list(Registers.all_registers())
INSTRUCTIONS = list(CPU.all_instructions())
PSEUDO_OPS = [
'.asciiz',
'.double',
'.extern',
'.global',
'.align',
'.float',
'.kdata',
'.ktext',
'.space',
'.ascii',
'.byte',
'.data',
'.half',
'.text',
'.word'
'.set',
]
COMMENT_START = ["#", ";"]
REG_VALID_SYMBOL_LABEL = re.compile(r'^([A-z_.][A-z_0-9.]*[A-z_0-9]|[A-z_]):')
REG_WHITESPACE_UNTIL_NEWLINE = re.compile(r'^(\s*)\n')
"""
RiscEmu (c) 2021 Anton Lydike
REG_WHITESPACE = re.compile(r'^\s*')
SPDX-License-Identifier: MIT
"""
REG_NONWHITESPACE = re.compile(r'^[^\s]*')
REG_UNTIL_NEWLINE = re.compile(r'^[^\n]*')
import re
from dataclasses import dataclass
from enum import Enum, auto
from typing import List, Iterable
REG_WHITESPACE_NO_LINEBREAK = re.compile(r'^[ \t]*')
from riscemu.decoder import RISCV_REGS
from riscemu.types.exceptions import ParseException
REG_VALID_ARGUMENT = re.compile(
r'^([+-]?(0x[0-9A-f]+|[0-9]+)|[A-z_.][A-z0-9_.]*[A-z_0-9]|[A-z_])(\(([A-z_.][A-z_0-9.]*[A-z_0-9]|[A-z_])\))?'
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]*)\)$"
)
REG_ARG_SPLIT = re.compile(r'^,[ \t]*')
def split_accepting_quotes(string, at=REG_ARG_SPLIT, quotes=('"', "'")):
pos = 0
last_piece = 0
pieces = []
in_quotes = False
if string is None:
return pieces
while pos < len(string):
match = at.match(string[pos:])
if match is not None:
if not in_quotes:
pieces.append(string[last_piece:pos])
pos += len(match.group(0))
last_piece = pos
else:
pos += len(match.group(0))
elif string[pos] in quotes:
in_quotes = not in_quotes
pos += 1
elif string[pos] in COMMENT_START and not in_quotes: # entering comment
break
else:
pos += 1
if in_quotes:
print("[Tokenizer.split] unbalanced quotes in \"{}\"!".format(string))
pieces.append(string[last_piece:pos])
return pieces
class RiscVInput:
def __init__(self, content: str):
self.content = content
self.pos = 0
self.len = len(content)
def peek(self, offset: int = 0, size: int = 1, regex: re.Pattern = None, text: str = None, regex_group: int = 0):
at = self.pos + offset
if regex:
if not isinstance(regex, re.Pattern):
print("uncompiled regex passed to peek!")
reges = re.compile(regex)
match = regex.match(self.content[at:])
if match is None:
return None
if regex_group != 0 and not match.group(0).startswith(match.group(regex_group)):
print("Cannot peek regex group that does not start at match start!")
return None
return match.group(regex_group)
if text:
if self.content[at:].startswith(text):
return self.content[at:at + len(text)]
return False
return self.content[at:at + size]
def peek_one_of(self, options: List[str]):
for text in options:
if self.peek(text=text):
return text
return False
def consume(self, size: int = 1, regex: re.Pattern = None, text: str = None, regex_group: int = 0):
at = self.pos
if regex:
if not isinstance(regex, re.Pattern):
print("uncompiled regex passed to peek!")
regex = re.compile(regex)
match = regex.match(self.content[at:])
if match is None:
print("Regex matched none at {}!".format(self.context()))
return None
if regex_group != 0 and not match.group(0).startswith(match.group(regex_group)):
print("Cannot consume regex group that does not start at match start!")
return None
self.pos += len(match.group(regex_group))
return match.group(regex_group)
if text:
if self.content[at:].startswith(text):
self.pos += len(text)
return text
return None
self.pos += size
return self.content[at:at + size]
def consume_one_of(self, options: List[str]):
for text in options:
if self.peek(text=text):
return self.consume(text=text)
return False
def seek_newline(self):
return self.consume(regex=REG_WHITESPACE_UNTIL_NEWLINE, regex_group=1)
def consume_whitespace(self, linebreak=True):
if linebreak:
return self.consume(regex=REG_WHITESPACE)
return self.consume(regex=REG_WHITESPACE_NO_LINEBREAK)
def has_next(self):
return self.pos < self.len
def context(self, size: int = 5):
"""
returns a context string:
<local input before pos>|<local input after pos>
"""
start = max(self.pos - size, 0)
end = min(self.pos + size, self.len - 1)
return self.content[start:self.pos] + '|' + self.content[self.pos:end]
REGISTER_NAMES = RISCV_REGS
class TokenType(IntEnum):
SYMBOL = 0
INSTRUCTION = 1
PSEUDO_OP = 2
class TokenType(Enum):
COMMA = auto()
ARGUMENT = auto()
PSEUDO_OP = auto()
INSTRUCTION_NAME = auto()
NEWLINE = auto()
LABEL = auto()
def __repr__(self):
return self.name
def __str__(self):
return self.name
class RiscVToken:
@dataclass(frozen=True)
class Token:
type: TokenType
value: str
def __init__(self, t_type: TokenType):
self.type = t_type
def __repr__(self):
return "{}[{}]({})".format(self.__class__.__name__, self.type, self.text())
def text(self):
"""
create text representation of instruction
"""
return "unknown"
class RiscVInstructionToken(RiscVToken):
def __init__(self, name, args):
super().__init__(TokenType.INSTRUCTION)
self.instruction = name
self.args = args
def text(self):
if len(self.args) == 0:
return self.instruction
if len(self.args) == 1:
return "{} {}".format(self.instruction, self.args[0])
if len(self.args) == 2:
return "{} {}, {}".format(self.instruction, *self.args)
return "{} {}, {}, {}".format(self.instruction, *self.args)
class RiscVSymbolToken(RiscVToken):
def __init__(self, name):
super().__init__(TokenType.SYMBOL)
self.name = name
def text(self):
return self.name
class RiscVPseudoOpToken(RiscVToken):
def __init__(self, name, args):
super().__init__(TokenType.PSEUDO_OP)
self.name = name
self.args = args
def text(self):
return "{} {}".format(self.name, self.args)
class RiscVTokenizer:
def __init__(self, input: RiscVInput):
self.input = input
self.tokens = []
def tokenize(self):
while self.input.has_next():
# remove leading whitespaces, place cursor at text start
self.input.consume_whitespace()
# check if we have a pseudo op
if self.input.peek_one_of(PSEUDO_OPS):
self.parse_pseudo_op()
# check if we have a symbol (like main:)
elif self.input.peek(regex=REG_VALID_SYMBOL_LABEL):
self.parse_symbol()
# comment
elif self.input.peek() in COMMENT_START:
self.parse_comment()
# must be instruction
elif self.input.peek_one_of(INSTRUCTIONS):
self.parse_instruction()
else:
token = self.input.peek(size=5)
print("Unknown token around {} at: {}".format(repr(token), repr(self.input.context())))
self.input.consume_whitespace()
print("After whitespace at: {}".format(repr(self.input.context())))
self.input.consume_whitespace()
def parse_pseudo_op(self):
name = self.input.consume_one_of(PSEUDO_OPS)
self.input.consume_whitespace(linebreak=False)
arg_str = self.input.consume(regex=REG_UNTIL_NEWLINE)
if not arg_str:
args = []
else:
args = split_accepting_quotes(arg_str)
self.tokens.append(RiscVPseudoOpToken(name[1:], args))
def parse_symbol(self):
name = self.input.consume(regex=REG_VALID_SYMBOL_LABEL)
self.tokens.append(RiscVSymbolToken(name[:-1]))
if not self.input.consume_whitespace():
print("[Tokenizer] symbol declaration should always be followed by whitespace (at {})!".format(
self.input.context()))
def parse_instruction(self):
ins = self.input.consume_one_of(INSTRUCTIONS)
args = []
self.input.consume_whitespace(linebreak=False)
while self.input.peek(regex=REG_VALID_ARGUMENT) and len(args) < 3:
arg = self.input.consume(regex=REG_VALID_ARGUMENT)
args.append(arg)
if self.input.peek(text=','):
self.input.consume(text=',')
self.input.consume_whitespace(linebreak=False)
else:
break
self.tokens.append(RiscVInstructionToken(ins, args))
def parse_comment(self):
# just consume the rest
self.input.consume(regex=REG_UNTIL_NEWLINE)
def __str__(self):
if self.type == TokenType.NEWLINE:
return "\\n"
if self.type == TokenType.COMMA:
return ", "
return "{}({})".format(self.type.name[0:3], self.value)
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")
if not line:
continue
parts = list(part for part in split_whitespace_respecting_quotes(line) if part)
yield from parse_line(parts)
yield NEWLINE
def parse_line(parts: List[str]) -> Iterable[Token]:
if len(parts) == 0:
return ()
first_token = parts[0]
if first_token[0] == ".":
yield Token(TokenType.PSEUDO_OP, first_token)
elif first_token[-1] == ":":
yield Token(TokenType.LABEL, first_token)
yield from parse_line(parts[1:])
return
else:
yield Token(TokenType.INSTRUCTION_NAME, first_token)
for part in parts[1:]:
if part == ",":
yield COMMA
continue
yield from parse_arg(part)
def parse_arg(arg: str) -> Iterable[Token]:
comma = arg[-1] == ","
arg = arg[:-1] if comma else arg
mem_match_resul = re.match(MEMORY_ADDRESS_PATTERN, arg)
if mem_match_resul:
register = mem_match_resul.group(2).lower()
immediate = mem_match_resul.group(1)
yield Token(TokenType.ARGUMENT, register)
yield Token(TokenType.ARGUMENT, immediate)
else:
yield Token(TokenType.ARGUMENT, arg)
if comma:
yield COMMA
def print_tokens(tokens: Iterable[Token]):
for token in tokens:
print(token, end="\n" if token == NEWLINE else "")
print("", flush=True, end="")
def split_whitespace_respecting_quotes(line: str) -> Iterable[str]:
quote = ""
part = ""
for c in line:
if c == quote:
yield part
part = ""
quote = ""
continue
if quote != "":
part += c
continue
if c in "\"'":
if part:
yield part
quote = c
part = ""
continue
if c in " \t\n":
if part:
yield part
part = ""
continue
part += c
if part:
yield part

@ -0,0 +1,2 @@
#!/usr/bin/env bash
python3 -m riscemu "$@"

@ -0,0 +1,41 @@
from typing import Dict, Any
import re
# define some base type aliases so we can keep track of absolute and relative addresses
T_RelativeAddress = int
T_AbsoluteAddress = int
# parser options are just dictionaries with arbitrary values
T_ParserOpts = Dict[str, Any]
NUMBER_SYMBOL_PATTERN = re.compile(r"^\d+[fb]$")
# base classes
from .flags import MemoryFlags
from .int32 import UInt32, Int32
from .float32 import Float32
from .instruction import Instruction
from .instruction_context import InstructionContext
from .memory_section import MemorySection
from .program import Program
from .program_loader import ProgramLoader
from .cpu import CPU
from .simple_instruction import SimpleInstruction
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,
INS_NOT_IMPLEMENTED,
)

@ -0,0 +1,56 @@
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: Optional[MemoryFlags] = None,
):
super().__init__(
name,
flags if flags is not None else MemoryFlags(False, False),
len(data),
base,
owner,
context,
)
self.data = data
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]
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"
)
if len(data[0:size]) != 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",
)

@ -0,0 +1,124 @@
from abc import ABC, abstractmethod
from typing import List, Type, Callable, Set, Dict, TYPE_CHECKING, Iterable
from ..registers import Registers
from ..config import RunConfig
from ..colors import FMT_NONE, FMT_CPU
from . import T_AbsoluteAddress, Instruction, Program, ProgramLoader
if TYPE_CHECKING:
from ..MMU import MMU
from ..instructions import InstructionSet
class CPU(ABC):
# static cpu configuration
INS_XLEN: int = 4
# housekeeping variables
regs: Registers
mmu: "MMU"
pc: T_AbsoluteAddress
cycle: int
halted: bool
# debugging context
debugger_active: bool
# instruction information
instructions: Dict[str, Callable[[Instruction], None]]
instruction_sets: Set["InstructionSet"]
# configuration
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
self.instruction_sets = set()
self.instructions = dict()
for set_class in instruction_sets:
ins_set = set_class(self)
self.instructions.update(ins_set.load())
self.instruction_sets.add(ins_set)
self.halted = False
self.cycle = 0
self.pc = 0
self.debugger_active = False
def run_instruction(self, ins: Instruction):
"""
Execute a single instruction
:param ins: The instruction to execute
"""
if ins.name in self.instructions:
self.instructions[ins.name](ins)
else:
# this should never be reached, as unknown instructions are imparseable
raise RuntimeError("Unknown instruction: {}".format(ins))
def load_program(self, program: Program):
self.mmu.load_program(program)
def __repr__(self):
"""
Returns a representation of the CPU and some of its state.
"""
return "{}(pc=0x{:08X}, cycle={}, halted={} instructions={})".format(
self.__class__.__name__,
self.pc,
self.cycle,
self.halted,
" ".join(s.name for s in self.instruction_sets),
)
@abstractmethod
def step(self, verbose: bool = False):
pass
@abstractmethod
def run(self, verbose: bool = False):
pass
def launch(self, verbose: bool = False):
entrypoint = self.mmu.find_entrypoint()
if entrypoint is None:
entrypoint = self.mmu.programs[0].entrypoint
if self.conf.verbosity > 0:
print(
FMT_CPU
+ "[CPU] Started running from {}".format(
self.mmu.translate_address(entrypoint)
)
+ FMT_NONE
)
self.pc = entrypoint
self.run(verbose)
@classmethod
@abstractmethod
def get_loaders(cls) -> Iterable[Type[ProgramLoader]]:
pass
def get_best_loader_for(self, file_name: str) -> Type[ProgramLoader]:
return max(self.get_loaders(), key=lambda ld: ld.can_parse(file_name))
@property
def sections(self):
return self.mmu.sections
@property
def programs(self):
return self.mmu.programs

@ -0,0 +1,200 @@
"""
RiscEmu (c) 2021 Anton Lydike
SPDX-License-Identifier: MIT
"""
from abc import abstractmethod
from ..colors import *
import typing
if typing.TYPE_CHECKING:
from . import Instruction
class RiscemuBaseException(BaseException):
@abstractmethod
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: 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
)
def ASSERT_EQ(a1, a2):
if 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),
)
def ASSERT_NOT_NULL(a1):
if a1 is None:
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)
)
def ASSERT_IN(a1, a2):
if a1 not in a2:
raise ParseException(
"ASSERTION_FAILED: Expected {} to not be in {}".format(a1, a2), (a1, a2)
)
class LinkerException(RiscemuBaseException):
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
)
# MMU Exceptions
class MemoryAccessException(RiscemuBaseException):
def __init__(self, msg: str, addr, size, op):
super(MemoryAccessException, self).__init__()
self.msg = msg
self.addr = addr
self.size = size
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
)
class OutOfMemoryException(RiscemuBaseException):
def __init__(self, action):
self.action = action
def message(self):
return (
FMT_MEM
+ "{}(Ran out of memory during {})".format(
self.__class__.__name__, self.action
)
+ FMT_NONE
)
class InvalidAllocationException(RiscemuBaseException):
def __init__(self, msg, name, size, flags):
self.msg = msg
self.name = name
self.size = size
self.flags = flags
def message(self):
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):
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
)
class InvalidRegisterException(RiscemuBaseException):
def __init__(self, reg):
self.reg = reg
def message(self):
return (
FMT_CPU
+ "{}(Invalid register {})".format(self.__class__.__name__, self.reg)
+ FMT_NONE
)
class InvalidSyscallException(RiscemuBaseException):
def __init__(self, scall):
self.scall = scall
def message(self):
return (
FMT_SYSCALL
+ "{}(Invalid syscall: {})".format(self.__class__.__name__, self.scall)
+ FMT_NONE
)
def INS_NOT_IMPLEMENTED(ins):
raise UnimplementedInstruction(ins)
class NumberFormatException(RiscemuBaseException):
def __init__(self, msg):
super().__init__()
self.msg = msg
def message(self):
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) -> str:
return ""

@ -0,0 +1,12 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class MemoryFlags:
read_only: bool
executable: bool
def __repr__(self):
return "r{}{}".format(
"-" if self.read_only else "w", "x" if self.executable else "-"
)

@ -0,0 +1,198 @@
import struct
from ctypes import c_float
from typing import Union, Any
bytes_t = bytes
class Float32:
__slots__ = ("_val",)
_val: c_float
@property
def value(self) -> float:
"""
The value represented by this float
"""
return self._val.value
@property
def bytes(self) -> bytes:
"""
The values bit representation (as a bytes object)
"""
return struct.pack("<f", self.value)
@property
def bits(self) -> int:
"""
The values bit representation as an int (for easy bit manipulation)
"""
return int.from_bytes(self.bytes, byteorder="little")
@classmethod
def from_bytes(cls, val: Union[int, bytes_t, bytearray]):
if isinstance(val, int):
val = struct.unpack("!f", struct.pack("!I", val))[0]
return Float32(val)
def __init__(
self, val: Union[float, c_float, "Float32", bytes_t, bytearray, int] = 0
):
if isinstance(val, (float, int)):
self._val = c_float(val)
elif isinstance(val, c_float):
self._val = c_float(val.value)
elif isinstance(val, (bytes, bytearray)):
self._val = c_float(struct.unpack("<f", val)[0])
elif isinstance(val, Float32):
self._val = val._val
else:
raise ValueError(
"Unsupported value passed to Float32: {} ({})".format(
repr(val), type(val)
)
)
def __add__(self, other: Union["Float32", float]):
if isinstance(other, Float32):
other = other.value
return self.__class__(self.value + other)
def __sub__(self, other: Union["Float32", float]):
if isinstance(other, Float32):
other = other.value
return self.__class__(self.value - other)
def __mul__(self, other: Union["Float32", float]):
if isinstance(other, Float32):
other = other.value
return self.__class__(self.value * other)
def __truediv__(self, other: Any):
return self // other
def __floordiv__(self, other: Any):
if isinstance(other, Float32):
other = other.value
return self.__class__(self.value // other)
def __mod__(self, other: Union["Float32", float]):
if isinstance(other, Float32):
other = other.value
return self.__class__(self.value % other)
def __eq__(self, other: object) -> bool:
if isinstance(other, (float, int)):
return self.value == other
elif isinstance(other, Float32):
return self.value == other.value
return False
def __neg__(self):
return self.__class__(-self.value)
def __abs__(self):
return self.__class__(abs(self.value))
def __bytes__(self) -> bytes_t:
return self.bytes
def __repr__(self):
return "{}({})".format(self.__class__.__name__, self.value)
def __str__(self):
return str(self.value)
def __format__(self, format_spec: str):
return self.value.__format__(format_spec)
def __hash__(self):
return hash(self.value)
def __gt__(self, other: Any):
if isinstance(other, Float32):
other = other.value
return self.value > other
def __lt__(self, other: Any):
if isinstance(other, Float32):
other = other.value
return self.value < other
def __le__(self, other: Any):
if isinstance(other, Float32):
other = other.value
return self.value <= other
def __ge__(self, other: Any):
if isinstance(other, Float32):
other = other.value
return self.value >= other
def __bool__(self):
return bool(self.value)
def __pow__(self, power, modulo=None):
if modulo is not None:
raise ValueError("Float32 pow with modulo unsupported")
return self.__class__(self.value**power)
# right handed binary operators
def __radd__(self, other: Any):
return self + other
def __rsub__(self, other: Any):
return self.__class__(other) - self
def __rmul__(self, other: Any):
return self * other
def __rtruediv__(self, other: Any):
return self.__class__(other) // self
def __rfloordiv__(self, other: Any):
return self.__class__(other) // self
def __rmod__(self, other: Any):
return self.__class__(other) % self
def __rand__(self, other: Any):
return self.__class__(other) & self
def __ror__(self, other: Any):
return self.__class__(other) | self
def __rxor__(self, other: Any):
return self.__class__(other) ^ self
# bytewise operators:
def __and__(self, other: Union["Float32", float, int]):
if isinstance(other, float):
other = Float32(other)
if isinstance(other, Float32):
other = other.bits
return self.from_bytes(self.bits & other)
def __or__(self, other: Union["Float32", float]):
if isinstance(other, float):
other = Float32(other)
if isinstance(other, Float32):
other = other.bits
return self.from_bytes(self.bits | other)
def __xor__(self, other: Union["Float32", float]):
if isinstance(other, float):
other = Float32(other)
if isinstance(other, Float32):
other = other.bits
return self.from_bytes(self.bits ^ other)
def __lshift__(self, other: int):
return self.from_bytes(self.bits << other)
def __rshift__(self, other: int):
return self.from_bytes(self.bits >> other)

@ -0,0 +1,31 @@
from abc import ABC, abstractmethod
from typing import Tuple
class Instruction(ABC):
name: str
args: tuple
@abstractmethod
def get_imm(self, num: int) -> int:
"""
parse and get immediate argument
"""
pass
@abstractmethod
def get_imm_reg(self, num: int) -> Tuple[int, str]:
"""
parse and get an argument imm(reg)
"""
pass
@abstractmethod
def get_reg(self, num: int) -> str:
"""
parse and get an register argument
"""
pass
def __repr__(self):
return "{} {}".format(self.name, ", ".join(self.args))

@ -0,0 +1,64 @@
from collections import defaultdict
from typing import Dict, List, Optional
from .exceptions import ParseException
from ..types import T_AbsoluteAddress, T_RelativeAddress, NUMBER_SYMBOL_PATTERN
class InstructionContext:
base_address: T_AbsoluteAddress
"""
The address where the instruction block is placed
"""
labels: Dict[str, T_RelativeAddress]
"""
This dictionary maps all labels to their relative position of the instruction block
"""
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
"""
global_symbol_dict: Dict[str, T_AbsoluteAddress]
"""
A reference to the MMU's global symbol dictionary for access to global symbols
"""
def __init__(self):
self.labels = dict()
self.numbered_labels = defaultdict(list)
self.base_address = 0
self.global_symbol_dict = dict()
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
)
)
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,
)
else:
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)

@ -0,0 +1,54 @@
from typing import List
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,
):
self.name = name
self.base = base
self.context = context
self.size = len(instructions) * 4
self.flags = MemoryFlags(True, True)
self.instructions = instructions
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",
)
def write(self, offset: T_RelativeAddress, size: int, data: bytearray):
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",
)
return self.instructions[offset // 4]

@ -0,0 +1,285 @@
from typing import Any, Union
from ctypes import c_int32, c_uint32
class Int32:
"""
This class implements 32bit signed integers (see :class:`UInt32` for unsigned integers)
It implements basically all mathematical dunder magic methods (__add__, __sub__, etc.)
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",)
def __init__(
self, val: Union[int, c_int32, c_uint32, "Int32", bytes, bytearray, bool] = 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)
)
elif isinstance(val, self.__class__._type):
self._val = val
elif isinstance(val, (c_uint32, c_int32, Int32)):
self._val = self.__class__._type(val.value)
elif isinstance(val, (int, bool)):
self._val = self.__class__._type(val)
else:
raise RuntimeError(
"Unknonw {} input type: {} ({})".format(
self.__class__.__name__, type(val), val
)
)
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]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value - other)
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: Any):
return 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]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value % other)
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]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self._val.value | other)
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]):
if isinstance(other, Int32):
other = other.value
return self.__class__(self.value << other)
def __rshift__(self, other: Union["Int32", int]):
if isinstance(other, Int32):
other = other.value
return self.__class__(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)
def __abs__(self):
return self.__class__(abs(self.value))
def __bytes__(self):
return self.to_bytes(4)
def __repr__(self):
return "{}({})".format(self.__class__.__name__, self.value)
def __str__(self):
return str(self.value)
def __format__(self, format_spec: str):
return self.value.__format__(format_spec)
def __hash__(self):
return hash(self.value)
def __gt__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value > other
def __lt__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value < other
def __le__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value <= other
def __ge__(self, other: Any):
if isinstance(other, Int32):
other = other.value
return self.value >= other
def __bool__(self):
return bool(self.value)
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: Any):
return self + other
def __rsub__(self, other: Any):
return self.__class__(other) - self
def __rmul__(self, other: Any):
return self * other
def __rtruediv__(self, other: Any):
return self.__class__(other) // self
def __rfloordiv__(self, other: Any):
return self.__class__(other) // self
def __rmod__(self, other: Any):
return self.__class__(other) % self
def __rand__(self, other: Any):
return self.__class__(other) & self
def __ror__(self, other: Any):
return self.__class__(other) | self
def __rxor__(self, other: Any):
return self.__class__(other) ^ self
@property
def value(self) -> int:
"""
The value represented by this Integer
:return:
"""
return self._val.value
def unsigned(self) -> "UInt32":
"""
Convert to an unsigned representation. See :class:Uint32
:return:
"""
return UInt32(self)
def to_bytes(self, bytes: int = 4) -> bytearray:
"""
Convert to a bytearray of length :param:bytes
: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]
def signed(self) -> "Int32":
"""
Convert to a signed representation. See :class:Int32
:return:
"""
if self.__class__ == Int32:
return self
return Int32(self)
@property
def unsigned_value(self):
"""
Return the value interpreted as an unsigned integer
:return:
"""
return c_uint32(self.value).value
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.
This is equivalent to (self.value % 0x100000000) >> ammount
:param ammount: Number of positions to shift
:return: A new Int32 object representing the shifted value (keeps the signed-ness of the source)
"""
if isinstance(ammount, Int32):
ammount = ammount.value
return self.__class__((self.value % 0x100000000) >> ammount)
def __int__(self):
return self.value
def __hex__(self):
return hex(self.value)
@classmethod
def sign_extend(cls, data: Union[bytes, bytearray, int], bits: int):
"""
Create an instance of Int32 by sign extending :param:bits bits from :param:data
to 32 bits
:param data: The source data
:param bits: The number of bits in the source data
:return: An instance of Int32, holding the sign-extended value
"""
if isinstance(data, (bytes, bytearray)):
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)
return cls(data)
class UInt32(Int32):
"""
An unsigned version of :class:Int32.
"""
_type = c_uint32
def unsigned(self) -> "UInt32":
"""
Return a new instance representing the same bytes, but signed
:return:
"""
return self
@property
def unsigned_value(self) -> int:
return self._val.value
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)
"""
if isinstance(ammount, Int32):
ammount = ammount.value
return UInt32(self.value >> ammount)

@ -0,0 +1,173 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
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,
)
@dataclass
class MemorySection(ABC):
name: str
flags: MemoryFlags
size: int
base: T_AbsoluteAddress
owner: str
context: InstructionContext
@property
def end(self):
return self.base + self.size
@abstractmethod
def read(self, offset: T_RelativeAddress, size: int) -> bytearray:
pass
@abstractmethod
def write(self, offset: T_RelativeAddress, size: int, data: bytearray):
pass
@abstractmethod
def read_ins(self, offset: T_RelativeAddress) -> Instruction:
pass
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.
:param start: The address to start at
:param end: The end of the printed space
:param fmt: either hex, int, char or asm
:param bytes_per_row: the number of bytes displayed per row
:param rows: the number of rows displayed
:param group: Group this many bytes into one when displaying
:param highlight: Highlight the group containing this address
:return:
"""
if isinstance(start, Int32):
start = start.value
if isinstance(end, Int32):
end = end.value
if fmt is None:
if self.flags.executable and self.flags.read_only:
bytes_per_row = 4
fmt = "asm"
else:
fmt = "hex"
if fmt == "char":
if bytes_per_row is None:
bytes_per_row = 4
if group is None:
group = 1
if group is None:
group = 4
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 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
)
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
)
)
else:
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
)
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,
)
)
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,
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(
self.__class__.__name__,
self.name,
self.base,
self.size,
self.flags,
self.owner,
)

@ -0,0 +1,117 @@
from typing import List, Optional, Set
from ..colors import FMT_RED, FMT_BOLD, FMT_NONE, FMT_MEM
from ..helpers import get_section_base_name
from . import InstructionContext, T_AbsoluteAddress, MemorySection
class Program:
"""
This represents a collection of sections which together form an executable program
When you want to create a program which can be located anywhere in memory, set base to None,
this signals the other components, that this is relocatable. Set the base of each section to
the offset in the program, and everything will be taken care of for you.
"""
name: str
context: InstructionContext
global_labels: Set[str]
relative_labels: Set[str]
sections: List[MemorySection]
base: Optional[T_AbsoluteAddress]
is_loaded: bool
@property
def size(self):
if len(self.sections) == 0:
return 0
if self.base is None:
return self.sections[-1].base + self.sections[-1].size
return (self.sections[-1].base - self.base) + self.sections[-1].size
def __init__(self, name: str, base: Optional[int] = None):
self.name = name
self.context = InstructionContext()
self.sections = []
self.global_labels = set()
self.relative_labels = set()
self.base = base
self.is_loaded = False
def add_section(self, sec: MemorySection):
# print a warning when a section is located before the programs base
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(
sec, self.name, self.base
)
+ FMT_NONE
)
self.sections.append(sec)
# keep section list ordered
self.sections.sort(key=lambda section: section.base)
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,
)
@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")
for sec in self.sections:
if get_section_base_name(sec.name) == ".text" and sec.flags.executable:
return sec.base
def loaded_trigger(self, at_addr: T_AbsoluteAddress):
"""
This trigger is called when the binary is loaded and its final address in memory is determined
This will do a small sanity check to prevent programs loading twice, or at addresses they don't
expect to be loaded.
Then it will finalize all relative symbols defined in it to point to the correct addresses.
:param at_addr: the address where the program will be located
"""
if self.is_loaded:
if at_addr != self.base:
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
)
# check if we are relocating
if self.base != at_addr:
offset = at_addr if self.base is None else at_addr - self.base
# move all sections by the offset
for sec in self.sections:
sec.base += offset
# move all relative symbols by the offset
for name in self.relative_labels:
self.context.labels[name] += offset
self.base = at_addr
self.context.base_address = at_addr

@ -0,0 +1,58 @@
import os
from abc import abstractmethod, ABC
from typing import Union, Iterator, List
from . import T_ParserOpts, Program
class ProgramLoader(ABC):
"""
A program loader is always specific to a given source file. It is a place to store all state
concerning the parsing and loading of that specific source file, including options.
"""
def __init__(self, source_path: str, options: T_ParserOpts):
self.source_path = source_path
self.options = options
self.filename = os.path.split(self.source_path)[-1]
@classmethod
@abstractmethod
def can_parse(cls, source_path: str) -> float:
"""
Return confidence that the file located at source_path
should be parsed and loaded by this loader
:param source_path: the path of the source file
:return: the confidence that this file belongs to this parser
"""
pass
@classmethod
@abstractmethod
def get_options(cls, argv: List[str]) -> [List[str], T_ParserOpts]:
"""
parse command line args into an options dictionary
:param argv: the command line args list
:return: all remaining command line args and the parser options object
"""
pass
@classmethod
def instantiate(cls, source_path: str, options: T_ParserOpts) -> "ProgramLoader":
"""
Instantiate a loader for the given source file with the required arguments
:param source_path: the path to the source file
:param options: the parsed options (guaranteed to come from this classes get_options method.
:return: An instance of a ProgramLoader for the spcified source
"""
return cls(source_path, options)
@abstractmethod
def parse(self) -> Union[Program, Iterator[Program]]:
"""
:return:
"""
pass

@ -0,0 +1,30 @@
from typing import Union, Tuple
from . import Instruction, T_RelativeAddress, InstructionContext
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,
):
self.context = context
self.name = name
self.args = args
self.addr = addr
def get_imm(self, num: int) -> int:
resolved_label = self.context.resolve_label(self.args[num], self.addr)
if resolved_label is None:
return parse_numeric_argument(self.args[num])
return resolved_label
def get_imm_reg(self, num: int) -> Tuple[int, str]:
return self.get_imm(num + 1), self.get_reg(num)
def get_reg(self, num: int) -> str:
return self.args[num]

@ -1,30 +0,0 @@
from riscemu import *
if __name__ == '__main__':
example_progr = """
.data 0x200
fibs: .space 56
.text
main:
add s1, zero, 0 # storage index
add s2, zero, 56 # last storage index
add t0, zero, 1 # t0 = F_{i}
add t1, zero, 1 # t1 = F_{i+1}
loop:
sw t0, fibs(s1) # save
add t2, t1, t0 # t2 = F_{i+2}
add t0, t1, 0 # t0 = t1
add t1, t2, 0 # t1 = t2
add 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, 0
add a7, zero, 93
scall # exit with code 0
"""
tk = RiscVTokenizer(RiscVInput(example_progr))
tk.tokenize()
for token in tk.tokens:
print(token)

@ -0,0 +1,44 @@
import setuptools
from glob import glob
import riscemu
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setuptools.setup(
name="riscemu",
version=riscemu.__version__,
author=riscemu.__author__,
author_email="pip@antonlydike.de",
description="RISC-V userspace and machine mode emulator",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/antonlydike/riscemu",
project_urls={
"Bug Tracker": "https://github.com/AntonLydike/riscemu/issues",
},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
package_dir={"": "."},
packages=[
"riscemu",
"riscemu.decoder",
"riscemu.instructions",
"riscemu.IO",
"riscemu.priv",
"riscemu.types",
],
package_data={
"riscemu": ["libc/*.s", "py.typed"],
},
data_files=[
("libc", glob("libc/*.s")),
],
scripts=["riscemu/tools/riscemu"],
python_requires=">=3.8",
install_requires=["pyelftools~=0.27"],
)

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

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

@ -0,0 +1,2 @@
sphinx
recommonmark

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save