线程在完成执行代码之前在动态库中中止

Posted

技术标签:

【中文标题】线程在完成执行代码之前在动态库中中止【英文标题】:Thread aborts in a dynamic library before it has finished executing code 【发布时间】:2016-04-04 16:59:27 【问题描述】:

我正在开发一个与 C API 兼容的库。

在库中会有一个对象的全局实例,它的成员是std::thread。似乎由于某种原因,当main 返回并调用exit() 时,线程被自动杀死/终止。如果代码在同一个项目中使用(直接在可执行文件中而不是通过库),则不会发生这种情况。

我希望以下示例在无限循环中运行:while(1) ...thread.join() 应该相互阻塞。与库一起使用时不会,当调用 CThread 的析构函数时,线程似乎已经被杀死/完成。我在这里错过了什么?

CThread.h

#ifndef CTHREAD_H
#define CTHREAD_H

#ifdef EXPORT_C_THREAD
# define EXPORT_CTHREAD __declspec(dllexport) __cdecl
#else
# define EXPORT_CTHREAD __declspec(dllimport) __cdecl
#endif

void EXPORT_CTHREAD testFunc();

#endif

CThread.cpp

#include "CThread.h"

#include <thread>
#include <memory>

class CThread

   std::thread m_thread;
   void infiniteLoop()
   
      while (1) 
         std::this_thread::sleep_for(std::chrono::microseconds(100));
      
   
public:
   CThread()
   
      m_thread = std::thread(&CThread::infiniteLoop, this);
   

   ~CThread()
   
      m_thread.join();
   

;

std::unique_ptr<CThread> cthread;

void testFunc()

   cthread = std::make_unique<CThread>();

ma​​in.cpp(这是在另一个项目中。我正在链接到上面的库。)

#include "CThread.h"

int main()

   testFunc();
    return 0;

更新

按照建议,我尝试在DLL_PROCESS_ATTACH 期间初始化DllMain() 函数中的cthread 对象,并在DLL_PROCESS_DETACH 期间解除分配。由于DllMain()-函数将获取加载程序锁,我必须稍后初始化线程。然而,和以前一样,当DLL_PROCESS_DETACH 被“调用”时,线程已经中止了。 DLL_THREAD_DETACH 不会在退出时被调用。

还有什么建议吗?谢谢!

【问题讨论】:

这不是C 代码所以请删除c 标签。 因为它是库的 C 接口,所以我认为没问题。现在已删除。 Visual Studio 2013 DLL 调用不导出 C++ 语义。使用 fdwReason = DLL_PROCESS_DETACH 查看 DllMain 进行 DLL 清理。 @RyanBemrose 我没有导出任何 C++ 语义(只是一个 C 函数)。在线程中止之前不会分离 DLL。当线程中止时,全局实例 cthread 仍然可用。您能否给我一些关于 DLL 清理的附加信息/cmets?谢谢! 【参考方案1】:

我能够使用 VS2015 重现此行为。

问题在于std::unique_ptr&lt;CThread&gt; pthread; 是一个全局对象。指针的删除与main 线程的执行无关。 dll的全局数据和host exe的全局数据不会以可预测的方式相互同步。引入线程时还有更多复杂性,这些会在进程退出时停止。

正如您所指出的,将所有代码移动到单个 exe 允许全局数据在 main 线程上适当地同步。

要解决这个问题,您可以导出一个 RAII 样式的类来管理线程的执行,或者简单地提供一个“清除线程”或清理函数并将其导出;该函数将具有以下形式;

void cleanupData()

   cthread = nullptr; // block waiting for thread exit

为了进一步协助客户端代码,仍然可以提供支持这种“清除线程”的 RAII 类,但不需要从 dll 中导出。

客户端 RAII 可以根据需要搭载 std::unique_ptrstd::shared_ptr。最简单的形式如下:

struct Cleanup 
    Cleanup() = default;
    Cleanup(Cleanup const&) = delete;
    Cleanup& operator=(Cleanup const&) = delete;
    Cleanup(Cleanup&&) = delete;
    Cleanup& operator=(Cleanup&&) = delete;

    ~Cleanup()  cleanupData();  // clean up the threads...
;

这需要与开始使用或从 dll 导入的数据的生命周期相关联。

用作;


    auto cleanup = std::make_unique<Cleanup>();
    testFunc();
    // ...

顺便说一句 在析构函数中,在 join() 线程之前,测试以确保它是 joinable()


鉴于更新;这是否可以更改或改进,即一旦执行离开main,是否可以控制线程? TL;DR,没有。

“新旧事物”成名的Raymond Chen,引自here:

另一方面,当您退出main 线程时,C 运行时库会自动调用ExitProcess,而不管是否有任何工作线程仍处于活动状态。控制台程序的这种行为是由 C 语言强制要求的,它说 (5.1.2.2.3) “从对 main 函数的初始调用返回等效于使用 @987654338 返回的值调用退出函数@函数作为它的参数。” C++ 语言具有等效要求 (3.6.1)。据推测,C 运行时人员将此行为带到WinMain 以保持一致性。

ExitProcess() do 是什么意思?

...

    进程中的所有线程(调用线程除外)都会终止它们的执行,而不会收到DLL_THREAD_DETACH 通知。 在步骤 1 中终止的所有线程的状态都变为信号状态。 所有加载的动态链接库 (DLL) 的入口点函数都使用 DLL_PROCESS_DETACH 调用。

...

特别是上面的第 1 点和第 3 点,当std::unique_ptr&lt;CThread&gt; 的析构函数运行时,线程已经停止并发出信号。一旦主线程调用ExitProcess(),您就不能依赖后台线程执行,它们已经停止并且操作系统正在清理与该进程相关的所有资源。


您在 cmets 中提到;

我将在线程中使用 IPC 互斥体,因此正确清理非常重要。

如果问题是互斥锁而不仅仅是线程,那会稍微改变问题。然后将互斥锁提取到全局级别并允许DllMain(使用DLL_PROCESS_ATTACH 等)管理它会更容易。线程将被允许正常访问。可能有一些额外的代码来表示互斥锁的状态,但也可以使用 dll 进行管理。

请记住,IPC 互斥机制通常包含一个“已放弃”状态来实现此目的。如果互斥锁的持有者意外失败并放弃互斥锁,则在尝试访问互斥锁时会通知该互斥锁的其余客户端。

来自WIN32 Mutex:

如果线程在没有释放其对互斥对象的所有权的情况下终止,则认为互斥对象已被放弃。等待线程可以获取被放弃的互斥对象的所有权,但是等待函数会返回WAIT_ABANDONED,表示该互斥对象被放弃了……

【讨论】:

感谢您的帮助并验证相同的问题是否可以在 VS2015 上重现。我在这里展示的代码只是库的一小部分,只是用来说明问题。在“真实”代码中,我有一个 Open() 和一个 Close() 函数来清理资源。但是,我希望我可以自动清理资源,而无需强制客户端确保他们调用了 Close() 函数(如果他们忘记了或者程序会崩溃)。你能给我一些关于这样一个 RAII 风格的课程的指导吗?感谢您的帮助! 它们有几个选项,unique_ptrshared_ptr 等。但最简单的方法是在实例与生命周期相关的类的析构函数中调用清理或关闭函数从 dll 导入的数据。您也可以将其与shared_ptr 结合使用,以实现“最后一个关灯”类型的方法。 另外,为什么dll中的全局数据与可执行文件不同步?据我所见,全局对象的解构函数将在调用 return() 后被破坏,正如预期的那样,但是全局对象中的线程已经被终止。你有什么想法吗? 我不确定当进程开始退出时在 dll 中运行的线程的所有细节,IIRC,当进程退出时它们被操作系统“中止”;所以基本上他们的执行不再受到尊重。 感谢您的建议@Niall,但问题仍然是我必须依靠客户进行适当的清理。据我了解,dll应该可以清理资源(因为它实际上适用于可执行文件)。是否可以“改善”数据的同步?【参考方案2】:

似乎由于某种原因,当 main 返回并调用 exit() 时,线程被自动杀死/终止。

这是exit 函数的预期行为。 IE。它终止整个进程及其所有线程。

在库中将有一个对象的全局实例,其成员为std::thread

看起来这个对象在编译成.dll 时没有创建。您可能需要显式导出该对象,以便它不会被优化掉,因为 .dll 中没有任何内容引用它,例如:

__declspec(dllexport) std::unique_ptr<CThread> cthread;

【讨论】:

我目前无法测试您的解决方案,但是我有一些 cmets/问题:我知道退出行为将终止进程和线程,但是线程在解构之前已经终止/杀死调用 CThread (我希望它在解构函数之后终止,当它不是库时会终止)。此外,如果我在 main 函数的 return 之前放置一个断点,我可以看到线程正在运行(它在 infiniteLoop() 函数中)。这不应该暗示对象被创建了吗? 是的,这是我正在测试的 dll 版本。我希望上面的代码在无限循环中运行,这不适用于 dll 版本。不幸的是,明确导出对象对我没有帮助。

以上是关于线程在完成执行代码之前在动态库中中止的主要内容,如果未能解决你的问题,请参考以下文章

链接器如何在剥离的动态库中定位代码?

库的简介和分类

Android 逆向Android 进程注入工具开发 ( 注入代码分析 | 获取注入的 libbridge.so 动态库中的 load 函数地址 并 通过 远程调用 执行该函数 )

Windows静态库和动态库区别

Android 逆向Android 进程注入工具开发 ( 注入代码分析 | 远程调用 目标进程中 libc.so 动态库中的 mmap 函数 三 | 等待远程函数执行完毕 | 寄存器获取返回值 )(代

linux上动态链接期间符号的替代实现