Java反编译器剖析(中) - ImportNew Your browser (Internet Explorer 7 or lower) is out of date. It has known security flaws and may not display all features of this and other websites. Learn how to update your browser. X 首页 资讯 Web Android 架构 基础技术 职业生涯 书籍 教程 视频 投稿 资源 首页 资讯 Web Android 架构 基础技术 职业生涯 书籍 教程 视频 投稿 资源 Java反编译器剖析(中) 分享到: 本文由 ImportNew - 邬柏 翻译自 javacodegeeks。如需转载本文,请先参见文章末尾处的转载要求。Importnew注:如果你也对Java技术翻译分享感兴趣,欢迎加入我们的Java开发小组。参与方式请查看小组简介。 在上一篇文章中,我们介绍了翻译器的功能、简单的字节码知识回顾、反编译和栈分析。本文将继续讨论反编译器中对条件表达式、变量类型分析、短路运算符和方法调用在反编译器中的处理。 条件表达式 在这里可以决定我们的代码是否使用了三元运算符(?:):有一个判断条件,条件的每个分支都对同一个栈变量 s{1,2} 进行一次赋值,赋值后两条路径会进行合并。 一旦确定了这个模式,就可直接使用三元表达式。 复制传播后 合并三元表达式 0 1 4 5 8 9 10 11 if (v0 == 0) goto #8 s{1,2} = v1 goto 9 s{1,2} = v2 v3 = s{1,2}return v3 v3 = v0 != 0 ? v1 : v2 return v3 值得注意的是,作为转换的一部分,我们对 #9 处的条件进行了取反。可以看出 javac 生成的代码对判断条件取反这一行为是有规律的。因此,如果将转换后的条件取反,就可以更加接近原来的代码。 画外音:类型是什么? 当处理栈值时,JVM使用了一个比 Java 代码更为简单的类型系统。特别是 boolean、char 和 short 的值都被作为 int 值使用同一指令处理。因此, v0! = 0 可以翻译成: v0 != false ? v1 : v2 或者 v0 != 0 ? v1 : v2 甚至还可以翻译为 v0 != false ? v1 == true : v2 == true ……还有很多其它的翻译结果! 在这个例子中,我们很幸运地知道 v0 的精确类型,这个类型包含在方法描述中: descriptor: (ZII)I flags: ACC_PUBLIC, ACC_STATIC 方法签名由此可以知形式如下: public static int plus(boolean, int, int) 通过签名还可以知道,v3 是 int 型(而不是 boolean 型)。因为它是返回值,通过描述符已经知道了返回值类型。接下来,还需要翻译: v3 = v0 ? v1 : v2 return v3 另外,如果 v0 是一个本地变量(不是形参),可能无法知道其类型是 boolean 而不是 int。还记得我们之前提到的本地变量表,就是包含了原始本地变量名的那个表吗?除了变量名,它还记录了有变量的类型。因此,如果编译时带有debug信息,就可以从本地变量表中知道变量的类型。此外,还有一张 LocalVariableTypeTable 表,此表也包含类似的信息。两者的主要区别在于 LocalVariableTypeTable 包含了泛型信息。然而,由于 LocalVariableTypeTable 中的信息是未经验证的元数据,因此不能完全依赖这些数据。一些非常规的混淆器(obfuscator)会在这些表中填入假信息,但是修改后的字节码却依然可以执行!所以请自行决定如何使用这些表。 短路运算符(‘&&’ 和 ’||’) public static boolean fn(boolean a, boolean b, boolean c){ return a || b && c; } 怎么能更简单呢?不幸的是,关于字节码的理解总是有一点痛苦…… 字节码 栈变量 复制传播后 0 1 4 5 8 9 12 13 16 17 iload_0 ifne #12 iload_1 ifeq #16 iload_2 ifeq #16 iconst_1 goto #17 iconst_0 ireturn s0 = v0 if (s0 != 0) goto #12 s1 = v1 if (s1 == 0) goto #16 s2 = v2 if (s2 == 0) goto #16 s3 = 1 goto 17 s4 = 0 return s{3,4} if (v0 != 0) goto #12 if (v1 == 0) goto #16 if (v2 == 0) goto #16 s{3,4} = 1 goto 17 s{3,4} = 0 return s{3,4} 根据选择的路径不同,位于 #17 位置的 ireturn 指令可能返回 s3 或者 s4。我们为其分别命名,然后使用复制传播来消除 s0、s1 和 s2。 接下来,在 #1、#5 和 #7 位置有三个连续的条件。如之前提到的那样,条件分支要么跳转,要么接着执行下一条指令。 上面的字节码包含了一组遵循特定的使用模式,这些模式非常实用: 条件与(&&) 条件或(||) T1: if (c1) goto L1 if (c2) goto L2L1: …变成了 if (!c1 && c2) goto L2L1: … T1: if (c1) goto L2 if (c2) goto L2L1: …变成了 if (c1 || c2) goto L2L1: … 如果考虑上面表中的临近条件组,#1 … #5 不遵循上面任何一种模式,但 #5 … #9 却是一个条件或(||),因此可以进行如下转换: 1: if (v0 != 0) goto #12 5: if (v1 == 0 || v2 == 0) goto #16 12: s{3,4} = 1 13: goto #17 16: s{3,4} = 0 17: return s{3,4} 注意:每次转换都可能引入新的转换。这种情况下,可以应用 || 对条件进行重组。现在可以对 #1...#5 应用 && 模式!通过将这些代码合并为单个条件分支可以进一步简化方法: 1: if (v0 == 0 && (v1 == 0 || v2 == 0)) goto #16 12: s{3,4} = 1 13: goto #17 16: s{3,4} = 0 17: return s{3,4} 这是不是看起来和其他地方很类似?是的,现在这个字节码就符合之前的三元操作符(? :)规则了。我们可以将 #1...#16 缩减为一个独立的表达式,再使用复制传播将 s{3,4} 内联到为 #17 的 return 语句。 return (v0 == 0 && (v1 == 0 || v2 == 0)) ? 0 : 1; 利用方法描述符和本地变量类型表可以推断变量类型,这样缩减后的表达式如下: return (v0 == false && (v1 == false || v2 == false)) ? false : true; 好吧,现在的结果比反编译的内容更加精炼了,但是仍然不够美观。让我们看看可以做点什么。首先,折叠比较运算符,比如把 x==true 和 x==false 简写为 x 和 !x。还可以消除三元操作符,比如把 x ? false:true 简写为 !x。 return !(!v0 && (!v1 || !v2)); 如果你还记得你高中的离散数学,那么根据德摩根定理,更进一步可以缩写为: !(a || b) --> (!a) && (!b) !(a && b) --> (!a) || (!b) 因此, return ! ( !v0 && ( !v1 || !v2 ) ) 可以变为, return ! ( !v0 && ( !v1 || !v2 ) ) 接着变成, return ( v0 || !(!v1 || !v2 ) ) ……最终会变成: return ( v0 || (v1 && v2) ) 万岁! 处理方法调用 我们已经了解调用方法的流程:先将参数“存入”本地数组;要进行方法调用,必须将参数推到栈上,并且紧跟一个指向实例方法的 this 指针。方法调用的字节码正如你预想的那样: push arg_0 push arg_1 invokevirtual METHODREF 在上面的代码中可以看到 invokevirtual,该指令可以用来调用大多数的实例方法。JVM有一组方法调用的指令,每个指令都有特定的功能: invokeinterface:调用接口方法。 invokevirtual:调用使用 virtual 语义的实例方法,比如调用的方法在运行时根据重载分派到不同的实例方法。 invokespecial:调用一个具体的实例方法(非 virtual 语义)。该指令常用来调用构造器(constructor),但也可以调用类似 super.method() 这样的方法。 invokestatic:调用静态方法。 invokedynamic:使用“引导方法”(bootstrap)启动自定义调用点,该命令(在Java中)很少使用。引入该命令是为了支持动态语言,在Java8中被用来实现lambda表达式。 反编译器有一个重要细节,class的常量池中包含了所有方法调用的信息,包括参数的数量、类型和返回值类型。调用的类会记录这些信息,运行时会确保该方法在调用时已存在,并对方法签名进行检查。如果调用的是第三方代码的函数,并且函数的签名发生了改变,任何试图对旧版本的调用都会抛出错误(而不是产生不可预知的行为)。 回到上面的例子,从 invokevirtual 操作码可以得知目标方法是一种实例方法。因此,需要将 this 指针作为隐含的第一参数。常量池中的 METHODREF 告诉我们该这个方法有一个形参,所以除了实例方法的指针还需要从栈上弹出一个参数。接下来代码可以重写为: arg_0.METHODREF(arg_1) 当然,不是所有的字节码看起来都如此“友好”。栈中的参数并不要求一个接一个排列整齐。假如参数中有一个三元表达式,那么中间就会有加载、存储和分支指令,这些都需要单独转换。混淆器可能会将方法重写成为一种特别复杂的指令序列。优秀的反编译器需要足够灵活,才能处理很多有趣的边界情形。这些已经超出了本文的讨论内容。 下一篇我们会继续探讨反编译器的更多细节和流程控制。 原文链接: javacodegeeks 翻译: ImportNew.com - 邬柏译文链接: http://www.importnew.com/9206.html[ 转载请保留原文出处、译者和译文链接。] 相关文章Java反编译器剖析(下)Java反编译器剖析(上)Netty 教程系列Android性能优化案例研究(上)Hadoop教程(二)JVM并发机制的探讨——内存模型、内存可见性和指令重排序免费在线阅读“Gradle Beyond the Basics”Java垃圾回收精粹 — Part1Servlet 3特性:异步Servlet在测试中使用匹配器 邬柏 (新浪微博:@Alex_Aisin-Gioro) 没有评论邬柏2014 年 2 月 6 日基础技术Decompiler 发表评论 取消回复 name email (not published) website 欢迎关注并订阅ImportNew 近期热评文章 Integer.valueOf(String) 方法之惑… 怎么理解Condition Java中的String对象是不可变的吗… Java 泛型: 什么是PECS(Producer Extend… Java反射教程 Java8学习:Lambda表达式、Stream API和功能性… HashMap vs. TreeMap vs. Hashtabl… Java中的equals()和hashCode()契约… 一年内,Nexus 7 从最好到最烂… 为什么要使用SLF4J而不是Log4J… 最新评论Viva 发表在《如何用Map对象创建Set对象》死亡飞猪 发表在《10个有关String的面试问题》winter 发表在《一年内,Nexus 7 从最好到最烂》johnHe 发表在《Java中的String对象是不可变的吗》帅克 发表在《HashMap的工作原理》blackwiz 发表在《Integer.valueOf(String) 方法之惑》darkmi 发表在《怎么理解Condition》wanghaoinfo 发表在《Integer.valueOf(String) 方法之惑》Ian 发表在《Java反射教程》郭龙 发表在《有经验的Java开发者和架构师容易犯的10个错误(上)》 关于我们ImportNew 是一个专注于 Java 技术分享的博客。于2012年11月11日 11:11正式上线。是的,这是一个很特别的时刻 :) ImportNew 由两个 Java 关键字 import 和 new 组成,意指:Java 开发者学习新知识的网站。 import 可认为是学习和吸收, new 则可认为是新知识、新技术圈子和新朋友等等。联系我们Email: ImportNew.com@gmail.com 新浪微博:@ImportNew 微信号:importnew 广告与商务合作Q:630772296 (加Q请注明来意)加入我们Java小组 // 和我们一起分享与翻译Java技术文章 © Copyright 2014 ImportNew.com 京ICP备13000878号