Android 反编译教程(一)
零、前言
《反编译 Java》最初出版于 2004 年,由于多种原因,对于对反编译感兴趣的人来说,它更像是一本深奥的书,而不是面向普通编程读者的任何东西。
早在 1998 年我开始写这本书的时候,网站上有很多小应用,一想到有人可以下载你的辛勤工作并将其逆向工程成 Java 源代码,对许多人来说是一个可怕的想法。但是小程序和拨号上网走的是同一条路,我怀疑这本书的许多读者从来没有在网页上见过小程序。
在这本书出版后,我意识到有人反编译你的 Java 类文件的唯一方法是首先侵入你的 web 服务器并从那里下载它们。如果他们做到了这一点,你就比人们反编译你的代码要担心得多。
除了一些明显的例外——比如 Corel 的 Java for Office 作为桌面应用运行,以及其他 Swing 应用——十多年来,Java 代码主要存在于服务器上。客户端浏览器上几乎没有任何东西,对类文件的零访问意味着零反编译问题。但奇怪的是,随着 Android 平台的出现,这一切都发生了变化:你的 Android 应用存在于你的移动设备上,可以被编程知识非常有限的人轻松下载并进行逆向工程。
一个 Android 应用以 APK 文件的形式下载到你的设备上,该文件包括所有的图像和资源以及存储在单个classes.dex文件中的代码。这是一种与 Java 类文件非常不同的格式,设计用于在 Android Dalvik 虚拟机(DVM)上运行。但是可以很容易地将其转换回 Java 类文件,并反编译回原始源代码。
反编译是将机器可读代码转换为人类可读格式的过程。当一个可执行文件或者一个 Java 类文件或者一个 DLL 被反编译时,你并不能完全得到原始的格式;相反,你得到的是一种伪源代码,它通常是不完整的,而且几乎总是没有注释。但是,通常,理解原始代码就足够了。
反编译 Android 解决了编程社区中未被满足的需求。出于某种原因,反编译 Android APKs 的能力在很大程度上被忽略了,尽管对于任何有适当思维的人来说,将 APK 反编译回 Java 代码都相对容易。这本书通过观察那些试图恢复源代码的人和那些试图使用混淆等方法保护源代码的人目前使用的工具和技巧来重新平衡。
这本书是为那些想通过反编译来学习 Android 编程的人,那些只想学习如何将 Android 应用反编译成源代码的人,那些想保护他们的 Android 代码的人,以及最后,那些想通过构建一个.dex反编译器来更好地理解.dex字节码和 DVM 的人。
本书通过以下方式让你对反编译器和混淆器的理解更上一层楼
- Explore Java bytecode and opcode in detail.
- Study the structure of DEX file and opcode, and explain its relationship with Java class files.
- The difference between, with examples to show you how to decompile Android APK files.
- Give simple strategies to show you how to protect your code.
- Show you to build your own decompiler and obfuscator.
反编译 Android 不是一般的 Android 编程书。事实上,这与作者教你如何将想法和概念转化为代码的标准教科书完全相反。您对将部分编译的 Android 操作码转换回源代码感兴趣,这样您就可以了解最初的程序员在想什么。除了与操作码和 DVM 相关的地方,我不会深入讨论语言结构。所有的重点都放在低级虚拟机设计上,而不是语言语法上。
本书的第一部分解释了 APK 格式,并向您展示了 Java 代码是如何存储在 DEX 文件中并随后由 DVM 执行的。你还会看到反编译和混淆的理论和实践。我展示了一些反编译器的技巧,并解释了如何解开最尴尬的 APK。您了解了人们试图保护其源代码的不同方式;在适当的时候,我会揭露这些技术的任何缺陷或潜在问题,以便您在使用任何源代码保护工具之前得到适当的通知。
本书的第二部分主要关注如何编写你自己的 Android 反编译器和混淆器。您构建了一个可扩展的 Android 字节码反编译器。尽管 Java 虚拟机(JVM)的设计是固定的,但语言却不是。许多早期的反编译器不能处理 JDK 1.1 中出现的 Java 结构,比如内部类。因此,如果新的构造出现在classes.dex中,您将准备好处理它们。
一、奠定基础
首先,在这一章中,我向你介绍了反编译器的问题,以及为什么虚拟机和 Android 平台面临如此大的风险。你了解了反编译器的历史;你可能会惊讶,它们几乎和计算机一样存在了很久。因为这是一个非常情绪化的话题,我花了一些时间来讨论反编译背后的法律和道德问题。最后,如果你想保护你的代码,你会看到一些选择。
编译器和反编译器
计算机语言的发展是因为大多数正常人不能用机器代码或其最接近的等价物,汇编程序工作。幸运的是,在计算技术发展的早期,人们就意识到人类不适合用机器代码编程。诸如 Fortran、COBOL、C、VB 以及最近的 Java 和 C#等计算机语言的发展,使我们能够以一种人类友好的格式表达我们的想法,然后可以转换成计算机芯片可以理解的格式。
最基本的是,编译器的工作是将这种文本表示或源代码翻译成一系列 0 和 1 或机器代码,计算机可以将其解释为您希望它执行的操作或步骤。它使用一系列模式匹配规则来做到这一点。词法分析器标记源代码——任何错误或不在编译器词典中的单词都会被拒绝。然后,这些标记被传递给语言解析器,它将一个或多个标记与一系列规则进行匹配,并将这些标记翻译成中间代码 (VB。NET、C#、Pascal 或 Java)或有时直接转换成机器代码(Objective-C、C++或 Fortran)。任何不符合编译器规则的源代码都会被拒绝,编译会失败。
现在你知道编译器是做什么的了,但我只是触及了皮毛。编译器技术一直是一个专门的、有时是复杂的计算领域。现代的进步意味着事情会变得更加复杂,尤其是在虚拟机领域。在某种程度上,这种驱动力来自 Java 和. net。JIT 编译器试图通过优化 Java 字节码的执行来缩短 Java 和 C++执行时间的差距。这似乎是一个不可能的任务,因为 Java 字节码毕竟是解释的,而 C++是编译的。但是 JIT 编译器技术取得了显著的进步,也使得 Java 编译器和虚拟机变得更加复杂。
大多数编译器会做大量的预处理和后处理。预处理程序通过去除所有不必要的信息,如程序员的注释,并添加任何标准的或包含的头文件或包,为词法分析准备源代码。典型的后处理器阶段是代码优化,编译器解析或扫描代码,重新排序,并删除任何冗余以提高代码的效率和速度。
反编译器(这并不奇怪)将机器码或中间代码翻译回源代码。换句话说,整个编译过程是相反的。机器码以某种方式被标记化,并被解析或翻译回源代码。不过,这种转换很少会产生原始源代码,因为信息会在预处理和后处理阶段丢失。
考虑与人类语言的类比:将 Android 包文件(APK)反编译回 Java 源代码就像将德语(classes.dex)翻译成法语(Java 类文件),然后翻译成英语(Java 源代码)。一路上,一些信息在翻译过程中丢失了。Java 源代码是为人类而不是为计算机设计的,通常有些步骤是多余的,或者以稍微不同的顺序执行会更快。由于这些丢失的元素,很少(如果有的话)反编译产生原始源代码。
目前有一些反编译器,但是它们没有被广泛宣传。反编译器或反汇编器可用于 Clipper(瓦尔基里),FoxPro (ReFox 和 Defox),Pascal,C (dcc,decomp,Hex-Rays),Objective-C (Hex-Rays),Ada,当然还有 Java。即使是世界各地杜恩斯伯里迷们喜爱的牛顿,也不安全。毫不奇怪,对于解释型语言(如 VB、Pascal 和 Java)来说,反编译器更为常见,因为要传递大量的信息。
虚拟机反编译器
有过几次值得注意的反编译机器码的尝试。克里斯蒂娜·希福恩特斯的 dcc 和最近的 Hex-Ray 的 IDA 反编译器只是两个例子。然而,在机器码级别,数据和指令是混合在一起的,要恢复原始代码要困难得多(但并非不可能)。
在虚拟机中,代码只是简单地通过了预处理器,而反编译器的工作是逆转编译的预处理阶段。这使得解释的代码更容易反编译。当然,没有评论,更糟糕的是,没有规范,但也没有 R&D 成本。
为什么 Java 配 Android?
在我谈论“为什么是 Android?”我首先需要问,“为什么是 Java?”这并不是说所有的 Android 应用都是用 Java 编写的——我也包括 HTML5 应用。但是 Java 和 Android 是紧密结合在一起的,所以我真的不能离开另一个来讨论一个。
最初的 Java 虚拟机(JVM)被设计为在电视有线机顶盒上运行。因此,它是一个非常小堆栈的机器,使用有限的指令集将指令推入堆栈或从堆栈中取出。这使得使用相对较少的练习就能很容易理解这些说明。因为编译现在是一个两阶段的过程,JVM 还要求编译器传递大量信息,比如变量和方法名,否则这些信息是不可用的。当你试图理解反编译的源代码时,这些名字几乎和注释一样有用。
JVM 的当前设计独立于 Java 开发工具包(JDK)。换句话说,语言和库可能会改变,但是 JVM 和操作码是固定的。这意味着,如果 Java 现在易于反编译,它也很可能总是易于反编译。正如您将看到的,在许多情况下,反编译 Java 类就像运行简单的 DOS 或 UNIX 命令一样简单。
将来,JVM 很可能会被修改以停止反编译,但是这会破坏任何向后兼容性,并且所有当前的 Java 代码都必须重新编译。虽然这种情况以前在使用不同版本 VB 的微软世界中也发生过,但是除了 Oracle 之外,许多公司都开发了虚拟机。
让这种情况变得更加有趣的是,那些希望让自己的操作系统或浏览器支持 Java 的公司通常会创建自己的 JVM。Oracle 只负责 JVM 规范。这种情况已经发展到这样的程度,对 JVM 规范的任何基本改变都必须是向后兼容的。修改 JVM 以防止反编译将需要大量的手术,并且很可能会破坏这种向后兼容性,从而确保 Java 类在可预见的将来会反编译。
JDK 没有这样的兼容性限制,并且每个版本都添加了更多的功能。虽然第一批反编译器,如 Mocha,在 JDK 1.1 中引入内部类时失败了,但目前最受欢迎的 JD-GUI 完全能够处理内部类或 Java 语言的后续添加,如泛型。
在下一章中,你会学到更多关于为什么 Java 面临着反编译的风险,但是现在,这里是 Java 易受攻击的七个原因:
- 为了便于移植,Java 代码被部分编译,然后由 JVM 解释。
- Java 编译后的类包含大量 JVM 的符号信息。
- 由于向后兼容的问题,JVM 的设计不太可能改变。
- JVM 中几乎没有指令或操作码。
- JVM 是一个简单的堆栈机器。
- 标准应用对反编译没有真正的保护。
- Java 应用被自动编译成更小的模块化类。
让我们从一个简单的类文件例子开始,如清单 1-1 所示。
清单 1-1。 简单的 Java 源代码示例
public class Casting { public static void main(String args[]){ for(char c=0; c < 128; c++) { System.out.println("ascii " + (int)c + " character "+ c); } } }
清单 1-2 显示了使用 javap 的清单 1-1 中的类文件的输出,javap 是 JDK 附带的 Java 的类文件反汇编器。你可以很容易地反编译 Java,因为——正如你在本书后面看到的 JVM 是一个简单的堆栈机器,没有寄存器和有限数量的高级指令或操作码。
清单 1-2。 Javap 输出
`Compiled from Casting.java public synchronized class Casting extends java.lang.Object /* ACC_SUPER bit set / { public static void main(java.lang.String[]); / Stack=4, Locals=2, Args_size=1 / public Casting(); / Stack=1, Locals=1, Args_size=1 */ }
Method void main(java.lang.String[]) 0 iconst_0 1 istore_1 2 goto 41 5 getstatic #12 8 new #6 11 dup 12 ldc #2 <String "ascii "> 14 invokespecial #9 <Method java.lang.StringBuffer(java.lang.String)> 17 iload_1 18 invokevirtual #10 <Method java.lang.StringBuffer append(char)> 21 ldc #1 <String " character "> 23 invokevirtual #11 <Method java.lang.StringBuffer append(java.lang.String)> 26 iload_1 27 invokevirtual #10 <Method java.lang.StringBuffer append(char)> 30 invokevirtual #14 <Method java.lang.String toString()> 33 invokevirtual #13 <Method void println(java.lang.String)> 36 iload_1 37 iconst_1 38 iadd 39 i2c 40 istore_1 41 iload_1 42 sipush 128 45 if_icmplt 5 48 return
Method Casting() 0 aload_0 1 invokespecial #8 <Method java.lang.Object()> 4 return<`
显然,一个类文件包含了大量的源代码信息。我在本书中的目的是向您展示如何获取这些信息,并将其逆向工程为源代码。我还将向您展示可以采取哪些步骤来保护信息。
为什么选择安卓?
到目前为止,除了 applets 和 Java Swing 应用之外,Java 代码通常位于服务器端,很少或没有代码在客户端运行。随着谷歌 Android 操作系统的推出,这种情况发生了变化。Android 应用,不管是用 Java 还是 HTML5/CSS 写的,都是 apk 形式的客户端应用。这些 apk 然后在 Dalvik 虚拟机(DVM)上执行。
DVM 在许多方面不同于 JVM。首先,它是基于寄存器的机器,不像基于堆栈的 JVM。DVM 使用一个具有不同结构和操作码的 Dalvik 可执行(DEX)文件,而不是将多个类文件捆绑到一个 jar 文件中。从表面上看,反编译 APK 似乎要困难得多。然而,有人已经为您做了所有的艰苦工作:一个名为 dex2jar 的工具允许您将 dex 文件转换回 jar 文件,然后可以将其反编译回 Java 源代码。
因为 apk 存在于手机上,所以可以很容易地下载到 PC 或 Mac 上,然后进行反编译。您可以使用许多不同的工具和技术来访问 APK,还有许多反编译器,我将在本书的后面介绍。但是获取源代码最简单的方法是使用市场上的任何文件管理工具,比如 ASTRO File Manager,将 APK 复制到手机的 SD 卡上。一旦 SD 卡插入 PC 或 Mac,就可以使用 dex2jar 进行反编译,然后使用您喜欢的反编译器,如 JD-GUI。
Google 已经使得在你的构建中添加 ProGuard 变得非常容易,但是在默认情况下不会发生混淆。目前(直到这个问题得到更高的关注),代码不太可能使用模糊处理来保护,所以代码很有可能被完全反编译回源代码。ProGuard 作为混淆工具也不是 100%有效,正如你在第四章和第七章中看到的。
许多 Android 应用通过 web 服务与后端系统对话。他们在数据库中寻找物品,或者完成一次购买,或者将数据添加到工资单系统,或者将文档上传到文件服务器。允许应用连接到这些后端系统的用户名和密码通常是硬编码在 Android 应用中的。因此,如果你没有保护你的代码,并且你把你的后台系统的钥匙留在你的应用中,你就冒着有人破坏你的数据库并获得他们不应该访问的系统的风险。
不太可能,但完全有可能,有人可以访问源代码,并可以重新编译应用,让它与不同的后端系统对话,并将其用作获取用户名和密码的一种方式。这些信息可以在稍后阶段使用真正的 Android 应用访问私人数据。
这本书解释了如何隐藏你的信息,不让这些窥探的眼睛看到,并提高门槛,因此找到后端服务器的钥匙或找到存储在你手机上的信用卡信息需要比基本知识多得多的知识。
在将你的 Android 应用发布到市场之前,保护好它也是非常重要的。几个网站和论坛共享 APK,因此即使你通过发布更新版本来保护你的应用,原始的未受保护的 APK 可能仍然存在于手机和论坛上。您的 web 服务 API 也必须同时更新,这迫使用户更新他们的应用,并导致糟糕的用户体验和潜在的客户流失。
在第四章中,你会学到更多关于为什么 Android 会面临反编译的风险,但是现在这里列出了 Android 应用易受攻击的原因:
- 有多种简单的方法可以访问 Android APKs。
- 将 APK 翻译成 Java jar 文件以便后续反编译是很简单的。
- 到目前为止,几乎没有人使用混淆或任何形式的保护。
- 一旦 APK 被释放,就很难取消访问。
- 使用 apktool 等工具,一键反编译是可能的。
- apk 在黑客论坛上分享。
清单 1-3 显示了来自清单 1-1 的Casting.java文件在转换成 DEX 格式后的 dexdump 输出。如您所见,这是类似的信息,但采用了新的格式。第三章更详细地介绍了不同之处。
**清单 1-3。**dex dump 输出
Class #0 - Class descriptor : 'LCasting;' Access flags : 0x0001 (PUBLIC) Superclass : 'Ljava/lang/Object;' Interfaces - Static fields - Instance fields - Direct methods - #0 : (in LCasting;) name : '<init>' type : '()V' access : 0x10001 (PUBLIC CONSTRUCTOR) code - registers : 1 ins : 1 outs : 1 insns size : 4 16-bit code units catches : (none) positions : 0x0000 line=1 locals : 0x0000 - 0x0004 reg=0 this LCasting; #1 : (in LCasting;) name : 'main' type : '([Ljava/lang/String;)V' access : 0x0009 (PUBLIC STATIC) code - registers : 5 ins : 1 outs : 2 insns size : 44 16-bit code units catches : (none) positions : 0x0000 line=3 0x0005 line=4 0x0027 line=3 0x002b line=6 locals : Virtual methods - source_file_idx : 3 (Casting.java)
反编译器的历史
关于反编译器的历史写得很少,这很令人惊讶,因为几乎每个编译器都有一个反编译器。让我们花一点时间来谈谈它们的历史,这样你就能明白为什么这么快就为 JVM 和 DVM 创建了反编译器。
自从简陋的 PC 出现之前——不要说了,自从 COBOL 出现之前,反编译器就已经以这样或那样的形式存在了。你可以一直追溯到 ALGOL,找到最早的反编译器的例子。早在 1960 年,Joel Donnelly 和 Herman Englander 就在美国海军电子实验室(NEL)实验室编写了 D-Neliac。它的主要功能是将非 Neliac 编译的程序转换成与 Neliac 兼容的二进制文件。(Neliac 是一种 ALGOL 类型的语言,代表海军电子实验室国际 ALGOL 编译器。)
多年来,已经有了其他针对 COBOL、Ada、Fortran 和许多其他深奥的主流语言的反编译器,它们运行在 IBM 大型机、PDP-11 和 UNIVACs 等之上。这些早期开发的主要原因可能是翻译软件或转换二进制文件以在不同的硬件上运行。
最近,规避 2000 年问题的逆向工程成为反编译的可接受的一面——转换遗留代码以规避 2000 年问题通常需要反汇编或完全反编译。但是逆向工程是一个巨大的增长领域,并没有在世纪之交后消失。道琼斯指数触及 10,000 点大关和欧元的引入所引发的问题导致了金融计划的失败。
逆向工程技术也被用来分析旧代码,这些代码通常有成千上万的增量变化,以消除冗余并将这些遗留系统转换成更高效的动物。
在一个更基本的层面上,PC 机代码的十六进制转储给程序员提供了额外的洞察力,让他们了解某些东西是如何实现的,并被用来打破对软件的人为限制。例如,包含游戏和其他工具的定时炸弹或限制副本的杂志光盘经常被打补丁,以将演示副本更改为软件的完整版本;这通常是用原始的反汇编程序完成的,如 DOS 的调试程序。
任何精通汇编语言的人都可以学会快速发现代码中的模式,并绕过适当的源代码片段。盗版软件是软件业的一个大问题,而分解代码只是专业和业余盗版者使用的一种技术。因此,许多神秘的复制保护技术都失败了。但是这些都是原始的工具和技术,从头开始编写代码可能比用汇编程序重新创建源代码更快。
多年来,传统软件公司也参与了软件逆向工程。竞赛使用逆向工程和反编译工具在世界各地研究和复制新技术。一般来说,这些都是内部的反编译器,不供公众使用。
很可能第一个真正的 Java 反编译器是由 IBM 编写的,而不是由《摩卡》的作者 Hanpeter van Vliet 编写的。丹尼尔·福特的白皮书《Jive:一个 Java 反编译器》(1996 年 5 月)出现在 IBM Research 的搜索引擎中;这打败了摩卡,摩卡直到第二年 7 月才公布。
像 dcc 这样的学术反编译器可以在公共领域获得。Hex-Ray 的 IDA 等商业反编译器也开始出现。幸运的是,对于微软这样的公司来说,使用 dcc 或 Hex-Rays 反编译 Office 会产生如此多的代码,以至于它对用户来说就像 debug 或十六进制转储一样友好。大多数现代商业软件的源代码是如此之大,以至于没有设计文档和大量的源代码注释就变得难以理解。让我们面对现实吧:许多人的 C++代码在他们写出来六个月后仍然很难读懂。对于其他人来说,在没有帮助的情况下破译来自编译 C++代码的 C 代码有多容易?
更仔细地回顾解释型语言:Visual Basic
让我们以 VB 为例来看看早期版本的解释语言。VB 的早期版本由它的运行时模块vbrun.dll以一种有点类似于 Java 和 JVM 的方式解释。像 Java 类文件一样,VB 程序的源代码被捆绑在二进制文件中。奇怪的是,VB3 比 Java 保留了更多的信息——甚至包括程序员的注释。
最初版本的 VB 生成了一个中间伪代码,叫做 p-code ,是 Pascal 语言,起源于 P-System ( [www.threedee.com/jcm/psystem/](http://www.threedee.com/jcm/psystem/))。在你说任何事情之前,是的,Pascal 和它的所有衍生物都同样容易被反编译——包括微软早期版本的 C 编译器,所以没有人会觉得被遗漏了。p 代码与字节码没有什么不同,本质上是在运行时由vbrun.dll解释的 VB 操作码。如果你曾经想知道为什么你需要在 VB 可执行文件中包含vbrun300.dll,现在你知道了。你必须包含vbrun.dll,这样它才能解释 p 代码并执行你的程序。
来自德国的 H. P. Diettrich 博士是同名著作《DoDi——也许是最著名的 VB 反编译器——的作者。曾经,VB 有一种反编译器和混淆器(或保护工具,因为它们在 VB 中被称为)的文化。但是随着 VB 转向编译代码而不是解释代码,反编译器的数量急剧减少。DoDi 在其网站上免费提供了 VBGuard,其他来源也提供了诸如 Decompiler Defeater、Protect、Overwrite、Shield 和 VBShield 等程序。但是它们也随着 VB5 和 VB6 一起消失了。
那当然是以前了。NET,这又兜了一圈:VB 再一次被演绎。毫不奇怪,许多反编译器和混淆器再次出现在。NET 世界,如 ILSpy 和 Reflector 反编译器,以及风度和 Dotfuscator 混淆器。
汉彼得·范·弗利特和摩卡
奇怪的是,作为一个技术主题,这本书也有非常人性化的元素。1996 年,在荷兰从癌症手术中康复期间,Hanpeter van Vliet 编写了第一个公共领域反编译器 Mocha。他还编写了一个名为 Crema 的混淆器,试图保护 applet 的源代码。如果说摩卡是乌兹机枪,那么克莉玛就是防弹衣。在如今经典的互联网营销策略中,摩卡是免费的,而克莉玛则收取少量费用。
当摩卡的测试版首次出现在汉彼得的网站上时,引起了巨大的争议,尤其是在 CNET 的一篇文章中。由于争议,汉彼得采取了非常光荣的步骤,从他的网站上删除了摩卡。然后他允许他网站的访问者投票决定是否应该再次提供摩卡咖啡。投票结果是十比一支持摩卡,很快它就重新出现在了汉彼得的网站上。
然而,摩卡从未走出测试版。在为一篇关于这一主题的网络技术文章做研究时,我从他的妻子英格丽德那里得知,汉彼得的喉癌最终夺去了他的生命,他于 1996 年新年前夕去世,享年 34 岁。
克莉玛和摩卡的源代码在汉彼得去世前不久卖给了博兰,所有收益归英格丽所有。JBuilder 的一些早期版本附带了一个混淆器,可能是 Crema。它试图通过用控制字符替换 ASCII 变量名来保护 Java 代码不被反编译。
我将在本书的后面更多地讨论其他 Java 反编译器和混淆器的宿主。
反编译时要考虑的法律问题
在您开始构建自己的反编译器之前,让我们借此机会考虑一下为了您自己的享受或利益而反编译他人代码的法律含义。仅仅因为 Java 将反编译技术从一些非常严肃的领域转移到了更主流的计算领域,并不能减少你或你的公司被起诉的可能性。这可能会让它更有趣,但你真的应该小心。
作为一小组基本规则,请尝试以下方法:
- 不要反编译一个 APK,重新编译它,然后把它当成你自己的。
- 不要想把重新编译的 APK 卖给任何第三方。
- 尽量不要反编译带有明确禁止反编译或逆向工程代码的许可协议的 APK 或应用。
- 不要反编译一个 APK 来移除任何保护机制,然后为了你自己的使用而重新编译它。
保护法
在过去的几年里,当涉及到反编译软件时,大企业已经使法律坚定地向它倾斜。公司可以使用许多法律机制来阻止你反编译他们的软件;如果你因为一家公司发现你反编译了它的程序而不得不出现在法庭上,你几乎没有或者根本没有法律辩护。专利法、版权法、包覆许可证中的反逆向工程条款,以及诸如数字千年版权法(DMCA)等许多法律都可能被用来对付您。不同的国家或州可能适用不同的法律:例如,“无反向工程条款”软件许可证在欧盟(EU)是无效条款。但基本概念是相同的:为了将代码克隆到另一个竞争产品中而反编译一个程序,你可能违反了法律。秘诀在于,你不应该站着、跪着或非常用力地压制原作者的合法权利(版权)。这并不是说反编译是不可行的。在某些有限的条件下,法律倾向于通过所谓的合理使用的概念进行反编译或逆向工程。几乎从时间的开端,当然也是从工业时代开始,许多人类最伟大的发明都来自那些站在巨人肩膀上创造出一些特别东西的人。例如,蒸汽火车和电灯泡的发明是相对温和的技术进步。其他人提供了底层的概念,而最终的对象是由像乔治·斯蒂芬森或托马斯·爱迪生这样的人来创造的。(你可以在[www.usgennet.org/usa/topic/steam/Early/Time.html](http://www.usgennet.org/usa/topic/steam/Early/Time.html)看到斯蒂芬森欠许多其他发明家的债务的一个很好的例子,比如詹姆斯·瓦特。这是专利出现的原因之一:允许人们建立在其他发明的基础上,同时仍然给最初的发明者一些补偿,比如说 20 年。
双亲
在软件领域,商业秘密通常受到版权法的保护,并且越来越多地受到专利的保护。专利可以保护程序的某些部分,但是一个完整的程序被一个专利或一系列专利保护的可能性很小。软件公司希望保护他们的投资,所以他们通常求助于版权法或软件许可证来防止人们从本质上窃取他们的研究和开发成果。
版权
但是版权法并不是坚如磐石,因为否则就没有为一个想法申请专利的动机,专利局也会很快倒闭。版权保护并不延伸到计算机程序的接口,如果开发者能够证明他们已经反编译了程序,以查看他们如何能够与程序中任何未发布的*应用编程接口(API)*进行互操作,那么他们可以使用合理使用辩护。
计算机程序法律保护指令
如果你住在欧盟,那么你很可能会受到计算机程序法律保护指令的约束。这个指令声明你可以在一定的限制条件下反编译器:例如,当你试图理解功能需求来创建一个与你自己的程序兼容的接口。换句话说,如果您需要访问第三方程序的内部调用,并且作者拒绝以任何价格泄露 API,您可以进行反编译。但是你只能用这些信息来创建你自己程序的接口,而不是创建一个有竞争力的产品。你也不能对任何受保护的区域进行逆向工程。
多年来,微软的应用据称从对 Windows 3.1 和 Windows 95 的底层未发布 API 调用中获得了不公平的优势,这些调用比已发布的 API 快几个数量级。电子前沿基金会(EFF)提出了一个有用的路线图类比来帮助解释这种情况。假设您正从底特律前往纽约,但您的地图没有显示任何州际路线;当然,你最终会通过走小路到达那里,但是如果你有一张完整的州际地图,路程会短很多。如果这些情况属实,欧盟指令将成为拆卸 Windows 2000 或微软 Office 的理由,但在尝试之前,你最好请一位好律师。
逆向工程
在美国,判例也允许法律反编译。迄今为止最著名的案例是 Sega 诉 Accolade ( [digital-law-online.info/cases/24PQ2D1561.htm](http://digital-law-online.info/cases/24PQ2D1561.htm))。1992 年,Accolade 公司赢得了对世嘉公司的诉讼;裁决称,雅高未经授权拆卸世嘉目标代码并不侵犯版权。Accolade 将世嘉的二进制文件逆向工程为一个中间代码,允许 Accolade 提取一个软件密钥,使 Accolade 的游戏能够与世嘉 Genesis 视频控制台进行交互。显然,世嘉不打算让 Accolade 访问其 API,或者在这种情况下,解锁世嘉游戏平台的代码。法院支持 Accolade,判定反向工程构成了合理使用。但在你认为这给了你反编译代码的全权委托之前,你可能想知道雅达利诉任天堂([digital-law-online.info/cases/24PQ2D1015.htm](http://digital-law-online.info/cases/24PQ2D1015.htm))在非常相似的情况下对雅达利不利。
法律大局
总之——你可以看出这是法律部分——美国的法院案例和欧盟指令都强调,在某些情况下,逆向工程可以用于理解互操作性和创建程序接口。它不能用来制作复制品,并作为竞争产品出售。大多数 Java 反编译不属于互操作性范畴。更有可能的是,反编译器想要盗版代码,或者,充其量,理解软件背后的基本思想和技术。
尚不清楚通过逆向工程来发现 APK 是如何创作的是否构成合理使用。美国 1976 年的版权法排除了“任何想法、程序、过程、系统、操作方法、概念、原则或发现,无论其描述的形式如何”,这听起来像是辩护的开始,也是越来越多的软件专利被颁发的原因之一。反编译盗版或非法出售软件无法辩护。
但从开发商的角度来看,形势看起来很黯淡。唯一的保护——用户许可证——几乎和禁止盗版 MP3 的法律一样有用。它不会从物理上阻止任何人进行非法复制,也不会对家庭用户起到真正的威慑作用。没有任何法律手段可以保护你的代码免受黑客的攻击,有时,试图创建当今安全系统的人似乎感觉自己站在了白痴的肩膀上。你只需要看看对电子书保护计划的调查([slashdot.org/article.pl?sid=01/07/17/130226](http://slashdot.org/article.pl?sid=01/07/17/130226))和 DeCSS 惨败([cyber.law.harvard.edu/openlaw/DVD/resources.html](http://cyber.law.harvard.edu/openlaw/DVD/resources.html))就能明白许多所谓的安全系统实际上有多脆弱。
道德问题
反编译是学习 Android 开发和 DVM 如何工作的一个很好的方法。如果你遇到一种你以前没见过的技术,你可以快速反编译它,看看它是如何完成的。反编译通过看到其他人的编程技术,帮助人们爬上 Android 学习曲线。反编译 apk 的能力可以决定基本的 Android 理解和深入的知识。的确,有大量的开源示例可供参考,但是如果您可以选择自己的示例并修改它们以满足您的需求,那会更有帮助。
但是如果没有讨论窃取他人代码背后的道德问题,任何一本关于反编译的书都是不完整的。由于这种情况,Android 应用带有完整的源代码:如果你愿意,可以说是强制开源。
作者、出版商、作者的代理人以及作者代理人的母亲想要声明,我们并不是提倡本书的读者为了教育目的之外的任何目的反编译器。本书的目的是向您展示如何反编译源代码,但我们不鼓励任何人反编译其他程序员的代码,然后尝试使用、出售或重新打包,就好像这是您自己的代码一样。请不要对任何有许可协议的代码进行逆向工程,该协议声明您不应该反编译代码。这不公平,你只会给自己找麻烦。(此外,你永远无法确定反编译器生成的代码是 100%准确的。如果你打算使用反编译作为你自己产品的基础,你可能会大吃一惊。话虽如此,数以千计的 apk 可用,当反编译时,将帮助你理解好的和坏的 Android 编程技术。
在某种程度上,我是在为“不要射杀信使”辩护。我不是第一个发现 Java 中这个缺陷的人,当然也不会是最后一个写这个主题的人。我写这本书的原因,就像互联网的早期一样,基本上是利他的。换句话说,我发现了一个很酷的小技巧,我想告诉大家。
保护自己
盗版软件是许多软件公司头疼的问题,也是其他公司的大生意。至少,软件盗版者可以使用反编译器来消除许可限制;但是想象一下,如果这项技术可以反编译 Office 2010,重新编译它,并作为一种新的竞争产品出售,会有什么后果。在某种程度上,当 Corel 发布其 Office for Java 的测试版时,这种情况很容易发生。
你能做些什么来保护你的代码吗?是:
- 许可协议:许可协议并不能为想要反编译你代码的程序员提供任何真正的保护。
- *代码中的保护方案:*在代码中散布保护方案(比如检查手机是否有根)是没有用的,因为这些方案可以在反编译代码之外进行注释。
- *代码指纹:*这被定义为用于标记或指纹化源代码以证明所有权的伪造代码。它可以与许可协议一起使用,但只有在法庭上才真正有用。更好的反编译工具可以分析代码并删除任何虚假代码。
- *混淆:*混淆将一个类文件中的方法名和变量名替换成怪异而奇妙的名字。这可能是一个很好的威慑,但源代码通常仍然是可见的,这取决于您选择的混淆器。
- *知识产权(IPR)保护方案:*这些方案,如 Android Market 数字版权管理(DRM),通常会在几小时或几天内被破坏,通常不会提供太多保护。
- *服务器端代码:*对 APKs 最安全的保护是隐藏 web 服务器上所有有趣的代码,只使用 APK 作为瘦前端 GUI。这样做的缺点是,您可能仍然需要在某个地方隐藏一个 API 密钥来访问 web 服务器。
- *原生代码:*Android 原生开发套件(NDK)允许你在 C++文件中隐藏密码信息,这些文件可以反汇编但不能反编译,并且仍然运行在 DVM 之上。如果操作正确,这项技术可以增加一层重要的保护。它还可以与数字签名检查一起使用,以确保没有人在另一个 APK 中窃取您精心隐藏的信息。
- *加密:*加密还可以与 NDK 结合使用,以提供额外的防拆卸保护,或者作为向任何后端 web 服务器传递公钥和私钥信息的一种方式。
这些选项中的前四个仅起威慑作用(一些混淆器比另一些更好),其余四个是有效的,但有其他含义。我将在本书的后面更详细地讨论它们。
总结
反编译是新 Android 程序员最好的学习工具之一。要了解如何编写 Android 应用,还有什么比从手机中取出一个例子并反编译成源代码更好的方法呢?当移动软件公司破产时,反编译也是一个必要的工具,修复代码的唯一方法就是自己反编译。但是,如果你试图保护无数小时的设计和开发投资,反编译也是一种威胁。
这本书的目的是创造关于反编译和源代码保护的对话——区分事实和虚构,并展示反编译 Android 应用有多容易,以及可以采取什么措施来保护代码。有些人可能会说反编译不是问题,开发人员总是可以被训练去阅读竞争对手的汇编程序。但是一旦允许轻松访问 Android 应用文件,任何人都可以下载 dex2jar 或 JD-GUI,反编译就会变得容易得多。不信?然后继续读下去,自己做决定。
一、机器中的幽灵
如果你想知道混淆器或反编译器到底有多好,那么看看 DEX f文件和相应的 Java 类文件中发生了什么会有所帮助。否则,你就要依赖第三方供应商的话,或者充其量是一个知识渊博的评论家的话。对于大多数人来说,当您试图保护任务关键型代码时,这还不够好。至少,您应该能够明智地谈论反编译领域,并提出显而易见的问题来理解正在发生的事情。
“别理窗帘后面的那个人。”
绿野仙踪
目前,来自谷歌的各种声音都在说,反编译 Android 代码没什么可担心的。大家不都是在组装层面做了好几年了吗?Java 刚起步的时候也有类似的杂音。
在本章中,您将打开一个 Java 类文件;在下一章中,您将剖析 DEX 文件格式。这将为后面关于混淆理论的章节打下基础,并在反编译器的设计过程中帮助你。为了达到这个阶段,您需要理解字节码、操作码和类文件,以及它们与 Dalvik 虚拟机(DVM)和 Java 虚拟机(JVM)的关系。
市场上有几本关于 JVM 的非常好的书。最好的是比尔·凡纳斯在 Java 2 虚拟机内部的*(麦格劳-希尔,2000)。这本书的一些章节可以在网上[www.artima.com/insidejvm/ed2/](http://www.artima.com/insidejvm/ed2/)找到。如果你找不到这本书,那就去看看 Venners 同样优秀的关于 JavaWorld.com 的文章。这一系列的文章是他后来扩展成书的原始素材。Sun 的 *Java 虚拟机规范,第二版(Addison-Wesley,1999 年),由 Tim Lindholm 和 Frank Yellin 编写,对于潜在的反编译器作者来说,既全面又非常翔实。但是作为一个规范,它不是你所说的好的读物。这本书也可以在[java.sun.com/docs/books/vmspec](http://java.sun.com/docs/books/vmspec)在线获得。
然而,这里的重点与其他 JVM 书籍有很大不同。我从相反的方向看待事物。我的任务是让您从字节码到源代码,而其他人都想知道源代码是如何被翻译成字节码并最终执行的。您感兴趣的是如何将 DEX 文件转换成类文件,以及如何将类文件转换成源代码,而不是如何解释类文件。
本章着眼于如何将一个类文件分解成字节码,以及如何将这些字节码转换成源代码。当然,你需要知道每个字节码是如何工作的;但是你对它们在 JVM 中会发生什么不太感兴趣,因此这一章的重点也有所不同。
JVM:一个可利用的设计
Java 类文件是为通过网络或互联网快速传输而设计的。因此,它们很紧凑,也相对容易理解。为了便于移植,Java 编译器 javac 只将类文件的一部分编译成字节码。然后由 JVM 解释和执行,通常是在不同的机器或操作系统上。
JVM 的类文件接口由 Java 虚拟机规范严格定义。但是 JVM 最终如何将字节码转换成机器码,这取决于开发人员。这真的与你无关,因为你的兴趣再次停留在 JVM 上。如果您认为类文件类似于其他语言(如 C 或 C++)中的目标文件,等待被 JVM 链接和执行,只是带有更多的符号信息,这可能会有所帮助。
一个类文件携带如此多的信息有很多原因。许多人认为互联网有点像现代的蛮荒西部,在那里,骗子正密谋用病毒感染你的硬盘,或者等着窃取任何可能经过他们的信用卡信息。结果,JVM 被设计成自下而上地保护 web 浏览器免受流氓小程序的攻击。通过一系列的检查,JVM 和类加载器确保没有恶意代码可以上传到网页上。
但是所有的检查都必须以极快的速度执行,以减少下载时间,所以最初的 JVM 设计者选择一个简单的堆栈机器来完成这些重要的安全检查并不奇怪。事实上,JVM 的设计相当安全,尽管一些早期的浏览器实现犯了几个或三个严重的错误。如今,Java 小程序不太可能在任何浏览器中运行,但是 JVM 的设计还是一样的。
对开发人员来说不幸的是,保持代码安全的同时也使代码更容易被反编译。JVM 受限的执行环境和不复杂的体系结构,以及它的许多指令的高级本质,都对程序员不利,而对反编译器有利。
在这一点上,可能也值得一提脆弱的超类问题。在 C++中添加一个新方法意味着所有引用该类的类都需要重新编译。Java 通过将所有必要的符号信息放入类文件来解决这个问题。然后,JVM 负责链接和最终的名称解析,动态加载所有需要的类——包括任何外部引用的字段和方法。这种延迟的链接或动态加载,可能是 Java 更容易被反编译的原因。
顺便说一下,我在这些讨论中忽略了本机方法。本地方法当然是包含在应用中的本地 C 或 C++代码。使用它们会破坏 Java 应用的可移植性,但这是防止 Java 程序被反编译的一种可靠方法。
事不宜迟,让我们简单看一下 JVM 的设计。
简易堆垛机
JVM 本质上是一个简单的堆栈机器,有一个程序寄存器来管理程序流。Java 类加载器获取类并将其呈现给 JVM。
您可以将 JVM 分成四个独立的、不同的部分:
- 许多
- 程序计数器(PC)寄存器
- 方法区域
- JVM 堆栈
每个 Java 应用或 applet 都有自己的堆和方法区,每个线程都有自己的寄存器或程序计数器和 JVM 堆栈。然后,每个 JVM 堆栈被进一步细分为堆栈框架,每个方法都有自己的堆栈框架。一段话包含了很多信息。图 2-1 用一个简单的图表说明。
**图 2-1。**Java 虚拟机
图 2-1 中的阴影部分是所有线程共享的,白色部分是特定于线程的。
堆
让我们先处理堆,让它不碍事,因为它对 Java 反编译过程的影响很小或没有影响。
与 C 或 C++开发人员不同,Java 程序员不能分配和释放内存;它由 JVM 负责。new 操作符在堆上分配对象和内存,当程序不再引用某个对象时,JVM 垃圾收集器会自动释放这些对象和内存。
这有几个很好的理由;安全性要求 Java 中不使用指针,这样黑客就不能突破应用进入操作系统。没有指针意味着其他东西——在本例中是 JVM——必须负责分配和释放内存。内存泄漏也应该成为过去,至少理论上是这样的。一些用 C 和 C++编写的应用因像筛子一样泄漏内存而臭名昭著,因为程序员没有注意在适当的时候释放不需要的内存——并不是说任何阅读本文的人都会犯这样的罪。垃圾收集也应该使程序员更有效率,花在调试内存问题上的时间更少。
如果您确实想了解更多关于堆中发生的事情,请尝试 Oracle 的堆分析工具(hat)。它使用 Java 2 SDK 版和更高版本生成的 JVM 堆的hprof文件转储或快照。它被设计成“调试不必要的对象保留”(内存泄漏给你和我)。你看,垃圾收集算法,比如引用计数和标记清除技术,也不是 100%准确。类文件可能有没有正确终止的线程,ActionListener未能注销,或者对一个对象的静态引用在该对象应该被垃圾收集很久之后仍然存在。
这对反编译过程几乎没有影响。我提到它只是因为它是一个有趣的东西——或者是一个帮助调试 Java 代码的重要工具,这取决于您的思维方式或您老板的立场。
这留下了三个需要关注的区域:程序寄存器、堆栈和方法区域。
程序计数器寄存器
为了简单起见,JVM 使用很少的寄存器:控制程序流的程序计数器,以及堆栈中的另外三个寄存器。尽管如此,每个线程都有自己的程序计数器寄存器,用于保存堆栈上正在执行的当前指令的地址。Sun 选择使用有限数量的寄存器来满足支持很少寄存器的体系结构。
方法区
如果您跳到“类文件内部”一节,您会看到类文件被分解成许多组成部分,以及方法的确切位置。每个方法都有自己的代码属性,其中包含特定方法的字节码。
尽管类文件包含关于程序计数器应该为每条指令指向哪里的信息,但类加载器在代码开始执行之前会注意代码在内存区域中的位置。
当程序执行时,程序计数器通过指向下一条指令来跟踪程序的当前位置。方法区域中的字节码遍历其类似汇编程序的指令,在处理变量时使用堆栈作为临时存储区域,而程序遍历该方法的完整字节码。程序在方法区域内的执行不一定是线性的;跳跃和 gotos 很常见。
JVM 栈
栈只不过是临时变量的存储区。所有的程序执行和变量操作都是通过将变量推入堆栈框架或从堆栈框架中取出来实现的。每个线程都有自己的 JVM 堆栈框架。
JVM 堆栈由三个不同的部分组成,分别是局部变量(var)、执行环境(frame)和操作数堆栈(optop)。vars、frame 和 optop 寄存器指向堆栈的每个不同区域。该方法在自己的环境中执行,操作数堆栈用作字节码指令的工作空间。optop 寄存器指向操作数堆栈的顶部。
正如我所说的,JVM 是一个非常简单的机器,它在操作数堆栈上弹出和推送临时变量,并在变量中保留任何局部变量,同时继续在堆栈框架中执行方法。堆栈夹在堆和寄存器之间。
因为堆栈非常简单,所以不能存储复杂的对象。这些被外包给垃圾场。
在一个类文件内
为了获得一个类文件的整体视图,让我们再看一下来自第一章的Casting.java文件,如清单 2-1 所示。使用 javac 编译它,然后对二进制类文件进行十六进制转储,如图 2-2 中的所示。
清单 2-1。 Casting.java,现在拥有田地!
`public class Casting {
static final String ascStr = "ascii "; static final String chrStr = " character ";
public static void main(String args[]){
for(char c=0; c < 128; c++) {
System.out.println(ascStr + (int)c + chrStr + c);
}
}
}`
图 2-2。Casting.class
正如您所看到的,Casting.class 很小很紧凑,但是它包含了 JVM 执行Casting.java代码所需的所有信息。
为了进一步打开类文件,在本章中,您将通过将类文件分成不同的部分来模拟反汇编程序的操作。当我们分解 Casting.class 时,我们还将构建一个名为 ClassToXML 的原始反汇编器,它将类文件输出为易读的 XML 格式。ClassToXML 使用来自[www.freeinternals.org](http://www.freeinternals.org)的 Java 类文件库(jCFL)来完成繁重的工作,可以从该书的 Apress.com 页面下载。
您可以将类文件分成以下组成部分。
- 幻数
- 次要和主要版本号
- 恒定池计数
- 常数存储库
- 访问标志
this阶级- 超类
- 接口计数
- 接口
- 字段计数
- 菲尔茨
- 方法计数
- 方法
- 属性计数
- 属性
JVM 规范使用类似于 struct- 的格式来显示类文件的不同组件;参见清单 2-2 。
清单 2-2。 类文件结构
Classfile { int magic, short minor_version, short major_version, short constant_pool_count, cp_info constant_pool[constant_pool_count-1], short access_flags, short this_class, short super_class, short interfaces_count, short interfaces [interfaces_count], short fields_count, field_info fields [fields_count], short methods_count, method_info methods [methods_count], short attributes_count attributes_info attributes[attributes_count] }
这似乎一直是显示类文件的一种非常麻烦的方式,所以您可以使用一种 XML 格式,这种格式允许您更快地遍历类文件的内部结构。当您试图解开它的含义时,它也使类文件信息更容易理解。图 2-3 显示了完整的类文件结构,所有 XML 节点都已折叠。
图 2-3。*Casting.class*的 XML 表示
接下来,您将看到每个不同的节点及其形式和功能。在第六章中,您将学习为所有 Java 类文件创建 ClassToXML 本章中的代码仅适用于Casting.class。要运行本章的代码,首先从[www.freeinternals.org](http://www.freeinternals.org)下载 jCFL jar 文件,并把它放在您的类路径中。然后执行以下命令:
javac ClassToXML.java java ClassToXML < Casting.class > Casting.xml
神奇的数字
很容易找到魔术号和版本号,因为它们出现在类文件的开头——你应该能在图 2-2 中找到它们。十六进制的幻数是类文件的前 4 个字节(0xCAFEBABE),它告诉 JVM 它正在接收一个类文件。奇怪的是,这些也是下一代平台上多架构二进制(MAB)文件的前四个字节。在 Java 的早期实现中,Sun 和 NeXT 之间一定发生过一些人员的交叉授粉。
选择 0xCAFEBABE 有很多原因。首先,很难从字母 A 到 F 中找出有意义的八个字母的单词。据詹姆斯·高斯林说,“死亡咖啡馆”是他们办公室附近一家咖啡馆的名字,感恩而死乐队过去常在那里演出。所以 0xCAFEDEAD 和此后不久的 0xCAFEBABE 成为了 Java 文件格式的一部分。我的第一反应是,很遗憾 0xGETALIFE 不是一个合法的十六进制字符串,但我也想不出更好的十六进制名称。还有更糟糕的幻数,比如 0xFEEDFACE、0xDEADBEEF,可能还有最糟糕的 0xDEADBABE,它们分别被摩托罗拉、IBM 和 Sun 使用。
Microsoft 的 CLR 文件有一个类似的头,BSJB,它是以。Net 平台:布莱恩·哈利,苏珊·拉德克-斯普尔,杰森·詹德和比尔·艾文思。好吧,也许 0xCAFEBABE 也没那么糟糕。
次要和主要版本
次要和主要版本号是接下来的四个字节 0x0000 和 0x0033,参见清单 2-2 ,或者次要版本 0 和主要版本 51,这意味着代码是由 JDK 1.7.0 编译的。JVM 使用这些主要编号和次要编号来确保它能够识别并完全理解类文件的格式。JVM 将拒绝执行任何具有更高的主版本号和次版本号的类文件。
次要版本用于需要更新 JVM 的小变更,主要版本用于需要完全不同和不兼容的 JVM 的大规模基本变更。
恒定池计数
所有的类和接口常量都存储在常量池中。令人惊讶的是,常量池计数占用了接下来的 2 个字节,告诉您常量池中有多少个可变长度元素。
0x0035 或整数 53 是示例中的数字。JVM 规范告诉你constant_pool[0]是 JVM 保留的。事实上,它甚至没有出现在类文件中,所以常量池元素存储在constant_pool[1]到constant_pool[52]中。
恒池
下一项是常量池本身,它的类型是cp_info;参见清单 2-3 。
清单 2-3。 cp_info 结构
cp_info { byte tag, byte info[] }
常量池由可变长度元素的数组组成。在后面的类文件中,它充满了对常量池中其他条目的符号引用。常量池计数告诉你常量池中有多少个变量。
类文件所需的每个常量和变量名都可以在常量池中找到。这些通常是字符串、整数、浮点数、方法名等等,所有这些都是固定的。然后,在类文件中的任何地方,每个常量都被其常量池索引引用。
常量池中的每个元素(记住示例中有 53 个)都以一个标签开始,告诉您下一个常量是什么类型。表 2-1 列出了类文件中使用的有效标签及其相应的值。
常量池中的许多标签是对常量池其他成员的符号引用。例如,每个String指向一个Utf8标签,字符串最终存储在那里。Utf8的数据结构如清单 2-4 所示。
清单 2-4。 Utf8结构
Utf8 { byte tag, int length, byte bytes[length] }
我在常量池的 XML 输出中尽可能地折叠了这些数据结构(见清单 2-5 ),这样你就可以很容易地阅读了。
清单 2-5 。Casting.class恒池为
<ConstantPool> <ConstantPoolEntry> <id>1</id> <Type>Methodref</Type> <ConstantPoolAddress>13,27</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>2</id> <Type>Fieldref</Type> <ConstantPoolAddress>28,29</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>3</id> <Type>Class</Type> <ConstantPoolAddress>30</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>4</id> <Type>Methodref</Type> <ConstantPoolAddress>3,27</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>5</id> <Type>String</Type> <ConstantPoolAddress>31</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>6</id> <Type>Methodref</Type> <ConstantPoolAddress>3,32</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>7</id> <Type>Methodref</Type> <ConstantPoolAddress>3,33</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>8</id> <Type>String</Type> <ConstantPoolAddress>34</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>9</id> <Type>Methodref</Type> <ConstantPoolAddress>3,35</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>10</id> <Type>Methodref</Type> <ConstantPoolAddress>3,36</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>11</id> <Type>Methodref</Type> <ConstantPoolAddress>37,38</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>12</id> <Type>Class</Type> <ConstantPoolAddress>39</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>13</id> <Type>Class</Type> <ConstantPoolAddress>40</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>14</id> <Type>Utf8</Type> <ConstantPoolValue>ascStr</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>15</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/String</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>16</id> <Type>Utf8</Type> <ConstantPoolValue>ConstantValue</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>17</id> <Type>Utf8</Type> <ConstantPoolValue>chrStr</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>18</id> <Type>Utf8</Type> <ConstantPoolValue><init></ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>19</id> <Type>Utf8</Type> <ConstantPoolValue>V</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>20</id> <Type>Utf8</Type> <ConstantPoolValue>Code</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>21</id> <Type>Utf8</Type> <ConstantPoolValue>LineNumberTable</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>22</id> <Type>Utf8</Type> <ConstantPoolValue>main</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>23</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/String</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>24</id> <Type>Utf8</Type> <ConstantPoolValue>StackMapTable</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>25</id> <Type>Utf8</Type> <ConstantPoolValue>SourceFile</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>26</id> <Type>Utf8</Type> <ConstantPoolValue>Casting</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>27</id> <Type>NameAndType</Type> <ConstantPoolAddress>18,19</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>28</id> <Type>Class</Type> <ConstantPoolAddress>41</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>29</id> <Type>NameAndType</Type> <ConstantPoolAddress>42,43</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>30</id> <Type>Utf8</Type> <ConstantPoolValue>java/lang/StringBuilder</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>31</id> <Type>Utf8</Type> <ConstantPoolValue>ascii</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>32</id> <Type>NameAndType</Type> <ConstantPoolAddress>44,45</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>33</id> <Type>NameAndType</Type> <ConstantPoolAddress>44,46</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>34</id> <Type>Utf8</Type> <ConstantPoolValue>character</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>35</id> <Type>NameAndType</Type> <ConstantPoolAddress>44,47</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>36</id> <Type>NameAndType</Type> <ConstantPoolAddress>48,49</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>37</id> <Type>Class</Type> <ConstantPoolAddress>50</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>38</id> <Type>NameAndType</Type> <ConstantPoolAddress>51,52</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>39</id> <Type>Utf8</Type> <ConstantPoolValue>Casting</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>40</id> <Type>Utf8</Type> <ConstantPoolValue>java/lang/Object</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>41</id> <Type>Utf8</Type> <ConstantPoolValue>java/lang/System</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>42</id> <Type>Utf8</Type> <ConstantPoolValue>out</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>43</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/io/PrintStream</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>44</id> <Type>Utf8</Type> <ConstantPoolValue>append</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>45</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/StringBuilder</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>46</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/StringBuilder</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>47</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/StringBuilder</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>48</id> <Type>Utf8</Type> <ConstantPoolValue>toString</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>49</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/String</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>50</id> <Type>Utf8</Type> <ConstantPoolValue>java/io/PrintStream</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>51</id> <Type>Utf8</Type> <ConstantPoolValue>println</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>52</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/String</ConstantPoolValue> </ConstantPoolEntry> </ConstantPool>
当您花时间检查类文件的输出时,这是一个简单而优雅的设计。取第一种方法引用,constant_pool[1]:
<ConstantPoolEntry> <id>1</id> <Type>Methodref</Type> <ConstantPoolAddress>13,27</ConstantPoolAddress> </ConstantPoolEntry>
这告诉您在constant_pool[13]中查找类,以及在constant_pool[27]中查找类名和类型
<ConstantPoolEntry> <id>13</id> <Type>Class</Type> <ConstantPoolAddress>40</ConstantPoolAddress> </ConstantPoolEntry>
指向constant_pool[40]:
<ConstantPoolEntry> <id>40</id> <Type>Utf8</Type> <ConstantPoolValue>java/lang/Object</ConstantPoolValue> </ConstantPoolEntry>
但是您也有constant_pool[27]要解析,它给出了方法的名称和类型:
<ConstantPoolEntry> <id>27</id> <Type>NameAndType</Type> <ConstantPoolAddress>18,19</ConstantPoolAddress> </ConstantPoolEntry>
常量池的元素 18 和 19 包含方法名及其描述符。根据 JVM 规范,方法描述符采用以下形式:
(ParameterDescriptor *) ReturnDescriptor
返回描述符可以是 void 的V或者是FieldType中的一个(见表 2-2 ):
<ConstantPoolEntry> <id>18</id> <Type>Utf8</Type> <ConstantPoolValue><init></ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>19</id> <Type>Utf8</Type> <ConstantPoolValue>V</ConstantPoolValue> </ConstantPoolEntry>
在这种情况下,方法的名称是<init>,它是每个类文件中的一个内部 JVM 方法;其方法描述符为()V,或void,用于字段描述符映射(见表 2-2 )。
因此,您现在可以重新创建该方法,如下所示:
void init()
你也可以试着解开一些其他的类。如果从目标类或方法向后工作,可能会有所帮助。有些字符串很难理解,但是稍加练习,方法签名就变得清晰了。
最早的混淆器只是将这些字符串重新命名为完全无法理解的东西。这阻止了原始的反编译器,但没有损害类文件,因为 JVM 使用了指向常量池中的字符串的指针,而不是字符串本身,只要您没有重命名内部方法(如<init>)或破坏对外部库中任何 Java 类的引用。
从下面的条目中,您已经知道您的 import 语句需要什么类:constant_pool[36, 37, 39, 46]。注意在Casting.java例子中没有接口或者静态最终类(参见清单 2-1 )。这些将作为常量池中的字段引用出现,但是到目前为止,这个简单的类解析器已经足够完整,可以处理您想处理的任何类文件。
访问标志
访问标志包含位掩码,它告诉你是在处理一个类还是一个接口,以及它是公共的、最终的等等。所有接口都是抽象的。
共有八种访问标志类型(见表 2-3 ,但将来可能会引入更多类型。ACC_SYNTHETIC、ACC_ANNOTATION和ACC_ENUM是 JDK 1.5 中相对较新的新增内容。
在this类或接口之前,访问标志被or结合在一起以给出修饰符的描述。0x21告诉你Casting.class中的this类是一个公共(和超)类,你可以通过回溯到清单 2-1 中的代码来验证它的正确性:
<AccessFlags>0x21</AccessFlags>
本类和超类
接下来的两个值指向this类和超类的常量池索引。
<ThisClass>12</ThisClass> <SuperClass>13</SuperClass>
如果您遵循清单 2-5 中的 XML 输出,constant_pool[12]指向constant_pool[39];这里的Utf8结构包含字符串Casting,告诉你this是Casting类。超类在constant_pool[13],指向constant_pool[40];这里的Utf8结构包含java/lang/Object,因为每个类都有object作为它的超类。
接口和接口数
清单 2-1 中的Casting.java例子没有任何接口,所以你必须看一个不同的例子来更好地理解接口是如何在类文件中实现的(参见清单 2-6 )。
清单 2-6。 界面示例
`interface IProgrammer { public void code(); public void earnmore(); }
interface IWriter { public void pullhairout(); public void earnless(); }
public class Person implements IProgrammer, IWriter {
public Person() { Geek g = new Geek(this); Author t = new Author(this); }
public void code() { /* ..... / }
public void earnmore() { / ..... / }
public void pullhairout() { / ..... / }
public void earnless() { / ..... */ } }
class Geek { IProgrammer iprog = null;
public Geek(IProgrammer iprog) { this.iprog = iprog; iprog.code(); iprog.earnmore(); } }
class Author { IWriter iwriter = null;
public Author(IWriter iwriter) { this.iwriter = iwriter; iwriter.pullhairout(); iwriter.earnless(); } }`
清单 2-6 有两个接口,IProgrammer和IWriter。对类文件运行 ClassToXML 会在 interfaces 部分提供以下信息:
<InterfaceCount>2</InterfaceCount> <Interfaces> <Interface>8</Interface> <Interface>9</Interface> </Interfaces>
这解析为常量池中的IProgrammer和IWriter字符串,如下所示:
<ConstantPoolEntry> <id>8</id> `Class
27
9
Class
28
字段和字段计数
field_info的结构如清单 2-7 所示。
清单 2-7 。field_info数据结构
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
Casting.class有两个静态和最终字段,ascStr和chrStr(见清单 2-1 )。我还使它们成为静态的和最终的,以强制一个ConstantValue字段属性。
现在,如果您取出 XML 中的相关部分,您会看到有两个字段(清单 2-8 )。让我们关注第一个。
清单 2-8。 Casting.java字段信息
<FieldCount>2</FieldCount> <Fields> <Field> <AccessFlags>ACC_STATIC, ACC_FINAL</AccessFlags> <Name>ascStr</Name> <Descriptor>java.lang.String</Descriptor> <Attributes> <Attribute> <AttributeType>String</AttributeType> <AttributeName>ascii</AttributeName> </Attribute> </Attributes> </Field> <Field> <AccessFlags>ACC_STATIC, ACC_FINAL</AccessFlags> <Name>chrStr</Name> <Descriptor>java.lang.String</Descriptor> <Attributes> <Attribute> <AttributeType>String</AttributeType> <AttributeName>character</AttributeName> </Attribute> </Attributes> </Field> </Fields>
字段访问标志(参见表 2-4 )告诉您该字段是public、private、protected、static、final、volatile还是transient。
对于任何编写过 Java 的人来说,前五个和最后一个关键字应该是显而易见的。volatile关键字告诉一个线程该变量可能被另一个线程更新,transient关键字用于对象序列化。本例中的访问标志0x0018表示静态最终字段。
回到表 2-2 ,在你解开不同的字段描述符之前,让你的头脑清醒一下:
<Field> <AccessFlags>ACC_STATIC, ACC_FINAL</AccessFlags> <Name>ascStr</Name> <Descriptor>java.lang.String</Descriptor> <Attributes> <Attribute> <AttributeType>String</AttributeType> <AttributeName>ascii</AttributeName> </Attribute> </Attributes> </Field>
描述符指回constant_pool[14]字段ascStr,它有字段描述符constant_pool[15]或Ljava/lang/String;这是一个String类的实例。
字段属性
毫无疑问,属性计数就是属性的数量,后面紧跟着属性本身。整个类文件的属性的格式如清单 2-9 所示。
清单 2-9。 attribute-info结构
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
在字段数据结构、方法数据结构和属性数据结构(类文件数据结构的最后一个元素)中可以找到几种不同的属性类型。但是这里真正感兴趣的只有两个字段属性,ConstantValue和Synthetic。ConstantValue用于常量变量,例如在当前示例中声明为 static 和 final 的变量。在 JDK 1.1 中引入了Synthetic变量来支持内部类。
Signature和Deprecated属性也是可能的,用户也可以定义他们自己的属性类型,但是它们与当前的讨论无关。
清单 2-11。 字段属性数据
<Attributes> <Attribute> <AttributeType>String</AttributeType> <AttributeName>ascii</AttributeName> </Attribute> </Attributes>
第一个字段的属性(见清单 2-11 ),是一个可以在constant_pool[5](见清单 2-12 )中找到的常量,一个字符串,它依次指向字符串“ascii”。
**清单 2-12。**恒池中的字段
<ConstantPoolEntry> <id>5</id> <Type>String</Type> <ConstantPoolAddress>31</ConstantPoolAddress> </ConstantPoolEntry> <ConstantPoolEntry> <id>31</id> <Type>Utf8</Type> <ConstantPoolValue>ascii</ConstantPoolValue> </ConstantPoolEntry>
现在,您已经将第一个字段反编译成了它的原始格式:
static final String ascStr = "ascii ";
方法和方法计数
现在是类文件最重要的部分:方法。所有的源代码都被转换成字节码并存储或包含在method_info区域中。(实际上,它在方法的代码属性中,但是您已经非常接近了。)如果有人可以得到字节码,那么他们可以尝试将它转换回源代码。Methods元素前面是方法计数和数据结构(见清单 2-13 ),与前一节中的field_info结构没有什么不同。三种类型的属性通常出现在method_info级别:Code、Exceptions,对于内部类再次出现Synthetic。
清单 2-13。 method_info结构
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
清单 2-1 的Casting.class中的方法如清单 2-14 所示。
清单 2-14。 Casting.class方法信息
<MethodCount>2</MethodCount> <Methods> <Method> <Attributes> <Attribute> <AttributeName>Code:</AttributeName> <Max_Stack>1</Max_Stack> <Max_Locals>1</Max_Locals> <Method_Args>1</Method_Args> <Method_Code> 0: aload_0 1: invokespecial #1 4: return </Method_Code> <Method_LineNumberTable>line 1: 0</Method_LineNumberTable> </Attribute> </Attributes> <AccessFlags>public</AccessFlags> <Name>Casting</Name> <Descriptor>();</Descriptor> </Method> <Method> <Attributes> <Attribute> <AttributeName>Code:</AttributeName> <Max_Stack>3</Max_Stack> <Max_Locals>2</Max_Locals> <Method_Args>1</Method_Args> <Method_Code> 0: iconst_0 1: istore_1 2: iload_1 3: sipush 128 6: if_icmpge 51 9: getstatic #2 12: new #3 15: dup 16: invokespecial #4 19: ldc #5 21: invokevirtual #6 24: iload_1 25: invokevirtual #7 28: ldc #8 30: invokevirtual #6 33: iload_1 34: invokevirtual #9 37: invokevirtual #10 40: invokevirtual #11 43: iload_1 44: iconst_1 45: iadd 46: i2c 47: istore_1 48: goto 2 51: return </Method_Code> <Method_LineNumberTable> line 8: 0 line 9: 9 line 8: 43 line 11: 51 </Method_LineNumberTable> <Method_StackMapTableEntries>2</Method_StackMapTableEntries> <Method_StackMapTable> frame_type = 252 /* append */ offset_delta = 2 locals = [ int ] frame_type = 250 /* chop */ offset_delta = 48</Method_StackMapTable> </Attribute> </Attributes> <AccessFlags>public, static</AccessFlags> <Name>main</Name> <Descriptor>(java.lang.String[]);</Descriptor> </Method> </Methods>
根据原始源代码中使用的修饰符,为每个方法设置不同的访问标志;参见表 2-5 。存在许多限制,因为一些访问标志是互斥的——换句话说,一个方法不能同时声明为ACC_PUBLIC和ACC_PRIVATE,甚至不能声明为ACC_PROTECTED。然而,您通常不会反汇编非法的字节码,所以您不太可能遇到任何这样的可能性。
示例中的第一个方法是 public 第二种是公共静态方法。
现在您可以找到最终方法的名称和方法描述符:
<Name>main</Name> <Descriptor>(java.lang.String[]);</Descriptor>
您从constant_pool[22]和constant_pool[23]中提取方法的名称和描述,如清单 2-15 所示。
清单 2-15。 Casting.class方法名和描述符常量池信息
<ConstantPoolEntry> <id>22</id> <Type>Utf8</Type> <ConstantPoolValue>main</ConstantPoolValue> </ConstantPoolEntry> <ConstantPoolEntry> <id>23</id> <Type>Utf8</Type> <ConstantPoolValue>Ljava/lang/String</ConstantPoolValue> </ConstantPoolEntry>
现在,您可以在没有任何基础代码的情况下重新组装该方法
public static void main(java.lang.String args[]) { /* */ }
或者干脆
import java.lang.String; ... public static void main(String args[]) { /* */ }
其余的方法以类似的方式退出常量池。
方法属性
属性出现在类文件结构的field、method和attributes元素中。每个属性都以引用常量池和属性长度的attribute_name_index开头。但是类文件的肉在方法属性中(见清单 2-16 )。
清单 2-16 。初始化方法属性
<Attributes> <Attribute> <AttributeName>Code:</AttributeName> <Max_Stack>1</Max_Stack> <Max_Locals>1</Max_Locals> <Method_Args>1</Method_Args> <Method_Code> 0: aload_0 1: invokespecial #1 4: return </Method_Code> <Method_LineNumberTable>line 1: 0</Method_LineNumberTable> </Attribute> </Attributes>
本例中的属性类型是代码属性。代码属性如清单 2-17 所示。
清单 2-17。 代码属性
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; { u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
attribute_length是代码属性的长度减去前 6 个字节。attribute_type和attribute_name占用前 6 个字节,不包含在attribute_length中。max_stack和max_locals给出操作数堆栈和堆栈帧局部变量部分的最大变量数。这将告诉你栈有多深,以及有多少变量将被推入栈中和栈外。
代码长度给出了以下代码数组的大小。代码数组只是一系列字节,其中每个字节码是一个保留的字节值或操作码,后跟零个或多个操作数,或者换句话说:
opcode operand
查看在Casting.class上运行 ClassToXML 的输出(清单 2-14 ,您会看到有两个方法,main和init,这是一个空的构造函数,当开发人员选择不添加他们自己的构造函数时,Java 编译器总是会添加它。每个方法都有自己的代码数组。
方法
在我解释什么字节码映射到哪个操作码之前,让我们看一下最简单的方法,这是第一个代码段:
2ab70001b1
当您将它转换成操作码和操作数时,它就变成了
2a aload 0 b70001 invokespecial #1 b1 return
2a变成了aload 0。这将本地变量 0 加载到堆栈中,这是invokespecial所要求的。b70001变成了invokespecial #1,其中invokespecial用于在有限的情况下调用一个方法,比如实例初始化方法(对你我来说是<init>),这就是你在这里看到的。#1是对constant_pool[1]的引用,是一个CONSTANT_Methodref结构。清单 2-18 收集了constant_pool[1]的所有相关常量池条目。
清单 2-18。 <init>法恒池解析
` 1 Methodref 13,27
13 Class 40 40 Utf8 java/lang/Object 27 NameAndType 18,19 18 Utf8 init 19 Utf8 V `您可以手动将符号引用解析为
<Method java.lang.Object.<init>()V>
这是 javac 编译器添加到所有还没有构造函数的类中的空构造函数。最后一个b1操作码是一个简单的return语句。所以第一个方法可以直接转换回下面的代码,一个空的构造函数:
public class Casting() { return; }
主要方法
第二个代码属性不太重要。为了更进一步,您需要知道每个十六进制值映射到哪个操作码。
**注意:**尽管这个例子列表比大多数其他操作码列表都短(你忽略了任何大于 201 的操作码),它仍然运行了几页;你可以在附录 A 和[www.apress.com](http://www.apress.com)中查阅。注意,超过 201 的操作码是为将来使用而保留的,因为它们对类文件中的原始字节码没有影响,可以安全地忽略。
您还需要知道 Java 语言的每个元素是如何被编译成字节码的,这样您就可以颠倒这个过程。然后,您可以看到如何将剩余的代码属性转化为操作码及其操作数。
main方法有以下 52 字节的byte_code属性,它在清单 2-19 中被分解成操作码和操作数
033c1b110080a2002db20002bb000359b700041205b600061bb600071208b60006 1bb60009b6000ab6000b1b0460923ca7ffd2b1
清单 2-19。 主要方法
<Method> <Attributes> <Attribute> <AttributeName>Code:</AttributeName> <Max_Stack>3</Max_Stack> <Max_Locals>2</Max_Locals> <Method_Args>1</Method_Args> <Method_Code> 0: iconst_0 1: istore_1 2: iload_1 3: sipush 128 6: if_icmpge 51 9: getstatic #2 12: new #3 15: dup 16: invokespecial #4 19: ldc #5 21: invokevirtual #6 24: iload_1 25: invokevirtual #7 28: ldc #8 30: invokevirtual #6 33: iload_1 34: invokevirtual #9 37: invokevirtual #10 40: invokevirtual #11 43: iload_1 44: iconst_1 45: iadd 46: i2c 47: istore_1 48: goto 2 51: return </Method_Code> <Method_LineNumberTable> line 8: 0 line 9: 9 line 8: 43 line 11: 51 </Method_LineNumberTable> <Method_StackMapTableEntries>2</Method_StackMapTableEntries> <Method_StackMapTable> frame_type = 252 /* append */ offset_delta = 2 locals = [ int ] frame_type = 250 /* chop */ offset_delta = 48</Method_StackMapTable> </Attribute> </Attributes> <AccessFlags>static</AccessFlags> <Name>main</Name> <Descriptor>(java.lang.String[]);</Descriptor> </Method> </Methods>
你可以用与前一种方法类似的方式对操作码和操作数进行逆向工程,如你在表 2-6 中所见。
iconst_0和istore_1将数字 0 推送到堆栈上,sipush将数字 128 推送到堆栈上,if_icmpge比较两个数字和goto程序计数器或PC = 51(即如果数字相等则返回)。下面是来自Casting.class代码的代码片段:
for(char c=0; c < 128; c++) { }
以同样的方式,您可以完成分析以返回完整的 main 方法。本书的目的是向您展示如何通过编程来实现这一点。
可以从 Apress 站点的下载区获得 ClassToXML,它像一个真正的反汇编器一样输出字节码。现在您已经看到了创建一个反汇编器是多么容易,您可能会明白为什么这么多反汇编器都有用户界面。
属性和属性计数
最后两个元素包含类文件属性的数量和剩余的属性,通常是SourceFile和InnerClasses。
SourceFile是最初用来生成代码的 Java 文件的名称。InnerClasses属性有点复杂,被几个不能处理内部类的反编译器忽略了。
您并不局限于SourceFile和InnerClasses属性。可以在这里或者在任何字段或方法属性部分中定义新属性。开发人员可能希望将信息存储在自定义属性中,可能使用它进行一些低级检查,或者存储加密的代码属性以防止反编译。假设您的新代码属性遵循所有其他属性的格式,您可以添加任何想要的属性,这将被 JVM 忽略。每个属性需要 2 个字节,提供一个指向常量池的数字,给出属性的名称,attribute_name_index;4 个字节给出属性中剩余字节的长度,attribute_length。
总结
您最终到达了类文件的末尾,并在此过程中手动反汇编了 ClassToXML(参考[www.apress.com](http://www.apress.com)上的相应文件)。我希望你开始明白这一切是如何结合在一起的。尽管类文件的设计简洁紧凑,但是由于初始和最终编译阶段的分离,您需要大量的信息来帮助您恢复源代码。多年来,程序员一直受到编译成可执行文件通常提供的编码的保护,但是在中间阶段拆分编译并携带如此多的信息是自找麻烦。
第三章着眼于解开 DEX 文件格式,帮助你理解如何将它逆向工程回 Java 源代码。
三、在 DEX 文件中
我们需要另一个虚拟机用于 Android 手机,而 Java 虚拟机(JVM)不够好,这看起来可能有点奇怪。但为了优化和性能,所有 Android 手机上都使用了 Dalvik 虚拟机(DVM)。它是以一个原始开发者在冰岛家乡的一个地方命名的,在设计上与 JVM 有很大的不同。DVM 使用寄存器,而不是压入-弹出堆栈机器。相应的 DVM 字节码或 DEX 文件也是与 Java 类文件完全不同的设计。
但是并没有失去一切。关于 DEX 文件规范有足够多的信息来重复你在第二章中查看的关于类文件的相同练习,并得出相同的令人愉快的结论,让你获得对 DEX 文件字节码的访问,并将其转换回 Java 源代码,即使你在本章中是手动进行的。DEX 文件可以分解成不同的部分:header和常量池的 DEX 版本,常量池的data部分包含字符串、字段、方法和类信息的指针。
机器中的幽灵,第二部
当你从 Android Market 或亚马逊 Marketplace 下载一个应用到你的 Android 手机上时,你正在下载一个 Android 包(APK)文件。每一个 APK 文件都是 zip 格式的。将.apk文件扩展名改为.zip,解压文件后会得到 APK 中包含的资源、图像、、AndroidManifest.xml文件和classes.dex文件,结构类似于图 3-1 中的所示。
图 3-1。 解压后的 APK 文件
一个 Java jar 文件有许多类文件,而每个 APK 文件只有一个classes.dex文件,如图 3-2 中的所示。根据谷歌的说法,出于性能和安全原因,APK 格式不同于类文件格式。但是不管原因是什么,从逆向工程的角度来看,这意味着您的目标现在是classes.dex文件。您已经完全脱离了 Java 类文件格式,现在需要理解classes.dex文件的内容,这样您就可以将它反编译回 Java 源代码。
图 3-2。 Class 文件 vs DEX 文件
第四章介绍了许多 Android 和第三方工具,可以帮助你分离 apk 和classes.dex文件。在本章中,您将手动创建自己的classes.dex反汇编器。
转换铸件
首先,你需要将你的Casting.class文件从第三章转换成classes.dex文件,这样你就有东西可以用了。这个classes.dex文件将在 Android 手机的命令行上运行,但它不是一个经典的 APK 文件。然而,classes.dex格式是一样的,所以这是开始 DEX 文件研究的好地方。
您可以使用 Android 平台工具附带的 dx 程序进行这种转换。确保Casting.class文件在casting文件夹中,并执行以下命令:
javac c:\apress\chap3\casting\Casting.java dx --dex --output=c:\temp\classes.dex C:\apress\chap3\casting
图 3-3 以十六进制格式显示了清单 3-1 中Casting.java代码的结果classes.dex文件。
清单 3-1。??Casting.java
`public class Casting {
static final String ascStr = "ascii "; static final String chrStr = " character ";
public static void main(String args[]){
for(char c=0; c < 128; c++) {
System.out.println("ascii " + (int)c + " character "+
c);
}
}
}`
图 3-3。classes.dex
为了进一步打开 DEX 文件,在本章中,您将通过将 DEX 文件分解成几个部分来模拟反汇编程序的操作。您可以通过构建自己的名为 DexToXML 的原语反汇编器来实现这一点,该反汇编器获取 DEX 文件并将代码输出为易读的 XML 格式。
将 DEX 文件分解成组成部分
您可以将类文件分成以下组成部分:
headerstring_idstype_idsproto_idsfield_idsmethod_idsclass_defsdatalink_data
header部分包含了文件信息的摘要、文件大小以及指向其他信息的指针或偏移量。String_ids列出了文件中的所有字符串,Java 类型可以在type_ids部分找到。稍后您将看到原型的proto_ids、field_ids、method_ids和class_defs部分如何让您将类名、方法调用和字段反向工程回 Java。data部分是安卓版的常量池。link_data部分是针对静态链接文件的,与本讨论无关,所以本章不提供相关部分。
DEX 文件格式规范([source.android.com/tech/dalvik/dex-format.html](http://source.android.com/tech/dalvik/dex-format.html))使用类似结构的格式来显示 DEX 文件的组成部分;参见清单 3-2 。
清单 3-2。 DEX 文件结构
Dexfile { header header_item, string_ids string_id_item[], type_ids type_id_item[], proto_ids proto_id_item[], field_ids field_id_item[], method_ids method_id_item[], class_defs class_def_item[], data ubyte[], link_data ubyte[] }
和上一章一样,您使用 XML 格式,因为它允许您更快地在 DEX 文件的内部结构中来回遍历。它还使 DEX 文件信息更容易理解,因为你解开了它的含义。DEX 文件结构——所有 XML 节点都已折叠——如清单 3-3 所示。
清单 3-3。 DexToXML
`
`以下部分解释了每个节点中的内容。
标题部分
header部分包含文件剩余部分的顶层信息。DEX 文件的结构与其原始格式的 Java 类文件有很大不同,类似于微软。Net PE 文件比你在上一章看到的更多。header报头包含幻数、校验和、签名和类文件的大小。剩下的信息告诉您字符串、类型、原型、方法和类有多大,并提供一个地址指针或偏移量,您可以在 classes.dex 文件中找到实际的字符串、类型、原型、方法和类。在data部分还有一个指向地图信息的指针,它重复了header部分的许多信息。
清单 3-4 使用了一种类似结构的格式来展示header是如何布局的。
清单 3-4。 Header截面结构
DexfileHeader{ ubyte[8] magic, int checksum, ubyte[20] signature, uint file_size, uint header_size, uint endian_tag, uint link_size, uint link_off, uint map_off, uint string_ids_size, uint string_ids_off, uint type_ids_size, uint type_ids_off, uint proto_ids_size, uint proto_ids_off, uint field_ids_size, uint field_ids_off, uint method_ids_size, uint method_ids_off, uint class_defs_size, uint class_defs_off, uint data_size, uint data_off }
header字段详见表 3-1 。
DEX 文件中的header部分在图 3-4 中突出显示,你可以使用表 3-1 来跟踪每个字段出现的位置。但是读取十六进制需要某种自虐,这就是为什么 DexToXML 以更容易阅读的 XML 格式输出相同的数据。清单 3-5 中的显示了 DexToXML 标题字段。
图 3-4。*classes.dex*中的表头字段
清单 3-5。??header段的 DexToXML 输出
`
dex\n035\0 628B4418 DAA921CA9C4FB4C521D777BC2A184A380DA2AAFE 0x00000450 112 0x12345678 0 0x00000000 0x000003A4 26 0x00000070 10 0x000000D8 7 0x00000100 3 0x00000154 9 0x0000016C 1 0x000001B4 0x0000027C 0x000001D4 `其中几个字段需要进一步解释:magic、checksum、header_size和Endian_tag。header部分的其余字段是尺寸和到其他部分的偏移量。
魔法
DEX 文件的幻数是前 8 个字节,并且总是十六进制的64 65 78 0A 30 33 35 00或字符串dex\n035\0。规范提到换行符和\0是为了防止某些类型的腐败。035预计会像类文件中的主版本和次版本一样随时间而变化。
校验和
校验和是文件的 Adler32 校验和,不包括幻数。在图 3-4 中的classes.dex文件中,第二块中第一行的十六进制为62 8B 44 18。但是数据是以小端存储的,所以真正的校验和是相反的,是值0x18448B62。
Header_size
Header所有classes.dex文件的大小相同:0x70。
Endian _ 标签
所有classes.dex文件中的endian_tag是0x12345678,它告诉你数据是以小端存储的(反向)。未来的 DEX 文件不一定总是如此。但是现在,你可以假设它是小尾序的。
字符串 _ 标识部分
从header部分可以知道,这个classes.dex文件中有<string_ids_size>26</string_ids_size>个字符串,可以在下面的地址找到:<string_ids_offset>0x00000070</string_ids_offset>。顺便说一下,这是在header部分的末尾。但是你已经从标题大小知道了:<header_size>0x00000070</header_size>。
在classes.dex文件中的这 26 个条目中的每一个都是一个 8 字节的地址偏移量或string_data_off,它指向data部分中的实际字符串。在图 3-5 中,可以看到第一个string_ids条目是72 02 00 00。记住存储是 little-endian 的,这告诉你第一个字符串可以在地址0x00000272找到,在文件的data部分的更下面。最后一个字符串条目是73 03 00 00,它告诉您最后一个字符串位于0x00000373的偏移量或地址处。
图 3-5**。** string_ids段classes.dex
清单 3-6 显示了 XML 格式的strings_ids部分,你继续构建文件的 XML 表示。
**清单 3-6。**DexToXMLstring_ids节
`
0 0x00000272 1 0x0000027F ` ` 2 0x00000287 3 0x0000028A 4 0x00000298 5 0x0000029B 6 0x0000029E 7 0x000002A2 7 0x000002A2 8 0x000002AD 9 0x000002B1 10 0x000002B5 11 0x000002CC 12 0x000002E0 ` `13 0x000002F4 13 0x000002F4 14 0x0000030F 15 0x00000323 16 0x00000326 17 0x0000032A 18 0x0000033F 19 0x00000347 20 0x0000034F 21 0x00000357 22 0x0000035F 23 0x00000365 24` `0x0000036A 25 0x00000373 ...... `与类文件不同,字符串不会混杂在常量池中。string_ids部分完全由指向存储在data部分中的字符串的指针组成。这些字符串可以在从0x00000272开始的data部分找到,也可以在列表 3-7 中找到。
**清单 3-7。**中的弦data节中的
string[0]: character string[1]: <init> string[2]: C string[3]: Casting.java string[4]: I string[5]: L string[6]: LC string[7]: LCasting; string[8]: LI string[9]: LL string[10]: Ljava/io/PrintStream; string[11]: Ljava/lang/Object; string[12]: Ljava/lang/String; string[13]: Ljava/lang/StringBuilder; string[14]: Ljava/lang/System; string[15]: V string[16]: VL string[17]: [Ljava/lang/String; string[18]: append string[19]: ascStr string[20]: ascii string[21]: chrStr string[22]: main string[23]: out string[24]: println string[25]: toString
type _ ids 部分
header部分告诉您有 10 个type_ids从偏移0x000000D8开始(清单 3-8 )。第一个type_id,如图 3-6 所示,是02 00 00 00。那指向string_id[2],那指向data段的C(见strings_ids)。其余的type_id以类似的方式脱落。
图 3-6。 type_ids段classes.dex
**清单 3-8。**DexToXMLtype_ids节
`
0` `2 1 4 3 10 4 11 5 12 6 13 7 14 8 15 9 17 ... `正如您在上一节中看到的,字符串是在data节中的string_ids节中给定的偏移量或地址处找到的。我已经抽出了type_id的字符串,这样你可以更容易地遵循逆向工程过程;参见清单 3-9 。
**清单 3-9。**中的类型data部分
type[0]: C type[1]: I type[2]: LCasting; type[3]: Ljava/io/PrintStream; type[4]: Ljava/lang/Object; type[5]: Ljava/lang/String; type[6]: Ljava/lang/StringBuilder; type[7]: Ljava/lang/System; type[8]: V type[9]: Ljava/lang/String;
proto _ ids 部分
Proto_id s 包含了Casting.java中的原型方法。DVM 使用Proto_id和相关的type_id组装method_id。[图 3-7 再次显示了它们在classes.dex文件中的位置。
每个proto_id有三个部分,如清单 3-10 中的结构所示。这些是指向method参数的简短描述或 ShortyDescriptor(参见表 2-2 )的string_id的指针,一个指向返回类型的type_id的指针,以及一个进入data部分的地址偏移量以找到参数列表。
图 3-7**。** proto_ids段classes.dex
清单 3-10。 proto_id结构
ProtoID{ uint shorty_idx, uint return_type_idx, uint parameters_off }
在这个例子中,根据header文件有七个proto_id。这些在清单 3-11 的 DexToXML 中显示;原型本身显示在清单 3-12 中。
**清单 3-11。**DexToXMLproto_ids节
`
0 5 5 0x0 1 6 6 0x254 2 8 6 0x25c 3 9 6 0x264 4 15 8` `0x0 5 10 8 0x264 6 10 8 0x26c .... `清单 3-12 显示了来自data部分的原型。
**清单 3-12。**中的原型data章节
proto[0]: Ljava/lang/String; proto( ) proto[1]: Ljava/lang/StringBuilder; proto( C ) proto[2]: Ljava/lang/StringBuilder; proto( I ) proto[3]: Ljava/lang/StringBuilder; proto( Ljava/lang/String; ) proto[4]: V proto( ) proto[5]: V proto( Ljava/lang/String; ) Proto[6]: V proto( Ljava/lang/String; )
字段标识部分
接下来是field_id s。每个field_id有三个部分:类名、字段类型和字段名称。清单 3-13 以结构格式展示了这一点。
清单 3-13。 field_id结构
FieldID{ ushort class_idx, ushort type_idx, uint name_idx }
图 3-8 显示了classes.dex文件中field_ids段的位置。
图 3-8**。** field_ids段classes.dex
在这个例子中,根据header文件有三个fields_id。这些显示在清单 3-14 的 DexToXML 中,字段本身显示在清单 3-15 中。
**清单 3-14。**DexToXMLfield_ids节
`
0 2` `5/type_id> 19 1 2 5 21 2/id> 7 3 23 ... `在这一部分中,您可以根据前面的string_ids和type_ids部分中的信息组合字段。对于field [0],可以看到类的名称是type_id[2]或Casting,字段的类型是type_id[5]或string,字段的名称是string_id[19]或ascStr:
type_id[2] = LCasting; type_id[5] = Ljava/lang/String; string_id[19] = ascStr
清单 3-15 显示了这一点以及类似解析的剩余字段。
清单 3-15。 字段信息
field_ids[0]: Casting.ascStr:Ljava/lang/String; field_ids[1]: Casting.chrStr:Ljava/lang/String; field_ids[2]: java.lang.System.out:Ljava/io/PrintStream;
method _ ids 部分
每个method_id都有三个部分:类名、来自proto_ids部分的方法原型和方法名。清单 3-16 以结构格式展示了这一点。
清单 3-16。 method_id结构
MethodIDStruct{ ushort class_idx, ushort proto_idx, uint name_idx }
图 3-9 显示了classes.dex文件中method_ids段的位置。
图 3-9。 method_ids段classes.dex
在这个例子中,根据header文件有九个method_id。这些显示在清单 3-17 的 DexToXML 中,方法本身显示在清单 3-18 中。
**清单 3-17。**DexToXMLmethod_ids节
`
0 2 4 1 1 2 6 22 2 3 5 24 3 4 4 1 4 6 4 1 5 6 1 18 6 6 2 18 ` ` 7 6 3 18 8 6 0 25 `您可以根据前面章节中的信息手工组装这些方法,而不必转到data章节。对于method [0],类的名字是type_id[2]或者 L Casting,方法的原型是proto_id[4]或者V proto (),方法的名字是string_id[1[ <init>:
type_id[2] = LCasting; proto_id[4] = V proto( ) string_id[1] = <init>
清单 3-18 显示了这一点以及类似的解决方法。
清单 3-18。 方法
method[0]: Casting.<init> (<init>()V) method[1]: Casting.main (main([Ljava/lang/String;)V) method[2]: java.io.PrintStream.println (println(Ljava/lang/String;)V) method[3]: java.lang.Object.<init> (<init>()V) method[4]: java.lang.StringBuilder.<init> (<init>()V) method[5]: java.lang.StringBuilder.append (append(C)Ljava/lang/StringBuilder;) method[6]: java.lang.StringBuilder.append (append(I)Ljava/lang/StringBuilder;) method[7]: java.lang.StringBuilder.append (append(Ljava/lang/String;)Ljava/lang/StringBuilder;) method[8]: java.lang.StringBuilder.toString (toString()Ljava/lang/String;)
class _ defs 部分
每个class_def有八个部分:类的id、类的access_flags、超类的type_id、接口列表的address、源文件名的 string_id、任何注释(与逆向工程源代码无关)的另一个address、类数据的address(在这里可以找到更多的类信息)以及最后的address,在这里可以找到任何静态字段的初始值。清单 3-19 以结构格式显示了这一点。
清单 3-19。 class_defs结构
ClassDefsStruct { uint class_idx, uint access_flags, uint superclass_idx, uint interfaces_off, uint source_file_idx, uint annotations_off, uint class_data_off, uint static_values_off, }
图 3-10 显示了classes.dex文件中class_defs段的位置。
图 3-10。 class_defs段classes.dex
在这个例子中,只有一个类,如清单 3-20 中的 DexToXML 所示。
清单 3-20。 DexToXML class_defs段
`
0 2 public 4 0x0 3 0x0 0x00000392 0x0000038D ... `在classes.dex中,类别Casting的access_flags值为0x00000001。表 3-2 列出了访问标志的转换;在这种情况下,访问标志是public。
通过手工组装 Java 代码,你可以看到这个类被定义为来自type_id[2]的公共类Casting,超类是来自type_id[4]的java/Lang/Object,源文件是来自string_id[3]的Casting.java:
access_flags = public type_id[2] = LCasting; type_id[4] = Ljava/lang/Object; string_id[3] = Casting.java
数据部分
你现在在data段,是classes.dex的真肉。早先的信息导致了这一点。当您解析文件的剩余部分时,您有一个选择:您可以顺序地解析它,或者开始跟随数据偏移量中的地址来查找您想要反编译的字节码。
最明显的做法是遵循数据偏移,所以让我们试试这种方法。首先是来自class_defs的class_data_item,假设您正在寻找字节码。class_data_item部分包含关于字段和方法的信息;接下来是code_item部分,它包含字节码。
类别 _ 数据 _ 项目
从反汇编class_defs中,你知道这个文件中唯一的类Casting.java的地址是0x392。信息是在一个class_data_item结构中,类似于清单 3-21 。
清单 3-21。 class_data_item结构
ClassDataItemStruct { uleb128 static_fields_size, uleb128 instance_fields_size, uleb128 direct_method_size, uleb128 virtual_method_size, encoded_field[static_fields_size] static_fields, encoded_field[instance_fields_size] instance_fields, encoded_method[direct_fields_size] direct_methods, encoded_method[virtual_fields_size] virtual_methods }
Uleb128 是一种用于存储大整数的无符号小端基 128 编码格式。要将整数转换为 uleb128,您需要将其转换为二进制,将其填充为 7 位的倍数,将其分成 7 个位组,在除最后一个位组之外的所有位上添加高 1 位以形成字节(即 8 位),转换为十六进制,然后将结果翻转为 little-endian。如果你看一个例子,这就更有意义了。下面这个例子来自维基百科([en.wikipedia.org/wiki/LEB128](http://en.wikipedia.org/wiki/LEB128)):
10011000011101100101 In raw binary 010011000011101100101 Padded to a multiple of 7 bits 0100110 0001110 1100101 Split into 7-bit groups 00100110 10001110 11100101 Add high 1 bits on all but last group to form bytes 0x26 0x8E 0xE5 In hexadecimal 0xE5 0x8E 0x26 Output stream
多亏了例子中的小整数,转换就容易多了:uleb128 中的0x2是2。
encoded_field和encoded_method还有另外两种结构,采用清单 3-22 和清单 3-23 所示的格式。Field_idx_diff的不同之处在于,尽管第一个条目是直接的field_id[]引用,但是任何后续的field_id[]条目都被列为与之前列出的field_id的差异。Method_idx_diff遵循同样的模式。
清单 3-22。 encoded_field结构
EncodedFieldStruct{ uleb128 field_idx_diff, (explain that it's diff and directly for the first) uleb128 access_flags }
清单 3-23。 encoded_method结构
EncodedMethodStruct{ uleb128 method_idx_diff, (explain that it's diff and directly for the first) uleb128 access_flags, uleb128 code_off }
图 3-11 显示了你在classes.dex文件中的位置:在data部分的正中间。
图 3-11**。** class_data_item段classes.dex
清单 3-24 显示了class_defs部分的 DexToXML 输出。
**清单 3-24。**DexToXMLclass_defs
`
2 0 2 0 0 0 static final 1 1 static final 0 0 public constructor 0x1d4` ` 1 1 public static 0x1ec `手动组装信息,您可以看到如下所示的静态字段和方法信息:
static_field[0] field[0]: Casting.ascStr:Ljava/lang/String; access_flags = static & final static_field[1] field[1]: Casting.chrStr:Ljava/lang/String; access_flags = static & final direct_method[0] method[0]: Casting.<init> (<init>()V) access_flags = public & constructor code_offset = 0x00001d4 direct_method[1] method[1]: Casting.main (main(Ljava/lang/String;)V) access_flags = public & static code_offset = 0x00001ec
现在,您已经拥有了类中方法和字段的所有信息;如果这不明显,您将从外向内重新创建Casting.java代码。您还应该特别注意code_offset,因为它是字节码所在的位置,将用于重新创建源代码。那是你接下来要去的地方。
代码石
class_data_item告诉你code_item从0x1d4开始第一个<init>方法,0x1ec开始主方法。信息在一个code_item结构中,类似于[清单 3-25 。
清单 3-25。 code_item结构
CodeItemStruct { ushort registers_size, ushort ins_size, ushort outs_size, ushort tries_size, uint debug_info_off, uint insns_size, ushort[insns_size] insns, ushort padding, try_item[tries_size] tries, encoded_catch_handler_list handlers }
这花了一些时间,但是要特别注意CodeItemStruct中的insns元素:那是classes.dex存储字节码指令的地方。
图 3-12 显示了classes.dex文件中code_item段(init和main)的位置。这个突出显示的区域是两个code_item的,它们是一个接一个存储的。
图 3-12**。** code_item段classes.dex
清单 3-26 显示了每个code_item部分的 DexToXML 输出。
**清单 3-26。**DexToXMLcode_item
`
` ` 0 1 1 1 0 0x37d 4 invoke-direct {v0},java/lang/Object/ ; ()V return-void 1 5 1 2 0 0x382 44 const/4 v0,0 const/16 v1,128 if-ge v0,v1,l252 sget-object v1,java/lang/System.out Ljava/io/PrintStream; new-instance v2,java/lang/StringBuilder invoke- direct {v2},java/lang/StringBuilder/ ; ()V const-string v3,"ascii " invoke- virtual {v2,v3},java/lang/StringBuilder/append ; append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v2` `invoke- virtual {v2,v0},java/lang/StringBuilder/append ; append(I)Ljava/lang/StringBuilder; move-result-object v2 const-string v3," character " invoke- virtual {v2,v3},java/lang/StringBuilder/append ; append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v2 invoke- virtual {v2,v0},java/lang/StringBuilder/append ; append(C)Ljava/lang/StringBuilder; move-result-object v2 invoke- virtual {v2},java/lang/StringBuilder/toString ; toString()Ljava/lang/String; move-result-object v2 invoke- virtual {v1,v2},java/io/PrintStream/println ; println(Ljava/lang/String;)V add-int/lit8 v0,v0,1 int-to-char v0,v0 goto l1fe return-void `注: 附录中的表 A-2“DVM 字节码到操作码的映射”列出了 DVM 操作码,您可以使用这些操作码将十六进制代码转换成等效的操作码或字节码,并完成反汇编。进行转换时,请参考这一点。
你知道清单 3-26<init>中的第一个方法基于<insns_size />有四条指令。从十六进制文件中可以看到,这四个十六进制代码是7010 0300 0000 0e00。您可以手动将其转换为以下内容:
7010 invoke-direct 10 string[16]: VL 0003 method[3]: java.lang.Object.<init> (<init>()V) 0000 no argument 0e00 return-void
这是 javac 编译器添加到所有还没有构造函数的类中的空构造函数。因此,您的第一个方法可以直接转换回以下代码,这是一个空的构造函数:
public class Casting() { }
总结
这就完成了对classes.dex文件的分解。完整的 DexToXML 解析器代码在 Apress 网站上([www.apress.com](http://www.apress.com))。它包括其他部分,如map_data和debug_info,它们也在classes.dex文件中,但与反编译过程无关。下一章将讨论 Android 反汇编、反编译和混淆世界中所有可用的工具和技术。在第五章和第六章中,你将回到 DexToXML 和 DexToSource,你的 Android 反编译器。