Java字节码忍者禁术

Java语言本身是由Java语言规格说明(JLS)所定义的,而Java虚拟机的可执行字节码则是由一个完全独立的标准,即Java虚拟机规格说明(通常也被称为VMSpec)所定义的。\

JVM字节码是通过javac对Java源代码文件进行编译后生成的,生成的字节码与原本的Java语言存在着很大的不同。比方说,在Java语言中为人熟知的一些高级特性,在编译过程中会被移除,在字节码中完全不见踪影。\

这方面最明显的一个例子莫过于Java中的各种循环关键字了(for、while等等),这些关键字在编译过程中会被消除,并替换为字节码中的分支指令。这就意味着在字节码中,每个方法内部的流程控制只包含if语句与jump指令(用于循环)。\

在阅读本文前,我假设读者对于字节码已经有了基本的了解。如果你需要了解一些基本的背景知识,请参考《Java程序员修炼之道》(Well-Grounded Java Developer)一书(作者为Evans与Verburg,由Manning于 2012年出版),或是来自于RebelLabs的这篇报告(下载PDF需要注册)。\

让我们来看一下这个示例,它对于还不熟悉的JVM字节码的新手来说很可能会感到困惑。该示例使用了javap工具,它本质上是一个Java字节码的反汇编工具,在下载的JDK或JRE中可以找到它。在这个示例中,我们将讨论一个简单的类,它实现了Callable接口:\

\public class ExampleCallable implements Callable {\    public Double call() {\        return 3.1415;\    }\}
\

我们可以通过对javap工具进行最简单形式的使用,对这个类进行反汇编后得到以下结果:\

\$ javap kathik/java/bytecode_examples/ExampleCallable.class\Compiled from \"ExampleCallable.java\"\public class kathik.java.bytecode_examples.ExampleCallable \       implements java.util.concurrent.Callable {\  public kathik.java.bytecode_examples.ExampleCallable();\  public java.lang.Double call();\  public java.lang.Object call() throws java.lang.Exception;\}
\

这个反汇编后的结果看上去似乎是错误的,毕竟我们只写一个call方法,而不是两个。而且即使我们尝试手工创建这两个方法,javac也会提示,代码中有两个具有相同名称和参数的方法,它们仅有返回类型的不同,因此这段代码是无法编译的。然而,这个类确确实实是由上面那个真实的、有效的Java源文件所生成的。\

这个示例能够清晰地表明在使用Java中广为人知的一种限制:不可对返回类型进行重载,其实这只是Java语言的一种限制,而不是JVM字符码本身的强制要求。javac确实会在代码中插入一些不存在于原始的类文件中的内容,如果你为此感到担忧,那大可放心,因为这种事每时每刻都在发生!每一位Java程序员最先学到的一个知识点就是:“如果你不提供一个构造函数,那么编译器会为你自动添加一个简单的构造函数”。在javap的输出中,你也能看到其中有一个构造函数存在,而它并不存在于我们的代码中。\

这些额外的方法从某种程度上表明,语言规格说明的需求比VM规格说明中的细节更为严格。如果我们能够直接编写字节码,就可以实现许多“不可能”实现的功能,而这种字节码虽然是合法的,却没有任何一个Java编译器能够生成它们。\

举例来说,我们可以创建出完全不含构造函数的类。Java语言规格说明中要求每个类至少要包含一个构造函数,而如果我们在代码中没有加入构造函数,javac会自动加入一个简单的void构造函数。但是,如果我们能够直接编写字节码,我们完全可以忽略构造函数。这种类是无法实例化的,即使通过反射也不行。\

我们的最后一个例子已经接近成功了,但还是差一口气。在字节码中,我们可以编写一个方法,它将试图调用一个其它类中定义的私有方法。这段字节码是有效的,但如果任何程序打算加载它,它将无法正确地进行链接。这是因为在类型加载器中(classloader)的校验器会检测出这个方法调用的访问控制限制,并且拒绝这个非法访问。\

介绍ASM

\

如果我们打算在创建的代码中实现这些超越Java语言的行为,那就需要完全手动创建这样的一个类文件。由于这个类文件的格式是两进制的,因此可以选择使用某种类库,它能够让我们对某个抽象的数据结构进行操作,随后将其转换为字节码,并通过流方式将其写入磁盘。\

具备这种功能的类库有多个选择,但在本文中我们将关注于ASM。这是一个非常常见的类库,在Java 8分发包中有一个以内部API的形式提供的版本(其内容稍有不同)。对于用户代码来说,我们选择使用通用的开源类库,而不是JDK中提供的版本,毕竟我们不应当依赖于内部API来实现所需的功能。\

ASM的核心功能在于,它提供了一种API,虽然它看上去有些神秘莫测(有时也会显得有些粗糙),但能够以一种直接的方式反映出字节码的数据结构。\

我们看到的Java运行时是由多年之前的各种设计决策所产生的结果,而在后续各个版本的类文件格式中,我们能够清晰地看到各种新增的内容。\

ASM致力于尽量使构建的类文件接近于真实形态,因此它的基础API会分解为一系列相对简单的方法片段(而这些片段正是用于建模的二进制所关注的)。\

如果程序员打算完全手动编写类文件,就必需理解类文件的整体结构,而这种结构是会随时改变的。幸运的是,ASM能够处理多个不同Java版本中的类文件格式之间的细微差别,而Java平台本身对于可兼容性的高要求也侧面帮助了我们。\

一个类文件依次包含以下内容:\

  • 某个特殊的数字(在传统的Unix平台上,Java中的特殊数字是这个历史悠久的、人见人爱的0xCAFEBABE)\
  • 正在使用中的类文件格式版本号\
  • 常量\
  • 访问控制标记(例如类的访问范围是public、protected还是package等等)\
  • 该类的类型名称\
  • 该类的超类\
  • 该类所实现的接口\
  • 该类拥有的字段(处于超类中的字段上方)\
  • 该类拥有的方法(处于超类中的方法上方)\
  • 属性(类级别的注解)

可以用下面这个方法帮助你记忆JVM类文件中的主要部分:\

f3bad89eed0532dc848e4a69b82d1ee7.png

\

ASM中提供了两个API,其中最简单的那个依赖于访问者模式。在常见的形式中,ASM只包含最简单的字段以及ClassWrite类(当已经熟悉了ASM的使用和直接操作字节码的方式之后,许多开发者会发现CheckClassAdapter是一个很实用的起点,作为一个ClassVisitor,它对代码进行检查的方式,与Java的类加载子系统中的校验器的工作方式非常想像。)\

让我们看几个简单的类生成的例子,它们都是按照常规的模式创建的:\

  • 启动一个ClassVisitor(在我们的示例中就是一个ClassWriter)\
  • 写入头信息\
  • 生成必要的方法和构造函数\
  • 将ClassVisitor转换为字节数组,并写入输出

示例

\public class Simple implements ClassGenerator {\ // Helpful constants\ private static final String GEN_CLASS_NAME = \"GetterSetter\";\ private static final String GEN_CLASS_STR = PKG_STR + GEN_CLASS_NAME;\\ @Override\ public byte[] generateClass() {\   ClassWriter cw = new ClassWriter(0);\   CheckClassAdapter cv = new CheckClassAdapter(cw);\   // Visit the class header\   cv.visit(V1_7, ACC_PUBLIC, GEN_CLASS_STR, null, J_L_O, new String[0]);\   generateGetterSetter(cv);\   generateCtor(cv);\   cv.visitEnd();\   return cw.toByteArray();\ }\\ private void generateGetterSetter(ClassVisitor cv) {\   // Create the private field myInt of type int. Effectively:\   // private int myInt;\   cv.visitField(ACC_PRIVATE, \"myInt\
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值