Gcc 混合内联汇编与C代码-如何保护寄存器和最小化内存访问

Gcc 混合内联汇编与C代码-如何保护寄存器和最小化内存访问,gcc,x86-64,inline-assembly,gnu-assembler,Gcc,X86 64,Inline Assembly,Gnu Assembler,我有一个我想在汇编中编写的例程,但我需要调用C函数来获取一些我需要处理的数据。在某些情况下,我可以预消化数据并加载一个带有指针的寄存器,但在其他情况下,我必须调用完整函数,因为可能的数据集太大。无法修改这些函数,因为它们是其他人的代码,并且对于其他代码段,其接口需要保持不变。其中一些驻留在共享库中,尽管有些是通过头文件(我无法更改)实现的内联函数 我可以使用asm构造将局部变量分配给寄存器: register int myReg asm( "%r13" ); 我担心如果我直接在汇编中操作%r1

我有一个我想在汇编中编写的例程,但我需要调用C函数来获取一些我需要处理的数据。在某些情况下,我可以预消化数据并加载一个带有指针的寄存器,但在其他情况下,我必须调用完整函数,因为可能的数据集太大。无法修改这些函数,因为它们是其他人的代码,并且对于其他代码段,其接口需要保持不变。其中一些驻留在共享库中,尽管有些是通过头文件(我无法更改)实现的内联函数

我可以使用asm构造将局部变量分配给寄存器:

register int myReg asm( "%r13" );
我担心如果我直接在汇编中操作%r13,调用一个C函数,然后返回,它将需要从内存中刷新,或者更糟糕的是完全被覆盖。对于某些ABI,我自己直接按下/弹出寄存器也不安全,对吗?我在Linux上使用x86-64

我现在所做的似乎是使用-g-O0,但我担心当我打开C代码的优化时,它会开始触及我希望得到保护的寄存器

通常,我的代码流如下所示:

asm( "movq %[src], %%r13" : : [src] "m" (myVariablePointer) : "r13" );

localVariable1 = callSomeCfunction( stuff );

storageLocation = index * sizeof( longTermStorageItem );
longTermStorage[storageLocation] = localVariable1;
// some intermediate registers need to be used here for dereferences and math

switch ( localVariable1 )
{
   case CONSTANT_VAL_A:
     C_MACRO_LOOP_CONSTRUCT
     {
       asm( "movdqa (%r13), %xmm0\n"
            // ... do some more stuff
     } C_MACRO_LOOP_CONSTRUCT_ENDING
   break;
   case CONSTANT_VAL_B:
     // ... and so forth
}
“C#u MACRO_LOOP#CONSTRUCT”是从外部头文件中定义的,其中包含“for”循环,需要在过程中取消引用某些指针等,并将迭代器存储在局部变量中

因此,我关心的是如何确保在所有这些内容中保留%r13。到目前为止,编译器还没有触及它,但我相信这更多的是运气,而不是设计。保存价值本身并不是我唯一关心的。如果可能的话,我想把它留在我放的登记册里。将其移出到本地/堆栈存储并频繁返回会降低我的性能

有没有一种方法可以更好地保护编译器/优化器中的一小部分寄存器

其他信息

这就是为什么我要这么做。请看下面的代码:

#include <emmintrin.h>
#include <stdio.h>

__m128d buffer[100];   

int main( void )
{
  unsigned long long *valPtr;

  register __m128d val;
  register __m128d *regPtr;
#ifdef FORCED  
  asm( "movq %[src], %%r13" :
       :
       [src] "r"  (buffer) );
  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], (%%r13)" : :
       [src] "x" (val) );
  asm( "movdqa %[src], 16(%%r13)" : :
       [src] "x" (val) );   
#else
  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer+1) :
       [src] "x" (val) );
#endif

  valPtr = (unsigned long long *)buffer;
  printf( "OUTPUT: [0] %016llx%016llx, [1] %016llx%016llx\n",
   valPtr[0], valPtr[1], valPtr[2], valPtr[3] );

  return 0;
}
所以我想我的问题应该是,我可以使用带有适当约束的索引寻址模式吗?我试过“m”、“X”和“o”。没有区别。如果我尝试将偏移量拉入部件并拉出参数,如下所示:

asm( "movdqa %[src], 16(%[dst])" :
 [dst] "=m" (buffer) :
 [src] "x" (val) );
__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm("# val: %0" : "=x" (val)); /* fix "is used uninitialized" warning */

  asm( "pcmpeqd %[sval], %[dval]\n\t"
       "movdqa %[dval], %[buffer]\n\t"
       "movdqa %[dval], %[buffer1]" :

       [dval] "=x" (val), [buffer] "=m" (buffer[0]), [buffer1] "=m" (buffer[1]) :
       [sval] "x" (val) );
}
GCC的回应是:

/tmp/ccoNwyco.s: Assembler messages:
/tmp/ccoNwyco.s:28: Error: junk `(buffer(%rip))' after expression

你知道如何使用这种寻址模式并消除不必要的指令吗?

既然你问了关于附加部分的问题,我将重点讨论这个问题。查看您的第一个#if块:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm( "movq %[src], %%r13" :
       :
       [src] "r"  (buffer) );
  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], (%%r13)" : :
       [src] "x" (val) );
  asm( "movdqa %[src], 16(%%r13)" : :
       [src] "x" (val) );   
}
此片段写入r13,而不告诉编译器。那太糟糕了。即使在调用此asm之前在某些局部变量上有一个asm(“r13”),这也会很糟糕。您仍然需要将该局部变量列为输出,然后列为后续ASM的输入。更重要的是,它既让维护人员感到困惑,也没有必要

还有,像这样有多个asm语句是个坏主意。gcc可能不会选择按此顺序保存它们。在这种情况下,我建议采取类似的措施:

asm( "movdqa %[src], 16(%[dst])" :
 [dst] "=m" (buffer) :
 [src] "x" (val) );
__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm("# val: %0" : "=x" (val)); /* fix "is used uninitialized" warning */

  asm( "pcmpeqd %[sval], %[dval]\n\t"
       "movdqa %[dval], %[buffer]\n\t"
       "movdqa %[dval], %[buffer1]" :

       [dval] "=x" (val), [buffer] "=m" (buffer[0]), [buffer1] "=m" (buffer[1]) :
       [sval] "x" (val) );
}
至于你的#else块:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm( "pcmpeqd %[src], %[dst]" :
       [dst] "=x" (val) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer) :
       [src] "x" (val) );
  asm( "movdqa %[src], %[dst]" :
       [dst] "=X" (buffer+1) :
       [src] "x" (val) );
}
我建议:

__m128d buffer[100];   

int main( void )
{
  register __m128d val;

  asm("# val: %0" : "=x" (val)); /* fix "is used uninitialized" warning */

  asm( "pcmpeqd %[sval], %[dval]\n\t"
       "movdqa %[dval], (%[sbuffer])\n\t"
       "movdqa %[dval], 16(%[sbuffer])" :

       [dval] "=x" (val), [buffer] "=m"  (buffer), [buffer1] "=m" (buffer[1]) :
       [sval] "x" (val), [sbuffer] "r"  (buffer));
}
这里有几件事需要注意

  • 我正在使用第一个asm语句来解决编译器在分配val之前发出的有关使用val的警告。这是由于使用val作为输入而导致的,而val从未分配过值。在实际代码中,您可能会在使用它之前指定一个合理的值
  • 通过将3条asm语句放在一个asm块中,gcc无法移动单个片段
  • 为什么我有sbuffer、buffer和buffer1,但从不引用buffer和buffer1?sbuffer用于将指向数组的指针放入寄存器。“buffer”和“buffer1”列为输出,因为我必须告诉gcc我正在更改它们。使用“内存”缓冲更容易,但这可能会对性能产生严重影响。或者,我可以使用某种形式的(来自gcc文档重新扩展的asm):
  • {“m”({struct{charx[10];}*p=(void*)ptr;*p;}))}

    这告诉gcc将访问从ptr开始的10个字符。难看,但如果您在编译时知道要修改多少字节的内存,它就可以工作。重点是,如果要更改asm中的任何值(甚至是数组中的条目),必须让gcc知道

    还有什么?啊,是的,让我们看看asm(来自-Os):

    据我所知,您尝试使用r13的全部原因是为了避免在调用一些不受控制的子例程时寄存器被破坏,从而浪费了在每个循环中重新加载寄存器的周期。所以让这段代码使用rax,这似乎不是个好主意,对吧?但是等等!观察此代码会发生什么:

    __m128d buffer[100];   
    
    int main( void )
    {
      register __m128d val;
    
      for (int x=0; x < 10; x++)
      {
        asm("# val: %0" : "=x" (val)); /* fix "is used uninitialized" */
    
        asm( "pcmpeqd %[src], %[dst]\n\t"
             "movdqa %[src], (%[sbuffer])\n\t" /* buffer[0] */
             "movdqa %[src], 16(%[sbuffer])" : /* buffer[1] */
    
             [dst] "=x" (val), [buffer] "=m"  (buffer), [buffer1] "=m" (buffer[1]) :
             [src] "x" (val), [sbuffer] "r"  (buffer));
    
         printf("%d\n", val);
       }
    }
    
    嗯,它已经从rax变为rbx。这样更好吗?嗯,事实上是这样。在c中调用子例程时,编译器必须遵循一些规则(ABI)。这些规则控制诸如参数传递的位置、返回值的位置、谁清理堆栈,以及(对于我们这里的目的来说,最重要的是)子例程必须保留哪些寄存器(即返回时必须具有相同的值)。这里有一些关于这方面的讨论和有用的链接。值得注意的是,必须保留rbx(对于x86-64)

    因此,如果您查看围绕此代码的asm,您会注意到rbx只加载一次(在循环之外)。Gcc知道,如果任何子例程弄脏了rbx,他们会在完成后将其放回原处。此外,由于子例程知道必须保留rbx,因此它们倾向于避免使用它,除非多使用一个寄存器的好处大于保存/恢复它的成本

    至于“保留”寄存器并阻止任何子例程使用它的整个想法,我不会说这是不可能的(参见和),但我会说通常这是一个糟糕的想法。x86上的寄存器是非常有用且非常有限的资源。特里伊
    .L2:
        leaq    .LC0(%rip), %rcx
        movq    %rdi, %rdx
        pcmpeqd %xmm6, %xmm0
        movdqa %xmm6, (%rbx)
        movdqa %xmm6, 16(%rbx)
        movapd  %xmm0, 32(%rsp)
        call    printf
        subl    $1, %esi
        jne .L2