Java 反汇编分析(一)

2020-07-21  本文已影响0人  WqyJh

以这段Java代码为例,反汇编分析一下对应的Java字节码。将该文件保存为BooleanTest.java

package ex3;

public class BooleanTest {
    public static void create() {
        boolean a = true;
        boolean b = false;
        boolean[] arr = new boolean[100];
        arr[5] = true;
        System.out.println(a);
        System.out.println(b);
        System.out.println(arr);
    }
    public static void print(int a) {
        int b = a;
        System.out.printf("%d %d\n", a, b);
    }
}

使用的Java版本为OpenJDK 1.8.0_171。

Picked up _JAVA_OPTIONS:   -Dawt.useSystemAAFontSettings=gasp
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-2-b11)
OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode)

执行javac BooleanTest.java; javap -v BooleanTest.class,会输出完整的反汇编代码,大致可以分为几部分:元数据+常量池、一系列方法,我们分开来看。

常量池

public class ex3.BooleanTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #20.#21        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #22.#23        // java/io/PrintStream.println:(Z)V
   #4 = Methodref          #22.#24        // java/io/PrintStream.println:(Ljava/lang/Object;)V
   #5 = String             #25            // %d %d\n
   #6 = Class              #26            // java/lang/Object
   #7 = Methodref          #27.#28        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #8 = Methodref          #22.#29        // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
   #9 = Class              #30            // ex3/BooleanTest
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               create
  #15 = Utf8               print
  #16 = Utf8               (I)V
  #17 = Utf8               SourceFile
  #18 = Utf8               BooleanTest.java
  #19 = NameAndType        #10:#11        // "<init>":()V
  #20 = Class              #31            // java/lang/System
  #21 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #22 = Class              #34            // java/io/PrintStream
  #23 = NameAndType        #35:#36        // println:(Z)V
  #24 = NameAndType        #35:#37        // println:(Ljava/lang/Object;)V
  #25 = Utf8               %d %d\n
  #26 = Utf8               java/lang/Object
  #27 = Class              #38            // java/lang/Integer
  #28 = NameAndType        #39:#40        // valueOf:(I)Ljava/lang/Integer;
  #29 = NameAndType        #41:#42        // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
  #30 = Utf8               ex3/BooleanTest
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (Z)V
  #37 = Utf8               (Ljava/lang/Object;)V
  #38 = Utf8               java/lang/Integer
  #39 = Utf8               valueOf
  #40 = Utf8               (I)Ljava/lang/Integer;
  #41 = Utf8               printf
  #42 = Utf8               (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;

根据《Java虚拟机规范》,每一个class文件对应下面这样一个ClassFile结构。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

常量池中的每一项都具有如下通用格式,单字节的tag表示cp_info的实际类型,后面info数组的内容由类型决定。tag可以表示Class, Fieldref, Methodref, InterfaceMethodref, String, Integer, Float, Long, Double, NameAndType, Utf8, MethodHandle, MethodType, InvokeDynamic

cp_info {
    u1 tag;
    u1 info[];
}

Class_info的结构如下,表示一个类或接口,name_index是常量池中一个Utf8_info项的下标,表示类或接口名。

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

Fieldref_info, Methodref_info的结构如下,分别表示字段引用和方法引用,他们包含两个字段,class_index表示字段、方法所在的类在常量池中的索引,name_and_type_index表示当前字段或方法的名字和描述符。

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

String_info的结构如下,用于表示一个String类型的常量对象,string_index是常量池中一个Utf8_info项的下标。

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

Utf8_info的结构如下,用于表示一个Utf8字符串值常量。

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

NameAndType_info结构如下,用于表示一个方法或字段,不包含类或接口的信息(无法得知它属于的类或接口)。name_index是常量池中一个Utf8_info的下标,表示方法名称;descriptor_index也是一个Utf8_info的下标,表示一个字段描述符(变量类型)或方法描述符(方法参数和返回值类型)。

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

字段描述符:例如Ljava/lang/Object;表示一个Object实例,[[[D表示double[][][]

方法描述符:例如Object m(int i, double d, Thread t)(IDLjava/lang/Thread;)Ljava/lang/Object;

现在来看反汇编代码中常量池的内容,结构为#<index> = cp_info

   #1 = Methodref          #6.#19         // java/lang/Object."<init>":()V
   #6 = Class              #26            // java/lang/Object
   #10 = Utf8               <init>
   #11 = Utf8               ()V
   #19 = NameAndType        #10:#11        // "<init>":()V
   #26 = Utf8               java/lang/Object

从第1行开始,经过一系列的递归查表,才能确定这个方法所属的类、方法名、参数、返回值。javap将这个值写在了行末的注释里,方便阅读,但实际的字节码里是没有这些的。Methodref这些也是助记符,class文件里只有一条条的字节码。

 #42 = Utf8               (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;

再来看一个例子,这是一个Utf8类型的记录,可以看出它是一个方法描述符,有两个参数,分别为String类型和Object[]类型,返回值为PrintStream类型,这就是System.out.printf的方法描述符。

虚拟机指令

研究方法的字节码之前需要了解一些基本的字节码指令。

在汇编中,一条指令分为操作码和操作数,常见的CPU的操作数都是放在寄存器中的,例如mov eax ecx,将一个寄存器中的值赋予另一个寄存器。

对于虚拟机来说,如果把操作数放在寄存器中,就称为基于寄存器的虚拟机,如果把操作数放在栈中,就称为基于栈的虚拟机。一般来说,基于寄存器的虚拟机更为复杂,因为它往往与CPU相关,性能会更好;基于栈的虚拟机更为简单,但性能会更差。

JVM是一个基于栈的虚拟机,它的大多指令,都涉及到对栈的操作。例如load系列指令,会将变量值压入栈顶;而store系列指令,会将栈顶元素出栈,存入变量;还有的指令如newarray,它会将栈上的操作数出栈,然后将指令执行的结果(返回值)入栈。

本文中涉及到的指令:

create方法

以下代码是通过javap -c BooleanTest.class反汇编得来的,-c不会输出方法中的局部变量表等信息,更为简洁清晰。

  public static void create();
    Code:
       0: iconst_1
       1: istore_0
       2: iconst_0
       3: istore_1
       4: bipush        100
       6: newarray       boolean
       8: astore_2
       9: aload_2
      10: iconst_5
      11: iconst_1
      12: bastore
      13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: iload_0
      17: invokevirtual #3                  // Method java/io/PrintStream.println:(Z)V
      20: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: iload_1
      24: invokevirtual #3                  // Method java/io/PrintStream.println:(Z)V
      27: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      30: aload_2
      31: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      34: return

第一行boolean a = true对应2条指令,iconst_1将整型常量1入栈,istore_0将栈顶的一个整型常量出栈并存入局部变量0,也就是变量a

第二行boolean b = false也对应2条指令,iconst_0将0入栈,istore_1将0出栈存入局部变量1,也就是b

第三行boolean[] arr = new boolean[100]对应3条指令,bipush 100将100入栈,newarray boolean,创建一个长度为100的boolean类型的数组,数组引用放到栈顶,astore_2将栈顶存入局部变量2,即arr

第四行arr[5] = true对应4条指令,aload_2arr引用入栈,iconst_5将5入栈,iconst_1将1入栈,bastore将数组下标为5的位置赋值为1。

第五行System.out.println(a)对应3条指令,getstatic获得System.out对象的引用并入栈,iload_0将局部变量1入栈,invokevirtual索引println方法,以栈顶为参数进行调用。

print方法

以下代码是通过javap -v BooleanTest.class反汇编得来的,因为我们需要观察局部变量表。

  public static void print(int);
    descriptor: (I)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=6, locals=2, args_size=1
         0: iload_0
         1: istore_1
         2: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: ldc           #5                  // String %d %d\n
         7: iconst_2
         8: anewarray     #6                  // class java/lang/Object
        11: dup
        12: iconst_0
        13: iload_0
        14: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        17: aastore
        18: dup
        19: iconst_1
        20: iload_1
        21: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        24: aastore
        25: invokevirtual #8                  // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
        28: pop
        29: return
      LineNumberTable:
        line 13: 0
        line 14: 2
        line 15: 29
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0     a   I
            2      28     1     b   I

通过LocalVariableTable可以看出,函数参数a也是当做局部变量来处理的,下标为0,局部变量b下标为1。

第一行int b = a包含两条指令,iload_0a的值入栈,istore_1将栈顶出栈到b

第二行System.out.printf("%d %d\n", a, b)包含多条指令

总结

本文实践探索了一个class文件大致的结构:

参考

上一篇 下一篇

猜你喜欢

热点阅读