Heim  >  Artikel  >  Java  >  JVM vertiefendes Lernen – Beispielcode für den Java-Analyse-Klassendateiprozess

JVM vertiefendes Lernen – Beispielcode für den Java-Analyse-Klassendateiprozess

黄舟
黄舟Original
2017-03-18 10:47:351764Durchsuche

Vorwort:

Wie kann ich als JavaProgrammierer JVM nicht verstehen? Wenn Sie JVM lernen möchten, müssen Sie die Datei Klasse verstehen ist für die virtuelle Maschine, genau wie ein Fisch für Wasser, die virtuelle Maschine lebt aufgrund der Klasse. „Vertiefendes Verständnis von Java Virtual Machine“ verbringt ein ganzes Kapitel damit, Klassendateien zu erklären, aber nachdem ich es gelesen habe, bin ich immer noch verwirrt und habe es nur halb verstanden. Ich habe vor einiger Zeit zufällig ein sehr gutes Buch gelesen: „Write Your Own Java Virtual Machine“. Der Autor hat die Go-Sprache verwendet, um eine einfache JVM zu implementieren, obwohl sie nicht alle Funktionen der JVM vollständig implementiert Für diejenigen, die sich ein wenig für die JVM interessieren, ist die Lesbarkeit immer noch sehr hoch. Der Autor erklärt es ausführlich und jeder Prozess ist in ein Kapitel unterteilt, in dem ein Teil erklärt, wie Klassendateien analysiert werden.

Dieses Buch ist nicht zu dick und ich habe es schnell gelesen. Nachdem ich es gelesen hatte, habe ich viel gewonnen. Aber es war nur eine Frage der Zeit, bis ich es auf dem Papier lernen würde, und ich wusste, dass ich es im Detail machen musste, also habe ich versucht, die Klassendatei selbst zu analysieren. Obwohl die Go-Sprache hervorragend ist, beherrsche ich sie doch nicht, insbesondere wenn ich nicht an die Syntax gewöhnt bin, in der Typen nach

Variablen eingefügt werden, also bleibe ich besser bei Java.

Klassendatei

Was ist eine Klassendatei?

Der Grund, warum Java plattformübergreifend sein kann, liegt darin, dass in der Kompilierungsphase der Code nicht direkt in die plattformbezogene Maschinensprache kompiliert wird, sondern zunächst in die binäre Form des Java-Bytecodes kompiliert und in die Klassendatei eingefügt wird , lädt die virtuelle Maschine dann die Klassendatei und analysiert den zum Ausführen des Programms erforderlichen Inhalt. Jede Klasse wird in eine separate Klassendatei kompiliert und die interne Klasse wird auch als unabhängige Klasse zum Generieren einer eigenen Klasse verwendet.

Grundstruktur

Suchen Sie einfach eine Klassendatei und öffnen Sie sie mit Sublime Text wie folgt:

屏幕快照 2017-02-06 上午8.44.42.png

Sind Sie jedoch verwirrt? Das Grundformat von Klassendateien ist in der Java Virtual Machine-Spezifikation angegeben. Sie müssen es nur gemäß diesem Format analysieren:

ClassFile {
    u4 magic;
       u2 minor_version;
       u2 major_version;
       u2 constant_pool_count;
       cp_info constant_pool[constant_pool_count-1];
       u2 access_flags;
       u2 this_class;
       u2 super_class;
       u2 interfaces_count;
       u2 interfaces[interfaces_count];
       u2 fields_count;
       field_info fields[fields_count];
       u2 methods_count;
      method_info methods[methods_count];
       u2 attributes_count;
       attribute_info attributes[attributes_count];
}
Die Feldtypen in ClassFile sind u1, u2 und u4 das? Wolltuch? Tatsächlich ist es sehr einfach, es bedeutet 1 Byte, 2 Byte bzw. 4 Byte. Die ersten vier Bytes von

sind:

Magic, die zur eindeutigen Identifizierung des Dateiformats verwendet werden. Sie werden im Allgemeinen als magische Nummer bezeichnet, damit die virtuelle Maschine die geladene Datei erkennen kann Es liegt im Klassenformat vor, die magische Anzahl an Klassendateien ist Cafebabe. Nicht nur Klassendateien, im Grunde haben die meisten Dateien eine magische Nummer, um ihr Format zu identifizieren.

Der nächste Teil enthält hauptsächlich einige Informationen zur Klassendatei, z. B. Konstantenpool, Klassenzugriffsflags, übergeordnete Klasse, Schnittstelleninformationen, Felder, Methoden usw. Spezifische Informationen finden Sie unter „Java Virtual Machine“. Spezifikation".

Parsing

Feldtyp

Wie oben erwähnt, sind die Feldtypen in ClassFile u1, u2, u4, die jeweils 1 Byte, 2 Byte und 4 Ganzzahl darstellen von Bytes. In Java sind short, int und long vorzeichenbehaftete Ganzzahlen von jeweils 2, 4 und 8 Byte. Ohne das Vorzeichenbit können sie zur Darstellung von u1, u2 und u4 verwendet werden.

public class U1 {
    public static short read(InputStream inputStream) {
        byte[] bytes = new byte[1];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        short value = (short) (bytes[0] & 0xFF);
        return value;
    }
}

public class U2 {
    public static int read(InputStream inputStream) {
        byte[] bytes = new byte[2];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        int num = 0;
        for (int i= 0; i < bytes.length; i++) {
            num <<= 8;
            num |= (bytes[i] & 0xff);
        }
        return num;
    }
}                                                                                                                                                                                   

public class U4 {
    public static long read(InputStream inputStream) {
        byte[] bytes = new byte[4];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long num = 0;
        for (int i= 0; i < bytes.length; i++) {
            num <<= 8;
            num |= (bytes[i] & 0xff);
        }
        return num;
    }
}
Konstanter Pool

Nachdem wir den Feldtyp definiert haben, müssen wir zunächst grundlegende Informationen wie magische Zahlen lesen:

FileInputStream inputStream = new FileInputStream(file);
ClassFile classFile = new ClassFile();
classFile.magic = U4.read(inputStream);
classFile.minorVersion = U2.read(inputStream);
classFile.majorVersion = U2.read(inputStream);
Dieser Teil ist nur ein Aufwärmen, der nächste große Teil ist das Dauerbecken. Bevor wir den konstanten Pool analysieren, erklären wir zunächst, was der konstante Pool ist.

Konstantenpool ist, wie der Name schon sagt, ein Ressourcenpool, der Konstanten speichert. Die Konstanten beziehen sich hier auf Literale und Symbolreferenzen. Literale beziehen sich auf einige

-String--Ressourcen, und Symbolreferenzen sind in drei Kategorien unterteilt: Klassensymbolreferenzen, Methodensymbolreferenzen und Feldsymbolreferenzen. Durch das Platzieren von Ressourcen im Konstantenpool können andere Elemente direkt als Indizes im Konstantenpool definiert werden, wodurch Platzverschwendung vermieden wird. Dies gilt nicht nur für die Klassendatei, sondern auch für die ausführbare Datei String Ressourcen usw. werden in DexData platziert, und andere Elemente suchen Ressourcen über Indizes. Die Java Virtual Machine-Spezifikation gibt das Format jedes Elements im Konstantenpool an: Das obige Format ist nur ein allgemeines Format. Der Konstantenpool enthält tatsächlich 14 Datenformate Tag-Werte sind unterschiedlich, wie unten gezeigt:

cp_info {
    u1 tag;
    u1 info[]; 
}

Da es zu viele Formate gibt, wird zur Erläuterung nur ein Teil des Artikels ausgewählt: 屏幕快照 2017-02-06 下午2.20.08.png

Hier lesen wir zuerst die Größe des Konstantenpools, initialisieren den Konstantenpool:

Als nächstes lesen wir jedes Element einzeln und speichern es im

Array
//解析常量池
int constant_pool_count = U2.read(inputStream);
ConstantPool constantPool = new ConstantPool(constant_pool_count);
constantPool.read(inputStream);
cpInfo Beachten Sie hier, dass der Index cpInfo[] bei 1 beginnt. Anfangs ist 0 ungültig und die tatsächliche konstante Poolgröße ist konstant_pool_count-1.

Werfen wir zunächst einen Blick auf das CONSTANT_Utf8-Format. Dieses Element speichert eine MUTF-8-codierte Zeichenfolge:

public class ConstantPool {
    public int constant_pool_count;
    public ConstantInfo[] cpInfo;

    public ConstantPool(int count) {
        constant_pool_count = count;
        cpInfo = new ConstantInfo[constant_pool_count];
    }

    public void read(InputStream inputStream) {
        for (int i = 1; i < constant_pool_count; i++) {
            short tag = U1.read(inputStream);
            ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag);
            constantInfo.read(inputStream);
            cpInfo[i] = constantInfo;
            if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) {
                i++;
            }
        }
    }
}

Wie ist dieses Element zu lesen?

public class ConstantUtf8 extends ConstantInfo {
    public String value;

    @Override
    public void read(InputStream inputStream) {
        int length = U2.read(inputStream);
        byte[] bytes = new byte[length];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            value = readUtf8(bytes);
        } catch (UTFDataFormatException e) {
            e.printStackTrace();
        }
    }

    private String readUtf8(byte[] bytearr) throws UTFDataFormatException {
        //copy from java.io.DataInputStream.readUTF()
    }
}

很简单,首先读取这一项的字节数组长度,接着调用readUtf8(),将字节数组转化为String字符串。

再来看看CONSTANT_Class这一项,这一项存储的是类或者接口的符号引用:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

注意这里的name_index并不是直接的字符串,而是指向常量池中cpInfo数组的name_index项,且cpInfo[name_index]一定是CONSTANT_Utf8格式。

public class ConstantClass extends ConstantInfo {
    public int nameIndex;

    @Override
    public void read(InputStream inputStream) {
        nameIndex = U2.read(inputStream);
    }
}

常量池解析完毕后,就可以供后面的数据使用了,比方说ClassFile中的this_class指向的就是常量池中格式为CONSTANT_Class的某一项,那么我们就可以读取出类名:

int classIndex = U2.read(inputStream);
ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex];
ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex];
classFile.className = className.value;
System.out.print("classname:" + classFile.className + "\n");

字节码指令

解析常量池之后还需要接着解析一些类信息,如父类、接口类、字段等,但是相信大家最好奇的还是java指令的存储,大家都知道,我们平时写的java代码会被编译成java字节码,那么这些字节码到底存储在哪呢?别急,讲解指令之前,我们先来了解下ClassFile中的method_info,其格式如下:

method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

method_info里主要是一些方法信息:如访问标志、方法名索引、方法描述符索引及属性数组。这里要强调的是属性数组,因为字节码指令就存储在这个属性数组里。属性有很多种,比如说异常表就是一个属性,而存储字节码指令的属性为CODE属性,看这名字也知道是用来存储代码的了。属性的通用格式为:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

根据attribute_name_index可以从常量池中拿到属性名,再根据属性名就可以判断属性种类了。

Code属性的具体格式为:

Code_attribute {
    u2 attribute_name_index; u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length; 
    {
        u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

其中code数组里存储就是字节码指令,那么如何解析呢?每条指令在code[]中都是一个字节,我们平时javap命令反编译看到的指令其实是助记符,只是方便阅读字节码使用的,jvm有一张字节码与助记符的对照表,根据对照表,就可以将指令翻译为可读的助记符了。这里我也是在网上随便找了一个对照表,保存到本地txt文件中,并在使用时解析成HashMap。代码很简单,就不贴了,可以参考我代码中InstructionTable.java。

接下来我们就可以解析字节码了:

for (int j = 0; j < methodInfo.attributesCount; j++) {
    if (methodInfo.attributes[j] instanceof CodeAttribute) {
        CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j];
        for (int m = 0; m < codeAttribute.codeLength; m++) {
            short code = codeAttribute.code[m];
            System.out.print(InstructionTable.getInstruction(code) + "\n");
        }
    }
}

运行

整个项目终于写完了,接下来就来看看效果如何,随便找一个class文件解析运行:

屏幕快照 2017-02-06 下午3.55.10.png

哈哈,是不是很赞!

Das obige ist der detaillierte Inhalt vonJVM vertiefendes Lernen – Beispielcode für den Java-Analyse-Klassendateiprozess. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn