Говорят, что код программ не разрушает память, если эти программы, будучи запущенными в одном адресном пространстве, корректно изолированы друг от друга, то есть ошибки в одной программе не могут привести к изменению структур данных другой программы.
Верификация кода - это процесс автоматического доказательства того, что этот код не разрушает память.
В этой главе мы рассмотрим общие вопросы верификации кода и алгоритм верификации, приведенный в спецификации CLI и реализованный в Microsoft .NET Framework.
Алгоритмы верификации являются метавычислительными алгоритмами (верификаторы кода стоят в одном ряду с такими программами, как частичные вычислители и суперкомпиляторы). Поэтому совершенно неудивительно, что алгоритмы верификации в общем случае имеют проблемы, свойственные любым метавычислительным алгоритмам, а именно:
Понятно, что алгоритмы, работающие неизвестно сколько времени (а возможно, вообще никогда не завершающиеся) и потребляющие неизвестно какое количество ресурсов, представляют скорее теоретический, нежели практический интерес. Поэтому их разработчикам приходится каким-то образом ограничивать задачу, чтобы получить применимые на практике результаты. Для алгоритмов верификации кода известно два подхода, позволяющие снизить их сложность и добиться терминируемости:
Для этого подхода характерно то, что существуют программы, в которых верификатор не находит ни одной ошибки и которые, тем не менее, разрушают память.
Если задача некоторого метавычислительного алгоритма была как-то ограничена, то сразу же возникает вопрос о том, какие входные данные (программы) он сможет обработать. Для суперкомпиляторов и частичных вычислителей этот вопрос до сих пор остается открытым (ответ на него приходится искать
Запуск такого верификатора можно рассматривать как один из тестов, которым подвергается программа. Если в некоторой программе верификатор не обнаружил ни одной ошибки, то можно считать, что его запуск был напрасным.
Использование таких верификаторов оправдано и удобно в случаях, если существуют специальные компиляторы, генерирующие такой код, который не только заведомо не разрушает память, но и гарантированно проходит верификацию.
Тем самым ответ на вопрос о том, для каких программ верификатор может доказать, что они не разрушают память, становится тривиально прост: для тех программ, которые были откомпилированы специальным компилятором.
Верификатор кода .NET относится ко второму типу. Он может доказать, что сборки, генерируемые компиляторами C#, J# и Visual Basic .NET, не разрушают память. При этом он терминируем и имеет
Здесь может возникнуть следующий вопрос: зачем нужен верификатор, если компиляторы и так генерируют не разрушающий память код? Чтобы ответить на этот вопрос, нужно вспомнить, что:
Другими словами, верификатор .NET предназначен не для поиска ошибок в программах, а для обеспечения безопасности системы.
Мы будем различать два достаточно близких понятия, относящихся к верификации. Это верифицированный код и
Верифицированный код - это код, для которого верификатор доказал, что он не разрушает память. То есть чтобы из верифицируемого кода получить верифицированный код, нужно обязательно запустить верификатор.
В спецификации CLI предложен базовый вариант алгоритма верификации. Любая реализация CLI должна включать верификатор, верифицирующий по крайней мере те программы, которые допускает базовый алгоритм.
Алгоритм верификации представляет собой вариант абстрактного интерпретатора CIL-кода. Линейность алгоритма верификации достигается за счет того, что он просматривает
Пусть S и T - типы. Тогда S[] и T[] - соответствующие им массивные типы, а S и T - типы соответствующих управляемых указателей.
Тот факт, что S совместим по присваиванию с T, мы будем записывать как S := T.
Операция := рефлексивна и транзитивна.
Правила
S := T, если S - базовый класс для T или интерфейс, реализуемый T, и при этом T не является типом-значением.S := T, если S и T - интерфейсы, и реализация T требует реализации S.S := null, если S - S[] := T[], если S := T и S := T, если S := T.Если ни одно из этих правил не выполняется, то типы S и T несовместимы.
Будем называть конфигурацией стека данные о количестве слотов на стеке и типы значений, лежащих в этих слотах. При этом конфигурацию, содержащую 0 слотов, будем называть пустой конфигурацией.
Рассмотрим две операции над конфигурациями, которые используются в алгоритме верификации:
Операция проверки совместимости имеет два операнда: конфигурации C и K. Она возвращает булевское значение, показывающее, совместима ли конфигурация C с конфигурацией K.
Приведем алгоритм вычисления операции проверки совместимости:
C и K различно, то возвращаем false. В противном случае пусть N - количество слотов в конфигурации C (естественно, и в K тоже).N = 0, то возвращаем true.i пробегает значения от 1 до N. Тогда для каждого такого i выполняем следующее:S - тип i-го слота конфигурации C, а T - тип i-го слота конфигурации K ;T := S, то возвращаем false.true.Операция слияния двух конфигураций также имеет два операнда: конфигурации C и K. Она может либо закончиться неуспехом, либо возвращает конфигурацию R, вычисляемую по следующему алгоритму:
C и K различно, то алгоритм завершается неуспехом. В противном случае пусть N - количество слотов в конфигурации C (естественно, и в K тоже).N = 0, то возвращаем пустую конфигурацию.i пробегает значения от 1 до N. Тогда для каждого такого i выполняем следующее:S - тип i-го слота конфигурации C, а T - тип i-го слота конфигурации K.U i-го слота результирующей конфигурации:S := T, то U = S.T := S, то U = T.T и S - V, то U = V.В процессе работы алгоритма для каждой инструкции CIL вычисляется конфигурация стека. При этом фактические значения, лежащие на стеке, в локальных переменных и параметрах метода, не учитываются.
Алгоритм верификации работает со следующими данными:
P, содержащий инструкции CIL в том порядке, в каком они записаны в N - количество инструкций.M размера N, в котором хранятся вычисляемые в процессе верификации конфигурации стека.Возможны два результата работы алгоритма:
Метод не содержит неверифицируемых инструкций. Все возможные пути передачи управления в
Метод либо содержит неверифицируемые инструкции, либо в процессе вычисления конфигурации стека для некоторой инструкции было выявлено противоречие.
В начале работы алгоритма в массиве M не записано ни одной конфигурации.
Алгоритм последовательно просматривает массив P, то есть на каждом шаге работает с одной инструкцией P[i], где i пробегает значения от 1 до N (будем считать, что массивы P и M нумеруются, начиная с единицы).
На каждом шаге алгоритма выполняются следующие действия:
P[i] - текущая рассматриваемая инструкция. Тогда смотрим, что собой представляет M[i]. Если M[i] еще не содержит конфигурацию стека, то записываем в M[i] пустую конфигурацию.P[i], учитывая конфигурацию стека M[i] и информацию, содержащуюся в метаданных. Если инструкция в принципе не P[i]. Другими словами, вычисляем конфигурацию стека S, которая получилась бы после реального выполнения инструкции.P[i]. Для каждой такой инструкции P[j] выполняем следующее:j < i, то проверяем, совместима ли конфигурация S с конфигурацией M[j]. Если оказывается, что несовместима, то алгоритм завершается неуспехом;j >= i и M[j] еще не содержит конфигурации стека, то M[j] = S ;j >= i и M[j] уже содержит конфигурацию стека, то выполняем попытку слияния конфигураций S и M[j]. Если попытка увенчалась успехом, то записываем результат слияния в M[j]. В противном случае алгоритм завершается неуспехом.Если все инструкции были просмотрены и ни на одном шаге алгоритм не завершился неуспехом, то метод успешно прошел верификацию.
Во второй главе мы рассмотрели написанную на языке C программу, осуществлявшую генерацию сборки .NET. Эта программа не использовала никаких библиотек для работы с метаданными и CIL-кодом, то есть задача генерации сборки была решена на самом низком уровне - на уровне двоичных форматов. Хотя такой подход во многих случаях бывает оправдан, на практике гораздо удобнее воспользоваться специальными библиотеками, предназначенными для разработчиков метаинструментов.
Напомним, что метаинструменты для платформы .NET - это программы, рассматривающие сборки .NET в качестве объектов анализа, генерации или преобразования. При этом анализ существующей сборки выполняется верификаторами, загрузчиками, отладчиками и т.д. Генерация новой сборки осуществляется компиляторами и
Сборка .NET представляет собой совокупность метаданных, CIL-кода и, возможно, ресурсов. Поэтому библиотеки, необходимые для создания метаинструментов, должны обеспечивать чтение и генерацию этой информации. В составе .NET Framework SDK поставляются две библиотеки, частично решающие эти задачи. Это
При использовании
Для того чтобы использовать
#include <corhlpr.h> #pragma comment(lib, "format.lib")
Первая строка подключает заголовочный файл, в котором описываются нужные интерфейсы, а также вспомогательные структуры данных и функции. Вторая строка дает указание компоновщику использовать библиотеку format.lib.
Взаимодействие с
| Интерфейс | Описание |
|---|---|
IMetadataDispenserEx |
Позволяет загружать образ исполняемого либо |
IMetadataImport |
Предназначен для чтения метаданных |
IMetadataEmit |
Предназначен для генерации метаданных |
Работа с
CoInitialize(NULL);
IMetaDataDispenser *dispenser;
HRESULT h = CoCreateInstance(
CLSID_CorMetaDataDispenser,
NULL,
CLSCTX_INPROC_SERVER,
IID_IMetaDataDispenserEx,
(void **)dispenser
);
if (h)
printf("Error");
Затем можно с помощью метода OpenScope этого интерфейса загрузить существующую сборку в память или с помощью метода DefineScope создать новую сборку. Оба метода возвращают указатель на интерфейс, через который можно в дальнейшем осуществлять чтение или генерацию метаданных. В следующем фрагменте программы происходит вызов метода OpenScope для чтения метаданных из сборки test.exe:
IMetaDataImport *mdimp;
HRESULT h = dispenser->OpenScope(
L"test.exe",
0,
IID_IMetaDataImport,
(IUnknown**)mdimp
);
if (h)
printf("Error");
При завершении работы с
mdimp->Release(); dispenser->Release();
Чтение метаданных из сборки осуществляется через методы интерфейса IMetadataImport, которые условно можно разделить на три основные группы:
EnumXXX возвращают массивы mdTypeDef tmp;
mdTypeDef *tokens;
HCORENUM Enum = 0;
unsigned long tokenCount;
mdimp->EnumTypeDefs(Enum,tmp,1,tokenCount);
mdimp->CountEnum(Enum,tokenCount);
if (tokenCount > 0)
{
tokens = new mdTypeDef [tokenCount];
tokens[0] = tmp;
if (tokenCount > 1)
mdimp->EnumTypeDefs(
Enum,tokens+1,tokenCount-1,
tokenCount
);
mdimp->CloseEnum(Enum);
}
Первый вызов метода EnumTypeDefs создает Enum ), а также возвращает первый элемент этого массива (он сохраняется в переменной tmp ).
Затем метод CountEnum записывает в переменную tokenCount размер массива tokens ) и второй раз вызывается метод EnumTypeDefs. Обратите внимание, что первому элементу массива tokens мы присваиваем значение переменной tmp.
FindXXX предназначены для поиска mdTypeDef token; mdimp->FindTypeByName(L"MyType1",NULL,token);
GetXXX используются для получения свойств strToken, выглядит так:unsigned short s[1024]; unsigned long len; mdimp->GetUserString(strToken,s,1024,len);
Обратите внимание, что для хранения одного символа применяется тип unsigned short. Причина в том, что для представления строковых данных в .NET используется 16-разрядная кодировка Unicode.
Генерация метаданных осуществляется через методы интерфейса IMetadataEmit, которые можно условно разделить на две группы:
DefineXXX добавляют новые SetXXX устанавливают свойства Сгенерированные метаданные могут быть сохранены на диске при помощи метода Save.
Библиотека рефлексии содержит классы для работы с метаданными на высоком уровне (эти классы располагаются в пространствах имен System. и System. ). Она соответствует спецификации
Метаинструмент, использующий библиотеку рефлексии, получает доступ к метаданным сборки через экземпляр класса Assembly, который создается при загрузке сборки в память. Затем через методы класса Assembly можно получить объекты класса Module, которые соответствуют модулям, входящим в сборку. Класс Module, в свою очередь, позволяет получить набор экземпляров класса Type для входящих в модуль типов, а уже через эти объекты можно добраться до конструкторов (класс ConstructorInfo ), методов (класс MethodInfo ) и полей (класс FieldInfo ) типов. То есть библиотека рефлексии спроектирована таким образом, что создание объектов рефлексии, соответствующих элементам метаданных, осуществляется путем вызова методов других объектов рефлексии (схема получения доступа к объектам рефлексии показана на рис. 4.5, при этом сплошные стрелки обозначают основные пути получения доступа, а пунктирные - дополнительные пути). Другими словами, конструкторы классов, входящих в библиотеку, для пользователя библиотеки недоступны. Информацию о любом элементе метаданных можно прочитать из свойств соответствующего ему объекта рефлексии.
(рис 4.5) Получение доступа к объектам рефлексии
Экземпляр класса Assembly, с создания которого, как правило, начинается работа со сборкой через рефлексию, можно получить разными способами:
GetExecutingAssembly класса Assembly:Assembly assembly = Assembly.GetExecutingAssembly()
GetAssembly:Assembly assembly = Assembly.GetAssembly(typeof(int))
LoadFrom:Assembly assembly = Assembly.LoadFrom("test.exe")
Сборка .NET, как правило, состоит из одного модуля, хотя существует возможность включения в сборку сразу нескольких модулей. Получение доступа к объектам класса Module, описывающим модули, из которых состоит сборка, осуществляется одним из двух способов:
GetModule класса Assembly:Module module = assembly.GetModule("SomeModule.exe")
GetModules для получения Module, соответствующих всем модулям в сборке:Module[] modules = assembly.GetModules(false)
Через объект класса Module мы можем обратиться к глобальным полям и функциям модуля (глобальные поля и функции не принадлежат ни одному классу), используя методы GetField и GetFields для полей, а также GetMethod и GetMethods для функций. При этом полям будут соответствовать объекты класса FieldInfo, а методам - объекты класса MethodInfo. В следующем примере мы выводим на экран сигнатуры всех глобальных функций некоторой сборки "test.exe":
Assembly assembly = Assembly.LoadFrom("test.exe");
Module[] modules = assembly.GetModules(false);
foreach (Module mod in modules)
{
MethodInfo[] methods = mod.GetMethods();
foreach (MethodInfo met in methods)
Console.WriteLine(met);
}
Особое место в библиотеке рефлексии занимает класс Type, представляющий типы. Область применения этого класса значительно шире, чем области применения остальных классов рефлексии, и этот факт отражен в том обстоятельстве, что класс Type входит в пространство имен System, а не System..
В каждом классе существует унаследованный от класса System.Object метод GetType, возвращающий экземпляр класса Type, который описывает этот класс. В следующем примере метод GetType будет использован для динамического определения типа объекта o (так как этот объект представляет собой строку, то на печать будет выведено сообщение "Sytem.String"):
object o = new String("qwerty");
Type t = o.GetType();
Console.WriteLine(t);
В C# определен специальный оператор typeof, который возвращает объект класса Type. Например, если нам нужно получить объект Type, представляющий
Type t = typeof(string[]);
Кроме этого, доступ к информации о типе можно получить, зная имя этого типа:
GetType класса Type:Type t = Type.GetType("System.Char");
Assembly или Module, которые соответствуют сборке или модулю, содержащему нужный тип:Type t = module.GetType("Class1") ;Type t = assembly.GetType("Class1").Мы также можем воспользоваться методами GetTypes классов Assembly и Module для получения массива типов, объявленных в сборке или модуле. В следующем примере на экран выводится список типов, содержащихся в сборке "test.exe":
Assembly assembly = Assembly.LoadFrom("test.exe");
Type[] types = assembly.GetTypes();
foreach (Type t in types)
Console.WriteLine(t);
Имея объект рефлексии, соответствующий некоторому типу, мы имеем возможность получить доступ к объектам рефлексии, описывающим его поля, конструкторы, методы, свойства и GetField, GetConstructor, GetMethod, GetProperty и GetNestedType класса Type. А если мы хотим получить массивы объектов рефлексии, описывающих члены типа, то нам нужно вызвать методы GetFields, GetConstructors, GetMethods, GetProperties и GetNestedTypes. В следующем примере на экран выводится список типов, содержащихся в сборке, вместе с сигнатурами их методов:
Assembly assembly = Assembly.LoadFrom("test.exe");
Type[] types = assembly.GetTypes();
foreach (Type t in types)
{
Console.WriteLine(""+t+":");
MethodInfo[] methods = t.GetMethods();
foreach (MethodInfo m in methods)
Console.WriteLine(" "+m);
}
Для каждого метода и конструктора мы можем получить доступ к GetParameters, объявленный в классах MethodInfo и ConstructorInfo. Данный метод возвращает массив объектов класса ParameterInfo:
ParameterInfo[] parms = method.GetParameters();
Кроме чтения метаданных, библиотека рефлексии позволяет создавать экземпляры типов, входящих в обрабатываемую сборку, вызывать методы этих типов, читать и изменять значения полей.
Рассмотрим класс TestClass:
class TestClass
{
private int val;
public TestClass() { val = 7; }
protected void print() { Console.WriteLine(val); }
}
В следующем примере мы используем метод MethodInfo для вызова метода print объекта класса TestClass. Обратите внимание, что метод print объявлен с protected.
static void CallMethodDemo()
{
TestClass a = new TestClass();
BindingFlags flags = (BindingFlags)
(BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo printer =
typeof(TestClass).GetMethod("print",flags);
printer.Invoke(a, null);
}
Путем внесения небольших изменений в CallMethodDemo мы можем добиться того, что объект класса TestClass будет также создаваться через рефлексию (с помощью вызова его конструктора):
static void CallMethodDemo2()
{
Type t = typeof(TestClass);
BindingFlags flags = (BindingFlags)
(BindingFlags.NonPublic | BindingFlags.Instance);
ConstructorInfo ctor = t.GetConstructor(new Type[0]);
MethodInfo printer = t.GetMethod("print",flags);
object a = ctor.Invoke(null);
printer.Invoke(a, null);
}
А теперь продемонстрируем, как через рефлексию можно получить значение поля объекта. Это достигается путем вызова метода GetField объекта класса Type:
static void GetFieldDemo()
{
TestClass a = new TestClass();
BindingFlags flags = (BindingFlags)
(BindingFlags.NonPublic |
BindingFlags.Instance);
FieldInfo val =
typeof(TestClass).GetField("val",flags);
Console.WriteLine(val.GetValue(a));
}
Для генерации метаданных в библиотеке рефлексии предназначены классы пространства имен System.. Создание новой сборки начинается с создания экземпляра класса AssemblyBuilder. Далее путем вызова методов класса AssemblyBuilder создается нужное количество модулей (объектов класса ModuleBuilder ), в модулях создаются типы (объекты класса TypeBuilder ), а в типах - конструкторы, методы и поля (объекты классов ConstructorBuilder, MethodBuilder и FieldBuilder, соответственно). То есть при генерации метаданных идеология такая же, как и при чтении их из готовой сборки (на рис. 4.6 представлена схема, показывающая последовательность создания метаданных).
(рис 4.6) Последовательность создания метаданных
В отличие от ILGenerator. Он позволяет после создания объекта MethodBuilder (или ConstructorBuilder ) сгенерировать для соответствующего метода (или конструктора)
В следующем примере библиотека рефлексии используется для генерации сборки .NET, состоящей из одного глобального метода main, выводящего на экран сообщение "Hello, World!":
using System;
using System.Threading;
using System.Reflection;
using System.Reflection.Emit;
class HelloGenerator
{
static void Main(string[] args)
{
AppDomain appDomain = Thread.GetDomain();
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = "hello.exe";
AssemblyBuilder assembly =
appDomain.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.RunAndSave
);
ModuleBuilder module =
assembly.DefineDynamicModule(
"hello.exe", "hello.exe"
);
MethodBuilder mainMethod =
module.DefineGlobalMethod(
"main",
MethodAttributes.Static |
MethodAttributes.Public,
typeof(void),
null
);
MethodInfo ConsoleWriteLineMethod =
((typeof(Console)).GetMethod("WriteLine",
new Type[] { typeof(string) } ));
ILGenerator il = mainMethod.GetILGenerator();
il.Emit(OpCodes.Ldstr,"Hello, World!");
il.Emit(OpCodes.Call,ConsoleWriteLineMethod);
il.Emit(OpCodes.Ret);
module.CreateGlobalFunctions();
assembly.SetEntryPoint(
mainMethod,PEFileKinds.ConsoleApplication
);
assembly.Save("hello.exe");
}
}
Возможности, предоставляемые библиотекой
| Чтение метаданных | + | + |
| Генерация метаданных | + | + |
| Чтение CIL-кода | - | - |
| Генерация CIL-кода | - | + |
Из таблицы следует, что ни одна из библиотек, поставляемых вместе с .NET Framework, не позволяет читать CIL-код, хотя эта возможность требуется целому ряду метаинструментов (например, верификаторам и оптимизаторам кода).
Говорят, что код программ не разрушает память, если эти программы, будучи запущенными в одном адресном пространстве, корректно изолированы друг от друга, то есть ошибки в одной программе не могут привести к изменению структур данных другой программы.
Верификация кода - это процесс автоматического доказательства того, что этот код не разрушает память.
В этой главе мы рассмотрим общие вопросы верификации кода и алгоритм верификации, приведенный в спецификации CLI и реализованный в Microsoft .NET Framework.
Алгоритмы верификации являются метавычислительными алгоритмами (верификаторы кода стоят в одном ряду с такими программами, как частичные вычислители и суперкомпиляторы). Поэтому совершенно неудивительно, что алгоритмы верификации в общем случае имеют проблемы, свойственные любым метавычислительным алгоритмам, а именно:
Понятно, что алгоритмы, работающие неизвестно сколько времени (а возможно, вообще никогда не завершающиеся) и потребляющие неизвестно какое количество ресурсов, представляют скорее теоретический, нежели практический интерес. Поэтому их разработчикам приходится каким-то образом ограничивать задачу, чтобы получить применимые на практике результаты. Для алгоритмов верификации кода известно два подхода, позволяющие снизить их сложность и добиться терминируемости:
Для этого подхода характерно то, что существуют программы, в которых верификатор не находит ни одной ошибки и которые, тем не менее, разрушают память.
Если задача некоторого метавычислительного алгоритма была как-то ограничена, то сразу же возникает вопрос о том, какие входные данные (программы) он сможет обработать. Для суперкомпиляторов и частичных вычислителей этот вопрос до сих пор остается открытым (ответ на него приходится искать
Запуск такого верификатора можно рассматривать как один из тестов, которым подвергается программа. Если в некоторой программе верификатор не обнаружил ни одной ошибки, то можно считать, что его запуск был напрасным.
Использование таких верификаторов оправдано и удобно в случаях, если существуют специальные компиляторы, генерирующие такой код, который не только заведомо не разрушает память, но и гарантированно проходит верификацию.
Тем самым ответ на вопрос о том, для каких программ верификатор может доказать, что они не разрушают память, становится тривиально прост: для тех программ, которые были откомпилированы специальным компилятором.
Верификатор кода .NET относится ко второму типу. Он может доказать, что сборки, генерируемые компиляторами C#, J# и Visual Basic .NET, не разрушают память. При этом он терминируем и имеет
Здесь может возникнуть следующий вопрос: зачем нужен верификатор, если компиляторы и так генерируют не разрушающий память код? Чтобы ответить на этот вопрос, нужно вспомнить, что:
Другими словами, верификатор .NET предназначен не для поиска ошибок в программах, а для обеспечения безопасности системы.
Мы будем различать два достаточно близких понятия, относящихся к верификации. Это верифицированный код и
Верифицированный код - это код, для которого верификатор доказал, что он не разрушает память. То есть чтобы из верифицируемого кода получить верифицированный код, нужно обязательно запустить верификатор.
В спецификации CLI предложен базовый вариант алгоритма верификации. Любая реализация CLI должна включать верификатор, верифицирующий по крайней мере те программы, которые допускает базовый алгоритм.
Алгоритм верификации представляет собой вариант абстрактного интерпретатора CIL-кода. Линейность алгоритма верификации достигается за счет того, что он просматривает
Пусть S и T - типы. Тогда S[] и T[] - соответствующие им массивные типы, а S и T - типы соответствующих управляемых указателей.
Тот факт, что S совместим по присваиванию с T, мы будем записывать как S := T.
Операция := рефлексивна и транзитивна.
Правила
S := T, если S - базовый класс для T или интерфейс, реализуемый T, и при этом T не является типом-значением.S := T, если S и T - интерфейсы, и реализация T требует реализации S.S := null, если S - S[] := T[], если S := T и S := T, если S := T.Если ни одно из этих правил не выполняется, то типы S и T несовместимы.
Будем называть конфигурацией стека данные о количестве слотов на стеке и типы значений, лежащих в этих слотах. При этом конфигурацию, содержащую 0 слотов, будем называть пустой конфигурацией.
Рассмотрим две операции над конфигурациями, которые используются в алгоритме верификации:
Операция проверки совместимости имеет два операнда: конфигурации C и K. Она возвращает булевское значение, показывающее, совместима ли конфигурация C с конфигурацией K.
Приведем алгоритм вычисления операции проверки совместимости:
C и K различно, то возвращаем false. В противном случае пусть N - количество слотов в конфигурации C (естественно, и в K тоже).N = 0, то возвращаем true.i пробегает значения от 1 до N. Тогда для каждого такого i выполняем следующее:S - тип i-го слота конфигурации C, а T - тип i-го слота конфигурации K ;T := S, то возвращаем false.true.Операция слияния двух конфигураций также имеет два операнда: конфигурации C и K. Она может либо закончиться неуспехом, либо возвращает конфигурацию R, вычисляемую по следующему алгоритму:
C и K различно, то алгоритм завершается неуспехом. В противном случае пусть N - количество слотов в конфигурации C (естественно, и в K тоже).N = 0, то возвращаем пустую конфигурацию.i пробегает значения от 1 до N. Тогда для каждого такого i выполняем следующее:S - тип i-го слота конфигурации C, а T - тип i-го слота конфигурации K.U i-го слота результирующей конфигурации:S := T, то U = S.T := S, то U = T.T и S - V, то U = V.В процессе работы алгоритма для каждой инструкции CIL вычисляется конфигурация стека. При этом фактические значения, лежащие на стеке, в локальных переменных и параметрах метода, не учитываются.
Алгоритм верификации работает со следующими данными:
P, содержащий инструкции CIL в том порядке, в каком они записаны в N - количество инструкций.M размера N, в котором хранятся вычисляемые в процессе верификации конфигурации стека.Возможны два результата работы алгоритма:
Метод не содержит неверифицируемых инструкций. Все возможные пути передачи управления в
Метод либо содержит неверифицируемые инструкции, либо в процессе вычисления конфигурации стека для некоторой инструкции было выявлено противоречие.
В начале работы алгоритма в массиве M не записано ни одной конфигурации.
Алгоритм последовательно просматривает массив P, то есть на каждом шаге работает с одной инструкцией P[i], где i пробегает значения от 1 до N (будем считать, что массивы P и M нумеруются, начиная с единицы).
На каждом шаге алгоритма выполняются следующие действия:
P[i] - текущая рассматриваемая инструкция. Тогда смотрим, что собой представляет M[i]. Если M[i] еще не содержит конфигурацию стека, то записываем в M[i] пустую конфигурацию.P[i], учитывая конфигурацию стека M[i] и информацию, содержащуюся в метаданных. Если инструкция в принципе не P[i]. Другими словами, вычисляем конфигурацию стека S, которая получилась бы после реального выполнения инструкции.P[i]. Для каждой такой инструкции P[j] выполняем следующее:j < i, то проверяем, совместима ли конфигурация S с конфигурацией M[j]. Если оказывается, что несовместима, то алгоритм завершается неуспехом;j >= i и M[j] еще не содержит конфигурации стека, то M[j] = S ;j >= i и M[j] уже содержит конфигурацию стека, то выполняем попытку слияния конфигураций S и M[j]. Если попытка увенчалась успехом, то записываем результат слияния в M[j]. В противном случае алгоритм завершается неуспехом.Если все инструкции были просмотрены и ни на одном шаге алгоритм не завершился неуспехом, то метод успешно прошел верификацию.
Во второй главе мы рассмотрели написанную на языке C программу, осуществлявшую генерацию сборки .NET. Эта программа не использовала никаких библиотек для работы с метаданными и CIL-кодом, то есть задача генерации сборки была решена на самом низком уровне - на уровне двоичных форматов. Хотя такой подход во многих случаях бывает оправдан, на практике гораздо удобнее воспользоваться специальными библиотеками, предназначенными для разработчиков метаинструментов.
Напомним, что метаинструменты для платформы .NET - это программы, рассматривающие сборки .NET в качестве объектов анализа, генерации или преобразования. При этом анализ существующей сборки выполняется верификаторами, загрузчиками, отладчиками и т.д. Генерация новой сборки осуществляется компиляторами и
Сборка .NET представляет собой совокупность метаданных, CIL-кода и, возможно, ресурсов. Поэтому библиотеки, необходимые для создания метаинструментов, должны обеспечивать чтение и генерацию этой информации. В составе .NET Framework SDK поставляются две библиотеки, частично решающие эти задачи. Это
При использовании
Для того чтобы использовать
#include <corhlpr.h> #pragma comment(lib, "format.lib")
Первая строка подключает заголовочный файл, в котором описываются нужные интерфейсы, а также вспомогательные структуры данных и функции. Вторая строка дает указание компоновщику использовать библиотеку format.lib.
Взаимодействие с
| Интерфейс | Описание |
|---|---|
IMetadataDispenserEx |
Позволяет загружать образ исполняемого либо |
IMetadataImport |
Предназначен для чтения метаданных |
IMetadataEmit |
Предназначен для генерации метаданных |
Работа с
CoInitialize(NULL);
IMetaDataDispenser *dispenser;
HRESULT h = CoCreateInstance(
CLSID_CorMetaDataDispenser,
NULL,
CLSCTX_INPROC_SERVER,
IID_IMetaDataDispenserEx,
(void **)dispenser
);
if (h)
printf("Error");
Затем можно с помощью метода OpenScope этого интерфейса загрузить существующую сборку в память или с помощью метода DefineScope создать новую сборку. Оба метода возвращают указатель на интерфейс, через который можно в дальнейшем осуществлять чтение или генерацию метаданных. В следующем фрагменте программы происходит вызов метода OpenScope для чтения метаданных из сборки test.exe:
IMetaDataImport *mdimp;
HRESULT h = dispenser->OpenScope(
L"test.exe",
0,
IID_IMetaDataImport,
(IUnknown**)mdimp
);
if (h)
printf("Error");
При завершении работы с
mdimp->Release(); dispenser->Release();
Чтение метаданных из сборки осуществляется через методы интерфейса IMetadataImport, которые условно можно разделить на три основные группы:
EnumXXX возвращают массивы mdTypeDef tmp;
mdTypeDef *tokens;
HCORENUM Enum = 0;
unsigned long tokenCount;
mdimp->EnumTypeDefs(Enum,tmp,1,tokenCount);
mdimp->CountEnum(Enum,tokenCount);
if (tokenCount > 0)
{
tokens = new mdTypeDef [tokenCount];
tokens[0] = tmp;
if (tokenCount > 1)
mdimp->EnumTypeDefs(
Enum,tokens+1,tokenCount-1,
tokenCount
);
mdimp->CloseEnum(Enum);
}
Первый вызов метода EnumTypeDefs создает Enum ), а также возвращает первый элемент этого массива (он сохраняется в переменной tmp ).
Затем метод CountEnum записывает в переменную tokenCount размер массива tokens ) и второй раз вызывается метод EnumTypeDefs. Обратите внимание, что первому элементу массива tokens мы присваиваем значение переменной tmp.
FindXXX предназначены для поиска mdTypeDef token; mdimp->FindTypeByName(L"MyType1",NULL,token);
GetXXX используются для получения свойств strToken, выглядит так:unsigned short s[1024]; unsigned long len; mdimp->GetUserString(strToken,s,1024,len);
Обратите внимание, что для хранения одного символа применяется тип unsigned short. Причина в том, что для представления строковых данных в .NET используется 16-разрядная кодировка Unicode.
Генерация метаданных осуществляется через методы интерфейса IMetadataEmit, которые можно условно разделить на две группы:
DefineXXX добавляют новые SetXXX устанавливают свойства Сгенерированные метаданные могут быть сохранены на диске при помощи метода Save.
Библиотека рефлексии содержит классы для работы с метаданными на высоком уровне (эти классы располагаются в пространствах имен System. и System. ). Она соответствует спецификации
Метаинструмент, использующий библиотеку рефлексии, получает доступ к метаданным сборки через экземпляр класса Assembly, который создается при загрузке сборки в память. Затем через методы класса Assembly можно получить объекты класса Module, которые соответствуют модулям, входящим в сборку. Класс Module, в свою очередь, позволяет получить набор экземпляров класса Type для входящих в модуль типов, а уже через эти объекты можно добраться до конструкторов (класс ConstructorInfo ), методов (класс MethodInfo ) и полей (класс FieldInfo ) типов. То есть библиотека рефлексии спроектирована таким образом, что создание объектов рефлексии, соответствующих элементам метаданных, осуществляется путем вызова методов других объектов рефлексии (схема получения доступа к объектам рефлексии показана на рис. 4.5, при этом сплошные стрелки обозначают основные пути получения доступа, а пунктирные - дополнительные пути). Другими словами, конструкторы классов, входящих в библиотеку, для пользователя библиотеки недоступны. Информацию о любом элементе метаданных можно прочитать из свойств соответствующего ему объекта рефлексии.
(рис 4.5) Получение доступа к объектам рефлексии
Экземпляр класса Assembly, с создания которого, как правило, начинается работа со сборкой через рефлексию, можно получить разными способами:
GetExecutingAssembly класса Assembly:Assembly assembly = Assembly.GetExecutingAssembly()
GetAssembly:Assembly assembly = Assembly.GetAssembly(typeof(int))
LoadFrom:Assembly assembly = Assembly.LoadFrom("test.exe")
Сборка .NET, как правило, состоит из одного модуля, хотя существует возможность включения в сборку сразу нескольких модулей. Получение доступа к объектам класса Module, описывающим модули, из которых состоит сборка, осуществляется одним из двух способов:
GetModule класса Assembly:Module module = assembly.GetModule("SomeModule.exe")
GetModules для получения Module, соответствующих всем модулям в сборке:Module[] modules = assembly.GetModules(false)
Через объект класса Module мы можем обратиться к глобальным полям и функциям модуля (глобальные поля и функции не принадлежат ни одному классу), используя методы GetField и GetFields для полей, а также GetMethod и GetMethods для функций. При этом полям будут соответствовать объекты класса FieldInfo, а методам - объекты класса MethodInfo. В следующем примере мы выводим на экран сигнатуры всех глобальных функций некоторой сборки "test.exe":
Assembly assembly = Assembly.LoadFrom("test.exe");
Module[] modules = assembly.GetModules(false);
foreach (Module mod in modules)
{
MethodInfo[] methods = mod.GetMethods();
foreach (MethodInfo met in methods)
Console.WriteLine(met);
}
Особое место в библиотеке рефлексии занимает класс Type, представляющий типы. Область применения этого класса значительно шире, чем области применения остальных классов рефлексии, и этот факт отражен в том обстоятельстве, что класс Type входит в пространство имен System, а не System..
В каждом классе существует унаследованный от класса System.Object метод GetType, возвращающий экземпляр класса Type, который описывает этот класс. В следующем примере метод GetType будет использован для динамического определения типа объекта o (так как этот объект представляет собой строку, то на печать будет выведено сообщение "Sytem.String"):
object o = new String("qwerty");
Type t = o.GetType();
Console.WriteLine(t);
В C# определен специальный оператор typeof, который возвращает объект класса Type. Например, если нам нужно получить объект Type, представляющий
Type t = typeof(string[]);
Кроме этого, доступ к информации о типе можно получить, зная имя этого типа:
GetType класса Type:Type t = Type.GetType("System.Char");
Assembly или Module, которые соответствуют сборке или модулю, содержащему нужный тип:Type t = module.GetType("Class1") ;Type t = assembly.GetType("Class1").Мы также можем воспользоваться методами GetTypes классов Assembly и Module для получения массива типов, объявленных в сборке или модуле. В следующем примере на экран выводится список типов, содержащихся в сборке "test.exe":
Assembly assembly = Assembly.LoadFrom("test.exe");
Type[] types = assembly.GetTypes();
foreach (Type t in types)
Console.WriteLine(t);
Имея объект рефлексии, соответствующий некоторому типу, мы имеем возможность получить доступ к объектам рефлексии, описывающим его поля, конструкторы, методы, свойства и GetField, GetConstructor, GetMethod, GetProperty и GetNestedType класса Type. А если мы хотим получить массивы объектов рефлексии, описывающих члены типа, то нам нужно вызвать методы GetFields, GetConstructors, GetMethods, GetProperties и GetNestedTypes. В следующем примере на экран выводится список типов, содержащихся в сборке, вместе с сигнатурами их методов:
Assembly assembly = Assembly.LoadFrom("test.exe");
Type[] types = assembly.GetTypes();
foreach (Type t in types)
{
Console.WriteLine(""+t+":");
MethodInfo[] methods = t.GetMethods();
foreach (MethodInfo m in methods)
Console.WriteLine(" "+m);
}
Для каждого метода и конструктора мы можем получить доступ к GetParameters, объявленный в классах MethodInfo и ConstructorInfo. Данный метод возвращает массив объектов класса ParameterInfo:
ParameterInfo[] parms = method.GetParameters();
Кроме чтения метаданных, библиотека рефлексии позволяет создавать экземпляры типов, входящих в обрабатываемую сборку, вызывать методы этих типов, читать и изменять значения полей.
Рассмотрим класс TestClass:
class TestClass
{
private int val;
public TestClass() { val = 7; }
protected void print() { Console.WriteLine(val); }
}
В следующем примере мы используем метод MethodInfo для вызова метода print объекта класса TestClass. Обратите внимание, что метод print объявлен с protected.
static void CallMethodDemo()
{
TestClass a = new TestClass();
BindingFlags flags = (BindingFlags)
(BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo printer =
typeof(TestClass).GetMethod("print",flags);
printer.Invoke(a, null);
}
Путем внесения небольших изменений в CallMethodDemo мы можем добиться того, что объект класса TestClass будет также создаваться через рефлексию (с помощью вызова его конструктора):
static void CallMethodDemo2()
{
Type t = typeof(TestClass);
BindingFlags flags = (BindingFlags)
(BindingFlags.NonPublic | BindingFlags.Instance);
ConstructorInfo ctor = t.GetConstructor(new Type[0]);
MethodInfo printer = t.GetMethod("print",flags);
object a = ctor.Invoke(null);
printer.Invoke(a, null);
}
А теперь продемонстрируем, как через рефлексию можно получить значение поля объекта. Это достигается путем вызова метода GetField объекта класса Type:
static void GetFieldDemo()
{
TestClass a = new TestClass();
BindingFlags flags = (BindingFlags)
(BindingFlags.NonPublic |
BindingFlags.Instance);
FieldInfo val =
typeof(TestClass).GetField("val",flags);
Console.WriteLine(val.GetValue(a));
}
Для генерации метаданных в библиотеке рефлексии предназначены классы пространства имен System.. Создание новой сборки начинается с создания экземпляра класса AssemblyBuilder. Далее путем вызова методов класса AssemblyBuilder создается нужное количество модулей (объектов класса ModuleBuilder ), в модулях создаются типы (объекты класса TypeBuilder ), а в типах - конструкторы, методы и поля (объекты классов ConstructorBuilder, MethodBuilder и FieldBuilder, соответственно). То есть при генерации метаданных идеология такая же, как и при чтении их из готовой сборки (на рис. 4.6 представлена схема, показывающая последовательность создания метаданных).
(рис 4.6) Последовательность создания метаданных
В отличие от ILGenerator. Он позволяет после создания объекта MethodBuilder (или ConstructorBuilder ) сгенерировать для соответствующего метода (или конструктора)
В следующем примере библиотека рефлексии используется для генерации сборки .NET, состоящей из одного глобального метода main, выводящего на экран сообщение "Hello, World!":
using System;
using System.Threading;
using System.Reflection;
using System.Reflection.Emit;
class HelloGenerator
{
static void Main(string[] args)
{
AppDomain appDomain = Thread.GetDomain();
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = "hello.exe";
AssemblyBuilder assembly =
appDomain.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.RunAndSave
);
ModuleBuilder module =
assembly.DefineDynamicModule(
"hello.exe", "hello.exe"
);
MethodBuilder mainMethod =
module.DefineGlobalMethod(
"main",
MethodAttributes.Static |
MethodAttributes.Public,
typeof(void),
null
);
MethodInfo ConsoleWriteLineMethod =
((typeof(Console)).GetMethod("WriteLine",
new Type[] { typeof(string) } ));
ILGenerator il = mainMethod.GetILGenerator();
il.Emit(OpCodes.Ldstr,"Hello, World!");
il.Emit(OpCodes.Call,ConsoleWriteLineMethod);
il.Emit(OpCodes.Ret);
module.CreateGlobalFunctions();
assembly.SetEntryPoint(
mainMethod,PEFileKinds.ConsoleApplication
);
assembly.Save("hello.exe");
}
}
Возможности, предоставляемые библиотекой
| Чтение метаданных | + | + |
| Генерация метаданных | + | + |
| Чтение CIL-кода | - | - |
| Генерация CIL-кода | - | + |
Из таблицы следует, что ни одна из библиотек, поставляемых вместе с .NET Framework, не позволяет читать CIL-код, хотя эта возможность требуется целому ряду метаинструментов (например, верификаторам и оптимизаторам кода).
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.