一、基本介绍
1.1、java的平台无关性
- JAVA源代码->Class字节码->JVM解
释执⾏(依赖于不同的jvm实现跨平台) - Java 虚拟机(JVM):负责将字节码⽂
件翻译成特定平台下的机器码然后运
⾏。 - 不同的平台有不同的JVM实现
1.2、字节码的语言无关性
字节码(ByteCode)是构成平台无关性的基石,这也决定了,只要是能转换成字节码,其他语言也可以运行在jvm之上,所以现在不只是java,还有kotlin、groovy等都可以运行在jvm之上。
所以把JVM(Java Virtual Machine Java虚拟机
)叫CVM(Class Virtual Machine Class虚拟机
)反而更合适一些.
1.3、Class文件格式
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中。
1.3.1、字节码的两种基本数据类型
Class文件结构包含两种数据类型:
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
1.3.2、Class文件表构成
1.3.3、编译测试
编译下面这个类
public class JavaCodeTest {}
然后把得到的.class
用16进制编辑器直接打开字节码显示是这样的:
可以看到开头的四个字节是
CAFEBABE
,后面的字节可以意思对应上方表格进行查看,都是严格按照顺序一一对应的。
1.4、常用属性介绍
在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。任何人实现编译器都可以向属性表中写入自己自定义的属性信息,但是java虚拟机运行时会忽略掉他不认识的属性。
《Java虚拟机规范(Java SE 7)》版中,预定义属性已经增加到21项,如下表所示:
1.4.1、Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。
Code属性是Class文件中最重要的一个属性
,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能。
比如这个类
public class JavaCodeTest {
private int a;
public int testAdd() {
return a + 1;
}
}
编译后的方法部分
如下所示(下面这种为转义过的,未转义的都如上面介绍的那样,为16进制形式)
如下所示,具体每一个字段的释义可以参考注释。
{
//---略
public com.canzhang.asmdemo.test.JavaCodeTest();
//descriptor 对方法参数和返回值进行描述
descriptor: ()V//`()`表示无参数,`V`表示Void,无返回值
flags: ACC_PUBLIC//方法修饰符,表示是public的,可以有多个。
Code://方法体
stack=1, locals=1, args_size=1//参数个数
//aload_0指令表示:将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
0: aload_0
//invokespecial指令表示:以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法
1: invokespecial #1 // 这里的`#1`是invokespecial指令的参数,表示指向常量池声明的常量:Method java/lang/Object."<init>":()V
//指令return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
4: return
//LineNumberTable 是用来描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系,可以配置不生成,不生成就无法获取异常发生源码行号,也无法按照源码的行数来调试程序。
LineNumberTable:
line 6: 0
public int testAdd();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 10: 0
}
//---略
疑问点:
- 第一个方法
<init>()
是什么?
java在编译之后会在字节码文件中生成<init>方法,称之为实例构造器 - 实例构造器<init>()和testAdd(),这两个方法很明显都是没有参数的,为什么Args_size会为1?而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那Locals又为什么会等于1?
在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果testAdd()方法声明为static,那Args_size就不会等于1而是等于0了。
附录:
生成字节码样例参考
java文件
public class JavaCodeTest {
private int a;
public int testAdd() {
return a + 1;
}
}
//生成字节码
javac /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.java
//反编译字节码
javap -v /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.class
反编译结果
Classfile /Users/canzhang/AndroidStudioProjects/ASMDemo/app/src/main/java/com/canzhang/asmdemo/test/JavaCodeTest.class
Last modified 2020-11-5; size 311 bytes
MD5 checksum 985828dc144886121bd06e4d19423d65
Compiled from "JavaCodeTest.java"
public class com.canzhang.asmdemo.test.JavaCodeTest
minor version: 0//jdk次版本号
major version: 52//jdk主版本号
flags: ACC_PUBLIC, ACC_SUPER
Constant pool://常量池
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/canzhang/asmdemo/test/JavaCodeTest.a:I
#3 = Class #17 // com/canzhang/asmdemo/test/JavaCodeTest
#4 = Class #18 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code//属性的名称 对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示
#10 = Utf8 LineNumberTable//属性的名称
#11 = Utf8 testAdd
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 JavaCodeTest.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // a:I
#17 = Utf8 com/canzhang/asmdemo/test/JavaCodeTest
#18 = Utf8 java/lang/Object
{
public com.canzhang.asmdemo.test.JavaCodeTest();
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 6: 0
public int testAdd();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 10: 0
}
常用的几个指令
- .java--->.class:
javac /xxxx/JavaCodeTest.java
- .class 转换字节码内容:
javap -c /xxxx/JavaCodeTest.class
用法: 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> 覆盖引导类文件的位置
对应android javac
,并不能保证所有都正常编译,因为有很多android sdk的内容是识别不了的,另外android编译过程中也会有自己一些额外处理(比如脱糖、插桩一类的),所以最好直接使用android工程编译后的产物也进行字节码分析,一般工程如下图目录已经存在了编译后的.class文件,我们可以直接取用.class 使用javap转换成可以读懂的文字内容就可以分析了。
具体目录:app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/xxx
android studio 便捷命令配置
为了方便我们使用javap,可以在android studio 配置tool,方便我们使用:
如图示 依次打开 tools-external tools -点击左下角的添加按钮,按照箭头输入几个关键项就可以了:
- Name:随便填写
- Program:
$JDKPath$\bin\javap
- Arguments:
-c $FileClass$
- Working directory:
$OutputPath$
然后点击 ok apply 就可以使用了,使用方法,如下图所示:
注意事项:
- 使用的时候默认获取的是当前打开的类作为输入入参,比如你想看
MainActivity.java
对应的字节码文件,那么就打开MainActivity.java
就可以了 - 需要工程编译后才能按照配置的路径,找到对应的.class文件。
- 如果编译了,依然提示文件不存在,上面的配置项可以尝试清空,使用右侧的 insert 去插入对应路径,上面的几个路径,都是有选项可选的,按照名字选择即可。
字节码查看器
Hex Fiend
下载后直接双击打开,就可以看到对应的字节码,选中还可以高亮对应。
未完待续.....
参考文章
kotilin中的某些特性 :https://juejin.im/post/6844903588716609543
反射原理