Course:CPSC312-2023-RiscV-Emulator

From UBC Wiki

RISC-V Emulator

Authors: Em Chu, Adam Mitha

What is the problem?

We will implement an emulator for the RISC-V instruction set. To demonstrate a successful implementation, we will compile a simple C program to RISC-V assembly and execute it on our emulator and compare it to the output of some reference implementation (e.g. QEMU).

Haskell (and functional languages more broadly) have a reputation for being particularly well suited for interpreter/emulator/compiler implementation. This project will allow us to investigate this claim and discover what exactly it is about functional languages that makes them well suited for this task.

We intend to support the base unprivileged RISC-V instruction set as defined in the spec.

What is the something extra?

We might include a tool for benchmarking our emulator. This will allow us to compare the efficiency of our implementation against others in more efficient languages (e.g. C). This is subject to change! We may support extensions or the privileged ISA if time permits.

What did we learn from doing this?

Our implementation features minimal IO components with a fully functional machine state; Registers are a 32-long immutable vector, and memory is a map from memory addresses modulo 4 to 4-byte data words. I won't pretend these decisions were the result of careful deliberation (instead of not knowing how IO worked when the modules were designed), but the resulting implementation actually worked to benefit us in several ways.

Memory implemented using Data.Map allowed for O(logn) insertions, which wouldn't be true of a functional Data.Vector: O(n) for functional update. As well, functional modification of a map does not copy the whole map—the new structure can share data with the old structure, and since we are working functionally, the old structure is guaranteed to stay the same. This data structure choice allowed us to efficiently make a list of memory states, one per step in execution, and use them for debugging purposes. The same thing couldn't be done imperatively with a mutable array/vector implementation.

We learned about the components of an ISA and what capabilities are necessary before an operating system can run on it. Writing Haskell is probably the only way to learn about its quirks, which we did.

We have found that functional programming is well-suited to the task of making a small emulator. This was especially true since our focus was not on performance, but on 1) making sure the emulator had the correct behaviour compared to the RISC-V specification and 2) tracing register/memory values over time.

Code

https://github.com/mlechu/riscv-hs