美文网首页
Rust-悬垂指针

Rust-悬垂指针

作者: 鱼蛮子9527 | 来源:发表于2024-04-23 16:24 被阅读0次
    rust-social-wide.jpg

    裸指针的创建

    在 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<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

    相关文章

      网友评论

          本文标题:Rust-悬垂指针

          本文链接:https://www.haomeiwen.com/subject/fqrixjtx.html