Трансляторы, ориентированные на платформу .NET, должны генерировать код на
Программы на
Основные черты архитекутры виртуальной машины
Существует несколько вариантов хранения переменных в
Статическая область памяти предназначена, например, для статических полей класса, глобальных переменных, констант и т.п. Например, в этой области памяти должны находиться переменные, описанные с модификатором static в языке C.
Куча предназначена для хранения
Машина
На практике это означает, что в безопасном режиме указатели на локальные переменные могут быть только переданы параметрами в другие функции; присваивание их в другие переменные запрещено.
Кроме того, в .NET SDK входит программа PEVerify, которая осуществляет проверку типовой корректности исполняемого модуля.
Перейдем к рассмотрению основных команд
ldimm <число> - загрузка константыldstr <строка> - загрузка строковой константыldsflda <поле> - загрузка адреса статического поляldloca <# переменной> - загрузка адреса локальной переменнойldflda <поле> - загрузка адреса поля объектаldind - косвенная загрузка, берет адрес со стека и помещает на его место значение, размещенное по этому адресуПоскольку, как правило, нам необходим не адрес переменной, а ее значение, то существуют команды загрузки значения на стек: ldsfld, ldloc, ldfld. Каждая из этих команд эквивалентна паре команд ldxxxa; ldind.
Команды выгрузки в основном построены так же, как и команды загрузки (только с противоположным результатом работы), и потому не особо нуждаются в комментариях.
Отметим команду stind, которая берет со стека адрес значения вместе с самим значением и записывает значение по выбранному адресу.
Кроме того, упомянем, что как и в командах загрузки, команды
stloc, stfld, stsfld
эквивалентны следующим парам команд:
ldxxxxa; stind
Команды целочисленной арифметики существуют в знаковом и беззнаковом (с суффиксом .u) вариантах и могут быть записаны с суффиксом обработки переполнения (.ADD, SUB, MUL, DIV, MOD.
Также есть логические операции, которые не могут быть знаковыми или иметь суффикс контроля за переполнением. Логические операции делятся на бинарные ( AND, OR, XOR ) и унарные ( NOT, NEG ).
Наконец, в 0/1. Они могут быть беззнаковыми или знаковыми (с суффиксом .s). Кроме того, существуют специальные варианты сравнения, учитывающие возможность сравнения чисел с плавающей запятой различного порядка (такие операции имеют суффикс .un).
Интересно отметить, что при наличии полного комплекта операций перехода, создатели <= " и " >= " Это приводит к тому, что для целочисленных значений операцию " <= " приходится эмулировать с помощью следующего набора команд:
cgt; ldc.i4.0; ceq
Соответственно, для вещественных значений операцию " <= " необходимо представлять аналогично, только первая команда должна быть заменена на cgt.un. Тем не менее, с точки зрения конечной программы в машинных кодах это, видимо, несущественно, так как такой набор операций легко соптимизировать в одну ассемблерную команду целевой архитектуры.
Переходы в .NET мало чем отличаются от используемых в обычных ассемблерах. Сразу отметим, что все команды переходов существуют в стандартном и коротком виде (для записи коротких переходов используется суффикс .s). Помимо обычного безусловного перехода ( BR ), в BEQ, BNE, BGT, BRFALSE - переход по false, null или нулю на верхушке стека - и все прочие переходы, включая беззнаковые и неупорядоченные варианты).
Существует две основных команды вызова:
CALL )CALLVIRT )Если вызывается CALLVIRT этот параметр обязателен, поскольку виртуальных статических методов в .NET не бывает.
Команда вызова может снабжена префиксом tail. Это означает, что значение, возвращаемое вызываемой процедурой, является также возвращаемым значением и для вызывающей процедуры. В таком случае можно превратить вызов процедуры с последующим возвратом значения в одну команду безусловного перехода на вызываемую процедуру; для этого также необходимо удалить текущую рамку стека. Эта оптимизация позволяет избежать разрастания стека во многих рекурсивных алгоритмах. Недостатком такой оптимизации являются трудности отслеживания стека вызовов при отладке.
Помимо основных методов вызова, .NET предоставляет команду CALLI, выполняющую небезопасный вызов процедуры по адресу точки входа.
Возврат осуществляется командой RET, которая для методов, не возвращающих результат, не имеет параметров. Для всех прочих методов эта команда ожидает параметр - возвращаемое значение на верхушке стека.
Упомянем вкратце некоторые интересные команды
Во-первых, внимания заслуживают специальные команды BOX и
Во-вторых, в
В-третьих, отметим, что THROW, RETHROW, ENDFINALLY, ENDFILTER, LEAVE ), что не очень традиционно для низкоуровневых языков.Общая идея реализации исключений заключается в следующем: транслятором создается специальная таблица обработчиков исключений в данном try -блоке; затем при возникновении исключения виртуальная машина .NET просматривает эти таблицы и вызывает соответствующие обработчики. На самом деле, детали реализации исключений не очень существенны, так как при генерации Reflection.Emit ).
Трансляция в
using System;
class Fib // числа Фибоначчи
{
public static void Main (String[] args)
{
int a = 1, b = 1;
for (int i = 1; i != 10; ++i)
{
Console.WriteLine (a);
int c = a + b;
a = b; b = c;
}
}
}
Продемонстрируем трансляцию в
using System;
class Fib
{
public static void Main (String[] args)
{
int a = 1, b = 1;
for (int i = 1; i != 10; ++i)
{
Console.WriteLine (a);
int c = a + b;
a = b; b = c;
}
}
}
На следующих слайдах мы покажем результаты трансляции этой программы в
// объявление имени assembly
.assembly fib as "fib" { /* здесь могут быть параметры */ }.class public Fib {
.method public static void Main () {
.entrypoint // означает начало assembly
.locals (int32 a, int32 b)
ldc.i4.1 // загрузка константы 1
stloc a // сохранение 1 в a (a = 1)
ldc.i4.1
stloc b // аналогично: b = 1
ldc.i4.1 // загрузка 1 на стек (счетчик цикла)
Loop:
ldloc a
call void System.Console::WriteLine(int32)
ldloc a // stack: 1 a
ldloc b // stack: 1 a b
add // stack: 1 (a+b)
ldloc b
stloc a // a = b
stloc b // b = (a+b)
ldc.i4.1
add // инкремент счетчика
dup
ldc.i4.s 10
bne.un.s Loop // сравнение и переход на новую итерацию
pop // удаление счетчика цикла со стека
ret
}
}
Программа на
Затем объявляется класс , в котором производятся вычисления. Здесь же находится основная точка входа в сборку ( .entrypoint внутри Main ). Затем объявляются локальные переменные; отметим, что в процессе реальной трансляции имена этих переменных будут утеряны.
Наконец, происходит инициализация переменных, подготовка к началу цикла (загрузка счетчика цикла на стек) и выполнение основных
Затем происходит сравнение счетчика цикла с максимальным значением цикла и в случае выполнения неравенства " счетчик не равен 10 " происходит переход на начало цикла. По окончании цикла происходит удаление счетчика цикла со стека и выход из метода.
В .NET предусмотрен специальный механизм доступа к метаданным приложения, который называется рефлексия . С помощью рефлексии мы можем получить доступ к полной информации о типах данных приложения во время исполнения, а также можем создавать новые типы данных и исполняемый код. Сначала мы изучим, каким образом можно получить доступ к уже существующим метаданным.
С помощью методов класса Reflection по данному объекту или его имени можно получить значение типа Type , которое содержит практически исчерпывающую информацию об этом классе - в частности, можно получить его список полей, методов, непосредственного предка данного класса, интерфейсы, которые реализованы в этом объекте и т.д. Всю эту информацию можно хранить и обрабатывать с помощью специальных классов, таких, как MethodInfo , FieldInfo и т.п. Таким образом, для данной сборки можно получить список ее модулей, у которых можно получить список типов и так далее до любых подробностей устройства объекта (вплоть до конкретного бинарного кода его методов, разумеется, только при наличии соответствующих прав доступа).
Отметим, что не все классы, имеющие отношение к рефлексии находятся в пространстве имен Reflection , например, сам класс Type находится в пространстве имен System . Такая "нелогичность" объясняется тем, что класс Type используется не только в ситуациях, связанных с рефлексией. Например, параметром метода ToArray класса является значение типа Type , задающее тип элемента массива.
Поскольку рефлексия является сложной и объемной темой, мы продемонстрируем этот механизм на небольшом примере, в котором реализована функция, позволяющая распечатать объект практически любого типа в виде, пригодном для чтения человеком. Схожий пример ( MetaInfo ) можно также найти в примерах, входящих в состав .NET SDK.
// Пример, демонстрирующий использование рефлексии
// Автор: Антон Москаль,
// Санкт-Петербургский государственный университет, 2001
using System;
using System.Text;
using System.Reflection;
class Sample
{
public struct Struct { public String name; public int[] vec; }
public static void Main (String[] args)
{
Struct s;
s.name = "Name";
s.vec = new int [2];
s.vec [0] = 1;
s.vec [1] = 4;
Console.WriteLine (Refl.Repr(s));
}
}
На данном слайде приведена программа, вызывающая упомянутую выше функцию распечатки объекта. Результатом работы данного примера будет следующая строка:
STRUCT{name:Name, vec:[2]{1, 4}}
Рассмотрим реализацию функции класса Refl :
class Refl
{
public static string Repr (Object o)
{
// получаем дескриптор типа o
Type t = o.GetType ();
// если o - массив:
if (t.IsArray)
return ReprArray ((Array) o);
// если o - структура (value type, не являющийся встроенным типом):
else if (t.IsValueType !t.IsPrimitive)
return ReprStruct (t, o);
// в противном случае используем стандартную фукнцию ToString:
else
return o.ToString ();
}
public static string ReprArray (Array a)
{
string res = "["+a.Length+"]{";
// и дальше в цикле добавляем в res
// список представлений его элементов:
String sep = "";
for (int i = 0; i != a.Length; ++i, sep = ", ")
// для бестипового массива индексацию почему-то использовать
// нельзя, поэтому приходится пользоваться функцией GetValue:
res += sep + Repr (a.GetValue (i));
return res + "}";
}
public static string ReprStruct (Type t, Object o)
{
// получаем массив дескрипторов полей:
FieldInfo [] flds = t.GetFields ();
string res = "STRUCT{";
String sep = "";
// в цикле добавляем представления полей в виде
// <имя поля>: <значение поля>
foreach (FieldInfo fld in flds)
{
// метод GetValue в дескриторе поля
// позволяет выбрать это поле из объекта:
res += sep + fld.Name + ":" + Repr (fld.GetValue (o));
sep = ", ";
}
return res + "}";
}
}
Практически для каждой сущности в Reflection.Emit существует специальный класс с суффиксом Builder, который может быть использован для генерации этой сущности (например, AssemblyBuilder или TypeBuilder ). Отметим, что соглашение об образовании имен не всегда строгое - например, классу FieldInfo соответствует FieldBuilder, а не FieldInfoBuilder, как можно было бы подумать.
Классы с суффиксом Builder предназначены для генерации описываемых сущностей. В каждом таком классе есть методы, позволяющие добавлять поля, методы и прочее содержание, например:
TypeBuilder tb;
MethodBuilder mb = tb.DefineMethod("Call");
ILGenerator ilg = mb.GetILGenerator();
Класс ILGenerator, использованный в приведенном примере, уже может генерировать собственно код виртуальной машины путем использования метода Emit и различных дополнительных методов ( DefineLabel, DeclareLocal, BeginScope-EndScope и т.п.).
Структура
AssemblyBuilder, который будет использоваться для создания сборкиDefineDynamicModule создаются объекты типа ModuleBuilder, которые будут использоваться для порождения входящих в TypeBuilders для классов всех уровней (в том числе, и для вложенных классов); при этом конкретный порядок создания классов может быть весьма нетривиальным из-за возможности ссылок классов друг на друга в иерархии наследованияТакая последовательность шагов при генерации является практически единственно возможной, поскольку на каждом следующем этапе нам может понадобиться сослаться на сущности, созданные на предыдущем этапе. Например, для создания поля нам нужно иметь возможность сослаться на его тип, а для генерации кода - иметь возможность сослаться на используемые поля и методы.
class LowLevelSample {
public static void Run() {
int i;
i = 0;
try {
Start:
if (i == 10) throw new Exception();
Console.WriteLine (i);
i = i + 1;
goto Start;
}
catch (Exception) {
Console.WriteLine ("Finished");
}
}
}
В качестве примера генерации кода для виртуальной машины попробуем породить код на
class LowLevelSample {
public static void Run() {
int i;
i = 0;
try {
Start:
if (i == 10) throw new Exception();
Console.WriteLine (i);
i = i + 1;
goto Start;
}
catch (Exception) {
Console.WriteLine ("Finished");
}
}
}
На примере этой программы мы подробно рассмотрим процесс генерации кода, в том числе и такие интересные аспекты, как генерация меток и обработка исключительных ситуаций. Данная программа носит чисто демонстрационный характер - разумеется, использовать исключения для выхода из цикла в нормальной программе не следует.
Создание сборки и класса
Вначале мы создаем сборку, в которой будет находиться модуль, содержащий наш класс:
AppDomain ad = System.Threading.Thread.GetDomain();
AssemblyName an = new AssemblyName();
an.Name=System.IO.Path.GetFileNameWithoutExtension("numbers.exe");
AssemblyBuilder ab = ad.DefineDynamicAssembly
(an, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modb = ab.DefineDynamicModule
("numbers.exe", "numbers.exe");
modb.CreateGlobalFunctions();TypeBuilder classb = modb.DefineType ("Sample");
MethodBuilder mb = classb.DefineMethod
("Run",
MethodAttributes.Public|MethodAttributes.Static,
typeof (void), null);
Вначале необходимо создать
AppDomain ad = System.Threading.Thread.GetDomain();
Далее мы создаем имя сборки:
AssemblyName an = new AssemblyName();
an.Name = System.IO.Path.GetFileNameWithoutExtension ("numbers.exe");
Теперь создаем класс, генерирующий сборку, и используем его для создания модуля, который будет содержать наш класс (отметим, что атрибут RunAndSave означает, что данная сборка может быть исполнена или сохранена на диске):
AssemblyBuilder ab = ad.DefineDynamicAssembly
(an, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modb = ab.DefineDynamicModule ("numbers.exe", "numbers.exe");
modb.CreateGlobalFunctions ();
Далее мы создаем билдер класса Sample и создаем в этом классе метод Run. Первый параметр DefineMethod задает имя метода, второй - атрибуты метода (в нашем случае - public и static ), затем задается тип результата метода и массив типов параметров (если параметры у метода отсутствуют, как в нашем примере, то можно передать пустой указатель).
TypeBuilder classb = modb.DefineType ("LowLevelSample");
MethodBuilder mb = classb.DefineMethod
("Run",
MethodAttributes.Public|MethodAttributes.Static,
typeof (void), null);
Теперь мы можем перейти к собственно генерации кода; генерируется присваивание, начинается блок try, определяется и ставится метка
ILGenerator il = mb.GetILGenerator(); LocalBuilder var_i = il.DeclareLocal (typeof(int)); il.Emit (OpCodes.Ldc_I4_0); il.Emit (OpCodes.Stloc, var_i); Label EndTry = il.BeginExceptionBlock(); Label Start = il.DefineLabel(); il.MarkLabel (Start);
Теперь мы можем создать генератор кода и приступить к порождению машинного кода для метода Run. Класс ILGenerator содержит в себе методы, необходимые для порождения команд var_I. В случае наличия BeginScope() и EndScope().
ILGenerator il = mb.GetILGenerator(); LocalBuilder var_i = il.DeclareLocal (typeof (int));
Теперь мы генерируем присваивание значения 0 в эту переменную. Метод Emit является основным методом для генерации кода содержит константы, соответствующие всем командам Emit перегружен и может иметь различные параметры, тип которых зависит от генерируемой команды. Например, в последующем операторе мы используем параметр типа LocalBuilder для ссылки на локальную переменную:
il.Emit (OpCodes.Ldc_I4_0); il.Emit (OpCodes.Stloc, var_i);
Для оформления try-catch блока используется метода BeginExceptionBlock , который "открывает" блок обработки исключений и создает метку, на которую впоследствии можно будет сослаться.
Label EndTry = il.BeginExceptionBlock();
Теперь мы заводим метку, которая понадобится для организации цикла. Интересно, что метод DefineLabel не генерирует никакого кода, а просто создает значение типа Label , которое в дальнейшем может быть использовано для генерации переходов. Рано или поздно метка должна быть привязана к позиции в MarkLabel. Таким образом, DefineLabel соответствует объявлению идентификатора метки (в явном виде такая операция есть только в Pascal), в то время как MarkLabel соответствует определению (установке) метки.
Label Start = il.DefineLabel(); il.MarkLabel (Start);
Далее мы генерируем условный оператор, исключение и завершаем цикл:
Label Next = il.DefineLabel();
il.Emit (OpCodes.Ldloc, var_i);
il.Emit (OpCodes.Ldc_I4, 10);
il.Emit (OpCodes.Bne_Un_S, Next);
Type[] no_types = new Type [0];
il.Emit (OpCodes.Newobj, typeof
(System.Exception).GetConstructor (no_types));
il.ThrowException (typeof (System.Exception));
il.MarkLabel (Next);
il.Emit (OpCodes.Ldloc, var_i);
Type [] arg = new Type [1];
arg [0] = typeof (int);
MethodInfo writeLine = typeof(System.Console).GetMethod
("WriteLine", arg);
il.EmitCall (OpCodes.Call, writeLine, null);
...
Теперь нам необходимо оттранслировать условный оператор. Так как никаких if'ов в ассемблере if (i!=10) goto Next , где Next - это специальная метка, расположенная после ветки then условного оператора.
Label Next = il.DefineLabel(); il.Emit (OpCodes.Ldloc, var_i); il.Emit (OpCodes.Ldc_I4, 10); il.Emit (OpCodes.Bne_Un_S, Next);
Затем мы создаем новый экземпляр System.Exception: метод GetConstructor по массиву, содержащему типы параметров конструктора, находит в типе соответствующий конструктор. В нашем случае аргументов у конструктора нет, поэтому массив пуст. После этих действий можно спокойно поставить метку Next:
Type[] no_types = new Type [0]; il.Emit (OpCodes.Newobj, typeof (System.Exception).GetConstructor (no_types)); il.ThrowException (typeof (System.Exception)); il.MarkLabel (Next);
Далее генерируется вывод значения переменной i на консоль. Процесс мало отличается от генерации создания нового объекта, за исключением того, что мы вызываем не конструктор, а метод, и, следовательно, должны задать его имя:
il.Emit (OpCodes.Ldloc, var_i);
Type[] arg = new Type [1];
arg [0] = typeof (int);
MethodInfo writeLine = typeof (System.Console).GetMethod ("WriteLine", arg);
il.EmitCall (OpCodes.Call, writeLine, null);
il.Emit (OpCodes.Ldloc, var_i); // i = i + 1;
il.Emit (OpCodes.Ldc_I4_1);
il.Emit (OpCodes.Add);
il.Emit (OpCodes.Stloc, var_i);
Наконец, генерируется переход к следующей итерации цикла ( goto Start ):
il.Emit (OpCodes.Br, Start);
Наконец, мы генерируем блок catch и завершаем генерацию нашей программы:
il.BeginCatchBlock (typeof (System.Exception));
il.EmitWriteLine ("Finished");
il.EndExceptionBlock ();
il.Emit (OpCodes.Ret);
classb.CreateType ();
ab.SetEntryPoint (mb);
ab.Save ("numbers.exe");
Теперь мы перейдем к генерации catch -блока. Стоит отметить следующую деталь: если бы try не заканчивался переходом к следующей итерации, то метод BeginCatchBlock автоматически сгенерировал бы команду для завершения блока.
il.BeginCatchBlock (typeof (System.Exception));
Метод EmitWriteLine используется для генерации вызова System.Console.WriteLine от строковой константы. Тот же эффект можно достичь и более прямым путем (так же, как мы выше выводили значение переменной var_i ). Результат будет идентичен, но использованный нами только что способ более краток и удобен, например, для вставки в код вывода отладочной информации.
il.EmitWriteLine ("Finished");
Метод EndExceptionBlock завершает генерацию try-catch блока. Он одновременно выполняет определение метки EndTry, которую создал BeginExceptionBlock. После окончания этого блока необходимо вставить генерацию оператора return.
il.EndExceptionBlock (); il.Emit (OpCodes.Ret);
Теперь нам осталось выполнить только несколько завершающих действий: операция CreateType завершает создание типа. После этого мы уже не можем внести никаких изменений и дополнений в класс LowLevelSample.
classb.CreateType();
Метод SetEntryPoint позволяет установить точку входа для сборки (на уровне генерации Main ):
ab.SetEntryPoint (mb);
ab.Save ("numbers.exe");
И вот выполнено последнее действие: после операции Save сборка записана в исполняемый файл на диске. Процесс генерации кода завершен.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.