RvRun is an emulator for RISC-V 64bit Linux programs. It is meant to run under Linux and can emulate over 100 instructions. Tested under Gentoo, Archlinux, and Ubuntu 25.10
RvRun is an emulator for RISC-V 64bit Linux programs. It is meant to run under Linux and can emulate over 100 instructions. Tested under Gentoo, Archlinux, and Ubuntu 25.10
I built an emulator for the RISC-V unprivileged architecture, it emulates over a hundred instructions, 4 different extensions (Rv64IMF + Zicsr), and 6 different Linux system calls.
RISC-V is an open architecture developed by UC Berkley and other universities, “open” here does not mean open source, as there is no source, but it means anyone is allowed to create hardware for it without paying any license fees. Development of the ISA is also public. These characteristics resonate a lot with what I personally thought of flavortown, so I figured it would be an interesting project to do and ship.
RvRun was written in accordance to the RISC-V specification 1, as a matter of fact, I’d say I spent more time reading it than writing code. There are almost 700 pages, after all. I also tried following IEE-754-2008 2 whenever I could, but I must admit that was almost impossible, since it’s not exactly a free document.
Due to the very nature of RvRun, it cannot be used outside of Linux. For that reason, I added several asciinema recordings of it working. All of the .cast files are in the asciinema/ directory in the repository, but you can also watch them on the web. The URL for each example program is in the README, under the “Asciinema recordings” section.
RvRun does not have any dependencies, I implemented everything from scratch in C. Example programs were written in assembly.
Implementing floating point instructions was quite hard (as you might guess by my devlogs). It took Berkley to write a library to handle it, so doing everything myself wasn’t exactly the smartest of the choices. Regardless, I am very proud of my somewhat successful implementation.
The demo includes a pre-compiled binary for x86-64 Linux, and six different RISC-V 64bit binaries that can be emulated by RvRun. Simply download the RvRun-v1.1.zip file and unzip it. If you’d prefer, compilation is also supported with both clang and gcc, instructions on how to do that are in the README.
You can find the pre-compiled example programs in the examples/ directory, the repository only contains the code for them, but the release includes the ELF files too. Simply run rvrun examples/<PROGRAM>.elf to test it, instructions on how each example can be used are found in examples/README.md
If you want to write your own assembly programs and emulate them under RISC-V, use the riscv64-elf-linux-gnu-* toolchain for compilation. Keep in mind you’ll need to add the -march=rv64imf (or your assembler’s equivalent) argument to your assembling command, because even if you only use the instructions present in these extensions, there are other extensions (e.g. the C one) that can be added transparently to the programmer, and that are not supported by RvRun. You will also need to keep your programs statically linked, and without a libc.
on commit 2607fc6:
example/sin.asm program to serve as an example during voting.include/riscv.h:insn2rs2()
EBREAK instruction also show floating point registersRvRun has now almost ten thousand lines of SLOC, and even more lines of code. It’s also almost in the hundredth commit. It can support over a hundred instructions, and only somewhat lacks in system call support, although the support is still enough to do both normal and file I/O, and could easily be expanded for things such as internet connections, allowing entire servers to be emulated.
I am very happy with the results of this project, testings in Gentoo, Arch, and Ubuntu 25.10 went amazing, no bugs were found. I wish I had used a softfloat library while implementing the F extension, but that’s something I can work on the future, as the minor uncompliances with IEE-754 that RvRun has are completely irrelevant for any program worth emulating.
Not just that, but I managed to support multiple compilers while using compiler-dependent features for optimizations (e.g. __builtin_expect()), and the build system satisfies me a lot.
The v1.1 version has also been released as an unstable release, mostly due to the minor uncompliances previously mentioned, as every other feature seems to be reasonably stable to me, I have updated the demo to reflect on it, and will now fill in the shipping application.
The attachment shows RvRun emulating examples/sin.asm, a program written in pure assembly that is able to calculate the sine of radians between 3.14 and -3.14 (numbers with bigger absolute values could also be calculated if it weren’t for the level of precision 32bit floats offer).
Log in to leave a comment
on commit 63b065a:
This was a hell lot of work. I now realize I should have just used a softfloat library, and I’ll probably rewrite all of src/rv_f.c and include/rv_f.h (about 1400 lines) in the future. I imagine I did not follow the IEE-754 standard several times, but I don’t understand floating point math enough to see where. It is surprisingly difficult to handle all the different NaNs, subnormals, rounding modes, and other such things. Regardless, I’d like to ship my project one last time before flavortown ends, and the details I worry about are irrelevant to almost every application that RvRun can currently emulate.
I will now add example programs, update the README, and do a v1.1 unstable release, that will be my next (and unfortunately last) ship. I’m thinking of good example programs to add, and mathmetical functions like sine and cosine are currently my best candidates. I’d like to ship this project today, so it’s going to be a lot of work.
The output is RvRun running a program with several floating point instructions (showing it can fetch and decode all of them). Since dbg_log()’s were removed from insn_* instructions, there is no real output.
Log in to leave a comment
on commit 929deb5:
Dealing with NaNs and other floating point details (many of which fell irrelevant) was really annoying. I am afraid I’m not complying to either the RISC-V specification or the IEE-754 one, but all the code seems fine. I’m planning on testing everything with the riscv test-suit, but this will probably be done after flavortown, as it would take too much time.
I now need to add fused multiply-add instructions, compare instructions, convert/move instructions, and classification instructions. They might seem a lot, but I am imagining that they will be actually easier to implement (and less boring too), except for the fused instructions, these ones will be hell.
Below is a screenshot of RvRun calculating divisions, multiplications, square roots, and other such things (The small imprecisions are expected). The dbg logs are not in the code, and were added just for this devlog
Log in to leave a comment
on commit 92433ef:
While adding support for FCSR, I had to figure out a way to make so that floating point operations didn’t have to be made bit-by-bit, for that, I used the features from fenv.h. But there’s two problems with that: firstly, there’s no RMM (Round-to-Nearest, Ties-to-Max) support in fenv.h, nor in x86-64 CPUS at all. Secondly, x86-64 allows for some weird optimizations that treat subnormal values as zero (DAZ (Denormals-Are-Zero) and FTZ (Flush-To-Zero)). Basically, this means when the CPU finds a very very small number, it just pretends it’s zero, so it can run faster. It all sounds cool, until you realize that this violates IEE-754-2008, which I have to follow. I couldn’t find documentation that said if these were already turned on or off at the start of a program, so I just added code to turn it off (which was a pain to figure out). I never want to deal with floats again, but that’s too bad, because the next extension that will be added is the single-precision floating-point arithmetic one (aka “F”).
I will now start work on the “actual” instructions for the F extension. They’re quite a lot, and testing will be somewhat difficult, but I expect to have finished it by Monday.
As always, there’s simply not a lot of output to show. The Zicsr extension simply means emulated programs can now access CSR registers, and while technically challenging, it doesn’t change graphical output at all.
The screenshot shows RvRun running a program that writes and reads FCSR, firstly there’s the assembly code for the program (not currently in github), then, it’s output, and finally, RvRun’s logs
Log in to leave a comment
on commit ea0568d (not in commit order):
Will now start work on the F extension, before shipping it again. Might add more system calls too.
The Zicsr extension is a pre-req for the F one, so I’ll work on that before too.
Log in to leave a comment
🔥 AVD marked your project as well cooked! As a prize for your nicely cooked project, look out for a bonus prize in the mail :)
on commit 39cd6a0:
time command, should be of bigger impact if emulating big enough programs.I’ll be adding example programs for the M extensions, most likely a fibbonaci computation (multiplication was needed for displaying the number), and an algorithm to computate GCD (greates common denominator) using Euclidian’s algorithm. After that, work on the F extension should be started.
There really isn’t any new output to be shown, so I decided to show how the v1.0 release could not be compiled by clang (minor change in the Makefile so clang is actually used), and how the compilation is sucessful in the current commit
Log in to leave a comment
on commit c6f8e8f:
Therefore, I will probably only do some small refactoring/optimizations before getting work started on the F and D extensions. I’m curious on how I can implement the floating point environment instructions, and if simply affecting the emulator’s environment is enough (after all, that’s considerably faster than saving/fetching bit masks and doing floating point operations by hand). Regardless, I probably won’t be coding in the next 4 days for personal reasons, so I’ll have lots of time to think about a good design.
It is also worth mentioning that these instructions were not included in my first ship (which while currently pending, was done at the 15th devlog). The binary on the v1.0 release only supports the base ISA (Rv64I). I’ll be making a second ship later this month, perhaps in one or two weeks, that supports all of these instructions, and maybe a few more system calls.
Log in to leave a comment
on commit 493c809:
I had some problems with shipping, but it should hopefully be fixed by now too! The people on slack are really helpful.
Log in to leave a comment
I built a RISC-V emulator for 64bit Linux binaries. It attempts to fully comply to chapters 1, 2, and 4 of the unprivileged specification[1], aka the base ISA. It also implements a few Linux system calls, these were derived from their respective manual pages.
RvRun emerged from a time where I was learning RISC-V assembly, perhaps to no ones surprise, I’d say “run, run, run!” quite often, staring at the menancing screen, waiting for it to reply: “Segmentation fault”, and crash my program.
After some time, I became quite comfortable with it, perhaps too comfortable, and decided I should read the specification, and make an emulator that complied to it. I shouldn’t. When I first opened the 696 pages document, it was like a bucket of cold water had hit me, some chapters have entire sub-sub-sub-sections! regardless, through-out 2 months, RvRun was made, with a few on’s and off’s, but it is, finally, ready!
Most of the time spent on this project was probably reading, not coding! I read and re-read many many sections of the specification multiple times, I looked through the Linux Kernel source code, read multiple man pages (specially elf(5)), read ABI specifications, etc, etc. It was tho, surprisingly, and I am very proud of having finished it.
RvRun uses no libraries other than libc (available on all systems), everything was done from scratch.
[1] https://docs.riscv.org/reference/isa/_attachments/riscv-unprivileged.pdf
on commit 0616547:
The project has now had it’s first release (on github), and I’m going to ship it here, on flavortown, too!
Log in to leave a comment
on commit 5bbcc22:
I am now taking a look at packaging, I added install and uninstall targets to my Makefile, and will rewrite the README to better reflect where the project now is. I’m going to put a statically compiled binary on the github releases page, to make sure no problems can happen, it is about 15 times bigger than the dynamically compiled one, but still under 1Mb (there are no dependencies other than libc, after all), so no problems.
I also realized that I need a banner, so I’m thinking of making a pixelart for a logo.
Log in to leave a comment
on commit c1bf362:
I’m also thinking of what programs I should write as examples, they’ll probably be recreations of classical UNIX utilities, like cat(1), ls(1), echo(1), etc.
Log in to leave a comment
I had some problems with hacktime’s time tracking, but am finally back!
on commit fbcaefb:
Log in to leave a comment
on commit 4f3ebba:
Added a README, should have done this some time ago, but only now the project is starting to get usable. I’ll finish implementing the base ISA, perhaps add some system calls, and then release the version one.
Log in to leave a comment
on commit 1883f67:
RvRun can now interpret arguments, this might have been a bit late, but there are actual things to be set as arguments now. The currently added ones are:
Log in to leave a comment
on commit74aaa7b :
I Added all the W-type instructions, some other ones I had forgotten, and added support for file descriptor related system calls, like openat(), close(), read(), and write(). There is no open() system call on RISC-V, it is only a libc wrapper around open() (no __NR_open macro in include/uapi/asm-generic/unitsd.h under the linux kernel source code).
I am working on passing options to the emulator, writing a README, and making some examples, I’ll probably do that over the weekend. After that, there shouldn’t be that much work other than simple repetitive stuff (aka adding more instructions and more system calls)
I forgot to do the devlog earlier, so about one hour of the time being logged was actually spent adding flag support, haha.
Log in to leave a comment
on commit 1eb5f81:
I implemented ECALL, and can now emulate both exit(2) and exit_group(2) (they are both the same, as I don’t have thread support yet), with now a proper way to decide when a process exits and when it doesn’t, I implemented an actual Von Neumann cycle too.
I am going to iterate back between finishing the implementation of all Rv64I instructions, and supporting more system calls (most likely, file descriptor related ones). My objective is to support all the instructions from Rv64IMFD, and a handful of system calls, in about 2 weeks and a half.
Log in to leave a comment
on commit b264055:
I am finally back! I did some refactoring on src/proc.c yesterday, and implemented the I-type from Rv32I instructions today, I am ignoring the W variations that Rv64I adds for simplicity, I’d like to cover the most important instructions first.
I will probably start basic work on ecall next, so that I can do a real von neumann execution cycle by having an exit system call, after that, I’ll go on with the U-type, B-type, and J-type instructions, before going back to the things I ignored, and continuing with all the system calls.
I will probably change the license to GPLv2, too, and perhaps make some artwork.
Log in to leave a comment
on commit 6423204:
I can now execute all R instructions from Rv64I, except for the “*W” ones. I am downloading and building https://github.com/riscv/riscv-opcodes.git at compile time, setting this in the Makefile was a bit of a pain but I eventually got it. I will now work on some logging/debugging functions, so that I can test the instructions I add properly, and then work on the Register-Immediate instructions.
I’m not sure if I’m going to “speedrun” a hello-world, or try to support most instructions from the base ISA first, but they should both lead to the same outcome.
All of this is being done while I read the specification, and while it can get quite tedious, I feel way better in RISC-V now, I might even write my next project in raw assembly.
Log in to leave a comment
on commit 9611fd6:
I can now load and store values into memory, it respects the permissions set when loading the ELF binary, and fail gracefully both when trying to access out of bound memory or when doing invalid operations (aka writing to a read-only address). Unaligned memory access is also supported, as RISC-V allows that, even if the host machine does not support it. I decided to use C11 to allow for the _Generic macro, and enabled the _GNU_SOURCE macro to use some extensions (they weren’t needed, but I’ll be using other ones further down the road anyways).
I will now implement instruction fetching, and later the decoding
Log in to leave a comment
on commit 0e626b8:
I ended up spending the whole day thinking about how to support both 32bit and 64bit applications, I went over C11’s _Generic, void pointers, wider types + type casts, etc, but ended up realizing the best choice was to only support one. Some time was thrown away, but at the very least, I think I made a really good and important design decision, one I could not go back later.
Log in to leave a comment
on commit d7c2c71:
Finished implementing the loading of ELF segments, it felt a bit tricky at first but it turns out the format is beautifully designed.
I am using a linked list to represent the segments, it saves the RISC-V addresses it maps, a pointer to the memory that represents it, and the flags (Read/Write/Exec). I might change this design by using mmap, but I’m not sure how I would mark the address space as executable or not (since it should be executable by the emulator, not by the host OS itself). But it seems to work now, therefore I’ll leave this to a future refactor when I have a better idea of how the project as a whole should be designed.
Log in to leave a comment
Started working on my project, created the directory structure, added a license, and a Makefile. Will add README further down the road.
Had some problems making GNU Make automatically deduce prerequisites, but it eventually worked.
Log in to leave a comment