SERVER-94540 Vendor cpptrace (#40807)

GitOrigin-RevId: c6e998d87ec2f7adfb02996a659b0e72d079709e
This commit is contained in:
Alex Li 2025-09-03 18:22:05 -04:00 committed by MongoDB Bot
parent 7af4081495
commit 6c3bf5e2a3
103 changed files with 16333 additions and 0 deletions

1
.github/CODEOWNERS vendored
View File

@ -3080,6 +3080,7 @@ WORKSPACE.bazel @10gen/devprod-build @svc-auto-approve-bot
/src/third_party/**/benchmark @10gen/server-programmability @svc-auto-approve-bot
/src/third_party/**/boost @10gen/server-programmability @svc-auto-approve-bot
/src/third_party/**/cares @10gen/server-networking-and-observability @svc-auto-approve-bot
/src/third_party/**/cpptrace @10gen/server-programmability @svc-auto-approve-bot
/src/third_party/**/croaring @10gen/query-execution @svc-auto-approve-bot
/src/third_party/**/fmt @10gen/server-programmability @svc-auto-approve-bot
/src/third_party/**/folly @10gen/server-workload-resilience @svc-auto-approve-bot

View File

@ -2894,6 +2894,49 @@
]
},
"scope": "excluded"
},
{
"name": "cpptrace",
"supplier": {
"name": "Organization: github"
},
"version": "v1.0.3",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"purl": "pkg:github/jeremy-rifkin/cpptrace@v1.0.3",
"properties": [
{
"name": "internal:team_responsible",
"value": "Server Programmability"
},
{
"name": "emits_persisted_data",
"value": "false"
},
{
"name": "info_link",
"value": "https://github.com/jeremy-rifkin/cpptrace"
},
{
"name": "import_script_path",
"value": "src/third_party/cpptrace/scripts/import.sh"
}
],
"type": "library",
"bom-ref": "ae9c7977-70ac-4706-9231-a7d10f05542b",
"evidence": {
"occurrences": [
{
"location": "src/third_party/cpptrace"
}
]
},
"scope": "excluded"
}
],
"dependencies": [

View File

@ -21,6 +21,9 @@ filters:
- "cares":
approvers:
- 10gen/server-networking-and-observability
- "cpptrace":
approvers:
- 10gen/server-programmability
- "croaring":
approvers:
- 10gen/query-execution

38
src/third_party/cpptrace/BUILD.bazel vendored Normal file
View File

@ -0,0 +1,38 @@
load("//bazel:mongo_src_rules.bzl", "mongo_cc_library")
package(default_visibility = ["//visibility:public"])
mongo_cc_library(
name = "cpptrace",
srcs = glob([
"dist/src/**/*.hpp",
"dist/src/**/*.cpp",
]),
hdrs = glob([
"dist/include/cpptrace/*.hpp",
"dist/include/ctrace/*.h",
]),
includes = [
"dist/include",
"dist/src",
],
linkopts = [
"-ldl",
],
local_defines = [
"CPPTRACE_UNWIND_WITH_LIBUNWIND",
"CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF",
"CPPTRACE_DEMANGLE_WITH_CXXABI",
],
target_compatible_with = ["@platforms//os:linux"] + select({
"//bazel/config:release_enabled": ["@platforms//:incompatible"],
"//bazel/config:libunwind_disabled": ["@platforms//:incompatible"],
"//conditions:default": [],
}),
visibility = ["//visibility:public"],
deps = [
"//src/third_party/libdwarf",
"//src/third_party/zlib",
"//src/third_party/zstandard:zstd",
],
)

View File

@ -0,0 +1,604 @@
# Changelog
- [Changelog](#changelog)
- [v1.0.3](#v103)
- [v1.0.2](#v102)
- [v1.0.1](#v101)
- [v1.0.0](#v100)
- [v0.8.3](#v083)
- [v0.8.2](#v082)
- [v0.8.1](#v081)
- [v0.8.0](#v080)
- [v0.7.5](#v075)
- [v0.7.4](#v074)
- [v0.7.3](#v073)
- [v0.7.2](#v072)
- [v0.7.1](#v071)
- [v0.7.0](#v070)
- [v0.6.3](#v063)
- [v0.6.2](#v062)
- [v0.6.1](#v061)
- [v0.6.0](#v060)
- [v0.5.4](#v054)
- [v0.5.3](#v053)
- [v0.5.2](#v052)
- [v0.5.1](#v051)
- [v0.5.0](#v050)
- [v0.4.1](#v041)
- [v0.4.0](#v040)
- [v0.3.1](#v031)
- [v0.3.0](#v030)
- [v0.2.1](#v021)
- [v0.2.0](#v020)
- [v0.1.1](#v011)
- [v0.1](#v01)
# v1.0.3
Added:
- Added line indicator to make source code snippets in case colors aren't being used
- Added column indicator to source code snippets
- Added 32-bit ARM support for StackWalk64 https://github.com/jeremy-rifkin/cpptrace/pull/271 (@Xottab-DUTY)
# v1.0.2
Added:
- Added `break_before_filename` formatting option https://github.com/jeremy-rifkin/cpptrace/issues/259 (@codeinred)
Fixes:
- Fixed 32-bit clang-cl build
- Fixed build on gcc 4.8.5
- Fixed StackWalk64 for 64-bit arm https://github.com/jeremy-rifkin/cpptrace/pull/270 (@mcourteaux)
- Fixed compatibility issue with cmake versions before 3.23
Other:
- Added a couple notes to the README
# v1.0.1
Added:
- Added from-current-exception utility for SEH on windows (`CPPTRACE_SEH_TRY`/`CPPTRACE_SEH_EXCEPT`)
Fixes:
- Fixed a static assert without a message causing issues on some C++11 builds
- Fixed build 32-bit build on linux
- Fixed from-current-exception system for 32-bit windows where SEH behaves much differently
- Fixed clang-cl build
# v1.0.0
Major changes:
- Overhauled how the from-current-exception system and `CPPTRACE_TRY`/`CPPTRACE_CATCH` macros work. They now check the
thrown exception type against the type the catch accepts to decide whether or not to collect a trace. This eliminates
the need for The `TRYZ`/`CATCHZ` variants of the macros as now the normal macro is equally zero-overhead. As such, the
`Z` variants have been removed.
Breaking changes:
- `CPPTRACE_TRYZ` and `CPPTRACE_CATCHZ` have been removed, change uses to `CPPTRACE_TRY`/`CPPTRACE_CATCH`
- `CPPTRACE_TRY`/`CPPTRACE_CATCH` macros no longer support multiple handlers and `CPPTRACE_CATCH_ALT` has been removed.
Instead, use `cpptrace::try_catch`.
> **Details**
>
> Author's note: I apologize for this non-trivial breaking change, however, it allows for a much improved
> implementation of the from-current-exception system. The impact should be [very minimal](https://github.com/search?type=code&q=%2F%5CbCPPTRACE_CATCH_ALT%5Cb%2F+language%3Ac%2B%2B+-is%3Afork+-repo%3Ajeremy-rifkin%2Fcpptrace+-path%3Afrom_current.hpp)
> for most codebases.
>
> Transitioning to `cpptrace::try_catch` is straightforward:
> <table>
> <thead>
> <tr>
> <td>
> Before
> </td>
> <td>
> Now
> </td>
> </tr>
> </thead>
> <tbody>
> <tr>
> <td>
> ```cpp
> CPPTRACE_TRY {
> foo();
> } CPPTRACE_CATCH(const std::logic_error& e) {
> handle_logic_error(e);
> } CPPTRACE_CATCH_ALT(const std::exception& e) {
> handle_exception(e);
> } CPPTRACE_CATCH_ALT(...) {
> handle_unknown_exception();
> }
> ```
> </td>
> <td>
> ```cpp
> cpptrace::try_catch(
> [&] { // try block
> foo();
> },
> [&] (const std::runtime_error& e) {
> handle_logic_error(e);
> },
> [&] (const std::exception& e) {
> handle_exception(e);
> },
> [&] () { // `catch(...)`
> handle_unknown_exception();
> }
> );
> ```
> </td>
> </tr>
> </tbody>
> </table>
>
> Please note as well that code such as the following, which was valid before, will still compile now but may report a
> misleading trace. The second catch handler will work but it cpptrace won't know about the handler's type and won't
> collect a trace that doesn't match the first handler's type, `std::logic_error`.
>
> ```cpp
> CPPTRACE_TRY {
> foo();
> } CPPTRACE_CATCH(const std::logic_error& e) {
> ...
> } catch(const std::exception& e) {
> ...
> }
> ```
Potentially-breaking changes:
- This version of cpptrace reworks the public interface to use an inline ABI versioning namespace. All symbols in the
public interface are now secretly in the `cpptrace::v1` namespace. This is an ABI break, but any ABI mismatch will
result in linker errors instead of silent bugs. This change is an effort to allow future evolution of cpptrace in a
way that respects ABI.
- This version fixes a problem with returns from `CPPTRACE_TRY` blocks on windows. Unfortunately, this macro has to use
an IILE on windows and as such `return` statements won't properly return from the enclosing function. This was a
footgun and now `return` statements in `CPPTRACE_TRY` blocks are prevented from compiling on windows.
Added
- Added `cpptrace::try_catch` for handling multiple alternatives with access to current exception traces
- Added `cpptrace::rethrow` utility for rethrowing exceptions from `CPPTRACE_CATCH` while preserving the stacktrace https://github.com/jeremy-rifkin/cpptrace/issues/214
- Added utilities for getting the current trace from the last rethrow point
(`cpptrace::raw_trace_from_current_exception_rethrow`, `cpptrace::from_current_exception_rethrow`,
`cpptrace::current_exception_was_rethrown`)
- Added a logger system to allow cpptrace to report errors in a configurable manner. By default, cpptrace doesn't log
anything. The following functions can be used to change this: (`cpptrace::set_log_level`,
`cpptrace::set_log_callback`, and `cpptrace::use_default_stderr_logger`)
- Added `cpptrace::basename` utility
- Added `cpptrace::prettify_type` utility
- Added `cpptrace::prune_symbol` utility
- Added formatter option for symbol formatting (`cpptrace::formatter::symbols`)
- Added `cpptrace::detail::lazy_trace_holder::is_resolved`
- Added support for C++20 modules https://github.com/jeremy-rifkin/cpptrace/pull/248
- Added `cpptrace::load_symbols_for_file` to support DLLs loaded at runtime when using dbghelp https://github.com/jeremy-rifkin/cpptrace/pull/247
Removed
- Removed `CPPTRACE_TRYZ` and `CPPTRACE_CATCHZ` macros
- Removed the `CPPTRACE_CATCH_ALT` macro
Fixed
- Fixed a problem where `CPPTRACE_TRY` blocks could contain `return` statements but not return on windows due to an IILE. This is now an error. https://github.com/jeremy-rifkin/cpptrace/issues/245
- Fixed cases where cpptrace could print to stderr on internal errors without the user desiring so https://github.com/jeremy-rifkin/cpptrace/issues/234
- Fixed a couple internal locking mistakes
- Fixed a couple of code paths that could be susceptible to static init order issues
- Fixed bug with loading elf symbol tables that contain zero-sized entries
- Fixed an incorrect assertion regarding looking up symbols at program counters that reside before any seen subprogram DIE https://github.com/jeremy-rifkin/cpptrace/issues/250
- Fixed issue with `cpptrace::stacktrace::to_string()` ending with a newline on empty traces
Other
- Marked some paths in `CPPTRACE_CATCH` and `CPPTRACE_CATCHZ` as unreachable to improve usability in cases where the
compiler may warn about missing returns.
- Improved resilience to libdwarf errors https://github.com/jeremy-rifkin/cpptrace/pull/251
- Removed internal use of `std::is_trivial` which is deprecated in C++26 https://github.com/jeremy-rifkin/cpptrace/issues/236
- Bumped libdwarf to 2.0.0
- Added `--address` flag for internal symbol table tool
- Various internal work to improve the codebase and reduce complexity
# v0.8.3
Added:
- Added basic JIT support https://github.com/jeremy-rifkin/cpptrace/issues/226
- Added `cpptrace::formatter::transform` https://github.com/jeremy-rifkin/cpptrace/issues/227
- Added support for gcc 4.8.5 https://github.com/jeremy-rifkin/cpptrace/issues/220
Fixed:
- Fixed bug related to calling `dwarf_dealloc` on strings from `dwarf_formstring` and `dwarf_diename` https://github.com/davea42/libdwarf-code/issues/279
- Fixed incorrect cmake version variable https://github.com/jeremy-rifkin/cpptrace/issues/231
- Fixed `address_mode::none` not working https://github.com/jeremy-rifkin/cpptrace/issues/221
- Fixed use of `-Wall` for clang-cl
Other:
- Added ARM CI
- Miscellaneous work on supporting old compilers
- Updated cpptrace cmake target configuration to not add public compile definitions
- Internal refactoring, cleanup, and code improvements
# v0.8.2
Fixed:
- Fixed printing of internal error messages when an object file can't be loaded, mainly affecting MacOS https://github.com/jeremy-rifkin/cpptrace/issues/217
Other:
- Bumped zstd via FetchContent to 1.5.7
# v0.8.1
Fixed:
- Fixed compile error on msvc https://github.com/jeremy-rifkin/cpptrace/issues/215
Added:
- Added `cpptrace::can_get_safe_object_frame()`
Breaking changes:
- Renamed ctrace's `can_signal_safe_unwind` to `ctrace_can_signal_safe_unwind`. This was an oversight. Apologies for
including a breaking change in a patch release. Github code search suggests this API isn't used in public code, at
least.
Other:
- Added CI workflow to test on old msvc
- Made some internal improvements on robustness and cleanliness
# v0.8.0
Added:
- Added support for resolving symbols from elf and mach-o symbol tables, allowing function names to be resolved even in
a build that doesn't include debug information https://github.com/jeremy-rifkin/cpptrace/issues/201
- Added a configurable stack trace formatter https://github.com/jeremy-rifkin/cpptrace/issues/164
- Added configuration options for the libdwarf back-end that can be used to lower memory usage on memory-constrained
systems https://github.com/jeremy-rifkin/cpptrace/issues/193
- Added `cpptrace::nullable<T>::null_value`
- Made `cpptrace::nullable<T>` member functions conditionally `constexpr` where possible
Fixed:
- Fixed handling of `SymInitialize` when other code has already called `SymInitialize`. `SymInitialize` must only be
called once per handle and cpptrace now attempts to duplicate the current process handle to avoid conflicts.
https://github.com/jeremy-rifkin/cpptrace/issues/204
- Fixed a couple of locking edge cases surrounding dbghelp functions
- Fixed improper deallocation of `dwarf_errmsg` in the libdwarf back-end
Breaking changes:
- `cpptrace::get_snippet` previously included a newline at the end but it now does not. This also affects the behavior
of trace formatting with snippets enabled.
Other:
- Significantly improved memory usage and performance of the libdwarf back-end
- Improved implementation and organization of internal utility types, such as `optional` and `Result`
- Improved trace printing and formatting implementation
- Added unit tests for library internal utilities
- Added logic to the cxxabi demangler to ensure external names begin with `_Z` or `__Z` before attempting to demangle
- Added various internal tools and abstractions to improve maintainability and clarity
- Various internal improvements for robustness
- Added a small handful of utility tool programs that are useful for continued development, maintenance, and debugging
- Improved library CI setup
- Marked the `CPPTRACE_BUILD_BENCHMARK` option as advanced
# v0.7.5
Fixed:
- Fixed missing `<typeinfo>` include https://github.com/jeremy-rifkin/cpptrace/pull/202
- Added `__cdecl` to a terminate handler to appease MSVC under some configurations https://github.com/jeremy-rifkin/cpptrace/issues/197
- Set C++ standard for cmake support checks https://github.com/jeremy-rifkin/cpptrace/issues/200
- Changed hyphens to underscores for cmake component names due to cpack issue https://github.com/jeremy-rifkin/cpptrace/issues/203
# v0.7.4
Added:
- Added `<cpptrace/version.hpp>` header with version macros
Fixes:
- Bumped libdwarf to 0.11.0 which fixes a number of dwarf 5 debug fission issues
Other:
- Various improvements to internal testing setup
# v0.7.3
Fixed:
- Fixed missing include affecting macos https://github.com/jeremy-rifkin/cpptrace/pull/183
- Fixed issue with cmake not using the ccache program found by `find_program` https://github.com/jeremy-rifkin/cpptrace/pull/184
- Fixed missing include and warnings affecting mingw https://github.com/jeremy-rifkin/cpptrace/pull/186
- Fixed issue with identifying inlined call frames when the `DW_TAG_inlined_subroutine` is under a `DW_TAG_lexical_block`
- Fixed a typo in the README
- Improved unittest support on various configurations
- Improved unittest robustness under LTO
- Fixed bug signal_demo in the event `fork()` fails
Added:
- Added color overload for `stacktrace_frame::to_string`
- Added CMake `export()` definition for cpptrace as well as a definition for libdwarf which currently doesn't provide one
Changed:
- Updated documentation surrounding the signal safe API
# v0.7.2
Changes:
- Better support for older CMake with using `FetchContent_Declare` from a URL https://github.com/jeremy-rifkin/cpptrace/pull/176
- Better portability for page size detection https://github.com/jeremy-rifkin/cpptrace/pull/177
- Improved compile times https://github.com/jeremy-rifkin/cpptrace/pull/172
- Split up `cpptrace.hpp` into finer-grained headers for lower compile time impact
- Some minor readme restructuring
# v0.7.1
Added
- Better support for finding libunwind on macos https://github.com/jeremy-rifkin/cpptrace/pull/162
- Support for libbacktrace under mingw https://github.com/jeremy-rifkin/cpptrace/pull/166
Fixed
- Computation of object address for safe object frames https://github.com/jeremy-rifkin/cpptrace/issues/169
- Nested microfmt in cpptrace's namespace due to an ODR problem with libassert https://github.com/jeremy-rifkin/libassert/issues/103
- Compilation on iOS https://github.com/jeremy-rifkin/cpptrace/pull/167
- Compilation on old MSVC https://github.com/jeremy-rifkin/cpptrace/pull/165
- Dbghelp use on 32 bit https://github.com/jeremy-rifkin/cpptrace/issues/170
- Warning in brand new cmake due to `FetchContent_Populate` being deprecated https://github.com/jeremy-rifkin/cpptrace/issues/171
Other changes
- Bumped the buffer size for execinfo and CaptureStackBackTrace to 400 frames
- Switched to execinfo.h for unwinding on clang/apple clang on macos due to `_Unwind` not working with `-fno-exceptions` https://github.com/jeremy-rifkin/cpptrace/issues/161
# v0.7.0
Added
- Added `cpptrace::from_current_exception()` and associated exception handler macros to allow tracing of all exceptions,
even without cpptrace traced exception objects.
Fixes:
- Fixed issue with using `resolve_safe_object_frame` on `safe_object_frame`s with empty paths
- Fixed handling of dwarf 4 rangelist base addresses when a `DW_AT_low_pc` is not present
- Fixed use of `-g` with MSVC
Other changes:
- Bazel is now supported on linux (https://github.com/jeremy-rifkin/cpptrace/pull/153)
- More work on testing
- Some internal refactoring
# v0.6.3
Added:
- Added a flag to disable inclusion of `<format>` by cpptrace.hpp and the definition of formatter specializations
Fixes:
- Fixed use after free during cleanup of split dwarf information https://github.com/jeremy-rifkin/cpptrace/issues/141
- Fixed an issue with TCO by clang on arm interfering with unwinding skip counts for internal methods
- Fixed issue with incorrect object addresses being reported on macos when debug maps are used
- Fixed issue with handling of split dwarf emitted by clang under dwarf4 mode
Other changes:
- Added note about signal-safe tracing requiring `_dl_find_object` to documentation and fixed errors in the signal-safe
tracing docs
- Added more configurations to unittest ci setup
- Optimized unittest ci matrix setup
- Added options for zstd and libdwarf sources if FetchContent is being used to bring the dependencies in
- Optimized includes in cpptrace.hpp
# v0.6.2
Fixes:
- Fix an issue with unwinding to collect stack traces during exception creation on arm https://github.com/jeremy-rifkin/cpptrace/issues/134
- Fix issue where `dladdr1` wasn't being used even when detected
Robustness:
- Setup more robust unit tests and added them to CI
# v0.6.1
Fixes:
- Fix for detection of `dladdr1` and `_dl_find_object` support
# v0.6.0
New:
- Added a `cpptrace::system_error` utility
- Added support for musl https://github.com/jeremy-rifkin/cpptrace/issues/128
- Added support for split dwarf / debug fission
Fixes:
- Fixed address formatting in stack traces
- Fixed frame pointer calculation for signal frames from libunwind https://github.com/jeremy-rifkin/cpptrace/issues/123
- Fixed dwarf_ranges handling of lowpc == pc causing erroneous symbol resolution
- Fixed implementation of the exception helper system/reference implementation's `lazy_trace_holder`
# v0.5.4
Fixes:
- Fixed bug with resolving object information when `dladdr` is used and an unexpected `argv[0]` is provided to the
binary.
# v0.5.3
Fixes:
- Fixed bug with formatting of hex values on MSVC
- Fixed error handling for libbacktrace back-end when debug info is not present
- Fixed bug with cmake resolution of zstd when no zstd cmake config file is installed
Other changes:
- Added error handling for an edge case in the signal tracing demo
- Updated conan recipe to allow libunwind to be chosen
- Improved msvc support in internal formatting system
- Bumped libdwarf to 0.9.2
# v0.5.2
Fixes:
- Fixed bug with resolution of inlined calls
Other changes:
- Improved internal string formatting
- Improved internal error handling
# v0.5.1
Fixes:
- Fix MSVC warning treated as error for 32-bit windows
- Fix MSVC issue with min/max macros
- Fix potential null dereference issue identified by eyalgolan1337
# v0.5.0
New:
- Traces with source code snippets with `cpptrace::stacktrace::print_with_snippets`
- Added `cpptrace::get_snippet` utility
- Added `cpptrace::can_signal_safe_unwind` utility
- Added `stacktrace_frame::get_object_info`
Changes:
- The library is now compiled with position-independent code by default
Fixes:
- Fixed issue with `_dl_find_object` implementation
Misc:
- Various refactoring, cleanup, and improvements
# v0.4.1
Changes:
- Renamed `stacktrace_frame.address` -> `stacktrace_frame.raw_address`
- Added `stacktrace_frame.object_address`
- Fixed segfault due to an edge case with dwarf file table indices
- For the libdwarf back-end: At least show object frame information if resolution fails
- Extremely small performance improvements
- Small documentation updates
- Small fix for conan
- Updated cmake to not FetchContent zstd when using CPPTRACE_USE_EXTERNAL_LIBDWARF
- CI improvements
- Test the default configuration first before doing the exhaustive and slow matrix of all configurations.
- Cleanup of duplicated prerequisite installation code
- Cleanup of built and test python scripts
# v0.4.0
What's new:
- Cpptrace now has a C API! 🎉
- Cpptrace is now able to parse macOS debug maps and resolve stack traces without dSYM files
Most notable improvements:
- Updated cpptrace exception objects to generate traces at the callsite for improved consistency with trace output. As
part of this cpptrace exception objects have had their constructors updated.
- Improved dwarf back-end robustness
- Fallback to the compilation-unit cache or walking compilation-units if aranges lookup fails
- Eliminated reliance on a CMake-generated export header
- Added a configuration to control resolution of inlined function calls
- Made architecture selection in Mach-O universal binaries
Other improvements:
- Improved documentation for installation and usage
- Generally improved README content and organization
- Fixed an MSVC workaround producing dozens of warnings
- Better handle compiler color diagnostic arguments if compiler families differ
- Improvements for handling libdwarf's header placement
- Fixed issue with libunwind resolution
- `-Werror` is now used in CI
- More library configurations are now tested in CI
- Updated to libdwarf 9
- Updated the library's CMake to acquire zstd through FetchContent
- Fixed minor issue with stacktrace printing always trying to enable virtual terminal processing, even when not actually
printing to the terminal.
# v0.3.1
Tiny patch:
- Fix `CPPTRACE_EXPORT` annotations
- Add workaround for [msvc bug][msvc bug] affecting msvc 19.38.
[msvc bug]: https://developercommunity.visualstudio.com/t/MSVC-1938331290-preview-fails-to-comp/10505565
# v0.3.0
Interface Changes:
- Overhauled the API for traced `cpptrace::exception` objects
- Added `cpptrace::isatty` utility
- Added specialized `std::terminate` handler and `cpptrace::register_terminate_handler` utility
- Added `cpptrace::frame_ptr` as an alias for the appropriate type capable of representing an instruction pointer
- Added signal-safe tracing support and a guide for [how to trace safely](signal-safe-tracing.md)
- Added `cpptrace::nullable<T>` utility for better indicating when line / column information is not present
- Added `CPPTRACE_FORCE_NO_INLINE` utility macro to cpptrace.hpp
- Added `CPPTRACE_WRAP` and `CPPTRACE_WRAP_BLOCK` utilities to catch non-`cpptrace::exception`s and rethrow wrapped in a
traced exception.
- Updated `cpptrace::stacktrace::to_string` to take a `bool color` parameter
- Eliminated uses of `std::uint_least32_t` in favor of other types
- Updated `object_frame` data member names
Other changes:
- Added object resolution with `_dl_find_object` which is much faster than `dladdr`
- Added column support for dwarf
- Added inlined call resolution
- Added `cpptrace::stacktrace_frame::is_inline`
- Added libunwind as a back-end
- Unbundled libdwarf
- Increased hard max frame count, used by some back-end requiring fixed buffers, from 100 to 200
- Improved libgcc unwind backend
- Improved trace output when information is missing
- Added a lookup table for faster dwarf line information lookup
News:
- The library is now on conan and vcpkg
Minor changes:
- Assorted bug fixes
- Various code quality improvements
- CI improvements
- Documentation improvements
- CMake improvements
- Internal refactoring
# v0.2.1
Patches:
- Fixed uintptr_t implicit conversion issue for msvc
- Better handling for PIC and static linkage in CMake
- Added gcc 5 support
- Various warning fixes
- Added stackwalk64 support for 32-bit x86 mingw/clang and architecture detection
- Added check for stackwalk64 support and CaptureStackBacktrace as a fallback
- Various cmake cleanup and changes to use cpptrace through package managers
- Added sonarlint and implemented some sonarlint fixes
# v0.2.0
Key changes:
- Added libdwarf as a back-end so cpptrace doesn't have to rely on addr2line or libbacktrace
- Overhauled library's public-facing interface to make the library more useful
- Added `raw_trace` interface
- Added `object_trace` interface
- Added `stacktrace` interface
- Updated `generate_trace` to return a `stacktrace` rather than a vector of frames
- Added `generate_trace` counterparts for raw and object traces
- Added `generate_trace` overloads with max_depth
- Added interface for internal demangling utility
- Added cache mode configuration
- Added option to absorb internal trace exceptions (by default it absorbs)
- Added `cpptrace::exception`, which automatically generates and stores a stacktrace when thrown
- Added `exception_with_message`
- Added traced analogs for stdexcept errors: `logic_error`, `domain_error`, `invalid_argument`, `length_error`,
`out_of_range`, `runtime_error`, `range_error`, `overflow_error`, and `underflow_error`.
Other changes:
- Bundled libdwarf with cpptrace so the library can essentially be self-contained and not have to rely on libraries that
might not already be on a system
- Added StackWalk64 as an unwinding back-end on windows
- Added system for multiple symbol back-ends to be used, mainly for more complete stack traces on mingw
- Fixed sporadic line number reporting errors due to not adjusting the program counter from the unwinder
- Improved addr2line/atos invocation back-end on macos
- Lots of error handling improvements
- Performance improvements
- Updated default back-ends for most systems
- Removed full tracing backends
- Cleaned up library cmake
- Lots of internal cleanup and refactoring
- Improved library usage instructions in README
# v0.1.1
Fixed:
- Handle errors when object files don't exist or can't be opened for reading
- Handle paths with spaces when using addr2line on windows
# v0.1
Initial release of the library 🎉

View File

@ -0,0 +1,45 @@
# Contributing
Welcome, thank you for your interest in the project!
## Getting started
Contributions are always welcome. If you have not already, consider joining the community discord
(linked in the README). There is discussion about library development there as well as a development
roadmap. Github issues are also a good place to start.
I'm happy to merge fixes, improvements, and features as well as help with getting pull requests
(PRs) over the finish line. That being said, I can't merge stylistic changes,
premature-optimizations, or micro-optimizations.
When contributing, please try to match the current code style in the codebase. Style doesn't matter
too much ultimately but consistency within a codebase is important.
Please base changes against the `dev` branch, which is used for development.
## Code organization
The library's public interface is defined in headers in `include/`. Declarations for the public interface have
definitions in .cpp files in the top-level of `src/`. Implementation for various actions such as unwinding, demangling,
symbol resolution, etc. are put in various sub-directories of `src/`.
## Local development
The easiest way to get started with local development is running `make debug` (which automatically configures cmake and
builds). Note: This requires ninja at the moment.
For more control over how the library is built you can manually build with cmake:
`cmake ..` in a `build/` folder along with any cmake configurations you desire. Then run `make -j` or `ninja` or
`msbuild cpptrace.sln`.
Some useful configurations:
- `-DCMAKE_BUILD_TYPE=Debug|Release|RelWithDebInfo`: Build in debug / release / etc.
- `-DBUILD_SHARED_LIBS=On`: Build shared library
- `-DCPPTRACE_SANITIZER_BUILD=On`: Turn on sanitizers
- `-DCPPTRACE_BUILD_TESTING=On`: Build small test and demo program
## Testing
Unfortunately because this library is so platform-dependent the best way to test thoroughly is with
github's CI. This will happen automatically when you open a PR.

18
src/third_party/cpptrace/dist/LICENSE vendored Normal file
View File

@ -0,0 +1,18 @@
The MIT License (MIT)
Copyright (c) 2023-2025 Jeremy Rifkin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1772
src/third_party/cpptrace/dist/README.md vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
# Security Policy
We take security seriously and I'm grateful for reports of security vulnerabilities.
## Reporting a Vulnerability
If the vulnerability can be reported without revealing exploitable specifics, please open an issue.
If the vulnerability can't be reported publicly without leaving an obvious exploit in the public eye please email me
at jeremy@rifkin.dev or reach out to me on [discord](https://discord.gg/7kv5AuCndG).
I will do my best to get back to you within a day.

View File

@ -0,0 +1,250 @@
#ifndef CPPTRACE_BASIC_HPP
#define CPPTRACE_BASIC_HPP
#include <cpptrace/forward.hpp>
#include <limits>
#include <string>
#include <vector>
#include <iosfwd>
#ifdef _WIN32
#define CPPTRACE_EXPORT_ATTR __declspec(dllexport)
#define CPPTRACE_IMPORT_ATTR __declspec(dllimport)
#else
#define CPPTRACE_EXPORT_ATTR __attribute__((visibility("default")))
#define CPPTRACE_IMPORT_ATTR __attribute__((visibility("default")))
#endif
#ifdef CPPTRACE_STATIC_DEFINE
# define CPPTRACE_EXPORT
# define CPPTRACE_NO_EXPORT
#else
# ifndef CPPTRACE_EXPORT
# ifdef cpptrace_lib_EXPORTS
/* We are building this library */
# define CPPTRACE_EXPORT CPPTRACE_EXPORT_ATTR
# else
/* We are using this library */
# define CPPTRACE_EXPORT CPPTRACE_IMPORT_ATTR
# endif
# endif
#endif
#if __cplusplus >= 201703L
#define CONSTEXPR_SINCE_CPP17 constexpr
#else
#define CONSTEXPR_SINCE_CPP17
#endif
#ifdef _MSC_VER
#define CPPTRACE_FORCE_NO_INLINE __declspec(noinline)
#else
#define CPPTRACE_FORCE_NO_INLINE __attribute__((noinline))
#endif
#ifdef _MSC_VER
#pragma warning(push)
// warning C4251: using non-dll-exported type in dll-exported type, firing on std::vector<frame_ptr> and others for some
// reason
// 4275 is the same thing but for base classes
#pragma warning(disable: 4251; disable: 4275)
#endif
CPPTRACE_BEGIN_NAMESPACE
struct CPPTRACE_EXPORT raw_trace {
std::vector<frame_ptr> frames;
static raw_trace current(std::size_t skip = 0);
static raw_trace current(std::size_t skip, std::size_t max_depth);
object_trace resolve_object_trace() const;
stacktrace resolve() const;
void clear();
bool empty() const noexcept;
using iterator = std::vector<frame_ptr>::iterator;
using const_iterator = std::vector<frame_ptr>::const_iterator;
inline iterator begin() noexcept { return frames.begin(); }
inline iterator end() noexcept { return frames.end(); }
inline const_iterator begin() const noexcept { return frames.begin(); }
inline const_iterator end() const noexcept { return frames.end(); }
inline const_iterator cbegin() const noexcept { return frames.cbegin(); }
inline const_iterator cend() const noexcept { return frames.cend(); }
};
struct CPPTRACE_EXPORT object_frame {
frame_ptr raw_address;
frame_ptr object_address;
std::string object_path;
};
struct CPPTRACE_EXPORT object_trace {
std::vector<object_frame> frames;
static object_trace current(std::size_t skip = 0);
static object_trace current(std::size_t skip, std::size_t max_depth);
stacktrace resolve() const;
void clear();
bool empty() const noexcept;
using iterator = std::vector<object_frame>::iterator;
using const_iterator = std::vector<object_frame>::const_iterator;
inline iterator begin() noexcept { return frames.begin(); }
inline iterator end() noexcept { return frames.end(); }
inline const_iterator begin() const noexcept { return frames.begin(); }
inline const_iterator end() const noexcept { return frames.end(); }
inline const_iterator cbegin() const noexcept { return frames.cbegin(); }
inline const_iterator cend() const noexcept { return frames.cend(); }
};
// This represents a nullable integer type
// The max value of the type is used as a sentinel
// This is used over std::optional because the library is C++11 and also std::optional is a bit heavy-duty for this
// use.
template<typename T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
struct nullable {
T raw_value = null_value();
constexpr nullable() noexcept = default;
constexpr nullable(T value) noexcept : raw_value(value) {}
CONSTEXPR_SINCE_CPP17 nullable& operator=(T value) noexcept {
raw_value = value;
return *this;
}
constexpr bool has_value() const noexcept {
return raw_value != null_value();
}
CONSTEXPR_SINCE_CPP17 T& value() noexcept {
return raw_value;
}
constexpr const T& value() const noexcept {
return raw_value;
}
constexpr T value_or(T alternative) const noexcept {
return has_value() ? raw_value : alternative;
}
CONSTEXPR_SINCE_CPP17 void swap(nullable& other) noexcept {
std::swap(raw_value, other.raw_value);
}
CONSTEXPR_SINCE_CPP17 void reset() noexcept {
raw_value = (std::numeric_limits<T>::max)();
}
constexpr bool operator==(const nullable& other) const noexcept {
return raw_value == other.raw_value;
}
constexpr bool operator!=(const nullable& other) const noexcept {
return raw_value != other.raw_value;
}
constexpr static T null_value() noexcept {
return (std::numeric_limits<T>::max)();
}
constexpr static nullable null() noexcept {
return { null_value() };
}
};
struct CPPTRACE_EXPORT stacktrace_frame {
frame_ptr raw_address;
frame_ptr object_address;
nullable<std::uint32_t> line;
nullable<std::uint32_t> column;
std::string filename;
std::string symbol;
bool is_inline;
bool operator==(const stacktrace_frame& other) const {
return raw_address == other.raw_address
&& object_address == other.object_address
&& line == other.line
&& column == other.column
&& filename == other.filename
&& symbol == other.symbol;
}
bool operator!=(const stacktrace_frame& other) const {
return !operator==(other);
}
object_frame get_object_info() const;
std::string to_string() const;
std::string to_string(bool color) const;
friend CPPTRACE_EXPORT std::ostream& operator<<(std::ostream& stream, const stacktrace_frame& frame);
};
struct CPPTRACE_EXPORT stacktrace {
std::vector<stacktrace_frame> frames;
static stacktrace current(std::size_t skip = 0);
static stacktrace current(std::size_t skip, std::size_t max_depth);
void print() const;
void print(std::ostream& stream) const;
void print(std::ostream& stream, bool color) const;
void print_with_snippets() const;
void print_with_snippets(std::ostream& stream) const;
void print_with_snippets(std::ostream& stream, bool color) const;
void clear();
bool empty() const noexcept;
std::string to_string(bool color = false) const;
friend CPPTRACE_EXPORT std::ostream& operator<<(std::ostream& stream, const stacktrace& trace);
using iterator = std::vector<stacktrace_frame>::iterator;
using const_iterator = std::vector<stacktrace_frame>::const_iterator;
inline iterator begin() noexcept { return frames.begin(); }
inline iterator end() noexcept { return frames.end(); }
inline const_iterator begin() const noexcept { return frames.begin(); }
inline const_iterator end() const noexcept { return frames.end(); }
inline const_iterator cbegin() const noexcept { return frames.cbegin(); }
inline const_iterator cend() const noexcept { return frames.cend(); }
private:
friend void print_terminate_trace();
};
CPPTRACE_EXPORT raw_trace generate_raw_trace(std::size_t skip = 0);
CPPTRACE_EXPORT raw_trace generate_raw_trace(std::size_t skip, std::size_t max_depth);
CPPTRACE_EXPORT object_trace generate_object_trace(std::size_t skip = 0);
CPPTRACE_EXPORT object_trace generate_object_trace(std::size_t skip, std::size_t max_depth);
CPPTRACE_EXPORT stacktrace generate_trace(std::size_t skip = 0);
CPPTRACE_EXPORT stacktrace generate_trace(std::size_t skip, std::size_t max_depth);
// Path max isn't so simple, so I'm choosing 4096 which seems to encompass what all major OS's expect and should be
// fine in all reasonable cases.
// https://eklitzke.org/path-max-is-tricky
// https://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html
#define CPPTRACE_PATH_MAX 4096
// safe tracing interface
// signal-safe
CPPTRACE_EXPORT std::size_t safe_generate_raw_trace(
frame_ptr* buffer,
std::size_t size,
std::size_t skip = 0
);
// signal-safe
CPPTRACE_EXPORT std::size_t safe_generate_raw_trace(
frame_ptr* buffer,
std::size_t size,
std::size_t skip,
std::size_t max_depth
);
struct CPPTRACE_EXPORT safe_object_frame {
frame_ptr raw_address;
// This ends up being the real object address. It was named at a time when I thought the object base address
// still needed to be added in
frame_ptr address_relative_to_object_start;
char object_path[CPPTRACE_PATH_MAX + 1];
// To be called outside a signal handler. Not signal safe.
object_frame resolve() const;
};
// signal-safe
CPPTRACE_EXPORT void get_safe_object_frame(frame_ptr address, safe_object_frame* out);
CPPTRACE_EXPORT bool can_signal_safe_unwind();
CPPTRACE_EXPORT bool can_get_safe_object_frame();
// JIT API
CPPTRACE_EXPORT void register_jit_object(const char*, std::size_t);
CPPTRACE_EXPORT void unregister_jit_object(const char*);
CPPTRACE_EXPORT void clear_all_jit_objects();
CPPTRACE_END_NAMESPACE
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#endif

View File

@ -0,0 +1,9 @@
#ifndef CPPTRACE_HPP
#define CPPTRACE_HPP
#include <cpptrace/basic.hpp>
#include <cpptrace/utils.hpp>
#include <cpptrace/exceptions.hpp>
#include <cpptrace/io.hpp>
#endif

View File

@ -0,0 +1,203 @@
#ifndef CPPTRACE_EXCEPTIONS_HPP
#define CPPTRACE_EXCEPTIONS_HPP
#include <cpptrace/basic.hpp>
#include <cpptrace/exceptions_macros.hpp>
#include <exception>
#include <system_error>
#ifdef _MSC_VER
#pragma warning(push)
// warning C4251: using non-dll-exported type in dll-exported type, firing on std::vector<frame_ptr> and others for some
// reason
// 4275 is the same thing but for base classes
#pragma warning(disable: 4251; disable: 4275)
#endif
CPPTRACE_BEGIN_NAMESPACE
// tracing exceptions:
namespace detail {
// This is a helper utility, if the library weren't C++11 an std::variant would be used
class CPPTRACE_EXPORT lazy_trace_holder {
bool resolved;
union {
raw_trace trace;
stacktrace resolved_trace;
};
public:
// constructors
lazy_trace_holder() : resolved(false), trace() {}
explicit lazy_trace_holder(raw_trace&& _trace) : resolved(false), trace(std::move(_trace)) {}
explicit lazy_trace_holder(stacktrace&& _resolved_trace) : resolved(true), resolved_trace(std::move(_resolved_trace)) {}
// logistics
lazy_trace_holder(const lazy_trace_holder& other);
lazy_trace_holder(lazy_trace_holder&& other) noexcept;
lazy_trace_holder& operator=(const lazy_trace_holder& other);
lazy_trace_holder& operator=(lazy_trace_holder&& other) noexcept;
~lazy_trace_holder();
// access
const raw_trace& get_raw_trace() const;
stacktrace& get_resolved_trace();
const stacktrace& get_resolved_trace() const;
bool is_resolved() const;
private:
void clear();
};
CPPTRACE_EXPORT raw_trace get_raw_trace_and_absorb(std::size_t skip, std::size_t max_depth);
CPPTRACE_EXPORT raw_trace get_raw_trace_and_absorb(std::size_t skip = 0);
}
// Interface for a traced exception object
class CPPTRACE_EXPORT exception : public std::exception {
public:
const char* what() const noexcept override = 0;
virtual const char* message() const noexcept = 0;
virtual const stacktrace& trace() const noexcept = 0;
};
// Cpptrace traced exception object
// I hate to have to expose anything about implementation detail but the idea here is that
class CPPTRACE_EXPORT lazy_exception : public exception {
mutable detail::lazy_trace_holder trace_holder;
mutable std::string what_string;
public:
explicit lazy_exception(
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) : trace_holder(std::move(trace)) {}
// std::exception
const char* what() const noexcept override;
// cpptrace::exception
const char* message() const noexcept override;
const stacktrace& trace() const noexcept override;
};
class CPPTRACE_EXPORT exception_with_message : public lazy_exception {
mutable std::string user_message;
public:
explicit exception_with_message(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept : lazy_exception(std::move(trace)), user_message(std::move(message_arg)) {}
const char* message() const noexcept override;
};
class CPPTRACE_EXPORT logic_error : public exception_with_message {
public:
explicit logic_error(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT domain_error : public exception_with_message {
public:
explicit domain_error(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT invalid_argument : public exception_with_message {
public:
explicit invalid_argument(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT length_error : public exception_with_message {
public:
explicit length_error(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT out_of_range : public exception_with_message {
public:
explicit out_of_range(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT runtime_error : public exception_with_message {
public:
explicit runtime_error(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT range_error : public exception_with_message {
public:
explicit range_error(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT overflow_error : public exception_with_message {
public:
explicit overflow_error(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT underflow_error : public exception_with_message {
public:
explicit underflow_error(
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: exception_with_message(std::move(message_arg), std::move(trace)) {}
};
class CPPTRACE_EXPORT nested_exception : public lazy_exception {
std::exception_ptr ptr;
mutable std::string message_value;
public:
explicit nested_exception(
const std::exception_ptr& exception_ptr,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept
: lazy_exception(std::move(trace)), ptr(exception_ptr) {}
const char* message() const noexcept override;
std::exception_ptr nested_ptr() const noexcept;
};
class CPPTRACE_EXPORT system_error : public runtime_error {
std::error_code ec;
public:
explicit system_error(
int error_code,
std::string&& message_arg,
raw_trace&& trace = detail::get_raw_trace_and_absorb()
) noexcept;
const std::error_code& code() const noexcept;
};
// [[noreturn]] must come first due to old clang
[[noreturn]] CPPTRACE_EXPORT void rethrow_and_wrap_if_needed(std::size_t skip = 0);
CPPTRACE_END_NAMESPACE
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#endif

View File

@ -0,0 +1,21 @@
#ifndef CPPTRACE_EXCEPTIONS_MACROS_HPP
#define CPPTRACE_EXCEPTIONS_MACROS_HPP
// Exception wrapper utilities
#define CPPTRACE_WRAP_BLOCK(statements) do { \
try { \
statements \
} catch(...) { \
::cpptrace::rethrow_and_wrap_if_needed(); \
} \
} while(0)
#define CPPTRACE_WRAP(expression) [&] () -> decltype((expression)) { \
try { \
return expression; \
} catch(...) { \
::cpptrace::rethrow_and_wrap_if_needed(1); \
} \
} ()
#endif // CPPTRACE_EXCEPTIONS_MACROS_HPP

View File

@ -0,0 +1,91 @@
#ifndef CPPTRACE_FORMATTING_HPP
#define CPPTRACE_FORMATTING_HPP
#include <cpptrace/basic.hpp>
#include <string>
#include <functional>
CPPTRACE_BEGIN_NAMESPACE
class CPPTRACE_EXPORT formatter {
class impl;
// can't be a std::unique_ptr due to msvc awfulness with dllimport/dllexport and https://stackoverflow.com/q/4145605/15675011
impl* pimpl;
public:
formatter();
~formatter();
formatter(formatter&&);
formatter(const formatter&);
formatter& operator=(formatter&&);
formatter& operator=(const formatter&);
formatter& header(std::string);
enum class color_mode {
always,
none,
automatic,
};
formatter& colors(color_mode);
enum class address_mode {
raw,
object,
none,
};
formatter& addresses(address_mode);
enum class path_mode {
// full path is used
full,
// only the file name is used
basename,
};
formatter& paths(path_mode);
formatter& snippets(bool);
formatter& snippet_context(int);
formatter& columns(bool);
enum class symbol_mode {
// full demangled symbol
full,
// symbols are prettified to clean up some especially long template argument lists
pretty,
// template arguments and function parameters are pruned
pruned
};
formatter& symbols(symbol_mode);
formatter& filtered_frame_placeholders(bool);
formatter& filter(std::function<bool(const stacktrace_frame&)>);
formatter& transform(std::function<stacktrace_frame(stacktrace_frame)>);
formatter& break_before_filename(bool do_break = true);
std::string format(const stacktrace_frame&) const;
std::string format(const stacktrace_frame&, bool color) const;
// The last argument is the indent to use for the filename, if break_before_filename is set
std::string format(const stacktrace_frame&, bool color, size_t filename_indent) const;
std::string format(const stacktrace&) const;
std::string format(const stacktrace&, bool color) const;
void print(const stacktrace_frame&) const;
void print(const stacktrace_frame&, bool color) const;
void print(std::ostream&, const stacktrace_frame&) const;
void print(std::ostream&, const stacktrace_frame&, bool color) const;
// The last argument is the indent to use for the filename, if break_before_filename is set
void print(std::ostream&, const stacktrace_frame&, bool color, size_t filename_indent) const;
void print(std::FILE*, const stacktrace_frame&) const;
void print(std::FILE*, const stacktrace_frame&, bool color) const;
// The last argument is the indent to use for the filename, if break_before_filename is set
void print(std::FILE*, const stacktrace_frame&, bool color, size_t filename_indent) const;
void print(const stacktrace&) const;
void print(const stacktrace&, bool color) const;
void print(std::ostream&, const stacktrace&) const;
void print(std::ostream&, const stacktrace&, bool color) const;
void print(std::FILE*, const stacktrace&) const;
void print(std::FILE*, const stacktrace&, bool color) const;
};
CPPTRACE_EXPORT const formatter& get_default_formatter();
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,26 @@
#ifndef CPPTRACE_FORWARD_HPP
#define CPPTRACE_FORWARD_HPP
#include <cstdint>
#define CPPTRACE_BEGIN_NAMESPACE \
namespace cpptrace { \
inline namespace v1 {
#define CPPTRACE_END_NAMESPACE \
} \
}
CPPTRACE_BEGIN_NAMESPACE
// Some type sufficient for an instruction pointer, currently always an alias to std::uintptr_t
using frame_ptr = std::uintptr_t;
struct raw_trace;
struct object_trace;
struct stacktrace;
struct object_frame;
struct stacktrace_frame;
struct safe_object_frame;
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,153 @@
#ifndef CPPTRACE_FROM_CURRENT_HPP
#define CPPTRACE_FROM_CURRENT_HPP
#include <exception>
#include <typeinfo>
#include <utility>
#ifdef _MSC_VER
#include <windows.h>
#endif
#include <cpptrace/basic.hpp>
#include <cpptrace/from_current_macros.hpp>
CPPTRACE_BEGIN_NAMESPACE
CPPTRACE_EXPORT const raw_trace& raw_trace_from_current_exception();
CPPTRACE_EXPORT const stacktrace& from_current_exception();
CPPTRACE_EXPORT const raw_trace& raw_trace_from_current_exception_rethrow();
CPPTRACE_EXPORT const stacktrace& from_current_exception_rethrow();
CPPTRACE_EXPORT bool current_exception_was_rethrown();
CPPTRACE_NORETURN CPPTRACE_EXPORT CPPTRACE_FORCE_NO_INLINE
void rethrow();
CPPTRACE_NORETURN CPPTRACE_EXPORT CPPTRACE_FORCE_NO_INLINE
void rethrow(std::exception_ptr exception);
CPPTRACE_EXPORT void clear_current_exception_traces();
namespace detail {
template<typename>
struct argument;
template<typename R, typename Arg>
struct argument<R(Arg)> {
using type = Arg;
};
template<typename R>
struct argument<R(...)> {
using type = void;
};
#ifdef _MSC_VER
CPPTRACE_EXPORT CPPTRACE_FORCE_NO_INLINE
int maybe_collect_trace(EXCEPTION_POINTERS* exception_ptrs, int filter_result);
CPPTRACE_EXPORT CPPTRACE_FORCE_NO_INLINE
void maybe_collect_trace(EXCEPTION_POINTERS* exception_ptrs, const std::type_info& type_info);
template<typename E>
CPPTRACE_FORCE_NO_INLINE inline int exception_filter(EXCEPTION_POINTERS* exception_ptrs) {
maybe_collect_trace(exception_ptrs, typeid(E));
return EXCEPTION_CONTINUE_SEARCH;
}
class dont_return_from_try_catch_macros {
public:
explicit dont_return_from_try_catch_macros() = default;
};
#else
CPPTRACE_EXPORT CPPTRACE_FORCE_NO_INLINE
void maybe_collect_trace(const std::type_info*, const std::type_info*, void**, unsigned);
template<typename T>
class unwind_interceptor {
public:
static int init;
CPPTRACE_FORCE_NO_INLINE static bool can_catch(
const std::type_info* /* this */,
const std::type_info* throw_type,
void** throw_obj,
unsigned outer
) {
maybe_collect_trace(&typeid(T), throw_type, throw_obj, outer);
return false;
}
};
CPPTRACE_EXPORT void do_prepare_unwind_interceptor(
const std::type_info&,
bool(*)(const std::type_info*, const std::type_info*, void**, unsigned)
);
template<typename T>
inline int prepare_unwind_interceptor() {
do_prepare_unwind_interceptor(typeid(unwind_interceptor<T>), unwind_interceptor<T>::can_catch);
return 1;
}
template<typename T>
int unwind_interceptor<T>::init = prepare_unwind_interceptor<T>();
template<typename F>
using unwind_interceptor_for = unwind_interceptor<typename argument<F>::type>;
inline void nop(int) {}
#endif
}
namespace detail {
template<typename R, typename Arg>
Arg get_callable_argument_helper(R(*) (Arg));
template<typename R, typename F, typename Arg>
Arg get_callable_argument_helper(R(F::*) (Arg));
template<typename R, typename F, typename Arg>
Arg get_callable_argument_helper(R(F::*) (Arg) const);
template<typename R>
void get_callable_argument_helper(R(*) ());
template<typename R, typename F>
void get_callable_argument_helper(R(F::*) ());
template<typename R, typename F>
void get_callable_argument_helper(R(F::*) () const);
template<typename F>
decltype(get_callable_argument_helper(&F::operator())) get_callable_argument_wrapper(F);
template<typename T>
using get_callable_argument = decltype(get_callable_argument_wrapper(std::declval<T>()));
template<typename E, typename F, typename Catch, typename std::enable_if<!std::is_same<E, void>::value, int>::type = 0>
void do_try_catch(F&& f, Catch&& catcher) {
CPPTRACE_TRY {
std::forward<F>(f)();
} CPPTRACE_CATCH(E e) {
std::forward<Catch>(catcher)(std::forward<E>(e));
}
}
template<typename E, typename F, typename Catch, typename std::enable_if<std::is_same<E, void>::value, int>::type = 0>
void do_try_catch(F&& f, Catch&& catcher) {
CPPTRACE_TRY {
std::forward<F>(f)();
} CPPTRACE_CATCH(...) {
std::forward<Catch>(catcher)();
}
}
template<typename F>
void try_catch_impl(F&& f) {
std::forward<F>(f)();
}
// TODO: This could be made more efficient to reduce the number of interceptor levels that do typeid checks
// and possible traces
template<typename F, typename Catch, typename... Catches>
void try_catch_impl(F&& f, Catch&& catcher, Catches&&... catches) {
// match the first catch at the inner-most level... no real way to reverse a pack or extract from the end so
// we have to wrap with a lambda
auto wrapped = [&] () {
using E = get_callable_argument<Catch>;
do_try_catch<E>(std::forward<F>(f), std::forward<Catch>(catcher));
};
try_catch_impl(std::move(wrapped), std::forward<Catches>(catches)...);
}
}
template<typename F, typename... Catches>
void try_catch(F&& f, Catches&&... catches) {
return detail::try_catch_impl(std::forward<F>(f), std::forward<Catches>(catches)...);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,78 @@
#ifndef CPPTRACE_FROM_CURRENT_MACROS_HPP
#define CPPTRACE_FROM_CURRENT_MACROS_HPP
// https://godbolt.org/z/4MsT6KqP1
#ifdef _MSC_VER
#define CPPTRACE_UNREACHABLE() __assume(false)
#else
#define CPPTRACE_UNREACHABLE() __builtin_unreachable()
#endif
// https://godbolt.org/z/7neGPEche
// gcc added support in 4.8 but I'm too lazy to check the minor version
#if defined(__GNUC__) && (__GNUC__ < 5)
#define CPPTRACE_NORETURN __attribute__((noreturn))
#else
#define CPPTRACE_NORETURN [[noreturn]]
#endif
#ifdef _MSC_VER
#if defined(__clang__)
#define CPPTRACE_PUSH_EXTENSION_WARNINGS \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wlanguage-extension-token\"")
#define CPPTRACE_POP_EXTENSION_WARNINGS \
_Pragma("clang diagnostic pop")
#else
#define CPPTRACE_PUSH_EXTENSION_WARNINGS
#define CPPTRACE_POP_EXTENSION_WARNINGS
#endif
#define CPPTRACE_TYPE_FOR(param) \
typename ::cpptrace::detail::argument<void(param)>::type
// this awful double-IILE is due to C2713 "You can't use structured exception handling (__try/__except) and C++
// exception handling (try/catch) in the same function."
#define CPPTRACE_TRY \
try { \
[&]() -> ::cpptrace::detail::dont_return_from_try_catch_macros { \
CPPTRACE_PUSH_EXTENSION_WARNINGS \
__try { \
CPPTRACE_POP_EXTENSION_WARNINGS \
return [&]() -> ::cpptrace::detail::dont_return_from_try_catch_macros {
#define CPPTRACE_CATCH(param) \
return ::cpptrace::detail::dont_return_from_try_catch_macros(); \
}(); \
CPPTRACE_PUSH_EXTENSION_WARNINGS \
} __except(::cpptrace::detail::exception_filter<CPPTRACE_TYPE_FOR(param)>(GetExceptionInformation())) { \
CPPTRACE_POP_EXTENSION_WARNINGS \
CPPTRACE_UNREACHABLE(); \
} \
}(); \
} catch(param)
#define CPPTRACE_SEH_TRY __try
#define CPPTRACE_SEH_EXCEPT(filter) \
__except(::cpptrace::detail::maybe_collect_trace(GetExceptionInformation(), (filter)))
#else
#define CPPTRACE_UNWIND_INTERCEPTOR_FOR(param) \
::cpptrace::detail::unwind_interceptor_for<void(param)>
#define CPPTRACE_TRY \
try { \
try {
#define CPPTRACE_CATCH(param) \
} catch(const CPPTRACE_UNWIND_INTERCEPTOR_FOR(param)&) { \
CPPTRACE_UNREACHABLE(); \
/* force instantiation of the init-er */ \
::cpptrace::detail::nop(CPPTRACE_UNWIND_INTERCEPTOR_FOR(param)::init); \
} \
} catch(param)
#endif
#ifdef CPPTRACE_UNPREFIXED_TRY_CATCH
#define TRY CPPTRACE_TRY
#define CATCH(param) CPPTRACE_CATCH(param)
#ifdef _MSC_VER
#define SEH_TRY CPPTRACE_SEH_TRY
#define SEH_EXCEPT(filter) CPPTRACE_SEH_EXCEPT(filter)
#endif
#endif
#endif // CPPTRACE_FROM_CURRENT_MACROS_HPP

View File

@ -0,0 +1,53 @@
#ifndef CPPTRACE_GDB_JIT_HPP
#define CPPTRACE_GDB_JIT_HPP
#include <cpptrace/basic.hpp>
#include <cstdint>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
// https://sourceware.org/gdb/current/onlinedocs/gdb.html/JIT-Interface.html
extern "C" {
typedef enum
{
JIT_NOACTION = 0,
JIT_REGISTER_FN,
JIT_UNREGISTER_FN
} jit_actions_t;
struct jit_code_entry
{
struct jit_code_entry *next_entry;
struct jit_code_entry *prev_entry;
const char *symfile_addr;
uint64_t symfile_size;
};
struct jit_descriptor
{
uint32_t version;
/* This type should be jit_actions_t, but we use uint32_t
to be explicit about the bitwidth. */
uint32_t action_flag;
struct jit_code_entry *relevant_entry;
struct jit_code_entry *first_entry;
};
extern struct jit_descriptor __jit_debug_descriptor;
}
}
namespace experimental {
inline void register_jit_objects_from_gdb_jit_interface() {
clear_all_jit_objects();
detail::jit_code_entry* entry = detail::__jit_debug_descriptor.first_entry;
while(entry) {
register_jit_object(entry->symfile_addr, entry->symfile_size);
entry = entry->next_entry;
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,52 @@
#ifndef CPPTRACE_IO_HPP
#define CPPTRACE_IO_HPP
#include <cpptrace/basic.hpp>
#include <iosfwd>
#ifndef CPPTRACE_NO_STD_FORMAT
#if __cplusplus >= 202002L
#ifdef __has_include
#if __has_include(<format>)
#define CPPTRACE_STD_FORMAT
#include <format>
#endif
#endif
#endif
#endif
#ifdef _MSC_VER
#pragma warning(push)
// warning C4251: using non-dll-exported type in dll-exported type, firing on std::vector<frame_ptr> and others for some
// reason
// 4275 is the same thing but for base classes
#pragma warning(disable: 4251; disable: 4275)
#endif
CPPTRACE_BEGIN_NAMESPACE
CPPTRACE_EXPORT std::ostream& operator<<(std::ostream& stream, const stacktrace_frame& frame);
CPPTRACE_EXPORT std::ostream& operator<<(std::ostream& stream, const stacktrace& trace);
CPPTRACE_END_NAMESPACE
#if defined(CPPTRACE_STD_FORMAT) && defined(__cpp_lib_format)
template <>
struct std::formatter<cpptrace::stacktrace_frame> : std::formatter<std::string> {
auto format(cpptrace::stacktrace_frame frame, format_context& ctx) const {
return formatter<string>::format(frame.to_string(), ctx);
}
};
template <>
struct std::formatter<cpptrace::stacktrace> : std::formatter<std::string> {
auto format(cpptrace::stacktrace trace, format_context& ctx) const {
return formatter<string>::format(trace.to_string(), ctx);
}
};
#endif
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#endif

View File

@ -0,0 +1,81 @@
#ifndef CPPTRACE_UTILS_HPP
#define CPPTRACE_UTILS_HPP
#include <cpptrace/basic.hpp>
#include <functional>
#ifdef _MSC_VER
#pragma warning(push)
// warning C4251: using non-dll-exported type in dll-exported type, firing on std::vector<frame_ptr> and others for some
// reason
// 4275 is the same thing but for base classes
#pragma warning(disable: 4251; disable: 4275)
#endif
CPPTRACE_BEGIN_NAMESPACE
CPPTRACE_EXPORT std::string demangle(const std::string& name);
CPPTRACE_EXPORT std::string basename(const std::string& path);
CPPTRACE_EXPORT std::string prettify_symbol(std::string symbol);
CPPTRACE_EXPORT std::string prune_symbol(const std::string& symbol);
CPPTRACE_EXPORT std::string get_snippet(
const std::string& path,
std::size_t line,
std::size_t context_size,
bool color = false
);
CPPTRACE_EXPORT std::string get_snippet(
const std::string& path,
std::size_t line,
nullable<std::uint32_t> column,
std::size_t context_size,
bool color = false
);
CPPTRACE_EXPORT bool isatty(int fd);
CPPTRACE_EXPORT extern const int stdin_fileno;
CPPTRACE_EXPORT extern const int stderr_fileno;
CPPTRACE_EXPORT extern const int stdout_fileno;
CPPTRACE_EXPORT void register_terminate_handler();
// options:
CPPTRACE_EXPORT void absorb_trace_exceptions(bool absorb);
CPPTRACE_EXPORT void enable_inlined_call_resolution(bool enable);
enum class log_level { debug, info, warning, error };
CPPTRACE_EXPORT void set_log_level(log_level level);
CPPTRACE_EXPORT void set_log_callback(std::function<void(log_level, const char*)>);
CPPTRACE_EXPORT void use_default_stderr_logger();
CPPTRACE_EXPORT const char* to_string(log_level level);
enum class cache_mode {
// Only minimal lookup tables
prioritize_memory = 0,
// Build lookup tables but don't keep them around between trace calls
hybrid = 1,
// Build lookup tables as needed
prioritize_speed = 2
};
namespace experimental {
CPPTRACE_EXPORT void set_cache_mode(cache_mode mode);
}
// dwarf options
namespace experimental {
CPPTRACE_EXPORT void set_dwarf_resolver_line_table_cache_size(nullable<std::size_t> max_entries);
CPPTRACE_EXPORT void set_dwarf_resolver_disable_aranges(bool disable);
}
// dbghelp
#ifdef _WIN32
CPPTRACE_EXPORT void load_symbols_for_file(const std::string& filename);
#endif
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,164 @@
#ifndef CTRACE_H
#define CTRACE_H
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#ifdef _WIN32
#define CPPTRACE_EXPORT_ATTR __declspec(dllexport)
#define CPPTRACE_IMPORT_ATTR __declspec(dllimport)
#else
#define CPPTRACE_EXPORT_ATTR __attribute__((visibility("default")))
#define CPPTRACE_IMPORT_ATTR __attribute__((visibility("default")))
#endif
#ifdef CPPTRACE_STATIC_DEFINE
# define CPPTRACE_EXPORT
# define CPPTRACE_NO_EXPORT
#else
# ifndef CPPTRACE_EXPORT
# ifdef cpptrace_lib_EXPORTS
/* We are building this library */
# define CPPTRACE_EXPORT CPPTRACE_EXPORT_ATTR
# else
/* We are using this library */
# define CPPTRACE_EXPORT CPPTRACE_IMPORT_ATTR
# endif
# endif
#endif
#if defined(__cplusplus)
#define CTRACE_BEGIN_DEFINITIONS extern "C" {
#define CTRACE_END_DEFINITIONS }
#else
#define CTRACE_BEGIN_DEFINITIONS
#define CTRACE_END_DEFINITIONS
#endif
#ifdef _MSC_VER
#define CTRACE_FORCE_NO_INLINE __declspec(noinline)
#else
#define CTRACE_FORCE_NO_INLINE __attribute__((noinline))
#endif
#ifdef _MSC_VER
#define CTRACE_FORCE_INLINE __forceinline
#elif defined(__clang__) || defined(__GNUC__)
#define CTRACE_FORCE_INLINE __attribute__((always_inline)) inline
#else
#define CTRACE_FORCE_INLINE inline
#endif
/* See `CPPTRACE_PATH_MAX` for more info. */
#define CTRACE_PATH_MAX 4096
CTRACE_BEGIN_DEFINITIONS
typedef struct ctrace_raw_trace ctrace_raw_trace;
typedef struct ctrace_object_trace ctrace_object_trace;
typedef struct ctrace_stacktrace ctrace_stacktrace;
/* Represents a boolean value, ensures a consistent ABI. */
typedef int8_t ctrace_bool;
/* A type that can represent a pointer, alias for `uintptr_t`. */
typedef uintptr_t ctrace_frame_ptr;
typedef struct ctrace_object_frame ctrace_object_frame;
typedef struct ctrace_stacktrace_frame ctrace_stacktrace_frame;
typedef struct ctrace_safe_object_frame ctrace_safe_object_frame;
/* Type-safe null-terminated string wrapper */
typedef struct {
const char* data;
} ctrace_owning_string;
struct ctrace_object_frame {
ctrace_frame_ptr raw_address;
ctrace_frame_ptr obj_address;
const char* obj_path;
};
struct ctrace_stacktrace_frame {
ctrace_frame_ptr raw_address;
ctrace_frame_ptr object_address;
uint32_t line;
uint32_t column;
const char* filename;
const char* symbol;
ctrace_bool is_inline;
};
struct ctrace_safe_object_frame {
ctrace_frame_ptr raw_address;
ctrace_frame_ptr relative_obj_address;
char object_path[CTRACE_PATH_MAX + 1];
};
struct ctrace_raw_trace {
ctrace_frame_ptr* frames;
size_t count;
};
struct ctrace_object_trace {
ctrace_object_frame* frames;
size_t count;
};
struct ctrace_stacktrace {
ctrace_stacktrace_frame* frames;
size_t count;
};
/* ctrace::string: */
CPPTRACE_EXPORT ctrace_owning_string ctrace_generate_owning_string(const char* raw_string);
CPPTRACE_EXPORT void ctrace_free_owning_string(ctrace_owning_string* string);
/* ctrace::generation: */
CPPTRACE_EXPORT ctrace_raw_trace ctrace_generate_raw_trace(size_t skip, size_t max_depth);
CPPTRACE_EXPORT ctrace_object_trace ctrace_generate_object_trace(size_t skip, size_t max_depth);
CPPTRACE_EXPORT ctrace_stacktrace ctrace_generate_trace(size_t skip, size_t max_depth);
/* ctrace::freeing: */
CPPTRACE_EXPORT void ctrace_free_raw_trace(ctrace_raw_trace* trace);
CPPTRACE_EXPORT void ctrace_free_object_trace(ctrace_object_trace* trace);
CPPTRACE_EXPORT void ctrace_free_stacktrace(ctrace_stacktrace* trace);
/* ctrace::resolve: */
CPPTRACE_EXPORT ctrace_stacktrace ctrace_resolve_raw_trace(const ctrace_raw_trace* trace);
CPPTRACE_EXPORT ctrace_object_trace ctrace_resolve_raw_trace_to_object_trace(const ctrace_raw_trace* trace);
CPPTRACE_EXPORT ctrace_stacktrace ctrace_resolve_object_trace(const ctrace_object_trace* trace);
/* ctrace::safe: */
CPPTRACE_EXPORT size_t ctrace_safe_generate_raw_trace(ctrace_frame_ptr* buffer, size_t size, size_t skip, size_t max_depth);
CPPTRACE_EXPORT void ctrace_get_safe_object_frame(ctrace_frame_ptr address, ctrace_safe_object_frame* out);
CPPTRACE_EXPORT ctrace_bool ctrace_can_signal_safe_unwind(void);
CPPTRACE_EXPORT ctrace_bool ctrace_can_get_safe_object_frame(void);
/* ctrace::io: */
CPPTRACE_EXPORT ctrace_owning_string ctrace_stacktrace_to_string(const ctrace_stacktrace* trace, ctrace_bool use_color);
CPPTRACE_EXPORT void ctrace_print_stacktrace(const ctrace_stacktrace* trace, FILE* to, ctrace_bool use_color);
/* ctrace::utility: */
CPPTRACE_EXPORT ctrace_owning_string ctrace_demangle(const char* mangled);
CPPTRACE_EXPORT int ctrace_stdin_fileno(void);
CPPTRACE_EXPORT int ctrace_stderr_fileno(void);
CPPTRACE_EXPORT int ctrace_stdout_fileno(void);
CPPTRACE_EXPORT ctrace_bool ctrace_isatty(int fd);
CPPTRACE_EXPORT ctrace_object_frame ctrace_get_object_info(const ctrace_stacktrace_frame* frame);
/* ctrace::config: */
typedef enum {
/* Only minimal lookup tables */
ctrace_prioritize_memory = 0,
/* Build lookup tables but don't keep them around between trace calls */
ctrace_hybrid = 1,
/* Build lookup tables as needed */
ctrace_prioritize_speed = 2
} ctrace_cache_mode;
CPPTRACE_EXPORT void ctrace_set_cache_mode(ctrace_cache_mode mode);
CPPTRACE_EXPORT void ctrace_enable_inlined_call_resolution(ctrace_bool enable);
CTRACE_END_DEFINITIONS
#endif

View File

@ -0,0 +1,488 @@
#include "binary/elf.hpp"
#include "utils/error.hpp"
#include "utils/io/base_file.hpp"
#include "utils/io/memory_file_view.hpp"
#include "utils/optional.hpp"
#include "utils/io/file.hpp"
#include "utils/string_view.hpp"
#if IS_LINUX
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <type_traits>
#include <unordered_map>
#include <elf.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
elf::elf(
std::unique_ptr<base_file> file,
bool is_little_endian,
bool is_64
) : file(std::move(file)), is_little_endian(is_little_endian), is_64(is_64) {}
Result<elf, internal_error> elf::open(std::unique_ptr<base_file> file) {
// Initial checks/metadata
auto magic = file->read<std::array<char, 4>>(0);
if(magic.is_error()) {
return std::move(magic).unwrap_error();
}
if(magic.unwrap_value() != (std::array<char, 4>{0x7F, 'E', 'L', 'F'})) {
return internal_error("File is not ELF {}", file->path());
}
auto ei_class = file->read<std::uint8_t>(4);
if(ei_class.is_error()) {
return std::move(ei_class).unwrap_error();
}
bool is_64 = ei_class.unwrap_value() == 2;
auto ei_data = file->read<std::uint8_t>(5);
if(ei_data.is_error()) {
return std::move(ei_data).unwrap_error();
}
bool is_little_endian = ei_data.unwrap_value() == 1;
auto ei_version = file->read<std::uint8_t>(6);
if(ei_version.is_error()) {
return std::move(ei_version).unwrap_error();
}
if(ei_version.unwrap_value() != 1) {
return internal_error("Unexpected ELF version {}", file->path());
}
return elf(std::move(file), is_little_endian, is_64);
}
Result<elf, internal_error> elf::open(cstring_view object_path) {
auto file_res = file::open(object_path);
if(!file_res) {
return internal_error("Unable to read object file {}", object_path);
}
auto& file = file_res.unwrap_value();
return open(make_unique(std::move(file)));
}
Result<elf, internal_error> elf::open(cbspan object) {
return open(make_unique<memory_file_view>(object));
}
Result<std::uintptr_t, internal_error> elf::get_module_image_base() {
// get image base
if(is_64) {
return get_module_image_base_impl<64>();
} else {
return get_module_image_base_impl<32>();
}
}
template<std::size_t Bits>
Result<std::uintptr_t, internal_error> elf::get_module_image_base_impl() {
static_assert(Bits == 32 || Bits == 64, "Unexpected Bits argument");
using PHeader = typename std::conditional<Bits == 32, Elf32_Phdr, Elf64_Phdr>::type;
auto header = get_header_info();
if(header.is_error()) {
return std::move(header).unwrap_error();
}
const auto& header_info = header.unwrap_value();
// PT_PHDR will occur at most once
// Should be somewhat reliable https://stackoverflow.com/q/61568612/15675011
// It should occur at the beginning but may as well loop just in case
for(unsigned i = 0; i < header_info.e_phnum; i++) {
auto loaded_ph = file->read<PHeader>(header_info.e_phoff + header_info.e_phentsize * i);
if(loaded_ph.is_error()) {
return std::move(loaded_ph).unwrap_error();
}
const PHeader& program_header = loaded_ph.unwrap_value();
if(byteswap_if_needed(program_header.p_type) == PT_PHDR) {
return byteswap_if_needed(program_header.p_vaddr) -
byteswap_if_needed(program_header.p_offset);
}
}
// Apparently some objects like shared objects can end up missing this header. 0 as a base seems correct.
return 0;
}
optional<std::string> elf::lookup_symbol(frame_ptr pc) {
if(auto symtab = get_symtab()) {
if(auto symbol = lookup_symbol(pc, symtab.unwrap_value())) {
return symbol;
}
}
if(auto dynamic_symtab = get_dynamic_symtab()) {
if(auto symbol = lookup_symbol(pc, dynamic_symtab.unwrap_value())) {
return symbol;
}
}
return nullopt;
}
optional<std::string> elf::lookup_symbol(frame_ptr pc, const optional<symtab_info>& maybe_symtab) {
if(!maybe_symtab) {
return nullopt;
}
auto& symtab = maybe_symtab.unwrap();
if(symtab.strtab_link == SHN_UNDEF) {
return nullopt;
}
auto strtab_ = get_strtab(symtab.strtab_link);
if(strtab_.is_error()) {
return nullopt;
}
auto& strtab = strtab_.unwrap_value();
auto it = first_less_than_or_equal(
symtab.entries.begin(),
symtab.entries.end(),
pc,
[] (frame_ptr pc, const symtab_entry& entry) {
return pc < entry.st_value;
}
);
if(it == symtab.entries.end()) {
return nullopt;
}
if(pc <= it->st_value + it->st_size) {
return strtab.data() + it->st_name;
}
return nullopt;
}
Result<std::vector<elf::pc_range>, internal_error> elf::get_pc_ranges() {
std::vector<pc_range> vec;
auto header_info_ = get_header_info();
if(header_info_.is_error()) {
return header_info_.unwrap_error();
}
auto& header_info = header_info_.unwrap_value();
auto strtab_ = get_strtab(header_info.e_shstrndx);
if(strtab_.is_error()) {
return strtab_.unwrap_error();
}
auto& strtab = strtab_.unwrap_value();
auto sections_res = get_sections();
if(!sections_res) {
return sections_res.unwrap_error();
}
const auto& sections = sections_res.unwrap_value();
for(const auto& section : sections) {
if(string_view(strtab.data() + section.sh_name) == ".text") {
vec.push_back(
pc_range{to<frame_ptr>(section.sh_addr), to<frame_ptr>(section.sh_addr + section.sh_size)}
);
}
}
return vec;
}
Result<optional<std::vector<elf::symbol_entry>>, internal_error> elf::get_symtab_entries() {
return resolve_symtab_entries(get_symtab());
}
Result<optional<std::vector<elf::symbol_entry>>, internal_error> elf::get_dynamic_symtab_entries() {
return resolve_symtab_entries(get_dynamic_symtab());
}
Result<optional<std::vector<elf::symbol_entry>>, internal_error> elf::resolve_symtab_entries(
const Result<const optional<elf::symtab_info> &, internal_error>& symtab
) {
if(!symtab) {
return symtab.unwrap_error();
}
if(!symtab.unwrap_value()) {
return nullopt;
}
const auto& info = symtab.unwrap_value().unwrap();
optional<const std::vector<char>&> strtab;
if(info.strtab_link != SHN_UNDEF) {
auto strtab_ = get_strtab(info.strtab_link);
if(strtab_.is_error()) {
return strtab_.unwrap_error();
}
strtab = strtab_.unwrap_value();
}
std::vector<symbol_entry> res;
for(const auto& entry : info.entries) {
res.push_back({
strtab.has_value() ? strtab.unwrap().data() + entry.st_name : "<strtab error>",
entry.st_shndx,
entry.st_value,
entry.st_size
});
}
return res;
}
template<typename T, typename std::enable_if<std::is_integral<T>::value, int>::type>
T elf::byteswap_if_needed(T value) {
if(detail::is_little_endian() == is_little_endian) {
return value;
} else {
return byteswap(value);
}
}
Result<const elf::header_info&, internal_error> elf::get_header_info() {
if(header) {
return header.unwrap();
}
if(tried_to_load_header) {
return internal_error("previous header load failed {}", file->path());
}
tried_to_load_header = true;
if(is_64) {
return get_header_info_impl<64>();
} else {
return get_header_info_impl<32>();
}
}
template<std::size_t Bits>
Result<const elf::header_info&, internal_error> elf::get_header_info_impl() {
static_assert(Bits == 32 || Bits == 64, "Unexpected Bits argument");
using Header = typename std::conditional<Bits == 32, Elf32_Ehdr, Elf64_Ehdr>::type;
auto loaded_header = file->read<Header>(0);
if(loaded_header.is_error()) {
return std::move(loaded_header).unwrap_error();
}
const Header& file_header = loaded_header.unwrap_value();
if(file_header.e_ehsize != sizeof(Header)) {
return internal_error("ELF file header size mismatch {}", file->path());
}
header_info info;
info.e_phoff = byteswap_if_needed(file_header.e_phoff);
info.e_phnum = byteswap_if_needed(file_header.e_phnum);
info.e_phentsize = byteswap_if_needed(file_header.e_phentsize);
info.e_shoff = byteswap_if_needed(file_header.e_shoff);
info.e_shnum = byteswap_if_needed(file_header.e_shnum);
info.e_shentsize = byteswap_if_needed(file_header.e_shentsize);
info.e_shstrndx = byteswap_if_needed(file_header.e_shstrndx);
header = info;
return header.unwrap();
}
Result<const std::vector<elf::section_info>&, internal_error> elf::get_sections() {
if(did_load_sections) {
return sections;
}
if(tried_to_load_sections) {
return internal_error("previous sections load failed {}", file->path());
}
tried_to_load_sections = true;
if(is_64) {
return get_sections_impl<64>();
} else {
return get_sections_impl<32>();
}
}
template<std::size_t Bits>
Result<const std::vector<elf::section_info>&, internal_error> elf::get_sections_impl() {
static_assert(Bits == 32 || Bits == 64, "Unexpected Bits argument");
using SHeader = typename std::conditional<Bits == 32, Elf32_Shdr, Elf64_Shdr>::type;
auto header = get_header_info();
if(header.is_error()) {
return std::move(header).unwrap_error();
}
const auto& header_info = header.unwrap_value();
for(unsigned i = 0; i < header_info.e_shnum; i++) {
auto loaded_sh = file->read<SHeader>(header_info.e_shoff + header_info.e_shentsize * i);
if(loaded_sh.is_error()) {
return std::move(loaded_sh).unwrap_error();
}
const SHeader& section_header = loaded_sh.unwrap_value();
section_info info;
info.sh_name = byteswap_if_needed(section_header.sh_name);
info.sh_type = byteswap_if_needed(section_header.sh_type);
info.sh_addr = byteswap_if_needed(section_header.sh_addr);
info.sh_offset = byteswap_if_needed(section_header.sh_offset);
info.sh_size = byteswap_if_needed(section_header.sh_size);
info.sh_entsize = byteswap_if_needed(section_header.sh_entsize);
info.sh_link = byteswap_if_needed(section_header.sh_link);
sections.push_back(info);
}
did_load_sections = true;
return sections;
}
Result<const std::vector<char>&, internal_error> elf::get_strtab(std::size_t index) {
auto res = strtab_entries.insert({index, {}});
auto it = res.first;
auto did_insert = res.second;
auto& entry = it->second;
if(!did_insert) {
if(entry.did_load_strtab) {
return entry.data;
}
if(entry.tried_to_load_strtab) {
return internal_error("previous strtab load failed {}", file->path());
}
}
entry.tried_to_load_strtab = true;
auto sections_ = get_sections();
if(sections_.is_error()) {
return std::move(sections_).unwrap_error();
}
const auto& sections = sections_.unwrap_value();
if(index >= sections.size()) {
return internal_error("requested strtab section index out of range");
}
const auto& section = sections[index];
if(section.sh_type != SHT_STRTAB) {
return internal_error("requested strtab section not a strtab (requested {} of {})", index, file->path());
}
entry.data.resize(section.sh_size + 1);
auto read_res = file->read_bytes(
span<char>{entry.data.data(), to<std::size_t>(section.sh_size)},
section.sh_offset
);
if(!read_res) {
return read_res.unwrap_error();
}
entry.data[section.sh_size] = 0; // just out of an abundance of caution
entry.did_load_strtab = true;
return entry.data;
}
Result<const optional<elf::symtab_info>&, internal_error> elf::get_symtab() {
if(did_load_symtab) {
return symtab;
}
if(tried_to_load_symtab) {
return internal_error("previous symtab load failed {}", file->path());
}
tried_to_load_symtab = true;
if(is_64) {
auto res = get_symtab_impl<64>(false);
if(res.has_value()) {
symtab = std::move(res).unwrap_value();
did_load_symtab = true;
return symtab;
} else {
return std::move(res).unwrap_error();
}
} else {
auto res = get_symtab_impl<32>(false);
if(res.has_value()) {
symtab = std::move(res).unwrap_value();
did_load_symtab = true;
return symtab;
} else {
return std::move(res).unwrap_error();
}
}
}
Result<const optional<elf::symtab_info>&, internal_error> elf::get_dynamic_symtab() {
if(did_load_dynamic_symtab) {
return dynamic_symtab;
}
if(tried_to_load_dynamic_symtab) {
return internal_error("previous dynamic symtab load failed {}", file->path());
}
tried_to_load_dynamic_symtab = true;
if(is_64) {
auto res = get_symtab_impl<64>(true);
if(res.has_value()) {
dynamic_symtab = std::move(res).unwrap_value();
did_load_dynamic_symtab = true;
return dynamic_symtab;
} else {
return std::move(res).unwrap_error();
}
} else {
auto res = get_symtab_impl<32>(true);
if(res.has_value()) {
dynamic_symtab = std::move(res).unwrap_value();
did_load_dynamic_symtab = true;
return dynamic_symtab;
} else {
return std::move(res).unwrap_error();
}
}
}
template<std::size_t Bits>
Result<optional<elf::symtab_info>, internal_error> elf::get_symtab_impl(bool dynamic) {
// https://refspecs.linuxfoundation.org/elf/elf.pdf
// page 66: only one sht_symtab and sht_dynsym section per file
// page 32: symtab spec
static_assert(Bits == 32 || Bits == 64, "Unexpected Bits argument");
using SymEntry = typename std::conditional<Bits == 32, Elf32_Sym, Elf64_Sym>::type;
auto sections_ = get_sections();
if(sections_.is_error()) {
return std::move(sections_).unwrap_error();
}
const auto& sections = sections_.unwrap_value();
optional<symtab_info> symbol_table;
for(const auto& section : sections) {
if(section.sh_type == (dynamic ? SHT_DYNSYM : SHT_SYMTAB)) {
if(section.sh_entsize != sizeof(SymEntry)) {
return internal_error("elf seems corrupted, sym entry mismatch {}", file->path());
}
if(section.sh_size % section.sh_entsize != 0) {
return internal_error("elf seems corrupted, sym entry vs section size mismatch {}", file->path());
}
std::vector<SymEntry> buffer(section.sh_size / section.sh_entsize);
auto res = file->read_span(make_span(buffer.begin(), buffer.end()), section.sh_offset);
if(!res) {
return res.unwrap_error();
}
symbol_table = symtab_info{};
symbol_table.unwrap().entries.reserve(buffer.size());
for(const auto& entry : buffer) {
symtab_entry normalized;
normalized.st_name = byteswap_if_needed(entry.st_name);
normalized.st_info = byteswap_if_needed(entry.st_info);
normalized.st_other = byteswap_if_needed(entry.st_other);
normalized.st_shndx = byteswap_if_needed(entry.st_shndx);
normalized.st_value = byteswap_if_needed(entry.st_value);
normalized.st_size = byteswap_if_needed(entry.st_size);
// on arm I've observed zero-size symbols that overlap with symbols we care about
// this interferes with some symbol lookup - that could be fixed by enhancing the logic there but
// also it's easy to just exclude zero-size symbols here
// 1413: 00000000000349e0 0 NOTYPE LOCAL DEFAULT 13 $x
// 32341: 00000000000349e0 220 FUNC GLOBAL DEFAULT 13 _Z33stacktrace_from_current_rethrow_3RSt6vectorIiSaIiEE
if(normalized.st_size != 0) {
symbol_table.unwrap().entries.push_back(normalized);
}
}
std::sort(
symbol_table.unwrap().entries.begin(),
symbol_table.unwrap().entries.end(),
[] (const symtab_entry& a, const symtab_entry& b) {
return a.st_value < b.st_value;
}
);
symbol_table.unwrap().strtab_link = section.sh_link;
break;
}
}
return symbol_table;
}
Result<maybe_owned<elf>, internal_error> open_elf_cached(const std::string& object_path) {
if(object_path.empty()) {
return internal_error{"empty object_path"};
}
if(get_cache_mode() == cache_mode::prioritize_memory) {
return elf::open(object_path)
.transform([](elf&& obj) { return maybe_owned<elf>{detail::make_unique<elf>(std::move(obj))}; });
} else {
static std::mutex m;
std::unique_lock<std::mutex> lock{m};
// TODO: Re-evaluate storing the error
static std::unordered_map<std::string, Result<elf, internal_error>> cache;
auto it = cache.find(object_path);
if(it == cache.end()) {
auto res = cache.emplace(object_path, elf::open(object_path));
VERIFY(res.second);
it = res.first;
}
return it->second.transform([](elf& obj) { return maybe_owned<elf>(&obj); });
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,144 @@
#ifndef ELF_HPP
#define ELF_HPP
#include "cpptrace/forward.hpp"
#include "utils/common.hpp"
#include "utils/io/base_file.hpp"
#include "utils/span.hpp"
#include "utils/utils.hpp"
#if IS_LINUX
#include <cstdint>
#include <string>
#include <unordered_map>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
// TODO: make methods const and a bunch of members mutable
class elf {
std::unique_ptr<base_file> file;
bool is_little_endian;
bool is_64;
struct header_info {
uint64_t e_phoff;
uint32_t e_phnum;
uint32_t e_phentsize;
uint64_t e_shoff;
uint32_t e_shnum;
uint32_t e_shentsize;
uint16_t e_shstrndx;
};
bool tried_to_load_header = false;
optional<header_info> header;
struct section_info {
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_addr;
uint64_t sh_offset;
uint64_t sh_size;
uint64_t sh_entsize;
uint32_t sh_link;
};
bool tried_to_load_sections = false;
bool did_load_sections = false;
std::vector<section_info> sections;
struct strtab_entry {
bool tried_to_load_strtab = false;
bool did_load_strtab = false;
std::vector<char> data;
};
std::unordered_map<std::size_t, strtab_entry> strtab_entries;
struct symtab_entry {
uint32_t st_name;
unsigned char st_info;
unsigned char st_other;
uint16_t st_shndx;
uint64_t st_value;
uint64_t st_size;
};
struct symtab_info {
std::vector<symtab_entry> entries;
std::size_t strtab_link = 0;
};
bool tried_to_load_symtab = false;
bool did_load_symtab = false;
optional<symtab_info> symtab;
bool tried_to_load_dynamic_symtab = false;
bool did_load_dynamic_symtab = false;
optional<symtab_info> dynamic_symtab;
elf(std::unique_ptr<base_file> file, bool is_little_endian, bool is_64);
static NODISCARD Result<elf, internal_error> open(std::unique_ptr<base_file> file);
public:
static NODISCARD Result<elf, internal_error> open(cstring_view object_path);
static NODISCARD Result<elf, internal_error> open(cbspan object);
elf(elf&&) = default;
public:
Result<std::uintptr_t, internal_error> get_module_image_base();
private:
template<std::size_t Bits>
Result<std::uintptr_t, internal_error> get_module_image_base_impl();
public:
optional<std::string> lookup_symbol(frame_ptr pc);
private:
optional<std::string> lookup_symbol(frame_ptr pc, const optional<symtab_info>& maybe_symtab);
public:
struct pc_range {
frame_ptr low;
frame_ptr high; // not inclusive
};
// for in-memory JIT elves
Result<std::vector<pc_range>, internal_error> get_pc_ranges();
struct symbol_entry {
std::string st_name;
uint16_t st_shndx;
uint64_t st_value;
uint64_t st_size;
};
Result<optional<std::vector<symbol_entry>>, internal_error> get_symtab_entries();
Result<optional<std::vector<symbol_entry>>, internal_error> get_dynamic_symtab_entries();
private:
Result<optional<std::vector<symbol_entry>>, internal_error> resolve_symtab_entries(
const Result<const optional<symtab_info> &, internal_error>&
);
private:
template<typename T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
T byteswap_if_needed(T value);
Result<const header_info&, internal_error> get_header_info();
template<std::size_t Bits>
Result<const header_info&, internal_error> get_header_info_impl();
Result<const std::vector<section_info>&, internal_error> get_sections();
template<std::size_t Bits>
Result<const std::vector<section_info>&, internal_error> get_sections_impl();
Result<const std::vector<char>&, internal_error> get_strtab(std::size_t index);
Result<const optional<symtab_info>&, internal_error> get_symtab();
Result<const optional<symtab_info>&, internal_error> get_dynamic_symtab();
template<std::size_t Bits>
Result<optional<symtab_info>, internal_error> get_symtab_impl(bool dynamic);
};
NODISCARD Result<maybe_owned<elf>, internal_error> open_elf_cached(const std::string& object_path);
}
CPPTRACE_END_NAMESPACE
#endif
#endif

View File

@ -0,0 +1,730 @@
#include "binary/mach-o.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include "utils/io/file.hpp"
#include "utils/io/memory_file_view.hpp"
#if IS_APPLE
// A number of mach-o functions are deprecated as of macos 13
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include <iostream>
#include <iomanip>
#include <mach-o/loader.h>
#include <mach-o/swap.h>
#include <mach-o/fat.h>
#include <crt_externs.h>
#include <mach-o/nlist.h>
#include <mach-o/stab.h>
#include <mach-o/arch.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
bool is_mach_o(std::uint32_t magic) {
switch(magic) {
case FAT_MAGIC:
case FAT_CIGAM:
case MH_MAGIC:
case MH_CIGAM:
case MH_MAGIC_64:
case MH_CIGAM_64:
return true;
default:
return false;
}
}
bool file_is_mach_o(cstring_view object_path) noexcept {
auto file = raii_wrap(std::fopen(object_path.c_str(), "rb"), file_deleter);
if(file == nullptr) {
return false;
}
auto magic = load_bytes<std::uint32_t>(file, 0);
if(magic) {
return is_mach_o(magic.unwrap_value());
} else {
return false;
}
}
bool is_fat_magic(std::uint32_t magic) {
return magic == FAT_MAGIC || magic == FAT_CIGAM;
}
// Based on https://github.com/AlexDenisov/segment_dumper/blob/master/main.c
// and https://lowlevelbits.org/parsing-mach-o-files/
bool is_magic_64(std::uint32_t magic) {
return magic == MH_MAGIC_64 || magic == MH_CIGAM_64;
}
bool should_swap_bytes(std::uint32_t magic) {
return magic == MH_CIGAM || magic == MH_CIGAM_64 || magic == FAT_CIGAM;
}
void swap_mach_header(mach_header_64& header) {
swap_mach_header_64(&header, NX_UnknownByteOrder);
}
void swap_mach_header(mach_header& header) {
swap_mach_header(&header, NX_UnknownByteOrder);
}
void swap_segment_command(segment_command_64& segment) {
swap_segment_command_64(&segment, NX_UnknownByteOrder);
}
void swap_segment_command(segment_command& segment) {
swap_segment_command(&segment, NX_UnknownByteOrder);
}
void swap_nlist(struct nlist& entry) {
swap_nlist(&entry, 1, NX_UnknownByteOrder);
}
void swap_nlist(struct nlist_64& entry) {
swap_nlist_64(&entry, 1, NX_UnknownByteOrder);
}
#ifdef __LP64__
#define LP(x) x##_64
#else
#define LP(x) x
#endif
Result<const char*, internal_error> mach_o::symtab_info_data::get_string(std::size_t index) const {
if(stringtab && index < symtab.strsize) {
return stringtab.unwrap().data() + index;
} else {
return internal_error("can't retrieve symbol from symtab");
}
}
Result<monostate, internal_error> mach_o::load() {
if(magic == FAT_MAGIC || magic == FAT_CIGAM) {
return load_fat_mach();
} else {
fat_index = 0;
if(is_magic_64(magic)) {
return load_mach<64>();
} else {
return load_mach<32>();
}
}
}
Result<mach_o, internal_error> mach_o::open(std::unique_ptr<base_file> file) {
auto magic = file->read<std::uint32_t>(0);
if(!magic) {
return magic.unwrap_error();
}
if(!is_mach_o(magic.unwrap_value())) {
return internal_error("File is not mach-o {}", file->path());
}
mach_o obj(std::move(file), magic.unwrap_value());
auto result = obj.load();
if(result.is_error()) {
return result.unwrap_error();
} else {
return obj;
}
}
Result<mach_o, internal_error> mach_o::open(cstring_view object_path) {
auto file_res = file::open(object_path);
if(!file_res) {
return internal_error("Unable to read object file {}", object_path);
}
auto& file = file_res.unwrap_value();
return open(make_unique(std::move(file)));
}
Result<mach_o, internal_error> mach_o::open(cbspan object) {
return open(make_unique<memory_file_view>(object));
}
Result<std::uintptr_t, internal_error> mach_o::get_text_vmaddr() {
for(const auto& command : load_commands) {
if(command.cmd == LC_SEGMENT_64 || command.cmd == LC_SEGMENT) {
auto segment = command.cmd == LC_SEGMENT_64
? load_segment_command<64>(command.file_offset)
: load_segment_command<32>(command.file_offset);
if(segment.is_error()) {
return std::move(segment).unwrap_error();
}
if(std::strcmp(segment.unwrap_value().segname, "__TEXT") == 0) {
return segment.unwrap_value().vmaddr;
}
}
}
// somehow no __TEXT section was found...
return internal_error("Couldn't find __TEXT section while parsing Mach-O object");
}
std::size_t mach_o::get_fat_index() const {
VERIFY(fat_index != std::numeric_limits<std::size_t>::max());
return fat_index;
}
void mach_o::print_segments() const {
int i = 0;
for(const auto& command : load_commands) {
if(command.cmd == LC_SEGMENT_64 || command.cmd == LC_SEGMENT) {
auto segment_load = command.cmd == LC_SEGMENT_64
? load_segment_command<64>(command.file_offset)
: load_segment_command<32>(command.file_offset);
fprintf(stderr, "Load command %d\n", i);
if(segment_load.is_error()) {
fprintf(stderr, " error\n");
segment_load.drop_error();
continue;
}
auto& segment = segment_load.unwrap_value();
fprintf(stderr, " cmd %u\n", segment.cmd);
fprintf(stderr, " cmdsize %u\n", segment.cmdsize);
fprintf(stderr, " segname %s\n", segment.segname);
fprintf(stderr, " vmaddr 0x%llx\n", segment.vmaddr);
fprintf(stderr, " vmsize 0x%llx\n", segment.vmsize);
fprintf(stderr, " off 0x%llx\n", segment.fileoff);
fprintf(stderr, " filesize %llu\n", segment.filesize);
fprintf(stderr, " nsects %u\n", segment.nsects);
}
i++;
}
}
Result<std::vector<mach_o::pc_range>, internal_error> mach_o::get_pc_ranges() {
std::vector<pc_range> ranges;
for(const auto& command : load_commands) {
if(command.cmd == LC_SEGMENT_64 || command.cmd == LC_SEGMENT) {
auto segment_res = command.cmd == LC_SEGMENT_64
? load_segment_command<64>(command.file_offset)
: load_segment_command<32>(command.file_offset);
if(segment_res.is_error()) {
return std::move(segment_res).unwrap_error();
}
auto& segment = segment_res.unwrap_value();
if(std::strcmp(segment.segname, "__TEXT") == 0) {
ranges.push_back({segment.vmaddr, segment.vmaddr + segment.vmsize});
}
}
}
return ranges;
}
Result<std::reference_wrapper<optional<mach_o::symtab_info_data>>, internal_error> mach_o::get_symtab_info() {
if(!symtab_info.has_value() && !tried_to_load_symtab) {
// don't try to load the symtab again if for some reason loading here fails
tried_to_load_symtab = true;
for(const auto& command : load_commands) {
if(command.cmd == LC_SYMTAB) {
symtab_info_data info;
auto symtab = load_symbol_table_command(command.file_offset);
if(!symtab) {
return std::move(symtab).unwrap_error();
}
info.symtab = symtab.unwrap_value();
auto string = load_string_table(info.symtab.stroff, info.symtab.strsize);
if(!string) {
return std::move(string).unwrap_error();
}
info.stringtab = std::move(string).unwrap_value();
symtab_info = std::move(info);
break;
}
}
}
return std::reference_wrapper<optional<symtab_info_data>>{symtab_info};
}
void mach_o::print_symbol_table_entry(
const nlist_64& entry,
const char* stringtab,
std::size_t stringsize,
std::size_t j
) const {
const char* type = "";
if(entry.n_type & N_STAB) {
switch(entry.n_type) {
case N_SO: type = "N_SO"; break;
case N_OSO: type = "N_OSO"; break;
case N_BNSYM: type = "N_BNSYM"; break;
case N_ENSYM: type = "N_ENSYM"; break;
case N_FUN: type = "N_FUN"; break;
}
} else if((entry.n_type & N_TYPE) == N_SECT) {
type = "N_SECT";
}
fprintf(
stderr,
"%5llu %8llx %2llx %7s %2llu %4llx %16llx %s\n",
to_ull(j),
to_ull(entry.n_un.n_strx),
to_ull(entry.n_type),
type,
to_ull(entry.n_sect),
to_ull(entry.n_desc),
to_ull(entry.n_value),
stringtab == nullptr
? "Stringtab error"
: entry.n_un.n_strx < stringsize
? stringtab + entry.n_un.n_strx
: "String index out of bounds"
);
}
void mach_o::print_symbol_table() {
int i = 0;
for(const auto& command : load_commands) {
if(command.cmd == LC_SYMTAB) {
auto symtab_load = load_symbol_table_command(command.file_offset);
fprintf(stderr, "Load command %d\n", i);
if(symtab_load.is_error()) {
fprintf(stderr, " error\n");
symtab_load.drop_error();
continue;
}
auto& symtab = symtab_load.unwrap_value();
fprintf(stderr, " cmd %llu\n", to_ull(symtab.cmd));
fprintf(stderr, " cmdsize %llu\n", to_ull(symtab.cmdsize));
fprintf(stderr, " symoff 0x%llu\n", to_ull(symtab.symoff));
fprintf(stderr, " nsyms %llu\n", to_ull(symtab.nsyms));
fprintf(stderr, " stroff 0x%llu\n", to_ull(symtab.stroff));
fprintf(stderr, " strsize %llu\n", to_ull(symtab.strsize));
auto stringtab = load_string_table(symtab.stroff, symtab.strsize);
if(!stringtab) {
stringtab.drop_error();
}
for(std::size_t j = 0; j < symtab.nsyms; j++) {
auto entry = bits == 32
? load_symtab_entry<32>(symtab.symoff, j)
: load_symtab_entry<64>(symtab.symoff, j);
if(!entry) {
fprintf(stderr, "error loading symtab entry\n");
entry.drop_error();
continue;
}
print_symbol_table_entry(
entry.unwrap_value(),
stringtab ? stringtab.unwrap_value().data() : nullptr,
symtab.strsize,
j
);
}
}
i++;
}
}
// produce information similar to dsymutil -dump-debug-map
Result<mach_o::debug_map, internal_error> mach_o::get_debug_map() {
// we have a bunch of symbols in our binary we need to pair up with symbols from various .o files
// first collect symbols and the objects they come from
debug_map debug_map;
auto symtab_info_res = get_symtab_info();
if(!symtab_info_res) {
return std::move(symtab_info_res).unwrap_error();
}
if(!symtab_info_res.unwrap_value().get()) {
return internal_error("No symtab info");
}
const auto& symtab_info = symtab_info_res.unwrap_value().get().unwrap();
const auto& symtab = symtab_info.symtab;
// TODO: Take timestamp into account?
std::string current_module;
optional<debug_map_entry> current_function;
for(std::size_t j = 0; j < symtab.nsyms; j++) {
auto load_entry = bits == 32
? load_symtab_entry<32>(symtab.symoff, j)
: load_symtab_entry<64>(symtab.symoff, j);
if(!load_entry) {
return std::move(load_entry).unwrap_error();
}
auto& entry = load_entry.unwrap_value();
// entry.n_type & N_STAB indicates symbolic debug info
if(!(entry.n_type & N_STAB)) {
continue;
}
switch(entry.n_type) {
case N_SO:
// pass - these encode path and filename for the module, if applicable
break;
case N_OSO:
{
// sets the module
auto str = symtab_info.get_string(entry.n_un.n_strx);
if(!str) {
return std::move(str).unwrap_error();
}
current_module = str.unwrap_value();
}
break;
case N_BNSYM: break; // pass
case N_ENSYM: break; // pass
case N_FUN:
{
auto str = symtab_info.get_string(entry.n_un.n_strx);
if(!str) {
return std::move(str).unwrap_error();
}
if(str.unwrap_value()[0] == 0) {
// end of function scope
if(!current_function) { /**/ }
current_function.unwrap().size = entry.n_value;
debug_map[current_module].push_back(std::move(current_function).unwrap());
} else {
current_function = debug_map_entry{};
current_function.unwrap().source_address = entry.n_value;
current_function.unwrap().name = str.unwrap_value();
}
}
break;
}
}
return debug_map;
}
Result<const std::vector<mach_o::symbol_entry>&, internal_error> mach_o::symbol_table() {
if(symbols) {
return symbols.unwrap();
}
if(tried_to_load_symbols) {
return internal_error("previous symbol table load failed");
}
tried_to_load_symbols = true;
std::vector<symbol_entry> symbol_table;
// we have a bunch of symbols in our binary we need to pair up with symbols from various .o files
// first collect symbols and the objects they come from
auto symtab_info_res = get_symtab_info();
if(!symtab_info_res) {
return std::move(symtab_info_res).unwrap_error();
}
if(!symtab_info_res.unwrap_value().get()) {
return internal_error("No symtab info");
}
const auto& symtab_info = symtab_info_res.unwrap_value().get().unwrap();
const auto& symtab = symtab_info.symtab;
// TODO: Take timestamp into account?
for(std::size_t j = 0; j < symtab.nsyms; j++) {
auto load_entry = bits == 32
? load_symtab_entry<32>(symtab.symoff, j)
: load_symtab_entry<64>(symtab.symoff, j);
if(!load_entry) {
return std::move(load_entry).unwrap_error();
}
auto& entry = load_entry.unwrap_value();
if(entry.n_type & N_STAB) {
continue;
}
if((entry.n_type & N_TYPE) == N_SECT) {
auto str = symtab_info.get_string(entry.n_un.n_strx);
if(!str) {
return std::move(str).unwrap_error();
}
symbol_table.push_back({
entry.n_value,
str.unwrap_value()
});
}
}
std::sort(
symbol_table.begin(),
symbol_table.end(),
[] (const symbol_entry& a, const symbol_entry& b) { return a.address < b.address; }
);
symbols = std::move(symbol_table);
return symbols.unwrap();
}
optional<std::string> mach_o::lookup_symbol(frame_ptr pc) {
auto symtab_ = symbol_table();
if(!symtab_) {
return nullopt;
}
const auto& symtab = symtab_.unwrap_value();;
auto it = first_less_than_or_equal(
symtab.begin(),
symtab.end(),
pc,
[] (frame_ptr pc, const symbol_entry& entry) {
return pc < entry.address;
}
);
if(it == symtab.end()) {
return nullopt;
}
ASSERT(pc >= it->address);
// TODO: We subtracted one from the address so name + diff won't show up in the objdump, decide if desirable
// to have an easier offset to lookup
return microfmt::format("{} + {}", it->name, pc - it->address);
}
// produce information similar to dsymutil -dump-debug-map
void mach_o::print_debug_map(const debug_map& debug_map) {
for(const auto& entry : debug_map) {
std::cout<<entry.first<<": "<< '\n';
for(const auto& symbol : entry.second) {
std::cerr
<< " "
<< symbol.name
<< " "
<< std::hex
<< symbol.source_address
<< " "
<< symbol.size
<< std::dec
<< '\n';
}
}
}
template<std::size_t Bits>
Result<monostate, internal_error> mach_o::load_mach() {
static_assert(Bits == 32 || Bits == 64, "Unexpected Bits argument");
bits = Bits;
using Mach_Header = typename std::conditional<Bits == 32, mach_header, mach_header_64>::type;
std::size_t header_size = sizeof(Mach_Header);
auto load_header = file->read<Mach_Header>(load_base);
if(!load_header) {
return load_header.unwrap_error();
}
Mach_Header& header = load_header.unwrap_value();
magic = header.magic;
if(should_swap()) {
swap_mach_header(header);
}
cputype = header.cputype;
cpusubtype = header.cpusubtype;
filetype = header.filetype;
n_load_commands = header.ncmds;
sizeof_load_commands = header.sizeofcmds;
flags = header.flags;
// handle load commands
std::uint32_t ncmds = header.ncmds;
std::uint32_t load_commands_offset = load_base + header_size;
// iterate load commands
std::uint32_t actual_offset = load_commands_offset;
for(std::uint32_t i = 0; i < ncmds; i++) {
auto load_cmd = file->read<load_command>(actual_offset);
if(!load_cmd) {
return load_cmd.unwrap_error();
}
load_command& cmd = load_cmd.unwrap_value();
if(should_swap()) {
swap_load_command(&cmd, NX_UnknownByteOrder);
}
load_commands.push_back({ actual_offset, cmd.cmd, cmd.cmdsize });
actual_offset += cmd.cmdsize;
}
return monostate{};
}
Result<monostate, internal_error> mach_o::load_fat_mach() {
std::size_t header_size = sizeof(fat_header);
std::size_t arch_size = sizeof(fat_arch);
auto load_header = file->read<fat_header>(0);
if(!load_header) {
return load_header.unwrap_error();
}
fat_header& header = load_header.unwrap_value();
if(should_swap()) {
swap_fat_header(&header, NX_UnknownByteOrder);
}
// thread_local static struct LP(mach_header)* mhp = _NSGetMachExecuteHeader();
// off_t arch_offset = (off_t)header_size;
// for(std::size_t i = 0; i < header.nfat_arch; i++) {
// fat_arch arch = load_bytes<fat_arch>(file, arch_offset);
// if(should_swap()) {
// swap_fat_arch(&arch, 1, NX_UnknownByteOrder);
// }
// off_t mach_header_offset = (off_t)arch.offset;
// arch_offset += arch_size;
// std::uint32_t magic = load_bytes<std::uint32_t>(file, mach_header_offset);
// std::cerr<<"xxx: "<<arch.cputype<<" : "<<mhp->cputype<<std::endl;
// std::cerr<<" "<<arch.cpusubtype<<" : "<<static_cast<cpu_subtype_t>(mhp->cpusubtype & ~CPU_SUBTYPE_MASK)<<std::endl;
// if(
// arch.cputype == mhp->cputype &&
// static_cast<cpu_subtype_t>(mhp->cpusubtype & ~CPU_SUBTYPE_MASK) == arch.cpusubtype
// ) {
// load_base = mach_header_offset;
// fat_index = i;
// if(is_magic_64(magic)) {
// load_mach<64>(true);
// } else {
// load_mach<32>(true);
// }
// return;
// }
// }
std::vector<fat_arch> fat_arches;
fat_arches.reserve(header.nfat_arch);
off_t arch_offset = (off_t)header_size;
for(std::size_t i = 0; i < header.nfat_arch; i++) {
auto load_arch = file->read<fat_arch>(arch_offset);
if(!load_arch) {
return load_arch.unwrap_error();
}
fat_arch& arch = load_arch.unwrap_value();
if(should_swap()) {
swap_fat_arch(&arch, 1, NX_UnknownByteOrder);
}
fat_arches.push_back(arch);
arch_offset += arch_size;
}
thread_local static struct LP(mach_header)* mhp = _NSGetMachExecuteHeader();
fat_arch* best = NXFindBestFatArch(
mhp->cputype,
mhp->cpusubtype,
fat_arches.data(),
header.nfat_arch
);
if(best) {
off_t mach_header_offset = (off_t)best->offset;
auto magic = file->read<std::uint32_t>(mach_header_offset);
if(!magic) {
return magic.unwrap_error();
}
load_base = mach_header_offset;
fat_index = best - fat_arches.data();
if(is_magic_64(magic.unwrap_value())) {
load_mach<64>();
} else {
load_mach<32>();
}
return monostate{};
}
// If this is reached... something went wrong. The cpu we're on wasn't found.
return internal_error("Couldn't find appropriate architecture in fat Mach-O");
}
template<std::size_t Bits>
Result<segment_command_64, internal_error> mach_o::load_segment_command(std::uint32_t offset) const {
using Segment_Command = typename std::conditional<Bits == 32, segment_command, segment_command_64>::type;
auto load_segment = file->read<Segment_Command>(offset);
if(!load_segment) {
return load_segment.unwrap_error();
}
Segment_Command& segment = load_segment.unwrap_value();
ASSERT(segment.cmd == LC_SEGMENT_64 || segment.cmd == LC_SEGMENT);
if(should_swap()) {
swap_segment_command(segment);
}
// fields match just u64 instead of u32
segment_command_64 common;
common.cmd = segment.cmd;
common.cmdsize = segment.cmdsize;
static_assert(sizeof common.segname == 16 && sizeof segment.segname == 16, "xx");
memcpy(common.segname, segment.segname, 16);
common.vmaddr = segment.vmaddr;
common.vmsize = segment.vmsize;
common.fileoff = segment.fileoff;
common.filesize = segment.filesize;
common.maxprot = segment.maxprot;
common.initprot = segment.initprot;
common.nsects = segment.nsects;
common.flags = segment.flags;
return common;
}
Result<symtab_command, internal_error> mach_o::load_symbol_table_command(std::uint32_t offset) const {
auto load_symtab = file->read<symtab_command>(offset);
if(!load_symtab) {
return load_symtab.unwrap_error();
}
symtab_command& symtab = load_symtab.unwrap_value();
ASSERT(symtab.cmd == LC_SYMTAB);
if(should_swap()) {
swap_symtab_command(&symtab, NX_UnknownByteOrder);
}
return symtab;
}
template<std::size_t Bits>
Result<nlist_64, internal_error> mach_o::load_symtab_entry(std::uint32_t symbol_base, std::size_t index) const {
using Nlist = typename std::conditional<Bits == 32, struct nlist, struct nlist_64>::type;
uint32_t offset = load_base + symbol_base + index * sizeof(Nlist);
auto load_entry = file->read<Nlist>(offset);
if(!load_entry) {
return load_entry.unwrap_error();
}
Nlist& entry = load_entry.unwrap_value();
if(should_swap()) {
swap_nlist(entry);
}
// fields match just u64 instead of u32
nlist_64 common;
common.n_un.n_strx = entry.n_un.n_strx;
common.n_type = entry.n_type;
common.n_sect = entry.n_sect;
common.n_desc = entry.n_desc;
common.n_value = entry.n_value;
return common;
}
Result<std::vector<char>, internal_error> mach_o::load_string_table(std::uint32_t offset, std::uint32_t byte_count) const {
std::vector<char> buffer(byte_count + 1);
auto read_res = file->read_bytes(span<char>{buffer.data(), byte_count}, load_base + offset);
if(!read_res) {
return read_res.unwrap_error();
}
buffer[byte_count] = 0; // just out of an abundance of caution
return buffer;
}
bool mach_o::should_swap() const {
return should_swap_bytes(magic);
}
Result<bool, internal_error> macho_is_fat(cstring_view object_path) {
auto file = raii_wrap(std::fopen(object_path.c_str(), "rb"), file_deleter);
if(file == nullptr) {
return internal_error("Unable to read object file {}", object_path);
}
auto magic = load_bytes<std::uint32_t>(file, 0);
if(!magic) {
return magic.unwrap_error();
} else {
return is_fat_magic(magic.unwrap_value());
}
}
Result<maybe_owned<mach_o>, internal_error> open_mach_o_cached(const std::string& object_path) {
if(object_path.empty()) {
return internal_error{"empty object_path"};
}
if(get_cache_mode() == cache_mode::prioritize_memory) {
return mach_o::open(object_path)
.transform([](mach_o&& obj) {
return maybe_owned<mach_o>{detail::make_unique<mach_o>(std::move(obj))};
});
} else {
static std::mutex m;
std::unique_lock<std::mutex> lock{m};
// TODO: Re-evaluate storing the error
static std::unordered_map<std::string, Result<mach_o, internal_error>> cache;
auto it = cache.find(object_path);
if(it == cache.end()) {
auto res = cache.insert({ object_path, mach_o::open(object_path) });
VERIFY(res.second);
it = res.first;
}
return it->second.transform([](mach_o& obj) { return maybe_owned<mach_o>(&obj); });
}
}
}
CPPTRACE_END_NAMESPACE
#pragma GCC diagnostic pop
#endif

View File

@ -0,0 +1,150 @@
#ifndef MACHO_HPP
#define MACHO_HPP
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include "utils/span.hpp"
#include "utils/io/base_file.hpp"
#if IS_APPLE
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <mach-o/arch.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
bool file_is_mach_o(cstring_view object_path) noexcept;
struct load_command_entry {
std::uint32_t file_offset;
std::uint32_t cmd;
std::uint32_t cmdsize;
};
class mach_o {
public:
struct debug_map_entry {
uint64_t source_address;
uint64_t size;
std::string name;
};
struct symbol_entry {
uint64_t address;
std::string name;
};
// map from object file to a vector of symbols to resolve
using debug_map = std::unordered_map<std::string, std::vector<debug_map_entry>>;
private:
std::unique_ptr<base_file> file;
std::uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
std::uint32_t filetype;
std::uint32_t n_load_commands;
std::uint32_t sizeof_load_commands;
std::uint32_t flags;
std::size_t bits = 0; // 32 or 64 once load_mach is called
std::size_t load_base = 0;
std::size_t fat_index = std::numeric_limits<std::size_t>::max();
std::vector<load_command_entry> load_commands;
struct symtab_info_data {
symtab_command symtab;
optional<std::vector<char>> stringtab;
Result<const char*, internal_error> get_string(std::size_t index) const;
};
bool tried_to_load_symtab = false;
optional<symtab_info_data> symtab_info;
bool tried_to_load_symbols = false;
optional<std::vector<symbol_entry>> symbols;
mach_o(std::unique_ptr<base_file> file, std::uint32_t magic) : file(std::move(file)), magic(magic) {}
Result<monostate, internal_error> load();
static NODISCARD Result<mach_o, internal_error> open(std::unique_ptr<base_file> file);
public:
static NODISCARD Result<mach_o, internal_error> open(cstring_view object_path);
static NODISCARD Result<mach_o, internal_error> open(cbspan object);
mach_o(mach_o&&) = default;
~mach_o() = default;
Result<std::uintptr_t, internal_error> get_text_vmaddr();
std::size_t get_fat_index() const;
void print_segments() const;
struct pc_range {
frame_ptr low;
frame_ptr high; // not inclusive
};
// for in-memory JIT mach-o's
Result<std::vector<pc_range>, internal_error> get_pc_ranges();
Result<std::reference_wrapper<optional<symtab_info_data>>, internal_error> get_symtab_info();
void print_symbol_table_entry(
const nlist_64& entry,
const char* stringtab,
std::size_t stringsize,
std::size_t j
) const;
void print_symbol_table();
// produce information similar to dsymutil -dump-debug-map
Result<debug_map, internal_error> get_debug_map();
Result<const std::vector<symbol_entry>&, internal_error> symbol_table();
optional<std::string> lookup_symbol(frame_ptr pc);
// produce information similar to dsymutil -dump-debug-map
static void print_debug_map(const debug_map& debug_map);
private:
template<std::size_t Bits>
Result<monostate, internal_error> load_mach();
Result<monostate, internal_error> load_fat_mach();
template<std::size_t Bits>
Result<segment_command_64, internal_error> load_segment_command(std::uint32_t offset) const;
Result<symtab_command, internal_error> load_symbol_table_command(std::uint32_t offset) const;
template<std::size_t Bits>
Result<nlist_64, internal_error> load_symtab_entry(std::uint32_t symbol_base, std::size_t index) const;
Result<std::vector<char>, internal_error> load_string_table(std::uint32_t offset, std::uint32_t byte_count) const;
bool should_swap() const;
};
Result<bool, internal_error> macho_is_fat(cstring_view object_path);
NODISCARD Result<maybe_owned<mach_o>, internal_error> open_mach_o_cached(const std::string& object_path);
}
CPPTRACE_END_NAMESPACE
#endif
#endif

View File

@ -0,0 +1,97 @@
#include "binary/module_base.hpp"
#include "platform/platform.hpp"
#include "utils/utils.hpp"
#include <string>
#include <mutex>
#include <unordered_map>
#if IS_LINUX || IS_APPLE
#include <unistd.h>
#include <dlfcn.h>
#if IS_APPLE
#include "binary/mach-o.hpp"
#else
#include "binary/elf.hpp"
#endif
#elif IS_WINDOWS
#include "binary/pe.hpp"
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
#if IS_LINUX
Result<std::uintptr_t, internal_error> get_module_image_base(const std::string& object_path) {
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
static std::unordered_map<std::string, std::uintptr_t> cache;
auto it = cache.find(object_path);
if(it == cache.end()) {
// arguably it'd be better to release the lock while computing this, but also arguably it's good to not
// have two threads try to do the same computation
auto elf_object = open_elf_cached(object_path);
// TODO: Cache the error
if(!elf_object) {
return elf_object.unwrap_error();
}
auto base = elf_object.unwrap_value()->get_module_image_base();
if(base.is_error()) {
return std::move(base).unwrap_error();
}
cache.insert(it, {object_path, base.unwrap_value()});
return base;
} else {
return it->second;
}
}
#elif IS_APPLE
Result<std::uintptr_t, internal_error> get_module_image_base(const std::string& object_path) {
// We have to parse the Mach-O to find the offset of the text section.....
// I don't know how addresses are handled if there is more than one __TEXT load command. I'm assuming for
// now that there is only one, and I'm using only the first section entry within that load command.
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
static std::unordered_map<std::string, std::uintptr_t> cache;
auto it = cache.find(object_path);
if(it == cache.end()) {
// arguably it'd be better to release the lock while computing this, but also arguably it's good to not
// have two threads try to do the same computation
auto mach_o_object = open_mach_o_cached(object_path);
// TODO: Cache the error
if(!mach_o_object) {
return mach_o_object.unwrap_error();
}
auto base = mach_o_object.unwrap_value()->get_text_vmaddr();
if(!base) {
return std::move(base).unwrap_error();
}
cache.insert(it, {object_path, base.unwrap_value()});
return base;
} else {
return it->second;
}
}
#else // Windows
Result<std::uintptr_t, internal_error> get_module_image_base(const std::string& object_path) {
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
static std::unordered_map<std::string, std::uintptr_t> cache;
auto it = cache.find(object_path);
if(it == cache.end()) {
// arguably it'd be better to release the lock while computing this, but also arguably it's good to not
// have two threads try to do the same computation
auto base = pe_get_module_image_base(object_path);
// TODO: Cache the error
if(!base) {
return std::move(base).unwrap_error();
}
cache.insert(it, {object_path, base.unwrap_value()});
return base;
} else {
return it->second;
}
}
#endif
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,15 @@
#ifndef IMAGE_MODULE_BASE_HPP
#define IMAGE_MODULE_BASE_HPP
#include "utils/utils.hpp"
#include <cstdint>
#include <string>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
Result<std::uintptr_t, internal_error> get_module_image_base(const std::string& object_path);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,190 @@
#include "binary/object.hpp"
#include "platform/platform.hpp"
#include "utils/utils.hpp"
#include "binary/module_base.hpp"
#include "logging.hpp"
#include <string>
#include <system_error>
#include <vector>
#include <mutex>
#include <unordered_map>
#if IS_LINUX || IS_APPLE
#include <unistd.h>
#include <dlfcn.h>
#if IS_LINUX
#include <link.h> // needed for dladdr1's link_map info
#endif
#elif IS_WINDOWS
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
#if IS_LINUX || IS_APPLE
#if defined(CPPTRACE_HAS_DL_FIND_OBJECT) || defined(CPPTRACE_HAS_DLADDR1)
std::string resolve_l_name(const char* l_name) {
if(l_name != nullptr && l_name[0] != 0) {
return l_name;
} else {
// empty l_name, this means it's the currently running executable
// TODO: Caching and proper handling
char buffer[CPPTRACE_PATH_MAX + 1]{};
auto res = readlink("/proc/self/exe", buffer, CPPTRACE_PATH_MAX);
if(res == -1) {
return ""; // TODO
} else {
return buffer;
}
}
}
#endif
// dladdr queries are needed to get pre-ASLR addresses and targets to run symbol resolution on
// _dl_find_object is preferred if at all possible as it is much faster (added in glibc 2.35)
// dladdr1 is preferred if possible because it allows for a more accurate object path to be resolved (glibc 2.3.3)
#ifdef CPPTRACE_HAS_DL_FIND_OBJECT // we don't even check for this on apple
object_frame get_frame_object_info(frame_ptr address) {
// Use _dl_find_object when we can, it's orders of magnitude faster
object_frame frame;
frame.raw_address = address;
frame.object_address = 0;
dl_find_object result;
if(_dl_find_object(reinterpret_cast<void*>(address), &result) == 0) { // thread safe
frame.object_path = resolve_l_name(result.dlfo_link_map->l_name);
frame.object_address = address - to_frame_ptr(result.dlfo_link_map->l_addr);
}
return frame;
}
#elif defined(CPPTRACE_HAS_DLADDR1)
object_frame get_frame_object_info(frame_ptr address) {
// https://github.com/bminor/glibc/blob/91695ee4598b39d181ab8df579b888a8863c4cab/elf/dl-addr.c#L26
Dl_info info;
link_map* link_map_info;
object_frame frame;
frame.raw_address = address;
frame.object_address = 0;
if(
// thread safe
dladdr1(reinterpret_cast<void*>(address), &info, reinterpret_cast<void**>(&link_map_info), RTLD_DL_LINKMAP)
) {
frame.object_path = resolve_l_name(link_map_info->l_name);
auto base = get_module_image_base(frame.object_path);
if(base.has_value()) {
frame.object_address = address
- reinterpret_cast<std::uintptr_t>(info.dli_fbase)
+ base.unwrap_value();
} else {
if(!should_absorb_trace_exceptions()) {
base.drop_error();
}
}
}
return frame;
}
#else
// glibc dladdr may not return an accurate dli_fname as it uses argv[0] for addresses in the main executable
// https://github.com/bminor/glibc/blob/caed1f5c0b2e31b5f4e0f21fea4b2c9ecd3b5b30/elf/dl-addr.c#L33-L36
// macos doesn't have dladdr1 but its dli_fname behaves more sensibly, same with some other libc's like musl
object_frame get_frame_object_info(frame_ptr address) {
// reference: https://github.com/bminor/glibc/blob/master/debug/backtracesyms.c
Dl_info info;
object_frame frame;
frame.raw_address = address;
frame.object_address = 0;
if(dladdr(reinterpret_cast<void*>(address), &info)) { // thread safe
frame.object_path = info.dli_fname;
auto base = get_module_image_base(info.dli_fname);
if(base.has_value()) {
frame.object_address = address
- reinterpret_cast<std::uintptr_t>(info.dli_fbase)
+ base.unwrap_value();
} else {
if(!should_absorb_trace_exceptions()) {
base.drop_error();
}
}
}
return frame;
}
#endif
#else
std::string get_module_name(HMODULE handle) {
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
static std::unordered_map<HMODULE, std::string> cache;
auto it = cache.find(handle);
if(it == cache.end()) {
char path[MAX_PATH];
if(GetModuleFileNameA(handle, path, sizeof(path))) {
cache.insert(it, {handle, path});
return path;
} else {
log::error(std::system_error(GetLastError(), std::system_category()).what());
cache.insert(it, {handle, ""});
return "";
}
} else {
return it->second;
}
}
object_frame get_frame_object_info(frame_ptr address) {
object_frame frame;
frame.raw_address = address;
frame.object_address = 0;
HMODULE handle;
// Multithread safe as long as another thread doesn't come along and free the module
if(GetModuleHandleExA(
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
reinterpret_cast<const char*>(address),
&handle
)) {
frame.object_path = get_module_name(handle);
auto base = get_module_image_base(frame.object_path);
if(base.has_value()) {
frame.object_address = address
- reinterpret_cast<std::uintptr_t>(handle)
+ base.unwrap_value();
} else {
if(!should_absorb_trace_exceptions()) {
base.drop_error();
}
}
} else {
log::error(std::system_error(GetLastError(), std::system_category()).what());
}
return frame;
}
#endif
std::vector<object_frame> get_frames_object_info(const std::vector<frame_ptr>& addresses) {
std::vector<object_frame> frames;
frames.reserve(addresses.size());
for(const frame_ptr address : addresses) {
frames.push_back(get_frame_object_info(address));
}
return frames;
}
object_frame resolve_safe_object_frame(const safe_object_frame& frame) {
std::string object_path = frame.object_path;
if(object_path.empty()) {
return {
frame.raw_address,
0,
""
};
}
return {
frame.raw_address,
frame.address_relative_to_object_start,
std::move(object_path)
};
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,19 @@
#ifndef OBJECT_HPP
#define OBJECT_HPP
#include <cpptrace/forward.hpp>
#include <vector>
#include <cstdint>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
object_frame get_frame_object_info(frame_ptr address);
std::vector<object_frame> get_frames_object_info(const std::vector<frame_ptr>& addresses);
object_frame resolve_safe_object_frame(const safe_object_frame& frame);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,97 @@
#include "binary/pe.hpp"
#include "platform/platform.hpp"
#include "utils/error.hpp"
#include "utils/utils.hpp"
#if IS_WINDOWS
#include <array>
#include <cstdio>
#include <cstring>
#include <string>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
template<typename T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
T pe_byteswap_if_needed(T value) {
// PE header values are little endian, I think dos e_lfanew should be too
if(!is_little_endian()) {
return byteswap(value);
} else {
return value;
}
}
Result<std::uintptr_t, internal_error> pe_get_module_image_base(cstring_view object_path) {
// https://drive.google.com/file/d/0B3_wGJkuWLytbnIxY1J5WUs4MEk/view?pli=1&resourcekey=0-n5zZ2UW39xVTH8ZSu6C2aQ
// https://0xrick.github.io/win-internals/pe3/
// Endianness should always be little for dos and pe headers
std::FILE* file_ptr;
errno_t ret = fopen_s(&file_ptr, object_path.c_str(), "rb");
auto file = raii_wrap(std::move(file_ptr), file_deleter);
if(ret != 0 || file == nullptr) {
return internal_error("Unable to read object file {}", object_path);
}
auto magic = load_bytes<std::array<char, 2>>(file, 0);
if(!magic) {
return std::move(magic).unwrap_error();
}
if(std::memcmp(magic.unwrap_value().data(), "MZ", 2) != 0) {
return internal_error("File is not a PE file {}", object_path);
}
auto e_lfanew = load_bytes<DWORD>(file, 0x3c); // dos header + 0x3c
if(!e_lfanew) {
return std::move(e_lfanew).unwrap_error();
}
DWORD nt_header_offset = pe_byteswap_if_needed(e_lfanew.unwrap_value());
auto signature = load_bytes<std::array<char, 4>>(file, nt_header_offset); // nt header + 0
if(!signature) {
return std::move(signature).unwrap_error();
}
if(std::memcmp(signature.unwrap_value().data(), "PE\0\0", 4) != 0) {
return internal_error("File is not a PE file {}", object_path);
}
auto size_of_optional_header_raw = load_bytes<WORD>(file, nt_header_offset + 4 + 0x10); // file header + 0x10
if(!size_of_optional_header_raw) {
return std::move(size_of_optional_header_raw).unwrap_error();
}
WORD size_of_optional_header = pe_byteswap_if_needed(size_of_optional_header_raw.unwrap_value());
if(size_of_optional_header == 0) {
return internal_error("Unexpected optional header size for PE file");
}
auto optional_header_magic_raw = load_bytes<WORD>(file, nt_header_offset + 0x18); // optional header + 0x0
if(!optional_header_magic_raw) {
return std::move(optional_header_magic_raw).unwrap_error();
}
WORD optional_header_magic = pe_byteswap_if_needed(optional_header_magic_raw.unwrap_value());
VERIFY(
optional_header_magic == IMAGE_NT_OPTIONAL_HDR_MAGIC,
("PE file does not match expected bit-mode " + std::string(object_path)).c_str()
);
// finally get image base
if(optional_header_magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
// 32 bit
auto bytes = load_bytes<DWORD>(file, nt_header_offset + 0x18 + 0x1c); // optional header + 0x1c
if(!bytes) {
return std::move(bytes).unwrap_error();
}
return to<std::uintptr_t>(pe_byteswap_if_needed(bytes.unwrap_value()));
} else {
// 64 bit
// I get an "error: 'QWORD' was not declared in this scope" for some reason when using QWORD
auto bytes = load_bytes<std::uint64_t>(file, nt_header_offset + 0x18 + 0x18); // optional header + 0x18
if(!bytes) {
return std::move(bytes).unwrap_error();
}
return to<std::uintptr_t>(pe_byteswap_if_needed(bytes.unwrap_value()));
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,19 @@
#ifndef PE_HPP
#define PE_HPP
#include "platform/platform.hpp"
#include "utils/utils.hpp"
#if IS_WINDOWS
#include <cstdint>
#include <string>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
Result<std::uintptr_t, internal_error> pe_get_module_image_base(cstring_view object_path);
}
CPPTRACE_END_NAMESPACE
#endif
#endif

View File

@ -0,0 +1,74 @@
#include "binary/safe_dl.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include "platform/program_name.hpp"
#include <string>
#include <vector>
#include <unordered_map>
#include <cstring>
#include <iostream>
#ifdef CPPTRACE_HAS_DL_FIND_OBJECT
#if IS_LINUX || IS_APPLE
#include <unistd.h>
#include <dlfcn.h>
#include <link.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
void get_safe_object_frame(frame_ptr address, safe_object_frame* out) {
out->raw_address = address;
dl_find_object result;
if(_dl_find_object(reinterpret_cast<void*>(address), &result) == 0) { // thread-safe, signal-safe
out->address_relative_to_object_start = address - to_frame_ptr(result.dlfo_link_map->l_addr);
if(result.dlfo_link_map->l_name != nullptr && result.dlfo_link_map->l_name[0] != 0) {
std::size_t path_length = std::strlen(result.dlfo_link_map->l_name);
std::memcpy(
out->object_path,
result.dlfo_link_map->l_name,
std::min(path_length + 1, std::size_t(CPPTRACE_PATH_MAX + 1))
);
} else {
// empty l_name, this means it's the currently running executable
memset(out->object_path, 0, CPPTRACE_PATH_MAX + 1);
// signal-safe
auto res = readlink("/proc/self/exe", out->object_path, CPPTRACE_PATH_MAX);
if(res == -1) {
// error handling?
}
// TODO: Special handling for /proc/pid/exe unlink edge case
}
} else {
out->address_relative_to_object_start = 0;
out->object_path[0] = 0;
}
// TODO: Handle this part of the documentation?
// The address can be a code address or data address. On architectures using function descriptors, no attempt is
// made to decode the function descriptor. Depending on how these descriptors are implemented, _dl_find_object
// may return the object that defines the function descriptor (and not the object that contains the code
// implementing the function), or fail to find any object at all.
}
bool has_get_safe_object_frame() {
return true;
}
}
CPPTRACE_END_NAMESPACE
#else
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
void get_safe_object_frame(frame_ptr address, safe_object_frame* out) {
out->raw_address = address;
out->address_relative_to_object_start = 0;
out->object_path[0] = 0;
}
bool has_get_safe_object_frame() {
return false;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,14 @@
#ifndef SAFE_DL_HPP
#define SAFE_DL_HPP
#include "utils/common.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
void get_safe_object_frame(frame_ptr address, safe_object_frame* out);
bool has_get_safe_object_frame();
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,321 @@
#include <cpptrace/cpptrace.hpp>
#include <cpptrace/formatting.hpp>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <fstream>
#include "cpptrace/basic.hpp"
#include "jit/jit_objects.hpp"
#include "symbols/symbols.hpp"
#include "unwind/unwind.hpp"
#include "demangle/demangle.hpp"
#include "utils/common.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
#include "binary/object.hpp"
#include "binary/safe_dl.hpp"
#include "snippets/snippet.hpp"
#include "options.hpp"
CPPTRACE_BEGIN_NAMESPACE
CPPTRACE_FORCE_NO_INLINE
raw_trace raw_trace::current(std::size_t skip) {
try { // try/catch can never be hit but it's needed to prevent TCO
return generate_raw_trace(skip + 1);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return raw_trace{};
}
}
CPPTRACE_FORCE_NO_INLINE
raw_trace raw_trace::current(std::size_t skip, std::size_t max_depth) {
try { // try/catch can never be hit but it's needed to prevent TCO
return generate_raw_trace(skip + 1, max_depth);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return raw_trace{};
}
}
object_trace raw_trace::resolve_object_trace() const {
try {
return object_trace{detail::get_frames_object_info(frames)};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return object_trace{};
}
}
stacktrace raw_trace::resolve() const {
try {
std::vector<stacktrace_frame> trace = detail::resolve_frames(frames);
for(auto& frame : trace) {
frame.symbol = detail::demangle(frame.symbol, true);
}
return {std::move(trace)};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return stacktrace{};
}
}
void raw_trace::clear() {
frames.clear();
}
bool raw_trace::empty() const noexcept {
return frames.empty();
}
CPPTRACE_FORCE_NO_INLINE
object_trace object_trace::current(std::size_t skip) {
try { // try/catch can never be hit but it's needed to prevent TCO
return generate_object_trace(skip + 1);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return object_trace{};
}
}
CPPTRACE_FORCE_NO_INLINE
object_trace object_trace::current(std::size_t skip, std::size_t max_depth) {
try { // try/catch can never be hit but it's needed to prevent TCO
return generate_object_trace(skip + 1, max_depth);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return object_trace{};
}
}
stacktrace object_trace::resolve() const {
try {
std::vector<stacktrace_frame> trace = detail::resolve_frames(frames);
for(auto& frame : trace) {
frame.symbol = detail::demangle(frame.symbol, true);
}
return {std::move(trace)};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return stacktrace();
}
}
void object_trace::clear() {
frames.clear();
}
bool object_trace::empty() const noexcept {
return frames.empty();
}
object_frame stacktrace_frame::get_object_info() const {
return detail::get_frame_object_info(raw_address);
}
std::string stacktrace_frame::to_string() const {
return to_string(false);
}
std::string stacktrace_frame::to_string(bool color) const {
return get_default_formatter().format(*this, color);
}
std::ostream& operator<<(std::ostream& stream, const stacktrace_frame& frame) {
return stream << frame.to_string();
}
CPPTRACE_FORCE_NO_INLINE
stacktrace stacktrace::current(std::size_t skip) {
try { // try/catch can never be hit but it's needed to prevent TCO
return generate_trace(skip + 1);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return stacktrace{};
}
}
CPPTRACE_FORCE_NO_INLINE
stacktrace stacktrace::current(std::size_t skip, std::size_t max_depth) {
try { // try/catch can never be hit but it's needed to prevent TCO
return generate_trace(skip + 1, max_depth);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return stacktrace{};
}
}
void stacktrace::print() const {
get_default_formatter().print(*this);
}
void stacktrace::print(std::ostream& stream) const {
get_default_formatter().print(stream, *this);
}
void stacktrace::print(std::ostream& stream, bool color) const {
get_default_formatter().print(stream, *this, color);
}
namespace detail {
const formatter& get_default_snippet_formatter() {
static formatter snippet_formatter = formatter{}.snippets(true);
return snippet_formatter;
}
}
void stacktrace::print_with_snippets() const {
detail::get_default_snippet_formatter().print(*this);
}
void stacktrace::print_with_snippets(std::ostream& stream) const {
detail::get_default_snippet_formatter().print(stream, *this);
}
void stacktrace::print_with_snippets(std::ostream& stream, bool color) const {
detail::get_default_snippet_formatter().print(stream, *this, color);
}
void stacktrace::clear() {
frames.clear();
}
bool stacktrace::empty() const noexcept {
return frames.empty();
}
std::string stacktrace::to_string(bool color) const {
return get_default_formatter().format(*this, color);
}
std::ostream& operator<<(std::ostream& stream, const stacktrace& trace) {
get_default_formatter().print(stream, trace);
return stream;
}
CPPTRACE_FORCE_NO_INLINE
raw_trace generate_raw_trace(std::size_t skip) {
try {
return raw_trace{detail::capture_frames(skip + 1, SIZE_MAX)};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return raw_trace{};
}
}
CPPTRACE_FORCE_NO_INLINE
raw_trace generate_raw_trace(std::size_t skip, std::size_t max_depth) {
try {
return raw_trace{detail::capture_frames(skip + 1, max_depth)};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return raw_trace{};
}
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_generate_raw_trace(frame_ptr* buffer, std::size_t size, std::size_t skip) {
try { // try/catch can never be hit but it's needed to prevent TCO
return detail::safe_capture_frames(buffer, size, skip + 1, SIZE_MAX);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return 0;
}
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_generate_raw_trace(
frame_ptr* buffer,
std::size_t size,
std::size_t skip,
std::size_t max_depth
) {
try { // try/catch can never be hit but it's needed to prevent TCO
return detail::safe_capture_frames(buffer, size, skip + 1, max_depth);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return 0;
}
}
CPPTRACE_FORCE_NO_INLINE
object_trace generate_object_trace(std::size_t skip) {
try {
return object_trace{detail::get_frames_object_info(detail::capture_frames(skip + 1, SIZE_MAX))};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return object_trace{};
}
}
CPPTRACE_FORCE_NO_INLINE
object_trace generate_object_trace(std::size_t skip, std::size_t max_depth) {
try {
return object_trace{detail::get_frames_object_info(detail::capture_frames(skip + 1, max_depth))};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return object_trace{};
}
}
CPPTRACE_FORCE_NO_INLINE
stacktrace generate_trace(std::size_t skip) {
try { // try/catch can never be hit but it's needed to prevent TCO
return generate_trace(skip + 1, SIZE_MAX);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return stacktrace{};
}
}
CPPTRACE_FORCE_NO_INLINE
stacktrace generate_trace(std::size_t skip, std::size_t max_depth) {
try {
std::vector<frame_ptr> frames = detail::capture_frames(skip + 1, max_depth);
std::vector<stacktrace_frame> trace = detail::resolve_frames(frames);
for(auto& frame : trace) {
frame.symbol = detail::demangle(frame.symbol, true);
}
return {std::move(trace)};
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return stacktrace();
}
}
object_frame safe_object_frame::resolve() const {
return detail::resolve_safe_object_frame(*this);
}
void get_safe_object_frame(frame_ptr address, safe_object_frame* out) {
detail::get_safe_object_frame(address, out);
}
bool can_signal_safe_unwind() {
return detail::has_safe_unwind();
}
bool can_get_safe_object_frame() {
return detail::has_get_safe_object_frame();
}
void register_jit_object(const char* ptr, std::size_t size) {
detail::register_jit_object(ptr, size);
}
void unregister_jit_object(const char* ptr) {
detail::unregister_jit_object(ptr);
}
void clear_all_jit_objects() {
detail::clear_all_jit_objects();
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,109 @@
module;
#include <cpptrace/basic.hpp>
#include <cpptrace/cpptrace.hpp>
#include <cpptrace/exceptions.hpp>
#include <cpptrace/formatting.hpp>
#include <cpptrace/forward.hpp>
#include <cpptrace/from_current.hpp>
export module cpptrace;
CPPTRACE_BEGIN_NAMESPACE
// cpptrace/basic
export using cpptrace::raw_trace;
export using cpptrace::object_frame;
export using cpptrace::object_trace;
export using cpptrace::nullable;
export using cpptrace::stacktrace_frame;
export using cpptrace::stacktrace;
export using cpptrace::generate_raw_trace;
export using cpptrace::generate_object_trace;
export using cpptrace::generate_trace;
export using cpptrace::safe_generate_raw_trace;
export using cpptrace::safe_object_frame;
export using cpptrace::can_get_safe_object_frame;
export using cpptrace::can_signal_safe_unwind;
export using cpptrace::can_get_safe_object_frame;
export using cpptrace::register_jit_object;
export using cpptrace::unregister_jit_object;
export using cpptrace::clear_all_jit_objects;
// cpptrace/exceptions
export using cpptrace::exception;
export using cpptrace::lazy_exception;
export using cpptrace::exception_with_message;
export using cpptrace::logic_error;
export using cpptrace::domain_error;
export using cpptrace::invalid_argument;
export using cpptrace::length_error;
export using cpptrace::out_of_range;
export using cpptrace::runtime_error;
export using cpptrace::range_error;
export using cpptrace::overflow_error;
export using cpptrace::underflow_error;
export using cpptrace::nested_exception;
export using cpptrace::system_error;
export using cpptrace::rethrow_and_wrap_if_needed;
// cpptrace/formatting
export using cpptrace::basename;
export using cpptrace::prettify_symbol;
export using cpptrace::formatter;
export using cpptrace::get_default_formatter;
// cpptrace/forward
export using cpptrace::frame_ptr;
// cpptrace/from_current.hpp
export using cpptrace::raw_trace_from_current_exception;
export using cpptrace::from_current_exception;
export using cpptrace::raw_trace_from_current_exception_rethrow;
export using cpptrace::from_current_exception_rethrow;
export using cpptrace::current_exception_was_rethrown;
export using cpptrace::rethrow;
export using cpptrace::clear_current_exception_traces;
export using cpptrace::try_catch;
namespace detail {
#ifdef _MSC_VER
export using cpptrace::detail::argument;
export using cpptrace::detail::exception_filter;
#else
export using cpptrace::detail::unwind_interceptor;
export using cpptrace::detail::unwind_interceptor_for;
export using cpptrace::detail::nop;
#endif
}
// cpptrace/io
export using cpptrace::operator<<; // FIXME: make hidden friend
// cpptrace/utils
export using cpptrace::demangle;
export using cpptrace::prune_symbol;
export using cpptrace::get_snippet;
export using cpptrace::isatty;
export using cpptrace::stdin_fileno;
export using cpptrace::stderr_fileno;
export using cpptrace::stdout_fileno;
export using cpptrace::register_terminate_handler;
export using cpptrace::absorb_trace_exceptions;
export using cpptrace::enable_inlined_call_resolution;
export using cpptrace::cache_mode;
export using cpptrace::log_level;
export using cpptrace::set_log_level;
export using cpptrace::set_log_callback;
export using cpptrace::use_default_stderr_logger;
export using cpptrace::to_string;
export using cpptrace::cache_mode;
namespace experimental {
export using cpptrace::experimental::set_cache_mode;
export using cpptrace::experimental::set_dwarf_resolver_line_table_cache_size;
export using cpptrace::experimental::set_dwarf_resolver_disable_aranges;
}
#ifdef _WIN32
export using cpptrace::load_symbols_for_file;
#endif
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,442 @@
#include <ctrace/ctrace.h>
#include <cpptrace/cpptrace.hpp>
#include <algorithm>
#include "symbols/symbols.hpp"
#include "unwind/unwind.hpp"
#include "demangle/demangle.hpp"
#include "platform/exception_type.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include "binary/object.hpp"
#include "binary/safe_dl.hpp"
#include "utils/string_view.hpp"
#define ESC "\033["
#define RESET ESC "0m"
#define RED ESC "31m"
#define GREEN ESC "32m"
#define YELLOW ESC "33m"
#define BLUE ESC "34m"
#define MAGENTA ESC "35m"
#define CYAN ESC "36m"
#if defined(__GNUC__) && ((__GNUC__ > 2) || (__GNUC__ == 2 && __GNUC_MINOR__ >= 6))
# define CTRACE_GNU_FORMAT(...) __attribute__((format(__VA_ARGS__)))
#elif defined(__clang__)
// Probably requires llvm >3.5? Not exactly sure.
# define CTRACE_GNU_FORMAT(...) __attribute__((format(__VA_ARGS__)))
#else
# define CTRACE_GNU_FORMAT(...)
#endif
#if defined(__clang__)
# define CTRACE_FORMAT_PROLOGUE \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wformat-security\"")
# define CTRACE_FORMAT_EPILOGUE \
_Pragma("clang diagnostic pop")
#elif defined(__GNUC_MINOR__)
# define CTRACE_FORMAT_PROLOGUE \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wformat-security\"")
# define CTRACE_FORMAT_EPILOGUE \
_Pragma("GCC diagnostic pop")
#else
# define CTRACE_FORMAT_PROLOGUE
# define CTRACE_FORMAT_EPILOGUE
#endif
namespace ctrace {
static constexpr std::uint32_t invalid_pos = ~0U;
CTRACE_FORMAT_PROLOGUE
template <typename...Args>
CTRACE_GNU_FORMAT(printf, 2, 0)
static void ffprintf(std::FILE* f, const char fmt[], Args&&...args) {
(void)std::fprintf(f, fmt, args...);
(void)fflush(f);
}
CTRACE_FORMAT_EPILOGUE
static bool is_empty(std::uint32_t pos) noexcept {
return pos == invalid_pos;
}
static bool is_empty(const char* str) noexcept {
return !str || std::char_traits<char>::length(str) == 0;
}
static ctrace_owning_string generate_owning_string(cpptrace::detail::string_view raw_string) noexcept {
// Returns length to the null terminator.
char* new_string = new char[raw_string.size() + 1];
std::char_traits<char>::copy(new_string, raw_string.data(), raw_string.size());
new_string[raw_string.size()] = '\0';
return { new_string };
}
static void free_owning_string(const char* owned_string) noexcept {
if(!owned_string) return; // Not necessary but eh
delete[] owned_string;
}
static void free_owning_string(ctrace_owning_string& owned_string) noexcept {
free_owning_string(owned_string.data);
}
static ctrace_object_frame convert_object_frame(const cpptrace::object_frame& frame) {
const char* new_path = generate_owning_string(frame.object_path).data;
return { frame.raw_address, frame.object_address, new_path };
}
static ctrace_object_trace c_convert(const std::vector<cpptrace::object_frame>& trace) {
std::size_t count = trace.size();
auto* frames = new ctrace_object_frame[count];
std::transform(trace.begin(), trace.end(), frames, convert_object_frame);
return { frames, count };
}
static ctrace_stacktrace_frame convert_stacktrace_frame(const cpptrace::stacktrace_frame& frame) {
ctrace_stacktrace_frame new_frame;
new_frame.raw_address = frame.raw_address;
new_frame.object_address = frame.object_address;
new_frame.line = frame.line.value_or(invalid_pos);
new_frame.column = frame.column.value_or(invalid_pos);
new_frame.filename = generate_owning_string(frame.filename).data;
new_frame.symbol = generate_owning_string(cpptrace::detail::demangle(frame.symbol, true)).data;
new_frame.is_inline = ctrace_bool(frame.is_inline);
return new_frame;
}
static cpptrace::stacktrace_frame convert_stacktrace_frame(const ctrace_stacktrace_frame& frame) {
using nullable_type = cpptrace::nullable<std::uint32_t>;
static constexpr auto null_v = nullable_type::null().raw_value;
cpptrace::stacktrace_frame new_frame;
new_frame.raw_address = frame.raw_address;
new_frame.object_address = frame.object_address;
new_frame.line = nullable_type{is_empty(frame.line) ? null_v : frame.line};
new_frame.column = nullable_type{is_empty(frame.column) ? null_v : frame.column};
new_frame.filename = frame.filename;
new_frame.symbol = frame.symbol;
new_frame.is_inline = bool(frame.is_inline);
return new_frame;
}
static ctrace_stacktrace c_convert(const std::vector<cpptrace::stacktrace_frame>& trace) {
std::size_t count = trace.size();
auto* frames = new ctrace_stacktrace_frame[count];
std::transform(
trace.begin(),
trace.end(), frames,
static_cast<ctrace_stacktrace_frame(*)(const cpptrace::stacktrace_frame&)>(convert_stacktrace_frame)
);
return { frames, count };
}
static cpptrace::stacktrace cpp_convert(const ctrace_stacktrace* ptrace) {
if(!ptrace || !ptrace->frames) {
return { };
}
std::vector<cpptrace::stacktrace_frame> new_frames;
new_frames.reserve(ptrace->count);
for(std::size_t i = 0; i < ptrace->count; ++i) {
new_frames.push_back(convert_stacktrace_frame(ptrace->frames[i]));
}
return cpptrace::stacktrace{std::move(new_frames)};
}
}
extern "C" {
// ctrace::string
ctrace_owning_string ctrace_generate_owning_string(const char* raw_string) {
return ctrace::generate_owning_string(raw_string);
}
void ctrace_free_owning_string(ctrace_owning_string* string) {
if(!string) {
return;
}
ctrace::free_owning_string(*string);
string->data = nullptr;
}
// ctrace::generation:
CTRACE_FORCE_NO_INLINE
ctrace_raw_trace ctrace_generate_raw_trace(size_t skip, size_t max_depth) {
try {
std::vector<cpptrace::frame_ptr> trace = cpptrace::detail::capture_frames(skip + 1, max_depth);
std::size_t count = trace.size();
auto* frames = new ctrace_frame_ptr[count];
std::copy(trace.data(), trace.data() + count, frames);
return { frames, count };
} catch(...) {
// Don't check rethrow condition, it's risky.
return { nullptr, 0 };
}
}
CTRACE_FORCE_NO_INLINE
ctrace_object_trace ctrace_generate_object_trace(size_t skip, size_t max_depth) {
try {
std::vector<cpptrace::object_frame> trace = cpptrace::detail::get_frames_object_info(
cpptrace::detail::capture_frames(skip + 1, max_depth)
);
return ctrace::c_convert(trace);
} catch(...) { // NOSONAR
// Don't check rethrow condition, it's risky.
return { nullptr, 0 };
}
}
CTRACE_FORCE_NO_INLINE
ctrace_stacktrace ctrace_generate_trace(size_t skip, size_t max_depth) {
try {
std::vector<cpptrace::frame_ptr> frames = cpptrace::detail::capture_frames(skip + 1, max_depth);
std::vector<cpptrace::stacktrace_frame> trace = cpptrace::detail::resolve_frames(frames);
return ctrace::c_convert(trace);
} catch(...) { // NOSONAR
// Don't check rethrow condition, it's risky.
return { nullptr, 0 };
}
}
// ctrace::freeing:
void ctrace_free_raw_trace(ctrace_raw_trace* trace) {
if(!trace) {
return;
}
ctrace_frame_ptr* frames = trace->frames;
delete[] frames;
trace->frames = nullptr;
trace->count = 0;
}
void ctrace_free_object_trace(ctrace_object_trace* trace) {
if(!trace || !trace->frames) {
return;
}
ctrace_object_frame* frames = trace->frames;
for(std::size_t i = 0; i < trace->count; ++i) {
const char* path = frames[i].obj_path;
ctrace::free_owning_string(path);
}
delete[] frames;
trace->frames = nullptr;
trace->count = 0;
}
void ctrace_free_stacktrace(ctrace_stacktrace* trace) {
if(!trace || !trace->frames) {
return;
}
ctrace_stacktrace_frame* frames = trace->frames;
for(std::size_t i = 0; i < trace->count; ++i) {
ctrace::free_owning_string(frames[i].filename);
ctrace::free_owning_string(frames[i].symbol);
}
delete[] frames;
trace->frames = nullptr;
trace->count = 0;
}
// ctrace::resolve:
ctrace_stacktrace ctrace_resolve_raw_trace(const ctrace_raw_trace* trace) {
if(!trace || !trace->frames) {
return { nullptr, 0 };
}
try {
std::vector<cpptrace::frame_ptr> frames(trace->count, 0);
std::copy(trace->frames, trace->frames + trace->count, frames.begin());
std::vector<cpptrace::stacktrace_frame> resolved = cpptrace::detail::resolve_frames(frames);
return ctrace::c_convert(resolved);
} catch(...) { // NOSONAR
// Don't check rethrow condition, it's risky.
return { nullptr, 0 };
}
}
ctrace_object_trace ctrace_resolve_raw_trace_to_object_trace(const ctrace_raw_trace* trace) {
if(!trace || !trace->frames) {
return { nullptr, 0 };
}
try {
std::vector<cpptrace::frame_ptr> frames(trace->count, 0);
std::copy(trace->frames, trace->frames + trace->count, frames.begin());
std::vector<cpptrace::object_frame> obj = cpptrace::detail::get_frames_object_info(frames);
return ctrace::c_convert(obj);
} catch(...) { // NOSONAR
// Don't check rethrow condition, it's risky.
return { nullptr, 0 };
}
}
ctrace_stacktrace ctrace_resolve_object_trace(const ctrace_object_trace* trace) {
if(!trace || !trace->frames) {
return { nullptr, 0 };
}
try {
std::vector<cpptrace::frame_ptr> frames(trace->count, 0);
std::transform(
trace->frames,
trace->frames + trace->count,
frames.begin(),
[] (const ctrace_object_frame& frame) -> cpptrace::frame_ptr {
return frame.raw_address;
}
);
std::vector<cpptrace::stacktrace_frame> resolved = cpptrace::detail::resolve_frames(frames);
return ctrace::c_convert(resolved);
} catch(...) { // NOSONAR
// Don't check rethrow condition, it's risky.
return { nullptr, 0 };
}
}
// ctrace::safe:
size_t ctrace_safe_generate_raw_trace(ctrace_frame_ptr* buffer, size_t size, size_t skip, size_t max_depth) {
return cpptrace::safe_generate_raw_trace(buffer, size, skip, max_depth);
}
void ctrace_get_safe_object_frame(ctrace_frame_ptr address, ctrace_safe_object_frame* out) {
// TODO: change this?
static_assert(sizeof(cpptrace::safe_object_frame) == sizeof(ctrace_safe_object_frame), "");
cpptrace::get_safe_object_frame(address, reinterpret_cast<cpptrace::safe_object_frame*>(out));
}
ctrace_bool ctrace_can_signal_safe_unwind() {
return cpptrace::can_signal_safe_unwind();
}
ctrace_bool ctrace_can_get_safe_object_frame(void) {
return cpptrace::can_get_safe_object_frame();
}
// ctrace::io:
ctrace_owning_string ctrace_stacktrace_to_string(const ctrace_stacktrace* trace, ctrace_bool use_color) {
if(!trace || !trace->frames) {
return ctrace::generate_owning_string("<empty trace>");
}
auto cpp_trace = ctrace::cpp_convert(trace);
std::string trace_string = cpp_trace.to_string(bool(use_color));
return ctrace::generate_owning_string(trace_string);
}
void ctrace_print_stacktrace(const ctrace_stacktrace* trace, FILE* to, ctrace_bool use_color) {
if(
use_color && (
(to == stdout && cpptrace::isatty(cpptrace::stdout_fileno)) ||
(to == stderr && cpptrace::isatty(cpptrace::stderr_fileno))
)
) {
cpptrace::detail::enable_virtual_terminal_processing_if_needed();
}
ctrace::ffprintf(to, "Stack trace (most recent call first):\n");
if(trace->count == 0 || !trace->frames) {
ctrace::ffprintf(to, "<empty trace>\n");
return;
}
const auto reset = use_color ? ESC "0m" : "";
const auto green = use_color ? ESC "32m" : "";
const auto yellow = use_color ? ESC "33m" : "";
const auto blue = use_color ? ESC "34m" : "";
const auto frame_number_width = cpptrace::detail::n_digits(cpptrace::detail::to<unsigned>(trace->count - 1));
ctrace_stacktrace_frame* frames = trace->frames;
for(std::size_t i = 0; i < trace->count; ++i) {
static constexpr auto ptr_len = 2 * sizeof(cpptrace::frame_ptr);
ctrace::ffprintf(to, "#%-*llu ", int(frame_number_width), i);
if(frames[i].is_inline) {
(void)std::fprintf(to, "%*s",
int(ptr_len + 2),
"(inlined)");
} else {
(void)std::fprintf(to, "%s0x%0*llx%s",
blue,
int(ptr_len),
cpptrace::detail::to_ull(frames[i].raw_address),
reset);
}
if(!ctrace::is_empty(frames[i].symbol)) {
(void)std::fprintf(to, " in %s%s%s",
yellow,
frames[i].symbol,
reset);
}
if(!ctrace::is_empty(frames[i].filename)) {
(void)std::fprintf(to, " at %s%s%s",
green,
frames[i].filename,
reset);
if(ctrace::is_empty(frames[i].line)) {
ctrace::ffprintf(to, "\n");
continue;
}
(void)std::fprintf(to, ":%s%llu%s",
blue,
cpptrace::detail::to_ull(frames[i].line),
reset);
if(ctrace::is_empty(frames[i].column)) {
ctrace::ffprintf(to, "\n");
continue;
}
(void)std::fprintf(to, ":%s%llu%s",
blue,
cpptrace::detail::to_ull(frames[i].column),
reset);
}
// always print newline at end :M
ctrace::ffprintf(to, "\n");
}
}
// utility::demangle:
ctrace_owning_string ctrace_demangle(const char* mangled) {
if(!mangled) {
return ctrace::generate_owning_string("");
}
std::string demangled = cpptrace::demangle(mangled);
return ctrace::generate_owning_string(demangled);
}
// utility::io
int ctrace_stdin_fileno(void) {
return cpptrace::stdin_fileno;
}
int ctrace_stderr_fileno(void) {
return cpptrace::stderr_fileno;
}
int ctrace_stdout_fileno(void) {
return cpptrace::stdout_fileno;
}
ctrace_bool ctrace_isatty(int fd) {
return cpptrace::isatty(fd);
}
// utility::cache:
void ctrace_set_cache_mode(ctrace_cache_mode mode) {
static constexpr auto cache_max = cpptrace::cache_mode::prioritize_speed;
if(mode > unsigned(cache_max)) {
return;
}
auto cache_mode = static_cast<cpptrace::cache_mode>(mode);
cpptrace::experimental::set_cache_mode(cache_mode);
}
void ctrace_enable_inlined_call_resolution(ctrace_bool enable) {
cpptrace::enable_inlined_call_resolution(enable);
}
ctrace_object_frame ctrace_get_object_info(const ctrace_stacktrace_frame* frame) {
try {
cpptrace::object_frame new_frame = cpptrace::detail::get_frame_object_info(frame->raw_address);
return ctrace::convert_object_frame(new_frame);
} catch(...) {
return {0, 0, nullptr};
}
}
}

View File

@ -0,0 +1,14 @@
#ifndef DEMANGLE_HPP
#define DEMANGLE_HPP
#include <cpptrace/forward.hpp>
#include <string>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::string demangle(const std::string& name, bool check_prefix);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,61 @@
#include "utils/microfmt.hpp"
#ifdef CPPTRACE_DEMANGLE_WITH_CXXABI
#include "demangle/demangle.hpp"
#include "utils/utils.hpp"
#include <cxxabi.h>
#include <cstdlib>
#include <functional>
#include <string>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::string demangle(const std::string& name, bool check_prefix) {
// https://itanium-cxx-abi.github.io/cxx-abi/abi.html#demangler
// Check both _Z and __Z, apple prefixes all symbols with an underscore
if(check_prefix && !(starts_with(name, "_Z") || starts_with(name, "__Z"))) {
return name;
}
// Apple clang demangles __Z just fine but gcc doesn't, so just offset the leading underscore
std::size_t offset = 0;
if(starts_with(name, "__Z")) {
offset = 1;
}
// Mangled names don't have spaces, we might add a space and some extra info somewhere but we still want it to
// be demanglable. Look for a space, if there is one swap it with a null terminator briefly.
auto end = name.find(' ');
std::string name_copy;
std::reference_wrapper<const std::string> to_demangle = name;
std::string rest;
if(end != std::string::npos) {
name_copy = name.substr(0, end);
rest = name.substr(end);
to_demangle = name_copy;
}
// presumably thread-safe
// it appears safe to pass nullptr for status however the docs don't explicitly say it's safe so I don't
// want to rely on it
int status;
auto demangled = raii_wrap(
abi::__cxa_demangle(to_demangle.get().c_str() + offset, nullptr, nullptr, &status),
[] (char* str) { std::free(str); }
);
// demangled will always be nullptr on non-zero status, and if __cxa_demangle ever fails for any reason
// we'll just quietly return the mangled name
if(demangled.get()) {
std::string str = demangled.get();
if(!rest.empty()) {
str += rest;
}
return str;
} else {
return name;
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,15 @@
#ifdef CPPTRACE_DEMANGLE_WITH_NOTHING
#include "demangle/demangle.hpp"
#include <string>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::string demangle(const std::string& name, bool) {
return name;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,31 @@
#ifdef CPPTRACE_DEMANGLE_WITH_WINAPI
#include "demangle/demangle.hpp"
#include "platform/dbghelp_utils.hpp"
#include <string>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <dbghelp.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::string demangle(const std::string& name, bool) {
// Dbghelp is is single-threaded, so acquire a lock.
auto lock = get_dbghelp_lock();
char buffer[500];
auto ret = UnDecorateSymbolName(name.c_str(), buffer, sizeof(buffer) - 1, 0);
if(ret == 0) {
return name;
} else {
buffer[ret] = 0; // just in case, ms' docs unclear if null terminator inserted
return buffer;
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,187 @@
#include <cpptrace/exceptions.hpp>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <exception>
#include <new>
#include <stdexcept>
#include <string>
#include "platform/exception_type.hpp"
#include "utils/common.hpp"
#include "options.hpp"
#include "logging.hpp"
#include "utils/error.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
lazy_trace_holder::lazy_trace_holder(const lazy_trace_holder& other) : resolved(other.resolved) {
if(other.resolved) {
new (&resolved_trace) stacktrace(other.resolved_trace);
} else {
new (&trace) raw_trace(other.trace);
}
}
lazy_trace_holder::lazy_trace_holder(lazy_trace_holder&& other) noexcept : resolved(other.resolved) {
if(other.resolved) {
new (&resolved_trace) stacktrace(std::move(other.resolved_trace));
} else {
new (&trace) raw_trace(std::move(other.trace));
}
}
lazy_trace_holder& lazy_trace_holder::operator=(const lazy_trace_holder& other) {
clear();
resolved = other.resolved;
if(other.resolved) {
new (&resolved_trace) stacktrace(other.resolved_trace);
} else {
new (&trace) raw_trace(other.trace);
}
return *this;
}
lazy_trace_holder& lazy_trace_holder::operator=(lazy_trace_holder&& other) noexcept {
clear();
resolved = other.resolved;
if(other.resolved) {
new (&resolved_trace) stacktrace(std::move(other.resolved_trace));
} else {
new (&trace) raw_trace(std::move(other.trace));
}
return *this;
}
lazy_trace_holder::~lazy_trace_holder() {
clear();
}
// access
const raw_trace& lazy_trace_holder::get_raw_trace() const {
if(resolved) {
throw std::logic_error(
"cpptrace::detail::lazy_trace_holder::get_resolved_trace called on resolved holder"
);
}
return trace;
}
stacktrace& lazy_trace_holder::get_resolved_trace() {
if(!resolved) {
raw_trace old_trace = std::move(trace);
*this = lazy_trace_holder(stacktrace{});
try {
if(!old_trace.empty()) {
resolved_trace = old_trace.resolve();
}
} catch(const std::exception& e) {
if(!should_absorb_trace_exceptions()) {
log::error(
"Exception occurred while resolving trace in cpptrace::detail::lazy_trace_holder: {}",
e.what()
);
}
}
}
return resolved_trace;
}
const stacktrace& lazy_trace_holder::get_resolved_trace() const {
if(!resolved) {
throw std::logic_error(
"cpptrace::detail::lazy_trace_holder::get_resolved_trace called on unresolved const holder"
);
}
return resolved_trace;
}
bool lazy_trace_holder::is_resolved() const {
return resolved;
}
void lazy_trace_holder::clear() {
if(resolved) {
resolved_trace.~stacktrace();
} else {
trace.~raw_trace();
}
}
CPPTRACE_FORCE_NO_INLINE
raw_trace get_raw_trace_and_absorb(std::size_t skip, std::size_t max_depth) {
try {
return generate_raw_trace(skip + 1, max_depth);
} catch(const std::exception& e) {
if(!should_absorb_trace_exceptions()) {
log::error(
"Exception occurred while resolving trace in cpptrace::exception object: {}",
e.what()
);
}
return raw_trace{};
}
}
CPPTRACE_FORCE_NO_INLINE
raw_trace get_raw_trace_and_absorb(std::size_t skip) {
try { // try/catch can never be hit but it's needed to prevent TCO
return get_raw_trace_and_absorb(skip + 1, SIZE_MAX);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return raw_trace{};
}
}
}
const char* lazy_exception::what() const noexcept {
if(what_string.empty()) {
what_string = message() + std::string(":\n") + trace_holder.get_resolved_trace().to_string();
}
return what_string.c_str();
}
const char* lazy_exception::message() const noexcept {
return "cpptrace::lazy_exception";
}
const stacktrace& lazy_exception::trace() const noexcept {
return trace_holder.get_resolved_trace();
}
const char* exception_with_message::message() const noexcept {
return user_message.c_str();
}
system_error::system_error(int error_code, std::string&& message_arg, raw_trace&& trace) noexcept
: runtime_error(
message_arg + ": " + std::error_code(error_code, std::generic_category()).message(),
std::move(trace)
),
ec(std::error_code(error_code, std::generic_category())) {}
const std::error_code& system_error::code() const noexcept {
return ec;
}
const char* nested_exception::message() const noexcept {
if(message_value.empty()) {
try {
std::rethrow_exception(ptr);
} catch(std::exception& e) {
message_value = std::string("Nested exception: ") + e.what();
} catch(...) {
message_value = "Nested exception holding instance of " + detail::exception_type_name();
}
}
return message_value.c_str();
}
std::exception_ptr nested_exception::nested_ptr() const noexcept {
return ptr;
}
CPPTRACE_FORCE_NO_INLINE
void rethrow_and_wrap_if_needed(std::size_t skip) {
try {
std::rethrow_exception(std::current_exception());
} catch(cpptrace::exception&) {
throw; // already a cpptrace::exception
} catch(...) {
throw nested_exception(std::current_exception(), detail::get_raw_trace_and_absorb(skip + 1));
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,498 @@
#include <cpptrace/formatting.hpp>
#include <cpptrace/utils.hpp>
#include "utils/optional.hpp"
#include "utils/utils.hpp"
#include "utils/replace_all.hpp"
#include "snippets/snippet.hpp"
#include <cstdio>
#include <string>
#include <functional>
#include <iostream>
#include <sstream>
#include <regex>
CPPTRACE_BEGIN_NAMESPACE
std::string basename(const std::string& path) {
return detail::basename(path, true);
}
std::string prettify_symbol(std::string symbol) {
// > > -> >> replacement
// could put in analysis:: but the replacement is basic and this is more convenient for
// using in the stringifier too
detail::replace_all_dynamic(symbol, "> >", ">>");
// "," -> ", " and " ," -> ", "
static const std::regex comma_re(R"(\s*,\s*)");
detail::replace_all(symbol, comma_re, ", ");
// class C -> C for msvc
static const std::regex class_re(R"(\b(class|struct)\s+)");
detail::replace_all(symbol, class_re, "");
// `anonymous namespace' -> (anonymous namespace) for msvc
// this brings it in-line with other compilers and prevents any tokenization/highlighting issues
static const std::regex msvc_anonymous_namespace("`anonymous namespace'");
detail::replace_all(symbol, msvc_anonymous_namespace, "(anonymous namespace)");
// rules to replace std::basic_string -> std::string and std::basic_string_view -> std::string
// rule to replace ", std::allocator<whatever>"
static const std::pair<std::regex, std::string> basic_string = {
std::regex(R"(std(::[a-zA-Z0-9_]+)?::basic_string<char)"), "std::string"
};
detail::replace_all_template(symbol, basic_string);
static const std::pair<std::regex, std::string> basic_string_view = {
std::regex(R"(std(::[a-zA-Z0-9_]+)?::basic_string_view<char)"), "std::string_view"
};
detail::replace_all_template(symbol, basic_string_view);
static const std::pair<std::regex, std::string> allocator = {
std::regex(R"(,\s*std(::[a-zA-Z0-9_]+)?::allocator<)"), ""
};
detail::replace_all_template(symbol, allocator);
static const std::pair<std::regex, std::string> default_delete = {
std::regex(R"(,\s*std(::[a-zA-Z0-9_]+)?::default_delete<)"), ""
};
detail::replace_all_template(symbol, default_delete);
// replace std::__cxx11 -> std:: for gcc dual abi
// https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html
detail::replace_all_dynamic(symbol, "std::__cxx11::", "std::");
return symbol;
}
class formatter::impl {
struct {
std::string header = "Stack trace (most recent call first):";
color_mode color = color_mode::automatic;
address_mode addresses = address_mode::raw;
path_mode paths = path_mode::full;
bool snippets = false;
bool break_before_filename = false;
int context_lines = 2;
bool columns = true;
symbol_mode symbols = symbol_mode::full;
bool show_filtered_frames = true;
std::function<bool(const stacktrace_frame&)> filter;
std::function<stacktrace_frame(stacktrace_frame)> transform;
} options;
public:
void header(std::string header) {
options.header = std::move(header);
}
void colors(formatter::color_mode mode) {
options.color = mode;
}
void addresses(formatter::address_mode mode) {
options.addresses = mode;
}
void paths(path_mode mode) {
options.paths = mode;
}
void snippets(bool snippets) {
options.snippets = snippets;
}
void snippet_context(int lines) {
options.context_lines = lines;
}
void columns(bool columns) {
options.columns = columns;
}
void symbols(symbol_mode mode) {
options.symbols = mode;
}
void filtered_frame_placeholders(bool show) {
options.show_filtered_frames = show;
}
void filter(std::function<bool(const stacktrace_frame&)> filter) {
options.filter = filter;
}
void transform(std::function<stacktrace_frame(stacktrace_frame)> transform) {
options.transform = std::move(transform);
}
void break_before_filename(bool do_break) {
options.break_before_filename = do_break;
}
std::string format(
const stacktrace_frame& frame,
detail::optional<bool> color_override = detail::nullopt,
size_t filename_indent = 0
) const {
std::ostringstream oss;
print_internal(oss, frame, color_override.value_or(options.color == color_mode::always), filename_indent);
return std::move(oss).str();
}
std::string format(const stacktrace& trace, detail::optional<bool> color_override = detail::nullopt) const {
std::ostringstream oss;
print_internal(oss, trace, color_override.value_or(options.color == color_mode::always));
return std::move(oss).str();
}
void print(const stacktrace_frame& frame, detail::optional<bool> color_override = detail::nullopt) const {
print(std::cout, frame, color_override);
}
void print(
std::ostream& stream,
const stacktrace_frame& frame,
detail::optional<bool> color_override = detail::nullopt,
size_t filename_indent = 0
) const {
print_internal(stream, frame, color_override, filename_indent);
stream << "\n";
}
void print(
std::FILE* file,
const stacktrace_frame& frame,
detail::optional<bool> color_override = detail::nullopt,
size_t filename_indent = 0
) const {
auto str = format(frame, color_override, filename_indent);
str += "\n";
std::fwrite(str.data(), 1, str.size(), file);
}
void print(const stacktrace& trace, detail::optional<bool> color_override = detail::nullopt) const {
print(std::cout, trace, color_override);
}
void print(
std::ostream& stream,
const stacktrace& trace,
detail::optional<bool> color_override = detail::nullopt
) const {
print_internal(stream, trace, color_override);
stream << "\n";
}
void print(
std::FILE* file,
const stacktrace& trace,
detail::optional<bool> color_override = detail::nullopt
) const {
auto str = format(trace, color_override);
str += "\n";
std::fwrite(str.data(), 1, str.size(), file);
}
private:
struct color_setting {
bool color;
color_setting(bool color) : color(color) {}
detail::string_view reset() const {
return color ? RESET : "";
}
detail::string_view green() const {
return color ? GREEN : "";
}
detail::string_view yellow() const {
return color ? YELLOW : "";
}
detail::string_view blue() const {
return color ? BLUE : "";
}
};
bool stream_is_tty(std::ostream& stream) const {
// not great, but it'll have to do
return (&stream == &std::cout && isatty(stdout_fileno))
|| (&stream == &std::cerr && isatty(stderr_fileno));
}
void maybe_ensure_virtual_terminal_processing(std::ostream& stream, bool color) const {
if(color && stream_is_tty(stream)) {
detail::enable_virtual_terminal_processing_if_needed();
}
}
bool should_do_color(std::ostream& stream, detail::optional<bool> color_override) const {
bool do_color = options.color == color_mode::always || color_override.value_or(false);
if(
(options.color == color_mode::automatic || options.color == color_mode::always) &&
(!color_override || color_override.unwrap() != false) &&
stream_is_tty(stream)
) {
do_color = true;
}
return do_color;
}
void print_internal(std::ostream& stream, const stacktrace_frame& input_frame, detail::optional<bool> color_override, size_t col_indent) const {
bool color = should_do_color(stream, color_override);
maybe_ensure_virtual_terminal_processing(stream, color);
detail::optional<stacktrace_frame> transformed_frame;
if(options.transform) {
transformed_frame = options.transform(input_frame);
}
const stacktrace_frame& frame = options.transform ? transformed_frame.unwrap() : input_frame;
write_frame(stream, frame, color, col_indent);
}
void print_internal(std::ostream& stream, const stacktrace& trace, detail::optional<bool> color_override) const {
bool color = should_do_color(stream, color_override);
maybe_ensure_virtual_terminal_processing(stream, color);
write_trace(stream, trace, color);
}
void write_trace(std::ostream& stream, const stacktrace& trace, bool color) const {
if(!options.header.empty()) {
stream << options.header << '\n';
}
std::size_t counter = 0;
const auto& frames = trace.frames;
if(frames.empty()) {
stream << "<empty trace>";
return;
}
const auto frame_number_width = detail::n_digits(static_cast<int>(frames.size()) - 1);
for(size_t i = 0; i < frames.size(); ++i) {
detail::optional<stacktrace_frame> transformed_frame;
if(options.transform) {
transformed_frame = options.transform(frames[i]);
}
const stacktrace_frame& frame = options.transform ? transformed_frame.unwrap() : frames[i];
bool filter_out_frame = options.filter && !options.filter(frame);
if(filter_out_frame && !options.show_filtered_frames) {
counter++;
continue;
}
size_t filename_indent = write_frame_number(stream, frame_number_width, counter);
if(filter_out_frame) {
microfmt::print(stream, "(filtered)");
} else {
write_frame(stream, frame, color, filename_indent);
if(frame.line.has_value() && !frame.filename.empty() && options.snippets) {
auto snippet = detail::get_snippet(
frame.filename,
frame.line.value(),
frame.column,
options.context_lines,
color
);
if(!snippet.empty()) {
stream << '\n';
stream << snippet;
}
}
}
if(i + 1 != frames.size()) {
stream << '\n';
}
counter++;
}
}
/// Write the frame number, and return the number of characters written
size_t write_frame_number(std::ostream& stream, unsigned int frame_number_width, size_t counter) const
{
microfmt::print(stream, "#{<{}} ", frame_number_width, counter);
return 2 + frame_number_width;
}
void write_frame(std::ostream& stream, const stacktrace_frame& frame, color_setting color, size_t col) const {
col += write_address(stream, frame, color);
if(frame.is_inline || options.addresses != address_mode::none) {
stream << ' ';
col += 1;
}
if(!frame.symbol.empty()) {
write_symbol(stream, frame, color);
}
if(!frame.symbol.empty() && !frame.filename.empty()) {
if(options.break_before_filename) {
microfmt::print(stream, "\n{<{}}", col, "");
} else {
stream << ' ';
}
}
if(!frame.filename.empty()) {
write_source_location(stream, frame, color);
}
}
/// Write the address of the frame, return the number of characters written
size_t write_address(std::ostream& stream, const stacktrace_frame& frame, color_setting color) const {
if(frame.is_inline) {
microfmt::print(stream, "{<{}}", 2 * sizeof(frame_ptr) + 2, "(inlined)");
return 2 * sizeof(frame_ptr) + 2;
} else if(options.addresses != address_mode::none) {
auto address = options.addresses == address_mode::raw ? frame.raw_address : frame.object_address;
microfmt::print(stream, "{}0x{>{}:0h}{}", color.blue(), 2 * sizeof(frame_ptr), address, color.reset());
return 2 * sizeof(frame_ptr) + 2;
}
return 0;
}
void write_symbol(std::ostream& stream, const stacktrace_frame& frame, color_setting color) const {
detail::optional<std::string> maybe_stored_string;
detail::string_view symbol;
switch(options.symbols) {
case symbol_mode::full:
symbol = frame.symbol;
break;
case symbol_mode::pruned:
maybe_stored_string = prune_symbol(frame.symbol);
symbol = maybe_stored_string.unwrap();
break;
case symbol_mode::pretty:
maybe_stored_string = prettify_symbol(frame.symbol);
symbol = maybe_stored_string.unwrap();
break;
default:
PANIC("Unhandled symbol mode");
}
microfmt::print(stream, "in {}{}{}", color.yellow(), symbol, color.reset());
}
void write_source_location(std::ostream& stream, const stacktrace_frame& frame, color_setting color) const {
microfmt::print(
stream,
"at {}{}{}",
color.green(),
options.paths == path_mode::full ? frame.filename : detail::basename(frame.filename, true),
color.reset()
);
if(frame.line.has_value()) {
microfmt::print(stream, ":{}{}{}", color.blue(), frame.line.value(), color.reset());
if(frame.column.has_value() && options.columns) {
microfmt::print(stream, ":{}{}{}", color.blue(), frame.column.value(), color.reset());
}
}
}
};
formatter::formatter() : pimpl(new impl) {}
formatter::~formatter() {
delete pimpl;
}
formatter::formatter(formatter&& other) : pimpl(detail::exchange(other.pimpl, nullptr)) {}
formatter::formatter(const formatter& other) : pimpl(new impl(*other.pimpl)) {}
formatter& formatter::operator=(formatter&& other) {
if(pimpl) {
delete pimpl;
}
pimpl = detail::exchange(other.pimpl, nullptr);
return *this;
}
formatter& formatter::operator=(const formatter& other) {
if(pimpl) {
delete pimpl;
}
pimpl = new impl(*other.pimpl);
return *this;
}
formatter& formatter::header(std::string header) {
pimpl->header(std::move(header));
return *this;
}
formatter& formatter::colors(color_mode mode) {
pimpl->colors(mode);
return *this;
}
formatter& formatter::addresses(address_mode mode) {
pimpl->addresses(mode);
return *this;
}
formatter& formatter::paths(path_mode mode) {
pimpl->paths(mode);
return *this;
}
formatter& formatter::snippets(bool snippets) {
pimpl->snippets(snippets);
return *this;
}
formatter& formatter::snippet_context(int lines) {
pimpl->snippet_context(lines);
return *this;
}
formatter& formatter::columns(bool columns) {
pimpl->columns(columns);
return *this;
}
formatter& formatter::symbols(symbol_mode mode) {
pimpl->symbols(mode);
return *this;
}
formatter& formatter::filtered_frame_placeholders(bool show) {
pimpl->filtered_frame_placeholders(show);
return *this;
}
formatter& formatter::filter(std::function<bool(const stacktrace_frame&)> filter) {
pimpl->filter(std::move(filter));
return *this;
}
formatter& formatter::transform(std::function<stacktrace_frame(stacktrace_frame)> transform) {
pimpl->transform(std::move(transform));
return *this;
}
formatter& formatter::break_before_filename(bool do_break) {
pimpl->break_before_filename(do_break);
return *this;
}
std::string formatter::format(const stacktrace_frame& frame) const {
return pimpl->format(frame);
}
std::string formatter::format(const stacktrace_frame& frame, bool color) const {
return pimpl->format(frame, color);
}
std::string formatter::format(const stacktrace_frame& frame, bool color, size_t filename_indent) const {
return pimpl->format(frame, color, filename_indent);
}
std::string formatter::format(const stacktrace& trace) const {
return pimpl->format(trace);
}
std::string formatter::format(const stacktrace& trace, bool color) const {
return pimpl->format(trace, color);
}
void formatter::print(const stacktrace& trace) const {
pimpl->print(trace);
}
void formatter::print(const stacktrace& trace, bool color) const {
pimpl->print(trace, color);
}
void formatter::print(std::ostream& stream, const stacktrace& trace) const {
pimpl->print(stream, trace);
}
void formatter::print(std::ostream& stream, const stacktrace& trace, bool color) const {
pimpl->print(stream, trace, color);
}
void formatter::print(std::FILE* file, const stacktrace& trace) const {
pimpl->print(file, trace);
}
void formatter::print(std::FILE* file, const stacktrace& trace, bool color) const {
pimpl->print(file, trace, color);
}
void formatter::print(const stacktrace_frame& frame) const {
pimpl->print(frame);
}
void formatter::print(const stacktrace_frame& frame, bool color) const {
pimpl->print(frame, color);
}
void formatter::print(std::ostream& stream, const stacktrace_frame& frame) const {
pimpl->print(stream, frame);
}
void formatter::print(std::ostream& stream, const stacktrace_frame& frame, bool color) const {
pimpl->print(stream, frame, color);
}
void formatter::print(std::ostream& stream, const stacktrace_frame& frame, bool color, size_t filename_indent) const {
pimpl->print(stream, frame, color, filename_indent);
}
void formatter::print(std::FILE* file, const stacktrace_frame& frame) const {
pimpl->print(file, frame);
}
void formatter::print(std::FILE* file, const stacktrace_frame& frame, bool color) const {
pimpl->print(file, frame, color);
}
void formatter::print(std::FILE* file, const stacktrace_frame& frame, bool color, size_t filename_indent) const {
pimpl->print(file, frame, color, filename_indent);
}
const formatter& get_default_formatter() {
static formatter formatter;
return formatter;
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,704 @@
#include <cpptrace/cpptrace.hpp>
#include <cpptrace/from_current.hpp>
#include <cstdint>
#include <exception>
#include <system_error>
#include <typeinfo>
#include "platform/platform.hpp"
#include "utils/error.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
#include "logging.hpp"
#include "unwind/unwind.hpp"
#ifndef _MSC_VER
#include <string.h>
#if IS_WINDOWS
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#if IS_APPLE
#include <mach/mach.h>
#ifdef CPPTRACE_HAS_MACH_VM
#include <mach/mach_vm.h>
#endif
#else
#include <fstream>
#include <ios>
#endif
#endif
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
thread_local lazy_trace_holder current_exception_trace;
thread_local lazy_trace_holder saved_rethrow_trace;
bool& get_rethrow_switch() {
static thread_local bool rethrow_switch = false;
return rethrow_switch;
}
void save_current_trace(raw_trace trace) {
if(get_rethrow_switch()) {
saved_rethrow_trace = lazy_trace_holder(std::move(trace));
} else {
current_exception_trace = lazy_trace_holder(std::move(trace));
saved_rethrow_trace = lazy_trace_holder();
}
}
#if defined(_MSC_VER) && defined(CPPTRACE_UNWIND_WITH_DBGHELP)
CPPTRACE_FORCE_NO_INLINE void collect_current_trace(std::size_t skip, EXCEPTION_POINTERS* exception_ptrs) {
try {
#if defined(_M_IX86) || defined(__i386__)
(void)skip; // don't skip any frames, the context record is at the throw point
auto trace = raw_trace{detail::capture_frames(0, SIZE_MAX, exception_ptrs)};
#else
(void)exception_ptrs;
auto trace = raw_trace{detail::capture_frames(skip + 1, SIZE_MAX)};
#endif
save_current_trace(std::move(trace));
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
}
}
#else
CPPTRACE_FORCE_NO_INLINE void collect_current_trace(std::size_t skip) {
try {
auto trace = raw_trace{detail::capture_frames(skip + 1, SIZE_MAX)};
save_current_trace(std::move(trace));
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
}
}
#endif
#ifdef _MSC_VER
// https://www.youtube.com/watch?v=COEv2kq_Ht8
// https://github.com/tpn/pdfs/blob/master/2018%20CppCon%20Unwinding%20the%20Stack%20-%20Exploring%20how%20C%2B%2B%20Exceptions%20work%20on%20Windows%20-%20James%20McNellis.pdf
// https://github.com/ecatmur/stacktrace-from-exception/blob/main/stacktrace-from-exception.cpp
// https://github.com/wine-mirror/wine/blob/7f833db11ffea4f3f4fa07be31d30559aff9c5fb/dlls/msvcrt/except.c#L371
// https://github.com/facebook/folly/blob/d17bf897cb5bbf8f07b122a614e8cffdc38edcde/folly/lang/Exception.cpp
// ClangCL doesn't define ThrowInfo so we use our own
// sources:
// - https://github.com/ecatmur/stacktrace-from-exception/blob/main/stacktrace-from-exception.cpp
// - https://github.com/catboost/catboost/blob/master/contrib/libs/cxxsupp/libcxx/src/support/runtime/exception_pointer_msvc.ipp
// - https://www.geoffchappell.com/studies/msvc/language/predefined/index.htm
#pragma pack(push, 4)
struct CatchableType {
std::uint32_t properties;
std::int32_t pType;
std::uint32_t non_virtual_adjustment; // these next three are from _PMD
std::uint32_t offset_to_virtual_base_ptr;
std::uint32_t virtual_base_table_index;
std::uint32_t sizeOrOffset;
std::int32_t copyFunction;
};
struct ThrowInfo {
std::uint32_t attributes;
std::int32_t pmfnUnwind;
std::int32_t pForwardCompat;
std::int32_t pCatchableTypeArray;
};
#pragma warning(push)
#pragma warning(disable:4200)
#if IS_CLANG
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wc99-extensions"
#endif
struct CatchableTypeArray {
uint32_t nCatchableTypes;
int32_t arrayOfCatchableTypes[];
};
#if IS_CLANG
#pragma clang diagnostic pop
#endif
#pragma warning(pop)
#pragma pack(pop)
static constexpr unsigned EH_MAGIC_NUMBER1 = 0x19930520; // '?msc' version magic, see ehdata.h
static constexpr unsigned EH_EXCEPTION_NUMBER = 0xE06D7363; // '?msc', 'msc' | 0xE0000000
using catchable_type_array_t = decltype(ThrowInfo::pCatchableTypeArray);
class catchable_type_info {
HMODULE module_pointer = nullptr;
const CatchableTypeArray* catchable_types = nullptr;
public:
catchable_type_info(const HMODULE module_pointer, catchable_type_array_t catchable_type_array)
: module_pointer(module_pointer) {
catchable_types = rtti_rva<const CatchableTypeArray*>(catchable_type_array);
}
class iterator {
const catchable_type_info& info;
std::size_t i;
public:
iterator(const catchable_type_info& info, std::size_t i) : info(info), i(i) {}
const std::type_info& operator*() const {
return info.get_type_info(i);
}
bool operator!=(const iterator& other) const {
return i != other.i;
}
iterator& operator++() {
i++;
return *this;
}
};
using const_iterator = iterator;
const_iterator begin() const {
return {*this, 0};
}
const_iterator end() const {
return {*this, catchable_types ? catchable_types->nCatchableTypes : std::size_t{}};
}
private:
template<typename T, typename A>
T rtti_rva(A address) const {
#ifdef _WIN64
return reinterpret_cast<T>((uintptr_t)module_pointer + (uintptr_t)address);
#else
(void)module_pointer;
return reinterpret_cast<T>(address);
#endif
}
const std::type_info& get_type_info(std::size_t i) const {
return *rtti_rva<const std::type_info*>(get_catchable_type(i)->pType);
}
const CatchableType* get_catchable_type(std::size_t i) const {
return rtti_rva<const CatchableType*>(
reinterpret_cast<const std::int32_t*>(catchable_types->arrayOfCatchableTypes)[i]
);
}
};
catchable_type_info get_catchable_types(const EXCEPTION_RECORD* exception_record) {
static_assert(EXCEPTION_MAXIMUM_PARAMETERS >= 4, "Unexpected EXCEPTION_MAXIMUM_PARAMETERS");
// ExceptionInformation will contain
// [0] EH_MAGIC_NUMBER1
// [1] ExceptionObject
// [2] ThrowInfo
HMODULE module_pointer = nullptr;
catchable_type_array_t catchable_type_array{}; // will be either an int or pointer
if(
exception_record->ExceptionInformation[0] == EH_MAGIC_NUMBER1
&& exception_record->NumberParameters >= 3
) {
if(exception_record->NumberParameters >= 4) {
module_pointer = reinterpret_cast<HMODULE>(exception_record->ExceptionInformation[3]);
}
auto throw_info = reinterpret_cast<const ThrowInfo*>(exception_record->ExceptionInformation[2]);
if (throw_info) {
catchable_type_array = throw_info->pCatchableTypeArray;
}
}
return {module_pointer, catchable_type_array};
}
bool matches_exception(EXCEPTION_RECORD* exception_record, const std::type_info& type_info) {
if (type_info == typeid(void)) {
return true;
}
for (const auto& catchable_type : get_catchable_types(exception_record)) {
if (catchable_type == type_info) {
return true;
}
}
return false;
}
#endif
#ifndef _MSC_VER
#if IS_LIBSTDCXX
constexpr size_t vtable_size = 11;
#elif IS_LIBCXX
constexpr size_t vtable_size = 10;
#else
#warning "Cpptrace from_current: Unrecognized C++ standard library, from_current() won't be supported"
constexpr size_t vtable_size = 0;
#endif
#if IS_WINDOWS
int get_page_size() {
SYSTEM_INFO info;
GetSystemInfo(&info);
return info.dwPageSize;
}
constexpr auto memory_readonly = PAGE_READONLY;
constexpr auto memory_readwrite = PAGE_READWRITE;
int mprotect_page_and_return_old_protections(void* page, int page_size, int protections) {
DWORD old_protections;
if(!VirtualProtect(page, page_size, protections, &old_protections)) {
throw internal_error(
"VirtualProtect call failed: {}",
std::system_error(GetLastError(), std::system_category()).what()
);
}
return old_protections;
}
void mprotect_page(void* page, int page_size, int protections) {
mprotect_page_and_return_old_protections(page, page_size, protections);
}
void* allocate_page(int page_size) {
auto page = VirtualAlloc(nullptr, page_size, MEM_COMMIT | MEM_RESERVE, memory_readwrite);
if(!page) {
throw internal_error(
"VirtualAlloc call failed: {}",
std::system_error(GetLastError(), std::system_category()).what()
);
}
return page;
}
#else
int get_page_size() {
#if defined(_SC_PAGESIZE)
return sysconf(_SC_PAGESIZE);
#else
return getpagesize();
#endif
}
constexpr auto memory_readonly = PROT_READ;
constexpr auto memory_readwrite = PROT_READ | PROT_WRITE;
#if IS_APPLE
int get_page_protections(void* page) {
// https://stackoverflow.com/a/12627784/15675011
#ifdef CPPTRACE_HAS_MACH_VM
mach_vm_size_t vmsize;
mach_vm_address_t address = (mach_vm_address_t)page;
#else
vm_size_t vmsize;
vm_address_t address = (vm_address_t)page;
#endif
vm_region_basic_info_data_t info;
mach_msg_type_number_t info_count =
sizeof(size_t) == 8 ? VM_REGION_BASIC_INFO_COUNT_64 : VM_REGION_BASIC_INFO_COUNT;
memory_object_name_t object;
kern_return_t status =
#ifdef CPPTRACE_HAS_MACH_VM
mach_vm_region
#else
vm_region_64
#endif
(
mach_task_self(),
&address,
&vmsize,
VM_REGION_BASIC_INFO,
(vm_region_info_t)&info,
&info_count,
&object
);
if(status == KERN_INVALID_ADDRESS) {
throw internal_error("vm_region failed with KERN_INVALID_ADDRESS");
}
int perms = 0;
if(info.protection & VM_PROT_READ) {
perms |= PROT_READ;
}
if(info.protection & VM_PROT_WRITE) {
perms |= PROT_WRITE;
}
if(info.protection & VM_PROT_EXECUTE) {
perms |= PROT_EXEC;
}
return perms;
}
#else
// Code for reading /proc/self/maps
// Unfortunately this is the canonical and only way to get memory permissions on linux
// It comes with some surprising behaviors. Because it's a pseudo-file and maps could update at any time, reads of
// the file can tear. The surprising observable behavior here is overlapping ranges:
// - https://unix.stackexchange.com/questions/704987/overlapping-address-ranges-in-proc-maps
// - https://stackoverflow.com/questions/59737950/what-is-the-correct-way-to-get-a-consistent-snapshot-of-proc-pid-smaps
// Additional info:
// Note: reading /proc/PID/maps or /proc/PID/smaps is inherently racy (consistent
// output can be achieved only in the single read call).
// This typically manifests when doing partial reads of these files while the
// memory map is being modified. Despite the races, we do provide the following
// guarantees:
//
// 1) The mapped addresses never go backwards, which implies no two
// regions will ever overlap.
// 2) If there is something at a given vaddr during the entirety of the
// life of the smaps/maps walk, there will be some output for it.
//
// https://www.kernel.org/doc/Documentation/filesystems/proc.txt
// Ideally we could do everything as a single read() call but I don't think that's practical, especially given that
// the kernel has limited buffers internally. While we shouldn't be modifying mapped memory while reading
// /proc/self/maps here, it's theoretically possible that we could allocate and that could go to the OS for more
// pages.
// While reading this is inherently racy, as far as I can tell tears don't happen within a line but they can happen
// between lines.
// The code that writes /proc/pid/maps:
// - https://github.com/torvalds/linux/blob/3d0ebc36b0b3e8486ceb6e08e8ae173aaa6d1221/fs/proc/task_mmu.c#L304-L365
struct address_range {
uintptr_t low;
uintptr_t high;
int perms;
bool operator<(const address_range& other) const {
return low < other.low;
}
};
// returns nullopt on eof
optional<address_range> read_map_entry(std::ifstream& stream) {
uintptr_t start;
uintptr_t stop;
stream>>start;
stream.ignore(1); // dash
stream>>stop;
if(stream.eof()) {
return nullopt;
}
if(stream.fail()) {
throw internal_error("Failure reading /proc/self/maps");
}
stream.ignore(1); // space
char r, w, x; // there's a private/shared flag after these but we don't need it
stream>>r>>w>>x;
if(stream.fail() || stream.eof()) {
throw internal_error("Failure reading /proc/self/maps");
}
int perms = 0;
if(r == 'r') {
perms |= PROT_READ;
}
if(w == 'w') {
perms |= PROT_WRITE;
}
if(x == 'x') {
perms |= PROT_EXEC;
}
stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
return address_range{start, stop, perms};
}
// returns a vector or nullopt if a tear is detected
optional<std::vector<address_range>> try_load_mapped_region_info() {
std::ifstream stream("/proc/self/maps");
stream>>std::hex;
std::vector<address_range> ranges;
while(auto entry = read_map_entry(stream)) {
const auto& range = entry.unwrap();
VERIFY(range.low <= range.high);
if(!ranges.empty()) {
const auto& last_range = ranges.back();
if(range.low < last_range.high) {
return nullopt;
}
}
ranges.push_back(range);
}
return ranges;
}
// we can allocate during try_load_mapped_region_info, in theory that could cause a tear
optional<std::vector<address_range>> try_load_mapped_region_info_with_retries(int n) {
VERIFY(n > 0);
for(int i = 0; i < n; i++) {
if(auto info = try_load_mapped_region_info()) {
return info;
}
}
throw internal_error("Couldn't successfully load /proc/self/maps after {} retries", n);
}
const std::vector<address_range>& load_mapped_region_info() {
static std::vector<address_range> regions;
static bool has_loaded = false;
if(!has_loaded) {
has_loaded = true;
if(auto info = try_load_mapped_region_info_with_retries(2)) {
regions = std::move(info).unwrap();
}
}
return regions;
}
int get_page_protections(void* page) {
const auto& mapped_region_info = load_mapped_region_info();
auto it = first_less_than_or_equal(
mapped_region_info.begin(),
mapped_region_info.end(),
reinterpret_cast<uintptr_t>(page),
[](uintptr_t a, const address_range& b) {
return a < b.low;
}
);
if(it == mapped_region_info.end()) {
throw internal_error(
"Failed to find mapping for {>16:0h} in /proc/self/maps",
reinterpret_cast<uintptr_t>(page)
);
}
return it->perms;
}
#endif
void mprotect_page(void* page, int page_size, int protections) {
if(mprotect(page, page_size, protections) != 0) {
throw internal_error("mprotect call failed: {}", strerror(errno));
}
}
int mprotect_page_and_return_old_protections(void* page, int page_size, int protections) {
auto old_protections = get_page_protections(page);
mprotect_page(page, page_size, protections);
return old_protections;
}
void* allocate_page(int page_size) {
auto page = mmap(nullptr, page_size, memory_readwrite, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if(page == MAP_FAILED) {
throw internal_error("mmap call failed: {}", strerror(errno));
}
return page;
}
#endif
void perform_typeinfo_surgery(const std::type_info& info, bool(*do_catch_function)(const std::type_info*, const std::type_info*, void**, unsigned)) {
if(vtable_size == 0) { // set to zero if we don't know what standard library we're working with
return;
}
void* type_info_pointer = const_cast<void*>(static_cast<const void*>(&info));
void** type_info_vtable_pointer = *static_cast<void***>(type_info_pointer);
// the type info vtable pointer points to two pointers inside the vtable, adjust it back
// https://itanium-cxx-abi.github.io/cxx-abi/abi.html#vtable-components (see offset to top, typeinfo ptr,
// and the following bullet point)
type_info_vtable_pointer -= 2;
// for libstdc++ the class type info vtable looks like
// 0x7ffff7f89d18 <_ZTVN10__cxxabiv117__class_type_infoE>: 0x0000000000000000 0x00007ffff7f89d00
// [offset ][typeinfo pointer ]
// 0x7ffff7f89d28 <_ZTVN10__cxxabiv117__class_type_infoE+16>: 0x00007ffff7dd65a0 0x00007ffff7dd65c0
// [base destructor ][deleting dtor ]
// 0x7ffff7f89d38 <_ZTVN10__cxxabiv117__class_type_infoE+32>: 0x00007ffff7dd8f10 0x00007ffff7dd8f10
// [__is_pointer_p ][__is_function_p ]
// 0x7ffff7f89d48 <_ZTVN10__cxxabiv117__class_type_infoE+48>: 0x00007ffff7dd6640 0x00007ffff7dd6500
// [__do_catch ][__do_upcast ]
// 0x7ffff7f89d58 <_ZTVN10__cxxabiv117__class_type_infoE+64>: 0x00007ffff7dd65e0 0x00007ffff7dd66d0
// [__do_upcast ][__do_dyncast ]
// 0x7ffff7f89d68 <_ZTVN10__cxxabiv117__class_type_infoE+80>: 0x00007ffff7dd6580 0x00007ffff7f8abe8
// [__do_find_public_src][other ]
// In libc++ the layout is
// [offset ][typeinfo pointer ]
// [base destructor ][deleting dtor ]
// [noop1 ][noop2 ]
// [can_catch ][search_above_dst ]
// [search_below_dst ][has_unambiguous_public_base]
// Relevant documentation/implementation:
// https://itanium-cxx-abi.github.io/cxx-abi/abi.html
// libstdc++
// https://github.com/gcc-mirror/gcc/blob/b13e34699c7d27e561fcfe1b66ced1e50e69976f/libstdc%252B%252B-v3/libsupc%252B%252B/typeinfo
// https://github.com/gcc-mirror/gcc/blob/b13e34699c7d27e561fcfe1b66ced1e50e69976f/libstdc%252B%252B-v3/libsupc%252B%252B/class_type_info.cc
// libc++
// https://github.com/llvm/llvm-project/blob/648f4d0658ab00cf1e95330c8811aaea9481a274/libcxx/include/typeinfo
// https://github.com/llvm/llvm-project/blob/648f4d0658ab00cf1e95330c8811aaea9481a274/libcxxabi/src/private_typeinfo.h
// shouldn't be anything other than 4096 but out of an abundance of caution
auto page_size = get_page_size();
if(page_size <= 0 && is_positive_power_of_two(page_size)) {
throw internal_error("getpagesize() is not a power of 2 greater than zero (was {})", page_size);
}
if(static_cast<size_t>(page_size) < vtable_size * sizeof(void*)) {
throw internal_error(
"Page size isn't big enough for a vtable: Needed {}, got {}",
vtable_size * sizeof(void*),
page_size
);
}
// allocate a page for the new vtable so it can be made read-only later
// the OS cleans this up, no cleanup done here for it
void* new_vtable_page = allocate_page(page_size);
// Double-check alignment: "This address must have the alignment required for pointers"
// https://itanium-cxx-abi.github.io/cxx-abi/abi.html#vtable-components
constexpr auto ptr_align = alignof(void*);
static_assert(is_positive_power_of_two(ptr_align), "alignof has to return a power of two");
auto align_mask = ptr_align - 1;
if((reinterpret_cast<uintptr_t>(new_vtable_page) & align_mask) != 0) {
throw internal_error("Bad allocation alignment: {}", reinterpret_cast<uintptr_t>(new_vtable_page));
}
// make our own copy of the vtable
memcpy(new_vtable_page, type_info_vtable_pointer, vtable_size * sizeof(void*));
// ninja in the custom __do_catch interceptor
auto new_vtable = static_cast<void**>(new_vtable_page);
// double cast is done here because older (and some newer gcc versions) warned about it under -Wpedantic
new_vtable[6] = reinterpret_cast<void*>(reinterpret_cast<std::uintptr_t>(do_catch_function));
// make the page read-only
mprotect_page(new_vtable_page, page_size, memory_readonly);
// make the vtable pointer for unwind_interceptor's type_info point to the new vtable
auto type_info_addr = reinterpret_cast<uintptr_t>(type_info_pointer);
auto page_addr = type_info_addr & ~(page_size - 1);
// make sure the memory we're going to set is within the page
if(type_info_addr - page_addr + sizeof(void*) > static_cast<unsigned>(page_size)) {
throw internal_error("pointer crosses page boundaries");
}
auto old_protections = mprotect_page_and_return_old_protections(
reinterpret_cast<void*>(page_addr),
page_size,
memory_readwrite
);
*static_cast<void**>(type_info_pointer) = static_cast<void*>(new_vtable + 2);
mprotect_page(reinterpret_cast<void*>(page_addr), page_size, old_protections);
}
bool can_catch(
const std::type_info* type,
const std::type_info* throw_type,
void** throw_obj,
unsigned outer
) {
if (*type == typeid(void)) {
return true;
}
// get the vtable for the type_info and call the function pointer in the 6th slot
// see below: perform_typeinfo_surgery
void* type_info_pointer = const_cast<void*>(static_cast<const void*>(type));
void** type_info_vtable_pointer = *static_cast<void***>(type_info_pointer);
// the type info vtable pointer points to two pointers inside the vtable, adjust it back
type_info_vtable_pointer -= 2;
auto* can_catch_fn =
#if IS_GCC
// error: ISO C++ forbids casting between pointer-to-function and pointer-to-object on old gcc
__extension__
#endif
reinterpret_cast<decltype(can_catch)*>(type_info_vtable_pointer[6]);
return can_catch_fn(type, throw_type, throw_obj, outer);
}
#endif
// called when unwinding starts after rethrowing, after search phase
void rethrow_scope_cleanup() {
get_rethrow_switch() = false;
}
scope_guard<void(&)()> setup_rethrow() {
get_rethrow_switch() = true;
// will flip the switch back to true as soon as the search phase completes and the unwinding begins
return scope_exit<void(&)()>(rethrow_scope_cleanup);
}
}
CPPTRACE_END_NAMESPACE
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
#ifdef _MSC_VER
bool matches_exception(EXCEPTION_POINTERS* exception_ptrs, const std::type_info& type_info) {
CPPTRACE_PUSH_EXTENSION_WARNINGS
__try {
auto* exception_record = exception_ptrs->ExceptionRecord;
// Check if the SEH exception is a C++ exception
if(exception_record->ExceptionCode == EH_EXCEPTION_NUMBER) {
return detail::matches_exception(exception_record, type_info);
}
} __except(EXCEPTION_EXECUTE_HANDLER) {
// pass
}
CPPTRACE_POP_EXTENSION_WARNINGS
return false;
}
CPPTRACE_FORCE_NO_INLINE
int maybe_collect_trace(EXCEPTION_POINTERS* exception_ptrs, int filter_result) {
if(filter_result == EXCEPTION_EXECUTE_HANDLER) {
#ifdef CPPTRACE_UNWIND_WITH_DBGHELP
collect_current_trace(1, exception_ptrs);
#else
collect_current_trace(1);
(void)exception_ptrs;
#endif
}
return filter_result;
}
CPPTRACE_FORCE_NO_INLINE
void maybe_collect_trace(EXCEPTION_POINTERS* exception_ptrs, const std::type_info& type_info) {
if(matches_exception(exception_ptrs, type_info)) {
#ifdef CPPTRACE_UNWIND_WITH_DBGHELP
collect_current_trace(2, exception_ptrs);
#else
collect_current_trace(2);
(void)exception_ptrs;
#endif
}
}
#else
CPPTRACE_FORCE_NO_INLINE
void maybe_collect_trace(
const std::type_info* type,
const std::type_info* throw_type,
void** throw_obj,
unsigned outer
) {
if(detail::can_catch(type, throw_type, throw_obj, outer)) {
collect_current_trace(2);
}
}
void do_prepare_unwind_interceptor(const std::type_info& type_info, bool(*can_catch)(const std::type_info*, const std::type_info*, void**, unsigned)) {
try {
detail::perform_typeinfo_surgery(
type_info,
can_catch
);
} catch(std::exception& e) {
detail::log::error("Exception occurred while preparing from_current support: {}", e.what());
} catch(...) {
detail::log::error("Unknown exception occurred while preparing from_current support");
}
}
#endif
}
const raw_trace& raw_trace_from_current_exception() {
return detail::current_exception_trace.get_raw_trace();
}
const stacktrace& from_current_exception() {
return detail::current_exception_trace.get_resolved_trace();
}
const raw_trace& raw_trace_from_current_exception_rethrow() {
return detail::saved_rethrow_trace.get_raw_trace();
}
const stacktrace& from_current_exception_rethrow() {
return detail::saved_rethrow_trace.get_resolved_trace();
}
bool current_exception_was_rethrown() {
if(detail::saved_rethrow_trace.is_resolved()) {
return !detail::saved_rethrow_trace.get_resolved_trace().empty();
} else {
return !detail::saved_rethrow_trace.get_raw_trace().empty();
}
}
// The non-argument overload is to serve as room for possible future optimization under Microsoft's STL
CPPTRACE_FORCE_NO_INLINE void rethrow() {
auto guard = detail::setup_rethrow();
std::rethrow_exception(std::current_exception());
}
CPPTRACE_FORCE_NO_INLINE void rethrow(std::exception_ptr exception) {
auto guard = detail::setup_rethrow();
std::rethrow_exception(exception);
}
void clear_current_exception_traces() {
detail::current_exception_trace = detail::lazy_trace_holder{raw_trace{}};
detail::saved_rethrow_trace = detail::lazy_trace_holder{raw_trace{}};
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,144 @@
#include "jit/jit_objects.hpp"
#include "cpptrace/forward.hpp"
#include "utils/error.hpp"
#include "utils/optional.hpp"
#include "utils/span.hpp"
#include "binary/elf.hpp"
#include "binary/mach-o.hpp"
#include <algorithm>
#include <iostream>
#include <fstream>
#include <map>
#include <vector>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
#if IS_LINUX || IS_APPLE
class jit_object_manager {
struct object_entry {
const char* object_start;
std::unique_ptr<jit_object_type> object;
};
std::vector<object_entry> objects;
struct range_entry {
frame_ptr low;
frame_ptr high; // not inclusive
const char* object_start;
jit_object_type* object;
bool operator<(const range_entry& other) const {
return low < other.low;
}
};
// TODO: Maybe use a set...
std::vector<range_entry> range_list;
public:
void add_jit_object(cbspan object) {
auto object_res = jit_object_type::open(object);
if(object_res.is_error()) {
if(!should_absorb_trace_exceptions()) {
object_res.drop_error();
}
return;
}
objects.push_back({object.data(), make_unique<jit_object_type>(std::move(object_res).unwrap_value())});
auto* object_file = objects.back().object.get();
auto ranges_res = object_file->get_pc_ranges();
if(ranges_res.is_error()) {
if(!should_absorb_trace_exceptions()) {
ranges_res.drop_error();
}
return;
}
auto& ranges = ranges_res.unwrap_value();
for(auto range : ranges) {
range_entry entry{range.low, range.high, object.data(), object_file};
// TODO: Perf
range_list.insert(std::upper_bound(range_list.begin(), range_list.end(), entry), entry);
}
}
void remove_jit_object(const char* ptr) {
// TODO: Perf
objects.erase(
std::remove_if(
objects.begin(),
objects.end(),
[&](const object_entry& entry) { return entry.object_start == ptr; }
),
objects.end()
);
range_list.erase(
std::remove_if(
range_list.begin(),
range_list.end(),
[&](const range_entry& entry) { return entry.object_start == ptr; }
),
range_list.end()
);
}
optional<jit_object_lookup_result> lookup(frame_ptr pc) const {
auto it = first_less_than_or_equal(
range_list.begin(),
range_list.end(),
pc,
[](frame_ptr pc, const range_entry& entry) {
return pc < entry.low;
}
);
if(it == range_list.end()) {
return nullopt;
}
ASSERT(pc >= it->low);
if(pc < it->high) {
return jit_object_lookup_result{*it->object, it->low};
} else {
return nullopt;
}
}
void clear_all_jit_objects() {
objects.clear();
range_list.clear();
}
};
#else
class jit_object_manager {
public:
void add_jit_object(cbspan) {}
void remove_jit_object(const char*) {}
void clear_all_jit_objects() {}
};
#endif
jit_object_manager& get_jit_object_manager() {
static jit_object_manager manager;
return manager;
}
void register_jit_object(const char* ptr, std::size_t size) {
auto& manager = get_jit_object_manager();
manager.add_jit_object(make_span(ptr, size));
}
void unregister_jit_object(const char* ptr) {
auto& manager = get_jit_object_manager();
manager.remove_jit_object(ptr);
}
void clear_all_jit_objects() {
auto& manager = get_jit_object_manager();
manager.clear_all_jit_objects();
}
#if IS_LINUX || IS_APPLE
optional<jit_object_lookup_result> lookup_jit_object(frame_ptr pc) {
return get_jit_object_manager().lookup(pc);
}
#endif
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,31 @@
#ifndef JIT_OBJECTS_HPP
#define JIT_OBJECTS_HPP
#include "binary/elf.hpp"
#include "binary/mach-o.hpp"
#include "cpptrace/forward.hpp"
#include "utils/optional.hpp"
#include "platform/platform.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
void register_jit_object(const char*, std::size_t);
void unregister_jit_object(const char*);
void clear_all_jit_objects();
#if IS_LINUX || IS_APPLE
#if IS_LINUX
using jit_object_type = elf;
#elif IS_APPLE
using jit_object_type = mach_o;
#endif
struct jit_object_lookup_result {
jit_object_type& object;
frame_ptr base;
};
optional<jit_object_lookup_result> lookup_jit_object(frame_ptr pc);
#endif
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,72 @@
#include "logging.hpp"
#include <cpptrace/forward.hpp>
#include <cpptrace/utils.hpp>
#include <atomic>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::atomic<log_level> current_log_level(log_level::warning); // NOSONAR
void default_null_logger(log_level, const char*) {}
void default_stderr_logger(log_level level, const char* message) {
microfmt::print(std::cerr, "[cpptrace {}] {}\n", to_string(level), message);
}
std::function<void(log_level, const char*)>& log_callback() {
static std::function<void(log_level, const char*)> callback{default_null_logger};
return callback;
}
void do_log(log_level level, const char* message) {
if(level < current_log_level) {
return;
}
log_callback()(level, message);
}
namespace log {
void error(const char* message) {
do_log(log_level::error, message);
}
void warn(const char* message) {
do_log(log_level::warning, message);
}
void info(const char* message) {
do_log(log_level::info, message);
}
void debug(const char* message) {
do_log(log_level::debug, message);
}
}
}
CPPTRACE_END_NAMESPACE
CPPTRACE_BEGIN_NAMESPACE
void set_log_level(log_level level) {
detail::current_log_level = level;
}
void set_log_callback(std::function<void(log_level, const char*)> callback) {
detail::log_callback() = std::move(callback);
}
void use_default_stderr_logger() {
detail::log_callback() = detail::default_stderr_logger;
}
const char* to_string(log_level level) {
switch(level) {
case log_level::debug: return "debug";
case log_level::info: return "info";
case log_level::warning: return "warning";
case log_level::error: return "error";
default: return "unknown log level";
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,39 @@
#ifndef LOGGING_HPP
#define LOGGING_HPP
#include <cpptrace/utils.hpp>
#include "utils/microfmt.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace log {
// exported for test purposes
CPPTRACE_EXPORT void error(const char*);
template<typename... Args>
void error(const char* format, Args&&... args) {
error(microfmt::format(format, args...).c_str());
}
CPPTRACE_EXPORT void warn(const char*);
template<typename... Args>
void warn(const char* format, Args&&... args) {
warn(microfmt::format(format, args...).c_str());
}
CPPTRACE_EXPORT void info(const char*);
template<typename... Args>
void info(const char* format, Args&&... args) {
info(microfmt::format(format, args...).c_str());
}
CPPTRACE_EXPORT void debug(const char*);
template<typename... Args>
void debug(const char* format, Args&&... args) {
debug(microfmt::format(format, args...).c_str());
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,41 @@
#include <cpptrace/basic.hpp>
#include "options.hpp"
#include <atomic>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::atomic_bool absorb_trace_exceptions(true); // NOSONAR
std::atomic_bool resolve_inlined_calls(true); // NOSONAR
std::atomic<cache_mode> current_cache_mode(cache_mode::prioritize_speed); // NOSONAR
bool should_absorb_trace_exceptions() {
return absorb_trace_exceptions;
}
bool should_resolve_inlined_calls() {
return resolve_inlined_calls;
}
cache_mode get_cache_mode() {
return current_cache_mode;
}
}
CPPTRACE_END_NAMESPACE
CPPTRACE_BEGIN_NAMESPACE
void absorb_trace_exceptions(bool absorb) {
detail::absorb_trace_exceptions = absorb;
}
void enable_inlined_call_resolution(bool enable) {
detail::resolve_inlined_calls = enable;
}
namespace experimental {
void set_cache_mode(cache_mode mode) {
detail::current_cache_mode = mode;
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,15 @@
#ifndef OPTIONS_HPP
#define OPTIONS_HPP
#include <cpptrace/utils.hpp>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
// exported for test purposes
CPPTRACE_EXPORT bool should_absorb_trace_exceptions();
bool should_resolve_inlined_calls();
cache_mode get_cache_mode();
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,148 @@
#include "platform/platform.hpp"
#if IS_WINDOWS
#include "platform/dbghelp_utils.hpp"
#if defined(CPPTRACE_UNWIND_WITH_DBGHELP) \
|| defined(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP) \
|| defined(CPPTRACE_DEMANGLE_WITH_WINAPI)
#include "utils/error.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
#include <unordered_map>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <dbghelp.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
dbghelp_syminit_info::dbghelp_syminit_info(void* handle, bool should_sym_cleanup, bool should_close_handle)
: handle(handle), should_sym_cleanup(should_sym_cleanup), should_close_handle(should_close_handle) {}
dbghelp_syminit_info::~dbghelp_syminit_info() {
release();
}
void dbghelp_syminit_info::release() {
if(!handle) {
return;
}
if(should_sym_cleanup) {
if(!SymCleanup(handle)) {
throw internal_error("SymCleanup failed with code {}\n", GetLastError());
}
}
if(should_close_handle) {
if(!CloseHandle(handle)) {
throw internal_error("CloseHandle failed with code {}\n", GetLastError());
}
}
}
dbghelp_syminit_info dbghelp_syminit_info::make_not_owned(void* handle) {
return dbghelp_syminit_info(handle, false, false);
}
dbghelp_syminit_info dbghelp_syminit_info::make_owned(void* handle, bool should_close_handle) {
return dbghelp_syminit_info(handle, true, should_close_handle);
}
dbghelp_syminit_info::dbghelp_syminit_info(dbghelp_syminit_info&& other) {
handle = exchange(other.handle, nullptr);
should_sym_cleanup = other.should_sym_cleanup;
should_close_handle = other.should_close_handle;
}
dbghelp_syminit_info& dbghelp_syminit_info::operator=(dbghelp_syminit_info&& other) {
release();
handle = exchange(other.handle, nullptr);
should_sym_cleanup = other.should_sym_cleanup;
should_close_handle = other.should_close_handle;
return *this;
}
void* dbghelp_syminit_info::get_process_handle() const {
return handle;
}
dbghelp_syminit_info dbghelp_syminit_info::make_non_owning_view() const {
return make_not_owned(handle);
}
std::unordered_map<HANDLE, dbghelp_syminit_info>& get_syminit_cache() {
static std::unordered_map<HANDLE, dbghelp_syminit_info> syminit_cache;
return syminit_cache;
}
dbghelp_syminit_info ensure_syminit() {
auto lock = get_dbghelp_lock(); // locking around the entire access of the cache unordered_map
HANDLE proc = GetCurrentProcess();
if(get_cache_mode() == cache_mode::prioritize_speed) {
auto& syminit_cache = get_syminit_cache();
auto it = syminit_cache.find(proc);
if(it != syminit_cache.end()) {
return it->second.make_non_owning_view();
}
}
auto duplicated_handle = raii_wrap<void*>(nullptr, [] (void* handle) {
if(handle) {
if(!CloseHandle(handle)) {
throw internal_error("CloseHandle failed with code {}\n", GetLastError());
}
}
});
// https://github.com/jeremy-rifkin/cpptrace/issues/204
// https://github.com/jeremy-rifkin/cpptrace/pull/206
// https://learn.microsoft.com/en-us/windows/win32/debug/initializing-the-symbol-handler
// Apparently duplicating the process handle is the idiomatic thing to do and this avoids issues of
// SymInitialize being called twice.
// DuplicateHandle requires the PROCESS_DUP_HANDLE access right. If for some reason DuplicateHandle we fall back
// to calling SymInitialize on the process handle.
optional<DWORD> maybe_duplicate_handle_error_code;
if(!DuplicateHandle(proc, proc, proc, &duplicated_handle.get(), 0, FALSE, DUPLICATE_SAME_ACCESS)) {
maybe_duplicate_handle_error_code = GetLastError();
}
if(!SymInitialize(maybe_duplicate_handle_error_code ? proc : duplicated_handle.get(), NULL, TRUE)) {
if(maybe_duplicate_handle_error_code) {
throw internal_error(
"SymInitialize failed with error code {} after DuplicateHandle failed with error code {}",
GetLastError(),
maybe_duplicate_handle_error_code.unwrap()
);
} else {
throw internal_error("SymInitialize failed with error code {}", GetLastError());
}
}
auto info = dbghelp_syminit_info::make_owned(
maybe_duplicate_handle_error_code ? proc : exchange(duplicated_handle.get(), nullptr),
!maybe_duplicate_handle_error_code
);
// either cache and return a view or return the owning wrapper
if(get_cache_mode() == cache_mode::prioritize_speed) {
auto& syminit_cache = get_syminit_cache();
auto pair = syminit_cache.insert({proc, std::move(info)});
VERIFY(pair.second);
return pair.first->second.make_non_owning_view();
} else {
return info;
}
}
std::unique_lock<std::recursive_mutex> get_dbghelp_lock() {
static std::recursive_mutex mutex;
return std::unique_lock<std::recursive_mutex>{mutex};
}
}
CPPTRACE_END_NAMESPACE
#endif
#endif

View File

@ -0,0 +1,50 @@
#ifndef DBGHELP_UTILS_HPP
#define DBGHELP_UTILS_HPP
#if defined(CPPTRACE_UNWIND_WITH_DBGHELP) \
|| defined(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP) \
|| defined(CPPTRACE_DEMANGLE_WITH_WINAPI)
#include "utils/common.hpp"
#include <unordered_map>
#include <mutex>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
class dbghelp_syminit_info {
// `void*` is used to avoid including the (expensive) windows.h header here
void* handle = nullptr;
bool should_sym_cleanup; // true if cleanup is not managed by the syminit cache
bool should_close_handle; // true if cleanup is not managed by the syminit cache and the handle was duplicated
dbghelp_syminit_info(void* handle, bool should_sym_cleanup, bool should_close_handle);
public:
~dbghelp_syminit_info();
void release();
NODISCARD static dbghelp_syminit_info make_not_owned(void* handle);
NODISCARD static dbghelp_syminit_info make_owned(void* handle, bool should_close_handle);
dbghelp_syminit_info(const dbghelp_syminit_info&) = delete;
dbghelp_syminit_info(dbghelp_syminit_info&&);
dbghelp_syminit_info& operator=(const dbghelp_syminit_info&) = delete;
dbghelp_syminit_info& operator=(dbghelp_syminit_info&&);
void* get_process_handle() const;
dbghelp_syminit_info make_non_owning_view() const;
};
// Ensure SymInitialize is called on the process. This function either
// - Finds that SymInitialize has been called for a handle to the current process already, in which case it returns
// a non-owning dbghelp_syminit_info instance holding the handle
// - Calls SymInitialize a handle to the current process, caches it, and returns a non-owning dbghelp_syminit_info
// - Calls SymInitialize and returns an owning dbghelp_syminit_info which will handle cleanup
dbghelp_syminit_info ensure_syminit();
NODISCARD std::unique_lock<std::recursive_mutex> get_dbghelp_lock();
}
CPPTRACE_END_NAMESPACE
#endif
#endif

View File

@ -0,0 +1,28 @@
#ifndef EXCEPTION_TYPE_HPP
#define EXCEPTION_TYPE_HPP
#include <string>
#include "platform/platform.hpp"
// libstdc++ and libc++
#if defined(CPPTRACE_HAS_CXX_EXCEPTION_TYPE) && (IS_LIBSTDCXX || IS_LIBCXX)
#include <typeinfo>
#include <cxxabi.h>
#include "demangle/demangle.hpp"
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
inline std::string exception_type_name() {
#if defined(CPPTRACE_HAS_CXX_EXCEPTION_TYPE) && (IS_LIBSTDCXX || IS_LIBCXX)
const std::type_info* t = abi::__cxa_current_exception_type();
return t ? detail::demangle(t->name(), false) : "<unknown>";
#else
return "<unknown>";
#endif
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,48 @@
#ifndef PATH_HPP
#define PATH_HPP
#include "platform/platform.hpp"
#include "utils/string_view.hpp"
#include <string>
#include <cctype>
#if IS_WINDOWS
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
#if IS_WINDOWS
constexpr char PATH_SEP = '\\';
inline bool is_absolute(string_view path) {
// I don't want to bring in shlwapi as a dependency just for PathIsRelativeA so I'm following the guidance of
// https://stackoverflow.com/a/71941552/15675011 and
// https://github.com/wine-mirror/wine/blob/b210a204137dec8d2126ca909d762454fd47e963/dlls/kernelbase/path.c#L982
if(path.empty() || IsDBCSLeadByte(path[0])) {
return false;
}
if(path[0] == '\\') {
return true;
}
if(path.size() >= 2 && std::isalpha(path[0]) && path[1] == ':') {
return true;
}
return false;
}
#else
constexpr char PATH_SEP = '/';
inline bool is_absolute(string_view path) {
if(path.empty()) {
return false;
}
return path[0] == '/';
}
#endif
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,48 @@
#ifndef PLATFORM_HPP
#define PLATFORM_HPP
#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 "Unexpected platform"
#endif
#define IS_CLANG 0
#define IS_GCC 0
#define IS_MSVC 0
#if defined(__clang__)
#undef IS_CLANG
#define IS_CLANG 1
#elif defined(__GNUC__) || defined(__GNUG__)
#undef IS_GCC
#define IS_GCC 1
#elif defined(_MSC_VER)
#undef IS_MSVC
#define IS_MSVC 1
#else
#error "Unsupported compiler"
#endif
#define IS_LIBSTDCXX 0
#define IS_LIBCXX 0
#if defined(__GLIBCXX__) || defined(__GLIBCPP__)
#undef IS_LIBSTDCXX
#define IS_LIBSTDCXX 1
#elif defined(_LIBCPP_VERSION)
#undef IS_LIBCXX
#define IS_LIBCXX 1
#endif
#endif

View File

@ -0,0 +1,102 @@
#ifndef PROGRAM_NAME_HPP
#define PROGRAM_NAME_HPP
#include <mutex>
#include <string>
#include "platform/platform.hpp"
#if IS_WINDOWS
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#define CPPTRACE_MAX_PATH MAX_PATH
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
inline const char* program_name() {
static std::mutex mutex;
const std::lock_guard<std::mutex> lock(mutex);
static std::string name;
static bool did_init = false;
static bool valid = false;
if(!did_init) {
did_init = true;
char buffer[MAX_PATH + 1];
int res = GetModuleFileNameA(nullptr, buffer, MAX_PATH);
if(res) {
name = buffer;
valid = true;
}
}
return valid && !name.empty() ? name.c_str() : nullptr;
}
}
CPPTRACE_END_NAMESPACE
#elif IS_APPLE
#include <cstdint>
#include <mach-o/dyld.h>
#include <sys/syslimits.h>
#define CPPTRACE_MAX_PATH CPPTRACE_PATH_MAX
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
inline const char* program_name() {
static std::mutex mutex;
const std::lock_guard<std::mutex> lock(mutex);
static std::string name;
static bool did_init = false;
static bool valid = false;
if(!did_init) {
did_init = true;
char buffer[CPPTRACE_PATH_MAX + 1];
std::uint32_t bufferSize = sizeof buffer;
if(_NSGetExecutablePath(buffer, &bufferSize) == 0) {
name.assign(buffer, bufferSize);
valid = true;
}
}
return valid && !name.empty() ? name.c_str() : nullptr;
}
}
CPPTRACE_END_NAMESPACE
#elif IS_LINUX
#include <sys/types.h>
#include <unistd.h>
#define CPPTRACE_MAX_PATH CPPTRACE_PATH_MAX
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
inline const char* program_name() {
static std::mutex mutex;
const std::lock_guard<std::mutex> lock(mutex);
static std::string name;
static bool did_init = false;
static bool valid = false;
if(!did_init) {
did_init = true;
char buffer[CPPTRACE_PATH_MAX + 1];
const ssize_t size = readlink("/proc/self/exe", buffer, CPPTRACE_PATH_MAX);
if(size == -1) {
return nullptr;
}
buffer[size] = 0;
name = buffer;
valid = true;
}
return valid && !name.empty() ? name.c_str() : nullptr;
}
}
CPPTRACE_END_NAMESPACE
#endif
#endif

View File

@ -0,0 +1,986 @@
#include "cpptrace/forward.hpp"
#include <array>
#include <cctype>
#include <vector>
#include "utils/error.hpp"
#include "utils/optional.hpp"
#include "utils/string_view.hpp"
#include "utils/utils.hpp"
// Docs
// https://itanium-cxx-abi.github.io/cxx-abi/abi.html
// https://en.wikiversity.org/wiki/Visual_C%2B%2B_name_mangling
// Demangling
// https://github.com/llvm/llvm-project/blob/main/libcxxabi/src/demangle/ItaniumDemangle.h
// https://github.com/gcc-mirror/gcc/blob/b76f1fb7bf8a7b66b8acd469309257f8b18c0c51/libiberty/cp-demangle.c#L6794
// https://github.com/wine-mirror/wine/blob/3295365ba5654d6ff2da37c1ffa84aed81291fc1/dlls/msvcrt/undname.c#L1476
// Mangling
// https://github.com/llvm/llvm-project/blob/1463da8c4063cf1f1513aa5dbcedb44d2099c87f/clang/include/clang/AST/Mangle.h
// https://github.com/llvm/llvm-project/blob/1463da8c4063cf1f1513aa5dbcedb44d2099c87f/clang/lib/AST/MicrosoftMangle.cpp#L1709-L1721
// Test cases
// https://github.com/llvm/llvm-project/tree/d1b0b4bb4405c144e23be3d5c0459b03f95bd5ac/llvm/test/Demangle
// https://github.com/llvm/llvm-project/blob/d1b0b4bb4405c144e23be3d5c0459b03f95bd5ac/libcxxabi/test/DemangleTestCases.inc
// https://github.com/llvm/llvm-project/blob/d1b0b4bb4405c144e23be3d5c0459b03f95bd5ac/libcxxabi/test/test_demangle.pass.cpp
// https://github.com/wine-mirror/wine/blob/3295365ba5654d6ff2da37c1ffa84aed81291fc1/dlls/msvcrt/tests/cpp.c#L108
// https://github.com/wine-mirror/wine/blob/3295365ba5654d6ff2da37c1ffa84aed81291fc1/dlls/ucrtbase/tests/cpp.c#L57
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
template<typename T, typename Arg>
bool is_any(const T& value, const Arg& arg) {
return value == arg;
}
template<typename T, typename Arg, typename... Args>
bool is_any(const T& value, const Arg& arg, const Args&... args) {
return (value == arg) || is_any(value, args...);
}
// http://eel.is/c++draft/lex.name#nt:identifier
bool is_identifier_start(char c) {
return isalpha(c) || c == '$' || c == '_';
}
bool is_identifier_continue(char c) {
return isdigit(c) || is_identifier_start(c);
}
bool is_hex_digit(char c) {
return isdigit(c) || is_any(c, 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F');
}
bool is_octal_digit(char c) {
return is_any(c, '0', '1', '2', '3', '4', '5', '6', '7');
}
bool is_simple_escape_char(char c) {
return is_any(c, '\'', '"', '?', '\\', 'a', 'b', 'f', 'n', 'r', 't', 'v');
}
// http://eel.is/c++draft/lex.operators#nt:operator-or-punctuator
const std::vector<string_view> punctuators_and_operators = []() {
std::vector<string_view> vec{
"{", "}", "[", "]", "(", ")",
"<:", ":>", "<%", "%>", ";", ":", "...",
"?", "::", ".", ".*", "->", "->*", "~",
"!", "+", "-", "*", "/", "%", "^", "&", "|",
"=", "+=", "-=", "*=", "/=", "%=", "^=", "&=", "|=",
"==", "!=", "<", ">", "<=", ">=", "<=>", "&&", "||",
"<<", ">>", "<<=", ">>=", "++", "--", ",",
// "and", "or", "xor", "not", "bitand", "bitor", "compl",
// "and_eq", "or_eq", "xor_eq", "not_eq",
"#", // extension for {lambda()#1}
};
std::sort(vec.begin(), vec.end(), [](string_view a, string_view b) { return a.size() > b.size(); });
return vec;
} ();
const std::array<string_view, 2> anonymous_namespace_spellings = {"(anonymous namespace)", "`anonymous namespace'"};
bool is_opening_punctuation(string_view token) {
return token == "(" || token == "[" || token == "{" || token == "<";
}
bool is_closing_punctuation(string_view token) {
return token == ")" || token == "]" || token == "}" || token == ">";
}
string_view get_corresponding_punctuation(string_view token) {
if(token == "(") {
return ")";
} else if(token == "[") {
return "]";
} else if(token == "{") {
return "}";
} else if(token == "<") {
return ">";
}
PANIC();
}
bool is_ignored_identifier(string_view string) {
return is_any(string, "const", "volatile", "decltype", "noexcept");
}
bool is_microsoft_calling_convention(string_view string) {
return is_any(
string,
"__cdecl",
"__clrcall",
"__stdcall",
"__fastcall",
"__thiscall",
"__vectorcall"
);
}
// There are five kinds of tokens in C++: identifiers, keywords, literals, operators, and other separators
// We tokenize a mostly-subset of this:
// - identifiers/keywords
// - literals: char, string, int, float. Msvc `strings' too.
// - punctuation
// Additionally we tokenize a few things that are useful
// - anonymous namespace tags
enum class token_type {
identifier,
punctuation,
literal,
anonymous_namespace
};
struct token {
token_type type;
string_view str;
bool operator==(const token& other) const {
return type == other.type && str == other.str;
}
};
bool is_pointer_ref(const token& token) {
return token.type == token_type::punctuation && is_any(token.str, "*", "&", "&&");
}
struct parse_error {
int x; // this works around a gcc bug with warn_unused_result and empty structs
explicit parse_error() = default;
string_view what() const {
return "Parse error";
}
};
#define CONCAT_IMPL(X, Y) X##Y
#define CONCAT(X, Y) CONCAT_IMPL(X, Y)
#define UNIQUE(X) CONCAT(X, __COUNTER__)
#define TRY_PARSE_IMPL(ACTION, SUCCESS, RES) \
Result<bool, parse_error> RES = (ACTION); \
if((RES).is_error()) { \
return std::move((RES)).unwrap_error(); \
} else if((RES).unwrap_value()) { \
SUCCESS; \
}
#define TRY_PARSE(ACTION, SUCCESS) TRY_PARSE_IMPL(ACTION, SUCCESS, UNIQUE(res))
#define TRY_TOK_IMPL(RES, ACTION, TMP) \
const auto TMP = (ACTION); \
if((TMP).is_error()) { \
return std::move((TMP)).unwrap_error(); \
} \
const auto RES = std::move((TMP)).unwrap_value()
#define TRY_TOK(RES, ACTION) TRY_TOK_IMPL(RES, ACTION, UNIQUE(tmp))
class symbol_tokenizer {
private:
string_view source;
optional<token> next_token;
bool peek(string_view text, size_t pos = 0) const {
return text == source.substr(pos, text.size());
}
NODISCARD Result<optional<token>, parse_error> peek_anonymous_namespace() const {
for(const auto& spelling : anonymous_namespace_spellings) {
if(peek(spelling)) {
return token{token_type::anonymous_namespace, {source.begin(), spelling.size()}};
}
}
return nullopt;
}
NODISCARD Result<optional<token>, parse_error> peek_number() const {
// More or less following pp-number https://eel.is/c++draft/lex.ppnumber
auto cursor = source.begin();
if(cursor != source.end() && std::isdigit(*cursor)) {
while(
cursor != source.end()
&& (
std::isdigit(*cursor)
|| is_identifier_continue(*cursor)
|| is_any(*cursor, '\'', '-', '+', '.')
)
) {
cursor++;
}
}
if(cursor == source.begin()) {
return nullopt;
}
return token{token_type::literal, {source.begin(), cursor}};
}
NODISCARD Result<optional<token>, parse_error> peek_msvc_string() const {
// msvc strings look like `this'
// they nest, e.g.: ``int main(void)'::`2'::<lambda_1>::operator()(void)const'
// TODO: Escapes?
auto cursor = source.begin();
if(cursor != source.end() && *cursor == '`') {
int depth = 0;
do {
if(*cursor == '`') {
depth++;
} else if(*cursor == '\'') {
depth--;
}
cursor++;
} while(cursor != source.end() && depth != 0);
if(depth != 0) {
return parse_error{};
}
}
if(cursor == source.begin()) {
return nullopt;
}
return token{token_type::literal, {source.begin(), cursor}};
}
NODISCARD Result<optional<token>, parse_error> parse_quoted_string() const {
auto cursor = source.begin();
if(cursor != source.end() && is_any(*cursor, '\'', '"')) {
auto closing_quote = *cursor;
cursor++;
while(cursor != source.end() && *cursor != closing_quote) {
if(*cursor == '\\') {
if(cursor + 1 == source.end()) {
return parse_error{};
}
cursor += 2;
}
cursor++;
}
if(cursor == source.end() || *cursor != closing_quote) {
return parse_error{};
}
cursor++;
}
if(cursor == source.begin()) {
return nullopt;
}
return token{token_type::literal, {source.begin(), cursor}};
}
NODISCARD Result<optional<token>, parse_error> peek_literal() const {
TRY_TOK(number, peek_number());
if(number) {
return number;
}
TRY_TOK(msvc_string, peek_msvc_string());
if(msvc_string) {
return msvc_string;
}
TRY_TOK(quoted_string, parse_quoted_string());
if(quoted_string) {
return quoted_string;
}
return nullopt;
}
NODISCARD Result<optional<token>, parse_error> peek_punctuation(size_t pos = 0) const {
for(const auto punctuation : punctuators_and_operators) {
if(peek(punctuation, pos)) {
return token{token_type::punctuation, {source.begin() + pos, punctuation.size()}};
}
}
return nullopt;
}
NODISCARD Result<optional<token>, parse_error> peek_identifier(size_t pos = 0) const {
auto start = source.begin() + std::min(pos, source.size());;
auto cursor = start;
if(cursor != source.end() && is_identifier_start(*cursor)) {
while(cursor != source.end() && is_identifier_continue(*cursor)) {
cursor++;
}
}
if(cursor == start) {
return nullopt;
}
return token{token_type::identifier, {start, cursor}};
}
NODISCARD token peek_misc() const {
ASSERT(!source.empty());
return token{token_type::punctuation, {source.begin(), 1}};
}
Result<monostate, parse_error> maybe_load_next_token() {
if(next_token.has_value()) {
return monostate{};
}
while(!source.empty() && std::isspace(source[0])) {
source.advance(1);
}
if(source.empty()) {
return monostate{};
}
TRY_TOK(anon, peek_anonymous_namespace());
if(anon) {
next_token = anon.unwrap();
return monostate{};
}
TRY_TOK(literal, peek_literal());
if(literal) {
next_token = literal.unwrap();
return monostate{};
}
TRY_TOK(punctuation, peek_punctuation());
if(punctuation) {
next_token = punctuation.unwrap();
return monostate{};
}
TRY_TOK(identifier, peek_identifier());
if(identifier) {
next_token = identifier.unwrap();
return monostate{};
}
next_token = peek_misc();
return monostate{};
}
optional<token> get_adjusted_next_token(bool in_template_argument_list) {
// https://eel.is/c++draft/temp.names#4 decompose >> to > when we think we're in a template argument list.
// We don't have to do this for >>= or >=.
if(next_token && in_template_argument_list && next_token.unwrap() == token{token_type::punctuation, ">>"}) {
auto copy = next_token.unwrap();
copy.str = copy.str.substr(0, 1); // ">"
return copy;
}
return next_token;
}
public:
symbol_tokenizer(string_view source) : source(source) {}
NODISCARD Result<optional<token>, parse_error> peek(bool in_template_argument_list = false) {
auto res = maybe_load_next_token();
if(res.is_error()) {
return res.unwrap_error();
}
return get_adjusted_next_token(in_template_argument_list);
}
Result<optional<token>, parse_error> advance(bool in_template_argument_list = false) {
TRY_TOK(next, peek(in_template_argument_list));
if(!next) {
return nullopt;
}
source.advance(next.unwrap().str.size());
next_token.reset();
return next;
}
NODISCARD Result<optional<token>, parse_error> accept(token_type type, bool in_template_argument_list = false) {
TRY_TOK(next, peek(in_template_argument_list));
if(next && next.unwrap().type == type) {
advance();
return next;
}
return nullopt;
}
NODISCARD Result<optional<token>, parse_error> accept(token token, bool in_template_argument_list = false) {
TRY_TOK(next, peek(in_template_argument_list));
if(next && next.unwrap() == token) {
advance();
return next;
}
return nullopt;
}
};
std::string prune_symbol(string_view symbol);
/*
Approximate grammar, very hacky:
full-symbol := { symbol }
symbol := symbol-fragment { "::" ["*"] symbol-fragment }
symbol-fragment := symbol-base [ pointer-refs ] [ punctuation-and-trailing-modifiers ]
symbol-base := symbol-term function-pointer
| symbol-term
| function-pointer
punctuation-and-trailing-modifiers := { [ balanced-punctuation ] [ ignored-identifier ] [ pointer-refs ] }
symbol-term := anonymous-namespace
| operator
| name
| lambda
| unnamed
function-pointer := "(" pointer-refs-junk symbol ")"
pointer-refs-junk := { function-pointer-modifier } { pointer-refs } { ignored-identifier }
anonymous-namespace := "(anonymous namespace)" | "`anonymous namespace'"
operator := "operator" operator-type
operator-type := new-delete
| special-decltype
| "co_await"
| "\"\"" IDENTIFIER
| OPERATOR
| matching-punctuation
| conversion-operator
new-delete := ( "new" | "delete" ) [ "[" "]" ]
special-decltype := "decltype" "(" ( "auto" | "nullptr" ) ")"
matching-punctuation := "(" ")"
| "[" "]"
conversion-operator := symbol [ symbol ] // kind of a hack
name := [ "~" ] IDENTIFIER
lambda := "{" "lambda" { PUNCTUATION } "#" LITERAL "}"
| LITERAL // 'lambda*' or `symbol'
| "<" IDENTIFIER ">" // lambda_*
unnamed := "{" "unnamed" "#" LITERAL "}"
| LITERAL // 'unnamed*'
balanced-punctuation := "(" balanced-punctuation-innards ")"
| "[" balanced-punctuation-innards "]"
| "<" balanced-punctuation-innards ">"
| "{" balanced-punctuation-innards "}"
balanced-punctuation-innards := { ANY-TOKEN } | balanced-punctuation
pointer-refs := { "*" | "&" | "&&" }
ignored-identifier := "const" | "volatile" | "decltype" | "noexcept"
function-pointer-modifier := "__cdecl" | "__clrcall" | "__stdcall" | "__fastcall" | "__thiscall" | "__vectorcall"
*/
class symbol_parser {
symbol_tokenizer& tokenizer;
std::string name_output;
bool last_was_identifier = false;
bool reset_output_flag = false;
void append_output(token token) {
auto is_identifier = token.type == token_type::identifier;
if(reset_output_flag) {
name_output.clear();
reset_output_flag = false;
last_was_identifier = false;
} else if(is_identifier && last_was_identifier) {
name_output += ' ';
}
name_output += token.str;
last_was_identifier = is_identifier;
}
NODISCARD Result<bool, parse_error> accept_pointer_ref(bool append) {
bool matched = false;
while(true) {
TRY_TOK(token, tokenizer.peek());
if(!token || !is_pointer_ref(token.unwrap())) {
break;
}
matched = true;
if(append) {
append_output(token.unwrap());
}
tokenizer.advance();
}
return matched;
}
NODISCARD Result<bool, parse_error> consume_balanced(string_view opening_punctuation) {
// priority means only consider this punctuation type and no priority means considers each independently
bool is_in_template_list = opening_punctuation == "<";
int depth = 1;
auto closing_punctuation = get_corresponding_punctuation(opening_punctuation);
while(depth > 0) {
// first try to accept an operator, this is for things like void foo<&S::operator>(S const&)>()::test
if(is_in_template_list) {
TRY_PARSE(accept_operator(true), continue);
}
// otherwise handle arbitrary token
TRY_TOK(next, tokenizer.advance(is_in_template_list));
if(!next) {
break;
}
if(next.unwrap().type == token_type::punctuation) {
if(next.unwrap().str == opening_punctuation) {
depth++;
} else if(next.unwrap().str == closing_punctuation) {
depth--;
} else if(is_in_template_list && is_opening_punctuation(next.unwrap().str)) {
// If we're in a template list, recurse into any balanced punctuation we see. This handles cases
// like foo<(S > S)>
TRY_PARSE(consume_balanced(next.unwrap().str), (void)0);
}
}
}
return true;
}
NODISCARD Result<bool, parse_error> accept_balanced_punctuation() {
TRY_TOK(token, tokenizer.peek());
if(token && token.unwrap().type == token_type::punctuation && is_opening_punctuation(token.unwrap().str)) {
tokenizer.advance();
TRY_PARSE(consume_balanced(token.unwrap().str), (void)0);
return true;
}
return false;
}
NODISCARD Result<bool, parse_error> accept_ignored_identifier() {
TRY_TOK(token, tokenizer.peek());
if(token && token.unwrap().type == token_type::identifier && is_ignored_identifier(token.unwrap().str)) {
tokenizer.advance();
return true;
}
return false;
}
NODISCARD Result<bool, parse_error> accept_anonymous_namespace() {
TRY_TOK(token, tokenizer.accept(token_type::anonymous_namespace));
if(token) {
append_output({ token_type::identifier, "(anonymous namespace)" });
return true;
}
return false;
}
NODISCARD Result<bool, parse_error> accept_new_delete() {
TRY_TOK(token, tokenizer.peek());
if(token && token.unwrap().type == token_type::identifier && is_any(token.unwrap().str, "new", "delete")) {
tokenizer.advance();
append_output(token.unwrap());
TRY_TOK(op, tokenizer.accept({token_type::punctuation, "["}));
if(op) {
append_output(op.unwrap());
TRY_TOK(op2, tokenizer.accept({token_type::punctuation, "]"}));
if(!op2) {
return parse_error{};
}
append_output(op2.unwrap());
}
return true;
}
return false;
}
NODISCARD Result<bool, parse_error> accept_special_decltype() {
TRY_TOK(token, tokenizer.accept({token_type::identifier, "decltype"}));
if(token) {
append_output(token.unwrap());
TRY_TOK(op, tokenizer.accept({token_type::punctuation, "("}));
if(!op) {
return parse_error{};
}
append_output(op.unwrap());
TRY_TOK(ident, tokenizer.accept(token_type::identifier));
if(!ident) {
return parse_error{};
}
if(!is_any(ident.unwrap().str, "auto", "nullptr")) {
return parse_error{};
}
append_output(ident.unwrap());
TRY_TOK(op2, tokenizer.accept({token_type::punctuation, ")"}));
if(!op2) {
return parse_error{};
}
append_output(op2.unwrap());
return true;
}
return false;
}
NODISCARD Result<bool, parse_error> accept_operator(bool is_in_template_list = false) {
// If we're in a template argument list we skip new/delete/auto/co_await/literal/conversion and only handle
// the operator punctuation case. We also don't append for template lists.
TRY_TOK(token, tokenizer.accept({token_type::identifier, "operator"}));
if(token) {
if(!is_in_template_list) {
append_output(token.unwrap());
TRY_PARSE(accept_new_delete(), return true);
TRY_PARSE(accept_special_decltype(), return true);
TRY_TOK(coawait, tokenizer.accept({token_type::identifier, "co_await"}));
if(coawait) {
append_output(coawait.unwrap());
return true;
}
TRY_TOK(literal, tokenizer.accept({token_type::literal, "\"\""}));
if(literal) {
TRY_TOK(name, tokenizer.accept(token_type::identifier));
if(!name) {
return parse_error{};
}
append_output(literal.unwrap());
append_output(name.unwrap());
return true;
}
}
TRY_TOK(op, tokenizer.accept(token_type::punctuation));
if(op) {
if(!is_in_template_list) {
append_output(op.unwrap());
}
if(is_any(op.unwrap().str, "(", "[")) {
TRY_TOK(op2, tokenizer.accept(token_type::punctuation));
if(!op2 || op2.unwrap().str != get_corresponding_punctuation(op.unwrap().str)) {
return parse_error{};
}
if(!is_in_template_list) {
append_output(op2.unwrap());
}
}
return true;
}
if(!is_in_template_list) {
// Otherwise try to parse a symbol, in the case of a conversion operator
// There is a bit of a grammer hack here, it doesn't properly "nest," but it works
TRY_PARSE(parse_symbol(), (void)0);
// In the case of a member function pointer, there will be a type symbol followed by a symbol for
// the type that the member pointer points to
TRY_TOK(maybe_ident, tokenizer.peek());
if(maybe_ident && maybe_ident.unwrap().type == token_type::identifier) {
TRY_PARSE(parse_symbol(), (void)0);
}
return true;
}
}
return false;
}
NODISCARD Result<bool, parse_error> accept_identifier_token() {
bool expect = false;
TRY_TOK(complement, tokenizer.accept({token_type::punctuation, "~"}));
if(complement) {
append_output(complement.unwrap());
expect = true;
}
TRY_TOK(token, tokenizer.accept(token_type::identifier));
if(token) {
append_output(token.unwrap());
return true;
} else if(expect) {
return parse_error{};
}
return false;
}
NODISCARD Result<bool, parse_error> consume_punctuation() {
bool did_consume = false;
while(true) {
TRY_TOK(token, tokenizer.peek());
if(
!token
|| !(
token.unwrap().type == token_type::punctuation
&& token.unwrap().str != "::"
&& token.unwrap().str != "#"
)
) {
break;
}
TRY_PARSE(accept_balanced_punctuation(), {did_consume = true; continue;});
// otherwise, if not balanced punctuation, just consume and drop
tokenizer.advance();
did_consume = true;
}
return did_consume;
}
NODISCARD Result<bool, parse_error> accept_lambda_or_unnamed() {
// LLVM does main::'lambda'<...>(...)::operator()<...>(...) -- apparently this can be 'lambda<count>'
// GCC does main::{lambda<...>(...)#1}::operator()<...>(...)
// MSVC does `int main(void)'::`2'::<lambda_1>::operator()<...>(...)
// https://github.com/llvm/llvm-project/blob/90beda2aba3cac34052827c560449fcb184c7313/libcxxabi/src/demangle/ItaniumDemangle.h#L1848-L1850 TODO: What about the count?
// https://github.com/gcc-mirror/gcc/blob/b76f1fb7bf8a7b66b8acd469309257f8b18c0c51/libiberty/cp-demangle.c#L6210-L6251 TODO: What special characters can appear?
TRY_TOK(opening_brace, tokenizer.accept({token_type::punctuation, "{"}));
if(opening_brace) {
token token1{};
token token2{};
bool two_tokens = false; // this awfulness to work around gcc's maybe-uninitialized analysis
TRY_TOK(lambda_token, tokenizer.accept({token_type::identifier, "lambda"}));
if(lambda_token) {
token1 = lambda_token.unwrap();
} else {
TRY_TOK(unnamed_token, tokenizer.accept({token_type::identifier, "unnamed"}));
if(!unnamed_token) {
return parse_error{};
}
TRY_TOK(type_token, tokenizer.accept({token_type::identifier, "type"}));
if(!type_token) {
return parse_error{};
}
token1 = unnamed_token.unwrap();
token2 = type_token.unwrap();
two_tokens = true;
}
TRY_PARSE(consume_punctuation(), (void)0);
TRY_TOK(hash_token, tokenizer.accept({token_type::punctuation, "#"}));
if(!hash_token) {
return parse_error{};
}
TRY_TOK(discriminator_token, tokenizer.accept(token_type::literal));
if(!discriminator_token) {
return parse_error{};
}
TRY_TOK(closing_brace, tokenizer.accept({token_type::punctuation, "}"}));
if(!closing_brace) {
return parse_error{};
}
append_output({token_type::punctuation, "<"});
append_output(token1);
if(two_tokens) {
append_output(token2);
}
append_output(hash_token.unwrap());
append_output(discriminator_token.unwrap());
append_output({token_type::punctuation, ">"});
return true;
}
TRY_TOK(maybe_literal_token, tokenizer.peek());
if(
maybe_literal_token
&& maybe_literal_token.unwrap().type == token_type::literal
&& (
maybe_literal_token.unwrap().str.starts_with("'lambda")
|| maybe_literal_token.unwrap().str.starts_with("'unnamed")
)
&& maybe_literal_token.unwrap().str.ends_with("'")
) {
tokenizer.advance();
append_output({token_type::punctuation, "<"});
auto str = maybe_literal_token.unwrap().str;
append_output({token_type::punctuation, str.substr(1, str.size() - 2)});
append_output({token_type::punctuation, ">"});
return true;
}
if(
maybe_literal_token
&& maybe_literal_token.unwrap().type == token_type::literal
&& maybe_literal_token.unwrap().str.starts_with("`")
&& maybe_literal_token.unwrap().str.ends_with("'")
) {
tokenizer.advance();
// append_output(maybe_literal_token.unwrap());
// This string is going to be another symbol, recursively reduce it
append_output({token_type::punctuation, "`"});
auto symbol = maybe_literal_token.unwrap().str;
ASSERT(symbol.size() >= 2);
auto name = detail::prune_symbol(symbol.substr(1, symbol.size() - 2));
append_output({token_type::literal, name});
append_output({token_type::punctuation, "'"});
return true;
}
TRY_TOK(opening_bracket, tokenizer.accept({token_type::punctuation, "<"}));
if(opening_bracket) {
TRY_TOK(lambda_token, tokenizer.accept(token_type::identifier));
if(!lambda_token || !lambda_token.unwrap().str.starts_with("lambda_")) {
return parse_error{};
}
TRY_TOK(closing_bracket, tokenizer.accept({token_type::punctuation, ">"}));
if(!closing_bracket) {
return parse_error{};
}
append_output(opening_bracket.unwrap());
append_output(lambda_token.unwrap());
append_output(closing_bracket.unwrap());
return true;
}
return false;
}
// lookahead to match "(*", "(__cdecl*", etc
NODISCARD bool lookahead_is_function_pointer() const {
auto res = [] (symbol_tokenizer tokenizer_copy) -> Result<bool, parse_error> {
TRY_TOK(maybe_opening_parenthesis, tokenizer_copy.accept({token_type::punctuation, "("}));
if(maybe_opening_parenthesis) {
while(true) {
auto res = tokenizer_copy.advance();
if(res.is_error()) {
return false;
}
auto token = res.unwrap_value();
if(token && is_pointer_ref(token.unwrap())) {
return true;
} else if(
token
&& token.unwrap().type == token_type::identifier
&& is_microsoft_calling_convention(token.unwrap().str)
) {
// pass
} else {
break;
}
}
return false;
}
return false;
} (tokenizer);
if(res.is_error()) {
return false;
}
return res.unwrap_value();
}
NODISCARD Result<bool, parse_error> parse_function_pointer() {
ASSERT(lookahead_is_function_pointer());
TRY_TOK(opening_parenthesis, tokenizer.accept({token_type::punctuation, "("}));
if(opening_parenthesis) {
bool saw_pointer = false;
while(true) {
TRY_TOK(next, tokenizer.peek());
if(next && is_pointer_ref(next.unwrap())) {
tokenizer.advance();
saw_pointer = true;
} else if(
next
&& next.unwrap().type == token_type::identifier
&& (
is_microsoft_calling_convention(next.unwrap().str)
|| is_ignored_identifier(next.unwrap().str)
)
) {
tokenizer.advance();
} else {
break;
}
}
if(!saw_pointer) {
return parse_error{};
}
bool did_parse = false;
reset_output_flag = true;
TRY_PARSE(parse_symbol(), did_parse = true);
if(!did_parse) {
return parse_error{};
}
TRY_TOK(closing_parenthesis, tokenizer.accept({token_type::punctuation, ")"}));
if(!closing_parenthesis) {
return parse_error{};
}
return true;
}
return false;
}
NODISCARD Result<bool, parse_error> parse_symbol_term() {
TRY_PARSE(accept_anonymous_namespace(), return true);
TRY_PARSE(accept_operator(), return true);
TRY_PARSE(accept_identifier_token(), return true);
TRY_PARSE(accept_lambda_or_unnamed(), return true);
return false;
}
NODISCARD Result<bool, parse_error> consume_punctuation_and_trailing_modifiers() {
bool did_consume = false;
while(true) {
TRY_TOK(token, tokenizer.peek());
if(!token) {
break;
}
TRY_PARSE(accept_balanced_punctuation(), {did_consume = true; continue;});
TRY_PARSE(accept_ignored_identifier(), {did_consume = true; continue;});
TRY_PARSE(accept_pointer_ref(false), {did_consume = true; continue;});
break;
}
return did_consume;
}
NODISCARD Result<bool, parse_error> parse_symbol() {
bool made_progress = false;
while(true) {
TRY_TOK(token, tokenizer.peek());
if(!token) {
break;
}
bool did_match_term = false;
TRY_PARSE(parse_symbol_term(), did_match_term = true);
if(lookahead_is_function_pointer()) {
TRY_PARSE(parse_function_pointer(), did_match_term = true);
}
if(did_match_term) {
made_progress = true;
} else {
break;
}
TRY_PARSE(accept_pointer_ref(true), made_progress = true);
TRY_PARSE(consume_punctuation_and_trailing_modifiers(), made_progress = true);
TRY_TOK(scope_resolution, tokenizer.accept({token_type::punctuation, "::"}));
if(scope_resolution) {
append_output(scope_resolution.unwrap());
made_progress = true;
// for pointer to members
TRY_TOK(star, tokenizer.accept({token_type::punctuation, "*"}));
if(star) {
append_output(star.unwrap());
}
} else {
break;
}
}
return made_progress;
}
public:
symbol_parser(symbol_tokenizer& tokenizer) : tokenizer(tokenizer) {}
NODISCARD Result<monostate, parse_error> parse() {
while(true) {
TRY_TOK(token, tokenizer.peek());
if(!token) {
break;
}
reset_output_flag = true;
bool made_progress = false;
TRY_PARSE(parse_symbol(), made_progress = true);
if(!made_progress) {
return parse_error{};
}
}
return monostate{};
}
NODISCARD std::string name() && {
return std::move(name_output);
}
};
NODISCARD std::string prune_symbol(string_view symbol) {
detail::symbol_tokenizer tokenizer(symbol);
detail::symbol_parser parser(tokenizer);
auto res = parser.parse();
if(res.is_error()) {
// error
return std::string(symbol);
}
auto name = std::move(parser).name();
if(name.empty()) {
return std::string(symbol);
}
return name;
}
}
CPPTRACE_END_NAMESPACE
CPPTRACE_BEGIN_NAMESPACE
std::string prune_symbol(const std::string& symbol) {
try {
return detail::prune_symbol(symbol);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
return symbol;
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,210 @@
#include "snippets/snippet.hpp"
#include <algorithm>
#include <cstdint>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <fstream>
#include <iostream>
#include "utils/common.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
constexpr std::int64_t max_size = 1024 * 1024 * 10; // 10 MiB
struct line_range {
std::size_t begin;
std::size_t end; // one past the end
};
class snippet_manager {
bool loaded_contents;
std::string contents;
// 1-based indexing
std::vector<line_range> line_table;
public:
snippet_manager(const std::string& path) : loaded_contents(false) {
std::ifstream file;
try {
file.open(path, std::ios::ate);
if(file.is_open()) {
std::ifstream::pos_type size = file.tellg();
if(size == std::ifstream::pos_type(-1) || size > max_size) {
return;
}
// else load file
file.seekg(0, std::ios::beg);
contents.resize(to<std::size_t>(size));
if(!file.read(&contents[0], size)) {
// error ...
}
build_line_table();
loaded_contents = true;
}
} catch(const std::ifstream::failure&) {
// ...
}
}
// takes a 1-index line
std::string get_line(std::size_t line) const {
if(!loaded_contents || line > line_table.size()) {
return "";
} else {
return contents.substr(line_table[line].begin, line_table[line].end - line_table[line].begin);
}
}
std::size_t num_lines() const {
return line_table.size();
}
bool ok() const {
return loaded_contents;
}
private:
void build_line_table() {
line_table.push_back({0, 0});
std::size_t pos = 0; // stores the start of the current line
while(true) {
// find the end of the current line
std::size_t terminator_pos = contents.find('\n', pos);
if(terminator_pos == std::string::npos) {
line_table.push_back({pos, contents.size()});
break;
} else {
std::size_t end_pos = terminator_pos; // one past the end of the current line
if(end_pos > 0 && contents[end_pos - 1] == '\r') {
end_pos--;
}
line_table.push_back({pos, end_pos});
pos = terminator_pos + 1;
}
}
}
};
const snippet_manager& get_manager(const std::string& path) {
static std::mutex snippet_manager_mutex;
static std::unordered_map<std::string, const snippet_manager> snippet_managers;
std::unique_lock<std::mutex> lock(snippet_manager_mutex);
auto it = snippet_managers.find(path);
if(it == snippet_managers.end()) {
return snippet_managers.insert({path, snippet_manager(path)}).first->second;
} else {
return it->second;
}
}
// how wide the margin for the line number should be
constexpr std::size_t margin_width = 8;
struct snippet_context {
std::size_t original_begin;
std::size_t begin;
std::size_t end;
std::vector<std::string> lines;
};
optional<snippet_context> get_lines(const std::string& path, std::size_t target_line, std::size_t context_size) {
const auto& manager = get_manager(path);
if(!manager.ok()) {
return nullopt;
}
auto begin = target_line <= context_size + 1 ? 1 : target_line - context_size;
auto original_begin = begin;
auto end = std::min(target_line + context_size, manager.num_lines() - 1);
std::vector<std::string> lines;
for(auto line = begin; line <= end; line++) {
lines.push_back(manager.get_line(line));
}
// trim blank lines
while(begin < target_line && lines[begin - original_begin].empty()) {
begin++;
}
while(end > target_line && lines[end - original_begin].empty()) {
end--;
}
return snippet_context{original_begin, begin, end, std::move(lines)};
}
std::string write_line_number(std::size_t line, std::size_t target_line, bool color) {
std::string snippet;
auto line_str = std::to_string(line);
if(line == target_line) {
if(color) {
snippet += YELLOW;
}
auto line_width = line_str.size() + 3;
snippet += microfmt::format(
"{>{}} > {}: ",
line_width > margin_width ? 0 : margin_width - line_width,
"",
line_str
);
if(color) {
snippet += RESET;
}
} else {
snippet += microfmt::format("{>{}}: ", margin_width, line_str);
}
return snippet;
}
std::string write_carrot(std::uint32_t column, bool color) {
std::string snippet;
snippet += microfmt::format("\n{>{}}", margin_width + 2 + column - 1, "");
if(color) {
snippet += YELLOW;
}
snippet += "^";
if(color) {
snippet += RESET;
}
return snippet;
}
std::string write_line(
std::size_t line,
nullable<std::uint32_t> column,
std::size_t target_line,
bool color,
const snippet_context& context
) {
std::string snippet;
snippet += write_line_number(line, target_line, color);
snippet += context.lines[line - context.original_begin];
if(line == target_line && column.has_value()) {
snippet += write_carrot(column.value(), color);
}
return snippet;
}
// 1-indexed line
std::string get_snippet(
const std::string& path,
std::size_t target_line,
nullable<std::uint32_t> column,
std::size_t context_size,
bool color
) {
const auto context_res = get_lines(path, target_line, context_size);
if(!context_res) {
return "";
}
const auto& context = context_res.unwrap();
std::string snippet;
for(auto line = context.begin; line <= context.end; line++) {
snippet += write_line(line, column, target_line, color, context);
if(line != context.end) {
snippet += '\n';
}
}
return snippet;
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,22 @@
#ifndef SNIPPET_HPP
#define SNIPPET_HPP
#include <cstddef>
#include <string>
#include <cpptrace/basic.hpp>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
// 1-indexed line
std::string get_snippet(
const std::string& path,
std::size_t line,
nullable<std::uint32_t> column,
std::size_t context_size,
bool color
);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,208 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF
#include "symbols/dwarf/resolver.hpp"
#include <cpptrace/basic.hpp>
#include "symbols/symbols.hpp"
#include "utils/common.hpp"
#include "utils/error.hpp"
#include "binary/object.hpp"
#include "binary/mach-o.hpp"
#include "utils/utils.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace libdwarf {
#if IS_APPLE
struct target_object {
std::string object_path;
bool path_ok = true;
optional<std::unordered_map<std::string, uint64_t>> symbols;
std::unique_ptr<symbol_resolver> resolver;
target_object(std::string object_path) : object_path(std::move(object_path)) {}
std::unique_ptr<symbol_resolver>& get_resolver() {
if(!resolver) {
// this seems silly but it's an attempt to not repeatedly try to initialize new dwarf_resolvers if
// exceptions are thrown, e.g. if the path doesn't exist
resolver = detail::make_unique<null_resolver>();
resolver = make_dwarf_resolver(object_path);
}
return resolver;
}
std::unordered_map<std::string, uint64_t>& get_symbols() {
if(!symbols) {
// this is an attempt to not repeatedly try to reprocess mach-o files if exceptions are thrown, e.g. if
// the path doesn't exist
std::unordered_map<std::string, uint64_t> symbols;
this->symbols = symbols;
auto mach_o_object = open_mach_o_cached(object_path);
if(!mach_o_object) {
return this->symbols.unwrap();
}
const auto& symbol_table = mach_o_object.unwrap_value()->symbol_table();
if(!symbol_table) {
return this->symbols.unwrap();
}
for(const auto& symbol : symbol_table.unwrap_value()) {
symbols[symbol.name] = symbol.address;
}
this->symbols = std::move(symbols);
}
return symbols.unwrap();
}
CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
frame_with_inlines resolve_frame(
const object_frame& frame_info,
const std::string& symbol_name,
std::size_t offset
) {
const auto& symbol_table = get_symbols();
auto it = symbol_table.find(symbol_name);
if(it != symbol_table.end()) {
auto frame = frame_info;
// substitute a translated address object for the target file in
frame.object_address = it->second + offset;
auto res = get_resolver()->resolve_frame(frame);
// replace the translated address with the object address in the binary
res.frame.object_address = frame_info.object_address;
return res;
} else {
return {
{
frame_info.raw_address,
frame_info.object_address,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
frame_info.object_path,
symbol_name,
false
},
{}
};
}
}
};
struct debug_map_symbol_info {
uint64_t source_address;
uint64_t size;
std::string name;
nullable<uint64_t> target_address; // T(-1) is used as a sentinel
std::size_t object_index; // index into target_objects
};
class debug_map_resolver : public symbol_resolver {
std::vector<target_object> target_objects;
std::vector<debug_map_symbol_info> symbols;
public:
debug_map_resolver(const std::string& source_object_path) {
// load mach-o
// TODO: Cache somehow?
auto mach_o_object = open_mach_o_cached(source_object_path);
if(!mach_o_object) {
return;
}
mach_o& source_mach = *mach_o_object.unwrap_value();
auto source_debug_map = source_mach.get_debug_map();
if(!source_debug_map) {
return;
}
// get symbol entries from debug map, as well as the various object files used to make this binary
for(auto& entry : source_debug_map.unwrap_value()) {
// object it came from
target_objects.push_back({entry.first});
// push the symbols
auto& map_entry_symbols = entry.second;
symbols.reserve(symbols.size() + map_entry_symbols.size());
for(auto& symbol : map_entry_symbols) {
symbols.push_back({
symbol.source_address,
symbol.size,
std::move(symbol.name),
nullable<uint64_t>::null(),
target_objects.size() - 1
});
}
}
// sort for binary lookup later
// TODO: Redundant?
std::sort(
symbols.begin(),
symbols.end(),
[] (
const debug_map_symbol_info& a,
const debug_map_symbol_info& b
) {
return a.source_address < b.source_address;
}
);
}
CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
frame_with_inlines resolve_frame(const object_frame& frame_info) override {
// resolve object frame:
// find the symbol in this executable corresponding to the object address
// resolve the symbol in the object it came from, based on the symbol name
auto closest_symbol_it = first_less_than_or_equal(
symbols.begin(),
symbols.end(),
frame_info.object_address,
[] (
uint64_t pc,
const debug_map_symbol_info& symbol
) {
return pc < symbol.source_address;
}
);
if(closest_symbol_it != symbols.end()) {
if(frame_info.object_address <= closest_symbol_it->source_address + closest_symbol_it->size) {
return target_objects[closest_symbol_it->object_index].resolve_frame(
{
frame_info.raw_address,
// the resolver doesn't care about the object address here, only the offset from the start
// of the symbol and it'll lookup the symbol's base-address
frame_info.object_address,
frame_info.object_path
},
closest_symbol_it->name,
frame_info.object_address - closest_symbol_it->source_address
);
}
}
// There was either no closest symbol or the closest symbol didn't end up containing the address we're
// looking for, so just return a blank frame
return {
{
frame_info.raw_address,
frame_info.object_address,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
frame_info.object_path,
"",
false
},
{}
};
};
};
std::unique_ptr<symbol_resolver> make_debug_map_resolver(const std::string& object_path) {
return detail::make_unique<debug_map_resolver>(object_path);
}
#endif
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,542 @@
#ifndef DWARF_HPP
#define DWARF_HPP
#include <cpptrace/basic.hpp>
#include "utils/error.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
#include <functional>
#include <type_traits>
#ifdef CPPTRACE_USE_NESTED_LIBDWARF_HEADER_PATH
#include <libdwarf/libdwarf.h>
#include <libdwarf/dwarf.h>
#else
#include <libdwarf.h>
#include <dwarf.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace libdwarf {
static_assert(std::is_pointer<Dwarf_Die>::value, "Dwarf_Die not a pointer");
static_assert(std::is_pointer<Dwarf_Debug>::value, "Dwarf_Debug not a pointer");
using rangelist_entries = std::vector<std::pair<Dwarf_Addr, Dwarf_Addr>>;
[[noreturn]] inline void handle_dwarf_error(Dwarf_Debug dbg, Dwarf_Error error) {
Dwarf_Unsigned ev = dwarf_errno(error);
// dwarf_dealloc_error deallocates the message, attaching to msg is convenient
auto msg = raii_wrap(dwarf_errmsg(error), [dbg, error] (char*) { dwarf_dealloc_error(dbg, error); });
throw internal_error("dwarf error {} {}", ev, msg.get());
}
struct die_object {
Dwarf_Debug dbg = nullptr;
Dwarf_Die die = nullptr;
// Error handling helper
// For some reason R (*f)(Args..., void*)-style deduction isn't possible, seems like a bug in all compilers
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56190
template<
typename... Args,
typename... Args2,
typename std::enable_if<
std::is_same<
decltype(
(void)std::declval<int(Args...)>()(std::forward<Args2>(std::declval<Args2>())..., nullptr)
),
void
>::value,
int
>::type = 0
>
int wrap(int (*f)(Args...), Args2&&... args) const {
Dwarf_Error error = nullptr;
int ret = f(std::forward<Args2>(args)..., &error);
if(ret == DW_DLV_ERROR) {
handle_dwarf_error(dbg, error);
}
return ret;
}
die_object(Dwarf_Debug dbg, Dwarf_Die die) : dbg(dbg), die(die) {
ASSERT(dbg != nullptr);
}
~die_object() {
release();
}
void release() {
if(die) {
dwarf_dealloc_die(exchange(die, nullptr));
}
}
die_object(const die_object&) = delete;
die_object& operator=(const die_object&) = delete;
// dbg doesn't strictly have to be st to null but it helps ensure attempts to use the die_object after this to
// segfault. A valid use otherwise would be moved_from.get_sibling() which would get the next CU.
die_object(die_object&& other) noexcept
: dbg(exchange(other.dbg, nullptr)), die(exchange(other.die, nullptr)) {}
die_object& operator=(die_object&& other) noexcept {
release();
dbg = exchange(other.dbg, nullptr);
die = exchange(other.die, nullptr);
return *this;
}
die_object clone() const {
Dwarf_Off global_offset = get_global_offset();
Dwarf_Bool is_info = dwarf_get_die_infotypes_flag(die);
Dwarf_Die die_copy = nullptr;
VERIFY(wrap(dwarf_offdie_b, dbg, global_offset, is_info, &die_copy) == DW_DLV_OK);
return {dbg, die_copy};
}
die_object get_child() const {
Dwarf_Die child = nullptr;
int ret = wrap(dwarf_child, die, &child);
if(ret == DW_DLV_OK) {
return die_object(dbg, child);
} else if(ret == DW_DLV_NO_ENTRY) {
return die_object(dbg, nullptr);
} else {
PANIC();
}
}
die_object get_sibling() const {
Dwarf_Die sibling = nullptr;
int ret = wrap(dwarf_siblingof_b, dbg, die, true, &sibling);
if(ret == DW_DLV_OK) {
return die_object(dbg, sibling);
} else if(ret == DW_DLV_NO_ENTRY) {
return die_object(dbg, nullptr);
} else {
PANIC();
}
}
operator bool() const {
return die != nullptr;
}
Dwarf_Die get() const {
return die;
}
std::string get_name() const {
char empty[] = "";
char* name = empty;
// Note: It's important to not free the string from this function.
int ret = wrap(dwarf_diename, die, &name);
std::string str;
if(ret != DW_DLV_NO_ENTRY) {
str = name;
}
return name;
}
optional<std::string> get_string_attribute(Dwarf_Half attr_num) const {
Dwarf_Attribute attr;
if(wrap(dwarf_attr, die, attr_num, &attr) == DW_DLV_OK) {
auto attwrapper = raii_wrap(attr, [] (Dwarf_Attribute attr) { dwarf_dealloc_attribute(attr); });
char* raw_str;
// Note: It's important to not free the string from this function.
// https://github.com/davea42/libdwarf-code/issues/279
VERIFY(wrap(dwarf_formstring, attr, &raw_str) == DW_DLV_OK);
std::string str = raw_str;
return str;
} else {
return nullopt;
}
}
optional<Dwarf_Unsigned> get_unsigned_attribute(Dwarf_Half attr_num) const {
Dwarf_Attribute attr;
if(wrap(dwarf_attr, die, attr_num, &attr) == DW_DLV_OK) {
auto attwrapper = raii_wrap(attr, [] (Dwarf_Attribute attr) { dwarf_dealloc_attribute(attr); });
// Dwarf_Half form = 0;
// VERIFY(wrap(dwarf_whatform, attr, &form) == DW_DLV_OK);
Dwarf_Unsigned val;
VERIFY(wrap(dwarf_formudata, attr, &val) == DW_DLV_OK);
return val;
} else {
return nullopt;
}
}
bool has_attr(Dwarf_Half attr_num) const {
Dwarf_Bool present = false;
VERIFY(wrap(dwarf_hasattr, die, attr_num, &present) == DW_DLV_OK);
return present;
}
Dwarf_Half get_tag() const {
Dwarf_Half tag = 0;
VERIFY(wrap(dwarf_tag, die, &tag) == DW_DLV_OK);
return tag;
}
const char* get_tag_name() const {
const char* tag_name;
if(dwarf_get_TAG_name(get_tag(), &tag_name) == DW_DLV_OK) {
return tag_name;
} else {
return "<unknown tag name>";
}
}
Dwarf_Off get_global_offset() const {
Dwarf_Off off;
VERIFY(wrap(dwarf_dieoffset, die, &off) == DW_DLV_OK);
return off;
}
die_object resolve_reference_attribute(Dwarf_Half attr_num) const {
Dwarf_Attribute attr;
VERIFY(dwarf_attr(die, attr_num, &attr, nullptr) == DW_DLV_OK);
auto wrapper = raii_wrap(attr, [] (Dwarf_Attribute attr) { dwarf_dealloc_attribute(attr); });
Dwarf_Half form = 0;
VERIFY(wrap(dwarf_whatform, attr, &form) == DW_DLV_OK);
switch(form) {
case DW_FORM_ref1:
case DW_FORM_ref2:
case DW_FORM_ref4:
case DW_FORM_ref8:
case DW_FORM_ref_udata:
{
Dwarf_Off off = 0;
Dwarf_Bool is_info = dwarf_get_die_infotypes_flag(die);
VERIFY(wrap(dwarf_formref, attr, &off, &is_info) == DW_DLV_OK);
Dwarf_Off global_offset = 0;
VERIFY(wrap(dwarf_convert_to_global_offset, attr, off, &global_offset) == DW_DLV_OK);
Dwarf_Die target = nullptr;
VERIFY(wrap(dwarf_offdie_b, dbg, global_offset, is_info, &target) == DW_DLV_OK);
return die_object(dbg, target);
}
case DW_FORM_ref_addr:
{
Dwarf_Off off;
VERIFY(wrap(dwarf_global_formref, attr, &off) == DW_DLV_OK);
int is_info = dwarf_get_die_infotypes_flag(die);
Dwarf_Die target = nullptr;
VERIFY(wrap(dwarf_offdie_b, dbg, off, is_info, &target) == DW_DLV_OK);
return die_object(dbg, target);
}
case DW_FORM_ref_sig8:
{
Dwarf_Sig8 signature;
VERIFY(wrap(dwarf_formsig8, attr, &signature) == DW_DLV_OK);
Dwarf_Die target = nullptr;
Dwarf_Bool targ_is_info = false;
VERIFY(wrap(dwarf_find_die_given_sig8, dbg, &signature, &target, &targ_is_info) == DW_DLV_OK);
return die_object(dbg, target);
}
default:
PANIC(microfmt::format("unknown form for attribute {} {}\n", attr_num, form));
}
}
Dwarf_Unsigned get_ranges_base_address(const die_object& cu_die) const {
// After libdwarf v0.11.0 this can use dwarf_get_ranges_baseaddress, however, in the interest of not
// requiring v0.11.0 just yet the logic is implemented here too.
// The base address is:
// - If the die has a rangelist, use the low_pc for that die
// - Otherwise use the low_pc from the CU if present
// - Otherwise 0
if(has_attr(DW_AT_ranges)) {
if(has_attr(DW_AT_low_pc)) {
Dwarf_Addr lowpc;
if(wrap(dwarf_lowpc, die, &lowpc) == DW_DLV_OK) {
return lowpc;
}
}
}
if(cu_die.has_attr(DW_AT_low_pc)) {
Dwarf_Addr lowpc;
if(wrap(dwarf_lowpc, cu_die.get(), &lowpc) == DW_DLV_OK) {
return lowpc;
}
}
return 0;
}
Dwarf_Unsigned get_ranges_offset(Dwarf_Attribute attr) const {
Dwarf_Unsigned off = 0;
Dwarf_Half form = 0;
VERIFY(wrap(dwarf_whatform, attr, &form) == DW_DLV_OK);
if (form == DW_FORM_rnglistx) {
VERIFY(wrap(dwarf_formudata, attr, &off) == DW_DLV_OK);
} else {
VERIFY(wrap(dwarf_global_formref, attr, &off) == DW_DLV_OK);
}
return off;
}
template<typename F>
// callback should return true to keep going
void dwarf5_ranges(F callback) const {
Dwarf_Attribute attr = nullptr;
if(wrap(dwarf_attr, die, DW_AT_ranges, &attr) != DW_DLV_OK) {
return;
}
auto attrwrapper = raii_wrap(attr, [] (Dwarf_Attribute attr) { dwarf_dealloc_attribute(attr); });
Dwarf_Unsigned offset = get_ranges_offset(attr);
Dwarf_Half form = 0;
VERIFY(wrap(dwarf_whatform, attr, &form) == DW_DLV_OK);
// get .debug_rnglists info
Dwarf_Rnglists_Head head = nullptr;
Dwarf_Unsigned rnglists_entries = 0;
Dwarf_Unsigned dw_global_offset_of_rle_set = 0;
int res = wrap(
dwarf_rnglists_get_rle_head,
attr,
form,
offset,
&head,
&rnglists_entries,
&dw_global_offset_of_rle_set
);
auto headwrapper = raii_wrap(head, [] (Dwarf_Rnglists_Head head) { dwarf_dealloc_rnglists_head(head); });
if(res == DW_DLV_NO_ENTRY) {
return;
}
VERIFY(res == DW_DLV_OK);
for(std::size_t i = 0 ; i < rnglists_entries; i++) {
unsigned entrylen = 0;
unsigned rle_value_out = 0;
Dwarf_Unsigned raw1 = 0;
Dwarf_Unsigned raw2 = 0;
Dwarf_Bool unavailable = 0;
Dwarf_Unsigned cooked1 = 0;
Dwarf_Unsigned cooked2 = 0;
res = wrap(
dwarf_get_rnglists_entry_fields_a,
head,
i,
&entrylen,
&rle_value_out,
&raw1,
&raw2,
&unavailable,
&cooked1,
&cooked2
);
if(res == DW_DLV_NO_ENTRY) {
continue;
}
VERIFY(res == DW_DLV_OK);
if(unavailable) {
continue;
}
switch(rle_value_out) {
// Following the same scheme from libdwarf-addr2line
case DW_RLE_end_of_list:
case DW_RLE_base_address:
case DW_RLE_base_addressx:
// Already handled
break;
case DW_RLE_offset_pair:
case DW_RLE_startx_endx:
case DW_RLE_start_end:
case DW_RLE_startx_length:
case DW_RLE_start_length:
if(!callback(cooked1, cooked2)) {
return;
}
break;
default:
PANIC("Something is wrong");
break;
}
}
}
template<typename F>
// callback should return true to keep going
void dwarf4_ranges(Dwarf_Addr baseaddr, F callback) const {
Dwarf_Attribute attr = nullptr;
if(wrap(dwarf_attr, die, DW_AT_ranges, &attr) != DW_DLV_OK) {
return;
}
auto attrwrapper = raii_wrap(attr, [] (Dwarf_Attribute attr) { dwarf_dealloc_attribute(attr); });
Dwarf_Unsigned offset;
if(wrap(dwarf_global_formref, attr, &offset) != DW_DLV_OK) {
return;
}
Dwarf_Addr baseaddr_original = baseaddr;
Dwarf_Ranges* ranges = nullptr;
Dwarf_Signed count = 0;
VERIFY(
wrap(
dwarf_get_ranges_b,
dbg,
offset,
die,
nullptr,
&ranges,
&count,
nullptr
) == DW_DLV_OK
);
auto rangeswrapper = raii_wrap(
ranges,
[this, count] (Dwarf_Ranges* ranges) { dwarf_dealloc_ranges(dbg, ranges, count); }
);
for(int i = 0; i < count; i++) {
if(ranges[i].dwr_type == DW_RANGES_ENTRY) {
if(!callback(baseaddr + ranges[i].dwr_addr1, baseaddr + ranges[i].dwr_addr2)) {
return;
}
} else if(ranges[i].dwr_type == DW_RANGES_ADDRESS_SELECTION) {
baseaddr = ranges[i].dwr_addr2;
} else {
ASSERT(ranges[i].dwr_type == DW_RANGES_END);
baseaddr = baseaddr_original;
}
}
}
template<typename F>
// callback should return true to keep going
void dwarf_ranges(const die_object& cu_die, int version, F callback) const {
Dwarf_Addr lowpc;
if(wrap(dwarf_lowpc, die, &lowpc) == DW_DLV_OK) {
Dwarf_Addr highpc = 0;
enum Dwarf_Form_Class return_class;
if(wrap(dwarf_highpc_b, die, &highpc, nullptr, &return_class) == DW_DLV_OK) {
if(return_class == DW_FORM_CLASS_CONSTANT) {
highpc += lowpc;
}
if(!callback(lowpc, highpc)) {
return;
}
}
}
if(version >= 5) {
dwarf5_ranges(callback);
} else {
dwarf4_ranges(get_ranges_base_address(cu_die), callback);
}
}
rangelist_entries get_rangelist_entries(const die_object& cu_die, int version) const {
rangelist_entries vec;
dwarf_ranges(cu_die, version, [&vec] (Dwarf_Addr low, Dwarf_Addr high) {
// Simple coalescing optimization:
// Sometimes the range list entries are really continuous: [100, 200), [200, 300)
// Other times there's just one byte of separation [300, 399), [400, 500)
// Those are the main two cases I've observed.
// This will not catch all cases, presumably, as the range lists aren't sorted. But compilers/linkers
// seem to like to emit the ranges in sorted order.
if(!vec.empty() && low - vec.back().second <= 1) {
vec.back().second = high;
} else {
vec.push_back({low, high});
}
return true;
});
return vec;
}
Dwarf_Bool pc_in_die(const die_object& cu_die, int version, Dwarf_Addr pc) const {
bool found = false;
dwarf_ranges(cu_die, version, [&found, pc] (Dwarf_Addr low, Dwarf_Addr high) {
if(pc >= low && pc < high) {
found = true;
return false;
}
return true;
});
return found;
}
void print() const {
std::fprintf(
stderr,
"%08llx %s %s\n",
to_ull(get_global_offset()),
get_tag_name(),
get_name().c_str()
);
}
};
// walk die list, callback is called on each die and should return true to
// continue traversal
// returns true if traversal should continue
inline bool walk_die_list(
const die_object& die,
const std::function<bool(const die_object&)>& fn
) {
// TODO: Refactor so there is only one fn call
bool continue_traversal = true;
if(fn(die)) {
die_object current = die.get_sibling();
while(current) {
if(fn(current)) {
current = current.get_sibling();
} else {
continue_traversal = false;
break;
}
}
}
return continue_traversal;
}
// walk die list, recursing into children, callback is called on each die
// and should return true to continue traversal
// returns true if traversal should continue
inline bool walk_die_list_recursive(
const die_object& die,
const std::function<bool(const die_object&)>& fn
) {
return walk_die_list(
die,
[&fn](const die_object& die) {
auto child = die.get_child();
if(child) {
if(!walk_die_list_recursive(child, fn)) {
return false;
}
}
return fn(die);
}
);
}
class maybe_owned_die_object {
// Hacky... I wish std::variant existed.
optional<die_object> owned_die;
optional<const die_object*> ref_die;
maybe_owned_die_object(die_object&& die) : owned_die(std::move(die)) {}
maybe_owned_die_object(const die_object& die) : ref_die(&die) {}
public:
static maybe_owned_die_object owned(die_object&& die) {
return maybe_owned_die_object{std::move(die)};
}
static maybe_owned_die_object ref(const die_object& die) {
return maybe_owned_die_object{die};
}
const die_object& get() {
ASSERT(owned_die || ref_die, "Mal-formed maybe_owned_die_object");
if(owned_die) {
return owned_die.unwrap();
} else {
return *ref_die.unwrap();
}
}
};
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,33 @@
#include "symbols/dwarf/dwarf_options.hpp"
#include <cpptrace/utils.hpp>
#include <atomic>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::atomic<nullable<std::size_t>> dwarf_resolver_line_table_cache_size{nullable<std::size_t>::null()};
std::atomic<bool> dwarf_resolver_disable_aranges{false};
optional<std::size_t> get_dwarf_resolver_line_table_cache_size() {
auto max_entries = dwarf_resolver_line_table_cache_size.load();
return max_entries.has_value() ? optional<std::size_t>(max_entries.value()) : nullopt;
}
bool get_dwarf_resolver_disable_aranges() {
return dwarf_resolver_disable_aranges.load();
}
}
CPPTRACE_END_NAMESPACE
CPPTRACE_BEGIN_NAMESPACE
namespace experimental {
void set_dwarf_resolver_line_table_cache_size(nullable<std::size_t> max_entries) {
detail::dwarf_resolver_line_table_cache_size.store(max_entries);
}
void set_dwarf_resolver_disable_aranges(bool disable) {
detail::dwarf_resolver_disable_aranges.store(disable);
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,15 @@
#ifndef DWARF_OPTIONS_HPP
#define DWARF_OPTIONS_HPP
#include "utils/optional.hpp"
#include <cstddef>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
optional<std::size_t> get_dwarf_resolver_line_table_cache_size();
bool get_dwarf_resolver_disable_aranges();
}
CPPTRACE_END_NAMESPACE
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,163 @@
#ifndef DWARF_UTILS_HPP
#define DWARF_UTILS_HPP
#include <cpptrace/basic.hpp>
#include "symbols/dwarf/dwarf.hpp" // has dwarf #includes
#include "utils/error.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace libdwarf {
class srcfiles {
Dwarf_Debug dbg = nullptr;
char** dw_srcfiles = nullptr;
Dwarf_Unsigned dw_filecount = 0;
public:
srcfiles(Dwarf_Debug dbg, char** dw_srcfiles, Dwarf_Signed filecount)
: dbg(dbg), dw_srcfiles(dw_srcfiles), dw_filecount(static_cast<Dwarf_Unsigned>(filecount))
{
if(filecount < 0) {
throw internal_error("Unexpected dw_filecount {}", filecount);
}
}
~srcfiles() {
release();
}
void release() {
if(dw_srcfiles) {
for(unsigned i = 0; i < dw_filecount; i++) {
dwarf_dealloc(dbg, dw_srcfiles[i], DW_DLA_STRING);
dw_srcfiles[i] = nullptr;
}
dwarf_dealloc(dbg, dw_srcfiles, DW_DLA_LIST);
dw_srcfiles = nullptr;
}
}
srcfiles(const srcfiles&) = delete;
srcfiles(srcfiles&& other) {
*this = std::move(other);
}
srcfiles& operator=(const srcfiles&) = delete;
srcfiles& operator=(srcfiles&& other) {
release();
dbg = exchange(other.dbg, nullptr);
dw_srcfiles = exchange(other.dw_srcfiles, nullptr);
dw_filecount = exchange(other.dw_filecount, 0);
return *this;
}
// note: dwarf uses 1-indexing
const char* get(Dwarf_Unsigned file_i) const {
if(file_i >= dw_filecount) {
throw internal_error(
"Error while accessing the srcfiles list, requested index {} is out of bounds (count = {})",
file_i,
dw_filecount
);
}
return dw_srcfiles[file_i];
}
Dwarf_Unsigned count() const {
return dw_filecount;
}
};
// container of items which are keyed by ranges
template<typename K, typename V>
class range_map {
public:
struct handle {
std::uint32_t index;
};
private:
struct PACKED range_entry {
handle item;
K low;
K high;
};
std::vector<V> items;
std::vector<range_entry> range_entries;
public:
handle add_item(V&& item) {
items.push_back(std::move(item));
VERIFY(items.size() < std::numeric_limits<std::uint32_t>::max());
return handle{static_cast<std::uint32_t>(items.size() - 1)};
}
void insert(handle handle, K low, K high) {
range_entries.push_back({handle, low, high});
}
void finalize() {
std::sort(range_entries.begin(), range_entries.end(), [] (const range_entry& a, const range_entry& b) {
return a.low < b.low;
});
}
std::size_t ranges_count() const {
return range_entries.size();
}
optional<const V&> lookup(K key) const {
auto vec_it = first_less_than_or_equal(
range_entries.begin(),
range_entries.end(),
key,
[] (K key, const range_entry& entry) {
return key < entry.low;
}
);
if(vec_it == range_entries.end()) {
return nullopt;
}
return items.at(vec_it->item.index);
}
};
struct line_entry {
Dwarf_Addr low;
// Dwarf_Addr high;
// int i;
Dwarf_Line line;
optional<std::string> path;
optional<std::uint32_t> line_number;
optional<std::uint32_t> column_number;
line_entry(Dwarf_Addr low, Dwarf_Line line) : low(low), line(line) {}
};
struct line_table_info {
Dwarf_Unsigned version = 0;
Dwarf_Line_Context line_context = nullptr;
// sorted by low_addr
// TODO: Make this optional at some point, it may not be generated if cache mode switches during program exec...
std::vector<line_entry> line_entries;
line_table_info(
Dwarf_Unsigned version,
Dwarf_Line_Context line_context,
std::vector<line_entry>&& line_entries
) : version(version), line_context(line_context), line_entries(std::move(line_entries)) {}
~line_table_info() {
release();
}
void release() {
dwarf_srclines_dealloc_b(line_context);
line_context = nullptr;
}
line_table_info(const line_table_info&) = delete;
line_table_info(line_table_info&& other) {
*this = std::move(other);
}
line_table_info& operator=(const line_table_info&) = delete;
line_table_info& operator=(line_table_info&& other) {
release();
version = other.version;
line_context = exchange(other.line_context, nullptr);
line_entries = std::move(other.line_entries);
return *this;
}
};
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,57 @@
#ifndef SYMBOL_RESOLVER_HPP
#define SYMBOL_RESOLVER_HPP
#include <cpptrace/basic.hpp>
#include "symbols/symbols.hpp"
#include "platform/platform.hpp"
#include "utils/string_view.hpp"
#include <memory>
#if false
#define CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING CPPTRACE_FORCE_NO_INLINE
#else
#define CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace libdwarf {
class symbol_resolver {
public:
virtual ~symbol_resolver() = default;
CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
virtual frame_with_inlines resolve_frame(const object_frame& frame_info) = 0;
};
class null_resolver : public symbol_resolver {
public:
explicit null_resolver() = default;
null_resolver(cstring_view) {}
CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
frame_with_inlines resolve_frame(const object_frame& frame_info) override {
return {
{
frame_info.raw_address,
frame_info.object_address,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
frame_info.object_path,
"",
false
},
{}
};
};
};
std::unique_ptr<symbol_resolver> make_dwarf_resolver(cstring_view object_path);
#if IS_APPLE
std::unique_ptr<symbol_resolver> make_debug_map_resolver(const std::string& object_path);
#endif
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,72 @@
#ifndef SYMBOLS_HPP
#define SYMBOLS_HPP
#include <cpptrace/basic.hpp>
#include <functional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
using collated_vec = std::vector<
std::pair<std::reference_wrapper<const object_frame>, std::reference_wrapper<stacktrace_frame>>
>;
struct frame_with_inlines {
stacktrace_frame frame;
std::vector<stacktrace_frame> inlines;
};
using collated_vec_with_inlines = std::vector<
std::pair<std::reference_wrapper<const object_frame>, std::reference_wrapper<frame_with_inlines>>
>;
// These two helpers create a map from a target object to a vector of frames to resolve
std::unordered_map<std::string, collated_vec> collate_frames(
const std::vector<object_frame>& frames,
std::vector<stacktrace_frame>& trace
);
std::unordered_map<std::string, collated_vec_with_inlines> collate_frames(
const std::vector<object_frame>& frames,
std::vector<frame_with_inlines>& trace
);
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE
namespace libbacktrace {
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames);
}
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF
namespace libdwarf {
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames);
}
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDL
namespace libdl {
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames);
}
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE
namespace addr2line {
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames);
}
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_DBGHELP
namespace dbghelp {
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames);
}
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_NOTHING
namespace nothing {
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames);
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames);
}
#endif
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames);
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,156 @@
#include <cpptrace/basic.hpp>
#include <cpptrace/utils.hpp>
#include "cpptrace/forward.hpp"
#include "symbols/symbols.hpp"
#include <vector>
#include <unordered_map>
#include "utils/error.hpp"
#include "binary/object.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
template<typename CollatedVec, typename Entry>
std::unordered_map<std::string, CollatedVec> collate_frames(
const std::vector<object_frame>& frames,
std::vector<Entry>& trace
) {
std::unordered_map<std::string, CollatedVec> entries;
for(std::size_t i = 0; i < frames.size(); i++) {
const auto& entry = frames[i];
// The path may be empty. This can happens if libdl fails to find the shared object for a frame, e.g. I've
// observed this on macos when looking up the shared object containing `start`.
// It can also happen for JIT frames. As such, we don't exclude them from the output.
entries[entry.object_path].emplace_back(
entry,
trace[i]
);
}
return entries;
}
std::unordered_map<std::string, collated_vec> collate_frames(
const std::vector<object_frame>& frames,
std::vector<stacktrace_frame>& trace
) {
return collate_frames<collated_vec>(frames, trace);
}
std::unordered_map<std::string, collated_vec_with_inlines> collate_frames(
const std::vector<object_frame>& frames,
std::vector<frame_with_inlines>& trace
) {
return collate_frames<collated_vec_with_inlines>(frames, trace);
}
/*
*
*
* All the code here is awful and I'm not proud of it.
*
*
*
*/
// Resolver must not support walking inlines
void fill_blanks(
std::vector<stacktrace_frame>& vec,
std::vector<stacktrace_frame> (*resolver)(const std::vector<frame_ptr>&)
) {
std::vector<frame_ptr> addresses;
for(const auto& frame : vec) {
if(frame.symbol.empty() || frame.filename.empty()) {
addresses.push_back(frame.raw_address);
}
}
std::vector<stacktrace_frame> new_frames = resolver(addresses);
std::size_t i = 0;
for(auto& frame : vec) {
if(frame.symbol.empty() || frame.filename.empty()) {
// three cases to handle, either partially overwrite or fully overwrite
if(frame.symbol.empty() && frame.filename.empty()) {
frame = new_frames[i];
} else if(frame.symbol.empty() && !frame.filename.empty()) {
frame.symbol = new_frames[i].symbol;
} else {
ASSERT(!frame.symbol.empty() && frame.filename.empty());
frame.filename = new_frames[i].filename;
frame.line = new_frames[i].line;
frame.column = new_frames[i].column;
}
i++;
}
}
}
// TODO: Symbol resolution code should probably handle when object addresses are 0
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames) {
#if defined(CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF) && defined(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP)
std::vector<stacktrace_frame> trace = libdwarf::resolve_frames(frames);
fill_blanks(trace, dbghelp::resolve_frames);
return trace;
#else
#if defined(CPPTRACE_GET_SYMBOLS_WITH_LIBDL) \
|| defined(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP) \
|| defined(CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE)
// actually need to go backwards to a void*
std::vector<frame_ptr> raw_frames(frames.size());
for(std::size_t i = 0; i < frames.size(); i++) {
raw_frames[i] = frames[i].raw_address;
}
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDL
return libdl::resolve_frames(raw_frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF
return libdwarf::resolve_frames(frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_DBGHELP
return dbghelp::resolve_frames(raw_frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE
return addr2line::resolve_frames(frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE
return libbacktrace::resolve_frames(raw_frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_NOTHING
return nothing::resolve_frames(frames);
#endif
#endif
}
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames) {
#if defined(CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF) \
|| defined(CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE)
auto dlframes = get_frames_object_info(frames);
#endif
#if defined(CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF) && defined(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP)
std::vector<stacktrace_frame> trace = libdwarf::resolve_frames(dlframes);
fill_blanks(trace, dbghelp::resolve_frames);
return trace;
#else
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDL
return libdl::resolve_frames(frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF
return libdwarf::resolve_frames(dlframes);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_DBGHELP
return dbghelp::resolve_frames(frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE
return addr2line::resolve_frames(dlframes);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE
return libbacktrace::resolve_frames(frames);
#endif
#ifdef CPPTRACE_GET_SYMBOLS_WITH_NOTHING
return nothing::resolve_frames(frames);
#endif
#endif
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,328 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE
#include <cpptrace/basic.hpp>
#include "symbols/symbols.hpp"
#include "utils/common.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
#include <cstdint>
#include <cstdio>
#include <functional>
#include <mutex>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#if IS_LINUX || IS_APPLE
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#endif
#include "binary/object.hpp"
#include "options.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace addr2line {
#if IS_LINUX || IS_APPLE
bool has_addr2line() {
static std::mutex mutex;
static bool has_addr2line = false;
static bool checked = false;
std::lock_guard<std::mutex> lock(mutex);
if(!checked) {
checked = true;
// Detects if addr2line exists by trying to invoke addr2line --help
constexpr int magic = 42;
const pid_t pid = fork();
if(pid == -1) { return false; }
if(pid == 0) { // child
close(STDOUT_FILENO);
close(STDERR_FILENO); // atos --help writes to stderr
#ifdef CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH
#if !IS_APPLE
execlp("addr2line", "addr2line", "--help", nullptr);
#else
execlp("atos", "atos", "--help", nullptr);
#endif
#else
#ifndef CPPTRACE_ADDR2LINE_PATH
#error "CPPTRACE_ADDR2LINE_PATH must be defined if CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH is not"
#endif
execl(CPPTRACE_ADDR2LINE_PATH, CPPTRACE_ADDR2LINE_PATH, "--help", nullptr);
#endif
_exit(magic);
}
int status;
waitpid(pid, &status, 0);
has_addr2line = WEXITSTATUS(status) == 0;
}
return has_addr2line;
}
struct pipe_ends {
int read;
int write;
};
struct pipe_t {
union {
pipe_ends end;
int data[2];
};
};
static_assert(sizeof(pipe_t) == 2 * sizeof(int), "Unexpected struct packing");
std::string resolve_addresses(const std::string& addresses, const std::string& executable) {
pipe_t output_pipe;
pipe_t input_pipe;
if(pipe(output_pipe.data) != 0 || pipe(input_pipe.data) != 0) {
throw internal_error("call to pipe failed: {}", errno);
}
const pid_t pid = fork();
if(pid == -1) { return ""; } // error? TODO: Diagnostic
if(pid == 0) { // child
dup2(output_pipe.end.write, STDOUT_FILENO);
dup2(input_pipe.end.read, STDIN_FILENO);
close(output_pipe.end.read);
close(output_pipe.end.write);
close(input_pipe.end.read);
close(input_pipe.end.write);
close(STDERR_FILENO); // TODO: Might be worth conditionally enabling or piping
#ifdef CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH
#if !IS_APPLE
execlp("addr2line", "addr2line", "-e", executable.c_str(), "-f", "-C", "-p", nullptr);
#else
execlp("atos", "atos", "-o", executable.c_str(), "-fullPath", nullptr);
#endif
#else
#ifndef CPPTRACE_ADDR2LINE_PATH
#error "CPPTRACE_ADDR2LINE_PATH must be defined if CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH is not"
#endif
#if !IS_APPLE
execl(
CPPTRACE_ADDR2LINE_PATH,
CPPTRACE_ADDR2LINE_PATH,
"-e",
executable.c_str(),
"-f",
"-C",
"-p",
nullptr
);
#else
execl(
CPPTRACE_ADDR2LINE_PATH,
CPPTRACE_ADDR2LINE_PATH,
"-o", executable.c_str(),
"-fullPath",
nullptr
);
#endif
#endif
_exit(1); // TODO: Diagnostic?
}
if(write(input_pipe.end.write, addresses.data(), addresses.size()) == -1) {
throw internal_error("call to write failed: {}", errno);
}
close(input_pipe.end.read);
close(input_pipe.end.write);
close(output_pipe.end.write);
std::string output;
constexpr int buffer_size = 4096;
char buffer[buffer_size];
std::size_t count = 0;
while((count = read(output_pipe.end.read, buffer, buffer_size)) > 0) {
output.insert(output.end(), buffer, buffer + count);
}
// TODO: check status from addr2line?
waitpid(pid, nullptr, 0);
return output;
}
#elif IS_WINDOWS
bool has_addr2line() {
static std::mutex mutex;
static bool has_addr2line = false;
static bool checked = false;
std::lock_guard<std::mutex> lock(mutex);
if(!checked) {
// TODO: Popen is a hack. Implement properly with CreateProcess and pipes later.
checked = true;
#ifdef CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH
std::FILE* p = popen("addr2line --version", "r");
#else
#ifndef CPPTRACE_ADDR2LINE_PATH
#error "CPPTRACE_ADDR2LINE_PATH must be defined if CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH is not"
#endif
std::FILE* p = popen(CPPTRACE_ADDR2LINE_PATH " --version", "r");
#endif
if(p) {
has_addr2line = pclose(p) == 0;
}
}
return has_addr2line;
}
std::string resolve_addresses(const std::string& addresses, const std::string& executable) {
// TODO: Popen is a hack. Implement properly with CreateProcess and pipes later.
///fprintf(stderr, ("addr2line -e " + executable + " -fCp " + addresses + "\n").c_str());
#ifdef CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH
std::FILE* p = popen(("addr2line -e \"" + executable + "\" -fCp " + addresses).c_str(), "r");
#else
#ifndef CPPTRACE_ADDR2LINE_PATH
#error "CPPTRACE_ADDR2LINE_PATH must be defined if CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH is not"
#endif
std::FILE* p = popen(
(CPPTRACE_ADDR2LINE_PATH " -e \"" + executable + "\" -fCp " + addresses).c_str(),
"r"
);
#endif
std::string output;
constexpr int buffer_size = 4096;
char buffer[buffer_size];
std::size_t count = 0;
while((count = std::fread(buffer, 1, buffer_size, p)) > 0) {
output.insert(output.end(), buffer, buffer + count);
}
pclose(p);
///fprintf(stderr, "%s\n", output.c_str());
return output;
}
#endif
void update_trace(const std::string& line, std::size_t entry_index, const collated_vec& entries_vec) {
#if !IS_APPLE
// Result will be of the form "<symbol> at path:line"
// The path may be ?? if addr2line cannot resolve, line may be ?
// Edge cases:
// ?? ??:0
// symbol :?
const std::size_t at_location = line.find(" at ");
std::size_t symbol_end;
std::size_t filename_start;
if(at_location != std::string::npos) {
symbol_end = at_location;
filename_start = at_location + 4;
} else {
VERIFY(line.find("?? ") == 0, "Unexpected edge case while processing addr2line output");
symbol_end = 2;
filename_start = 3;
}
auto symbol = line.substr(0, symbol_end);
auto colon = line.rfind(':');
VERIFY(colon != std::string::npos);
VERIFY(colon >= filename_start); // :? to deal with "symbol :?" edge case
auto filename = line.substr(filename_start, colon - filename_start);
auto line_number = line.substr(colon + 1);
if(line_number != "?") {
entries_vec[entry_index].second.get().line = std::stoi(line_number);
}
if(!filename.empty() && filename != "??") {
entries_vec[entry_index].second.get().filename = filename;
}
if(!symbol.empty()) {
entries_vec[entry_index].second.get().symbol = symbol;
}
#else
// Result will be of the form "<symbol> (in <object name>) (file:line)"
// The symbol may just be the given address if atos can't resolve it
// Examples:
// trace() (in demo) (demo.cpp:8)
// 0x100003b70 (in demo)
// 0xffffffffffffffff
// foo (in bar) + 14
// I'm making some assumptions here. Support may need to be improved later. This is tricky output to
// parse.
const std::size_t in_location = line.find(" (in ");
if(in_location == std::string::npos) {
// presumably the 0xffffffffffffffff case
return;
}
const std::size_t symbol_end = in_location;
entries_vec[entry_index].second.get().symbol = line.substr(0, symbol_end);
const std::size_t object_end = line.find(")", in_location);
VERIFY(
object_end != std::string::npos,
"Unexpected edge case while processing addr2line/atos output"
);
const std::size_t filename_start = line.find(") (", object_end);
if(filename_start == std::string::npos) {
// presumably something like 0x100003b70 (in demo) or foo (in bar) + 14
return;
}
const std::size_t filename_end = line.find(":", filename_start);
VERIFY(
filename_end != std::string::npos,
"Unexpected edge case while processing addr2line/atos output"
);
entries_vec[entry_index].second.get().filename = line.substr(
filename_start + 3,
filename_end - filename_start - 3
);
const std::size_t line_start = filename_end + 1;
const std::size_t line_end = line.find(")", filename_end);
VERIFY(
line_end == line.size() - 1,
"Unexpected edge case while processing addr2line/atos output"
);
entries_vec[entry_index].second.get().line = std::stoi(line.substr(line_start, line_end - line_start));
#endif
}
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames) {
// TODO: Refactor better
std::vector<stacktrace_frame> trace(frames.size(), null_frame());
for(std::size_t i = 0; i < frames.size(); i++) {
trace[i].raw_address = frames[i].raw_address;
trace[i].object_address = frames[i].object_address;
// Set what is known for now, and resolutions from addr2line should overwrite
trace[i].filename = frames[i].object_path;
}
if(has_addr2line()) {
const auto entries = collate_frames(frames, trace);
for(const auto& entry : entries) {
try {
const auto& object_name = entry.first;
if(object_name.empty()) {
continue;
}
const auto& entries_vec = entry.second;
// You may ask why it'd ever happen that there could be an empty entries_vec array, if there're
// no addresses why would get_addr2line_targets do anything? The reason is because if things in
// get_addr2line_targets fail it will silently skip. This is partly an optimization but also an
// assertion below will fail if addr2line is given an empty input.
if(entries_vec.empty()) {
continue;
}
std::string address_input;
for(const auto& pair : entries_vec) {
address_input += microfmt::format(
"{:h}{}",
pair.first.get().object_address,
#if !IS_WINDOWS
'\n'
#else
' '
#endif
);
}
auto output = split(trim(resolve_addresses(address_input, object_name)), "\n");
VERIFY(output.size() == entries_vec.size());
for(std::size_t i = 0; i < output.size(); i++) {
update_trace(output[i], i, entries_vec);
}
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
}
}
}
return trace;
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,512 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_DBGHELP
#include <cpptrace/basic.hpp>
#include <cpptrace/utils.hpp>
#include "symbols/symbols.hpp"
#include "platform/dbghelp_utils.hpp"
#include "binary/object.hpp"
#include "utils/common.hpp"
#include "utils/error.hpp"
#include "utils/utils.hpp"
#include "options.hpp"
#include "logging.hpp"
#include <regex>
#include <system_error>
#include <vector>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <dbghelp.h>
#include <psapi.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace dbghelp {
// SymFromAddr only returns the function's name. In order to get information about parameters,
// important for C++ stack traces where functions may be overloaded, we have to manually use
// Windows DIA to walk debug info structures. Resources:
// https://web.archive.org/web/20201027025750/http://www.debuginfo.com/articles/dbghelptypeinfo.html
// https://web.archive.org/web/20201203160805/http://www.debuginfo.com/articles/dbghelptypeinfofigures.html
// https://github.com/DynamoRIO/dynamorio/blob/master/ext/drsyms/drsyms_windows.c#L1370-L1439
// TODO: Currently unable to detect rvalue references
// TODO: Currently unable to detect const
enum class SymTagEnum {
SymTagNull, SymTagExe, SymTagCompiland, SymTagCompilandDetails, SymTagCompilandEnv,
SymTagFunction, SymTagBlock, SymTagData, SymTagAnnotation, SymTagLabel, SymTagPublicSymbol,
SymTagUDT, SymTagEnum, SymTagFunctionType, SymTagPointerType, SymTagArrayType,
SymTagBaseType, SymTagTypedef, SymTagBaseClass, SymTagFriend, SymTagFunctionArgType,
SymTagFuncDebugStart, SymTagFuncDebugEnd, SymTagUsingNamespace, SymTagVTableShape,
SymTagVTable, SymTagCustom, SymTagThunk, SymTagCustomType, SymTagManagedType,
SymTagDimension, SymTagCallSite, SymTagInlineSite, SymTagBaseInterface, SymTagVectorType,
SymTagMatrixType, SymTagHLSLType, SymTagCaller, SymTagCallee, SymTagExport,
SymTagHeapAllocationSite, SymTagCoffGroup, SymTagMax
};
enum class IMAGEHLP_SYMBOL_TYPE_INFO {
TI_GET_SYMTAG, TI_GET_SYMNAME, TI_GET_LENGTH, TI_GET_TYPE, TI_GET_TYPEID, TI_GET_BASETYPE,
TI_GET_ARRAYINDEXTYPEID, TI_FINDCHILDREN, TI_GET_DATAKIND, TI_GET_ADDRESSOFFSET,
TI_GET_OFFSET, TI_GET_VALUE, TI_GET_COUNT, TI_GET_CHILDRENCOUNT, TI_GET_BITPOSITION,
TI_GET_VIRTUALBASECLASS, TI_GET_VIRTUALTABLESHAPEID, TI_GET_VIRTUALBASEPOINTEROFFSET,
TI_GET_CLASSPARENTID, TI_GET_NESTED, TI_GET_SYMINDEX, TI_GET_LEXICALPARENT, TI_GET_ADDRESS,
TI_GET_THISADJUST, TI_GET_UDTKIND, TI_IS_EQUIV_TO, TI_GET_CALLING_CONVENTION,
TI_IS_CLOSE_EQUIV_TO, TI_GTIEX_REQS_VALID, TI_GET_VIRTUALBASEOFFSET,
TI_GET_VIRTUALBASEDISPINDEX, TI_GET_IS_REFERENCE, TI_GET_INDIRECTVIRTUALBASECLASS,
TI_GET_VIRTUALBASETABLETYPE, TI_GET_OBJECTPOINTERTYPE, IMAGEHLP_SYMBOL_TYPE_INFO_MAX
};
enum class BasicType {
btNoType = 0, btVoid = 1, btChar = 2, btWChar = 3, btInt = 6, btUInt = 7, btFloat = 8,
btBCD = 9, btBool = 10, btLong = 13, btULong = 14, btCurrency = 25, btDate = 26,
btVariant = 27, btComplex = 28, btBit = 29, btBSTR = 30, btHresult = 31
};
// SymGetTypeInfo utility
template<typename T, IMAGEHLP_SYMBOL_TYPE_INFO SymType, bool FAILABLE = false>
T get_info(ULONG type_index, HANDLE proc, ULONG64 modbase) {
T info;
if(
!SymGetTypeInfo(
proc,
modbase,
type_index,
static_cast<::IMAGEHLP_SYMBOL_TYPE_INFO>(SymType),
&info
)
) {
if(FAILABLE) {
return (T)-1;
} else {
throw internal_error(
"SymGetTypeInfo failed: {}", std::system_error(GetLastError(), std::system_category()).what()
);
}
}
return info;
}
template<IMAGEHLP_SYMBOL_TYPE_INFO SymType, bool FAILABLE = false>
std::string get_info_wchar(ULONG type_index, HANDLE proc, ULONG64 modbase) {
WCHAR* info;
if(
!SymGetTypeInfo(proc, modbase, type_index, static_cast<::IMAGEHLP_SYMBOL_TYPE_INFO>(SymType), &info)
) {
throw internal_error(
"SymGetTypeInfo failed: {}", std::system_error(GetLastError(), std::system_category()).what()
);
}
// special case to properly free a buffer and convert string to narrow chars, only used for
// TI_GET_SYMNAME
static_assert(
SymType == IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_SYMNAME,
"get_info_wchar called with unexpected IMAGEHLP_SYMBOL_TYPE_INFO"
);
std::wstring wstr(info);
std::string str;
str.reserve(wstr.size());
for(const auto c : wstr) {
str.push_back(static_cast<char>(c));
}
LocalFree(info);
return str;
}
// Translate basic types to string
static std::string get_basic_type(ULONG type_index, HANDLE proc, ULONG64 modbase) {
auto basic_type = get_info<BasicType, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_BASETYPE>(
type_index,
proc,
modbase
);
//auto length = get_info<ULONG64, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_LENGTH>(type_index, proc, modbase);
switch(basic_type) {
case BasicType::btNoType:
return "<no basic type>";
case BasicType::btVoid:
return "void";
case BasicType::btChar:
return "char";
case BasicType::btWChar:
return "wchar_t";
case BasicType::btInt:
return "int";
case BasicType::btUInt:
return "unsigned int";
case BasicType::btFloat:
return "float";
case BasicType::btBool:
return "bool";
case BasicType::btLong:
return "long";
case BasicType::btULong:
return "unsigned long";
default:
return "<unknown basic type>";
}
}
static std::string resolve_type(ULONG type_index, HANDLE proc, ULONG64 modbase);
struct class_name_result {
bool has_class_name;
std::string name;
};
// Helper for member pointers
static class_name_result lookup_class_name(ULONG type_index, HANDLE proc, ULONG64 modbase) {
DWORD class_parent_id = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_CLASSPARENTID, true>(
type_index,
proc,
modbase
);
if(class_parent_id == (DWORD)-1) {
return {false, ""};
} else {
return {true, resolve_type(class_parent_id, proc, modbase)};
}
}
struct type_result {
std::string base;
std::string extent;
};
// Resolve more complex types
// returns [base, extent]
static type_result lookup_type(ULONG type_index, HANDLE proc, ULONG64 modbase) {
auto tag = get_info<SymTagEnum, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_SYMTAG>(type_index, proc, modbase);
switch(tag) {
case SymTagEnum::SymTagBaseType:
return {get_basic_type(type_index, proc, modbase), ""};
case SymTagEnum::SymTagPointerType: {
DWORD underlying_type_id = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_TYPEID>(
type_index,
proc,
modbase
);
bool is_ref = get_info<BOOL, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_IS_REFERENCE>(
type_index,
proc,
modbase
);
std::string pp = is_ref ? "&" : "*"; // pointer punctuator
auto class_name_res = lookup_class_name(type_index, proc, modbase);
if(class_name_res.has_class_name) {
pp = class_name_res.name + "::" + pp;
}
const auto type = lookup_type(underlying_type_id, proc, modbase);
if(type.extent.empty()) {
return {type.base + (pp.size() > 1 ? " " : "") + pp, ""};
} else {
return {type.base + "(" + pp, ")" + type.extent};
}
}
case SymTagEnum::SymTagArrayType: {
DWORD underlying_type_id = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_TYPEID>(
type_index,
proc,
modbase
);
DWORD length = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_COUNT>(
type_index,
proc,
modbase
);
const auto type = lookup_type(underlying_type_id, proc, modbase);
return {type.base, "[" + std::to_string(length) + "]" + type.extent};
}
case SymTagEnum::SymTagFunctionType: {
DWORD return_type_id = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_TYPEID>(
type_index,
proc,
modbase
);
DWORD n_children = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_COUNT, true>(
type_index,
proc,
modbase
);
DWORD class_parent_id = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_CLASSPARENTID, true>(
type_index,
proc,
modbase
);
int n_ignore = class_parent_id != (DWORD)-1; // ignore this param
// this must be ignored before TI_FINDCHILDREN_PARAMS::Count is set, else error
n_children -= n_ignore;
// return type
const auto return_type = lookup_type(return_type_id, proc, modbase);
if(n_children == 0) {
return {return_type.base, "()" + return_type.extent};
} else {
// alignment should be fine
std::size_t sz = sizeof(TI_FINDCHILDREN_PARAMS) +
(n_children) * sizeof(TI_FINDCHILDREN_PARAMS::ChildId[0]);
TI_FINDCHILDREN_PARAMS* children = (TI_FINDCHILDREN_PARAMS*) new char[sz];
auto guard = scope_exit([&] {
delete[] (char*) children;
});
children->Start = 0;
children->Count = n_children;
if(
!SymGetTypeInfo(
proc, modbase, type_index,
static_cast<::IMAGEHLP_SYMBOL_TYPE_INFO>(
IMAGEHLP_SYMBOL_TYPE_INFO::TI_FINDCHILDREN
),
children
)
) {
throw internal_error(
"SymGetTypeInfo failed: {}",
std::system_error(GetLastError(), std::system_category()).what()
);
}
// get children type
std::string extent = "(";
if(children->Start != 0) {
throw internal_error("Error: children->Start == 0");
}
for(std::size_t i = 0; i < n_children; i++) {
extent += (i == 0 ? "" : ", ") + resolve_type(children->ChildId[i], proc, modbase);
}
extent += ")";
return {return_type.base, extent + return_type.extent};
}
}
case SymTagEnum::SymTagFunctionArgType: {
DWORD underlying_type_id =
get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_TYPEID>(type_index, proc, modbase);
return {resolve_type(underlying_type_id, proc, modbase), ""};
}
case SymTagEnum::SymTagTypedef:
case SymTagEnum::SymTagEnum:
case SymTagEnum::SymTagUDT:
case SymTagEnum::SymTagBaseClass:
return {
get_info_wchar<IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_SYMNAME>(type_index, proc, modbase), ""
};
default:
return {
"<unknown type " +
std::to_string(static_cast<std::underlying_type<SymTagEnum>::type>(tag)) +
">",
""
};
};
}
static std::string resolve_type(ULONG type_index, HANDLE proc, ULONG64 modbase) {
const auto type = lookup_type(type_index, proc, modbase);
return type.base + type.extent;
}
struct function_info {
HANDLE proc;
ULONG64 modbase;
int counter;
int n_children;
int n_ignore;
std::string str;
};
// Enumerates function parameters
static BOOL __stdcall enumerator_callback(
PSYMBOL_INFO symbol_info,
ULONG,
PVOID data
) {
function_info* ctx = (function_info*)data;
if(ctx->counter++ >= ctx->n_children) {
return false;
}
if(ctx->n_ignore-- > 0) {
return true; // just skip
}
ctx->str += resolve_type(symbol_info->TypeIndex, ctx->proc, ctx->modbase);
if(ctx->counter < ctx->n_children) {
ctx->str += ", ";
}
return true;
}
// TODO: Handle backtrace_pcinfo calling the callback multiple times on inlined functions
stacktrace_frame resolve_frame(HANDLE proc, frame_ptr addr) {
// The get_frame_object_info() ends up being inexpensive, at on my machine
// debug release
// uncached trace resolution (29 frames) 1.9-2.1 ms 1.4-1.8 ms
// cached trace resolution (29 frames) 1.1-1.2 ms 0.2-0.4 ms
// get_frame_object_info() 0.001-0.002 ms 0.0003-0.0006 ms
// At some point it might make sense to make an option to control this.
auto object_frame = get_frame_object_info(addr);
// Dbghelp is is single-threaded, so acquire a lock.
auto lock = get_dbghelp_lock();
alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
SYMBOL_INFO* symbol = (SYMBOL_INFO*)buffer;
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
symbol->MaxNameLen = MAX_SYM_NAME;
union { DWORD64 a; DWORD b; } displacement;
IMAGEHLP_LINE line;
bool got_line = SymGetLineFromAddr(proc, addr, &displacement.b, &line);
if(SymFromAddr(proc, addr, &displacement.a, symbol)) {
if(got_line) {
IMAGEHLP_STACK_FRAME frame;
frame.InstructionOffset = symbol->Address;
// https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetcontext
// "If you call SymSetContext to set the context to its current value, the
// function fails but GetLastError returns ERROR_SUCCESS."
// This is the stupidest fucking api I've ever worked with.
if(SymSetContext(proc, &frame, nullptr) == FALSE && GetLastError() != ERROR_SUCCESS) {
log::error("Stack trace: Internal error while calling SymSetContext");
return {
addr,
object_frame.object_address,
{ static_cast<std::uint32_t>(line.LineNumber) },
nullable<std::uint32_t>::null(),
line.FileName,
symbol->Name,
false
};
}
DWORD n_children = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_COUNT, true>(
symbol->TypeIndex,
proc,
symbol->ModBase
);
DWORD class_parent_id = get_info<DWORD, IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_CLASSPARENTID, true>(
symbol->TypeIndex,
proc,
symbol->ModBase
);
function_info fi {
proc,
symbol->ModBase,
0,
int(n_children),
class_parent_id != (DWORD)-1,
""
};
SymEnumSymbols(proc, 0, nullptr, enumerator_callback, &fi);
std::string signature = symbol->Name + std::string("(") + fi.str + ")";
// There's a phenomina with DIA not inserting commas after template parameters. Fix them here.
static std::regex comma_re(R"(,(?=\S))");
signature = std::regex_replace(signature, comma_re, ", ");
return {
addr,
object_frame.object_address,
{ static_cast<std::uint32_t>(line.LineNumber) },
nullable<std::uint32_t>::null(),
line.FileName,
signature,
false,
};
} else {
return {
addr,
object_frame.object_address,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
"",
symbol->Name,
false
};
}
} else {
return {
addr,
object_frame.object_address,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
"",
"",
false
};
}
}
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames) {
// Dbghelp is is single-threaded, so acquire a lock.
auto lock = get_dbghelp_lock();
std::vector<stacktrace_frame> trace;
trace.reserve(frames.size());
// TODO: When does this need to be called? Can it be moved to the symbolizer?
SymSetOptions(SYMOPT_ALLOW_ABSOLUTE_SYMBOLS);
auto syminit_info = ensure_syminit();
for(const auto frame : frames) {
try {
trace.push_back(resolve_frame(syminit_info.get_process_handle() , frame));
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
auto entry = null_frame();
entry.raw_address = frame;
trace.push_back(entry);
}
}
return trace;
}
}
}
CPPTRACE_END_NAMESPACE
CPPTRACE_BEGIN_NAMESPACE
/*
When a module was loaded at runtime with LoadLibrary after SymInitialize was already called,
it is necessary to manually load the symbols from that module with SymLoadModuleEx.
See "Symbol Handler Initialization" in Microsoft documentation at
https://learn.microsoft.com/en-us/windows/win32/debug/symbol-handler-initialization
*/
void load_symbols_for_file(const std::string& filename) {
HMODULE hModule = GetModuleHandleA(filename.c_str());
if (hModule == NULL) {
throw detail::internal_error(
"Unable to get module handle for file '{}' : {}",
filename,
std::system_error(GetLastError(), std::system_category()).what()
);
}
// SymLoadModuleEx needs the module's base address and size, so get these with GetModuleInformation.
MODULEINFO module_info;
if (
!GetModuleInformation(
GetCurrentProcess(),
hModule,
&module_info,
sizeof(module_info)
)
) {
throw detail::internal_error(
"Unable to get module information for file '{}' : {}",
filename,
std::system_error(GetLastError(), std::system_category()).what()
);
}
auto lock = detail::get_dbghelp_lock();
HANDLE syminit_handle = detail::ensure_syminit().get_process_handle();
if (
!SymLoadModuleEx(
syminit_handle,
NULL,
filename.c_str(),
NULL,
(DWORD64)module_info.lpBaseOfDll,
// The documentation says this is optional, but if omitted (0), symbol loading fails
module_info.SizeOfImage,
NULL,
0
)
) {
throw detail::internal_error(
"Unable to load symbols for file '{}' : {}",
filename,
std::system_error(GetLastError(), std::system_category()).what()
);
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,56 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDL
#include <cpptrace/basic.hpp>
#include "symbols/symbols.hpp"
#include "binary/module_base.hpp"
#include <cstdint>
#include <memory>
#include <vector>
#include <dlfcn.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace libdl {
stacktrace_frame resolve_frame(const frame_ptr addr) {
Dl_info info;
if(dladdr(reinterpret_cast<void*>(addr), &info)) { // thread-safe
auto base = get_module_image_base(info.dli_fname);
return {
addr,
base.has_value()
? addr - reinterpret_cast<std::uintptr_t>(info.dli_fbase) + base.unwrap_value()
: 0,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
info.dli_fname ? info.dli_fname : "",
info.dli_sname ? info.dli_sname : "",
false
};
} else {
return {
addr,
0,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
"",
"",
false
};
}
}
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames) {
std::vector<stacktrace_frame> trace;
trace.reserve(frames.size());
for(const auto frame : frames) {
trace.push_back(resolve_frame(frame));
}
return trace;
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,107 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE
#include <cpptrace/basic.hpp>
#include "symbols/symbols.hpp"
#include "platform/program_name.hpp"
#include "utils/error.hpp"
#include "utils/common.hpp"
#include "options.hpp"
#include <cstdint>
#include <cstdio>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <vector>
#ifdef CPPTRACE_BACKTRACE_PATH
#include CPPTRACE_BACKTRACE_PATH
#else
#include <backtrace.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace libbacktrace {
int full_callback(void* data, std::uintptr_t address, const char* file, int line, const char* symbol) {
stacktrace_frame& frame = *static_cast<stacktrace_frame*>(data);
frame.raw_address = address;
frame.line = line;
frame.filename = file ? file : "";
frame.symbol = symbol ? symbol : "";
return 0;
}
void syminfo_callback(void* data, std::uintptr_t address, const char* symbol, std::uintptr_t, std::uintptr_t) {
stacktrace_frame& frame = *static_cast<stacktrace_frame*>(data);
frame.raw_address = address;
frame.line = 0;
frame.filename = "";
frame.symbol = symbol ? symbol : "";
}
void error_callback(void*, const char* msg, int errnum) {
if(msg == std::string("no debug info in ELF executable")) {
// https://github.com/jeremy-rifkin/cpptrace/issues/114
// https://github.com/ianlancetaylor/libbacktrace/blob/ae1e707dbacd4a5cc82fcf2d3816f410e9c5fec4/elf.c#L592
// not a critical error, just return
return;
}
throw internal_error("Libbacktrace error: {}, code {}", msg, errnum);
}
backtrace_state* get_backtrace_state() {
static std::mutex mutex;
const std::lock_guard<std::mutex> lock(mutex);
// backtrace_create_state must be called only one time per program
static backtrace_state* state = nullptr;
static bool called = false;
if(!called) {
state = backtrace_create_state(program_name(), true, error_callback, nullptr);
called = true;
}
return state;
}
// TODO: Handle backtrace_pcinfo calling the callback multiple times on inlined functions
stacktrace_frame resolve_frame(const frame_ptr addr) {
try {
stacktrace_frame frame = null_frame();
frame.raw_address = addr;
backtrace_pcinfo(
get_backtrace_state(),
addr,
full_callback,
error_callback,
&frame
);
if(frame.symbol.empty()) {
// fallback, try to at least recover the symbol name with backtrace_syminfo
backtrace_syminfo(
get_backtrace_state(),
addr,
syminfo_callback,
error_callback,
&frame
);
}
return frame;
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
return null_frame();
}
}
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames) {
std::vector<stacktrace_frame> trace;
trace.reserve(frames.size());
for(const auto frame : frames) {
trace.push_back(resolve_frame(frame));
}
return trace;
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,184 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF
#include "symbols/symbols.hpp"
#include <cpptrace/basic.hpp>
#include "dwarf/resolver.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include "binary/elf.hpp"
#include "binary/mach-o.hpp"
#include "jit/jit_objects.hpp"
#include <cstdint>
#include <cstdio>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <vector>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace libdwarf {
std::unique_ptr<symbol_resolver> get_resolver_for_object(const std::string& object_path) {
#if IS_APPLE
// Check if dSYM exist, if not fallback to debug map
if(!directory_exists(object_path + ".dSYM")) {
return make_debug_map_resolver(object_path);
}
#endif
return make_dwarf_resolver(object_path);
}
// not thread-safe, replies on caller to lock
maybe_owned<symbol_resolver> get_resolver(const std::string& object_name) {
// cache resolvers since objects are likely to be traced more than once
static std::unordered_map<std::string, std::unique_ptr<symbol_resolver>> resolver_map;
auto it = resolver_map.find(object_name);
if(it != resolver_map.end()) {
return it->second.get();
} else {
std::unique_ptr<symbol_resolver> resolver_object = get_resolver_for_object(object_name);
if(get_cache_mode() == cache_mode::prioritize_speed) {
// .emplace needed, for some reason .insert tries to copy <= gcc 7.2
return resolver_map.emplace(object_name, std::move(resolver_object)).first->second.get();
} else {
// gcc 4 has trouble with automatic moves of locals here https://godbolt.org/z/9oWdWjbf8
return maybe_owned<symbol_resolver>{std::move(resolver_object)};
}
}
}
// flatten trace with inlines
std::vector<stacktrace_frame> flatten_inlines(std::vector<frame_with_inlines>& trace) {
std::vector<stacktrace_frame> final_trace;
for(auto& entry : trace) {
// most recent call first
if(!entry.inlines.empty()) {
// insert in reverse order
final_trace.insert(
final_trace.end(),
std::make_move_iterator(entry.inlines.rbegin()),
std::make_move_iterator(entry.inlines.rend())
);
}
final_trace.push_back(std::move(entry.frame));
if(!entry.inlines.empty()) {
// rotate line info due to quirk of how dwarf stores this stuff
// inclusive range
auto begin = final_trace.end() - (1 + entry.inlines.size());
auto end = final_trace.end() - 1;
auto carry_line = end->line;
auto carry_column = end->column;
std::string carry_filename = std::move(end->filename);
for(auto it = end; it != begin; it--) {
it->line = (it - 1)->line;
it->column = (it - 1)->column;
it->filename = std::move((it - 1)->filename);
}
begin->line = carry_line;
begin->column = carry_column;
begin->filename = std::move(carry_filename);
}
}
return final_trace;
}
#if IS_LINUX || IS_APPLE
CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
void try_resolve_jit_frame(const cpptrace::object_frame& dlframe, frame_with_inlines& frame) {
auto object_res = lookup_jit_object(dlframe.raw_address);
// TODO: At some point, dwarf resolution
if(object_res) {
frame.frame.symbol = object_res.unwrap().object
.lookup_symbol(dlframe.raw_address - object_res.unwrap().base).value_or("");
}
}
#endif
CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
void try_resolve_frame(
symbol_resolver* resolver,
const cpptrace::object_frame& dlframe,
frame_with_inlines& frame
) {
try {
frame = resolver->resolve_frame(dlframe);
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
frame.frame.raw_address = dlframe.raw_address;
frame.frame.object_address = dlframe.object_address;
frame.frame.filename = dlframe.object_path;
}
}
CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames) {
std::vector<frame_with_inlines> trace(frames.size(), {null_frame(), {}});
// Locking around all libdwarf interaction per https://github.com/davea42/libdwarf-code/discussions/184
// And also locking for interactions with get_resolver
static std::mutex mutex;
const std::lock_guard<std::mutex> lock(mutex);
for(const auto& group : collate_frames(frames, trace)) {
try {
const auto& object_name = group.first;
if(object_name.empty()) {
#if IS_LINUX || IS_APPLE
for(const auto& entry : group.second) {
try_resolve_jit_frame(entry.first.get(), entry.second.get());
}
#endif
continue;
}
// TODO PERF: Potentially a duplicate open and parse with module base stuff (and debug map resolver)
#if IS_LINUX
auto object = open_elf_cached(object_name);
#elif IS_APPLE
auto object = open_mach_o_cached(object_name);
#endif
auto resolver = get_resolver(object_name);
for(const auto& entry : group.second) {
const auto& dlframe = entry.first.get();
auto& frame = entry.second.get();
try_resolve_frame(resolver.get(), dlframe, frame);
#if IS_LINUX || IS_APPLE
// fallback to symbol tables
if(frame.frame.symbol.empty() && object.has_value()) {
frame.frame.symbol = object
.unwrap_value()
->lookup_symbol(dlframe.object_address).value_or("");
}
#endif
}
} catch(...) { // NOSONAR
detail::log_and_maybe_propagate_exception(std::current_exception());
}
}
// fill in basic info for any frames where there were resolution issues
for(std::size_t i = 0; i < frames.size(); i++) {
const auto& dlframe = frames[i];
auto& frame = trace[i];
if(frame.frame == null_frame()) {
frame = {
{
dlframe.raw_address,
dlframe.object_address,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
dlframe.object_path,
"",
false
},
{}
};
}
}
// flatten and finish
return flatten_inlines(trace);
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,23 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_NOTHING
#include <cpptrace/basic.hpp>
#include "symbols/symbols.hpp"
#include "utils/common.hpp"
#include <vector>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
namespace nothing {
std::vector<stacktrace_frame> resolve_frames(const std::vector<frame_ptr>& frames) {
return std::vector<stacktrace_frame>(frames.size(), null_frame());
}
std::vector<stacktrace_frame> resolve_frames(const std::vector<object_frame>& frames) {
return std::vector<stacktrace_frame>(frames.size(), null_frame());
}
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,42 @@
#ifndef UNWIND_HPP
#define UNWIND_HPP
#include <cpptrace/basic.hpp>
#include <cstddef>
#include <vector>
#ifdef CPPTRACE_UNWIND_WITH_DBGHELP
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
#ifdef CPPTRACE_HARD_MAX_FRAMES
constexpr std::size_t hard_max_frames = CPPTRACE_HARD_MAX_FRAMES;
#else
constexpr std::size_t hard_max_frames = 400;
#endif
#ifdef CPPTRACE_UNWIND_WITH_DBGHELP
CPPTRACE_FORCE_NO_INLINE
std::vector<frame_ptr> capture_frames(
std::size_t skip,
std::size_t max_depth,
EXCEPTION_POINTERS* exception_pointers = nullptr
);
#else
CPPTRACE_FORCE_NO_INLINE
std::vector<frame_ptr> capture_frames(std::size_t skip, std::size_t max_depth);
#endif
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_capture_frames(frame_ptr* buffer, std::size_t size, std::size_t skip, std::size_t max_depth);
bool has_safe_unwind();
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,180 @@
#ifdef CPPTRACE_UNWIND_WITH_DBGHELP
#include <cpptrace/basic.hpp>
#include "unwind/unwind.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include "platform/dbghelp_utils.hpp"
#include <vector>
#include <cstddef>
#include <windows.h>
#include <dbghelp.h>
// Fucking windows headers
#ifdef min
#undef min
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
#if IS_MSVC
#pragma warning(push)
#pragma warning(disable: 4740) // warning C4740: flow in or out of inline asm code suppresses global optimization
#endif
CPPTRACE_FORCE_NO_INLINE
std::vector<frame_ptr> capture_frames(
std::size_t skip,
std::size_t max_depth,
EXCEPTION_POINTERS* exception_pointers
) {
// https://jpassing.com/2008/03/12/walking-the-stack-of-the-current-thread/
// Get current thread context
// GetThreadContext cannot be used on the current thread.
// RtlCaptureContext doesn't work on i386
CONTEXT context;
ZeroMemory(&context, sizeof(CONTEXT));
if(exception_pointers) {
context = *exception_pointers->ContextRecord;
} else {
skip++; // we're unwinding from the capture_frames frame, skip it
#if defined(_M_IX86) || defined(__i386__)
context.ContextFlags = CONTEXT_CONTROL;
#if IS_MSVC
__asm {
label:
mov [context.Ebp], ebp;
mov [context.Esp], esp;
mov eax, [label];
mov [context.Eip], eax;
}
#else
asm(
"label:\n\t"
"mov{l %%ebp, %[cEbp] | %[cEbp], ebp};\n\t"
"mov{l %%esp, %[cEsp] | %[cEsp], esp};\n\t"
"mov{l $label, %%eax | eax, OFFSET label};\n\t"
"mov{l %%eax, %[cEip] | %[cEip], eax};\n\t"
: [cEbp] "=r" (context.Ebp),
[cEsp] "=r" (context.Esp),
[cEip] "=r" (context.Eip)
);
#endif
#else
RtlCaptureContext(&context);
#endif
}
// Setup current frame
STACKFRAME64 frame;
ZeroMemory(&frame, sizeof(STACKFRAME64));
DWORD machine_type;
#if defined(_M_IX86) || defined(__i386__)
machine_type = IMAGE_FILE_MACHINE_I386;
frame.AddrPC.Offset = context.Eip;
frame.AddrPC.Mode = AddrModeFlat;
frame.AddrFrame.Offset = context.Ebp;
frame.AddrFrame.Mode = AddrModeFlat;
frame.AddrStack.Offset = context.Esp;
frame.AddrStack.Mode = AddrModeFlat;
#elif defined(_M_X64) || defined(__x86_64__)
machine_type = IMAGE_FILE_MACHINE_AMD64;
frame.AddrPC.Offset = context.Rip;
frame.AddrPC.Mode = AddrModeFlat;
frame.AddrFrame.Offset = context.Rsp;
frame.AddrFrame.Mode = AddrModeFlat;
frame.AddrStack.Offset = context.Rsp;
frame.AddrStack.Mode = AddrModeFlat;
#elif defined(_M_IA64)
machine_type = IMAGE_FILE_MACHINE_IA64;
frame.AddrPC.Offset = context.StIIP;
frame.AddrPC.Mode = AddrModeFlat;
frame.AddrFrame.Offset = context.IntSp;
frame.AddrFrame.Mode = AddrModeFlat;
frame.AddrBStore.Offset= context.RsBSP;
frame.AddrBStore.Mode = AddrModeFlat;
frame.AddrStack.Offset = context.IntSp;
frame.AddrStack.Mode = AddrModeFlat;
#elif defined(_M_ARM) || defined(__arm__)
machine_type = IMAGE_FILE_MACHINE_ARM;
frame.AddrPC.Offset = context.Pc;
frame.AddrPC.Mode = AddrModeFlat;
frame.AddrFrame.Offset = context.R11;
frame.AddrFrame.Mode = AddrModeFlat;
frame.AddrStack.Offset = context.Sp;
frame.AddrStack.Mode = AddrModeFlat;
#elif defined(_M_ARM64) || defined(__aarch64__)
machine_type = IMAGE_FILE_MACHINE_ARM64;
frame.AddrPC.Offset = context.Pc;
frame.AddrPC.Mode = AddrModeFlat;
frame.AddrFrame.Offset = context.Fp;
frame.AddrFrame.Mode = AddrModeFlat;
frame.AddrStack.Offset = context.Sp;
frame.AddrStack.Mode = AddrModeFlat;
#else
#error "Cpptrace: StackWalk64 not supported for this platform yet"
#endif
std::vector<frame_ptr> trace;
// Dbghelp is is single-threaded, so acquire a lock.
auto lock = get_dbghelp_lock();
// For some reason SymInitialize must be called before StackWalk64
// Note that the code assumes that
// SymInitialize( GetCurrentProcess(), NULL, TRUE ) has
// already been called.
//
auto syminit_info = ensure_syminit();
HANDLE thread = GetCurrentThread();
while(trace.size() < max_depth) {
if(
!StackWalk64(
machine_type,
syminit_info.get_process_handle(),
thread,
&frame,
machine_type == IMAGE_FILE_MACHINE_I386 ? NULL : &context,
NULL,
SymFunctionTableAccess64,
SymGetModuleBase64,
NULL
)
) {
// Either failed or finished walking
break;
}
if(frame.AddrPC.Offset != 0) {
// Valid frame
if(skip) {
skip--;
} else {
// On x86/x64/arm, as far as I can tell, the frame return address is always one after the call
// So we just decrement to get the pc back inside the `call` / `bl`
// This is done with _Unwind too but conditionally based on info from _Unwind_GetIPInfo.
trace.push_back(to_frame_ptr(frame.AddrPC.Offset) - 1);
}
} else {
// base
break;
}
}
return trace;
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_capture_frames(frame_ptr*, std::size_t, std::size_t, std::size_t) {
// Can't safe trace with dbghelp
return 0;
}
#if IS_MSVC
#pragma warning(pop)
#endif
bool has_safe_unwind() {
return false;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,45 @@
#ifdef CPPTRACE_UNWIND_WITH_EXECINFO
#include "unwind/unwind.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include <algorithm>
#include <climits>
#include <cstddef>
#include <vector>
#include <execinfo.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
CPPTRACE_FORCE_NO_INLINE
std::vector<frame_ptr> capture_frames(std::size_t skip, std::size_t max_depth) {
skip++;
std::vector<void*> addrs(skip + std::min(hard_max_frames, max_depth), nullptr);
// thread safe
const int n_frames = backtrace(addrs.data(), static_cast<int>(addrs.size()));
// I hate the copy here but it's the only way that isn't UB
std::vector<frame_ptr> frames(n_frames - skip, 0);
for(int i = skip; i < n_frames; i++) {
// On x86/x64/arm, as far as I can tell, the frame return address is always one after the call
// So we just decrement to get the pc back inside the `call` / `bl`
// This is done with _Unwind too but conditionally based on info from _Unwind_GetIPInfo.
frames[i - skip] = reinterpret_cast<frame_ptr>(addrs[i]) - 1;
}
return frames;
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_capture_frames(frame_ptr*, std::size_t, std::size_t, std::size_t) {
// Can't safe trace with execinfo
return 0;
}
bool has_safe_unwind() {
return false;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,88 @@
#ifdef CPPTRACE_UNWIND_WITH_LIBUNWIND
#include "unwind/unwind.hpp"
#include "utils/common.hpp"
#include "utils/error.hpp"
#include "utils/utils.hpp"
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <vector>
#define UNW_LOCAL_ONLY
#include <libunwind.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
CPPTRACE_FORCE_NO_INLINE
std::vector<frame_ptr> capture_frames(std::size_t skip, std::size_t max_depth) {
skip++;
std::vector<frame_ptr> frames;
unw_context_t context;
unw_cursor_t cursor;
unw_getcontext(&context);
unw_init_local(&cursor, &context);
do {
unw_word_t pc;
unw_word_t sp;
unw_get_reg(&cursor, UNW_REG_IP, &pc);
unw_get_reg(&cursor, UNW_REG_SP, &sp);
if(skip) {
skip--;
} else {
// pc is the instruction after the `call`, adjust back to the previous instruction
frames.push_back(to_frame_ptr(pc) - 1);
}
} while(unw_step(&cursor) > 0 && frames.size() < max_depth);
return frames;
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_capture_frames(frame_ptr* buffer, std::size_t size, std::size_t skip, std::size_t max_depth) {
// some code duplication, but whatever
skip++;
unw_context_t context;
unw_cursor_t cursor;
// thread and signal-safe https://www.nongnu.org/libunwind/man/unw_getcontext(3).html
unw_getcontext(&context);
// thread and signal-safe https://www.nongnu.org/libunwind/man/unw_init_local(3).html
unw_init_local(&cursor, &context);
size_t i = 0;
while(i < size && i < max_depth) {
unw_word_t pc;
unw_word_t sp;
// thread and signal-safe https://www.nongnu.org/libunwind/man/unw_get_reg(3).html
unw_get_reg(&cursor, UNW_REG_IP, &pc);
unw_get_reg(&cursor, UNW_REG_SP, &sp);
if(skip) {
skip--;
} else {
// thread and signal-safe
if(unw_is_signal_frame(&cursor)) {
// pc is the instruction that caused the signal
// just a cast, thread and signal safe
buffer[i] = to_frame_ptr(pc);
} else {
// pc is the instruction after the `call`, adjust back to the previous instruction
// just a cast, thread and signal safe
buffer[i] = to_frame_ptr(pc) - 1;
}
i++;
}
// thread and signal-safe as long as the cursor is in the local address space, which it is
// https://www.nongnu.org/libunwind/man/unw_step(3).html
if(unw_step(&cursor) <= 0) {
break;
}
}
return i;
}
bool has_safe_unwind() {
return true;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,25 @@
#ifdef CPPTRACE_UNWIND_WITH_NOTHING
#include "unwind/unwind.hpp"
#include <cstddef>
#include <vector>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
std::vector<frame_ptr> capture_frames(std::size_t, std::size_t) {
return {};
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_capture_frames(frame_ptr*, std::size_t, std::size_t, std::size_t) {
return 0;
}
bool has_safe_unwind() {
return false;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,75 @@
#ifdef CPPTRACE_UNWIND_WITH_UNWIND
#include "unwind/unwind.hpp"
#include "utils/common.hpp"
#include "utils/error.hpp"
#include "utils/utils.hpp"
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <vector>
#include <unwind.h>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
struct unwind_state {
std::size_t skip;
std::size_t max_depth;
std::vector<frame_ptr>& vec;
};
_Unwind_Reason_Code unwind_callback(_Unwind_Context* context, void* arg) {
unwind_state& state = *static_cast<unwind_state*>(arg);
if(state.skip) {
state.skip--;
if(_Unwind_GetIP(context) == frame_ptr(0)) {
return _URC_END_OF_STACK;
} else {
return _URC_NO_REASON;
}
}
ASSERT(
state.vec.size() < state.max_depth,
"Somehow cpptrace::detail::unwind_callback is being called beyond the max_depth"
);
int is_before_instruction = 0;
frame_ptr ip = _Unwind_GetIPInfo(context, &is_before_instruction);
if(!is_before_instruction && ip != frame_ptr(0)) {
ip--;
}
if (ip == frame_ptr(0)) {
return _URC_END_OF_STACK;
} else {
state.vec.push_back(ip);
if(state.vec.size() >= state.max_depth) {
return _URC_END_OF_STACK;
} else {
return _URC_NO_REASON;
}
}
}
CPPTRACE_FORCE_NO_INLINE
std::vector<frame_ptr> capture_frames(std::size_t skip, std::size_t max_depth) {
std::vector<frame_ptr> frames;
unwind_state state{skip + 1, max_depth, frames};
_Unwind_Backtrace(unwind_callback, &state); // presumably thread-safe
return frames;
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_capture_frames(frame_ptr*, std::size_t, std::size_t, std::size_t) {
// Can't safe trace with _Unwind
return 0;
}
bool has_safe_unwind() {
return false;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,56 @@
#ifdef CPPTRACE_UNWIND_WITH_WINAPI
#include <cpptrace/basic.hpp>
#include "unwind/unwind.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include <algorithm>
#include <cstdint>
#include <vector>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
// Fucking windows headers
#ifdef min
#undef min
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
CPPTRACE_FORCE_NO_INLINE
std::vector<frame_ptr> capture_frames(std::size_t skip, std::size_t max_depth) {
std::vector<void*> addrs(skip + std::min(hard_max_frames, max_depth), nullptr);
std::size_t n_frames = CaptureStackBackTrace(
static_cast<ULONG>(skip + 1),
static_cast<ULONG>(addrs.size()),
addrs.data(),
NULL
);
// I hate the copy here but it's the only way that isn't UB
std::vector<frame_ptr> frames(n_frames, 0);
for(std::size_t i = 0; i < n_frames; i++) {
// On x86/x64/arm, as far as I can tell, the frame return address is always one after the call
// So we just decrement to get the pc back inside the `call` / `bl`
// This is done with _Unwind too but conditionally based on info from _Unwind_GetIPInfo.
frames[i] = reinterpret_cast<frame_ptr>(addrs[i]) - 1;
}
return frames;
}
CPPTRACE_FORCE_NO_INLINE
std::size_t safe_capture_frames(frame_ptr*, std::size_t, std::size_t, std::size_t) {
// Can't safe trace with winapi
return 0;
}
bool has_safe_unwind() {
return false;
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,108 @@
#include <cpptrace/utils.hpp>
#include <cpptrace/exceptions.hpp>
#include <cpptrace/formatting.hpp>
#include <iostream>
#include "demangle/demangle.hpp"
#include "snippets/snippet.hpp"
#include "utils/utils.hpp"
#include "platform/exception_type.hpp"
#include "options.hpp"
#if !IS_WINDOWS
#include <unistd.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
std::string demangle(const std::string& name) {
return detail::demangle(name, false);
}
std::string get_snippet(const std::string& path, std::size_t line, std::size_t context_size, bool color) {
return detail::get_snippet(path, line, nullable<std::uint32_t>::null(), context_size, color);
}
std::string get_snippet(
const std::string& path,
std::size_t line,
nullable<std::uint32_t> column,
std::size_t context_size,
bool color
) {
return detail::get_snippet(path, line, column, context_size, color);
}
bool isatty(int fd) {
return detail::isatty(fd);
}
#if IS_WINDOWS
extern const int stdin_fileno = detail::fileno(stdin);
extern const int stdout_fileno = detail::fileno(stdout);
extern const int stderr_fileno = detail::fileno(stderr);
#else
extern const int stdin_fileno = STDIN_FILENO;
extern const int stdout_fileno = STDOUT_FILENO;
extern const int stderr_fileno = STDERR_FILENO;
#endif
namespace detail {
const formatter& get_terminate_formatter() {
static formatter the_formatter = formatter{}
.header("Stack trace to reach terminate handler (most recent call first):");
return the_formatter;
}
}
CPPTRACE_FORCE_NO_INLINE void print_terminate_trace() {
try { // try/catch can never be hit but it's needed to prevent TCO
detail::get_terminate_formatter().print(std::cerr, generate_trace(1));
} catch(...) {
detail::log_and_maybe_propagate_exception(std::current_exception());
}
}
[[noreturn]] void MSVC_CDECL terminate_handler() {
// TODO: Support std::nested_exception?
try {
auto ptr = std::current_exception();
if(ptr == nullptr) {
fputs("terminate called without an active exception", stderr);
print_terminate_trace();
} else {
std::rethrow_exception(ptr);
}
} catch(cpptrace::exception& e) {
microfmt::print(
stderr,
"Terminate called after throwing an instance of {}: {}\n",
demangle(typeid(e).name()),
e.message()
);
e.trace().print(std::cerr, isatty(stderr_fileno));
} catch(std::exception& e) {
microfmt::print(
stderr, "Terminate called after throwing an instance of {}: {}\n", demangle(typeid(e).name()), e.what()
);
print_terminate_trace();
} catch(...) {
microfmt::print(
stderr, "Terminate called after throwing an instance of {}\n", detail::exception_type_name()
);
print_terminate_trace();
}
std::flush(std::cerr);
abort();
}
void register_terminate_handler() {
std::set_terminate(terminate_handler);
}
#if defined(_WIN32) && !defined(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP)
void load_symbols_for_file(const std::string&) {
// nop
}
#endif
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,63 @@
#ifndef COMMON_HPP
#define COMMON_HPP
#include <cpptrace/basic.hpp>
#include "platform/platform.hpp"
#include <cstdint>
#define ESC "\033["
#define RESET ESC "0m"
#define RED ESC "31m"
#define GREEN ESC "32m"
#define YELLOW ESC "33m"
#define BLUE ESC "34m"
#define MAGENTA ESC "35m"
#define CYAN ESC "36m"
#if IS_GCC || IS_CLANG
#define NODISCARD __attribute__((warn_unused_result))
// #elif IS_MSVC && _MSC_VER >= 1700
// #define NODISCARD _Check_return_
#else
#define NODISCARD
#endif
// workaround a bizarre gcc bug https://godbolt.org/z/s78vnf7jv
// https://github.com/jeremy-rifkin/cpptrace/issues/220
#if defined(__GNUC__) && (__GNUC__ < 7)
#undef NODISCARD
#define NODISCARD
#endif
#if IS_MSVC
#define MSVC_CDECL __cdecl
#else
#define MSVC_CDECL
#endif
// support is pretty good https://godbolt.org/z/djTqv7WMY, checked in cmake during config
#ifdef HAS_ATTRIBUTE_PACKED
#define PACKED __attribute__((packed))
#else
#define PACKED
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
inline stacktrace_frame null_frame() {
return {
0,
0,
nullable<std::uint32_t>::null(),
nullable<std::uint32_t>::null(),
"",
"",
false
};
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,85 @@
#include "utils/error.hpp"
#include <cpptrace/utils.hpp>
#include <exception>
#include "platform/exception_type.hpp"
#include "logging.hpp"
#include "options.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
internal_error::internal_error(std::string message) : msg("Cpptrace internal error: " + std::move(message)) {}
constexpr const char* assert_actions[] = {"assertion", "verification", "panic"};
constexpr const char* assert_names[] = {"ASSERT", "VERIFY", "PANIC"};
void assert_fail(
assert_type type,
const char* expression,
const char* signature,
source_location location,
const char* message
) {
const char* action = assert_actions[static_cast<std::underlying_type<assert_type>::type>(type)];
const char* name = assert_names[static_cast<std::underlying_type<assert_type>::type>(type)];
if(message == nullptr) {
throw internal_error(
"Cpptrace {} failed at {}:{}: {}\n"
" {}({});\n",
action, location.file, location.line, signature,
name, expression
);
} else {
throw internal_error(
"Cpptrace {} failed at {}:{}: {}: {}\n"
" {}({});\n",
action, location.file, location.line, signature, message,
name, expression
);
}
}
void panic(
const char* signature,
source_location location,
string_view message
) {
if(message == "") {
throw internal_error(
"Cpptrace panic {}:{}: {}\n",
location.file, location.line, signature
);
} else {
throw internal_error(
"Cpptrace panic {}:{}: {}: {}\n",
location.file, location.line, signature, message
);
}
}
void log_and_maybe_propagate_exception(std::exception_ptr ptr) {
try {
if(ptr) {
std::rethrow_exception(ptr);
}
} catch(const internal_error& e) {
log::error("Unhandled cpptrace internal error: {}", e.what());
} catch(const std::exception& e) {
log::error(
"Unhandled cpptrace internal error of type {}: {}",
cpptrace::demangle(typeid(e).name()),
e.what()
);
} catch(...) {
log::error("Unhandled cpptrace internal error of type {}", detail::exception_type_name());
}
if(!should_absorb_trace_exceptions()) {
if(ptr) {
std::rethrow_exception(ptr);
}
}
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,133 @@
#ifndef ERROR_HPP
#define ERROR_HPP
#include <exception>
#include <string>
#include <utility>
#include "platform/platform.hpp"
#include "utils/microfmt.hpp"
#include "utils/string_view.hpp"
#include <cpptrace/utils.hpp>
#if IS_MSVC
#define CPPTRACE_PFUNC __FUNCSIG__
#else
#define CPPTRACE_PFUNC __extension__ __PRETTY_FUNCTION__
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
class internal_error : public std::exception {
std::string msg;
public:
internal_error(std::string message);
template<typename... Args>
internal_error(const char* format, Args&&... args) : internal_error(microfmt::format(format, args...)) {}
const char* what() const noexcept override {
return msg.c_str();
}
};
// Lightweight std::source_location.
struct source_location {
const char* const file;
const int line;
constexpr source_location(
const char* _file,
int _line
) : file(_file), line(_line) {}
};
#define CPPTRACE_CURRENT_LOCATION ::cpptrace::detail::source_location(__FILE__, __LINE__)
enum class assert_type {
assert,
verify,
panic,
};
[[noreturn]] CPPTRACE_EXPORT void assert_fail(
assert_type type,
const char* expression,
const char* signature,
source_location location,
const char* message
);
[[noreturn]] void panic(
const char* signature,
source_location location,
string_view message = ""
);
template<typename... Args>
void nullfn(Args&&...);
#define PHONY_USE(...) (static_cast<decltype(nullfn(__VA_ARGS__))>(0))
// Work around a compiler warning
template<typename T>
std::string as_string(T&& value) {
return std::string(std::forward<T>(value));
}
inline std::string as_string() {
return "";
}
// Check condition in both debug and release. std::runtime_error on failure.
#define PANIC(...) ((::cpptrace::detail::panic)(CPPTRACE_PFUNC, CPPTRACE_CURRENT_LOCATION, ::cpptrace::detail::as_string(__VA_ARGS__)))
inline void assert_impl(
bool condition,
const char* message,
assert_type type,
const char* args,
const char* signature,
source_location location
) {
if(!condition) {
assert_fail(type, args, signature, location, message);
}
}
inline void assert_impl(
bool condition,
assert_type type,
const char* args,
const char* signature,
source_location location
) {
assert_impl(
condition,
nullptr,
type,
args,
signature,
location
);
}
// Check condition in both debug and release. std::runtime_error on failure.
#define VERIFY(...) ( \
assert_impl(__VA_ARGS__, ::cpptrace::detail::assert_type::verify, #__VA_ARGS__, CPPTRACE_PFUNC, CPPTRACE_CURRENT_LOCATION) \
)
#ifndef NDEBUG
// Check condition in both debug. std::runtime_error on failure.
#define ASSERT(...) ( \
assert_impl(__VA_ARGS__, ::cpptrace::detail::assert_type::assert, #__VA_ARGS__, CPPTRACE_PFUNC, CPPTRACE_CURRENT_LOCATION) \
)
#else
// Check condition in both debug. std::runtime_error on failure.
#define ASSERT(...) PHONY_USE(__VA_ARGS__)
#endif
void log_and_maybe_propagate_exception(std::exception_ptr);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,50 @@
#ifndef BASE_FILE_HPP
#define BASE_FILE_HPP
#include "utils/span.hpp"
#include "utils/utils.hpp"
#include <type_traits>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
class base_file {
public:
virtual ~base_file() = default;
virtual string_view path() const = 0;
virtual Result<monostate, internal_error> read_bytes(bspan buffer, off_t offset) const = 0;
template<
typename T,
typename std::enable_if<
std::is_standard_layout<T>::value && is_trivially_copyable<T>::value && !is_span<T>::value,
int
>::type = 0
>
Result<T, internal_error> read(off_t offset) {
T object{};
auto res = read_bytes(make_bspan(object), offset);
if(!res) {
return res.unwrap_error();
}
return object;
}
template<
typename T,
typename std::enable_if<
std::is_standard_layout<T>::value && is_trivially_copyable<T>::value,
int
>::type = 0
>
Result<monostate, internal_error> read_span(span<T> items, off_t offset) {
return read_bytes(
make_span(reinterpret_cast<char*>(items.data()), reinterpret_cast<char*>(items.data() + items.size())),
offset
);
}
};
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,28 @@
#define _CRT_SECURE_NO_WARNINGS
#include "utils/io/file.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
string_view file::path() const {
return object_path;
}
Result<file, internal_error> file::open(cstring_view object_path) {
auto file_obj = raii_wrap(std::fopen(object_path.c_str(), "rb"), file_deleter);
if(file_obj == nullptr) {
return internal_error("Unable to read object file {}", object_path);
}
return file(std::move(file_obj), object_path);
}
Result<monostate, internal_error> file::read_bytes(bspan buffer, off_t offset) const {
if(std::fseek(file_obj, offset, SEEK_SET) != 0) {
return internal_error("fseek error in {} at offset {}", path(), offset);
}
if(std::fread(buffer.data(), buffer.size(), 1, file_obj) != 1) {
return internal_error("fread error in {} at offset {} for {} bytes", path(), offset, buffer.size());
}
return monostate{};
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,30 @@
#ifndef FILE_HPP
#define FILE_HPP
#include "utils/string_view.hpp"
#include "utils/span.hpp"
#include "utils/io/base_file.hpp"
#include "utils/utils.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
class file : public base_file {
file_wrapper file_obj;
std::string object_path;
file(file_wrapper file_obj, string_view path) : file_obj(std::move(file_obj)), object_path(path) {}
public:
file(file&&) = default;
~file() override = default;
string_view path() const override;
static Result<file, internal_error> open(cstring_view object_path);
virtual Result<monostate, internal_error> read_bytes(bspan buffer, off_t offset) const override;
};
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,23 @@
#include "utils/io/memory_file_view.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
string_view memory_file_view::path() const {
return object_path;
}
Result<monostate, internal_error> memory_file_view::read_bytes(bspan buffer, off_t offset) const {
if(offset < 0) {
return internal_error("Illegal read in memory file {}: offset {}", path(), offset);
}
if(offset + buffer.size() > data.size()) {
return internal_error(
"Illegal read in memory file {}: offset = {}, size = {}, file size = {}",
path(), offset, buffer.size(), data.size()
);
}
std::memcpy(buffer.data(), data.data() + offset, buffer.size());
return monostate{};
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,26 @@
#ifndef MEMORY_FILE_VIEW_HPP
#define MEMORY_FILE_VIEW_HPP
#include "utils/error.hpp"
#include "utils/span.hpp"
#include "utils/io/base_file.hpp"
#include "utils/utils.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
class memory_file_view : public base_file {
cbspan data;
std::string object_path = "<memory file>";
public:
memory_file_view(cbspan data) : data(data) {}
~memory_file_view() override = default;
string_view path() const override;
virtual Result<monostate, internal_error> read_bytes(bspan buffer, off_t offset) const override;
};
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,108 @@
#ifndef LRU_CACHE_HPP
#define LRU_CACHE_HPP
#include "utils/error.hpp"
#include "utils/optional.hpp"
#include <list>
#include <unordered_map>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
template<typename K, typename V>
class lru_cache {
struct kvp {
K key;
V value;
};
using list_type = std::list<kvp>;
using list_iterator = typename list_type::iterator;
mutable list_type lru;
std::unordered_map<K, list_iterator> map;
optional<std::size_t> max_size;
public:
lru_cache() = default;
lru_cache(optional<std::size_t> max_size) : max_size(max_size) {
VERIFY(!max_size || max_size.unwrap() > 0);
}
void set_max_size(optional<std::size_t> size) {
VERIFY(!size || size.unwrap() > 0);
max_size = size;
maybe_trim();
}
void maybe_touch(const K& key) {
auto it = map.find(key);
if(it == map.end()) {
return;
}
auto list_it = it->second;
touch(list_it);
}
optional<V&> maybe_get(const K& key) {
auto it = map.find(key);
if(it == map.end()) {
return nullopt;
} else {
touch(it->second);
return it->second->value;
}
}
optional<const V&> maybe_get(const K& key) const {
auto it = map.find(key);
if(it == map.end()) {
return nullopt;
} else {
touch(it->second);
return it->second->value;
}
}
void set(const K& key, V value) {
auto it = map.find(key);
if(it == map.end()) {
insert(key, std::move(value));
} else {
touch(it->second);
it->second->value = std::move(value);
}
}
optional<V&> insert(const K& key, V value) {
auto pair = map.insert({key, lru.end()});
if(!pair.second) {
// didn't insert
return nullopt;
}
auto map_it = pair.first;
lru.push_front({key, std::move(value)});
map_it->second = lru.begin();
maybe_trim();
return lru.front().value;
}
std::size_t size() const {
return lru.size();
}
private:
void touch(list_iterator list_it) const {
lru.splice(lru.begin(), lru, list_it);
}
void maybe_trim() {
while(max_size && lru.size() > max_size.unwrap()) {
const auto& to_remove = lru.back();
map.erase(to_remove.key);
lru.pop_back();
}
}
};
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,15 @@
#include "utils/microfmt.hpp"
#include <iostream>
CPPTRACE_BEGIN_NAMESPACE
namespace microfmt {
namespace detail {
std::ostream& get_cout() {
return std::cout;
}
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,312 @@
#ifndef MICROFMT_HPP
#define MICROFMT_HPP
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <string>
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L)
#include <string_view>
#endif
#ifdef _MSC_VER
#include <intrin.h>
#endif
#include "utils/string_view.hpp"
// https://github.com/jeremy-rifkin/microfmt
// Format: {[align][width][:[fill][base]]} # width: number or {}
CPPTRACE_BEGIN_NAMESPACE
namespace microfmt {
namespace detail {
inline std::uint64_t clz(std::uint64_t value) {
#ifdef _MSC_VER
unsigned long out = 0;
#ifdef _WIN64
_BitScanReverse64(&out, value);
#else
if(_BitScanReverse(&out, std::uint32_t(value >> 32))) {
return 63 - int(out + 32);
}
_BitScanReverse(&out, std::uint32_t(value));
#endif
return 63 - out;
#else
return __builtin_clzll(value);
#endif
}
template<typename U, typename V> U to(V v) {
return static_cast<U>(v); // A way to cast to U without "warning: useless cast to type"
}
enum class alignment { left, right };
struct format_options {
alignment align = alignment::left;
char fill = ' ';
size_t width = 0;
char base = 'd';
};
template<typename OutputIt, typename InputIt>
void do_write(OutputIt out, InputIt begin, InputIt end, const format_options& options) {
auto size = end - begin;
if(static_cast<std::size_t>(size) >= options.width) {
std::copy(begin, end, out);
} else {
if(options.align == alignment::left) {
std::copy(begin, end, out);
std::fill_n(out, options.width - size, options.fill);
} else {
std::fill_n(out, options.width - size, options.fill);
std::copy(begin, end, out);
}
}
}
template<int shift, int mask>
std::string to_string(std::uint64_t value, const char* digits = "0123456789abcdef") {
if(value == 0) {
return "0";
} else {
// digits = floor(1 + log_base(x))
// log_base(x) = log_2(x) / log_2(base)
// log_2(x) == 63 - clz(x)
// 1 + (63 - clz(value)) / (63 - clz(1 << shift))
// 63 - clz(1 << shift) is the same as shift
auto n_digits = to<std::size_t>(1 + (63 - clz(value)) / shift);
std::string number;
number.resize(n_digits);
std::size_t i = n_digits - 1;
while(value > 0) {
number[i--] = digits[value & mask];
value >>= shift;
}
return number;
}
}
inline std::string to_string(std::uint64_t value, const format_options& options) {
switch(options.base) {
case 'H': return to_string<4, 0xf>(value, "0123456789ABCDEF");
case 'h': return to_string<4, 0xf>(value);
case 'o': return to_string<3, 0x7>(value);
case 'b': return to_string<1, 0x1>(value);
default: return std::to_string(value); // failure: decimal
}
}
struct string_view {
const char* data;
std::size_t size;
};
class format_value {
enum class value_type {
char_value,
int64_value,
uint64_value,
string_value,
string_view_value,
c_string_value,
};
union {
char char_value;
std::int64_t int64_value;
std::uint64_t uint64_value;
const std::string* string_value;
string_view string_view_value;
const char* c_string_value;
};
value_type value;
public:
format_value(char c) : char_value(c), value(value_type::char_value) {}
format_value(short int_val) : int64_value(int_val), value(value_type::int64_value) {}
format_value(int int_val) : int64_value(int_val), value(value_type::int64_value) {}
format_value(long int_val) : int64_value(int_val), value(value_type::int64_value) {}
format_value(long long int_val) : int64_value(int_val), value(value_type::int64_value) {}
format_value(unsigned char int_val) : uint64_value(int_val), value(value_type::uint64_value) {}
format_value(unsigned short int_val) : uint64_value(int_val), value(value_type::uint64_value) {}
format_value(unsigned int int_val) : uint64_value(int_val), value(value_type::uint64_value) {}
format_value(unsigned long int_val) : uint64_value(int_val), value(value_type::uint64_value) {}
format_value(unsigned long long int_val) : uint64_value(int_val), value(value_type::uint64_value) {}
format_value(const std::string& string) : string_value(&string), value(value_type::string_value) {}
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L)
format_value(std::string_view sv)
: string_view_value{sv.data(), sv.size()}, value(value_type::string_view_value) {}
#endif
format_value(cpptrace::detail::string_view sv)
: string_view_value{sv.data(), sv.size()}, value(value_type::string_view_value) {}
format_value(const char* c_string) : c_string_value(c_string), value(value_type::c_string_value) {}
int unwrap_int() const {
switch(value) {
case value_type::int64_value: return static_cast<int>(int64_value);
case value_type::uint64_value: return static_cast<int>(uint64_value);
default: return 0; // failure: just 0
}
}
public:
template<typename OutputIt>
void write(OutputIt out, const format_options& options) const {
switch(value) {
case value_type::char_value:
do_write(out, &char_value, &char_value + 1, options);
break;
case value_type::int64_value:
{
std::string str;
std::int64_t val = int64_value;
if(val < 0) {
str += '-';
val *= -1;
}
str += to_string(static_cast<std::uint64_t>(val), options);
do_write(out, str.begin(), str.end(), options);
}
break;
case value_type::uint64_value:
{
std::string str = to_string(uint64_value, options);
do_write(out, str.begin(), str.end(), options);
}
break;
case value_type::string_value:
do_write(out, string_value->begin(), string_value->end(), options);
break;
case value_type::string_view_value:
do_write(out, string_view_value.data, string_view_value.data + string_view_value.size, options);
break;
case value_type::c_string_value:
do_write(out, c_string_value, c_string_value + std::strlen(c_string_value), options);
break;
} // failure: nop
}
};
// note: previously used std::array and there was a bug with std::array<T, 0> affecting old msvc
// https://godbolt.org/z/88T8hrzzq mre: https://godbolt.org/z/drd8echbP
template<typename OutputIt, typename InputIt>
void format(OutputIt out, InputIt fmt_begin, InputIt fmt_end, const std::initializer_list<format_value>& args) {
std::size_t arg_i = 0;
auto it = fmt_begin;
auto peek = [&] (std::size_t dist) -> char { // 0 on failure
return fmt_end - it > signed(dist) ? *(it + dist) : 0;
};
auto read_number = [&] () -> int { // -1 on failure
auto scan = it;
int num = 0;
while(scan != fmt_end && isdigit(*scan)) {
num *= 10;
num += *scan - '0';
scan++;
}
if(scan != it) {
it = scan;
return num;
} else {
return -1;
}
};
for(; it != fmt_end; it++) {
if((*it == '{' || *it == '}') && peek(1) == *it) { // parse {{ and }} escapes
it++;
} else if(*it == '{' && it + 1 != fmt_end) {
auto saved_it = it;
auto handle_formatter = [&] () {
it++;
format_options options;
// try to parse alignment
if(*it == '<' || *it == '>') {
options.align = *it++ == '<' ? alignment::left : alignment::right;
}
// try to parse width
auto width = read_number(); // handles fmt_end check
if(width != -1) {
options.width = width;
} else if(it != fmt_end && *it == '{') { // try to parse variable width
if(peek(1) != '}') {
return false;
}
it += 2;
options.width = arg_i < args.size() ? args.begin()[arg_i++].unwrap_int() : 0;
}
// try to parse fill/base
if(it != fmt_end && *it == ':') {
it++;
if(fmt_end - it > 1 && *it != '}' && peek(1) != '}') { // two chars before the }, fill+base
options.fill = *it++;
options.base = *it++;
} else if(it != fmt_end && *it != '}') { // one char before the }, just base
if(*it == 'd' || *it == 'h' || *it == 'H' || *it == 'o' || *it == 'b') {
options.base = *it++;
} else {
options.fill = *it++;
}
}
}
if(it == fmt_end || *it != '}') {
return false;
}
if(arg_i < args.size()) {
args.begin()[arg_i++].write(out, options);
}
return true;
};
if(handle_formatter()) {
continue; // If reached here, successfully parsed and wrote a formatter. Don't write *it.
}
it = saved_it; // go back
}
*out++ = *it;
}
}
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L)
template<typename OutputIt>
void format(OutputIt out, std::string_view fmt, const std::initializer_list<format_value>& args) {
return format(out, fmt.begin(), fmt.end(), args);
}
#endif
template<typename OutputIt>
void format(OutputIt out, const char* fmt, const std::initializer_list<format_value>& args) {
return format(out, fmt, fmt + std::strlen(fmt), args);
}
std::ostream& get_cout();
}
template<typename S, typename... Args>
std::string format(const S& fmt, Args&&... args) {
std::string str;
detail::format(std::back_inserter(str), fmt, {detail::format_value(args)...});
return str;
}
template<typename S, typename... Args>
void print(const S& fmt, Args&&... args) {
detail::format(std::ostream_iterator<char>(detail::get_cout()), fmt, {args...});
}
template<typename S, typename... Args>
void print(std::ostream& ostream, const S& fmt, Args&&... args) {
detail::format(std::ostream_iterator<char>(ostream), fmt, {args...});
}
template<typename S, typename... Args>
void print(std::FILE* stream, const S& fmt, Args&&... args) {
auto str = format(fmt, args...);
fwrite(str.data(), 1, str.size(), stream);
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,170 @@
#ifndef OPTIONAL_HPP
#define OPTIONAL_HPP
#include <functional>
#include <new>
#include <type_traits>
#include <utility>
#include "utils/common.hpp"
#include "utils/error.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
struct nullopt_t {};
static constexpr nullopt_t nullopt;
template<
typename T,
typename std::enable_if<
!std::is_same<typename std::decay<T>::type, void>::value && !std::is_rvalue_reference<T>::value,
int
>::type = 0
>
using well_behaved = typename std::conditional<
std::is_reference<T>::value, std::reference_wrapper<typename std::remove_reference<T>::type>, T
>::type;
template<
typename T,
typename std::enable_if<!std::is_same<typename std::decay<T>::type, void>::value, int>::type = 0
>
class optional {
using value_type = well_behaved<T>;
union {
char x{};
value_type uvalue;
};
bool holds_value = false;
public:
optional() noexcept {}
optional(nullopt_t) noexcept {}
~optional() {
reset();
}
optional(const optional& other) : holds_value(other.holds_value) {
if(holds_value) {
new (static_cast<void*>(std::addressof(uvalue))) value_type(other.uvalue);
}
}
optional(optional&& other)
noexcept(std::is_nothrow_move_constructible<value_type>::value)
: holds_value(other.holds_value)
{
if(holds_value) {
new (static_cast<void*>(std::addressof(uvalue))) value_type(std::move(other.uvalue));
}
}
optional& operator=(const optional& other) {
optional copy(other);
swap(copy);
return *this;
}
optional& operator=(optional&& other)
noexcept(std::is_nothrow_move_assignable<value_type>::value && std::is_nothrow_move_constructible<value_type>::value)
{
if (this != &other) {
reset();
if (other.holds_value) {
new (static_cast<void*>(std::addressof(uvalue))) value_type(std::move(other.uvalue));
holds_value = true;
}
}
return *this;
}
template<
typename U = T,
typename std::enable_if<!std::is_same<typename std::decay<U>::type, optional<T>>::value, int>::type = 0
>
optional(U&& value) : holds_value(true) {
new (static_cast<void*>(std::addressof(uvalue))) value_type(std::forward<U>(value));
}
template<
typename U = T,
typename std::enable_if<!std::is_same<typename std::decay<U>::type, optional<T>>::value, int>::type = 0
>
optional& operator=(U&& value) {
optional o(std::forward<U>(value));
swap(o);
return *this;
}
optional& operator=(nullopt_t) noexcept {
reset();
return *this;
}
void swap(optional& other) noexcept {
if(holds_value && other.holds_value) {
std::swap(uvalue, other.uvalue);
} else if(holds_value && !other.holds_value) {
new (&other.uvalue) value_type(std::move(uvalue));
uvalue.~value_type();
} else if(!holds_value && other.holds_value) {
new (static_cast<void*>(std::addressof(uvalue))) value_type(std::move(other.uvalue));
other.uvalue.~value_type();
}
std::swap(holds_value, other.holds_value);
}
bool has_value() const {
return holds_value;
}
explicit operator bool() const {
return holds_value;
}
void reset() {
if(holds_value) {
uvalue.~value_type();
}
holds_value = false;
}
NODISCARD T& unwrap() & {
ASSERT(holds_value, "Optional does not contain a value");
return uvalue;
}
NODISCARD const T& unwrap() const & {
ASSERT(holds_value, "Optional does not contain a value");
return uvalue;
}
NODISCARD T&& unwrap() && {
ASSERT(holds_value, "Optional does not contain a value");
return std::move(uvalue);
}
NODISCARD const T&& unwrap() const && {
ASSERT(holds_value, "Optional does not contain a value");
return std::move(uvalue);
}
template<typename U>
NODISCARD T value_or(U&& default_value) const & {
return holds_value ? static_cast<T>(uvalue) : static_cast<T>(std::forward<U>(default_value));
}
template<typename U>
NODISCARD T value_or(U&& default_value) && {
return holds_value ? static_cast<T>(std::move(uvalue)) : static_cast<T>(std::forward<U>(default_value));
}
};
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,54 @@
#include "utils/replace_all.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
void replace_all(std::string& str, string_view substr, string_view replacement) {
std::string::size_type pos = 0;
while((pos = str.find(substr.data(), pos, substr.size())) != std::string::npos) {
str.replace(pos, substr.size(), replacement.data(), replacement.size());
pos += replacement.size();
}
}
void replace_all(std::string& str, const std::regex& re, string_view replacement) {
std::smatch match;
std::size_t i = 0;
while(std::regex_search(str.cbegin() + i, str.cend(), match, re)) {
str.replace(i + match.position(), match.length(), replacement.data(), replacement.size());
i += match.position() + replacement.size();
}
}
void replace_all_dynamic(std::string& str, string_view substr, string_view replacement) {
std::string::size_type pos = 0;
while((pos = str.find(substr.data(), pos, substr.size())) != std::string::npos) {
str.replace(pos, substr.size(), replacement.data(), replacement.size());
// advancing by one rather than replacement.length() in case replacement leads to
// another replacement opportunity, e.g. folding > > > to >> > then >>>
pos++;
}
}
void replace_all_template(std::string& str, const std::pair<std::regex, string_view>& rule) {
const auto& re = rule.first;
const auto& replacement = rule.second;
std::smatch match;
std::size_t cursor = 0;
while(std::regex_search(str.cbegin() + cursor, str.cend(), match, re)) {
// find matching >
const std::size_t match_begin = cursor + match.position();
std::size_t end = match_begin + match.length();
for(int c = 1; end < str.size() && c > 0; end++) {
if(str[end] == '<') {
c++;
} else if(str[end] == '>') {
c--;
}
}
// make the replacement
str.replace(match_begin, end - match_begin, replacement.data(), replacement.size());
cursor = match_begin + replacement.size();
}
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,25 @@
#ifndef REPLACE_ALL
#define REPLACE_ALL
#include <string>
#include <regex>
#include "utils/string_view.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
// replace all instances of substr with the replacement
void replace_all(std::string& str, string_view substr, string_view replacement);
// replace all regex matches with the replacement
void replace_all(std::string& str, const std::regex& re, string_view replacement);
// replace all instances of substr with the replacement, including new instances introduced by the replacement
void replace_all_dynamic(std::string& str, string_view substr, string_view replacement);
// replace all matches of a regex including template parameters
void replace_all_template(std::string& str, const std::pair<std::regex, string_view>& rule);
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,185 @@
#ifndef RESULT_HPP
#define RESULT_HPP
#include <new>
#include <type_traits>
#include <utility>
#include "utils/common.hpp"
#include "utils/error.hpp"
#include "utils/optional.hpp"
#include "options.hpp"
#include "logging.hpp"
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
template<typename T, typename E, typename std::enable_if<!std::is_same<T, E>::value, int>::type = 0>
class Result {
using value_type = well_behaved<T>;
union {
value_type value_;
E error_;
};
enum class member { value, error };
member active;
void destroy() {
if(active == member::value) {
value_.~value_type();
} else {
error_.~E();
}
}
public:
Result(value_type&& value) : value_(std::move(value)), active(member::value) {}
Result(E&& error) : error_(std::move(error)), active(member::error) {
log::debug("Error result constructed: {}", unwrap_error().what());
}
Result(const value_type& value) : value_(value_type(value)), active(member::value) {}
Result(const E& error) : error_(E(error)), active(member::error) {
log::debug("Error result constructed: {}", unwrap_error().what());
}
template<
typename U = T,
typename std::enable_if<
!std::is_same<typename std::decay<U>::type, Result<T, E>>::value &&
std::is_constructible<value_type, U>::value &&
!std::is_constructible<E, U>::value,
int
>::type = 0
>
Result(U&& u) : Result(value_type(std::forward<U>(u))) {}
Result(Result&& other) : active(other.active) {
if(other.active == member::value) {
new (&value_) value_type(std::move(other.value_));
} else {
new (&error_) E(std::move(other.error_));
}
}
~Result() {
destroy();
}
Result& operator=(const Result& other) {
if (this != &other) {
destroy();
if(other.active == member::value) {
new (&value_) value_type(other.value_);
} else {
new (&error_) E(other.error_);
}
active = other.active;
}
return *this;
}
Result& operator=(Result&& other)
noexcept(
std::is_nothrow_move_constructible<value_type>::value && std::is_nothrow_move_constructible<E>::value
)
{
if (this != &other) {
destroy();
if(other.active == member::value) {
new (&value_) value_type(std::move(other.value_));
} else {
new (&error_) E(std::move(other.error_));
}
active = other.active;
}
return *this;
}
bool has_value() const {
return active == member::value;
}
bool is_error() const {
return active == member::error;
}
explicit operator bool() const {
return has_value();
}
NODISCARD optional<T> value() const & {
return has_value() ? optional<T>(value_) : nullopt;
}
NODISCARD optional<E> error() const & {
return is_error() ? optional<E>(error_) : nullopt;
}
NODISCARD optional<T> value() && {
return has_value() ? optional<T>(std::move(value_)) : nullopt;
}
NODISCARD optional<E> error() && {
return is_error() ? optional<E>(std::move(error_)) : nullopt;
}
NODISCARD T& unwrap_value() & {
ASSERT(has_value(), "Result does not contain a value");
return value_;
}
NODISCARD const T& unwrap_value() const & {
ASSERT(has_value(), "Result does not contain a value");
return value_;
}
NODISCARD T unwrap_value() && {
ASSERT(has_value(), "Result does not contain a value");
return std::move(value_);
}
NODISCARD E& unwrap_error() & {
ASSERT(is_error(), "Result does not contain an error");
return error_;
}
NODISCARD const E& unwrap_error() const & {
ASSERT(is_error(), "Result does not contain an error");
return error_;
}
NODISCARD E unwrap_error() && {
ASSERT(is_error(), "Result does not contain an error");
return std::move(error_);
}
template<typename U>
NODISCARD T value_or(U&& default_value) const & {
return has_value() ? static_cast<T>(value_) : static_cast<T>(std::forward<U>(default_value));
}
template<typename U>
NODISCARD T value_or(U&& default_value) && {
return has_value() ? static_cast<T>(std::move(value_)) : static_cast<T>(std::forward<U>(default_value));
}
template<typename F>
NODISCARD auto transform(F&& f) & -> Result<decltype(f(std::declval<T&>())), E> {
if(has_value()) {
return f(unwrap_value());
} else {
return unwrap_error();
}
}
template<typename F>
NODISCARD auto transform(F&& f) && -> Result<decltype(f(std::declval<T&&>())), E> {
if(has_value()) {
return f(std::move(unwrap_value()));
} else {
return unwrap_error();
}
}
void drop_error() const {
if(is_error()) {
log::error(unwrap_error().what());
}
}
};
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,98 @@
#ifndef SPAN_HPP
#define SPAN_HPP
#include "utils/utils.hpp"
#include <cstddef>
#include <iterator>
#include <memory>
#include <type_traits>
#include <utility>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
// basic span implementation
// I haven't implemented most members because I don't need them, more will be added as needed
template<typename T>
class span {
T* ptr;
std::size_t count;
public:
using element_type = T;
using value_type = typename std::remove_cv<T>::type;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using iterator = T*;
using const_iterator = const T*;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<reverse_iterator>;
using i_am_span = void;
span() : ptr(nullptr), count(0) {}
span(T* ptr, std::size_t count) : ptr(ptr), count(count) {}
template<typename It>
span(It begin, It end) : ptr(std::addressof(*begin)), count(end - begin) {}
T* data() const noexcept {
return ptr;
}
std::size_t size() const noexcept {
return count;
}
bool empty() const noexcept {
return count == 0;
}
iterator begin() noexcept {
return ptr;
}
iterator end() noexcept {
return ptr + count;
}
const_iterator begin() const noexcept {
return ptr;
}
const_iterator end() const noexcept {
return ptr + count;
}
};
using bspan = span<char>;
using cbspan = span<const char>;
template<typename T, typename = int>
struct is_span : std::false_type {};
template<typename T>
struct is_span<T, void_t<typename T::i_am_span>> : std::true_type {};
template<typename It>
auto make_span(It begin, It end) -> span<typename std::remove_reference<decltype(*begin)>::type> {
return {begin, end};
}
template<typename It>
auto make_span(It begin, std::size_t count) -> span<typename std::remove_reference<decltype(*begin)>::type> {
return {begin, count};
}
template<
typename T,
typename std::enable_if<
std::is_standard_layout<T>::value && is_trivially_copyable<T>::value && !is_span<T>::value,
int
>::type = 0
>
span<char> make_bspan(T& object) {
return span<char>(reinterpret_cast<char*>(std::addressof(object)), sizeof(object));
}
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,63 @@
#include "utils/string_view.hpp"
#include "utils/error.hpp"
#include "utils/microfmt.hpp"
#include <algorithm>
#include <cstring>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
constexpr std::size_t string_view::npos;
char string_view::operator[](size_t i) const {
ASSERT(i < size());
return ptr[i];
}
char string_view::at(size_t i) const {
if(i >= size()) {
throw std::runtime_error(microfmt::format("Out of bounds access {} >= {}", i, size()));
}
return ptr[i];
}
bool string_view::starts_with(string_view str) const {
return substr(0, str.size()) == str;
}
bool string_view::ends_with(string_view str) const {
if(size() < str.size()) {
return false;
}
return substr(size() - str.size(), str.size()) == str;
}
std::size_t string_view::find_last_of(string_view chars) const {
if(empty() || chars.empty()) {
return npos;
}
std::size_t pos = size();
while(pos-- > 0) {
if(std::find(chars.begin(), chars.end(), ptr[pos]) != chars.end()) {
return pos;
}
}
return npos;
}
bool operator==(string_view a, string_view b) {
return a.size() == b.size() && std::memcmp(a.data(), b.data(), a.size()) == 0;
}
constexpr std::size_t cstring_view::npos;
cstring_view cstring_view::substr(std::size_t pos) const {
ASSERT(pos <= count);
return {ptr + pos, count - pos};
}
void cstring_view::check_null() const {
ASSERT(ptr[count] == 0);
}
}
CPPTRACE_END_NAMESPACE

View File

@ -0,0 +1,179 @@
#ifndef STRING_VIEW_HPP
#define STRING_VIEW_HPP
#include <cstddef>
#include <cstring>
#include <iterator>
#include <string>
#include <cpptrace/utils.hpp>
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
// Simple string view implementations
// I haven't implemented all members because I don't need most of them currently, more may be added as needed
// members exported for tests
class string_view {
const char* ptr;
std::size_t count;
public:
using traits_type = std::char_traits<char>;
using value_type = char;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using pointer = char*;
using const_pointer = const char*;
using reference = char&;
using const_reference = const char&;
using iterator = char*;
using const_iterator = const char*;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<reverse_iterator>;
CPPTRACE_EXPORT static constexpr std::size_t npos = std::string::npos;
string_view() : ptr(nullptr), count(0) {}
string_view(const char* str) : ptr(str), count(std::strlen(str)) {}
string_view(const std::string& str) : ptr(str.c_str()), count(str.size()) {}
string_view(const char* ptr, std::size_t count) : ptr(ptr), count(count) {}
string_view(const char* begin, const char* end) : ptr(begin), count(end - begin) {}
explicit operator std::string() {
return std::string(ptr, ptr + count);
}
const char* data() const noexcept {
return ptr;
}
std::size_t size() const noexcept {
return count;
}
bool empty() const noexcept {
return count == 0;
}
CPPTRACE_EXPORT char operator[](size_t i) const;
CPPTRACE_EXPORT char at(size_t i) const;
char front() {
return operator[](0);
}
char back() {
return operator[](size() - 1);
}
string_view substr(size_t pos = 0, size_t n = npos) const {
return {ptr + pos, (std::min)(size() - pos, n)};
}
void advance(size_t n) {
*this = substr(n);
}
CPPTRACE_EXPORT bool starts_with(string_view chars) const;
CPPTRACE_EXPORT bool ends_with(string_view chars) const;
CPPTRACE_EXPORT std::size_t find_last_of(string_view chars) const;
const_iterator begin() const noexcept {
return ptr;
}
const_iterator end() const noexcept {
return ptr + count;
}
};
CPPTRACE_EXPORT bool operator==(string_view, string_view);
inline bool operator!=(string_view a, string_view b) {
return !(a == b);
}
inline void operator+=(std::string& str, string_view sv) {
str.append(sv.data(), sv.size());
}
class cstring_view {
const char* ptr;
std::size_t count;
public:
using traits_type = std::char_traits<char>;
using value_type = char;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using pointer = char*;
using const_pointer = const char*;
using reference = char&;
using const_reference = const char&;
using iterator = char*;
using const_iterator = const char*;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<reverse_iterator>;
CPPTRACE_EXPORT static constexpr std::size_t npos = string_view::npos;
cstring_view() : ptr(nullptr), count(0) {}
cstring_view(const char* str) : ptr(str), count(std::strlen(str)) {}
cstring_view(const std::string& str) : ptr(str.c_str()), count(str.size()) {}
cstring_view(const char* ptr, std::size_t count) : ptr(ptr), count(count) {
check_null();
}
explicit operator std::string() {
return std::string(ptr, ptr + count);
}
operator string_view() const noexcept {
return string_view(ptr, count);
}
const char* data() const noexcept {
return ptr;
}
const char* c_str() const noexcept {
return ptr;
}
std::size_t size() const noexcept {
return count;
}
bool empty() const noexcept {
return count == 0;
}
char operator[](size_t i) const {
return operator string_view().operator[](i);
}
char at(size_t i) const {
return operator string_view().at(i);
}
char front() {
return operator string_view().front();
}
char back() {
return operator string_view().back();
}
bool starts_with(string_view chars) const {
return operator string_view().starts_with(chars);
}
bool ends_with(string_view chars) const {
return operator string_view().ends_with(chars);
}
std::size_t find_last_of(string_view chars) const {
return operator string_view().find_last_of(chars);
}
CPPTRACE_EXPORT cstring_view substr(std::size_t pos) const;
const_iterator begin() const noexcept {
return ptr;
}
const_iterator end() const noexcept {
return ptr + count;
}
private:
CPPTRACE_EXPORT void check_null() const;
};
}
CPPTRACE_END_NAMESPACE
#endif

View File

@ -0,0 +1,61 @@
#include "utils/utils.hpp"
#include "utils/string_view.hpp"
#if IS_WINDOWS
#include <io.h>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/stat.h>
#include <unistd.h>
#endif
CPPTRACE_BEGIN_NAMESPACE
namespace detail {
bool isatty(int fd) {
#if IS_WINDOWS
return _isatty(fd);
#else
return ::isatty(fd);
#endif
}
int fileno(std::FILE* stream) {
#if IS_WINDOWS
return _fileno(stream);
#else
return ::fileno(stream);
#endif
}
void enable_virtual_terminal_processing_if_needed() noexcept {
// enable colors / ansi processing if necessary
#if IS_WINDOWS
// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
constexpr DWORD ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4;
#endif
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwMode = 0;
if(hOut == INVALID_HANDLE_VALUE) return;
if(!GetConsoleMode(hOut, &dwMode)) return;
if(dwMode != (dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
if(!SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) return;
#endif
}
bool directory_exists(cstring_view path) {
#if IS_WINDOWS
DWORD dwAttrib = GetFileAttributesA(path.c_str());
return dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY);
#else
struct stat sb;
return stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode);
#endif
}
}
CPPTRACE_END_NAMESPACE

Some files were not shown because too many files have changed in this diff Show More