GCC内联汇编:“g”约束和参数大小

时间:2018-01-21 01:17:37

标签: c gcc assembly inline-assembly

背景

我知道使用内联汇编解决以下问题是一个坏主意。我目前正在学习内联汇编作为linux内核类的一部分,这是该类赋值的一部分。

设置

开头,下面是一段几乎正确的代码片段,而不是段错误。它是一个函数,它将src的子字符串从索引s_idx开始,并在索引e_idx结束(排他地),仅使用内联汇编复制到预分配的dest。 / p>

static inline char *asm_sub_str(char *dest, char *src, int s_idx, int e_idx) {
  asm("addq %q2, %%rsi;"  /* Add start index to src (ptrs are 64-bit) */
      "subl %k2, %%ecx;"  /* Get length of substr as e - s (int is 32-bit) */
      "cld;"              /* Clear direction bit (force increment) */
      "rep movsb;"        /* Move %ecx bytes of str at %esi into str at %edi */
      : /* No Ouputs */
      : "S" (src), "D" (dest), "g" (s_idx), "c" (e_idx)
      : "cc", "memory"
      );

  return dest;
}

此代码的问题是第二个输入参数的约束。使用gcc的默认优化和-ggdb进行编译时,会生成以下程序集:

Dump of assembler code for function asm_sub_str:
   0x00000000004008e6 <+0>:     push   %rbp
   0x00000000004008e7 <+1>:     mov    %rsp,%rbp
   0x00000000004008ea <+4>:     mov    %rdi,-0x8(%rbp)
   0x00000000004008ee <+8>:     mov    %rsi,-0x10(%rbp)
   0x00000000004008f2 <+12>:    mov    %edx,-0x14(%rbp)
   0x00000000004008f5 <+15>:    mov    %ecx,-0x18(%rbp)
   0x00000000004008f8 <+18>:    mov    -0x10(%rbp),%rax
   0x00000000004008fc <+22>:    mov    -0x8(%rbp),%rdx
   0x0000000000400900 <+26>:    mov    -0x18(%rbp),%ecx
   0x0000000000400903 <+29>:    mov    %rax,%rsi
   0x0000000000400906 <+32>:    mov    %rdx,%rdi
   0x0000000000400909 <+35>:    add    -0x14(%rbp),%rsi
   0x000000000040090d <+39>:    sub    -0x14(%rbp),%ecx
   0x0000000000400910 <+42>:    cld    
   0x0000000000400911 <+43>:    rep movsb %ds:(%rsi),%es:(%rdi)
   0x0000000000400913 <+45>:    mov    -0x8(%rbp),%rax
   0x0000000000400917 <+49>:    pop    %rbp
   0x0000000000400918 <+50>:    retq 

这与第二个输入参数的约束设置为"m"而不是"g"时生成的程序集相同,这使我相信编译器正在有效地选择"m"约束。在使用gdb逐步执行这些指令时,我发现有问题的指令是+35,它将起始偏移量索引s_idx添加到src中的%rsi指针。问题当然是s_idx只有32位,而静态上该位置的64位整数的高4字节不一定是0.在我的机器上,它实际上是非零并导致除了混淆%rsi的高4字节,这导致指令+43中的段错误。

问题

当然,上面的解决方案是将参数2的约束更改为"r",因此它被放置在自己的64位寄存器中,其中前4个字节被正确归零并称之为天。相反,我的问题是,当表达式"g"表示参数"m"的值时,为什么gcc会将"r"约束解析为"%q2"而不是2将用作64位值?

我不太了解gcc如何解析内联汇编,我知道在汇编中没有真正的输入感,但我认为gcc可以识别s_idxlong的有效隐式转换{1}}当它在第一个内联指令中用作64位值时。 FWIW,如果我明确地将"g" (s_idx)更改为"g" ((long) s_idx),则gcc会将"g"约束解析为"r",因为(long) s_idx是临时值。我认为gcc也可以隐含地做到这一点吗?

1 个答案:

答案 0 :(得分:4)

  

但我认为gcc可以识别s_idx在第一个内联指令中用作64位值时long的有效隐式转换。

不,在编译周围的代码时,gcc只会查看约束,而不是asm模板字符串)。填充%模板操作数的gcc部分与周围代码的寄存器分配和代码生成完全分开。

没有任何东西检查是否理智或理解正在使用模板操作数的上下文。也许你有一个16位输入,并希望将其复制到vmovd %k[input], %%xmm0 / vpbroadcastw %%xmm0, %%ymm0的向量寄存器中。高16位被忽略,因此您不希望gcc浪费时间零或为您进行符号扩展。但是你肯定想使用vmovd而不是vpinsrw $0, %[input], %%xmm0,因为那会更多uops并且具有错误的依赖性。对于所有gcc知道或关心,你可以在asm注释行中使用操作数,如"# low word of input = %h2 \n

GNU C inline asm的设计使得约束可以告诉编译器它需要知道的一切。因此,您需要手动将s_idx投射到long

您不需要为ECX转换输入,因为sub指令会隐式地将结果零扩展(到RCX中)。您的输入是签名类型,但可能您希望差异始终是正面的。

必须始终假定寄存器输入具有超出输入类型宽度的高垃圾。这类似于x86-64 System V调用约定中的函数args可以具有{{3}但是(我假设)没有关于扩展到32位的不成文规则。 (请注意,在函数内联之后,你的asm语句的输入可能不是函数args。你不想使用__attribute__((noinline)),正如我所说它无论如何都不会有帮助。 )

  

让我相信编译器正在有效地选择&#34; m&#34;约束

是的,gcc -O0在每个C语句之间将所有内容泄露到内存中(因此如果在断点处停止,您可以使用调试器更改它)。因此,内存操作数是编译器最有效的选择。它需要一个加载指令才能将其恢复到寄存器中。即值{em>在<{1}}语句之前的内存中 ,位于asm

(clang在多选项限制条件下很糟糕,甚至在-O0时也会选择内存,即使这意味着首先出现溢出,但gcc也没有这个问题。)

当输入是数字文字常量时,

-O3(和gcc -O0)将使用立即数作为clang约束。 g。在你的情况下,你得到:

"g" (1234)

... addq $1234, %rsi; subl $1234, %ecx; rep movsb ... 之类的输入即使在"g" ((long)s_idx)也会使用注册,就像-O0或任何其他临时结果一样(只要x+y已经不存在s_idx)。有趣的是,即使long(unsigned)的大小相同,并且强制转换没有指令,即使int也会产生注册操作数。在这一点上,您确切地看到了unsigned的优化程度,因为您得到的更多依赖于gcc内部设计的设计方式,而不是有意义或有效的方式。

启用优化后编译如果您想看到有趣的asm。请参阅can have garbage in the upper 32 bits,特别是关于Matt Godbolt的CppCon2017关于查看编译器输出的讨论的链接。

虽然在没有优化的情况下检查asm对于内联asm来说也是好的;如果它只是寄存器,你可能没有意识到使用gcc -O0覆盖的问题,尽管 仍然是一个问题。检查它在q中如何内联到几个不同的调用者中也很有用(特别是如果你使用一些编译时常量输入进行测试)。

您的代码严重损坏

除了上面讨论的高垃圾问题,你修改输入操作数寄存器而不告诉编译器。

通过使其中一些-O3读/写输出来解决此问题意味着默认情况下您的asm语句不再是"+",因此如果输出未使用,编译器将对其进行优化。 (这包括在函数内联之后,因此volatile对于独立版本是足够的,但是如果调用者忽略返回值则不在内联之后。)

你确实使用了return dest clobber,因此编译器会假设你读/写内存。您可以告诉它您读取和写入的内存,因此它可以更有效地优化您的副本。请参阅How to remove "noise" from GCC/clang assembly output?:您可以使用虚拟内存输入/输出约束,例如"memory"

"m" (*(const char (*)[]) src)

格式化:请注意在每行末尾使用char *asm_sub_str_fancyconstraints(char *dest, char *src, int s_idx, int e_idx) { asm ( "addq %[s_idx], %%rsi; \n\t" /* Add start index to src (ptrs are 64-bit) */ "subl %k[s_idx], %%ecx; \n\t" /* Get length of substr as e - s (int is 32-bit) */ // the calling convention requires DF=0, and inline-asm can safely assume it, too // (it's widely done, including in the Linux kernel) //"cld;" /* Clear direction bit (force increment) */ "rep movsb; \n\t" /* Move %ecx bytes of str at %esi into str at %edi */ : [src]"+&S" (src), [dest]"+D" (dest), [e_idx]"+c" (e_idx) , "=m" (*(char (*)[]) dest) // dummy output: all of dest : [s_idx]"g" ((long long)s_idx) , "m" (*(const char (*)[]) src) // dummy input: tell the compiler we read all of src[0..infinity] : "cc" ); return 0; // asm statement not optimized away, even without volatile, // because of the memory output. // Just like dest++; could optimize away, but *dest = 0; couldn't. } 以提高可读性;否则asm指令全部在一行上,仅由\n\t分隔。 (如果你正在检查你的asm模板是如何计算出来的话,它会很好地组合,但不是很容易阅读。)

将(使用gcc -O3)编译为

;

我放get string length in inline GNU Assembler。一个更简单的版本修复了错误,但仍使用asm_sub_str_fancyconstraints: movslq %edx, %rdx # from the (long long)s_idx xorl %eax, %eax # from the return 0, which I changed to test that it doesn't optimize away addq %rdx, %rsi; subl %edx, %ecx; # your code zero-extends (e_idx - s_idx) rep movsb; ret clobber + "memory"来获得更正确的编译时优化成本,而不是告诉编译器读取和写入哪个内存的版本。

早期clobber :请注意asm volatile约束:

如果由于一些奇怪的原因,编译器知道"+&S"地址和src相等,它可以对两个输入使用相同的寄存器(s_idx)。这会导致在esi/rsi中使用之前修改s_idx。声明保持sub的寄存器早期被破坏(在最后一次读取所有输入寄存器之前)将强制编译器选择不同的寄存器。

请参阅上面的Godbolt链接,了解导致破解但没有src的早期破坏者。 (但只有无意义的&)。早期删除声明通常是多指令asm语句所必需的,以防止更真实的破坏可能性,因此请务必牢记这一点,并且只有当您确定任何只读时才能将其删除输入以与输出或输入/输出操作数共享寄存器。 (当然使用特定寄存器约束限制了这种可能性。)

我在src = (char*)s_idx;中遗漏了e_idx中的早期咒语声明,因为唯一的&#34;免费&#34;参数为ecx,并将它们放在同一个寄存器中将导致s_idxsub same,same根据需要运行0次迭代。

让编译器进行数学运算当然会更有效率,只需要在正确的寄存器中询问rep movsb的输入。特别是如果rep movsbe_idx都是编译时常量,那么迫使编译器s_idx立即转移到寄存器然后减去另一个立即数是愚蠢的。

甚至更好,根本不使用inline asm。 (但如果你真的希望mov测试它的性能,那么内联asm是一种方法.gcc还有调整选项来控制rep movsb内联的方式,如果有的话。)

如果你可以避免使用 this + a couple other versions on the Godbolt compiler explorer with gcc + clang ,那么内联的asm答案是完整的。