Rust与算法基础(4++):你以为的安全真的安全吗?
我们用了一个我们认为安全的复制方法,绕过Copy Trait实现了向量内元素的移动。然而它真的安全吗?
在插入排序的单元测试里加入这么一个用例:
#[test]
fn it_struct_sort_ascending_equal_box() {
#[derive(Debug, PartialEq)]
struct Foo {
id: u32,
name: &'static str,
}
let mut v = vec![
Box::new(Foo {
id: 22,
name: "ZS",
}),
Box::new(Foo {
id: 43,
name: "LS",
}),
Box::new(Foo {
id: 145,
name: "WW",
}),
Box::new(Foo {
id: 1,
name: "ZL",
}),
Box::new(Foo {
id: 9,
name: "SQ",
}),
Box::new(Foo {
id: 43,
name: "LS2",
})
];
InsertionSorter(&mut v).sort_by(|prev, next| prev.id <= next.id);
assert_eq!(
v,
vec![
Box::new(Foo {
id: 1,
name: "ZL",
}),
Box::new(Foo {
id: 9,
name: "SQ",
}),
Box::new(Foo {
id: 22,
name: "ZS",
}),
Box::new(Foo {
id: 43,
name: "LS",
}),
Box::new(Foo {
id: 43,
name: "LS2",
}),
Box::new(Foo {
id: 145,
name: "WW",
})
]
);
}
执行发现报错了,它甚至无法正常输出错误信息,断点调试直接跳到汇编去了。我也不是很懂汇编,那就在报错前看看调试器监测的数据吧。
可以看到在函数执行到
vec[j] = e;
这一句之后,e
和vec[1]
都变成了错误的数据(一个很大的id),此时e仍然能被监测到,到下一个循环时e按理说就被释放掉了。 而到下一次vec[j] = e
时崩溃了。
我们现在把vec[j] = e
换成下面的,再调试一下:
ptr::copy(&e, &mut vec[j], 1);
这次调试的运行情况比之前有一些细微的变化。
这一句之后,e
和vec[1]
还是正确的数,而到结束这个循环到下一个循环时,e
不再被监测得到,同时vec[1]
的值才在这时候变成了错误的值。 而且看e
的类型,是个Foo *
,也就是Box<Foo>
,可以猜测,是在e脱离作用域后,被释放掉了,这不是浅层地释放掉Box指针而已, 而是层层调用释放函数,将资源释放掉。
e
是被拷贝出来的,所有权也在本函数内,即使再放回(以copy的方式)原位,数组没有变化,也会因为e
和vec[1]
两个指针指向同一个数据,e
被释放掉导致vec[1]
悬垂了。
再对比之前的运行,执行vec[j] = e
之后,e的所有权被移交出去了,而在所有权移交的同时,数据发生了改变。
第一个循环,vec[1] --> e vec[0] < e,e维持原位,重新赋值回vec[1] vec[1]数据出错(被释放了,并强行读取了数据),id是一个很大的数
注意e
是拷贝出来的,也就是vec[1]
也是一个有所有权的数据。e
移动到vec[1]
,就意味着vec[1]
原本存放的数据没人要了(显然也没有任何办法可以索引到了),没人要的数据会立即结束声明周期,就地释放。
而又因为被释放的数据和e
以及vec[1]
是同一个指针,导致e
和vec[1]
都悬垂了。到了下一个循环时,当被复制出来的数据要再次替换vec某个位置的数据时, 这个数据再次被替换了出去,触发了已释放的空间重复释放,导致程序崩溃。
第二个循环,vec[2] --> e vec[1] > e,vec[1] --> vec[2] vec[0] < e, 尝试e --> vec[1] vec[1]已经被释放,再次释放,程序崩溃
这里还有一个需要注意的点,就是移动的意义。可以看出移动也是复制,e
和vec[1]
同时悬垂也意味着两者存的是同一个指针。 那么这种移动在一般的安全实现中,移出方结束生命周期,为什么接受方并没有悬垂呢?这正是因为移出的数据已经标记为不拥有所有权, 所以这个变量在结束生命周期时,不会执行释放,仅销毁变量本身所保存的值(比如指针)。
那么e是被强制复制出来的,现在同时存在两个指向同一个资源的指针,我们要做的就是防止其中一个指针在声明周期结束时释放资源。 也就是说,手动把一个变量标记为“已移出”。正好,Rust的mem::forget()
就是干这个的。
所以在
ptr::copy(&e, &mut vec[j], 1);
的后面,加上
mem::forget(e);
这个问题就解决了。
应当注意的是,mem::forget()
并不是一个unsafe函数。因为Rust的安全机制并不保证资源的释放, 所以即使程序只写安全代码也会造成内存泄漏。Rust的安全机制是保证只要不显式地使用forget
、 drop
等等手动选择释放或者不释放资源的操作, 一般的安全行为不会隐式地制造内存泄漏、指针悬垂、重复释放等内存安全问题。
copy
再forget
的写法还有一个更优雅的解法。注意这个e是用ptr::read()
拷贝出来的, 这个函数是不安全的,因为拷贝出来之后原来保存的指针还在。 而在一系列移位之后,原来保存的指针被覆盖掉了,这时候的e又回到正常的单指针状态了,是可以自然释放的。
对应前面的read
,这里就把刚才的copy
再forget
写法改成ptr::write()
:
ptr::write(&mut vec[j], e);
write
与copy
不同的是,copy
的src参数是一个raw pointer,不涉及所有权。 而write
函数对src
是夺取所有权的,将e代入src,e会被吃掉。 这个Box指针会被重新写入数组,只要算法本身没实现错,这个Box指针就仍是唯一的, e也因为被夺取所有权,不会被重复释放。