“+&r”和“+r”有什么不同?

5
GCC的嵌入汇编语言识别声明符=r=&r。这些对我有意义: =r让汇编器将一个输入寄存器重用为输出寄存器。
然而,GCC的嵌入汇编语言还识别声明符+r+&r。这些对我来说不太好理解。毕竟,+r+&r之间的区别不是无关紧要的吗?只使用+r难道不足以告诉编译器为单个变量保留一个寄存器吗?
例如,下面的GCC代码有什么问题?
#include <stdio.h>
int main()
{
    int a = 0;
    printf("Initially, a == %d.\n", a);
    /* The architecture is amd64/x86-64. */
    asm(
        "inc %[a]\n"
        : [a] "+r" (a)
        : : "cc"
    );
    printf("However, after incrementation, a == %d.\n", a);
    return 0;
}

顺便注意一下,我的内联汇编缺少输入声明,因为在我的(也许是错误的)想法中,+r 包括了输入、破坏和输出等所有内容。请问我误解了什么?

背景

我曾经在程序设计中使用过8位和16位微控制器的汇编语言,但几乎没有在托管环境下编写汇编语言的经验。


1
@thb 我明白了。我建议你使用英特尔的内嵌函数。内联汇编应该是最后的选择,因为它以难以捉摸著称。 - fuz
2
早期破坏(&)修饰符的文档对于在读写操作数上使用它的用法有这样一种说法:此外,如果早期破坏操作数也是一个读/写操作数,则该操作数仅在使用后才被写入。 - Michael Petch
2
@thb 在某种意义上,编译器在给定的约束条件下会进行任意的寄存器选择,因此它是不稳定的。但是,如果你的内联汇编包含超过一两个语句,编译器往往会以你没有预料到的方式随机分配寄存器。这就是为什么我建议先学习使用内置函数或通过编写单独文件中的汇编函数来学习汇编语言。 - fuz
1
@MichaelPetch:你很有洞察力。我也注意到了那个句子。这个句子对我来说似乎是重言的,但也许我读错了。毕竟,这是我的内联汇编,所以我决定何时写入和使用操作数,不是吗?据我所知,编译器对这些事情一无所知。我想我不明白+&r告诉编译器什么,因为编译器从一个简单的+r中已经知道了这些东西。 - thb
3
可能会有一种情况会产生影响,那就是当一个读写操作数和一个只读操作数都传递了相同的值时,优化器可能会选择同一个寄存器。如果内联汇编在原始寄存器的值被使用之前修改了寄存器,则可能会得到错误的结果。在输入/输出操作数上使用 early clobber 会强制编译器使用两个单独的寄存器。 - Michael Petch
显示剩余6条评论
1个回答

10
默认情况下,GCC认为内联汇编语句由一个简单的汇编指令组成,该指令在写入任何输出操作数之前会消耗其所有输入操作数。当编写使用多个汇编指令的内联汇编语句时,通常会违反这个假设,因此需要使用早期破坏约束修饰符 & 来指示在消耗所有输入操作数之前写入哪些输出操作数。这对于使用 = 修饰符的输出操作数以及使用 + 的读/写输出操作数都是必需的。例如,请考虑以下两个函数:
int
foo() {
    int a = 1;
    asm("add %1, %0" : "+r" (a) : "r" (1));
    return a;
}

int
bar() {
    int a = 1;
    asm("add %1, %0\n\t"
        "add %1, %0"
        : "+r" (a) : "r" (1));
    return a;
}

两个嵌入式汇编语句使用相同的操作数和约束条件,但只有 foo 中的嵌入式汇编语句是正确的,bar 中的则存在问题。启用优化后,GCC 为这两个函数生成以下代码:

_foo:
    movl    $1, %eax
/APP
    add %eax, %eax
/NO_APP
    ret

_bar:
    movl    $1, %eax
/APP
    add %eax, %eax
    add %eax, %eax
/NO_APP
    ret

GCC认为在内联汇编语句中,可以使用相同的寄存器EAX来存储两个操作数。虽然这在foo中没有问题,但会导致bar计算出错误的结果4,而非预期的3。

一个正确的bar版本应该使用早期修改约束符:

int
baz() {
    int a = 1;
    asm("add %1, %0\n\t"
        "add %1, %0"
        : "+&r" (a) : "r" (1));
    return a;
}

_baz:
    movl    $1, %eax
    movl    %eax, %edx
/APP
    add %edx, %eax
    add %edx, %eax
/NO_APP
    ret

编译baz时,GCC知道要为两个操作数使用不同的寄存器,因此在第二次读取输入操作数之前修改了读写输出操作数并不重要。


3
我是点赞。以下是应避免使用内联汇编的原因,除非你知道自己在做什么。许多人不考虑代码优化时可能采取的快捷方式以简化传递给扩展汇编模板的输入和输出操作数。加上GCC文档含糊不清也没有帮助。谢谢,您的示例说明了我对OP的评论所说的内容。 - Michael Petch
1
@thb 我想我应该说的是,如果你正在编写生产代码,那么最好知道自己在做什么。如果你所做的不是关键任务,那么就没有什么损失。但是在生产代码中,应该尽可能使用内置函数。如果你不想处理更复杂的扩展内联汇编的所有细节,那么将其放入单独的汇编对象(并链接它)会更好。虽然可能会失去优化的好处,但你可能会获得更好的稳定性。 - Michael Petch
1
如果你想看一个最近关于某个奇怪问题的讨论例子,你可以查看这个SO问题和答案。它涉及将一个(长度不固定的)数组作为内存操作数传递。我们确定了编译器的行为,但有些人还是被吓到了。最终在GCC邮件列表上发布了一篇文章,以确定预期的行为和处理它的最佳方法。看起来结果将是未来的GCC内联汇编文档将被更新,以更清晰地说明如何解决这个问题。 - Michael Petch
1
@tbh:我提到另一个问题的原因是为了表明有时候预期的行为和/或文档可能会足够模糊,以至于即使那些已经使用内联汇编模板一段时间的人也会感到困惑。我非常节制地使用内联汇编。在代码审查中,我会对内联汇编进行额外的严格检查,因为它可能是一些非常恶意的错误的根源,这些错误可能会在你最不希望出现的时候出现。 - Michael Petch
2
@thb 我已经使用GCC及其内联汇编形式将近30年了,我也相当熟悉它所基于的GCC内部结构(虽然已经过时),但我仍然觉得它是个雷区。我最近犯的一个错误是错误地假设在+中不需要使用&。因此,我避免使用内联汇编,除了在Stack Overflow答案中,我已经很久没有在其他地方使用它了。 - Ross Ridge
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接