系统化程序分析

左志强,南京大学计算机系副研究员,研究领域包括程序分析,编译技术,系统软件等。

本文以技术文章的方式回顾左老师在 SIG-程序分析 技术沙龙上的分享,回顾视频也已经上传 B 站,欢迎小伙伴们点开观看。

SIG-程序分析技术沙龙回顾|面向千万行代码的高精度静态分析系统_哔哩哔哩_bilibiliicon-default.png?t=L892https://www.bilibili.com/video/BV1NP4y1W7cF


Introduction #

大家好,非常高兴今天能有这个机会在我们国内做程序分析的一个新 group 来给大家分享一下我们最近做的一些尝试。

从广义上来说,程序分析可以大致分为两类,包括:

  • 静态分析 Static Analysis

  • 动态分析 Dynamic Analysis

在了解到编程语言社区的 SIG-程序分析后,我对自已之前的工作做了一个简单的回顾,发现我大部分的工作都可以归结到这两类程序分析上。简单罗列一些我们的工作:

图片

切入主题,今天我想跟大家分享的主题叫做系统化程序分析,主要面向静态分析,即如何将静态分析做到足够地精确,同时又保证一定的效率,或者说所谓的可扩展性。

静态分析

静态分析,是一种不需要去执行代码,而静态地抽取程序信息的技术。其起源可以说是来自于编译优化,可以讲当前所有的编译器涉及到的编译优化技术都必然会包含静态分析。

图片

静态分析有很多广泛的应用场景,比如软件中的代码缺陷检测,编译优化,安全等,相信大家对此都有一定的认知:

图片

但是,将静态分析的技术应用到现代软件上,会有什么难点呢?

系统软件代码规模越来越大

Information is beautiful [1] 这里有列举一些超过百万行规模的系统软件(不是最新的数据)。大家可以看到,其实这种百万规模的软件越来越多,可以说在我们日常生活中是随处可见的。

图片

系统软件代码规模 [1]

举一些具体例子,Linux kernel 的代码应该超过 15 million 行了,一些浏览器、数据库的管理系统肯定是在百万行,甚至千万行之上的规模。

图片

简单的程序分析已经不够用了但是,对这种超大规模的软件进行静态分析也不是不可能。当然我们唯一能做的是一些比较简单的静态分析,无非是要在精确度和效率之间做权衡。

下图罗列了一些学术界的开源工具,还有一些商业化静态分析的工具,其中大部分工具为了获得足够多的性能和可扩展性,需要做一些非常简单的,比如说 intra 分析,pattern-based 分析等。

图片

一些静态分析工具

这样会造成什么问题?你的 false positive 其实会非常可怕。这直接影响了静态分析的可用性。

因此,在这种大规模的软件上做非常精确的静态分析,其实是非常难的,主要原因就是:

  • 超大规模的代码量:

    • Google codebase (2B+)

    • Linux kernel (17M+)

    • Boeing 787 (14M+)

  • 非常复杂的高精度:

    • Context-sensitive analysis

    • Path-sensitive analysis

    • Flow-sensitive analysis

那么不得已,我们就需要面对非常低效的可扩展性问题。低效可以体现在两个方面,一是计算时间可能会很长,另一个方面是计算过程中的内存消耗可能会非常大。

图片

如何提高 Scalability

在面对 poor scalability 的问题上,我们的研究人员做过哪些尝试?

一种方式是改进分析算法。我们可以对某些代码部分应用一系列包括 abstraction、approximation、selective sensitivity 等高精度的分析,对另一些代码部分做化简的分析。

这种方式可以进一步地减少算法的复杂度,从而提高 scalability。但是,这种方式会带来很明显的实用性问题。即改进的分析算法如果要真的实现,其工作量会难以预估,实际应用的分析算法会非常复杂。

如果大家写过程序分析,或者对已有的一些程序分析的工具、框架熟悉的话,就会知道,这些工具的基础部分,代码量其实非常少。大部分已有工具的代码量,其实都在做一系列复杂的 treatment,以解决算法的复杂度,同时提高效率。这对于已有一定程序分析基础的 expert 来讲,其实也是不小的挑战,更不用说对单纯想应用程序分析的用户,其学习成本以及后期对工具维护的成本是相当高的。

图片

举个例子。当前在 llvm 的某个版本上可能提供了某种程序分析的算法,但你可能会发现它在下个版本上就被去掉了。很大可能的原因就是,开发这个分析的几个程序员或者专家已经离职,其他人很难去 debug。

为了解决程序分析算法的开发效率问题,有一些研究人员尝试基于 declarative 的写法(比如基于 Datalog)来实现。用户可以写几句简单的 declarative 的语句,去 specify 一系列的关系,后续的分析任务就可以完全交给后面的引擎(比如一些 Datalog 的引擎)来做。当然这又回到了前面提到的问题,往往这些引擎面临的最大问题就是,它的效率以及可扩展性是非常差的。

一种新的架构

我们可以思考一下,如何跳出已有的问题来提高 scalability?设想一下,我们是否可以从系统的角度去提供更多的支持,而不是对算法进行更复杂的改变?是否可以充分利用现代的计算资源,从而提供一种更便于分布式或者并行化的分析系统?

图片

我理解这种系统的好处是,可以将前端后端进行一定的隔离,后端可以作为底层的分析系统提供一系列的分析加速,在上层提供给用户设计友好的实现接口。基于此,用户若要实现不同的分析,他唯一需要去做的工作就在前端,通过类似 declarative 的方式,减轻用户对算法实现的负担。

图片

“系统化”的程序分析

这种前后端解耦的分析系统,我们认为起码从三个方面是比较友好的:

  • 首先,把底层的计算任务完全交给后端的系统,充分利用一系列并行资源来解决内存的问题;

  • 同时,上层提供一些用户友好的接口,用户只需要实现非常简单的前端;

  • 此外,还可以实现多语言支持,通过对不同的语言上层实现不同的前端,中间转为统一中间表示,交由后端做分析计算。

图片

基于上述思路,我们进行了一系列的工作:

图片

Chianina #

今天向大家分享一下我们在 PLDI'21 上发表的最新工作 Chianina [2],基于硬盘的核外计算的方式去做对上下文及流敏感分析的支持。

Flow Sensitivity

在开始之前,先简单介绍一下,什么是流敏感分析(Flow-Sensitive Analysis),什么是流非敏感分析(Flow-Insensitive Analysis)?

假设有一个函数 foo(),下图是它的 control-flow graph,graph 内有一个 entry node 和一个 exit node,中间有一系列的 node,我们可以将中间这部分看作一个 basic block,或者一条简单的 statement。

一个流非敏感(flow-insensitive)分析,如图左所示,对整个函数只会计算一个结果,该结果在这个函数的所有节点上都成立。

相对比的是流敏感(flow-senstiive)分析,如图右所示,它需要在函数内的每个节点上分别计算结果,这个结果可能在不同节点上有差异,比如这里的Info_1 、Info_2Info_3Info_4

图片

流敏感与流非敏感分析对比

那么显然地,相对于 flow-insensitive 的分析,flow-sensitive 的分析其复杂度是更高的,相当于对程序中的每一个不同的节点都去计算一个不同的结果。

我们其实可以将基于 flow-sensitive 的基本计算看成一个非常简单的模式:

假设有 s3 和 s4 两个中间节点,其后续的节点是 s5s5 的后一个节点是 s6,在每一个节点上都计算一个分析的结果,这个计算本质上是一个 Worklist 算法 [3] [4]。Worklist 算法 的每一步操作都可以看成是一个 combine() 和一个 transfer() 函数的执行。

以 s5 为例,先通过 combine() 得到 s3 s4 计算结果的组合值,得到Input_5 ,然后基于 s5 statement 代表的具体语义去做 transfer() 的计算,得到Info_5作为 s5 最终的计算结果。

图片

Worklist 算法 [3] [4]

整个计算就是在 control-flow graph 上对每一个 node 一直往复地做 combine() 和 transfer(),直到所有 node 的Info都达到了一个稳定的值,即 fixed point。

整个计算步骤是非常 general 的,一些 data-flow analysis、abstract interpretation,甚至 flow-sensitive pointer analysis、cache analysis 等都可以归结为这一类计算。

Context Sensitivity

Chianina 也支持 context-sensitive 的分析。

假设有函数 A() 和 B()A() 和 B() 分别在某个 node 上调用了 foo()

Context-insensitive 的方式如下所示,把对 foo() 调用连接到 foo() 内部的 entry,然后再 exit 退出 foo(),同时 A() 和 B() 的调用不会做区分,这就是一个经典的 context-insensitive analysis。

图片

Context-Insensitive Analysis 示例

与之对比的是 context-sensitive 分析,A() 和 B() 调用 foo() 的情况需要区分来看。一种方式是,这里作为一个 conceptual illustration,即把 foo() 做两份的拷贝,以经典的 clone 的方式做区分。具体来讲,左侧 foo() 的 exit 点必须只能返回到 A() 的调用点;右侧 foo() 的 exit 同样只能返回到 B() 的调用点。这就是一种上下文区分的方式,相对来说其分析精度是更高的。

图片

Context-Sensitive Analysis 示例

Context- and Flow-Sensitive Analysis

基于前面所述的背景,Chianina 就是对 context 与 flow 都 sensitive 的分析提供高性能的系统化支持。

我们将这种场景转换为一种图的计算问题。在上层,对一个很大的 interprocedural control-flow graph 做一系列的 transfer() 和 control() 计算。这个图会有多大呢,大家可以想象一下,其规模(节点/边)可能是 million 甚至 billion 级别的。因此在计算和存储两方面,这都是一个非常大规模的分析。这也解释了为什么已有的一系列工具和算法很难做到这样规模的分析。

Chianina 针对这两个问题进行了设计:

为了解决内存的问题,Chianina 采用了核外(out-of-core)的支持。具体来说,Chianina 会把整个的图进行分区化处理,每次去处理一部分子图的计算,依次迭代地去计算各个部分,直到最后的整个图上达到 fixed point,完成计算。

为了提高计算效率,Chianina 采用了两级的并行化处理。一级是在分区上,即多个分区同时处理(整体同步并行计算模型,Bulk Synchronous Parallel Model [6]),另一级是在每个分区内多 node 同时做计算(异步并行)。

此外,为了进一步减少内存消耗,减少计算的迭代次数,Chianina 还采用了一些局部 compression 的技术。比如说前面所举的例子中,Info_1 Info_2 Info_3 Info_4是比较紧邻的点,而在大部分的分析上,这些 Info 之间其实会存在大量的信息 overlap。我们能不能针对这些 overlap 做一些 compression,从而减少内存的消耗?

框架

下图是 Chianina 的框架示例。

图片

Chianina: A System for Flow- and Context-Sensitive Analyses [2]

  • 假设前端提供 control-flow graph 的 initial data

  • 通过分区,initial data 被分成不同的 partition 存在硬盘中,作为后续处理的 input data

  • 通过定制化的 scheduling 策略,系统每次会 load 一些 partition 到内存中,partition 的数量会根据内存大小进行动态调整

  • 在每个 partition 上,系统会进行异步的 worklist 算法(即 combine() 和 transfer()),同时系统还会对每个 partition 执行一系列的 compression

  • 鉴于 partition 之间可能会有相连接的边,故每轮计算完成后,需要通过消息传递(message-passing)的方式做 synchronization,分析上一次的计算中哪些需要 propagate 到下一个 partition(这是一个经典的 BSP model)

  • 重复每轮的计算,直到最后图中所有的 node 都达到 fixed point,scheduler 判定不需要 load 任何 partition 进来了,计算完成。

接下来我简单介绍一下其中的几个部分。

## Partitioning and Scheduling

如果大家熟悉一些大数据的技术或者系统的话,应该比较容易理解这部分。假设下图是刚才进行 context-sensitive 处理的一个规模很大的 control-flow graph。大家可以看到,左右两侧的边和节点是完全一样的。

图片

将整个图划分为多个分区

首先,进行 partition 的分区操作。这里每一个蓝色的暗区代表一个 partition,如上图示,整个图被分成了不同的 partition(当然 partition 之间可能会有边进行 cross 的操作,如左侧的 s2->s4s3->s5 等,后续我会在 communication 的部分展开讲)。

为了做到比较 balance 的 partitioning,Chianina 采用了一些例如 topological order 的 data-flow analysis 相关理论的支持,去分解整个大图,从而减少迭代计算的次数,提高系统处理的并行度。

另外,在 scheduling 的过程中,我们会根据当前 partition 的 active node 数量,以及它是否在内存中,来决策下一轮系统应该 load 哪些 partition 到内存中去做计算。

## Computation and Compression

有了前面分好区的 partition 后,系统每次会 load 一些 partition 做并行计算。并行计算分为两个 level:

  • Partition level 使用 Bulk synchronous parallel (BSP) 对每次 load 的多个 partition 处理

  • Active Node level 在每个 partition 内对多个 active note 进行并行化计算(这里用到的经典技术 [5] 不展开赘述)

如下图示,假设在某一时刻,系统加载了三个黄色的 partition 到内存中做 Worklist 算法的计算,另外三个蓝色的 partition 依然存放在硬盘里。

图片

加载一部分分区到内存中

以最左侧被加载到内存中的 partition 为例,这就是一个 control-flow graph 的子图。在这个子图上可能同时存在多个 active node(如下图中的黄色节点),这些 node 会被异步并行地进行计算处理,从而加速整个的分析过程。

图片

在子图上迭代地进行并行计算

## FCS-based De-duplication

此外,每一轮内存中的计算结束后,可能在一些 control-flow point 上会存在大量的 overlap 的 Info,我们可以基于此去做进一步的压缩。

若将 Info 看做一个图,那么图之间可能存在一些共用的子图,那么我们需要挖掘出这些共用的子图,来取一个 reference。

同样是在刚才的计算中,假设存在  Info_3 Info_4 Info_5 Info_6这 4 个点的信息需要保存,如下图示,我们以基于 CFL 的 pointer analysis 为例。此时,我们可以假设每个 Info 是一个图,其中相邻的 Info 里存在大量的共用子图,   ,我们可以利用一些 reference 的信息去进一步压缩计算。

图片

FCS-based De-duplication

具体来讲,上图的Info_3可以用g_1指代,Info_4可以是 g_1 和 g_2 两个 pointer,Info_5 和 Info_6  也是同样的做法。这样我们就可以大量地减少内存消耗。

实验结果

给大家展示一下 Chianina 的实验结果。

## 被测对象

我们选定了几个常见的大型系统软件,包括 Linux kernal、Firefox、PostgreSQL 等(规模都在百万甚至上千万行),并在这些软件上实现了一系列的 flow- 和 context-sensitive 的分析。

图片

## 实验环境

实验环境就是一个常见的 PC 上:

  • Intel Xeon W2145 8-Core CPU, 16GB memory, and 1TB SSD

  • Ubuntu 16.04

## 实验算法

我们分别实现了响应的前端来支持这三种经典的 data-flow analysis,生成的图交由 Chianina 的后端完成计算:

  • 指针/别名分析 Pointer/alias analysis

    • Flow-sensitive analysis with strong update

    • 550 lines of C++ code implementation

  • 空值的流分析 Null-value flow analysis with alias tracking

    • IFDS-like dataflow analysis with alias information

    • 700 lines of C++ code implementation

  • ICache 分析 Instruction cache analysis

    • Non-IFDS dataflow analysis with <instruction, age> abstract model

    • 430 lines of C++ code implementation

## Performance

Alias Analysis

这是 Pointer/alias analysis 的 performance 结果。我们可以看到 Linux kernel 用时是最久的,约 21 个小时,Firefox 在 11 个小时多一点,Httpd 就比较快,四五分钟就可以算完。

图片

Alias analysis

NULL Value Flow Analysis

实验中的 NULL Value Flow Analysis 为了获得更精确的分析,结合了 Alias Analysis。结果可以对比看到,比之前的 Alias Analysis 用时稍微长了一点。

图片

NULL value flow analysis with alias tracking

Cache Analysis

实验中的 Cache Analysis 同样是在经典的 data-flow analysis 下做了 ICache 的分析。可以看到,Linux kernel 用时 24 个小时多一点,具体的一些数据在我们的论文中都有讨论,这里就不展开讲了。

图片

Cache analysis

## Performance Breakdown

我们的实验对 performance breakdown 也进行了分析。在整个的计算过程中,可以大概分成下图所示的 4 个部分。其中包含了 Preprocess(对图的一个预处理过程,partition 会包括一些读入读出),I/O (因为我们实现的是一个 out-of-core 的算法,每个 partition 要经过多次的 I/O 处理),BSP(核心计算部分,执行 combine() 和 transfer()),以及 FCS(压缩)。

从结果可以看出,大部分的时间都是消耗在 BSP 计算上,相对来讲其余部分的用时还是很少的。这个实验结果是跑在 8-core CPU 上的,如果基于更多的分布式并行的资源,可以大大缩减这里的计算时长。

图片

Performance Breakdown

## Scalability

我们也针对 parallel scalability 做了测试,即通过限定核数(下面结果是对比了 1-core,2-core,4-core,8-core)分别去看执行时长。从结果可以看出,其 scalability 是接近线性的,这说明 Chianina 的并行度是非常高的。

图片

我们可以从几个方面猜想一下,为什么 Chianina 的并行度可以这么高?

首先,在解决传统 context-sensitive 的过程中,我们采用了 inline 的方式(function 做 copy),这种方式虽然看上去其内存消耗量会是一个非常大的问题,但在 Chianina 中,我们用 out-of-core 的方式解决了这个问题。同时,inline 的方式也给并行计算带来好处,即减少了大量的 data sharing。传统的 summary 或 top-down 的方式,避免不了一个函数多次被调用的问题(如前面示例中,A() 和 B() 都调用了 foo()),那么这时候你对整个 foo() 的处理上会存在很多的 sharing access,因此会大大影响并行化的处理效率。

## Performance Comparison

我们也与一系列已有的工具做了简单的对比,包括经典的基于 sparse representation 的 flow-sensitive analysis [7],value-flow analysis [8] 等。从结果可以看到,在一些小的程序上,已有的这些工具从性能上来讲要比 Chianina 更优,因为他没有去做任何的图处理,其效率会更高;但面对大型的系统,已有的工具很难做到 scalability 的支持。

图片

Context-insensitive and flow-sensitive alias analysis, Chianina vs Reference [7], SVF [8]

## 其他实验结果

我们针对 compression 以及 partition 对性能影响也进行了测试:

图片

Partition 和 Compression 等对性能的影响

同时我们也针对一些实际的应用做了测试,验证 Chianina 的 context- 以及 flow-sensitive analysis 的有效性,从实验对比可以明显看出,针对 checker 的误报分析是有明显改善的:

图片

图片

Usefulness of Gained Precision

目录

# Introduction #

# 静态分析

# 如何提高 Scalability

# 一种新的架构

# Chianina #

# Flow Sensitivity

# Context Sensitivity

# Context- and Flow-Sensitive Analysis

# 框架

## Partitioning and Scheduling

## Computation and Compression

## FCS-based De-duplication

# 实验结果

## 被测对象

## 实验环境

## 实验算法

## Performance

## Performance Breakdown

## Scalability

## Performance Comparison

## 其他实验结果

# 总结 #


总结 #

今天的分享就到此结束,大家若对更具体的原理和实现细节感兴趣的话,欢迎查看我们的文章,也可以跑一下我们的工具。欢迎大家加入 SIG-程序分析 多多交流!

  • 个人主页:https://z-zhiqiang.github.io/

  • 论文地址:https://dl.acm.org/doi/abs/10.1145/3453483.3454085


参考

[1] Million Lines of Code - Information is Beautiful https://informationisbeautiful.net/visualizations/million-lines-of-code/

[2] Zhiqiang Zuo, Yiyu Zhang, Qiuhong Pan, Shenming Lu, Yue Li, LinzhangWang, Xuandong Li, and Guoqing Harry Xu.Chianina: An Evolving GraphSystem for Flow- and Context-Sensitive Analyses of Million Lines of C Code,page 914–929. Association for Computing Machinery, New York, NY, USA,2021. https://dl.acm.org/doi/abs/10.1145/3453483.3454085

[3] Flemming Nielson, Hanne Nielson, and Chris Hankin.Principles of ProgramAnalysis. 01 1999. https://www.researchgate.net/publication/265352570_Principles_of_Program_Analysis

[4] Meng Wu and Chao Wang. Abstract interpretation under speculative execu-tion. InProceedings of the 40th ACM SIGPLAN Conference on ProgrammingLanguage Design and Implementation, PLDI 2019, page 802–815, New York,NY, USA, 2019. Association for Computing Machinery. https://dl.acm.org/doi/abs/10.1145/3314221.3314647

[5] Leslie G. Valiant. A bridging model for parallel computation.Commun. ACM,33(8):103–111, August 1990. https://dl.acm.org/doi/10.1145/79173.79181

[6] Grzegorz Malewicz, Matthew H. Austern, Aart J.C Bik, James C. Dehnert, IlanHorn, Naty Leiser, and Grzegorz Czajkowski. Pregel: A system for large-scalegraph processing. InProceedings of the 2010 ACM SIGMOD InternationalConference on Management of Data, SIGMOD ’10, page 135–146, New York,NY, USA, 2010. Association for Computing Machinery. https://doi.org/10.1145/1807167.1807184

[7] Ben Hardekopf and Calvin Lin. Flow-sensitive pointer analysis for millionsof lines of code. InProceedings of the 9th Annual IEEE/ACM InternationalSymposium on Code Generation and Optimization, CGO ’11, page 289–298,USA, 2011. IEEE Computer Society. https://dl.acm.org/doi/10.5555/2190025.2190075

[8] Yulei Sui and Jingling Xue. Svf: Interprocedural static value-flow analysisin llvm. InProceedings of the 25th International Conference on CompilerConstruction, CC 2016, page 265–266, New York, NY, USA, 2016. Associationfor Computing Machinery. https://doi.org/10.1145/2892208.2892235

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值