对共享变量的内联 asm 访问算作 C++11 中的数据竞争吗?

Does inline asm access to shared variables count as a data race in C++11?

#include <thread>
int i;
int main()
{
    std::thread t1([&i]() {
        asm volatile("movl , %0"
                     : "=m"(i));
    });
    std::thread t2([&i]() {
        asm volatile("movl , %0"
                     : "=m"(i));
    });
    t2.join();
    t1.join();
    return 0;
}

这是 C++ 中的数据竞争(结果是 UB)吗?让我们假设 i 的地址是对齐的,并且在我们的 CPU load/store 上对齐的双字是原子的。

很可能,它满足数据竞争的定义。

出于显而易见的原因,该标准保留内联 asm 实现定义。要对此发表任何看法,我们必须编造一些东西并挥舞一下。

更重要的是,我们不再考虑纯 ISO C++,而是 C++ 的 GNU 方言,它定义了许多 ISO C++ 未定义的行为。例如the gcc manual says type-punning by writing one union member and reading another is well-defined in GNU C++, even though it's UB in ISO C++. A lot of things are still UB in GNU C++, and "whatever g++ actually does" doesn't count as a definition. See the manual's implementation-defined behaviour section in the table of contents.

gcc 的 C++ -> asm 阶段甚至不理解 inline-asm 语句中的指令,它只是填充操作数并将其传递给汇编程序。它不 "think about" 指令在做什么;它将其视为由输出、输入和破坏约束描述的黑盒。

由于您使用了 "=m"(i) 输出操作数,您的 asm 语句 确实 以 C++ 方式与 C++ 变量交互(而不是在编译器的背后asm("mov , i");)。我认为编译器将其视为 i = __builtin_my_asm_statement(); 之类的东西。加上防止编译时重新排序/提升/死代码消除的 volatile 关键字。


并非每个数据争用都是导致数据争用的 C++ 标准未定义行为。例如,如果 i 是 std::atomic 类型,由于 race conditioni 的最终值仍然不确定。 (不过,该程序将没有 C++ UB,并且 i 将是 2 或 1,没有撕裂。当然,未定义的行为在技术上意味着任何事情都可能发生。)


那么,关于这段代码我们能说些什么

  • 我们可以假设 i 是自然对齐的,因为所有常用的 x86 ABI 都保证了这一点。
  • 我们知道 asm 会将存储作为一条指令包含在内存中,而不是一次复制一个字节。 (无论如何,任何理智的编译器都不会这样做)。
  • 不是 100%确定我们可以保证store直接进入i的共享值,而不是从头开始space 在编译器将从中复制的堆栈上。这样做的邪恶编译器会破坏 很多 代码,包括任何像 Linux 内核这样使用内联 asm 到 运行 locked 指令的东西在使用内存操作数的共享变量上。

因此,如果我们可以假设一个非恶意编译器,我们就可以非常确定编译器输出将做什么。或者 "=m" 操作数对共享值的行为应该被认为是 GNU C 中明确定义的行为,所以我们可以说这是明确定义的。