Основы .NET – структура .NET сборки

Microsoft .NET

Иногда не .NET разработчики (VB6, C++ и т.д.) просят меня объяснить «как работает .NET, как работает GC (сборщик мусора), почему боксинг это плохо и т.д. и т.п.». Обычно я пытаюсь найти ссылку и сохранить мое время, но для некоторых тем я не могу найти подходящих ссылок (или же они слишком широко или узко и частично раскрывают нужный вопрос).  Поэтому чтобы сохранить мое время при будущих таких объяснениях, я решил сделать несколько постов объясняющих основы .NET. К тому же я устал от всех этих архитектурных постов:)

Объяснение на общем уровне

.NET  фреймворк дает разработчикам свободу выбора языка на котором они предпочитают программировать под .NET (C#, VB, C++/CLI …). Он даже позволяет использовать несколько языков в одном проекте где код разработанный на разных языках легко взаимодействует между собой. Это возможно благодаря тому, что .NET фреймворк оперирует только промежуточным языком (IL). IL код создается во время компиляции языковыми компиляторами, которые транслируют код высокого уровня (c#, vb..) в комбинацию языконезависимого IL кода и его метаданных. IL код вместе с этими метаданными и хедерами составляет модуль, который называется управляемым модулем.

Один или несколько управляемых модулей и ноль или больше ресурсных файлов объеденяются языковым компилятором или линкером сборок в управляемую сборку, которую мы видим как .NET DLL файл. Каждая сборка также содержит вложенный файл манифеста, который описывает структуру определения членов сборки, структуру ссылок на члены внешних сборок и т.д.

image 2  Основы .NET – структура .NET сборки

Эта диаграмма и приведенное объяснение на общем уровне достаточны для понимания общей картины, но я предпочитаю всегда дополнять общее объяснение некоторыми конкретными деталями реализации, так что я сделаю это и здесь, подробнее объяснив структуру управляемого модуля.

Файл кода C#

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

Что то вроде этого:

namespace CSharp_ILCode
{
    class Program
    {
        static void Main(string[] args)
        {
            System.Console.WriteLine("Hello world!");
            Hello2();
        }

        static void Hello2()
        {
            System.Console.WriteLine("Hello world 2x!");
        }
    }
}

Как мы видим на диаграмме выше, этот код во время компиляции будет «переведен» на IL язык с соответствующим определением метаданных и все это станет одним управляемым модулем, который войдет в управляемую сборку.

Управляемый модуль

Любой управляемый модуль, независимо от того из какого кода он был создан, состоит из следующих четырех больших частей:

  1. Хедера PE32
  2. CLR хедера
  3. Метаданных
  4. IL кода

PE хедер

Каждый управляемый модуль содержит стандартный windows PE execute хедер как и не управляемые — родные приложения. Единственным отличием в случае управляемого кода является то, что масса информации PE хедера просто игнорируется, в то время как в случае нативного кода PE хедер содержит информацию про родной код CPU.

Чтобы получить некоторую информацию про PE хедер, в командной строке Visual studio нужно выполнить следующую комманду:

dumpbin /all assembly_name > result.txt

Эта команда создаст файл result.txt, который будет содержать следующую информацию о PE хедере (среди множества другой информации:

image thumb 10  Основы .NET – структура .NET сборки

На этой картинке мы видим что PE хедер содержит информацию про:

  • какого типа является этот модуль
  • каково значение временной метки создания модуля
  • для какой архитектуры CPU оптимизирован IL код (PE32 32 bit/64 bit Windows, PE32+ Win 64 bit only)
  • точка входа (entry point) представляющая адрес в памяти функции _CorExeMain() (больше про это в секции «Исполнение сборки» этого поста)

Пока я рассказываю про PE хедер, я воспользуюсь возможностью и отвечу на один из общих вопросов по .NET который я часто слышу:

«Как из PE хедера определить что модуль является управляемым модулем?»

Если мы промотаем вниз опциональные значения хедера в результирующем dumpbin текстовом файле, то увидим что число директорий 10, и это число больше чем число директорий в родных сборках. Одна из дополнительных директорий, специфичная для управляемого кода, это «COM Descriptor Directory» и это та запись в этой «таблице содержания», которая описывает где во время исполнения получить метаданные и IL.

image thumb 11  Основы .NET – структура .NET сборки

CLR хедер

Если мы промотаем файл result.txt до секции хедера CLR, мы увидим следующее:

image thumb 13  Основы .NET – структура .NET сборки

Отсюда мы видим:

  • целевая версия CLR для этого модуля 2.05 (NET 2 SP1)
  • модуль состоит только из управляемого кода
  • точка входа управляемого модуля — метод Main имеет значение токена метаданных равное 6000001

Метаданные

Пока у нас еще открыт dumbin result.txt файл, давайте взглянем на что то очень крутое, а именно на то как распознать где в модуле начинается сегмент метаданных.

Если промотаем к первой строке данных, то увидим что то вроде этого:

image thumb 15  Основы .NET – структура .NET сборки

Начало блока определения метаданных обознчено байтами 42 53 4A 42 (BSJB), которые являются первыми буквами имен разработчиков реализулющих метаданные во фреймворке .NET 1.0. Я провел 2 часа пытаясь найти их имена, но никакого результата… Похоже что либо никто не знает кто они, либо не хочет называть их.

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

Чтобы использовать утилиту нужно опять открыть командную строку Visual Studio, перейти в папку где находится результирирующая сборка и выполнить следуюущую команду:

ildasm CSharp_ILCode.exe

Как только это будет выполнено, мы увидим окно приложения ILDasm, которое покажет первую часть встроенного файла манифеста.

image thumb 16  Основы .NET – структура .NET сборки

Чтобы увидеть что содержит манифест я дважды кликнул по нему. Результирующее окно содержит определение данных уровня сборки и определяет данные необходимые для связи с внешними сборками.

В нашем примере определение mscoree.dll будет выглядеть следующим образом:

image thumb 17  Основы .NET – структура .NET сборки

Ранее в части CLR хедера мы видели что там есть информация про токен метаданных точки входа сборки, который имел значение 6000001.

Зная что токены начинающиеся с 06 являются MethodDef токенами, приводит нас к изучению метаданных относящимся к MethodDef. Итак, пока окна ILDasm находятся в фокусе, я нажал <Ctrl>+<M> и легко нашел MethodDef с этим значением токена которое указывает на метод Main (как мы уже видили это в определении кода C#).

image thumb 20  Основы .NET – структура .NET сборки

Подытожим:

  • в CLR хедере мы определили значение токена
  • потом значение токена используется в метаданных для для поиска подходящего элемента MethodDef
  • этот элемент описывает часть IL кода которая будет выполена как точка входа.

Данные бинарного блока метаданных состят из нескольких таблиц которые могут быть категоризированы в таблицы определения, таблицы связей и таблицы манифестов, но рамки этого поста не позволяют его подробное объяснение (в случае если вы заинтересованы в более глубоких деталях касающихся метаданных проверьте главную страницу метаданных MSDN).

IL Код

После этого я раскрыл дерево ILDasm и дважды кликнул элемент метода Main.

image thumb 22  Основы .NET – структура .NET сборки

Как мы видим, статический метод Main обозначен как .entrypoint.

IL базируется на стеке, а это означает что значения операндов записываются в стек выполнения а результаты вытягиваются из стека, без манипулирования регистрами.

Поэтому в L_0000 код записывает в стек значение «Hello world» которое будет использовано в L_0005.

IL игнорирует пространства имен, тоесть пространства имен из C# кода в IL становятся всего лишь префиксами в «полном» имени типа. В IL коде каждый член определен в формате полного имени типа как «Namespace.Type:MemberName».

Вот почему мы имеем:

  • L_0005  System.Console:WriteLine(string) (System namespace, console type, write line member)
  • L_000a CSharp_ILCode.Program:Hello2()    (CSharp_ILCode namespace, Program type, Hello2 member)

Как подсвечено на картинке IL кода, полное имя типа строки L_0005 имеет один дополнительный префикс потому что тип Console определен во внешней сборке (в этом случае это сборка ядра .NET — mscorwks.dll).

Один вопрос неизбежен:

Как .NET узнает где найти этот [mscorlib]?

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

Пока мы все еще в окне IL кода, давайте ответим на еще один вопрос:

Как работает дебаггинг .NET?

IL код представленный на скрине выше скомпилирован оптимизированным в Release режиме и мы все знаем что мы не можем дебажить код скомпилированный в режиме Release. Что бы получить ответ почему мы должны скомпилировать в режиме Debug, давайте посмотрим как выглядит IL код для того же C# кода скомпилированного в режиме Debug:

image thumb 2  Основы .NET – структура .NET сборки

Как мы видим, компилятор вставил перед каждой линией одно NOP выражение. Когда мы ставим точку прерывания (break point) на линии в среде разработки Visual Studio, точка прерывания по факту ставиться на NOP функции перед этой линией. Так как в Release режиме нет NOP инструкций созданных компилятором нет никакой возможности поставить точку перерывания.

Этот пост остановиться здесь (что бы он был не очень длинным) и будет продолжен в следующем посте с описанием того как код .NET сборки выполняется в рантайме.

Источник: http://blog.vuscode.com/malovicn/archive/2007/12/24/net-foundations-net-assembly-structure.aspx

1 комментарий

  1. kvak:

    BSJB это вроде Brian Harry, Susan Radke-Sproull, Jason Zander и Bill Evans

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>