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.