Oracle® Developer Studio 12.5:数值计算指南

退出打印视图

更新时间: 2016 年 6 月
 
 

4.4 查找异常

找出异常发生的位置的一种方法是在整个程序中各个点处测试异常标志。但是要用这种方法精确地隔离异常需要很多测试并且会产生巨大成本。

用来更方便的确定异常的发生位置的方法就是启用它的陷阱。当发生启用了陷阱的异常时,操作系统将通过发送 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,发出 catch fpe 命令以便在 SIGFPE 发出时停止,然后运行该程序。在基于 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,则可以通过直接修改浮点状态寄存器来手动启用陷阱。这会有些麻烦,因为只有当在程序中首次使用浮点(此时浮点状态寄存器会在所有的陷阱处于禁用状态时初始化)之后,操作系统才启用浮点单元。因此,只有在该程序至少执行了一个浮点指令之后,才能手动启用自陷。在我们的示例中,在调用 sqrtm1 函数之前已经访问了浮点单元,因此我们可以在该函数的入口处设置断点,对无效运算异常启用自陷,命令 dbx 停止接收 SIGFPE 信号,然后继续执行。在基于 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 的情况下,通过建立用来启用捕获的初始化例程来启用陷阱。这可能非常有用,例如,如果您希望在异常发生时中止该程序,而不运行调试器。可通过两种方法来建立这样的例程。

如果存在包含该程序的对象文件和库,则可以通过用适当的初始化例程重新链接该程序来启用捕获。首先,创建一个类似如下的 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 的系统上,情况会略显复杂。通常,在建立由 –fround–ftrap–fprecision 标志选择的任何非缺省模式并将控制权交给主程序前,由编译器自动提供的启动代码会调用 __fpstart 例程(位于标准 C 库 libc 中)将所有的浮点模式重置为缺省设置。因此,在基于 x86 的系统上,为了通过用初始化例程预装共享对象文件来启用捕获或者更改任何其他缺省浮点模式,必须覆盖 __fpstart 例程,以便它不重置缺省浮点模式。但是,__fpstart 替换例程仍应当像标准例程那样执行初始化函数的其余部分。下面的代码显示一种执行该操作的方法。此段代码假设主机平台运行于 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 可以同时启用对五个 IEEE 浮点异常中的任何异常的捕获,要求在发生指定的异常时中止该程序或者建立 SIGFPE 处理程序。还可以使用较低级别的函数 sigfpe(3)、signal(3c) 或 sigaction(2) 之一来安装 SIGFPE 处理程序;但是,这些函数不像 ieee_handler 那样启用捕获功能。切记,只有启用了浮点异常的陷阱时,浮点异常才触发 SIGFPE 信号。

4.4.2.1 ieee_handler (3m)

ieee_handler 的调用语法是:

i = ieee_handler(action, exception, handler)

前两个输入参数 actionexception 是字符串。第三个输入参数 handler 的类型为 sigfpe_handler_type,该类型定义在 floatingpoint.h 中。

三个输入参数可使用以下值:

输入参数
C 或 C++ 类型
可能的值
action
char *
get, set, clear
exception
char *
invaliddivisionoverflow,
underflowinexact
allcommon
handler
sigfpe_handler_type
用户定义的例程
SIGFPE_DEFAULT
SIGFPE_IGNORE
SIGFPE_ABORT

当请求的操作为 "set" 时,ieee_handler 建立由 handler 指定的名为 exception 的异常的处理函数。处理函数可能为 SIGFPE_DEFAULTSIGFPE_IGNORE,两者都选择缺省 IEEE 行为 SIGFPE_ABORT 或者用户提供的子例程的地址,当发生任一指定异常时,选择此缺省行为会导致程序中止,选择此地址会导致该子例程被调用(通过 sigaction(2) 手册页中关于装有 SA_SIGINFO 标志设置的信号处理程序中所介绍的参数来完成)。如果处理程序为 SIGFPE_DEFAULTSIGFPE_IGNORE,发生指定异常时,ieee_handler 也会禁用捕获;对于任何其他处理程序,ieee_handler 会启用捕获。

在 x86 平台上,每当异常的陷阱处于启用状态并且引发相应标志时,浮点硬件都会启用陷阱。因此,要避免伪陷阱,程序应在调用 ieee_handler 来启用捕获之前清除每个指定的 exception 的标志。

当请求的 action"clear" 时,ieee_handler 将撤销处理函数当前正针对指定 exception 安装的异常并禁用其陷阱。这与针对 SIGFPE_DEFAULT 执行 "set" 操作相同。当 action"clear" 时,会忽略第三个参数。

对于 "set""clear" 操作,如果请求的操作可用,则 ieee_handler 返回 0,否则返回非零值。

当请求的 action"get" 时,ieee_handler 返回当前为指定 exception 安装的处理程序的地址,如果未安装任何处理程序,则返回 SIGFPE_DEFAULT

下面的示例显示几个代码段,通过它们可阐释如何使用 ieee_handler。下面的 C 代码导致该程序在遇到除以零时中止:

#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_codesip->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 信号类型。整数除以零 (FPE_INTDIV) 和整数溢出 (FPE_INTOVF) 也包括在 SIGFPE 类型中,但是由于它们不是 IEEE 浮点异常,所以您不能通过 ieee_handler 来为它们安装处理程序。可通过 sigfpe(3) 来为这些 SIGFPE 类型安装处理程序;但是,请注意,在所有的 SPARC 和 x86 平台上,会在缺省情况下忽略整数溢出。特殊的指令可导致传递 FPE_INTOVF 类型的 SIGFPE 信号,但是 Sun 编译器不生成这些指令。

对于与 IEEE 浮点异常对应的 SIGFPE 信号,sip->si_code 成员用于指明出现了哪个异常。在基于 x86 的系统上,它实际上用于指明引发了其标志的拥有最高优先级的解除屏蔽异常。这通常与最后出现的异常是一样的。在基于 SPARC 的系统上,sip->si_addr 成员存放导致了异常的指令的地址,而在基于 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 指令的长度恰好为五个字节时。完全通用的 SSE2 解决方案涉及对优化代码进行解码,以查找下一指令的开头。改用 fex_set_handling

在大多数情况下,在捕获处于启用状态时,导致异常的指令不传递 IEEE 缺省结果:在上面的输出结果中,针对 max_normal * max_normal 报告的值不是溢出运算(即,带有正确符号的无穷大)的缺省结果。通常,SIGFPE 处理程序必须为导致捕获异常的运算提供一个结果,以便继续用有意义的值进行计算。要查看执行上述操作的一种方法,请参见处理异常

4.4.3 使用 libm 异常处理扩展来查找异常

使用 libm 中 C99 浮点环境函数的异常处理扩展,C/C++ 程序可通过多种方法来查找异常。这些扩展包括可建立处理程序并同时启用陷阱(正如 ieee_handler 所执行的操作那样)的函数,但是它们提供更大的灵活性。它们还支持将有关浮点异常的回顾诊断消息记录到选定文件中。

4.4.3.1 fex_set_handling(3m)

fex_set_handling 函数允许您选择某个选项或模式来处理每种类型的浮点异常。fex_set_handling 的调用语法是:

ret = fex_set_handling(ex, mode, handler);

ex 参数指定要应用调用的异常集合。它必须是表 34的第一列中列出的值的按位“或”。(这些值在 fenv.h 中定义。)

表 34  fex_set_handling 的异常代码
异常
FEX_INEXACT
不精确结果
FEX_UNDERFLOW
下溢
FEX_OVERFLOW
溢出
FEX_DIVBYZERO
除以零
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(溢出、除以零和所有的无效运算)和 FEX_ALL(所有异常)。

mode 参数指定要为指出的异常建立的异常处理模式。有五种可能的模式:

  • 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中的某个值)和一个指针(它所指向的结构用来记录有关导致该异常的运算的附加信息)组成。该结构的内容在下一节和 fex_set_handling(3m) 手册页中介绍。

请注意,如果指定的 modeFEX_NONSTOPFEX_NOHANDLERFEX_ABORThandler 参数会被忽略。如果指定的模式是为指定的异常建立的,fex_set_handling 会返回非零值,否则将返回零。在下面的示例中,返回值将被忽略。

下面的示例假设使用 fex_set_handling 方法来查找某些类型的异常。要中止 0/0 异常,请使用以下命令:

fex_set_handling(FEX_INV_ZDZ, FEX_ABORT, NULL);

要针对溢出和除以零安装 SIGFPE 处理程序,请使用以下命令:

fex_set_handling(FEX_OVERFLOW | FEX_DIVBYZERO, FEX_SIGNAL,
    handler);

在上面的示例中,处理程序函数可打印通过 sip 参数提供给 SIGFPE 处理程序的诊断信息,如上面的子段所示。与之相反,下面的示例打印有关该异常的、提供给安装在 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 异常处理扩展查找异常的另一种方法是启用对有关浮点异常的回顾诊断消息的记录。当您启用对回顾诊断消息的记录时,系统会记录有关某些异常的信息。这些信息包括异常的类型、导致该异常的指令的地址、将要处理该异常的方式以及类似于调试器所生成的跟踪的栈跟踪。用回顾诊断消息记录的栈跟踪只包含指令地址和函数名;对于其他调试信息(如行号、源文件名和参数值),必须使用调试器。

并非每个所发生的异常都包含在回顾诊断日志中;如果出现了这种情况,则典型日志将非常大,而且将不可能隔离与众不同的异常。相反,日志记录机制会消除冗余的消息。在以下任一情况下,消息会被认为冗余:

  • 以前在同一位置(即,具有相同的指令地址和栈跟踪)记录了同一个异常,或者

  • FEX_NONSTOP 模式对于该异常有效,而且它的标志以前被引发过。

尤其是,在大多数程序中,对于每种类型的异常将只记录第一次出现的异常。当 FEX_NONSTOP 处理模式对于某个异常有效时,如果通过 C99 浮点环境的任一函数清除该异常的标志,则允许在该异常下次发生时记录它,但前提是它不在以前记录它的位置发生。

要启用日志记录,请使用 fex_set_log 函数指定应将消息传递到的文件。例如,要将消息记录到标准错误文件,请使用:

fex_set_log(stderr);

以下代码示例将以下两个功能组合在一起:对回顾诊断消息的记录与上一节中阐释的共享对象预装功能。通过创建下面的 C 源文件、将它编译为共享对象、通过在 LD_PRELOAD 环境变量中提供共享对象的路径名来预装共享对象、在 FTRAP 环境变量中指定一个或多个异常的名称(用逗号分开),可同时针对指定的异常中止该程序,并获取可显示每个异常发生位置的诊断输出结果。

示例 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) 手册页。