@article{soto2023coverage,
title={Coverage-based debloating for java bytecode},
author={Soto-Valero, C{\'e}sar and Durieux, Thomas and Harrand, Nicolas and Baudry, Benoit},
journal={ACM Transactions on Software Engineering and Methodology},
volume={32},
number={2},
pages={1--34},
year={2023},
publisher={ACM New York, NY}
}
1 介绍
- [18,22,43,56] 软件系统日趋复杂
- 软件膨胀出现原因: 软件框架[3,30,44],代码复用[17,50,62]
- 软件简化指自动化移除不必要的代码[19],者包含几个挑战:
-
- 定位膨胀的位置[11,42,46]
-
- 移除膨胀并保持原有的功能
-
- 大多数SOTA简化技术使用静态分析因为其扩展性[26,46,49,54],然而由于动态语言的特征其精准度不够,这在现代软件实践中很常见[51]. 动态程序分析因为考虑了程序使用时的信息比静态分析表现得更好[11,42],然而在大规模软件系统中捕捉完整和准确的动态使用信息很困难
- 本文,我们引入 面向Java字节码的基于Cov的简化技术,我们实现了Java DeBloater(JDBL)工具,通过工业级的Cov动态分析技术来解决精确捕捉动态使用的问题。基于这些信息,JDBL自动化地简化项目的字节码,同时对其进行功能验证。为此,我们使用了原项目的测试套件进行评估。
- 我们技术的关键贡献在于精确的代码覆盖来减小错误的简化。
- 精确捕捉可以移除代码的两个挑战:
- 从源代码到字节码的编译过程中信息的丢失
- 必需存在但未被执行的软件元素
- 现有的覆盖工具不能处理第三方库,但库也是软件膨胀的一大来源。JDBL扩展了MAVEN的构建机制,收集了第三方库的信息
- 我们对 94 个独特开源 Java 库的 395 个版本中的 211 个版本进行简化来评估 JDBL
- 包括 1kw行代码的分析,103032 个类,以及 187 个三方库
- 3个粒度量化简化的影响:移除的方法数、类数和依赖项数
- JDBL发现 60.1% 的类是膨胀的,20.3% 的第三方库可以完全移除。
- 与JShrink[8]比较,JDBL实现了更大的减小率,同时保留了原始行为
- 我们首次评估了简化库的可用性,简化库依旧可以正常使用
- 本文的贡献如下:
-
- 基于从多个来源收集完整覆盖信息的实用自动化在字节码上的简化工具。
-
- 一个开源工具 JDBL,它在 Maven 构建管道中执行并自动生成简化的 Java 工件版本。
-
- 进行了最大的简化实证研究,涉及 211 个简化库,并在三个粒度级别上调查了代码减少情况。
-
- 第一次评估简化后的第三方库对客户端的影响,涉及 988 个简化库
-
2 动机示例
- 如图所示,对于一个Java项目,通常由主模块
JPROJECT
和其它模块构成,图中绿色即为实际使用的部分,红色即为未使用的膨胀部分,然而它们都被打入到了一个Jar包中 - 本文重点关注编译后的Java项目的简化,这涉及到检测和移除对所需功能无用的字节码,更大粒度的话包括项目自身的类和依赖项的累
3 基于Cov的简化
- Defineition 1 :Coverage-based Debloating
3.1 收集准确和完整覆盖率以进行简化的挑战
- Java有丰富的收集代码覆盖率的工具和算法,这些工具都基于 字节码转换[61],主要包括3个关键步骤
-
- 字节码注入探针:在程序控制流的特定位置根据覆盖粒度的不同,在字节码中插入探针。
-
- 执行注入字节码:执行被注入探针的字节码,以收集在运行时激活的探针信息。
-
- 映射与报告生成:将激活的字节码区域与源代码映射,并生成覆盖率报告提供给用户。
-
- 现在先进的Cov工具
,在简化时仍有2个限制 -
- 插桩策略在特定 角落情况(corner case)能力有限。
-
- 默认情况不处理第三方库。 即对库不插桩
-
- 代码覆盖率 与 源代码编译和字节码插桩有很大关联
-
- 字节码插桩必须安全高效,不能改变程序的功能行为并且运行时开销较小
-
- 插桩必须充分,所有执行的地方都必须插桩
-
- 以下三种因素会影响覆盖率的完整性:
-
- 目前没有覆盖工具能够捕获整个Java项目中依赖树的覆盖信息
-
- 不同的工具有不同的插桩策略,可能会导致不同的覆盖率
-
- Java编译器会丢失源代码的信息,这会导致覆盖率的不准确
-
- 我们共识别出5个挑战
- 挑战1:从调用的方法中抛出的隐式异常
- 挑战2:枚举类型中的隐式方法
- 挑战3:Java 编译器优化
- 挑战4:Java接口:无法对接口插桩,没有可以运行的代码
- 挑战5:第三方依赖
3.2 解决简化的覆盖挑战
以下为我们巩固覆盖信息的措施
3.2.1 汇总覆盖报告
- 使用baseline 覆盖报告 JaCoCo,随后使用其它工具来补充覆盖信息
- 为了处理隐式异常(挑战1),我们开发了Yajta
- 为了处理编译器生成的方法(挑战2),我们包括了JCov的报告
- 为了处理(挑战3),我们利用 JVM 类加载器获取动态加载的类列表
- 为了处理隐式异常(挑战1),我们开发了Yajta
3.2.2 保留所有必要的无法被覆盖的字节码
- 应对(挑战4)
- Java 语言包含一些特定的构造,用于实现编程抽象,如接口、异常、枚举和注解。
- 这些元素不执行程序逻辑,也没有实例化,但在编译时是必要的,纯动态简化无法确定它们是否冗余
- 为此,我们始终在简化后的字节码中保留这些元素,以确保简化后的字节码仍然有效
- 同时这些语言构造体积较小,不会对简化的效果产生太大影响
3.2.3 捕获整个依赖树中的覆盖率
- 应对(挑战5)
- 我们扩展了JaCoCo提供的覆盖信息,以涵盖依赖级别,这需要修改JaCoCo与Maven的交互方式
- 我们通过Maven 的自动化构建基础设施来编译 Java 项目并解析其依赖项。
- 我们可以在工作负载执行之前,对所有依赖项进行插桩,随后再进行编译打包,这样我们就可以捕获整个依赖树的覆盖率
3.3 基于覆盖的简化过程
- JDBL细节
- 输入:一个Java项目(可以通过Maven构建),执行该项目的一个工作负载
- 输出:简化的打包的项目,能正确构建,能正确运行工作负载
- 三个阶段:
- 覆盖收集阶段
- 字节码移除阶段
- 验证阶段
- 算法1描述了JDBL的简化过程
3.3.1 覆盖收集
- 两个输入:可编译的原代码集合,一个工作负载(执行编译源代码的入口和资源集合,可以是测试套件)
- 输出:原始的字节码和覆盖报告
- 算法描述:1-11行
-
- 原代码和依赖编译
-
- 插桩
-
- 收集使用过的代码元素
-
- 使用工作负载W运行插桩后的代码(W可以是Maven项目的测试套件)
-
- 保存使用过的类及其方法
-
3.3.2 字节码移除
- 通过消除在运行特定工作负载 W 时未被使用的方法、类和依赖关系来优化项目。
- 首先移除未使用的类及其依赖关系
- 随后在覆盖的类中,移除未被覆盖的方法,将其主体替换为抛出 UnsupportedOperationException,避免由于方法的缺失而导致的 JVM 验证错误,这些方法可能是接口和抽象类的实现。
3.3.3 工件验证
- 评估语法正确性:验证简化后字节码的完整性,包括检查JVM运行时要加载的字节码的有效性,以及简化是有依赖项或资源错误地从Maven项目类路径移除
- 评估语义正确性:检查简化后项目在工作负载下是否正常执行
3.3.4 实现细节
- JDBL核心实现在于 程序的代码覆盖工具和字节码转换技术
- 我们基于覆盖率的简化集成在Maven不同的构建阶段,我们关注Maven因其是最广泛采用的构建工具
- JDBL使用 maven-dependency 插件的 copy-dependencies 目标来收集直接和传递的依赖关系,这使我们能够操控项目的类路径,从而在依赖关系级别扩展代码覆盖工具
- 我们使用ASM
,分析和删除字节码 - 通过在Maven构建管道中集成 JaCoCo,JCov,Yajta和JVM类加载器完成函数的插桩
- JDML为5k行Java代码的多模块Maven项目,被设计用于单模块Maven项目简化,可以作为Maven插件在Maven包装阶段执行
- 可用性:可以在Maven构建生命周期中轻松调用并执行,使用JDBL,只需要在
pom.xml
文件的build变迁中添加Maven插件即可
- 可用性:可以在Maven构建生命周期中轻松调用并执行,使用JDBL,只需要在
- JDBL源码
4 实证研究
4.1 RQ
- 研究
- 正确性:RQ1,2
- 有效性:RQ3,4,5
- 影响:RQ6,7
- 通过4个验证层:简化Java库的编译和测试,客户端的编译和测试
- 7个RQ:
- RQ1:通用的,完全自动化的基于覆盖的简化技术可以在多大程度生成简化版本的Java库?
- RQ2:简化后的Java库在多大程度上保持了在工作负载上的行为
- RQ3:在编译的库和依赖中移除了多少字节码
- RQ4:使用基于覆盖率的简化方法对打包工件的大小有什么影响?
- RQ5:基于覆盖率的简化与当前 Java 简化技术的先进水平相比,在打包工件的大小和行为保持方面如何?
- RQ6:简化后的库的客户端能成功编译吗?
- RQ7:当使用简化库时,客户端的行为是否正确?
4.2 数据收集
- 构建了一个数据集,包含两部分
-
- 一组库
-
- 使用1中库的Java项目
-
- 详细收集步骤见论文
4.3 试验协议
- 该章节介绍了实验的具体步骤
4.3.1 基于覆盖的简化运行
- 为了规模化运行JDBL,我们创建了一个框架来自动化运行
由三步组成 -
- 编译和测试原始库
-
- 配置JDBL
-
- 运行JDBL
4.3.2 简化正确性(RQ1,2)
- RQ1评估JDBL生成简化JAR文件的能力
- RQ2分析简化后的库在测试套件上行为前后是否一致
- 如果不一致手动分析原因
- 如果不一致手动分析原因
4.3.3 简化有效性(RQ3,4,5)
- RQ3:计算简化的方法和类的比例
- RQ4:比较原始和简化后的Jar包大小变化
- RQ5:与JShrink比较字节码简化的效果,使用JShrink的基准
4.3.4 简化影响(RQ6,7)
- RQ6:验证在将原始库替换为简化版本后,客户端是否仍能编译。我们检查 JDBL 是否没有移除客户端编译所需的类或方法。
- 保证该阶段里客户端里引入了库
- RQ7:确定 JDBL 是否保留了对客户端必要的功能,即是否能通过测试套件
- 保证简化库至少被一个测试套件覆盖
5 结果
5.1 简化正确性
- RQ1:JDBL生成了302个简化正确的库,占所有编译正确的库的 85.3%。这是文献中去冗余成功的库数量最多的记录。
- RQ2:211(69.9%)的库通过了全部的测试用例;241个库中,341430(99.59%)个测试用例成功通过。这种基于覆盖的去冗余行为评估表明,JDBL 保留了绝大多数库的行为,这对于满足库用户的期望至关重要。
5.2 简化有效性
- RQ3:JDBL在所有库中分别减少了20.3%、60.1%和59.4%的依赖项、类和方法。这一结果确认了基于覆盖率的简化方法在减少Java项目中不必要的字节码的同时,保持其正确性的重要性。
- 本探究中还发现,依赖的膨胀程度是要大于库的膨胀程度的,这意味着依赖树上的一些完全没用的库被引入到了项目中
- RQ4:JDBL 移除了JAR文件中68.3%的纯字节码,这代表每个库的JAR文件平均减少了25.8%的大小。与没有依赖项的库相比,具有至少一个依赖项的库的JAR大小减少显著更高。
- 一个Jar包包含资源文件和字节码
- RQ5:在JShrink的基准中,成功简化了17个单模块Java项目,平均大小减少35.1%,并通过全部测试;而JShrink平均大小减小15.1%
5.3 客户端简化影响
- RQ6:JDBL 保持了950个(96.2%)使用JDBL简化库的客户端的语法正确性。这是首次实证证明简化可以保留关键功能,从而成功编译简化库的客户端。
- RQ7:JDBL 保持了 229 个(81.5%)简化库的客户端的行为。其余 52 个客户端仍然通过了 43,684 个(98.5%)测试用例。在这些情况下,99.1% 的测试失败是由于缺少类或方法,这些问题可以很容易地被定位和修复。该实验表明,从软件库中移除代码对其客户端的风险是有限的
6 讨论
6.1 代码覆盖工具的完备性
-
- 多工具优势
- 精确度提升,减少误判。如JVM类加载器记录了动态加载的类
- 减少误删的可能性
-
- 覆盖情况分析
- JaCoCo:覆盖了78.%的实用类
- JVM加载器:捕获了最多的唯一类
- JCov和Yajta:分别增加了2个和662个唯一覆盖类
-
- 完整覆盖的重要性,未来需要更优秀的覆盖工具
6.2 运行时间
- 原因:将简化集成到构建管道上,执行时间很重要
- 结果:
- 在395个库上执行JDBL,总共耗时1天10小时55分钟,平均每个库5.3分钟。
- 在JShrink基准上,平均简化时间不到3min,是JShrink11倍快
6.3 有效性威胁
- 内部有效性
- 基于覆盖方法的天然缺陷:
- 测试用例不足:选择高覆盖率的库作为研究对象
- 测试可能存在随机行为:重复测试
- 工具依赖和执行:
- JDBL使用的工具可能存在缺陷:使用多种工具之间可以互补
- Maven配置问题:设置Maven插件默认配置,避免与其它插件冲突
- 基于覆盖方法的天然缺陷:
- 外部有效性
- 研究对象特定性:单模块Maven项目和Java生态系统
- 研究对象的规模
- 库和客户端的选择
- 构建有效性
- 依赖于Maven构建生命周期
- 工作负载的质量
7 相关工作
7.1 软件简化
针对Java
- [53,54] 对Java简化进行了开创性研究,其提出一套全面的转换方法来减少 Java 字节码的大小,包括类层次结构合并、名称压缩、常量池压缩和方法内联。
- Jiang 等人 [26] 提出了 JRed,这是一个通过修剪 Java 二进制文件中的冗余代码来减少攻击面工具。
- RedDroid [25] 和 PolyDroid [20] 提出了针对移动设备的简化技术。他们发现,简化显著减少了分发应用程序时的带宽消耗,通过优化资源提高了系统性能。
- [9]简化提升Maven构建性能
- [50]简化膨胀依赖项
- [59]缓解运行时膨胀
- [19]探讨了使用静态分析检测Java程序中不必要的代码
- [8]JShrink,动态简化Java程序。然而,JShrink 不能直接自动化集成到构建管道中,并且没有研究简化对库客户端的影响。
- [46] 提出了 Trimmer,这是一种依赖用户提供的配置和编译器优化来减少代码大小的简化方法
- [42] 提出了 RAZOR,这是一种基于测试用例和控制流启发式的程序二进制文件简化工具
7.2 动态分析
- 动态分析是从程序执行中收集和分析数据的过程。被用于多种技术,例如程序切片 [2]、程序理解 [12] 或动态污点跟踪 [4]
- 基于追踪的编译使用动态识别的频繁执行的代码序列(追踪)作为优化编译的单元 [16,24]
- [37] 实现了一种基于客户端调用点的本地化来进行按需加载库的方案。这种方法通过预测在执行期间特定调用点所需的近似确切的库函数集合,从而减少暴露的易受攻击的库代码表面。
- [40] 使用动态分析有效总结了现代应用程序的执行和行为,这些应用程序依赖于大型面向对象的库和组件。
- 我们采用动态分析来进行字节码减少,与以往工作中关注的运行时内存膨胀不同 [5,35,36,38,39,58,60]。
- 在 Java 中,动态分析常用于克服静态分析的局限性。
- Landman [31] 对动态特性使用进行了研究,发现 78% 的分析项目中使用了反射。
- Xin 等人 [57] 的最新工作利用执行追踪通过分析 Android 应用程序的动态行为来识别和理解其功能。
8 结论
- 本工作中,我们为Java引用引入了基于覆盖的简化技术,实现工具为JDBL
- 我们解决了动态简化的关键挑战:收集准确和完备的覆盖信息,包括了成功运行工作负载的类和方法的最小集合。
- 我们对JDBL作了最大的实证研究,包括354个库及1354个使用它们的客户端。在库行为,大小和客户端上的表现进行了评估
- 我们的结果表明
- JDBL 可以将字节码大小减少 68.3%,并且 211 个(69.9%)经过简化的库可以成功编译并保持其测试行为。
- 我们的结果大幅领先JShrink。
- 文献中首次评估了简化库对其客户端的实用性:81.5% 的客户端可以成功编译并运行其测试套件。
- 我们的结果证明了软件应用中存在大量不必要代码,并且简化技术可以有效处理这一现象。并且动态分析可以用于自动化简化库,同时保留客户端需要的功能。
- 未来工作:
- 基于覆盖的简化下一步是根据生产环境中收集的使用配置文件来专门化应用程序,将简化扩展到程序栈的其他部分。例如Java的运行时环境,程序资源或容器化应用程序
- 简化的实证研究:评估基于覆盖的简化在减少现代应用程序攻击面方面的有效性
个人总结
- 简化库的可用性,这个问题可以深究
- 源代码可见 和 测试套件全通过,哪个更让人信服:在字节码上Cov,是可以更好捕捉动态信息,但编译后的字节码信息丢失,如果映射回源码,可能无法找到对应片段,代码审计性降低。但是,这里又可以通过测试套件来验证,这就取决于测试套件的可信度了
- 可能的答案:熟悉代码的人可能是前者,不熟悉的话可能是后者
- 这里应该运行人自由作答,不应该是一个固定的答案
- 源码简化与编译后代码简化:编译后代码简化往往有更大的简化空间,但是在指令级别,语义遭到破坏,程序无法验证
- 代码覆盖机制:代码覆盖都是从二进制文件覆盖映射到源码覆盖,编译导致的信息丢入如何影响代码覆盖?
- 论文结果章节写法:在对结果罗列后,再对结果中的问题进行分类讨论