diff --git a/CMakeLists.txt b/CMakeLists.txt index ec400fb0..f3bdf050 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,7 @@ if (NOT WIN32) memory/system.hpp debug_posix.cpp debug/crash_posix.cpp + debug/fpe_posix.cpp debug/system_posix.cpp io_posix.cpp io_posix.hpp @@ -147,6 +148,7 @@ if (WIN32) APPEND UTIL_FILES debug_win32.cpp debug/crash_win32.cpp + debug/fpe_win32.cpp debug/system_win32.cpp exe_win32.cpp io_win32.cpp @@ -308,6 +310,8 @@ list ( debug/crash.hpp debug/debugger.cpp debug/debugger.hpp + debug/fpe.hpp + debug/fpe.cpp debug/memory.cpp debug/memory.hpp debug/panic.cpp @@ -692,6 +696,7 @@ if (TESTS) concepts comparator coord + debug/fpe encode/number encode/base endian diff --git a/debug/fpe.cpp b/debug/fpe.cpp new file mode 100644 index 00000000..e69de29b diff --git a/debug/fpe.hpp b/debug/fpe.hpp new file mode 100644 index 00000000..43fb177e --- /dev/null +++ b/debug/fpe.hpp @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 2021, Danny Robson + */ + + +/////////////////////////////////////////////////////////////////////////////// +namespace cruft::debug::fpe { + void enable (void); + void disable (void); + + bool value (void); + bool value (bool); + + + /// Returns the FPE state to the value it was at construction time when the object falls out of scope. + /// + /// Copy and move constructors are deliberately deleted to avoid accidentally leaking state. + class scoped_reset { + public: + /// An explict enablement state to enact at construction. + scoped_reset (bool enabled); + /// Caches the current state without changing anything. + scoped_reset (); + + scoped_reset (scoped_reset const&) = delete; + scoped_reset& operator= (scoped_reset const&) = delete; + + scoped_reset (scoped_reset &&) noexcept = delete; + scoped_reset& operator= (scoped_reset &&) noexcept = delete; + + ~scoped_reset (); + + private: + bool m_prev; + }; +} \ No newline at end of file diff --git a/debug/fpe_posix.cpp b/debug/fpe_posix.cpp new file mode 100644 index 00000000..7adf5a3d --- /dev/null +++ b/debug/fpe_posix.cpp @@ -0,0 +1,68 @@ +#include "./fpe.hpp" + +#include + +#include + +#include + +using cruft::debug::fpe::scoped_reset; + + +/////////////////////////////////////////////////////////////////////////////// +static constexpr int EXCEPTION_MASK = FE_INVALID | FE_DIVBYZERO; + + +//----------------------------------------------------------------------------- +void +cruft::debug::fpe::enable (void) +{ + feenableexcept (EXCEPTION_MASK); +} + + +//----------------------------------------------------------------------------- +void +cruft::debug::fpe::disable (void) +{ + fedisableexcept (EXCEPTION_MASK); +} + + +//----------------------------------------------------------------------------- +bool cruft::debug::fpe::value (void) +{ + return feenableexcept (0) & EXCEPTION_MASK; +} + + +//----------------------------------------------------------------------------- +bool cruft::debug::fpe::value (bool val) +{ + if (val) + enable (); + else + disable (); + return val; +} + + +/////////////////////////////////////////////////////////////////////////////// +scoped_reset::scoped_reset () + : m_prev (value ()) +{ } + + +//----------------------------------------------------------------------------- +scoped_reset::scoped_reset (bool _value) + : scoped_reset () +{ + value (_value); +} + + +//----------------------------------------------------------------------------- +scoped_reset::~scoped_reset () +{ + value (m_prev); +} \ No newline at end of file diff --git a/debug/fpe_win32.cpp b/debug/fpe_win32.cpp new file mode 100644 index 00000000..e69de29b diff --git a/test/debug/fpe.cpp b/test/debug/fpe.cpp new file mode 100644 index 00000000..9ec5ccd7 --- /dev/null +++ b/test/debug/fpe.cpp @@ -0,0 +1,121 @@ +#include +#include +#include + +#include + +#include +#include + + +/////////////////////////////////////////////////////////////////////////////// +static int volatile fpe_value = 0; + +static sigjmp_buf fpe_jump; + + +//----------------------------------------------------------------------------- +static void handle_fpe (int, siginfo_t*, void *) +{ + fpe_value = 1; + siglongjmp (fpe_jump, 1); +} + + +/////////////////////////////////////////////////////////////////////////////// +float volatile nan_val = NAN; +float volatile inf_val = INFINITY; +float volatile zero_val = 0.f; + + +//----------------------------------------------------------------------------- +int main () +{ + struct sigaction fpe_sigaction {}; + fpe_sigaction.sa_flags = SA_RESTART; + fpe_sigaction.sa_sigaction = handle_fpe; + sigaction (SIGFPE, &fpe_sigaction, nullptr); + + cruft::TAP::logger tap; + + // Test that we don't receive any exceptions by default. + fpe_value = 0; + cruft::debug::escape (inf_val - inf_val); + tap.expect_eq (fpe_value, 0, "FE_INVALID is disabled by default"); + + fpe_value = 0; + cruft::debug::escape (1.f / zero_val); + tap.expect_eq (fpe_value, 0, "FE_DIVBYZERO is disabled by default"); + + // After the signal handler exits we'll immediate re-enter the handler as + // the faulting floating ops are re-executed. + // + // Ideally we'd modify the FPU state, but we can't access that from the + // handler. + // + // So we longjmp out of the handler and use a conditional to avoid + // re-executing. + // + // However this approach doesn't restore the FPU state (I think; it doesn't + // restore _something_) and so the exceptions are permanently disabled. + // + // But threads get their own state. So.. we spin up a thread for each + // test. It's nasty, but: + // * it works + // * I'm not familiar enough with the area to do it a cleaner way + // * I've spent too much time on just writing this test already. + std::thread ([] () { + cruft::debug::fpe::enable (); + fpe_value = 0; + if (!sigsetjmp (fpe_jump, 1)) + cruft::debug::escape (inf_val - inf_val); + }).join (); + tap.expect_eq (fpe_value, 1, "FE_INVALID is enabled after request"); + + std::thread ([] () { + cruft::debug::fpe::enable (); + fpe_value = 0; + if (!sigsetjmp (fpe_jump, 1)) + cruft::debug::escape (1.f / zero_val); + }).join (); + tap.expect_eq (fpe_value, 1, "FE_DIVBYZERO is enabled after request"); + + std::thread ([] () { + fpe_value = 0; + cruft::debug::fpe::scoped_reset resetter; + if (!sigsetjmp (fpe_jump, 1)) + cruft::debug::escape (inf_val - inf_val); + }).join (); + tap.expect_eq (fpe_value, 0, "scoped_reset enables exceptions"); + + std::thread ([] () { + fpe_value = 0; + + // Do a test beforehand just in case we've forgotten to reset the + // fpe_value variable properly. + if (!sigsetjmp (fpe_jump, 1)) + cruft::debug::escape (inf_val - inf_val); + + { + cruft::debug::fpe::scoped_reset resetter; + } + + if (!sigsetjmp (fpe_jump, 1)) + cruft::debug::escape (inf_val - inf_val); + }).join (); + tap.expect_eq (fpe_value, 0, "scoped_reset disables exceptions"); + + // Ensure that the scoped object actually disables state if it's + // previously been statically set. + std::thread ([] { + fpe_value = 0; + + cruft::debug::fpe::enable (); + cruft::debug::fpe::scoped_reset resetter (false); + + if (!sigsetjmp (fpe_jump, 1)) + cruft::debug::escape (inf_val - inf_val); + }).join (); + + return tap.status (); +} \ No newline at end of file