Floating Point Exceptions
What happens when we do something bad? Consider this example:
#include <iostream>
#include <cmath>
double trouble(double x) {
return std::sqrt(x);
}
int main() {
double x{-1};
double y = trouble(x);
for (int i = 0; i < 10; ++i) {
y += std::pow(x, i);
}
std::cout << y << std::endl;
}
Here, we pass -1
to trouble()
which then takes the square root
of it—this results in a NaN. But if we run the code, it goes
merrily about its way, using that result in the later computations.
Unix uses signals to indicate that a problem has happened during the code execution. If a program created a signal handler then that signal can be trapped and any desired action can be taken.
Note
This example was only tested on a Linux machine with GCC. Other OSes or compilers might have slightly different headers or functionality.
There are a few parts to trapping a floating point exception (FPE). First we need to enable exception trapping via:
feenableexcept(FE_INVALID|FE_DIVBYZERO|FE_OVERFLOW);
That catches 3 different types of floating point exceptions—invalid, divide-by-zero, and overflows.
Note
See cppreference floating point environment for a full list of the macros of exceptions that can be trapped.
Next we need to add a handler to deal with the exception:
signal(SIGFPE, fpe_handler);
Here, SIGFPE
is the standard name for a floating point exception,
and fpe_handler
is the name of a function that will be called when
we detect a SIGFPE
.
Finally, we need to write the handler, and we’d like to get a backtrace. There are two ways that we can approach this, which we see next.
Linux backtrace method
We can use the Linux backtrace()
function to access the stack of
our program execution. This is really a C-function, so we need to use
C-style arrays here.
Here’s the new version of our code:
#include <iostream>
#include <cmath>
#include <csignal>
#include <cfenv>
#include <execinfo.h>
void fpe_handler(int s) {
std::cout << "floating point exception, signal " << s << std::endl;
const int nbuf = 64;
void *bt_buffer[nbuf];
int nentries = backtrace(bt_buffer, nbuf);
char **strings = backtrace_symbols(bt_buffer, nentries);
for (int i = 0; i < nentries; ++i) {
std::cout << i << ": " << strings[i] << std::endl;
}
abort();
}
double trouble(double x) {
return std::sqrt(x);
}
int main() {
feenableexcept(FE_INVALID|FE_DIVBYZERO|FE_OVERFLOW);
signal(SIGFPE, fpe_handler);
double x{-1};
double y = trouble(x);
for (int i = 0; i < 10; ++i) {
y += std::pow(x, i);
}
std::cout << y << std::endl;
}
When we compile the code, we want to add the -g
option to store the
symbols in the code—this allows us to understand where problems arise:
g++ -g -o undefined_trap undefined_trap.cpp
Now when we run this, the program aborts and we see:
floating point exception, signal 8
0: ./undefined_trap() [0x401261]
1: /lib64/libc.so.6(+0x42750) [0x7f3dc35dc750]
2: /lib64/libm.so.6(+0x1435c) [0x7f3dc37d335c]
3: ./undefined_trap() [0x4012ff]
4: ./undefined_trap() [0x401347]
5: /lib64/libc.so.6(+0x2d560) [0x7f3dc35c7560]
6: /lib64/libc.so.6(__libc_start_main+0x7c) [0x7f3dc35c760c]
7: ./undefined_trap() [0x401145]
Aborted (core dumped)
This is the call stack for our program. In the brackets are the address in the program where the execution was when the FPE occurred.
Note
The addresses you see might be different, and they will depend on the compiler and OS.
These lines are ordered such that the calling function is below the function where the execution is. So it usually is best to look at the addresses near the top.
We can turn those into line numbers using addr2line
:
addr2line -e undefined_trap 0x4012ff
gives:
/home/zingale/classes/phy504/examples/floating_point/undefined_trap.cpp:23
and that line is precisely where the sqrt()
is!
We can get slightly nicer output (including the function name) by doing:
addr2line -C -f -p -e undefined_trap 0x4012ff
which gives:
trouble(double) at /home/zingale/classes/phy504/examples/floating_point/undefined_trap.cpp:23
Note
On the MathLab machines, the stack trace seems to include an offset, like:
floating point exception, signal 8
0: ./undefined_trap(+0xc03) [0x561d8799dc03]
1: /lib/x86_64-linux-gnu/libc.so.6(+0x3ef10) [0x7f5461e3df10]
2: /lib/x86_64-linux-gnu/libm.so.6(+0x11397) [0x7f5462201397]
3: ./undefined_trap(+0xccf) [0x561d8799dccf]
4: ./undefined_trap(+0xd21) [0x561d8799dd21]
5: /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f5461e20c87]
6: ./undefined_trap(+0xaaa) [0x561d8799daaa]
Aborted (core dumped)
and we need to use that offset instead with addr2line
, like:
addr2line -a -f -e ./undefined_trap +0xcd5
C++23 stacktrace library
A more modern (and simpler) way to get the stacktrace is available in C++23 via the stacktrace library.
#include <iostream>
#include <cmath>
#include <csignal>
#include <cfenv>
#include <stacktrace>
void fpe_handler(int s) {
std::cout << "floating point exception, signal " << s << std::endl;
std::cout << std::stacktrace::current() << std::endl;
abort();
}
double trouble(double x) {
return std::sqrt(x);
}
int main() {
feenableexcept(FE_INVALID|FE_DIVBYZERO|FE_OVERFLOW);
signal(SIGFPE, fpe_handler);
double x{-1};
double y = trouble(x);
for (int i = 0; i < 10; ++i) {
y += std::pow(x, i);
}
std::cout << y << std::endl;
}
To build this, we need to load an extra library. For GCC 12 or 13, we can do:
g++ -g -std=c++23 -o undefined_stactrace undefined_stacktrace.cpp -lstdc++_libbacktrace
For GCC 14, we would do:
g++ -g -std=c++23 -o undefined_stactrace undefined_stacktrace.cpp -lstdc++exp