Common Intermediate Language ( CIL ), ранее называвшийся Microsoft Intermediate Language ( MSIL ) или Intermediate Language ( IL ), [1] — это набор двоичных инструкций промежуточного языка , определенный в спецификации Common Language Infrastructure (CLI). [2] Инструкции CIL выполняются средой выполнения, совместимой с CIL, такой как Common Language Runtime . Языки, ориентированные на CLI, компилируются в CIL. CIL — это объектно-ориентированный байт -код на основе стека . Среды выполнения обычно компилируют инструкции CIL в машинный код «точно вовремя» .
CIL изначально был известен как Microsoft Intermediate Language (MSIL) во время бета-релизов языков .NET. Благодаря стандартизации C# и CLI, байт-код теперь официально известен как CIL. [3] Определения вирусов Windows Defender продолжают ссылаться на двоичные файлы, скомпилированные с его помощью, как на MSIL. [4]
Во время компиляции языков программирования CLI исходный код транслируется в код CIL, а не в объектный код, специфичный для платформы или процессора . CIL — это набор инструкций, не зависящий от процессора и платформы, который может выполняться в любой среде, поддерживающей Common Language Infrastructure, например, среда выполнения .NET в Windows или кроссплатформенная среда выполнения Mono . Теоретически это устраняет необходимость распространять различные исполняемые файлы для различных платформ и типов процессоров. Код CIL проверяется на безопасность во время выполнения, обеспечивая лучшую безопасность и надежность, чем изначально скомпилированные исполняемые файлы. [5] [6]
Процесс выполнения выглядит следующим образом:
Байт-код CIL содержит инструкции для следующих групп задач:
Общий промежуточный язык является объектно-ориентированным и основан на стеке , что означает, что параметры инструкций и результаты хранятся в одном стеке, а не в нескольких регистрах или других областях памяти, как в большинстве языков программирования .
Код, который складывает два числа на языке ассемблера x86 , где eax и edx указывают два разных регистра общего назначения :
добавить eax , edx
Код на промежуточном языке (IL), где 0 — это eax, а 1 — это edx:
ldloc . 0 // поместить локальную переменную 0 в стек ldloc . 1 // поместить локальную переменную 1 в стек add // извлечь и добавить два верхних элемента стека, затем поместить результат в стек stloc . 0 // извлечь и сохранить верхний элемент стека в локальной переменной 0
В последнем примере значения двух регистров, eax и edx, сначала помещаются в стек. Когда вызывается инструкция добавления, операнды "выталкиваются" или извлекаются, а результат "выталкивается" или сохраняется в стеке. Затем полученное значение выталкивается из стека и сохраняется в eax.
CIL разработан как объектно-ориентированный. Можно создавать объекты, вызывать методы и использовать другие типы членов, такие как поля.
Каждый метод должен (за некоторыми исключениями) находиться в классе. То же самое касается и этого статического метода:
. class public Foo { . method public static int32 Add ( int32 , int32 ) cil managed { . maxstack 2 ldarg . 0 // загрузить первый аргумент; ldarg . 1 // загрузить второй аргумент; add // добавить их; ret // вернуть результат; } }
Метод Add не требует объявления какого-либо экземпляра Foo, поскольку он объявлен как статический, и его можно использовать в C# следующим образом:
int r = Foo.Add ( 2,3 ) ; // 5
На языке CIL это будет выглядеть так:
ldc.i4.2 ldc.i4.3 call int32 Foo :: Add ( int32 , int32 ) stloc.0
Класс экземпляра содержит по крайней мере один конструктор и несколько членов экземпляра . Следующий класс имеет набор методов, представляющих действия объекта Car.
. class public Car { . method public specialname rtspecialname instance void . ctor ( int32 , int32 ) cil managed { /* Конструктор */ } . method public void Move ( int32 ) cil manage { /* Пропуск реализации */ } . method public void TurnRight () cil manage { /* Пропуск реализации */ } . method public void TurnLeft () cil manage { /* Пропуск реализации */ } . method public void Brake () cil manage { /* Пропуск реализации */ } }
В C# экземпляры классов создаются следующим образом:
Автомобиль myCar = новый автомобиль ( 1 , 4 ); Автомобиль yourCar = новый автомобиль ( 1 , 3 );
И эти утверждения примерно совпадают с этими инструкциями в CIL:
ldc.i4.1 ldc.i4.4 newobj instance void Car :: . ctor ( int , int ) stloc.0 // myCar = new Car ( 1 , 4 ) ; ldc.i4.1 ldc.i4.3 newobj instance void Car :: . ctor ( int , int ) stloc.1 // yourCar = new Car ( 1 , 3 ) ;
Методы экземпляра вызываются в C# следующим образом:
myCar . Переместить ( 3 );
Как указано в CIL:
ldloc . 0 // Загрузить объект "myCar" в стек ldc . i4 . 3 вызвать экземпляр void Car :: Move ( int32 )
Common Language Infrastructure (CLI) записывает информацию о скомпилированных классах в виде метаданных . Подобно библиотеке типов в Component Object Model , это позволяет приложениям поддерживать и обнаруживать интерфейсы, классы, типы, методы и поля в сборке. Процесс чтения таких метаданных называется « рефлексией ».
Метаданные могут быть данными в форме «атрибутов». Атрибуты можно настраивать, расширяя Attribute
класс. Это мощная функция. Она позволяет создателю класса украсить его дополнительной информацией, которую потребители класса могут использовать различными осмысленными способами в зависимости от области применения.
Ниже приведена базовая программа "Hello, World!", написанная на ассемблере CIL. Она выведет на экран строку "Hello, world!".
. assembly Hello {} . assembly extern mscorlib {} . method static void Main () { . entrypoint . maxstack 1 ldstr "Привет, мир!" call void [ mscorlib ] System . Console :: WriteLine ( string ) ret }
Следующий код более сложен по количеству кодов операций.
Этот код также можно сравнить с соответствующим кодом в статье о байт-коде Java .
static void Main ( string [ ] args ) { for ( int i = 2 ; i < 1000 ; i ++ ) { for ( int j = 2 ; j < i ; j ++ ) { if ( i % j == 0 ) goto external ; } Console.WriteLine ( i ) ; external :; } }
В синтаксисе ассемблера CIL это выглядит так:
. метод private hidebysig static void Main ( string [] args ) cil управляемый { . entrypoint . maxstack 2 . locals init ( int32 V_0 , int32 V_1 ) ООО . i4 .2 стлок .0 бр . s IL_001f IL_0004 : ldc . i4 .2 стлок .1 комн . s IL_0011 IL_0008 : ldloc .0 ldloc .1 rem brfalse . s IL_001b ldloc .1 ldc . i4 .1 добавить stloc .1 IL_0011 : ldloc .1 ldloc .0 blt . s IL_0008 ldloc .0 call void [ mscorlib ] System . Консоль :: WriteLine ( int32 ) IL_001b : ldloc .0 ldc . i4 .1 добавить stloc .0 IL_001f : ldloc .0 ldc . i4 0x3e8 блт . с IL_0004 в отставку }
Это всего лишь представление того, как выглядит CIL вблизи уровня виртуальной машины (VM). При компиляции методы хранятся в таблицах, а инструкции хранятся в виде байтов внутри сборки, которая является Portable Executable (PE).
Сборка CIL и инструкции генерируются либо компилятором, либо утилитой, называемой ассемблером IL ( ILAsm ), которая поставляется вместе со средой выполнения.
Собранный CIL также можно снова разобрать в код с помощью IL Disassembler (ILDASM). Существуют и другие инструменты, такие как .NET Reflector , которые могут декомпилировать CIL в высокоуровневый язык (например, C# или Visual Basic ). Это делает CIL очень легкой целью для обратного проектирования. Эта черта свойственна и байт-коду Java . Однако существуют инструменты, которые могут запутать код и сделать так, что код будет нелегко читать, но все равно будет запускаться.
Компиляция Just-in-time (JIT) подразумевает преобразование байт-кода в код, немедленно исполняемый ЦП. Преобразование выполняется постепенно во время выполнения программы. Компиляция JIT обеспечивает оптимизацию, специфичную для среды, безопасность типов во время выполнения и проверку сборки. Для этого компилятор JIT проверяет метаданные сборки на наличие несанкционированного доступа и соответствующим образом обрабатывает нарушения.
Совместимые с CLI среды выполнения также поддерживают возможность предварительной компиляции (AOT) сборки, что ускоряет ее выполнение за счет удаления процесса JIT во время выполнения.
В .NET Framework есть специальный инструмент под названием Native Image Generator (NGEN), который выполняет AOT. Другой подход для AOT — CoreRT , который позволяет компилировать код .Net Core в один исполняемый файл без зависимости от среды выполнения. В Mono также есть возможность выполнить AOT.
Заметным отличием от байт-кода Java является то, что CIL поставляется с ldind
, stind
, ldloca
, и многими инструкциями вызова, которых достаточно для манипуляции указателями данных/функций, необходимой для компиляции кода C/C++ в CIL.
класс A { public : virtual void __stdcall meth () {} }; void test_pointer_operations ( int param ) { int k = 0 ; int * ptr = & k ; * ptr = 1 ; ptr = & param ; * ptr = 2 ; A a ; A * ptra = &a a ; птра -> мет (); }
Соответствующий код на языке CIL можно представить следующим образом:
. метод сборки static void modopt ([ mscorlib ] System . Runtime . CompilerServices . CallConvCdecl ) test_pointer_operations ( int32 param ) cil managed { . vtentry 1 : 1 // Размер кода 44 (0x2c) . maxstack 2 . locals ([ 0 ] int32 * ptr , [ 1 ] valuetype A * V_1 , [ 2 ] valuetype A * a , [ 3 ] int32 k ) // k = 0; IL_0000 : ldc . i4 .0 IL_0001 : stloc .3 // ptr = &k; IL_0002 : ldloca . s k // инструкция загрузки адреса локальной переменной IL_0004 : stloc .0 // *ptr = 1; IL_0005 : ldloc.0 IL_0006 : ldc . i4 .1 IL_0007 : стоять . i4 // инструкция косвенного обращения // ptr = ¶m IL_0008 : ldarga . s param // инструкция загрузки адреса параметра IL_000a : stloc .0 // *ptr = 2 IL_000b : ldloc .0 IL_000c : ldc . i4 .2 IL_000d : стоять . i4 // a = новый A; IL_000e : ldloca . s a IL_0010 : вызов valuetype A * modopt ([ mscorlib ] System . Runtime . CompilerServices . CallConvThiscall ) ' A . { ctor } ' ( valuetype A * modopt ([ mscorlib ] Система . Время выполнения . Службы компилятора . IsConst ) modopt ([ mscorlib ] System . Runtime . CompilerServices . IsConst )) IL_0015 : pop // ptra = &a; IL_0016 : ldloca . IL_0018 : stloc .1 // ptra->meth() ; IL_0019 : ldloc.1 IL_001a : dup IL_001b : ldind . i4 // чтение VMT для виртуального вызова IL_001c : ldind . i4 IL_001d : calli unmanaged stdcall void modopt ([ mscorlib ] System . Runtime . CompilerServices . CallConvStdcall )( native int ) IL_0022 : ret } // конец метода 'Global Functions'::test_pointer_operations
CIL: ... Когда мы компилируем [проект]. NET, он [преобразуется] не напрямую в двоичный код, а в промежуточный язык. Когда проект запускается, каждый язык программирования .NET преобразуется в двоичный код в CIL. Только некоторая часть CIL, которая требуется во время выполнения, преобразуется в двоичный код. DLL и EXE .NET также находятся в форме CIL.