【发布时间】:2014-03-05 21:57:36
【问题描述】:
当我尝试 Trunc() Real 值时,我得到了一个(可重复的)浮点异常。
例如:
Trunc(1470724508.0318);
实际上实际代码更复杂:
ns: Real;
v: Int64;
ns := ((HighPerformanceTickCount*1.0)/g_HighResolutionTimerFrequency) * 1000000000;
v := Trunc(ns);
但最终还是归结为:
Trunc(ARealValue);
现在,我不能在其他任何地方重复它——就在这个地方。每次都失败的地方。
这不是巫毒教
幸运的是,计算机并不是魔法。英特尔 CPU 执行非常具体的可观察操作。所以我应该能够弄清楚为什么浮点运算失败了。
进入 CPU 窗口
v := Trunc(ns)
fld qword ptr [ebp-$10]
这会将 ebp-$10 处的 8 字节浮点值加载到浮点寄存器 ST0。
内存地址 [ebp-$10] 的字节为:
0018E9D0: 6702098C 41D5EA5E (as DWords)
0018E9D0: 41D5EA5E6702098C (as QWords)
0018E9D0: 1470724508.0318 (as Doubles)
调用成功,浮点寄存器中包含适当的值:
接下来是对 RTL Trunc 函数的实际调用:
call @TRUNC
接下来是 Delphi RTL 的 Trunc 函数的内脏:
@TRUNC:
sub esp,$0c wait fstcw word ptr [esp] //Store Floating-Point Control Word on the stack wait fldcw word ptr [cwChop] //Load Floating-Point Control Word fistp qword ptr [esp+$04] //Converts value in ST0 to signed integer //stores the result in the destination operand //and pops the stack (increments the stack pointer) wait fldcw word ptr [esp] //Load Floating-Point Control Word pop ecx pop eax pop edx ret
或者我想我可以只是从 rtl 粘贴它,而不是从 CPU 窗口转录它:
const cwChop : Word = $1F32;
procedure _TRUNC;
asm
{ -> FST(0) Extended argument }
{ <- EDX:EAX Result }
SUB ESP,12
FSTCW [ESP] //Store foating-control word in ESP
FWAIT
FLDCW cwChop //Load new control word $1F32
FISTP qword ptr [ESP+4] //Convert ST0 to int, store in ESP+4, and pop the stack
FWAIT
FLDCW [ESP] //restore the FPCW
POP ECX
POP EAX
POP EDX
end;
异常发生在实际fistp操作期间。
fistp qword ptr [esp+$04]
在此调用的时刻,ST0 寄存器将包含相同的浮点值:
注意:细心的观察者会注意到上面截图中的值与第一个截图不匹配。那是因为我采取了不同的方式。我宁愿不必为了使它们保持一致而仔细重做问题中的所有常量 - 但请相信我:当我到达
fistp指令时和在fld指令之后是一样的。
走向它:
-
sub esp,$0c:我看到它把堆栈向下推了 12 个字节 -
fstcw word ptr [esp]: 我看着它把 $027F 推入当前堆栈指针 -
fldcw word ptr [cwChop]: 我看浮点控制标志的变化 -
fistp qword ptr [esp+$04]: 即将把 Int64 写入栈中创建的房间
然后它崩溃了。
这里到底发生了什么?
它也发生在其他值上,这并不是说这个特定的浮点值有什么问题。但我什至尝试在其他地方设置测试用例。
知道浮点数的 8 字节十六进制值是:$41D5EA5E6702098C,我尝试设计设置:
var
ns: Real;
nsOverlay: Int64 absolute ns;
v: Int64;
begin
nsOverlay := $41d62866a2f270dc;
v := Trunc(ns);
end;
这给出了:
nsOverlay := $41d62866a2f270dc;
mov [ebp-$08],$a2f270dc mov [ebp-$04],$41d62866v := Trunc(ns)
fld qword ptr [ebp-$08] call @TRUNC
而在call到@trunc这一点,浮点寄存器ST0包含一个值:
但调用确实不失败。它只会在我的代码的这一部分中每次都失败。
可能发生什么导致 CPU 抛出 invalid floating point exception?
cwChop 在加载控制字之前的值是多少?
cwChop 的值在加载控制字之前看起来是正确的,$1F32。但是加载后,实际控制字错了:
奖金聊天
实际失败的功能是将高性能滴答计数转换为纳秒:
function PerformanceTicksToNs(const HighPerformanceTickCount: Int64): Int64;
//Convert high-performance ticks into nanoseconds
var
ns: Real;
v: Int64;
begin
Result := 0;
if HighPerformanceTickCount = 0 then
Exit;
if g_HighResolutionTimerFrequency = 0 then
Exit;
ns := ((HighPerformanceTickCount*1.0)/g_HighResolutionTimerFrequency) * 1000000000;
v := Trunc(ns);
Result := v;
end;
我创建了所有中间临时变量来尝试追踪失败的位置。
我什至尝试使用它作为模板来尝试重现它:
var
i1, i2: Int64;
ns: Real;
v: Int64;
vOver: Int64 absolute ns;
begin
i1 := 5060170;
i2 := 3429541;
ns := ((i1*1.0)/i2) * 1000000000;
//vOver := $41d62866a2f270dc;
v := Trunc(ns);
但它工作正常。在 DUnit 单元测试期间调用它的时间有所不同。
浮点控制字标志
Delphi的标准控制字:$1332:
$1332 = 0001 00 11 00 110010
0 ;Don't allow invalid numbers
1 ;Allow denormals (very small numbers)
0 ;Don't allow divide by zero
0 ;Don't allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
0 ;reserved exception mask
0 ;reserved
11 ;Precision Control - 11B (Double Extended Precision - 64 bits)
00 ;Rounding control -
0 ;Infinity control - 0 (not used)
The Windows API required value:$027F
$027F = 0000 00 10 01 111111
1 ;Allow invalid numbers
1 ;Allow denormals (very small numbers)
1 ;Allow divide by zero
1 ;Allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
1 ;reserved exception mask
0 ;reserved
10 ;Precision Control - 10B (double precision)
00 ;Rounding control
0 ;Infinity control - 0 (not used)
crChop 控制字:$1F32
$1F32 = 0001 11 11 00 110010
0 ;Don't allow invalid numbers
1 ;Allow denormals (very small numbers)
0 ;Don't allow divide by zero
0 ;Don't allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
0 ;reserved exception mask
0 ;unused
11 ;Precision Control - 11B (Double Extended Precision - 64 bits)
11 ;Rounding Control
1 ;Infinity control - 1 (not used)
000 ;unused
加载$1F32后的CTRL标志:$1F72
$1F72 = 0001 11 11 01 110010
0 ;Don't allow invalid numbers
1 ;Allow denormals (very small numbers)
0 ;Don't allow divide by zero
0 ;Don't allow overflow
1 ;Allow underflow
1 ;Allow inexact precision
1 ;reserved exception mask
0 ;unused
11 ;Precision Control - 11B (Double Extended Precision - 64 bits)
11 ;Rounding control
1 ;Infinity control - 1 (not used)
00011 ;unused
CPU 所做的只是打开一个保留的、未使用的掩码位。
RaiseLastFloatingPointError()
如果您要为 Windows 开发程序,您确实需要接受这样一个事实:浮点异常应该被 CPU 屏蔽,这意味着您必须自己注意它们。像Win32Check 或RaiseLastWin32Error,我们想要RaiseLastFPError。我能想到的最好的是:
procedure RaiseLastFPError();
var
statWord: Word;
const
ERROR_InvalidOperation = $01;
// ERROR_Denormalized = $02;
ERROR_ZeroDivide = $04;
ERROR_Overflow = $08;
// ERROR_Underflow = $10;
// ERROR_InexactResult = $20;
begin
{
Excellent reference of all the floating point instructions.
(Intel's architecture manuals have no organization whatsoever)
http://www.plantation-productions.com/Webster/www.artofasm.com/Linux/HTML/RealArithmetica2.html
Bits 0:5 are exception flags (Mask = $2F)
0: Invalid Operation
1: Denormalized - CPU handles correctly without a problem. Do not throw
2: Zero Divide
3: Overflow
4: Underflow - CPU handles as you'd expect. Do not throw.
5: Precision - Extraordinarily common. CPU does what you'd want. Do not throw
}
asm
fwait //Wait for pending operations
FSTSW statWord //Store floating point flags in AX.
//Waits for pending operations. (Use FNSTSW AX to not wait.)
fclex //clear all exception bits the stack fault bit,
//and the busy flag in the FPU status register
end;
if (statWord and $0D) <> 0 then
begin
//if (statWord and ERROR_InexactResult) <> 0 then raise EInexactResult.Create(SInexactResult)
//else if (statWord and ERROR_Underflow) <> 0 then raise EUnderflow.Create(SUnderflow)}
if (statWord and ERROR_Overflow) <> 0 then raise EOverflow.Create(SOverflow)
else if (statWord and ERROR_ZeroDivide) <> 0 then raise EZeroDivide.Create(SZeroDivide)
//else if (statWord and ERROR_Denormalized) <> 0 then raise EUnderflow.Create(SUnderflow)
else if (statWord and ERROR_InvalidOperation) <> 0 then raise EInvalidOp.Create(SInvalidOp);
end;
end;
一个可重现的案例!
我发现了一个案例,当 Delphi 的默认浮点控制字时,这是一个无效浮点异常的原因(虽然我以前从未见过它,因为它被屏蔽了)。现在我看到了,为什么会这样!而且它是可重现的:
procedure TForm1.Button1Click(Sender: TObject);
var
d: Real;
dover: Int64 absolute d;
begin
d := 1.35715152325557E020;
// dOver := $441d6db44ff62b68; //1.35715152325557E020
d := Round(d); //<--floating point exception
Self.Caption := FloatToStr(d);
end;
您可以看到ST0 寄存器包含一个有效的浮点值。浮点控制字为$1372。有浮点异常标志全部清除:
然后,一旦执行,它就是一个无效的操作:
-
IE(无效操作)标志已设置 -
ES(异常)标志已设置
我很想把这个问题当作另一个问题来问,但这将是完全相同的问题——除了这次打电话给Round()。
【问题讨论】:
-
一个可能的罪魁祸首似乎是
cwChop的值。它是否已被导致故障位置的某些东西损坏? -
@500-InternalServerError 我想我明白你在看什么。它被指示
load更正标志,但之后CTRL寄存器错误。 -
你可能想看看这个:wiert.me/2009/05/06/…
-
顺便问一句好问题。照常。截图有我需要的一切。不用担心 1f32 和 1f72 之间的差异。 CTRL 寄存器总是这样做的。它是寄存器中未使用的保留部分。一切如预期。
-
将float转换为Int64(Round和Trunc),当float大于maxInt64时会导致浮点异常。