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!