使用内联汇编时出现分段错误(核心转储)错误

segmentation fault(core dumped) error while using inline assembly

我在 GCC 中使用内联汇编。我想将一个变量内容向左旋转2位(我把变量移到rax寄存器然后旋转2次)。我写了下面的代码,但我遇到了分段错误(核心转储)错误。 如果你能帮助我,我将不胜感激。

uint64_t X = 13835058055282163712U;
 asm volatile(
            "movq %0 , %%rax\n"
            "rol %%rax\n"
            "rol %%rax\n"
            :"=r"(X)
            :"r"(X)
         );
printf("%" PRIu64 "\n" , X);

理解内联 asm 的关键是理解每个 asm 语句有两部分:

  1. 实际汇编程序的文本,编译器将在其中进行文本替换,但不理解

    这是文档中的 AssemblerTemplate__asm__() 中第一个 : 之前的所有内容)。

  2. 关于汇编程序的描述,编译器理解

    : OutputOperands : InputOperands : Clobbers the documentation.

    这必须告诉编译器汇编器如何适应编译器围绕它生成的所有代码。代码生成忙于分配寄存器来保存值、决定以何种顺序执行操作、将操作移出循环、消除未使用的代码片段、丢弃不再需要的值,等等。

    实际的汇编器是一个黑匣子,它接受这里描述的输入,产生描述的输出,作为副作用可能 'clobber' 一些寄存器 and/or 内存。 必须 是对汇编程序所做工作的完整描述...否则编译器围绕您的模板生成的 asm 将与其发生冲突并依赖于错误的假设。

    有了这些信息,编译器 可以决定汇编程序可以使用哪些寄存器,您应该让它这样做。

那么,您的片段:

 asm volatile(
            "movq %0 , %%rax\n"
            "rol %%rax\n"
            "rol %%rax\n"
            :"=r"(X)
            :"r"(X)
         );

有几个"issues":

  • 您可能已经选择了 %rax 作为结果,因为 asm() 就像一个函数,并且可能期望 return 在 %rax 中得到结果 --但事实并非如此。
  • 你继续使用 %rax,编译器可能(很好)已经分配给其他东西......所以你实际上是 'clobbering' %rax 但你未能将其告知编译器!
  • 您指定了 =r(X) (OutputOperand),它告诉编译器期望某个寄存器中的输出,并且该输出将是变量的新值 X. AssemblerTemplate 中的 %0 将替换为为输出选择的寄存器。可悲的是,您的程序集将 %0 作为输入 :-( 实际上,输出在 %rax 中——如上所述,编译器并不知道。
  • 您还指定了 r(X) (InputOperand),它告诉编译器将变量 X 的当前值安排为放在一些寄存器中供汇编器使用。这将是 AssemblerTemplate 中的 %1。遗憾的是,您的程序集不使用此输入。

    即使输出和输入操作数都引用 X,编译器可能不会使 %0%1 相同的寄存器。 (这允许它使用 asm 块作为非破坏性操作,使输入的原始值保持不变。如果这不是您的模板的工作方式,请不要那样写。

  • 当所有输入和输出都由约束正确描述时,通常您不需要 volatile。如果(所有)输出未被使用,编译器将做的一件好事是丢弃 asm() ... volatile 告诉编译器不要这样做(并告诉它一个其他一些事情......请参阅手册)。

除此之外,一切都很棒。以下是安全的,并且避免了 mov 指令:

 asm("rol %0\n"
     "rol %0\n"   : "+r"(X));

其中 "+r"(X) 表示需要一个组合的输入和输出寄存器,采用 X 的旧值和 returning 一个新值。

现在,如果您不想替换 X,那么假设 Y 是结果,您可以:

 asm("mov %1, %0\n"
     "rol %0\n"
     "rol %0\n"   : "=r"(Y) : "r"(X));

但最好让编译器来决定它是否需要 mov 或者它是否可以让输入被销毁。


有一些关于 InputOperands 的规则值得一提:

  • 汇编器不得覆盖任何InputOperands——编译器正在跟踪它在注册,并期望 InputOperands 被保留。

  • 编译器期望在 any OutputOperand[=138= 之前读取所有 InputOperands ] 写的。当编译器知道给定的 InputOperandasm() 之后不再使用时,这一点很重要,因此它可以分配 InputOperand的注册到 OutputOperand。有一种叫做 earlyclobber (=&r(foo)) 的东西可以解决这个小问题。

在上面,如果您实际上没有再次使用 X,编译器可能会将 %0%1 分配给同一个寄存器!但是(冗余的)mov 仍然会被汇编——记住编译器真的不理解 AssemblerTemplate。所以,你通常最好在 C 中改组值,而不是 asm()。参见 https://gcc.gnu.org/wiki/DontUseInlineAsm and Best practices for circular shift (rotate) operations in C++


所以这是一个主题的四个变体,以及生成的代码 (gcc -O2):

// (1) uses both X and Y in the printf() -- does mov %1, %0 in asm()
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    [=13=]x492782,%edi   # address of format string
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    [=13=]x63,%esi       # X = 99
  X = 99 ;                                     rol    %rsi            # 1st asm
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             mov    %rsi,%rdx       # 2nd asm, compiler using it as a copy-and-rotate
      ) ;                                      rol    %rdx
                                               rol    %rdx
  __asm__("\t mov  %1, %0\n"                   jmpq   0x4010a0 <printf@plt>  # tailcall printf
          "\t rol  %0\n"
          "\t rol  %0\n" : "=r"(Y) : "r"(X)
      ) ;

  printf("%lx %lx\n", X, Y) ;
}

// (2) uses both X and Y in the printf() -- does Y = X in 'C'
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    [=13=]x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    [=13=]x63,%esi
  X = 99 ;                                     rol    %rsi       # 1st asm
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             mov    %rsi,%rdx  # compiler-generated mov
      ) ;                                      rol    %rdx       # 2nd asm
                                               rol    %rdx
  Y = X ;                                      jmpq   0x4010a0 <printf@plt>
  __asm__("\t rol  %0\n"
          "\t rol  %0\n" : "+r"(Y)
      ) ;

  printf("%lx %lx\n", X, Y) ;
}

// (3) uses only Y in the printf() -- does mov %1, %0 in asm()
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    [=13=]x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    [=13=]x63,%esi
  X = 99 ;                                     rol    %rsi
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             mov    %rsi,%rsi   # redundant instruction because of mov in the asm template
      ) ;                                      rol    %rsi
                                               rol    %rsi
  __asm__("\t mov  %1, %0\n"                   jmpq   0x4010a0 <printf@plt>
          "\t rol  %0\n"
          "\t rol  %0\n" : "=r"(Y) : "r"(X)
      ) ;

  printf("%lx\n", Y) ;
}

// (4) uses only Y in the printf() -- does Y = X in 'C'
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    [=13=]x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    [=13=]x63,%esi
  X = 99 ;                                     rol    %rsi
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             rol    %rsi    # no wasted mov, compiler picked %0=%1=%rsi
      ) ;                                      rol    %rsi
                                               jmpq   0x4010a0 <printf@plt>
  Y = X ;
  __asm__("\t rol  %0\n"
          "\t rol  %0\n" : "+r"(Y)
      ) ;

  printf("%lx\n", Y) ;
}

这有望证明编译器忙于将值分配给寄存器,跟踪它需要保留哪些值,最大限度地减少 register/register 移动,并且通常很聪明。

所以诀窍是与编译器一起工作,理解 :OutputOperands:InputOperands :Clobbers 是您描述汇编器正在做什么的地方。