0%

Rustlings

这是我第一次刷Rustlings,还是很有收获的。最后新增的一些算法题也很有意思,不过整体来说还是适合有Rust语法知识基础的人学习。最后几个算法题刷的很慢,我的数据结构和算法学的不是很好,在这里有学到很多。不过我也参考了许多资料。如果有人看的话,也算一点收获叭。整体来说Rust写起来很爽!

参考资料

Rust语法基础

初学者应该主要从三个方面了解Rust:Rust语法基础,Cargo以及Rust的标准库和官方文档。

Rust 程序设计语言 - Rust 程序设计语言 简体中文版

这是社区推荐的的The Rust Programming Language的中文翻译版本。主要讲Rust语法基础,国内有开源作者撰写了Rust语言圣经两本书的内容比较相似,后者语言比较生动,内容也比较丰富,前者的话语言精炼一些,个人比较推荐去读前者,读不懂的时候再去看Rust语言圣经的版本,会有新的收获。

The Cargo Book

Introduction - The Cargo Book

The Cargo Book是关于Cargo的一本书,初学者可能只会使用到Cargo的一少部分命令和参数,但实际上,Rust受到广泛关注的一个原因,就来自于强大的构建和包管理工具Cargo,值得注意的是这本书的中文翻译版本最后的更新时间是2019年,相关的内容和英文最新版差别比较大,最好读英文版本。

Rust标准库文档

List of all items in this crate

Rust的标准库文档涵盖了基础阶段大部分的内容,结构体、宏、智能指针等等都在标准库文档中有详细的说明,Rust程序相比其他许多语言确实比较在学习和编写上难度更大 ,但是Rust设计者们也在极力减少开发者的心智负担,对于一些数据类型和结构,标准库中定义了一些好用的方法和属性,方便大家学习和使用。另外,标准库中也定义了一些基本的API接口,方便开发。总之Rust标准库是一座宝库。

Rust

crates.io: Rust Package Registry

crate.io是提供了诸多Rust开发者开发的库,可以直接在cargo.toml里面配置库名和版本就能使用,很方便,基础阶段Rust标准库文档是小宝库,在后续进阶开发阶段,crate.io就是名副其实的大宝库。言下之意是,基础阶段暂时不用看这个。

Youtube

Rustlings 5.0 | Intro | Learn Rust Interactively

**目前国内的Rust学习资源还在初级阶段,B站上暂时没有很完善的教程,推荐一个油管博主的视频,讲到比较陌生不好理解的地方,博主会把Rust基础教程和标准库文档贴出来,对着讲解,还是很有收获的,还能教你怎么快速找到自己需要查的知识点。但是这个博主是之前录的视频,没有训练营版本Rustlings最后一些练习的讲解。

Rustlings

这是我第一次刷Rustlings,还是很有收获的。最后新增的一些算法题也很有意思,不过整体来说还是适合有Rust语法知识基础的人学习。最后几个算法题刷的很慢,我的数据结构和算法学的不是很好,在这里有学到很多。不过我也参考了许多资料。如果有人看的话,也算一点收获叭。整体来说Rust写起来很爽!

参考资料

Rust语法基础

初学者应该主要从三个方面了解Rust:Rust语法基础,Cargo以及Rust的标准库和官方文档。

Rust 程序设计语言 - Rust 程序设计语言 简体中文版

这是社区推荐的的The Rust Programming Language的中文翻译版本。主要讲Rust语法基础,国内有开源作者撰写了Rust语言圣经两本书的内容比较相似,后者语言比较生动,内容也比较丰富,前者的话语言精炼一些,个人比较推荐去读前者,读不懂的时候再去看Rust语言圣经的版本,会有新的收获。

The Cargo Book

Introduction - The Cargo Book

The Cargo Book是关于Cargo的一本书,初学者可能只会使用到Cargo的一少部分命令和参数,但实际上,Rust受到广泛关注的一个原因,就来自于强大的构建和包管理工具Cargo,值得注意的是这本书的中文翻译版本最后的更新时间是2019年,相关的内容和英文最新版差别比较大,最好读英文版本。

Rust标准库文档

List of all items in this crate

Rust的标准库文档涵盖了基础阶段大部分的内容,结构体、宏、智能指针等等都在标准库文档中有详细的说明,Rust程序相比其他许多语言确实比较在学习和编写上难度更大 ,但是Rust设计者们也在极力减少开发者的心智负担,对于一些数据类型和结构,标准库中定义了一些好用的方法和属性,方便大家学习和使用。另外,标准库中也定义了一些基本的API接口,方便开发。总之Rust标准库是一座宝库。

Rust

crates.io: Rust Package Registry

crate.io是提供了诸多Rust开发者开发的库,可以直接在cargo.toml里面配置库名和版本就能使用,很方便,基础阶段Rust标准库文档是小宝库,在后续进阶开发阶段,crate.io就是名副其实的大宝库。言下之意是,基础阶段暂时不用看这个。

Youtube

Rustlings 5.0 | Intro | Learn Rust Interactively

**目前国内的Rust学习资源还在初级阶段,B站上暂时没有很完善的教程,推荐一个油管博主的视频,讲到比较陌生不好理解的地方,博主会把Rust基础教程和标准库文档贴出来,对着讲解,还是很有收获的,还能教你怎么快速找到自己需要查的知识点。但是这个博主是之前录的视频,没有训练营版本Rustlings最后一些练习的讲解。

有关于Rust基础,是笔者在做毕设时因需要而学的,故而阶段一没有花费什么精力。但是对于阶段二,情况则截然不同。笔者没有听从项目文档的建议,选择了单枪匹马完成所有5个任务,期间也出现了许多迷之错误…

对于Rust这门编程语言来说,笔者个人认为,虽然它可以被用来编写底层的代码,包括bootloader、UEFI、OS内核等等,但是它的编译器已经决定了Rust的上限。笔者不才,曾经研究过一部分Rust的反编译分析,发现Rust对于栈内存的使用似乎非常奢侈。与C语言不同,Rust对于Shadowing的处理不是复用原来的栈内存空间,而是直接使用新的空间。另外Rust的结构也相比C臃肿很多,这就导致Rust作为底层架构的开发语言时,使用的内存空间要远多于C语言,粗略估计,实现同一种功能的情况下,Rust平均需要比C至少多1倍的栈内存空间。在时间效率上,Rust要实现与C语言相同的时间效率,编写的unsafe代码定然不少,对于开发人员的要求也必然不低,在这一点上,Rust相较于C可能并没有提升太多的编程效率。因此,使用Rust与C编写底层代码各有千秋,Rust定然无法取代C,而C也必然要接受Rust的发展。当前,Rust底层生态严重不足,导致上层建筑难以跟进,一旦Rust适配了足够多的ISA与外设,将大大方便底层中较高级别的编程开发,但对于MCU等内存需求、效率需求高的场景,C依然是不可替代的选择。

在此附上笔者的blog网址,其中有之前编写的9篇Rust逆向分析blog:网址界面丑了点,不要在意

根据笔者对于Rust反编译的研究理解发现,Rust的各类结构具有一定的特征,在进行调试时,如果能够识别出此类特征,就能将汇编代码与源代码相对应,从而大致恢复源代码的执行逻辑,甚至直接逆向还原出源代码。正是因为笔者对Rust汇编的理解,在本次实验阶段二中的调试才能更快地找到问题所在。

rCore 总结

实验文档的第一句说:从零开始写一个内核,但后来发现并不是让学生真的从零开始写,只是在写好的内核上添加功能,有一点点失望,但还是学到了很多。

本次实验让我学到了:

  • Rust 的一些编程技巧,如 UPSafeCell。
  • RISC-V 的特权级机制
  • 使用上下文保存/恢复来进行多道程序调度
  • SV39 多级页表机制及在内核中的实现
  • shell 的原理:fork + exec
  • 文件系统的原理:inode + …
  • 并发同步互斥的原理

虽然没有感觉理解得非常深入,但感谢 rCore 带我在操作系统领域入了门,让我对之后的阶段有更多期待。

Rustlings 总结

在本次训练营之前都是写的 C/C++,也接触过一点 Rust,读了点官方教程,尝试用它写过几道力扣,感觉非常的痛苦,后来就没有再用了。

这次训练营第一次知道有 Rustlings 这个东西,通过 100 多道题来慢慢带新手入门,合理的难度设置加上之前的一点基础,让我基本熟悉了 Rust 的使用。

现在已经能够熟练编写 Rust 基础代码,使用所有权、泛型、Trait 等特性。但是标准库和各种高级用法如宏定义等还不太熟悉,用来完成 rCore 是够用了。

这次训练营的确让我喜欢上了 Rust,写代码不用考虑悬垂指针、内存泄露,以及优秀的包管理,写起来真是太爽了。期待以后使用 Rust 进行更多的项目!

总结

去年就已经参加一次了,但是止步第一阶段,今年能做完第二阶段已经出乎我的意料了,收获良多。

syscall

syscall是怎么发起的,系统调用时发生了哪些事情,这些都是第一次接触。

收获

spawn实现

如果不考虑其他因素,其实spawn类似于vfork+exec。

假定fork采用的是直接复制的策略,那vfork就是采用的阻塞父进程执行、获得父进程地址空间的引用、子进程运行、执行完成、恢复父进程。

fork 调用

主要部分是调用TaskControlBlock::fork, 返回父进程的TaskControlBlock的拷贝,并分配一个新的Pid。

Task部分各个结构的关系

TaskControlBlock

TaskControlBlock 代表一个任务或进程的控制块,用于存储任务的基本信息和状态。它包含了所有不会在运行时改变的内容,如进程ID (pid) 和内核栈 (kernel_stack)。此外,它还包含一个可变的 inner 字段,该字段封装了实际的任务状态信息:

pid:任务的唯一标识符。
kernel_stack:内核栈,用于保存该任务在内核态运行的栈信息。
inner:包含该任务的动态状态信息,用于存储在运行中可能变化的内容,如内存空间、任务上下文、进程树信息等。

TaskControlBlockInner

TaskControlBlockInner 是 TaskControlBlock 的内部状态结构体,用于存储运行期间动态变化的内容,如任务的上下文、内存管理、父子关系等。每个 TaskControlBlockInner 都包含以下字段:

trap_cx_ppn:存储陷入上下文(Trap Context)的物理页号,用于保存用户态的CPU上下文。
base_size:应用程序的基本大小,用于约束任务在内存中的地址空间。
task_cx:任务上下文,表示当前任务的 CPU 状态。
task_status:当前任务的状态(如 Ready、Running、Zombie)。
memory_set:用于管理该任务的地址空间。
parent 和 children:当前任务的父子进程关系。
exit_code:任务退出时的状态码。
heap_bottom 和 program_brk:用于管理堆内存的范围。

TaskManager

负责调度所有准备好运行的Task,它维护了一个 ready_queue 队列,包含了所有准备好运行的任务的 TaskControlBlock,从中取出任务并将其交给调度器:

ready_queue:一个队列,存储处于“Ready”状态的任务。

add 和 fetch:add 将任务添加到 ready_queue 中,fetch 从队列中取出任务进行调度。

文件系统

目录项

目录项

对于文件而言,它的内容在文件系统或内核看来没有任何既定的格式,只是一个字节序列。目录的内容却需要遵从一种特殊的格式,它可以看成一个目录项的序列,每个目录项都是一个二元组,包括目录下文件的文件名和索引节点编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// easy-fs/src/layout.rs

const NAME_LENGTH_LIMIT: usize = 27;

#[repr(C)]
pub struct DirEntry {
name: [u8; NAME_LENGTH_LIMIT + 1],
inode_number: u32,
}

pub const DIRENT_SZ: usize = 32;

impl DirEntry {
pub fn empty() -> Self;
pub fn new(name: &str, inode_number: u32) -> Self;
pub fn name(&self) -> &str;
pub fn inode_number(&self) -> u32
}

在从目录中读取目录项,或将目录项写入目录时,需要将目录项转化为缓冲区(即字节切片)的形式来符合 read_at OR write_at 接口的要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// easy-fs/src/layout.rs

impl DirEntry {
pub fn as_bytes(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(
self as *const _ as usize as *const u8,
DIRENT_SZ,
)
}
}
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
unsafe {
core::slice::from_raw_parts_mut(
self as *mut _ as usize as *mut u8,
DIRENT_SZ,
)
}
}
}

Rustling总结

我想成为Rustacean,通过rustling的练习,我学到一些没接触过的Rust知识

repr attribute

在c/c++中,常常使用attribute((align))的方式来确保内存对齐,repr在rust的功能久石让每一个数据能够按照k的整数倍分配,k通常是基本类型。

Rusty 阶乘

1
2
3
pub fn factorial(num: u64) -> u64 {
(1..=num).fold(1, |acc, x| acc * x)
}

范围 (1..=num):

这个表达式创建了一个从 1 到 num 的范围,包括 num 本身。..= 是一个闭区间语法,表示这个范围是包含起始值和结束值的。

fold 方法:

fold 是一个高阶函数,它用于将一个迭代器中的所有元素合并成一个单一的值。
它的第一个参数是初始值,在这里是 1。这个值会作为累加器的初始状态。
第二个参数是一个闭包(匿名函数),它接受两个参数:acc(累加器的当前值)和 x(迭代器中的当前值)。
在每一次迭代中,闭包会将 acc 和 x 相乘,并返回新的累加器值。

流敏感分析

依靠 Borrow Checker 确保内存安全,原理如下:

执行路径分析:Rust 编译器在编译过程中会为每个变量跟踪其借用状态或所有权。在程序的不同控制流(如条件判断、循环、函数调用等)中,编译器会检查在这些不同路径下变量的状态变化,并在所有路径中保持一致性。

数据流分析:Rust 编译器通过数据流分析来确定变量的生存期、是否被借用、以及是否存在竞争条件。这个分析是流敏感的,意味着它会根据程序的控制流更新变量状态。

作用域检查:编译器在分析时会检查变量是否在作用域内、是否已经被销毁或者转移所有权。通过流敏感分析,编译器能够精确确定哪些变量在某条执行路径中被有效引用或借用。

take

在 Rust 中,take() 方法通常用于在某些容器类型(如 Option、Result 等)中“取走”值,将原来的值替换为一个默认值(通常是 None 或 Err),同时返回原来的值。

take() 经常用于以下场景:

转移所有权:从一个可选值中取出所有权,并清空该值,避免复制或克隆。

链表或树结构:当你遍历或修改链表或树结构时,可以用 take() 来“取走”节点的引用并避免借用冲突。

zero-cost futures

async/await实现的future类型不会引入任何额外的运行时开销。

无运行时依赖:与其他编程语言(如 JavaScript 或 Python)不同,Rust 的 async/await 本身不依赖特定的运行时机制。Future 是惰性的,它本质上是一个状态机,只有在被轮询时才会前进。虽然需要某种形式的运行时(如 Tokio 或 async-std)来调度异步任务,但这些运行时并没有与 async/await 特性本身紧耦合。

Any Trait

Rust中的Any trait允许在运行时进行类型检查和类型转换。这个类型在处理动态类型时较为有用。

功能:

  1. 类型检查:通过is::()方法,可以检查一个值是否是某种特定类型。

  2. 类型转换:使用downcast_ref::()和downcast::()方法,可以将一个&dyn Any或者Box类型的值转换回具体类型T。

注意:Any trait只能用于’static 生命周期的类型,这意味着他不能用于包含非静态引用的类型。

并且,Any会引入运行时开销,因为它依赖动态分派。

UnsafeCell

Rust 中内部可变性的核心原语。

如果您使用的是 &T,则通常在 Rust 中,编译器基于 &T 指向不可变数据的知识来执行优化。例如通过别名或通过将 &T 转换为 &mut T 来可变的该数据,被认为是未定义的行为。 UnsafeCell 选择退出 &T 的不可变性保证:共享的引用 &UnsafeCell 可能指向正在发生可变的数据。这称为内部可变性。

所有其他允许内部可变性的类型,例如 Cell 和 RefCell,在内部使用 UnsafeCell 来包装它们的数据。

所有其他允许内部可变性的类型,例如 Cell 和 RefCell,在内部使用 UnsafeCell 来包装它们的数据。

UnsafeCell API 本身在技术上非常简单: .get() 为其内容提供了裸指针 *mut T。正确使用该裸指针取决于您。

如果您使用生命周期 ‘a (&T 或 &mut T 引用) 创建安全引用,那么您不得以任何与 ‘a 其余部分的引用相矛盾的方式访问数据。 例如,这意味着如果您从 UnsafeCell 中取出 *mut T 并将其转换为 &T,则 T 中的数据必须保持不可变 (当然,对 T 中找到的任何 UnsafeCell 数据取模),直到引用的生命周期到期为止。 同样,如果您创建的 &mut T 引用已发布为安全代码,则在引用终止之前,您不得访问 UnsafeCell 中的数据。

对于没有 UnsafeCell<> 的 &T 和 &mut T,在引用过期之前,您也不得释放数据。作为一个特殊的例外,给定一个 &T,它在 UnsafeCell<> 内的任何部分都可能在引用的生命周期期间被释放,在最后一次使用引用之后 (解引用或重新借用)。 因为您不能释放引用指向的部分,这意味着只有当它的每一部分 (包括填充) 都在 UnsafeCell 中时,&T 指向的内存才能被释放。

但是,无论何时构造或解引用 &UnsafeCell,它仍必须指向活动内存,并且如果编译器可以证明该内存尚未被释放,则允许编译器插入虚假读取。

在任何时候,您都必须避免数据竞争。如果多个线程可以访问同一个 UnsafeCell,那么任何写操作都必须在与所有其他访问 (或使用原子) 相关之前发生正确的事件。

为了帮助进行正确的设计,以下情况明确声明为单线程代码合法:

&T 引用可以释放为安全代码,并且可以与其他 &T 引用共存,但不能与 &mut T 共存

&mut T 引用可以发布为安全代码,前提是其他 &mut T 和 &T 都不共存。&mut T 必须始终是唯一的。

请注意,虽然可以更改 &UnsafeCell 的内容 (即使其他 &UnsafeCell 引用了该 cell 的别名) 也可以 (只要以其他方式实现上述不变量即可),但是具有多个 &mut UnsafeCell 别名仍然是未定义的行为。 也就是说,UnsafeCell 是一个包装器,旨在通过 &UnsafeCell<> 与 shared accesses (i.e. 进行特殊交互 (引用) ; 通过 &mut UnsafeCell<> 处理 exclusive accesses (e.g. 时没有任何魔术) : 在该 &mut 借用期间, cell 和包装值都不能被别名。

.get_mut() 访问器展示了这一点,该访问器是产生 &mut T 的 safe getter。

UnsafeCell 与其内部类型 T 具有相同的内存表示。此保证的结果是可以在 T 和 UnsafeCell 之间进行转换。 将 Outer 类型内的嵌套 T 转换为 Outer<UnsafeCell> 类型时必须特别小心: 当 Outer 类型启用 niche 优化时,这不是正确的。

std::mem::MaybeUninit

Rust 编译器要求变量要根据其类型正确初始化。

比如引用类型的变量必须对齐且非空。这是一个必须始终坚持的不变量,即使在 Unsafe 代码中也是如此。因此,零初始化引用类型的变量会导致立即未定义行为,无论该引用是否访问过内存。

编译器利用这一点,进行各种优化,并且可以省略运行时检查。

由调用者来保证MaybeUninit确实处于初始化状态。当内存尚未完全初始化时调用 assume_init() 会导致立即未定义的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

#![allow(unused)]

fn main() {
use std::mem::{self, MaybeUninit};
// 不符合:零初始化引用
let x: &i32 = unsafe { mem::zeroed() }; // undefined behavior! ⚠️
// 等价于 `MaybeUninit<&i32>`:
let x: &i32 = unsafe { MaybeUninit::zeroed().assume_init() }; // undefined behavior!
// 不符合:布尔值必须初始化
let b: bool = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️
// 等价于 `MaybeUninit<bool>`:
let b: bool = unsafe { MaybeUninit::uninit().assume_init() }; // undefined behavior!
// 不符合:整数类型也必须初始化
let x: i32 = unsafe { mem::uninitialized() }; // undefined behavior! ⚠️
// 等价于 `MaybeUninit<i32>`:
let x: i32 = unsafe { MaybeUninit::uninit().assume_init() };

// 不符合:Vec未初始化内存使用 set_len 是未定义行为
let mut vec: Vec<u8> = Vec::with_capacity(1000);
unsafe { vec.set_len(1000); }
reader.read(&mut vec); // undefined behavior!
}

主要就是这些收获。

2024一阶段总结

由于我在暑假期间花了十几天时间学习了一下Rust,所以在这一阶段期间我就是相当于直接做题了,没什么可说的。但是,第一阶段也是Rust学习阶段,那么这样来说,这一阶段也就不仅仅是那三个星期,还包括了暑假期间的学习。所以,我就把这一阶段的总结写在这里。

Read more »

第二阶段开始从零开始构建操作系统的各个模块,不断完善操作系统核心功能,这一阶段,我首先从裸机开始实现了一个简化的内核启动程序,学习到了从了裸机开发OS的基本思路,后面随着每章的习题练习,不断学习了进程管理、内存管理、文件系统、并发等操作系统必备的各个子系统的基本原理。

第一个实验是实现sys_task_info系统调用,这个系统调用用于获取任务的一些基本信息,包含任务状态、使用的系统调用次数以及任务运行的时间,通过这个系统调用的实现我学习到了任务的基本实现原理及任务调度的基本原理。

第二个实验是实现 mmap 和 munmap 两个系统调用,首先将传入的参数转换成VirtAddr结构, mmap中, 获取当前进程task, 通过调用 task 中 memory_set.insert_framed_area 进行映射并插入到 memory_set 中; munmap中, 同样 获取当前进程的task, 然后获取task的memory_set检查内存区域是否合法, 最后在调用 memory_set.umap接触映射。这个实现我学习到了内存管理实现的基本原理。

第三个实验是实现 spawn 系统调用和stride 调度算法。spawn 实现了fork+exec,根据进程名解析elf,使用TaskControlBlock::new创建进程,加入到任务队列中,即完成了进程的创建及执行。stride 实现了任务根据优先级进行简单调度算法,通过优先级计算pass,P.pass = BigStride / P.priority 进程每次调度进行累加,每次调度根据stride,找到最小的进行调度。通过这个实验也算是自己实现了一个任务调度算法,感觉还是比较神奇的。

第四个实验是实现了三个系统调用 sys_linkat、sys_unlinkat、sys_stat,这三个系统调用都是和文件相关的,我实现的比较粗暴,link实现根据源文件,创建DirEntry跟原来的文件名不同,其他相同,unlink根据文件名删除Direntry, stat实现为每一个File示例实现stat函数,或许在细致实现一下可以增加一些字段来进行必要的记录,以避免每次都遍历root node。这个实验算是文件系统的小时牛刀,对于理解文件系统的原理十分有帮助。

第五个实验实现了是否开启死锁检查系统调用enable_deadlock_detect及死锁检查的算法。enable_deadlock_detect实现是为每个进程增加一个标记enable_dld来控制是否开启死锁检测,死锁检测算法是按照题目中给出的算法描述严格实现的,其实实际上可以简化很多,例如如果将锁比作临界资源,一个进程一次获取锁只相当于获取了数量为1的资源,是不可能同时占有多于1个 临界资源的。

在逐步构建和完善操作系统的过程中,我深入理解操作系统的内部机制,并逐步实现从基础功能到复杂功能的过渡。

rcore秋季训练营总结

在 RCore 夏令营第二阶段,我收获颇丰,对 Rust 语言与操作系统有了更为深入的理解。在 Rust 语言学习方面,我全面掌握了其独特的所有权、借用和生命周期规则,这些特性构成了 Rust 保障内存安全的坚固防线。对于操作系统,我不再是浅尝辄止。从内核架构层面,清晰地理解了从启动到各个模块交互的过程,特别是在进程管理中,明白了进程的创建、销毁以及状态转换原理,以及不同调度算法对 CPU 资源分配的影响。在内存管理领域,深入研究了从物理内存分配到虚拟内存映射的机制,页表机制作为连接二者的关键,让我惊叹于内存管理的复杂性和精妙性。在实现小型系统组件的个人项目中,我将 Rust 与操作系统知识融合,经历了从设计、实现到调试的完整流程,在解决诸多难题中实现了自我提升。这一阶段的学习为我打开了新世界的大门,未来我将持续探索,把所学更好地应用于实践。