【问题标题】:Decimal byte array constructor in Binaryformatter SerializationBinaryformatter 序列化中的十进制字节数组构造函数
【发布时间】:2025-11-22 02:40:01
【问题描述】:

我正面临一个我无法识别的非常棘手的问题。
我正在运行一个包含数千个对象的非常大的业务 ASP.Net 应用程序;它通过 MemoryStream 使用内存中的序列化/反序列化来克隆应用程序的状态(保险合同)并将其传递给其他模块。多年来它工作得很好。现在有时,不是系统地,在序列化中它会抛出异常

十进制字节数组构造函数需要一个长度为四且包含有效十进制字节的数组。

使用相同的数据运行相同的应用程序,5 次中有 3 次有效。 我启用了所有 CLR 异常,调试 - 异常 - CLR 异常 - 已启用, 所以我猜如果发生错误的初始化/分配到十进制字段,程序应该停止。它不会发生。
我试图在更基本的对象中拆分序列化,但很难尝试识别导致问题的字段。 从生产中的工作版本和这个我从 .Net 3.5 传递到 .NET 4.0 的版本,对 UI 部分而不是业务部分进行了一致的更改。 我会耐心地完成所有的更改。

char *p 在不应该写入的地方看起来像老式的 C 问题,并且只有在序列化过程中检查所有数据时才会出现问题。

在 .Net 的托管环境中是否可能发生这样的事情? 该应用程序很大,但我看不到异常的内存增长。 有什么方法可以调试和追踪问题?

下面是堆栈跟踪的一部分

[ArgumentException: Decimal byte array constructor requires an array of length four containing valid decimal bytes.]
   System.Decimal.OnSerializing(StreamingContext ctx) +260

[SerializationException: Value was either too large or too small for a Decimal.]
   System.Decimal.OnSerializing(StreamingContext ctx) +6108865
   System.Runtime.Serialization.SerializationEvents.InvokeOnSerializing(Object obj, StreamingContext context) +341
   System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder) +448
   System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(WriteObjectInfo objectInfo, NameInfo memberNameInfo, NameInfo typeNameInfo) +969
   System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, Header[] inHeaders, __BinaryWriter serWriter, Boolean fCheck) +1016
   System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Header[] headers, Boolean fCheck) +319
   System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph) +17
   Allianz.Framework.Helpers.BinaryUtilities.SerializeCompressObject(Object obj) in D:\SVN\SUV\branches\SUVKendo\DotNet\Framework\Allianz.Framework.Helpers\BinaryUtilities.cs:98
   Allianz.Framework.Session.State.BusinessLayer.BLState.SaveNewState(State state) in 

对于长篇大论和未确定的问题,我将不胜感激。

【问题讨论】:

  • 我遇到了同样的错误,原来是由联合引起的(使用 StructLayout(LayoutKind.Explicit) 我不小心将布尔解释为小数。应用程序的其余部分使用了小数作为正常的 0,但只有在单独的应用程序的反序列化过程中才会引发此错误,我通过在序列化之前执行 Decimal.GetBytes 并注意到倒数第三个字节是 1 而不是预期的 0 来意识到问题。

标签: c# asp.net serialization memory-management binaryformatter


【解决方案1】:

@Marc 你几乎得到了正确答案。缺少的是发生这种情况的原因。

我遇到了同样的错误,我可以确定这绝对是 .NET 框架中的错误

如何获得异常“十进制字节数组构造函数需要一个长度为四且包含有效十进制字节的数组。”?

如果您正在运行纯 C# 代码,您将永远不会看到此异常。 但是,如果您从 C++ 代码中获取十进制变量,如果 ma​​rshaling 是从 C 中的 VARIANT 到 C# 中的 System.Decimal 完成的,您将看到它。原因是您已经发现的 Decimal.SetBits() 函数中的一个错误。

我将 C 中的 DECIMAL 结构 (wtypes.h) 翻译成 C#,如下所示:

[StructLayout(LayoutKind.Sequential, Pack=1)]
public struct DECIMAL
{
    public UInt16 wReserved;
    public Byte   scale;
    public Byte   sign;
    public UInt32 Hi32;
    public UInt32 Lo32;
    public UInt32 Mid32;
}

但微软在 .NET 框架中为System.Decimal 定义的是不同的:

[Serializable, StructLayout(LayoutKind.Sequential), ComVisible(true)]
public struct Decimal : IFormattable, ....
{
    private int flags;
    private int hi;
    private int lo;
    private int mid;
}

当此结构从 C 传输到 .NET 时,它会被打包到 VARIANT 结构中,并由 .NET Marshaling 转换为托管代码。

现在有趣的部分来了:VARIANT 有这个定义(oaidl.h,简化):

struct tagVARIANT        // 16 byte
{
    union                // 16 byte
    {
        VARTYPE vt;      // 2 byte
        WORD wReserved1; // 2 byte
        WORD wReserved2; // 2 byte
        WORD wReserved3; // 2 byte
        union            // 8 byte
        {
            LONGLONG llVal;
            LONG lVal;
            BYTE bVal;
            SHORT iVal;
            FLOAT fltVal;
            ....
            etc..
        }
        DECIMAL decVal;  // 16 byte      
    }
};

这是一个非常危险的定义,因为DECIMAL 在存储所有其他值的联合之外。 DECIMALVARIANT 大小相同! 这意味着定义VARIANT类型的重要成员VARIANT.vtDECIMAL.wReserved相同。 这可能会导致严重的错误:

 void XYZ(DECIMAL& k_Dec)
 {
    VARIANT k_Var;
    k_Var.vt     = VT_DECIMAL; // WRONG ORDER !
    k_Var.decVal = k_Dec;
    .....
 }

此代码将不起作用,因为在分配 decVal 时,vt 的值被覆盖。

正确的是:

 void XYZ(DECIMAL& k_Dec)
 {
    VARIANT k_Var;
    k_Var.decVal = k_Dec;        
    k_Var.vt     = VT_DECIMAL;
    .....
 }

现在会发生什么:值VT_DECIMAL (14) 被写入DECIMAL.wReserved 因此,在将其编组到 .NET 之后,您将拥有 System.Decimal.flags = 14(假设比例和符号为 0) 现在出现了System.Decimal 类中的错误:

private void SetBits(int[] bits)
{
    ....

    int num = bits[3];
    if (((num & 0x7F00FFFF) == 0) && ((num & 0xFF0000) <= 0x1C0000))
    {
        this.lo = bits[0];
        this.mid = bits[1];
        this.hi = bits[2];
        this.flags = num;
        return;
    }
    throw new ArgumentException(Environment.GetResourceString("Arg_DecBitCtor"));
}

正确的做法是将 0x7F00FFFF 替换为 0x7F000000,因为 DECIMAL.wReserved 完全不相关。 该字段从未使用过。填VARIANT.vt就行了,否则必须为零才可以避免这个bug。

幸运的是,我找到了一个简单的解决方法。 如果您有一个来自VARIANT 编组的十进制变量d_Param,请使用以下代码修复它:

int[] s32_Bits = Decimal.GetBits(d_Param);
s32_Bits[3] = (int)((uint)s32_Bits[3] & 0xFFFF0000); // set DECIMAL.wReserved = 0
d_Param1 = new Decimal(s32_Bits);

这很好用。

【讨论】:

    【解决方案2】:

    那是……非常有趣;那实际上并不是在读取或写入数据 - 它正在调用序列化前回调,又名[OnSerializing],这里映射到decimal.OnSerializingthat 所做的是尝试对这些位进行完整性检查 - 但看起来 BCL 中只是存在一个错误。这是 4.5 中的实现(咳嗽“反射器”咳嗽):

    [OnSerializing]
    private void OnSerializing(StreamingContext ctx)
    {
        try
        {
            this.SetBits(GetBits(this));
        }
        catch (ArgumentException exception)
        {
            throw new SerializationException(Environment.GetResourceString("Overflow_Decimal"), exception);
        }
    }
    

    GetBits 获取 lo/mid/hi/flags 数组,因此我们可以确定传递给 SetBits 的数组是非空的且长度正确。所以要失败,必须失败的部分在SetBits,这里:

    private void SetBits(int[] bits)
    {
        ....
    
        int num = bits[3];
        if (((num & 0x7f00ffff) == 0) && ((num & 0xff0000) <= 0x1c0000))
        {
            this.lo = bits[0];
            this.mid = bits[1];
            this.hi = bits[2];
            this.flags = num;
            return;
        }
        throw new ArgumentException(Environment.GetResourceString("Arg_DecBitCtor"));
    }
    

    基本上,如果if测试通过我们进入,赋值,并成功退出;如果if 测试失败,它最终会抛出异常。 bits[3]flags 块,它包含符号和比例,IIRC。所以这里的问题是:你是如何得到一个无效的decimal 和一个损坏的flags 块的?

    引用 MSDN:

    返回数组的第四个元素包含比例因子和 标志。它由以下部分组成: 位 0 到 15,低位 字,未使用且必须为零。位 16 到 23 必须包含一个 0 到 28 之间的指数,表示除以 10 的幂 整数。位 24 到 30 未使用,必须为零。位 31 包含符号:0 表示正,1 表示负。

    所以要通过这个测试:

    • 指数无效(0-28 以外)
    • 低位字非零
    • 高字节(不包括 MSB)非零

    不幸的是,我没有任何神奇的方法可以找出哪个 decimal 无效...

    我能想到的唯一方法是:

    • 在整个代码中分散GetBits / new decimal(bits) - 可能作为void SanityCheck(this decimal) 方法(可能带有[Conditional("DEBUG")] 或其他东西)
    • [OnSerializing] 方法添加到您的主域模型中,该方法记录在某处(可能是控制台),这样您就可以看到它在爆炸时正在处理的对象

    【讨论】:

    • 异常消息似乎也颇具误导性——它谈论的是一个由 4 个“十进制字节”组成的数组,而 decimal 没有这样的构造函数……什么是“十进制字节”?
    • @MatthewWatson 确实——我们甚至没有在这里使用构造函数;通常,SetBitspublic Decimal(int[] bits) 调用,这有点解释了该消息,但是是的:令人困惑。
    最近更新 更多