From 046da6e80d61c9a77907f3d22700603c0ad4e03c Mon Sep 17 00:00:00 2001 From: Jeremy <51220084+jeremy-rifkin@users.noreply.github.com> Date: Fri, 31 May 2024 17:42:27 -0500 Subject: [PATCH] Add breakpoint if being debugged functionality --- include/libassert/assert.hpp | 21 ++++++ include/libassert/platform.hpp | 87 ++++++++++++++++++++++- src/common.hpp | 10 +++ src/platform.cpp | 124 +++++++++++++++++++++++++++++++-- src/utils.cpp | 6 +- src/utils.hpp | 2 + tests/CMakeLists.txt | 1 + tests/binaries/basic_demo.cpp | 5 ++ tests/unit/literals.cpp | 2 - 9 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 tests/binaries/basic_demo.cpp diff --git a/include/libassert/assert.hpp b/include/libassert/assert.hpp index 2a5d3040..0c7b062e 100644 --- a/include/libassert/assert.hpp +++ b/include/libassert/assert.hpp @@ -56,6 +56,13 @@ namespace libassert { LIBASSERT_ATTR_COLD LIBASSERT_EXPORT bool isatty(int fd); + LIBASSERT_ATTR_COLD LIBASSERT_EXPORT bool is_debugger_present() noexcept; + enum class debugger_check_mode { + check_once, + check_every_time, + }; + LIBASSERT_ATTR_COLD LIBASSERT_EXPORT void set_debugger_check_mode(debugger_check_mode mode) noexcept; + // returns the type name of T template [[nodiscard]] std::string_view type_name() noexcept { @@ -689,6 +696,17 @@ namespace libassert { #define LIBASSERT_IGNORE_UNUSED_VALUE #endif +#ifdef LIBASSERT_BREAK_ON_FAIL + #define LIBASSERT_BREAKPOINT_IF_DEBUGGING() \ + do \ + if(libassert::is_debugger_present()) { \ + LIBASSERT_BREAKPOINT(); \ + } \ + while(0) +#else + #define LIBASSERT_BREAKPOINT_IF_DEBUGGING() +#endif + #define LIBASSERT_INVOKE(expr, name, type, failaction, ...) \ /* must push/pop out here due to nasty clang bug https://github.com/llvm/llvm-project/issues/63897 */ \ /* must do awful stuff to workaround differences in where gcc and clang allow these directives to go */ \ @@ -704,6 +722,7 @@ namespace libassert { LIBASSERT_WARNING_PRAGMA_POP_GCC \ if(LIBASSERT_STRONG_EXPECT(!static_cast(libassert_decomposer.get_value()), 0)) { \ libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \ + LIBASSERT_BREAKPOINT_IF_DEBUGGING(); \ failaction \ LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, #expr, __VA_ARGS__) \ if constexpr(sizeof libassert_decomposer > 32) { \ @@ -727,6 +746,7 @@ namespace libassert { #define LIBASSERT_INVOKE_PANIC(name, type, ...) \ do { \ libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \ + LIBASSERT_BREAKPOINT_IF_DEBUGGING(); \ LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, "", __VA_ARGS__) \ libassert::detail::process_panic( \ libassert_params \ @@ -783,6 +803,7 @@ namespace libassert { /* https://godbolt.org/z/Kq8Wb6q5j https://godbolt.org/z/nMnqnsMYx */ \ if(LIBASSERT_STRONG_EXPECT(!LIBASSERT_STATIC_CAST_TO_BOOL(libassert_value), 0)) { \ libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \ + LIBASSERT_BREAKPOINT_IF_DEBUGGING(); \ failaction \ LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, #expr, __VA_ARGS__) \ if constexpr(sizeof libassert_decomposer > 32) { \ diff --git a/include/libassert/platform.hpp b/include/libassert/platform.hpp index b7db1c0d..d8982e7c 100644 --- a/include/libassert/platform.hpp +++ b/include/libassert/platform.hpp @@ -41,7 +41,6 @@ #define LIBASSERT_CLANG_VERSION 0 #endif - #if (defined(__GNUC__) || defined(__GNUG__)) && !defined(__clang__) && !defined(__INTEL_COMPILER) #define LIBASSERT_IS_GCC 1 #define LIBASSERT_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__) @@ -50,7 +49,6 @@ #define LIBASSERT_GCC_VERSION 0 #endif - #ifdef _MSC_VER #define LIBASSERT_IS_MSVC 1 #define LIBASSERT_MSVC_VERSION _MSC_VER @@ -59,6 +57,18 @@ #define LIBASSERT_MSVC_VERSION 0 #endif +#ifdef __INTEL_COMPILER + #define LIBASSERT_IS_ICC 1 +#else + #define LIBASSERT_IS_ICC 0 +#endif + +#ifdef __INTEL_LLVM_COMPILER + #define LIBASSERT_IS_ICX 1 +#else + #define LIBASSERT_IS_ICX 0 +#endif + /// /// Detect standard library versions. /// @@ -220,4 +230,77 @@ namespace libassert::detail { } } +#if LIBASSERT_IS_CLANG || LIBASSERT_IS_GCC + #if LIBASSERT_IS_GCC + #define LIBASSERT_WARNING_PRAGMA_PUSH_GCC _Pragma("GCC diagnostic push") + #define LIBASSERT_WARNING_PRAGMA_POP_GCC _Pragma("GCC diagnostic pop") + #define LIBASSERT_WARNING_PRAGMA_PUSH_CLANG + #define LIBASSERT_WARNING_PRAGMA_POP_CLANG + #else + #define LIBASSERT_WARNING_PRAGMA_PUSH_GCC + #define LIBASSERT_WARNING_PRAGMA_POP_GCC + #define LIBASSERT_WARNING_PRAGMA_PUSH_CLANG _Pragma("GCC diagnostic push") + #define LIBASSERT_WARNING_PRAGMA_POP_CLANG _Pragma("GCC diagnostic pop") + #endif +#else + #define LIBASSERT_WARNING_PRAGMA_PUSH_CLANG + #define LIBASSERT_WARNING_PRAGMA_POP_CLANG + #define LIBASSERT_WARNING_PRAGMA_PUSH_GCC + #define LIBASSERT_WARNING_PRAGMA_POP_GCC +#endif + +#if LIBASSERT_IS_CLANG || LIBASSERT_IS_ICX + // clang and icx support this as far back as this library could care + #define LIBASSERT_BREAKPOINT() __builtin_debugtrap() +#elif LIBASSERT_IS_MSVC || LIBASSERT_IS_ICC + // msvc and icc support this as far back as this library could care + #define LIBASSERT_BREAKPOINT() __debugbreak() +#elif LIBASSERT_IS_GCC + #if LIBASSERT_GCC_VERSION >= 1200 + #define LIBASSERT_IGNORE_CPP20_EXTENSION_WARNING _Pragma("GCC diagnostic ignored \"-Wc++20-extensions\"") + #else + #define LIBASSERT_IGNORE_CPP20_EXTENSION_WARNING + #endif + #define LIBASSERT_ASM_BREAKPOINT(instruction) \ + do { \ + LIBASSERT_WARNING_PRAGMA_PUSH_GCC \ + LIBASSERT_IGNORE_CPP20_EXTENSION_WARNING \ + __asm__ __volatile__(instruction) \ + ; \ + LIBASSERT_WARNING_PRAGMA_POP_GCC \ + } while(0) + // precedence for these come from llvm's __builtin_debugtrap() implementation + // arm: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/ARM/ARMInstrInfo.td#L2393-L2394 + // def : Pat<(debugtrap), (BKPT 0)>, Requires<[IsARM, HasV5T]>; + // def : Pat<(debugtrap), (UDF 254)>, Requires<[IsARM, NoV5T]>; + // thumb: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/ARM/ARMInstrThumb.td#L1444-L1445 + // def : Pat<(debugtrap), (tBKPT 0)>, Requires<[IsThumb, HasV5T]>; + // def : Pat<(debugtrap), (tUDF 254)>, Requires<[IsThumb, NoV5T]>; + // aarch64: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/AArch64/AArch64FastISel.cpp#L3615-L3618 + // case Intrinsic::debugtrap: + // BuildMI(*FuncInfo.MBB, FuncInfo.InsertPt, MIMD, TII.get(AArch64::BRK)) + // .addImm(0xF000); + // return true; + // x86: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/X86/X86InstrSystem.td#L81-L84 + // def : Pat<(debugtrap), + // (INT3)>, Requires<[NotPS]>; + #if defined(__i386__) || defined(__x86_64__) + #define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("int3") + #elif defined(__arm__) || defined(__thumb__) + #if __ARM_ARCH >= 5 + #define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("bkpt #0") + #else + #define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("udf #0xfe") + #endif + #elif defined(__aarch64__) + #define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("brk #0xf000") + #else + // some architecture we aren't prepared for + #define LIBASSERT_BREAKPOINT() + #endif +#else + // some compiler we aren't prepared for + #define LIBASSERT_BREAKPOINT() +#endif + #endif diff --git a/src/common.hpp b/src/common.hpp index 532285bd..998dc03a 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -24,10 +24,20 @@ #define BASIC_PURPL ESC "35m" #define IS_WINDOWS 0 +#define IS_LINUX 0 +#define IS_APPLE 0 #if defined(_WIN32) #undef IS_WINDOWS #define IS_WINDOWS 1 +#elif defined(__linux) + #undef IS_LINUX + #define IS_LINUX 1 +#elif defined(__APPLE__) + #undef IS_APPLE + #define IS_APPLE 1 +#else + #error "Libassert doesn't recognize this system, please open an issue at https://github.com/jeremy-rifkin/libassert" #endif #endif diff --git a/src/platform.cpp b/src/platform.cpp index 33593cd0..fded05ea 100644 --- a/src/platform.cpp +++ b/src/platform.cpp @@ -1,9 +1,11 @@ #include "platform.hpp" +#include #include #include #include #include +#include #include #include "common.hpp" @@ -14,13 +16,16 @@ #include #undef min // fucking windows headers, man #undef max -#elif defined(__linux) || defined(__APPLE__) || defined(__unix__) +#elif IS_LINUX #include #include - // NOLINTNEXTLINE(misc-include-cleaner) - #include // MAX_PATH -#else - #error "Libassert doesn't recognize this system, please open an issue at https://github.com/jeremy-rifkin/libassert" + #include + #include +#elif IS_APPLE + #include + #include + #include + #include #endif #include @@ -52,6 +57,114 @@ namespace libassert { #endif } + #if IS_LINUX + class file_closer { + int fd; + public: + file_closer(int fd_) : fd(fd_) {} + file_closer(const file_closer&) = delete; + file_closer(file_closer&&) = delete; + file_closer& operator=(const file_closer&) = delete; + file_closer& operator=(file_closer&&) = delete; + ~file_closer() { + if(close(fd) == -1) { + perror("Libassert: Something went wrong when closing a file descriptor."); + } + } + }; + #endif + + LIBASSERT_ATTR_COLD bool is_debugger_present_internal() noexcept { + #if IS_WINDOWS + return IsDebuggerPresent(); + #elif IS_LINUX + // https://www.man7.org/linux/man-pages/man5/proc.5.html + // We're looking at the top of /proc/self/status until we find "TracerPid:" + // Sample: + // Name: cat + // Umask: 0022 + // State: R (running) + // Tgid: 106085 + // Ngid: 0 + // Pid: 106085 + // PPid: 104045 + // TracerPid: 0 + // The name is truncated at 16 characters, so this whole thing should be under 256 chars + constexpr std::size_t read_goal = 256; + int fd = open("/proc/self/status", O_RDONLY); + if(fd == -1) { + // something went wrong + return false; + } + file_closer closer(fd); + char buffer[read_goal]; + auto size = read(fd, buffer, read_goal); + if(size == -1) { + // something went wrong + return false; + } + std::string_view status{buffer, std::size_t(size)}; + constexpr std::string_view key = "TracerPid:"; + auto pos = status.find(key); + if(pos == std::string_view::npos) { + return false; + } + auto pid_start = status.find_first_not_of(detail::whitespace_chars, pos + key.size()); + if(pid_start == std::string_view::npos) { + return false; + } + auto pid_end = status.find_first_of(detail::whitespace_chars, pid_start); + if(pid_end == std::string_view::npos) { + return false; + } + // since we found a whitespace character after the pid we know the read wasn't somehow weirdly truncated + auto tracer_pid = status.substr(pid_start, pid_end - pid_start); + int value; + if(std::from_chars(tracer_pid.data(), tracer_pid.data() + tracer_pid.size(), value).ec == std::errc{}) { + return value != 0; + } else { + return false; + } + return false; + #else + // https://developer.apple.com/library/archive/qa/qa1361/_index.html + int mib[4] {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; + struct kinfo_proc info; + info.kp_proc.p_flag = 0; + size_t size = sizeof(info); + int res = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + if(res != 0) { + // something went wrong, assume false + return false; + } + return info.kp_proc.p_flag & P_TRACED; + #endif + } + + std::atomic check_mode = debugger_check_mode::check_once; + std::mutex is_debugger_present_mutex; + std::optional cached_is_debugger_present; + + LIBASSERT_ATTR_COLD + bool is_debugger_present() noexcept { + if(check_mode.load() == debugger_check_mode::check_every_time) { + return is_debugger_present_internal(); + } else { + std::unique_lock lock(is_debugger_present_mutex); + if(cached_is_debugger_present) { + return *cached_is_debugger_present; + } else { + cached_is_debugger_present = is_debugger_present_internal(); + return *cached_is_debugger_present; + } + } + } + + LIBASSERT_ATTR_COLD LIBASSERT_EXPORT + void set_debugger_check_mode(debugger_check_mode mode) noexcept { + check_mode = mode; + } + LIBASSERT_ATTR_COLD LIBASSERT_EXPORT void enable_virtual_terminal_processing_if_needed() { // enable colors / ansi processing if necessary #if IS_WINDOWS @@ -82,7 +195,6 @@ namespace libassert::detail { LIBASSERT_ATTR_COLD std::string strerror_wrapper(int e) { std::unique_lock lock(strerror_mutex); - // NOLINTNEXTLINE(concurrency-mt-unsafe) return strerror(e); } } diff --git a/src/utils.cpp b/src/utils.cpp index c3502f7e..f84e0aca 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -101,15 +101,13 @@ namespace libassert::detail { return vec; } - constexpr const char * const ws = " \t\n\r\f\v"; - LIBASSERT_ATTR_COLD std::string_view trim(const std::string_view s) { - const size_t l = s.find_first_not_of(ws); + const size_t l = s.find_first_not_of(whitespace_chars); if(l == std::string_view::npos) { return ""; } - const size_t r = s.find_last_not_of(ws) + 1; + const size_t r = s.find_last_not_of(whitespace_chars) + 1; return s.substr(l, r - l); } diff --git a/src/utils.hpp b/src/utils.hpp index e190fc5b..c057969c 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -67,6 +67,8 @@ namespace libassert::detail { return a; } + constexpr const char* const whitespace_chars = " \t\n\r\f\v"; + LIBASSERT_ATTR_COLD std::string_view trim(std::string_view s); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fb809c16..ef12f788 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -142,6 +142,7 @@ if(PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) set( binary_sources tests/binaries/basic_test.cpp + tests/binaries/basic_demo.cpp tests/binaries/gtest-demo.cpp tests/binaries/catch2-demo.cpp tests/binaries/tokens_and_highlighting.cpp diff --git a/tests/binaries/basic_demo.cpp b/tests/binaries/basic_demo.cpp new file mode 100644 index 00000000..863cd93d --- /dev/null +++ b/tests/binaries/basic_demo.cpp @@ -0,0 +1,5 @@ +#include + +int main() { + ASSERT(1 + 1 == 3); +} diff --git a/tests/unit/literals.cpp b/tests/unit/literals.cpp index 7c6ed56f..a4071197 100644 --- a/tests/unit/literals.cpp +++ b/tests/unit/literals.cpp @@ -25,8 +25,6 @@ inline std::vector split(std::string_view s, std::string_view delim return vec; } -// https://stackoverflow.com/a/25385766/15675011 -// TODO: template constexpr const char * const ws = " \t\n\r\f\v"; // trim from end of string (right)