An operating system for RISC-V architecture based on xv6. The original project is written C and this is my attempt at translating it into Rust.
Since kernel development is as low-level as it gets, this project makes me realize how hard it is to re-write everything that usually comes with the standard library.
no_std
Without Rust's standard library (and consequently the core library), even the simplest things we take for granted are much harder to do.
First of all, there is no heap and no runtime memory allocation. I had to bring my allocator and I picked the buddy_alloc crate. This was the only time I opt-in to use a dependency. At this stage, there was nothing stable on the kernel, and trying to implement an allocator from scratch would be a huge undertaking for me.
To use the allocator, I still needed a way to guarantee correct access to the shared heap in this multi-core environment. This required me to implement a mutex and just like the original project, I used a spin lock to do it. At this point, the standard library implementation of mutex was a great guide. Leveraging the type system of Rust, implementing a mutex is quite straightforward. If you are unsure how mutex and mutex guard co-operate, I highly recommend checking that out.
interoperability with c
While everything is written in Rust (with some assembly exceptions), a lot of the stuff has to be C-compatible. There are two main reasons for this:
- rust does not have a stable ABI (see #600),
- rust does not guarantee a specific memory ordering for composite data types like structs (see repr(Rust)).
No stable ABI means the Rust compiler does not guarantee a concrete calling convention for
functions. This is not important if we stay in Rust boundaries at all times but this is not the case
here. At the very least, we must transition from assembly to our main
function when the machine
starts (see start.rs). For this
reason, we make sure the main
function is compiled using the extern "C"
keyword:
#[export_name = "main"]
extern "C" fn main() -> ! {
This dictates the compiler to use the C calling conventions when creating the foreign function interface (FFI).
Another thing we have to consider is memory layout and alignment for our structs. This is most
apparent when implementing the virtual memory code. We have to guarantee that each memory page is
exactly 4 kilobytes long and laid out with no reordering and/or padding. For this reason, most of
the structs are marked with repr
macros:
#[repr(C, align(4096))]
struct Page([u8; 4096]);
This ensures the struct will be placed in the memory just the way you would expect it to be: no hidden changes or compiler optimizations. The benefit of doing this is that we can get any continous chunk of 4kB memory and use it as a Page struct (much like how you would do in C).
The project is still ongoing, and there is a lot left to do. It is most definitely the hardest challenge I took upon myself, but I am looking forward to completing it, even if it takes years.
See this project on github.