发表时间:2022-03-25来源:网络
Java最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。使用字节码可以玩出很多高级的玩法,最高级的还是在 Java 程序运行时进行字节码修改和代码注入。听起来是不是一些很黑客,也很黑科技的事?是的,这个方式使用 Java 这门静态语言在运行时可以进行各种动态的代码修改,而且可以进行无侵入的编程。
比如,我们不需要在代码中埋点做统计或监控,可以使用这种技术把我们的监控代码直接以字节码的方式注入到别人的代码中,从而实现对实际程序运行情况进行统计和监控。但是要做到这个事,还需要学习一个叫 Java Agent 的技术(可以参考我的这篇文章:Java Agent)。
根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表,它由下表所示的数据项构成。
为方便讲解,在这里准备了一段最简单的代码,也希望大家能跟着实际操作一遍:
package org.clazz; public class TestClazz { private int m; public int inc() { return m + 1; } }使用 javac 将这个文件转换成 Class,然后用十六进制编辑器 WinHex 打开这个 Class 文件:
有了以上的知识准备,现在我们一起分析上面的 Class 分别代表什么意思。揭开这层神秘的面纱!
每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。
很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。
文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。
我们看到 TestClazz.class 的魔数也就是头 4 个字节为 CA FE BA BE,用十六进制表示是 0xCAFEBABE(咖啡宝贝?这个名称也太浪漫了吧)。这也意味着每个 Class 文件的魔数值都必须为 0xCAFEBABE。
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:5-6 个字节是次版本号(Minor Version),7-8 个字节是主版本号(Major Version)。Java 的版本号是从 45 开始的,JDK 1.1 之后的每个JDK 大版本发布主版本号加 1,高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过起把那本号的 Class 文件。
Class 文件版本号:
再看看文件对应的值:
我们看到代表主版本号的 7-8 个字节的值为 0x0034,也即十进制的 52,该版本号说明这个文件是可以被 JDK 1.8 或以上版本虚拟机执行的 Class 文件。
紧接着主次版本号之后的是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,它是占用 Class 文件空间最大的数据项目之一。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。
如上图所示,常量池容量为十六进制数 0x0016,即十进制的 19,结合上面的 Class 表,我们能知道常量池中有 19 - 1 = 18 项常量。
常量池容量计数值之后就是常量池,常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。
符号引用则属于编译原理方面的概念,包括了下面三类常量:
常量池中每一项常量都是一个表,总共 14 种表:
类型标志描述CONSTANT_Utf8_info1UTF-8 编码的字符串CONSTANT_Integer_info3整型字面量CONSTANT_Float_info4浮点型字面量CONSTANT_Long5长整型字面量CONSTANT_Double_info6双精度浮点型字面量CONSTANT_Class_info7类或接口的符号引用CONSTANT_String_info8字符串类型字面量CONSTANT_Fieldref_info9字段的符号引用CONSTANT_Methidref_info10类中方法的符号引用CONSTANT_InterfaceMethodref_info11接口中方法的符号引用CONSTANT_NameAndType_info12字段或方法的部分符号引用CONSTANT_MethodHandle_info15表示方法句柄CONSTANT_MethodType_info16标识方法类型CONSTANT_InvokeDynamic_info18标识一个动态方法调用点之所以说常量池是最烦琐的数据,是因为这 14 中常量类型各自均有自己的结构。
我们再来看图中常量池的第一项常量,它的标志位(偏移地hi:0x0000000A)是 0x0A,转换为十进制的值为 10,查常量表中对应的标志为 10 的常量属于 CONSTANT_Methodref_info 类型。
我们看一下 CONSTANT_Methodref_info 类型常量的结构:
上图中的第一个 index 十六进制为 0x0004,即十进制的 4,表示指向常量池中第 4 个常量。
第二个 index 十六进制为 0x000F,即十进制的 15,表示指向常量吃中的第 15 个常量。
(先不管第4、15 常量表示什么)
上面分析的是第一个常量值,接着分析第二个常量值,它的标志位(地址:0x0000000F)是 0x09,即十进制的 9,表示这个常量属于 CONSTANT_Fieldref_info 类型,此常量代表字段的符号引用。
CONSTANT_Fieldref_info 型常量的结构:
以上分析了 TestClazz.class 常量池中 18 个常量中的前两个,其余的 16 个依次类推:
需要注意的是第 18 个常量,tag 标志为 0x01 表示 CONSTANT_Utf8_info :
注意 bytes 字段的长度,是根据 length 计算的,length 为 0x0010 转换十进制为 16,所以后面的 bytes 占用 16 个字节。
最后将 14 中常量项的结构定义总结为下表,供大家参考:
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。具体标志位以及标志的含义见下表:
标志名称标志值含义ACC_PUBLIC0x0001是否为 public 类型ACC_FINAL0x0010是否被声明为 final,只有类可设置ACC_SUPER0x0020是否允许使用 invokespecial 字节码指令的新语意,invokespecial指令的语意在 JDK 1.0.2 发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2 之后编译出来的类的这个标志都必须为真ACC_INTERFACE0x0200标识这是一个接口ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其他类值为假ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的ACC_ANNOTATION0x2000标识这是一个注解ACC_ENUM0x4000标识这是一个枚举
TestClazz.class 是一个普通的 java 类,不是接口、枚举,因此它的ACC_PUBLIC、ACC_SUPER标志为真,其他标志为假,因此它的 access_flags 的值为:0x0001|0x0020 = 0x0021。
类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引结合(interfaces)是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。
类索引和父类索引都是指向一个类型为 CONSTANT_Class_info 的类描述符常量。
图中看到,TestClazz中的类索引指向的是第 3 个常量,父类索引指向的是第 4 个常量。
对于接口索引集合,入口的第一项——u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为 0 ,后面接口的索引表不再占用任何字节。
字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
字段表结构:
字段修饰符放在 access_flags 项目中,它与类中的 access_flags 项目是非常类似的,都是一个 u2 的数据类型,其中可以设置的标志位和含义见表:
标志名称标志值含义ACC_PUBLIC0x0001字段是否为 publicACC_PRIVATE0x0002字段是否为 privateACC_PROTECTED0x0004字段是否为 proctectedACC_STATIC0x0008字段是否为 staticACC_FINAL0x0010字段是否为 finalACC_VOLATILE0x0040字段是否为 volatileACC_TRANSIENT0x0080字段是否为 transientACC_SYNTHETIC0x1000字段是否由编译器自动产生的ACC_ENUM0x4000字段是否为 enum跟随 access_flags 标志的是两项索引值:name_index 和 descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
方法表的内容和字段表几乎采用了完全一致的方式,方法表结构:
类型名称数量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_countaccess_flags 方法访问标志:
标志名称标志值含义ACC_PUBLIC0x0001方法是否为 publicACC_PRIVATE0x0002方法是否为 privateACC_PROTECTED0x0004方法是否为 proctectedACC_STATIC0x0008方法是否为 staticACC_FINAL0x0010方法是否为 finalACC_SYNCHRONIZED0x0020方法是否为 synchronizedACC_BRIDGE0x0040方法是否是由编译器产生的桥接方法ACC_VARARGS0x0080方法是否接受不定参数ACC_NATIVE0x0100方法是否为 nativeACC_ABSTRACT0x0400方法是否为 abstractACC_STRICTFP0x0800方法是否为 strictfpACC_SYNTHETIC0x1000方法是否由编译器自动产生的TestClazz对应的位置:
注意,方法表集合只存放了方法名称,索引等,方法里的代码存放在方法属性表集合中一个名为“Code”的属性里面,这就是下面需要将到的属性表集合。
属性表(attribute_info)在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
虚拟机规范预定义的属性:
对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可,一个符合规则的属性表应该满足下表所定义的结构。
属性表结构:
Java 程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。
Code 属性表结构:
Code 属性是 Class 文件中最重要的一个属性,如果把一个 Java 程序中的信息分为代码(java代码)和元数据(类、字段、方法定义及其他信息)两部分,那么在整个 Class 文件中,Code 属性用于描述代码,所有其他数据项目都用于描述元数据。
继续以 TestClazz.class 文件为例
它的操作数栈的最大深度和本地变量表的容量都为 0x0001,字节码区域所占空间的长度为 0x0005。
虚拟机读取到字节码长度后,按照顺序依次读入紧随的 5 个字节,并根据字节码指令表翻译出所对应的字节码指令。
翻译 “2A B7 00 0A B1” 的过程:
1.读入 2A,查表得 0x2A 对应得指令为 aload_0,这个指令得含义是将第 0 个 Slot 中为 reference 类型得本地变量推送到操作数栈顶。
2.读入 B7,查表得 0xB7 对应得指令为 invokespecial,这条指令的作用是以栈顶的 reference 类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者它的父类的方法。
3.读入 00 0A,这是 invokespecial 的参数,查常量池得 0x000A 对应的常量为实例构造器“”方法的符号引用。
4.读入 B1,查表得 0xB1 对应得指令为 return,含义是返回此方法,这条指令执行后,当前方法结束。
属性表集合除了 Code 属性,还有 Exceptions 属性、LineNumberTable 属性等等,这里就不一一介绍了。有兴趣得童鞋可以自行了解。
在 JDK 的 bin 目录中,Oracle 公司已经为我们准备好一个专门用于分析 Class 文件字节码的工具:javap
使用命令:
代码清单:
Last modified 2019-1-14; size 285 bytes MD5 checksum c434da45f0fff84f21348a725448f2f5 Compiled from "TestClazz.java" public class org.clazz.TestClazz minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."":()V #2 = Fieldref #3.#16 // org/clazz/TestClazz.m:I #3 = Class #17 // org/clazz/TestClazz #4 = Class #18 // java/lang/Object #5 = Utf8 m #6 = Utf8 I #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 inc #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 TestClazz.java #15 = NameAndType #7:#8 // "":()V #16 = NameAndType #5:#6 // m:I #17 = Utf8 org/clazz/TestClazz #18 = Utf8 java/lang/Object { public org.clazz.TestClazz(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 3: 0 public int inc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 7: 0 } SourceFile: "TestClazz.java"到此,相信大家能对字节码有一个较深的认识,Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更强大。






上一篇:java中字节是什么意思
下一篇:java的工作机制是什么意思
皓盘云建最新版下载v9.0 安卓版
53.38MB |商务办公
ris云客移动销售系统最新版下载v1.1.25 安卓手机版
42.71M |商务办公
粤语翻译帮app下载v1.1.1 安卓版
60.01MB |生活服务
人生笔记app官方版下载v1.19.4 安卓版
125.88MB |系统工具
萝卜笔记app下载v1.1.6 安卓版
46.29MB |生活服务
贯联商户端app下载v6.1.8 安卓版
12.54MB |商务办公
jotmo笔记app下载v2.30.0 安卓版
50.06MB |系统工具
鑫钜出行共享汽车app下载v1.5.2
44.7M |生活服务
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-02-15
2022-02-14