Introduction
Hi everyone! This sample is the starting point to export methods inside unmanaged language programs, such as VB, C++ or Lotus Notes, etc. Yes, I wondered looking how I can do this, but in the network, I found only samples about the usage of .NET runtime with blocks of unmanaged code such as a DLL or a COM object.
What you must know to go on
- The base knowledge of .NET.
- The knowledge of the manifest and some practice with the IL.
- Must have at least disassembled a .NET program :-D.
- Knowledge of pointers.
What you must have
- Visual Studio 2003 ;-) (facultative).
- .NET Framework SDK.
Background
I wondered searching and searching, searching and seeking again, and again and again, the Google oracle did not help a lot. Then, confused and terribly demoralized, I started to study, I saw the light in a wonderful book "Inside Microsoft .NET IL assembler".
My developing career started in the university with the knowledge of the cool 80x86 assembly, and today I will write some little comparison between the managed and native processor code.
Let's start with the classes
OK here we go, the way to understand something is make the thing in the simplest way.
using System;
namespace ILtest
{
public class Car
{
public Car()
{
}
public string Drive(ref int speed)
{
return "my current speed is: " + speed.ToString();
}
}
}
That's all we want to export the method Drive
in the unmanaged code, but this method must remain safe and we don't want to use the unsafe
directive.
We'd like the caller to use the classic methods: LoadLibrary(...)
and GetProcAddress(..., "Drive");
.
I will observe how this is generated, and to do this, I use two .NET utilities from the command line: ILDASM and ILASM. The first is a decompiler into the native IL language, the second, instead is the compiler. To obtain the scope of this article we need to:
- decompile the ILtest generated DLL,
- make some changes (little little),
- and compile cak with ILASM.
Come on, decompile me!
Compile without debugging information and open the Visual Studio .NET Command Prompt.
Go in the bin/release folder and observe the ILtest.dll.
Decompile with the following command:
ildasm Iltest.dll /OUT:iltest.il
Delete the res file, it is out of this scope, and examine the .IL file.
A small look at the output file
Open the iltest.il with Notepad (I prefer UltraEdit).
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 1:0:5000:0
}
.assembly myIltest
{
.custom instance void [mscorlib]
System.Reflection.AssemblyKeyNameAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyConfigurationAttribute::.ctor(string)
= ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyDescriptionAttribute::.ctor(string)
= ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 00 00 00 )
.hash algorithm 0x00008004
.ver 1:0:1702:23004
}
.module myIltest.dll
.imagebase 0x11000000
.subsystem 0x00000003
.file alignment 4096
.corflags 0x00000001
.namespace ILtest
{
.class public auto ansi beforefieldinit Car
extends [mscorlib]System.Object
{
}
}
.namespace ILtest
{
.class public auto ansi beforefieldinit Car
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
}
.method public hidebysig instance string
Drive(int32& speed) cil managed
{
.maxstack 2
IL_0000: ldstr "my current speed is: "
IL_0005: ldarg.1
IL_0006: call instance string [mscorlib]System.Int32::ToString()
IL_000b: call string [mscorlib]System.String::Concat(string,
string)
IL_0010: ret
}
}
}
The IL language is very similar to a normal assembly, it reminds me of the Z80 coda, do you remember the zx spectrum? HAHAHAH! Yes, there are the load instructions!!
.method public hidebysig instance string
Drive(int32& speed) cil managed
{
.maxstack 2
IL_0000: ldstr "my current speed is: "
IL_0005: ldarg.1
IL_0006: call instance string [mscorlib]System.Int32::ToString()
IL_000b: call string [mscorlib]System.String::Concat(string,
string)
IL_0010: ret
}
OK, let's start with the deep examination, and don't worry about the jockes. The first thing we can observe is, the method we want to publish is defined in the following line of code. Our method does the following instructions.
Loads the const string "my current speed is: ", loads the argument speed
, and does the internal Concat
method (defined statically) of the String
class.
We can observe, there is no pointer reference to this function and we must create one with some few declarations:
In the beginning, we must build a v-table in order to show methods externally; this includes vtablefxup table and some export tables such as the export address table and the name pointer table, the ordinal table, the export name table, and the export directory table.
IL syntax
.vtfixup [<num_slots>] <flags> at <data_label>
<num_slots>
is an integer constant indicating the number of v-table slots grouped into one entry.
<flags> int32/int64 processor dependent
fromunmanaged
is the keyword that instructs the call is done outside the .NET framework. Here's a sample:
.vtfixup [1] int32 fromunmanaged at VT_01
.data VT_01 = int32(0x0100001A)
The IL Assember method for actually declaring a method as exported is:
.export [<ordinal>] as <export_name>
<ordinal>
is an integer constant
<export_name> is the name alias of the exported method.
The results entry for our sample is the following:
.vtfixup [1] int32 from unmanaged at VT_01
.data VT_01 = int32(0)
.method public hidebysig instance string
Drive(int32& speed) cil managed
{
.vtentry 1 : 1
.export [1] as CarDrive
.maxstack 2
IL_0000: ldstr "my current speed is: "
IL_0005: ldarg.1
IL_0006: call instance string [mscorlib]System.Int32::ToString()
IL_000b: call string [mscorlib]System.String::Concat(string,
This is all we need besides a little fix: .corflags 0x00000001
changed to .corflags 0x00000002
.
Now the code is ready, and I publish the new one.
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 1:0:5000:0
}
.assembly myIltest
{
.custom instance void [mscorlib]
System.Reflection.AssemblyKeyNameAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyConfigurationAttribute::.ctor(string)
= ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyDescriptionAttribute::.ctor(string)
= ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 00 00 00 )
.hash algorithm 0x00008004
.ver 1:0:1702:23004
}
.module myIltest.dll
.imagebase 0x11000000
.subsystem 0x00000003
.file alignment 4096
.corflags 0x00000002
.vtfixup [1] int32 from unmanaged at VT_01
.data VT_01 = int32(0)
.namespace ILtest
{
.class public auto ansi beforefieldinit Car
extends [mscorlib]System.Object
{
}
}
.namespace ILtest
{
.class public auto ansi beforefieldinit Car
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.vtentry 1 : 1
.export [1] as CarDrive
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
}
.method public hidebysig instance string
Drive(int32& speed) cil managed
{
.maxstack 2
IL_0000: ldstr "my current speed is: "
IL_0005: ldarg.1
IL_0006: call instance string [mscorlib]System.Int32::ToString()
IL_000b: call string [mscorlib]System.String::Concat(string,
string)
IL_0010: ret
}
}
}
In the practice, we had wrote an external entry point to our function
Last step
We can compile back the DLL with this command:
ILasm ILtest.il /out:iltest.dll
That's all! Other unmanaged languages can see the Drive
method! Isn't it great?!
Point of interest
The parameters must be passed by reference, i.e., Drive(ref string speed)
-> Drive(int32& speed)
.
References
- "Inside Microsoft .NET Assembler" - Microsoft Press.
I hope it is useful for somebody!