当我尝试 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
等待
fstcw word ptr [esp] //在堆栈上存储浮点控制字
等待
fldcw word ptr [cwChop] //加载浮点控制字
fistp qword ptr [esp $ 04] //将ST0中的值转换为有符号整数
//将结果存储在目标操作数中
//并弹出堆栈(递增堆栈指针)
等待
fldcw word ptr [esp] //加载浮点控制字
pop ecx
流行的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 寄存器将包含相同的浮点值:
注意:仔细观察者会注意到上面屏幕截图中的值与第一个屏幕截图不匹配 . 那是因为我采取了不同的运行方式 . 我宁愿不必仔细重做问题中的所有常量只是为了使它们保持一致 - 但请相信我:当我达到fld指令之后的help指令时,它是一样的 .
导致它:
-
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],$ 41d62866
v:= Trunc(ns)fld qword ptr [ebp- $ 08]
打电话给@TRUNC
在 call
到 @trunc
时,浮点寄存器ST0包含一个值:
但呼叫确实 not 失败了 . 它只会失败, every time 在我的代码的这一部分中 .
可能发生什么导致CPU抛出 invalid floating point exception
?
加载控制字之前cwChop的值是多少?
在负载控制字 $1F32
之前, cwChop
的值看起来是正确的 . 但是在加载之后, actual 控制字是错误的:
Bonus Chatter
失败的实际功能是将高性能滴答计数转换为纳秒:
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;
我创建了所有intermeidate临时变量,以尝试追踪失败的位置 .
我甚至尝试使用它作为模板来尝试重现它:
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单元测试期间调用它的事情 .
浮点控制字标志
德尔福的标准控制字: $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()
.
2 回答
问题发生在其他地方 . 当您的代码输入
Trunc
时,控制字设置为$027F
,即IIRC,默认的Windows控制字 . 这有掩盖的所有例外 . 那个's a problem because Delphi'的RTL期望异常被解除屏蔽 .看看FPU窗口,确定有错误 . IE和PE标志都已设置 . 重要的是IE . 这意味着在代码序列的早期有一个被屏蔽的无效操作 .
然后调用
Trunc
,修改控制字以取消屏蔽异常 . 看看你的第二个FPU窗口截图 . IE是1,但IM是0.所以繁荣,早先的例外被提出,你被认为是Trunc
的错 . 它不是 .您需要跟踪调用堆栈,以找出控制字不是Delphi程序中应该存在的原因 . 它应该是
$1332
. 很可能你正在调用一些第三方库来修改控制字并且不会恢复它 . 每当对该函数的任何调用返回时,您都必须找到罪魁祸首并负责 .一旦你将控制字恢复到控制之下,你就会发现这个异常的真正原因 . 显然,有一个非法的FP操作 . 一旦控制字取消屏蔽异常,就会在正确的位置引发错误 .
请注意,没有什么可担心
$1372
和$1332
之间或$1F72
和$1F32
之间的差异 . 对于CTRL
控制字而言,这只是一个奇怪的事情,其中一些字节是保留的,并忽略了你的劝诫以清除它们 .您的最新更新基本上会提出一个不同的问题 . 它询问此代码引发的异常:
此代码失败,因为
Round()
的作业是将d
舍入为最接近的Int64
值 . 但是d
的值大于可以存储在Int64
中的最大可能值,因此浮点单元陷阱 .