How to trap floating-point exceptions on M1 Macs?

Viewed 525

The way to trap floating-point exceptions is architecture-dependent. This is code I have tested successfully on an Intel (x86) Mac: it takes the square root of a negative number twice, once before, and once after, enabling floating-point exception trapping. The second time, fpe_signal_handler() is called.

#include <cmath>        // for sqrt()                                           
#include <csignal>      // for signal()                                         
#include <iostream>
#include <xmmintrin.h>  // for _mm_setcsr                                       

void fpe_signal_handler(int /*signal*/) {
  std::cerr << "Floating point exception!\n";
  exit(1);
}

void enable_floating_point_exceptions() {
 _mm_setcsr(_MM_MASK_MASK & ~_MM_MASK_INVALID);
 signal(SIGFPE, fpe_signal_handler);
}

int main() {
  const double x{-1.0};
  std::cout << sqrt(x) << "\n";
  enable_floating_point_exceptions();
  std::cout << sqrt(x) << "\n";
}

Compiling with the apple-clang compiler provided by Xcode

clang++ -g -std=c++17 -o fpe fpe.cpp

and running gives the following expected output:

nan
Floating point exception!

I would like to write an analogous program that does the same thing as the above program on an M1 (arm64) Mac. I tried the following:

#include <cfenv>        // for std::fenv_t                                      
#include <cmath>        // for sqrt()                                           
#include <csignal>      // for signal()                                         
#include <fenv.h>       // for fegetenv(), fesetenv()                           
#include <iostream>

void fpe_signal_handler(int /*signal*/) {
  std::cerr << "Floating point exception!\n";
  exit(1);
}

void enable_floating_point_exceptions() {
 std::fenv_t env;
 fegetenv(&env);
 env.__fpcr = env.__fpcr | __fpcr_trap_invalid;
 fesetenv(&env);
 signal(SIGFPE, fpe_signal_handler);
}

int main() {
  const double x{-1.0};
  std::cout << sqrt(x) << "\n";
  enable_floating_point_exceptions();
  std::cout << sqrt(x) << "\n";
}

It almost works: After compiling with the apple-clang compiler provided by Xcode

clang++ -g -std=c++17 -o fpe fpe.cpp

I get the following output:

nan
zsh: illegal hardware instruction  ./fpe

I tried adding the -fexceptions flag, but that didn't make a difference. I noticed that the ARM Compiler toolchain "does not support floating-point exception trapping for AArch64 targets," but I'm not sure if this applies to M1 Macs with Apple's toolchain.

Am I correct that the M1 Mac hardware just doesn't support floating-point exception trapping? Or is there a way to modify this program so it traps the second floating-point exception and then calls fpe_signal_handler()?


Synchronously testing for exceptions within the same thread does work fine, using ISO C fetestexcept from <fenv.h> as in the cppreference example. The problem here is getting FP exceptions to actually trap so the OS delivers SIGFPE, instead of just setting sticky flags in the FP environment.

1 Answers

Based on the long discussion below (many thanks to the patience of @Peter Cordes), it seems that with MacOS on Aarch64, unmasking FP exceptions and then generating the corresponding bad FP math results in a SIGILL rather than a SIGFPE. A signal code ILL_ILLTRP can be detected in the handler.

#include <fenv.h>    
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

static void
fpe_signal_handler( int sig, siginfo_t *sip, void *scp )
{
    int fe_code = sip->si_code;

    printf("In signal handler : ");

    if (fe_code == ILL_ILLTRP)
        printf("Illegal trap detected\n");
    else
        printf("Code detected : %d\n",fe_code);

    abort();
}

void enable_floating_point_exceptions()
{
    fenv_t env;
    fegetenv(&env);

    env.__fpcr = env.__fpcr | __fpcr_trap_invalid;
    fesetenv(&env);

    struct sigaction act;
    act.sa_sigaction = fpe_signal_handler;
    sigemptyset (&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    sigaction(SIGILL, &act, NULL);
}

void main()
{
    const double x = -1;    
    printf("y = %f\n",sqrt(x));
    enable_floating_point_exceptions();
    printf("y = %f\n",sqrt(x));
}

This results in :

y = nan
In signal handler : Illegal trap detected
Abort trap: 6

Other floating point exceptions can be detected in a similar manner, e.g unmasking __fpcr_trap_divbyzero, and then generating double x=0; double y=1/x. If the exception is not unmasked then the program terminates normally.

Without a SIGFPE, however, it doesn't seem possible to detect exactly which floating point operation triggered the signal handler. One can imagine unmasking all exceptions (e.g. using FE_ALL_EXCEPT). Then, when a bad math op generates a SIGILL, we don't have enough information in the handler to know what operation sent the signal. Unmasking all exceptions and playing around a bit with fetestexcept in the handler didn't produce very reliable results. This might take some more investigation.

Related