アセンブラとか


アセンブラとは

アセンブリ言語をアセンブルするのがアセンブラである。
ただし、アセンブリ言語を指してアセンブラと言うことが多い。

CPUの中身を知りたいと思ったらアセンブラでCPUを触ってみるとよい。

アセンブラ*1を使うには

環境は、WindowsでVBとVC++があるのを想定。
VC++にはインラインアセンブラというのがある。または、アセンブルだけ他のアセンブラ(NASMなど)にやってもらうこともできる。

VBならVC++でDLL化したものを使う。
自分でアセンブラを作れる兵はVB単体で可。VarPtr?なる謎な関数が存在し、変数のポインタを返してくれる。適当な配列にマシン語を書き込み、APIで実行する。状況依存でコード生成できると面白いかも。

VBとVC++の食べ合わせ

アセンブラの話ではないが、VBからVC++のDLLを使う場合の注意点。

関数の呼び出し規約がVBとVC++で違う。関数に渡す引数はスタックにpushするのだが、VBはこのpushした分を呼び出した先の関数がクリアしてくれると思っている。それに対してVC++で作った関数は呼び出し元のコードがクリアするものだと思っている(押し付け合ってるのかよ!)。
これはVC++で関数を__stdcall指定で作ればいい(デフォルトでは_cdecl)。

また、VBはデフォルトが参照渡し(ポインタを渡す)なので、VC++側でfunc(int a);などとしてもaに希望の値は入ってない。func(int *a);とするか、VB側でByVal?とつけて値渡しにするか、目的に応じて選ぶとよい。
配列や構造体を渡すときは参照渡しを使う。VBでは関数に配列を渡すときにa()と書くが、外部の関数に対してはエラーになる。こういうときは、a(0)を参照渡しにすれば配列aのポインタを渡すことができる。a(i)と書いて配列の途中から渡すこともできる。

スタック

プログラムが関数をどんどん呼び出していると、それぞれの関数で使っているメモリをかぶらないように確保できているのだろうかと心配になるが、スタックというデータ構造を使えば大丈夫。
(マルチタスクのメモリ管理は、また別の話だ)

スタックというのは、先入れ後出しという方式(反対の先入れ先出しはキュー)。a,b,cという順番でデータを保存したら、c,b,aという順番に取り出す。具体的にx86での実装は、espレジスタがスタックポインタとなりespより小さいアドレスが空きでesp以上のところが確保されているメモリだ。スタックトップに4byteのデータが入っている場合、そのデータは[esp],[esp+1],[esp+2],[esp+3]という4byteを占有している。
スタックにeaxの値を保存したかったらpush eax とする。これはespから4(eaxのサイズ)を引いて、それから[esp]にeaxの値を保存する命令だ。直後にpop ebxとすると、最後に保存したeaxの値がebxに取り出せる。取り出したのでそのメモリ上のデータは解放される。つまりespに4が足されて元に戻る。
espが「ここまでは使用中のメモリですよ」という共通規約になっているのでメモリがかぶらないのだ。push命令を使わなくても、自分でespから確保したいメモリのバイト数を引けばそれでいい。

スタックを使うpushやpop以外の主な命令として、callとretがある。
eipというレジスタに今実行している命令のアドレスが入っているのだが、callは指定されたアドレスへ命令の実行をジャンプさせ(つまりeipの値を変更する)、スタックにリターンアドレス(call命令の次の命令があるアドレス)をpushする。
retはスタックトップ([esp])にあるアドレス値をpopしてeipに入れる([esp]に入っているアドレスへジャンプする)。
callした後にジャンプ先でretすると元に戻るわけだ。

push esp は、espから4が引かれスタックトップに元のespの値が入る。そういう動作だが、espの値だけはスタックに"保存"できない。保存してもespがなかったら、どこがスタックトップかわからないのだ。
espを破壊すると、本当に自分がどこにいるのかわからなくなってしまう。call命令を実行した後に自分が戻るべき場所へのポインタがespなので、callやretが使えない。レジスタか、espなしでアクセスできるメモリ上にespを保存しておく必要がある。
もちろん、普通はespを破壊する必要はないし、してはいけない。

関数の呼び出し規約(cdeclの場合)

callは関数呼び出しのための命令だが、関数の引数はスタックにpushして渡す。
pushする順番は、Cで言うと右に書いてある引数から順にpushしていく。つまり、引数がアドレスの小さい方から順に並んでいるようにする。
関数側でそれを読むときには、スタックトップにcall命令がリターンアドレスをpushしているので[esp+4]から上(アドレスの大きい方)を読む。
関数内では、eax,ecx,edxのレジスタが無断で使用できる。他のレジスタを使うときは、内容をメモリなどに保存しておき、関数を抜けるときに元の状態に戻す必要がある。
関数から抜けるときには、戻り値をeaxに格納してからret命令を使う。この命令は、スタックトップにリターンアドレスがあるものとして、そのアドレスへジャンプし、リターンアドレスのデータをpopする。
stdcallの場合はリターン時にret 12 (引数が全部で12byteだった場合)などとする。これはリターンアドレスをpopした後、pushしてもらった引数もpopする。

関数へジャンプした後の一般的な動作は、次のようなものだ。

ebpの値は関数に入ったときと抜けた後で同じでなければならないので、ebpの値をスタックにpushして、空いたebpにespをコピーする。そして関数内で使うローカル変数のバイト数をespから引いてメモリを確保する。
ebpの役割は元のespの値の保存とローカル変数のアドレスを与えることだ。こうしておけば、コードの途中でpush命令を使うなどしてespの値を変えても問題にならない。
関数内ではebpが固定され、[ebp]には元のebpの値、ebpの下(アドレスの小さい方)にローカル変数、すぐ上にはリターンアドレス、更に上には渡された引数が入っていることになる。関数を抜けるときには、ebpからコピーすることでespの値を元に戻し(ローカル変数のクリア)、ebpをスタックからpopして元の値に戻して、最後にret命令で呼び出し元のアドレスへ戻る(このときにリターンアドレスもクリアされる)。


インラインアセンブラの書き方(VC++

インラインアセンブラはCのコードの中に埋め込むので、Cの変数との提携ができるという特徴を持つ。例えばローカルに確保したint(32bit)型変数kが[ebp-4]にあったとする。このときmov eax,k と書くとmov eax,[ebp-4] を意味する。つまりkの中身がeaxにコピーされる。
lea eax,k はlea eax,[ebp-4] で、これはkが格納されている場所のアドレス値がeaxに入る。leaの場合は、kの型によらずポインタの値(32bit整数)が得られる。

さて、ローカル変数のint p[4]; があって、p[0]が[ebp-16]にあったとする。p[3]の値を読みたいと思ったら、mov eax,p[12] と書けばいい。これはmov eax,[ebp-4] とみなされる。mov eax,p[12] の"12"はintのバイト数*3で、mov eax,p+12 と書いてもいいし、mov eax,[p+12] と書いても同じことになる。mov eax,[d+ecx+4] という書き方も可能だ。
ところが、ローカル変数のポインタint *q; に対してq[3]を読むことは、mov eax,q[12] とは書けない。書くとすれば、まずmov edx,q としてから、mov eax,[edx+12] とする必要がある。x86命令の中に、mov eax,[[ebp-4]+12] のような形の命令が存在しないためである。ベースポインタのebpからの相対位置が静的(コンパイル時)にはわからないため、ポインタを介したメモリアクセスは、遅くなるか、またはポインタを格納するレジスタが一個余計に必要になるというデメリットがある。

逆に、ローカル変数の配列はインラインアセンブラでポインタとして使うことができない。16bit整数の配列のshort p[4]; があり、p[0]が[ebp-12]にあったとする。ここでmov eax,p と書くとコンパイルエラーになる。eaxにポインタpの値を入れたかったのだろうが、ebp-12という値はメモリ上にもレジスタ上にも存在しないので、mov命令で得ることはできないのだ。こういうときは、lea eax,p とすればいい。"ebp-12"であることはコンパイル時にわかっているので、lea eax,[ebp-12] と解釈してくれる。
ちなみに、mov eax,p はmov eax,p[0] と解釈される。エラーになるのはp[0]が32bitでないからだ。

メモリアクセスのサイズは、明示しないと32bitになる。例えば、fld [ecx] は単精度の変数を読み込む。倍精度を読み込みたい場合はfld qword ptr [ecx] とする。1,2,4,8,10byteの場合、それぞれbyte,word,dword,qword,tbyteと表す。

Menu

About
過去ログ

div命令の
レイテンシ

BBS
アップローダ

最新の4件

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

今日の4件

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


*1 アセンブリ言語の意