Fix linux waterbox
Sorry about that -- some things were removed but not others to go with them. It had been working before.
This commit is contained in:
parent
2c2ed72dc6
commit
42ceba7d21
|
@ -32,31 +32,5 @@ 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>
|
||||
/// <param name="pagedata">
|
||||
/// Caller-owned array that should indicate which areas were set to RW_Stack.
|
||||
/// Will not be modified by callee. Some implementations need this to get all of the correct information in dest.
|
||||
/// </param>
|
||||
void GetWriteStatus(WriteDetectionStatus[] dest, MemoryBlock.Protection[] pagedata);
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ namespace BizHawk.BizInvoke
|
|||
Size = WaterboxUtils.AlignUp(size);
|
||||
EndExclusive = Start + Size;
|
||||
_pageData = (Protection[])(object)new byte[GetPage(EndExclusive - 1) + 1];
|
||||
_dirtydata = (WriteDetectionStatus[])(object)new byte[GetPage(EndExclusive - 1) + 1];
|
||||
|
||||
_pal = OSTailoredCode.IsUnixHost
|
||||
? (IMemoryBlockPal)new MemoryBlockLinuxPal(Start, Size)
|
||||
|
@ -39,7 +38,6 @@ namespace BizHawk.BizInvoke
|
|||
|
||||
/// <summary>stores last set memory protection value for each page</summary>
|
||||
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"/>)
|
||||
|
@ -52,11 +50,6 @@ namespace BizHawk.BizInvoke
|
|||
/// <summary>starting address of the memory block</summary>
|
||||
public readonly ulong Start;
|
||||
|
||||
/// <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; }
|
||||
|
||||
|
@ -76,13 +69,6 @@ namespace BizHawk.BizInvoke
|
|||
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"/>!
|
||||
|
@ -108,8 +94,6 @@ namespace BizHawk.BizInvoke
|
|||
throw new InvalidOperationException("Already active");
|
||||
_pal.Activate();
|
||||
ProtectAll();
|
||||
if (_sealed)
|
||||
_pal.SetWriteStatus(_dirtydata);
|
||||
Active = true;
|
||||
}
|
||||
|
||||
|
@ -120,22 +104,10 @@ namespace BizHawk.BizInvoke
|
|||
public void Deactivate()
|
||||
{
|
||||
EnsureActive();
|
||||
if (_sealed)
|
||||
_pal.GetWriteStatus(_dirtydata, _pageData);
|
||||
_pal.Deactivate();
|
||||
Active = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// read the of the current full contents of the block, including unreadable areas
|
||||
/// but not uncommitted areas
|
||||
/// </summary>
|
||||
public byte[] FullHash()
|
||||
{
|
||||
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)
|
||||
|
@ -143,9 +115,7 @@ namespace BizHawk.BizInvoke
|
|||
EnsureActive();
|
||||
if (length == 0)
|
||||
return;
|
||||
if (_sealed)
|
||||
_pal.GetWriteStatus(_dirtydata, _pageData);
|
||||
|
||||
|
||||
// Note: asking for prot.none on memory that was not previously committed, commits it
|
||||
|
||||
var computedStart = WaterboxUtils.AlignDown(start);
|
||||
|
@ -166,17 +136,10 @@ namespace BizHawk.BizInvoke
|
|||
for (int i = pstart; i <= pend; i++)
|
||||
{
|
||||
_pageData[i] = prot;
|
||||
// inform the low level code what addresses might fault on it
|
||||
if (prot == Protection.RW || prot == Protection.RW_Stack)
|
||||
_dirtydata[i] |= WriteDetectionStatus.CanChange;
|
||||
else
|
||||
_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>
|
||||
|
@ -188,211 +151,17 @@ 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] || _dirtydata[i] != _dirtydata[i + 1])
|
||||
if (i == pageLimit - 1 || _pageData[i] != _pageData[i + 1])
|
||||
{
|
||||
ulong zstart = GetStartAddr(ps);
|
||||
ulong zend = GetStartAddr(i + 1);
|
||||
var prot = _pageData[i];
|
||||
// adjust frontend notion of prot to the PAL layer's expectation
|
||||
if (prot == Protection.RW_Stack)
|
||||
{
|
||||
if (!_sealed)
|
||||
{
|
||||
// don't activate this protection yet
|
||||
prot = Protection.RW;
|
||||
}
|
||||
else
|
||||
{
|
||||
var didChange = (_dirtydata[i] & WriteDetectionStatus.DidChange) != 0;
|
||||
if (didChange)
|
||||
// don't needlessly retrigger
|
||||
prot = Protection.RW;
|
||||
}
|
||||
}
|
||||
else if (prot == Protection.RW_Invisible)
|
||||
{
|
||||
// this never matters to the backend
|
||||
prot = Protection.RW;
|
||||
}
|
||||
else if (_sealed && prot == Protection.RW)
|
||||
{
|
||||
var didChange = (_dirtydata[i] & WriteDetectionStatus.DidChange) != 0;
|
||||
if (!didChange)
|
||||
// set to trigger if we have not before
|
||||
prot = Protection.R;
|
||||
}
|
||||
|
||||
_pal.Protect(zstart, zend - zstart, prot);
|
||||
ps = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Seal()
|
||||
{
|
||||
EnsureActive();
|
||||
if (_sealed)
|
||||
throw new InvalidOperationException("Already sealed");
|
||||
_snapshot = new byte[CommittedSize];
|
||||
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, _pageData);
|
||||
|
||||
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 (_pageData[p] != Protection.RW_Invisible && (_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);
|
||||
}
|
||||
}
|
||||
// Console.WriteLine($"{Start:x16}");
|
||||
// var voom = _pageData.Take((int)(CommittedSize >> 12)).Select(p =>
|
||||
// {
|
||||
// switch (p)
|
||||
// {
|
||||
// case Protection.None: return ' ';
|
||||
// case Protection.R: return 'R';
|
||||
// case Protection.RW: return 'W';
|
||||
// case Protection.RX: return 'X';
|
||||
// case Protection.RW_Invisible: return '!';
|
||||
// case Protection.RW_Stack: return '+';
|
||||
// default: return '?';
|
||||
// }
|
||||
// }).Select((c, i) => new { c, i }).GroupBy(a => a.i / 60);
|
||||
// var zoom = _dirtydata.Take((int)(CommittedSize >> 12)).Select(p =>
|
||||
// {
|
||||
// switch (p)
|
||||
// {
|
||||
// case WriteDetectionStatus.CanChange: return '.';
|
||||
// case WriteDetectionStatus.CanChange | WriteDetectionStatus.DidChange: return '*';
|
||||
// case 0: return ' ';
|
||||
// case WriteDetectionStatus.DidChange: return '!';
|
||||
// default: return '?';
|
||||
// }
|
||||
// }).Select((c, i) => new { c, i }).GroupBy(a => a.i / 60);
|
||||
// foreach (var l in voom.Zip(zoom, (a, b) => new { a, b }))
|
||||
// {
|
||||
// Console.WriteLine("____" + new string(l.a.Select(a => a.c).ToArray()));
|
||||
// Console.WriteLine("____" + new string(l.b.Select(a => a.c).ToArray()));
|
||||
// }
|
||||
}
|
||||
|
||||
public void LoadState(BinaryReader r)
|
||||
{
|
||||
EnsureActive();
|
||||
EnsureSealed();
|
||||
_pal.GetWriteStatus(_dirtydata, _pageData);
|
||||
|
||||
if (r.ReadUInt64() != MAGIC || r.ReadUInt64() != Start || r.ReadUInt64() != Size)
|
||||
throw new InvalidOperationException("Savestate internal mismatch");
|
||||
if (!r.ReadBytes(_hash.Length).SequenceEqual(_hash))
|
||||
{
|
||||
// romhackurz need this not to throw on them.
|
||||
// anywhere where non-sync settings enter non-invisible ram, we need this not to throw
|
||||
Console.Error.WriteLine("WARNING: MEMORY BLOCK CONSISTENCY CHECK 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;
|
||||
var inState = newPageData[p] != Protection.RW_Invisible && newDirty;
|
||||
|
||||
if (dirty || inState)
|
||||
{
|
||||
// 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);
|
||||
|
||||
// NB: There are some weird behaviors possible if a block transitions to or from RW_Invisible,
|
||||
// but nothing really "broken" (as far as I know); just reflecting the fact that you cannot track it.
|
||||
|
||||
if (inState)
|
||||
{
|
||||
// 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()
|
||||
{
|
||||
if (_pal != null)
|
||||
|
@ -415,15 +184,6 @@ namespace BizHawk.BizInvoke
|
|||
R,
|
||||
RW,
|
||||
RX,
|
||||
/// <summary>
|
||||
/// This area should not be tracked for changes, and should not be saved.
|
||||
/// </summary>
|
||||
RW_Invisible,
|
||||
/// <summary>
|
||||
/// This area may be used as a stack and should use a change detection that will work on stacks.
|
||||
/// (In windows, this is an inferior detection that triggers on reads. In Linux, this flag has no effect.)
|
||||
/// </summary>
|
||||
RW_Stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,10 +74,6 @@ namespace BizHawk.BizInvoke
|
|||
throw new InvalidOperationException($"{nameof(mmap)}() failed with error {Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
}
|
||||
if ((IntPtr)LinGuard.AddTripGuard(Z.UU(_start), Z.UU(_size)) == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(LinGuard.AddTripGuard)}() returned NULL");
|
||||
}
|
||||
_active = true;
|
||||
}
|
||||
|
||||
|
@ -89,8 +85,6 @@ namespace BizHawk.BizInvoke
|
|||
if (errorCode != 0)
|
||||
throw new InvalidOperationException($"{nameof(munmap)}() failed with error {Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
if (!LinGuard.RemoveTripGuard(Z.UU(_start), Z.UU(_size)))
|
||||
throw new InvalidOperationException($"{nameof(LinGuard.RemoveTripGuard)}() returned FALSE");
|
||||
_active = false;
|
||||
}
|
||||
|
||||
|
@ -123,12 +117,6 @@ namespace BizHawk.BizInvoke
|
|||
return MemoryProtection.Read | MemoryProtection.Write;
|
||||
case Protection.RX:
|
||||
return MemoryProtection.Read | MemoryProtection.Execute;
|
||||
case Protection.RW_Invisible:
|
||||
return MemoryProtection.Read | MemoryProtection.Write;
|
||||
case Protection.RW_Stack:
|
||||
// because of sigaltstack, LinGuard has no issues with readonly stacks and the special distinction that
|
||||
// the windows port draws between stack vs non stack memory is ignored here
|
||||
return MemoryProtection.Read;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(prot));
|
||||
}
|
||||
|
@ -144,56 +132,5 @@ namespace BizHawk.BizInvoke
|
|||
if (errorCode != 0)
|
||||
throw new InvalidOperationException($"{nameof(mprotect)}() failed with error {Marshal.GetLastWin32Error()}!");
|
||||
}
|
||||
|
||||
public void GetWriteStatus(WriteDetectionStatus[] dest, Protection[] pagedata)
|
||||
{
|
||||
var p = (IntPtr)LinGuard.ExamineTripGuard(Z.UU(_start), Z.UU(_size));
|
||||
if (p == IntPtr.Zero)
|
||||
throw new InvalidOperationException($"{nameof(LinGuard.ExamineTripGuard)}() returned NULL!");
|
||||
Marshal.Copy(p, (byte[])(object)dest, 0, dest.Length);
|
||||
}
|
||||
|
||||
public void SetWriteStatus(WriteDetectionStatus[] src)
|
||||
{
|
||||
var p = (IntPtr)LinGuard.ExamineTripGuard(Z.UU(_start), Z.UU(_size));
|
||||
if (p == IntPtr.Zero)
|
||||
throw new InvalidOperationException($"{nameof(LinGuard.ExamineTripGuard)}() returned NULL!");
|
||||
Marshal.Copy((byte[])(object)src, 0, p, src.Length);
|
||||
}
|
||||
|
||||
private static unsafe class LinGuard
|
||||
{
|
||||
/// <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 LinGuard function, or trips any tracked page during this call.
|
||||
/// CALLER'S RESPONSIBILITY: Pages to be tracked are mprotected to R. Pages with write permission
|
||||
/// cause no issues, but they will not trip.
|
||||
/// </summary>
|
||||
/// <returns>The same information as ExamineTripGuard, or null on failure</returns>
|
||||
[DllImport("linguard.so")]
|
||||
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 LinGuard 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("linguard.so")]
|
||||
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 LinGuard 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("linguard.so")]
|
||||
public static extern WriteDetectionStatus* ExamineTripGuard(UIntPtr start, UIntPtr length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,10 +50,6 @@ 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");
|
||||
}
|
||||
_active = true;
|
||||
}
|
||||
|
||||
|
@ -61,8 +57,6 @@ namespace BizHawk.BizInvoke
|
|||
{
|
||||
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");
|
||||
_active = false;
|
||||
}
|
||||
|
||||
|
@ -87,11 +81,6 @@ namespace BizHawk.BizInvoke
|
|||
case Protection.R: p = Kernel32.MemoryProtection.READONLY; break;
|
||||
case Protection.RW: p = Kernel32.MemoryProtection.READWRITE; break;
|
||||
case Protection.RX: p = Kernel32.MemoryProtection.EXECUTE_READ; break;
|
||||
// VEH can't work when the stack is not writable, because VEH is delivered on that selfsame stack. The kernel
|
||||
// simply declines to return to user mode on a first chance exception if the stack is not writable.
|
||||
// So we use guard pages instead, which are much worse because reads set them off as well, but it doesn't matter
|
||||
// in this case.
|
||||
case Protection.RW_Stack: p = Kernel32.MemoryProtection.READWRITE | Kernel32.MemoryProtection.GUARD_Modifierflag; break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(prot));
|
||||
}
|
||||
return p;
|
||||
|
@ -116,51 +105,6 @@ namespace BizHawk.BizInvoke
|
|||
}
|
||||
}
|
||||
|
||||
public void GetWriteStatus(WriteDetectionStatus[] dest, Protection[] pagedata)
|
||||
{
|
||||
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);
|
||||
|
||||
// guard pages do not trigger VEH when they are actually being used as a stack and there are other
|
||||
// free pages below them, so virtualquery to get that information out
|
||||
Kernel32.MEMORY_BASIC_INFORMATION mbi;
|
||||
for (int i = 0; i < pagedata.Length;)
|
||||
{
|
||||
if (pagedata[i] == Protection.RW_Stack)
|
||||
{
|
||||
var q = ((ulong)i << WaterboxUtils.PageShift) + _start;
|
||||
Kernel32.VirtualQuery(Z.UU(q), &mbi, Z.SU(sizeof(Kernel32.MEMORY_BASIC_INFORMATION)));
|
||||
var pstart = (int)(((ulong)mbi.BaseAddress - _start) >> WaterboxUtils.PageShift);
|
||||
var pend = pstart + (int)((ulong)mbi.RegionSize >> WaterboxUtils.PageShift);
|
||||
if (pstart != i)
|
||||
throw new Exception("Huh?");
|
||||
|
||||
if ((mbi.Protect & Kernel32.MemoryProtection.GUARD_Modifierflag) == 0)
|
||||
{
|
||||
// tripped!
|
||||
for (int j = pstart; j < pend; j++)
|
||||
dest[j] |= WriteDetectionStatus.DidChange;
|
||||
}
|
||||
i = pend;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -279,40 +223,5 @@ namespace BizHawk.BizInvoke
|
|||
[DllImport("kernel32.dll")]
|
||||
public static extern UIntPtr VirtualQuery(UIntPtr lpAddress, MEMORY_BASIC_INFORMATION* lpBuffer, UIntPtr dwLength);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ELFSharp" Version="2.10.0" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.4" />
|
||||
<Reference Include="FlatBuffers.Core" HintPath="$(ProjectDir)../../References/FlatBuffers.Core.dll" Private="true" />
|
||||
<Reference Include="Virtu" HintPath="$(ProjectDir)../../References/Virtu.dll" Private="true" />
|
||||
|
|
|
@ -1,372 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using BizHawk.BizInvoke;
|
||||
using BizHawk.Common;
|
||||
using BizHawk.Emulation.Common;
|
||||
using ELFSharp.ELF;
|
||||
using ELFSharp.ELF.Sections;
|
||||
using ELFSharp.ELF.Segments;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Waterbox
|
||||
{
|
||||
public class ElfLoader : IImportResolver, IDisposable, IBinaryStateable
|
||||
{
|
||||
private readonly ELF<ulong> _elf;
|
||||
private readonly byte[] _elfHash;
|
||||
|
||||
private readonly List<SymbolEntry<ulong>> _allSymbols;
|
||||
private readonly Dictionary<string, SymbolEntry<ulong>> _visibleSymbols;
|
||||
private readonly Dictionary<string, Section<ulong>> _sectionsByName;
|
||||
private readonly List<SymbolEntry<ulong>> _importSymbols;
|
||||
|
||||
private readonly Section<ulong> _imports;
|
||||
private readonly Section<ulong> _sealed;
|
||||
private readonly Section<ulong> _invisible;
|
||||
|
||||
private readonly bool _skipCoreConsistencyCheck;
|
||||
private readonly bool _skipMemoryConsistencyCheck;
|
||||
|
||||
private bool _everythingSealed;
|
||||
|
||||
public MemoryBlock Memory { get; private set; }
|
||||
|
||||
public string ModuleName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Where writable data begins
|
||||
/// </summary>
|
||||
private ulong _writeStart;
|
||||
/// <summary>
|
||||
/// Where writiable data begins after seal
|
||||
/// </summary>
|
||||
private ulong _postSealWriteStart;
|
||||
/// <summary>
|
||||
/// Where executable data ends
|
||||
/// </summary>
|
||||
private ulong _execEnd;
|
||||
|
||||
public ElfLoader(string moduleName, byte[] fileData, ulong assumedStart, bool skipCoreConsistencyCheck, bool skipMemoryConsistencyCheck)
|
||||
{
|
||||
ModuleName = moduleName;
|
||||
_skipCoreConsistencyCheck = skipCoreConsistencyCheck;
|
||||
_skipMemoryConsistencyCheck = skipMemoryConsistencyCheck;
|
||||
|
||||
_elfHash = WaterboxUtils.Hash(fileData);
|
||||
_elf = ELFReader.Load<ulong>(new MemoryStream(fileData, false), true);
|
||||
|
||||
var loadsegs = _elf.Segments.Where(s => s.Type == SegmentType.Load);
|
||||
var start = loadsegs.Min(s => s.Address);
|
||||
start = WaterboxUtils.AlignDown(start);
|
||||
var end = loadsegs.Max(s => s.Address + s.Size);
|
||||
end = WaterboxUtils.AlignUp(end);
|
||||
var size = end - start;
|
||||
|
||||
if (start != assumedStart)
|
||||
throw new InvalidOperationException($"{nameof(assumedStart)} did not match actual origin in elf file");
|
||||
|
||||
if (_elf.Sections.Any(s => s.Name.StartsWith(".rel")))
|
||||
throw new InvalidOperationException("Elf has relocations!");
|
||||
|
||||
_allSymbols = ((ISymbolTable)_elf.GetSection(".symtab"))
|
||||
.Entries
|
||||
.Cast<SymbolEntry<ulong>>()
|
||||
.ToList();
|
||||
|
||||
_sectionsByName = _elf.Sections
|
||||
.ToDictionary(s => s.Name);
|
||||
|
||||
_sectionsByName.TryGetValue(".wbxsyscall", out _imports);
|
||||
if (_imports == null)
|
||||
{
|
||||
// Likely cause: This is a valid elf file, but it was not compiled by our toolchain at all
|
||||
throw new InvalidOperationException("Missing .wbxsyscall section!");
|
||||
}
|
||||
_sectionsByName.TryGetValue(".sealed", out _sealed);
|
||||
_sectionsByName.TryGetValue(".invis", out _invisible);
|
||||
|
||||
_visibleSymbols = _allSymbols
|
||||
.Where(s => s.Binding == SymbolBinding.Global && s.Visibility == SymbolVisibility.Default)
|
||||
.ToDictionary(s => s.Name);
|
||||
|
||||
_importSymbols = _allSymbols
|
||||
// TODO: No matter what attributes I provide, I seem to end up with Local and/or Hidden symbols in
|
||||
// .wbxsyscall a lot of the time on heavily optimized release builds.
|
||||
// Fortunately, there's nothing else in .wbxsyscall so we can just not filter at all.
|
||||
.Where(s => s.PointedSection == _imports)
|
||||
.ToList();
|
||||
|
||||
Memory = MemoryBlock.Create(start, size);
|
||||
Memory.Activate();
|
||||
Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.RW);
|
||||
|
||||
foreach (var seg in loadsegs)
|
||||
{
|
||||
var data = seg.GetFileContents();
|
||||
Marshal.Copy(data, 0, Z.US(seg.Address), Math.Min((int)seg.Size, (int)seg.FileSize));
|
||||
}
|
||||
|
||||
{
|
||||
// Compute RW boundaries
|
||||
|
||||
var allocated = _elf.Sections
|
||||
.Where(s => (s.Flags & SectionFlags.Allocatable) != 0);
|
||||
var writable = allocated
|
||||
.Where(s => (s.Flags & SectionFlags.Writable) != 0);
|
||||
var postSealWritable = writable
|
||||
.Where(s => !IsSpecialReadonlySection(s));
|
||||
var saveable = postSealWritable
|
||||
.Where(s => s != _invisible);
|
||||
var executable = allocated
|
||||
.Where(s => (s.Flags & SectionFlags.Executable) != 0);
|
||||
|
||||
_writeStart = WaterboxUtils.AlignDown(writable.Min(s => s.LoadAddress));
|
||||
_postSealWriteStart = WaterboxUtils.AlignDown(postSealWritable.Min(s => s.LoadAddress));
|
||||
_execEnd = WaterboxUtils.AlignUp(executable.Max(s => s.LoadAddress + s.Size));
|
||||
|
||||
// validate; this may require linkscript cooperation
|
||||
// due to the segment limitations, the only thing we'd expect to catch is a custom eventually readonly section
|
||||
// in the wrong place (because the linkscript doesn't know "eventually readonly")
|
||||
if (_execEnd > _writeStart)
|
||||
throw new InvalidOperationException($"ElfLoader: Executable data to {_execEnd:X16} overlaps writable data from {_writeStart}");
|
||||
}
|
||||
|
||||
PrintSections();
|
||||
PrintGdbData();
|
||||
PrintTopSavableSymbols();
|
||||
Protect();
|
||||
}
|
||||
|
||||
private void PrintGdbData()
|
||||
{
|
||||
Console.WriteLine("GDB Symbol Load:");
|
||||
Console.WriteLine($" add-sym {ModuleName} -s .text {_elf.Sections.Single(s => s.Name == ".text").LoadAddress}");
|
||||
}
|
||||
|
||||
private void PrintSections()
|
||||
{
|
||||
Console.WriteLine($"Mounted `{ModuleName}` @{Memory.Start:x16}");
|
||||
foreach (var s in _elf.Sections.OrderBy(s => s.LoadAddress))
|
||||
{
|
||||
Console.WriteLine(" @{0:x16} {1}{2}{3} `{4}` {5} bytes",
|
||||
s.LoadAddress,
|
||||
(s.Flags & SectionFlags.Allocatable) != 0 ? "R" : " ",
|
||||
(s.Flags & SectionFlags.Writable) != 0 ? "W" : " ",
|
||||
(s.Flags & SectionFlags.Executable) != 0 ? "X" : " ",
|
||||
s.Name,
|
||||
s.Size);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintTopSavableSymbols()
|
||||
{
|
||||
// TODO: With change detection, this isn't so great anymore.
|
||||
// It could look at actual intersections with what we saved, but that would mean going into MemoryBlock
|
||||
var tops = _allSymbols
|
||||
.OrderByDescending(s => s.Size)
|
||||
.Where(s => s.Size >= 20 * 1024)
|
||||
.Take(30)
|
||||
.Select(s => $" {s.Name} {s.Size / 1024}kiB")
|
||||
.ToList();
|
||||
|
||||
if (tops.Count > 0)
|
||||
{
|
||||
Console.WriteLine("Top potential savestate symbols:");
|
||||
foreach (var text in tops)
|
||||
{
|
||||
Console.WriteLine(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if section is readonly after init
|
||||
/// </summary>
|
||||
private bool IsSpecialReadonlySection(Section<ulong> sec)
|
||||
{
|
||||
// TODO: I don't think there are any more relro sections, right?
|
||||
return sec.Name.Contains(".rel.ro")
|
||||
|| sec.Name.StartsWith(".got")
|
||||
|| sec.Name == ".init_array"
|
||||
|| sec.Name == ".fini_array"
|
||||
|| sec.Name == ".tbss"
|
||||
|| sec == _imports
|
||||
|| sec == _sealed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set memory protections, pre or post seal
|
||||
/// </summary>
|
||||
private void Protect()
|
||||
{
|
||||
var writeStart = _everythingSealed ? _postSealWriteStart : _writeStart;
|
||||
|
||||
Memory.Protect(Memory.Start, _execEnd - Memory.Start, MemoryBlock.Protection.RX);
|
||||
Memory.Protect(_execEnd, writeStart - _execEnd, MemoryBlock.Protection.R);
|
||||
Memory.Protect(writeStart, Memory.EndExclusive - writeStart, MemoryBlock.Protection.RW);
|
||||
if (_invisible != null)
|
||||
{
|
||||
if (!WaterboxUtils.Aligned(_invisible.LoadAddress))
|
||||
throw new InvalidOperationException("Invisible section start not aligned! Check linker script");
|
||||
var end = WaterboxUtils.AlignUp(_invisible.LoadAddress + _invisible.Size);
|
||||
if (_sectionsByName.Values.Any(s => s != _invisible && s.LoadAddress < end && s.LoadAddress >= _invisible.LoadAddress))
|
||||
{
|
||||
throw new InvalidOperationException("Invisible section end not aligned, or not padded! Check linker script");
|
||||
}
|
||||
Memory.Protect(
|
||||
_invisible.LoadAddress,
|
||||
_invisible.Size,
|
||||
MemoryBlock.Protection.RW_Invisible);
|
||||
}
|
||||
}
|
||||
|
||||
// connect all of the .wbxsyscall stuff
|
||||
public void ConnectSyscalls(IImportResolver syscalls)
|
||||
{
|
||||
Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.RW);
|
||||
|
||||
var tmp = new IntPtr[1];
|
||||
var ptrSize = (ulong)IntPtr.Size;
|
||||
|
||||
foreach (var s in _importSymbols)
|
||||
{
|
||||
if (s.Size == ptrSize)
|
||||
{
|
||||
var p = syscalls.GetProcAddrOrThrow(s.Name);
|
||||
tmp[0] = p;
|
||||
Marshal.Copy(tmp, 0, Z.US(s.Value), 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (s.Size % ptrSize != 0)
|
||||
{
|
||||
// They're supposed to be arrays of pointers, so uhhh yeah?
|
||||
throw new InvalidOperationException($"Symbol {s.Name} has unexpected size");
|
||||
}
|
||||
var count = (int)(s.Size / ptrSize);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var p = syscalls.GetProcAddrOrThrow($"{s.Name}[{i}]");
|
||||
tmp[0] = p;
|
||||
Marshal.Copy(tmp, 0, Z.US(s.Value + ((ulong)i * ptrSize)), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Protect();
|
||||
}
|
||||
|
||||
public void RunNativeInit()
|
||||
{
|
||||
CallingConventionAdapters.Waterbox.GetDelegateForFunctionPointer<Action>(Z.US(_elf.EntryPoint))();
|
||||
}
|
||||
|
||||
public void SealImportsAndTakeXorSnapshot()
|
||||
{
|
||||
if (_everythingSealed)
|
||||
throw new InvalidOperationException($"{nameof(ElfLoader)} already sealed!");
|
||||
|
||||
// save import values, then zero them all (for hash purposes), then take our snapshot, then load them again,
|
||||
// then set the .wbxsyscall area to read only
|
||||
byte[] impData = null;
|
||||
impData = new byte[_imports.Size];
|
||||
Marshal.Copy(Z.US(_imports.LoadAddress), impData, 0, (int)_imports.Size);
|
||||
WaterboxUtils.ZeroMemory(Z.US(_imports.LoadAddress), (long)_imports.Size);
|
||||
|
||||
byte[] invData = null;
|
||||
if (_invisible != null)
|
||||
{
|
||||
invData = new byte[_invisible.Size];
|
||||
Marshal.Copy(Z.US(_invisible.LoadAddress), invData, 0, (int)_invisible.Size);
|
||||
WaterboxUtils.ZeroMemory(Z.US(_invisible.LoadAddress), (long)_invisible.Size);
|
||||
}
|
||||
|
||||
Memory.Seal();
|
||||
|
||||
Marshal.Copy(impData, 0, Z.US(_imports.LoadAddress), (int)_imports.Size);
|
||||
|
||||
if (_invisible != null)
|
||||
{
|
||||
Marshal.Copy(invData, 0, Z.US(_invisible.LoadAddress), (int)_invisible.Size);
|
||||
}
|
||||
|
||||
_everythingSealed = true;
|
||||
Protect();
|
||||
}
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
Memory.Dispose();
|
||||
Memory = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public IntPtr GetProcAddrOrZero(string entryPoint)
|
||||
{
|
||||
if (_visibleSymbols.TryGetValue(entryPoint, out var sym))
|
||||
{
|
||||
return Z.US(sym.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public IntPtr GetProcAddrOrThrow(string entryPoint)
|
||||
{
|
||||
if (_visibleSymbols.TryGetValue(entryPoint, out var sym))
|
||||
{
|
||||
return Z.US(sym.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Couldn't find {nameof(entryPoint)} {entryPoint} in {ModuleName}");
|
||||
}
|
||||
}
|
||||
|
||||
const ulong MAGIC = 0x6018ab7df99310ca;
|
||||
|
||||
public void SaveStateBinary(BinaryWriter bw)
|
||||
{
|
||||
if (!_everythingSealed)
|
||||
throw new InvalidOperationException(".wbxsyscall section must be closed before saving state");
|
||||
|
||||
bw.Write(MAGIC);
|
||||
bw.Write(_elfHash);
|
||||
Memory.SaveState(bw);
|
||||
}
|
||||
|
||||
public void LoadStateBinary(BinaryReader br)
|
||||
{
|
||||
if (!_everythingSealed)
|
||||
// operations happening in the wrong order. probable cause: internal logic error. make sure frontend calls Seal
|
||||
throw new InvalidOperationException(".wbxsyscall section must be closed before loading state");
|
||||
|
||||
if (br.ReadUInt64() != MAGIC)
|
||||
// file id is missing. probable cause: garbage savestate
|
||||
throw new InvalidOperationException("Savestate corrupted!");
|
||||
|
||||
var elfHash = br.ReadBytes(_elfHash.Length);
|
||||
if (_skipCoreConsistencyCheck)
|
||||
{
|
||||
throw new InvalidOperationException("We decided that the core consistency check should always run");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!elfHash.SequenceEqual(_elfHash))
|
||||
// the .dll file that is loaded now has a different hash than the .dll that created the savestate
|
||||
throw new InvalidOperationException("Core consistency check failed. Is this a savestate from a different version?");
|
||||
}
|
||||
|
||||
Memory.LoadState(br);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
using BizHawk.BizInvoke;
|
||||
using BizHawk.Emulation.Common;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Waterbox
|
||||
{
|
||||
/// <summary>
|
||||
/// a simple grow-only fixed max size heap
|
||||
/// </summary>
|
||||
internal sealed class Heap : IBinaryStateable, IDisposable
|
||||
{
|
||||
public MemoryBlock Memory { get; private set; }
|
||||
/// <summary>
|
||||
/// name, used in identifying errors
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
/// <summary>
|
||||
/// total number of bytes used
|
||||
/// </summary>
|
||||
public ulong Used { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// true if the heap has been sealed, preventing further changes
|
||||
/// </summary>
|
||||
public bool Sealed { get; private set; }
|
||||
|
||||
private byte[] _hash;
|
||||
|
||||
public Heap(ulong start, ulong size, string name)
|
||||
{
|
||||
Memory = MemoryBlock.Create(start, size);
|
||||
Used = 0;
|
||||
Name = name;
|
||||
Console.WriteLine("Created heap `{1}` at {0:x16}:{2:x16}", start, name, start + size);
|
||||
}
|
||||
|
||||
private ulong AlignForAllocation(int align)
|
||||
{
|
||||
if (align > 1)
|
||||
{
|
||||
ulong newused = ((Used - 1) | (ulong)((uint)align - 1)) + 1;
|
||||
if (newused > Memory.Size)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to meet alignment {align} on heap {Name}");
|
||||
}
|
||||
return newused;
|
||||
}
|
||||
return Used;
|
||||
}
|
||||
|
||||
public ulong Allocate(ulong size, int align)
|
||||
{
|
||||
if (Sealed)
|
||||
throw new InvalidOperationException($"Attempt made to allocate from sealed heap {Name}");
|
||||
|
||||
ulong allocstart = AlignForAllocation(align);
|
||||
ulong newused = allocstart + size;
|
||||
if (newused > Memory.Size)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to allocate {size} bytes from heap {Name}");
|
||||
}
|
||||
ulong ret = Memory.Start + allocstart;
|
||||
Memory.Protect(Memory.Start + Used, newused - Used, MemoryBlock.Protection.RW);
|
||||
Used = newused;
|
||||
Console.WriteLine($"Allocated {size} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)");
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void Seal()
|
||||
{
|
||||
if (!Sealed)
|
||||
{
|
||||
Memory.Protect(Memory.Start, Used, MemoryBlock.Protection.R);
|
||||
_hash = WaterboxUtils.Hash(Memory.GetStream(Memory.Start, WaterboxUtils.AlignUp(Used), false));
|
||||
Sealed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Attempt to reseal heap {Name}");
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveStateBinary(BinaryWriter bw)
|
||||
{
|
||||
bw.Write(Name);
|
||||
bw.Write(Used);
|
||||
if (!Sealed)
|
||||
{
|
||||
Memory.SaveState(bw);
|
||||
}
|
||||
else
|
||||
{
|
||||
bw.Write(_hash);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadStateBinary(BinaryReader br)
|
||||
{
|
||||
var name = br.ReadString();
|
||||
if (name != Name)
|
||||
// probable cause: internal error
|
||||
throw new InvalidOperationException($"Name did not match for heap {Name}");
|
||||
var used = br.ReadUInt64();
|
||||
if (used > Memory.Size)
|
||||
throw new InvalidOperationException($"Heap {Name} used {used} larger than available {Memory.Size}");
|
||||
if (!Sealed)
|
||||
{
|
||||
Memory.LoadState(br);
|
||||
Used = used;
|
||||
}
|
||||
else
|
||||
{
|
||||
var hash = br.ReadBytes(_hash.Length);
|
||||
if (!hash.SequenceEqual(_hash))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash did not match for heap {Name}. Is this the same rom with the same SyncSettings?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Memory != null)
|
||||
{
|
||||
Memory.Dispose();
|
||||
Memory = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,368 +0,0 @@
|
|||
using BizHawk.Emulation.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using BizHawk.BizInvoke;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Waterbox
|
||||
{
|
||||
/// <summary>
|
||||
/// a heap that supports basic alloc, free, and realloc calls
|
||||
/// </summary>
|
||||
internal sealed class MapHeap : IBinaryStateable, IDisposable
|
||||
{
|
||||
public MemoryBlock Memory { get; private set; }
|
||||
/// <summary>
|
||||
/// name, used in identifying errors
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// total number of bytes allocated
|
||||
/// </summary>
|
||||
public ulong Used { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// get a page index within the block
|
||||
/// </summary>
|
||||
private int GetPage(ulong addr)
|
||||
{
|
||||
return (int)((addr - Memory.Start) >> WaterboxUtils.PageShift);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get a start address for a page index within the block
|
||||
/// </summary>
|
||||
private ulong GetStartAddr(int page)
|
||||
{
|
||||
return ((ulong)page << WaterboxUtils.PageShift) + Memory.Start;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sentinel value used to indicate unmapped pages.
|
||||
/// Needed because mapped pages are allowed to be protected to None
|
||||
/// </summary>
|
||||
private const MemoryBlock.Protection FREE = (MemoryBlock.Protection)255;
|
||||
|
||||
/// <summary>
|
||||
/// Bitmap of protections. Similar to MemoryBlock._pageData (and unfortunately mirrors the same information),
|
||||
/// but also handles FREE
|
||||
/// </summary>
|
||||
private readonly MemoryBlock.Protection[] _pages;
|
||||
/// <summary>
|
||||
/// alias of _pages used for serialization
|
||||
/// </summary>
|
||||
private readonly byte[] _pagesAsBytes;
|
||||
|
||||
public MapHeap(ulong start, ulong size, string name)
|
||||
{
|
||||
size = WaterboxUtils.AlignUp(size);
|
||||
Memory = MemoryBlock.Create(start, size);
|
||||
Name = name;
|
||||
_pagesAsBytes = new byte[size >> WaterboxUtils.PageShift];
|
||||
_pages = (MemoryBlock.Protection[])(object)_pagesAsBytes;
|
||||
for (var i = 0; i < _pages.Length; i++)
|
||||
_pages[i] = FREE;
|
||||
Console.WriteLine($"Created {nameof(MapHeap)} `{name}` at {start:x16}:{start + size:x16}");
|
||||
}
|
||||
|
||||
// find consecutive unused pages to map
|
||||
private int FindConsecutiveFreePages(int count)
|
||||
{
|
||||
return FindConsecutiveFreePagesAssumingFreed(count, -1, -1);
|
||||
}
|
||||
|
||||
// find consecutive unused pages to map, pretending that [startPage..startPage + numPages) is free
|
||||
// used in realloc
|
||||
private int FindConsecutiveFreePagesAssumingFreed(int count, int startPage, int numPages)
|
||||
{
|
||||
// TODO: I'm sure there are sublinear algorithms for this, if the right data is maintained
|
||||
var starts = new List<int>();
|
||||
var sizes = new List<int>();
|
||||
|
||||
var currStart = 0;
|
||||
for (var i = 0; i <= _pages.Length; i++)
|
||||
{
|
||||
if (i == _pages.Length || _pages[i] != FREE && (i < startPage || i >= startPage + numPages))
|
||||
{
|
||||
if (currStart < i)
|
||||
{
|
||||
starts.Add(currStart);
|
||||
var size = i - currStart;
|
||||
if (size == count)
|
||||
return currStart;
|
||||
sizes.Add(i - currStart);
|
||||
}
|
||||
currStart = i + 1;
|
||||
}
|
||||
}
|
||||
// find smallest hole to reduce fragmentation
|
||||
// TODO: Is this needed?
|
||||
int bestIdx = -1;
|
||||
int bestSize = int.MaxValue;
|
||||
for (int i = 0; i < sizes.Count; i++)
|
||||
{
|
||||
if (sizes[i] < bestSize && sizes[i] >= count)
|
||||
{
|
||||
bestSize = sizes[i];
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
if (bestIdx != -1)
|
||||
return starts[bestIdx];
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void ProtectInternal(int startPage, int numPages, MemoryBlock.Protection prot, bool wasUsed)
|
||||
{
|
||||
for (var i = startPage; i < startPage + numPages; i++)
|
||||
_pages[i] = prot;
|
||||
|
||||
ulong start = GetStartAddr(startPage);
|
||||
ulong length = ((ulong)numPages) << WaterboxUtils.PageShift;
|
||||
if (prot == FREE)
|
||||
{
|
||||
Memory.Protect(start, length, MemoryBlock.Protection.RW);
|
||||
WaterboxUtils.ZeroMemory(Z.US(start), (long)length);
|
||||
Memory.Protect(start, length, MemoryBlock.Protection.None);
|
||||
Used -= length;
|
||||
Console.WriteLine($"Freed {length} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Memory.Protect(start, length, prot);
|
||||
if (wasUsed)
|
||||
{
|
||||
Console.WriteLine($"Set protection for {length} bytes on {Name} to {prot}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Used += length;
|
||||
Console.WriteLine($"Allocated {length} bytes on {Name}, utilization {Used}/{Memory.Size} ({100.0 * Used / Memory.Size:0.#}%)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshProtections(int startPage, int pageCount)
|
||||
{
|
||||
int ps = startPage;
|
||||
for (int i = startPage; i < startPage + pageCount; i++)
|
||||
{
|
||||
if (i == startPage + pageCount - 1 || _pages[i] != _pages[i + 1])
|
||||
{
|
||||
var p = _pages[i];
|
||||
ulong zstart = GetStartAddr(ps);
|
||||
ulong zlength = (ulong)(i - ps + 1) << WaterboxUtils.PageShift;
|
||||
Memory.Protect(zstart, zlength, p == FREE ? MemoryBlock.Protection.None : p);
|
||||
ps = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshAllProtections()
|
||||
{
|
||||
RefreshProtections(0, _pages.Length);
|
||||
}
|
||||
|
||||
private bool EnsureMapped(int startPage, int pageCount)
|
||||
{
|
||||
for (int i = startPage; i < startPage + pageCount; i++)
|
||||
{
|
||||
if (_pages[i] == FREE)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private bool EnsureMappedNonStack(int startPage, int pageCount)
|
||||
{
|
||||
for (int i = startPage; i < startPage + pageCount; i++)
|
||||
{
|
||||
if (_pages[i] == FREE || _pages[i] == MemoryBlock.Protection.RW_Stack)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public ulong Map(ulong size, MemoryBlock.Protection prot)
|
||||
{
|
||||
if (size == 0)
|
||||
return 0;
|
||||
int numPages = WaterboxUtils.PagesNeeded(size);
|
||||
int startPage = FindConsecutiveFreePages(numPages);
|
||||
if (startPage == -1)
|
||||
return 0;
|
||||
var ret = GetStartAddr(startPage);
|
||||
ProtectInternal(startPage, numPages, prot, false);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public ulong Remap(ulong start, ulong oldSize, ulong newSize, bool canMove)
|
||||
{
|
||||
// TODO: what is the expected behavior when everything requested for remap is allocated,
|
||||
// but with different protections?
|
||||
if (start < Memory.Start || start + oldSize > Memory.EndExclusive || oldSize == 0 || newSize == 0)
|
||||
return 0;
|
||||
|
||||
var oldStartPage = GetPage(start);
|
||||
var oldNumPages = WaterboxUtils.PagesNeeded(oldSize);
|
||||
if (!EnsureMappedNonStack(oldStartPage, oldNumPages))
|
||||
return 0;
|
||||
var oldProt = _pages[oldStartPage];
|
||||
|
||||
int newNumPages = WaterboxUtils.PagesNeeded(newSize);
|
||||
|
||||
if (!canMove)
|
||||
{
|
||||
if (newNumPages <= oldNumPages)
|
||||
{
|
||||
if (newNumPages < oldNumPages)
|
||||
ProtectInternal(oldStartPage + newNumPages, oldNumPages - newNumPages, FREE, true);
|
||||
return start;
|
||||
}
|
||||
else if (newNumPages > oldNumPages)
|
||||
{
|
||||
for (var i = oldStartPage + oldNumPages; i < oldStartPage + newNumPages; i++)
|
||||
if (_pages[i] != FREE)
|
||||
return 0;
|
||||
ProtectInternal(oldStartPage + oldNumPages, newNumPages - oldNumPages, oldProt, false);
|
||||
return start;
|
||||
}
|
||||
}
|
||||
|
||||
// if moving is allowed, we always move to simplify and defragment when possible
|
||||
int newStartPage = FindConsecutiveFreePagesAssumingFreed(newNumPages, oldStartPage, oldNumPages);
|
||||
if (newStartPage == -1)
|
||||
return 0;
|
||||
|
||||
var copyDataLen = Math.Min(oldSize, newSize);
|
||||
var copyPageLen = Math.Min(oldNumPages, newNumPages);
|
||||
|
||||
var data = new byte[copyDataLen];
|
||||
Memory.Protect(start, copyDataLen, MemoryBlock.Protection.RW);
|
||||
Marshal.Copy(Z.US(start), data, 0, (int)copyDataLen);
|
||||
|
||||
var pages = new MemoryBlock.Protection[copyPageLen];
|
||||
Array.Copy(_pages, oldStartPage, pages, 0, copyPageLen);
|
||||
|
||||
ProtectInternal(oldStartPage, oldNumPages, FREE, true);
|
||||
ProtectInternal(newStartPage, newNumPages, MemoryBlock.Protection.RW, false);
|
||||
|
||||
var ret = GetStartAddr(newStartPage);
|
||||
Marshal.Copy(data, 0, Z.US(ret), (int)copyDataLen);
|
||||
|
||||
Array.Copy(pages, 0, _pages, newStartPage, copyPageLen);
|
||||
RefreshProtections(newStartPage, copyPageLen);
|
||||
if (newNumPages > oldNumPages)
|
||||
ProtectInternal(newStartPage + oldNumPages, newNumPages - oldNumPages, oldProt, true);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public bool Unmap(ulong start, ulong size)
|
||||
{
|
||||
// TODO: eliminate copy+pasta between unmap and protect
|
||||
if (start < Memory.Start || start + size > Memory.EndExclusive || size == 0)
|
||||
return false;
|
||||
|
||||
var startPage = GetPage(start);
|
||||
var numPages = WaterboxUtils.PagesNeeded(size);
|
||||
if (!EnsureMapped(startPage, numPages))
|
||||
return false;
|
||||
|
||||
ProtectInternal(startPage, numPages, FREE, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Protect(ulong start, ulong size, MemoryBlock.Protection prot)
|
||||
{
|
||||
// TODO: eliminate copy+pasta between unmap and protect
|
||||
if (start < Memory.Start || start + size > Memory.EndExclusive || size == 0)
|
||||
return false;
|
||||
|
||||
var startPage = GetPage(start);
|
||||
var numPages = WaterboxUtils.PagesNeeded(size);
|
||||
if (!EnsureMappedNonStack(startPage, numPages))
|
||||
return false;
|
||||
|
||||
ProtectInternal(startPage, numPages, prot, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Memory != null)
|
||||
{
|
||||
Memory.Dispose();
|
||||
Memory = null;
|
||||
}
|
||||
}
|
||||
|
||||
private const ulong MAGIC = 0x1590abbcdeef5910;
|
||||
|
||||
public void SaveStateBinary(BinaryWriter bw)
|
||||
{
|
||||
bw.Write(Name);
|
||||
bw.Write(Memory.Size);
|
||||
bw.Write(Used);
|
||||
bw.Write(_pagesAsBytes);
|
||||
Memory.SaveState(bw);
|
||||
bw.Write(MAGIC);
|
||||
}
|
||||
|
||||
public void LoadStateBinary(BinaryReader br)
|
||||
{
|
||||
var name = br.ReadString();
|
||||
if (name != Name)
|
||||
throw new InvalidOperationException($"Name did not match for {nameof(MapHeap)} {Name}");
|
||||
var size = br.ReadUInt64();
|
||||
if (size != Memory.Size)
|
||||
throw new InvalidOperationException($"Size did not match for {nameof(MapHeap)} {Name}");
|
||||
Used = br.ReadUInt64();
|
||||
br.Read(_pagesAsBytes, 0, _pagesAsBytes.Length);
|
||||
Memory.LoadState(br);
|
||||
if (br.ReadUInt64() != MAGIC)
|
||||
throw new InvalidOperationException("Savestate internal error");
|
||||
}
|
||||
|
||||
public static void StressTest()
|
||||
{
|
||||
var allocs = new Dictionary<ulong, ulong>();
|
||||
var mmo = new MapHeap(0x36a00000000, 256 * 1024 * 1024, "ballsacks");
|
||||
var rnd = new Random(12512);
|
||||
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
ulong siz = (ulong)(rnd.Next(256 * 1024) + 384 * 1024);
|
||||
siz = siz / 4096 * 4096;
|
||||
var ptr = mmo.Map(siz, MemoryBlock.Protection.RW);
|
||||
allocs.Add(ptr, siz);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
int idx = rnd.Next(allocs.Count);
|
||||
var elt = allocs.ElementAt(idx);
|
||||
mmo.Unmap(elt.Key, elt.Value);
|
||||
allocs.Remove(elt.Key);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
ulong siz = (ulong)(rnd.Next(256 * 1024) + 384 * 1024);
|
||||
siz = siz / 4096 * 4096;
|
||||
var ptr = mmo.Map(siz, MemoryBlock.Protection.RW);
|
||||
allocs.Add(ptr, siz);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
int idx = rnd.Next(allocs.Count);
|
||||
var elt = allocs.ElementAt(idx);
|
||||
mmo.Unmap(elt.Key, elt.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue