C语言 在Apple芯片上捕获浮点异常和信号处理

disbfnqx  于 2023-02-03  发布在  其他
关注(0)|答案(2)|浏览(167)

为了在MacOS上捕获浮点异常,我使用了一个提供feenableexcept功能的扩展。
http://www-personal.umich.edu/~williams/archive/computation/fe-handling-example.c

注意:如果您看到这篇文章是为了了解如何在MacOS(Intel或Apple芯片)上捕获浮点异常,您可能需要跳过汇编讨论,直接阅读下面的详细信息

现在我想更新这个苹果芯片的扩展,并可能删除一些过时的代码。通过fenv.h的挖掘,很清楚如何更新苹果芯片的例程feenableexceptfegetexceptfedisableexcept。但是,不太清楚如何处理2009年扩展中提供的汇编代码,或者为什么包括这些代码。
上面链接中提供的扩展相当长,所以我只提取涉及程序集的片段:

#if DEFINED_INTEL

// x87 fpu
#define getx87cr(x)    __asm ("fnstcw %0" : "=m" (x));
#define setx87cr(x)    __asm ("fldcw %0"  : "=m" (x));
#define getx87sr(x)    __asm ("fnstsw %0" : "=m" (x));

// SIMD, gcc with Intel Core 2 Duo uses SSE2(4)
#define getmxcsr(x)    __asm ("stmxcsr %0" : "=m" (x));
#define setmxcsr(x)    __asm ("ldmxcsr %0" : "=m" (x));

#endif  // DEFINED_INTEL

此代码用于sigaction机制的处理程序中,该机制用于报告捕获的浮点异常的类型。

fhdl ( int sig, siginfo_t *sip, ucontext_t *scp )
{
  int fe_code = sip->si_code;
  unsigned int excepts = fetestexcept (FE_ALL_EXCEPT);

  /* ... see complete code in link above ... */ 
     
    if ( sig == SIGFPE )
    {
#if DEFINED_INTEL
        unsigned short x87cr,x87sr;
        unsigned int mxcsr;

        getx87cr (x87cr);
        getx87sr (x87sr);
        getmxcsr (mxcsr);
        printf ("X87CR:   0x%04X\n", x87cr);
        printf ("X87SR:   0x%04X\n", x87sr);
        printf ("MXCSR:   0x%08X\n", mxcsr);
#endif

        // ....
    }
    printf ("signal:  SIGFPE with code %s\n", fe_code_name[fe_code]);
    printf ("invalid flag:    0x%04X\n", excepts & FE_INVALID);
    printf ("divByZero flag:  0x%04X\n", excepts & FE_DIVBYZERO);
  }
  else printf ("Signal is not SIGFPE, it's %i.\n", sig);

  abort();
}

提供了一个捕获异常并通过sigaction处理它们的示例。对feenableexcept的调用将是定义了feenableexcept的系统(例如,非Apple硬件)的本机实现,或者是上面链接的扩展中提供的实现。

int main (int argc, char **argv)
{
    double s;
    struct sigaction act;

    act.sa_sigaction = (void(*))fhdl;
    sigemptyset (&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    

//  printf ("Old divByZero exception: 0x%08X\n", feenableexcept (FE_DIVBYZERO));
    printf ("Old invalid exception:   0x%08X\n", feenableexcept (FE_INVALID));
    printf ("New fp exception:        0x%08X\n", fegetexcept ());

    // set handler
    if (sigaction(SIGFPE, &act, (struct sigaction *)0) != 0)
    {
        perror("Yikes");
        exit(-1);
    }

//  s = 1.0 / 0.0;  // FE_DIVBYZERO
    s = 0.0 / 0.0;  // FE_INVALID
    return 0;
}

当我在基于Intel的Mac上运行这个程序时,我得到:

Old invalid exception:   0x0000003F
New fp exception:        0x0000003E
X87CR:   0x037F
X87SR:   0x0000
MXCSR:   0x00001F80
signal:  SIGFPE with code FPE_FLTINV
invalid flag:    0x0000
divByZero flag:  0x0000
Abort trap: 6

我的问题是:

  • 为什么汇编代码和对fetestexcept的调用都包含在处理程序中?报告被捕获的异常类型是否都有必要?
  • 处理程序捕获了一个FE_INVALID异常。为什么,excepts & FE_INVALID是零?
  • sigaction处理程序在苹果芯片上完全被忽略了。它应该工作吗?或者我不明白使用sigaction的信号处理工作原理的一些更基本的东西,而不是引发FP异常时会发生什么?

我用gcc和clang编译。

    • 详情**:下面是一个从原始代码中提取的最小示例,它提炼了我上面的问题。在这个示例中,我提供了Intel或Apple芯片上MacOS缺少的feeableexcept功能。然后我测试了使用和不使用sigaction的情况。
#include <fenv.h>    
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

#if defined(__APPLE__)
#if defined(__arm) || defined(__arm64) || defined(__aarch64__)
#define DEFINED_ARM 1
#define FE_EXCEPT_SHIFT 8
#endif

void feenableexcept(unsigned int excepts)
{
    fenv_t env;
    fegetenv(&env);

#if (DEFINED_ARM==1)
    env.__fpcr = env.__fpcr | (excepts << FE_EXCEPT_SHIFT);
#else
    /* assume Intel */
    env.__control = env.__control & ~excepts;
    env.__mxcsr = env.__mxcsr & ~(excepts << 7);
#endif
    fesetenv(&env);
}
#else
/* Linux may or may not have feenableexcept. */
#endif

static void
fhdl ( int sig, siginfo_t *sip, ucontext_t *scp )
{
    int fe_code = sip->si_code;
    unsigned int excepts = fetestexcept (FE_ALL_EXCEPT);

    if (fe_code == FPE_FLTDIV)
        printf("In signal handler : Division by zero.  Flag is : 0x%04X\n", excepts & FE_DIVBYZERO);

    abort();
}

void main()
{
#ifdef HANDLE_SIGNAL
    struct sigaction act;
    act.sa_sigaction = (void(*))fhdl;
    sigemptyset (&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    sigaction(SIGFPE, &act, NULL);
#endif    
    
    feenableexcept(FE_DIVBYZERO);

    double x  = 0; 
    double y = 1/x;
}
    • 无签名的结果**

在英特尔上:

% gcc -o stack_except stack_except.c
% stack_except
Floating point exception: 8

在苹果芯片上:

% gcc -o stack_except stack_except.c
% stack_except
Illegal instruction: 4

以上操作按预期工作,当遇到被零除时代码终止。

    • 带sigaction的结果**

英特尔结果:

% gcc -o stack_signal stack_signal.c -DHANDLE_SIGNAL
% stack_signal
In signal handler : Division by zero.  Flag is : 0x0000
Abort trap: 6

代码在英特尔上按预期工作。但是,

  • fetestexcept(从信号处理程序调用)返回的值为零。为什么会这样?异常在被处理程序处理之前被清除了吗?

Apple芯片的结果:

% gcc -o stack_signal stack_signal.c -DHANDLE_SIGNAL
% stack_signal
Illegal instruction: 4

信号处理程序被完全忽略了。这是为什么?我是不是错过了一些关于信号如何处理的基本知识?

    • 在原始代码中使用程序集(请参见帖子顶部的链接)**

我的最后一个问题是关于文章顶部的原始示例中汇编的使用。为什么汇编用于查询信号处理程序中的标志?使用fetestexcept还不够吗?或者检查siginfo.si_code?* 可能的答案:fetestexcept,当在处理程序内部使用时,不会检测异常(?)。(这就是为什么只从处理程序内部打印0x0000的原因吗?)*
这里有一个类似问题的相关帖子。如何在M1 Mac上捕获浮点异常?

7dl7o3gd

7dl7o3gd1#

事实证明,AArch 64上的MacOS将为未屏蔽的FP异常提供SIGILL,而不是SIGFPE。How to trap floating-point exceptions on M1 Macs?显示了一个示例,包括如何取消屏蔽特定的FP异常,并且是AArch 64上的实际目标的复制品。我不知道为什么MacOS会忽略POSIX标准,并为算术异常提供不同的信号)。
答案的其余部分只涉及x86 asm部分。
我想您还需要了解POSIX信号(如SIGSEGVSIGFPE)、硬件异常(如页面错误或x86 #DE整数除法异常)与“fp异常”(在FPU状态寄存器中设置标志的事件,或者如果未屏蔽则被视为CPU异常,则捕获以运行内核代码)之间的区别。
如果FP异常没有被屏蔽,则意味着FP数学指令可以trap(将执行发送到内核,而不是继续执行下一条用户空间指令),操作系统的trap处理程序决定发送一个POSIX信号(或者修复页面错误本身的问题,然后返回用户空间重新运行出错的指令,也就是被捕获的指令)。
如果FP异常被屏蔽了,它们就不会导致CPU异常(陷阱),所以你只能用fetestexcept从同一个线程检查它们。
除非AArch 64也像x86那样有两个独立的FP异常掩码/状态(x87和SSE),否则我看不出有什么理由需要内联asm。fenv.h函数应该可以工作。
遗憾的是,ISO C没有提供一种真正 * 取消屏蔽 * 异常的方法,只有fetestexcept(FE_DIVBYZERO)等来检查FP-exception状态下的状态标志(如果任何操作引发了它们,则自上次清除以来,这些标志保持设置)。https://en.cppreference.com/w/c/numeric/fenv/fetestexcept
但是MacOS fenv.h确实有一些常量可以在FP环境中使用fegetenv/fesetenv设置FP异常掩码位,这是GNU C feenableexcept的替代方法。
x86上的Asm /intrinsic非常有用,因为它有两个独立的FP系统,即传统的x87和现代的SSE/AVX。
fetestexcept将测试x87或SSE异常,具体取决于编译器默认用于FP数学的异常。(SSE 2用于x86-64,除了使用x87的long double...)因此有理由同时检查这两种异常,以确保它与fetestexcept匹配。
此外,x87状态字具有精度控制位(使其始终舍入到与双精度或浮点数相同的尾数精度,而不是舍入到完整的80位),并且MXCSR具有DAZ / FTZ(非规格化为零/刷新为零),以禁用逐渐下溢,因为如果发生这种情况,速度会很慢。fenv不提供这种功能。

x86内联asm非常幼稚,而且很糟糕

如果您确实需要这些x87操作的 Package 器,请到其他地方仔细寻找。
#define setx87cr(x) __asm ("fldcw %0" : "=m" (x));是超级坏的。它告诉编译器x是一个纯输出(由asm模板编写),但实际上运行了一个从它读取的asm指令。我预计它会在除调试构建之外的任何情况下坏(因为死存储消除)。ldmxcsr Package 器也是如此,它甚至更没用,因为#include <immintrin.h>_mm_setcsr
它们都必须是asm volatile,否则它们就被认为是输入的纯函数,所以如果没有输入只有一个输出,编译器就可以假设它总是写相同的输出并进行相应的优化,所以如果你想多次读取status来检查一系列计算之后的新异常,编译器可能只会重用第一个结果。
(With只有输入操作数而不是输出操作数,fldcw的正确 Package 器将隐含地是volatile的)。
另一个复杂的问题是编译器可能会选择早于或晚于预期执行FP操作,一种解决方法是使用FP值作为输入,如asm volatile("fnstsw %0" : "=am"(sw) : "g"(fpval) )。(我还使用"a"作为可能的输出之一,因为有一种形式的指令会写入AX而不是内存(当然,您需要它是uint16_tshort)。
或者使用"+g"(fpval)读+写“输出”操作数来告诉编译器它读/写fpval,所以这必须在使用它的某些计算之前发生。
在这个答案中,我不打算尝试完全正确的版本,但这正是我们要寻找的。
我最初猜测s = 0.0 / 0.0;可能不会编译为带有clang的AArch 64除法指令。如果不使用类似下面的语句,您可能只会得到一个编译时常量NaN,并优化掉一个未使用的结果

volatile double s = 0.0;
    s = 0.0 / s;             // s is now unknown to the compiler

您可以检查编译器的asm输出,以确保存在实际的FP除法指令。
顺便说一句,ARM和AArch 64不会在整数除以0时陷入陷阱(不像x86),但如果FP异常没有屏蔽,希望FP操作可以。但如果这仍然不起作用,那么是时候阅读asm手册并查看编译器asm输出了。

xqkwcwgp

xqkwcwgp2#

GCC在gfortran/config中有fpu-aarch64.h头文件,它实现了在Apple M上处理FP异常所需的一切。

相关问题