参考:https://kaisery.github.io/trpl-zh-cn/ch19-01-unsafe-rust.html
理解 unsafe
Rust 在编译时会强制执行的内存安全保证。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rust (unsafe Rust )。它与常规 Rust 代码无异,但是会提供额外的超级力量:
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问
union
的字段
使用 unsafe
注意事项:
unsafe
并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe
关键字只是提供了那五个不会被编译器检查 内存安全的功能。你仍然能在不安全块中获得某种程度的安全。unsafe
不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保unsafe
块中的代码以有效的方式访问内存。- 通过要求这五类操作必须位于标记为
unsafe
的块中,就能够知道任何与内存安全相关的错误必定位于unsafe
块内。保持unsafe
块尽可能小,如此当之后调查内存 bug 时就会感谢你自己了。 - 为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 。标准库的一部分被实现为在被评审过的不安全代码之上的安全抽象。这个技术防止了
unsafe
泄露到所有你或者用户希望使用由unsafe
代码实现的功能的地方,因为使用其安全抽象是安全的。
使用 unsafe Rust 的原因:
- 使用以上 5 种“力量”。因为 静态分析 本质上是保守的当编译器尝试确定一段代码是否支持某个保证时,拒绝一些有效的程序比接受无效程序要好一些。这必然意味着有时代码可能是合法的,但是 Rust 不这么认为。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。”这么做的缺点就是你只能靠自己了:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。
- 底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互,甚至于编写你自己的操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。
程序静态分析(Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。
1. 解引用裸指针
不安全 Rust 有两个被称为 裸指针 (raw pointers )的类似于引用的新类型。裸指针:这里的星号不是解引用运算符;它是类型名称的一部分
- 不可变裸指针:
*const T
- 可变裸指针:
*mut T
,不可变意味着指针解引用之后不能直接赋值。
裸指针注意事项:let mut num = 5;
// 可以在安全代码中 创建 裸指针
// 只是不能在安全代码中 解引用 裸指针
// 这里使用 as 将不可变和可变引用强转为对应的裸指针类型
// 因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 必须使用 unsafe 块 对裸指针解引用,无论是读取还是修改
unsafe {
// r1 不可变裸指针,r2 可变裸指针,它们同时指向相同的内存位置
// 这在 safe 代码中是不被通过编译的,在 unsafe 块中,编译器不会检查内存安全
// 若通过可变指针修改数据,则可能潜在造成数据竞争
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
// 但是不能认为任何裸指针总是有效的
// 这是一个不能确定其有效性的裸指针
let address = 0x012345usize;
let r = address as *const i32;
- 创建一个指针不会造成任何危险,所以可以在 safe 代码中创建裸指针;只有当访问其指向的值时才有可能遇到无效的值,因此必须在 unsafe 块中 解引用 裸指针。
- 尝试使用任意内存是 未定义行为 (undefined behavior):此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)
- 既然存在这么多的危险,为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口。
与引用和智能指针的区别在于,裸指针:
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不适用。
2. 调用不安全函数或方法
标准库中的安全函数 [split_at_mut](https://doc.rust-lang.org/src/core/slice/mod.rs.html#1515-1520)
为例:它获取一个 slice 并从给定的索引参数开始将其分为两个 slice。
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
出于简单考虑,我们将 split_at_mut
实现为函数而不是方法,并只处理 i32
值而非泛型 T
的 slice。这个函数无法只通过安全 Rust 实现:
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
// 该断言意味着如果传入的索引比要分割的 slice 的索引更大,此函数在尝试使用这个索引前 panic
assert!(mid <= len);
// 编译器会报错:两次可变借用 slice
(&mut slice[..mid], &mut slice[mid..])
}
本质上借用 slice 的不同部分是可以的,因为结果两个 slice 不会重叠,不过 Rust 还没有智能到能够理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了。
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
// mid 必然小于等于 len 的断言可以保证执行 unsafe 代码前所有的裸指针将是有效的 slice 中数据的指针
assert!(mid <= len);
unsafe {
// slice::from_raw_parts_mut 函数获取一个裸指针和一个长度来创建一个 slice
(slice::from_raw_parts_mut(ptr, mid),
// ptr 上调用 add 方法并使用 mid 作为参数来获取一个从 mid 开始的裸指针
slice::from_raw_parts_mut(ptr.add(mid), len - mid))
}
}
slice::from_raw_parts_mut
函数是不安全的因为它必须确信这个裸指针是有效的。比如这个调用就是未定义行为:
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let slice: &[i32] = unsafe {
// 我们并不拥有这个任意地址的内存,也不能保证这段代码创建的 slice 包含有效的 i32 值
slice::from_raw_parts_mut(r, 10000)
};
裸指针上的 add
方法也是不安全的,因为其必须确信此地址偏移量也是有效的指针。
因此必须将 slice::from_raw_parts_mut
和 add
放入 unsafe
块中以便能调用它们。
关于函数与 unsafe 块:
使用 unsafe 声明函数是不安全的,并且在 unsafe 块中使用 不安全的函数。
unsafe fn dangerous() {}
unsafe {
dangerous();
}
不安全函数体也是有效的
unsafe
块,所以在不安全函数中进行另一个不安全操作时无需新增额外的unsafe
块。
比如上面的slice::from_raw_parts_mut(ptr.add(mid), len - mid))
已经在 unsafe 块中, 无需再把它放在一个 unsafe 块里面。- 仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。事实上,将不安全代码封装进安全函数是一个常见的抽象。
split_at_mut
是一个被封装好的安全函数,无需把它标记成 unsafe,即可以在 unsafe 块之外使用,且被编译器检查内存安全。
extern
关键字:reference: external-blocks、reference: extern-function-qualifier
- 帮助创建和使用 外部函数接口 (Foreign Function Interface , FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
extern
的使用无需unsafe
,因为extern
块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任。extern "C" {
// Rust 调用的 C 语言中的外部函数的签名和名称
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
也可以使用
extern
来创建一个允许其他语言调用 Rust 函数的接口:在fn
关键字之前增加extern
关键字并指定所用到的 ABI,并且需增加#[no_mangle]
注解来告诉 Rust 编译器不要 mangle 此函数的名称。// 一个供 C 语言调用 Rust 的 ABI
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
Mangling :编译器会将我们指定的函数名修改为不同的名称,而且增加用于其他编译过程的额外信息,这导致变更后的函数名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling,让我们定义的函数名可以直接被其他语言所用。
ABI (应用二进制接口)参考资料:
- https://doc.rust-lang.org/nightly/reference/abi.html](f60315636859a8682006dd92f09a451d))
- wikipedia: Application Binary Interface (ABI)
- wikipedia: Application Programming Interface (API)
- ABI vs. API
3. 访问或修改可变静态变量
静态(static)变量:
- Rust 中的全局变量。对于 Rust 的所有权规则来说是有问题的:如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
- 静态变量也分为 可变和不可变,它们只能储存拥有
'static
生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。 创建不可变静态变量:
static SCREAMING_SNAKE_CASE: type_anonotation = value
。 通常静态变量的名称采用SCREAMING_SNAKE_CASE
写法,并 必须 标注变量的类型。
创建可变静态变量:static mut SCREAMING_SNAKE_CASE: type_anonotation = value
static HELLO_WORLD: &str = "Hello, world!"; // 这里的 &str 等价于 &'static str
fn main() {
println!("name is: {}", HELLO_WORLD);
}
安全性:
- 访问不可变静态变量是安全的
- 访问和修改可变静态变量都是 不安全 的
// 全局变量是在在所有其他作用域之外声明的
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用第十六章讨论的并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。
Rust 有两种常量,可以在任意作用域声明,包括全局作用域。它们都需要显式的类型声明:
const
:不可改变的值(通常使用这种)。static
:具有['static](e07616f63a6339701a765c467d72ac7b)
生命周期的,可以是可变的变量(译注:须使用static mut
关键字)。 |
| 不可变变量 | 常量 | 静态变量 | | —- | —- | —- | —- | | 声明方式 |let snake_case = value;
|const SCREAMING_SNAKE_CASE: type_anonotation = value
|static SCREAMING_SNAKE_CASE: type_anonotation = value
static mut SCREAMING_SNAKE_CASE: type_anonotation = value
| | 可变操作 |let mut snake_case = value;
| 不能改变值 |static mut ...
且在unsafe
块中读取和改变 可变 静态变量的值 | | 使用范围 | local (局部作用域) | global (全局作用域) | global (全局作用域) | | 内存地址 | / | 不固定:允许在任何被用到的时候复制其数据 | 不可变静态变量值的内存地址是固定的;
使用这个值总是会访问相同的地址。 |
此外:str
(字面值)可以不经改动就被赋给一个 static
变量,因为它 的类型标记:&'static str
就包含了所要求的生命周期 'static
。其他的引用类型都 必须特地声明,使之拥有'static
生命周期。
// 全局变量是在在所有其他作用域之外声明的。
static LANGUAGE: &'static str = "Rust"; // 使用 &str 类型标注也可以
const THRESHOLD: i32 = 10;
fn is_big(n: i32) -> bool {
// 在一般函数中访问常量
n > THRESHOLD
}
fn main() {
let n = 16; // n: i32
// 在 main 函数(主函数)中访问常量
println!("This is {}", LANGUAGE);
println!("The threshold is {}", THRESHOLD);
println!("{} is {}", n, if is_big(n) { "big" } else { "small" });
}
'static
生命周期是可能的生命周期中最长的,它会在整个程序运行的时期中 存在。'static
生命周期也可被强制转换成一个更短的生命周期。有两种方式使变量 拥有 'static
生命周期,它们都把数据保存在可执行文件的只读内存区:
- 使用
static
声明来产生常量(constant)。 - 产生一个拥有
&'static str
类型的string
字面量。
例子见:https://rustwiki.org/zh-CN/rust-by-example/scope/lifetime/static_lifetime.html
4. 实现不安全 trait
不安全的 trait:至少一个方法中包含编译器不能验证的不变量。
在 trait
之前增加 unsafe
关键字将 trait 声明为 unsafe
,同时 trait 的实现也必须标记为 unsafe
:
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
例子:如果实现了一个包含一些不是 Send
或 Sync
的类型,比如裸指针,并希望将此类型标记为 Send
或 Sync
,则必须使用 unsafe
。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 unsafe
表明。
5. 访问联合体中的字段
union
和 struct
类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看参考文档了解有关联合体的更多信息。
何时使用不安全代码
使用 unsafe
来进行这五个操作(超级力量)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 unsafe
代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 unsafe
代码时,是可以这么做的,通过使用显式的 unsafe
标注使得在出现错误时易于追踪问题的源头。