hex文件格式解析_Java字节码文件的解析

代码变成的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步

1. class字节码文件的解析

1.1. 背景

最近由于工作的需要,对Class文件做了一定的了解。然而枯燥无味的课程总是让人犯困,在学习的过程中,总有一种虚无缥缈的感受,看起来好像已经会了。但又好像什么也没懂。故想到不如自己写一篇博客,写一下解析Java字节码的代码,来验证一下自己到底明白了么。 (本以为几天就搞定,然而断断续续拖了很久很久,很久很久很久!!!深感学习不易,写代码更难,写文章就是难上加难)

1.2. 概述

字节码文件说白了就是一个程序指令的一种表现形式,为了实现“一次编写,到处运行”的宣传口号,Java虚拟机可以被搭载在各个平台之上运行字节码文件。顺便一提,Class文件并不与Java绑定的,Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。 学习Class文件有什么用呢?或许没有,又或许有。 本文将尽力结合Java代码把Class文件讲清楚,篇幅可能很长,且啰嗦。如果需要更细致的文章,建议读《深入理解Java虚拟机》。

1.3. 准备前提

要学习Java字节码文件,当然要先学习字节码是什么,首先要知道1 Byte = 8 bit,其他细节建议Google学习。这里先介绍一个自己编写的工具类,HexUtil.java。在读取字节码文件的时候,我们拿到的都是二进制位,类似于0001这样的比特位,为了解析它,需要将bit位与整数直接的转换,bit位与字符串直接的转换。 直接看代码吧。

public class HexUtils {
      
    /**
     * byte数组转int
     *
     * @param bytes
     * @return
     */
    public static int bytes2Int(byte[] bytes) {
      
        // 根据byte的位数
        int result = 0x00000000;
        for (int i = 0; i < bytes.length; i++) {
      
            result = result ^ ((bytes[i] & 0x000000ff) << (8 * (bytes.length - i - 1)));
        }
        return result;
    }

    public static int bytes2Int(byte a, byte b) {
      
        return a << 8 & 0x0000ff00 ^ b & 0x000000ff;
    }

    /**
     * byte数组 转换成 16进制小写字符串
     */
    public static String bytes2Hex(byte[] bytes) {
      
        if (bytes == null || bytes.length == 0) {
      
            return null;
        }

        StringBuilder hex = new StringBuilder();

        for (byte b : bytes) {
      
            hex.append(Integer.toHexString(b & 0x00000ff)); // 将低八位取出来
        }

        return hex.toString();
    }

    /**
     * 16进制字符串 转换为对应的 byte数组
     */
    public static byte[] hex2Bytes(String hex) {
      
        if (hex == null || hex.length() == 0) {
      
            return null;
        }

        char[] hexChars = hex.toCharArray();
        byte[] bytes = new byte[hexChars.length / 2];   // 如果 hex 中的字符不是偶数个, 则忽略最后一个

        for (int i = 0; i < bytes.length; i++) {
      
            bytes[i] = (byte) Integer.parseInt("" + hexChars[i * 2] + hexChars[i * 2 + 1], 16);
        }

        return bytes;
    }

}

可能会有人看了以后毫无感觉,甚至会说“我曹,写的什么垃圾玩意。” 确实挺垃圾,但好在勉强可以用。先解释一下。

public static int bytes2Int(byte[] bytes) {
      
        // 根据byte的位数
        int result = 0x00000000;
        for (int i = 0; i < bytes.length; i++) {
      
            result = result ^ ((bytes[i] & 0x000000ff) << (8 * (bytes.length - i - 1)));
        }
        return result;
    }

为什么字节转int,要与上0xff呢?这里就要说道遇到的一个坑。在Java中,Java中数值的二进制是采用补码的形式表示的。为了拿到原始的数字就要每一位与上0xff。为什么?举个例子: 举个例子,原来魔数十六进制字符串为 cafebabe 换成二进制就是 cafebabe ------> 11001010 11111110 10111010 10111110

这些数字用int表示应该是

11001010 -> 00000000 00000000 00000000 11001010 -> 202
11111110 -> 254
10111010 -> 186
10111110 -> 190

但java读到的数字确是 -54, -2, -70, -66 这几个数字在计算机中表示为

00000000 00000000 00000000 00110110 -> 54的源码
 11111111 11111111 11111111 11001001 -> 54的反码
 11111111 11111111 11111111 11001010 -> 54的补码 -54在计算机中的表示

 00000000 00000000 00000000 00000010 -> 2的源码
 11111111 11111111 11111111 11111101 -> 2的反码
 11111111 11111111 11111111 11111110 -> 2的补码 -2在计算机中的表示

可以看出来低八位就是原来数字 理论基础搞定,接下来就好办了,我们只需将每一个字节的低八位取出来,经过移位操作就可以恢复出原始的数字。

1.4. Class类文件的结构

回到正题上。

Class文件是一组以8字节为基础单位的二进制字节流,各个数据项目严格按照循序紧凑的排列在Class文件中,中间没有添加任何分隔符。见下图:

hex文件格式解析_Java字节码文件的解析_第1张图片

在右图中,class文件被读成了一个个十六进制展示,最明显的可以看到前四个字节的十六进制表示为“CAFEBABE”。而我们的任务不仅是要看懂这一坨二进制,而且要编写代码解析他们,把它们用类来表示。

根据Java虚拟机规范的规定,Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。

1.4.1. 无符号数

所谓的无符号数就是把几个字节提取出来表示为一个数字,这个数字可以用来描述索引、数量值、或者按照UTF-8编码构成的字符串值。

那么几个字节呢?比较术语的讲,class文件中包含了四种类型,分别是u1、u2、u4、u8,其各自代表了一个字节、两个字节、四个字节、八个字节。

举个栗子,在下面这段代码中,Count表示“值”的概念。在class文件中,“值”一般是两个字节(也有可能是四个字节)。那么我们就可以在读取class文件的两个或四个字节,并把读取到的字节转换为int类型

public class Count extends ReadBytes {
      
    // 表示数量、需要读两个字节(默认),或四个字节来表示这个数量
    private static final int length = UKind.U2; // 一般这个
    private int num;

    public Count(FileInputStream fileInputStream) {
      

        super(fileInputStream, length);
        num = HexUtils.bytes2Int(bytes);
    }

    public Count(FileInputStream fileInputStream, int length) {
      
        super(fileInputStream,length);
        num = HexUtils.bytes2Int(bytes);
    }

    public int getNum() {
      
        return num;
    }

    @Override
    public String toString() {
      
        return ""+num;
    }
}

1.4.2. 表

表抽象的看就是一个类,它里面包含了其他的无符号数和表。这个class文件可以认为就是一个表。

1.4.3. Class文件格式

|类 型|名 称|数 量| |:----:|:----:|:----:| |u4|magic(魔数)|1| |u2|minor_version(小版本)|1| |u2|major_version(主版本)|1| |u2|constant_pool_coun(常量个数)|1| |cp_infoconstant_pool(常量池)|constant_pool_count - 1| |u2|access_flags(访问标识)|1| |u2|this_class(当前类的名字)|1| |u2|super_class(父类的名字)|1| |u2|interfaces_count(接口的数量)|1| |u2|interfaces(接口)|interfaces_count| |u2|fields_count(字段的个数)|1| |field_infofields(字段表)|fields_countu2methods_count(方法表的个数)|1| |method_infomethods(方法表)|methods_count| |u2|attributes_count(属性表个数)|1| |attribute_info|attributes(属性表)|attributes_count|

无论是表还是无符号数,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列的连续的某一类型的数据为某一类型的集合

1.5. 开始解析Class文件

public class ReadBytes {
      
    public byte[] bytes;
    private FileInputStream fileInputStream;

    public ReadBytes(FileInputStream fileInputStream,int length){
      
        bytes = new byte[length];
        try {
      
            int readNum = fileInputStream.read(bytes);
            if (readNum == -1){
      
                System.out.println("字节读取失败");
            } else {
      

            }
        } catch (IOException e) {
      
            e.printStackTrace();
        }
    }
}

使用这段代码对class文件进行字节码的读取。

1.5.1. 魔数与Class文件的版本

首先从简单的开始,从Class类文件的结构可以看到,前四种类型的含义和长度都是固定的。

1.5.1.1. 魔数

每一个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

代码很简单,只需要把class文件的前四个字节读取出来,就可以了。

public class MagicNumber extends ReadBytes {
      
    // 版本魔数
    // 占四个字节 u4类型
    private static final int length = UKind.U4;
    private String hexString;
    public MagicNumber(FileInputStream fileInputStream) throws Exception {
      
        super(fileInputStream, length);
        System.out.println(Arrays.toString(bytes));
        hexString = HexUtils.bytes2Hex(bytes); // 将四个字节转为十六进制字符串
        if (!hexString.equals("cafebabe")){
      
            throw new Exception("该文件不是正确的class文件");
        }
    }

    public String getHexString() {
      
        return hexString;
    }

    @Override
    public String toString() {
      
        return hexString;
    }

1.5.1.2. 版本号

第五和第六字节是次版本号,第七和第八字节是主版本号。代码同样很简单,只需要读两个字节到byte数组中,然后将字节转为int类型就可以了。

public class Version extends ReadBytes {
      
    // 版本信息
    private static final int length = UKind.U2;
    private int versionNum;
    public Version(FileInputStream fileInputStream) {
      
        super(fileInputStream,length);
        versionNum = HexUtils.bytes2Int(bytes);
    }


    public int getVersionNum() {
      
        return versionNum;
    }

    @Override
    public String toString() {
      
        return ""+versionNum;
    }
}

1.5.1.3. 常量池

紧接着主次版本号的是常量池的信息,常量池可以理解为Class文件之中的资源仓库,它里面存储着class文件的所有信息,包括变量名、定义的字符串等等,它是class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据。

由于常量池中的常量是不固定的,所以在常量池的入口需要放置一个u2类型的数据,代表常量池容量计数值。需要注意一点的是,这个计数是从1开始的,其中0表示Null。

我们需要一个类去读取class文件中的count,对于数量这个概念而言,通常是用两个或四个字节来表示,代码很简单,只需要读出来并转换为int类型即可。

public class Count extends ReadBytes {
      
    // 表示数量、需要读两个字节(默认),或四个字节来表示这个数量
    private static final int length = UKind.U2; // 一般这个
    private int num;

    public Count(FileInputStream fileInputStream) {
      

        super(fileInputStream, length);
        num = HexUtils.bytes2Int(bytes);
    }

    public Count(FileInputStream fileInputStream, int length) {
      
        super(fileInputStream,length);
        num = HexUtils.bytes2Int(bytes);
    }

    public int getNum() {
      
        return num;
    }

    @Override
    public String toString() {
      
        return ""+num;
    }
}

在代码中,我们创建了一个长度为count+1的数组,用于存放常量。

得知了有常量池中常量的数量以后,我们需要根据数量从class文件中将常量池信息解析出来,但常量都是什么呢?

constantPoolCount = new Count(fi);
constantPool = new ConstantInfo[constantPoolCount.getNum() + 1]; // 加一是因为多了一个null
constantPool[0] = null;
IntStream.range(1, constantPoolCount.getNum()).forEach(i -> constantPool[i] = ConstantFactory.createConstantInfo(i,fi));

常量池中主要存放两大类常量:字面量和符号引用。

字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。

而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符

可以将符号引用理解为java文件中出现的单词。在常量池中存下了有关类、字段和方法的表示。

常量池中的项目类型如下表所示,后面我们可以得知,每一种类型开始的第一位都是一个一字节(u1)类型的标志位,代表当前这个常量属于哪种常量类型。利用这种特性,我们可以使用工厂模式,去解析每一个常量类型。

|类 型|标志|描述| |:----:|:----:|:----:| |CONSTANT_Utf8_info|1|UTF-8编码的字符串| |CONSTANT_Integer_info|3|整形字面量| |CONSTANT_Float_info|4|浮点型字面量| |CONSTANT_Long_info|5|长整型字面量| |CONSTANT_Doubel_info|6|双精度浮点型字面量| |CONSTANT_Class_info|7|类或接口的符号引用| |CONSTANT_String_info|8|字符串类型字面量| |CONSTANT_Fieldref_info|9|字段的符号引用| |CONSTANT_Methodref_info|10|类中方法的符号引用| |CONSTANT_InterfaceMethodref_info|11|接口中方法的符号引用| |CONSTANT_NameAndType_info|12|字段或方法的部分符号引用| |CONSTANT_MethodHandle_info|15|表示方法句柄| |CONSTANT_MethodType_info|16|表示方法类型| |CONSTANT_InvokeDynamic_info|18|表示一个动态方法调用点|

常量池中所有可能存在类型共上面14种,每一种的结构都不太相同,但庆幸的是,它们的第一个字节用于表示自己是什么类型。我们可以使用这个字节去动态创建出对于的常量。

public class Tag extends ReadBytes{
      
    private static final int length = UKind.U1;
    private int num;
    public Tag(FileInputStream fileInputStream) {
      
        super(fileInputStream, length);
        num = bytes[0];
    }

    public int getNum() {
      
        return num;
    }
}


public static ConstantInfo createConstantInfo(FileInputStream fi) {
      
        Tag tag = new Tag(fi);
        switch (tag.getNum()) {
      
            case Constants.CONSTANT_Utf8:
                return new ConstantUtf8Info(fi, new Count(fi).getNum());
            case Constants.CONSTANT_Integer:
                return new ConstantIntegerInfo(fi);
            case Constants.CONSTANT_Float:
                return new ConstantFloatInfo(fi);
            case Constants.CONSTANT_Long:
                return new ConstantLongInfo(fi);
            case Constants.CONSTANT_Double:
                return new ConstantDoubleInfo(fi);
            case Constants.CONSTANT_Class:
                return new ConstantClassInfo(fi);
            case Constants.CONSTANT_String:
                return new ConstantStringInfo(fi);
            case Constants.CONSTANT_Fieldref:
                return new ConstantFieldrefInfo(fi);
            case Constants.CONSTANT_Methodref:
                return new ConstantMethodrefInfo(fi);
            case Constants.CONSTANT_InterfaceMethodref:
                return new ConstantInterfaceMethodRefInfo(fi);
            case Constants.CONSTANT_NameAndType:
                return new ConstantNameAndTypeInfo(fi);
            case Constants.CONSTANT_MethodHandle:
                return new ConstantMethodHandleInfo(fi);
            case Constants.CONSTANT_MethodType:
                return new ConstantMethodTpeInfo(fi);
            case Constants.CONSTANT_InvokeDynamic:
                return new ConstantInvokeDynamicInfo(fi);
            default:
                return null;
        }
    }

所以,我们可以很轻易的写出上述代码,只需要先读出tag,就可以根据响应的tag创建对应的常量种类。

接下来,我们要根据上表来编写代码,如果看懂了一个表示方式,剩下就应该全明白了,可以适当跳过。

CONSTANT_Utf8_info

需要读出的长度由length决定,而length是一个u2类型的数。所以只需要先读出字符串所占的长度,就可以读出来bytes,然后将byte数组转为字符串即可。

public class ConstantUtf8Info extends ReadBytes implements ConstantInfo {
      
    private String constantString;

    public ConstantUtf8Info(FileInputStream fi, int length) {
      
        super(fi, length);
        constantString = new String(bytes, Charset.defaultCharset());
    }

    public String getConstantString() {
      
        return constantString;
    }

    @Override
    public String toString() {
      
        return "#" + " = Utf8" + "          " + constantString;
    }
}

CONSTANT_Integer_info

你可能感兴趣的:(hex文件格式解析,java,byte,十六进制,java,byte数组转int,java,byte比较,java,byte转int,java,byte转integer)