1247 lines
36 KiB
C#
1247 lines
36 KiB
C#
using BizHawk.Common;
|
|
using BizHawk.Common.BizInvoke;
|
|
using BizHawk.Emulation.Common;
|
|
using PeNet;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace BizHawk.Emulation.Cores.Waterbox
|
|
{
|
|
public class PeRunnerOptions
|
|
{
|
|
// string directory, string filename, ulong heapsize, ulong sealedheapsize, ulong invisibleheapsize
|
|
/// <summary>
|
|
/// path which the main executable and all associated libraries should be found
|
|
/// </summary>
|
|
public string Path { get; set; }
|
|
|
|
/// <summary>
|
|
/// filename of the main executable; expected to be in Path
|
|
/// </summary>
|
|
public string Filename { get; set; }
|
|
|
|
/// <summary>
|
|
/// how large the normal heap should be. it services sbrk calls
|
|
/// can be 0, but sbrk calls will crash.
|
|
/// </summary>
|
|
public uint SbrkHeapSizeKB { get; set; }
|
|
|
|
/// <summary>
|
|
/// how large the sealed heap should be. it services special allocations that become readonly after init
|
|
/// Must be > 0 and at least large enough to store argv and envp, and any alloc_sealed() calls
|
|
/// </summary>
|
|
public uint SealedHeapSizeKB { get; set; }
|
|
|
|
/// <summary>
|
|
/// how large the invisible heap should be. it services special allocations which are not savestated
|
|
/// Must be > 0 and at least large enough for the internal vtables, and any alloc_invisible() calls
|
|
/// </summary>
|
|
public uint InvisibleHeapSizeKB { get; set; }
|
|
|
|
/// <summary>
|
|
/// how large the "plain" heap should be. it is savestated, and contains
|
|
/// Must be > 0 and at least large enough for the internal pthread structure, and any alloc_plain() calls
|
|
/// </summary>
|
|
public uint PlainHeapSizeKB { get; set; }
|
|
|
|
/// <summary>
|
|
/// how large the mmap heap should be. it is savestated.
|
|
/// can be 0, but mmap calls will crash.
|
|
/// </summary>
|
|
public uint MmapHeapSizeKB { get; set; }
|
|
|
|
/// <summary>
|
|
/// start address in memory
|
|
/// </summary>
|
|
public ulong StartAddress { get; set; } = PeRunner.CanonicalStart;
|
|
}
|
|
|
|
public class PeRunner : Swappable, IImportResolver, IBinaryStateable
|
|
{
|
|
/// <summary>
|
|
/// serves as a standin for libpsxscl.so
|
|
/// </summary>
|
|
private class Psx
|
|
{
|
|
private readonly PeRunner _parent;
|
|
private readonly List<Delegate> _traps = new List<Delegate>();
|
|
|
|
public Psx(PeRunner parent)
|
|
{
|
|
_parent = parent;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct PsxContext
|
|
{
|
|
public int Size;
|
|
public int Options;
|
|
public IntPtr SyscallVtable;
|
|
public IntPtr LdsoVtable;
|
|
public IntPtr PsxVtable;
|
|
public uint SysIdx;
|
|
public uint LibcIdx;
|
|
public IntPtr PthreadSurrogate;
|
|
public IntPtr PthreadCreate;
|
|
public IntPtr DoGlobalCtors;
|
|
public IntPtr DoGlobalDtors;
|
|
}
|
|
|
|
private void PopulateVtable(string moduleName, ICollection<string> entries, IntPtr table)
|
|
{
|
|
var imports = _parent._exports[moduleName];
|
|
var pointers = entries.Select(e =>
|
|
{
|
|
var ptr = imports.Resolve(e);
|
|
if (ptr == IntPtr.Zero)
|
|
{
|
|
var s = string.Format("Trapped on unimplemented function {0}:{1}", moduleName, e);
|
|
Action del = () =>
|
|
{
|
|
Console.WriteLine(s);
|
|
throw new InvalidOperationException(s);
|
|
};
|
|
_traps.Add(del);
|
|
ptr = CallingConventionAdapters.Waterbox.GetFunctionPointerForDelegate(del);
|
|
}
|
|
return ptr;
|
|
}).ToArray();
|
|
Marshal.Copy(pointers, 0, table, pointers.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// called by the PeRunner to reset pointers after a loadsave
|
|
/// </summary>
|
|
public void ReloadVtables()
|
|
{
|
|
_traps.Clear();
|
|
|
|
PopulateVtable("__syscalls", Enumerable.Range(0, 340).Select(i => "n" + i).ToList(), _syscallVtable);
|
|
PopulateVtable("__syscalls", new[] // ldso
|
|
{
|
|
"dladdr", "dlinfo", "dlsym", "dlopen", "dlclose", "dlerror", "reset_tls"
|
|
}, _ldsoVtable);
|
|
PopulateVtable("__syscalls", new[] // psx
|
|
{
|
|
"start_main", "convert_thread", "unmapself", "log_output", "pthread_self"
|
|
}, _psxVtable);
|
|
/*unsafe
|
|
{
|
|
var ptr = (IntPtr*)_psxVtable;
|
|
Console.WriteLine("AWESOMES: " + ptr[0]);
|
|
}*/
|
|
}
|
|
|
|
private IntPtr _syscallVtable;
|
|
private IntPtr _ldsoVtable;
|
|
private IntPtr _psxVtable;
|
|
|
|
private IntPtr AllocVtable(int count)
|
|
{
|
|
return Z.US(_parent._invisibleheap.Allocate((ulong)(count * IntPtr.Size), 16));
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "__psx_init")]
|
|
public int PsxInit(ref int argc, ref IntPtr argv, ref IntPtr envp, [In, Out]ref PsxContext context)
|
|
{
|
|
{
|
|
// argc = 1, argv = ["foobar, NULL], envp = [NULL]
|
|
argc = 1;
|
|
var argArea = _parent._sealedheap.Allocate(32, 16);
|
|
argv = Z.US(argArea);
|
|
envp = Z.US(argArea + (uint)IntPtr.Size * 2);
|
|
Marshal.WriteIntPtr(Z.US(argArea), Z.US(argArea + 24));
|
|
Marshal.WriteInt64(Z.US(argArea + 24), 0x7261626f6f66);
|
|
}
|
|
|
|
context.SyscallVtable = _syscallVtable = AllocVtable(340);
|
|
context.LdsoVtable = _ldsoVtable = AllocVtable(7);
|
|
context.PsxVtable = _psxVtable = AllocVtable(5);
|
|
// ctx comes from the native stack, where it could have any garbage in uninited fields
|
|
context.SysIdx = 0;
|
|
context.LibcIdx = 0;
|
|
context.DoGlobalCtors = IntPtr.Zero;
|
|
context.DoGlobalDtors = IntPtr.Zero;
|
|
|
|
ReloadVtables();
|
|
|
|
// TODO: we can't set these pointers 4 and preserve across session
|
|
// until we find out where they get saved to and add a way to reset them
|
|
/*var extraTable = CreateVtable("__syscalls", new[]
|
|
{
|
|
"pthread_surrogate", "pthread_create", "do_global_ctors", "do_global_dtors"
|
|
});
|
|
var tmp = new IntPtr[4];
|
|
Marshal.Copy(extraTable, tmp, 0, 4);
|
|
context.PthreadSurrogate = tmp[0];
|
|
context.PthreadCreate = tmp[1];
|
|
context.DoGlobalCtors = tmp[2];
|
|
context.DoGlobalDtors = tmp[3];*/
|
|
|
|
return 0; // success
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// special emulator-functions
|
|
/// </summary>
|
|
private class Emu
|
|
{
|
|
private readonly PeRunner _parent;
|
|
public Emu(PeRunner parent)
|
|
{
|
|
_parent = parent;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "alloc_sealed")]
|
|
public IntPtr AllocSealed(UIntPtr size)
|
|
{
|
|
return Z.US(_parent._sealedheap.Allocate((ulong)size, 16));
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "alloc_invisible")]
|
|
public IntPtr AllocInvisible(UIntPtr size)
|
|
{
|
|
return Z.US(_parent._invisibleheap.Allocate((ulong)size, 16));
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "alloc_plain")]
|
|
public IntPtr AllocPlain(UIntPtr size)
|
|
{
|
|
return Z.US(_parent._plainheap.Allocate((ulong)size, 16));
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "_debug_puts")]
|
|
public void DebugPuts(IntPtr s)
|
|
{
|
|
Console.WriteLine("_debug_puts:" + Marshal.PtrToStringAnsi(s));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// syscall emulation layer, as well as a bit of other stuff
|
|
/// </summary>
|
|
private class Syscalls : IBinaryStateable
|
|
{
|
|
public interface IFileObject : IBinaryStateable
|
|
{
|
|
bool Open(FileAccess access);
|
|
bool Close();
|
|
Stream Stream { get; }
|
|
string Name { get; }
|
|
}
|
|
|
|
private class SpecialFile : IFileObject
|
|
{
|
|
// stdin, stdout, stderr
|
|
public string Name { get; }
|
|
public Stream Stream { get; }
|
|
public bool Close() => false;
|
|
public bool Open(FileAccess access) => false;
|
|
|
|
public void SaveStateBinary(BinaryWriter writer) { }
|
|
public void LoadStateBinary(BinaryReader reader) { }
|
|
|
|
public SpecialFile(Stream stream, string name)
|
|
{
|
|
Stream = stream;
|
|
Name = name;
|
|
}
|
|
}
|
|
|
|
private class ReadonlyFirmware : IFileObject
|
|
{
|
|
private readonly byte[] _data;
|
|
private readonly byte[] _hash;
|
|
|
|
public string Name { get; }
|
|
public Stream Stream { get; private set; }
|
|
public bool Close()
|
|
{
|
|
if (Stream == null)
|
|
return false;
|
|
Stream = null;
|
|
return true;
|
|
}
|
|
|
|
public bool Open(FileAccess access)
|
|
{
|
|
if (Stream != null || access != FileAccess.Read)
|
|
return false;
|
|
Stream = new MemoryStream(_data, false);
|
|
return true;
|
|
}
|
|
|
|
public void LoadStateBinary(BinaryReader br)
|
|
{
|
|
if (!br.ReadBytes(_hash.Length).SequenceEqual(_hash))
|
|
throw new InvalidOperationException("Savestate internal firmware mismatch");
|
|
var pos = br.ReadInt64();
|
|
if (pos == -1)
|
|
{
|
|
Stream = null;
|
|
}
|
|
else
|
|
{
|
|
if (Stream == null)
|
|
Open(FileAccess.Read);
|
|
Stream.Position = pos;
|
|
}
|
|
}
|
|
|
|
public void SaveStateBinary(BinaryWriter bw)
|
|
{
|
|
bw.Write(_hash);
|
|
bw.Write(Stream != null ? Stream.Position : -1);
|
|
}
|
|
|
|
public ReadonlyFirmware(byte[] data, string name)
|
|
{
|
|
_data = data;
|
|
_hash = WaterboxUtils.Hash(data);
|
|
Name = name;
|
|
}
|
|
}
|
|
|
|
private class TransientFile : IFileObject
|
|
{
|
|
private bool _inUse = false;
|
|
public string Name { get; }
|
|
public Stream Stream { get; }
|
|
public bool Close()
|
|
{
|
|
if (_inUse)
|
|
{
|
|
_inUse = false;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool Open(FileAccess access)
|
|
{
|
|
if (_inUse)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// TODO: if access != RW, the resultant handle lets you do those all anyway
|
|
_inUse = true;
|
|
Stream.Position = 0;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public void LoadStateBinary(BinaryReader br)
|
|
{
|
|
throw new InvalidOperationException("Internal savestate error!");
|
|
}
|
|
|
|
public void SaveStateBinary(BinaryWriter bw)
|
|
{
|
|
throw new InvalidOperationException("Transient files cannot be savestated!");
|
|
}
|
|
|
|
public TransientFile(byte[] data, string name)
|
|
{
|
|
Stream = new MemoryStream();
|
|
Name = name;
|
|
if (data != null)
|
|
{
|
|
Stream.Write(data, 0, data.Length);
|
|
Stream.Position = 0;
|
|
}
|
|
}
|
|
|
|
public byte[] GetContents()
|
|
{
|
|
if (_inUse)
|
|
throw new InvalidOperationException();
|
|
return ((MemoryStream)Stream).ToArray();
|
|
}
|
|
}
|
|
|
|
private readonly List<IFileObject> _openFiles = new List<IFileObject>();
|
|
private readonly Dictionary<string, IFileObject> _availableFiles = new Dictionary<string, IFileObject>();
|
|
|
|
private readonly PeRunner _parent;
|
|
public Syscalls(PeRunner parent)
|
|
{
|
|
_parent = parent;
|
|
var stdin = new SpecialFile(Stream.Null, "___stdin");
|
|
var stdout = new SpecialFile(Console.OpenStandardOutput(), "___stdout");
|
|
var stderr = new SpecialFile(Console.OpenStandardError(), "___stderr");
|
|
|
|
_openFiles = new List<IFileObject>
|
|
{
|
|
stdin,
|
|
stdout,
|
|
stderr
|
|
};
|
|
_availableFiles = new Dictionary<string, IFileObject>
|
|
{
|
|
[stdin.Name] = stdin,
|
|
[stdout.Name] = stdout,
|
|
[stderr.Name] = stderr
|
|
};
|
|
}
|
|
|
|
private IntPtr _pthreadSelf;
|
|
|
|
public void Init()
|
|
{
|
|
// as the inits are done in a defined order with a defined memory map,
|
|
// we don't need to savestate _pthreadSelf, only its contents
|
|
_pthreadSelf = Z.US(_parent._plainheap.Allocate(512, 1));
|
|
}
|
|
|
|
private Stream StreamForFd(int fd)
|
|
{
|
|
if (fd >= 0 && fd < _openFiles.Count)
|
|
return _openFiles[fd].Stream;
|
|
else
|
|
return null;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "log_output")]
|
|
public void DebugPuts(IntPtr s)
|
|
{
|
|
Console.WriteLine("_psx_log_output:" + Marshal.PtrToStringAnsi(s));
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n12")]
|
|
public UIntPtr Brk(UIntPtr _p)
|
|
{
|
|
var heap = _parent._heap;
|
|
|
|
var start = heap.Memory.Start;
|
|
var end = start + heap.Used;
|
|
var max = heap.Memory.End;
|
|
|
|
var p = (ulong)_p;
|
|
|
|
if (p < start || p > max)
|
|
{
|
|
// failure: return current break
|
|
return Z.UU(end);
|
|
}
|
|
else if (p > end)
|
|
{
|
|
// increase size of heap
|
|
heap.Allocate(p - end, 1);
|
|
return Z.UU(p);
|
|
}
|
|
else if (p < end)
|
|
{
|
|
throw new InvalidOperationException("We don't support shrinking heaps");
|
|
}
|
|
else
|
|
{
|
|
// no change
|
|
return Z.UU(end);
|
|
}
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n16")]
|
|
public int IoCtl(int fd, ulong req)
|
|
{
|
|
return 0; // sure it worked, honest
|
|
}
|
|
|
|
public struct Iovec
|
|
{
|
|
public IntPtr Base;
|
|
public ulong Length;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n0")]
|
|
public long Read(int fd, IntPtr buff, ulong count)
|
|
{
|
|
var s = StreamForFd(fd);
|
|
if (s == null || !s.CanRead)
|
|
return -1;
|
|
var tmp = new byte[count];
|
|
var ret = s.Read(tmp, 0, (int)count);
|
|
Marshal.Copy(tmp, 0, buff, ret);
|
|
return ret;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n1")]
|
|
public long Write(int fd, IntPtr buff, ulong count)
|
|
{
|
|
var s = StreamForFd(fd);
|
|
if (s == null || !s.CanWrite)
|
|
return -1;
|
|
var tmp = new byte[count];
|
|
Marshal.Copy(buff, tmp, 0, (int)count);
|
|
s.Write(tmp, 0, tmp.Length);
|
|
return (long)count;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n19")]
|
|
public unsafe long Readv(int fd, Iovec* iov, int iovcnt)
|
|
{
|
|
long ret = 0;
|
|
for (int i = 0; i < iovcnt; i++)
|
|
{
|
|
var len = Read(fd, iov[i].Base, iov[i].Length);
|
|
if (len < 0)
|
|
return len;
|
|
ret += len;
|
|
if (len != (long)iov[i].Length)
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n20")]
|
|
public unsafe long Writev(int fd, Iovec* iov, int iovcnt)
|
|
{
|
|
long ret = 0;
|
|
for (int i = 0; i < iovcnt; i++)
|
|
{
|
|
if (iov[i].Base != IntPtr.Zero)
|
|
ret += Write(fd, iov[i].Base, iov[i].Length);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n2")]
|
|
public int Open(string path, int flags, int mode)
|
|
{
|
|
IFileObject o;
|
|
if (!_availableFiles.TryGetValue(path, out o))
|
|
return -1;
|
|
if (_openFiles.Contains(o))
|
|
return -1;
|
|
FileAccess access;
|
|
switch (flags & 3)
|
|
{
|
|
case 0:
|
|
access = FileAccess.Read;
|
|
break;
|
|
case 1:
|
|
access = FileAccess.Write;
|
|
break;
|
|
case 2:
|
|
access = FileAccess.ReadWrite;
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
if (!o.Open(access))
|
|
return -1;
|
|
int fd;
|
|
for (fd = 0; fd < _openFiles.Count; fd++)
|
|
if (_openFiles[fd] == null)
|
|
break;
|
|
if (fd == _openFiles.Count)
|
|
_openFiles.Add(o);
|
|
else
|
|
_openFiles[fd] = o;
|
|
return fd;
|
|
}
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n3")]
|
|
public int Close(int fd)
|
|
{
|
|
if (fd < 0 || fd >= _openFiles.Count)
|
|
return -1;
|
|
var o = _openFiles[fd];
|
|
if (o == null || !o.Close())
|
|
return -1;
|
|
_openFiles[fd] = null;
|
|
return 0;
|
|
}
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n8")]
|
|
public long Seek(int fd, long offset, int type)
|
|
{
|
|
var s = StreamForFd(fd);
|
|
if (s == null || !s.CanSeek)
|
|
return -1;
|
|
SeekOrigin o;
|
|
switch (type)
|
|
{
|
|
case 0:
|
|
o = SeekOrigin.Begin;
|
|
break;
|
|
case 1:
|
|
o = SeekOrigin.Current;
|
|
break;
|
|
case 2:
|
|
o = SeekOrigin.End;
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
return s.Seek(offset, o);
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n4")]
|
|
public int Stat(string path, IntPtr statbuf)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n5")]
|
|
public int Fstat(int fd, IntPtr statbuf)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
//[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
//private delegate int LibcStartMain(IntPtr main, int argc, IntPtr argv);
|
|
|
|
//bool _firstTime = true;
|
|
|
|
// aka __psx_init_frame (it's used elsewhere for thread setup)
|
|
// in midipix, this just sets up a SEH frame and then calls musl's start_main
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "start_main")]
|
|
public unsafe int StartMain(IntPtr main, int argc, IntPtr argv, IntPtr libc_start_main)
|
|
{
|
|
//var del = (LibcStartMain)CallingConventionAdapters.Waterbox.GetDelegateForFunctionPointer(libc_start_main, typeof(LibcStartMain));
|
|
// this will init, and then call user main, and then call exit()
|
|
//del(main, argc, argv);
|
|
//int* foobar = stackalloc int[128];
|
|
|
|
|
|
// if we return from this, psx will then halt, so break out
|
|
//if (_firstTime)
|
|
//{
|
|
//_firstTime = false;
|
|
throw new InvalidOperationException("This shouldn't be called");
|
|
//}
|
|
//else
|
|
//{
|
|
// return 0;
|
|
//}
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_self")]
|
|
public IntPtr PthreadSelf()
|
|
{
|
|
return _pthreadSelf;
|
|
}
|
|
|
|
/*[BizExport(CallingConvention.Cdecl, EntryPoint = "convert_thread")]
|
|
public void ConvertThread()
|
|
{
|
|
|
|
}*/
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n218")]
|
|
public long SetTidAddress(IntPtr address)
|
|
{
|
|
return 8675309;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public class TimeSpec
|
|
{
|
|
public long Seconds;
|
|
public long NanoSeconds;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n228")]
|
|
public int SysClockGetTime(int which, [In, Out] TimeSpec time)
|
|
{
|
|
time.Seconds = 1495889068;
|
|
time.NanoSeconds = 0;
|
|
return 0;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n9")]
|
|
public IntPtr MMap(IntPtr address, UIntPtr size, int prot, int flags, int fd, IntPtr offs)
|
|
{
|
|
if (address != IntPtr.Zero)
|
|
return Z.SS(-1);
|
|
MemoryBlock.Protection mprot;
|
|
switch (prot)
|
|
{
|
|
case 0: mprot = MemoryBlock.Protection.None; break;
|
|
default:
|
|
case 6: // W^X
|
|
case 7: // W^X
|
|
case 4: // exec only????
|
|
case 2: return Z.SS(-1); // write only????
|
|
case 3: mprot = MemoryBlock.Protection.RW; break;
|
|
case 1: mprot = MemoryBlock.Protection.R; break;
|
|
case 5: mprot = MemoryBlock.Protection.RX; break;
|
|
}
|
|
if ((flags & 0x20) == 0)
|
|
{
|
|
// MAP_ANONYMOUS is required
|
|
return Z.SS(-1);
|
|
}
|
|
if ((flags & 0xf00) != 0)
|
|
{
|
|
// various unsupported flags
|
|
return Z.SS(-1);
|
|
}
|
|
|
|
var ret = _parent._mmapheap.Map((ulong)size, mprot);
|
|
return ret == 0 ? Z.SS(-1) : Z.US(ret);
|
|
}
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n25")]
|
|
public IntPtr MRemap(UIntPtr oldAddress, UIntPtr oldSize,
|
|
UIntPtr newSize, int flags)
|
|
{
|
|
if ((flags & 2) != 0)
|
|
{
|
|
// don't support MREMAP_FIXED
|
|
return Z.SS(-1);
|
|
}
|
|
var ret = _parent._mmapheap.Remap((ulong)oldAddress, (ulong)oldSize, (ulong)newSize,
|
|
(flags & 1) != 0);
|
|
return ret == 0 ? Z.SS(-1) : Z.US(ret);
|
|
}
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n11")]
|
|
public int MUnmap(UIntPtr address, UIntPtr size)
|
|
{
|
|
return _parent._mmapheap.Unmap((ulong)address, (ulong)size) ? 0 : -1;
|
|
}
|
|
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "n10")]
|
|
public int MProtect(UIntPtr address, UIntPtr size, int prot)
|
|
{
|
|
MemoryBlock.Protection mprot;
|
|
switch (prot)
|
|
{
|
|
case 0: mprot = MemoryBlock.Protection.None; break;
|
|
default:
|
|
case 6: // W^X
|
|
case 7: // W^X
|
|
case 4: // exec only????
|
|
case 2: return -1; // write only????
|
|
case 3: mprot = MemoryBlock.Protection.RW; break;
|
|
case 1: mprot = MemoryBlock.Protection.R; break;
|
|
case 5: mprot = MemoryBlock.Protection.RX; break;
|
|
}
|
|
return _parent._mmapheap.Protect((ulong)address, (ulong)size, mprot) ? 0 : -1;
|
|
}
|
|
|
|
public void SaveStateBinary(BinaryWriter bw)
|
|
{
|
|
bw.Write(_availableFiles.Count);
|
|
foreach (var f in _availableFiles.Values.OrderBy(f => f.Name))
|
|
{
|
|
bw.Write(f.Name);
|
|
f.SaveStateBinary(bw);
|
|
}
|
|
bw.Write(_openFiles.Count);
|
|
foreach (var f in _openFiles)
|
|
{
|
|
bw.Write(f != null);
|
|
if (f != null)
|
|
bw.Write(f.Name);
|
|
}
|
|
}
|
|
|
|
public void LoadStateBinary(BinaryReader br)
|
|
{
|
|
if (_availableFiles.Count != br.ReadInt32())
|
|
throw new InvalidOperationException("Internal savestate error: Filelist change");
|
|
foreach (var f in _availableFiles.Values.OrderBy(f => f.Name))
|
|
{
|
|
if (br.ReadString() != f.Name)
|
|
throw new InvalidOperationException("Internal savestate error: Filelist change");
|
|
f.LoadStateBinary(br);
|
|
}
|
|
var c = br.ReadInt32();
|
|
_openFiles.Clear();
|
|
for (int i = 0; i < c; i++)
|
|
{
|
|
_openFiles.Add(br.ReadBoolean() ? _availableFiles[br.ReadString()] : null);
|
|
}
|
|
}
|
|
|
|
|
|
private T RemoveFileInternal<T>(string name)
|
|
where T : IFileObject
|
|
{
|
|
IFileObject o;
|
|
if (!_availableFiles.TryGetValue(name, out o))
|
|
throw new InvalidOperationException("File was never registered!");
|
|
if (o.GetType() != typeof(T))
|
|
throw new InvalidOperationException("Object was not a the right kind of file");
|
|
if (_openFiles.Contains(o))
|
|
throw new InvalidOperationException("Core never closed the file!");
|
|
_availableFiles.Remove(name);
|
|
return (T)o;
|
|
}
|
|
|
|
public void AddReadonlyFile(byte[] data, string name)
|
|
{
|
|
_availableFiles.Add(name, new ReadonlyFirmware(data, name));
|
|
}
|
|
|
|
public void RemoveReadonlyFile(string name)
|
|
{
|
|
RemoveFileInternal<ReadonlyFirmware>(name);
|
|
}
|
|
|
|
public void AddTransientFile(byte[] data, string name)
|
|
{
|
|
_availableFiles.Add(name, new TransientFile(data, name));
|
|
}
|
|
public byte[] RemoveTransientFile(string name)
|
|
{
|
|
return RemoveFileInternal<TransientFile>(name).GetContents();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// libc.so functions that we want to redirect
|
|
/// </summary>
|
|
private class LibcPatch
|
|
{
|
|
private readonly PeRunner _parent;
|
|
public LibcPatch(PeRunner parent)
|
|
{
|
|
_parent = parent;
|
|
}
|
|
|
|
|
|
private bool _didOnce = false;
|
|
private readonly Dictionary<uint, IntPtr> _specificKeys = new Dictionary<uint, IntPtr>();
|
|
private uint _nextSpecificKey = 401;
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
public delegate void PthreadCallback();
|
|
|
|
// pthread stuff:
|
|
// since we don't allow multiple threads (for now), this is all pretty simple
|
|
/*
|
|
// int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_key_create")]
|
|
public int PthreadKeyCreate(ref uint key, PthreadCallback destructor)
|
|
{
|
|
key = _nextSpecificKey++;
|
|
_specificKeys.Add(key, IntPtr.Zero);
|
|
return 0;
|
|
}
|
|
// int pthread_key_delete(pthread_key_t key);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_key_delete")]
|
|
public int PthreadKeyDelete(uint key)
|
|
{
|
|
_specificKeys.Remove(key);
|
|
return 0;
|
|
}
|
|
// int pthread_setspecific(pthread_key_t key, const void *value);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_setspecific")]
|
|
public int PthreadSetSpecific(uint key, IntPtr value)
|
|
{
|
|
_specificKeys[key] = value;
|
|
return 0;
|
|
}
|
|
// void *pthread_getspecific(pthread_key_t key);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_getspecific")]
|
|
public IntPtr PthreadGetSpecific(uint key)
|
|
{
|
|
IntPtr ret;
|
|
_specificKeys.TryGetValue(key, out ret);
|
|
return ret;
|
|
}
|
|
|
|
// int pthread_once(pthread_once_t* once_control, void (*init_routine)(void));
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_once")]
|
|
public int PthreadOnce(IntPtr control, PthreadCallback init)
|
|
{
|
|
if (!_didOnce)
|
|
{
|
|
System.Diagnostics.Debugger.Break();
|
|
_didOnce = true;
|
|
init();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t* attr);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_mutex_init")]
|
|
public int PthreadMutexInit(IntPtr mutex, IntPtr attr) { return 0; }
|
|
// int pthread_mutex_destroy(pthread_mutex_t* mutex);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_mutex_destroy")]
|
|
public int PthreadMutexDestroy(IntPtr mutex) { return 0; }
|
|
|
|
// int pthread_mutex_lock(pthread_mutex_t* mutex);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_mutex_lock")]
|
|
public int PthreadMutexLock(IntPtr mutex) { return 0; }
|
|
// int pthread_mutex_trylock(pthread_mutex_t* mutex);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_mutex_trylock")]
|
|
public int PthreadMutexTryLock(IntPtr mutex) { return 0; }
|
|
// int pthread_mutex_unlock(pthread_mutex_t* mutex);
|
|
[BizExport(CallingConvention.Cdecl, EntryPoint = "pthread_mutex_unlock")]
|
|
public int PthreadMutexUnlock(IntPtr mutex) { return 0; }*/
|
|
}
|
|
|
|
/// <summary>
|
|
/// usual starting point for the executable
|
|
/// </summary>
|
|
public const ulong CanonicalStart = 0x0000036f00000000;
|
|
|
|
public const ulong AlternateStart = 0x0000036e00000000;
|
|
|
|
/// <summary>
|
|
/// the next place where we can put a module or heap
|
|
/// </summary>
|
|
private ulong _nextStart = CanonicalStart;
|
|
|
|
/// <summary>
|
|
/// increment _nextStart after adding a module
|
|
/// </summary>
|
|
private void ComputeNextStart(ulong size)
|
|
{
|
|
_nextStart += size;
|
|
// align to 1MB, then increment 16MB
|
|
_nextStart = ((_nextStart - 1) | 0xfffff) + 0x1000001;
|
|
}
|
|
|
|
/// <summary>
|
|
/// standard malloc() heap
|
|
/// </summary>
|
|
private Heap _heap;
|
|
|
|
/// <summary>
|
|
/// sealed heap (writable only during init)
|
|
/// </summary>
|
|
private Heap _sealedheap;
|
|
|
|
/// <summary>
|
|
/// invisible heap (not savestated, use with care)
|
|
/// </summary>
|
|
private Heap _invisibleheap;
|
|
|
|
/// <summary>
|
|
/// extra savestated heap
|
|
/// </summary>
|
|
private Heap _plainheap;
|
|
|
|
/// <summary>
|
|
/// memory map emulation
|
|
/// </summary>
|
|
private MapHeap _mmapheap;
|
|
|
|
/// <summary>
|
|
/// all loaded PE files
|
|
/// </summary>
|
|
private readonly List<PeWrapper> _modules = new List<PeWrapper>();
|
|
|
|
/// <summary>
|
|
/// all loaded heaps
|
|
/// </summary>
|
|
private readonly List<Heap> _heaps = new List<Heap>();
|
|
|
|
/// <summary>
|
|
/// anything at all that needs to be disposed on finish
|
|
/// </summary>
|
|
private readonly List<IDisposable> _disposeList = new List<IDisposable>();
|
|
|
|
/// <summary>
|
|
/// anything at all that needs its state saved and loaded
|
|
/// </summary>
|
|
private readonly List<IBinaryStateable> _savestateComponents = new List<IBinaryStateable>();
|
|
|
|
/// <summary>
|
|
/// all of the exports, including real PeWrapper ones and fake ones
|
|
/// </summary>
|
|
private readonly Dictionary<string, IImportResolver> _exports = new Dictionary<string, IImportResolver>();
|
|
|
|
private Psx _psx;
|
|
private Emu _emu;
|
|
private Syscalls _syscalls;
|
|
private LibcPatch _libcpatch;
|
|
|
|
/// <summary>
|
|
/// timestamp of creation acts as a sort of "object id" in the savestate
|
|
/// </summary>
|
|
private readonly long _createstamp = WaterboxUtils.Timestamp();
|
|
|
|
private Heap CreateHeapHelper(uint sizeKB, string name, bool saveStated)
|
|
{
|
|
if (sizeKB != 0)
|
|
{
|
|
var heap = new Heap(_nextStart, sizeKB * 1024, name);
|
|
heap.Memory.Activate();
|
|
ComputeNextStart(sizeKB * 1024);
|
|
AddMemoryBlock(heap.Memory, name);
|
|
if (saveStated)
|
|
_savestateComponents.Add(heap);
|
|
_disposeList.Add(heap);
|
|
_heaps.Add(heap);
|
|
return heap;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
private delegate void LibcEntryRoutineD(IntPtr appMain, IntPtr psxInit, int options);
|
|
|
|
public PeRunner(PeRunnerOptions opt)
|
|
{
|
|
_nextStart = opt.StartAddress;
|
|
Initialize(_nextStart);
|
|
using (this.EnterExit())
|
|
{
|
|
// load any predefined exports
|
|
_psx = new Psx(this);
|
|
_exports.Add("libpsxscl.so", BizExvoker.GetExvoker(_psx, CallingConventionAdapters.Waterbox));
|
|
_emu = new Emu(this);
|
|
_exports.Add("libemuhost.so", BizExvoker.GetExvoker(_emu, CallingConventionAdapters.Waterbox));
|
|
_syscalls = new Syscalls(this);
|
|
_exports.Add("__syscalls", BizExvoker.GetExvoker(_syscalls, CallingConventionAdapters.Waterbox));
|
|
|
|
// load and connect all modules, starting with the executable
|
|
var todoModules = new Queue<string>();
|
|
todoModules.Enqueue(opt.Filename);
|
|
|
|
while (todoModules.Count > 0)
|
|
{
|
|
var moduleName = todoModules.Dequeue();
|
|
if (!_exports.ContainsKey(moduleName))
|
|
{
|
|
var path = Path.Combine(opt.Path, moduleName);
|
|
var gzpath = path + ".gz";
|
|
byte[] data;
|
|
if (File.Exists(gzpath))
|
|
{
|
|
using (var fs = new FileStream(gzpath, FileMode.Open, FileAccess.Read))
|
|
{
|
|
data = Util.DecompressGzipFile(fs);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
data = File.ReadAllBytes(path);
|
|
}
|
|
|
|
var module = new PeWrapper(moduleName, data, _nextStart);
|
|
ComputeNextStart(module.Size);
|
|
AddMemoryBlock(module.Memory, moduleName);
|
|
_savestateComponents.Add(module);
|
|
_disposeList.Add(module);
|
|
|
|
_exports.Add(moduleName, module);
|
|
_modules.Add(module);
|
|
foreach (var name in module.ImportsByModule.Keys)
|
|
{
|
|
todoModules.Enqueue(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
_libcpatch = new LibcPatch(this);
|
|
_exports["libc.so"] = new PatchImportResolver(_exports["libc.so"], BizExvoker.GetExvoker(_libcpatch, CallingConventionAdapters.Waterbox));
|
|
|
|
ConnectAllImports();
|
|
|
|
// load all heaps
|
|
_heap = CreateHeapHelper(opt.SbrkHeapSizeKB, "brk-heap", true);
|
|
_sealedheap = CreateHeapHelper(opt.SealedHeapSizeKB, "sealed-heap", true);
|
|
_invisibleheap = CreateHeapHelper(opt.InvisibleHeapSizeKB, "invisible-heap", false);
|
|
_plainheap = CreateHeapHelper(opt.PlainHeapSizeKB, "plain-heap", true);
|
|
|
|
if (opt.MmapHeapSizeKB != 0)
|
|
{
|
|
_mmapheap = new MapHeap(_nextStart, opt.MmapHeapSizeKB * 1024, "mmap-heap");
|
|
_mmapheap.Memory.Activate();
|
|
ComputeNextStart(opt.MmapHeapSizeKB * 1024);
|
|
AddMemoryBlock(_mmapheap.Memory, "mmap-heap");
|
|
_savestateComponents.Add(_mmapheap);
|
|
_disposeList.Add(_mmapheap);
|
|
}
|
|
|
|
_syscalls.Init();
|
|
|
|
Console.WriteLine("About to enter unmanaged code");
|
|
if (Win32Hacks.IsDebuggerReallyPresent() && !System.Diagnostics.Debugger.IsAttached)
|
|
{
|
|
// this means that GDB or another unconventional debugger is attached.
|
|
// if that's the case, and it's observing this core, it probably wants a break
|
|
System.Diagnostics.Debugger.Break();
|
|
}
|
|
|
|
// run unmanaged init code
|
|
var libcEnter = _exports["libc.so"].SafeResolve("__libc_entry_routine");
|
|
var psxInit = _exports["libpsxscl.so"].SafeResolve("__psx_init");
|
|
|
|
var del = (LibcEntryRoutineD)CallingConventionAdapters.Waterbox.GetDelegateForFunctionPointer(libcEnter, typeof(LibcEntryRoutineD));
|
|
// the current mmglue code doesn't use the main pointer at all, and this just returns
|
|
del(IntPtr.Zero, psxInit, 0);
|
|
|
|
foreach (var m in _modules)
|
|
{
|
|
m.RunGlobalCtors();
|
|
}
|
|
|
|
/*try
|
|
{
|
|
_modules[0].RunExeEntry();
|
|
//throw new InvalidOperationException("main() returned!");
|
|
}
|
|
catch //(EndOfMainException)
|
|
{
|
|
}
|
|
_modules[0].RunGlobalCtors();
|
|
foreach (var m in _modules.Skip(1))
|
|
{
|
|
if (!m.RunDllEntry())
|
|
throw new InvalidOperationException("DllMain() returned false");
|
|
m.RunGlobalCtors();
|
|
}*/
|
|
}
|
|
}
|
|
|
|
public IntPtr Resolve(string entryPoint)
|
|
{
|
|
// modules[0] is always the main module
|
|
return _modules[0].Resolve(entryPoint);
|
|
}
|
|
|
|
public void Seal()
|
|
{
|
|
using (this.EnterExit())
|
|
{
|
|
// if libco is used, the jmp_buf for the main cothread can have stack stuff in it.
|
|
// this isn't a problem, since we only savestate when the core is not running, and
|
|
// the next time it's run, that buf will be overridden again.
|
|
// but it breaks xor state verification, so when we seal, nuke it.
|
|
|
|
// this could be the responsibility of something else other than the PeRunner; I am not sure yet...
|
|
IImportResolver libco;
|
|
if (_exports.TryGetValue("libco.so", out libco))
|
|
{
|
|
Console.WriteLine("Calling co_clean()...");
|
|
CallingConventionAdapters.Waterbox.GetDelegateForFunctionPointer<Action>(libco.SafeResolve("co_clean"))();
|
|
}
|
|
|
|
_sealedheap.Seal();
|
|
foreach (var h in _heaps)
|
|
{
|
|
if (h != _invisibleheap && h != _sealedheap) // TODO: if we have more non-savestated heaps, refine this hack
|
|
h.Memory.SaveXorSnapshot();
|
|
}
|
|
foreach (var pe in _modules)
|
|
{
|
|
pe.SealImportsAndTakeXorSnapshot();
|
|
}
|
|
if (_mmapheap != null)
|
|
_mmapheap.Memory.SaveXorSnapshot();
|
|
}
|
|
}
|
|
|
|
private void ConnectAllImports()
|
|
{
|
|
foreach (var module in _modules)
|
|
{
|
|
foreach (var name in module.ImportsByModule.Keys)
|
|
{
|
|
module.ConnectImports(name, _exports[name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a file that will appear to the waterbox core's libc. the file will be read only.
|
|
/// All savestates must have the same file list, so either leave it up forever or remove it during init!
|
|
/// </summary>
|
|
/// <param name="data"></param>
|
|
/// <param name="name">the filename that the unmanaged core will access the file by</param>
|
|
public void AddReadonlyFile(byte[] data, string name)
|
|
{
|
|
_syscalls.AddReadonlyFile((byte[])data.Clone(), name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a file previously added by AddReadonlyFile. Frees the internal copy of the filedata, saving memory.
|
|
/// All savestates must have the same file list, so either leave it up forever or remove it during init!
|
|
/// </summary>
|
|
/// <param name="name"></param>
|
|
public void RemoveReadonlyFile(string name)
|
|
{
|
|
_syscalls.RemoveReadonlyFile(name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a transient file that will appear to the waterbox core's libc. The file will be readable
|
|
/// and writable. Any attempt to save state while the file is loaded will fail.
|
|
/// </summary>
|
|
public void AddTransientFile(byte[] data, string name)
|
|
{
|
|
_syscalls.AddTransientFile(data, name); // don't need to clone data, as it's used at init only
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a file previously added by AddTransientFile
|
|
/// </summary>
|
|
/// <returns>The state of the file when it was removed</returns>
|
|
public byte[] RemoveTransientFile(string name)
|
|
{
|
|
return _syscalls.RemoveTransientFile(name);
|
|
}
|
|
|
|
public void SaveStateBinary(BinaryWriter bw)
|
|
{
|
|
bw.Write(_createstamp);
|
|
bw.Write(_savestateComponents.Count);
|
|
using (this.EnterExit())
|
|
{
|
|
foreach (var c in _savestateComponents)
|
|
{
|
|
c.SaveStateBinary(bw);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void LoadStateBinary(BinaryReader br)
|
|
{
|
|
var differentCore = br.ReadInt64() != _createstamp; // true if a different core instance created the state
|
|
if (br.ReadInt32() != _savestateComponents.Count)
|
|
throw new InvalidOperationException("Internal savestate error");
|
|
using (this.EnterExit())
|
|
{
|
|
foreach (var c in _savestateComponents)
|
|
{
|
|
c.LoadStateBinary(br);
|
|
}
|
|
if (differentCore)
|
|
{
|
|
// if a different runtime instance than this one saved the state,
|
|
// Exvoker imports need to be reconnected
|
|
Console.WriteLine("Restoring PeRunner state from a different core...");
|
|
ConnectAllImports();
|
|
_psx.ReloadVtables();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
if (disposing)
|
|
{
|
|
foreach (var d in _disposeList)
|
|
d.Dispose();
|
|
_disposeList.Clear();
|
|
PurgeMemoryBlocks();
|
|
_modules.Clear();
|
|
_exports.Clear();
|
|
_heap = null;
|
|
_sealedheap = null;
|
|
_invisibleheap = null;
|
|
_plainheap = null;
|
|
_mmapheap = null;
|
|
}
|
|
}
|
|
}
|
|
}
|