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