using System; using System.IO; using System.Runtime.InteropServices; using BizHawk.Common; namespace BizHawk.BizInvoke { public abstract class MemoryBlockBase : IDisposable { /// allocate bytes starting at a particular address /// is not aligned or is 0 protected MemoryBlockBase(ulong start, ulong size) { if (!WaterboxUtils.Aligned(start)) throw new ArgumentOutOfRangeException(nameof(start), start, "start address must be aligned"); if (size == 0) throw new ArgumentOutOfRangeException(nameof(size), size, "cannot create 0-length block"); Size = WaterboxUtils.AlignUp(size); AddressRange = start.RangeToExclusive(start + Size); _pageData = new Protection[1 + GetPage(AddressRange.EndInclusive)]; } /// stores last set memory protection value for each page protected readonly Protection[] _pageData; /// valid address range of the memory block public readonly Range AddressRange; /// total size of the memory block public readonly ulong Size; /// snapshot for XOR buffer protected byte[] _snapshot; /// true if this is currently swapped in public bool Active { get; protected set; } public byte[] XorHash { get; protected set; } /// get a page index within the block protected int GetPage(ulong addr) => AddressRange.Contains(addr) ? (int) ((addr - AddressRange.Start) >> WaterboxUtils.PageShift) : throw new ArgumentOutOfRangeException(nameof(addr), addr, "invalid address"); /// get a start address for a page index within the block protected ulong GetStartAddr(int page) => AddressRange.Start + ((ulong) page << WaterboxUtils.PageShift); /// Get a stream that can be used to read or write from part of the block. Does not check for or change ! /// or end (= + ) are outside public Stream GetStream(ulong start, ulong length, bool writer) { if (start < AddressRange.Start) throw new ArgumentOutOfRangeException(nameof(start), start, "invalid address"); if (AddressRange.EndInclusive < start + length - 1) throw new ArgumentOutOfRangeException(nameof(length), length, "requested length implies invalid end address"); return new MemoryViewStream(!writer, writer, (long) start, (long) length, this); } /// get a stream that can be used to read or write from part of the block. both reads and writes will be XORed against an earlier recorded snapshot /// or end (= + ) are outside /// no snapshot taken (haven't called ) public Stream GetXorStream(ulong start, ulong length, bool writer) { if (start < AddressRange.Start) throw new ArgumentOutOfRangeException(nameof(start), start, "invalid address"); if (AddressRange.EndInclusive < start + length - 1) throw new ArgumentOutOfRangeException(nameof(length), length, "requested length implies invalid end address"); if (_snapshot == null) throw new InvalidOperationException("No snapshot taken!"); return new MemoryViewXorStream(!writer, writer, (long) start, (long) length, this, _snapshot, (long) (start - AddressRange.Start)); } /// activate the memory block, swapping it in at the pre-specified address public abstract void Activate(); /// deactivate the memory block, removing it from RAM but leaving it immediately available to swap back in public abstract void Deactivate(); /// take a hash of the current full contents of the block, including unreadable areas public abstract byte[] FullHash(); /// set r/w/x protection on a portion of memory. rounded to encompassing pages public abstract void Protect(ulong start, ulong length, Protection prot); /// restore all recorded protections protected abstract void ProtectAll(); /// take a snapshot of the entire memory block's contents, for use in public abstract void SaveXorSnapshot(); public abstract void Dispose(bool disposing); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } ~MemoryBlockBase() { Dispose(false); } /// allocate bytes starting at a particular address public static MemoryBlockBase CallPlatformCtor(ulong start, ulong size) => OSTailoredCode.IsUnixHost ? (MemoryBlockBase) new MemoryBlockUnix(start, size) : new MemoryBlock(start, size); /// allocate bytes at any address public static MemoryBlockBase CallPlatformCtor(ulong size) => CallPlatformCtor(0, size); /// Memory protection constant public enum Protection : byte { None, R, RW, RX } private class MemoryViewStream : Stream { public MemoryViewStream(bool readable, bool writable, long ptr, long length, MemoryBlockBase owner) { _readable = readable; _writable = writable; _ptr = ptr; _length = length; _owner = owner; _pos = 0; } private readonly long _length; private readonly MemoryBlockBase _owner; private readonly long _ptr; private readonly bool _readable; private readonly bool _writable; private long _pos; public override bool CanRead => _readable; public override bool CanSeek => true; public override bool CanWrite => _writable; public override long Length => _length; public override long Position { get => _pos; set { if (value < 0 || _length < value) throw new ArgumentOutOfRangeException(); _pos = value; } } private void EnsureNotDisposed() { if (_owner.AddressRange.Start == 0) throw new ObjectDisposedException(nameof(MemoryBlockBase)); //TODO bug? } public override void Flush() {} public override int Read(byte[] buffer, int offset, int count) { if (!_readable) throw new InvalidOperationException(); if (count < 0 || buffer.Length < count + offset) throw new ArgumentOutOfRangeException(); EnsureNotDisposed(); count = (int) Math.Min(count, _length - _pos); Marshal.Copy(Z.SS(_ptr + _pos), buffer, offset, count); _pos += count; return count; } public override long Seek(long offset, SeekOrigin origin) { long newpos; switch (origin) { default: case SeekOrigin.Begin: newpos = offset; break; case SeekOrigin.Current: newpos = _pos + offset; break; case SeekOrigin.End: newpos = _length + offset; break; } Position = newpos; return newpos; } public override void SetLength(long value) { throw new InvalidOperationException(); } public override void Write(byte[] buffer, int offset, int count) { if (!_writable) throw new InvalidOperationException(); if (count < 0 || _length - _pos < count || buffer.Length < count + offset) throw new ArgumentOutOfRangeException(); EnsureNotDisposed(); Marshal.Copy(buffer, offset, Z.SS(_ptr + _pos), count); _pos += count; } } private class MemoryViewXorStream : MemoryViewStream { public MemoryViewXorStream(bool readable, bool writable, long ptr, long length, MemoryBlockBase owner, byte[] initial, long offset) : base(readable, writable, ptr, length, owner) { _initial = initial; _offset = (int) offset; } /// the initial data to XOR against for both reading and writing private readonly byte[] _initial; /// offset into the XOR data that this stream is representing private readonly int _offset; public override int Read(byte[] buffer, int offset, int count) { var pos = (int) Position; count = base.Read(buffer, offset, count); XorTransform(_initial, _offset + pos, buffer, offset, count); return count; } public override void Write(byte[] buffer, int offset, int count) { var pos = (int) Position; if (count < 0 || Length - pos < count || buffer.Length < count + offset) throw new ArgumentOutOfRangeException(); // is mutating the buffer passed to Stream.Write kosher? XorTransform(_initial, _offset + pos, buffer, offset, count); base.Write(buffer, offset, count); } /// bounds check already done by calling method i.e. in base.Read (for ) or in private static unsafe void XorTransform(byte[] source, int sourceOffset, byte[] dest, int destOffset, int length) { // TODO: C compilers can make this pretty snappy, but can the C# jitter? Or do we need intrinsics fixed (byte* _s = source, _d = dest) { byte* s = _s + sourceOffset; byte* d = _d + destOffset; byte* sEnd = s + length; while (s < sEnd) *d++ ^= *s++; } } } } }