Поскольку это может быть кому-то интересно, я решил написать этот пост о Как выглядит двоичный формат сериализованных объектов .NET и как мы можем его правильно интерпретировать?
Я основывал все свои исследования на .NET Remoting: структура данных в двоичном формате спецификация.
Пример класса:
Чтобы иметь рабочий пример, я создал простой класс с именем A, который содержит 2 свойства, одну строку и одно целочисленное значение, они называются SomeString и SomeValue.
Класс A выглядит так:
[Serializable()]
public class A
{
public string SomeString
{
get;
set;
}
public int SomeValue
{
get;
set;
}
}
Для сериализации я, конечно, использовал BinaryFormatter:
BinaryFormatter bf = new BinaryFormatter();
StreamWriter sw = new StreamWriter("test.txt");
bf.Serialize(sw.BaseStream, new A() { SomeString = "abc", SomeValue = 123 });
sw.Close();
Как видно, я передал новый экземпляр класса A, содержащий abc и 123 в качестве значений.
Пример данных результатов:
Если мы посмотрим на сериализованный результат в шестнадцатеричном редакторе, то получим что-то вроде этого:

Давайте интерпретируем результаты примера:
Согласно указанной выше спецификации (вот прямая ссылка на PDF: [MS-NRBF].pdf) каждая запись в потоке идентифицируется RecordTypeEnumeration. Раздел 2.1.2.1 RecordTypeNumeration гласит:
Это перечисление определяет тип записи. Каждая запись (кроме MemberPrimitiveUnTyped) начинается с перечисления типа записи. Размер перечисления составляет один БАЙТ.
SerializationHeaderRecord:
Итак, если мы вернемся к полученным данным, мы можем начать интерпретировать первый байт:

Как указано в 2.1.2.1 RecordTypeEnumeration, значение 0 идентифицирует SerializationHeaderRecord, указанное в 2.6.1 SerializationHeaderRecord:
Запись SerializationHeaderRecord ДОЛЖНА быть первой записью в двоичной сериализации. Эта запись имеет основную и дополнительную версии формата и идентификаторы верхнего объекта и заголовков.
Это состоит из:
- RecordTypeEnum (1 байт)
- RootId (4 байта)
- Заголовок (4 байта)
- Основная версия (4 байта)
- Младшая версия (4 байта)
Зная это, мы можем интерпретировать запись, содержащую 17 байтов:

00 представляет RecordTypeEnumeration, которое в нашем случае равно SerializationHeaderRecord.
01 00 00 00 представляет RootId
Если в потоке сериализации нет ни записи BinaryMethodCall, ни записи BinaryMethodReturn, значение этого поля ДОЛЖНО содержать ObjectId записи Class, Array или BinaryObjectString, содержащейся в потоке сериализации.
Так что в нашем случае это должно быть ObjectId со значением 1 (поскольку данные сериализуются с использованием прямого порядка байтов), что мы надеемся увидеть снова ;-)
FF FF FF FF представляет HeaderId
01 00 00 00 представляет MajorVersion
00 00 00 00 представляет MinorVersion
BinaryLibrary:
Как указано, каждая запись должна начинаться с RecordTypeEnumeration. Поскольку последняя запись завершена, мы должны считать, что начинается новая.
Давайте интерпретируем следующий байт:

Как видим, в нашем примере за SerializationHeaderRecord следует запись BinaryLibrary:
Запись BinaryLibrary связывает идентификатор INT32 (как указано в разделе 2.2.22 [MS-DTYP]) с именем библиотеки. Это позволяет другим записям ссылаться на имя библиотеки с помощью идентификатора. Этот подход уменьшает размер проводника при наличии нескольких записей, ссылающихся на одно и то же имя библиотеки.
Это состоит из:
- RecordTypeEnum (1 байт)
- ID библиотеки (4 байта)
- LibraryName (переменное количество байтов (то есть
LengthPrefixedString))
Как указано в 2.1.1.6 LengthPrefixedString...
LengthPrefixedString представляет строковое значение. Строка имеет префикс длины строки в кодировке UTF-8 в байтах. Длина кодируется в поле переменной длины минимум 1 байт и максимум 5 байт. Чтобы минимизировать размер провода, длина кодируется как поле переменной длины.
В нашем простом примере длина всегда кодируется с помощью 1 byte. Зная это, мы можем продолжить интерпретацию байтов в потоке:

0C представляет RecordTypeEnumeration, который идентифицирует запись BinaryLibrary.
02 00 00 00 представляет LibraryId, которое в нашем случае равно 2.
Теперь LengthPrefixedString следует:

42 представляет информацию о длине LengthPrefixedString, которая содержит LibraryName.
В нашем случае информация о длине 42 (десятичное число 66) говорит нам, что нам нужно прочитать следующие 66 байт и интерпретировать их как LibraryName.
Как уже было сказано, строка имеет кодировку UTF-8, поэтому результат приведенных выше байтов будет примерно таким: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Класс с членами и типами:
Опять же, запись завершена, поэтому мы интерпретируем RecordTypeEnumeration следующей записи:

05 идентифицирует запись ClassWithMembersAndTypes. Раздел 2.3.2.1 ClassWithMembersAndTypes гласит:
Запись ClassWithMembersAndTypes является самой подробной из записей Class. Он содержит метаданные об участниках, включая имена и типы удаленного взаимодействия участников. Он также содержит идентификатор библиотеки, который ссылается на имя библиотеки класса.
Это состоит из:
- RecordTypeEnum (1 байт)
- ClassInfo (переменное количество байтов)
- MemberTypeInfo (переменное количество байтов)
- ID библиотеки (4 байта)
Информация о классе:
Как указано в 2.3.1.1 ClassInfo, запись состоит из:
- Идентификатор объекта (4 байта)
- Имя (переменное количество байтов (что опять же
LengthPrefixedString))
- Количество участников (4 байта)
- MemberNames (это последовательность
LengthPrefixedString, где количество элементов ДОЛЖНО быть равно значению, указанному в поле MemberCount).
Вернемся к необработанным данным, шаг за шагом:

01 00 00 00 представляет ObjectId. Мы уже видели это, оно было указано как RootId в SerializationHeaderRecord.

0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41 представляет Name класса, который представлен с помощью LengthPrefixedString. Как уже упоминалось, в нашем примере длина строки определяется 1 байтом, поэтому первый байт 0F указывает, что 15 байтов должны быть прочитаны и декодированы с использованием UTF-8. Результат выглядит примерно так: StackOverFlow.A — очевидно, я использовал StackOverFlow в качестве имени пространства имен.

02 00 00 00 представляет MemberCount, это говорит нам о том, что 2 участника, оба представленные LengthPrefixedString, последуют за нами.
Имя первого участника: 
1B 3C 53 6F 6D 65 53 74 72 69 6E 67 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 представляет собой первое MemberName, 1B снова представляет собой длину строки, которая составляет 27 байтов, что приводит к чему-то вроде этого: <SomeString>k__BackingField.
Имя второго члена: 
1A 3C 53 6F 6D 65 56 61 6C 75 65 3E 6B 5F 5F 42 61 63 6B 69 6E 67 46 69 65 6C 64 представляет второе MemberName, 1A указывает, что длина строки составляет 26 байт. В результате получается что-то вроде этого: <SomeValue>k__BackingField.
MemberTypeInfo:
После ClassInfo следует MemberTypeInfo.
Раздел 2.3.1.2 - MemberTypeInfo гласит, что структура содержит:
- BinaryTypeEnums (переменная длина)
Последовательность значений BinaryTypeEnumeration, представляющая передаваемые типы элементов. Массив ДОЛЖЕН:
Иметь то же количество элементов, что и поле MemberNames структуры ClassInfo.
Быть упорядоченным таким образом, чтобы BinaryTypeEnumeration соответствовал имени элемента в поле MemberNames структуры ClassInfo.
- AdditionalInfos (переменной длины), в зависимости от
BinaryTpeEnum дополнительная информация может присутствовать или отсутствовать.
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
Итак, принимая это во внимание, мы почти у цели... Мы ожидаем 2 значения BinaryTypeEnumeration (потому что у нас было 2 члена в MemberNames).
Снова вернемся к необработанным данным полной записи MemberTypeInfo:

01 представляет BinaryTypeEnumeration первого члена, согласно 2.1.2.2 BinaryTypeEnumeration мы можем ожидать String, и он представлен с использованием LengthPrefixedString.
00 представляет собой BinaryTypeEnumeration второго члена, и опять же, согласно спецификации, это Primitive. Как указано выше, за Primitive следует дополнительная информация, в данном случае PrimitiveTypeEnumeration. Вот почему нам нужно прочитать следующий байт, который равен 08, сопоставить его с таблицей, указанной в 2.1.2.3 PrimitiveTypeEnumeration, и с удивлением заметить, что мы можем ожидать Int32, который представлен 4 байтами, как указано в каком-то другом документе об основных типах данных.
Идентификатор библиотеки:
После MemerTypeInfo следует LibraryId, он представлен 4 байтами:

02 00 00 00 представляет LibraryId, равное 2.
Значения:
Как указано в 2.3 Class Records:
Значения элементов класса ДОЛЖНЫ быть сериализованы в виде записей, следующих за этой записью, как указано в разделе 2.7. Порядок записей ДОЛЖЕН соответствовать порядку MemberNames, указанному в структуре ClassInfo (раздел 2.3.1.1).
Вот почему теперь мы можем ожидать значения членов.
Давайте посмотрим на последние несколько байтов:

06 определяет BinaryObjectString. Он представляет значение нашего свойства SomeString (точнее, <SomeString>k__BackingField).
Согласно 2.5.7 BinaryObjectString содержит:
- RecordTypeEnum (1 байт)
- Идентификатор объекта (4 байта)
- Значение (переменная длина, представленная как
LengthPrefixedString)
Зная это, мы можем четко определить, что

03 00 00 00 представляет ObjectId.
03 61 62 63 представляет собой Value, где 03 — это длина самой строки, а 61 62 63 — байты содержимого, которые преобразуются в abc.
Надеюсь, вы помните, что был второй участник, Int32. Зная, что Int32 представлен с использованием 4 байтов, мы можем сделать вывод, что

должно быть Value нашего второго члена. 7B в шестнадцатеричном формате равно 123 в десятичном, что, похоже, соответствует коду нашего примера.
Итак, вот полная запись ClassWithMembersAndTypes: 
Конец сообщения:

Наконец, последний байт 0B представляет запись MessageEnd.
person
Markus Safar
schedule
11.05.2015