Injecting Code Dynamically with the Debugging API
This section describes dynamic code injection by using the common language runtime (CLR) debugging API. Dynamic code injection executes a function that was not present in the original portable executable (PE) file. For example, you can use dynamic code injection to execute an expression in the Immediate window while you are debugging in the Microsoft Visual Studio integrated development environment (IDE). The CLR hijacks an active thread to execute the code. The debugger may request the CLR to run or freeze the remaining threads. Because dynamic code injection is built on function evaluation, you can debug a dynamically injected function as if it were regular code. That is, you can invoke all standard debugging services such as setting breakpoints, stepping, and so on for the dynamically injected code.
A debugger must do the following to dynamically inject code:
Compose a function that will execute the dynamic code.
Inject the code into the debuggee by using Edit and Continue.
Execute the code, repeatedly if you want, by invoking the composed function.
The following subsections describe these steps in detail.
Composing the Function
The first step is to compute the signature for the function that will execute the dynamic code. To do this, append the signature for the local variables to the signature for the method that is being executed in the leaf frame. A signature that is constructed in this manner does not require a computation of the minimal subset of used variables. The runtime will ignore the unused variables. See "Getting the Signature from the Metadata" later in this topic for more information.
All arguments to the function must be declared to be ByRef. This will enable function evaluation to propagate any changes to the variables in the injected function back to the leaf frame in the debuggee.
Some variables may not be in scope when the dynamic code is executed. In such situations, you should pass a null reference. If you reference an out-of-scope variable, a NullReferenceException will be thrown. When this occurs, the debugger can complete function evaluation by calling the ICorDebugManagedCallback::EvalException callback.
Choose a unique name for the function. You must prefix the function name with the string "_Hidden:" so that the debugger will prevent a user from browsing the function. When this function is added, a flag will be set that indicates that the function name is special.
Injecting the Code
The debugger should ask the compiler to build a function whose body is the code that will be injected dynamically.
The debugger computes a delta portable executable (PE) file. To do this, the debugger calls the ICorDebugModule::GetEditAndContinueSnapshot method to obtain an ICorDebugEditAndContinueSnapshot object. The debugger calls ICorDebugEditAndContinueSnapshot methods to create the delta PE image and ICorDebugController::CommitChanges to install the delta PE in the running image.
The dynamically injected function should be placed at the same level of visibility as the leaf frame in which it will execute. If the leaf frame is an instance method, the dynamically injected function should also be an instance method in the same class. If the leaf frame is a static method, the dynamically injected function should also be a static method. Global methods are static methods that belong to a particular class.
The function will exist in the debuggee even after the dynamic code injection process is completed. This enables previously injected code to be reevaluated repeatedly without having to recompose the function and inject the code again. Therefore, the steps described in this section and in the previous section can be skipped for previously injected code.
Executing Injected Code
The first step is to get the value (an ICorDebugValue object) of each variable in the signature by using the debugging inspection routines. You can use either the ICorDebugThread::GetActiveFrame or the ICorDebugChain::GetActiveFrame method to get the leaf frame, and QueryInterface for ICorDebugILFrame. Call the ICorDebugILFrame::EnumerateLocalVariables, ICorDebugILFrame::EnumerateArguments, ICorDebugILFrame::GetLocalVariable, or ICorDebugILFrame::GetArgument method to get the actual variables.
If the debugger is attached to a debuggee that does not have CORDBG_ENABLE set (that is, a debuggee that has not been collecting debugging information), the debugger will not be able to get an ICorDebugILFrame object, and therefore will not be able to collect values for the function evaluation.
It is not important whether the objects that are arguments to the dynamically injected function are dereferenced weakly or strongly. When the function is evaluated, the runtime will hijack the thread in which the injection occurred. This will leave the original leaf frame and all the original strong references on the stack. However, if all the references in the debuggee are weak, running the dynamic code injection may trigger a garbage collection that may cause the object to be garbage-collected.
Use the ICorDebugThread::CreateEval method to create an ICorDebugEval object. ICorDebugEval provides methods to evaluate a function. Call one of these methods. A method such as ICorDebugEval::CallFunction only sets up the function evaluation. The debugger must call ICorDebugController::Continue to run the debuggee and evaluate the function. When the evaluation has completed, the debugging services will call the ICorDebugManagedCallback::EvalComplete or ICorDebugManagedCallback::EvalException method to notify the debugger about the function evaluation.
If the function evaluation returns an object, the object will be strongly referenced.
If the dynamically injected code tries to dereference a null reference that is passed to the function that wraps the code, the CLR debugging services will call ICorDebugManagedCallback::EvalException. In response, the debugger can notify the user that it cannot evaluate the injected code.
If the debuggee is multithreaded and the debugger does not want any of the other threads running, the debugger should call ICorDebugController::SetAllThreadsDebugState and set the state of all threads to THREAD_SUSPEND except for the thread that is used for function evaluation. This setting may cause a deadlock, depending on what the dynamically injected code does.
Because .NET Framework security is determined by context policies, dynamic code injection will operate with the same security permissions and capabilities as the leaf frame, unless you specifically change security settings.
Dynamically injected functions can be added wherever the Edit and Continue feature allows functions to be added. A logical choice is to add them to the leaf frame.
There are no limits on the number of fields, instance methods, or static methods that you can add to a class by using Edit and Continue operations. The maximum amount of static data allowed is predefined and currently limited to 1 MB per module.
Non-local goto statements are not allowed in dynamically injected code.
Getting the Signature from the Metadata
Obtaining a MetaData Dispenser
The debugger calls the ICorDebugModule::GetMetaDataInterface method with the REFIID IID_IMetaDataDispenser to obtain a metadata dispenser.
Finding the Method by Using IMetaDataImport
The debugger calls IMetaDataImport::GetMethodProps to look up a method by using its token. The method token can be obtained by using the ICorDebugFunction::GetToken method. IMetaDataImport::GetMethodProps will return the signature of the method.
The debugger calls the IMetaDataImport::GetSigFromToken method to obtain the local signature, that is, the signature of the local variables. The debugger must supply the token for the signature of the local variables. The debugger can obtain this token by calling the ICorDebugFunction::GetLocalVarSigToken method.
Constructing the Function Signature
The format of the signature is documented in the "Type and Signature Encoding in Metadata" specification and takes precedence over the information in this overview.
The "Method Declaration" section in the specification describes the format for the method signature. The format is a single byte for the calling convention, followed by a single byte for the count of arguments, followed by a list of types. Each type can have a different size. If a variable argument calling convention is specified, the count of arguments will be the total number of arguments, that is, fixed arguments plus variable arguments. An ELEMENT_TYPE_SENTINEL byte marks where the fixed arguments end and the variable arguments begin.
The "Stand-Alone Signatures" section in the specification describes the format of local signatures. Stand-alone signatures do not use a variable argument calling convention.
After obtaining the method signature and the local signature, the debugger should allocate space for a new signature. The debugger should then iterate through the method signature. For each type, it should put the ELEMENT_TYPE_BYREF byte followed by the type in the new signature. The process should be repeated until either the end-of-method signature or a type marked ELEMENT_TYPE_SENTINEL is reached. Next, the debugger should copy the local signature types and mark each type ELEMENT_TYPE_BYREF. If the method signature has a variable argument calling convention, the debugger should copy those types and mark them ELEMENT_TYPE_BYREF. Finally, the debugger should update the count of arguments.