为啥包装在函数中的 GAS 内联汇编为调用者生成的指令与纯汇编函数不同

Posted

技术标签:

【中文标题】为啥包装在函数中的 GAS 内联汇编为调用者生成的指令与纯汇编函数不同【英文标题】:Why does GAS inline assembly wrapped in a function generate different instructions for the caller than a pure assembly function为什么包装在函数中的 GAS 内联汇编为调用者生成的指令与纯汇编函数不同 【发布时间】:2016-03-30 16:03:00 【问题描述】:

我一直在使用 GCC 的 asm 编写一些基本函数来练习实际应用程序。

我的函数 prettywrappure 生成相同的指令,将 64 位整数解压缩为 128 位向量。分别调用prettywrapadd1add2 也会生成相同的指令。但是add3 的不同之处在于通过将其推入堆栈而不是将其复制到另一个xmm 寄存器来保存其xmm0 寄存器。这我不明白,因为编译器可以看到pure 的详细信息,知道其他xmm 寄存器不会被破坏。

这里是 C++

#include <immintrin.h>

__m128i pretty(long long b)  return (__m128i)b,b; 

__m128i wrap(long long b) 
    asm ("mov qword ptr [rsp-0x10], rdi\n"
         "vmovddup xmm0, qword ptr [rsp-0x10]\n"
         :
         : "r"(b)
         );


extern "C" __m128i pure(long long b);
asm (".text\n.global pure\n\t.type pure, @function\n"
     "pure:\n\t"
     "mov qword ptr [rsp-0x10], rdi\n\t"
     "vmovddup xmm0, qword ptr [rsp-0x10]\n\t"
     "ret\n\t"
     );

__m128i add1(__m128i in, long long in2)  return in + pretty(in2);
__m128i add2(__m128i in, long long in2)  return in + wrap(in2);
__m128i add3(__m128i in, long long in2)  return in + pure(in2);

g++ -c so.cpp -march=native -masm=intel -O3 -fno-inline编译,用objdump -d -M intel so.o | c++filt反汇编。

so.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <pure>:
   0:   48 89 7c 24 f0          mov    QWORD PTR [rsp-0x10],rdi
   5:   c5 fb 12 44 24 f0       vmovddup xmm0,QWORD PTR [rsp-0x10]
   b:   c3                      ret
   c:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]

0000000000000010 <pretty(long long)>:
  10:   48 89 7c 24 f0          mov    QWORD PTR [rsp-0x10],rdi
  15:   c5 fb 12 44 24 f0       vmovddup xmm0,QWORD PTR [rsp-0x10]
  1b:   c3                      ret
  1c:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]

0000000000000020 <wrap(long long)>:
  20:   48 89 7c 24 f0          mov    QWORD PTR [rsp-0x10],rdi
  25:   c5 fb 12 44 24 f0       vmovddup xmm0,QWORD PTR [rsp-0x10]
  2b:   c3                      ret
  2c:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]

0000000000000030 <add1(long long __vector(2), long long)>:
  30:   c5 f8 28 c8             vmovaps xmm1,xmm0
  34:   48 83 ec 08             sub    rsp,0x8
  38:   e8 00 00 00 00          call   3d <add1(long long __vector(2), long long)+0xd>
  3d:   48 83 c4 08             add    rsp,0x8
  41:   c5 f9 d4 c1             vpaddq xmm0,xmm0,xmm1
  45:   c3                      ret
  46:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  4d:   00 00 00

0000000000000050 <add2(long long __vector(2), long long)>:
  50:   c5 f8 28 c8             vmovaps xmm1,xmm0
  54:   48 83 ec 08             sub    rsp,0x8
  58:   e8 00 00 00 00          call   5d <add2(long long __vector(2), long long)+0xd>
  5d:   48 83 c4 08             add    rsp,0x8
  61:   c5 f9 d4 c1             vpaddq xmm0,xmm0,xmm1
  65:   c3                      ret
  66:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  6d:   00 00 00

0000000000000070 <add3(long long __vector(2), long long)>:
  70:   48 83 ec 18             sub    rsp,0x18
  74:   c5 f8 29 04 24          vmovaps XMMWORD PTR [rsp],xmm0
  79:   e8 00 00 00 00          call   7e <add3(long long __vector(2), long long)+0xe>
  7e:   c5 f9 d4 04 24          vpaddq xmm0,xmm0,XMMWORD PTR [rsp]
  83:   48 83 c4 18             add    rsp,0x18
  87:   c3                      ret

【问题讨论】:

你为什么将 pure 定义为 extern "C" 而不是其他的?我怀疑这是造成差异的原因,因为您已强制编译器遵循“C”调用约定。 这是来自本网站 (cs.uaf.edu/2011/fall/cs301/lecture/10_12_asm_c.html) 的推荐,然后我不必担心名称损坏。 不依赖于immintrin.h如何定义__m128ipretty的正常写法是_mm_set1_epi64x(b)。它编译相同:gcc 选择 store/vmovddup(更差的延迟,少一个 ALU uop),clang 选择 vmovq xmm0, rdi / vpbroadcastq xmm0, xmm0(更好的延迟,Haswell 上的两个 port5 uop) 【参考方案1】:

GCC 不懂汇编语言。

由于pure 是一个外部函数,它无法确定它改变了哪些寄存器,因此根据ABI 必须假设所有xmm 寄存器都已更改。

wrap 具有未定义的行为,因为 asm 语句 clobbers xmm0[rsp-0x10] 未列为 clobbers 或输出(到可能依赖或不依赖于 b 的值),以及函数没有return 声明。

编辑:ABI 不适用于内联汇编,如果您从命令行中删除 -fno-inline,我预计您的程序将无法运行。

【讨论】:

我的印象是xmm0 还可以,但现在我想起来了;但我认为red zone 我认为[rsp-0x10] 落入其中并不能保证函数调用之间的一致性。将xmm0指定为返回寄存器的调用约定不会涵盖缺少返回语句吗?那么wrap 的任何调用者都会去那里检索函数声明所描述的值? 我将xmm0memory 添加到了clobber 列表中(可以解决它吗?),唯一的变化是在vmovddup 之后立即添加了vzeroupper 指令。 @chewsocks:你must not clobber the red zone from inline asm,因为似乎没有办法告诉 gcc 你想这样做。如果你想在内存中为vmovddup 设置一个值,可以这样写:asm ("vmovddup %[result], %[src]" : [result] "=x" (output) : [src] "m" (b) ); return output;。然后 gcc 决定使用什么内存,并且可以直接从堆栈以外的地方加载。如果值已经在内存中,它不会强制 gcc 加载/存储/重新加载。 working example on godbolt. 另外,@chewsocks:要求-masm=intel 构建不是常见的做法。 AT&T 语法还不错。除非这是其他人不需要构建的内部项目,否则最好将其吸收并使用 GNU 内联汇编的标准 AT&T 语法。甚至使用方言替代语法:"vmovddup %[src], %[result] | %[src], %[result]", IIRC。

以上是关于为啥包装在函数中的 GAS 内联汇编为调用者生成的指令与纯汇编函数不同的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 的扩展方法中为调用者变量赋值

为啥这个 C++ 包装类没有被内联?

纯 C++ 代码比内联汇编程序快 10 倍。为啥?

GCC内联汇编中的C数组?

使用内联汇编器从 GCC 中的共享库调用函数

gcc中的arm内联汇编