Win32上双重强制转换为 unsignedint 的结果截断为2,147,483,648

编译以下代码:

double getDouble()
{
double value = 2147483649.0;
return value;
}


int main()
{
printf("INT_MAX: %u\n", INT_MAX);
printf("UINT_MAX: %u\n", UINT_MAX);


printf("Double value: %f\n", getDouble());
printf("Direct cast value: %u\n", (unsigned int) getDouble());
double d = getDouble();
printf("Indirect cast value: %u\n", (unsigned int) d);


return 0;
}

输出(MSVC x86) :

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649

输出(MSVC x64) :

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649

Microsoft 文档中,从 doubleunsigned int的转换中没有提到有符号整数 max 值。

当返回一个函数时,所有高于 INT_MAX的值都被截断为 2147483648

我使用 Visual Studio 2019来构建程序,这在 GCC上是不会发生的。

我是不是做错了什么? 有没有安全的方法把 double转换成 unsigned int

4846 次浏览

编译器漏洞..。

从@anastaciu 提供的汇编中,直接转换代码调用 __ftol2_sse,它似乎将数字转换为 签了很长时间。例程名称为 ftol2_sse,因为这是一台启用了 sse 的机器——但是 float 位于 x87浮点寄存器中。

; Line 17
call    _getDouble
call    __ftol2_sse
push    eax
push    OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@
call    _printf
add esp, 8

另一方面,间接铸造的确如此

; Line 18
call    _getDouble
fstp    QWORD PTR _d$[ebp]
; Line 19
movsd   xmm0, QWORD PTR _d$[ebp]
call    __dtoui3
push    eax
push    OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@
call    _printf
add esp, 8

它弹出并将 double 值存储到局部变量中,然后将其加载到 SSE 寄存器中,并调用 __dtoui3,这是一个 double 到 unsignedint 的转换例程..。

直接铸造的行为不符合 C89; 也不符合以后的任何修订—— 甚至 C89明确指出:

将整数类型的值转换为无符号类型时所完成的余数操作不需要在将浮点类型的值转换为无符号类型时完成。因此,可移植值的范围是 [0,Utype _ MAX + 1)


我认为问题可能出在 从2005年开始一直延续下去上——过去有一个叫做 __ftol2的转换函数,这个函数可能对这段代码有用,也就是说,它会把值转换成 有签名的号码-2147483647,这样在解释一个无符号数字时就会产生正确的结果。

不幸的是,__ftol2_sse并不是 __ftol2的替代品,因为它不会仅仅通过返回 LONG_MIN/0x80000000来获取最低有效值位,而是通过返回 LONG_MIN/0x80000000来发出超出范围的错误信号,这里解释为无符号 long 根本不是预期的那样。__ftol2_sse的行为对于 signed long是有效的,因为将一个双 a 值 > LONG_MAX转换为 signed long将具有未定义的行为。

@ AnttiHaapala 的回答之后,我使用优化 /Ox测试了代码,发现这将消除这个 bug,因为不再使用 __ftol2_sse:

//; 17   :     printf("Direct cast value: %u\n", (unsigned int)getDouble());


push    -2147483647             //; 80000001H
push    OFFSET $SG10116
call    _printf


//; 18   :     double d = getDouble();
//; 19   :     printf("Indirect cast value: %u\n", (unsigned int)d);


push    -2147483647             //; 80000001H
push    OFFSET $SG10117
call    _printf
add esp, 28                 //; 0000001cH

这些优化内联了 getdouble()并添加了常量表达式计算,从而消除了在运行时进行转换的需要,从而消除了 bug。

只是出于好奇,我做了一些测试,即在运行时更改代码以强制执行浮点数到整数的转换。在这种情况下,结果仍然是正确的,编译器通过优化在两个转换中都使用了 __dtoui3:

//; 19   :     printf("Direct cast value: %u\n", (unsigned int)getDouble(d));


movsd   xmm0, QWORD PTR _d$[esp+24]
add esp, 12                 //; 0000000cH
call    __dtoui3
push    eax
push    OFFSET $SG9261
call    _printf


//; 20   :     double db = getDouble(d);
//; 21   :     printf("Indirect cast value: %u\n", (unsigned int)db);


movsd   xmm0, QWORD PTR _d$[esp+20]
add esp, 8
call    __dtoui3
push    eax
push    OFFSET $SG9262
call    _printf

然而,为了防止内联,__declspec(noinline) double getDouble(){...}会把 bug 带回来:

//; 17   :     printf("Direct cast value: %u\n", (unsigned int)getDouble(d));


movsd   xmm0, QWORD PTR _d$[esp+76]
add esp, 4
movsd   QWORD PTR [esp], xmm0
call    _getDouble
call    __ftol2_sse
push    eax
push    OFFSET $SG9261
call    _printf


//; 18   :     double db = getDouble(d);


movsd   xmm0, QWORD PTR _d$[esp+80]
add esp, 8
movsd   QWORD PTR [esp], xmm0
call    _getDouble


//; 19   :     printf("Indirect cast value: %u\n", (unsigned int)db);


call    __ftol2_sse
push    eax
push    OFFSET $SG9262
call    _printf

在两个转换中都调用了 __ftol2_sse,使得输出 2147483648在两种情况下都是正确的。


汇编详情:

  • 使用命令行:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c
  • 在 Visual Studio 中:

    • 禁用 Project -> Properties -> Code Generation中的 RTC并将 基本运行时检查设置为 < strong > default

    • Project -> Properties -> Optimization中启用优化并将 优化设置为

    • 使用 x86模式的调试器。

没有人关注多发性硬化症 __ftol2_sse的高峰。

从结果中,我们可以推断它可能从 x87转换为有符号的 int/long(在 Windows 上都是32位类型) ,而不是安全地转换为 uint32_t

溢出整数结果的整数指令不只是包装/截断: 当目标 高位设置,其他位清晰,即 0x80000000中不能表示精确值时,它们会产生 Intel 所称的“整数不确定”

(或者如果 FP 无效异常没有被屏蔽,它将触发并且不存储任何值。但是在默认的 FP 环境中,所有 FP 异常都被屏蔽了。这就是为什么 FP 计算可以得到 NaN 而不是错误。)

这包括像 fistp这样的 x87指令(使用当前舍入模式)和像 cvttsd2si eax, xmm0这样的 SSE2指令(使用向0截断,这就是额外的 t的意思)。

因此,将 double-> unsigned转换为对 __ftol2_sse的调用是一个 bug。


旁注/切线:

在 x86-64上,FP-> uint32 _ t 可以编译为 cvttsd2si rax, xmm0,转换为64位有符号目标,在整数目标的下半部分(EAX)生成所需的 uint32 _ t。

如果结果在0之外,则为 C 和 C + + UB。. 2 ^ 32-1范围,因此,如果大的正值或负值将使整数不确定位模式的 RAX (EAX)的下半部分为零,则没有问题。(与整数-> 整数转换不同,值的模约简是 没有保证的。是否在 C 标准中定义了将负双精度数转换为无符号整型数的行为?在 ARM 和 x86上的不同行为.需要说明的是,有个问题中没有未定义的行为,甚至没有实现定义的行为。我只是指出,如果有 FP-> int64 _ t,就可以使用它有效地实现 FP-> uint32 _ t。其中包括 x87 fistp,它可以在32位和16位模式下编写64位整数目标,而 SSE2指令只能在64位模式下直接处理64位整数。