Skip to content

Commit

Permalink
Merge pull request #140 from BadRyuner/ldftn_delegates
Browse files Browse the repository at this point in the history
Add ldftn and DelegateShim
  • Loading branch information
Washi1337 authored Jun 4, 2024
2 parents e57f8e3 + 67044d7 commit 687c1fe
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using AsmResolver.DotNet;
using AsmResolver.DotNet.Signatures;
Expand Down Expand Up @@ -236,6 +237,14 @@ private void Step(CilExecutionContext context)
if (CallStack.Peek().IsRoot)
throw new CilEmulatorException("No method is currently being executed.");

if (CallStack.Peek().IsTrampoline)
{
var trampolineFrame = CallStack.Pop();
if (trampolineFrame.Method.Signature!.ReturnsValue)
CallStack.Peek().EvaluationStack.Push(trampolineFrame.EvaluationStack.Pop());
return;
}

var currentFrame = CallStack.Peek();
if (currentFrame.Body is not { } body)
throw new CilEmulatorException("Emulator only supports managed method bodies.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ private static CilDispatchResult Invoke(CilExecutionContext context, IMethodDesc

return CilDispatchResult.Success();

case InvocationResultType.FullyHandled:
return CilDispatchResult.Success();

case InvocationResultType.Exception:
// There was an exception during the invocation. Throw it.
return CilDispatchResult.Exception(result.ExceptionObject);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using AsmResolver.DotNet;
using AsmResolver.PE.DotNet.Cil;

namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel;

/// <summary>
/// Implements a CIL instruction handler for <c>ldftn</c> instruction.
/// </summary>
[DispatcherTableEntry(CilCode.Ldftn)]
public class LdftnHandler : FallThroughOpCodeHandler
{
/// <inheritdoc/>
protected override CilDispatchResult DispatchInternal(CilExecutionContext context, CilInstruction instruction)
{
var stack = context.CurrentFrame.EvaluationStack;
var factory = context.Machine.ValueFactory;
var methods = context.Machine.ValueFactory.ClrMockMemory.MethodEntryPoints;
var type = context.Machine.ContextModule.CorLibTypeFactory.IntPtr;

var functionPointer = methods.GetAddress((IMethodDescriptor)instruction.Operand!);
stack.Push(factory.CreateNativeInteger(functionPointer), type);

return CilDispatchResult.Success();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public static class DefaultInvokers
/// </summary>
public static UnsafeInvoker UnsafeShim => UnsafeInvoker.Instance;

/// <summary>
/// Gets the default shim for the <see cref="System.Delegate"/> class.
/// </summary>
public static DelegateInvoker DelegateShim => DelegateInvoker.Instance;

/// <summary>
/// Gets the default shim for the <see cref="System.Runtime.CompilerServices.RuntimeHelpers"/> class.
/// </summary>
Expand Down Expand Up @@ -96,7 +101,8 @@ public static IMethodInvoker CreateDefaultShims() => StringShim
.WithFallback(UnsafeShim)
.WithFallback(RuntimeHelpersShim)
.WithFallback(IntrinsicsShim)
.WithFallback(MemoryMarshalShim);
.WithFallback(MemoryMarshalShim)
.WithFallback(DelegateShim);

/// <summary>
/// Chains the first method invoker with the provided method invoker in such a way that if the result of the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using AsmResolver.DotNet;
using AsmResolver.DotNet.Signatures;
using AsmResolver.PE.DotNet.Cil;
using Echo.Memory;
using Echo.Platforms.AsmResolver.Emulation.Dispatch;

namespace Echo.Platforms.AsmResolver.Emulation.Invocation;
/// <summary>
/// Wrapper for Delegates
/// </summary>
public class DelegateInvoker : IMethodInvoker
{
/// <summary>
/// Instance
/// </summary>
public static DelegateInvoker Instance { get; } = new();

/// <inheritdoc />
public InvocationResult Invoke(CilExecutionContext context, IMethodDescriptor method, IList<BitVector> arguments)
{
if (method is not { Name: { } name, DeclaringType: { } declaringType, Signature: { } signature })
return InvocationResult.Inconclusive();

if (declaringType.Resolve() is { IsDelegate: false })
return InvocationResult.Inconclusive();

if (method.Name == ".ctor")
{
return ConstructDelegate(context, arguments);
}
else if (method.Name == "Invoke")
{
return InvokeDelegate(context, method, arguments);
}
else
{
return InvocationResult.Inconclusive();
}
}

private InvocationResult ConstructDelegate(CilExecutionContext context, IList<BitVector> arguments)
{
var vm = context.Machine;
var valueFactory = vm.ValueFactory;

var self = arguments[0].AsObjectHandle(vm);
var obj = arguments[1];
var methodPtr = arguments[2];

self.WriteField(valueFactory.DelegateTargetField, obj);
self.WriteField(valueFactory.DelegateMethodPtrField, methodPtr);

return InvocationResult.StepOver(null);
}

private InvocationResult InvokeDelegate(CilExecutionContext context, IMethodDescriptor invokeMethod, IList<BitVector> arguments)
{
var vm = context.Machine;
var valueFactory = vm.ValueFactory;

var self = arguments[0].AsObjectHandle(vm);

var methodPtrFieldValue = self.ReadField(valueFactory.DelegateMethodPtrField);

if (!methodPtrFieldValue.IsFullyKnown)
throw new CilEmulatorException($"Field {self.GetObjectType().FullName}::_methodPtr contains an unknown value. Perhaps delegate was initialised with an error, or memory was corrupted.");

var methodPtr = methodPtrFieldValue.AsSpan().ReadNativeInteger(vm.Is32Bit);

if (!valueFactory.ClrMockMemory.MethodEntryPoints.TryGetObject(methodPtr, out var method))
throw new CilEmulatorException($"Cant resolve method from {self.GetObjectType().FullName}::_methodPtr. Possible causes: IMethodDescriptor was not mapped by the emulator, or memory was corrupted.");

var newArguments = new BitVector[method!.Signature!.GetTotalParameterCount()];

int argumentIndex = 0;

// read and push this for HasThis methods
if (method!.Signature!.HasThis)
newArguments[argumentIndex++] = self.ReadField(valueFactory.DelegateTargetField);

// skip 1 for delegate "this"
for (var i = 1; i < arguments.Count; i++)
newArguments[argumentIndex++] = arguments[i];

var result = context.Machine.Invoker.Invoke(context, method, newArguments);

if (!result.IsSuccess)
{
// return error
return result;
}
else if (result.ResultType == InvocationResultType.StepOver)
{
// return value
return result;
}
else if (result.ResultType == InvocationResultType.StepIn)
{
// create and push the trampoline frame
var trampolineFrame = context.Thread.CallStack.Push(invokeMethod);
trampolineFrame.IsTrampoline = true;

// create and push the method for which the delegate was created
var frame = context.Thread.CallStack.Push(method);
for (int i = 0; i < newArguments.Length; i++)
frame.WriteArgument(i, newArguments[i]);
}

return InvocationResult.FullyHandled();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ private InvocationResult(InvocationResultType resultType, BitVector? value, Obje
/// The result that was produced by the method, or <c>null</c> if the method does not return a value.
/// </param>
public static InvocationResult StepOver(BitVector? value) => new(InvocationResultType.StepOver, value, default);


/// <summary>
/// Constructs a new conclusive invocation result, where the invocation was fully emulated by the invoker.
/// </summary>
public static InvocationResult FullyHandled() => new(InvocationResultType.FullyHandled, null, default);

/// <summary>
/// Constructs a new failed invocation result with the provided pointer to an exception object describing the
/// error that occurred.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public enum InvocationResultType
/// Indicates the invocation was not handled yet.
/// </summary>
Inconclusive,

/// <summary>
/// Indicates the invocation is handled as a step-in action.
/// </summary>
Expand All @@ -20,7 +20,12 @@ public enum InvocationResultType
/// Indicates the invocation is handled fully by the invoker.
/// </summary>
StepOver,


/// <summary>
/// Indicates the invocation is fully emulated by the invoker.
/// </summary>
FullyHandled,

/// <summary>
/// Indicates the invocation resulted in an error.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ public class ClrMockMemory : IMemorySpace
/// </summary>
public ClrMockMemory()
{
_backingBuffer = new VirtualMemory(0x0300_0000);
_backingBuffer = new VirtualMemory(0x0400_0000);

MethodTables = new GenericMockMemory<ITypeDescriptor>(0x0100_0000, 0x100, SignatureComparer.Default);
Methods = new GenericMockMemory<IMethodDescriptor>(0x0100_0000, 0x20, SignatureComparer.Default);
MethodEntryPoints = new GenericMockMemory<IMethodDescriptor>(0x0100_0000, 0x10, SignatureComparer.Default);
Fields = new GenericMockMemory<IFieldDescriptor>(0x0100_0000, 0x20, SignatureComparer.Default);

_backingBuffer.Map(0x0000_0000, MethodTables);
_backingBuffer.Map(0x0100_0000, Methods);
_backingBuffer.Map(0x0200_0000, Fields);
_backingBuffer.Map(0x0200_0000, MethodEntryPoints);
_backingBuffer.Map(0x0300_0000, Fields);
}

/// <inheritdoc />
Expand All @@ -48,6 +50,14 @@ public GenericMockMemory<IMethodDescriptor> Methods
get;
}

/// <summary>
/// Gets the memory assigned for method entry points
/// </summary>
public GenericMockMemory<IMethodDescriptor> MethodEntryPoints
{
get;
}

/// <summary>
/// Gets the memory assigned for field descriptor structures.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ void AllocateFrameField(TypeSignature type)
/// </summary>
public bool IsRoot { get; }

/// <summary>
/// Gets a value indicating that the frame is a trampoline, and that it immediately returns the value of the next method on the stack
/// </summary>
public bool IsTrampoline { get; set; }

/// <summary>
/// Gets the method which this frame was associated with.
/// </summary>
Expand Down
40 changes: 40 additions & 0 deletions src/Platforms/Echo.Platforms.AsmResolver/Emulation/ValueFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ public ValueFactory(ModuleDefinition contextModule, bool is32Bit)
// Force System.String to be aligned at 4 bytes (required for low level string APIs).
GetTypeDefOrRefContentsLayout(contextModule.CorLibTypeFactory.String.Type, default, 4);

DelegateType = new TypeReference(
contextModule,
contextModule.CorLibTypeFactory.CorLibScope,
nameof(System),
nameof(Delegate)).Resolve()!;

DelegateTargetField = new MemberReference(
(IMemberRefParent)DelegateType,
"_target",
new FieldSignature(contextModule.CorLibTypeFactory.Object));

DelegateMethodPtrField = new MemberReference(
(IMemberRefParent)DelegateType,
"_methodPtr",
new FieldSignature(contextModule.CorLibTypeFactory.IntPtr));

DecimalType = new TypeReference(
contextModule,
contextModule.CorLibTypeFactory.CorLibScope,
Expand Down Expand Up @@ -147,6 +163,30 @@ public ITypeDescriptor DecimalType
get;
}

/// <summary>
/// Gets a reference to the <see cref="Delegate"/> type.
/// </summary>
public ITypeDescriptor DelegateType
{
get;
}

/// <summary>
/// Get a reference to the <see cref="Delegate"/> _target field.
/// </summary>
public IFieldDescriptor DelegateTargetField
{
get;
}

/// <summary>
/// Get a reference to the <see cref="Delegate"/> _methodPtr field.
/// </summary>
public IFieldDescriptor DelegateMethodPtrField
{
get;
}

/// <summary>
/// Gets a reference to the <see cref="InvalidProgramException"/> type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,5 +662,53 @@ public void StepPrefixedInstructionsShouldStepOverAllInstructions()
_mainThread.Step();
Assert.Equal(instructions[3].Offset, frame.ProgramCounter);
}

[Fact]
public void CallDelegate()
{
var method = _fixture.MockModule
.LookupMember<TypeDefinition>(typeof(TestClass).MetadataToken)
.Methods.First(m => m.Name == nameof(TestClass.TestDelegateCall));

_vm.Invoker = DefaultInvokers.DelegateShim.WithFallback(DefaultInvokers.StepIn);
_mainThread.CallStack.Push(method);

var instructions = method.CilMethodBody!.Instructions;

var callDelegateOffset = instructions.First(instruction => instruction.OpCode.Code == CilCode.Callvirt).Offset;

_mainThread.StepWhile(CancellationToken.None, context => context.CurrentFrame.ProgramCounter != callDelegateOffset);
_mainThread.Step(); // call delegate::invoke
// callstack:
// (0) root -> (1) TestClass::TestDelegateCall -> (2) ReturnAnyIntDelegate::Invoke -> (3) TestClass::ReturnAnyInt
Assert.Equal(4, _mainThread.CallStack.Count);

_mainThread.StepOut();
// callstack:
// (0) root -> (1) TestClass::TestDelegateCall -> (2) ReturnAnyIntDelegate::Invoke
// evaluation stack:
// (0) i32: 5
Assert.Equal(3, _mainThread.CallStack.Count);
Assert.Single(_mainThread.CallStack.Peek().EvaluationStack);
Assert.Equal(5, _mainThread.CallStack.Peek().EvaluationStack.Peek().Contents.AsSpan().I32);

_mainThread.StepOut();
// callstack:
// (0) root -> (1) TestClass::TestDelegateCall
// evaluation stack:
// (0) i32: 5
Assert.Equal(2, _mainThread.CallStack.Count);
Assert.Single(_mainThread.CallStack.Peek().EvaluationStack);
Assert.Equal(5, _mainThread.CallStack.Peek().EvaluationStack.Peek().Contents.AsSpan().I32);

_mainThread.StepOut();
// callstack:
// (0) root
// evaluation stack:
// (0) i32: 5
Assert.Single(_mainThread.CallStack);
Assert.Single(_mainThread.CallStack.Peek().EvaluationStack);
Assert.Equal(5, _mainThread.CallStack.Peek().EvaluationStack.Peek().Contents.AsSpan().I32);
}
}
}
Loading

0 comments on commit 687c1fe

Please sign in to comment.