# 反编译遇到的相关指令问题
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
,里面包含了stack
,locals
,args_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 | 调用接口方法 |