A basic educational operating system written in baremetal Rust for Raspberry Pi 3+ hardware, accompanied by a step-by-step textbook that teaches you how to build your own operating system from scratch.
demo.mp4
This is a basic operating system, written in Rust. Designed to run baremetal on the Raspberry Pi 3B+ computer hardware. It includes:
AtOS was built in order to learn about OS development, and low level programming. And to help others learn from it.
There is a fully documented guide book written on AtOS development. It aims to help others to also build a basic OS in Rust for the Raspberry Pi hardware. Which may also help in ARM low level development.
It can be found at: zackygamedev.github.io/RustOSGuideforRPi3/ (It is still being written roughly weekly)
- Process management with creation, termination, execution, and reaping of children
- Mutex locks and Spinlocks
- Round Robin Scheduler with NSP Timer
- Syscall pipeline for user programs with common syscalls (
wait(),sleep(),fork(), etc...) - Fully virtualized user space
- 4KB granule paging management with 39 bit VA space
- Mini UART handling for I/O
- ELF parsing and execution
- Full exception handling pipeline
Not supported:
- Dynamic Page Allocation
- Dynamic Memory/Heap Allocation
- Networking
- Multicore execution
- GUI (All IO is through UART)
You may test this OS out on QEMU. Makefiles are already included for this purpose. You may simply do:
make clean
make
make runOnce the OS starts, you may use help command to explore your options.
User space is created as a separate cargo project under src/user. All user programs technically are bare metal ARM projects. Programs have a no_std and no_main environment, entry point being marked through the entry! macro.
You may try creating your own user programs for the OS! Explore templates/ under user/src/bin/.
Theres entry! or entry_args! macro depending on whether your program needs to use entry arguments or not. The expand into proper entry runtime that parses arguments and setups environment for the user program to run.
All syscalls work by using the traditional svc instruction. Both src/user/src/stdlib and src/kernel/ contain a file named syscalls.rs which facilitates syscall pipeline. For user side, syscall.rs executes svc instruction to prompt kernel with some data. On kernel side a hardware exception is caused by svc which is identified and handled as a syscall in kernel::syscalls
So far syscalls available are:
sys_print: Writes some string to the UART output.sys_read: Reads a line of string from te UART input.sys_exit: Terminates the process.sys_fork: Forks current running process.sys_exec: Executes a new program in the place of current running process.sys_wait: Waits for some or any child of current process.sys_poll_char: Read a byte of character from UART if available, else returns0.sys_sleep: Wait for given amount of milliseconds.sys_print_os_info: Shows afastfetchinspired output with hardware information about OS.sys_p_info: Returns meta information about the running process.
The kernel is mapped in higher ttbr1 space starting at 0xFFFFFF8000000000. First two GB of that VA space are identity mapped to first two GB of PA space. This is so kernel has a unfiltered view of true RAM state. Effectively for kernel, the true memory is found at some offset in virtual memory. The VA space immediately after that region is used for kernel stacks for different processes.
The user space is allocated whenever a new process is spawned. User programs work in ttbr0 VA space from 0x0000000000000000 to 0x0000007FFFFFFFFF. User stack starting at the ending of said region.
All processes are given their own kernel stack. Which the kernel uses when prompted from corresponding process.
Pages and Kernel stacks are allocated and freed accordingly to process management.
Spinlocks and Mutex locks are available to use. Spinlocks upon acquiring block any IRQs or interrupts to current code. Any hypothetical other cores trying to acquire it would need to wait in spin. Mutex locks do not block IRQ or other interruptions. It is simply that if other process tries to acquire the same mutex, it is put to sleep and woken up when said mutex is available next.
Before a user program is executed into a process, arguments can optionally be passed to it. Arguments passed are written to the user program's stack. Whereby the user space runtime through the entry macros parses the stack and passes it to the program as &[&str].
The format in which the arguments are written on the stack is as follows:
Let's say arguments are "program hello world"
STACK TOP ADDRESS (initial sp)
┌────────────────────┐
│ "program" │ ← argv[0]
├────────────────────┤
│ "hello" │ ← argv[1]
├────────────────────┤
│ "world" │ ← argv[2]
├────────────────────┤
│ padding │
├────────────────────┤
│ offset → "world" │ ← &argv[2] - final sp
├────────────────────┤
│ offset → "hello" │ ← &argv[1] - final sp
├────────────────────┤
│ offset → "program" │ ← &argv[0] - final sp
└────────────────────┘
Final SP
Where the runtime is given the stack top, and final sp values, along with number of args (argc).
make in root directory is equivalent to:
# Navigate into user directory and compile Rust binaries for user programs
cd src/user
cargo build
# Create output folders for user binaries
mkdir -p build
mkdir -p build/dump
# Loop over every .rs file in src/user/src/bin/ (e.g., shell, init)
# For each program, copy the ELF binary and convert it to raw binary format
for prog in $(ls src/bin/*.rs | xargs -n 1 basename | sed 's/\.rs//'); do \
cp target/aarch64-unknown-none/debug/$prog build/$prog; \
aarch64-linux-gnu-objcopy -O binary target/aarch64-unknown-none/debug/$prog build/dump/$prog.bin; \
echo "Built $prog.bin"; \
done
cd ../.. # Return to root directory
cargo build --release --target aarch64-unknown-none
cargo build --target aarch64-unknown-none
aarch64-linux-gnu-objcopy target/aarch64-unknown-none/release/AtOS -O binary kernel8.imgOutput image:
kernel8.img
You can either flash it to hardware, or run it in QEMU using make run which is equivalent to:
qemu-system-aarch64 \
-M raspi3b \
-kernel kernel8.img \
-serial null \
-serial stdio \
-display none- All kernel operations are written in
src/kernel/in an individual file, for each feature. - All user side operations for syscalls and core features are written in
src/user/src/stdlib/. - Main user program runtime is written in
src/user/src/lib.rsasusercrate. - All individual user programs are written in
src/user/src/bin/
Intro text format alongisde logo is stored in atos_intro.txt. build.rs generates a valid intro string to print on the fly on building.
This project exists to learn OS development through Rust on an ARM environment. And to help others also learn to achieve a similar task. All features of this OS are being documented in the guide book, in a implementation timeline order. An easy to follow beginner friendly manner to create an easy entry for people new to low level work.
Contributions are welcomed. New features or improvement of pre-existing features. Simple bug fixes/quality of life changes. Or even brand new user programs to try on AtOS. All are welcome.
(merge is subject to review)