x87 命令の使い方
x87 命令の一覧は、x87 命令 で確認してください。
x87 命令は、以下のような手順で使用します。
x87 命令のオペランドには、x87 レジスタまたはメモリしか指定できません。
即値や汎用レジスタは指定できないため、それらの値をロードしたい場合は、まずスタックなどのメモリ領域に格納してから、x87 命令で値をプッシュする形になります。
また、読み込み先のレジスタ位置は直接指定できないため、常に TOP をデクリメントした後の ST(0) が格納先になります。
スタックのように値を積み重ねてから、演算で使用する形になります。
x87 レジスタは8個なので、最大で8つの値をロードできます。
一番最後に格納した値は ST(0)、その前に格納した値は ST(1)、というように参照することになります。
基本的に、ST(0) と ST(1) の値が対象になる場合が多いです。
ポップとは、スタックの先頭の値を解放して (スタック先頭のレジスタを空にする)、TOP をインクリメントし、一つ前の位置に戻ることを意味します。
演算後の ST(1) には、結果の値が入っていますが、TOP をインクリメントすることで、スタックの先頭位置が一つ前に戻るので、ポップ後の ST(0) は、演算結果の値の位置になります。
解放されたレジスタは空の状態になるので、その後、新しい値をロードすることができます。
x87 命令では、演算後に同時にポップを行うものと、行わないものがあります。
末尾に P が付いているものは、演算後にポップを行います。
つまり、レジスタを一度もポップ (または解放) しないまま、何度も値をロードすることはできません。
このような場合、スタックオーバーフローが発生します。
レジスタに値をプッシュしたら、その値が不要になったタイミングで、適切にポップしていく必要があります。
また、一連の演算で結果を算出した後は、後続の x87 命令のために、すべてのレジスタは空の状態に戻しておくことが必要です。
もしくは、演算を行う前に、x87 の状態を初期化することもできます。
32bit 単精度や 64bit 倍精度などのフォーマットに変換することができます。
その後、メモリから汎用レジスタに値をコピーするなどします。
x87 命令は、以下のような手順で使用します。
- 演算に必要な値を x87 レジスタにプッシュする。
- 演算命令を使って、レジスタの値を演算する。
- 必要がなくなったレジスタの値をポップする。
- 結果のレジスタの値は、一度メモリにコピーしてから、汎用レジスタにロードするなどして使用する。
値を x87 レジスタにプッシュする
まずは、演算に必要な値を、x87 レジスタにプッシュする必要があります。x87 命令のオペランドには、x87 レジスタまたはメモリしか指定できません。
即値や汎用レジスタは指定できないため、それらの値をロードしたい場合は、まずスタックなどのメモリ領域に格納してから、x87 命令で値をプッシュする形になります。
また、読み込み先のレジスタ位置は直接指定できないため、常に TOP をデクリメントした後の ST(0) が格納先になります。
スタックのように値を積み重ねてから、演算で使用する形になります。
x87 レジスタは8個なので、最大で8つの値をロードできます。
一番最後に格納した値は ST(0)、その前に格納した値は ST(1)、というように参照することになります。
演算を行う
x87 の演算命令を使って、x87 レジスタの値を元に演算を行います。基本的に、ST(0) と ST(1) の値が対象になる場合が多いです。
必要のないレジスタの値をポップ
例えば、ST(1) + ST(0) = ST(1) という演算において、加算後の結果の値だけが必要で、ST(0) の値 (スタック先頭の値) はもう使わないから破棄したいという場合、レジスタをポップする必要があります。ポップとは、スタックの先頭の値を解放して (スタック先頭のレジスタを空にする)、TOP をインクリメントし、一つ前の位置に戻ることを意味します。
演算後の ST(1) には、結果の値が入っていますが、TOP をインクリメントすることで、スタックの先頭位置が一つ前に戻るので、ポップ後の ST(0) は、演算結果の値の位置になります。
解放されたレジスタは空の状態になるので、その後、新しい値をロードすることができます。
x87 命令では、演算後に同時にポップを行うものと、行わないものがあります。
末尾に P が付いているものは、演算後にポップを行います。
FINCSTP 命令で TOP をインクリメントすることと、レジスタをポップすることは、同じではありません。
FINCSTP 命令は、レジスタを空にはしないため、単純にスタックの現在位置を移動するだけになります。
FINCSTP 命令は、レジスタを空にはしないため、単純にスタックの現在位置を移動するだけになります。
スタックオーバーフロー
タグワード・レジスタの値によって、8つのレジスタがすべて空ではない状態になっている場合は、新しい値をロードすることができません。つまり、レジスタを一度もポップ (または解放) しないまま、何度も値をロードすることはできません。
このような場合、スタックオーバーフローが発生します。
レジスタに値をプッシュしたら、その値が不要になったタイミングで、適切にポップしていく必要があります。
また、一連の演算で結果を算出した後は、後続の x87 命令のために、すべてのレジスタは空の状態に戻しておくことが必要です。
もしくは、演算を行う前に、x87 の状態を初期化することもできます。
結果の値
x87 の演算結果を、汎用レジスタなどで使用したい場合は、レジスタの値を、任意の形式でメモリにコピーします。32bit 単精度や 64bit 倍精度などのフォーマットに変換することができます。
その後、メモリから汎用レジスタに値をコピーするなどします。
System V AMD64 ABI
アセンブラの関数内で、x87 命令や MMX 命令を使用する場合、以下の点に注意してください。
- 関数に入った時は、x87 レジスタが使用できる状態 (レジスタがすべて空の状態) である必要があります。
MMX 命令を使用して、MMX レジスタの値が変更された場合、すべての x87 レジスタが空ではない状態になるため、MMX 命令を使った後、関数から戻る時、または別の関数に入る前に、EMMS 命令で、x87 レジスタの状態を空にする必要があります。 - コントロールワード・レジスタの、精度や丸めの設定は、関数内で変更した場合、元の値に戻しておく必要があります。
- 他の関数を呼ぶ前に、ステータスワード・レジスタの状態を維持しておく必要がある場合は、値を保存しておきます。
サンプルコード
x87 で、100 * sin(-45度) を計算して、戻り値で値を返すサンプルです。
角度をラジアン単位に変換するには、度 * PI / 180 を計算します。
<26_x87.c>
<26_x87.c>
x87 での計算と、C での計算結果が同じになっています。
角度をラジアン単位に変換するには、度 * PI / 180 を計算します。
<26_x87.c>
#include <stdio.h> #include <math.h> double testfunc(void); int main(void) { double d = testfunc(); printf("asm: %.6f\nc: %.6f\n", d, 100.0 * sin(-45 * 3.14159265359 / 180)); return 0; }
<26_x87.c>
global testfunc section .text ; 戻り値 = 100 * sin(-45度) testfunc: push rbp mov rbp, rsi sub rsi, 16 ; メモリに 64bit 即値は代入できないので、レジスタを介してコピー ; RBX は退避しなければならないので、使わない mov rax, __?float64?__(-0.7853981634) ; -45 * PI / 180 mov rcx, __?float64?__(100.0) mov qword [rbp-16], rax mov qword [rbp-8], rcx fld qword [rbp-16] ; st0 = ラジアン fsin ; sin(st0) -> st0 fld qword [rbp-8] ; st0 = 100, st1 = fsin の結果 fmulp ; st1 * st0 -> st1 & POP ; st0 に結果が入っている fstp qword [rbp-8] ; st0 -> double & POP movq xmm0, [rbp-8] ; xmm0 に 64bit 転送 (ゼロ拡張) ; double の戻り値は xmm0 に格納 mov rsi, rbp pop rbp ret
$ nasm -f elf64 26_x87.asm $ cc -o test 26_x87.c 26_x87.o $ ./test asm: -70.710678 c: -70.710678
x87 での計算と、C での計算結果が同じになっています。
解説
-45 度のラジアン値と、100.0 の値 は、倍精度浮動小数点数として、スタックに値を格納しておきます。
なお、MOV 命令では、メモリ位置に 64bit 即値を代入することができないため、一度レジスタにコピーしてから、メモリに格納します。
まず、sin(-45度) を計算するために、-45 度のラジアン値をプッシュします。
その後、FSIN 命令で、sin(st0) を計算して、結果を st0 に格納します (st0 の値が置き換わります)。
次に、その結果に 100.0 を掛けるため、新しい値 100.0 をプッシュします。
この時、「st0 = 100.0」「st1 = sin の結果」となります。
FMULP 命令で、st1 * st0 を演算した後、結果を st1 に格納して、同時にレジスタをポップします。
これにより、スタック先頭の 100.0 の値が破棄され、st0 = 乗算結果の値となります。
結果の値が計算できたので、FSTP 命令を使って、st0 の値を倍精度形式に変換してメモリに格納し、レジスタをポップします。
これにより、すべての x87 レジスタが空の状態になります。
後は、結果の値を、関数の戻り値として返します。
浮動小数点数の場合は、XMM0 レジスタに値を格納する必要があるため、MOVQ 命令でメモリから値を読み込みます。
XMM0 レジスタは 128bit ですが、MOVQ 命令によって、下位 64bit に値が格納されると、上位 64bit は 0 になります (ゼロ拡張)。
結果の値が負なので、128bit に符号拡張する必要があるような気がしますが、実際は、C 言語の方で、戻り値が double 型として明確に指定されているので、XMM0 の上位 64bit は無視されます。
返ってきた値は、C 言語の計算結果と同じになっているので、x87 での計算は正しく行えています。
なお、MOV 命令では、メモリ位置に 64bit 即値を代入することができないため、一度レジスタにコピーしてから、メモリに格納します。
まず、sin(-45度) を計算するために、-45 度のラジアン値をプッシュします。
その後、FSIN 命令で、sin(st0) を計算して、結果を st0 に格納します (st0 の値が置き換わります)。
次に、その結果に 100.0 を掛けるため、新しい値 100.0 をプッシュします。
この時、「st0 = 100.0」「st1 = sin の結果」となります。
FMULP 命令で、st1 * st0 を演算した後、結果を st1 に格納して、同時にレジスタをポップします。
これにより、スタック先頭の 100.0 の値が破棄され、st0 = 乗算結果の値となります。
結果の値が計算できたので、FSTP 命令を使って、st0 の値を倍精度形式に変換してメモリに格納し、レジスタをポップします。
これにより、すべての x87 レジスタが空の状態になります。
後は、結果の値を、関数の戻り値として返します。
浮動小数点数の場合は、XMM0 レジスタに値を格納する必要があるため、MOVQ 命令でメモリから値を読み込みます。
XMM0 レジスタは 128bit ですが、MOVQ 命令によって、下位 64bit に値が格納されると、上位 64bit は 0 になります (ゼロ拡張)。
結果の値が負なので、128bit に符号拡張する必要があるような気がしますが、実際は、C 言語の方で、戻り値が double 型として明確に指定されているので、XMM0 の上位 64bit は無視されます。
返ってきた値は、C 言語の計算結果と同じになっているので、x87 での計算は正しく行えています。