論理演算
ビット単位の論理演算を行うには、算術演算と同様に論理演算を行う専用の命令を使います。論理演算を行う命令の名前は明快です。論理積を求める命令はAND、論理和を求める命令はOR、排他的論理和を求める命令はXORとなります。
and dest, src
or dest, src
xor dest, src
これらの命令は、srcにソース・オペランドを、destにディスティネーション・オペランドを設定し、ソース・オペランドとディスティネーション・オペランドとの論理演算を行い、その結果をディスティネーション・オペランドにストアします。ディスティネーション・オペランドにはレジスタまたはメモリ領域を指定し、ソース・オペランドには即値、レジスタ、またはメモリ領域を指定することができます。
#include <stdio.h> int main() { int and_r, or_r, xor_r; __asm { mov eax, 0xFF00FF00; and eax, 0x0F0FF00F; mov and_r, eax; mov eax, 0xFF00FF00; or eax, 0x0F0FF00F; mov or_r, eax; mov eax, 0xFF00FF00; xor eax, 0x0F0FF00F; mov xor_r, eax; } printf("and=%X, or=%X, xor=%X\n", and_r, or_r, xor_r); return 0; }
and=F00F000, or=FF0FFF0F, xor=F00F0F0F
sample07は、ディスティネーション・オペランドの値を0xFF00FF00で、ソース・オペランドの値を0x0F0F00Fで、AND、OR、XORの命令をそれぞれ実行しています。実行結果を見れば、適切に論理積、論理和、排他的論理和が求められていることを確認できます。このサンプルではソース・オペランドをすべて即値で指定していますが、これらの命令のソース・オペランドにはメモリ領域やレジスタを指定することも可能です。
シフトと回転
レジスタやメモリに保存されている値をビットの列として解釈し、ビットの列を左右に移動させるシフト演算は行うにはシフト命令を使います。シフト命令には、ビット列を左に移動させる左シフト命令と、ビット列を右にシフトさせる右シフト命令があります。
コンピュータの世界では、2進数値の最上位ビットを符号として利用し、最上位ビットが1であれば負数であることを表します。そのため、右シフトするときに最上位ビットの符号を保存したままで、最上位ビット以外をシフトの対象とする算術シフトと、最上位ビットの符号に関係なく、すべてのビットをシフトさせる論理シフトに分かれています。
- 左シフト命令
- 論理シフト命令 :
SHL - 算術シフト命令 :
SAL - 右シフト命令
- 論理シフト命令 :
SHR - 算術シフト命令 :
SAR
こうした構造から、右論理シフトを実行したい場合はSHRを、右算術シフトを実行したい場合はSARを使います。左シフト命令には算術シフトと論理シフトの区別は存在しないのですが、命令としては個別に用意されています。しかし、実行結果はSHL、SAR共に同じです。
shl dest, count
sal dest, count
shr dest, count
sar dest, count
これらの命令の第1オペランドdestはディスティネーション・オペランド、第2オペランドcountはカウント・オペランドと呼ばれています。カウント・オペランドに指定されている値だけ、右または左にディスティネーション・オペランドの値をシフトさせ、その結果をディスティネーション・オペランドにストアします。ディスティネーション・オペランドに即値を指定することはできませんが、カウント・オペランドには即値を指定することができます。
#include <stdio.h> int main() { int a = 100, b = 40, c = -10, d = -1; __asm { shl a, 2; shr b, 3; sar c, 1; shr d, 1; } printf("a=%d, b=%d, c=%d, d=%d\n", a, b, c, d); return 0; }
a=400, b=5, c=-5, d=2147483647
sample08は、変数a、b、c、dを宣言し、それぞれを適当な値で初期化しています。これらの変数をアセンブリ言語から右および左シフトし、その結果を出力しています。最初にSHL命令を用いてa変数を左に2回シフトしています。ビット列の性質から、左に1回シフトするということは2で乗算することに等しいため、結果としてaの値は400となります。次にb変数をSHR命令で右に3回シフトしています。右に1回シフトすると言うということは2で除算することに等しいため、bの値は5となります。
c変数とd変数の値はそれぞれ負数になっているため、符号用の最上位ビットが1となっています。このような符号付きの値に対しては算術シフトと論理シフトで結果が異なります。算術シフトを行った場合は最上位ビットが維持されたまま右にシフトされるため符号が残されますが、論理シフトを行った場合は最上位ビットを含めてビット列全体をシフトし、最上位ビットが0で埋められるため符号が反転します。
シフト命令の場合、シフトによってビット列から漏れた値は破棄されてしまいます。例えば、右に1回シフトするとシフトの場合は最下位のビットがビット列から漏れ出して破棄され、最上位ビットが0で埋められます。これに対して、シフトによって漏れたビットを反対側のビットに埋める計算があります。これを回転と呼びます。
左シフトの回転はROL命令を使い、右シフトの回転はROR命令を使います。
rol dest, count
ror dest, count
destには回転対象となるディスティネーション・オペランド、countには左右に何回シフトするのかを指定するカウント・オペランドです。オペランドの指定方法は基本的にシフト演算と変わりません。これらの命令ではシフトによって漏れ出したビットを反対側のビットに設定します。つまり、ビット列が回転します。
#include <stdio.h> int main() { int a = 0xFF00FF00, b = 0x0F0F0F0F; __asm { rol a, 4; ror b, 4; } printf("a=%X, b=%X\n", a, b); return 0; }
a=F00FF00F, b=F0F0F0F0
sample09は、int型の変数aとbを用意し、これらの変数を左右に回転させています。a変数は初期値で0xFF00FF00という値を持っていますが、これを左に4回シフトすると、最上位の4ビットが最下位に回転によって移動するため0xF00FF00Fという結果が得られています。同様に、0x0F0F0F0Fという値を持つb変数を右に4回シフトすると、最下位4ビットが回転によって移動するため0xF0F0F0F0という結果が得られています。
この他に、回転命令には漏れ出した1ビットをキャリーフラグと呼ばれる特殊なレジスタに保存し、キャリーフラグを含めて回転を行う命令があります。キャリー通過左回転はRCL、キャリー通過右回転はRCR命令を使います。RCLとRCRも、キャリーフラグ1ビットが回転するビット列に加わるという点を除いて、基本的に通常の回転と同じです。
おわりに
本稿では、インラインアセンブラ内で基本的な計算を行う方法を解説してきました。これらの計算はC言語を用いれば簡単に行うことができるため、特別な事情がない限りプログラム内で使われることはないと考えられます。しかし、プログラムの事情からレジスタを直接操作しなければならない場合や、ゲームなどで描画用のイメージをより効率的に計算させる手段としてインラインアセンブラが使われるというシナリオは現実的に考えられるものであります。
今後、より高度なアセンブリ言語の命令を学習するためにも、こうした基本的な計算命令は必要な知識となるでしょう。また、こうした基礎的な計算プログラムを通してアセンブリ言語の面白さを体感していただければ幸いです。複雑なプログラムをアセンブリ言語で記述した場合は可読性も低くなり、初心者にとっては学ぶ意欲が失われてしまうかと思います。プロフェッショナルでも、他人が書いたアセンブリ言語のプログラムを読みたいと思うものではありません。
数行の計算プログラムでは実用的なプログラムを作ることはできませんが、こうした単純な命令からアセンブリ言語を少しずつ学んでいただければ、高度で複雑といわれているアセンブリ言語も難なく身に付けることができます。
参考資料
- CodeZine 『インラインアセンブラによるアセンブリプログラミング体験』 赤坂玲音 著、翔泳社、2006年6月
- IA-32 インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル 中巻
- 『はじめて読む 8086』 蒲池輝尚 著、アスキー、1987年3月
