zoukankan      html  css  js  c++  java
  • 破解class文件的第一步:深入理解JAVA Class文件

    摘要: java定义了一套与操作系统,硬件无关的字节码格式,这个字节码就是用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。

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

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

     本文分享自华为云社区《java之深入class文件》,原文作者:技术火炬手。

    点击关注,第一时间了解华为云新鲜技术~

  • 相关阅读:
    异常处理
    组合,封装
    自我介绍
    27python更多实例
    28python类代码编写细节
    29python运算符重载
    30python 类的设计
    31python类的高级主题
    32python异常基础
    33python异常编码细节
  • 原文地址:https://www.cnblogs.com/huaweiyun/p/14692653.html
Copyright © 2011-2022 走看看