反编译字节码文件 使用到java内置的一个反编译工具 javap 可以反编译字节码文件。 通过 javap -help 可了解javap的基本用法 用法: javap 其中, 可能的选项包括:
-help --help -? 输出此用法消息-version 版本信息-v -verbose 输出附加信息-l 输出行号和本地变量表-public 仅显示公共类和成员-protected 显示受保护的/公共类和成员-package 显示程序包/受保护的/公共 和成员 (默认)-p -private 显示所有类和成员-c对代码进行反汇编-c 对代码进行反汇编 -s 输出内部类型签名-sysinfo 显示正在处理的类的,系统信息 (路径, 大小, 日期, MD5 散列)-constants 显示最终常量-classpath 指定查找用户类文件的位置 -cp 指定查找用户类文件的位置 -bootclasspath 覆盖引导类文件的位置运行 javap -verbose
Classfile /Users/quyixiao/project/spring_tiny/target/classes/com/spring_1_100/test_31_40/test35_resource_inject/MyTest35.class Last modified 2020-10-7; size 550 bytes MD5 checksum 321538c217c561494eb5f1b943ba8bce Compiled from "MyTest35.java" public class com.spring_1_100.test_31_40.test35_resource_inject.MyTest35 minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/spring_1_100/test_31_40/test35_resource_inject/MyTest35.a:I #3 = Class #22 // com/spring_1_100/test_31_40/test35_resource_inject/MyTest35 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest35.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/spring_1_100/test_31_40/test35_resource_inject/MyTest35 #23 = Utf8 java/lang/Object { public com.spring_1_100.test_31_40.test35_resource_inject.MyTest35(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 3: 0 line 4: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 11: 0 line 12: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35; 0 6 1 a I } SourceFile: "MyTest35.java"这里是构造方法:MyTest35(),返回值为void, 公开方法。 code内的主要属性为:
stack 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为2locals: 局部变量所需的存储空间,单位为Slot,Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的。args_size: 方法参数的个数,这里是1,因为每个实例方法都会有一个隐藏参数thisattribute_info方法体内容,0,1,4为字节码"行号",该段代码的意思是将第一个引用类型本地变量推送至栈顶,然后执行该类型的实例方法,也就是常量池存放的第一个变量,也就是注释里的"java/lang/Object."": ()V", 然后执行返回语句,结束方法。LineNumberTable 该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用 -g:none或-g:lines选项来取消或要求生成这项信息,如果选择不生成LineNumberTable,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码 的行数来调试程序。LocalVariableTable 该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或-g:vars来取消或生成 这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。 start表示该局部变量在哪一行开始可见,length表示可见行数,Slot代表所在帧栈位置,Name是变量名称,然后是类型签名。SourceFile 源码文件名称打开class 文件的二进制
CA FE BA BE 00 00 00 33 00 18 0A 00 04 00 14 09 00 03 00 15 07 00 16 07 00 17 01 00 01 61 01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 3D 4C 63 6F 6D 2F 73 70 72 69 6E 67 5F 31 5F 31 30 30 2F 74 65 73 74 5F 33 31 5F 34 30 2F 74 65 73 74 33 35 5F 72 65 73 6F 75 72 63 65 5F 69 6E 6A 65 63 74 2F 4D 79 54 65 73 74 33 35 3B 01 00 04 67 65 74 41 01 00 03 28 29 49 01 00 04 73 65 74 41 01 00 04 28 49 29 56 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0D 4D 79 54 65 73 74 33 35 2E 6A 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00 3B 63 6F 6D 2F 73 70 72 69 6E 67 5F 31 5F 31 30 30 2F 74 65 73 74 5F 33 31 5F 34 30 2F 74 65 73 74 33 35 5F 72 65 73 6F 75 72 63 65 5F 69 6E 6A 65 63 74 2F 4D 79 54 65 73 74 33 35 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00 06 00 00 00 03 00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 03 00 04 00 04 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00 00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 07 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 00 01 00 10 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 0B 00 05 00 0C 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 00 06 00 01 00 01 00 12 00 00 00 02 00 13
使用 javap -verbose 命令分析一个字节码文件时,将会分析该字节码文件魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息。在分析字节码之前 ,我们先来了解一下关于字节码的基本概念。 1.魔数:所有的.class 文件字节码文件的前4个字节都是魔数,魔数的固定值是: 0xCAFEBABE 。 2.版本号:魔数之后的4个字节为版本信息,前两个字节为minor version(次版本号),后两个字节为major version (主版本号),00 00 00 33 换算成10进制为 次版本号为0,主版本号为51 。java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为 1.7.0。 通过 java -version 命令稍加验证, 可得结果。 java version “1.7.0_131” Java™ SE Runtime Environment (build 1.7.0_131-b11) Java HotSpot™ 64-Bit Server VM (build 25.131-b11, mixed mode) 3.常量池(constant pool) : 紧接着主版本号之后的就是常量池,一个 Java类中定义的很多的信息都是由常量池来维护和描述的,可以将常量池看作是 class 文件资源仓库,比如说 Java 类中定义的方法与变量信息,都是存储在常量池中,常量池中主要存储两类常量:字面量和符号引用
字面量:字面量比如文本字符串,Java 中声明的 final 的常量值等。符号引用:符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。字节码文件信息 开头的7行信息包括:Class文件当前所在位置,最后修改时间,文件大小,MD5值,编译自哪个文件,类的全限定名,jdk次版本号,主版本 号。 然后紧接着的是该类的访问标志:ACC_PUBLIC, ACC_SUPER,访问标志的含义如下:
标志名称标志值含义修饰对象ACC_PUBLIC0x0001是否为Public类型class, field, methodACC_PRIVATE0x0002是否为private类型class, field, methodACC_PROTECTED0x0004是否为protected类型class, field, methodACC_STATIC0x0008static 类型field, methodACC_FINAL0x0010是否被声明为final,只有类可以设置class, field, method, parameterACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义.classACC_SYNCHRONIZED0x0020synchronized类型methodACC_VOLATILE0x0040volatile类型fieldACC_BRIDGE0x0040bridge类型methodACC_VARARGS0x0080varargs类型methodACC_TRANSIENT0x0080transient类型fieldACC_NATIVE0x0100native 类型methodACC_INTERFACE0x0200标志这是一个接口classACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说, 次标志值为真,其他类型为假class, methodACC_STRICT0x0800strict类型methodACC_SYNTHETIC0x1000标志这个类并非由用户代码产生class, field, method, parameterACC_ANNOTATION0x2000标志这是一个注解classACC_ENUM0x4000标志这是一个枚举class(?) field innerACC_MANDATED0x8000mandated类型parameter4.常量池的总体结构:Java类所对应的常量池主要由常量池数量与常量池数组这两部分共同构成。常量池数量紧跟在主版本号后面,占据两个字节,常量池数组则紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型,结构都是不同的,长度当然也就不同,但是,每一种元素的第一个数据都是一个 u1类型,该字节是个标志位,占据1个字节,JVM在解析常量池时,会根据这个 u1类型来获取元素的具体类型,值得注意的是常量池数组中元素的个数=常量池数-1(其中0暂时不使用)。目的是满足某些常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】 的含义,根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应 null 值,所以常量池的索引 从1而非0开始。
Class 文件结构中常量池中11种数据类型的结构总表常量项目类型描述 CONSTANT_Utf8_infotagU1值为1lengthU2UTF-8编码字符串长度bytesU(length)长度为 length 的 UTF-8编码的字符串CONSTANT_Integer_infotagU1值为3bytesU4按照高位在前存储 int 值CONSTANT_Float_infotagU1值为4bytesU4按照高位在前存储 float 值CONSTANT_Long_infotagU1值为5bytesU8按照高位在前存储 long 值CONSTANT_Double_infotagU1值为6bytesU8按照高位在前存储 double 值CONSTANT_Class_infotagU1值为7indexU2指向全限定名常量项的索引CONSTANT_String_infotagU1值为8indexU2指向字符串字面量索引CONSTANT_FieIdref_infotagU1值为9indexU2指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项indexU2指向字段描述符 CONSTANT_NameAndType_Info 的索引项CONSTANT_Methodref_infotagU1值为10indexU2指向声明方法的类描述符CONSTANT_Class_info的索引项indexU2指向名称及类型描述符 CONSTANT_NameAndType_Info 的索引项CONSTANT_InterfaceMethodref_infotagU1值为11indexU2指向声明方法的类描述符CONSTANT_Class_info的索引项indexU2指向名称及类型描述符 CONSTANT_NameAndType_Info 的索引项CONSTANT_NameAndType_infotagU1值为12indexU2指向该字段或方法名称常量项索引indexU2指向该字段或方法描述符常量项的索引在 JVM 规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量,类型与顺序) 与返回值,根据描述符规则,基本数据类型和代表无返回值的 void 类型都用一个大写的字符来表示的,对象类型则使用字符 L加对象的全限定名称来表示,为了压缩字节码文件的体积。对于基本的数据类型,JVM 都只使用一个大写的字母来表示,如下所示:
标识字符含义B基本类型 byteC基本类型 charD基本类型 doubleF基本类型 floatI基本类型 intJ基本类型 longS基本类型 shortZ基本类型 booleanV特殊类型 voidL对象类型,以分号结尾,如 java/lang/Object;对于数组类型,每一位使用一个前置的"[“字符来描述,如定义一个 java.lang.String[][]类型的二维数组,将被记录为”[[Ljava/lang/String;" 5.用描述符来描述方法时,按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内,如方法:String getRealnamelyIdAndNickname(int id ,String name ) 的描述符为:(I,Ljava/lang/String;)Ljava/lang/String;
对于上面字节码,魔数和版本号后面是常量池。对于常量池,我们一个一个来分析。 在Class 文件结构中常量池中11种数据类型的结构总表中,u1表示占用一个字节,u2表示占用两个字节。在分析之前我们先来了解一下 Java 字节码整体结构 :
Magic Number4个字节魔数,值为0xCAFEBABE,Java创始人 James Gosling 制定Version2+2 个字节包括 minor_version 和 major_version,minor_version:1.1(45),1.2(46),1.3(47),1.4(48),1.5(49),1.6(50),1.7(51)Constant Pool2+n个字节包括字符串常量,数值常量等Access Flags2 个字节访问标志,java 中一个类的的访问标志是多种多样的,比如 public ,static 等This Class Name2个字节当前这个类的名字是什么Super Class Name2个字节父类名字Interfaces2 + n 个字节java 类可以多继承,因此,继承一个接口,消耗2个字节Fields2 + n 个字节描述了关系类的属性的信息Methods2 + n 个字节描述了一些方法的信息Attributes2 + n 个字节一些编译器在编译时可以加入一些 Attritutes 信息,方便编译时使用整体所占有的字节
类型名称数量u4magic(魔数)1u2minor_version(次版本号)1u2major_version(主版本号)1u2constant_pool_count(常量个数 )1cp_infoconstant_pool(常量池表)constant_pool_count -1u2access_flags(类访问控制权限)1u2this_class(类名)1u2super_class(父类名)1u2interfaces_count(接口个数)1u2interfaces(接口名)interfaces_countu2fields_count(域个数)1field_infofields(域的表)fields_countu2methods_count(方法的个数)1method_infomethod(方法表)methods_countu2attributes_count(附加属性的个数)1attribute_infoattributes(附加属性的表)attributes_count类字节码文件完整结构 : ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } Class 字节码中有两种数据类型
字节数据直接量:这是基本的数据类型,其细分为u1,u2,u4,u8四种,分别代表连续的1个字节,2个字节,4个字节,8个字节组成的整体数据 。表(数组):表是由多个基本数据或其他表,按照既定的顺序组成的大的数据集合,表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是己经严格定义好的。下面来一一分析字节码,【字节码都是16进制数,这里需要我们手动转化为10进制数】:
CA FE BA BE : 魔数
00 00 00 33 :次版本号(minor version: 0) ,主版本号 (major version: 51)
00 18 : 10进制数是 24,表示常量池的数量,常量池数组中元素的个数=常量池数-1(其中0暂时不使用)。目的是满足某些常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】 的含义,根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应 null 值,所以常量池的索引 从1而非0开始。
0A 00 04 00 14 : 0A 10进制数是10,在【Class 文件结构中常量池中11种数据类型的结构总表】中对应的是CONSTANT_Methodref_info,00 04 (占两个字节)指向声明方法的类描述符CONSTANT_Class_info的索引项,从下图中可以看出,#4 指向了#23 ,而#23对应的是 java.lang.Object 类型。 00 14 (占两个字节,10进制是20)指向名称及类型描述符 CONSTANT_NameAndType_Info 的索引项,#20索引项指向了#7和#8, #7表示构造函数,#8表示void 类型,因此。常量池中的第一个常量方法引用表示的是:
#1 = Methodref #4.#20 // java/lang/Object."": ()V :表示构造方法,()V 表示 void 类型,表示的是定义一个 void 类型的构造方法,并且父类为 java.lang.Object
09 00 03 00 15:我们继续来解析常量池中的第二个常量,第一位(09)对应的10进制是#9,而9在【Class 文件结构中常量池中11种数据类型的结构总表】对应的是CONSTANT_FieIdref_info,FieIdref后面第一位(index U2) 占两个字节 ,而 00 03 的10进制对应是3 ,因此,指向了 #3,而#3代表常量com/spring_1_100/test_31_40/test35_resource_inject/MyTest35 。 而 00 15 转化为10进制指向了#21,而#21分别指向了#5:#6,而#5表示 a,#6表示 I(int 类型),如下图所示 因此第二个常量的意思就是 (#2 = Fieldref #3.#21 // com/spring_1_100/test_31_40/test35_resource_inject/MyTest35.a:I)在MyTest35中定义一个int 类型的属性 a。
07 00 16 :07在【Class 文件结构中常量池中11种数据类型的结构总表】表示的是CONSTANT_Class_info,而后面占两个字节(index U2)指向全限定名常量项的索引,16的十进制数是22,表示指向常量 #22,而#22表示的是com/spring_1_100/test_31_40/test35_resource_inject/MyTest35。
07 00 17 :07在【Class 文件结构中常量池中11种数据类型的结构总表】表示的是CONSTANT_Class_info,而后面占两个字节(index U2)指向全限定名常量项的索引,17的十进制数是23,表示指向常量 #23,而#23代表 java.lang.Object 。
01 00 01 61 :01在【Class 文件结构中常量池中11种数据类型的结构总表】表示的是CONSTANT_Utf8_info,代表是一个 utf-8的字符编码,UTF-8编码字符串长度占两个字节(length U2)00 01 ,00 01 转化为10进制数为1,因此字符串本身占1个字节,从 00 01 向后数1个字节,得到61 ,61转化为10进制数是97,而97对应的ascii 码是 a, 因此,01 00 01 61 代表的是字符串常量 a,如下图所示:
01 00 01 49 : 01在【Class 文件结构中常量池中11种数据类型的结构总表】表示的是CONSTANT_Utf8_info,代表是一个 utf-8的字符编码,UTF-8编码字符串长度占两个字节(length U2)00 01 ,00 01 转化为10进制数为1,因此字符串本身占1个字节,从 00 01 向后数1个字节,得到49,49对应的 ascii 码是 I,如下图所示:
01 00 06 3C 69 6E 69 74 3E:01在【Class 文件结构中常量池中11种数据类型的结构总表】表示的是CONSTANT_Utf8_info,代表是一个 utf-8的字符编码,UTF-8编码字符串长度占两个字节(length U2)00 06 ,00 06 转化为10进制数为6,因此字符串本身占6个字节,从00 06 向后数6个字节,得到3C 69 6E 69 74 3E 因此上面的组合起来就是<init>
01 00 03 28 29 56 : 略 ,UTF-8编码字符串长度占两个字节(length U2)00 03 ,00 03 转化为10进制数为3,因此字符串本身占3个字节,从00 03 向后数3个字节,得到28 29 56 ,而28 29 56对应的是()V。
01 00 04 43 6F 64 65 :略。00 04 转化为10进制数为4,因此字符串本身占4个字节,从00 04 向后数4个字节,得到43 6F 64 65 ,而43 6F 64 65对应的是 Code
01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 :略。UTF-8编码字符串长度占两个字节(length U2)00 0F ,00 0F 转化为10进制数为15,字符串本身占15个字节,从 00 0F 向后数15个字节,得到4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65,而4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65对应的是 LineNumberTable,(LineNumberTable表示行号表,主要用来记录字节码中的位置和源码的行号对应关系,方便在抛出异常时,打印出行号)。
01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 :略,UTF-8编码字符串长度占两个字节(length U2)00 02,00 12 转化为10进制数为18,因此字符串本身占18字节,从00 12 向后数18个字节,得到4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65,【4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65】对应的字符串是LocalVariableTable(局部变量表)。
01 00 04 74 68 69 73 :略,UTF-8编码字符串长度占两个字节(length U2)00 04 ,00 04 转化为10进制数为4,因此字符串本身占4字节,从 00 04 向后数4个字节,得到74 68 69 73 ,表示 this 字符串。
01 00 3D 4C 63 6F 6D 2F 73 70 72 69 6E 67 5F 31 5F 31 30 30 2F 74 65 73 74 5F 33 31 5F 34 30 2F 74 65 73 74 33 35 5F 72 65 73 6F 75 72 63 65 5F 69 6E 6A 65 63 74 2F 4D 79 54 65 73 74 33 35 3B :略, 00 3D后面的字符串对应的是Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35;
01 00 04 67 65 74 41 :略,00 04 后面对应的是getA。
01 00 03 28 29 49 :略 ()I,方法描述符,传入参数为空,返回值为 Int类型。
01 00 04 73 65 74 41 :setA 方法。
01 00 04 28 49 29 56 :略 (I)V,方法描述符,表示传入一个 In类型的参数,返回值为 void 类型。
01 00 0A 53 6F 75 72 63 65 46 69 6C 65 :略 SourceFile,表示的是源文件。
01 00 0D 4D 79 54 65 73 74 33 35 2E 6A 61 76 61 :SourceFile MyTest35.java,表示当前的字节码是由哪个源文编译出来的。
0C 00 07 00 08 :0C在【Class 文件结构中常量池中11种数据类型的结构总表】表示的是CONSTANT_NameAndType_info,10进制表示12,00 07 (index U2)占两个字节,指向该字段或方法名称常量项索引#7,而#7表示<init>,表示构造方法。00 08 (index U2) 占两个字节,指向该字段或方法描述符常量项的索引#8,而#8,表示()V,返回值为 void 类型的方法。而这个常量的意思就是表示一个 void 类型的构造方法。
0C 00 05 00 06:0C在【Class 文件结构中常量池中11种数据类型的结构总表】表示的是CONSTANT_NameAndType_info,0C 10进制表示12,00 05 (index U2)占两个字节,指向该字段或方法名称常量项索引#5,而#5表示字符串 a, 而00 06(#6) 表示 Int 类型。此常量,描述了一个 int a 。
01 00 3B 63 6F 6D 2F 73 70 72 69 6E 67 5F 31 5F 31 30 30 2F 74 65 73 74 5F 33 31 5F 34 30 2F 74 65 73 74 33 35 5F 72 65 73 6F 75 72 63 65 5F 69 6E 6A 65 63 74 2F 4D 79 54 65 73 74 33 35 :3B 表示59,从00 3B 向后数59个字节,com/spring_1_100/test_31_40/test35_resource_inject/MyTest35,表示这个类的全额限定名。
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 :略 ,java/lang/Object,描述当前类的父类的完全限定名。到这里,常量池己经分析完成。
00 21 :Acess_Flag 访问标志,访问标志信息包括该 Class 文件是类还是接口,是否被定义为 public,是否是 abstract,如果是类,是否被声明成 final,通过上面的源代码,我们知道该文件是类并且是 public 。 类的访问标志符,下面将是类的访问标识符和16进制对应关系。
标志名称标志值含义修饰对象ACC_PUBLIC0x0001是否为Public类型class, field, methodACC_PRIVATE0x0002是否为private类型class, field, methodACC_PROTECTED0x0004是否为protected类型class, field, methodACC_STATIC0x0008static 类型field, methodACC_FINAL0x0010是否被声明为final,只有类可以设置class, field, method, parameterACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义.classACC_SYNCHRONIZED0x0020synchronized类型methodACC_VOLATILE0x0040volatile类型fieldACC_BRIDGE0x0040bridge类型methodACC_VARARGS0x0080varargs类型methodACC_TRANSIENT0x0080transient类型fieldACC_NATIVE0x0100native 类型methodACC_INTERFACE0x0200标志这是一个接口classACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说, 次标志值为真,其他类型为假class, methodACC_STRICT0x0800strict类型methodACC_SYNTHETIC0x1000标志这个类并非由用户代码产生class, field, method, parameterACC_ANNOTATION0x2000标志这是一个注解classACC_ENUM0x4000标志这是一个枚举class(?) field innerACC_MANDATED0x8000mandated类型parameter0x00 21 :是0x0020和0x0001的并集,表示 ACC_PUBLIC 与 ACC_SUPER的合并。
00 03 :表示当前类的类名,指向常量池中的#3 ,而#3对应是com/spring_1_100/test_31_40/test35_resource_inject/MyTest35。00 04 :父类的名字,指向常量池中#4 ,java/lang/Object00 00 :接口数量,当前类没有实现接口。因为接口数为0,后面的接口名就在字节码中无显示 。00 01 :属性个数,类中只声明了一个 int a ,因此属性个数为1。00 02 00 05 00 06 00 00:字段表集合,fields_count:u2 字段表结构 类型名称数量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_countfield_info{ u2 access_flags; 0002 u2 name_index; 0006 u2 descriptor_index; 0006 u2 attributes_count; 0000 attribute_info attributes[attributes_count]; }
00 02:表示属性的访问标识符为ACC_PRIVATE类型
00 05:指向常量池中第5个常量#5,是字符串 a
00 06:属性类型描述,指向常量池中第6个常量#6,I ,表示 int 类型
00 00:属性个数为0,因此attributes在字节码中不体现。 通过上面的分析,我们得到,MyTest35中有一个属性,属性的名称为 a,类型为 int,访问标识符为 private。
00 03 :方法个数,表示 Mytest35中有3个方法,构造方法,get,set 方法。
00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 03 00 04 00 04 00 0B 00 01 00 00 00 0A 00 0C 00 0D 00 00:方法表 methods_count:u2
1.方法表结构
类型名称数量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_countmethod_info{ u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
2.方法的属性结构,方法中每个属性都是一个 attribute_info 结构 ,JVM预定义了部分 attribute,但是编译器自己也可以实现了自己的 attribute 写入 class 文件里,供运行时使用。不同的 attribute 通过attribute_name_index 来区分。 attribute_info{ u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
3.Code结构:Code Attribute 的作用是保存该方法的结构,如所对应的字节码 Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_lenght]; u2 attributes_count; attribute_info attributes[attributes_count]; }
attribute_length:表示 attribute所包含的字节数,不包含 attribute_name_index 和attribute_length 字段 。max_stack:表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。max_locals:表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。code_length:表示该方法所包含的字节码的字节数以及具体的指令码。具体字节码即是该方法被调用时,虚拟机所执行的字节码。exception_table:这里存放的是处理异常的信息。每个exception_table 表项由start_pc,end_pc,handler_pc,catch_type 组成start_pc 和 end_pc :表示在 code数组中的从 start_pc 到 end_pc (包含 start_pc,不包含 end_pc) 的指令抛出的异常会由这个表项来处理。handler_pc:表示处理异常的代码的开始处。 catch_type:表示会被处理的异常类型,它指向常量池里的一个异常类,当 catch_type 为0时,表示处理所有的异常。附加属性
LineNumberTable:这个属性用来表示 code 数组中的字节码和 Java 代码行数之间的关系,这个属性可以用来在调试的时候定位代码执行的行数。 LineNumberTable 的结构录下:
LineNumberTable_attribute{ u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; } }
助记符
aload_0 = 42 (0x2a) : 将索引为0的元素推送到操作栈的栈顶。invokespecial :调用实例方法,后面可以接参数。iconst_<i>: 定义常量。putfield:将栈顶的元素赋值。return : 表示返回。了解到了上面基本概念以后,我们继续来跟进解析方法表结构 : 00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 03 00 04 00 04 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00
00 01:表示方法是一个 public 方法
00 07:表示方法名称索引,指向常量池中的#7, #7是<init>
00 08:表示方法返回值索引,指向常量池中#8,#8指向常量池中()V
00 01:表示方法的属性个数,说明只有一个属性。
00 09:表示方法属性名称索引 ,这里指向#9,表示是一个 Code 类型。Code 表示要执行的代码。
00 00 00 38:Attribute length ,属性长度56。
00 02 :max_stack 为2
00 01 :max_locals 为1
00 00 00 0A :code_length 代码长度10,向后面取10个长度,如下:
2A B7 00 01 2A 04 B5 00 02 B1:如果不懂,可以到 Oracle 官网查看助记符对应关系表。
2A :表示 aload_0 ,将索引为0的元素推送到操作栈栈顶。B7 00 01:B7 表示 invokespecial ,00 01 指向常量池中的#1 ,这里表示的是调用 Object 类的构造方法。2A :表示 aload_0 ,将索引为0的元素推送到操作栈栈顶。04 :表示 iconst_1, 在构造方法将常量压入栈中。B5 00 02 :B5 表示putfield ,后带参数,指向常量池中的#2 ,将栈顶的元素,保存到 field 中。B1 :表示 return ,构造函数没有返回值。00 00 : 异常表,在这里,没有定义 try_catch 信息,因此异常表为0.
00 02 : 表示两个属性。
00 0A:attribute_name_index 指向常量池中的#10,表示行号(LineNumberTable)表。
00 00 00 0A :attribute_length占4个节节(可参照上面行号表结构), 表示属性长度。向后数10个字节
00 02 00 00 00 03 00 04 00 04 :line_number_table_length 表示行号表的实际长度。
00 02 :表示有两对映射00 00 00 03 :start_pc 为0,Line Number 3 ,3行源码对应指令 0: aload_000 04 00 04:start_pc 为4 ,Line Number 4 , 4 行源码对应 aload_0
00 0B: 指向常量池中#11,表示的是局部变量表 (LocalVariableTable)。
00 00 00 0C : 表示局部变量表所点字节个数 。这里是12。
00 01 00 00 00 0A 00 0C 00 0D 00 00 :
00 01 :局部变量的个数为1.00 00 00 0A :局部变量作用范围0-9 ,也就是说在 0: aload_0 1: invokespecial #1 // Method java/lang/Object.""😦)V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return 在上面的范围之内都可以使用局部变量00 0C :局部变量对应常量池中的索引#10,而对应的就是 this,从字节码的角度上来看,对于每一个非 static 方法,都会隐式的传递一个局部变量 this 到这个方法里 。这一点和 python 类似。00 0D :对当前局部变量描述,指向常量池中的#13,而#13指的是当前类Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35; 00 00 :表示的是 exception stack label table ,为000 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 07 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00
00 01 :表示的是method_info的access_flags,占两个字节,0x0001 表示是 public
00 0E:表示的是name_index,名称索引#14,而常量池中的#14表示的是 getA 方法。
00 0F:表示的是descriptor_index,属性描述符,而常量池中的#15,而#15表示的是I,int 类型。
00 01:方法属性个数。
00 09 :指向常量池中#9,表示方法的属性 Code。
00 00 00 2F :attribute_length ,属性长度 47
00 01:max_stack,最大操作数栈个数为1
00 01:max_locals,局部变量最大个数
00 00 00 05 :code_length ,助记符长度。
2A B4 00 02 AC 00 00 :
2A :助记符 aload_0 。B4 00 02 : 助记符 getfield ,从对象获取字段AC :对应的助记符是ireturn, 从方法返回 int00 00 :表示的是exception stack label table00 02 :有两个属性
00 0A : 行号表 ,LineNumberTable
00 00 00 06 :行号表中属性长度attribute_length,占4个字节。
00 01 :行号表的长度1
00 00 00 07 :前两个字节为start_pc,值为0 ,后两个字节为Line Number,值为7,源码中的第7行指向start_pc 为 aload_0
00 0B :指向常量池中#11,而#11表示局部变量表。
00 00 00 0C :局部变量表长度12。
00 01 00 00 00 05 00 0C 00 0D 00 00:
00 01:局部变量表的个数100 00 00 05:start_pc 为0 ,Length为5 ,局部变量表的作用范围为0-4。00 0C :指向常量池#12 ,值为 this。00 0D :指向常量池#13,值为Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35;也就是说,局部变量表 this,是MyTest35对象。00 00 :表示异常堆栈信息为000 01 00 10 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 0B 00 05 00 0C 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 00 06 00 01 00 01 :表示的是method_info的access_flags,占两个字节,0x0001 表示是 public00 10 :表示的是name_index,名称索引#16,而常量池中的#16表示的是 setA 方法。00 11 :表示的是descriptor_index,属性描述符,而常量池中的#17,而#17表示的是(I)V,void 类型。00 01 :方法属性个数100 09 :指向常量池中#9,表示方法的属性 Code。00 00 00 3E :attribute_length ,属性长度6200 02 :max_stack,最大操作数栈个数为200 02 :max_locals,局部变量最大个数为200 00 00 06:code_length ,助记符长度为6。2A 1B B5 00 02 B1 00 00: 2A :助记符 aload_0 。1B :助记符 iload_1B5 00 02 : 助记符 putfield ,从对象获取字段 ,方法参数#2,而#2代表的是#com/spring_1_100/test_31_40/test35_resource_inject/MyTest35.a:I,MyTest35中的 int 类型的属性aB1 :助记符是return,无返回值00 00: 表示无exception_table_length 00 02 :有两个属性00 0A : 行号表 ,LineNumberTable00 00 00 0A :行号表中属性长度attribute_length,占10个字节。00 02 :行号表的长度2
00 00 00 0B:Start_pc 为0 ,LineNumber11 ,源码中11行指向aload_0
00 05 00 0C: Start_pc 为5 ,LineNumber12,源码中的第12行指向 return 。
00 0B :指向常量池中#11,而#11表示局部变量表。
00 00 00 16 :局部变量表长度22。
00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 00 06 00 01 :
00 02:局部变量表的个数200 00 00 06 :start_pc 为0 ,Length为6 ,局部变量表的作用范围为 0-600 0C :指向常量池#12 ,值为 this。00 0D :指向常量池#13,值为Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35;00 00 :exception_table_length 为000 00 00 06 : start_pc 为0 ,Length为6,局部变量表的作用范围为 0-600 05 :指向常量池#5 ,值为 a。00 06 : 指向常量池#6,值为 I,Int 类型。00 01 :这个,我也不知道。 00 01 00 12 00 00 00 02 00 13: 00 01 :属性个数为1个00 12 :指向常量池中#18,而18对应的源文件SourceFile。00 00 00 02:源文件占2个字节,向后数两个字节。00 13:指向常量池中#13,而13对应的是Lcom/spring_1_100/test_31_40/test35_resource_inject/MyTest35;类。到这里,我们终于完成了一个简单的字节码解析。 下面我们来对 Class 结构总结一下。
先是魔数解析magic主版本号次版本号访问标志位常量池的解析从字节码中获取当前类从字节码中获取父类从字节码中获取接口属性获取方法提取编译器植入attribute当前字节码的源文件名 到这里,己经将一个类全部解析完成,但是在解析之前,一定要对 JVM规范中的基础知识了解,方法表结构,属性结构,行号表结构等,结构中有几个属性,每个属性占几个字节,等都要有一定印象,才去后面字节码的解析,不然,你会觉得云里雾里。不知所以。不过还要耐心,一个字节一个字节的去读取,也许你会花一天,或者多天来解析字节码,但是,只要你解析过一遍以后,将是你学习 java 中宝贵的财富。 个人总结,还是希望读者自己去尝试一下,不然,看了也没有什么用。本文的 github 地址是https://github.com/quyixiao/spring_tiny/blob/master/src/main/java/com/spring_1_100/test_31_40/test35_resource_inject/MyTest35.java