diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e127d2c..9c45d3f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,8 +2,8 @@ name: Deploy on GitHub Pages on: workflow_dispatch: push: - branches: ["main", "lab/*"] - paths: ["docs/**"] + branches: ['main', 'lab/*'] + paths: ['docs/**'] permissions: contents: read @@ -11,7 +11,7 @@ permissions: id-token: write concurrency: - group: "pages" + group: 'pages' cancel-in-progress: false jobs: diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..5f769a5 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,14 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "overrides": [ + { + "files": "*.md", + "options": { + "tabWidth": 4 + } + } + ] +} diff --git a/README.md b/README.md index 49518b8..2fc0bed 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ ## 实验反馈 -- 交流实验中遇到的困难:[![Discussions](https://img.shields.io/github/discussions/YatSenOS/YatSenOS-Tutorial-Volume-2)](https://github.com/YatSenOS/YatSenOS-Tutorial-Volume-2/discussions) -- 提出实验设计的问题:[![Issues](https://img.shields.io/github/issues/YatSenOS/YatSenOS-Tutorial-Volume-2)](https://github.com/YatSenOS/YatSenOS-Tutorial-Volume-2/issues) -- 改善这个实验:[![Issues-pr](https://img.shields.io/github/issues-pr/YatSenOS/YatSenOS-Tutorial-Volume-2)](https://github.com/YatSenOS/YatSenOS-Tutorial-Volume-2/pulls) +- 交流实验中遇到的困难:[![Discussions](https://img.shields.io/github/discussions/YatSenOS/YatSenOS-Tutorial-Volume-2)](https://github.com/YatSenOS/YatSenOS-Tutorial-Volume-2/discussions) +- 提出实验设计的问题:[![Issues](https://img.shields.io/github/issues/YatSenOS/YatSenOS-Tutorial-Volume-2)](https://github.com/YatSenOS/YatSenOS-Tutorial-Volume-2/issues) +- 改善这个实验:[![Issues-pr](https://img.shields.io/github/issues-pr/YatSenOS/YatSenOS-Tutorial-Volume-2)](https://github.com/YatSenOS/YatSenOS-Tutorial-Volume-2/pulls) ## 常见问题解答 diff --git a/docs/css/extra.css b/docs/css/extra.css index 2703b3d..660fe93 100644 --- a/docs/css/extra.css +++ b/docs/css/extra.css @@ -11,11 +11,11 @@ } h5 { - font-size: .75rem; + font-size: 0.75rem; } h6 { - font-size: .7rem; + font-size: 0.7rem; } details, @@ -54,22 +54,22 @@ h2:before { } h3:before { counter-increment: h3; - content: counter(h2) "." counter(h3); + content: counter(h2) '.' counter(h3); margin-right: 0.8rem; } h4:before { counter-increment: h4; - content: counter(h2) "." counter(h3) "." counter(h4); + content: counter(h2) '.' counter(h3) '.' counter(h4); margin-right: 0.8rem; } h5:before { counter-increment: h5; - content: counter(h2) "." counter(h3) "." counter(h4) "." counter(h5); + content: counter(h2) '.' counter(h3) '.' counter(h4) '.' counter(h5); margin-right: 0.8rem; } h6:before { counter-increment: h6; - content: counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." + content: counter(h2) '.' counter(h3) '.' counter(h4) '.' counter(h5) '.' counter(h6); margin-right: 0.8rem; } @@ -84,20 +84,21 @@ table.wikitable { color: var(--md-default-fg-color); } -.wikitable th,.wikitable td { +.wikitable th, +.wikitable td { border: 1px #aaa solid; - padding: 0.2em + padding: 0.2em; } .wikitable th { background-color: var(--md-code-bg-color); - text-align: center + text-align: center; } .wikitable caption { font-weight: bold; } -[data-md-color-scheme="default"] .transparent-img { - filter: invert(1) hue-rotate(180deg) +[data-md-color-scheme='default'] .transparent-img { + filter: invert(1) hue-rotate(180deg); } diff --git a/docs/css/fonts.css b/docs/css/fonts.css index 4368606..2d49608 100644 --- a/docs/css/fonts.css +++ b/docs/css/fonts.css @@ -1,53 +1,53 @@ @font-face { - font-family: "JetBrains Mono"; + font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/ecefdeb1.woff2) format("woff2"); + src: url(../fonts/ecefdeb1.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } @font-face { - font-family: "JetBrains Mono"; + font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/9f48e746.woff2) format("woff2"); + src: url(../fonts/9f48e746.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } @font-face { - font-family: "JetBrains Mono"; + font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/4e44607d.woff2) format("woff2"); + src: url(../fonts/4e44607d.woff2) format('woff2'); unicode-range: U+0370-03FF; } @font-face { - font-family: "JetBrains Mono"; + font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/4c204432.woff2) format("woff2"); + src: url(../fonts/4c204432.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; } @font-face { - font-family: "JetBrains Mono"; + font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/a6e389bf.woff2) format("woff2"); + src: url(../fonts/a6e389bf.woff2) format('woff2'); unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } @font-face { - font-family: "JetBrains Mono"; + font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/7c53386f.woff2) format("woff2"); + src: url(../fonts/7c53386f.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; diff --git a/docs/general/faq.md b/docs/general/faq.md index ea42560..b5613c9 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -36,7 +36,6 @@ THU 的 rCore 课程实验是一个非常优秀的操作系统课程设计,它 > 如果需要进行多架构支持,需要对页表、寄存器、控制等内容进行抽象和统一,这部分的工程化抽象开销会很大的增加学生的学习成本。 - ### 为什么选择基于 UEFI 而不是自己使用汇编编写 bootloader? 汇编是一种非常底层的语言,对于大部分同学的学习经历来说,只在组成原理课程上接触过简单的 MIPS / RISC-V,而 x86 / ARM 基本接触很少。根据以往的经验,汇编的调试、编写、理解对于同学们都是很大的挑战。 diff --git a/docs/general/help.md b/docs/general/help.md index 3d5ceda..cf95372 100644 --- a/docs/general/help.md +++ b/docs/general/help.md @@ -58,18 +58,18 @@ no_comments: true 提问时需要避免 X-Y 问题:即你实际遇到了 X 问题,你认为 X 问题要用 Y 方法解决,在实现 Y 方法时遇到困难,然后向他人提问如何实现 Y 方法。但很多时候 Y 方法并不是 X 问题的正确解法,此时可能会得到错误的答案。引用 [X-Y Problem](https://xyproblem.info/) 的一个例子: -- X 问题:想要截取文件名的后缀名 -- Y 方法:截取文件名的后三位字符 -- 此时的 Y 方法并不是解决 X 问题的正确方法,因为后缀名不一定是三个字符 -- 正确的 X 问题的解法是找到最后一个 `.`,然后提取它之后的字符 -- 如果提问的时候只提及了 Y 方法,而没有 X 问题,就会出现这样的问题 +- X 问题:想要截取文件名的后缀名 +- Y 方法:截取文件名的后三位字符 +- 此时的 Y 方法并不是解决 X 问题的正确方法,因为后缀名不一定是三个字符 +- 正确的 X 问题的解法是找到最后一个 `.`,然后提取它之后的字符 +- 如果提问的时候只提及了 Y 方法,而没有 X 问题,就会出现这样的问题 因此在提问时,为了避免 X-Y 问题,建议按照下面的模板来描述完整的问题: -- 我正在做 X 问题 -- 我认为为了解决 X 问题,可以用 Y 方法 -- 在解决 X 问题时,采用了 Y 方法,过程中遇到了 Z 问题,尝试用 A 方法解决,但是没有效果 +- 我正在做 X 问题 +- 我认为为了解决 X 问题,可以用 Y 方法 +- 在解决 X 问题时,采用了 Y 方法,过程中遇到了 Z 问题,尝试用 A 方法解决,但是没有效果 而不是: -- 我遇到了 Z 问题,求救 T_T +- 我遇到了 Z 问题,求救 T_T diff --git a/docs/general/specification.md b/docs/general/specification.md index 77ae3bc..911c7ca 100644 --- a/docs/general/specification.md +++ b/docs/general/specification.md @@ -46,9 +46,9 @@ no_comments: true 建议使用工具来辅助代码风格的检查: -- 使用 `rustfmt`,`cargo fmt --all` 命令来格式化代码; -- 使用 `clippy`,`cargo clippy` 来检查代码风格。 -- 使用 [typos](https://github.com/crate-ci/typos) 检查拼写错误,可以使用 `cargo install typos-cli` 安装。 +- 使用 `rustfmt`,`cargo fmt --all` 命令来格式化代码; +- 使用 `clippy`,`cargo clippy` 来检查代码风格。 +- 使用 [typos](https://github.com/crate-ci/typos) 检查拼写错误,可以使用 `cargo install typos-cli` 安装。 !!! note "请注意,由于项目 target 不尽相同,`clippy` 需要在每一个 `package` 下使用。" @@ -56,18 +56,18 @@ no_comments: true ### 提交历史 -- 每个提交都应该有一定的意义,例如实现了新功能,修复了一个问题,定义了新的函数; -- 比较复杂的程序,要边开发边提交,而不是写完了再一次性提交; -- 不强求线性历史,**但是不允许使用 force push**。 +- 每个提交都应该有一定的意义,例如实现了新功能,修复了一个问题,定义了新的函数; +- 比较复杂的程序,要边开发边提交,而不是写完了再一次性提交; +- 不强求线性历史,**但是不允许使用 force push**。 ### 提交消息 -- 简单明了地描述这个提交的内容; -- 建议用英文写,用中文写也可以; -- 不要编写的过于详细或过于简略; -- 可以采用一些格式,例如 [**Conventional Commits**](https://www.conventionalcommits.org/en/v1.0.0/#examples); -- 不掺杂个人情绪; -- 可以添加一些 Emoji,[gitmoji](https://gitmoji.dev/) 为提交说明中使用的 Emoji 提出了一些建议,可以参考。 +- 简单明了地描述这个提交的内容; +- 建议用英文写,用中文写也可以; +- 不要编写的过于详细或过于简略; +- 可以采用一些格式,例如 [**Conventional Commits**](https://www.conventionalcommits.org/en/v1.0.0/#examples); +- 不掺杂个人情绪; +- 可以添加一些 Emoji,[gitmoji](https://gitmoji.dev/) 为提交说明中使用的 Emoji 提出了一些建议,可以参考。 ### 代码打包 diff --git a/docs/general/typst.md b/docs/general/typst.md index a127294..c7ec239 100644 --- a/docs/general/typst.md +++ b/docs/general/typst.md @@ -34,7 +34,6 @@ 之后,在当前文件夹中打开终端或使用 VSCode,正常使用 typst 即可,例如: - !!! note "保证 typst 的工作路径为 `.`" 引用其他更上层的目录是被 Typst 默认禁止的,这是为了避免[任意文件读取](https://github.com/typst/typst/issues/219)问题。 @@ -165,4 +164,4 @@ $ sum_(k=1)^n k = (n(n+1)) / 2 $ 会被渲染成单独的一行: -$$ \sum_{k=1}^n k = \frac{n(n+1)}{2} $$ +$$ \sum\_{k=1}^n k = \frac{n(n+1)}{2} $$ diff --git a/docs/index.md b/docs/index.md index 9b55bd9..89d155a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,12 +10,12 @@ 每一次实验区间为两周。 -- [实验零:环境搭建与实验准备](./labs/0x00/index.md) -- [实验一:操作系统的启动](./labs/0x01/index.md) -- [实验二:中断处理](./labs/0x02/index.md) -- [实验三:内核线程与缺页异常](./labs/0x03/index.md) -- [实验四:用户程序与系统调用](./labs/0x04/index.md) -- [实验五:fork 的实现、并发与锁机制](./labs/0x05/index.md) -- [实验六:硬盘驱动与文件系统](./labs/0x06/index.md) -- [实验七:更好的内存管理](./labs/0x07/index.md) -- [实验八:扩展实验](./labs/0x08/index.md) +- [实验零:环境搭建与实验准备](./labs/0x00/index.md) +- [实验一:操作系统的启动](./labs/0x01/index.md) +- [实验二:中断处理](./labs/0x02/index.md) +- [实验三:内核线程与缺页异常](./labs/0x03/index.md) +- [实验四:用户程序与系统调用](./labs/0x04/index.md) +- [实验五:fork 的实现、并发与锁机制](./labs/0x05/index.md) +- [实验六:硬盘驱动与文件系统](./labs/0x06/index.md) +- [实验七:更好的内存管理](./labs/0x07/index.md) +- [实验八:扩展实验](./labs/0x08/index.md) diff --git a/docs/labs/0x00/index.md b/docs/labs/0x00/index.md index 5b55be5..47c92ce 100644 --- a/docs/labs/0x00/index.md +++ b/docs/labs/0x00/index.md @@ -22,11 +22,11 @@ 对于本次实验内容,你需要参考学习如下实验资料: -- [Linux 环境配置](../../wiki/linux.md) -- [Windows 环境配置](../../wiki/windows.md) -- [Rust 语言学习](../../wiki/rust.md) -- [UEFI 启动过程](../../wiki/uefi.md) -- [QEMU 使用参考](../../wiki/qemu.md) +- [Linux 环境配置](../../wiki/linux.md) +- [Windows 环境配置](../../wiki/windows.md) +- [Rust 语言学习](../../wiki/rust.md) +- [UEFI 启动过程](../../wiki/uefi.md) +- [QEMU 使用参考](../../wiki/qemu.md) ## 实验任务与要求 @@ -37,7 +37,7 @@ 3. 依据 [实验任务](./tasks.md) 完成实验。 - 代码编写任务:观察提供的代码,完善所有标记为 `FIXME:` 的部分,并验证结果是否符合预期。**请在报告中介绍实现思路,截图展示关键结果。** - - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。*注:思考题可能也是理解代码、实现功能的重要提示。* + - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。_注:思考题可能也是理解代码、实现功能的重要提示。_ - Bonus 加分项:学有余力的同学可以任选 Bonus 部分完成,尝试完成更多的功能,并在报告中进行展示。这部分内容不是必须的要求。 4. 请在实验报告中涵盖相关任务的实现截图、实验任务对应问题的解答、实验过程中遇到的问题与解决方案等内容。 diff --git a/docs/labs/0x00/tasks.md b/docs/labs/0x00/tasks.md index 70e00e6..74c3ae7 100644 --- a/docs/labs/0x00/tasks.md +++ b/docs/labs/0x00/tasks.md @@ -14,10 +14,10 @@ 我们推荐在以下环境进行实验: -- Ubuntu 22.04 LTS (jammy) on WSL 2 **(推荐 Windows 用户选择)** -- Windows 10/11 **(Windows 原生备选,GDB 相关功能无法使用)** -- Ubuntu 22.04 LTS (jammy) -- macOS with Apple Silicon +- Ubuntu 22.04 LTS (jammy) on WSL 2 **(推荐 Windows 用户选择)** +- Windows 10/11 **(Windows 原生备选,GDB 相关功能无法使用)** +- Ubuntu 22.04 LTS (jammy) +- macOS with Apple Silicon 以上环境经过我们的测试和验证,可以正常进行实验。对于其他常用的 Linux 发行版,通常也可以正常进行实验,但我们不提供技术支持。 @@ -33,11 +33,11 @@ 在 Windows 平台上我们建议通过 VSCode + Python + CodeLLDB 插件进行开发、调试。 -- 对于选择使用 Linux 的同学,请参考 [Linux 环境配置](../../wiki/linux.md) 进行配置,**文档包含 Linux 相关安装指南**。 +- 对于选择使用 Linux 的同学,请参考 [Linux 环境配置](../../wiki/linux.md) 进行配置,**文档包含 Linux 相关安装指南**。 -- 对于选择使用 Windows 的同学,请参考 [Windows 环境配置](../../wiki/windows.md) 进行配置。 +- 对于选择使用 Windows 的同学,请参考 [Windows 环境配置](../../wiki/windows.md) 进行配置。 -- 对于选择使用 macOS 的同学,请安装 `brew` 和相应工具,参考 [Linux 环境配置](../../wiki/linux.md) 进行配置。 +- 对于选择使用 macOS 的同学,请安装 `brew` 和相应工具,参考 [Linux 环境配置](../../wiki/linux.md) 进行配置。 ## 尝试使用 Rust 进行编程 @@ -49,7 +49,7 @@ - 在你不熟悉新语言的时候,我们非常推荐你借助 LLM 进行学习。 - 在满足题目描述的情况下,如有需要,**参数类型和返回值类型可以自行选择和修改**。 -1. 使用 Rust 编写一个程序,完成以下任务: +1. 使用 Rust 编写一个程序,完成以下任务: 1. 创建一个函数 `count_down(seconds: u64)` @@ -83,13 +83,13 @@ 注意:在处理文件操作时,需要使用到 Rust 的文件处理相关库,如 `std::fs` 和 `std::io`。在处理错误时,需要使用到 Rust 的错误处理机制,如 `expect` 和 `unwrap` 等。 -2. 实现一个进行字节数转换的函数,并格式化输出: +2. 实现一个进行字节数转换的函数,并格式化输出: - 1. 实现函数 `humanized_size(size: u64) -> (f64, &'static str)` 将字节数转换为人类可读的大小和单位 + 1. 实现函数 `humanized_size(size: u64) -> (f64, &'static str)` 将字节数转换为人类可读的大小和单位 使用 1024 进制,并使用二进制前缀(B, KiB, MiB, GiB)作为单位 - 2. 补全格式化代码,使得你的实现能够通过如下测试: + 2. 补全格式化代码,使得你的实现能够通过如下测试: ```rust #[test] @@ -123,28 +123,26 @@ - `use super::*;` 表示引入当前模块的所有内容(tests 模块是当前模块的子模块) - `#[test]` 表示该函数是一个测试函数,会被 `cargo test` 执行 - -3. **自行搜索学习如何利用现有的 crate** 在终端中输出彩色的文字 +3. **自行搜索学习如何利用现有的 crate** 在终端中输出彩色的文字 输出一些带有颜色的字符串,并尝试直接使用 `print!` 宏输出一到两个相同的效果。 尝试输出如下格式和内容: - - `INFO: Hello, world!`,其中 `INFO:` 为绿色,后续内容为白色 - - `WARNING: I'm a teapot!`,颜色为黄色,加粗,并为 `WARNING` 添加下划线 - - `ERROR: KERNEL PANIC!!!`,颜色为红色,加粗,并尝试让这一行在控制行窗口居中 - - 一些你想尝试的其他效果和内容…… + - `INFO: Hello, world!`,其中 `INFO:` 为绿色,后续内容为白色 + - `WARNING: I'm a teapot!`,颜色为黄色,加粗,并为 `WARNING` 添加下划线 + - `ERROR: KERNEL PANIC!!!`,颜色为红色,加粗,并尝试让这一行在控制行窗口居中 + - 一些你想尝试的其他效果和内容…… !!! tip "如果你想进一步了解,可以尝试搜索 **ANSI 转义序列**" - -4. 使用 `enum` 对类型实现同一化 +4. 使用 `enum` 对类型实现同一化 实现一个名为 `Shape` 的枚举,并为它实现 `pub fn area(&self) -> f64` 方法,用于计算不同形状的面积。 - - 你可能需要使用模式匹配来达到相应的功能 - - 请实现 `Rectangle` 和 `Circle` 两种 `Shape`,并使得 `area` 函数能够正确计算它们的面积 - - 使得你的实现能够通过如下测试: + - 你可能需要使用模式匹配来达到相应的功能 + - 请实现 `Rectangle` 和 `Circle` 两种 `Shape`,并使得 `area` 函数能够正确计算它们的面积 + - 使得你的实现能够通过如下测试: ```rust #[test] @@ -162,13 +160,13 @@ !!! note "可以使用标准库提供的 `std::f64::consts::PI`" -5. 实现一个元组结构体 `UniqueId(u16)` +5. 实现一个元组结构体 `UniqueId(u16)` 使得每次调用 `UniqueId::new()` 时总会得到一个新的不重复的 `UniqueId`。 - - 你可以在函数体中定义 `static` 变量来存储一些全局状态 - - 你可以尝试使用 `std::sync::atomic::AtomicU16` 来确保多线程下的正确性(无需进行验证,相关原理将在 Lab 5 介绍,此处不做要求) - - 使得你的实现能够通过如下测试: + - 你可以在函数体中定义 `static` 变量来存储一些全局状态 + - 你可以尝试使用 `std::sync::atomic::AtomicU16` 来确保多线程下的正确性(无需进行验证,相关原理将在 Lab 5 介绍,此处不做要求) + - 使得你的实现能够通过如下测试: ```rust #[test] diff --git a/docs/labs/0x01/index.md b/docs/labs/0x01/index.md index b4f9ccb..146b247 100644 --- a/docs/labs/0x01/index.md +++ b/docs/labs/0x01/index.md @@ -21,10 +21,10 @@ 对于本次实验内容,你需要参考学习如下实验资料: -- [分页内存简述](../../wiki/paging.md) -- [ELF 文件格式](../../wiki/elf.md) -- [串口输出简介](../../wiki/uart.md) -- [实验调试指南](../../wiki/debug.md) +- [分页内存简述](../../wiki/paging.md) +- [ELF 文件格式](../../wiki/elf.md) +- [串口输出简介](../../wiki/uart.md) +- [实验调试指南](../../wiki/debug.md) ## 实验任务与要求 @@ -35,7 +35,7 @@ 3. 依据 [实验任务](./tasks.md) 完成实验。 - 代码编写任务:观察提供的代码,完善所有标记为 `FIXME:` 的部分,并验证结果是否符合预期。**请在报告中介绍实现思路,截图展示关键结果。** - - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。*注:思考题可能也是理解代码、实现功能的重要提示。* + - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。_注:思考题可能也是理解代码、实现功能的重要提示。_ - Bonus 加分项:学有余力的同学可以任选 Bonus 部分完成,尝试完成更多的功能,并在报告中进行展示。这部分内容不是必须的要求。 4. 请在实验报告中涵盖相关任务的实现截图、实验任务对应问题的解答、实验过程中遇到的问题与解决方案等内容。 diff --git a/docs/labs/0x01/tasks.md b/docs/labs/0x01/tasks.md index 2dedf21..865ef9a 100644 --- a/docs/labs/0x01/tasks.md +++ b/docs/labs/0x01/tasks.md @@ -32,22 +32,22 @@ ```json { - "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", - "linker-flavor": "ld.lld", - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "arch": "x86_64", - "os": "none", - "executables": true, - "linker": "rust-lld", - "disable-redzone": true, - "features": "-mmx,-sse,+soft-float", - "panic-strategy": "abort", - "pre-link-args": { - "ld.lld": ["-Tpkg/kernel/config/kernel.ld", "-export-dynamic"] - } + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + "linker-flavor": "ld.lld", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "arch": "x86_64", + "os": "none", + "executables": true, + "linker": "rust-lld", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float", + "panic-strategy": "abort", + "pre-link-args": { + "ld.lld": ["-Tpkg/kernel/config/kernel.ld", "-export-dynamic"] + } } ``` @@ -89,11 +89,11 @@ SECTIONS { 实验在 `pkg/boot` 中提供了一些基本的功能实现: -- `allocator.rs`:为 `uefi` crate 中的 `UEFIFrameAllocator` 实现 `x86_64` crate 所定义的 `FrameAllocator` trait,以便在页面分配、页表映射时使用。 -- `config.rs`:提供了一个读取并解析 `boot.conf` 的基本实现,可以使用它来自定义 bootloader 的行为、启动参数等等。 -- `fs.rs`:提供了在 UEFI 环境下打开文件、列出目录、加载文件、释放 `ElfFile` 的功能,你可以参考这部分代码了解与文件系统相关操作的基本内容。在后期的实验中,你将自己实现对文件系统的相关操作。 -- `lib.rs`:这部分内容定义了 bootloader 将要传递给内核的信息、内核的入口点、跳转到内核的实现等等。定义在 `lib.rs` 中是为了能够在内核实现中引用这些数据结构,确保内核与 bootloader 的数据结构一致。 -- `main.rs`:这里是 bootloader 的入口点,你可以在这里编写你的 bootloader 代码。 +- `allocator.rs`:为 `uefi` crate 中的 `UEFIFrameAllocator` 实现 `x86_64` crate 所定义的 `FrameAllocator` trait,以便在页面分配、页表映射时使用。 +- `config.rs`:提供了一个读取并解析 `boot.conf` 的基本实现,可以使用它来自定义 bootloader 的行为、启动参数等等。 +- `fs.rs`:提供了在 UEFI 环境下打开文件、列出目录、加载文件、释放 `ElfFile` 的功能,你可以参考这部分代码了解与文件系统相关操作的基本内容。在后期的实验中,你将自己实现对文件系统的相关操作。 +- `lib.rs`:这部分内容定义了 bootloader 将要传递给内核的信息、内核的入口点、跳转到内核的实现等等。定义在 `lib.rs` 中是为了能够在内核实现中引用这些数据结构,确保内核与 bootloader 的数据结构一致。 +- `main.rs`:这里是 bootloader 的入口点,你可以在这里编写你的 bootloader 代码。 同时在 `pkg/elf` 中实验提供了加载 ELF 文件的相关代码,其中也有需要你自己实现的部分。 @@ -183,23 +183,23 @@ unsafe { 最后,你需要检验是否成功加载了内核: -- 使用 `make build DBG_INFO=true` 或 `python ysos.py build -p debug` 编译内核,确保编译时开启了调试信息。 -- 使用 `make debug` 或 `python ysos.py launch -d` 启动 QEMU 并进入调试模式,这时候 QEMU 将会等待 GDB 的连接。 -- 在另一个终端中,使用 `gdb -q` 命令进入 GDB 调试环境。 +- 使用 `make build DBG_INFO=true` 或 `python ysos.py build -p debug` 编译内核,确保编译时开启了调试信息。 +- 使用 `make debug` 或 `python ysos.py launch -d` 启动 QEMU 并进入调试模式,这时候 QEMU 将会等待 GDB 的连接。 +- 在另一个终端中,使用 `gdb -q` 命令进入 GDB 调试环境。 !!! note "使用 `.gdbinit` 方便你的调试过程" - 以下是一个 `.gdbinit` 的例子,你可以将其放置在你的工作目录下,这样每次进入 GDB 调试环境时,它都会自动加载。请注意部分指令是 `gef` 所提供的,详情请见调试文档。 + 以下是一个 `.gdbinit` 的例子,你可以将其放置在你的工作目录下,这样每次进入 GDB 调试环境时,它都会自动加载。请注意部分指令是 `gef` 所提供的,详情请见调试文档。 - ```bash - file esp/KERNEL.ELF - gef-remote localhost 1234 - tmux-setup - b ysos_kernel::init - ``` + ```bash + file esp/KERNEL.ELF + gef-remote localhost 1234 + tmux-setup + b ysos_kernel::init + ``` -- 使用 `c` 命令继续执行,你将会看到 QEMU 窗口中的输出,同时 GDB 将会在断点处停下。 -- 查看断点处的汇编和符号是否正确,使用 `vmmap` 和 `readelf` 等指令查看内核的加载情况。 +- 使用 `c` 命令继续执行,你将会看到 QEMU 窗口中的输出,同时 GDB 将会在断点处停下。 +- 查看断点处的汇编和符号是否正确,使用 `vmmap` 和 `readelf` 等指令查看内核的加载情况。 !!! tip "遇到了奇怪的问题?尝试更改 `log::set_max_level(log::LevelFilter::Info);` 来调整日志输出的等级,以便你能够观察到更多的日志输出。" @@ -362,7 +362,7 @@ pub fn try_lock(&self) -> Option> { 为了与串口设备进行交互,你需要存储一个设备端口的基地址,对于 COM1 端口,它的基地址为 `0x3F8`。 -在这一基地址的基础上,你可以通过偏移量来访问串口设备的寄存器,例如 `0x3F8 + 0` 将会访问串口设备的数据寄存器,`0x3F8 + 1` 将会访问串口设备的中断使能寄存器等等。*上方链接中的资料中有详细的寄存器地址和偏移量的对应关系。* +在这一基地址的基础上,你可以通过偏移量来访问串口设备的寄存器,例如 `0x3F8 + 0` 将会访问串口设备的数据寄存器,`0x3F8 + 1` 将会访问串口设备的中断使能寄存器等等。_上方链接中的资料中有详细的寄存器地址和偏移量的对应关系。_ 为了与这些寄存器进行交互,你可以使用 `x86_64` crate 中的 `Port`,以下是一个简单的例子: @@ -374,8 +374,8 @@ let ret = data.read(); 对于只读和只写的寄存器,你可以使用 `PortWriteOnly` 和 `PortReadOnly` 来从类型系统上防止误操作的发生。 -- 偏移量为 `1` 的寄存器是中断使能寄存器,可以使用 `PortWriteOnly::new(base + 1)` 操作。 -- 偏移量为 `5` 的寄存器是线控寄存器,可以使用 `PortReadOnly::new(base + 5)` 操作。 +- 偏移量为 `1` 的寄存器是中断使能寄存器,可以使用 `PortWriteOnly::new(base + 1)` 操作。 +- 偏移量为 `5` 的寄存器是线控寄存器,可以使用 `PortReadOnly::new(base + 5)` 操作。 对于串口设备,其寄存器均为 8 位,你可以使用 `u8` 类型来进行读写操作。 @@ -509,17 +509,17 @@ println!("{}", record.args()); ## 思考题 -1. 在 `pkg/kernel` 的 `Cargo.toml` 中,指定了依赖中 `boot` 包为 `default-features = false`,这是为了避免什么问题?请结合 `pkg/boot` 的 `Cargo.toml` 谈谈你的理解。 +1. 在 `pkg/kernel` 的 `Cargo.toml` 中,指定了依赖中 `boot` 包为 `default-features = false`,这是为了避免什么问题?请结合 `pkg/boot` 的 `Cargo.toml` 谈谈你的理解。 -2. 在 `pkg/boot/src/main.rs` 中参考相关代码,聊聊 `max_phys_addr` 是如何计算的,为什么要这么做? +2. 在 `pkg/boot/src/main.rs` 中参考相关代码,聊聊 `max_phys_addr` 是如何计算的,为什么要这么做? -3. 串口驱动是在进入内核后启用的,那么在进入内核之前,显示的内容是如何输出的? +3. 串口驱动是在进入内核后启用的,那么在进入内核之前,显示的内容是如何输出的? -4. 在 QEMU 中,我们通过指定 `-nographic` 参数来禁用图形界面,这样 QEMU 会默认将串口输出重定向到主机的标准输出。 +4. 在 QEMU 中,我们通过指定 `-nographic` 参数来禁用图形界面,这样 QEMU 会默认将串口输出重定向到主机的标准输出。 - - 假如我们将 `Makefile` 中取消该选项,QEMU 的输出窗口会发生什么变化?请观察指令 `make run QEMU_OUTPUT=` 的输出,结合截图分析对应现象。 - - 在移除 `-nographic` 的情况下,如何依然将串口重定向到主机的标准输入输出?请尝试自行构造命令行参数,并查阅 QEMU 的文档,进行实验。 - - 如果你使用 `ysos.py` 来启动 qemu,可以尝试修改 `-o` 选项来实现上述功能。 + - 假如我们将 `Makefile` 中取消该选项,QEMU 的输出窗口会发生什么变化?请观察指令 `make run QEMU_OUTPUT=` 的输出,结合截图分析对应现象。 + - 在移除 `-nographic` 的情况下,如何依然将串口重定向到主机的标准输入输出?请尝试自行构造命令行参数,并查阅 QEMU 的文档,进行实验。 + - 如果你使用 `ysos.py` 来启动 qemu,可以尝试修改 `-o` 选项来实现上述功能。 !!! note "现象观察提示" @@ -527,7 +527,6 @@ println!("{}", record.args()); **这一步骤不做要求,如果自身环境实现遇到困难,可以尝试与其他同学合作进行观察。** - ## 加分项 1. 😋 线控寄存器的每一比特都有特定的含义,尝试使用 `bitflags` 宏来定义这些标志位,并在 `uart16550` 驱动中使用它们。 diff --git a/docs/labs/0x02/index.md b/docs/labs/0x02/index.md index 1ce2c95..57d8813 100644 --- a/docs/labs/0x02/index.md +++ b/docs/labs/0x02/index.md @@ -23,9 +23,9 @@ 对于本次实验内容,你需要参考学习如下实验资料: -- [CPU 异常与中断处理](../../wiki/interrupts.md) -- [APIC 可编程中断控制器](../../wiki/apic.md) -- [x64 数据结构概述](../../wiki/structures.md) +- [CPU 异常与中断处理](../../wiki/interrupts.md) +- [APIC 可编程中断控制器](../../wiki/apic.md) +- [x64 数据结构概述](../../wiki/structures.md) ## 实验任务与要求 @@ -36,7 +36,7 @@ 3. 依据 [实验任务](./tasks.md) 完成实验。 - 代码编写任务:观察提供的代码,完善所有标记为 `FIXME:` 的部分,并验证结果是否符合预期。**请在报告中介绍实现思路,截图展示关键结果。** - - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。*注:思考题可能也是理解代码、实现功能的重要提示。* + - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。_注:思考题可能也是理解代码、实现功能的重要提示。_ - Bonus 加分项:学有余力的同学可以任选 Bonus 部分完成,尝试完成更多的功能,并在报告中进行展示。这部分内容不是必须的要求。 4. 请在实验报告中涵盖相关任务的实现截图、实验任务对应问题的解答、实验过程中遇到的问题与解决方案等内容。 diff --git a/docs/labs/0x02/tasks.md b/docs/labs/0x02/tasks.md index 995fcd9..266c3a6 100644 --- a/docs/labs/0x02/tasks.md +++ b/docs/labs/0x02/tasks.md @@ -22,19 +22,19 @@ 在 `pkg/kernel/src/memory` 文件夹中,增量代码补充包含了如下的模块: -- `address.rs`:定义了物理地址到虚拟地址的转换函数,这一模块接受启动结构体提供的物理地址偏移,从而对物理地址进行转换。此部分内容在 lab 1 中已经有所涉及,你可以参考[完整的物理地址映射](https://os.phil-opp.com/paging-implementation/#map-the-complete-physical-memory)进行深入了解。 -- `frames.rs`:利用 bootloader 传入的内存布局进行物理内存帧分配,实现 x86_64 的 `FrameAllocator` trait。**本次实验中不会涉及,后续实验中会用到。** -- `gdt.rs`:定义 TSS 和 GDT,为内核提供内存段描述符和任务状态段。 -- `allocator.rs`:注册内核堆分配器,为内核堆分配提供能力。从而能够在内核中使用 `alloc` 提供的操作和数据结构,进行动态内存分配的操作,如 `Vec`、`String`、`Box` 等。 +- `address.rs`:定义了物理地址到虚拟地址的转换函数,这一模块接受启动结构体提供的物理地址偏移,从而对物理地址进行转换。此部分内容在 lab 1 中已经有所涉及,你可以参考[完整的物理地址映射](https://os.phil-opp.com/paging-implementation/#map-the-complete-physical-memory)进行深入了解。 +- `frames.rs`:利用 bootloader 传入的内存布局进行物理内存帧分配,实现 x86_64 的 `FrameAllocator` trait。**本次实验中不会涉及,后续实验中会用到。** +- `gdt.rs`:定义 TSS 和 GDT,为内核提供内存段描述符和任务状态段。 +- `allocator.rs`:注册内核堆分配器,为内核堆分配提供能力。从而能够在内核中使用 `alloc` 提供的操作和数据结构,进行动态内存分配的操作,如 `Vec`、`String`、`Box` 等。 !!! note "动态内存分配算法在这里不做要求,本次实验直接使用现有的库赋予内核堆分配能力。" 在 `pkg/kernel/src/interrupt` 文件夹中,增量代码补充包含了如下的模块: -- `apic`:有关 XAPIC、IOAPIC 和 LAPIC 的定义和实现。 -- `consts.rs`:有关于中断向量、IRQ 的常量定义。 -- `exceptions.rs`:包含了 CPU 异常的处理函数,并暴露 `register_idt` 用于注册 IDT。 -- `mod.rs`:定义了 `init` 函数,用于初始化中断系统,加载 IDT。 +- `apic`:有关 XAPIC、IOAPIC 和 LAPIC 的定义和实现。 +- `consts.rs`:有关于中断向量、IRQ 的常量定义。 +- `exceptions.rs`:包含了 CPU 异常的处理函数,并暴露 `register_idt` 用于注册 IDT。 +- `mod.rs`:定义了 `init` 函数,用于初始化中断系统,加载 IDT。 ## GDT 与 TSS @@ -179,7 +179,7 @@ impl XApic { 下面以部分操作为例讲解如何进行 APIC 的初始化。 -- 检测系统中是否存在 APIC,在 `x86_64` 中可以通过如下代码获知: +- 检测系统中是否存在 APIC,在 `x86_64` 中可以通过如下代码获知: ```rust CpuId::new().get_feature_info().map( @@ -187,35 +187,35 @@ impl XApic { ).unwrap_or(false) ``` -- 操作 SPIV 寄存器,启用 APIC 并设置 Spurious IRQ Vector。 +- 操作 SPIV 寄存器,启用 APIC 并设置 Spurious IRQ Vector。 查询文档可知,SPIV 寄存器的偏移量为 0xF0。其位描述如下: - - - - - - - - - - - - - - - - - - - - - - - - -
31109843210
FCENVector1111
+ + + + + + + + + + + + + + + + + + + + + + + + +
31109843210
FCENVector1111
因此,需要在保持其他位不变的情况下,将 EN bit 设置为 1,并将 Vector 设置为 `Irq::Spurious`,但是请注意实际设置的中断向量号需要加上 `Interrupts::IrqBase`。同时,此寄存器的 0-3 bit 无法被修改,始终为 1。 @@ -230,88 +230,88 @@ impl XApic { self.write(0xF0, spiv); ``` -- 设置 LVT 寄存器。 +- 设置 LVT 寄存器。 Local Vector Table 寄存器用于设置中断向量号和触发模式。它们的位描述如下: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
31181716151413121180
Timer-TPM-DS-Vector
LINT0-MTMRIIPDS-DModeVector
LINT1-MTMRIIPDS-DModeVector
ERROR-M-DS-Vector
PCINT-M-DS-DModeVector
- - - Vector 为中断向量号,当中断发生时,CPU 会跳转到中断向量表中对应处理程序执行。 - - DMode(Delivery Mode)为中断传递模式,本实验中不做理解要求。 - - DS(Delivery Status)为中断传递状态,只读。 - - M(Mask)为中断屏蔽位,取值为 1 表示中断已屏蔽。 - - TP(Timer Periodic Mode)为定时器周期模式,决定定时器周期触发还是仅触发一次。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
31181716151413121180
Timer-TPM-DS-Vector
LINT0-MTMRIIPDS-DModeVector
LINT1-MTMRIIPDS-DModeVector
ERROR-M-DS-Vector
PCINT-M-DS-DModeVector
+ + - Vector 为中断向量号,当中断发生时,CPU 会跳转到中断向量表中对应处理程序执行。 + - DMode(Delivery Mode)为中断传递模式,本实验中不做理解要求。 + - DS(Delivery Status)为中断传递状态,只读。 + - M(Mask)为中断屏蔽位,取值为 1 表示中断已屏蔽。 + - TP(Timer Periodic Mode)为定时器周期模式,决定定时器周期触发还是仅触发一次。 其余的位暂时不需要关注,如有兴趣可以参考 APIC 文档下的参考资料。 @@ -333,12 +333,12 @@ impl XApic { self.write(0x350, 1 << 16); // set Mask ``` -- 设置计时器相关寄存器。 +- 设置计时器相关寄存器。 APIC 中控制计时器的寄存器包括 TDCR、TICR 和 LVT Timer。其中,TDCR 用于设置分频系数,TICR 用于设置初始计数值。 - - TDCR(0x3E0) 的分频系数决定了总线时钟与计时器时钟的比例,也即计时器的计数频率。 - - TICR(0x380) 的初始计数值决定了计时器的计数周期,每当计数到 0 时,就会触发中断。 + - TDCR(0x3E0) 的分频系数决定了总线时钟与计时器时钟的比例,也即计时器的计数频率。 + - TICR(0x380) 的初始计数值决定了计时器的计数周期,每当计数到 0 时,就会触发中断。 分频系数和 TDCR 寄存器的取值关系如下表所示,第二比特总是为 0: @@ -356,7 +356,7 @@ impl XApic { self.write(0x380, 0x20000); // set initial count to 0x20000 ``` -- 清除错误状态寄存器。 +- 清除错误状态寄存器。 APIC 中的错误状态寄存器(Error Status Register, 0x280)用于记录 APIC 内部的错误状态。当 APIC 发生错误时,CPU 会将错误信息写入此寄存器。为了避免错误状态寄存器中的错误信息影响后续的错误处理,需要在初始化 APIC 时清除错误状态寄存器中的错误信息。 @@ -367,66 +367,66 @@ impl XApic { self.write(0x280, 0); ``` -- 设置 ICR 寄存器。 +- 设置 ICR 寄存器。 中断命令寄存器由两个 32 位寄存器组成,一个在 0x300,另一个在 0x310。它用于向不同的处理器发送中断。在写入 0x300 时发出中断,但在写入 0x310 时不发出中断。因此,要发送中断命令,应首先写入 0x310,然后写入 0x300。 中断命令寄存器的位描述如下: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
635632
0x310DF-
31201816151413121180
0x300-DSH-TMLV-DSDMDModeVector
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
635632
0x310DF-
31201816151413121180
0x300-DSH-TMLV-DSDMDModeVector
具体的配置配置细节这里不做理解要求,只需要按照如下描述进行配置即可: - - DSH(Destination Shorthand):设置为 2,始终将中断发送给所有 APIC - - DMode(Delivery Mode):设置为 5,INIT De-assert 模式 - - LV(Level):设置为 0,INIT De-assert 模式 - - TM(Trigger Mode):设置为 1,INIT De-assert 模式 + - DSH(Destination Shorthand):设置为 2,始终将中断发送给所有 APIC + - DMode(Delivery Mode):设置为 5,INIT De-assert 模式 + - LV(Level):设置为 0,INIT De-assert 模式 + - TM(Trigger Mode):设置为 1,INIT De-assert 模式 参考代码如下: @@ -529,7 +529,7 @@ enable_irq(Irq::Serial0 as u8, 0); // enable IRQ4 for CPU0 按照下列描述,补全 `src/drivers/input.rs` 驱动代码: -1. 使用你喜欢的数据结构存储用户输入的数据。 +1. 使用你喜欢的数据结构存储用户输入的数据。 此缓冲区大小和存储的数据类型由你自行决定,一个参考的缓冲区大小为 128。 @@ -544,7 +544,7 @@ enable_irq(Irq::Serial0 as u8, 0); // enable IRQ4 for CPU0 crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] } ``` -2. 处理数据结构的初始化,暴露基本功能。 +2. 处理数据结构的初始化,暴露基本功能。 初始化 `INPUT_BUFFER`,你可以直接使用 `lazy_static` 初始化: @@ -568,11 +568,11 @@ enable_irq(Irq::Serial0 as u8, 0); // enable IRQ4 for CPU0 } ``` -3. 实现并暴露 `pop_key` 函数。 +3. 实现并暴露 `pop_key` 函数。 利用 `try_pop_key` 函数,从缓冲区中**阻塞**取出数据:循环等待,直到缓冲区中有数据,并返回获取到的数据。 -4. 实现并暴露 `get_line` 函数。 +4. 实现并暴露 `get_line` 函数。 从缓冲区中**阻塞**取出数据,并将其实时打印出来。直到遇到换行符 `\n`。将数据转换为 `String` 类型,并返回。 @@ -580,7 +580,7 @@ enable_irq(Irq::Serial0 as u8, 0); // enable IRQ4 for CPU0 删除操作可以通过发送 `0x08`、`0x20`、`0x08` 序列实现。你可以在串口驱动中将它封装为 `backspace` 函数。 - *Note: `String::with_capacity` 可以帮助你预先分配足够的内存。* + _Note: `String::with_capacity` 可以帮助你预先分配足够的内存。_ 串口的输入中断与时钟中断类似,在 `src/interrupt/serial.rs` 中补全代码,为 **IRQ4 Serial0** 设置中断处理程序: diff --git a/docs/labs/0x03/index.md b/docs/labs/0x03/index.md index f517221..a82bbda 100644 --- a/docs/labs/0x03/index.md +++ b/docs/labs/0x03/index.md @@ -29,7 +29,7 @@ 3. 依据 [实验任务](./tasks.md) 完成实验。 - 代码编写任务:观察提供的代码,完善所有标记为 `FIXME:` 的部分,并验证结果是否符合预期。**请在报告中介绍实现思路,截图展示关键结果。** - - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。*注:思考题可能也是理解代码、实现功能的重要提示。* + - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。_注:思考题可能也是理解代码、实现功能的重要提示。_ - Bonus 加分项:学有余力的同学可以任选 Bonus 部分完成,尝试完成更多的功能,并在报告中进行展示。这部分内容不是必须的要求。 4. 请在实验报告中涵盖相关任务的实现截图、实验任务对应问题的解答、实验过程中遇到的问题与解决方案等内容。 diff --git a/docs/labs/0x03/tasks.md b/docs/labs/0x03/tasks.md index 2c6a762..32dbb82 100644 --- a/docs/labs/0x03/tasks.md +++ b/docs/labs/0x03/tasks.md @@ -56,11 +56,11 @@ 在之前的实验中,已经描述过在 x86_64 架构下的中断发生时,CPU 会将当前的一部分上下文保存到内核栈中,然后跳转到中断处理函数。这些上下文包括: -- `instruction_pointer`:指令指针,保存了中断发生时 CPU 正在执行的指令的地址。 -- `code_segment`:代码段寄存器,保存了当前正在执行的代码段的选择子。 -- `cpu_flags`:CPU 标志寄存器,保存了中断前的 CPU 标志状态。 -- `stack_pointer`:栈指针,保存了中断前的栈指针。 -- `stack_segment`:栈段寄存器,保存了中断前的栈段选择子,在 x86_64 下总是为 0。 +- `instruction_pointer`:指令指针,保存了中断发生时 CPU 正在执行的指令的地址。 +- `code_segment`:代码段寄存器,保存了当前正在执行的代码段的选择子。 +- `cpu_flags`:CPU 标志寄存器,保存了中断前的 CPU 标志状态。 +- `stack_pointer`:栈指针,保存了中断前的栈指针。 +- `stack_segment`:栈段寄存器,保存了中断前的栈段选择子,在 x86_64 下总是为 0。 而在进行进程切换时,通常还需要保存和恢复更多的上下文,这些内容主要包括通用寄存器和浮点寄存器。为了简化实现,实验在编译架构选项中禁用了浮点寄存器,因此只需要保存和恢复通用寄存器即可。 @@ -251,29 +251,29 @@ pub fn new(init: Arc) -> Self { 在一次进程切换的过程中,需要**关闭中断**,之后完成以下几个步骤: -- 保存当前进程的上下文 +- 保存当前进程的上下文 经过上述进程上下文的描述可知,作为可变引用参数被传入的 `ProcessContext` 中存储了进程切换时需要保存的所有寄存器的值,并且其中的内容将会在进程切换完成后被恢复,进而真正实现进程的切换。 因此,进程切换的第一步就是将 `context` 保存至当前进程的 `ProcessInner` 中,以便下次恢复运行状态。 -- 更新当前进程的状态 +- 更新当前进程的状态 进程切换时,若它当前的状态并非 `Dead`,则当前进程的状态会被更新为 `Ready`。 同时,为了记录进程的执行时间,一般也会记录进程的调度次数,这里使用 `usize` 类型的 `ticks_passed` 字段来记录进程的调度次数。 -- 将当前进程放入就绪队列 +- 将当前进程放入就绪队列 当前进程被切换出去后,它会被放入就绪队列的末尾,等待下一次调度。 !!! tip "被调度的进程状态可能在就绪队列中时发生了改变,因此需要进行一些检查。" -- 从就绪队列中选取下一个进程 +- 从就绪队列中选取下一个进程 进程调度器会从就绪队列中选取第一个进程,检查进程的状态,如果进程处于可调度状态,就将其状态更新为 `Running`,并将其 PID 写入 `Processor` 中。 -- 切换进程上下文和页表 +- 切换进程上下文和页表 进程调度器会将选中的进程的上下文重新加载,并将新进程的页表物理地址写入 `Cr3` 寄存器,从而完成进程的切换。 @@ -283,17 +283,17 @@ pub fn new(init: Arc) -> Self { 在已经进行过的实验中,笔者为大家预设了一些内存布局,你应该或多或少接触过这些地址: -- 物理内存偏移:`0xFFFF800000000000` +- 物理内存偏移:`0xFFFF800000000000` 通过定义物理内存偏移,借助 2MB 的页映射,将物理内存线性映射到了这一偏移量所对应的的地址空间中。在内核中,当需要访问一个物理内存地址,如 `0x1000` 时,就可以通过 `0xFFFF800000001000` 来访问。 -- 内核空间起始地址:`0xFFFFFF0000000000` +- 内核空间起始地址:`0xFFFFFF0000000000` 在操作系统中,一般会将内核地址映射到高偏移,而将用户地址映射到低偏移。在实验中,内核空间的起始地址被定义在了 `0xFFFFFF0000000000`,这相当于为内核预留了 1TiB 的地址空间。 通过 `kernel.ld` 链接器脚本,将内核的起始地址设置为了这一地址,实验中编译的操作系统内核会被链接到从这一地址开始的地址空间中。同时,通过 bootloader 中的内核加载函数,读取 ELF 文件的描述,并将这些内容加载到了对应的地址空间中。 -- 内核栈地址:`0xFFFFFF0100000000` +- 内核栈地址:`0xFFFFFF0100000000` 内核栈的起始地址通过配置文件被定义在了 `0xFFFFFF0100000000`,距离内核起始地址 4GiB。默认大小为 512 个 4KiB 的页面,即 2MiB。 @@ -301,9 +301,9 @@ pub fn new(init: Arc) -> Self { !!! note "缺页异常?" - 你可能会注意到,4GiB 的栈空间中实际映射的目前只有 2MiB,剩下的页面都是没有映射的。 + 你可能会注意到,4GiB 的栈空间中实际映射的目前只有 2MiB,剩下的页面都是没有映射的。 - 在未来的实验内容中,你将会处理这种缺页异常,并在需要时为进程进行映射。 + 在未来的实验内容中,你将会处理这种缺页异常,并在需要时为进程进行映射。 在了解了这些虚拟地址空间预设的情况下,进一步来考虑其他进程的内存布局。本次实验中,由于还没有“用户进程”的概念,可以暂时先考虑的少一些,但是便于本次实验使用的解决方案。 @@ -336,7 +336,6 @@ pub const STACK_MAX: u64 = 0x400000000000; 在开启了 ASLR(Address space layout randomization)的内核中,栈的内存区域也是随机的,堆内存和 `mmap` 调用的结果也是随机的。这些随机性赋予了应用程序更高的安全性,不过在这里不再过多展开。 - ```rust pub const STACK_START_MASK: u64 = !(STACK_MAX_SIZE - 1); @@ -395,19 +394,19 @@ pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; 在 `pkg/kernel/src/utils` 文件夹中,增量代码补充包含了如下的模块: -- `regs.rs`:对需要保存的一系列寄存器进行了封装,规定了其输出方式,补全了进程切换时需要使用的汇编代码及 `as_handler` 宏。 -- `func.rs`:定义了用于测试执行的两个函数,其中 `test` 用以验证进程调度、并发的正确性,`huge_stack` 用以验证处理缺页异常的正确性。 +- `regs.rs`:对需要保存的一系列寄存器进行了封装,规定了其输出方式,补全了进程切换时需要使用的汇编代码及 `as_handler` 宏。 +- `func.rs`:定义了用于测试执行的两个函数,其中 `test` 用以验证进程调度、并发的正确性,`huge_stack` 用以验证处理缺页异常的正确性。 在 `pkg/kernel/src/proc` 文件夹中,增量代码补充包含了如下的模块: -- `context.rs`:进程上下文的定义和实现,其中包含了加载、保存进程上下文的相关函数。 -- `data.rs`:进程数据结构体的定义,这里存储的数据在进程被杀死后会被释放,包含了使用 `Arc` 保护的线程间共享的数据(子进程相关内容将在下次实验中使用)。 -- `vm/{mod.rs,stack.rs}`:进程的虚拟内存管理,包含了栈空间的分配和释放函数,以及一些常量的定义。 -- `manager.rs`:进程管理器的定义和实现,时钟中断最终会通过进程管理器来进行任务切换。 -- `paging.rs`:进程页表的存储、切换所用数据,使用 `load` 函数加载进程页表到 `Cr3` 寄存器,使用 `clone` 函数来获得当前页表的副本,用于创建新进程。 -- `pid.rs`:使用元组结构体将一个 `u16` 作为进程 ID,需要为 `new` 函数确保获取唯一的 PID。 -- `processor.rs`:对处理器的抽象,使用 `AtomicU16` 来存储当前正在运行的进程的 PID,使用 `set_pid` 函数来设置当前进程的 PID,使用 `get_pid` 函数来获取当前进程的 PID。 -- `process.rs`:进程结构体的核心实现,包含了进程的状态、调度计数、退出返回值、父子关系、中断上下文等内容,是管理进程的核心模块。 +- `context.rs`:进程上下文的定义和实现,其中包含了加载、保存进程上下文的相关函数。 +- `data.rs`:进程数据结构体的定义,这里存储的数据在进程被杀死后会被释放,包含了使用 `Arc` 保护的线程间共享的数据(子进程相关内容将在下次实验中使用)。 +- `vm/{mod.rs,stack.rs}`:进程的虚拟内存管理,包含了栈空间的分配和释放函数,以及一些常量的定义。 +- `manager.rs`:进程管理器的定义和实现,时钟中断最终会通过进程管理器来进行任务切换。 +- `paging.rs`:进程页表的存储、切换所用数据,使用 `load` 函数加载进程页表到 `Cr3` 寄存器,使用 `clone` 函数来获得当前页表的副本,用于创建新进程。 +- `pid.rs`:使用元组结构体将一个 `u16` 作为进程 ID,需要为 `new` 函数确保获取唯一的 PID。 +- `processor.rs`:对处理器的抽象,使用 `AtomicU16` 来存储当前正在运行的进程的 PID,使用 `set_pid` 函数来设置当前进程的 PID,使用 `get_pid` 函数来获取当前进程的 PID。 +- `process.rs`:进程结构体的核心实现,包含了进程的状态、调度计数、退出返回值、父子关系、中断上下文等内容,是管理进程的核心模块。 !!! warning "利用好先前实现的日志系统和调试工具,帮助定位问题和实现功能" @@ -560,12 +559,12 @@ pub fn new_test_thread(id: &str) -> ProcessId { 在操作系统进行虚拟内存管理的时候经常会遇到缺页中断,作为可恢复的异常,它发生的可能性有很多: -- 内存页被标记为懒分配,只有当进程访问到这一页面时才会被分配。 -- 部分可执行的代码段尚未被加载到内存中,需要从磁盘文件进行加载。 -- 内存被交换到了磁盘上,再次使用需要交换回来。 -- 内存页面被标记为只读,在进程尝试写页面的时候触发了 COW(Copy on Write)机制,需要进行页面的复制。 -- 进程访问量权限不允许的内存区域,比如用户态进程尝试访问内核空间的内存。 -- …… +- 内存页被标记为懒分配,只有当进程访问到这一页面时才会被分配。 +- 部分可执行的代码段尚未被加载到内存中,需要从磁盘文件进行加载。 +- 内存被交换到了磁盘上,再次使用需要交换回来。 +- 内存页面被标记为只读,在进程尝试写页面的时候触发了 COW(Copy on Write)机制,需要进行页面的复制。 +- 进程访问量权限不允许的内存区域,比如用户态进程尝试访问内核空间的内存。 +- …… 在本实验设计中,并不会完全的实现上述的所有功能,只实现一个功能来作为缺页异常处理的示例:**为栈空间进行自动扩容。** diff --git a/docs/labs/0x04/index.md b/docs/labs/0x04/index.md index eaba1a9..495d8b0 100644 --- a/docs/labs/0x04/index.md +++ b/docs/labs/0x04/index.md @@ -23,7 +23,7 @@ 对于本次实验内容,你需要参考学习如下实验资料: -- [用户空间](../../wiki/userspace.md) +- [用户空间](../../wiki/userspace.md) ## 实验任务与要求 @@ -34,7 +34,7 @@ 3. 依据 [实验任务](./tasks.md) 完成实验。 - 代码编写任务:观察提供的代码,完善所有标记为 `FIXME:` 的部分,并验证结果是否符合预期。**请在报告中介绍实现思路,截图展示关键结果。** - - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。*注:思考题可能也是理解代码、实现功能的重要提示。* + - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。_注:思考题可能也是理解代码、实现功能的重要提示。_ - Bonus 加分项:学有余力的同学可以任选 Bonus 部分完成,尝试完成更多的功能,并在报告中进行展示。这部分内容不是必须的要求。 4. 请在实验报告中涵盖相关任务的实现截图、实验任务对应问题的解答、实验过程中遇到的问题与解决方案等内容。 diff --git a/docs/labs/0x04/tasks.md b/docs/labs/0x04/tasks.md index 5c803b0..0fb449f 100644 --- a/docs/labs/0x04/tasks.md +++ b/docs/labs/0x04/tasks.md @@ -34,9 +34,9 @@ 在 `pkg/kernel` 中,添加了如下一些模块: -- `interrupt/syscall`:定义系统调用及其服务的实现。 -- `memory/user`:用户堆内存分配的实现,会被用在系统调用的处理中,将用户态的内存分配委托给内核。 -- `utils/resource`:定义了用于进行 I/O 操作的 `Resource` 结构体,用于处理用户态的读写系统调用。 +- `interrupt/syscall`:定义系统调用及其服务的实现。 +- `memory/user`:用户堆内存分配的实现,会被用在系统调用的处理中,将用户态的内存分配委托给内核。 +- `utils/resource`:定义了用于进行 I/O 操作的 `Resource` 结构体,用于处理用户态的读写系统调用。 !!! tip "别忘了更新 `Cargo.toml`" @@ -82,8 +82,8 @@ fn main() -> isize { entry!(main); ``` -- `#![no_std]` 表示不使用标准库,rust 并没有支持 YSOS 的标准库,需要我们自行实现。 -- `#![no_main]` 表示不使用标准的 `main` 函数入口,而是使用 `entry!` 宏定义的入口函数。 +- `#![no_std]` 表示不使用标准库,rust 并没有支持 YSOS 的标准库,需要我们自行实现。 +- `#![no_main]` 表示不使用标准的 `main` 函数入口,而是使用 `entry!` 宏定义的入口函数。 `entry!` 宏的定义如下: @@ -373,7 +373,6 @@ if user_access { 这一标志位只应当为用户进程所使用,内核相关代码不应当拥有这一权限。由于用户程序是不可信的,需要以此防止用户态程序访问内核的内存空间。 - !!! note "**对于用户进程而言,不再与内核共享页表,而是通过克隆内核页表获取了自己的页表。这意味着可以为每个用户进程分配同样的栈地址,而不会相互干扰。**" 之后在 `ProcessInner` 和 `ProcessVm` 中实现 `load_elf` 函数,来处理代码段映射等内容。 @@ -440,10 +439,10 @@ pub fn init_stack_frame(&mut self, entry: VirtAddr, stack_top: VirtAddr) { 以 x86_64 的 Linux 为例,系统调用的部分调用约定如下所示: -- 系统调用号通过 `rax` 寄存器传递 -- 参数通过 `rdi`、`rsi`、`rdx`、`r10`、`r8`、`r9` 寄存器传递 -- 参数数量大于 6 时,通过栈传递 -- 返回值通过 `rax` 寄存器传递 +- 系统调用号通过 `rax` 寄存器传递 +- 参数通过 `rdi`、`rsi`、`rdx`、`r10`、`r8`、`r9` 寄存器传递 +- 参数数量大于 6 时,通过栈传递 +- 返回值通过 `rax` 寄存器传递 !!! tip "在系统调用中,由于 `rcx` 寄存器有其他用途,因此使用 `r10` 寄存器代替函数调用约定 `__fastcall` 中的 `rcx` 寄存器。" @@ -501,9 +500,9 @@ pub enum Syscall { 由于一些额外的执念,这里的读写、进程操作的系统调用号基本与 Linux 中功能类似的系统调用号一致,而有些系统调用号则是自定义的。 -- `ListApp` 用于列出当前系统中的所有用户程序,由于尚不会进行文件系统的实现,因此需要这样一个系统调用来获取用户程序的信息。 -- `Stat` 用于获取系统中的一些统计信息,例如内存使用情况、进程列表等,用于调试和监控。 -- `Allocate/Deallocate` 用于分配和释放内存。在当前没有完整的用户态内存分配支持的情况下,可以利用系统调用将其委托给内核来完成。 +- `ListApp` 用于列出当前系统中的所有用户程序,由于尚不会进行文件系统的实现,因此需要这样一个系统调用来获取用户程序的信息。 +- `Stat` 用于获取系统中的一些统计信息,例如内存使用情况、进程列表等,用于调试和监控。 +- `Allocate/Deallocate` 用于分配和释放内存。在当前没有完整的用户态内存分配支持的情况下,可以利用系统调用将其委托给内核来完成。 ### 软中断处理 @@ -844,15 +843,15 @@ Syscall::WaitPid => { /* ... */}, 至此,你可以编写自己的 Shell 了!作为用户与操作系统的交互方式,它需要实现一些必须功能: -- 列出当前系统中的所有用户程序 -- 列出当前正在运行的全部进程 -- 运行一个用户程序 +- 列出当前系统中的所有用户程序 +- 列出当前正在运行的全部进程 +- 运行一个用户程序 同时,它也可以实现一些辅助的能力: -- 列出帮助信息 -- 清空屏幕 -- ... +- 列出帮助信息 +- 清空屏幕 +- ... 为了实现一些信息的查看,你也需要实现如下两个系统调用: diff --git a/docs/labs/0x05/tasks.md b/docs/labs/0x05/tasks.md index d37a7b5..02278c8 100644 --- a/docs/labs/0x05/tasks.md +++ b/docs/labs/0x05/tasks.md @@ -20,11 +20,11 @@ YSOS 的 `fork` 系统调用设计如下描述: !!! note "出于实验设计考量:
本实现与 Linux 或 [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/) 中所定义的 `fork` 有所不同,也结合了 Linux 中 `vfork` 的行为。" -- `fork` 会创建一个新的进程,新进程称为子进程,原进程称为父进程。 -- **子进程在系统调用后将得到 `0` 的返回值,而父进程将得到子进程的 PID。** 如果创建失败,父进程将得到 `-1` 的返回值。 -- `fork` **不复制**父进程的内存空间,**不实现** Cow (Copy on Write) 机制,即父子进程将持有一定的共享内存:代码段、数据段、堆、bss 段等。 -- `fork` 子进程与父进程共享内存空间(页表),但**子进程拥有自己独立的寄存器和栈空间,即在一个不同的栈的地址继承原来的数据。** -- **由于上述内存分配机制的限制,`fork` 系统调用必须在任何 Rust 内存分配(堆内存分配)之前进行。** +- `fork` 会创建一个新的进程,新进程称为子进程,原进程称为父进程。 +- **子进程在系统调用后将得到 `0` 的返回值,而父进程将得到子进程的 PID。** 如果创建失败,父进程将得到 `-1` 的返回值。 +- `fork` **不复制**父进程的内存空间,**不实现** Cow (Copy on Write) 机制,即父子进程将持有一定的共享内存:代码段、数据段、堆、bss 段等。 +- `fork` 子进程与父进程共享内存空间(页表),但**子进程拥有自己独立的寄存器和栈空间,即在一个不同的栈的地址继承原来的数据。** +- **由于上述内存分配机制的限制,`fork` 系统调用必须在任何 Rust 内存分配(堆内存分配)之前进行。** 为了实现父子进程的资源共享,在先前的实验中,已经做了一些准备工作: @@ -166,19 +166,19 @@ impl Stack { 关于具体的代码实现,参考如下的提示和说明: -1. 将功能的具体实现委托至下一级进行,保持代码语义的简洁。 +1. 将功能的具体实现委托至下一级进行,保持代码语义的简洁。 - - 系统调用静态函数,并将其委托给 `ProcessManager::fork`。 - - `ProcessManager::fork` 将具体实现委托给当前进程的 `Process::fork`。 - - `Process::fork` 将具体实现委托给 `ProcessInner::fork`。 + - 系统调用静态函数,并将其委托给 `ProcessManager::fork`。 + - `ProcessManager::fork` 将具体实现委托给当前进程的 `Process::fork`。 + - `Process::fork` 将具体实现委托给 `ProcessInner::fork`。 每一层代码只关心自己层级的逻辑和数据,不关心持有自身的锁或其他外部数据的状态,进而提高代码可维护性。 -2. 使用先前实现的 `save_current` 和 `switch_next` 等函数,提高代码复用性。 +2. 使用先前实现的 `save_current` 和 `switch_next` 等函数,提高代码复用性。 如果使用时遇到了问题,很可能是你的代码过于相互耦合,尝试将逻辑进行分离,保证函数功能的单一性。 -3. 分别为父子进程设置返回值。 +3. 分别为父子进程设置返回值。 进程调用系统调用后,会根据**恢复的寄存器的值**获取系统调用的返回值。 @@ -186,11 +186,11 @@ impl Stack { 设置子进程的 `context` 时,先根据父进程进行复制,并在复制后修改 `rax` 为 0。 -4. 使用 `Arc::downgrade` 获取 `Weak` 引用,从而避免循环引用。 +4. 使用 `Arc::downgrade` 获取 `Weak` 引用,从而避免循环引用。 父进程持有子进程的强引用,子进程持有父进程的弱引用,这样可以避免循环引用导致的内存泄漏。 -5. 为了复制栈空间,你可以使用 `core::intrinsics::copy_nonoverlapping` 函数。 +5. 为了复制栈空间,你可以使用 `core::intrinsics::copy_nonoverlapping` 函数。 这个函数会使用底层 LLVM 所提供的内存复制相关指令,具有较高的性能。需要调用侧保证源和目标的内存空间不会重叠。可以封装为如下函数进行使用: @@ -212,7 +212,7 @@ impl Stack { } ``` -6. 记录父子进程共用的页表。 +6. 记录父子进程共用的页表。 可以使用 `Arc` 来提供引用计数,来确保进程逐个退出时,只有最后一个退出的进程会进行页表内容的释放。为此,你需要补充一些相关的函数调用: @@ -245,7 +245,7 @@ impl Stack { } ``` -8. 为子进程分配合适的栈空间。 +7. 为子进程分配合适的栈空间。 通过子进程数量、页表引用计数、当前父进程的栈等信息,为子进程分配合适的栈空间。 @@ -283,7 +283,6 @@ impl Stack { 在任意进程尝试写入时,再对整个页面进行复制。这种策略被称为写时复制(Copy on Write,COW),它可以大大减少内存的使用和开销,提高性能。 - ### 功能测试 在完成了 `fork` 的实现后,你需要通过如下功能测试来验证你的实现是否正确: @@ -633,9 +632,9 @@ unsafe impl Sync for SpinLock {} 在实现 `acquire` 和 `release` 时,你需要使用 `AtomicBool` 的原子操作来保证锁的正确性: -- `load` 函数用于读取当前值。 -- `store` 函数用于设置新值。 -- `compare_exchange` 函数用于原子地进行比较-交换,也即比较当前值是否为目标值,如果是则将其设置为新值,否则返回当前值。 +- `load` 函数用于读取当前值。 +- `store` 函数用于设置新值。 +- `compare_exchange` 函数用于原子地进行比较-交换,也即比较当前值是否为目标值,如果是则将其设置为新值,否则返回当前值。 在进行循环等待时,可以使用 `core::hint::spin_loop` 提高性能,在 x86_64 架构中,它实际上会编译为 `pause` 指令。 @@ -643,18 +642,18 @@ unsafe impl Sync for SpinLock {} 得利于 Rust 良好的底层封装,自旋锁的实现非常简单。但是也存在一定的问题: -- 忙等待:自旋锁会一直占用 CPU 时间,直到获取到锁为止,这会导致 CPU 利用率的下降。 -- 饥饿:如果一个线程一直占用锁,其他线程可能会一直无法获取到锁。 -- 死锁:如果两个线程互相等待对方占有的锁,就会导致死锁。 +- 忙等待:自旋锁会一直占用 CPU 时间,直到获取到锁为止,这会导致 CPU 利用率的下降。 +- 饥饿:如果一个线程一直占用锁,其他线程可能会一直无法获取到锁。 +- 死锁:如果两个线程互相等待对方占有的锁,就会导致死锁。 信号量 `Semaphore` 是一种更为复杂的同步机制,它可以用于控制对共享资源的访问,也可以用于控制对临界区的访问。通过与进程调度相关的操作,信号量还可以用于控制进程的执行顺序、提高 CPU 利用率等。 信号量需要实现四种操作: -- `new`:根据所给出的 `key` 创建一个新的信号量。 -- `remove`:根据所给出的 `key` 删除一个已经存在的信号量。 -- `signal`:也叫做 `V` 操作,也可以被 `release/up/verhogen` 表示,它用于释放一个资源,使得等待的进程可以继续执行。 -- `wait`:也叫做 `P` 操作,也可以被 `acquire/down/proberen` 表示,它用于获取一个资源,如果资源不可用,则进程将会被阻塞。 +- `new`:根据所给出的 `key` 创建一个新的信号量。 +- `remove`:根据所给出的 `key` 删除一个已经存在的信号量。 +- `signal`:也叫做 `V` 操作,也可以被 `release/up/verhogen` 表示,它用于释放一个资源,使得等待的进程可以继续执行。 +- `wait`:也叫做 `P` 操作,也可以被 `acquire/down/proberen` 表示,它用于获取一个资源,如果资源不可用,则进程将会被阻塞。 为了实现与内核的交互,信号量的操作将被实现为一个系统调用,它将使用到三个系统调用参数: @@ -687,10 +686,10 @@ pub enum SemaphoreResult { } ``` -- `Ok`:表示操作成功,且无需进行阻塞或唤醒。 -- `NotExist`:表示信号量不存在。 -- `Block(ProcessId)`:表示操作需要阻塞线程,一般是当前进程。 -- `WakeUp(ProcessId)`:表示操作需要唤醒线程。 +- `Ok`:表示操作成功,且无需进行阻塞或唤醒。 +- `NotExist`:表示信号量不存在。 +- `Block(ProcessId)`:表示操作需要阻塞线程,一般是当前进程。 +- `WakeUp(ProcessId)`:表示操作需要唤醒线程。 为了实现信号量的 KV 存储,使用 `SemaphoreSet` 定义信号量集合的操作: @@ -809,30 +808,30 @@ pub fn sys_sem(args: &SyscallArgs, context: &mut ProcessContext) { 创建一个用户程序 `pkg/app/mq`,结合使用信号量,实现一个消息队列: -- 父进程使用 fork 创建额外的 16 个进程,其中一半为生产者,一半为消费者。 +- 父进程使用 fork 创建额外的 16 个进程,其中一半为生产者,一半为消费者。 -- 生产者不断地向消息队列中写入消息,消费者不断地从消息队列中读取消息。 +- 生产者不断地向消息队列中写入消息,消费者不断地从消息队列中读取消息。 -- 每个线程处理的消息总量共 10 条。 +- 每个线程处理的消息总量共 10 条。 即生产者会产生 10 个消息,每个消费者只消费 10 个消息。 -- 在每个线程生产或消费的时候,输出相关的信息。 +- 在每个线程生产或消费的时候,输出相关的信息。 你可能需要使用信号量或旋锁来实现一个互斥锁,保证操作和信息输出之间不会被打断。 -- 在生产者和消费者完成上述操作后,使用 `sys_exit(0)` 直接退出。 +- 在生产者和消费者完成上述操作后,使用 `sys_exit(0)` 直接退出。 -- 最终使用父进程等待全部的子进程退出后,输出消息队列的消息数量。 +- 最终使用父进程等待全部的子进程退出后,输出消息队列的消息数量。 -- 在父进程创建完成 16 个进程后,使用 `sys_stat` 输出当前的全部进程的信息。 +- 在父进程创建完成 16 个进程后,使用 `sys_stat` 输出当前的全部进程的信息。 你需要保证最终消息队列中的消息数量为 0,你可以开启内核更加详细的日志,并使用输出的相关信息尝试证明队列的正常工作: -- 在从队列取出消息时,消息为空吗? -- 在向队列写入消息时,队列是否满了? -- 在队列为空时,消费者是否被阻塞? -- 在队列满时,生产者是否被阻塞? +- 在从队列取出消息时,消息为空吗? +- 在向队列写入消息时,队列是否满了? +- 在队列为空时,消费者是否被阻塞? +- 在队列满时,生产者是否被阻塞? !!! question "分别设置队列容量为 `1, 4, 8, 16`,记录观察生产者和消费者的行为:" @@ -850,22 +849,22 @@ pub fn sys_sem(args: &SyscallArgs, context: &mut ProcessContext) { 创建一个用户程序 `pkg/app/dinner`,使用课上学到的知识,实现并解决哲学家就餐问题: -- 创建一个程序,模拟五个哲学家的行为。 +- 创建一个程序,模拟五个哲学家的行为。 -- 每个哲学家都是一个独立的线程,可以同时进行思考和就餐。 +- 每个哲学家都是一个独立的线程,可以同时进行思考和就餐。 -- 使用互斥锁来保护每个筷子,确保同一时间只有一个哲学家可以拿起一根筷子。 +- 使用互斥锁来保护每个筷子,确保同一时间只有一个哲学家可以拿起一根筷子。 -- 使用等待操作调整哲学家的思考和就餐时间,以增加并发性和实际性。 +- 使用等待操作调整哲学家的思考和就餐时间,以增加并发性和实际性。 - - 如果你实现了 `sys_time` 系统调用(Lab 4),可以使用它来构造 `sleep` 操作。 - - 如果你并没有实现它,可以参考多线程计数器中的 `delay` 函数进行实现。 + - 如果你实现了 `sys_time` 系统调用(Lab 4),可以使用它来构造 `sleep` 操作。 + - 如果你并没有实现它,可以参考多线程计数器中的 `delay` 函数进行实现。 -- 当哲学家成功就餐时,输出相关信息,如哲学家编号、就餐时间等。 +- 当哲学家成功就餐时,输出相关信息,如哲学家编号、就餐时间等。 -- 向程序中引入一些随机性,例如在尝试拿筷子时引入一定的延迟,模拟竞争条件和资源争用。 +- 向程序中引入一些随机性,例如在尝试拿筷子时引入一定的延迟,模拟竞争条件和资源争用。 -- 可以设置等待时间或循环次数,以确保程序能够运行足够长的时间,并尝试观察到不同的情况,如死锁和饥饿。 +- 可以设置等待时间或循环次数,以确保程序能够运行足够长的时间,并尝试观察到不同的情况,如死锁和饥饿。 ??? tip "在用户态中引入伪随机数" @@ -904,9 +903,9 @@ pub fn sys_sem(args: &SyscallArgs, context: &mut ProcessContext) { 通过观察程序的输出和行为,请尝试构造并截图记录以下现象: -- 某些哲学家能够成功就餐,即同时拿到左右两侧的筷子。 -- 尝试构造死锁情况,即所有哲学家都无法同时拿到他们需要的筷子。 -- 尝试构造饥饿情况,即某些哲学家无法获得足够的机会就餐。 +- 某些哲学家能够成功就餐,即同时拿到左右两侧的筷子。 +- 尝试构造死锁情况,即所有哲学家都无法同时拿到他们需要的筷子。 +- 尝试构造饥饿情况,即某些哲学家无法获得足够的机会就餐。 尝试解决上述可能存在的问题,并介绍你的解决思路。 @@ -940,23 +939,23 @@ pub fn sys_sem(args: &SyscallArgs, context: &mut ProcessContext) { ## 加分项 -1. 🤔 尝试实现如下用户程序任务,完成用户程序 `fish`: +1. 🤔 尝试实现如下用户程序任务,完成用户程序 `fish`: - - 创建三个子进程,让它们分别能输出且只能输出 `>`,`<` 和 `_`。 - - 使用学到的方法对这些子进程进行同步,使得打印出的序列总是 `<><_` 和 `><>_` 的组合。 + - 创建三个子进程,让它们分别能输出且只能输出 `>`,`<` 和 `_`。 + - 使用学到的方法对这些子进程进行同步,使得打印出的序列总是 `<><_` 和 `><>_` 的组合。 在完成这一任务的基础上,其他细节可以自行决定如何实现,包括输出长度等。 -2. 🤔 尝试和前文不同的其他方法解决哲学家就餐问题,并验证你的方法能够正确解决它,简要介绍你的方法,并给出程序代码和测试结果。 +2. 🤔 尝试和前文不同的其他方法解决哲学家就餐问题,并验证你的方法能够正确解决它,简要介绍你的方法,并给出程序代码和测试结果。 -3. 🔥 尝试使用符合 Rust 做法的方式处理互斥锁,使用 RAII 的方式来保证锁的释放: +3. 🔥 尝试使用符合 Rust 做法的方式处理互斥锁,使用 RAII 的方式来保证锁的释放: RAII(Resource Acquisition Is Initialization)是一种资源获取即初始化的技术,它通过在对象的构造函数中获取资源,然后在析构函数中释放资源的方法,来保证资源的正确释放。 对于 Rust,也即实现 `MutexGuard` 类似的结构,它在构造时获取锁,然后在此结构体被移出作用域时释放锁。 - - 在 `acquire` 时候返回 `MutexGuard` 对象。 - - 移除 `release` 函数,使用 `MutexGuard` 的 `Drop` trait 来释放锁。 + - 在 `acquire` 时候返回 `MutexGuard` 对象。 + - 移除 `release` 函数,使用 `MutexGuard` 的 `Drop` trait 来释放锁。 !!! danger "本项实现难度较大,不建议初学者尝试。" diff --git a/docs/labs/0x06/index.md b/docs/labs/0x06/index.md index 66a1f53..9274172 100644 --- a/docs/labs/0x06/index.md +++ b/docs/labs/0x06/index.md @@ -23,9 +23,9 @@ 对于本次实验内容,你需要参考学习如下实验资料: -- [文件系统概述](../../wiki/fs.md) -- [ATA 硬盘简介](../../wiki/ata.md) -- [FAT 文件系统](../../wiki/fat.md) +- [文件系统概述](../../wiki/fs.md) +- [ATA 硬盘简介](../../wiki/ata.md) +- [FAT 文件系统](../../wiki/fat.md) ## 实验任务与要求 @@ -36,7 +36,7 @@ 3. 依据 [实验任务](./tasks.md) 完成实验。 - 代码编写任务:观察提供的代码,完善所有标记为 `FIXME:` 的部分,并验证结果是否符合预期。**请在报告中介绍实现思路,截图展示关键结果。** - - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。*注:思考题可能也是理解代码、实现功能的重要提示。* + - 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,**在报告中简要进行回答**。_注:思考题可能也是理解代码、实现功能的重要提示。_ - Bonus 加分项:学有余力的同学可以任选 Bonus 部分完成,尝试完成更多的功能,并在报告中进行展示。这部分内容不是必须的要求。 4. 请在实验报告中涵盖相关任务的实现截图、实验任务对应问题的解答、实验过程中遇到的问题与解决方案等内容。 diff --git a/docs/labs/0x06/tasks.md b/docs/labs/0x06/tasks.md index 862fb9d..71d54dc 100644 --- a/docs/labs/0x06/tasks.md +++ b/docs/labs/0x06/tasks.md @@ -26,17 +26,17 @@ 在 `pkg/storage/src/common` 中,提供了众多有关存储的底层结构: -- `block.rs`: 提供了数据块的抽象,用于存储数据,内部为指定大小的 `u8` 数组。 -- `device.rs`: 目前只提供了块设备的抽象,提供分块读取数据的接口。 -- `error.rs`: 定义了文件系统、磁盘、文件名可能遇到的一系列错误,并定义了以 `FsError` 为错误类型的 `Result`。 -- `filesystem.rs`: 定义了文件系统的抽象,提供了文件系统的基本操作接口。 -- `io.rs`: 定义了 `Read`、`Write` 和 `Seek` 的行为,不过在本次实验中只需要实现 `Read`。 -- `metadata.rs`:定义了统一的文件元信息,包含文件名、修改时间、大小等信息。 +- `block.rs`: 提供了数据块的抽象,用于存储数据,内部为指定大小的 `u8` 数组。 +- `device.rs`: 目前只提供了块设备的抽象,提供分块读取数据的接口。 +- `error.rs`: 定义了文件系统、磁盘、文件名可能遇到的一系列错误,并定义了以 `FsError` 为错误类型的 `Result`。 +- `filesystem.rs`: 定义了文件系统的抽象,提供了文件系统的基本操作接口。 +- `io.rs`: 定义了 `Read`、`Write` 和 `Seek` 的行为,不过在本次实验中只需要实现 `Read`。 +- `metadata.rs`:定义了统一的文件元信息,包含文件名、修改时间、大小等信息。 同时,有了接口定义了统一的行为之后,可以利用他们来实现具有更丰富功能的结构体: -- `filehandle.rs`: 定义了文件句柄,它持有一个实现了 `FileIO` trait 的字段,并维护了文件的元数据。 -- `mount.rs`: 定义了挂载点,它持有一个实现了 `Filesystem` trait 的字段,并维护了一个固定的挂载点路径,它会将挂载点路径下的文件操作请求转发给内部的文件系统。 +- `filehandle.rs`: 定义了文件句柄,它持有一个实现了 `FileIO` trait 的字段,并维护了文件的元数据。 +- `mount.rs`: 定义了挂载点,它持有一个实现了 `Filesystem` trait 的字段,并维护了一个固定的挂载点路径,它会将挂载点路径下的文件操作请求转发给内部的文件系统。 在 `pkg/storage/src/partition/mod.rs` 中,定义了 `Partition` 结构体,和 `PartitionTable` trait,用于统一块设备的分区表行为。 @@ -119,10 +119,10 @@ storage = { package = "ysos_storage", path = "../storage" } 回顾一下之前编写串口驱动的过程,它与即将实现的驱动类似,都是 PIO 来进行数据传输: -- 根据规范定义端口,使用端口进行读写操作控制外设寄存器 -- 按照规定修改外设寄存器,使得设备按照预期的方式运行 -- 通过数据和状态寄存器,实现数据的发送和接收 -- 通过启用设备的中断,实现异步的数据传输(与轮询方式相对) +- 根据规范定义端口,使用端口进行读写操作控制外设寄存器 +- 按照规定修改外设寄存器,使得设备按照预期的方式运行 +- 通过数据和状态寄存器,实现数据的发送和接收 +- 通过启用设备的中断,实现异步的数据传输(与轮询方式相对) 在 [ATA 硬盘简介](../../wiki/ata.md) 中,介绍了 ATA 硬盘的基本工作原理,以及相关概念。 @@ -260,30 +260,30 @@ storage = { package = "ysos_storage", path = "../storage" } 在实现了上述文件系统的数据格式之后,你需要在 `fs/fat16/impls.rs` 中实现你需要的一系列函数,包括但不限于: -- 将 `cluster: &Cluster` 转换为 `sector` -- 根据当前 `cluster: &Cluster`,利用 FAT 表,获取下一个 `cluster` -- 根据当前文件夹 `dir: &Directory` 信息,获取名字为 `name: &str` 的 `DirEntry` -- 遍历文件夹 `dir: &Directory`,获取其中文件信息 -- 其他你可能需要的帮助函数 +- 将 `cluster: &Cluster` 转换为 `sector` +- 根据当前 `cluster: &Cluster`,利用 FAT 表,获取下一个 `cluster` +- 根据当前文件夹 `dir: &Directory` 信息,获取名字为 `name: &str` 的 `DirEntry` +- 遍历文件夹 `dir: &Directory`,获取其中文件信息 +- 其他你可能需要的帮助函数 在实现了一系列函数后,为 `impl FileSystem for Fat16` 补全实现: -- `Iterator` 可以简单利用 `Vec::into_iter` 作为返回值,不需要考虑懒求值。 -- `FileHandle` 的 `file` 部分直接使用 `fs/fat16/file.rs` 中定义的 `File` 结构体,并使用 `Box` 包装。 +- `Iterator` 可以简单利用 `Vec::into_iter` 作为返回值,不需要考虑懒求值。 +- `FileHandle` 的 `file` 部分直接使用 `fs/fat16/file.rs` 中定义的 `File` 结构体,并使用 `Box` 包装。 最后,为 `File` 实现 `Read` trait,需要注意: -- `cluster` 链需要使用上述函数读取 FAT 表进行获取。 -- `offset` 用于记录当前文件读取到了什么位置,需要实时更新。 -- 一个 `cluster` 中存在多个 `sector`,你需要根据 `bpb` 信息进行读取操作。 -- `buf` 参数是不定长的,你需要考虑文件长度、块长度以及缓冲区长度,来决定什么时候终止读取。 +- `cluster` 链需要使用上述函数读取 FAT 表进行获取。 +- `offset` 用于记录当前文件读取到了什么位置,需要实时更新。 +- 一个 `cluster` 中存在多个 `sector`,你需要根据 `bpb` 信息进行读取操作。 +- `buf` 参数是不定长的,你需要考虑文件长度、块长度以及缓冲区长度,来决定什么时候终止读取。 除此之外,本部分的实现不作任何要求,阅读并理解给出的 [FAT 文件系统](../../wiki/fat.md) 内容,尝试实现文件系统的功能。 同时,由于文件系统的实现相对较为严格,笔者鼓励大家多多查找相关已有实现,参考完善自己的文件系统,下面给出几个可供参考的仓库: -- [embedded-sdmmc-rs](https://github.com/rust-embedded-community/embedded-sdmmc-rs) -- [rust-fatfs](https://github.com/rafalh/rust-fatfs) +- [embedded-sdmmc-rs](https://github.com/rust-embedded-community/embedded-sdmmc-rs) +- [rust-fatfs](https://github.com/rafalh/rust-fatfs) ## 接入操作系统 diff --git a/docs/labs/0x07/tasks.md b/docs/labs/0x07/tasks.md index 04d0783..eef5957 100644 --- a/docs/labs/0x07/tasks.md +++ b/docs/labs/0x07/tasks.md @@ -20,8 +20,8 @@ 本次实验代码量较小,给出的代码集中于 `pkg/kernel/src/proc/vm` 目录下。 -- `heap.rs`:添加了 `Heap` 结构体,用于管理堆内存。 -- `mod.rs`:除栈外,添加了堆内存、ELF 文件映射的初始化和清理函数。 +- `heap.rs`:添加了 `Heap` 结构体,用于管理堆内存。 +- `mod.rs`:除栈外,添加了堆内存、ELF 文件映射的初始化和清理函数。 !!! note "关于 `ProcessVm` 的角色" @@ -154,11 +154,11 @@ ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] 其中所示的内存区域: -- `/usr/bin/cat`:ELF 文件映射的内存区域,这里包含了 `cat` 程序的 `.text`、`.data`、`.bss` 等段。 -- `[heap]`:堆区的内存区域,这里包含了 `cat` 程序的堆内存,使用 `brk` 系统调用进行分配。 -- `/usr/lib/x86_64-linux-gnu/*.so.*`:动态链接库的内存区域,这里包含了程序使用的动态链接库的 `.text`、`.data`、`.bss` 等段。 -- `[stack]`:栈区的内存区域,这里包含了程序的栈内存,在达到栈区的最大大小前,栈区会自动增长。 -- `[vvar]`、`[vdso]`、`[vsyscall]`:内核的内存区域,这里包含了内核相关的一些数据结构,如 `vvar` 包含了一些内核和用户空间之间共享的变量;`vdso` 是虚拟的动态共享对象 (Virtual Dynamic Shared Object) 区域,用于在用户空间和内核空间之间提供一些系统调用的快速访问。 +- `/usr/bin/cat`:ELF 文件映射的内存区域,这里包含了 `cat` 程序的 `.text`、`.data`、`.bss` 等段。 +- `[heap]`:堆区的内存区域,这里包含了 `cat` 程序的堆内存,使用 `brk` 系统调用进行分配。 +- `/usr/lib/x86_64-linux-gnu/*.so.*`:动态链接库的内存区域,这里包含了程序使用的动态链接库的 `.text`、`.data`、`.bss` 等段。 +- `[stack]`:栈区的内存区域,这里包含了程序的栈内存,在达到栈区的最大大小前,栈区会自动增长。 +- `[vvar]`、`[vdso]`、`[vsyscall]`:内核的内存区域,这里包含了内核相关的一些数据结构,如 `vvar` 包含了一些内核和用户空间之间共享的变量;`vdso` 是虚拟的动态共享对象 (Virtual Dynamic Shared Object) 区域,用于在用户空间和内核空间之间提供一些系统调用的快速访问。 你也可以查看程序的内存使用情况,使用 `cat /proc//status`,依旧以 `cat` 程序为例: @@ -182,12 +182,12 @@ VmSwap: 0 kB 其中有几个需要注意的字段: -- `VmPeak` / `VmSize`:进程的峰值虚拟内存大小和当前虚拟内存大小,指的是整个虚拟内存空间的大小。 -- `VmHWM` / `VmRSS`:进程的峰值物理内存大小和当前物理内存大小,指的是进程实际使用的物理内存大小。 -- `RssAnon` / `RssFile` / `RssShmem`:进程的匿名内存、文件映射内存和共享内存的大小。 -- `VmData` / `VmStk` / `VmExe`:进程的数据段、栈段、代码段的大小。 -- `VmLib`:进程的动态链接库的大小,对于 `cat` 程序来说,这里主要是 `libc` 的占用,但这部分内存可以被多个进程很好地共享。 -- `VmPTE`:进程的页表项的大小。 +- `VmPeak` / `VmSize`:进程的峰值虚拟内存大小和当前虚拟内存大小,指的是整个虚拟内存空间的大小。 +- `VmHWM` / `VmRSS`:进程的峰值物理内存大小和当前物理内存大小,指的是进程实际使用的物理内存大小。 +- `RssAnon` / `RssFile` / `RssShmem`:进程的匿名内存、文件映射内存和共享内存的大小。 +- `VmData` / `VmStk` / `VmExe`:进程的数据段、栈段、代码段的大小。 +- `VmLib`:进程的动态链接库的大小,对于 `cat` 程序来说,这里主要是 `libc` 的占用,但这部分内存可以被多个进程很好地共享。 +- `VmPTE`:进程的页表项的大小。 当使用 `ps aux` 查看进程时,你可以看到更多的信息,如进程的 CPU 占用、内存占用、进程的状态等。 @@ -425,8 +425,8 @@ pub fn fork(&self) -> Self { } ``` -- `clone_level_4` 用于复制当前页表(仅第四级页表),并将其作为一个新的页表上下文返回,用于 `spawn` 时复制内核页表; -- `fork` 则是直接对 `Arc` 进行 `clone` 操作,使得新的进程与父进程共享页表。 +- `clone_level_4` 用于复制当前页表(仅第四级页表),并将其作为一个新的页表上下文返回,用于 `spawn` 时复制内核页表; +- `fork` 则是直接对 `Arc` 进行 `clone` 操作,使得新的进程与父进程共享页表。 也即,在目前的实现中,对于每一棵独立的“进程树”,它们的页表是独立的,但是在同一棵“进程树”中,它们的页表是共享的。 @@ -618,8 +618,8 @@ pub fn init_kernel_vm(mut self, pages: &KernelPages) -> Self { 需要用到的配置项在 Lab 1 中已经给出,即 `kernel_stack_auto_grow`,对它的行为进行如下约定: -- 默认为 `0`,这时内核栈区所需的全部页面(页面数量为 `kernel_stack_size`)将会在内核加载时一次性分配。 -- 当这一参数为非零值时,表示内核栈区的初始化页面数量,从栈顶开始向下分配这一数量的初始化页面,并交由内核进行自己的栈区管理。 +- 默认为 `0`,这时内核栈区所需的全部页面(页面数量为 `kernel_stack_size`)将会在内核加载时一次性分配。 +- 当这一参数为非零值时,表示内核栈区的初始化页面数量,从栈顶开始向下分配这一数量的初始化页面,并交由内核进行自己的栈区管理。 ```rust let (stack_start, stack_size) = if config.kernel_stack_auto_grow > 0 { @@ -697,11 +697,11 @@ pub struct Heap { 下面对 `brk` 系统调用的参数和行为进行简单的约定: -- `brk` 系统调用的参数是一个可为 `NULL` 的指针,表示用户程序希望调整的堆区结束地址; -- 如果参数为 `NULL`,则表示用户程序希望获取当前的堆区结束地址,即返回 `end` 的值; -- 在系统调用的实现时,用户参数采用 `0` 表示 `NULL`,返回值采用 `-1` 表示失败; -- 在内核内部处理时,参数使用 `Option` 进行传递,`None` 表示用户程序传入的参数为 `NULL`;`brk` 的返回值也为 `Option`,表示内核调整后的堆区结束地址,如果调整失败则返回 `None`; -- 如果用户程序传入的参数不为 `NULL`,则检查用户传入的地址是否合法,即在 `[HEAP_START, HEAP_END]` 区间内,如果不合法则返回 `None`。 +- `brk` 系统调用的参数是一个可为 `NULL` 的指针,表示用户程序希望调整的堆区结束地址; +- 如果参数为 `NULL`,则表示用户程序希望获取当前的堆区结束地址,即返回 `end` 的值; +- 在系统调用的实现时,用户参数采用 `0` 表示 `NULL`,返回值采用 `-1` 表示失败; +- 在内核内部处理时,参数使用 `Option` 进行传递,`None` 表示用户程序传入的参数为 `NULL`;`brk` 的返回值也为 `Option`,表示内核调整后的堆区结束地址,如果调整失败则返回 `None`; +- 如果用户程序传入的参数不为 `NULL`,则检查用户传入的地址是否合法,即在 `[HEAP_START, HEAP_END]` 区间内,如果不合法则返回 `None`。 根据上述约定,给出用户态的系统调用函数: @@ -718,10 +718,10 @@ pub fn sys_brk(addr: Option) -> Option { 对于有效输入的处理,需要满足如下行为: -- 初始化堆区时,`base` 和 `end` 的值均为 `HEAP_START`; -- 如果用户传入的地址为 `base`,即用户希望释放整个堆区; -- 如果用户传入的地址比当前 `end` 小,即用户希望缩小堆区,对指向地址向上对齐到页边界,释放多余的页面; -- 如果用户传入的地址比当前 `end` 大,即用户希望扩大堆区,对指向地址向上对齐到页边界,分配新的页面。 +- 初始化堆区时,`base` 和 `end` 的值均为 `HEAP_START`; +- 如果用户传入的地址为 `base`,即用户希望释放整个堆区; +- 如果用户传入的地址比当前 `end` 小,即用户希望缩小堆区,对指向地址向上对齐到页边界,释放多余的页面; +- 如果用户传入的地址比当前 `end` 大,即用户希望扩大堆区,对指向地址向上对齐到页边界,分配新的页面。 对于一段典型的系统调用过程,可以参考如下代码: @@ -741,32 +741,33 @@ assert!(ret == heap_end, "Failed to allocate heap"); 如果直接替换现有的用户态堆分配,则很难找出可能存在的问题,因此下面给出一个测试和实现流程作为参考: 1. 新建一个用户程序,参考上述代码,尝试在其中使用 `brk` 系统调用来调整堆区的大小,并进行写入和读取操作; + 2. 若上述操作没有问题,则可以在 `lib` 中实现可选的第二个内存分配器(参考给出代码 `pkg/lib/src/allocator/brk.rs`); - > 内存分配器的自主实现不是本次实验的内容,因此这里直接使用 `linked_list_allocator` 进行代劳。 - > - > 在后续的实验中,如果你想要自行实现内存管理算法,可以参考给出的方式添加 `feature` 对代码进行隔离,以便于测试和调试。 + 内存分配器的自主实现不是本次实验的内容,因此这里直接使用 `linked_list_allocator` 进行代劳。 + + 在后续的实验中,如果你想要自行实现内存管理算法,可以参考给出的方式添加 `feature` 对代码进行隔离,以便于测试和调试。 3. 尝试在进程中使用如下方式来暂时使用新的内存分配器: - ```diff - [dependencies] - - lib = { package = "yslib", path = "../../lib" } + ```diff + [dependencies] + - lib = { package = "yslib", path = "../../lib" } - + [dependencies.lib] - + package = "yslib" - + path = "../../lib" - + default-features = false - + features = ["brk_alloc"] - ``` + + [dependencies.lib] + + package = "yslib" + + path = "../../lib" + + default-features = false + + features = ["brk_alloc"] + ``` 4. 在你测试通过后,可以将其作为默认的内存分配器: - ```diff - [features] - - default = ["kernel_alloc"] - + default = ["brk_alloc"] - ``` + ```diff + [features] + - default = ["kernel_alloc"] + + default = ["brk_alloc"] + ``` 如果想要实现一系列操作的自主测试,可以在自定义的用户程序中进行一系列的操作,或者直接将其实现为接受用户输入的 Shell 命令,进一步测试并记录你的 `brk` 系统调用的行为。 @@ -787,7 +788,9 @@ assert!(ret == heap_end, "Failed to allocate heap"); 4. 尝试查找资料,了解 `mmap`、`munmap` 和 `mprotect` 系统调用的功能和用法,回答下列问题: - `mmap` 的主要功能是什么?它可以实现哪些常见的内存管理操作? + - `munmap` 的主要功能是什么?什么时候需要使用 `munmap`? + - `mprotect` 的主要功能是什么?使用 `mprotect` 可以实现哪些内存保护操作? - 编写 C 程序,使用 `mmap` 将一个文件映射到内存中,并读写该文件的内容。 diff --git a/docs/labs/0x08/index.md b/docs/labs/0x08/index.md index ffe0d6b..3b77451 100644 --- a/docs/labs/0x08/index.md +++ b/docs/labs/0x08/index.md @@ -12,6 +12,8 @@ 请从下列任务中,**选择一到两个目标进行实现**: +--- + ### VGA 显示输出 串口输出只是最基本的交互方式,尝试在 bootloader 中,利用 `get_handle_for_protocol::` 获取到 VGA 显示的模式、分辨率、缓冲区地址等信息。 @@ -30,6 +32,8 @@ 1. 利用键盘输入中断,为 QEMU 的 GUI 界面实现输入输出。 2. 尝试将 Shell 部分限制在屏幕的下半部分,并在上半部分实现一些图像的绘制。作为一个例子,你可以利用 `sleep` 等方式,实现一个不断绘制转动的钟表的进程,并让它作为一个后台进程运行。 +--- + ### 可读写的临时文件系统 根据现有的其他 OS 实验中的文件系统设计、或参考现有的文件系统设计,结合你学过的算法、组织数据的方式,实现一个简单的文件系统的驱动。 @@ -50,6 +54,8 @@ 1. 尝试实现文件系统的硬链接,并测试读写操作。 2. 修改 QEMU 参数,挂载一块虚拟磁盘,尝试实现文件的持久化。 +--- + ### 内存管理算法 在上次实验实现了堆内存的分配和释放后,用户态有了自己的能力去管理自己的页面,但是其实际的动态内存分配还是依赖于内核提供的服务。 @@ -73,6 +79,8 @@ !!! tip "将你的内存分配器作为 `static` 变量在线程间共享使用有助于避免线程导致的内存错误。" +--- + ### 块设备的缓存层 在 `storage` 包中,基于目前已有的实现,实现一个块设备的缓存层。 @@ -106,6 +114,8 @@ where 1. 向操作系统暴露缓存的使用情况,并在系统状态中进行展示。 2. 验证缓存是否能带来一定的性能提升,设计相关测试并记录输出。 +--- + ### 多核进程调度 在之前的实现中,进程只会在同一个核心上被调度执行,而在实际的多核系统中,多个核心可以并行地执行多个进程。 @@ -125,3 +135,5 @@ where 1. 尝试实现进程亲和性,使得进程能够在特定的核心上运行。 2. 实现多核多队列模型,或尝试实现任一负载均衡算法(二选一)。 + +--- diff --git a/docs/overrides/partials/comments.html b/docs/overrides/partials/comments.html index afb3a2b..b5b8484 100644 --- a/docs/overrides/partials/comments.html +++ b/docs/overrides/partials/comments.html @@ -19,34 +19,34 @@ > {% endif %} diff --git a/docs/scripts/katex.js b/docs/scripts/katex.js index bcb8cee..5ba35d7 100644 --- a/docs/scripts/katex.js +++ b/docs/scripts/katex.js @@ -1,11 +1,11 @@ -document.addEventListener("DOMContentLoaded", function() { +document.addEventListener('DOMContentLoaded', function () { renderMathInElement(document.body, { delimiters: [ - {left: '$$', right: '$$', display: true}, - {left: '$', right: '$', display: false}, - {left: '\\(', right: '\\)', display: false}, - {left: '\\[', right: '\\]', display: true} + { left: '$$', right: '$$', display: true }, + { left: '$', right: '$', display: false }, + { left: '\\(', right: '\\)', display: false }, + { left: '\\[', right: '\\]', display: true }, ], - throwOnError : false - }); -}); + throwOnError: false, + }) +}) diff --git a/docs/wiki/apic.md b/docs/wiki/apic.md index 47c35b7..98aacb7 100644 --- a/docs/wiki/apic.md +++ b/docs/wiki/apic.md @@ -18,42 +18,42 @@ x2APIC 是 xAPIC 的变体和扩展,主要改进解决了支持的 CPU 数量 APIC 的初始化过程基本包括以下几个步骤: -- 禁用 8259 PIC,使得系统只使用 APIC 进行中断处理。 +- 禁用 8259 PIC,使得系统只使用 APIC 进行中断处理。 - 这一步被 UEFI BIOS 自动完成,我们无需关心。 + 这一步被 UEFI BIOS 自动完成,我们无需关心。 -- 检测系统中是否存在 APIC。 +- 检测系统中是否存在 APIC。 -- 确定 APIC 的地址空间,即 LAPIC 和 IOAPIC 的 MMIO 地址空间。 +- 确定 APIC 的地址空间,即 LAPIC 和 IOAPIC 的 MMIO 地址空间。 由于我们采用了虚拟地址空间,所以这里需要将物理地址映射到虚拟地址空间中,之后再进行 APIC 的相关操作。 -- 操作 SPIV(Spurious Interrupt Vector Register, 0xF0)寄存器,启用 APIC 并设置 Spurious IRQ Vector。 +- 操作 SPIV(Spurious Interrupt Vector Register, 0xF0)寄存器,启用 APIC 并设置 Spurious IRQ Vector。 -- 设置计时器相关寄存器: +- 设置计时器相关寄存器: - - TDCR(0x3E0): Divide Configuration Register,设置分频系数。 - - TICR(0x380): Initial Count Register,设置初始计数值。 - - LVT Timer(0x320): Local Vector Table Timer,设置中断向量号和触发模式。 + - TDCR(0x3E0): Divide Configuration Register,设置分频系数。 + - TICR(0x380): Initial Count Register,设置初始计数值。 + - LVT Timer(0x320): Local Vector Table Timer,设置中断向量号和触发模式。 -- 禁用 LVT LINT0, LVT LINT1,LVT PCINT,向对应寄存器写入 Mask 位。 +- 禁用 LVT LINT0, LVT LINT1,LVT PCINT,向对应寄存器写入 Mask 位。 -- 设置错误中断 LVT Error 到对应的中断向量号。 +- 设置错误中断 LVT Error 到对应的中断向量号。 -- 连续写入两次 0 以清除错误状态寄存器。 +- 连续写入两次 0 以清除错误状态寄存器。 -- 向 EOI 寄存器写入 0 以确认任何挂起的中断。 +- 向 EOI 寄存器写入 0 以确认任何挂起的中断。 -- 设置 ICR 寄存器: +- 设置 ICR 寄存器: - - Destination Shorthand(bit 18-19): 设置为 2,始终将中断发送给所有 APIC - - Delivery Mode(bit 8-10): 设置为 5,INIT De-assert 模式所需 - - Level(bit 14): 设置为 0,INIT De-assert 所需 - - Trigger Mode(bit 15): 设置为 1,INIT De-assert 所需 + - Destination Shorthand(bit 18-19): 设置为 2,始终将中断发送给所有 APIC + - Delivery Mode(bit 8-10): 设置为 5,INIT De-assert 模式所需 + - Level(bit 14): 设置为 0,INIT De-assert 所需 + - Trigger Mode(bit 15): 设置为 1,INIT De-assert 所需 设置完成后等待 Delivery Status(bit 12) 为 0。 -- 设置 TPR 寄存器为 0,允许接收中断。 +- 设置 TPR 寄存器为 0,允许接收中断。 以上过程的代码示例会在实验任务文档中进行详细描述,具体细节和设置原因涉及对称多处理 SMP 等内容,不做理解要求,如有兴趣可以自行查阅参考资料了解。 @@ -67,9 +67,9 @@ APIC 的初始化过程基本包括以下几个步骤: ## 参考资料 -- [APIC - OSDev](https://wiki.osdev.org/APIC) -- [/arch/x86/kernel/apic/apic.c - Linux](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/apic/apic.c?h=v6.7#n1525) -- [Symmetric Multiprocessing - OSDev](https://wiki.osdev.org/Symmetric_Multiprocessing) -- [APIC Timer - OSDev](https://wiki.osdev.org/APIC_timer) -- [Multiprocessing Support for Hobby OSes Explained](http://www.osdever.net/tutorials/view/multiprocessing-support-for-hobby-oses-explained) -- [apic crate - theseus-os](https://www.theseus-os.com/Theseus/doc/apic/) +- [APIC - OSDev](https://wiki.osdev.org/APIC) +- [/arch/x86/kernel/apic/apic.c - Linux](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/apic/apic.c?h=v6.7#n1525) +- [Symmetric Multiprocessing - OSDev](https://wiki.osdev.org/Symmetric_Multiprocessing) +- [APIC Timer - OSDev](https://wiki.osdev.org/APIC_timer) +- [Multiprocessing Support for Hobby OSes Explained](http://www.osdever.net/tutorials/view/multiprocessing-support-for-hobby-oses-explained) +- [apic crate - theseus-os](https://www.theseus-os.com/Theseus/doc/apic/) diff --git a/docs/wiki/ata.md b/docs/wiki/ata.md index a94e635..0fb3d35 100644 --- a/docs/wiki/ata.md +++ b/docs/wiki/ata.md @@ -10,8 +10,8 @@ ATA 接口经历了多个版本的发展,包括 ATA、ATAPI(用于附加设 由上介绍,ATA 硬盘使用 IDE 接口,分为 PATA 和 SATA 两种类型。 -- **PATA 硬盘**(Parallel ATA):即并行 ATA 硬盘接口规范,有着非常悠久的迭代历史,但已经不常见。 -- **SATA 硬盘**(Serial ATA):即串行 ATA 硬盘接口规范,为常见类型,如今我们常说的外存设备 HDD 或 SSD 多数为 SATA 接口。 +- **PATA 硬盘**(Parallel ATA):即并行 ATA 硬盘接口规范,有着非常悠久的迭代历史,但已经不常见。 +- **SATA 硬盘**(Serial ATA):即串行 ATA 硬盘接口规范,为常见类型,如今我们常说的外存设备 HDD 或 SSD 多数为 SATA 接口。 PATA 之所以被 SATA 取代,其中一个重要原因是 SATA 接口的传输速度更快。到目前为止,ATA-7 是 ATA 接口的最后一个版本,也叫 ATA133 。 ATA133 最大支持 133 MB/s 数据传输速度。但 SATA 接口的第一代就已经达到了 150 MB/s 的传输速度,第二代则达到了 300 MB/s,第三代则达到了 600 MB/s。从数据上可以直观发现,SATA 硬盘的最大传输速度远远超过了 PATA 硬盘。 @@ -23,8 +23,8 @@ PATA 之所以被 SATA 取代,其中一个重要原因是 SATA 接口的传输 为什么 PATA 接口会比 SATA 接口慢呢?更进一步,为什么现在流行的高速接口都是串行接口呢?直观上感觉,相同情况下 ATA 的**多通道**应该比 SATA 的**单通道**应该更快才对。这里我们需要了解一些比特数据传输与速率限制的知识。相较 PATA 的多通道,SATA 的单数据通道速度频率限制: -- 由于多通道并行,PATA 必须在数据线中一次传输 16 个信号,如果信号没有及时到达,就会产生错误传输结果。因此,比特流传输的速度必须**减缓**以纠正错误。 -- 而 SATA 一次只传输一个比特的数据,此时比特流的传递速度要快得多。 +- 由于多通道并行,PATA 必须在数据线中一次传输 16 个信号,如果信号没有及时到达,就会产生错误传输结果。因此,比特流传输的速度必须**减缓**以纠正错误。 +- 而 SATA 一次只传输一个比特的数据,此时比特流的传递速度要快得多。 因此,SATA 传输线的传输速度比 PATA 要快了近 30 倍。此外,SATA 另一个进步在于它的数据连线,它的体积更小,散热也更好,与硬盘的连接相当方便。与 PATA 相比,SATA 的功耗更低,同时配备的 CRC 技术让数据传输也更为安全。 @@ -76,7 +76,7 @@ PIO (Programmed Input/Output) 是过 CPU 执行 I/O 端口指令来进行数据 ## 参考资料 -- [ATA PIO - OSDev](https://wiki.osdev.org/ATA_PIO) -- [ATA - Wikipedia](https://en.wikipedia.org/wiki/Parallel_ATA) -- [百度百科 - ATA 硬盘](https://baike.baidu.com/item/ATA%E7%A1%AC%E7%9B%98/10009258) -- [知乎 - ATA PIO 模式](https://zhuanlan.zhihu.com/p/653413684) +- [ATA PIO - OSDev](https://wiki.osdev.org/ATA_PIO) +- [ATA - Wikipedia](https://en.wikipedia.org/wiki/Parallel_ATA) +- [百度百科 - ATA 硬盘](https://baike.baidu.com/item/ATA%E7%A1%AC%E7%9B%98/10009258) +- [知乎 - ATA PIO 模式](https://zhuanlan.zhihu.com/p/653413684) diff --git a/docs/wiki/debug.md b/docs/wiki/debug.md index 174bce3..a6ad6da 100644 --- a/docs/wiki/debug.md +++ b/docs/wiki/debug.md @@ -1,222 +1,220 @@ -# 程序调试:从 GDB 到 VSCode - -## 概述 - -在写代码时很难一蹴而就,往往需要反复观察、不断调试。 - -最简单的调试方法即“ `printf` 大法”:在代码中插入 `printf` 语句,然后打印程序中间状态,观察输出结果。 - -尽管便利,但手动插入输出不仅需要调整源码、反复编译,而且不适合本课程的场景。课程要求结合 QEMU 开发操作系统内核,简单的输出无法完全支持实验观察内核的运行状态如寄存器、内存乃至函数调用栈的需求。 - -因此,本章 Wiki 将从 GDB 出发,介绍更为便利的程序调试方法论,同时结合先进的插件(gef)和强大的 IDE(VSCode)介绍进一步提高调试效率的方法。 - -!!! note "开始之前" - - 本章内容仅提供了一个快速上手的必备方法以及简略的参考资料,鼓励大家主动查找、翻阅应用文档,以解锁高阶调试技巧。 - -## 基本方法:GDB - -GDB(GNU Debugger)是一个功能强大的开源调试器,用于调试各种编程语言的程序。它是 GNU 项目的一部分,可在多个操作系统上使用。GDB 提供了一系列强大的调试功能,如断点调试、内存查看、堆栈跟踪以及远程调试等,帮助开发者诊断和修复程序中的错误。 - -在正式开始之前,需要准备好 GDB。GDB 的安装非常简单,主流的 Linux 的包管理器都提供了 GDB 的安装包。在 Ubuntu 下,只需要执行以下指令: - -```bash -sudo apt install gdb -``` - -其次,为了能够在 GDB 中调试内核,需要在编译时开启调试符号的生成。 - -贴心的 TA 已经在 `Makefile` 中为大家准备好了相关指令,只需要在编译时加上 `DBG_INFO=true` 即可,指令示例如下: - -```bash -make build DBG_INFO=true -``` - -如果你没有配置好的 make 环境(比如 Windows 下),你也可以通过 `ysos.py` 脚本来编译内核,指令示例如下: - -```bash -python ysos.py build -p debug -``` - -当观察到终端输出如下信息时,说明带调试符号已经生成成功: - -```log -Finished release-with-debug [optimized + debuginfo] target(s) in 0.04s -``` - -!!! tip "调试符号" - - 调试符号文件是编译器在编译时生成的一种特殊文件,其中包含了: - - - **源代码中的符号**(变量名、函数名等); - - 编译后的二进制文件中的**地址的对应关系**。 - - 调试符号文件可以帮助调试器在调试时将二进制文件中的地址转换为源代码中的符号,从而方便调试。 - - 如果感兴趣的话,你可以使用反编译工具(如 `readelf -S` 或者 `objdump -g` 等)来查看调试符号的内容。 - -最后,是一些 GDB 的基本使用方法。在本实验中,主要使用 GDB 的以下几个功能: - -1. `file `:加载调试的二进制文件,如加载 `ysos` 内核二进制: `file esp/KERNEL.ELF` - -2. 远程连接:QEMU 提供了 GDB 服务器暴露远程调试接口。可以通过 GDB 连接到对应服务器,进而调试 QEMU 中运行的内核。 - - - QEMU 设置:请参考[QEMU Wiki](./qemu.md)中对`-s`参数的解释。 - - GDB 设置: `target remote :`:连接到远程调试服务器,如连接到 QEMU 的 GDB 服务器:`target remote localhost:1234` - - 使用 gef 时进行远程调试:`gef-remote `:连接到远程调试服务器,如:`gef-remote localhost 1234` - -3. `run`:运行程序,简写为 `r`。 - -4. `continue`:继续执行程序,直到遇到断点或者程序结束。简写为 `c`。 - -5. `next`:执行下一条指令,**不会进入函数**。简写为 `n`。 - -6. `step`:执行下一条指令,**会进入函数**。简写为 `s`。 - -7. `list`:查看源代码和可用断点,简写为 `l`。 - -8. `break `:在指定函数处设置断点,如在`main`函数处设置断点:`break ysos_kernel::`。简写为 `b`。 - -9. `bt`:查看函数调用栈。 - -10. `x/x `:查看内存中的数据,从 `` 开始,连续查看 `` 个字节。 - - - 若要查看内存中的前 16 个字节:`x/16x `。简写为 `x`。 - - 若要查看 $rdi$ 寄存器指向的内存中的前 16 个字节,可以使用 `x/16x $rdi`。 - - 若要查看内存中对应的 20 条汇编指令,可以使用 `x/20i `: - -11. `info registers`:查看寄存器的值。 - -12. `grep `:在内存中搜索 ``,如搜索字符串 "YatSenOS":`grep YatSenOS`。详细可见文档:[search-pattern - gef](https://hugsy.github.io/gef/commands/search-pattern) - -一些简单的命令使用示例如下图展示: - -![GDB Commands](./assets/debug/gdb-commands-example.png) - -为了方便初始化一个调试会话,通常使用 `.gdbinit` 来配置和初始化 gdb。 - -GDB 在启动时会默认调用该脚本,你可以在其中设置GDB的默认行为,减少重复操作。这里附上一个 `.gdbinit` 以供参考: - -```bash -file esp/KERNEL.ELF -gef config context.layout "-legend regs -stack code -args source -threads -trace extra memory" -gef-remote localhost 1234 -tmux-setup -b ysos_kernel::init -``` - -!!! tip "GDB 用法提示" - - 万丈高楼平地起,GDB 的使用也是如此。下文介绍的所有高级功能都基于 GDB / LLDB,建议大家尽能理解并掌握上述基本指令。 - -以上是 GDB 的基本使用方法,更多的 GDB 使用方法请参考: - -- [官方文档](https://sourceware.org/gdb/current/onlinedocs/gdb/) -- [知乎:GDB 调试入门指南](https://zhuanlan.zhihu.com/p/74897601) - -## 命令行调试进阶:gef - -[gef](https://github.com/hugsy/gef) 是一个功能更为强大的调试器插件,用于调试和分析二进制程序。gef 基于 GDB(GNU Debugger),并提供了一系列功能来简化二进制程序的调试过程。在实验场景下,其主要的帮助有: - -1. 进阶调试指令:在原生 GDB 的基础上,gef 提供了一个更加丰富的调试指令集,辅助进阶调试。 - -2. 调试信息展示:gef 可以显示二进制程序的调试信息,如符号表、函数名、堆栈跟踪等。相较于 GDB,其**默认常驻**显示以上信息,简化操作。 - -3. 脚本和扩展支持:gef 允许用户编写脚本和扩展,以满足特定的调试需求。用户可以编写自定义命令、自动化调试任务、定制常驻显示窗口等。 - -参照[官方文档](https://hugsy.github.io/gef/install/),以下是**数个**参考的安装流程,大家可以根据自己的**网络情况**灵活选择: - -```bash -# via the install script -## using curl -$ bash -c "$(curl -fsSL https://gef.blah.cat/sh)" - -## using wget -$ bash -c "$(wget https://gef.blah.cat/sh -O -)" - -# or manually -$ wget -O ~/.gdbinit-gef.py -q https://gef.blah.cat/py -$ echo source ~/.gdbinit-gef.py >> ~/.gdbinit -``` - -完整调试效果如下: - -![gef](./assets/debug/gef-screenshot.png) - -!!! note "gef 用法提示" - - gef 的使用方法与 GDB 类似,但是其提供了更多的功能,也有一个更加炫酷的界面。 - - 以上截图展示效果正是源于 gef 提供的指令 `gef config context.layout` 和 `gef config context.redirect`。 - - 请阅读[官方文档](https://hugsy.github.io/gef)来学习如何定制你的调试界面、如何使用 gef 的其余功能。 - -## IDE 调试进阶:VSCode - -VSCode 是一个轻量级的跨平台编辑器,支持多种编程语言,同时提供了丰富的插件支持。因此,除了**简单的代码编辑器**功能,VSCode 还可以作为**强大的 IDE **使用。在本节中,将介绍如何使用 VSCode 进行调试。 - -!!! tip "调试提示" - - 所有源码级别的调试工具都需要二进制文件中包含调试符号。 - - 因此,需要在编译时加上 `DBG_INFO=true` 参数,或在使用 `ysos.py` 时加上 `-p debug`,以生成带调试符号的二进制文件。 - -### 配置 VSCode - -需要安装 VSCode 的插件:[CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb),它基于 LLDB、用于调试,安装时会自动安装 LLDB 调试器,无需额外安装。 - -之后需要在 VSCode 中配置调试器。在当前目录下创建 `.vscode/launch.json` 文件,内容如下: - -```json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "(lldb) YSOS", - "type": "lldb", - "request": "launch", - "program": "${workspaceFolder}/esp/KERNEL.ELF", - "args": [], - "cwd": "${workspaceFolder}", - "processCreateCommands": [ - "gdb-remote 1234" - ] - } - ] -} -``` - -### 启动调试 - -完成以上配置后,你应该可以在 VSCode 中调试内核了。 - -首先,完成内核编译,并启动 QEMU 调试。贴心的 TA 已经将指令集成进 Makefile 中,只需要执行以下指令即可: - -```bash -make build DBG_INFO=true -make debug -``` - -同样的,如果你没有配置好的 make 环境,也可以通过 `ysos.py` 脚本来运行内核: - -```bash -python ysos.py run -p debug -d -``` - -!!! note "SIGTRAP" - - 刚开始调试时,你会在 VSCode 中看到一个位于 `0x0000FFF0` 的 `SIGTRAP` 异常,这是正常的情况。 - - 因为 QEMU 开启了 `-S` 参数,它将在执行第一条指令时暂停虚拟机,并等待调试器连接。 - -在 VSCode 中设置断点,按下 `F5` 即可开始调试。例如,调试内核入口函数: - -![vscode](./assets/debug/vscode-screenshot.jpg) - -以上即是 VSCode + LLDB 的调试效果。可以看到,VSCode 提供了一个非常方便的调试界面,同时也提供了丰富的调试功能: - -- 可以在左侧的 `VARIABLES` 中查看变量的值; -- 也可以在 `CALL STACK` 中查看函数调用栈。 -- 更多的调试功能请参考[官方文档](https://code.visualstudio.com/docs/editor/debugging)。 - -以上即基本调试方法入门,欢迎大家探索属于自己的调试工具链。 +# 程序调试:从 GDB 到 VSCode + +## 概述 + +在写代码时很难一蹴而就,往往需要反复观察、不断调试。 + +最简单的调试方法即“ `printf` 大法”:在代码中插入 `printf` 语句,然后打印程序中间状态,观察输出结果。 + +尽管便利,但手动插入输出不仅需要调整源码、反复编译,而且不适合本课程的场景。课程要求结合 QEMU 开发操作系统内核,简单的输出无法完全支持实验观察内核的运行状态如寄存器、内存乃至函数调用栈的需求。 + +因此,本章 Wiki 将从 GDB 出发,介绍更为便利的程序调试方法论,同时结合先进的插件(gef)和强大的 IDE(VSCode)介绍进一步提高调试效率的方法。 + +!!! note "开始之前" + + 本章内容仅提供了一个快速上手的必备方法以及简略的参考资料,鼓励大家主动查找、翻阅应用文档,以解锁高阶调试技巧。 + +## 基本方法:GDB + +GDB(GNU Debugger)是一个功能强大的开源调试器,用于调试各种编程语言的程序。它是 GNU 项目的一部分,可在多个操作系统上使用。GDB 提供了一系列强大的调试功能,如断点调试、内存查看、堆栈跟踪以及远程调试等,帮助开发者诊断和修复程序中的错误。 + +在正式开始之前,需要准备好 GDB。GDB 的安装非常简单,主流的 Linux 的包管理器都提供了 GDB 的安装包。在 Ubuntu 下,只需要执行以下指令: + +```bash +sudo apt install gdb +``` + +其次,为了能够在 GDB 中调试内核,需要在编译时开启调试符号的生成。 + +贴心的 TA 已经在 `Makefile` 中为大家准备好了相关指令,只需要在编译时加上 `DBG_INFO=true` 即可,指令示例如下: + +```bash +make build DBG_INFO=true +``` + +如果你没有配置好的 make 环境(比如 Windows 下),你也可以通过 `ysos.py` 脚本来编译内核,指令示例如下: + +```bash +python ysos.py build -p debug +``` + +当观察到终端输出如下信息时,说明带调试符号已经生成成功: + +```log +Finished release-with-debug [optimized + debuginfo] target(s) in 0.04s +``` + +!!! tip "调试符号" + + 调试符号文件是编译器在编译时生成的一种特殊文件,其中包含了: + + - **源代码中的符号**(变量名、函数名等); + - 编译后的二进制文件中的**地址的对应关系**。 + + 调试符号文件可以帮助调试器在调试时将二进制文件中的地址转换为源代码中的符号,从而方便调试。 + + 如果感兴趣的话,你可以使用反编译工具(如 `readelf -S` 或者 `objdump -g` 等)来查看调试符号的内容。 + +最后,是一些 GDB 的基本使用方法。在本实验中,主要使用 GDB 的以下几个功能: + +1. `file `:加载调试的二进制文件,如加载 `ysos` 内核二进制: `file esp/KERNEL.ELF` + +2. 远程连接:QEMU 提供了 GDB 服务器暴露远程调试接口。可以通过 GDB 连接到对应服务器,进而调试 QEMU 中运行的内核。 + + - QEMU 设置:请参考[QEMU Wiki](./qemu.md)中对`-s`参数的解释。 + - GDB 设置: `target remote :`:连接到远程调试服务器,如连接到 QEMU 的 GDB 服务器:`target remote localhost:1234` + - 使用 gef 时进行远程调试:`gef-remote `:连接到远程调试服务器,如:`gef-remote localhost 1234` + +3. `run`:运行程序,简写为 `r`。 + +4. `continue`:继续执行程序,直到遇到断点或者程序结束。简写为 `c`。 + +5. `next`:执行下一条指令,**不会进入函数**。简写为 `n`。 + +6. `step`:执行下一条指令,**会进入函数**。简写为 `s`。 + +7. `list`:查看源代码和可用断点,简写为 `l`。 + +8. `break `:在指定函数处设置断点,如在`main`函数处设置断点:`break ysos_kernel::`。简写为 `b`。 + +9. `bt`:查看函数调用栈。 + +10. `x/x `:查看内存中的数据,从 `` 开始,连续查看 `` 个字节。 + + - 若要查看内存中的前 16 个字节:`x/16x `。简写为 `x`。 + - 若要查看 $rdi$ 寄存器指向的内存中的前 16 个字节,可以使用 `x/16x $rdi`。 + - 若要查看内存中对应的 20 条汇编指令,可以使用 `x/20i `: + +11. `info registers`:查看寄存器的值。 + +12. `grep `:在内存中搜索 ``,如搜索字符串 "YatSenOS":`grep YatSenOS`。详细可见文档:[search-pattern - gef](https://hugsy.github.io/gef/commands/search-pattern) + +一些简单的命令使用示例如下图展示: + +![GDB Commands](./assets/debug/gdb-commands-example.png) + +为了方便初始化一个调试会话,通常使用 `.gdbinit` 来配置和初始化 gdb。 + +GDB 在启动时会默认调用该脚本,你可以在其中设置GDB的默认行为,减少重复操作。这里附上一个 `.gdbinit` 以供参考: + +```bash +file esp/KERNEL.ELF +gef config context.layout "-legend regs -stack code -args source -threads -trace extra memory" +gef-remote localhost 1234 +tmux-setup +b ysos_kernel::init +``` + +!!! tip "GDB 用法提示" + + 万丈高楼平地起,GDB 的使用也是如此。下文介绍的所有高级功能都基于 GDB / LLDB,建议大家尽能理解并掌握上述基本指令。 + +以上是 GDB 的基本使用方法,更多的 GDB 使用方法请参考: + +- [官方文档](https://sourceware.org/gdb/current/onlinedocs/gdb/) +- [知乎:GDB 调试入门指南](https://zhuanlan.zhihu.com/p/74897601) + +## 命令行调试进阶:gef + +[gef](https://github.com/hugsy/gef) 是一个功能更为强大的调试器插件,用于调试和分析二进制程序。gef 基于 GDB(GNU Debugger),并提供了一系列功能来简化二进制程序的调试过程。在实验场景下,其主要的帮助有: + +1. 进阶调试指令:在原生 GDB 的基础上,gef 提供了一个更加丰富的调试指令集,辅助进阶调试。 + +2. 调试信息展示:gef 可以显示二进制程序的调试信息,如符号表、函数名、堆栈跟踪等。相较于 GDB,其**默认常驻**显示以上信息,简化操作。 + +3. 脚本和扩展支持:gef 允许用户编写脚本和扩展,以满足特定的调试需求。用户可以编写自定义命令、自动化调试任务、定制常驻显示窗口等。 + +参照[官方文档](https://hugsy.github.io/gef/install/),以下是**数个**参考的安装流程,大家可以根据自己的**网络情况**灵活选择: + +```bash +# via the install script +## using curl +$ bash -c "$(curl -fsSL https://gef.blah.cat/sh)" + +## using wget +$ bash -c "$(wget https://gef.blah.cat/sh -O -)" + +# or manually +$ wget -O ~/.gdbinit-gef.py -q https://gef.blah.cat/py +$ echo source ~/.gdbinit-gef.py >> ~/.gdbinit +``` + +完整调试效果如下: + +![gef](./assets/debug/gef-screenshot.png) + +!!! note "gef 用法提示" + + gef 的使用方法与 GDB 类似,但是其提供了更多的功能,也有一个更加炫酷的界面。 + + 以上截图展示效果正是源于 gef 提供的指令 `gef config context.layout` 和 `gef config context.redirect`。 + + 请阅读[官方文档](https://hugsy.github.io/gef)来学习如何定制你的调试界面、如何使用 gef 的其余功能。 + +## IDE 调试进阶:VSCode + +VSCode 是一个轻量级的跨平台编辑器,支持多种编程语言,同时提供了丰富的插件支持。因此,除了**简单的代码编辑器**功能,VSCode 还可以作为**强大的 IDE **使用。在本节中,将介绍如何使用 VSCode 进行调试。 + +!!! tip "调试提示" + + 所有源码级别的调试工具都需要二进制文件中包含调试符号。 + + 因此,需要在编译时加上 `DBG_INFO=true` 参数,或在使用 `ysos.py` 时加上 `-p debug`,以生成带调试符号的二进制文件。 + +### 配置 VSCode + +需要安装 VSCode 的插件:[CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb),它基于 LLDB、用于调试,安装时会自动安装 LLDB 调试器,无需额外安装。 + +之后需要在 VSCode 中配置调试器。在当前目录下创建 `.vscode/launch.json` 文件,内容如下: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "(lldb) YSOS", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/esp/KERNEL.ELF", + "args": [], + "cwd": "${workspaceFolder}", + "processCreateCommands": ["gdb-remote 1234"] + } + ] +} +``` + +### 启动调试 + +完成以上配置后,你应该可以在 VSCode 中调试内核了。 + +首先,完成内核编译,并启动 QEMU 调试。贴心的 TA 已经将指令集成进 Makefile 中,只需要执行以下指令即可: + +```bash +make build DBG_INFO=true +make debug +``` + +同样的,如果你没有配置好的 make 环境,也可以通过 `ysos.py` 脚本来运行内核: + +```bash +python ysos.py run -p debug -d +``` + +!!! note "SIGTRAP" + + 刚开始调试时,你会在 VSCode 中看到一个位于 `0x0000FFF0` 的 `SIGTRAP` 异常,这是正常的情况。 + + 因为 QEMU 开启了 `-S` 参数,它将在执行第一条指令时暂停虚拟机,并等待调试器连接。 + +在 VSCode 中设置断点,按下 `F5` 即可开始调试。例如,调试内核入口函数: + +![vscode](./assets/debug/vscode-screenshot.jpg) + +以上即是 VSCode + LLDB 的调试效果。可以看到,VSCode 提供了一个非常方便的调试界面,同时也提供了丰富的调试功能: + +- 可以在左侧的 `VARIABLES` 中查看变量的值; +- 也可以在 `CALL STACK` 中查看函数调用栈。 +- 更多的调试功能请参考[官方文档](https://code.visualstudio.com/docs/editor/debugging)。 + +以上即基本调试方法入门,欢迎大家探索属于自己的调试工具链。 diff --git a/docs/wiki/elf.md b/docs/wiki/elf.md index 8d57a21..c64b71a 100644 --- a/docs/wiki/elf.md +++ b/docs/wiki/elf.md @@ -12,10 +12,10 @@ ELF 文件大体上由文件头和数据组成,它还可以加上额外的调 一般来说,ELF 有以下几个部分 -- ELF 文件头 -- Section header table,为 relocatable files 所必须,loadable files 可选,链接器需要 Section Table 进行链接 -- Program header table,为 loadable files 所必需,但 relocatable files 可选,Program header table 描述了所有可加载的 segments 和其他数据结构,这或许会是我们遇见最多的 -- 有文件头还得有内容,即 section 和 segment,这包括了各种可加载的数据,字符串表,符号表等等。每个 segment 里可以包含多个 sections。 +- ELF 文件头 +- Section header table,为 relocatable files 所必须,loadable files 可选,链接器需要 Section Table 进行链接 +- Program header table,为 loadable files 所必需,但 relocatable files 可选,Program header table 描述了所有可加载的 segments 和其他数据结构,这或许会是我们遇见最多的 +- 有文件头还得有内容,即 section 和 segment,这包括了各种可加载的数据,字符串表,符号表等等。每个 segment 里可以包含多个 sections。 ### ELF Header @@ -57,117 +57,117 @@ typedef __u64 Elf64_Xword; typedef __s64 Elf64_Sxword; ``` -- `e_ident` ,即 ELF identification,描述了“这是一个 ELF 文件” - - ```shell - ➜ xiao hexdump -C ./this_is_an_elf_file | head -1 - 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| - ``` - - 这 16 个 bytes 表示了不同的意思,接下来通过写一个简单的 ELF parser 来描述这一段内容吧! - - ```c - #include - #include - #include - #include - #include - #include - #include - - #define EI_MAG0 0 /* e_ident[] indexes */ - #define EI_MAG1 1 - #define EI_MAG2 2 - #define EI_MAG3 3 - #define EI_CLASS 4 - #define EI_DATA 5 - #define EI_VERSION 6 - #define EI_OSABI 7 - #define EI_PAD 8 - - #define ELFMAG0 0x7f /* EI_MAG */ - #define ELFMAG1 'E' - #define ELFMAG2 'L' - #define ELFMAG3 'F' - #define ELFMAG "\177ELF" - #define SELFMAG 4 - - int main() { - int fd = open("./this_is_an_elf_file", 0, 0); - uint8_t ident[0x10] = { 0 }; - read(fd, &ident, 0x10); - - // the first 4 bytes - uint8_t *magic = (uint8_t *)ident; - - // identify the ELF file - assert( - magic[0] == ELFMAG0 && - magic[1] == ELFMAG1 && - magic[2] == ELFMAG2 && - magic[3] == ELFMAG3 - ); - - // ELF class - if (ident[EI_CLASS] == ELFCLASS64) { - printf("[*] 64 bit files\n"); - } else if (ident[EI_CLASS] == ELFCLASS32) { - printf("[*] 32 bit files\n"); +- `e_ident` ,即 ELF identification,描述了“这是一个 ELF 文件” + + ```shell + ➜ xiao hexdump -C ./this_is_an_elf_file | head -1 + 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| + ``` + + 这 16 个 bytes 表示了不同的意思,接下来通过写一个简单的 ELF parser 来描述这一段内容吧! + + ```c + #include + #include + #include + #include + #include + #include + #include + + #define EI_MAG0 0 /* e_ident[] indexes */ + #define EI_MAG1 1 + #define EI_MAG2 2 + #define EI_MAG3 3 + #define EI_CLASS 4 + #define EI_DATA 5 + #define EI_VERSION 6 + #define EI_OSABI 7 + #define EI_PAD 8 + + #define ELFMAG0 0x7f /* EI_MAG */ + #define ELFMAG1 'E' + #define ELFMAG2 'L' + #define ELFMAG3 'F' + #define ELFMAG "\177ELF" + #define SELFMAG 4 + + int main() { + int fd = open("./this_is_an_elf_file", 0, 0); + uint8_t ident[0x10] = { 0 }; + read(fd, &ident, 0x10); + + // the first 4 bytes + uint8_t *magic = (uint8_t *)ident; + + // identify the ELF file + assert( + magic[0] == ELFMAG0 && + magic[1] == ELFMAG1 && + magic[2] == ELFMAG2 && + magic[3] == ELFMAG3 + ); + + // ELF class + if (ident[EI_CLASS] == ELFCLASS64) { + printf("[*] 64 bit files\n"); + } else if (ident[EI_CLASS] == ELFCLASS32) { + printf("[*] 32 bit files\n"); + } + + // ELF encoding + if (ident[EI_DATA] == ELFDATA2LSB) { + printf("[*] little endian ELF\n"); + } else if (ident[EI_DATA] == ELFDATA2MSB) { + printf("[*] big endian ELF\n"); + } + + // ELF OS ABI + if (ident[EI_OSABI] == ELFOSABI_SYSV) { + printf("[*] System V ABI\n"); + } else if (ident[EI_OSABI] == ELFOSABI_HPUX) { + printf("[*] HP-UX operating system ABI\n"); + } else if (ident[EI_OSABI] == ELFOSABI_STANDALONE) { + printf("[*] Standalone (embedded) application\n"); + } + + printf("[*] API version: %d\n", ident[EI_VERSION]); + + return 0; } + ``` - // ELF encoding - if (ident[EI_DATA] == ELFDATA2LSB) { - printf("[*] little endian ELF\n"); - } else if (ident[EI_DATA] == ELFDATA2MSB) { - printf("[*] big endian ELF\n"); - } - - // ELF OS ABI - if (ident[EI_OSABI] == ELFOSABI_SYSV) { - printf("[*] System V ABI\n"); - } else if (ident[EI_OSABI] == ELFOSABI_HPUX) { - printf("[*] HP-UX operating system ABI\n"); - } else if (ident[EI_OSABI] == ELFOSABI_STANDALONE) { - printf("[*] Standalone (embedded) application\n"); - } - - printf("[*] API version: %d\n", ident[EI_VERSION]); - - return 0; - } - ``` - -- `e_type` 描述 ELF 的类型,包括: +- `e_type` 描述 ELF 的类型,包括: - - `ET_NONE` 没有类型也是类型 - - `ET_REL` Relocatable file - - `ET_EXEC` Executable file - - `ET_DYN` Shared object file - - `ET_CORE` Core file, Coredump 也是 ELF 类型 + - `ET_NONE` 没有类型也是类型 + - `ET_REL` Relocatable file + - `ET_EXEC` Executable file + - `ET_DYN` Shared object file + - `ET_CORE` Core file, Coredump 也是 ELF 类型 -- `e_machine` 描述目标平台 +- `e_machine` 描述目标平台 -- `e_version` 描述版本 +- `e_version` 描述版本 -- `e_entry` 储存 ELF 文件的入口虚拟地址 +- `e_entry` 储存 ELF 文件的入口虚拟地址 -- `e_phoff` 储存 ELF Program header 的 offset,也就是说,Program header 储存在距离文件开头 `e_phoff`的位置 +- `e_phoff` 储存 ELF Program header 的 offset,也就是说,Program header 储存在距离文件开头 `e_phoff`的位置 -- `e_shoff` 储存 ELF Section header 的 offset +- `e_shoff` 储存 ELF Section header 的 offset -- `e_flags` 处理器特定的 flags +- `e_flags` 处理器特定的 flags -- `e_ehsize` ELF 文件头的大小 +- `e_ehsize` ELF 文件头的大小 -- `e_phentsize` ELF Program header entry 的大小 +- `e_phentsize` ELF Program header entry 的大小 -- `e_phnum` ELF Program header 的数量 +- `e_phnum` ELF Program header 的数量 -- `e_shentsize` 类似 `e_phentsize`但是是 Section +- `e_shentsize` 类似 `e_phentsize`但是是 Section -- `e_shnum` 同上类推 +- `e_shnum` 同上类推 -- `e_shstrndx` Section 中字符串表的 index +- `e_shstrndx` Section 中字符串表的 index ### Section Header @@ -189,26 +189,26 @@ typedef struct elf64_shdr { ``` -- `sh_flags` 描述了 Section 的一些属性,包括 `SHF_WRITE`,`SHF_ALLOC`,`SHF_EXECINSTR` 等等 +- `sh_flags` 描述了 Section 的一些属性,包括 `SHF_WRITE`,`SHF_ALLOC`,`SHF_EXECINSTR` 等等 -- `sh_type`描述了 Section 的类型,包括了储存 dynamic linking table 的 `SHT_DYNAMIC` ,存放 linker symbol table 的 `SHT_SYMTAB`,由程序定义的 `SHT_PROGBITS`等等 +- `sh_type`描述了 Section 的类型,包括了储存 dynamic linking table 的 `SHT_DYNAMIC` ,存放 linker symbol table 的 `SHT_SYMTAB`,由程序定义的 `SHT_PROGBITS`等等 - 使用 `readelf -S`可以观察程序的 section headers + 使用 `readelf -S`可以观察程序的 section headers - ```log - root@da070736a297:/# readelf -S /bin/sh - There are 28 section headers, starting at offset 0x1d358: + ```log + root@da070736a297:/# readelf -S /bin/sh + There are 28 section headers, starting at offset 0x1d358: - Section Headers: - [Nr] Name Type Address Offset - Size EntSize Flags Link Info Align - [ 0] NULL 0000000000000000 00000000 - 0000000000000000 0000000000000000 0 0 0 - [ 1] .interp PROGBITS 0000000000000238 00000238 - 000000000000001c 0000000000000000 A 0 0 1 - [ 2] .note.ABI-tag NOTE 0000000000000254 00000254 - 0000000000000020 0000000000000000 A 0 0 4 - ``` + Section Headers: + [Nr] Name Type Address Offset + Size EntSize Flags Link Info Align + [ 0] NULL 0000000000000000 00000000 + 0000000000000000 0000000000000000 0 0 0 + [ 1] .interp PROGBITS 0000000000000238 00000238 + 000000000000001c 0000000000000000 A 0 0 1 + [ 2] .note.ABI-tag NOTE 0000000000000254 00000254 + 0000000000000020 0000000000000000 A 0 0 4 + ``` ### Program Header @@ -228,52 +228,52 @@ typedef struct elf64_phdr { ``` -- `p_type`表示 segment 的类型,包括有 `PT_LOAD`,`PT_DYNAMIC`,`PT_INTERP`等等。 - -- `p_flags` 包括有 `PF_X`,`PF_W`,`PF_R`等等,通过不同的 bit 表达不同的信息,可以相互组合。这决定了 segment 映射时的权限。 - - ```log - root@da070736a297:/# readelf -l /bin/sh - - Elf file type is DYN (Shared object file) - Entry point 0x4a20 - There are 9 program headers, starting at offset 64 - - Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 - 0x00000000000001f8 0x00000000000001f8 R 0x8 - INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238 - 0x000000000000001c 0x000000000000001c R 0x1 - [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] - LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 - 0x000000000001b268 0x000000000001b268 R E 0x200000 - LOAD 0x000000000001bf50 0x000000000021bf50 0x000000000021bf50 - 0x00000000000012d0 0x0000000000003f00 RW 0x200000 - DYNAMIC 0x000000000001cb28 0x000000000021cb28 0x000000000021cb28 - 0x00000000000001f0 0x00000000000001f0 RW 0x8 - NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254 - 0x0000000000000044 0x0000000000000044 R 0x4 - GNU_EH_FRAME 0x00000000000179e4 0x00000000000179e4 0x00000000000179e4 - 0x00000000000007dc 0x00000000000007dc R 0x4 - GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 - 0x0000000000000000 0x0000000000000000 RW 0x10 - GNU_RELRO 0x000000000001bf50 0x000000000021bf50 0x000000000021bf50 - 0x00000000000010b0 0x00000000000010b0 R 0x1 - - Section to Segment mapping: - Segment Sections... - 00 - 01 .interp - 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame - 03 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss - 04 .dynamic - 05 .note.ABI-tag .note.gnu.build-id - 06 .eh_frame_hdr - 07 - 08 .init_array .fini_array .data.rel.ro .dynamic .got - ``` +- `p_type`表示 segment 的类型,包括有 `PT_LOAD`,`PT_DYNAMIC`,`PT_INTERP`等等。 + +- `p_flags` 包括有 `PF_X`,`PF_W`,`PF_R`等等,通过不同的 bit 表达不同的信息,可以相互组合。这决定了 segment 映射时的权限。 + + ```log + root@da070736a297:/# readelf -l /bin/sh + + Elf file type is DYN (Shared object file) + Entry point 0x4a20 + There are 9 program headers, starting at offset 64 + + Program Headers: + Type Offset VirtAddr PhysAddr + FileSiz MemSiz Flags Align + PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 + 0x00000000000001f8 0x00000000000001f8 R 0x8 + INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238 + 0x000000000000001c 0x000000000000001c R 0x1 + [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] + LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 + 0x000000000001b268 0x000000000001b268 R E 0x200000 + LOAD 0x000000000001bf50 0x000000000021bf50 0x000000000021bf50 + 0x00000000000012d0 0x0000000000003f00 RW 0x200000 + DYNAMIC 0x000000000001cb28 0x000000000021cb28 0x000000000021cb28 + 0x00000000000001f0 0x00000000000001f0 RW 0x8 + NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254 + 0x0000000000000044 0x0000000000000044 R 0x4 + GNU_EH_FRAME 0x00000000000179e4 0x00000000000179e4 0x00000000000179e4 + 0x00000000000007dc 0x00000000000007dc R 0x4 + GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 + 0x0000000000000000 0x0000000000000000 RW 0x10 + GNU_RELRO 0x000000000001bf50 0x000000000021bf50 0x000000000021bf50 + 0x00000000000010b0 0x00000000000010b0 R 0x1 + + Section to Segment mapping: + Segment Sections... + 00 + 01 .interp + 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame + 03 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss + 04 .dynamic + 05 .note.ABI-tag .note.gnu.build-id + 06 .eh_frame_hdr + 07 + 08 .init_array .fini_array .data.rel.ro .dynamic .got + ``` ## 控制 ELF 的结构 diff --git a/docs/wiki/fat.md b/docs/wiki/fat.md index 53bda35..1803365 100644 --- a/docs/wiki/fat.md +++ b/docs/wiki/fat.md @@ -38,12 +38,12 @@ root/ 下图表示了一个文件系统磁盘中可能的样子: -- `#0` 标号的内存空间对应着系统的根目录 `/` 节点,其内部有两个有效的文件信息,分别是 `system` 和 `home`,均为目录类别。 -- `system` 首文件块指向 `#8`,并且 `#8` 为其最后一个文件块,因此 `#8` 实际上就对应着 `/system/` 节点,其中存储了 `kernel.elf` 一个文件的信息。 -- 对于 `home`,首文件块指向 `#14`,其下一个文件块为 `#16`,并且是最后一个文件块,因此 `#14`, `#16` 共同构成 `/home/` 节点,在这个节点中存储了两个有效文件信息,分别是 `boo.baz` 和 `bar`,分别是文件和目录。 -- `kernel.elf` 首文件块为 `#4`,`#4` 下一个文件块为 `#6`,`#6` 为最后一个文件块,因此 `kernel.elf` 对应的文件数据为 `#4`, `#6` 上的数据。 -- `boo.baz` 首文件块为 `#10`,后续文件块依次为 `#12`、`#5`。因此 `#10`, `#12`, `#5` 共同构成 `boo.baz` 文件的数据 -- `bar` 首文件块指向 `#18`,后续文件块为 `#11`,因此 `#18`, `#11` 共同构成了目录树中的 `/home/bar/` 节点的子树信息 +- `#0` 标号的内存空间对应着系统的根目录 `/` 节点,其内部有两个有效的文件信息,分别是 `system` 和 `home`,均为目录类别。 +- `system` 首文件块指向 `#8`,并且 `#8` 为其最后一个文件块,因此 `#8` 实际上就对应着 `/system/` 节点,其中存储了 `kernel.elf` 一个文件的信息。 +- 对于 `home`,首文件块指向 `#14`,其下一个文件块为 `#16`,并且是最后一个文件块,因此 `#14`, `#16` 共同构成 `/home/` 节点,在这个节点中存储了两个有效文件信息,分别是 `boo.baz` 和 `bar`,分别是文件和目录。 +- `kernel.elf` 首文件块为 `#4`,`#4` 下一个文件块为 `#6`,`#6` 为最后一个文件块,因此 `kernel.elf` 对应的文件数据为 `#4`, `#6` 上的数据。 +- `boo.baz` 首文件块为 `#10`,后续文件块依次为 `#12`、`#5`。因此 `#10`, `#12`, `#5` 共同构成 `boo.baz` 文件的数据 +- `bar` 首文件块指向 `#18`,后续文件块为 `#11`,因此 `#18`, `#11` 共同构成了目录树中的 `/home/bar/` 节点的子树信息 directory-structure @@ -55,10 +55,10 @@ root/ 现在,假设我们需要访问 `/system/kernel.elf` 文件,那么就需要: -- 首先从树的根部开始,找到类型为目录,名称为 `system` 的信息。 -- 从信息中读取出目录数据的第一个块 `#8`,并根据链表中的信息读入整个目录,由于这里只有一个块,所以就读入 `#8`。 -- 在目录的数据中找到类型为文件,名称为 `kernel.elf` 的信息。 -- 从信息中读出文件数据的第一个块 `#4`,并根据链表读入 `#6`,得到整个文件。 +- 首先从树的根部开始,找到类型为目录,名称为 `system` 的信息。 +- 从信息中读取出目录数据的第一个块 `#8`,并根据链表中的信息读入整个目录,由于这里只有一个块,所以就读入 `#8`。 +- 在目录的数据中找到类型为文件,名称为 `kernel.elf` 的信息。 +- 从信息中读出文件数据的第一个块 `#4`,并根据链表读入 `#6`,得到整个文件。 为了提高文件读写的速度,系统一般会维护一个打开的文件表 (Open-file table) 用于维护所有打开的文件的信息,所以一般来说,当打开一个文件的时候,除了会将它的全部数据就被读取到内存中,还会将其信息添加到打开文件表中,后续所有的增删改都只会对内存中的数据起作用,直到文件关闭时才再次写回磁盘。这样就避免了每次读写过程中对文件数据的定位以及读取。 @@ -90,9 +90,9 @@ root/ 第一章大体可以略过,其中较为重要的概念如下: -- 簇 (cluster): 一组逻辑连续的扇区。卷内的每个簇都可以通过一个簇号 N 指代 (referred to)。簇是文件分配的最小单元 (A unit of allocation),给任何一个文件所分配的全部空间的大小 _(All allocation for a file)_ 必须是簇的整数倍。 -- 分区 (partition): 在一个卷内的一系列扇区 -- 卷 (volume): 一个逻辑上的连续的扇区地址空间 +- 簇 (cluster): 一组逻辑连续的扇区。卷内的每个簇都可以通过一个簇号 N 指代 (referred to)。簇是文件分配的最小单元 (A unit of allocation),给任何一个文件所分配的全部空间的大小 _(All allocation for a file)_ 必须是簇的整数倍。 +- 分区 (partition): 在一个卷内的一系列扇区 +- 卷 (volume): 一个逻辑上的连续的扇区地址空间 ### FAT 卷结构 @@ -138,8 +138,8 @@ if(CountofClusters < 4085) { 也就是说,FAT 的类型只与簇的数量有关,而与磁盘的大小、扇区数量以及 BPB 中的 BS_FilSysType 域都无关,其中 -- FAT12 不能有超过 4084 个簇 -- FAT16 不能有超过 65524 个簇,也不能少于 4085 个簇 +- FAT12 不能有超过 4084 个簇 +- FAT16 不能有超过 65524 个簇,也不能少于 4085 个簇 ### FAT @@ -151,9 +151,9 @@ if(CountofClusters < 4085) { 对于不同的 FAT 格式,FAT 表中每个条目 (entry) 的大小不同: -- 对于 FAT12 而言,每个条目长 12 位 -- 对于 FAT16 而言,每个条目长 16 位 -- 对于 FAT32 而言,每个条目长 32 位 +- 对于 FAT12 而言,每个条目长 12 位 +- 对于 FAT16 而言,每个条目长 16 位 +- 对于 FAT32 而言,每个条目长 32 位 这时候就发现了,原来 FAT 后面的数字就是指代的 FAT 表中每个条目的长度,因此对于我们需要实现的 FAT16,每个条目长 16 位 @@ -171,9 +171,9 @@ FAT 表中第一个保留的条目 `FAT [0]` 包含了 `BPB_Media` 域中的内 例如,如果 `BPB_Media` 的值为 `0xF8`,那么 -- 对于 FAT12,值为 `0xFF8` -- 对于 FAT16,值为 `0xFFF8` -- 对于 FAT32,值为 `0xFFFFFF8` +- 对于 FAT12,值为 `0xFF8` +- 对于 FAT16,值为 `0xFFF8` +- 对于 FAT32,值为 `0xFFFFFF8` 第二个保留的条目 `FAT [1]` 在格式化时会被格式化工具赋一个 EOC 值,具体用处不明。 @@ -197,12 +197,12 @@ FAT 表中第一个保留的条目 `FAT [0]` 包含了 `BPB_Media` 域中的内 除此之外,短名称域还遵循以下规则: -- 若首字节为 `0xE5` 则代表当前条目为空 -- 若首字节为 `0x00` 则同样代表当前条目为空,并且还代表当前条目之后的所有条目都为空 -- 首字节不能为空格,也就是说文件名不能以空格开头 -- 目录中不能出现名称相同的两个条目 -- 不能出现小写字母 -- 不能出现ASCII 值小于 `0x20` 的字符(控制字符)以及 `0x22`、`0x2A`、`0x2B`、`0x2C`、`0x2E`、`0x2F`、`0x3A`、`0x3B`、`0x3C`、`0x3D`、`0x3E`、`0x3F`、`0x5B`、`0x5C`、`0x5D`、`0x7C`(部分特殊符号) +- 若首字节为 `0xE5` 则代表当前条目为空 +- 若首字节为 `0x00` 则同样代表当前条目为空,并且还代表当前条目之后的所有条目都为空 +- 首字节不能为空格,也就是说文件名不能以空格开头 +- 目录中不能出现名称相同的两个条目 +- 不能出现小写字母 +- 不能出现ASCII 值小于 `0x20` 的字符(控制字符)以及 `0x22`、`0x2A`、`0x2B`、`0x2C`、`0x2E`、`0x2F`、`0x3A`、`0x3B`、`0x3C`、`0x3D`、`0x3E`、`0x3F`、`0x5B`、`0x5C`、`0x5D`、`0x7C`(部分特殊符号) 然而,如果文件名称很长的话,短名称就显得不太够用了,所以 FAT 还额外提出了长名称解决方案,并且称存储短名称的条目为 `SFNEntry`,存储长名称的条目为 `LFNEntry`。 @@ -210,11 +210,11 @@ FAT 表中第一个保留的条目 `FAT [0]` 包含了 `BPB_Media` 域中的内 在目录中,一个文件对应的长名称条目和短名称条目连续存储,其中地址从低到高依次存储: -- 第 N 个长名称条目 -- 第 N-1 个长名称条目 -- … -- 第 1 个长名称条目 -- 短名称条目 +- 第 N 个长名称条目 +- 第 N-1 个长名称条目 +- … +- 第 1 个长名称条目 +- 短名称条目 但是目前短名称足够使用了,所以不妨暂时跳过长名称的进一步介绍和实现。 @@ -230,20 +230,20 @@ FAT 表中第一个保留的条目 `FAT [0]` 包含了 `BPB_Media` 域中的内 其中文件的日期包括三个部分: -- `[4:0]`:日(从 1 至 31) -- `[8:5]`:月(从 1 至 12) -- `[15:9]`:从 1980 年起的年份偏移(从 0 至 127) +- `[4:0]`:日(从 1 至 31) +- `[8:5]`:月(从 1 至 12) +- `[15:9]`:从 1980 年起的年份偏移(从 0 至 127) 文件的时间同样也包括三个部分: -- `[4:0]`:从零起经过的 2s 间隔数(从 0 至 29) -- `[10:5]`:分(从 0 至 59) -- `[15:11]`:时(从 0 至 23) +- `[4:0]`:从零起经过的 2s 间隔数(从 0 至 29) +- `[10:5]`:分(从 0 至 59) +- `[15:11]`:时(从 0 至 23) 可以看到,秒的精度为 2s,这也就引出了 CrtTimeTenth 这个域,其精度为 10ms,范围从 0 至 199,正好填补了 2s 之间的空缺,使得精度提高到 10ms。 ## 参考资料 -- [FAT - OSDev](https://osdev.org/FAT) -- [File Allocation Table - Wikipedia](https://en.wikipedia.org/wiki/File_Allocation_Table) -- [南京大学操作系统课程中关于 FAT 和 UNIX 文件系统知识的讲解 - Bilibili](https://www.bilibili.com/video/BV1oZ4y1t7ce) +- [FAT - OSDev](https://osdev.org/FAT) +- [File Allocation Table - Wikipedia](https://en.wikipedia.org/wiki/File_Allocation_Table) +- [南京大学操作系统课程中关于 FAT 和 UNIX 文件系统知识的讲解 - Bilibili](https://www.bilibili.com/video/BV1oZ4y1t7ce) diff --git a/docs/wiki/fs.md b/docs/wiki/fs.md index d0f2efe..e259c0f 100644 --- a/docs/wiki/fs.md +++ b/docs/wiki/fs.md @@ -16,8 +16,8 @@ 要想深刻理解文件系统,首先需要了解文件与持久存储。由于计算机内存中的数据在掉电后会丢失,应用程序需要将数据保存在持久存储设备上。为了满足这个需求,计算机操作系统一般会有两种处理方法: -- 应用程序直接操作持久存储设备,自己维护数据的存储和读写。 -- 应用程序通过操作系统提供的文件系统接口,由操作系统负责维护数据的存储和读写。 +- 应用程序直接操作持久存储设备,自己维护数据的存储和读写。 +- 应用程序通过操作系统提供的文件系统接口,由操作系统负责维护数据的存储和读写。 对于前者,由之前的学习可以知道:计算机中应用程序若要和设备直接交互,需要操作系统让渡部分设备管理权限,无形中会引入安全隐患(如侧信道攻击等)。此外,不同的设备有不同的操作模式,应用程序需要针对不同的设备编写不同的代码,增加了开发难度的同时,还降低了应用的可移植性。 @@ -55,9 +55,9 @@ 在本实验和[rCore](https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter6/2fs-implementation.html)的设计中,都采用了这样的思路,具体可以体现在: -- `BlockDevice`:文件系统与底层设备驱动之间通过抽象层 `BlockDevice` 来连接,避免了与设备驱动、分区方式的绑定。 -- `alloc` crate:通过 Rust 提供的 `alloc crate` 来隔离了语言层面的内存管理,避免了直接调用内存管理的内核函数,同时提供了在 `std` 环境下进行测试的能力。 -- `trait` 和 `dyn`:通过对文件系统的功能、相关数据结构进行抽象封装,从而使得创建虚拟文件系统和支持更多不同的文件系统成为可能。 +- `BlockDevice`:文件系统与底层设备驱动之间通过抽象层 `BlockDevice` 来连接,避免了与设备驱动、分区方式的绑定。 +- `alloc` crate:通过 Rust 提供的 `alloc crate` 来隔离了语言层面的内存管理,避免了直接调用内存管理的内核函数,同时提供了在 `std` 环境下进行测试的能力。 +- `trait` 和 `dyn`:通过对文件系统的功能、相关数据结构进行抽象封装,从而使得创建虚拟文件系统和支持更多不同的文件系统成为可能。 ### 实现方法 @@ -95,11 +95,11 @@ FAT 的实现很简单:它将文件系统的元信息和文件数据都存储 ## 参考资料 -- [File Systems - OSDev](https://wiki.osdev.org/File_Systems) -- [rCore - fs implementation](https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter6/index.html#) -- [FAT 和 UNIX 文件系统](https://jyywiki.cn/OS/2022/slides/27.slides.html#/) -- [Xv6 文件系统实现](https://jyywiki.cn/OS/2022/slides/29.slides.html#/) -- [你管这破玩意叫文件系统 - 微信科普公众号](https://mp.weixin.qq.com/s/q6OjwCXSk05TvX_BIu1M0g) -- [OSDI '22 Best Paper: XRP](https://www.usenix.org/conference/osdi22/presentation/zhong) -- [SPDKL: The Storage Performance Development Kit](https://spdk.io/doc/about.html) -- [存储系统-从没入门到刚入门](https://www.yuque.com/wwyf/blog/dhoobh) +- [File Systems - OSDev](https://wiki.osdev.org/File_Systems) +- [rCore - fs implementation](https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter6/index.html#) +- [FAT 和 UNIX 文件系统](https://jyywiki.cn/OS/2022/slides/27.slides.html#/) +- [Xv6 文件系统实现](https://jyywiki.cn/OS/2022/slides/29.slides.html#/) +- [你管这破玩意叫文件系统 - 微信科普公众号](https://mp.weixin.qq.com/s/q6OjwCXSk05TvX_BIu1M0g) +- [OSDI '22 Best Paper: XRP](https://www.usenix.org/conference/osdi22/presentation/zhong) +- [SPDKL: The Storage Performance Development Kit](https://spdk.io/doc/about.html) +- [存储系统-从没入门到刚入门](https://www.yuque.com/wwyf/blog/dhoobh) diff --git a/docs/wiki/interrupts.md b/docs/wiki/interrupts.md index b45efa7..184555f 100644 --- a/docs/wiki/interrupts.md +++ b/docs/wiki/interrupts.md @@ -54,11 +54,11 @@ 基于以上介绍,我们可以总结出中断的处理过程: -1. 触发中断 +1. 触发中断 中断可以由硬件(硬件中断)或软件(软件中断)引发。除了我们主动调用中断或硬件产生中断外,CPU 每执行完一条指令后,CPU 还会检查中断控制器,看看是否有中断请求。 -2. 保存当前状态(上下文) +2. 保存当前状态(上下文) 在处理中断之前,CPU 会保存当前的执行状态,包括寄存器内容、标志寄存器等。 @@ -72,21 +72,21 @@ 请查阅 [x64 数据结构概述](./structures.md) 中关于 TSS 的介绍,思考一下这个问题。 -3. 查找中断描述符 +3. 查找中断描述符 CPU 根据中断向量号在 IDT 中查找对应的中断门描述符(Interrupt Gate Descriptor)。 !!! note "与 IDT 有关的细节可以查阅 [x64 数据结构概述](./structures.md)中的介绍。" -4. 执行中断处理程序 +4. 执行中断处理程序 CPU 跳转到中断门描述符指定的中断处理程序的入口地址,开始执行中断处理代码。 -5. 中断处理程序执行 +5. 中断处理程序执行 中断处理程序负责处理特定中断类型。它可能包括设备输入输出、异常处理、系统调用等。 -6. 恢复状态 +6. 恢复状态 中断处理程序执行完毕后,CPU 从内核栈恢复之前保存的状态,继续执行被中断的程序。 diff --git a/docs/wiki/paging.md b/docs/wiki/paging.md index f977494..c37e204 100644 --- a/docs/wiki/paging.md +++ b/docs/wiki/paging.md @@ -48,9 +48,9 @@ 页表中的每一个表项都是一个 64 位的数据,能够描述一个页表项的属性,及一些权限管理: -- Readable:是否可读,表示是否允许从这个页读取数据。 -- Writable:是否可写,表示是否允许向这个页写入数据。 -- Executable:是否可执行,表示是否允许执行这个页上的指令。 +- Readable:是否可读,表示是否允许从这个页读取数据。 +- Writable:是否可写,表示是否允许向这个页写入数据。 +- Executable:是否可执行,表示是否允许执行这个页上的指令。 这三个主要权限一般被简写为 R/W/X,这部分内容将在处理 ELF 文件时用到。 @@ -66,9 +66,9 @@ 在 x64 架构中,有一些特殊的寄存器与页表相关,且在本实验中会用到,它们分别是: -- CR0:控制寄存器,存储了一些控制系统运行状态的标志位,包括根页表的写保护位。 -- CR2:页错误地址寄存器,存储了最近一次页错误的**虚拟地址**。 -- CR3:页表根地址寄存器,存储了页表的**物理地址**,也就是 PML4 的地址。 +- CR0:控制寄存器,存储了一些控制系统运行状态的标志位,包括根页表的写保护位。 +- CR2:页错误地址寄存器,存储了最近一次页错误的**虚拟地址**。 +- CR3:页表根地址寄存器,存储了页表的**物理地址**,也就是 PML4 的地址。 ## 参考资料 diff --git a/docs/wiki/rust.md b/docs/wiki/rust.md index 3c97be1..fd9163e 100644 --- a/docs/wiki/rust.md +++ b/docs/wiki/rust.md @@ -10,30 +10,30 @@ Rust 语言的基础语法可以参考 [Rust圣经](https://course.rs/) 或者 [ 其他可参考的学习资料: -- [Rust 之旅(交互式课程)](https://tourofrust.com/00_zh-cn.html) -- [Rust by Example](https://doc.rust-lang.org/rust-by-example/) -- [Rust Cookbook](https://rust-lang-nursery.github.io/rust-cookbook/) -- [清华的 Rust 课程](https://lab.cs.tsinghua.edu.cn/rust/) +- [Rust 之旅(交互式课程)](https://tourofrust.com/00_zh-cn.html) +- [Rust by Example](https://doc.rust-lang.org/rust-by-example/) +- [Rust Cookbook](https://rust-lang-nursery.github.io/rust-cookbook/) +- [清华的 Rust 课程](https://lab.cs.tsinghua.edu.cn/rust/) 值得注意的是,本实验内容并不要求你对 Rust 语言有深入的了解,只需要你能够 **理解并使用** Rust 语言的以下内容: -- **基本语法** +- **基本语法** 变量绑定、常量、表达式、基本类型、条件语句、模式匹配、函数 -- **所有权与结构化数据** +- **所有权与结构化数据** 所有权、移动语义、借用与可变引用、结构体、元组结构体、单位元结构体、枚举 -- **标准库** +- **标准库** `String`、`Vec`、`Result`、`Option`、错误处理、单元测试 -- **泛型、特型与生命周期** +- **泛型、特型与生命周期** 泛型、特型、标准库提供的常用特性、生命周期入门 -- **项目管理与常用库** +- **项目管理与常用库** Cargo 项目结构、命名规范、智能指针 diff --git a/docs/wiki/structures.md b/docs/wiki/structures.md index 6b45dbd..33a1190 100644 --- a/docs/wiki/structures.md +++ b/docs/wiki/structures.md @@ -28,9 +28,9 @@ 在 64 位长模式下,TSS 的结构与 32 位不同,它并不直接与任务切换挂钩,但是它仍然被用于存储特权级栈和中断栈: -- 特权级栈:最多三个,当权限级别从较低权限级别更改为较高权限级别时使用的栈指针。 -- 中断栈:最多七个,当中断发生时,如果定义了对应的中断栈,则使用对应栈指针。它可以用于存储不同**大小和用途**的中断栈,以处理不同的中断。 -- I/O 映射基址:包含从 TSS 底部到 I/O 权限位图的 16 位偏移量,在我们的实验中不会使用到。 +- 特权级栈:最多三个,当权限级别从较低权限级别更改为较高权限级别时使用的栈指针。 +- 中断栈:最多七个,当中断发生时,如果定义了对应的中断栈,则使用对应栈指针。它可以用于存储不同**大小和用途**的中断栈,以处理不同的中断。 +- I/O 映射基址:包含从 TSS 底部到 I/O 权限位图的 16 位偏移量,在我们的实验中不会使用到。 ## 中断描述符表 @@ -50,9 +50,9 @@ 门类型(Gate Type)字段指定了中断门的类型,包括: -- 中断门(Interrupt Gate):用于处理中断,中断处理程序会被调用,中断返回后,CPU 会恢复中断前的执行状态。 -- 陷阱门(Trap Gate):用于处理陷阱,陷阱处理程序会被调用,不同之处在于,对于中断门,中断在进入时自动禁用,并在 IRET 时重新启用,而陷阱门则不会主动禁用中断。 -- 任务门(Task Gate):曾用于任务切换,它在 x86-64 上已被完全删除。 +- 中断门(Interrupt Gate):用于处理中断,中断处理程序会被调用,中断返回后,CPU 会恢复中断前的执行状态。 +- 陷阱门(Trap Gate):用于处理陷阱,陷阱处理程序会被调用,不同之处在于,对于中断门,中断在进入时自动禁用,并在 IRET 时重新启用,而陷阱门则不会主动禁用中断。 +- 任务门(Task Gate):曾用于任务切换,它在 x86-64 上已被完全删除。 ## 页表 diff --git a/docs/wiki/uart.md b/docs/wiki/uart.md index 764fdb8..c2cd588 100644 --- a/docs/wiki/uart.md +++ b/docs/wiki/uart.md @@ -1,66 +1,67 @@ -# 串口输出简介 - -## 概述 - -处理器与外部设备进行通信有两种方式:并行通信与串行通信。与之对应的是两种硬件接口:并行接口与串行接口。并行接口通常用于高速数据传输,例如连接打印机、显示器、硬盘等设备。由于替代技术的普及和串口协议的迭代,并口其在计算机领域的使用程度已经大幅下降。 - -串口(Serial Port)是一种常见的计算机接口,用于在计算机和外部设备之间进行**串行**数据传输。串口是一种通用的调试接口,几乎所有计算机和嵌入式设备都提供了串口接口。这使得串口成为一种广泛支持的调试方法,可以在各种硬件平台和操作系统上使用。 - -串口提供了**低级别的硬件访问能力**,可以**直接**与设备进行通信。因此,串口通常用于**低级别的系统调试和硬件调试**,例如在操作系统启动之前或操作系统不可用的情况下进行调试。 - -基于实用性和实现的简便考虑,实验将采用串口作为操作系统的输入输出接口,从而避免对输出图像相关驱动、渲染相关问题的考虑。通过将操作系统的输出重定向到串口和终端程序,让它们完成输出信息的显示渲染工作。 - -!!! note "串口与屏幕显示" - 串口输出与常见的屏幕显示不同,这是两套**独立**的输出逻辑,请大家注意甄别。 - -## UART 与串口 - -在 x86 系统上,串口(Serial Port)通常使用 RS-232 协议,通过 UART 实现串行数据的发送和接收。UART 是在串行交互界面上负责对数据完成编解码硬件芯片。相较于其他串行数据交互协议,UART 的特点如下: - -1. 异步通信:UART 采用异步通信方式,发送方和接收方之间没有时钟信号同步,而是通过数据帧中的起始位和停止位来同步数据。 - -2. 全双工通信:UART 采用全双工通信方式,发送方和接收方可以同时发送和接收数据。 - -!!! note "关于 UART 细节" - 经讨论,TA 们认为 UART 硬件细节不是实验的重点,因此不要求同学们掌握 UART 细节,只需了解 UART 16550 接口的调用方法即可。 - - 如果你对 UART 协议的细节感兴趣,可以参考 [Serial Ports - OSDev](https://wiki.osdev.org/Serial_Ports) 以及 [UART串口知识整理 - 知乎](https://zhuanlan.zhihu.com/p/467003598)。 - - -## UART 16550 - -UART 16550 是一种集成电路芯片,用于串口通信。它是最常见和广泛使用的 UART 芯片之一,具有高可靠性和兼容性。UART 16550 芯片提供了一个标准的串行接口,可用于将计算机与外部设备进行数据传输。 - -在 x86_64 体系结构中,UART 16550 常用于完成与串口设备的通信,对应的接口也被命名为 COM 接口。COM 接口会被映射到标准的 I/O 端口,可以通过**读写 I/O 端口**来完成与串口通信。 - -COM 端口和 I/O 端口的映射关系你可以在 [Port Addresses](https://wiki.osdev.org/Serial_Ports#Port_Addresses) 中找到。 - -在本实验设计中,只需要关心 COM1 端口即可,它的 I/O 端口地址为 `0x3F8`。 - -## x86 I/O 端口 - -!!! note "关于 I/O 的访问方式" - - CPU 与计算机外部 I/O 设备的常见交互模式分为 Memory-mapped I/O (MMIO) 和 Port-mapped I/O 两种,这两种方式也被称为统一编址和独立编址。 - - - Memory-mapped I/O (MMIO) 即通过将需要进行交互的 I/O 设备的相关寄存器映射到某一段内存地址空间,从而实现对 I/O 设备的访问。在启用虚拟内存机制的系统中,这些内存空间**同样需要通过虚拟地址进行访问**。 - - - Port-mapped I/O 即将 I/O 设备的相关寄存器编址在相对与内存地址独立的地址空间,并使用专门的指令与 I/O 设备进行交互。在 x86 系统中,I/O 端口的地址空间为 0x0000 - 0xFFFF,可以通过 `in` 和 `out` 指令进行访问。 - - 由于历史遗留原因,x86 架构中同时存在 MMIO 和 port-mapped I/O 两种访问方式,不过由于 Port I/O 地址空间太小和其他的一些需求,现代硬件越来越偏向于使用 MMIO。 - - 有关串口设备驱动实现的更多信息,请参考 [Serial Ports - OSDev](https://wiki.osdev.org/Serial_Ports)。 - -在 x86 体系中,I/O 端口的读写是通过 `in` 和 `out` 指令完成的。`in` 指令用于从 I/O 端口读取数据,`out` 指令用于向 I/O 端口写入数据。 - -在使用 [x86_64 crate](https://docs.rs/x86_64) 时,可以通过 `x86_64::instructions::port` 模块中的 [`PortGeneric`](https://docs.rs/x86_64/latest/x86_64/instructions/port/struct.PortGeneric.html) 结构体来完成对 I/O 端口的读写。 - -!!! question "x86_64 crate 中是怎么封装 I/O 端口的?你能否通过查看源码找到答案?" - -## 参考资料 - -1. [UART 16550 - docs.rs](https://docs.rs/uart_16550) -2. [Serial Ports - OSDev](https://wiki.osdev.org/Serial_Ports) -3. [UART 16550 Tutorial - byterunner](http://byterunner.com/16550.html) -4. [UART 16550 Tutorial](http://www.larvierinehart.com/serial/serialadc/serial.htm) -5. [UART串口知识整理 - 知乎](https://zhuanlan.zhihu.com/p/467003598) +# 串口输出简介 + +## 概述 + +处理器与外部设备进行通信有两种方式:并行通信与串行通信。与之对应的是两种硬件接口:并行接口与串行接口。并行接口通常用于高速数据传输,例如连接打印机、显示器、硬盘等设备。由于替代技术的普及和串口协议的迭代,并口其在计算机领域的使用程度已经大幅下降。 + +串口(Serial Port)是一种常见的计算机接口,用于在计算机和外部设备之间进行**串行**数据传输。串口是一种通用的调试接口,几乎所有计算机和嵌入式设备都提供了串口接口。这使得串口成为一种广泛支持的调试方法,可以在各种硬件平台和操作系统上使用。 + +串口提供了**低级别的硬件访问能力**,可以**直接**与设备进行通信。因此,串口通常用于**低级别的系统调试和硬件调试**,例如在操作系统启动之前或操作系统不可用的情况下进行调试。 + +基于实用性和实现的简便考虑,实验将采用串口作为操作系统的输入输出接口,从而避免对输出图像相关驱动、渲染相关问题的考虑。通过将操作系统的输出重定向到串口和终端程序,让它们完成输出信息的显示渲染工作。 + +!!! note "串口与屏幕显示" + + 串口输出与常见的屏幕显示不同,这是两套**独立**的输出逻辑,请大家注意甄别。 + +## UART 与串口 + +在 x86 系统上,串口(Serial Port)通常使用 RS-232 协议,通过 UART 实现串行数据的发送和接收。UART 是在串行交互界面上负责对数据完成编解码硬件芯片。相较于其他串行数据交互协议,UART 的特点如下: + +1. 异步通信:UART 采用异步通信方式,发送方和接收方之间没有时钟信号同步,而是通过数据帧中的起始位和停止位来同步数据。 + +2. 全双工通信:UART 采用全双工通信方式,发送方和接收方可以同时发送和接收数据。 + +!!! note "关于 UART 细节" + + 经讨论,TA 们认为 UART 硬件细节不是实验的重点,因此不要求同学们掌握 UART 细节,只需了解 UART 16550 接口的调用方法即可。 + + 如果你对 UART 协议的细节感兴趣,可以参考 [Serial Ports - OSDev](https://wiki.osdev.org/Serial_Ports) 以及 [UART串口知识整理 - 知乎](https://zhuanlan.zhihu.com/p/467003598)。 + +## UART 16550 + +UART 16550 是一种集成电路芯片,用于串口通信。它是最常见和广泛使用的 UART 芯片之一,具有高可靠性和兼容性。UART 16550 芯片提供了一个标准的串行接口,可用于将计算机与外部设备进行数据传输。 + +在 x86_64 体系结构中,UART 16550 常用于完成与串口设备的通信,对应的接口也被命名为 COM 接口。COM 接口会被映射到标准的 I/O 端口,可以通过**读写 I/O 端口**来完成与串口通信。 + +COM 端口和 I/O 端口的映射关系你可以在 [Port Addresses](https://wiki.osdev.org/Serial_Ports#Port_Addresses) 中找到。 + +在本实验设计中,只需要关心 COM1 端口即可,它的 I/O 端口地址为 `0x3F8`。 + +## x86 I/O 端口 + +!!! note "关于 I/O 的访问方式" + + CPU 与计算机外部 I/O 设备的常见交互模式分为 Memory-mapped I/O (MMIO) 和 Port-mapped I/O 两种,这两种方式也被称为统一编址和独立编址。 + + - Memory-mapped I/O (MMIO) 即通过将需要进行交互的 I/O 设备的相关寄存器映射到某一段内存地址空间,从而实现对 I/O 设备的访问。在启用虚拟内存机制的系统中,这些内存空间**同样需要通过虚拟地址进行访问**。 + + - Port-mapped I/O 即将 I/O 设备的相关寄存器编址在相对与内存地址独立的地址空间,并使用专门的指令与 I/O 设备进行交互。在 x86 系统中,I/O 端口的地址空间为 0x0000 - 0xFFFF,可以通过 `in` 和 `out` 指令进行访问。 + + 由于历史遗留原因,x86 架构中同时存在 MMIO 和 port-mapped I/O 两种访问方式,不过由于 Port I/O 地址空间太小和其他的一些需求,现代硬件越来越偏向于使用 MMIO。 + + 有关串口设备驱动实现的更多信息,请参考 [Serial Ports - OSDev](https://wiki.osdev.org/Serial_Ports)。 + +在 x86 体系中,I/O 端口的读写是通过 `in` 和 `out` 指令完成的。`in` 指令用于从 I/O 端口读取数据,`out` 指令用于向 I/O 端口写入数据。 + +在使用 [x86_64 crate](https://docs.rs/x86_64) 时,可以通过 `x86_64::instructions::port` 模块中的 [`PortGeneric`](https://docs.rs/x86_64/latest/x86_64/instructions/port/struct.PortGeneric.html) 结构体来完成对 I/O 端口的读写。 + +!!! question "x86_64 crate 中是怎么封装 I/O 端口的?你能否通过查看源码找到答案?" + +## 参考资料 + +1. [UART 16550 - docs.rs](https://docs.rs/uart_16550) +2. [Serial Ports - OSDev](https://wiki.osdev.org/Serial_Ports) +3. [UART 16550 Tutorial - byterunner](http://byterunner.com/16550.html) +4. [UART 16550 Tutorial](http://www.larvierinehart.com/serial/serialadc/serial.htm) +5. [UART 串口知识整理 - 知乎](https://zhuanlan.zhihu.com/p/467003598) diff --git a/docs/wiki/userspace.md b/docs/wiki/userspace.md index 099d115..ab662d0 100644 --- a/docs/wiki/userspace.md +++ b/docs/wiki/userspace.md @@ -4,8 +4,8 @@ 从执行原理来说,不同进程的指令均运行在同一个处理器上,基于同一个处理器访问系统资源。但从资源管理、安全管理、崩溃预防的角度来看,操作系统需隔离敏感资源,限制不同进程的可执行的指令以及可访问的地址空间。因此,现代操作系统通常会将进程分为两种状态: -- 用户态(user mode):是指进程运行在低特权级别,只能执行受限指令,访问受限资源; -- 内核态(kernel mode):是指进程运行在高特权级别,可以执行特权指令,并且可以直接访问所有系统资源。 +- 用户态(user mode):是指进程运行在低特权级别,只能执行受限指令,访问受限资源; +- 内核态(kernel mode):是指进程运行在高特权级别,可以执行特权指令,并且可以直接访问所有系统资源。 处理器通常借助某个控制寄存器中的一个模式位(mode bit)来提供区分用户态、内核态。该寄存器描述了进程当前享有的特权。一个运行在内核态的进程可以执行指令集中的任何指令,并访问系统中的任何内存位置。 @@ -23,9 +23,9 @@ 在 x86_64 体系结构中,操作系统和应用程序运行在不同的特权级别(privilege levels)中,这些级别被称为 Rings。一般来说,较*低*的数字代表更*高*的特权级别: -- Ring 0:内核态,最高特权级别,可以执行所有指令,访问所有内存。 -- Ring 1/2:被设计用于设备驱动程序和其他系统服务,但在现代操作系统中不再使用。 -- Ring 3:用户态,最低特权级别,只能执行受限指令,需要访问受限资源时,必须通过系统调用等方式。 +- Ring 0:内核态,最高特权级别,可以执行所有指令,访问所有内存。 +- Ring 1/2:被设计用于设备驱动程序和其他系统服务,但在现代操作系统中不再使用。 +- Ring 3:用户态,最低特权级别,只能执行受限指令,需要访问受限资源时,必须通过系统调用等方式。 !!! question "为什么现代操作系统不使用 Ring 1/2?" @@ -68,9 +68,9 @@ C 语言标准库提供了一组函数和数据结构,供应用程序使用以 这些用户态库的设计与其提供的服务紧密相关: -- **跨平台兼容性**:用户态库的设计要求之一即屏蔽底层硬件的差异,向上提供统一的编程接口。换言之,用户态库的一致可以保持用户态程序跨平台兼容性,在更换平台时不需要修改应用程序的代码。 -- **性能与效率**:用户态库的设计需要考虑到性能和效率,尤其是对于系统调用等底层操作的封装,需要尽可能地减少额外的开销。[这篇文章](http://arkanis.de/weblog/2017-01-05-measurements-of-system-call-performance-and-overhead)测量了不同系统调用的性能和开销,并讨论了系统调用设计方法。 -- **功能完备性**:库中需要包含常用的系统调用封装和其他功能,以便开发者可以方便地进行应用程序开发,而不需要重复造轮子。 +- **跨平台兼容性**:用户态库的设计要求之一即屏蔽底层硬件的差异,向上提供统一的编程接口。换言之,用户态库的一致可以保持用户态程序跨平台兼容性,在更换平台时不需要修改应用程序的代码。 +- **性能与效率**:用户态库的设计需要考虑到性能和效率,尤其是对于系统调用等底层操作的封装,需要尽可能地减少额外的开销。[这篇文章](http://arkanis.de/weblog/2017-01-05-measurements-of-system-call-performance-and-overhead)测量了不同系统调用的性能和开销,并讨论了系统调用设计方法。 +- **功能完备性**:库中需要包含常用的系统调用封装和其他功能,以便开发者可以方便地进行应用程序开发,而不需要重复造轮子。 ### 用户态库的服务 @@ -92,10 +92,10 @@ Rust 语言中的标准用户态库是 `std`,它提供了一系列的系统调 ## 参考资料 -- [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) +- [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/docs/wiki/windows.md b/docs/wiki/windows.md index 16751e5..8be3bc3 100644 --- a/docs/wiki/windows.md +++ b/docs/wiki/windows.md @@ -39,8 +39,8 @@ 如果想要变更安装路径,可以通过指定如下环境变量来实现: -- `RUSTUP_HOME`:rustup 的安装路径 -- `CARGO_HOME`:cargo 的安装路径 +- `RUSTUP_HOME`:rustup 的安装路径 +- `CARGO_HOME`:cargo 的安装路径 rustup 将会把 `CARO_HOME\bin` 添加到 `PATH` 环境变量中。 @@ -48,12 +48,12 @@ rustup 将会把 `CARO_HOME\bin` 添加到 `PATH` 环境变量中。 rust 提供了两种 windows 上的工具链:`msvc` 和 `gnu`,详细信息可以参考 [Windows - The rustup book](https://rust-lang.github.io/rustup/installation/windows.html)。 -- `msvc`:使用 Visual Studio 的 C++ 编译器,需要安装 Visual Studio。 +- `msvc`:使用 Visual Studio 的 C++ 编译器,需要安装 Visual Studio。 在安装 Visual Studio 时,需要选择如下组件: - - `MSVC v143 - VS 2022 C++ x64/x86 build tools (latest)` - - `Windows 11 SDK` + - `MSVC v143 - VS 2022 C++ x64/x86 build tools (latest)` + - `Windows 11 SDK` `msvc` 工具链可以提供更好的 Windows 应用兼容性,也是 Windows 上开发 rust 应用推荐的工具链。 @@ -61,7 +61,7 @@ rust 提供了两种 windows 上的工具链:`msvc` 和 `gnu`,详细信息 此工具链本体占用约为 600MB。推荐本来就有 Visual Studio 的同学使用此工具链。 -- `gnu`:使用 GNU 工具链,需要安装 MinGW,会随工具链一起自动安装。 +- `gnu`:使用 GNU 工具链,需要安装 MinGW,会随工具链一起自动安装。 此工具链总体占用约 1GB。推荐未安装 Visual Studio 的同学使用此工具链。 diff --git a/mkdocs.yml b/mkdocs.yml index 1f1734f..945686e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,12 +8,12 @@ theme: custom_dir: docs/overrides language: zh palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate toggle: icon: material/brightness-4 @@ -37,55 +37,55 @@ theme: nav: - 主页: - - index.md - - 代码与提交规范: general/specification.md - - 寻求帮助: general/help.md - - 常见问题及解答: general/faq.md - - 使用 Typst 编写报告: general/typst.md + - index.md + - 代码与提交规范: general/specification.md + - 寻求帮助: general/help.md + - 常见问题及解答: general/faq.md + - 使用 Typst 编写报告: general/typst.md - 实验指南: - - 实验零:实验准备: - - labs/0x00/index.md - - 实验任务: labs/0x00/tasks.md - - 实验一:操作系统的启动: - - labs/0x01/index.md - - 实验任务: labs/0x01/tasks.md - - 实验二:中断处理: - - labs/0x02/index.md - - 实验任务: labs/0x02/tasks.md - - 实验三:内核线程与缺页异常: - - labs/0x03/index.md - - 实验任务: labs/0x03/tasks.md - - 实验四:用户程序与系统调用: - - labs/0x04/index.md - - 实验任务: labs/0x04/tasks.md - - 实验五:fork、阻塞与并发: - - labs/0x05/index.md - - 实验任务: labs/0x05/tasks.md - - 实验六:硬盘驱动与文件系统: - - labs/0x06/index.md - - 实验任务: labs/0x06/tasks.md - - 实验七:更好的内存管理: - - labs/0x07/index.md - - 实验任务: labs/0x07/tasks.md - - 实验八:扩展实验: - - labs/0x08/index.md + - 实验零:实验准备: + - labs/0x00/index.md + - 实验任务: labs/0x00/tasks.md + - 实验一:操作系统的启动: + - labs/0x01/index.md + - 实验任务: labs/0x01/tasks.md + - 实验二:中断处理: + - labs/0x02/index.md + - 实验任务: labs/0x02/tasks.md + - 实验三:内核线程与缺页异常: + - labs/0x03/index.md + - 实验任务: labs/0x03/tasks.md + - 实验四:用户程序与系统调用: + - labs/0x04/index.md + - 实验任务: labs/0x04/tasks.md + - 实验五:fork、阻塞与并发: + - labs/0x05/index.md + - 实验任务: labs/0x05/tasks.md + - 实验六:硬盘驱动与文件系统: + - labs/0x06/index.md + - 实验任务: labs/0x06/tasks.md + - 实验七:更好的内存管理: + - labs/0x07/index.md + - 实验任务: labs/0x07/tasks.md + - 实验八:扩展实验: + - labs/0x08/index.md - 实验资料: - - Linux 环境配置: wiki/linux.md - - Windows 环境配置: wiki/windows.md - - Rust 语言基础: wiki/rust.md - - 实验调试指南: wiki/debug.md - - UEFI 启动过程: wiki/uefi.md - - QEMU 使用参考: wiki/qemu.md - - 分页内存简述: wiki/paging.md - - ELF 文件格式: wiki/elf.md - - UART 串口通信: wiki/uart.md - - CPU 中断处理: wiki/interrupts.md - - APIC 可编程中断控制器: wiki/apic.md - - x64 数据结构概述: wiki/structures.md - - 用户空间: wiki/userspace.md - - 文件系统概述: wiki/fs.md - - ATA 硬盘简介: wiki/ata.md - - FAT 文件系统: wiki/fat.md + - Linux 环境配置: wiki/linux.md + - Windows 环境配置: wiki/windows.md + - Rust 语言基础: wiki/rust.md + - 实验调试指南: wiki/debug.md + - UEFI 启动过程: wiki/uefi.md + - QEMU 使用参考: wiki/qemu.md + - 分页内存简述: wiki/paging.md + - ELF 文件格式: wiki/elf.md + - UART 串口通信: wiki/uart.md + - CPU 中断处理: wiki/interrupts.md + - APIC 可编程中断控制器: wiki/apic.md + - x64 数据结构概述: wiki/structures.md + - 用户空间: wiki/userspace.md + - 文件系统概述: wiki/fs.md + - ATA 硬盘简介: wiki/ata.md + - FAT 文件系统: wiki/fat.md markdown_extensions: - attr_list: @@ -95,7 +95,8 @@ markdown_extensions: pygments_lang_class: true - pymdownx.superfences: - pymdownx.tabbed: - slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}} + slugify: + !!python/object/apply:pymdownx.slugs.slugify { kwds: { case: lower } } alternate_style: true - pymdownx.arithmatex: generic: true @@ -103,7 +104,8 @@ markdown_extensions: - admonition: - toc: permalink: true - slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}} + slugify: + !!python/object/apply:pymdownx.slugs.slugify { kwds: { case: lower } } plugins: - minify: @@ -122,7 +124,6 @@ extra_javascript: - https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js - scripts/katex.js - extra_css: - https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css - css/fonts.css