Add hook for non-IL methods (#15)

This commit is contained in:
Patrick Stevens
2025-05-26 10:43:46 +01:00
committed by GitHub
parent 48b4fecc86
commit 0b10ccedfd
7 changed files with 234 additions and 73 deletions

View File

@@ -51,7 +51,9 @@ and MethodState =
}
static member advanceProgramCounter (state : MethodState) =
MethodState.jumpProgramCounter (IlOp.NumberOfBytes state.ExecutingMethod.Locations.[state.IlOpIndex]) state
MethodState.jumpProgramCounter
(IlOp.NumberOfBytes state.ExecutingMethod.Instructions.Value.Locations.[state.IlOpIndex])
state
static member peekEvalStack (state : MethodState) : EvalStackValue option = EvalStack.Peek state.EvaluationStack
@@ -132,14 +134,21 @@ and MethodState =
$"Non-static method {method.Name} should have had %i{method.Parameters.Length + 1} parameters, but was given %i{args.Length}"
let localVariableSig =
match method.LocalVars with
match method.Instructions with
| None -> ImmutableArray.Empty
| Some vars -> vars
| Some method ->
match method.LocalVars with
| None -> ImmutableArray.Empty
| Some vars -> vars
// I think valid code should remain valid if we unconditionally localsInit - it should be undefined
// to use an uninitialised value? Not checked this; TODO.
let localVars =
localVariableSig |> Seq.map CliType.zeroOf |> ImmutableArray.CreateRange
do
let args = args |> Seq.map string<CliType> |> String.concat " ; "
System.Console.Error.WriteLine $"Setting args list in {method.Name}: {args}"
{
EvaluationStack = EvalStack.Empty
LocalVariables = localVars
@@ -518,6 +527,8 @@ module IlMachineState =
afterPop <- afterPop'
args.Add poppedArg
args.Reverse ()
let newFrame =
MethodState.Empty
methodToCall
@@ -534,12 +545,6 @@ module IlMachineState =
else
let args = ImmutableArray.CreateBuilder (methodToCall.Parameters.Length + 1)
let poppedArg, afterPop = activeMethodState |> MethodState.popFromStack
// it only matters that the RuntimePointer is a RuntimePointer, so that the coercion has a target of the
// right shape
args.Add (
EvalStackValue.toCliTypeCoerced (CliType.RuntimePointer (CliRuntimePointer.Unmanaged ())) poppedArg
)
let mutable afterPop = afterPop
for i = 1 to methodToCall.Parameters.Length do
@@ -549,6 +554,14 @@ module IlMachineState =
afterPop <- afterPop'
args.Add poppedArg
// it only matters that the RuntimePointer is a RuntimePointer, so that the coercion has a target of the
// right shape
args.Add (
EvalStackValue.toCliTypeCoerced (CliType.RuntimePointer (CliRuntimePointer.Unmanaged ())) poppedArg
)
args.Reverse ()
let newFrame =
MethodState.Empty
methodToCall
@@ -1055,6 +1068,10 @@ module AbstractMachine =
currentThread
{ threadStateAtEndOfMethod with
ActiveMethodState = returnState.JumpTo
ActiveAssembly =
snd
threadStateAtEndOfMethod.MethodStates.[returnState.JumpTo].ExecutingMethod
.DeclaringType
}
}
@@ -1315,7 +1332,36 @@ module AbstractMachine =
| Ldind_i2 -> failwith "TODO: Ldind_i2 unimplemented"
| Ldind_i4 -> failwith "TODO: Ldind_i4 unimplemented"
| Ldind_i8 -> failwith "TODO: Ldind_i8 unimplemented"
| Ldind_u1 -> failwith "TODO: Ldind_u1 unimplemented"
| Ldind_u1 ->
let popped, state = IlMachineState.popEvalStack currentThread state
let value =
match popped with
| EvalStackValue.NativeInt nativeIntSource -> failwith $"TODO: in Ldind_u1, {nativeIntSource}"
| EvalStackValue.ManagedPointer src ->
match src with
| ManagedPointerSource.Null -> failwith "unexpected null pointer in Ldind_u1"
| ManagedPointerSource.LocalVariable (sourceThread, methodFrame, whichVar) ->
let methodState =
state.ThreadState.[sourceThread].MethodStates.[methodFrame].LocalVariables
.[int<uint16> whichVar]
match methodState with
| CliType.Bool b -> b
| CliType.Numeric numeric -> failwith $"tried to load a Numeric as a u8: {numeric}"
| CliType.Char _ -> failwith "tried to load a Char as a u8"
| CliType.ObjectRef _ -> failwith "tried to load an ObjectRef as a u8"
| CliType.RuntimePointer _ -> failwith "tried to load a RuntimePointer as a u8"
| ManagedPointerSource.Heap managedHeapAddress -> failwith "todo"
| EvalStackValue.ObjectRef managedHeapAddress -> failwith "todo"
| popped -> failwith $"unexpected Ldind_u1 input: {popped}"
let state =
state
|> IlMachineState.pushToEvalStack (CliType.Numeric (CliNumericType.UInt8 value)) currentThread
|> IlMachineState.advanceProgramCounter currentThread
(state, WhatWeDid.Executed) |> ExecutionResult.Stepped
| Ldind_u2 -> failwith "TODO: Ldind_u2 unimplemented"
| Ldind_u4 -> failwith "TODO: Ldind_u4 unimplemented"
| Ldind_u8 -> failwith "TODO: Ldind_u8 unimplemented"
@@ -1467,7 +1513,12 @@ module AbstractMachine =
| Choice2Of2 _field -> failwith "tried to Call a field"
| Choice1Of2 method -> state, method
| MetadataToken.MethodDef defn -> state, (state.ActiveAssembly thread).Methods.[defn]
| MetadataToken.MethodDef defn ->
let activeAssy = state.ActiveAssembly thread
match activeAssy.Methods.TryGetValue defn with
| true, method -> state, method
| false, _ -> failwith $"could not find method in {activeAssy.Name}"
| k -> failwith $"Unrecognised kind: %O{k}"
state.WithThreadSwitchedToAssembly (snd methodToCall.DeclaringType) thread
@@ -1645,8 +1696,12 @@ module AbstractMachine =
| EvalStackValue.Float f -> failwith "todo: float"
| EvalStackValue.ManagedPointer managedPointerSource ->
match managedPointerSource with
| ManagedPointerSource.LocalVariable (source, whichVar) ->
failwith $"todo: local variable {whichVar}"
| ManagedPointerSource.LocalVariable (sourceThread, methodFrame, whichVar) ->
let currentValue =
state.ThreadState.[sourceThread].MethodStates.[methodFrame].LocalVariables
.[int<uint16> whichVar]
failwith $"todo: local variable {currentValue} {field}"
| ManagedPointerSource.Heap managedHeapAddress -> failwith $"todo: heap addr {managedHeapAddress}"
| ManagedPointerSource.Null -> failwith "TODO: raise NullReferenceException"
| EvalStackValue.ObjectRef managedHeapAddress -> failwith $"todo: {managedHeapAddress}"
@@ -1818,7 +1873,28 @@ module AbstractMachine =
|> IlMachineState.advanceProgramCounter currentThread
|> IlMachineState.jumpProgramCounter currentThread (int b)
|> Tuple.withRight WhatWeDid.Executed
| Brfalse_s b -> failwith "TODO: Brfalse_s unimplemented"
| Brfalse_s b ->
let popped, state = IlMachineState.popEvalStack currentThread state
let isTrue =
match popped with
| EvalStackValue.Int32 i -> i <> 0
| EvalStackValue.Int64 i -> i <> 0L
| EvalStackValue.NativeInt i -> not (NativeIntSource.isZero i)
| EvalStackValue.Float f -> failwith "TODO: Brfalse_s float semantics undocumented"
| EvalStackValue.ManagedPointer ManagedPointerSource.Null -> false
| EvalStackValue.ManagedPointer _ -> true
| EvalStackValue.ObjectRef _ -> failwith "TODO: Brfalse_s ObjectRef comparison unimplemented"
| EvalStackValue.UserDefinedValueType ->
failwith "TODO: Brfalse_s UserDefinedValueType comparison unimplemented"
state
|> IlMachineState.advanceProgramCounter currentThread
|> if isTrue then
id
else
IlMachineState.jumpProgramCounter currentThread (int b)
|> Tuple.withRight WhatWeDid.Executed
| Brtrue_s b ->
let popped, state = IlMachineState.popEvalStack currentThread state
@@ -1908,12 +1984,17 @@ module AbstractMachine =
| Ldloc_s b -> failwith "TODO: Ldloc_s unimplemented"
| Ldloca_s b ->
let threadState = state.ThreadState.[currentThread]
let methodState = threadState.MethodStates.[threadState.ActiveMethodState]
let state =
state
|> IlMachineState.pushToEvalStack'
(EvalStackValue.ManagedPointer (ManagedPointerSource.LocalVariable ((), uint16<uint8> b)))
(EvalStackValue.ManagedPointer (
ManagedPointerSource.LocalVariable (
currentThread,
threadState.ActiveMethodState,
uint16<uint8> b
)
))
currentThread
|> IlMachineState.advanceProgramCounter currentThread
@@ -1940,7 +2021,14 @@ module AbstractMachine =
let logger = loggerFactory.CreateLogger typeof<Dummy>.DeclaringType
let instruction = state.ThreadState.[thread].MethodState
match instruction.ExecutingMethod.Locations.TryGetValue instruction.IlOpIndex with
match instruction.ExecutingMethod.Instructions with
| None ->
failwith
$"TODO: tried to IL-interpret a method in {snd(instruction.ExecutingMethod.DeclaringType).Name} named {instruction.ExecutingMethod.Name} with no implementation"
| Some instructions ->
match instructions.Locations.TryGetValue instruction.IlOpIndex with
| false, _ -> failwith "Wanted to execute a nonexistent instruction"
| true, executingInstruction ->
@@ -1960,7 +2048,7 @@ module AbstractMachine =
executingInstruction
)
match instruction.ExecutingMethod.Locations.[instruction.IlOpIndex] with
match instruction.ExecutingMethod.Instructions.Value.Locations.[instruction.IlOpIndex] with
| IlOp.Nullary op -> executeNullary state thread op
| IlOp.UnaryConst unaryConstIlOp -> executeUnaryConst state thread unaryConstIlOp |> ExecutionResult.Stepped
| IlOp.UnaryMetadataToken (unaryMetadataTokenIlOp, bytes) ->

View File

@@ -402,14 +402,17 @@ module Assembly =
let print (main : MethodDefinitionHandle) (dumped : DumpedAssembly) : unit =
for KeyValue (_, typ) in dumped.TypeDefs do
printfn "\nType: %s.%s" typ.Namespace typ.Name
Console.WriteLine $"\nType: %s{typ.Namespace}.%s{typ.Name}"
for method in typ.Methods do
if method.Handle = main then
printfn "Entry point!"
Console.WriteLine "Entry point!"
printfn "\nMethod: %s" method.Name
Console.WriteLine $"\nMethod: %s{method.Name}"
method.Instructions
|> List.map (fun (op, index) -> IlOp.Format op index)
|> List.iter Console.WriteLine
match method.Instructions with
| None -> Console.WriteLine "<no IL instructions>"
| Some instructions ->
instructions.Instructions
|> List.map (fun (op, index) -> IlOp.Format op index)
|> List.iter Console.WriteLine

View File

@@ -60,7 +60,7 @@ type CliValueType =
| Float64 of float
[<RequireQualifiedAccess>]
type CliRuntimePointerSource = | LocalVariable of source : unit * whichVar : uint16
type CliRuntimePointerSource = | LocalVariable of sourceThread : ThreadId * methodFrame : int * whichVar : uint16
type CliRuntimePointer =
| Unmanaged of unit

View File

@@ -3,9 +3,7 @@ namespace WoofWare.PawPrint
open Microsoft.FSharp.Core
type ManagedPointerSource =
// TODO: need to somehow obtain a "pointer" to the dynamically-updating contents of that stack frame.
// Perhaps we should store it as a list of historical values?
| LocalVariable of source : unit * whichVar : uint16
| LocalVariable of sourceThread : ThreadId * methodFrame : int * whichVar : uint16
| Heap of ManagedHeapAddress
| Null
@@ -90,8 +88,13 @@ module EvalStackValue =
match popped with
| EvalStackValue.ManagedPointer ptrSource ->
match ptrSource with
| ManagedPointerSource.LocalVariable _ ->
failwith "TODO: trying to fit a local variable address into an ObjectRef"
| ManagedPointerSource.LocalVariable (sourceThread, methodFrame, whichVar) ->
CliType.RuntimePointer (
CliRuntimePointer.Managed (
CliRuntimePointerSource.LocalVariable (sourceThread, methodFrame, whichVar)
)
)
// failwith "TODO: trying to fit a local variable address into an ObjectRef"
| ManagedPointerSource.Heap managedHeapAddress -> CliType.ObjectRef (Some managedHeapAddress)
| ManagedPointerSource.Null -> CliType.ObjectRef None
| EvalStackValue.NativeInt nativeIntSource ->
@@ -113,9 +116,11 @@ module EvalStackValue =
match src with
| ManagedPointerSource.Heap addr -> CliType.OfManagedObject addr
| ManagedPointerSource.Null -> CliType.ObjectRef None
| ManagedPointerSource.LocalVariable (source, var) ->
| ManagedPointerSource.LocalVariable (sourceThread, methodFrame, var) ->
CliType.RuntimePointer (
CliRuntimePointer.Managed (CliRuntimePointerSource.LocalVariable (source, var))
CliRuntimePointer.Managed (
CliRuntimePointerSource.LocalVariable (sourceThread, methodFrame, var)
)
)
| _ -> failwith $"TODO: %O{popped}"
| CliType.Char _ -> failwith "TODO: char"
@@ -131,7 +136,7 @@ type EvalStack =
}
static member Pop (stack : EvalStack) : EvalStackValue * EvalStack =
eprintfn $"Popping value from stack"
System.Console.Error.WriteLine "Popping value from stack"
match stack.Values with
| [] -> failwith "eval stack was empty on pop instruction"
@@ -146,7 +151,7 @@ type EvalStack =
static member Peek (stack : EvalStack) : EvalStackValue option = stack.Values |> List.tryHead
static member Push' (v : EvalStackValue) (stack : EvalStack) : EvalStack =
eprintfn $"Pushing value {v} to stack"
System.Console.Error.WriteLine $"Pushing value {v} to stack"
{
Values = v :: stack.Values
@@ -181,7 +186,9 @@ type EvalStack =
| CliRuntimePointer.Unmanaged () -> failwith "todo: unmanaged"
| CliRuntimePointer.Managed ptr ->
match ptr with
| CliRuntimePointerSource.LocalVariable (source, var) ->
EvalStackValue.ManagedPointer (ManagedPointerSource.LocalVariable (source, var))
| CliRuntimePointerSource.LocalVariable (sourceThread, methodFrame, var) ->
EvalStackValue.ManagedPointer (
ManagedPointerSource.LocalVariable (sourceThread, methodFrame, var)
)
EvalStack.Push' v stack

View File

@@ -84,6 +84,29 @@ module GenericParameter =
)
|> ImmutableArray.CreateRange
type MethodInstructions =
{
/// <summary>
/// The IL instructions that compose the method body, along with their offset positions.
/// Each tuple contains the instruction and its offset in the method body.
/// </summary>
Instructions : (IlOp * int) list
/// <summary>
/// A map from instruction offset (program counter) to the corresponding IL operation.
/// This is the inverse of Instructions for efficient lookup.
/// </summary>
Locations : Map<int, IlOp>
/// <summary>
/// Whether local variables in this method should be initialized to their default values.
/// This corresponds to the localsinit flag in the method header.
/// </summary>
LocalsInit : bool
LocalVars : ImmutableArray<TypeDefn> option
}
/// <summary>
/// Represents detailed information about a method in a .NET assembly.
/// This is a strongly-typed representation of MethodDefinition from System.Reflection.Metadata.
@@ -104,16 +127,11 @@ type MethodInfo =
Name : string
/// <summary>
/// The IL instructions that comprise the method body, along with their offset positions.
/// Each tuple contains the instruction and its offset in the method body.
/// The IL instructions that compose the method body, along with their offset positions.
///
/// There may be no instructions for this method, e.g. if it's an `InternalCall`.
/// </summary>
Instructions : (IlOp * int) list
/// <summary>
/// A map from instruction offset (program counter) to the corresponding IL operation.
/// This is the inverse of Instructions for efficient lookup.
/// </summary>
Locations : Map<int, IlOp>
Instructions : MethodInstructions option
/// <summary>
/// The parameters of this method.
@@ -131,17 +149,13 @@ type MethodInfo =
Signature : TypeMethodSignature<TypeDefn>
/// <summary>
/// Whether this method is implemented as a platform invoke (P/Invoke) to unmanaged code.
/// Custom attributes defined on the method. I've never yet seen one of these in practice.
/// </summary>
IsPinvokeImpl : bool
CustomAttributes : WoofWare.PawPrint.CustomAttribute ImmutableArray
/// <summary>
/// Whether local variables in this method should be initialized to their default values.
/// This corresponds to the localsinit flag in the method header.
/// </summary>
LocalsInit : bool
MethodAttributes : MethodAttributes
LocalVars : ImmutableArray<TypeDefn> option
ImplAttributes : MethodImplAttributes
/// <summary>
/// Whether this method is static (true) or an instance method (false).
@@ -149,6 +163,19 @@ type MethodInfo =
IsStatic : bool
}
/// <summary>
/// Whether this method's implementation is directly supplied by the CLI, rather than being loaded
/// from an assembly as IL.
/// </summary>
member this.IsCliInternal : bool =
this.ImplAttributes.HasFlag MethodImplAttributes.InternalCall
/// <summary>
/// Whether this method is implemented as a platform invoke (P/Invoke) to unmanaged code.
/// </summary>
member this.IsPinvokeImpl : bool =
this.MethodAttributes.HasFlag MethodAttributes.PinvokeImpl
[<RequireQualifiedAccess>]
module MethodInfo =
type private Dummy = class end
@@ -494,19 +521,47 @@ module MethodInfo =
(methodHandle : MethodDefinitionHandle)
: MethodInfo option
=
let logger = loggerFactory.CreateLogger "MethodInfo"
let assemblyName = metadataReader.GetAssemblyDefinition().GetAssemblyName ()
let methodDef = metadataReader.GetMethodDefinition methodHandle
let methodName = metadataReader.GetString methodDef.Name
let methodSig = methodDef.DecodeSignature (TypeDefn.typeProvider, ())
let methodBody = readMethodBody peReader metadataReader methodDef
let implAttrs = methodDef.ImplAttributes
let methodBody =
if
implAttrs.HasFlag MethodImplAttributes.InternalCall
|| implAttrs.HasFlag MethodImplAttributes.Runtime
then
None
elif methodDef.Attributes.HasFlag MethodAttributes.PinvokeImpl then
None
else
match readMethodBody peReader metadataReader methodDef with
| None ->
logger.LogDebug $"no method body in {assemblyName.Name} {methodName}"
None
| Some body ->
{
MethodInstructions.Instructions = body.Instructions
Locations = body.Instructions |> List.map (fun (a, b) -> b, a) |> Map.ofList
LocalsInit = body.LocalInit
LocalVars = body.LocalSig
}
|> Some
let declaringType = methodDef.GetDeclaringType ()
match methodBody with
| None ->
let logger = loggerFactory.CreateLogger typeof<Dummy>.DeclaringType
logger.LogDebug ("No method body for {MethodWithoutBody}", metadataReader.GetString methodDef.Name)
None
| Some methodBody ->
let attrs =
let result = ImmutableArray.CreateBuilder ()
let attrs = methodDef.GetCustomAttributes ()
for attr in attrs do
metadataReader.GetCustomAttribute attr
|> CustomAttribute.make attr
|> result.Add
result.ToImmutable ()
let typeSig = TypeMethodSignature.make methodSig
@@ -519,14 +574,13 @@ module MethodInfo =
DeclaringType = (declaringType, assemblyName)
Handle = methodHandle
Name = methodName
Instructions = methodBody.Instructions
Locations = methodBody.Instructions |> List.map (fun (a, b) -> b, a) |> Map.ofList
Instructions = methodBody
Parameters = methodParams
Generics = methodGenericParams
Signature = typeSig
IsPinvokeImpl = methodDef.Attributes.HasFlag MethodAttributes.PinvokeImpl
LocalsInit = methodBody.LocalInit
LocalVars = methodBody.LocalSig
MethodAttributes = methodDef.Attributes
CustomAttributes = attrs
IsStatic = not methodSig.Header.IsInstance
ImplAttributes = implAttrs
}
|> Some

View File

@@ -109,12 +109,19 @@ module Program =
| TypeDefn.PrimitiveType PrimitiveType.Int32 -> ()
| _ -> failwith "Main method must return int32; other types not currently supported"
// TODO: now overwrite the main thread which we used for object initialisation. The below is not right.
let state, mainThread =
state
|> IlMachineState.addThread
(MethodState.Empty mainMethod (ImmutableArray.Create (CliType.OfManagedObject arrayAllocation)) None)
dumped.Name
// Now that BCL initialisation has taken place, overwrite the main thread completely.
let methodState =
MethodState.Empty mainMethod (ImmutableArray.Create (CliType.OfManagedObject arrayAllocation)) None
let threadState =
{ state.ThreadState.[mainThread] with
MethodStates = ImmutableArray.Create methodState
}
let state =
{ state with
ThreadState = state.ThreadState |> Map.add mainThread threadState
}
let rec go (state : IlMachineState) =
match AbstractMachine.executeOneStep loggerFactory baseClassTypes state mainThread with
@@ -122,7 +129,9 @@ module Program =
| ExecutionResult.Stepped (state', whatWeDid) ->
match whatWeDid with
| WhatWeDid.Executed -> logger.LogInformation "Executed one step."
| WhatWeDid.Executed ->
logger.LogInformation
$"Executed one step; active assembly: {state'.ActiveAssembly(mainThread).Name.Name}."
| WhatWeDid.SuspendedForClassInit ->
logger.LogInformation "Suspended execution of current method for class initialisation."
| WhatWeDid.BlockedOnClassInit threadBlockingUs ->

View File

@@ -21,9 +21,9 @@
<Compile Include="TypeInfo.fs" />
<Compile Include="Assembly.fs" />
<Compile Include="Corelib.fs" />
<Compile Include="AbstractMachineDomain.fs" />
<Compile Include="BasicCliType.fs" />
<Compile Include="ManagedHeap.fs" />
<Compile Include="AbstractMachineDomain.fs" />
<Compile Include="TypeInitialisation.fs" />
<Compile Include="EvalStack.fs" />
<Compile Include="AbstractMachine.fs" />