| // Copyright Microsoft and CHERIoT Contributors. |
| // SPDX-License-Identifier: MIT |
| |
| #pragma once |
| #include <__debug.h> |
| #include <cheri.hh> |
| #include <compartment.h> |
| #include <concepts> |
| #include <cstddef> |
| #include <platform-uart.hh> |
| #include <string.h> |
| #include <switcher.h> |
| |
| #include <array> |
| #include <string_view> |
| #include <type_traits> |
| |
| namespace DebugConcepts |
| { |
| /// Helper concept for matching booleans |
| template<typename T> |
| concept IsBool = std::is_same_v<T, bool>; |
| |
| /// Helper concept for matching enumerations. |
| template<typename T> |
| concept IsEnum = std::is_enum_v<T>; |
| |
| /// Concept for something that can be lazily called to produce a bool. |
| template<typename T> |
| concept LazyAssertion = requires(T v) |
| { |
| { |
| v() |
| } -> IsBool; |
| }; |
| |
| template<typename T> |
| concept IsPointerButNotCString = |
| std::is_pointer_v<T> && !std::is_same_v<T, const char *>; |
| |
| template<typename T> |
| concept IsConvertibleToAddress = |
| std::convertible_to<T, ptraddr_t> && !IsEnum<T>; |
| |
| } // namespace DebugConcepts |
| |
| /** |
| * Abstract class for writing debug output. This is used for custom output. |
| * |
| * This may be changed in the future to provide better support for custom |
| * formatting. |
| */ |
| struct DebugWriter |
| { |
| /** |
| * Write a single character. |
| */ |
| virtual void write(char) = 0; |
| /** |
| * Write a C string. |
| */ |
| virtual void write(const char *) = 0; |
| /** |
| * Write a string view. |
| */ |
| virtual void write(std::string_view) = 0; |
| /** |
| * Write a 32-bit unsigned integer. |
| */ |
| virtual void write(uint32_t) = 0; |
| /** |
| * Write a 32-bit signed integer. |
| */ |
| virtual void write(int32_t) = 0; |
| /** |
| * Write a 64-bit unsigned integer. |
| */ |
| virtual void write(uint64_t) = 0; |
| /** |
| * Write a 64-bit signed integer. |
| */ |
| virtual void write(int64_t) = 0; |
| }; |
| |
| /** |
| * Helper function for writing enumerations. Enumerations are written using |
| * magic_enum to provide a string and then a numeric value. |
| */ |
| template<typename T> |
| void debug_enum_helper(uintptr_t value, |
| DebugWriter &writer) requires DebugConcepts::IsEnum<T> |
| { |
| writer.write(magic_enum::enum_name<T>(static_cast<T>(value))); |
| writer.write('('); |
| writer.write(uint32_t(value)); |
| writer.write(')'); |
| } |
| |
| /** |
| * Callback for custom types in debug output. This should use the second |
| * argument to write the first argument to the debug output. |
| */ |
| using DebugCallback = void (*)(uintptr_t, DebugWriter &); |
| |
| /** |
| * Adaptor that turns an argument of type `T` into a `DebugFormatArgument`. |
| * |
| * Users may specialise these to provide custom formatters. See the |
| * specialisation for enumerations for an example. |
| */ |
| template<typename T> |
| struct DebugFormatArgumentAdaptor; |
| |
| /** |
| * Boolean specialisation, prints "true" or "false". |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<bool> |
| { |
| __always_inline static DebugFormatArgument construct(bool value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentBool}; |
| } |
| }; |
| |
| /** |
| * Character specialisation, prints the character. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<char> |
| { |
| __always_inline static DebugFormatArgument construct(char value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentCharacter}; |
| } |
| }; |
| |
| /** |
| * Unsigned character specialisation, prints the character as a hex number. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<uint8_t> |
| { |
| __always_inline static DebugFormatArgument construct(uint8_t value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32}; |
| } |
| }; |
| |
| /** |
| * Unsigned 16-bit integer specialisation, prints the integer as a hex number. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<uint16_t> |
| { |
| __always_inline static DebugFormatArgument construct(uint16_t value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32}; |
| } |
| }; |
| |
| /** |
| * Unsigned 32-bit integer specialisation, prints the integer as a hex number. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<uint32_t> |
| { |
| __always_inline static DebugFormatArgument construct(uint32_t value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32}; |
| } |
| }; |
| |
| /** |
| * Unsigned 64-bit integer specialisation, prints the integer as a hex number. |
| * |
| * All smaller sizes are handled by zero extending to 32 bits. We treat 64-bit |
| * separately because it requires decomposing the two halves for printing and |
| * that's redundant overhead for the majority of cases. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<uint64_t> |
| { |
| __always_inline static DebugFormatArgument construct(uint64_t value) |
| { |
| uintptr_t fudgedValue; |
| memcpy(&fudgedValue, &value, sizeof(fudgedValue)); |
| return {fudgedValue, |
| DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber64}; |
| } |
| }; |
| |
| /** |
| * Signed 8-bit integer specialisations, print the integer as a decimal number. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<int8_t> |
| { |
| __always_inline static DebugFormatArgument construct(int8_t value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentSignedNumber32}; |
| } |
| }; |
| |
| /** |
| * Signed 16-bit integer specialisations, print the integer as a decimal number. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<int16_t> |
| { |
| __always_inline static DebugFormatArgument construct(int16_t value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentSignedNumber32}; |
| } |
| }; |
| |
| /** |
| * Signed 32-bit integer specialisations, print the integer as a decimal number. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<int32_t> |
| { |
| __always_inline static DebugFormatArgument construct(int32_t value) |
| { |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentSignedNumber32}; |
| } |
| }; |
| |
| /** |
| * Signed 64-bit integer specialisations, print the integer as a decimal number. |
| * |
| * All smaller sizes are handled by sign extending to 32 bits. We treat 64-bit |
| * separately because it requires 64-bit division to convert a 64-bit integer |
| * to a decimal and that, in turn, requires libcalls. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<int64_t> |
| { |
| __always_inline static DebugFormatArgument construct(int64_t value) |
| { |
| static_assert(sizeof(uintptr_t) == sizeof(uint64_t)); |
| uintptr_t fudgedValue; |
| memcpy(&fudgedValue, &value, sizeof(fudgedValue)); |
| return {fudgedValue, |
| DebugFormatArgumentKind::DebugFormatArgumentSignedNumber64}; |
| } |
| }; |
| |
| /** |
| * C string specialisation, prints the string as-is. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<const char *> |
| { |
| __always_inline static DebugFormatArgument construct(const char *value) |
| { |
| return {reinterpret_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentCString}; |
| } |
| }; |
| |
| /** |
| * String view specialisation, prints the string as-is. |
| * |
| * Note that this relies on the string view persisting for the duration of the |
| * call. It passes a pointer to the string-view argument. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<std::string_view> |
| { |
| __always_inline static DebugFormatArgument |
| construct(std::string_view &value) |
| { |
| return {reinterpret_cast<uintptr_t>(&value), |
| DebugFormatArgumentKind::DebugFormatArgumentStringView}; |
| } |
| }; |
| |
| /** |
| * String view specialisation, use the C string handler. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<std::string> |
| { |
| __always_inline static DebugFormatArgument construct(std::string &value) |
| { |
| return DebugFormatArgumentAdaptor<const char *>::construct( |
| value.c_str()); |
| } |
| }; |
| |
| /** |
| * Enum specialisation, prints the enum as a string and then the numeric value. |
| * |
| * This specialisation uses the generic printing facility in the library call |
| * and passes a callback that will map the enumeration to a string. |
| */ |
| template<DebugConcepts::IsEnum T> |
| struct DebugFormatArgumentAdaptor<T> |
| { |
| __always_inline static DebugFormatArgument construct(T value) |
| { |
| #ifdef CHERIOT_AVOID_CAPRELOCS |
| return {static_cast<uintptr_t>(value), |
| DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32}; |
| #else |
| return {static_cast<uintptr_t>(value), |
| reinterpret_cast<uintptr_t>(&debug_enum_helper<T>)}; |
| #endif |
| } |
| }; |
| |
| /** |
| * Permission set specialisation. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<CHERI::PermissionSet> |
| { |
| __always_inline static DebugFormatArgument |
| construct(CHERI::PermissionSet value) |
| { |
| return {static_cast<uintptr_t>(value.as_raw()), |
| DebugFormatArgumentKind::DebugFormatArgumentPermissionSet}; |
| } |
| }; |
| |
| /** |
| * Null pointer specialisation. |
| */ |
| template<> |
| struct DebugFormatArgumentAdaptor<std::nullptr_t> |
| { |
| __always_inline static DebugFormatArgument construct(std::nullptr_t) |
| { |
| return {0, DebugFormatArgumentKind::DebugFormatArgumentPointer}; |
| } |
| }; |
| |
| /** |
| * Pointer specialisation, prints the pointer as a full capability. |
| */ |
| template<DebugConcepts::IsPointerButNotCString T> |
| struct DebugFormatArgumentAdaptor<T> |
| { |
| __always_inline static DebugFormatArgument construct(T value) |
| { |
| return {reinterpret_cast<uintptr_t>( |
| static_cast<const volatile void *>(value)), |
| DebugFormatArgumentKind::DebugFormatArgumentPointer}; |
| } |
| }; |
| |
| /** |
| * Specialisation for the CHERI capability wrapper class, prints the capability |
| * in the same format as bare pointers. |
| */ |
| template<typename T> |
| struct DebugFormatArgumentAdaptor<CHERI::Capability<T>> |
| { |
| __always_inline static DebugFormatArgument |
| construct(CHERI::Capability<T> value) |
| { |
| return {reinterpret_cast<uintptr_t>( |
| static_cast<const volatile void *>(value)), |
| DebugFormatArgumentKind::DebugFormatArgumentPointer}; |
| } |
| }; |
| |
| /** |
| * Specialisation for things that can be converted to addresses, prints as an |
| * unsigned number. |
| */ |
| template<DebugConcepts::IsConvertibleToAddress T> |
| struct DebugFormatArgumentAdaptor<T> |
| { |
| __always_inline static DebugFormatArgument construct(ptraddr_t value) |
| { |
| return {static_cast<uintptr_t>(value), |
| |
| DebugFormatArgumentKind::DebugFormatArgumentUnsignedNumber32}; |
| } |
| }; |
| |
| /** |
| * Recursive helper that maps from a tuple representing the arguments into a |
| * type-erased array. |
| */ |
| template<size_t I> |
| __always_inline inline void map_debug_argument(DebugFormatArgument *arguments, |
| auto &&argumentTuple) |
| { |
| arguments[I] = |
| DebugFormatArgumentAdaptor< |
| std::remove_cvref_t<decltype(std::get<I>(argumentTuple))>>{} |
| .construct(std::get<I>(argumentTuple)); |
| if constexpr (I > 0) |
| { |
| map_debug_argument<I - 1>(arguments, argumentTuple); |
| } |
| } |
| |
| /** |
| * Convert `args` into a type-erased array of `DebugFormatArgument`s in |
| * `arguments`. |
| */ |
| template<typename... Args> |
| __always_inline inline void |
| make_debug_arguments_list(DebugFormatArgument *arguments, Args... args) |
| { |
| if constexpr (sizeof...(Args) > 0) |
| { |
| map_debug_argument<sizeof...(Args) - 1>(arguments, |
| std::forward_as_tuple(args...)); |
| } |
| } |
| |
| /** |
| * Library function that writes a debug message. This runs with interrupts |
| * disabled (to avoid interleaving) and prints an array of debug messages. This |
| * is intended to allow a single call to print multiple format strings without |
| * requiring the format strings to be copied, so that the debugging APIs can |
| * wrap a user-provided format string. |
| */ |
| __cheri_libcall void debug_log_message_write(const char *context, |
| const char *format, |
| DebugFormatArgument *messages, |
| size_t messageCount); |
| |
| __cheri_libcall void debug_report_failure(const char *kind, |
| const char *file, |
| const char *function, |
| int line, |
| const char *fmt, |
| DebugFormatArgument *arguments, |
| size_t argumentCount); |
| |
| namespace |
| { |
| /** |
| * Helper class wrapping a string for use as a template argument. This is |
| * used to describe a context for conditional debugging that will be |
| * prefixed to debug lines. |
| */ |
| template<size_t N> |
| struct DebugContext |
| { |
| /** |
| * Constructor, captures the string argument. |
| */ |
| constexpr DebugContext(const char (&str)[N]) |
| { |
| std::copy_n(str, N, value); |
| } |
| |
| /** |
| * Implicit conversion to a C string. |
| */ |
| constexpr operator const char *() const |
| { |
| return value; |
| } |
| |
| /** |
| * The captured string. Must be public for this class to meet the |
| * requirements of a structural type for use as a template argument. |
| */ |
| char value[N]; |
| }; |
| |
| /** |
| * Our libc++ does not currently provide source_location, but our clang |
| * provides the necessary builtins for one. |
| */ |
| struct SourceLocation |
| { |
| /** |
| * Explicitly construct a source location. |
| */ |
| constexpr SourceLocation(int lineNumber, |
| int columnNumber, |
| const char *fileName, |
| const char *functionName) |
| : lineNumber(lineNumber), |
| columnNumber(columnNumber), |
| fileName(fileName), |
| functionName(functionName) |
| { |
| } |
| |
| /** |
| * Construct a source location for the caller. |
| */ |
| static constexpr SourceLocation __always_inline |
| current(int lineNumber = __builtin_LINE(), |
| int columnNumber = __builtin_COLUMN(), |
| const char *fileName = __builtin_FILE(), |
| const char *functionName = __builtin_FUNCTION()) noexcept |
| { |
| return {lineNumber, columnNumber, fileName, functionName}; |
| } |
| |
| /// Returns the line number for this source location. |
| [[nodiscard]] __always_inline constexpr int line() const noexcept |
| { |
| return lineNumber; |
| } |
| /// Returns the column number for this source location. |
| [[nodiscard]] __always_inline constexpr int column() const noexcept |
| { |
| return columnNumber; |
| } |
| /// Returns the file name for this source location. |
| [[nodiscard]] __always_inline constexpr const char * |
| file_name() const noexcept |
| { |
| return fileName; |
| } |
| /// Returns the function name for this source location. |
| [[nodiscard]] __always_inline constexpr const char * |
| function_name() const noexcept |
| { |
| return functionName; |
| } |
| |
| private: |
| /// The line number of this source location. |
| int lineNumber; |
| /// The column number of this source location. |
| int columnNumber; |
| /// The file name of this source location. |
| const char *fileName; |
| /// The function name of this source location. |
| const char *functionName; |
| }; |
| |
| /** |
| * Conditional debug class. Used to control conditional output and |
| * assertion checking. Enables debug log messages and assertions if |
| * `Enabled` is true. Uses `Context` to print additional detail on debug |
| * lines. Each line is prefixed with the context string in magenta to make |
| * it easy to see debug output from different subsystems in the same trace. |
| * |
| * This class is expected to be used as a type alias, something like: |
| * |
| * ```c++ |
| * constexpr bool DebugFoo = DEBUG_FOO; |
| * using Debug = ConditionalDebug<DebugFoo, "Foo">; |
| * ``` |
| */ |
| template<bool Enabled, DebugContext Context> |
| class ConditionalDebug |
| { |
| public: |
| /** |
| * Log a message. |
| * |
| * This function does nothing if the `Enabled` condition is false. |
| */ |
| template<typename... Args> |
| static void log(const char *fmt, Args... args) |
| { |
| if constexpr (Enabled) |
| { |
| asm volatile("" ::: "memory"); |
| DebugFormatArgument arguments[sizeof...(Args)]; |
| make_debug_arguments_list(arguments, args...); |
| const char *context = Context; |
| debug_log_message_write( |
| context, fmt, arguments, sizeof...(Args)); |
| asm volatile("" ::: "memory"); |
| } |
| } |
| |
| /** |
| * Helper to report failure. |
| * |
| * This must not take the `SourceLocation` directly because doing so |
| * prevents the compiler from decomposing and subsequently |
| * constant-propagating its fields in the caller, resulting in 24 bytes |
| * of stack space consumed for every assert or invariant. |
| */ |
| template<typename... Args> |
| static inline void report_failure(const char *kind, |
| const char *file, |
| const char *function, |
| int line, |
| const char *fmt, |
| Args... args) |
| { |
| // Ensure that the compiler does not reorder messages. |
| DebugFormatArgument arguments[sizeof...(Args)]; |
| make_debug_arguments_list(arguments, args...); |
| const char *context = Context; |
| debug_report_failure( |
| kind, file, function, line, fmt, arguments, sizeof...(Args)); |
| } |
| |
| /** |
| * Function-like class for invariants that is expected to be used via |
| * the deduction guide as: |
| * |
| * ConditionalDebug::Invariant(someCondition, "A message...", ...); |
| * |
| * Invariants are checked unconditionally but will log a verbose |
| * message only if `Enabled` is true. |
| */ |
| template<typename... Args> |
| struct Invariant |
| { |
| /** |
| * Constructor, performs the invariant check. |
| */ |
| __always_inline |
| Invariant(bool condition, |
| const char *fmt, |
| Args... args, |
| SourceLocation loc = SourceLocation::current()) |
| { |
| if (__predict_false(!condition)) |
| { |
| if constexpr (Enabled) |
| { |
| report_failure("Invariant", |
| loc.file_name(), |
| loc.function_name(), |
| loc.line(), |
| fmt, |
| std::forward<Args>(args)...); |
| } |
| __builtin_trap(); |
| } |
| } |
| }; |
| /** |
| * Function-like class for assertions that is expected to be used via |
| * the deduction guide as: |
| * |
| * ConditionalDebug::Assert(someCondition, "A message...", ...); |
| * |
| * Assertions are checked only if `Enabled` is true. |
| */ |
| template<typename... Args> |
| struct Assert |
| { |
| /** |
| * Constructor, performs the assertion check. |
| */ |
| template<typename T> |
| requires DebugConcepts::IsBool<T> __always_inline |
| Assert(T condition, |
| const char *fmt, |
| Args... args, |
| SourceLocation loc = SourceLocation::current()) |
| { |
| if constexpr (Enabled) |
| { |
| if (__predict_false(!condition)) |
| { |
| report_failure("Assertion", |
| loc.file_name(), |
| loc.function_name(), |
| loc.line(), |
| fmt, |
| std::forward<Args>(args)...); |
| __builtin_trap(); |
| } |
| } |
| } |
| |
| /** |
| * Constructor, performs an assertion check. |
| * |
| * This version is passed a lambda that returns a bool, rather than |
| * a boolean condition. This allows the compiler to completely |
| * elide the contents of the lambda in builds where this component |
| * is not being debugged. This version should be used for places |
| * where the assertion condition has side effects. |
| */ |
| template<typename T> |
| requires DebugConcepts::LazyAssertion<T> __always_inline |
| Assert(T &&condition, |
| const char *fmt, |
| Args... args, |
| SourceLocation loc = SourceLocation::current()) |
| { |
| if constexpr (Enabled) |
| { |
| if (__predict_false(!condition())) |
| { |
| report_failure("Assertion", |
| loc.file_name(), |
| loc.function_name(), |
| loc.line(), |
| fmt, |
| std::forward<Args>(args)...); |
| __builtin_trap(); |
| } |
| } |
| } |
| }; |
| |
| /** |
| * Deduction guide, allows `Invariant` to be used as if it were a |
| * function. |
| */ |
| template<typename T, typename... Ts> |
| Invariant(T, const char *, Ts &&...) -> Invariant<Ts...>; |
| |
| /** |
| * Deduction guide, allows `Assert` to be used as if it were a |
| * function. |
| */ |
| template<typename... Ts> |
| Assert(bool, const char *, Ts &&...) -> Assert<Ts...>; |
| |
| /** |
| * Deduction guide, allows `Assert` to be used as if it were a |
| * function with a lambda argument. |
| */ |
| template<typename... Ts> |
| Assert(auto, const char *, Ts &&...) -> Assert<Ts...>; |
| }; |
| |
| enum class StackCheckMode |
| { |
| Disabled, |
| Logging, |
| Asserting, |
| }; |
| |
| /** |
| * Check the (dynamic) stack usage of a function, including all of its |
| * callees. This is intended to be used in compartment entry points to |
| * check the stack size that is required for that entry point. Use this |
| * with some test vectors that explore different code paths to identify the |
| * maximum stack usage. This can then be used with the |
| * [[cheriot::minimum_stack]] attribute to ensure that the switcher will |
| * never invoke the entry point with insufficient stack. |
| * |
| * Note that this records only the stack usage for the current compartment |
| * invocation (including any invoked shared libraries). If this |
| * compartment calls others, then a larger stack may be required. This |
| * failure is recoverable: cross-compartment calls should always be assumed |
| * to potentially fail. This check is to ensure that an attacker cannot |
| * cause stack overflow to crash the compartment, not to ensure that an |
| * attacker cannot cause stack overflow to make the operation that they |
| * requested fail. |
| * |
| * Stack checks run in one of three modes: |
| * |
| * - Disabled: The checks do not run. |
| * - Logging: The checks run and print a message if the stack usage |
| * exceeds expectations. |
| * - Asserting: After printing the message, the function will trap. |
| * |
| * In logging mode, one message will be printed for each invocation of the |
| * function that exceeds the previous maximum stack usage. |
| * |
| * An instance of this should be created as a local variable on entry to a |
| * function. The destructor (which prints the message) will then run after |
| * any other destructors, ensuring the most accurate stack usage |
| * measurement. |
| * |
| * Unfortunately, default arguments for templates are not evaluated in the |
| * enclosing scope and so `__builtin_FUNCTION()` cannot be used here. |
| * Instead, we must pass this explicitly, typically as something like: |
| * |
| * ```c++ |
| * StackUsageCheck<DebugFlag, 128, __PRETTY_FUNCTION__> stackCheck; |
| * ``` |
| */ |
| template<StackCheckMode Mode, size_t Expected, DebugContext Fn> |
| class StackUsageCheck |
| { |
| /** |
| * The expected maximum. This class is templated on the function name |
| * and so there is one copy of this per function. |
| */ |
| static inline size_t stackUsage = Expected; |
| |
| public: |
| /** |
| * Default constructor, does nothing. |
| */ |
| StackUsageCheck() = default; |
| |
| /** |
| * Destructor, runs at the end of the function to print the message. |
| */ |
| __always_inline ~StackUsageCheck() |
| { |
| if constexpr (Mode != StackCheckMode::Disabled) |
| { |
| ptraddr_t lowest = stack_lowest_used_address(); |
| ptraddr_t highest = |
| CHERI::Capability{__builtin_cheri_stack_get()}.top(); |
| size_t used = highest - lowest; |
| if (used > stackUsage) |
| { |
| stackUsage = used; |
| ConditionalDebug<true, Fn>::log("Stack used: {} bytes", |
| stackUsage); |
| if constexpr (Mode == StackCheckMode::Asserting) |
| { |
| __builtin_trap(); |
| } |
| } |
| } |
| } |
| }; |
| |
| } // namespace |