【问题标题】:Linux & Windows: Is there a way to force SO/DLL to unload from memoryLinux & Windows:有没有办法强制 SO/DLL 从内存中卸载
【发布时间】:2021-08-22 16:52:16
【问题描述】:

所以我们正在处理 SO 和 DLL 文件。在dlclose() / FreeLibrary() 之后,SO/DLL 仍然在内存中,这是一个问题,因为将来可能会更新 SO/DLL 文件,并且在第二个 dlopen() / LoadLibrary() 之后是旧版本缓存在进程的内存中。在 Windows 上,这是一个更大的问题,因为在进程运行时无法删除 .DLL 文件。

在 Linux 上,我们的问题通过 GCC 的 -fno-gnu-unique 选项解决,效果很好。我们的问题是,我们不想依赖 SO 是如何编译的,主要是因为我们不是为我们的产品编写 SO/DLL 文件的团队,我们只是使用这些库。是否可以以“强制方式”卸载 SO?我的意思是,我们不应该依赖 SO 是否使用-fno-gnu-unique 编译。我还研究了如何将 .SO 文件映射到进程内存映射 (/proc/pid/maps),不幸的是从进程中取消映射 .SO 不起作用。

在 Windows 上我无法解决这个问题。 FreeLibrary() 返回 OK,但 GetModuleHandle() 表示 DLL 仍然存在于内存中。我还确定没有对 DLL 的引用,当不再需要时,每个指针都设置为 nullptr。我绑定了UnmapViewOfFile() 句柄,我可以在进程完成之前删除.DLL!但是我的进程在程序退出或我再次尝试LoadLibrary() 时崩溃。

关于 Stackoverflow/etc 的许多问题都与此有关,但我无法理解,为什么我们不能强制从进程中清除 SO/DLL。 GCC 的-fno-gnu-unique 选项中告诉了基本答案:因为STB_GNU_UNIQUE 对象应该存在直到程序终止。在 Linux 上,我可以使用 -fno-gnu-unique 覆盖此行为。我很好奇,为什么这在 Windows 上不可能(使用 MS Visual Studio 编译器)?我还在其他地方阅读了一条评论“DLL 不应该被卸载”,但我想知道为什么

谢谢。

示例

这是一个最小的例子:

DLL:

class Writer
{
public:
    Writer(int i) { std::cout << "Writer ctor" << std::endl; this->i = i; }
    ~Writer() { std::cout << "Writer dtor" << std::endl; }
    int get() { return i; }
    void set(int i) { this->i = i; }

private:
    int i;
};

std::thread* WorkerThread;

void proc()
{
    while (true)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "DLL: I'm alive" << std::endl;
    }
}

inline void __cdecl function()
{
    static Writer w(1);
    std::cout << "i=" << w.get() << std::endl;
    w.set(w.get() + 1);

    if (WorkerThread == nullptr)
    {
        WorkerThread = new std::thread(proc);
    }
}

用户:

#include <iostream>
#include <windows.h>
#include <libloaderapi.h>

typedef void (__cdecl* function_type)();

int main()
{
    while (1)
    {
        std::cout << "Enter to continue...";
        getchar();

        function_type function;
        HINSTANCE dll_handler;

        dll_handler = LoadLibraryA("power.dll");
        if (dll_handler == NULL)
        {
            std::cout << "Error: Unable to load dll" << std::endl;
            continue;
        }

        if ((function = (function_type)GetProcAddress(dll_handler, "function")) == NULL)
        {
            std::cout << "Error: Unable to find function 'initialize' entry point in dll" << std::endl;
        }
        else
        {
            function();
            std::cout << "function() returned " << std::endl;
        }

        std::cout << "Enter to FreeLibrary()...";
        getchar();
        int res = FreeLibrary(dll_handler);
        if (res == 0)
        {
            std::cout << "FreeLibrary() failed" << std::endl;
        }
        else
        {
            std::cout << "FreeLibrary() succeed" << std::endl;
        }
    }

    return 0;
}

我使用 Visual Studio 16 在 Windows 10 上编译。

一个典型的输出:

Enter to continue...
Writer ctor
i=1
function() returned
Enter to FreeLibrary()...DLL: I'm alive
DLL: I'm alive
DLL: I'm alive

FreeLibrary() succeed
Enter to continue...DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive

i=2
function() returned
Enter to FreeLibrary()...DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive
DLL: I'm alive

FreeLibrary() succeed
Enter to continue...DLL: I'm alive
DLL: I'm alive

i=3
function() returned
Enter to FreeLibrary()...DLL: I'm alive
DLL: I'm alive
DLL: I'm alive

FreeLibrary() succeed
Enter to continue...DLL: I'm alive

i=4
function() returned
Enter to FreeLibrary()...DLL: I'm alive
DLL: I'm alive
FreeLibrary() succeed
Enter to continue...Writer dtor
^C

DLL 启动一个线程,并且该线程不会停止。 “FreeLibrary()成功”后,power.dllpower_main.exe使用,无法删除,但程序在下一个LoadLibrary()之前等待,所以理论上DLL应该是可删除的。

static Writer 对象和 i= 打印输出表明该对象在调用 FreeLibrary() 时没有被释放。

我想实现在调用FreeLibrary()时杀死DLL启动的线程并从程序内存中清除DLL。当然,优秀的程序员不会喜欢强行停止线程的想法,因为它可能持有资源和/或以不一致的方式离开其状态。

我们的问题是我们的程序无法删除FreeLibrary() 之后的 DLL 文件,因为 DLL 以“错误”的方式编程。前面说过,我们加载的DLL是其他开发团队开发的,说实话,我们不关心DLL的东西是否因为其他开发团队的编程错误而保持不一致,所以我们想强行清除DLL如上所述,来自我们的程序。你怎么看,有可能吗?

谢谢。

【问题讨论】:

  • 请用更新后的代码编辑问题,而不是链接。

标签: linux windows dll shared-libraries


【解决方案1】:

此示例在我的系统上正常运行。我认为您在某处遇到了引用计数问题。您可以从输出中看到 DLL 确实卸载并切换到了第二个副本,它们甚至被加载到了相同的地址。

test1.c

__declspec(dllexport) const char* func(void) { return "foo"; }

test2.c

__declspec(dllexport) const char* func(void) { return "bar"; }

test.c

#include <Windows.h>
#include <stdio.h>

typedef char* (*FUNC)(void);

int main() {
    FUNC f;
    HMODULE h;
    BOOL result;

    result = CopyFile("test1.dll", "test.dll", FALSE);
    printf("Copyfile %d\n", result);
    h = LoadLibrary("test.dll");
    printf("h = %p\n", h);
    f = (FUNC)GetProcAddress(h, "func");
    printf("%s\n", f());
    result = FreeLibrary(h);
    printf("FreeLibrary %d\n", result);

    result = CopyFile("test2.dll", "test.dll", FALSE);
    printf("Copyfile %d\n", result);
    h = LoadLibrary("test.dll");
    printf("h = %p\n", h);
    f = (FUNC)GetProcAddress(h, "func");
    printf("%s\n", f());
    result = FreeLibrary(h);
    printf("FreeLibrary %d\n", result);
}

输出:

Copyfile 1
h = 00007FFD53890000
foo
FreeLibrary 1
Copyfile 1
h = 00007FFD53890000
bar
FreeLibrary 1

根据提供的最小示例进行编辑

尝试卸载时 OP 的 DLL 有一个正在运行的线程。正在运行的线程持有一个引用。如果在正在运行的线程上卸载进程会崩溃(如果您再调用一次FreeLibrary,则会崩溃)。

以下更简洁的代码演示了如果您停止线程,它就会工作。注释掉stop() 调用以查看DLL 无法卸载。这是有道理的。 LoadLibraryTHREAD_ATTACH 调用DllMainFreeLibraryTHREAD_DETACH 调用DllMain 等将启动和停止一个新线程。似乎 Windows 正在引用计数附加到 DLL 的线程数。

power.cpp - 编译成 power1.dllpower2.dll 并带有:

  • cl /EHsc /LD /W4 /DMESSAGE=power1 power.cpp /Fepower1.dll
  • cl /EHsc /LD /W4 /DMESSAGE=power2 power.cpp /Fepower2.dll
#include <iostream>
#include <chrono>
#include <thread>
#define API __declspec(dllexport)
#define MSG1(m) #m
#define MSG(m) MSG1(m)
using namespace std;

thread* WorkerThread;
bool run;

void proc() {
    while(run) {
        this_thread::sleep_for(chrono::milliseconds(1000));
        cout << "DLL: " << MSG(MESSAGE) << endl;
    }
}

extern "C" {
API void start() {
    run = true;
    if (WorkerThread == nullptr)
        WorkerThread = new thread(proc);
}

API void stop() {
    run = false;
    WorkerThread->join();
    delete WorkerThread;
    WorkerThread = nullptr;
}
}

power_main.cpp - 编译成 power_main.exe 并带有:

  • cl /EHsc /W4 power_main.cpp
#include <iostream>
#include <windows.h>

typedef void (__cdecl* function_type)();

int main() {
        function_type start;
        function_type stop;
        HINSTANCE dll_handler;

        CopyFile("power1.dll", "power.dll", FALSE);
        dll_handler = LoadLibraryA("power.dll");
        start = (function_type)GetProcAddress(dll_handler, "start");
        start();

        std::cout << "Enter to FreeLibrary()...";
        getchar();
        stop = (function_type)GetProcAddress(dll_handler, "stop");
        stop();  // Comment this out to show that the DLL won't switch
        FreeLibrary(dll_handler);

        CopyFile("power2.dll", "power.dll", FALSE);
        dll_handler = LoadLibraryA("power.dll");
        start = (function_type)GetProcAddress(dll_handler, "start");
        start();

        std::cout << "Enter to FreeLibrary()...";
        getchar();
        stop = (function_type)GetProcAddress(dll_handler, "stop");
        stop();
        FreeLibrary(dll_handler);
}

输出:

Enter to FreeLibrary()...DLL: power1

DLL: power1
Enter to FreeLibrary()...DLL: power2
DLL: power2

DLL: power2

stop() 被注释掉:

Enter to FreeLibrary()...DLL: power1
DLL: power1

Enter to FreeLibrary()...DLL: power1
DLL: power1

DLL: power1

【讨论】:

  • 耶,我忘了mentoin,DLL 应该“足够复杂”才能重现。我们有一个简单的加载程序和一个“足够复杂”的 DLL 来重现问题;明天我清除密码并带给你。
  • @user2148758 即便如此,这表明您有引用计数问题,即使“我也确定没有对 DLL 的引用”。每次调用 LoadLibrary 都需要一个匹配的 FreeLibrary。您可以多次调用 FreeLibrary,直到 DLL 实际卸载,以了解剩余的引用数量。
  • 我添加了一个最小的复制示例。
  • @user2148758 线程正在运行。你试过停止线程吗?
  • @user2148758 你必须停止线程。我添加了一个加入和删除线程的“停止”功能。停止之前,我无法删除 DLL,但停止后我可以。 std::thread 持有一个引用。在线程执行时卸载代码是“不好的”,你不觉得吗?事实上,为了证明在写“停止”之前有一个额外的引用,我只是第二次调用 FreeLibrary,进程就崩溃了。那是“坏”?。
【解决方案2】:

确定使用的是内存中的旧版本,而不是磁盘上的新版本吗? Unix (Linux) 机制(在启动时由ld.so 编排)本质上是将mmap(3) 共享库(可执行文件引用的文件)放入进程的虚拟内存中。如果文件是不同的文件,内存中的陈旧数据将毫无用处。当然,旧进程(在切换之前启动)将继续使用旧版本。任何以某种方式直接链接到旧版本的可执行文件(即,不是到 libxyz.so.2,现在是到 libxyz.so.2.3 的符号链接,而是直接到旧的 libxyz.so.2.1.7)仍然需要过时的版本。

【讨论】:

  • "您确定使用的是内存中的旧版本,而不是磁盘上的新版本吗?"是的,当调用“磁盘上较新版本的 SO/DLL”时,我看到“较旧的打印输出”。如果需要,我可以提供一个最小的示例,只需要一些时间来清除我们的代码。 “任何以某种方式直接链接到旧版本的可执行文件......”我们的过程没有直接链接到这些库。 .SO/.DLL文件由dlclose()/LoadLibrary()打开,当然在Linux上,ldd programname中没有提及
  • @user2148758,使用旧库启动程序。停止它,用新库替换,重新开始。 dlopen(3) 调用(绝对不是 dlclose(3))在调用时搜索共享库文件,它几乎无法链接到内存中的陈旧版本。
  • 在 SO/DLL 更新期间停止我们的程序不是一个选项。我们正在考虑将 SO/DLL 加载/卸载部分移动到一个单独的进程中,因为我们能够启动/停止子进程,但这不是一个优雅的解决方案。我只是对为什么不能强制卸载 DLL 感兴趣。明天我带来一个重现的例子。
  • @user2148758,那么显然已经加载的库将保留。
  • 我添加了一个最小的复制示例。
【解决方案3】:

@Mark Tolonen:谢谢你的回答,我很感激,但恐怕这不是我问的。

当然我知道线程应该在卸载之前停止。我们称之为“友好方式”。在我提供的示例中,我故意没有这样做。

但是我在最初的问题和评论中也指出,我们不是为我们的应用程序编写 DLL 的开发团队。当然,我们发布了一个 SDK,其中包含我们在 FreeLibrary() 之前调用的 DLL 中的 stop() 函数。老实说,DLL 开发团队的专有技术数量值得怀疑……所以我们不想依赖stop() 方法是否正确实现。我知道这听起来很奇怪,但我不想提供更多信息,为什么我们不信任提供给我们的 DLL。

我最初的问题是:好的……所以“友好的方式”不符合我们产品负责人的要求;是否有“强制方式”从进程中清除 DLL? (问题标题是:“有没有办法强制 SO/DLL 从内存中卸载?”)我的问题是理论上的:如果我承担责任,我们可以“强制方式”这样做吗?

根据我目前的知识,经过几天的研究,我会说答案是“否”——如果是,MS 将其隐藏得很好。

当然我理解背后的原因,例如,我们不想“强制”停止一个线程,因为它可能持有资源。

@Mark Tolonen:无论如何,感谢您在这个问题上所做的努力。 :)

【讨论】:

    猜你喜欢
    • 2010-11-28
    • 2010-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-19
    • 1970-01-01
    相关资源
    最近更新 更多