loadlibサンプルの中には、マシン語(機械語)を使って、和を求める処理やクイックソートなどのさまざまな処理を高速に行なっているスクリプトがあることをご存知でしょうか。ほかにも、llmodモジュールに添付されているドラッグ&ドロップモジュールでは、マシン語で書かれたコールバック関数が使用されています。筆者のページでも、マシン語を使用しているモジュールをいくつか公開しています。
マシン語のバイナリコードを変数に格納してそれを使用することで、処理を非常に高速に行なうことや、hsgetmsg.dllのようなメッセージ取得系のDLLのみでは実現できないコールバック関数を使用することができるようになるのであります。拡張DLLの数を増やさなくて済むというメリットもありますしね。
ところが、多くの(というか、おそらくほとんどすべての)プログラマさん方は、マシン語を直接変数に打ち込むことなどできないでしょう。また、loadlibサンプルを作成されたtomさんは、どうやらアセンブリ言語で書かれたプログラムをMASMでアセンブルし、リストファイルを出力、という方法を使ったようですが、アセンブリ言語を使えるプログラマというのもそう多くはないはずです。
そこで、ここでは、直接マシン語を打ち込んだり、アセンブリ言語などのような難しい言語を使用したりせずに、一般的に用いられているC言語を用いてマシン語コードの作成を実践してみることにします。
まずは、必要なもの(ソフトウェア)を以下に示しておきます。
ここのページでは、Microsoft Visual C++(以下VC++またはVCと記述)を使った方法と、Borland C++ Compiler(以下BCCと記述)を使った方法の2つをあわせて紹介していきます。
VCは、言わずと知れた、マイクロソフトが販売しているC/C++開発環境なのであります。おそらくWindowsのC/C++開発環境で最も一般的に使われているものだと思います。筆者もこれを使っているのであります。おひとつウン万円という値段がついていて、少々買うのに気が引けるものでもあります。(まあ、学生さん方にはお値段控えめアカデミーパックというものもありますが。筆者もそれ。)
……が、いつの間にやら、コンパイル環境だけなら、現在ではフリーで手に入ってしまいます。マイクロソフトは『Microsoft Visual C++ Toolkit 2003』というものを公開しており、これをインストールすることで、VC++.NET 2003 Pro版と同等の最適化コンパイラを使うことがでるのであります。製品版VC++には含まれているMFCやATLといったライブラリ、統合開発環境(IDE)などはないのですが、Platform SDKを別にインストールすることでWin32 APIを使用したWindows GUIプログラムを作成することも可能になります。
一方のBCCは、Borland社から配布されている無料のC/C++コンパイラなのであります。もともとはBorland C++ Builderという C/C++ 開発環境の一部なのですが、そのうちのコンパイラの部分だけを取り出して、コマンドラインツールとしてフリーで公開しているものであります。それでもWindows GUIアプリケーションやDLLを作成できてしまったりするので、なかなかすごいヤツなのであります。ただし、コマンドラインで操作しなければならないので、DOSプロンプトでの操作をしたことがない、という人は慣れるまで時間かかるかも。
このページでは、VC++ 6.0とBCC 5.5.1を使って説明していますが、バージョンによる違いはあまりないと思います。もうすでにVCを持っている人はVCを、持っていない人はBorland社のホームページからBCCをダウンロードしてそれを使えばいいでしょう。新たにVC++ ToolkitやBCCをインストールする方のために、これらのインストール・設定方法などは次で説明します。
VC++ Toolkit 2003 については、時間的な余裕があれば対応してみたいと思います。
Cコンパイラとあわせて、バイナリファイルエディタ(バイナリエディタ、ヘキサエディタとも呼ばれる)も必要になります。基本的にはどんなものでも大丈夫なはずです。筆者は、DDS2さんが開発・公開されているStirlingというフリーソフトウェアを使っています。バイナリエディタなんて持ってないよー、という人は、これを使ってみてください。
ただ、現在ではもうDDS2さんのホームページにはつながらないようなので、Vectorなどからダウンロードすることにしましょう。
C/C++ で使われるすべての機能が使えるわけではありません。実はいろいろな問題点が出てきてしまうため、以下のような制限をつけなければなりません。
C言語で記述します。C++のクラスなどは使用しません。
1つのソースファイルに複数の関数を記述(実装)することも可能ではありますが、ここでは、(少なくともはじめのうちは)1つのソースファイルにつき関数1つまでとします。
関数には必ずプレフィックスWINAPIまたはCALLBACKまたは__stdcallを付けます。
文字列定数(リテラル文字列)は使用しません。
原則として、浮動小数点実数の演算は行ないません。数値は整数のみを使用します。
ソースコード内の変数はすべてローカル変数を使用します。
外部変数/静的変数を使用したい場合は、HSP側で準備した変数に対してポインタ指定でアクセスします。
文字列を使用したい場合は、HSP側で準備した変数に文字列を格納しておき、その変数に対してポインタ指定でアクセスします。ただし、動的に確保されたメモリブロックやローカル変数上に格納して使用する場合にはこの限りではありません。
Win32 APIなど外部の関数を使用する場合は、HSPスクリプト側で関数のアドレスの取得を行ない、Cソースコードでは関数ポインタによる呼び出しを行ないます。
原則として、HSPスクリプト側で関数アドレスを取得できない関数の呼び出しはできません。(ただし、1つのソースファイルに複数の関数を記述した場合には、同じファイル内の別の関数を直接呼び出せる場合があります。)
さらに、Windows XP SP2以降(またはWindows Server 2003 SP1以降)、ある特定の環境では、従来の方法ではマシン語を実行させることができなくなってしまっています。
Windows XP SP2より、データ実行防止(Data execution prevention;DEP)という新しいメモリ保護機能が追加され、これによって、アドレス空間内で「実行属性」を付けられていない領域のマシン語コードを実行させようとすると、強制終了されるようになってしまいました。そのため、マシン語を格納する変数領域に実行属性をつけるように記述しなければならなくなっています。
実際のところ、この機能が働くためにはCPU側が実行属性保護をサポートしていることが必要であり、現在(2004年9月)一般的に使われているCPUの多くにはこの機能は搭載されていないため、現状では、この対策をしなかったとしてもあまり大きな被害はないかもしれません。しかし、現在でも一部のCPU(AMD Athlon64, Intel Itanium)にはすでにこの機能が搭載されており、今後ほとんどのCPUで実行保護属性がサポートされていく予定であるため、そのような環境でも動作するようにプログラムを作成していかなければいけません。
ここでは、xdimというマクロ命令を定義して、それを使用することにします。このxdim命令を使用するためには、次のファイルをダウンロードして、スクリプトにインクルードする必要があります。
このxdim命令は、変数領域に実行属性を追加する以外はdim命令と同等です。具体的には、dim命令で確保した領域に対して、Win32 API関数のVirtualProtectを呼び出すことにより、実行属性を付加しています。マシン語コードを格納するための変数は、必ずxdim 命令で確保するようにしてください。
すでに公開してしまっているソフトウェアで、このxdim命令を使うことなくマシン語コードを埋め込んでいる場合には、上記の環境ではもはやそのプログラムが動かすことができないのかというと、そういうわけではありません。
もちろん、正しく作成された新しいバージョンを公開するのが望ましいのですが、何らかの理由でそれができない場合には、そのソフトウェアに対してデータ実行防止機能が働かないように設定することができます。その設定は、ソフトウェアを使用するユーザーが行わなくてはいけません。「コントロールパネル」の「システム」→「詳細設定」タブ→「パフォーマンス」の「設定」→「データ実行防止」タブを開き、「次に選択するものを除くすべてのプログラムおよびサービスについてDEPを有効にする」を選択して、ソフトウェアの実行ファイルを追加します。
ただし、この方法によっても、すべてのプログラムに対してDEPが有効になるように設定されているマシン(BOOT.INI内で「/NoExecute=AlwaysOn」が指定されている場合)では、実行させることができないので注意してください。
それでは、VC版とBCC版に分けて説明していくことにしましょう。