当销毁 thread_local shared_ptr 对象时引起 sigsegv。

11
我有一个程序,它使用thread_local std::shared_ptr来管理一些主要在本地线程上访问的对象。然而,当线程被加入并且线程本地的shared_ptr正在销毁时,如果该程序是由MinGW(Windows 10)编译的,则调试时总会出现SIGSEGV。以下是最小的重现错误的代码:
// main.cpp
#include <memory>
#include <thread>

void f() {
    thread_local std::shared_ptr<int> ptr = std::make_shared<int>(0);
}

int main() {
    std::thread th(f);
    th.join();
    return 0;
}

如何编译:

g++ main.cpp -o build\main.exe -std=c++17

编译器版本:

>g++ --version
g++ (x86_64-posix-seh-rev2, Built by MinGW-W64 project) 12.2.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

使用gdb运行时,当主线程等待join()时,它会在新线程中产生SIGSEGV。在gcc、clang(Linux)和MSVC(Windows)编译时,它能正常工作。

我尝试进行调试并发现,在调用RtlpWow64SetContextOnAmd64时,一个连续的内存段包含线程本地的shared_ptr被擦除为重复的0xfeeefeee,然后才被销毁。帧:

RtlpWow64SetContextOnAmd64 0x00007ffd8f4deb5f
RtlpWow64SetContextOnAmd64 0x00007ffd8f4de978
SbSelectProcedure 0x00007ffd8f4ae2e0
CloseHandle 0x00007ffd8ce3655b
pthread_create_wrapper 0x00007ffd73934bac
_beginthreadex 0x00007ffd8e9baf5a
_endthreadex 0x00007ffd8e9bb02c
BaseThreadInitThunk 0x00007ffd8ec87614
RtlUserThreadStart 0x00007ffd8f4c26a1

汇编语言:

...
mov    %rax,(%rdi)
movdqu %xmm0,(%rsi)               ; <------ erased here
call   0x7ffd8f491920             ; <ntdll!RtlReleaseSRWLockShared>
mov    $0x1,%r9d
mov    0x30(%rsp),%rbx
...

后来,当shared_ptr被销毁时,在读取0xfeeefeee时会出现SIGSEGV。

我想知道:

  • 为什么MinGW(或Windows库?)在销毁之前擦除线程本地存储?在我看来,擦除内存应该只发生在销毁之后。我注意到,如果将join()替换为detach(),程序正常退出。也许join()对新线程有指示以擦除存储的操作?
  • 这种行为是否违反标准?我认为标准应禁止在销毁之前擦除内存。如果我错了,请指正。

2
thread_local 和本地变量混合使用是不寻常的用例。我猜想 ptrf 结束时被销毁。建议将该变量移至全局作用域。另一方面,这意味着一个静态局部变量:https://dev59.com/JGEh5IYBdhLWcg3wHARD#22794640 - 273K
1
最好使用MSYS2 https://www.msys2.org/ - 273K
2
在godbolt.org上尝试使用不同的编译器版本,也许如果您选择比您正在使用的更新的版本,则问题会消失。 - Pepijn Kramer
2
顺便说一句,我可以在Win 11、g++ (Rev6, Built by MSYS2 project) 12.2.0中重现这个问题。它显示为Thread 5 received signal SIGSEGV, Segmentation fault. [Switching to Thread 15196.0x27dc] 0x00007ff7e54133f4 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() () (gdb) x/i $rbp 0xfb957ff4b0: loopne 0xfb957ff4a6 - Eritque arcus
1
@SolomonSlow,FYI:“如果thread_local是应用于块作用域变量的唯一存储类说明符,则也隐含了static…”。https://en.cppreference.com/w/cpp/language/storage_duration - Richard Critten
显示剩余12条评论
1个回答

3

这是一个长期存在的、公开的并且已知的mingw bug,在github上有相应的问题和分析链接: https://github.com/msys2/MINGW-packages/issues/2519

是的,这违反了标准: 它不应该崩溃。 基本上,析构顺序是错误的,正如你之前怀疑的那样。 0xfeeefeee 是由 HeapFree() 用于标记已释放内存的魔术数字。详见 this post

引用 lhmouse 的话:

所以这里有一个经验法则:不要在MinGW目标上使用thread_local。


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