To trap floating point exceptions on MacOS, I use an extension that provides feenableexcept
functionality. The original extension (written in 2009) is here
http://www-personal.umich.edu/~williams/archive/computation/fe-handling-example.c
NOTE: If you came across this post to see how you can trap floating point exceptions on MacOS (either with Intel or Apple silicon), you might want to skip over the assembly discussion to the DETAILS below.
I'd now like to update this extension for Apple silicon and possibly remove some outdated code. Digging through fenv.h
, it is clear how to update the routines feenableexcept
, fegetexcept
and fedisableexcept
for Apple silicon. However, it is less clear what to do with the assembly code provided in the 2009 extension, or why this code is even included.
The extension provided in the link above is quite long, so I'll just extract the fragments involving the assembly :
#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
This code is used in a handler for a sigaction
mechanism that is provided to report on the type of floating point exception trapped.
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();
}
An example is provided that traps exceptions and handles them through sigaction
. The call to feenableexcept
will either be a native implementation for systems that have feenableexcept
defined (e.g. non Apple hardware) or the implementation provided in the extension linked above.
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;
}
When I run this on an Intel-based Mac, I get;
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
My questions are:
Why is the assembly code and a call to
fetestexcept
both included in the handler? Are both necessary to report on the type of exception that was trapped?An
FE_INVALID
exception was trapped by the handler. Why, then isexcepts & FE_INVALID
zero?The
sigaction
handler is completely ignored on Apple silicon. Should it work? Or am I not understanding something more fundamental about the signal handing works usingsigaction
, vs. what happens when a FP exception is raised?
I am compiling with gcc and clang.
DETAILS : Here is a minimal example extracted from the original code that distills my questions above. In this example, I provide the missing feeableexcept
functionality for MacOS on Intel or Apple silicon. Then I test with and without 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;
}
Results without sigaction
On Intel:
% gcc -o stack_except stack_except.c
% stack_except
Floating point exception: 8
And on Apple silicon :
% gcc -o stack_except stack_except.c
% stack_except
Illegal instruction: 4
The above works as expected and code terminates when the division by zero is encountered.
Results with sigaction
Results on Intel:
% gcc -o stack_signal stack_signal.c -DHANDLE_SIGNAL
% stack_signal
In signal handler : Division by zero. Flag is : 0x0000
Abort trap: 6
The code works as expected on Intel. However,
- The return from
fetestexcept
(called from the signal handler) is zero. Why is this? Was the exception cleared before being processed by the handler?
Results on Apple silicon :
% gcc -o stack_signal stack_signal.c -DHANDLE_SIGNAL
% stack_signal
Illegal instruction: 4
The signal handler is ignored completely. Why is this? Am I missing something fundamental about how signals are processed?
Use of assembly in original code (see link at top of post)
My final question was concerning the use of assembly in the original example posted at the top of the post. Why was assembly used to query the flags in the signal handler? Is it not enough to use fetestexcept
? Or to check siginfo.si_code
? Possible answer: fetestexcept
, when used inside the handler doesn't detect the exception (?). (Is this why only 0x0000
is printed from inside the handler?.)
Here is related post with a similar questions. How to trap floating-point exceptions on M1 Macs?
Turns out MacOS on AArch64 will deliver SIGILL, not SIGFPE, for unmasked FP exceptions. How to trap floating-point exceptions on M1 Macs? shows an example including how to unmask specific FP exceptions, and is a duplicate for the actual goal on AArch64. (Linux on AArch64 apparently delivers SIGFPE; I don't know why MacOS would ignore the POSIX standard and deliver a different signal for arithmetic exceptions).
The rest of this answer just covers the x86 asm parts.
I suspect you also need to learn the difference between a POSIX signal like
SIGSEGV
orSIGFPE
, a hardware exception like a page fault or x86#DE
integer divide exception, vs. an "fp exception" (event that either sets a flag in an FPU status register, or if unmasked is treated as a CPU exception, trapping to run kernel code.)Having FP exceptions unmasked means an FP math instruction can trap (send execution into the kernel, instead of continuing to the next user-space instruction). The OS's trap handler decides to deliver a POSIX signal (or fix the problem itself on pagefault, for example, and return to user-space to rerun the instruction that faulted aka trapped.)
If FP exceptions are masked, they don't result in CPU exceptions (traps), so you can only check them from the same thread with
fetestexcept
. The point offeenableexcept
is to unmask some exceptions.Unless AArch64 also has two separate FP exception-masks / statuses like x86 does (x87 and SSE), I don't see any reason you'd need inline asm.
fenv.h
functions should work.Unfortunately ISO C doesn't provide a way to actually unmask exceptions, just
fetestexcept(FE_DIVBYZERO)
etc. to check the status flags in the FP-exception state (which stay set if any operation ever raised them, since they were last cleared). https://en.cppreference.com/w/c/numeric/fenv/fetestexceptBut MacOS
fenv.h
does have some constants for setting the FP exception-mask bits in the FP environment withfegetenv
/fesetenv
. This is an alternative to GNU Cfeenableexcept
.Asm / intrinsics on x86 can be useful because it has two independent FP systems, legacy x87 and modern SSE/AVX.
fetestexcept
would test either x87 or SSE exceptions, depending on which the compiler used by default for FP math. (SSE2 for x86-64, except for long double using x87...) So there's reason to want to check both to make sure it matches up withfetestexcept
.Also, the x87 status word has precision-control bits (to make it always round to the same mantissa precision as double or float, instead of to full 80-bit), and MXCSR has DAZ / FTZ (denormals are zero / flush to zero) to disable gradual underflow because it's slow if it happens.
fenv
doesn't portably expose that.The x86 inline asm is very naive and broken
If you do actually want wrappers for these x87 operations, look elsewhere for ones written carefully.
#define setx87cr(x) __asm ("fldcw %0" : "=m" (x));
is super broken. It tells the compiler that x is a pure output (written by the asm template), but actually runs an asm instruction that reads from it. I expect that to break (because of dead store elimination) in anything except a debug build. Same for theldmxcsr
wrapper, which is even more useless because#include <immintrin.h>
has_mm_setcsr
They all need to be
asm volatile
, otherwise they're considered a pure function of the inputs, so with no inputs and one output, the compiler can assume that it always writes the same output and optimize accordingly. So if you wanted to read status multiple times to check for new exceptions after each of a series of calculations, the compiler would likely just reuse the first result.(With only an input instead of an output operand, a correct wrapper for
fldcw
would be volatile implicitly.)Another complication is that a compiler could choose to do an FP op earlier or later than you expected. One way you can fix that is by using the FP value as an input, like
asm volatile("fnstsw %0" : "=am"(sw) : "g"(fpval) )
. (I also used"a"
as one of the possible outputs, since there's a form of that instruction which writes to AX instead of memory. Of course you need it to be auint16_t
orshort
.)Or use a
"+g"(fpval)
read+write "output" operand to tell the compiler it reads/writes fpval, so this has to happen before some calculation that uses it.I'm not going to attempt fully correct versions myself in this answer, but that's what to look for.
I had originally guessed that
s = 0.0 / 0.0;
might not be compiling to a divide instruction with clang for AArch64. You might just get a compile-time-constant NaN, and optimize away an unused result, if you don't use something likeYou can check the compiler's asm output to make sure there is an actual FP divide instruction.
BTW, ARM and AArch64 don't trap on integer division by 0 (unlike x86), but with the FP exception unmasked hopefully FP ops do. But if this still doesn't work, then it's time to read the asm manuals and look at compiler asm output.