MSYS2 GCC 在禁用 SSE 的情况下对浮点运算进行清零

MSYS2 GCC zeros out doubles on floating point operations with SSE disabled

考虑下面的 C 程序。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
    double x = 4.5;
    double x2 = atof("3.5");
    printf("%.6f\n", x);
    printf("%.6f\n", x2);
    return 0;
}

使用 MSYS2 可用的 GCC 版本编译时,输出最终取决于 SSE 的可用性:

$ gcc test.c && ./a.exe
4.500000
3.500000

$ gcc -mno-sse test.c && ./a.exe
4.500000
0.000000

这种行为是否有意义,如果没有,在这种情况下是否有任何方法可以让 GCC 产生合理的结果(除了删除 -mno-sse 的简单解决方案之外)?这是一些版本信息:

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-msys/7.3.0/lto-wrapper.exe
Target: x86_64-pc-msys
Configured with: /msys_scripts/gcc/src/gcc-7.3.0/configure --build=x86_64-pc-msys --prefix=/usr --libexecdir=/usr/lib --
enable-bootstrap --enable-shared --enable-shared-libgcc --enable-static --enable-version-specific-runtime-libs --with-ar
ch=x86-64 --with-tune=generic --disable-multilib --enable-__cxa_atexit --with-dwarf2 --enable-languages=c,c++,fortran,lt
o --enable-graphite --enable-threads=posix --enable-libatomic --enable-libcilkrts --enable-libgomp --enable-libitm --ena
ble-libquadmath --enable-libquadmath-support --disable-libssp --disable-win32-registry --disable-symvers --with-gnu-ld -
-with-gnu-as --disable-isl-version-check --enable-checking=release --without-libiconv-prefix --without-libintl-prefix --
with-system-zlib --enable-linker-build-id --with-default-libstdcxx-abi=gcc4-compatible
Thread model: posix
gcc version 7.3.0 (GCC)

下面是反汇编的结果 main:

   0x0000000100401080 <+0>:     push   %rbp
   0x0000000100401081 <+1>:     mov    %rsp,%rbp
   0x0000000100401084 <+4>:     sub    [=13=]x30,%rsp
   0x0000000100401088 <+8>:     mov    %ecx,0x10(%rbp)
   0x000000010040108b <+11>:    mov    %rdx,0x18(%rbp)
   0x000000010040108f <+15>:    callq  0x1004010f0 <__main>
   0x0000000100401094 <+20>:    fldl   0x1f76(%rip)        # 0x100403010
   0x000000010040109a <+26>:    fstpl  -0x8(%rbp)
   0x000000010040109d <+29>:    lea    0x1f5c(%rip),%rcx        # 0x100403000
   0x00000001004010a4 <+36>:    callq  0x100401100 <atof>
   0x00000001004010a9 <+41>:    mov    %rax,-0x10(%rbp)
   0x00000001004010ad <+45>:    mov    -0x8(%rbp),%rax
   0x00000001004010b1 <+49>:    mov    %rax,%rdx
   0x00000001004010b4 <+52>:    lea    0x1f49(%rip),%rcx        # 0x100403004
   0x00000001004010bb <+59>:    callq  0x100401110 <printf>
   0x00000001004010c0 <+64>:    mov    -0x10(%rbp),%rax
   0x00000001004010c4 <+68>:    mov    %rax,%rdx
   0x00000001004010c7 <+71>:    lea    0x1f36(%rip),%rcx        # 0x100403004
   0x00000001004010ce <+78>:    callq  0x100401110 <printf>
   0x00000001004010d3 <+83>:    mov    [=13=]x0,%eax
   0x00000001004010d8 <+88>:    add    [=13=]x30,%rsp
   0x00000001004010dc <+92>:    pop    %rbp
   0x00000001004010dd <+93>:    retq
   0x00000001004010de <+94>:    nop
   0x00000001004010df <+95>:    nop

值得注意的是,尝试在 Linux 版本的 GCC 上编译同一程序会产生错误(原因在 this question 中讨论):

$ gcc -mno-sse test2.c
test2.c: In function ‘main’:
test2.c:6:12: error: SSE register return with SSE disabled
     double x2 = atof("3.5");
            ^~

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/6/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 6.3.0-18+deb9u1' --with-bugurl=file:///usr/share/doc/gcc-
6/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-6 --program-pr
efix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enabl
e-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-l
ibstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --
enable-plugin --enable-default-pie --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo
--with-java-home=/usr/lib/jvm/java-1.5.0-gcj-6-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-
gcj-6-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-6-amd64 --with-arch-directory=amd64 --with-ecj-jar=/u
sr/share/java/eclipse-ecj.jar --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --with-arch-32=i686 --w
ith-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x8
6_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)

您应该从 msys gcc -mno-sse 得到同样的错误。标准调用约定(x64Windows__fastcall)使用xmm0..3(SSE向量寄存器)传递和returnfloatdouble.

从您展示的 asm main 看来,-mno-sse 将整数寄存器中的 gcc 调用约定的想法更改为 pass/return double,例如 soft-float在 ARM 上。所以调用约定不匹配,实际发生的情况取决于 asm 细节和机会。

Windows x64 调用约定有一个有趣的设计特性,使得实现像 printf 这样的可变参数函数更简单:当 调用可变参数函数时,整数和 XMM 寄存器都用于该插槽必须包含值https://docs.microsoft.com/en-gb/cpp/build/varargs?view=vs-2017). Thus the function can dump rcx,rdx,r8, and r9 into the shadow space and form an array of 8-byte args (contiguous with the stack args), before looking at args to figure out which ones are FP and which are integer. (See How to set function arguments in assembly during runtime in a 64bit application on Windows? 是一个丑陋的例子。)与 x86-64 System V ABI 不同,第二个 arg 整体进入 XMM1,而不是第二个 FP 参数。所以 regs 中总共只能有 4 个 args,即使混合了 FP 和整数。

因此,gcc 在 %rdx 中传递 double 位模式实际上有效,因为这个库 printf 只关心%rdx 中的值,忽略 %xmm1 中的值。

但是 atof returns 在 XMM0 中,RAX 持有垃圾。您的 -mno-sse main 使用保存 RAX 并将其传递给第二个 printf。它要么为零,要么非常小 double.

如果 RAX 持有一个地址,高 16 位将为零,因此对该位模式进行类型双关到 IEEE double (https://en.wikipedia.org/wiki/Double-precision_floating-point_format) 给我们指数 = 0,以及有效数的一些位。一个小的正整数会更小 double.

所以你可能打印了一个非常小的次正规 double,它以那种格式四舍五入到 0,它来自 RAX 中留下的任何垃圾 atof return在 XMM0 中编辑了一个值。