Writing an OS in Rust to run on RISC-V
gist.github.com
gist.github.com
Apologize ahead of time if this is a naive question and I need to jump into more traditional OS dev to get my bearing. I’m a mobile/services taking a break and I just happen to currently be intrigued by Rust and WASM.
Granted, most programs don't run long enough/process enough data for poor lifecycle hygiene to become noticeable in GC'd languages.
In that context you can think of the hypervisor as your microkernel which provides a unified but very low level API to the unikernel-based microservices.
Ah, a microkernel approach! It's not a bad idea but tends to lose out in performance terms.
> Some interim solution for providing sockets would have to be built until wasi spec supports it
With a little ambition, and an inter-process communication framework, you could have a "network card microservice" that's got access to the PCIe registers and takes packets in and out. You could then either do "user-mode networking" in the application or run a normal IP stack to hand out packets to more normal looking sockets.
> No file system, no writing to disc
No reason why you can't write a NVMe driver in WASM...
https://github.com/betrusted-io/xous-core
It is written in Rust and is targeted for a RISC-V
Xous for devices.
There is also Hubris and TockOS for embedded and IT.
There are a few others.
> it's a clean, modern RISC architecture without all the legacy crud of MIPS
Particularly the word "crud", and its implied denigration. Indeed, amusingly, the original Japanese translates:
> but I think it is a sophisticated RISC ISA with some negative legacies gone
Yes, but a lot (possibly most) of the benefit is likely to be from dropping bug-for-bug compatibility with defects of preexisting operating systems and instruction set architectures individually. (Like x86 segmentation or unix tcsetattr/termios, for a pair of very obvious examples.)
For paging specifically, you'll have something functionally equivalent unless your system is deficient in the same ways a 6502 is (no virtual memory or memory protection). You might come up with something functionally equivalent but better, but if so that would be a novel discovery (either in the scientific sense, or in the sense of "I discovered a research paper from 1960 that solves this problem trivially as long as you have larger than 6-bit bytes and more than 64K[0] of RAM.").
0: Something like four rooms of vacuum tubes in 1960 money, which is why it never caught on. (/not-even-all-that-s)
The microkernel idea is to move everything that can be moved, out to user-space. The minimal OS services is typically thread management, address space management, and some kind of IPC. And if you choose not to provide virtual memory or memory protection, you have a very tiny core OS.
Everything else, all the "normal" OS services can be provided by application level programs - device handlers, file systems, network stacks, time services, graphics,... almost everything.
* the illusion of multi-tasking! (context switching) (when you have one CPU-core)
* the bridge to your hardware! (e.g. ignoring the basics like your CPU/RAM/motherboard/IO devices, network interfaces themselves are an endless, unreliable torrent of data!)
* security! (unless you are literally God and make everything yourself, you have to trust ~something~ external)
* and more....
and complexity for CPUs (even ignoring legacy cruft) comes from :
* Branch prediction
* Register renaming
* Optimizing dat IPC
* More hacks for performance!
* Performance!!!
* and more....
Because at the end of the day, no matter how "simple" things are, creating the universe from scratch is never simple
2. Per origin, per program, and per identity security context I think is required to deal away with the current prerequisite of all web browsers that the underlying system be uncompromised. Basically a world where every js bundle gets executed with it's own user as its own process and having to explicitly request access to your data.
3. Combined with the above to limit the risk of compromised root accounts, if they are limited to causing DOS and data loss it's much less dangerous than a world where your entire life can be usurped by assholes with a 0day. This implies major changes in driver architectures of OS/kernels but I think it's entirely unreasonable not to make these changes. The world has changed since the 90s.
I also wish more Flatpak applications actually used those sandboxing options.
Switching from user mode to kernel mode and back (a “context switch”) is expensive. Traditionally, OSes did that on every system call. You can go faster if you have some mechanism for sending more than one system call in a single context switch.
> security context
Basically, what is a program allowed to do? Traditionally, Unixy (nowadays, Linux and friends) limit this based on just the current user, assuming that the user trusts every program they run. But nowadays you often don’t trust every program you run, and want it to be in its own limited sandbox.
> limit […] root
Most OSes have a super user that can do anything (pid 0 on Unixy things; Windows is more complex but still has them). They’re saying to not have that, instead make the super user only capable of stopping problematic things, not creating new things.
This is the one I’m more ambivalent on. At some point something has to set up the whole system, and the thing that kicks that off is going to look pretty superuser-like to me.
Another notion that I find interesting is one-address space. You still get per process protection, but address space is global. Supporting virtual memory is extremely expensive (we are paying the price today with hardware table walkers, multi-level caching of translations, various flushing on context switches). It is much cheaper if we only have to implement permissions (there are many options here) and it can make zero-copy process communication much cheaper.
Also, if you are giving up on paging, we can move beyond the 4096 byte page which we got with the 1962 Atlas (it had the equivalent of 96 KiB total memory; if pages had kept that ratio we would be using ~ 8 GiB pages today).
Would it be possible to make linux work with one address space? It seems like that should be a reasonably easy thing to do given how many different hardware platforms linux runs on. And it'd be interesting to know how much performance uplift you get from not flushing as much during context switches.
Or are there more complex interactions with different parts of the kernel that I'm missing?
Not without major changes to programs' ABI. On Linux with an address space per process, programs depend on having their private resources on fixed addresses.
The upcoming/vaporware The Mill CPU offers only a single address space, and emulates fixed addresses by aliasing a fixed part of each process' address space to somewhere else — in hardware.
In software, the ABI would have to pass a pointer to each callee's context when calling them. This is e.g. what you did in AmigaOS when you called a library function. But not all functions can be this way. You don't want all "function pointers" to be fat: function and context. For those to work, dereferenced functions would need to either belong to the program binary only, be "pure" (not access any global variables) or use a system service (on a fixed address..) to look up its context.
A MMU is complex, but allows simpler programs in the entire system overall.
So what creates/specifies user contexts, loads drivers, and installs kernel updates?
Beautiful and simple, as everything else RISC-V.
(I know of at least one RISC-V core implemented with 74-series TTL chips and a vacuum-tube implementation is surely in the works somewhere).
I already tried:
- https://github.com/connorkuehl/xv6-rust - https://github.com/tiqwab/xv6-rust
Cannot build both succesfully with latest Rust on Mac. Maybe I need to use Linux for this purpose...
(Or has this been tried 33 times and always failed?)
What reason would someone have for doing such a thing?
Obviously not “any” language but it has more compile targets than your average bear.
Rust might get compiled down through MIR, down through LLVM IR, down to assembly or wasm... which then might be JIT or AOT (re)compiled into other bytecodes... which might perhaps be decompiled back up to C... and C might be retranslated back to horrific unsafe-spamming Rust by the likes of https://c2rust.com/. We've come full circle!
The main issue is that retranslating high level languages into other high level languages isn't something that there's actually a lot of demand for, especially commercially, especially given the N x M translation matrix going on. So a lot of the projects "stabilize" (get abandoned). And automatically translating between the idioms of those languages gets even nastier in terms of matrix bloat.
Well, you've got stuff like MSIL and JVM bytecodes which are higher level, and preserve more type information, and can be compiled to / decompiled from while still preserving more structure, but they still form competing incompatible ecosystems.
The generated code isn’t particularly readable and it comes with some security issues[2] but the upshot is you have the benefits of a C++ compiler.
[1]https://docs.unity3d.com/530/Documentation/Manual/IL2CPP.htm...
Is there any? I think most modern compiler share the same codegen backend.
You don't get the benefit of the frontend when you transpile.
JIT-focused .NET is one of the ecosystems disparate from AOT-focused LLVM. While there's a bit more cross pollination now, at the time the C# to C++ transpiler was authored, there was much less so. I've taken a stab at porting mono to a new platform at a similar time period - it was rather nontrivial (I ran out of time and thus failed.) Worse still, many of Unity's targets (e.g. iOS, XB1, ...) explicitly ban JIT technology in the name of security, which wasn't something I had to deal with.
A MSIL bytecode -> C++ translator might be pretty quick and dirty... yet effective, gives you AOT compilation, avoids the need to explicitly target every architecture by hand. For all it's faults, it's not too terribly hard to figure out how to compile C++ for a given platform, typically, generally requiring exactly zero reverse engineering.
https://docs.julialang.org/en/v1/manual/calling-c-and-fortra...
But C compatibility is everywhere. Almost every language has a FFI (foreign function interface) for C code. C interop shows up in every language because at the end of the day every language needs to be able to talk to the operating system. For example, to read and write files your language needs to call functions in libc (or equivalent). And that library is written in C.
So, Nodejs can call C via has native modules & NAPI. Java can call C through JNI. Ruby has the ffi gem. Python, Luajit, Go, C#, ... the list goes on. They all have a mechanism to call C code.
If you're looking for another language that can live alongside your C code, you can choose any.
The closest you get to this sort of thing in practice is something like a parser generator, or an automatic interface generator. ANTLR and Tree-sitter both allow generating parsers in a variety of languages, but their input is basically a description of an AST in a highly specialized and extremely declarative context.
Once you start having actual logic in your code generation, there is very little benefit to codegening to a high-level language instead of going directly to a compiler IR, a bytecode, or assembly directly. The places where you see that happening--JavaScript being the biggest one--is largely limited to where there is no other alternative.
Technically it'd probably be possible to compile down to Rust. It's gc is essentially wrapping in Rc's. Though not sure it'd bring much vs linking to Nim's C output.