Ответ 1
Потому что это может быть интересно для кого-то, я решил сделать это сообщение о . Как выглядит двоичный формат сериализованных .NET-объектов и как мы можем его правильно интерпретировать?
Я основывал все свои исследования в спецификации .NET Remoting: Binary Format Data Structure.
Класс класса:
Чтобы иметь рабочий пример, я создал простой класс с именем 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) начинается с перечисления типа записи. Размер перечисления - один BYTE.
SerializationHeaderRecord:
Итак, если мы оглянемся на полученные данные, мы можем начать интерпретировать первый байт:
Как указано в 2.1.2.1 RecordTypeEnumeration
, значение 0
идентифицирует SerializationHeaderRecord
, указанное в 2.6.1 SerializationHeaderRecord
:
Запись SerializationHeaderRecord ДОЛЖНА быть первой записью в двоичной сериализации. Эта запись имеет основную и второстепенную версию формата и идентификаторы верхнего объекта и заголовков.
Он состоит из:
- RecordTypeEnum (1 байт)
- RootId (4 байта)
- HeaderId (4 байта)
- MajorVersion (4 байта)
- MinorVersion (4 байта)
С этими знаниями мы можем интерпретировать запись, содержащую 17 байт:
00
представляет RecordTypeEnumeration
, который SerializationHeaderRecord
в нашем случае.
01 00 00 00
представляет RootId
Если в потоке сериализации не присутствует ни BinaryMethodCall, ни BinaryMethodReturn, значение этого поля ДОЛЖНО содержать ObjectId записи Class, Array или BinaryObjectString, содержащейся в потоке сериализации.
Итак, в нашем случае это должно быть ObjectId
со значением 1
(потому что данные сериализуются с использованием little-endian), которые мы будем надеяться увидеть снова; -)
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 байт)
- LibraryId (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
(decimal 66) сообщает нам, что нам нужно прочитать следующие 66 байтов и интерпретировать их как LibraryName
.
Как уже говорилось, строка UTF-8
закодирована, поэтому результат байтов выше будет примерно таким: _WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ClassWithMembersAndTypes:
И снова запись завершена, поэтому мы интерпретируем RecordTypeEnumeration
следующего:
05
идентифицирует запись ClassWithMembersAndTypes
. В разделе 2.3.2.1 ClassWithMembersAndTypes
указано:
Запись ClassWithMembersAndTypes является самой многословной из записей Class. Он содержит метаданные о членах, включая имена и типы удаленных элементов. Он также содержит идентификатор библиотеки, который ссылается на имя библиотеки класса.
Он состоит из:
- RecordTypeEnum (1 байт)
- ClassInfo (переменное число байтов)
- MemberTypeInfo (переменное количество байтов)
- LibraryId (4 байта)
ClassInfo:
Как указано в 2.3.1.1 ClassInfo
, запись состоит из:
- ObjectId (4 байта)
- Имя (переменное число байтов (опять-таки
LengthPrefixedString
)) - MemberCount (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
.
Имя второго члена:
<Т411 >
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.
- ДополнительноInfos (переменная по длине), в зависимости от
BinaryTpeEnum
дополнительная информация может быть или не быть.
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
Поэтому, учитывая это, мы почти там...
Мы ожидаем 2 BinaryTypeEnumeration
значений (потому что в MemberNames
было 2 члена).
Снова вернемся к исходным данным полной записи MemberTypeInfo
:
01
представляет BinaryTypeEnumeration
первого члена, в соответствии с 2.1.2.2 BinaryTypeEnumeration
можно ожидать a String
, и оно представляется с помощью LengthPrefixedString
.
00
представляет BinaryTypeEnumeration
второго элемента, и, опять же, согласно спецификации, это Primitive
. Как указано выше, за Primitive
следует дополнительная информация, в данном случае a PrimitiveTypeEnumeration
. Поэтому нам нужно прочитать следующий байт, который равен 08
, сопоставить его с таблицей, указанной в 2.1.2.3 PrimitiveTypeEnumeration
, и удивляться тому, что мы можем ожидать Int32
, который представлен 4 байтами, как указано в некоторых другой документ об основных типах данных.
LibraryId:
После 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 байт)
- ObjectId (4 байта)
- Значение (переменная длина, представленная как
LengthPrefixedString
)
Поэтому, зная это, мы можем четко определить, что
03 00 00 00
представляет ObjectId
.
03 61 62 63
представляет Value
, где 03
- это длина самой строки, а 61 62 63
- это байты содержимого, которые переводятся на abc
.
Надеюсь, вы помните, что был второй член, Int32
. Зная, что Int32
представляется с использованием 4 байтов, мы можем заключить, что
должен быть Value
нашего второго члена. 7B
шестнадцатеричный эквивалент 123
десятичный символ, который, по-видимому, соответствует нашему примеру.
Итак, вот полная запись ClassWithMembersAndTypes
:
MessageEnd:
Наконец, последний байт 0B
представляет запись MessageEnd
.