NJU Static Program Analysis | Assignment-3 Dead-Code Detection.
2024.11.7已更新
实验信息&食用指南
见本博客A1 & A2
Tips: 实验指南和框架代码仍然是最一手最正确的资料, 对于实验指南中已涉及的信息, 本记录不会过多赘述.
实验目标
基于Tai-e 框架实现一个 死代码检测 算法.
实验简析
实验指南没有给出实现的框架, 因此至此已有完成本次知识的全部基础知识(活跃变量分析 & 常量传播算法).
故本次实验主要考察对A1, A2的实验理解, 有遗忘或有不清楚的读者还望移步至官方教学视频复习后再完成本次实验.
死代码检测是一个Must Analysis , 允许漏报而不允许错报.
由分支不可达的特性, 采用Forward Analysis 比较符合死代码检测的特性.
为了走一遍CFG 便得到结果, 我们可以将全部
Stmt加入DeadCodeSet中, 将不是死代码的Stmt删去. 留下的便是死代码.此处笔者采用BFS算法进行CFG遍历, 使用
Set Traveled避免环路影响.
伪代码框架
死代码检测的大致框架伪代码如下:
1 | liveVarsResult <- LiveVarsAnalysis(IR) |
更新:
Compare:
ir.getStmts()&cfg.getNodes()笔者一开始未意识到这个问题, IR 中是不包含
Entry和Exit的. 换句话说,Entry和Exit是由CFG 生成的. 因此, 在初始化时, 需要使用ir.getStmts()而不是cfg.getNodes(), 否则在面对Infinity Loop 时,Exit节点会错误的被当作Dead Code.
CFG生成细节
完成本实验最重要的部分莫过于CFG 的结构,
实验指南没有为我们提供所有的实现细节,
而且CFGBuilder部分的代码对我们来说是不公开的.
那我们该如何获取关于CFG 的细节?
框架代码提供的接口
好在tai-e框架为我们提供了Class Assignment来分析自测样例,
输出*.dot文件到./output/目录下, 可以使用[Graphviz]可视化*.dot文件.
使用细节见上述链接中的官方文档.
笔者所使用的IDE 为JetBrian IDEA 2023.1.4
Community版本, Class Assignment运行配置如下.
在终端中进入工作目录(此处为output/)执行以下命令:
1 | dot -Tpng ClassMethod.dot -o ClassMethod.png |
然后我们就可以得到*.dot文件的可视化CFG
结果.
根据自测样例推测CFG的生成逻辑
经过前两次的实验, 普通节点和跳转的关联顺序在CFG
中是简单的,
for和while的循环结构在tai-e中转化为简单的goto stmt.
因此此处笔者着重说明If和Switch在Tai-e
框架中的处理方式.
控制流不可达代码
源代码如下:
1 | public class ControlFlowUnreachable { |
代码对应IR如下:
1 | -------------------- <ControlFlowUnreachable: int foo()> -------------------- |
CFG 可视结果如下:
不难发现, 控制流不可达代码在可视化图中的表示是独立于Entry到Exit 路径的子图
If Stmt
我们先来看看最基础的情况:
1 | class UnreachableIfBranch { |
1 | -------------------- <UnreachableIfBranch: int branch()> -------------------- |
可以从可视化CFG 中获取不少信息.
[line]:if(ConditionExp) goto [line]是if stmt结构的入口(图中为2: if(x > y) goto 4), tai-e框架会为if stmt结构提供一个统一的nop出口(图中为9: nop).if结构会根据ConditionExp的T/F取值生成Edge, 分为IF_FALSE和IF_TRUE,Edge Target分别为T/F 的代码块. 可以观测到框架会为T/F的代码块添加一个nop stmt作为代码块的起始(图中为4: nop和7: nop).
如果我们不显式指定else结构:
1 | class UnreachableIfBranch { |
1 | -------------------- <UnreachableIfBranch: int branch()> -------------------- |
- 可以看到
if结构的IF_FALSE对应的代码块消失,IF_FALSE直接跳转到if结构的结束nop语句.(图中为6: nop)Tips:
如果显式写出
else但else为空体, tai-e框架会生成一条nop语句对应该else空体, 感兴趣的读者可以自行测试, 笔者在此不再赘述.通过阅读框架代码中的
Class If extends JumpStmt和Class JumpStmt可以知晓,If将JumpStmt的成员变量target和对应方法(如getTarget())和If结构中的IF_TRUE对应起来, 因此此时的Edge IF_FALSE其实等价于FALL_THROUGH,IF_FALSE的target是一条指向真正False代码块的goto stmt. 通过阅读框架代码, 便不难理解为何CFG 要这样处理IF结构.
if-else的嵌套等价于普通的if-else的组合.
1 | class UnreachableIfBranch { |
1 | -------------------- <UnreachableIfBranch: int branch()> -------------------- |
Switch Stmt
与If一样, 我们先来看看一般情况:
1 | class UnreachableSwitchBranch { |
1 | -------------------- <UnreachableSwitchBranch: void lookupSwitch()> -------------------- |
- 与
If一致,[line]: [method]switch([var]) {[branch]}是Switch结构的起点(图中为19: lookupswitch(y) {2->4, 4->7, 8->10, default->13}). 同样会生成一条nop stmt表示Switch结构的出口.- 经过笔者的多次测试, tai-e框架的
Switch语句处理有些的微妙的奇怪之处. 与If不同,Switch语句的入口在line num上位于Switch结构的最后部分, 到达该入口需要经过两条nop stmt, 并且会在CASE和DEFAULT代码块之外添加一个到Switch结构出口的goto stmt. 然而由于DEFAULT一定存在(下文会解释), 因此这条goto语句一定为死代码, 不存在一条从Entry到该语句的路径.Tips: 笔者认为这是一个非常奇怪的处理, 猜测可能跟Soot生成的字节码有关(Tai-e框架基于Soot搭建) 该猜想未证实.
- 笔者推测
CASE和DEFAULT代码块中的第一条nop语句来源是case 2:和default:, 即Tai-e框架选择保留了原来的控制信息语句(控制信息在CFG中体现为Edge), 但将其替换成了nop语句(该推测在不显示指定default时得到部分证实).
不显式指定default时:
1 | class UnreachableSwitchBranch { |
1 | -------------------- <UnreachableSwitchBranch: void lookupSwitch()> -------------------- |
- 可以观测到CFG 会添加未被显式写出的
default, 但不生成default指向的代码块(如果显示写出default但为空体, 会保留nop和goto跳转, 笔者在此不再赘述, 感兴趣的读者可以自行测试), 而是直接指向Switch结构的出口.
case & break
了解java语法性质的读者应该理解笔者在本小结的标题.
我们来看看如下代码:
1 | public class Main { |
与预期不同, 这段代码的输出如下:
1 | java Main |
当switch匹配2成功时, 如果不加break,
会顺次匹配在case 2:之后的所有case.
这称为case的穿透性.
我们来看看CFG是如何处理这种情况的:
1 | class UnreachableSwitchBranch { |
1 | -------------------- <UnreachableSwitchBranch: void lookupSwitch()> -------------------- |
- 这是一个符合预期的处理, 也是一个非常自然而然的想法.
DeadCodeDetection
本次实验只需要完成Class DeadCodeDetection中的analysis()方法.
准备工作
将A1,
A2中完成的活跃变量分析和常量传播代码copy到本次实验A3的工作目录中.
需要注意的是,
本次活跃变量分析部分不使用IteratorSovler而是使用BackwardWorklist,
此部分代码需要额外完成.
Tips: 不要过多的依赖诸如ChatGPT或Copilot的书写代码功能, 其生成的代码很可能存在潜在的不易发现的bug(
你猜我为什么会加这个Tips). 实际上,JetBrain IDEA的成员变量和方法补全功能已经足够好使, 使用Copilot的代码补全反而可能造成一些不必要的麻烦.
控制流不可达代码实现细节
实验指南已写明该部分的具体原理, 此处涉及一些实现细节.
- 通过恰当的维护
BFSlist可以仅遍历可达代码(主要处理If和Switch), 因此初始化所有的Stmt为DeadCode, 遍历到即排除其为死代码是一个不错的选择. - 无论使用BFS还是DFS,
都要处理环路引起的Infinity Loop,
一个简单的方法是创建一个
Traveled Set保证每个Stmt仅会被遍历到一次(同样的, 对于BFSList维护也可以通过类似的技巧提升效率). - 遍历一次就能得到结果依托于不需要处理由删除死代码产生的死代码(例子可见实验指南).
- 关于图的遍历方式,
笔者在此推荐使用
CFG的顶层方法getSuccOf()和getOutEdge(), 而不是If Stmt和Switch Stmt中的方法. 这两个跳转语句的实现因为继承自JumpStmt所以细节上可能与想象中有所差异, 如果一定要使用, 还请小心谨慎的阅读API注释以获取详细信息. - 关于
ConditionExp的处理, 可以复用A2中实现的evaluate()方法.
无用赋值代码实现细节
同上, 原理部分请参见实验指南.
- 继承
AssignStmt的子类有足足十几个, 可以从lib反编译或者科研版代码中获取相关细节, 关于副作用(Side Effect)在框架代码中已给出API, 解释了哪些派生类可能引起副作用.
1 | /** |
- 小心, 不要忘记判断
lValue是否为Var, 与之前相同, 我们只需要处理Var类型的语句.
总结
本次实验难度某种意义上低于A1, 是一个比较简单的实验, 旨在考察对分析结果的应用能力. 祝早日AC!