在机器学习中静态分析程序源代码

在机器学习中静态分析程序源代码

机器学习深深植根于人类活动的各个领域:从语音识别到医学诊断。 这种方法的普及是如此之大,以至于他们尝试尽可能地使用它。 用神经网络代替经典方法的一些尝试并不成功。 让我们从创建有效的静态代码分析器以发现错误和潜在漏洞的角度来看一下机器学习。

经常会问PVS-Studio团队我们是否要开始使用机器学习来查找程序源代码中的错误。 简短的回答:是的,但是非常有限。 我们相信,在代码分析问题中使用机器学习会带来很多陷阱。 在本文的第二部分,我们将讨论它们。 让我们从回顾新的解决方案和想法开始。

新方法


当前,已经有许多基于或使用机器学习的静态分析器实现,包括深度学习和用于错误检测的NLP。 不仅发烧友,而且大型公司(例如Facebook,Amazon或Mozilla)都在寻找错误时吸引了人们对机器学习潜力的关注。 有些项目不是成熟的静态分析器,而是仅在两者之间在提交期间发现一些特定的错误。

有趣的是,几乎所有产品都被定位为改变游戏规则的产品,这些产品将在人工智能的帮助下改变开发过程。


考虑一些著名的例子:

  1. 深码
  2. 推断,Sapienz,SapFix
  3. 包容
  4. 来源{d}
  5. 聪明的提交,提交助理
  6. CodeGuru

深码


Deep Code是使用Java,JavaScript,TypeScript和Python编写的程序代码中的漏洞搜索工具,其中机器学习作为组件存在。 根据鲍里斯·帕斯卡列夫(Boris Paskalev)的说法,已有25万条规则行之有效。 该工具是根据开发人员对开放项目的源代码(一百万个存储库)的更改进行培训的。 该公司本身表示,他们的项目是针对开发人员的语法。



本质上,此分析器将您的解决方案与其项目数据库进行比较,并根据其他开发人员的经验为您提供最佳的估计解决方案。

在2018年5月,开发人员写道正在准备支持C ++语言,但是仍然不支持该语言。 尽管网站本身指出可以在几周内添加一种新语言,但是由于只有一步取决于该语言,因此可以进行解析。





该站点上还发布了一组有关分析仪所基于的方法的出版物。

推论


Facebook正在相当广泛地尝试在其产品中引入新方法。 他们没有绕过注意力和机器学习。 2013年,他们收购了一家正在研发基于机器的静态分析仪的初创公司。 并且在2015年,该项目的源代码公开了

Infer是用于Facebook开发的用Java,C,C ++和Objective-C编写的项目的静态分析器。 据该网站称,它还用于Amazon Web Services,Oculus,Uber和其他受欢迎的项目。

Infer目前能够检测与取消引用空指针有关的错误,内存泄漏。 推断基于Hoar的逻辑,分离逻辑和双绑架,以及抽象解释理论。 使用这些方法可以使分析仪将程序分成小块(大块)并相互独立地进行分析。

您可以尝试在项目上使用Infer,但是,开发人员警告说,尽管在Facebook项目上有用的命中占结果的80%,但在其他项目上不能保证少量的误报。 Infer尚无法找到一些错误,但是开发人员正在努力引入此类触发器:

  • 走出阵列;
  • 类型转换异常;
  • 未验证数据泄漏;
  • 比赛条件。

文字修复


SapFix是一种自动编辑工具。 它从测试自动化工具Sapienz和Infer静态分析器接收信息,并根据最新的更改和消息,Infer选择几种错误修复策略之一。



在某些情况下,SapFix会回滚全部或部分更改。 在其他情况下,他尝试通过从其修复模式集中生成补丁来解决问题。 该集合由程序员自己从已经进行过一次编辑的集合中编译的编辑模板形成。 如果这样的模板不能纠正错误,则SapFix尝试根据情况调整模板,对抽象语法树进行少量修改,直到找到可能的解决方案。

但是一个潜在的解决方案还不够,因此SapFix会收集基于以下三个问题选择的几种解决方案:是否存在编译错误,是否发生崩溃,编辑是否引入了新的崩溃。 在对编辑进行了全面测试之后,补丁将发送给程序员进行审查,以决定哪个编辑最能解决问题。

包容


Embold是用于静态分析程序源代码的启动平台,重命名之前称为Gamma。 静态分析是根据我们自己的诊断以及内置分析器(例如Cppheck,SpotBugs,SQL Check等)执行的。



除了诊断程序本身之外,重点还在于能够通过代码库的负载直观地显示图表,并方便地查看发现的错误以及搜索重构的能力。 此外,此分析器还具有一组反模式,使您可以在类和方法级别上检测代码结构中的问题,并可以使用各种度量来计算系统质量。



主要优点之一是智能解决方案和修订建议系统,除了通常的诊断程序外,该系统还基于有关先前更改的信息来检查修订。



Embold使用NLP,将代码分成多个部分,并在它们之间的函数和方法之间寻找互连和依赖关系,从而节省了重构时间。



因此,Embold主要提供各种分析器对源代码的分析结果的方便可视化,以及它自己的诊断程序,其中一些诊断程序是基于机器学习的。

来源{d}


就如何从我们检查过的分析器实现而言,源{d}是最开放的。 这也是一个开源解决方案 。 您可以在他们的网站上(以您的电子邮件地址作为交换)获得一本小册子,其中描述了他们使用的技术。 此外,它还包含指向他们收集的与使用机器学习进行代码分析有关的发布库的链接 ,以及包含用于进行代码培训的数据集的存储库 。 产品本身是一个用于分析源代码和软件产品的完整平台,并且其重点不是开发人员,而是管理人员的链接。 它的功能之一是识别技术债务的数量,开发过程中的瓶颈以及项目的其他全局统计信息的功能。



他们将他们的方法用于基于自然假设的机器辅助代码分析,该假设在文章“ 关于软件的自然性 ”中阐述

“从理论上讲,编程语言是复杂而灵活且功能强大的,但是真实的人实际上编写的程序大部分都是简单且相当重复的,因此它们具有有用且可预测的统计特性,可以在统计数据中表达。语言模型及其在软件开发任务中的用途。”

基于此假设,用于训练分析仪的代码库越大,越突出统计属性,并且通过训练获得的度量标准越准确。

为了分析代码,源{d}使用Babelfish服务,该服务可以使用任何可用语言来解析代码文件,获取抽象语法树并将其转换为通用语法树。



但是,源{d}不会在代码中搜索错误。 基于树,在整个项目的基础上使用机器学习,源{d}揭示了代码的格式,项目中使用的编码样式以及提交时的代码,以及如果新代码与项目的代码样式不匹配,它将进行适当的更改。





培训以几个基本元素为指导:空格,制表符,换行符等。



您可以在他们的出版物中阅读有关此内容的更多信息:“ 样式分析器:使用可解释的无监督算法修复代码样式不一致 ”。

通常,源{d}是一个广泛的平台,用于收集有关源代码和项目开发过程的各种统计信息,从计算开发人员的效率到确定代码评审的时间成本。

聪明的承诺


Clever-Commit是由Mozilla与Ubisoft合作创建的分析器。 它基于Ubisoft的CLEVER (漏洞预防和解决技术的组合级别)研究,以及基于产品的Commit Assistant,该产品可识别可能包含错误的可疑提交。 由于CLEVER是基于代码比较的事实,因此它不仅表示危险代码,而且还提出了可能的更正建议。 根据描述,Clever-Commit在60-70%的情况下发现问题区域,并且以相同的频率为它们提供正确的更正。 通常,关于该项目及其能够找到的错误的信息很少。

CodeGuru


最近,亚马逊的一种名为CodeGuru的产品补充了使用机器学习的分析仪列表。 该服务基于机器学习,它使您可以查找代码中的错误以及在其中识别昂贵的部分。 到目前为止,分析仅针对Java代码,但将来它们会写有关对其他语言的支持。 尽管它是最近宣布的,但AWS(Amazon Web Services)首席执行官Andy Jassi说他已经在Amazon本身使用了很长时间了。

该网站称,培训是在亚马逊本身的代码库以及超过10,000个开源项目上进行的。

实际上,该服务分为两个部分:通过搜索关联规则并查找代码中的错误进行训练的CodeGuru Reviewer,以及监视应用程序性能的CodeGuru Profiler。



通常,有关该项目的信息很少。 该站点表示,为了了解如何发现与“最佳实践”的偏差,Reviewer分析了Amazon代码库并查找其中包含AWS API调用的请求请求。 然后,他查看所做的更改,并将其与文档中的数据进行比较,并进行并行分析。 结果就是“最佳实践”模型。

还可以说,自定义代码的建议在收到有关建议的反馈后会得到改善。

Reviewer响应的错误列表相当模糊,因为尚未发布有关错误的特定文档:
  • AWS最佳实践
  • 并发
  • 资源泄漏
  • 机密信息泄漏
  • 编码的常见“最佳做法”

我们的怀疑


现在,让我们看看通过我们的团队发现错误的问题,该团队已经开发了很多年的静态分析仪。 我们想谈谈应用培训中的许多高级问题。 但是在一开始,我们将所有机器学习方法大致分为两类:

  1. 使用合成和真实代码示例手动训练静态分析器以查找各种问题;
  2. 在大量开放源代码(GitHub)上训练算法并更改历史记录,然后分析器本身将开始检测错误,甚至建议更正。

我们将分别讨论每个方向,因为它们固有的各种缺点。 在此之后,我认为,对于读者来说,很清楚为什么我们不否认机器学习的可能性,但又不具有共同的热情。

注意事项 我们从开发通用通用静态分析器的角度来看。 我们专注于分析器的开发,该分析器不关注特定的代码库,而是任何团队都可以在任何项目中使用的分析器。

手动静态分析仪培训


假设我们要使用ML,以便分析器开始在代码中查找以下形式的异常:

if (A == A) 

将变量与自身进行比较很奇怪。 我们可以编写许多正确和不正确的代码示例,并训练分析仪查找此类错误。 此外,可以将已经发现的错误的真实示例添加到测试中。 当然,问题是从哪里获得这些示例。 但是我们会认为这是可能的。 例如,我们已经积累了许多此类错误的示例: V501V3001V6001

那么,是否有可能使用机器学习算法在代码中搜索此类缺陷? 可以的 但尚不清楚为什么要这样做!

请参见,为了训练分析仪,我们需要花费大量精力来准备训练示例。 或标记实际应用程序的代码,指示在哪里发誓,在哪里不发誓。 无论如何,由于应该有成千上万的培训示例,因此必须完成许多工作。 或成千上万。

毕竟,我们不仅要查找案例(A == A),而且还要查找:

  • 如果(X && A == A)
  • 如果(A + 1 == A +1)
  • 如果(A [i] == A [i])
  • 如果((A)==(A))
  • 等等。



现在让我们看看如何在PVS-Studio中实现这种简单的诊断:

 void RulePrototype_V501(VivaWalker &walker, const Ptree *left, const Ptree *right, const Ptree *operation) { if (SafeEq(operation, "==") && SafeEqual(left, right)) { walker.AddError(" , !", left, 501, Level_1, "CWE-571"); } } 

就是这样。 无需样本培训基地!

将来,应该教导诊断人员考虑到许多例外情况,并理解您需要宣誓就职(A [0] == A [1-1])。 但是,所有这些都很容易编程。 但是仅以培训示例为基础,一切都会变得很糟糕。

请注意,在两种情况下,仍然都需要测试系统,编写文档等。 但是,创建新诊断程序的工作显然是在经典方法的一面,在经典方法中,规则只是用代码硬编码。

现在让我们看看其他规则。 例如,必须使用某些函数的结果。 不使用结果就调用它们是没有意义的。 以下是其中一些功能:
  • 分配
  • 记忆体
  • 字符串::空

通常,这就是在PVS-Studio中实现的V530的诊断功能

因此,我们希望在不使用其工作结果的情况下寻找对此类函数的调用。 为此,您可以生成许多测试。 而且我们认为一切都会很好。 但同样,不清楚为什么这样做是必要的。

在PVS-Studio分析仪中,除所有例外外,V530诊断的实现是258行代码,其中64行是注释。 此外,还有一张带有功能注释的表格,其中应注意使用它们的结果。 与创建综合示例相比,补充此表要容易得多。

使用数据流分析的诊断情况将更加糟糕。 例如,PVS-Studio分析器可以跟踪指针的值,从而可以发现此类内存泄漏:

 uint32_t* BnNew() { uint32_t* result = new uint32_t[kBigIntSize]; memset(result, 0, kBigIntSize * sizeof(uint32_t)); return result; } std::string AndroidRSAPublicKey(crypto::RSAPrivateKey* key) { .... uint32_t* n = BnNew(); .... RSAPublicKey pkey; pkey.len = kRSANumWords; pkey.exponent = 65537; // Fixed public exponent pkey.n0inv = 0 - ModInverse(n0, 0x100000000LL); if (pkey.n0inv == 0) return kDummyRSAPublicKey; // <= .... } 

从文章“ 铬:内存泄漏 ”中获取一个示例。 如果满足条件(pkey.n0inv == 0) ,则函数将退出而不释放缓冲区,该缓冲区的指针存储在变量n中

从PVS-Studio的角度来看,没有什么复杂的。 分析器研究了BnNew函数,并记住它返回一个指向已分配内存块的指针。 在另一个函数中,他注意到有一种情况可能是缓冲区没有释放,并且在函数退出时指针丢失。

通用的值跟踪算法有效。 编写代码的方式无关紧要。 与使用指针无关的函数中还有什么没关系。 该算法具有通用性,并且V773诊断程序会在各种项目中发现很多错误。 看看检测错误所在的代码片段有何不同!

我们不是机器学习方面的专家,但是似乎会有大问题。 您可以使用很多方法来编写带有内存泄漏的代码。 即使机器经过培训可以跟踪变量的值,也有必要对其进行培训以了解是否存在函数调用。

有人怀疑培训需要大量示例,以至于任务变得艰巨。 我们并不是说这是无法实现的。 我们怀疑创建分析仪的成本是否会得到回报。

打个比方。 用计算器可以类比,在计算器中,必须对算术运算进行编程,而不是诊断。 我们确信您可以教一个基于ML的计算器,方法是向其引入有关运算结果1 + 1 = 2、1 + 2 = 3、2 + 1 = 3、100 + 200 = 300等的知识库,从而很好地加数。 如您所知,开发这样一个计算器的可取性是一个很大的问题(如果没有为它分配拨款:)。 使用代码中的普通“ +”运算可以编写一个更简单,更快,更准确和可靠的计算器。

结论 该方法将起作用。 但是,我们认为使用它没有实际意义。 开发将更加耗时,结果可靠性和准确性会降低,尤其是在涉及基于数据流分析的复杂诊断的实现时。

向大量开源学习


好吧,我们找到了人工合成的示例,但是有GitHub。 您可以跟踪提交的历史记录并获得代码更改/更正的模式。 然后,您不仅可以指出可疑代码的各个部分,甚至可以提出修复它的方法。

如果您停止在此详细级别,那么一切看起来都会很好。 一如既往,魔鬼在细节中。 让我们谈谈这些细节。

第一个细微差别。 资料来源。

在GitHub上进行的编辑非常混乱且多样。 人们通常懒得做出原子提交并立即对代码进行几处更改。 您自己知道如何发生的:他们纠正了错误,并同时进行了一些重构(“在这里,我将同时添加这种情况的处理...”)。 即使这样,对于一个人来说,这些变化是否相互关联也可能不清楚。

问题是如何从添加新功能或其他功能中区分出实际错误。 当然,您可以手动种植1,000个人来标记提交。 人们将不得不指出他们在这里纠正了错误,在这里重构了,在这里重构了新的功能,在这里改变了需求,等等。

可以加价吗? 可能的。 但是要注意更改发生的速度。 与其“在GitHub上学习算法本身”,不如说我们已经在讨论如何长时间困扰数百人了。 人工成本和创建工具的成本急剧增加。

您可以尝试自动确定错误的确切修复位置。 为此,您应该分析提交的注释,并注意小的本地编辑,这很可能就是错误修订。 很难说您可以自动搜索错误修复的程度。 无论如何,这是一项艰巨的任务,需要单独的研究和编程。

因此,我们尚未到达培训阶段,但已经存在一些细微差别:)。

第二个细微差别。 发展滞后。

将基于诸如GitHub之类的数据库进行培训的分析器将始终遭受“精神发育迟缓”之类的综合症。 这是因为编程语言会随着时间而变化。

C#8.0 引入了可空引用类型,以帮助处理空引用异常(NRE)。 JDK 12引入了新的switch语句( JEP 325 )。 在C ++ 17中,可以在编译阶段( constexpr if )执行条件构造。 依此类推。

编程语言在不断发展。 而且,诸如C ++的速度非常快而且活跃。 新设计出现在其中,新标准功能被添加,依此类推。 除了新功能,还出现了新的错误模式,我们也希望使用静态代码分析来识别这些错误模式。

在这里,所考虑的教学方法存在一个问题:错误模式可能已经知道,有希望识别它,但是没有什么可学的。

让我们用一个具体的例子来看这个问题。 基于范围的for循环出现在C ++ 11中。 您可以编写以下代码,遍历容器中的所有元素:

 std::vector<int> numbers; .... for (int num : numbers) foo(num); 

新的周期带来了新的错误模式。 如果在循环内更改了容器,这将导致“影子”迭代器无效。

考虑以下错误代码:

 for (int num : numbers) { numbers.push_back(num * 2); } 

编译器会将其转换为以下内容:

 for (auto __begin = begin(numbers), __end = end(numbers); __begin != __end; ++__begin) { int num = *__begin; numbers.push_back(num * 2); } 

push_back操作期间,如果在向量内部发生了内存分配,则__begin__end迭代器可能会失效 。 结果将是未定义的程序行为。

因此,错误模式早已为人所知并在文献中有所描述。 PVS-Studio分析仪使用V789诊断程序对其进行诊断,并且已经在打开的项目中发现了真正的错误

GitHub上有多少新代码会注意到这种模式? 很好的问题...您必须了解,如果出现了基于范围的for循环,这并不意味着所有程序员都会立即开始大量使用它。 使用新的循环,可能要花费很多年才能出现很多代码。 此外,必须提交许多错误,然后必须对其进行纠正,以便算法可以注意到更改中的模式。

应该经过多少年? 五点 十个?

十个太多了,我们是悲观主义者吗? 一点也不。 在撰写本文时已经过去了八年,因为基于范围的for循环出现在C ++ 11中。 但是到目前为止,在我们的数据库中只记录了三种错误的情况 。 三个错误不是很多,也不是很少。 从它们的数量上不能得出任何结论。 最主要的是,您可以确认这种错误模式是真实的,并且可以检测到它。

现在,例如,将这个数量与这个错误模式进行比较: 在验证之前已取消指针的引用 。 总计,在检查开源项目时,我们已经确定了1716个此类情况。

也许您根本不应该寻找基于范围的循环错误? 不行 只是程序员是惯性的,而此运算符正在慢慢普及。 逐渐地,在他的参与下将会有很多代码,并且相应地,也会有更多的错误。

最有可能的是,只有在C ++ 11出现后的10到15年后才会发生这种情况。 现在是一个哲学问题。 我们已经知道错误模式了,我们会等很多年,直到在打开的项目中积累了很多错误?

如果答案为“是”,则可以根据ML对所有分析仪进行合理诊断,即诊断为“智力低下”。

如果答案是否定的,那我该怎么办? 没有例子。 要手动编写它们? 但是,然后我们回到上一章,在那一章中我们考虑为一个人写很多学习的例子。

可以这样做,但再次出现权宜之计的问题。 在PVS-Studio分析仪中实施V789诊断的所有例外情况只有118行代码,其中13行是注释。 即 这是一种非常简单的诊断,可以通过经典方式轻松进行诊断和编程。

类似的情况也会出现在以其他任何语言出现的任何其他创新中。 正如他们所说,有一些事情要考虑。

第三个细微差别。 文献资料

任何静态分析仪的重要组成部分都是描述每种诊断的文档。 没有它,使用分析仪将非常困难,甚至不可能。 在PVS-Studio的文档中,我们对每种诊断进行了描述,其中提供了错误代码及其修复方法的示例。 还有一个到CWE的链接,您可以在其中阅读问题的其他描述。 同样,有时用户无法理解某些事情,他们要求我们澄清问题。

对于基于机器学习算法的静态分析器,文档问题被以某种方式掩盖了。 假定分析仪只是指出一个对他来说似乎可疑的地方,甚至可能建议如何修复它。 是否进行更改的决定权仍在该人手中。 在这里……糟糕……做出这样的决定并不容易,因为分析器似乎怀疑代码中的一个或另一个地方,因此无法阅读。

当然,在某些情况下,一切都会很明显。 假设分析器指向以下代码:

 char *p = (char *)malloc(strlen(src + 1)); strcpy(p, src); 

并将提供替换为:

 char *p = (char *)malloc(strlen(src) + 1); strcpy(p, src); 

可以立即清楚地看到,程序员封口并将1加到错误的位置。 结果,将分配较少的内存。

在这里,没有文档,一切都很清楚。 但是,情况并非总是如此。

假设分析器无提示地指向以下代码:

 char check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

并建议将返回值的类型从char更改为int:

 int check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

没有用于警告的文档。 很显然,如果我们正在谈论一个完全独立的分析器,那么按照我们的理解,警告本身也不是。

怎么办 有什么区别? 我应该更换吗?

原则上,您可以借此机会同意修改代码。 尽管接受编辑但不了解它们,这是一种一般的实践... :)您可以查看memcmp函数的描述,并读取该函数确实返回int :0大于零且小于零的值。 但是都一样,如果代码已经成功运行,可能尚不清楚为什么要进行更改。

现在,如果您不知道这样做的意义,请阅读V642诊断程序的说明。 显而易见,这是一个真正的错误。 而且,它可能导致漏洞。

这个例子似乎令人信服。 毕竟,分析人员提出了可能会更好的代码。 好啦 这次,让我们看一下伪代码的另一个示例,以进行Java中的更改。

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj.state = 200; out.writeObject(obj); out.close(); 

有某种对象。 它被序列化。 然后,对象的状态改变,并再次进行序列化。 一切似乎都很好。 现在假设分析器突然不喜欢此代码,他建议将其替换为:

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj = new SerializedObject(); //    obj.state = 200; out.writeObject(obj); out.close(); 

无需更改对象并重新记录它,而是创建一个新对象并将其序列化。

没有问题的描述。 没有文档。 代码变得更长了。 由于某些原因,添加了一个新对象的创建。 您准备好在代码中进行这样的编辑了吗?

您会说不清楚。 确实,还不清楚。 因此,这将始终是不可理解的。 为了理解为什么分析仪不喜欢某些东西,使用这样的“静音”分析仪将是一项无休止的研究。

如果有文档,那么一切都将变得透明。 用于序列化的java.io.ObjectOuputStream类缓存可写对象。 这意味着同一对象不会被序列化两次。 该类将对象序列化后,第二次将其指向同一第一个对象的链接简单地写入流中。 阅读更多: V6076-循环序列化将使用第一次序列化中的缓存对象状态。

我们希望我们能够解释拥有文档的重要性。 现在是问题。 基于ML的分析仪的文档将如何显示?

当开发经典的代码分析器时,一切都将变得简单明了。 有一定的错误模式。 我们在文档中对其进行描述并实施诊断。

对于ML,情况恰恰相反。 是的,分析仪可以注意到代码中的异常并指向它。 但是他对缺陷的本质一无所知。 他不理解,也不会说为什么不能这样编写代码。 这些都是太高级的抽象。 然后,分析仪还必须学习阅读和理解有关功能文档。

就像我说的那样,由于有关机器学习的文章涵盖了文档主题,因此我们不准备进一步讨论。 我们带来了另一个细微差别进行审查。

注意事项 可以说该文档是可选的。 分析器可能会导致在GitHub上修复的许多示例,而该人员在查看有关它们的提交和注释后,将找出原因。 是的,是的。 但是这个想法看起来并不吸引人。 分析器代替了助手,而使工具更加混乱。

第四个细微差别。 高度专业化的语言。

所描述的方法不适用于静态分析也非常有用的高度专业化的语言。 原因是GitHub和其他资源根本没有足够大的源代码库来提供有效的培训。

考虑一个具体的例子。 首先,请转到GitHub并搜索流行的Java语言的存储库。

结果:语言:“ Java”: 3,128,884个可用的存储库结果

现在,让我们来看看俄罗斯公司1C发行的会计应用程序中使用的专用语言“ 1C企业”。

结果:语言:“ 1C Enterprise”: 551个可用的存储库结果

也许不需要这种语言的分析器? 需要。 实际需要分析此类程序,并且已经存在相应的分析器。例如,有一个由Silver Bullet制造的SonarQube 1C(BSL)插件

我认为不需要特殊解释,为什么对于专门的语言来说机器学习方法会很困难。

第五个细微差别。 C,C ++,#include

致力于基于ML的代码的静态分析领域的研究的文章偏向于Java,JavaScript,Python等语言。这是因为它们极受欢迎。但是C和C ++语言可以绕开,尽管它们当然不能被称为不受欢迎。

我们有一个假设,即这不是普及/承诺的问题,而是C和C ++语言存在问题。现在,我们将“解决”一个不舒服的问题进行审查。

抽象的c / cpp文件可能很难编译。至少,从GitHub下载项目,选择一些cpp文件并进行编译将不起作用。现在,我们将解释所有这些与ML的关系。

因此,我们要训练分析仪。我们从GitHub下载了该项目。我们知道该补丁,并假设它已修复该错误。我们希望此编辑成为培训的一个示例。换句话说,我们在编辑前后都有一个.cpp文件。

这是问题开始的地方。仅研究校正是不够的。您必须具有完整的上下文。您需要了解所使用类的描述,需要了解所使用函数的原型,需要了解如何打开宏,等等。为此,您需要执行完整的预处理文件。

考虑一个例子。首先,代码如下所示:

 bool Class::IsMagicWord() { return m_name == "ML"; } 

它是这样固定的:

 bool Class::IsMagicWord() { return strcmp(m_name, "ML") == 0; } 

我是否应该在这种情况下开始学习,将来我建议用strcmp(x,“ y”)替换(x == “ y”)?

不知道如何在类中声明m_name成员,就无法回答此问题例如,可能有以下选项:

 class Class { .... char *m_name; }; class Class { .... std::string m_name; }; 

如果我们谈论的是普通索引,将进行编辑。如果不考虑变量的类型,则可以学习给出有害的警告以及有用的警告(对于std :: string)。

类声明通常在标头.h文件中找到。因此,我们需要执行预处理以获取所有必要的信息。这对于C和C ++非常非常重要。

如果有人说您可以不用预处理就可以做,那么他要么是char脚,要么就是对C或C ++语言不太熟悉。

要收集所有必要的信息,您需要进行适当的预处理。为此,您需要知道在哪里以及哪些头文件位于哪里,在构建过程中设置了哪些宏。为此,您需要知道如何构建特定的cpp文件。

这就是问题所在。您不仅可以获取并编译文件(或者,指定编译器的密钥,以便它生成经过预处理的文件)。您需要弄清楚该文件如何编译。此信息在构建脚本中,但是在一般情况下,这是从那里获取信息的方法,该任务非常困难。



此外,GitHub上的许多项目都一团糟。如果从那里进行一个抽象项目,则通常必须对其进行修补才能进行编译。缺少某些库,需要手动找到并下载。它使用某种samopisny组装系统,必须对其进行处理。也许什么有时,下载的项目原则上会拒绝汇编,需要“使用文件进行修改”。无需简单地获取项目并自动获取.cpp文件的预处理(.i)表示形式。这甚至可能很难手动完成。

您可以说,非组合项目的问题是可以理解的,但并不可怕。让我们仅处理那些可以组装的项目。无论如何,预处理特定文件的任务仍然存在。而且,对于将要使用某些专用编译器的情况,例如嵌入式系统,我们将不予赘述。

通常,所描述的问题不是不可克服的。然而,所有这些在任何情况下都是非常困难且耗时的。对于C和C ++,仅GitHub上的源代码不会产生任何结果。要学习如何自动启动编译器,需要完成大量工作。

注意事项。如果读者仍然不理解这些问题,那么我们建议他进行以下实验。从GitHub上获取十个中型C ++项目,并尝试对其进行编译,然后为.cpp文件获取其预处理版本。之后,关于此任务的复杂性的问题将消失:)。

其他语言可能也有类似的问题,但是在C和C ++中,它们特别明显。

第六个细微差别。消除误报的代价。

静态分析仪容易产生误报,并且必须不断完善诊断以减少其数量。

让我们回到先前讨论的V789诊断显示基于范围的for循环中的容器更改。假设我们在开发过程中不够谨慎,并且客户报告了误报。他写道,在修改容器后,循环结束时,分析器没有考虑方案,因此没有麻烦。他给出了以下代码示例,其中分析器产生了误报:

 std::vector<int> numbers; .... for (int num : numbers) { if (num < 5) { numbers.push_back(0); break; // , , return } } 

是的,这是一个缺陷。在经典分析仪中,它的消除是非常快速和廉价的。在PVS-Studio中,此异常的实现包含26行代码。

当分析仪基于学习算法构建时,可以消除这种缺陷。当然,您可以通过制作数十个或数百个应视为正确的代码示例来完成此操作。

再一次,问题不是可行性,而是实用性。有一种怀疑是,在ML的情况下,打击打扰客户的特定误报的成本要高得多。在消除误报方面的客户支持将花费更多的钱。

第七个细微差别。很少使用的功能和长长的尾巴。

以前,曾考虑过高度专业化的语言的问题,为此可能没有足够的学习源代码。很少使用的功能(系统,WinAPI,来自流行的库等)的类似问题。

如果我们在谈论诸如strcmp之类的C语言功能,那么就有学习的基础。GitHub,可用代码结果:

  • strcmp-40,462,158
  • 斯特里姆-1,256,053

是的,有许多使用示例。分析器也许会学会注意例如以下模式:
  • 如果仅将字符串进行比较,这很奇怪。这已得到纠正。
  • 如果其中一个指针为NULL,则很奇怪。这已得到纠正。
  • 奇怪的是没有使用此函数的结果。这已得到纠正。
  • 依此类推。

一切都很棒吗? 不行在这里,我们面临着“长尾巴”。简而言之,“长尾巴”的实质如下。在书店里只出售最受欢迎和当前阅读的前50种图书是不切实际的。是的,每本这样的书被收购的频率是未在此清单上的书的100倍。但是,大部分收入将来自其他书籍,正如他们所说,这些书籍可以找到读者。例如,在线商店Amazon.com从超过13万种“最受欢迎商品”中获利超过一半。

有流行的功能,但很少。有一些不受欢迎的,但是有很多。例如,字符串比较功能仍然有以下几种:

  • g_ascii_strncasecmp-35,695
  • lstrcmpiA-27,512
  • _wcsicmp_l-5,737
  • _strnicmp_l-5,848
  • _mbscmp_l-2,458

如您所见,它们的使用频率要低得多,但是在使用它们时您仍然会犯同样的错误。很少有例子可以识别模式。但是,不能忽略这些功能。另外,它们很少使用,但总的来说,已经编写了许多对检查有用的代码。这就是“长尾巴”显现出来的地方。

在PVS-Studio中,我们手动注释功能。例如,对于C和C ++,当前大约标记了7200个功能。标记须遵守:

  • Winapi
  • C标准库
  • 标准模板库(STL),
  • glibc(GNU C库)
  • t
  • 制造商
  • zlib
  • libpng
  • Openssl

一方面,这似乎是一个死胡同。注释所有内容都是不可能的。另一方面,它起作用。

现在的问题。 ML有什么好处?虽然没有明显的优势,但是有困难。

可以说,基于ML本身的算法将揭示具有常用功能的模式,而不必将其标记出来。是的,这是真的。但是,独立标记诸如strcmpmalloc这样的流行函数没有问题

长长的尾巴开始出现问题。您可以通过综合示例进行训练。但是,在这里,我们返回到本文的那一部分,我们认为,那么编写经典的诊断程序比生成许多示例要容易和快捷。

以例如_fread_nolock之类的功能为例当然,它的使用频率比fread但是使用它时,您可能会犯同样的错误。例如,缓冲区应该足够大。此大小应不小于第二个和第三个参数相乘的结果。也就是说,我要检测这样的错误代码:

 int buffer[10]; size_t n = _fread_nolock(buffer, size_of(int), 100, stream); 

这是PVS-Studio中此功能的注释:

 C_"size_t _fread_nolock" "(void * _DstBuf, size_t _ElementSize, size_t _Count, FILE * _File);" ADD(HAVE_STATE | RET_SKIP | F_MODIFY_PTR_1, nullptr, nullptr, "_fread_nolock", POINTER_1, BYTE_COUNT, COUNT, POINTER_2). Add_Read(from_2_3, to_return, buf_1). Add_DataSafetyStatusRelations(0, 3); 

乍一看,这样的注释可能看起来很复杂,但是实际上,当您开始编写它们时,它变得很简单。另外,请记住,这是只写代码。写了,忘了。注释很少更改。

现在让我们从ML谈起这个功能。GitHub不会帮助我们。大约有15,000个对此功能的引用。甚至没有那么明智的代码。搜索结果的很大一部分是相似的:

 #define fread_unlocked _fread_nolock 

有哪些选择?

  1. 什么都不做 这是通往无处可去的道路。
  2. 仅通过为此函数编写数百个示例来培训分析器,以便分析器了解缓冲区大小与其他参数之间的关系。是的,您可以这样做,但这在经济上是不合理的。这是通往无处可去的道路。
  3. , , . , . ML :). .

如您所见,ML和很少使用的函数的长尾未合并。

在这里,与ML主题相关的人们反对我们,并说当分析器研究功能并得出有关其工作的结论时,我们没有考虑该选项。在这里,显然,我们要么不了解专家,要么他们不了解我们。

功能体可能未知。例如,它可能是与WinAPI相关的功能。如果这是一个很少使用的功能,那么分析仪如何理解它的作用?我们可以想象,在培训期间,分析仪将使用Google本身,查找功能的说明,阅读并理解它。此外,有必要从文档中得出高级结论。在_fread_nolock的描述中关于缓冲区大小,第二个和第三个参数之间的关系,没有任何地方说过。必须根据对编程的一般原理以及C ++语言的工作原理的理解,由人工智能独立地进行这种比较。在这一切看来,很可能在20年

后就可以这么认为了,虽然有很多机构可以使用,但可能没有任何意义。考虑像memmove这样的函数。它通常是这样实现的:

 void *memmove (void *dest, const void *src, size_t len) { return __builtin___memmove_chk(dest, src, len, __builtin_object_size(dest, 0)); } 

__builtin___memmove_chk是什么?这是编译器本身实现的固有功能。此功能没有源代码。

memmove可能看起来像这样:汇编程序中遇到第一个版本。您可以教分析器了解不同的汇编程序变体,但是有些事情是不正确的。

好吧,有时功能实体确实很有名。此外,用户代码中的功能主体也是已知的。在这种情况下,似乎ML通过阅读和理解所有这些功能的作用而获得了巨大的优势。

但是,在这种情况下,我们也充满了悲观情绪。这个任务太复杂了。一个人很难。请记住,您很难理解未编写的代码。如果这对人类来说很困难,那么为什么对AI来说这项任务就容易了?实际上,人工智能在理解高级概念方面存在很大的问题。如果我们要讨论的是理解代码,那么我们就不能不从实现细节中进行抽象并在较高层次上考虑算法。看来您可以在这里再次推迟20年的讨论。

其他细微差别

也应考虑其他几点,但是我们没有仔细考虑。而且这篇文章已经拖延了。因此,我们简要列出了更多细微差别,让读者思考。
  • . , , . , - . . C++ auto_ptr . unique_ptr .
  • . , C C++ , . , . , . , long Windows 32/64 32 . Linux 32/64 . , . -. , , . , ( ). , .
  • . ML, , , . 即 , — , , . , . , , — , . . , / , , , . . : " PVS-Studio: ". , , .


我们不否认机器学习的前景,包括静态代码分析的任务。可能会在错别字搜索问题,过滤错误消息,搜索新的(尚未在任何地方描述)错误模式等方面使用ML。但是,我们绝对不同意代码分析问题中有关ML的文章所充满的乐观态度。

在本文中,我们描述了以ML为基础必须解决的几个问题。所描述的细微差别在很大程度上否定了新方法的优点;此外,用于实施分析仪的旧的经典方法更有利且在经济上可行。

有趣的是,机器学习方法的拥护者的文章没有提到这些陷阱。但是,这并不奇怪。ML主题现在过于夸张,令人感到奇怪的是,它的辩护者期望在静态代码分析任务中对其适用性进行平衡评估。

从我们的角度来看,机器学习将在静态分析器中使用的技术中占据一席之地,同时对控制流进行分析,符号计算等等。

静态分析方法可以从ML的实现中受益,但不会夸大该技术的功能。

聚苯乙烯


由于该文章通常是至关重要的,因此有人可能会认为我们担心新内容以及Luddites如何对抗ML,担心失去静态分析工具的市场。

卢迪特独角兽


不,我们不害怕。在PVS-Studio代码分析器的开发中,我们只是没有理由花钱购买低效的方法。我们将以一种或另一种形式采用ML。而且,某些诊断程序已经包含了自学习算法的元素。但是,我们绝对会非常保守,只采用明显比基于循环和if-ah :)的经典方法效果更好的方法。毕竟,我们需要创建一个有效的工具,而不是制定赠款:)。

写这篇文章的原因是,对于所讨论的主题提出了越来越多的问题,我想写一篇有答案的文章,将所有内容都放在原处。

谢谢您的关注。并且我们提供了熟悉的文章“ 将PVS-Studio静态代码分析器引入开发过程的原因”



如果您想与讲英语的读者分享这篇文章,请使用以下链接:Andrey Karpov,Victoria Khanieva。机器学习在程序源代码静态分析中的应用

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


All Articles