测试浮点 NaN 会导致堆栈溢出

Posted

技术标签:

【中文标题】测试浮点 NaN 会导致堆栈溢出【英文标题】:Testing for a float NaN results in a stack overflow 【发布时间】:2014-10-02 00:41:19 【问题描述】:

C#,VS 2010

我需要确定一个浮点值是否为 NaN。

使用

测试 NaN 的浮点数
float.IsNaN(aFloatNumber) 

由于堆栈溢出而崩溃。

也是这样

aFloatNumber.CompareTo(float.NaN).

以下内容不会崩溃,但它没有用,因为它无论如何都会返回 NaN:

aFloatNumber - float.NaN 

搜索“堆栈溢出”会返回有关此网站的结果,而不是有关实际堆栈溢出的结果,因此我找不到相关答案。

为什么我的应用程序在测试 NaN 时会发生堆栈溢出?

编辑:调用堆栈:

编辑:这显然是我的代码中的内容:此声明:

bool aaa = float.IsNaN(float.NaN);
在应用程序的构造函数中工作正常,就在 InitializeComponent() 之后; 在自定义控件类的构造函数中工作正常,就在 InitializeComponent() 之后; 但在自定义控件类内的事件处理程序中崩溃。

所以,这就是我正在做的:

抽象自定义控件:公共抽象部分类 ConfigNumberBaseBox : TextBox 有一个验证事件处理程序 ValidateTextBoxEntry ValidateTextBoxEntry 在 ConfigNumberBaseBox 类中定义 从 ConfigNumberBaseBox 继承的自定义控件:公共部分类 ConfigTemperBox:ConfigNumberBaseBox 运行应用程序 当我完成对 ConfigTemperBox 控件的编辑后,将调用 ValidateTextBoxEntry ValidateTextBoxEntry 运行良好,直到遇到 float.IsNaN 堆栈溢出

编辑:

Debug.WriteLine() 显示代码只执行一次:没有递归。

编辑:

这行得通:

float fff = 0F;
int iii = fff.CompareTo(float.PositiveInfinity);

这会崩溃:

float fff = 0F;
int iii = fff.CompareTo(float.NaN);

【问题讨论】:

我不相信这是比较,它必须是您代码中的其他内容。当你得到 *** 时,调用堆栈是什么? IsNaN 定义为return (*(int*)(&f) & 0x7FFFFFFF) > 0x7F800000;。这本身不能进行堆栈溢出。你的代码有问题。 创建一个显示此行为的最小示例。 这可能是调试器在不相关的行上向您显示异常,如果您有递归,请查看其他地方 ValidateTextBoxEntry 可能被递归调用(当然是无意的 :),在其中创建一个 Debug.WriteLine() 并观察应用程序的输出 【参考方案1】:

在自定义控件的类的构造函数中工作正常

这是对潜在问题的唯一真正暗示。在线程上运行的代码可以操作处理器内的两个堆栈。一个是每个人都知道的普通网站,并给这个网站起了名字。然而,还有另一个,很好地隐藏在 FPU(浮点单元)中。它在进行浮点计算时存储中间操作数值。它有 8 层深。

FPU 内的任何类型的事故都应该产生运行时异常。 CLR 假定 FPU 配置了 FPU 控制字的默认值,它可以生成的硬件异常应该被禁用。

当您的程序使用来自 1990 年代的代码时,确实有出错的诀窍,当启用 FPU 异常听起来仍然是个好主意时。例如,由 Borland 工具生成的代码就因为这样做而臭名昭著。它的 C 运行时模块重新编程 FPU 控制字并取消屏蔽硬件异常。您可以获得的异常类型可能非常神秘,在代码中使用 NaN 是触发此类异常的好方法。

这应该至少在调试器中部分可见。在“仍然很好”的代码上设置断点并使用 Debug + Windows + Registers 调试器窗口。右键单击它并选择“浮点”。您将看到与浮点计算相关的所有寄存器,例如 ST0 到 ST7 是堆栈寄存器。这里重要的标记为CTRL,它在.NET 进程中的正常值是027F。该值的最后 6 位是异常屏蔽位 (0x3F),全部打开以防止硬件异常。

单步执行代码,您会看到 CTRL 值发生变化。一旦这样做,您就会找到邪恶的代码。如果您启用非托管调试,那么您还应该在“输出”窗口中看到加载通知,并看到它出现在“调试 + Windows + 模块”窗口中。

撤消 DLL 造成的损害是相当尴尬的。例如,您必须在 msvcrt.dll 中 pinvoke _control87() 才能恢复 CTRL 字。或者您可以使用一个简单的技巧,您可以有意抛出异常。 CLR 内部的异常处理逻辑会重置 FPU 控制字。所以运气好的话,这种代码可以解决你的问题:

    InitializeComponent();
    try  throw new Exception("Please ignore, resetting FPU"); 
    catch 

您可能需要移动它,下一个最佳猜测是 Load 事件。调试器应该会告诉你在哪里。

【讨论】:

天哪!如果我在对 float.NaN 的引用之前添加代码,我将不再得到 Stack Overflow。 (将它放在构造函数中没有区别)。 好吧,我猜对了这个问题。你应该为它找到一个更好的地方,在DLL加载之后是最好的地方。使用我描述的调试器来找到那个地方。 耶稣我希望有一天能达到这个水平 这与你的“糟糕代码”没有任何关系,它是别人的。不依赖于您不了解、无法真正支持并且其程序员可能很久以前就已经开始工作的代码当然是最好的。我当然不能给你这样的建议,这完全取决于你。 @DavideAndrea Hans Passant 说您正在使用一些外部代码/组件changes the control word。【参考方案2】:

我刚刚写了一个例子来重现错误: 1. 创建一个本地 C/C++ DLL 来导出这个函数:

extern "C" __declspec(dllexport)  int SetfloatingControlWord(void)

    //unmask all the floating excetpions
    int err = _controlfp_s(NULL, 0, _MCW_EM);    
    return err;

2。创建一个C#控制台程序,调用SetfloatingControlWord函数,然后进行一些浮动操作,如NaN比较,导致堆栈溢出。

 [DllImport("floatcontrol.dll")]
        public static extern Int32 SetfloatingControlWord();       
        static void Main(string[] args)
        
           int n =   SetfloatingControlWord();          
           float fff = 0F;
           int iii = fff.CompareTo(float.NaN);
        

几年前我也遇到过同样的问题,我注意到在抛出 .NET 异常后,一切正常,我花了一段时间才弄清楚原因并跟踪更改 FPU 的代码。

正如函数_controlfp_s 的文档所说:默认情况下,运行时库会屏蔽所有浮点异常。公共语言运行时 (CLR) 仅支持默认浮点精度,因此 CLR 不处理此类异常。

正如MSDN所说:系统默认关闭所有FP异常。因此,计算会导致 NAN 或 INFINITY,而不是异常。

在 IEEE 754 1985 引入 NaN 后,假设应用软件不再需要处理浮点异常。

【讨论】:

您极大地推进了我对问题的搜索。谢谢!我将发布答案并将您称为灵感。【参考方案3】:

解决方案

首先,感谢@Matt 为我指明了正确的方向,感谢@Hans Passant 提供了解决方法。

应用程序与中国制造商 QM_CAN 的 CAN-USB 适配器通信。

问题出在他们的驱动程序上。

DLL 语句和驱动程序导入:

   // DLL Statement
    IntPtr QM_DLL;
    TYPE_Init_can Init_can;
    TYPE_Quit_can Quit_can;
    TYPE_Can_send Can_send;
    TYPE_Can_receive Can_receive;
    delegate int TYPE_Init_can(byte com_NUM, byte Model, int CanBaudRate, byte SET_ID_TYPE, byte FILTER_MODE, byte[] RXF, byte[] RXM);
    delegate int TYPE_Quit_can();
    delegate int TYPE_Can_send(byte[] IDbuff, byte[] Databuff, byte FreamType, byte Bytes);
    delegate int TYPE_Can_receive(byte[] IDbuff, byte[] Databuff, byte[] FreamType, byte[] Bytes);

    // Driver
    [DllImport("kernel32.dll")]
    static extern IntPtr LoadLibrary(string lpFileName);
    [DllImport("kernel32.dll")]
    static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

调用有问题的代码,包括 Hans 的解决方法:

   private void InitCanUsbDLL() // Initiate the driver for the CAN-USB dongle
    

        // Here is an example of dynamically loaded DLL functions
        QM_DLL = LoadLibrary("QM_USB.dll");
        if (QM_DLL != IntPtr.Zero)
        
            IntPtr P_Init_can = GetProcAddress(QM_DLL, "Init_can");
            IntPtr P_Quit_can = GetProcAddress(QM_DLL, "Quit_can");
            IntPtr P_Can_send = GetProcAddress(QM_DLL, "Can_send");
            IntPtr P_Can_receive = GetProcAddress(QM_DLL, "Can_receive");
            // The next line results in a FPU stack overflow if float.NaN is called by a handler
            Init_can = (TYPE_Init_can)Marshal.GetDelegateForFunctionPointer(P_Init_can, typeof(TYPE_Init_can));
            // Reset the FPU, otherwise we get a stack overflow when we work with float.NaN within a event handler
            // Thanks to Matt for pointing me in the right direction and to Hans Passant for this workaround:
            // http://***.com/questions/25205112/testing-for-a-float-nan-results-in-a-stack-overflow/25206025
            try  throw new Exception("Please ignore, resetting FPU"); 
            catch   
            Quit_can = (TYPE_Quit_can)Marshal.GetDelegateForFunctionPointer(P_Quit_can, typeof(TYPE_Quit_can));
            Can_send = (TYPE_Can_send)Marshal.GetDelegateForFunctionPointer(P_Can_send, typeof(TYPE_Can_send));
            Can_receive = (TYPE_Can_receive)Marshal.GetDelegateForFunctionPointer(P_Can_receive, typeof(TYPE_Can_receive));
        
    

当在事件处理程序中而不是在构造函数中引用 float.NaN 时应用程序崩溃的原因是一个简单的时间问题:构造函数在 InitCanUsbDLL() 之前调用,但事件处理程序被调用 long在 InitCanUsbDLL() 损坏 FPU 寄存器之后。

【讨论】:

我真的很惊讶。我的建议:使用此驱动程序创建单独的进程,并通过一些进程间通信技术(例如 TCP 或一些消息队列)与您的主进程进行通信。我对其他一些本机库有奇怪的问题,这解决了它(以及 64 位系统问题上的 32 位库)。最好是用 C++ 创建你的中间进程。 假设这不会改变行为,我会将try throw new Exception("Please ignore, resetting FPU"); catch 放在它自己的单行函数中。拥有一个包含 try...catch 的函数有助于明确哪些代码是 FPU 修复的一部分,哪些不是。

以上是关于测试浮点 NaN 会导致堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

堆栈缓冲区溢出会导致堆损坏吗?

为啥带有 setTimeout 的函数不会导致堆栈溢出

为啥增加递归深度会导致堆栈溢出错误?

为啥这个 BigInteger 值会导致堆栈溢出异常? C#

堆栈溢出一般是由啥原因导致的?

UIKit pushViewController:animated: 上的调用会导致最终的堆栈溢出(或其他异常)吗?