【问题标题】:`std::call_once` always segfaults on Clang 12 on Windows (when using libstdc++)`std::call_once` 在 Windows 上的 Clang 12 上总是出现段错误(使用 libstdc++ 时)
【发布时间】:2021-04-26 07:22:00
【问题描述】:

我正在寻找一种解决方法,这可能涉及修补 libstdc++ 标头。保留二进制兼容性是首选但不是强制性的,因为我没有使用除 libstdc++ 之外的任何预编译 C++ 代码。

我想保留std::call_once 接口,因为我正在尝试编译使用is 的第三方代码,我不想更改它。


这是我的代码:

#include <iostream>
#include <mutex>

int main()
{
    std::once_flag flag;
    std::call_once(flag, []{std::cout << "Once!\n";});
}

运行它会导致分段错误。

我使用 Clang 12 使用 MSYS2 GCC 10.2 中的标准库将它从 Ubuntu 交叉编译到 Windows。然后我用 Wine 测试结果(快速测试表明它也在 VM 上崩溃)。但是您应该能够通过在 Windows 上本地编译来重现结果(使用官方 Clang 二进制文件 + MSYS2 GCC,因为 MSYS2 还没有 Clang 12)。

我是这样编译的:

clang++-12 1.cpp --target=x86_64-w64-mingw32 --sysroot=/mingw64 -pthread -femulated-tls

如果我添加-g,GDB 显示如下:

Program received signal SIGSEGV, Segmentation fault.
0x00000001e014dc4a in ?? () from Z:\home\holyblackcat\Sandbox\2\libgcc_s_seh-1.dll
(gdb) bt
#0  0x00000001e014dc4a in ?? () from Z:\home\holyblackcat\Sandbox\2\libgcc_s_seh-1.dll
#1  0x00000000004015f3 in std::call_once<main::$_0> (__once=..., __f=...) at /mingw64/include/c++/10.2.0/mutex:721
#2  0x00000000004015b5 in main () at 1.cpp:8
(gdb) f 1
#1  0x00000000004015f3 in std::call_once<main::$_0> (__once=..., __f=...) at /mingw64/include/c++/10.2.0/mutex:721
721           __once_callable = std::__addressof(__callable);
(gdb) list
716           auto __callable = [&] {
717               std::__invoke(std::forward<_Callable>(__f),
718                             std::forward<_Args>(__args)...);
719           };
720     #ifdef _GLIBCXX_HAVE_TLS
721           __once_callable = std::__addressof(__callable);
722           __once_call = []{ (*(decltype(__callable)*)__once_callable)(); };
723     #else
724           unique_lock<mutex> __functor_lock(__get_once_mutex());
725           __once_functor = __callable;

Clang 版本是:

# clang++-12 --version --target=x86_64-w64-mingw32 --sysroot=/mingw64 -pthread -femulated-tls
Ubuntu clang version 12.0.1-++20210423082613+072c90a863aa-1~exp1~20210423063319.76
Target: x86_64-w64-windows-gnu
Thread model: posix

GCC 版本(提供 libstdc++)是:

# g++ --version
g++.exe (Rev10, Built by MSYS2 project) 10.2.0

用这个 GCC 编译代码(它是本机的,不是交叉编译的),会产生一个工作代码。

这里发生了什么?是否有任何解决方法,或者我必须降级到 Clang 11?


我举报了Clang bug

This bug 看起来相关。


这是call_once的当前段错误实现,经过预处理:

struct once_flag
{
  private:
    typedef __gthread_once_t __native_type;
    __native_type _M_once = 0;

  public:
    constexpr once_flag() noexcept = default;
    once_flag(const once_flag &) = delete;
    once_flag &operator=(const once_flag &) = delete;
    template <typename _Callable, typename... _Args>
    friend void call_once(once_flag &__once, _Callable &&__f, _Args &&...__args);
};

extern __thread void *__once_callable;
extern __thread void (*__once_call)();
extern "C" void __once_proxy(void);

template <typename _Callable, typename... _Args>
void call_once(once_flag &__once, _Callable &&__f, _Args &&...__args)
{
    auto __callable = [&]
    {
        std::__invoke(std::forward<_Callable>(__f), std::forward<_Args>(__args)...);
    };
    __once_callable = std::__addressof(__callable);
    __once_call = []{(*(decltype(__callable) *)__once_callable)();};
    int __e = __gthread_once(&__once._M_once, &__once_proxy);
    if (__e)
        __throw_system_error(__e);
}

【问题讨论】:

  • 适用于 VS2019 v16.9.4 cl.exe(编译器)v19.28.29914。绝对是一个错误。
  • 您尝试过AppVeyor Windows CI 实例吗?
  • @Casey 是的,毫无疑问这是一个错误。我希望有一个解决方法,因为我不想回滚到 Clang 11。不,我没有尝试 AppVeyor,但我希望结果是一样的。
  • #define call_once call_once_workaround(在正确的位置)并自己实施?
  • @C.M.是的,或者我可以修补标题本身。但我觉得我的知识不足以实现它。

标签: c++ clang


【解决方案1】:

这已在 Clang 13 中的 commit 0e4cf80 中修复。 (感谢@mstorsjo。)


如果您被 Clang 12 卡住,您可以修补 libstdc++ 标头作为解决方法。下面的补丁将全局线程局部变量替换为static函数局部线程局部变量,不受bug影响。

要申请,请将关注保存到patch.txt,然后执行patch /mingw64/include/c++/10.3.0/mutex patch.txt

@@ -691,8 +691,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION

   /// @cond undocumented
 #ifdef _GLIBCXX_HAVE_TLS
-  extern __thread void* __once_callable;
-  extern __thread void (*__once_call)();
+  inline void *&__once_callable_get() {static __thread void *__ret; return __ret;}
+  inline void (*&__once_call_get())() {static __thread void (*__ret)(); return __ret;}
 #else
   extern function<void()> __once_functor;

@@ -703,7 +703,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
   __get_once_mutex();
 #endif

-  extern "C" void __once_proxy(void);
+  extern "C" inline void __once_proxy_inline(void) {__once_call_get()();}
   /// @endcond

   /// Invoke a callable and synchronize with other calls using the same flag
@@ -718,15 +718,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
            std::forward<_Args>(__args)...);
       };
 #ifdef _GLIBCXX_HAVE_TLS
-      __once_callable = std::__addressof(__callable); // NOLINT: PR 82481
-      __once_call = []{ (*(decltype(__callable)*)__once_callable)(); };
+      __once_callable_get() = std::__addressof(__callable); // NOLINT: PR 82481
+      __once_call_get() = []{ (*(decltype(__callable)*)__once_callable_get())(); };
 #else
       unique_lock<mutex> __functor_lock(__get_once_mutex());
       __once_functor = __callable;
       __set_once_functor_lock_ptr(&__functor_lock);
 #endif

-      int __e = __gthread_once(&__once._M_once, &__once_proxy);
+      int __e = __gthread_once(&__once._M_once, &__once_proxy_inline);

 #ifndef _GLIBCXX_HAVE_TLS
       if (__functor_lock)

这是 GCC 10.2 的等效补丁(以上是 10.3):

@@ -691,8 +691,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
   /// @cond undocumented
 #ifdef _GLIBCXX_HAVE_TLS
-  extern __thread void* __once_callable;
-  extern __thread void (*__once_call)();
+  inline void *&__once_callable_get() {static __thread void *__ret; return __ret;}
+  inline void (*&__once_call_get())() {static __thread void (*__ret)(); return __ret;}
 #else
   extern function<void()> __once_functor;
 
@@ -703,7 +703,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
   __get_once_mutex();
 #endif
 
-  extern "C" void __once_proxy(void);
+  extern "C" inline void __once_proxy_inline(void) {__once_call_get()();}
   /// @endcond
 
   /// Invoke a callable and synchronize with other calls using the same flag
@@ -718,15 +718,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
            std::forward<_Args>(__args)...);
       };
 #ifdef _GLIBCXX_HAVE_TLS
-      __once_callable = std::__addressof(__callable);
-      __once_call = []{ (*(decltype(__callable)*)__once_callable)(); };
+      __once_callable_get() = std::__addressof(__callable);
+      __once_call_get() = []{ (*(decltype(__callable)*)__once_callable_get())(); };
 #else
       unique_lock<mutex> __functor_lock(__get_once_mutex());
       __once_functor = __callable;
       __set_once_functor_lock_ptr(&__functor_lock);
 #endif
 
-      int __e = __gthread_once(&__once._M_once, &__once_proxy);
+      int __e = __gthread_once(&__once._M_once, &__once_proxy_inline);
 
 #ifndef _GLIBCXX_HAVE_TLS
       if (__functor_lock)
@@ -735,8 +735,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
 #ifdef __clang_analyzer__
       // PR libstdc++/82481
-      __once_callable = nullptr;
-      __once_call = nullptr;
+      __once_callable_get() = nullptr;
+      __once_call_get() = nullptr;
 #endif
 
       if (__e)

【讨论】:

  • 古老的“用本地静态替换全局静态”技巧仍然有效 :)
  • @C.M.是的!感谢您的帮助。
【解决方案2】:

解决此问题的一种方法是利用 static 变量自 C++11 以来以线程安全方式初始化的事实。示例:

#include <iostream>

void test ()
{
    std::cout << "test\n";
    static bool once = [] { std::cout << "Once!\n"; return true; } ();
    (void) once;
}

int main()
{
    test ();
    test ();
}

输出:

test
Once!
test

如您所见,lambda 在静态变量第一次进入作用域时被调用(并且只会被调用一次)。

【讨论】:

  • 问题是,我正在尝试编译一些使用std::call_once 的库。所以我需要std::call_once拥有的相同接口。
【解决方案3】:

猜测:由于缺少InitializeCriticalSectionEx(),它正在崩溃,这意味着您需要将您的 Windows VM 升级到 XP 以上,或者向编译器/链接器指定您的目标是 Windows 子系统 v5.02。不需要补丁。 ;-)

编辑#1:

在黑暗中射击:尝试将-D_WIN32_WINNT=0x0502 添加到您的编译中。它可能会解决一些链接错误。尽管混合和匹配libstd++ 版本可能永远无法在这里工作,并且确切的问题会因平台/编译器而异。

正如另一位评论者指出的那样,函数本地静态变量的初始化现在在 C++ 中是线程安全的,这似乎迫使 TLS 在 Windows 上的工作方式发生了一些变化,一直到“子系统”级别。在我们自己的构建中,我们将-Zc:threadSafeInit-/subsystem:windows,5.02 (x64) 或/subsystem:windows,5.01 (x86) 传递给VC++,这样它就可以在XP 上运行。但是,这些新奇的线程安全静态局部变量不会发生。我只能想象在 NT 加载器是多线程的 Windows 10 上可能会出现什么问题。

编辑#2:

最后一次窃取赏金的尝试,现在来自 OP 本人:我正在倒下this path。我目前的理论是这个新的 clang 11 编译器中存在一个错误,导致它从错误的位置获取“已知”标头 &lt;iostream&gt;,尽管 --sysroot。也许只有在交叉编译时。

lstrand@styx:~/scratch/emulated-tls$ strace -o strace.out clang++-12 -E 1.exe 1.cpp --target=x86_64-w64-mingw32 --sysroot=/home/lstrand/scratch/emulated-tls/root > pp
clang: warning: 1.exe: 'linker' input unused [-Wunused-command-line-argument]
1.cpp:1:10: fatal error: 'iostream' file not found
#include <iostream>
         ^~~~~~~~~~
1 error generated.
lstrand@styx:~/scratch/emulated-tls$ grep iostream strace.out 
pread64(3, "#include <iostream>\n#include <mu"..., 134, 0) = 134
openat(AT_FDCWD, "/usr/lib/llvm-12/lib/clang/12.0.1/include/iostream", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "./iostream", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
write(2, "'iostream' file not found", 25) = 25
write(2, "#include <iostream>", 19)     = 19
lstrand@styx:~/scratch/emulated-tls$ 

【讨论】:

  • VM 有 Win 10,它在 Wine 上也崩溃了。 “指定编译器/链接器的目标是 Windows 子系统 v5.02” 我该怎么做?
  • 嗯。那你为什么用-femulated-tls
  • 因为否则我会收到链接器错误。我认为如果您将 Clang 与 libstdc++ 一起使用,您应该使用它,例如解释。 here.
  • 我添加了一个“在黑暗中拍摄”的视角。我不知道 C++ 示例的链接器错误是什么,因为我只是在我的 Ubuntu 机器上安装了 clang 12,并且似乎缺少一些标头...
  • -D_WIN32_WINNT=0x0502 没有改变任何东西,无论有没有-femulated-tls
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-13
  • 1970-01-01
  • 1970-01-01
  • 2014-03-16
相关资源
最近更新 更多