吃透JVM篇(2)-class字节码里都是啥
目录
吃透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相关的内容