likes
comments
collection
share

Rust-悬垂指针问题总结裸指针的创建 在 Rust 中获取裸指针的方式,常用的有两种方法。 强制引用 (&T) 或可变

作者站长头像
站长
· 阅读数 10

Rust-悬垂指针问题总结裸指针的创建 在 Rust 中获取裸指针的方式,常用的有两种方法。 强制引用 (&T) 或可变

裸指针的创建

在 Rust 中获取裸指针的方式,常用的有两种方法

  1. 强制引用 (&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 _;	
  1. 消费 box (Box)
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

转载自:https://juejin.cn/post/7368823201067253810
评论
请登录