技术研究:Java 反编译之 If 语句解析(二)
**前文指路:**Yak,公众号:Yak ProjectJava 反编译技术(一)
在上一篇文章中,我们讨论了与控制流图(CFG)和支配图相关的概念。从这篇开始,我们将利用这些概念,介绍具体的控制流还原案例。本篇主要针对 if语句的相关案例进行介绍。
识别 If 语句
在 JVM 中,if 语句对应的字节码指令有:
OP_IFEQ, OP_IFNE, OP_IFLE, OP_IFLT, OP_IFGT, OP_IFGE,
OP_IF_ACMPEQ, OP_IF_ACMPNE,
OP_IF_ICMPLT, OP_IF_ICMPGE, OP_IF_ICMPGT, OP_IF_ICMPNE,
OP_IF_ICMPEQ, OP_IF_ICMPLE,
OP_IFNONNULL, OP_IFNULL
这些指令与源码中的 if 语句一一对应。
常见if语句案例
案例 1: 标准的 if-else 结构
源码:
voidmain() {
int a = 1;
if (a > 1) {
a = 2;
} else {
a = 3;
}
return;
}
对应字节码:
0: iconst_1
1: istore_1
2: iload_1
3: iconst_1
4: if_icmple 12
7: iconst_2
8: istore_1
9: goto 14
12: iconst_3
13: istore_1
14: return
if_icmple 表示:当栈顶两个元素满足 <= 时跳转。翻译后逻辑为:
if (a <= 1) {
a = 3;
} else {
a = 2;
}
可以看到,字节码中的 if 条件与源码相反,if 和 else 的 body 也被交换,但逻辑等价。
结论: 字节码中大多数 if 条件是反转的,因此在反编译时应对条件取反,同时互换 if 和 else 的 body。
流程图如下:
可以看出从 if 相关指令开始,出现两条分支,分别对应 if body 和 else body 的代码块。两个 body 最终在一个点汇合标志着if语句的结束。
案例 2: 无 else 的 if 语句
流程图如下:
可以看出 if 指令指向两个节点,一个节点是 if body,另一个节点是 if 语句的结束。
案例 3:逻辑运算
一般来说,if 语句的 cfg 应该是一个树状的,不存在一个代码块同时属于多个 body的情况。
如图:
但逻辑运算表达式对应字节码的 cfg 会出现两个 if 语句 body 重叠的情况,如下:
源码:
if (a > 1 || a > 0){
a = 2;
}
问题:如果直接翻译会出现下面的情况,源码中的 if body 出现了两次。
if (a > 1) {
a = 2;
} else {
if (a > 0) {
a = 2; // 这里的body和if (a > 1)的body相同
}
}
为了解决这种问题,需要对指令中的 if 语句化简,将这种特殊情况的 if 语句嵌套转为逻辑运算。
案例4: break / continue
break 和 continue 在字节码中表现为 goto,会干扰 if body 的范围识别。
如图:
可以看出,a = 1 是循环的入口点,end 为循环结束。图中的 goto 指向循环结束,说明 goto 其实是 break 语句。
可以将上面的图等价变为下面的图:
解决方案:在解析 if 语句前,先识别所有循环结构,并将 goto 恢复为 break / continue,补充 LoopEnd 节点。
案例5:三元表达式
在之前的案例中,if 语句的 body 从开始到结束,虚拟机栈深是不变的,也就是 body 内是完整的代码块。
但在三元表达式的情况下,body 可能是向栈 push 一个 constant。
源码:
int a = b > c ? 1 : 0;
字节码:
0: iload_1
1: iload_2
2: if_icmple 5
3: iconst_1
4: goto 6
5: iconst_0
6: istore_3
问题:
if的两个分支都从开始到结束栈深发生变化istore才是真正的赋值语句if body/else body无法单独构成完整语句
【解决方案】
方案一:引入中间变量
int tmp;if (b > c) { tmp = 1;} else { tmp = 0;}int a = tmp;
方案二:栈模拟
- 模拟所有指令的栈深
- 在汇合节点处栈深发生变动,说明是三元表达式,针对三元表达式进行化简
- 将
if替换为?:表达式
小结
实际情况更复杂,例如在 body 中执行函数、执行赋值、嵌套等操作:
int a = b > 1 ? getValue() : b < 2 ? c = b : c = 1;
int a = (b > 1 ? object1 : object2).getValue();
经过验证,栈模拟的方案才能解决这种复杂的情况。在实际反编译过程中需要将整个 if 语句替换为一个占位变量,在还原并化简 if 语句后,尝试将 if condition 和栈中的操作数替换到占位变量中。
案例 6: 比较运算
在 java 中的比较运算也是通过 if 指令实现,例如:
bool a = b > 1;
上面的代码等价于:
bool a = b > 1 ? true : false
所以同理的,可以转为三元表达式的情况处理,最后再对三元表达式化简从而还原源码。
小结
if 的结束点的必要条件
1.被 if 节点支配
2.被 if body 起始节点支配
3.被 else body 起始节点支配
条件表达式的简化
1.将前缀表达式转为后缀表达式
2.对逻辑表达式进行归约与优化
解析 if 语句
if指令跳转地址可能是 body 开始,也可能是结束- 汇合节点即为
if的结束点(不一定被 if 指令支配) - 若两个
if指向同一个 body,且汇合节点相同,可尝试合并为逻辑表达式 - 反编译时需注意
if条件的反转 - 特殊情况如
break/continue、三元表达式需额外处理
总结
本篇文章深入分析了 Java 字节码中if 语句的结构、识别方式及反编译策略。通过流程图、控制流图、支配关系等方法,可以准确地识别 if、else、三元表达式等结构,并为后续的语法还原和优化打下基础。
本文首发于 Yak Project 公众号,阅读原文。
