美文网首页java
深入理解Class文件-2

深入理解Class文件-2

作者: 左手落花 | 来源:发表于2020-03-22 18:39 被阅读0次

    深入理解Class文件-1这篇文章中,大致了解了Class文件的结构,在平时的使用中,我一般都倾向于使用工具来解析并查看class文件,如jdk自带 javap 程序,使用:

    javap -c class文件所在路径 。

    平时我个人使用IDEA开发,一般使用 <jclasslib Bytecode Viewer> 这个插件查看。

    作为一名程序员,为了更深入的了解class文件,靠看是不够的,如果能够自己手写解析class文件的代码,并输出其中的内容,才是学会了class文件的结构。

    在开始之前,放一个官方文档的链接
    https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.7

    遇到不懂,先看官方文档。

    待解析的类

    package temp;
    
    import temp.*;
    
    
    public class TestClassFile extends TestSuperClass implements TestInterface{
    
        public static String str = "felix.zhu";
        
        @Override
        public void debug() {
            System.out.println("debug");
        }
        
        public static void main(String[] args) {
            TestClassFile tf = new TestClassFile();
            Runtime.getRuntime().totalMemory();
        }
    }
    

    超类

    package temp;
    
    public class TestSuperClass{
        
        private int value;
        
        public int getValue() {
            return this.value;
        }
    }
    

    接口

    package temp;
    
    interface TestInterface {
        
        public void debug();
    }
    

    将类编译成class文件,放到一个指定的目录

    main() 解析方法

    目前只解析到接口名称,其他的留到后续的文章

            File file = new File("D:\\temp\\TestClassFile.class");
            DataInput in = getInputStream(file);
            ClassFileReader classFileReader = new ClassFileReader();
            //读取魔数
            classFileReader.readMagic(in);
            //读取版本号
            classFileReader.readVersion(in);
            //读取常量池
            classFileReader.readConstansPool(in);
            //读取访问标识
            classFileReader.readAccessFlag(in);
            //读取class名称
            classFileReader.readClass(in);
            //读取superclass
            classFileReader.readSuperClass(in);
            //读取接口名称
            classFileReader.readInterfaces(in);
    

    getInputString()

    public static DataInput getInputStream(File file) throws FileNotFoundException {
    
            return new DataInputStream(
                    new BufferedInputStream(
                            new FileInputStream(file)));
    }
    

    由于考虑到字节操作,这里选择DataInputStream,它提供了readInt, readUnsignedShort 等等方法正好适合,尤其喜欢它有一个readUTF函数,由于java的字符串是以Unicode的形式存储的,字符串会经过UTF-8 encode写入到class文件中,读取出来后需要转成可读的字符串就需要写decodeUTF-8函数,这里懒得写,就用现成的了。

    读取魔数readMagic()

    class文件开头四个字节是魔数,我们用readInt读取出来,在java中Int占4个字节,最后转换成16进制就可以看到啦。

    public void readMagic(DataInput input) throws IOException {
            //魔数占4个字节
            int magicNumb = input.readInt();
    
            print("magic : " + Integer.toHexString(magicNumb));
    }
    

    读取版本号readVersion()

    public void readVersion(DataInput input) throws IOException {
            int minorVersion = input.readUnsignedShort();
    
            int majorVersion = input.readUnsignedShort();
            //根据文档描述 实际是 如果两个值都有,最终的版本是  majorVersion.minorVersion
            print("minorVersion: " + minorVersion + " majorVersion: " + majorVersion);
    
            String classVersion = getMajorVersionName(majorVersion);
    
            print("当前class文件版本号:" + classVersion);
    }
    

    根据文档描述读取出来的版本号最小从45开始,为了方便阅读,写一个版本号转换的函数

    public String getMajorVersionName(int majorVersion) {
            switch (majorVersion) {
                case 45:
                    return "1.1";
                case 46:
                    return "1.2";
                case 47:
                    return "1.3";
                case 48:
                    return "1.4";
                case 49:
                    return "1.5";
                case 50:
                    return "1.6";
                case 51:
                    return "1.7";
                case 52:
                    return "1.8";
                case 53:
                    return "1.9";
                default:
                    return "unknown value " + majorVersion;
            }
      }
    

    读取常量池readConstansPool()

    public void readConstansPool(DataInput input) throws IOException {
           //开头两个字节是常量池的大小
            int poolCount = input.readUnsignedShort();
    
            indexItems = new int[poolCount]; //常量池index索引数组
            strings = new String[poolCount];
    
            //根据文档描述 The constant_pool table is indexed from 1 to constant_pool_count-1.
            for (int i = 1; i < poolCount; ++i) {
                byte tag = input.readByte();
    
                switch (tag) {
                    case FIELD:
                    case METH:
                    case IMETH:
                    case INT:
                    case FLOAT:
                    case NAME_TYPE:
                        input.skipBytes(4);
                        break;
                    case CLASS:
                        indexItems[i] = input.readUnsignedShort();
                        break;
                    case LONG:
                    case DOUBLE:
                        input.skipBytes(8);
                        break;
    
                    case UTF8:
                        strings[i] = input.readUTF();
                        break;
    
                    default:
                        input.skipBytes(2);
                        break;
    
                }
            }
        }
    

    1 根据文档的描述常量池下标从1开始,i初始化为1
    2 indexItems 数组理解成存储对应常量池数据的下标,后面读取的数据都是根据下标到常量池里找数据。
    3 strings 用来存储对应下标的String值
    4 文档描述常量池里的类型如下:


    image.png

    常量池里的每个元素都有可能是这些类型当中的其中一个。
    这里用CONSTANT_Class 举例


    image.png
    可以看到开头是一个字节(u1) 的tag,对应上图。
    程序里的switch语句是根据读取到的tag做相应的处理,我定义了全局变量
    static final int CLASS = 7;
    接着u2 name_index表示该class全限定名在常量池的下标。我存到strings这个数组里了。
    其他的类型可以自行参考官方文档,我这里用不到这些类型,所以用skipBytes方法跳过。

    class文件里的字符串是CONSTANT_Utf8_info结构的,如下图


    image.png
    readUTF方法会先读取length,在根据length读取字节数组转换成String。

    readAccessFlag() 读取访问标识

    public void readAccessFlag(DataInput input) throws IOException {
            int flag = input.readUnsignedShort();
    
            List<String> flagNames = getFlagsByName(flag);
    
            print("flags: " + String.join(",", flagNames));
        }
    

    将flag转成Name

    public List<String> getFlagsByName(int flags) {
            int cpFlags = flags;
            List<String> flagsNames = new ArrayList<>();
            for (AccessFlag classAccessFlag : CLASS_ACCESS_FLAGS) {
            //通过 & 操作,如果存在,这添加
                if((cpFlags & classAccessFlag.getFlag()) != 0) {
                    flagsNames.add(flagToName(classAccessFlag));
                    flags = ~classAccessFlag.getFlag() & flags;
                }
            }
            return flagsNames;
        }
    
    public static String flagToName(AccessFlag flag) {
            switch (flag) {
                case ACC_PUBLIC:
                    return "ACC_PUBLIC";
                case ACC_FINAL:
                    return "ACC_FINAL";
                case ACC_SUPER:
                    return "ACC_SUPER";
                case ACC_INTERFACE:
                    return "ACC_INTERFACE";
                case ACC_ABSTRACT:
                    return "ACC_ABSTRACT";
                case ACC_SYNTHETIC:
                    return "ACC_SYNTHETIC";
                case ACC_ANNOTATION:
                    return "ACC_ANNOTATION";
                case ACC_ENUM:
                    return "ACC_ENUM";
                default:
                    return null;
            }
        }
    

    class类的修饰,如果是public final class, 读出来的值就是 0x0011

    image.png

    定义一个枚举表示它

    enum AccessFlag{
    
        ACC_PUBLIC(0x0001, "public"),
        ACC_FINAL(0x0010, "final"),
        ACC_SUPER(0x0020, "Treat superclass methods"),
        ACC_INTERFACE(0x0200, "interface"),
        ACC_ABSTRACT(0x0400, "abstract"),
        ACC_SYNTHETIC(0x1000, "synthetic"),
        ACC_ANNOTATION(0x2000, "annotation"),
        ACC_ENUM(0x4000, "enum");
    
        AccessFlag(int flag, String name) {
            this.flag = flag;
            this.name = name;
        }
    
        private int flag;
    
        private String name;
    
        public int getFlag() {
            return flag;
        }
    
        public String getName() {
            return name;
        }
    }
    

    readClass() 读取class名称

    public void readClass(DataInput input) throws IOException {
            // classInfo结构在常量池的位置
            int classIndex = input.readUnsignedShort();
    
            //class全名称字符串在常量池的位置
            int stringIndex = indexItems[classIndex];
    
            String className = strings[stringIndex];
    
    
            print("class Name: " + className);
    }
    

    这里就用到根据常量池解析出来的indexItems数组和strings数组了

    readSuperClass() 超类

    public void readSuperClass(DataInput input) throws IOException {
            int superClassIndex = input.readUnsignedShort();
    
            int stringIndex = indexItems[superClassIndex];
    
            String superClassName = strings[stringIndex];
    
    
            print("superClass Name: " + superClassName);
        }
    

    和读取class名称一样

    readInterfaces() 接口

    public void readInterfaces(DataInput input) throws IOException {
            //有效索引范围为0~interfaces_count
            int interfaceCount = input.readUnsignedShort();
            String[] interfaces = new String[interfaceCount];
            for(int i = 0; i < interfaceCount; i++) {
                int interfaceIndex = input.readUnsignedShort();
                int stringIndex = indexItems[interfaceIndex];
                String interfaceName = strings[stringIndex];
                interfaces[i] = interfaceName;
            }
    
            print("interfaces Name: " + String.join(",",interfaces));
        }
    

    java接口是多继承的,可能会有多个,class文件里是数组

    最后的结果

    image.png

    结尾

    可以看到,虽然看class文件格式觉得自己理解了,但是解析class文件还是比较复杂的,通过自己动手解析class文件,能加深对class文件的理解,如果你有其他更好的解析class文件的方法,欢迎加入一起探讨。

    我是左手落花,一个喜欢通过coding来探究原理的人。

    相关文章

      网友评论

        本文标题:深入理解Class文件-2

        本文链接:https://www.haomeiwen.com/subject/zgublhtx.html