Allow offline symbol resolution with any addr2line-compatible tool

The addr2line backend of tracy-update now builds on every platform, including Windows, and can be pointed at any addr2line-compatible executable:

- `-a`: path to a custom symbol resolution tool (e.g. `llvm-addr2line` or a cross-compilation toolchain's `addr2line`). Works on all platforms and takes precedence over the platform default (DbgHelp on Windows, the `addr2line` found in `PATH` elsewhere). Path-like values are validated up front so a wrong path fails with an actionable message instead of a cryptic, localized shell error.
- `-A`: extra arguments passed verbatim to the tool, e.g. `--relative-address` so `llvm-addr2line`/`llvm-symbolizer` accept the image-relative offsets Tracy records for images with a non-zero preferred base (PE, Mach-O).
- `-v`: verbose output while patching symbols.
This commit is contained in:
Clément Grégoire
2026-06-04 12:26:01 +02:00
parent f5526d01a2
commit 2b11785b05
6 changed files with 148 additions and 23 deletions

View File

@@ -2041,6 +2041,20 @@ filesystem setup as the one used to run the tracy instrumented application).
You can do path substitution with the \texttt{-p} option to perform any number of path
substitions in order to use symbols located elsewhere.
By default symbol resolution is performed with the platform's native facility: the DbgHelp
library on Windows, and the \texttt{addr2line} tool found in \texttt{PATH} elsewhere. You can
override this with the \texttt{-a} option, passing the path to a custom
\texttt{addr2line}-compatible tool (for instance an \texttt{addr2line} from a cross-compilation
toolchain, or \texttt{llvm-addr2line}). The \texttt{-a} option works on all platforms, including
Windows, and takes precedence over the platform default.
Extra arguments can be passed verbatim to the resolution tool with the \texttt{-A} option. Tracy
records callstack frame offsets relative to the image base, but \texttt{addr2line}-compatible
tools expect a full virtual address for images that have a non-zero preferred image base (such as
PE on Windows or Mach-O on Apple). For these, pass \texttt{-A "--relative-address"} so that
\texttt{llvm-addr2line} or \texttt{llvm-symbolizer} adds the image base back. ELF images need no
such adjustment.
\begin{bclogo}[
noborder=true,
couleur=black!5,

View File

@@ -11,6 +11,22 @@
#include "OfflineSymbolResolver.h"
bool ResolveSymbols( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
#ifdef _WIN32
// On Windows the default (no custom tool given) is the DbgHelp backend.
if( addr2lineToolPath.empty() )
{
return ResolveSymbolsDbgHelp( imagePath, inputEntryList, resolvedEntries );
}
#endif
// Everywhere else, and whenever a custom tool is given, use the addr2line-compatible backend.
// An empty path lets that backend fall back to the 'addr2line' found in PATH.
return ResolveSymbolsAddr2Line( addr2lineToolPath, addr2lineArgs, imagePath, inputEntryList, resolvedEntries );
}
bool ApplyPathSubstitutions( std::string& path, const PathSubstitutionList& pathSubstitutionlist )
{
for( const auto& substitution : pathSubstitutionlist )
@@ -31,7 +47,8 @@ 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 )
bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstitutionlist,
const std::string& addr2lineToolPath, const std::string& addr2lineArgs, bool verbose )
{
uint64_t callstackFrameCount = worker.GetCallstackFrameCount();
std::string relativeSoNameMatch = "[unresolved]";
@@ -91,7 +108,7 @@ bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& p
}
SymbolEntryList resolvedEntries;
ResolveSymbols( imagePath, entries, resolvedEntries );
ResolveSymbols( addr2lineToolPath, addr2lineArgs, imagePath, entries, resolvedEntries );
if( resolvedEntries.size() != entries.size() )
{
@@ -131,7 +148,8 @@ bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& p
return true;
}
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings, bool verbose )
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings,
const std::string& addr2lineToolPath, const std::string& addr2lineArgs, bool verbose )
{
std::cout << "Resolving and patching symbols..." << std::endl;
@@ -160,7 +178,7 @@ void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSu
}
}
if ( !PatchSymbolsWithRegex(worker, pathSubstitutionList, verbose) )
if ( !PatchSymbolsWithRegex(worker, pathSubstitutionList, addr2lineToolPath, addr2lineArgs, verbose) )
{
std::cerr << "Failed to patch symbols" << std::endl;
}

View File

@@ -29,12 +29,35 @@ struct SymbolEntry
using SymbolEntryList = std::vector<SymbolEntry>;
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
// Dispatches to the appropriate backend depending on the platform and whether a custom
// addr2line-compatible tool was specified. When addr2lineToolPath is non-empty, the tool at
// that path is invoked (on any platform); otherwise the platform default is used (DbgHelp on
// Windows, the 'addr2line' found in PATH elsewhere). addr2lineArgs are extra arguments passed
// verbatim to the addr2line-compatible tool (e.g. "--relative-address").
bool ResolveSymbols( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries );
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings, bool verbose = false );
// Backend invoking an addr2line-compatible tool. Available on all platforms. An empty
// addr2lineToolPath falls back to the 'addr2line' found in PATH. addr2lineArgs are inserted
// verbatim into the tool's command line.
bool ResolveSymbolsAddr2Line( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries );
#ifdef _WIN32
// Backend using the Windows DbgHelp library.
bool ResolveSymbolsDbgHelp( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries );
#endif
void PatchSymbols( tracy::Worker& worker, const std::vector<std::string>& pathSubstitutionsStrings,
const std::string& addr2lineToolPath = std::string(),
const std::string& addr2lineArgs = std::string(), bool verbose = false );
using PathSubstitutionList = std::vector<std::pair<std::regex, std::string> >;
bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstituionlist, bool verbose = false );
bool PatchSymbolsWithRegex( tracy::Worker& worker, const PathSubstitutionList& pathSubstituionlist,
const std::string& addr2lineToolPath = std::string(),
const std::string& addr2lineArgs = std::string(), bool verbose = false );
#endif // __SYMBOLRESOLVER_HPP__

View File

@@ -1,5 +1,3 @@
#ifndef _WIN32
#include "OfflineSymbolResolver.h"
#include <fstream>
@@ -10,6 +8,11 @@
#include <memory>
#include <stdio.h>
#ifdef _WIN32
# define popen _popen
# define pclose _pclose
#endif
std::string ExecShellCommand( const char* cmd )
{
std::array<char, 128> buffer;
@@ -29,23 +32,65 @@ std::string ExecShellCommand( const char* cmd )
class SymbolResolver
{
public:
SymbolResolver()
SymbolResolver( const std::string& addr2lineToolPath, const std::string& addr2lineArgs )
{
// Extra arguments are inserted verbatim into the tool invocation. Tracy records frame
// offsets as RVAs; for images with a non-zero preferred image base (PE, Mach-O) the user
// can pass "--relative-address" here so llvm-addr2line / llvm-symbolizer add the base back.
if( !addr2lineArgs.empty() )
{
m_addr2LineArgs = " " + addr2lineArgs;
}
if( !addr2lineToolPath.empty() )
{
// If the value looks like a path (not a bare command name resolved via PATH), verify
// it exists so a wrong path fails with an actionable error instead of a cryptic shell one.
const bool looksLikePath = addr2lineToolPath.find( '/' ) != std::string::npos ||
addr2lineToolPath.find( '\\' ) != std::string::npos;
if( looksLikePath && !std::ifstream( addr2lineToolPath ).good() )
{
std::cerr << "Specified symbol resolution tool not found: '" << addr2lineToolPath
<< "' (check the path passed to the '-a' option)" << std::endl;
return;
}
m_addr2LinePath = addr2lineToolPath;
std::cout << "Using user-specified symbol resolution tool: '" << m_addr2LinePath.c_str() << "'" << std::endl;
return;
}
#ifdef _WIN32
std::cerr << "No symbol resolution tool specified (use the '-a' option to provide one)" << std::endl;
#else
std::stringstream result( ExecShellCommand("which addr2line") );
std::getline(result, m_addr2LinePath);
if( !m_addr2LinePath.length() )
{
std::cerr << "'addr2line' was not found in the system, please installed it" << std::endl;
std::cerr << "'addr2line' was not found in the system, please install it" << std::endl;
}
else
{
std::cout << "Using 'addr2line' found at: '" << m_addr2LinePath.c_str() << "'" << std::endl;
}
#endif
}
static void escapeShellParam(std::string const& s, std::string& out)
{
#ifdef _WIN32
// cmd.exe / the CRT command parser do not understand POSIX backslash escapes, and
// backslashes are path separators on Windows. Wrap the parameter in double quotes
// (which handles spaces) and drop any embedded quotes, which cannot appear in a path.
out.reserve( s.size() + 2 );
out.push_back( '"' );
for( char c : s )
{
if( c != '"' ) out.push_back( c );
}
out.push_back( '"' );
#else
out.reserve( s.size() + 2 );
out.push_back( '"' );
for( unsigned char c : s )
@@ -73,13 +118,14 @@ public:
}
}
out.push_back( '"' );
#endif
}
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
if( !m_addr2LinePath.length() ) return false;
std:: string escapedPath;
escapeShellParam( imagePath, escapedPath );
@@ -93,14 +139,22 @@ public:
// generate a single addr2line cmd line for all addresses in one invocation
std::stringstream ss;
ss << m_addr2LinePath << " -C -f -e " << escapedPath << " -a ";
ss << m_addr2LinePath << " -C -f" << m_addr2LineArgs << " -e " << escapedPath << " -a ";
for( ; entryIdx < batchEndIdx; entryIdx++ )
{
const FrameEntry& entry = inputEntryList[entryIdx];
ss << " 0x" << std::hex << entry.symbolOffset;
}
std::string resultStr = ExecShellCommand( ss.str().c_str() );
std::string cmd = ss.str();
#ifdef _WIN32
// _popen runs the command through 'cmd.exe /c', which strips the outermost pair of
// quotes. Wrap the whole command so the quoting around the (possibly spaced) tool
// and image paths survives.
cmd = "\"" + cmd + "\"";
#endif
std::string resultStr = ExecShellCommand( cmd.c_str() );
std::stringstream result( resultStr );
//printf("executing: '%s' got '%s'\n", ss.str().c_str(), result.str().c_str());
@@ -147,13 +201,13 @@ public:
private:
std::string m_addr2LinePath;
std::string m_addr2LineArgs;
};
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
bool ResolveSymbolsAddr2Line( const std::string& addr2lineToolPath, const std::string& addr2lineArgs,
const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
static SymbolResolver symbolResolver;
static SymbolResolver symbolResolver( addr2lineToolPath, addr2lineArgs );
return symbolResolver.ResolveSymbols( imagePath, inputEntryList, resolvedEntries );
}
#endif // #ifndef _WIN32

View File

@@ -122,8 +122,8 @@ private:
char SymbolResolver::s_symbolResolutionBuffer[symbolResolutionBufferSize];
bool ResolveSymbols( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
bool ResolveSymbolsDbgHelp( const std::string& imagePath, const FrameEntryList& inputEntryList,
SymbolEntryList& resolvedEntries )
{
static SymbolResolver resolver;
return resolver.ResolveSymbolsForModule( imagePath, inputEntryList, resolvedEntries );

View File

@@ -39,6 +39,10 @@ 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( " -a: path to a custom addr2line-compatible tool to use for symbol resolution\n");
printf( " -A: extra arguments passed verbatim to the symbol resolution tool,\n");
printf( " e.g. \"--relative-address\" for llvm-addr2line on PE/Mach-O images\n");
printf( " -v: verbose output while resolving symbols\n");
printf( " -j: number of threads to use for compression (-1 to use all cores)\n" );
exit( 1 );
@@ -62,9 +66,12 @@ int main( int argc, char** argv )
bool cacheSource = false;
bool resolveSymbols = false;
std::vector<std::string> pathSubstitutions;
std::string addr2lineToolPath;
std::string addr2lineArgs;
bool verboseSymbols = false;
int c;
while( ( c = getopt( argc, argv, "4hez:ds:crp:j:" ) ) != -1 )
while( ( c = getopt( argc, argv, "4hez:ds:crp:a:A:vj:" ) ) != -1 )
{
switch( c )
{
@@ -140,6 +147,15 @@ int main( int argc, char** argv )
case 'p':
pathSubstitutions.push_back(optarg);
break;
case 'a':
addr2lineToolPath = optarg;
break;
case 'A':
addr2lineArgs = optarg;
break;
case 'v':
verboseSymbols = true;
break;
case 'j':
streams = atoi( optarg );
break;
@@ -181,7 +197,7 @@ 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 ) PatchSymbols( worker, pathSubstitutions, addr2lineToolPath, addr2lineArgs, verboseSymbols );
auto w = std::unique_ptr<tracy::FileWrite>( tracy::FileWrite::Open( output, clev, zstdLevel, streams ) );
if( !w )