使用Java字节码分析四则运算
19 Jun 2021 | Java |下面展示了一个简单的 Java
类,进行了四则运算。
public class Hello{
public static void main(String[] args){
int a = 1;
int b = 2 + a;
int c = 3 * b;
int d = c - a;
float e = d / 2f;
System.out.println(e);
}
}
通过命令 javac -g Hello.java
进行编译,然后通过命令 java Hello
运行编译后的代码,得到结果是 4.0
。
接着通过命令 javap -c -verbose Hello
进行反编译,输出结果如下:
Classfile /D:/Hello.class
Last modified 2021-6-19; size 604 bytes
MD5 checksum 2529ef11eea3e7947cfc9553f36bda77
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #29.#30 // java/io/PrintStream.println:(F)V
#4 = Class #31 // Hello
#5 = Class #32 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 LHello;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 d
#22 = Utf8 e
#23 = Utf8 F
#24 = Utf8 SourceFile
#25 = Utf8 Hello.java
#26 = NameAndType #6:#7 // "<init>":()V
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(F)V
#31 = Utf8 Hello
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (F)V
{
public Hello();
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 2: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LHello;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=6, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: iload_1
4: iadd
5: istore_2
6: iconst_3
7: iload_2
8: imul
9: istore_3
10: iload_3
11: iload_1
12: isub
13: istore 4
15: iload 4
17: i2f
18: fconst_2
19: fdiv
20: fstore 5
22: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
25: fload 5
27: invokevirtual #3 // Method java/io/PrintStream.println:(F)V
30: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 6
line 8: 10
line 9: 15
line 11: 22
line 12: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
2 29 1 a I
6 25 2 b I
10 21 3 c I
15 16 4 d I
22 9 5 e F
在编译时加上了 -g
参数,是为了生成局部变量表 LocalVariableTable
;在反编译时加上了 -verbose
是为了输出附件信息。 在上面的例子中反编译输出的结果主要包括:类的来源,校验和,版本号,常量池,构造函数,main
函数。
尝试对其中的信息进行分析:
Classfile /D:/Hello.class //描述了文件来源
Last modified 2021-6-19; size 604 bytes //修改信息,文件大小
MD5 checksum 2529ef11eea3e7947cfc9553f36bda77 // MD5 校验和
Compiled from "Hello.java" // 对哪个类进行反编译
public class Hello
minor version: 0
major version: 52 // java 版本号
flags: ACC_PUBLIC, ACC_SUPER // 该类是 public
Constant pool: // 常量池
#1 = Methodref #5.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #29.#30 // java/io/PrintStream.println:(F)V
#4 = Class #31 // Hello
#5 = Class #32 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 LHello;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 d
#22 = Utf8 e
#23 = Utf8 F
#24 = Utf8 SourceFile
#25 = Utf8 Hello.java
#26 = NameAndType #6:#7 // "<init>":()V
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(F)V
#31 = Utf8 Hello
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (F)V
{
public Hello(); //构造器
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1 //构造器函数 使用的栈深度是1,用于存放this指针
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable: //行号表:java源文件行号与字节码文件偏移量之间的对应关系
line 2: 0
LocalVariableTable: // 局部变量表:在一个方法中用到的变量
Start Length Slot Name Signature
0 5 0 this LHello;
public static void main(java.lang.String[]); // main 方法
descriptor: ([Ljava/lang/String;)V //方法描述:是一个String对象的数组
flags: ACC_PUBLIC, ACC_STATIC // 方法是 public static
Code:
stack=2, locals=6, args_size=1 // 当前方法栈深度是2,有6个变量,1个入参
0: iconst_1 // 常量值1放到栈
1: istore_1 // 将栈顶值放到 局部变量表中的1号槽位
2: iconst_2 // 常量值2放到栈
3: iload_1 // 将局部变量表中的1号槽位的值放到栈顶
4: iadd //执行一次加法操作
5: istore_2 //将栈顶值放到 局部变量表中的2号槽位
6: iconst_3 // 常量值3放到栈
7: iload_2 // 将局部变量表中的2号槽位的值放到栈顶
8: imul //执行一次乘法操作
9: istore_3 // 将栈顶值放到 局部变量表中的3号槽位
10: iload_3 // 将局部变量表中的3号槽位的值放到栈顶
11: iload_1 // 将局部变量表中的1号槽位的值放到栈顶
12: isub //执行一次减法操作
13: istore 4 // 将栈顶一个int类型的值放到 局部变量表中的4号槽位
15: iload 4 // 将局部变量表中的4号槽位的值放到栈顶
17: i2f //int 类型转换成 float类型
18: fconst_2 // 一个float类型的常量值2放到栈
19: fdiv //执行一次除法操作
20: fstore 5 // 将栈顶一个float类型的值放到 局部变量表中的5号槽位
22: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
25: fload 5
27: invokevirtual #3 // Method java/io/PrintStream.println:(F)V //方法调用
30: return //返回
LineNumberTable: //行号表:java源文件行号与字节码文件偏移量之间的对应关系
line 5: 0 // 源文件中表示 int a = 1;
line 6: 2
line 7: 6
line 8: 10
line 9: 15
line 11: 22
line 12: 30
LocalVariableTable: // 局部变量表:在一个方法中用到的变量
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String; // 0号槽位对应变量args, 类型是Stirng[]
2 29 1 a I // 1号槽位对应变量a, 类型是Integer
6 25 2 b I // 2号槽位对应变量b, 类型是Integer
10 21 3 c I // 3号槽位对应变量c, 类型是Integer
15 16 4 d I // 4号槽位对应变量d, 类型是Integer
22 9 5 e F // 5号槽位对应变量f, 类型是Float
在上面对其中的信息进行了注释,相信能够看的明白各个助记符的含义。
文章到这里,都还没有介绍 Java字节码
,因为我想先通过实际的用例来说明Java字节码
的含义。Java bytecode
由单字节( byte )
的指令组成, 理论上最多支持 256
个操作码(opcode)
。实际上Java
只使用了200
左右的操作码, 还有一些操作码则保留给调试操作。
操作码, 或者称为 指令 ,主要由 类型前缀
和 操作名称
两部分组成。 例如, i
前缀代表 integer
,所以,iadd
表示对整数执行加法运算。i2f
表示 int
类型转换成 float
类型。 fdiv
表示对浮点数执行除法运算。
根据指令的性质,主要分为四个大类:
- 栈操作指令,包括与局部变量交互的指令
- 程序流程控制指令
- 对象操作指令,包括方法调用指令
- 算术运算以及类型转换指令
在上面给的例子中,可以看到四则运算的过程有很多栈
的操作,因为JVM
是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack)
, 用于存储 栈帧 (Frame)
。每一次方法调用,JVM
都会自动创建一个栈帧。 栈帧 由 操作数栈 , 局部变量表 以及一个 class指针
组成。 class指针
指向当前方法 在运行时常量池中对应的class
。用一个图说明他们之间的关系。
操作数栈和局部变量表之间频繁使用的指令是 store
和 load
。
刚才提到了JVM
是一台基于栈的计算机器,现在用一个简单的示例看一下计算过程。
public class Hello{
public static void main(String[] args){
int a = 4;
int b = 5;
int c = a + b;
System.out.println(c);
}
}
编译 :javac -g Hello.java
运行: java Hello
反编译: javap -c -verbose Hello
(下面只是展示了main函数中的计算过程)
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_4
1: istore_1
2: iconst_5
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 4
line 9: 8
line 10: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
2 14 1 a I
4 12 2 b I
8 8 3 c I
对上面的指令步步解析:
首先是 ` 0: iconst_4 ,生成一个整数类型的常量值
4` 并放到栈顶。
然后是 ` 1: istore_1 ,将栈顶的整数值存储到局部变量表中1号槽位,即
a=4`。
接着是 ` 2: iconst_5 ,生成一个整数类型的常量值
5` 并放到栈顶。
然后是 ` 3: istore_2 ,将栈顶的整数值存储到局部变量表中
2号槽位,即
b=4`。
接着是 ` 4: iload_1 ,将局部变量表中
1`号槽位的值加载到栈顶。
接着是 ` 5: iload_2 ,将局部变量表中
2`号槽位的值加载到栈顶。
接着是 ` 6: iadd` ,执行一次加法运算。
接着是 ` 7: istore_3 ,将栈顶的整数值存储到局部变量表中
3号槽位,即
c=9`。
接着是 ` 8: getstatic ,获取静态字段,即此时执行了
System.out`。
接着是 ` 11: iload_3 ,将局部变量表中
3`号槽位的值加载到栈顶。
接着 ` 12: invokevirtual ,执行方法调用,即此时执行了
out.println(c)`。
最后是 ` 15: return` ,方法返回。
最后,从上面的分析过程,我们还可以看到:一个简单的加法操作是需要三个指令才能完成的:int c = a + b
。
4: iload_1
5: iload_2
6: iadd