Lors du démarrage d'un langage de programmation, il est courant d'imprimer "Hello World". Surtout quand je dois l'utiliser pour les affaires, j'écrirai Hello World à la vitesse de gokiburi dash et j'essaierai de passer à l'étape suivante.
Mais ai-je compris comment fonctionnait ce Hello World? Cette fois, je voudrais revenir au début et revoir Hello World dans la mesure où je ne peux pas être dérouté par le Dr Retsu, "Kisama a ridiculisé Hello World."
Windows 10 java version "1.8.0_202" Java(TM) SE Runtime Environment (build 1.8.0_202-b08) Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
Créez et exécutez Hello World comme d'habitude. Tout d'abord, écrivez le code java dans un éditeur de texte.
HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
Exécutez la commande javac avec le fichier java créé comme entrée pour créer un fichier de classe. Cette fois, ajoutez l'option "-g: none" pour empêcher la génération d'informations de débogage.
javac -g:none HelloWorld.java
Exécutez le "HelloWorld.class" créé avec la commande java.
>java HelloWorld
Hello World
"Hello World" est sorti. Contrairement aux fichiers exécutables créés avec c ou c ++, le HelloWorld.class créé fonctionne de la même manière sous Windows, Mac et Linux tant qu'il contient Java. Non, Java est pratique.
**la fin! !! Fermé! !! c'est tout! !! Tout le monde s'est dissous! !! ** **
C'était la fin. Oui, je n'ai pas eu l'occasion de penser à ce qu'est cette HelloWorld.class. Cette fois, c'est un gros problème, alors vérifions le contenu du fichier binaire créé.
Le contenu de HelloWorld.class est constitué des binaires suivants.
Pour lire ce fichier binaire, vous devez lire la spécification de la machine virtuelle Java®. https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
Je décrirai brièvement les spécifications de la JVM utilisée cette fois, mais si vous voulez analyser rapidement le binaire du fichier de classe, veuillez [Ignorer](analyse binaire de #helloworldclass).
class La structure du fichier est la suivante.
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];
}
u1 représente 1 octet, u2 représente 2 octets et u4 représente 4 octets de données. D'autres sont des structures, donc je vous expliquerai à chaque fois.
magic est un nombre magique qui identifie le format de fichier de classe et est "0xCAFEBABE". L'origine de ce nombre magique est décrite dans CAFE BABE: Java's Magic Word, veuillez donc le lire. Je l'utilise à une date et il est dessiné.
minor_version et major_version sont les numéros de version mineure et majeure de ce fichier de classe. Les numéros de version majeure et mineure déterminent la version du format de fichier de classe.
constant_pool [] et constant_pool_count sont la table et le nombre de structures qui représentent les constantes de chaîne, les noms de classe et d'interface, les noms de champ et d'autres constantes. La plage valide pour l'index de constant_pool est de 1 à constant_pool_count-1.
access_flags est une combinaison des indicateurs suivants.
Nom du drapeau | valeur | Interprétation |
---|---|---|
ACC_PUBLIC | 0x0001 | public est déclaré. Il est accessible depuis l'extérieur du package. |
ACC_FINAL | 0x0010 | final est déclaré. Les sous-classes ne sont pas autorisées. |
ACC_SUPER | 0x0020 | Traite les méthodes de superclasse spécialement lorsqu'elles sont appelées par l'instruction invokespecial. |
ACC_INTERFACE | 0x0200 | C'est une interface, pas une classe. |
ACC_ABSTRACT | 0x0400 | Il a été déclaré abstrait. N'instanciez pas. |
ACC_SYNTHETIC | 0x1000 | Indique qu'il a été généré par le compilateur. * Par exemple, un fichier de classe créé lorsqu'une classe est créée dans une classe(ex. Hello$Test.class)A été donné à |
ACC_ANNOTATION | 0x2000 | Déclaré comme une annotation. |
ACC_ENUM | 0x4000 | Déclaré comme type enum. |
this_class doit être une valeur d'index valide dans constant_pool []. Les données référencées dans l'index doivent être une structure CONSTANT_Class_info qui contient les informations pour la classe spécifiée dans le fichier.
super_class doit être 0 ou une valeur d'index valide dans constant_pool []. Les données référencées dans l'index doivent être une structure CONSTANT_Class_info qui représente la superclasse directe de la classe définie dans ce fichier de classe.
interfaces_count et interfaces [] sont un tableau d'index dans la structure CONSTANT_Class_info qui représente les interfaces de la classe définie dans ce fichier de classe. S'il n'y a pas d'interfaces, interfaces_count sera égal à 0 et les interfaces [] n'existeront pas.
field_count, fields [] représentent les champs de la classe définie dans ce fichier de classe [structure de champ](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html C'est un tableau de # jvms-4.5). S'il n'y a pas de champs, fields_count sera égal à 0 et les champs [] n'existeront pas.
methods_count et methods [] sont un tableau de [structures de méthode](#method structures) qui représentent les méthodes de la classe définie dans ce fichier de classe. S'il n'y a pas de champs, methods_count sera égal à 0 et les méthodes [] n'existeront pas.
attributes_count et attributes [] sont un tableau de [structures d'attributs](# structures d'attributs) qui représentent les informations d'attribut pour les classes définies dans ce fichier de classe.
constant_pool Cette structure est une balise de 1 octet qui détermine à quoi ressemblera la structure.
Constant Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
Nous expliquerons ce que nous utiliserons cette fois-ci, veuillez donc vous référer à ce qui suit pour les autres structures. https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140
CONSTANT_Class Utilisé pour représenter une classe ou une interface.
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
7 représentant CONSTANT_Class est stocké dans la balise. La valeur de l'élément name_index sera l'index de la [structure CONSTANT_Utf8_info](# constant_utf8) dans la table constant_pool.
CONSTANT_Fieldref
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
La balise contient 9 qui représente la valeur CONSTANT_Fieldref_info. La valeur de class_index est l'index de la structure CONSTANT_Class_info dans la table constant_pool. La valeur de l'élément name_and_type_index est l'index de la structure CONSTANT_NameAndType_info (#constant_nameandtype) dans la table constant_pool.
CONSTANT_Methodref
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
La balise contient 10 qui représente la valeur CONSTANT_Methodref. La valeur de class_index est l'index de la structure CONSTANT_Class_info dans la table constant_pool. La valeur de l'élément name_and_type_index est l'index de la structure CONSTANT_NameAndType_info (#constant_nameandtype) dans la table constant_pool.
CONSTANT_String
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
La balise contient 8 qui représente la valeur CONSTANT_String. La valeur de string_index est l'index de la [structure CONSTANT_Utf8_info](# constant_utf8) dans la table constant_pool.
CONSTANT_NameAndType Utilisé pour représenter un champ ou une méthode. Cependant, il n'indique pas le type de classe ou d'interface auquel il appartient.
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
La balise contient 12 qui représente la valeur CONSTANT_NameAndType. La valeur du champ name_index doit être un index valide de la [structure CONSTANT_Utf8_info](# constant_utf8) dans constant_pool.
CONSTANT_Utf8
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
La balise contient 1 qui représente la valeur CONSTANT_Utf8. length représente le nombre d'octets dans le tableau d'octets (pas la longueur de la chaîne) Le tableau d'octets contient les octets de la chaîne. De plus, le caractère terminal n'est pas inclus. Voir ci-dessous pour plus d'informations sur cette chaîne. https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.7
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes [attributes_count];
}
La valeur de l'élément access_flags est une combinaison d'autorisations sur cette méthode et les indicateurs utilisés pour indiquer les propriétés de cette méthode.
Nom du drapeau | valeur | La description |
---|---|---|
ACC_PUBLIC | 0x0001 | Public déclaré. Il est accessible depuis l'extérieur du package. |
ACC_PRIVATE | 0x0002 | Déclaré privé. Uniquement accessible dans la classe de définition. |
ACC_PROTECTED | 0x0004 | Déclaré protégé. Vous pouvez y accéder dans une sous-classe. |
ACC_STATIC | 0x0008 | Déclaré statique. |
ACC_FINAL | 0x0010 | Déclaré définitif. Il ne doit pas être écrasé. |
ACC_SYNCHRONIZED | 0x0020 | Déclaré synchronisé. |
ACC_BRIDGE | 0x0040 | Utilisé pour indiquer la méthode de pont générée par le compilateur pour le langage de programmation Java.Java Generics - Bridge method?Prière de se référer à. |
ACC_VARARGS | 0x0080 | Déclaré avec un nombre variable d'arguments. |
ACC_NATIVE | 0x0100 | Déclaré natif. Il est implémenté dans des langages autres que Java. |
ACC_ABSTRACT | 0x0400 | Résumé déclaré. Aucune implémentation n'est fournie. |
ACC_STRICT | 0x0800 | Déclaré strictfp. |
ACC_SYNTHETIC | 0x1000 | Indique qu'il n'apparaît pas dans le code source généré par le compilateur. |
La valeur de name_index est l'index de la [structure CONSTANT_Utf8_info](# constant_utf8) dans la table constant_pool. Contient le nom de la méthode ou
La valeur de descriptor_index est l'index de la [structure CONSTANT_Utf8_info](# constant_utf8) dans la table constant_pool. Contient le descripteur de méthode (https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3).
attributes_count et attributes [] sont un tableau de [structures d'attributs](# structures d'attributs) qui représentent les informations d'attribut pour les classes définies dans ce fichier de classe.
La forme de cette structure change en fonction de l'attribut. Le format commun est le suivant.
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info [attribute_length];
}
La valeur de attribute_name_index est l'index de la [structure CONSTANT_Utf8_info](# constant_utf8) dans la table constant_pool. attribute_length représente la longueur des informations suivantes en octets. Les informations sont différentes pour chaque attribut.
attribut | Location |
---|---|
SourceFile | ClassFile |
InnerClasses | ClassFile |
EnclosingMethod | ClassFile |
SourceDebugExtension | ClassFile |
BootstrapMethods | ClassFile |
ConstantValue | field_info |
Code | method_info |
Exceptions | method_info |
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations | method_info |
AnnotationDefault | method_info |
MethodParameters | method_info |
Synthetic | ClassFile, field_info, method_info |
Deprecated | ClassFile, field_info, method_info |
Signature | ClassFile, field_info, method_info |
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations | ClassFile, field_info, method_info |
LineNumberTable | Code |
LocalVariableTable | Code |
LocalVariableTypeTable | Code |
StackMapTable | Code |
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations | ClassFile, field_info, method_info, Code |
Voir ci-dessous pour les éléments non couverts ici. https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7
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];
}
attribut_name_index et attribute_length sont décrits dans un format courant. Le caractère spécifié par attribut_name_index doit être "Code".
La valeur de l'élément max_stack est la profondeur maximale de la pile d'opérateurs de cette méthode. Ce sera.
La valeur de l'élément max_locals correspond aux variables locales (https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.1) affectées lors de l'appel de cette méthode. ).
La valeur de l'élément code_length est le numéro de code [].
Le tableau de code affiche les octets réels du code de la machine virtuelle Java qui implémente la méthode. Ce code est décrit ci-dessous. https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
exception_table_length stocke le nombre d'entrées pour exception_table.
exception_table représente les informations d'exception. Le contenu de chaque élément de exception_table est le suivant. -Start_pc, end_pc: indique la valeur d'index du tableau de codes pour lequel le gestionnaire d'exceptions est activé. En code java, c'est la plage entourée par try ward. -L'élément de handler_pc est la valeur d'index du tableau de code démarré par le gestionnaire d'exceptions. En code java, c'est la plage entourée par la zone de capture. -Catch_type est 0 ou un index valide de la table constant_pool, et cet index est la structure CONSTANT_Class_info qui représente la classe d'exception.
attributes_count et attributes [] sont un tableau de [structures d'attributs](# structures d'attributs) qui représentent les informations d'attribut pour les classes définies dans ce fichier de classe.
Le frère du langage machine sera capable de lire les nombres hexadécimaux en utilisant n'importe quel éditeur binaire, mais honnêtement, il est difficile de lire les nombres hexadécimaux à l'ère de Reiwa, alors considérez un éditeur binaire qui semble être aussi facile à lire que possible. J'ai essayé de.
Cette fois, nous utiliserons BZ Editor. Les raisons d'adopter BZ Editor sont les suivantes. ・ Peut être utilisé sur Windows (récemment, il semble qu'il puisse être utilisé sur MacOS s'il est construit) ・ La structure peut être affichée. ・ Hino peut utiliser la vraie langue -Comme la source est ouverte au public, elle peut être étendue si vous en avez envie. https://github.com/devil-tamachan/binaryeditorbz
Si vous souhaitez envisager d'autres éditeurs binaires, consultez Wikipedia pour un tableau de comparaison des éditeurs binaires. https://en.wikipedia.org/wiki/Comparison_of_hex_editors Parmi eux, HxD semblait être facile à utiliser.
Vous pouvez définir la structure dans Bz.def, qui se trouve dans le même dossier que le fichier exécutable de BZ Editor. Notez que seules des structures de taille fixe peuvent être spécifiées, donc l'analyse ne peut pas être effectuée parfaitement.
Bz.def
struct ClassFile_1 {
BYTE magic[4];
short minor_version;
short majoir_version;
short constant_pool_count;
} class;
struct ClassFile_2 {
BYTE access_flags[2];
short this_class;
short super_class;
short interfaces_count;
} class;
struct CONSTANT_Class {
BYTE tag;
short index;
} class;
struct CONSTANT_Methodref_info {
BYTE tag;
short class_index;
short name_and_type_index;
} class;
struct CONSTANT_Fieldref {
BYTE tag;
short class_index;
short name_and_type_index;
} class;
struct CONSTANT_NameAndType_info {
BYTE tag;
short name_index;
short descriptor_index;
} class;
struct CONSTANT_String_info {
BYTE tag;
short string_index;
} class;
struct CONSTANT_Utf8 {
BYTE tag;
short length;
} class;
struct Code_attribute {
short attribute_name_index;
int attribute_length;
short max_stack;
short max_locals;
int code_length;
} class;
Ce BZ.def peut être écrit comme le langage C. Voir TYPESTR [NUM_MEMBERS] dans le code ci-dessous pour les types que vous pouvez utiliser. https://github.com/devil-tamachan/binaryeditorbz/blob/master/Bz/BZFormVw.cpp
Après avoir démarré BZEditor, cochez «Affichage»> «Affichage de la structure» pour afficher la fenêtre enfant pour l'affichage de la structure.
En double-cliquant sur une adresse, les informations de structure à partir de cette adresse s'affichent.
Sélectionnez Motorola lors de l'analyse du fichier de classe.
Analysons ClassFile depuis le début.
"0x CAFE BABE" s'affiche pour la magie. la version mineure est 0 major_version est 52. constant_pool_count sera 26, et l'octet suivant sera l'entrée pour constant_pool.
constant_pool[1] Puisque le premier octet est 0x0A = 10, l'entrée pour ce constant_pool sera CONSTANT_Methodref. class_index vaut 6 et name_and_type_index vaut 12. Après avoir examiné tous les constants_pools, vérifiez ce que ces index indiquent réellement.
constant_pool[2] Puisque le premier octet est 0x09, l'entrée pour ce constant_pool sera CONSTANT_Fieldref. class_index vaut 13 et name_and_type_index vaut 14.
constant_pool[3]
Comme le premier octet est 0x08, l'entrée pour constant_pool sera CONSTANT_String. L'indice sera de 15.
constant_pool[4]
Puisque le premier octet est 0x0A = 10, l'entrée pour ce constant_pool sera CONSTANT_Methodref. class_index vaut 16 et name_and_type_index vaut 17.
constant_pool[5]
Étant donné que le premier octet est 0x07, l'entrée pour ce pool_ constant sera CONSTANT_Class. L'indice sera de 18.
constant_pool[6]
Étant donné que le premier octet est 0x07, l'entrée pour ce pool_ constant sera CONSTANT_Class. L'indice sera de 19.
constant_pool[7]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 6 et les 6 octets suivants stockent les caractères "\ <init >".
constant_pool[8]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 3 et le caractère "() V" est stocké dans les 3 octets suivants.
constant_pool[9]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 4 et les 4 octets suivants stockent le caractère "Code".
constant_pool[10]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 4, et les 4 octets après cela stockent le caractère "main".
constant_pool[11]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 22, et les 22 octets après cela stockent le caractère "([Ljava / lang / String;) V".
constant_pool[12]
Étant donné que le premier octet est 0x0C = 12, l'entrée pour ce pool_ constant sera la structure CONSTANT_NameAndType_info. name_index vaut 7 et descriptor_index vaut 8.
constant_pool[13]
Étant donné que le premier octet est 0x07, l'entrée pour ce pool_ constant sera CONSTANT_Class. L'indice sera de 20.
constant_pool[14]
Étant donné que le premier octet est 0x0C = 12, l'entrée pour ce pool_ constant sera la structure CONSTANT_NameAndType_info. name_index sera 21 et descriptor_index sera 22.
constant_pool[15]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 11 et les 11 octets suivants stockent les caractères "Hello World".
constant_pool[16]
Étant donné que le premier octet est 0x07, l'entrée pour ce pool_ constant sera CONSTANT_Class. L'indice sera 23.
constant_pool[17]
Étant donné que le premier octet est 0x0C = 12, l'entrée pour ce pool_ constant sera la structure CONSTANT_NameAndType_info. name_index vaut 24 et descriptor_index vaut 25.
constant_pool[18]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 10 et les 10 octets suivants stockent les caractères "Hello World".
constant_pool[19]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 16 et les 16 octets suivants stockent les caractères "java / lang / Object".
constant_pool[20]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 16 et les 16 octets suivants stockent les caractères "java / lang / System".
constant_pool[21]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 3 et le caractère «out» est stocké dans les 3 octets suivants.
constant_pool[22] Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 21 et les 21 octets qui suivent stockent les caractères "Ljava / io / PrintStream;".
constant_pool[23]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 19 et les 19 octets suivants stockent les caractères «java / io / PrintStream».
constant_pool[24]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 7 et les 7 octets après cela stockent le caractère "println".
constant_pool[25]
Étant donné que le premier octet est 0x01, l'entrée pour ce pool_constants sera [CONSTANT_Utf8](# constant_utf8). La longueur est de 21, et les 21 octets après cela stockent le caractère "(Ljava / lang / String;) V".
Le constant_pool peut être résumé comme suit.
No | Structure | Contenu |
---|---|---|
1 | CONSTANT_Methodref | class_l'indiceest6:HelloWorld、name_and_type_l'indiceest12: |
2 | CONSTANT_Fieldref | class_l'indice est 13:java/lang/System、name_and_type_l'indice est 14: out:Ljava/io/PrintStream; |
3 | CONSTANT_String | l'indice est 15:「Hello World」 |
4 | CONSTANT_Methodref | class_l'indiceest16:java/io/PrintStream、name_and_type_l'indiceest17:println:(Ljava/lang/String;)V |
5 | CONSTANT_Class | l'indice est 18:「HelloWorld」 |
6 | CONSTANT_Class | l'indice est 19:「java/lang/Object」 |
7 | CONSTANT_Utf8 | 「<init>Est la chaîne de caractères |
8 | CONSTANT_Utf8 | 「()La chaîne "V" |
9 | CONSTANT_Utf8 | La chaîne "Code" |
10 | CONSTANT_Utf8 | La chaîne "main" |
11 | CONSTANT_Utf8 | 「([Ljava/lang/String;)La chaîne "V" |
12 | CONSTANT_NameAndType_info | name_l'indiceest7:「<init>」,descriptor_l'indiceest8:「()V」。MethodDescriptorsVoir |
13 | CONSTANT_Class | l'indice est 20;「java/lang/System」 |
14 | CONSTANT_NameAndType_info | name_l'indiceest21:「out」,descriptor_l'indiceest22:「Ljava/io/PrintStream;」FieldDescriptorsVoir |
15 | CONSTANT_Utf8 | La chaîne "Hello World" |
16 | CONSTANT_Class | l'indice est 23:「java/io/PrintStream」 |
17 | CONSTANT_NameAndType_info | name_l'indiceest24:「println」,descriptor_l'indiceest25:「(Ljava/lang/String;)V」MethodDescriptorsVoir |
18 | CONSTANT_Utf8 | La chaîne "Hello World" |
19 | CONSTANT_Utf8 | 「java/lang/La chaîne "Object" |
20 | CONSTANT_Utf8 | 「java/lang/La chaîne "System" |
21 | CONSTANT_Utf8 | La chaîne "out" |
22 | CONSTANT_Utf8 | 「Ljava/io/PrintStream;Est la chaîne de caractères |
23 | CONSTANT_Utf8 | 「java/io/La chaîne "Print Stream" |
24 | CONSTANT_Utf8 | La chaîne "println" |
25 | CONSTANT_Utf8 | 「(Ljava/lang/String;)La chaîne "V" |
-Access_flags est 0x0021. Autrement dit, ACC_SUPER (0x20) et ACC_PUBLIC (0x01).
-Comme this_class est constant_pool [5], c'est une classe HelloWorld.
-Puisque super_class est constant_pool [6], c'est une classe java / lang / Object.
-Interfaces_count vaut 0, et il n'y a pas d'interfaces ultérieures [].
Puisque fields_count vaut 0, il n'y a aucun fichier à suivre.
methods_count~methods[]
Puisque methods_count vaut 0x0002, il y a 2 cas, suivis de la structure method_info.
method_info[0]
-Access_flags est 0x0001. Autrement dit, ACC_PUBLIC (0x01).
-Name_index sera "\ <init >" de constant_pool [7]. Il s'agit d'un constructeur implicite créé lors de la compilation Java.
-L'index de description sera "() V" de constant_pool [8].
-Attributes_count est égal à 1 et il existe une structure d'attributs.
method_info[0].attributes[0]
Puisque l'attribut_name_index est le "Code" de constant_pool [9], cette structure est la [structure Code_attribute](structure #code_attribute).
attribute_length sera de 17 octets et déterminera la taille de cette structure.
max_stack vaut 1 et max_locals vaut 1.
Le code_length est 5, ce qui signifie que le prochain "0 x 2A B7 00 01 B1" est un code d'octet. 0x2a devient aload_0. Cette instruction place ceci sur la pile d'opérandes.
0xb7 devient invokesplecial.
Cette instruction appelle la méthode avec les 2 octets suivants comme index de constant_pool.
Dans ce cas, il s'agit de "0x00 01", donc constant_pool [1], "java / lang / Object."
0xb1 devient return.
Exception_table_length et attributes_count ont la valeur 0.
method_info[1]
-Access_flags est 0x0009. Autrement dit, ACC_PUBLIC (0x01) et ACC_STATIC (0x08).
-Name_index sera "principal" de constant_pool [10].
-L'index de description sera "([Ljava / lang / String;) V" de constant_pool [11].
-Attributes_count est égal à 1 et il existe une structure d'attributs.
method_info[1].attributes[0]
Puisque l'attribut_name_index est le "Code" de constant_pool [9], cette structure est la [structure Code_attribute](structure #code_attribute).
attribute_length sera de 21 octets et déterminera la taille de cette structure.
max_stack vaut 2 et max_locals vaut 1.
Le code_length est 9, ce qui signifie que le prochain "0 x B2 00 02 12 03 B6 00 04 B1" est un code d'octet.
0xb2 est getstatic. Cette instruction obtient le champ de la classe statique avec les 2 octets suivants comme index de constant_pool. Dans ce cas, il s'agit de "0x00 02", alors sortez "out: Ljava / io / PrintStream" de la classe "java / lang / System", qui est constant_pool [2]. Le résultat obtenu est chargé sur la pile d'opérandes.
0x12 est ldc. Cette instruction utilise l'octet suivant comme index de constant_pool et charge son contenu dans la pile d'opérandes. Dans ce cas, il s'agit de "0x03", donc la chaîne de caractères "Hello World" de constant_pool [3] est chargée sur la pile d'opérandes.
0xb6 est invokevirtual. Cette instruction utilise les 2 octets suivants comme index pour constant_pool pour exécuter sa méthode. Dans ce cas, c'est "0x00 04", donc exécutez println: (Ljava / lang / String;) V de la classe java / io / PrintStream, qui est constant_pool [4].
0xb1 devient return.
Exception_table_length et attributes_count ont la valeur 0.
Puisque attributes_count vaut 0, il n'y a pas de données d'attributs.
De cette façon, vous pouvez analyser les fichiers de classe avec les spécifications JVM et l'éditeur binaire. ** Cependant, vous pouvez analyser avec la commande javap sans utiliser un éditeur binaire aussi gênant. ** **
>javap -v HelloWorld
Classfile /C:/XXXXXXX/HelloWorld.class
Last modified 2019/06/09; size 340 bytes
MD5 checksum 3ee6d0a4b44197baaeb0cec79a0b73d3
public class HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#12 // java/lang/Object."<init>":()V
#2 = Fieldref #13.#14 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #15 // Hello World
#4 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #18 // HelloWorld
#6 = Class #19 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = NameAndType #7:#8 // "<init>":()V
#13 = Class #20 // java/lang/System
#14 = NameAndType #21:#22 // out:Ljava/io/PrintStream;
#15 = Utf8 Hello World
#16 = Class #23 // java/io/PrintStream
#17 = NameAndType #24:#25 // println:(Ljava/lang/String;)V
#18 = Utf8 HelloWorld
#19 = Utf8 java/lang/Object
#20 = Utf8 java/lang/System
#21 = Utf8 out
#22 = Utf8 Ljava/io/PrintStream;
#23 = Utf8 java/io/PrintStream
#24 = Utf8 println
#25 = Utf8 (Ljava/lang/String;)V
{
public HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
C'est plus facile que de lire un binaire. Eh bien, cela a approfondi ma compréhension de Hello World. Je suis heureux.
Explorons un peu plus en détail le fonctionnement de HelloWorld.class.
On dit souvent que Java peut être compilé plusieurs fois. Qu'est-ce que cela signifie? Ceci a été décrit par Tobias Hartmann [Java HotSpot VM](https://www.ethz.ch/content/dam/ethz/special-interest/infk/inst-cs/lst-dam/documents/Education/ Classes / Spring2018 / 210_Compiler_Design / Slides / 2018-Compiler-Design-Guest-Talk.pdf).
Le bytecode créé comme indiqué ci-dessus peut être exécuté soit en code machine compilé avec C1 ou C2, soit dans un interpréteur. De plus, en Java8, ce qui fonctionnait sur l'interpréteur peut changer pour celui compilé sur C1 à partir du milieu, ou il peut être compilé étape par étape en tant qu'interpréteur → C1 → C2.
** Je veux dire, je n'avais aucune idée du fonctionnement de Hello World ... **
Existe-t-il un moyen de savoir si le code d'octet exécuté a été exécuté dans l'interpréteur ou dans le code machine compilé? Vous pouvez le savoir en utilisant "-XX: + PrintCompilation" lors de l'exécution de java.
>java -XX:+PrintCompilation HelloWorld
73 1 3 java.lang.String::hashCode (55 bytes)
74 2 3 java.lang.String::equals (81 bytes)
75 4 n 0 java.lang.System::arraycopy (native) (static)
76 3 4 java.lang.String::charAt (29 bytes)
76 5 3 java.lang.Object::<init> (1 bytes)
78 6 4 sun.misc.ASCIICaseInsensitiveComparator::toLower (16 bytes)
78 7 4 sun.misc.ASCIICaseInsensitiveComparator::isUpper (18 bytes)
79 8 4 java.lang.String::length (6 bytes)
79 9 3 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
80 10 3 java.lang.Character::toLowerCase (9 bytes)
80 11 3 java.lang.CharacterData::of (120 bytes)
81 15 1 java.lang.Object::<init> (1 bytes)
81 5 3 java.lang.Object::<init> (1 bytes) made not entrant
81 12 3 java.lang.CharacterDataLatin1::toLowerCase (39 bytes)
82 13 3 java.lang.CharacterDataLatin1::getProperties (11 bytes)
82 17 3 java.io.WinNTFileSystem::isSlash (18 bytes)
84 16 3 java.lang.AbstractStringBuilder::append (29 bytes)
84 18 s 3 java.lang.StringBuffer::append (13 bytes)
85 14 3 java.lang.Math::min (11 bytes)
86 19 3 java.lang.StringBuilder::append (8 bytes)
88 20 3 java.lang.String::getChars (62 bytes)
90 22 3 java.lang.String::indexOf (70 bytes)
91 21 3 java.util.Arrays::copyOfRange (63 bytes)
92 23 3 java.lang.System::getSecurityManager (4 bytes)
Hello World
Vous pouvez voir quelle méthode a été compilée et exécutée à l'aide de PrintCompilation. Les détails de cette sortie peuvent être trouvés dans Stack Overflow, mais je vais jeter un œil au code source d'Open SDK. ..
Le code source Java 8 peut être obtenu à partir de ce qui suit. https://download.java.net/openjdk/jdk8u40/ri/openjdk-8u40-src-b25-10_feb_2015.zip
Jetons un coup d'œil au code ci-dessous, qui est pensé pour créer le contenu qui est généré lorsque la compilation d'impression est ajoutée dans ce code.
openjdk\hotspot\src\share\vm\compiler\compileBroker.cpp
// ------------------------------------------------------------------
// CompileTask::print_compilation_impl
void CompileTask::print_compilation_impl(outputStream* st, Method* method, int compile_id, int comp_level,
bool is_osr_method, int osr_bci, bool is_blocking,
const char* msg, bool short_form) {
if (!short_form) {
st->print("%7d ", (int) st->time_stamp().milliseconds()); // print timestamp
}
st->print("%4d ", compile_id); // print compilation number
// For unloaded methods the transition to zombie occurs after the
// method is cleared so it's impossible to report accurate
// information for that case.
bool is_synchronized = false;
bool has_exception_handler = false;
bool is_native = false;
if (method != NULL) {
is_synchronized = method->is_synchronized();
has_exception_handler = method->has_exception_handler();
is_native = method->is_native();
}
// method attributes
const char compile_type = is_osr_method ? '%' : ' ';
const char sync_char = is_synchronized ? 's' : ' ';
const char exception_char = has_exception_handler ? '!' : ' ';
const char blocking_char = is_blocking ? 'b' : ' ';
const char native_char = is_native ? 'n' : ' ';
// print method attributes
st->print("%c%c%c%c%c ", compile_type, sync_char, exception_char, blocking_char, native_char);
if (TieredCompilation) {
if (comp_level != -1) st->print("%d ", comp_level);
else st->print("- ");
}
st->print(" "); // more indent
if (method == NULL) {
st->print("(method)");
} else {
method->print_short_name(st);
if (is_osr_method) {
st->print(" @ %d", osr_bci);
}
if (method->is_native())
st->print(" (native)");
else
st->print(" (%d bytes)", method->code_size());
}
if (msg != NULL) {
st->print(" %s", msg);
}
if (!short_form) {
st->cr();
}
}
Le tampon de type est affiché dans la première colonne.
La deuxième colonne est compilation_id et method_attributes. cocmpilation_id est un nombre à 4 chiffres. method_attributes est une combinaison d'indicateurs et s'affiche comme suit.
lettre | conditions |
---|---|
% | Pour la méthode OCR.Dans le cas de InvalidOSREntryBci lorsque MethodCompilation de type enum est défini et qu'il existe InvocationEntryBci et InvalidOSREntryBci. |
s | En cas de synchronisé |
! | exception_Si vous avez un gestionnaire |
b | En cas de blocage |
n | Pour le code natif |
La troisième colonne indique le niveau de compilation lorsque la compilation par niveaux est activée. Cette compilation hiérarchisée peut être contrôlée avec les options -XX: -Tiered Compilation ou + XX: -Tiered Compilation, mais pour Java 8, la valeur par défaut est ON. Le niveau de compilation est le suivant.
level | Contenu |
---|---|
0 | interpreter |
1 | C1 with full optimization (no profiling) |
2 | C1 with limited profiling |
3 | C1 with full profiling |
4 | C2 |
En d'autres termes, le même C1 est divisé en trois étapes.
Le nom de la méthode est affiché dans la 4e colonne.
Regardons maintenant la sortie du premier -XX: + PrintCompilation. Il n'inclut pas le principal de la classe HelloWorld, vous pouvez donc voir que le code est en cours d'exécution dans l'interpréteur.
Le code machine créé en compilant avec C1 et C2 n'est pas sorti dans un fichier, il n'existe qu'en mémoire. Plusieurs étapes sont nécessaires pour le confirmer.
Tout d'abord, obtenez hsdis-amd64.dll qui autorise l'assembleur inversé. Pour Windows, vous pouvez le télécharger depuis: https://sourceforge.net/projects/fcml/files/
Après avoir téléchargé la DLL, passez-la par le chemin.
Exécutez ensuite la commande suivante.
java -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly -XX:+LogCompilation HelloWorld
Un fichier journal est créé dans le répertoire courant et vous pouvez voir quel type de code machine vous avez créé.
Exemple de sortie
Decoding compiled method 0x0000000002d00750:
Code:
RIP: 0x2d008a0 Code size: 0x000001f0
[Entry Point]
[Constants]
# {method} {0x00000000192f4fc0} 'hashCode' '()I' in 'java/lang/String'
# [sp+0x40] (sp of caller)
0x0000000002d008a0: mov r10d,dword ptr [rdx+8h]
0x0000000002d008a4: shl r10,3h
0x0000000002d008a8: cmp r10,rax
0x0000000002d008ab: jne 2c35f60h ; {runtime_call}
0x0000000002d008b1: nop word ptr [rax+rax+0h]
0x0000000002d008bc: nop
[Verified Entry Point]
0x0000000002d008c0: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x0000000002d008c7: push rbp
0x0000000002d008c8: sub rsp,30h
0x0000000002d008cc: mov rax,193e7ac8h
0x0000000002d008d6: mov esi,dword ptr [rax+8h]
0x0000000002d008d9: add esi,8h
0x0000000002d008dc: mov dword ptr [rax+8h],esi
//Abréviation
Le fichier journal qui produit le code machine contient une grande quantité d'informations et il sera difficile de trouver les informations souhaitées. Dans ce cas, vous souhaiterez peut-être naviguer sur JitWatch. https://github.com/AdoptOpenJDK/jitwatch/
Veuillez consulter ce qui suit pour une utilisation détaillée.
** Regardez la compilation JIT sur JIT Watch! ** ** https://www.sakatakoichi.com/entry/2014/12/04/202747
A présent, nous savons que HelloWorld :: main s'exécute sur l'interpréteur. Ensuite, où et comment les commandes suivantes telles que getstatic to return sont-elles traitées?
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
L'équipe de recherche, qui cherchait le code source de l'OpenSDK pour résoudre ce mystère, a finalement trouvé la partie pertinente. J'ai trouvé que des instructions telles que getstatic et ldc sont interprétées et exécutées par runWithChecks () / run () dans bytecodeInterpreter.cpp.
hotspot\src\share\vm\interpreter\bytecodeInterpreter.cpp
/*
* BytecodeInterpreter::run(interpreterState istate)
* BytecodeInterpreter::runWithChecks(interpreterState istate)
*
* The real deal. This is where byte codes actually get interpreted.
* Basically it's a big while loop that iterates until we return from
* the method passed in.
*
* The runWithChecks is used if JVMTI is enabled.
*
*/
#if defined(VM_JVMTI)
void
BytecodeInterpreter::runWithChecks(interpreterState istate) {
#else
void
BytecodeInterpreter::run(interpreterState istate) {
#endif
//Abréviation
#ifndef USELABELS
while (1)
#endif
{
#ifndef PREFETCH_OPCCODE
opcode = *pc;
#endif
// Seems like this happens twice per opcode. At worst this is only
// need at entry to the loop.
// DEBUGGER_SINGLE_STEP_NOTIFY();
/* Using this labels avoids double breakpoints when quickening and
* when returing from transition frames.
*/
opcode_switch:
assert(istate == orig, "Corrupted istate");
/* QQQ Hmm this has knowledge of direction, ought to be a stack method */
assert(topOfStack >= istate->stack_limit(), "Stack overrun");
assert(topOfStack < istate->stack_base(), "Stack underrun");
#ifdef USELABELS
DISPATCH(opcode);
#else
switch (opcode)
#endif
{
CASE(_nop):
UPDATE_PC_AND_CONTINUE(1);
//Abréviation
}
}
}
}
Cette boucle en while jusqu'à ce que tous les codes d'octet de la méthode soient exécutés, et est branchée et exécutée dans la section CASE selon le code d'instruction. Par exemple, getstatic a l'implémentation suivante.
getstatic
CASE(_getfield):
CASE(_getstatic):
{
u2 index;
ConstantPoolCacheEntry* cache;
//Remarque: emplacement actuel du bytecode pc+Obtenez 1 à 2 octets de données et stockez-les dans l'index
index = Bytes::get_native_u2(pc+1);
// QQQ Need to make this as inlined as possible. Probably need to
// split all the bytecode cases out so c++ compiler has a chance
// for constant prop to fold everything possible away.
//Remarque: constatnt_Spécifiez l'index de la table et prenez une valeur.
cache = cp->entry_at(index);
if (!cache->is_resolved((Bytecodes::Code)opcode)) {
CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode),
handle_exception);
cache = cp->entry_at(index);
}
#ifdef VM_JVMTI
if (_jvmti_interp_events) {
int *count_addr;
oop obj;
// Check to see if a field modification watch has been set
// before we take the time to call into the VM.
count_addr = (int *)JvmtiExport::get_field_access_count_addr();
if ( *count_addr > 0 ) {
if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
obj = (oop)NULL;
} else {
obj = (oop) STACK_OBJECT(-1);
VERIFY_OOP(obj);
}
CALL_VM(InterpreterRuntime::post_field_access(THREAD,
obj,
cache),
handle_exception);
}
}
#endif /* VM_JVMTI */
oop obj;
if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
//Remarque: constante_table[2]Obtenez les informations de classe et mettez-les dans obj
Klass* k = cache->f1_as_klass();
obj = k->java_mirror();
MORE_STACK(1); // Assume single slot push
} else {
obj = (oop) STACK_OBJECT(-1);
CHECK_NULL(obj);
}
//
// Now store the result on the stack
//
TosState tos_type = cache->flag_state();
//Remarque: constante_table[2]Obtenir des informations sur le champ du champ_Mettre en offset
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
if (tos_type == atos) {
VERIFY_OOP(obj->obj_field_acquire(field_offset));
SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
} else if (tos_type == itos) {
SET_STACK_INT(obj->int_field_acquire(field_offset), -1);
} else if (tos_type == ltos) {
SET_STACK_LONG(obj->long_field_acquire(field_offset), 0);
MORE_STACK(1);
} else if (tos_type == btos) {
SET_STACK_INT(obj->byte_field_acquire(field_offset), -1);
} else if (tos_type == ctos) {
SET_STACK_INT(obj->char_field_acquire(field_offset), -1);
} else if (tos_type == stos) {
SET_STACK_INT(obj->short_field_acquire(field_offset), -1);
} else if (tos_type == ftos) {
SET_STACK_FLOAT(obj->float_field_acquire(field_offset), -1);
} else {
SET_STACK_DOUBLE(obj->double_field_acquire(field_offset), 0);
MORE_STACK(1);
}
} else {
if (tos_type == atos) {
//Remarque: constante_table[2]Obtient le champ de la classe de et stocke le résultat sous forme d'objet sur la pile.
VERIFY_OOP(obj->obj_field(field_offset));
SET_STACK_OBJECT(obj->obj_field(field_offset), -1);
} else if (tos_type == itos) {
SET_STACK_INT(obj->int_field(field_offset), -1);
} else if (tos_type == ltos) {
SET_STACK_LONG(obj->long_field(field_offset), 0);
MORE_STACK(1);
} else if (tos_type == btos) {
SET_STACK_INT(obj->byte_field(field_offset), -1);
} else if (tos_type == ctos) {
SET_STACK_INT(obj->char_field(field_offset), -1);
} else if (tos_type == stos) {
SET_STACK_INT(obj->short_field(field_offset), -1);
} else if (tos_type == ftos) {
SET_STACK_FLOAT(obj->float_field(field_offset), -1);
} else {
SET_STACK_DOUBLE(obj->double_field(field_offset), 0);
MORE_STACK(1);
}
}
//Exécutez l'instruction 3 octets avant getstatic.
UPDATE_PC_AND_CONTINUE(3);
}
// Have to do this dispatch this way in C++ because otherwise gcc complains about crossing an
// initialization (which is is the initialization of the table pointer...)
#define DISPATCH(opcode) goto *(void*)dispatch_table[opcode]
//Abréviation
#define UPDATE_PC_AND_CONTINUE(opsize) { \
pc += opsize; opcode = *pc; \
DO_UPDATE_INSTRUCTION_COUNT(opcode); \
DEBUGGER_SINGLE_STEP_NOTIFY(); \
DISPATCH(opcode); \
}
DISPACH est exécuté lorsque l'instruction suivante est exécutée. Il s'agit d'une instruction goto, qui exécute l'instruction suivante en passant à l'étiquette d'instruction.
Comme vous pouvez le voir, si vous regardez l'interrupteur à partir de ce code comme point de départ, vous pouvez avoir une idée du type de traitement qu'il effectue. ~~ À ce stade, il fait plus de 30 000 caractères, il est donc difficile de rassembler toutes les informations analysées ~~
Cette fois, je suis retourné au début et j'ai vu comment fonctionne Hello World.
Je dis: «Voir c'est croire, voir c'est croire», mais je suis désolé, j'ai léché Hello World. C'était très ennuyeux.
Demystifying the JVM: Interpretation, JIT and AOT Compilation https://metebalci.com/blog/demystifying-the-jvm-interpretation-jit-and-aot-compilation/#disqus_thread
DEMYSTIFYING THE JVM: JVM VARIANTS, CPPINTERPRETER AND TEMPLATEINTERPRETER https://metebalci.com/blog/demystifying-the-jvm-jvm-variants-cppinterpreter-and-templateinterpreter/#disqus_thread
** Enquête sur la compilation Java JIT avec JIT Watch ** https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-MA15-Architect-newland.pdf
** Regardez la compilation JIT sur JIT Watch! ** ** https://www.sakatakoichi.com/entry/2014/12/04/202747
** J'ai essayé de sortir [Java]
Recommended Posts