The Asterinas Book

asterinas-logo

Welcome to the documentation for Asterinas, an open-source project and community focused on developing cutting-edge Rust OS kernels.

Book Structure

This book is divided into five distinct parts:

Part 1: Asterinas Kernel

Explore the modern OS kernel at the heart of Asterinas. Designed to realize the full potential of Rust, Asterinas Kernel implements Linux ABI in a safe and efficient way. This means it can seamlessly replace Linux, offering enhanced safety and security.

Part 2: Asterinas Framework

The Framework lays down a minimalistic, powerful, and solid foundation for OS development. It's akin to Rust's std crate but crafted for the demands of safe Rust OS development. Asterinas Kernel is built on this very Framework.

Part 3: Asterinas OSDK

The OSDK is a command-line tool that streamlines the workflow to create, build, test, and run Rust OS projects that are built upon Asterinas Framework. Developed specifically for OS developers, it extends Rust's Cargo tool to better suite their specific needs. OSDK is instrumental in the development of Asterinas Kernel.

Part 4: Contributing to Asterinas

Asterinas is in its early stage and welcomes your contributions! This part provides guidance on how you can become an integral part of the Asterinas project.

Part 5: Requests for Comments (RFCs)

Significant decisions in Asterinas are made through a transparent RFC process. This part describes the RFC process and archives all approvaed RFCs.

Licensing

Asterinas's source code and documentation primarily use the Mozilla Public License (MPL), Version 2.0. Select components are under more permissive licenses, detailed here.

Our choice of the weak-copyleft MPL license reflects a strategic balance:

  1. Commitment to open-source freedom: We believe that OS kernels are a communal asset that should benefit humanity. The MPL ensures that any alterations to MPL-covered files remain open source, aligning with our vision. Additionally, we do not require contributors to sign a Contributor License Agreement (CLA), preserving their rights and preventing the possibility of their contributions being made closed source.

  2. Accommodating proprietary modules: Recognizing the evolving landscape where large corporations also contribute significantly to open-source, we accommodate the business need for proprietary kernel modules. Unlike GPL, the MPL permits the linking of MPL-covered files with proprietary code.

In conclusion, we believe that MPL is the best choice to foster a vibrant, robust, and inclusive open-source community around Asterinas.

Asterinas Kernel

Overview

Asterinas is a secure, fast, and general-purpose OS kernel that provides Linux-compatible ABI. It can serve as a seamless replacement for Linux while enhancing memory safety and developer friendliness.

  • Asterinas prioritizes memory safety by employing Rust as its sole programming language and limiting the use of unsafe Rust to a clearly defined and minimal Trusted Computing Base (TCB). This innovative approach, known as the framekernel architecture, establishes Asterinas as a more secure and dependable kernel option.

  • Asterinas surpasses Linux in terms of developer friendliness. It empowers kernel developers to (1) utilize the more productive Rust programming language, (2) leverage a purpose-built toolkit called OSDK to streamline their workflows, and (3) choose between releasing their kernel modules as open source or keeping them proprietary, thanks to the flexibility offered by MPL.

While the journey towards a production-grade OS kernel can be challenging, we are steadfastly progressing towards our goal. Currently, Asterinas only supports x86-64 VMs. However, our aim for 2024 is to make Asterinas production-ready on x86-64 for both bare-metal and VM environments.

Getting Started

Get yourself an x86-64 Linux machine with Docker installed. Follow the three simple steps below to get Asterinas up and running.

  1. Download the latest source code.
git clone https://github.com/asterinas/asterinas
  1. Run a Docker container as the development environment.
docker run -it --privileged --network=host --device=/dev/kvm -v asterinas:/root/asterinas asterinas/asterinas:0.4.2
  1. Inside the container, go to the project folder to build and run Asterinas.
make build
make run

If everything goes well, Asterinas is now up and running inside a VM.

Advanced Build and Test Instructions

User-Mode Unit Tests

Asterinas consists of many crates, some of which do not require a VM environment and can be tested with the standard cargo test. They are listed in the root Makefile and can be tested together through the following Make command.

make test

To test an individual crate, enter the directory of the crate and invoke cargo test.

Kernel-Mode Unit Tests

Many crates in Asterinas do require a VM environment to be tested. The unit tests for these crates are empowered by OSDK.

make ktest

To test an individual crate in kernel mode, enter the directory of the crate and invoke cargo osdk test.

cd asterinas/framework/aster-frame
cargo osdk test

Integration Test

Regression Test

The following command builds and runs the test binaries in regression/apps directory on Asterinas.

make run AUTO_TEST=regression

Syscall Test

The following command builds and runs the syscall test binaries on Asterinas.

make run AUTO_TEST=syscall

To run system call tests interactively, start an instance of Asterinas with the system call tests built and installed.

make run BUILD_SYSCALL_TEST=1

Then, in the interactive shell, run the following script to start the syscall tests.

/opt/syscall_test/run_syscall_test.sh

Debug

Using GDB to Debug

To debug Asterinas by QEMU GDB support, one could compile Asterinas in the debug profile, start an Asterinas instance and run the GDB interactive shell in another terminal.

Start a GDB-enabled VM of Asterinas with OSDK and wait for debugging connection:

make gdb_server

The server will listen at the default address specified in Makefile, i.e., a local TCP port :1234. Change the address in Makefile for your convenience, and check cargo osdk run -h for more details about the address.

Two options are provided to interact with the debug server.

  • A GDB client: start a GDB client in another terminal.

    make gdb_client
    
  • VS Code: CodeLLDB extension is required. After starting a debug server with OSDK from the shell with make gdb_server, a temporary launch.json is generated under .vscode. Your previous launch configs will be restored after the server is down. Press F5(Run and Debug) to start a debug session via VS Code. Click Continue(or, press F5) at the fisrt break to resume the paused server instance, then it will continue until reaching your first breakpoint.

The Framekernel Architecture

Framekernel: What and Why

The security of a microkernel, the speed of a monolithic kernel.

Asterinas introduces a novel OS architecture called framekernel, which unleashes the full power of Rust to bring the best of both monolithic kernels and microkernels.

Within the framekernel architecture, the entire OS resides in the same address space (like a monolithic kernel) and is required to be written in Rust. However, there's a twist---the kernel is partitioned in two halves: the OS Framework (akin to a microkernel) and the OS Services. Only the OS Framework is allowed to use unsafe Rust, while the OS Services must be written exclusively in safe Rust.

Unsafe RustResponsibilitiesCode Sizes
OS FrameworkAllowedEncapsulate low-level unsafe code within high-level safe APIsSmall
OS ServicesNot allowedImplement OS functionalities, e.g., system calls, file systems, device driversLarge

As a result, the memory safety of the kernel can be reduced to that of the OS Framework, thus minimizing the Trusted Computing Base (TCB) associated with the kernel's memory safety. On the other hand, the single address space allows different parts of the kernel to communicate in the most efficient means, e.g., function calls and shared memory. Thanks to the framekernel architecture, Asterinas can offer both exceptional performance and enhanced safety.

A comparison between different OS architectures

Requirements for the OS Framework

While the concept of framekernel is straightforward, the design and implementation of the required OS framework present challenges. It must concurrently fulfill four criteria.

The four requirements for the OS framework

  • Soundness. The safe APIs of the framework are considered sound if no undefined behaviors shall be triggered by whatever safe Rust code that a programmer may write using the APIs ---as long as the code is verified by the Rust toolchain. Soundness ensures that the OS framework, in conjunction with the Rust toolchain, bears the full responsibility for the kernel's memory safety.

  • Expressiveness. The framework should empower developers to implement a substantial range of OS functionalities in safe Rust using the APIs. It is especially important that the framework enables writing device drivers in safe Rust, considering that device drivers comprise the bulk of the code in a fully-fleged OS kernel (like Linux).

  • Minimalism. As the TCB for memory safety, the framework should be kept as small as possible. No functionality should be implemented inside the framework if doing it outside is possible.

  • Efficiency. The safe API provided by the framework is only allowed to introduce minimal overheads. Ideally, these APIs should be realized as zero-cost abstractions.

Fortunatelly, our efforts to design and implement an OS framework meeting these standards have borne fruit in the form of the Asterinas Framework. Using this framework as a foundation, we have developed the Asterinas Kernel; this framework also enables others to create their own framekernels, with different goals and tradeoffs.

Linux Compatibility

"We don't break user space."

--- Linus Torvalds

Asterinas is dedicated to maintaining compatibility with the Linux ABI, ensuring that applications and administrative tools designed for Linux can seamlessly operate within Asterinas. While we prioritize compatibility, it is important to note that Asterinas does not, nor will it in the future, support the loading of Linux kernel modules.

System Calls

At the time of writing, Asterinas implements 130 out of the 310+ system calls provided by Linux on x86-64 architecture.

NumbersNamesIs Implemented
0read
1write
2open
3close
4stat
5fstat
6lstat
7poll
8lseek
9mmap
10mprotect
11munmap
12brk
13rt_sigaction
14rt_sigprocmask
15rt_sigreturn
16ioctl
17pread64
18pwrite64
19readv
20writev
21access
22pipe
23select
24sched_yield
25mremap
26msync
27mincore
28madvise
29shmget
30shmat
31shmctl
32dup
33dup2
34pause
35nanosleep
36getitimer
37alarm
38setitimer
39getpid
40sendfile
41socket
42connect
43accept
44sendto
45recvfrom
46sendmsg
47recvmsg
48shutdown
49bind
50listen
51getsockname
52getpeername
53socketpair
54setsockopt
55getsockopt
56clone
57fork
58vfork
59execve
60exit
61wait4
62kill
63uname
64semget
65semop
66semctl
67shmdt
68msgget
69msgsnd
70msgrcv
71msgctl
72fcntl
73flock
74fsync
75fdatasync
76truncate
77ftruncate
78getdents
79getcwd
80chdir
81fchdir
82rename
83mkdir
84rmdir
85creat
86link
87unlink
88symlink
89readlink
90chmod
91fchmod
92chown
93fchown
94lchown
95umask
96gettimeofday
97getrlimit
98getrusage
99sysinfo
100times
101ptrace
102getuid
103syslog
104getgid
105setuid
106setgid
107geteuid
108getegid
109setpgid
110getppid
111getpgrp
112setsid
113setreuid
114setregid
115getgroups
116setgroups
117setresuid
118getresuid
119setresgid
120getresgid
121getpgid
122setfsuid
123setfsgid
124getsid
125capget
126capset
127rt_sigpending
128rt_sigtimedwait
129rt_sigqueueinfo
130rt_sigsuspend
131sigaltstack
132utime
133mknod
134uselib
135personality
136ustat
137statfs
138fstatfs
139sysfs
140getpriority
141setpriority
142sched_setparam
143sched_getparam
144sched_setscheduler
145sched_getscheduler
146sched_get_priority_max
147sched_get_priority_min
148sched_rr_get_interval
149mlock
150munlock
151mlockall
152munlockall
153vhangup
154modify_ldt
155pivot_root
156_sysctl
157prctl
158arch_prctl
159adjtimex
160setrlimit
161chroot
162sync
163acct
164settimeofday
165mount
166umount2
167swapon
168swapoff
169reboot
170sethostname
171setdomainname
172iopl
173ioperm
174create_module
175init_module
176delete_module
177get_kernel_syms
178query_module
179quotactl
180nfsservctl
181getpmsg
182putpmsg
183afs_syscall
184tuxcall
185security
186gettid
187readahead
188setxattr
189lsetxattr
190fsetxattr
191getxattr
192lgetxattr
193fgetxattr
194listxattr
195llistxattr
196flistxattr
197removexattr
198lremovexattr
199fremovexattr
200tkill
201time
202futex
203sched_setaffinity
204sched_getaffinity
205set_thread_area
206io_setup
207io_destroy
208io_getevents
209io_submit
210io_cancel
211get_thread_area
212lookup_dcookie
213epoll_create
214epoll_ctl_old
215epoll_wait_old
216remap_file_pages
217getdents64
218set_tid_address
219restart_syscall
220semtimedop
221fadvise64
222timer_create
223timer_settime
224timer_gettime
225timer_getoverrun
226timer_delete
227clock_settime
228clock_gettime
229clock_getres
230clock_nanosleep
231exit_group
232epoll_wait
233epoll_ctl
234tgkill
235utimes
236vserver
237mbind
238set_mempolicy
239get_mempolicy
240mq_open
241mq_unlink
242mq_timedsend
243mq_timedreceive
244mq_notify
245mq_getsetattr
246kexec_load
247waitid
248add_key
249request_key
250keyctl
251ioprio_set
252ioprio_get
253inotify_init
254inotify_add_watch
255inotify_rm_watch
256migrate_pages
257openat
258mkdirat
259mknodat
260fchownat
261futimesat
262newfstatat
263unlinkat
264renameat
265linkat
266symlinkat
267readlinkat
268fchmodat
269faccessat
270pselect6
271ppoll
272unshare
273set_robust_list
274get_robust_list
275splice
276tee
277sync_file_range
278vmsplice
279move_pages
280utimensat
281epoll_pwait
282signalfd
283timerfd_create
284eventfd
285fallocate
286timerfd_settime
287timerfd_gettime
288accept4
289signalfd4
290eventfd2
291epoll_create1
292dup3
293pipe2
294inotify_init1
295preadv
296pwritev
297rt_tgsigqueueinfo
298perf_event_open
299recvmmsg
300fanotify_init
301fanotify_mark
302prlimit64
303name_to_handle_at
304open_by_handle_at
305clock_adjtime
306syncfs
307sendmmsg
308setns
309getcpu
310process_vm_readv
311process_vm_writev
312kcmp
313finit_module
318getrandom
322execveat

File Systems

Here is the list of supported file systems:

  • Devfs
  • Devpts
  • Ext2
  • Procfs
  • Ramfs

Sockets

Here is the list of supported socket types:

  • TCP sockets over IPv4
  • UDP sockets over IPv4
  • Unix sockets

vDSO

Here is the list of supported symbols in vDSO:

  • __vdso_clock_gettime
  • __vdso_gettimeofday
  • __vdso_time

Boot Protocols

Here is the list of supported boot protocols:

Roadmap

Asterinas is a general-purpose OS kernel designed to support multiple CPU architectures and a variety of use cases. Currently, it only supports x86-64 VMs. Our roadmap includes the following plans:

  • By 2024, we aim to achieve production-ready status for VM environments on x86-64.
  • In 2025 and beyond, we will expand our support for CPU architectures and hardware devices.

Target Early Use Cases

One of the biggest challenges for a new OS kernel is driver support. Linux has been widely accepted due to its wide range of hardware support. As a newcomer, Asterinas faces the challenge of implementing drivers for all devices on a target platform, which would take a significant amount of time.

To address this obstacle, we have decided to enter the cloud market first. In an IaaS cloud, workloads of different tenants are run in VMs or VM-style bare-metal servers for maximum isolation and elasticity. The main device driver requirement for the VM environment is virtio, which is already supported by Asterinas. Therefore, using Asterinas as the guest OS of a VM or the host OS of a VM-style bare-metal server in production looks quite feasible in the near future.

Asterinas provides high assurance of memory safety thanks to the framekernel architecture. Thus, in the cloud setting, Asterinas is attractive for usage scenarios where Linux ABI is necessary but Linux itself is considered insecure due to its large Trusted Computing Base (TCB) and memory unsafety. Specifically, we are focusing on two use cases:

  1. VM-based TEEs: All major CPU architectures have introduced VM-based Trusted Execution Environment (TEE) technology, such as ARM CCA, AMD SEV, and Intel TDX. Applications running inside TEEs often handle private or sensitive data. By running on a lightweight and memory-safe OS kernel like Asterinas, they can greatly enhance security and privacy.

  2. Secure containers: In the cloud-native era, applications are commonly deployed in containers. The popular container runtimes like runc and Docker rely on the OS-level isolation enforced by Linux. However, Linux containers are prone to privilege escalation bugs. With its safety and security prioritized architecture, Asterinas can offer more reliable OS-level isolation, making it ideal for secure containers.

Asterinas Framework

Confucious remarked, "I could follow whatever my heart desired without transgressing the law."

子曰: "从心所欲,不逾矩。"

With Asterinas Framework, you don't have to learn the dark art of unsafe Rust programming and risk shooting yourself in the foot. You will be doing whatever your heart desired and be confident that your kernel will never crash or be hacked due to undefined behaviors, even if today marks your Day 1 as a Rust programmer.

APIs

Asterinas Framework stands as a powerful and solid foundation for safe kernel development, providing high-level safe Rust APIs that are

  1. Essential for OS development, and
  2. Dependent on the use of unsafe Rust.

Most of these APIs fall into the following categories:

  • Memory management (e.g., allocating and accessing physical memory pages)
  • Task management (e.g., context switching between kernel tasks)
  • User space (e.g., manipulating and entering the user space)
  • Interrupt handling (e.g., registering interrupt handlers)
  • Timer management (e.g., registering timer handlers)
  • Driver support (e.g., performing DMA and MMIO)
  • Boot support (e.g., retrieving information from the bootloader)
  • Synchronization (e.g., locking and sleeping)

To explore how these APIs come into play, see the example of a 100-line kernel in safe Rust.

Four Requirements Satisfied

In designing and implementing Asterinas Framework, we have risen to meet the challenge of fulfilling the aforementioned four criteria as demanded by the framekernel architecture.

Expressiveness is evident through Asterinas Kernel itself, where all system calls, file systems, network protocols, and device drivers (e.g., Virtio drivers) have been implemented in safe Rust by leveraging the Framework.

Adopting a minimalist philosophy, the Framework has a small codebase. At its core lies the aster-frame crate, currently encompassing about 10K lines of code ---a figure that is even smaller than those of many microkernels. As the Framework evolves, its codebase will expand, albeit at a relatively slow rate in comparison to the OS services layered atop it.

The Framework's efficiency is measurable through the performance metrics of its APIs and the system calls of Asterinas Kernel. No intrinsic limitations have been identified within Rust or the framekernel architecture that could hinder kernel performance.

Soundness, unlike the other three requirements, is not as easily quantified or proved. While formal verification stands as the gold standard, it requires considerable resources and time and is not an immediate priority. As a more pragmatic approach, we will explain why the high-level design is sound in the soundness analysis and rely on the many eyes of the community to catch any potential flaws in the implementation.

Example: Writing a Kernel in About 100 Lines of Safe Rust

To give you a sense of how Asterinas Framework enables writing kernels in safe Rust, we will show a new kernel in about 100 lines of safe Rust.

Our new kernel will be able to run the following Hello World program.

.global _start                      # entry point
.section .text                      # code section
_start:
    mov     $1, %rax                # syscall number of write
    mov     $1, %rdi                # stdout
    mov     $message, %rsi          # address of message         
    mov     $message_end, %rdx
    sub     %rsi, %rdx              # calculate message len
    syscall
    mov     $60, %rax               # syscall number of exit, move it to rax
    mov     $0, %rdi                # exit code, move it to rdi
    syscall  

.section .rodata                    # read only data section
message:
    .ascii  "Hello, world\n"
message_end:

The assembly program above can be compiled with the following command.

gcc -static -nostdlib hello.S -o hello

The user program above requires our kernel to support three main features:

  1. Loading a program as a process image in user space;
  2. Handling the write system call;
  3. Handling the exit system call.

A sample implementation of the kernel in safe Rust is given below. Comments are added to highlight how the APIs of Asterinas Framework enable safe kernel development.

#![no_std]

extern crate alloc;

use align_ext::AlignExt;
use core::str;

use alloc::sync::Arc;
use alloc::vec;

use aster_frame::cpu::UserContext;
use aster_frame::prelude::*;
use aster_frame::task::{Task, TaskOptions};
use aster_frame::user::{UserEvent, UserMode, UserSpace};
use aster_frame::vm::{PageFlags, PAGE_SIZE, Vaddr, VmAllocOptions, VmIo, VmMapOptions, VmSpace};

/// The kernel's boot and initialization process is managed by Asterinas Framework.
/// After the process is done, the kernel's execution environment
/// (e.g., stack, heap, tasks) will be ready for use and the entry function
/// labeled as `#[aster_main]` will be called.
#[aster_main]
pub fn main() {
    let program_binary = include_bytes!("../hello_world");
    let user_space = create_user_space(program_binary);
    let user_task = create_user_task(Arc::new(user_space));
    user_task.run();
}

fn create_user_space(program: &[u8]) -> UserSpace {
    let user_pages = {
        let nframes = program.len().align_up(PAGE_SIZE) / PAGE_SIZE;
        let vm_frames = VmAllocOptions::new(nframes).alloc().unwrap();
        // Phyiscal memory pages can be only accessed
        // via the VmFrame abstraction.
        vm_frames.write_bytes(0, program).unwrap();
        vm_frames
    };
    let user_address_space = {
        const MAP_ADDR: Vaddr = 0x0040_0000; // The map addr for statically-linked executable

        // The page table of the user space can be
        // created and manipulated safely through
        // the VmSpace abstraction.
        let vm_space = VmSpace::new();
        let mut options = VmMapOptions::new();
        options.addr(Some(MAP_ADDR)).flags(PageFlags::RWX);
        vm_space.map(user_pages, &options).unwrap();
        vm_space
    };
    let user_cpu_state = {
        const ENTRY_POINT: Vaddr = 0x0040_1000; // The entry point for statically-linked executable

        // The user-space CPU states can be initialized
        // to arbitrary values via the UserContext
        // abstraction.
        let mut user_cpu_state = UserContext::default();
        user_cpu_state.set_rip(ENTRY_POINT);
        user_cpu_state
    };
    UserSpace::new(user_address_space, user_cpu_state)
}

fn create_user_task(user_space: Arc<UserSpace>) -> Arc<Task> {
    fn user_task() {
        let current = Task::current();
        // Switching between user-kernel space is
        // performed via the UserMode abstraction.
        let mut user_mode = {
            let user_space = current.user_space().unwrap();
            UserMode::new(user_space)
        };

        loop {
            // The execute method returns when system
            // calls or CPU exceptions occur.
            let user_event = user_mode.execute();
            // The CPU registers of the user space
            // can be accessed and manipulated via
            // the `UserContext` abstraction.
            let user_context = user_mode.context_mut();
            if UserEvent::Syscall == user_event {
                handle_syscall(user_context, current.user_space().unwrap());
            }
        }
    }

    // Kernel tasks are managed by the Framework,
    // while scheduling algorithms for them can be
    // determined by the users of the Framework.
    TaskOptions::new(user_task)
        .user_space(Some(user_space))
        .data(0)
        .build()
        .unwrap()
}

fn handle_syscall(user_context: &mut UserContext, user_space: &UserSpace) {
    const SYS_WRITE: usize = 1;
    const SYS_EXIT: usize = 60;

    match user_context.rax() {
        SYS_WRITE => {
            // Access the user-space CPU registers safely.
            let (_, buf_addr, buf_len) =
                (user_context.rdi(), user_context.rsi(), user_context.rdx());
            let buf = {
                let mut buf = vec![0u8; buf_len];
                // Copy data from the user space without
                // unsafe pointer dereferencing.
                user_space
                    .vm_space()
                    .read_bytes(buf_addr, &mut buf)
                    .unwrap();
                buf
            };
            // Use the console for output safely.
            println!("{}", str::from_utf8(&buf).unwrap());
            // Manipulate the user-space CPU registers safely.
            user_context.set_rax(buf_len);
        }
        SYS_EXIT => Task::current().exit(),
        _ => unimplemented!(),
    }
}

OSDK User Guide

Overview

OSDK (short for Operating System Development Kit) is designed to simplify the development of Rust operating systems. It aims to streamline the process by leveraging the framekernel architecture.

OSDK provides a command-line tool cargo-osdk, which facilitates project management for those developed on the framekernel architecture. cargo-osdk can be used as a subcommand of Cargo. Much like Cargo for Rust projects, cargo-osdk enables building, running, and testing projects conveniently.

Install OSDK

Requirements

Currently, OSDK only works on x86_64 ubuntu system. We will add support for more operating systems in the future.

To run a kernel developed by OSDK with QEMU, the following tools need to be installed:

  • Rust >= 1.75.0
  • cargo-binutils
  • gcc
  • qemu-system-x86_64
  • grub
  • ovmf
  • xorriso

About how to install Rust, you can refer to the official site.

cargo-binutils can be installed after Rust is installed by

cargo install cargo-binutils

Other tools can be installed by

apt install build-essential grub-efi-amd64 grub2-common \
    libpixman-1-dev mtools qemu-system-x86 ovmf xorriso

Install

cargo-osdk is published on crates.io, and can be installed by

cargo install cargo-osdk

Upgrate

If cargo-osdk is already installed, the tool can be upgraded by

cargo install --force cargo-osdk

Why OSDK

OSDK is designed to elevate the development experience for Rust OS developers to the ease and convenience typically associated with Rust application development. Imagine crafting operating systems with the same simplicity as applications! This is important to Asterinas as we believe that the project's success is intricately tied to the productivity and happiness of its developers. So OSDK is here to upgrade your dev experience.

To be honest, writing OS kernels is hard. Even when you're using Rust, which is a total game-changer for OS devs, the challenge stands tall. There is a bunch of reasons.

First, it is hard to write a new kernel from scratch. Everything that has been taken for granted by application developers are gone: no stack, no heap, no threads, not even the standard I/O. It's just you and the no_std world of Rust. You have to implement these basic programming primitives by getting your hands dirty with the most low-level, error-prone, nitty-gritty of computer architecture. It's a journey of learning, doing, and a whole lot of finger-crossing to make sure everything clicks into place. This means a high entry bar for new OS creators.

Second, it is hard to reuse OS-related libraries/crates across projects. Think about it: most applications share a common groundwork, like libc, Rust's std library, or an SDK. This isn't the case with kernels -- they lack this shared starting point, forcing each one to craft its own set of tools from the ground up. Take device drivers, for example. They often need DMA-capable buffers for chatting with hardware, but since every kernel has its own DMA API flavor, a driver for one kernel is pretty much a no-go for another. This means that for each new kernel out there, developers find themselves having to 'reinvent the wheel' for many core components that are standard in other kernels.

Third, it is hard to do unit tests for OS functionalities. Unit testing plays a crucial role in ensuring code quality, but when you're dealing with a monolithic kernel like Linux, it's like a spaghetti bowl of intertwined parts. Trying to isolate one part for testing? Forget about it. You'd have to boot the whole kernel just to test a slice of it. Loadable kernel modules are no exception: you can't test them without plugging them into a live kernel. This monolithic approach to unit testing is slow and unproductive as it performs the job of unit tests at a price of integration tests. Regardless of the kernel architecture, Rust's built-in unit testing facility is not suited for kernel development, leaving each kernel to hack together their own testing frameworks.

Last, it is hard to avoid writing unsafe Rust in a Rust kernel. Rust brings safety... well, at least for Rust applications, where you can pretty much stay in the wonderland of safe Rust all the way through. But for a Rust kernel, one cannot help but use unsafe Rust. This is because, among other reasons, low-level operations (e.g., managing page tables, doing context switching, handling interrupts, and interacting with devices) have to be expressed with unsafe Rust features (like executing assembly code or dereferencing raw pointers). The misuse of unsafe Rust could lead to various safety and security issues, as reported by RustSec Advisory Database. Despite having a whole book to document "the Dark Arts of Unsafe Rust", unsafe Rust is still tricky to use correctly, even among reasoned Rust developers.

Creating an OS Project

OSDK can be used to create a new kernel project or a new library project. A kernel project defines the entry point of the kernel and can be run with QEMU. A library project can provide certain OS functionalities and be imported by other OSes.

Creating a new kernel project

Creating a new kernel project is simple. You only need to execute the following command:

cargo osdk new --kernel myos

Creating a new library project

Creating a new library project requires just one command:

cargo osdk new mylib

Generated files

Next, we will introduce the contents of the generated project in detail. If you don't wish to delve into the details, you can skip the following sections.

Overview

The generated directory for both the kernel project and library project contains the following contents:

myos/
├── Cargo.toml
├── OSDK.toml
├── rust-toolchain.toml
└── src/
    └── lib.rs

src/lib.rs

Kernel project

The src/lib.rs file contains the code for a simple kernel. The function marked with the #[aster_main] macro is considered the kernel entry point by OSDK. The kernel will print Hello world from the guest kernel!to the console and then abort.

There is also a code snippet that demonstrates how to write kernel mode unit tests. It follows a similar code pattern as user mode unit tests. The test module is marked with the #[cfg(ktest)] macro, and each test case is marked with #[ktest].

#![no_std]
#![forbid(unsafe_code)]

use aster_frame::prelude::*;

#[aster_main]
fn kernel_main() {
    println!("Hello world from guest kernel!");
}

Library project

The src/lib.rs of library project only contains a simple kernel mode unit test.

#![no_std]
#![forbid(unsafe_code)]

#[macro_use]
extern crate ktest;
extern crate aster_frame;

#[cfg(ktest)]
mod tests {
    #[ktest]
    fn it_works() {
        let memory_regions = aster_frame::boot::memory_regions();
        assert!(!memory_regions.is_empty());
    }
}

Cargo.toml

The Cargo.toml file is the Rust project manifest. In addition to the contents of a normal Rust project, OSDK will add the dependencies of the Asterinas framework to the file. The dependency version may change over time.

[dependencies.aster_frame]
git = "https://github.com/asterinas/asterinas"
branch = "main"

[dependencies.ktest]
git = "https://github.com/asterinas/asterinas"
branch = "main"

OSDK.toml

The OSDK.toml file is a manifest that defines the exact behavior of OSDK. By default, it contains the following contents. It includes settings on how to start QEMU to run a kernel. The meaning of each key can be found in the manifest documentation. Please avoid changing the default settings unless you know what you are doing.

project_type = "lib"

[boot]
method = "qemu-direct"

[qemu]
args = """\
    -machine q35,kernel-irqchip=split \
    -cpu Icelake-Server,+x2apic \
    --no-reboot \
    -m 2G \
    -smp 1 \
    -nographic \
    -serial chardev:mux \
    -monitor chardev:mux \
    -chardev stdio,id=mux,mux=on,signal=off \
    -display none \
    -device isa-debug-exit,iobase=0xf4,iosize=0x04 \
"""

rust-toolchain.toml

The Rust toolchain for the kernel. It aligns with the toolchain of the Asterinas framework.

Testing or Running an OS Project

OSDK allows for convenient building, running, and testing of an OS project. The following example shows the typical workflow.

Suppose you have created a new kernel project named myos and you are in the project directory:

cargo osdk new --kernel myos && cd myos

Build the project

To build the project and its dependencies, simply type:

cargo osdk build

Run the project

To launch the kernel with QEMU, use the following command:

cargo osdk run

OSDK will boot the kernel and initialize OS resources like the console for output, and then hand over control to the kernel entry point to execute the kernel code.

Note: Only kernel projects (the projects that defines the function marked with #[aster_main]) can be run; library projects cannot.

Test the project

To run the kernel mode tests, use the following command:

cargo osdk test

OSDK will run all the kernel mode tests in the crate.

If you want to run a specific test with a given name, for example, if the test is named foo, use the following command:

cargo osdk test foo

Options

Both build, run, and test commands accept options to control their behavior, such as how to compile and launch the kernel. The following documentations provide details on all the available options:

Working in a Workspace

Typically, an operating system may consist of multiple crates, and these crates may be organized in a workspace. OSDK also supports managing projects in a workspace. Below is an example that demonstrates how to create, build, run, and test projects in a workspace.

Creating a new workspace

Create a new workspace by executing the following commands:

mkdir myworkspace && cd myworkspace
touch Cargo.toml

Then, add the following content to Cargo.toml:

[workspace] members = [] resolver = "2"

Creating a kernel project and a library project

The two projects can be created using the following commands:

cargo osdk new --kernel myos
cargo osdk new mymodule

The generated directory structure will be as follows:

myworkspace/
  ├── Cargo.toml
  ├── OSDK.toml
  ├── rust-toolchain.toml
  ├── myos/
  │   ├── Cargo.toml
  │   └── src/
  │       └── lib.rs
  └── mymodule/
      ├── Cargo.toml
      └── src/
          └── lib.rs

At present, OSDK mandates that there must be only one kernel project within a workspace.

In addition to the two projects, OSDK will also generate OSDK.toml and rust-toolchain.toml at the root of the workspace.

Next, add the following function to mymodule/src/lib.rs. This function will calculate the available memory after booting:

// SPDX-License-Identifier: MPL-2.0

pub fn available_memory() -> usize { let regions = aster_frame::boot::memory_regions(); regions.iter().map(|region| region.len()).sum() }

Then, add a dependency on mymodule to myos/Cargo.toml:

[dependencies] mymodule = { path = "../mymodule" }

In myos/src/lib.rs, modify the file content as follows. This main function will call the function from mymodule:

// SPDX-License-Identifier: MPL-2.0

#![no_std] #![forbid(unsafe_code)]

use aster_frame::prelude::*;

#[aster_main] fn kernel_main() { let avail_mem_as_mb = mymodule::available_memory() / 1_000_000; println!("The available memory is {} MB", avail_mem_as_mb); }

Building and Running the kernel

Build and run the project using the following commands:

cargo osdk build
cargo osdk run

If everything goes well, you will see the output from the guest kernel.

Running unit test

You can run test cases from all crates by using the following command in the workspace folder:

cargo osdk test

If you want to run test cases from a specific crate, navigate to the crate's folder and run cargo osdk test. For example, if you want to test myos, use the following command:

cd myos && cargo osdk test

OSDK User Reference

OSDK is a command line tool that can be used as a subcommand of Cargo. The common usage of OSDK is:

cargo osdk <COMMAND>

You can use cargo osdk -h to see the full list of available commands. For the specific usage of a subcommand, you can use cargo osdk help <COMMAND>.

Manifest

OSDK utilizes a manifest named OSDK.toml to define its precise behavior regarding how to run a kernel with QEMU. The OSDK.toml file should be placed in the same folder as the project's Cargo.toml. The Manifest documentation provides an introduction to all the available configuration options.

The command line tool can also be used to set the options in the manifest. If both occur, the command line options will always take priority over the options in the manifest. For example, if the manifest defines the path of QEMU as:

[qemu]
path = "/usr/bin/qemu-system-x86_64"

But the user provides a new QEMU path when running the project using:

cargo osdk run --qemu.path="/usr/local/qemu-kvm"

Then, the actual path of QEMU should be /usr/local/qemu-kvm since command line options have higher priority.

Commands

OSDK provides similar commands as Cargo, and these commands have simalar meanings as corresponding Cargo commands.

Currently, OSDK supports the following commands:

  • new: Create a new kernel package or library package
  • build: Compile the project and its dependencies
  • run: Run the kernel with a VMM
  • test: Execute kernel mode unit test by starting a VMM
  • check: Analyze the current package and report errors
  • clippy: Check the current package and catch common mistakes

The new, build, run and test commands can accept additional options, while the check and clippy commands cannot.

cargo osdk new

Overview

The cargo osdk new command is used to create a kernel project or a new library project. The usage is as follows:

cargo osdk new [OPTIONS] <name>

Arguments

<name>: the name of the crate.

Options

--kernel: Use the kernel template. If this option is not set, the library template will be used by default.

Examples

  • Create a new kernel named myos:
cargo osdk new --kernel myos
  • Create a new library named mymodule:
cargo osdk new mymodule

cargo osdk build

Overview

The cargo osdk build command is used to compile the project and its dependencies. The usage is as follows:

cargo osdk build [OPTIONS]

Options

The options can be divided into two types: Cargo options that can be accepted by Cargo, and Manifest options that can also be defined in the manifest named OSDK.toml.

Cargo options

  • --profile <PROFILE>: Build artifacts with the specified Cargo profile (built-in candidates are 'dev', 'release', 'test', and 'bench') [default: dev]

  • --release: Build artifacts in release mode, with optimizations

  • --features <FEATURES>: Space or comma separated list of features to activate

Manifest options

These options can also be defined in the project's manifest named OSDK.toml. Command line options are used to override or append values in OSDK.toml. The allowed values for each option can be found in the Manifest Documentation.

  • --kcmd-args <ARGS>: Command line arguments for the guest kernel
  • --init-args <ARGS>: Command line arguments for the init process
  • --initramfs <PATH>: Path of the initramfs
  • --boot-method <METHOD>: The method to boot the kernel
  • --grub-boot-protocol <PROTOCOL>: The boot protocol for booting the kernel
  • --display-grub-menu: To display the GRUB menu if booting with GRUB
  • --qemu-path <PATH>: Path of QEMU
  • --qemu-args <ARGS>: Extra arguments for running QEMU

Examples

  • Build a project with ./initramfs.cpio.gz as the initramfs and multiboot2 as the boot protocol used by GRUB:
cargo osdk build --initramfs="./initramfs.cpio.gz" --grub-boot-protocol="multiboot2"
  • Build a project and append sh, -l to init process arguments:
cargo osdk build --init_args="sh" --init_args="-l"

cargo osdk run

Overview

cargo osdk run is used to run the kernel with QEMU. The usage is as follows:

cargo osdk run [OPTIONS]

Options

Most options are the same as those of cargo osdk build. Refer to the documentation of cargo osdk build for more details.

Options related with debugging:

  • -G, --enable-gdb: Enable QEMU GDB server for debugging.
  • --vsc: Generate a '.vscode/launch.json' for debugging kernel with Visual Studio Code (only works when QEMU GDB server is enabled, i.e., --enable-gdb). Requires CodeLLDB.
  • --gdb-server-addr <ADDR>: The network address on which the GDB server listens, it can be either a path for the UNIX domain socket or a TCP port on an IP address. [default: .aster-gdb-socket(a local UNIX socket)]

See Debug Command to interact with the GDB server in terminal.

Examples

  • Launch a debug server via QEMU with an unix socket stub, e.g. .debug:
cargo osdk run --enable-gdb --gdb-server-addr .debug
  • Launch a debug server via QEMU with a TCP stub, e.g., localhost:1234:
cargo osdk run --enable-gdb --gdb-server-addr :1234
  • Launch a debug server via QEMU and use VSCode to interact:
cargo osdk run --enable-gdb --vsc --gdb-server-addr :1234

cargo osdk test

cargo osdk test is used to execute kernel mode unit test by starting QEMU. The usage is as follows:

cargo osdk test [OPTIONS] [TESTNAME]

Arguments

[TESTNAME]: Only run tests containing this string in their names

Options

The options are the same as those of cargo osdk build. Refer to the documentation of cargo osdk build for more details.

Examples

  • Execute tests containing foo in their names with q35 as the QEMU machine type:
cargo osdk test foo --qemu.machine="q35"

Manifest

Overview

OSDK utilizes a manifest to define its precise behavior. Typically, the configuration file is named OSDK.toml and is placed in the root directory of the workspace (the same directory as the workspace's Cargo.toml). If there is only one crate and no workspace, the file is placed in the crate's root directory.

For a crate inside workspace, it may have two distinct related manifests, one is of the workspace (in the same directory as the workspace's Cargo.toml) and one of the crate (in the same directory as the crate's Cargo.toml). OSDK will firstly refer to the crate-level manifest, then query the workspace-level manifest for undefined fields. In other words, missing fields of the crate manifest will inherit values from the workspace manifest.

Configurations

Below, you will find a comprehensive version of the available configurations in the manifest.

Here are notes for some fields with special value treatings:

  • * marks the field as "will be evaluated", that the final value of string "S" will be the output of echo "S" using the host's shell.
  • + marks the path fields. The relative paths written in the path fields will be relative to the manifest's enclosing directory.

If values are given in the tree that's the default value inferred if that the field is not explicitly stated.

project_type = "kernel"     # The type of the current crate. Can be lib/kernel[/module]

# --------------------------- the default schema settings -------------------------------
supported_archs = ["x86_64"]# List of strings, that the arch the schema can apply to
[build]
features = []               # List of strings, the same as Cargo
profile = "dev"             # String, the same as Cargo
[boot]
method = "qemu-direct"      # "grub-rescue-iso"/"qemu-direct"/"grub-qcow2"
kcmd_args = []              # <1>
init_args = []              # <2>
initramfs = "path/to/it"    # + The path to the initramfs
[grub]                      # Grub options are only needed if boot method is related to GRUB
mkrescue_path = "path/to/it"# + The path to the `grub-mkrescue` executable
protocol = "multiboot2"     # The protocol GRUB used. "linux"/"multiboot"/"multiboot2"
display_grub_menu = false   # To display the GRUB menu when booting with GRUB
[qemu]
path +                      # The path to the QEMU executable
args *                      # String. <3>
[run]                       # Special settings for running, which will override default ones
build                       # Overriding [build]
boot                        # Overriding [boot]
grub                        # Overriding [grub]
qemu                        # Overriding [qemu]
[test]                      # Special settings for testing, which will override default ones
build                       # Overriding [build]
boot                        # Overriding [boot]
grub                        # Overriding [grub]
qemu                        # Overriding [qemu]
# ----------------------- end of the default schema settings ----------------------------

[schema."user_custom_schema"]
#...                        # All the other fields in the default schema. Missing but
                            # needed values will be firstly filled with the default
                            # value then the corresponding field in the default schema

Here are some additional notes for the fields:

  1. The arguments provided will be passed to the guest kernel.

    Optional. The default value is empty.

    Each argument should be in one of the following two forms: KEY=VALUE or KEY if no value is required. Each KEY can appear at most once.

  2. The arguments provided will be passed to the init process, usually, the init shell.

    Optional. The default value is empty.

  3. Additional arguments passed to QEMU that is organized in a single string that can have any POSIX shell compliant separators.

    Optional. The default value is empty.

    Each argument should be in the form of KEY and VALUE or KEY if no value is required. Some keys can appear multiple times (e.g., -device, -netdev), while other keys can appear at most once. Certain keys, such as -kernel and -initrd, are not allowed to be set here as they may conflict with the settings of OSDK.

    The field will be evaluated, so it is ok to use environment variables in the arguments (usually for paths or conditional arguments). You can even use this mechanism to read from files by using command replacement $(cat path/to/your/custom/args/file).

Example

Here is a sound, self-explanatory example similar to our usage of OSDK in the Asterinas project.

In the script ./tools/qemu_args.sh, the environment variables will be used to determine the actual set of qemu arguments.

project_type = "kernel"

[boot]
method = "grub-rescue-iso"

[run]
boot.kcmd_args = [
    "SHELL=/bin/sh",
    "LOGNAME=root",
    "HOME=/",
    "USER=root",
    "PATH=/bin:/benchmark",
    "init=/usr/bin/busybox",
]
boot.init_args = ["sh", "-l"]
boot.initramfs = "regression/build/initramfs.cpio.gz"

[test]
boot.method = "qemu-direct"

[grub]
protocol = "multiboot2"
display_grub_menu = true

[qemu]
args = "$(./tools/qemu_args.sh)"

[scheme."microvm"]
boot.method = "qemu-direct"
qemu.args = "$(./tools/qemu_args.sh microvm)"

[scheme."iommu"]
supported_archs = ["x86_64"]
qemu.args = "$(./tools/qemu_args.sh iommu)"

[scheme."intel_tdx"]
supported_archs = ["x86_64"]
build.features = ["intel_tdx"]
boot.method = "grub-qcow2"
grub.mkrescue_path = "~/tdx-tools/grub"
grub.protocol = "linux"
qemu.args = """\
    -accel kvm \
    -name process=tdxvm,debug-threads=on \
    -m ${MEM:-8G} \
    -smp $SMP \
    -vga none \
"""

Scheme

Scheme is an advanced feature to create multiple profiles for the same actions under different scenarios. Scheme allows any user-defined keys and can be selected by the --scheme CLI argument. The key scheme can be used to create special settings (especially special QEMU configurations). If a scheme action is matched, unspecified and required arguments will be inherited from the default scheme.