diff --git a/.gitignore b/.gitignore index e9cc209..b58101e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ CodeCoverage/ TestResult.xml nunit-*.xml *.vsidx +/Brovan/.vs diff --git a/Brovan/Core/BinaryFile.cs b/Brovan/Core/BinaryFile.cs index a978c57..513486f 100644 --- a/Brovan/Core/BinaryFile.cs +++ b/Brovan/Core/BinaryFile.cs @@ -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 PeSectionCache = new SectionLookupCache(GetPeSectionVa, GetPeSectionSize); + private readonly SectionLookupCache ElfSectionCache = new SectionLookupCache(GetElfSectionVa, GetElfSectionSize); private Dictionary RvaToFileOffsetCache; - private bool HasLastPESec; - private PortableBinarySection LastPESec; - /// /// Executable Formats Magic Number. /// @@ -139,7 +135,7 @@ namespace Brovan.Core /// public T ReadStruct(ReadOnlySpan 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(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 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 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(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 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 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; } - /// - /// Compare two PE sections by their virtual address for sorting. - /// - /// First section. - /// Second section. - /// - /// A negative value if should come before , a positive value if it should come after, - /// or zero if they have the same virtual address. - /// - 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 where TSection : struct + { + private readonly Func GetVa; + private readonly Func GetSize; + + private TSection[]? SortedByVa; + private uint[]? VaStart; + private uint[]? VaEnd; + private bool HasLast; + private TSection Last; + + public SectionLookupCache(Func GetVa, Func 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; + } } /// @@ -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(4096); - HasLastPESec = false; + } + + /// + /// Builds internal lookup structures for ELF sections, enabling fast resolution of VAs to sections. + /// + private void BuildELFSectionLookupCache() + { + if (ELF.Sections == null || ELF.Sections.Length == 0) + return; + + ElfSectionCache.Build(ELF.Sections); } /// @@ -929,38 +1025,7 @@ namespace Brovan.Core /// returns true if the RVA maps to a section, otherwise false. 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); } /// @@ -971,29 +1036,15 @@ namespace Brovan.Core /// returns true if the RVA maps to a section, otherwise false. 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; + /// + /// Try to locate the ELF section that contains the specified virtual address using a small cache for sequential access. + /// + private bool TryFindELFSectionByVaFast(ulong VirtualAddress, out ElfBinarySection Section) + { + return ElfSectionCache.TryFindByVa(VirtualAddress, out Section); } /// @@ -2133,11 +2184,7 @@ namespace Brovan.Core ulong EntryPoint = Architecture == BinaryArchitecture.x64 ? ReadStruct(DataSpan, 0).e_entry : ReadStruct(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 DynStr = DataSpan.Slice(RawOffset, BytesToCopy); // Create a map of symbol index to name - Dictionary 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 SymbolNames = new Dictionary(SymbolCount); for (int i = 0; i < SymbolCount; i++) { @@ -2286,10 +2325,11 @@ namespace Brovan.Core ELF64_SYMBOL Symbol = ReadStruct(DataSpan, (int)DynSymSection.RawOffset + i * SymbolSize); if (Symbol.st_name != 0) { - int EndOff = Array.IndexOf(DynStr, (byte)0, (int)Symbol.st_name); + ReadOnlySpan 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(DataSpan, (int)DynSymSection.RawOffset + i * SymbolSize); if (Symbol.st_name != 0) { - int EndOff = Array.IndexOf(DynStr, (byte)0, (int)Symbol.st_name); + ReadOnlySpan 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; }