前往Shuct.Net首页

Shudepb PB反编译专家长时间以来,为业内同类软件事实上的唯一选择.细节,彰显专业.态度,决定品质.

关于反编译的搜索

Java反编译器剖析 - WEB开发者 WEB开发者导航 WEB开发者论坛 --> 收藏本站 首页 XHTML/CSS JavaScript HTML5/CSS3 ASP.NET Java WEB PHP Python RubyOnRails Flash C/C++ Android 编程语言 数据库 服务器 操作系统 云计算/大数据 移动开发 前端 后端 算法 设计模式 浏览器 安全 软件工程 WEB综合 开发手册 UI/UE UI素材 SEO IDC 运营推广 业界资讯 观点评论 其它 职场 创业 程序人生 幽默 段子 首页&#187;Java WEB&#187;Java反编译器剖析 Java反编译器剖析 来源:importnew 发布时间:2014-02-07 阅读次数:   反编译器(或者解码器),简而言之,就是将目标程序码反转成源代码。但是其中的过程却比较复杂,也很有意思&mdash;&mdash;Java源码是结构化的,字节码却不是。而且,转换不是一一对应的:两段完全不同的Java程序也可能生成完全相同的字节码,有时需要一些试探才能更加接近源码。  (一段简短的)字节码教程   为了更好的理解反编译器如何工作,现在有必要理解一下字节码基础。如果你对此非常熟悉,可以略过此处直接跳到下一部分。   (不同于基于寄存器 register-based 的方式)JVM运行基于栈。这就意味着指令会在 evaluation stack(计算堆栈)上执行。操作对象可能先出栈,进行一些操作,然后再把结果入栈来进行接下来的操作。考虑如下场景: public static int plus(int a, int b) { int c = a + b; return c; }   注:本文所有的相关的字节码都是由 javap 产生,例如执行命令 javap -c -p MyClass。 public static int plus(int, int); Code: stack=2, locals=3, arguments=2 0: iload_0 // load &lsquo;x&rsquo; from slot 0, push onto stack 1: iload_1 // load &lsquo;y&rsquo; from slot 1, push onto stack 2: iadd // pop 2 integers, add them together, and push the result 3: istore_2 // pop the result, store as &lsquo;sum&rsquo; in slot 2 4: iload_2 // load &lsquo;sum&rsquo; from slot 2, push onto stack 5: ireturn // return the integer at the top of the stack   方法中的本地变量(包括方法声明)被寄存在所谓的JVM本地变量数组中。为了简单起见,在这里我们将一个存放在本地变量数组位置 #x 处的变量称为 slot#x (参见JVM规范3.6.1)。   对于示例方法,slot#0 的值一般是 this 指针。然后从左到右依次是方法中的各个变量,接下来是方法中声明的本地变量。在上面的示例中,由于方法是静态的,所以没有 this 指针。相应的 slot#0 存放的是参数 x,slot#y 存放的是参数 y,本地变量 sum 存放在 slot#2 中。   有意思的是,每个方法的栈大小和本地变量存储空间都有最大值的限制。二者都是在编译时决定。   目前为止,所有内容都是非常直白的,仅有一点没有达到你的预期:编译器一直没有尝试去优化这些代码。事实上,javac 几乎从未支持字节码优化。这样有很多好处,比如几乎可以在任何地方设置断点:一旦移除 load/store 操作,就会失去这种特性。所以,大部分压力都转移到了运行时JIT编译器(just-in-time compiler)。  反编译   那么,怎样才能将一个非结构化、基于栈的字节码转换为结构化的Java代码呢?通常,第一步要先摈弃操作对象栈。可以通过映射栈的值成变量,并插入合适的 load/store 操作来实现这个步骤。   如果一个&ldquo;栈变量&rdquo;仅仅分配并使用一次,你会发现这将产生非常多的重复变量&mdash;&mdash;而且接下来会生成的重复变量会更多!反编译器会将这些字节码缩减成更简单的指令集。这里对此不作深究。   我们使用 s0 代表栈变量, v0 代表原始的字节码在本地的真实引用(存在 slot 上)。 字节码 栈变量 复制传播 0 1 2 3 4 5 iload_0 iload_1 iadd istore_2 iload_2 ireturn s0 = v0 s1 = v1 s2 = s0 + s1 v2 = s2 s3 = v2 return s3 v2 = v0 + v1 return v2   通过为 push 或 pop 的每个值分配一个标识符,可以将字节码转换为本地变量。比如 iadd 是将两个操作数出栈并、相加,并将结果入栈。   然后,使用一种复制传播(copy propagation)的技术,可以消除一些重复变量。复制传播是内联的一种形式,可以将变量简单替换为指定值,前提是这种转换是有效的。   如何定义&rdquo;有效性&ldquo;?这里包含了一些重要准则。考虑下面这种情况: 0: s0 = v1 1: v1 = s4 2: v2 = s0 <-- s0 cannot be replaced with v1   在这里,如果将 s0 替换为 v1 结果将大不相同。因为 v1 的值在 s0 被指定之后改变了,虽然此时 v1 的值却还没有被使用(译注:原文这里是V0,根据注释可以确认为笔误)。为了避开这种复杂的情形,这里复制传播只考虑仅被赋值一次的内联变量(inline variable)。   译注:一个简单的(C语言)内联变量手动解析示例,来自Wikipedia:inline expansion int pred(int x) { if (x == 0) return 0; else return x - 1; }   进行 inline 操作前: int f(int y) { return pred(y) + pred(0) + pred(y+1); }   进行 inline 操作以后: int f(int y) { int temp; if (y == 0) temp = 0; else temp = y - 1; /* (1) */ if (0 == 0) temp += 0; else temp += 0 - 1; /* (2) */ if (y+1 == 0) temp += 0; else temp += (y + 1) - 1; /* (3) */ return temp; }   一种改进的方案&mdash;&mdash;跟踪所有非栈变量的存储空间。比如,我们知道 v1 在 #0 赋值给 v10,同时在 #2 被赋值给 v11。当对 v1 赋值超过一次,则不能进行复制传播。   不过我们最初的那个例子没有这么复杂,因而我们得到如下的优美精确的结果: v2 = v0 + v1 return v2  画外音:存储变量名   如果变量在字节码中被简化为 slot 的引用,那么接下来怎样才能知道原来对象的名称呢?很有可能无法知道。为了改变这情况,改进调试的用户体验,每个方法的字节码都包含有一个特殊的部分&mdash;&mdash;本地变量表。这个表中记录了原代码中每个变量的名称、slot 编号和变量名对应的字节码。通过 javap 的 -v 选项可以把本地变量表(以及其他有用的元数据)包含到反汇编代码。对于上面示例中的 plus() 方法,它的本地变量表看起来像下面这样: Start Length Slot Name Signature 0 6 0 a I 0 6 1 b I 4 2 2 c I   可以看到 v2 是 int 类型的变量,原来变量名为 c,偏移位于字节码 #4-5。   如果编译的类没有包含本地变量表(也可能被混淆器删掉),必须自己生成变量名。处理这种情况有很多办法:聪明的方法会根据变量的使用情况定义合适的名字。  栈分析   前面的示例中,在任何时刻都可以确保栈顶的变量,因此可以依次命名为 s0、s1等。   目前为止,在处理变量的时候都是比较直接的,因为我们仅仅采用一种代码路径来探索方法。在真实的应用环境里,多数的方法都不是那么&rdquo;善解人意&ldquo;。每当为方法增加一个循环或者判断,就会增加了很多可能的调用情况。让我们来看一下改进版的示例: public static int plus(boolean t, int a, int b) { int c = t ? a : b; return c; }   现在情况更加复杂,如果按照之前的分配方式操作,将会遇到很大的问题。 字节码 栈变量 0 1 4 5 8 9 10 11 iload_0 ifeq 8 iload_1 goto 9 iload_2 istore_3 iload_3 ireturn s0 = v0 if (s0 == 0) goto #8 s1 = v1 goto #9 s2 = v2 v3 = {s1,s2} s4 = v3 return s4   我们需要对如何用栈标识符赋值更加谨慎。由于可能有多个路径能够到达,因此仅考虑每个指令自身是不够的,需要对给定的位置查看整个栈的情况。   在我们检查 #9 的时候,看到 istore_3 出栈了一个值。但是这个值可能有两个来源,可能来自于 #5 或者 #8。栈顶 #9 的值可能是 s1 也可能是 s2,这取决于它是来自于 #5 还是 #8。因此,我们认为这可能是同一个变量&mdash;&mdash;因此我们将其合并,所有引用 s1 或者 s2 的地方都指向这个无歧义的变量 s{1,2}。&rdquo;重新标记&ldquo;(relabeling)后,可以安全地进行复制传播。 重新标记后 复制传播后 0 1 4 5 8 9 10 11 s0 = v0 if (s0 == 0) goto #8 s{1,2} = v1 goto #9 s{1,2} = :v2 v3 = s{1,2} s4 = v3 return s4 if (v0 == 0) goto #8 s{1,2} = v1 goto #9 s{1,2} = v2 v3 = s{1,2}return v3   值得注意的是:在 #1 处的条件分支:如果 s0 的值是0,就跳到 else 块;否则,继续当前的路径。有趣的是,与原始代码相比,这里测试条件是取反的。   接下来将我们进行更深入的研究&hellip;&hellip; 上一页123下一页 【QQ交流群】Java Web技术专区:176206386 验证消息:Admin10000 Java | 反编译 提示:转载本站内容请用超链接形式注明来源 相关文档 Google的Java编码规范 Java的历史 深入理解Java HelloWorld Java编程细节之十个最佳实践 Java之父逐个评价甲骨文对Sun技术的处 2013最吃香的技能:Java称霸、Androi Java最困扰你的那些事 Java百问 java的LINQ :Linq4j简明介绍 Java 8 是否还需要 LINQ?还是已经比 Java 中使用内存映射文件需要考虑的 你应该远离的6个Java特性 Java 日志管理最佳实践 Java程序员应该知道的10个面向对象理 网友评论(共0条评论) 正在载入评论...... 理智评论文明上网,拒绝恶意谩骂 发表评论 / 共0条评论 登录会员中心 阅读排行 使用Eclipse调试Java 程序的10个技巧用来理解 Java 编程语言的 8 个图表推荐一些国外高质量Java开发者的博客Java开发牛人十大必备网站Java的内存回收机制5款工具助你写出更好的Java代码Java 8 的新特性和改进总览Java 代码优化过程的实例介绍直接拿来用!超实用的Java数组技巧攻略Java程序员应该知道的10个面向对象理论 推荐文档 Java枚举的七种常见用法多线程的概念Servlet和JSP规范及版本对应关系如何学习Java Web开发通过 JSP Model 深入学习 MVC详细介绍JSP技术的两种架构模型常见的Java WEB服务器最为流行的几款Java IDEJSP九大内置对象详解JAVA SSH 框架介绍 随机文档 26个提升java性能需要注意的地方对Java初学者的忠告Java长存!12个Java长久占居主要地位的原因5款工具助你写出更好的Java代码使用 Java 测试网络连通性的几种方法常见的Java WEB服务器和Lambdas的第一次亲密接触J2EE中你必须了解的13种技术规范Hibernate的10个常见面试问题及答案10 个Java 编码中微妙的最佳实践 关于我们 | 版权声明 | 免责条款 | 隐私政策 | 网站合作 | 捐赠本站 | 建议留言 Copyright &copy; 2012 Admin10000.com All rights reserved 陕ICP备06007334号