Refactor section lookup and span usage

Optimize ELF Parsing by reducing some allocations, caching, etc. and cleaned up and improved the code a bit.
This commit is contained in:
AdvDebug
2026-05-21 18:51:44 +03:00
parent 0cabb3290b
commit 3843ba27dc
2 changed files with 173 additions and 132 deletions
+1
View File
@@ -56,3 +56,4 @@ CodeCoverage/
TestResult.xml
nunit-*.xml
*.vsidx
/Brovan/.vs
+172 -132
View File
@@ -77,15 +77,11 @@ namespace Brovan.Core
public bool IsDisposed { get; private set; }
private bool Quick = false;
private PortableBinarySection[] SortedPESecsByVa;
private uint[] SortedPESecsVaStart;
private uint[] SortedPESecsVaEnd;
private readonly SectionLookupCache<PortableBinarySection> PeSectionCache = new SectionLookupCache<PortableBinarySection>(GetPeSectionVa, GetPeSectionSize);
private readonly SectionLookupCache<ElfBinarySection> ElfSectionCache = new SectionLookupCache<ElfBinarySection>(GetElfSectionVa, GetElfSectionSize);
private Dictionary<uint, uint> RvaToFileOffsetCache;
private bool HasLastPESec;
private PortableBinarySection LastPESec;
/// <summary>
/// Executable Formats Magic Number.
/// </summary>
@@ -139,7 +135,7 @@ namespace Brovan.Core
/// <exception cref="ArgumentOutOfRangeException"></exception>
public T ReadStruct<T>(ReadOnlySpan<byte> Data, int Offset) where T : unmanaged
{
if (Data == null || Data.Length == 0)
if (Data.IsEmpty || Data.Length == 0)
throw new ArgumentNullException(nameof(Data), $"The parameter \"{nameof(Data)}\" byte array cannot be null or empty when reading a struct.");
if (Offset < 0)
@@ -450,12 +446,11 @@ namespace Brovan.Core
ELF64_SECTION_HEADER StringTableHeader = ReadStruct<ELF64_SECTION_HEADER>(Data, (int)(ElfHeader.e_shoff + ((ulong)ElfHeader.e_shstrndx * (ulong)ElfHeader.e_shentsize)));
if (StringTableHeader.sh_size > (ulong)Array.MaxLength || StringTableHeader.sh_size < 0)
throw new ArgumentOutOfRangeException(nameof(StringTableHeader.sh_size), "sh_size is out-of-range for a byte array.");
byte[] StringTable = new byte[StringTableHeader.sh_size];
int ShOffset = (int)StringTableHeader.sh_offset;
long EndPos = ShOffset + (long)StringTableHeader.sh_size;
if ((ShOffset > Data.Length) || (ShOffset < 0 || EndPos > Data.Length))
throw new IndexOutOfRangeException("sh_offset or size is larger than the binary data length.");
Data.Slice(ShOffset, (int)StringTableHeader.sh_size).CopyTo(StringTable);
ReadOnlySpan<byte> StringTable = Data.Slice(ShOffset, (int)StringTableHeader.sh_size);
int SectionHeaderSize = ElfHeader.e_shentsize;
long SectionHeadersBase = (long)ElfHeader.e_shoff;
for (int i = 0; i < SectionCount; i++)
@@ -470,11 +465,12 @@ namespace Brovan.Core
if (NameOffset < 0 || NameOffset >= StringTable.Length)
throw new IndexOutOfRangeException("Section name offset is outside the string-table size.");
int EndOffset = NameOffset;
while (EndOffset < StringTable.Length && StringTable[EndOffset] != 0)
EndOffset++;
ReadOnlySpan<byte> NameSlice = StringTable.Slice(NameOffset);
int EndOffset = NameSlice.IndexOf((byte)0);
if (EndOffset < 0)
EndOffset = NameSlice.Length;
SectionName = Encoding.ASCII.GetString(StringTable, NameOffset, EndOffset - NameOffset);
SectionName = Encoding.ASCII.GetString(NameSlice.Slice(0, EndOffset));
}
ELF.Sections[i] = new ElfBinarySection
@@ -529,12 +525,11 @@ namespace Brovan.Core
ELF32_SECTION_HEADER StringTableHeader = ReadStruct<ELF32_SECTION_HEADER>(Data, (int)(ElfHeader.e_shoff + (ElfHeader.e_shstrndx * ElfHeader.e_shentsize)));
if (StringTableHeader.sh_size > Array.MaxLength || StringTableHeader.sh_size < 0)
throw new OverflowException("sh_size is out-of-range for a byte array.");
byte[] StringTable = new byte[StringTableHeader.sh_size];
int ShOffset = (int)StringTableHeader.sh_offset;
long EndPos = ShOffset + StringTableHeader.sh_size;
if ((ShOffset > Data.Length) || (ShOffset < 0 || EndPos > Data.Length))
throw new IndexOutOfRangeException("sh_offset or size is larger than the binary data length.");
Data.Slice(ShOffset, (int)StringTableHeader.sh_size).CopyTo(StringTable);
ReadOnlySpan<byte> StringTable = Data.Slice(ShOffset, (int)StringTableHeader.sh_size);
int SectionHeaderSize = ElfHeader.e_shentsize;
int SectionHeadersBase = (int)ElfHeader.e_shoff;
@@ -551,11 +546,12 @@ namespace Brovan.Core
if (NameOffset < 0 || NameOffset >= StringTable.Length)
throw new IndexOutOfRangeException("Section name offset is outside the string-table size.");
int EndOffset = NameOffset;
while (EndOffset < StringTable.Length && StringTable[EndOffset] != 0)
EndOffset++;
ReadOnlySpan<byte> NameSlice = StringTable.Slice(NameOffset);
int EndOffset = NameSlice.IndexOf((byte)0);
if (EndOffset < 0)
EndOffset = NameSlice.Length;
SectionName = Encoding.ASCII.GetString(StringTable, NameOffset, EndOffset - NameOffset);
SectionName = Encoding.ASCII.GetString(NameSlice.Slice(0, EndOffset));
}
ELF.Sections[i] = new ElfBinarySection
@@ -570,6 +566,8 @@ namespace Brovan.Core
}
}
BuildELFSectionLookupCache();
// Parse functions and imports
ParseELFFunctions();
ParseELFImports(Is64Bit);
@@ -866,24 +864,133 @@ namespace Brovan.Core
return false;
}
/// <summary>
/// Compare two PE sections by their virtual address for sorting.
/// </summary>
/// <param name="A">First section.</param>
/// <param name="B">Second section.</param>
/// <returns>
/// A negative value if <paramref name="A"/> should come before <paramref name="B"/>, a positive value if it should come after,
/// or zero if they have the same virtual address.
/// </returns>
private static int CompareSectionByVirtualAddress(PortableBinarySection A, PortableBinarySection B)
private static uint GetPeSectionVa(PortableBinarySection Section) => Section.VirtualAddress;
private static uint GetPeSectionSize(PortableBinarySection Section)
{
if (A.VirtualAddress < B.VirtualAddress)
return -1;
uint Size = Section.VirtualSize;
return Size == 0 ? Section.RawSize : Size;
}
if (A.VirtualAddress > B.VirtualAddress)
return 1;
private static uint GetElfSectionVa(ElfBinarySection Section) => Section.VirtualAddress;
return 0;
private static uint GetElfSectionSize(ElfBinarySection Section) => Section.VirtualSize;
private sealed class SectionLookupCache<TSection> where TSection : struct
{
private readonly Func<TSection, uint> GetVa;
private readonly Func<TSection, uint> GetSize;
private TSection[]? SortedByVa;
private uint[]? VaStart;
private uint[]? VaEnd;
private bool HasLast;
private TSection Last;
public SectionLookupCache(Func<TSection, uint> GetVa, Func<TSection, uint> GetSize)
{
this.GetVa = GetVa ?? throw new ArgumentNullException(nameof(GetVa));
this.GetSize = GetSize ?? throw new ArgumentNullException(nameof(GetSize));
}
public void Build(TSection[] Sections)
{
if (Sections == null || Sections.Length == 0)
{
SortedByVa = null;
VaStart = null;
VaEnd = null;
HasLast = false;
return;
}
SortedByVa = (TSection[])Sections.Clone();
Array.Sort(SortedByVa, (Left, Right) =>
{
uint LeftVa = GetVa(Left);
uint RightVa = GetVa(Right);
if (LeftVa < RightVa)
return -1;
if (LeftVa > RightVa)
return 1;
return 0;
});
int Count = SortedByVa.Length;
VaStart = new uint[Count];
VaEnd = new uint[Count];
for (int i = 0; i < Count; i++)
{
TSection Section = SortedByVa[i];
uint Va = GetVa(Section);
uint Size = GetSize(Section);
VaStart[i] = Va;
VaEnd[i] = Va + Size;
}
HasLast = false;
}
public bool TryFindByVa(ulong VirtualAddress, out TSection Section)
{
Section = default;
if (SortedByVa == null || VaStart == null || VaEnd == null)
return false;
if (VirtualAddress > uint.MaxValue)
return false;
uint Va = (uint)VirtualAddress;
if (HasLast)
{
uint Start = GetVa(Last);
uint End = Start + GetSize(Last);
if (Va >= Start && Va < End)
{
Section = Last;
return true;
}
}
int Lo = 0;
int Hi = SortedByVa.Length - 1;
while (Lo <= Hi)
{
int Mid = Lo + ((Hi - Lo) >> 1);
uint Start = VaStart[Mid];
uint End = VaEnd[Mid];
if (Va < Start)
{
Hi = Mid - 1;
continue;
}
if (Va >= End)
{
Lo = Mid + 1;
continue;
}
Section = SortedByVa[Mid];
HasLast = true;
Last = Section;
return true;
}
return false;
}
}
/// <summary>
@@ -895,30 +1002,19 @@ namespace Brovan.Core
if (PE.Sections == null || PE.Sections.Length == 0)
return;
SortedPESecsByVa = (PortableBinarySection[])PE.Sections.Clone();
Array.Sort(SortedPESecsByVa, CompareSectionByVirtualAddress);
int Count = SortedPESecsByVa.Length;
SortedPESecsVaStart = new uint[Count];
SortedPESecsVaEnd = new uint[Count];
for (int i = 0; i < Count; i++)
{
PortableBinarySection Sec = SortedPESecsByVa[i];
uint Va = Sec.VirtualAddress;
uint Size = Sec.VirtualSize;
if (Size == 0)
Size = Sec.RawSize;
SortedPESecsVaStart[i] = Va;
SortedPESecsVaEnd[i] = Va + Size;
}
PeSectionCache.Build(PE.Sections);
RvaToFileOffsetCache = new Dictionary<uint, uint>(4096);
HasLastPESec = false;
}
/// <summary>
/// Builds internal lookup structures for ELF sections, enabling fast resolution of VAs to sections.
/// </summary>
private void BuildELFSectionLookupCache()
{
if (ELF.Sections == null || ELF.Sections.Length == 0)
return;
ElfSectionCache.Build(ELF.Sections);
}
/// <summary>
@@ -929,38 +1025,7 @@ namespace Brovan.Core
/// <returns>returns true if the RVA maps to a section, otherwise false.</returns>
private bool TryFindPESectionByRva(uint Rva, out PortableBinarySection Section)
{
Section = default;
if (SortedPESecsByVa == null)
return false;
int Lo = 0;
int Hi = SortedPESecsByVa.Length - 1;
while (Lo <= Hi)
{
int Mid = Lo + ((Hi - Lo) >> 1);
uint Start = SortedPESecsVaStart[Mid];
uint End = SortedPESecsVaEnd[Mid];
if (Rva < Start)
{
Hi = Mid - 1;
continue;
}
if (Rva >= End)
{
Lo = Mid + 1;
continue;
}
Section = SortedPESecsByVa[Mid];
return true;
}
return false;
return PeSectionCache.TryFindByVa(Rva, out Section);
}
/// <summary>
@@ -971,29 +1036,15 @@ namespace Brovan.Core
/// <returns>returns true if the RVA maps to a section, otherwise false.</returns>
private bool TryFindPESectionByRvaFast(uint Rva, out PortableBinarySection Section)
{
if (HasLastPESec)
{
uint Start = LastPESec.VirtualAddress;
uint Size = LastPESec.VirtualSize;
return PeSectionCache.TryFindByVa(Rva, out Section);
}
if (Size == 0)
Size = LastPESec.RawSize;
uint End = Start + Size;
if (Rva >= Start && Rva < End)
{
Section = LastPESec;
return true;
}
}
if (!TryFindPESectionByRva(Rva, out Section))
return false;
HasLastPESec = true;
LastPESec = Section;
return true;
/// <summary>
/// Try to locate the ELF section that contains the specified virtual address using a small cache for sequential access.
/// </summary>
private bool TryFindELFSectionByVaFast(ulong VirtualAddress, out ElfBinarySection Section)
{
return ElfSectionCache.TryFindByVa(VirtualAddress, out Section);
}
/// <summary>
@@ -2133,11 +2184,7 @@ namespace Brovan.Core
ulong EntryPoint = Architecture == BinaryArchitecture.x64 ? ReadStruct<ELF64_HEADER>(DataSpan, 0).e_entry : ReadStruct<ELF32_HEADER>(DataSpan, 0).e_entry;
ElfBinarySection EntrySection = ELF.Sections.FirstOrDefault(s =>
EntryPoint >= s.VirtualAddress &&
EntryPoint < s.VirtualAddress + s.VirtualSize);
if (EntrySection.SectionName != null)
if (TryFindELFSectionByVaFast(EntryPoint, out ElfBinarySection EntrySection) && EntrySection.SectionName != null)
{
uint EntryOffset = EntrySection.RawOffset + (uint)(EntryPoint - EntrySection.VirtualAddress);
uint EndOffset = FindFunctionEnd(EntryOffset);
@@ -2192,11 +2239,7 @@ namespace Brovan.Core
if (SymbolInfo != 0x12 || SymbolName == 0)
continue;
ElfBinarySection SymbolSection = ELF.Sections.FirstOrDefault(s =>
SymbolValue >= s.VirtualAddress &&
SymbolValue < s.VirtualAddress + s.VirtualSize);
if (SymbolSection.SectionName == null)
if (!TryFindELFSectionByVaFast(SymbolValue, out ElfBinarySection SymbolSection) || SymbolSection.SectionName == null)
continue;
uint FileOffset = SymbolSection.RawOffset + (uint)(SymbolValue - SymbolSection.VirtualAddress);
@@ -2253,7 +2296,6 @@ namespace Brovan.Core
return;
// Read dynamic string table
byte[] DynStr = new byte[DynStrSection.RawSize];
if (DynStrSection.RawOffset > Data.Length || DynStrSection.RawSize < 0)
throw new IndexOutOfRangeException("Invalid .dynstr data.");
@@ -2269,15 +2311,12 @@ namespace Brovan.Core
if (BytesToCopy > MaxReadable)
BytesToCopy = MaxReadable;
if (BytesToCopy > DynStr.Length)
throw new ArgumentException("Destination buffer is too small.", nameof(DynStr));
DataSpan.Slice(RawOffset, BytesToCopy).CopyTo(DynStr);
ReadOnlySpan<byte> DynStr = DataSpan.Slice(RawOffset, BytesToCopy);
// Create a map of symbol index to name
Dictionary<uint, string> SymbolNames = new();
int SymbolSize = Is64Bit ? 24 : 16; // if 64-bit arch, set to 24 (Elf64_Sym) and if not then use 16 (Elf32_Sym)
int SymbolCount = (int)(DynSymSection.RawSize / SymbolSize);
Dictionary<uint, string> SymbolNames = new Dictionary<uint, string>(SymbolCount);
for (int i = 0; i < SymbolCount; i++)
{
@@ -2286,10 +2325,11 @@ namespace Brovan.Core
ELF64_SYMBOL Symbol = ReadStruct<ELF64_SYMBOL>(DataSpan, (int)DynSymSection.RawOffset + i * SymbolSize);
if (Symbol.st_name != 0)
{
int EndOff = Array.IndexOf(DynStr, (byte)0, (int)Symbol.st_name);
ReadOnlySpan<byte> NameSlice = DynStr.Slice((int)Symbol.st_name);
int EndOff = NameSlice.IndexOf((byte)0);
if (EndOff != -1)
{
string Name = Encoding.ASCII.GetString(DynStr, (int)Symbol.st_name, EndOff - (int)Symbol.st_name);
string Name = Encoding.ASCII.GetString(NameSlice.Slice(0, EndOff));
SymbolNames[(uint)i] = Name;
}
}
@@ -2299,10 +2339,11 @@ namespace Brovan.Core
ELF32_SYMBOL Symbol = ReadStruct<ELF32_SYMBOL>(DataSpan, (int)DynSymSection.RawOffset + i * SymbolSize);
if (Symbol.st_name != 0)
{
int EndOff = Array.IndexOf(DynStr, (byte)0, (int)Symbol.st_name);
ReadOnlySpan<byte> NameSlice = DynStr.Slice((int)Symbol.st_name);
int EndOff = NameSlice.IndexOf((byte)0);
if (EndOff != -1)
{
string Name = Encoding.ASCII.GetString(DynStr, (int)Symbol.st_name, EndOff - (int)Symbol.st_name);
string Name = Encoding.ASCII.GetString(NameSlice.Slice(0, EndOff));
SymbolNames[(uint)i] = Name;
}
}
@@ -2672,7 +2713,6 @@ namespace Brovan.Core
this.Quick = Quick;
ParseBinary(Data.AsSpan());
this.Location = Path.GetFullPath(BinaryPath);
this.SortedPESecsByVa = null;
this.RvaToFileOffsetCache?.Clear();
this.RvaToFileOffsetCache = null;
}