How to trap floating-point exceptions on M1 Macs? - arm

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.

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.

You can decode the nature of the FPU exception in the SIGILL handler by casting the third argument of your sigaction handler to a struct ucontext*, and then decoding scp->uc_mcontext->__es.esr in your handler. This is the value of the ESR_ELx, Exception Syndrome Register (ELx) register. If its top 6 bits (EC) are 0b101100, then the signal was triggered by a trapping AArch64 FPU operation. If in that case bit 23 of that register (TFV) is also 1, then the register's lower 7 bits will match what the lower 7 bits of the fpsr register would have been in case trapping would have been disabled (i.e., the kind of FPU exception triggered by the instruction).
See section D17.2.37 of the ARMv8 architecture manual for more information ("ESR_EL1, Exception Syndrome Register (EL1)").

Related

glibc: unable to get unwind information for certain sections

I am currently facing issue with the glibc v2.22 where I am not able to get the proper unwind information.
When there is SIGABRT application, it is calling abort function from glibc. It should be using unwind information which is enabled in the build. However, it is scanning the stack (as indicated by the red line below the address in the screenshot) and providing the misleading information as shown in the screenshot attached (using sentry for analyzing the dump).
Here, do_crash is called which does assert(0) which then aborts the main application. While analyzing the dump, the do_crash function calls the _fini which is never in the main application's stack.
I have enabled unwind for the glibc by using CFLAGS += "-funwind-tables". I also tried with the flags such as -rdynamic and -fno-omit-frame-pointer but it was also of no use.
Am I missing something here? How can I get the complete backtrace of the signals, particularly SIGABRT?
Thanks in advance
When there is SIGABRT application, it is calling abort function from glibc
That is not true, this is not happening, unless you explicitly registered it.
I have enabled unwind for the glibc by using CFLAGS += "-funwind-tables"
It tells the compiler to add the information, it doesn't "enable unwind". What exactly happens when compiling with -funwind-tables?
Here, do_crash is called which does assert(0) which then aborts the main application.
This is not related to receiving SIGABRT signal.
Am I missing something here?
I believe you are making wrong assumptions - that something is called on SIGABRT, that SIGABRT is sent on assert, that abort() is called on SIGABRT. Nothing is called on SIGABRT and the program is terminated when receiving SIGABRT by default (see man 7 signal), assert just terminates the program and doesn't raise SIGABRT, and abort() raises the SIGABRT signal, not receives it.
How can I get the complete backtrace of the signals, particularly SIGABRT?
Register a handler that will do that. See How to automatically generate a stacktrace when my program crashes .
#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int sig) {
void *array[10];
size_t size;
size = backtrace(array, 10);
backtrace_symbols_fd(array, size, STDERR_FILENO);
_Exit(1);
}
int main(int argc, char **argv) {
signal(SIGABRT, handler); // install our handler
raise(SIGABRT);
}
If you want to print stacktrace on assert() that's completely different and you would overwrite the glibc handler for assert to do that:
#include <assert.h>
#include <execinfo.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
void print_trace(void) {
void *array[10];
size_t size;
size = backtrace(array, 10);
backtrace_symbols_fd(array, size, STDERR_FILENO);
}
// based on https://code.woboq.org/userspace/glibc/assert/assert.c.html
void __assert_fail(const char *assertion, const char *file, unsigned int line, const char *function) {
extern const char *__progname;
fprintf(stderr, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
print_trace();
abort();
}
int main() {
assert(0);
}

Why was default SIGPIPE handler changed?

I'm working on a test and i'm asked how to make a "read" sleep or a "write" stop a process"
For the latter I don't understand why my sigpipe is, indeed raised, but isn't stopping the process:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#define READING 0
#define WRITING 1
#define DESCRIPTOR_COUNT 2
void signal_handler(int signal){
printf("sigpipe received\n");
}
int main(void)
{
int tube[DESCRIPTOR_COUNT];
pipe(tube);
// signal(SIGPIPE, signal_handler);
close(tube[READING]);
if(write(tube[WRITING], "message", 8)<0)
if(errno==EPIPE)
printf("EPIPE returned\n");
printf("123");
return EXIT_SUCCESS;
}
Without signal() (quoted)
With signal() (unquoted)
SIGPIPE is indeed received, but in the case I don't handle it the process should stop, but since I can write "123" this means the process wasn't stopped.
Why?
Also I'm on Fedora 28, i'm using codeblocks 17.12.
Was SIGPIPE ignored...? Reason?
SOLUTION ?
struct sigaction action;
action.sa_handler = SIG_DFL;
sigaction(SIGPIPE, &action, 0);
Replacing signal() with this will have the default behaviour as expected!
EDIT I've now changed the title from "SIGPIPE doesn't stop process" to "Why was default SIGPIPE handler changed?"
======================================================
Answer
After talking with the guys from codeblocks, codeblocks uses wxWidgets, and on linux (fedora 28 here) wxWidgets uses gtk library, as explained by Mark Plotnick in the comments, gtk changes the signal handler for SIGPIPE, since codeblocks runs codes using a fork or an exec, the code run through codeblocks is influenced by gtk library.
The behaviour you are reporting is consistent with the Code::Blocks IDE setting, implicitly or explicitly, the SIGPIPE behaviour to SIG_IGN. This is readily inherited. It isn't what I'd expect — I'd expect your program to be launched with SIGPIPE (and, indeed, all other signals) set to SIG_DFL, the default signal behaviour. If this turns out to be the problem, you have the basis for a bug report to the developers of Code::Blocks. If it turns out not to be the problem, then we've got some hard thinking ahead to work out what actually is happening*.
You could demonstrate whether this is what's happening with Code::Blocks by paying attention to the return value from signal(), or by using sigaction() to interrogate the signal handling mode without modifying it.
For example:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#define READING 0
#define WRITING 1
#define DESCRIPTOR_COUNT 2
static void signal_handler(int signum)
{
// Lazy; normally, I'd format the signal number into the string, carefully
(void)signum;
write(STDOUT_FILENO, "sigpipe received\n", sizeof("sigpipe received\n")-1);
}
int main(void)
{
void (*handler)(int) = signal(SIGPIPE, signal_handler);
if (handler == SIG_DFL)
printf("old handler was SIG_DFL\n");
else if (handler == SIG_IGN)
{
printf("old handler was SIG_IGN\n");
(void)signal(SIGPIPE, SIG_IGN);
}
else
{
// Standard C does not allow a cast from function pointer to object pointer
//printf("there was a non-standard handler installed (%p)\n", (void *)handler);
printf("there was a non-standard handler installed\n");
}
int tube[DESCRIPTOR_COUNT];
pipe(tube);
close(tube[READING]);
if (write(tube[WRITING], "message", 8) < 0)
{
if (errno == EPIPE)
printf("EPIPE returned\n");
else
printf("errno = %d\n", errno);
}
printf("123\n");
return EXIT_SUCCESS;
}
Note that the standard idiom for setting a signal handler using signal() in a program was:
if (signal(signum, SIG_IGN) != SIG_IGN)
signal(signum, signal_handler);
This means that if a program was protected from signals, it stays protected. If it was handling signals (by default, or perhaps explicitly by a prior call to signal()), then you install your own signal handler. Equivalent code using sigaction() would be:
struct sigaction sa;
if (sigaction(signum, 0, &sa) == 0 && sa.sa_handler != SIG_IGN)
{
sa.sa_handler = signal_handler;
sa.sa_flag &= ~SA_SIGINFO;
sigaction(signum, sa, 0);
}
(One advantage of this: the structure is initialized by the call to sigaction(), so there's no need to fiddle with the masks. The tweak to the flags ensures that the basic handler, not the extended handler, is used.)
When I compile the source code (pipe13.c) into program pipe13 and run it, I get:
$ make pipe13
gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes pipe13.c -o pipe13
$ pipe13
old handler was SIG_DFL
sigpipe received
EPIPE returned
123
$ (trap '' 13; pipe13)
old handler was SIG_IGN
EPIPE returned
123
$
This variant uses sigaction() to interrogate the signal handling:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#define READING 0
#define WRITING 1
#define DESCRIPTOR_COUNT 2
int main(void)
{
struct sigaction sa;
if (sigaction(SIGPIPE, 0, &sa) != 0)
fprintf(stderr, "siagaction() failed\n");
else if (sa.sa_handler == SIG_DFL)
printf("old handler was SIG_DFL\n");
else if (sa.sa_handler == SIG_IGN)
printf("old handler was SIG_IGN\n");
else
printf("there was a non-standard handler installed\n");
int tube[DESCRIPTOR_COUNT];
pipe(tube);
close(tube[READING]);
if (write(tube[WRITING], "message", 8) < 0)
{
if (errno == EPIPE)
printf("EPIPE returned\n");
else
printf("errno = %d\n", errno);
}
printf("123\n");
return EXIT_SUCCESS;
}
When run (program pipe83), I get:
$ pipe83
old handler was SIG_DFL
$ echo $?
141
$ (trap '' 13; pipe83)
old handler was SIG_IGN
EPIPE returned
123
$
Note that with the default signal handling, the program terminates before printing 123. POSIX shells encode 'child died from signal N' by reporting the exit status as 128 + N; SIGPIPE is 13, so 141 indicates that the shell died from a SIGPIPE signal. (Yes, modern youngsters would probably write (trap '' PIPE; pipe83) and it works — such niceties weren't available when I learned shell programming.)
It would not be all that hard to generalize the code to test whether Code::Blocks sets any other signals to other than the default handling. It can be a little fiddly, though, if you want to adapt to which signals are available on a machine.
* In chat, we established that the program is run in a VMware image running Fedora 28, hosted on a Windows 10 machine. Because of this, there are enough possible places for there to be trouble that it is not clear that the problem is necessarily in Code::Blocks — it is simply not clear where the problem originates. However, the problem does indeed seem to be that the test program is started with SIGPIPE handling set to SIG_IGN instead of SIG_DFL when it is run from Code::Blocks.
Code::Blocks is not a compiler, just an IDE. It (probably) runs the GCC compiler. And the GCC compiler don't matter much for signal handling. Read signal(7) and signal-safety(7) for more (calling printf inside a signal handler is forbidden because printf is not async-signal-safe so your printf("sigpipe received\n"); inside a signal handler is undefined behavior). Signal handling is done mostly by the Linux kernel (see its source code on kernel.org) with a small bit of it handled by your C standard library, probably GNU glibc.
It seems codeblocks changes the default signal handler for SIGPIPE
This is very unlikely (and almost certainly false). You could use strace(1) on your program to understand what system calls it is doing.
printf("123");
You forgot a \n. Remember that stdout is usually line-buffered. Or you should call fflush(3).
When you run a program from inside Code::Blocks it could run without a terminal. I strongly recommend running your program inside a terminal emulator (see tty(4) and pty(7)). The C standard library is permitted to use something like isatty(3) to behave differently when stdout is or is not a tty (in particular, buffering is done differently, see setvbuf(3)). Read the tty demystified and termios(3).
You could also try to run your program by redirecting its stdin, stdout, stderr to a file or a pipe (perhaps using |& cat or |& less with bash or zsh), or anything which is not a tty.
BTW, Code::Blocks is free software. You can study its source code to understand what it is doing.
Consider also using stdbuf(1) to run your program with different buffering operations.

Trapping floating-point overflow in C

I am trying to trap floating-point overflow in C. Here is what I have tried
#define _GNU_SOURCE
#include <fenv.h>
#include <signal.h>
#include <stdio.h>
void catch_overflow (int sig) {
printf ("caught division by zero\n");
signal (sig, catch_overflow);
}
int main(void) {
feenableexcept(FE_DIVBYZERO);
signal (FE_DIVBYZERO, catch_overflow);
float a = 1., b = 0.; float c = a/b; return 0; }
I expected to see "caught division by zero" message, but I only get a core dump message, "Floating point exception (core dumped)". How could I modify the program to get the "caught division by zero" message?
Thanks.
A floating point exception is translated in machine- and OS-dependent way to some signaling approach. But no such way allows mixing of different constant spaces, as FE_xxx and SIGxxx. Using FE_DIVBYZERO for signal(), you really caught SIGILL which is not generated for floating-point errors (because you did not specify OS, I was free to choose any - Linux in my case).
For your program, I have made this exception catching working under Linux with two changes:
Setting signal number to handle to SIGFPE.
Declaring a, b, c as volatile (prevents compiler from calculating c as INF at compile time).
After this, the program fell into eternal cycle printing "caught division by zero" because signal handler for such error restores execution to the same command. You shall carefully consider how to fix such errors without making an erroneous instruction eternal. For example, you can use longjmp to go out of signal handler to an installed exception handler.
And, you still should consult your target OS manuals for how to catch FP errors.
(This has been already described in comments in main parts - but I considered to form an explicit answer.)

Returning From Catching A Floating Point Exception

So, I am trying to return from a floating point exception, but my code keeps looping instead. I can actually exit the process, but what I want to do is return and redo the calculation that causes the floating point error.
The reason the FPE occurs is because I have a random number generator that generates coefficients for a polynomial. Using some LAPACK functions, I solve for the roots and do some other things. Somewhere in this math intensive chain, a floating point exception occurs. When this happens, what I want to do is increment the random number generator state, and try again until the coefficients are such that the error doesn't materialize, as it usually doesn't, but very rarely does and causes catastrophic results.
So I wrote a simple test program to learn how to work with signals. It is below:
In exceptions.h
#ifndef EXCEPTIONS_H
#define EXCEPTIONS_H
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <math.h>
#include <errno.h>
#include <float.h>
#include <fenv.h>
void overflow_handler(int);
#endif // EXCEPTIONS_H //
In exceptions.c
#include "exceptions.h"
void overflow_handler(int signal_number)
{
if (feclearexcept(FE_OVERFLOW | FE_UNDERFLOW | FE_DIVBYZERO | FE_INVALID)){
fprintf(stdout, "Nothing Cleared!\n");
}
else{
fprintf(stdout, "All Cleared!\n");
}
return;
}
In main.c
#include "exceptions.h"
int main(void)
{
int failure;
float oops;
//===Enable Exceptions===//
failure = 1;
failure = feenableexcept(FE_OVERFLOW | FE_UNDERFLOW | FE_DIVBYZERO | FE_INVALID);
if (failure){
fprintf(stdout, "FE ENABLE EXCEPTIONS FAILED!\n");
}
//===Create Error Handler===//
signal(SIGFPE, overflow_handler);
//===Raise Exception===//
oops = exp(-708.5);
fprintf(stdout, "Oops: %f\n", oops);
return 0;
}
The Makefile
#===General Variables===#
CC=gcc
CFLAGS=-Wall -Wextra -g3 -Ofast
#===The Rules===#
all: makeAll
makeAll: makeExceptions makeMain
$(CC) $(CFLAGS) exceptions.o main.o -o exceptions -ldl -lm
makeMain: main.c
$(CC) $(CFLAGS) -c main.c -o main.o
makeExceptions: exceptions.c exceptions.h
$(CC) $(CFLAGS) -c exceptions.c -o exceptions.o
.PHONY: clean
clean:
rm -f *~ *.o
Why doesn't this program terminate when I am clearing the exceptions, supposedly successfully? What do I have to do in order to return to the main, and exit?
If I can do this, I can put code in between returning and exiting, and do something after the FPE has been caught. I think that I will set some sort of flag, and then clear all most recent info in the data structures, redo the calculation etc based on whether or not that flag is set. The point is, the real program must not abort nor loop forever, but instead, must handle the exception and keep going.
Help?
"division by zero", overflow/underflow, etc. result in undefined behaviour in the first place. If the system, however, generates a signal for this, the effect of UB is "suspended". The signal handler takes over instead. But if the handler returns, the effect of UB will "resume".
Therefore, the standard disallows returning from such a situation.
Just think: How would the program have to recover from e.g. DIV0? The abstract machine has no idea about FPU registers or status flags, and even if - what result would have to be generated?
C also has no provisions to unroll the stack properly like C++.
Note also, that generating signals for arithmetic exceptions is optional, so there is no guarantee a signal will actually be generated. The handler is mostly meant to notify about the event and possibly clean up external resources.
Behaviour is different for signals which do not origin from undefined behaviour, but just interrupt program execution. This is well defined as the program state is well-defined.
Edit:
If you have to rely on the program to continue under all circumstances, you hae to check all arguments of arithmetic operations before doing the actual operation and/or use safe operations only (re-order, use larger intermediate types, etc.). One exaple for integers might be to use unsigned instead of signed integers, as for those overflow-behavior is well-defined (wrap), so intermediate results overflowing will not make trouble as long as that is corrected afterwards and the wrap is not too much. (Disclaimer: that does not always work, of course).
Update:
While I am still not completely sure, according to comments, the standard might allow, for a hosted environment at least, to use LIA-1 traps and to recover from them (see Annex H. As these are not necessarily precise, I suspect recovery is not possible under all circumstances. Also, math.h might present additional aspects which have to be carefully evaluated.
Finally: I still think there is nothing gained with such approach, but some uncertainty added compared to using safe algorithms. It would be different, if there wer not so much different components involved. For a bare-metal embedded system, the view might be completely different.
I think you're supposed to mess around with the calling stack frame if you want to skip an instruction or break out of exp or whatever. This is high voodoo and bound to be unportable.
The GNU C library lets you use setjmp() outside of a signal handler to which you can longjmp() from inside. This seems like a better way to go. Here is a self-contained modification of your program showing how to do it:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <math.h>
#include <errno.h>
#include <float.h>
#include <fenv.h>
sigjmp_buf oh_snap;
void overflow_handler(int signal_number) {
if (feclearexcept(FE_OVERFLOW | FE_UNDERFLOW | FE_DIVBYZERO | FE_INVALID)){
fprintf(stdout, "Nothing Cleared!\n");
}
else{
fprintf(stdout, "All Cleared!\n");
}
siglongjmp(oh_snap, 1);
return;
}
int main(void) {
int failure;
float oops;
failure = 1;
failure = feenableexcept(FE_OVERFLOW | FE_UNDERFLOW | FE_DIVBYZERO | FE_INVALID);
if (failure){
fprintf(stdout, "FE ENABLE EXCEPTIONS FAILED!\n");
}
signal(SIGFPE, overflow_handler);
if (sigsetjmp(oh_snap, 1)) {
printf("Oh snap!\n");
} else {
oops = exp(-708.5);
fprintf(stdout, "Oops: %f\n", oops);
}
return 0;
}

How to raise a float point exception

guys. I am doing some work around float point operations. The 0.1 is inexact represented by binary float point format. Thus I wrote down this
float i = 0.1f;
and expecting the inexact exception to arise. I turnned on the -fp-trap-all=all option, set fp-mode to be strict and installed SIGFPE signal handler in my code. But nothing happened. Then I tried
float i = 0.1f,j = 0.2f, c;
c = i + j;
still can not catch any exceptions! It drive my crazy.
Sorry to mention that I am using intel c++ compiler on Linux at last.
You have to test for exceptions yourself. The following code works for me:
#include <stdio.h>
#include <fenv.h>
#ifndef FE_INEXACT
# error No FP Exception handling!
#endif
int main()
{
double a = 4.0;
a /= 3.0;
if (fetestexcept(FE_INEXACT) & FE_INEXACT)
{
printf("Exception occurred\n");
}
else
{
printf("No exception.\n");
}
}
If you replace 4.0 by 3.0, you will not get the exception.
You can do something similar with double a = 0.0; a = sin(a);.
Trapping exceptions are only supported conditionally. To check, use the macros described in the documentation:
#define _GNU_SOURCE
#include <fenv.h>
#ifndef FE_NOMASK_ENV
# warning Cannot raise FP exceptions!
#else
# warning Trapping FP exceptions enabled.
feenableexcept(FE_INEXACT);
#endif
According to this answer, inexact exception is only raised if the rounded version of the float isn't the same as the mathematically exact amount. In your case, the rounded response is the same, so no exception raised.

Resources