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向量寄存器)传递和returnfloat
和double
.
从您展示的 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 中编辑了一个值。
考虑下面的 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向量寄存器)传递和returnfloat
和double
.
从您展示的 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 中编辑了一个值。