Javaクラスの内部を見てください

今月の「JavaInDepth」へようこそ。 Javaの初期の課題の1つは、Javaが有能な「システム」言語として耐えられるかどうかでした。質問の根本は、Javaクラスが仮想マシンでJavaクラスと一緒に実行されている他のクラスを認識できないようにするJavaの安全機能に関係していました。クラスを「内部を見る」この機能は、イントロスペクションと呼ばれます。 Alpha3として知られる最初のパブリックJavaリリースでは、クラスの内部コンポーネントの可視性に関する厳密な言語規則は、クラスの使用によって回避される可能性がありますObjectScope。その後、ベータ版の間にObjectScope、セキュリティ上の懸念からランタイムから削除されたとき、多くの人々がJavaを「深刻な」開発には適さないと宣言しました。

言語が「システム」言語と見なされるために、なぜ内省が必要なのですか?答えの一部はかなり平凡です。「何もない」(つまり、初期化されていないVM)から「何か」(つまり、実行中のJavaクラス)に移行するには、システムの一部がクラスを検査できる必要があります。それらをどうするかを理解するために実行します。この問題の標準的な例は、次のとおりです。「別の言語コンポーネントの「内部」を見ることができない言語で記述されたプログラムは、他のすべてのコンポーネントの実行の開始点である最初の言語コンポーネントの実行をどのように開始しますか? 「」

Javaでイントロスペクションを処理するには、クラスファイルインスペクションとJava1.1.xの一部である新しいリフレクションAPIの2つの方法があります。両方の手法について説明しますが、このコラムでは、最初のクラスのファイル検査に焦点を当てます。将来のコラムでは、リフレクションAPIがこの問題をどのように解決するかを見ていきます。(この列の完全なソースコードへのリンクは、「リソース」セクションにあります。)

私のファイルを深く調べてください...

Javaの1.0.xリリースでは、Javaランタイムの最大の問題の1つは、Java実行可能ファイルがプログラムを起動する方法です。何が問題ですか?実行は、ホストオペレーティングシステム(Win 95、SunOSなど)のドメインからJava仮想マシンのドメインに移行しています。 「java MyClass arg1 arg2」という行を入力すると、Javaインタープリターによって完全にハードコーディングされた一連のイベントが開始されます。

最初のイベントとして、オペレーティングシステムのコマンドシェルはJavaインタープリターをロードし、引数として文字列「MyClassarg1arg2」を渡します。次のイベントは、JavaインタープリターMyClassが、クラスパスで識別されたディレクトリの1つで指定されたクラスを見つけようとしたときに発生します。クラスが見つかった場合、3番目のイベントは、という名前のクラス内でメソッドを見つけることです。このメソッドのmainシグネチャには、修飾子「public」と「static」がありString、引数としてオブジェクトの配列を取ります。このメソッドが見つかると、原始スレッドが構築され、メソッドが呼び出されます。次に、Javaインタプリタは「arg1arg2」を文字列の配列に変換します。このメソッドが呼び出されると、他のすべては純粋なJavaになります。

これはmain、実行時にまだ存在していないJava環境でメソッドを呼び出すことができないため、メソッドを静的にする必要があることを除いて、すべてうまくいきます。さらに、mainコマンドラインでメソッドの名前をインタプリタに伝える方法がないため、最初のメソッドに名前を付ける必要があります。メソッドの名前をインタプリタに伝えたとしても、それが最初に名前を付けたクラスにあるかどうかを確認する一般的な方法はありません。最後に、mainメソッドは静的であるため、インターフェイスで宣言することはできません。つまり、次のようなインターフェイスを指定することはできません。

パブリックインターフェイスアプリケーション{publicvoid main(String args []); }

上記のインターフェースが定義され、クラスがそれを実装している場合、少なくともinstanceofJavaの演算子を使用して、アプリケーションがあるかどうかを判断し、コマンドラインからの呼び出しに適しているかどうかを判断できます。要するに、(インターフェースを定義する)ことはできず、(Javaインタープリターに組み込まれている)ことはできないので、(クラスファイルがアプリケーションであるかどうかを簡単に判断する)ことはできません。それで、あなたは何ができますか?

実際、何を探し、どのように使用するかを知っていれば、かなりのことができます。

クラスファイルの逆コンパイル

Javaクラスファイルはアーキテクチャに依存しません。つまり、Windows95マシンからロードされた場合でもSunSolarisマシンからロードされた場合でも同じビットセットです。また、LindholmとYellinによる「Java仮想マシン仕様」という本にも非常によく記載されています。クラスファイル構造は、一部、SPARCアドレス空間に簡単にロードできるように設計されています。基本的に、クラスファイルを仮想アドレス空間にマップしてから、クラス内の相対ポインタを修正して、presto!インスタントクラス構造がありました。これはIntelアーキテクチャマシンではあまり役に立ちませんでしたが、この遺産により、クラスファイル形式は理解しやすく、さらに分解しやすくなりました。

1994年の夏、私はJavaグループで作業し、Javaの「最小特権」セキュリティモデルと呼ばれるものを構築していました。私が本当にやりたかったのは、Javaクラスの内部を調べ、現在の特権レベルで許可されていない部分を切り取り、カスタムクラスローダーを介して結果をロードすることであると理解し終えたところです。そのとき、メインランタイムにクラスファイルの構築について知っているクラスがないことに気づきました。コンパイラのクラスツリーにはバージョンがありましたが(コンパイルされたコードからクラスファイルを生成する必要がありました)、既存のクラスファイルを操作するための何かを構築することに興味がありました。

私は、入力ストリームで提示されたJavaクラスファイルを分解できるJavaクラスを構築することから始めました。私はそれに元の名前よりも少ない名前を付けましたClassFile。このクラスの始まりを以下に示します。

パブリッククラスClassFile {int magic;短いmajorVersion;短いminorVersion; ConstantPoolInfo constantPool [];短いaccessFlags; ConstantPoolInfo thisClass; ConstantPoolInfoスーパークラス; ConstantPoolInfoインターフェイス[]; FieldInfo fields []; MethodInfoメソッド[]; AttributeInfo属性[];ブールisValidClass = false; public static final int ACC_PUBLIC = 0x1; public static final int ACC_PRIVATE = 0x2; public static final int ACC_PROTECTED = 0x4; public static final int ACC_STATIC = 0x8; public static final int ACC_FINAL = 0x10; public static final int ACC_SYNCHRONIZED = 0x20; public static final int ACC_THREADSAFE = 0x40; public static final int ACC_TRANSIENT = 0x80; public static final int ACC_NATIVE = 0x100; public static final int ACC_INTERFACE = 0x200; public static final int ACC_ABSTRACT = 0x400;

ご覧のとおり、クラスのインスタンス変数ClassFileは、Javaクラスファイルの主要コンポーネントを定義します。特に、Javaクラスファイルの中心的なデータ構造は、定数プールと呼ばれます。クラスファイルの他の興味深いチャンクは、独自のクラスを取得します。MethodInfoメソッド、FieldInfoフィールド(クラス内の変数宣言)、AttributeInfoクラスファイル属性の保持、およびクラスファイルの仕様から直接取得された定数のセットフィールド、メソッド、およびクラスの宣言に適用されるさまざまな修飾子をデコードします。

このクラスの主なメソッドはです。これはread、ディスクからクラスファイルを読み取りClassFile、データから新しいインスタンスを作成するために使用されます。readメソッドのコードを以下に示します。メソッドがかなり長くなる傾向があるので、説明にコードを散在させました。

1 public boolean read(InputStream in)2 throws IOException {3 DataInputStream di = new DataInputStream(in); 4整数カウント; 56マジック= di.readInt(); 7 if(magic!=(int)0xCAFEBABE){8 return(false); 9} 10 11 majorVersion = di.readShort(); 12 minorVersion = di.readShort(); 13カウント= di.readShort(); 14 constantPool = new ConstantPoolInfo [count]; 15 if(debug)16 System.out.println( "read():Read header ..."); 17 constantPool [0] = new ConstantPoolInfo(); 18 for(int i = 1; i <constantPool.length; i ++){19 constantPool [i] = new ConstantPoolInfo(); 20 if(!constantPool [i] .read(di)){21 return(false); 22} 23 //これらの2つのタイプはテーブル24の「2つの」スポットを占めますif((constantPool [i] .type == ConstantPoolInfo.LONG)|| 25(constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27}

As you can see, the code above begins by first wrapping a DataInputStream around the input stream referenced by the variable in. Further, in lines 6 through 12, all of the information necessary to determine that the code is indeed looking at a valid class file is present. This information consists of the magic "cookie" 0xCAFEBABE, and the version numbers 45 and 3 for the major and minor values respectively. Next, in lines 13 through 27, the constant pool is read into an array of ConstantPoolInfo objects. The source code to ConstantPoolInfo is unremarkable -- it simply reads in data and identifies it based on its type. Later elements from the constant pool are used to display information about the class.

上記のコードに従って、readメソッドは定数プールを再スキャンし、定数プール内の他のアイテムを参照する定数プール内の参照を「修正」します。修正コードを以下に示します。参照は通常、定数プールへのインデックスであるため、この修正が必要です。これらのインデックスを既に解決しておくと便利です。これは、クラスファイルが一定のプールレベルで破損していないことを読者が知るためのチェックも提供します。

28 for(int i = 1; i 0)32 constantPool [i] .arg1 = constantPool [constantPool [i] .index1]; 33 if(constantPool [i] .index2> 0)34 constantPool [i] .arg2 = constantPool [constantPool [i] .index2]; 35} 36 37 if(dumpConstants){38 for(int i = 1; i <constantPool.length; i ++){39 System.out.println( "C" + i + "-" + constantPool [i]); 30} 31}

上記のコードでは、各定数プールエントリはインデックス値を使用して、別の定数プールエントリへの参照を把握しています。36行目で完了すると、オプションでプール全体がダンプされます。

コードが定数プールを超えてスキャンされると、クラスファイルはプライマリクラス情報(クラス名、スーパークラス名、実装インターフェイス)を定義します。リードの下に示されるように、これらの値のコードをスキャン。

32 accessFlags = di.readShort(); 33 34 thisClass = constantPool [di.readShort()]; 35 superClass = constantPool [di.readShort()]; 36 if(debug)37 System.out.println( "read():Read class info ..."); 38 39 / * 30 *このクラスによって実装されたすべてのインターフェースを識別します31 * / 32 count = di.readShort(); 33 if(count!= 0){34 if(debug)35 System.out.println( "クラスは" + count + "インターフェイスを実装します。"); 36インターフェイス=新しいConstantPoolInfo [count]; 37 for(int i = 0; i <count; i ++){38 int iindex = di.readShort(); 39 if((iindex constantPool.length --1))40 return(false); 41インターフェイス[i] =定数プール[iindex]; 42 if(デバッグ)43 System.out.println( "I" + i + ":" + interfaces [i]); 44} 45} 46 if(debug)47 System.out.println( "read():Read interface info ...");

このコードが完成すると、readメソッドはクラスの構造についてかなり良いアイデアを構築します。残っているのは、フィールド定義、メソッド定義、そしておそらく最も重要なこととして、クラスファイルの属性を収集することだけです。

クラスファイル形式は、これら3つのグループのそれぞれを、番号と、それに続く探しているもののインスタンスの数で構成されるセクションに分割します。したがって、フィールドの場合、クラスファイルには定義されたフィールドの数があり、次にその数のフィールド定義があります。フィールドでスキャンするコードを以下に示します。

48 count = di.readShort(); 49 if (debug) 50 System.out.println("This class has "+count+" fields."); 51 if (count != 0) { 52 fields = new FieldInfo[count]; 53 for (int i = 0; i < count; i++) { 54 fields[i] = new FieldInfo(); 55 if (! fields[i].read(di, constantPool)) { 56 return (false); 57 } 58 if (debug) 59 System.out.println("F"+i+": "+ 60 fields[i].toString(constantPool)); 61 } 62 } 63 if (debug) 64 System.out.println("read(): Read field info..."); 

上記のコードは、48行目のカウントを読み取ることから始まり、カウントがゼロ以外の場合は、FieldInfoクラスを使用して新しいフィールドを読み取ります。このFieldInfoクラスは、Java仮想マシンへのフィールドを定義するデータを入力するだけです。メソッドと属性を読み取るコードは同じですが、への参照をFieldInfoへの参照MethodInfoまたは必要AttributeInfoに応じて置き換えるだけです。そのソースはここには含まれていませんが、以下の「リソース」セクションのリンクを使用してソースを確認できます。