[rCore学习笔记 016]实现应用程序
写在前面
本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
GitHhub:https://github.com/WindDevil (目前啥也没有
设计方法
了解了特权级机制,实际上如果要设计一个应用程序就需要保证它符合U
模式的要求,不要去访问S
模式下的功能,那么其实现要点是:
- 应用程序的内存布局
- 应用程序发出的系统调用
具体设计
需要添加的功能
具体实现的时候也要按照设计方法中提供的要点来进行设计:
- 应用程序的内存布局
- 设置用户库的入口位置
- 配置
linker.ld
文件,设置好用户苦在ROM
中的地址
- 应用程序发出的系统调用
- 实现可以调用
ecall
的接口
- 实现可以调用
需要实现的应用程序
应用程序是单独编译单独生成ELF
文件并且裁剪为.bin
文件的.那么要对它进行调用只需要先链接进内核再由内核在合适的时机加载到内存.那么在本节介绍中需要实现:
hello_world
:在屏幕上打印一行Hello world from user mode program!
store_fault
:访问一个非法的物理地址,测试批处理系统是否会被该错误影响power
:不断在计算操作和打印字符串操作之间进行特权级切换
创建工程
保证当前位置为workspace
,
cd ¬/workspace
用cargo
创建工程,在项目目录下创建user
文件夹,用来储存 用户态 的接口和代码.:
cargo new ./user
工程文件中已经有了保存 用户库 的user/src
, 接下来创建保存 应用程序 的user/src/bin
:
mkdir user/src/bin
实现对内存布局的配置
在user/src
下创建链接文件linker.ld
.
touch linker.ld
因为应用用户层的入口地址在0x80400000
,所以要对第一章给出的linker.ld
进行修改,把BASE_ADDRESS
设置为0x80400000
:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80400000;
SECTIONS
{
. = BASE_ADDRESS;
skernel = .;
stext = .;
.text : {
*(.text.entry)
*(.text .text.*)
}
. = ALIGN(4K);
etext = .;
srodata = .;
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
. = ALIGN(4K);
erodata = .;
sdata = .;
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
. = ALIGN(4K);
edata = .;
.bss : {
*(.bss.stack)
sbss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
}
. = ALIGN(4K);
ebss = .;
ekernel = .;
/DISCARD/ : {
*(.eh_frame)
}
}
这个链接文件表达的内存布局如图所示,这里要注意 低地址 和 高地址 在图中的位置:
- 已初始化数据段保存程序中那些已初始化的全局数据,分为
.rodata
和.data
两部分。前者存放只读的全局数据,通常是一些常数或者是 常量字符串等;而后者存放可修改的全局数据。 - 未初始化数据段
.bss
保存程序中那些未初始化的全局数据,通常由程序的加载者代为进行零初始化,即将这块区域逐字节清零; - 堆 (heap)区域用来存放程序运行时动态分配的数据,如 C/C++ 中的 malloc/new 分配到的数据本体就放在堆区域,它向高地址增长;
- 栈 (stack)区域不仅用作函数调用上下文的保存与恢复,每个函数作用域内的局部变量也被编译器放在它的栈帧内,它向低地址增长。
回想我们把二进制文件刷入QEMU
的指令,实际上就是把编译好的内核放在了0x80200000
,也就是.text
段:
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios ../bootloader/rustsbi-qemu.bin \
-device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000
将 _start
所在的 .text.entry
放在整个程序的开头,也就是说批处理系统只要在加载之后跳转到 0x80400000
就已经进入了 用户库的入口点,并会在初始化之后跳转到应用程序主逻辑.
这里注意ENTRY(_start)
,是设置程序的入口,回顾第一章entry.asm
的内容,这里设置了.section .text.entry
,而在main.rs
中设置了global_asm!(include_str!("entry.asm"))
,引用了这段代码:
# os/src/entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main
.section .bss.stack
.globl boot_stack_lower_bound
boot_stack_lower_bound:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
提供了最终生成可执行文件的 .bss
段的起始和终止地址,方便 clear_bss
函数使用.
观察~/App/rCore-Tutorial-v3/user/src/linker.ld
:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80400000;
SECTIONS
{
. = BASE_ADDRESS;
.text : {
*(.text.entry)
*(.text .text.*)
}
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
.bss : {
start_bss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
end_bss = .;
}
/DISCARD/ : {
*(.eh_frame)
*(.debug*)
}
}
可以看到与我们自己按照文件构建的link.ld
文件相比,少了xxx(label) = .;
的描述和. = ALIGN(4K);
的偏移.
xxx(label) = .;
用于定义一个符号xxx
,并且把当前指针位置设置给他.
. = ALIGN(4K)
用于检查当前链接位置.
是否已经对齐到4K的边界,如果当前链接位置不是4K边界的倍数,链接器会填充足够的字节直到下一个 4K 边界.
因此可以知道,在原本的linker.ld
中,要设置一些label
,这样我们可以通过extern C
去得到这些指针从而知道编译之后的各个部分的大小.并且每次设置一个节的时候需要检查对齐4K边界.
detail (这一部分要细看尤其是DISCARD)
OUTPUT_ARCH(riscv)
:定义了目标输出架构是RISC-V。ENTRY(_start)
:指定了程序的入口点为_start
函数。这是程序开始执行的地方。BASE_ADDRESS = 0x80400000;
:设置程序的基地址为0x80400000,这是程序加载到内存中的起始地址。SECTIONS
:开始定义内存段的布局。. = BASE_ADDRESS;
:设置当前地址为基地址。skernel = .;
:记录内核段的起始地址。stext = .;
:记录文本段(代码段)的起始地址。.text
:定义文本段,包含可执行代码。*(.text.entry)
和*(.text .text.*)
表示将所有.text
和.text.*
节的内容链接到这里。. = ALIGN(4K);
:对齐当前地址到4K(4096字节)边界。etext = .;
:记录文本段的结束地址。srodata = .;
:记录只读数据段的起始地址。.rodata
:定义只读数据段,包含常量和只读数据。*(.rodata .rodata.*)
和*(.srodata .srodata.*)
表示将所有.rodata
和.srodata
节的内容链接到这里。erodata = .;
:记录只读数据段的结束地址。sdata = .;
:记录初始化数据段的起始地址。.data
:定义初始化数据段,包含已初始化的全局变量。*(.data .data.*)
和*(.sdata .sdata.*)
表示将所有.data
和.sdata
节的内容链接到这里。edata = .;
:记录初始化数据段的结束地址。.bss
:定义未初始化数据段(BSS段),包含未初始化的全局变量。*(.bss.stack)
和*(.bss .bss.*)
以及*(.sbss .sbss.*)
表示将所有.bss
、.sbss
和.bss.stack
节的内容链接到这里。sbss = .;
记录BSS段的起始地址。ebss = .;
:记录未初始化数据段的结束地址。ekernel = .;
:记录整个内核段的结束地址。/DISCARD/
:定义一个丢弃节,用于排除不需要的节,这里是指定不包含.eh_frame
节,通常这个节包含了异常处理帧信息。
对应创建系统入口和初始化函数
创建lib.rs
模块:
touch lib.rs
和os
的main.rs
一样,创建函数入口的同时,使用#[no_mangle]
保证函数名称不被优化,并且使用了一个新的宏#[link_section = ".text.entry"]
使得_start 这段代码编译后的汇编代码中放在一个名为 .text.entry 的代码段中,方便我们在后续链接的时候调整它的位置使得它能够作为用户库的入口,这里要注意我们仍然只能使用core
库,因此要使用#![no_std]
宏:
#![no_std]
#[no_mangle]
#[link_section = ".text.entry"]
pub extern "C" fn _start() -> ! {
clear_bss();
exit(main());
panic!("unreachable after sys_exit!");
}
对应第一章也需要清除.bss
段,并且使用panic!
宏:
#![feature(panic_info_message)]
fn clear_bss() {
extern "C" {
fn start_bss();
fn end_bss();
}
(start_bss as usize..end_bss as usize).for_each(|addr| unsafe {
(addr as *mut u8).write_volatile(0);
});
}
并且使用exit
接口调用main
函数,这里exit
接口在后边通过ecall
才能实现,而对于main
函数,若bin
目录下有main
符号,则程序可以正常链接,但当我们找不到main
的时候也需要有一个保障,这里就涉及了弱链接,如果找不到main
则链接这个main
:
#![feature(linkage)]
#[linkage = "weak"]
#[no_mangle]
fn main() -> i32 {
panic!("Cannot find main!");
}
创建系统调用模块
创建syscall
模块,这个文件创建在user/src
文件下:
touch syscall.rs
我们使用Rust
嵌入汇编代码调用ecall
以实现 在用户态中发起系统调用 ,ecall
的原理如下:
当一个进程执行ecall
指令时,处理器会触发一个异常,导致控制权转移到预先设定的内核异常处理程序。此时,内核可以检查引发ecall
的上下文,并根据传入的参数提供适当的服务,比如打开文件、创建进程、分配内存等。
ecall
指令本身并不携带任何参数,但是它可以访问通用寄存器中的值作为参数传递给内核。通常,以下寄存器会被用来传递参数:
x10
(a0):第一个参数x11
(a1):第二个参数x12
(a2):第三个参数x13
(a3):第四个参数x14
(a4):第五个参数x15
(a5):第六个参数x16
(a6):第七个参数x17
(a7):第八个参数,同时也作为系统调用号
那么就可以在syscall.rs
文件下创建接口:
// user/src/syscall.rs
use core::arch::asm;
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
asm!(
"ecall",
inlateout("x10") args[0] => ret,
in("x11") args[1],
in("x12") args[2],
in("x17") id
);
}
ret
}
asm!
宏的格式:
- 首先在第 6 行是我们要插入的汇编代码段本身,这里我们只插入一行
ecall
指令,不过它可以支持同时插入多条指令。 - 从第 7 行开始我们在编译器的帮助下将输入/输出变量绑定到寄存器。
- 比如第 8 行的
in("x11") args[1]
则表示将输入参数args[1]
绑定到ecall
的输入寄存器x11
即a1
中,编译器自动插入相关指令并保证在ecall
指令被执行之前寄存器a1
的值与args[1]
相同。 - 以同样的方式我们可以将输入参数
args[2]
和id
分别绑定到输入寄存器a2
和a7
中。 - 这里比较特殊的是
a0
寄存器,它同时作为输入和输出,因此我们将in
改成inlateout
,并在行末的变量部分使用{in_var} => {out_var}
的格式,其中{in_var}
和{out_var}
分别表示上下文中的输入变量和输出变量。
在本章中,应用程序和批处理系统之间按照 API 的结构,约定如下两个系统调用:
/// 功能:将内存中缓冲区中的数据写入文件。
/// 参数:`fd` 表示待写入文件的文件描述符;
/// `buf` 表示内存中缓冲区的起始地址;
/// `len` 表示内存中缓冲区的长度。
/// 返回值:返回成功写入的长度。
/// syscall ID:64
fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize;
/// 功能:退出应用程序并将返回值告知批处理系统。
/// 参数:`exit_code` 表示应用程序的返回值。
/// 返回值:该系统调用不应该返回。
/// syscall ID:93
fn sys_exit(exit_code: usize) -> !;
那么对应的,我们可以使用syscall
来实现这两个API
:
// user/src/syscall.rs
const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}
pub fn sys_exit(xstate: i32) -> isize {
syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
}
注意 sys_write
使用一个 &[u8]
切片类型来描述缓冲区,这是一个 胖指针 (Fat Pointer),里面既包含缓冲区的起始地址,还 包含缓冲区的长度。我们可以分别通过 as_ptr
和 len
方法取出它们并独立地作为实际的系统调用参数。
进一步封装接口
为了将上述两个系统调用在用户库 user_lib
中进一步封装,从而更加接近在 Linux 等平台的实际系统调用接口,修改lib.rs
文件,在其中键入:
mod syscall;
use syscall::*;
pub fn write(fd: usize, buf: &[u8]) -> isize {
sys_write(fd, buf)
}
pub fn exit(exit_code: i32) -> isize {
sys_exit(exit_code)
}
更改console
的实现
我们把 console
子模块中 Stdout::write_str
改成基于 write
的实现,且传入的 fd
参数设置为 1,它代表标准输出, 也就是输出到屏幕。目前我们不需要考虑其他的 fd
选取情况。这样,应用程序的 println!
宏借助系统调用变得可用了。
创建console.rs
文件,内容和第一章该模块保持一致,只修改Stdout
的Write
特性的实现:
// user/src/console.rs
const STDOUT: usize = 1;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
write(STDOUT, s.as_bytes());
Ok(())
}
}
最终的文件内容为:
use super::write;
use core::fmt::{self, Write};
struct Stdout;
const STDOUT: usize = 1;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
write(STDOUT, s.as_bytes());
Ok(())
}
}
pub fn print(args: fmt::Arguments) {
Stdout.write_fmt(args).unwrap();
}
#[macro_export]
macro_rules! print {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!($fmt $(, $($arg)+)?));
}
}
#[macro_export]
macro_rules! println {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
}
}
阅读三个应用程序的代码
可以通过访问~/App/rCore-Tutorial-v3
,使用git checkout ch2
,切换到第二章的代码查看三个应用的源码.并且可以将他们复制到我们工程的user/src/bin
目录下
cd ~/App/rCore-Tutorial-v3
git checkout ch2
cd user/src/bin
cp 00hello_world.rs ~/workspace/user/src/bin
cp 01store_fault.rs ~/workspace/user/src/bin
cp 02power.rs ~/workspace/user/src/bin
hello_world
#![no_std]
#![no_main]
#[macro_use]
extern crate user_lib;
#[no_mangle]
fn main() -> i32 {
println!("Hello, world!");
0
}
store_fault
#![no_std]
#![no_main]
#[macro_use]
extern crate user_lib;
#[no_mangle]
fn main() -> i32 {
println!("Into Test store_fault, we will insert an invalid store operation...");
println!("Kernel should kill this application!");
unsafe {
core::ptr::null_mut::<u8>().write_volatile(0);
}
0
}
power
#![no_std]
#![no_main]
#[macro_use]
extern crate user_lib;
const SIZE: usize = 10;
const P: u32 = 3;
const STEP: usize = 100000;
const MOD: u32 = 10007;
#[no_mangle]
fn main() -> i32 {
let mut pow = [0u32; SIZE];
let mut index: usize = 0;
pow[index] = 1;
for i in 1..=STEP {
let last = pow[index];
index = (index + 1) % SIZE;
pow[index] = last * P % MOD;
if i % 10000 == 0 {
println!("{}^{}={}(MOD {})", P, i, pow[index], MOD);
}
}
println!("Test power OK!");
0
}
编译生成应用程序二进制码
创建自动构建脚本
直接借用~/App/rCore-Tutorial-v3/user
里的脚本,在user
目录下创建Makefile
文件:
TARGET := riscv64gc-unknown-none-elf
MODE := release
APP_DIR := src/bin
TARGET_DIR := target/$(TARGET)/$(MODE)
APPS := $(wildcard $(APP_DIR)/*.rs)
ELFS := $(patsubst $(APP_DIR)/%.rs, $(TARGET_DIR)/%, $(APPS))
BINS := $(patsubst $(APP_DIR)/%.rs, $(TARGET_DIR)/%.bin, $(APPS))
OBJDUMP := rust-objdump --arch-name=riscv64
OBJCOPY := rust-objcopy --binary-architecture=riscv64
elf:
@cargo build --release
binary: elf
@$(foreach elf, $(ELFS), $(OBJCOPY) $(elf) --strip-all -O binary $(patsubst $(TARGET_DIR)/%, $(TARGET_DIR)/%.bin, $(elf));)
build: binary
解析:
- 变量定义:
TARGET
:设置为目标架构riscv64gc-unknown-none-elf
,这是 Rust 对 RISC-V 架构的支持。MODE
:设置为release
,意味着使用 Rust 的发布模式进行构建,这通常会产生优化后的二进制文件。APP_DIR
:源代码目录,这里为src/bin
,通常存放 Rust 的可执行程序源文件。TARGET_DIR
:构建输出目录,这里根据目标架构和构建模式进行构建。APPS
:使用wildcard
函数列出APP_DIR
目录下所有的.rs
文件,即 Rust 源文件。ELFS
:通过patsubst
函数转换APPS
列表,将每个源文件转换为其对应的 ELF 格式的目标文件路径。BINS
:同样使用patsubst
函数,将APPS
列表转换为.bin
格式的二进制文件路径。
- 命令工具定义:
OBJDUMP
:使用rust-objdump
工具,指定架构为riscv64
,用于查看目标文件的内部结构。OBJCOPY
:使用rust-objcopy
工具,同样指定架构为riscv64
,用于从 ELF 格式转换为其他格式(如二进制)。
- 目标定义:
elf
:这个目标调用cargo build --release
命令,使用 Cargo(Rust 的包管理器和构建工具)构建项目,生成 ELF 格式的可执行文件。binary
:这个目标依赖于elf
目标,然后遍历ELFS
列表,使用OBJCOPY
将每个 ELF 文件转换为剥离符号的二进制文件。build
:这个目标依赖于binary
目标,意味着构建过程的最终目标是生成二进制文件。
执行自动构建指令
进入user
目录,执行应用程序的自动构建指令make build
.
报错:
error[E0583]: file not found for module `lang_items`
--> src/lib.rs:7:1
|
7 | mod lang_items;
| ^^^^^^^^^^^^^^^
|
= help: to create the module `lang_items`, create file "src/lang_items.rs" or "src/lang_items/mod.rs"
= note: if there is a `mod lang_items` elsewhere in the crate already, import it with `use crate::...` instead
error: invalid register `x10`: unknown register
--> src/syscall.rs:11:13
|
11 | inlateout("x10") args[0] => ret,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: invalid register `x11`: unknown register
--> src/syscall.rs:12:13
|
12 | in("x11") args[1],
| ^^^^^^^^^^^^^^^^^
error: invalid register `x12`: unknown register
--> src/syscall.rs:13:13
|
13 | in("x12") args[2],
| ^^^^^^^^^^^^^^^^^
error: invalid register `x17`: unknown register
--> src/syscall.rs:14:13
|
14 | in("x17") id
| ^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0583`.
error: could not compile `user` (lib) due to 5 previous errors
make: *** [Makefile:13: elf] Error 101
可以看到lang_items
模块不存在,并且所有的寄存器都被标记为invalid register
.
我们同样去查看~/App/rCore-Tutorial-v3/user/src
里的实现:
#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
let err = panic_info.message().unwrap();
if let Some(location) = panic_info.location() {
println!(
"Panicked at {}:{}, {}",
location.file(),
location.line(),
err
);
} else {
println!("Panicked: {}", err);
}
loop {}
}
可以看到和第一章一样,实现了一个#[panic_handler]
注解的函数,只不过没有使用sbi
提供的shutdown
和log
提供的error!
宏,这里注意不要思维僵化,认为要使用panic!
宏就一定要实现一个名为panic
的函数,而是实现了有这个注解的函数即可.
把这个文件拷贝过来用:
cp lang_items.rs ~/workspace/user/src/
重新进行编译:
cd ~/workspace/user
make build
发现仍然要报错:
error: invalid register `x10`: unknown register
--> src/syscall.rs:11:13
|
11 | inlateout("x10") args[0] => ret,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: invalid register `x11`: unknown register
--> src/syscall.rs:12:13
|
12 | in("x11") args[1],
| ^^^^^^^^^^^^^^^^^
error: invalid register `x12`: unknown register
--> src/syscall.rs:13:13
|
13 | in("x12") args[2],
| ^^^^^^^^^^^^^^^^^
error: invalid register `x17`: unknown register
--> src/syscall.rs:14:13
|
14 | in("x17") id
| ^^^^^^^^^^^^
error: could not compile `user` (lib) due to 4 previous errors
make: *** [Makefile:13: elf] Error 101
可能是依赖文件出现问题,发现似乎没有依赖rust-sbi
,我们查看~/App/rCore-Tutorial-v3/user/Cargo.toml
,发现实际上不是rust-sbi
没有依赖,而是需要risc-v的汇编依赖:
[dependencies]
riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] }
此时重新在user
下运行make build
,仍然报错.
error: invalid register `x10`: unknown register
--> src/syscall.rs:11:13
|
11 | inlateout("x10") args[0] => ret,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: invalid register `x11`: unknown register
--> src/syscall.rs:12:13
|
12 | in("x11") args[1],
| ^^^^^^^^^^^^^^^^^
error: invalid register `x12`: unknown register
--> src/syscall.rs:13:13
|
13 | in("x12") args[2],
| ^^^^^^^^^^^^^^^^^
error: invalid register `x17`: unknown register
--> src/syscall.rs:14:13
|
14 | in("x17") id
| ^^^^^^^^^^^^
error: could not compile `user` (lib) due to 4 previous errors
make: *** [Makefile:13: elf] Error 101
这时候思考Makefile
文件中的内容,使用cargo
进行的构建,那么是不是cargo
的设置出现了问题呢,对比~/App/rCore-Tutorial-v3/user
,可以看到里边存在.cargo
文件夹,之前我们也使用过这个文件夹:
在Rust编程环境中,.cargo/config.toml
和 Cargo.toml
都是配置文件,但它们各自负责不同的任务。
.cargo/config.toml
这个文件是用来配置Rust工具链(包括Cargo)的行为的。它允许用户设置一些全局性的偏好,例如编译目标架构、编译器的默认行为(如是否开启调试信息、优化等级等)、路径到私有仓库、编译时的环境变量,以及其他各种高级配置选项,如镜像源、工具链选择等。
.cargo/config.toml
文件通常位于用户主目录下的.cargo
文件夹中,但是也可以在项目的根目录下创建一个同名文件来覆盖默认配置,这样配置就会对特定项目生效。
Cargo.toml
文件是一个项目的清单文件,它包含了关于Rust项目的重要元数据,包括项目名称、版本、作者、许可证等元信息;项目的依赖库及其版本要求;构建脚本和自定义构建依赖;项目的工作区成员,即属于同一个工作区的其他项目;特定于编译配置的特性,这些特性可以开启或关闭额外的功能;目标二进制文件、库或测试模块的定义。
简而言之,Cargo.toml
描述了项目本身的结构和需求,而.cargo/config.toml
控制了Cargo如何处理项目构建过程中的各种细节。每个Rust项目都有一个Cargo.toml
文件,而.cargo/config.toml
则是可选的,可以全局设置或在项目目录中局部覆盖。
那么我们查看这个文件内容,果然规定了编译器和链接文件:
[build]
target = "riscv64gc-unknown-none-elf"
[target.riscv64gc-unknown-none-elf]
rustflags = [
"-Clink-args=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
]
我们在/user
下建立/user/.cargo/config.toml
:
touch /user/.cargo/config.toml
把上述内容拷贝到其中,重新运行make build
:
error[E0463]: can't find crate for `user_lib`
--> src/bin/01store_fault.rs:5:1
|
5 | extern crate user_lib;
| ^^^^^^^^^^^^^^^^^^^^^^ can't find crate
error[E0463]: can't find crate for `user_lib`
--> src/bin/00hello_world.rs:5:1
|
5 | extern crate user_lib;
| ^^^^^^^^^^^^^^^^^^^^^^ can't find crate
error: cannot find macro `println` in this scope
--> src/bin/01store_fault.rs:10:5
|
10 | println!("Kernel should kill this application!");
| ^^^^^^^
|
help: consider importing this macro
|
4 + use user::println;
|
error: cannot find macro `println` in this scope
--> src/bin/01store_fault.rs:9:5
|
9 | println!("Into Test store_fault, we will insert an invalid store operation...");
| ^^^^^^^
|
help: consider importing this macro
|
4 + use user::println;
|
error: `#[panic_handler]` function required, but not found
error: cannot find macro `println` in this scope
--> src/bin/00hello_world.rs:9:5
|
9 | println!("Hello, world!");
| ^^^^^^^
|
help: consider importing this macro
|
4 + use user::println;
|
For more information about this error, try `rustc --explain E0463`.
error: could not compile `user` (bin "01store_fault") due to 4 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `user` (bin "00hello_world") due to 3 previous errors
make: *** [Makefile:13: elf] Error 101
仍旧报错,发现要求依赖的包user_lib
是不存在的,而且println
是因为缺少这个依赖导致的.这里查看官方文档:
这个外部库其实就是 user
目录下的 lib.rs
以及它引用的若干子模块中。至于这个外部库为何叫 user_lib
而不叫 lib.rs
所在的目录的名字 user
,是因为在 user/Cargo.toml
中我们对于库的名字进行了设置: name = "user_lib"
。它作为 bin
目录下的源程序所依赖的用户库,等价于其他编程语言提供的标准库。
那么我们把user/Cargo.toml
里的name
进行修改, name = "user_lib"
,再进行make build
,没有报错.
在~/workspace/user/target/riscv64gc-unknown-none-elf/release
中可以找到,00hello_world
,01store_fault
,02power
.
实现操作系统前执行应用程序
使用qemu-riscv64
可以直接用用户态模拟器直接执行应用程序而不需要os
.
到~/workspace/user/target/riscv64gc-unknown-none-elf/release
文件夹下,尝试执行qemu-riscv64 ./00hello_world
.
发现报错:
Command 'qemu-riscv64' not found, but can be installed with:
sudo apt install qemu-user
尝试安装:
sudo apt install qemu-user
安装成功后使用:
qemu-riscv64 ./00hello_world
- Hello, world!
qemu-riscv64 ./01store_fault
- Into Test store_fault, we will insert an invalid store operation...
- Kernel should kill this application!
- Segmentation fault (core dumped)
qemu-riscv64 ./02power
- 3^10000=5079(MOD 10007)
- 3^20000=8202(MOD 10007)
- 3^30000=8824(MOD 10007)
- 3^40000=5750(MOD 10007)
- 3^50000=3824(MOD 10007)
- 3^60000=8516(MOD 10007)
- 3^70000=2510(MOD 10007)
- 3^80000=9379(MOD 10007)
- 3^90000=2621(MOD 10007)
- 3^100000=2749(MOD 10007)
- Test power OK!
可以看到执行00hello_world
之后,在用户态可以正常执行,在执行01store_fault
之后,因为尝试在空指针中写入0
,因此出现了报错.
执行02power
之后运行了用户态的计算和println!
,而println!
实际上调用了write
,调用了syscall
,可以看到反复在用户态和内核态转换也是可行的.
根据官方文档中的描述,还有两个app
可以进行编译运行:03priv_inst
和04priv_csr
,分别验证"用户态应用直接触发从用户态到内核态的异常的原因"的两种情况:
- 指令本身属于高特权级的指令,如
sret
指令(表示从 S 模式返回到 U 模式) - 指令访问了 S模式特权级下才能访问的寄存器 或内存,如表示S模式系统状态的 控制状态寄存器
sstatus
等
这时候同样拷贝这两个app
的源码:
cp 03priv_inst.rs ~/workspace/user/src/bin/
cp 04priv_csr.rs ~/workspace/user/src/bin/
编译make build
,并且运行:
cd ~/workspace/user
make build
cd ~/workspace/user/target/riscv64gc-unknown-none-elf/release
qemu-riscv64 03priv_inst
- Try to execute privileged instruction in U Mode
- Kernel should kill this application!
- Illegal instruction (core dumped)
qemu-riscv64 04priv_csr
- Try to access privileged CSR in U Mode
- Kernel should kill this application!
- Illegal instruction (core dumped)