Java Class解析器の実装方法の例

4675 ワード

最近、ClassAnalyzerというプライベートプロジェクトを書いています.ClassAnalyzerの目的は、Java Classファイルの設計と構造を深く理解することです.本体フレームワークと基本機能はすでに完成しており、いくつかの詳細機能は後日追加されます.実際にJDKはコマンドラインツールjavapを提供してClassファイルを逆コンパイルしていますが、この文章では解析器を実現するための私の考え方を説明します.
Classファイル
クラスまたはインタフェース情報の担体として、各Classファイルはクラスを完全に定義します.Java仮想マシン仕様は、Javaプログラムが「一度作成し、どこでも実行できる」ようにClassファイルを厳格に規定しています.Classファイルを構成する基本データ単位はバイトであり、これらのバイトの間には区切り記号が存在しないため、Classファイル全体に格納されているコンテンツのほとんどがプログラム実行に必要なデータであり、1バイトでは表現できないデータは複数の連続するバイトで表される.
Java仮想マシンの仕様に従って、ClassファイルはC言語構造体に似た擬似構造を採用してデータを格納します.この擬似構造には2つのデータ型しかありません.シンボル数とテーブルがありません.Java仮想マシン仕様は、u 1、u 2、u 4、およびu 8を定義し、1バイト、2バイト、4バイト、および8バイトの符号なし数をそれぞれ表し、符号なし数は、数値、インデックス参照、数量値、または文字列を記述するために使用することができる.テーブルは、複数の符号なし数または他のテーブルをデータ項目として構成する複合データ型であり、階層関係のある複合構造のデータを記述するために使用されるため、Classファイル全体が本質的にテーブルである.ClassAnalyzerではbyte、short、int、longがそれぞれu 1、u 2、u 4、u 8のデータ型に対応し、Classファイルは以下のJavaクラスとして記述される.

public class ClassFile {
 public U4 magic;       // magic
 public U2 minorVersion;      // minor_version
 public U2 majorVersion;      // major_version
 public U2 constantPoolCount;    // constant_pool_count
 public ConstantPoolInfo[] cpInfo;   // cp_info
 public U2 accessFlags;      // access_flags
 public U2 thisClass;      // this_class
 public U2 superClass;      // super_class
 public U2 interfacesCount;     // interfaces_count
 public U2[] interfaces;      // interfaces
 public U2 fieldsCount;      // fields_count
 public FieldInfo[] fields;     // fields
 public U2 methodsCount;      // methods_count
 public MethodInfo[] methods;    // methods
 public U2 attributesCount;     // attributes_count
 public BasicAttributeInfo[] attributes;  // attributes
}

解析方法
Classファイルを構成する各データ項目のうち、魔数、Classファイルのバージョンなどのデータ項目、アクセスフラグ、クラスインデックス、親インデックスは、各Classファイルに固定数のバイトを占有し、解析時には対応する数のバイトのみを読み込む必要があります.これに加えて、柔軟な処理が必要なのは、定数プール、フィールド・テーブル・セット、メソッド・テーブル・セット、およびプロパティ・テーブル・セットの4つのセクションです.フィールドもメソッドも独自の属性を持つことができ,Class自体にも対応する属性があるため,フィールドテーブル集合とメソッドテーブル集合を解析するとともに属性テーブルの解析も含まれる.
定数プールはClassファイルの大部分のデータを占め、数値と文字列定数、クラス名、インタフェース名、フィールド名、メソッド名など、すべての定数情報を格納します.Java仮想マシン仕様では、複数の定数タイプが定義されており、各定数タイプには独自の構造があります.定数プール自体はテーブルであり,解析にはいくつか注意が必要である.
各定数タイプは、u 1タイプのtagによって識別される.
ヘッダによって与えられる定数プールサイズ(constantPoolCount)は、実際より1大きく、たとえばconstantPoolCountが47に等しい場合、定数プールには46の定数があります.
定数プールのインデックス範囲は1から始まり、例えばconstantPoolCountが47に等しい場合、定数プールのインデックス範囲は1~46となる.設計者は、0番目の項目を空にして、「定数プールプロジェクトを参照しない」ことを表すために使用します.
CONSTANT_Utf8_inf o型定数の構造には、u 1型のtag、u 2型のlength、およびlength個のu 1型からなるbytesが含まれる.このlengthバイトの連続データは、MUTTF-8(Modified UTF-8)を用いて符号化された文字列である.MUTF-8はUTF-8と互換性がなく、主な違いは2つある.1つはnull文字が2バイト(0 xC 0と0 x 80)に符号化されることである.2つ目は、補足文字がUTF-16に従ってエージェントペアに分割されてそれぞれ符号化されていることであり、詳細はここを参照することができる(変種UTF-8).
プロパティ・シートは、特定のシーン固有の情報を記述するために使用されます.Classファイル、フィールド・テーブル、メソッド・テーブルには、対応するプロパティ・テーブルのセットがあります.Java仮想マシン仕様では複数の属性が定義されており、ClassAnalyzerでは現在、一般的な属性の解析が実現されています.定数タイプのデータ・アイテムとは異なり、プロパティにはプロパティのタイプを識別するtagはありませんが、各プロパティにはu 2タイプのattribute_が含まれています.name_index,attribute_name_indexは定数プールのCONSTANT_を指しますUtf8_infoタイプの定数.属性の名前が含まれています.プロパティを解析するとき、ClassAnalyzerはattribute_を介しています.name_indexが指す定数に対応する属性名は,属性のタイプを知る.
フィールド表は、クラスまたはインタフェースで宣言された変数を記述するために使用されます.フィールドには、クラスレベル変数およびインスタンスレベル変数が含まれます.フィールドテーブルの構造はu 2タイプのaccessを含むflags、u 2タイプのname_index、u 2タイプのdescriptor_index、u 2タイプのattributes_义齿count個attribute_infoタイプのattributes.属性テーブルの解析を紹介し,attributesの解析方式は属性テーブルの解析方式と一致した.
Classのファイルメソッドテーブルは、フィールドテーブルと同じストレージフォーマットを採用していますが、access_flagsの対応の意味は異なる.メソッドテーブルには、Codeプロパティという重要なプロパティが含まれています.Code属性にはJavaコードをコンパイルしたバイトコード命令が格納されており、ClassAnalyzerではCodeに対応するJavaクラスは以下のようになっている(クラス属性のみがリストされている)

public class Code extends BasicAttributeInfo {
 private short maxStack;
 private short maxLocals;
 private long codeLength;
 private byte[] code;
 private short exceptionTableLength;
 private ExceptionInfo[] exceptionTable;
 private short attributesCount;
 private BasicAttributeInfo[] attributes;
 ...
 private class ExceptionInfo {
  public short startPc;
  public short endPc;
  public short handlerPc;
  public short catchType;
   ...
 }
}

Code属性では、codeLengthとcodeは、それぞれバイトコード長とバイトコード命令を格納するために使用され、各命令は1バイト(u 1タイプ)である.仮想マシンが実行されると、codeの各バイトコードを読み出し、バイトコードを対応する命令に翻訳します.また、codeLengthはu 4タイプの値であるが、実際には65535バイトコード命令を超えることは許されない.
コード実装
ClassAnalyzerのソースコードはGitHubに置かれています.ClassAnalyzerのREADMEでは、クラスのClassファイルを例に、そのClassファイルのバイトごとに分析していますので、皆さんの理解に役立てたいと思います.
リファレンス
Java仮想マシンの理解
以上、Java class解析器を実装する方法について説明しました.