Writing a RISC-V OS in Rust: System Calls

栏目: IT技术 · 发布时间: 4年前

内容简介:System calls are a way for unprivileged, user applications to request services from the kernel. In the RISC-V architecture, we invoke the call using theWe have to set up our convention for handling system calls. We can use a convention that already exists,

Video

https://www.youtube.com/watch?v=6GW_jgkdGPw

Overview

System calls are a way for unprivileged, user applications to request services from the kernel. In the RISC-V architecture, we invoke the call using the ecall instruction. This will cause the CPU to halt what it's doing, elevate privilege modes, and then jump to whatever function handler is stored in the mtvec (machine trap vector) register. Remember, this is the "funnel" where all traps are handled, including our system calls.

We have to set up our convention for handling system calls. We can use a convention that already exists, so we can interface with a library, such as newlib. But, let's make this ours! We get to say what the system call numbers are, and where they will be when we execute a system call.

System Call Procedure

We usually only need to execute a system call if we're in a lower privilege mode. If we're in the kernel, we already have access to most of the privileged systems, which precludes us from actually needing to go into a system call.

Our system call comes to us through synchronous trap #8, which is the cause for a user-mode ecall. So, in our #8 handler, we forward the data over to our syscall handler. We'll be completely in Rust. We also need to be able to manipulate the program counter. Think of it, our exit system call must be able to move on to another process, so we manipulate that through the mepc (machine exception program counter) register.

Rust System Calls

As always, make sure you import your system call code with the following.

pub mod syscall;

This will go in your lib.rs file.

Ordering and Numbering

Several libraries already have a certain order they expect your system calls to be in. However, we will be creating our own "C library" for our simple applications. Therefore, we're good to go as long as we're consistent.

Think about how we can do this. Whenever we execute the ecall instruction, the CPU elevates privileges and jumps to a trap vector. How do we send data along with it? The ARM architecture allows you to encode a number into their svc (supervisor call) instruction. However, many operating systems forego that implementation. So, what do we do instead?

The answer is: registers. We have a ton of registers in the RISC-V architecture, so we aren't limited nearly as much as we were in the x86 days. For our system call convention, we will put the number of the system call into the first argument register, a0 . Subsequent parameters will go in a1, a2, a3, ..., a7. Then, we will return anything back using the same a0 register.

This is the same calling convention for regular functions, so it will interface well with what we already know. In RISC-V, we can use the pseudoinstruction call to make a normal function call, or ecall to make a supervisor call. Consistency is cey, or Konsistency is Key. Hmm.. Consistency is key isn't consistently sounding the 'k' :(.

Implementing System Calls

We redirect synchronous cause #8 to our system call Rust code.

8 => {
  // Environment (system) call from User mode
  println!("E-call from User mode! CPU#{} -> 0x{:08x}", hart, epc);
  return_pc = do_syscall(return_pc, frame);
},

Most operating systems build a table with function pointers, but I'm using Rust's match statement here. I haven't done any performance calculations, but I don't think one has a distinct advantage over another. Again, don't quote me, I haven't actually tested it.

As you can see, we receive a new program counter, which is the address of the instruction we want to execute when we return. The system call function must at least move this by 4 because the ecall instruction is what's actually causing the synchronous interrupt. If we didn't move the program counter, we'd keep executing the ecall instruction over and over again. Fortunately, unlike x86, all instructions are 32-bits except for the 16-bit compressed instructions, but ecall is always 32-bit because it doesn't have a compressed form.

Each mode has a different ecall cause. If we make an ecall in machine mode (the most privileged mode), we get cause #11. Otherwise, if we make an ecall in supervisor mode (typically the kernel mode), we get cause #9. So, we can split off what system calls mean what given the privilege mode. I'm going to keep it easy and konsistent (consistent?) by linking all three to the same system call function.

Let There Be System Calls!

Let's take a look at the code in Rust.

pub fn do_syscall(mepc: usize, frame: *mut TrapFrame) -> usize {
  let syscall_number;
  unsafe {
      // A0 is X10, so it's register number 10.
      syscall_number = (*frame).regs[10];
  }
  match syscall_number {
      0 => {
          // Exit
          println!("You called the exit system call!");
          mepc + 4
      },
      _ => {
          print!("Unknown syscall number {}", syscall_number);
          mepc + 4
      }
  }

The very first thing we need is the value of A0, which is the system call number. Since this is stored in the context during the trap handler phase, we can just retrieve it directly from memory. We have to put this in an unsafe context because we're dereferencing a raw pointer, which may or may not be an accurate memory address. Since Rust cannot guarantee that it is, we're required to put this in an unsafe block.

This might be interesting to non-Rustaceans.

let syscall_number;
unsafe {
    // A0 is X10, so it's register number 10.
    syscall_number = (*frame).regs[10];
}

I'm creating a variable called syscall_number , but since I cannot get its value until I'm inside of an unsafe block, it is just a placeholder. In fact, Rust doesn't put a type to it until we give it a value. You'll notice that I haven't put any constraints on the variable type, so I'm letting Rust decide.

Why did I do this? The unsafe block creates a new block, and thus, it creates a new scope. However, I want syscall_number to contain an immutable value outside of the unsafe context. This is why I decided to do it this way. Technically, I can constrain the data type by using let syscall_number: u64; , but it's not required since Rust will evaluate the data type whenever we get around to setting the variable equal to something.

Where do we go from here?

We will write the scheduler so that our system calls can actually do stuff--yes, stuff in the most technically accurate way possible! For example, we may need to postpone a process until after a certain amount of time (sort of like how sleep() works), or we need to close the process (much like how exit() ) works. What about writing to the console--yep, we need that one too.

So, next, we will add processes and the system calls necessary as we reach them. We aren't making any future predictions, which might be dangerous, but we're implementing our OS as we find an unworkable solution. Hopefully, this will give you an appreciation for how operating systems are implemented to be all things to all applications!

Table of Contents →Chapter 6 → (Chapter 7) → Chapter 8


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

写给大忙人看的C++

写给大忙人看的C++

【美】Brian Overland(布莱恩.奥弗兰德) / 卢涛、李颖 / 电子工业出版社 / 2015-8 / 109.00

《写给大忙人看的C++》全面介绍了C++语言知识,既提供了学习C++语言最新功能的捷径,也为快速找到特定问题的答案提供了便利。《写给大忙人看的C++》简明地描述了C++核心语言和标准库中几乎所有的函数、对象和运算符,一目了然地显示了语法、结构和重要函数的信息,内容组织形式便于快速查找信息。《写给大忙人看的C++》精选了实用的例子来深入地讲解概念,还提供了富有挑战性的练习及参考答案,便于读者举一反三......一起来看看 《写给大忙人看的C++》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试