I have been playing around with Mono.Cecil recently, mainly for a compiler that I'm planning on writing. I started by trying out the code in this answer. Here is the code:
var myHelloWorldApp = AssemblyDefinition.CreateAssembly(
new AssemblyNameDefinition("HelloWorld", new Version(1, 0, 0, 0)), "HelloWorld", ModuleKind.Console);
var module = myHelloWorldApp.MainModule;
// create the program type and add it to the module
var programType = new TypeDefinition("HelloWorld", "Program",
Mono.Cecil.TypeAttributes.Class | Mono.Cecil.TypeAttributes.Public, module.TypeSystem.Object);
module.Types.Add(programType);
// add an empty constructor
var ctor = new MethodDefinition(".ctor", Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.HideBySig
| Mono.Cecil.MethodAttributes.SpecialName | Mono.Cecil.MethodAttributes.RTSpecialName, module.TypeSystem.Void);
// create the constructor's method body
var il = ctor.Body.GetILProcessor();
il.Append(il.Create(OpCodes.Ldarg_0));
// call the base constructor
il.Append(il.Create(OpCodes.Call, module.ImportReference(typeof(object).GetConstructor(Array.Empty<Type>()))));
il.Append(il.Create(OpCodes.Nop));
il.Append(il.Create(OpCodes.Ret));
programType.Methods.Add(ctor);
// define the 'Main' method and add it to 'Program'
var mainMethod = new MethodDefinition("Main",
Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Static, module.TypeSystem.Void);
programType.Methods.Add(mainMethod);
// add the 'args' parameter
var argsParameter = new ParameterDefinition("args",
Mono.Cecil.ParameterAttributes.None, module.ImportReference(typeof(string[])));
mainMethod.Parameters.Add(argsParameter);
// create the method body
il = mainMethod.Body.GetILProcessor();
il.Append(il.Create(OpCodes.Nop));
il.Append(il.Create(OpCodes.Ldstr, "Hello World"));
var writeLineMethod = il.Create(OpCodes.Call,
module.ImportReference(typeof(Console).GetMethod("WriteLine", new[] { typeof(string) })));
// call the method
il.Append(writeLineMethod);
il.Append(il.Create(OpCodes.Nop));
il.Append(il.Create(OpCodes.Ret));
// set the entry point and save the module
myHelloWorldApp.EntryPoint = mainMethod;
myHelloWorldApp.Write("HelloWorld.exe");
Note that I changed module.Import
to module.ImportReference
since the former is apparently obsoleted.
I put this into a .NET 5 project, and this created a HelloWorld.exe
. Since I was on macOS, I tried running the exe with mono:
mono HelloWorld.exe
And it printed "Hello World". So far so good.
The problem arises, when I sent this HelloWorld.exe
to my friend who is on a Windows machine. When he runs it like this (Note that he doesn't have mono on Windows):
.\HelloWorld.exe
It outputs the error:
Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'System.Console, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependences. The system cannot find the file specified.
at
HelloWorld.Program.Main(String[] args)
I tried to look up the error message, but all the results were about not able to find System.Runtime
. And is System.Console
even an assembly? Isn't it a class?
How can I run the exe on a Windows Machine? Is there something I need to change in the code? Or is there something that the Windows machine needs to install? I think this might have to do with me using .NET 5, but the Windows machine only has .NET Framework.
Question ends there, below are my findings:
As the "control group", I tried doing the simplest Hello World C# program:
class Program {
public static void Main() {
System.Console.WriteLine("Hello World");
}
}
Compiling that with csc
on macOS (i.e. the Mono compiler), and running the output exe on Windows. This works, so I disassembled (using dotnet-ildasm
) both the exe produced by Mono.Cecil and the exe produced by csc
, and compared them. The most interesting difference I found is that there are these extra assembly references in the "broken" exe:
.assembly extern System.Private.CoreLib
{
.publickeytoken = ( 7C EC 85 D7 BE A7 79 8E ) // ....y.
.ver 5:0:0:0
}
.assembly extern System.Console
{
.publickeytoken = ( B0 3F 5F 7F 11 D5 0A 3A ) // .._.....
.ver 5:0:0:0
}
If you use typeof(...)
, you are importing types from the current runtime (.NET 5). I guess you want to compile for .NET Framework, because that's installed on Windows. You need to import mscorlib from .NET Framework on Windows, or mono (no idea where it is on Mac):
// Load mscorlib from .NET framework or mono
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll (Windows, 64-bit)
// C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll (Windows, 32-bit)
// /usr/lib/mono/4.5/mscorlib.dll (Linux)
// /usr/lib/mono/4.8-api/mscorlib.dll (Linux, reference-only)
var mscorlib = ModuleDefinition.ReadModule("/usr/lib/mono/4.8-api/mscorlib.dll");
When you call the base constructor, call the constructor of the object in the loaded mscorlib, not the .NET 5 one:
// module.TypeSystem.Object.Resolve() will resolve to .NET 5
var objectCtor = mscorlib.GetType("System.Object").Methods.Where(m => m.IsConstructor && !m.HasParameters).First();
// call the base constructor
il.Append(il.Create(OpCodes.Call, module.ImportReference(objectCtor)));
Again, do not import string from the current runtime:
// add the 'args' parameter
var argsParameter = new ParameterDefinition("args",
Mono.Cecil.ParameterAttributes.None, new ArrayType(module.TypeSystem.String));
If you look up System.Console docs, you see that it's in mscorlib, so you can get the type reference:
// Obtain System.Console from .NET Framework
var netfxConsole = mscorlib.GetType("System.Console");
// Obtain the correct WriteLine method, there might be a better way
var writeLine = netfxConsole
.Methods
.Where(m => m.Name == "WriteLine" && m.Parameters.Count == 1 && m.Parameters[0].ParameterType == mscorlib.TypeSystem.String)
.First();
// Use .NET Framework's Console.WriteLine
var writeLineMethod = il.Create(OpCodes.Call, module.ImportReference(writeLine));
And this works with mono (Linux) and on Windows (VM)
However, the disassemblers (monodis, ilspycmd) show that there are 2 mscorlib
s referenced (one from module.TypeSystem
and one from the loaded mscorlib
, which seem to be the same):
.assembly extern mscorlib
{
.ver 4:0:0:0
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
}
.assembly extern mscorlib
{
.ver 4:0:0:0
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
}
.assembly 'HelloWorld'
{
.hash algorithm 0x00000000
.ver 1:0:0:0
}
.module HelloWorld // GUID = {CB393199-8BA7-4CB5-8B55-1F81ED0FA68C}