软妹手把手教你javap反编译分解代码,授人以鱼不如授人以渔
我之前写了一篇关于class文件重要性的 , 并且从宏观角度解释了下class文件的构成 , 文章直通车(www.juejin.im/post/684490…)
这篇我们就深入的了解一下class文件的各项内容 , 先看看字节码的样子 。

文章图片

文章图片
以下对字节码的分析 , 就以这个简单的例子为主 。 所有的字节码都是cafebabe开头 , java一直给咖啡代言 , 可说是咖啡的忠实粉丝了 , 就像我爱大幂幂一样 , 撒花~

文章图片
class文件可真是个小机灵鬼 , 正是class文件+JVM组合 , 各种语言编写的代码只要能编译成JVM可以正确识别的class文件 , 就可以运行在JVM上面 , 才使得JAVA语言乃至所有可以运行在JVM上的语言实现了平台无关性 , JVM更是可以向语言无关性发展 , class文件的使命就是教JVM怎么运行 , 运行什么 。
class文件是一组以8位字节为基础单位的十六进制流 , 中间没有任何分隔符 , 细细品这句话 。 正是因为class文件是流式的 , 中间没有任何分隔符所以class文件里面的数据项在顺序和数量上面是严格限定的 , 每个字节的含义 , 长度 , 先后顺序 , 都不允许改变 , 因为JVM靠的就是上面所说的长度 , 先后顺序等这些信息来翻译class文件 , 哪些内容是一组信息哪些符号是另外一组信息 , 清楚了这一点 , 我们再来看class文件的设计就会更加的清晰啦~ 。
class文件采用类似于C语言结构体的伪结构体来存储数据 , class文件是包含了虚拟机指令 , 符号表以及其他辅助信息这三大内容 , 还是这张表 , 包含了任意class文件的所有内容 。

文章图片
简单介绍一下class文件结构的这张表内容:
两种数据类型:无符号数和表
无符号数属于基本数据类型(Java类中也有基本数据类型) , 以u1,u2,u4,u8这种来代表1个字节 , 2个字节,4个字节 , 8个字节的无符号数 , 可以用来描述数字 , 索引引用 , 数量值或者字符串值;表就跟Java类中的对象引用类型一样 , 对象属性可以是基本数据类型(对应U1,U2无符号数) , 也可以是其他的对象(对应其他的表),Java工程项目中参数实体通常以"_Param"结尾(class文件的表都习惯以“_info”结尾);上图中的顺序 , 就是Class文件严格要求的顺序;各个计数器主要是用来描述表里面数据个数 , 例如方法计数器的值是methods_count,代表方法表method——info里面有“methods_count”个方法;JAVA代码千变万化 , 然而所有的内容却都归纳在了区区一张表里面?弄不懂这张表 , 誓不当程序员!!!但愿不会啪啪打脸 。

文章图片
每个Class文件的头4个字节成为魔数(MagicNumber),它唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件 。 值为:0xCAFEBABE(咖啡宝贝)

文章图片
紧接魔数的4个字节是Class文件的版本号:第5-6字节是次版本号(MinorVersion) , 第7-8字节是主版本号(MajorVersion)

文章图片
J2SE8=52(0x34hex)J2SE7=51(0x33hex)J2SE6.0=50(0x32hex)J2SE5.0=49(0x31hex)JDK1.4=48(0x30hex)JDK1.3=47(0x2Fhex)JDK1.2=46(0x2Ehex)JDK1.1=45(0x2Dhex)复制代码这是十六进制分别对应的JDK版本号 , 十六进制的34换算成十进制是52 , 对应jdk1.8 , 由于本人用的是JDK1.8所以此处是34 。 高版本的JDK能向下兼容低版本的class文件 , 但不能运行比他高版本的class文件 。

文章图片
常量池代表Class文件中的仓库资源 , 紧接着主次版本号之后就是常量池入口 , 由于常量池中常量的数据是不固定的 , 所以在常量池的入口放置了一项u2类型的数据 , 代表常量池容量计数值 , 从1开始 , 字节码里面是0x002d(即十进制的45个 , 代表有44项常量 , 索引值范围1~44 , 第0项空了出来 , 这样做目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表示不引用任何一个常量池项目的目的) 。
常量池主要存放两大类常量:字面量;符号引用 。
字面量接近Java语言层面的常量概念 , 如文本字符串、声名为final的常量值等;符号引用包含三类常量:类和接口的全限定名org.springframework.....Bean字段的名称和描述符private/public/protected方法的名称和描述符private/public/protected
文章图片
u1类型表示的标志tag(1~18)代表当前这个常量属于哪种常量类型 , 如10代表了类中方法的符号引用 , 回到我们的字节码截图里面 , 他的标志位是0x0a,对应到表中就是10即此类型的常量代表一个类中方法的符号引用 。
看图哇事 , 这玩意繁琐又多 , 且都是_info结尾 , 每一项都有自己的结构 , 主要是字面量 , 和字段 , 类 , 接口方法的符号引用 , 谁都往里参合了一脚啊这是 。

文章图片
标志位为10的CONSTANT_Methodref_info的结构
类型名称数量u1tag1u2name_index1u2name_index1复制代码name_index就是图中的index , 是一个索引值代表了这个类或者接口的全限定名 , 字节码中name_index都占2个u,的值分别是0x0009(十进制值为9) , 0x001d(十进制值为29) , 根据表可知分别是指向声明方法的类描述符以及指向名称及类型描述符的索引;
然后字节码是0x09,查表得知此9代表字段的符号引用Fieldref , 结构和CONSTANT_Methodref_info一样 , 依次推算可得到所有的44个常量的内容以及索引 。
这里借助javap看看其他的情况 , javap-verboseTestJVM
Classfile/Users/zengzhiqin/Desktop/daima/leetcode/out/production/leetcode/TestJVM.classLastmodified2020-9-20;size731bytesMD5checksum73a774d54f51805cb2319a2133c47c04Compiledfrom"TestJVM.java"publicclassTestJVMminorversion:0majorversion:52flags:ACC_PUBLIC,ACC_SUPERConstantpool:#1=Methodref#9.#29//java/lang/Object."":()V#2=Fieldref#5.#30//TestJVM.a:I#3=Fieldref#5.#31//TestJVM.b:I#4=Fieldref#32.#33//java/lang/System.out:Ljava/io/PrintStream;#5=Class#34//TestJVM#6=Methodref#5.#29//TestJVM."":()V#7=Methodref#5.#35//TestJVM.multi:()I#8=Methodref#36.#37//java/io/PrintStream.println:(I)V#9=Class#38//java/lang/Object#10=Utf8a#11=Utf8I#12=Utf8b#13=Utf8#14=Utf8()V#15=Utf8Code#16=Utf8LineNumberTable#17=Utf8LocalVariableTable#18=Utf8this#19=Utf8LTestJVM;#20=Utf8add#21=Utf8()I#22=Utf8multi#23=Utf8main#24=Utf8([Ljava/lang/String;)V#25=Utf8args#26=Utf8[Ljava/lang/String;#27=Utf8SourceFile#28=Utf8TestJVM.java#29=NameAndType#13:#14//"":()V#30=NameAndType#10:#11//a:I#31=NameAndType#12:#11//b:I#32=Class#39//java/lang/System#33=NameAndType#40:#41//out:Ljava/io/PrintStream;#34=Utf8TestJVM#35=NameAndType#22:#21//multi:()I#36=Class#42//java/io/PrintStream#37=NameAndType#43:#44//println:(I)V#38=Utf8java/lang/Object#39=Utf8java/lang/System#40=Utf8out#41=Utf8Ljava/io/PrintStream;#42=Utf8java/io/PrintStream#43=Utf8println#44=Utf8(I)V复制代码对照一下可知 , 前面两个常量和我们算到的结果一致 , 我们看到图中出现了很多I , V , 《init》,LineNumberTable等非人类能理解在代码里面也从未出现过的东西 , 这些都会被后面要说到的字段表 , 方法表 , 属性表引用到 , 用来描述一些不可名状的东西 , 不方便用固定字节表示的内容 , 例如方法的返回值是什么 , 有几个参数 , 每个参数类型是啥等等 , 也就是这些不确定的东西需要常量表的符号引用进行表达 。
添加一个方法时 , 常量池中会增加4个常量;同理 , 添加字段也是如此 , 添加的内容有:
CONSTANT_Methodref_info方法的符号引用方法符号引用指向的CONSTANT_NameAndType_info方法的部分符号引用方法的名称方法的描述符紧接着常量池之后的两个字节代表访问标志(access_flags) , 用于识别一些类或者接口层次的访问信息 , 包括:这个Class是类还是接口、是否为public类型、是否为abstract类型、类是否声名为final等 。 标志位及其含义如下表:

文章图片
TestJVM这个类仅仅被public修饰了 , 因此其他的标志都为假 , 最终access_flags应为0x0001|0x0020=0x0021 , 字节码中值内容确实是这个 。

文章图片
访问标志之后顺序排列类索引(this)、父类索引(super)、接口索引集合(interfaces) 。 Class文件由这三项来确定这个类的集成关系 。

文章图片
类索引和父类索引引用2个u2类型的索引值表示 , 他们各自指向一个类型为CONSTANT_Class_info的类描述符常量 , 通过CONSTANT_Class_info类型的常量中的索引值找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串 , 从而找到类 。
类索引和父类索引都是u2类型的数据 。 
文章图片
JAVAP里面看到的这两个索引 , 分别是此类继承自Object基类 , 就无别的继承关系了 。

文章图片
接口索引集合入口第一项是u2类型的接口计数器(interfaces_count)表示索引表的容量(即实现了几个接口) 。 如果该类没有实现任何接口 , 则计数器值为0 , 后面的接口索引表不再占用任何字节 , 0x0000因为此类没有实现任何接口 。看到这里已经很累了吧 , 我写的都累了 , 安利给读者们一首小阿七的歌《不谓侠》 , 很好听啊~
接口索引集合后边的是字段计数器:用于标识有多少个字段 , 接着就是字段表集合 。 字段表(field_info)用于描述接口或者类中声明的变量 。
字段包括类级变量以及实例级变量 。 可以包括的信息有:
字段的作用域(public、private、protected修饰符)实例变量还是类变量(static修饰符)可变性(final)并发可见性(volatile)可否被序列化(transient)字段数据类型(基本类型 , 对象 , 数组)字段名称各个修饰符都是布尔值 , 要么有要么没有 , 这个可以使用标志位表示;但字段叫什么名字、字段被定义成什么类型 , 都是无法固定的 , 所以只能引用常量池中的常量来描述 。 由字段的这些内容信息 , 抽象得到如下的字段表结构:

文章图片
name_index和descriptor_index都是对常量池的引用 , 分别代表着字段的简单名称以及字段和方法的描述符 , 关于全限定名、简单名称及描述符的区别:
全限定名ai/yunxi/vm/TestClasss类的全限定名,仅仅是把类中的“.”替换成了“/”
简单名称没有类型和参数修饰的方法或者字段名称如:add()和intm简单名称就是:add、m
描述符用来描述字段的数据类型、方法的参数列表(数量、类型及顺序)和返回值

文章图片
字节码中0x0002代表字段为private , 字节码代表如下:
u20x0002->第一个跟着的是fields_count,这个类只有2个字段表数据以下是字段表内容:u20x0002private为真 , 其他为假u20x000a字段名称name_index,由上面JAVAP常量表可知#10为au20x000b字段描述符descriptor_index,由上面JAVAP常量表可知#11指向常量池字符串I , 这个描述符标识字符含义标识基本类型intu20x0000attribute_count属性表集合无属性 , 为0表示没有额外描述的信息attribute_info上面无内容 , 不占字节复制代码描述符标识字符含义 , 上面的标识基本类型为I , 即对应的下面表的基本类型int

文章图片
由上面这些信息可以推导源代码定义的字段为privateinta;和源码相符 。
懂了字段表之后 , 方法表结构几乎和字段表结构是一模一样的 , 通过访问标志、名称索引、描述符索引可清楚的表达方法的定义 。 除了一些标志位不同 , 毕竟有些修饰符可以修饰方法不能修饰字段 , 有些修饰符可以修饰字段但是方法没有 , 内容如下对比字段表标志有添加有删减有相同:

文章图片
重载(Overload)一个方法:
要与原方法具有相同的简单名称要与原方法有不同的特征签名(特征签名就是一个方法中各个参数在常量池中字段符号的引用集合 , 因为返回值不在特征签名里面 , 所以返回值不同作为重载条件)
文章图片
u20x0004->第一个跟着的是方法数量,这个类有4个方法表数据 , 分别是add(),multi(),main()和构造器方法以下是第一个方法表内容:u20x0001public为真 , 其他为假u20x000d方法简单名称name_index,由上面JAVAP常量表可知#14为()v,v由描述符含义可知是特殊类型void,()代表无参数 , 即构造函数u20x000e方法描述符descriptor_index,由上面JAVAP常量表可知#15指Code,Code之后再讲u20x0001attribute_count,属性表集合有一项属性用于存储一些额外信息attribute_info0x000f由JAVAP看到的指令 , 指向#15,即对应常量”Code“ , 说明此属性是方法的字节码描述复制代码
文章图片
第一个方法:

文章图片
讲了大半年 , 还只是讲了字段 , 方法头这些内容可以通过访问标志 , 名称索引 , 方法描述符来表达清楚 , 这些都是些元数据 , 那么方法体上哪去了呢?这就要属性表出山啦!
有眼力见的朋友可能已经讲字段表和方法表的时候 , 就发现了属性表的踪影 , 用来描述某些场景专有信息的 , 与上面讲到的其他的数据项目不同的是 , 其他数据项目要求严格的顺序 , 长度和内容 , 属性表的限制是放养状态 , 不要求各个属性表具有严格的顺序 , 只要不与已有的属性名重复 , 任何人实现的编译器都可以向属性表中写入自己定义的属性信息 , JVM会运行时会忽略掉他不认识的 。
Java程序方法体中的代码经过Javac编译处理后 , 最终变为字节码指令存储在Code属性中 , Code属性出现在方法表的属性集合之中 。 但并非所有方法表都有Code属性 , 例如抽象类或接口 。
code属性表结构如图:

文章图片
attribute_name_index指向CONSTANT_Utf8_info类型常量的值固定为“Code”attribute_length标识属性值的总长度max_stack代表了操作数据(OperandStacks)深度的最大值max_locals代表了局部变量所表示的存储空间单位:Slotcode_length和code是用来存储Java源程序编译后产生的字节码指令 , codelength代表字节码长度 , code是用于存储字节码指令的一系列字节流 。 字节码指令 , 每个指令字节码代表的指令含义 , 是否需参数 , 是u1类型的单字节 , 取值范围是0x00~0xFF , 即0~255 , 一共可以表达256条指令 , 目前JVM规范已经定义了约200条指令了 。属性有很多的 , JAVA虚拟机规范预定义了21项 , 我们平时能看到的都有

文章图片
还是javap-verboseTestJVM将所有剩下的指令展示出来 , 可以看到方法的描述和调用
常量表前面已经贴出了{publicintb;descriptor:Iflags:ACC_PUBLICpublicTestJVM();descriptor:()Vflags:ACC_PUBLICCode:stack=2,locals=1,args_size=10:aload_01:invokespecial#1//Methodjava/lang/Object."":()V4:aload_05:iconst_36:putfield#2//Fielda:I9:aload_010:iconst_411:putfield#3//Fieldb:I14:returnLineNumberTable:line5:0line6:4line7:9LocalVariableTable:StartLengthSlotNameSignature0150thisLTestJVM;publicintadd();descriptor:()Iflags:ACC_PUBLICCode:stack=2,locals=1,args_size=10:aload_01:getfield#2//Fielda:I4:aload_05:getfield#3//Fieldb:I8:iadd9:ireturnLineNumberTable:line10:0LocalVariableTable:StartLengthSlotNameSignature0100thisLTestJVM;publicintmulti();descriptor:()Iflags:ACC_PUBLICCode:stack=2,locals=1,args_size=10:aload_01:getfield#2//Fielda:I4:aload_05:getfield#3//Fieldb:I8:imul9:ireturnLineNumberTable:line14:0LocalVariableTable:StartLengthSlotNameSignature0100thisLTestJVM;publicstaticvoidmain(java.lang.String[]);descriptor:([Ljava/lang/String;)Vflags:ACC_PUBLIC,ACC_STATICCode:stack=3,locals=1,args_size=10:getstatic#4//Fieldjava/lang/System.out:Ljava/io/PrintStream;3:new#5//classTestJVM6:dup7:invokespecial#6//Method"":()V10:invokevirtual#7//Methodmulti:()I13:invokevirtual#8//Methodjava/io/PrintStream.println:(I)V16:returnLineNumberTable:line18:0line20:16LocalVariableTable:StartLengthSlotNameSignature0170args[Ljava/lang/String;}SourceFile:"TestJVM.java"复制代码可以看到一共四个方法 , 和我们之前看到字节码推论到的数目一样 , args_size都为1 , 但是无论是实例构造器 , 还是add() , multi()方法都没有参数 , 这个的原因是:在任何的实例方法我们知道可以通过this.method()来进行调用 , 通过this来访问到此方法所属对象 , 他的实现就是通过javac编译器编译的时候把对this关键字的访问变成对一个普通方法参数的访问 , 然后在虚拟机调用实例方法时候自动传入此参数 , 因此在实例方法的局部变量表里面至少会存在一个指向当前对象实例的局部变量 , 局部变量表也会预留第一个slot位来存放对象实例的引用 , 其他的方法参数自然靠边站从1开始计算了 。
字节码分析 , 从上面的方法属性表位置开始:

文章图片
attribute_name_index是一项指向CONSTANT_UTF8_INFO的常量索引 , 常量值固定为Code,代表了该属性的属性名称 。
看一段包含异常语法的简单代码:
/***@authorbyzengzhiqin*2020-09-13*/publicclassTestException{publicintinc(){intx;try{x=1;returnx;}catch(Exceptione){x=2;returnx;}finally{x=3;}}}复制代码再看其内容(字节码0~4行做的就是将证书1赋值给变量x,并且将x的值复制一份副本到最后一个本地变量表的slot中 , 这个slot里面的值在ireturn指令执行前将会被重读到操作栈顶 , 作为方法返回值使用 , 这个slot用returnValue表示):
0:iconst_0//常量0压入操作数栈1:istore_2//弹出操作数栈栈顶元素 , 保存到局部变量表第2个位置2:iload_0//第0个变量压入操作数栈顶3:iload_1//第1个变量压入操作数栈顶4:iadd//操作数栈中的前两个int相加 , 并将结果压入操作数栈顶5:istore_2//弹出操作数栈栈顶元素 , 保存到局部变量表第2个位置6:iload_2//加载局部变量表的第2个变量到操作数栈顶7:ireturn//返回8:aload//从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶复制代码上面是一些需要用的的指令的相关解释
zengzhiqin@cengzhiqindeMacBook-Pro~/Desktop/daima/leetcode/ srcjavap-cTestExceptionCompiledfrom"TestException.java"publicclassTestException{publicTestException();Code:0:aload_01:invokespecial#1//Methodjava/lang/Object."":()V4:returnpublicintinc();Code:0:iconst_1//try中x=1,1压入操作数栈1:istore_1//将1从操作数栈存储到局部变量表第一个位置,x=12:iload_1//加载局部变量表第一个位置元素到操作数栈顶3:istore_2//弹出操作数栈顶元素1 , 保存到局部变量表第2个位置4:iconst_3//finally块中x=3 , 将3压入操作数栈5:istore_1//弹出栈顶元素3 , 将其保存到局部变量表第1个位置6:iload_2//将变量表第2个位置值1放到栈顶 , 准备给ireturn返回7:ireturn//正常情况下返回1正确吻合~8:astore_2//给catch中定义的Exceptione赋值 , 存储在slot2中9:iconst_2//catch中x=2 , 2压入操作数栈10:istore_1//弹出栈顶的2 , 保存到slot111:iload_1//局部变量表第1个位置的2压入栈顶12:istore_3//弹出栈顶元素2保存到局部变量第3个位置13:iconst_3//finally中x=3 , 将3压入操作数栈14:istore_1//将3放到局部变量表第1个位置 , 准备给ireturn返回15:iload_3//加载局部变量表第3个位置的值2到栈顶16:ireturn//返回栈顶元素2catch异常返回2正确 , 吻合17:astore4//如果出现了不属于java.lang.Exception及其子类异常走到这里19:iconst_3//finally块中x=3 , 将3压入操作数栈20:istore_1//将3存储到局部变量表第1个位置21:aload4//将异常引用放在栈顶 , 并且抛出23:athrow//抛出异常Exceptiontable:fromtotargettype048Classjava/lang/Exception0417any81317any171917any}复制代码这里可初步推测 , Java虚拟机执行字节码是基于栈的体系结构 , 执行过程可以看我上一篇的讲解~懒得贴链接了 。
异常的执行过程 , finally代码块会在所有正常及异常的路径上都复制一份 , 在这段字节码中 , iconst_3就是对应着finally代码块 , 共三份 , 所以即便在try或者catch代码块中有return语句 , 最终还是会会执行finally代码块中的内容 , 这段代码毫无疑问是返回1 , 如果在finally里面加上returnX,那么就是返回3了 , 这个return什么值的原因是这样来滴!!!
我们可以看到异常表 , 归纳出异常表结构:

文章图片
字节码0-4行所做的操作数就是将整数1赋值给变量x如果这时没有出现异常 , 则会继续走到第5-7行如果出现了异常 , PC寄存器指针转到第8行如果0-4行出现任何异常 , 则跳转17行如果8-13行出现任何异常 , 则跳转17行如果17-19行出现任何异常 , 则跳转17行可知 , 异常表实际上是JAVA代码的一部分 , 编译器使用异常表而不是简单命令来实现JAVA异常以及finally处理机制的 。
异常是平时最常用的 , 其他的属性大家有兴趣再去深入了解亦可 , 写到这里本可爱是真的很累 , 而你百分之八十的几率是直接跳着看到我这句话的 , 路过的小哥哥们随手点个赞吧 ,
【软妹手把手教你javap反编译分解代码,授人以鱼不如授人以渔】作者:阿甘的马路链接:来源:掘金著作权归作者所有 。 商业转载请联系作者获得授权 , 非商业转载请注明出处 。
推荐阅读
- 教你自制岩烧乳酪
- 教你做虎皮蛋糕上的虎皮,掌握2个技巧,保证起虎皮,做法很简单
- 不用油不用牛奶,教你做蜂蜜蛋糕,蒸或烤都可以,只需4种食材
- 春天来了教你几道好吃不油腻的家常菜,美味简单下饭,百吃不厌!
- 冬天到了,教你几道暖胃家常菜,营养健康,孩子营养不发愁!
- 春节过后请客吃饭教你几道美味特色的下酒菜,简单易学,味道超赞!
- 想吃点心不用买,教你在家做“驴打滚”,不用烤箱,软糯香甜!
- 家常炖老母鸡,不用炖3小时,教你一妙招,40分钟鸡肉软嫩入味
- 豆腐不要红烧了,教你做外脆里嫩的炸豆腐,家人吃到赞不绝口
- 想吃香蕉饼不要出去买,教你在家做,香甜松软又好吃,5分钟出锅
