代码之家  ›  专栏  ›  技术社区  ›  Tao

如何分析二进制序列化流的内容?

  •  30
  • Tao  · 技术社区  · 14 年前

    我正在使用二进制序列化(binaryFormatter)作为一种临时机制,将状态信息存储在一个相对复杂(游戏)对象结构的文件中;这些文件正在输出 许多的 比我预期的要大,而且我的数据结构包括递归引用——所以我想知道BinaryFormatter是否实际存储了同一对象的多个副本,或者我的基本“对象数和我应该拥有的值”运算是否离基太远,或者过大的大小来自何处。

    搜索堆栈溢出时,我找到了Microsoft二进制远程处理格式的规范: http://msdn.microsoft.com/en-us/library/cc236844(PROT.10).aspx

    我找不到任何现有的查看器,它允许您“浏览”BinaryFormatter输出文件的内容-获取文件中不同对象类型的对象计数和总字节数等;

    我觉得这一定是我的“google fu”让我失望了(我只有一点点)-有人能帮我吗?这个 必须 以前做过,对吗??


    更新 :我找不到它,也没有答案,所以我把一些相对快速的东西放在一起(链接到下面的可下载项目);我可以确认BinaryFormatter不存储同一对象的多个副本,但它确实向流中打印了大量的元数据。如果需要有效的存储,请构建自己的自定义序列化方法。

    4 回复  |  直到 9 年前
        1
  •  66
  •   Markus Safar    9 年前

    因为这可能对我决定写这篇文章的人感兴趣,关于 序列化.NET对象的二进制格式是什么样子的,我们如何正确地解释它?

    我的所有研究都基于 .net remoting:binary format data structure 规范。


    示例类:

    为了有一个工作示例,我创建了一个名为 a的简单类,它包含2个属性、一个字符串和一个整数值,它们被称为 somestring somevalue

    class a 如下所示:

    [serializable()]
    公开课A
    {
    公共字符串somestring
    {
    得到;
    集合;
    }
    
    公共int somevalue
    {
    得到;
    集合;
    }
    }
    < /代码> 
    
    

    对于序列化,我使用了binaryFormatter当然:

    binaryFormatter bf=new binaryFormatter();
    streamwriter sw=new streamwriter(“test.txt”);
    bf.serialize(sw.basestream,new a()someString=“abc”,someValue=123);
    关闭();
    < /代码> 
    
    

    可以看到,我传递了classa的一个新实例,其中包含abcand123as values.


    示例结果数据:

    如果我们在十六进制编辑器中查看序列化结果,就会得到如下结果:


    让我们解释示例结果数据:

    根据上述规范(这里是指向pdf的直接链接:[ms-nrbf].pdf)流中的每个记录都由recordtypeenumeration标识。部分2.1.2.1 recordtypenumerationstates:。

    < Buff行情>

    此枚举标识记录的类型。每个记录(memberPrimitiveUntyped除外)都以记录类型枚举开头。枚举的大小为一个字节。

    < /块引用>
    SerializationHeaderRecord:

    所以如果我们回顾一下我们得到的数据,我们就可以开始解释第一个字节:

    2.1.2.1 recordtypeenumerationa value of0identifies theserializationHeaderRecordwhich is specified in2.6.1 serializationHeaderRecord:。

    < Buff行情>

    序列化HeaderRecord记录必须是二进制序列化中的第一个记录。此记录具有格式的主版本和次版本,以及顶部对象和标题的ID。

    < /块引用>

    它包括:

    • 记录类型枚举(1字节)
    • rootid(4字节)
    • HeaderID(4字节)
    • 主版本(4字节)
    • minorversion(4字节)

    根据这些知识,我们可以解释包含17个字节的记录:

    00表示recordtypeenumerationwhich isserializationHeaderRecordin our case.

    01 00 00 00表示rootid

    < Buff行情>

    如果序列化流中既不存在BinaryMethodCall也不存在BinaryMethodReturn记录,则此字段的值必须包含序列化流中包含的类、数组或BinaryObjectString记录的ObjectId。

    < /块引用>

    因此在我们的例子中,这应该是值为objectid的1(因为数据是使用小endian序列化的),我们希望再次看到;-)。

    ff ff ff表示headerid

    01 00 00 00表示majorVersion

    00 00 00 00表示minorversion




    二进制库:

    按照规定,每个记录必须以recordtypeenumeration开头。当最后一个记录完成时,我们必须假设一个新的记录开始。
    BR/> 让我们解释下一个字节:

    如我们所见,在我们的示例中,serializationHeaderRecordit后跟binaryLibraryrecord:。

    < Buff行情>

    binarylibrary记录将一个int32 id(如[ms-dtyp]第2.2.22节中指定的)与一个库名相关联。这允许其他记录使用ID引用库名称。当有多个记录引用同一库名称时,此方法会减小线大小。

    < /块引用>

    它包括:

    • 记录类型枚举(1字节)
    • 库ID(4字节)
    • libraryname(可变字节数(这是一个lengthPrefixedString))

    2.1.1.6 lengthPrefixedString中所述。

    < Buff行情>

    lengthPrefixedString表示字符串值。字符串的前缀是以字节为单位的utf-8编码字符串的长度。长度编码在可变长度字段中,最小为1字节,最大为5字节。为了最小化导线尺寸,长度被编码为可变长度字段。

    < /块引用>

    在我们的简单示例中,长度总是使用1 byte>进行编码。有了这些知识,我们可以继续解释流中的字节:

    0c表示recordtypeenumerationwhich identifies thebinarylibraryrecord.

    02 00 00 00表示libraryidwhich is2in our case.


    现在,lengthPrefixedString如下:

    42表示lengthPrefixedString的长度信息which contains thelibraryname.>code>。

    在我们的例子中,42(decimal 66)tell's us的长度信息表明,我们需要读取下一个66字节,并将其解释为libraryname.>code>。

    如前所述,字符串是utf-8encoded,因此上面字节的结果类似于:工作区\,version=1.0.0.0,culture=neutral,publickeytoken=null


    ClassWithMembersandTypes:

    同样,记录是完整的,因此我们解释下一个记录的recordtypeenumeration

    05identifies aclasswithmembersandtypesrecord.章节2.3.2.1 ClassWithMembersandTypesStates:。

    < Buff行情>

    ClassWithMembersandTypes记录是类记录中最详细的记录。它包含有关成员的元数据,包括成员的名称和远程处理类型。它还包含一个引用类的库名称的库ID。

    < /块引用>

    它包括:

    • 记录类型枚举(1字节)
    • ClassInfo(可变字节数)
    • membertypeinfo(可变字节数)
    • 库ID(4字节)

    classinfo:

    2.3.1.1 classinfo中所述,记录包括:

    • 对象ID(4字节)
    • 名称(可变字节数(又是一个lengthPrefidString))
    • 成员计数(4字节)
    • 成员名称(这是一个lengthPrefiedString的序列,其中项目数必须等于memberCount字段中指定的值。)

    回到原始数据,一步一步:

    01 00 00 00表示objectid。我们已经看到这一个,它被指定为序列化headerrecord中的rootid->code>。
    BR/>

    0f 53 74 61 63 6b 4f 76 65 72 46 6c 6f 77 2e 41表示使用alengthPrefixedString表示的类的如前所述,在我们的示例中,字符串的长度定义为1个字节,因此第一个字节0f指定必须使用UTF-8读取和解码15个字节。结果是这样的:stackoverflow.a-所以很明显我使用了stackoverflowas name of the namespace.
    BR/>

    02 00 00 00表示memberCount,它告诉我们2个成员,都用lengthPrefiedString表示。
    BR/> 第一个成员的名称:

    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>,1bis again the length of the string which is 27 bytes in length an results in some like this:lt;somestring>k_u backingfield
    BR/> 第二个成员的名称:

    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 64represets the secondmembername,1aspecifys that the string is 26 bytes long.结果是这样的:someValue>k_uuu backingfield


    membertypeinfo:

    classinfothemembertypeinfofollows.

    2.3.1.2节-membertypeinfo规定,结构包含:

    • 二进制类型枚举(长度可变)
    < Buff行情>

    表示正在传输的成员类型的BinaryTypeEnumeration值序列。数组必须:

    • 与classinfo结构的membernames字段具有相同数量的项。

    • 按照这样的顺序排列:BinaryTypeEnumeration对应于ClassInfo结构的MemberNames字段中的成员名称。

    < /块引用>
    • 附加信息(长度可变),取决于binarytpeenumadditional info may or may not be present.
    < Buff行情>

    binarytypeenum additionalInfos code>
    ---------------+---------------------.
    primitive primitivetypeEnumeration.
    string none

    < /块引用>

    考虑到这一点,我们就快到了…… 我们期望2binaryTypeEnumerationvalues(因为我们在membernames中有2个成员)。


    同样,返回完整的原始数据membertypeinforecord:。

    01表示第一个成员的BinaryTypeEnumeration根据2.1.2.2 BinaryTypeEnumerationWe can expect astringand it is representated using alengthPrefixedString

    00表示第二个成员的binarytypeEnumeration并且再次,根据规范,它是一个primitive。如上所述,primitive后面是附加信息,在本例中是aprimitivetypeEnumeration。这就是为什么我们需要读取下一个字节,即08,将其与2.1.2.3 primitivetypeEnumeration中所述的表匹配,并惊讶地注意到,我们可以期望一个int32由4个字节表示,如某些其他文档中所述,关于基本数据类型。


    库ID:

    memertypeinfothelibraryidfollows之后,它由4个字节表示:

    02 00 00 00表示libraryidwhich is 2.


    值:

    2.3 class records中所述

    < Buff行情>

    类成员的值必须序列化为该记录后面的记录,如第2.7节所述。记录的顺序必须与ClassInfo(第2.3.1.1节)结构中指定的成员名顺序匹配。

    < /块引用>

    这就是为什么我们现在可以期待成员的价值观。
    BR/> 让我们看看最后几个字节:

    06标识一个binaryObjectString。它表示我们的somestringproperty(thesomestring>k_uuu backingfieldto be exact)的值。

    根据2.5.7 binaryObjectStringit contains:。

    • 记录类型枚举(1字节)
    • 对象ID(4字节)
    • 值(可变长度,表示为alengthPrefixedString)

    所以知道了这一点,我们就可以清楚地确定

    03 00 00 00表示objectid

    03 61 62 63表示where03is the length of the string itself and61 62 63are the content bytes that translate toabc

    希望您能记住还有第二个成员,即int32。知道Int32由4个字节表示,我们可以得出结论,即

    必须是我们的第二个成员的值。7bhexagon equals123decimal which appears to fit our example code.

    以下是完整的classwithmembersandtypesrecord:


    messageend:

    最后,最后一个字节0b表示messageendrecord.

    序列化.NET对象的二进制格式是什么样子的?如何正确解释它?

    我所有的研究都基于.NET Remoting: Binary Format Data Structure规范。



    实例类:

    为了有一个有效的例子,我创建了一个名为A它包含两个属性,一个字符串和一个整数值,它们被调用SomeStringSomeValue.

    等级如下所示:

    [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();
    

    可以看到,我传递了一个新的类实例包含abc123作为价值。



    示例结果数据:

    如果我们在十六进制编辑器中查看序列化结果,会得到如下结果:

    Example result data



    让我们解释示例结果数据:

    根据上述规范(以下是PDF的直接链接:[MS-NRBF].pdf)流中的每个记录都由RecordTypeEnumeration. 截面2.1.2.1 RecordTypeNumeration国家:

    此枚举标识记录的类型。每个记录(memberPrimitiveUntyped除外)都以记录类型枚举开头。枚举的大小为一个字节。



    序列化标题记录:

    因此,如果我们回顾一下我们得到的数据,我们就可以开始解释第一个字节:

    SerializationHeaderRecord_RecordTypeEnumeration

    如上所述2.1.2.1 RecordTypeEnumeration一个值0标识SerializationHeaderRecord2.6.1 SerializationHeaderRecord:

    序列化HeaderRecord记录必须是二进制序列化中的第一个记录。此记录具有格式的主版本和次版本,以及顶部对象和标题的ID。

    它包括:

    • 记录类型枚举(1字节)
    • rootid(4字节)
    • HeaderID(4字节)
    • 主版本(4字节)
    • MinorVersion(4字节)



    利用这些知识,我们可以解释包含17个字节的记录:

    SerializationHeaderRecord_Complete

    00代表记录类型枚举哪个是序列化标题记录在我们的例子中。

    01 00 00 00代表RootId

    如果序列化流中既不存在BinaryMethodCall也不存在BinaryMethodReturn记录,则此字段的值必须包含序列化流中包含的类、数组或BinaryObjectString记录的ObjectId。

    所以在我们的例子中,这应该是ObjectId用价值1(因为数据是用小endian序列化的),我们希望能再次看到;-)

    FF FF FF FF代表HeaderId

    01 00 00 00代表MajorVersion

    00 00 00 00代表MinorVersion



    BinaryLibrary:

    按照规定,每个记录必须以记录类型枚举. 当最后一个记录完成时,我们必须假设一个新的记录开始。

    让我们解释下一个字节:

    BinaryLibraryRecord_RecordTypeEnumeration

    如我们所见,在我们的示例中,序列化标题记录后面是BinaryLibrary记录:

    binarylibrary记录将一个int32 id(如[ms-dtyp]第2.2.22节中指定的)与一个库名相关联。这允许其他记录使用ID引用库名称。当有多个记录引用同一库名称时,此方法会减小线大小。

    它包括:

    • 记录类型枚举(1字节)
    • 库ID(4字节)
    • libraryname(可变字节数LengthPrefixedString)



    如上所述2.1.1.6 LengthPrefixedString

    lengthPrefixedString表示字符串值。字符串的前缀是以字节为单位的utf-8编码字符串的长度。长度编码在可变长度字段中,最小为1字节,最大为5字节。为了最小化导线尺寸,长度被编码为可变长度字段。

    在我们的简单示例中,长度总是使用1 byte. 有了这些知识,我们可以继续解释流中的字节:

    BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

    0C代表记录类型枚举它可以识别二进制库记录。

    02 00 00 00代表LibraryId哪个是2在我们的例子中。



    现在长度前缀字符串跟随:

    BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

    42表示的长度信息长度前缀字符串其中包含LibraryName.

    在我们的例子中,长度信息四十二(十进制66)告诉我们,我们需要读取接下来的66个字节,并将其解释为图书馆名称.

    如前所述,字符串是UTF-8已编码,因此上面字节的结果类似于:_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null



    用成员和类型分类:

    同样,记录是完整的,所以我们解释记录类型枚举下一个:

    ClassWithMembersAndTypesRecord_RecordTypeEnumeration

    05标识一个ClassWithMembersAndTypes记录。截面2.3.2.1 ClassWithMembersAndTypes国家:

    ClassWithMembersandTypes记录是类记录中最详细的记录。它包含有关成员的元数据,包括成员的名称和远程处理类型。它还包含引用类的库名称的库ID。

    它包括:

    • 记录类型枚举(1字节)
    • classinfo(可变字节数)
    • membertypeinfo(可变字节数)
    • 库ID(4字节)



    ClassInfo:

    如上所述2.3.1.1 ClassInfo记录包括:

    • 对象ID(4字节)
    • 名称(可变字节数长度前缀字符串)
    • 成员计数(4字节)
    • 成员名(是长度前缀字符串其中的项数必须等于MemberCount字段)



    回到原始数据,一步一步:

    ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

    01 00 00 00代表客体. 我们已经看到了这个,它被指定为腮腺序列化标题记录.

    ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

    0F 53 74 61 63 6B 4F 76 65 72 46 6C 6F 77 2E 41代表Name使用长度前缀字符串. 如前所述,在我们的示例中,字符串的长度定义为1字节,因此第一个字节0F指定必须使用UTF-8读取和解码15个字节。结果如下:StackOverFlow.A-所以很明显我用过StackOverFlow作为命名空间的名称。

    ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount

    02 00 00 00代表成员计数,它告诉我们,两个成员,都代表长度前缀字符串我们会跟进的。

    第一个成员的名称: ClassWithMembersAndTypesRecord_MemberNameOne

    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.

    第二个成员的名称: ClassWithMembersAndTypesRecord_MemberNameTwo

    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表示第二个成员姓名,1A指定字符串的长度为26个字节。结果是这样的:<SomeValue>k__BackingField.



    MemberTypeInfo:

    ClassInfo这个MemberTypeInfo跟随。

    截面2.3.1.2 - MemberTypeInfo声明结构包含:

    • 二进制类型枚举(长度可变)

    表示正在传输的成员类型的BinaryTypeEnumeration值的序列。数组必须:

    • 与classinfo结构的membernames字段具有相同数量的项。

    • 按照这样的顺序排列:BinaryTypeEnumeration对应于ClassInfo结构的MemberNames字段中的成员名称。

    • 附加信息(长度可变),取决于BinaryTpeEnum其他信息可能存在,也可能不存在。

    | BinaryTypeEnum | AdditionalInfos |
    |----------------+--------------------------|
    | Primitive | PrimitiveTypeEnumeration |
    | String | None |

    考虑到这一点,我们就快到了… 我们期待2BinaryTypeEnumeration价值观(因为我们在MemberNames)



    再次,回到原始数据的完整成员类型信息记录:

    ClassWithMembersAndTypesRecord_MemberTypeInfo

    01代表二进制类型枚举根据2.1.2.2 BinaryTypeEnumeration我们可以期待String它用一个长度前缀字符串.

    00代表二进制类型枚举对于第二个构件,根据规范,它是Primitive. 如上所述,本原后面是附加信息,在本例中是PrimitiveTypeEnumeration. 这就是为什么我们需要读取下一个字节,也就是08,将其与中所述的表匹配2.1.2.3 PrimitiveTypeEnumeration当我们发现Int32它由4个字节表示,如其他一些关于基本数据类型的文档所述。



    LibraryId:

    MemerTypeInfo这个图书馆ID下面,它由4个字节表示:

    ClassWithMembersAndTypesRecord_LibraryId

    02 00 00 00代表图书馆ID哪一个是2。



    价值观:

    如规定2.3 Class Records:

    类成员的值必须序列化为该记录后面的记录,如第2.7节所述。记录的顺序必须与ClassInfo(第2.3.1.1节)结构中指定的成员名顺序匹配。

    这就是为什么我们现在可以期待成员的价值观。

    让我们看看最后几个字节:

    BinaryObjectStringRecord_RecordTypeEnumeration

    06标识一个BinaryObjectString. 它代表了我们的价值弦乐财产<somestring>k_uu后退场确切地说)

    根据2.5.7 BinaryObjectString它包含:

    • 记录类型枚举(1字节)
    • 对象ID(4字节)
    • 值(可变长度,表示为长度前缀字符串)



    所以知道了这一点,我们就可以清楚地确定

    BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

    03 00 00 00代表客体.

    03 61 62 63代表Value哪里03是字符串本身的长度,并且61 62 63是转换为abc.

    希望你能记得有第二个成员英特32. 知道英特32用4个字节表示,我们可以得出结论:

    BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

    必须是价值我们的第二个成员。7B十六进制等于一百二十三十进制似乎适合我们的示例代码。

    所以这是完整的与成员和类型分类记录: ClassWithMembersAndTypesRecord_Complete



    MessageEnd:

    MessageEnd_RecordTypeEnumeration

    最后一个字节0B代表MessageEnd记录。

        2
  •  7
  •   Tao    13 年前

    Vasily是对的,因为我最终需要实现自己的格式化程序/序列化过程,以便更好地处理版本控制和输出更紧凑的流(在压缩之前)。

    但是,我确实想了解流中发生了什么,所以我写了一个(相对)快速的类,可以实现我想要的:

    • 通过流解析它的方式,构建对象名称、计数和大小的集合
    • 完成后,输出它发现的内容的快速摘要-流中的类、计数和总大小

    对于我来说,把它放在像codeproject这样的可见位置是不够有用的,所以我只是把这个项目放在我的网站上的一个zip文件中: http://www.architectshack.com/BinarySerializationAnalysis.ashx

    在我的具体案例中,问题是双重的:

    • BinaryFormatter非常冗长(这是众所周知的,我只是没有意识到它的范围)
    • 我在课堂上确实遇到了问题,结果发现我在存储我不想要的对象。

    希望这能在某个时候帮助别人!


    更新:IanWright与我联系,因为原始代码有问题,当源对象包含“decimal”值时,它崩溃了。现在已经纠正了这一点,我利用这个机会将代码移动到Github并给它一个(许可的,BSD)许可证。

        3
  •  5
  •   Vasyl Boroviak    14 年前

    我们的应用程序运行大量数据。它可以占用1-2 GB的内存,就像你的游戏一样。我们遇到了同样的“存储同一对象的多个副本”问题。另外,二进制序列化存储了太多的元数据。在第一次实现时,序列化文件大约占用1-2GB。现在我设法降低了值-50-100MB。我们做了什么。

    简短的回答-不要使用.NET二进制序列化,创建自己的二进制序列化机制。 我们有自己的BinaryFormatter类和ISerializable接口(有两个方法:序列化和反序列化)。

    同一对象不应序列化多次。我们保存它的唯一ID并从缓存中恢复对象。

    如果你要求的话,我可以分享一些代码。

    编辑: 看来你是对的。看下面的代码-它证明我错了。

    [Serializable]
    public class Item
    {
        public string Data { get; set; }
    }
    
    [Serializable]
    public class ItemHolder
    {
        public Item Item1 { get; set; }
    
        public Item Item2 { get; set; }
    }
    
    public class Program
    {
        public static void Main(params string[] args)
        {
            {
                Item item0 = new Item() { Data = "0000000000" };
                ItemHolder holderOneInstance = new ItemHolder() { Item1 = item0, Item2 = item0 };
    
                var fs0 = File.Create("temp-file0.txt");
                var formatter0 = new BinaryFormatter();
                formatter0.Serialize(fs0, holderOneInstance);
                fs0.Close();
                Console.WriteLine("One instance: " + new FileInfo(fs0.Name).Length); // 335
                //File.Delete(fs0.Name);
            }
    
            {
                Item item1 = new Item() { Data = "1111111111" };
                Item item2 = new Item() { Data = "2222222222" };
                ItemHolder holderTwoInstances = new ItemHolder() { Item1 = item1, Item2 = item2 };
    
                var fs1 = File.Create("temp-file1.txt");
                var formatter1 = new BinaryFormatter();
                formatter1.Serialize(fs1, holderTwoInstances);
                fs1.Close();
                Console.WriteLine("Two instances: " + new FileInfo(fs1.Name).Length); // 360
                //File.Delete(fs1.Name);
            }
        }
    }
    

    看起来像 BinaryFormatter 使用Object.Equals查找相同的对象。

    你看过生成的文件吗?如果从代码示例中打开“temp-file0.txt”和“temp-file1.txt”,就会看到它有很多元数据。这就是我建议您创建自己的序列化机制的原因。

    很抱歉喝咖啡。

        4
  •  0
  •   Juan Nunez    14 年前

    也许您可以在调试模式下运行程序并尝试添加控制点。

    如果由于游戏的大小或其他依赖关系而无法实现这一点,那么您始终可以编写一个简单/小的应用程序,其中包括反序列化代码和从调试模式中进行扫视。

    推荐文章