現在ここら辺からVisual Studio 2008 Express Editonがダウンロードできる。
WindowsXPSP2とユーザー登録が必要だが、無料!
いい時代になったものだ。
ただし、このページでの解説はVisual Studio 6.0で行う。
#include <stdio.h> int main() { printf("Hello World\n"); return 0; }
よくわからないorもっと詳しいことが知りたいというときは、ここ(ロベールの部屋)のC++講座の最初を読むのがおすすめ。
「Win32 Console Application」を選択するところで、代わりに「Win32 Dynamic-Link Library」を選ぶ。
ここでは、VBで整数を2乗する関数が欲しいことを想定して、SQUという関数を作ってみる。
ここで__stdcallと入れたのは、VBの関数規約に合わせるためである。
int __stdcall SQU(int a) { return a*a; }
まず、このように書いてビルドしてみる。しかし、これではうまく行かない。
そこで、ソースファイルに拡張子が.defのファイルを追加し、次のように書く。
EXPORTS SQU @1
これでビルドすれば、ちゃんとSQUという名前でDLL内の関数にアクセスできる。
関数を追加する場合は、次の行にhoge @2などと書けばいい。
ちなみに、VBからは、次のように宣言しておけばSQUが関数として使えるようになる。
Private Declare Function SQU Lib "DLLの場所" (ByVal n As Long) As Long
「DLLの場所」には、フルパスを指定してもいいが、実行ファイルと同じフォルダにDLLを置いてあればファイル名だけでいい。
次のコードを実行すると、「8」と出力される。
#include <stdio.h> int main() { int a=3; __asm{ mov eax, a ;eaxにaの値(今は3)をコピーする add eax, 5 ;eaxの値に5を加算する(eaxの値は8になる) mov a, eax ;eaxの値(今は8)をaにコピーする } printf("%d\n",a); return 0; }
各命令の意味はソースコードのコメントに書いた通り。
eaxというのはCPU内のレジスタで、多くの命令はレジスタに対して操作を行う。
そのレジスタにデータを読み込むのが、mov eax, a という操作で、メモリ上の変数aから値をロードしている。
ただし、C++で宣言した変数aを使うこの書き方は、インラインアセンブラ特有のもの。
NASM用コードの説明で述べるような気遣いが要らないのも大きな利点であり、
また、インラインアセンブラはぬるいと言われるゆえんである。
ここではNASMを使用する。
さっきのSQU関数をアセンブラで書いてNASMでアセンブルし、それをC++で呼び出してみる。
まず、C++のコードは次のように書く。最終的に「9」と出力されれば成功だ。
#include <stdio.h> extern "C" int SQU(int); int main() { int a=3; printf("%d\n",SQU(a)); return 0; }
extern "C"と書かないと、関数名が内部でSQUでないものに変えられてしまう。
C++はC言語と違って、引数の違う別の関数を同じ関数名で作れるのだが、その影響。
次に、アセンブラのコードを書く。
NASMを使うので拡張子を.nasとしてファイルを追加して、次のコードを書く。
segment .text global _SQU _SQU: mov eax, [esp+4] ;eaxに、アドレスesp+4のメモリからデータをロードする imul eax, eax ;eaxの値にeaxの値をかける ret ;関数の呼び出し元へ戻る
このままではNASMにアセンブルしてもらえないので、まずはNASMの実行ファイルへパスを通す。
メニューの「ツール」から「オプション」を開き、「ディレクトリ」タブの「表示するディレクトリ」で
「実行可能ファイル」を選択して、nasmw.exeのあるフォルダを指定する。
次に、FileView?からNASM用のファイルを右クリックし、「設定」を選ぶ。
「カスタム ビルド」タブの「コマンド」のところに次のように入力する。
nasmw -f win32 -o $(IntDir)\$(InputName).obj $(InputDir)/$(InputName).nas
「出力」のところには、こう入れる。
$(OutDir)/$(InputName).obj
これは、VC++側からNASMに渡すコマンドを指定している。
例えば、$(InputName?)は入力ファイル名を置き換えているだけである。
これでビルドできるようになった。
VC++がNASMに指示を出してアセンブルさせ、VC++はC++コードのコンパイル、
生成された.objファイルの中の関数をリンクして実行ファイルのできあがりだ。
上で示したコードの下3行がSQU関数の中身となる。
C++からSQU(a);と呼び出したときの動作を追っていこう。
SQU(a);としてaを引数に関数を呼び出すと、スタックにaの値が保存され、
続いて現在の命令の位置も保存した上で関数へジャンプする。
スタックへ最後に保存されたのは命令ポインタだが、その保存場所は[esp]である。
これは4byteなので、aの値はその4byte先の[esp+4]に保存されている。
それで、[esp+4]からeaxに値を読み出し、それを2乗しているのだ。
ここではレジスタのeaxを使ったが、eax,ecx,edx以外の汎用レジスタは勝手に使ってはいけない。
ebxなど、それ以外のレジスタを使うときには、自分でメモリなどへ保存しておき、
関数を抜ける前に復元しておく必要がある。
逆に、アセンブラから他の関数を呼んだ場合、eax,ecx,edxの値は保たれていない可能性がある。
次に、C++でのreturnに当たることをしないといけない。
返り値は、この場合整数なので、eaxレジスタに入れて返す規約になっている。
ここでは、既にimul eax, eaxの時点でeaxに返り値が入っているので、そのままでOK。
そうしたら関数を抜けて元の場所に戻るのだが、戻るべき場所は[esp]に入っている。
ret命令は、[esp]の指す場所へジャンプして、更にその場所情報をスタックからクリアする命令である。
あとは、スタックに保存したaの値もクリアする必要があるが、
VC++のデフォルトでは呼び出した側がやることになっているので、呼び出される関数側は心配しなくていい。
VBで使える関数にするためには、関数側でクリアする必要があるが、
この場合はret 4とすることで呼び出し側で保存した4byte分のスタックをクリアして戻ることができる。
関数規約とスタック、その他の命令については、ここ(休止中です><;)の最適化のためのアセンブラ入門を参照のこと。
CPUの中には1クロックごとに1ずつ値が増えているカウンタがある。
そのカウンタの値を読む命令。
eaxに下位32bit、edxに上位32bitがロードされ、全体では符号なし64bit整数となる。
1GHzのCPUならば、4秒ちょっとに1回のペースでedxの値が1増えることになる。
条件付きジャンプ命令だが、定型的に
mov ecx, 1000 ;ループ回数 label: ;色々なコード dec ecx jnz labelとすれば指定回数のループが簡単に書ける。
何もしない命令。
もちろんフラグも変えない。命令ポインタ(eip)を1増やすだけという命令。
命令長は1byteであり、xchg eax,eax と等価である。
例として、命令フェッチの位置を調整して高速化するためにループ前などへnopを配置する、という使い方がある。