このセクションでは、 dbx を使用して、浮動小数点例外の原因を調査し、例外が発生した命令を特定する方法の例を示します。dbx のソースレベルのデバッグ機能を使用するには、プログラムを –g フラグを指定してコンパイルする必要があります。詳細は、Oracle Solaris Studio 12.4: dbx コマンドによるデバッグ を参照してください。
次の C プログラムについて考察します。
#include <stdio.h> #include <math.h> double sqrtm1(double x) { return sqrt(x) - 1.0; } int main(void) { double x, y; x = -4.2; y = sqrtm1(x); printf("%g %g\n", x, y); return 0; }
このプログラムをコンパイルして実行すると、次のように出力されます。
-4.2 NaN
出力に NaN が表示されているのは、無効な演算例外が発生した可能性があることを示しています。それを判別するには、–ftrap オプションを指定して再コンパイルすることによって無効な演算のトラップを有効化し、dbx を使用してプログラムを実行して、SIGFPE シグナルが送信されたときに停止します。または、無効な演算のトラップを有効にする起動ルーチンとリンクするか、手動でトラップを有効にすると、プログラムを再コンパイルすることなく dbx を使用できます。
浮動小数点例外の原因であるコードを特定するためのもっとも簡単な方法は、–g フラグおよび –ftrap フラグを指定して再コンパイルしてから、dbx を使用して例外が発生している場所を追跡することです。まず、プログラムを次のように再コンパイルします。
example% cc -g -ftrap=invalid ex.c -lm
–g を指定してコンパイルすると、dbx のソースレベルのデバッグ機能を使用できるようになります。–ftrap=invalid を指定すると、無効な演算例外のトラップを有効にしてプログラムが実行されます。次に、dbx を起動して、SIGFPE が発生したときに停止するように catch fpe コマンドを発行し、プログラムを実行します。SPARC ベースのシステムでは、結果は次のようになります。
example% dbx a.out Reading a.out Reading ld.so.1 Reading libm.so.2 Reading libc.so.1 (dbx) catch fpe (dbx) run Running: a.out (process id 2773) signal FPE (invalid floating point operation) in __sqrt at 0x7fa9839c 0x7fa9839c: __sqrt+0x005c: srlx %o1, 63, %l5 Current function is sqrtm1 5 return sqrt(x) - 1.0; (dbx) print x x = -4.2 (dbx)
この出力は、負数の平方根を求めようとした結果、sqrtm1 関数で例外が発生したことを示しています。
dbx を使用すると、ライブラリルーチンなどの –g を指定してコンパイルされていないコードで例外の原因を識別することもできます。この場合、dbx はソースファイルおよび行番号を返すことはできませんが、例外が発生した命令を示すことができます。ここでも、最初の手順では –ftrap を指定して主プログラムを再コンパイルします。
example% cc -ftrap=invalid ex.c -lm
dbx を起動して、catch fpe コマンドを使用し、プログラムを実行します。無効な演算例外が発生すると、dbx は例外の原因となった命令の次の命令で停止します。例外の原因となった命令を特定するには、いくつかの命令を逆アセンブルし、dbx が停止した命令より前にある最後の浮動小数点命令を探します。SPARC ベースのシステムでは、結果は次の出力のようになります。
example% dbx a.out Reading a.out Reading ld.so.1 Reading libm.so.2 Reading libc.so.1 (dbx) catch fpe (dbx) run Running: a.out (process id 2931) signal FPE (invalid floating point operation) in __sqrt at 0x7fa9839c 0x7fa9839c: __sqrt+0x005c: srlx %o1, 63, %l5 (dbx) dis __sqrt+0x50/4 dbx: warning: unknown language, 'c' assumed 0x7fa98390: __sqrt+0x0050: neg %o4, %o1 0x7fa98394: __sqrt+0x0054: srlx %o2, 63, %l6 0x7fa98398: __sqrt+0x0058: fsqrtd %f0, %f2 0x7fa9839c: __sqrt+0x005c: srlx %o1, 63, %l5 (dbx) print $f0f1 $f0f1 = -4.2 (dbx) print $f2f3 $f2f3 = -NaN.0 (dbx)
この出力は、例外の原因が fsqrtd であったことを示しています。ソースレジスタを検査すると、負数の平方根を求めようとしたためにこの例外が発生したことがわかります。
x86 ベースのシステムでは、命令が固定長ではないため、コードの逆アセンブルを開始する正しいアドレスを見つけるには試行錯誤が必要になることがあります。この例では、関数の先頭付近で例外が発生しているので、ここから逆アセンブルできます。この出力は、–xlibmil フラグを指定してプログラムがコンパイルされていることを想定しています。一般的な結果は次の出力のようになります。
example% dbx a.out Reading a.out Reading ld.so.1 Reading libc.so.1 (dbx) catch fpe (dbx) run Running: a.out (process id 18566) signal FPE (invalid floating point operation) in sqrtm1 at 0x80509ab 0x080509ab: sqrtm1+0x001b: fstpl 0xffffffe0(%ebp) (dbx) dis sqrtm1+0x16/5 dbx: warning: unknown language, 'c' assumed 0x080509a6: sqrtm1+0x0016: fsqrt 0x080509a8: sqrtm1+0x0018: addl $0x00000008,%esp 0x080509ab: sqrtm1+0x001b: fstpl 0xffffffe0(%ebp) 0x080509ae: sqrtm1+0x001e: fwait 0x080509af: sqrtm1+0x001f: movsd 0xffffffe0(%ebp),%xmm0 (dbx) print $st0 $st0 = -4.20000000000000017763568394002504647e+00 (dbx)
この出力は、例外の原因が fsqrt 命令であったことを示しています。浮動小数点レジスタを調べると、負数の平方根を求めようとしたためにこの例外が発生したことがわかります。
上記の例では、–ftrap フラグを指定して主サブプログラムを再コンパイルすることによって、無効な演算例外のトラップを有効にしました。場合によっては、主プログラムの再コンパイルを行うことができず、ほかの方法でトラップを有効にする必要があることがあります。これを行うにはいくつかの方法があります。
dbx を使用しているときに、浮動小数点ステータスレジスタを直接変更することによって、トラップを手動で有効にできます。オペレーティングシステムはプログラム内で浮動小数点ユニットが最初に使用されるまで浮動小数点ユニットを使用可能にせず、その時点で浮動小数点の状態が初期化され、すべてのトラップが無効になるため、これには注多少の注意が必要です。したがって、プログラムが少なくとも 1 つの浮動小数点命令を実行するまでは、トラップを手動で有効にできません。この例では、sqrtm1 関数が呼び出されるまでに浮動小数点ユニットはアクセスされているので、この関数への入口にブレークポイントを設定して、無効な演算例外のトラップを有効化し、SIGFPE シグナルの受信時に停止するように dbx に指示して、実行を継続できます。SPARC ベースのシステムでの手順は次のようになります。無効な演算例外のトラップを有効にするために、assign コマンドを使用して %fsr を変更しています。
example% dbx a.out Reading a.out ... etc. (dbx) stop in sqrtm1 dbx: warning: 'sqrtm1' has no debugger info -- will trigger on first instruction (2) stop in sqrtm1 (dbx) run Running: a.out (process id 23086) stopped in sqrtm1 at 0x106d8 0x000106d8: sqrtm1 : save %sp, -0x70, %sp (dbx) assign $fsr=0x08000000 dbx: warning: unknown language, 'c' assumed (dbx) catch fpe (dbx) cont signal FPE (invalid floating point operation) in __sqrt at 0xff36b3c4 0xff36b3c4: __sqrt+0x003c: be __sqrt+0x98 (dbx)
x86 ベースのシステムでは、同じ手順は次のようになります。
example% dbx a.out Reading a.out ... etc. (dbx) stop in sqrtm1 dbx: warning: 'sqrtm1' has no debugger info -- will trigger on first instruction (2) stop in sqrtm1 (dbx) run Running: a.out (process id 25055) stopped in sqrtm1 at 0x80506b0 0x080506b0: sqrtm1 : pushl %ebp (dbx) assign $fctrl=0x137e dbx: warning: unknown language, 'c' assumed (dbx) catch fpe (dbx) cont signal FPE (invalid floating point operation) in sqrtm1 at 0x8050696 0x08050696: sqrtm1+0x0016: fstpl -16(%ebp) (dbx)
上記の例では、assign コマンドによって、浮動小数点制御ワード内の無効な演算例外がアンマスクされています (つまり、トラップを有効にしています)。プログラムで SSE2 命令を使用している場合は、MXCSR レジスタの例外をアンマスクして、それらの命令によって発生した例外のトラップを有効にする必要があります。
トラップを有効にする初期化ルーチンを設定すると、主プログラムを再コンパイルしたり、dbx を使用したりすることなくトラップを有効にできます。この方法は、デバッガで実行せずに、例外が発生したときにプログラムを中止する場合などに便利です。このようなルーチンを作成する方法は 2 つあります。
プログラムを構成するオブジェクトファイルとライブラリが使用可能な場合は、プログラムを適切な初期化ルーチンに再リンクすることによってトラップを有効にできます。最初に、次のような C のソースファイルを作成します。
#include <ieeefp.h> #pragma init (trapinvalid) void trapinvalid() { /* FP_X_INV et al are defined in ieeefp.h */ fpsetmask(FP_X_INV); }
このファイルをコンパイルしてオブジェクトファイルを作成し、元のプログラムをこのオブジェクトファイルにリンクします。
example% cc -c init.c example% cc ex.o init.o -lm example% a.out Arithmetic Exception
再リンクを行うことはできないが、プログラムが動的にリンクされている場合は、実行時リンカーの、共有オブジェクトを事前ロードする機能を使用するとトラップを有効にすることができます。SPARC ベースのシステムでこれを行うには、上記と同じ C ソースファイルを作成して、次のようにコンパイルします。
example% cc -Kpic -G -ztext init.c -o init.so -lc
トラップを有効にするには、init.so オブジェクトのパス名を、環境変数 LD_PRELOAD によって指定される事前ロードする共有オブジェクトのリストに追加します。
example% env LD_PRELOAD=./init.so a.out Arithmetic Exception
共有オブジェクトの作成および事前ロードについては、Oracle Solaris 11.2 リンカーとライブラリガイド を参照してください。
前述したように、浮動小数点の制御モードの初期化方法は、共有オブジェクトを事前ロードすることによって原則として変更できます。ただし、共有オブジェクト内の初期化ルーチンは、事前ロードされる場合も明示的にリンクされる場合も、メインの実行可能ファイルの一部である起動コードに制御を渡す前に、実行時リンカーによって実行されます。起動コードは、–ftrap、–fround、–fns (SPARC)、または –fprecision (x86) コンパイラフラグを介して選択される非デフォルトモードを設定し、メインの実行可能ファイルの一部である初期化ルーチン (静的にリンクされているものを含む) を実行して、最後に制御を主プログラムに渡します。このため、SPARC では次のことに留意してください。
上記の例で有効にされたトラップのような、共有オブジェクト内の初期化ルーチンによって設定される浮動小数点制御モードはすべて、オーバーライドされないかぎりプログラムの実行中に有効な状態のままになります。
コンパイラフラグを介して選択された非デフォルトモードは、共有オブジェクト内の初期化ルーチンによって設定されるモードを無効にします (ただし、コンパイラフラグを介して選択されたデフォルトモードは、以前に設定されているモードを無効にしません)。
メインの実行可能ファイルの一部である初期化ルーチン、または主プログラム自体によって設定されるモードは、両方とも無効にします。
x86 ベースのシステムでは状況はもう少し複雑です。通常、コンパイラによって自動的に提供される起動コードは、__fpstart ルーチン (標準 C ライブラリ libc にあります) を呼び出すことによってすべての浮動小数点モードをデフォルトにリセットしてから、–fround、–ftrap、または –fprecision フラグによって選択された非デフォルトモードを設定して、主プログラムに制御を渡します。そのため、初期化ルーチンを持つ共有オブジェクトを事前ロードすることによって x86 ベースのシステムでトラップを有効にしたり、ほかのデフォルトの浮動小数点モードを変更したりするには、デフォルトの浮動小数点モードを __fpstart ルーチンがリセットしないように、このルーチンを無効にする必要があります。ただし、代替の __fpstart ルーチンは、標準のルーチンが行う初期化機能の残りの部分を実行する必要があります。次のコードは、これを行うための 1 つの方法を示しています。このコードは、ホスト プラットフォームで Oracle Solaris 10 OS 以降のリリースが実行されていることを想定しています。
#include <ieeefp.h> #include <sys/sysi86.h> #pragma init (trapinvalid) void trapinvalid() { /* FP_X_INV et al are defined in ieeefp.h */ fpsetmask(FP_X_INV); } extern int __fltrounds(), __flt_rounds; extern int _fp_hw, _sse_hw; void __fpstart() { /* perform the same floating point initializations as the standard __fpstart() function but leave all floating point modes as is */ __flt_rounds = __fltrounds(); (void) sysi86(SI86FPHW, &_fp_hw); /* set the following variable to 0 instead if the host platform does not support SSE2 instructions */ _sse_hw = 1; }