跳到主要内容

技术研究:Java 反编译之 If 语句解析(二)

· 阅读需 6 分钟
Yak ProjectYak Project

**前文指路:**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 条件与源码相反,ifelse 的 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

breakcontinue 在字节码中表现为 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 语句的结构、识别方式及反编译策略。通过流程图、控制流图、支配关系等方法,可以准确地识别 ifelse、三元表达式等结构,并为后续的语法还原和优化打下基础。


本文首发于 Yak Project 公众号,阅读原文