一、C\C++ 運行時庫編譯選項簡單說明
問題:我的dll別人沒法用
運行時庫是個很復雜的東西,作為開發過程中dll制作需要了解的一部分,這里主要簡單介紹一下如何選擇編譯選項。
在我們的開發過程中時常會遇到這樣的問題:
1. 我的VS版本比較高(比如:VS2012),我想制作一個dll,封裝了幾個函數給別人用。
2. 打包后發現我的dll引用了msvcr110.dll或者msvcr110d.dll,這個dll別人電腦可能沒有。
3. 於是別人使用時出現了諸如:“無法在DLL“XXXX.dll”中找到名為“XXXX()”的入口點”等問題。
最終結果就是,反復檢查發現都沒有錯,用工具查看也發現函數確實已經導出了,但是別人就沒法用。
這里可能就需要對編譯選項進行修改了。
解釋:如何避免上述問題
在VS中打開:項目屬性——>配置屬性——>C/C++——>代碼生成——>運行時。其中可以看到多個選項,如下圖所示:
在微軟的msdn中對CRT庫進行了簡單解釋:
https://msdn.microsoft.com/zh-cn/library/2kzt1wy3(VS.80).aspx
https://msdn.microsoft.com/zh-cn/library/abx4dbyh(v=vs.110).aspx
下面是我黏貼的表格:
選項 | 說明 |
/MD | 使應用程序使用運行時庫的多線程並特定於 DLL 的版本。定義 _MT 和 _DLL,並使編譯器將庫名 MSVCRT.lib 放入 .obj 文件中。 用此選項編譯的應用程序靜態鏈接到 MSVCRT.lib。該庫提供允許鏈接器解析外部引用的代碼層。實際工作代碼包含在 MSVCR80.DLL 中,該庫必須在運行時對於與 MSVCRT.lib 鏈接的應用程序可用。 當在定義了 _STATIC_CPPLIB (/D_STATIC_CPPLIB) 的情況下使用 /MD 時,它將導致應用程序與靜態多線程標准 C++ 庫 (libcpmt.lib) 而非動態版本 (msvcprt.lib) 鏈接,同時仍通過 msvcrt.lib 動態鏈接到主 CRT。 |
/MDd | 定義 _DEBUG、_MT 和 _DLL,並使應用程序使用運行時庫的調試多線程並特定於 DLL 的版本。它還使編譯器將庫名 MSVCRTD.lib 放入 .obj 文件中。 |
/MT | 使應用程序使用運行時庫的多線程靜態版本。定義 _MT 並使編譯器將庫名 LIBCMT.lib 放入 .obj 文件中,以便鏈接器使用 LIBCMT.lib 解析外部符號。 |
/MTd | 定義 _DEBUG 和 _MT。此選項還使編譯器將庫名 LIBCMTD.lib 放入 .obj 文件中,以便鏈接器使用 LIBCMTD.lib 解析外部符號。 |
/LD | 創建 DLL。 將 /DLL 選項傳遞到鏈接器。鏈接器查找 DllMain 函數,但並不需要該函數。如果沒有編寫 DllMain 函數,鏈接器將插入返回 TRUE 的DllMain 函數。 鏈接 DLL 啟動代碼。 如果命令行上未指定導出 (.exp) 文件,則創建導入庫 (.lib);將導入庫鏈接到調用您的 DLL 的應用程序。 將 /Fe(命名 EXE 文件)解釋為命名 DLL 而不是 .exe 文件;默認程序名成為 basename.dll 而不是 basename.exe。 除非顯式指定 /MD,否則將暗指 /MT。 |
/LDd | 創建調試 DLL。定義 _MT 和 _DEBUG。 |
就從VS的dll庫的編譯選項來說就前面四項,/MD、/MDd、/MT和/MTd。其中/MDd、/MTd后面的“d”表示編譯生成的是Debug版本,也就是用於編譯生成Debug版本的程序;不加“d”表示是Release版本的程序,如果此時你把項目配置成Debug版本,編譯會不通過。
動態鏈接多線程庫(MD/MDd)
動態鏈接的運行時庫,此時將msvcrt.lib安置到obj文件中,它連接到dll的方式是靜態鏈接,實際上工作的庫是msvcrxx.dll。所有的 C 庫函數保存在動態鏈接庫 msvcrXX.dll中, 由msvcrXX.dll處理多線程問題。也就是說,這種編譯方式下我們是通過msvcrXX.dll這個動態鏈接庫去鏈接CRT。
此時我們編譯的dll引用了msvcrXX.dll文件,這個文件在使用的時候必須能夠被計算機查詢到。比如:我的電腦編譯引用了msvcr110.dll,那么我的dll給別人調用的時候,對方計算機一定要有msvcr110.dll,否則dll庫就無法使用。因此這種編譯方式,必須將msvcr110.dll一同攜帶過去,並且要在對方計算機上進行注冊。
使用此方式編譯時,使用Depends查看,我們可以看到dll 引用了msvcr110.dll,如下圖:
靜態鏈接多線程庫(MT/MTd)
靜態鏈接多線程庫,此時編譯器把LIBCMT.lib 安置到OBJ文件中,讓鏈接器使用LIBCMT.lib 處理外部符號。這種方式說簡單點就是讓我們的dll能夠直接鏈接到CRT,無需引用msvcrXX.dll這個動態鏈接庫。這樣我們就不需要擔心對方電腦是否擁有對應版本的msvcrXX.dll,因此這是一種方便的好辦法。
在這種方式下,編譯的dll通過Depends查看就是這樣的:
注意:幾個注意點
光從上面來看,MT的方式很好用,但是必須要注意一個問題:
不要混合使用庫的靜態版本和動態版本。在一個進程中有多個庫副本會導致問題,因為副本中的靜態數據不與其他副本共享。(還應該避免在一個進程中混合使用這些庫的調試版本和非調試版本)。
還有,在實際使用中能用MD就用MD的方式,因為這種方式軟件更小,調用同一個dll時在內存中使用的是同一個副本。這就不會有堆空間釋放問題:
不同的模塊各自有一份C運行時庫代碼,各個C運行庫會有各自的堆,導致了各個模塊會有各自的堆。如果在A堆中申請空間,到B堆中釋放就會有崩潰,在模塊A申請的空間,必須在模塊A中釋放。
二、函數調用方式_cdecl與_stdcall的選擇
_stdcall調用
_stdcall是Pascal程序的缺省調用方式,參數采用從右到左的壓棧方式,被調函數自身在返回前清空堆棧。形式如下:
#define SW_REV extern "C" _declspec(dllexport) SW_REV int __stdcall add(int a, int b); int __stdcall add(int a, int b) { return a+b; }
_cdecl調用
_cdecl是C/C++的缺省調用方式,參數采用從右到左的壓棧方式,傳送參數的內存棧由調用者維護。_cedcl約定的函數只能被C/C++調用,每一個調用它的函數都包含清空堆棧的代碼,所以產生的可執行文件大小會比調用_stdcall函數的大。
在不加修飾的情況下,VC++默認使用這種調用方式,形式如下(默認,可省略):
#define SW_REV extern "C" _declspec(dllexport) SW_REV int add(int a, int b); int add(int a, int b) { return a+b; }
什么時候區分?
其實兩種方式都一樣,區別在於調用。例如:有些語言要求調用時只認_stdcall,那么就只能按照要求使用_stdcall版本的dll。很多情況下,調用方式都可選擇,說白了,就是一定要保持一致,例如C#調用上述_stdcall方式的dll:
[DllImport("SwLib.dll", EntryPoint = "add", CallingConvention = CallingConvention.StdCall)] private static extern int add(int a, int b);
這里CallingConvention需要選擇CallingConvention.StdCall,如果我將其改成 CallingConvention.Cdecl,程序就會報錯,指出堆棧調用不對稱: