はじめてのVC++


はじめてのVC++

現在ここら辺からVisual Studio 2008 Express Editonがダウンロードできる。
WindowsXPSP2とユーザー登録が必要だが、無料!
いい時代になったものだ。

ただし、このページでの解説はVisual Studio 6.0で行う。



とにかく作ってみる

  • まずはVC++を起動する
    • メニューの「ファイル」から「新規作成」をクリックする。
    • デフォルトで「プロジェクト」タブが選択されているので、そのまま。
    • 「プロジェクト名」のところに適当な名前(例えば「Test」)を入れる。
    • 左の「Win32 Console Application」をダブルクリック(もしくは、選択してOKボタンを押す)。
    • 「空のプロジェクト」を選択し、「終了」ボタンを押す。
    • 次の画面も「OK」を押せばいい。
  • 次にソースファイルを追加
    • 画面左の少し下の方にある「FileView?」というタブを選択する。
    • プロジェクト名を「Test」にしていたら、「Test ファイル」と書いてあるので、それをダブルクリック。
    • 「Source Files」を右クリックし、「ファイルをフォルダに追加」をクリック。
    • 「ファイル名」のところに好きな名前(例えば「a.cpp」)を入れてOKボタンを押す。なお、ちゃんと拡張子まで入力すること。
    • 確認メッセージに「はい」を押すと「a.cpp」というファイルへの参照が作られる。
    • 「Source Files」の中の「a.cpp」をダブルクリックして確認メッセージに「はい」。
    • 「a.cpp」というファイルが作られ、画面右の大きなスペースで編集できるようになる。
    • 次のコードを入力する。
      #include <stdio.h>
      int main()
      {
      	printf("Hello World\n");
      	return 0;
      }
    • メニューの「ビルド」から「ビルド」をクリック。コンパイルとリンクが行われる。
    • メニューの「ビルド」から「実行」をクリック。
    • 画面にHello Worldと表示されれば成功。
  • よくある間違い
    • 「studio.h」と書かないこと。
    • 行の終わりの「;」を忘れないこと。
  • その他
    • 「Win32 Debug」のところは「Win32 Release」にしておくとコンパイルも実行も速くなる。デバッグが使いたくなったら戻せばいい。
    • メニューの「プロジェクト」から「設定」クリックし、「C/C++」タブから色々設定できる。

よくわからないorもっと詳しいことが知りたいというときは、ここ(ロベールの部屋)のC++講座の最初を読むのがおすすめ。

DLLの作り方

「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ファイルの中の関数をリンクして実行ファイルのできあがりだ。

NASM用コードの説明

で示したコードの下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分のスタックをクリアして戻ることができる。

関数規約とスタック、その他の命令については、ここ(休止中です><;)の最適化のためのアセンブラ入門を参照のこと。

命令の紹介

  • rdtsc

    CPUの中には1クロックごとに1ずつ値が増えているカウンタがある。
    そのカウンタの値を読む命令。
    eaxに下位32bit、edxに上位32bitがロードされ、全体では符号なし64bit整数となる。
    1GHzのCPUならば、4秒ちょっとに1回のペースでedxの値が1増えることになる。

  • jnz

    条件付きジャンプ命令だが、定型的に

    	mov ecx, 1000	;ループ回数
    label:
    	;色々なコード
    	dec ecx
    	jnz label
    とすれば指定回数のループが簡単に書ける。
    ただし、この場合はループ内でecxレジスタを別の用途に使えないので注意(読み出すだけならよい)。
  • nop

    何もしない命令。
    もちろんフラグも変えない。命令ポインタ(eip)を1増やすだけという命令。
    命令長は1byteであり、xchg eax,eax と等価である。
    例として、命令フェッチの位置を調整して高速化するためにループ前などへnopを配置する、という使い方がある。

Menu

About
過去ログ

div命令の
レイテンシ

BBS
アップローダ

最新の4件

2016-01-20 2014-07-08 2012-07-08 2008-06-18

今日の4件

  • counter: 5405
  • today: 1
  • yesterday: 0
  • online: 1