diff --git a/update/CMakeLists.txt b/update/CMakeLists.txt index 06a7084547..3cf4732309 100644 --- a/update/CMakeLists.txt +++ b/update/CMakeLists.txt @@ -1,7 +1,9 @@ cmake_minimum_required(VERSION 3.16) -option(NO_ISA_EXTENSIONS "Disable ISA extensions (don't pass -march=native or -mcpu=native to the compiler)" OFF) +option(NO_ISA_EXTENSIONS "Disable ISA extensions (don't pass -march=native or -mcpu=native to the compiler)" OFF ) option(NO_STATISTICS "Disable calculation of statistics" ON) +option(LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT "Suppport using libdw for offline symbol resolution." OFF) +option(LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT "Suppport using libbacktrace for offline symbol resolution." OFF) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/version.cmake) @@ -21,9 +23,24 @@ set(PROGRAM_FILES src/OfflineSymbolResolver.cpp src/OfflineSymbolResolverAddr2Line.cpp src/OfflineSymbolResolverDbgHelper.cpp + src/OfflineSymbolResolverLibdw.cpp + src/OfflineSymbolResolverLibBacktrace.cpp src/update.cpp ) add_executable(${PROJECT_NAME} ${PROGRAM_FILES} ${COMMON_FILES} ${SERVER_FILES}) target_link_libraries(${PROJECT_NAME} PRIVATE TracyServer TracyGetOpt) + +if(LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT) + message("LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT = ${LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT}" ) + target_compile_definitions(${PROJECT_NAME} PRIVATE LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT) + # expects the requires libraries be installed in the target system + target_link_libraries(${PROJECT_NAME} PRIVATE dl dw elf) +endif() +if(LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT) + message("LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT = ${LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT}" ) + target_compile_definitions(${PROJECT_NAME} PRIVATE LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT) + target_link_libraries(${PROJECT_NAME} PRIVATE backtrace) +endif() + set_property(DIRECTORY ${CMAKE_CURRENT_LIST_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME}) \ No newline at end of file diff --git a/update/src/OfflineSymbolResolver.cpp b/update/src/OfflineSymbolResolver.cpp index c5f0cd6198..6dd9b5f7be 100644 --- a/update/src/OfflineSymbolResolver.cpp +++ b/update/src/OfflineSymbolResolver.cpp @@ -1,5 +1,9 @@ #include #include +#include +#include +#include +#include #include #include #include @@ -11,6 +15,56 @@ #include "OfflineSymbolResolver.h" +struct OfflineResolverEntry +{ + const char* name; + bool (*resolveSymbolsCallback)(const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries); +}; +static OfflineResolverEntry offlineResolverConfig[] = +{ + // ordered by preference (default is the first one of the list available) +#ifdef _WIN32 + OfflineResolverEntry("dbghelp", &ResolveSymbolsWithDbgHelp), +#else + #ifdef LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT + OfflineResolverEntry("libdw", &ResolveSymbolsWithLibDW), + #endif // #ifdef LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT + OfflineResolverEntry("addr2line", &ResolveSymbolsWithAddr2Line), + #ifdef LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT + OfflineResolverEntry("libacktrace", &ResolveSymbolsWithLibBacktrace), + #endif // #ifdef LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT +#endif //#ifndef _WIN32 +}; + +int GetOfflineSymbolResolverCount() +{ + return sizeof(offlineResolverConfig)/sizeof(offlineResolverConfig[0]); +} +const char* GetOfflineSymbolResolverName(int index) +{ + return (index < GetOfflineSymbolResolverCount()) ? offlineResolverConfig[index].name : ""; +} +const char* GetDefaultOfflineSymbolResolver() +{ + return GetOfflineSymbolResolverCount() ? offlineResolverConfig[0].name : ""; +} + +// main entrypoint to resolving symbols +bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries, const ResolveOptions& options) +{ + for (int i = 0; i < GetOfflineSymbolResolverCount(); ++i) + { + OfflineResolverEntry& entry = offlineResolverConfig[i]; + if (options.resolver == entry.name) + { + return entry.resolveSymbolsCallback(imagePath, inputEntryList, resolvedEntries); + } + } + return false; +} + bool ApplyPathSubstitutions( std::string& path, const PathSubstitutionList& pathSubstitutionlist ) { for( const auto& substitution : pathSubstitutionlist ) @@ -24,6 +78,21 @@ bool ApplyPathSubstitutions( std::string& path, const PathSubstitutionList& path return false; } +bool ShouldSkipImage( const std::string& imagePath, const SkipImageList* skipImageList ) +{ + if(!skipImageList) + return false; + + for( const auto& skipMatch : *skipImageList ) + { + if( std::regex_match( imagePath, skipMatch ) ) + { + return true; + } + } + return false; +} + tracy::StringIdx AddSymbolString( tracy::Worker& worker, const std::string& str ) { // TODO: use string hash map to reduce potential string duplication? @@ -31,12 +100,85 @@ tracy::StringIdx AddSymbolString( tracy::Worker& worker, const std::string& str return tracy::StringIdx( location.idx ); } -bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstitutionlist, bool verbose ) +using JobCallback = std::function; +void processJobsInParallel( size_t maxJobs, size_t maxConcurrent, JobCallback&& onJobStart, JobCallback&& func, + JobCallback&& onJobEnd ) +{ + if (maxConcurrent <=1) + { + for(size_t index = 0; index < maxJobs; ++index) + { + onJobStart(index); + func(index); + onJobEnd(index); + } + } + + struct JobEntry + { + size_t jobIndex; + std::future future; + }; + + std::vector results; + size_t index = 0; + + while( index < maxJobs || !results.empty() ) + { + // Launch new jobs if there is room + while( index < maxJobs && results.size() < maxConcurrent ) + { + onJobStart( index ); + auto future = std::async( std::launch::async, std::bind( func, index ) ); + results.push_back( { index, std::move(future) } ); + index++; + } + + // Remove completed jobs + results.erase( std::remove_if( results.begin(), results.end(), + [onJobEnd]( JobEntry& entry ) + { + const bool finished = entry.future.wait_for( std::chrono::milliseconds(0) ) == std::future_status::ready; + if (finished) + { + onJobEnd( entry.jobIndex ); + } + return finished; + }), + results.end() ); + } +} + +class Stopwatch { +public: + explicit Stopwatch() + : start_(std::chrono::high_resolution_clock::now()) + {} + void start() + { + start_ = std::chrono::high_resolution_clock::now(); + } + size_t getTimeFromStartInMs() const + { + auto end = std::chrono::high_resolution_clock::now(); + auto durationMs = std::chrono::duration_cast(end - start_).count(); + return durationMs; + } +private: + std::chrono::high_resolution_clock::time_point start_; +}; + +bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstitutionlist, + const SkipImageList* skipImageList, const ResolveOptions& options) +{ + Stopwatch overallStopwatch; + uint64_t callstackFrameCount = worker.GetCallstackFrameCount(); std::string relativeSoNameMatch = "[unresolved]"; - std::cout << "Found " << callstackFrameCount << " callstack frames. Batching into image groups..." << std::endl; + std::cout << "* Found " << callstackFrameCount << " callstack frames. Batching into image groups..." << std::endl; + Stopwatch batchIntoGroupsStopwatch; // batch the symbol queries by .so so we issue the least amount of requests using FrameEntriesPerImageIdx = std::unordered_map; @@ -72,53 +214,139 @@ bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& p } } - std::cout << "Batched into " << entriesPerImageIdx.size() << " unique image groups" << std::endl; + std::cout << "* Batched into " << entriesPerImageIdx.size() + << " unique image groups [took: " << batchIntoGroupsStopwatch.getTimeFromStartInMs() << " ms]" << std::endl; - // FIXME: the resolving of symbols here can be slow and could be done in parallel per "image" - // - be careful with string allocation though as that would be not safe to do in parallel - for( FrameEntriesPerImageIdx::iterator imageIt = entriesPerImageIdx.begin(), - imageItEnd = entriesPerImageIdx.end(); imageIt != imageItEnd; ++imageIt ) + struct JobEntry { - tracy::StringIdx imageIdx( imageIt->first ); - std::string imagePath = worker.GetString( imageIdx ); + FrameEntriesPerImageIdx::iterator imageIt; + SymbolEntryList results; + std::string imagePath; + Stopwatch jobStopWatch; + }; + + using AllResoledEntries = std::vector; + AllResoledEntries resolvedResults; + resolvedResults.resize( entriesPerImageIdx.size() ); + size_t index = 0; + size_t totalEntries = 0; + size_t processedEntries = 0; + for( FrameEntriesPerImageIdx::iterator imageIt = entriesPerImageIdx.begin(), imageItEnd = entriesPerImageIdx.end(); + imageIt != imageItEnd; ++imageIt, ++index ) + { + resolvedResults[index].imageIt = imageIt; FrameEntryList& entries = imageIt->second; + tracy::StringIdx imageIdx( imageIt->first ); + resolvedResults[index].imagePath = worker.GetString( imageIdx ); + totalEntries += entries.size(); + } - if( !entries.size() ) continue; +#ifdef _WIN32 + // DbgHelper is not thread safe + unsigned int maxConcurrent = 1; +#else + unsigned int maxConcurrent = + (options.maxParallelism <= 0) ? std::thread::hardware_concurrency() : int(options.maxParallelism); +#endif - std::cout << "Resolving " << entries.size() << " symbols for image: '" - << imagePath << "'" << std::endl; - const bool substituted = ApplyPathSubstitutions( imagePath, pathSubstitutionlist ); - if( substituted ) + std::cout << "* Running " << resolvedResults.size() << " resolution jobs in parallel (batches of " << maxConcurrent << ")" << std::endl; + Stopwatch parallelResolveStopwatch; + + // run symbol resolution for each image in parallel + processJobsInParallel( resolvedResults.size(), maxConcurrent, + + [&resolvedResults, &worker]( size_t jobIndex ) { - std::cout << "\tPath substituted to: '" << imagePath << "'" << std::endl; - } + FrameEntriesPerImageIdx::iterator imageIt = resolvedResults[jobIndex].imageIt; + FrameEntryList& entries = imageIt->second; + const std::string& imagePath = resolvedResults[jobIndex].imagePath; - SymbolEntryList resolvedEntries; - ResolveSymbols( imagePath, entries, resolvedEntries ); + std::cout << "[job " << jobIndex << "/" << resolvedResults.size() << "] Starting resolving " + << entries.size() << " symbols for image: '" << imagePath << "' ..." << std::endl; + }, - if( resolvedEntries.size() != entries.size() ) + [&resolvedResults, &worker, &pathSubstitutionlist, skipImageList, &options]( size_t jobIndex ) { - std::cerr << " failed to resolve all entries! (got: " - << resolvedEntries.size() << ")" << std::endl; - continue; - } + std::string imagePath = resolvedResults[jobIndex].imagePath; - // finally patch the string with the resolved symbol data - for ( size_t i = 0; i < resolvedEntries.size(); ++i ) + if( ShouldSkipImage( imagePath, skipImageList ) ) + { + std::cerr << " * Skipping image ' " << imagePath << "' as requested..." << std::endl; + return; + } + + FrameEntriesPerImageIdx::iterator imageIt = resolvedResults[jobIndex].imageIt; + FrameEntryList& entries = imageIt->second; + if( entries.size() ) + { + ApplyPathSubstitutions( imagePath, pathSubstitutionlist ); + + SymbolEntryList& resolvedEntries = resolvedResults[jobIndex].results; + ResolveSymbols( imagePath, entries, resolvedEntries, options); + + if( resolvedEntries.size() != entries.size() ) + { + std::cerr << " failed to resolve all entries! (got: " << resolvedEntries.size() + << ", expected: " << entries.size() << ") discarding results ..." << std::endl; + resolvedEntries.clear(); + } + } + }, + [&resolvedResults, &worker, totalEntries, &processedEntries, ¶llelResolveStopwatch]( size_t jobIndex ) + { + FrameEntriesPerImageIdx::iterator imageIt = resolvedResults[jobIndex].imageIt; + const size_t totalJobs = resolvedResults.size(); + const size_t entriesForJob = imageIt->second.size(); + processedEntries += entriesForJob; + int finishedPercent = int( ( float( processedEntries ) * 100.0f ) / float( totalEntries ) ); + + const std::string& imagePath = resolvedResults[jobIndex].imagePath; + std::cout << "[job " << jobIndex << "/" << totalJobs << "] [progress: " << finishedPercent + << "%, duration: " << parallelResolveStopwatch.getTimeFromStartInMs() / 1000 << " s] finished " + << entriesForJob << " entries for: '" << imagePath << "' in: " + << ( resolvedResults[jobIndex].jobStopWatch.getTimeFromStartInMs() / 1000 ) << " s" << std::endl; + } + ); + + std::cout << "* Parallel resolve took " << parallelResolveStopwatch.getTimeFromStartInMs() / 1000 << " s" << std::endl; + std::cout << "* Patching resolved entries ..." << std::endl; + Stopwatch patchingEntriesStopwatch; + + uint32_t totalEntriesAttemped = 0; + uint32_t totalFailedResolved = 0; + + // after resolution, patch all the strings with the results. This has to be done serially unfortunately as the string manipulation + // in the worker is not multi thread safe! + for( AllResoledEntries::iterator resolvedEntryit = resolvedResults.begin(), itEnd = resolvedResults.end(); + resolvedEntryit != itEnd; ++resolvedEntryit ) + { + FrameEntriesPerImageIdx::iterator imgIt = resolvedEntryit->imageIt; + FrameEntryList& entries = imgIt->second; + SymbolEntryList& resolvedEntries = resolvedEntryit->results; + tracy::StringIdx imageIdx( imgIt->first ); + std::string imagePath = worker.GetString( imageIdx ); + + for( size_t i = 0; i < resolvedEntries.size(); ++i ) { FrameEntry& frameEntry = entries[i]; const SymbolEntry& symbolEntry = resolvedEntries[i]; tracy::CallstackFrame& frame = *frameEntry.frame; - if( !symbolEntry.name.length() ) continue; + if (!symbolEntry.resolved) + ++totalFailedResolved; + else + ++totalEntriesAttemped; - if( verbose ) + if( !symbolEntry.name.length() ) + continue; + + if( options.verbose ) { const char* nameStr = worker.GetString( frame.name ); - std::cout << "patching '" << nameStr << "' of '" << imagePath - << "' -> '" << symbolEntry.name << "'" << std::endl; + std::cout << "patching '" << nameStr << "' of '" << imagePath << "' -> '" << symbolEntry.name << "'" + << std::endl; } frame.name = AddSymbolString( worker, symbolEntry.name ); @@ -132,10 +360,18 @@ bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& p } } + std::cout << "* Patching entries took " << patchingEntriesStopwatch.getTimeFromStartInMs() << " ms" << std::endl; + size_t timeInseconds = overallStopwatch.getTimeFromStartInMs() / 1000; + std::cout << "The whole process took " << timeInseconds << " s" << std::endl; + std::cout << "* Attempted resolve of " << totalEntriesAttemped + << " entries, failed to resolve " << totalFailedResolved + << "(" << (totalFailedResolved*100 / totalEntriesAttemped) << "%)" << std::endl; + return true; } -void PatchSymbols( tracy::Worker& worker, const std::vector& pathSubstitutionsStrings, bool verbose ) +void PatchSymbols( tracy::Worker& worker, const std::vector& pathSubstitutionsStrings, + const std::vector& skipImageListStr, const ResolveOptions& options) { std::cout << "Resolving and patching symbols..." << std::endl; @@ -164,7 +400,15 @@ void PatchSymbols( tracy::Worker& worker, const std::vector& pathSu } } - if ( !PatchSymbolsWithRegex(worker, pathSubstitutionList, verbose) ) + SkipImageList skipImageList; + for( const std::string& imageName : skipImageListStr ) + { + std::cout << "Adding regex image skip: '" << imageName << "'" << std::endl; + skipImageList.push_back( std::regex( imageName ) ); + } + + if( !PatchSymbolsWithRegex( worker, pathSubstitutionList, + (skipImageList.empty() ? nullptr : &skipImageList), options)) { std::cerr << "Failed to patch symbols" << std::endl; } diff --git a/update/src/OfflineSymbolResolver.h b/update/src/OfflineSymbolResolver.h index e3216eb5cc..35af1675c8 100644 --- a/update/src/OfflineSymbolResolver.h +++ b/update/src/OfflineSymbolResolver.h @@ -25,16 +25,44 @@ struct SymbolEntry std::string name; std::string file; int line = 0; + bool resolved = false; }; using SymbolEntryList = std::vector; -bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList, - SymbolEntryList& resolvedEntries ); +struct ResolveOptions +{ + bool verbose = false; + std::string resolver; + int maxParallelism = -1; +}; + +int GetOfflineSymbolResolverCount(); +const char* GetOfflineSymbolResolverName(int index); +const char* GetDefaultOfflineSymbolResolver(); -void PatchSymbols( tracy::Worker& worker, const std::vector& pathSubstitutionsStrings, bool verbose = false ); +void PatchSymbols( tracy::Worker& worker, const std::vector& pathSubstitutionsStrings, + const std::vector& skipImageList, const ResolveOptions& options); +using SkipImageList = std::vector; using PathSubstitutionList = std::vector >; -bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstituionlist, bool verbose = false ); +bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstituionlist, + const SkipImageList& skipImageList, const ResolveOptions& options); + +bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries, const ResolveOptions& options); + +// for linux we have multiple options: +#ifdef _WIN32 + bool ResolveSymbolsWithDbgHelp(const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ); +#else + bool ResolveSymbolsWithLibBacktrace( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ); + bool ResolveSymbolsWithLibDW( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ); + bool ResolveSymbolsWithAddr2Line( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ); +#endif //#ifndef _WIN32 #endif // __SYMBOLRESOLVER_HPP__ \ No newline at end of file diff --git a/update/src/OfflineSymbolResolverAddr2Line.cpp b/update/src/OfflineSymbolResolverAddr2Line.cpp index d2003321aa..bef213dd07 100644 --- a/update/src/OfflineSymbolResolverAddr2Line.cpp +++ b/update/src/OfflineSymbolResolverAddr2Line.cpp @@ -10,26 +10,26 @@ #include #include -std::string ExecShellCommand( const char* cmd ) +class Addr2LineSymbolResolver { - std::array buffer; - std::string result; - std::unique_ptr pipe(popen(cmd, "r"), pclose); - if( !pipe ) + static std::string ExecShellCommand( const char* cmd ) { - return ""; - } - while( fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr ) - { - result += buffer.data(); + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if( !pipe ) + { + return ""; + } + while( fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr ) + { + result += buffer.data(); + } + return result; } - return result; -} -class SymbolResolver -{ public: - SymbolResolver() + Addr2LineSymbolResolver() { std::stringstream result( ExecShellCommand("which addr2line") ); std::getline(result, m_addr2LinePath); @@ -119,7 +119,8 @@ class SymbolResolver std::string addr; std::getline( result, addr ); std::getline( result, newEntry.name ); - if( newEntry.name == "??" ) + newEntry.resolved =( newEntry.name != "??" ); + if (!newEntry.resolved) { newEntry.name = "[unknown] + " + std::to_string( inputEntry.symbolOffset ); } @@ -149,10 +150,10 @@ class SymbolResolver std::string m_addr2LinePath; }; -bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList, - SymbolEntryList& resolvedEntries ) +bool ResolveSymbolsWithAddr2Line( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ) { - static SymbolResolver symbolResolver; + static Addr2LineSymbolResolver symbolResolver; return symbolResolver.ResolveSymbols( imagePath, inputEntryList, resolvedEntries ); } diff --git a/update/src/OfflineSymbolResolverDbgHelper.cpp b/update/src/OfflineSymbolResolverDbgHelper.cpp index d454feb573..b28a470f2f 100644 --- a/update/src/OfflineSymbolResolverDbgHelper.cpp +++ b/update/src/OfflineSymbolResolverDbgHelper.cpp @@ -69,12 +69,9 @@ class SymbolResolver if( SymFromAddr( m_procHandle, address, NULL, symbolInfo ) ) { newEntry.name = symbolInfo->Name; + newEntry.resolved = true; //std::cout << "Resolved symbol to: '" << newEntry.name << "'" << std::endl; } - else - { - newEntry.name = "[unknown] + " + std::to_string(offset); - } IMAGEHLP_LINE lineInfo = { 0 }; lineInfo.SizeOfStruct = sizeof(IMAGEHLP_LINE64); @@ -122,8 +119,8 @@ class SymbolResolver char SymbolResolver::s_symbolResolutionBuffer[symbolResolutionBufferSize]; -bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList, - SymbolEntryList& resolvedEntries ) +bool ResolveSymbolsWithDbgHelp(const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ) { static SymbolResolver resolver; return resolver.ResolveSymbolsForModule( imagePath, inputEntryList, resolvedEntries ); diff --git a/update/src/OfflineSymbolResolverLibBacktrace.cpp b/update/src/OfflineSymbolResolverLibBacktrace.cpp new file mode 100644 index 0000000000..f83f80f609 --- /dev/null +++ b/update/src/OfflineSymbolResolverLibBacktrace.cpp @@ -0,0 +1,261 @@ +#if defined(LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT) && !defined(_WIN32) + +#include "OfflineSymbolResolver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// include lib backtrace code. TODO: move to it's own file! +#define TRACY_LIBBACKTRACE_ELF_DYNLOAD_SUPPORT +#include "../../public/libbacktrace/alloc.cpp" +#include "../../public/libbacktrace/dwarf.cpp" +#include "../../public/libbacktrace/fileline.cpp" +#include "../../public/libbacktrace/mmapio.cpp" +#include "../../public/libbacktrace/posix.cpp" +#include "../../public/libbacktrace/sort.cpp" +#include "../../public/libbacktrace/state.cpp" +#include "../../public/libbacktrace/elf.cpp" + +//#define VERBOSE_LOG + +using namespace tracy; + +// Structure to hold search parameters for dl_iterate_phdr +struct FindBaseAddressData { + const char* so_path; + uintptr_t base_address = 0; +}; + +// dl_iterate_phdr callback to find the base address of the shared object +int find_base_address_callback(struct dl_phdr_info* info, size_t, void* data) { + auto* find_data = static_cast(data); + if (strstr(info->dlpi_name, find_data->so_path) != nullptr) { + find_data->base_address = info->dlpi_addr; + return 1; // Stop iterating + } + return 0; // Continue iterating +} + +// Get the base address of the shared object +uintptr_t get_so_base_address(const char* so_path) { + FindBaseAddressData data{so_path, 0}; + dl_iterate_phdr(find_base_address_callback, &data); + + if (data.base_address == 0) + { + std::cerr << "Failed to find base address of '" << so_path << "'" << std::endl; + } + return data.base_address; +} + +// demangling +constexpr size_t demangle_buffer_len = 1024*1024; +char demangle_buffer[demangle_buffer_len]; +const char* demangle( const char* mangled ) +{ + if( !mangled || mangled[0] != '_' ) + return nullptr; + if( strlen( mangled ) > demangle_buffer_len ) + return nullptr; + int status; + size_t len = demangle_buffer_len; + return abi::__cxa_demangle( mangled, demangle_buffer, &len, &status ); +} + +struct tracy::backtrace_state* backtraceState = nullptr; + +void* dlopen_wrapper(const char* path, int flag) { + // Buffer to hold the resolved path + char resolved_path[PATH_MAX]; + + // Resolve the symbolic link to its real path + if (realpath(path, resolved_path) == NULL) + { + std::cerr << "Error resolving real path: '" << path << "'" << std::endl; + return nullptr; + } + + // Call dlopen with the resolved path + void* handle = dlopen(resolved_path, flag); + if (!handle) + { + std::cerr << "Error loading library: '" << resolved_path << "', error: '" << dlerror() << "'" << std::endl; + } + else + { + std::cerr << "dlopen ok for: '" << resolved_path << "'" << std::endl; + } + + return handle; +} + +// Helper to reolve symbols for a shared lib +struct BacktraceSoResolver +{ + struct SymbolInfo + { + std::string function_name; + std::string file_name; + int line_number; + bool resolved = false; + }; + + BacktraceSoResolver(const char* so_path) + { + // Load the shared object, we need it's base address to use our offsets off it + handle = dlopen_wrapper(so_path, RTLD_LAZY | RTLD_LOCAL); + if (!handle) + { + std::cerr << "Failed to load SO:' " << so_path << std::endl; + return; + } + + base_address = get_so_base_address(so_path); + #ifdef VERBOSE_LOG + std::cout << "Library: " << so_path << std::endl; + std::cout << "Base Address: 0x" << std::hex << base_address << std::dec << std::endl; + #endif //#ifdef VERBOSE_LOG + + if (!backtraceState) + { + backtraceState = backtrace_create_state(nullptr, 0, errorCallback, nullptr); + if (!backtraceState) + { + std::cerr << "Failed to initialize backtrace state" << std::endl; + return; + } + } + } + + ~BacktraceSoResolver() + { + if(handle) + { + //dlclose(handle); + } + } + + static bool resolveSymbolsForImage( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ) + { + resolvedEntries.clear(); + BacktraceSoResolver resolverForSo(imagePath.c_str()); + + for ( const FrameEntry& entry : inputEntryList) + { + BacktraceSoResolver::SymbolInfo symbol; + const bool resolved = resolverForSo.resolveSymbol(entry.symbolOffset, symbol); + + SymbolEntry newEntry; + newEntry.resolved = resolved; + if(resolved) + { + const char* demangledVersion = demangle(symbol.function_name.c_str()); + newEntry.name = demangledVersion ? demangledVersion : symbol.function_name; + newEntry.file = symbol.file_name; + newEntry.line = symbol.line_number; + #ifdef VERBOSE_LOG + std::cout << "resolved symbol: 0x" << std::hex << entry.symbolOffset << std::dec << " of '" << imagePath << "'" + << ", function: '" << newEntry.name << "' | '" << newEntry.file << "':" << newEntry.line << std::endl; + #endif //#ifdef VERBOSE_LOG + } + else + { + #ifdef VERBOSE_LOG + std::cout << "failed to resolve symbol: 0x" << std::hex << entry.symbolOffset << std::dec << " of '" + << imagePath << "'" << std::endl; + #endif //#ifdef VERBOSE_LOG + } + + resolvedEntries.push_back( std::move(newEntry) ); + } + + return true; + } + +private: + + bool resolveSymbol(uintptr_t address, SymbolInfo& info) + { + if (!backtraceState) + { + std::cerr << "Backtrace was not initialized!\n"; + return false; + } + + uintptr_t absolute_address = base_address + address; + backtrace_pcinfo(backtraceState, absolute_address, symbolCallback, errorCallback, &info); + + if (!info.resolved) + { + // Use dladdr to get function name + Dl_info dl_info; + if (dladdr((void*)absolute_address, &dl_info)) + { + if (dl_info.dli_sname) + { + info.function_name = dl_info.dli_sname; + info.resolved = true; + } + #ifdef VERBOSE_LOG + std::cout << "dladdr Symbol: " << (dl_info.dli_sname ? dl_info.dli_sname : "Unknown") << std::endl; + #endif //#ifdef VERBOSE_LOG + } + else + { + #ifdef VERBOSE_LOG + std::cerr << "dladdr failed to find function name" << std::endl; + #endif //#ifdef VERBOSE_LOG + } + } + + return info.resolved; + } + + // Callback function for symbol resolution + static int symbolCallback(void* data, uintptr_t pc, uintptr_t lowaddr, const char* filename, int lineno, const char* function ) + { + #ifdef VERBOSE_LOG + std::cout << pc << "| 0x" << std::hex << lowaddr << std::dec + << "|" << (filename?filename:"") << "|" << lineno << "|" << (function?function:"") << std::endl; + #endif //#ifdef VERBOSE_LOG + SymbolInfo* info = static_cast(data); + if (function) info->function_name = function; + if (filename) info->file_name = filename; + info->line_number = lineno; + info->resolved = (function != nullptr); + return 1; + } + + // Error callback + static void errorCallback(void* /*data*/, const char* msg, int errnum) + { + std::cerr << "libbacktrace error: " << msg << " (Error Code: " << errnum << ")\n"; + } + + void* handle = nullptr; + uintptr_t base_address = 0x0; +}; + +bool ResolveSymbolsWithLibBacktrace( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ) +{ + return BacktraceSoResolver::resolveSymbolsForImage( imagePath, inputEntryList, resolvedEntries ); +} + +#endif // #if defined(LIBBACKTRACE_OFFLINE_SYMBOL_RESOLUTION_SUPPORT) && !defined(_WIN32) \ No newline at end of file diff --git a/update/src/OfflineSymbolResolverLibdw.cpp b/update/src/OfflineSymbolResolverLibdw.cpp new file mode 100644 index 0000000000..829ed3a1ac --- /dev/null +++ b/update/src/OfflineSymbolResolverLibdw.cpp @@ -0,0 +1,389 @@ +#if defined(LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT) && !defined(_WIN32) + +#include "OfflineSymbolResolver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// libdw +#include +#include +#include + +static char *debuginfo_path = nullptr; +static const Dwfl_Callbacks offline_callbacks = { + // We use this table for core files too. + .find_elf = dwfl_build_id_find_elf, + .find_debuginfo = dwfl_standard_find_debuginfo, + .section_address = dwfl_offline_section_address, + .debuginfo_path = &debuginfo_path, +}; + +// Helper to reolve symbols for a shared lib +struct LibDWResolver +{ +public: + static bool resolveSymbolsForImage( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ) + { + resolvedEntries.clear(); + LibDWResolver resolverForSo(imagePath.c_str()); + + for ( const FrameEntry& entry : inputEntryList) + { + LibDWResolver::SymbolInfo symbol; + const bool resolved = resolverForSo.resolveSymbol(entry.symbolOffset, symbol); + + SymbolEntry newEntry; + newEntry.resolved = resolved; + if(resolved) + { + const char* demangledVersion = demangle(symbol.function_name.c_str()); + newEntry.name = demangledVersion ? demangledVersion : symbol.function_name; + newEntry.file = symbol.file_name; + newEntry.line = symbol.line_number; + #ifdef VERBOSE_LOG + std::cout << "LIBDW: resolved symbol: 0x" << std::hex << entry.symbolOffset << std::dec << " of '" << imagePath << "'" + << ", function: '" << newEntry.name << "' | '" << newEntry.file << "':" << newEntry.line << std::endl; + #endif //#ifdef VERBOSE_LOG + } + else + { + newEntry.name = "[unknown] + " + std::to_string(entry.symbolOffset); + + #ifdef VERBOSE_LOG + std::cout << "LIBDW: failed to resolve symbol: 0x" << std::hex << entry.symbolOffset << std::dec << " of '" + << imagePath << "'" << std::endl; + #endif //#ifdef VERBOSE_LOG + } + + resolvedEntries.push_back( std::move(newEntry) ); + } + + return true; + } + +private: + Dwfl *dwfl = nullptr; + + struct SymbolInfo + { + std::string function_name; + std::string file_name; + int line_number; + }; + + static void print_dwfl_error(const char *msg) { + auto err = dwfl_errno(); + std::cerr << msg << ": " << dwfl_errmsg(err) << std::endl; + } + + // c++ name demangling + static const char* demangle( const char* mangled ) + { + constexpr size_t demangle_buffer_len = 1024*1024; + static char demangle_buffer[demangle_buffer_len]; + + if( !mangled || mangled[0] != '_' ) + return nullptr; + if( strlen( mangled ) > demangle_buffer_len ) + return nullptr; + int status; + size_t len = demangle_buffer_len; + return abi::__cxa_demangle( mangled, demangle_buffer, &len, &status ); + } + + LibDWResolver(const char* so_path) + { + dwfl = dwfl_begin(&offline_callbacks); + if (!dwfl) + { + print_dwfl_error("dwfl_begin() failed"); + return; + } + + if (!dwfl_report_elf(dwfl, so_path, so_path, -1, 0, 0)) + { + print_dwfl_error("dwfl_report_offline() failed"); + return; + } + + if (dwfl_report_end(dwfl, NULL, NULL)) + { + print_dwfl_error("dwfl_report_end() failed"); + return; + } + + Dwfl_Module *mod = dwfl_addrmodule(dwfl, 0x0); + buildExportedSymbolsList(mod); + } + + ~LibDWResolver() + { + if (dwfl) + { + dwfl_end(dwfl); + } + } + + bool resolveSymbol(uintptr_t offset, SymbolInfo& info) + { + if (!dwfl) + { + return false; + } + + Dwfl_Module *mod = dwfl_addrmodule(dwfl, offset); + if (!mod) + { + print_dwfl_error("dwfl_addrmodule() failed"); + return false; + } + + Dwarf_Addr bias{}; + dwfl_module_getdwarf(mod, &bias); + Dwarf_Addr addr = offset + bias; + + // do full resolve first using DWARF symbol info + if (resolveWithDebugSymbols(mod, addr, info)) + { + return true; + } + // fallback to ELF exported symbols + else if (getFunctionNameFromElfExportedSymbols(mod, offset, info)) + { + return true; + } + return false; + } + + // full DWARD debug info symbol decoding (including inlines) + bool resolveWithDebugSymbols(Dwfl_Module *mod, Dwarf_Addr addr, SymbolInfo& info) + { + const char* function_name = nullptr; + const char* file_name = nullptr; + int line_number = 0; + + if (resolveSymbolFromSymtab(mod, addr, function_name, file_name, line_number)) + { + //getInlineSymbol(mod, addr, function_name, file_name, line_number); + } + + if (function_name) + { + info.function_name = function_name; + if(file_name) + info.file_name = file_name; + info.line_number = (unsigned int)line_number; + return true; + } + + return false; + } + + // match exported symbols ex: .symtab + bool resolveSymbolFromSymtab(Dwfl_Module *mod, Dwarf_Addr addr, + const char*& function_name, const char*& file_name, + int& line_number) + { + function_name = dwfl_module_addrname(mod, addr); + if (function_name) + { + Dwfl_Line* line = dwfl_module_getsrc(mod, addr); + if (!line) + { + line = dwfl_getsrc(dwfl, addr); + } + if (line) + { + file_name = dwfl_lineinfo(line, nullptr, &line_number, nullptr, nullptr, nullptr); + } + return true; + } + return false; + } + + // match exported symbols ex: .symtab + bool resolveSymbolFromSymtab(Dwfl_Module *mod, Dwarf_Addr addr, SymbolInfo& info) + { + const char* funcname = nullptr; + const char *filename = nullptr; + int line_number = 0; + if (resolveSymbolFromSymtab(mod, addr, funcname, filename, line_number)) + { + info.function_name = funcname; + if(filename) + info.file_name = filename; + info.line_number = (unsigned int)line_number; + return true; + } + return false; + } + + // get inlined symbols + bool getInlineSymbol(Dwfl_Module *mod, Dwarf_Addr addr, + const char*& function_name, const char*& file_name, + int& line_number) + { + Dwarf_Addr bias = 0; + Dwarf_Die* cudie = dwfl_module_addrdie(mod, addr, &bias); + + // This function retrieves the scopes (DIEs) that are relevant to a specific address. + // Scopes can include functions, lexical blocks, inlined subroutines, + // and other constructs that define a range of addresses in the code. + Dwarf_Die* scopes = nullptr; + int nscopes = dwarf_getscopes(cudie, addr - bias, &scopes); + if (nscopes <= 0) + return false; + + Dwarf_Die subroutine; + Dwarf_Off dieoff = dwarf_dieoffset(&scopes[0]); + dwarf_offdie(dwfl_module_getdwarf(mod, &bias), dieoff, &subroutine); + free(scopes); + scopes = nullptr; + + nscopes = dwarf_getscopes_die(&subroutine, &scopes); + if (nscopes <= 1) + { + free(scopes); + return false; + } + + Dwarf_Die cu; + Dwarf_Files *files; + if (dwarf_diecu(&scopes[0], &cu, nullptr, nullptr) == nullptr || + dwarf_getsrcfiles(cudie, &files, nullptr) != 0) + { + free(scopes); + return false; + } + + for (int i = 0; i < nscopes - 1; i++) + { + Dwarf_Word val; + Dwarf_Attribute attr; + Dwarf_Die *die = &scopes[i]; + if (dwarf_tag(die) != DW_TAG_inlined_subroutine) + continue; + + // Search for the parent inline or function. + // It might not be directly above this inline -- e.g. there could be a lexical_block in between. + for (int j = i + 1; j < nscopes; j++) + { + Dwarf_Die *parent = &scopes[j]; + int tag = dwarf_tag(parent); + if (tag == DW_TAG_inlined_subroutine || tag == DW_TAG_entry_point || tag == DW_TAG_subprogram) + { + Dwarf_Attribute attr; + function_name = dwarf_formstring(dwarf_attr_integrate(die, DW_AT_linkage_name, &attr)); + break; + } + } + + if (function_name) + { + if (dwarf_formudata(dwarf_attr(die, DW_AT_call_file, &attr), &val) == 0) + file_name = dwarf_filesrc(files, val, NULL, NULL); + if (dwarf_formudata(dwarf_attr(die, DW_AT_call_line, &attr), &val) == 0) + line_number = val; + + free(scopes); + return true; + } + } + free(scopes); + return false; + } + + struct ExportedSymbolInfo { + const char* name; + uintptr_t address; + size_t size; + }; + std::vector symbols_; + + bool getFunctionNameFromElfExportedSymbols(Dwfl_Module *mod, uintptr_t offset, SymbolInfo& info) + { + ExportedSymbolInfo* exportedSymbol = getExportedSymbolByAddress(offset); + if(exportedSymbol) + { + //std::cout << "Found matching sym: '" << exportedSymbol->name << "':" << std::hex << "0x" << exportedSymbol->address << "-" + // << "0x" << (exportedSymbol->address + exportedSymbol->size) << "'" << std::dec << std::endl; + info.function_name = exportedSymbol->name; + return true; + } + return false; + } + + void buildExportedSymbolsList(Dwfl_Module *mod) + { + symbols_.clear(); + + int sym_idx = 0; + GElf_Sym sym; + const char* symName = nullptr; + while ((symName = dwfl_module_getsym(mod, sym_idx++, &sym, nullptr)) != nullptr) + { + // Only consider function and object symbols + unsigned char sym_type = GELF_ST_TYPE(sym.st_info); + if ( (sym_type == STT_FUNC || sym_type == STT_OBJECT) && + sym.st_value != 0x0 && sym.st_size != 0x0) + { + //std::cout << "sym: '" << symName << "':" << std::hex << "0x" << sym.st_value << "-" + // << "0x" << (sym.st_value + sym.st_size) << "'" << std::endl; + + ExportedSymbolInfo newSymbol{symName, uintptr_t(sym.st_value), size_t(sym.st_size)}; + symbols_.push_back(newSymbol); + } + } + + std::sort(symbols_.begin(), symbols_.end(), [](const ExportedSymbolInfo &a, const ExportedSymbolInfo &b) { + return a.address < b.address; + }); + } + + // use the exported symbols start addresses as the sizes generally are not right, so we match a symbol that falls in + // between the adress start range of 2 consecutive symbols + ExportedSymbolInfo* getExportedSymbolByAddress(uintptr_t offset) + { + auto it = std::lower_bound(symbols_.begin(), symbols_.end(), offset, [](const ExportedSymbolInfo &symbol, uintptr_t offset) { + return symbol.address < offset; + }); + + if (it != symbols_.begin()) + { + --it; + if (offset >= it->address ) + { + return &(*it); + } + } + return nullptr; + } +}; + +bool ResolveSymbolsWithLibDW( const std::string& imagePath, const FrameEntryList& inputEntryList, + SymbolEntryList& resolvedEntries ) +{ + return LibDWResolver::resolveSymbolsForImage( imagePath, inputEntryList, resolvedEntries ); +} + +#endif // #ifdef LIBDW_OFFLINE_SYMBOL_RESOLUTION_SUPPORT && !defined(_WIN32) \ No newline at end of file diff --git a/update/src/update.cpp b/update/src/update.cpp index a9cb625edd..0635a6a83d 100644 --- a/update/src/update.cpp +++ b/update/src/update.cpp @@ -37,6 +37,18 @@ void Usage() printf( " -c: scan for source files missing in cache and add if found\n" ); printf( " -r: resolve symbols and patch callstack frames\n"); printf( " -p: substitute symbol resolution path with an alternative: \"REGEX_MATCH;REPLACEMENT\"\n"); + printf( " -x: skip resolution for matching path: \"REGEX_MATCH\"\n" ); + + printf( " -n: choose the symbol resolver to use (" ); + std::string defaultResolver = GetDefaultOfflineSymbolResolver(); + for(int i = 0; i < GetOfflineSymbolResolverCount(); ++i) + { + const char* resolverName = GetOfflineSymbolResolverName(i); + printf( "%s\"%s\"%s", (i ? ", ":""), resolverName, (defaultResolver == resolverName) ? "*":""); + } + printf( ")\n" ); + + printf( " -m: the max parallelism to run the symbol resolver (-1 to use all cores)\n" ); printf( " -j: number of threads to use for compression (-1 to use all cores)\n" ); exit( 1 ); @@ -60,9 +72,12 @@ int main( int argc, char** argv ) bool cacheSource = false; bool resolveSymbols = false; std::vector pathSubstitutions; + std::vector skipImageList; + std::string resolver = GetDefaultOfflineSymbolResolver(); + int resolverMaxParallelism = -1; int c; - while( ( c = getopt( argc, argv, "4hez:ds:crp:j:" ) ) != -1 ) + while( ( c = getopt( argc, argv, "4hez:ds:crpx:n:m:j:" ) ) != -1 ) { switch( c ) { @@ -138,6 +153,15 @@ int main( int argc, char** argv ) case 'p': pathSubstitutions.push_back(optarg); break; + case 'x': + skipImageList.push_back( optarg ); + break; + case 'n': + resolver = optarg; + break; + case 'm': + resolverMaxParallelism = atoi( optarg ); + break; case 'j': streams = atoi( optarg ); break; @@ -179,7 +203,16 @@ int main( int argc, char** argv ) const auto t1 = std::chrono::high_resolution_clock::now(); if( cacheSource ) worker.CacheSourceFiles(); - if( resolveSymbols ) PatchSymbols( worker, pathSubstitutions ); + if( resolveSymbols ) + { + ResolveOptions options = {}; + options.verbose = false; + options.resolver = resolver; + options.maxParallelism = resolverMaxParallelism; + + printf( "Attempting to resolve symbols offline with the '%s' resolver...\r", resolver.c_str()); + PatchSymbols( worker, pathSubstitutions, skipImageList, options); + } auto w = std::unique_ptr( tracy::FileWrite::Open( output, clev, zstdLevel, streams ) ); if( !w )