编写用户程序

本节的工作很类似第一章第四节移除 runtime 依赖的工作,但区别是,第一章第四节移除 runtime 依赖是要完全移除对 runtime 的需求,以构造 OS;而本节需要实现一个支持 U Mode 应用程序的最小 runtime ,这个 runtime 仅仅需要支持很少系统调用访问和基本的动态内存分配。虽然有区别,很多本节很多代码都可以直接参考第一章第四节移除 runtime 依赖的设计思路和代码。

我们的用户程序一般在 CPU 的用户态 (U Mode) 下执行,而它只能通过执行 ecall 指令,触发 Environment call from U-mode 异常 l 来发出系统服务请求,此时 CPU 进入内核态 (S Mode) ,OS 通过中断服务例程收到请求,执行相应内核服务,并返回到 U Mode。

这一章中,简单起见,内核和用户程序约定两个系统调用

  • 在屏幕上输出一个字符,系统调用

    编写用户程序 - 图1

  • 退出用户线程,系统调用

    编写用户程序 - 图2

创建用户程序模板

我们的内核能给程序提供的唯一支持就是两个简单的系统调用。

所以我们的用户程序基本还是要使用前两章的方法,不同的则是要把系统调用加入进去。

创建 usr 目录,并在 usr 目录下使用 Cargo 新建一个二进制项目,再删除掉默认生成的 usr/rust/src/main.rs

  1. $ mkdir usr; cd usr
  2. $ cargo new rust --bin
  3. $ rm usr/rust/src/main.rs

加上工具链

  1. // usr/rust/rust-toolchain
  2. nightly

建立最小 Runtime 系统

访问系统调用

我们先来看访问系统调用的实现:

  1. // usr/rust/src/syscall.rs
  2. enum SyscallId {
  3. Write = 64,
  4. Exit = 93,
  5. }
  6. #[inline(always)]
  7. fn sys_call(
  8. syscall_id: SyscallId,
  9. arg0: usize,
  10. arg1: usize,
  11. arg2: usize,
  12. arg3: usize,
  13. ) -> i64 {
  14. let id = syscall_id as usize;
  15. let mut ret: i64;
  16. unsafe {
  17. asm!(
  18. "ecall"
  19. : "={x10}"(ret)
  20. : "{x17}"(id), "{x10}"(arg0), "{x11}"(arg1), "{x12}"(arg2), "{x13}"(arg3)
  21. : "memory"
  22. : "volatile"
  23. );
  24. }
  25. ret
  26. }
  27. pub fn sys_write(ch: u8) -> i64 {
  28. sys_call(SyscallId::Write, ch as usize, 0, 0, 0)
  29. }
  30. pub fn sys_exit(code: usize) -> ! {
  31. sys_call(SyscallId::Exit, code, 0, 0, 0);
  32. loop {}
  33. }

看起来很像内核中 src/sbi.rs 获取 OpenSBI 服务的代码对不对?其实内核中是在 S Mode 去获取 OpenSBI 提供的 M Mode 服务;这里是用户程序在 U Mode 去获取内核提供的 S Mode 服务。所以看起来几乎一模一样。

相信内核会给我们提供这两项服务,我们可在用户程序中放心的调用 sys_write, sys_exit 两函数了!

格式化输出

接着是一些我们在构建最小化内核时用到的代码,有一些变动,但这里不多加赘述。

格式化输出代码:

  1. // usr/rust/src/io.rs
  2. use crate::syscall::sys_write;
  3. use core::fmt::{self, Write};
  4. pub fn putchar(ch: char) {
  5. // 这里 OpenSBI 提供的 console_putchar 不存在了
  6. // 然而我们有了新的依靠:sys_write
  7. sys_write(ch as u8);
  8. }
  9. //其他部分与os/src/io.rs 一样
  10. ......

语义项支持

语义项代码:

  1. // usr/rust/src/lang_items.rs
  2. ......
  3. use crate::DYNAMIC_ALLOCATOR;
  4. // 初始化用户堆,用于U Mode中动态内存分配
  5. fn init_heap() {
  6. const HEAP_SIZE: usize = 0x1000;
  7. static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
  8. unsafe {
  9. DYNAMIC_ALLOCATOR.lock().init(HEAP.as_ptr() as usize, HEAP_SIZE);
  10. }
  11. }
  12. #[panic_handler]
  13. fn panic(_info: &PanicInfo) -> ! {
  14. let location = _info.location().unwrap();
  15. let message = _info.message().unwrap();
  16. println!(
  17. "\nPANIC in {} at line {} \n\t{}",
  18. location.file(),
  19. location.line(),
  20. message
  21. );
  22. loop {}
  23. }
  24. // 这里是程序入口
  25. // 调用 main 函数,并利用 sys_exit 系统调用退出
  26. #[no_mangle]
  27. pub extern "C" fn _start(_args: isize, _argv: *const u8) -> ! {
  28. init_heap();
  29. sys_exit(main())
  30. }
  31. #[no_mangle]
  32. pub extern fn abort() {
  33. panic!("abort");
  34. }
  35. #[lang = "oom"]
  36. fn oom(_: Layout) -> ! {
  37. panic!("out of memory!");
  38. }

看起来很像内核中 src/lang_item.rs 获取 OpenSBI 服务的代码对不对?其实内核中是在 S Mode 去获取 OpenSBI 提供的 M Mode 服务;这里是用户程序在 U Mode 去获取内核提供的 S Mode 服务。所以看起来几乎一模一样。

形成 runtime lib

还有 lib.rs

  1. // usr/rust/Cargo.toml
  2. [dependencies]
  3. buddy_system_allocator = "0.3"
  4. // usr/rust/src/lib.rs
  5. #![no_std]
  6. #![feature(asm)]
  7. #![feature(lang_items)]
  8. #![feature(panic_info_message)]
  9. #![feature(linkage)]
  10. extern crate alloc;
  11. #[macro_use]
  12. pub mod io;
  13. pub mod syscall;
  14. pub mod lang_items;
  15. use buddy_system_allocator::LockedHeap;
  16. #[global_allocator]
  17. static DYNAMIC_ALLOCATOR: LockedHeap = LockedHeap::empty();

应用程序模板

现在我们可以将每一个含有 main 函数的 Rust 源代码放在 usr/rust/src/bin 目录下。它们每一个都会被编译成一个独立的可执行文件。

其模板为:

  1. // usr/rust/src/bin/model.rs
  2. #![no_std]
  3. #![no_main]
  4. extern crate alloc;
  5. #[macro_use]
  6. extern crate user;
  7. #[no_mangle]
  8. pub fn main() -> usize {
  9. 0
  10. }

这里返回的那个值即为程序最终的返回值。

Hello World 应用程序

基于上述应用程序模板,我们可以实现一个最简单的Hello World程序:

  1. // usr/rust/src/bin/hello_world.rs
  2. #![no_std]
  3. #![no_main]
  4. extern crate alloc;
  5. #[macro_use]
  6. extern crate user;
  7. #[no_mangle]
  8. pub fn main() -> usize {
  9. for _ in 0..10 {
  10. println!("Hello world! from user mode program!");
  11. }
  12. 0
  13. }

和内核项目一样,这里也创建一个 .cargo/config 文件指定默认的目标三元组。但这次我们就不用自定义链接脚本了,用默认的即可。

  1. # .cargo/config
  2. [build]
  3. target = "riscv64imac-unknown-none-elf"

切换到 usr/rust 目录,就可以进行交叉编译:

  1. $ cargo build

我们将能够在 usr/rust/target/riscv64imac-unknown-none-elf/debug/hello_world 看到我们编译出来的可执行文件,接下来的问题就是如何把它加载到内核中执行了!

目前的代码可以在这里找到。