Quite often I receive random questions about dnlib from my friends. To be honest, I have no idea why they think I know the answers to life the universe and everything else. π So, in this series of posts I'll attempt to solve their problems - and hope that the solution helps someone else too.
So, today's question is:
We're trying to add a byte array to an assembly using dnlib. We wrote some code* but dnlib throws exception when saving modified assembly:
An unhandled exception of type 'dnlib.DotNet.Writer.ModuleWriterException' occurred in dnlib.dll
Additional information: Field System.Byte[] ::2026170854 (04000000) initial value size != size of field type
I gave the friend the standard answer - make a sample app, see how it looks and then implement it with dnlib. Seriously, how hard can it be? π
Well, array initialization in .NET is anything but simple.
How arrays are initialized in C#
Note - the following explanation is shamelessly copied from "Maximizing .NET Performance" by Nick Wienholt. It's a very nice book but getting little outdated. You can Google for "Apress.Maximizing.Dot.NET.Performance.eBook-LiB", if interested.
Value type array initialization in C# can be achieved in two distinct waysβinline with the array variable declaration, and through set operations on each individual array element, as shown in the following snippet:
//inline int[] arrInline = new int[]{0,1,2}; //set operation per element int[] arrPerElement = new int[3]; arrPerElement[0] = 0; arrPerElement[1] = 1; arrPerElement[2] = 2;
For a value type array that is initialized inline and has more than three elements, the C# compiler in both .NET 1.0 and .NET 1.1 generates a type named <PrivateImplementationDetails> that is added to the assembly at the root namespace level. This type contains nested value types that reference the binary data needed to initialize the array, which is stored in a .data section of the PE file. At runtime, the System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray method is called to perform a memory copy of the data referenced by the <PrivateImplementationDetails>Β nested structure into the array's memory location. The direct memory copy is roughly twice as fast for the initialization of a 20-by-20 element array of 64-bit integers, and array initialization syntax is generally cleaner for the inline initialization case.
Say what? You can read the text 3 times and still be no wiser. So, let's make a small sample application and disassemble it.
How array initialization looks in MSIL
Let's start with sample app that does nothing.
using System; class Program { static byte[] bla = new byte[] {1,2,3,4,5}; static void Main() { } }
Compile without optimizations, and disassemble using ildasm. And even after removing all extra stuff, there's still a lot of code & metadata for such a simple thing. π
.assembly hello {} .class private auto ansi beforefieldinit Program extends [mscorlib]System.Object { .field public static uint8[] bla .method private hidebysig specialname rtspecialname static void .cctor() cil managed { ldc.i4.5 newarr [mscorlib]System.Byte dup ldtoken field valuetype '<PrivateImplementationDetails>{E21EC13E-4669-42C8-B7A5-2EE7FBD85904}'/'__StaticArrayInitTypeSize=5' '<PrivateImplementationDetails>{E21EC13E-4669-42C8-B7A5-2EE7FBD85904}'::'$$method0x6000003-1' call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle) stsfld uint8[] Program::bla ret } } .data cil I_00002098 = bytearray (01 02 03 04 05) .class private auto ansi '<PrivateImplementationDetails>{E21EC13E-4669-42C8-B7A5-2EE7FBD85904}' extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .class explicit ansi sealed nested private '__StaticArrayInitTypeSize=5' extends [mscorlib]System.ValueType { .pack 1 .size 5 } .field static assembly valuetype '<PrivateImplementationDetails>{E21EC13E-4669-42C8-B7A5-2EE7FBD85904}'/'__StaticArrayInitTypeSize=5' '$$method0x6000003-1' at I_00002098 }
For one byte array that we declared, compiler created .data directive, 2 static fields, one class and one nested class. And it added a global static constructor. Yikes!
Implementing it in dnlib
Now that we know all the stuff that's required for an array, we can make a tool that will add byte array to an assembly of our choice. To make things simpler, I decided not to create a holder class (named <PrivateImplementationDetails>{E21EC13E-4669-42C8-B7A5-2EE7FBD85904} in the example) and put everything in global module instead.
Note - Since I'm not a .NET/dnlib wizard, I always do it one step at a time, make sure it works and then continue. So, my workflow looks like this: write a code that does X → compile and run it → disassemble the result → verify that result X matches the expected → fix the bugs and repeat. Only after I've tested one thing, I move to the next one.
It also helps to make small test program first. Once you know that your code works as intended, you can use it in a larger project. Debugging the entire ConfuserEx project just to find a small bug in modifications made by someone - it's not fun! So, step-by-step...
First, we need to add the class with layout. It's called '__StaticArrayInitTypeSize=5' in the example above. That's quite simple to do in dnlib:
ModuleDefMD mod = ModuleDefMD.Load(args[0]); Importer importer = new Importer(mod); ITypeDefOrRef valueTypeRef = importer.Import(typeof(System.ValueType)); TypeDef classWithLayout = new TypeDefUser("'__StaticArrayInitTypeSize=5'", valueTypeRef); classWithLayout.Attributes |= TypeAttributes.Sealed | TypeAttributes.ExplicitLayout; classWithLayout.ClassLayout = new ClassLayoutUser(1, 5); mod.Types.Add(classWithLayout);
Now we need to add the static field with data, called '$$method0x6000003-1'.
FieldDef fieldWithRVA = new FieldDefUser("'$$method0x6000003-1'", new FieldSig(classWithLayout.ToTypeSig()), FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA); fieldWithRVA.InitialValue = new byte[] {1,2,3,4,5}; mod.GlobalType.Fields.Add(fieldWithRVA);
Once that is done, we can add our byte array field, called bla in the example.
ITypeDefOrRef byteArrayRef = importer.Import(typeof(System.Byte[])); FieldDef fieldInjectedArray = new FieldDefUser("bla", new FieldSig(byteArrayRef.ToTypeSig()), FieldAttributes.Static | FieldAttributes.Public); mod.GlobalType.Fields.Add(fieldInjectedArray);
That's it, we have all the fields. Now we need to add code to global .cctor to initialize the array properly.
ITypeDefOrRef systemByte = importer.Import(typeof(System.Byte)); ITypeDefOrRef runtimeHelpers = importer.Import(typeof(System.Runtime.CompilerServices.RuntimeHelpers)); IMethod initArray = importer.Import(typeof(System.Runtime.CompilerServices.RuntimeHelpers).GetMethod("InitializeArray", new Type[] { typeof(System.Array), typeof(System.RuntimeFieldHandle) })); MethodDef cctor = mod.GlobalType.FindOrCreateStaticConstructor(); IList instrs = cctor.Body.Instructions; instrs.Insert(0, new Instruction(OpCodes.Ldc_I4, 5)); instrs.Insert(1, new Instruction(OpCodes.Newarr, systemByte)); instrs.Insert(2, new Instruction(OpCodes.Dup)); instrs.Insert(3, new Instruction(OpCodes.Ldtoken, fieldWithRVA)); instrs.Insert(4, new Instruction(OpCodes.Call, initArray)); instrs.Insert(5, new Instruction(OpCodes.Stsfld, fieldInjectedArray));
And that's it! Simples!
Further reading
Commented demo code at Pastebin
Longer explanation how array initialization works in C#
Updates
Just to clarify - this is a sample code. It works for me but if it blows up in your project, it's your problem. And there always are some things that can be improved.
• Sometimes I'm overcomplicating things.. You don't need to explicitly import System.Byte, you can use mod.CorLibTypes.Byte for that.
instrs.Insert(1, new Instruction(OpCodes.Newarr, mod.CorLibTypes.Byte.ToTypeDefOrRef()));
• SZArraySig is a cleaner but less obvious way to refer to any array. If you need to reference complex arrays, this is better:
FieldDef fieldInjectedArray = new FieldDefUser("bla", new FieldSig(new SZArraySig(mod.CorLibTypes.Byte)), FieldAttributes.Static | FieldAttributes.Public); mod.GlobalType.Fields.Add(fieldInjectedArray);
Hey kao,
Great job in explaning something again π
I like the fact that you really help people to understand what's going on, so they learn how things are made and can be coded π
I really hope to see more of your interesting posts!
Cheers,
yq8
WoW ... your "customer-relationship program" works REALLY WELL, LOL π
Thanks again for all the details and the ready-made step-by-step solution! Awesome.
Best Regards,
Tony
Thanks for your effort to explain this in detail, I enjoyed it π
You're welcome. π
Hey, I'm using dnSpy to look at the IL code, but the '.data' segment is nowhere to be found. What gives?
Hello!
After I did it, I`m getting this error in Mono,
TypeLoadException: Invalid type dummyClass for instance field :dummyField
Hi Hobbit!
If you could put together a small demo: the code you used + resulting executable + info how you run it in Mono, I can try to reproduce the issue and come up with a solution.
If tried that from a .net core 7 on a dll or exe from .net 4.8 it will give me:
...system.io.filenotfoundexception: could not load filesystem type initialization exception could not load file or assembly 'system.private.CoreLib, version= 7.0.0.0'...
This is a blog post from year 2015. Things have changed in the meantime. π
In general .NET Core and .NET Framework don't play well with each other. Most likely when you do
it tries to import type from .NET Core, not from .NET Framework.