关于java:破解class文件的第一步深入理解JAVA-Class文件

11次阅读

共计 3653 个字符,预计需要花费 10 分钟才能阅读完成。

摘要: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 代码

public class ByteCode {public ByteCode(){}}

该类非接口,非抽象类,非枚举,非零碎代码,非 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 代码

public class Test {public void add(int a,int b){System.out.println(a+b);
        System.out.println(a-b);
        System.out.println(a*b);
        System.out.println(a/b);
    }
}

字节码指令如下:

(3)类型转换指令

源码:

Java 代码

public class Test {public void add(int a,int b){
        int c = 1;
        long d = c;
    }
}

字节码指令:

(4)创立实例指令

这个不必多讲,就是 new

(5)创立数组指令

源码:

Java 代码

public class Test {public void add(int a,int b){int[] c = new int[4];
        String[] d = new String[5];
    }
}

字节码指令:

(6)拜访字段指令

源码:

Java 代码

public class Test {
    private static String name = "1";
    private String age = "2";
    public static void main(String[] args) {Test test = new Test();
        String a = test.age;
        String b = Test.name;
    }
}

字节码指令:

(7)数组存取指令

源码:

Java 代码

public static void main(String[] args) {String[] a = new String[5];
    a[1] = "2";
    String b = a[1];
}

字节码指令:

(8)查看实例类型指令

就是 instanceof,演示略

(9)办法返回指令

就是 return,演示略

五. 异样操作

间接看一段代码:

Java 代码

public class Test {public void test() {
        InputStream in = null;
        try {in = new FileInputStream("i.txt");
        } catch (FileNotFoundException e) {e.printStackTrace();
        }finally {
            try {in.close();
            } catch (IOException e) {e.printStackTrace();
            }
        }
    }
}

代码是一段典型的文件流操作,与其余代码不同的是,它捕捉了两个异样。

那么,字节码指令又是如何解决该异样的呢

咱们能够看到,最底下呈现了一个 exception table,即异样表,它记录了所有的异样数据

以异样表第一行举例,from,to 别离代表,如果第 12 行,到第 16 行间产生异样,则间接跳到第 19 行(target)。

六. 装箱拆箱

这是绕不过来的一个话题。

凡是有一点 java 根底的人都晓得,java 有八大根底数据类型,每一种类型都对应一种包装类。如 int 之于 Integer,long 之于 Long。

一般来讲,根底数据类型和包装类都能够互相赋值。但这其中的逻辑如何呢?

Java 代码

public class Test {public static void main(String[] args) {
       Integer i = 1;
       int a = 2;
       int b = 3;
       i = a;
       b = i;
    }
}

咱们来看看字节码指令

从字节码指令中,咱们能够看到,有三次拆装操作

  • 第一次,调用 Integer 的 valueOf 办法,讲常量 1 转为 Integer 类型;
  • 第二次,调用 Integer 的 valueOf 办法,讲栈顶值 2 转为 Integer 类型;
  • 第三次,调用 intValue 办法,讲 Integer 转为 int,而后赋值给 b。

前两部为装箱,后一步为拆箱。

这就是拆装箱的底层实现逻辑了。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0