逆コンパイラの作成Javaとリバース・エンジニアリング必要な知識まず、逆コンパイラを作成する前に、知っておかなくてはならないことがあります。それらは前章の「Java逆コンパイル技術」に詳しく書かれています。Javaの構成要素(属性の型と名前、属性の型、戻り値と名前など)の関係などは知っておく必要があります。Java言語で出来たクラスファイルを解析しようとしているのですから当然ですよね。仮想マシン内のクラスやオブジェクトに関する内部情報の参照と変更をサポートするAPIです。メンバまたは所定クラスが宣言したメンバへのアクセスが可能になります。これによって何が出来るか分かるでしょうか?「クラスやオブジェクトに関する内部情報の参照と変更」という点が重要なのです。その中でもとくに、「内部情報」です。存在するクラスファイルの内部情報を覗けるわけです。これがなぜ重要なのでしょうか?多くのプログラマにとって、他の人が書いた実行ファイルの内部情報を覗けるというのは、たいへん魅力です。プログラムは頭の中の世界ですから、人より優れたアルゴリズムを開発した人の方が成功するわけです。人は、優れたアルゴリズムを開発したらそれを他の人には秘密にするはずです。それが自分の武器になるからです。リフレクションAPIを使えば、自分より優れたアルゴリズムを書く人がどのようなコードを書いているかを推測する(あるいは断定する)重要な助けになります。javapSunが提供しているJDK1.1以上の中に、javapという逆アッセンブラがあります。このユーティリティーの-privateオプションを使って次のような内容を持つクラスファイルを逆コンパイルしてみましょう。Channnelメソッドはm_nMAX_CHANNEL以上のチャンネル入力を拒否します。}結果は、次のようになります。TV();}多少の違いはありますが、大体の情報は引き出せています。定数のfinalや型情報のint、アクセス情報なども完全に復元できています。TV()は、もとのファイルにはありませんが、これは、デフォルト・コンストラクタが暗黙のうちに呼び出されているからです。java.lang.Objectも、デフォルトで暗黙のうちに呼び出されるものです。ここでは、このような結果を表示するユーティリティーを、リフレクションAPIを使って作成してみようと思います。ClassAnalyzer・・・どうもベタですが、「クラス情報を解析するツール」という意味で、"ClassAnalyzer"という名前で作ってみることにしましょう。クラスビューワを作るには、java.lang.Classとjava.lang.reflectionパッケージを使います。java.lang.Classは、指定されたクラスに関する情報を得るのにまず必要な、エントリー・ポイントとでもいえるものです。以下のことが実行できます。・・・どうもよく分かりません。実際にやってみたらそのうち分かってくるのでしょうか。ステップ1:修飾情報、名前、スーパークラス、インターフェースを表示するでは、実際にクラスの情報をClassやjava.lang.reflect.*などを使って獲得してみましょう。TVPhone」などの、クラスの宣言文を得るコードを書くことにしましょう。ClassAnalyzerステップ1のソースは次のようになります。コマンドラインよりクラス名を読み取る。修飾子、名前、スーパークラス、そしてインターフェースを表示する。}}ではメインメソッドから見ていきましょう。それは、次のようになっています。コマンドラインよりクラス名を読み取る。オブジェクトを作成します。このClassオブジェクトは、コマンドラインから読み取ったクラスそのものを表現します。このクラスオブジェクトを使って、このクラスに含まれる情報を引き出せるわけです。このメインメソッドは、ClassNotFoundExceptionを捕捉して、クラス名が見つからなかった時にはメッセージを表示して終了します。コマンドライン文字列が見つからなかった場合には使用方法を表示します。例外が発生しなかった場合には、printClassメソッドにクラスオブジェクトを渡します。printClassメソッドは、修飾子、名前、スーパークラス、そしてインターフェース名を取得して表示します。printClassメソッドに入りましょう。printClassメソッドは、次のようになっています。}ここでまず気付くのは、Classオブジェクトのほかに、Modifierという新しいオブジェクトが出てきたことです。言語修飾子情報の解読に役立ちます。要するに、修飾子情報を得るためにこのオブジェクトを使うわけです。最初に、メインメソッドから渡されたClassオブジェクトがインターフェースかどうかを判断するif文に入ります。インターフェースだった場合は、そのクラスオブジェクトから修飾子情報を取得し、Classオブジェクトの名前を取得して表示します。ITVPhone」などのような文字列が表示されるわけです。Classオブジェクトがインターフェースではなかった場合、Classオブジェクトから取得した修飾子情報を文字列に変換し、スーパークラスの名前を取得して、表示します。インターフェースを含んでいた場合、そしてそのClassオブジェクトがインターフェースだった場合、スーパーインターフェースを表示します。Classオブジェクトがクラスだった場合は、implementsしているインターフェースを表示します。以上のコードで、得られる文字列は、例えば以下のようなものです。java.awt.event.ItemListenersynchronizedやjava.lang.Objectなど、元のソースには無いものが付け足されていますが、これらは、暗黙のうちに呼ばれているもので、コンパイル時に追加されたものです。クラスビューワの方が元のソースファイルより相関関係をより正確に描き出すわけですね。これでクラスの宣言部分は取得することができました。バイトコードであるクラスファイルからもとのソースファイルの一部分を復元できたと言う点で、これでもう十分逆コンパイラの機能はあるといえます。次は、クラスに含まれるメンバのリストを取得してみましょう。クラスに含まれるメンバとは、コンストラクタ、メソッド、メンバ関数などがあります。コンストラクタを取得するコードを追加してみましょう。ステップ2:コンストラクタの表示ここでは、コンストラクタの情報を取得するために、また新しいオブジェクトが出てきます。メソッドを使って、基本コンストラクタの形式パラメータ型と確認済みの例外の型を取得することができます。じっさいコーディングに入りましょう。コンストラクタは、複数存在する可能性があります。Javaはメソッドのオーバーロードをすることができるからです。コンストラクタオブジェクトの配列は、Classオブジェクトから、getDeclaredConstructors()することで取得することができます。c.getDeclaredConstructors();コンストラクタは複数存在する可能性があるため、取得したコンストラクタオブジェクトを、コンストラクタ情報を取得するためのメソッドにループで一つ一つ渡します。);ここで、関数名がprintMethodとなっているのは、実は意味があるのです。コンストラクタも一種のメソッドだと私が理解しているのと、後でメソッドのリスト表示関数と統合する予定であることに関係があるのですが、今は気にしないことにしましょう。printMethodのコーディングへいきましょう。以下は、実際のprintMethodメソッドと、関係するメソッド群です。}typeNameやmodifiersというメソッドはよく使うので、一つのメソッドとしてまとめてあります。ところで、なぜprintMethodの引数が、Memberというオブジェクトになっているのでしょうか?私がprintMethodメソッドに渡したのはConstructorオブジェクトだったはずです。インタフェースを実装します。Method,Constructorオブジェクトを引数に取れるわけです。「ポリモーフィズム」ってやつですね。ここでは、MethodオブジェクトもしくはConstructorオブジェクトが渡されます。次に、戻り値、パラメータ、例外を示すClassオブジェクトを作成します。引数がConstructorオブジェクトの場合は、printMethodメソッドに渡されたMemberオブジェクトをConstructorにキャストして、Constructorオブジェクトを復元します。オブジェクトの配列を、宣言順に取得します。オブジェクトの配列を取得します。次はコンストラクタの修飾子と戻り値、そしてのコンストラクタの名前処理です。ここで、コンストラクタには戻り値はないので、Constructorオブジェクトが渡された場合は、returntypeはいつもnullになります。ここで、modifierというメソッドが出てきました。言語修飾子を整数型にコード化して返すので、このメソッドは、渡されたオブジェクトが0の場合(修飾子が無い場合)には空白文字列を返し、それ以外の場合には修飾子情報を文字列にして返します。最後にパラメータと例外を取得するわけですが、ここではtypeNameメソッドにループで各オブジェクトを渡しています。typeNameはなにをするかと言えば、渡されたオブジェクトが配列かどうかをチェックし、配列であれば"[]"をオブジェクトの名前の後ろに追加して返しています。"オブジェクトが配列であるかもしれない"可能性のことを考えなくてはいけないためです。ステップ3:メソッドの表示コンストラクタの表示が出来たら、メソッドの表示は簡単です。インターフェースを実装しているからです。メソッドオブジェクトの配列は、次のようにClassオブジェクトから取得することが出来ます。c.getDeclaredMethods();そして、ループでprintMethodにメソッドオブジェクトを渡します。オブジェクトにも対応できるようにするために、printMethodメソッドを少し変えなくてはいけません。変更する所は、以下の通りです。c.getExceptionTypes();}に変更します。memberのインスタンスがMethodだった場合、つまり、printMethodメソッドに渡されたMemberオブジェクトがMethodオブジェクトだった場合、Methodにキャストして、各種設定します。これの意味は分かると思います。はい、ステップ3は簡単でしたね。次はフィールド情報を表示させることにしましょう。ステップ4:フィールド情報を表示する「フィールド」とは何でしょうか?フィールドとは、メンバ変数や定数のことです。フィールド情報へのアクセスは、Fieldクラスのメソッドを使用します。基本的に、Fieldオブジェクトを取得するための作業は今までと変わりません。Fieldオブジェクトは、次のようにして、Classオブジェクトから取得します。c.getDeclaredFields();このフィールドオブジェクトを、新しいメソッドに渡します。them.printField(fields[i]);では、printFiledメソッドを作成することにしましょう。printFieldメソッドは、次のようなものです。}getModifierメソッドで修飾子情報を取得し、modifiersメソッドでそれを名前にし、タイプ情報を名前+配列か名前にのみに変更して、変数名を取得して、表示します。このプロセスは、今までやってきたこととほとんど同じなので、理解がしやすいと思います。完成ここまでで出来上がった、ClassAnalyzerのソースファイルは、ここにあります。ここまで来た所で、プログラミングにおける一番の難所である、「デバッグ」に進みましょう。これが一番いやな作業なんですよね、自分がいかに考えて作ってないかっていうのをまざまざと見せられますから。このクラスビューワのバグを発見してみましょう。found」と表示されてしまいます。内部では「TV.class」は、「TV/class」として、フォルダ/クラスファイル名という風に処理されているので、そのように表示した方が良いかもしれません。found」という風なメッセージに変えればユーザにも比較的分かりやすくなるかもしれません。found」よりはマシなはずです。今のままでは、メソッドやフィールドを検索した時に、Javaが、そこに使用されている型情報を取得できなかった時などに、例外が発生する可能性があります。そのような時に、try〜catchブロックで例外を捕捉しておかないと、プログラムの実行がそこでストップしてしまいます。エラーストリームにでも例外発生を通知するようにしておいた方が親切だと思います。プログラムが実行途中で異常終了するような事体だけはなんとしても避けなくてはいけません。するように変更します。・・・などなど、いろいろバグ修正したClassAnalyzerのソースファイルは、ここにあります。次のステップ今のままでは、フィールド値などのパッケージ修飾がそのまま現れてしまい、正確さは増しますが、結果的に出力が見にくくなってしまっています。するようにプログラムを変更してみてはいかがでしょうか。クラスのAPIドキュメントを読み、その中のinvolkメソッドやgetメソッドを上手く使えば、定数値やグローバル変数の値を取得することも多分可能でしょう。オブフスケートされたクラスファイルの内容を解析するためにもう一工夫考えることもできるでしょう。オプションをもっと増やして、よりユーザが使いやすいようにするもの良いかもしれません。オプションでメソッドのみを表示させるとか。)最後にここまで、リフレクションAPIを使ってクラスビューワを作ってきたわけですが、このクラスビューワは、ある意味で限定的な逆コンパイラであるといえます。このクラスファイルもJavaで出来ているわけですから、この逆コンパイラもこの逆コンパイラによって逆コンパイルできるわけです。なんだか哲学的なものを感じませんか?(^^;世の中にはJavaで出来た優秀な逆コンパイラがあります。それに対抗する手段として、オブフスケーション(曖昧化)などという方法もあるにはありますが(大抵は逆コンパイラを作った人が作っています。)、それらもその逆コンパイラによって解読されてしまいます。逆コンパイラですら比較的容易に逆コンパイルできるのです。そのことは、逆コンパイラを作った人でさえもそれに対するまともな対抗策がない、と言うことを示していると思います。それは、Javaにとって大変致命的な欠点であると思います。ここまでJavaが発展しても、まだ商用のJavaアプリが少ないのもこの要因があるからではないでしょうか。苦労して作っても簡単に解読されてしまうようでは、発展の道はありません。それとも、Sunがなんとかしてくれるのでしょうか?結局、コンパイル済みのクラスファイルを配布するのは、ソースコードを配っているのと同じことだ、という認識でプログラムした方が良いでしょう。