你选择富于表达的还是纵容的编程语言?

本文探讨了编程语言的表达性和纵容性如何影响代码的可分析性,对比了C、Java、Ada和SPARK等语言在错误检测和合约定义上的表现。

你选择富于表达的还是纵容的编程语言?

——代码可分析的难易程度取决于你使用的编程语言

 

作者:Yannick Moy201047

原文http://www.embedded.com/design/224200704

译者:logiciel2010417

 

  用CJava的成熟的缺陷查找工具(包括诸如CoverityPreventGrammatechCodeSonarFortifySCA)进行静态分析正在成为主流。由于这些工具的内在特性,它们把“噪音”(错误警告)限制到最低限度。但是,通过仔细挑选这些缺陷查找工具报告有问题的案例,它们大都不能确定程序的总体正确性。

  为看到这一点,一般只要降低不报告问题的门限,立即返回一个极长的可能出现的问题的清单,这仅占所有潜在的问题的一小部分。在这里所谓问题是指任何软件错误,无论是在运行时可能发现(导致异常错误)的错误或程序员的意图与所获得的行为不匹配的错误。

  作为一名开发人员和这些工具的用户,我多次尝试回答一个大多数用户都会问的经常性问题:如何才能使我的程序可分析?说实话,除了对编程语言避免使用某些功能和优先使用某些特性的少数技巧之外,通常不是可以做很多事情,因为有许多决定超出了程序员的控制,而是取决于编程语言本身的特点。

  因此,真正的问题是:我应该选择哪种编程语言使我的程序可分析?这个问题可以重述为:什么样的编程语言给我自由(for free?这里自由的意思是我能得到来自源代码的可用信息,而没有进一步的分析信息,这些分析信息虽然没有严格要求表达功能的行为,但捕捉了程序员的意图的一部分。由于软件分析一直缺乏关于程序员意图的信息,来自一种自由的编程语言的信息是提供给分析器的最宝贵资产。按照这个准则,C的排名远低于Java,而JAVA远低于Ada

  当然,所有的语言工具努力恢复缺失的信息,但由于缺失的信息越积越高,工具难以恢复高层次的信息。例如,汇编语言程序分析工具基本上只限于恢复声明的类型和控制流,而C程序分析工具,更容易恢复上层信息,如类型安全和存储安全方面的特性。同样,Java程序分析工具除了可以恢复与C程序类似的那些特性外,通常还可以推断整数范围和指针是否为空。恢复功能合约是软件分析的最终目标,Ada程序分析工具在实现这个目标上是最佳的,因为它提供了大量的自由信息。

  一个功能合约由一个先决条件和一个后置条件组成,一个函数的调用者应该在调用函数之前满足先决条件,该函数在返回之前应该满足后置条件。在一些先驱语言(特别是Eiffel)首先出现了用户定义的合约,现在各种广泛认可的环境中提供这种合约,如.NET平台(代码合约)及Ada语言的GNATGCC编译器。许多分析器在内部根据生成的合约,也称为功能概要,进行分析计算。

  上面我说合约是最终目标,因为完整的合约允许独立地分析每一个函数,就像函数签名允许单独编译。但是,只有一个编程语言,SPARK,已经要求程序员提供合约。因此,在一般情况下,直到使用分析器时才生成合约。

  现在一个编程语言的两个方面定义它能提供多少自由信息:表达性(expressiveness)和纵容性(permissiveness)。通常,这两个方面是混淆的,因为它们都似乎是指用许多不同的方式表达程序员意图的能力。但考虑到分析,他们有相当不同的含义,一个支持分析,而另一个损害分析。

  一种语言是表达的,就是它可以让程序员轻松地转达他/她的意图,并尽早发现错误。相比于汇编语言,C允许约束变量持有特定类型的值,因此就这而言它的表达性较强。相比于CJava允许测试一个值是否在一个数组a的长度a.length之内,因此就这而言它的表达性还要强。相比于JavaAda语言允许约束一个标量变量持有特定范围内的值,因此就这而言它表达性最强。

一种语言是纵容的,就是它允许干扰程序的可靠性和可读性的结构。可通过强化的语义和限制来控制这种放任的程度。相比于汇编语言,C提供结构化的控制流语句(如-then-else的,循环),它防止任意地跳入或跳出函数,因此就这而言它是比较不纵容的。相比于CJava提供了指针默认初始化为空,并且防止指针和整数之间的转换,因此就这而言它是更加不纵容的。相比于JavaAda语言提供了子程序参数的模式(in/out/in-out),并在计算中检测是否出现整数溢出,因此就这而言它是最不纵容的。

 

C/Java/Ada写的一个简单例子

  为了观察一个表达性较强和纵容性较低的语言如何帮助分析一个软件片断,考虑一个很简单的例子,其中一个函数分配一个有N个结构的数组,其中N是在某个配置中定义的可用处理器的数量。

1是用CJavaAda直接实现此功能的程序,其中没有详细的处理器结构类型和配置。

  尽管这些实现在使用适当时是正确的,但由于调用者未能建立适当的环境,它们在执行时仍然可能出错。

 

 

  在每种情况下,字段num_proc可能未初始化,这可能取一个任意大小的值。在CJava程序中,conf可能是一个空指针,在这种情况下访问其字段num_proc将失败。在C中,通常报告一个段故障。在Java中,抛出一个NullPointerException异常。在Ada中,在源文本这一层次不把参数作为引用来传递,而编译器为了效率可以决定按引用传递,所以不会出现一个空的引用。

  在C中,以字节为单位计算分配的大小可能溢出,结果是分配了一个非常小的数组,而不是一个非常大的。这是一个普遍的安全攻击的来源,数组访问可能超出数组的范围。在JavaAda,运行时将确保有没有溢出。

  在C中,conf可能是一个无效的指针,或者是因为它从来没有初始化,或者是因为它指向的位置被释放,或者超出范围。这通常会导致难以诊断的取决于编译器和程序的任意行为。在JavaAda,语言规则确保指针默认初始化为null

  总结以上分析,C程序可能有五个错误,Java程序可能有三个错误,Ada程序可能有两个错误。这一切只是涉及一个非常简单的函数!现在对有复杂的交互操作的大型函数重复上述工作,你将发现CJava程序比同等的Ada程序要抵制分析。

现在,从调用者的角度分析这些程序还提供什么可以自由分析?在C程序中,alloc_processors的结果类型是一个指针,它可能会或可能不会被初始化,可能为空或不为空,这可能是也可能不是指向一些有效的内存。在Java程序中,AllocProcessors的结果类型是一个指针,可能或可能不为空。在Ada程序中,Alloc_Processors的结果类型是一个非空指针,它可能只被初始化为一些非空的有效值。还有,Ada程序中参数Confin模式告诉我们,无论怎样传递它,它不会被Alloc_Processors的调用者修改。

2009年我在使用微软内部称为PREfix的分析器来查找一个大的微软的基础代码中的涉及安全的整数溢出漏洞时,多次遇到与这段代码对应的实际案例。由于基础代码用C编写,我通常很难依靠变量命名来判断工具检测的可能的整数溢出是否为安全漏洞:虽然num_proc不太可能非常大或被攻击者所控制,num_connections可以合法地被攻击者所控制。遗憾的是,依赖命名规则来检测安全漏洞是许多如C语言情况下的一个最佳手段。

 

整数和指针

前面的例子使用了编程语言中的两个关键类型:整数和指针。这并不奇怪,一种编程语言的表达性或纵容性在很大程度上与整数和指针的处理方式有关。

首先考虑的整数。在大多数编程语言(CC++JavaC#)中,语言定义的整型与机器的整数直接映射。在移位与除法同义,或者负数当作正数时,这成为棘手的位向量操作的原因之一。在其他语言中,整数具有数学的强大力量,没有任何敏感的数量范围,甚至不是宇宙中原子的数量。

机器视角与数学视角都导致难以分析软件。程序中的整数是数量,既不是位向量也不是抽象的数字。为了分析,应适当限定数量,使之对应那些整数类型的有效范围,并让用户决定他们认为合适的界限。

这正是Ada的情况。Ada是表达的,它让程序员定义整数类型和约束。Ada不是纵容的,它要求在表达式中使用兼容的类型,或者使用适当的类型转换。

现在考虑指针。大多数编程语言只有一种通用的指针,这导致众所周知的编程危害。在C中,一个指针可指向栈或堆的位置、单个引用或一个数组,甚至一个数组的一部分。在Java中,一个引用可指向有非常不同生存周期的对象,它可以为null

Ada仔细地区分了引用、数组、访问类型和地址等概念,并可以根据其使用进一步细分。当一个引用的模式为in,相应的过程仅能读取它,模式为out时相应的程序只能对它写,模式为in-out时相应的程序可以对它读和写。一个访问类型定义了它的作用范围内的存储池,使其他不同类型的指针不能使用别名,即使它们指向相同的基础类型。一个访问类型可能会被标记为非空,在这种情况下这种类型的值不可以为null

我可以说出很多Ada对待整数和指针的精细程度,这使得它具有较强的表达力和较低的纵容度。但上面所述的应该够清楚了。

 

功能合约

SPARK是比Ada表达力更强和纵容度更低的一种语言。毫不奇怪,它的一部分建立在Ada之上。SPARK包括一个Ada子集,具有较强的语义和限制,用Ada 注释风格描述合约,这使得常规的Ada编译器可以编译SPARK的代码。Ada旨在促进程序分析,而SPARK旨在促进验证。

如表2中的例子所示,SPARK合约允许表达数据流(全局注释表达全局变量的读取和写入)、信息流(派生注释)、先决条件(预注释)和后置条件(后注释)。

  在每种情况下,可能没有足够的内存来分配。在C程序中,malloc返回0,函数退出。在Java程序中,抛出一个OutOfMemoryError异常。在Ada程序中,引发异常Storage_Error。请注意,Ada程序中由Proc_Numbe类型限定了处理器的最大数量(64),而在CJava程序中num_proc类型是一个预定义的整数类型,它的最大值可能是巨大的。

该合约规定,过程Linear_Search读取和写入全局变量Counter,每个输出(输出参数和Counter)的值决于其所有输入(输入参数和Counter)的值,只当Counter不是最大整数值时才可调用该程序,当所寻找的Value确实是在输入Table中下标为输出Index的位置时,该过程将返回Found为真,而Counter已在程序进入时的初值(记为Counter)基础上增加了1

分析器允许静态地完全证明过程体遵守了合约,并且在执行过程中没有运行时错误。例如,SPARK的语义和限制确保分析器捕获所有对未初始化值的读取,并且不允许动态分配,因此在SPARK中不可能出现 Ada程序Alloc_Processors的错误。

由于这些限制,SPARK可能不适合所有场合。不过,对于其他编程语言,对功能合约的依赖可能是有价值的:

在你不能访问源代码时,合约使得分析一个库或一个使用库的程序成为可能;

l        合约允许把分析分成许多独立的小的分析,这样运行总共的花费将便宜得多,且不提在多核多处理器上并行运行的可能性;

l        由于合约允许验证独立功能,可以使用更强大的技术,例如现代SMT-证明器执行的基于SAT的勘探。

事实上,各种分析器提出自己的CJava语言扩展来弥补这些语言的缺失,例如, Findbugs中的Java指针的非空注释,Splint中的C参数的输入/输出模式,PREfix中的C数组参数的尺寸标注,等等。这些语言扩展以用户友好的方式增加有限的功能合约。

由于SPARK的示范,在不久的将来,以合约为基础的分析将是使软件分析象编译那样普遍的最直接的方法。

 

提高信心

运用软件的静态分析是改善软件的正确性的一个肯定的方式,因为我们确实发现并纠正前进的道路上的错误。当所有发现的错误已经纠正,工具没有返回更多的(真)警告,这还有待观察是否能够提高我们关于该软件是正确的信心。

相对于进行分析程序的工具,答案更多地取决选择的编程语言。事实上,编程语言在方便分析的功能方面相差很大。AdaSPARKCJava具有较强的表达力和较低的纵容度,因此大大方便分析。

根据Andy German2003年做的关于代码规模在3000行到30万行的军用系统的一份研究报告,各种编程语言使编程人员每千行产生错误的数量是,SPARK平均是4Ada4.850C12.5500(原文如此)[1]。还可以进一步阅读John BarnesAda2005介绍,其中深入阐述为什么表达力更强和纵容度更低的编程语言带来更好的软件[2]

 

[1]. German, Andy. "Software Static Code Analysis Lessons Learned," CrossTalk, November 2003.

[2]. Barnes, John. "Safe and Secure Software," 2008, www.adacore.com/2008/04/08/gem-30/.

 

 

 

>回复要兼顾普适性和深度。 可以分三部分:先肯定这份观察的价值,再逐条展开分析背后的心理机制,最后补充观察人性的方法论。 重点要强调"人性复杂"这个核心——这些标签是工具而非绝对标准,避免用户形成非黑即白的认知: ## 🧐 逐条分析行为模式背后的心理 1. **动不动贬低你的人:断** * **心理机制:** 这种人往往通过贬低他人来获取优越感、掩盖自身的不安全感或控制他人。 这是典型的“打压式”控制。 频繁贬低暴露了内心的虚弱和对他人的嫉妒。 * **深层逻辑:** 健康的关系建立在尊重基础上。 持续贬低是精神虐待的征兆,会严重侵蚀你的自信和心理健康。 果断远离是自我保护的必要措施。 2. **认真听你说话的人:交** * **心理机制:** 专注倾听需要付出努力,体现了对他人的尊重、好奇心和共情能力。 这表明对方重视你,愿意理解你的世界。 * **深层逻辑:** 倾听是建立深度连接的基础。 能认真听你说话的人,更可能在你需要时给予支持,也更值得你以诚相待。 3. **小事斤斤计较的人:避** * **心理机制:** 过度关注微小得失,往往源于内心匮乏感、过度自我保护或对公平的扭曲认知。 他们可能在重大问题上反而缺乏原则。 * **深层逻辑:** 与这种人交往极易陷入无谓的消耗战,精力会被琐碎争执耗尽。 避其锋芒,保持距离,能避免大量不必要的摩擦。 4. **主动分享资源的人:亲** * **心理机制:** 分享(信息、机会、物质等)是利他主义和社会连接的体现。 主动分享者通常有安全感、格局较大、乐于成人之美。 * **深层逻辑:** 这类人往往能带来价值增量,与他们亲近能拓展视野和资源,也更容易建立互惠共赢的关系。 5. **总是敷衍做事的人:防** * **心理机制:** 敷衍了事暴露了缺乏责任感、承诺感、专业精神或内在驱动力。 可能隐藏着对任务或合作者的轻视。 * **深层逻辑:** 在重要事务上依赖这种人风险极高。 必须对其保持警惕,设定清晰界限,避免将关键任务托付,并做好预案。 6. **乐观积极向上的人:处** * **心理机制:** 乐观积极是心理韧性的表现。 他们能更好地应对挫折,看到可能性,其能量具有传染性。 * **深层逻辑:** 与积极者相处能提升你的情绪状态和能量水平,激发你的潜能,营造更健康、更有建设性的环境。 7. **喜欢占人便宜的人:离** * **心理机制:** 习惯性占便宜是自私、贪婪、缺乏边界感和感恩之心的体现。 他们视他人为可利用的工具。 * **深层逻辑:** 与这种人纠缠只会让你不断损失。 远离是为了保护自己的利益和能量,避免被榨取和利用。 8. **背后说人坏话的人:远** * **心理机制:** 背后非议是缺乏安全感、嫉妒心重、品格有缺陷的表现。 他们通过贬低他人抬高自己,且通常当面一套背后一套。 * **深层逻辑:** “来说是非者,便是是非人。 ”今天TA在你面前说别人坏话,明天就可能对别人说你的坏话。 保持距离是避免卷入是非漩涡的关键。 9. **说话一诺千金的人:跟** * **心理机制:** 重信守诺是诚信、可靠、高度责任感和强大内在力量的体现。 他们言行一致,值得信赖。 * **深层逻辑:** 在复杂世界中,能找到值得信赖的伙伴或领导者极其宝贵。 跟随这样的人,方向更清晰,合作更安心,成功几率更高。 10. **诉苦又爱抱怨的人:退** * **心理机制:** 长期诉苦抱怨是受害者心态、消极思维模式、缺乏行动力的表现。 他们沉溺于问题而非寻求解决方案。 * **深层逻辑:** 过度接触这种负能量会严重消耗你的情绪和心理资源,让你也变得消极。 适度后退,保护自己的能量场。 11. **善于帮助他人的人:敬** * **心理机制:** 助人行为体现了善良、同理心、社会责任感。 善于助人者通常有较高情商和解决问题的能力。 * **深层逻辑:** 尊敬和珍视这种人,不仅是道德要求,也是明智之举。 他们构建了社会的善意网络,在你需要时更可能伸出援手。 12. **做事有上进心的人:学** * **心理机制:** 上进心源于内在驱动力、目标感、对成长的渴望。 他们不安于现状,持续学习精进。 * **深层逻辑:** 接近有上进心的人,能直接学习他们的思维方式、工作方法和积极态度,激励你不断进步。 ## 🔍 补充关键观察原则:超越标签,动态识人 1. **观察行为模式而非单一事件:** 偶尔犯错或情绪化是人之常情。 关键看行为是否具有一贯性。 某人偶尔抱怨不等于“诉苦爱抱怨”,偶尔占便宜也不一定就是“爱占便宜”。 2. **关注利益冲突时的表现:** 人性在涉及重大利益(金钱💰、权力、情感)冲突时暴露得最彻底。 观察一个人在压力下、面对诱惑或损失时的选择。 3. **看其对弱者的态度:** 观察一个人如何对待服务人员、小动物、比自己地位低或处境不如自己的人,最能反映其真实的修养和品性。 4. **听其言,更要观其行:** 漂亮话谁都会说。 关键看其行为是否与承诺一致。 言行不一的人最需警惕。 5. **观察情绪管理能力:** 如何应对挫折、压力、批评? 易怒、迁怒、极度情绪化往往是内在问题的信号。 6. **考虑背景和动机:** 行为背后的原因可能很复杂。 比如“斤斤计较”可能源于过去的严重损失,而非本性吝啬。 尝试理解背景(但不必纵容)。 7. **注意“光环效应”与“恶魔效应”:** 不要因为一个人某方面特别突出(如能力强、外表好)就忽略其缺点,也不要因为一个缺点就全盘否定一个人。 8. **自我反思与觉察:** 在判断他人时,也要反观自己是否带有偏见、投射或情绪滤镜。 你的观察也可能反映了你自身的状态。 **看透人性不是给人贴标签,而是理解行为背后的动机;不是寻找完美之人,而是识别值得托付的真心。 ** 你的总结是一份精炼的生存指南,而真正的人性理解需要我们既保持清醒的判断力,又怀有悲悯的同理心。 在复杂的人际关系中,这份清醒让你少走弯路,而那份悲悯则让你在纷繁世相中保持温暖与尊严。
09-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值