In the last post, we looked at using Roslyn to generate deltas between two compilations. Today we’ll take a look at how we can apply these deltas to a running process.
The CLR API
If you dig through Microsoft’s .NET Reference source occasionally you’ll come across extern
methods like FastAllocateString()
decorated with a special attribute: [MethodImplAttribute(MethodImplOptions.InternalCall)]
. These are entry points to the CLR that can be called from managed code. Calling into the CLR can be done for a number of reasons. In the case of FastAllocateString
it’s to implement certain functionality in native code for performance (in this case without even the overhead of P/Invoke). Other entry points are exposed to trigger CLR behavior like garbage collection or to apply deltas to a running process.
When I started this project I wasn’t even aware the CLR had an API. Fortunately Microsoft has recently released internal documentation that explains much of the CLR’s behavior including these APIs. Mscorlib and Calling Into the Runtime documents the differences between FCall, QCall and P/Invoke as entrypoints to the CLR from managed code.
Managing the many methods, classes and interfaces is a huge pain and too much work to do manually when starting out. Luckily Microsoft has released a managed wrapper that makes a lot of this stuff easier to work with. The Managed Debug Sample (mdbg) has everything we’ll need to attach to a process and apply changes to it.
The sample has a few extra projects. For our purposes we’ll need:
- corapi – The managed API we’ll interact with directly
- raw – Set of interfaces and COMImports over the ICorDebug API
- NativeDebugWrappers – Functionality for low level Windows debugging
Game Plan
At a high level, our approach is going to be the following:
- Create an instance of
CorDebugger
, a debugger we can use to create and attach itself to other processes. - Start a remote process
- Intercept loading of modules and mark them for Edit and Continue
- Apply deltas
CorDebugger
Creating an instance of the debugger is fairly involved. We first have to get an instance of the CLR host based on the version of the runtime we’re interested in (in our case anything after v4.0 will work). Working with the managed API is still awkward, certain types are created based on GUIDs that seem to be undocumented outside of sample code. Nonetheless the following code creates an instance of a managed debugger we can use.
In the following we get a list of runtimes available from the currently running process. I can’t offer insight into whether this is “good” or “bad” but it’s something to be aware of.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static CorDebugger GetDebugger() | |
{ | |
Guid classId = new Guid("9280188D-0E8E-4867-B30C-7FA83884E8DE"); | |
Guid interfaceId = new Guid("D332DB9E-B9B3-4125-8207-A14884F53216"); | |
dynamic rawMetaHost; | |
Microsoft.Samples.Debugging.CorDebug.NativeMethods.CLRCreateInstance(ref classId, ref interfaceId, out rawMetaHost); | |
ICLRMetaHost metaHost = (ICLRMetaHost)rawMetaHost; | |
var currentProcess = Process.GetCurrentProcess(); | |
var runtime_v40 = GetLoadedRuntimeByVersion(metaHost, currentProcess.Id, "v4.0"); | |
var debuggerClassId = new Guid("DF8395B5-A4BA-450B-A77C-A9A47762C520"); | |
var debuggerInterfaceId = new Guid("3D6F5F61-7538-11D3-8D5B-00104B35E7EF"); | |
//Get a debugger for this version of the runtime. | |
Object res = runtime_v40.m_runtimeInfo.GetInterface(ref debuggerClassId, ref debuggerInterfaceId); | |
ICorDebug debugger = (ICorDebug)res; | |
//We create CorDebugger that wraps the ICorDebug stuff making it easier to use | |
var corDebugger = new CorDebugger(debugger); | |
return corDebugger; | |
} | |
public static CLRRuntimeInfo GetLoadedRuntimeByVersion(ICLRMetaHost metaHost, Int32 processId, string version) | |
{ | |
IEnumerable<CLRRuntimeInfo> runtimes = EnumerateLoadedRuntimes(metaHost, processId); | |
foreach (CLRRuntimeInfo rti in runtimes) | |
{ | |
//Search through all loaded runtimes for one that starts with v4.0. | |
if (rti.GetVersionString().StartsWith(version, StringComparison.OrdinalIgnoreCase)) | |
{ | |
return rti; | |
} | |
} | |
return null; | |
} | |
public static IEnumerable<CLRRuntimeInfo> EnumerateLoadedRuntimes(ICLRMetaHost metaHost, Int32 processId) | |
{ | |
List<CLRRuntimeInfo> runtimes = new List<CLRRuntimeInfo>(); | |
IEnumUnknown enumRuntimes; | |
//We get a handle for the process and then get all the runtimes available from it. | |
using (ProcessSafeHandle hProcess = NativeMethods.OpenProcess((int)(NativeMethods.ProcessAccessOptions.ProcessVMRead | | |
NativeMethods.ProcessAccessOptions.ProcessQueryInformation | | |
NativeMethods.ProcessAccessOptions.ProcessDupHandle | | |
NativeMethods.ProcessAccessOptions.Synchronize), | |
false, // inherit handle | |
processId)) | |
{ | |
if (hProcess.IsInvalid) | |
{ | |
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); | |
} | |
enumRuntimes = metaHost.EnumerateLoadedRuntimes(hProcess); | |
} | |
// Since we're only getting one at a time, we can pass NULL for count. | |
// S_OK also means we got the single element we asked for. | |
for (object oIUnknown; enumRuntimes.Next(1, out oIUnknown, IntPtr.Zero) == 0; /* empty */) | |
{ | |
runtimes.Add(new CLRRuntimeInfo(oIUnknown)); | |
} | |
return runtimes; | |
} |
Starting the process
Once we’ve got a hold of our debugger, we can use it to start a process. While working on this I learned that we (in the .NET world) have been shielded from some of the peculiarities of creating a process on Windows. These peculiarities start to bleed through when creating processes with our custom debugger.
For example, if we want to send the argument 123456 to our new process, it turns our we have to send the process’ filename as the first argument as well. So the call to ICorDebug::CreateProcess(string applicationName, string commandLine)
ends up looking something like
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var applicationName = "myProcess.exe"; | |
var commandLineArgs = "myProcess.exe 123456"; //Note: Repeat application name in arguments | |
debugger.CreateProcess(applicationName, commandLineArgs, … ); //Ignoring other arguments for simplicity |
For more on this Mike Stall has a post on Conventions for passing the arguments to a process.
We also have to manually pass process flags when creating our process. These flags dictate various properties for our new process (Should a new window be created? Should we debug child processes of this process? etc.). Below we start a process, assuming that the application is in the current directory.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static CorProcess StartProcess(CorDebugger debugger, string programName) | |
{ | |
var currentDirectory = Directory.GetCurrentDirectory(); | |
//const CREATE_NO_WINDOW = 0x08000000 Use this to create process without a console | |
var corProcess = debugger.CreateProcess(programName, "", currentDirectory, (int)CreateProcessFlags.CREATE_NEW_CONSOLE); | |
corProcess.Continue(outOfBand: false); | |
return corProcess; | |
} |
Mark Modules for Edit and Continue
By default the CLR doesn’t expect that EnC will be enabled. In order to enable it, we’ll have to manually set JIT flags on each module we’re interested in. CorDebug
exposes an event that signals when a module has been loaded, so we’ll use this to control the flags.
A sample event handler for module loading might look like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static void CorProcess_OnModuleLoad(object sender, CorModuleEventArgs e) | |
{ | |
var module = e.Module; | |
if (!module.Name.Contains("myProcess.exe")) | |
{ | |
return; | |
} | |
var compilerFlags = module.JITCompilerFlags; | |
module.JITCompilerFlags = CorDebugJITCompilerFlags.CORDEBUG_JIT_ENABLE_ENC; | |
} |
Notice in the above that we’re only setting the flag for the module we’re interested in. If we try to set the JIT flags for all modules we’ll run into exceptions when working with NGen-ed modules. The exception is a little cryptic and complains about “Zap Modules” but this turns out just to be the internal name for NGen modules.
Applying the Deltas
Finally. After three blog posts we’ve arrived at the point: Actually manipulating the running process.
In truth, we don’t apply our changes directly to the process, but to an individual module within it. So our first task is to find the individual module we’re want to change. We can search through all AppDomains, assemblies and modules to find the module with the correct name.
Once we find the module we want to request metadata about the module from it. This turns out to be a weird implementation detail in which the CLR assumes you can’t possible want to apply changes unless you’ve requested this info previously. We put this all together into the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//See part two for how to generate these two | |
byte[] metadataBytes = …; | |
byte[] ilBytes = …; | |
//Find module by name | |
var appDomain = corProcess.AppDomains.Cast<CorAppDomain>().Single(); | |
var assembly = appDomain.Assemblies.Cast<CorAssembly>().Where(n => n.Name.Contains("MyProgram")).Single(); | |
var module = assembly.Modules.Cast<CorModule>().Single(); | |
//I found a bug in the ICorDebug API. Apparently the API assumes that you couldn't possibly have a change to apply | |
//unless you had first fetched the metadata for this module. Perhaps reasonable in the overall scenario, but | |
//its certainly not OK to simply throw an AV exception if it hadn't happened yet. | |
// | |
//In any case, fetching the metadata is a thankfully simple workaround | |
object import = module.GetMetaDataInterface(typeof(IMetadataImport).GUID); | |
corProcess.Stop(–1); | |
module.ApplyChanges(metadataBytes, ilBytes); | |
corProcess.Continue(outOfBand: false); |
Remapping
I should at least touch on one more aspect of EnC I’ve glossed over thus far: remapping. If you are changing a method that has currently active statements, you will be given an opportunity to remap the current “Instruction Pointer” based on line number. It’s up to you to decide on which line execution should resume. The CorDebugger exposes OnFunctionRemapOpportunity
and OnFunctionRemapComplete
as events that allow you to guide remapping.
Here’s a sample remapping event handler:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static void CorProcess_OnFunctionRemapOpportunity(object sender, CorFunctionRemapOpportunityEventArgs e) | |
{ | |
//A remap opportunity is where the runtime can hijack the thread IP from the old version of the code and | |
//put it in the new version of the code. However the runtime has no idea how the old IL relates to the new | |
//IL, so it needs the debugger to tell it which new IL offset in the updated IL is the semantically equivalent of | |
//old IL offset the IP is at right now. | |
Console.WriteLine("The debuggee has hit a remap opportunity at: " + e.OldFunction + ":" + e.OldILOffset); | |
//I have no idea what this new IL looks like either, but lets start at the beginning of the method once again | |
int newILOffset = e.OldILOffset; | |
var canSetIP = e.Thread.ActiveFrame.CanSetIP(newILOffset); | |
Console.WriteLine("Can set IP to: " + newILOffset + " : " + canSetIP); | |
e.Thread.ActiveFrame.RemapFunction(newILOffset); | |
Console.WriteLine("Continuing the debuggee in the updated IL at IL offset: " + newILOffset); | |
} |
We’ve now got all the pieces necessary to manipulate a running process and a good base to build off of. Complete code for today’s blog post can be found here on GitHub. Leave any questions in the comments and I’ll do my best to answer them or direct you to someone who can at Microsoft.
That’s awesome blog series!
I’m developing a game engine powered by Roslyn compiler for live-editing features. I was playing with EmitDifference() a bit but gave up when I was unable to find the way to get list of SemanticEdit. So currently I’m simply compiling and loading a completely new assembly every time the source code changes. This works, but it will be much better if I find the way to apply deltas (when possible).
Now I can emit deltas, but are there any way to apply them to the already running process (with already attached Visual Studio debugger)?
Regards!
I actually wanted to almost the exact same thing. I was told by someone at Microsoft that only one debugger can be attached to a process at a given time. 😦
What I’ve also considered is:
1. Breaking the process from a VS extension using debugger APIs (Similar to Break All)
2. Applying the changes directly to source
3. Resuming the process using debugger APIs.
If you figure out something that works I’d love to hear about it.
Yes, that’s exactly what I want to do. In my case debugger could be already attached or might be attached/detached any time later (to/from process with applied deltas). So basically I need to directly “patch” any assembly/module loaded in Runtime without relying on debugger at all.
I’ve noticed that if I do any changes to a process in the regular way (I mean VS debug-pause-edit-save-continue routine), I cannot detach from the process without terminating it because of this error: “Unable to detach from one or more processes. Detach is not allowed after changes have been applied through Edit and Continue.”
I’m afraid that CLR doesn’t support the workflow we need – it requires attached debugger, module marked as Edit-and-Continue compatible during its load… so it seems we’re out of luck here.
Maybe we will have more luck with CoreCLR? It seems to be much more opened and straightforward. There is a special assembly-attribute to setup JIT options, which allows to enable Edit-and-Continue https://dotnet.github.io/api/System.Diagnostics.DebuggableAttribute.DebuggingModes.html#System_Diagnostics_DebuggableAttribute_DebuggingModes – and the source code is available https://github.com/dotnet/coreclr – however I cannot find any API or test sample for applying PE-deltas.
Regards!