Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emulator Threads #136

Merged
merged 3 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/Core/Echo/Memory/VirtualMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,18 @@ public AddressRange AddressRange
/// <exception cref="ArgumentException">Occurs when the address was already in use.</exception>
public void Map(long address, IMemorySpace space)
{
if (space.AddressRange.Length == 0)
throw new ArgumentException("Cannot map an empty memory space.");

if (!AddressRange.Contains(address))
throw new ArgumentException($"Address {address:X8} does not fall within the virtual memory.");
throw new ArgumentException($"Address 0x{address:X8} does not fall within the virtual memory.");

if (!AddressRange.Contains(address + space.AddressRange.Length - 1))
throw new ArgumentException($"Insufficient space available at address 0x{address:X8} to map {space.AddressRange.Length} bytes within the virtual memory.");

int index = GetMemorySpaceIndex(address);
if (index != -1)
throw new ArgumentException($"Address {address:X8} is already in use.");
throw new ArgumentException($"Address 0x{address:X8} is already in use.");

// Insertion sort to ensure _spaces remains sorted.
int i = 0;
Expand Down
292 changes: 292 additions & 0 deletions src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using AsmResolver.DotNet;
using AsmResolver.DotNet.Signatures;
using AsmResolver.PE.DotNet.Cil;
using Echo.Memory;
using Echo.Platforms.AsmResolver.Emulation.Dispatch;
using Echo.Platforms.AsmResolver.Emulation.Stack;

namespace Echo.Platforms.AsmResolver.Emulation
{
/// <summary>
/// Represents a single execution thread in a virtualized .NET process.
/// </summary>
public class CilThread
{
private CilExecutionContext? _singleStepContext;

internal CilThread(CilVirtualMachine machine, CallStack callStack)
{
Machine = machine;
CallStack = callStack;
IsAlive = true;
}

/// <summary>
/// Gets the parent machine the thread is running in.
/// </summary>
public CilVirtualMachine Machine
{
get;
}

/// <summary>
/// Gets the current state of the call stack.
/// </summary>
/// <remarks>
/// The call stack is also addressable from <see cref="Memory"/>.
/// </remarks>
public CallStack CallStack
{
get;
}

/// <summary>
/// Gets a value indicating whether the thread is alive and present in the parent machine.
/// </summary>
public bool IsAlive
{
get;
internal set;
}

/// <summary>
/// Runs the virtual machine until it halts.
/// </summary>
public void Run() => Run(CancellationToken.None);

/// <summary>
/// Runs the virtual machine until it halts.
/// </summary>
/// <param name="cancellationToken">A token that can be used for canceling the emulation.</param>
public void Run(CancellationToken cancellationToken)
{
StepWhile(cancellationToken, context => !context.CurrentFrame.IsRoot);
}

/// <summary>
/// Calls the provided method in the context of the virtual machine.
/// </summary>
/// <param name="method">The method to call.</param>
/// <param name="arguments">The arguments.</param>
/// <returns>The return value, or <c>null</c> if the provided method does not return a value.</returns>
/// <remarks>
/// This method is blocking until the emulation of the call completes.
/// </remarks>
public BitVector? Call(IMethodDescriptor method, object[] arguments)
{
// Short circuit before we do expensive marshalling...
if (arguments.Length != method.Signature!.GetTotalParameterCount())
throw new TargetParameterCountException();

var marshalled = arguments.Select(x => Machine.ObjectMarshaller.ToBitVector(x)).ToArray();
return Call(method, CancellationToken.None, marshalled);
}

/// <summary>
/// Calls the provided method in the context of the virtual machine.
/// </summary>
/// <param name="method">The method to call.</param>
/// <param name="arguments">The arguments.</param>
/// <returns>The return value, or <c>null</c> if the provided method does not return a value.</returns>
/// <remarks>
/// This method is blocking until the emulation of the call completes.
/// </remarks>
public BitVector? Call(IMethodDescriptor method, BitVector[] arguments)
{
return Call(method, CancellationToken.None, arguments);
}

/// <summary>
/// Calls the provided method in the context of the virtual machine.
/// </summary>
/// <param name="method">The method to call.</param>
/// <param name="cancellationToken">A token that can be used for canceling the emulation.</param>
/// <param name="arguments">The arguments.</param>
/// <returns>The return value, or <c>null</c> if the provided method does not return a value.</returns>
/// <remarks>
/// This method is blocking until the emulation of the call completes or the emulation is canceled.
/// </remarks>
public BitVector? Call(IMethodDescriptor method, CancellationToken cancellationToken, BitVector[] arguments)
{
if (arguments.Length != method.Signature!.GetTotalParameterCount())
throw new TargetParameterCountException();

var pool = Machine.ValueFactory.BitVectorPool;

// Instantiate any generic types if available.
var context = GenericContext.FromMethod(method);
var signature = method.Signature.InstantiateGenericTypes(context);

// Set up callee frame.
var frame = CallStack.Push(method);
for (int i = 0; i < arguments.Length; i++)
{
var slot = Machine.ValueFactory.Marshaller.ToCliValue(arguments[i], signature.ParameterTypes[i]);
frame.WriteArgument(i, slot.Contents);
pool.Return(slot.Contents);
}

// Run until we return.
StepOut(cancellationToken);

// If void, then we don't have anything else to do.
if (!signature.ReturnsValue)
return null;

// If we produced a return value, return a copy of it to the caller.
// As the return value may be a rented bit vector, we should copy it to avoid unwanted side-effects.
var callResult = CallStack.Peek().EvaluationStack.Pop(signature.ReturnType);
var result = callResult.Clone();
pool.Return(callResult);

return result;
}

/// <summary>
/// Continues execution of the virtual machine while the provided predicate returns <c>true</c>.
/// </summary>
/// <param name="cancellationToken">A token that can be used for canceling the emulation.</param>
/// <param name="condition">
/// A predicate that is evaluated on every step of the emulation, determining whether execution should continue.
/// </param>
public void StepWhile(CancellationToken cancellationToken, Predicate<CilExecutionContext> condition)
{
var context = new CilExecutionContext(this, cancellationToken);

do
{
Step(context);
cancellationToken.ThrowIfCancellationRequested();
} while (condition(context));
}

/// <summary>
/// Performs a single step in the virtual machine. If the current instruction performs a call, the emulation
/// is treated as a single instruction.
/// </summary>
public void StepOver() => StepOver(CancellationToken.None);

/// <summary>
/// Performs a single step in the virtual machine. If the current instruction performs a call, the emulation
/// is treated as a single instruction.
/// </summary>
/// <param name="cancellationToken">A token that can be used for canceling the emulation.</param>
public void StepOver(CancellationToken cancellationToken)
{
int stackDepth = CallStack.Count;
StepWhile(cancellationToken, context => context.Thread.CallStack.Count > stackDepth);
}

/// <summary>
/// Continues execution of the virtual machine until the current call frame is popped from the stack.
/// </summary>
public void StepOut() => StepOut(CancellationToken.None);

/// <summary>
/// Continues execution of the virtual machine until the current call frame is popped from the stack.
/// </summary>
/// <param name="cancellationToken">A token that can be used for canceling the emulation.</param>
public void StepOut(CancellationToken cancellationToken)
{
int stackDepth = CallStack.Count;
StepWhile(cancellationToken, context => context.Thread.CallStack.Count >= stackDepth);
}

/// <summary>
/// Performs a single step in the virtual machine.
/// </summary>
public void Step()
{
_singleStepContext ??= new CilExecutionContext(this, CancellationToken.None);
Step(_singleStepContext);
}

/// <summary>
/// Performs a single step in the virtual machine.
/// </summary>
/// <param name="cancellationToken">A token that can be used for canceling the emulation.</param>
public void Step(CancellationToken cancellationToken) => Step(new CilExecutionContext(this, cancellationToken));

private void Step(CilExecutionContext context)
{
if (!IsAlive)
throw new CilEmulatorException("The thread is not alive.");

if (CallStack.Peek().IsRoot)
throw new CilEmulatorException("No method is currently being executed.");

var currentFrame = CallStack.Peek();
if (currentFrame.Body is not { } body)
throw new CilEmulatorException("Emulator only supports managed method bodies.");

// Determine the next instruction to dispatch.
int pc = currentFrame.ProgramCounter;
var instruction = body.Instructions.GetByOffset(pc);
if (instruction is null)
throw new CilEmulatorException($"Invalid program counter in {currentFrame}.");

// Are we entering any protected regions?
UpdateExceptionHandlerStack();

// Dispatch the instruction.
var result = Machine.Dispatcher.Dispatch(context, instruction);

if (!result.IsSuccess)
{
var exceptionObject = result.ExceptionObject;
if (exceptionObject.IsNull)
throw new CilEmulatorException("A null exception object was thrown.");

// If there were any errors thrown after dispatching, it may trigger the execution of one of the
// exception handlers in the entire call stack.
if (!UnwindCallStack(exceptionObject))
throw new EmulatedException(exceptionObject);
}
}

private void UpdateExceptionHandlerStack()
{
var currentFrame = CallStack.Peek();

int pc = currentFrame.ProgramCounter;
var availableHandlers = currentFrame.ExceptionHandlers;
var activeHandlers = currentFrame.ExceptionHandlerStack;

for (int i = 0; i < availableHandlers.Count; i++)
{
var handler = availableHandlers[i];
if (handler.ProtectedRange.Contains(pc) && handler.Enter() && !activeHandlers.Contains(handler))
activeHandlers.Push(handler);
}
}

private bool UnwindCallStack(ObjectHandle exceptionObject)
{
while (!CallStack.Peek().IsRoot)
{
var currentFrame = CallStack.Peek();

var result = currentFrame.ExceptionHandlerStack.RegisterException(exceptionObject);
if (result.IsSuccess)
{
// We found a handler that needs to be called. Jump to it.
currentFrame.ProgramCounter = result.NextOffset;

// Push the exception on the stack.
currentFrame.EvaluationStack.Clear();
currentFrame.EvaluationStack.Push(exceptionObject);

return true;
}

CallStack.Pop();
}

return false;
}
}
}
Loading
Loading