From 22484400557b74cf1892cbf6d03466ad445426c0 Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 20 Mar 2024 19:54:14 +0100 Subject: [PATCH 1/8] Add RuntimeTypeManager and type initialization handling on method calls. --- .../Emulation/CilVirtualMachine.cs | 10 ++ .../Dispatch/ObjectModel/CallHandlerBase.cs | 10 ++ .../Emulation/Runtime/ClrMockMemory.cs | 1 + .../Emulation/Runtime/RuntimeTypeManager.cs | 103 ++++++++++++++++++ .../Runtime/TypeInitializerResult.cs | 60 ++++++++++ .../Emulation/ValueFactory.cs | 60 ++++++---- .../Dispatch/ObjectModel/CallHandlerTest.cs | 21 +++- test/Platforms/Mocks/ClassWithInitializer.cs | 10 ++ 8 files changed, 254 insertions(+), 21 deletions(-) create mode 100644 src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs create mode 100644 src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/TypeInitializerResult.cs create mode 100644 test/Platforms/Mocks/ClassWithInitializer.cs diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs index cc93fabe..81aff2d7 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilVirtualMachine.cs @@ -62,6 +62,7 @@ public CilVirtualMachine(ModuleDefinition contextModule, bool is32Bit) } Dispatcher = new CilDispatcher(); + TypeManager = new RuntimeTypeManager(this); Threads = new ReadOnlyCollection(_threads); } @@ -152,6 +153,15 @@ public IMethodInvoker Invoker set; } = DefaultInvokers.ReturnUnknown; + /// + /// Gets the service that is responsible for the initialization and management of runtime types residing in + /// the virtual machine. + /// + public RuntimeTypeManager TypeManager + { + get; + } + /// /// Gets or sets the service that is responsible for resolving unknown values on the stack in critical moments. /// diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/CallHandlerBase.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/CallHandlerBase.cs index f480838f..2e134e13 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/CallHandlerBase.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/CallHandlerBase.cs @@ -156,6 +156,16 @@ private static CilDispatchResult Invoke(CilExecutionContext context, IMethodDesc frame.WriteArgument(i, arguments[i]); context.Thread.CallStack.Push(frame); + + // Ensure type initializer is called for declaring type when necessary. + // TODO: Handle `beforefieldinit` flag. + if (method.DeclaringType is { } declaringType) + { + return context.Machine.TypeManager + .HandleInitialization(context.Thread, declaringType) + .ToDispatchResult(); + } + return CilDispatchResult.Success(); case InvocationResultType.StepOver: diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/ClrMockMemory.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/ClrMockMemory.cs index ccc9c986..695cb6a3 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/ClrMockMemory.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/ClrMockMemory.cs @@ -1,3 +1,4 @@ + using System; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs new file mode 100644 index 00000000..f68549b1 --- /dev/null +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs @@ -0,0 +1,103 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using AsmResolver.DotNet; + +namespace Echo.Platforms.AsmResolver.Emulation.Runtime; + +/// +/// Provides a mechanism for initialization and management of types residing in a virtual machine. +/// +public sealed class RuntimeTypeManager +{ + private readonly CilVirtualMachine _machine; + + private readonly ConcurrentDictionary _initializations + = new(EqualityComparer.Default); + + /// + /// Creates a new runtime type manager. + /// + /// The machine the type is made for. + public RuntimeTypeManager(CilVirtualMachine machine) + { + _machine = machine; + } + + private TypeInitialization GetInitialization(ITypeDescriptor type) + { + if (_initializations.TryGetValue(type, out var initialization)) + return initialization; + + var newInitialization = new TypeInitialization(type); + while (!_initializations.TryGetValue(type, out initialization)) + { + if (_initializations.TryAdd(type, newInitialization)) + { + initialization = newInitialization; + break; + } + } + + return initialization; + } + + /// + /// Handles the type initialization on the provided thread. + /// + /// The thread the initialization is to be called on. + /// The type to initialize. + /// The initialization result. + public TypeInitializerResult HandleInitialization(CilThread thread, ITypeDescriptor type) + { + var initialization = GetInitialization(type); + + lock (initialization) + { + // If we already have an exception cached as a result of a previous type-load failure, rethrow it. + if (!initialization.Exception.IsNull) + return TypeInitializerResult.Exception(initialization.Exception); + + // We only need to call the constructor once. + if (initialization.ConstructorCalled) + return TypeInitializerResult.NoAction(); + + // Try resolve the type that is being initialized. + var definition = type.Resolve(); + if (definition is null) + { + initialization.Exception = _machine.Heap + .AllocateObject(_machine.ValueFactory.TypeInitializationExceptionType, true) + .AsObjectHandle(_machine); + + return TypeInitializerResult.Exception(initialization.Exception); + } + + // "Call" the constructor. + initialization.ConstructorCalled = true; + + // Actually find the constructor and call it if it is there. + var cctor = definition.GetStaticConstructor(); + if (cctor is not null) + { + thread.CallStack.Push(cctor); + return TypeInitializerResult.Redirected(); + } + + return TypeInitializerResult.NoAction(); + } + } + + private sealed class TypeInitialization + { + public TypeInitialization(ITypeDescriptor type) + { + Type = type; + } + + public ITypeDescriptor Type { get; } + + public bool ConstructorCalled { get; set; } + + public ObjectHandle Exception { get; set; } + } +} \ No newline at end of file diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/TypeInitializerResult.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/TypeInitializerResult.cs new file mode 100644 index 00000000..ebac5450 --- /dev/null +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/TypeInitializerResult.cs @@ -0,0 +1,60 @@ +using Echo.Platforms.AsmResolver.Emulation.Dispatch; + +namespace Echo.Platforms.AsmResolver.Emulation.Runtime; + +/// +/// Describes a result of a type initialization. +/// +public readonly struct TypeInitializerResult +{ + private TypeInitializerResult(bool isNoAction, ObjectHandle exceptionObject) + { + IsNoAction = isNoAction; + ExceptionObject = exceptionObject; + } + + /// + /// Gets a value indicating whether the type initialization does not require any further action. + /// + public bool IsNoAction { get; } + + /// + /// Gets a value indicating whether control was redirected to the class constructor of a type. + /// + public bool IsRedirectedToConstructor => !IsNoAction; + + /// + /// Gets the exception that was thrown when initialization the type, if any. + /// + public ObjectHandle ExceptionObject { get; } + + /// + /// Creates a result that indicates no further action was taken. + /// + /// The result. + public static TypeInitializerResult NoAction() => new(true, default); + + /// + /// Creates a result that indicates control was redirected to a class constructor. + /// + /// The result. + public static TypeInitializerResult Redirected() => new(false, default); + + /// + /// Creates a result that throws a type initialization exception. + /// + /// The exception that was thrown. + /// The result. + public static TypeInitializerResult Exception(ObjectHandle exception) => new(false, exception); + + /// + /// Transforms the type initialization result into a . + /// + /// The new dispatcher result. + public CilDispatchResult ToDispatchResult() + { + if (!ExceptionObject.IsNull) + return CilDispatchResult.Exception(ExceptionObject); + return CilDispatchResult.Success(); + } +} \ No newline at end of file diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ValueFactory.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ValueFactory.cs index abd8bb79..30fe8dab 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ValueFactory.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/ValueFactory.cs @@ -49,50 +49,62 @@ public ValueFactory(ModuleDefinition contextModule, bool is32Bit) DecimalType = new TypeReference( contextModule, contextModule.CorLibTypeFactory.CorLibScope, - "System", - "Decimal").Resolve()!; + nameof(System), + nameof(Decimal)).Resolve()!; InvalidProgramExceptionType = new TypeReference( - contextModule, - contextModule.CorLibTypeFactory.CorLibScope, - "System", - "InvalidProgramException").Resolve()!; + contextModule, + contextModule.CorLibTypeFactory.CorLibScope, + nameof(System), + nameof(InvalidProgramException)).Resolve()!; + + TypeInitializationExceptionType = new TypeReference( + contextModule, + contextModule.CorLibTypeFactory.CorLibScope, + nameof(System), + nameof(TypeInitializationException)).Resolve()!; NullReferenceExceptionType = new TypeReference( contextModule, contextModule.CorLibTypeFactory.CorLibScope, - "System", - "NullReferenceException").Resolve()!; + nameof(System), + nameof(NullReferenceException)).Resolve()!; + + InvalidProgramExceptionType = new TypeReference( + contextModule, + contextModule.CorLibTypeFactory.CorLibScope, + nameof(System), + nameof(InvalidProgramException)).Resolve()!; IndexOutOfRangeExceptionType = new TypeReference( contextModule, contextModule.CorLibTypeFactory.CorLibScope, - "System", - "IndexOutOfRangeException").Resolve()!; + nameof(System), + nameof(IndexOutOfRangeException)).Resolve()!; StackOverflowExceptionType = new TypeReference( contextModule, contextModule.CorLibTypeFactory.CorLibScope, - "System", - "StackOverflowException").Resolve()!; + nameof(System), + nameof(StackOverflowException)).Resolve()!; MissingMethodExceptionType = new TypeReference( contextModule, contextModule.CorLibTypeFactory.CorLibScope, - "System", - "MissingMethodException").Resolve()!; + nameof(System), + nameof(MissingMethodException)).Resolve()!; InvalidCastExceptionType = new TypeReference( contextModule, contextModule.CorLibTypeFactory.CorLibScope, - "System", - "InvalidCastException").Resolve()!; + nameof(System), + nameof(InvalidCastException)).Resolve()!; OverflowExceptionType = new TypeReference( contextModule, contextModule.CorLibTypeFactory.CorLibScope, - "System", - "OverflowException").Resolve()!; + nameof(System), + nameof(OverflowException)).Resolve()!; } /// @@ -102,7 +114,7 @@ public ModuleDefinition ContextModule { get; } - + /// /// Gets a value indicating whether the environment is a 32-bit or 64-bit system. /// @@ -134,7 +146,7 @@ public ITypeDescriptor DecimalType { get; } - + /// /// Gets a reference to the type. /// @@ -143,6 +155,14 @@ public ITypeDescriptor InvalidProgramExceptionType get; } + /// + /// Gets a reference to the type. + /// + public TypeDefinition TypeInitializationExceptionType + { + get; + } + /// /// Gets a reference to the type. /// diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs index 41f4ce7e..95e4232c 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs @@ -9,6 +9,7 @@ using Echo.Platforms.AsmResolver.Emulation.Stack; using Echo.Platforms.AsmResolver.Tests.Emulation.Invocation; using Echo.Platforms.AsmResolver.Tests.Mock; +using Mocks; using Xunit; namespace Echo.Platforms.AsmResolver.Tests.Emulation.Dispatch.ObjectModel @@ -236,6 +237,24 @@ public void CallMethodWithGenericArguments() Assert.Single(invoker.LastArguments!); Assert.Equal(1, invoker.LastArguments![0].AsSpan().I32); } - } + [Fact] + public void CallStepInWithInitializer() + { + // Look up metadata. + var type = ModuleFixture.MockModule.LookupMember(typeof(ClassWithInitializer).MetadataToken); + var cctor = type.GetStaticConstructor(); + var method = type.Methods.First(m => m.Name == nameof(ClassWithInitializer.MethodFieldAccess)); + + // Step into method. + Context.Machine.Invoker = DefaultInvokers.StepIn; + var result = Dispatcher.Dispatch(Context, new CilInstruction(CilOpCodes.Call, method)); + + Assert.True(result.IsSuccess); + + // Verify that the .cctor is called. + Assert.Same(cctor, Context.Thread.CallStack.Peek(0).Method); + Assert.Same(method, Context.Thread.CallStack.Peek(1).Method); + } + } } \ No newline at end of file diff --git a/test/Platforms/Mocks/ClassWithInitializer.cs b/test/Platforms/Mocks/ClassWithInitializer.cs new file mode 100644 index 00000000..47adce94 --- /dev/null +++ b/test/Platforms/Mocks/ClassWithInitializer.cs @@ -0,0 +1,10 @@ +namespace Mocks; + +public class ClassWithInitializer +{ + public static string Field = "Test"; + + public static string MethodNoFieldAccess() => "MethodNoFieldAccess"; + + public static string MethodFieldAccess() => "MethodFieldAccess: " + Field; +} \ No newline at end of file From 14893433af3364c60cf11dbd45869c782313266d Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 20 Mar 2024 20:11:45 +0100 Subject: [PATCH 2/8] Add type initialization hooks on field opcodes. --- .../ObjectModel/FieldOpCodeHandler.cs | 47 +++++++++++++++++++ .../Dispatch/ObjectModel/LdFldHandler.cs | 9 ++-- .../Dispatch/ObjectModel/LdFldaHandler.cs | 8 ++-- .../Dispatch/ObjectModel/LdsFldHandler.cs | 8 ++-- .../Dispatch/ObjectModel/LdsFldaHandler.cs | 8 ++-- .../Dispatch/ObjectModel/StFldHandler.cs | 9 ++-- .../Dispatch/ObjectModel/StsFldHandler.cs | 8 ++-- .../Dispatch/ObjectModel/LdsFldHandlerTest.cs | 42 +++++++++++++++-- 8 files changed, 115 insertions(+), 24 deletions(-) create mode 100644 src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/FieldOpCodeHandler.cs diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/FieldOpCodeHandler.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/FieldOpCodeHandler.cs new file mode 100644 index 00000000..8bd2f853 --- /dev/null +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/FieldOpCodeHandler.cs @@ -0,0 +1,47 @@ +using AsmResolver.DotNet; +using AsmResolver.PE.DotNet.Cil; + +namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel; + +/// +/// Represents a handler that handles opcodes related to field access. +/// +public abstract class FieldOpCodeHandler : ICilOpCodeHandler +{ + /// + public CilDispatchResult Dispatch(CilExecutionContext context, CilInstruction instruction) + { + var field = (IFieldDescriptor) instruction.Operand!; + + // Ensure the enclosing type is initialized in the runtime. + if (field.DeclaringType is { } declaringType) + { + var initResult = context.Machine.TypeManager.HandleInitialization(context.Thread, declaringType); + if (!initResult.IsNoAction) + return initResult.ToDispatchResult(); + } + + // Handle the actual field operation. + var dispatchResult = DispatchInternal(context, instruction, field); + + // We are not inheriting from FallThroughOpCodeHandler because of the type initialization. + // This means we need to manually increase the PC on success. + if (dispatchResult.IsSuccess) + context.CurrentFrame.ProgramCounter += instruction.Size; + + return dispatchResult; + } + + /// + /// Handles the actual operation on the field. + /// + /// The context to evaluate the instruction in. + /// The instruction to dispatch and evaluate. + /// The field to perform the operation on. + /// The dispatching result. + protected abstract CilDispatchResult DispatchInternal( + CilExecutionContext context, + CilInstruction instruction, + IFieldDescriptor field + ); +} \ No newline at end of file diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldHandler.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldHandler.cs index 1e5c3882..34bbea90 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldHandler.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldHandler.cs @@ -1,7 +1,6 @@ using System; using AsmResolver.DotNet; using AsmResolver.PE.DotNet.Cil; -using Echo.Memory; using Echo.Platforms.AsmResolver.Emulation.Stack; namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel @@ -10,15 +9,17 @@ namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel /// Implements a CIL instruction handler for ldfld operations. /// [DispatcherTableEntry(CilCode.Ldfld)] - public class LdFldHandler : FallThroughOpCodeHandler + public class LdFldHandler : FieldOpCodeHandler { /// - protected override CilDispatchResult DispatchInternal(CilExecutionContext context, CilInstruction instruction) + protected override CilDispatchResult DispatchInternal( + CilExecutionContext context, + CilInstruction instruction, + IFieldDescriptor field) { var stack = context.CurrentFrame.EvaluationStack; var factory = context.Machine.ValueFactory; - var field = (IFieldDescriptor) instruction.Operand!; var instance = stack.Pop(); try diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldaHandler.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldaHandler.cs index db79a7e5..87ec9742 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldaHandler.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdFldaHandler.cs @@ -9,15 +9,17 @@ namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel /// Implements a CIL instruction handler for ldflda operations. /// [DispatcherTableEntry(CilCode.Ldflda)] - public class LdFldaHandler : FallThroughOpCodeHandler + public class LdFldaHandler : FieldOpCodeHandler { /// - protected override CilDispatchResult DispatchInternal(CilExecutionContext context, CilInstruction instruction) + protected override CilDispatchResult DispatchInternal( + CilExecutionContext context, + CilInstruction instruction, + IFieldDescriptor field) { var stack = context.CurrentFrame.EvaluationStack; var factory = context.Machine.ValueFactory; - var field = (IFieldDescriptor) instruction.Operand!; var instance = stack.Pop(); var result = context.Machine.ValueFactory.RentNativeInteger(false); diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldHandler.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldHandler.cs index 6a8b85f8..a6189a75 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldHandler.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldHandler.cs @@ -7,12 +7,14 @@ namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel /// Implements a CIL instruction handler for ldsfld operations. /// [DispatcherTableEntry(CilCode.Ldsfld)] - public class LdsFldHandler : FallThroughOpCodeHandler + public class LdsFldHandler : FieldOpCodeHandler { /// - protected override CilDispatchResult DispatchInternal(CilExecutionContext context, CilInstruction instruction) + protected override CilDispatchResult DispatchInternal( + CilExecutionContext context, + CilInstruction instruction, + IFieldDescriptor field) { - var field = (IFieldDescriptor) instruction.Operand!; var fieldSpan = context.Machine.StaticFields.GetFieldSpan(field); context.CurrentFrame.EvaluationStack.Push(fieldSpan, field.Signature!.FieldType); return CilDispatchResult.Success(); diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldaHandler.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldaHandler.cs index e95529d9..89920358 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldaHandler.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/LdsFldaHandler.cs @@ -8,12 +8,14 @@ namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel /// Implements a CIL instruction handler for ldsflda operations. /// [DispatcherTableEntry(CilCode.Ldsflda)] - public class LdsFldaHandler : FallThroughOpCodeHandler + public class LdsFldaHandler : FieldOpCodeHandler { /// - protected override CilDispatchResult DispatchInternal(CilExecutionContext context, CilInstruction instruction) + protected override CilDispatchResult DispatchInternal( + CilExecutionContext context, + CilInstruction instruction, + IFieldDescriptor field) { - var field = (IFieldDescriptor) instruction.Operand!; var address = context.Machine.ValueFactory.RentNativeInteger( context.Machine.StaticFields.GetFieldAddress(field)); context.CurrentFrame.EvaluationStack.Push(new StackSlot(address, StackSlotTypeHint.Integer)); diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StFldHandler.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StFldHandler.cs index c6c1870a..4930cbba 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StFldHandler.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StFldHandler.cs @@ -1,6 +1,5 @@ using AsmResolver.DotNet; using AsmResolver.PE.DotNet.Cil; -using Echo.Memory; using Echo.Platforms.AsmResolver.Emulation.Stack; namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel @@ -9,15 +8,17 @@ namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel /// Implements a CIL instruction handler for stfld operations. /// [DispatcherTableEntry(CilCode.Stfld)] - public class StFldHandler : FallThroughOpCodeHandler + public class StFldHandler : FieldOpCodeHandler { /// - protected override CilDispatchResult DispatchInternal(CilExecutionContext context, CilInstruction instruction) + protected override CilDispatchResult DispatchInternal( + CilExecutionContext context, + CilInstruction instruction, + IFieldDescriptor field) { var stack = context.CurrentFrame.EvaluationStack; var factory = context.Machine.ValueFactory; - var field = (IFieldDescriptor) instruction.Operand!; var value = stack.Pop(field.Signature!.FieldType); var instance = stack.Pop(); diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StsFldHandler.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StsFldHandler.cs index bb408b5f..6a907cfc 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StsFldHandler.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Dispatch/ObjectModel/StsFldHandler.cs @@ -7,12 +7,14 @@ namespace Echo.Platforms.AsmResolver.Emulation.Dispatch.ObjectModel /// Implements a CIL instruction handler for stsfld operations. /// [DispatcherTableEntry(CilCode.Stsfld)] - public class StsFldHandler : FallThroughOpCodeHandler + public class StsFldHandler : FieldOpCodeHandler { /// - protected override CilDispatchResult DispatchInternal(CilExecutionContext context, CilInstruction instruction) + protected override CilDispatchResult DispatchInternal( + CilExecutionContext context, + CilInstruction instruction, + IFieldDescriptor field) { - var field = (IFieldDescriptor) instruction.Operand!; var value = context.CurrentFrame.EvaluationStack.Pop(field.Signature!.FieldType); try diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs index 67e12b32..89d342fc 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs @@ -1,7 +1,12 @@ +using System.Linq; +using System.Text; using AsmResolver.DotNet; using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; +using Echo.Platforms.AsmResolver.Emulation; +using Echo.Platforms.AsmResolver.Emulation.Stack; using Echo.Platforms.AsmResolver.Tests.Mock; +using Mocks; using Xunit; namespace Echo.Platforms.AsmResolver.Tests.Emulation.Dispatch.ObjectModel; @@ -26,8 +31,7 @@ public void ReadStaticInt32() var result = Dispatcher.Dispatch(Context, new CilInstruction(CilOpCodes.Ldsfld, field)); Assert.True(result.IsSuccess); - Assert.Single(stack); - Assert.Equal(1337, stack.Pop().Contents.AsSpan().I32); + Assert.Equal(1337, Assert.Single(stack).Contents.AsSpan().I32); } [Fact] @@ -43,7 +47,37 @@ public void ReadStaticFloat64() var result = Dispatcher.Dispatch(Context, new CilInstruction(CilOpCodes.Ldsfld, field)); Assert.True(result.IsSuccess); - Assert.Single(stack); - Assert.Equal(1337.0, stack.Pop().Contents.AsSpan().F64); + Assert.Equal(1337.0, Assert.Single(stack).Contents.AsSpan().F64); + } + + [Fact] + public void ReadStaticInt32WithInitializer() + { + // Lookup metadata. + var type = ModuleFixture.MockModule.LookupMember(typeof(ClassWithInitializer).MetadataToken); + var cctor = type.GetStaticConstructor(); + var field = type.Fields.First(x => x.Name == nameof(ClassWithInitializer.Field)); + + // Load static field. + var result = Dispatcher.Dispatch(Context, new CilInstruction(CilOpCodes.Ldsfld, field)); + + // Verify we jumped into the cctor. + Assert.True(result.IsSuccess); + Assert.Same(cctor, Context.Thread.CallStack.Peek(0).Method); + + // Finish execution of the cctor. + Context.Thread.StepOut(); + + // Load static field again. + result = Dispatcher.Dispatch(Context, new CilInstruction(CilOpCodes.Ldsfld, field)); + + // Verify we did not jump into the cctor. + Assert.True(result.IsSuccess); + Assert.NotSame(cctor, Context.Thread.CallStack.Peek(0).Method); + + // Verify that a string is pushed. + var slot = Assert.Single(Context.CurrentFrame.EvaluationStack).Contents.AsObjectHandle(Context.Machine); + Assert.False(slot.IsNull); + Assert.Equal(ClassWithInitializer.Field, Encoding.Unicode.GetString(slot.ReadStringData().Bits)); } } \ No newline at end of file From 9805a21128f47c660b022a10ab0ad632590a7c19 Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 20 Mar 2024 20:13:41 +0100 Subject: [PATCH 3/8] Avoid locks in runtime type manager. --- .../Emulation/Runtime/RuntimeTypeManager.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs index f68549b1..9fac299d 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs @@ -51,16 +51,23 @@ public TypeInitializerResult HandleInitialization(CilThread thread, ITypeDescrip { var initialization = GetInitialization(type); + // If we already have an exception cached as a result of a previous type-load failure, rethrow it. + if (!initialization.Exception.IsNull) + return TypeInitializerResult.Exception(initialization.Exception); + + // We only need to call the constructor once. + if (initialization.ConstructorCalled) + return TypeInitializerResult.NoAction(); + lock (initialization) { - // If we already have an exception cached as a result of a previous type-load failure, rethrow it. + // Try check if any thread beat us in the initialization handling. if (!initialization.Exception.IsNull) return TypeInitializerResult.Exception(initialization.Exception); - - // We only need to call the constructor once. + if (initialization.ConstructorCalled) return TypeInitializerResult.NoAction(); - + // Try resolve the type that is being initialized. var definition = type.Resolve(); if (definition is null) @@ -71,7 +78,7 @@ public TypeInitializerResult HandleInitialization(CilThread thread, ITypeDescrip return TypeInitializerResult.Exception(initialization.Exception); } - + // "Call" the constructor. initialization.ConstructorCalled = true; From 75d449a6fe9a4bb4ccf6111cf2a57f53627d714a Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 20 Mar 2024 20:27:50 +0100 Subject: [PATCH 4/8] Create TypeInitializationException instances when unwrapping from .cctors. --- .../Emulation/CilThread.cs | 8 ++++-- .../Emulation/Runtime/RuntimeTypeManager.cs | 27 +++++++++++++++++++ .../Dispatch/ObjectModel/CallHandlerTest.cs | 21 +++++++++++++++ .../Mocks/ClassWithThrowingInitializer.cs | 17 ++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 test/Platforms/Mocks/ClassWithThrowingInitializer.cs diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs index 47e351f7..9482c0d9 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs @@ -260,7 +260,7 @@ private void Step(CilExecutionContext context) // 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)) + if (!UnwindCallStack(ref exceptionObject)) throw new EmulatedException(exceptionObject); } } @@ -281,12 +281,16 @@ private void UpdateExceptionHandlerStack() } } - private bool UnwindCallStack(ObjectHandle exceptionObject) + private bool UnwindCallStack(ref ObjectHandle exceptionObject) { while (!CallStack.Peek().IsRoot) { var currentFrame = CallStack.Peek(); + // If the exception happened in a .cctor, register it and wrap it in a type initialization error. + if (currentFrame.Body?.Owner is { IsConstructor: true, IsStatic: true, DeclaringType: {} type }) + exceptionObject = Machine.TypeManager.RegisterTypeInitializationException(type, exceptionObject); + var result = currentFrame.ExceptionHandlerStack.RegisterException(exceptionObject); if (result.IsSuccess) { diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs index 9fac299d..ea56f12c 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs @@ -41,6 +41,33 @@ private TypeInitialization GetInitialization(ITypeDescriptor type) return initialization; } + /// + /// Registers the event that a type has failed to initialize. + /// + /// The type that failed to initialize. + /// The exception object that describes the failure. + /// The resulting TypeInitializationException instance. + public ObjectHandle RegisterTypeInitializationException(ITypeDescriptor type, ObjectHandle innerException) + { + var initialization = GetInitialization(type); + if (!initialization.Exception.IsNull) + return initialization.Exception; + + lock (initialization) + { + if (initialization.Exception.IsNull) + { + initialization.Exception = _machine.Heap + .AllocateObject(_machine.ValueFactory.TypeInitializationExceptionType, true) + .AsObjectHandle(_machine); + } + + // TODO: incorporate `exceptionObject`. + } + + return initialization.Exception; + } + /// /// Handles the type initialization on the provided thread. /// diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs index 95e4232c..d0fc96dc 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/CallHandlerTest.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; @@ -5,6 +6,7 @@ using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; using Echo.Memory; +using Echo.Platforms.AsmResolver.Emulation; using Echo.Platforms.AsmResolver.Emulation.Invocation; using Echo.Platforms.AsmResolver.Emulation.Stack; using Echo.Platforms.AsmResolver.Tests.Emulation.Invocation; @@ -256,5 +258,24 @@ public void CallStepInWithInitializer() Assert.Same(cctor, Context.Thread.CallStack.Peek(0).Method); Assert.Same(method, Context.Thread.CallStack.Peek(1).Method); } + + [Fact] + public void CallStepInWithThrowingInitializer() + { + // Look up metadata. + var type = ModuleFixture.MockModule.LookupMember(typeof(ClassWithThrowingInitializer).MetadataToken); + var cctor = type.GetStaticConstructor(); + var method = type.Methods.First(m => m.Name == nameof(ClassWithThrowingInitializer.MethodFieldAccess)); + + // Step into method. + Context.Machine.Invoker = DefaultInvokers.StepIn; + Dispatcher.Dispatch(Context, new CilInstruction(CilOpCodes.Call, method)); + + Assert.Same(cctor, Context.Thread.CallStack.Peek(0).Method); + Assert.Same(method, Context.Thread.CallStack.Peek(1).Method); + + var exception = Assert.Throws(() => Context.Thread.StepOut()); + Assert.Equal(nameof(TypeInitializationException), exception.ExceptionObject.GetObjectType().Name); + } } } \ No newline at end of file diff --git a/test/Platforms/Mocks/ClassWithThrowingInitializer.cs b/test/Platforms/Mocks/ClassWithThrowingInitializer.cs new file mode 100644 index 00000000..81b39dc8 --- /dev/null +++ b/test/Platforms/Mocks/ClassWithThrowingInitializer.cs @@ -0,0 +1,17 @@ +using System; + +namespace Mocks; + +public class ClassWithThrowingInitializer +{ + public static string Field = "Test"; + + static ClassWithThrowingInitializer() + { + throw new Exception(); + } + + public static string MethodNoFieldAccess() => "MethodNoFieldAccess"; + + public static string MethodFieldAccess() => "MethodFieldAccess: " + Field; +} \ No newline at end of file From ce8ddfe768a8c97434939a358fea7e3972a5c906 Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 20 Mar 2024 20:51:55 +0100 Subject: [PATCH 5/8] Add static method and field integration test. --- .../Emulation/CilThread.cs | 2 +- .../Emulation/Runtime/RuntimeTypeManager.cs | 4 ++-- .../Emulation/CilVirtualMachineTest.cs | 19 +++++++++++++++++++ test/Platforms/Mocks/ClassWithInitializer.cs | 3 +++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs index 9482c0d9..8fd524b3 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/CilThread.cs @@ -289,7 +289,7 @@ private bool UnwindCallStack(ref ObjectHandle exceptionObject) // If the exception happened in a .cctor, register it and wrap it in a type initialization error. if (currentFrame.Body?.Owner is { IsConstructor: true, IsStatic: true, DeclaringType: {} type }) - exceptionObject = Machine.TypeManager.RegisterTypeInitializationException(type, exceptionObject); + exceptionObject = Machine.TypeManager.RegisterInitializationException(type, exceptionObject); var result = currentFrame.ExceptionHandlerStack.RegisterException(exceptionObject); if (result.IsSuccess) diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs index ea56f12c..99a1432f 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs @@ -47,7 +47,7 @@ private TypeInitialization GetInitialization(ITypeDescriptor type) /// The type that failed to initialize. /// The exception object that describes the failure. /// The resulting TypeInitializationException instance. - public ObjectHandle RegisterTypeInitializationException(ITypeDescriptor type, ObjectHandle innerException) + public ObjectHandle RegisterInitializationException(ITypeDescriptor type, ObjectHandle innerException) { var initialization = GetInitialization(type); if (!initialization.Exception.IsNull) @@ -62,7 +62,7 @@ public ObjectHandle RegisterTypeInitializationException(ITypeDescriptor type, Ob .AsObjectHandle(_machine); } - // TODO: incorporate `exceptionObject`. + // TODO: incorporate `innerException`. } return initialization.Exception; diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs index 3c698ae9..4199ffdd 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs @@ -12,6 +12,7 @@ using Echo.Platforms.AsmResolver.Emulation.Invocation; using Echo.Platforms.AsmResolver.Tests.Emulation.Dispatch; using Echo.Platforms.AsmResolver.Tests.Mock; +using Mocks; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; @@ -587,5 +588,23 @@ public void CallAndThrowUnhandledException(string methodName, object parameter) Assert.Equal(expectedException.GetType().FullName, result.ExceptionObject.GetObjectType().FullName); } + [Fact] + public void StaticFieldWithInitialValueUpdate() + { + var type = _fixture.MockModule.LookupMember(typeof(ClassWithInitializer).MetadataToken); + var increment = type.Methods.First(m => m.Name == nameof(ClassWithInitializer.IncrementCounter)); + var counter = type.Fields.First(f => f.Name == nameof(ClassWithInitializer.Counter)); + + // Verify uninitialized value. + Assert.Equal(0, _vm.StaticFields.GetFieldSpan(counter).I32); + + // Call and verify value. + _mainThread.Call(increment, Array.Empty()); + Assert.Equal(1337 + 1, _vm.StaticFields.GetFieldSpan(counter).I32); + _mainThread.Call(increment, Array.Empty()); + Assert.Equal(1337 + 2, _vm.StaticFields.GetFieldSpan(counter).I32); + _mainThread.Call(increment, Array.Empty()); + Assert.Equal(1337 + 3, _vm.StaticFields.GetFieldSpan(counter).I32); + } } } \ No newline at end of file diff --git a/test/Platforms/Mocks/ClassWithInitializer.cs b/test/Platforms/Mocks/ClassWithInitializer.cs index 47adce94..2ebc33bb 100644 --- a/test/Platforms/Mocks/ClassWithInitializer.cs +++ b/test/Platforms/Mocks/ClassWithInitializer.cs @@ -3,8 +3,11 @@ namespace Mocks; public class ClassWithInitializer { public static string Field = "Test"; + public static int Counter = 1337; public static string MethodNoFieldAccess() => "MethodNoFieldAccess"; public static string MethodFieldAccess() => "MethodFieldAccess: " + Field; + + public static void IncrementCounter() => Counter++; } \ No newline at end of file From c77651d942678c22034e2e27f695ca7658e20681 Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 20 Mar 2024 21:10:12 +0100 Subject: [PATCH 6/8] BUGFIX: Use SignatureComparer in runtime type manager. --- .../Emulation/Runtime/RuntimeTypeManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs index 99a1432f..de57236d 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; namespace Echo.Platforms.AsmResolver.Emulation.Runtime; @@ -12,7 +13,7 @@ public sealed class RuntimeTypeManager private readonly CilVirtualMachine _machine; private readonly ConcurrentDictionary _initializations - = new(EqualityComparer.Default); + = new(SignatureComparer.Default); /// /// Creates a new runtime type manager. From 8cb57ac4eded6beb87c8a7592a491182b8db8d43 Mon Sep 17 00:00:00 2001 From: Washi Date: Wed, 20 Mar 2024 21:44:38 +0100 Subject: [PATCH 7/8] Add FieldRva initialization. --- .../Emulation/Runtime/RuntimeTypeManager.cs | 3 ++- .../Emulation/Runtime/StaticFieldStorage.cs | 8 ++++++++ .../Dispatch/ObjectModel/LdsFldHandlerTest.cs | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs index de57236d..be24a519 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/RuntimeTypeManager.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using AsmResolver; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; @@ -109,7 +110,7 @@ public TypeInitializerResult HandleInitialization(CilThread thread, ITypeDescrip // "Call" the constructor. initialization.ConstructorCalled = true; - + // Actually find the constructor and call it if it is there. var cctor = definition.GetStaticConstructor(); if (cctor is not null) diff --git a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/StaticFieldStorage.cs b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/StaticFieldStorage.cs index bb506660..747b64a0 100644 --- a/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/StaticFieldStorage.cs +++ b/src/Platforms/Echo.Platforms.AsmResolver/Emulation/Runtime/StaticFieldStorage.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using AsmResolver; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using Echo.Memory; @@ -51,6 +52,13 @@ public long GetFieldAddress(IFieldDescriptor field) { var layout = _valueFactory.GetTypeValueMemoryLayout(field.Signature!.FieldType); address = _heap.Allocate(layout.Size, true); + + if (field.Resolve() is { IsStatic: true, HasFieldRva: true, FieldRva: IReadableSegment data } def) + { + field = def; + _heap.GetChunkSpan(address).Write(data.WriteIntoArray()); + } + _fields.Add(field, address); } diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs index 89d342fc..ed29e93c 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/Dispatch/ObjectModel/LdsFldHandlerTest.cs @@ -1,5 +1,7 @@ +using System; using System.Linq; using System.Text; +using AsmResolver; using AsmResolver.DotNet; using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; @@ -80,4 +82,20 @@ public void ReadStaticInt32WithInitializer() Assert.False(slot.IsNull); Assert.Equal(ClassWithInitializer.Field, Encoding.Unicode.GetString(slot.ReadStringData().Bits)); } + + [Fact] + public void ReadStaticInt32WithFieldRva() + { + var field = new FieldDefinition( + "StaticInt32WithFieldRva", + FieldAttributes.Static | FieldAttributes.HasFieldRva, + ModuleFixture.MockModule.CorLibTypeFactory.Int32 + ); + field.FieldRva = new DataSegment(BitConverter.GetBytes(1337)); + + var result = Dispatcher.Dispatch(Context, new CilInstruction(CilOpCodes.Ldsfld, field)); + + Assert.True(result.IsSuccess); + Assert.Equal(1337, Assert.Single(Context.CurrentFrame.EvaluationStack).Contents.AsSpan().I32); + } } \ No newline at end of file From 4ff94aa23d0c662c9b5824861ccafc1ee6fa1b01 Mon Sep 17 00:00:00 2001 From: Washi Date: Fri, 22 Mar 2024 17:56:46 +0100 Subject: [PATCH 8/8] Add nested class constructors test. --- .../Emulation/CilVirtualMachineTest.cs | 17 +++++++++++++++++ .../Mocks/ClassWithNestedInitializer.cs | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 test/Platforms/Mocks/ClassWithNestedInitializer.cs diff --git a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs index 4199ffdd..496189d5 100644 --- a/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs +++ b/test/Platforms/Echo.Platforms.AsmResolver.Tests/Emulation/CilVirtualMachineTest.cs @@ -588,6 +588,23 @@ public void CallAndThrowUnhandledException(string methodName, object parameter) Assert.Equal(expectedException.GetType().FullName, result.ExceptionObject.GetObjectType().FullName); } + [Fact] + public void SteppingWithNestedInitializers() + { + // Look up metadata. + var method = _fixture + .MockModule.LookupMember(typeof(ClassWithNestedInitializer.Class1).MetadataToken) + .Methods.First(m => m.Name == nameof(ClassWithNestedInitializer.Class1.Method)); + + // CAll method. + _vm.Invoker = DefaultInvokers.StepIn; + var result = _mainThread.Call(method, Array.Empty()); + + // Verify. + Assert.NotNull(result); + Assert.Equal(1337, result.AsSpan().I32); + } + [Fact] public void StaticFieldWithInitialValueUpdate() { diff --git a/test/Platforms/Mocks/ClassWithNestedInitializer.cs b/test/Platforms/Mocks/ClassWithNestedInitializer.cs new file mode 100644 index 00000000..8e1a5b11 --- /dev/null +++ b/test/Platforms/Mocks/ClassWithNestedInitializer.cs @@ -0,0 +1,16 @@ +namespace Mocks; + + public class ClassWithNestedInitializer + { + public class Class1 + { + public static int Field = Class2.Field; + + public static int Method() => Field; + } + + public class Class2 + { + public static int Field = 1337; + } + } \ No newline at end of file