6

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

十进制字节数组构造函数需要一个长度为 4 的数组,其中包含有效的十进制字节。

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

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

在 .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 

对于长篇大论和未确定的问题,我将非常感谢任何帮助。

4

2 回答 2

3

That is.... very interesting; that is not actually reading or writing data at that time - it is calling the before-serialization callback, aka [OnSerializing], which here maps to decimal.OnSerializing. What that does is attempt to sanity-check the bits - but it looks like there is simply a bug in the BCL. Here's the implementation in 4.5 (cough "reflector" cough):

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

The GetBits gets the lo/mid/hi/flags array, so we can be pretty sure that the array passed to SetBits is non-null and the right length. So for that to fail, the part that must be failing is in SetBits, here:

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"));
}

Basically, if the if test passes we get in, assign the values, and exit successfully; if the if test fails, it ends up throwing an exception. bits[3] is the flags chunk, which holds the sign and scale, IIRC. So the question here is: how have you gotten hold of an invalid decimal with a broken flags chunk?

to quote from MSDN:

The fourth element of the returned array contains the scale factor and sign. It consists of the following parts: Bits 0 to 15, the lower word, are unused and must be zero. Bits 16 to 23 must contain an exponent between 0 and 28, which indicates the power of 10 to divide the integer number. Bits 24 to 30 are unused and must be zero. Bit 31 contains the sign: 0 mean positive, and 1 means negative.

So to fail this test:

  • the exponent is invalid (outside 0-28)
  • the lower word is non-zero
  • the upper byte (excluding the MSB) is non-zero

Unfortunately, I have no magic way of finding which decimal is invalid...

The only ways I can think of looking here are:

  • scatter GetBits / new decimal(bits) throughout your code - perhaps as a void SanityCheck(this decimal) method (maybe with a [Conditional("DEBUG")] or something)
  • add [OnSerializing] methods into your main domain model, that log somewhere (console maybe) so you can see what object it was working on when it exploded
于 2013-08-09T08:01:52.463 回答
1

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

我得到了同样的错误,我可以保证这绝对是.NET 框架中的一个错误

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

好吧,如果您正在运行纯 C# 代码,您将永远不会看到此异常。但是,如果您从 C++ 代码中获取十进制变量,那么如果编组是从VARIANTC 到System.DecimalC# 中完成的,您将看到它。原因是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它位于存储所有其他值的联合之外。 DECIMAL并且VARIANT具有相同的尺寸!这意味着VARIANT.vt定义类型的重要成员VARIANT与 相同DECIMAL.wReserved。这可能会导致严重的错误:

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

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

正确的是:

 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"));
}

正确的做法是用 0x7F000000 替换 0x7F00FFFF 因为DECIMAL.wReserved完全不相关。该字段从未使用过。只是填写,VARIANT.vt否则它必须为零以避免这个错误。

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

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

这完美地工作。

于 2020-02-01T17:54:44.073 回答