diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 7cac5d8f0..41b8f41e7 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -16,6 +16,7 @@
+
diff --git a/src/PerfView/PerfView.csproj b/src/PerfView/PerfView.csproj
index aa2492bb7..e4dacf3b4 100644
--- a/src/PerfView/PerfView.csproj
+++ b/src/PerfView/PerfView.csproj
@@ -119,6 +119,7 @@
HeapDump dependencies are pulled from the HeapDump output directory because HeapDump runs out of process
and can have a different set of dependencies.
-->
+
@@ -411,6 +412,13 @@
Microsoft.Diagnostics.FastSerialization.dll
False
+
+ Non-Resx
+ false
+ .\Microsoft.Bcl.HashCode.dll
+ Microsoft.Bcl.HashCode.dll
+ False
+
Non-Resx
false
diff --git a/src/TraceEvent/EventPipe/EventPipeEventSource.cs b/src/TraceEvent/EventPipe/EventPipeEventSource.cs
index 24bdb382e..af9e855c3 100644
--- a/src/TraceEvent/EventPipe/EventPipeEventSource.cs
+++ b/src/TraceEvent/EventPipe/EventPipeEventSource.cs
@@ -240,6 +240,10 @@ internal void ReadTraceBlockV6OrGreater(Block block)
{
_expectedCPUSamplingRate = intVal3;
}
+ else if (key == "SystemPageSize" && ulong.TryParse(value, out ulong ulongVal) && ulongVal > 0)
+ {
+ _systemPageSize = ulongVal;
+ }
}
}
@@ -656,6 +660,7 @@ private DynamicTraceEventData CreateTemplate(EventPipeMetadata metadata)
private int _lastLabelListId;
internal int _processId;
internal int _expectedCPUSamplingRate;
+ internal ulong _systemPageSize;
private RewindableStream _stream;
private bool _isStreaming;
private ThreadCache _threadCache;
diff --git a/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs b/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs
index f39c478ad..307da3edd 100644
--- a/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs
+++ b/src/TraceEvent/Parsers/UniversalSystemTraceEventParser.cs
@@ -481,8 +481,10 @@ internal sealed class ELFProcessMappingSymbolMetadata : ProcessMappingSymbolMeta
[JsonPropertyName("build_id")]
public string BuildId { get; set; }
[JsonPropertyName("p_vaddr")]
+ [JsonConverter(typeof(HexUInt64Converter))]
public ulong VirtualAddress { get; set; }
[JsonPropertyName("p_offset")]
+ [JsonConverter(typeof(HexUInt64Converter))]
public ulong FileOffset { get; set; }
}
@@ -535,4 +537,43 @@ public override void Write(Utf8JsonWriter writer, ProcessMappingSymbolMetadata v
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
+
+ internal class HexUInt64Converter : JsonConverter
+ {
+ public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ return reader.GetUInt64();
+ }
+
+ if (reader.TokenType != JsonTokenType.String)
+ {
+ throw new JsonException($"Cannot convert token of type {reader.TokenType} to ulong.");
+ }
+
+ string text = reader.GetString();
+ if (text != null && text.StartsWith("0x", StringComparison.OrdinalIgnoreCase) && text.Length > 2)
+ {
+ if (ulong.TryParse(text.Substring(2), System.Globalization.NumberStyles.HexNumber,
+ System.Globalization.CultureInfo.InvariantCulture, out ulong hexValue))
+ {
+ return hexValue;
+ }
+ }
+
+ if (ulong.TryParse(text, System.Globalization.NumberStyles.Integer,
+ System.Globalization.CultureInfo.InvariantCulture, out ulong value))
+ {
+ return value;
+ }
+
+ throw new JsonException($"Cannot convert \"{text}\" to ulong.");
+ }
+
+ public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue("0x" + value.ToString("X"));
+ }
+ }
}
diff --git a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs
index f6f686f7e..846043bf7 100644
--- a/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs
+++ b/src/TraceEvent/SourceConverters/NettraceUniversalConverter.cs
@@ -27,6 +27,15 @@ public static void RegisterParsers(TraceLog traceLog)
public void BeforeProcess(TraceLog traceLog, TraceEventDispatcher source)
{
+ // Extract system page size for ELF RVA calculations.
+ if (source is EventPipeEventSource eventPipeSource)
+ {
+ eventPipeSource.HeadersDeserialized += delegate ()
+ {
+ traceLog.systemPageSize = eventPipeSource._systemPageSize;
+ };
+ }
+
UniversalSystemTraceEventParser universalSystemParser = new UniversalSystemTraceEventParser(source);
universalSystemParser.ExistingProcess += delegate (ProcessCreateTraceData data)
{
diff --git a/src/TraceEvent/Symbols/ElfSymbolModule.cs b/src/TraceEvent/Symbols/ElfSymbolModule.cs
index 3ab108767..c75a79895 100644
--- a/src/TraceEvent/Symbols/ElfSymbolModule.cs
+++ b/src/TraceEvent/Symbols/ElfSymbolModule.cs
@@ -1,8 +1,12 @@
using System;
+using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Text;
+using System.Threading;
namespace Microsoft.Diagnostics.Symbols
{
@@ -85,24 +89,439 @@ public string FindNameForRva(uint rva, ref uint symbolStart)
{
symbolStart = m_symbols[hi].Start;
- // Lazy name decode on first access.
- if (m_symbols[hi].Name == null)
+ // Thread-safe lazy name decode on first access.
+ if (Volatile.Read(ref m_symbolNames[hi]) == null)
{
string name = ReadNullTerminatedString(m_strtab, m_symbols[hi].StrtabOffset);
name = TryDemangle(name);
- var entry = m_symbols[hi];
- entry.Name = name;
- m_symbols[hi] = entry;
+ Interlocked.CompareExchange(ref m_symbolNames[hi], name, null);
}
- return m_symbols[hi].Name;
+ return m_symbolNames[hi];
}
return string.Empty;
}
+ ///
+ /// Reads the GNU build-id from an ELF file by scanning PT_NOTE program headers.
+ /// Uses program headers (not section headers) because they are always present
+ /// even when section headers have been stripped.
+ ///
+ /// Path to the ELF file.
+ /// Lowercase hex string of the build-id, or null if not found or on any error.
+ internal static string ReadBuildId(string filePath)
+ {
+ try
+ {
+ using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ // Read and validate the ELF header.
+ byte[] header = new byte[Unsafe.SizeOf()];
+ int headerRead = ReadFully(stream, header, 0, header.Length);
+ if (!TryReadElfHeader(header, headerRead, out var hdr, "ReadBuildId"))
+ {
+ return null;
+ }
+
+ bool is64Bit = hdr.Is64Bit;
+ bool bigEndian = hdr.BigEndian;
+
+ if (hdr.PhOffset == 0 || hdr.PhEntrySize == 0 || hdr.PhCount == 0)
+ {
+ Debug.WriteLine("ReadBuildId: No program headers found.");
+ return null;
+ }
+
+ int minPhentsize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf();
+ if (hdr.PhEntrySize < minPhentsize)
+ {
+ Debug.WriteLine("ReadBuildId: ePhentsize too small: " + hdr.PhEntrySize);
+ return null;
+ }
+
+ if (hdr.PhCount > MaxProgramHeaderCount)
+ {
+ Debug.WriteLine("ReadBuildId: Program header count too large: " + hdr.PhCount);
+ return null;
+ }
+
+ // Read all program headers in one bulk read.
+ int phTableSize = hdr.PhCount * hdr.PhEntrySize;
+ byte[] phTable = new byte[phTableSize];
+ stream.Seek((long)hdr.PhOffset, SeekOrigin.Begin);
+ if (ReadFully(stream, phTable, 0, phTableSize) < phTableSize)
+ {
+ Debug.WriteLine("ReadBuildId: Could not read program headers.");
+ return null;
+ }
+
+ // Iterate program headers looking for PT_NOTE segments.
+ for (int i = 0; i < hdr.PhCount; i++)
+ {
+ int phPos = i * hdr.PhEntrySize;
+ ReadProgramHeader(phTable, phPos, is64Bit, bigEndian, out uint pType, out ulong pOffset, out ulong pFilesz, out _);
+
+ if (pType != PT_NOTE)
+ {
+ continue;
+ }
+
+ if (pFilesz == 0 || pFilesz > MaxNoteSizeBytes)
+ {
+ continue;
+ }
+
+ // Read the PT_NOTE segment data.
+ byte[] noteData = new byte[(int)pFilesz];
+ stream.Seek((long)pOffset, SeekOrigin.Begin);
+ if (ReadFully(stream, noteData, 0, noteData.Length) < noteData.Length)
+ {
+ continue;
+ }
+
+ // Iterate notes within the segment looking for GNU build-id.
+ string buildId = ExtractBuildId(noteData, bigEndian);
+ if (buildId != null)
+ {
+ return buildId;
+ }
+ }
+
+ Debug.WriteLine("ReadBuildId: No GNU build-id note found.");
+ return null;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("ReadBuildId: Error reading file: " + ex.Message);
+ return null;
+ }
+ }
+
+ ///
+ /// Reads the .gnu_debuglink section from an ELF file and returns the debug file name.
+ /// The .gnu_debuglink section contains a null-terminated filename followed by padding
+ /// and a CRC32 checksum. Only the filename is returned (CRC is not validated, matching
+ /// one-collect behavior).
+ ///
+ /// Path to the ELF file.
+ /// The debug link filename (e.g. "libcoreclr.so.dbg"), or null if not found or on any error.
+ internal static string ReadDebugLink(string filePath)
+ {
+ try
+ {
+ using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ // Read and validate the ELF header.
+ byte[] header = new byte[Unsafe.SizeOf()];
+ int headerRead = ReadFully(stream, header, 0, header.Length);
+ if (!TryReadElfHeader(header, headerRead, out var hdr, "ReadDebugLink"))
+ {
+ return null;
+ }
+
+ bool is64Bit = hdr.Is64Bit;
+ bool bigEndian = hdr.BigEndian;
+
+ if (hdr.ShOffset == 0 || hdr.ShEntrySize == 0 || hdr.ShCount == 0)
+ {
+ Debug.WriteLine("ReadDebugLink: No section headers found.");
+ return null;
+ }
+
+ int minShentsize = is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf();
+ if (hdr.ShEntrySize < minShentsize || hdr.ShEntrySize > MaxShentsize)
+ {
+ Debug.WriteLine("ReadDebugLink: Invalid section header entry size: " + hdr.ShEntrySize);
+ return null;
+ }
+
+ if (hdr.ShCount > MaxSectionCount)
+ {
+ Debug.WriteLine("ReadDebugLink: Section count too large: " + hdr.ShCount);
+ return null;
+ }
+
+ if (hdr.ShStrIndex >= hdr.ShCount)
+ {
+ Debug.WriteLine("ReadDebugLink: Invalid shstrndx: " + hdr.ShStrIndex);
+ return null;
+ }
+
+ // Read all section headers in one bulk read.
+ int shTableSize = hdr.ShCount * hdr.ShEntrySize;
+ byte[] shTable = new byte[shTableSize];
+ stream.Seek((long)hdr.ShOffset, SeekOrigin.Begin);
+ if (ReadFully(stream, shTable, 0, shTableSize) < shTableSize)
+ {
+ Debug.WriteLine("ReadDebugLink: Could not read section headers.");
+ return null;
+ }
+
+ // Read the section name string table (shstrtab).
+ int shstrPos = hdr.ShStrIndex * hdr.ShEntrySize;
+ ReadSectionHeader(shTable, shstrPos, is64Bit, bigEndian, out _, out _, out ulong shstrOffset, out ulong shstrSize, out _, out _);
+
+ if (shstrSize == 0 || shstrSize > MaxShstrtabSize)
+ {
+ Debug.WriteLine("ReadDebugLink: Invalid shstrtab size.");
+ return null;
+ }
+
+ byte[] shstrtab = new byte[(int)shstrSize];
+ stream.Seek((long)shstrOffset, SeekOrigin.Begin);
+ if (ReadFully(stream, shstrtab, 0, shstrtab.Length) < shstrtab.Length)
+ {
+ Debug.WriteLine("ReadDebugLink: Could not read shstrtab.");
+ return null;
+ }
+
+ // Iterate sections looking for .gnu_debuglink by name.
+ for (int i = 0; i < hdr.ShCount; i++)
+ {
+ int shPos = i * hdr.ShEntrySize;
+ ReadSectionHeader(shTable, shPos, is64Bit, bigEndian, out uint shName, out _, out ulong secOffset, out ulong secSize, out _, out _);
+
+ if (shName >= shstrtab.Length)
+ {
+ continue;
+ }
+
+ // Compare section name against ".gnu_debuglink".
+ if (!SectionNameEquals(shstrtab, (int)shName, GnuDebugLinkName))
+ {
+ continue;
+ }
+
+ // Found .gnu_debuglink — read its contents.
+
+ // The section must contain at least a filename byte + null + 4-byte CRC.
+ if (secSize < MinDebugLinkSectionSize || secSize > MaxDebugLinkSectionSize)
+ {
+ Debug.WriteLine("ReadDebugLink: Invalid .gnu_debuglink section size: " + secSize);
+ return null;
+ }
+
+ byte[] sectionData = new byte[(int)secSize];
+ stream.Seek((long)secOffset, SeekOrigin.Begin);
+ if (ReadFully(stream, sectionData, 0, sectionData.Length) < sectionData.Length)
+ {
+ Debug.WriteLine("ReadDebugLink: Could not read .gnu_debuglink section data.");
+ return null;
+ }
+
+ // Extract the null-terminated filename.
+ int nullPos = Array.IndexOf(sectionData, (byte)0);
+ if (nullPos <= 0)
+ {
+ Debug.WriteLine("ReadDebugLink: Empty or missing filename in .gnu_debuglink.");
+ return null;
+ }
+
+ return Encoding.UTF8.GetString(sectionData, 0, nullPos);
+ }
+
+ Debug.WriteLine("ReadDebugLink: No .gnu_debuglink section found.");
+ return null;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("ReadDebugLink: Error reading file: " + ex.Message);
+ return null;
+ }
+ }
+
#region private
+ // Name of the .gnu_debuglink section (UTF-8 bytes for fast comparison).
+ private static readonly byte[] GnuDebugLinkName = Encoding.UTF8.GetBytes(".gnu_debuglink");
+
+ ///
+ /// Common fields extracted from an ELF header (Ehdr) after validation.
+ ///
+ private struct ElfHeaderInfo
+ {
+ public bool Is64Bit;
+ public bool BigEndian;
+ // Program header table.
+ public ulong PhOffset;
+ public ushort PhEntrySize;
+ public ushort PhCount;
+ // Section header table.
+ public ulong ShOffset;
+ public ushort ShEntrySize;
+ public ushort ShCount;
+ public ushort ShStrIndex;
+ }
+
+ ///
+ /// Validates an ELF header buffer and extracts common fields into .
+ /// The caller must have already read at least Unsafe.SizeOf<Elf64_Ehdr>() bytes
+ /// into .
+ ///
+ /// True if the header is valid; false otherwise (with a Debug.WriteLine message).
+ private static bool TryReadElfHeader(byte[] header, int headerRead, out ElfHeaderInfo info, string callerName)
+ {
+ info = default;
+
+ if (headerRead < EI_NIDENT)
+ {
+ Debug.WriteLine(callerName + ": File too small.");
+ return false;
+ }
+
+ if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3)
+ {
+ Debug.WriteLine(callerName + ": Invalid ELF magic.");
+ return false;
+ }
+
+ byte eiClass = header[EI_CLASS];
+ byte eiData = header[EI_DATA];
+ info.Is64Bit = (eiClass == ElfClass64);
+ info.BigEndian = (eiData == ElfDataMsb);
+
+ if (eiClass != ElfClass32 && eiClass != ElfClass64)
+ {
+ Debug.WriteLine(callerName + ": Unknown ELF class " + eiClass + ".");
+ return false;
+ }
+
+ int ehSize = info.Is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf();
+ if (headerRead < ehSize)
+ {
+ Debug.WriteLine(callerName + ": Header too small.");
+ return false;
+ }
+
+ // Extract all commonly needed fields from the typed header.
+ if (info.Is64Bit)
+ {
+ var ehdr = ReadStruct(header, 0, info.BigEndian);
+ info.PhOffset = ehdr.e_phoff;
+ info.PhEntrySize = ehdr.e_phentsize;
+ info.PhCount = ehdr.e_phnum;
+ info.ShOffset = ehdr.e_shoff;
+ info.ShEntrySize = ehdr.e_shentsize;
+ info.ShCount = ehdr.e_shnum;
+ info.ShStrIndex = ehdr.e_shstrndx;
+ }
+ else
+ {
+ var ehdr = ReadStruct(header, 0, info.BigEndian);
+ info.PhOffset = ehdr.e_phoff;
+ info.PhEntrySize = ehdr.e_phentsize;
+ info.PhCount = ehdr.e_phnum;
+ info.ShOffset = ehdr.e_shoff;
+ info.ShEntrySize = ehdr.e_shentsize;
+ info.ShCount = ehdr.e_shnum;
+ info.ShStrIndex = ehdr.e_shstrndx;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Reads section header fields from a byte array at the given position.
+ ///
+ private static void ReadSectionHeader(byte[] shTable, int shPos, bool is64Bit, bool bigEndian,
+ out uint name, out uint shType, out ulong offset, out ulong size, out uint link, out ulong entsize)
+ {
+ if (is64Bit)
+ {
+ var shdr = ReadStruct(shTable, shPos, bigEndian);
+ name = shdr.sh_name;
+ shType = shdr.sh_type;
+ offset = shdr.sh_offset;
+ size = shdr.sh_size;
+ link = shdr.sh_link;
+ entsize = shdr.sh_entsize;
+ }
+ else
+ {
+ var shdr = ReadStruct(shTable, shPos, bigEndian);
+ name = shdr.sh_name;
+ shType = shdr.sh_type;
+ offset = shdr.sh_offset;
+ size = shdr.sh_size;
+ link = shdr.sh_link;
+ entsize = shdr.sh_entsize;
+ }
+ }
+
+ ///
+ /// Reads program header fields from a byte array at the given position.
+ ///
+ private static void ReadProgramHeader(byte[] phTable, int phPos, bool is64Bit, bool bigEndian,
+ out uint pType, out ulong pOffset, out ulong pFilesz, out ulong pVaddr)
+ {
+ if (is64Bit)
+ {
+ var phdr = ReadStruct(phTable, phPos, bigEndian);
+ pType = phdr.p_type;
+ pOffset = phdr.p_offset;
+ pFilesz = phdr.p_filesz;
+ pVaddr = phdr.p_vaddr;
+ }
+ else
+ {
+ var phdr = ReadStruct(phTable, phPos, bigEndian);
+ pType = phdr.p_type;
+ pOffset = phdr.p_offset;
+ pFilesz = phdr.p_filesz;
+ pVaddr = phdr.p_vaddr;
+ }
+ }
+
+ ///
+ /// Reads symbol table entry fields from a byte array at the given position.
+ ///
+ private static void ReadSymbolEntry(byte[] symData, int pos, bool is64Bit, bool bigEndian,
+ out uint stName, out byte stInfo, out ulong stValue, out ulong stSize)
+ {
+ if (is64Bit)
+ {
+ var sym = ReadStruct(symData, pos, bigEndian);
+ stName = sym.st_name;
+ stInfo = sym.st_info;
+ stValue = sym.st_value;
+ stSize = sym.st_size;
+ }
+ else
+ {
+ var sym = ReadStruct(symData, pos, bigEndian);
+ stName = sym.st_name;
+ stInfo = sym.st_info;
+ stValue = sym.st_value;
+ stSize = sym.st_size;
+ }
+ }
+
+ ///
+ /// Compares a null-terminated string in a byte array against an expected byte sequence.
+ ///
+ private static bool SectionNameEquals(byte[] strtab, int offset, byte[] expected)
+ {
+ if (offset + expected.Length > strtab.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < expected.Length; i++)
+ {
+ if (strtab[offset + i] != expected[i])
+ {
+ return false;
+ }
+ }
+
+ // Ensure the string in strtab is null-terminated right after the match.
+ int endPos = offset + expected.Length;
+ return endPos >= strtab.Length || strtab[endPos] == 0;
+ }
+
// ELF identification (e_ident) constants.
private const byte ElfMagic0 = 0x7f;
private const byte ElfMagic1 = (byte)'E';
@@ -119,52 +538,343 @@ public string FindNameForRva(uint rva, ref uint symbolStart)
// ELF data encoding values.
private const byte ElfDataMsb = 2; // Big-endian.
- // ELF header sizes.
- private const int Elf32EhdrSize = 52;
- private const int Elf64EhdrSize = 64;
+ // Maximum program header count to accept from ELF headers.
+ private const int MaxProgramHeaderCount = 4096;
+
+ // Maximum section header entry size and section count.
+ private const int MaxShentsize = 256;
+ private const uint MaxSectionCount = 65535;
// Section header types.
private const uint SHT_SYMTAB = 2;
private const uint SHT_DYNSYM = 11;
+ // Program header types.
+ private const uint PT_NOTE = 4; // Note segment.
+
+ // Note types.
+ private const uint NT_GNU_BUILD_ID = 3; // GNU build-id note type.
+
+ // Expected namesz for GNU notes ("GNU\0").
+ private const uint GnuNoteNameSize = 4;
+
+ // PT_NOTE segment size limit for ReadBuildId. Real build-id notes are < 100 bytes;
+ // 64 KB is generous. Prevents OOM from crafted ELF with large p_filesz.
+ private const int MaxNoteSizeBytes = 64 * 1024;
+
+ // ReadDebugLink section size limits.
+ private const int MaxShstrtabSize = 1024 * 1024; // 1 MB
+ private const int MinDebugLinkSectionSize = 6; // 1-char filename + null + 4-byte CRC
+ private const int MaxDebugLinkSectionSize = 4096;
+
// Symbol table constants.
private const byte STT_FUNC = 2; // Symbol type: function.
private const byte STT_MASK = 0xf; // Mask to extract symbol type from st_info.
- // Elf64_Sym field offsets: st_name(4), st_info(1), st_other(1), st_shndx(2), st_value(8), st_size(8).
- private const int Sym64_Name = 0;
- private const int Sym64_Info = 4;
- private const int Sym64_Value = 8;
- private const int Sym64_Size = 16;
+ private const int StrtabSegmentSize = 65536; // 64KB segments to avoid LOH.
- // Elf32_Sym field offsets: st_name(4), st_value(4), st_size(4), st_info(1), st_other(1), st_shndx(2).
- private const int Sym32_Name = 0;
- private const int Sym32_Value = 4;
- private const int Sym32_Size = 8;
- private const int Sym32_Info = 12;
+ #region ELF binary format structs
- private const int StrtabSegmentSize = 65536; // 64KB segments to avoid LOH.
+ // These structs match the ELF specification layouts exactly. Fields use ELF naming conventions
+ // (e_phoff, sh_type, st_value, etc.) for easy cross-reference with the spec.
+ // MemoryMarshal.Read is used to read them from byte arrays — the same pattern as PEFile.cs.
+ // For big-endian ELF files, SwapEndian() reverses each multi-byte field after reading.
+ // All structs implement IElfStruct so ReadStruct can handle endian swapping automatically.
+
+ ///
+ /// Implemented by all ELF binary structs so that can
+ /// perform endian conversion in one place.
+ ///
+ private interface IElfStruct
+ {
+ void SwapEndian();
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf64_Ehdr : IElfStruct
+ {
+ // e_ident[16]
+ public byte ei_mag0, ei_mag1, ei_mag2, ei_mag3;
+ public byte ei_class, ei_data, ei_version, ei_osabi;
+ public byte ei_abiversion, ei_pad1, ei_pad2, ei_pad3, ei_pad4, ei_pad5, ei_pad6, ei_pad7;
+ // Header fields
+ public ushort e_type;
+ public ushort e_machine;
+ public uint e_version;
+ public ulong e_entry;
+ public ulong e_phoff;
+ public ulong e_shoff;
+ public uint e_flags;
+ public ushort e_ehsize;
+ public ushort e_phentsize;
+ public ushort e_phnum;
+ public ushort e_shentsize;
+ public ushort e_shnum;
+ public ushort e_shstrndx;
+
+ public void SwapEndian()
+ {
+ e_type = BinaryPrimitives.ReverseEndianness(e_type);
+ e_machine = BinaryPrimitives.ReverseEndianness(e_machine);
+ e_version = BinaryPrimitives.ReverseEndianness(e_version);
+ e_entry = BinaryPrimitives.ReverseEndianness(e_entry);
+ e_phoff = BinaryPrimitives.ReverseEndianness(e_phoff);
+ e_shoff = BinaryPrimitives.ReverseEndianness(e_shoff);
+ e_flags = BinaryPrimitives.ReverseEndianness(e_flags);
+ e_ehsize = BinaryPrimitives.ReverseEndianness(e_ehsize);
+ e_phentsize = BinaryPrimitives.ReverseEndianness(e_phentsize);
+ e_phnum = BinaryPrimitives.ReverseEndianness(e_phnum);
+ e_shentsize = BinaryPrimitives.ReverseEndianness(e_shentsize);
+ e_shnum = BinaryPrimitives.ReverseEndianness(e_shnum);
+ e_shstrndx = BinaryPrimitives.ReverseEndianness(e_shstrndx);
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf32_Ehdr : IElfStruct
+ {
+ // e_ident[16]
+ public byte ei_mag0, ei_mag1, ei_mag2, ei_mag3;
+ public byte ei_class, ei_data, ei_version, ei_osabi;
+ public byte ei_abiversion, ei_pad1, ei_pad2, ei_pad3, ei_pad4, ei_pad5, ei_pad6, ei_pad7;
+ // Header fields
+ public ushort e_type;
+ public ushort e_machine;
+ public uint e_version;
+ public uint e_entry;
+ public uint e_phoff;
+ public uint e_shoff;
+ public uint e_flags;
+ public ushort e_ehsize;
+ public ushort e_phentsize;
+ public ushort e_phnum;
+ public ushort e_shentsize;
+ public ushort e_shnum;
+ public ushort e_shstrndx;
+
+ public void SwapEndian()
+ {
+ e_type = BinaryPrimitives.ReverseEndianness(e_type);
+ e_machine = BinaryPrimitives.ReverseEndianness(e_machine);
+ e_version = BinaryPrimitives.ReverseEndianness(e_version);
+ e_entry = BinaryPrimitives.ReverseEndianness(e_entry);
+ e_phoff = BinaryPrimitives.ReverseEndianness(e_phoff);
+ e_shoff = BinaryPrimitives.ReverseEndianness(e_shoff);
+ e_flags = BinaryPrimitives.ReverseEndianness(e_flags);
+ e_ehsize = BinaryPrimitives.ReverseEndianness(e_ehsize);
+ e_phentsize = BinaryPrimitives.ReverseEndianness(e_phentsize);
+ e_phnum = BinaryPrimitives.ReverseEndianness(e_phnum);
+ e_shentsize = BinaryPrimitives.ReverseEndianness(e_shentsize);
+ e_shnum = BinaryPrimitives.ReverseEndianness(e_shnum);
+ e_shstrndx = BinaryPrimitives.ReverseEndianness(e_shstrndx);
+ }
+ }
+
+ // 64-bit: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8), p_memsz(8), p_align(8)
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf64_Phdr : IElfStruct
+ {
+ public uint p_type;
+ public uint p_flags;
+ public ulong p_offset;
+ public ulong p_vaddr;
+ public ulong p_paddr;
+ public ulong p_filesz;
+ public ulong p_memsz;
+ public ulong p_align;
+
+ public void SwapEndian()
+ {
+ p_type = BinaryPrimitives.ReverseEndianness(p_type);
+ p_flags = BinaryPrimitives.ReverseEndianness(p_flags);
+ p_offset = BinaryPrimitives.ReverseEndianness(p_offset);
+ p_vaddr = BinaryPrimitives.ReverseEndianness(p_vaddr);
+ p_paddr = BinaryPrimitives.ReverseEndianness(p_paddr);
+ p_filesz = BinaryPrimitives.ReverseEndianness(p_filesz);
+ p_memsz = BinaryPrimitives.ReverseEndianness(p_memsz);
+ p_align = BinaryPrimitives.ReverseEndianness(p_align);
+ }
+ }
+
+ // 32-bit: p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4), p_memsz(4), p_flags(4), p_align(4)
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf32_Phdr : IElfStruct
+ {
+ public uint p_type;
+ public uint p_offset;
+ public uint p_vaddr;
+ public uint p_paddr;
+ public uint p_filesz;
+ public uint p_memsz;
+ public uint p_flags;
+ public uint p_align;
+
+ public void SwapEndian()
+ {
+ p_type = BinaryPrimitives.ReverseEndianness(p_type);
+ p_offset = BinaryPrimitives.ReverseEndianness(p_offset);
+ p_vaddr = BinaryPrimitives.ReverseEndianness(p_vaddr);
+ p_paddr = BinaryPrimitives.ReverseEndianness(p_paddr);
+ p_filesz = BinaryPrimitives.ReverseEndianness(p_filesz);
+ p_memsz = BinaryPrimitives.ReverseEndianness(p_memsz);
+ p_flags = BinaryPrimitives.ReverseEndianness(p_flags);
+ p_align = BinaryPrimitives.ReverseEndianness(p_align);
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf64_Shdr : IElfStruct
+ {
+ public uint sh_name;
+ public uint sh_type;
+ public ulong sh_flags;
+ public ulong sh_addr;
+ public ulong sh_offset;
+ public ulong sh_size;
+ public uint sh_link;
+ public uint sh_info;
+ public ulong sh_addralign;
+ public ulong sh_entsize;
+
+ public void SwapEndian()
+ {
+ sh_name = BinaryPrimitives.ReverseEndianness(sh_name);
+ sh_type = BinaryPrimitives.ReverseEndianness(sh_type);
+ sh_flags = BinaryPrimitives.ReverseEndianness(sh_flags);
+ sh_addr = BinaryPrimitives.ReverseEndianness(sh_addr);
+ sh_offset = BinaryPrimitives.ReverseEndianness(sh_offset);
+ sh_size = BinaryPrimitives.ReverseEndianness(sh_size);
+ sh_link = BinaryPrimitives.ReverseEndianness(sh_link);
+ sh_info = BinaryPrimitives.ReverseEndianness(sh_info);
+ sh_addralign = BinaryPrimitives.ReverseEndianness(sh_addralign);
+ sh_entsize = BinaryPrimitives.ReverseEndianness(sh_entsize);
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf32_Shdr : IElfStruct
+ {
+ public uint sh_name;
+ public uint sh_type;
+ public uint sh_flags;
+ public uint sh_addr;
+ public uint sh_offset;
+ public uint sh_size;
+ public uint sh_link;
+ public uint sh_info;
+ public uint sh_addralign;
+ public uint sh_entsize;
+
+ public void SwapEndian()
+ {
+ sh_name = BinaryPrimitives.ReverseEndianness(sh_name);
+ sh_type = BinaryPrimitives.ReverseEndianness(sh_type);
+ sh_flags = BinaryPrimitives.ReverseEndianness(sh_flags);
+ sh_addr = BinaryPrimitives.ReverseEndianness(sh_addr);
+ sh_offset = BinaryPrimitives.ReverseEndianness(sh_offset);
+ sh_size = BinaryPrimitives.ReverseEndianness(sh_size);
+ sh_link = BinaryPrimitives.ReverseEndianness(sh_link);
+ sh_info = BinaryPrimitives.ReverseEndianness(sh_info);
+ sh_addralign = BinaryPrimitives.ReverseEndianness(sh_addralign);
+ sh_entsize = BinaryPrimitives.ReverseEndianness(sh_entsize);
+ }
+ }
+
+ // 64-bit: st_name(4), st_info(1), st_other(1), st_shndx(2), st_value(8), st_size(8) = 24 bytes
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf64_Sym : IElfStruct
+ {
+ public uint st_name;
+ public byte st_info;
+ public byte st_other;
+ public ushort st_shndx;
+ public ulong st_value;
+ public ulong st_size;
+
+ public void SwapEndian()
+ {
+ st_name = BinaryPrimitives.ReverseEndianness(st_name);
+ st_shndx = BinaryPrimitives.ReverseEndianness(st_shndx);
+ st_value = BinaryPrimitives.ReverseEndianness(st_value);
+ st_size = BinaryPrimitives.ReverseEndianness(st_size);
+ }
+ }
+
+ // 32-bit: st_name(4), st_value(4), st_size(4), st_info(1), st_other(1), st_shndx(2) = 16 bytes
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf32_Sym : IElfStruct
+ {
+ public uint st_name;
+ public uint st_value;
+ public uint st_size;
+ public byte st_info;
+ public byte st_other;
+ public ushort st_shndx;
+
+ public void SwapEndian()
+ {
+ st_name = BinaryPrimitives.ReverseEndianness(st_name);
+ st_value = BinaryPrimitives.ReverseEndianness(st_value);
+ st_size = BinaryPrimitives.ReverseEndianness(st_size);
+ st_shndx = BinaryPrimitives.ReverseEndianness(st_shndx);
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Elf_Nhdr : IElfStruct
+ {
+ public uint n_namesz;
+ public uint n_descsz;
+ public uint n_type;
+
+ public void SwapEndian()
+ {
+ n_namesz = BinaryPrimitives.ReverseEndianness(n_namesz);
+ n_descsz = BinaryPrimitives.ReverseEndianness(n_descsz);
+ n_type = BinaryPrimitives.ReverseEndianness(n_type);
+ }
+ }
+
+ ///
+ /// Reads an ELF struct from a byte array at the given offset.
+ /// When is true,
+ /// is called automatically before returning.
+ ///
+ private static T ReadStruct(byte[] data, int offset, bool bigEndian) where T : struct, IElfStruct
+ {
+ T value = MemoryMarshal.Read(data.AsSpan(offset));
+ if (bigEndian)
+ {
+ value.SwapEndian();
+ }
+ return value;
+ }
+
+ #endregion
private readonly List m_symbols;
private readonly ulong m_pVaddr;
private readonly ulong m_pOffset;
private readonly bool m_demangle;
- private readonly ItaniumDemangler m_itaniumDemangler = new ItaniumDemangler();
- private readonly RustDemangler m_rustDemangler = new RustDemangler();
+
+ // Demanglers use mutable parser state and are not thread-safe. ThreadLocal ensures
+ // each thread gets its own instance for safe concurrent FindNameForRva calls.
+ private readonly ThreadLocal m_itaniumDemangler = new ThreadLocal(() => new ItaniumDemangler());
+ private readonly ThreadLocal m_rustDemangler = new ThreadLocal(() => new RustDemangler());
private SegmentedList m_strtab; // Retained for lazy name resolution.
+ private string[] m_symbolNames; // Thread-safe lazy name cache (parallel to m_symbols).
private bool m_is64Bit;
private bool m_bigEndian;
///
/// Represents a resolved ELF symbol with its address range.
- /// Name is decoded lazily on first FindNameForRva hit.
+ /// Name is decoded lazily on first FindNameForRva hit via m_symbolNames.
///
private struct ElfSymbolEntry : IComparable
{
- public uint Start; // Adjusted RVA: (st_value - pVaddr) + pOffset.
+ public uint Start; // RVA: (st_value - pVaddr) + pOffset.
public uint End; // Start + size - 1 (inclusive).
public uint StrtabOffset; // Offset into m_strtab for lazy name decode.
- public string Name; // Null until first lookup, then cached.
public int CompareTo(ElfSymbolEntry other) => Start.CompareTo(other.Start);
}
@@ -177,83 +887,45 @@ private struct ElfSymbolEntry : IComparable
///
private void ParseElf(Stream stream)
{
- // Read the ELF header (max 64 bytes for 64-bit).
- byte[] header = new byte[Elf64EhdrSize];
+ // Read and validate the ELF header.
+ byte[] header = new byte[Unsafe.SizeOf()];
int headerRead = ReadFully(stream, header, 0, header.Length);
- if (headerRead < EI_NIDENT)
- {
- Debug.WriteLine("ElfSymbolModule: File too small.");
- return;
- }
-
- // Verify ELF magic bytes: 0x7f 'E' 'L' 'F'.
- if (header[0] != ElfMagic0 || header[1] != ElfMagic1 || header[2] != ElfMagic2 || header[3] != ElfMagic3)
+ if (!TryReadElfHeader(header, headerRead, out var hdr, "ElfSymbolModule"))
{
- Debug.WriteLine("ElfSymbolModule: Invalid ELF magic.");
return;
}
- byte eiClass = header[EI_CLASS];
- byte eiData = header[EI_DATA];
-
- m_is64Bit = (eiClass == ElfClass64);
- m_bigEndian = (eiData == ElfDataMsb);
+ m_is64Bit = hdr.Is64Bit;
+ m_bigEndian = hdr.BigEndian;
- if (eiClass != ElfClass32 && eiClass != ElfClass64)
+ if (hdr.ShOffset == 0 || hdr.ShEntrySize == 0)
{
- Debug.WriteLine("ElfSymbolModule: Unknown ELF class " + eiClass + ".");
+ Debug.WriteLine("ElfSymbolModule: No section headers found.");
return;
}
- int ehSize = m_is64Bit ? Elf64EhdrSize : Elf32EhdrSize;
- if (headerRead < ehSize)
+ // Valid ELF section header sizes are 40 (32-bit) or 64 (64-bit).
+ // Reject values below the minimum struct size (would cause out-of-bounds reads)
+ // and cap at 256 to guard against overflow in sectionCount * eShentsize.
+ int minShentsize = m_is64Bit ? Unsafe.SizeOf() : Unsafe.SizeOf();
+ if (hdr.ShEntrySize < minShentsize || hdr.ShEntrySize > MaxShentsize)
{
- return;
- }
-
- // Parse ELF header fields.
- int pos = 16 + 2 + 2 + 4; // skip e_ident(16), e_type(2), e_machine(2), e_version(4)
-
- ulong eShoff;
- if (m_is64Bit)
- {
- pos += 8 + 8; // e_entry, e_phoff
- eShoff = ReadU64(header, pos); pos += 8;
- }
- else
- {
- pos += 4 + 4;
- eShoff = ReadU32(header, pos); pos += 4;
- }
-
- pos += 4 + 2 + 2 + 2; // e_flags, e_ehsize, e_phentsize, e_phnum
- ushort eShentsize = ReadU16(header, pos); pos += 2;
- ushort eShnum = ReadU16(header, pos);
-
- if (eShoff == 0 || eShentsize == 0)
- {
- Debug.WriteLine("ElfSymbolModule: No section headers found.");
+ Debug.WriteLine("ElfSymbolModule: Invalid section header entry size: " + hdr.ShEntrySize);
return;
}
// Handle extended section count.
- uint sectionCount = eShnum;
- if (eShnum == 0)
+ uint sectionCount = hdr.ShCount;
+ if (hdr.ShCount == 0)
{
- byte[] firstSh = new byte[eShentsize];
- stream.Seek((long)eShoff, SeekOrigin.Begin);
+ byte[] firstSh = new byte[hdr.ShEntrySize];
+ stream.Seek((long)hdr.ShOffset, SeekOrigin.Begin);
if (ReadFully(stream, firstSh, 0, firstSh.Length) < firstSh.Length)
{
return;
}
- if (m_is64Bit)
- {
- sectionCount = (uint)ReadU64(firstSh, 8 + 8 + 8 + 8);
- }
- else
- {
- sectionCount = ReadU32(firstSh, 8 + 4 + 4 + 4);
- }
+ ReadSectionHeader(firstSh, 0, m_is64Bit, m_bigEndian, out _, out _, out _, out ulong extSize, out _, out _);
+ sectionCount = (uint)extSize;
}
if (sectionCount == 0)
@@ -261,10 +933,17 @@ private void ParseElf(Stream stream)
return;
}
+ // Guard against corrupt ELF headers with unreasonably large section counts.
+ if (sectionCount > MaxSectionCount)
+ {
+ Debug.WriteLine("ElfSymbolModule: Section count too large: " + sectionCount);
+ return;
+ }
+
// Read all section headers in one bulk read.
- int shTableSize = (int)sectionCount * eShentsize;
+ int shTableSize = (int)sectionCount * hdr.ShEntrySize;
byte[] shTable = new byte[shTableSize];
- stream.Seek((long)eShoff, SeekOrigin.Begin);
+ stream.Seek((long)hdr.ShOffset, SeekOrigin.Begin);
if (ReadFully(stream, shTable, 0, shTableSize) < shTableSize)
{
return;
@@ -275,10 +954,9 @@ private void ParseElf(Stream stream)
long totalSymbolCount = 0;
for (uint i = 0; i < sectionCount; i++)
{
- int shPos = (int)i * eShentsize;
- uint shType;
- long shOffset, shSize, shLink, shEntsize;
- ReadSectionHeader(shTable, shPos, out shType, out shOffset, out shSize, out shLink, out shEntsize);
+ int shPos = (int)i * hdr.ShEntrySize;
+ ReadSectionHeader(shTable, shPos, m_is64Bit, m_bigEndian,
+ out _, out uint shType, out _, out ulong shSize, out uint shLink, out ulong shEntsize);
if (shType != SHT_SYMTAB && shType != SHT_DYNSYM)
{
@@ -287,15 +965,19 @@ private void ParseElf(Stream stream)
if (shEntsize > 0)
{
- totalSymbolCount += shSize / shEntsize;
+ totalSymbolCount += (long)(shSize / shEntsize);
}
// Get the linked string table size.
- int strtabShPos = (int)shLink * eShentsize;
- uint strtabType;
- long strtabOffset, strtabSize, strtabLink, strtabEntsize;
- ReadSectionHeader(shTable, strtabShPos, out strtabType, out strtabOffset, out strtabSize, out strtabLink, out strtabEntsize);
- totalStrtabSize += strtabSize;
+ if (shLink == 0 || shLink >= sectionCount)
+ {
+ continue;
+ }
+
+ int strtabShPos = (int)shLink * hdr.ShEntrySize;
+ ReadSectionHeader(shTable, strtabShPos, m_is64Bit, m_bigEndian,
+ out _, out _, out _, out ulong strtabSize, out _, out _);
+ totalStrtabSize += (long)strtabSize;
}
// Pre-allocate with known sizes.
@@ -306,10 +988,9 @@ private void ParseElf(Stream stream)
long strtabBaseOffset = 0;
for (uint i = 0; i < sectionCount; i++)
{
- int shPos = (int)i * eShentsize;
- uint shType;
- long shOffset, shSize, shLink, shEntsize;
- ReadSectionHeader(shTable, shPos, out shType, out shOffset, out shSize, out shLink, out shEntsize);
+ int shPos = (int)i * hdr.ShEntrySize;
+ ReadSectionHeader(shTable, shPos, m_is64Bit, m_bigEndian,
+ out _, out uint shType, out ulong shOffset, out ulong shSize, out uint shLink, out ulong shEntsize);
if (shType != SHT_SYMTAB && shType != SHT_DYNSYM)
{
@@ -317,19 +998,23 @@ private void ParseElf(Stream stream)
}
// Load the linked string table into the SegmentedList.
- int strtabShPos = (int)shLink * eShentsize;
- uint strtabType;
- long strtabOffset, strtabSize, strtabLink, strtabEntsize;
- ReadSectionHeader(shTable, strtabShPos, out strtabType, out strtabOffset, out strtabSize, out strtabLink, out strtabEntsize);
+ if (shLink == 0 || shLink >= sectionCount)
+ {
+ continue;
+ }
- if (strtabSize <= 0)
+ int strtabShPos = (int)shLink * hdr.ShEntrySize;
+ ReadSectionHeader(shTable, strtabShPos, m_is64Bit, m_bigEndian,
+ out _, out _, out ulong strtabOffset, out ulong strtabSize, out _, out _);
+
+ if (strtabSize == 0)
{
continue;
}
// Read strtab in chunks and append to SegmentedList.
- stream.Seek(strtabOffset, SeekOrigin.Begin);
- long remaining = strtabSize;
+ stream.Seek((long)strtabOffset, SeekOrigin.Begin);
+ long remaining = (long)strtabSize;
byte[] readBuf = new byte[Math.Min(remaining, StrtabSegmentSize)];
while (remaining > 0)
{
@@ -344,20 +1029,21 @@ private void ParseElf(Stream stream)
}
// Read the symbol table section.
- byte[] symData = new byte[shSize];
- stream.Seek(shOffset, SeekOrigin.Begin);
- if (ReadFully(stream, symData, 0, (int)shSize) < shSize)
+ byte[] symData = new byte[(long)shSize];
+ stream.Seek((long)shOffset, SeekOrigin.Begin);
+ if (ReadFully(stream, symData, 0, (int)shSize) < (long)shSize)
{
- strtabBaseOffset += strtabSize;
+ strtabBaseOffset += (long)strtabSize;
continue;
}
- ReadSymbolTable(symData, shSize, shEntsize, strtabBaseOffset);
- strtabBaseOffset += strtabSize;
+ ReadSymbolTable(symData, (long)shSize, (long)shEntsize, strtabBaseOffset);
+ strtabBaseOffset += (long)strtabSize;
}
// Sort symbols by start address for binary search.
m_symbols.Sort();
+ m_symbolNames = new string[m_symbols.Count];
}
///
@@ -378,35 +1064,6 @@ private static int ReadFully(Stream stream, byte[] buffer, int offset, int count
return totalRead;
}
- ///
- /// Reads section header fields from a byte array at the given position.
- ///
- private void ReadSectionHeader(byte[] data, int pos, out uint shType, out long shOffset,
- out long shSize, out long shLink, out long shEntsize)
- {
- pos += 4; // skip sh_name
- shType = ReadU32(data, pos); pos += 4;
-
- if (m_is64Bit)
- {
- pos += 8 + 8; // sh_flags, sh_addr
- shOffset = (long)ReadU64(data, pos); pos += 8;
- shSize = (long)ReadU64(data, pos); pos += 8;
- shLink = ReadU32(data, pos); pos += 4;
- pos += 4 + 8; // sh_info, sh_addralign
- shEntsize = (long)ReadU64(data, pos);
- }
- else
- {
- pos += 4 + 4; // sh_flags, sh_addr
- shOffset = ReadU32(data, pos); pos += 4;
- shSize = ReadU32(data, pos); pos += 4;
- shLink = ReadU32(data, pos); pos += 4;
- pos += 4 + 4; // sh_info, sh_addralign
- shEntsize = ReadU32(data, pos);
- }
- }
-
///
/// Reads all symbol entries from a pre-loaded symbol table byte array.
/// Stores strtab offsets for lazy name resolution instead of decoding strings.
@@ -423,41 +1080,7 @@ private void ReadSymbolTable(byte[] symData, long size, long entsize, long strta
for (long i = 0; i < count; i++)
{
int pos = (int)(i * entsize);
-
- uint stName;
- byte stInfo;
- ulong stValue;
- ulong stSize;
-
- if (m_is64Bit && !m_bigEndian)
- {
- // Fast path for 64-bit little-endian (the common case).
- stName = BitConverter.ToUInt32(symData, pos + Sym64_Name);
- stInfo = symData[pos + Sym64_Info];
- stValue = BitConverter.ToUInt64(symData, pos + Sym64_Value);
- stSize = BitConverter.ToUInt64(symData, pos + Sym64_Size);
- }
- else if (m_is64Bit)
- {
- stName = ReadU32(symData, pos + Sym64_Name);
- stInfo = symData[pos + Sym64_Info];
- stValue = ReadU64(symData, pos + Sym64_Value);
- stSize = ReadU64(symData, pos + Sym64_Size);
- }
- else if (!m_bigEndian)
- {
- stName = BitConverter.ToUInt32(symData, pos + Sym32_Name);
- stValue = BitConverter.ToUInt32(symData, pos + Sym32_Value);
- stSize = BitConverter.ToUInt32(symData, pos + Sym32_Size);
- stInfo = symData[pos + Sym32_Info];
- }
- else
- {
- stName = ReadU32(symData, pos + Sym32_Name);
- stValue = ReadU32(symData, pos + Sym32_Value);
- stSize = ReadU32(symData, pos + Sym32_Size);
- stInfo = symData[pos + Sym32_Info];
- }
+ ReadSymbolEntry(symData, pos, m_is64Bit, m_bigEndian, out uint stName, out byte stInfo, out ulong stValue, out ulong stSize);
// Filter to STT_FUNC symbols with non-zero value and size.
if ((stInfo & STT_MASK) != STT_FUNC || stValue == 0 || stSize == 0)
@@ -492,6 +1115,72 @@ private void ReadSymbolTable(byte[] symData, long size, long entsize, long strta
}
}
+ ///
+ /// Searches a PT_NOTE segment's raw bytes for a GNU build-id note.
+ /// Note format: namesz(4) + descsz(4) + type(4) + name(aligned to 4) + desc(aligned to 4).
+ ///
+ /// Raw bytes of the PT_NOTE segment.
+ /// True if the ELF file uses big-endian encoding.
+ /// Lowercase hex string of the build-id, or null if not found.
+ private static string ExtractBuildId(byte[] noteData, bool bigEndian)
+ {
+ int pos = 0;
+ int length = noteData.Length;
+ int nhdrSize = Unsafe.SizeOf();
+
+ while (pos + nhdrSize <= length)
+ {
+ var nhdr = ReadStruct(noteData, pos, bigEndian);
+ uint namesz = nhdr.n_namesz;
+ uint descsz = nhdr.n_descsz;
+ uint type = nhdr.n_type;
+ pos += nhdrSize;
+
+ // Guard against uint overflow in alignment arithmetic: (x + 3) wraps
+ // when x >= 0xFFFFFFFD, producing a small aligned value and an infinite loop.
+ uint remaining = (uint)(length - pos);
+ if (namesz > remaining || descsz > remaining)
+ {
+ break;
+ }
+
+ // Align name and desc sizes to 4-byte boundaries.
+ uint nameAligned = (namesz + 3) & ~3u;
+ uint descAligned = (descsz + 3) & ~3u;
+ uint noteSize = nameAligned + descAligned;
+
+ // Validate that the note fits within the segment data.
+ if (noteSize > remaining)
+ {
+ break;
+ }
+
+ // Check for GNU build-id: name == "GNU\0" (namesz == 4) and type == NT_GNU_BUILD_ID (3).
+ if (type == NT_GNU_BUILD_ID && namesz == GnuNoteNameSize &&
+ noteData[pos] == (byte)'G' && noteData[pos + 1] == (byte)'N' &&
+ noteData[pos + 2] == (byte)'U' && noteData[pos + 3] == 0)
+ {
+ if (descsz == 0)
+ {
+ return null;
+ }
+
+ // Extract the build-id descriptor bytes as lowercase hex.
+ int descStart = pos + (int)nameAligned;
+ var sb = new StringBuilder((int)descsz * 2);
+ for (int j = 0; j < (int)descsz; j++)
+ {
+ sb.Append(noteData[descStart + j].ToString("x2"));
+ }
+ return sb.ToString();
+ }
+
+ pos += (int)noteSize;
+ }
+
+ return null;
+ }
+
///
/// Attempts to demangle a symbol name using available demanglers.
/// Supports Itanium C++ ABI (_Z prefix) and Rust v0 (_R prefix) mangling.
@@ -505,7 +1194,7 @@ private string TryDemangle(string name)
if (name.StartsWith("_Z"))
{
- string demangled = m_itaniumDemangler.Demangle(name);
+ string demangled = m_itaniumDemangler.Value.Demangle(name);
if (demangled != null)
{
return demangled;
@@ -514,7 +1203,7 @@ private string TryDemangle(string name)
if (name.StartsWith("_R"))
{
- string demangled = m_rustDemangler.Demangle(name);
+ string demangled = m_rustDemangler.Value.Demangle(name);
if (demangled != null)
{
return demangled;
@@ -574,45 +1263,6 @@ private static string ReadNullTerminatedString(SegmentedList data, uint of
return Encoding.UTF8.GetString(bytes.ToArray(), 0, bytes.Count);
}
- #region Endianness helpers
-
- /// Little-endian uint16 read from byte array.
- private static ushort ReadU16LE(byte[] data, int offset)
- {
- return (ushort)(data[offset] | data[offset + 1] << 8);
- }
-
- /// Big-endian uint16 read from byte array.
- private static ushort ReadU16BE(byte[] data, int offset)
- {
- return (ushort)(data[offset] << 8 | data[offset + 1]);
- }
-
- private ushort ReadU16(byte[] data, int offset)
- {
- return m_bigEndian ? ReadU16BE(data, offset) : ReadU16LE(data, offset);
- }
-
- private uint ReadU32(byte[] data, int offset)
- {
- if (m_bigEndian)
- {
- return (uint)(data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]);
- }
- return BitConverter.ToUInt32(data, offset);
- }
-
- private ulong ReadU64(byte[] data, int offset)
- {
- if (m_bigEndian)
- {
- return ((ulong)ReadU32(data, offset) << 32) | ReadU32(data, offset + 4);
- }
- return BitConverter.ToUInt64(data, offset);
- }
-
- #endregion
-
#endregion
}
}
diff --git a/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs b/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs
index 3e6720ac8..7091c1e42 100644
--- a/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs
+++ b/src/TraceEvent/Symbols/R2RPerfMapSymbolModule.cs
@@ -168,6 +168,78 @@ private void FinalizeSymbols()
_symbols.Sort((a, b) => a.StartAddress.CompareTo(b.StartAddress));
}
+ ///
+ /// Reads only the Signature and Version from an R2R perfmap file without parsing
+ /// the full symbol table. This is used for cheap identity validation (analogous to
+ /// for ELF files).
+ /// Returns false if the file cannot be read or does not contain valid header metadata.
+ ///
+ internal static bool ReadSignatureAndVersion(string filePath, out Guid signature, out uint version)
+ {
+ signature = Guid.Empty;
+ version = 0;
+ bool foundSignature = false;
+ bool foundVersion = false;
+
+ using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
+ using (var reader = new StreamReader(stream))
+ {
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ int firstSpace = line.IndexOf(' ');
+ if (firstSpace == -1) continue;
+
+ string addressStr = line.Substring(0, firstSpace);
+ if (!uint.TryParse(addressStr, System.Globalization.NumberStyles.HexNumber, null, out uint address))
+ {
+ continue;
+ }
+
+ // Signature marker
+ if (address == 0xFFFFFFFF)
+ {
+ string remainder = line.Substring(firstSpace + 1);
+ int secondSpace = remainder.IndexOf(' ');
+ if (secondSpace >= 0)
+ {
+ string name = remainder.Substring(secondSpace + 1);
+ if (Guid.TryParse(name, out signature))
+ {
+ foundSignature = true;
+ }
+ }
+ }
+ // Version marker
+ else if (address == 0xFFFFFFFE)
+ {
+ string remainder = line.Substring(firstSpace + 1);
+ int secondSpace = remainder.IndexOf(' ');
+ if (secondSpace >= 0)
+ {
+ string name = remainder.Substring(secondSpace + 1);
+ if (uint.TryParse(name, out version))
+ {
+ foundVersion = true;
+ }
+ }
+ }
+ // Once we have both, we can stop — no need to parse the symbol table.
+ else if (address < 0xFFFFFFFB)
+ {
+ break;
+ }
+
+ if (foundSignature && foundVersion)
+ {
+ break;
+ }
+ }
+ }
+
+ return foundSignature && foundVersion;
+ }
+
private int BinarySearch(uint rva)
{
int left = 0;
diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs
index 7f354b2b4..b7ea16039 100644
--- a/src/TraceEvent/Symbols/SymbolReader.cs
+++ b/src/TraceEvent/Symbols/SymbolReader.cs
@@ -34,6 +34,8 @@ public SymbolReader(TextWriter log, string nt_symbol_path = null, DelegatingHand
m_symbolModuleCache = new Cache(10);
m_pdbPathCache = new Cache(10);
m_r2rPerfMapPathCache = new Cache(10);
+ m_elfPathCache = new Cache(10);
+ m_elfModuleCache = new Cache(10);
m_symbolPath = nt_symbol_path;
if (m_symbolPath == null)
@@ -272,7 +274,7 @@ public string FindSymbolFilePath(string pdbFileName, Guid pdbIndexGuid, int pdbI
}
else
{
- m_log.WriteLine("FindSymbolFilePath: location {0} is remote and cacheOnly set, giving up.", filePath);
+ m_log.WriteLine("FindSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", filePath);
}
}
if (pdbPath != null)
@@ -306,19 +308,38 @@ public string FindSymbolFilePath(string pdbFileName, Guid pdbIndexGuid, int pdbI
return pdbPath;
}
- internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSignature, int perfMapVersion)
+ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSignature, int perfMapVersion, string dllFilePath = null)
{
m_log.WriteLine("FindR2RPerfMapSymbolFile: *{{ Locating R2R perfmap symbol file {0} Signature {1} Version {2}", perfMapName, perfMapSignature, perfMapVersion);
string indexPath = null;
string perfMapPath = null;
string symbolCacheTargetPath = null;
+ string perfMapSimpleName = Path.GetFileName(perfMapName);
R2RPerfMapSignature cacheKey = new R2RPerfMapSignature() { Name = perfMapName, Signature = perfMapSignature, Version = perfMapVersion };
if (m_r2rPerfMapPathCache.TryGet(cacheKey, out perfMapPath))
{
m_log.WriteLine("FindR2RPerfMapSymbolFile: }} Hit Cache, returning {0}", perfMapPath);
return perfMapPath;
}
+
+ // Check next to the binary first (mirrors PDB local search).
+ if (perfMapPath == null && dllFilePath != null)
+ {
+ string dllDir = Path.GetDirectoryName(dllFilePath);
+ if (!string.IsNullOrEmpty(dllDir))
+ {
+ string candidate = Path.Combine(dllDir, perfMapSimpleName);
+ m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Checking relative to DLL path {0}", candidate);
+ if (R2RPerfMapMatches(candidate, perfMapSignature, perfMapVersion))
+ {
+ perfMapPath = candidate;
+ }
+ }
+ }
+
+ if (perfMapPath == null)
+ {
SymbolPath path = new SymbolPath(SymbolPath);
foreach (SymbolPathElement element in path.Elements)
{
@@ -345,10 +366,10 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig
}
else
{
- string filePath = Path.Combine(element.Target, perfMapName);
+ string filePath = Path.Combine(element.Target, perfMapSimpleName);
if ((Options & SymbolReaderOptions.CacheOnly) == 0 || !element.IsRemote)
{
- if (File.Exists(filePath))
+ if (R2RPerfMapMatches(filePath, perfMapSignature, perfMapVersion, checkSecurity: false))
{
perfMapPath = filePath;
break;
@@ -356,10 +377,11 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig
}
else
{
- m_log.WriteLine("FindR2RPerfMapSymbolFilePath: location {0} is remote and cacheOnly set, giving up.", filePath);
+ m_log.WriteLine("FindR2RPerfMapSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", filePath);
}
}
}
+ }
if (perfMapPath != null)
{
@@ -380,6 +402,202 @@ internal string FindR2RPerfMapSymbolFilePath(string perfMapName, Guid perfMapSig
return perfMapPath;
}
+ ///
+ /// Given an ELF module's filename and GNU build-id, attempts to find the corresponding
+ /// debug symbol file (.debug) or the binary itself from symbol servers and local paths.
+ /// Tries debug symbols (_.debug/elf-buildid-sym-{buildId}/_.debug) first, then falls
+ /// back to the binary ({filename}/elf-buildid-{buildId}/{filename}).
+ ///
+ /// The simple filename of the ELF module (e.g., "libcoreclr.so")
+ /// The GNU build-id as a lowercase hex string
+ /// The local file path to the downloaded symbol file, or null if not found.
+ public string FindElfSymbolFilePath(string fileName, string buildId, string elfFilePath = null)
+ {
+ if (fileName == null)
+ {
+ throw new ArgumentNullException(nameof(fileName));
+ }
+
+ if (buildId == null)
+ {
+ throw new ArgumentNullException(nameof(buildId));
+ }
+
+ m_log.WriteLine("FindElfSymbolFilePath: *{{ Searching for {0} with BuildId {1}", fileName, buildId);
+
+ string simpleFileName = Path.GetFileName(fileName);
+
+ // Normalize the build ID to lowercase. Build IDs vary in length depending on the
+ // hash algorithm (e.g., SHA-1 = 40 hex chars, MD5/UUID = 32), so we use the exact
+ // value without padding.
+ string normalizedBuildId = buildId.ToLowerInvariant();
+
+ ElfBuildIdSignature cacheKey = new ElfBuildIdSignature() { FileName = simpleFileName, BuildId = normalizedBuildId };
+ if (m_elfPathCache.TryGet(cacheKey, out string cachedPath))
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: }} Hit Cache, returning {0}", cachedPath ?? "NULL");
+ return cachedPath;
+ }
+
+ // SSQP key conventions for ELF debug symbols and binaries.
+ string debugIndexPath = $"_.debug/elf-buildid-sym-{normalizedBuildId}/_.debug";
+ string binaryIndexPath = $"{simpleFileName}/elf-buildid-{normalizedBuildId}/{simpleFileName}";
+
+ string resultPath = null;
+
+ // Phase 1: Check for debug symbol files adjacent to the binary (mirrors PDB local search).
+ // Only look for dedicated debug files here — the binary itself is deferred to Phase 3.
+ if (elfFilePath != null)
+ {
+ string elfDir = Path.GetDirectoryName(elfFilePath);
+ if (!string.IsNullOrEmpty(elfDir))
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: Checking relative to ELF binary path {0}", elfFilePath);
+ string basePath = elfFilePath;
+
+ // Try {path}.debug
+ string candidate = basePath + ".debug";
+ if (ElfBuildIdMatches(candidate, normalizedBuildId))
+ {
+ resultPath = candidate;
+ }
+
+ // Try {path}.dbg
+ if (resultPath == null)
+ {
+ candidate = basePath + ".dbg";
+ if (ElfBuildIdMatches(candidate, normalizedBuildId))
+ {
+ resultPath = candidate;
+ }
+ }
+
+ // Read .gnu_debuglink from the binary if it exists locally.
+ // The debuglink section contains the exact filename of the companion debug file.
+ if (resultPath == null)
+ {
+ string debugLink = null;
+ if (File.Exists(basePath))
+ {
+ debugLink = ElfSymbolModule.ReadDebugLink(basePath);
+ if (debugLink != null)
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: Binary has .gnu_debuglink = {0}", debugLink);
+ }
+ }
+
+ if (debugLink != null)
+ {
+ // Try {bindir}/{debuglink}
+ candidate = Path.Combine(elfDir, debugLink);
+ if (ElfBuildIdMatches(candidate, normalizedBuildId))
+ {
+ resultPath = candidate;
+ }
+
+ // Try {bindir}/.debug/{debuglink}
+ if (resultPath == null)
+ {
+ candidate = Path.Combine(elfDir, ".debug", debugLink);
+ if (ElfBuildIdMatches(candidate, normalizedBuildId))
+ {
+ resultPath = candidate;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Phase 2: Search symbol servers and symbol path directories.
+ if (resultPath == null)
+ {
+ SymbolPath path = new SymbolPath(SymbolPath);
+ foreach (SymbolPathElement element in path.Elements)
+ {
+ if (element.IsSymServer)
+ {
+ string cache = element.Cache;
+ if (cache == null)
+ {
+ cache = path.DefaultSymbolCache();
+ }
+
+ // Try debug symbols first (preferred — has .symtab with full symbols).
+ resultPath = GetFileFromServer(element.Target, debugIndexPath, Path.Combine(cache, debugIndexPath));
+ if (resultPath != null)
+ {
+ break;
+ }
+
+ // Fall back to the binary (may only have .dynsym).
+ resultPath = GetFileFromServer(element.Target, binaryIndexPath, Path.Combine(cache, binaryIndexPath));
+ if (resultPath != null)
+ {
+ break;
+ }
+ }
+ else
+ {
+ string target = element.Target;
+ if (target != null)
+ {
+ if ((Options & SymbolReaderOptions.CacheOnly) != 0 && element.IsRemote)
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: location {0} is remote and cacheOnly set, skipping.", target);
+ continue;
+ }
+
+ // Try SSQP-structured debug symbols first.
+ string debugPath = Path.Combine(target, debugIndexPath);
+ if (ElfBuildIdMatches(debugPath, normalizedBuildId, checkSecurity: false))
+ {
+ resultPath = debugPath;
+ break;
+ }
+
+ // Try SSQP-structured binary.
+ string binaryPath = Path.Combine(target, binaryIndexPath);
+ if (ElfBuildIdMatches(binaryPath, normalizedBuildId, checkSecurity: false))
+ {
+ resultPath = binaryPath;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Phase 3: Last resort — try the binary itself (has .dynsym at minimum).
+ // This is deferred until after symbol servers so we prefer proper debug symbols
+ // (.symtab) over the stripped binary whenever a symbol server can provide them.
+ if (resultPath == null && elfFilePath != null)
+ {
+ if (ElfBuildIdMatches(elfFilePath, normalizedBuildId))
+ {
+ resultPath = elfFilePath;
+ }
+ }
+
+ if (resultPath != null)
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: *}} Successfully found ELF symbols for {0} BuildId {1} at {2}", simpleFileName, normalizedBuildId, resultPath);
+ }
+ else
+ {
+ string where = "";
+ if ((Options & SymbolReaderOptions.CacheOnly) != 0)
+ {
+ where = " in local cache";
+ }
+
+ m_log.WriteLine("FindElfSymbolFilePath: *}} Failed to find ELF symbols for {0}{1} BuildId {2}", simpleFileName, where, normalizedBuildId);
+ }
+
+ m_elfPathCache.Add(cacheKey, resultPath);
+ return resultPath;
+ }
+
// Find an executable file path (not a PDB) based on information about the file image.
///
/// This API looks up an executable file, by its build-timestamp and size (on a symbol server), 'fileName' should be
@@ -498,6 +716,25 @@ internal R2RPerfMapSymbolModule OpenR2RPerfMapSymbolFile(string filePath, uint l
return new R2RPerfMapSymbolModule(filePath, loadedLayoutTextOffset);
}
+ ///
+ /// Opens an ELF symbol module, returning a cached instance if the same file and load
+ /// parameters have been seen before. This avoids re-parsing large ELF debug files when
+ /// the same binary is loaded across multiple processes in a trace.
+ ///
+ internal ElfSymbolModule OpenElfSymbolFile(string filePath, ulong pVaddr, ulong pOffset)
+ {
+ var cacheKey = new ElfModuleSignature() { FilePath = filePath, VAddr = pVaddr, Offset = pOffset };
+ if (m_elfModuleCache.TryGet(cacheKey, out ElfSymbolModule cached))
+ {
+ m_log.WriteLine("OpenElfSymbolFile: Cache hit for {0}", filePath);
+ return cached;
+ }
+
+ var module = new ElfSymbolModule(filePath, pVaddr, pOffset);
+ m_elfModuleCache.Add(cacheKey, module);
+ return module;
+ }
+
// Various state that controls symbol and source file lookup.
///
/// The symbol path used to look up PDB symbol files. Set when the reader is initialized.
@@ -510,6 +747,9 @@ public string SymbolPath
m_symbolPath = value;
m_symbolModuleCache.Clear();
m_pdbPathCache.Clear();
+ m_r2rPerfMapPathCache.Clear();
+ m_elfPathCache.Clear();
+ m_elfModuleCache.Clear();
m_log.WriteLine("Symbol Path Updated to {0}", m_symbolPath);
m_log.WriteLine("Symbol Path update forces clearing Pdb lookup cache");
}
@@ -583,6 +823,9 @@ public SymbolReaderOptions Options
{
_Options = value;
m_pdbPathCache.Clear();
+ m_r2rPerfMapPathCache.Clear();
+ m_elfPathCache.Clear();
+ m_elfModuleCache.Clear();
m_log.WriteLine("Setting SymbolReaderOptions forces clearing Pdb lookup cache");
}
}
@@ -943,7 +1186,102 @@ private bool PdbMatches(string filePath, Guid pdbGuid, int pdbAge, bool checkSec
}
///
- /// Fetches a file from the server 'serverPath' with pdb signature path 'pdbSigPath' (concatenate them with a / or \ separator
+ /// Returns true if 'filePath' exists and is an R2R perfmap file whose Signature and Version match.
+ /// Analogous to for PDB files.
+ ///
+ private bool R2RPerfMapMatches(string filePath, Guid expectedSignature, int expectedVersion, bool checkSecurity = true)
+ {
+ try
+ {
+ if (File.Exists(filePath))
+ {
+ if (checkSecurity && !CheckSecurity(filePath))
+ {
+ m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Aborting, security check failed on {0}", filePath);
+ return false;
+ }
+
+ if (R2RPerfMapSymbolModule.ReadSignatureAndVersion(filePath, out Guid actualSignature, out uint actualVersion))
+ {
+ if (actualSignature == expectedSignature && actualVersion == (uint)expectedVersion)
+ {
+ return true;
+ }
+ else
+ {
+ m_log.WriteLine("FindR2RPerfMapSymbolFilePath: ************ FOUND R2R perfmap {0} has Signature {1} Version {2} != Desired Signature {3} Version {4}",
+ filePath, actualSignature, actualVersion, expectedSignature, expectedVersion);
+ }
+ }
+ else
+ {
+ m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Could not read signature/version from {0}", filePath);
+ }
+ }
+ else
+ {
+ m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Probed file location {0} does not exist", filePath);
+ }
+ }
+ catch (Exception e)
+ {
+ m_log.WriteLine("FindR2RPerfMapSymbolFilePath: Aborting match of {0} Exception thrown: {1}", filePath, e.Message);
+ }
+ return false;
+ }
+
+ ///
+ /// Returns true if 'filePath' exists and is an ELF file whose GNU build-id matches 'expectedBuildId'.
+ /// Analogous to for PDB files.
+ ///
+ private bool ElfBuildIdMatches(string filePath, string expectedBuildId, bool checkSecurity = true)
+ {
+ try
+ {
+ if (File.Exists(filePath))
+ {
+ if (checkSecurity && !CheckSecurity(filePath))
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: Aborting, security check failed on {0}", filePath);
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(expectedBuildId))
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: No expected build-id provided, cannot verify match for {0}", filePath);
+ return false;
+ }
+
+ string actualBuildId = ElfSymbolModule.ReadBuildId(filePath);
+ if (actualBuildId != null && string.Equals(actualBuildId, expectedBuildId, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ else if (actualBuildId == null)
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: Could not read build-id from {0} (may be stripped)", filePath);
+ }
+ else
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: ************ FOUND ELF file {0} has build-id {1} != expected {2}",
+ filePath, actualBuildId, expectedBuildId);
+ }
+ }
+ else
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: Probed file location {0} does not exist", filePath);
+ }
+ }
+ catch (Exception e)
+ {
+ m_log.WriteLine("FindElfSymbolFilePath: Aborting match of {0} Exception thrown: {1}", filePath, e.Message);
+ }
+ return false;
+ }
+
+
+ ///
+ /// Fetches a file from the server 'serverPath' with pdb signature path 'pdbSigPath'(concatenate them with a / or \ separator
/// to form a complete URL or path name). It will place the file in 'fullDestPath' It will return true if successful
/// If 'contentTypeFilter is present, this predicate is called with the URL content type (e.g. application/octet-stream)
/// and if it returns false, it fails. This ensures that things that are the wrong content type (e.g. redirects to
@@ -1642,12 +1980,32 @@ private struct R2RPerfMapSignature : IEquatable
public int Version;
}
+ // Used as the key to the m_elfPathCache.
+ private struct ElfBuildIdSignature : IEquatable
+ {
+ public override int GetHashCode() { return HashCode.Combine(FileName, BuildId); }
+ public bool Equals(ElfBuildIdSignature other) { return FileName == other.FileName && BuildId == other.BuildId; }
+ public string FileName;
+ public string BuildId;
+ }
+
+ private struct ElfModuleSignature : IEquatable
+ {
+ public override int GetHashCode() { return HashCode.Combine(FilePath, VAddr, Offset); }
+ public bool Equals(ElfModuleSignature other) { return FilePath == other.FilePath && VAddr == other.VAddr && Offset == other.Offset; }
+ public string FilePath;
+ public ulong VAddr;
+ public ulong Offset;
+ }
+
internal TextWriter m_log;
private string m_SymbolCacheDirectory;
private string m_SourceCacheDirectory;
private Cache m_symbolModuleCache;
private Cache m_pdbPathCache;
private Cache m_r2rPerfMapPathCache;
+ private Cache m_elfPathCache;
+ private Cache m_elfModuleCache;
private string m_symbolPath;
#endregion
diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs
index d1e1578f3..e715f173c 100644
--- a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs
+++ b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfBuilder.cs
@@ -15,6 +15,8 @@ internal class ElfBuilder
private bool m_bigEndian = false;
private ulong m_pVaddr = 0x400000;
private ulong m_pOffset = 0;
+ private byte[] m_buildId = null;
+ private string m_debugLink = null;
private readonly List m_symtabSymbols = new List();
private readonly List m_dynsymSymbols = new List();
@@ -28,14 +30,21 @@ private struct SymbolDef
// ELF section types.
private const uint SHT_NULL = 0;
+ private const uint SHT_PROGBITS = 1;
private const uint SHT_STRTAB = 3;
private const uint SHT_SYMTAB = 2;
private const uint SHT_DYNSYM = 11;
+ // ELF program header types.
+ private const uint PT_NOTE = 4;
+
// Symbol type helpers.
private const byte STT_FUNC = 2;
private const byte STB_GLOBAL = 1;
+ // GNU build-id note type.
+ private const uint NT_GNU_BUILD_ID = 3;
+
public ElfBuilder Set64Bit(bool is64Bit)
{
m_is64Bit = is64Bit;
@@ -58,6 +67,25 @@ public ElfBuilder SetPTLoad(ulong pVaddr, ulong pOffset)
return this;
}
+ ///
+ /// Sets the GNU build-id that will be embedded as a PT_NOTE program header.
+ ///
+ public ElfBuilder SetBuildId(byte[] buildId)
+ {
+ m_buildId = buildId;
+ return this;
+ }
+
+ ///
+ /// Sets the .gnu_debuglink section filename. When set, the builder adds a .shstrtab
+ /// section so ReadDebugLink can find the section by name.
+ ///
+ public ElfBuilder SetDebugLink(string filename)
+ {
+ m_debugLink = filename;
+ return this;
+ }
+
///
/// Adds a STT_FUNC symbol to the .symtab section.
///
@@ -105,7 +133,7 @@ public ElfBuilder AddDynFunction(string name, ulong virtualAddress, ulong size)
///
/// Builds a complete ELF binary and returns it as a byte array.
- /// Layout: [ELF Header] [Section Data...] [Section Headers]
+ /// Layout: [ELF Header] [Section Data...] [DebugLink Data] [ShStrTab Data] [Note Data] [Program Headers] [Section Headers]
///
public byte[] Build()
{
@@ -118,11 +146,20 @@ public byte[] Build()
// [2] .symtab (symbol table)
// [3] .dynstr (string table for .dynsym) — only if dynsym symbols exist
// [4] .dynsym — only if dynsym symbols exist
+ // [N] .gnu_debuglink — only if debuglink is set
+ // [N+1] .shstrtab — only if debuglink is set (needed for section names)
bool hasDynsym = m_dynsymSymbols.Count > 0;
+ bool hasBuildId = m_buildId != null;
+ bool hasDebugLink = m_debugLink != null;
int sectionCount = hasDynsym ? 5 : 3;
+ if (hasDebugLink)
+ {
+ sectionCount += 2; // .gnu_debuglink + .shstrtab
+ }
int ehSize = m_is64Bit ? 64 : 52;
int shEntSize = m_is64Bit ? 64 : 40;
+ int phEntSize = m_is64Bit ? 56 : 32;
// Build string table for .symtab.
byte[] strtab = BuildStringTable(m_symtabSymbols, out int[] strtabOffsets);
@@ -140,6 +177,30 @@ public byte[] Build()
dynsym = BuildSymbolTable(m_dynsymSymbols, dynstrOffsets);
}
+ // Build note data for GNU build-id if requested.
+ byte[] noteData = null;
+ if (hasBuildId)
+ {
+ noteData = BuildBuildIdNote(m_buildId);
+ }
+
+ // Build .gnu_debuglink and .shstrtab section data if requested.
+ byte[] debugLinkData = null;
+ byte[] shstrtab = null;
+ int debugLinkShName = 0;
+ int shstrtabShName = 0;
+ int debugLinkSectionIndex = 0;
+ int shstrtabSectionIndex = 0;
+ if (hasDebugLink)
+ {
+ debugLinkData = BuildDebugLinkSection(m_debugLink);
+ debugLinkSectionIndex = hasDynsym ? 5 : 3;
+ shstrtabSectionIndex = debugLinkSectionIndex + 1;
+
+ // Build .shstrtab: "\0.gnu_debuglink\0.shstrtab\0"
+ shstrtab = BuildShStrTab(out debugLinkShName, out shstrtabShName);
+ }
+
// Section data starts right after the ELF header.
long dataStart = ehSize;
@@ -148,16 +209,44 @@ public byte[] Build()
long symtabOffset = strtabOffset + strtab.Length;
long dynstrOffset = symtabOffset + symtab.Length;
long dynsymOffset = hasDynsym ? dynstrOffset + dynstr.Length : dynstrOffset;
- long sectionHeadersOffset = hasDynsym ? dynsymOffset + dynsym.Length : dynstrOffset;
+ long afterSections = hasDynsym ? dynsymOffset + dynsym.Length : dynstrOffset;
+
+ // Write debuglink and shstrtab after other section data.
+ long debugLinkOffset = afterSections;
+ long shstrtabOffset = hasDebugLink ? debugLinkOffset + debugLinkData.Length : afterSections;
+ long afterDebugLink = hasDebugLink ? shstrtabOffset + shstrtab.Length : afterSections;
- // Align section headers to 8-byte boundary.
+ // Write note data after sections.
+ long noteOffset = afterDebugLink;
+ long afterNote = hasBuildId ? noteOffset + noteData.Length : afterDebugLink;
+
+ // Write program headers after note data (align to 8 bytes).
+ long phOffset = 0;
+ ushort phNum = 0;
+ if (hasBuildId)
+ {
+ phOffset = afterNote;
+ if (phOffset % 8 != 0)
+ {
+ phOffset += 8 - (phOffset % 8);
+ }
+ phNum = 1;
+ }
+
+ long afterPh = hasBuildId ? phOffset + phEntSize : afterNote;
+
+ // Section headers follow everything else (align to 8 bytes).
+ long sectionHeadersOffset = afterPh;
if (sectionHeadersOffset % 8 != 0)
{
sectionHeadersOffset += 8 - (sectionHeadersOffset % 8);
}
// Write ELF header.
- WriteElfHeader(writer, (ulong)sectionHeadersOffset, (ushort)sectionCount, (ushort)shEntSize);
+ ushort headerPhEntSize = hasBuildId ? (ushort)phEntSize : (ushort)0;
+ ushort eShstrndx = hasDebugLink ? (ushort)shstrtabSectionIndex : (ushort)0;
+ WriteElfHeader(writer, (ulong)sectionHeadersOffset, (ushort)sectionCount, (ushort)shEntSize,
+ (ulong)phOffset, headerPhEntSize, phNum, eShstrndx);
// Write section data.
writer.BaseStream.Seek(strtabOffset, SeekOrigin.Begin);
@@ -169,6 +258,28 @@ public byte[] Build()
writer.Write(dynsym);
}
+ // Write debuglink and shstrtab section data.
+ if (hasDebugLink)
+ {
+ writer.BaseStream.Seek(debugLinkOffset, SeekOrigin.Begin);
+ writer.Write(debugLinkData);
+ writer.Write(shstrtab);
+ }
+
+ // Write note data.
+ if (hasBuildId)
+ {
+ writer.BaseStream.Seek(noteOffset, SeekOrigin.Begin);
+ writer.Write(noteData);
+ }
+
+ // Write program headers.
+ if (hasBuildId)
+ {
+ writer.BaseStream.Seek(phOffset, SeekOrigin.Begin);
+ WriteProgramHeader(writer, PT_NOTE, (ulong)noteOffset, (ulong)noteData.Length);
+ }
+
// Pad to section header offset.
while (writer.BaseStream.Position < sectionHeadersOffset)
{
@@ -195,6 +306,17 @@ public byte[] Build()
WriteSectionHeader(writer, 0, SHT_DYNSYM, (ulong)dynsymOffset, (ulong)dynsym.Length, 3, (ulong)symEntSize);
}
+ if (hasDebugLink)
+ {
+ // .gnu_debuglink (SHT_PROGBITS)
+ WriteSectionHeader(writer, (uint)debugLinkShName, SHT_PROGBITS,
+ (ulong)debugLinkOffset, (ulong)debugLinkData.Length, 0, 0);
+
+ // .shstrtab (SHT_STRTAB)
+ WriteSectionHeader(writer, (uint)shstrtabShName, SHT_STRTAB,
+ (ulong)shstrtabOffset, (ulong)shstrtab.Length, 0, 0);
+ }
+
return ms.ToArray();
}
}
@@ -210,7 +332,8 @@ public void GetPTLoadParams(out ulong pVaddr, out ulong pOffset)
#region Private helpers
- private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, ushort eShentsize)
+ private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, ushort eShentsize,
+ ulong ePhoff, ushort ePhentsize, ushort ePhnum, ushort eShstrndx = 0)
{
// e_ident: magic + class + data + version + padding (16 bytes total).
writer.Write((byte)0x7f);
@@ -230,23 +353,23 @@ private void WriteElfHeader(BinaryWriter writer, ulong eShoff, ushort eShnum, us
if (m_is64Bit)
{
WriteUInt64(writer, 0); // e_entry
- WriteUInt64(writer, 0); // e_phoff
+ WriteUInt64(writer, ePhoff); // e_phoff
WriteUInt64(writer, eShoff); // e_shoff
}
else
{
WriteUInt32(writer, 0); // e_entry
- WriteUInt32(writer, 0); // e_phoff
+ WriteUInt32(writer, (uint)ePhoff); // e_phoff
WriteUInt32(writer, (uint)eShoff); // e_shoff
}
WriteUInt32(writer, 0); // e_flags
WriteUInt16(writer, (ushort)(m_is64Bit ? 64 : 52)); // e_ehsize
- WriteUInt16(writer, 0); // e_phentsize
- WriteUInt16(writer, 0); // e_phnum
+ WriteUInt16(writer, ePhentsize); // e_phentsize
+ WriteUInt16(writer, ePhnum); // e_phnum
WriteUInt16(writer, eShentsize); // e_shentsize
WriteUInt16(writer, eShnum); // e_shnum
- WriteUInt16(writer, 0); // e_shstrndx
+ WriteUInt16(writer, eShstrndx); // e_shstrndx
}
private void WriteSectionHeader(BinaryWriter writer, uint shName, uint shType,
@@ -347,6 +470,116 @@ private void WriteSymbolEntry(BinaryWriter writer, uint stName, ulong stValue, u
}
}
+ ///
+ /// Builds a .note.gnu.build-id note: namesz(4) + descsz(4) + type(4) + "GNU\0" + buildId.
+ ///
+ private byte[] BuildBuildIdNote(byte[] buildId)
+ {
+ using (var ms = new MemoryStream())
+ using (var writer = new BinaryWriter(ms))
+ {
+ WriteUInt32(writer, 4); // namesz: length of "GNU\0"
+ WriteUInt32(writer, (uint)buildId.Length); // descsz: length of build-id
+ WriteUInt32(writer, NT_GNU_BUILD_ID); // type: NT_GNU_BUILD_ID (3)
+ writer.Write((byte)'G'); // name: "GNU\0" (already 4-byte aligned)
+ writer.Write((byte)'N');
+ writer.Write((byte)'U');
+ writer.Write((byte)0);
+ writer.Write(buildId); // desc: build-id bytes
+
+ // Pad descriptor to 4-byte alignment.
+ int descPadding = ((buildId.Length + 3) & ~3) - buildId.Length;
+ for (int i = 0; i < descPadding; i++)
+ {
+ writer.Write((byte)0);
+ }
+
+ return ms.ToArray();
+ }
+ }
+
+ ///
+ /// Writes a single ELF program header entry (PT_NOTE).
+ ///
+ private void WriteProgramHeader(BinaryWriter writer, uint pType, ulong pOffset, ulong pFilesz)
+ {
+ if (m_is64Bit)
+ {
+ // Elf64_Phdr: p_type(4), p_flags(4), p_offset(8), p_vaddr(8), p_paddr(8), p_filesz(8), p_memsz(8), p_align(8)
+ WriteUInt32(writer, pType); // p_type
+ WriteUInt32(writer, 0); // p_flags
+ WriteUInt64(writer, pOffset); // p_offset
+ WriteUInt64(writer, 0); // p_vaddr
+ WriteUInt64(writer, 0); // p_paddr
+ WriteUInt64(writer, pFilesz); // p_filesz
+ WriteUInt64(writer, pFilesz); // p_memsz
+ WriteUInt64(writer, 4); // p_align
+ }
+ else
+ {
+ // Elf32_Phdr: p_type(4), p_offset(4), p_vaddr(4), p_paddr(4), p_filesz(4), p_memsz(4), p_flags(4), p_align(4)
+ WriteUInt32(writer, pType); // p_type
+ WriteUInt32(writer, (uint)pOffset); // p_offset
+ WriteUInt32(writer, 0); // p_vaddr
+ WriteUInt32(writer, 0); // p_paddr
+ WriteUInt32(writer, (uint)pFilesz); // p_filesz
+ WriteUInt32(writer, (uint)pFilesz); // p_memsz
+ WriteUInt32(writer, 0); // p_flags
+ WriteUInt32(writer, 4); // p_align
+ }
+ }
+
+ ///
+ /// Builds a .gnu_debuglink section: null-terminated filename + padding to 4 bytes + CRC32 (0).
+ ///
+ private static byte[] BuildDebugLinkSection(string filename)
+ {
+ using (var ms = new MemoryStream())
+ using (var writer = new BinaryWriter(ms))
+ {
+ byte[] nameBytes = Encoding.UTF8.GetBytes(filename);
+ writer.Write(nameBytes);
+ writer.Write((byte)0); // null terminator
+
+ // Pad to 4-byte alignment.
+ int nameLen = nameBytes.Length + 1;
+ int padding = ((nameLen + 3) & ~3) - nameLen;
+ for (int i = 0; i < padding; i++)
+ {
+ writer.Write((byte)0);
+ }
+
+ // CRC32 (not validated by ReadDebugLink, write 0).
+ writer.Write((uint)0);
+
+ return ms.ToArray();
+ }
+ }
+
+ ///
+ /// Builds a section name string table (.shstrtab) containing ".gnu_debuglink" and ".shstrtab".
+ /// Returns the sh_name offsets for each section.
+ ///
+ private static byte[] BuildShStrTab(out int debugLinkShName, out int shstrtabShName)
+ {
+ using (var ms = new MemoryStream())
+ {
+ ms.WriteByte(0); // Index 0: empty string
+
+ debugLinkShName = (int)ms.Position;
+ byte[] dlName = Encoding.UTF8.GetBytes(".gnu_debuglink");
+ ms.Write(dlName, 0, dlName.Length);
+ ms.WriteByte(0);
+
+ shstrtabShName = (int)ms.Position;
+ byte[] ssName = Encoding.UTF8.GetBytes(".shstrtab");
+ ms.Write(ssName, 0, ssName.Length);
+ ms.WriteByte(0);
+
+ return ms.ToArray();
+ }
+ }
+
#region Endianness helpers
private void WriteUInt16(BinaryWriter writer, ushort val)
diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs
index 68f10a6ea..2e8e527be 100644
--- a/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs
+++ b/src/TraceEvent/TraceEvent.Tests/Symbols/ElfSymbolModuleTests.cs
@@ -1,6 +1,8 @@
using System;
+using System.Diagnostics;
using System.IO;
using Microsoft.Diagnostics.Symbols;
+using Microsoft.Diagnostics.Tracing.Etlx;
using Xunit;
using Xunit.Abstractions;
@@ -443,6 +445,81 @@ public void NonZeroPOffset_AdjustsRvaCorrectly()
Assert.Equal(string.Empty, module.FindNameForRva(0x1000, ref symbolStart));
}
+ ///
+ /// Regression test for the libcoreclr.so bug where p_vaddr != p_offset.
+ /// In the real trace: p_vaddr=0x1c9060, p_offset=0x1c8060, pageSize=4096.
+ /// The Linux loader maps at PAGE_DOWN(p_vaddr) = 0x1c9000.
+ /// The caller (OpenElfSymbolsForModuleFile) page-aligns pVaddr and passes
+ /// the actual pOffset. LookupSymbolsForModule adds pOffset to
+ /// (address - ImageBase) so that the lookup RVA matches the ElfSymbolModule
+ /// formula (st_value - alignedPVaddr) + pOffset.
+ ///
+ [Fact]
+ public void NonPageAlignedPVaddr_CallerPageAligns()
+ {
+ // These values are from a real libcoreclr.so trace.
+ ulong rawPVaddr = 0x1c9060;
+ ulong rawPOffset = 0x1c8060;
+ // Note: rawPOffset (0x1c8060) differs from rawPVaddr — this is the root cause of the bug.
+ ulong pageSize = 4096;
+
+ // The caller (OpenElfSymbolsForModuleFile) page-aligns before passing to ElfSymbolModule.
+ ulong alignedPVaddr = rawPVaddr & ~(pageSize - 1); // 0x1c9000
+ Assert.Equal((ulong)0x1c9000, alignedPVaddr);
+
+ // Symbol at virtual address 0x1D0000 (inside the executable segment).
+ ulong symbolAddr = 0x1D0000;
+ ulong symbolSize = 0x100;
+
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(alignedPVaddr, rawPOffset)
+ .AddFunction("coreclr_execute_assembly", symbolAddr, symbolSize);
+
+ byte[] data = builder.Build();
+
+ // The caller passes (alignedPVaddr, rawPOffset) — the actual p_offset.
+ var module = CreateModule(data, alignedPVaddr, rawPOffset);
+
+ uint symbolStart = 0;
+ // The ElfSymbolModule RVA formula: (st_value - pVaddr) + pOffset
+ // = (0x1D0000 - 0x1c9000) + 0x1c8060 = 0x7000 + 0x1c8060 = 0x1CF060
+ // The caller (LookupSymbolsForModule) computes: (address - ImageBase) + pOffset
+ // = (st_value - alignedPVaddr) + pOffset — same value.
+ uint lookupRva = (uint)(symbolAddr - alignedPVaddr + rawPOffset);
+ Assert.Equal("coreclr_execute_assembly", module.FindNameForRva(lookupRva, ref symbolStart));
+ Assert.Equal(lookupRva, symbolStart);
+ }
+
+ ///
+ /// Verifies that ElfSymbolInfo.PageAlignedVirtualAddress correctly page-aligns p_vaddr.
+ ///
+ [Fact]
+ public void ElfSymbolInfo_PageAlignedVirtualAddress()
+ {
+ var info = new Microsoft.Diagnostics.Tracing.Etlx.ElfSymbolInfo();
+
+ // With page size set, non-aligned p_vaddr gets aligned.
+ info.VirtualAddress = 0x1c9060;
+ info.PageSize = 4096;
+ Assert.Equal((ulong)0x1c9000, info.PageAlignedVirtualAddress);
+
+ // Already-aligned p_vaddr stays the same.
+ info.VirtualAddress = 0x400000;
+ info.PageSize = 4096;
+ Assert.Equal((ulong)0x400000, info.PageAlignedVirtualAddress);
+
+ // PageSize=0 (unknown) returns raw VirtualAddress.
+ info.VirtualAddress = 0x1c9060;
+ info.PageSize = 0;
+ Assert.Equal((ulong)0x1c9060, info.PageAlignedVirtualAddress);
+
+ // 64K pages (ARM64).
+ info.VirtualAddress = 0x1c9060;
+ info.PageSize = 65536;
+ Assert.Equal((ulong)0x1c0000, info.PageAlignedVirtualAddress);
+ }
+
#endregion
#region Demangling Integration
@@ -636,6 +713,303 @@ public void FilePathConstructor_LoadsSymbols()
#endregion
+ #region ReadBuildId
+
+ [Fact]
+ public void ReadBuildId_ValidElf64_ReturnsBuildId()
+ {
+ byte[] buildId = new byte[] { 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89,
+ 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89,
+ 0xab, 0xcd, 0xef, 0x01 };
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(0x400000, 0)
+ .SetBuildId(buildId);
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadBuildId(path);
+ Assert.Equal("abcdef0123456789abcdef0123456789abcdef01", result);
+ });
+ }
+
+ [Fact]
+ public void ReadBuildId_ValidElf32_ReturnsBuildId()
+ {
+ byte[] buildId = new byte[] { 0xde, 0xad, 0xbe, 0xef };
+ var builder = new ElfBuilder()
+ .Set64Bit(false)
+ .SetPTLoad(0x400000, 0)
+ .SetBuildId(buildId);
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadBuildId(path);
+ Assert.Equal("deadbeef", result);
+ });
+ }
+
+ [Fact]
+ public void ReadBuildId_NoBuildId_ReturnsNull()
+ {
+ // ELF with no build-id note (no program headers).
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(0x400000, 0)
+ .AddFunction("test", 0x401000, 0x100);
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadBuildId(path);
+ Assert.Null(result);
+ });
+ }
+
+ [Fact]
+ public void ReadBuildId_NotElfFile_ReturnsNull()
+ {
+ byte[] data = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadBuildId(path);
+ Assert.Null(result);
+ });
+ }
+
+ [Fact]
+ public void ReadBuildId_EmptyFile_ReturnsNull()
+ {
+ RunWithTempFile(Array.Empty(), (path) =>
+ {
+ string result = ElfSymbolModule.ReadBuildId(path);
+ Assert.Null(result);
+ });
+ }
+
+ [Fact]
+ public void ReadBuildId_NonExistentFile_ReturnsNull()
+ {
+ string result = ElfSymbolModule.ReadBuildId(@"C:\nonexistent\path\fake.so");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ReadBuildId_BigEndianElf64_ReturnsBuildId()
+ {
+ byte[] buildId = new byte[] { 0x11, 0x22, 0x33, 0x44 };
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetBigEndian(true)
+ .SetPTLoad(0x400000, 0)
+ .SetBuildId(buildId);
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadBuildId(path);
+ Assert.Equal("11223344", result);
+ });
+ }
+
+ #endregion
+
+ #region ReadDebugLink
+
+ [Fact]
+ public void ReadDebugLink_WithDebugLink_ReturnsFilename()
+ {
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(0x400000, 0)
+ .SetDebugLink("libcoreclr.so.dbg");
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadDebugLink(path);
+ Assert.Equal("libcoreclr.so.dbg", result);
+ });
+ }
+
+ [Fact]
+ public void ReadDebugLink_WithDebugLinkElf32_ReturnsFilename()
+ {
+ var builder = new ElfBuilder()
+ .Set64Bit(false)
+ .SetPTLoad(0x400000, 0)
+ .SetDebugLink("mylib.debug");
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadDebugLink(path);
+ Assert.Equal("mylib.debug", result);
+ });
+ }
+
+ [Fact]
+ public void ReadDebugLink_WithDebugLinkAndBuildId_ReturnsBoth()
+ {
+ byte[] buildId = new byte[] { 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89,
+ 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89,
+ 0xab, 0xcd, 0xef, 0x01 };
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(0x400000, 0)
+ .SetBuildId(buildId)
+ .SetDebugLink("libcoreclr.so.dbg");
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string debugLink = ElfSymbolModule.ReadDebugLink(path);
+ Assert.Equal("libcoreclr.so.dbg", debugLink);
+
+ string buildIdResult = ElfSymbolModule.ReadBuildId(path);
+ Assert.Equal("abcdef0123456789abcdef0123456789abcdef01", buildIdResult);
+ });
+ }
+
+ [Fact]
+ public void ReadDebugLink_WithoutDebugLink_ReturnsNull()
+ {
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(0x400000, 0)
+ .AddFunction("test", 0x401000, 0x100);
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadDebugLink(path);
+ Assert.Null(result);
+ });
+ }
+
+ [Fact]
+ public void ReadDebugLink_InvalidFile_ReturnsNull()
+ {
+ byte[] data = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadDebugLink(path);
+ Assert.Null(result);
+ });
+ }
+
+ [Fact]
+ public void ReadDebugLink_NonExistentFile_ReturnsNull()
+ {
+ string result = ElfSymbolModule.ReadDebugLink(@"C:\nonexistent\path\fake.so");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ReadDebugLink_BigEndianElf64_ReturnsFilename()
+ {
+ var builder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetBigEndian(true)
+ .SetPTLoad(0x400000, 0)
+ .SetDebugLink("libtest.so.debug");
+
+ byte[] data = builder.Build();
+ RunWithTempFile(data, (path) =>
+ {
+ string result = ElfSymbolModule.ReadDebugLink(path);
+ Assert.Equal("libtest.so.debug", result);
+ });
+ }
+
+ #endregion
+
+ #region MatchOrInit tests
+
+ [Fact]
+ public void MatchOrInitPE_WhenNull_CreatesPESymbolInfo()
+ {
+ var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0);
+ var pe = moduleFile.MatchOrInitPE();
+ Assert.NotNull(pe);
+ Assert.IsType(pe);
+ Assert.Equal(ModuleBinaryFormat.PE, moduleFile.BinaryFormat);
+ }
+
+ [Fact]
+ public void MatchOrInitPE_WhenAlreadyPE_ReturnsSame()
+ {
+ var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0);
+ var pe1 = moduleFile.MatchOrInitPE();
+ var pe2 = moduleFile.MatchOrInitPE();
+ Assert.Same(pe1, pe2);
+ }
+
+ [Fact]
+ public void MatchOrInitPE_WhenElf_ReturnsNull()
+ {
+ var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0);
+ moduleFile.MatchOrInitElf(); // Set as ELF first
+
+ // Suppress Debug.Assert so we can verify the return value.
+ var listeners = new TraceListener[Trace.Listeners.Count];
+ Trace.Listeners.CopyTo(listeners, 0);
+ Trace.Listeners.Clear();
+ try
+ {
+ var pe = moduleFile.MatchOrInitPE();
+ Assert.Null(pe);
+ }
+ finally
+ {
+ Trace.Listeners.AddRange(listeners);
+ }
+ }
+
+ [Fact]
+ public void MatchOrInitElf_WhenNull_CreatesElfSymbolInfo()
+ {
+ var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0);
+ var elf = moduleFile.MatchOrInitElf();
+ Assert.NotNull(elf);
+ Assert.IsType(elf);
+ Assert.Equal(ModuleBinaryFormat.ELF, moduleFile.BinaryFormat);
+ }
+
+ [Fact]
+ public void MatchOrInitElf_WhenAlreadyElf_ReturnsSame()
+ {
+ var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0);
+ var elf1 = moduleFile.MatchOrInitElf();
+ var elf2 = moduleFile.MatchOrInitElf();
+ Assert.Same(elf1, elf2);
+ }
+
+ [Fact]
+ public void MatchOrInitElf_WhenPE_ReturnsNull()
+ {
+ var moduleFile = new TraceModuleFile(null, 0, (ModuleFileIndex)0);
+ moduleFile.MatchOrInitPE(); // Set as PE first
+
+ // Suppress Debug.Assert so we can verify the return value.
+ var listeners = new TraceListener[Trace.Listeners.Count];
+ Trace.Listeners.CopyTo(listeners, 0);
+ Trace.Listeners.Clear();
+ try
+ {
+ var elf = moduleFile.MatchOrInitElf();
+ Assert.Null(elf);
+ }
+ finally
+ {
+ Trace.Listeners.AddRange(listeners);
+ }
+ }
+
+ #endregion
+
#region Helpers
///
diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs
index 2a17ca2cc..bb0eec08b 100644
--- a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs
+++ b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs
@@ -1,4 +1,4 @@
-using Microsoft.Diagnostics.Symbols;
+using Microsoft.Diagnostics.Symbols;
using PerfView.TestUtilities;
using System;
using System.Collections.Generic;
@@ -543,6 +543,829 @@ public void HttpRequestIncludesMsfzAcceptHeader()
}
}
+ #region FindElfSymbolFilePath Tests
+
+ [Fact]
+ public void FindElfSymbolFilePath_DebugSymbolsFoundLocally()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-local-debug");
+ try
+ {
+ string buildId = "abc123";
+ string normalizedBuildId = buildId.ToLowerInvariant();
+
+ // Create SSQP debug symbol directory structure with valid ELF build-id.
+ string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId);
+ Directory.CreateDirectory(debugDir);
+ string debugFile = Path.Combine(debugDir, "_.debug");
+ File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ _symbolReader.SymbolPath = tempDir;
+ string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", buildId);
+
+ Assert.NotNull(result);
+ Assert.Equal(Path.GetFullPath(debugFile), Path.GetFullPath(result));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_BinaryFallbackLocally()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-local-binary");
+ try
+ {
+ string buildId = "def456";
+ string normalizedBuildId = buildId.ToLowerInvariant();
+
+ // Create only the binary directory structure (no debug symbols).
+ string binaryDir = Path.Combine(tempDir, "libcoreclr.so", "elf-buildid-" + normalizedBuildId);
+ Directory.CreateDirectory(binaryDir);
+ string binaryFile = Path.Combine(binaryDir, "libcoreclr.so");
+ File.WriteAllBytes(binaryFile, CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ _symbolReader.SymbolPath = tempDir;
+ string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", buildId);
+
+ Assert.NotNull(result);
+ Assert.Equal(Path.GetFullPath(binaryFile), Path.GetFullPath(result));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_DebugPreferredOverBinary()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-local-prefer-debug");
+ try
+ {
+ string buildId = "aabbcc";
+ string normalizedBuildId = buildId.ToLowerInvariant();
+
+ // Create both debug and binary directory structures with valid ELF build-ids.
+ string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId);
+ Directory.CreateDirectory(debugDir);
+ string debugFile = Path.Combine(debugDir, "_.debug");
+ File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ string binaryDir = Path.Combine(tempDir, "libtest.so", "elf-buildid-" + normalizedBuildId);
+ Directory.CreateDirectory(binaryDir);
+ string binaryFile = Path.Combine(binaryDir, "libtest.so");
+ File.WriteAllBytes(binaryFile, CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ _symbolReader.SymbolPath = tempDir;
+ string result = _symbolReader.FindElfSymbolFilePath("libtest.so", buildId);
+
+ Assert.NotNull(result);
+ Assert.Equal(Path.GetFullPath(debugFile), Path.GetFullPath(result));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_NotFoundLocally()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-local-empty");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+
+ _symbolReader.SymbolPath = tempDir;
+ string result = _symbolReader.FindElfSymbolFilePath("libmissing.so", "deadbeef");
+
+ Assert.Null(result);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Theory]
+ [InlineData("abcd", "abcd")]
+ [InlineData("ABC123", "abc123")]
+ [InlineData("aabbccdd00112233445566778899aabbccddeeff", "aabbccdd00112233445566778899aabbccddeeff")]
+ public void FindElfSymbolFilePath_BuildIdNormalization(string inputBuildId, string expectedNormalized)
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-buildid-norm");
+ try
+ {
+ // Create debug symbol directory structure with valid ELF build-id.
+ string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + expectedNormalized);
+ Directory.CreateDirectory(debugDir);
+ string debugFile = Path.Combine(debugDir, "_.debug");
+ File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(expectedNormalized));
+
+ _symbolReader.SymbolPath = tempDir;
+ string result = _symbolReader.FindElfSymbolFilePath("libnorm.so", inputBuildId);
+
+ Assert.NotNull(result);
+ Assert.Equal(Path.GetFullPath(debugFile), Path.GetFullPath(result));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_AbsolutePathExtractsFilename()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-abspath");
+ try
+ {
+ string buildId = "1122334455";
+ string normalizedBuildId = buildId.ToLowerInvariant();
+
+ // Create binary directory structure using just the simple filename.
+ string binaryDir = Path.Combine(tempDir, "libc.so.6", "elf-buildid-" + normalizedBuildId);
+ Directory.CreateDirectory(binaryDir);
+ string binaryFile = Path.Combine(binaryDir, "libc.so.6");
+ File.WriteAllBytes(binaryFile, CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ _symbolReader.SymbolPath = tempDir;
+ // Pass an absolute path — only the filename portion should be used for lookup.
+ string result = _symbolReader.FindElfSymbolFilePath("/usr/lib/x86_64-linux-gnu/libc.so.6", buildId);
+
+ Assert.NotNull(result);
+ Assert.Equal(Path.GetFullPath(binaryFile), Path.GetFullPath(result));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_CacheOnlySkipsRemotePaths()
+ {
+ // Use a UNC-style path that is "remote" but won't actually be accessed.
+ _symbolReader.SymbolPath = @"\\nonexistent-server\symbols";
+ _symbolReader.Options = SymbolReaderOptions.CacheOnly;
+
+ string result = _symbolReader.FindElfSymbolFilePath("libcoreclr.so", "aabbccdd");
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_CacheHitSkipsSearch()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-cache-hit");
+ try
+ {
+ string buildId = "cacced1d12";
+ string normalizedBuildId = buildId.ToLowerInvariant();
+
+ string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId);
+ Directory.CreateDirectory(debugDir);
+ string debugFile = Path.Combine(debugDir, "_.debug");
+ File.WriteAllBytes(debugFile, CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ _symbolReader.SymbolPath = tempDir;
+
+ // First call populates the cache.
+ string result1 = _symbolReader.FindElfSymbolFilePath("libcache.so", buildId);
+ Assert.NotNull(result1);
+
+ // Remove the file so only cache can return it.
+ File.Delete(debugFile);
+ Directory.Delete(debugDir);
+
+ string result2 = _symbolReader.FindElfSymbolFilePath("libcache.so", buildId);
+ Assert.Equal(result1, result2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_NegativeCacheReturnsNull()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-negative-cache");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ _symbolReader.SymbolPath = tempDir;
+
+ // First call: nothing found, null is cached.
+ string result1 = _symbolReader.FindElfSymbolFilePath("libnocache.so", "ffffffff");
+ Assert.Null(result1);
+
+ // Now create the file — but the negative cache should still return null.
+ string normalizedBuildId = "ffffffff";
+ string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId);
+ Directory.CreateDirectory(debugDir);
+ File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), new byte[] { 0x7F });
+
+ string result2 = _symbolReader.FindElfSymbolFilePath("libnocache.so", "ffffffff");
+ Assert.Null(result2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_DifferentBuildIdsAreDifferentCacheKeys()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-diff-keys");
+ try
+ {
+ string buildId1 = "aaaa";
+ string buildId2 = "bbbb";
+ string norm1 = buildId1;
+ string norm2 = buildId2;
+
+ // Only create debug symbols for the second build ID.
+ string debugDir2 = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + norm2);
+ Directory.CreateDirectory(debugDir2);
+ string debugFile2 = Path.Combine(debugDir2, "_.debug");
+ File.WriteAllBytes(debugFile2, CreateMinimalElfWithBuildId(norm2));
+
+ _symbolReader.SymbolPath = tempDir;
+
+ string result1 = _symbolReader.FindElfSymbolFilePath("lib.so", buildId1);
+ Assert.Null(result1);
+
+ string result2 = _symbolReader.FindElfSymbolFilePath("lib.so", buildId2);
+ Assert.NotNull(result2);
+ Assert.Equal(Path.GetFullPath(debugFile2), Path.GetFullPath(result2));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_DebugLinkDiscovery()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-debuglink");
+ try
+ {
+ string buildId = "aabb0011";
+
+ // Build an ELF binary with .gnu_debuglink pointing to "libtest.so.dbg".
+ var binaryBuilder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(0x400000, 0)
+ .SetBuildId(HexToBytes(buildId))
+ .SetDebugLink("libtest.so.dbg");
+ byte[] binaryData = binaryBuilder.Build();
+
+ // Build a debug ELF file with matching build-id.
+ byte[] debugData = CreateMinimalElfWithBuildId(buildId);
+
+ // Place the binary and debug file in the same directory.
+ Directory.CreateDirectory(tempDir);
+ string binaryPath = Path.Combine(tempDir, "libtest.so");
+ string debugPath = Path.Combine(tempDir, "libtest.so.dbg");
+ File.WriteAllBytes(binaryPath, binaryData);
+ File.WriteAllBytes(debugPath, debugData);
+
+ // Set symbol path to an empty location (no SSQP match),
+ // but provide elfFilePath so adjacent search kicks in.
+ // SecurityCheck is needed because adjacent search uses checkSecurity: true.
+ string emptyDir = Path.Combine(tempDir, "empty");
+ Directory.CreateDirectory(emptyDir);
+ _symbolReader.SymbolPath = emptyDir;
+ _symbolReader.SecurityCheck = _ => true;
+
+ string result = _symbolReader.FindElfSymbolFilePath("libtest.so", buildId, elfFilePath: binaryPath);
+
+ Assert.NotNull(result);
+ Assert.Equal(Path.GetFullPath(debugPath), Path.GetFullPath(result));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindElfSymbolFilePath_DebugLinkInSubdir()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-debuglink-subdir");
+ try
+ {
+ string buildId = "ccdd0022";
+
+ // Build an ELF binary with .gnu_debuglink pointing to "libfoo.debug".
+ var binaryBuilder = new ElfBuilder()
+ .Set64Bit(true)
+ .SetPTLoad(0x400000, 0)
+ .SetBuildId(HexToBytes(buildId))
+ .SetDebugLink("libfoo.debug");
+ byte[] binaryData = binaryBuilder.Build();
+
+ // Build a debug ELF file with matching build-id.
+ byte[] debugData = CreateMinimalElfWithBuildId(buildId);
+
+ // Place the binary in tempDir, debug file in {tempDir}/.debug/ subdir.
+ Directory.CreateDirectory(tempDir);
+ string debugSubDir = Path.Combine(tempDir, ".debug");
+ Directory.CreateDirectory(debugSubDir);
+
+ string binaryPath = Path.Combine(tempDir, "libfoo.so");
+ string debugPath = Path.Combine(debugSubDir, "libfoo.debug");
+ File.WriteAllBytes(binaryPath, binaryData);
+ File.WriteAllBytes(debugPath, debugData);
+
+ string emptyDir = Path.Combine(tempDir, "empty");
+ Directory.CreateDirectory(emptyDir);
+ _symbolReader.SymbolPath = emptyDir;
+ _symbolReader.SecurityCheck = _ => true;
+
+ string result = _symbolReader.FindElfSymbolFilePath("libfoo.so", buildId, elfFilePath: binaryPath);
+
+ Assert.NotNull(result);
+ Assert.Equal(Path.GetFullPath(debugPath), Path.GetFullPath(result));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ #endregion
+
+ #region FindR2RPerfMapSymbolFilePath Tests
+
+ [Fact]
+ public void FindR2RPerfMapSymbolFilePath_FoundLocally()
+ {
+ string tempDir = Path.Combine(OutputDir, "r2r-local");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ var sig = new Guid("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
+ int version = 1;
+ string perfMapFile = Path.Combine(tempDir, "CoreLib.r2rmap");
+ File.WriteAllBytes(perfMapFile, CreateMinimalR2RPerfMap(sig, version));
+
+ _symbolReader.SymbolPath = tempDir;
+ string result = _symbolReader.FindR2RPerfMapSymbolFilePath("CoreLib.r2rmap", sig, version);
+
+ Assert.NotNull(result);
+ Assert.Equal(perfMapFile, result);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindR2RPerfMapSymbolFilePath_NotFound()
+ {
+ string tempDir = Path.Combine(OutputDir, "r2r-empty");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+
+ _symbolReader.SymbolPath = tempDir;
+ var sig = new Guid("11111111-2222-3333-4444-555555555555");
+ string result = _symbolReader.FindR2RPerfMapSymbolFilePath("Missing.r2rmap", sig, 1);
+
+ Assert.Null(result);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindR2RPerfMapSymbolFilePath_CacheOnlySkipsRemotePaths()
+ {
+ _symbolReader.SymbolPath = @"\\nonexistent-server\symbols";
+ _symbolReader.Options = SymbolReaderOptions.CacheOnly;
+
+ var sig = new Guid("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
+ string result = _symbolReader.FindR2RPerfMapSymbolFilePath("CoreLib.r2rmap", sig, 1);
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void FindR2RPerfMapSymbolFilePath_CacheHitSkipsSearch()
+ {
+ string tempDir = Path.Combine(OutputDir, "r2r-cache-hit");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ var sig = new Guid("cc000000-0000-0000-0000-000000000000");
+ int version = 1;
+ string perfMapFile = Path.Combine(tempDir, "Cached.r2rmap");
+ File.WriteAllBytes(perfMapFile, CreateMinimalR2RPerfMap(sig, version));
+
+ _symbolReader.SymbolPath = tempDir;
+
+ // First call populates the cache.
+ string result1 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, version);
+ Assert.NotNull(result1);
+
+ // Remove the file so only cache can return it.
+ File.Delete(perfMapFile);
+
+ string result2 = _symbolReader.FindR2RPerfMapSymbolFilePath("Cached.r2rmap", sig, version);
+ Assert.Equal(result1, result2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void FindR2RPerfMapSymbolFilePath_DifferentSignaturesAreDifferentCacheKeys()
+ {
+ string tempDir = Path.Combine(OutputDir, "r2r-diff-keys");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ // No file on disk — both lookups will miss the file system.
+
+ _symbolReader.SymbolPath = tempDir;
+ var sig1 = new Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+ var sig2 = new Guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
+
+ string result1 = _symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig1, 1);
+ Assert.Null(result1);
+
+ // Now create the file with sig2's identity — sig2 should find it (not negatively cached).
+ string perfMapFile = Path.Combine(tempDir, "Test.r2rmap");
+ File.WriteAllBytes(perfMapFile, CreateMinimalR2RPerfMap(sig2, 1));
+
+ string result2 = _symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig2, 1);
+ Assert.NotNull(result2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ #endregion
+
+ #region FindSymbolFilePathForModule Tests
+
+ [Fact]
+ public void FindSymbolFilePathForModule_FileDoesNotExist()
+ {
+ string result = _symbolReader.FindSymbolFilePathForModule(@"C:\nonexistent\path\fake.dll");
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void FindSymbolFilePathForModule_InvalidPeFile()
+ {
+ string tempDir = Path.Combine(OutputDir, "module-invalid-pe");
+ Directory.CreateDirectory(tempDir);
+ string invalidDll = Path.Combine(tempDir, "invalid.dll");
+ File.WriteAllText(invalidDll, "This is not a valid PE file");
+
+ // Should not throw — exception is caught internally and null returned.
+ string result = _symbolReader.FindSymbolFilePathForModule(invalidDll);
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void FindSymbolFilePathForModule_FindsPdbNextToDll()
+ {
+ PrepareTestData();
+
+ // The test data has PDB files. We need a DLL that references one of those PDBs.
+ // Since we may not have a matching DLL in test data, verify the basic "file exists" path
+ // by testing that a DLL file that exists but has no CodeView signature returns null gracefully.
+ string tempDir = Path.Combine(OutputDir, "module-no-codeview");
+ Directory.CreateDirectory(tempDir);
+ // Create a minimal valid PE file (just MZ header + PE signature) that lacks CodeView info.
+ // The DOS stub points to PE signature at offset 0x80.
+ byte[] minimalPe = new byte[0x100];
+ minimalPe[0] = 0x4D; // 'M'
+ minimalPe[1] = 0x5A; // 'Z'
+ minimalPe[0x3C] = 0x80; // e_lfanew
+ minimalPe[0x80] = 0x50; // 'P'
+ minimalPe[0x81] = 0x45; // 'E'
+ minimalPe[0x82] = 0x00;
+ minimalPe[0x83] = 0x00;
+ string minimalDll = Path.Combine(tempDir, "minimal.dll");
+ File.WriteAllBytes(minimalDll, minimalPe);
+
+ // This PE file has no CodeView debug directory, so FindSymbolFilePathForModule
+ // should return null (either via no PDB signature or PE parsing gracefully failing).
+ string result = _symbolReader.FindSymbolFilePathForModule(minimalDll);
+ Assert.Null(result);
+ }
+
+ #endregion
+
+ #region Cache Invalidation Tests
+
+ [Fact]
+ public void ElfCache_ClearedWhenSymbolPathChanges()
+ {
+ string tempDir1 = Path.Combine(OutputDir, "elf-cache-inv1");
+ string tempDir2 = Path.Combine(OutputDir, "elf-cache-inv2");
+ try
+ {
+ // Set up: first path has nothing, second path has the file.
+ Directory.CreateDirectory(tempDir1);
+
+ string buildId = "cace0e0010";
+ string normalizedBuildId = buildId;
+ string debugDir = Path.Combine(tempDir2, "_.debug", "elf-buildid-sym-" + normalizedBuildId);
+ Directory.CreateDirectory(debugDir);
+ File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ // First search against empty path — null is cached.
+ _symbolReader.SymbolPath = tempDir1;
+ Assert.Null(_symbolReader.FindElfSymbolFilePath("lib.so", buildId));
+
+ // Change SymbolPath — cache should be cleared, so the new path is searched.
+ _symbolReader.SymbolPath = tempDir2;
+ Assert.NotNull(_symbolReader.FindElfSymbolFilePath("lib.so", buildId));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir1)) Directory.Delete(tempDir1, true);
+ if (Directory.Exists(tempDir2)) Directory.Delete(tempDir2, true);
+ }
+ }
+
+ [Fact]
+ public void R2RCache_ClearedWhenSymbolPathChanges()
+ {
+ string tempDir1 = Path.Combine(OutputDir, "r2r-cache-inv1");
+ string tempDir2 = Path.Combine(OutputDir, "r2r-cache-inv2");
+ try
+ {
+ Directory.CreateDirectory(tempDir1);
+ Directory.CreateDirectory(tempDir2);
+ var sig = new Guid("12345678-1234-1234-1234-123456789abc");
+ int version = 1;
+ File.WriteAllBytes(Path.Combine(tempDir2, "Test.r2rmap"), CreateMinimalR2RPerfMap(sig, version));
+
+ // First search against empty path — null is cached.
+ _symbolReader.SymbolPath = tempDir1;
+ Assert.Null(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, version));
+
+ // Change SymbolPath — cache should be cleared, so the new path is searched.
+ _symbolReader.SymbolPath = tempDir2;
+ Assert.NotNull(_symbolReader.FindR2RPerfMapSymbolFilePath("Test.r2rmap", sig, version));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir1)) Directory.Delete(tempDir1, true);
+ if (Directory.Exists(tempDir2)) Directory.Delete(tempDir2, true);
+ }
+ }
+
+ [Fact]
+ public void ElfCache_ClearedWhenOptionsChange()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-cache-opt");
+ try
+ {
+ string buildId = "00ee0010";
+ string normalizedBuildId = buildId;
+ string debugDir = Path.Combine(tempDir, "_.debug", "elf-buildid-sym-" + normalizedBuildId);
+ Directory.CreateDirectory(debugDir);
+ File.WriteAllBytes(Path.Combine(debugDir, "_.debug"), CreateMinimalElfWithBuildId(normalizedBuildId));
+
+ // First: find it successfully and cache it.
+ _symbolReader.SymbolPath = tempDir;
+ Assert.NotNull(_symbolReader.FindElfSymbolFilePath("lib.so", buildId));
+
+ // Remove the file.
+ Directory.Delete(debugDir, true);
+
+ // Without cache invalidation the cached path would still be returned.
+ // Changing Options should clear the cache, forcing a fresh lookup.
+ _symbolReader.Options = SymbolReaderOptions.CacheOnly;
+ Assert.Null(_symbolReader.FindElfSymbolFilePath("lib.so", buildId));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
+ }
+ }
+
+ #endregion
+
+ #region ELF Module Cache Tests
+
+ [Fact]
+ public void OpenElfSymbolFile_CacheHitReturnsSameInstance()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-mod-cache-hit");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ // Create a dummy file — ElfSymbolModule gracefully handles non-ELF content.
+ string elfFile = Path.Combine(tempDir, "libtest.so");
+ File.WriteAllBytes(elfFile, new byte[] { 0x00 });
+
+ var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0);
+ var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0);
+
+ Assert.Same(module1, module2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void OpenElfSymbolFile_DifferentParamsAreDifferentCacheEntries()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-mod-cache-diff");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ string elfFile = Path.Combine(tempDir, "libtest.so");
+ File.WriteAllBytes(elfFile, new byte[] { 0x00 });
+
+ var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0);
+ var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x2000, 0x0);
+
+ Assert.NotSame(module1, module2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void OpenElfSymbolFile_CacheClearedOnSymbolPathChange()
+ {
+ string tempDir = Path.Combine(OutputDir, "elf-mod-cache-clear");
+ try
+ {
+ Directory.CreateDirectory(tempDir);
+ string elfFile = Path.Combine(tempDir, "libtest.so");
+ File.WriteAllBytes(elfFile, new byte[] { 0x00 });
+
+ var module1 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0);
+
+ // Changing SymbolPath clears all caches including the module cache.
+ _symbolReader.SymbolPath = tempDir;
+
+ var module2 = _symbolReader.OpenElfSymbolFile(elfFile, 0x1000, 0x0);
+
+ // Should be a different instance because cache was cleared.
+ Assert.NotSame(module1, module2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Creates a minimal valid ELF64 little-endian binary with a GNU build-id note.
+ /// Used by tests that need a file whose build-id can be read by ReadBuildId.
+ ///
+ /// Lowercase hex string (e.g., "abc123" → 3 bytes: 0xab, 0xc1, 0x23).
+ private static byte[] CreateMinimalElfWithBuildId(string buildIdHex)
+ {
+ // Convert hex string to bytes.
+ int byteCount = buildIdHex.Length / 2;
+ byte[] buildIdBytes = new byte[byteCount];
+ for (int i = 0; i < byteCount; i++)
+ {
+ buildIdBytes[i] = byte.Parse(buildIdHex.Substring(i * 2, 2), NumberStyles.HexNumber);
+ }
+
+ // Build the GNU build-id note.
+ // Note header: namesz(4) + descsz(4) + type(4) = 12 bytes.
+ // Name: "GNU\0" = 4 bytes (already 4-byte aligned).
+ // Desc: buildId bytes, padded to 4-byte alignment.
+ uint descsz = (uint)buildIdBytes.Length;
+ uint descAligned = (descsz + 3) & ~3u;
+ int noteSize = 12 + 4 + (int)descAligned; // header + name + aligned desc
+
+ // ELF64 header (64 bytes) + one program header (56 bytes) + note.
+ int phOffset = 64;
+ int noteOffset = 64 + 56;
+ int totalSize = noteOffset + noteSize;
+ byte[] elf = new byte[totalSize];
+
+ // ELF header.
+ elf[0] = 0x7f; elf[1] = (byte)'E'; elf[2] = (byte)'L'; elf[3] = (byte)'F'; // magic
+ elf[4] = 2; // ELFCLASS64
+ elf[5] = 1; // ELFDATA2LSB
+ elf[6] = 1; // EV_CURRENT
+ // e_type = ET_EXEC (2)
+ elf[16] = 2;
+ // e_machine = EM_X86_64 (0x3e)
+ elf[18] = 0x3e;
+ // e_version = 1
+ elf[20] = 1;
+ // e_phoff = 64 (0x40)
+ elf[32] = 0x40;
+ // e_ehsize = 64 (0x40)
+ elf[52] = 0x40;
+ // e_phentsize = 56 (0x38)
+ elf[54] = 0x38;
+ // e_phnum = 1
+ elf[56] = 1;
+
+ // Program header (PT_NOTE at offset 64).
+ // p_type = PT_NOTE (4)
+ elf[phOffset] = 4;
+ // p_flags (at +4 for ELF64)
+ // p_offset (at +8) = noteOffset
+ elf[phOffset + 8] = (byte)noteOffset;
+ // p_filesz (at +32) = noteSize
+ elf[phOffset + 32] = (byte)(noteSize & 0xFF);
+ elf[phOffset + 33] = (byte)((noteSize >> 8) & 0xFF);
+ // p_memsz (at +40) = noteSize
+ elf[phOffset + 40] = (byte)(noteSize & 0xFF);
+ elf[phOffset + 41] = (byte)((noteSize >> 8) & 0xFF);
+
+ // Note at noteOffset.
+ int np = noteOffset;
+ // namesz = 4
+ elf[np] = 4;
+ // descsz
+ elf[np + 4] = (byte)(descsz & 0xFF);
+ elf[np + 5] = (byte)((descsz >> 8) & 0xFF);
+ // type = NT_GNU_BUILD_ID (3)
+ elf[np + 8] = 3;
+ // name = "GNU\0"
+ elf[np + 12] = (byte)'G';
+ elf[np + 13] = (byte)'N';
+ elf[np + 14] = (byte)'U';
+ elf[np + 15] = 0;
+ // desc = build-id bytes
+ Array.Copy(buildIdBytes, 0, elf, np + 16, buildIdBytes.Length);
+
+ return elf;
+ }
+
+ ///
+ /// Creates a minimal valid R2R perfmap text file with the given Signature and Version.
+ /// Used by tests that need a file whose Signature/Version can be read by R2RPerfMapSymbolModule.
+ ///
+ private static byte[] CreateMinimalR2RPerfMap(Guid signature, int version)
+ {
+ // R2R perfmap format: each line is "address size name"
+ // Signature: FFFFFFFF 0 {guid}
+ // Version: FFFFFFFE 0 {version}
+ string content = $"FFFFFFFF 0 {signature:D}\nFFFFFFFE 0 {version}\n";
+ return Encoding.UTF8.GetBytes(content);
+ }
+
+ ///
+ /// Converts a hex string to a byte array.
+ ///
+ private static byte[] HexToBytes(string hex)
+ {
+ int byteCount = hex.Length / 2;
+ byte[] bytes = new byte[byteCount];
+ for (int i = 0; i < byteCount; i++)
+ {
+ bytes[i] = byte.Parse(hex.Substring(i * 2, 2), NumberStyles.HexNumber);
+ }
+ return bytes;
+ }
+
protected void PrepareTestData()
{
lock (s_fileLock)
@@ -658,4 +1481,4 @@ protected override IEnumerable GetSourceLinkJson()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TraceEvent/TraceEvent.csproj b/src/TraceEvent/TraceEvent.csproj
index fbb141a7b..c8abab7f6 100644
--- a/src/TraceEvent/TraceEvent.csproj
+++ b/src/TraceEvent/TraceEvent.csproj
@@ -68,6 +68,7 @@
+
diff --git a/src/TraceEvent/TraceLog.cs b/src/TraceEvent/TraceLog.cs
index b7957ae87..9bf1902ca 100644
--- a/src/TraceEvent/TraceLog.cs
+++ b/src/TraceEvent/TraceLog.cs
@@ -1557,12 +1557,15 @@ private unsafe void SetupCallbacks(TraceEventDispatcher rawEvents)
// TODO review: is using the timestamp the best way to make the association
if (lastDbgData != null && data.TimeStampQPC == lastDbgData.TimeStampQPC)
{
- moduleFile.pdbName = lastDbgData.PdbFileName;
- moduleFile.pdbSignature = lastDbgData.GuidSig;
- moduleFile.pdbAge = lastDbgData.Age;
- // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time
- // We tolerate the exceptions, because it is a useful check most of the time
- Debug.Assert(RoughDllPdbMatch(moduleFile.fileName, moduleFile.pdbName));
+ if (moduleFile.MatchOrInitPE() is { } peInfo)
+ {
+ peInfo.PdbName = lastDbgData.PdbFileName;
+ peInfo.PdbSignature = lastDbgData.GuidSig;
+ peInfo.PdbAge = lastDbgData.Age;
+ // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time
+ // We tolerate the exceptions, because it is a useful check most of the time
+ Debug.Assert(RoughDllPdbMatch(moduleFile.fileName, moduleFile.PdbName));
+ }
}
moduleFile.timeDateStamp = data.TimeDateStamp;
moduleFile.imageChecksum = data.ImageChecksum;
@@ -1593,14 +1596,17 @@ private unsafe void SetupCallbacks(TraceEventDispatcher rawEvents)
hasPdbInfo = true;
// The ImageIDDbgID_RSDS may be after the ImageLoad
- if (lastTraceModuleFile != null && lastTraceModuleFileQPC == data.TimeStampQPC && string.IsNullOrEmpty(lastTraceModuleFile.pdbName))
- {
- lastTraceModuleFile.pdbName = data.PdbFileName;
- lastTraceModuleFile.pdbSignature = data.GuidSig;
- lastTraceModuleFile.pdbAge = data.Age;
- // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time
- // We tolerate the exceptions, because it is a useful check most of the time
- Debug.Assert(RoughDllPdbMatch(lastTraceModuleFile.fileName, lastTraceModuleFile.pdbName));
+ if (lastTraceModuleFile != null && lastTraceModuleFileQPC == data.TimeStampQPC && string.IsNullOrEmpty(lastTraceModuleFile.PdbName))
+ {
+ if (lastTraceModuleFile.MatchOrInitPE() is { } peInfo)
+ {
+ peInfo.PdbName = data.PdbFileName;
+ peInfo.PdbSignature = data.GuidSig;
+ peInfo.PdbAge = data.Age;
+ // There is no guarantee that the names of the DLL and PDB match, but they do 99% of the time
+ // We tolerate the exceptions, because it is a useful check most of the time
+ Debug.Assert(RoughDllPdbMatch(lastTraceModuleFile.fileName, lastTraceModuleFile.PdbName));
+ }
lastDbgData = null;
}
else // Or before (it is handled in ImageGroup callback above)
@@ -4140,7 +4146,7 @@ void IFastSerializable.FromStream(Deserializer deserializer)
}
int IFastSerializableVersion.Version
{
- get { return 76; }
+ get { return 77; }
}
int IFastSerializableVersion.MinimumVersionCanRead
{
@@ -4165,6 +4171,7 @@ int IFastSerializableVersion.MinimumReaderVersion
private string etlxFilePath;
private int memorySizeMeg;
private int eventsLost;
+ internal ulong systemPageSize;
private string osName;
private string osBuild;
private long bootTime100ns; // This is a windows FILETIME object
@@ -7069,11 +7076,14 @@ internal void ManagedModuleLoadOrUnload(ModuleLoadUnloadTraceData data, bool isL
process.Log.ModuleFiles.SetModuleFileName(module.ModuleFile, ilModulePath);
}
- if (module.ModuleFile.pdbSignature == Guid.Empty && data.ManagedPdbSignature != Guid.Empty)
+ if (module.ModuleFile.PdbSignature == Guid.Empty && data.ManagedPdbSignature != Guid.Empty)
{
- module.ModuleFile.pdbSignature = data.ManagedPdbSignature;
- module.ModuleFile.pdbAge = data.ManagedPdbAge;
- module.ModuleFile.pdbName = data.ManagedPdbBuildPath;
+ if (module.ModuleFile.MatchOrInitPE() is { } peInfo)
+ {
+ peInfo.PdbSignature = data.ManagedPdbSignature;
+ peInfo.PdbAge = data.ManagedPdbAge;
+ peInfo.PdbName = data.ManagedPdbBuildPath;
+ }
}
if (module.NativeModule != null)
@@ -7082,11 +7092,14 @@ internal void ManagedModuleLoadOrUnload(ModuleLoadUnloadTraceData data, bool isL
module.NativeModule.ModuleFile.managedModule.FilePath == module.ModuleFile.FilePath);
module.NativeModule.ModuleFile.managedModule = module.ModuleFile;
- if (nativePdbSignature != Guid.Empty && module.NativeModule.ModuleFile.pdbSignature == Guid.Empty)
+ if (nativePdbSignature != Guid.Empty && module.NativeModule.ModuleFile.PdbSignature == Guid.Empty)
{
- module.NativeModule.ModuleFile.pdbSignature = nativePdbSignature;
- module.NativeModule.ModuleFile.pdbAge = data.NativePdbAge;
- module.NativeModule.ModuleFile.pdbName = data.NativePdbBuildPath;
+ if (module.NativeModule.ModuleFile.MatchOrInitPE() is { } nativePeInfo)
+ {
+ nativePeInfo.PdbSignature = nativePdbSignature;
+ nativePeInfo.PdbAge = data.NativePdbAge;
+ nativePeInfo.PdbName = data.NativePdbBuildPath;
+ }
}
module.InitializeNativeModuleIsReadyToRun();
@@ -7128,7 +7141,6 @@ internal TraceModuleFile UniversalMapping(string fileName, Address startAddress,
// A loaded and managed modules depend on a module file, so get or create one.
// The key is the file name. For jitted code on Linux, this will be a memfd with a static name, which is OK
// because this path will use the StartAddress to ensure that we get the right one.
- // TODO: We'll need to store FileOffset as well to handle elf images.
TraceModuleFile moduleFile = process.Log.ModuleFiles.GetOrCreateModuleFile(fileName, startAddress);
long newImageSize = (long)(endAddress - startAddress);
@@ -7169,16 +7181,31 @@ internal TraceModuleFile UniversalMapping(string fileName, Address startAddress,
Debug.Assert(moduleFile != null);
CheckClassInvarients();
- PEProcessMappingSymbolMetadata symbolMetadata = metadata?.ParsedSymbolMetadata as PEProcessMappingSymbolMetadata;
- if (symbolMetadata != null)
+ PEProcessMappingSymbolMetadata peMetadata = metadata?.ParsedSymbolMetadata as PEProcessMappingSymbolMetadata;
+ if (peMetadata != null)
{
- moduleFile.pdbName = symbolMetadata.PdbName;
- moduleFile.pdbAge = symbolMetadata.PdbAge;
- moduleFile.pdbSignature = symbolMetadata.PdbSignature;
- moduleFile.r2rPerfMapSignature = symbolMetadata.PerfmapSignature;
- moduleFile.r2rPerfMapVersion = symbolMetadata.PerfmapVersion;
- moduleFile.r2rPerfMapName = symbolMetadata.PerfmapName;
- moduleFile.r2rImageTextVirtualOffset = (uint)symbolMetadata.TextOffset;
+ if (moduleFile.MatchOrInitPE() is { } peInfo)
+ {
+ peInfo.PdbName = peMetadata.PdbName;
+ peInfo.PdbAge = peMetadata.PdbAge;
+ peInfo.PdbSignature = peMetadata.PdbSignature;
+ peInfo.R2RPerfMapSignature = peMetadata.PerfmapSignature;
+ peInfo.R2RPerfMapVersion = peMetadata.PerfmapVersion;
+ peInfo.R2RPerfMapName = peMetadata.PerfmapName;
+ peInfo.R2RImageTextVirtualOffset = (uint)peMetadata.TextOffset;
+ }
+ }
+
+ ELFProcessMappingSymbolMetadata elfMetadata = metadata?.ParsedSymbolMetadata as ELFProcessMappingSymbolMetadata;
+ if (elfMetadata != null)
+ {
+ if (moduleFile.MatchOrInitElf() is { } elfInfo)
+ {
+ elfInfo.BuildId = elfMetadata.BuildId;
+ elfInfo.VirtualAddress = elfMetadata.VirtualAddress;
+ elfInfo.FileOffset = elfMetadata.FileOffset;
+ elfInfo.PageSize = process.Log.systemPageSize;
+ }
}
return moduleFile;
@@ -7614,7 +7641,10 @@ internal void InitializeNativeModuleIsReadyToRun()
{
if (NativeModule != null && (flags & ModuleFlags.ReadyToRunModule) != ModuleFlags.None)
{
- NativeModule.ModuleFile.isReadyToRun = true;
+ if (NativeModule.ModuleFile.MatchOrInitPE() is { } pe)
+ {
+ pe.IsReadyToRun = true;
+ }
}
}
@@ -8898,26 +8928,59 @@ private void LookupSymbolsForModule(SymbolReader reader, TraceModuleFile moduleF
reader.m_log.WriteLine("[Loading symbols for " + moduleFile.FilePath + "]");
- // There is where we hook up R2R symbol lookup for Linux. These symbol modules are .r2rmap files.
- // The R2R modules that are used on Linux still have a pointer to the IL PDB, so we need to look for a R2R symbol module
- // before attempting to lookup a PDB, or we may end up with an IL PDB, which won't be helpful for symbol lookup.
- // For Windows traces this call will always return immediately because the module won't have any R2R perfmap information.
+ // Dispatch symbol lookup by binary format.
ISymbolLookup symbolLookup = null;
- R2RPerfMapSymbolModule r2rSymbolModule = OpenR2RPerfMapForModuleFile(reader, moduleFile);
- if (r2rSymbolModule != null)
+ Func computeRva = null;
+ switch (moduleFile.BinaryFormat)
{
- symbolLookup = r2rSymbolModule;
+ case ModuleBinaryFormat.ELF:
+ {
+ ElfSymbolModule elfModule = OpenElfSymbolsForModuleFile(reader, moduleFile);
+ if (elfModule != null)
+ {
+ symbolLookup = elfModule;
+ // ELF RVA = (address - ImageBase) + FileOffset, matching ElfSymbolModule's
+ // (st_value - pVaddr) + pOffset formula.
+ ulong fileOffset = moduleFile.ElfInfo.FileOffset;
+ computeRva = (address) => checked((uint)(address - moduleFile.ImageBase) + (uint)fileOffset);
+ }
+ }
+ break;
+
+ case ModuleBinaryFormat.PE:
+ {
+ // Try R2R perfmap first (Linux managed with precompiled code),
+ // then fall back to PDB.
+ R2RPerfMapSymbolModule r2rSymbolModule = OpenR2RPerfMapForModuleFile(reader, moduleFile);
+ if (r2rSymbolModule != null)
+ {
+ symbolLookup = r2rSymbolModule;
+ }
+ else
+ {
+ NativeSymbolModule moduleReader = OpenPdbForModuleFile(reader, moduleFile) as NativeSymbolModule;
+ if (moduleReader != null)
+ {
+ symbolLookup = moduleReader;
+ }
+ }
+ // PE RVA = address - ImageBase (standard Windows convention).
+ computeRva = (address) => (uint)(address - moduleFile.ImageBase);
+ }
+ break;
+
+ default:
+ {
+ Debug.Assert(false, "LookupSymbolsForModule: unknown binary format " + moduleFile.BinaryFormat);
+ reader.m_log.WriteLine("LookupSymbolsForModule: Unknown binary format {0} for {1}, skipping.", moduleFile.BinaryFormat, moduleFile.FilePath);
+ }
+ break;
}
- else
- {
- NativeSymbolModule moduleReader = OpenPdbForModuleFile(reader, moduleFile) as NativeSymbolModule;
- if (moduleReader == null)
- {
- reader.m_log.WriteLine("Could not find PDB file.");
- return;
- }
- symbolLookup = moduleReader;
+ if (symbolLookup == null)
+ {
+ reader.m_log.WriteLine("Could not find symbols.");
+ return;
}
reader.m_log.WriteLine("Loaded, resolving symbols");
@@ -8948,7 +9011,9 @@ private void LookupSymbolsForModule(SymbolReader reader, TraceModuleFile moduleF
else
{
uint symbolStart = 0;
- var newMethodName = symbolLookup.FindNameForRva((uint)(address - moduleFile.ImageBase), ref symbolStart);
+ uint rva = computeRva(address);
+
+ var newMethodName = symbolLookup.FindNameForRva(rva, ref symbolStart);
if (newMethodName.Length > 0)
{
// TODO FIX NOW
@@ -9118,7 +9183,7 @@ private unsafe ManagedSymbolModule OpenPdbForModuleFile(SymbolReader symReader,
var nativePdb = symbolReaderModule as NativeSymbolModule;
if (nativePdb != null)
{
- nativePdb.LogManagedInfo(managed.PdbName, managed.PdbSignature, managed.pdbAge);
+ nativePdb.LogManagedInfo(managed.PdbName, managed.PdbSignature, managed.PdbAge);
}
}
}
@@ -9132,30 +9197,72 @@ private unsafe ManagedSymbolModule OpenPdbForModuleFile(SymbolReader symReader,
///
private unsafe R2RPerfMapSymbolModule OpenR2RPerfMapForModuleFile(SymbolReader symReader, TraceModuleFile moduleFile)
{
- // If we have a signature, use it
- if (moduleFile.r2rPerfMapSignature != Guid.Empty)
+ Debug.Assert(moduleFile.PEInfo != null, "OpenR2RPerfMapForModuleFile called with null PEInfo");
+ var peInfo = moduleFile.PEInfo;
+ if (peInfo == null || peInfo.R2RPerfMapSignature == Guid.Empty || string.IsNullOrEmpty(peInfo.R2RPerfMapName))
{
- string filePath = symReader.FindR2RPerfMapSymbolFilePath(moduleFile.R2RPerfMapName, moduleFile.R2RPerfMapSignature, moduleFile.R2RPerfMapVersion);
- if (filePath != null)
- {
- R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(filePath, moduleFile.R2RImageTextVirtualOffset);
- if (symbolModule != null && symbolModule.Signature == moduleFile.R2RPerfMapSignature && symbolModule.Version == moduleFile.R2RPerfMapVersion)
- {
- return symbolModule;
- }
- else
- {
- symReader.m_log.WriteLine("ERROR: The R2R perfmap does not match the loaded module. Actual Signature = " + symbolModule.Signature + " Requested Signature = " + moduleFile.R2RPerfMapSignature);
- throw new Exception("ERROR: The R2R perfmap does not match the loaded module.");
- }
- }
+ symReader.m_log.WriteLine("No R2R perfmap signature for {0} in trace.", moduleFile.FilePath);
+ return null;
}
- else
+
+ // Find handles all search: sym server, sym path, and adjacent-to-binary (via dllFilePath).
+ string filePath = symReader.FindR2RPerfMapSymbolFilePath(peInfo.R2RPerfMapName, peInfo.R2RPerfMapSignature, peInfo.R2RPerfMapVersion, moduleFile.FilePath);
+ if (filePath == null)
{
- symReader.m_log.WriteLine("No R2R perfmap signature for {0} in trace.", moduleFile.FilePath);
+ return null;
}
- return null;
+ R2RPerfMapSymbolModule symbolModule = symReader.OpenR2RPerfMapSymbolFile(filePath, peInfo.R2RImageTextVirtualOffset);
+ if (symbolModule == null)
+ {
+ return null;
+ }
+
+ // Post-open validation (belt and suspenders — Find already validated via R2RPerfMapMatches).
+ if (symbolModule.Signature != peInfo.R2RPerfMapSignature || symbolModule.Version != peInfo.R2RPerfMapVersion)
+ {
+ symReader.m_log.WriteLine("ERROR: R2R perfmap {0} does not match. Actual Signature={1} Version={2}, Expected Signature={3} Version={4}",
+ filePath, symbolModule.Signature, symbolModule.Version, peInfo.R2RPerfMapSignature, peInfo.R2RPerfMapVersion);
+ return null;
+ }
+
+ return symbolModule;
+ }
+
+ ///
+ /// Attempts to find and open ELF debug symbols for the given module file.
+ /// Returns an ElfSymbolModule if symbols are found, null otherwise.
+ ///
+ private ElfSymbolModule OpenElfSymbolsForModuleFile(SymbolReader reader, TraceModuleFile moduleFile)
+ {
+ Debug.Assert(moduleFile.ElfInfo != null, "OpenElfSymbolsForModuleFile called with null ElfInfo");
+ var elfInfo = moduleFile.ElfInfo;
+ if (elfInfo == null || string.IsNullOrEmpty(elfInfo.BuildId))
+ {
+ return null;
+ }
+
+ ulong alignedVAddr = elfInfo.PageAlignedVirtualAddress;
+
+ // Find handles all search: sym server, sym path, and adjacent-to-binary (via elfFilePath).
+ string symbolFilePath = reader.FindElfSymbolFilePath(moduleFile.Name, elfInfo.BuildId, moduleFile.FilePath);
+ if (symbolFilePath == null)
+ {
+ reader.m_log.WriteLine("Could not find ELF symbol file for {0} (BuildId: {1})", moduleFile.Name, elfInfo.BuildId);
+ return null;
+ }
+
+ try
+ {
+ reader.m_log.WriteLine("Opening ELF symbols from {0} (pVaddr=0x{1:x}, aligned=0x{2:x}, pOffset=0x{3:x}, pageSize={4})",
+ symbolFilePath, elfInfo.VirtualAddress, alignedVAddr, elfInfo.FileOffset, elfInfo.PageSize);
+ return reader.OpenElfSymbolFile(symbolFilePath, alignedVAddr, elfInfo.FileOffset);
+ }
+ catch (Exception e)
+ {
+ reader.m_log.WriteLine("Error opening ELF symbol file {0}: {1}", symbolFilePath, e.Message);
+ return null;
+ }
}
///
@@ -9429,7 +9536,7 @@ internal void SetModuleFileIndex(TraceModuleFile moduleFile)
moduleFileIndex = moduleFile.ModuleFileIndex;
if (optimizationTier == Parsers.Clr.OptimizationTier.Unknown &&
- moduleFile.IsReadyToRun &&
+ (moduleFile.PEInfo?.IsReadyToRun ?? false) &&
moduleFile.ImageBase <= Address &&
Address < moduleFile.ImageEnd)
{
@@ -10447,35 +10554,76 @@ public string Name
///
/// The name of the symbol file (PDB file) associated with the DLL
///
- public string PdbName { get { return pdbName; } }
+ public string PdbName { get { return PEInfo?.PdbName ?? ""; } }
///
/// Returns the GUID that uniquely identifies the symbol file (PDB file) for this DLL
///
- public Guid PdbSignature { get { return pdbSignature; } }
+ public Guid PdbSignature { get { return PEInfo?.PdbSignature ?? Guid.Empty; } }
///
/// Returns the age (which is a small integer), that is also needed to look up the symbol file (PDB file) on a symbol server.
///
- public int PdbAge { get { return pdbAge; } }
+ public int PdbAge { get { return PEInfo?.PdbAge ?? 0; } }
///
- /// Returns the GUID that uniquely identifies the R2R perfmap file for this DLL
+ /// The binary format of this module file (PE, ELF, or Unknown).
///
- public Guid R2RPerfMapSignature { get { return r2rPerfMapSignature; } }
+ public ModuleBinaryFormat BinaryFormat { get { return symbolInfo?.Format ?? ModuleBinaryFormat.Unknown; } }
///
- /// Returns the version number of the R2R perfmap file format.
+ /// PE-specific symbol info (PDB identity + R2R). Null if this is not a PE module.
///
- public int R2RPerfMapVersion { get { return r2rPerfMapVersion; } }
+ public PESymbolInfo PEInfo { get { return symbolInfo as PESymbolInfo; } }
///
- /// Returns the name of the R2R perfmap file.
+ /// ELF-specific symbol info (BuildId + load header). Null if this is not an ELF module.
///
- public string R2RPerfMapName { get { return r2rPerfMapName; } }
+ public ElfSymbolInfo ElfInfo { get { return symbolInfo as ElfSymbolInfo; } }
///
- /// Returns the offset in bytes between the beginning of the PE image and the beginning of the text section according to the loaded layout.
+ /// Returns PESymbolInfo if this module's symbolInfo is already PE, creates one if null.
+ /// Returns null if symbolInfo is a different type (e.g. ELF) — prevents silent overwrites.
+ /// Use with pattern matching: if (moduleFile.MatchOrInitPE() is { } pe) { pe.Field = value; }
///
- public uint R2RImageTextVirtualOffset { get { return r2rImageTextVirtualOffset; } }
+ internal PESymbolInfo MatchOrInitPE()
+ {
+ if (symbolInfo is PESymbolInfo pe)
+ {
+ return pe;
+ }
+
+ if (symbolInfo != null)
+ {
+ Debug.Assert(false, $"MatchOrInitPE called but symbolInfo is {symbolInfo.GetType().Name}, not PESymbolInfo. This is a bug — module metadata is being set for the wrong binary format.");
+ return null;
+ }
+
+ pe = new PESymbolInfo();
+ symbolInfo = pe;
+ return pe;
+ }
+
+ ///
+ /// Returns ElfSymbolInfo if this module's symbolInfo is already ELF, creates one if null.
+ /// Returns null if symbolInfo is a different type (e.g. PE) — prevents silent overwrites.
+ /// Use with pattern matching: if (moduleFile.MatchOrInitElf() is { } elf) { elf.Field = value; }
+ ///
+ internal ElfSymbolInfo MatchOrInitElf()
+ {
+ if (symbolInfo is ElfSymbolInfo elf)
+ {
+ return elf;
+ }
+
+ if (symbolInfo != null)
+ {
+ Debug.Assert(false, $"MatchOrInitElf called but symbolInfo is {symbolInfo.GetType().Name}, not ElfSymbolInfo. This is a bug — module metadata is being set for the wrong binary format.");
+ return null;
+ }
+
+ elf = new ElfSymbolInfo();
+ symbolInfo = elf;
+ return elf;
+ }
///
/// Returns the file version string that is optionally embedded in the DLL's resources. Returns the empty string if not present.
@@ -10508,7 +10656,7 @@ public string Name
///
/// Tells if the module file is ReadyToRun (the has precompiled code for some managed methods)
///
- public bool IsReadyToRun { get { return isReadyToRun; } }
+ public bool IsReadyToRun { get { return PEInfo?.IsReadyToRun ?? false; } }
///
/// If the Product Version fields has a GIT Commit Hash component, this returns it, Otherwise it is empty.
@@ -10605,7 +10753,6 @@ internal TraceModuleFile(string fileName, Address imageBase, ModuleFileIndex mod
this.moduleFileIndex = moduleFileIndex;
fileVersion = "";
productVersion = "";
- pdbName = "";
}
internal string fileName;
@@ -10613,16 +10760,8 @@ internal TraceModuleFile(string fileName, Address imageBase, ModuleFileIndex mod
internal Address imageBase;
internal string name;
private ModuleFileIndex moduleFileIndex;
- internal bool isReadyToRun;
internal TraceModuleFile next; // Chain of modules that have the same path (But different image bases)
- internal string pdbName;
- internal Guid pdbSignature;
- internal int pdbAge;
- internal Guid r2rPerfMapSignature;
- internal int r2rPerfMapVersion;
- internal string r2rPerfMapName;
- internal uint r2rImageTextVirtualOffset;
internal string fileVersion;
internal string productName;
internal string productVersion;
@@ -10630,6 +10769,7 @@ internal TraceModuleFile(string fileName, Address imageBase, ModuleFileIndex mod
internal int imageChecksum; // used to validate if the local file is the same as the one from the trace.
internal int codeAddressesInModule;
internal TraceModuleFile managedModule;
+ internal TraceModuleFileSymbolInfo symbolInfo;
void IFastSerializable.ToStream(Serializer serializer)
@@ -10638,13 +10778,14 @@ void IFastSerializable.ToStream(Serializer serializer)
serializer.Write(imageSize);
serializer.WriteAddress(imageBase);
- serializer.Write(pdbName);
- serializer.Write(pdbSignature);
- serializer.Write(pdbAge);
- serializer.Write(r2rPerfMapSignature);
- serializer.Write(r2rPerfMapVersion);
- serializer.Write(r2rPerfMapName);
- serializer.Write((int)r2rImageTextVirtualOffset);
+ // Write symbol info with format discriminator
+ byte format = (byte)(symbolInfo?.Format ?? ModuleBinaryFormat.Unknown);
+ serializer.Write(format);
+ if (symbolInfo != null)
+ {
+ symbolInfo.ToStream(serializer);
+ }
+
serializer.Write(fileVersion);
serializer.Write(productVersion);
serializer.Write(timeDateStamp);
@@ -10659,13 +10800,25 @@ void IFastSerializable.FromStream(Deserializer deserializer)
deserializer.Read(out imageSize);
deserializer.ReadAddress(out imageBase);
- deserializer.Read(out pdbName);
- deserializer.Read(out pdbSignature);
- deserializer.Read(out pdbAge);
- deserializer.Read(out r2rPerfMapSignature);
- deserializer.Read(out r2rPerfMapVersion);
- deserializer.Read(out r2rPerfMapName);
- r2rImageTextVirtualOffset = (uint)deserializer.ReadInt();
+ // Read symbol info with format discriminator
+ byte format = deserializer.ReadByte();
+ switch ((ModuleBinaryFormat)format)
+ {
+ case ModuleBinaryFormat.PE:
+ var pe = new PESymbolInfo();
+ pe.FromStream(deserializer);
+ symbolInfo = pe;
+ break;
+ case ModuleBinaryFormat.ELF:
+ var elf = new ElfSymbolInfo();
+ elf.FromStream(deserializer);
+ symbolInfo = elf;
+ break;
+ default:
+ symbolInfo = null;
+ break;
+ }
+
deserializer.Read(out fileVersion);
deserializer.Read(out productVersion);
deserializer.Read(out timeDateStamp);
@@ -10677,6 +10830,143 @@ void IFastSerializable.FromStream(Deserializer deserializer)
#endregion
}
+ ///
+ /// Identifies the binary format of a module file.
+ ///
+ public enum ModuleBinaryFormat : byte
+ {
+ /// The module format is unknown.
+ Unknown = 0,
+ /// Windows Portable Executable format.
+ PE = 1,
+ /// Linux ELF (Executable and Linkable Format).
+ ELF = 2,
+ }
+
+ ///
+ /// Holds symbol identity metadata for a TraceModuleFile, discriminated by binary format.
+ /// Subclasses contain the format-specific fields needed for symbol server lookup and resolution.
+ ///
+ public abstract class TraceModuleFileSymbolInfo
+ {
+ /// The binary format this symbol info represents.
+ public abstract ModuleBinaryFormat Format { get; }
+
+ internal abstract void ToStream(Serializer serializer);
+ internal abstract void FromStream(Deserializer deserializer);
+ }
+
+ ///
+ /// Symbol info for Windows PE modules. Contains PDB identity and optional R2R perfmap info.
+ ///
+ public class PESymbolInfo : TraceModuleFileSymbolInfo
+ {
+ /// Returns ModuleBinaryFormat.PE.
+ public override ModuleBinaryFormat Format => ModuleBinaryFormat.PE;
+
+ /// The name of the PDB file associated with this module.
+ public string PdbName { get; set; } = "";
+ /// GUID that uniquely identifies the PDB file.
+ public Guid PdbSignature { get; set; }
+ /// Age (small integer) needed along with signature for symbol server lookup.
+ public int PdbAge { get; set; }
+ /// Whether this module contains ReadyToRun precompiled code.
+ public bool IsReadyToRun { get; set; }
+ /// GUID identifying the R2R perfmap file.
+ public Guid R2RPerfMapSignature { get; set; }
+ /// Version number of the R2R perfmap format.
+ public int R2RPerfMapVersion { get; set; }
+ /// Name of the R2R perfmap file.
+ public string R2RPerfMapName { get; set; }
+ /// Offset in bytes between PE image beginning and text section beginning.
+ public uint R2RImageTextVirtualOffset { get; set; }
+
+ internal override void ToStream(Serializer serializer)
+ {
+ serializer.Write(PdbName);
+ serializer.Write(PdbSignature);
+ serializer.Write(PdbAge);
+ serializer.Write(IsReadyToRun);
+ serializer.Write(R2RPerfMapSignature);
+ serializer.Write(R2RPerfMapVersion);
+ serializer.Write(R2RPerfMapName);
+ serializer.Write((int)R2RImageTextVirtualOffset);
+ }
+
+ internal override void FromStream(Deserializer deserializer)
+ {
+ deserializer.Read(out string pdbName);
+ PdbName = pdbName;
+ deserializer.Read(out Guid pdbSignature);
+ PdbSignature = pdbSignature;
+ deserializer.Read(out int pdbAge);
+ PdbAge = pdbAge;
+ IsReadyToRun = deserializer.ReadBool();
+ deserializer.Read(out Guid r2rPerfMapSignature);
+ R2RPerfMapSignature = r2rPerfMapSignature;
+ deserializer.Read(out int r2rPerfMapVersion);
+ R2RPerfMapVersion = r2rPerfMapVersion;
+ deserializer.Read(out string r2rPerfMapName);
+ R2RPerfMapName = r2rPerfMapName;
+ R2RImageTextVirtualOffset = (uint)deserializer.ReadInt();
+ }
+ }
+
+ ///
+ /// Symbol info for Linux ELF modules. Contains BuildId and load header info for symbol resolution.
+ ///
+ public class ElfSymbolInfo : TraceModuleFileSymbolInfo
+ {
+ /// Returns ModuleBinaryFormat.ELF.
+ public override ModuleBinaryFormat Format => ModuleBinaryFormat.ELF;
+
+ /// The GNU build-id of the ELF file (lowercase hex string, typically 40 chars).
+ public string BuildId { get; set; }
+ /// Virtual address of the first executable PT_LOAD segment (p_vaddr).
+ public ulong VirtualAddress { get; set; }
+ /// File offset of the first executable PT_LOAD segment (p_offset).
+ public ulong FileOffset { get; set; }
+ /// System page size from the trace header (e.g. 4096 for x86_64). 0 if not available.
+ public ulong PageSize { get; set; }
+
+ ///
+ /// Returns the page-aligned virtual address of the executable PT_LOAD segment.
+ /// This is the base address used for computing symbol RVAs, and matches the
+ /// "address - ImageBase" coordinate system: the Linux loader maps the executable
+ /// segment at PAGE_DOWN(p_vaddr) relative to the module load base.
+ /// Falls back to raw VirtualAddress if PageSize is not set.
+ ///
+ public ulong PageAlignedVirtualAddress
+ {
+ get
+ {
+ if (PageSize > 0)
+ {
+ return VirtualAddress & ~(PageSize - 1);
+ }
+
+ return VirtualAddress;
+ }
+ }
+
+ internal override void ToStream(Serializer serializer)
+ {
+ serializer.Write(BuildId);
+ serializer.Write((long)VirtualAddress);
+ serializer.Write((long)FileOffset);
+ serializer.Write((long)PageSize);
+ }
+
+ internal override void FromStream(Deserializer deserializer)
+ {
+ deserializer.Read(out string buildId);
+ BuildId = buildId;
+ VirtualAddress = (ulong)deserializer.ReadInt64();
+ FileOffset = (ulong)deserializer.ReadInt64();
+ PageSize = (ulong)deserializer.ReadInt64();
+ }
+ }
+
///
/// A ActivityIndex uniquely identifies an Activity in the log. Valid values are between
/// 0 and Activities.Count-1.