Создание сборки .NET на CIL

72

Ознакомившись с синтаксисом и семантикой языка CIL, пришла пора закрепить изученный материал, создав .NET-приложение с использованием одной только утилиты ilasm.ехе и предпочитаемого текстового редактора. Это приложение будет состоять из приватно развертываемой однофайловой сборки *.dll, содержащей определения двух типов классов, и консольной сборки *.ехе, взаимодействующей с этими типами.

В первую очередь необходимо создать сборку *.dll, которую будет использовать клиент. Для этого откройте текстовый редактор и создайте новый файл *.il. В этой однофайловой сборке будут использоваться две внешних сборки .NET.

В этой сборке будет содержаться два типа класса. Первый называется UserInfo и имеет два поля данных и специальный конструктор, а второй — WriteInfoUser и имеет единственный статический метод по имени DisplayUserInfo(), принимающий UserInfo в качестве параметра и возвращающий void. Оба они должны размещаться в пространстве имен UserInfo. CIL-код данной программы представлен ниже:

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 4:0:0:0
}
.assembly extern System.Windows.Forms
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 4:0:0:0
}

// Определяем сборку
.assembly UserInfo
{
    .hash algorithm 0x00008004
	.ver 1:0:0:1
}
.module userinfo.dll

// Реализуем тип UserInfo
.namespace UserInfo
{
    .class public auto ansi beforefieldinit UserInfo
	     extends [mscorlib]System.Object
    {
	    // Определяем два поля
		.field public string Name
		.field public int32 Age
		
		// Конструктор
		.method public hidebysig specialname rtspecialname
		    instance void .ctor(int32 i, string s) cil managed
		{
		    .maxstack 8
			ldarg.0
			call instance void [mscorlib]System.Object::.ctor()
			ldarg.0
			ldarg.1
			stfld int32 UserInfo.UserInfo::Age
			ldarg.0
			ldarg.2
			stfld string UserInfo.UserInfo::Name
			ret
		}
	}
	
	.class public auto ansi beforefieldinit WriteUserInfo
	    extends [mscorlib]System.Object
    {
	    .method public hidebysig static void
		    InDisplay(class UserInfo.UserInfo ui) cil managed
		{
		    .maxstack 8
			// Используем локальную переменную
			.locals init ([0] string caption)
			ldstr "Имя: {0}\n"
			ldstr "Возраст: {1}"
			ldarg.0
			// Помещаем в стек значение передаваемого аргумента
			ldfld string UserInfo.UserInfo::Name
			call string [mscorlib]System.String::Format(string, object)
			stloc.0
			ldarg.0
			ldflda int32 UserInfo.UserInfo::Age
			call instance string [mscorlib]System.Int32::ToString()
			ldloc.0
			// Вызов метода MessageBox.Shadow()
			call valuetype [System.Windows.Forms]
			    System.Windows.Forms.DialogResult
				[System.Windows.Forms]
				System.Windows.Forms.MessageBox::Show(string, string)
			pop
			ret
		}
	}
}

Теперь можно скомпилировать новую сборку *.dll с помощью ilasm.exe и проверить содержащийся внутри нее CIL-код на предмет правильности с семантической точки зрения с помощью утилиты peverify.exe.

Далее можно создать простую сборку *.ехе с методом Main() внутри, который будет создавать объект UserInfo и передавать его статическому методу WriteUserInfo.IsDisplay(). Для этого создадим новый файл *.il, добавим в него ссылки на внешние сборки mscorlib.dll и UserInfo.dll (не забыв поместить копию последней в каталог клиентского приложения) и определим в нем единственный тип (Program), в котором будут производиться все необходимые манипуляции над сборкой UserInfo.dll. Ниже показан полный код:

.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 4:0:0:0
}
.assembly extern UserInfo
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
  .ver 4:0:0:0
}
// Определение сборки
.assembly UserInfo
{
   .ver 1:0:0:1
}
.module UserInfo.exe

.namespace Program
{
    .class private auto ansi beforefieldinit Program
         extends [mscorlib]System.Object
    {
        .method private hidebysig static void Main() cil managed
		{
		    // Точка входа в *.exe
			.entrypoint
			.maxstack 8
			.locals init ([0] class [UserInfo]UserInfo.UserInfo myUser)
			ldc.i4 26
			ldstr "Alex"
			newobj instance void [UserInfo]UserInfo.UserInfo::.ctor(int32, string)
			stloc.0
			ldloc.0
			// Вызов метода InDisplay()
			call void [UserInfo]
			   UserInfo.WriteUserInfo::InDisplay(
			       class [UserInfo]UserInfo.UserInfo)
			ret
		}
    }
}

Единственным кодом операции, на который здесь важно обратить внимание, является .entrypoint. Этот код применяется для обозначения того, какой из методов должен быть входной точкой в модуле *.ехе. В действительности, поскольку по .entrypoint CLR-среда определяет начальный метод для выполнения, этот метод может иметь какое угодно имя, хотя для него было использовано стандартное имя Main(). В остальной части CIL-кода этого метода Main() производятся типичные операции по помещению и извлечению значений из стека.

Пройди тесты
Лучший чат для C# программистов