Java 反汇编分析(一)

以这段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];
}
  • magic:唯一作用是确定这个文件是否为一个class文件,值固定为0xCAFEBABE
  • minor_versionmajor_version用来表示class文件的版本号,不同版本的JDK编译出的版本号不同,例如主版本号为52表示是Java 8版本的class、55表示是Java 11版本的class,参考
  • constant_pool_count常量池大小。
  • constant_pool常量池。
  • access_flags访问标志,ACCESS_PUBLIC表示这是个public类,ACCESS_SUPER没什么意义,所有jdk 1.0.2之后编译出的class都带有这个标志。
  • this_class本类在常量池中的一个索引。
  • ......

常量池中的每一项都具有如下通用格式,单字节的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行是一个Methodref类型的,它的class_index为6,name_and_type_index为19,后面的注释表明这个是Object类的构造方法。
  • 第6行,是Class类型,它的name_index为26。
  • 第26行,是Utf8类型,它的值为java/lang/Object
  • 第19行,是NameAndType类型,它的name_index为10,descriptor_index为11。
  • 第10行,是Utf8类型,它的值为<init>,因此它表示的方法名称为<init>,这是一个特殊方法名称,是构造方法。
  • 第11行,是Utf8类型,它的值为()V,表明方法没有参数,返回值为void。

从第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,它会将栈上的操作数出栈,然后将指令执行的结果(返回值)入栈。

本文中涉及到的指令:

  • iconst_<i>int常量i入栈,i的范围为-1,0,1,2,3,4,5。
  • bipush <b>byte常量b入栈,在栈中被扩展成有符号整型。iconst_<i>指令与对应的bipush <i>等价。
  • astore <index>将栈顶存入下标为index的局部变量,引用类型必须为referencereturnAddress
  • astore_<n>astore <n>等价,n的范围为0,1,2,3。
  • newarray <type>创建type类型的数组,数组的长度count由栈顶的整型指定,创建完成后数组的引用会被push到栈顶。type只能为基本数据类型。
  • bastorebyteboolean类型的数组赋值,栈结构为arrayref, index, value,相当于arrayref[index]=value
  • getstatic <byte1> <byte2>获取类的静态字段值并压入栈中。(byte1<<8)|byte2为该字段在运行时常量池中的下标。
  • iload <index>将index表示的int局部变量加载到栈顶。
  • iload_<n>等价于iload <n>n的范围为0,1,2,3。
  • invokevirtual <byte1> <byte2>调用实例方法。操作数栈中结构是这样的..., objectref, arg1, arg2 ...,使用(byte1<<8)|byte2来索引方法,运行时常量池中的符号引用包含了方法描述、方法参数个数等信息。将方法所需的nargs出栈,并被设置成方法的局部变量,然后切换到方法的栈帧中,修改PC(程序计数器),开始执行方法中的指令。
  • ldc <index>将运行时常量池中下标为index的entry入栈,该entry必须为一个运行时常量,类型为intlong、字符串字面量引用、符号引用、方法句柄。
  • anewarray <byte1> <byte2>创建数组,通过(byte1<<8)|byte2在常量池中索引类、数组、接口,由此确定数组类型,栈顶参数指定数组长度,创建好的数组引用入栈。
  • aastore将值存入数组,栈结构为arrayref, index, value,相当于arrayref[index]=value,执行完后3个操作数全部出栈。
  • pop弹出一个栈顶操作数。
  • return返回void

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)包含多条指令

  • getstatic入栈System.out的引用,ldc将常量池中%d %d\n的引用入栈,iconst_2int常量2入栈,anewarray创建一个Object[2]数组并入栈;
  • dup复制栈顶的数组引用并入栈,iconst_0int常量0入栈,iload_0将变量a入栈,invokestatic调用Integer.valueOf将栈顶装箱成Integer对象,aastore将装箱后的a设置到数组下标0的位置;
  • dup复制栈顶的数组引用并入栈,iconst_1int常量1入栈,iload_1将变量b入栈,invokestatic调用Integer.valueOf将栈顶装箱,aastore将装箱后的b设置到数组下标1的位置;
  • invokevirtual调用printf方法;
  • pop指令将printf的返回值出栈

总结

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

  • 常量池就是一个数组,数组的每一项都具有指令的类型,类型后面是它的值。常量池中的项目通过下标引用。
  • class中的常量池称为静态常量池,在类加载的过程中会被合并到运行时常量池中。
  • boolean类型是当做int来处理的,1表示true0表示false
  • boolean[]类型不是byte[]来处理的,虽然在使用newarray指令创建的时候指定的是boolean类型,但是在赋值的时候使用的是字节数组操作指令bastore
  • 在字节码层面观察到了Integer的装箱。
  • 局部变量表按照局部变量的声明顺序依次编号的。
  • 方法参数也当做局部变量来处理,并且率先编号。
  • 方法参数的值是由invokevirtual指令设置好的,在方法中可以直接使用。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。