The simplest possible code example that anyone can learn from.
When I first started learning IL rewriting and Cecil about several years ago, one of the difficulties that I struggled with was the fact that there were very few practical examples on how to take an existing assembly and modify it at runtime. In many ways, I was stranded in heavily undocumented territory, and needless to say, this lack of documentation made it very difficult to learn how to do anything useful with Cecil.
Meanwhile, in the Year 2011…
It’s now 2011, and I think it’s safe to say that for many people, IL rewriting (much less Cecil) is still a “big mystery wrapped in an enigma containing frustration”. Indeed, Cecil is an incredible library that can let you do some incredible things, but at the same time, it can be very frustrating since the learning curve is still steep and there are still no practical guides for using it. As a user, there must be some sample code out there that shows how to do the most basic tasks with Cecil, right?
Establishing the Feedback Loop
In order to learn any skill (such as IL rewriting), we need to establish a simple feedback loop that allows users to easily experiment with the tools they are given so that they know:
- What went wrong if it doesn’t work
- Where to fix it if it breaks
- How to see the results of their experiments without getting mired in the implementation details of the tests themselves
In this case, we’ll need to set up a basic environment that will let users experiment and learn how to modify assemblies at runtime with Cecil. We will need:
- A test fixture that loads a sample assembly and gives users the chance to modify it before reloading the modified assembly into memory (An NUnit base fixture)
- A way to display/diagnose any invalid assembly errors that occur due to making changes to the original assembly (PEVerify)
- To make it easy to change so that we can experiment with different approaches to modifying IL, thus “closing” the feedback loop
Lost in Bytecode
Given these requirements, where would we even begin? It’s not every day that one decides to randomly parse .NET assemblies and learn how to change the underlying bytecode that ultimately defines their behavior. This can seem like a daunting task for even the most intelligent of budding interlopers, but fortunately for my readers, most of the work has already been done for you in these examples. All you need to do is sit back and scroll down the page, as I proceed to tell you the “ins” and “outs” about Cecil, and the practical lessons learned from rewriting IL. With that in mind, let’s get started!
A WriteLine for Another WriteLine
One of the simplest things that you can possibly do with Cecil is to swap a single static method call for another static method call with the same parameters and the same return type. (It’s a fairly simple operation since both methods have the same signature, and you don’t need to add any additional instructions to make it happen). In this case, I opted to swap all calls to Console.WriteLine() with calls to FakeConsole.WriteLine():
As you can see from the example above, I used a simple LINQ query to identify all the call instructions that needed to be modified. More experienced Cecil users will probably notice that I decided to rewrite all method calls to point to FakeConsole.WriteLine() instead of individually checking to make sure that the method call I was replacing was indeed Console.WriteLine(). Indeed, that was an intentional move, given that the FizzBuzz.Print() method doesn’t make any other external calls to any other methods besides Console.WriteLine().
Assuming that I somehow created an instruction that caused an invalid modified assembly, however, how would I be able to know what went wrong, much less know how to fix it?
PEVerify, how do I love
and hate thee…
As it turns out, there is a tool called PEVerify.exe that can tell you whether or not the assemblies that you modify with Cecil are valid or invalid. For example, if I were to remove all the IL instructions out of the FizzBuzz.Print() method, PEVerify would give me the following error message:
(Believe me, it’s much prettier when it’s zoomed out)
PEVerify will examine any given assembly and be able to tell you whether or not the compiler (or in this case, you, the human compiler) made any mistakes in creating the assembly. It can be a very useful tool, and that’s why I modified the sample test fixture to run PEVerify right after the user modifies the sample assembly. If you don’t already have PEVerify installed, make sure you download it and configure the Lesson1 app.config file to point to where PEVerify is installed:
Once PEVerify has been configured as part of the tests, the rest is up to your imagination.
Exploring Method Replacement and Beyond
Now that the basic IL rewriting setup has been laid out for you, the onus is on you to explore the possibilities with Cecil and IL rewriting, even if it means that you have to start with some small, basic steps. In the next installment in this series, I’ll show you how to use PEVerify and Cecil to keep the stack balanced so that you can do things like swap static method calls for instance method calls, and even do things like install runtime hooks so you can change your code as your application is running. Stay tuned!