如何使用扩展的gcc程序集指定x87 FPU堆栈的破坏底部?

时间:2016-09-27 15:18:42

标签: c gcc assembly x86 x87

在我们的代码库中,我发现这个片段在x87上快速,向负 - 无限 1 舍入:

inline int my_int(double x)
{
  int r;
#ifdef _GCC_
  asm ("fldl %1\n"
       "fistpl %0\n"
       :"=m"(r)
       :"m"(x));
#else
  // ...
#endif
  return r;
}

我不是非常熟悉GCC扩展汇编语法,而是从我从文档中收集到的内容:

  • r必须是一个记忆位置,我写回来的东西;
  • x也必须是内存位置,数据来自哪里。
  • 没有clobber规范,因此编译器可以放心,在代码片段的末尾,寄存器就像他离开时一样。

现在,回答我的问题:最终FPU堆栈是平衡的,但是如果所有8个位置都已经在使用并且我已经溢出了怎么办?编译器如何知道它不能信任ST(7)它离开它的位置?应该添加一些咒语吗?

编辑 我试图在clobber列表中指定st(7),它似乎影响了codegen,现在我等待一些确认这个事实。

作为旁注:在glibc和MinGW中查看准系统lrint的实现,我看到类似

的内容
__asm__ __volatile__ ("fistpl %0"
                      : "=m" (retval)
                      : "t" (x)
                      : "st");

我们要求将输入直接放在ST(0)中(这可以避免那些可能无用的fldl);什么是"st" clobber?文档似乎只提到t(即堆栈的顶部)。

  1. 是的,它取决于当前的舍入模式,在我们的应用程序中应始终为"朝向负无穷大"。

1 个答案:

答案 0 :(得分:4)

  

在glibc和MinGW中查看准系统lrint的实现我看到类似

的内容
__asm__ __volatile__ ("fistpl %0"
                     : "=m" (retval)
                     : "t" (x)
                     : "st");
     

我们要求将输入直接放在ST(0)中(这可以避免那些可能无用的fldl

这实际上是将所需代码表示为内联汇编的正确方法。

要获得最佳的可能代码,您需要使用输入和输出。不要硬编码必要的加载/存储指令,而是让编译器生成它们。这不仅会引入消除可能不必要的指令的可能性,而且还意味着编译器可以在需要时更好地调度这些指令(也就是说,它可以在先前的代码序列中交错指令) ,通常会降低成本)。

  那是什么"st" clobber?文档似乎只提到t(即堆栈的顶部)。

"st" clobber是指st(0)寄存器,,x87 FPU堆栈的顶部。英特尔/ MASM表示法称为st(0),AT& T / GAS表示法通常仅指st。而且,根据GCC的clobbers文档,clobber列表中的项目是"注册名称或特殊符号" ("cc"(条件代码/标志)和"memory")。所以这只是意味着内联汇编(覆盖)st(0)寄存器。这个clobber必要的原因是fistpl指令弹出堆栈的顶部,从而破坏了st(0)的原始内容。

关于此代码,我唯一关心的是文档中的以下段落:

  

Clobber描述可能不会以任何方式与输入或输出操作数重叠。例如,在clobber列表中列出该寄存器时,您可能没有描述具有一个成员的寄存器类的操作数。声明存在于特定寄存器中的变量(请参阅Explicit Register Variables)并用作asm输入或输出操作数,必须在clobber描述中未提及任何部分。特别是,没有办法指定输入操作数被修改而不将它们指定为输出操作数。

     

当编译器选择用于表示输入和输出操作数的寄存器时,它不使用任何被破坏的寄存器。因此,破坏寄存器可用于汇编代码中的任何用途。

如您所知,t constraint表示x87 FPU堆栈的顶部。问题是,这与st寄存器相同,文档非常明确地说我们没有一个clobber指定与输入/输出操作数之一相同的寄存器。此外,由于文档声明编译器禁止使用任何被破坏的寄存器来表示输入/输出操作数,因此这个内联汇编使得一个不可能的请求 - 将该值加载到x87 FPU堆栈的顶部而不将其放入{{ 1}}!

现在,我假设glibc的作者知道他们在做什么,并且比你或我更熟悉编译器内联汇编的实现,所以这段代码可能是合法合法。

实际上,似乎x87的类似堆栈的寄存器的异常情况迫使clobbers和操作数之间的正常交互例外。 official documentation说:

  

在x86目标上,有一些规则在asm的操作数中使用类似堆栈的寄存器。这些规则仅适用于类似堆栈的寄存器:

     
      
  1. 给定一组输入寄存器死在asm中,有必要知道哪些是由asm隐式弹出的,哪些必须由GCC显式弹出。

         

    由asm隐式弹出的输入寄存器必须明确地被破坏,除非它被约束为匹配输出操作数。

  2.   

完全适合我们的情况。

the official documentation中显示的示例(链接部分的底部)提供了进一步的确认:

  

这个asm接受两个输入,由st操作码弹出,并用一个输出替换它们。 fyl2xp1 clobber是编译器必须知道st(1)弹出两个输入所必需的。

fyl2xp1

此处,clobber asm ("fyl2xp1" : "=t" (result) : "0" (x), "u" (y) : "st(1)"); 与输入约束st(1)相同,这似乎违反了上述有关clobbers的文档,但使用和证明的原因与{{{{{{{ 1}}用作原始代码中的clobber,因为u会弹出输入。

所有这些都说,现在您知道如何在内联汇编中正确编写代码,我必须回应以前的评论者,他们建议最佳解决方案不要使用内联汇编所有。只需调用"st",它不仅具有您想要的确切语义,而且在某些情况下也可以由编译器更好地优化(例如,将其转换为单个fistpl当目标体系结构支持SSE时的指令。)