在 GCC 内联 asm 中影响内存操作数寻址模式的早期破坏者的不正确行为的具体示例?

Concrete example of incorrect behavior of an early-clobber affecting a memory operand's addressing mode in GCC inline asm?

以下摘自 GCC manual's Extended Asm docs,关于使用 asm 关键字在 C 语言中嵌入汇编指令:

The same problem can occur if one output parameter (a) allows a register constraint and another output parameter (b) allows a memory constraint. The code generated by GCC to access the memory address in b can contain registers which might be shared by a, and GCC considers those registers to be inputs to the asm. As above, GCC assumes that such input registers are consumed before any outputs are written. This assumption may result in incorrect behavior if the asm statement writes to a before using b. Combining the ‘&’ modifier with the register constraint on a ensures that modifying a does not affect the address referenced by b. Otherwise, the location of b is undefined if a is modified before using b.

斜体字表示如果 asm 语句在使用 b 之前写入 a 可能存在“不正确的行为”。

我想不通怎么会出现这样的“错误行为”,所以我希望有一个具体的asm代码示例来演示“错误行为”,以便我对这段话有更深刻的理解。

当两个这样的asm代码并行运行时,我可以感知到问题,但是上面的段落没有提到多处理场景。

如果我们只有一个 CPU 和一个内核,请您提供一个可能产生这种不正确行为的 asm 代码,即修改 a 会影响 [= 引用的地址12=] 这样 b 的位置未定义。

我唯一熟悉的汇编语言是 Intel x86 汇编,所以请针对该平台制作示例。

考虑以下示例:

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

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

    return r;
}

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

        subl    , %esp
        call    foo
        mov [=11=], %eax
        add (%eax), %eax
        addl    , %esp
        ret

请注意,在下一条指令尝试使用它来引用输入参数之前,mov [=16=], %eax 清零 eax,因此此代码将崩溃。使用 early clobber,您可以强制编译器选择不同的寄存器。在我的例子中,生成的代码是:

        subl    , %esp
        call    foo
        mov [=12=], %edx
        add (%eax), %edx
        addl    , %esp
        movl    %edx, %eax
        ret

编译器本可以将 foo() 的结果移动到 edx(或任何其他空闲寄存器),如下所示:

        subl    , %esp
        call    foo
        mov     %eax, %edx
        mov [=13=], %eax
        add (%edx), %eax
        addl    , %esp
        ret

此示例对输入参数使用了内存约束,但该概念同样适用于输出。

鉴于下面的代码,具有 -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] 所需的地址。