xv6rs is a Rust implementation of the xv6 operating system, originally developed at MIT. While the original xv6 is written in C, this project reimplements it in Rust, targeting the RISC-V architecture.
This project is a Rust port of the xv6 operating system. It demonstrates how to implement a Unix-like operating system using Rust's safety features while maintaining low-level control necessary for OS development.
The project consists of three main components:
-
kernel - The OS kernel
- Memory management (kalloc.rs)
- Process management (proc.rs, process.rs)
- File system (fs.rs, file.rs)
- Device drivers (uart.rs, e1000.rs, virtio.rs)
- Network stack (net/ directory)
- CPU and interrupt handling (cpu.rs, trap.rs)
- Low-level assembly code (entry.S, kernelvec.S, swtch.S, trampoline.S)
-
user - User programs
- Basic Unix commands (cat, echo, ls, etc.)
- System call interface (syscall.rs)
- Test programs
-
mkfs - Filesystem creation tool
- Tool to create the filesystem image
- Rust for bare-metal programming: Using
#![no_std]
environment, custom memory allocators, and low-level hardware manipulation - RISC-V architecture: Targeting the RISC-V 64-bit architecture
- Multi-core support: Support for multiple CPU cores (harts)
- Networking: Basic networking capabilities including a TCP/IP stack
- Testing infrastructure: Built-in testing for both kernel and user programs
- Rust nightly toolchain with RISC-V target support
- QEMU with RISC-V support
- Install the Rust nightly toolchain:
rustup toolchain install nightly
- Add the RISC-V target to your nightly Rust toolchain:
rustup target add riscv64imac-unknown-none-elf --toolchain nightly
Note: This project requires the nightly Rust toolchain because it uses unstable features like
custom_test_frameworks
,alloc_error_handler
, andallocator_api
.Important: The RISC-V assembly code in this project requires the
zicsr
andzifencei
extensions. These extensions are needed for instructions likecsrr
which are used in the assembly code. The build system has been configured to include these extensions.
make build
make qemu
# Run tests (always in release mode)
make test
Note: Tests always run in release mode by default. The release mode compilation helps avoid certain issues that may occur in debug mode:
Compiler Optimizations: Release mode enables various optimizations that can prevent certain runtime issues, particularly in low-level code that interacts directly with hardware.
Memory Layout: Debug builds include additional information and different memory layouts that can sometimes trigger alignment issues or other memory-related problems in bare-metal environments.
Inlining and Code Generation: Release mode's aggressive inlining and code generation can avoid certain edge cases in function calls, especially in concurrent or interrupt-driven code.
Performance: Tests run significantly faster in release mode, which is beneficial for the more extensive test suites.
The make test
command performs the following steps:
-
Builds the mkfs tool and user programs
- Creates necessary binaries for the filesystem tool and user applications
-
Builds the test harness for user libraries
- Runs
cargo test
with the--no-run
flag for the user component - Creates symbolic links to the test executables
- Runs
-
Creates a test filesystem image
- Uses the mkfs tool to create a special filesystem image (fs.test.img)
- Includes the test harness and user programs in this image
-
Builds the test harness for the kernel library
- Compiles the kernel tests using
cargo test
with the--no-run
flag
- Compiles the kernel tests using
-
Executes tests in QEMU
- Runs QEMU with the test filesystem image
- Boots the kernel test harness
- Tests run inside the emulated RISC-V environment
The OS implements standard Unix system calls:
- Process management: fork, exit, wait, exec
- File operations: open, read, write, close, unlink, mkdir, chdir, fstat
- Network operations: socket, bind, connect
- Memory management:
User programs in xv6rs are implemented in Rust with the following characteristics:
- No Standard Library: All programs use
#![no_std]
attribute since they run in a bare-metal environment without the Rust standard library. - Entry Point: Programs use the
entry_point!
macro which sets up the proper entry point and argument handling. - System Call Interface: Programs interact with the kernel through system calls defined in
user/src/syscall.rs
. - Heap Allocation Support: User programs can use heap allocation (like
Box
,Vec
, etc.) through the custom global allocator implemented in the user environment.
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(xv6rs_user::test_runner)]
#![reexport_test_harness_main = "test_main"]
use xv6rs_user::{
entry_point,
syscall::{sys_write, sys_read}, // Import needed syscalls
Args,
};
entry_point!(main);
fn main(args: &mut Args) -> Result<i32, &'static str> {
// Program logic here
// Return 0 for success or an error message
Ok(0)
}
The shell (sh.rs
) is implemented with the following features:
- Command Execution: Basic command execution using
fork
andexec
system calls - Pipes: Support for command piping with the
|
operator - Redirection: Input (
<
) and output (>
) redirection - Background Execution: Running commands in the background with
&
- Built-in Commands: Support for built-in commands like
cd
andexit
The shell implementation avoids heap allocation by using a more direct approach:
- For simple commands, it parses and executes them directly
- For pipes, it splits the input buffer and processes each part separately
- For background execution, it sets a flag to avoid waiting for the child process
$ ls # List files
$ cat file.txt # Display file contents
$ echo hello > file.txt # Write to a file
$ cat < file.txt # Read from a file
$ ls | grep txt # Pipe output of ls to grep
$ sleep 10 & # Run sleep in the background
$ cd / # Change directory
$ exit # Exit the shell
User programs are built as part of the main build process:
make build
This compiles all Rust files in user/src/bin/
directory and includes them in the filesystem image.