waterbox - track writes for smaller savestates
The waterbox system now uses host os facilities to track whether memory has been written to, to automatically choose what thing to savestate. This results in a large size decrease for some cores, like snes9x or gpgx (when running cartridge games). Doesn't do much for cores that were already memory efficient, or for bsnes because of libco compatibility issues; but those cores don't regress either.
This commit is contained in:
parent
0ad26a7435
commit
d06ed05929
Binary file not shown.
Binary file not shown.
|
@ -11,6 +11,8 @@ namespace BizHawk.BizInvoke
|
|||
/// Map in the memory area at the predetermined address. uncommitted space should be unreadable.
|
||||
/// For all other space, there is no requirement on initial protection value;
|
||||
/// correct protections will be applied via Protect() immediately after this call.
|
||||
/// There is no assumption for the values of WriteStatus either, which will be supplied immediately
|
||||
/// after this call
|
||||
/// </summary>
|
||||
void Activate();
|
||||
/// <summary>
|
||||
|
@ -30,5 +32,27 @@ namespace BizHawk.BizInvoke
|
|||
/// after this call; protections will be applied via Protect() immediately after this call.
|
||||
/// </summary>
|
||||
void Commit(ulong length);
|
||||
/// <summary>
|
||||
/// Gets the current write detection status on each page in the block. Pages marked with CanChange
|
||||
/// that are also committed and set to R, will not trigger a segmentation violation on write; instead
|
||||
/// automatically changing to RW and setting DidChange
|
||||
/// </summary>
|
||||
/// <param name="dest">Caller-owned array that the PAL will overwrite with page data</param>
|
||||
void GetWriteStatus(WriteDetectionStatus[] dest);
|
||||
/// <summary>
|
||||
/// Sets the current write detection status on each page in the block. Pages marked with CanChange
|
||||
/// that are also committed and set to R, will not trigger a segmentation violation on write; instead
|
||||
/// automatically changing to RW and setting DidChange
|
||||
/// </summary>
|
||||
/// <param name="src">Caller-owned array that the PAL will read data from into its internal buffers</param>
|
||||
void SetWriteStatus(WriteDetectionStatus[] src);
|
||||
}
|
||||
[Flags]
|
||||
public enum WriteDetectionStatus : byte
|
||||
{
|
||||
/// <summary>If set, the page will be allowed to transition from R to RW</summary>
|
||||
CanChange = 1,
|
||||
/// <summary>If set, the page transitioned from R to RW</summary>
|
||||
DidChange = 2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using BizHawk.Common;
|
||||
|
||||
namespace BizHawk.BizInvoke
|
||||
{
|
||||
public class MemoryBlock : IDisposable
|
||||
public class MemoryBlock : IDisposable /*, IBinaryStateable */
|
||||
{
|
||||
/// <summary>allocate <paramref name="size"/> bytes starting at a particular address <paramref name="start"/></summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="start"/> is not aligned or <paramref name="size"/> is <c>0</c></exception>
|
||||
|
@ -15,10 +16,13 @@ namespace BizHawk.BizInvoke
|
|||
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");
|
||||
if (start == 0)
|
||||
throw new NotImplementedException("Start == 0 doesn't work right now, not really");
|
||||
Start = start;
|
||||
Size = WaterboxUtils.AlignUp(size);
|
||||
EndExclusive = Start + Size;
|
||||
_pageData = new Protection[GetPage(EndExclusive - 1) + 1];
|
||||
_pageData = (Protection[])(object)new byte[GetPage(EndExclusive - 1) + 1];
|
||||
_dirtydata = (WriteDetectionStatus[])(object)new byte[GetPage(EndExclusive - 1) + 1];
|
||||
|
||||
_pal = OSTailoredCode.IsUnixHost
|
||||
? (IMemoryBlockPal)new MemoryBlockUnixPal(Start, Size)
|
||||
|
@ -33,14 +37,9 @@ namespace BizHawk.BizInvoke
|
|||
/// </summary>
|
||||
private ulong CommittedSize;
|
||||
|
||||
/// <summary>
|
||||
/// The last CommittedSize that was sent to the PAL layer when we were active.
|
||||
/// TODO: Do we really need the ability to change protections while inactive?
|
||||
/// </summary>
|
||||
private ulong LastActiveCommittedSize;
|
||||
|
||||
/// <summary>stores last set memory protection value for each page</summary>
|
||||
private readonly Protection[] _pageData;
|
||||
private Protection[] _pageData;
|
||||
private WriteDetectionStatus[] _dirtydata;
|
||||
|
||||
/// <summary>
|
||||
/// end address of the memory block (not part of the block; class invariant: equal to <see cref="Start"/> + <see cref="Size"/>)
|
||||
|
@ -53,14 +52,14 @@ namespace BizHawk.BizInvoke
|
|||
/// <summary>starting address of the memory block</summary>
|
||||
public readonly ulong Start;
|
||||
|
||||
/// <summary>snapshot for XOR buffer</summary>
|
||||
/// <summary>snapshot containing a clean state for all committed pages</summary>
|
||||
private byte[] _snapshot;
|
||||
|
||||
private byte[] _hash;
|
||||
|
||||
/// <summary>true if this is currently swapped in</summary>
|
||||
public bool Active { get; private set; }
|
||||
|
||||
public byte[] XorHash { get; private set; }
|
||||
|
||||
/// <summary>get a page index within the block</summary>
|
||||
private int GetPage(ulong addr)
|
||||
{
|
||||
|
@ -72,6 +71,19 @@ namespace BizHawk.BizInvoke
|
|||
/// <summary>get a start address for a page index within the block</summary>
|
||||
private ulong GetStartAddr(int page) => ((ulong) page << WaterboxUtils.PageShift) + Start;
|
||||
|
||||
private void EnsureActive()
|
||||
{
|
||||
if (!Active)
|
||||
throw new InvalidOperationException("MemoryBlock is not currently active");
|
||||
}
|
||||
private void EnsureSealed()
|
||||
{
|
||||
if (!_sealed)
|
||||
throw new InvalidOperationException("MemoryBlock is not currently sealed");
|
||||
}
|
||||
|
||||
private bool _sealed;
|
||||
|
||||
/// <summary>
|
||||
/// Get a stream that can be used to read or write from part of the block. Does not check for or change <see cref="Protect"/>!
|
||||
/// </summary>
|
||||
|
@ -85,27 +97,7 @@ namespace BizHawk.BizInvoke
|
|||
throw new ArgumentOutOfRangeException(nameof(start), start, "invalid address");
|
||||
if (EndExclusive < start + length)
|
||||
throw new ArgumentOutOfRangeException(nameof(length), length, "requested length implies invalid end address");
|
||||
return new MemoryViewStream(!writer, writer, (long)start, (long)length, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// <paramref name="start"/> or end (= <paramref name="start"/> + <paramref name="length"/> - <c>1</c>) are outside
|
||||
/// [<see cref="Start"/>, <see cref="EndExclusive"/>), the range of the block
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">no snapshot taken (haven't called <see cref="SaveXorSnapshot"/>)</exception>
|
||||
public Stream GetXorStream(ulong start, ulong length, bool writer)
|
||||
{
|
||||
if (start < Start)
|
||||
throw new ArgumentOutOfRangeException(nameof(start), start, "invalid address");
|
||||
if (EndExclusive < start + length)
|
||||
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 - Start));
|
||||
return new MemoryViewStream(!writer, writer, (long)start, (long)length);
|
||||
}
|
||||
|
||||
/// <summary>activate the memory block, swapping it in at the pre-specified address</summary>
|
||||
|
@ -115,12 +107,9 @@ namespace BizHawk.BizInvoke
|
|||
if (Active)
|
||||
throw new InvalidOperationException("Already active");
|
||||
_pal.Activate();
|
||||
if (CommittedSize > LastActiveCommittedSize)
|
||||
{
|
||||
_pal.Commit(CommittedSize);
|
||||
LastActiveCommittedSize = CommittedSize;
|
||||
}
|
||||
ProtectAll();
|
||||
if (_sealed)
|
||||
_pal.SetWriteStatus(_dirtydata);
|
||||
Active = true;
|
||||
}
|
||||
|
||||
|
@ -130,38 +119,32 @@ namespace BizHawk.BizInvoke
|
|||
/// </exception>
|
||||
public void Deactivate()
|
||||
{
|
||||
if (!Active)
|
||||
throw new InvalidOperationException("Not active");
|
||||
EnsureActive();
|
||||
if (_sealed)
|
||||
_pal.GetWriteStatus(_dirtydata);
|
||||
_pal.Deactivate();
|
||||
Active = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// take a hash of the current full contents of the block, including unreadable areas
|
||||
/// read the of the current full contents of the block, including unreadable areas
|
||||
/// but not uncommitted areas
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// <see cref="MemoryBlock.Active"/> is <see langword="false"/> or failed to make memory read-only
|
||||
/// </exception>
|
||||
public byte[] FullHash()
|
||||
{
|
||||
if (!Active)
|
||||
throw new InvalidOperationException("Not active");
|
||||
// temporarily switch the committed parts to `R` so we can read them
|
||||
if (CommittedSize > 0)
|
||||
_pal.Protect(Start, CommittedSize, Protection.R);
|
||||
var ret = WaterboxUtils.Hash(GetStream(Start, CommittedSize, false));
|
||||
ProtectAll();
|
||||
return ret;
|
||||
EnsureSealed();
|
||||
return _hash;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>set r/w/x protection on a portion of memory. rounded to encompassing pages</summary
|
||||
/// <exception cref="InvalidOperationException">failed to protect memory</exception>
|
||||
public void Protect(ulong start, ulong length, Protection prot)
|
||||
{
|
||||
EnsureActive();
|
||||
if (length == 0)
|
||||
return;
|
||||
if (_sealed)
|
||||
_pal.GetWriteStatus(_dirtydata);
|
||||
|
||||
// Note: asking for prot.none on memory that was not previously committed, commits it
|
||||
|
||||
|
@ -169,32 +152,30 @@ namespace BizHawk.BizInvoke
|
|||
var computedEnd = WaterboxUtils.AlignUp(start + length);
|
||||
var computedLength = computedEnd - computedStart;
|
||||
|
||||
int pstart = GetPage(start);
|
||||
int pend = GetPage(start + length - 1);
|
||||
|
||||
for (int i = pstart; i <= pend; i++)
|
||||
_pageData[i] = prot; // also store the value for later use
|
||||
|
||||
// potentially commit more memory
|
||||
var minNewCommittedSize = computedEnd - Start;
|
||||
if (minNewCommittedSize > CommittedSize)
|
||||
{
|
||||
CommittedSize = minNewCommittedSize;
|
||||
// Since Commit() was called, we have to do a full ProtectAll -- remember that when refactoring
|
||||
_pal.Commit(CommittedSize);
|
||||
}
|
||||
|
||||
if (Active) // it's legal to Protect() if we're not active; the information is just saved for the next activation
|
||||
int pstart = GetPage(start);
|
||||
int pend = GetPage(start + length - 1);
|
||||
for (int i = pstart; i <= pend; i++)
|
||||
{
|
||||
if (CommittedSize > LastActiveCommittedSize)
|
||||
{
|
||||
_pal.Commit(CommittedSize);
|
||||
LastActiveCommittedSize = CommittedSize;
|
||||
ProtectAll();
|
||||
}
|
||||
_pageData[i] = prot;
|
||||
if (prot == Protection.RW)
|
||||
_dirtydata[i] |= WriteDetectionStatus.CanChange;
|
||||
else
|
||||
{
|
||||
_pal.Protect(computedStart, computedLength, prot);
|
||||
}
|
||||
_dirtydata[i] &= ~WriteDetectionStatus.CanChange;
|
||||
}
|
||||
|
||||
// TODO: restore the previous behavior where we would only reprotect a partial range
|
||||
ProtectAll();
|
||||
if (_sealed)
|
||||
_pal.SetWriteStatus(_dirtydata);
|
||||
}
|
||||
|
||||
/// <summary>restore all recorded protections</summary>
|
||||
|
@ -206,40 +187,151 @@ namespace BizHawk.BizInvoke
|
|||
int pageLimit = (int)(CommittedSize >> WaterboxUtils.PageShift);
|
||||
for (int i = 0; i < pageLimit; i++)
|
||||
{
|
||||
if (i == pageLimit - 1 || _pageData[i] != _pageData[i + 1])
|
||||
if (i == pageLimit - 1 || _pageData[i] != _pageData[i + 1] || _dirtydata[i] != _dirtydata[i + 1])
|
||||
{
|
||||
ulong zstart = GetStartAddr(ps);
|
||||
ulong zend = GetStartAddr(i + 1);
|
||||
_pal.Protect(zstart, zend - zstart, _pageData[i]);
|
||||
var prot = _pageData[i];
|
||||
if (_sealed && prot == Protection.RW)
|
||||
{
|
||||
var didChange = (_dirtydata[i] & WriteDetectionStatus.DidChange) != 0;
|
||||
if (!didChange)
|
||||
prot = Protection.R;
|
||||
}
|
||||
_pal.Protect(zstart, zend - zstart, prot);
|
||||
ps = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>take a snapshot of the entire memory block's contents, for use in <see cref="GetXorStream"/></summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// snapshot already taken, <see cref="MemoryBlock.Active"/> is <see langword="false"/>, or failed to make memory read-only
|
||||
/// </exception>
|
||||
public void SaveXorSnapshot()
|
||||
public void Seal()
|
||||
{
|
||||
// note: The snapshot only holds up to the current committed size. We compensate for that in xorstream
|
||||
if (_snapshot != null)
|
||||
throw new InvalidOperationException("Snapshot already taken");
|
||||
if (!Active)
|
||||
throw new InvalidOperationException("Not active");
|
||||
|
||||
// temporarily switch the entire committed area to `R`: in case some areas are unreadable, we don't want
|
||||
// that to complicate things
|
||||
if (CommittedSize > 0)
|
||||
_pal.Protect(Start, CommittedSize, Protection.R);
|
||||
|
||||
EnsureActive();
|
||||
if (_sealed)
|
||||
throw new InvalidOperationException("Already sealed");
|
||||
_snapshot = new byte[CommittedSize];
|
||||
var ds = new MemoryStream(_snapshot, true);
|
||||
var ss = GetStream(Start, CommittedSize, false);
|
||||
ss.CopyTo(ds);
|
||||
XorHash = WaterboxUtils.Hash(_snapshot);
|
||||
|
||||
if (CommittedSize > 0)
|
||||
{
|
||||
// temporarily switch the committed parts to `R` so we can read them
|
||||
_pal.Protect(Start, CommittedSize, Protection.R);
|
||||
Marshal.Copy(Z.US(Start), _snapshot, 0, (int)CommittedSize);
|
||||
}
|
||||
_hash = WaterboxUtils.Hash(_snapshot);
|
||||
_sealed = true;
|
||||
ProtectAll();
|
||||
_pal.SetWriteStatus(_dirtydata);
|
||||
}
|
||||
|
||||
const ulong MAGIC = 18123868458638683;
|
||||
|
||||
public void SaveState(BinaryWriter w)
|
||||
{
|
||||
EnsureActive();
|
||||
EnsureSealed();
|
||||
_pal.GetWriteStatus(_dirtydata);
|
||||
|
||||
w.Write(MAGIC);
|
||||
w.Write(Start);
|
||||
w.Write(Size);
|
||||
w.Write(_hash);
|
||||
w.Write(CommittedSize);
|
||||
w.Write((byte[])(object)_pageData);
|
||||
w.Write((byte[])(object)_dirtydata);
|
||||
|
||||
var buff = new byte[4096];
|
||||
int p;
|
||||
ulong addr;
|
||||
ulong endAddr = Start + CommittedSize;
|
||||
for (p = 0, addr = Start; addr < endAddr; p++, addr += 4096)
|
||||
{
|
||||
if ((_dirtydata[p] & WriteDetectionStatus.DidChange) != 0)
|
||||
{
|
||||
// TODO: It's slow to toggle individual pages like this.
|
||||
// Maybe that's OK because None is not used much?
|
||||
if (_pageData[p] == Protection.None)
|
||||
_pal.Protect(addr, 4096, Protection.R);
|
||||
Marshal.Copy(Z.US(addr), buff, 0, 4096);
|
||||
w.Write(buff);
|
||||
if (_pageData[p] == Protection.None)
|
||||
_pal.Protect(addr, 4096, Protection.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadState(BinaryReader r)
|
||||
{
|
||||
EnsureActive();
|
||||
EnsureSealed();
|
||||
_pal.GetWriteStatus(_dirtydata);
|
||||
|
||||
if (r.ReadUInt64() != MAGIC || r.ReadUInt64() != Start || r.ReadUInt64() != Size)
|
||||
throw new InvalidOperationException("Savestate internal mismatch");
|
||||
if (!r.ReadBytes(_hash.Length).SequenceEqual(_hash))
|
||||
{
|
||||
// TODO: We'll probably have to allow this for romhackurz
|
||||
throw new InvalidOperationException("Waterbox consistency guarantee failed");
|
||||
}
|
||||
var newCommittedSize = r.ReadUInt64();
|
||||
if (newCommittedSize > CommittedSize)
|
||||
{
|
||||
_pal.Commit(newCommittedSize);
|
||||
}
|
||||
else if (newCommittedSize < CommittedSize)
|
||||
{
|
||||
// PAL layer won't let us shrink commits, but that's kind of OK
|
||||
var start = Start + newCommittedSize;
|
||||
var size = CommittedSize - newCommittedSize;
|
||||
_pal.Protect(start, size, Protection.RW);
|
||||
WaterboxUtils.ZeroMemory(Z.US(start), (long)size);
|
||||
_pal.Protect(start, size, Protection.None);
|
||||
}
|
||||
CommittedSize = newCommittedSize;
|
||||
var newPageData = (Protection[])(object)r.ReadBytes(_pageData.Length);
|
||||
var newDirtyData = (WriteDetectionStatus[])(object)r.ReadBytes(_dirtydata.Length);
|
||||
|
||||
var buff = new byte[4096];
|
||||
int p;
|
||||
ulong addr;
|
||||
ulong endAddr = Start + CommittedSize;
|
||||
for (p = 0, addr = Start; addr < endAddr; p++, addr += 4096)
|
||||
{
|
||||
var dirty = (_dirtydata[p] & WriteDetectionStatus.DidChange) != 0;
|
||||
var newDirty = (newDirtyData[p] & WriteDetectionStatus.DidChange) != 0;
|
||||
|
||||
if (dirty || newDirty)
|
||||
{
|
||||
// must write out changed data
|
||||
|
||||
// TODO: It's slow to toggle individual pages like this
|
||||
if (_pageData[p] != Protection.RW)
|
||||
_pal.Protect(addr, 4096, Protection.RW);
|
||||
|
||||
if (newDirty)
|
||||
{
|
||||
// changed data comes from the savestate
|
||||
r.Read(buff, 0, 4096);
|
||||
Marshal.Copy(buff, 0, Z.US(addr), 4096);
|
||||
}
|
||||
else
|
||||
{
|
||||
// data comes from the snapshot
|
||||
var offs = (int)(addr - Start);
|
||||
if (offs < _snapshot.Length)
|
||||
{
|
||||
Marshal.Copy(_snapshot, offs, Z.US(addr), 4096);
|
||||
}
|
||||
else
|
||||
{
|
||||
// or was not in the snapshot at all, so had never been changed by seal
|
||||
WaterboxUtils.ZeroMemory(Z.US(addr), 4096);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_pageData = newPageData;
|
||||
_dirtydata = newDirtyData;
|
||||
ProtectAll();
|
||||
_pal.SetWriteStatus(_dirtydata);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -260,151 +352,5 @@ namespace BizHawk.BizInvoke
|
|||
/// <summary>Memory protection constant</summary>
|
||||
public enum Protection : byte { None, R, RW, RX }
|
||||
|
||||
private class MemoryViewStream : Stream
|
||||
{
|
||||
public MemoryViewStream(bool readable, bool writable, long ptr, long length, MemoryBlock owner)
|
||||
{
|
||||
_readable = readable;
|
||||
_writable = writable;
|
||||
_ptr = ptr;
|
||||
_length = length;
|
||||
_owner = owner;
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
private readonly long _length;
|
||||
private readonly MemoryBlock _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 || value > _length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
_pos = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
{
|
||||
if (_owner.Start == 0)
|
||||
throw new ObjectDisposedException(nameof(MemoryBlock));
|
||||
}
|
||||
|
||||
public override void Flush() {}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (!_readable)
|
||||
throw new InvalidOperationException();
|
||||
if (count < 0 || offset + count > buffer.Length)
|
||||
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 || _pos + count > _length || offset + count > buffer.Length)
|
||||
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, MemoryBlock owner, byte[] initial, long offset)
|
||||
: base(readable, writable, ptr, length, owner)
|
||||
{
|
||||
_initial = initial;
|
||||
_offset = (int)offset;
|
||||
}
|
||||
|
||||
/// <summary>the initial data to XOR against for both reading and writing</summary>
|
||||
private readonly byte[] _initial;
|
||||
|
||||
/// <summary>offset into the XOR data that this stream is representing</summary>
|
||||
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 || pos + count > Length || offset + count > buffer.Length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
// is mutating the buffer passed to Stream.Write kosher?
|
||||
XorTransform(_initial, _offset + pos, buffer, offset, count);
|
||||
base.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
private static unsafe void XorTransform(byte[] source, int sourceOffset, byte[] dest, int destOffset, int length)
|
||||
{
|
||||
// bounds checks on dest have been done already, but not on source
|
||||
// If a xorsnapshot was created and then the heap was later expanded, those other bytes are effectively 0
|
||||
|
||||
// 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 + Math.Min(sourceOffset + length, source.Length);
|
||||
while (s < sEnd)
|
||||
*d++ ^= *s++;
|
||||
// for anything left in dest past source.Length, we need not transform at all
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,12 @@ namespace BizHawk.BizInvoke
|
|||
/// </exception>
|
||||
public MemoryBlockUnixPal(ulong start, ulong size)
|
||||
{
|
||||
_start = start;
|
||||
_size = size;
|
||||
_fd = memfd_create("MemoryBlockUnix", 0);
|
||||
if (_fd == -1)
|
||||
throw new InvalidOperationException($"{nameof(memfd_create)}() returned -1");
|
||||
throw new NotImplementedException();
|
||||
// _start = start;
|
||||
// _size = size;
|
||||
// _fd = memfd_create("MemoryBlockUnix", 0);
|
||||
// if (_fd == -1)
|
||||
// throw new InvalidOperationException($"{nameof(memfd_create)}() returned -1");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -85,5 +86,15 @@ namespace BizHawk.BizInvoke
|
|||
if (errorCode != 0)
|
||||
throw new InvalidOperationException($"{nameof(mprotect)}() returned {errorCode}!");
|
||||
}
|
||||
|
||||
public void GetWriteStatus(WriteDetectionStatus[] dest)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetWriteStatus(WriteDetectionStatus[] src)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ using static BizHawk.BizInvoke.MemoryBlock;
|
|||
|
||||
namespace BizHawk.BizInvoke
|
||||
{
|
||||
internal sealed class MemoryBlockWindowsPal : IMemoryBlockPal
|
||||
internal sealed unsafe class MemoryBlockWindowsPal : IMemoryBlockPal
|
||||
{
|
||||
/// <summary>
|
||||
/// handle returned by CreateFileMapping
|
||||
|
@ -12,6 +12,7 @@ namespace BizHawk.BizInvoke
|
|||
private IntPtr _handle;
|
||||
private ulong _start;
|
||||
private ulong _size;
|
||||
private bool _guardActive;
|
||||
|
||||
/// <summary>
|
||||
/// Reserve bytes to later be swapped in, but do not map them
|
||||
|
@ -49,12 +50,20 @@ namespace BizHawk.BizInvoke
|
|||
{
|
||||
throw new InvalidOperationException($"{nameof(Kernel32.MapViewOfFileEx)}() returned NULL");
|
||||
}
|
||||
if ((IntPtr)WinGuard.AddTripGuard(Z.UU(_start), Z.UU(_size)) == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(WinGuard.AddTripGuard)}() returned NULL");
|
||||
}
|
||||
_guardActive = true;
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
if (!Kernel32.UnmapViewOfFile(Z.US(_start)))
|
||||
throw new InvalidOperationException($"{nameof(Kernel32.UnmapViewOfFile)}() returned NULL");
|
||||
if (!WinGuard.RemoveTripGuard(Z.UU(_start), Z.UU(_size)))
|
||||
throw new InvalidOperationException($"{nameof(WinGuard.RemoveTripGuard)}() returned FALSE");
|
||||
_guardActive = false;
|
||||
}
|
||||
|
||||
public void Protect(ulong start, ulong size, Protection prot)
|
||||
|
@ -89,10 +98,31 @@ namespace BizHawk.BizInvoke
|
|||
{
|
||||
Kernel32.CloseHandle(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
if (_guardActive)
|
||||
{
|
||||
WinGuard.RemoveTripGuard(Z.UU(_start), Z.UU(_size));
|
||||
_guardActive = false;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetWriteStatus(WriteDetectionStatus[] dest)
|
||||
{
|
||||
var p = (IntPtr)WinGuard.ExamineTripGuard(Z.UU(_start), Z.UU(_size));
|
||||
if (p == IntPtr.Zero)
|
||||
throw new InvalidOperationException($"{nameof(WinGuard.ExamineTripGuard)}() returned NULL!");
|
||||
Marshal.Copy(p, (byte[])(object)dest, 0, dest.Length);
|
||||
}
|
||||
|
||||
public void SetWriteStatus(WriteDetectionStatus[] src)
|
||||
{
|
||||
var p = (IntPtr)WinGuard.ExamineTripGuard(Z.UU(_start), Z.UU(_size));
|
||||
if (p == IntPtr.Zero)
|
||||
throw new InvalidOperationException($"{nameof(WinGuard.ExamineTripGuard)}() returned NULL!");
|
||||
Marshal.Copy((byte[])(object)src, 0, p, src.Length);
|
||||
}
|
||||
|
||||
~MemoryBlockWindowsPal()
|
||||
{
|
||||
Dispose();
|
||||
|
@ -183,5 +213,40 @@ namespace BizHawk.BizInvoke
|
|||
|
||||
public static readonly IntPtr INVALID_HANDLE_VALUE = Z.US(0xffffffffffffffff);
|
||||
}
|
||||
|
||||
private static unsafe class WinGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Add write detection to an area of memory. Any page in the specified range that has CanChange
|
||||
/// set and triggers an access violation on write
|
||||
/// will be noted, set to read+write permissions, and execution will be continued.
|
||||
/// CALLER'S RESPONSIBILITY: All addresses are page aligned.
|
||||
/// CALLER'S RESPONSIBILITY: No other thread enters any WinGuard function, or trips any tracked page during this call.
|
||||
/// CALLER'S RESPONSIBILITY: Pages to be tracked are VirtualProtected to R (no G) beforehand. Pages with write permission
|
||||
/// cause no issues, but they will not trip. WinGuard will not intercept Guard flag exceptions in any way.
|
||||
/// </summary>
|
||||
/// <returns>The same information as ExamineTripGuard, or null on failure</returns>
|
||||
[DllImport("winguard.dll")]
|
||||
public static extern WriteDetectionStatus* AddTripGuard(UIntPtr start, UIntPtr length);
|
||||
/// <summary>
|
||||
/// Remove write detection from the specified addresses.
|
||||
/// CALLER'S RESPONSIBILITY: All addresses are page aligned.
|
||||
/// CALLER'S RESPONSIBILITY: No other thread enters any WinGuard function, or trips any tracked guard page during this call.
|
||||
/// </summary>
|
||||
/// <returns>false on failure (usually, the address range did not match a known one)</returns>
|
||||
[DllImport("winguard.dll")]
|
||||
public static extern bool RemoveTripGuard(UIntPtr start, UIntPtr length);
|
||||
/// <summary>
|
||||
/// Examines a previously installed guard page detection.
|
||||
/// CALLER'S RESPONSIBILITY: All addresses are page aligned.
|
||||
/// CALLER'S RESPONSIBILITY: No other thread enters any WinGuard function, or trips any tracked guard page during this call.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A pointer to an array of bytes, one byte for each memory page in the range. Caller should set CanChange on pages to
|
||||
/// observe, and read back DidChange to see if things changed.
|
||||
/// </returns>
|
||||
[DllImport("winguard.dll")]
|
||||
public static extern WriteDetectionStatus* ExamineTripGuard(UIntPtr start, UIntPtr length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace BizHawk.BizInvoke
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a stream that allows read/write over a set of unmanaged memory pointers
|
||||
/// The validity and lifetime of those pointers is YOUR responsibility
|
||||
/// </summary>
|
||||
public class MemoryViewStream : Stream
|
||||
{
|
||||
public MemoryViewStream(bool readable, bool writable, long ptr, long length)
|
||||
{
|
||||
_readable = readable;
|
||||
_writable = writable;
|
||||
_ptr = ptr;
|
||||
_length = length;
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
private readonly long _length;
|
||||
private readonly long _ptr;
|
||||
private readonly bool _readable;
|
||||
private readonly bool _writable;
|
||||
private bool _closed;
|
||||
|
||||
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 || value > _length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
_pos = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
{
|
||||
if (_closed)
|
||||
throw new ObjectDisposedException(nameof(MemoryViewStream));
|
||||
}
|
||||
|
||||
public override void Flush() {}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (!_readable)
|
||||
throw new InvalidOperationException();
|
||||
if (count < 0 || offset + count > buffer.Length)
|
||||
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 || _pos + count > _length || offset + count > buffer.Length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
EnsureNotDisposed();
|
||||
|
||||
Marshal.Copy(buffer, offset, Z.SS(_ptr + _pos), count);
|
||||
_pos += count;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_closed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -278,7 +278,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
WaterboxUtils.ZeroMemory(Z.US(_invisible.LoadAddress), (long)_invisible.Size);
|
||||
}
|
||||
|
||||
Memory.SaveXorSnapshot();
|
||||
Memory.Seal();
|
||||
|
||||
Marshal.Copy(impData, 0, Z.US(_imports.LoadAddress), (int)_imports.Size);
|
||||
|
||||
|
@ -336,12 +336,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
|
||||
bw.Write(MAGIC);
|
||||
bw.Write(_elfHash);
|
||||
bw.Write(Memory.XorHash);
|
||||
|
||||
var len = Memory.EndExclusive - _saveStart;
|
||||
var ms = Memory.GetXorStream(_saveStart, len, false);
|
||||
bw.Write(len);
|
||||
ms.CopyTo(bw.BaseStream);
|
||||
Memory.SaveState(bw);
|
||||
}
|
||||
|
||||
public void LoadStateBinary(BinaryReader br)
|
||||
|
@ -366,20 +361,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
throw new InvalidOperationException("Core consistency check failed. Is this a savestate from a different version?");
|
||||
}
|
||||
|
||||
var xorHash = br.ReadBytes(Memory.XorHash.Length);
|
||||
if (!_skipMemoryConsistencyCheck)
|
||||
{
|
||||
if (!xorHash.SequenceEqual(Memory.XorHash))
|
||||
// the post-Seal memory state is different. probable cause: different rom or different version of rom,
|
||||
// different syncsettings
|
||||
throw new InvalidOperationException("Memory consistency check failed. Is this savestate from different SyncSettings?");
|
||||
}
|
||||
|
||||
var len = Memory.EndExclusive - _saveStart;
|
||||
if (br.ReadUInt64() != len)
|
||||
throw new InvalidOperationException("Unexpected saved length");
|
||||
var ms = Memory.GetXorStream(_saveStart, len, true);
|
||||
WaterboxUtils.CopySome(br.BaseStream, ms, (long)len);
|
||||
Memory.LoadState(br);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,9 +88,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
bw.Write(Used);
|
||||
if (!Sealed)
|
||||
{
|
||||
bw.Write(Memory.XorHash);
|
||||
var ms = Memory.GetXorStream(Memory.Start, WaterboxUtils.AlignUp(Used), false);
|
||||
ms.CopyTo(bw.BaseStream);
|
||||
Memory.SaveState(bw);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -109,32 +107,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
throw new InvalidOperationException($"Heap {Name} used {used} larger than available {Memory.Size}");
|
||||
if (!Sealed)
|
||||
{
|
||||
var hash = br.ReadBytes(Memory.XorHash.Length);
|
||||
if (!hash.SequenceEqual(Memory.XorHash))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash did not match for heap {Name}. Is this the same rom with the same SyncSettings?");
|
||||
}
|
||||
var oldUsedAligned = WaterboxUtils.AlignUp(Used);
|
||||
var usedAligned = WaterboxUtils.AlignUp(used);
|
||||
if (usedAligned > oldUsedAligned)
|
||||
{
|
||||
// grow
|
||||
var s = Memory.Start + oldUsedAligned;
|
||||
var l = usedAligned - oldUsedAligned;
|
||||
Memory.Protect(s, l, MemoryBlock.Protection.RW);
|
||||
}
|
||||
else if (usedAligned < oldUsedAligned)
|
||||
{
|
||||
// shrink
|
||||
var s = Memory.Start + usedAligned;
|
||||
var l = oldUsedAligned - usedAligned;
|
||||
// like elsewhere, we zero on free to avoid nondeterminism if later reallocated
|
||||
WaterboxUtils.ZeroMemory(Z.US(s), (long)l);
|
||||
Memory.Protect(s, l, MemoryBlock.Protection.None);
|
||||
}
|
||||
|
||||
var ms = Memory.GetXorStream(Memory.Start, usedAligned, true);
|
||||
WaterboxUtils.CopySome(br.BaseStream, ms, (long)usedAligned);
|
||||
Memory.LoadState(br);
|
||||
Used = used;
|
||||
}
|
||||
else
|
||||
|
|
|
@ -285,21 +285,9 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
bw.Write(Name);
|
||||
bw.Write(Memory.Size);
|
||||
bw.Write(Used);
|
||||
bw.Write(Memory.XorHash);
|
||||
bw.Write(_pagesAsBytes);
|
||||
|
||||
Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.R);
|
||||
var srcs = Memory.GetXorStream(Memory.Start, Memory.Size, false);
|
||||
for (int i = 0, addr = 0; i < _pages.Length; i++, addr += WaterboxUtils.PageSize)
|
||||
{
|
||||
if (_pages[i] != FREE)
|
||||
{
|
||||
srcs.Seek(addr, SeekOrigin.Begin);
|
||||
WaterboxUtils.CopySome(srcs, bw.BaseStream, WaterboxUtils.PageSize);
|
||||
}
|
||||
}
|
||||
Memory.SaveState(bw);
|
||||
bw.Write(MAGIC);
|
||||
RefreshAllProtections();
|
||||
}
|
||||
|
||||
public void LoadStateBinary(BinaryReader br)
|
||||
|
@ -310,31 +298,11 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
var size = br.ReadUInt64();
|
||||
if (size != Memory.Size)
|
||||
throw new InvalidOperationException($"Size did not match for {nameof(MapHeap)} {Name}");
|
||||
var used = br.ReadUInt64();
|
||||
var hash = br.ReadBytes(Memory.XorHash.Length);
|
||||
if (!hash.SequenceEqual(Memory.XorHash))
|
||||
throw new InvalidOperationException($"Hash did not match for {nameof(MapHeap)} {Name}. Is this the same rom?");
|
||||
|
||||
if (br.BaseStream.Read(_pagesAsBytes, 0, _pagesAsBytes.Length) != _pagesAsBytes.Length)
|
||||
throw new InvalidOperationException("Unexpected error reading!");
|
||||
|
||||
Used = 0;
|
||||
Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.RW);
|
||||
var dsts = Memory.GetXorStream(Memory.Start, Memory.Size, true);
|
||||
for (int i = 0, addr = 0; i < _pages.Length; i++, addr += WaterboxUtils.PageSize)
|
||||
{
|
||||
if (_pages[i] != FREE)
|
||||
{
|
||||
dsts.Seek(addr, SeekOrigin.Begin);
|
||||
WaterboxUtils.CopySome(br.BaseStream, dsts, WaterboxUtils.PageSize);
|
||||
Used += (uint)WaterboxUtils.PageSize;
|
||||
}
|
||||
}
|
||||
if (Used != used)
|
||||
throw new InvalidOperationException("Internal savestate error");
|
||||
Used = br.ReadUInt64();
|
||||
br.Read(_pagesAsBytes, 0, _pagesAsBytes.Length);
|
||||
Memory.LoadState(br);
|
||||
if (br.ReadUInt64() != MAGIC)
|
||||
throw new InvalidOperationException("Savestate internal error");
|
||||
RefreshAllProtections();
|
||||
}
|
||||
|
||||
public static void StressTest()
|
||||
|
|
|
@ -293,7 +293,7 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
IntPtr co_clean;
|
||||
if ((co_clean = _module.GetProcAddrOrZero("co_clean")) != IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Calling co_clean()...");
|
||||
Console.WriteLine("Calling co_clean().");
|
||||
CallingConventionAdapters.Waterbox.GetDelegateForFunctionPointer<Action>(co_clean)();
|
||||
}
|
||||
|
||||
|
@ -301,10 +301,21 @@ namespace BizHawk.Emulation.Cores.Waterbox
|
|||
foreach (var h in _heaps)
|
||||
{
|
||||
if (h != _invisibleheap && h != _sealedheap) // TODO: if we have more non-savestated heaps, refine this hack
|
||||
h.Memory.SaveXorSnapshot();
|
||||
h.Memory.Seal();
|
||||
}
|
||||
_module.SealImportsAndTakeXorSnapshot();
|
||||
_mmapheap?.Memory.SaveXorSnapshot();
|
||||
_mmapheap?.Memory.Seal();
|
||||
|
||||
// MemoryBlock's lazy write detection can't work on the current stack, because the VEH handler runs in the stack
|
||||
// so it has to already be writable. This isn't a problem for normal stacks, which are outside the cycle, but
|
||||
// co stacks have to be fully probed before they're used as stacks.
|
||||
|
||||
IntPtr co_probe;
|
||||
if ((co_probe = _module.GetProcAddrOrZero("co_probe")) != IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Calling co_probe().");
|
||||
CallingConventionAdapters.Waterbox.GetDelegateForFunctionPointer<Action>(co_probe)();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
NO_WBX_TARGETS := 1
|
||||
CCFLAGS := -std=c99
|
||||
CCFLAGS := -std=c99 -Wall
|
||||
SRCS := amd64.c coswap.s
|
||||
AS := nasm
|
||||
ASFLAGS := -f elf64 -Wall
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
license: public domain
|
||||
*/
|
||||
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
@ -14,79 +13,147 @@
|
|||
#include <sys/mman.h>
|
||||
#include <emulibc.h>
|
||||
|
||||
static long long co_active_buffer[64];
|
||||
static cothread_t co_active_handle = 0;
|
||||
|
||||
// allocations are 16k larger than asked for,
|
||||
// and include guard space between the stack and the storage
|
||||
|
||||
static void* alloc_thread(size_t* size)
|
||||
{
|
||||
// align up to 4k
|
||||
*size = (*size + 16384 + 4095) & ~4095;
|
||||
typedef struct {
|
||||
// used by coswap.s
|
||||
uint64_t jmp_buf[32];
|
||||
// lowest pointer in the stack; when we go below here, we'll hit the fake guard pages and overflow
|
||||
uint64_t* stack_bottom;
|
||||
// pointer just off the top (starting) end of the stack
|
||||
uint64_t* stack_top;
|
||||
// total size that went to mmap
|
||||
uint64_t mmap_size;
|
||||
uint64_t padding[477];
|
||||
// will be protected to unreadable, unwritable
|
||||
uint64_t guard[0x3000 / sizeof(uint64_t)];
|
||||
uint64_t stack[0];
|
||||
} cothread_impl;
|
||||
|
||||
uint64_t* ptr = mmap(NULL, *size,
|
||||
PROT_READ | PROT_WRITE, MAP_ANONYMOUS, -1, 0);
|
||||
if (ptr == (uint64_t*)(-1))
|
||||
// the cothread that represents the real host thread we started from
|
||||
static cothread_impl co_host_buffer;
|
||||
// what cothread are we in right now
|
||||
static cothread_impl* co_active_handle;
|
||||
|
||||
// a list of all real cothreads (does not include co_host_buffer)
|
||||
#define MAX_THREADS 64
|
||||
static cothread_impl* allthreads[MAX_THREADS];
|
||||
|
||||
static cothread_impl* alloc_thread(uint64_t size)
|
||||
{
|
||||
cothread_impl** dest = NULL;
|
||||
for (int i = 0; i < MAX_THREADS; i++)
|
||||
{
|
||||
if (!allthreads[i])
|
||||
{
|
||||
dest = &allthreads[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dest)
|
||||
return NULL;
|
||||
|
||||
ptr[512] = *size;
|
||||
for (int i = 513; i < 2048; i++)
|
||||
ptr[i] = 0xdeadbeefdeadbeef;
|
||||
// align up to 4k
|
||||
size = (size + 4095) & ~4095;
|
||||
|
||||
if (mprotect(ptr + 512, 512 * 3 * sizeof(uint64_t), PROT_NONE) != 0)
|
||||
uint64_t alloc_size = sizeof(cothread_impl) + size;
|
||||
|
||||
cothread_impl* co = mmap(NULL, alloc_size,
|
||||
PROT_READ | PROT_WRITE, MAP_ANONYMOUS, -1, 0);
|
||||
|
||||
if (co == (cothread_impl*)(-1))
|
||||
return NULL;
|
||||
|
||||
co->stack_bottom = &co->stack[0];
|
||||
co->stack_top = &co->stack[size / sizeof(uint64_t)];
|
||||
co->mmap_size = alloc_size;
|
||||
|
||||
for (int i = 0; i < 0x3000 / sizeof(uint64_t); i++)
|
||||
co->guard[i] = 0xdeadbeeffeedface;
|
||||
|
||||
if (mprotect(&co->guard[0], sizeof(co->guard), PROT_NONE) != 0)
|
||||
abort();
|
||||
|
||||
return ptr;
|
||||
|
||||
*dest = co;
|
||||
return co;
|
||||
}
|
||||
|
||||
static void free_thread(void* p)
|
||||
static void free_thread(cothread_impl* co)
|
||||
{
|
||||
uint64_t* ptr = (uint64_t*)p;
|
||||
if (mprotect(ptr + 512, 512 * 3 * sizeof(uint64_t), PROT_READ | PROT_WRITE) != 0)
|
||||
cothread_impl** src = NULL;
|
||||
for (int i = 0; i < MAX_THREADS; i++)
|
||||
{
|
||||
if (allthreads[i] == co)
|
||||
{
|
||||
src = &allthreads[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!src)
|
||||
abort();
|
||||
uint64_t size = ptr[512];
|
||||
memset(p, 0, size);
|
||||
if (munmap(ptr, size) != 0)
|
||||
|
||||
if (mprotect(&co->guard[0], sizeof(co->guard), PROT_READ | PROT_WRITE) != 0)
|
||||
abort();
|
||||
uint64_t alloc_size = co->mmap_size;
|
||||
memset(co, 0, alloc_size);
|
||||
if (munmap(co, alloc_size) != 0)
|
||||
abort();
|
||||
|
||||
*src = NULL;
|
||||
}
|
||||
|
||||
extern void co_swap(cothread_t, cothread_t);
|
||||
|
||||
static void crash()
|
||||
static void crash(void)
|
||||
{
|
||||
assert(0); /* called only if cothread_t entrypoint returns */
|
||||
}
|
||||
|
||||
ECL_EXPORT void co_clean()
|
||||
ECL_EXPORT void co_clean(void)
|
||||
{
|
||||
memset(co_active_buffer, 0, sizeof(co_active_buffer));
|
||||
memset(&co_host_buffer, 0, sizeof(co_host_buffer));
|
||||
}
|
||||
|
||||
cothread_t co_active()
|
||||
ECL_EXPORT void co_probe(void)
|
||||
{
|
||||
// VEH is delivered on the same thread that experienced the exception.
|
||||
// That means the stack has to be writable already, so the waterbox host's fault detector won't work
|
||||
// when we're on a readonly cothread stack. So we conservatively probe this entire stack before switching to it.
|
||||
for (int i = 0; i < MAX_THREADS; i++)
|
||||
{
|
||||
if (allthreads[i])
|
||||
{
|
||||
uint64_t volatile* p = (uint64_t volatile*)allthreads[i]->stack_bottom;
|
||||
uint64_t* pend = allthreads[i]->stack_top;
|
||||
for (; p < pend; p++)
|
||||
*p = *p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cothread_t co_active(void)
|
||||
{
|
||||
if (!co_active_handle)
|
||||
co_active_handle = &co_active_buffer;
|
||||
co_active_handle = &co_host_buffer;
|
||||
return co_active_handle;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int sz, void (*entrypoint)(void))
|
||||
{
|
||||
cothread_t handle;
|
||||
cothread_impl* co;
|
||||
if (!co_active_handle)
|
||||
co_active_handle = &co_active_buffer;
|
||||
co_active_handle = &co_host_buffer;
|
||||
|
||||
uint64_t size = sz;
|
||||
|
||||
if (handle = (cothread_t)alloc_thread(&size))
|
||||
if ((co = alloc_thread(sz)))
|
||||
{
|
||||
uint64_t* p = (uint64_t*)((char*)handle + size); // seek to top of stack
|
||||
*--p = (uint64_t)crash; /* crash if entrypoint returns */
|
||||
*--p = (uint64_t)entrypoint; /* start of function */
|
||||
*(uint64_t*)handle = (uint64_t)p; /* stack pointer */
|
||||
uint64_t* p = co->stack_top; // seek to top of stack
|
||||
*--p = (uint64_t)crash; // crash if entrypoint returns
|
||||
*--p = (uint64_t)entrypoint; // start of function */
|
||||
*(uint64_t*)co = (uint64_t)p; // stack pointer
|
||||
}
|
||||
|
||||
return handle;
|
||||
return co;
|
||||
}
|
||||
|
||||
void co_delete(cothread_t handle)
|
||||
|
|
|
@ -1,57 +1,51 @@
|
|||
section .text
|
||||
global co_swap
|
||||
global __imp_co_swap
|
||||
|
||||
; TODO: how to tell GCC it doesn't need this?
|
||||
align 16
|
||||
__imp_co_swap:
|
||||
dq co_swap
|
||||
|
||||
align 16
|
||||
co_swap:
|
||||
mov [rdx],rsp
|
||||
mov rsp,[rcx]
|
||||
pop rax
|
||||
mov [rdx+ 8],rbp
|
||||
mov [rdx+16],rsi
|
||||
mov [rdx+24],rdi
|
||||
mov [rdx+32],rbx
|
||||
mov [rdx+40],r12
|
||||
mov [rdx+48],r13
|
||||
mov [rdx+56],r14
|
||||
mov [rdx+64],r15
|
||||
;#if !defined(LIBCO_NO_SSE)
|
||||
movaps [rdx+ 80],xmm6
|
||||
movaps [rdx+ 96],xmm7
|
||||
movaps [rdx+112],xmm8
|
||||
add rdx,112
|
||||
movaps [rdx+ 16],xmm9
|
||||
movaps [rdx+ 32],xmm10
|
||||
movaps [rdx+ 48],xmm11
|
||||
movaps [rdx+ 64],xmm12
|
||||
movaps [rdx+ 80],xmm13
|
||||
movaps [rdx+ 96],xmm14
|
||||
movaps [rdx+112],xmm15
|
||||
;#endif
|
||||
mov rbp,[rcx+ 8]
|
||||
mov rsi,[rcx+16]
|
||||
mov rdi,[rcx+24]
|
||||
mov rbx,[rcx+32]
|
||||
mov r12,[rcx+40]
|
||||
mov r13,[rcx+48]
|
||||
mov r14,[rcx+56]
|
||||
mov r15,[rcx+64]
|
||||
;#if !defined(LIBCO_NO_SSE)
|
||||
movaps xmm6, [rcx+ 80]
|
||||
movaps xmm7, [rcx+ 96]
|
||||
movaps xmm8, [rcx+112]
|
||||
add rcx,112
|
||||
movaps xmm9, [rcx+ 16]
|
||||
movaps xmm10,[rcx+ 32]
|
||||
movaps xmm11,[rcx+ 48]
|
||||
movaps xmm12,[rcx+ 64]
|
||||
movaps xmm13,[rcx+ 80]
|
||||
movaps xmm14,[rcx+ 96]
|
||||
movaps xmm15,[rcx+112]
|
||||
;#endif
|
||||
jmp rax
|
||||
section .text
|
||||
global co_swap
|
||||
|
||||
align 16
|
||||
co_swap:
|
||||
mov [rdx],rsp
|
||||
mov rsp,[rcx]
|
||||
pop rax
|
||||
mov [rdx+ 8],rbp
|
||||
mov [rdx+16],rsi
|
||||
mov [rdx+24],rdi
|
||||
mov [rdx+32],rbx
|
||||
mov [rdx+40],r12
|
||||
mov [rdx+48],r13
|
||||
mov [rdx+56],r14
|
||||
mov [rdx+64],r15
|
||||
;#if !defined(LIBCO_NO_SSE)
|
||||
movaps [rdx+ 80],xmm6
|
||||
movaps [rdx+ 96],xmm7
|
||||
movaps [rdx+112],xmm8
|
||||
add rdx,112
|
||||
movaps [rdx+ 16],xmm9
|
||||
movaps [rdx+ 32],xmm10
|
||||
movaps [rdx+ 48],xmm11
|
||||
movaps [rdx+ 64],xmm12
|
||||
movaps [rdx+ 80],xmm13
|
||||
movaps [rdx+ 96],xmm14
|
||||
movaps [rdx+112],xmm15
|
||||
;#endif
|
||||
mov rbp,[rcx+ 8]
|
||||
mov rsi,[rcx+16]
|
||||
mov rdi,[rcx+24]
|
||||
mov rbx,[rcx+32]
|
||||
mov r12,[rcx+40]
|
||||
mov r13,[rcx+48]
|
||||
mov r14,[rcx+56]
|
||||
mov r15,[rcx+64]
|
||||
;#if !defined(LIBCO_NO_SSE)
|
||||
movaps xmm6, [rcx+ 80]
|
||||
movaps xmm7, [rcx+ 96]
|
||||
movaps xmm8, [rcx+112]
|
||||
add rcx,112
|
||||
movaps xmm9, [rcx+ 16]
|
||||
movaps xmm10,[rcx+ 32]
|
||||
movaps xmm11,[rcx+ 48]
|
||||
movaps xmm12,[rcx+ 64]
|
||||
movaps xmm13,[rcx+ 80]
|
||||
movaps xmm14,[rcx+ 96]
|
||||
movaps xmm15,[rcx+112]
|
||||
;#endif
|
||||
jmp rax
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
/*
|
||||
libco v18 (2016-09-14)
|
||||
author: byuu
|
||||
license: public domain
|
||||
*/
|
||||
|
||||
#ifndef LIBCO_H
|
||||
#define LIBCO_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* cothread_t;
|
||||
|
||||
cothread_t co_active();
|
||||
cothread_t co_create(unsigned int, void (*)(void));
|
||||
void co_delete(cothread_t);
|
||||
void co_switch(cothread_t);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ifndef LIBCO_H */
|
||||
#endif
|
||||
/*
|
||||
libco v18 (2016-09-14)
|
||||
author: byuu
|
||||
license: public domain
|
||||
*/
|
||||
|
||||
#ifndef LIBCO_H
|
||||
#define LIBCO_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* cothread_t;
|
||||
|
||||
cothread_t co_active(void);
|
||||
cothread_t co_create(unsigned int, void(*)(void));
|
||||
void co_delete(cothread_t);
|
||||
void co_switch(cothread_t);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ifndef LIBCO_H */
|
||||
#endif
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
ifeq (,$(findstring MINGW,$(shell uname)))
|
||||
$(error This must be built with mingw)
|
||||
endif
|
||||
|
||||
winguard.dll: winguard.c
|
||||
gcc -O2 -s -o $@ $< -shared
|
||||
|
||||
install: winguard.dll
|
||||
cp $< ../../output/dll
|
||||
clean:
|
||||
rm winguard.dll
|
||||
|
||||
.PHONY: install clean
|
|
@ -0,0 +1,101 @@
|
|||
#include <windows.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define MAX_TRIPS 64
|
||||
|
||||
typedef struct {
|
||||
uintptr_t start;
|
||||
uintptr_t length;
|
||||
uint8_t tripped[0];
|
||||
} tripwire_t;
|
||||
|
||||
static tripwire_t* Trips[MAX_TRIPS];
|
||||
static int HandlerInstalled;
|
||||
|
||||
static LONG VectoredHandler(struct _EXCEPTION_POINTERS* p_info)
|
||||
{
|
||||
EXCEPTION_RECORD* p_record = p_info->ExceptionRecord;
|
||||
|
||||
// CONTEXT* p_context = p_info->ContextRecord;
|
||||
DWORD64 flags = p_record->ExceptionInformation[0];
|
||||
|
||||
if (p_record->ExceptionCode != STATUS_ACCESS_VIOLATION // only trigger on access violations...
|
||||
|| !(flags & 1)) // ...due to a write attempts
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
uintptr_t faultAddress = (uintptr_t)p_record->ExceptionInformation[1];
|
||||
for (int i = 0; i < MAX_TRIPS; i++)
|
||||
{
|
||||
if (Trips[i] && faultAddress >= Trips[i]->start && faultAddress < Trips[i]->start + Trips[i]->length)
|
||||
{
|
||||
uintptr_t page = (faultAddress - Trips[i]->start) >> 12;
|
||||
if (Trips[i]->tripped[page] & 1) // should change
|
||||
{
|
||||
DWORD oldprot;
|
||||
if (!VirtualProtect((void*)faultAddress, 1, PAGE_READWRITE, &oldprot))
|
||||
{
|
||||
RaiseFailFastException(NULL, NULL, 0);
|
||||
while (1)
|
||||
;
|
||||
}
|
||||
Trips[i]->tripped[page] = 3; // did change
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
else
|
||||
{
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
}
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
__declspec(dllexport) uint8_t* AddTripGuard(uintptr_t start, uintptr_t length)
|
||||
{
|
||||
if (!HandlerInstalled)
|
||||
{
|
||||
if (!AddVectoredExceptionHandler(1 /* CALL_FIRST */, VectoredHandler))
|
||||
return NULL;
|
||||
HandlerInstalled = 1;
|
||||
}
|
||||
|
||||
uintptr_t npage = length >> 12;
|
||||
for (int i = 0; i < MAX_TRIPS; i++)
|
||||
{
|
||||
if (!Trips[i])
|
||||
{
|
||||
Trips[i] = calloc(1, sizeof(*Trips[i]) + npage);
|
||||
if (!Trips[i])
|
||||
return NULL;
|
||||
Trips[i]->start = start;
|
||||
Trips[i]->length = length;
|
||||
return &Trips[i]->tripped[0];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
__declspec(dllexport) int64_t RemoveTripGuard(uintptr_t start, uintptr_t length)
|
||||
{
|
||||
for (int i = 0; i < MAX_TRIPS; i++)
|
||||
{
|
||||
if (Trips[i] && Trips[i]->start == start && Trips[i]->length == length)
|
||||
{
|
||||
free(Trips[i]);
|
||||
Trips[i] = NULL;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
__declspec(dllexport) uint8_t* ExamineTripGuard(uintptr_t start, uintptr_t length)
|
||||
{
|
||||
for (int i = 0; i < MAX_TRIPS; i++)
|
||||
{
|
||||
if (Trips[i] && Trips[i]->start == start && Trips[i]->length == length)
|
||||
return &Trips[i]->tripped[0];
|
||||
}
|
||||
return NULL;
|
||||
}
|
Loading…
Reference in New Issue