【问题标题】:Trapping floating point exceptions and signal handling on Apple silicon在 Apple 芯片上捕获浮点异常和信号处理
【发布时间】:2023-02-01 14:17:16
【问题描述】:

为了在 MacOS 上捕获浮点异常,我使用了一个提供 feenableexcept 功能的扩展。原始扩展(写于 2009 年)在这里

http://www-personal.umich.edu/~williams/archive/computation/fe-handling-example.c

笔记:如果您看到这篇文章以了解如何在 MacOS(使用 Intel 或 Apple 芯片)上捕获浮点异常,您可能想跳过汇编讨论到细节以下。

我现在想为 Apple silicon 更新此扩展,并可能删除一些过时的代码。翻遍fenv.h,很清楚如何更新Apple silicon的例程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 处理程序在 Apple 芯片上被完全忽略。它应该工作吗?还是我不了解有关使用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

该代码在 Intel 上按预期工作。然而,

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

苹果硅的结果:

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

信号处理程序被完全忽略。为什么是这样?我是否遗漏了有关信号处理方式的一些基本知识?

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

我的最后一个问题是关于在帖子顶部发布的原始示例中使用 assembly 。为什么程序集用于查询信号处理程序中的标志?用fetestexcept还不够吗?或者查看siginfo.si_code可能的答案:fetestexcept,在处理程序内部使用时未检测到异常(?)。 (这就是为什么只从处理程序内部打印 0x0000 的原因吗?。)

这是有类似问题的相关帖子。 How to trap floating-point exceptions on M1 Macs?

【问题讨论】:

  • #define setx87cr(x) __asm ("fldcw %0" : "=m" (x));超级坏。它告诉编译器 x 是一个纯输出(由 asm 模板编写),但实际上运行的是从中读取的 asm 指令。我希望它能在除调试版本之外的任何地方中断(因为死存储消除)。 ldmxcsr 包装器也一样,它更没用,因为 #include &lt;immintrin.h&gt;_mm_setcsr
  • 除非 AArch64 也像 x86(x87 和 SSE)一样有两个单独的 FP 异常掩码/状态,否则我看不出有任何理由需要自定义函数/宏而不是 ISO C fenv.h 函数。 fetestexcept(FE_DIVBYZERO) 等应该可以解决问题。 en.cppreference.com/w/c/numeric/fenv/fetestexcept
  • 是的 - fetestexcept 将测试是否发生了异常,但只有在发生异常之后。因此,必须为每一行可疑代码调用它。而 feenableexcept 是一个方便的函数,(出于某种原因,OSX 没有提供)它只使用 fegetenv 和 fesetenv 来设置环境以在发生异常时终止执行 - 对 gdb 非常有用。
  • 我的意思是在您的异常处理程序中使用 fetestexcept 而不是 getmxcsr。您不需要任何 mxcsr 或 x87 的 AArch64 端口。
  • fetestexcept 会测试任何一个x87 或 SSE 异常,具体取决于默认情况下用于 FP 数学的编译器。 (x86-64 的 SSE2,long double 使用 x87 除外...)因此有理由要检查两者以确保它与 fetestexcept 匹配。此外,x87 状态字具有精度控制位(使其始终舍入到与 doublefloat 相同的尾数精度,而不是完整的 80 位),并且 MXCSR 具有 DAZ / FTZ(非正规数为零) / flush to zero) 以禁用逐渐下溢,因为如果发生下溢,它会很慢。 fenv 不会轻易暴露这一点。

标签: c macos x86 arm64 floating-point-exceptions


【解决方案1】:

事实证明,AArch64 上的 MacOS 将为未屏蔽的 FP 异常提供 SIGILL,而不是 SIGFPE。 How to trap floating-point exceptions on M1 Macs? 展示了一个示例,包括如何取消屏蔽特定的 FP 异常,并且与 AArch64 上的实际目标重复。 (Linux on AArch64 apparently delivers SIGFPE;我不知道为什么 MacOS 会忽略 POSIX 标准并为算术异常提供不同的信号)。
该答案的其余部分仅涵盖 x86 asm 部分。


我怀疑您还需要了解 POSIX 信号(如 SIGSEGVSIGFPE)、硬件异常(如页面错误或 x86 #DE 整数除法异常)与“fp 异常”(设置的事件)之间的区别FPU 状态寄存器中的一个标志,或者如果未屏蔽被视为 CPU 异常,则陷入运行内核代码。)

取消屏蔽 FP 异常意味着 FP 数学指令可以陷阱(将执行发送到内核,而不是继续执行下一个用户空间指令)。操作系统的陷阱处理程序决定传递一个 POSIX 信号(或修复页面错误本身的问题,例如,并返回到用户空间以重新运行出错的指令,也称为陷阱。)

如果 FP 异常被屏蔽,它们不会导致 CPU 异常(陷阱),因此您只能从与 fetestexcept 相同的线程中检查它们。 feenableexcept 的目的是揭露一些异常。


除非 AArch64 也像 x86(x87 和 SSE)一样有两个独立的 FP 异常掩码/状态,否则我看不出有任何理由需要内联 asm。 fenv.h 函数应该可以工作。

不幸的是,ISO C 没有提供一种方法来实际揭开面具异常,只是 fetestexcept(FE_DIVBYZERO) 等检查 FP 异常状态中的状态标志(如果有任何操作引发它们,它们将保持设置状态,因为它们最后被清除)。 https://en.cppreference.com/w/c/numeric/fenv/fetestexcept

但是 MacOS fenv.h 确实有一些常量用于在 FP 环境中使用 fegetenv / fesetenv 设置 FP 异常掩码位。这是 GNU C feenableexcept 的替代品。


x86 上的 Asm / intrinsics 可能很有用,因为它有两个独立的 FP 系统,旧版 x87 和现代 SSE/AVX。

fetestexcept 将测试 x87 或 SSE 异常,具体取决于默认情况下用于 FP 数学的编译器。 (用于 x86-64 的 SSE2,除了使用 x87 的 long double ...)所以有理由要检查两者以确保它与 fetestexcept 匹配。

此外,x87 状态字具有精度控制位(使其始终舍入到与双精度或浮点数相同的尾数精度,而不是完整的 80 位),并且 MXCSR 具有 DAZ / FTZ(非正规数为零/刷新为零) 来禁用逐渐下溢,因为如果它发生的话它会很慢。 fenv 并没有公开这一点。


x86 内联 asm 非常幼稚和破烂

如果您确实需要这些 x87 操作的包装器,请在别处寻找仔细编写的包装器。

#define setx87cr(x) __asm ("fldcw %0" : "=m" (x));超级坏了。它告诉编译器 x 是纯输出(由 asm 模板编写),但实际上运行的是从中读取的 asm 指令。我希望它能在除调试版本之外的任何地方中断(因为死存储消除)。 ldmxcsr 包装器也一样,它更没用,因为 #include &lt;immintrin.h&gt;_mm_setcsr

它们都需要是asm volatile,否则它们将被视为输入的纯函数,因此在没有输入和一个输出的情况下,编译器可以假定它始终写入相同的输出并相应地进行优化。因此,如果您想在一系列计算中的每一次计算之后多次读取状态以检查新的异常,编译器可能只会重用第一个结果。

(只有一个输入而不是输出操作数,fldcw 的正确包装器将隐含地变易变。)

另一个复杂情况是编译器可以选择比您预期的更早或更晚执行 FP 操作。解决此问题的一种方法是使用 FP 值作为输入,例如 asm volatile("fnstsw %0" : "=am"(sw) : "g"(fpval) )。 (我还使用 "a" 作为可能的输出之一,因为该指令有一种形式写入 AX 而不是内存。当然你需要它是 uint16_tshort。)

或者使用 "+g"(fpval) 读+写“输出”操作数来告诉编译器它读/写 fpval,所以这必须在使用它的某些计算之前发生。

在这个答案中,我不会自己尝试完全正确的版本,但这就是要寻找的内容。



我最初猜测 s = 0.0 / 0.0; 可能不会为 AArch64 编译为带有 clang 的除法指令。如果您不使用类似

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

您可以检查编译器的 asm 输出以确保存在实际的 FP 除法指令。

顺便说一句,ARM 和 AArch64 不会陷入除以 0 的整数除法(与 x86 不同),但希望 FP ops 可以隐藏 FP 异常。但如果这仍然不起作用,那么是时候阅读 asm 手册并查看编译器 asm 输出了。

【讨论】:

  • @Donna:我的猜测是,在为 AArch64 编译时,首先没有触发 FP 异常,这就是没有传递信号的原因。你的minimal reproducible example没有在主线程中使用fetestexcept,只在信号处理程序中使用,所以直到你刚才的评论你已经确认你可以在同一个线程中检测到被零除才清楚,只是没有收到信号。但听起来你是说你确实测试并确认了?
  • @Donna:en.cppreference.com/w/cpp/numeric/fenv 指出feenableexcept 是 GNU 扩展。 (glibc 手册确认它是 GNU,甚至不是 POSIX 或其他东西)。它在 MacOS 上不可用吗?似乎 ISO C fenv.h 没有让 FP 数学传递信号的设施。
  • (不幸的是,同一个词“异常”被用于非常不同的事情,在 FP 状态寄存器中设置一个粘性标志位与捕获到操作系统以便它可以传递信号。)
  • @唐娜:你说你“可以使用 feenableexcept 在我的 M1 上捕获异常。“所以我猜你在 MacOS 上确实有这个功能。当他们陷入困境时会发生什么?你的进程死于 SIGFPE?
  • @Donna:另外,你说你用过double x=0; double y = 1/x;。这省略了 volatile,这是练习的重点(除非您在禁用优化的情况下进行编译,在这种情况下,所有变量都被视为跨语句的 volatile。然后只需将它分成两个单独的语句就可以了。)无论如何,你启用了所有例外吗?也许在 AArch64 上没有单独的除以零与无效的方法?
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-14
    • 1970-01-01
    • 2021-07-16
    • 1970-01-01
    • 2017-12-07
    • 2010-12-11
    • 1970-01-01
    相关资源
    最近更新 更多