運行時庫是個很復雜的東西,作為開發過程中dll制作需要了解的一部分,這裡主要簡單介紹一下如何選擇編譯選項。
在我們的開發過程中時常會遇到這樣的問題:
1. 我的VS版本比較高(比如:VS2012),我想制作一個dll,封裝了幾個函數給別人用。
2. 打包後發現我的dll引用了msvcr110.dll或者msvcr110d.dll,這個dll別人電腦可能沒有。
3. 於是別人使用時出現了諸如:“無法在DLL“XXXX.dll”中找到名為“XXXX()”的入口點”等問題。
最終結果就是,反復檢查發現都沒有錯,用工具查看也發現函數確實已經導出了,但是別人就沒法用。
這裡可能就需要對編譯選項進行修改了。
在VS中打開:項目屬性——>配置屬性——>C/C++——>代碼生成——>運行時。其中可以看到多個選項,如下圖所示:
選項 說明 /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版本,編譯會不通過。
動態鏈接的運行時庫,此時將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,如下圖:
#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是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,程序就會報錯,指出堆棧調用不對稱:
PS:寫這個東西真的好費勁,上次寫到現在好久了,有好多想記錄一下的東西,發現自己懶得寫,好懶!