Java是一门"半编译半解释"型语言.通过使用jdk提供的javac编译器可以将Java源码编译为Java虚拟机(Java Virtual Machine, JVM)可读的字节码(bytecode),即*.class文件.

学习字节码可以使你更好的理解Java虚拟机的行为,甚至对学习其它基于Java虚拟机的语言(如:Scala,Clojure,Kotlin等)有很大的帮助

入门

实际上*.class文件并不是人类可读的文件格式,我们可以使用JDK提供的反会汇编器javap来分析字节码

我们先来分析一个最经典的例子:

Hello.java

 

class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

 

这可能是大家接触的第一个Java程序,我们先使用=> javac Hello.java编译得到Hello.class文件.然后使用javap -c -verbose将文件反汇编

java uml 反向 java逆向_java uml 反向

 

常量池

其中Constant pool就是传说中的常量池.
常量池可以看作是一个数组,#后面的数字代表数组的索引.
=后面是数组的值.
第一列代表这个常量的tag,第二列会根据tag的不同而不同.

以Class为例,tag为Class的常量结构体为

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

name_index 表示一个常量池中的有效索引,这个索引的tag必须是Utf8

在#5位置上的name_index为常量池中#21位置中的常量,实际上保存的就是这个类的类名.类名会使用正斜杠/代替.来表示类型的完整名称

关于更多常量池的介绍可以查看

The Java Virtual Machine Specification 4.4

通过查看反汇编过的字节码我们可以发现这个类有两个方法,一个是无参的构造器

Hello(),另一个是main方法

 

 

descriptor

descriptor中描述这个参数和方法类型(返回值类型).其中()中代表的是这个方法的参数,后面跟的是这个方法的返回值类型,V代表void,即无返回值

下表列出了一些返回值符号对应的含义

java uml 反向 java逆向_java uml 反向_02

 

需要注意的是引用类型名和常量池中类型的命名方式一致

比如main方法的参数描述符和类型描述符为([Ljava/lang/String;)V,代表这个方法接受一个String[]类型参数,并且无返回值.

 

flags

flags中描述了这个方法的访问权限和基本属性,下表列出flags描述符对应的含义

java uml 反向 java逆向_Java_03

 

在上面我们定义的程序中并没有显示的定义构造器,这里的构造器属于编译器自动生成.但是jvm标准规定类型的初始化等与人工实现无关的方法可以不用加ACC_SYNTHETIC.注

 

code

code是方法的代码部分

stack=2, locals=1, args_size=1,分别代表操作数栈的深度,局部变量表大小和方法参数个数.其中实例方法的第一个局部变量和参数是this.局部变量表中每个参数大小都是32位,所以long和double会占用局部变量表中两个连续的位置

在构造器中,aload_0表示将第一个参数压入栈中,即this.然后会使用invokespecial指令调用一个特殊的初始化方法java/lang/Object."<init>":()V

在main函数中,先使用getstatic指令获取java/lang/System.out:Ljava/io/PrintStream的静态域,再将它压入栈中(java/io/PrintStream的实例).然后再使用ldc将常量池中的字符串指针(即"Hello World")压入栈中,ldc指令表示将一个常量池中的对象压入操作数栈中.
接着,使用invokevirtual调用java/io/PrintStream.println,invokevirtual指令用于调用实例的方法.最终调用return指令返回