CS 110L: Safety in Systems Programming记录
前两周刷了下斯坦福公开课:CS 110L: Safety in Systems Programming,笔记记录下
课程地址:
- 2020 B站搬运:https://www.bilibili.com/video/BV1Ra411A7kN
- 2020:https://reberhardt.com/cs110l/spring-2020/
- 2021:https://reberhardt.com/cs110l/spring-2021/
- 2022:https://web.stanford.edu/class/cs110l/
Program Analysis程序分析
How can we find bugs in a program?
- Dynamic Analysis
- Fuzzing test
- Static Analysis
Dynamic Analysis动态分析
- Valgrind
- LLVM Sanitizers
- AddressSanitizer:out of bounds memory accesses, double free, use after free
- LeakSanitizer:memory leaks
- MemorySanitizer:use of uninitialized memory
- UndefinedBehaviorSanitizer:usage of null pointers, integer/float overflow, etc
- ThreadSanitizer:improper usage of threads
Fuzzing test模糊测试
Static Analysis静态分析
- Common C/C++ linter: clang-tidy
- Dataflow analysis
Memory Safety in Rust
题外话:语言和编译器
- 在C和C++中,我们通过推理和测试来知道内存错误
- 语言和编译器没有帮助我们太多
- 为了更容易地推理程序,Rust 对您可以编写的程序进行了一些限制
- 这可能很烦。。。
- 但它也给了我们一些很好的保证!
Note:这个“很烦”,是对于那边不理解内部(内存)发生了什么的人而言的。会让不知道自己在干什么的程序员不出能运行的代码
常见内存问题:
- Memory Leaks
- Double Frees
- Dangling Pointers
- Iterator Invalidation
什么是良好的代码?
- 前置/后置条件对于将代码分解为具有明确定义的接口的小块至关重要
- 开发者的职责则是维护好这些前置/后置条件
良好的内存管理
- 在任何复杂的程序中,内存应该释放在哪里?
- 释放得太早,代码的其他部分可能仍在使用指向该内存的指针,会有Dangling Pointers
- 如果没有在任何地方释放,会有Memory Leaks
- 良好的C/C++代码将清楚地定义内存是如何传递的,以及"谁"负责清理内存
- 如果您阅读C/C++代码,您将在注释中看到“所有权”的概念,其中“所有者”负责内存
Type systems类型系统
Ownership所有权
Borrowing借用
Ownership Rules所有权规则
First, let’s take a look at the ownership rules. Keep these rules in mind as we work through the examples that illustrate them:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
The ownership and borrowing rules are enforced at compile time!
Takeaways
- 在rust里面,每一块内存都由一个变量/函数"拥有"
- 所有权在代码中都是“显示的”(对比C/C++,“所有权”的描述在函数描述中)
- 当所有者出了scope范围,编译器会插入代码去释放内存
- 因为有了所有权模型,你就不会犯如下错误:
- Memory leaks
- Double frees
- Use-after-frees
- Other memory errors — next class!
Ownership Continued
Ownership
Ownership in Memory
- 当我们到达范围的末尾(由大括号指定)时,将调用
Drop
函数。 - 可以认为这是一个特殊函数,可以正确释放整个对象
- 类似于C++中的析构函数
- 具有 Rust Drop 特征的类型,有要调用的 Drop 函数
Clone function
在rust中,有些值不会使用堆上的内存,他们直接存放在栈上(整型,布尔型,其他...)
对于这些类型,赋值拷贝是全拷贝
当赋值时,那些只需要栈上存储的对象,默认一般会被拷贝
Types with this property have the Copy trait.
Instead of transferring ownership, ‘=‘ operator for assignment (e.g.,
let ryan = julio
) will create a copy如果类型实现了 Copy trait, Rust不会让它实现 Drop trait
Borrowing
Variables Rules in Rust
- 在Rust,所有数据片段(变量)默认都是不可变的
- 相当于每个变量都隐式的加了
cosnt
(只给数据所需要的权限) - 这
mut
关键字用来指定这个数据的变量能改变,和const
相反 - 如果变量没有
mut
关键字,并且你修改了这个数据,Rust的编译器不会让你编译通过
"Borrowing Type" == References
Code: Immutable + Mutable References
References Rules
- 对于不可变引用:在scope内,可以同时有多个不可变引用
- 对于可变引用:在scope内,同时只能有一个可变引用
Note: 如果你创建了一个reference, 这个原始的变量是:
- 如果这个引用是可变的:原始变量会暂时不可用
- 如果这个引用是不可变的:原始变量会暂时不可变???怎么理解,不是本来就不能变吗?也还有其它情况:内部可变
回顾:
- 使用所有权和借用规则,可以避免很多内存错误
- 编译器对代码会有很多限制,对程序员的要求高,写程序时会与编译器做斗争
Error Handling
Issues
- 缺乏适当的错误处理
- 使用 NULL 代替实际值
Error handling in C
- 如果函数可能遇到错误,则其返回类型为 int(有时为 void*)
- 如果函数成功,则返回 0。否则,如果遇到错误,则返回 -1
- 遇到错误的函数将全局变量 errno 设置为指示出错之处的整数。如果调用方看到函数返回 -1 或 NULL,它可以检查 errno 以查看遇到什么错误
关键点:
- 不同的返回值可能性表示成功+不同类型的错误
- 错误都记录在文档页面和/或标题注释中
- 所有这些都只是整数
- 调用者必须记得处理所有情况
Error-handling in C++(and many other languages)
Exceptions
对 C 样式错误处理的重大改进:
- 不必在调用可能产生错误的函数时每次编写错误传播代码
- 错误不会被忽视或遗漏
Error handling in Rust: Enums
更好的错误处理: Enums
- 枚举(enumeration) 是一种可以包含多个变体之一的类型
- Rust:match 表达式就像 C/C++/Java 中的 switch 语句,除了必须涵盖所有可能的变体
- 如果您只对少数几种错误情况感兴趣,可以使用默认绑定来捕获所有其他情况
- 与大多数常见语言中的枚举不同,Rust 枚举可以存储任意数据!
- You can extract data from variants using a match expression:
Error handling in Rust:Result
- 如果我们使用枚举来清楚地表示成功的返回/错误会怎样?
- 如果这个函数运行成功,返回OK(value)
- 如果有错误发生,返回Err(some error object)
Comparison to C errors
- 在C错误处理方面遇到了两个主要问题:
- 太容易遗漏错误
- 正确的错误处理过于冗长
- 这解决了第一个问题:现在从函数签名中可以明显看出哪些函数可以返回错误,并且(由于枚举规则)编译器将验证您是否对返回的错误执行了某些操作
- 第二个问题仍然是一个问题
Meet the ? operator
- 假设我们有个函数:
helper_function() -> Result<T, E>
let val: T = helper_function()?
是什么意思:- 如果
helper_function
返回成功Ok(some value),提取这个值设置给val
- 如果
helper_function
返回失败Err(some error), 停止并返回/传播该错误
- 如果
Panics
- 什么样的错误,我们不希望去传递或处理?
- 严重的、无法恢复的错误
- 们预计不会发生的错误,并且不想将精力投入到处理中
panic
宏会使程序立马崩溃,同时携带一些信息- 当返回成功时,
Result::unwrap()
和Result::expect()
允许我们去提取Ok中的值,如果返回Err,则会panic
unwrap()
and expect()
// File::open returns Result: Ok(file) or Err(error)
// Unwrap means:
// - “if result is Ok: store value inside enum in `file`
// - “if result is Err (opening file failed): panic (crash program)” // Panic if opening a file fails:
let mut file = File::open(filename).unwrap();
// `expect` is the same as `unwrap`, but allows you to print a
// more descriptive error message when panicking.
let mut file = File::open(filename).expect("Failed to open file");
// One more example with `expect` — panic with a helpful error message
// if reading from standard input fails. (Nothing to return here.)
let mut input = String::new();
io::stdin().read_to_string(&mut input).expect("Failed to read from stdin”);
Handling nulls
NULL:十亿美元的错误
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
- Tony Hoare
Custom Types
Box in Rust
- Create a Box
- Box goes on the heap
- Anything can go in the box
- Box owns whatever is in the box. When box goes out of scope -> value in box destroyed
- Same thing as unique_ptr in C++:
Throwback to Options
Chain of ownership
Introducing as_ref()
- 转换
&Option<T>
成Option<&T>
- 如果
Option
内部是None
,则返回None
Notes on writing Rust Code
- 阅读编译器错误信息,它们通常非常有用
rustc --explain
能获取到更多详细信息- 推荐使用
rust-analyzer
插件
Box
- Box类型存储一个指向heap内存的指针
- 能放任何东西在Box里面
Box::new(...)
分配内存并初始化- Box drop函数释放heap上的内存
- 这个drop函数是自动的被编译器插入
Struct
- Create a new struct
- Creating methods for a struct
- Let’s make a constructor
- Let’s make a function
Garbage collection
Overview
- C/C++有个问题:什么时候释放内存
- Rust: 使用类型系统来表示谁来释放内存,并让编译器检查一切是否正确
- 更古老的方法:Garbage collection
- 写程序时,不用但心内存释放的问题
- 程序运行时,runtime将观察内存何时不再使用,并为您释放内存
- 不用手动的去管理内存
Tracing garbage collection
Downsides of garbage collection垃圾收集的缺点
- Expensive
- Disruptive:会打断正在做的事情,去做GC
- Non-deterministic:什么时候发生GC?不确定,取决于使用了多少内存
- Precludes manual optimization
Latency matters延迟问题
- User interfaces界面开发
- Games游戏
- Self-driving cars自动驾驶汽车
- Payment processing支付处理
- High frequency trading高频交易
Takeaways
- 当有意义的时候使用GC语言,但要知道它们的限制
- 如果花了很长时间来开发应用程序而没有人使用它,那么您节省了多少内存并不重要
- 如果效率成为问题,您可以随时用其他语言重写某些组件
- GC 语言仍可能导致资源泄漏 — 文件描述符、数据库句柄、多线程代码中的争用条件等。
- 在资源受限或延迟敏感的环境中,GC可能不是一个可行的选择
Where is Rust used?
内存安全:在手动管理内存方面,C和C++通常很烂
GC通常会消耗很多资源,还有延迟问题
Rust 仍在进行手动内存管理
编译器为你做了很多工作
Rust 的类型系统旨在帮助我们传达我们期望的,以便编译器可以验证它们
对于同时需要内存安全和资源效率/低延迟的应用程序,会注意到人们转向 Rust
Object Oriented Programming in Rust: Traits
Classes
Advantages to Class Design类设计的优势
- 模块化:我们可以将大型系统分解为可管理的组件,这些组件提供清晰的接口,并且可以单独进行测试。
- 封装:将相关数据和方法组合成一个"对象"。
- 代码隐藏:不需要公开用户与类交互不需要的部分。
- 代码重用:希望对象根据它所接收的文件而有所不同?将一个参数添加到其构造函数中,突然之间,就有了两个不同的实现,但只有一个类!
- Other things? What do you think?
Reusing code with “inheritance”使用继承复用代码
Inheritance继承
- 通过继承,我们能够在许多不同类型的对象上使用一种方法的相同实现,这些对象通过父子关系组合在一起
- 子类继承所有方法和属性,它们能选择去覆写父类的函数
- Big concept in languages like Java
What might be the weaknesses of Inheritance?继承的缺点
- Inheritance Trees
Traits
How else can we decompose?
Traits Overview
Big Standard Rust Traits
- Copy: 当使用
=
时,会创建一个新的拷贝的实例,而不是转移所有权 - Clone: 当调用
.clone()
函数时,会返回一个新的copy实例 - Drop: 当变量超出了它的作用域,会定义一种释放实例内存的方法
- Display: 当要显示它(比如使用println时),定义一种格式化的方法
- Debug: 类似于Display
- Eq: 确定一个类型的两个实例是否相等
- PartialOrd: 定义实例比较的方法
Milestone: derive Debug
Milestone: functions -> traits
Generics in Rust泛型
Consolidating repetitive code合并重用代码
Rust generics have no runtime overhead没有运行时开销
编译时生成代码
Trait bounds
限制类型
Generics and Data Structures
Storing different types together
Vec<Box<dyn Roar>>
:dynamic dispatch
Reflecting on Traits vs. Inheritance
Traits vs. Inheritance: thinking about tradeoffs权衡
todo