# 反编译遇到的相关指令问题

JAVA 2019-04-15

# 概述

JVM用线程来执行方法,每个线程都包含一个线程栈,线程栈存放的是栈帧,每个栈帧都有自己的操作数栈和本地变量表。当一个方法被执行时候,首先会在栈顶push一个栈帧,栈帧的创建就需要指定操作数栈和本地变量表的大小。接下来方法的参数会被传入一个方法的本地变量表,本地变量表的访问采用下表索引的方式来访问,第0个位置会传入this(无参时候会调用自身实例,有参数则第1个位置会传入参数类型:如int,第2个位置会传入参数类型:如int,第N个位置...)

在栈帧完全准备好后,就可以开始执行执行字节码指令了,上述iload_1,i_load_2分别将本地变量表的1,2位置的数据push到操作数栈中,iadd随后会pop两个值用来做加法操作,并将结果push到操作数栈,最后ireturn将栈顶数据返回。

针对一个简单的i++自增方法,做如下尝试,用javap反编译后,看看jvm是具体按照什么指令来操作对象和工作流程的。

# 实例应用

  • 源码
/**
 * @create.date: 2019/3/31
 * @comment: <p></p>
 * @author: Jackie.Yang
 * @see:com.xianyu.facade.demo
 */
public class TestJava {

    public volatile int n;

    public void add() {
        n++;
    }
}
  • 反编译 javap 常用的参数: -p :会打印私有的字段和方法, -v :它会尽可能地打印出所有信息 -c :只查询相关方法对应的字节码

  • javap -v

D:\Workspaces\fascade\target\classes\com\xianyu\facade\demo>javap -v TestJava
警告: 二进制文件TestJava包含com.xianyu.facade.demo.TestJava
Classfile /D:/Workspaces/fascade/target/classes/com/xianyu/facade/demo/TestJava.class
  Last modified 2019-3-31; size 398 bytes
  MD5 checksum 4c1a4e863b058de2dd2a5c9ec019420a
  Compiled from "TestJava.java"
public class com.xianyu.facade.demo.TestJava
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#18         // com/xianyu/facade/demo/TestJava.n:I
   #3 = Class              #19            // com/xianyu/facade/demo/TestJava
   #4 = Class              #20            // java/lang/Object
   #5 = Utf8               n
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/xianyu/facade/demo/TestJava;
  #14 = Utf8               add
  #15 = Utf8               SourceFile
  #16 = Utf8               TestJava.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = NameAndType        #5:#6          // n:I
  #19 = Utf8               com/xianyu/facade/demo/TestJava
  #20 = Utf8               java/lang/Object
{
  public volatile int n;
    descriptor: I
    flags: ACC_PUBLIC, ACC_VOLATILE

  public com.xianyu.facade.demo.TestJava();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/xianyu/facade/demo/TestJava;

  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field n:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field n:I
        10: return
      LineNumberTable:
        line 14: 0
        line 15: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/xianyu/facade/demo/TestJava;
}
SourceFile: "TestJava.java"
  • javap -v
D:\Workspaces\fascade\target\classes\com\xianyu\facade\demo>javap -c TestJava
警告: 二进制文件TestJava包含com.xianyu.facade.demo.TestJava
Compiled from "TestJava.java"
public class com.xianyu.facade.demo.TestJava {
  public volatile int n;

  public com.xianyu.facade.demo.TestJava();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field n:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field n:I
      10: return
}

# 解析参考

  • 描述文件(Descriptor) 是一个字符串,可以用来描述一个方法的参数和返回类型。格式如下:
//(参数描述符)返回类型
(Ljava/lang/Object;[Ljava/lang/Object;)I
(II)I

括号中表示参数,括号外表示返回类型。L表示引用类型,I表示int类型,[表示数组。 以一个L开头的描述符,就是类描述符,它后紧跟着类的字符串,然后分号“;”结束。 比如Ljava/lang/String;就是表示类型String; Ljava/lang/String;它是一种对函数返回值和参数的编码。这种编码叫做JNI字段描述符(JavaNative Interface FieldDescriptors)。 当一个函数不需要返回参数类型时,就使用”V”来表示。

  • JNI符号对照表
Java 类型 JNI符号
Boolean Z
Byte B
Char C
Short S
Int I
Long J
Float F
Double D
数组 [
Void V
objects对象 L开头,以;结尾,中间是用/ 隔开的包及类名。
  • 访问标志(access_flags)
标志名称 说明
ACC_PUBLIC 是否为public类型
ACC_FINAL 是否被声明为final,只有类可设置
ACC_SUPER 是否允许使用invokespecial字节码指令
ACC_INTERFACE 标志这个是一个接口
ACC_ABSTRACT 是否为abstract类型,对于接口或抽象类来说,此标志值为true,其他值为false
ACC_SYNTHETIC 标志这个类并非由用户产生的
ACC_ANNOTATION 标志这个一个注解
ACC_ENUM 标志这是一个枚举
  • 属性表集合
属性名称 使用位置 说明
Code 方法表 java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被表明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code 属性 Java源码的行号与字节码指定的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的

Code节点就是CodeAttribute,里面包含了stacklocalsargs_size,以及几条指令。stack表示执行这个方法所需要的栈大小,这个值在编译时已经确定了,locals表示本地变量表的大小,args_size表示方法参数的数量,由于这个方法是个实例方法,所以第一个传进来的参数是实例自身,也就是this,然后才是方法的两个参数,所以参数为1。

# 附加操作指令

  • 对象操作指令
操作码 说明
new 创建新的对象实例
checkcast 类型强转
instanceof 判断类型
getfield 获取对象字段的值
putfield 给对象字段赋值
getstatic 获取静态字段的值
putstatic 给静态字段赋值
  • 整数运算指令
操作码 说明
iadd 将栈顶两int类型数相加,结果入栈
isub 将栈顶两int类型数相减,结果入栈
imul 将栈顶两int类型数相乘,结果入栈
idiv 将栈顶两int类型数相除,结果入栈
irem 将栈顶两int类型数取模,结果入栈
ineg 将栈顶int类型值取负,结果入栈
  • 常量入栈指令
操作码 说明
iconst_0 0(int)值入栈
iconst_1 1(int)值入栈
ldc(load constant) 常量池中的常量值(int, float, string reference, object reference)入栈。
  • 通用(无类型)栈操作指令
操作码 说明
nop 空操作
pop 从栈顶弹出一个字长的数据
pop2 从栈顶弹出两个字长的数据
dup 复制栈顶一个字长的数据,将复制后的数据压栈
dup_x1 复制栈顶一个字长的数据,弹出栈顶两个字长数据,先将复制后的数据压栈,再将弹出的两个字长数据压栈
dup2 复制栈顶两个字长的数据,将复制后的两个字长的数据压栈
dup2_x1 复制栈顶两个字长的数据,弹出栈顶三个字长的数据,将复制后的两个字长的数据压栈,再将弹出的三个字长的数据压栈

dup指令为复制操作数栈顶值,并将其压入栈顶,也就是说此时操作数栈上有连续相同的两个对象地址,也就是说需要从栈顶弹出两个实例对象的引用。其实对于每一个new指令来说一般编译器都会在其下面生成 一个dup指令,这是因为实例的初始化方法肯定需要用到一次,然后第二个留给程序员使用,例如给变量赋值,抛出异常等,如果我们不用,那编译器也会生成dup指令,在初始化方法调用完成后再从栈顶pop出来。

  • 将栈顶值保存到局部变量中指令
操作码 说明
astore_0 将栈顶引用类型值保存到局部变量0中
astore_1 将栈顶引用类型值保存到局部变量1中
  • 局部变量值转载到栈中指令
操作码 说明
aload_0 从局部变量0中装载引用类型值入栈
aload_1 从局部变量1中装载引用类型值入栈
iload_0 从局部变量0中装载int类型值入栈
iload_1 从局部变量1中装载int类型值入栈

aload_0从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶。 aload_0把this装载到了操作数栈中。 与iconst不同的是,iload操作的值是已经定义好存在的值,iconst是定义时的压栈操作。

  • 方法调用指令

处理方法调用,先将参数“存入”本地数组;要进行方法调用,必须将参数推到栈上,并且紧跟一个指向实例方法的 this 指针。

操作码 说明
invokevirtual 调用使用 virtual 语义的实例方法,比如调用的方法在运行时根据重载分派到不同的实例方法。
invokespecial 调用一个具体的实例方法(非 virtual 语义)。该指令常用来调用构造器
invokestatic 调用静态方法
invokeinterface 调用接口方法