【问题标题】:In assembly, how to add integers without destroying either operand?在汇编中,如何在不破坏任一操作数的情况下添加整数?
【发布时间】:2021-12-27 21:22:02
【问题描述】:

在 x86-64 上使用 AT&T 语法,我希望将 c = a + b; 组装为

add %[a], %[b], %[c]

不幸的是,GNU 的汇编器不会这样做。为什么不呢?

详情

根据英特尔的软件开发人员手册, rev. 75(2021 年 6 月),卷。 2,第 2.5 节,

VEX 编码的通用寄存器指令具有...对三个可编码操作数的指令语法支持。

VEX 前缀是 AVX 功能,因此从 Sandy Bridge/Bulldozer 开始的 x86-64 CPU 实现了它。那是十年前的事了,所以 GNU 的汇编器应该汇编我的三操作数指令,不是吗?

为了澄清,我知道可以用旧样式写成

mov %[a], %[c]
add %[b], %[c]

但是,我希望以新的 VEX 风格编写它。顺便说一句,我通过向 GCC 发出 -march=skylake 命令行选项来通知汇编器我有一个现代 CPU。

请问我的错误是什么?

示例代码

在 C++ 包装器中,

#include <cstddef>
#include <iostream>

int main()
{
    volatile int a{8};
    volatile int b{5};
    volatile int c{0};
    //c = a + b;
    asm volatile (
        //"mov %[a], %[c]\n\t"
        //"add %[b], %[c]\n\t"
        "add %[a], %[b], %[c]\n\t"
        : [c] "=&r" (c)
        : [a] "r" (a), [b] "r" (b)
        : "cc"
    );
    std::cout << c << "\n";
}

【问题讨论】:

  • 回答您的问题,您是否尝试过使用 lea 指令?
  • @fuz 不。这是一个很好的观点,因为它解释了为什么英特尔和 AMD 不会费心为add 指令实现 VEX。有人怀疑未来的访客会发现您的观察很有帮助;那么,当您有时间时,您愿意将其添加为正确的答案吗?
  • 我认为您在这里有某种误解。 VEX 前缀不适用于 AVX/AVX2 之外的现有指令。所有带有 VEX 前缀的标量指令都是全新的,带有新的操作码。通常的添加指令(操作码 0003)甚至不能进行 VEX 编码,因为 VEX 编码具有这些指令所缺少的隐式 0f0f 380f 3a 前缀。您不能只接受任何随机指令并对其应用 VEX 前缀。这不是它的工作原理。

标签: assembly x86-64 avx gnu-assembler


【解决方案1】:

只有少数特定的 GPR 指令具有 VEX 编码,主要是在 AVX 已经存在之后添加的 BMI1/BMI2 指令。请参阅表 2-28 中的列表,其中包含ANDN, BEXTR, BLSI, BLSMSK, BLSR, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX,以及 5.1.16.1 中的相同列表。例如,andn's manual entry 仅列出 VEX 编码,and's manual entry 不列出任何编码。

所以英特尔(很遗憾)没有为整个指令集引入全新的三操作数替代编码。他们只是介绍了一些特定的指令,这些指令采用三个操作数并使用 VEX。在某些情况下,它们具有与现有指令相似或等效的功能,例如SHLX 用于具有变量计数的 SHL,因此有效地提供了先前两操作数指令的三操作数版本,但仅在那些特殊情况下。没有全面的等效说明。

“旧式”双操作数形式仍然是 add 指令的唯一版本。然而,正如 fuz 在 cmets 中指出的那样,lea 可能是添加两个寄存器并将结果写入第三个寄存器的好方法,但受操作数大小的一些限制。

请参阅Using LEA on values that aren't addresses / pointers? 了解 LEA 可以做的更一般的事情,例如将常量复制并添加到寄存器,或移位并添加。编译器已经知道这一点,并将在适当的情况下使用lea,只要它保存指令。 (或者使用一些调整选项,例如 -mtune=atom 用于旧的有序 Atom,即使他们可以使用 add,也会使用 lea。)

如果存在比 add 更灵活的通用整数指令编码,例如 and/xor/subgcc -O3 -march=skylake 将已经在其自己的 asm 输出中使用它们,而无需内联 asm。或者,如果替代指令可以完成工作,例如 leaadd,就会这样做,因此查看编译器输出以了解它知道哪些技巧是有意义的。自己尝试会更有意义,因为它可以在一个独立的.s 文件中使用,该文件只是进行退出系统调用,或者只是单步执行,从而消除了使用内联汇编的复杂性。 (默认情况下,GAS 不限制指令集。gcc -march=skylake 不会将其传递给汇编器,as。)


在您的内联汇编中,您的 c 操作数应为仅输出:=r 而不是 +r。旧值被覆盖,因此无需告诉编译器将其生成为输入。 (就像你说的,你想要c = a+b 而不是c += a+b。)

使用单个 lea 作为 asm 模板意味着您不需要 =&amp;r early-clobber 输出,因为您的 asm 将在写入该输出之前读取其所有输入。在您的情况下,将其作为输入/输出可能会阻止编译器选择与输入之一相同的寄存器,这可能与mov; add 发生冲突。

【讨论】:

  • 啊哈。你刚刚为我节省了很多时间。我很感激。有道理,现在。
  • @thb:我对这个答案做了一些主要的小改进。主要的新点是关于你的内联汇编:即使你原来的 mov;add 版本应该一直在使用 "=&amp;r",但我想知道你的 "+r" 是否是让它发生工作的黑客,因为你不知道早期-clobber 输出。无论如何,通常我只是在.s 文件中处理 asm,因此内联 asm 的额外复杂性是分开的。将诸如a+b 之类的简单内容的代码生成留给编译器,这样它就可以进行常量传播或使用内存源操作数而不是单独加载或其他任何东西。
  • @PeterCordes 您的改进已被接受。诚然,我很少写汇编,除非作为练习,同时试图理解处理器的某些特性,所以我的技术无疑是笨拙的。当然,您对“=&r”是正确的。我会相应地编辑问题。
猜你喜欢
  • 1970-01-01
  • 2011-08-19
  • 2021-04-21
  • 2011-11-26
  • 1970-01-01
  • 2015-01-03
  • 2021-01-25
  • 1970-01-01
  • 2021-12-29
相关资源
最近更新 更多