65.9K
CodeProject 正在变化。 阅读更多。
Home

Visual C++ 的 pow 函数第六次不起作用!

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.48/5 (11投票s)

2006年7月31日

CPOL

3分钟阅读

viewsIcon

55230

一个导致浮点运算失效的奇怪场景

引言

这篇文章是关于一个奇怪的 bug 以及重现它的场景。Pow(10, n) 函数在第六次调用时会出错。很奇怪,对吧?这是报告给我的 bug 的标题,以下是所做的分析。

分析

在调试时,我发现 pow(10, n) 的结果,其中 n 在所有测试用例中都是 1 或 2,在多次调用时都没问题,但到某个时刻,它开始返回 1#INF,而不是仅仅返回 10 或 100。

深入到 pow 的反汇编代码中,我发现到某个时刻,一个 fld1 命令,该命令原本应该将 1.0 插入到协处理器堆栈中,却插入了 1#IND,而不是 1。

我首先想到的是,某些东西正在破坏协处理器的状态。因此,我决定将 pow 行向上移动调用堆栈,并计算它在哪个迭代中会返回 1#INF。我重复在每次函数调用之前和之后进行检查,并向上遍历调用堆栈。这是我使用的调查代码。

static int y = 0;
y++;
double x = pow(10, 2);
ASSERT(x==100.0);
// Here goes a function call to check
double x = pow(10, 2);
ASSERT(x==100.0);

最后,我发现一些函数,当我在调用前后进行这两次调用时,会在调用后失败。这发生在第 6 次迭代中。但是,当我在该函数的开始和结束时进行检查时,它会在第 7 次迭代中失败,并且是在开始检查时失败。

现在很清楚,从该函数返回时协处理器的状态被破坏了。起初,我不明白为什么仅仅返回会导致问题。

查看该函数的原型,它非常简单。该函数被声明为返回 double,并且是从一个 DLL 中导出的。调用 EXE 使用 LoadLibrary 加载 DLL,并将从 GetProcAddress 返回的指针强制转换为返回 void 的函数的指针。我一直知道这不是问题,但是当我修复它时,pow bug 就消失了。

结论

就是这么简单。当 C++ 编译器编译一个返回 int 的函数时,它将其放入处理器通用寄存器 *EAX* 中。如果您只是忽略了结果,甚至将函数强制转换为返回 void 的函数,一切都会很好。我以为返回 double 也是这种情况,但事实并非如此。

当 C++ 编译器编译一个返回 double 的函数时,它会将返回值压入协处理器堆栈(即,在 *ST0* 上)。如果调用者忽略了返回值,编译器会生成指令来释放协处理器的堆栈。但是,如果我们将函数指针强制转换为返回 void 的函数,则调用者不会释放协处理器,因此它会在一段时间后发生堆栈溢出。这就是为什么 fld1(原本应该添加 1.0)会添加 1#IND。有时这会抛出异常,有时则不会,我不知道为什么。

示例

这个例子展示了如何重现这个 bug。

#include "stdio.h"
#include "math.h" 
double function()
{
    return 0.0;
}
 
typedef void (*LPVOIDPROC) ();

int main()
{
    LPVOIDPROC lpVoidProc = (LPVOIDPROC)function; 
    double dValue;
    int iCounter = 0; 
    do
    {
        lpVoidProc(); 
        dValue = pow(10, 2);
        printf("Iteration %d -> Value %g\r\n", ++iCounter, dValue);
    }
    while(dValue == 100); 
    return 0;
}

您会发现从第 1 到第 5 次迭代输出都是 100,而在第 6 次迭代时,它会给出 1#INF。

当调用返回 double 的函数时,编译器将忽略协处理器堆栈上的值,因为我们将该函数强制转换为返回 void 的函数。当然,这会导致其他奇怪的浮点计算问题,而不仅仅是 pow 函数。pow 只是我遇到的一个例子。

历史

  • 2006 年 7 月 31 日:首次发布
© . All rights reserved.