裸指针的创建
在 Rust 中获取裸指针的方式,常用的有两种方法。
- 强制引用 (&T) 或可变引用 (&mut T)
let my_num: i32 = 10;
let my_num_ptr = &my_num as *const _;
let mut my_speed: i32 = 88;
let my_speed_ptr = &mut my_speed as *mut _;
- 消费 box (Box<T>)
let my_speed: Box<i32> = Box::new(88);
let my_speed: *mut i32 = Box::into_raw(my_speed);
// 拥有原始 Box<T> 的所有权,在使用后需要释放掉
unsafe {
drop(Box::from_raw(my_speed));
}
那么这两种获取的方式有什么区别吗?在官方文档中是这么描述的:
第 1 种方式:This does not take ownership of the original allocation and requires no resource management later, but you must not use the pointer after its lifetime.
第 2 中方式:The into_raw function consumes a box and returns the raw pointer. It doesn’t destroy T or deallocate any memory.
简单来说,使用第 1 种方式,不会获取数据的所有权,不能在他的生命周期之后使用。而使用第 2 种方式,将消费 box 并获取数据的所有权(作者自己加的),不会销毁数据及释放内存,需要使用者自己进行管理。
使用的区别
当大家看到上面的描述时候,不知道是否跟我一样一脸懵逼。如果不是,那么恭喜你,你肯定骨骼精奇,是万中无一的 Rust 奇才。
下面让我们用程序来实际验证下吧。
#[derive(Debug)]
struct Tmp {
n: i32,
}
impl Drop for Tmp {
fn drop(&mut self) {
println!("Dropping with data ({})!", self.n);
}
}
fn get_raw_point(elem: i32) -> *mut Tmp{
let mut t = Tmp { n: elem};
let t_r = &mut t as *mut _;
t_r
}
#[test]
fn test_raw_point() {
let mut p = get_raw_point(1);
p = get_raw_point(2);
unsafe {
(*p).n = 3;
println!("{:?},{:?}", *p, p);
}
}
上面这段程序中,我们定义了名为一个 Tmp 的结构体,并为其实现了 Drop trait。实现 Drop trait 的原因是我们想看下结构体何时被销毁。然后我们定义一个名为 get_raw_point 的函数,函数中,我们首先实例化了 Tmp 结构体,然后使用第 1 种方式获取其裸指针并返回。最后就是简单的 test 方法,调用了两次 get_raw_point 方法,然后通过裸指针修改 Tmp 实例中名为 n 的字段值,最后 print Tmp 实例。
大家可以猜想下,这段代码是否能正常运行?输出又是否符合预期呢?
running 1 test
Dropping with data (1)!
Dropping with data (2)!
Tmp { n: 3 },0x70000249e690
test tmp::test_raw_point ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 37 filtered out; finished in 0.00s
结果是程序可以正常运行,并且输出的结果完全符合预期。
但是我们应该发现中间的确夹杂了两行“Dropping with data XXX”,这说明 Tmp 实例的确是在调用完 get_raw_point 方法之后(离开其生命周期)就被销毁了,那为什么程序既没有 panic,输出还完全符合预期?这个我们暂时留在下面解释。让我们先看下使用第 2 种获取裸指针的方式,运行起来有什么区别。
#[derive(Debug)]
struct Tmp {
n: i32,
}
impl Drop for Tmp {
fn drop(&mut self) {
println!("Dropping with data ({})!", self.n);
}
}
fn get_raw_point(elem: i32) -> *mut Tmp{
let mut t = Tmp { n: elem};
// 修改使用 Box::into_raw 获取裸指针
let t_r = Box::into_raw(Box::new(t));
t_r
}
#[test]
fn test_raw_point() {
let mut p = get_raw_point(1);
p = get_raw_point(2);
unsafe {
(*p).n = 3;
println!("{:?},{:?}", *p, p);
}
}
很简单,只需要修改 get_raw_point 方法中的一行代码就可以了,我们再看下运行结果。
running 1 test
Tmp { n: 3 },0x7f77ba70acd0
test tmp::test_raw_point ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 37 filtered out; finished in 0.00s
结果的 print 跟上面是一致的,但是少了“Dropping with data XXX”的输出,官方文档诚不欺我。
但是,但是,为什么第一种方式的 print 结果也完全没问题呢?这个问题我找了半天暂时没有找到特别权威的解答,于是我问了下 ChatGPT,给出了如下解答:
在某些情况下,虽然这是未定义行为,程序似乎还是打印出了看起来是“正确”的信息。这是因为:
- 时间紧密:在 get_raw_point 函数返回后内存立即被访问,实际内存还没有被操作系统或其他部分程序回收或覆盖。
- 代码未优化:编译器没有足够的优化,以至于内存在栈上的布局实际上没有立即改变。
即使如此,这完全属于偶然现象,是未定义行为的典型案例。
未定义行为意味着编译器不保证程序的任何特定行为。程序可能:
- 崩溃或给出错误的输出。
- 看起来正常运行并出现正确的结果。
- 在一些执行环境中正常工作,在其他环境中失败。
- 造成安全风险、数据破坏或其他可能的负面结果。
在 Rust 中进行安全编程意味着要避免未定义行为。即使在您当前的测试案例中程序表现正常,这段代码在生产环境中、在不同的操作系统上、或在经过不同等级优化的编译器手中都可能导致严重错误。因此,不应依赖看似“正常”的未定义行为,应该修复代码来确保所有行为都是定义明确的。
看起来好像蛮有道理的,于是我在使用裸指针之前加了“thread::sleep(Duration::from_secs(30));”这段代码,想验证下它说的第一种原因,但是程序还是正常输出了。第二种原因我暂时不知道如何去验证。这里就留待后面来填坑吧。
Undefined Behavior
但是 GBT 说的“Rust 中进行安全编程意味着要避免未定义行为”,这个肯定是对的,也完全符合官网文档中的描述。我们的确是在实例的生命周期结束之后还使用了其裸指针,只是恰好程序输出没问题而已。
那么什么是“未定义行为”呢,它的英文是“Undefined Behavior”,这是英文文档,这是中文文档。简单来说,出现这个那么此代码被认为不正确。
那么有什么办法来检测“Undefined Behavior”吗,这时候就要有请 Miri 出马。安装啥的自己搜索吧,我们直接来使用。我们还是来运行使用第 1 种获取裸指针方式的测试代码。
cargo +nightly miri test -- --show-output test_raw_point
running 1 test
test tmp::test_raw_point ... error: Undefined Behavior: out-of-bounds pointer arithmetic: alloc102330 has been freed, so this pointer is dangling
--> src/tmp/mod.rs:34:9
|
34 | (*p).n = 3;
| ^^^^^^^^^^ out-of-bounds pointer arithmetic: alloc102330 has been freed, so this pointer is dangling
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc102330 was allocated here:
--> src/tmp/mod.rs:20:9
|
20 | let mut t = Tmp { n: elem};
| ^^^^^
help: alloc102330 was deallocated here:
--> src/tmp/mod.rs:26:1
|
26 | }
| ^
= note: BACKTRACE (of the first span) on thread `tmp::test_raw_point`:
= note: inside `tmp::test_raw_point` at src/tmp/mod.rs:34:9: 34:19
note: inside closure
--> src/tmp/mod.rs:29:20
|
28 | #[test]
| ------- in this procedural macro expansion
29 | fn test_raw_point() {
| ^
= note: this error originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 112 warnings emitted
error: test failed, to rerun pass `--bin hello_cargo`
Caused by:
process didn't exit successfully: `/Users/yuman/.rustup/toolchains/nightly-x86_64-apple-darwin/bin/cargo-miri runner /Users/yuman/rust-workspace/hello_cargo/target/miri/x86_64-apple-darwin/debug/deps/hello_cargo-321ab85bbf5b6b94 --show-output test_raw_point` (exit status: 1)
note: test exited abnormally; to see the full output pass --nocapture to the harness.
可以看到的确是出问题了,“ Undefined Behavior: out-of-bounds pointer arithmetic: alloc102330 has been freed, so this pointer is dangling”,意思是越界的指针使用,由于内存已经被释放了,这个指针是一个悬垂指针。并且还给了很明确的错误过程,Miri,牛!
然后我们再来测试使用第 2 种获取裸指针方式的代码,这个需要稍微修改下测试函数。这是因为使用 Box::into_raw 这种方式,将获取数据的所有权,需要在使用之后由使用方主动释放,在官方文档中也有明确说明。
#[derive(Debug)]
struct Tmp {
n: i32,
}
impl Drop for Tmp {
fn drop(&mut self) {
println!("Dropping with data ({})!", self.n);
}
}
fn get_raw_point(elem: i32) -> *mut Tmp{
let mut t = Tmp { n: elem};
let t_r = Box::into_raw(Box::new(t));
t_r
}
#[test]
fn test_raw_point() {
// 修改为只获取一次
let mut p = get_raw_point(1);
unsafe {
(*p).n = 3;
println!("{:?},{:?}", *p, p);
// 修改主动释放
Box::from_raw(p);
}
}
然后继续使用 Miri test
cargo +nightly miri test -- --show-output test_raw_point
running 1 test
test tmp::test_raw_point ... ok
successes:
---- tmp::test_raw_point stdout ----
Tmp { n: 3 },0x23ec88
Dropping with data (3)!
successes:
tmp::test_raw_point
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 36 filtered out; finished in 0.38s
可以看到测试运行通过,print 输出也完全符合预期,并且还输出了“Dropping with data XXX”,证明 Tmp 实例也被销毁。
到这里我们应该可以知道,Rust 中表面运行没有问题的程序,不一定没问题。
问题复现
那有没有办法让使用第 1 种获取裸指针方式的程序输出不要符合预期呢?有办法的,下面是我用来测试的程序。
#[derive(Debug)]
struct Tmp {
n: i32,
}
impl Drop for Tmp {
fn drop(&mut self) {
println!("Dropping with data ({})!", self.n);
}
}
struct Tp {
p: *mut Tmp,
}
impl Tp {
fn get_raw_point(&mut self, elem: i32) {
let mut t = Tmp { n: elem};
let t_r = &mut t as *mut _;
self.p = t_r;
}
}
#[test]
fn test_raw_point_2() {
let mut tp = Tp{p : ptr::null_mut()};
tp.get_raw_point(1);
tp.get_raw_point(2);
unsafe {
(*tp.p).n = 3;
println!("{:?},{:?}", *tp.p, tp.p);
}
}
这里新增了一个结构体 Tp,里面的只有一个字段 p 是 Tmp 结构体的裸指针。Tp 中的 get_raw_point 方法基本与之前的定义一致。然后不要使用 Miri 来运行 test_raw_point_2 测试方法。
running 1 test
test tmp::test_raw_point_2 ... ok
successes:
---- tmp::test_raw_point_2 stdout ----
Dropping with data (1)!
Dropping with data (2)!
Tmp { n: 0 },0x70000768e664
successes:
tmp::test_raw_point_2
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 37 filtered out; finished in 0.00s
可以看到这次的 print 输出就不符合预期了。至于是什么原因?我暂时也还没搞明白 ̄□ ̄||
其他
当使用 Box::from_raw 来释放通过第 1 种方式获取的裸指针会发生什么呢?让我们来运行下面的代码
#[derive(Debug)]
struct Tmp {
n: i32,
}
impl Drop for Tmp {
fn drop(&mut self) {
println!("Dropping with data ({})!", self.n);
}
}
fn get_raw_point(elem: i32) -> *mut Tmp{
let mut t = Tmp { n: elem};
let t_r = &mut t as *mut _;
t_r
}
#[test]
fn test_raw_point() {
let mut p = get_raw_point(1);
unsafe {
(*p).n = 3;
println!("{:?},{:?}", *p, p);
Box::from_raw(p);
}
}
running 1 test
hello_cargo-acd45817435c4902(64460,0x700005d1d000) malloc: *** error for object 0x700005d1c680: pointer being freed was not allocated
hello_cargo-acd45817435c4902(64460,0x700005d1d000) malloc: *** set a breakpoint in malloc_error_break to debug
error: test failed, to rerun pass `--bin hello_cargo`
Caused by:
process didn't exit successfully: `/Users/yuman/rust-workspace/hello_cargo/target/debug/deps/hello_cargo-acd45817435c4902 --show-output test_raw_point` (signal: 6, SIGABRT: process abort signal)
可以看到,使用这种方式将会直接 panic,因为我们释放了一个未被分配的指针。
以上就是我在学习 Rust 裸指针这里的一些思考,如有纰漏欢迎指正
Primitive Type pointerCopy item path
Behavior considered undefined
网友评论