因为我决定写这篇文章的人可能会感兴趣,关于序列化 .NET 对象的二进制格式是什么样的以及我们如何正确解释它?
我的所有研究都基于.NET Remoting: Binary Format Data Structure规范。
示例类:
为了有一个工作示例,我创建了一个名为的简单类A
,它包含 2 个属性,一个字符串和一个整数值,它们被称为SomeString
and 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 除外)都以记录类型枚举开头。枚举的大小是一个字节。
序列化标头记录:
所以如果我们回顾我们得到的数据,我们可以开始解释第一个字节:
如2.1.2.1 RecordTypeEnumeration
值 中所述,0
标识在SerializationHeaderRecord
中指定2.6.1 SerializationHeaderRecord
:
SerializationHeaderRecord 记录必须是二进制序列化中的第一条记录。该记录具有格式的主要版本和次要版本以及顶部对象和标题的 ID。
它包括:
- RecordTypeEnum (1 字节)
- RootId(4 字节)
- HeaderId(4 字节)
- 主要版本(4 字节)
- 次要版本(4 个字节)
有了这些知识,我们可以解释包含 17 个字节的记录:
00
代表我们的RecordTypeEnumeration
情况SerializationHeaderRecord
。
01 00 00 00
代表RootId
如果序列化流中既没有 BinaryMethodCall 也没有 BinaryMethodReturn 记录,则该字段的值必须包含序列化流中包含的 Class、Array 或 BinaryObjectString 记录的 ObjectId。
所以在我们的例子中,这应该是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 ID(如 [MS-DTYP] 第 2.2.22 节中指定)与库名称相关联。这允许其他记录通过使用 ID 来引用图书馆名称。当有多个记录引用同一个库名称时,这种方法会减小连线大小。
它包括:
- RecordTypeEnum (1 字节)
- 库 ID(4 个字节)
- LibraryName(可变字节数(即 a
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
。
在我们的例子中,(十进制 66)的长度信息42
告诉我们,我们需要读取接下来的 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 记录。它包含有关成员的元数据,包括成员的名称和远程处理类型。它还包含一个引用类的库名称的库 ID。
它包括:
- RecordTypeEnum (1 字节)
- ClassInfo(可变字节数)
- MemberTypeInfo(可变字节数)
- 库 ID(4 个字节)
类信息:
如2.3.1.1 ClassInfo
记录所述,包括:
- 对象 ID(4 个字节)
- 名称(可变字节数(又是 a
LengthPrefixedString
))
- 成员计数(4 字节)
- MemberNames(这是一个
LengthPrefixedString
's 的序列,其中项目的数量必须等于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
使用 a 表示的类的LengthPrefixedString
。如前所述,在我们的示例中,字符串的长度定义为 1 个字节,因此第一个字节0F
指定必须使用 UTF-8 读取和解码 15 个字节。结果看起来像这样:StackOverFlow.A
- 很明显我用作StackOverFlow
命名空间的名称。
02 00 00 00
代表MemberCount
,它告诉我们 2 个成员,都用LengthPrefixedString
's 代表将跟随。
第一位成员姓名:
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
.
成员类型信息:
之后ClassInfo
如下MemberTypeInfo
。
部分2.3.1.2 - MemberTypeInfo
指出,该结构包含:
表示正在传输的成员类型的 BinaryTypeEnumeration 值序列。阵列必须:
- AdditionalInfos(长度可变),取决于
BinaryTpeEnum
附加信息可能存在也可能不存在。
| BinaryTypeEnum | AdditionalInfos |
|----------------+--------------------------|
| Primitive | PrimitiveTypeEnumeration |
| String | None |
所以考虑到这一点,我们几乎就在那里......我们期望 2 个BinaryTypeEnumeration
值(因为我们在 2 个成员中有 2 个成员MemberNames
)。
再次,回到完整MemberTypeInfo
记录的原始数据:
01
表示BinaryTypeEnumeration
第一个成员的 ,根据2.1.2.2 BinaryTypeEnumeration
我们可以预期的 a String
,它使用 a 表示LengthPrefixedString
。
00
表示BinaryTypeEnumeration
第二个成员的 ,同样,根据规范,它是Primitive
. 如上所述,Primitive
's 后面是附加信息,在本例中为 a PrimitiveTypeEnumeration
。这就是为什么我们需要读取下一个字节,即08
,将其与表中所述的表相匹配,2.1.2.3 PrimitiveTypeEnumeration
并惊讶地发现我们可以预期一个Int32
由 4 个字节表示的字节,如其他一些关于基本数据类型的文档中所述。
图书馆编号:
以下之后MemerTypeInfo
,LibraryId
用4个字节表示:
02 00 00 00
表示LibraryId
哪个是 2。
价值:
如中所述2.3 Class Records
:
类成员的值必须序列化为该记录之后的记录,如第 2.7 节所述。记录的顺序必须与 ClassInfo(第 2.3.1.1 节)结构中指定的 MemberNames 的顺序相匹配。
这就是为什么我们现在可以期待成员的价值观。
让我们看看最后几个字节:
06
标识一个BinaryObjectString
. 它代表我们SomeString
财产的价值(<SomeString>k__BackingField
准确地说)。
据2.5.7 BinaryObjectString
它包含:
- RecordTypeEnum (1 字节)
- 对象 ID(4 个字节)
- 值(可变长度,表示为 a
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
记录。