Совместно используемые сборки и сборки со строгим именем

Два вида сборок - два вида развёртывания

CLR поддерживает два вида сборок: с нестрогими именами (прим. термин придуман Рихтером, в официальной документации его нет) и со строгими именами. Это одни и те же сборки, однако, сборки со строгим именем подписаны при помощи пары ключей, уникально идентифицирующих издателя сборки. Уникальная идентификация позволяет CLR при попытке привязки приложения к сборке применять политики безопасности.

Развёртывание может быть закрытым или глобальным. Для сборки с нестрогим именем допустимо лишь закрытое развёртывание.

Назначение сборки строгого имени

Если сборка используется несколькими приложениями, то её стоит поместить в общедоступный каталог. Однако тогда возникает "кошмар DLL" - появляется несколько сборок с одинаковым именем, последняя перезатирает первую и работа приложения, ссылавшегося на первую, нарушается.

image

Для различия сборок используют четыре атрибута: имя файла без расширения, номер версии, идентификатор регионального стандарта и открытый ключ (его хеш-код, так как ключи очень больше, хеш-код называют маркером открытого ключа/public key token).

Так как сборки с нестрогим именем всегда разворачиваются закрыто, то для их идентификации достаточно имени.

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

Глобальный кэш сборок

Место, где хранятся совместно используемые сборки, называется глобальным кэшем сборок (global assembly cache, GAC). GAC имеет иерархическое строение и содержит множество вложенных каталогов, имена которых генерируются по определённому алгоритму. Именно поэтому нельзя копировать сборки в GAC вручную, для этого надо использовать специальные инструменты (GACUtil.exe), которые умеют правильно создавать подкаталоги.

GACUtil.exe не поставляется свободно, поэтому для установки глобальных сборок можно использовать Windows Installer (MSI), так как это единственный инструмент, который позволяет установить сборки в GAC и гарантировано присутствует на машине конечного пользователя.

Регистрация сборки в GAC — это ещё один способ идентифицировать сборки с одинаковым именем, однако его стоит избегать, потому что такая установка делает невозможным простую установку, копирование, восстановление, перенос и удаление приложения.

Построение сборки, ссылающейся на сборку со строгим именем

При компоновке сборки, ссылающейся на System.Drawing.dll, компилятор найдёт её в том же каталоге, что и csc.exe. Однако загрузит он её из другого каталога.

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

Устойчивость сборок со строгими именами к несанкционированной модификации

Подписание файла закрытым ключом и внедрение подписи и открытого ключа в сборку позволяет CLR убедиться в том, что сборка не была модифицирована. При установке в GAC система хеширует содержимое файла с манифестом и сравнивает полученное значение с цифровой подписью RSA, встроенной в PE-файл. Кроме этого, хешируется содержимое других файлов сборки и сравнивание полученные значения с соответствующими из таблицы манифест FileDef.

Для поиска в GAC CLR использует свойства сборки. Если нужная сборка найдена, то возвращается путь к каталогу, где она находится и загружается файл с манифестом. Такой механизм гарантирует вызывающей стороне, что будет загружена нужная сборка. Эта гарантия возможна благодаря соответствию маркера открытого ключа в AssemblyRef и открытому ключу из AssemblyDef. Если сборки нет в GAC, то она ищется в базовом каталоге, а потом проверяются все закрытые пути. Потом, если приложение установление при помощи MSI, происходит поиск через него.

При загрузке из GAC CLR не проверяет их на несанкционированную модификацию, так как GAC и строги имена гарантируют это. В противном случае CLR необходимо дополнительное время, чтобы произвести проверку во время загрузки файла. Если обнаруживается несоответствие, то система выбрасывает System.IO.FileLoadException.

Отложенное подписание

Подготовившись к компоновке сборки со строгим именем, её надо подписать закрытым ключом. Однако при разработке и сборке очень неудобно постоянно доставать этот ключ, так как он хранится достаточно надёжно в компании. Поэтому .NET Framework поддерживает отложенное/частичное подписание (delayed/partial signing). Отложенное подписание позволяет построить сборку только с открытым ключом, оставляя её незащищённой к изменениям, что и не важно на этапе разработке. Готовая к компоновке сборка подписывается закрытым ключом.

Обнаружив, что подписание сборки откладывается, AL.exe генерирует в AssemblyDef запись с открытым ключом. Это позволяет разместить сборку в GAC. При этом в PE-файле остаётся место для подписи (которое высчитывается исходя из размеров открытого ключа). Хеширование при этом также не проводится.

Загрузить такую сборку в GAC можно, если запретить системе проверять целостность файлов сборки.

Алгоритм отложенного подписания:

  1. Создать сборку и подписать её открытым ключом.
  2. Разрешить добавлять в GAC нехешированные сборки.
  3. После завершения разработки подписать сборку закрытым ключом.
  4. Подчистить реестр, снова запретив добавление в GAC нехешированных сборок.

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

Закрытое развёртывание сборок со строгими именами

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

Как исполняющая среда разрешает ссылки на типы

В результате компиляции и компоновки получается сборка. При запуске приложения происходит загрузка и инициализация CLR. CLR сканирует CLR-заголовок сборки в поисках атрибута MethodDefToken, идентифицирующий метод Main, точку входа. CLR находит в таблице метаданных MethodDef смещение, по которому находится IL-код. Этот код компилируется в машинный с помощью JIT-компилятора. Во время JIT-компиляции CLR обнаруживает все типы и члены и сборки, в которых они определены. Маркер строки кода в IL идентифицирует запись в таблице MemberRef. Просматривая эту таблицу, CLR видит, что одно из полей ссылается на элемент таблицы TypeRef, которая направляет CLR к записи в таблице AssemblyRef. После этого CLR нужно только найти сборку в одном из трёх мест:

  • В том же файле. Обращение к типу, расположенному в том же файле, определяется при компиляции (раннее связывание). Тип загружается из файла и исполнение продолжается.
  • В другом файле той же сборки. CLR проверяет, что файл, на который ссылаются, описан в таблице FileRef в манифесте текущей сборки. При этом файл ищет в каталоге, откуда был загружен файл, содержащий манифест. Файл загружается, проверяется на целостность, затем CLR находит в нём нужный тип, и исполнение продолжается.
  • В файле другой сборки. Если файл находится в отдельной сборке, то сначала загружается файл с манифестом. Если нет там, то загружается соответствующий PE-файл.

Если во время разрешения ссылки файл не найден, то выбрасывается соответствующее исключение, которое можно обработать программно при помощи рефлексии.

image

Такой процесс верен для любой сборки, кроме стандартных. Сборки .NET Framework (в том числе MSCorLib.dll) тесно связаны с CLR. Любая такая сборка привязывается к версии CLR. Этот процесс называется унификацией, и Microsoft это поддерживает, так как для гарантии работоспособности все сборки тестируются на конкретной версии CLR.

Дополнительные административные средства (конфигурационные файлы)

Для поиска перемещённых сборок используется конфигурационный файл. Основные элементы:

  • Элемент probing. Определяет директории для поиска файлов сборок с нестрогим именем. Сборки со строгим именем ищутся в GAC или по url.
  • Первый набор элементов dependentAssembly, assemblyIdentity и bindingRedirect. Определяет перенаправления при поиске определённых сборок.
  • Элемент codebase. URL, по которому происходит попытка поиска dll.
  • Второй набор элементов dependentAssembly, assemblyIdentity и bindingRedirect. Определяет перенаправления при поиске определённых сборок.
  • Элемент publisherPolicy. Описание поведения с файлом политики издателя.

Управление версиями при помощи политики издателя

Политика издателя - удобный инструмент для переопределения сборок с целью исправить ошибки без редакции исходного приложения.

Сборка, скомпонованная с политикой издателя, должна помещаться в GAC.

Издатель должен создавать сборку со своей политикой лишь для развёртывания исправленной сборки, установка нового приложения не должна требовать политики издателя.

Если администратора не устроит новая сборка (например, в ней стало ещё больше ошибок), он может отменить исправления.

Сборка с политикой издателя должна использоваться как инструмент исправления ошибок, потому что иначе придётся тестировать новую сборку на обратную совместимость. Если же к сборке добавляются новые функции, то стоит подумать о том, чтобы отказаться от связи с прежними сборками.