Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alexbevi
GitHub Repository: alexbevi/BizHawk
Path: blob/master/BizHawk.Emulation.Cores/ElfRunner.cs
2 views
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using ELFSharp.ELF;
using ELFSharp.ELF.Sections;
using ELFSharp.ELF.Segments;
using System.Reflection;
using BizHawk.Common;
using System.Security.Cryptography;
using System.IO;
using System.Collections.Concurrent;
using System.Threading;

namespace BizHawk.Emulation.Cores
{
	public sealed class ElfRunner : IImportResolver, IDisposable, IMonitor
	{
		// TODO: a lot of things only work with our elves and aren't fully generalized

		private ELF<long> _elf;
		private byte[] _elfhash;

		/// <summary>
		/// executable is loaded here
		/// </summary>
		private MemoryBlock _base;
		/// <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>
		/// _base.Start, or 0 if we were relocated and so don't need to be swapped
		/// </summary>
		private ulong _lockkey;

		private long _loadoffset;
		private Dictionary<string, SymbolEntry<long>> _symdict;
		private List<SymbolEntry<long>> _symlist;

		/// <summary>
		/// everything to clean up at dispose time
		/// </summary>
		private List<IDisposable> _disposeList = new List<IDisposable>();

		/// <summary>
		/// everything to swap in for context switches
		/// </summary>
		private List<MemoryBlock> _memoryBlocks = new List<MemoryBlock>();

		private ulong GetHeapStart(ulong prevend)
		{
			// if relocatable, we won't have constant pointers, so put the heap anywhere
			// otherwise, put the heap at a canonical location aligned 1MB from the end of the elf, then incremented 16MB
			ulong heapstart = HasRelocations() ? 0 : ((prevend - 1) | 0xfffff) + 0x1000001;
			return heapstart;
		}

		public ElfRunner(string filename, long heapsize, long sealedheapsize, long invisibleheapsize)
		{
			using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read))
			{
				_elfhash = Hash(fs);
			}

			// todo: hack up this baby to take Streams
			_elf = ELFReader.Load<long>(filename);

			var loadsegs = _elf.Segments.Where(s => s.Type == SegmentType.Load);

			long orig_start = loadsegs.Min(s => s.Address);
			orig_start &= ~(Environment.SystemPageSize - 1);
			long orig_end = loadsegs.Max(s => s.Address + s.Size);
			if (HasRelocations())
			{
				_base = new MemoryBlock((ulong)(orig_end - orig_start));
				_loadoffset = (long)_base.Start - orig_start;
				_lockkey = 0;
			}
			else
			{
				_lockkey = (ulong)orig_start;
				_base = new MemoryBlock(_lockkey, (ulong)(orig_end - orig_start));
				_loadoffset = 0;
				Enter();
			}

			try
			{
				_disposeList.Add(_base);
				_memoryBlocks.Add(_base);
				_base.Activate();
				_base.Protect(_base.Start, _base.Size, MemoryBlock.Protection.RW);

				foreach (var seg in loadsegs)
				{
					var data = seg.GetContents();
					Marshal.Copy(data, 0, Z.SS(seg.Address + _loadoffset), data.Length);
				}
				RegisterSymbols();
				ProcessRelocations();

				_base.Protect(_base.Start, _base.Size, MemoryBlock.Protection.R);

				foreach (var sec in _elf.Sections.Where(s => (s.Flags & SectionFlags.Allocatable) != 0))
				{
					if ((sec.Flags & SectionFlags.Executable) != 0)
						_base.Protect((ulong)(sec.LoadAddress + _loadoffset), (ulong)sec.Size, MemoryBlock.Protection.RX);
					else if ((sec.Flags & SectionFlags.Writable) != 0)
						_base.Protect((ulong)(sec.LoadAddress + _loadoffset), (ulong)sec.Size, MemoryBlock.Protection.RW);
				}

				ulong end = _base.End;

				if (heapsize > 0)
				{
					_heap = new Heap(GetHeapStart(end), (ulong)heapsize, "sbrk-heap");
					_heap.Memory.Activate();
					end = _heap.Memory.End;
					_disposeList.Add(_heap);
					_memoryBlocks.Add(_heap.Memory);
				}

				if (sealedheapsize > 0)
				{
					_sealedheap = new Heap(GetHeapStart(end), (ulong)sealedheapsize, "sealed-heap");
					_sealedheap.Memory.Activate();
					end = _sealedheap.Memory.End;
					_disposeList.Add(_sealedheap);
					_memoryBlocks.Add(_sealedheap.Memory);
				}

				if (invisibleheapsize > 0)
				{
					_invisibleheap = new Heap(GetHeapStart(end), (ulong)invisibleheapsize, "invisible-heap");
					_invisibleheap.Memory.Activate();
					end = _invisibleheap.Memory.End;
					_disposeList.Add(_invisibleheap);
					_memoryBlocks.Add(_invisibleheap.Memory);
				}

				ConnectAllClibPatches();
				Console.WriteLine("Loaded {0}@{1:X16}", filename, _base.Start);
				foreach (var sec in _elf.Sections.Where(s => s.LoadAddress != 0))
				{
					Console.WriteLine("  {0}@{1:X16}, size {2}", sec.Name.PadLeft(20), sec.LoadAddress + _loadoffset, sec.Size.ToString().PadLeft(12));
				}

				PrintTopSavableSymbols();
			}
			catch
			{
				Dispose();
				throw;
			}
			finally
			{
				Exit();
			}
		}

		private void PrintTopSavableSymbols()
		{
			Console.WriteLine("Top savestate symbols:");
			foreach (var text in _symlist
				.Where(s => s.PointedSection != null && (s.PointedSection.Flags & SectionFlags.Writable) != 0)
				.OrderByDescending(s => s.Size)
				.Take(30)
				.Select(s => string.Format("{0} size {1}", s.Name, s.Size)))
			{
				Console.WriteLine(text);
			}
		}

		private class Elf32_Rel
		{
			public long Address;
			public byte Type;
			public int SymbolIdx;
			public long Addend;

			public Elf32_Rel(byte[] data, int start, int len)
			{
				if (len == 8 || len == 12)
				{
					Address = BitConverter.ToInt32(data, start);
					Type = data[start + 4];
					SymbolIdx = (int)(BitConverter.ToUInt32(data, start + 4) >> 8);
					Addend = data.Length == 12 ? BitConverter.ToInt32(data, start + 8) : 0;
				}
				else
				{
					throw new InvalidOperationException();
				}
			}
		}

		private bool HasRelocations()
		{
			return _elf.Sections.Any(s => s.Name.StartsWith(".rel"));
		}

		// elfsharp does not read relocation tables, so there
		private void ProcessRelocations()
		{
			// todo: amd64
			foreach (var rel in _elf.Sections.Where(s => s.Name.StartsWith(".rel")))
			{
				byte[] data = rel.GetContents();
				var symbols = Enumerable.Range(0, data.Length / 8)
					.Select(i => new Elf32_Rel(data, i * 8, 8));
				foreach (var symbol in symbols)
				{
					ApplyRelocation(symbol);
				}
			}
		}
		private void ApplyRelocation(Elf32_Rel rel)
		{
			// http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf
			// this is probably mostly wrong

			long val = 0;
			long A = rel.Addend;
			// since all symbols were moved by the same amount, just add _loadoffset here
			long S = _symlist[rel.SymbolIdx].Value + _loadoffset;
			long B = _loadoffset;
			switch (rel.Type)
			{
				case 0: val = 0; break;
				case 1: val = S + A; break;
				case 2: throw new NotImplementedException();
				case 3: throw new NotImplementedException();
				case 4: throw new NotImplementedException();
				case 5: val = 0; break;
				case 6: val = S; break;
				case 7: val = S; break;
				case 8: val = B + A; break;
				case 9: throw new NotImplementedException();
				case 10: throw new NotImplementedException();
				default: throw new InvalidOperationException();
			}
			byte[] tmp = new byte[4];
			Marshal.Copy((IntPtr)(rel.Address + _loadoffset), tmp, 0, 4);
			long currentVal = BitConverter.ToUInt32(tmp, 0);
			tmp = BitConverter.GetBytes((uint)(currentVal + val));
			Marshal.Copy(tmp, 0, (IntPtr)(rel.Address + _loadoffset), 4);
		}

		private void RegisterSymbols()
		{
			var symbols = ((ISymbolTable)_elf.GetSection(".symtab"))
			.Entries
			.Cast<SymbolEntry<long>>();

			// when there are duplicate names, don't register either in the dictionary
			_symdict = symbols
			.GroupBy(e => e.Name)
			.Where(g => g.Count() == 1)
			.ToDictionary(g => g.Key, g => g.First());

			_symlist = symbols.ToList();
		}

		public void Dispose()
		{
			// we don't need to activate to dispose
			Dispose(true);
			//GC.SuppressFinalize(this);
		}

		public void Seal()
		{
			Enter();
			try
			{
				_sealedheap.Seal();
			}
			finally
			{
				Exit();
			}
		}

		//~ElfRunner()
		//{
		//	Dispose(false);
		//}

		private void Dispose(bool disposing)
		{
			if (disposing)
			{
				foreach (var d in _disposeList)
					d.Dispose();
				_disposeList.Clear();
				_memoryBlocks.Clear();
				_base = null;
				_heap = null;
				_sealedheap = null;
				_invisibleheap = null;
			}
		}


		#region clib monkeypatches

		// our clib expects a few function pointers to be defined for it

		/// <summary>
		/// abort() / other abnormal situation
		/// </summary>
		/// <param name="status">desired exit code</param>
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		private delegate void Trap_D();

		/// <summary>
		/// expand heap
		/// </summary>
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		private delegate IntPtr Sbrk_D(UIntPtr n);

		/// <summary>
		/// output a string
		/// </summary>
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		private delegate void DebugPuts_D(string s);

		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		private delegate IntPtr SbrkSealed_D(UIntPtr n);

		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		private delegate IntPtr SbrkInvisible_D(UIntPtr n);

		[CLibPatch("_ecl_trap")]
		private void Trap()
		{
			throw new InvalidOperationException("Waterbox code trapped!");
		}

		[CLibPatch("_ecl_sbrk")]
		private IntPtr Sbrk(UIntPtr n)
		{
			return Z.US(_heap.Allocate((ulong)n, 1));
		}

		[CLibPatch("_ecl_debug_puts")]
		private void DebugPuts(string s)
		{
			Console.WriteLine("Waterbox debug puts: {0}", s);
		}

		[CLibPatch("_ecl_sbrk_sealed")]
		private IntPtr SbrkSealed(UIntPtr n)
		{
			return Z.US(_sealedheap.Allocate((ulong)n, 16));
		}

		[CLibPatch("_ecl_sbrk_invisible")]
		private IntPtr SbrkInvisible(UIntPtr n)
		{
			return Z.US(_invisibleheap.Allocate((ulong)n, 16));
		}

		/// <summary>
		/// list of delegates that need to not be GCed
		/// </summary>
		private List<Delegate> _delegates = new List<Delegate>();

		private void ConnectAllClibPatches()
		{
			_delegates.Clear(); // in case we're reconnecting

			var methods = GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
				.Where(mi => mi.GetCustomAttributes(typeof(CLibPatchAttribute), false).Length > 0);
			foreach (var mi in methods)
			{
				var delegateType = GetType().GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)
					.Single(t => t.Name == mi.Name + "_D");
				var del = Delegate.CreateDelegate(delegateType, this, mi);
				IntPtr ptr = Marshal.GetFunctionPointerForDelegate(del);
				_delegates.Add(del);
				var sym = _symdict[((CLibPatchAttribute)mi.GetCustomAttributes(typeof(CLibPatchAttribute), false)[0]).NativeName];
				if (sym.Size != IntPtr.Size)
					throw new InvalidOperationException("Unexpected function pointer size patching clib!");
				IntPtr dest = Z.SS(sym.Value + _loadoffset);
				Marshal.Copy(new[] { ptr }, 0, dest, 1);
			}
		}

		[AttributeUsage(AttributeTargets.Method)]
		private class CLibPatchAttribute : Attribute
		{
			public string NativeName { get; private set; }
			public CLibPatchAttribute(string nativeName)
			{
				NativeName = nativeName;
			}
		}

		#endregion

		public IntPtr Resolve(string entryPoint)
		{
			SymbolEntry<long> sym;
			if (_symdict.TryGetValue(entryPoint, out sym))
			{
				return Z.SS(sym.Value + _loadoffset);
			}
			else
			{
				return IntPtr.Zero;
			}
		}

		/// <summary>
		/// true if the IMonitor should be used for native calls
		/// </summary>
		public bool ShouldMonitor { get { return _lockkey != 0; } }

		// any ElfRunner is assumed to conflict with any other ElfRunner at the same base address,
		// but not any other starting address.  so don't put them too close together!

		private class LockInfo
		{
			public object Sync;
			public ElfRunner Loaded;
		}

		private static readonly ConcurrentDictionary<ulong, LockInfo> LockInfos = new ConcurrentDictionary<ulong, LockInfo>();

		static ElfRunner()
		{
			LockInfos.GetOrAdd(0, new LockInfo()); // any errant attempt to lock when ShouldMonitor == false will result in NRE
		}

		/// <summary>
		/// acquire lock and swap this into memory
		/// </summary>
		public void Enter()
		{
			var li = LockInfos.GetOrAdd(_lockkey, new LockInfo { Sync = new object() });
			Monitor.Enter(li.Sync);
			if (li.Loaded != this)
			{
				if (li.Loaded != null)
					li.Loaded.DeactivateInternal();
				li.Loaded = null;
				ActivateInternal();
				li.Loaded = this;
			}
		}

		/// <summary>
		/// release lock
		/// </summary>
		public void Exit()
		{
			var li = LockInfos.GetOrAdd(_lockkey, new LockInfo { Sync = new object() });
			Monitor.Exit(li.Sync);
		}

		private void DeactivateInternal()
		{
			Console.WriteLine("ElfRunner DeactivateInternal {0}", GetHashCode());
			foreach (var m in _memoryBlocks)
				m.Deactivate();
		}

		private void ActivateInternal()
		{
			Console.WriteLine("ElfRunner ActivateInternal {0}", GetHashCode());
			foreach (var m in _memoryBlocks)
				m.Activate();
		}

		#region state

		const ulong MAGIC = 0xb00b1e5b00b1e569;

		public void SaveStateBinary(BinaryWriter bw)
		{
			Enter();
			try
			{
				bw.Write(MAGIC);
				bw.Write(_elfhash);
				bw.Write(_loadoffset);
				foreach (var sec in _elf.Sections.Where(s => (s.Flags & SectionFlags.Writable) != 0))
				{
					var ms = _base.GetStream((ulong)(sec.LoadAddress + _loadoffset), (ulong)sec.Size, false);
					bw.Write(sec.Size);
					ms.CopyTo(bw.BaseStream);
				}

				if (_heap != null) _heap.SaveStateBinary(bw);
				if (_sealedheap != null) _sealedheap.SaveStateBinary(bw);
				bw.Write(MAGIC);
			}
			finally
			{
				Exit();
			}
		}

		public void LoadStateBinary(BinaryReader br)
		{
			Enter();
			try
			{
				if (br.ReadUInt64() != MAGIC)
					throw new InvalidOperationException("Magic not magic enough!");
				if (!br.ReadBytes(_elfhash.Length).SequenceEqual(_elfhash))
					throw new InvalidOperationException("Elf changed disguise!");
				if (br.ReadInt64() != _loadoffset)
					throw new InvalidOperationException("Trickys elves moved on you!");

				foreach (var sec in _elf.Sections.Where(s => (s.Flags & SectionFlags.Writable) != 0))
				{
					var len = br.ReadInt64();
					if (sec.Size != len)
						throw new InvalidOperationException("Unexpected section size for " + sec.Name);
					var ms = _base.GetStream((ulong)(sec.LoadAddress + _loadoffset), (ulong)sec.Size, true);
					CopySome(br.BaseStream, ms, len);
				}

				if (_heap != null) _heap.LoadStateBinary(br);
				if (_sealedheap != null) _sealedheap.LoadStateBinary(br);
				if (br.ReadUInt64() != MAGIC)
					throw new InvalidOperationException("Magic not magic enough!");

				// the syscall trampolines were overwritten in loadstate (they're in .bss), and if we're cross-session,
				// are no longer valid.  cores must similiarly resend any external pointers they gave the core.
				ConnectAllClibPatches();
			}
			finally
			{
				Exit();
			}
		}

		#endregion

		#region utils

		private static void CopySome(Stream src, Stream dst, long len)
		{
			var buff = new byte[4096];
			while (len > 0)
			{
				int r = src.Read(buff, 0, (int)Math.Min(len, 4096));
				dst.Write(buff, 0, r);
				len -= r;
			}
		}

		private static byte[] Hash(byte[] data)
		{
			using (var h = SHA1.Create())
			{
				return h.ComputeHash(data);
			}
		}

		private static byte[] Hash(Stream s)
		{
			using (var h = SHA1.Create())
			{
				return h.ComputeHash(s);
			}
		}

		private byte[] HashSection(ulong ptr, ulong len)
		{
			using (var h = SHA1.Create())
			{
				var ms = _base.GetStream(ptr, len, false);
				return h.ComputeHash(ms);
			}
		}

		/// <summary>
		/// a simple grow-only fixed max size heap
		/// </summary>
		private sealed class Heap : IDisposable
		{
			public MemoryBlock Memory { get; private set; }
			/// <summary>
			/// name, used in identifying errors
			/// </summary>
			public string Name { get; private set; }
			/// <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 = new MemoryBlock(start, size);
				Used = 0;
				Name = name;
			}

			private void EnsureAlignment(int align)
			{
				if (align > 1)
				{
					ulong newused = ((Used - 1) | (ulong)(align - 1)) + 1;
					if (newused > Memory.Size)
					{
						throw new InvalidOperationException(string.Format("Failed to meet alignment {0} on heap {1}", align, Name));
					}
					Used = newused;
				}
			}

			public ulong Allocate(ulong size, int align)
			{
				if (Sealed)
					throw new InvalidOperationException(string.Format("Attempt made to allocate from sealed heap {0}", Name));

				EnsureAlignment(align);

				ulong newused = Used + size;
				if (newused > Memory.Size)
				{
					throw new InvalidOperationException(string.Format("Failed to allocate {0} bytes from heap {1}", size, Name));
				}
				ulong ret = Memory.Start + Used;
				Memory.Protect(ret, newused - Used, MemoryBlock.Protection.RW);
				Used = newused;
				Console.WriteLine("Allocated {0} bytes on {1}", size, Name);
				return ret;
			}

			public void Seal()
			{
				if (!Sealed)
				{
					Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.R);
					_hash = Hash(Memory.GetStream(Memory.Start, Used, false));
					Sealed = true;
				}
				else
				{
					throw new InvalidOperationException(string.Format("Attempt to reseal heap {0}", Name));
				}
			}

			public void SaveStateBinary(BinaryWriter bw)
			{
				bw.Write(Name);
				bw.Write(Used);
				if (!Sealed)
				{
					var ms = Memory.GetStream(Memory.Start, Used, false);
					ms.CopyTo(bw.BaseStream);
				}
				else
				{
					bw.Write(_hash);
				}
			}

			public void LoadStateBinary(BinaryReader br)
			{
				var name = br.ReadString();
				if (name != Name)
					throw new InvalidOperationException(string.Format("Name did not match for heap {0}", Name));
				var used = br.ReadUInt64();
				if (used > Memory.Size)
					throw new InvalidOperationException(string.Format("Heap {0} used {1} larger than available {2}", Name, used, Memory.Size));
				if (!Sealed)
				{
					Memory.Protect(Memory.Start, Memory.Size, MemoryBlock.Protection.None);
					Memory.Protect(Memory.Start, used, MemoryBlock.Protection.RW);
					var ms = Memory.GetStream(Memory.Start, used, true);
					CopySome(br.BaseStream, ms, (long)used);
					Used = used;
				}
				else
				{
					var hash = br.ReadBytes(_hash.Length);
					if (!hash.SequenceEqual(_hash))
					{
						throw new InvalidOperationException(string.Format("Hash did not match for heap {0}.  Is this the same rom?"));
					}
				}
			}

			public void Dispose()
			{
				if (Memory != null)
				{
					Memory.Dispose();
					Memory = null;
				}
			}
		}

		#endregion
	}
}