从语音识别到医学诊断,机器学习已在各个领域牢牢扎根。 这种方法的普及是如此之大,以至于人们尝试在任何可能的地方使用它。 用神经网络代替经典方法的一些尝试失败了。 这次,我们将在创建有效的静态代码分析器以发现错误和潜在漏洞方面考虑机器学习。
经常会问PVS-Studio团队我们是否要开始使用机器学习来查找软件源代码中的错误。 简短的答案是肯定的,但在一定程度上是有限的。 我们相信,在机器学习中,代码分析任务存在很多陷阱。 在本文的第二部分,我们将介绍它们。 让我们从回顾新的解决方案和想法开始。
新方法
如今,有许多基于或使用机器学习的静态分析器,包括深度学习和用于错误检测的NLP。 发烧友不仅在机器学习潜能上加倍,而且在大型公司(例如Facebook,Amazon或Mozilla)上也加倍。 有些项目不是成熟的静态分析器,因为它们只能在提交中发现某些错误。
有趣的是,几乎所有产品都被定位为改变游戏规则的产品,由于人工智能,它们将在开发过程中取得突破。
让我们看一些著名的例子:
- 深码
- 推断,Sapienz,SapFix
- 包容
- 来源{d}
- 聪明的提交,提交助理
- 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能够找到与空指针取消引用和内存泄漏有关的错误。 推断基于Hoare的逻辑,分离逻辑和双拐行为以及抽象解释理论。 通过使用这些方法,分析人员可以将程序分解为多个块,并进行独立分析。
您可以在项目上尝试使用Infer,但开发人员警告说,尽管在Facebook项目中,它会生成约80%的有用警告,但不能保证其他项目上的误报率较低。 这是到目前为止Infer尚无法检测到的一些错误,但是开发人员正在努力实现这些警告:
- 数组索引超出范围;
- 类型转换异常;
- 未验证的数据泄漏;
- 比赛条件。
文字修复
SapFix是一种自动编辑工具。 它从测试自动化工具Sapienz和Infer静态分析器接收信息。 根据最近的更改和消息,Infer选择了几种修复错误的策略之一。
在某些情况下,SapFix会回滚所有更改或部分更改。 在其他情况下,它试图通过从其固定模式集中生成补丁来解决该问题。 该集合是由程序员自己从已经制定的一组修复程序中收集的修复程序模式组成的。 如果这种模式不能解决错误,则SapFix会尝试通过在抽象语法树中进行一些小的修改来适应情况,直到找到可能的解决方案为止。
但是,一个潜在的解决方案还不够,因此SapFix基于以下几点收集了多个解决方案:是否存在编译错误,是否崩溃,是否引入了新的崩溃。 一旦对编辑进行了全面测试,程序员将检查补丁,由程序员决定哪个编辑最能解决问题。
包容
Embold是一个用于静态分析软件源代码的重命名之前的启动平台。 静态分析器基于工具自身的诊断程序以及内置分析器(例如Cppcheck,SpotBugs,SQL Check等)运行。
除了诊断本身之外,该平台还专注于代码库负载的生动信息图表,方便查看发现的错误,以及搜索可能的重构。 此外,此分析器还具有一组反模式,使您可以在类和方法级别检测代码结构中的问题,并使用各种度量来计算系统质量。
主要优点之一是提供解决方案和编辑的智能系统,除常规诊断程序外,该系统还基于有关先前更改的信息检查编辑。
使用NLP,Embold可以将代码分开,并搜索函数和方法之间的互连和依赖关系,从而节省了重构时间。
这样,Embold基本上可以通过各种分析器以及它自己的诊断程序(它们中的某些诊断程序是基于机器学习的)来方便地可视化源代码分析结果。
来源{d}
与我们已经分析过的分析器相比,就其实现方式而言,源{d}是最开放的工具。 这也是一个
开放源代码解决方案 。 在他们的网站上,可以用您的邮件地址作为交换,您可以获取描述他们使用的技术的产品传单。 此外,该网站还提供了与机器学习用法相关出版物的数据库
链接,以进行代码分析,以及带有数据集的
存储库 ,用于基于代码的学习。 产品本身是一个用于分析源代码和软件产品的完整平台,并且不针对开发人员,而是针对管理人员。 它的功能包括计算技术债务规模,开发过程中的瓶颈以及该项目的其他全局统计信息。
他们通过机器学习进行代码分析的方法基于自然假设,如文章“
软件的自然性 ”中所述。
“从理论上讲,编程语言是复杂,灵活且功能强大的,但是实际的人实际上编写的程序大多是简单且相当重复的,因此它们具有有用的可预测统计属性,可以在统计语言模型中捕获并用于软件工程。任务。”基于此假设,代码库越大,统计属性越大,通过学习获得的度量标准越准确。
为了分析源{d}中的代码,使用了Babelfish服务,该服务可以解析任何可用语言的代码文件,获取抽象语法树并将其转换为通用语法树。
但是,源{d}不会在代码中搜索错误。 基于在整个项目上使用ML的树,源{d}检测代码格式,在项目中和提交中应用的样式。 如果新代码与项目代码样式不符,则会进行一些编辑。
学习重点在于几个基本元素:空格,制表符,换行符等。
在其出版物中阅读有关此内容的更多信息:“
样式分析器:使用可解释的无监督算法修复代码样式不一致 ”。
总而言之,源{d}是一个广泛的平台,用于收集有关源代码和项目开发过程的各种统计信息:从开发人员的效率计算到代码审查的时间成本。
聪明的承诺
Clever-Commit是由Mozilla与Ubisoft合作创建的分析器。 它基于Ubisoft及其子产品Commit Assistant进行的
CLEVER (错误预防和解决技术的组合级别)研究,该研究检测可能包含错误的可疑提交。 由于CLEVER基于代码比较,因此它既可以指向危险代码,也可以为可能的编辑提供建议。 根据描述,Clever-Commit在60-70%的情况下会发现问题所在,并以相同的概率提供正确的编辑。 通常,关于该项目及其能够找到的错误的信息很少。
CodeGuru
最近,来自亚马逊的产品CodeGuru已与使用机器学习的分析仪相一致。 它是一种机器学习服务,可让您查找代码中的错误以及确定其中的昂贵区域。 到目前为止,该分析仅适用于Java代码,但作者承诺将来会支持其他语言。 尽管它是在最近宣布的,但AWS(Amazon Web Services)首席执行官Andy Jassy表示,它已经在Amazon中使用了很长时间。
该网站称CodeGuru正在Amazon代码库以及10,000多个开源项目上学习。
基本上,该服务分为两个部分:使用搜索关联规则并查找代码错误的CodeGuru Reviewer和用于监视应用程序性能的CodeGuru Profiler。
通常,关于此项目的可用信息很少。 如网站所述,Reviewer分析Amazon代码库并搜索包含AWS API调用的拉取请求,以了解如何捕获与“最佳实践”的偏差。 接下来,它查看所做的更改并将它们与文档中的数据进行比较,并同时进行分析。 结果就是“最佳实践”模型。
还可以说,在收到用户代码的建议后,对它们的建议往往会有所改善。
Reviewer响应的错误列表相当模糊,因为尚未发布任何特定的错误文档:
- AWS最佳实践
- 并发
- 资源泄漏
- 机密信息泄漏
- 一般的“最佳做法”编码
我们的怀疑
现在让我们从我们的团队的角度考虑错误搜索,该团队已经开发了很多年的静态分析仪。 我们看到了许多高级的学习方法应用问题,我们将介绍这些问题。 首先,我们将所有ML方法分为两种类型:
- 那些通过合成和实际代码示例手动教静态分析器来搜索各种问题的人员;
- 那些在大量开源代码和修订历史(GitHub)上教授算法的人员,之后分析器将开始检测错误,甚至提供编辑。
我们将分别讨论每个方向,因为它们具有不同的缺点。 在那之后,我认为,读者将会明白为什么我们不否认机器学习的可能性,但仍然没有分享热情。
注意事项 我们从开发通用静态通用分析器的角度来看。 我们专注于开发任何团队都可以使用的分析器,而不是专注于特定代码库的分析器。
静态分析仪的手动教学
假设我们要使用ML开始寻找代码中的以下类型的缺陷:
if (A == A)
将变量与自身进行比较很奇怪。 我们可以编写许多正确和不正确的代码示例,并教会分析仪搜索此类错误。 此外,您可以将已经发现的错误的真实示例添加到测试中。 好吧,问题是在哪里可以找到这样的例子。 好吧,让我们假设这是可能的。 例如,我们有许多此类错误的示例:
V501 ,
V3001 ,
V6001 。
那么可以通过使用ML算法来识别代码中的此类缺陷吗? 是的,是的。 问题是-我们为什么需要它?
看,要教分析器,我们需要花费大量的精力来准备用于教学的示例。 另一个选择是标记实际应用程序的代码,指示分析仪必须发出警告的片段。 在任何情况下,都需要完成很多工作,因为应该有成千上万的学习示例。 或成千上万。
毕竟,我们不仅要检测(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("Oh boy! Holy cow!", left, 501, Level_1, "CWE-571"); } }
就是这样! 您不需要ML的任何示例基础!
将来,诊断人员必须学会考虑许多例外情况,并发出(A [0] == A [1-1])警告。 众所周知,可以很容易地对其进行编程。 相反,在这种情况下,以示例为基础会变得很糟糕。
请注意,在两种情况下,我们都需要一个测试系统,文档等。 至于创建新诊断程序所需的人工,经典方法是在代码中严格编程规则。
好的,现在该是另一个规则了。 例如,必须使用某些函数的结果的那个。 调用它们而不使用它们的结果是没有意义的。 以下是一些此类功能:
这就是PVS-Studio
V530诊断程序的工作。
因此,我们想要的是检测对此类函数的调用,其结果未使用。 为此,您可以生成许多测试。 而且我们认为一切都会很好。 但是同样不清楚为什么需要它。
除所有异常外,V530诊断实施在PVS-Studio分析仪中使用了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;
该示例摘自文章“
Chromium:内存泄漏 ”。 如果条件
(pkey.n0inv == 0)为true,则函数退出而不释放缓冲区,该缓冲区的指针存储在
n变量中。
从PVS-Studio的角度来看,这里没有什么复杂的。 分析器研究了
BnNew函数,并记住它返回了一个指向已分配内存块的指针。 在另一个函数中,它注意到缓冲区可能无法释放,并且在退出该函数时会丢失指向该缓冲区的指针。
这是跟踪值工作的常用算法。 编写代码的方式无关紧要。 与指针工作无关的函数中还有什么没关系。 该算法具有通用性,并且V773诊断程序会在各种项目中发现很多错误。 看看检测到错误的
代码片段有多不同!
我们不是ML方面的专家,但是我们感觉到大问题就在眼前。 您可以使用多种方式编写带有内存泄漏的代码。 即使机器很好地学习了如何跟踪变量的值,它也需要了解对函数的调用。
我们怀疑这将需要很多示例来学习,以至于任务变得无法掌握。 我们并不是说这是不现实的。 我们怀疑创建分析仪的成本是否会得到回报。
类比 我想到的是一个类似于计算器的计算器,在计算器中必须编写算术动作而不是诊断。 我们确信您可以教给基于ML的计算器,通过向其提供操作1 +1 = 2、1 + 2 = 3、2 +1 = 3、100 + 200 = 300等的结果来对数字求和。 如您所知,开发这种计算器的可行性是一个大问题(除非为其分配了赠款:)。 使用代码中的简单操作“ +”可以编写一个更简单,更快,更准确和可靠的计算器。
结论好吧,这种方法可以解决。 但是我们认为使用它没有实际意义。 开发将更加耗时,但结果-可靠性和准确性将降低,尤其是在基于数据流分析实现复杂的诊断时。
学习大量的开源代码
好的,我们整理了人工合成的示例,但还有GitHub。 您可以跟踪提交历史记录并推断代码更改/修复模式。 然后,您不仅可以指向可疑代码的片段,甚至可以提出修复代码的方法。
如果您停止在此详细信息级别,那么一切看起来都会很好。 一如既往,魔鬼在细节中。 因此,让我们谈谈这些细节。
第一个细微差别。 资料来源。GitHub的编辑非常随机且多样。 人们通常懒于进行原子提交并同时在代码中进行多次编辑。 您知道它是如何发生的:您将修复该错误,并同时对其进行一些重构(“在这里,我将添加对这种情况的处理...”)。 甚至一个人都可能无法理解,无论这些固定的彼此是否相关。
面临的挑战是如何通过添加新功能或其他方法来区分实际错误。 当然,您可以得到1000个人来手动标记提交。 人们必须指出:这里的错误已修复,这里的重构,这里的一些新功能,这里的需求已发生变化,等等。
这样的标记可能吗? 是的 但是请注意欺骗的发生速度。 我们已经在讨论如何长时间困扰数百人了,而不是“该算法基于GitHub进行学习”。 创建该工具的工作和成本急剧增加。
您可以尝试自动确定错误的修复位置。 为此,您应分析对提交的注释,注意小的本地编辑,这很可能是那些非常小的错误修复。 很难说出您可以自动搜索错误修复的程度。 无论如何,这是一项艰巨的任务,需要单独的研究和编程。
因此,我们甚至还没有开始学习,并且已经存在一些细微差别:)。
第二个细微差别。 发展滞后。将基于GitHub等平台学习的分析器将始终遭受“心理迟缓”之类的综合症。 这是因为编程语言会随着时间而变化。
从C#8.0开始,
出现了Nullable引用类型,这有助于对抗Null引用异常(NRE)。 在JDK 12中,出现了一个新的切换运算符(
JEP 325 )。 在C ++ 17中,可以执行编译时条件构造(
constexpr if )。 依此类推。
编程语言在不断发展。 而且,像C ++这样的程序发展很快。 出现新的结构,添加新的标准功能,等等。 除了新功能外,我们还希望通过静态代码分析来识别新的错误模式。
此时,ML方法面临一个问题:错误模式已经很清楚,我们希望检测到它,但是没有学习的代码库。
让我们用一个特定的例子来看这个问题。 基于范围的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中出现已有八年了。 但是到目前为止,在我们的数据库中,只有
三种情况出现这种错误。 三个错误不是很多,也不是少数。 不应从这一数字得出任何结论。 最主要的是要确认这种错误模式是真实的,并且有必要进行检测。
现在,例如,将此数字与以下错误模式进行比较:
指针在check之前被取消引用 。 在检查开源项目时,我们总共已经确定了1,716个此类案例。
也许我们根本不应该在基于范围的for循环中寻找错误? 不行 只是程序员是惯性的,而此运算符的普及速度非常缓慢。 逐渐地,将分别有更多的代码和错误。
这很可能在C ++ 11出现后仅10-15年发生。 这导致了一个哲学问题。 假设我们已经知道错误模式,我们将等很多年,直到开源项目中出现很多错误为止。 会这样吗?
如果为“是”,则可以安全地诊断所有基于ML的分析仪的“心理发育延迟”。
如果“否”,我们该怎么办? 没有例子。 手动写吗? 但是通过这种方式,我们回到了上一章,在该章中,当人们编写一整套用于学习的示例时,我们对该选项进行了详细描述。
可以做到,但是权宜之计再次出现。 在PVS-Studio分析仪中实施V789诊断的所有例外情况仅需要118行代码,其中13行是注释。 也就是说,这是一个非常简单的诊断,可以通过经典方式轻松进行编程。
这种情况将类似于以任何其他语言出现的任何其他创新。 正如他们所说,有一些事情要考虑。
第三个细微差别。 文献资料任何静态分析仪的重要组成部分都是描述每种诊断的文档。 没有它,使用分析仪将非常困难或不可能。 在PVS-Studio
文档中 ,我们对每个诊断进行了描述,其中提供了错误代码及其修复方法的示例。 我们还提供了
CWE的链接,您可以在其中阅读另一种问题描述。 而且,有时用户听不懂某些内容,他们问我们一些问题。
对于基于ML的静态分析器,文档问题被以某种方式掩盖了。 假定分析仪将仅指向对其似乎可疑的地方,甚至可能会建议如何修复它。 是否进行编辑取决于个人。 那就是麻烦开始的地方。。。如果不阅读就很难做出决定,这使得分析器似乎对代码中的特定位置产生了怀疑。
当然,在某些情况下,一切都会很明显。 假设分析器指向以下代码:
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); }
并建议我们更改int返回值的char类型:
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();
无需更改对象并重写它,而是创建一个新对象并将其序列化。
没有问题的描述。 没有文档。 代码变得更长了。 由于某种原因,将创建一个新对象。 您准备好在代码中进行这样的编辑了吗?
您会说不清楚。 确实,这是无法理解的。 一直如此。 为了理解为什么分析仪不喜欢任何东西,使用这样的“静音”分析仪将是一项无休止的研究。
如果有文档,一切将变得透明。 用于序列化的类
java.io.ObjectOuputStream缓存写入的对象。 这意味着同一对象不会被序列化两次。 该类一次将对象序列化,第二次只将对同一第一个对象的引用写入流中。 阅读更多:
V6076-循环序列化将使用第一次序列化中的缓存对象状态。
我们希望我们能够解释文档的重要性。 问题来了。 基于ML的分析器的文档将如何显示?
当开发经典的代码分析器时,一切都变得简单明了。 有一种错误模式。 我们在文档中对其进行描述并实施诊断。
如果是ML,则过程相反。 是的,分析仪可以注意到代码中的异常并指向它。 但是它对缺陷的本质一无所知。 它不理解,也不会告诉您为什么不能编写这样的代码。 这些都是太高级的抽象。 这样,分析仪还应该学习阅读和
理解功能文档。
正如我所说,由于有关机器学习的文章中避免了文档问题,因此我们不准备进一步讨论它。 这只是我们所说的另一个细微差别。
注意事项 您可能会认为文档是可选的。 分析人员可以参考GitHub上的许多修复示例,而该人员可以通过查看对它们的提交和注释来了解什么。 是的,是这样。 但是这个想法看起来并不吸引人。 分析器在这里是坏家伙,宁可让程序员困惑也不愿帮助他。
第四点细微差别。 高度专业化的语言。所描述的方法不适用于高度专业化的语言,对于这些语言而言,静态分析也可能非常有用。 原因是GitHub和其他资源根本没有足够大的源代码库来提供有效的学习。
我们来看一个具体的例子。 首先,让我们转到GitHub并搜索流行的Java语言的存储库。
结果:语言:“ Java”:
3,128,884个可用的存储库结果
现在,使用俄罗斯
1C公司生产的会计应用程序中使用的专用语言“ 1C企业”。
结果:语言:“ 1C Enterprise”:
551个可用的存储库结果
也许这种语言不需要分析仪? 不,是。 实际需要分析此类程序,并且已经有合适的分析器。 例如,有由“
Silver Bullet ”公司生产的SonarQube 1C(BSL)插件。
我认为,对于专用语言为何ML方法将变得困难,因此无需进行任何具体说明。
第五个细微差别。 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; }
分析仪是否应该开始学习以便建议
(x ==“ y”)替换strcmp(x,“ y”)?
如果不知道如何在类中声明
m_name成员,就无法回答该问题。 例如,可能有以下选项:
class Class { .... char *m_name; }; class Class { .... std::string m_name; };
如果我们在谈论普通指针,将进行编辑。 如果我们不考虑变量类型,那么分析器可能会学会发出好和坏警告(对于
std :: string的情况)。
类声明通常位于头文件中。 这里需要执行预处理以获取所有必要的信息。 对于C和C ++而言,这非常重要。
如果有人说不用预处理就可以做,那么他要么是骗子,要么就是不熟悉C或C ++语言。
要收集所有必要的信息,您需要正确的预处理。 为此,您需要知道什么位置和什么头文件位于何处,以及在构建过程中设置了哪些宏。 您还需要知道如何编译特定的cpp文件。
那就是问题所在。 一个人不只是编译文件(或者,指定编译器的键,以便它生成预处理文件)。 我们需要弄清楚该文件是如何编译的。 该信息在构建脚本中,但是问题是如何从那里获取信息。 通常,任务很复杂。
而且,GitHub上的许多项目都是一团糟。 如果您从那里进行一个抽象项目,则通常必须进行修补才能对其进行编译。 有一天,您缺少图书馆,需要手动查找和下载。 改天使用某种自写的构建系统,必须对其进行处理。 可能是任何东西。 有时,下载的项目只是拒绝构建,因此需要进行某种调整。 您不能只是获取并自动获取.cpp文件的预处理(.i)表示形式。 即使手动进行,也可能会很棘手。
我们可以说,非建筑项目的问题是可以理解的,但不是至关重要的。 让我们仅处理可以构建的项目。 仍然有预处理特定文件的任务。 更不用说我们处理一些专用编译器的情况,例如,嵌入式系统。
毕竟,所描述的问题不是无法克服的。 然而,所有这些都是非常困难且劳动密集的。 对于C和C ++,位于GitHub上的源代码不执行任何操作。 有很多工作要做,以学习如何自动运行编译器。
注意事项 如果读者仍然没有深入了解问题,我们邀请您参加以下实验。 从GitHub取十个中型随机项目,并尝试对其进行编译,然后为.cpp文件获取其预处理版本。 之后,有关此任务的艰巨性的问题将消失:)。
其他语言可能也有类似的问题,但是在C和C ++中它们尤其明显。
第六个细微差别。 消除误报的代价。静态分析仪容易产生误报,因此我们必须不断完善诊断以减少误报的数量。
现在,我们将回到先前考虑的
V789诊断,检测基于范围的for循环内的容器更改。 假设我们在编写时不够谨慎,并且客户报告了误报。 他写道,在更改容器后循环结束时,分析器没有考虑这种情况,因此没有问题。 然后,他给出了以下代码示例,其中分析器给出了误报:
std::vector<int> numbers; .... for (int num : numbers) { if (num < 5) { numbers.push_back(0); break;
是的,这是一个缺陷。 在经典分析仪中,消除它的过程非常快速且廉价。 在PVS-Studio中,此异常的实现包含26行代码。
当分析仪基于学习算法时,也可以纠正此缺陷。 当然,可以通过收集数十个或数百个应视为正确的代码示例来进行教授。
同样,问题不是可行性,而是实用的方法。 我们怀疑,针对ML而言,与困扰客户的特定误报进行斗争的成本要高得多。 也就是说,就消除误报而言,客户支持将花费更多的钱。
第七个细微差别。 很少使用的功能和长尾巴。以前,我们一直在解决高度专业化的语言的问题,因为对于这些语言来说,足够的语言无法学习。 很少使用的功能(系统功能,WinAPI,流行的库等)也会发生类似的问题。
如果我们在用C语言(如
strcmp)谈论此类功能,那么实际上是学习的基础。 GitHub,可用代码结果:
- strcmp-40,462,158
- 斯特里姆-1,256,053
是的,有许多用法示例。 分析器也许会学会注意例如以下模式:
- 如果将字符串与自身进行比较,这很奇怪。 它得到修复。
- 如果指针之一为NULL,这很奇怪。 它得到修复。
- 奇怪的是没有使用此函数的结果。 它得到修复。
- 依此类推。
是不是很酷? 不行 在这里,我们面临“长尾巴”的问题。 在下面非常简短地说明“长尾巴”的观点。 在书店仅出售最受欢迎和现在已阅读的Top50书是不切实际的。 是的,例如,每本此类书籍的购买频率是未从该清单中购买的书籍的100倍。 但是,大部分收益将由他们找到读者的其他书籍组成。 例如,在线商店Amazon.com从130,000种“最受欢迎的商品”之外获得了超过一半的利润。
流行的功能很少。 有不受欢迎的人,但有很多人。 例如,字符串比较功能具有以下变体:
- g_ascii_strncasecmp-35,695
- lstrcmpiA-27,512
- _wcsicmp_l-5,737
- _strnicmp_l-5,848
- _mbscmp_l-2,458
- 和其他。
如您所见,它们的使用频率降低了很多,但是当您使用它们时,您可能会犯同样的错误。 很少有例子可以识别模式。 但是,这些功能不能忽略。 个别情况下,很少使用它们,但是使用时会编写很多代码,最好检查一下。 那就是“长尾巴”显示出来的地方。
在PVS-Studio中,我们手动注释功能。 例如,到目前为止,已经为C和C ++注释了大约7,200个函数。 这是我们标记的内容:
- Winapi
- 标准C库,
- 标准模板库(STL),
- glibc(GNU C库)
- t
- 制造商
- zlib
- libpng
- Openssl
- 和其他。
一方面,这似乎是一种死胡同。 您无法注释所有内容。 另一方面,它起作用。
现在是问题。 机器学习有什么好处? 虽然优势并不明显,但是您可以看到它的复杂性。
您可能会争辩说,基于ML本身构建的算法将找到具有常用功能的模式,而不必注释它们。 是的,是的。 但是,独立注释诸如
strcmp或
malloc这样的流行函数没有问题。
但是,长长的尾巴会引起问题。 您可以通过综合示例进行教学。 但是,这里我们回到文章部分,在那儿我们说的是编写经典诊断程序比生成许多示例更容易,更快。
以一个函数为例,例如
_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
有哪些选择?
- 什么都不要做 这是无处可去的方法。
- 试想一下,通过为一个函数编写数百个示例来教给分析器,以使分析器了解缓冲区和其他参数之间的相互关系。 是的,您可以这样做,但这在经济上是不合理的。 这是一条死胡同。
- 当功能的注释将手动设置时,您可以想出与我们类似的方法。 这是一种明智的好方法。 那只是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年。其他细微差别还应考虑其他方面,但是我们没有深入研究它们。顺便说一下,这篇文章很长。因此,我们将简要列出其他一些细微差别,以供读者反思。- Outdated recommendations. As mentioned, languages change, and recommendations for their use change, respectively. If the analyzer learns on old source code, it might start issuing outdated recommendations at some point. Example. Formerly, C++ programmers have been recommended using auto_ptr instead of half-done pointers. This smart pointer is now considered obsolete and it is recommended that you use unique_ptr .
- Data models. At the very least, C and C++ languages have such a thing as a data model . This means that data types have different number of bits across platforms. If you don't take this into account, you can incorrectly teach the analyzer. For example, in Windows 32/64 the long type always has 32 bits. But in Linux, its size will vary and take 32/64 bits depending on the platform's number of bits. Without taking all this into account, the analyzer can learn to miscalculate the size of the types and structures it forms. But the types also align in different ways. All this, of course, can be taken into account. You can teach the analyzer to know about the size of the types, their alignment and mark the projects (indicate how they are building). However, all this is an additional complexity, which is not mentioned in the research articles.
- Behavioral unambiguousness. Since we're talking about ML, the analysis result is more likely to have probabilistic nature. That is, sometimes the erroneous pattern will be recognized, and sometimes not, depending on how the code is written. From our experience, we know that the user is extremely irritated by the ambiguity of the analyzer's behavior. He wants to know exactly which pattern will be considered erroneous and which will not, and why. In the case of the classical analyzer developing approach, this problem is poorly expressed. Only sometimes we need to explain our clients why there is a/there is no analyzer warning and how the algorithm works, what exceptions are handled in it. Algorithms are clear and everything can always be easily explained. An example of this kind of communication: " False Positives in PVS-Studio: How Deep the Rabbit Hole Goes ". It's not clear how the described problem will be solved in the analyzers built on ML.
Conclusions
我们不否认ML方向的前景,包括它在静态代码分析方面的应用。 ML可能用于错别字查找任务,过滤误报,搜索新的(尚未描述的)错误模式等。但是,我们不同意在代码分析方面充斥于ML文章的乐观态度。在本文中,我们概述了如果他要使用ML则必须处理的一些问题。所描述的细微差别在很大程度上否定了新方法的好处。另外,分析仪实施的旧经典方法更有利可图,并且在经济上更可行。有趣的是,机器学习方法的拥护者文章没有提到这些陷阱。好吧,没什么新鲜的。ML引起了一定的炒作,也许我们不应该期望它的辩护者对ML在静态代码分析任务中的适用性进行平衡评估。从我们的角度来看,机器学习将填补静态分析器,控制流分析,符号执行等技术中的一小部分。静态分析的方法可能会从ML的引入中受益,但不要夸大该技术的可能性。聚苯乙烯
由于该文章通常是至关重要的,因此某些人可能会认为我们担心新内容,而Luddites则反对ML,因为它担心失去静态分析工具的市场。不,我们不害怕。在PVS-Studio代码分析器的开发中,我们只是看不出在低效方法上花钱的意义。无论采用哪种形式,我们都将采用ML。而且,某些诊断程序已经包含了自学习算法的元素。但是,我们绝对会非常保守,只采取明显比基于循环和ifs :)的经典方法更大的效果。毕竟,我们需要创建一个有效的工具,而不是进行赠款:)。写这篇文章的原因是,对该主题提出了越来越多的问题,我们希望有一个说明性的文章将所有内容都放在适当的位置。谢谢您的关注。我们邀请您阅读文章“为什么选择PVS-Studio静态分析仪以集成到您的开发过程中。”