【问题标题】:segmentation fault(core dumped) error while using inline assembly使用内联汇编时出现分段错误(核心转储)错误
【发布时间】:2020-05-30 21:49:48
【问题描述】:

我在 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);

【问题讨论】:

  • 您在没有告诉编译器的情况下破坏了%rax,并且您错误地假设编译器将为单独的输入和输出约束选择相同的寄存器。查看函数的编译器输出以查看编译器围绕您的代码生成的内容。另请注意,asm("rol $2, %0" : "+r"(X)) 将在一条指令中完成。更重要的是,您可以使用优化为一条循环指令的纯 C 更有效地执行此操作,并且不会阻止常量传播优化:Best practices for circular shift (rotate) operations in C++
  • 不完全是 Why can't local variable be used in GNU C basic inline asm statements? 的副本,但与那里的扩展 asm 有类似的错误。答案有文档链接。更重要的是,解释一下 GNU C inline asm 的设计理念。

标签: c gcc segmentation-fault x86-64 inline-assembly


【解决方案1】:

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

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

    这是文档中的AssemblerTemplate(直到__asm__() 中的第一个:)。

  2. 用编译器确实理解的术语来描述汇编器的作用。

    这是the documentation中的: OutputOperands : InputOperands : Clobbers

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

    实际的汇编器是一个黑匣子,它接受此处描述的输入,产生所描述的输出,并且作为副作用可能会“破坏”一些寄存器和/或内存。这必须是对汇编器所做工作的完整描述...否则编译器围绕您的模板生成的 asm 将与其发生冲突并依赖错误的假设。

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

那么,你的片段:

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

有一些“问题”:

  • 您可能选择了%rax 作为结果,因为 asm() 就像一个函数,并且可能会在 %rax 中返回结果——但事实并非如此。
  • 你继续使用%rax,编译器可能(很好)已经分配给其他东西......所以你实际上是在“破坏”%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 的旧值并返回一个新值。

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

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

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


关于InputOperands有几条规则值得一提:

  • 汇编器不得覆盖任何 InputOperands -- 编译器正在跟踪它在哪些寄存器中具有哪些值,并且期待 InputOperands 被保留。

  • 编译器希望在写入 any OutputOperand 之前读取所有 InputOperands。当编译器知道给定的 InputOperandasm() 之后不再使用时,这一点很重要,因此它可以将 InputOperand 的寄存器分配给 输出操作数。有一种叫做earlyclobber (=&r(foo)) 的东西来处理这个小皱纹。

在上面,如果您实际上没有再次使用X,编译器可以将%0%1 分配到同一个寄存器!但是(冗余的)mov 仍然会被汇编——记住编译器真的不理解 AssemblerTemplate。所以,一般来说,你最好在 C 中改组值,而不是 asm()。见https://gcc.gnu.org/wiki/DontUseInlineAsmBest 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    $0x492782,%edi   # address of format string
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%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    $0x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%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    $0x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%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    $0x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%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) ;
}

希望它展示了编译器忙于向寄存器分配值、跟踪它需要保留哪些值、最小化寄存器/寄存器移动,并且通常很聪明。

所以诀窍是使用编译器,了解 :OutputOperands:InputOperands:Clobbers 是你在哪里描述汇编器在做什么。

【讨论】:

  • 否则编译器会变得混乱! 编译器不会“混乱”,它完全不知道模板的作用(包括遗漏)是否存在约束.我喜欢将其描述为在您的 asm 和编译器生成的 asm 之间的微妙舞蹈中“踩到编译器的脚趾”。
  • 另外,可悲的是, 这不是“可悲”的事情;它允许您告诉编译器何时可以将 asm 语句用作复制和修改。 (例如,如果 X 碰巧事先与另一个值共享一个寄存器,例如在 long X = y; 之后,即使稍后需要 y,编译器也不需要在您的 asm 语句之外发出 mov。)这是一个这段代码做出了错误的假设,但我不会把它描述为悲伤。
  • 顺便说一句,如果您指出自 186 年以来的 CPU(包括所有 x86-64)允许 ROL 立即操作数,则此答案会更好,因此使用两个 1 位 rol 是零点指示。 (隐式 1 ROL 与即时 ROL (felixcloutier.com/x86/rcl:rcr:rol:ror) 之间的 FLAG 设置略有不同,但没有 setcc 或 GCC6 条件代码输出约束来观察这一点。)
  • RORX 并不比ROR r64, imm8 快;两者都是端口 0 或 6 的单 uop,在 Haswell 及更高版本上具有 1 个周期延迟。 (uops.info/table.html)。唯一的优点是非破坏性的,因此您(或编译器)可以避免mov。 (好吧,不修改 FLAGS 可以让编译器围绕 cmov 或 setcc 排列代码)。 ror by implicit 1(这里使用的形式)在 Sandybridge-family 上是 2 uops,因为它必须设置 OF,同时不影响 SPAZO 重命名组中的其他标志。但当然汇编程序“优化”rol $1, %rax 为隐式 1 形式:/
  • 顺便说一句,我们可以看出英特尔的文档有一个微妙的错误:立即形式的掩码计数 1 仍然不会更新 OF,除非它在这种情况下以不同的方式解码。我认为它适用于 CL 形式,即 3 微秒。 uops.info 的即时计数轮换测试均使用 imm8 = 0 或 2,例如uops.info/html-tp/SKL/ROR_R32_I8-Measurements.html。顺便说一句,Zen 将所有旋转都作为单个 uop 运行(带有内存源的 RORX 除外),所以是的,只有英特尔的旋转速度很慢。
猜你喜欢
  • 1970-01-01
  • 2019-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多