破解 class 文件的第一步:深入理解 JAVA Class 文件
摘要: java 定义了一套与操作系统,硬件无关的字节码格式,这个字节码就是用 java class 文件来表示的,java class 文件内部定义了虚拟机可以识别的字节码格式,这个格式是平台无关性的。
本文分享自华为云社区《java 之深入 class 文件》,原文作者:技术火炬手。
java 语言是跨平台的,所谓一次编写,到处运行。之所以是跨平台的,就是 java 定义了一套与操作系统,硬件无关的字节码格式,这个字节码就是用 java class 文件来表示的,java class 文件内部定义了虚拟机可以识别的字节码格式,这个格式是平台无关性的,在 linux 系统或者在 windows 系统上都是一致的。这个就好比 html 文件,我们定义好规范,这个系统只要去按照规范显示出来里面的内容就好了。
一.JVM 的语言无关性
JVM 是干什么用的?
运行 java 的啊,难不成是运行 python 的?
这句话是对的,但不完整,JVM 并不是只能运行 java 程序。
事实上,JVM 上运行的本身也不是 java 文件,而是 class 文件。
而能够编译转化为 class 文件的,并不只有 java 一种。
这就是 JVM 的语言无关性。
至于能不能运行 python,取决于是否有一个能将 python 转成 class 文件的工具。
当然这样做没有太多的意义,毕竟 python 也有其运行环境,且在某种意义上,比 java 更强大,核心类库更完善。
各种语言也有各自的平台,所以没有必要强制编译。
但掌握 class 文件还是很有意义的。
作为一个程序员,你是否有过或者曾经有过创建一门语言的奢望?最好还是用汉语开发。
但现实,或者大学里的某个导师,却给你兜头一盆冷水。
先花个三五年研究汇编,再考虑实现这些。
三五年,黄花菜都凉了。
现在,有了 JVM,似乎看到了一点希望的曙光。
二.class 文件的本质
要实现之前的设想,或者说,想开发一个编译工具。首先要做的,就是要解构 class 文件本身。
无论如何得来,class 文件的本质都是一组以 8 位字节为基础单位的 2 进制流。
记住,是 2 进制。
为了证明这一点,我们还是要用到一些工具。比如,Sublime。
它并不是一个直接查看 2 进制的工具,而是 16 进制的编辑器(2 进制和 16 进制可以无缝切换)。
这里面似乎还有 python 的事情哦。使用时,直接点击 sublime_text.exe 文件即可。
然后选择 class 文件,打开,如下图的样子。
看的人眼花对不对?这都什么玩意!
前文说了,2 进制,不,这就是 16 进制啊。
如果你不想去看 16 进制,也可以使用 javap,直接去查看字节码指令(详细内容见前文《一段 java 代码是如何执行的》)。
如果你也不想打开命令行,还有一个叫 jclasslib 的工具,可提供图形化界面,它还有适用于 idea 的插件。
但它不是重点,暂且忽略。
三.class 文件结构揭秘
class 文件格式中只有两种数据类型,无符号数和表。
其中,无符号数包含所有的基础数据类型和字符串,索引引用等,根据字节长度又可以分为 u1,u2,u4,u8,分别代表无符号数的长度为 1,2,4,8。
而表,即对象类型。
接下来,以 sublime 文件解析的内容为蓝本,按顺序说说的 class 文件的构成。
(1)class 文件的头四个字节被称为魔数,它的作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。
如,上文中魔数的值为:
它代表该文件是一个 class 类型的文件,不信,你可以多打开几个 class 文件看看。
(2)接下来的四个字节代表 jdk 的版本
如上的内容代表 jdk 的版本为 1.8。
PS:jdk1.1 的版本数字为 45,以后每跨一个大版本,数字+1,所以 jdk1.8 的版本数字为 51(十进制),转化为 16 进制即为 34。
(3)下面一个概念是常量池
以上内容是常量池的计数器,通过该数字,我们计算出常量的个数为 15 个(计算出的数字减 1,因为该计数器的起始数不是 0,而是 1)
我们用 javap 命令打开常量池,证明常量的确是 15 个。
(4)常量池后面就是访问标志,访问标志主要分为如下类别
我们回头去看看这段 class 的源码(居然如此简单)
Java 代码
该类非接口,非抽象类,非枚举,非系统代码,非 final,有 pulbic,且编译器在 jdk1.2 之后,所以,满足条件的标志为:
ACC_PUBLIC 和 ACC_SUPER,对应标志数为 0001 和 0020,合并起来就是 0021。如下图位置:
(5)类索引,父类索引和接口索引
上文访问标志后面就是类索引,索引值为 0002,对应常量池第二位。
类索引后面就是夫类索引,索引值为 0003,对应常量池第三位。
父类索引后面就是接口索引,索引值为 0000,代表该类没有实现任何接口。
(6)字段表,方法表,属性表
三大索引之后就是字段表
字段表为 0000,代表无字段。
如上图,方法表分为四部分
方法表计数器的结果为 1,代表有一个字段
方法表访问标志为 0001,代表 public
方法表名称索引为 0004,对应常量池第 4 个
方法表描述索引为 0005,对应常量池第 5 个
属性表以此类推。
四.字节码指令
单独开一个章节讲讲字节码指令,它存在于方法表中,如下分类:
(1)加载和存储指令
此部分内容,见前文《一段 java 代码是如何执行的》)
(2)运算或算术指令
源码:
Java 代码
字节码指令如下:
(3)类型转换指令
源码:
Java 代码
字节码指令:
(4)创建实例指令
这个不用多讲,就是 new
(5)创建数组指令
源码:
Java 代码
字节码指令:
(6)访问字段指令
源码:
Java 代码
字节码指令:
(7)数组存取指令
源码:
Java 代码
字节码指令:
(8)检查实例类型指令
就是 instanceof,演示略
(9)方法返回指令
就是 return,演示略
五.异常操作
直接看一段代码:
Java 代码
代码是一段典型的文件流操作,与其他代码不同的是,它捕获了两个异常。
那么,字节码指令又是如何处理该异常的呢
我们可以看到,最底下出现了一个 exception table,即异常表,它记录了所有的异常数据
以异常表第一行举例,from,to 分别代表,如果第 12 行,到第 16 行间发生异常,则直接跳到第 19 行(target)。
六.装箱拆箱
这是绕不过去的一个话题。
但凡有一点 java 基础的人都知道,java 有八大基础数据类型,每一种类型都对应一种包装类。如 int 之于 Integer,long 之于 Long。
一般来讲,基础数据类型和包装类都可以相互赋值。但这其中的逻辑如何呢?
Java 代码
我们来看看字节码指令
从字节码指令中,我们可以看到,有三次拆装操作
第一次,调用 Integer 的 valueOf 方法,讲常量 1 转为 Integer 类型;
第二次,调用 Integer 的 valueOf 方法,讲栈顶值 2 转为 Integer 类型;
第三次,调用 intValue 方法,讲 Integer 转为 int,然后赋值给 b。
前两部为装箱,后一步为拆箱。
这就是拆装箱的底层实现逻辑了。
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/8cdb12be79a5776f7a92c631c】。文章转载请联系作者。
评论