在Rust中,不安全意味着什么?

哈Ha! 我向您介绍“ Rust的不安全之处是什么?”一文的翻译。 作者Nora Codes。


对于unsafe关键字对Rust语言的有用性和正确性以及将其推广为“安全系统编程语言”的含义,我已经看到了许多误解。 不幸的是,真相比短时间内的描述要复杂得多。 我就是这样看她的。


通常, unsafe关键字不会关闭保持Rust代码正确的类型系统 。 这仅使得可以使用某些“超能力”,例如取消引用指针。 unsafe用于基于根本上不安全的世界来实现安全的抽象,以便大多数Rust代码可以使用这些抽象并避免不安全的内存访问。


安全保障


Rust将安全性作为其核心原则之一。 可以说,这就是语言存在意义 。 但是,它在程序执行期间和使用垃圾收集器时没有提供传统意义上的安全性。 相反,Rust使用非常高级的类型系统来跟踪何时以及可以访问哪些值。 然后,编译器会静态分析每个Rust程序,以确保它始终处于正确的状态。


Python安全性


让我们以Python为例。 纯Python代码不能破坏内存。 访问列表项会检查是否超出边界; 计算函数返回的链接以避免出现悬挂的链接; 无法对指针进行任意算术。


这有两个后果。 首先,许多类型必须是“特殊的”。 例如,不可能在纯Python中实现有效的列表或字典。 相反,CPython解释器具有其内部实现。 其次,访问外部函数(在Python中未实现的函数)(称为外部函数的接口)要求使用特殊的ctypes模块,并且违反了语言安全性保证。


从某种意义上讲,这意味着用Python编写的所有内容都不能保证对内存的安全访问。


Rust的安全性


Rust还提供了安全性,但不是提供C语言中的不安全结构,而是提供了一个技巧:unsafe关键字。 这意味着Rust中的基本数据结构,例如Vec,VecDeque,BTreeMap和String,都在Rust中实现。


您可能会问:“但是,如果Rust提供了一个针对其代码安全性保证的技巧,并且标准库是使用此技巧实现的,那么Rust中的所有内容都不会被视为不安全吗?”


简而言之,亲爱的读者, 是的 ,完全是Python中的方式。 让我们更详细地看一下。


安全防锈中禁止使用什么?


Rust的安全性定义明确:我们对此进行了很多思考。 简而言之,安全的Rust程序不能:


  • 取消引用指向与编译器所知不同的类型的指针 。 这意味着没有指向null的指针(因为它们没有指向任何地方),没有超出范围的错误和/或分段错误(分段错误),没有缓冲区溢出。 但这也意味着释放内存或重新释放内存后没有任何用处(因为释放内存被认为是对指针的反引用),并且没有双关语
  • 对一个对象有多个可变的引用,或者对一个对象同时具有可变和不可变的引用 。 也就是说,如果您对一个对象有可变的引用,则只能拥有它;如果您对该对象有不可变的引用,则只有保留它,它才会改变。 这意味着您不能在Safe Rust中强制进行数据争用,这是大多数其他安全语言无法提供的保证。

Rust在类型系统中或使用代数数据类型对信息进行编码,例如Option表示值的存在/不存在,Result <T,E>表示错误/成功,或引用及其生存期 ,例如&T vs&mut T表示一个普通的(不可变的)链接和一个排他的(可变的)链接以及&'a T vs&'b T来区分在不同上下文中正确的链接(由于编译器足够聪明,可以自己找出来,所以通常将其省略) 。


例子


例如,以下代码将不编译,因为它包含一个悬挂的链接。 更具体地说, my_struct的寿命不足 。 换句话说,该函数将返回一个不再存在的链接,因此编译器无法(实际上甚至不知道如何)对其进行编译。


fn dangling_reference(v: &u64) -> &MyStruct { //     MyStruct   ,  v,   . let my_struct = MyStruct { value: v }; //      my_struct. return &my_struct; //  - my_struct  (  ). } 

此代码执行相同的操作,但是它尝试通过将值放在堆上来解决此问题(Box是Rust中基本智能指针的名称)。


 fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v }; //    Box         . let my_box = Box::new(my_struct); //      my_box. return &my_box; // my_box   .   "" my_struct       - , //    - MyStruct  . } 

Box本身会返回正确的代码,而不是对其进行引用。 这将所有权的转移(释放内存的责任)编码为函数的签名。 查看签名时,很明显,调用代码负责Box的操作,实际上,编译器会自动对其进行处理。

 fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //    my_box  . return my_box; //    .         , //    ;       //  Box<MyStruct>       ,      . } 

安全的Rust禁止某些坏事。 例如,从编译器的角度来看是允许的:
  • 导致程序死锁
  • 泄漏任意数量的内存
  • 无法关闭文件句柄,数据库连接或导弹井盖


Rust生态系统的优势在于,许多项目选择使用类型系统来确保代码尽可能准确,但是除非提供了安全的内存访问,否则编译器不需要这种强制性。

不安全的Rust允许使用什么?


不安全的Rust代码是带有unsafe关键字的Rust代码。 不安全的可以应用于函数或代码块。 当应用于函数时,它表示“此函数要求被调用的代码手动提供通常由编译器提供的不变式”。 当应用于代码块时,它的意思是“该代码块手动提供了防止对内存进行不安全访问所需的不变性,因此可以执行不安全的操作。”


换句话说,该函数的不安全含义是“您需要检查所有内容”,而在代码块上则是“我已经检查了所有内容”。


The Rust Programming Language中所述,标记为unsafe关键字的块中的代码可以:


  • 解引用指针。 这是关键的“超能力”,它使您可以实现双向链表,哈希图和其他基本数据结构。
  • 调用不安全的函数或方法。 有关此的更多信息。
  • 访问或修改可变的静态变量。 范围不受控制的静态变量无法进行静态检查,因此使用它们是不安全的。
  • 实施不安全特质。 不安全的特征用于标记特定类型是否保证某些不变性。 例如,“发送和同步”确定一种类型是可以在线程边界之间发送还是可以由多个线程同时使用。

还记得上面那些悬挂的指针吗? 加上不安全一词,编译器会发誓要翻倍,因为他不喜欢在不需要的地方使用不安全。


而是使用unsafe关键字基于任意指针操作来实现安全抽象。 例如,Vec类型使用不安全实现,但是使用它是安全的,因为它检查访问元素的尝试并且不允许溢出。 尽管它提供了类似set_len的操作,这可能会导致不安全的内存访问,但它们被标记为不安全。


例如,我们可以做与no_dangling_reference示例中相同的操作,但是会不合理地使用unsafe:


 fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //  Box    . let struct_pointer = Box::into_raw(my_box); return struct_pointer; //   ;     . // MyStruct     . } 

注意缺少不安全一词。 创建指针是绝对安全的。 如前所述,这存在内存泄漏的风险,但仅此而已,内存泄漏是安全的。 调用此功能也是安全的。 仅当某些东西试图取消引用指针时才需要unsafe。 另外,取消引用会自动释放分配的内存。


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" println!("Value: {}", my_boxed_struct.value); // my_boxed_struct    .       ,  //    - MyStruct } 

经过优化后,此代码等效于简单地返回Box。 Box是安全的基于指针的抽象,因为它阻止了指针在各处的分布。 例如,main的下一个版本将导致双重可用内存(double-free)。


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) }; // DOUBLE FREE BUG! let my_boxed_struct_2 = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" . println!("Value: {}", my_boxed_struct_1.value); println!("Value: {}", my_boxed_struct_2.value); // my_boxed_struct_2    .     ,  //    - MyStruct. //  my_boxed_struct_1    .      , //      - MyStruct.  double-free bug. } 

那么什么是安全抽象?


安全抽象是使用类型系统提供不能用于违反上述安全保证的API的抽象。 如上所示,Box更安全* mut T,因为它不会导致双重内存释放。


另一个示例是Rust中的Rc类型。 这是一个引用计数指针-对堆上数据的不可更改的引用。 由于它允许同时访问一个内存区域,因此必须防止更改,才能被认为是安全的。


除此之外,它也不是线程安全的。 如果需要线程安全,则必须使用Arc类型(Atomic Reference Counting),由于使用原子值进行链接计数并防止在多线程环境中发生数据争用,因此会导致性能下降。


编译器不允许在应该使用Arc的地方使用Rc,因为像Rc这样的创建者并未将其标记为线程安全的。 如果他们这样做,那将是不合理的:对安全的虚假承诺。


什么时候需要不安全的Rust?


当必须执行违反上述两个规则之一的操作时,始终需要不安全的Rust。 例如,在一个双向链接列表中,缺少指向同一数据(下一个元素和上一个元素)的可变链接完全剥夺了它的好处。 使用不安全时,双链列表实现者可以使用* mut Node指针编写代码,然后将其封装为安全的抽象。

另一个例子是使用嵌入式系统。 微控制器通常使用一组寄存器,其值由设备的物理状态决定。 当您从这样的寄存器中获取和更改u8时,世界无法停止,因此与设备支持板条箱配合使用是不安全的。 通常,此类包装箱将状态封装在透明的安全包装器中,该包装器会在可能的情况下复制数据,或使用其他提供编译器保证的技术。


有时有必要执行可能导致同时读写的操作或对内存的不安全访问的操作,而这是不安全的地方。 但是只要有机会确保在用户触摸某物之前保持安全不变式(即,不安全不安全),一切都会好起来的。


这种责任在谁的肩膀上?


我们来看一个更早的声明- 是的 ,Rust代码的有用性基于不安全的代码。 尽管这样做的方式与不安全地实现Python中基本数据结构的方式稍有不同,但Vec,Hashmap等的实现在某种程度上使用指针操作。


我们说Rust是安全的,基本假设是我们通过对标准库或其他库代码的依赖而使用的不安全代码已正确编写和封装。 Rust的基本优点是将不安全的代码驱动到不安全的块中,必须由其作者仔细检查。


在Python中,检查内存操作安全性的负担仅由解释器的开发人员和外部函数的接口的用户承担。 在C语言中,每个程序员都要承担这个负担。


在Rust中,它属于unsafe关键字的用户。 这是显而易见的,因为必须在此类代码内部手动维护不变式,因此有必要在库或应用程序代码中争取最小数量的此类代码。 检测到不安全性,将其突出显示并指示出来。 因此,如果Rust代码中出现段错误,那么您将在编译器中发现错误,或者在几行不安全的代码中发现错误。


这不是一个完美的系统,但是如果您同时需要速度,安全性和多线程,那么这是唯一的选择。

Source: https://habr.com/ru/post/zh-CN460295/


All Articles