Сообщений 19    Оценка 790        Оценить  
Система Orphus

Управляемые (managed) ресурсы в исполняемых файлах .NET

Авторы: Павел Румянцев
Владислав Чистяков
The RSDN Group

Источник: RSDN Magazine #3-2003
Опубликовано: 17.01.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Что такое ресурсы
Работа с ресурсами в приложениях
Программное добавление ресурсов
Добавление ресурсов из командной строки
Добавление ресурсов в VS.NET
Использование ресурсов в приложении
Физическая организация ресурсов в исполняемом файле.
Формат .resources
Заключение

Программа Researcher.exe

Что такое ресурсы

Ресурсы – это некоторые данные, логически связанные с программой, но не являющиеся частью метаданных или кода этой программы.

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

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

Кроме того, в ресурсы записываются данные, хранение которых в коде программы по каким-то причинам неудобно, например, битовые и векторные изображения.

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

Ресурсы хранятся в виде потоков (stream) или внешних файлов (которые в программе также выглядят как потоки).

Физически ресурсы могут находиться:

Именно в манифесте определяется, где конкретно находятся ресурсы. С точки зрения программиста ресурсы выглядят как некоторый поток данных, определение формата и интерпретация содержания которого возлагаются на программиста. Где бы ни находился ресурс (в той же сборке или отдельном файле), программный доступ к нему осуществляется одним-единственным способом – в виде потока (System.IO.Stream).

.NET определяет специальный формат ресурсов, изначально предназначенный для хранения данных дизайнеров, таких, как дизайнеры форм библиотек Windows Forms и ASP.NET. Часто этот формат называется в документации «ресурсом». Это иногда приводит к путанице. Чтобы отличить сами ресурсы от конкретного формата, в этой статье данный формат будет называться .resources, так как файлы в этом формате имеют расширение «.resources».

Работа с ресурсами в приложениях

Ресурсы в приложение могут быть добавлены несколькими способами:

Программное добавление ресурсов

Добавить ресурс программным способом можно при помощи API, предназначенного для создания сборок, например, System.Reflection.Emit. Разумеется, к этому моменту ресурсы уже должны быть созданы. Если в ресурсы необходимо поместить данные из уже существующего файла, то никаких проблем не возникнет. Если же ресурсы нужно разместить в формате .resources, придется использовать класс ResourceWriter, входящий в пространство имён System.Resources. К формату .resources мы ещё вернёмся и обсудим его более подробно, а пока поговорим о создании ресурсов при помощи ResourceWriter.

У класса ResourceWriter есть два конструктора:

        // Аргумент – имя создаваемого потока
        public ResourceWriter(Stream stream)
// Аргумент – имя создаваемого файла .resources.public ResourceWriter(string fileName)

После создания экземпляра класса ResourceWriter для добавления ресурса необходимо воспользоваться одним из методов:

        public
        virtual
        void AddResource(string name, byte[] value);
publicvirtualvoid AddResource(string name, object value);
publicvirtualvoid AddResource(string name, string value);

Первым аргументом методов является имя добавляемого ресурса, а вторым – непосредственно добавляемый ресурс.

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

В ресурсы могут добавляться системные типы (в состав которых входят примитивы (например, Int32), String, DateTime, TimeSpan, Decimal), а также произвольные объекты, поддерживающие сериализацию. Здесь следует уточнить, что объекты не добавляются, а сериализуются. Отсюда и вытекает одно из требований – для того, чтобы объект можно было разместить в ресурсах, он должен быть сериализуемым. Таким образом, «тело» ресурса представляет собой ни что иное, как сериализованное представление объекта того или иного типа.

Эти методы формируют данные в оперативной памяти. Запись в файл производится при помощи метода Generate(), не имеющего параметров. После завершения работы с файлом ресурсов ResourceWriter нужно «закрыть» при помощи методов Close или Dispose, которые вызывают метод Generate(). При работе на C# проще всего создавать экземпляр объекта ResourceWriter внутри оператора using.

Вот пример использования класса ResourceWriter:

        using(ResourceWriter rw = new ResourceWriter("MyRes.resource"))
{
  rw.AddResource("str1", "Моя строка в ресурсах.");
  rw.AddResource("Point1", new Point(2, 3));
}

Он создает в текущем каталоге файл с именем MyRes.resource, в который записывает строку с именем ресурса "str1" и структуру. Перед записью структуры производится боксинг. Использование оператора using позволяет не беспокоиться о вызове метода Close или Generate(), а также защититься от проблем, связанных с исключениями.

Добавление ресурсов из командной строки

Для включения ресурсов в исполняемый файл в компиляторе csc используется опция /resource (сокращённая форма /res), за которой через двоеточие должно быть указано имя файла ресурсов. Следующий пример создает приложение Form1.exe, включая в него в качестве ресурсов файлы Bitmap2.bmp и MyStrings.resources:

csc Form1.cs /res:MyStrings.resources /res:Bitmap2.bmp

В этом случае оба ресурса будут размещены в разделе управляемых (managed) ресурсов исполняемого файла.

Если же возникает необходимость подключить внешний файл ресурсов, не включённый в исполняемый файл, можно воспользоваться опцией /linkresource (сокращённая форма /linkres).

Например, чтобы приложение могло использовать MyStrings.resources как внешний файл, необходимо выдать команду:

csc Form1.cs /linkres:MyStrings.resources

Чтобы добавить управляемые (managed) ресурсы в файл, создаваемый при помощи компилятора MC++, необходимо воспользоваться опцией компоновщика /ASSEMBLYRESOURCE.

Для создания файла .resources из командной строки можно использовать утилиту resgen, которой в качестве входных данных можно указать файл формата .resx или текстовый файл, содержащий набор именованных строк. XML-формат файла .resx довольно сложен. Разумеется, создать файл такого формата вручную можно, но это сложно и неудобно. Поэтому вручную имеет смысл создавать только строковые ресурсы, используя для этого специальный инструмент – программу ResGen, которая поставляется вместе с SDK.. В большинстве случаев .resx-файлы создаются VS.NET автоматически, и предназначены для сериализации данных дизайнеров. Использовать такие файлы без IDE типа VS.NET крайне сложно. Для создания .resources-файлов, содержащих только строки, resgen использует более простой формат входного файла.

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

Имя=Строка

Утилита resgen, получив файл такого формата в качестве входного, создаёт другой файл, по умолчанию имеющий расширение .resources. Этот файл может быть добавлен в состав проекта.

Для примера был создан файл MyStrings.txt следующего содержания:

String1=Моя первая строка.
String2=This is my second string\nВторая строка

После выполнения команды

resgen MyStrings.txt

будет создан файл MyStrings.resources, который можно включить в проект.

Единственное, что осталось сказать – если вам нужен набор символов, отличный от ASCII (набор символов 1252), входной файл должен быть сохранен в кодировке Unicode или UTF-8. Это легко сделать с помощью редактора VS.NET или notepad.exe из любой версии ОС семейства NT.

Добавление ресурсов в VS.NET

В некотором смысле простейшим видом ресурса является файл как таковой, как единица хранения, причём тип файла не играет роли. Это может быть файл с картинкой, с какой-то закодированной информацией, и так далее… Добавление содержимого файла в состав ресурсов является, возможно, самым простым способом добавления ресурсов в исполняемый файл. Посмотрим, как это можно сделать.

Допустим, на винчестере уже приготовлен файл с именем Bitmap1.bmp, содержащий какую-то картинку (имя файла и характер информации в нём могут быть любыми). Чтобы добавить содержимое файла Bitmap1.bmp в исполняемый файл в качестве ресурса, необходимо произвести следующие действия:

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

В появившемся popup-меню выбрать элемент «Add», после чего в очередном popup-меню выбрать элемент «Add existing item».

В появившемся диалоговом окне выбрать файл, который должен быть добавлен в проект.

После выполнения всех этих действий файл появится в дереве проекта (см. рисунок 1):


Рисунок 1.

В контекстном меню добавленного файла выбрать пункт «Properties», что, в свою очередь, приведёт к появлению окна «Properties», в котором можно будет откорректировать свойства файла.

Выбрать в свойстве «Build Action» значение «Embedded Resource» (см. рисунок 2):


Рисунок 2.

После этих действий и компиляции содержимое файла Bitmap1.bmp будет включёно в состав ресурсов. Таким способом в состав ресурсов может быть включено любое (в разумных пределах, естественно) количество любых файлов. Понятно, что при этом никаких действий с содержимым файла производиться не будет, в разделе управляемых ресурсов появится копия содержимого файла.

Как уже говорилось, в состав ресурсов могут быть добавлены и ресурсы формата .resources. Для этого необходимо произвести следующие действия:

  1. В окне Solution Explorer’а выделить элемент, соответствующий разрабатываемому проекту, после чего нажать правую кнопку мыши.
  2. В появившемся popup-меню выбрать элемент «Add», после чего в очередном popup-меню выбрать элемент «Add new item».
  3. В левой части появившегося диалогового окна выбрать категорию «Resources».
  4. В правой части диалогового окна выбрать «Assembly Resource File».
  5. В появившемся grid’е ( см. рисунок 3) ввести все необходимые данные.


Рисунок 3.

В результате этих действий Visual Studio создаст XML-файл, часть содержимого которого можно увидеть на рисунке 4.


Рисунок 4.

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

К сожалению, при помощи grid’а (см. рисунок 3) Visual Studio .NET добавить в .resx данные не примитивных типов, например, картинку, практически невозможно, это может быть сделано только при помощи сериализации данных в ресурсы.

Особо необходимо сказать о файлах .resx, содержащих данные о формах, созданных Form Designer’ом. В них, естественно, также хранятся ресурсы. Но! Эти файлы генерируются автоматически. Поэтому в общем случае при добавлении в эти файлы каких-то данных при перекомпиляции проекта эти данные будут утрачены.

Использование ресурсов в приложении

Понятно, что ресурсы создаются для того, чтобы впоследствии извлечь из них данные. Доступ к ресурсам осуществляется при помощи метода GetManifestResourceStream. Скажем, для того, чтобы обратиться к ранее добавленному в ресурсы Bitmap1.bmp, можно использовать следующую последовательность операторов:

pictureBox2.Image = Image.FromStream(
  Assembly.GetExecutingAssembly().GetManifestResourceStream("Res1.Bitmap1.bmp"),
  true);

Res1 – это название пространства имён, используемого приложением по умолчанию. Его к имени ресурса добавляет Visual Studio.NET.

Следующий код считывает из ресурсов содержимое ранее добавленного текстового файла 1.txt:

StreamReader sr = new StreamReader(
  Assembly.GetExecutingAssembly().GetManifestResourceStream("1.txt"),
  Encoding.GetEncoding(1251));

Сonsole.WriteLine(sr.ReadToEnd());

В этом отрывке кода подразумевается, что файл добавлялся при помощи утилиты командной строки, а не при помощи VS.NET. Обратите внимание, что данные ресурсов представляются с помощью потока (System.IO.Stream), его необходимо читать с помощью одного из Reader’ов. В частности, для этой цели прекрасно подходит StreamReader. Но по умолчанию StreamReader будет работать в формате UTF-8, поэтому если в файле находится текст в кодировке 1251, необходимо указать её явно.

Очевидно, что формат .resource создавался с расчётом на то, что данные из него будут считываться поэлементно, причём поиск отдельного ресурса внутри этого формата будет производиться по имени этого ресурса. Для доступа к ресурсам можно использовать методы класса ResourceManager. Два его основных метода, GetString и GetObject, предназначены для загрузки из ресурсов строк и объектов соответственно. Ниже приведён пример использования объекта класса ResourceManager для загрузки строки, ранее добавленной в .resources:

        // .resource
ResourceManager rm = new ResourceManager("Res1.Resource1",
  Assembly.GetExecutingAssembly());
  label1.Text = rm.GetString("zzz", CultureInfo.InvariantCulture);

Этот метод возвращает не просто набор байтов, а экземпляр класса System.String. Метод GetObject находит ресурс-элемент с указанным именем, создаёт новый экземпляр объекта и загружает в него (с помощью BinaryFormatter’а) состояние объекта. Таким образом, можно сказать, что методы ResourceManager загружают объекты из ресурсов по неким логическим именам. Как говорилось ранее, в первую очередь этот механизм был предназначен для загрузки состояния компонентов и их содержимого в дизайнерах, таких, как Windows Forms и ASP.NET. По умолчанию дизайнеры стараются сериализовать состояние компонентов в код, но бывают случаи, когда сериализация в код невозможна. Например, затруднительно (и неразумно) сериализовать в код содержимое картинок. К тому же для того, чтобы дизайнер мог полностью сериализовать в код все свойства компонента, разработчик компонента должен предпринять некоторые действия (реализовать TypeConverter, преобразующий экземпляр объекта в специальный класс-описание экземпляра – InstanceDescriptor). Если такового не имеется, и сериализуемый класс не является компонентом, дизайнер попытается сериализовать состояние экземпляра BinaryFormatter’ом. Если это удастся, он запишет это состояние в .resx-файл, а в код подставит вызов метода ResourceManager.GetObject. При этом ресурсу будет дано имя вида ИмяКомпонента.ИмяСвойства. Например, компонент pictureBox1 (типа System.Windows.Forms.PictureBox) имеет свойство Image типа System.Drawing.Image. Этот тип не поддерживает сериализацию в код, да ему это не особо и требуется, так как основное его содержимое – это картинка. Поэтому дизайнеры сериализуют объекты такого типа в ресурсы (.resx-файлы). После компиляции .resx-файлы превращаются в ресурсы формата .resources с именами, соответствующими именам форм вида ПространствоИменПринятоеПоУмолчанию.ИмяФормы.resources. При этом в самом начале метода InitializeComponent соответствующей формы добавляется код, инициализирующий экземпляр ResourceManager:

System.Resources.ResourceManager resources = 
  new System.Resources.ResourceManager(typeof(Form2));

а участок кода инициализации компонента (в нашем случае pictureBox1) добавляется в код загрузки объекта из ресурса:

        this.pictureBox1.Image = ((System.Drawing.Image)(resources.GetObject(
  "pictureBox1.Image")));

По умолчанию основное содержимое формы, в том числе и строки, сериализуются в код. Если стоит задача создания многоязычного (поддерживающего локализацию) приложения, в свойствах формы нужно переключить свойство Localizable (по умолчанию оно выставлено в false). После этого все свойства формы будут сериализоваться в ресурсы. Если после этого в свойстве Language переключить язык, то для выбранного языка будет создан свой .resx-файл (а впоследствии и свой экземпляр .resources). Эти экземпляры отличаются суффиксом имени. Например, для английского языка используется суффикс «.en», а для русского – «.ru». Свойства Localizable и Language добавляются к дизайнерам верхнего уровня (root designer-ам) специальным расширенным провайдером – LocalizationExtenderProvider.

После компиляции в ресурс приложения записываются нейтральные к культуре ресурсы, а для разных языков создаются отдельные DLL. Эти DLL помещаются в поддиректории, имена которых соответствуют суффиксам языков. Имена DLL формируются следующим образом: ИмяПриложения.resources.dll, например, Res1.resources.dll.

При этом выбор DLL осуществляется в зависимости от культуры установленной системы, подчёркиваю, установленной системы, а не указанного в региональных установках языка. Язык, указанный в региональных установках, отображается в .NET на свойство Thread.CurrentThread.CurrentCulture или (что то же самое) CultureInfo.CurrentCulture. За язык интерфейса отвечает свойство Thread.CurrentThread.CurrentUICulture.

Если вы хотите, чтобы приложение под, скажем, английской версией Windows работало на русском языке, необходимо свойству Thread.CurrentThread.CurrentUICulture присвоить значение CultureInfo.CurrentCulture. Возможно, такие вещи лучше было бы делать настраиваемыми через пользовательский интерфейс.

Если стоит задача прочитать все содержимое ресурсного файла (или ресурса в формате .resources, находящегося в некоторой сборке), можно использовать класс System.Resources.ResourceReader, реализующий интерфейс IResourceReader. Конструкторы этого класса приведены ниже:

        public: ResourceReader(String fileName);
public: ResourceReader(Stream stream);

В качестве аргументов передаётся имя файла или указатель на поток, содержащий ресурсы. Поток можно получить способом, аналогичным тому, как это делалось выше для ресурса формата bmp, т.е. если в приложение Res1 добавлен ресурс MyRes.resource, чтобы получить поток, содержащий этот ресурс, можно воспользоваться кодом:

Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(
  "Res1.MyRes.resource");

При создании объекта производится считывание всей служебной информации, но не более. Считывание непосредственно ресурсов конструктор не производит. Следующий код последовательно читает все ресурсы, находящиеся в файле MyRes.resource, и выводит на консоль имя, тип и содержимое каждого отдельного ресурса:

        using(ResourceReader rr = new ResourceReader("MyRes.resource"))
{
  foreach(DictionaryEntry entry in rr)
    Console.WriteLine("Name: '{0}' Type: {1} Value: '{2}'", 
      entry.Key, entry.Value.GetType(), entry.Value);
}

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

Физическая организация ресурсов в исполняемом файле.

Чтобы определить, где располагаются ресурсы, необходимо проанализировать таблицу ManifestResource 40), входящую в состав метаданных. Так как нам придётся в дальнейшем работать с ней, приведем список её полей:

Первым делом необходимо определить, где находятся те ресурсы, которые необходимо разобрать. Для этого нужно проанализировать поле Implementation. Если его значение не равно нулю, оно содержит закодированный номер строки в таблицах File или AssemblyRef.

ПРИМЕЧАНИЕ

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

Определить, с какой таблицей предстоит работать, можно, проанализировав два младших бита поля Implementation. Если их значение равно нулю, ссылка производится на таблицу Files. Если их значение равно единице, ссылка осуществляется на таблицу AssemblyRef. Сдвинув старшие биты поля Implementation вправо на два разряда, получим номер строки в таблице, на которую производится ссылка. Если же значение поля Implementation равно нулю, ресурсы находятся непосредственно в исследуемом файле. Взглянув на таблицу ManifestResource в файле mscorlib.dll, мы увидим следующую картину (см. рисунок 5).


Рисунок 5. Таблица ManifestResource файла mscorlib.dll.

Понятно, что ресурс, данные о котором расположены по смещению 0x17e44a, находится в теле самой библиотеки, а ресурсы со смещениями 0x17e458 и 0x17e466 находятся в других файлах (но не в других сборках). Естественно, эти файлы должны быть перечислены в таблице File. Взглянув на содержание этой таблицы (см. рисунок 6), мы увидим, что в ней присутствуют ссылки на файлы ресурсов.


Рисунок 6

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

Как легко догадаться, поле Name таблицы ManifestResource определяет название ресурса. Точнее, не название, а смещение в хипе имён, начиная с которого располагается имя ресурса. И, естественно, значением поля Offset является смещение ресурса относительно того места, начиная с которого ресурсы располагаются в исполняемом файле. Еще раз, в данном случае речь идёт не об RVA, а о смещении.

Итак, можно сказать, что к этому моменту названия ресурсов и «координаты», по которым они находятся, известны. Ничто не мешает построить список ресурсов (см. рисунок 7):


Рисунок 7. Начало списка управляемых ресурсов.

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

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

Следом за длиной ресурса располагаются непосредственно данные ресурса.


Рисунок 8. Формат хранения данных ресурсов.

Кроме того, скорее всего, в данном случае (рисунок 8) по смещению 0x236с8 находится картинка в формате .png, а, скажем, по смещению 0x281a0 – какие-то данные в формате XML, которые были добавлены в состав ресурсов как отдельные файлы. О содержании .resources пока можно только догадываться.

Формат .resources

Обычно ресурсы в формате .resources имеют имя, оканчивающееся на .resources. Но ничто не мешает программисту добавить ресурс любого другого формата, имя которого будет оканчиваться на .resources. Видимо, чтобы увеличить надежность системы, программисты Microsoft ввели так называемое «магическое число» (MagicNumber) 0xBEEFCACE, записываемое в самом начале ресурса. Таким образом, если значение начального двойного слова тела ресурса равно приведённой выше сигнатуре, можно с большой вероятностью сказать, что его формат – .resources.

Формат .resources начинается с заголовка. Его называют ResourceManagerHeader. Этот заголовок состоит из нескольких полей и начинается сигнатурой, о которой мы говорили выше. Непосредственно за «волшебным числом» следует номер версии заголовка, также занимающий четыре байта. Это поле называется HeaderVersionNumber.

Четвёртое поле – это имя «считывателя» ресурсов, который должен быть использован системой для чтения ресурсов. Если ресурсы записываются стандартными средствами, система использует «System.Resources.ResourceReader, mscorlib». Поле пятое – название «набора» ресурсов, стандартное название этого поля – System.Resources.RuntimeResource.Set, mscorlib. Третье поле заголовка – это суммарная длина обоих имён.

Структура ResourceManagerHeader’а показана ниже (см. рисунок 9):


Рисунок 9. Структура ResourceManagerHeader’а.

Пусть читателя не удивляет тот факт, что на скриншоте не показаны типы переменных LenghtOfName и Name. Дело в том, что поле Size является «сжатым» представлением числа и может занимать один, два, три, четыре и так далее байтов. Какой в этом случае у него тип? Кроме того, Size фактически является составляющей Name’а. Какого типа в этом случае Name? Надеемся, что и без определения типов в данном случае информации больше, чем достаточно.

Непосредственно за заголовком менеджера ресурсов следует заголовок набора ресурсов. Из него мы можем «вытащить» побольше информации, чем из заголовка менеджера ресурсов. В частности, из первого двойного слова этого заголовка можно получить версию заголовка. Второе двойное слово (а это уже интересно!) хранит в себе число ресурсов, входящих в разбираемый файл .resources. А третье слово – это число типов, которым принадлежат ресурсы. Например, в хипе ресурсов все ресурсы могут принадлежать одному типу. Другой пример – все ресурсы могут быть разных типов. Пример третий – несколько ресурсов принадлежит одному типу, несколько – другому, и так далее…

За полем, хранящим число типов, следуют поля, в которых записаны названия типов. Каждому названию типов предшествует длина этого типа. Но, наверное, лучше всё это увидеть, чем пытаться представлять (см. рисунок 10).


Рисунок 10. Заголовок набора ресурсов и названия типов ресурсов.

А вот дальше начинается целая детективная история. За названиями типов следует массив хэшей имён ресурсов. В исходниках SSCLI (файл resourcereader.cs) в одном из комментариев написано, «Note that the name hashes array is aligned to 8 bytes so we can use pointers into it on 64 bit machines». Конечно, комментарии не являются технической документацией, однако код для перехода на эту границу восьми байтов, подтверждает написанное:

      long pos = _store.BaseStream.Position;
int alignBytes = ((int)pos) & 7;
bool fileIsAligned = true;
if (alignBytes != 0)
{
// For backwards compatibility, don't require the "PAD" stuff// in here, but we should definitely skip it if present.for(int i=0; i < 8 - alignBytes; i++)  //( Это условие пришлось изменить
  {
    byte b = _store.ReadByte();
    if (b != "PAD"[i % 3])
    {
      fileIsAligned = false;
      break;
    }
  }
// If we weren't aligned, we shouldn't have skipped this data!if (!fileIsAligned)
    _store.BaseStream.Position = pos;
  else
  {
    BCLDebug.Assert((_store.BaseStream.Position & 7) == 0,
              "ResourceReader: Stream should be aligned on an 8 byte boundary now!");
  }
}

Декомпилированный Anakrino код практически совпадает с приведённым выше. Другими словами, в этом месте до границы 8 байтов файл должен дополняться некоторой последовательностью символов ‘P’, ‘A’ и ‘D’. Но этот код не может объяснить ту ситуацию, с которой мы столкнулись при анализе одного из файлов (см. рисунок 11).


Рисунок 11 «Выравнивающие» байты.

Видно, что за именем последнего типа ресурсов, заканчивающегося строго на границе восьми байтов, следуют ещё четыре байта, заполненные «выравнивающими» символами. Пришлось допустить, что код «устарел»… :-)

А дальше, как уже говорилось выше, располагается массив хэшей имён ресурсов. Каждый элемент этого массива занимает четыре байта. Ничего интересного с точки зрения анализа ресурсов этот массив не представляет, но из песни, как говорится, слова не выкинешь (см. рисунок 12):


Рисунок 12. Массив хэшей названий ресурсов.

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


Рисунок 13. Хэши названий, смещения заголовков ресурсов, заголовки ресурсов.

Помимо названий ресурсов в заголовках ресурсов хранятся смещения тел ресурсов относительно начала массива тел ресурсов (этот массив можно назвать «Resource Bodies»).

К этому моменту не получен ответ только на один вопрос – а как определить, к какому типу принадлежит тот или иной ресурс?

Для ответа на этот вопрос достаточно взглянуть на рисунок 14:


Рисунок 14. Структура «тел» ресурсов.

«Тело» ресурса начинается с его типа. В данном случае тип ресурса является индексом имени типа в массиве названий типов (см. рисунок 11). Непосредственно за индексом следует «тело» ресурса. В частности, ресурс, располагающийся по смещению 0х280f1, представляет собой строку «EditRateForm».

Вероятно, полезно будет взглянуть на 16-ричное представление ресурса (см. рисунок 16).


Рисунок 15.

Рассматриваемый ресурс занимает место от 0x5b38c до 0x5b4f4 включительно (данные получены при разборе ресурсов). Видно, что в своём «теле» ресурс хранит данные о той библиотеке, в которой определён его тип, и непосредственно название типа. Логично предположить, что в данном случае перед нами – сериализованное представление объекта.

Заключение

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


Эта статья опубликована в журнале RSDN Magazine #3-2003. Информацию о журнале можно найти здесь
    Сообщений 19    Оценка 790        Оценить