CS140e (1) 驱动引导壳
第二次的作业主要内容有:驱动、引导、壳,带领你熟悉 Rust,鉴赏课程作者 Sergio Benitez 的精妙之作!
这次作业拖了好久,要是没有 @codeworm96 大神 push 我,我还能拖得更久囧。因为前面挖了一个不锁编译器版本的坑,所以为了让这次作业能够在 rustc 1.28.0-nightly (cd494c1f0 2018-06-27)
上面通过,还花了蛮多时间的。
Phase 1: Ferris Wheel
这个系列任务是让你按照要求把给定的小程序改对(或者是改成不能编译),有修改的行数的限制,另外还给了一个 test.sh
自动测试。
我刚把仓库拉下来就跑了一次 test.sh
,惊讶的发现竟然 compile-fail/
里面的两个点直接就过了,这肯定不对。按照 test.sh
手动执行
$ rustc compile-fail/modules-1.rs -Z no-trans
error: unknown debugging option: `no-trans`
查了一下,这个 -Z no-trans
只检查错误不做代码的变换。在新版本的编译器中,所有的 trans
都被改成了 codegen
。我觉得 codegen
确实是个好名字,一目了然就知道是生成代码,trans
没法一下子反应过来是做各种编译器变换、生成代码。
所以说,这里把 test.sh
里面的 -Z no-trans
改成 -Z no-codegen
就行了。下面罗列一下我在做这个系列的任务的时候觉得有意思的地方吧:
#[derive(Copy)]
要顺带#[derive(Clone)]
,因为Clone
is a supertrait ofCopy
- Rust 也有 const function(很合理),不过还不稳定,需要 feature gate。
u128
和i128
类型已经稳定了,不需要 feature gate。- Rust 竟然允许像这样
let x = &mut 10;
借一个字面值,语义是创建一个临时的值,然后借走。 - 一开始写
try.rs
的时候,感觉自己写的非常丑陋,发现我改完了比他的 diff budget 少了很多,而且这个文件的 diff budget 除了新增12行以外,还有额外的新增两个字符的限制,让我觉得我肯定少用了什么条件。然后想起来,以前在 Rust by Examples 上面看过错误类型的转换。用上了类型转换之后,就可以用?
操作符了,程序一下子就变得优雅了! - 要写一个又能接受
String
又能接受&str
的函数真复杂。
Phase 2: Oxidation » Subphase A: StackVec
写 IntoIterator
的时候,一开始我还在纠结又要造一个 IntoIter
类,好烦。后来一想,我们的 StackVec
不就是在 slice
外面包了一层吗,那不如直接用 slice
的。于是美滋滋地把 impl<'a, T> IntoIterator for &'a [T]
抄了过来。这样顺带 Deref
什么的也可以直接照搬。
另外,因为查文档的时候老是看到同一个东西又在 core
里面又在 std
里面,特地查了一下,一般 std
里面同名的东西就是从 core
又导出了一次。这样其实在写 no_std
的程序的时候挺开心的,因为还是有很多常用的功能在 core
里面的。包括像后面 kprintln!
也不用自己写格式化了,直接照搬 println!
就行。
Phase 2: Oxidation » Subphase B: volatile
这部分是让你鉴赏课程作者 Sergio Benitez 的精妙之作。
首先是搞了三个 trait:Readable<T>
、Writeable<T>
和 ReadableWriteable<T>
,把裸指针的 unsafe
操作包了起来,而且分开了读和写的功能,顺带提供了一些实用的工具函数。
接着三个宏:readable!
、writeable!
和 readable_writeable!
用来自动实现 trait。
然后就是定义和实现三个类:ReadVolatile<T>
、WriteVolatile<T>
和 Volatile<T>
。这三个类都标记了 Send
,因为这里面都只包含了一个指针,没有特殊的含义,所以可以把所有权转交给别的线程。
UniqueVolatile<T>
比 Volatile<T>
更复杂一些,多了一个 Unique<T>
。core::ptr::Unique
本身的处境就比较尴尬,先是被标记成了永远不稳定的特性,然后文档又被藏起来了。因为编译器内部实现用到了,所以实际上也还存在,没被删掉。尽管推荐使用 core::ptr::NonNull
替代,但是两者语义又不一样……
回到CS140e上面,作业问 UniqueVolatile<T>
有什么特别的地方,那就是 Unique<T>
多了 Sync
。文档是这么说的:
/// `Unique` pointers are `Send` if `T` is `Send` because the data they
/// reference is unaliased. Note that this aliasing invariant is
/// unenforced by the type system; the abstraction using the
/// `Unique` must enforce it.
#[unstable(feature = "ptr_internals", issue = "0")]
unsafe impl<T: Send + ?Sized> Send for Unique<T> { }
/// `Unique` pointers are `Sync` if `T` is `Sync` because the data they
/// reference is unaliased. Note that this aliasing invariant is
/// unenforced by the type system; the abstraction using the
/// `Unique` must enforce it.
#[unstable(feature = "ptr_internals", issue = "0")]
unsafe impl<T: Sync + ?Sized> Sync for Unique<T> { }
但是我觉得我还是无法理解,可能是因为我对 Send
和 Sync
的理解还不到位吧。而且有趣的是,好像作者自己也没有用到 UniqueVolatile<T>
……
另外,这一节作者特地教大家用 cargo doc --open
看文档,顺带其实也教了大家怎么写文档,感觉还是学到了。我顺带查了一下,原来 rustup doc
直接就能打开本地的 Rust 文档,包括 API 和官方的几本书。离线看文档还是很开心的,网页秒开。最惊喜的是连着几本书也一块带上了,要知道前不久为了在动车上看 The Rust Programming Language,我还特地开了热点先把所有章节打开……
Phase 2: Oxidation » Subphase C: xmodem
看代码的时候首先是看到了一个我已经遗忘掉的 Rust 语法糖了:
'next_packet: loop {
for _ in 0..10 {
match receiver.read_packet(&mut packet) {
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
Ok(0) => break 'next_packet,
Ok(n) => {
received += n;
into.write_all(&packet)?;
continue 'next_packet;
}
}
}
return Err(io::Error::new(io::ErrorKind::BrokenPipe, "bad receive"));
}
可以直接跳出多重循环,还是非常实用的。
然后写代码的时候,我发现了 Rust 一个很捉急的设定:
error[E0503]: cannot use `self.packet` because it was mutably borrowed
--> src/lib.rs:240:44
|
240 | self.expect_byte_or_cancel(self.packet, "wrong packet number")?;
| ---- ^^^^^^^^^^^ use of borrowed `*self`
| |
| borrow of `*self` occurs here
让我想起来以前看到的一篇文章 Non-lexical lifetimes: introduction,上面解释了说这样的问题出现是因为 Rust 的 lifetime checker 是根据词法来分析的,而没有根据语义来分析。虽然很容易就可以改掉,先 let packet = self.packet;
就可以解决了,但是这篇文章的作者也觉得这样很不好。好消息是,non-lexical lifetimes 和 MIR-based borrow-checker 已经开始在开发了!
还有一点就是,因为这里涉及到了非常多的 IO 操作,就一定会有失败的处理。这个时候就深刻地体会到 Rust ? operator
的优越性了。要是让我写 C/C++,我估计要么就偷懒不判断了,要么就 if
然后 goto
了,非常烦(其实还可以写宏)。
运行起来之后,我又遇到了一个小问题:thread '<unnamed>' panicked at 'attempt to add with overflow'
。这是在算校验码的时候,虽然根据 XModem 的规范,校验码计算就是自然溢出,但是 Rust 不知道,所以在 debug build 的情况下会自动 panic!
。我发现有个 std::num::Wrapping
类型可以用,可惜这里没有 std
,只好手动用 u8::wrapping_add(self, rhs: u8)
来算。
Phase 2: Oxidation » Subphase D: ttywrite
这里我遇到了一个小问题,就是怎么优雅地表达要么从文件输入、要么从 stdin
输入,我想要一个大概这样的效果:
let input: &mut CommonTraitOfStdinAndFile =
if opt.input_file {
// construct file input
} else {
// stdin
}
然而想想大概是做不到这样的,因为如果如果直接返回构造出了的对象,那 if
和 else
返回的类型不同;而如果分别在 if
和 else
块里面构造再返回引用,虽然 trait 是对的,但是构造出来的对象的生命周期比引用还短……所以唯一的办法就是把对象丢到堆上了……
Phase 3: Not a Seashell » Subphase A: Getting Started
首先我遇到的一个问题就是,我替换了 config.txt
之后,在 Raspberry Pi 3B+ 上面跑 act-led-blink.bin
跑不动,PWR 闪烁。查了一下说是供电不足,但是我插上了电源之后也不行……而我要是把课程给的 start.elf
复制上去后,就变回了 ACT 闪四下。这个事情我折腾了好久,我还去查了 device_tree=
是什么。
后来我发现,TTL线的RX/TX灯好像会稍微亮一下,赶紧用 screen
看了一眼,发现 PWR 灯亮的时候显示 On
;PWR 灯灭的时候显示 Off
。所实说实际上 act-led-blink.bin
是可以跑的,只不过迷之 PWR 灯和 ACT 灯搞反了……还好 TTY 还有输出,算破案了。
Phase 3: Not a Seashell » Hidden Subphase: rustc 1.28.0-nightly
自己挖的坑,自己就得填平……因为编译器版本比较新,os/kernel/
里面 make
不出所料地挂了。在 @codeworm96 大神的鼓励下,经过了不懈努力,终于能够编译通了……有需要的读者可以直接 patch
。下面是我遇到的各种问题:
首先最烦的一个事情是,每一次 make
都会从 core
开始全部编译一遍,哪怕是前面这些依赖之前都已经编译通过了。查了好久,最后发现把 Cargo.lock
删掉就好了,因为里面蜜汁有一个 std==1.0.0
的依赖,而我们这里的 std==0.1.0
,我也不清楚为什么我会有这个 Cargo.lock
。
然后有时候会告诉我找不到 compiler_builtins
,因为 ~/.xargo/lib/rustlib/aarch64-none-elf/lib
里面有不止一个候选……我也不清楚这是怎么造成的,反手直接 rm -rf
……
#![feature(macro_reexport)]
已经被 #![feature(use_extern_macros)]
包含了。
StrExt
和 SliceExt
两个 trait 已经删掉了,内容并入了 str
和 [T]
。
std_unicode
也类似,直接并入了 core
。
下面的三个文件变化较大,主要原因是很多原来的 std
实现的东西转移到了 core
里面,所以直接删掉就好了。我是直接 rustup doc
打开当前版本编译器文档看源代码,再跟课程给的内容做对比,然后复制粘贴删除一把梭:
到后面 kprintln!
用到了 panic!
之后,又遇到了非常神奇的链接错误:
build/kernel.a(kernel-06e9e5e237d25b27.kernel2.rcgu.o): In function `core::panicking::panic_fmt':
kernel2-30c9570e8bd6400576515b3e9ca4056b.rs:(.text._ZN4core9panicking9panic_fmt17hd5547619de8a957fE+0x38): undefined reference to `rust_begin_unwind'
找了半天也不知道咋办,后来瞎猫碰到死耗子,找到了 #[lang= "panic_impl"]
,竟然成功解决了:
#[lang = "panic_impl"]
#[no_mangle]
pub extern fn rust_begin_panic(info: &PanicInfo) -> ! {
loop {}
}
原理应该是把 rust_begin_panic
替换掉了,可能原先调用了 rust_begin_unwind
吧。
Phase 3: Not a Seashell » Subphase B: System Timer
在实现 timer::current_time
的时候我有个问题,我只知道计数器的值,又不知道时钟频率,我怎么算时间呢?唯一找到相关的地方就是手册第199页:
the SP804 expects a 1MHz clock
我就只好假定时钟频率是1MHz,这样正好计数器每1纳秒跳一次。
Phase 3: Not a Seashell » Subphase C: GPIO
课程作者一上来先吹了一下说这个编译期间的状态机转移检查只有 Rust 能做,感觉不明觉厉。看到作者给了一个
pub struct Gpio<State> {
pin: u8,
registers: &'static mut Registers,
_state: PhantomData<State>
}
之后去学习了一下 PhantomData<T>
,感觉这是一个萌萌哒的结构,文档上面还有萌萌哒的emoji。之后瞬间明白了这个状态机怎么做了,无非就是不同的 State 有不同的 impl。感觉 Rust 的泛型还是非常厉害的。
一番包装之后,主程序就完全不需要 unsafe
了,非常神奇,感觉又一次鉴赏了课程作者 Sergio Benitez 的精妙之作!
Phase 3: Not a Seashell » Subphase D: UART
在实现 UART 驱动的时候同样遇到了不知道时钟频率的问题,因为手册上老是说 250MHz
就假装是 250MHz
好了。
Phase 4: Boot ‘em Up
在写 shell 之前,我觉得还是先把 bootloader 写好比较合理。在这个时候就发现了前面 XModem 各种没测出来的 bug……调好了之后,第一次看到进度条成功走完真的非常兴奋。先把那个 act-led-blink.bin
传了过去,发现真的能跑。终于不用再来回拔插TF卡了!(虽然还是得来回拔插USB-TTL线)
Phase 3: Not a Seashell » Subphase E: The Shell
把前面写的所有东西都融合一下就好了。第一次写这么长一段 Rust(其实也就不到50行),算是新手经验大涨吧……