diff --git a/src/AsmResolver/AsmResolver.csproj b/src/AsmResolver/AsmResolver.csproj index 0b2a3f83e..ddaa59dfb 100644 --- a/src/AsmResolver/AsmResolver.csproj +++ b/src/AsmResolver/AsmResolver.csproj @@ -28,7 +28,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/AsmResolver/IO/ByteArrayInputFile.cs b/src/AsmResolver/IO/ByteArrayInputFile.cs index e76d080b8..d6e463782 100644 --- a/src/AsmResolver/IO/ByteArrayInputFile.cs +++ b/src/AsmResolver/IO/ByteArrayInputFile.cs @@ -49,6 +49,9 @@ public ByteArrayInputFile(string? filePath, byte[] data, ulong baseAddress) /// public uint Length => (uint) _dataSource.Length; + /// + public ulong BaseAddress => 0; + /// public BinaryStreamReader CreateReader(ulong address, uint rva, uint length) => new(_dataSource, address, rva, length); diff --git a/src/AsmResolver/IO/IInputFile.cs b/src/AsmResolver/IO/IInputFile.cs index 6c5972fd7..2d0063ee8 100644 --- a/src/AsmResolver/IO/IInputFile.cs +++ b/src/AsmResolver/IO/IInputFile.cs @@ -27,6 +27,14 @@ uint Length get; } + /// + /// Gets the base address of the data in this input. + /// + ulong BaseAddress + { + get; + } + /// /// Creates a new binary reader at the provided address. /// @@ -47,6 +55,6 @@ public static partial class IOExtensions /// The factory to use. /// The constructed reader. public static BinaryStreamReader CreateReader(this IInputFile factory) - => factory.CreateReader(0, 0, factory.Length); + => factory.CreateReader(factory.BaseAddress, 0, factory.Length); } } diff --git a/src/AsmResolver/IO/MemoryMappedDataSource.cs b/src/AsmResolver/IO/MemoryMappedDataSource.cs deleted file mode 100644 index 71d09193a..000000000 --- a/src/AsmResolver/IO/MemoryMappedDataSource.cs +++ /dev/null @@ -1,85 +0,0 @@ -#if !NET35 - -using System; -using System.IO.MemoryMappedFiles; - -namespace AsmResolver.IO -{ - /// - /// Represents a data source that obtains its data from a memory mapped file. - /// - public sealed class MemoryMappedDataSource : IDataSource, IDisposable -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - , ISpanDataSource -#endif - { - private readonly MemoryMappedViewAccessor _accessor; - - /// - /// Creates a new instance of the class. - /// - /// The memory accessor to use. - /// The length of the data. - public MemoryMappedDataSource(MemoryMappedViewAccessor accessor, ulong length) - { - Length = length; - _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); - } - - /// - public ulong BaseAddress => 0; - - /// - public byte this[ulong address] => _accessor.ReadByte((long) address); - - /// - public ulong Length - { - get; - } - - /// - public bool IsValidAddress(ulong address) => address < Length; - - /// - public int ReadBytes(ulong address, byte[] buffer, int index, int count) => - _accessor.ReadArray((long) address, buffer, index, count); - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - /// - public unsafe int ReadBytes(ulong address, Span buffer) - { - if (!IsValidAddress(address)) - return 0; - - var handle = _accessor.SafeMemoryMappedViewHandle; - int actualLength = (int) Math.Min(Length - address, (uint) buffer.Length); - -#if NET6_0_OR_GREATER - handle.ReadSpan(address, buffer[..actualLength]); -#else - byte* pointer = null; - - try - { - handle.AcquirePointer(ref pointer); - new ReadOnlySpan(pointer, actualLength).CopyTo(buffer); - } - finally - { - if (pointer != null) - { - handle.ReleasePointer(); - } - } -#endif - return actualLength; - } -#endif - - /// - public void Dispose() => _accessor?.Dispose(); - } -} - -#endif diff --git a/src/AsmResolver/IO/MemoryMappedFileService.cs b/src/AsmResolver/IO/MemoryMappedFileService.cs index 6f96181cf..ab372ebe9 100644 --- a/src/AsmResolver/IO/MemoryMappedFileService.cs +++ b/src/AsmResolver/IO/MemoryMappedFileService.cs @@ -1,5 +1,3 @@ -#if !NET35 - using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -39,5 +37,3 @@ public void Dispose() } } } - -#endif diff --git a/src/AsmResolver/IO/MemoryMappedInputFile.cs b/src/AsmResolver/IO/MemoryMappedInputFile.cs index 7092dae0d..56fd2443d 100644 --- a/src/AsmResolver/IO/MemoryMappedInputFile.cs +++ b/src/AsmResolver/IO/MemoryMappedInputFile.cs @@ -1,8 +1,6 @@ -#if !NET35 - using System; using System.IO; -using System.IO.MemoryMappedFiles; +using AsmResolver.Shims; namespace AsmResolver.IO { @@ -11,19 +9,18 @@ namespace AsmResolver.IO /// public sealed class MemoryMappedInputFile : IInputFile { - private readonly MemoryMappedFile _file; - private readonly MemoryMappedDataSource _dataSource; + private readonly MemoryMappedFileShim _file; + private readonly UnmanagedDataSource _dataSource; /// /// Creates a new reader factory for the provided file. /// /// The path to the file to read. - public MemoryMappedInputFile(string filePath) + public unsafe MemoryMappedInputFile(string filePath) { FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); - _file = MemoryMappedFile.CreateFromFile(filePath); - long fileSize = new FileInfo(filePath).Length; - _dataSource = new MemoryMappedDataSource(_file.CreateViewAccessor(0, fileSize), (ulong) fileSize); + _file = new MemoryMappedFileShim(filePath); + _dataSource = new UnmanagedDataSource(_file.BasePointer, (ulong)_file.Size); } /// @@ -35,6 +32,9 @@ public string FilePath /// public uint Length => (uint) _dataSource.Length; + /// + public unsafe ulong BaseAddress => (ulong)_file.BasePointer; + /// public BinaryStreamReader CreateReader(ulong address, uint rva, uint length) => new(_dataSource, address, rva, length); @@ -43,9 +43,6 @@ public string FilePath public void Dispose() { _file.Dispose(); - _dataSource.Dispose(); } } } - -#endif diff --git a/src/AsmResolver/Shims/MemoryMappedFileShim.Unix.cs b/src/AsmResolver/Shims/MemoryMappedFileShim.Unix.cs new file mode 100644 index 000000000..8c7ed4a0e --- /dev/null +++ b/src/AsmResolver/Shims/MemoryMappedFileShim.Unix.cs @@ -0,0 +1,52 @@ +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace AsmResolver.Shims; + +internal sealed unsafe partial class MemoryMappedFileShim +{ + private const int SEEK_END = 2; + private const int PROT_READ = 1; + private const int MAP_PRIVATE = 2; + + [DllImport("libc", ExactSpelling = true, CharSet = CharSet.Ansi, SetLastError = true)] + private static extern int open([In] string path, uint flags); + + [DllImport("libc", ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.U1)] + private static extern bool close(int fd); + + [DllImport("libc", ExactSpelling = true, SetLastError = true)] + private static extern nint mmap(void* addr, nuint length, int prot, int flags, int fd, long offset); + + [DllImport("libc", ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.U1)] + private static extern bool munmap(void* adrr, nuint length); + + [DllImport("libc", ExactSpelling = true, SetLastError = true)] + private static extern long lseek(int fd, long offset, int whence); + + private void AllocateFileUnix(string path) + { + int fd = open(path, 0); + if (fd == -1) + throw new Win32Exception(); + + _size = lseek(fd, 0, SEEK_END); + if (_size == -1) + throw new Win32Exception(); + + nint mapping = mmap(null, (nuint)_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (mapping == -1) + throw new Win32Exception(); + _file = (byte*)mapping; + + // mmap explicitly documents that it is fine to close the fd after mapping + close(fd); + } + + private void DisposeCoreUnix(void* filePointer) + { + munmap(filePointer, (nuint)_size); + } +} diff --git a/src/AsmResolver/Shims/MemoryMappedFileShim.Windows.cs b/src/AsmResolver/Shims/MemoryMappedFileShim.Windows.cs new file mode 100644 index 000000000..bc13cb567 --- /dev/null +++ b/src/AsmResolver/Shims/MemoryMappedFileShim.Windows.cs @@ -0,0 +1,74 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Threading; + +namespace AsmResolver.Shims; + +internal sealed unsafe partial class MemoryMappedFileShim +{ + private const uint GENERIC_READ = 0x80000000; + private const uint FILE_SHARE_READ = 0x2; + private const uint OPEN_EXISTING = 3; + private const uint PAGE_READONLY = 2; + // not on msdn, taken from https://github.com/terrafx/terrafx.interop.windows/blob/e681ccb7239bf9f5629083cd1e396b2876c24aae/sources/Interop/Windows/Windows/um/memoryapi/FILE.cs#L14 + private const uint FILE_MAP_READ = 4; + + private void* _fileHandle; + private void* _mappingHandle; + + [DllImport("kernel32", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] + private static extern nint CreateFileW(char* name, uint access, uint shareMode, void* securityAttributes, uint create, uint attributes); + + [DllImport("kernel32", ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetFileSizeEx(void* handle, out long size); + + [DllImport("kernel32", ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(void* handle); + + [DllImport("kernel32", ExactSpelling = true, CharSet = CharSet.Ansi, SetLastError = true)] + private static extern nint CreateFileMappingA(void* handle, void* mapAttributes, uint protection, uint maxSizeHigh, + uint maxSizeLow, sbyte* name); + + [DllImport("kernel32", ExactSpelling = true, SetLastError = true)] + private static extern void* MapViewOfFile(void* handle, uint access, uint offsetHigh, uint offsetLow, nuint size); + + [DllImport("kernel32", ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool UnmapViewOfFile(void* p); + + private void AllocateFileWindows(string path) + { + nint handle; + fixed (char* pathPointer = path) + { + handle = CreateFileW(pathPointer, GENERIC_READ, FILE_SHARE_READ, null, OPEN_EXISTING, 0); + } + if (handle == -1) + throw new Win32Exception(); + + _fileHandle = (void*)handle; + if (!GetFileSizeEx(_fileHandle, out _size)) + throw new Win32Exception(); + + handle = CreateFileMappingA(_fileHandle, null, PAGE_READONLY, 0, 0, null); + if (handle == -1) + throw new Win32Exception(); + _mappingHandle = (void*)handle; + + _file = (byte*)MapViewOfFile(_mappingHandle, FILE_MAP_READ, 0, 0, 0); + if (_file == null) + throw new Win32Exception(); + } + + private void DisposeCoreWindows(void* filePointer) + { + if (!UnmapViewOfFile(filePointer)) + return; + if (!CloseHandle(_mappingHandle)) + return; + CloseHandle(_fileHandle); + } +} diff --git a/src/AsmResolver/Shims/MemoryMappedFileShim.cs b/src/AsmResolver/Shims/MemoryMappedFileShim.cs new file mode 100644 index 000000000..954cb5ee2 --- /dev/null +++ b/src/AsmResolver/Shims/MemoryMappedFileShim.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading; + +namespace AsmResolver.Shims; + +internal sealed unsafe partial class MemoryMappedFileShim : IDisposable +{ + private byte* _file; + private long _size; + + public MemoryMappedFileShim(string path) + { + if (RuntimeInformationShim.IsRunningOnWindows) + AllocateFileWindows(path); + else + AllocateFileUnix(path); + } + + public long Size => _size; + public byte* BasePointer => _file; + + public byte ReadByte(long address) + { + if (_file == null) + throw new ObjectDisposedException("disposed"); + if ((ulong)address >= (ulong)_size) + throw new ArgumentOutOfRangeException(nameof(address)); + return _file[address]; + } + + private void DisposeCore() + { + void* filePointer; + fixed (byte** p = &_file) + { + filePointer = (void*)Interlocked.Exchange(ref *(IntPtr*)p, default); + } + if (filePointer == null) + return; + if (RuntimeInformationShim.IsRunningOnWindows) + DisposeCoreWindows(filePointer); + else + DisposeCoreUnix(filePointer); + } + + public void Dispose() + { + DisposeCore(); + GC.SuppressFinalize(this); + } + + ~MemoryMappedFileShim() + { + DisposeCore(); + } +} diff --git a/test/AsmResolver.Tests/IO/MemoryMappedIOTest.cs b/test/AsmResolver.Tests/IO/MemoryMappedIOTest.cs index 636f8d3d7..d7df8a91d 100644 --- a/test/AsmResolver.Tests/IO/MemoryMappedIOTest.cs +++ b/test/AsmResolver.Tests/IO/MemoryMappedIOTest.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using AsmResolver.IO; using AsmResolver.Tests.Runners; @@ -34,7 +35,7 @@ public void CreateReaderPastStreamShouldThrow() File.WriteAllBytes(tempPath, contents); using var service = new MemoryMappedFileService(); - Assert.Throws(() => + Assert.Throws(() => service.OpenFile(tempPath).CreateReader(0, 0, (uint) (contents.Length + 1))); } }