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






2.48/5 (11投票s)
一个导致浮点运算失效的奇怪场景
引言
这篇文章是关于一个奇怪的 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 日:首次发布