Java反汇编指南javap
1. 前⾔
在正式解读《Effective Java》之前,我们需要先了解 Java 反汇编,因为反汇编是我们学习和研究问题的重要⼿段之⼀。
结合反汇编才能更好地理解《Effective Java》⼀书中给出的⼀些建议的根本原因,更深⼊的学习知识。
因为贯穿整个专栏的很多章节会涉及到 Java 反汇编,这将是我们深⼊研究《Effective Java》相关知识点的重要⼿段。
本⽂将从反汇编的⼯具,反编译举例等⾓度来讲解。
2. 是什么
反汇编是指把⽬标代码转为汇编代码的过程,也就是把机器语⾔转为汇编语⾔代码的意思。
本⽂所提到的 Java 反汇编是将 Java 编译器编译的 class ⽂件转为更易读的形式,包括局部变量表、异常表、代码⾏偏移映射表、汇编指令等。
3. 为什么
很多⼈⼯作⼀两年甚⾄都没执⾏过⼀次 javap 命令。很多⼈会有这样的困惑,平时不学字节码也不影响⾃⼰的学习和⼯作啊。然⽽,为何要当做⼀个⽐较重要的前置章节来讲呢?
这是因为当你真正了解字节码时,更容易理解⼀些优化⼿段,能够从更深的层次理解 Java 语⾔,更容易认识到问题的本质原因。
4. 怎么做
接下来我们看下⾯⼀个⾮常简单的⼀个案例来学习反汇编,简单介绍字节码相关知识。
package ffectivejava;
public class SimpleDemo {
public static void main(String[] args) {
int a = 1 + 2; // 第 6 ⾏
System.out.println(a);
}
}
4.1 ⼯具
4.1.1 javap
java 为我们提供了⼀个字节码查看⼯具: javap。
直接通过 javap -help 查看其⽤法
⽤法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此⽤法消息
-version 版本信息
-v -verbose 输出附加信息
-
l 输出⾏号和本地变量表
-public 仅显⽰公共类和成员
-protected 显⽰受保护的/公共类和成员
-package 显⽰程序包/受保护的/公共类
和成员 (默认)
-p -private 显⽰所有类和成员
-c 对代码进⾏反汇编
-s 输出内部类型签名
-sysinfo 显⽰正在处理的类的
系统信息 (路径, ⼤⼩, ⽇期, MD5 散列)
-constants 显⽰最终常量
-
classpath <path> 指定查⽤户类⽂件的位置
-cp <path> 指定查⽤户类⽂件的位置
-bootclasspath <path> 覆盖引导类⽂件的位置
我们⾸先对源码进⾏编译javac SimpleDemo.java,然后采⽤ javap 进⾏反编译。
javap -c -v SimpleDemo
Classfile /Users/liuwangyang/Coding/git/javalearning/src/main/java/com/chujianyun/others/effectivejava/SimpleDemo.class
Last modified 2020-1-11; size 432 bytes
MD5 checksum e25f7c937eccaab6db2e5b99ef48733c
Compiled from "SimpleDemo.java"
public class hers.effectivejava.SimpleDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // com/chujianyun/others/effectivejava/SimpleDemo
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 SimpleDemo.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 com/chujianyun/others/effectivejava/SimpleDemo
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public hers.effectivejava.SimpleDemo();
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 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_3
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 6: 0
line 7: 2
line 8: 9
}
SourceFile: "SimpleDemo.java"
4.1.2 jclasslib
是⼀种可视化的字节码查看⼯具,还提供了。
安装以后,在 IDEA 编译源码后,可以选择 “view” ->“Show Bytecode With Jclasslib” 即可查看字节码:
如上图所⽰,可以直观地看到 class ⽂件包含基本信息、常量池、接⼝信息、字段信息、⽅法信息和属性信息。
其中⽅法信息⼜包含⾏号表、局部变量表,异常表等。
⾏号表的左侧表⽰源代码的⾏数,右侧表⽰在 Code 中的⾏数:
LineNumberTable:
line 6: 0
line 7: 2
line 8: 9
表⽰源代码中的第 6 ⾏对应 Code 的 0 ⾏(也可以理解为偏移数)。
使⽤ jclasslib 字节码查看⼯具的优势在于更直观,另外可以点击字节码指令会⾃动通过浏览器到 jvm 规范的相关指令的介绍⽹页,对⼤家熟悉字节码指令帮助⾮常⼤。
4.2 资料
字节码相关知识内容庞杂,全部掌握不是⼀朝⼀⼣的事情。
除了平时多动⼿之外,强烈推荐⼤家多读读两本⾮常经典的图书:《深⼊理解 Java 虚拟机》、《Java 虚拟机规范》。
⼤家也可以通过 Oracle 的⽹页⾥浏览和下载《Java 语⾔规范》、《Java 虚拟机规范》。
4.3 简要介绍
接下来要简单了解 class ⽂件结构和常见的字段描述符和助记符。
4.3.1 class ⽂件结构
从反汇编的代码中我们可以基本了解到:
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魔数固定值 0xCAFEBABE
minor_version副版本号
major_version主版本号
constant_pool_count常量池计数器
constant_pool[]常量池
access_flags访问标记
this_class类索引必须是常量池表中⼀个有效的索引值
super_class⽗类索引要么是 0 (Object 类),要么是常量池表中⼀个有效的索引值
interfaces_count接⼝计数器当前类或接⼝的直接接⼝数量
interfaces[]接⼝表
fields_count字段计数器
fields[]字段表每个成员都必须是 fields_info 结构
methods_count⽅法计数器
methods⽅法表每个成员都必须是 method_info 结构
attributes_count属性计数器
attributes[]⽂件属性表每个成员必须是 attribute_info 结构
想了解更详细的内容,⼤家可以进⼀步阅读《Java 虚拟机规范》。
4.3.2 常见知识讲解
读字节码最需要了解的是字段描述符:
FieldType 中的字符类型含义
B byte有符号的字节型数
C char Unicode 字符码点,UTF-16 编码
D double双精度浮点数
F float单精度浮点数
I int整型数
J long长整型
FieldType 中的字符类型含义
L className;reference ClassName类的实例
S short有符号短整型
Z boolean布尔值 true/false
[Reference⼀个⼀维数组
如 String 类的实例,其描述符为 Ljava/lang/String。⼆维数组 int [][] 类型的实例变量,其描述符为 [[I。常见的 Java 虚拟机指令的助记符:
类型助记符含义
常量const_null将 null 推到栈顶
iconst_1将整数类型的 1 推到栈顶
fconst_0将 float 类型的 2 推到栈顶
dcounst_0将 double 类型的 0 推到栈顶
ldc将 int 、float 或 String 类型常量值从常量池推⾄栈顶
…
加载iload将指定的 int 类型本地变量推送⾄栈顶
iload_2第 2 个 int 类型本地变量推送⾄栈顶
aload_3将第 3 个引⽤类型的本地变量推送⾄栈顶
cload将 char 类型数组的指定元素推送⾄栈顶
…
存储istore将栈顶 int 类型数值存⼊指定的本地变量
astore将栈顶引⽤类型的数值存⼊指定的本地变量
istore_3将栈顶 int 类型数值存⼊第 3 个本地变量
…
引⽤getstatic获取指定类的静态字段,并将其压⼊栈顶
putstatic为指定类的静态字段赋值
getfield获取指定类的实例字段,并将其值压⼊栈顶
putfield为指定类的实例字段赋值
invokevirtual调⽤实例⽅法
invokespecial调⽤⽗⽅法、实例初始化⽅法、私有⽅法
invokestatic调⽤静态⽅法
invokeinterface调⽤接⼝⽅法
invokedynamic调⽤动态连接⽅法
new创建⼀个对象,并将其引⽤压⼊栈顶
athrow将栈顶异常抛出
instanceof检查对象是否为指定类的实例,如果是,将 1 压到栈顶,否则将 0 压到栈顶
…
栈pop将栈顶数值弹出(⾮ long 和 double)
pop2将栈顶 long 或 double 数值弹出
dup复制栈顶数值并将复制值压⼊栈顶
…
控制ireturn从当前⽅法返回 int 值
return从当前⽅法返回 void 值
…
⽐较ifeq当栈顶的 int 类型的数值等于 0 时跳转
ifne当栈顶的 int 类型的数值不等于 0 时跳转
…
拓展为 null 时跳转
…
数学…
转换
⽐较
更多助记符的含义,请参考或使⽤ jclass 字节码查看插件快捷跳转到对应助记符的含义中进⼀步学习。
4.4 ⽰例讲解
有了上⾯的基础后,结合 main 函数的字节码,我们反推源码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_3
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 6: 0
line 7: 2
line 8: 9
}
通过描述符 (description) 可知:⽅法的参数为字符串类型的数组,返回值类型为 void。通过访问标记 (flags) 可知:该函数为 public static 修饰。
栈的最⼤深度为 2 ;局部变量表的元素有 2 个;参数的长度为 1。
局部变量表和操作数栈初始状态如下图所⽰:
执⾏iconst_3将 int 类型常量 3 压⼊栈顶。
执⾏istore_1栈顶将 int 类型的值存⼊局部变量表索引为 1 的变量中。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论