`std::call_once` 总是在 Windows 上的 Clang 12 上出现段错误(使用 libstdc++ 时)
`std::call_once` always segfaults on Clang 12 on Windows (when using libstdc++)
我正在寻找解决方法,这可能涉及修补 libstdc++ headers。保留二进制兼容性是首选,但不是强制性的,因为除了 libstdc++,我没有使用任何预编译的 C++ 代码。
我想保留 std::call_once
界面,因为我正在尝试编译 third-party 使用的代码,我不想更改它。
这是我的代码:
#include <iostream>
#include <mutex>
int main()
{
std::once_flag flag;
std::call_once(flag, []{std::cout << "Once!\n";});
}
运行 导致段错误。
我使用 Clang 12,使用来自 MSYS2 GCC 10.2 的标准库,cross-compile 它从 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\libgcc_s_seh-1.dll
(gdb) bt
#0 0x00000001e014dc4a in ?? () from Z:\home\holyblackcat\Sandbox\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(它是原生的,不是 cross-compiling)编译代码,生成一个工作代码。
这是怎么回事?是否有任何解决方法,或者我是否必须降级到 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);
}
解决此问题的一种方法是利用以下事实: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 在静态变量第一次进入作用域时被调用(并且只会被调用一次)。
这已在 commit 0e4cf80
. (Thanks @mstorsjo 的 Clang 13 中修复。)
如果您受困于 Clang 12,您可以修补 libstdc++ headers 作为解决方法。下面的补丁将全局 thread-local 变量替换为 static
function-local thread-local 变量,这些变量不受该错误的影响。
要申请,请将以下内容保存到 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)
大胆猜测:由于缺少 InitializeCriticalSectionEx()
,它正在崩溃,这意味着您需要将 Windows VM 升级到 XP 之后,或者指定 compiler/linker 您目标是 Windows 子系统 v5.02。无需补丁。 ;-)
编辑#1:
在黑暗中拍摄:尝试将 -D_WIN32_WINNT=0x0502
添加到您的编译中。它可能会解决一些 link 错误。虽然混合和匹配 libstd++
版本可能永远不会在这里工作,并且确切的问题会从一个 platform/compiler 到下一个不同。
正如另一位评论者所指出的,函数局部静态变量的初始化现在在 C++ 中是 thread-safe,这似乎迫使 TLS 在 Windows 上的工作方式发生了一些变化,对吧下降到“子系统”级别。在我们自己的构建中,我们将 -Zc:threadSafeInit-
和 /subsystem:windows,5.02
(x64) 或 /subsystem:windows,5.01
(x86) 传递给 VC++,这样它就可以在 XP 上运行。但据了解,这些新奇的 thread-safe 静态局部变量不会发生。我只能想象 Windows 10 上的问题可能是什么,其中 NT 加载器是 multi-threaded.
编辑#2:
最后一次尝试窃取赏金,现在来自 OP 本人:我正在倒下 this path。我目前的理论是,这个新的 clang 11 编译器中存在一个错误,导致它从错误的位置获取“已知”header<iostream>
,尽管 --sysroot
。也许只有当 cross-compiling.
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$
我正在寻找解决方法,这可能涉及修补 libstdc++ headers。保留二进制兼容性是首选,但不是强制性的,因为除了 libstdc++,我没有使用任何预编译的 C++ 代码。
我想保留 std::call_once
界面,因为我正在尝试编译 third-party 使用的代码,我不想更改它。
这是我的代码:
#include <iostream>
#include <mutex>
int main()
{
std::once_flag flag;
std::call_once(flag, []{std::cout << "Once!\n";});
}
运行 导致段错误。
我使用 Clang 12,使用来自 MSYS2 GCC 10.2 的标准库,cross-compile 它从 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\libgcc_s_seh-1.dll
(gdb) bt
#0 0x00000001e014dc4a in ?? () from Z:\home\holyblackcat\Sandbox\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(它是原生的,不是 cross-compiling)编译代码,生成一个工作代码。
这是怎么回事?是否有任何解决方法,或者我是否必须降级到 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);
}
解决此问题的一种方法是利用以下事实: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 在静态变量第一次进入作用域时被调用(并且只会被调用一次)。
这已在 commit 0e4cf80
. (Thanks @mstorsjo 的 Clang 13 中修复。)
如果您受困于 Clang 12,您可以修补 libstdc++ headers 作为解决方法。下面的补丁将全局 thread-local 变量替换为 static
function-local thread-local 变量,这些变量不受该错误的影响。
要申请,请将以下内容保存到 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)
大胆猜测:由于缺少 InitializeCriticalSectionEx()
,它正在崩溃,这意味着您需要将 Windows VM 升级到 XP 之后,或者指定 compiler/linker 您目标是 Windows 子系统 v5.02。无需补丁。 ;-)
编辑#1:
在黑暗中拍摄:尝试将 -D_WIN32_WINNT=0x0502
添加到您的编译中。它可能会解决一些 link 错误。虽然混合和匹配 libstd++
版本可能永远不会在这里工作,并且确切的问题会从一个 platform/compiler 到下一个不同。
正如另一位评论者所指出的,函数局部静态变量的初始化现在在 C++ 中是 thread-safe,这似乎迫使 TLS 在 Windows 上的工作方式发生了一些变化,对吧下降到“子系统”级别。在我们自己的构建中,我们将 -Zc:threadSafeInit-
和 /subsystem:windows,5.02
(x64) 或 /subsystem:windows,5.01
(x86) 传递给 VC++,这样它就可以在 XP 上运行。但据了解,这些新奇的 thread-safe 静态局部变量不会发生。我只能想象 Windows 10 上的问题可能是什么,其中 NT 加载器是 multi-threaded.
编辑#2:
最后一次尝试窃取赏金,现在来自 OP 本人:我正在倒下 this path。我目前的理论是,这个新的 clang 11 编译器中存在一个错误,导致它从错误的位置获取“已知”header<iostream>
,尽管 --sysroot
。也许只有当 cross-compiling.
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$