From 2d72091c0d51be2717684f19a5dc90de8afa5638 Mon Sep 17 00:00:00 2001 From: GZTime Date: Sat, 24 Feb 2024 15:43:20 +0800 Subject: [PATCH] =?UTF-8?q?Lab=204=EF=BC=9A=E7=94=A8=E6=88=B7=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E4=B8=8E=E7=B3=BB=E7=BB=9F=E8=B0=83=E7=94=A8=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zhe Tang --- docs/general/coding_convention.md | 72 -- docs/general/specification.md | 96 ++ docs/labs/0x00/index.md | 2 +- docs/labs/0x01/index.md | 2 +- docs/labs/0x01/tasks.md | 2 +- docs/labs/0x02/index.md | 2 +- docs/labs/0x02/tasks.md | 58 +- docs/labs/0x03/index.md | 2 +- docs/labs/0x03/tasks.md | 40 +- docs/labs/0x04/index.md | 5 +- docs/labs/0x04/tasks.md | 972 ++++++++++++++++++ docs/labs/0x05/index.md | 2 +- docs/labs/0x05/tasks.md | 4 + docs/labs/0x06/index.md | 2 +- docs/labs/0x07/index.md | 5 +- docs/wiki/apic.md | 2 +- docs/wiki/userspace.md | 101 ++ mkdocs.yml | 3 +- src/0x02/pkg/kernel/Cargo.toml | 1 + src/0x03/pkg/kernel/Cargo.toml | 1 + src/0x03/pkg/kernel/src/proc/mod.rs | 4 +- src/0x03/pkg/kernel/src/utils/func.rs | 2 +- src/0x04/Cargo.toml | 18 + src/0x04/Makefile | 102 ++ src/0x04/pkg/app/.cargo/config.toml | 6 + src/0x04/pkg/app/config/app.ld | 30 + .../pkg/app/config/x86_64-unknown-ysos.json | 18 + src/0x04/pkg/app/hello/Cargo.toml | 7 + src/0x04/pkg/app/hello/src/main.rs | 14 + .../pkg/kernel/src/interrupt/syscall/mod.rs | 95 ++ .../kernel/src/interrupt/syscall/service.rs | 73 ++ src/0x04/pkg/kernel/src/main.rs | 23 + src/0x04/pkg/kernel/src/memory/user.rs | 36 + src/0x04/pkg/kernel/src/utils/resource.rs | 46 + src/0x04/pkg/lib/Cargo.toml | 7 + src/0x04/pkg/lib/src/allocator.rs | 19 + src/0x04/pkg/lib/src/io.rs | 55 + src/0x04/pkg/lib/src/lib.rs | 56 + src/0x04/pkg/lib/src/macros.rs | 37 + src/0x04/pkg/lib/src/syscall.rs | 70 ++ src/0x04/pkg/syscall/Cargo.toml | 7 + src/0x04/pkg/syscall/src/lib.rs | 24 + src/0x04/pkg/syscall/src/macros.rs | 73 ++ 43 files changed, 2072 insertions(+), 124 deletions(-) delete mode 100644 docs/general/coding_convention.md create mode 100644 docs/general/specification.md create mode 100644 docs/wiki/userspace.md create mode 100644 src/0x04/Cargo.toml create mode 100644 src/0x04/Makefile create mode 100644 src/0x04/pkg/app/.cargo/config.toml create mode 100644 src/0x04/pkg/app/config/app.ld create mode 100644 src/0x04/pkg/app/config/x86_64-unknown-ysos.json create mode 100644 src/0x04/pkg/app/hello/Cargo.toml create mode 100644 src/0x04/pkg/app/hello/src/main.rs create mode 100644 src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs create mode 100644 src/0x04/pkg/kernel/src/interrupt/syscall/service.rs create mode 100644 src/0x04/pkg/kernel/src/main.rs create mode 100644 src/0x04/pkg/kernel/src/memory/user.rs create mode 100644 src/0x04/pkg/kernel/src/utils/resource.rs create mode 100644 src/0x04/pkg/lib/Cargo.toml create mode 100644 src/0x04/pkg/lib/src/allocator.rs create mode 100644 src/0x04/pkg/lib/src/io.rs create mode 100644 src/0x04/pkg/lib/src/lib.rs create mode 100644 src/0x04/pkg/lib/src/macros.rs create mode 100644 src/0x04/pkg/lib/src/syscall.rs create mode 100644 src/0x04/pkg/syscall/Cargo.toml create mode 100644 src/0x04/pkg/syscall/src/lib.rs create mode 100644 src/0x04/pkg/syscall/src/macros.rs diff --git a/docs/general/coding_convention.md b/docs/general/coding_convention.md deleted file mode 100644 index 26645e5..0000000 --- a/docs/general/coding_convention.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -no_comments: true ---- - -# 代码规范 - -!!! tip "部分内容引用修改自 [代码规范](https://lab.cs.tsinghua.edu.cn/rust/projects/coding_convention/)" - -本页面列举了一些常见的代码规范要求。 - -部分要求并不强制,但是建议尽量遵守和学习。 - -## Git 相关 - -=== "提交历史" - - - 每个提交都应该有一定的意义,例如实现了新功能,修复了一个问题,定义了新的函数; - - 比较复杂的程序,要边开发边提交,而不是写完了再一次性提交; - - 不强求线性历史,**但是不允许使用 force push**。 - -=== "提交消息" - - - 简单明了地描述这个提交的内容; - - 建议用英文写,用中文写也可以; - - 不要编写的过于详细或过于简略; - - 可以采用一些格式,例如 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#examples); - - 不掺杂个人情绪; - - 可以添加一些 Emoji,[gitmoji](https://gitmoji.dev/) 为提交说明中使用的 Emoji 提出了一些建议,可以参考。 - -## 代码风格 - -=== "简洁" - - 保证代码的简洁: - - - 有整齐的缩进,建议用空格缩进而非 tab,两个空格或者四个空格作为一级缩进都可以; - - 每一行不要有太多字符,例如不超过 80 - 100 个字符; - -=== "注释" - - 在代码中编写适当的注释: - - - 在比较复杂的代码块前,描述代码的功能; - - 过于简单的代码,一般不写注释; - - 函数一般要编写注释,包括其功能,参数和输出; - - 建议用英文,中文也可以,但是注意要用 UTF-8 编码。 - - 遵循 [Rustdoc 的约定](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html) - -=== "空白" - - 代码中应该有适当的空格和空行: - - - 函数中,实现不同功能的代码间需要添加空行; - - 操作符前后应该有空格,例如 `c = a + b`; - - 保持前后一致的风格。 - -=== "命名" - - 变量的命名,应该符合一定的规范: - - - 符合 Rust 编译器建议的变量命名规范; - - 尽量用英文单词,而不是中文拼音首字母; - - 命名与上下文要相关; - - 不用类似 `a, b, c, d` 的命名方式。 - - -建议使用工具来辅助代码风格的检查: - -- 使用 `rustfmt`,`cargo fmt --all` 命令来格式化代码; -- 使用 `clippy`,`cargo clippy` 来检查代码风格。 - -!!! note "请注意,由于项目 target 不尽相同,`clippy` 需要在每一个 `package` 下使用。" diff --git a/docs/general/specification.md b/docs/general/specification.md new file mode 100644 index 0000000..77ae3bc --- /dev/null +++ b/docs/general/specification.md @@ -0,0 +1,96 @@ +--- +no_comments: true +--- + +# 代码与提交规范 + +!!! tip "部分内容引用修改自 [代码规范](https://lab.cs.tsinghua.edu.cn/rust/projects/coding_convention/)" + +本页面列举了一些常见的代码规范要求。部分要求并不强制,但是建议尽量遵守和学习。 + +## 代码风格 + +=== "简洁" + + 保证代码的简洁: + + - 有整齐的缩进,建议用空格缩进而非 tab,两个空格或者四个空格作为一级缩进都可以; + - 每一行不要有太多字符,例如不超过 80 - 100 个字符; + +=== "注释" + + 在代码中编写适当的注释: + + - 在比较复杂的代码块前,描述代码的功能; + - 过于简单的代码,一般不写注释; + - 函数一般要编写注释,包括其功能,参数和输出; + - 建议用英文,中文也可以,但是注意要用 UTF-8 编码。 + - 遵循 [Rustdoc 的约定](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html) + +=== "空白" + + 代码中应该有适当的空格和空行: + + - 函数中,实现不同功能的代码间需要添加空行; + - 操作符前后应该有空格,例如 `c = a + b`; + - 保持前后一致的风格。 + +=== "命名" + + 变量的命名,应该符合一定的规范: + + - 符合 Rust 编译器建议的变量命名规范; + - 尽量用英文单词,而不是中文拼音首字母; + - 命名与上下文要相关; + - 不用类似 `a, b, c, d` 的命名方式。 + +建议使用工具来辅助代码风格的检查: + +- 使用 `rustfmt`,`cargo fmt --all` 命令来格式化代码; +- 使用 `clippy`,`cargo clippy` 来检查代码风格。 +- 使用 [typos](https://github.com/crate-ci/typos) 检查拼写错误,可以使用 `cargo install typos-cli` 安装。 + +!!! note "请注意,由于项目 target 不尽相同,`clippy` 需要在每一个 `package` 下使用。" + +## Git 相关 + +### 提交历史 + +- 每个提交都应该有一定的意义,例如实现了新功能,修复了一个问题,定义了新的函数; +- 比较复杂的程序,要边开发边提交,而不是写完了再一次性提交; +- 不强求线性历史,**但是不允许使用 force push**。 + +### 提交消息 + +- 简单明了地描述这个提交的内容; +- 建议用英文写,用中文写也可以; +- 不要编写的过于详细或过于简略; +- 可以采用一些格式,例如 [**Conventional Commits**](https://www.conventionalcommits.org/en/v1.0.0/#examples); +- 不掺杂个人情绪; +- 可以添加一些 Emoji,[gitmoji](https://gitmoji.dev/) 为提交说明中使用的 Emoji 提出了一些建议,可以参考。 + +### 代码打包 + +在进行代码提交时,使用如下命令将代码打包: + +```bash +git archive --format zip -o ../lab1.zip v0.1 +``` + +`git` 会遵守 `.gitignore` 文件中的规则,不会将不必要的文件打包。从而无需手动删除不必要的文件,或者频繁使用 `cargo clean` 等命令。 + +**在期末最后的实验报告提交时,需要提交整个代码仓库**,你可以使用如下命令进行仓库的打包,将 `00000000` 位置替换为你的学号: + +```bash +git bundle create ../ysos-00000000.bundle --all +``` + +??? note "给 TA 的注释" + + 可以直接使用 `git clone` 查看 `bundle` 文件: + + ```bash + git clone ysos-00000000.bundle /path/to/repo + ``` + + 之后可以直接在 `/path/to/repo` 目录下查看提交记录、开发时间等内容。 diff --git a/docs/labs/0x00/index.md b/docs/labs/0x00/index.md index 62c7b86..5b55be5 100644 --- a/docs/labs/0x00/index.md +++ b/docs/labs/0x00/index.md @@ -32,7 +32,7 @@ 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/labs/0x01/index.md b/docs/labs/0x01/index.md index 843e738..b4f9ccb 100644 --- a/docs/labs/0x01/index.md +++ b/docs/labs/0x01/index.md @@ -30,7 +30,7 @@ 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/labs/0x01/tasks.md b/docs/labs/0x01/tasks.md index 07fd26f..d86ef07 100644 --- a/docs/labs/0x01/tasks.md +++ b/docs/labs/0x01/tasks.md @@ -33,7 +33,7 @@ ```json { "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128", "linker-flavor": "ld.lld", "target-endian": "little", "target-pointer-width": "64", diff --git a/docs/labs/0x02/index.md b/docs/labs/0x02/index.md index d902ecf..1ce2c95 100644 --- a/docs/labs/0x02/index.md +++ b/docs/labs/0x02/index.md @@ -31,7 +31,7 @@ 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/labs/0x02/tasks.md b/docs/labs/0x02/tasks.md index c5f01d9..46bc016 100644 --- a/docs/labs/0x02/tasks.md +++ b/docs/labs/0x02/tasks.md @@ -496,7 +496,7 @@ x86_64::instructions::interrupts::enable(); 遵循 I/O 中断处理的 Top half & Bottom half 原则,在中断发生时,仅仅在中断处理中做尽量少的事:读取串口的输入,并将其放入缓冲区。而在中断处理程序之外,选择合适的时机,从缓冲区中读取数据,并进行处理。 -为了开启串口设备的中断,你需要参考如下代码,在 `src/drivers/uart16550.rs` 的 `init` 函数末尾为串口设备开启中断: +为了开启串口设备的中断,你需要参考如下 C 语言代码,在 `src/drivers/uart16550.rs` 的 `init` 函数末尾为串口设备开启中断: ```c #define PORT 0x3f8 // COM1 @@ -511,48 +511,56 @@ static int init_serial() { } ``` -为了承接全部(可能的)用户输入数据,并将它们统一在标准输入,需要为输入准备缓冲区,并将其封装为一个驱动,创建 `src/drivers/input.rs` 文件,并借助 `alloc`、`crossbeam_queue` 等 crate 实现一个输入缓冲区。 - -??? note "`crossbeam_queue` 的使用提示" - - `crossbeam_queue` 默认依赖于 `std`,可以使用如下方式引用: - - ```toml - [dependencies] - crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] } - ``` +为了承接全部(可能的)用户输入数据,并将它们统一在标准输入,需要为输入准备缓冲区,并将其封装为一个驱动,创建 `src/drivers/input.rs` 文件,使用 `crossbeam_queue` crate 实现一个无锁输入缓冲区。 !!! tip "在 memory 初始化的过程中,你已经有了内核堆分配的能力,可以动态分配内存。" 按照下列描述,补全 `src/drivers/input.rs` 驱动代码: -1. 使用 `crossbeam_queue::ArrayQueue` 存储用户输入的数据。 - - 借助 `once_mutex!` 和 `guard_access_fn!` 宏,构造一个上锁的全局静态变量 `INPUT_BUFFER`。 +1. 使用你喜欢的数据结构存储用户输入的数据。 此缓冲区大小和存储的数据类型由你自行决定,一个参考的缓冲区大小为 128。 -2. 实现并暴露 `init` 函数。 + 推荐使用 `crossbeam_queue::ArrayQueue` 作为缓冲区的实现,它是一个无锁的、固定大小的队列,可以在多线程环境下安全地进行读写操作。 - 初始化 `INPUT_BUFFER`,完成后输出日志:`Input Initialized.` 并在在 `src/lib.rs` 中调用它,在操作系统启动时进行。 + ??? example "`crossbeam_queue` 的使用提示" - 请注意:`ysos_kernel::init` 函数中组件的初始化存在顺序,各种组件间可能存在**依赖关系**。由于输入缓冲区初始化是动态分配内存,因此需要在 `memory` 模块初始化之后,才能进行初始化。 + `crossbeam_queue` 默认依赖于 `std`,可以使用如下方式引用: -3. 实现并暴露 `push_key` 函数。 + ```toml + [dependencies] + crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] } + ``` - 按照你所定义的类型,对 `INPUT_BUFFER` 上锁后,将数据放入缓冲区。若缓冲区已满,则丢弃数据,并使用 `warn!` 宏输出相关日志。 +2. 处理数据结构的初始化,暴露基本功能。 -4. 实现并暴露 `try_pop_key` 函数。 + 初始化 `INPUT_BUFFER`,你可以直接使用 `lazy_static` 初始化: - 从缓冲区中**非阻塞**取出数据。若缓冲区为空或上锁失败,则返回 `None`。 + ```rust + type Key = /* your input type */; - *Note: 或许需要在这一过程中暂时关闭中断。* + lazy_static! { + static ref INPUT_BUF: ArrayQueue = ArrayQueue::new(128); + } + + #[inline] + pub fn push_key(key: Key) { + if INPUT_BUF.push(key).is_err() { + warn!("Input buffer is full. Dropping key '{:?}'", key); + } + } + + #[inline] + pub fn try_get_key() -> Option { + INPUT_BUF.pop() + } + ``` -5. 实现并暴露 `pop_key` 函数。 +3. 实现并暴露 `pop_key` 函数。 - 利用 `try_pop_key` 函数,从缓冲区中**阻塞**取出数据。循环等待,直到缓冲区中有数据。 + 利用 `try_pop_key` 函数,从缓冲区中**阻塞**取出数据:循环等待,直到缓冲区中有数据,并返回获取到的数据。 -6. 实现并暴露 `get_line` 函数。 +4. 实现并暴露 `get_line` 函数。 从缓冲区中**阻塞**取出数据,并将其实时打印出来。直到遇到换行符 `\n`。将数据转换为 `String` 类型,并返回。 diff --git a/docs/labs/0x03/index.md b/docs/labs/0x03/index.md index 071f097..f517221 100644 --- a/docs/labs/0x03/index.md +++ b/docs/labs/0x03/index.md @@ -24,7 +24,7 @@ 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/labs/0x03/tasks.md b/docs/labs/0x03/tasks.md index f7702cb..1b9fad8 100644 --- a/docs/labs/0x03/tasks.md +++ b/docs/labs/0x03/tasks.md @@ -14,7 +14,17 @@ 在不同的操作系统中,进程模型的设计各有千秋。在 macOS xnu 和 Windows NT 内核中,线程是调度执行的基本单位,进程是资源分配的基本单位,线程通过共享进程所描述的一系列资源来互相协作。而在 Linux 内核中,没有线程的相关概念,而进程也同时是调度执行的基本单位,通过一些特殊的机制来实现进程间的协作和资源共享。有关进程模型的区别、设计在理论课上有详细的讲解,这里不再赘述。 -进程的实现和调度是(抢占式)操作系统的核心,在本实验所要实现的操作系统中将采用和 Linux 设计相近的进程模型设计理念,即进程也是被调度的单位。下图是本实验的进程模型设计示意图。**实验并不会一次将它完全实现,很多部分将在未来的实验中逐步补全,在这里希望大家能有一个整体性的理解,并在实现的过程中明确自己当前在做什么**: +进程的实现和调度是(抢占式)操作系统的核心,在本实验所要实现的操作系统中将采用和 Linux 设计相近的进程模型设计理念,即进程也是被调度的单位。下图是本实验的进程模型设计示意图。 + +**实验并不会一次将它完全实现,很多部分将在未来的实验中逐步补全,在这里希望大家能有一个整体性的理解,并在实现的过程中明确自己当前在做什么**: + +!!! warning "实验须知" + + 1. 本次实验为后续实验的核心基础,是个难啃的骨头! + + 2. 强烈建议在进行本次实验前,先阅读完本次实验的所有文档,了解本次实验的实验内容。 + + 3. 推荐根据给出的思维导图,将给出的代码浏览一遍,对代码结构有一个整体性的认识。 ![](../assets/proc.png) @@ -328,7 +338,7 @@ pub const STACK_START_MASK: u64 = !(STACK_MAX_SIZE - 1); pub const STACK_DEF_PAGE: u64 = 1; pub const STACK_DEF_SIZE: u64 = STACK_DEF_PAGE * PAGE_SIZE; -pub const STACT_INIT_BOT: u64 = STACK_MAX - STACK_DEF_SIZE; +pub const STACK_INIT_BOT: u64 = STACK_MAX - STACK_DEF_SIZE; pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; ``` @@ -336,7 +346,7 @@ pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; `STACK_DEF_PAGE` 定义了栈的默认大小,这里定义为 1 个 4KiB 的页面。 -`STACT_INIT_BOT` 定义了初始化栈的底部地址,它是栈的最大地址减去默认大小。 +`STACK_INIT_BOT` 定义了初始化栈的底部地址,它是栈的最大地址减去默认大小。 `STACK_INIT_TOP` 定义了初始化栈的顶部地址,它是栈的最大地址减去 8,这是为了进行内存对齐,保证 `rsp` 和 `rbp` 寄存器总是 8 的倍数。 @@ -356,6 +366,8 @@ pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; +---------------------+ ``` +!!! tip "**以 PID 2 为例:
初始化分配的栈的页面为 `0x3FFEFFFFF000` 到 `0x3FFF00000000`
默认栈顶地址为 `0x3FFEFFFFFFF8`**" + 有关于用户进程的其他部分内存布局的说明,将在下一次实验中详细讨论。 !!! note "在了解了内存布局的设计目的之后,或许你可以自己设计内存布局……" @@ -417,9 +429,9 @@ pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; 调用 `Process::new` 函数,创建内核进程,它会返回一个 `Process` 的智能指针。 - - 在上述的假设中,实验使用 `0` 表示无进程(正在运行),因此内核进程获取的 PID 为 `1`。 + - 在上述的假设中,实验使用 `0` 表示无进程(正在运行),内核进程的 PID 应为 `1`。 - 内核进程没有父进程,可以直接传入 `None`。 - - 内核进程的页表就是当前 `Cr3` 寄存器的内容,使用 `PageTableContext::new()` 进行加载。 + - 内核进程的页表就是当前 `Cr3` 寄存器的内容,使用 `PageTableContext::new()` 加载。 3. 内核进程的初始化状态 @@ -445,7 +457,17 @@ pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; 4. 你可以通过 `drop` 函数在合适的时候主动释放取得的锁。 5. 你可以通过 `print_process_list` 函数,在合适的情况下打印进程列表,或降低时钟中断的触发频率,从而便于调试。 -!!! tip "阶段性成果" +!!! question "如何获得一个关闭中断的上下文?" + + 可以使用 `without_interrupts` 函数来处理中断的开关,它接受一个闭包作为参数,这个闭包中的代码将会在关闭中断的情况下执行。 + + ```rust + x86_64::instructions::interrupts::without_interrupts(|| { + // do something + }) + ``` + +!!! success "阶段性成果" 在成功实现进程调度后,你应当可以观察到内核进程不断被调度,并继续执行的情况。 @@ -510,7 +532,7 @@ pub fn new_test_thread(id: &str) -> ProcessId { 最后,将全新创建的进程放入进程管理器和就绪队列中,使其能够被调度执行。 -!!! tip "阶段性成果" +!!! success "阶段性成果" 在成功实现内核线程的创建后,尝试在 `kernel_main` 中使用 `test` 命令来创建多个内核线程,它们应当被并发地调度执行。 @@ -605,7 +627,7 @@ pub extern "x86-interrupt" fn page_fault_handler( 3. 存储进程的返回值,以便其他进程可以利用它来查询进程的退出状态。 -!!! tip "阶段性成果" +!!! success "阶段性成果" 在成功实现缺页异常的处理后,尝试在 `kernel_main` 中使用 `stack` 命令来创建一个栈使用很大的内核线程,而它应当被正确地处理,不会导致进程的崩溃。 @@ -618,5 +640,3 @@ pub extern "x86-interrupt" fn page_fault_handler( 2. 在 `src/proc/process.rs` 中,有两次实现 `Deref` 和一次实现 `DerefMut` 的代码,它们分别是为了什么?使用这种方式提供了什么便利? 3. 中断的处理过程默认是不切换栈的,即在中断发生前的栈上继续处理中断过程,为什么在处理**缺页异常和时钟中断**时需要切换栈?如果不为它们切换栈会分别带来哪些问题?请假设具体的场景、或通过实际尝试进行回答。 - -## 加分项 diff --git a/docs/labs/0x04/index.md b/docs/labs/0x04/index.md index 0e2fc4e..cfd864f 100644 --- a/docs/labs/0x04/index.md +++ b/docs/labs/0x04/index.md @@ -1,6 +1,6 @@ # 实验四:用户程序与系统调用 -!!! tip "
 by" +!!! tip "精巧的机械齿轮配合无声的引擎,构筑为处理万物而生的机械。
 by" ## 实验目的 @@ -23,12 +23,13 @@ 对于本次实验内容,你需要参考学习如下实验资料: +- [用户空间](../../wiki/userspace.md) ## 实验任务与要求 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/labs/0x04/tasks.md b/docs/labs/0x04/tasks.md index e1acf4d..7d39616 100644 --- a/docs/labs/0x04/tasks.md +++ b/docs/labs/0x04/tasks.md @@ -1 +1,973 @@ # 实验四:用户程序与系统调用 + +!!! danger "在执行每一条命令前,请你对将要进行的操作进行思考" + + **为了你的数据安全和不必要的麻烦,请谨慎使用 `sudo`,并确保你了解每一条指令的含义。** + + **1. 实验文档给出的命令不需要全部执行** + + **2. 不是所有的命令都可以无条件执行** + + **3. 不要直接复制粘贴命令执行** + +## 合并实验代码 + +!!! tip "如何使用本次参考代码" + + 本次给出的参考代码为**增量补充**,即在上一次实验的基础上进行修改和补充。因此,你需要将本次参考代码与上一次实验的代码进行合并。 + + 文件的目录与上一次实验相同,因此你可以直接将本次参考代码的 `src` 目录下的文件复制到上一次实验的目录结构下,覆盖同名文件。 + + 合并后的代码并不能直接运行,你需要基于合并后的代码、按照文档进行修改补充,才能逐步实现本次实验的功能。 + +!!! warning "项目组织说明" + + 1. 本次实验中给出的均为**参考代码片段**,所有**代码内容、代码结构**均可按照需要自行调整。 + 2. 部分代码的使用**需要自行补全 `lib.rs` 和 `mod.rs` 等文件**,你也可以**添加、修改任何所需函数**。 + 3. 功能在逐步实现的过程中,部分未使用代码可以进行注释以通过编译检查。 + +在 `pkg/app` 中,定义提供了一些用户程序,这些程序将会在编译后提供给内核加载运行。 + +在 `pkg/syscall` 中,提供系统调用号和调用约束的定义,将会在内核和用户库中使用,在下文中会详细介绍。 + +在 `pkg/lib` 中,定义了用户态库并提供了一些基础实现,相关内容在下文中会详细介绍。 + +在 `pkg/kernel` 中,添加了如下一些模块: + +- `interrupt/syscall`:定义系统调用及其服务的实现。 +- `memory/user`:用户堆内存分配的实现,会被用在系统调用的处理中,将用户态的内存分配委托给内核。 +- `utils/resource`:定义了用于进行 I/O 操作的 `Resource` 结构体,用于处理用户态的读写系统调用。 + +!!! tip "别忘了更新 `Cargo.toml`" + +## 用户程序 + +### 编译用户程序 + +对于不同的运行环境,即使指令集相同,一个可执行的程序仍然有一定的差异。 + +与内核的编译类似,在 `pkg/app/config` 中,定义了用户程序的编译目标,并定义了相关的 LD 链接脚本。 + +在 `Cargo.toml` 中,使用通配符引用了 `pkg/app` 中的所有用户程序。相关的编译过程在先前给出的编译脚本中均已定义,可以直接编译。 + +通常而言,用户程序并不直接自行处理系统调用,而是由用户态库提供的函数进行调用。 + +在编写 C 语言时 `printf`、`scanf` 等函数并不是直接调用系统调用,以 gcc on Linux 的一般行为为例,这些函数被定义在 `glibc` 中,而 `glibc` 会处理系统调用。相对应的,在 Windows 上,也会存在 `msvcrt` (Microsoft Visual C Run-time) 等库。 + +为了让用户态程序更好地与 YSOS 进行交互,处理程序的生命周期,便于编写用户程序等,需要提供用户态库,以便用户程序调用。 + +用户态库被定义在 `pkg/lib` 中,在用户程序中,编辑 `Cargo.toml`,使用如下方式引用用户库: + +```rust +[dependencies] +lib = { path="../../lib", package="yslib"} +``` + +一个简单的用户程序示例如下所示,同样存在于 `app/hello/src/main.rs` 中: + +```rust +#![no_std] +#![no_main] + +use lib::*; + +extern crate lib; + +fn main() -> usize { + println!("Hello, world!!!"); + + 233 +} + +entry!(main); +``` + +- `#![no_std]` 表示不使用标准库,rust 并没有支持 YSOS 的标准库,需要我们自行实现。 +- `#![no_main]` 表示不使用标准的 `main` 函数入口,而是使用 `entry!` 宏定义的入口函数。 + +`entry!` 宏的定义如下: + +```rust +#[macro_export] +macro_rules! entry { + ($fn:ident) => { + #[export_name = "_start"] + pub extern "C" fn __impl_start() { + let ret = $fn(); + // FIXME: after syscall, add lib::sys_exit(ret); + loop {} + } + }; +} +``` + +在 `__impl_start` 函数中,调用用户程序的 `main` 函数,并在用户程序退出后,进入死循环。在后续完善了进程退出的系统调用后,你需要将 `FIXME` 的部分替换为正确的系统调用。 + +!!! note "关于 `libc` 的处理" + + 在 Linux 中,一个正常的用户程序在编译后也不会直接执行 `main` 函数,而是执行 `_start` 函数,这个函数会通过调用 `__libc_start_main`,最终通过 `__libc_stop_main`、`__exit` 等一系列函数,准备好应用程序需要执行的环境,并在程序退出后进行一些后续的工作。 + +!!! success "阶段性成果" + + 在一切配置顺利之后,应当可以使用 `cargo build` 在用户程序目录中正确地编译用户程序。 + +### 加载程序文件 + +在成功编译了用户程序后,用户程序将被脚本移动到 `esp/APP` 目录下,并**以文件夹命名**。辅助脚本 `ysos.py` 功能已完备,不过 `Makefile` 需要根据此次实验进行更新。 + +> 由于 FAT16 文件系统的限制,文件名长度不能超过 8 个字符,所以建议使用简短的文件夹名。 + +!!! note "关于用户程序的命名" + + 由于灵活性限制,`Makefile` 被设计为**只能将包名(`Cargo.toml` 中的 `package.name`)为 `ysos_$$app` 且文件夹名称为 `$$app`** 的用户程序复制到 `esp/APP` 目录下,其他的命名将会面临错误。 + + 而 `ysos.py` 脚本则会尝试读取 `Cargo.toml` 的配置,根据**对应名称**的直接复制到 `esp/APP` 目录下的**文件夹名称**,因此比 `Makefile` 更加灵活。 + + 如果你有其他需求,也可以自行修改辅助脚本,使得编译流程的正确执行。 + +目前的内核尚不具备访问磁盘和文件系统,并将它们读取加载的能力(将会在实验六中实现),因此需要另辟蹊径:在 bootloader 中将符合条件的用户程序加载到内存中,并将它们交给内核,用于生成用户进程。 + +!!! note "修改内核配置文件" + + 这就是 lab 1 中 `Config` 含有 `load_apps` 的原因。 + + 本次实验你应当在 `pkg/kernel/config/boot.conf` 中,将 `load_apps` 设置为 `true`。 + +为了存储用户程序的相关信息,在 `pkg\boot\src\lib.rs` 中,定义一个 `App` 结构体,并添加“已加载的用户程序”字段到 `BootInfo` 结构体中: + +```rust +use arrayvec::{ArrayString, ArrayVec}; + +/// App information +pub struct App<'a> { + /// The name of app + pub name: ArrayString<16>, + /// The ELF file + pub elf: ElfFile<'a>, +} + +pub type AppList = ArrayVec, 16>; + +/// This structure represents the information that the bootloader passes to the kernel. +pub struct BootInfo { + // ... + // Loaded apps + pub loaded_apps: Option, +} +``` + +!!! tip "更好的类型声明?" + + - 使用 `const` 指定用户程序数组的最大长度。 + - 尝试定义 `AppListRef` 类型,用于存储 `loaded_apps.as_ref()` 的返回值类型,可以只关心 `'static` 生命周期。 + - 抛弃 `App` 类型的生命周期,直接声明 `ElfFile<'static>`。 + +之后,在 `pkg/boot/src/fs.rs` 中,创建函数 `load_apps` 用于加载用户程序,并参考 `fs.rs` 中的其他函数,处理文件系统相关逻辑,补全代码: + +```rust +/// Load apps into memory, when no fs implemented in kernel +/// +/// List all file under "APP" and load them. +pub fn load_apps(bs: &BootServices) -> AppList { + let mut root = open_root(bs); + let mut buf = [0; 8]; + let cstr_path = uefi::CStr16::from_str_with_buf("\\APP\\", &mut buf).unwrap(); + + let mut handle = { /* FIXME: get handle for \APP\ dir */ }; + + let mut apps = ArrayVec::new(); + let mut entry_buf = [0u8; 0x100]; + + loop { + let info = handle + .read_entry(&mut entry_buf) + .expect("Failed to read entry"); + + match info { + Some(entry) => { + let file = { /* FIXME: get handle for app binary file */ }; + + if file.is_directory().unwrap_or(true) { + continue; + } + + let elf = { + // FIXME: load file with `load_file` function + // FIXME: convert file to `ElfFile` + }; + + let mut name = ArrayString::<16>::new(); + entry.file_name().as_str_in_buf(&mut name).unwrap(); + + apps.push(App { name, elf }); + } + None => break, + } + } + + info!("Loaded {} apps", apps.len()); + + apps +} +``` + +在 `boot/src/main.rs` 中,`main` 函数中加载好内核的 `ElfFile` 之后,根据配置选项按需加载用户程序,并将其信息传递给内核: + +```rust +// ... + +let apps = if config.load_apps { + info!("Loading apps..."); + Some(load_apps(system_table.boot_services())) +} else { + info!("Skip loading apps"); + None +}; + +// ... + +// construct BootInfo +let bootinfo = BootInfo { + // ... + loaded_apps: apps, +}; +``` + +修改 `ProcessManager` 的定义与初始化逻辑,将 `AppList` 添加到 `ProcessManager` 中: + +```rust +pub struct ProcessManager { + // ... + app_list: boot::AppListRef, +} +``` + +最后修改 `kernel/src/proc/mod.rs` 的 `init` 函数: + +```rust +/// init process manager +pub fn init(boot_info: &'static boot::BootInfo) { + // ... + let app_list = boot_info.loaded_apps.as_ref(); + manager::init(kproc, app_list); +} +``` + +之后,在 `kernel/src/proc/mod.rs` 中,定义一个 `list_app` 函数,用于列出当前系统中的所有用户程序和相关信息: + +```rust +pub fn list_app() { + x86_64::instructions::interrupts::without_interrupts(|| { + let app_list = get_process_manager().app_list(); + if app_list.is_none() { + println!("[!] No app found in list!"); + return; + } + + let apps = app_list + .unwrap() + .iter() + .map(|app| app.name.as_str()) + .collect::>() + .join(", "); + + // TODO: print more information like size, entry point, etc. + + println!("[+] App list: {}", apps); + }); +} +``` + +!!! success "阶段性成果" + + 在 `kernel/src/main.rs` 初始化内核之后,尝试调用 `list_app` 函数,查看是否成功加载。 + +### 生成用户程序 + +在 `kernel/src/proc/mod.rs` 中,添加 `spawn` 和 `elf_spawn` 函数,将 ELF 文件从列表中取出,并生成用户程序: + +```rust +pub fn spawn(name: &str) -> Option { + let app = x86_64::instructions::interrupts::without_interrupts(|| { + let app_list = get_process_manager().app_list()?; + app_list.iter().find(|&app| app.name.eq(name)) + })?; + + elf_spawn(name.to_string(), &app.elf) +} + +pub fn elf_spawn(name: String, elf: &ElfFile) -> Option { + let pid = x86_64::instructions::interrupts::without_interrupts(|| { + let manager = get_process_manager(); + let process_name = name.to_lowercase(); + let parent = Arc::downgrade(&manager.current()); + let pid = manager.spawn(elf, name, Some(parent), None); + + debug!("Spawned process: {}#{}", process_name, pid); + pid + }); + + Some(pid) +} +``` + +??? question "为什么独立一个 `elf_spawn`?" + + 在后续的实验中,`spawn` 将接收一个文件路径,操作系统需要从文件系统中读取文件,并将其加载到内存中。 + + 通过将 `elf_spawn` 独立出来,可以在后续实验中直接对接到文件系统的读取结果,而无需修改后续代码。 + +在 `ProcessManager` 中,实现 `spawn` 函数: + +```rust +pub fn spawn( + &self, + elf: &ElfFile, + name: String, + parent: Option>, + proc_data: Option, +) -> ProcessId { + let kproc = self.get_proc(&KERNEL_PID).unwrap(); + let page_table = kproc.read().clone_page_table(); + let proc = Process::new(name, parent, page_table, proc_data); + let pid = proc.pid(); + + let mut inner = proc.write(); + // FIXME: load elf to process pagetable + // FIXME: alloc new stack for process + // FIXME: mark process as ready + drop(inner); + + trace!("New {:#?}", &proc); + + // FIXME: something like kernel thread + pid +} +``` + +!!! warning "**删除或注释上次实验中有关内核线程的代码,防止后续修改后的进程模型在执行内核线程时遇到意外的问题。**" + +在加载的过程中,你可以复用 `elf::load_elf` 函数。可以通过为它添加参数的方式,控制这一映射的内容是否可以被用户权限(Ring 3)代码访问。 + +```rust +pub fn load_elf( + elf: &ElfFile, + physical_offset: u64, + page_table: &mut impl Mapper, + frame_allocator: &mut impl FrameAllocator, + user_access: bool +) -> /* return type */ { + // FIXME: just like kernel's load_elf +} +``` + +在映射页面时,根据此参数决定是否添加 `USER_ACCESSIBLE` 标志位: + +```rust +if user_access { + page_table_flags |= PageTableFlags::USER_ACCESSIBLE; +} +``` + +这一标志位只应当为用户进程所使用,内核相关代码不应当拥有这一权限。由于用户程序是不可信的,需要以此防止用户态程序访问内核的内存空间。 + +!!! note "**对于用户进程而言,不再与内核共享页表,而是通过克隆内核页表获取了自己的页表。这意味着可以为每个用户进程分配同样的栈地址,而不会相互干扰。**" + +需要在 GDT 中为 Ring 3 的代码段和数据段添加对应的选择子,在初始化栈帧的时候将其传入。在 `kernel/src/memory/gdt.rs` 中,你可以使用如下方式添加: + +```rust +lazy_static! { + static ref GDT: /* your type */ = { + let mut gdt = GlobalDescriptorTable::new(); + // ... + let user_code_selector = gdt.add_entry(Descriptor::user_code_segment()); + let user_data_selector = gdt.add_entry(Descriptor::user_data_segment()); + // ... + }; +} +``` + +之后将其通过合适的方式暴露出来,以供栈帧初始化时使用: + +```rust +pub fn init_stack_frame(&mut self, entry: VirtAddr, stack_top: VirtAddr) { + // ... + let selector = get_user_selector(); // FIXME: implement this function + + self.value.stack_frame.code_segment = selector.user_code_selector.0 as u64; + self.value.stack_frame.stack_segment = selector.user_data_selector.0 as u64; + // ... +} +``` + +!!! tip "一些提示" + + - 与内核类似,使用 `elf.header.pt2.entry_point()` 获取 ELF 文件的入口地址。 + - 或许可以在 `ProcessInner` 中实现一个 `load_elf` 函数,来处理代码段映射等内容。 + - 记得为进程分配好合适的栈空间,并使用 `init_stack_frame` 初始化程序栈和指令指针。 + - 或许你可以同时实现 **加分项 1** 所描述的功能。 + +!!! question "阶段性成果?" + + 但由于并没有实现任何系统调用服务(包括程序的退出、输入输出、内存分配等),因此你在加载用户程序后,基本无法进行任何操作。 + + 在这一阶段,为了不触发异常,你只能加载执行一个没有其他作用的死循环程序…… + + 因此,你需要在完成后续任务后,才能进行整体性调试。 + +## 系统调用的实现 + +为了为用户程序提供服务,操作系统需要实现一系列的系统调用,从而为用户态程序提供内核态服务。这些操作包括文件操作、进程操作、内存操作等,相关的指令一般需要更高的权限(相对于用户程序)才能执行。 + +### 调用约定 + +系统调用一般有系统调用号、参数、返回值等调用约定,不同的上下文参数对应的系统调用的行为存在不同。 + +以 x86_64 的 Linux 为例,系统调用的部分调用约定如下所示: + +- 系统调用号通过 `rax` 寄存器传递 +- 参数通过 `rdi`、`rsi`、`rdx`、`r10`、`r8`、`r9` 寄存器传递 +- 参数数量大于 6 时,通过栈传递 +- 返回值通过 `rax` 寄存器传递 + +!!! tip "在系统调用中,由于 `rcx` 寄存器有其他用途,因此使用 `r10` 寄存器代替函数调用约定 `__fastcall` 中的 `rcx` 寄存器。" + +实验实现中并不需要那么多的系统调用参数,在 `src/interrupt/syscall/mod.rs` 中定义了一个用于存储系统调用参数的结构体 `SyscallArgs`: + +```rust +#[derive(Clone, Debug)] +pub struct SyscallArgs { + pub syscall: Syscall, + pub arg0: usize, + pub arg1: usize, + pub arg2: usize, +} +``` + +之后在分发器函数 `dispatcher` 中,构造 `SyscallArgs` 结构体,尊重习惯性写法,使用 `rdi` / `rsi` / `rdx` 寄存器传递系统调用参数,之后调用相应的处理函数。 + +```rust +pub fn dispatcher(context: &mut ProcessContext) { + let args = super::syscall::SyscallArgs::new( + Syscall::try_from(context.regs.rax).unwrap_or_default(), + context.regs.rdi, + context.regs.rsi, + context.regs.rdx, + ); + + match args.syscall { + // ... + } +} +``` + +为了能在用户库(调用系统调用侧)和内核态(处理系统调用侧)之间达成系统调用号的一致性,在 `pkg/syscall` 中定义了一个 `Syscall` 枚举,用于存储系统调用号。 + +```rust +#[repr(usize)] +#[derive(Clone, Debug, TryFromPrimitive)] +pub enum Syscall { + Read = 0, + Write = 1, + + Spawn = 59, + Exit = 60, + WaitPid = 61, + + ListApp = 65531, + Stat = 65532, + Allocate = 65533, + Deallocate = 65534, + + #[num_enum(default)] + Unknown = 65535, +} +``` + +由于一些额外的执念,这里的读写、进程操作的系统调用号基本与 Linux 中功能类似的系统调用号一致,而有些系统调用号则是自定义的。 + +- `ListApp` 用于列出当前系统中的所有用户程序,由于尚不会进行文件系统的实现,因此需要这样一个系统调用来获取用户程序的信息。 +- `Stat` 用于获取系统中的一些统计信息,例如内存使用情况、进程列表等,用于调试和监控。 +- `Allocate/Deallocate` 用于分配和释放内存。在当前没有完整的用户态内存分配支持的情况下,可以利用系统调用将其委托给内核来完成。 + +### 软中断处理 + +在 [CPU 中断处理](../../wiki/interrupts.md)中介绍了软件中断的使用方式和用途,在 Linux 中,一个基于中断的系统调用可以用如下的汇编实现: + +```c +int _start() { + __asm__( + // 准备系统调用参数 + "xor %eax,%eax\n" + "push %eax\n" + "push $0x68732f2f\n" + "push $0x6e69622f\n" + "mov %esp,%ebx\n" + "push %eax\n" + "push %ebx\n" + "mov %esp,%ecx\n" + "mov $0xb,%al\n" + + // 系统调用中断 + "int $0x80\n" + ); +} +``` + +??? note "如果想要测试,需要编译为 32 位的可执行文件" + + 你可以使用如下的命令编译: + + ```bash + gcc syscall.c -o syscall -m32 -nostdlib -nodefaultlibs -fno-exceptions -no-pie + ``` + + 你可以使用 `objdump -d syscall` 来查看编译后的汇编代码。 + + 你可以在 x86_64 的机器上直接运行它。 + + 这段汇编的代码的作用是调用 `execve` 系统调用,执行 `/bin/sh`,也即一段 `shellcode`。 + +!!! question "`int 0x80`、`sysenter` 和 `syscall` 的区别?" + + `int 0x80` 是基于中断的系统调用实现,通常在早期的 32 位 x86 系统中使用。 + + 由于软中断保存、恢复现场、查询中断向量表等操作对于系统调用进入内核态来说过于**重**了,一定程度上影响 CPU 执行用户程序的效率,`sysenter` 和 `syscall` 被引入。 + + 它们专门用于系统调用,`sysenter` 用于 32 位系统,`syscall` 用于 64 位系统。它们不查询中断描述符表,通过直接访问某些特殊寄存器等指令和 CPU 设计上的优化,从而减少了系统调用的开销。 + + 你可以查阅资料,了解它们的具体实现和使用方式。但在本实验中,不需要考虑中断带来的性能损耗,实现时使用中断进行系统调用即可。 + +在 `src/interrupt/syscall/mod.rs` 中,补全中断注册函数,**并在合适的地方调用它**: + +1. 在 `idt` 的初始化中,注册 `0x80` 号中断的处理函数为 `syscall_handler`。_`int 0x80` 并非 Irq 中断,直接使用 `consts::Interrupts::Syscall` 即可。_ +2. 与时钟中断类似,或许为系统调用准备一个独立的栈是个好主意? +3. 使用 `set_privilege_level` 设置 `DPL` 为 `3`,以便用户态程序可以触发系统调用。 + +## 用户态库的实现 + +用户态库是用户程序的基础,它提供了一些基础的函数,用于调用系统调用,实现一些基础的功能。 + +在这一部分的实现中,着重实现了 `read` 和 `write` 系统调用的封装和内核侧的实现,并通过内存分配、释放的系统调用,给予用户态程序动态内存分配的能力。 + +### 动态内存分配 + +为了方便用户态程序使用动态内存分配,而不是基于 `mmap` 等方式进行完全用户态的动态内存管理,选择使用系统调用的方式,将内存分配的任务委托给内核完成。 + +与内核堆类似,在 `src/memory/user.rs` 中,定义了用户态的堆。 + +与内核使用 `static` 在内核 `bss` 段声明内存空间不同,由于在页表映射时需添加 `USER_ACCESSIBLE` 标志位,**用户态堆需要采用内核页面分配的能力完成**。其次需要注意的是,为了调试和安全性考量,这部分内存还需要 `NO_EXECUTE` 标志位。 + +有关用户态堆初始化的过程需要补全部分代码,指定合适的页面范围,分配并映射相关的页表,你可以参考、使用并修改 `elf::map_range` 完成这里的初始化。 + +!!! question "内存安全?" + + 这里的实现是不够安全的,共享用户态堆意味着用户程序可以任意读取访问其他用户进程的堆空间。 + + 不过这样的实现可以大幅简化用户态库的相关实现,在能且仅能运行自己的程序、完成实验方面,这样的选择是很合适的。 + +在系统调用的实现方面,给出的代码中已包含,可以参考它们的内容进行其他系统调用功能的实现。 + +### 标准输入输出 + +为了在系统调用中实现基础的读写操作,代码中定义了一个 `Resource` 枚举,并借用 Linux 中“文件描述符”的类似概念,将其存储在进程信息中。 + +!!! note "缓冲区与系统调用开销" + + 为了简化实验,这里的实现并不需要考虑 I/O 缓冲区和批处理的问题。但是在实际的操作系统中,这些考量对性能非常重要。 + + 在 libc 中,数据在用户态被累积起来,在达到一定数量后,通过一次系统调用进行处理(flush)—— 相较于 1024 次系统调用、每次输出一个字符的总开销,显然具有缓冲区之后的开销更小。 + +在 `src/proc/data.rs` 中,修改 `ProcessData` 结构体,类似于环境变量的定义,添加一个“文件描述符表”: + +```rust +pub(super) file_handles: Arc>> +``` + +在 `ProcessData` 的 `default` 函数中,初始化此表,并添加标准输入输出的资源: + +```rust +let mut file_handles = BTreeMap::new(); + +// stdin, stdout, stderr +file_handles.insert(0, Resource::Console(StdIO::Stdin)); +file_handles.insert(1, Resource::Console(StdIO::Stdout)); +file_handles.insert(2, Resource::Console(StdIO::Stderr)); +``` + +之后添加 `handle` 函数,用于在系统调用中根据文件描述符获取资源: + +```rust +pub fn handle(&self, fd: u8) -> Option { + self.file_handles.read().get(&fd).cloned() +} +``` + +系统调用总是为当前进程提供服务,因此可以在 `proc/mod.rs` 中对一些操作进行封装,封装获取当前进程、上锁等操作。以获取当前进程的文件描述符表为例: + +```rust +pub fn handle(fd: u8) -> Option { + x86_64::instructions::interrupts::without_interrupts(|| { + get_process_manager().current().read().handle(fd) + }) +} +``` + +对于 `write` 系统调用,用户程序需要将数据写入到资源中,对此系统调用进行如下约定: + +``` +fd: arg0 as u8, buf: &[u8] (ptr: arg1 as *const u8, len: arg2) +``` + +为了便于理解,给出了用户侧进行调用时的示例代码,从 `print!` 的实现开始: + +```rust +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => ($crate::_print(format_args!($($arg)*))); +} + +#[doc(hidden)] +pub fn _print(args: Arguments) { + stdout().write(format!("{}", args).as_str()); +} +``` + +将会调用 `Stdout` 的 `write` 方法: + +```rust +impl Stdout { + pub fn write(&self, s: &str) { + sys_write(1, s.as_bytes()); + } +} +``` + +传递给 `sys_write`,由此函数对传入的参数进行处理,并调用系统调用: + +```rust +pub fn sys_write(fd: u8, buf: &[u8]) -> Option { + let ret = syscall!( + Syscall::Write, + fd as usize, + buf.as_ptr() as usize, + buf.len() as usize + ) as isize; + if ret.is_negative() { + None + } else { + Some(ret as usize) + } +} +``` + +系统调用宏 `syscall!` 的实现如下: + +```rust +#[macro_export] +macro_rules! syscall { + // ... + ($n:expr, $a1:expr, $a2:expr, $a3:expr) => { + $crate::macros::syscall3($n, $a1 as usize, $a2 as usize, $a3 as usize) + }; +} + +#[doc(hidden)] +#[inline(always)] +pub fn syscall3(n: Syscall, arg0: usize, arg1: usize, arg2: usize) -> usize { + let ret: usize; + unsafe { + asm!( + "int 0x80", in("rax") n as usize, + in("rdi") arg0, in("rsi") arg1, in("rdx") arg2, + lateout("rax") ret + ); + } + ret +} +``` + +通过内联汇编的形式,将参数列表存入对应的寄存器中,再调用 `int 0x80` 中断,触发系统调用。 + +在内核正确响应中断,并进行服务分发后,调用对应的服务处理函数。 + +在 `interrupt/syscall/service.rs` 中,你需要实现 `sys_write` 函数,用于处理 `write` 系统调用,使得用户程序得以进程输出: + +1. 使用上述 `proc::handle` 获取文件描述符,并处理 `Option`。 +2. 使用 `core::slice::from_raw_parts` 将用户程序的缓冲区转换为 `&[u8]`。 +3. 将缓冲区传入资源的 `write` 方法中,并返回写入的字节数。 +4. 在分发函数中使用 `context.set_rax` 设置返回值,并调用 `sys_write` 函数。 + +!!! tip "参考 `sys_allocate` 的实现和相关用户侧代码进行实现" + +`sys_read` 的实现与 `sys_write` 类似,为了更好的兼容性和更低的实现难度,在实现 `read` 时候需要注意如下几点: + +1. 在 `Resource` 中添加 `read` 方法时,从内核输入缓冲区中读取数据并写入到用户程序的缓冲区中。 +2. 遵循相关原则,系统调用理论上不应等待,如果没有数据可读,应立即返回。 +3. 对用户输入的等待行为应当在用户态程序中实现,并应该提供非阻塞的读取方式。 +4. 对于一个固定的输入序列,需要特殊处理控制字符,如回车、退格等。这些处理需要在用户态进行,并通过 `write` 系统调用进行反馈输出。 +5. 在用户态需要控制好缓冲区的大小,遵守系统调用返回的长度进行处理。 +6. 可以使用 `from_raw_parts_mut` 将用户程序的缓冲区转换为 `&mut [u8]`。 + +### 进程的退出 + +与内核线程防止再次被调度的“退出”不同,用户程序的正常结束,需要在用户程序中调用 `exit` 系统调用,以通知内核释放资源。 + +由于此时通过中断进入内核态,与时钟中断类似,操作系统得以控制**退出中断时的 CPU 上下文**。因此可以在退出的时候清理进程占用的资源,并调用 `switch_next` 函数,切换到下一个就绪的进程。 + +> 为了实现的简单,不需要实验程序异常退出的相关情况处理,将错误展示出来也更便于调试。 + +与 `handle` 的实现类似,也可以通过在 `kernel/src/proc/mod.rs` 中实现 `exit` 函数,封装对应的功能,并暴露给系统调用: + +```rust +pub fn exit(ret: isize, context: &mut ProcessContext) { + x86_64::instructions::interrupts::without_interrupts(|| { + let manager = get_process_manager(); + manager.kill_self(ret); // FIXME: implement this for ProcessManager + manager.switch_next(context); + }) +} +``` + +!!! tip "增加代码重用" + + 退出的操作并不只用于当前进程,也可以作用于其他进程,因此可以通过如下方式将它们统一: + + ```rust + pub fn kill_self(&self, ret: isize) { + self.kill(processor::current_pid(), ret); + } + + pub fn kill(&self, pid: ProcessId, ret: isize) { + // FIXME: get process by pid + // FIXME: kill process and set return code + } + ``` + + 这只是一种可能的实现方式,你可以根据自己的需求进行调整。 + +进程退出释放资源的过程基本有如下几步: + +1. 释放进程的页表,释放页表占用的页面。 + + > 这部分将在后续实验中实现。此处不需要关心,但是建议完成 **加分项 2** 的内容 + +2. 删除进程的数据(`ProcessData`),可以使用 `take` 方法。 + +3. 设置进程的返回值,并设置状态为 `Dead`。 + +!!! success "阶段性成果" + + 终于!在实现进程的退出之后,用户程序可以生成并正确退出了! + + 尝试修改用户态库中的 `entry!` 和 `panic` 函数,在用户程序中调用 `exit` 系统调用,并传递一个返回值,以验证用户程序的退出功能。 + + 值此为止,你应当可以生成用户程序、输出 `Hello, world!!!`,并正确退出。 + +### 进程的创建与等待 + +操作系统的内核入口点将能够简单实现如下功能: + +1. 初始化内核 +2. 生成 `init` 进程,并等待它退出 +3. 关机 + +其中,等待进程退出的函数 `ysos::wait` 可以定义在 `kernel/src/lib.rs` 中: + +```rust +pub fn wait(init: proc::ProcessId) { + loop { + if proc::still_alive(init) { + x86_64::instructions::hlt(); + } else { + break; + } + } +} +``` + +并在 `kernel/src/proc/mod.rs` 中,补全 `still_alive` 函数: + +```rust +#[inline] +pub fn still_alive(pid: ProcessId) -> bool { + x86_64::instructions::interrupts::without_interrupts(|| { + // check if the process is still alive + }) +} +``` + +对于具体的进程操作、目录操作等功能,将会移步到用户态程序进行实现。为了给予用户态程序操作进程、等待进程退出的能力,这里还缺少最后两个系统调用需要实现:`spawn` 和 `waitpid`,对这两个系统调用有如下约定: + +```rust +// path: &str (arg0 as *const u8, arg1 as len) -> pid: u16 +Syscall::Spawn => { /* ... */ }, +// pid: arg0 as u16 -> status: isize +Syscall::WaitPid => { /* ... */}, +``` + +相关必要函数和参考代码在前文中均有涉及,你可以参考前文的内容进行系统调用的实现。 + +> 你可以自由定义你的内核与用户态的交互方式了! + +!!! note "关于 `WaitPid` 的问题" + + 你可能会发现,`WaitPid` 需要返回特殊状态,以区分进程正在运行还是已经退出。 + + 不过非常糟糕,当前进程的返回值也是一个 `isize` 类型的值,这意味着如果按照现在的设计,势必存在一些返回值和“正在运行”的状态冲突。 + + 不过在简单的实验中,这并不会造成太大的问题,而此问题的解决方案也留作加分项供大家研究。 + + **另请注意,`WaitPid` 虽然名为 `wait` 但是并不会进行阻塞,应当立刻返回进程的当前状态。** + +## 运行 Shell + +至此,你可以编写自己的 Shell 了!作为用户与操作系统的交互方式,它需要实现一些必须功能: + +- 列出当前系统中的所有用户程序 +- 列出当前正在运行的全部进程 +- 运行一个用户程序 + +同时,它也可以实现一些辅助的能力: + +- 列出帮助信息 +- 清空屏幕 +- ... + +为了实现一些信息的查看,你也需要实现如下两个系统调用: + +```rust +// None +Syscall::Stat => { /* ... */ }, +// None +Syscall::ListApp => { /* ... */}, +``` + +!!! tip "实验任务" + + 尝试结合 lab 0 的内容,实现一个你自己的 Shell! + + 它应当能够生成一个用户程序,等待它退出,并输出它的返回值。实现较为灵活,你可以自行调整用户态库和用户程序,以你喜欢的方式构建操作系统的用户态。 + + 除此之外,请实现 `help` 命令,并在其中输出帮助信息,并附加你的学号。 + +### 测试程序 + +作为之前所有程序功能的测试和检验,你需要在用户态下正确运行如下程序: + +```rust +const MOD: u64 = 1000000007; + +fn factorial(n: u64) -> u64 { + if n == 0 { + 1 + } else { + n * factorial(n - 1) % MOD + } +} + +fn main() -> usize { + print!("Input n: "); + + let input = lib::stdin().read_line(); + + // prase input as u64 + let n = input.parse::().unwrap(); + + if n > 1000000 { + println!("n must be less than 1000000"); + return 1; + } + + // calculate factorial + let result = factorial(n); + + // print system status + sys_stat(); + + // print result + println!("The factorial of {} under modulo {} is {}.", n, MOD, result); + + 0 +} + +entry!(main); +``` + +你可以进行一些交互测试,例如输入 `100` 或更大的数据(会产生更大的栈占用!)并查看输出结果。在最大规模(输入数据为 `999999` 时),预期输出为: + +``` +The factorial of 999999 under modulo 1000000007 is 128233642. +``` + +你可以使用此测例来进行对照参考。此用户程序约占用 3929 个页面,总计内存占用约 15.3MiB。 + +## 思考题 + +1. 是否可以在内核线程中使用系统调用?并借此来实现同样的进程退出能力?分析并尝试回答。 + +2. 为什么需要克隆内核页表?在系统调用的内核态下使用的是哪一张页表?用户态程序尝试访问内核空间会被正确拦截吗?尝试验证你的实现是否正确。 + +3. 为什么在使用 `still_alive` 函数判断进程是否存活时,需要关闭中断?在不关闭中断的情况下,会有什么问题? + +4. 对于如下程序,使用 `gcc` 直接编译: + + ```c + #include + + int main() { + printf("Hello, World!\n"); + return 0; + } + ``` + + 从本次实验及先前实验的所学内容出发,结合进程的创建、链接、执行、退出的生命周期,参考系统调用的调用过程(可以仅以 Linux 为例),解释程序的运行。 + +## 加分项 + +1. 😋 尝试在 `ProcessData` 中记录代码段的占用情况,并统计当前进程所占用的页面数量,并在打印进程信息时,将进程的内存占用打印出来。 + +2. 😋 尝试在 `kernel/src/memory/frames.rs` 中实现帧分配器的回收功能 `FrameDeallocator`,作为一个最小化的实现,你可以在 `Allocator` 使用一个 `Vec` 存储被释放的页面,并在分配时从中取出。 + +3. 🤔 基于帧回收器的实现,在 `elf` 中实现 `unmap_range` 函数,从页表中取消映射一段连续的页面,并使用帧回收器进行回收。利用它实现进程栈的回收(利用 `ProcessData` 中存储的页面信息)。**页表的回收将会在后续实现用实现,暂时不需要处理** + +4. 🤔 改进或重新设计进程返回值的相关内容,给予 `WaitPid` 更好的兼容性,描述你的设计和实现。 + +5. 🤔 尝试利用 `UefiRuntime` 和 `chrono` crate,获取当前时间,并将其暴露给用户态,以实现 `sleep` 函数。 + + `UefiRuntime` 的实现: + + ```rust + pub struct UefiRuntime { + runtime_service: &'static RuntimeServices, + } + + impl UefiRuntime { + pub unsafe fn new(boot_info: &'static BootInfo) -> Self { + Self { + runtime_service: boot_info.system_table.runtime_services(), + } + } + + pub fn get_time(&self) -> Time { + self.runtime_service.get_time().unwrap() + } + } + ``` + + 一个可能的 `sleep` 函数实现: + + ```rust + pub fn sleep(millisecs: i64) { + let start = sys_time(); + let dur = Duration::milliseconds(millisecs); + let mut current = start; + while current - start < dur { + current = sys_time(); + } + } + ``` + + 在实现后,写一个或更改现有用户程序,验证你的实现是否正确,尝试输出当前时间并等待一段时间。 + + > 当前实现是纯用户态、采用轮询的,这种实现是很低效的。在现代操作系统中,进程会被挂起,并等待对应事件触发后重新被调度。 diff --git a/docs/labs/0x05/index.md b/docs/labs/0x05/index.md index 11a894d..63c757c 100644 --- a/docs/labs/0x05/index.md +++ b/docs/labs/0x05/index.md @@ -27,7 +27,7 @@ 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/labs/0x05/tasks.md b/docs/labs/0x05/tasks.md index 3334971..26f5406 100644 --- a/docs/labs/0x05/tasks.md +++ b/docs/labs/0x05/tasks.md @@ -1 +1,5 @@ # 实验五:fork 的实现、并发与锁机制 + +## 思考题 + +1. 在 Lab 2 中设计输入缓冲区时,如果不使用无锁队列实现,而选择使用 `Mutex` 对一个同步队列进行保护,在编写相关函数时需要注意什么问题?考虑在进行 `pop` 操作过程中遇到串口输入中断的情形,尝试描述遇到问题的场景,并提出解决方案。 diff --git a/docs/labs/0x06/index.md b/docs/labs/0x06/index.md index 5390bd9..24d6aad 100644 --- a/docs/labs/0x06/index.md +++ b/docs/labs/0x06/index.md @@ -28,7 +28,7 @@ 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/labs/0x07/index.md b/docs/labs/0x07/index.md index c6753f7..1c4da42 100644 --- a/docs/labs/0x07/index.md +++ b/docs/labs/0x07/index.md @@ -6,8 +6,7 @@ ## 实验目的 1. 实现帧分配器的内存回收,操作系统的内存统计。 -2. 实现操作系统栈的自动增长。 -3. 尝试实现 mmap 系统调用,实现用户态的内存管理算法。 +2. 尝试实现 mmap 系统调用,实现用户态的内存管理算法。 ## 实验基础知识 @@ -28,7 +27,7 @@ 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 -2. 请参考 [代码规范](../../general/coding_convention.md) 进行实验代码编写。 +2. 请参考 [代码与提交规范](../../general/specification.md) 进行实验代码编写。 3. 依据 [实验任务](./tasks.md) 完成实验。 diff --git a/docs/wiki/apic.md b/docs/wiki/apic.md index d036b2e..47c35b7 100644 --- a/docs/wiki/apic.md +++ b/docs/wiki/apic.md @@ -61,7 +61,7 @@ APIC 的初始化过程基本包括以下几个步骤: 在启用分页内存的情况下,需要对 LAPIC 寄存器地址进行映射,并虚拟内存地址进行操作。每个寄存器位宽为 32 位,并期望以 32 位整数的形式进行写入和读取。 -
Offset Register name Read/Write permissions
000h - 010h Reserved
020h LAPIC ID Register Read/Write
030h LAPIC Version Register Read only
040h - 070h Reserved
080h Task Priority Register (TPR) Read/Write
090h Arbitration Priority Register (APR) Read only
0A0h Processor Priority Register (PPR) Read only
0B0h EOI register Write only
0C0h Remote Read Register (RRD) Read only
0D0h Logical Destination Register Read/Write
0E0h Destination Format Register Read/Write
0F0h Spurious Interrupt Vector Register Read/Write
100h - 170h In-Service Register (ISR) Read only
180h - 1F0h Trigger Mode Register (TMR) Read only
200h - 270h Interrupt Request Register (IRR) Read only
280h Error Status Register Read only
290h - 2E0h Reserved
2F0h LVT Corrected Machine Check Interrupt (CMCI) Register Read/Write
300h - 310h Interrupt Command Register (ICR) Read/Write
320h LVT Timer Register Read/Write
330h LVT Thermal Sensor Register Read/Write
340h LVT Performance Monitoring Counters Register Read/Write
350h LVT LINT0 Register Read/Write
360h LVT LINT1 Register Read/Write
370h LVT Error Register Read/Write
380h Initial Count Register (for Timer) Read/Write
390h Current Count Register (for Timer) Read only
3A0h - 3D0h Reserved
3E0h Divide Configuration Register (for Timer) Read/Write
3F0h Reserved
+
Offset Register name Read/Write permissions
000h - 010h Reserved
020h LAPIC ID Register Read/Write
030h LAPIC Version Register Read only
040h - 070h Reserved
080h Task Priority Register (TPR) Read/Write
090h Arbitration Priority Register (APR) Read only
0A0h Processor Priority Register (PPR) Read only
0B0h EOI register Write only
0C0h Remote Read Register (RRD) Read only
0D0h Logical Destination Register Read/Write
0E0h Destination Format Register Read/Write
0F0h Spurious Interrupt Vector Register Read/Write
100h - 170h In-Service Register (ISR) Read only
180h - 1F0h Trigger Mode Register (TMR) Read only
200h - 270h Interrupt Request Register (IRR) Read only
280h Error Status Register Read only
290h - 2E0h Reserved
2F0h LVT Corrected Machine Check Interrupt (CMCI) Register Read/Write
300h - 310h Interrupt Command Register (ICR) Read/Write
320h LVT Timer Register Read/Write
330h LVT Thermal Sensor Register Read/Write
340h LVT Performance Monitoring Counters Register (PCINT) Read/Write
350h LVT LINT0 Register Read/Write
360h LVT LINT1 Register Read/Write
370h LVT Error Register Read/Write
380h Initial Count Register (for Timer) Read/Write
390h Current Count Register (for Timer) Read only
3A0h - 3D0h Reserved
3E0h Divide Configuration Register (for Timer) Read/Write
3F0h Reserved
你可以参考文末给出的参考资料以获取这些寄存器的细节信息。 diff --git a/docs/wiki/userspace.md b/docs/wiki/userspace.md new file mode 100644 index 0000000..099d115 --- /dev/null +++ b/docs/wiki/userspace.md @@ -0,0 +1,101 @@ +# 用户空间 + +## 用户态与内核态 + +从执行原理来说,不同进程的指令均运行在同一个处理器上,基于同一个处理器访问系统资源。但从资源管理、安全管理、崩溃预防的角度来看,操作系统需隔离敏感资源,限制不同进程的可执行的指令以及可访问的地址空间。因此,现代操作系统通常会将进程分为两种状态: + +- 用户态(user mode):是指进程运行在低特权级别,只能执行受限指令,访问受限资源; +- 内核态(kernel mode):是指进程运行在高特权级别,可以执行特权指令,并且可以直接访问所有系统资源。 + +处理器通常借助某个控制寄存器中的一个模式位(mode bit)来提供区分用户态、内核态。该寄存器描述了进程当前享有的特权。一个运行在内核态的进程可以执行指令集中的任何指令,并访问系统中的任何内存位置。 + +而用户态中的进程不允许执行特权指令(privileged instruction),比如停止处理器、改变模式位、发起一个 I/O 操作等。用户进程也不能直接访问高权限级的地址空间,**任何这样的尝试都会导致致命的保护异常,触发中断**。 + +!!! note "关于一般保护异常" + + 一般保护异常(General Protection Fault,GPF)是操作系统在发现进程试图执行非法、未授权的或特权级别不足的操作时产生的一种异常。 + +用户程序必须通过内核态的介入,才能**间接地**访问敏感资源。进程可以通过一些特殊指令(如系统调用、中断)请求操作系统的服务,操作系统会在内核态中执行这些服务,然后返回结果给用户态。 + +相关数据的传递有一定的约束,这些被称为调用约定(calling convention)。调用约定规定了用户态和内核态之间的数据传递方式,包括参数传递、数据结构类型、返回值传递、异常处理等。 + +## Rings + +在 x86_64 体系结构中,操作系统和应用程序运行在不同的特权级别(privilege levels)中,这些级别被称为 Rings。一般来说,较*低*的数字代表更*高*的特权级别: + +- Ring 0:内核态,最高特权级别,可以执行所有指令,访问所有内存。 +- Ring 1/2:被设计用于设备驱动程序和其他系统服务,但在现代操作系统中不再使用。 +- Ring 3:用户态,最低特权级别,只能执行受限指令,需要访问受限资源时,必须通过系统调用等方式。 + +!!! question "为什么现代操作系统不使用 Ring 1/2?" + + 1. **内存保护的粒度问题:** 虽然 x86 CPU 提供了四个特权级别,但对应特权只能在内存段粒度生效,而非现代操作系统所期待的更细粒度的内存保护。因此,现代大多数 x86 操作系统忽略段机制,而是依赖基于页表项(PTEs)的保护机制。目前, x86 体系结构页表项的保护机制只有两个特权级别,即用户态和内核态。 + + 2. **可移植性问题:** 操作系统的设计者考虑需到跨平台的可移植性,不仅仅是限于 x86 架构。为了保持操作系统的可移植性,不依赖于特定处理器架构上的特权级别数量,而是统一使用两个特权级别,使得操作系统更易于在不同的处理器架构上移植和运行。 + + 3. **历史遗留问题:** x86 架构的设计早于现代操作系统的实现,许多 x86 的系统编程特性在设计时并不清楚操作系统将如何使用。随着现代操作系统对 x86 架构的深入了解和不断发展,许多早期设计的特性被现代操作系统所忽略,包括段机制和任务状态段等。对于 x64 架构,一些被废弃的特性被省略,使得操作系统可以更加简洁地设计和实现。 + +## 用户态库 + +### 从 hello world 开始 + +初学 C 语言时,教材展示的第一个程序往往如下: + +```c +#include + +int main() { + printf("Hello, World!\n"); + return 0; +} +``` + +`#include ` 与 `printf` 函数的组合太过于基础,以至于我们很少去思考:为什么要这么写?是谁实现的 `printf` 函数?这些函数是如何被调用、链接、执行的?为什么调用这些函数就能在屏幕上输出文字?这些问题的答案就在用户态库中。 + +### 用户态库概述 + +以 `printf` 为代表的常见函数并不是 C 语言关键字,而是 C 语言标准库的一部分。 + +C 语言标准库提供了一组函数和数据结构,供应用程序使用以实现各种功能。C 语言标准库中函数和数据结构的实现通常基于操作系统提供的用户态库。**用户态库集成了常见系统服务,屏蔽了底层硬件的细节,为应用程序提供了统一的编程接口。** + +在 Linux 系统中,常见的用户态库包括 glibc、musl 等,而在 Windows 系统中对应的用户态库则是 msvcrt 等。这些库通过暴露 C 语言接口,为开发者提供了一种统一的编程 API,使得开发者可以在不同的操作系统环境下编写跨平台的应用程序。 + +1. **glibc(GNU C Library)**:是 Linux 系统中最常用的 C 语言库之一,提供了许多系统调用的封装,包括文件操作、内存管理、进程控制等。它是 GNU 项目的一部分,具有广泛的支持和社区维护。 +2. **musl**:与 glibc 类似,也是一个 C 标准库,但相比于 glibc,musl 更注重轻量级和性能。它专注于遵循 POSIX 标准,并且更加注重代码的简洁和可维护性。 +3. **msvcrt**:是 Windows 系统下的 C 运行时库,提供了类似于 glibc 的功能。 + +### 用户态库的设计 + +这些用户态库的设计与其提供的服务紧密相关: + +- **跨平台兼容性**:用户态库的设计要求之一即屏蔽底层硬件的差异,向上提供统一的编程接口。换言之,用户态库的一致可以保持用户态程序跨平台兼容性,在更换平台时不需要修改应用程序的代码。 +- **性能与效率**:用户态库的设计需要考虑到性能和效率,尤其是对于系统调用等底层操作的封装,需要尽可能地减少额外的开销。[这篇文章](http://arkanis.de/weblog/2017-01-05-measurements-of-system-call-performance-and-overhead)测量了不同系统调用的性能和开销,并讨论了系统调用设计方法。 +- **功能完备性**:库中需要包含常用的系统调用封装和其他功能,以便开发者可以方便地进行应用程序开发,而不需要重复造轮子。 + +### 用户态库的服务 + +以 glibc 为例,以下是常见的一些用户态库 C 语言接口: + +1. **系统调用接口**:glibc 提供了访问操作系统底层服务的接口,如 `open()`、`read()`、`write()`等,这些接口允许程序与操作系统进行通信,执行诸如文件操作、进程管理等任务。 +2. **内存管理接口**:glibc 提供了内存分配和释放的接口,如 `malloc()`、`free()`、`calloc()`等,这些接口允许程序在运行时动态地分配和释放内存。 +3. **文件操作接口**:除了系统调用之外,glibc 还提供了更高层次的文件操作接口,如 `fopen()`、`fclose()`、`fread()`、`fwrite()` 等,这些接口提供了更便利的文件访问方法。 +4. **网络和套接字接口**:glibc 提供了一些网络编程的接口,如 `socket()`、`bind()`、`connect()`等,用于创建和管理网络套接字。 +5. **其他工具接口**:glibc 提供了一系列工具函数,如字符串处理的接口、数学函数接口等。 + +这些接口为 C 语言程序提供了丰富的功能和便利的操作方式,使得开发者能够更加轻松地编写各种类型的应用程序。 + +### Rust 中的用户态库 + +Rust 语言中的标准用户态库是 `std`,它提供了一系列的系统调用封装、内存管理、文件操作、网络编程等接口。与 C 语言的用户态库类似,Rust 的用户态库也提供了一系列的接口,使得开发者能够更加方便地进行应用程序开发。 + +不过,与 C 语言的用户态库的二进制 lib 分发不同,Rust 使用源码分发。这意味着开发者可以直接跳转并查看 `std` 的源码,而在 C 语言中定位相关函数时,一般仅能查看其头文件。 + +## 参考资料 + +- [Getting to Ring 3 - OSDev](https://wiki.osdev.org/Getting_to_Ring_3) +- [Security - OSDev](https://wiki.osdev.org/Security) +- [Paging - OSDev](https://wiki.osdev.org/Paging) +- [Protection ring - Wikipedia](https://en.wikipedia.org/wiki/Protection_ring) +- [Computer Systems: A Programmer's Perspective - CSAPP](http://www.csapp.cs.cmu.edu/3e/home.html) +- [Why do x86 CPUs only use 2 out of 4 rings?](https://superuser.com/questions/1063420/why-do-x86-cpus-only-use-2-out-of-4-rings) +- [printf 是怎么输出到控制台的呢?](https://www.zhihu.com/question/456916638/answer/3099313413) diff --git a/mkdocs.yml b/mkdocs.yml index b5b7fdd..05c535e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,7 +38,7 @@ theme: nav: - 主页: - index.md - - 代码规范要求: general/coding_convention.md + - 代码与提交规范: general/specification.md - 寻求帮助: general/help.md - 常见问题及解答: general/faq.md - 使用 Typst 编写报告: general/typst.md @@ -82,6 +82,7 @@ nav: - CPU 中断处理: wiki/interrupts.md - APIC 可编程中断控制器: wiki/apic.md - x64 数据结构概述: wiki/structures.md + - 用户空间: wiki/userspace.md markdown_extensions: - attr_list: diff --git a/src/0x02/pkg/kernel/Cargo.toml b/src/0x02/pkg/kernel/Cargo.toml index 234a4de..d88c1a3 100644 --- a/src/0x02/pkg/kernel/Cargo.toml +++ b/src/0x02/pkg/kernel/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] boot = { package = "ysos_boot", path = "../boot", default-features = false } lazy_static = { version = "1.4", features = ["spin_no_std"] } +crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] } paste = "1.0" spin = "0.9" x86 = "0.52" diff --git a/src/0x03/pkg/kernel/Cargo.toml b/src/0x03/pkg/kernel/Cargo.toml index ee4cad9..2a5f1ec 100644 --- a/src/0x03/pkg/kernel/Cargo.toml +++ b/src/0x03/pkg/kernel/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] boot = { package = "ysos_boot", path = "../boot", default-features = false } lazy_static = { version = "1.4", features = ["spin_no_std"] } +crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] } paste = "1.0" spin = "0.9" x86 = "0.52" diff --git a/src/0x03/pkg/kernel/src/proc/mod.rs b/src/0x03/pkg/kernel/src/proc/mod.rs index ff8231e..5ed8e51 100644 --- a/src/0x03/pkg/kernel/src/proc/mod.rs +++ b/src/0x03/pkg/kernel/src/proc/mod.rs @@ -7,12 +7,12 @@ mod process; mod processor; use manager::*; -use paging::*; use process::*; use crate::memory::PAGE_SIZE; use alloc::string::String; pub use context::ProcessContext; +pub use paging::PageTableContext; pub use data::ProcessData; pub use pid::ProcessId; @@ -29,7 +29,7 @@ pub const STACK_START_MASK: u64 = !(STACK_MAX_SIZE - 1); // init stack pub const STACK_DEF_PAGE: u64 = 1; pub const STACK_DEF_SIZE: u64 = STACK_DEF_PAGE * PAGE_SIZE; -pub const STACT_INIT_BOT: u64 = STACK_MAX - STACK_DEF_SIZE; +pub const STACK_INIT_BOT: u64 = STACK_MAX - STACK_DEF_SIZE; pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; // [bot..0xffffff0100000000..top..0xffffff01ffffffff] // kernel stack diff --git a/src/0x03/pkg/kernel/src/utils/func.rs b/src/0x03/pkg/kernel/src/utils/func.rs index 17766c6..7c7ba7c 100644 --- a/src/0x03/pkg/kernel/src/utils/func.rs +++ b/src/0x03/pkg/kernel/src/utils/func.rs @@ -11,7 +11,7 @@ pub fn test() -> ! { count += 1; if count == 1000 { count = 0; - print_serial!("\r{:-6} => Tick!", id); + print!("\r{:-6} => Tick!", id); } unsafe { x86_64::instructions::hlt(); diff --git a/src/0x04/Cargo.toml b/src/0x04/Cargo.toml new file mode 100644 index 0000000..c12576b --- /dev/null +++ b/src/0x04/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +resolver = "2" +members = [ + "pkg/elf", + "pkg/boot", + "pkg/kernel", + "pkg/syscall", + "pkg/lib", + "pkg/app/*" +] +exclude = ["pkg/app/config", "pkg/app/.cargo"] + +[profile.release-with-debug] +inherits = "release" +debug = true + +[profile.release-with-debug.package."*"] +debug = false diff --git a/src/0x04/Makefile b/src/0x04/Makefile new file mode 100644 index 0000000..cf124d4 --- /dev/null +++ b/src/0x04/Makefile @@ -0,0 +1,102 @@ +OVMF := assets/OVMF.fd +ESP := esp +BUILD_ARGS := +QEMU_ARGS := -m 96M +QEMU_OUTPUT := -nographic +MODE ?= release +CUR_PATH := $(shell pwd) +APP_PATH := $(CUR_PATH)/pkg/app +DBG_INFO ?= false + +APPS := $(shell find $(APP_PATH) -maxdepth 1 -type d) +APPS := $(filter-out $(APP_PATH),$(patsubst $(APP_PATH)/%, %, $(APPS))) +APPS := $(filter-out config,$(APPS)) +APPS := $(filter-out .cargo,$(APPS)) + +# Only add debug info for kernel +# this is required for VSCode GUI debugging +ifeq (${DBG_INFO}, true) + PROFILE = release-with-debug + PROFILE_ARGS = --profile=release-with-debug +else + PROFILE = ${MODE} + PROFILE_ARGS = $(BUILD_ARGS) +endif + +ifeq (${MODE}, release) + BUILD_ARGS := --release +endif + +.PHONY: build run debug clean launch intdbg \ + target/x86_64-unknown-uefi/$(MODE)/ysos_boot.efi \ + target/x86_64-unknown-none/$(PROFILE)/ysos_kernel \ + target/x86_64-unknown-ysos/$(MODE) + +run: build launch + +launch: + @qemu-system-x86_64 \ + -bios ${OVMF} \ + -net none \ + $(QEMU_ARGS) \ + $(QEMU_OUTPUT) \ + -drive format=raw,file=fat:rw:${ESP} + +intdbg: + @qemu-system-x86_64 \ + -bios ${OVMF} \ + -net none \ + $(QEMU_ARGS) \ + $(QEMU_OUTPUT) \ + -drive format=raw,file=fat:rw:${ESP} \ + -no-reboot -d int,cpu_reset + +debug: + @qemu-system-x86_64 \ + -bios ${OVMF} \ + -net none \ + $(QEMU_ARGS) \ + $(QEMU_OUTPUT) \ + -drive format=raw,file=fat:rw:${ESP} \ + -s -S + +clean: + @cargo clean + +list: + @for dir in $(APPS); do echo $$dir || exit; done + +build: $(ESP) + +$(ESP): $(ESP)/EFI/BOOT/BOOTX64.EFI $(ESP)/KERNEL.ELF $(ESP)/EFI/BOOT/boot.conf $(ESP)/APP + +$(ESP)/EFI/BOOT/BOOTX64.EFI: target/x86_64-unknown-uefi/$(MODE)/ysos_boot.efi + @mkdir -p $(@D) + cp $< $@ + +$(ESP)/EFI/BOOT/boot.conf: pkg/kernel/config/boot.conf + @mkdir -p $(@D) + cp $< $@ + +$(ESP)/KERNEL.ELF: target/x86_64-unknown-none/$(PROFILE)/ysos_kernel + @mkdir -p $(@D) + cp $< $@ + +$(ESP)/APP: target/x86_64-unknown-ysos/$(MODE) + @for app in $(APPS); do \ + mkdir -p $(ESP)/APP; \ + cp $ usize { + println!("Hello, world!!!"); + + 233 +} + +entry!(main); diff --git a/src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs b/src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs new file mode 100644 index 0000000..18b2711 --- /dev/null +++ b/src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs @@ -0,0 +1,95 @@ +use crate::{memory::gdt, proc::*}; +use alloc::format; +use syscall_def::Syscall; +use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame}; + +mod service; +use super::consts; + +// FIXME: write syscall service handler in `service.rs` +use service::*; + +pub unsafe fn register_idt(idt: &mut InterruptDescriptorTable) { + // FIXME: register syscall handler to IDT + // - standalone syscall stack + // - ring 3 +} + +pub extern "C" fn syscall(mut context: ProcessContext) { + x86_64::instructions::interrupts::without_interrupts(|| { + super::syscall::dispatcher(&mut context); + }); +} + +as_handler!(syscall); + +#[derive(Clone, Debug)] +pub struct SyscallArgs { + pub syscall: Syscall, + pub arg0: usize, + pub arg1: usize, + pub arg2: usize, +} + +pub fn dispatcher(context: &mut ProcessContext) { + let args = super::syscall::SyscallArgs::new( + Syscall::from(context.regs.rax), + context.regs.rdi, + context.regs.rsi, + context.regs.rdx, + ); + + match args.syscall { + // fd: arg0 as u8, buf: &[u8] (ptr: arg1 as *const u8, len: arg2) + Syscall::Read => { /* FIXME: read from fd & return length */}, + // fd: arg0 as u8, buf: &[u8] (ptr: arg1 as *const u8, len: arg2) + Syscall::Write => { /* FIXME: write to fd & return length */}, + + // path: &str (ptr: arg0 as *const u8, len: arg1) -> pid: u16 + Syscall::Spawn => { /* FIXME: spawn process from name */}, + // ret: arg0 as isize + Syscall::Exit => { /* FIXME: exit process with retcode */}, + // pid: arg0 as u16 -> status: isize + Syscall::WaitPid => { /* FIXME: check if the process is running or get retcode */}, + + // None + Syscall::Stat => { /* FIXME: list processes */ }, + // None + Syscall::ListApp => { /* FIXME: list avaliable apps */}, + + // ---------------------------------------------------- + // NOTE: following syscall examples are implemented + // ---------------------------------------------------- + + // layout: arg0 as *const Layout -> ptr: *mut u8 + Syscall::Allocate => context.set_rax(sys_allocate(&args)), + // ptr: arg0 as *mut u8 + Syscall::Deallocate => sys_deallocate(&args), + // Unknown + Syscall::Unknown => warn!("Unhandled syscall: {:x?}", context.regs.rax), + } +} + +impl SyscallArgs { + pub fn new(syscall: Syscall, arg0: usize, arg1: usize, arg2: usize) -> Self { + Self { + syscall, + arg0, + arg1, + arg2, + } + } +} + +impl core::fmt::Display for SyscallArgs { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "SYSCALL: {:<10} (0x{:016x}, 0x{:016x}, 0x{:016x})", + format!("{:?}", self.syscall), + self.arg0, + self.arg1, + self.arg2 + ) + } +} diff --git a/src/0x04/pkg/kernel/src/interrupt/syscall/service.rs b/src/0x04/pkg/kernel/src/interrupt/syscall/service.rs new file mode 100644 index 0000000..bcf61aa --- /dev/null +++ b/src/0x04/pkg/kernel/src/interrupt/syscall/service.rs @@ -0,0 +1,73 @@ +use core::alloc::Layout; + +use crate::proc::*; +use crate::utils::*; + +use super::SyscallArgs; + +pub fn spawn_process(args: &SyscallArgs) -> usize { + // FIXME: get app name by args + // - core::str::from_utf8_unchecked + // - core::slice::from_raw_parts + // FIXME: spawn the process by name + // FIXME: handle spawn error, return 0 if failed + // FIXME: return pid as usize + + 0 +} + +pub fn sys_write(args: &SyscallArgs) -> usize { + // FIXME: get handle by fd + // FIXME: handle read from fd & return length + // - core::slice::from_raw_parts + // FIXME: return 0 if failed + + 0 +} + +pub fn sys_read(args: &SyscallArgs) -> usize { + // FIXME: just like sys_write + + 0 +} + +pub fn exit_process(args: &SyscallArgs, context: &mut ProcessContext) { + // FIXME: exit process with retcode +} + +pub fn list_process() { + // FIXME: list all processes +} + +pub fn sys_allocate(args: &SyscallArgs) -> usize { + let layout = unsafe { (args.arg0 as *const Layout).as_ref().unwrap() }; + + if layout.size() == 0 { + return 0; + } + + let ret = crate::memory::user::USER_ALLOCATOR + .lock() + .allocate_first_fit(*layout); + + match ret { + Ok(ptr) => ptr.as_ptr() as usize, + Err(_) => 0, + } +} + +pub fn sys_deallocate(args: &SyscallArgs) { + let layout = unsafe { (args.arg1 as *const Layout).as_ref().unwrap() }; + + if args.arg0 == 0 || layout.size() == 0 { + return; + } + + let ptr = args.arg0 as *mut u8; + + unsafe { + crate::memory::user::USER_ALLOCATOR + .lock() + .deallocate(core::ptr::NonNull::new_unchecked(ptr), *layout); + } +} diff --git a/src/0x04/pkg/kernel/src/main.rs b/src/0x04/pkg/kernel/src/main.rs new file mode 100644 index 0000000..ad1626c --- /dev/null +++ b/src/0x04/pkg/kernel/src/main.rs @@ -0,0 +1,23 @@ +#![no_std] +#![no_main] + +use ysos::*; +use ysos_kernel as ysos; + +extern crate alloc; + +boot::entry_point!(kernel_main); + +pub fn kernel_main(boot_info: &'static boot::BootInfo) -> ! { + ysos::init(boot_info); + ysos::wait(spawn_init()); + ysos::shutdown(boot_info); +} + +pub fn spawn_init() -> proc::ProcessId { + // NOTE: you may want to clear the screen before starting the shell + // print_serial!("\x1b[1;1H\x1b[2J"); + + proc::list_app(); + proc::spawn("sh").unwrap() +} diff --git a/src/0x04/pkg/kernel/src/memory/user.rs b/src/0x04/pkg/kernel/src/memory/user.rs new file mode 100644 index 0000000..81e9e72 --- /dev/null +++ b/src/0x04/pkg/kernel/src/memory/user.rs @@ -0,0 +1,36 @@ +use crate::proc::PageTableContext; +use linked_list_allocator::LockedHeap; +use x86_64::structures::paging::{ + mapper::MapToError, FrameAllocator, Mapper, Page, PageTableFlags, Size4KiB, +}; +use x86_64::VirtAddr; + +pub const USER_HEAP_START: usize = 0x4000_0000_0000; +pub const USER_HEAP_SIZE: usize = 1024 * 1024; // 1 MiB +const USER_HEAP_PAGE: usize = USER_HEAP_SIZE / crate::memory::PAGE_SIZE as usize; + +pub static USER_ALLOCATOR: LockedHeap = LockedHeap::empty(); + +// NOTE: export mod user / call in the kernel init / after frame allocator +pub fn init() { + init_user_heap().expect("User Heap Initialization Failed."); + info!("User Heap Initialized."); +} + +pub fn init_user_heap() -> Result<(), MapToError> { + // Get current pagetable mapper + let mapper = &mut PageTableContext::new().mapper(); + // Get global frame allocator + let frame_allocator = &mut *super::get_frame_alloc_for_sure(); + + // FIXME: use elf::map_range to allocate & map + // frames (R/W/User Access) + + unsafe { + USER_ALLOCATOR + .lock() + .init(USER_HEAP_START as *mut u8, USER_HEAP_SIZE); + } + + Ok(()) +} diff --git a/src/0x04/pkg/kernel/src/utils/resource.rs b/src/0x04/pkg/kernel/src/utils/resource.rs new file mode 100644 index 0000000..5b672a2 --- /dev/null +++ b/src/0x04/pkg/kernel/src/utils/resource.rs @@ -0,0 +1,46 @@ +use alloc::string::String; + +#[derive(Debug, Clone)] +pub enum StdIO { + Stdin, + Stdout, + Stderr, +} + +#[derive(Debug, Clone)] +pub enum Resource { + Console(StdIO), + Null, +} + +impl Resource { + pub fn read(&self, buf: &mut [u8]) -> Option { + match self { + Resource::Console(stdio) => match stdio { + &StdIO::Stdin => { + // FIXME: just read from kernel input buffer + Some(0) + } + _ => None, + }, + Resource::Null => Some(0), + } + } + + pub fn write(&self, buf: &[u8]) -> Option { + match self { + Resource::Console(stdio) => match *stdio { + StdIO::Stdin => None, + StdIO::Stdout => { + print!("{}", String::from_utf8_lossy(buf)); + Some(buf.len()) + } + StdIO::Stderr => { + warn!("{}", String::from_utf8_lossy(buf)); + Some(buf.len()) + } + }, + Resource::Null => Some(buf.len()), + } + } +} diff --git a/src/0x04/pkg/lib/Cargo.toml b/src/0x04/pkg/lib/Cargo.toml new file mode 100644 index 0000000..74e2c50 --- /dev/null +++ b/src/0x04/pkg/lib/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "yslib" +version = "0.4.0" +edition = "2021" + +[dependencies] +syscall_def = { package = "ysos_syscall", path = "../syscall" } diff --git a/src/0x04/pkg/lib/src/allocator.rs b/src/0x04/pkg/lib/src/allocator.rs new file mode 100644 index 0000000..7b933b6 --- /dev/null +++ b/src/0x04/pkg/lib/src/allocator.rs @@ -0,0 +1,19 @@ +pub struct KernelAllocator; + +unsafe impl alloc::alloc::GlobalAlloc for KernelAllocator { + unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 { + crate::sys_allocate(&layout) + } + unsafe fn dealloc(&self, ptr: *mut u8, layout: core::alloc::Layout) { + crate::sys_deallocate(ptr, &layout); + } +} + +#[global_allocator] +static ALLOCATOR: KernelAllocator = KernelAllocator; + +#[cfg(not(test))] +#[alloc_error_handler] +fn alloc_error_handler(layout: alloc::alloc::Layout) -> ! { + panic!("allocation error: {:#?}", layout) +} diff --git a/src/0x04/pkg/lib/src/io.rs b/src/0x04/pkg/lib/src/io.rs new file mode 100644 index 0000000..43c2a67 --- /dev/null +++ b/src/0x04/pkg/lib/src/io.rs @@ -0,0 +1,55 @@ +use crate::*; +use alloc::string::{String, ToString}; +use alloc::vec; + +pub struct Stdin; +pub struct Stdout; +pub struct Stderr; + +impl Stdin { + fn new() -> Self { + Self + } + + pub fn read_line(&self) -> String { + // FIXME: allocate string + // FIXME: read from input buffer + // - maybe char by char? + // FIXME: handle backspace / enter... + // FIXME: return string + + String::new() + } +} + +impl Stdout { + fn new() -> Self { + Self + } + + pub fn write(&self, s: &str) { + sys_write(1, s.as_bytes()); + } +} + +impl Stderr { + fn new() -> Self { + Self + } + + pub fn write(&self, s: &str) { + sys_write(2, s.as_bytes()); + } +} + +pub fn stdin() -> Stdin { + Stdin::new() +} + +pub fn stdout() -> Stdout { + Stdout::new() +} + +pub fn stderr() -> Stderr { + Stderr::new() +} diff --git a/src/0x04/pkg/lib/src/lib.rs b/src/0x04/pkg/lib/src/lib.rs new file mode 100644 index 0000000..be7a648 --- /dev/null +++ b/src/0x04/pkg/lib/src/lib.rs @@ -0,0 +1,56 @@ +#![allow(dead_code)] +#![feature(panic_info_message)] +#![feature(alloc_error_handler)] +#![cfg_attr(not(test), no_std)] + +#[macro_use] +pub mod macros; + +#[macro_use] +extern crate syscall_def; + +#[macro_use] +pub mod io; +pub mod allocator; +pub extern crate alloc; + +mod syscall; + +use core::fmt::*; + +pub use alloc::*; +pub use io::*; +pub use syscall::*; +pub use utils::*; + +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => ($crate::_print(format_args!($($arg)*))); +} + +#[macro_export] +macro_rules! println { + () => ($crate::print!("\n")); + ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*))); +} + +#[macro_export] +macro_rules! err { + ($($arg:tt)*) => ($crate::_err(format_args!($($arg)*))); +} + +#[macro_export] +macro_rules! errln { + () => ($crate::err!("\n")); + ($($arg:tt)*) => ($crate::err!("{}\n", format_args!($($arg)*))); +} + +#[doc(hidden)] +pub fn _print(args: Arguments) { + stdout().write(format!("{}", args).as_str()); +} + +#[doc(hidden)] +pub fn _err(args: Arguments) { + stderr().write(format!("{}", args).as_str()); +} diff --git a/src/0x04/pkg/lib/src/macros.rs b/src/0x04/pkg/lib/src/macros.rs new file mode 100644 index 0000000..1279e60 --- /dev/null +++ b/src/0x04/pkg/lib/src/macros.rs @@ -0,0 +1,37 @@ +use crate::alloc::string::ToString; +use crate::errln; + +#[macro_export] +macro_rules! entry { + ($fn:ident) => { + #[export_name = "_start"] + pub extern "C" fn __impl_start() { + let ret = $fn(); + // FIXME: after syscall, add lib::sys_exit(ret); + loop {} + } + }; +} + +#[cfg_attr(not(test), panic_handler)] +fn panic(info: &core::panic::PanicInfo) -> ! { + let location = if let Some(location) = info.location() { + alloc::format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ) + } else { + "Unknown location".to_string() + }; + let msg = if let Some(msg) = info.message() { + alloc::format!("{}", msg) + } else { + "No more message...".to_string() + }; + errln!("\n\n\rERROR: panicked at {}\n\n\r{}", location, msg); + + // FIXME: after syscall, add lib::sys_exit(1); + loop {} +} diff --git a/src/0x04/pkg/lib/src/syscall.rs b/src/0x04/pkg/lib/src/syscall.rs new file mode 100644 index 0000000..0b40d86 --- /dev/null +++ b/src/0x04/pkg/lib/src/syscall.rs @@ -0,0 +1,70 @@ +use syscall_def::Syscall; + +#[inline(always)] +pub fn sys_write(fd: u8, buf: &[u8]) -> Option { + let ret = syscall!( + Syscall::Write, + fd as u64, + buf.as_ptr() as u64, + buf.len() as u64 + ) as isize; + if ret.is_negative() { + None + } else { + Some(ret as usize) + } +} + +#[inline(always)] +pub fn sys_read(fd: u8, buf: &mut [u8]) -> Option { + let ret = syscall!( + Syscall::Read, + fd as u64, + buf.as_ptr() as u64, + buf.len() as u64 + ) as isize; + if ret.is_negative() { + None + } else { + Some(ret as usize) + } +} + +#[inline(always)] +pub fn sys_wait_pid(pid: u16) -> isize { + // FIXME: try to get the return value for process + // loop & halt until the process is finished + + 0 +} + +#[inline(always)] +pub fn sys_list_app() { + syscall!(Syscall::ListApp); +} + +#[inline(always)] +pub fn sys_stat() { + syscall!(Syscall::Stat); +} + +#[inline(always)] +pub fn sys_allocate(layout: &core::alloc::Layout) -> *mut u8 { + syscall!(Syscall::Allocate, layout as *const _) as *mut u8 +} + +#[inline(always)] +pub fn sys_deallocate(ptr: *mut u8, layout: &core::alloc::Layout) -> usize { + syscall!(Syscall::Deallocate, ptr, layout as *const _) +} + +#[inline(always)] +pub fn sys_spawn(path: &str) -> u16 { + syscall!(Syscall::Spawn, path.as_ptr() as u64, path.len() as u64) as u16 +} + +#[inline(always)] +pub fn sys_exit(code: isize) -> ! { + syscall!(Syscall::Exit, code as u64); + unreachable!("This process should be terminated by now.") +} diff --git a/src/0x04/pkg/syscall/Cargo.toml b/src/0x04/pkg/syscall/Cargo.toml new file mode 100644 index 0000000..5bb4369 --- /dev/null +++ b/src/0x04/pkg/syscall/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ysos_syscall" +version = "0.4.0" +edition = "2021" + +[dependencies] +num_enum = { version = "0.7", default-features = false } diff --git a/src/0x04/pkg/syscall/src/lib.rs b/src/0x04/pkg/syscall/src/lib.rs new file mode 100644 index 0000000..d6741a7 --- /dev/null +++ b/src/0x04/pkg/syscall/src/lib.rs @@ -0,0 +1,24 @@ +#![no_std] + +use num_enum::FromPrimitive; + +pub mod macros; + +#[repr(usize)] +#[derive(Clone, Debug, FromPrimitive)] +pub enum Syscall { + Read = 0, + Write = 1, + + Spawn = 59, + Exit = 60, + WaitPid = 61, + + ListApp = 65531, + Stat = 65532, + Allocate = 65533, + Deallocate = 65534, + + #[num_enum(default)] + Unknown = 65535, +} diff --git a/src/0x04/pkg/syscall/src/macros.rs b/src/0x04/pkg/syscall/src/macros.rs new file mode 100644 index 0000000..d6c4472 --- /dev/null +++ b/src/0x04/pkg/syscall/src/macros.rs @@ -0,0 +1,73 @@ +use crate::Syscall; +use core::arch::asm; + +#[doc(hidden)] +#[inline(always)] +pub fn syscall0(n: Syscall) -> usize { + let ret: usize; + unsafe { + asm!( + "int 0x80", in("rax") n as usize, + lateout("rax") ret + ); + } + ret +} + +#[doc(hidden)] +#[inline(always)] +pub fn syscall1(n: Syscall, arg0: usize) -> usize { + let ret: usize; + unsafe { + asm!( + "int 0x80", in("rax") n as usize, + in("rdi") arg0, + lateout("rax") ret + ); + } + ret +} + +#[doc(hidden)] +#[inline(always)] +pub fn syscall2(n: Syscall, arg0: usize, arg1: usize) -> usize { + let ret: usize; + unsafe { + asm!( + "int 0x80", in("rax") n as usize, + in("rdi") arg0, in("rsi") arg1, + lateout("rax") ret + ); + } + ret +} + +#[doc(hidden)] +#[inline(always)] +pub fn syscall3(n: Syscall, arg0: usize, arg1: usize, arg2: usize) -> usize { + let ret: usize; + unsafe { + asm!( + "int 0x80", in("rax") n as usize, + in("rdi") arg0, in("rsi") arg1, in("rdx") arg2, + lateout("rax") ret + ); + } + ret +} + +#[macro_export] +macro_rules! syscall { + ($n:expr) => { + $crate::macros::syscall0($n) + }; + ($n:expr, $a1:expr) => { + $crate::macros::syscall1($n, $a1 as usize) + }; + ($n:expr, $a1:expr, $a2:expr) => { + $crate::macros::syscall2($n, $a1 as usize, $a2 as usize) + }; + ($n:expr, $a1:expr, $a2:expr, $a3:expr) => { + $crate::macros::syscall3($n, $a1 as usize, $a2 as usize, $a3 as usize) + }; +}