Rusttt
👴❤恰🦀
Rusttttt
给 [rust](Rustacean.net: Home of Ferris the Crab) 稍微做点记录, (虽然估计这种笔记一样的法子不一定有用, 但是个人比较想写下来
顺序十有八九会很混乱, 属于是看到哪里就记录到哪里的类型了, 下文里面全角是原文, 半角是笔者自己写的一些各种东西
照着这些学的: [rust-by-example](格式化输出 - 通过例子学 Rust 中文版 (rustwiki.org)), [rust圣经](安装 Rust 环境 - Rust语言圣经(Rust Course)), [练习题](sunface/rust-by-practice: Learning Rust By Practice, narrowing the gap between beginner and skilled-dev through challenging examples, exercises and projects. (github.com)).
后面暂时懒得写了, 有时间写完就补.
0x00 安装环境
暂时还在 win 上学 rust , 下个 c++ 环境然后使用 rustup , 等过段时间在 arch 装了会补.
1 |
|
0x01 Rust 格式化输出
这个宏的作用似乎就是让写 c 和 py 中 fmt 的人更加熟悉, 反正也没学好, 再学一遍.
std::fmt
包含多种 trait
(特质)来控制文字显示,其中重要的两种 trait 的基本形式如下:
fmt::Debug
:使用{:?}
标记。格式化文本以供调试使用.fmt::Display
:使用{}
标记。以更优雅和友好的风格来格式化文本.
基本的格式化宏是这些:
format!
:将格式化文本写到字符串print!
:与format!
类似,但将文本输出到控制台(io::stdout)。println!
: 与print!
类似,但输出结果追加一个换行符。eprint!
:与print!
类似,但将文本输出到标准错误(io::stderr)。eprintln!
:与eprint!
类似,但输出结果追加一个换行符。
参数也有很多种, 比如可以使用 {1}
这种位置参数, 也可以是命名参数,
还可以应用一些诸如宽度/对齐这一类的, 比如宽度:
1 |
|
比如说要补全, 例如从 10 变到 0010 需要 {:04}
, 指定宽度 + 填充字符.
这和 c 的格式化字符串的那些参数基本类似, 不多赘述了(
0x02 变量
绑定/可变性
rust 圣经里面说: 在其他语言中给一个变量赋值大概是一个这样的语句: var a = 'otto'
,
而在 rust 中, 这个赋值过程是这样的写法:
1 |
|
这个过程在 rust 中有一个新名字叫 ‘变量绑定’ .
为何不用赋值而用绑定呢(其实你也可以称之为赋值,但是绑定的含义更清晰准确)?这里就涉及 Rust 最核心的原则——所有权,简单来讲,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人(聪明的读者应该能猜到,在这种情况下,该对象之前的主人就会丧失对该对象的所有权),像极了我们的现实世界,不是吗?
这似乎看起来非常安全的样子(
绑定后的变量在一般情况下是不可变的, 即:
1 |
|
这种单纯使用类似 c 一样的赋值语句会报错 ‘无法对不可变的变量重复赋值’, 如果想要让这个变量是可变的, 只需要加入 mut
关键字即可.
下划线忽略
如果我们创建一个变量却不使用的话, 只需在前面加一个下划线, 编译器就不会给出提示.
感觉这也是偏向于安全性的提醒, 挺有趣的(
变量解构
let
表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容.
好像很复杂, 实际看起来就是匹配复杂表达式的值的感觉, 强调 let 的使用.
1 |
|
常量/变量
本来觉得 let 声明的变量不可变和 const 是差不多的(
rust 中也存在 const 关键字, 不可用 mut 标注, 且自始至终不可变, 这样看来似乎 mut 是单纯维护安全性的设定了, Rust 常量的命名约定是全部字母都使用大写, 并使用下划线分隔单词, 另外对数字字面量可插入下划线以提高可读性.
相同变量 (shadowing)
rust 在声明相同名字的变量时采取后者覆盖前者的一个设定,
1 |
|
但是这些叫做 x 的变量是完全不同的几个东西, 实际上是多次声明相同变量的行为.
0x03 各种类型
Rust 每个值都有其确切的数据类型,总的来说可以分为两类:基本类型和复合类型。 基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),由以下组成:
- 数值类型: 有符号整数 (
i8
,i16
,i32
,i64
,isize
)、 无符号整数 (u8
,u16
,u32
,u64
,usize
) 、浮点数 (f32
,f64
)、以及有理数、复数 - 字符串:字符串字面量和字符串切片
&str
- 布尔类型:
true
和false
- 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
- 单元类型: 即
()
,其唯一的值也是()
类型推导
这里大概的意思就是, rust 编译器比较智能, 它可以适当推测这个变量是个什么类型, 比如:
1 |
|
在给出这个 let 语句之后, 编译器就会猜这是个什么东西, 如果能猜出来就猜出来了, 猜不出来会报错.
但是实际上又没有那么智能, 所以实际上需要一些标注, 像这样:
1 |
|
类型后缀也可以, 比如 let a = 20i32
.
在 rust 中, 只有同一类型的变量才可进行比较.
数值
整数
整数是没有小数部分的数字。之前使用过的 i32
类型,表示有符号的 32 位整数( i
是英文单词 integer 的首字母,与之相反的是 u
,代表无符号 unsigned
类型)。下表显示了 Rust 中的内置的整数类型:
长度 | 有符号类型 | 无符号类型 |
---|---|---|
8 位 | i8 |
u8 |
16 位 | i16 |
u16 |
32 位 | i32 |
u32 |
64 位 | i64 |
u64 |
128 位 | i128 |
u128 |
视架构而定 | isize |
usize |
在 rust 中, 整型默认是 i32 .
整形字面量可以用下表的形式书写:
数字字面量 | 示例 |
---|---|
十进制 | 98_222 |
十六进制 | 0xff |
八进制 | 0o77 |
二进制 | 0b1111_0000 |
字节 (仅限于 u8 ) |
b'A' |
这个设计感觉是为了可读性设计的, 诸如我们现在看大数字的三位一分隔:
即 1000000 -> 1_000_000 .
整数溢出 (?
原理在这里就不多写了, 毕竟笔者是做 pwn 的(
有趣的点是, 在 rust 的 debug 模式下编译时会检测整数溢出, 如果存在这个问题就不过编译 (这个感觉挺有趣的) , 似乎如果是不在 debug 下编译的话会按照补码循环的方式 (其实就基本上是 c 的整数溢出处理方式) 去处理溢出, 并不会 panic .
但是 rust 中也给了一些显式处理溢出的办法:
- 使用
wrapping_*
方法在所有模式下都按照补码循环溢出规则处理,例如wrapping_add
- 如果使用
checked_*
方法时发生溢出,则返回None
值 - 使用
overflowing_*
方法返回该值和一个指示是否存在溢出的布尔值 - 使用
saturating_*
方法,可以限定计算后的结果不超过目标类型的最大值或低于最小值.
自己演示了一下这几个方法:
1 |
|
上面几个例子里面后面的 ‘*’ 指的都是运算, 实际结果在 checked
这里会有一点区别, 会返回一个 Some(xxx)
, 除此之外写个代码调一下都比较好懂.
浮点数
这里看 rust 圣经的写法感觉 rust 还挺重视底层逻辑的(大概是这种想法
默认浮点数类型是 f64
.
在这里浮点数使用的时候会出现一些所谓 ‘陷阱’, 但是似乎就是二进制不能准确完美表达十进制数字所导致的一些陷阱, 于是结果就是在实际 rust 计算的时候, 0.1 + 0.2 != 0.3
的样子.
NaN
对于数学上未定义的结果,例如对负数取平方根 -42.1.sqrt()
,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN
(not a number)来处理这些情况。
所有跟 NaN
交互的操作,都会返回一个 NaN
,而且 NaN
不能用来比较.
这里还提到一个 is_nan()
的方法可以处理这种情况, 感觉就是返回 1 和 0 的区别, 有时间调一下(
运算
运算和已知的那些运算是几乎等同的, 包括数字运算以及位运算, 但是也要求同类型, 懒得多写了.
序列 (Range)
这个看样子是 rust 提供的一个简单的生成连续数值的方法, 方便应用于循环中. 举个例子:
1..5
生成不包括 5 的连续数字, 即 1-41..=5
则会包括 5 .
但是这种序列只允许应用于数字和字符中, 原因是它们可以连续, 同时编译器也可以进行检查.
字符 (char)
在 rust 中的字符这个概念很有意思, 这里的字符不仅仅是 ASCII
, 所有的 unicode
都可以当作字符, 包括但不限于中文汉字, 其他的神魔语言, 甚至是 ‘👴’.
正因为是 Unicode
的形式, rust 中的字符是占用 4 字节的.
但是字符只能用 ''
表示, ""
是表示字符串时使用的.
Others
rust 圣经中还有一个 bool 和一个单元类型, bool 不用多说.
单元类型的话笔者想摘抄一下圣经原文:
单元类型就是
()
,对,你没看错,就是()
,唯一的值也是()
,一些读者读到这里可能就不愿意了,你也太敷衍了吧,管这叫类型?只能说,再不起眼的东西,都有其用途,在目前为止的学习过程中,大家已经看到过很多次
fn main()
函数的使用吧?那么这个函数返回什么呢?没错,
main
函数就返回这个单元类型()
,你不能说main
函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function )
,顾名思义,无法收敛的函数。例如常见的
println!()
的返回值也是单元类型()
。再比如,你可以用
()
作为map
的值,表示我们不关注具体的值,只关注key
。 这种用法和 Go 语言的 struct{} 类似,可以作为一个值用来占位,但是完全不占用任何内存。似乎在 rust 中很看重 ‘内存’ 以及其管理之类的一些概念(
语句/表达式
这两个概念在 rust 中分的很明确, 看起来也是很重要的两个概念.
大概看上去的区别就是, 语句是不返回一个值的, 而表达式是一定会返回值的, 所以在 rust 中, 包括去调用一个函数, 包括写一个常数, 有返回的值就是个表达式.
在比如函数返回值的表达式写法上 (或者是别的神魔东西) 有个要素就是不能有分号, 如果加了分号就会从表达式变成一条语句, 而不会返回值.
最后, 如果一个表达式不返回任何值的话, 会隐式返回一个单元类型 ()
.
函数
已经 8 是很陌生的一个概念 🌶 .
1 |
|
就像这样, 在 rust 函数中, 函数名和变量名采用 snake_case
命名, 位置随意, 只要定义即可过编译的样子.
参数
在 rust 函数中的每一个参数都需要标注出它的具体类型, 箭头后面标注返回值类型, 8 然就会报错.
返回
正常来说 rust 函数的返回值是最后一条表达式的返回值, 同时也可以使用 return
提前返回.
特殊返回
比如无返回值 ‘()’ , 如上提到的, 如果函数不给返回值则会返回一个 ‘()’, 如果误写一个语句做函数结尾的话也会返回一个 ‘()’ , 这东西就是用来表示 ‘无返回值’ 这一概念的.
还有在上面摘过的 diverge function
这一概念, 原文的表述是永不返回的发散函数, 这是一个 ‘不会返回的函数’ 的概念, 和返回 ‘()’ 是完全不一样的.
0x04 所有权/借用
学到这里的时候稍微查了一下, 对 rust 的介绍就是一种重视安全性的语言, 所以在开始看这个 ‘所有权’ 的概念的时候就觉得这种机制应该十有八九就是对安全性的一些实现了.
所有权
引用一下原文, 顺便多了解一些计算机机制.
所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:
- 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
- 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
- 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查
其中 Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。
先来看看一段来自 C 语言的糟糕代码:
1 |
|
这段代码虽然可以编译通过,但是其实非常糟糕,变量 a
和 c
都是局部变量,函数结束后将局部变量 a
的地址返回,但局部变量 a
存在栈中,在离开作用域后,a
所申请的栈上内存都会被系统回收,从而造成了 悬空指针(Dangling Pointer)
的问题。这是一个非常典型的内存安全问题,虽然编译可以通过,但是运行的时候会出现错误, 很多编程语言都存在。
再来看变量 c
,c
的值是常量字符串,存储于常量区,可能这个函数我们只调用了一次,也可能我们不再会使用这个字符串,但 "xyz"
只有当整个程序结束后系统才能回收这片内存。
笔者看着真是相当亲切啊(x, 在原文中接下来介绍了一大段堆栈, 但是笔者是做 pwn 的, 不多写了…
所有权规则
接下来是有关所有权的规则:
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
作用域这一概念在 rust 中和别的语言看上去区别不大, 不多赘述.
这里还介绍了 String
这个类型, 这个类型是动态分配的字符串, 和诸如 ‘otto’ 这种字符串字面值不一样, 字面值即类型为 &str
的变量, 是不可变的, 而动态分配的一个目的就是为了可变.
所有权操作
先来看一段代码:
1 |
|
这段代码的操作涉及到的值是整型, 是存在栈上的, 在变量绑定时只需要值 copy 即可, 不需要所有权的转移.
但是如果涉及到了诸如 String
这种动态的, 存在堆上的类型, 就无法做到像上面那样的简单的值 copy . 在 rust 中, 这个复杂类型的存储是分为 ‘栈上的地址指针 (指向堆) + 长度 + 容量, 即类型本身’ 和 ‘堆上的真实数据’ 两者.
实际上这个类型只是三个指针的组合, 这里是清晰概念的说法(
例如:
1 |
|
这里如果为了性能考虑的话, 是不会采用值 copy 的形式将这一堆东西全都 copy 到 s2 的, 而是只 copy 了 String
类型的三个指针, 但是这样会不符合所有权规则, 堆上的那些数据就有了 s1 和 s2 两个所有者.
如果一个值拥有 2 个所有者, 那这种情况就和 heap overlapping 无限类似了, 这时候如果离开作用域就无异于 double free
. 于是 rust 选择在 s1 赋予 s2 后马上 drop 掉 s1 , 此时 s1 失效, 所有权转移到 s2 .
看上去感觉类似于指针这一概念的转移, 只不过用所有权这一概念感觉更有针对性, 强调一个变量是否存在动态分配要素.
引用/借用
引用和 c 的引用没什么区别, 一样是 let a = &b
的形式, 也要解引用使用.
同时在 rust 中, 引用也可以使用关键字 ref
.
引用在默认情况下是不可变的, 如果想要使用可变引用需要在声明的时候就加入 mut
, 同时引用的那个变量也需要是可变的, 可变引用是不可以引用不可变变量的.
同一作用域下, 特定的一个变量只能有一个可变引用:
这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:
- 两个或更多的指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
数据竞争会导致未定义行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
很多时候可以通过加大括号的方式限制变量的作用域以至于防止这个问题, 默认情况下, 引用的作用域持续到最后一次使用 (大概是这个样子) , 同时, (大概是) 同一变量的作用域下不能同时出现一个可变引用和不可变引用.
这里 rust 编译器还有一点优化, 对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域(}
)结束前就不再被使用的代码位置。
总的来说,借用规则如下:
- 同一时刻, 你只能拥有要么一个可变引用, 要么任意多个不可变引用
- 引用必须总是有效的.
这里原文还写出了 dangling 引用的概念, 由于 rust 编译器会处理这种潜在的 uaf , 所以不多赘述.
0x05 复合类型
字符串
切片
大概和其他语言的切片没什么区别, rust 中的切片是对集合的一个引用, 大概语法是 &xx[x..x]
, 在字符串这里, 切片就是对字符串一部分的引用.
在切片这里的索引应该是按照字节的形式来的.
字符串
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
Rust 在语言级别,只有一种字符串类型: str
,它通常是以引用类型出现 &str
,也就是上文提到的字符串切片。虽然语言级别只有上述的 str
类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String
类型。
str
类型是硬编码进可执行文件,也无法被修改,但是 String
则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码。
除了 String
类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsString
, OsStr
, CsString
和 CsStr
等,注意到这些名字都以 String
或者 Str
结尾了吗?它们分别对应的是具有所有权和被借用的变量。
元组
元组是一个可以包含各种类型值的组合。元组使用括号 ()
来构造(construct),而每个元组自身又是一个类型标记为 (T1, T2, ...)
的值,其中 T1
、T2
是每个元素的类型。函数可以使用元组来返回多个值,因为元组可以拥有任意多个值。
数组
数组就 a : [类型; size]
结构体
1 |
|
枚举
enum
关键字允许创建一个从数个不同取值中选其一的枚举类型(enumeration)。任何一个在 struct
中合法的取值在 enum
中也合法。
相较于结构体来说, 枚举可以不是同一个类型
从这里开始属于一个抓进度的状态, 速学了, 不写太多了(
Others
这里应该是一些琐碎的东西.
Option/Some
在 Rust 中,Option
是一个枚举类型,用于表示一个值可能存在或不存在的情况。它有两个变体:
Some(T)
:表示一个值存在,其中T
是值的类型。None
:表示值不存在。
0x06 类型转换
Rust 使用 trait 解决类型之间的转换问题。最一般的转换会用到 From
和 Into
两个 trait。
From / Into
1 |
|
两者用法如上, 大体基本相同, 只不过在用 Into 的时候要在绑定的时候指定类型:
1 |
|
类似于 From
和 Into
,TryFrom
和 TryInto
是类型转换的通用 trait。不同于 From
/Into
的是,TryFrom
和 TryInto
trait 用于易出错的转换,也正因如此,其返回值是 Result
型。
要把任何类型转换成 String
,只需要实现那个类型的 ToString
trait。然而不要直接这么做,您应该实现fmt::Display
trait,它会自动提供 ToString
.
rust-by-example 前面一段对 trait 的描写真多啊(
我们经常需要把字符串转成数字。完成这项工作的标准手段是用 parse
函数, 这里需要提供类型, 方法是提供标注或 parse::<xx>()
.
多在这里写一个 str 到 String 的转换:
String::from("hello,world")
"hello,world".to_string()
0x07 流程控制
if else
if
-else
分支判断和其他语言类似。不同的是,Rust 语言中的布尔判断条件不必使用小括号包裹,且每个条件后面都跟着一个代码块。if
-else
条件选择是一个表达式,并且所有分支都必须返回相同的类型。
无限循环 (loop)
使用 loop 构建一个无限循环, 可以通过 break 直接退出循环, 或是 continue 继续下一次.
嵌套处理
在处理嵌套循环的时候可以 break
或 continue
外层循环。在这类情形中,循环必须用一些 'label
(标签)来注明,并且标签必须传递给 break
/continue
语句. 如下:
1 |
|
loop
有个用途是尝试一个操作直到成功为止。若操作返回一个值,则可能需要将其传递给代码的其余部分:将该值放在 break
之后,它就会被 loop
表达式返回。
WHile
while
关键字可以用作当型循环(当条件满足时循环), 用法类似, 不多写.
For
for in
结构可以遍历一个 Iterator
(迭代器)。创建迭代器的一个最简单的方法是使用区间标记 a..b
。这会生成从 a
(包含此值) 到 b
(不含此值)的,步长为 1 的一系列值。
1 |
|
for in
结构能以几种方式与 Iterator
互动。在 迭代器 trait 一节将会谈到,如果没有特别指定,for
循环会对给出的集合应用 into_iter
函数,把它转换成一个迭代器。这并不是把集合变成迭代器的唯一方法,其他的方法有 iter
和iter_mut
函数。
这三个函数会以不同的方式返回集合中的数据。
iter
- 在每次迭代中借用集合中的一个元素。这样集合本身不会被改变,循环之后仍可以使用。into_iter
- 会消耗集合。在每次迭代中,集合中的数据本身会被提供。一旦集合被消耗了,之后就无法再使用了,因为它已经在循环中被 “移除”(move)了。iter_mut
- 可变地(mutably)借用集合中的每个元素,从而允许集合被就地修改。
match 匹配机制
Rust 通过 match
关键字来提供模式匹配,和 C 语言的 switch
用法类似。第一个匹配分支会被比对,并且所有可能的值都必须被覆盖, 这个机制也可以用于解构一些复杂类型变量, 诸如一个元组我只需要 match 第一个元素, 然后对后续元素进行操作的这种情况.
由于在这里需要覆盖所有可能的值, 所以有的时候可能某些语句会比较多余, 这时候 rust 也提供了 if let
和 while let
机制,
0x08 有关函数
方法
方法(method)是依附于对象的函数。这些方法通过关键字 self
来访问对象中的数据和其他。方法在 impl
代码块中定义。
闭包
Rust 中的闭包(closure),也叫做 lambda 表达式或者 lambda,是一类能够捕获周围作用域中变量的函数。例如,一个可以捕获 x 变量的闭包如下:
1 |
|
它们的语法和能力使它们在临时(on the fly)使用时相当方便。调用一个闭包和调用一个函数完全相同,不过调用闭包时,输入和返回类型两者都可以自动推导,而输入变量名必须指明。
其他的特点包括:
- 声明时使用
||
替代()
将输入参数括起来。 - 函数体定界符(
{}
)对于单个表达式是可选的,其他情况必须加上。 - 有能力捕获外部环境的变量。