diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab45134..717175e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,12 +19,21 @@ jobs: pip3 install colorama - name: libdwarf run: | + cd .. + mkdir zstd + cd zstd + git init + git remote add origin https://github.com/facebook/zstd.git + git fetch --depth 1 origin 63779c798237346c2b245c546c40b72a5a5913fe # 1.5.5 + git checkout FETCH_HEAD + make -j + sudo make install cd .. mkdir libdwarf cd libdwarf git init - git remote add origin https://github.com/jeremy-rifkin/libdwarf-code.git - git fetch --depth 1 origin 6216e185863f41d6f19ab850caabfff7326020d7 + git remote add origin https://github.com/jeremy-rifkin/libdwarf-lite.git + git fetch --depth 1 origin 5c0cb251f94b27e90184e6b2d9a0c9c62593babc git checkout FETCH_HEAD mkdir build cd build @@ -36,7 +45,7 @@ jobs: run: | python3 ci/build-in-all-configs.py --${{matrix.compiler}} build-macos: - runs-on: macos-13 + runs-on: macos-14 strategy: fail-fast: false matrix: @@ -48,12 +57,21 @@ jobs: pip3 install colorama - name: libdwarf run: | + cd .. + mkdir zstd + cd zstd + git init + git remote add origin https://github.com/facebook/zstd.git + git fetch --depth 1 origin 63779c798237346c2b245c546c40b72a5a5913fe # 1.5.5 + git checkout FETCH_HEAD + make -j + sudo make install cd .. mkdir libdwarf cd libdwarf git init - git remote add origin https://github.com/jeremy-rifkin/libdwarf-code.git - git fetch --depth 1 origin 6216e185863f41d6f19ab850caabfff7326020d7 + git remote add origin https://github.com/jeremy-rifkin/libdwarf-lite.git + git fetch --depth 1 origin 5c0cb251f94b27e90184e6b2d9a0c9c62593babc git checkout FETCH_HEAD mkdir build cd build @@ -79,24 +97,34 @@ jobs: pip3 install colorama - name: libdwarf run: | - cd .. - mkdir libdwarf - cd libdwarf - git init - git remote add origin https://github.com/jeremy-rifkin/libdwarf-code.git - git fetch --depth 1 origin 6216e185863f41d6f19ab850caabfff7326020d7 - git checkout FETCH_HEAD - mkdir build - cd build if("${{matrix.compiler}}" -eq "gcc") { + cd .. + mkdir zstd + cd zstd + git init + git remote add origin https://github.com/facebook/zstd.git + git fetch --depth 1 origin 63779c798237346c2b245c546c40b72a5a5913fe # 1.5.5 + git checkout FETCH_HEAD + cd build/cmake + mkdir build + cd build + cmake .. -DZSTD_BUILD_SHARED=On -DZSTD_BUILD_SHARED=Off -DZSTD_LEGACY_SUPPORT=Off -DZSTD_BUILD_PROGRAMS=Off -DZSTD_BUILD_CONTRIB=Off -DZSTD_BUILD_TESTS=Off -G"Unix Makefiles" + make -j + make install + cd ../../../.. + mkdir libdwarf + cd libdwarf + git init + git remote add origin https://github.com/jeremy-rifkin/libdwarf-lite.git + git fetch --depth 1 origin 5c0cb251f94b27e90184e6b2d9a0c9c62593babc + git checkout FETCH_HEAD + mkdir build + cd build cmake .. -DPIC_ALWAYS=TRUE -DBUILD_DWARFDUMP=FALSE -G"Unix Makefiles" make -j make install - } else { - cmake .. -DPIC_ALWAYS=TRUE -DBUILD_DWARFDUMP=FALSE - msbuild INSTALL.vcxproj + cd ../../cpptrace } - cd ../../cpptrace - name: build run: | python3 ci/build-in-all-configs.py --${{matrix.compiler}} diff --git a/.github/workflows/cmake-integration.yml b/.github/workflows/cmake-integration.yml index 12121a8..84a6b7a 100644 --- a/.github/workflows/cmake-integration.yml +++ b/.github/workflows/cmake-integration.yml @@ -7,6 +7,10 @@ on: jobs: test-linux-fetchcontent: runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: test @@ -17,11 +21,15 @@ jobs: cp -rv cpptrace/test/fetchcontent-integration . mkdir fetchcontent-integration/build cd fetchcontent-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG=$tag + cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG=$tag -DBUILD_SHARED_LIBS=${{matrix.shared}} make ./main test-linux-findpackage: runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: test @@ -29,7 +37,7 @@ jobs: tag=$(git rev-parse --abbrev-ref HEAD) mkdir build cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug + cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=${{matrix.shared}} sudo make -j install cd ../.. cp -rv cpptrace/test/findpackage-integration . @@ -40,6 +48,10 @@ jobs: ./main test-linux-add_subdirectory: runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: build @@ -49,12 +61,16 @@ jobs: cp -rv cpptrace add_subdirectory-integration mkdir add_subdirectory-integration/build cd add_subdirectory-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug + cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=${{matrix.shared}} make ./main test-macos-fetchcontent: - runs-on: macos-13 + runs-on: macos-14 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: test @@ -65,11 +81,15 @@ jobs: cp -rv cpptrace/test/fetchcontent-integration . mkdir fetchcontent-integration/build cd fetchcontent-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG=$tag + cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG=$tag -DBUILD_SHARED_LIBS=${{matrix.shared}} make ./main test-macos-findpackage: - runs-on: macos-13 + runs-on: macos-14 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: test @@ -78,7 +98,7 @@ jobs: echo $tag mkdir build cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug + cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=${{matrix.shared}} sudo make -j install cd ../.. cp -rv cpptrace/test/findpackage-integration . @@ -88,7 +108,11 @@ jobs: make ./main test-macos-add_subdirectory: - runs-on: macos-13 + runs-on: macos-14 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: test @@ -98,12 +122,16 @@ jobs: cp -rv cpptrace add_subdirectory-integration mkdir add_subdirectory-integration/build cd add_subdirectory-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug + cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=${{matrix.shared}} make ./main test-mingw-fetchcontent: runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: test @@ -114,11 +142,41 @@ jobs: cp -Recurse cpptrace/test/fetchcontent-integration . mkdir fetchcontent-integration/build cd fetchcontent-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG="$tag" -DCMAKE_BUILD_TYPE=g++ "-GUnix Makefiles" + cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG="$tag" "-GUnix Makefiles" -DBUILD_SHARED_LIBS=${{matrix.shared}} make .\main.exe + test-mingw-findpackage: + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + shared: [On, Off] + steps: + - uses: actions/checkout@v4 + - name: test + run: | + $tag=$(git rev-parse --abbrev-ref HEAD) + echo $tag + mkdir build + cd build + cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=${{matrix.shared}} "-GUnix Makefiles" -DCMAKE_INSTALL_PREFIX=C:/foo + make -j install + cd ../.. + mv cpptrace/test/findpackage-integration . + mkdir findpackage-integration/build + cd findpackage-integration/build + cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH=C:/foo "-GUnix Makefiles" + make + if("${{matrix.shared}}" -eq "On") { + cp C:/foo/bin/libcpptrace.dll . + } + ./main test-mingw-add_subdirectory: runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: test @@ -128,11 +186,15 @@ jobs: cp -Recurse cpptrace add_subdirectory-integration mkdir add_subdirectory-integration/build cd add_subdirectory-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_BUILD_TYPE=g++ "-GUnix Makefiles" + cmake .. -DCMAKE_BUILD_TYPE=Debug "-GUnix Makefiles" -DBUILD_SHARED_LIBS=${{matrix.shared}} make .\main.exe test-windows-fetchcontent: runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: Enable Developer Command Prompt @@ -145,11 +207,44 @@ jobs: cp -Recurse cpptrace/test/fetchcontent-integration . mkdir fetchcontent-integration/build cd fetchcontent-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG="$tag" + cmake .. -DCMAKE_BUILD_TYPE=Debug -DCPPTRACE_TAG="$tag" -DBUILD_SHARED_LIBS=${{matrix.shared}} msbuild demo_project.sln .\Debug\main.exe + test-windows-findpackage: + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + shared: [On, Off] + steps: + - uses: actions/checkout@v4 + - name: Enable Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1.10.0 + - name: test + run: | + $tag=$(git rev-parse --abbrev-ref HEAD) + echo $tag + mkdir build + cd build + cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=${{matrix.shared}} -DCMAKE_INSTALL_PREFIX=C:/foo + msbuild cpptrace.sln + msbuild INSTALL.vcxproj + cd ../.. + mv cpptrace/test/findpackage-integration . + mkdir findpackage-integration/build + cd findpackage-integration/build + cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH=C:/foo + msbuild demo_project.sln + if("${{matrix.shared}}" -eq "On") { + cp C:/foo/bin/cpptrace.dll . + } + .\Debug\main.exe test-windows-add_subdirectory: runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + shared: [On, Off] steps: - uses: actions/checkout@v4 - name: Enable Developer Command Prompt @@ -161,6 +256,6 @@ jobs: cp -Recurse cpptrace add_subdirectory-integration mkdir add_subdirectory-integration/build cd add_subdirectory-integration/build - cmake .. -DCMAKE_BUILD_TYPE=Debug + cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=${{matrix.shared}} msbuild demo_project.sln .\Debug\main.exe diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ec85a1..5090241 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,12 +22,21 @@ jobs: pip3 install colorama - name: libdwarf run: | + cd .. + mkdir zstd + cd zstd + git init + git remote add origin https://github.com/facebook/zstd.git + git fetch --depth 1 origin 63779c798237346c2b245c546c40b72a5a5913fe # 1.5.5 + git checkout FETCH_HEAD + make -j + sudo make install cd .. mkdir libdwarf cd libdwarf git init - git remote add origin https://github.com/jeremy-rifkin/libdwarf-code.git - git fetch --depth 1 origin 6216e185863f41d6f19ab850caabfff7326020d7 + git remote add origin https://github.com/jeremy-rifkin/libdwarf-lite.git + git fetch --depth 1 origin 5c0cb251f94b27e90184e6b2d9a0c9c62593babc git checkout FETCH_HEAD mkdir build cd build @@ -39,7 +48,7 @@ jobs: run: | python3 ci/test-all-configs.py --${{matrix.compiler}} test-macos: - runs-on: macos-13 + runs-on: macos-14 strategy: fail-fast: false matrix: @@ -49,12 +58,21 @@ jobs: - uses: actions/checkout@v4 - name: libdwarf run: | + cd .. + mkdir zstd + cd zstd + git init + git remote add origin https://github.com/facebook/zstd.git + git fetch --depth 1 origin 63779c798237346c2b245c546c40b72a5a5913fe # 1.5.5 + git checkout FETCH_HEAD + make -j + sudo make install cd .. mkdir libdwarf cd libdwarf git init - git remote add origin https://github.com/jeremy-rifkin/libdwarf-code.git - git fetch --depth 1 origin 6216e185863f41d6f19ab850caabfff7326020d7 + git remote add origin https://github.com/jeremy-rifkin/libdwarf-lite.git + git fetch --depth 1 origin 5c0cb251f94b27e90184e6b2d9a0c9c62593babc git checkout FETCH_HEAD mkdir build cd build @@ -68,6 +86,17 @@ jobs: - name: build and test run: | python3 ci/test-all-configs.py --${{matrix.compiler}} + # - name: bundle artifacts + # if: always() + # run: | + # tar czfH bundle.tar.gz build + # - name: upload artifacts + # uses: actions/upload-artifact@v4 + # if: always() + # with: + # name: build-macos-${{matrix.compiler}}${{matrix.shared}} + # path: bundle.tar.gz + # retention-days: 2 test-windows: runs-on: windows-2022 strategy: @@ -84,24 +113,34 @@ jobs: pip3 install colorama - name: libdwarf run: | - cd .. - mkdir libdwarf - cd libdwarf - git init - git remote add origin https://github.com/jeremy-rifkin/libdwarf-code.git - git fetch --depth 1 origin 6216e185863f41d6f19ab850caabfff7326020d7 - git checkout FETCH_HEAD - mkdir build - cd build if("${{matrix.compiler}}" -eq "gcc") { + cd .. + mkdir zstd + cd zstd + git init + git remote add origin https://github.com/facebook/zstd.git + git fetch --depth 1 origin 63779c798237346c2b245c546c40b72a5a5913fe # 1.5.5 + git checkout FETCH_HEAD + cd build/cmake + mkdir build + cd build + cmake .. -DZSTD_BUILD_SHARED=On -DZSTD_BUILD_SHARED=Off -DZSTD_LEGACY_SUPPORT=Off -DZSTD_BUILD_PROGRAMS=Off -DZSTD_BUILD_CONTRIB=Off -DZSTD_BUILD_TESTS=Off -G"Unix Makefiles" + make -j + make install + cd ../../../.. + mkdir libdwarf + cd libdwarf + git init + git remote add origin https://github.com/jeremy-rifkin/libdwarf-lite.git + git fetch --depth 1 origin 5c0cb251f94b27e90184e6b2d9a0c9c62593babc + git checkout FETCH_HEAD + mkdir build + cd build cmake .. -DPIC_ALWAYS=TRUE -DBUILD_DWARFDUMP=FALSE -G"Unix Makefiles" make -j make install - } else { - cmake .. -DPIC_ALWAYS=TRUE -DBUILD_DWARFDUMP=FALSE - msbuild INSTALL.vcxproj + cd ../../cpptrace } - cd ../../cpptrace - name: build and test run: | python3 ci/test-all-configs.py --${{matrix.compiler}} diff --git a/.gitignore b/.gitignore index 252257b..fbd07ba 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build*/ repro*/ __pycache__ scratch +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt index b70e02e..aa4a1e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,6 +189,7 @@ add_library(cpptrace::cpptrace ALIAS ${target_name}) target_sources( ${target_name} PRIVATE include/cpptrace/cpptrace.hpp + include/ctrace/ctrace.h ) # add /src files to target @@ -196,6 +197,7 @@ target_sources( ${target_name} PRIVATE # src src/cpptrace.cpp + src/ctrace.cpp src/demangle/demangle_with_cxxabi.cpp src/demangle/demangle_with_winapi.cpp src/demangle/demangle_with_nothing.cpp @@ -229,20 +231,21 @@ target_compile_options( $<$:/W4 /WX /permissive-> ) -# ---- Generate Build Info Headers ---- - -# used in export header generated below -if(NOT CPPTRACE_BUILD_SHARED) - target_compile_definitions(${target_name} PUBLIC CPPTRACE_STATIC_DEFINE) +if(CPPTRACE_WERROR_BUILD) + target_compile_options( + ${target_name} + PRIVATE + $<$>:-Werror> + $<$:/WX> + ) endif() -# generate header file with export macros prefixed with BASE_NAME -include(GenerateExportHeader) -generate_export_header( - ${target_name} - BASE_NAME cpptrace - EXPORT_FILE_NAME include/cpptrace/cpptrace_export.hpp -) +# ---- Generate Build Info Headers ---- + +if(build_type STREQUAL "STATIC") + target_compile_definitions(${target_name} PUBLIC CPPTRACE_STATIC_DEFINE) + set(CPPTRACE_STATIC_DEFINE TRUE) +endif() # ---- Library Properties ---- @@ -335,29 +338,111 @@ endif() if(CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF) target_compile_definitions(${target_name} PUBLIC CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF) + # First, dependencies: Zstd and zlib (currently relying on system zlib) + if(CPPTRACE_USE_EXTERNAL_ZSTD) + find_package(zstd) + else() + include(FetchContent) + cmake_policy(SET CMP0074 NEW) + FetchContent_Declare( + zstd + GIT_REPOSITORY https://github.com/facebook/zstd.git + GIT_TAG 63779c798237346c2b245c546c40b72a5a5913fe # v1.5.5 + GIT_SHALLOW 1 + SOURCE_SUBDIR build/cmake + ) + # FetchContent_MakeAvailable(zstd) + FetchContent_GetProperties(zstd) + if(NOT zstd_POPULATED) + FetchContent_Populate(zstd) + set(ZSTD_BUILD_PROGRAMS OFF) + set(ZSTD_BUILD_CONTRIB OFF) + set(ZSTD_BUILD_TESTS OFF) + set(ZSTD_BUILD_STATIC ON) + set(ZSTD_BUILD_SHARED OFF) + set(ZSTD_LEGACY_SUPPORT OFF) + add_subdirectory("${zstd_SOURCE_DIR}/build/cmake" "${zstd_BINARY_DIR}") + endif() + endif() + # Libdwarf itself if(CPPTRACE_USE_EXTERNAL_LIBDWARF) find_package(libdwarf REQUIRED) - target_compile_definitions(${target_name} PRIVATE CPPTRACE_USE_EXTERNAL_LIBDWARF) else() set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) - set(PIC_ALWAYS TRUE) - set(BUILD_DWARFDUMP FALSE) + # set(PIC_ALWAYS TRUE) + # set(BUILD_DWARFDUMP FALSE) include(FetchContent) FetchContent_Declare( libdwarf - # GIT_REPOSITORY https://github.com/jeremy-rifkin/libdwarf-code.git + # GIT_REPOSITORY https://github.com/davea42/libdwarf-code.git # GIT_TAG 6216e185863f41d6f19ab850caabfff7326020d7 # v0.8.0 + # GIT_TAG 8b0bd09d8c77d45a68cb1bb00a54186a92b683d9 # v0.9.0 + # GIT_TAG 8cdcc531f310d1c5ae61da469d8056bdd36b77e7 # v0.9.1 + some cmake changes # Using a lightweight mirror that's optimized for clone + configure speed GIT_REPOSITORY https://github.com/jeremy-rifkin/libdwarf-lite.git - GIT_TAG c78e984f3abbd20f6e01d6f51819e826b1691f65 + # GIT_TAG c78e984f3abbd20f6e01d6f51819e826b1691f65 # v0.8.0 + # GIT_TAG 71090c680b4c943448ba87a0f1f864f174e4edda # v0.9.0 + GIT_TAG 5c0cb251f94b27e90184e6b2d9a0c9c62593babc # v0.9.1 + some cmake changes + # GIT_REPOSITORY https://github.com/jeremy-rifkin/libdwarf-code.git + # GIT_TAG 308b55331b564d4fdbe3bc6856712270e5b2395b GIT_SHALLOW 1 ) - FetchContent_MakeAvailable(libdwarf) + # FetchContent_MakeAvailable(libdwarf) + FetchContent_GetProperties(libdwarf) + if(NOT libdwarf_POPULATED) + set(PIC_ALWAYS TRUE) + set(BUILD_DWARFDUMP FALSE) + # set(ENABLE_DECOMPRESSION FALSE) + FetchContent_Populate(libdwarf) + add_subdirectory("${libdwarf_SOURCE_DIR}" "${libdwarf_BINARY_DIR}") + target_include_directories( + dwarf + PRIVATE + ${zstd_SOURCE_DIR}/lib + ) + endif() endif() if(CPPTRACE_CONAN) target_link_libraries(${target_name} PRIVATE libdwarf::libdwarf) + target_compile_definitions(${target_name} PRIVATE CPPTRACE_USE_NESTED_LIBDWARF_HEADER_PATH) elseif(CPPTRACE_VCPKG) - target_link_libraries(${target_name} PRIVATE $,libdwarf::dwarf-static,libdwarf::dwarf-shared>) + target_link_libraries(${target_name} PRIVATE libdwarf::dwarf) + elseif(CPPTRACE_USE_EXTERNAL_LIBDWARF) + if(DEFINED LIBDWARF_LIBRARIES) + target_link_libraries(${target_name} PRIVATE ${LIBDWARF_LIBRARIES}) + else() + # if LIBDWARF_LIBRARIES wasn't set by find_package, try looking for libdwarf::dwarf-static, + # libdwarf::dwarf-shared, libdwarf::dwarf, then libdwarf + # libdwarf v0.8.0 installs with the target libdwarf::dwarf somehow, despite creating libdwarf::dwarf-static or + # libdwarf::dwarf-shared under fetchcontent + if(TARGET libdwarf::dwarf-static) + set(LIBDWARF_LIBRARIES libdwarf::dwarf-static) + elseif(TARGET libdwarf::dwarf-shared) + set(LIBDWARF_LIBRARIES libdwarf::dwarf-shared) + elseif(TARGET libdwarf::dwarf) + set(LIBDWARF_LIBRARIES libdwarf::dwarf) + elseif(TARGET libdwarf) + set(LIBDWARF_LIBRARIES libdwarf) + else() + message(FATAL_ERROR "Couldn't find libdwarf target name to link against") + endif() + target_link_libraries(${target_name} PRIVATE ${LIBDWARF_LIBRARIES}) + endif() + # There seems to be no consistency at all about where libdwarf decides to place its headers........ Figure out if + # it's libdwarf/libdwarf.h and libdwarf/dwarf.h or just libdwarf.h and dwarf.h + include(CheckIncludeFileCXX) + # libdwarf's cmake doesn't properly set variables to indicate where its libraries live + get_target_property(LIBDWARF_INTERFACE_INCLUDE_DIRECTORIES ${LIBDWARF_LIBRARIES} INTERFACE_INCLUDE_DIRECTORIES) + set(CMAKE_REQUIRED_INCLUDES ${LIBDWARF_INTERFACE_INCLUDE_DIRECTORIES}) + CHECK_INCLUDE_FILE_CXX("libdwarf/libdwarf.h" LIBDWARF_IS_NESTED) + CHECK_INCLUDE_FILE_CXX("libdwarf.h" LIBDWARF_IS_NOT_NESTED) + # check_include_file("libdwarf/libdwarf.h" LIBDWARF_IS_NESTED) + # check_support(LIBDWARF_IS_NESTED nested_libdwarf_include.cpp "" "" "") + if(${LIBDWARF_IS_NESTED}) + target_compile_definitions(${target_name} PRIVATE CPPTRACE_USE_NESTED_LIBDWARF_HEADER_PATH) + elseif(NOT LIBDWARF_IS_NOT_NESTED) + message(FATAL_ERROR "Couldn't find libdwarf.h") + endif() else() target_link_libraries(${target_name} PRIVATE libdwarf::dwarf-static) endif() @@ -471,44 +556,28 @@ endif() # =============================================== Demo/test =============================================== +macro(add_test_dependencies exec_name) + target_compile_features(${exec_name} PRIVATE cxx_std_11) + target_link_libraries(${exec_name} PRIVATE ${target_name}) + # Clang has been fast to adopt dwarf 5, other tools (e.g. addr2line from binutils) have not + check_cxx_compiler_flag("-gdwarf-4" HAS_DWARF4) + if(HAS_DWARF4) + target_compile_options(${exec_name} PRIVATE "$<$:-gdwarf-4>") + endif() + # TODO: add debug info for mingw clang? + if(CPPTRACE_BUILD_TEST_RDYNAMIC) + set_property(TARGET ${exec_name} PROPERTY ENABLE_EXPORTS ON) + endif() +endmacro() + if(CPPTRACE_BUILD_TESTING) add_executable(test test/test.cpp) - target_compile_features(test PRIVATE cxx_std_11) - target_link_libraries(test PRIVATE ${target_name}) - # Clang has been fast to adopt dwarf 5, other tools (e.g. addr2line from binutils) have not - check_cxx_compiler_flag("-gdwarf-4" HAS_DWARF4) - if(HAS_DWARF4) - target_compile_options(test PRIVATE "$<$:-gdwarf-4>") - endif() - if(CPPTRACE_BUILD_TEST_RDYNAMIC) - set_property(TARGET test PROPERTY ENABLE_EXPORTS ON) - endif() - if(APPLE) # TODO: Temporary - add_custom_command( - TARGET test - POST_BUILD - COMMAND dsymutil $ - ) - endif() - add_executable(demo test/demo.cpp) - target_compile_features(demo PRIVATE cxx_std_11) - target_link_libraries(demo PRIVATE ${target_name}) - # Clang has been fast to adopt dwarf 5, other tools (e.g. addr2line from binutils) have not - check_cxx_compiler_flag("-gdwarf-4" HAS_DWARF4) - if(HAS_DWARF4) - target_compile_options(demo PRIVATE "$<$:-gdwarf-4>") - endif() - if(CPPTRACE_BUILD_TEST_RDYNAMIC) - set_property(TARGET demo PROPERTY ENABLE_EXPORTS ON) - endif() - if(APPLE) # TODO: Temporary - add_custom_command( - TARGET demo - POST_BUILD - COMMAND dsymutil $ - ) - endif() + add_executable(c_demo test/ctrace_demo.c) + + add_test_dependencies(test) + add_test_dependencies(demo) + add_test_dependencies(c_demo) if(UNIX) add_executable(signal_demo test/signal_demo.cpp) diff --git a/LICENSE b/LICENSE index 152a555..299bf1f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023 Jeremy Rifkin +Copyright (c) 2023-2024 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, diff --git a/README.md b/README.md index b080054..cb0b884 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Cpptrace is a simple, portable, and self-contained C++ stacktrace library supporting C++11 and greater on Linux, macOS, and Windows including MinGW and Cygwin environments. The goal: Make stack traces simple for once. +Cpptrace also has a C API, docs [here](docs/c-api.md). + ## Table of Contents - [30-Second Overview](#30-second-overview) @@ -23,6 +25,7 @@ and Windows including MinGW and Cygwin environments. The goal: Make stack traces - [Object Traces](#object-traces) - [Raw Traces](#raw-traces) - [Utilities](#utilities) + - [Configuration](#configuration) - [Traced Exceptions](#traced-exceptions) - [Wrapping std::exceptions](#wrapping-stdexceptions) - [Exception handling with cpptrace](#exception-handling-with-cpptrace) @@ -33,6 +36,8 @@ and Windows including MinGW and Cygwin environments. The goal: Make stack traces - [CMake FetchContent](#cmake-fetchcontent) - [System-Wide Installation](#system-wide-installation) - [Local User Installation](#local-user-installation) + - [Use Without CMake](#use-without-cmake) + - [Installation Without Package Managers or FetchContent](#installation-without-package-managers-or-fetchcontent) - [Package Managers](#package-managers) - [Conan](#conan) - [Vcpkg](#vcpkg) @@ -113,7 +118,7 @@ endif() Be sure to configure with `-DCMAKE_BUILD_TYPE=Debug` or `-DDCMAKE_BUILD_TYPE=RelWithDebInfo` for symbols and line information. -On macos a little extra work to generate a .dSYM file is required, see [Platform Logistics](#platform-logistics) below. +On macOS it is recommended to generate a .dSYM file, see [Platform Logistics](#platform-logistics) below. For other ways to use the library, such as through package managers, a system-wide installation, or on a platform without internet access see [Usage](#usage) below. @@ -126,7 +131,9 @@ Some day C++23's `` will be ubiquitous. And maybe one day the msvc i The original motivation for cpptrace was to support projects using older C++ standards and as the library has grown its functionality has extended beyond the standard library's implementation. -Cpptrace also provides additional functionality including being able to +Cpptrace also provides additional functionality including showing inlined function calls, allowing generation of +lightweight "raw traces" that can be resolved later, offering exception objects that embed a lightweight trace when +thrown, and providing an API for safe tracing from signal handlers. # In-Depth Documentation @@ -138,9 +145,6 @@ method to get lightweight raw traces, which are just vectors of program counters **Note:** Debug info (`-g`/`/Z7`/`/Zi`/`/DEBUG`) is generally required for good trace information. -**Note:** Currently on Mac .dSYM files are required, which can be generated with `dsymutil yourbinary`. A cmake snippet -for generating these is provided in [Platform Logistics](#platform-logistics) below. - All functions are thread-safe unless otherwise noted. ### Stack Traces @@ -218,7 +222,7 @@ namespace cpptrace { ### Raw Traces -Raw trace access: A vector of program counters. These are ideal for traces you want to resolve later. +Raw trace access: A vector of program counters. These are ideal for fast and cheap traces you want to resolve later. Note it is important executables and shared libraries in memory aren't somehow unmapped otherwise libdl calls (and `GetModuleFileName` in windows) will fail to figure out where the program counter corresponds to. @@ -246,14 +250,6 @@ namespace cpptrace { `cpptrace::demangle` provides a helper function for name demangling, since it has to implement that helper internally anyways. -The library makes an attempt to fail silently and continue during trace generation if any errors are encountered. -`cpptrace::absorb_trace_exceptions` can be used to configure whether these exceptions are absorbed silently internally -or wether they're rethrown to the caller. - -`cpptrace::experimental::set_cache_mode` can be used to control time-memory tradeoffs within the library. By default -speed is prioritized. If using this function, set the cache mode at the very start of your program before any traces are -performed. - `cpptrace::isatty` and the fileno definitions are useful for deciding whether to use color when printing stack taces. `cpptrace::register_terminate_handler()` is a helper function to set a custom `std::terminate` handler that prints a @@ -262,7 +258,6 @@ stack trace from a cpptrace exception (more info below) and otherwise behaves li ```cpp namespace cpptrace { std::string demangle(const std::string& name); - void absorb_trace_exceptions(bool absorb); bool isatty(int fd); extern const int stdin_fileno; @@ -270,6 +265,25 @@ namespace cpptrace { extern const int stdout_fileno; void register_terminate_handler(); +} +``` + +### Configuration + +`cpptrace::absorb_trace_exceptions`: Configure whether the library silently absorbs internal exceptions and continues. +Default is true. + +`cpptrace::ctrace_enable_inlined_call_resolution`: Configure whether the library will attempt to resolve inlined call +information for release builds. Default is true. + +`cpptrace::experimental::set_cache_mode`: Control time-memory tradeoffs within the library. By default speed is +prioritized. If using this function, set the cache mode at the very start of your program before any traces are +performed. + +```cpp +namespace cpptrace { + void absorb_trace_exceptions(bool absorb); + void ctrace_enable_inlined_call_resolution(bool enable); enum class cache_mode { // Only minimal lookup tables @@ -311,13 +325,13 @@ interface or type system but this seems to be the best way to do this. ```cpp namespace cpptrace { class lazy_exception : public exception { - mutable detail::lazy_trace_holder trace_holder; // basically std::variant, more docs later + // lazy_trace_holder is basically a std::variant, more docs later + mutable detail::lazy_trace_holder trace_holder; mutable std::string what_string; - protected: - explicit lazy_exception(std::size_t skip, std::size_t max_depth) noexcept; - explicit lazy_exception(std::size_t skip) noexcept; public: - explicit lazy_exception() noexcept : lazy_exception(1) {} + explicit lazy_exception( + raw_trace&& trace = detail::get_raw_trace_and_absorb() + ) noexcept : trace_holder(std::move(trace)) {} const char* what() const noexcept override; const char* message() const noexcept override; const stacktrace& trace() const noexcept override; @@ -332,19 +346,22 @@ well as a number of traced exception classes resembling ``: ```cpp namespace cpptrace { - class CPPTRACE_EXPORT exception_with_message : public lazy_exception { + class exception_with_message : public lazy_exception { mutable std::string user_message; - protected: - explicit exception_with_message(std::string&& message_arg, std::size_t skip) noexcept; - explicit exception_with_message(std::string&& message_arg, std::size_t skip, std::size_t max_depth) noexcept; public: - explicit exception_with_message(std::string&& message_arg) noexcept - : exception_with_message(std::move(message_arg), 1) {} - + 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; }; - // All stdexcept errors have analogs here. Same constructor as exception_with_message. + // All stdexcept errors have analogs here. All have the constructor: + // explicit the_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 logic_error : public exception_with_message { ... }; class domain_error : public exception_with_message { ... }; class invalid_argument : public exception_with_message { ... }; @@ -429,8 +446,8 @@ namespace cpptrace { } ``` -**Note:** Not all back-ends and platforms support these interfaces. If signal-safe unwinding isn't supported -`safe_generate_raw_trace` will just produce an empty trace and if object information can't be resolved in a signal-safe +**Note:** Not all back-ends and platforms support these interfaces. If signal-safe unwinding isn't supported, +`safe_generate_raw_trace` will just produce an empty trace, and if object information can't be resolved in a signal-safe way then `get_safe_object_frame` will not populate fields beyond the `raw_address`. **Another big note:** Calls to shared objects can be lazy-loaded where the first call to the shared object invokes @@ -504,7 +521,7 @@ namespace cpptrace { | DWARF in separate binary (binary gnu debug link) | ️️✔️ | | DWARF in separate binary (split dwarf) | ✔️ | | DWARF in dSYM | ✔️ | -| DWARF in via Mach-O debug map | Soon | +| DWARF in via Mach-O debug map | ✔️ | | Windows debug symbols in PDB | ✔️ | DWARF5 added DWARF package files. As far as I can tell no compiler implements these yet. @@ -609,6 +626,75 @@ Using manually: g++ main.cpp -o main -g -Wall -I$HOME/wherever/include -L$HOME/wherever/lib -lcpptrace ``` +## Use Without CMake + +To use the library without cmake first follow the installation instructions at +[System-Wide Installation](#system-wide-installation), [Local User Installation](#local-user-installation), +or [Package Managers](#package-managers). + +In addition to any include or library paths you'll need to specify to tell the compiler where cpptrace was installed the +typical dependencies for cpptrace are: + +| Compiler | Platform | Dependencies | +| ----------------------- | ---------------- | ---------------------------------- | +| gcc, clang, intel, etc. | Linux/macos/unix | `-lcpptrace -ldwarf -lz -ldl` | +| gcc | Windows | `-lcpptrace -ldbghelp -ldwarf -lz` | +| msvc | Windows | `cpptrace.lib dbghelp.lib` | +| clang | Windows | `-lcpptrace -ldbghelp` | + +Dependencies may differ if different back-ends are manually selected. + +## Installation Without Package Managers or FetchContent + +Some users may prefer, or need to, to install cpptrace without package managers or fetchcontent (e.g. if their system +does not have internet access). Below are instructions for how to install libdwarf and cpptrace. + +
+ Installation Without Package Managers or FetchContent + +Here is an example for how to build cpptrace and libdwarf. `~/scratch/cpptrace-test` is used as a working directory and +the libraries are installed to `~/scratch/cpptrace-test/resources`. + +```sh +mkdir -p ~/scratch/cpptrace-test/resources + +cd ~/scratch/cpptrace-test +git clone https://github.com/facebook/zstd.git +cd zstd +git checkout 63779c798237346c2b245c546c40b72a5a5913fe +cd build/cmake +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=~/scratch/cpptrace-test/resources -DZSTD_BUILD_PROGRAMS=On -DZSTD_BUILD_CONTRIB=On -DZSTD_BUILD_TESTS=On -DZSTD_BUILD_STATIC=On -DZSTD_BUILD_SHARED=On -DZSTD_LEGACY_SUPPORT=On +make -j +make install + +cd ~/scratch/cpptrace-test +git clone https://github.com/jeremy-rifkin/libdwarf-lite.git +cd libdwarf-lite +git checkout 5c0cb251f94b27e90184e6b2d9a0c9c62593babc +mkdir build +cd build +cmake .. -DPIC_ALWAYS=On -DBUILD_DWARFDUMP=Off -DCMAKE_PREFIX_PATH=~/scratch/cpptrace-test/resources -DCMAKE_INSTALL_PREFIX=~/scratch/cpptrace-test/resources +make -j +make install + +cd ~/scratch/cpptrace-test +git clone https://github.com/jeremy-rifkin/cpptrace.git +cd cpptrace +git checkout v0.3.1 +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=On -DCPPTRACE_USE_EXTERNAL_LIBDWARF=On -DCMAKE_PREFIX_PATH=~/scratch/cpptrace-test/resources -DCMAKE_INSTALL_PREFIX=~/scratch/cpptrace-test/resources +make -j +make install +``` + +The `~/scratch/cpptrace-test/resources` directory also serves as a bundle you can ship with all the installed files for +cpptrace and its dependencies. + +
+ ## Package Managers ### Conan @@ -659,7 +745,7 @@ if(WIN32) endif() ``` -Generating a .dSYM file on macos: +On macOS it's recommended to generate a dSYM file containing debug information for your program: In xcode cmake this can be done with @@ -812,7 +898,6 @@ and time-memory tradeoffs. If you find the current implementation is either slow to explore some of these options. A couple things I'd like to improve in the future: -- On MacOS .dSYM files are required - On Windows when collecting symbols with dbghelp (msvc/clang) parameter types are almost perfect but due to limitations in dbghelp the library cannot accurately show const and volatile qualifiers or rvalue references (these appear as pointers). diff --git a/ci/build-in-all-configs.py b/ci/build-in-all-configs.py index e5dc2e3..7754bd4 100644 --- a/ci/build-in-all-configs.py +++ b/ci/build-in-all-configs.py @@ -48,6 +48,8 @@ def build(matrix): f"-DCMAKE_CXX_COMPILER={matrix['compiler']}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", f"-D{matrix['unwind']}=On", f"-D{matrix['symbols']}=On", f"-D{matrix['demangle']}=On", @@ -63,6 +65,8 @@ def build(matrix): f"-DCMAKE_CXX_COMPILER={matrix['compiler']}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", f"-D{matrix['unwind']}=On", f"-D{matrix['symbols']}=On", f"-D{matrix['demangle']}=On", @@ -98,6 +102,8 @@ def build_full_or_auto(matrix): f"-DCMAKE_CXX_COMPILER={matrix['compiler']}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", f"-DCPPTRACE_BACKTRACE_PATH=/usr/lib/gcc/x86_64-linux-gnu/10/include/backtrace.h", ] if matrix["config"] != "": @@ -113,6 +119,8 @@ def build_full_or_auto(matrix): f"-DCMAKE_CXX_COMPILER={matrix['compiler']}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", ] if matrix["config"] != "": args.append(f"{matrix['config']}") diff --git a/ci/test-all-configs.py b/ci/test-all-configs.py index 1cfd0d5..a3ac065 100644 --- a/ci/test-all-configs.py +++ b/ci/test-all-configs.py @@ -30,7 +30,7 @@ def similarity(name: str, target: List[str]) -> int: return -1 return c -def output_matches(output: str, params: Tuple[str]): +def output_matches(raw_output: str, params: Tuple[str]): target = [] if params[0].startswith("gcc") or params[0].startswith("g++"): @@ -72,35 +72,45 @@ def output_matches(output: str, params: Tuple[str]): print(f"Reading from {file}") with open(os.path.join(expected_dir, file), "r") as f: - expected = f.read() + raw_expected = f.read() - if output.strip() == "": + if raw_output.strip() == "": print(f"Error: No output from test") return False - expected = [line.strip().split("||") for line in expected.split("\n")] - output = [line.strip().split("||") for line in output.split("\n")] + expected = [line.strip().split("||") for line in raw_expected.split("\n")] + output = [line.strip().split("||") for line in raw_output.split("\n")] max_line_diff = 0 errored = False - for i, ((output_file, output_line, output_symbol), (expected_file, expected_line, expected_symbol)) in enumerate(zip(output, expected)): - if output_file != expected_file: - print(f"Error: File name mismatch on line {i + 1}, found \"{output_file}\" expected \"{expected_file}\"") - errored = True - if abs(int(output_line) - int(expected_line)) > max_line_diff: - print(f"Error: File line mismatch on line {i + 1}, found {output_line} expected {expected_line}") - errored = True - if output_symbol != expected_symbol: - print(f"Error: File symbol mismatch on line {i + 1}, found \"{output_symbol}\" expected \"{expected_symbol}\"") - errored = True - if expected_symbol == "main" or expected_symbol == "main()": - break + try: + for i, ((output_file, output_line, output_symbol), (expected_file, expected_line, expected_symbol)) in enumerate(zip(output, expected)): + if output_file != expected_file: + print(f"Error: File name mismatch on line {i + 1}, found \"{output_file}\" expected \"{expected_file}\"") + errored = True + if abs(int(output_line) - int(expected_line)) > max_line_diff: + print(f"Error: File line mismatch on line {i + 1}, found {output_line} expected {expected_line}") + errored = True + if output_symbol != expected_symbol: + print(f"Error: File symbol mismatch on line {i + 1}, found \"{output_symbol}\" expected \"{expected_symbol}\"") + errored = True + if expected_symbol == "main" or expected_symbol == "main()": + break + except ValueError: + print("ValueError during output checking") + errored = True + + if errored: + print("Output:") + print(raw_output) + print("Expected:") + print(raw_expected) return not errored -def run_command(*args: List[str]): +def run_command(*args: List[str], always_output=False): global failed print(f"{Fore.CYAN}{Style.BRIGHT}Running Command \"{' '.join(args)}\"{Style.RESET_ALL}") p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -116,6 +126,11 @@ def run_command(*args: List[str]): return False else: print(f"{Fore.GREEN}{Style.BRIGHT}Command succeeded{Style.RESET_ALL}") + if always_output: + print("stdout:") + print(stdout.decode("utf-8"), end="") + print("stderr:") + print(stderr.decode("utf-8"), end="") return True def run_test(test_binary, params: Tuple[str]): @@ -126,7 +141,7 @@ def run_test(test_binary, params: Tuple[str]): print(Style.RESET_ALL, end="") # makefile in parallel sometimes messes up colors if test.returncode != 0: - print("[🔴 Test command failed]") + print(f"[🔴 Test command failed with code {test.returncode}]") print("stderr:") print(test_stderr.decode("utf-8"), end="") print("stdout:") @@ -155,6 +170,8 @@ def build(matrix): f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", f"-D{matrix['unwind']}=On", f"-D{matrix['symbols']}=On", f"-D{matrix['demangle']}=On", @@ -176,6 +193,8 @@ def build(matrix): f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", f"-D{matrix['unwind']}=On", f"-D{matrix['symbols']}=On", f"-D{matrix['demangle']}=On", @@ -202,6 +221,8 @@ def build_full_or_auto(matrix): f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", f"-DCPPTRACE_BACKTRACE_PATH=/usr/lib/gcc/x86_64-linux-gnu/10/include/backtrace.h", "-DCPPTRACE_BUILD_TESTING=On", f"-DBUILD_SHARED_LIBS={matrix['shared']}" @@ -220,6 +241,8 @@ def build_full_or_auto(matrix): f"-DCMAKE_C_COMPILER={get_c_compiler_counterpart(matrix['compiler'])}", f"-DCMAKE_CXX_STANDARD={matrix['std']}", f"-DCPPTRACE_USE_EXTERNAL_LIBDWARF=On", + f"-DCPPTRACE_USE_EXTERNAL_ZSTD=On", + f"-DCPPTRACE_WERROR_BUILD=On", "-DCPPTRACE_BUILD_TESTING=On", f"-DBUILD_SHARED_LIBS={matrix['shared']}" ] diff --git a/cmake/InstallRules.cmake b/cmake/InstallRules.cmake index 4fc7002..b35c4d7 100644 --- a/cmake/InstallRules.cmake +++ b/cmake/InstallRules.cmake @@ -5,7 +5,6 @@ include(CMakePackageConfigHelpers) install( DIRECTORY "${PROJECT_SOURCE_DIR}/include/" # our header files - "${PROJECT_BINARY_DIR}/include/" # generated header files DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" COMPONENT ${package_name}-development # PATTERN "**/third_party" EXCLUDE # skip third party directory diff --git a/cmake/OptionVariables.cmake b/cmake/OptionVariables.cmake index 377290d..022bfd8 100644 --- a/cmake/OptionVariables.cmake +++ b/cmake/OptionVariables.cmake @@ -14,6 +14,7 @@ # | CPPTRACE_INCLUDES_WITH_SYSTEM | Not Top-Level | ON | # | CPPTRACE_INSTALL_CMAKEDIR | Always | ${CMAKE_INSTALL_LIBDIR}/cmake/${package_name} | # | CPPTRACE_USE_EXTERNAL_LIBDWARF | Always | OFF | +# | CPPTRACE_USE_EXTERNAL_ZSTD | Always | OFF | # | ... | | | # --------------------------------------------------------------------------------------------------- @@ -158,15 +159,18 @@ if(PROJECT_IS_TOP_LEVEL) endif() option(CPPTRACE_USE_EXTERNAL_LIBDWARF "" OFF) +option(CPPTRACE_USE_EXTERNAL_ZSTD "" OFF) option(CPPTRACE_CONAN "" OFF) option(CPPTRACE_VCPKG "" OFF) option(CPPTRACE_SANITIZER_BUILD "" OFF) +option(CPPTRACE_WERROR_BUILD "" OFF) mark_as_advanced( CPPTRACE_BACKTRACE_PATH CPPTRACE_ADDR2LINE_PATH CPPTRACE_ADDR2LINE_SEARCH_SYSTEM_PATH CPPTRACE_SANITIZER_BUILD + CPPTRACE_WERROR_BUILD CPPTRACE_CONAN CPPTRACE_VCPKG ) diff --git a/cmake/in/cpptrace-config-cmake.in b/cmake/in/cpptrace-config-cmake.in index eea7703..412a9d0 100644 --- a/cmake/in/cpptrace-config-cmake.in +++ b/cmake/in/cpptrace-config-cmake.in @@ -4,13 +4,18 @@ # Dependencies if(@CPPTRACE_GET_SYMBOLS_WITH_LIBDWARF@) include(CMakeFindDependencyMacro) + find_dependency(zstd REQUIRED) find_dependency(libdwarf REQUIRED) endif() # We cannot modify an existing IMPORT target -if(NOT TARGET assert::assert) +if(NOT TARGET cpptrace::cpptrace) # import targets include("${CMAKE_CURRENT_LIST_DIR}/@package_name@-targets.cmake") endif() + +if(@CPPTRACE_STATIC_DEFINE@) + target_compile_definitions(cpptrace::cpptrace INTERFACE CPPTRACE_STATIC_DEFINE) +endif() diff --git a/docs/c-api.md b/docs/c-api.md new file mode 100644 index 0000000..b079f62 --- /dev/null +++ b/docs/c-api.md @@ -0,0 +1,167 @@ +# ctrace + +Cpptrace provides a C API under the name ctrace, documented below. + +## Table of Contents + +- [Documentation](#documentation) + - [Stack Traces](#stack-traces) + - [Object Traces](#object-traces) + - [Raw Traces](#raw-traces) + - [Utilities](#utilities) + - [Utility types](#utility-types) + - [Configuration](#configuration) + - [Signal-Safe Tracing](#signal-safe-tracing) + +## Documentation + +All ctrace declarations are in the `ctrace.h` header: + +```c +#include +``` + +### Stack Traces + +Generate stack traces with `ctrace_generate_trace()`. Often `skip = 0` and `max_depth = SIZE_MAX` is what you want for +the parameters. + +`ctrace_stacktrace_to_string` and `ctrace_print_stacktrace` can then be used for output. + +`ctrace_free_stacktrace` must be called when you are done with the trace. + +```c +typedef struct ctrace_stacktrace ctrace_stacktrace; + +struct ctrace_stacktrace_frame { + ctrace_frame_ptr address; + uint32_t line; + uint32_t column; + const char* filename; + const char* symbol; + ctrace_bool is_inline; +}; + +struct ctrace_stacktrace { + ctrace_stacktrace_frame* frames; + size_t count; +}; + +ctrace_stacktrace ctrace_generate_trace(size_t skip, size_t max_depth); +ctrace_owning_string ctrace_stacktrace_to_string(const ctrace_stacktrace* trace, ctrace_bool use_color); +void ctrace_print_stacktrace(const ctrace_stacktrace* trace, FILE* to, ctrace_bool use_color); +void ctrace_free_stacktrace(ctrace_stacktrace* trace); +``` + +### Object Traces + +Object traces contain the most basic information needed to construct a stack trace outside the currently running +executable. It contains the raw address, the address in the binary (ASLR and the object file's memory space and whatnot +is resolved), and the path to the object the instruction pointer is located in. + +`ctrace_free_object_trace` must be called when you are done with the trace. + +```c +typedef struct ctrace_object_trace ctrace_object_trace; + +struct ctrace_object_frame { + ctrace_frame_ptr raw_address; + ctrace_frame_ptr obj_address; + const char* obj_path; +}; + +struct ctrace_object_trace { + ctrace_object_frame* frames; + size_t count; +}; + +ctrace_object_trace ctrace_generate_object_trace(size_t skip, size_t max_depth); +ctrace_stacktrace ctrace_resolve_object_trace(const ctrace_object_trace* trace); +void ctrace_free_object_trace(ctrace_object_trace* trace); +``` + +### Raw Traces + +Raw traces are arrays of program counters. These are ideal for fast and cheap traces you want to resolve later. + +Note it is important executables and shared libraries in memory aren't somehow unmapped otherwise libdl calls (and +`GetModuleFileName` in windows) will fail to figure out where the program counter corresponds to. + +`ctrace_free_raw_trace` must be called when you are done with the trace. + +```c +typedef struct ctrace_raw_trace ctrace_raw_trace; + +ctrace_raw_trace ctrace_generate_raw_trace(size_t skip, size_t max_depth); +ctrace_stacktrace ctrace_resolve_raw_trace(const ctrace_raw_trace* trace); +void ctrace_free_raw_trace(ctrace_raw_trace* trace); +``` + +### Utilities + +`ctrace_demangle`: Helper function for name demangling + +`ctrace_stdin_fileno`, `ctrace_stderr_fileno`, `ctrace_stdout_fileno`: Returns the appropriate file descriptor for the +respective standard stream. + +`ctrace_isatty`: Checks if a file descriptor corresponds to a tty device. + +```c +ctrace_owning_string ctrace_demangle(const char* mangled); +int ctrace_stdin_fileno(void); +int ctrace_stderr_fileno(void); +int ctrace_stdout_fileno(void); +ctrace_bool ctrace_isatty(int fd); +``` + +### Utility types + +For ABI reasons `ctrace_bool`s are used for bools. `ctrace_owning_string` is a wrapper type which indicates that a +string is owned and must be freed. + +```c +typedef int8_t ctrace_bool; +typedef struct { + const char* data; +} ctrace_owning_string; +ctrace_owning_string ctrace_generate_owning_string(const char* raw_string); +void ctrace_free_owning_string(ctrace_owning_string* string); +``` + +### Configuration + +`experimental::set_cache_mode`: Control time-memory tradeoffs within the library. By default speed is prioritized. If +using this function, set the cache mode at the very start of your program before any traces are performed. Note: This +API is not set in stone yet and could change in the future. + +`ctrace_enable_inlined_call_resolution`: Configure whether the library will attempt to resolve inlined call information for +release builds. Default is true. + +```c + 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; + void ctrace_set_cache_mode(ctrace_cache_mode mode); + void ctrace_enable_inlined_call_resolution(ctrace_bool enable); +``` + +### Signal-Safe Tracing + +For more details on the signal-safe tracing interface please refer to the README and the +[signal-safe-tracing.md](signal-safe-tracing.md) guide. + +```c +typedef struct ctrace_safe_object_frame ctrace_safe_object_frame; +struct ctrace_safe_object_frame { + ctrace_frame_ptr raw_address; + ctrace_frame_ptr relative_obj_address; + char object_path[CTRACE_PATH_MAX + 1]; +}; +size_t ctrace_safe_generate_raw_trace(ctrace_frame_ptr* buffer, size_t size, size_t skip, size_t max_depth); +void ctrace_get_safe_object_frame(ctrace_frame_ptr address, ctrace_safe_object_frame* out); +``` diff --git a/signal-safe-tracing.md b/docs/signal-safe-tracing.md similarity index 100% rename from signal-safe-tracing.md rename to docs/signal-safe-tracing.md diff --git a/include/cpptrace/cpptrace.hpp b/include/cpptrace/cpptrace.hpp index 156e348..2a03162 100644 --- a/include/cpptrace/cpptrace.hpp +++ b/include/cpptrace/cpptrace.hpp @@ -10,7 +10,28 @@ #include #include -#include +#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 >= 202002L #ifdef __has_include @@ -213,7 +234,6 @@ namespace cpptrace { // utilities: CPPTRACE_EXPORT std::string demangle(const std::string& name); - CPPTRACE_EXPORT void absorb_trace_exceptions(bool absorb); CPPTRACE_EXPORT bool isatty(int fd); CPPTRACE_EXPORT extern const int stdin_fileno; @@ -222,19 +242,24 @@ namespace cpptrace { CPPTRACE_EXPORT void register_terminate_handler(); + // configuration: + CPPTRACE_EXPORT void absorb_trace_exceptions(bool absorb); + CPPTRACE_EXPORT void enable_inlined_call_resolution(bool enable); + enum class cache_mode { // Only minimal lookup tables - prioritize_memory, + prioritize_memory = 0, // Build lookup tables but don't keep them around between trace calls - hybrid, + hybrid = 1, // Build lookup tables as needed - prioritize_speed + prioritize_speed = 2 }; namespace experimental { CPPTRACE_EXPORT void set_cache_mode(cache_mode mode); } + // 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 { @@ -260,6 +285,9 @@ namespace cpptrace { private: void clear(); }; + + CPPTRACE_EXPORT raw_trace get_raw_trace_and_absorb(std::size_t skip, std::size_t max_depth) noexcept; + CPPTRACE_EXPORT raw_trace get_raw_trace_and_absorb(std::size_t skip = 0) noexcept; } // Interface for a traced exception object @@ -272,17 +300,14 @@ namespace cpptrace { // Cpptrace traced exception object // I hate to have to expose anything about implementation detail but the idea here is that - // TODO: CPPTRACE_FORCE_NO_INLINE annotations class CPPTRACE_EXPORT lazy_exception : public exception { mutable detail::lazy_trace_holder trace_holder; mutable std::string what_string; - protected: - explicit lazy_exception(std::size_t skip, std::size_t max_depth) noexcept; - explicit lazy_exception(std::size_t skip) noexcept : lazy_exception(skip + 1, SIZE_MAX) {} - public: - explicit lazy_exception() noexcept : lazy_exception(1) {} + explicit lazy_exception( + raw_trace&& trace = detail::get_raw_trace_and_absorb() + ) noexcept : trace_holder(std::move(trace)) {} // std::exception const char* what() const noexcept override; // cpptrace::exception @@ -293,87 +318,105 @@ namespace cpptrace { class CPPTRACE_EXPORT exception_with_message : public lazy_exception { mutable std::string user_message; - protected: - explicit exception_with_message( - std::string&& message_arg, - std::size_t skip - ) noexcept : lazy_exception(skip + 1), user_message(std::move(message_arg)) {} - - explicit exception_with_message( - std::string&& message_arg, - std::size_t skip, - std::size_t max_depth - ) noexcept : lazy_exception(skip + 1, max_depth), user_message(std::move(message_arg)) {} - public: - explicit exception_with_message(std::string&& message_arg) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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) noexcept - : exception_with_message(std::move(message_arg), 1) {} + 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(std::exception_ptr exception_ptr) noexcept - : lazy_exception(1), ptr(exception_ptr) {} - explicit nested_exception(std::exception_ptr exception_ptr, std::size_t skip) noexcept - : lazy_exception(skip + 1), ptr(exception_ptr) {} + explicit nested_exception( + 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; diff --git a/include/ctrace/ctrace.h b/include/ctrace/ctrace.h new file mode 100644 index 0000000..e9cdffc --- /dev/null +++ b/include/ctrace/ctrace.h @@ -0,0 +1,159 @@ +#ifndef CTRACE_H +#define CTRACE_H + +#include +#include +#include + +#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 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); + + /* 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); + + /* 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 diff --git a/src/binary/mach-o.hpp b/src/binary/mach-o.hpp index 6821e3a..b0260a3 100644 --- a/src/binary/mach-o.hpp +++ b/src/binary/mach-o.hpp @@ -12,7 +12,13 @@ #include #include +#include #include +#include +#include + +#include +#include #include #include @@ -20,10 +26,11 @@ #include #include #include +#include namespace cpptrace { namespace detail { - static bool is_mach_o(std::uint32_t magic) { + inline bool is_mach_o(std::uint32_t magic) { switch(magic) { case FAT_MAGIC: case FAT_CIGAM: @@ -37,170 +44,514 @@ namespace detail { } } - static bool is_fat_magic(std::uint32_t magic) { + inline bool file_is_mach_o(const std::string& object_path) noexcept { + try { + FILE* file = std::fopen(object_path.c_str(), "rb"); + if(file == nullptr) { + return false; + } + auto magic = load_bytes(file, 0); + return is_mach_o(magic); + } catch(...) { + return false; + } + } + + inline 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/ - static bool is_magic_64(std::uint32_t magic) { + inline bool is_magic_64(std::uint32_t magic) { return magic == MH_MAGIC_64 || magic == MH_CIGAM_64; } - static bool should_swap_bytes(std::uint32_t magic) { + inline bool should_swap_bytes(std::uint32_t magic) { return magic == MH_CIGAM || magic == MH_CIGAM_64 || magic == FAT_CIGAM; } - static void swap_mach_header(mach_header_64& header) { + inline void swap_mach_header(mach_header_64& header) { swap_mach_header_64(&header, NX_UnknownByteOrder); } - static void swap_mach_header(mach_header& header) { + inline void swap_mach_header(mach_header& header) { swap_mach_header(&header, NX_UnknownByteOrder); } - static void swap_segment_command(segment_command_64& segment) { + inline void swap_segment_command(segment_command_64& segment) { swap_segment_command_64(&segment, NX_UnknownByteOrder); } - static void swap_segment_command(segment_command& segment) { + inline void swap_segment_command(segment_command& segment) { swap_segment_command(&segment, NX_UnknownByteOrder); } + inline void swap_nlist(struct nlist& entry) { + swap_nlist(&entry, 1, NX_UnknownByteOrder); + } + + inline 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 - template - static optional macho_get_text_vmaddr_mach( - std::FILE* object_file, - const std::string& object_path, - off_t offset, - bool should_swap, - bool allow_arch_mismatch - ) { - static_assert(Bits == 32 || Bits == 64, "Unexpected Bits argument"); - using Mach_Header = typename std::conditional::type; - using Segment_Command = typename std::conditional::type; - std::uint32_t ncmds; - off_t load_commands_offset = offset; - std::size_t header_size = sizeof(Mach_Header); - Mach_Header header = load_bytes(object_file, offset); - if(should_swap) { - swap_mach_header(header); - } - thread_local static struct LP(mach_header)* mhp = _NSGetMachExecuteHeader(); - //std::fprintf( - // stderr, - // "----> %d %d; %d %d\n", - // header.cputype, - // mhp->cputype, - // static_cast(mhp->cpusubtype & ~CPU_SUBTYPE_MASK), - // header.cpusubtype - //); - if( - header.cputype != mhp->cputype || - static_cast(mhp->cpusubtype & ~CPU_SUBTYPE_MASK) != header.cpusubtype - ) { - if(allow_arch_mismatch) { - return nullopt; - } else { - PANIC("Mach-O file cpu type and subtype do not match current machine " + object_path); - } - } - ncmds = header.ncmds; - load_commands_offset += header_size; - // iterate load commands - off_t actual_offset = load_commands_offset; - for(std::uint32_t i = 0; i < ncmds; i++) { - load_command cmd = load_bytes(object_file, actual_offset); - if(should_swap) { - swap_load_command(&cmd, NX_UnknownByteOrder); - } - // TODO: This is a mistake? Need to check cmd.cmd == LC_SEGMENT_64 / cmd.cmd == LC_SEGMENT - Segment_Command segment = load_bytes(object_file, actual_offset); - if(should_swap) { - swap_segment_command(segment); - } - if(std::strcmp(segment.segname, "__TEXT") == 0) { - return segment.vmaddr; - } - actual_offset += cmd.cmdsize; - } - // somehow no __TEXT section was found... - PANIC("Couldn't find __TEXT section while parsing Mach-O object"); - return 0; - } + struct load_command_entry { + std::uint32_t file_offset; + std::uint32_t cmd; + std::uint32_t cmdsize; + }; - static std::uintptr_t macho_get_text_vmaddr_fat( - std::FILE* object_file, - const std::string& object_path, - bool should_swap - ) { - std::size_t header_size = sizeof(fat_header); - std::size_t arch_size = sizeof(fat_arch); - fat_header header = load_bytes(object_file, 0); - if(should_swap) { - swap_fat_header(&header, NX_UnknownByteOrder); - } - off_t arch_offset = (off_t)header_size; - optional text_vmaddr; - for(std::uint32_t i = 0; i < header.nfat_arch; i++) { - fat_arch arch = load_bytes(object_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(object_file, mach_header_offset); - if(is_magic_64(magic)) { - text_vmaddr = macho_get_text_vmaddr_mach<64>( - object_file, - object_path, - mach_header_offset, - should_swap_bytes(magic), - true - ); - } else { - text_vmaddr = macho_get_text_vmaddr_mach<32>( - object_file, - object_path, - mach_header_offset, - should_swap_bytes(magic), - true - ); - } - if(text_vmaddr.has_value()) { - return text_vmaddr.unwrap(); - } - } - // If this is reached... something went wrong. The cpu we're on wasn't found. - PANIC("Couldn't find appropriate architecture in fat Mach-O"); - return 0; - } + class mach_o { + std::FILE* file = nullptr; + std::string object_path; + 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 - static std::uintptr_t macho_get_text_vmaddr(const std::string& object_path) { - //std::fprintf(stderr, "--%s--\n", object_path.c_str()); - auto file = raii_wrap(std::fopen(object_path.c_str(), "rb"), file_deleter); - if(file == nullptr) { - throw file_error("Unable to read object file " + object_path); - } - std::uint32_t magic = load_bytes(file, 0); - VERIFY(is_mach_o(magic), "File is not Mach-O " + object_path); - bool is_64 = is_magic_64(magic); - bool should_swap = should_swap_bytes(magic); - if(magic == FAT_MAGIC || magic == FAT_CIGAM) { - return macho_get_text_vmaddr_fat(file, object_path, should_swap); - } else { - if(is_64) { - return macho_get_text_vmaddr_mach<64>(file, object_path, 0, should_swap, false).unwrap(); + std::size_t load_base = 0; + std::size_t fat_index = std::numeric_limits::max(); + + std::vector load_commands; + + struct symtab_info_data { + symtab_command symtab; + std::unique_ptr stringtab; + const char* get_string(std::size_t index) const { + if(stringtab && index < symtab.strsize) { + return stringtab.get() + index; + } else { + throw std::runtime_error("can't retrieve symbol from symtab"); + } + } + }; + + bool tried_to_load_symtab = false; + optional symtab_info; + + public: + mach_o(const std::string& object_path) : object_path(object_path) { + file = std::fopen(object_path.c_str(), "rb"); + if(file == nullptr) { + throw file_error("Unable to read object file " + object_path); + } + magic = load_bytes(file, 0); + VERIFY(is_mach_o(magic), "File is not Mach-O " + object_path); + if(magic == FAT_MAGIC || magic == FAT_CIGAM) { + load_fat_mach(); } else { - return macho_get_text_vmaddr_mach<32>(file, object_path, 0, should_swap, false).unwrap(); + fat_index = 0; + if(is_magic_64(magic)) { + load_mach<64>(); + } else { + load_mach<32>(); + } } } - } + + ~mach_o() { + if(file) { + std::fclose(file); + } + } + + std::uintptr_t 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(std::strcmp(segment.segname, "__TEXT") == 0) { + return segment.vmaddr; + } + } + } + // somehow no __TEXT section was found... + throw std::runtime_error("Couldn't find __TEXT section while parsing Mach-O object"); + } + + std::size_t get_fat_index() const { + VERIFY(fat_index != std::numeric_limits::max()); + return fat_index; + } + + void print_segments() const { + int i = 0; + 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); + fprintf(stderr, "Load command %d\n", i); + 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++; + } + } + + optional& 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; + info.symtab = load_symbol_table_command(command.file_offset); + info.stringtab = load_string_table(info.symtab.stroff, info.symtab.strsize); + symtab_info = std::move(info); + break; + } + } + } + return symtab_info; + } + + void print_symbol_table_entry( + const nlist_64& entry, + const std::unique_ptr& 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.get() + entry.n_un.n_strx + : "String index out of bounds" + ); + } + + void print_symbol_table() { + int i = 0; + for(const auto& command : load_commands) { + if(command.cmd == LC_SYMTAB) { + auto symtab = load_symbol_table_command(command.file_offset); + fprintf(stderr, "Load command %d\n", i); + 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); + for(std::size_t j = 0; j < symtab.nsyms; j++) { + nlist_64 entry = bits == 32 + ? load_symtab_entry<32>(symtab.symoff, j) + : load_symtab_entry<64>(symtab.symoff, j); + print_symbol_table_entry(entry, stringtab, symtab.strsize, j); + } + } + i++; + } + } + + 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>; + + // produce information similar to dsymutil -dump-debug-map + debug_map 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; + const auto& symtab_info = get_symtab_info().unwrap(); + const auto& symtab = symtab_info.symtab; + // TODO: Take timestamp into account? + std::string current_module; + optional current_function; + for(std::size_t j = 0; j < symtab.nsyms; j++) { + nlist_64 entry = bits == 32 + ? load_symtab_entry<32>(symtab.symoff, j) + : load_symtab_entry<64>(symtab.symoff, j); + // 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 + current_module = symtab_info.get_string(entry.n_un.n_strx); + break; + case N_BNSYM: break; // pass + case N_ENSYM: break; // pass + case N_FUN: + { + const char* str = symtab_info.get_string(entry.n_un.n_strx); + if(str[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; + } + } + break; + } + } + return debug_map; + } + + std::vector 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 + std::vector symbols; + const auto& symtab_info = get_symtab_info().unwrap(); + const auto& symtab = symtab_info.symtab; + // TODO: Take timestamp into account? + for(std::size_t j = 0; j < symtab.nsyms; j++) { + nlist_64 entry = bits == 32 + ? load_symtab_entry<32>(symtab.symoff, j) + : load_symtab_entry<64>(symtab.symoff, j); + if(entry.n_type & N_STAB) { + continue; + } + if((entry.n_type & N_TYPE) == N_SECT) { + symbols.push_back({ + entry.n_value, + symtab_info.get_string(entry.n_un.n_strx) + }); + } + } + return symbols; + } + + // produce information similar to dsymutil -dump-debug-map + static void print_debug_map(const debug_map& debug_map) { + for(const auto& entry : debug_map) { + std::cout< + void load_mach() { + static_assert(Bits == 32 || Bits == 64, "Unexpected Bits argument"); + bits = Bits; + using Mach_Header = typename std::conditional::type; + std::size_t header_size = sizeof(Mach_Header); + Mach_Header header = load_bytes(file, load_base); + 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++) { + load_command cmd = load_bytes(file, actual_offset); + if(should_swap()) { + swap_load_command(&cmd, NX_UnknownByteOrder); + } + load_commands.push_back({ actual_offset, cmd.cmd, cmd.cmdsize }); + actual_offset += cmd.cmdsize; + } + } + + void load_fat_mach() { + std::size_t header_size = sizeof(fat_header); + std::size_t arch_size = sizeof(fat_arch); + fat_header header = load_bytes(file, 0); + 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(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(file, mach_header_offset); + // std::cerr<<"xxx: "<cputype<(mhp->cpusubtype & ~CPU_SUBTYPE_MASK)<cputype && + // static_cast(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_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++) { + fat_arch arch = load_bytes(file, arch_offset); + 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; + std::uint32_t magic = load_bytes(file, mach_header_offset); + load_base = mach_header_offset; + fat_index = best - fat_arches.data(); + if(is_magic_64(magic)) { + load_mach<64>(); + } else { + load_mach<32>(); + } + return; + } + // If this is reached... something went wrong. The cpu we're on wasn't found. + throw std::runtime_error("Couldn't find appropriate architecture in fat Mach-O"); + } + + template + segment_command_64 load_segment_command(std::uint32_t offset) const { + using Segment_Command = typename std::conditional::type; + Segment_Command segment = load_bytes(file, offset); + 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; + } + + symtab_command load_symbol_table_command(std::uint32_t offset) const { + symtab_command symtab = load_bytes(file, offset); + ASSERT(symtab.cmd == LC_SYMTAB); + if(should_swap()) { + swap_symtab_command(&symtab, NX_UnknownByteOrder); + } + return symtab; + } + + template + nlist_64 load_symtab_entry(std::uint32_t symbol_base, std::size_t index) const { + using Nlist = typename std::conditional::type; + uint32_t offset = load_base + symbol_base + index * sizeof(Nlist); + Nlist entry = load_bytes(file, offset); + 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; + } + + std::unique_ptr load_string_table(std::uint32_t offset, std::uint32_t byte_count) const { + std::unique_ptr buffer(new char[byte_count + 1]); + VERIFY(std::fseek(file, load_base + offset, SEEK_SET) == 0, "fseek error"); + VERIFY(std::fread(buffer.get(), sizeof(char), byte_count, file) == byte_count, "fread error"); + buffer[byte_count] = 0; // just out of an abundance of caution + return buffer; + } + + bool should_swap() const { + return should_swap_bytes(magic); + } + }; inline bool macho_is_fat(const std::string& object_path) { auto file = raii_wrap(std::fopen(object_path.c_str(), "rb"), file_deleter); @@ -210,41 +561,6 @@ namespace detail { std::uint32_t magic = load_bytes(file, 0); return is_fat_magic(magic); } - - // returns index of the appropriate mach-o binary in the universal binary - // TODO: Code duplication with macho_get_text_vmaddr_fat - inline unsigned get_fat_macho_index(const std::string& object_path) { - auto file = raii_wrap(std::fopen(object_path.c_str(), "rb"), file_deleter); - if(file == nullptr) { - throw file_error("Unable to read object file " + object_path); - } - std::uint32_t magic = load_bytes(file, 0); - VERIFY(is_fat_magic(magic)); - bool should_swap = should_swap_bytes(magic); - std::size_t header_size = sizeof(fat_header); - std::size_t arch_size = sizeof(fat_arch); - fat_header header = load_bytes(file, 0); - if(should_swap) { - swap_fat_header(&header, NX_UnknownByteOrder); - } - off_t arch_offset = (off_t)header_size; - thread_local static struct LP(mach_header)* mhp = _NSGetMachExecuteHeader(); - for(std::uint32_t i = 0; i < header.nfat_arch; i++) { - fat_arch arch = load_bytes(file, arch_offset); - if(should_swap) { - swap_fat_arch(&arch, 1, NX_UnknownByteOrder); - } - arch_offset += arch_size; - if( - arch.cputype == mhp->cputype && - static_cast(mhp->cpusubtype & ~CPU_SUBTYPE_MASK) == arch.cpusubtype - ) { - return i; - } - } - // If this is reached... something went wrong. The cpu we're on wasn't found. - PANIC("Couldn't find appropriate architecture in fat Mach-O"); - } } } diff --git a/src/binary/object.hpp b/src/binary/object.hpp index 1af4f21..efb3ae0 100644 --- a/src/binary/object.hpp +++ b/src/binary/object.hpp @@ -56,7 +56,7 @@ namespace detail { 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 = macho_get_text_vmaddr(object_path); + auto base = mach_o(object_path).get_text_vmaddr(); cache.insert(it, {object_path, base}); return base; } else { diff --git a/src/cpptrace.cpp b/src/cpptrace.cpp index 564b660..d69a2c2 100644 --- a/src/cpptrace.cpp +++ b/src/cpptrace.cpp @@ -58,7 +58,7 @@ namespace cpptrace { for(auto& frame : trace) { frame.symbol = detail::demangle(frame.symbol); } - return stacktrace{std::move(trace)}; + return {std::move(trace)}; } catch(...) { // NOSONAR if(!detail::should_absorb_trace_exceptions()) { throw; @@ -91,7 +91,7 @@ namespace cpptrace { for(auto& frame : trace) { frame.symbol = detail::demangle(frame.symbol); } - return stacktrace{std::move(trace)}; + return {std::move(trace)}; } catch(...) { // NOSONAR if(!detail::should_absorb_trace_exceptions()) { throw; @@ -167,7 +167,11 @@ namespace cpptrace { } void stacktrace::print(std::ostream& stream, bool color, bool newline_at_end, const char* header) const { - if(color) { + if( + color && ( + (&stream == &std::cout && isatty(stdout_fileno)) || (&stream == &std::cerr && isatty(stderr_fileno)) + ) + ) { detail::enable_virtual_terminal_processing_if_needed(); } stream<<(header ? header : "Stack trace (most recent call first):")< cache_mode(cache_mode::prioritize_speed); // NOSONAR } - void absorb_trace_exceptions(bool absorb) { + 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) { + void set_cache_mode(cache_mode mode) { detail::cache_mode = mode; } } namespace detail { - bool should_absorb_trace_exceptions() { + bool should_absorb_trace_exceptions() { return absorb_trace_exceptions; } - enum cache_mode get_cache_mode() { + bool should_resolve_inlined_calls() { + return resolve_inlined_calls; + } + + enum cache_mode get_cache_mode() { return cache_mode; } @@ -450,6 +463,11 @@ namespace cpptrace { } } + CPPTRACE_FORCE_NO_INLINE + raw_trace get_raw_trace_and_absorb(std::size_t skip) noexcept { + return get_raw_trace_and_absorb(skip + 1, SIZE_MAX); + } + lazy_trace_holder::lazy_trace_holder(const lazy_trace_holder& other) : resolved(other.resolved) { if(other.resolved) { new (&resolved_trace) stacktrace(other.resolved_trace); @@ -530,9 +548,6 @@ namespace cpptrace { } } - lazy_exception::lazy_exception(std::size_t skip, std::size_t max_depth) noexcept - : trace_holder(detail::get_raw_trace_and_absorb(skip + 1, max_depth)) {} - const char* lazy_exception::what() const noexcept { if(what_string.empty()) { what_string = message() + std::string(":\n") + trace_holder.get_resolved_trace().to_string(); @@ -576,7 +591,7 @@ namespace cpptrace { } catch(cpptrace::exception&) { throw; // already a cpptrace::exception } catch(...) { - throw nested_exception(std::current_exception(), skip + 1); + throw nested_exception(std::current_exception(), detail::get_raw_trace_and_absorb(skip + 1)); } } } diff --git a/src/ctrace.cpp b/src/ctrace.cpp new file mode 100644 index 0000000..ecf775e --- /dev/null +++ b/src/ctrace.cpp @@ -0,0 +1,426 @@ +#include +#include +#include + +#include "symbols/symbols.hpp" +#include "unwind/unwind.hpp" +#include "demangle/demangle.hpp" +#include "utils/exception_type.hpp" +#include "utils/common.hpp" +#include "utils/utils.hpp" +#include "binary/object.hpp" +#include "binary/safe_dl.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 + 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::length(str) == 0; + } + + static ctrace_owning_string generate_owning_string(const char* raw_string) noexcept { + // Returns length to the null terminator. + std::size_t count = std::char_traits::length(raw_string); + char* new_string = new char[count + 1]; + std::char_traits::copy(new_string, raw_string, count); + new_string[count] = '\0'; + return { new_string }; + } + + static ctrace_owning_string generate_owning_string(const std::string& std_string) { + return generate_owning_string(std_string.c_str()); + } + + 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_trace c_convert(const std::vector& trace) { + std::size_t count = trace.size(); + auto* frames = new ctrace_object_frame[count]; + std::transform( + trace.begin(), + trace.end(), + frames, + [] (const cpptrace::object_frame& frame) -> ctrace_object_frame { + const char* new_path = generate_owning_string(frame.object_path).data; + return { frame.raw_address, frame.object_address, new_path }; + } + ); + return { frames, count }; + } + + static ctrace_stacktrace c_convert(const std::vector& trace) { + std::size_t count = trace.size(); + auto* frames = new ctrace_stacktrace_frame[count]; + std::transform( + trace.begin(), + trace.end(), + frames, + [] (const cpptrace::stacktrace_frame& frame) -> ctrace_stacktrace_frame { + ctrace_stacktrace_frame new_frame; + new_frame.address = frame.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)).data; + new_frame.is_inline = ctrace_bool(frame.is_inline); + return new_frame; + } + ); + return { frames, count }; + } + + static cpptrace::stacktrace cpp_convert(const ctrace_stacktrace* ptrace) { + if(!ptrace || !ptrace->frames) { + return { }; + } + std::vector new_frames; + new_frames.reserve(ptrace->count); + for(std::size_t i = 0; i < ptrace->count; ++i) { + using nullable_type = cpptrace::nullable; + static constexpr auto null_v = nullable_type::null().raw_value; + const ctrace_stacktrace_frame& old_frame = ptrace->frames[i]; + cpptrace::stacktrace_frame new_frame; + new_frame.address = old_frame.address; + new_frame.line = nullable_type{is_empty(old_frame.line) ? null_v : old_frame.line}; + new_frame.column = nullable_type{is_empty(old_frame.column) ? null_v : old_frame.column}; + new_frame.filename = old_frame.filename; + new_frame.symbol = old_frame.symbol; + new_frame.is_inline = bool(old_frame.is_inline); + new_frames.push_back(std::move(new_frame)); + } + 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 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 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 frames = cpptrace::detail::capture_frames(skip + 1, max_depth); + std::vector 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 frames(trace->count, 0); + std::copy(trace->frames, trace->frames + trace->count, frames.begin()); + std::vector 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 frames(trace->count, 0); + std::copy(trace->frames, trace->frames + trace->count, frames.begin()); + std::vector 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 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 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(out)); + } + + // 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(""); + } + 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, "\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(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].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(mode); + cpptrace::experimental::set_cache_mode(cache_mode); + } + + void ctrace_enable_inlined_call_resolution(ctrace_bool enable) { + cpptrace::enable_inlined_call_resolution(enable); + } +} diff --git a/src/symbols/symbols_with_dbghelp.cpp b/src/symbols/symbols_with_dbghelp.cpp index 82d2fd5..929419e 100644 --- a/src/symbols/symbols_with_dbghelp.cpp +++ b/src/symbols/symbols_with_dbghelp.cpp @@ -345,7 +345,7 @@ namespace dbghelp { std::fprintf(stderr, "Stack trace: Internal error while calling SymSetContext\n"); return { addr, - static_cast(line.LineNumber), + { static_cast(line.LineNumber) }, nullable::null(), line.FileName, symbol->Name, @@ -377,7 +377,7 @@ namespace dbghelp { signature = std::regex_replace(signature, comma_re, ", "); return { addr, - static_cast(line.LineNumber), + { static_cast(line.LineNumber) }, nullable::null(), line.FileName, signature, diff --git a/src/symbols/symbols_with_libdwarf.cpp b/src/symbols/symbols_with_libdwarf.cpp index a13c1bd..20d691b 100644 --- a/src/symbols/symbols_with_libdwarf.cpp +++ b/src/symbols/symbols_with_libdwarf.cpp @@ -3,7 +3,7 @@ #include #include "symbols.hpp" #include "../utils/common.hpp" -#include "../utils/dwarf.hpp" +#include "../utils/dwarf.hpp" // has dwarf #includes #include "../utils/error.hpp" #include "../binary/object.hpp" #include "../utils/utils.hpp" @@ -20,13 +20,8 @@ #include #include -#ifdef CPPTRACE_USE_EXTERNAL_LIBDWARF -#include -#include -#else -#include -#include -#endif +#include +#include // It's been tricky to piece together how to handle all this dwarf stuff. Some resources I've used are // https://www.prevanders.net/libdwarf.pdf @@ -82,7 +77,14 @@ namespace libdwarf { std::vector line_entries; }; - struct dwarf_resolver { + 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 dwarf_resolver : public symbol_resolver { std::string object_path; Dwarf_Debug dbg = nullptr; bool ok = false; @@ -95,9 +97,11 @@ namespace libdwarf { std::unordered_map> subprograms_cache; // Vector of ranges and their corresponding CU offsets std::vector cu_cache; + bool generated_cu_cache = false; // Map from CU -> {srcfiles, count} std::unordered_map> srcfiles_cache; + private: // 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 @@ -123,23 +127,37 @@ namespace libdwarf { return ret; } + public: CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING dwarf_resolver(const std::string& _object_path) { object_path = _object_path; + // use a buffer when invoking dwarf_init_path, which allows it to automatically find debuglink or dSYM + // sources + bool use_buffer = true; // for universal / fat mach-o files unsigned universal_number = 0; #if IS_APPLE if(directory_exists(object_path + ".dSYM")) { - object_path += ".dSYM/Contents/Resources/DWARF/" + basename(object_path); + // Possibly depends on the build system but a obj.cpp.o.dSYM/Contents/Resources/DWARF/obj.cpp.o can be + // created alongside .o files. These are text files containing directives, as opposed to something we + // can actually use + std::string dsym_resource = object_path + ".dSYM/Contents/Resources/DWARF/" + basename(object_path); + if(file_is_mach_o(dsym_resource)) { + object_path = std::move(dsym_resource); + } + use_buffer = false; // we resolved dSYM above as appropriate } if(macho_is_fat(object_path)) { - universal_number = get_fat_macho_index(object_path); + universal_number = mach_o(object_path).get_fat_index(); } #endif // Giving libdwarf a buffer for a true output path is needed for its automatic resolution of debuglink and // dSYM files. We don't utilize the dSYM logic here, we just care about debuglink. - std::unique_ptr buffer(new char[CPPTRACE_MAX_PATH]); + std::unique_ptr buffer; + if(use_buffer) { + buffer = std::unique_ptr(new char[CPPTRACE_MAX_PATH]); + } auto ret = wrap( dwarf_init_path_a, object_path.c_str(), @@ -165,21 +183,6 @@ namespace libdwarf { // Check for .debug_aranges for fast lookup wrap(dwarf_get_aranges, dbg, &aranges, &arange_count); } - if(ok && !aranges && get_cache_mode() != cache_mode::prioritize_memory) { - walk_compilation_units([this] (const die_object& cu_die) { - Dwarf_Half offset_size = 0; - Dwarf_Half dwversion = 0; - dwarf_get_version_of_die(cu_die.get(), &dwversion, &offset_size); - auto ranges_vec = cu_die.get_rangelist_entries(dwversion); - for(auto range : ranges_vec) { - cu_cache.push_back({ cu_die.clone(), dwversion, range.first, range.second }); - } - return true; - }); - std::sort(cu_cache.begin(), cu_cache.end(), [] (const cu_entry& a, const cu_entry& b) { - return a.low < b.low; - }); - } } CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING @@ -213,6 +216,7 @@ namespace libdwarf { line_contexts(std::move(other.line_contexts)), subprograms_cache(std::move(other.subprograms_cache)), cu_cache(std::move(other.cu_cache)), + generated_cu_cache(other.generated_cu_cache), srcfiles_cache(std::move(other.srcfiles_cache)) { other.dbg = nullptr; @@ -228,12 +232,14 @@ namespace libdwarf { line_contexts = std::move(other.line_contexts); subprograms_cache = std::move(other.subprograms_cache); cu_cache = std::move(other.cu_cache); + generated_cu_cache = other.generated_cu_cache; srcfiles_cache = std::move(other.srcfiles_cache); other.dbg = nullptr; other.aranges = nullptr; return *this; } + private: // walk all CU's in a dbg, callback is called on each die and should return true to // continue traversal void walk_compilation_units(const std::function& fn) { @@ -282,6 +288,25 @@ namespace libdwarf { } } + void lazy_generate_cu_cache() { + if(!generated_cu_cache) { + walk_compilation_units([this] (const die_object& cu_die) { + Dwarf_Half offset_size = 0; + Dwarf_Half dwversion = 0; + dwarf_get_version_of_die(cu_die.get(), &dwversion, &offset_size); + auto ranges_vec = cu_die.get_rangelist_entries(dwversion); + for(auto range : ranges_vec) { + cu_cache.push_back({ cu_die.clone(), dwversion, range.first, range.second }); + } + return true; + }); + std::sort(cu_cache.begin(), cu_cache.end(), [] (const cu_entry& a, const cu_entry& b) { + return a.low < b.low; + }); + generated_cu_cache = true; + } + } + std::string subprogram_symbol( const die_object& die, Dwarf_Half dwversion @@ -381,7 +406,9 @@ namespace libdwarf { ) { ASSERT(die.get_tag() == DW_TAG_subprogram); const auto name = subprogram_symbol(die, dwversion); - get_inlines_info(cu_die, die, pc, dwversion, inlines); + if(detail::should_resolve_inlined_calls()) { + get_inlines_info(cu_die, die, pc, dwversion, inlines); + } return name; } @@ -522,19 +549,14 @@ namespace libdwarf { it = subprograms_cache.find(off); } auto& vec = it->second; - auto vec_it = std::lower_bound( + auto vec_it = first_less_than_or_equal( vec.begin(), vec.end(), pc, - [] (const subprogram_entry& entry, Dwarf_Addr pc) { - return entry.low < pc; + [] (Dwarf_Addr pc, const subprogram_entry& entry) { + return pc < entry.low; } ); - // vec_it is first >= pc - // we want first <= pc - if(vec_it != vec.begin()) { - vec_it--; - } // If the vector has been empty this can happen if(vec_it != vec.end()) { //vec_it->die.print(); @@ -649,19 +671,14 @@ namespace libdwarf { if(get_cache_mode() == cache_mode::prioritize_speed) { // Lookup in the table auto& line_entries = table_info.line_entries; - auto table_it = std::lower_bound( + auto table_it = first_less_than_or_equal( line_entries.begin(), line_entries.end(), pc, - [] (const line_entry& entry, Dwarf_Addr pc) { - return entry.low < pc; + [] (Dwarf_Addr pc, const line_entry& entry) { + return pc < entry.low; } ); - // vec_it is first >= pc - // we want first <= pc - if(table_it != line_entries.begin()) { - table_it--; - } // If the vector has been empty this can happen if(table_it != line_entries.end()) { Dwarf_Line line = table_it->line; @@ -787,67 +804,81 @@ namespace libdwarf { } retrieve_line_info(cu_die, pc, frame); // no offset for line info retrieve_symbol(cu_die, pc, dwversion, frame, inlines); + return; } - } else { - if(get_cache_mode() == cache_mode::prioritize_memory) { - // walk for the cu and go from there - walk_compilation_units([this, pc, &frame, &inlines] (const die_object& cu_die) { - Dwarf_Half offset_size = 0; - Dwarf_Half dwversion = 0; - dwarf_get_version_of_die(cu_die.get(), &dwversion, &offset_size); - //auto p = cu_die.get_pc_range(dwversion); - //cu_die.print(); - //fprintf(stderr, " %llx, %llx\n", p.first, p.second); + } + // otherwise, or if not in aranges + // one reason to fallback here is if the compilation has dwarf generated from different compilers and only + // some of them generate aranges (e.g. static linking with cpptrace after specifying clang++ as the c++ + // compiler while the C compiler defaults to an older gcc) + if(get_cache_mode() == cache_mode::prioritize_memory) { + // walk for the cu and go from there + walk_compilation_units([this, pc, &frame, &inlines] (const die_object& cu_die) { + Dwarf_Half offset_size = 0; + Dwarf_Half dwversion = 0; + dwarf_get_version_of_die(cu_die.get(), &dwversion, &offset_size); + //auto p = cu_die.get_pc_range(dwversion); + //cu_die.print(); + //fprintf(stderr, " %llx, %llx\n", p.first, p.second); + if(trace_dwarf) { + std::fprintf(stderr, "CU: %d %s\n", dwversion, cu_die.get_name().c_str()); + } + if(cu_die.pc_in_die(dwversion, pc)) { if(trace_dwarf) { - std::fprintf(stderr, "CU: %d %s\n", dwversion, cu_die.get_name().c_str()); + std::fprintf( + stderr, + "pc in die %08llx %s (now searching for %08llx)\n", + to_ull(cu_die.get_global_offset()), + cu_die.get_tag_name(), + to_ull(pc) + ); } - if(cu_die.pc_in_die(dwversion, pc)) { - if(trace_dwarf) { - std::fprintf( - stderr, - "pc in die %08llx %s (now searching for %08llx)\n", - to_ull(cu_die.get_global_offset()), - cu_die.get_tag_name(), - to_ull(pc) - ); - } - retrieve_line_info(cu_die, pc, frame); // no offset for line info - retrieve_symbol(cu_die, pc, dwversion, frame, inlines); - return false; - } - return true; - }); + retrieve_line_info(cu_die, pc, frame); // no offset for line info + retrieve_symbol(cu_die, pc, dwversion, frame, inlines); + return false; + } + return true; + }); + } else { + lazy_generate_cu_cache(); + // look up the cu + auto vec_it = first_less_than_or_equal( + cu_cache.begin(), + cu_cache.end(), + pc, + [] (Dwarf_Addr pc, const cu_entry& entry) { + return pc < entry.low; + } + ); + // If the vector has been empty this can happen + if(vec_it != cu_cache.end()) { + //vec_it->die.print(); + if(vec_it->die.pc_in_die(vec_it->dwversion, pc)) { + retrieve_line_info(vec_it->die, pc, frame); // no offset for line info + retrieve_symbol(vec_it->die, pc, vec_it->dwversion, frame, inlines); + } } else { - // look up the cu - auto vec_it = std::lower_bound( - cu_cache.begin(), - cu_cache.end(), - pc, - [] (const cu_entry& entry, Dwarf_Addr pc) { - return entry.low < pc; - } - ); - // vec_it is first >= pc - // we want first <= pc - if(vec_it != cu_cache.begin()) { - vec_it--; - } - // If the vector has been empty this can happen - if(vec_it != cu_cache.end()) { - //vec_it->die.print(); - if(vec_it->die.pc_in_die(vec_it->dwversion, pc)) { - retrieve_line_info(vec_it->die, pc, frame); // no offset for line info - retrieve_symbol(vec_it->die, pc, vec_it->dwversion, frame, inlines); - } - } else { - ASSERT(cu_cache.size() == 0, "Vec should be empty?"); - } + ASSERT(cu_cache.size() == 0, "Vec should be empty?"); } } } + public: CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING - frame_with_inlines resolve_frame(const object_frame& frame_info) { + frame_with_inlines resolve_frame(const object_frame& frame_info) override { + if(!ok) { + return { + { + frame_info.raw_address, + nullable::null(), + nullable::null(), + frame_info.object_path, + "", + false + }, + {} + }; + } stacktrace_frame frame = null_frame; frame.filename = frame_info.object_path; frame.address = frame_info.raw_address; @@ -869,55 +900,248 @@ namespace libdwarf { } }; + class null_resolver : public symbol_resolver { + public: + null_resolver() = default; + null_resolver(const std::string&) {} + + CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING + frame_with_inlines resolve_frame(const object_frame& frame_info) override { + return { + { + frame_info.raw_address, + nullable::null(), + nullable::null(), + frame_info.object_path, + "", + false + }, + {} + }; + }; + }; + + #if IS_APPLE + struct target_object { + std::string object_path; + bool path_ok = true; + optional> symbols; + std::unique_ptr resolver; + + target_object(std::string object_path) : object_path(object_path) {} + + std::unique_ptr& 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 = std::unique_ptr(new null_resolver); + resolver = std::unique_ptr(new dwarf_resolver(object_path)); + } + return resolver; + } + + std::unordered_map& 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 symbols; + this->symbols = symbols; + auto symbol_table = mach_o(object_path).symbol_table(); + for(const auto& symbol : symbol_table) { + 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; + frame.object_address = it->second + offset; + return get_resolver()->resolve_frame(frame); + } else { + return { + { + frame_info.raw_address, + nullable::null(), + nullable::null(), + frame_info.object_path, + symbol_name, + false + }, + {} + }; + } + } + }; + + struct debug_map_symbol_info { + uint64_t source_address; + uint64_t size; + std::string name; + nullable target_address; // T(-1) is used as a sentinel + std::size_t object_index; + }; + + class debug_map_resolver : public symbol_resolver { + std::vector target_objects; + std::vector symbols; + public: + debug_map_resolver(const std::string& source_object_path) { + // load mach-o + // TODO: Cache somehow? + mach_o source_mach(source_object_path); + auto source_debug_map = source_mach.get_debug_map(); + // get symbol entries from debug map, as well as the various object files used to make this binary + for(auto& entry : source_debug_map) { + // object it came from + target_objects.push_back({std::move(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::null(), + target_objects.size() - 1 + }); + } + } + // sort for binary lookup later + 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, + [] ( + Dwarf_Addr 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 + 0, + 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, + nullable::null(), + nullable::null(), + frame_info.object_path, + "", + false + }, + {} + }; + }; + }; + #endif + + std::unique_ptr 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 std::unique_ptr(new debug_map_resolver(object_path)); + } + #endif + return std::unique_ptr(new dwarf_resolver(object_path)); + } + CPPTRACE_FORCE_NO_INLINE_FOR_PROFILING std::vector resolve_frames(const std::vector& frames) { std::vector trace(frames.size(), {null_frame, {}}); static std::mutex mutex; // cache resolvers since objects are likely to be traced more than once - static std::unordered_map resolver_map; + static std::unordered_map> resolver_map; // Locking around all libdwarf interaction per https://github.com/davea42/libdwarf-code/discussions/184 + // And also interactions with the above static map const std::lock_guard lock(mutex); for(const auto& object_entry : collate_frames(frames, trace)) { try { const auto& object_name = object_entry.first; - optional resolver_object = nullopt; - dwarf_resolver* resolver = nullptr; + std::unique_ptr resolver_object; + symbol_resolver* resolver = nullptr; auto it = resolver_map.find(object_name); if(it != resolver_map.end()) { - resolver = &it->second; + resolver = it->second.get(); } else { - resolver_object = dwarf_resolver(object_name); - resolver = &resolver_object.unwrap(); + resolver_object = get_resolver_for_object(object_name); + resolver = resolver_object.get(); } - // If there's no debug information it'll mark itself as not ok - if(resolver->ok) { - for(const auto& entry : object_entry.second) { - try { - const auto& dlframe = entry.first.get(); - auto& frame = entry.second.get(); - frame = resolver->resolve_frame(dlframe); - } catch(...) { - if(!should_absorb_trace_exceptions()) { - throw; - } - } - } - } else { - // at least copy the addresses - for(const auto& entry : object_entry.second) { + for(const auto& entry : object_entry.second) { + try { const auto& dlframe = entry.first.get(); auto& frame = entry.second.get(); - frame.frame.address = dlframe.raw_address; + frame = resolver->resolve_frame(dlframe); + } catch(...) { + if(!should_absorb_trace_exceptions()) { + throw; + } } } - if(resolver_object.has_value() && get_cache_mode() == cache_mode::prioritize_speed) { + if(resolver_object && get_cache_mode() == cache_mode::prioritize_speed) { // .emplace needed, for some reason .insert tries to copy <= gcc 7.2 - resolver_map.emplace(object_name, std::move(resolver_object).unwrap()); + resolver_map.emplace(object_name, std::move(resolver_object)); } } catch(...) { // NOSONAR if(!should_absorb_trace_exceptions()) { throw; } + for(const auto& entry : object_entry.second) { + const auto& dlframe = entry.first.get(); + auto& frame = entry.second.get(); + frame = { + { + dlframe.raw_address, + nullable::null(), + nullable::null(), + dlframe.object_path, + "", + false + }, + {} + }; + } } } // flatten trace with inlines diff --git a/src/unwind/unwind_with_winapi.cpp b/src/unwind/unwind_with_winapi.cpp index d10f6d9..a499357 100644 --- a/src/unwind/unwind_with_winapi.cpp +++ b/src/unwind/unwind_with_winapi.cpp @@ -21,7 +21,7 @@ namespace detail { CPPTRACE_FORCE_NO_INLINE std::vector capture_frames(std::size_t skip, std::size_t max_depth) { std::vector addrs(skip + std::min(hard_max_frames, max_depth), nullptr); - int n_frames = CaptureStackBackTrace( + std::size_t n_frames = CaptureStackBackTrace( static_cast(skip + 1), static_cast(addrs.size()), addrs.data(), diff --git a/src/utils/common.hpp b/src/utils/common.hpp index ccc7461..5f79f59 100644 --- a/src/utils/common.hpp +++ b/src/utils/common.hpp @@ -58,6 +58,7 @@ namespace detail { static const stacktrace_frame null_frame {0, nullable::null(), nullable::null(), "", "", false}; bool should_absorb_trace_exceptions(); + bool should_resolve_inlined_calls(); enum cache_mode get_cache_mode(); } } diff --git a/src/utils/dwarf.hpp b/src/utils/dwarf.hpp index 61a8ea5..39feb34 100644 --- a/src/utils/dwarf.hpp +++ b/src/utils/dwarf.hpp @@ -9,12 +9,12 @@ #include #include -#ifdef CPPTRACE_USE_EXTERNAL_LIBDWARF -#include -#include +#ifdef CPPTRACE_USE_NESTED_LIBDWARF_HEADER_PATH + #include + #include #else -#include -#include + #include + #include #endif namespace cpptrace { diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index ea0c53f..c51a2a2 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -1,6 +1,7 @@ #ifndef UTILS_HPP #define UTILS_HPP +#include #include #include #include @@ -72,6 +73,28 @@ namespace detail { return str; } + // first value in a sorted range such that *it <= value + template + ForwardIt first_less_than_or_equal(ForwardIt begin, ForwardIt end, const T& value) { + auto it = std::upper_bound(begin, end, value); + // it is first > value, we want first <= value + if(it != begin) { + return --it; + } + return end; + } + + // first value in a sorted range such that *it <= value + template + ForwardIt first_less_than_or_equal(ForwardIt begin, ForwardIt end, const T& value, Compare compare) { + auto it = std::upper_bound(begin, end, value, compare); + // it is first > value, we want first <= value + if(it != begin) { + return --it; + } + return end; + } + constexpr const char* const whitespace = " \t\n\r\f\v"; inline std::string trim(const std::string& str) { diff --git a/test/ctrace_demo.c b/test/ctrace_demo.c new file mode 100644 index 0000000..4dc85df --- /dev/null +++ b/test/ctrace_demo.c @@ -0,0 +1,39 @@ +#include +#include +#include +#include + +void trace() { + ctrace_raw_trace raw_trace = ctrace_generate_raw_trace(1, INT_MAX); + ctrace_object_trace obj_trace = ctrace_resolve_raw_trace_to_object_trace(&raw_trace); + ctrace_stacktrace trace = ctrace_resolve_object_trace(&obj_trace); + ctrace_print_stacktrace(&trace, stdout, 1); + ctrace_free_stacktrace(&trace); + ctrace_free_object_trace(&obj_trace); + ctrace_free_raw_trace(&raw_trace); + assert(raw_trace.frames == NULL && obj_trace.count == 0); +} + +void bar(int n) { + if(n == 0) { + trace(); + } else { + bar(n - 1); + } +} + +void foo(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) { + bar(1); +} + +void function_two(int a, float b) { + foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); +} + +void function_one(int a) { + function_two(0, 0); +} + +int main() { + function_one(0); +} diff --git a/test/signal_demo.cpp b/test/signal_demo.cpp index c7a2db2..dc7decc 100644 --- a/test/signal_demo.cpp +++ b/test/signal_demo.cpp @@ -12,7 +12,7 @@ #include void trace() { - *(char*)0 = 2; + *(volatile char*)0 = 2; } void bar() { diff --git a/test/speedtest/CMakeLists.txt b/test/speedtest/CMakeLists.txt index 6cfd3ee..a6eb38d 100644 --- a/test/speedtest/CMakeLists.txt +++ b/test/speedtest/CMakeLists.txt @@ -36,6 +36,7 @@ FetchContent_MakeAvailable(googletest) set(cpptrace_DIR "../../build/foo/lib/cmake/cpptrace") set(libdwarf_DIR "../../build/foo/lib/cmake/libdwarf") +set(zstd_DIR "../../build/foo/lib/cmake/zstd") find_package(cpptrace REQUIRED) add_executable(speedtest speedtest.cpp)