JVM 字节码
大家都知道 Java
是一门基于虚拟机的语言,我们只需要提供虚拟机能够执行的字节码就好了,每一个平台的不同实现的 JVM
虚拟机都执行同样的 JVM
字节码,这样也就实现了完美的跨平台,So cool。当然 JVM
字节码可不是 Java
语言所独有的,理论上任意的语言都可以编译成 JVM
字节码就可以供 JVM
虚拟机运行,只要你开发对应的编译器。 常见的 JVM
语言有,Kotlin
、 Scala
和 Groovy
等等。
了解一些 JVM
字节码的一些基础知识还是有必要的,当出现某些 Bug 的时候,我们可以通过字节码的方式来定位一些通过 Java
源代码无法定位到的问题,因为字节码的颗粒度比源代码更加细;如果你要通过 ASM
来完成对字节码的插桩,来完成某项功能,字节码就是一门必修课;还可以通过阅读某些语言编译后的字节码来学习这门语言,比如 Kotlin
。
文章开头我会先简单介绍一下 class
字节码文件的标准组成,这部分对阅读字节码指令的影响不大,读者可以按照自己的需求跳过,后面就重点介绍方法中重要的字节码指令,工具和例子分析。
1 JVM class 文件基本结构
1.1 基本结构
1.2 头部
4 Bytes:CAFE BABE (固定,也就是常说的 MAGIC NUM
,咖啡宝贝,用来识别是否为class字节码文件)
2 Bytes: 次版本号 (绝大部分情况为0)
2 Bytes: 主要版本号 (高版本的 JVM
虚拟机可以运行低版本编译器编译的字节码,比如 1.5 的编译器编译的字节码,1.8 的虚拟机是可以运行的,反之则不行)
1.3 常量池
常量池中会用来存放很多的信息,包括类的定义,方法的定义,成员变量的定义,还有类中用到的8种基本类型和字符串等等等等,其他地方如果需要这些信息,通过编号获取。这也是字节码中占用空间的大头。
所有的类型:
不同的类型的数据结构:
1.4 类的基本信息
2 Bytes: 访问标记 (也就是 Public
,Protect
,Private
啥的)
访问标记表(属性和方法也适用):
2 Bytes: 类索引
2 Bytes: 父类索引
2 Bytes: 实现接口数量
?Bytes:实现的接口信息
1.5 成员变量
2 Bytes: 成员变量数量
?Bytes:成员变量信息
成员变量数据结构:
1.6 方法
2 Bytes:方法数量
? Bytes: 方法信息
方法基本信息:
方法code部分基本信息:
方法方法 code
部分是很重要的部分,也是我们平时分析问题时最多的部分。其中包括重要的信息:最大操作数栈,本地变量表最大数,字节码长度,字节码操作指令,异常表,本地变量表,行号表 (也就是源码中的行号对应的字节码行号)
1.7 属性表
这个属性表是用来描述本 class
类中的一些熟悉的,不要和成员变量搞混了,比如说该 class
文件对应的源代码文件是什么,其中包含有哪些内部类。
2 Bytes: 属性表长度
?Bytes:属性信息
2 方法
2.1 方法栈帧
2.1.1 本地变量表
本地变量表储存了方法参数,当前对象引用(也就是this,如果不是静态方法的话),局部变量。本地变量表中的储存单位是叫做 Slot
(通常是 4 个字节),除了 long
和 double
类型的变量占用 2 个 Slot
,其他类型都占用一个 Slot
。 当运行的指令需要的参数通常是从本地变量表中加载到操作数栈中,指令返回的数据如果需要供后面的操作使用,就需要再从操作数栈中保存到本地变量表中。
这里注意本地变量表的大小不是所有变量的个数加起来的,因为变量表的位置是可以重复被不同的变量使用的,当可以确定某个 Slot
的数据不会在后续的操作中使用时,就可以被其他的变量使用,避免重新分配 Slot
,减少内存的浪费。在方法的信息里面有最大的 Slot
的,本地变量表的大小在编译的时候就已经确定了。
2.1.2 操作数栈
也就是执行操作指令的栈,栈里面存放指令所需要的参数,和保存指令返回的结果。操作数栈里面参数需要从本地变量表中加载,返回的结果如果需要保存在本地变量表中就通过指令保存,如果不需要这个结果,可以通过 pop
指令舍弃。
2.1.3 方法返回地址
2.1.4 动态链接
主要是invokeDynamic这个指令,1.8以后 labmda
表达式就会用到这个指令.
2.1.5 附加信息
2.2 字节码指令
字节码指令占用一个字节,也就是说最多也就256种指令。这里先给一个字节码指令表:shimo.im/sheets/GyrH...
在阅读字节码的时候,遇到不会的指令可以查一下。下面列出一些常见指令。
2.2.1 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,每个指令执行的参数必须在栈顶。
加载到操作数栈常见指令:iload(int, boolean, sort, char, byte), lload(long), fload(float), dload(double),aload(引用)
从操作数栈到本地变量表常见指令: istore(int, boolean, sort, char, byte), lstore(long), fstore(float), dstore(double),astore(引用)
2.2.2 运算指令
加减法指令:isub
乘法指令: imul
除法指令:idiv
求余指令: irem
取反指令: ineg
位移指令:ishl, ishr
或指令:ior
与指令:iand
异或指令:ixor
局部变量自增指令:iinc
2.2.3 类型转换指令
i2c, i2s, i2b, f2l
2.2.4 对象创建与访问指令
对象创建指令:new
数组创建指令: newarray, anewarray
访问字段指令: getfield, putfield
2.2.5 操作数栈指令
出栈指令: pop, pop2 (当调用一个函数的返回值不为空,但是你又不使用该变量,就应该调用这个指令让返回值出栈)
复制栈顶元素并重新入栈:dup, dup2
栈顶两个元素交换: swap
2.2.6 控制转移指令
条件分支:iflt,ifgt,ifeq,ifne
无条件分支: goto, jst
2.2.7 方法调用和返回指令
调用普通对象的public方法:invokevirtual
调用接口方法:invokeinterface
调用私有,父类,保护,初始化方法:invokespacial
调用静态方法:invokestatic
动态调用(Java8中的 lambda 用到了这个指令,有点复杂): invokedynamic
返回指令:return,lreturn, freturn, dreturn, areturn
3 与 Java 字节码相关的工具
3.1 javap
jdk
中提供的工具链,查看 class
字节码,也只能查看 class
字节码。
优点:稳定,查看单个 class
文件方便,能够查看的数据多。
缺点:一次只能查看单个文件,没有 UI,只能在控制台中查看
3.2 dexdump
Android 官方提供的查看 dex
文件中 class
文件信息
优点:能够直接查看 dex
文件
缺点:只能在控制台中运行没有 UI,输出的文本文件特别大,不是很推荐使用。
3.3 enjarify
Google官方出品, Github: github.com/Storyyeller...
能够将 apk
或者 dex
中的 Dalvik VM
字节码等效地转换成 Jvm
字节码
优点:功能强大,是 Dex2Jar
的替代品。
缺点:少数类无法转换。(很少的类), 转换成 jar
的过程中会清除本地变量表,class
属性等一些描述信息,最大操作数栈信息也不对。
总的来说推荐使用,也没有更好的选择。
3.4 jadx
Github: github.com/skylot/jadx
能够将 apk
, dex
, jar
, class
等多种类型文件反编译成源代码文件,就连 Android 中的资源文件都可以反编译,还有友好的 GUI 界面。
优点:功能强大,转换类型多,有 GUI,推荐使用。
缺点:-
3.5 jclasslib
Github:github.com/ingokegel/j...
能够查看jar和class文件的字节码,操作界面友好,能够同时查看多个class文件的字节码,还支持字节码的直接编辑,推荐使用。
4 使用工具查看 APK 中的字节码
4.1 使用enjarify工具将APK或者DEX转换成Jar包 (如果直接是jar或者是class这步跳过)
找到需要处理的apk文件
执行命令:
enjarify app-debug.apk
得到转换后的jar文件:
app-debug-enjarify.jar
4.2 使用jadx反编译成源码和字节码同步看 (可选)
执行命令:
jadx-gui app-debug-enjarify.jar
就可以直接看到 UI 了,然后找想看的类的反编译后的源代码就好。
4.3 使用 jclasslib 查看字节码
打开 jclasslib 软件将 4.1 中生成的jar包添加到 classpath,然后就可以找到需要查看的class 文件了。
对应的源码如下,我们只看 say()
方法:
Java
package com.tans.androidbytecode;
public class Person {
public final String name;
// ...
public void say(String words) {
System.out.printf("%s say: %s\n", name, words);
}
// ....
}
找到我们类对应的 say()
方法,然后查看字节码如下:
perl
0 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
3 astore_2
4 iconst_2
5 anewarray #4 <java/lang/Object>
8 astore_3
9 aload_0
10 getfield #19 <com/tans/androidbytecode/Person.name : Ljava/lang/String;>
13 astore 4
15 aload_3
16 iconst_0
17 aload 4
19 aastore
20 aload_3
21 iconst_1
22 aload_1
23 aastore
24 aload_2
25 ldc #45 <%s say: %s>
27 aload_3
28 invokevirtual #43 <java/io/PrintStream.printf : (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;>
31 pop
32 return
刚进入方法时本地变量表内容:
Slot | name | type |
---|---|---|
0 | this(变量自己) | Person |
1 | words(方法参数) | String |
执行下面指令:
kotlin
0 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;> //获取静态变量 out, 入操作数栈
3 astore_2 //添加到本地变量表
获取静态变量 System.out
,它的类型是 java.io.PrintStream
,将 getstatic
指令的返回结果存入本地变量表 Slot 2
中。
本地变量表内容:
Slot | name | type |
---|---|---|
0 | this(变量自己) | Person |
1 | words(方法参数) | String |
2 | out | PrintStream |
执行下面指令:
less
4 iconst_2 //静态变量值2,入操作数栈
5 anewarray #4 <java/lang/Object> // 创建Object数组,大小为2,同时入操作数栈
8 astore_3 // 添加到本地变量表,slot 3
通过 iconst_2
将常量 2 加入操作数栈,调用 anewarray
指令创建大小为 2 的 Object
数组,然后存放到本地变量表 Slot 3
中。
本地变量表内容:
Slot | name | type |
---|---|---|
0 | this(变量自己) | Person |
1 | words(方法参数) | String |
2 | out | PrintStream |
3 | arrays | Object[2] (空数组) |
执行下面指令:
arduino
9 aload_0 // 本地变量表slot 0,入操作数栈,也就是this。
10 getfield #19 <com/tans/androidbytecode/Person.name : Ljava/lang/String;> //this.name成员变量,如操操作数栈.
13 astore 4 // 将name,入本地变量表slot 4
通过 aload_0
加载本地变量表中 slot 0
中的数据到操作数栈 (也就是 this
,Persion
对象),然后调用 getfield
指令,加载 Persion#name
属性,类型是 String
到操作数栈,然后存放在本地变量表 slot 4
中。
本地变量表内容:
Slot | name | type |
---|---|---|
0 | this(变量自己) | Person |
1 | words(方法参数) | String |
2 | out | PrintStream |
3 | arrays | Object[2] (空数组) |
4 | name | String |
执行字节码指令:
arduino
15 aload_3 // arrays从变量表中入栈
16 iconst_0 // 静态变量0,入栈
17 aload 4 // name,入栈
19 aastore // 执行数组中插入值,也就是array[0] = name
20 aload_3 // 下面的命令和上面类似
21 iconst_1
22 aload_1 // 参数中的words,入栈。
23 aastore // 修改数组中的值,array[1] = words
这段指令主要是用来设置 arrays
中的数据,aload_3
将 arrays
入栈,iconst_0
将常数 0 入栈,aload_4
将成员变量 name
入栈,调用 aastore
为数组设值,其实上面做了这么多操作就是将 name
赋值给 Object[0]
,后面也是类似的操作,只是把参数中的 words
赋值给 Object[1]
。
本地变量表:
Slot | name | type |
---|---|---|
0 | this(变量自己) | Person |
1 | words(方法参数) | String |
2 | out | PrintStream |
3 | arrays | Object[2] (已经初始化值,[0]=name, [1]=words) |
4 | name | String |
执行字节码指令:
javascript
24 aload_2 // 从变量表加载out入栈
25 ldc #45 <%s say: %s> // 从常量池加载字符串入栈
27 aload_3 // 从变量表加载arrays入栈
28 invokevirtual #43 <java/io/PrintStream.printf : (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;> //调用out.printf方法
31 pop // 将printf的返回值出栈
32 return // 返回方法
aload_2
将 out
变量入栈,通过 ldc
指令从常量池中加载 %s say: %s
入栈,aload_3
将 arrays
入栈,最后执行 invokevirtual
方法,调用 out
变量的 printf
方法,方法的签名为 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
,由于我们没有用到 printf
方法的返回值,然后再调用 pop
指令,清空最上面的栈,我们的方法没有返回值,直接通过 return
返回就行了。
上面就完成了对一个方法的简单分析。
这里要注意一下:
本地变量表的空间是可以重复利用的,比如说 slot1
中的数据在后面的操作中是不会再使用的,新产生的数据是可以再放入 slot1
中的。比如说我要执行 int a = 1 + 1
,执行这个操作就需要将两个加数入栈,假如分别是 slot0
和 slot1
,执行完 iadd
指令后会产生新的值,这个时候 slot0
和 slot1
中的值就没有用了,新产生的值就不需要放在 slot2
中,直接放入 slot1
就好了。
猴,我相信你已经对 JVM
字节码已经懂一点点了,我们再去找一个场景练练手,那就选择 Android 环境下和 JVM
环境下的 lambda
对比。
5 对比 Android 和 JVM 中的 lambda 表达式
测试代码为:
MainActivity.kt
Kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
doSomething {
println("Hello, World")
}
}
private fun doSomething(f: TestInterface) {
f.f()
}
}
TestInterface.java
Java
public interface TestInterface {
void f();
}
5.1 Android 处理 Lambda
5.1.1 Labmda 中无其他引用
MainActivity.class onCreate方法字节码(忽略其他无关字节码)
swift
11 getstatic #46 <com/tans/myapplication/-$$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc.INSTANCE : Lcom/tans/myapplication/-$$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc;>
14 astore_2
15 aload_0
16 aload_2
17 invokespecial #50 <com/tans/myapplication/MainActivity.doSomething : (Lcom/tans/myapplication/TestInterface;)V>
上面字节码表示:获取静态对象 com/tans/myapplication/-$$LambdaMainActivitywt_38GqJcEewnV1aO0Q7GXbL5Fc.INSTANCE,把这个对象当成参数传递给自己的doSomthing方法。
MainActivity.class doSomething方法:
bash
0 aload_1
1 invokeinterface #13 <com/tans/myapplication/TestInterface.f : ()V> count 1
6 return
很简单直接调用传入对象的f方法.
com/tans/myapplication/-$$LambdaMainActivitywt_38GqJcEewnV1aO0Q7GXbL5Fc 对象的f方法:
bash
0 invokestatic #20 <com/tans/myapplication/MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc : ()V>
3 return
同样很简单,又反过去调用MainActivity的静态方法MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc。
MainActivity.class MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc静态方法:
bash
0 invokestatic #16 <com/tans/myapplication/MainActivity.onCreate$lambda-0 : ()V>
3 return
它又调用了自己的onCreate$lambda-0 方法
MainActivity.class onCreate$lambda-0 方法:
bash
0 getstatic #22 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #24 <Hello, World>
5 invokevirtual #30 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
8 return
直接打印Hello,World到控制台上.
将上面的字节码反编译一下,大概如下:
Java
public final class MainActivity extends AppCompatActivity {
/* access modifiers changed from: protected */
@Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.fragment.app.FragmentActivity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
doSomething($$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc.INSTANCE);
}
/* access modifiers changed from: private */
/* renamed from: onCreate$lambda-0 reason: not valid java name */
public static final void m1onCreate$lambda0() {
System.out.println((Object) "Hello, World");
}
private final void doSomething(TestInterface f) {
f.f();
}
}
Java
public final /* synthetic */ class $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc implements TestInterface {
public static final /* synthetic */ $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc INSTANCE = new $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc();
private /* synthetic */ $$Lambda$MainActivity$wt_38GqJcEewnV1aO0Q7GXbL5Fc() {
}
@Override // com.tans.myapplication.TestInterface
public final void f() {
MainActivity.lambda$wt_38GqJcEewnV1aO0Q7GXbL5Fc();
}
}
5.1.2 Labmda 中有其他引用
如果我将 MainActivity 的代码改为如下:
Kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val time = System.currentTimeMillis()
doSomething {
println("Hello, World, time: $time")
}
}
private fun doSomething(f: TestInterface) {
f.f()
}
}
在Labmda中有引用外部的变量.
字节码:
MainActivity.class onCreate
swift
11 invokestatic #57 <java/lang/System.currentTimeMillis : ()J>
14 lstore_2
15 new #59 <com/tans/myapplication/-$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw>
18 astore 4
20 aload 4
22 lload_2
23 invokespecial #61 <com/tans/myapplication/-$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw.<init> : (J)V>
26 aload_0
27 aload 4
29 invokespecial #65 <com/tans/myapplication/MainActivity.doSomething : (Lcom/tans/myapplication/TestInterface;)V>
和5.1.1中的不同的是com/tans/myapplication/-$$LambdaMainActivityV5EOAdm08AHNszPmNw9kNG0Rknw对象不是单例,而是直接new的一个对象,同时把我们需要的参数传递给该对象的构造函数.
com/tans/myapplication/-$$LambdaMainActivityV5EOAdm08AHNszPmNw9kNG0Rknw对象的f函数:
swift
0 aload_0
1 getfield #14 <com/tans/myapplication/-$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw.f$0 : J>
4 invokestatic #20 <com/tans/myapplication/MainActivity.lambda$V5EOAdm08AHNszPmNw9kNG0Rknw : (J)V>
7 return
同样和5.1.1中的不同的是这里调用,多了一个参数。
其他都大同小异,直接看Labmda中的实现方法.
vbnet
0 lload_0
1 invokestatic #23 <java/lang/Long.valueOf : (J)Ljava/lang/Long;>
4 astore_2
5 ldc #25 <Hello, World, time: >
7 aload_2
8 invokestatic #31 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
11 astore_2
12 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
15 aload_2
16 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
19 return
同样labmda实现的方法中也多了一个参数.
反编译后的代码大致如下:
Java
public final class MainActivity extends AppCompatActivity {
/* access modifiers changed from: protected */
@Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.fragment.app.FragmentActivity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
doSomething(new TestInterface(System.currentTimeMillis()) {
/* class com.tans.myapplication.$$Lambda$MainActivity$V5EOAdm08AHNszPmNw9kNG0Rknw */
public final /* synthetic */ long f$0;
{
this.f$0 = r1;
}
@Override // com.tans.myapplication.TestInterface
public final void f() {
MainActivity.lambda$V5EOAdm08AHNszPmNw9kNG0Rknw(this.f$0);
}
});
}
/* access modifiers changed from: private */
/* renamed from: onCreate$lambda-0 reason: not valid java name */
public static final void m1onCreate$lambda0(long $time) {
System.out.println((Object) Intrinsics.stringPlus("Hello, World, time: ", Long.valueOf($time)));
}
private final void doSomething(TestInterface f) {
f.f();
}
}
Labmda的最终实现会是在当前对象中生成一个静态方法. 如果该labmda内无其他参数的引用,会生成一个单例对象(自动生成,最终实现,会调用到Labmda最终实现的静态方法),反之就会new一个对象(自动生成,最终实现,会调用到Labmda最终实现的静态方法)。 所以,如果我们有意地在labmda中有意地不使用外部引用,某些情况下可以提升性能(少了new,和init函数的调用).
5.2 Java 1.8 处理 Lambda
MainActivity onCreate
bash
11 aload_0
12 invokedynamic #36 <f, BootstrapMethods #0>
17 invokespecial #40 <com/tans/myapplication/MainActivity.doSomething : (Lcom/tans/myapplication/TestInterface;)V>
20 return
我们看到它既没有用单利,也没有new对象,而是调用了invokedynamic指令生成了一个对象,这个就留给读者去解决这个问题吧.