Пятница, 28 сентября 2012 15:25

Модель выполнения кода в среде CLR: Загрузка CLR и исполнение кода сборки

Оцените материал
(2 голосов)

Загрузка CLR

Каждая создаваемая сборка представляет собой либо исполняемое приложение, либо библиотеку DLL, содержащую набор типов (компонентов) для использования в исполняемом приложении. Разумеется, среда CLR отвечает за управление исполнением кода.

Это значит, что на компьютере, выполняющем данное приложение, должна быть установлена платформа .NET Framework. В компании Microsoft был создан дистрибутивный пакет .NET Framework для свободного распространения, который вы можете бесплатно поставлять своим клиентам. Некоторые версии операционной системы семейства Windows поставляются с уже установленной платформой .NET Framework.

Для того чтобы понять, установлена ли платформа .NET Framework на компьютере, нужно попробовать найти файл MSCorEE.dll в каталоге %SystemRoot%\system32. Если он есть, то платформа .NET Framework установлена. Однако на одном компьютере может быть установлено одновременно несколько версий .NET Framework. Чтобы определить, какие именно версии установлены, проверьте следующий подраздел системного реестра:

{codecitation class="brush: plain; gutter: true;" width="100%" }HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NET Framework Setup\NDP{/codecitation}

В комплекте .NET Framework SDK компания Microsoft поставляет утилиту командной строки CLRVer.exe, позволяющую узнать, какие версии CLR установлены на машине, а также, какая именно версия среды CLR используется текущими процессами. Для этого нужно указать параметр -all или идентификатор интересующего процесса.

Прежде чем мы перейдем к загрузке среды CLR, поговорим поподробнее об особенностях 32- и 64-разрядных версий операционной системы Windows. Если сборка содержит только управляемый код с контролем типов, она должна одинаково хорошо работать на обеих версиях системы. Дополнительной модификации исходного кода не требуется. Созданный компилятором готовый ЕХЕ- или DLL-файл будет выполняться как на 32-разрядной Windows, так и на версиях х64 и IA64 64-разрядной Windows! Другими словами, один и тот же файл будет работать на любом компьютере с установленной платформой .NET Framework.

 В исключительно редких случаях разработчикам понадобится писать код, совместимый только с какой-то конкретной версией Windows. Обычно это требуется при работе с небезопасным кодом (unsafe code) или для взаимодействия с неуправляемым кодом, ориентированным на конкретную процессорную архитектуру. Для таких случаев у компилятора С# предусмотрен параметр командной строки /platform. Этот параметр позволяет указать конкретную версию целевой платформы, на которой планируется работа данной сборки: архитектуру х86, использующую только 32-разрядную систему Windows, архитектуру х64, использующую только 64-разрядную операционную систему Windows, или архитектуру Intel ltanium, использующую только 64-разрядную систему Windows. Если не указать платформу, компилятор задействует значение по умолчанию anycpu, которое означает, что сборка может выполняться на любой из данных операционных систем Windows. Пользователи Visual Studio могут указать целевую платформу в списке Platform Target на вкладке Build окна свойств проекта (рис. 1.3).

Рис. 1.3. Определение целевой платформы средствами Visual Studio

В зависимости от указанной целевой платформы С# генерирует заголовок - РЕ32 или РЕ32+, а также указывает требуемую процессорную архитектуру (или информирует о независимости от архитектуры) в заголовке. Для анализа заголовочной информации, созданной компилятором в управляемом модуле, Microsoft предоставляет две утилиты - DumpBin.exe и CorFlags.exe.

При запуске исполняемого файла Windows анализирует заголовок ЕХЕ­ файла для определения того, какое именно адресное пространство необходимо для его работы - 32- или 64-разрядное. Файл с заголовком РЕ32 может выполняться в адресном пространстве любого из указанных двух типов, а файлу с заголовком РЕ32+ требуется 64-разрядное пространство. Windows также проверяет информацию о процессорной архитектуре на предмет совместимости с имеющейся конфигурацией. Наконец, 64-разрядные версии Windows поддерживают технологию выполнения 32-разрядных приложений в 64-разрядной среде, которая называется WoW64 (Windows on Windows64). Она даже позволяет выполнять 32-разрядные приложения на машине с процессором Itanium за счет эмуляции команд х86, но за это приходится расплачиваться снижением производительности.

Таблица 1.2 иллюстрирует две важные вещи. Во-первых, в ней показан тип получаемого управляемого модуля при указании разных параметров /platform командной строки компилятора С#. Во-вторых, в ней представлены режимы выполнения приложений в различных версиях Windows.

Таблица 1.2. Влияние заданного значения параметра /platform на получаемый модуль и режим выполнения

Значениепараметра/platform

Типвыходноrо управляемоrо модуля

x86 Windows

x64 Windows

IA64 Windows

anycpu (по умолчанию)

РЕ32/независимый от платформы

Выполняется как 32-разрядное приложение

Выполняется как 64-разрядное приложение

Выполняется как 32-разрядное приложение

x86

РЕ32/х86

Выполняется как 32-разрядное приложение

Выполняется как WoW64-разрядное приложение

Выполняется как WoW64-разрядное приложение

x64

РЕ32+/х64

Невыполняется

Выполняется как 64-разрядное приложение

Невыполняется

Itanium

РЕ32+/Itanium

Невыполняется

Невыполняется

Выполняется как 64-разрядное приложение

 

После анализа заголовка ЕХЕ-файла для выяснения того, какой процесс необходимо запустить - 32, 64-разрядный или WoW64, - Windows загружает в адресное пространство процесса соответствующую (х86, х64 или IA64) версию библиотеки MSCorEE.dll. В системе Windows версии х86 одноименная версия MSCorEE.dll хранится в каталоге C:\Windows\System32. В системах х64 и IA64 версия х86 библиотеки находится в каталоге C:\Windows\SysWow64, а 64-разрядная версия MSCorEE.dll (х64 или IA64) размещается в каталоге С: \ Windows\System32 (это сделано из соображений обратной совместимости). Далее основной поток вызывает определенный в библиотеке MSCorEE.dll метод, который инициализирует CLR, загружает сборку ЕХЕ, а затем вызывает ее метод Main, в котором содержится точка входа. На этом процедура запуска управляемого приложения считается завершенной

Примечание. Сборки, созданные при помощи версий 7.0 и 7.1 компилятора С# от Microsoft, содержат заголовок РЕЗ2 и не зависят от архитектуры процессора. Тем не менее во время выполнения среда CLR считает их совместимыми только с архитектурой х86. Это повышает вероятность максимально корректной работы в 64-разрядной среде, так как исполняемый файл загружается в режиме WoW64, который обеспечивает процессу среду, максимально приближенную к существующей в 32-разрядной версии х86 Windows.

Когда неуправляемое приложение вызывает LoadLibrary, Windows автоматически загружает и инициализирует CLR (если это еще не сделано) для обработки содержащегося в сборке кода. Ясно, что в такой ситуации предполагается, что процесс запущен и работает, и это сокращает область применимости сборки. В частности, управляемая сборка, скомпилированная с параметром /platform: х86, не сможет загрузиться в 64-разрядный процесс, а исполняемый файл с таким же параметром загрузится в режиме WoW64 на компьютере с 64-разрядной Windows.

Исполнение кода сборки

Как говорилось ранее, управляемые модули содержат метаданные и программный код, написанный на языке IL. Это не зависящий от процессора машинный язык, разработанный компанией Microsoft после консультаций с несколькими коммерческими и академическими организациями, специализирующимися на разработке языков и компиляторов. IL - язык более высокого уровня по сравнению с большинством других машинных языков. Он позволяет работать с объектами и имеет команды для создания и инициализации объектов, вызова виртуальных методов и непосредственного манипулирования элементами массивов. В нем даже есть команды выбрасывания и перехвата исключений для обработки ошибок. IL можно рассматривать как объектно-ориентированный машинный язык.

Обычно разработчики программируют на высокоуровневых языках, таких как С#, C++/CLI или Visual Basic. Компиляторы этих языков генерируют IL-код. Однако такой код может быть написан и на языке ассемблера, так, Microsoft предоставляет ассемблер IL (ILAsm.exe), а также дизассемблер IL (ILDasm.exe).

Имейте в виду, что любой язык высокого уровня, скорее всего, использует лишь часть возможностей, предоставляемых CLR. При этом язык ассемблера IL открывает доступ ко всем возможностям CLR. В случае если выбранный вами язык программирования не дает доступа именно к тем функциям CLR, которые необходимы, можно написать часть программного кода на языке ассемблера IL или на другом языке программирования, позволяющем их задействовать.

Единственный способ узнать о возможностях CLR, доступных при использовании конкретного языка, - изучить соответствующую документацию. В этой книге сделан акцент на возможностях среды CLR и на том, какие из этих возможностей доступны при программировании на С#. Можно сделать предположение, что в других книгах и статьях среда CLR рассмотрена с точки зрения других языков и разработчики получат представление лишь о тех ее функциях, которые доступны при использовании описанных там языков. По крайней мере, если выбранный язык решает поставленные задачи, такой под­ ход не так уж плох.

Примечание. Возможность легко переключаться между языками при их тесной интеграции - чудесное качество CLR. К сожалению, я также практически уверен, что разработчики часто будут проходить мимо нее. Такие языки, как С# и Visual Basic, прекрасно подходят для программирования ввода-вывода. Язык APL (A Programmiпg Laпguage) - замечательный язык для инженерных и финансовых расчетов. Среда CLR позволяет написать на С# часть приложения, отвечающую за ввод-вывод, а инженерные рас­ четы - на языке APL. Среда CLR предлагает беспрецедентный уровень интеграции этих языков, и во многих проектах стоит серьезно задуматься об использовании одновременно нескольких языков.

Для выполнения какого-либо метода его IL-код должен быть преобразован в машинные команды. Этим занимается JIТ-компилятор (Just-ln-Time) среды CLR.

На рис. 1.4 показано, что происходит при первом обращении к методу.

Рис. 1.4. Первый вызов метода

Непосредственно перед исполнением метода Main среда CLR находит все типы данных, на которые ссылается программный код метода Main. При этом CLR выделяет внутренние структуры данных, используемые для управления доступом к типам, на которые есть ссылки. На рисунке 1.4 метод Main ссылается на единственный тип - Console, и среда CLR выделяет единственную внутреннюю структуру. Эта внутренняя структура данных содержит по одной записи для каждого метода, определенного в типе Consolе. Каждая запись содержит адрес, по которому можно найти реализацию метода. При инициализации этой структуры CLR заносит в каждую запись адрес внутренней недокументированной функции, содержащейся в самой среде CLR. Эта функция называется JIТCompiler.

Когда метод Main первый раз обращается к методу Writeline, вызывается функция JIТCompiler. Она отвечает за компиляцию IL-кода вызываемого метода в собственные команды процессора. Поскольку IL-код компилируется непосредственно перед выполнением («just in time» ), этот компонент CLR часто называют JIТ-компилятором.

Примечание. Если приложение исполняется в х86 версии Windows или в режиме WoW64, JIТ-компилятор генерирует команды для х86 архитектуры. Для приложений, выполняющихся как 54-разрядные в версии х64 или ltanium ОС Windows, JIТ-компилятор генерирует соответственно команды для архитектуры х64 или IA64.

Функции JitCompiler известен вызываемый метод и тип, в котором он определен. JIТCompiler ищет в метаданных соответствующей сборки IL-код вызываемого метода. Затем JIТCompiler проверяет и компилирует IL-код в собственные машинные команды, которые сохраняются в динамически выделенном блоке памяти. После этого JITCompiler возвращается к структуре внутренних данных типа, созданной средой CLR, и заменяет адрес вызываемого метода адресом блока памяти, содержащего готовые машинные команды. В завершение JIТCompiler передает управление коду в этом блоке памяти. Этот программный код является реализацией метода Writeline (вариант этого метода с параметром String). Из этого метода управление возвращается в метод Main, который про­ должает выполнение в обычном порядке.

Рассмотрим повторное обращение метода Маin к методу Writeline. К этому моменту код метода Writeline уже проверен и скомпилирован, так что обращение к блоку памяти производится, минуя вызов JIТCompi1er. Отработав, метод Writeline возвращает управление методу Маin. На рис. 1.5 показано, как выглядит ситуация при повторном обращении к методу Writeline.

Снижение производительности наблюдается только при первом вызове метода. Все последующие обращения выполняются «На максимальной скорости», потому что повторная верификация и компиляция не производятся.

 

Рис. 1.5. Повторный вызов метода

JIТ-компилятор хранит машинные команды в динамической памяти. Это значит, что скомпилированный код уничтожается по завершении работы приложения. Для повторного вызова приложения или для параллельного запуска его второго экземпляра (в другом процессе операционной системы) JIТ-компилятору придется заново скомпилировать IL-код в машинные команды.

Для большинства приложений снижение производительности, связанное с работой JIT -компилятора, незначительно. Большинство приложений раз за разом обращается к одним и тем же методам. На производительности это сказывается только один раз во время выполнения приложения. К тому же больше времени занимает выполнение самого метода, а не обращение к нему.

Необходимо также знать, что JIТ-компилятор среды CLR оптимизирует машинный код аналогично компилятору неуправляемого кода С++. И опять же: создание оптимизированного кода занимает больше времени, но при выполнении он гораздо производительнее, чем неоптимизированный.

Есть два параметра компилятора С#, влияющих на оптимизацию кода, - /optimize и /debug. В следующей таблице показан о их влияние на качество IL-кода, созданного компилятором С#, и машинного кода, сгенерированного JIT-компилятором.

Параметрыкомпилятора

Качество IL-кода компилятора

Качествомашинного JIТ-кода

/optimize- /debug-(поумолчанию)

Неоптимизированный

Оптимизированный

/optimize- /debug(+/full/pdbonly)

Неоптимизированный

Неоптимизированный

/optimize+ /debug(-/+/full /pbdonly)

 

Оптимизированный

Оптимизированный

 

С параметром /optimize- компилятор С# генерирует неоптимизированный IL-код, содержащий множество пустых команд (no-operation, NOP). Эти команды предназначены для поддержки функции «редактирования с продолжением выполнения» (edit-and-continue) в Visual Studio во время процесса отладки. Они также упрощают процесс отладки, позволяя расставлять точки останова (breakpoints) на управляющих командах, таких как for, whilе, do, if, else, а также блоках try, catch и finally. Во время оптимизации IL-кода компилятор С# удаляет эти посторонние команды, усложняя процесс отладки кода, но зато оптимизируя поток управления программой. Кроме того, возможно, некоторые оценочные функции не выполняются во время отладки. Однако IL-код меньше по размерам, и это уменьшает результирующий размер ЕХЕ- или DLL-файлов; кроме того, IL-код легче читать тем, кто обожает исследовать IL-код, пытаясь понять, что именно породил компилятор.

Прочитано 13118 раз
Другие материалы в этой категории: « Сборки Компоновка типов в модуль, метаданные »
   

Поиск  

   

Материалы