Rust-悬垂指针

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容