安全编程语言(PL)越来越流行。我想知道安全PL 的正式定义是什么。例如,C是不安全的,但是Java是安全的。我怀疑应该将“安全”属性应用于PL实施,而不是PL本身。如果是这样,让我们讨论安全PL实现的定义。我自己尝试将该概念形式化的结果很奇怪,所以我想听听其他意见。请不要说每个PL都有不安全的命令。我们总是可以选择一个安全的子集。
安全编程语言(PL)越来越流行。我想知道安全PL 的正式定义是什么。例如,C是不安全的,但是Java是安全的。我怀疑应该将“安全”属性应用于PL实施,而不是PL本身。如果是这样,让我们讨论安全PL实现的定义。我自己尝试将该概念形式化的结果很奇怪,所以我想听听其他意见。请不要说每个PL都有不安全的命令。我们总是可以选择一个安全的子集。
Answers:
当我们在某种程度上将一种语言称为“安全” 时,这正式意味着有证据表明,该语言的格式正确的程序无法做一些我们认为危险的事情。“安全”一词的使用也没有那么正式,但这就是这里的人们理解您的意思的意思。我们希望“安全”语言具有多种不同的属性定义。
一些重要的是:
Andrew Wright和Matthias Felleisen对“类型安全性”的定义在许多地方(包括Wikipedia)被引用为“类型安全性”的公认定义,并在1994年证明ML的一部分满足它。
Michael Hicks 在此处列出了“内存安全”的几个定义。有些是无法发生的错误类型的列表,有些是基于将指针视为功能的。Java unsafe
通过让垃圾回收器管理所有分配和释放来保证所有这些错误都是不可能的(除非您显式使用标记为的功能)。Rust unsafe
通过其仿射类型系统做出同样的保证(再次,除非您将代码明确标记为),该系统要求变量必须拥有或借用,才能使用一次。
同样,线程安全代码通常被定义为不能显示涉及线程和共享内存的某些类型的错误的代码,包括数据争用和死锁。这些属性通常在语言级别上强制执行:Rust保证在其类型系统中不会发生数据争用,C ++保证其std::shared_ptr
指向多个线程中相同对象的智能指针不会过早删除对象或在最后一次引用时无法删除它为了销毁它,C和C ++另外atomic
在语言中内置了变量,如果正确使用,原子操作可以保证强制执行某些类型的内存一致性。MPI将进程间通信限制为显式消息,并且OpenMP具有语法以确保从不同线程访问变量是安全的。
内存永不泄漏的属性通常称为空间安全。自动垃圾收集是确保这一点的一种语言功能。
许多语言都保证其操作将获得明确的结果,并且其程序将具有良好的行为习惯。正如supercat给出的上述示例所示,C这样做是为了执行无符号算术(保证可以安全地环绕),而不是执行有符号算术(允许溢出导致任意错误,因为C需要支持在执行有符号算术时执行完全不同的操作的CPU)溢出),但随后该语言有时会默默地将未签名的数量转换为已签名的数量。
函数式语言具有许多不变式,可以保证任何格式正确的程序都可以保持不变式,例如,纯函数不会引起副作用。这些可能会或可能不会被描述为“安全”。
某些语言(例如SPARK或OCaml)旨在帮助证明程序的正确性。这可能被描述为“安全”,也可能不会被描述为“安全”。
系统不能违反正式安全模型的证明(因此打趣说:“任何可证明是安全的系统可能都不是。”)
没有“安全编程语言”的正式定义;这是一个非正式的概念。相反,声称提供安全性的语言通常会提供关于要声明/保证/提供哪种安全性的准确的正式声明。例如,该语言可能提供类型安全性,内存安全性或某些其他类似的保证。
double
或在另一个线程中被修改的变量,long
不能保证不会产生一个值的一半以某种未指定的方式与另一个值的一半混合),而是API规范但是在某些情况下具有不确定的行为。
如果您可以从本杰明·皮尔斯(Benjamin Pierce)的《类型和编程语言》(Types and Programming Languages)的副本中获得经验,那么他对“安全语言”一词的各种观点都有很好的概述。
对于您可能会发现有趣的术语,一种建议的解释是:
“一种安全的语言完全由其程序员手册定义。”让一种语言的定义成为程序员需要理解的一组事物,以便预测该语言中每个程序的行为。然后,诸如C之类的语言的手册并未构成定义,因为某些程序(例如,涉及未经检查的数组访问或指针算术的程序)的行为无法在不知道特定C编译器如何在内存中布局结构的细节的情况下进行预测等等,并且当由不同的编译器执行时,同一程序可能具有完全不同的行为。
因此,我会犹豫使用术语“不安全”来指代编程语言实现。如果一种语言中未定义的术语在不同的实现中产生不同的行为,则其中一个实现可能会产生可能更令人期待的行为,但我不会将其称为“安全”。
安全不是二进制,而是连续的。
非正式地讲,安全是指对错误的反对,其中最常提到的2个是:
这些不是语言可以阻止的错误的唯一类别,非常需要数据争用自由或死锁自由,正确性的证明很不错,等等。
但是,简单地将不正确的程序很少视为“不安全的”(仅是错误的),并且术语“安全性”通常用于保证影响我们对程序进行推理的能力的保证。因此,具有未定义行为的C,C ++或Go是不安全的。
当然,还有一些语言带有不安全的子集(Java,Rust等),这些子集有目的地划定由开发人员负责维护语言保证且编译器处于“自动”模式的区域。尽管有逃生门,这是一种务实的定义,但语言通常仍被称为安全。
Obj.magic
在Ocaml中)。而在实践中,这些都是真正需要的
虽然我不同意DW的回答,但我认为这使“安全”的一部分未得到解决。
如前所述,有多种类型的安全性得到提升。我相信了解为什么会有多个概念是一件好事。每个概念都与这样的想法相关:程序特别遭受某种类型的错误的困扰,并且如果该语言阻止了程序员这样做,那么程序员将无法犯此类特定的错误。
应该注意的是,这些不同的概念因此具有不同的错误类别,并且这些类别不是互斥的,也不是这些类别涵盖了所有形式的错误。仅以DW的2个示例为例,某个内存位置是否持有某个对象的问题既是类型安全又是内存安全的问题。
对“安全语言”的进一步批评来自以下观察结果:禁止某些被认为是危险的结构使程序员有必要提出替代方案。根据经验,好的库可以更好地实现安全性。使用已经过现场测试的代码可以避免产生新的错误。
C和Java之间的根本区别在于,如果避免使用Java的某些易于识别的功能(例如,Unsafe
名称空间中的功能),则每个人可能尝试的所有动作(包括“错误的”动作)都会产生有限范围的可能结果。尽管这限制了人们在Java中的工作能力-至少在不使用Unsafe
命名空间的情况下,它还可以限制由错误程序(或更重要的是-可以正确处理的程序)可能造成的损害有效文件,但并没有特别注意防止错误文件。
传统上,C编译器会在“正常”情况下以标准定义的方式处理许多动作,而“以环境特征”处理许多特殊情况。如果使用的CPU会在发生数值溢出时短路并着火,并且希望避免CPU着火,则需要编写代码来避免数值溢出。但是,如果使用的CPU能够以2的补码形式完美地截断值,那么在这种截断会导致可接受的行为的情况下,不必避免溢出。
现代C向前迈进了一步:即使人们针对的平台自然可以为数值溢出之类的行为定义一个行为,而标准不会对此施加任何要求,但程序的一部分可能会影响程序其他部分的行为。以不受时间和因果关系定律约束的任意方式编程程序。例如,考虑如下内容:
uint32_t test(uint16_t x)
{
if (x < 50000) foo(x);
return x*x; // Note x will promote to "int" if that type is >16 bits.
}
给出类似上述内容的“现代” C编译器可能得出结论,因为如果x大于46340,则x * x的计算将溢出,因此它可以无条件执行对“ foo”的调用。请注意,即使在x超出范围的情况下使程序异常终止,或者在这种情况下让函数返回任何值,即使可以接受,使用x超出范围的foo()调用也可能会导致远远超出损坏的范围。这些可能性中的任何一个。传统的C语言不会提供超出编程器和基础平台所提供内容的任何安全保护装置,但会允许安全保护装置限制意外情况造成的损害。现代C会绕过任何安全措施,这些安全措施不能有效地控制一切。
int
是32位,x
则将被提升为签名int
。从原理上看,该标准的作者预期,非怪异的实施将视符号和无符号类型以等价的方式的一些具体情况之外,但GCC有时在打破,如果方法“优化” uint16_t
通过uint16_t
乘法产生超出INT_MAX结果,即使将结果用作无符号值也是如此。
-Wconversion
。
return x+1;
应该不会出现问题,并且将结果强制转换为uint32_t会扼杀消息而无法解决问题。
语言中的正确性有多个层次。为了增加抽象度:
在下一个级别,在编译时而不是在运行时检测到的错误使语言更安全。语法正确的程序在语义上也应该尽可能地正确。当然,编译器无法了解全局,因此这涉及细节级别。强大而富有表现力的数据类型是此级别安全的一方面。有人会说这种语言应该使犯某些类型的错误变得困难(类型错误,超出范围的访问权限,未初始化的变量等)。运行时类型信息(如带有长度信息的数组)可以避免错误。我在大学编程Ada 83,发现一个编译的Ada程序通常比相应的C程序包含的错误少几个数量级。只需使用Ada的用户定义整数类型的能力即可,这些类型在没有显式转换的情况下是无法分配的:整个太空飞船因为高度和高度的混淆而坠毁,而使用Ada可以避免这种情况。
在下一级别,该语言应提供避免样板代码的方法。如果您必须编写自己的容器,它们的排序或它们的串联,或者必须编写自己的容器,那么string::trim()
您会犯错误。由于抽象级别的提高,此标准涉及适当的语言以及语言的标准库。
如今,该语言应提供在语言级别上进行并发编程的方法。没有语言支持,并发很难实现,并且可能无法正确完成。
该语言应提供模块化和协作的手段。上面的功能强大,精心设计的用户定义类型有助于创建表达性API。
语言定义在某种程度上是正交的;语言和库文件应有据可查。错误或丢失的文档会导致错误和错误的程序。
请不要说每个PL都有不安全的命令。我们总是可以选择一个安全的子集。
我所知道的每种语言都有编写可以(编译和)运行的非法程序的方法。我所知道的每种语言都有一个安全的子集。那么,您真正的问题是什么?
安全是多维的和主观的。
某些语言有很多“不安全”的操作。其他人的此类操作较少。在某些语言中,默认的做事方式本质上是不安全的。在其他情况下,默认方式是安全的。在某些语言中,有一个明确的“不安全”子集。在其他语言中,根本没有这样的子集。
在某些语言中,“安全性”专门指内存安全性-由标准库和/或运行时提供的服务,在这些运行时中,使访问困难或不可能发生的访问冲突成为可能。换句话说,“安全性”明确包括线程安全性。在其他语言中,“安全性”是指保证程序不会崩溃(此要求包括不允许任何形式的未捕获异常)。最后,在许多语言中,“安全性”指类型安全性-如果类型系统在某些方面是一致的,则称其为“健全的”(顺便说一句,Java和C#没有完全健全的类型系统)。
并且在某些语言中,“安全”的所有不同含义都被视为类型安全的子集(例如Rust和Pony通过类型系统的属性来实现线程安全)。
这个答案更广泛。在最近的几十年中,英语国家社会中某些具有政治倾向的部分已经破坏了安全这个词,因此它们的常用用法几乎没有定义。但是,对于技术主题,我仍然将“安全”和“安全”定义为:防止意外使用某些设备或使意外使用变得更加困难的设备,以及处于这种设备保护之下的状态。
因此,安全的语言具有某种设备来限制特定类别的错误。当然,在某些情况下,限制有时会带来不便甚至不便,但这并不是说“不安全”的语言会导致错误。例如,我的叉子上没有安全软木塞,几十年来,我一直努力地避免进食时刺伤眼睛。当然,比使用软木塞所花费的精力更少。因此,安全性需要付出一定的代价,因此必须对其进行判断。(软木叉子是对史蒂夫·马丁角色的引用)