Oracle® Developer Studio 12.5: 数値計算ガイド

印刷ビューの終了

更新: 2016 年 6 月
 
 

4.4 例外の特定

例外が発生した場所を特定する方法の 1 つは、プログラム内のさまざまな箇所で例外フラグをテストすることです。ただし、この方法で正確に例外を特定するには、多くのテストが必要になり、大きなオーバーヘッドを伴う可能性があります。

例外が発生した場所を判別する簡単な方法は、例外のトラップを有効にすることです。トラップが有効にされている例外が発生すると、オペレーティングシステムは SIGFPE シグナルを送信することによってプログラムに通知します。signal(5) のマニュアルページを参照してください。したがって、例外のトラップを有効にすると、デバッガで実行して SIGFPE シグナルを受信した時点で停止するか、例外が発生した命令のアドレスを出力するように SIGFPE ハンドラを設定して、例外の発生箇所を判別できます。SIGFPE シグナルが生成されるようにするには、例外のトラップを有効にしておく必要があります。トラップが無効になっている場合に例外が発生すると、対応するフラグが設定され、実行は表 31 に示されているデフォルトの結果で継続されますが、シグナルは送信されません。

4.4.1 デバッガを使用して例外を特定する

このセクションでは、 dbx を使用して、浮動小数点例外の原因を調査し、例外が発生した命令を特定する方法の例を示します。dbx のソースレベルのデバッグ機能を使用するには、プログラムを –g フラグを指定してコンパイルする必要があります。詳細は、Oracle Developer Studio 12.5: 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 を使用できます。

4.4.1.1 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 命令であったことを示しています。浮動小数点レジスタを調べると、負数の平方根を求めようとしたためにこの例外が発生したことがわかります。

4.4.1.2 再コンパイルせずにトラップを有効にする

上記の例では、–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.3 リンカーとライブラリガイドを参照してください。

前述したように、浮動小数点の制御モードの初期化方法は、共有オブジェクトを事前ロードすることによって原則として変更できます。ただし、共有オブジェクト内の初期化ルーチンは、事前ロードされる場合も明示的にリンクされる場合も、メインの実行可能ファイルの一部である起動コードに制御を渡す前に、実行時リンカーによって実行されます。起動コードは、–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;
}

4.4.2 シグナルハンドラを使用して例外を特定する

前のセクションでは、例外の最初の発生を特定するためにプログラムの始めにトラップを有効にする方法をいくつか紹介しました。これに対して、プログラム内でトラップを有効にすることによって例外の特定の発生を見つけることもできます。トラップを有効にしていても、SIGFPE ハンドラをインストールしていない場合は、トラップされた例外の次の発生時にプログラムが中止されます。SIGFPE ハンドラをインストールしてある場合は、トラップされた例外が次に発生すると、システムは制御をハンドラに渡し、ハンドラは例外が発生した命令のアドレスなどの診断情報を出力して、実行を中止するか、再開します。実行を再開して意味のある結果を得るには、次のセクションで説明するように、例外演算の結果をハンドラで設定する必要がある場合があります。

ieee_handler を使用すると、5 つの IEEE 浮動小数点例外のすべてのトラップを有効にすると同時に、指定した例外が発生したときにプログラムを中止するように設定するか、SIGFPE ハンドラを設定できます。SIGFPE ハンドラは、低レベルの関数 (sigfpe(3)、signal(3c)、または sigaction(2)) を使用してインストールすることもできますが、ieee_handler とは異なり、これらの関数ではトラップは有効になりません。浮動小数点例外は、そのトラップが有効である場合にのみ SIGFPE シグナルをトリガーできます。

4.4.2.1 ieee_handler (3m)

ieee_handler を呼び出すための構文は、次のとおりです。

i = ieee_handler(action, exception, handler)

2 つの入力パラメータ action および exception は文字列です。3 番目のパラメータ handlersigfpe_handler_type 型であり、floatingpoint.h に定義されています。

3 つの入力パラメータには次の値を指定できます。

入力パラメータ
C または C++ の型
指定できる値
action
char *
getsetclear
exception
char *
invaliddivisionoverflow
underflowinexact
allcommon
ハンドラ
sigfpe_handler_type
ユーザー定義のルーチン
SIGFPE_DEFAULT
SIGFPE_IGNORE
SIGFPE_ABORT

要求された action が "set" の場合、ieee_handler は exception に指定された例外について、handler に指定された処理関数を設定します。処理関数は、デフォルトの IEEE 動作を選択する SIGFPE_DEFAULT または SIGFPE_IGNORE、指定した例外が発生するとプログラムを中止させる SIGFPE_ABORT、指定した例外のいずれかが発生するとサブルーチン (SA_SIGINFO フラグを設定してインストールしたシグナルハンドラのパラメータ (sigaction(2) のマニュアルページを参照) が渡される) を起動するユーザーが用意したサブルーチンのアドレスのいずれかです。ハンドラが SIGFPE_DEFAULT または SIGFPE_IGNORE の場合、ieee_handler は指定した例外のトラップを無効にし、その他のハンドラの場合、ieee_handler はトラップを有効にします。

x86 プラットフォームでは、例外のトラップが有効にされて、対応するフラグが発生するたびに、浮動小数点ハードウェアがトラップします。そのため、誤ったトラップを防ぐには、ieee_handler を呼び出してトラップを有効にする前に、指定された exception ごとにプログラムでフラグをクリアする必要があります。

要求された action"clear" の場合、ieee_handler は指定した exception について現在インストールされている処理関数を取り消し、そのトラップを無効にします。これは、"set"SIGFPE_DEFAULT を指定した場合と同じです。action"clear"の場合、3 番目のパラメータは無視されます。

action が "set" および "clear" のいずれの場合も、ieee_handler は要求された操作を実行できる場合はゼロを返し、それ以外の場合はゼロ以外の値を返します。

要求された action"get" の場合、ieee_handler は指定した exception について現在インストールされているハンドラのアドレスを返します (ハンドラがインストールされていない場合は SIGFPE_DEFAULT を返します)。

次の例は、ieee_handler の使用方法を表すいくつかのコードの一部を示しています。この C のコードは、0 による除算が発生した場合にプログラムを中止します。

#include <sunmath.h> 
/* uncomment the following line on x86 systems */
    /* ieee_flags("clear", "exception", "division", NULL); */
    if (ieee_handler("set", "division", SIGFPE_ABORT) != 0)
        printf("ieee trapping not supported here \n");

Fortran の場合は次のコードのようになります。

#include <floatingpoint.h> 
c uncomment the following line on x86 systems
c     ieee_flags('clear', 'exception', 'division', %val(0))
      i = ieee_handler('set', 'division', SIGFPE_ABORT) 
      if(i.ne.0) print *,'ieee trapping not supported here'

次の C のコードは、すべての例外について IEEE のデフォルトの例外処理に戻します。

#include <sunmath.h> 
    if (ieee_handler("clear", "all", 0) != 0) 
        printf("could not clear exception handlers\n");

Fortran の場合は次のようになります。

      i = ieee_handler('clear', 'all', 0) 
      if (i.ne.0) print *, 'could not clear exception handlers'

4.4.2.2 シグナルハンドラからの例外の報告

ieee_handler によってインストールされた SIGFPE ハンドラが呼び出されると、オペレーティングシステムによって、発生した例外のタイプ、例外の原因である命令のアドレス、およびマシンの整数レジスタと浮動小数点レジスタの内容を示す追加の情報が渡されます。ハンドラはこの情報を検査して、例外と例外の発生場所を示すメッセージを出力します。

システムによって提供される情報にアクセスするには、ハンドラを次のように宣言します。この章の以降の部分では、C のコード例を示します。Fortran の SIGFPE ハンドラの例については、を参照してください。

#include <siginfo.h>
#include <ucontext.h>
 
void handler(int sig, siginfo_t *sip, ucontext_t *uap)
{
    ...
}

このハンドラを呼び出すと、送信されたシグナルの番号が sig パラメータに設定されます。シグナル番号は sys/signal.h で定義されています。SIGFPE のシグナル番号は 8 です。

sip パラメータは、シグナルに関する追加情報を記録する構造体を指しています。SIGFPE シグナルの場合、この構造体の関連メンバーは sip‐>si_code および sip->si_addr です (/usr/include/sys/siginfo.h を参照してください)。これらのメンバーの重要性は、システム、および SIGFPE シグナルがトリガーされたイベントによって異なります。

メンバー sip->si_code は、表 33 に示されている SIGFPE シグナルのタイプのいずれかです。表示されているトークンは sys/machsig.h に定義されています。

表 33  算術例外のタイプ
SIGFPE のタイプ
IEEE のタイプ
FPE_INTDIV
n/a
FPE_INTOVF
n/a
FPE_FLTRES
不正確
FPE_FLTDIV
除算
FPE_FLTUND
アンダーフロー
FPE_FLTINV
無効
FPE_FLTOVF
オーバーフロー

上記の表に示されているように、各 IEEE 浮動小数点例外のタイプには対応する SIGFPE シグナルタイプがあります。整数の 0 による除算 (FPE_INTDIV) および整数オーバーフロー (FPE_INTOVF) は SIGFPE のタイプにも含まれていますが、これらは IEEE 浮動小数点例外ではないため、ieee_handler でハンドラをインストールすることはできません。これらの SIGFPE タイプのハンドラは sigfpe(3) を使用すると設定できます。ただし、整数オーバーフローは、デフォルトではすべての SPARC プラットフォームおよび x86 プラットフォームで無視されます。特殊な命令によって FPE_INTOVF タイプの SIGFPE シグナルを発生させることができますが、Sun のコンパイラはこのような命令を生成しません。

IEEE 浮動小数点例外に対応する SIGFPE シグナルの場合、メンバー sip‐>si_code は発生した例外を示します。x86 ベースのシステムでは、実際には、フラグが発生したもっとも優先度の高いアンマスクされた例外が示されます。通常、これは最後に発生した例外と同じです。メンバー sip->si_addr は、SPARC ベースのシステムでは例外の原因である命令のアドレスを保持し、x86 ベースのシステムではトラップされた時点の命令 (通常は例外の原因である命令の次の浮動小数点命令) のアドレスを保持します。

最後に、uap パラメータはトラップされた時点のシステムの状態を記録する構造体を指します。この構造体の内容はシステムによって異なります。いくつかのメンバーの定義については、/usr/include/sys/siginfo.h を参照してください。

オペレーティングシステムから提供される情報を使用すると、発生した例外のタイプと例外の原因である命令のアドレスを報告する SIGFPE ハンドラを記述できます。使用例 1 は、そのようなハンドラを示しています。

使用例 1  SIGFPE ハンドラ
#include <stdio.h>
#include <sys/ieeefp.h>
#include <sunmath.h>
#include <siginfo.h>
#include <ucontext.h>

void handler(int sig, siginfo_t *sip, ucontext_t *uap)
{
    unsigned    code, addr;

    code = sip->si_code;
    addr = (unsigned) sip->si_addr;
    fprintf(stderr, "fp exception %x at address %x\n", code,
        addr);
}

int main()
{
    double  x;

    /* trap on common floating point exceptions */
    if (ieee_handler("set", "common", handler) != 0)
        printf("Did not set exception handler\n");
    /* cause an underflow exception (will not be reported) */
    x = min_normal();
    printf("min_normal = %g\n", x);
    x = x / 13.0;
    printf("min_normal / 13.0 = %g\n", x);

    /* cause an overflow exception (will be reported) */
    x = max_normal();
    printf("max_normal = %g\n", x);
    x = x * x;
    printf("max_normal * max_normal = %g\n", x);
    ieee_retrospective(stderr);
    return 0;
}

SPARC システムでは、このプログラムからの出力は次のようになります。

min_normal = 2.22507e-308
min_normal / 13.0 = 1.7116e-309
max_normal = 1.79769e+308
fp exception 4 at address 10d0c
max_normal * max_normal = 1.79769e+308
Note: IEEE floating-point exception flags raised:
    Inexact;  Underflow; 
IEEE floating-point exception traps enabled:
    overflow; division by zero; invalid operation; 
See the Numerical Computation Guide, ieee_flags(3M), ieee_handler(3M)

x86 プラットフォームでは、オペレーティングシステムが累積例外フラグのコピーを保存し、SIGFPE ハンドラを呼び出す前にそれらをクリアします。ハンドラによって保存されないかぎり、累積フラグはハンドラから戻ったときに失われます。このため、–xarch=386 を指定してコンパイルされていると、上記のプログラムからの出力にはアンダーフロー例外が発生したことが示されません。

min_normal = 2.22507e-308
min_normal / 13.0 = 1.7116e-309
max_normal = 1.79769e+308
fp exception 4 at address 8048fe6
max_normal * max_normal = 1.79769e+308
 Note: IEEE floating-point exception traps enabled: 
    overflow;  division by zero;  invalid operation; 
 See the Numerical Computation Guide, ieee_handler(3M)

ただし、デフォルトの場合、または –xarch=sse2 を指定してコンパイルされている場合は、PC がループ命令を抜けられないため、テストプログラムがループします。Oracle Developer Studio12.5 の場合は、PC をインクリメントするコード行を追加すれば十分です。

uap -> UC_mcontext.gregs[REG_PC= +=5;

上記のコードは、–xarch=sse2 が指定されていて、SSE2 命令の長さが 5 バイトの場合にのみ使用できます。完全に汎用的な SSE2 の解決方法にするには、最適化されたコードをデコードして、次の命令の始まりを見つけます。代わりに fex_set_handling を使用してください。

多くの場合、トラップが有効であれば、例外の原因である命令は IEEE のデフォルトの結果を生成しません。上記の出力では、max_normal * max_normal に報告されている値は、オーバーフローした演算のデフォルトの結果 (つまり、正確な符号付き無限大) ではありません。通常は、意味のある値で計算を続行するために、トラップされた例外の原因である演算の結果を、SIGFPE ハンドラが提供する必要があります。これを行う 1 つの方法については、例外の処理を参照してください。

4.4.3 libm の例外処理拡張機能を使用して例外を特定する

C/C++ プログラムでは、libm にある C99 浮動小数点環境関数の例外処理拡張機能を使用し、いくつかの方法で例外を特定できます。これらの拡張機能には、ieee_handler による処理と同様に、ハンドラを設定して同時にトラップを有効にできる関数が含まれていますが、これらの関数は ieee_handler よりも柔軟です。これらの拡張機能は、選択されたファイルに対する、浮動小数点例外に関する遡及診断メッセージのロギングもサポートしています。

4.4.3.1 fex_set_handling(3m)

fex_set_handling 関数を使用すると、浮動小数点例外のそれぞれのタイプを処理するためのいつくかのオプション (またはモード) の 1 つを選択できます。fex_set_handling を呼び出すための構文は次のとおりです。

ret = fex_set_handling(ex, mode, handler);

引数 ex には、呼び出しを適用する一連の例外を指定します。これは、表 34 の最初の列に示されている値のビット単位の「or」である必要があります。(これらの値は、fenv.h に定義されています)。

表 34  fex_set_handling の例外コード
例外
FEX_INEXACT
不正確な結果
FEX_UNDERFLOW
アンダーフロー
FEX_OVERFLOW
オーバーフロー
FEX_DIVBYZERO
0 による除算
FEX_INV_ZDZ
0/0 の無効な演算
FEX_INV_IDI
無限大/無限大の無効な演算
FEX_INV_ISI
無限大-無限大の無効な演算
FEX_INV_ZMI
0*無限大の無効な演算
FEX_INV_SQRT
負数の平方根
FEX_INV_SNAN
シグナルを発生する NaN に対する演算
FEX_INV_INT
無効な整数変換
FEX_INV_CMP
無効な非順序付け比較

便宜上、fenv.h には、FEX_NONE (例外なし)、FEX_INVALID (すべての無効な演算例外)、FEX_COMMON (オーバーフロー、0 による除算、およびすべての無効な演算)、および FEX_ALL (すべての例外) の各値も定義されています。

引数 mode には、示された例外に設定する例外処理モードを指定します。指定可能なモードは 5 つあります。

  • FEX_NONSTOP モードでは、IEEE 754 のデフォルトの停止しない動作になります。これは、例外のトラップを無効にしておくことと同等です。ieee_handler と異なり、fex_set_handling では、無効な演算例外の特定のタイプに対してデフォルト以外の処理を設定し、残りのタイプは IEEE のデフォルト処理のままにできます。

  • FEX_NOHANDLER モードは、ハンドラを設定せずに例外のトラップを有効にすることと同じです。例外が発生すると、システムは、あらかじめインストールされている SIGFPE ハンドラが存在する場合はそのハンドラに制御を渡し、存在しない場合は処理を中止します。

  • FEX_ABORT モードでは、例外が発生するとプログラムは abort(3c) を呼び出します。

  • FEX_SIGNAL は、示された例外に対して、引数 handler によって指定された処理関数をインストールします。これらの例外のいずれかが発生すると、ieee_handler によってインストールされているかのように、同じ引数を使用してハンドラが呼び出されます。

  • FEX_CUSTOM は、示された例外に対し、handler によって指定された処理関数をインストールします。FEX_SIGNAL モードと異なり、例外が発生すると、簡略化された引数リストを使用してハンドラが呼び出されます。この引数は、整数 (値は表 34 に示されている値の 1 つ) と、例外の原因となった演算に関する追加情報を記録する構造体を指すポインタから構成されます。この構造体の内容は、次のセクションおよび fex_set_handling(3m) のマニュアルページで説明されています。

指定された modeFEX_NONSTOPFEX_NOHANDLER、または FEX_ABORT である場合、handler パラメータは無視されます。fex_set_handling は、示された例外に対して指定されたモードが設定される場合はゼロ以外の値を返し、それ以外の場合はゼロを返します。次の例では、戻り値は無視されます。

次の例は、fex_set_handling を使用して特定のタイプの例外を見つける方法を示しています。0/0 例外で停止するようにするには、次を使用します。

fex_set_handling(FEX_INV_ZDZ, FEX_ABORT, NULL);

オーバーフローおよび 0 による除算に対する SIGFPE ハンドラをインストールするには、次を使用します。

fex_set_handling(FEX_OVERFLOW | FEX_DIVBYZERO, FEX_SIGNAL,
    handler);

前の例では、前のサブセクションで示したように、ハンドラ関数は SIGFPE ハンドラに対する sip パラメータを介して渡される診断情報を出力できました。一方、次の例では、FEX_CUSTOM モードでインストールされたハンドラに渡される例外に関する情報を出力します。詳細は、fex_set_handling(3m) のマニュアルページを参照してください。

使用例 2  FEX_CUSTOM モードでインストールされたハンドラに渡される情報の出力
#include <fenv.h>

void handler(int ex, fex_info_t *info)
{
    switch (ex) {
    case FEX_OVERFLOW:
        printf("Overflow in ");
        break;
    case FEX_DIVBYZERO:
        printf("Division by zero in ");
        break;

    default:
        printf("Invalid operation in ");
    }
    switch (info->op) {
    case fex_add:
        printf("floating point add\n");
        break;
    case fex_sub:
        printf("floating point subtract\n");
        break;
    case fex_mul:
        printf("floating point multiply\n");
        break;
    case fex_div:
        printf("floating point divide\n");
        break;
    case fex_sqrt:
        printf("floating point square root\n");
        break;
    case fex_cnvt:
        printf("floating point conversion\n");
        break;
    case fex_cmp:
        printf("floating point compare\n");
        break;
    default:
        printf("unknown operation\n");
    }
    switch (info->op1.type) {
    case fex_int:
        printf("operand 1: %d\n", info->op1.val.i);
        break;
    case fex_llong:
        printf("operand 1: %lld\n", info->op1.val.l);
        break;
    case fex_float:
        printf("operand 1: %g\n", info->op1.val.f);
        break;
    case fex_double:
        printf("operand 1: %g\n", info->op1.val.d);
        break;

    case fex_ldouble:
        printf("operand 1: %Lg\n", info->op1.val.q);
        break;
    }
    switch (info->op2.type) {
    case fex_int:
        printf("operand 2: %d\n", info->op2.val.i);
        break;
    case fex_llong:
        printf("operand 2: %lld\n", info->op2.val.l);
        break;
    case fex_float:
        printf("operand 2: %g\n", info->op2.val.f);
        break;
    case fex_double:
        printf("operand 2: %g\n", info->op2.val.d);
        break;
    case fex_ldouble:
        printf("operand 2: %Lg\n", info->op2.val.q);
        break;
    }
}
...
fex_set_handling(FEX_COMMON, FEX_CUSTOM, handler);

上記の例のハンドラは、発生した例外のタイプ、原因となった演算の種類、およびオペランドを報告します。このハンドラは、例外が発生した場所を示しません。例外が発生した場所を特定するには、遡及診断を使用できます。

4.4.3.2 遡及診断

libm の例外処理拡張機能を使用して例外を特定するもう 1 つの方法は、浮動小数点例外に関する遡及診断メッセージのロギングを有効にすることです。遡及診断のロギングを有効にすると、システムは特定の例外に関する情報を記録します。この情報には、例外のタイプ、その原因となった命令のアドレス、例外の処理方法、およびデバッガによって生成されるものに類似したスタックトレースが含まれます。遡及診断メッセージとともに記録されるスタックトレースには、命令のアドレスと関数名のみが含まれています。行番号、ソースファイル名、引数の値などのほかのデバッグ情報を調べるには、デバッガを使用する必要があります。

遡及診断のログには、発生した例外ごとの情報は含まれていません。例外ごとの情報を記録すると、一般にログが非常に大きくなり、特異な例外を特定することが不可能になります。代わりに、ロギングメカニズムは冗長なメッセージを取り除きます。次の 2 つの状況のいずれかである場合、メッセージは冗長と見なされます。

  • 同じ例外が同じ位置 (つまり、同じ命令アドレスとスタックトレース) で前に記録されている。

  • 例外に対して FEX_NONSTOP モードが有効になっていて、そのフラグが前に発生している。

具体的には、ほとんどのプログラムでは、それぞれのタイプの例外が最初に発生したときにのみログに記録されます。ある例外に対して FEX_NONSTOP 処理モードが有効な場合、任意の C99 浮動小数点環境関数を使用してそのフラグをクリアすると、その例外が次に発生したときにログに記録されます (前にログ記録された位置で発生していない場合)。

ロギングを有効にするには、fex_set_log 関数を使用してメッセージを転送するファイルを指定します。たとえば、メッセージを標準のエラーファイルに記録するには、次のように記述します。

fex_set_log(stderr);

次のコード例では、前のセクションで示した共有オブジェクトの事前ロード機能と遡及診断のロギングを組み合わせています。次の C ソースファイルを作成して、それを共有オブジェクトにコンパイルし、LD_PRELOAD 環境変数にそのパス名を指定することによってその共有オブジェクトを事前ロードして、FTRAP 環境変数に 1 つ以上の例外の名前をコンマで区切って指定すると、指定した例外の発生時にプログラムを中止すると同時に、各例外が発生した位置を示す遡及診断出力を取得できます。

使用例 3  遡及診断のロギングと共有オブジェクトの事前ロードの組み合わせ
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fenv.h>

static struct ftrap_string {
    const char  *name;
    int         value;
} ftrap_table[] = {
    { "inexact", FEX_INEXACT },
    { "division", FEX_DIVBYZERO },

    { "underflow", FEX_UNDERFLOW },
    { "overflow", FEX_OVERFLOW },
    { "invalid", FEX_INVALID },
    { NULL, 0 }
};

#pragma init (set_ftrap)
void set_ftrap()
{
    struct ftrap_string  *f;
    char                 *s, *s0;
    int                  ex = 0;

    if ((s = getenv("FTRAP")) == NULL)
        return;

    if ((s0 = strtok(s, ",")) == NULL)
        return;

    do {
        for (f = ftrap_table[0]; f->name != NULL; f++) {
            if (!strcmp(s0, f->name))
                ex |= f->value;
        }
    } while ((s0 = strtok(NULL, ",")) != NULL);

    fex_set_handling(ex, FEX_ABORT, NULL);
    fex_set_log(stderr);
}

このセクションの始めに示したプログラム例とともに上記のコードを使用すると、SPARC ベースのシステムでは、次のような結果が出力されます。

env FTRAP=invalid LD_PRELOAD=./init.so a.out
Floating point invalid operation (sqrt) at 0x7fa98398 __sqrt, abort
  0x7fa9839c  __sqrt
  0x00010880  sqrtm1
  0x000108ec  main
Abort

上記の出力は、ルーチン sqrtm1 内の平方根演算の結果として無効な演算例外が発生したことを示しています。

前述したように、x86 プラットフォームで共有オブジェクトの初期化ルーチンからトラップを有効にするには、標準の __fpstart ルーチンを無効にする必要があります。

は、一般的なログ出力を示すほかの例を示しています。一般的な情報については、fex_set_log(3m) のマニュアルページを参照してください。