大端字节序码流中取出2字节_字节码忍者的秘密

大端字节序码流中取出2字节

Java语言由Java语言规范(JLS)定义。 但是,Java虚拟机的可执行字节码由单独的标准Java虚拟规范(通常称为VMSpec)定义。

JVM字节码由javac从Java源代码文件生成,并且该字节码与该语言有很大不同。 例如,一些熟悉的高级Java语言功能已被编译掉,根本没有出现在字节码中。

最明显的例子之一就是Java的循环关键字(for,while等),它们被编译掉并替换为字节码分支指令。 这意味着方法内部的字节码流控制仅由if语句和跳转(用于循环)组成。

在本文中,我们将假定读者具有字节码基础。 如果需要一些背景知识,请参阅The Well-Grounded Java Developer(Evans和Verburg,Manning 2012)或RebelLabs的此报告 (PDF需注册)。

让我们看一个示例,该示例经常使对JVM字节码不熟悉的开发人员感到困惑。 它使用JDK或JRE附带的javap工具,它实际上是Java字节码反汇编程序。 在我们的示例中,我们将讨论一个实现Callable接口的简单类:

public class ExampleCallable implements Callable<Double> {
    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<java.lang.Double> {
  public kathik.java.bytecode_examples.ExampleCallable();
  public java.lang.Double call();
  public java.lang.Object call() throws java.lang.Exception;
}

这种反汇编看起来是错误的-毕竟,我们编写了一个调用方法,而不是两个; 即使我们试图这样写,javac也会抱怨有两个具有相同名称和签名的方法,只是返回类型不同,因此该代码将无法编译。 但是,该类是从上面显示的真实有效的Java源文件中生成的。

这清楚地表明Java熟悉的模糊返回类型限制是Java语言约束,而不是JVM字节码要求。 如果javac插入了您没有写到类文件中的代码的想法令人不安,那不应该; 我们每天都看到它! Java程序员学习的第一个经验教训是“如果不提供构造函数,编译器会为您添加一个简单的构造函数”。 在javap的输出中,即使我们没有编写它,您甚至可以看到已经提供的构造函数。

这些额外的方法提供了一个语言规范要求比VM规范更为严格的示例。 如果我们直接编写字节码,那么有许多“不可能”的事情可以做-没有Java编译器会发出的合法字节码。

例如,我们可以创建完全没有构造函数的类。 Java语言规范要求每个类至少具有一个构造函数,如果我们未能提供一个简单的void构造函数,则javac会自动插入该构造函数。 但是,如果我们直接编写字节码,则可以随意省略一个。 即使通过反射,也无法实例化此类。

我们的最后一个例子是几乎可行的例子,但是效果并不理想。 在字节码中,我们可以编写一个方法来尝试调用属于另一个类的私有方法。 这是有效的字节码,但是如果有任何程序尝试加载它,它将无法正确链接。 这是因为对调用的访问控制限制将由类加载器的验证程序检测到,并且非法访问将被拒绝。

ASM简介

如果我们想创建可以实现某些非Java行为的代码,那么我们将需要从头开始生成一个类文件。 由于类文件格式是二进制的,因此使用使我们能够处理抽象数据结构,然后将其转换为字节码并将其流式传输到磁盘的库是有意义的。

有几种此类库可供选择,但是在本文中,我们将重点介绍ASM。 这是一个非常常见的库,在Java 8发行版中以内部API的形式出现(经过稍微修改)。 对于用户代码,我们希望使用通用的开源库而不是JDK的版本,因为我们不应该依赖内部API。

ASM的核心重点是提供一种API,该API虽然有些不可思议(有时是很笨拙),但以相当直接的方式对应于字节码数据结构。

Java运行时是经过多年设计决策的结果,并且在类文件格式的后续版本中可以清楚地看到所产生的积聚。

ASM试图相当紧密地对类文件进行建模-因此,基本API分为许多相当简单的方法部分(尽管这些部分模拟了二进制问题)。

希望从头开始创建类文件的程序员需要了解类文件的整体结构,并且随着时间的推移,这种情况确实会发生变化。 幸运的是,ASM处理了Java版本之间在类文件格式上的细微差异,并且Java平台的强兼容性要求也有帮助。

按顺序,一个类文件包含:

  • 幻数(在传统的Unix上-Java的幻数是过时的性别歧视0xCAFEBABE)
  • 使用的类文件格式的版本号
  • 不变
  • 访问控制标志(例如,公共类,受保护类,程序包访问类)
  • 此类的名称
  • 该类的超类
  • 此类实现的接口
  • 此类拥有的字段(超出超类的字段)
  • 此类拥有的方法(超越父类的方法)
  • 属性(类级注释)

可以使用以下助记符来调用JVM类文件的主要部分:

ASM提供了两个API,最容易使用的API在很大程度上取决于Visitor模式。 通常,ASM是从空白开始的,使用ClassWriter(当习惯于使用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", "I", null, 1).visitEnd();

   // Create a public getter method
   // public int getMyInt();
   MethodVisitor getterVisitor = 
      cv.visitMethod(ACC_PUBLIC, "getMyInt", "()I", null, null);
   // Get ready to start writing out the bytecode for the method
   getterVisitor.visitCode();
   // Write ALOAD_0 bytecode (push the this reference onto stack)
   getterVisitor.visitVarInsn(ALOAD, 0);
   // Write the GETFIELD instruction, which uses the instance on
   // the stack (& consumes it) and puts the current value of the
   // field onto the top of the stack
   getterVisitor.visitFieldInsn(GETFIELD, GEN_CLASS_STR, "myInt", "I");
   // Write IRETURN instruction - this returns an int to caller.
   // To be valid bytecode, stack must have only one thing on it
   // (which must be an int) when the method returns
   getterVisitor.visitInsn(IRETURN);
   // Indicate the maximum stack depth and local variables this
   // method requires
   getterVisitor.visitMaxs(1, 1);
   // Mark that we've reached the end of writing out the method
   getterVisitor.visitEnd();

   // Create a setter
   // public void setMyInt(int i);
   MethodVisitor setterVisitor = 
       cv.visitMethod(ACC_PUBLIC, "setMyInt", "(I)V", null, null);
   setterVisitor.visitCode();
   // Load this onto the stack
   setterVisitor.visitVarInsn(ALOAD, 0);
   // Load the method parameter (which is an int) onto the stack
   setterVisitor.visitVarInsn(ILOAD, 1);
   // Write the PUTFIELD instruction, which takes the top two 
   // entries on the execution stack (the object instance and
   // the int that was passed as a parameter) and set the field 
   // myInt to be the value of the int on top of the stack. 
   // Consumes the top two entries from the stack
   setterVisitor.visitFieldInsn(PUTFIELD, GEN_CLASS_STR, "myInt", "I");
   setterVisitor.visitInsn(RETURN);
   setterVisitor.visitMaxs(2, 2);
   setterVisitor.visitEnd();
 }

 private void generateCtor(ClassVisitor cv) {
   // Constructor bodies are methods with special name <init>
   MethodVisitor mv = 
       cv.visitMethod(ACC_PUBLIC, INST_CTOR, VOID_SIG, null, null);
   mv.visitCode();
   mv.visitVarInsn(ALOAD, 0);
   // Invoke the superclass constructor (we are basically 
   // mimicing the behaviour of the default constructor 
   // inserted by javac)
   // Invoking the superclass constructor consumes the entry on the top
   // of the stack.
   mv.visitMethodInsn(INVOKESPECIAL, J_L_O, INST_CTOR, VOID_SIG);
   // The void return instruction
   mv.visitInsn(RETURN);
   mv.visitMaxs(2, 2);
   mv.visitEnd();
 }

 @Override
 public String getGenClassName() {
   return GEN_CLASS_NAME;
 }
}

它使用一个具有单个方法的简单接口来生成类的字节,使用一个辅助方法来返回所生成类的名称以及一些有用的常量:

interface ClassGenerator {
public byte[] generateClass();

public String getGenClassName();

// Helpful constants
public static final String PKG_STR = "kathik/java/bytecode_examples/";
public static final String INST_CTOR = "<init>";
public static final String CL_INST_CTOR = "<clinit>";
public static final String J_L_O = "java/lang/Object";
public static final String VOID_SIG = "()V";
}

为了驱动我们将生成的类,我们使用一种称为Main的线束。 这提供了一个简单的类加载器,并提供了一种反映生成的类的方法的反射方式。 为了简单起见,我们还将生成的类写到Maven目标目录中的正确位置,以在IDE的类路径中进行选择:

public class Main {
public static void main(String[] args) {
   Main m = new Main();
   ClassGenerator cg = new Simple();
   byte[] b = cg.generateClass();
   try {
     Files.write(Paths.get("target/classes/" + PKG_STR +
       cg.getGenClassName() + ".class"), b, StandardOpenOption.CREATE);
   } catch (IOException ex) {
     Logger.getLogger(Simple.class.getName()).log(Level.SEVERE, null, ex);
   }
   m.callReflexive(cg.getGenClassName(), "getMyInt");
}

下面的类只是提供一种访问受保护的defineClass()方法的方法,因此我们可以将byte []转换为类对象以进行反射使用

private static class SimpleClassLoader extends ClassLoader {
 public Class<?> simpleDefineClass(byte[] clazzBytes) {
   return defineClass(null, clazzBytes, 0, clazzBytes.length);
 }
}

private void callReflexive(String typeName, String methodName) {
 byte[] buffy = null;
 try {
   buffy = Files.readAllBytes(Paths.get("target/classes/" + PKG_STR +
     typeName + ".class"));
   if (buffy != null) {
     SimpleClassLoader myCl = new SimpleClassLoader();
     Class<?> newClz = myCl.simpleDefineClass(buffy);
     Object o = newClz.newInstance();
     Method m = newClz.getMethod(methodName, new Class[0]);
     if (o != null && m != null) {
       Object res = m.invoke(o, new Object[0]);
       System.out.println("Result: " + res);
     }
   }
 } catch (IOException | InstantiationException | IllegalAccessException | 
         NoSuchMethodException | SecurityException | 
         IllegalArgumentException | InvocationTargetException ex) {
   Logger.getLogger(Simple.class.getName()).log(Level.SEVERE, null, ex);
 }
}

通过这种设置,我们只需进行少量修改即可轻松测试不同的类生成器,以探索字节码生成的不同方面。

非构造函数类非常相似。 例如,以下是如何生成具有单个静态字段的类的类,该类具有为其获取和设置的对象(此生成器未调用generateCtor()):

private void generateStaticGetterSetter(ClassVisitor cv) {
// Generate the static field
  cv.visitField(ACC_PRIVATE | ACC_STATIC, "myStaticInt", "I", null,
     1).visitEnd();

  MethodVisitor getterVisitor = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, 
                                         "getMyInt", "()I", null, null);
  getterVisitor.visitCode();
  getterVisitor.visitFieldInsn(GETSTATIC, GEN_CLASS_STR, "myStaticInt", "I");

  getterVisitor.visitInsn(IRETURN);
  getterVisitor.visitMaxs(1, 1);
  getterVisitor.visitEnd();

  MethodVisitor setterVisitor = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, "setMyInt", 
                                         "(I)V", null, null);
  setterVisitor.visitCode();
  setterVisitor.visitVarInsn(ILOAD, 0);
  setterVisitor.visitFieldInsn(PUTSTATIC, GEN_CLASS_STR, "myStaticInt", "I");

}

setterVisitor.visitInsn(RETURN);setterVisitor.visitMaxs(2,2);setterVisitor.visitEnd();

请注意如何使用ACC_STATIC标志集生成方法,以及方法参数如何在本地变量列表中首先出现(如ILOAD 0模式所隐含-在实例方法中,这将是ILOAD 1,作为“ this”引用)将存储在局部变量表中的0偏移处)。

使用javap,我们可以确认该类确实没有构造函数:

$ javap -c kathik/java/bytecode_examples/StaticOnly.class 
public class kathik.StaticOnly {
public static int getMyInt(); Code:
0: getstatic    #11                // Field myStaticInt:I
3: ireturn

public static void setMyInt(int); Code:
0: iload_0
1: putstatic    #11                // Field myStaticInt:I
4: return
}

使用生成的类

到目前为止,我们已经通过ASM生成的类进行了自反的工作。 这有助于保持
这些示例是独立的,但是在许多情况下,我们希望将生成的代码与常规Java文件一起使用。 这很容易做到。 这些示例有助于将生成的类放入Maven目标
目录,因此很简单:

$ cd target/classes
$ jar cvf gen-asm.jar kathik/java/bytecode_examples/GetterSetter.class kathik/java/bytecode_examples/StaticOnly.class
$ mv gen-asm.jar ../../lib/gen-asm.jar

现在,我们有了一个JAR文件,可以将其用作其他代码中的依赖项。 例如,我们可以使用GetterSetter类:

import kathik.java.bytecode_examples.GetterSetter;
public class UseGenCodeExamples {
 public static void main(String[] args) {
   UseGenCodeExamples ugcx = new UseGenCodeExamples();
   ugcx.run();
 }

 private void run() {
   GetterSetter gs = new GetterSetter();
   gs.setMyInt(42);
   System.out.println(gs.getMyInt());
 }
}

这不会在IDE中编译(因为GetterSetter类不在类路径中)。 但是,如果我们进入命令行并提供对类路径的适当依赖关系,则一切工作正常:

$ cd ../../src/main/java/
$ javac -cp ../../../lib/gen-asm.jar kathik/java/bytecode_examples/withgen/UseGenCodeExamples.java
$ java -cp .:../../../lib/gen-asm.jar kathik.java.bytecode_examples.withgen.UseGenCodeExamples
42

结论

在本文中,我们研究了使用ASM库中的简单API从头开始生成类文件的基础。 我们已经展示了Java语言和字节码要求之间的一些差异,并且Java的某些规则实际上只是该语言的约定,而不是由运行时强制执行的。 我们还展示了可以从该语言直接使用编写正确的类文件,就像它是由javac生成的一样。 这是Java与非Java语言(例如Groovy或Scala)的互操作性的基础。

有许多更高级的技术可用,但是本文应该为开始深入研究JVM运行时及其运行方式提供一个好地方。

翻译自: https://www.infoq.com/articles/Secrets-of-the-Bytecode-Ninjas/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

大端字节序码流中取出2字节

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值