Oracle® Solaris Studio 12.4:数值计算指南

退出打印视图

更新时间: 2015 年 1 月
 
 

4.4.1 使用调试器查找异常

本节举例说明如何使用 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,在无需重新编译该程序的情况下,通过用可启用运算无效时自陷的启动例程进行链接或者手动启用该陷阱来确定。

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.2 链接程序和库指南

原则上,您可以通过按上述方式预装共享对象来更改任何浮点控制模式的初始化方法。但是,在运行时链接程序将控制传递给属于主可执行文件的启动代码之前,无论共享对象文件中的初始化例程是预装的还是显式链接的,它们都由运行时链接程序来执行。启动代码随后建立任何通过 –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;
}