相信每一个研发人员,在日常的代码开发工作中,都会遇到同一个问题: 我合的这个代码,会不会有问题?。 这是一个最常见的场景。从最尖端的星球探测器,到每一个运营的小型 Web 站点,其上运行的软件,都会遇到相同的问题,以前写的代码,现在写的代码,有没问题?。
进行代码静态分析、Check List 多个检查、各类检查规则、多个检测流水线、拉大佬做 Code Review……各类工具和办法都一起使用,目的都是相同的,都是为了在代码上线之前,尽早察觉缺陷,找出 bug,发现代码漏洞,防止影响线上功能。
但同时,这又是一个非常难以处理的场景,它包含的范围面广,并且检测技术实现上也有很大的困难。美国某款著名付费的代码静态分析软件,深耕于此二十多年,可见一斑。
市场上也有多款检查工具,几乎每一款都对外称,我们对 bug、对漏洞的检测有效率达到了 9X.XX%。它们有的免费,有的付费,但没有一点一款可以宣称,使用我们的软件检测代码漏洞和风险,有效率为 100%,您可以高枕无忧了,您可以完全放心合代码了。
甲之蜜糖,乙之砒霜。每个团队/业务都有一些自己的痛点,都有自己的关注问题。比如同一种开发语言 java,客户端团队和服务端团队,同样都是 java 语言,但关注的痛点是一定不同的。即使有非常庞大的 java 规则集,覆盖面全,但真实的操作起来,检测规则只要有特异性,或者稍微一复杂,稍微一组装,那么通用规则就很难适配了。
我们通过一段时间的观察和复盘发现,凡是业务方自己提出的问题、规则,往往都是最重要的痛点。是价值最高的,最被需要的,但是往往这种规则,在公司内外的工具里,不易找到,或者不存在。业务方往往消耗很多精力,来处理这件事情,下面的例子来做具体的解答。最痛的反而不好解决,这并不是个例。
假设某一个公共函数 f1 过期了,如果项目中再使用 f1 函数,将使您的项目面临风险,您负责要将项目中的 f1 全部找到并且删除。如果 f1 就是一个 static 的函数,那么您能够最终靠一行 linux grep,就全部找到了。【grep 方式】
此时,如果 f1 的名称恰好是 get(),而且 get 函数不是静态函数,而是对象实例函数,那么,f1 需要先初始化一个对象N n = new N,再通过 n.get() 进行调用,此时,您 ctrl + shift +f 启动 IDE 的全局搜索,搜出来一片 get,因 get 是一个太常见的函数命名,redis,大量的 bean 等,都有 get 方法,甚至注释里面也有很多get,所以此时无法确定是 N 的 get 方法。您变换了一个思路,此时搜对象类型 N,然后通过 N 慢慢找到 n.get()。【全局搜索方式】虽然麻烦点,不过也可以接受。
此时,如果有好几个 class 类型都叫做 N,只是它们的路径不一样,但是全局搜都是 N。好吧,已经有些麻烦了。您通过 IDE 中的搜索技巧,比如正则之类的,较为精确地搜索到了这个类型 N,或许也能搞定该问题。【搜索+正则方式】您花费了很多时间,找到了所有的 N n , n.get()的代码片段,进行了代码删除,处理了该风险。
此时,leader 说,新提交的代码,不能再有 N 和它的 get 了。【增量管控】好吧,即使您能够保证自己的 IDE 技巧可以支撑 leader 的要求,也没办法保证其他同学也这样精通 IDE。这只是一个 N 和它的 get,如果以后再有 O , P , Q,其他的呢?
此时,您发现,或许能够正常的使用 AST,公司内外工具找了很多,然后琳琅满目的 AST 工具,还有不同的 language,每一款都要先 clone 下来,试试效果,发现没现成的,只有一个接近的。【调研】
然后,您需要大体看一下项目源代码,让这款工具可以适配 N 和它的 get(),找到需要改动的地方,改后再编译运行一下,发现能运行。【二次开发】然后要监听代码增量,需要做一个小型系统,处理 git 的 merge 消息,在 merge 消息触达之后,做多元化的分析,观察增量代码是否有 N 和它的 get。【系统开发】
刚开始运行的还可以,结果后来有些大的代码文件,解析 AST 时,解析程序崩溃了,又要找 bug,这下麻烦了,解析 AST 那个函数的源代码,有一万多行,还要看如何正确地处理。【运维】
一套操作下来,差不多一个多月进去了。这个例子,是一个真实的例子,您在这里,能够正常的看到做代码检测的成本,以及您要自己完整开发一个规则,来为自己团队的质量保驾护航,那么需要付出的成本,我们已基本为您罗列了。当公司内外的工具、平台,没有 现成的功能、规则能够正常的使用,那么自己上手的成本可能会很高昂,尤其是遇到不容易搞的、很复杂的问题,现成的工具更不好找,甚至要做适配性的二次开发,那成本就更高了。
好不好,看疗效。若使用了大量的规则集,或者使用了大量的扫描工具,尽管扫描的面积变大,但是相对应的,扫描出来的问题也变多了,真正的业务关注的问题,或者潜在的巨大隐患,都被掩藏了。比如扫出来几百个问题,但是真正会导致故障的可能就 1 个。
规则不动,研发行为易动。以上描述的,大多是静态扫描相关。静态扫描工具容易陷入一个误区,就是脱离实际的 代码仓库、人员、团队、经验这种水土,变得千篇一律。
换句话说,一段代码无论在任何地方,只要代码语法 match 了规则,都会报出问题。比如一个简单的例子,某检测软件对于一个变量 x 的赋值,x=True,会提示错误,因为该检测软件要求为常量都大写,也就是 x 必须写成 X,但是在下面的代码将 x 重新赋值为 x=False,x 即具有变量性质。但仍然报错。
不少研发同学都有这样的开发习惯,这不能说错,或者说这不能算作一个一定要解决的问题。每次出现,都要点击忽略,将这样一些问题忽略掉。可见,规则是不动的,研发的行为是动态变化的。另外,小组成员的动态状况也会产生潜在的风险,而它是不能被 静态分析 所检测到的。
比如一个简单的例子,笔者是做大数据开发和后台开发,接触到的后端 language 比较多,但是如果项目需要,笔者要改一些网页,写 js 代码,那么必然对这个代码合入,信心不足,需要前端同学帮助 Code Review,来避免风险。
比如有位同学要合入的代码,解决的 conflict 比较多,那么这可能会产生风险,需要更加多的 CR 投入,来确保排除风险。因此,这些风险和隐患都具有动态性,并不能通过单纯代码级别的静态检测来解决。
如果一次扫描,启用了多款扫描工具,启用了上百个规则或者更多规则,我们实践过,这个耗时对一般的代码量微服务都是十分钟起步了,如果是更大的代码仓库,超时情况会加剧。业务合入代码时,是为了后续发布上线的,但是这边的检查一直未完成,不得不删减规则集,甚至跳过检查,那么检查的效果就缺失了。
还有一个重要的方面,就是有些扫描工具,是需要COMPILE的,依赖编译后的数据,比如依赖各语言的 bytecode,这种往往需要更加多的分析时间,有的需要小时级,有的不支持增量,这就会使得 原本优秀的检测工具,业务方不去选择。
代码有多种形态,在编辑文件里的,在 .py,.java,.kt 文件中的,是研发同学的源代码。
有些语言编译后形成的,是字节码。实际在操作系统中运行的,是机器码。但是它们从源头上,都是源代码的演化。一般编译器前端处理的,往往是 AST(抽象语法树),是一种有效而直观的代码语法组成的表达。
AST 一般由 代码原文件直接生成,市面上对应各种语言,都有一些 AST 解析工具,但是解析能力、速度、精确度各有不同。这里要注意的是,并不是所有的工具,都可以将一个代码原文件,翻译成为正确的 AST。
也就是 AST 有误解析的概率,并且概率不低,我们解析过百万个级别的代码文件,出错的、误解析的、解析不出来的,有很多。原因有以下几点:
\\1. AST 的维度是文件级别,也就是说,一般一个文件对应一个 AST,因此一般不具备 LINK 多文件的能力,这就使得一些 import / include 存在语义分歧,易引起误解析。
\\2. AST 的解析工具,有些偏重于解析精准度,这也代表着,输入必须是一个完整的代码文件,如果是缺失内容的代码文件,则会直接报错;有些则偏重于解析速度和容错率,即使输入是一小段不完整的代码,它们也会基于此,生成一部分 AST。比如解析至 static 关键字,代码片段就戛然而止,那么后面是一个 variable 类型,还是一个 class 类型?解析器没办法做出判断。
\\3. 有些解析器通过 prediction 的方式来构造 ATN,反复尝试路径,直到找到唯一路径。这样的方式下,若遇到大的代码文件,或者代码文件中有大的 列表、数组、字典变量的文本,解析器很容易因为暂存的路径太多,而导致程序内存耗尽,解析崩溃。
以上原因,会直接影响解析效果,因为 AST 都是有问题,那么 func = Rule(AST),对 AST 做相关操作,必然没办法得到正确的结论。
由上文可知,AST 的解析存在置信度问题,由于代码文件、解析工具、解析策略、运行时机器资源 等多种因素的作用下,AST 对于源代码的 struct 还原,有几率存在误差。所以对于代码的静态分析,对于以检索、匹配为主的,一般基于 AST 来进行。但是对于一些深度解析,比如指针、资源泄漏、线程问题,这种一般依赖 COMPILE IR 的能力,也就是源代码在编译期的表达。当然,也不是完全绝对的,比如 AST 在一些场景下,也可拿来做资源泄漏中的问题。虽然 COMPILE 的分析深度和准确度要好于 AST,但是代价比较大,分析时间和资源都要远高于 AST。首先编译的时间,无法省略,这是 IR 的原材料,然后 IR 的数据已将调用栈 Link 到一起,类、函数、依赖库之间的调用链能够完整获取,因此在分析时,需要分析的内容更多,链路更长,范围也更大。
同时,在做分析时,通常用符号化执行的方法,对路径进行约束问题求解,从而判定问题是不是存在,当调用链变长、依赖关系更复杂、函数深度更长时,这样符号执行图也会更大,所需要暂存的路径也更多,约束条件也更复杂,所需要推断判定的 Variable 和 CallFunc 也更多。符号化执行所需要的资源,甚至比程序真正进入 Runtime 运行程序所需要的资源更多,原因就来自于此。由于带来更多的时间和资源的消耗,很多分析工具在资源少的服务器上,甚至无法运行,能够运行的,需要等待很久。这使得 COMPILE IR 的分析方式容易被弃用。但是,这也就失去了很多风险能够提前发现的可能性。
首先要说明的是,以上问题,市场上并没有一款产品能够完美解决,还是那句线 多年,如果好解决,也不用这么久了。
即使是市场上的一些付费软件,扫出来的效果真能有它们宣传的多么好,也不见得。
而且对扫描出来的问题,即使都改了,软件的质量能提高多少,这也是一个比较难以回答的问题。
这并不是市场上和社区的软件做的不好,相反,如果您 clone 下来代码,研究几天会发现,技术复杂度属于很高的一类。而且这些软件都在付出很多的努力,为了更好的提高检测性能和准确度。比如,仅面对语言的 Version 升级,出了新的语法特性,它们去做适配,这就需要大量的工作。
对于上述痛点,是都会存在的现象,这并不是一个或几个工具,部署上去,就能解决的了。
对于上述痛点,我们的工作也围绕它们,做了一些研究,有部分成果,附在下面的各个典型案例中,均为我们的工具能检查到,而其他工具忽略的。更多的是对其他工具的一种有效补充。
使用自研的、二次开发的规则。通用规则具有局限性,且互相联动、合并、拆解的能力有限,自研的规则则具备较强的针对性。
与业务方进行深度沟通,挖掘痛点。暂时不追求规则的广泛性,更多强调定制化,强调解决方法,强调点的命中率,抛弃一些大而全。维护一个定制化的规则能力,若其他团队也遇到相似的问题,则小成本部署到其他团队。
经过大量的踩坑、试验,在多个语言上,我们基本摸索出了较为贴切的 AST 工具、AST 规则低成本映射能力、COMPILE 编译分析能力,在其上能够直接进行一定的源代码二次开发。因此,对于一般的需求,可以一个人力,一天开发,一天测试上线,基本就能完成。另外,我们通用化了 MR 增量判定、命中判定、风险通知等能力,这些不需要再进行开发,直接投入到规则本身即可。
采取的策略是,重点维护业务方认为是痛点、定制的规则,确保这种规则的命中率,只要出现,就尽可能捕捉到,若遗漏或者误报,则立刻投入做修改。对于通用的规则,如果业务方认为不必要,则做下线处理。
借助数仓数据,可以获取到仓库、团队、同学的历史数据,因此能构建出来一些动态数据,比如开发习惯、常用语言、经验度、熟悉度、擅长领域、源代码历史改动情况,这对推荐评审人、推荐关注人、提示老代码变更很有效。
我们已对风险分析的时效性做了较大提升。当代码 MR 合入的消息触达时,我们的检测流水线会触发,进行增量判定。对于 AST 类的检查,全流程大约只需要 40 秒。对于 COMPILE 类的检查,已经能够做增量检测,全流程大约只需要 编译时间(依赖编译,不可省略) + 2 分钟的时间。两大类检查,时效性目前都满足了要求。
使用的工具,都经过了源代码二次开发,有的动了底层的引擎源代码。这是应对工具自有缺陷的唯一有效方法。底层包有问题时,上层修复是徒劳,因此必须进入源码中进行解决。
深度解析并不是效果不好,相反,效果可能会很好。只是受限于使用较为复杂,使用耗时较大,使用时内存 cpu 容易出问题、还要指定编译环境和编译器,但这些都不是不可解,我们目前在 COMPILE 分析中,不仅有了增量代码的分析能力,也跟业务方互相配合,解决了如上问题。
主要分类 AST 和 COMPILE 两大类,每一类都附录检测到的典型案例。
以 AST 解析和 AST 规则为基础。整体功能为: 业务方的研发同学在 git 上新建或更新 MR - 消息自动触发到我们平台和流水线上 - 对增量代码进行 AST 构建、分析、规则加载执行 - 若检查出问题,发送给相关研发同学或企业微信群。
若为增量判定,则监听 git 仓库的 hook,使得 MR 在 init 或者 update 时,可以发出来消息。
构建检测流水线,获取此次 MR 的元数据信息,在流水线上启动容器,进行代码拉取等。
启动风险分析插件,执行规则判定。若有发现风险,则再次判定是否为增量风险,若为增量风险,则发出通知。
对于不同语言的代码,抽象出不同的 AST 数据结构,驱动引擎使用同一个,但语法模板使用不相同的解析模板。
将不同语言的 AST,转化成为语言无关的 Intermediate Language(IL)结构。
将 IL 结构形成 CFG,下面的规则判定等都依据 CFG,由于不区分语言,减少了后续规则加载的成本。
若需要跟进变量的值,比如判断变量的条件语句等,则使用变量追踪,将变量的 Data Flow 暂存下来。
如果变量在 Data Flow 中存在值的覆盖,则进行变量值覆盖。如果有多次的值覆盖,则记录多个 Data Flow。
加载规则,将规则应用于变量的 Data Flow 中,如果有任意 Data Flow 不能到达尾节点,则命中该规则。反之,则规则未命中。
反向查询 CFG Data Flow 的变量所在的代码行号,对于命中的节点,记录变量的信息并且报出风险问题。
现在对于大部分仓库的 MR,性能平均在 40 秒,绝大多数在 1 分钟以内。优化之前一般在 5~10 分钟左右。所以如果加载我们的扫描能力,大约一次 MR 的风险评估结论,40 秒左右就能制作出来。当然,如果变更的代码量较多,或者规则较复杂,使 CFG 的 path 过多,则可能减缓速度。我们在此前进行了多个性能调优的措施。
统一了 AST 执行引擎,这使得我们并不是特别需要使用多个工具。若使用多个工具,不仅 fork process 存在浪费,最重要的是多个工具做了大量重复性的工作,而且工具的性能很难保证,只要有慢的,就会拖慢整体的进度。另一方面,维护多个工具/引擎会增加很多成本,一个工具有问题,需要修改源代码,另外一个出问题,也需要修改源代码,但是无论修改的内容是什么,都无法确保修改后的工具,性能好于或者等于之前的情况。
由于 AST 是代码级别粒度,因此直接对增量的代码文件进行 AST 化,并且对结果再进行变更行的判定,最大限度减少 AST 的开销。
数据库 cursor 未关闭,涉及到资源,需要确保正确的使用和关闭。一般以下为正确的写法。在 try 块内使用,并且在 finally 中进行关闭,考虑到 close()也是一个抛异常的方法,因此也用 try catch 进行包裹。
该案例中,整个使用和关闭的操作在一个 for 循环内,在 c = cr.query(XXX) 时,或者 c.close()时,都有一定的概率会抛出异常,并且中断循环。
一般 sql 语句,包含 delete 等操作是没问题的,只要不出现 sql 注入等,并且在程序逻辑正确的前提下,不会对业务造成危险的影响。
或者,delete 表或者 drop 表的语句,在一些 sql 脚本中,而不在 java 程序中,只要项目同学能确定这些语句仅是用于记录、备份等,也不会有问题。
但是,如果是在 java 代码语句中,并且使用了 database 的句柄去执行这类语句,那么就可能会有较大风险。
这里看到,研发同学在该赋值上面,注释了一行代码,如果这行代码执行后,rId 确实可能不为 0,但是注释了这行代码,因此不排除误写或者 debug 之后忘记改回的可能性。这样的情况也是时有发生。
在特定场景中,有些函数往往成对出现。比如有一个 onX 函数,则对应就有一个 onY 函数。业务方对它们的要求是,如果修改了 onX,则需要确认 onY 是否也有必要进行修改。
曾经有故障的发生,是因为修改了 onX,却没有修改 onY。这属于特定的需求,个人会使用 AST 引擎来解决该问题。
以编译字节码解析和编译期规则为基础。如上文中描述,使用字节码 IR 做深度代码分析,比 AST 更能够识别出不容易发现的问题,下面将列举的案例中,其中有些,我们研发同学也不是一眼就可以发现问题,问题有着非常强的隐蔽性。由于它的耗时、性能消耗较高,所以它的易用性并不如 AST,但这不代表它没有意义。
整体功能为: 业务方的研发同学在 git 上新建或更新 MR - 消息自动触发到我们平台和流水线上 - 加载编译的环境、脚本,并且记录增量的代码信息 - 执行编译- 抓取编译过程 - 找到增量代码以及其所依赖链上所对应的子图 - 进行 COMPILE 分析、规则加载执行 - 若检查出问题,发送给相关研发同学或企业微信群。
若为增量判定,则监听 git 仓库的 hook,使得 MR 在 init 或者 update 时,可以发出来消息。
构建检测流水线,获取此次 MR 的元数据信息,在流水线上启动容器,进行代码拉取等。
使用前置插件,找到变更代码文件,变更行和片段。并且加载编译所需要的环境、脚本以及编译前的准备事项。
执行分析过程,若有发现风险,则再次判定是否为增量风险,若为增量风险,则发出通知。
由于 COMPILE 整体的分析过程,都依赖于编译的字节码,因此,前提是编译过程需要完成。
extract 字节码的属性,得到所有涉及到的 class 的方法函数信息等。
对 class 进行切分,一个 Method 作为一个 Node 节点。
在 Method 中,拆分成为 Method Param、Body 和 Return 部分,其中 Method Body 由若干 Block 构成。
对 Block 之间传递的变量,进行 Path 的寻址,构造出 Variable - Block 的图关系。
加载规则,执行符号化计算 Se(Varaible),模拟输入范围,根据不同的路径条件预设不同的值。
根据符号计算的取值范围,对变量进行局部的取值求解。对于待解子图来说,某一个上层节点的取值将会传播至下游任一节点,该值会随着变量值覆盖的方法而发生变化。
反向查询 CFG Data Flow 的变量所在的代码行号,对于命中的节点,记录变量的信息并且报出风险问题。
现在该功能已经进行了多个优化措施,由于依赖编译过程,因此编译的时间无法省略,但是编译+分析,后面的分析过程优化至 2 分钟以内,所以全过程的时间为 编译时间 + 2 分钟。
完成了增量分析,使之从全部字节码的分析,转换成增量文件所对应依赖链路的分析,使得以前 40~50 分钟的分析时长,缩短为几秒钟~几分钟。
调配了容器配置、程序运行配置,适当增加子进程数量,并且通过观察容器的 cpu 负载、内存负载,反复调整程序运行时资源,最终达到一个相对合理的实践方案。
这是一个非常典型的字节码案例,并且有区别于代码语义,代码匹配,AST等基于代码的方式,它发现了更加隐秘的资源泄漏问题。
比较了解 AST 的同学能够准确的看出,这个案例是无法通过一般的 AST 等方式捕捉到的。它不同于上文中的数据库 cursor,它的逻辑性需要跟踪 CFG 来解决。
逻辑上有些绕,由于应用代码的 AST 并不能将 FileInputStream.close() 的 path 也加入到待 solve 的约束中来,因此这个案例只能是编译后的 IR 能够解决。
一般空指针问题是一定要通过 IR 能够有些解决的。如下这个案例,具有较高的隐蔽性。
在第 41 行,已经进行了判空处理,client 在 while 块中不会有空指针的问题。
rpcPort 的赋值,使用了另外一个文件中的 handShaking 函数,代码如下。
仅靠 AST 的话,不能有效发现这样的一个问题,因为 AST 需要连接在一起,才能还原这个调用关系。
字节码与之不同的是,在 handShaking 函数中,存在返回 null 路径的可能性,因此能够得出空指针的风险。
死锁出现的条件是: 锁 A 与锁 B,在不同线程中,执行加锁的顺序是相反的,则有可能导致死锁现象。如: 线 先锁 A,再锁 B ; 线 先锁 B,再锁 A。
这类风险问题不常见,但是很有代表性,而且出现一些明显的异常问题时,往往比较难查,还需要借助一些线程的日志。且死锁的危害性是众所周知的。
需要指出的是,即使是字节码的方法,在死锁问题上也不能够做到一个很高的命中率,它是在执行顺序 和 锁位置上做一种规则。
另外死锁规则,需要辅助的不是单一变量,而是一批锁变量,并且锁变量中也有锁的范围、锁的对象,形成一个 list,并且对 list 做路径求解。
// realPostRequest ,自身是一个类的普通 public 函数,非类的静态函数。在方法声明上,会对 当前类的实例对象 加锁。
// postRequest (问题片段 1),自身是一个类的普通 public 函数,非类的静态函数。在方法声明上,会对 当前类的实例对象 加锁。
首先,postRequest 函数,和 realPostRequest 函数,这两个函数,均为类的普通函数,非静态函数。
在一些 APP 客户端的场景中,主线程要求不能有阻塞式的操作,耗时较大或者阻塞式操作,需要在异步线程中执行。
问题出在 241~245 行,这个片段中。glide 是一个开源代码库,因此 glide 不属于应用代码,不为研发人员自己写的代码。但是,它是程序的一部分,会被字节码识别到。
这里贴出了 glide 源代码中较为重要的代码片段,以及 issue 中该问题的相关回答。
因此在主线程中使用 glide get(),是一个阻塞式调用,需要避免。
其中,代码合并环节作为 devops 环节的基础部分。我们在代码合并的时机,构建了上述文中介绍的功能。
在接入平台之后,会配置一个 企业微信群的信息,以后如果识别出风险,将会推送至该群中。
我们在 代码合入时 进行触发,使用 AST 的检验测试能力,与 COMPILE 的检验测试能力,进行风险的检查。从静态和动态两个方面,进行风险的判定。依托于我们自研的平台,进行风险的通知和处理。因为篇幅原因,有些内容就不展开描述了。咱们进行了较多的技术探索,由于水平有限,很多能力尚在探索中,也欢迎读者朋友一起来探索这样的领域。
INTERMEC - M6米乐app最新版下载 - Honeywell
友情链接: - Symbol - 百度 - DATAMAX - 上海条码 - Bartender - 心理测评系统 - 米乐体育在线ios下载