お読みになる前に
本稿の内容は、読者の環境にMASM32がインストールされていることを前提にしています。まだインストールしていない場合は、http://www.masm32.com/から入手してください。
はじめに
いよいよこのチュートリアルの最終回です。この第3回では、一般的な算術関数と、アセンブラを実装するうえで大きな助けになるMASM32のマクロをいくつか紹介したいと思います。
増分と減分
増分と減分の処理はinc
命令とdec
命令で行います。次に例を示します。
TestProc proc mov eax, 5 dec eax dec eax mov dl, 10 inc dl inc dl ret TestProc endp
dec
命令で減分を行うと、結果がゼロになったときにゼロフラグがセットされます。これを利用してループを実装することができます。次に例を示します。
TestProc proc mov ecx, 10 xor eax, eax ; efficient way of saying eax=0 LoopStart: inc eax dec ecx jnz LoopStart ; eax now equals 10 ret TestProc endp
加算と減算
加算と減算はadd
命令とsub
命令で行います。これらの命令の基本的な構文は次のとおりです。
add/sub (destination), (source)
レジスタとレジスタを加算するほかに、定数とレジスタを加算したり、メモリの内容とレジスタを加算したりすることができます。source
とdestination
は同じサイズ(ビット数)でなければなりません。どちらの命令も、システムのフラグに影響を与えます。たとえばsub
命令の結果がゼロの場合は、ゼロフラグがセットされます。次に例を示します。
AddValues proc dwValue1:DWORD, dwValue2:DWORD mov eax, dwValue1 add eax, dwValue2 ret AddValues endp
このメソッドでは、渡された2つの値を加算し、結果を返します。
乗算と除算
乗算と除算はmul
命令とdiv
命令で行います。これらの命令はアキュムレータレジスタ(eax
)に対してのみ処理を行い、データレジスタ(edx
)はオーバーフローとして使用します。レジスタのどの部分が影響を受けるかは、オペランドのサイズによって決まります。
次の図は、この2つの命令でアキュムレータレジスタとデータレジスタが使用されるときに、これらのレジスタがどのように組み合わされるかを示しています。
したがって、予想どおりの結果を得るためには、mul
またはdiv
を呼び出す前にedx
をゼロに設定することをお勧めします。次に例を示します。
TestProc proc mov eax, 10 xor edx, edx ; set edx to zero mul 10 div 10 ret TestProc endp
論理演算
通常の論理演算はor
、and
、xor
命令で行います。これらの命令の構文は次のとおりです。
logical operation (destination), (source)
source
とdestination
は同じサイズ(ビット数)でなければなりません。次に例を示します。
LogicalFunction proc xor eax, eax ; the efficient way of saying eax=0 mov ax, 100 mov bx, 5 and ax, 1 or ax, bx ret LogicalFunction endp
ビットシフト演算
shl
命令とshr
命令は、与えられたレジスタビットを指定のビット数だけ左または右にシフトします。この2つの命令は非常に効率的なので、2の累乗のパラメータに関しては、mul
命令やdiv
命令よりもこちらを使用した方がよいでしょう。次に例を示します。
ShiftFunction proc mov eax, 1 shl eax, 2 ; shift eax's bits left 2 times : i.e. eax *= 4 shr eax, 2 ; shift eax's bits right 2 times : i.e. eax /= 4 ret ShiftFunction endp
テスト命令とループ
特定の条件をテストするために使用できる命令は数多くあります。これらの命令は算術演算と同じ演算を行いますが、レジスタ内の値を変更せず、フラグだけを設定します。
たとえばcmp
命令はdestination
からsource
を効率的に減算しますが、結果の値を保存しません。次に例を示します。
CmpFunction proc mov eax, 100 cmp eax, 100 ; jump if equals je Equals ; not equal mov eax, 2 jmp EndIf Equals: mov eax, 1 EndIf: ret CmpFunction endp
test
命令は、source
オペランドとdestination
オペランドに対してand
演算を実行し、その結果に従ってフラグをセットします(結果は保存しません)。
loop
命令はecx
を1ずつ減分し、結果がゼロでない場合は指定の場所にジャンプします。
LoopFunction proc xor eax, eax mov ecx, 10 LoopStart: inc eax loop LoopStart ret LoopFunction endp
MASM32のマクロ
MASM32には、アセンブラ開発者の役に立つマクロが多数用意されています。以降ではそのごく一部を紹介したいと思います。
1つ目は.if
文です。これにより、2つのオペランドを標準のC++演算子(=
、>=
、<=
など)を使用して比較することができます。
IfProc proc mov eax, 100 mov ecx, 200 .if eax == ecx ; do something .else ; do something else .endif ret IfProc endp
2つ目は.repeat~.until
ループです。このループにはさまざまな形式があります。.untilcxz
は、ecx
を1ずつ減分し、結果がゼロでない場合はループを続行します。.until zero?
は、ゼロフラグがセットされるまでループを続行します。
LoopProc proc xor eax, eax mov ecx, 100 .repeat inc eax .untilcxz ret LoopProc endp
ループ内でループを実行するときは、レジスタeax
、ebx
、edx
を自由に使用するために、外側のループのecx
値をプッシュ(push
)しておき、内側のループを抜けたときにポップ(pop
)することができます。次に例を示します。
LoopInLoopProc proc xor eax, eax mov ecx, 100 .repeat push ecx mov ecx, 100 .repeat inc eax .untilcxz pop ecx .untilcxz ret LoopInLoopProc endp
アセンブラ内から関数を呼び出す
アセンブラコード内から関数を呼び出すにはinvoke
を使用します。invoke
の後に関数名を指定し、パラメータリストをカンマ区切りで指定します。次に例を示します。
Function1 proc dwValue:DWORD add eax, 100 ret Function1 endp MainFunction proc mov eax, 100 invoke Function1, eax ; eax now = 200, i.e. eax += 100 ret MainFunction endp
関数名と最初のパラメータの間にカンマを指定することに注意してください。
ローカルメモリ
MASM32では、関数にローカルなメモリを割り当て、このメモリにラベルを付けることができます。これはローカル変数と同じように考えられますが、対応するマシン語コードを分析してみると、メモリアクセスの変形にすぎないということがわかります。
ローカルメモリは関数の冒頭で定義します。マシン語コードを分析してみると、実際には、関数内の最初の命令の前に静的なメモリブロックを割り当てていることがわかります。このメモリのサイズは、MASM32の基本型(BYTE
、WORD
、またはDWORD
)によって決定されます。
ExampleLocalMemory proc LOCAL dwValue:DWORD ; allocates 4 bytes and labels it 'dwValue' LOCAL wValue:WORD ; allocates 2 bytes and labels it 'wValue' LOCAL bValue:BYTE ; allocates 1 byte and labels it 'bValue' xor eax, eax mov dwValue, eax mov wValue, ax mov bValue, al ret ExampleLocalMemory endp
最適化
効率的なコードを書くためには、すべての命令に同じ時間がかかるわけではないということを理解する必要があります。たとえばmul
命令やdiv
命令は、shr
命令やshl
命令のようなビットシフト演算よりも時間がかかります。それぞれの演算にどのくらいの時間がかかるかは、MASM32のヘルプファイルに記載されています。
効率的なコードを書くためにもう1つ注意しなければならないのは、ループ内に含まれる命令の数です。命令数が少なければ少ないほど、コードの実行速度は速くなります。
メモリへのアクセスはレジスタへのアクセスよりも遅いので、コードを記述するときは、関数のローカルメモリよりもレジスタを使用するように努めてください。
また、jmp
命令の速度はジャンプするバイト数によって左右されます。jmp
命令は8、16、または32ビットのオフセットを取り、32ビットのジャンプよりも8ビットのジャンプの方がずっと高速です。ループについても同じことが言えます。命令のサイズが128バイト未満のループは、長いコードブロックを含むループよりも効率的に実行されます。
最も重要なのはアルゴリズムそのものです。高速なアルゴリズムとは、単純なアルゴリズムです。アルゴリズムが単純であれば、必要な命令の数も少なくなるからです。特定のタスクに使用しているアルゴリズムを見直すことが大切です。ある程度の正確性または柔軟性を犠牲にすることで大幅な速度向上が見込める場合は、速度の方を優先すべきです。
アセンブラコードの最適化については、この他にも検討すべき点が数多くあります。コードの微調整に関しては、MASM32のヘルプファイルが大いに役立つでしょう。
まとめ
今回のシリーズはほんの導入であり、これですべてがわかるというものではありません。もっと詳しく知りたい方は、MASM32に付属しているチュートリアルとヘルプファイルを参照してください。
このシリーズを通じて、アセンブラのコーディングは難しいものではないと理解していただければ嬉しく思います。ぜひアセンブラを使用して、自作のアプリケーションの高速化や、リアルタイム処理の実現に挑戦してみてください。
この全3回のチュートリアルが皆さんのお役に立てば幸いです。