没有对象定义的二进制反序列化

时间:2013-08-01 14:21:46

标签: c# .net serialization deserialization binary-serialization

我正在尝试读取二进制序列化对象,我没有它的对象定义/来源。我在文件中找到了一个峰值并看到了属性名称,因此我手动重新创建了对象(让我们称之为SomeDataFormat)。

我最终得到了这个:

public class SomeDataFormat // 16 field
{
    public string Name{ get; set; }
    public int Country{ get; set; } 
    public string UserEmail{ get; set; }
    public bool IsCaptchaDisplayed{ get; set; }
    public bool IsForgotPasswordCaptchaDisplayed{ get; set; }
    public bool IsSaveChecked{ get; set; }
    public string SessionId{ get; set; } 
    public int SelectedLanguage{ get; set; } 
    public int SelectedUiCulture{ get; set; } 
    public int SecurityImageRefId{ get; set; } 
    public int LogOnId{ get; set; } 
    public bool BetaLogOn{ get; set; } 
    public int Amount{ get; set; }
    public int CurrencyTo{ get; set; }
    public int Delivery{ get; set; } 
    public bool displaySSN{ get; set; }
}   

现在我可以像这样反序列化它:

BinaryFormatter formatter = new BinaryFormatter();  
formatter.AssemblyFormat = FormatterAssemblyStyle.Full; // original uses this       
formatter.TypeFormat = FormatterTypeStyle.TypesWhenNeeded; // this reduces size
FileStream readStream = new FileStream("data.dat", FileMode.Open);
SomeDataFormat data = (SomeDataFormat) formatter.Deserialize(readStream);

首先可疑的是,只有2个字符串(SessionId& UserEmail)在反序列化的数据对象中具有值。其他属性为null或只是0.这可能是有意的,但我仍然怀疑在反序列化过程中出现了问题。

第二个可疑的事情是如果我重新序列化这个对象,我最终会得到不同的文件大小。原始(695字节)。重新序列化的对象是698个字节。所以有3bytes的区别。我应该获得与原始文件相同的文件大小。

查看原始文件和新的(重新编目的)文件:

最初序列化的文件:(zoom) enter image description here 重新编目的文件:(zoom) enter image description here

如您所见,在标题部分之后,数据显示的顺序不同。例如,您可以看到电子邮件和sessionID不在同一个地方。

UPDATE:会警告我“PublicKeyToken = null”之后的字节也不同。 (03< - > 05)

  • Q1:为什么两个文件中的值的顺序不同?
  • Q2:与2个序列化对象相比,为什么还有3个字节?
  • 问题3:我错过了什么?我怎么能这样做?

感谢任何帮助。


有关的问题: 1 2 3

5 个答案:

答案 0 :(得分:8)

  

为什么两个文件中的值的顺序不同?

这是因为会员订单不是基于声明订购。 http://msdn.microsoft.com/en-us/library/424c79hc.aspx

  

GetMembers方法不会按特定顺序返回成员,例如按字母顺序或声明顺序。您的代码不得依赖于返回成员的顺序,因为该顺序会有所不同。

  

与2个序列化对象相比,为什么还有3个字节?

首先,TypeFormat的“TypesWhenNeeded”实际上应该是'TypesAlways'。这就是为什么会有这么多差异的原因。例如,'= null'变为03之后的05就是由此产生的。

其次,你没有正确的类型。查看ILSpy中的BinaryFormatter和十六进制转储显示标记为“int”的成员实际上是“字符串”。

public class SomeDataFormat // 16 field
{
    public string Name { get; set; }
    public string Country { get; set; } 
    public string UserEmail{ get; set; }
    public bool IsCaptchaDisplayed{ get; set; }
    public bool IsForgotPasswordCaptchaDisplayed{ get; set; }
    public bool IsSaveChecked{ get; set; }
    public string SessionId{ get; set; } 
    public string SelectedLanguage{ get; set; } 
    public string SelectedUiCulture{ get; set; } 
    public string SecurityImageRefId{ get; set; } 
    public string LogOnId{ get; set; } 
    public bool BetaLogOn{ get; set; } 
    public string Amount{ get; set; }
    public string CurrencyTo{ get; set; }
    public string Delivery{ get; set; } 
    public bool displaySSN{ get; set; }
}
  

我错过了什么?我怎么能这样做?

我没有看到使用给定BinaryFormatter的方法。你可以反编译/反转BinaryFormatter的工作方式。

答案 1 :(得分:5)

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

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



示例类:

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

班级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();

可以看出,我传递了一个包含Aabc的类123的新实例作为值。



示例结果数据:

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

Example result data



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

根据上述规范(这里是PDF的直接链接:[MS-NRBF].pdf),流中的每条记录都由RecordTypeEnumeration标识。第2.1.2.1 RecordTypeNumeration节说明:

  

此枚举标识记录的类型。每条记录(MemberPrimitiveUnTyped除外)都以记录类型枚举开头。枚举的大小是一个BYTE。



SerializationHeaderRecord:

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

SerializationHeaderRecord_RecordTypeEnumeration

正如2.1.2.1 RecordTypeEnumeration中所述,0的值标识了SerializationHeaderRecord中指定的2.6.1 SerializationHeaderRecord

  

SerializationHeaderRecord记录必须是二进制序列化中的第一条记录。此记录包含格式的主要版本和次要版本以及顶部对象和标题的ID。

它包括:

  • RecordTypeEnum(1个字节)
  • RootId(4个字节)
  • HeaderId(4个字节)
  • MajorVersion(4个字节)
  • MinorVersion(4个字节)



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

SerializationHeaderRecord_Complete

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开头。当最后一条记录完成时,我们必须假设一条新记录开始。

让我们解释下一个字节:

BinaryLibraryRecord_RecordTypeEnumeration

我们可以看到,在我们的示例中,SerializationHeaderRecord后跟BinaryLibrary记录:

  

BinaryLibrary记录将INT32 ID(在[MS-DTYP]第2.2.22节中指定)与库名称相关联。这允许其他记录使用ID引用库名称。当有多个记录引用相同的库名称时,此方法会减小电线大小。

它包括:

  • RecordTypeEnum(1个字节)
  • LibraryId(4个字节)
  • LibraryName(可变字节数(LengthPrefixedString))



2.1.1.6 LengthPrefixedString中所述......

  

LengthPrefixedString表示字符串值。该字符串以UTF-8编码字符串的长度为前缀,以字节为单位。长度编码在可变长度字段中,最小为1个字节,最多为5个字节。为了最小化导线尺寸,将长度编码为可变长度字段。

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

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId

0C代表标识RecordTypeEnumeration记录的BinaryLibrary

02 00 00 00代表LibraryId,在我们的案例中为2



现在LengthPrefixedString跟随:

BinaryLibraryRecord_RecordTypeEnumeration_LibraryId_LibraryName

42代表包含LengthPrefixedString的{​​{1}}的长度信息。

在我们的例子中,LibraryName(十进制66)的长度信息告诉我们,我们需要读取接下来的66个字节并将它们解释为42

如前所述,字符串是LibraryName编码的,因此上面字节的结果类似于:UTF-8



ClassWithMembersAndTypes:

同样,记录已完成,因此我们解释了下一个的_WorkSpace_, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

ClassWithMembersAndTypesRecord_RecordTypeEnumeration

RecordTypeEnumeration标识05条记录。第ClassWithMembersAndTypes节说明:

  

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

它包括:

  • RecordTypeEnum(1个字节)
  • ClassInfo(可变字节数)
  • MemberTypeInfo(可变字节数)
  • LibraryId(4个字节)



的ClassInfo:

2.3.2.1 ClassWithMembersAndTypes所述,记录包括:

  • ObjectId(4个字节)
  • 名称(可变字节数(再次为2.3.1.1 ClassInfo))
  • MemberCount(4个字节)
  • MemberNames(LengthPrefixedString' s的序列,其中项目数必须等于LengthPrefixedString字段中指定的值。)



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

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId

MemberCount代表01 00 00 00。我们已经看过这个,它被指定为ObjectId中的RootId

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name

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

ClassWithMembersAndTypesRecord_RecordTypeEnumeration_ClassInfo_ObjectId_Name_MemberCount

StackOverFlow代表02 00 00 00,它告诉我们2位成员,均由MemberCount代表。

第一个成员的名字: ClassWithMembersAndTypesRecord_MemberNameOne

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 64MemberName再次是字符串的长度,长度为27个字节,结果如下:1B

第二个成员的名字: ClassWithMembersAndTypesRecord_MemberNameTwo

<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 64MemberName指定字符串长度为26个字节。结果如下:1A



MemberTypeInfo:

<SomeValue>k__BackingField ClassInfo之后。

MemberTypeInfo节声明该结构包含:

  • BinaryTypeEnums(长度可变)
  

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

     
      
  • 与ClassInfo结构的MemberNames字段具有相同数量的项目。

  •   
  • 按顺序排列,使BinaryTypeEnumeration对应ClassInfo结构的MemberNames字段中的成员名称。

  •   
  • AdditionalInfos(长度可变),取决于2.3.1.2 - MemberTypeInfo其他信息可能存在也可能不存在。
  

BinaryTpeEnum
  | BinaryTypeEnum | AdditionalInfos |
  |----------------+--------------------------|
  | Primitive | PrimitiveTypeEnumeration |

因此考虑到这一点,我们几乎就在那里...... 我们预计会有2 | String | None |个值(因为BinaryTypeEnumeration中有2个成员)。



再次,回到完整MemberNames记录的原始数据:

ClassWithMembersAndTypesRecord_MemberTypeInfo

MemberTypeInfo代表第一个成员的01,根据BinaryTypeEnumeration我们可以预期2.1.2.2 BinaryTypeEnumeration,并使用String表示。

LengthPrefixedString代表第二个成员的00,同样,根据规范,它是BinaryTypeEnumeration。如上所述,Primitive之后是其他信息,在本例中为Primitive。这就是为什么我们需要读取PrimitiveTypeEnumeration的下一个字节,使其与08中所述的表格相匹配,并惊讶地发现我们可以期待2.1.2.3 PrimitiveTypeEnumeration由4个字节表示,如关于基本数据类型的其他文档中所述。



库Id:

Int32跟随MemerTypeInfo之后,它由4个字节表示:

ClassWithMembersAndTypesRecord_LibraryId

LibraryId代表02 00 00 00,即2。



值:

LibraryId中所述:

  

类的成员的值必须序列化为遵循此记录的记录,如2.7节所述。记录的顺序必须与ClassInfo(第2.3.1.1节)结构中指定的MemberNames的顺序相匹配。

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

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

BinaryObjectStringRecord_RecordTypeEnumeration

2.3 Class Records标识06。它代表我们BinaryObjectString属性的值(确切地说SomeString。)

根据<SomeString>k__BackingField,它包含:

  • RecordTypeEnum(1个字节)
  • ObjectId(4个字节)
  • 值(可变长度,表示为2.5.7 BinaryObjectString



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

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue

LengthPrefixedString代表03 00 00 00

ObjectId代表03 61 62 63,其中Value是字符串本身的长度,03是转换为61 62 63的内容字节。

希望你能记得有第二个成员abc。知道使用4个字节表示Int32,我们可以得出结论,

BinaryObjectStringRecord_RecordTypeEnumeration_ObjectId_MemberOneValue_MemberTwoValue

必须是我们第二个成员的Int32Value十六进制等于7B十进制,似乎符合我们的示例代码。

所以这是完整的123记录: ClassWithMembersAndTypesRecord_Complete



MessageEnd:

MessageEnd_RecordTypeEnumeration

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

答案 2 :(得分:3)

如果我没弄错,二进制序列化程序会转储有关对象类型名称和命名空间的一些信息。如果这些值与原始类类型和新的“SomeDataFormat”不同,则可以解释大小差异。

您是否尝试过使用十六进制编辑器比较这两个文件?

答案 3 :(得分:2)

当你进行反序列化时,有些东西会升​​级就好了。例如

public class SomeClass()
{
   public short SomeProperty {get;set;}
}

将反序列化为

public class SomeClass()
{
   public long SomeProperty {get;set;}
}

但是如果序列化第二个SomeClass(即带有long的那个),它将导致不同的大小,SomeClass的序列化为short。在这种特殊情况下为6个字节。

更新

反序列化为通用对象,然后使用反射来获取类型。您可能需要对复杂对象进行递归和特殊处理。

using (var fileStream = new FileStream("TestFormatter.dat", FileMode.Open))
        {
            var binaryFormatter = new BinaryFormatter();
            var myObject = binaryFormatter.Deserialize(fileStream);
            var objectProperties = myObject.GetType().GetProperties();
            foreach (var property in objectProperties)
            {
                var propertyTypeName = property.PropertyType.Name; //This will tell you the property Type Name. I.e. string, int64 (long)
            }                
        }

答案 4 :(得分:1)

剩下的差异可能来自您班级缺少的属性。试试这个:

[StructLayout(LayoutKind.Sequential, Pack=1)]
public class SomeDataFormat // 16 field
{
   ...