Java

吃透JVM篇(2)-class字节码里都是啥

2022-02-24  本文已影响0人  bridegg

目录

吃透JVM篇(1)-JVM包含什么,如何运行的码
吃透JVM篇(2)-class字节码里都是啥
吃透JVM篇(3)-jvm的classLoader和android的classLoadder

这篇文章为啥属于JVM?

因为只有理解了classLoader拿了一坨啥东西,才能理解这坨东西怎么在jvm中存储使用的

这篇文章能学会啥?

1基础阅读字节码的能力

2使用神器classpy

3code里面大体都有哪些东西

为啥android的工程师要看这个?

还不是因为卷?想会插装,必定要会ASM,会ASM必须知道啥原理,想知道原理就得看class字节码都有啥

字节码里都有啥?

来吧~准备做实验,看看都是啥
编写一个啥也没有的java类保存并编译,并且查看


java类代码
编译成class并查看

然后你得到一坨乱码


乱码
不要慌,因为打开方式不正确,我们通过vim命令将展示变成16进制展示
代码如下 “:%!xxd”
看到这坨代码
16进制

明显看到class分为三大块,做成一大坨0是行号,右侧一大坨..是类似说明的东西,第一次的乱码就是展示的这玩意,真正的内容在中间每四个一位的16进制串

看到这里不禁会问:这玩意是啥?

这就是jvm可以理解的语言

那人可以读懂不?

既然jvm是人发明的,那jvm懂的东西,人也能读懂,现在可以把这坨东西理解成战争片里的加密电报,我们需要一份解密的密码表才能读懂。

寄出多啦A梦,掏出密码表

密码表
类型 名称 数量
u4 magic 魔数 tag 判断是否是class文件 1
u2 minor_version java副版本号 1
u2 major_version java主版本号 1
u2 constant_pool_count 常量池数量(后面有几个常量) 1
cp_info constant_pool 常量池 constant_pool_count - 1
u2 access_flags 访问权限 1
u2 this_class 该类在常量池索引 1
u2 super_class 父类在常量池索引 1
u2 interfaces_count 实现接口列表的数量 1
u2 interfaces 接口索引 interfaces_count
u2 fields_count 成员列表数量 1
field_info fields 成员详细信息 fields_count
u2 methods_count 方法数量 1
method_info methods 方法信息 methods_count
u2 attributes_count class属性数量 1
attribute_info attributes class的属性信息 attributes_count

这个表其实已经烂大街了,但是还是需要列举下,class的code就是按照上表的顺序一一通过16进制排列的。
首先可以看到类型里有u2,u4,各种_info
其中u1、u2..这种类型叫做无符号数,字节长度固定为u后面的数字
已info结尾的叫,一个表是由多个无符号数组成的,字节长度是由实际表中的数量来确定的

知道上面的知识我们对刚刚的代码做一个分析,如下图

前四位分析
一个字节由两个十六进制位组成,所以magic 的u4类型为8个十六进制位,对应:cafe babe(老梗了,java的咖啡梗)
minor_version u2,对应0000,转化十进制0
major_version u2,对应0034,转化十进制52(糟了,和我学习的文章撞车了。。都是java8,一会改个java11试试)
常量池里是000d,转成十进制 13个
后面的一坨暂时不能解析了,因为涉及到cp_info表的内容了,你可以理解为在战争片里后面的内容涉及到更高的机密,需要长官手里的密码表才能解密。那就去找下cp _info的内容。
解密cp_info前在网上找到了一张图,感觉非常清晰,这里附上文章链接
cp_info区域组成

这里需要解释为什么是count-1个cp_info ,因为0位需要表示没有常量引用,所以下标直接从1开始,所以数量就是count-1(就是生活中的第几个第几个,如果没有就0个)

常量名称 包含类型 长度类型 说明
CONSTANT_Utf8_info tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 长度为length的UTF-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照高位在前存储的int值
CONSTANT_Float_info tag u1 值为4
bytes u4 按照高位在前存储的float值
CONSTANT_Long_info tag u1 值为5
bytes u8 按照高位在前存储的long值
CONSTANT_Double_info tag u1 值为6
bytes u8 按照高位在前存储的double值
CONSTANT_Class_info tag u1 值为7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 值为8
index u2 指向字符串字面量的索引
CONSTANT_Fieldref_info tag u1 值为9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
Index u2 指向字段描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_info tag u1 值为10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_InterfaceMethodref_info tag u1 值为11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_NameAndType_info tag u1 值为12
index u2 指向该字段或方法名称常量项的索引
index u2 指向该字段或方法描述的索引
CONSTANT_MethodHandle_info tag u1 值为15
reference_kind u2 值必须在[1,9]之间,它决定了方法句柄的类型,方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 值必须是对常量池的有效引用
CONSTANT_MethodType_info tag u1 值为16
descriptor_index u2 值必须是对常量池的有效引用,常量池在索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_InvokeDynamic_info tag u1 值为18
bootstrap_method_attr_index u2 值必须是当前Class文件中引导方法表的bootstrap methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该处的索引必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

这么长一个表格,我粘都粘了很久,估计看就更没心情了,拿整个简单版,如下

常量类型 长度类型 tag十进制值
uft-8 u1 1
Integer u1 3
Float u1 4
Long u1 5
Double u1 6
Class u1 7
String u1 8
Field u1 9
Method u1 10
InterfaceMethod u1 11
NameAndType u1 12
MethodHandle(句柄类型) u1 15
MethodType u1 16
InvokeDynamic u1 18

现在我们按照这个常量表来查上面的一坨代码,首先知道目前有13个常量,先看tag


tag

0a对应10参考上面的表是Method,对应上面表的上面的表是这个

CONSTANT_Methodref_info tag u1 值为10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项

说明这个cp_info长度为u1+u2+u2=5byte,所以 这个cp表的完整数据就是 tag:0a, classInfo索引:00 03(这里0003指的是第三个cp_info),nameAndType索引:00 0a 对应的是第十个cp_info,后面用classpy会更加直观

剩下的访问权限,索引等等,方法同cp_info相同,需要提前知道本文第一个密码表(其实是结构表,别被我的文字带歪了),所有的对应结构,就可以阅读十六进制字节码了。

到此你已经具备基本的阅读class字节码的能力了,但是!我们是人类,需要学会使用工具,所以在这里我要推荐阅读class字节码的神器classpy

classpy的使用方式

brew

Mac 的同学 classpy提供了简便的方法,通过

brew tap guxingke/repo && brew install classpy

直接安装

下载代码编译

下载的时候需要确定自己的java版本,如果是8到java8的分支下载,目前首页是15

java15的编译方式,下载代码,进入命令行进入目录,输入如下代码编译

./gradlew fatJar

java 8分支用以下命令编译

./gradlew uberjar

编译成功后使用,下面代码运行

./gradlew run

我们现在把一开始生成的class文件加载到工具里,可以看到一个表结构非常清晰的class文件解析


工具类

通过工具我们可以看到,一个空类包含的常量有12个,大多都是utf-8(字符常量),但是也包含两个class,和一个nameAndType


image.png

其中因为Java万物皆object,所以所有的类型均继承自Object,所以每个类文件当然会包含一个Object类的索引(#03)
当前类Demo也会创建出来一个索引,供别人使用(#02)

那么03和02里又存的什么?
看下面的图


类中保存的限定名称

我们发现其实类里保存的是当前类的限定名称,
同理下面两个是一一样的道理


Object的限定名称
nameAndType

java还规定了如果没有自定义构造函数,就会默认创建一个空构造函数,而(10#)这个方法名称常量索引应该就是空构造函数初始化的方法。
最后关注下#5 发现有索引指向这个字符,别的字符都有一定的意义,这个字符什么鬼,只有一个V?
其实这个v代表的是void,这里有份表,将各个单个字符的意思做了说明


字符说明表

按照工具继续往下看

权限

从这里我们可以看出access_flag包含两个属性 public和super,其中public大家都了解,super代表的是invokespecial,其实就是执行父类方法时的一个特殊的执行命令,通过super就可以拿到父类方法。就是平时用的super关键字。

再看class相关

class

这里的十六进制对应的是常量地址#02,#03,所以这里指向的是常量对象Object和Demo,从这里也可以看出我们写的空类继承自Object。

剩下的由于没写,所以都为空,最后重点看下methods


methods

此处看到methods里的索引也是指向#0,而常量池开始位置为#1,所以当前存储的位置并没有指向常量池,但是里面的名字和方法描述是指向常量,所以这块索引到底属于哪?求大神解答。最后还有个attrbutes表,里面内容如下


attrbutes
其实这里code中的各个指令,将来就是运行时区的虚拟栈中的栈帧
指令都是干啥的这里暂时不展开研究,到分析jvm运行时应该还会碰到,到时候展开研究

以上就是javac编译完的class文件包含的内容,其实这篇文章的第一个表格就是class字节码里存储的内容。
最后附上java class 官方文档,里面的表格要啥有啥

最后引申android的Dex文件和oat文件

在上篇文章已经说过android的jvm是DVM和ART,对应执行的文件是dex文件,dex和class本质上是相同的,区别就是将大部分class整合,将原本的虚拟栈区域替换成了寄存器。那dex包含哪些东西呢?

dex

看下面的表

名称 格式 说明
header header_item 标头
string_ids string_id_item[] 字符串标识符列表。这些是此文件使用的所有字符串的标识符,用于内部命名(例如类型描述符)或用作代码引用的常量对象。此列表必须使用 UTF-16 代码点值按字符串内容进行排序(不采用语言区域敏感方式),且不得包含任何重复条目。
type_ids type_id_item[] 类型标识符列表。这些是此文件引用的所有类型(类、数组或原始类型)的标识符(无论文件中是否已定义)。此列表必须按 string_id 索引进行排序,且不得包含任何重复条目。
proto_ids proto_id_item[] 方法原型标识符列表。这些是此文件引用的所有原型的标识符。此列表必须按返回类型(按 type_id 索引排序)主要顺序进行排序,然后按参数列表(按 type_id 索引排序的各个参数,采用字典排序方法)进行排序。该列表不得包含任何重复条目。
field_ids field_id_item[] 字段标识符列表。这些是此文件引用的所有字段的标识符(无论文件中是否已定义)。此列表必须进行排序,其中定义类型(按 type_id 索引排序)是主要顺序,字段名称(按 string_id 索引排序)是中间顺序,而类型(按 type_id 索引排序)是次要顺序。该列表不得包含任何重复条目。
method_ids method_id_item[] 方法标识符列表。这些是此文件引用的所有方法的标识符(无论文件中是否已定义)。此列表必须进行排序,其中定义类型(按 type_id 索引排序)是主要顺序,方法名称(按 string_id 索引排序)是中间顺序,而方法原型(按 proto_id 索引排序)是次要顺序。该列表不得包含任何重复条目。
class_defs class_def_item[] 类定义列表。这些类必须进行排序,以便所指定类的超类和已实现的接口比引用类更早出现在该列表中。此外,对于在该列表中多次出现的同名类,其定义是无效的。
call_site_ids call_site_id_item[] 调用站点标识符列表。这些是此文件引用的所有调用站点的标识符(无论文件中是否已定义)。此列表必须按 call_site_off 以升序进行排序。
method_handles method_handle_item[] 方法句柄列表。此文件引用的所有方法句柄的列表(无论文件中是否已定义)。此列表未进行排序,而且可能包含将在逻辑上对应于不同方法句柄实例的重复项。
data ubyte[] 数据区,包含上面所列表格的所有支持数据。不同的项有不同的对齐要求;如有必要,则在每个项之前插入填充字节,以实现所需的对齐效果。
link_data ubyte[] 静态链接文件中使用的数据。本文档尚未指定本区段中 数据的格式。此区段在未链接文件中为空,而运行时实现可能会在适当的情况下使用这些数据。

附上dex文件格式的官方链接

dex和oat有什么不同

之前文章说过在dvm和art中所有的程序执行时需要通过解释器或者jit去翻译真实的机器语言,而oat文件是直接将机器语言保存起来,当需要直接加载已经编译成机器语言的oat文件,官方由张图片,大家感受下


oat和dex执行流程

class和dex搞明白了,就要开始研究加载的流程了,下篇文章主要来了解classLoader相关的内容

上一篇下一篇

猜你喜欢

热点阅读