第四章 用Java实现JVM之解析class文件

用Java实现JVM目录

第零章 用Java实现JVM之随便说点什么
第一章 用Java实现JVM之JVM的准备知识
第二章 用Java实现JVM之命令行工具
第三章 用Java实现JVM之查找Class文件
第四章 用Java实现JVM之解析class文件
第五章 用Java实现JVM之运行时数据区
第六章 用Java实现JVM之指令集和解释器
第七章 用Java实现JVM之类和对象
第八章 用Java实现JVM之方法调用和返回
第九章 用Java实现JVM之数组和字符串
第十章 用Java实现JVM之本地方法调用
第十一章 用Java实现JVM之异常处理
第十二章 用Java实现JVM之结束


文章目录


前言

上一篇我们已经实现了`class文件`的查找,既然能找到文件,就可以解析成我们可以使用的格式,今天开启新的征程,继续往下

Class文件

Java之所以号称一次编译,四处运行。jvm功不可没,但是还需要Class文件进行辅助,如果没有对Class文件统一的规范,啥都拿来给jvm运行,jvm会一脸懵逼:这是啥玩意。所以在 Java虚拟机规范 中明确规定了Class文件的格式:

整体看下来其实就是我们平时类的一个东西,比如说:访问标志、父类、接口数量等。我们只要把这些东西解析成一个对象,方便我们使用即可。所以这章不难,就是有点重复,工作量稍微有的大

Class文件结构

在上一章中,我们已经把Class文件读取出来放到ClassFileInfo中了,接下来需要做的是把那堆十六进制的字符串转成数据结构(也就是对象 ),才能给我们使用。Java虚拟机规范定义了u1u2u4三种数据类型来表示1、2和4字节无符号整数,我们把这些方法都抽取成对应的抽象类。AbstractClassCode代码如下:

java 复制代码
/**
 * @author hqd
 * 基础class信息
 */
@Getter
public abstract class AbstractClassCode {
    private ClassFileInfo classFileInfo;
    private String hexStr;

    public AbstractClassCode(ClassFileInfo classFileInfo) {
        this.classFileInfo = classFileInfo;
        this.hexStr = classFileInfo.getByteCode();
    }

    /**
     * 是否以只读的方式(不移动指针),读取指定长度字节
     *
     * @param length
     * @param isReadOnly
     * @return
     */
    public String read(int length, boolean isReadOnly) {
        String str = this.hexStr.substring(0, length << 1);
        if (!isReadOnly) {
            this.hexStr = this.hexStr.substring(length << 1);
        }
        return str;
    }

    /**
     * 读取指定长度字节
     *
     * @param length
     * @return
     */
    public String read(int length) {
        return read(length, false);
    }

    /**
     * 读取1个字节
     *
     * @return
     */
    public String readU1() {
        return read(1);
    }

    /**
     * 读取2个字节
     *
     * @return
     */
    public String readU2() {
        return read(2);
    }

    /**
     * 读取4个字节
     *
     * @return
     */
    public String readU4() {
        return read(4);
    }

    /**
     * 读取8个字节
     *
     * @return
     */
    public String readU8() {
        return read(8);
    }
}

按理说,接下来就是该定义对应ClassFile类,不过嘛,别急。再认真看下 Java虚拟机规范 中的Class文件的格式,发现有些并不是u1u2类型的,像什么cp_infofield_infomethod_infoattribute_info

这几个是啥玩意啊?简单的说,就是复杂对象。里面又包含了一堆的u1u2。这也是整个解析最麻烦的地方。老样子,先知道有这么个事,先易后难,先做简单的。我们来看看Class结构的每一项信息

魔数、版本

首先映入眼帘的是魔数和版本:

分别为:

  • 魔数(magic):魔数!!!不是刘谦老师的那个魔术。这玩意听着挺玄乎,实际上就一个作用,声明文件类型,各个文件都有自己的魔数。例如PDF文件以4字节"%PDF"(0x25、0x50、0x44、0x46)开头,ZIP文件以2字节"PK"(0x50、0x4B)开头。class文件的魔数是"0xCAFEBABE"

  • 副版本号(minor_version):简单理解就是大版本下的小版本。次版本号只在J2SE 1.2之前用过,从1.2开始基本上就没什么用了(都是0)

  • 主版本号(major_version):这个更不用说了,像我们平时常说的JDK8,大版本就是8

这几个要么是描述一个文件类型,要么就是版本信息,权且把他们都放到一个类里,方便操作。ClassVersion代码如下:

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ClassVersion {
    //魔数
    private String magic;
    //副版本号
    private Float minorVersion;
    //主版本号
    private Float majorVersion;
}

类访问标记、当前对象索引、父类索引

版本号之后是cp_info,也就是我们熟知的常量池,但是由于常量池比较复杂,看不懂的先跳过,放到后边再说。继续往下:

这个几个就不用多说了把,不过里面也有门道,现在我们先根据规范读取出来就行了

常量池

好了,简单的说了。接下来就是复杂结构的了,之所以称为复杂,主要是数量多,种类复杂。像接口、属性、方法。这些都可以有多个,必然是个数组,而后是他们又可以套其他内容,故而显得复杂。不过虽然复杂,但是并不难,只要联想我们平时开发的场景,就很好理解了,先来看第一个难啃的骨头了------常量池

常量池之所以称作常量池,必然就是放常量的地方。那么在一个Class中,哪些可以称作常量:类名、字符串、方法、属性。好,明白了这一层,再来看虚拟机规范:

其实常量池主要保存的就是符号引用和字面量,所谓符号引用,简单来说,就是一些类、方法、属性的引用,之所以叫做符号引用,是因为编译时期无法确定,只有等到运行时类加载器加载相关类之后,JVM 才能把这个"符号"解析成真实地址,称为动态链接。所谓字面量,简单的说,就是编译时候可以确定的值,例如:字符串、int、double这些,运行时直接拿来用就行了

好好好,继续套娃是吧。继续往下翻翻看

哦,搜得死噶,那其实就是因为常量池有多种类型,所以才使用tag区别,又因为各个类型长短不一,故而直接用u1数组表示了。既然tag表示类型,那就先定义个枚举类,ConstantInfoType代码如下:

java 复制代码
/**
 * 常量池类型
 *
 * @author hqd
 */

public enum ConstantInfoType {
    CONSTANT_UTF8(1),
    CONSTANT_INTEGER(3),
    CONSTANT_FLOAT(4),
    CONSTANT_LONG(5),
    CONSTANT_DOUBLE(6),
    CONSTANT_CLASS(7),
    CONSTANT_STRING(8),
    CONSTANT_FIELDREF(9),
    CONSTANT_METHODREF(10),
    CONSTANT_INTERFACEMETHODREF(11),
    CONSTANT_NAMEANDTYPE(12),
    CONSTANT_METHODHANDLE(15),
    CONSTANT_METHODTYPE(16),
    CONSTANT_INVOKEDYNAMIC(18);

    private int tag;

    ConstantInfoType(int tag) {
        this.tag = tag;
    }

    public int getTag() {
        return tag;
    }

    public static ConstantInfoType getType(Integer tag) {
        if (tag == null) {
            return null;
        }
        ConstantInfoType[] types = ConstantInfoType.values();
        for (ConstantInfoType type : types) {
            if (type.tag == tag) {
                return type;
            }
        }
        return null;
    }
}

在提取个常量池基础类,代码如下:

java 复制代码
/**
 * 常量池类
 */
@Getter
public class ConstantPool {
	protected ConstantInfoType tag;
	protected ClassFile classFile;
	//省略其他代码。。。
}

常量池的类型比较多就不一一展开了,就以一个CONSTANT_utf8_info字符串常量 )为例吧,CONSTANT_utf8_info 的结构如下:

代码如下:

java 复制代码
/**
 * 字符串常量池
 */
@Getter
public class ConstantUtf8Info extends ConstantPool {
	private String bytes;
	private Integer length;

	@Override
	public String getVal() {
		return bytes.trim().intern();
	}
}

其他常量池类型都是类似,有这么多常量池类型,每次都要判断类型在创建,也不是个事。所以再定义个简单工厂,用于创建各个不同类型的常量池信息。ConstantPoolFactory代码如下:

java 复制代码
/**
 * 常量池工厂
 *
 * @author hqd
 */
public class ConstantPoolFactory {
    public static List<ConstantPool> newConstantPool(ConstantInfoType tag, ClassFile classFile) {
        List<ConstantPool> constantPoolList = new LinkedList<>();
        switch (tag) {
            case CONSTANT_UTF8:
                constantPoolList.add(new ConstantUtf8Info(tag, classFile));
                break;
            case CONSTANT_INTEGER:
                constantPoolList.add(new ConstantIntegerInfo(tag, classFile));
                break;
            case CONSTANT_FLOAT:
                constantPoolList.add(new ConstantFloatInfo(tag, classFile));
                break;
            case CONSTANT_FIELDREF:
                constantPoolList.add(new ConstantFieldRefInfo(tag, classFile));
                break;
            case CONSTANT_METHODREF:
                constantPoolList.add(new ConstantMethodRefInfo(tag, classFile));
                break;
            case CONSTANT_INTERFACEMETHODREF:
                constantPoolList.add(new ConstantInterfaceMethodRefInfo(tag, classFile));
                break;
            case CONSTANT_NAMEANDTYPE:
                constantPoolList.add(new ConstantNameAndTypeInfo(tag, classFile));
                break;
            case CONSTANT_INVOKEDYNAMIC:
                constantPoolList.add(new ConstantInvokedynamicInfo(tag, classFile));
                break;
            case CONSTANT_METHODHANDLE:
                constantPoolList.add(new ConstantMethodhandleInfo(tag, classFile));
                break;
            case CONSTANT_LONG:
                constantPoolList.add(new ConstantLongInfo(tag, classFile));
                constantPoolList.add(new ConstantPool(""));
                break;
            case CONSTANT_DOUBLE:
                constantPoolList.add(new ConstantDoubleInfo(tag, classFile));
                constantPoolList.add(new ConstantPool(""));
                break;
            case CONSTANT_CLASS:
                constantPoolList.add(new ConstantClassInfo(tag, classFile));
                break;
            case CONSTANT_STRING:
                constantPoolList.add(new ConstantStringInfo(tag, classFile));
                break;
            case CONSTANT_METHODTYPE:
                constantPoolList.add(new ConstantMethodType(tag, classFile));
                break;
            default:
                throw new ClassFormatError("无效常量池类型");
        }
        return constantPoolList;
    }
}

弄到这里,常量池基本就搞定了。constant_pool_countcp_info也就完事了

属性

属性 ?听起来是不是有点误导?很多人一听"属性"就以为是类的字段(field),但这里的"属性",指的可不是我们平时写的private int age这种东西。

其实它也和我们写代码密切相关。想想看,常量池只保存了符号引用和字面量这些静态信息,那像泛型信息、方法参数名、注解、重载方法信息、异常、局部变量表等等这些"动态"又"丰富"的内容,要放哪儿?没错,这些内容就是通过 属性 表结构来保存的。结合我们平时的用到的是不是好理解了些?

属性 和常量池基本类似,都有很多种类型,但是又有基础的信息,如下:

既然都是这种结构,那我们也不客气,定义个基础类,AttributeInfo代码如下:

java 复制代码
/**
 * 属性信息
 *
 * @author hqd
 */

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AttributeInfo {
    /**
     * 属性名称
     */
    private Integer attributeNameIndex;
    /**
     * 属性长度
     */
    private Integer attributeLength;
    /**
     * 属性字节
     */
    private byte[] byteCodes;

    public AttributeInfo(Integer attributeNameIndex, Integer attributeLength) {
        this.attributeNameIndex = attributeNameIndex;
        this.attributeLength = attributeLength;
    }

    protected <T extends AttributeInfo> AttributeInfo newAttributeInfo(Integer attributeNameIndex, Integer attributeLength, ClassFile classFile) {
        return this;
    }

    /**
     * 创建一个新属性
     *
     * @param classFile
     * @param <T>
     * @return
     */
    public <T extends AttributeInfo> AttributeInfo newAttributeInfo(ClassFile classFile) {
        this.attributeNameIndex = HexStrTransformUtil.parseHexToInt(classFile.readU2());
        this.attributeLength = HexStrTransformUtil.parseHexToInt(classFile.readU4());
        return newAttributeInfo(attributeNameIndex, attributeLength, classFile);
    }
}

再定义一个枚举类型来映射属性类型,代码如下:

java 复制代码
/**
 * 属性类型
 */
public enum AttributeType {
    /**
     * 方法表	Java代码编译成的字节码指令
     */
    Code("Code"),
    /**
     * 字段表 final关键字定义的常量池
     */
    ConstantValue("ConstantValue"),
    /**
     * 类,方法
     * 字段表 被声明为deprecated的方法和字段
     */
    Deprecated("Deprecated"),

    /**
     * 方法表
     * 方法抛出的异常
     */
    Exceptions("Exceptions"),


    /**
     * 类文件
     * 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
     */
    EnclosingMethod("EnclosingMethod"),
    /**
     * 类文件
     * 内部类列表
     */
    InnerClasses("InnerClasses"),
    /**
     * Code属性
     * Java源码的行号与字节码指令的对应关系
     */
    LineNumberTable("LineNumberTable"),
    /**
     * Code属性
     * 方法的局部变量描述
     */
    LocalVariableTable("LocalVariableTable"),
    /**
     * Code属性
     * JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
     */
    StackMapTable("StackMapTable"),
    /**
     * 类,方法表
     * 字段表 用于支持泛型情况下的方法签名
     */
    Signature("Signature"),

    /**
     * 类文件
     * 记录源文件名称
     */
    SourceFile("SourceFile"),
    /**
     * 类文件
     * 用于存储额外的调试信息
     */
    SourceDebugExtension("SourceDebugExtension"),
    /**
     * 类,方法表
     * 字段表 标志方法或字段为编译器自动生成的
     */
    Synthetic("Synthetic"),
    /**
     * 类
     * 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
     */
    LocalVariableTypeTable("LocalVariableTypeTable"),
    /**
     * 类,方法表
     * 字段表 为动态注解提供支持
     */
    RuntimeVisibleAnnotations("RuntimeVisibleAnnotations"),
    /**
     * 表,方法表
     * 字段表 用于指明哪些注解是运行时不可见的
     */
    RuntimeInvisibleAnnotations("RuntimeInvisibleAnnotations"),
    /**
     * 方法表
     * 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
     */
    RuntimeVisibleParameterAnnotations("RuntimeVisibleParameterAnnotations"),
    /**
     * 方法表
     * 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
     */
    RuntimeInvisibleParameterAnnotations("RuntimeInvisibleParameterAnnotations"),
    /**
     * 方法表
     * 用于记录注解类元素的默认值
     */
    AnnotationDefault("AnnotationDefault"),
    /**
     * 类文件用于保存invokeddynamic指令引用的引导方式限定符
     */
    BootstrapMethods("BootstrapMethods");
    private String code;

    AttributeType(String code) {
        this.code = code;
    }

    public static AttributeType getTypeByVal(String code) {
        if (StringUtils.isNotBlank(code)) {
            AttributeType[] attributeTypes = AttributeType.values();
            for (AttributeType type : attributeTypes) {
                if (type.code.equals(code)) {
                    return type;
                }
            }
        }
        return null;
    }

    public String getCode() {
        return code;
    }
}

由于属性种类繁多,其他属性都解析方式都是类似的。这里就以ConstantValue 为例,按照虚拟机规范,ConstantValue 结构如下:

接着就是对应的实体类,代码如下:

java 复制代码
/**
 * 常量表达式
 *
 * @author hqd
 */
@Getter
@NoArgsConstructor
public class ConstantValue extends AttributeInfo {
    /**
     * 常量所在常量池的下标
     */
    private Integer constantValueIndex;

    @Override
    protected ConstantValue newAttributeInfo(Integer attributeNameIndex, Integer attributeLength, ClassFile classFile) {
        this.constantValueIndex = HexStrTransformUtil.parseHexToInt(classFile.readU2());
        return new ConstantValue(attributeNameIndex, attributeLength, constantValueIndex);
    }
}

和常量池一样,我们创建一个工厂类根据属性类型创建对应的属性实体类,不过这个工厂特殊点。属性 类型实在太多,我不想一个个判断,这里偷个懒,根据反射去获取对应的类。AttributeInfoFactory代码如下:

java 复制代码
/**
 * 属性工厂
 *
 * @author hqd
 */
public class AttributeInfoFactory {
    private static final String BASE_PACKAGE = "com.hqd.jjvm.classformat.attributeinfo.";

    private static Class<? extends AttributeInfo> getClassType(AttributeType attributeType) throws ClassNotFoundException {
        Class<? extends AttributeInfo> clazz = null;
        try {
            clazz = (Class<? extends AttributeInfo>) Class.forName((BASE_PACKAGE + attributeType.getCode()).intern());
        } catch (ClassNotFoundException e) {
            clazz = (Class<? extends AttributeInfo>) Class.forName((BASE_PACKAGE + attributeType.getCode() + "Attribute").intern());
        }
        return clazz;
    }

    public static AttributeInfo newAttributeInfo(ClassFile classFile, AttributeType attributeType) {
        try {
            Class<? extends AttributeInfo> clazz = getClassType(attributeType);
            // 获取带参构造方法对象
            // public Constructor<T> getConstructor(Class<?>... parameterTypes)
            AttributeInfo attributeInfo = clazz.newInstance();
            attributeInfo.newAttributeInfo(classFile);
            return attributeInfo;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

接口

常量池和属性都有了,再来看下剩余的那几个类型。先来看下接口的信息:

先出现的是接口的个数(2个字节),这个不用说,一个类可以有多个接口,自然是个数组了。可这数组里面的元素应该是什么。继续翻规范看下:

字段

接着是字段信息:




先出现的是字段的个数(2个字节),后边跟着的是每个字段的具体信息,这里由于字段和方法有些公共的信息,所以抽取一个基础类,代码如下:

java 复制代码
/**
 * 类元信息
 *
 * @author hqd
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ClassMemberInfo {
    private Integer accessFlags;
    private Integer nameIndex;
    private Integer descriptorIndex;
    private Integer attributeCount;
    private List<AttributeInfo> attributeInfos;
}

字段的信息只需直接继承即可,代码如下:

java 复制代码
/**
 * 字段信息
 *
 * @author hqd
 */
@AllArgsConstructor
public class FieldInfo extends ClassMemberInfo {
}

方法

方法信息的解析和字段信息类似:


基本一样,就不多赘言了。先定义方法实体类,代码如下:

java 复制代码
/**
 * 方法信息
 *
 * @author hqd
 */
@AllArgsConstructor
public class MethodInfo extends ClassMemberInfo {
}

Class文件结构

现在Class文件结构该有的东西都有了,现在可以定义Class文件结构对应的类了,ClassFile代码如下:

java 复制代码
/**
 * 类结构信息
 *
 * @author hqd
 */
@Getter
public class ClassFile extends AbstractClassCode {

    /**
     * 字节码版本
     */
    private ClassVersion version;
    /**
     * 常量池大小
     */
    private Integer constantPoolCount;
    /**
     * 常量池信息
     */
    private List<ConstantPool> constantPools;
    /**
     * 访问标识
     */
    private Integer accessFlags;
    /**
     * 类名索引
     */
    private Integer thisClassIndex;
    /**
     * 父类索引
     */
    private Integer superClassIndex;
    /**
     * 接口数量
     */
    private Integer interfaceCount;
    /**
     * 接口信息
     */
    private List<ConstantClassInfo> interfaces;
    /**
     * 字段数量
     */
    private Integer fieldCount;
    /**
     * 字段信息
     */
    private List<FieldInfo> fieldInfos;
    /**
     * 方法数量
     */
    private Integer methodCount;
    /**
     * 方法信息
     */
    private List<MethodInfo> methodInfos;
    /**
     * 属性数量
     */
    private Integer attributeCount;
    /**
     * 属性信息
     */
    private List<AttributeInfo> attributeInfos;

    public ClassFile(ClassFileInfo classFileInfo) {
        super(classFileInfo);
        parseClassFile();
    }


    protected void parseClassFile() {
        parseClassVersion();
        parseConstantPool();
        parseClassAccessFlags();
        parseThisClassIndex();
        parseSuperClassIndex();
        parseInterfaces();
        parseFieldInfo();
        parseMethodInfo();
        parseAttributeInfo();
    }
	//省略其他方法。。。
}

解析Class文件

现在万事俱备了,再来就是把ClassFileInfo转成对应的ClassFile就行了。ClassParse代码如下:

java 复制代码
/**
 * 类读取器
 *
 * @author hqd
 */
@NoArgsConstructor
@Getter
public class ClassParse {

    private ClassResource classResource;

    public Map<String, ClassFile> parseAll() throws IOException {
        List<ClassFileInfo> list = classResource.readAllClass();
        Map<String, ClassFile> classFileMap = new HashMap<>(list.size());
        for (ClassFileInfo classFileInfo : list) {
            ClassFile classFile = new ClassFile(classFileInfo);
            String className = classFileInfo.getFileName().substring(0, classFileInfo.getFileName().lastIndexOf("."));
            classFileMap.put(className, classFile);
        }
        return classFileMap;
    }

    public ClassFile parseClass(String className) throws IOException {
        className = className.replaceAll("\\.", "/");
        ClassFileInfo classFileInfo = classResource.readClass(className + ClassResource.CLASS_FILE_SUFFIX);
        if (classFileInfo != null) {
            return new ClassFile(classFileInfo);
        }
        return null;
    }

	//省略其他代码。。。。
}

测试

搞定了,测试一波开始验证今天的努力成果。先修改下CmdCommand#startJVM方法:

java 复制代码
   private void startJVM(CmdArgs cmdArgs) {
        ClasspathClassResource cp = new ClasspathClassResource(cmdArgs.getXJreOption(), cmdArgs.getCpOption());
        ClassParse parse = new ClassParse(cp);
        try {
            ClassFile classFile = parse.parseClass(cmdArgs.getMainClass());
            System.out.println(classFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

然后添加个测试类。ClassformatTest代码如下:

java 复制代码
public class ClassformatTest {
    public static void main(String[] args) {
        CmdCommand cmdCommand = new CmdCommand();
        cmdCommand.parseCmd(args);
    }
}

在配置下idea,添加参数-Xjre "D:\Oracle\Java\jdk1.8.0_281\jre" java.lang.Object

双击运行,结果如下:

好嘞,目测没有问题


总结

这一章先聊了一下Class文件结构和Class文件的解析,相对来说比较简单一些,主要弄明白常量池结构和属性结构即可。这里我们还缺少类加载器来加载Class文件,这个留到后边的章节再说**┗(•ω•;)┛**