GCC内联汇编中早期破坏寄存器的错误行为对内存操作数寻址模式的影响的具体示例是什么?

11
以下是从GCC手册的扩展汇编文档中摘录的内容,介绍如何使用asm关键字在C语言中嵌入汇编指令:
如果一个输出参数(a)允许寄存器约束,而另一个输出参数(b)允许内存约束,则可能会出现相同的问题。 GCC生成的代码用于访问b中的内存地址可以包含寄存器,这些寄存器可能被a共享,并且GCC认为这些寄存器是asm的输入。与上面一样,GCC假定在写入任何输出之前已经使用了这些输入寄存器。如果asm语句在使用b之前写入a,则此假设可能导致不正确的行为。通过将&符号修饰符与a上的寄存器约束相结合,可以确保修改a不会影响b所引用的地址。否则,如果在使用b之前修改a,则b的位置未定义。
斜体句子表示,如果asm语句在使用b之前写入a,则可能会出现“不正确的行为”。
我无法想象这样的“不正确行为”可能发生了,因此我希望有一个具体的asm代码示例来演示“不正确的行为”,以便我能深入理解这段文字。
当两个这样的asm代码并行运行时,我可以理解这个问题,但是上面的段落没有提到多处理场景。
如果我们只有一个CPU和一个核心,您可以展示一个可能产生这种不正确行为的汇编代码,即修改a会影响b所引用的地址,使b的位置未定义。
我熟悉的唯一汇编语言是Intel x86汇编语言,因此请将示例针对该平台。

这种情况的讽刺之处在于,许多人来到SO上寻求帮助,因为他们的代码出了问题:D - Jester
@TedLyngmo:GCC的内联汇编功能是符合C标准定义的合规C代码,在许多情况下,对于C语言编程非常有用。 - Eric Postpischil
@EricPostpischil 我明白混合语言的工作原理。我只是认为在这里使用C语言是不必要的。为什么需要一个包装语言呢?你的回答可能是可以的。 - Ted Lyngmo
2
顺便提一下,每个核心(以及通过上下文切换的每个软件线程)都有自己的私有寄存器。多线程不是通过随机混合在同一架构状态下操作的不同线程的指令来工作的。现代常见ISA中只有非常少量的架构寄存器(16或32个),代码(手写或编译器生成的)依赖于它们保持您放入其中的值。我不知道你是否提到了多线程,因为你认为这通常是如何工作的,还是你正在发明假想的奇怪事情,你可以想象会导致故障。 - Peter Cordes
2个回答

11

考虑以下示例:

extern int* foo();
int bar()
{
    int r;

    __asm__(
        "mov $0, %0 \n\t"
        "add %1, %0"
    : "=r" (r) : "m" (*foo()));

    return r;
}
通常的调用约定将返回值放入 eax 寄存器中。因此,编译器很有可能决定在整个过程中都使用 eax,以避免不必要的复制。生成的汇编代码可能如下所示:

通常的调用惯例会把返回值放到 eax 寄存器中。因此,编译器很有可能决定在整个过程中都使用 eax,以避免不必要的复制。生成的汇编代码可能如下:

        subl    $12, %esp
        call    foo
        mov $0, %eax
        add (%eax), %eax
        addl    $12, %esp
        ret

请注意,mov $0, %eax指令将会把eax寄存器清零,在下一条指令尝试使用它引用输入参数时,代码将会崩溃。通过使用“early clobber”语法,你可以强制编译器选择不同的寄存器。在我的情况下,生成的代码如下:

        subl    $12, %esp
        call    foo
        mov $0, %edx
        add (%eax), %edx
        addl    $12, %esp
        movl    %edx, %eax
        ret
编译器本来可以将foo()的结果移动到edx(或任何其他可用的寄存器)中,像这样:
        subl    $12, %esp
        call    foo
        mov     %eax, %edx
        mov $0, %eax
        add (%edx), %eax
        addl    $12, %esp
        ret

这个例子中使用了内存约束作为输入参数,但是这个概念同样适用于输出。


3
确实,https://godbolt.org/z/x4WMY7dns 显示了您所说的破损情况,并且使用 "=&r" 早期破坏输出可以避免这种情况。(适用于 x86-64 的 GCC11) - Peter Cordes
很棒的例子。它不仅展示了错误的行为,而且还说明了“GCC认为这些寄存器是asm的输入。与上面一样,GCC假设这样的输入寄存器在任何输出被写入之前都已经被消耗掉了。” %eax是asm的输入,因此GCC假设%eax已经被消耗掉了,并因此将其用于输出以生成“mov $0,%eax”。这个写操作会导致错误的行为,因为前面提到的假设不成立:输入的%eax被消耗掉,直到输出(为了优化目的而使用相同的位置)被写入。GCC的假设是... - zzzhhh
合理的做法是程序员应该使用“=&r”作为第一个正确的代码来分配其他寄存器用于输出,或者手动将输入%eax保存到其他地方作为第二个正确的代码(一种消耗方式),因此应该受到尊重。 - zzzhhh

6
在下面的代码中,使用 -O3 选项的Apple Clang 11将(%rax)用于a,并将%eax用于b
void foo(int *a)
{
    __asm__(
            "nop    # a is %[a].\n"
            "nop    # b is %[b].\n"
            "nop    # c is %[c].\n"
            "nop    # d is %[d].\n"
            "nop    # e is %[e].\n"
            "nop    # f is %[f].\n"
            "nop    # g is %[g].\n"
            "nop    # h is %[h].\n"
            "nop    # i is %[i].\n"
            "nop    # j is %[j].\n"
            "nop    # k is %[k].\n"
            "nop    # l is %[l].\n"
            "nop    # m is %[m].\n"
            "nop    # n is %[n].\n"
            "nop    # o is %[o].\n"
        :
            [a] "=m" (a[ 0]),
            [b] "=r" (a[ 1]),
            [c] "=r" (a[ 2]),
            [d] "=r" (a[ 3]),
            [e] "=r" (a[ 4]),
            [f] "=r" (a[ 5]),
            [g] "=r" (a[ 6]),
            [h] "=r" (a[ 7]),
            [i] "=r" (a[ 8]),
            [j] "=r" (a[ 9]),
            [k] "=r" (a[10]),
            [l] "=r" (a[11]),
            [m] "=r" (a[12]),
            [n] "=r" (a[13]),
            [o] "=r" (a[14])
        );
}

因此,如果将nop指令和注释替换为实际指令,在写入%[a]之前写入%[b],就会破坏%[a]所需的地址。

3
我在想为什么GCC和Clang如此不愿意重用寄存器,甚至更喜欢保存/恢复调用保留的寄存器,例如RBX。原来这仅仅是因为需要在汇编语句后使用数组指针int *a,将输出从寄存器传递到a[...]中。只有当你完全使用完寄存器输出数量时,gcc或clang才会选择EDI作为输出。链接(https://godbolt.org/z/9eeWfezzo)。其中,clang在代码之后做得非常糟糕,将它们全部溢出到堆栈中,而不是只溢出一个来重新加载指针,然后每次复制4B。 - Peter Cordes
2
在同一个Godbolt链接中,bar()函数在汇编语句后只返回2个输出的和,没有存储到a[]数组中;然后clang会很高兴地选择EDI作为输出寄存器,而不是R8D..R15D中的任何一个,只有5个寄存器可以输出。(尽管GCC仍然会优先选择R8D。) - Peter Cordes
2
顺便说一句,你两次使用了 [h]。GCC 和主线 clang 都会出错(https://godbolt.org/z/YvW69zzPe);我认为 Apple Clang 也会出错,这是一个复制粘贴的错误。此外,为了方便 Godbolt 的使用,我经常使用 nop # comment,这样该行就不会被过滤掉。(GCC 进行纯文本替换,因此它甚至不必是有效的汇编语言,而不像 clang 的内置汇编器)。 - Peter Cordes
2
@PeterCordes:关于“我认为Apple Clang也会做到”:它并没有! - Eric Postpischil
在Clang v9之前没有错误诊断,这相当令人惊讶。是时候更新你的Mac了! :-) - Cody Gray
@CodyGray:我希望我能这样做。我依赖于32位的Quicken 2007。macOS 10.14.6之后不支持32位,而我所依赖的Quicken的后续版本没有我需要的功能,而我在过去调查的Quicken替代品也没有。而且我已经达到了苹果开发者工具对macOS 10.14.6的限制。我正在设置VirtualBox中的10.14.6,以便我可以在更新真实系统到更高版本的同时保留Quicken。 - Eric Postpischil

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