Dll堆疊問題(Dll的靜態變數與全域性變數、vs的MT與MD)

人类观察者發表於2024-03-21

問題引入:
dll有一個匯出函式,函式引數是string&,string在函式內部被=賦值。在exe動態載入此dll,呼叫此匯出函式後,會崩潰。

原因:
如果任何STL類的實現中使用了靜態變數(我們無從得知但map、string存在此問題),且編譯dll時,vc的執行庫設定為MT或MTd,會靜態連結VC的執行時庫,這會導致採用靜態連結的方式將導致生成的目標模組擁有獨立的堆疊空間。即此靜態變數在dll中有獨立的一份,在任何載入此dll的exe中,也有獨立的一份。

由於"誰申請,誰釋放"的原則,如果按照上面的情景,由於string在dll的函式內部被賦值,因此是由dll去申請的記憶體。但是因為string&傳遞了出去,在exe中使用,是由exe釋放的string。因此會導致崩潰。

解決方法:
方法1.如果任何STL類內部使用了靜態變數(無論是直接還是間接使用),那麼就不要再寫出跨執行單元訪問它的程式碼。
方法2.對於跨模組使用string&,改為使用WCHAR*
方法3.編譯dll時,修改vc的執行庫為MD或MDd,這樣堆疊空間就是共享的了。


/MD 與 /MT、/MTd與/MDd的區別

1./MD 與 /MT 用於Release 版本,前者表示連結時,不連結VC的執行時庫(msvcrt.lib),而採用動態庫(msvcrtXX.dll,其中XX表示使用的版本);相應地,後者則表示靜態連結VC的執行時庫,這樣的結果是連結生成的的目標模組體積明顯比前者要大一些。

2、/MDD與/MTD 用於Debug版本,其它規則同上。

3、除了在是動、靜態連結VC執行時庫上有區別,另外的區別點在於,採用靜態連結的方式將導致生成的目標模組擁有獨立的堆疊空間,如果生成的是DLL,那意味著呼叫該DLL的EXE程式與該DLL有著不同的堆疊空間,如果發生了EXE拿到了在DLL中分配記憶體建立的物件,在EXE對其進行析構時,就會導致記憶體非法訪問,出現類似於“ windows已在XX.exe中觸發一個斷點... ...”的錯誤。所以,儘量不要使用 /MT與/MTD進行靜態執行時庫連結的方式,即使要使用,也一定要遵循“誰申請,誰釋放”的原則。但是該原則在使用類時很難遵循,因為類中可能會有申請記憶體的動作。

4、採用第1點靜態連結時,如果生成的模組拿給別人使用,別人若使用了不同版本的編譯器,則會在連結時產生一系列問題,比如經常需要手動忽略 msvcrt.lib這個庫。具體會導致的問題此處不做研究。

5.另外,多模組程式的記憶體空間很值得推敲研究。但Linux下貌似不存在這些問題。


DLL的全域性變數與靜態變數

微軟解釋:https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-data

在 DLL 原始碼檔案中宣告為全域性的變數被編譯器和連結器視為全域性變數,如果一個exe載入了此dll,那麼這個全域性變數對於exe和dll來說是相同的;但載入給定 DLL 的每個程序都會獲得該 DLL 全域性變數的自己的副本,這個全域性變數對於不同程序是不同的。

靜態變數的作用域僅限於宣告靜態變數的塊內,靜態全域性變數(函式)在本編譯單元之外不可見。如果某個編譯單元exe...(目標檔案)包含了dll的這個.h檔案,那麼他會獲得一份此靜態全域性變數的副本。這兩份靜態全域性變數是不一樣的。

因此,預設情況下,每個程序都有自己的 DLL 全域性變數和靜態變數例項。


當然我們可以使用共享資料段在不同程序間共享dll的全域性變數,當然我們必須明確限制同時訪問,例如使用命名互斥鎖。

動態連結庫
#ifdef MYDLL_EXPORTS
    define MYDLL_API __declspec(dllexport)
#else
    define MYDLL_API __declspec(dllimport)
#endif

MYDLL_API extern int latchCounter;


DLL檔案
#include "dll.h"

#pragma data_seg(".shared")
int latchCounter = 0;        // 定義在共享資料段的全域性變數,可以在不同程序中共享

#pragma data_seg()
// the 'S' here is the key to mark the ".shared" data segment as shared
#pragma comment(linker, "/SECTION:.shared,RWS")

主程式
#include <type_traits>
#include <memory>
#include <Windows.h>
#include "dll.h"

template<auto f>

using fn_constant = std::integral_constant<decltype(f), f>;

using handle_ptr = std::unique_ptr<void, fn_constant<&CloseHandle>>;

int main() 
{
    // increase latchCounter
    {
        handle_ptr mx{ CreateMutex(nullptr, false, L"MyLatchCounterMutex") };
 
        WaitForSingleObject(mx.get(), INFINITE);
        ++latchCounter;
        ReleaseMutex(mx);
    }

    // decrease latchCounter
    {
        handle_ptr mx{ CreateMutex(nullptr, false, L"MyLatchCounterMutex") };
 
        WaitForSingleObject(mx.get(), INFINITE);
 
        if (--latchCounter == 0) 
        {
            // do something
        }
        ReleaseMutex(mx);
    }
}

測試上面理論的用例:

//dll.h:

#include <tchar.h>
#include <iostream>
#include <vector>

#ifdef MATHLIB_EXPORT
#define MATHLIBAPI __declspec(dllexport)
#else
#define MATHLIBAPI __declspec(dllimport)
#endif

extern MATHLIBAPI int i;        // 匯出全域性變數,在同一個程序中是可見的

//extern static int iStatic;    // 提示錯誤,不能匯出,因為靜態全域性在本編譯單元之外不可見。
static int i = 1;    // dll中的靜態全域性變數(函式)在本編譯單元之外不可見,
                     // 如果某個編譯單元exe...(目標檔案)包含了dll的這個.h檔案,那麼他會獲得一份此靜態全域性變數的副本。

//dll.cpp:

int i = 1;

extern "C" __declspec(dllexport) void APIENTRY DllTestStatic(std::vector<std::wstring>& vecStr);

extern "C" __declspec(dllexport) void APIENTRY DllTestStatic2();

__declspec(dllexport) void APIENTRY DllTestStatic(std::vector<std::wstring>& vecStr)
{
    vecStr.push_back(_T("11111"));    // 申請wstring是在dll內部申請的
}

__declspec(dllexport) void APIENTRY DllTestStatic2()
{
    wprintf(_T("全域性變數 dll_iInt = %d\n"), i);

    wprintf(_T("靜態全域性變數 dll_iStatic = %d\n"), iStatic);
}


EXE的程式碼:

#include <vector>
#include "../Dll_Static/dllexprot.h"    // 實驗3、4

//typedef void(*FuncDllTestStatic)(std::vector<std::wstring>& vecStr);    // 實驗1
//typedef void(*FuncDllTestStatic2)();    // 實驗4

/*
  // 實驗1
  // 測試dll動態載入,傳遞string& - 設定MD執行庫的dll不會崩潰,設定MT的會崩潰

  // 崩潰原因:
  // 採用(MT)靜態連結的方式連結msvcrt.lib將導致生成的目標模組dll擁有獨立的堆疊空間,和exe不是一個堆疊空間
  // 由於stl中很多都具有static變數,這樣申請wstring是在dll內部申請的,但是釋放卻是在exe中。導致崩潰。

  std::vector<std::wstring> vecStr;
  HMODULE ModuleBase = LoadLibrary(_T("Dll_Static.dll"));
  FuncDllTestStatic pDllTestStatic = (FuncDllTestStatic)GetProcAddress(ModuleBase, "DllTestStatic");
  pDllTestStatic(vecStr);
*/

//------------------------------------------------------------------------------------------

/*
    // 實驗2
    // 測試dll隱式連結,傳遞string& - 設定MD執行庫的dll不會崩潰,設定MT的會崩潰,原因如上
    std::vector<std::wstring> vecStr;
    DllTestStatic(vecStr);
*/

//------------------------------------------------------------------------------------------

/*
    // 實驗3
    // 測試dll隱式連結 - 靜態全域性變數是不可見的(不管MD還是MT)
    DllTestStatic2();        // 列印1
    iStatic++;            // exe中的static變數變為了2
    DllTestStatic2();        // 列印1
*/

//------------------------------------------------------------------------------------------

/*
    // 實驗4
    // 測試動態連結dll - 靜態全域性變數是不可見的(不管MD還是MT)
    HMODULE ModuleBase = LoadLibrary(_T("Dll_Static.dll"));
    FuncDllTestStatic2 pDllTestStatic2 = (FuncDllTestStatic2)GetProcAddress(ModuleBase, "DllTestStatic2");
    pDllTestStatic2();        // 列印1
    iStatic++;            // exe中的static變數變為了2
    pDllTestStatic2();        // 列印1
*/

//------------------------------------------------------------------------------------------

    // 實驗5
    // 測試dll匯出的全域性變數,在同一個程序中是可見的
    HMODULE ModuleBase = LoadLibrary(_T("Dll_Static.dll"));
    FuncDllTestStatic2 pDllTestStatic2 = (FuncDllTestStatic2)GetProcAddress(ModuleBase, "DllTestStatic2");
    pDllTestStatic2();        // 列印1
    i++;                // exe中的全域性變數變為了2
    pDllTestStatic2();        // 列印2

相關文章