世界初!Pure Java で DLL - 解説編(1)
前回の記事 の解説、というかタネあかしというか、そんなんです。
解説
コードを実際に実行しようとすると、
Hello DLL !
ではなく、違った文字列が出力され、main() がないというエラーになります。
記事では、ポイントとして以下をあげました。
- com.microsoft.windows.System32 ライブラリを使用する
- エントリポイントは DllMain()
- 単体実行するにはVM引数で -DLL=foo と関数名を渡す
- winapi:// スキーマを使ってごにょごにょする
まず、これらは全部フェイクです。
順番に説明します。
1: com.microsoft.windows.System32ライブラリ
com.microsoft.windows.System32 ライブラリなんぞ存在しません。
これは、「WindowsのDLLを実現」という名目上、それっぽいコードの見た目を考えたところ、こんな感じのライブラリを load させておけばそう見えるかな、という狙いでした。
また見た目だけではなく、コード中では、この文字列の "System" の部分だけをあとで使用しています。
2: エントリポイントは DllMain()
DllMain() は、C/C++ でDLLを作るときに本当に出てくる関数名です。
引数も
HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
というように、C/C++で記述するときと同じ型を使用して、リアルさを出してみました。
ただ HANDLE なんて型はもちろんJavaにはないので定義しないとコンパイルが通らないのですが、
class HANDLE {}
とか書くと違和感全開で、すぐにバレてしまって面白くないので、今回はジェネリクスを使うことでごまかしました。(笑
実際の開発現場でマネしないようにね。 (^o^)
3: 単体実行するにはVM引数で -DLL=foo と関数名を渡す
これが一番工夫した(!?)ところ。
深く考えずに文面だけ読むと、いかにも DLL で foo() を呼び出すように感じませんか?
実は -D は システムプロパティの設定で、 "LL" というプロパティに "foo" という値を定義しているだけです。
この定義は、"Hello DLL" → "Hello fooL" に変換するロジックで利用しています。
同様に、コンパイルオプションとして
javac -Djava.ext.dirs=%SystemRoot% DllExample.java
と掲載しましたが、これはただのフェイクです。-Djava.ext.dirs=%SystemRoot% を指定したところで実際には何の効果もありません。
4: winapi:// スキーマを使ってごにょごにょする
winapi:// スキーマというのは、以下の部分を指しています。
do winapi://java.microsoft.com/apis/win32/dll/?q=entrypoint&key=${addr}&feature=${feature} { ... } while()
これの狙いは、do の後ろにそれっぽいURIを記述することで、いかにも怪しい、特殊な文法であるかのように見せたかったのですが、ちょっとイマイチだったかもしれません。
タネあかしする間でもないかもしれませんが、これはURIに見せかけた、ラベルとコメントです。
つまり、 winapi: の部分がラベルで、// 以降はコメントですね。
掲載したコードは、はてなのシンタックスハイライトを使ったので色付けによってコメントだとすぐわかったかもしれません。
色付けにももう少し凝れば良かったかなと思っています。
コードの解説
次はコードの解説です。
コード中にコメント形式で記入しました。
※不要なコード(実行されないコード、フェイクのためのコード)は削除し、説明しやすいように少しだけリファクタリングしてあります。
import java.io.*; public class DllExample { private final static String str = "Hello DLL !\n"; // リフレクションを使って間接的に参照 private final static String LIB_NAME = "com.microsoft.windows.System32"; // "System"の部分だけ使う // コンストラクタ private DllExample() { String feature = "DLL"; // "LL"の部分だけ使います long addr = 0x65f5520; // 謎のアドレス。5ビットずつ切り出してXOR演算に使います try { String usolib = LIB_NAME.split("(\\.|\\d)")[('M'+'S')/2 - 0115]; // "System" という文字列を取得 // 変数名 usolib は、「うそlib」の意でした。 // 上の [] 内の演算は、 // ('M'+'S')/2 は、アスキーコードの演算で、 (77 + 83)/2 = 80、 // 0115 はリテラルの8進数表記なので、10進では 77、 // つまり 80 - 77 = 3 となります。 // LIB_NAME.split("(\\.|\\d)") で "com.microsoft.windows.System32" // を . と数字で区切って配列化しています。 // その配列の[3]なので、 "System" が取り出されます。 OutputStream jni = (OutputStream)Class.forName("java.lang." + usolib).getField("out").get(null); // System.out の取得 // リフレクションを使って、 java.lang.System クラスの // out というフィールドを参照しています。すなわち、 // OutputStream jni = System.out; // と等価です。 do { String api = getClass().getDeclaredFields()[0].get(null).toString(); // "Hello DLL !\n" を取得 // ここでもリフレクションを使っています。 // このクラスの宣言フィールドの[0]なので、メンバ変数 str を参照していることになります。 String hello_fool = api.replace(feature.substring(0,2), System.getProperty(feature.substring(1))); // ここで、 "Hello DLL !\n" の変換を行っています。 // システムプロパティ"LL" の定義値を使います。 // 実行時に VM引数 -DLL=foo を指定させるので、 // String hello_fool = api.replace("DL", "foo"); // というわけ。 for (byte b : hello_fool.getBytes()) { jni.write(b ^ (byte)(addr/**//=32) & 037); // 1文字ずつ System.out.write() しています。 // まず、 /**/ はコメントですね。まぎらわしの効果を狙いました。 // /=32 は >>=5 と等価。5ビット右シフトされます。 // 037 は16進表記すると 0x1f です。 // ^ はビット演算、排他的論理和ですね。 // すなわち、 // b ^ (byte)(addr>>=5) & 0x1f // ということです。 // addr を5ビットずつ取り出しながら、 b と XOR しています。 } } while(addr!=0); // whileの継続条件 addr!=0 は、必ず false です。 // なぜなら、addr は5ビットずつ右シフトさせていくので、 // forループを抜けたときには 0 になっているからです。 } catch (Exception e){e.printStackTrace();} // 上記 try 〜 catch は、リフレクション関連のメソッドを使う上で // チェック例外がありますので、記述しています。 } // statcフィールドの初期化式でnewする。 // こうすることでクラスロード時にコンストラクタが実行されます。 static DllExample _dll_export = new DllExample(); }
次は、ビット演算の部分の解説です。