原文地址: https://blog.csdn.net/xirongxu_dlut/article/details/53185473
什么是DLL(動(dòng)態(tài)鏈接庫)?
DLL是一個(gè)包含可由多個(gè)程序同時(shí)使用的代碼和數(shù)據(jù)的庫。例如:在Windows操作系統(tǒng)中,Comdlg32 DLL執(zhí)行與對話框有關(guān)的常見函數(shù)。因此,每個(gè)程序都可以使用該DLL中包含的功能來實(shí)現(xiàn)“打開”對話框。這有助于促進(jìn)代碼重用和內(nèi)存的有效使用。這篇文章的目的就是讓你一次性就能了解和掌握DLL。
為什么要使用DLL(動(dòng)態(tài)鏈接庫)?
代碼復(fù)用是提高軟件開發(fā)效率的重要途徑。一般而言,只要某部分代碼具有通用性,就可以將它構(gòu)造成相對獨(dú)立的功能模塊并在之后的項(xiàng)目中重復(fù)使用。比較常見的例子是各種應(yīng)用程序框架,它們都以源代碼的形式發(fā)布。由于這種復(fù)用是源代碼級別的,源代碼完全暴露給了程序員,因而稱之為“白盒復(fù)用”。白盒復(fù)用有以下三個(gè)缺點(diǎn):
暴露源代碼,多份拷貝,造成存儲(chǔ)浪費(fèi);
容易與程序員的本地代碼發(fā)生命名沖突;
-
更新模塊功能比較困難,不利于問題的模塊化實(shí)現(xiàn);
為了彌補(bǔ)這些不足,就提出了“二進(jìn)制級別”的代碼復(fù)用了。使用二進(jìn)制級別的代碼復(fù)用一定程度上隱藏了源代碼,對于“黑盒復(fù)用”的途徑不只DLL一種,靜態(tài)鏈接庫,甚至更高級的COM組件都是。
使用DLL主要有以下優(yōu)點(diǎn):
- 使用較少的資源;當(dāng)多個(gè)程序使用同一函數(shù)庫時(shí),DLL可以減少在磁盤和物理內(nèi)存中加載的代碼的重復(fù)量。這不僅可以大大影響在前臺(tái)運(yùn)行的程序,而且可以大大影響其它在Windows操作系統(tǒng)上運(yùn)行的程序;
- 推廣模塊式體系結(jié)構(gòu);
- 簡化部署與安裝。
創(chuàng)建DLL
打開Visual Studio 2012,創(chuàng)建如下圖的工程:
輸入工程名字,單擊[OK];
單擊[Finish],工程創(chuàng)建完畢了。現(xiàn)在,我們就可以在工程中加入我們的代碼了。加入MyCode.h和MyCode.cpp兩個(gè)文件;在MyCode.h中輸入以下代碼:
#ifndef _MYCODE_H_
#define _MYCODE_H_
#ifdef DLLDEMO1_EXPORTS
#define EXPORTS_DEMO _declspec( dllexport )
#else
#define EXPORTS_DEMO _declspec(dllimport)
#endif
extern "C" EXPORTS_DEMO int Add (int a , int b);
#endif
在MyCode.cpp中輸入以下代碼:
#include "stdafx.h"
#include "MyCode.h"
int Add ( int a , int b )
{
return ( a + b );
}
編譯工程,就會(huì)生成DLLDemo1.dll文件。在代碼中,很多細(xì)節(jié)的地方,我稍后進(jìn)行詳細(xì)的講解。
使用DLL
當(dāng)我們的程序需要使用DLL時(shí),就需要去加載DLL,在程序中加載DLL有兩種方法,分別為加載時(shí)動(dòng)態(tài)鏈接和運(yùn)行時(shí)動(dòng)態(tài)鏈接。
- 在加載時(shí)動(dòng)態(tài)鏈接中,應(yīng)用程序像調(diào)用本地函數(shù)一樣對導(dǎo)出的DLL函數(shù)進(jìn)行顯示調(diào)用。要使用加載時(shí)動(dòng)態(tài)鏈接,需要在編譯和鏈接應(yīng)用程序時(shí)提供頭文件和導(dǎo)入庫文件(.lib)。當(dāng)這樣做的時(shí)候,鏈接器將向系統(tǒng)提供加載DLL所需的信息,并在加載時(shí)解析導(dǎo)出的DLL函數(shù)的位置;
- 在運(yùn)行時(shí)動(dòng)態(tài)鏈接中,應(yīng)用程序調(diào)用LoadLibrary函數(shù)或LoadLibraryEx函數(shù)以在運(yùn)行時(shí)加載DLL。成功加載DLL后,可以使用GetProcAddress函數(shù)獲得要調(diào)用的導(dǎo)出的DLL函數(shù)的地址。在使用運(yùn)行時(shí)動(dòng)態(tài)鏈接時(shí),不需要使用導(dǎo)入庫文件。
在實(shí)際編程時(shí)有兩種使用DLL的方法,那么到底應(yīng)該使用那一種呢?在實(shí)際開發(fā)時(shí),是基于以下幾點(diǎn)進(jìn)行考慮的:
- 啟動(dòng)性能如果應(yīng)用程序的初始啟動(dòng)性能很重要,則應(yīng)使用運(yùn)行時(shí)動(dòng)態(tài)鏈接;
- 易用性在加載時(shí)動(dòng)態(tài)鏈接中,導(dǎo)出的DLL函數(shù)類似于本地函數(shù),我們可以方便地進(jìn)行這些函數(shù)的調(diào)用;
- 應(yīng)用程序邏輯在運(yùn)行時(shí)動(dòng)態(tài)鏈接中,應(yīng)用程序可以分支,以便按照需要加載不同的模塊。
下面,我將分別使用兩種方法調(diào)用DLL動(dòng)態(tài)鏈接庫。
加載時(shí)動(dòng)態(tài)鏈接:
#include <windows.h>
#include <iostream>
//#include "..\\DLLDemo1\\MyCode.h"
using namespace std;
#pragma comment(lib, "..\\debug\\DLLDemo1.lib")
extern "C" _declspec(dllimport) int Add(int a, int b);
int main(int argc, char *argv[])
{
cout<<Add(2, 3)<<endl;
return 0;
}
運(yùn)行時(shí)動(dòng)態(tài)鏈接:
#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{
HMODULE hDll = LoadLibrary("DLLDemo1.dll");
if (hDll != NULL)
{
AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");
if (add != NULL)
{
cout<<add(2, 3)<<endl;
}
FreeLibrary(hDll);
}
}
上述代碼都在DLLDemo1工程中。(工程下載)。
DllMain函數(shù)
Windows在加載DLL時(shí),需要一個(gè)入口函數(shù),就像控制臺(tái)程序需要main函數(shù)一樣。有的時(shí)候,DLL并沒有提供DllMain函數(shù),應(yīng)用程序也能成功引用DLL,這是因?yàn)閃indows在找不到DllMain的時(shí)候,系統(tǒng)會(huì)從其它運(yùn)行庫中引入一個(gè)不做任何操作的默認(rèn)DllMain函數(shù)版本,并不意味著DLL可以拋棄DllMain函數(shù)。
根據(jù)編寫規(guī)范,Windows必須查找并執(zhí)行DLL里的DllMain函數(shù)作為加載DLL的依據(jù),它使得DLL得以保留在內(nèi)存里。這個(gè)函數(shù)并不屬于導(dǎo)出函數(shù),而是DLL的內(nèi)部函數(shù),這就說明不能在客戶端直接調(diào)用DllMain函數(shù),DllMain函數(shù)是自動(dòng)被調(diào)用的。
DllMain函數(shù)在DLL被加載和卸載時(shí)被調(diào)用,在單個(gè)線程啟動(dòng)和終止時(shí),DllMain函數(shù)也被調(diào)用。參數(shù)ul_reason_for_call指明了調(diào)用DllMain的原因,有以下四種情況:
DLL_PROCESS_ATTACH:當(dāng)一個(gè)DLL被首次載入進(jìn)程地址空間時(shí),系統(tǒng)會(huì)調(diào)用該DLL的DllMain函數(shù),傳遞的ul_reason_for_call參數(shù)值為DLL_PROCESS_ATTACH。這種情況只有首次映射DLL時(shí)才發(fā)生;
DLL_THREAD_ATTACH:該通知告訴所有的DLL執(zhí)行線程的初始化。當(dāng)進(jìn)程創(chuàng)建一個(gè)新的線程時(shí),系統(tǒng)會(huì)查看進(jìn)程地址空間中所有的DLL文件映射,之后用DLL_THREAD_ATTACH來調(diào)用DLL中的DllMain函數(shù)。要注意的是,系統(tǒng)不會(huì)為進(jìn)程的主線程使用值DLL_THREAD_ATTACH來調(diào)用DLL中的DllMain函數(shù);
DLL_PROCESS_DETACH:當(dāng)DLL從進(jìn)程的地址空間解除映射時(shí),參數(shù)ul_reason_for_call參數(shù)值為DLL_PROCESS_DETACH。當(dāng)DLL處理DLL_PROCESS_DETACH時(shí),DLL應(yīng)該處理與進(jìn)程相關(guān)的清理操作。如果進(jìn)程的終結(jié)是因?yàn)橄到y(tǒng)中有某個(gè)線程調(diào)用了TerminateProcess來終結(jié)的,那么系統(tǒng)就不會(huì)用DLL_PROCESS_DETACH來調(diào)用DLL中的DllMain函數(shù)來執(zhí)行進(jìn)程的清理工作。這樣就會(huì)造成數(shù)據(jù)丟失;
DLL_THREAD_DETACH:該通知告訴所有的DLL執(zhí)行線程的清理工作。注意的是如果線程的終結(jié)是使用TerminateThread來完成的,那么系統(tǒng)將不會(huì)使用值DLL_THREAD_DETACH來執(zhí)行線程的清理工作,這也就是說可能會(huì)造成數(shù)據(jù)丟失,所以不要使用TerminateThread來終結(jié)線程。以上所有講解在工程DLLMainDemo(工程下載)都有體現(xiàn)。
函數(shù)導(dǎo)出方式
在DLL的創(chuàng)建過程中,我使用的是_declspec( dllexport )方式導(dǎo)出函數(shù)的,其實(shí)還有另一種導(dǎo)出函數(shù)的方式,那就是使用導(dǎo)出文件(.def)。你可以在DLL工程中,添加一個(gè)Module-Definition File(.def)文件。.def文件為鏈接器提供了有關(guān)被鏈接器程序的導(dǎo)出、屬性及其它方面的信息。
對于上面的例子,.def可以是這樣的:
LIBRARY "DLLDemo2"
EXPORTS
Add @ 1 ;Export the Add function
Module-Definition File(.def)文件的格式如下:
LIBRARY語句說明.def文件對應(yīng)的DLL;
EXPORTS語句后列出要導(dǎo)出函數(shù)的名稱。可以在.def文件中的導(dǎo)出函數(shù)名后加@n,表示要導(dǎo)出函數(shù)的序號為n(在進(jìn)行函數(shù)調(diào)用時(shí),這個(gè)序號有一定的作用)。
使用def文件,生成了DLL,客戶端調(diào)用代碼如下:
#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{
HMODULE hDll = LoadLibrary("DLLDemo2.dll");
if (hDll != NULL)
{
AddFunc add = (AddFunc)GetProcAddress(hDll, MAKEINTRESOURCE(1));
if (add != NULL)
{
cout<<add(2, 3)<<endl;
}
FreeLibrary(hDll);
}
}
可以看到,在調(diào)用GetProcAddress函數(shù)時(shí),傳入的第二個(gè)參數(shù)是MAKEINTRESOURCE(1),這里面的1就是def文件中對應(yīng)函數(shù)的序號。(工程下載)
extern “C”
為什么要使用extern “C”呢?C++之父在設(shè)計(jì)C++時(shí),考慮到當(dāng)時(shí)已經(jīng)存在了大量的C代碼,為了支持原來的C代碼和已經(jīng)寫好的C庫,需要在C++中盡可能的支持C,而extern “C”就是其中的一個(gè)策略。在聲明函數(shù)時(shí),注意到我也使用了extern “C”,這里要詳細(xì)的說說extern “C”。
extern “C”包含兩層含義,首先是它修飾的目標(biāo)是”extern”的;其次,被它修飾的目標(biāo)才是”C”的。先來說說extern;在C/C++中,extern用來表明函數(shù)和變量作用范圍(可見性)的關(guān)鍵字,這個(gè)關(guān)鍵字告訴編譯器,它申明的函數(shù)和變量可以在本模塊或其它模塊中使用。extern的作用總結(jié)起來就是以下幾點(diǎn):
- 在一個(gè)文件內(nèi),如果外部變量不在文件的開頭定義,其有效范圍只限定在從定義開始到文件的結(jié)束處。如果在定義前需要引用該變量,則要在引用之前用關(guān)鍵字”extern”對該變量做”外部變量聲明”,表示該變量是一個(gè)已經(jīng)定義的外部變量。有了這個(gè)聲明,就可以從聲明處起合理地使用該變量了,例如:
/*
** FileName : Extern Demo
** Author : Jelly Young
** Date : 2013/11/18
** Description : More information, please go to http://www.jellythink.com
*/
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
extern int a;
cout<<a<<endl;
}
int a = 100;
在多文件的程序中,如果多個(gè)文件都要使用同一個(gè)外部變量,不能在各個(gè)文件中各定義一個(gè)外部變量,否則會(huì)出現(xiàn)“重復(fù)定義”的錯(cuò)誤。正確的做法是在任意一個(gè)文件中定義外部變量,其它文件用extern對變量做“外部變量聲明”。在編譯和鏈接時(shí),系統(tǒng)會(huì)知道該變量是一個(gè)已經(jīng)在別處定義的外部變量,并把另一文件中外部變量的作用域擴(kuò)展到本文件,這樣在本文件就可以合法地使用該外部變量了。寫過MFC程序的人都知道,在在CXXXApp類的頭文件中,就使用extern聲明了一個(gè)該類的變量,而該變量的實(shí)際定義是在CXXXApp類的實(shí)現(xiàn)文件中完成的;
-
外部函數(shù),在定義函數(shù)時(shí),如果在最左端加關(guān)鍵字extern,表示此函數(shù)是外部函數(shù)。C語言規(guī)定,如果在定義時(shí)省略extern,則隱含為外部函數(shù)。而內(nèi)部函數(shù)必須在前面加static關(guān)鍵字。在需要調(diào)用此函數(shù)的文件中,用extern對函數(shù)作聲明,表明該函數(shù)是在其它文件中定義的外部函數(shù).
接著說”C”的含義。我們都知道C++通過函數(shù)參數(shù)的不同類型支持重載機(jī)制,編譯器根據(jù)參數(shù)為每個(gè)重載函數(shù)產(chǎn)生不同的內(nèi)部標(biāo)識(shí)符;但是,如果遇到了C++程序要調(diào)用已經(jīng)被編譯后的C函數(shù),那該怎么辦呢?比如上面的int Add ( int a , int b )函數(shù)。該函數(shù)被C編譯器后在庫中的名字為_Add,而C++編譯器則會(huì)生成像_Add_int_int之類的名字用來支持函數(shù)重載和類型安全。由于編譯后的名字不同,C++程序不能直接調(diào)用C函數(shù),所以C++提供了一個(gè)C連接交換指定符號extern “C”來解決這個(gè)問題;所以,在上面的DLL中,Add函數(shù)的聲明格式為:extern “C” EXPORTS_DEMO int Add (int a , int b)。這樣就告訴了C++編譯器,函數(shù)Add是個(gè)C連接的函數(shù),應(yīng)該到庫中找名字_Add,而不是找_Add_int_int。當(dāng)我們將上面DLL中的”C”去掉,編譯生成新的DLL,使用Dependency Walker工具查看該DLL,如圖:
3.png
請注意導(dǎo)出方式為C++,而且導(dǎo)出的Add函數(shù)的名字添加了很多的東西,當(dāng)使用這種方式導(dǎo)出時(shí),客戶端調(diào)用時(shí),代碼就是下面這樣:
#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{
HMODULE hDll = LoadLibrary("DLLDemo1.dll");
if (hDll != NULL)
{
AddFunc add = (AddFunc)GetProcAddress(hDll, "?Add@@YAHHH@Z");
if (add != NULL)
{
cout<<add(2, 3)<<endl;
}
FreeLibrary(hDll);
}
}
請注意GetProcAddress函數(shù)的第二個(gè)參數(shù),該參數(shù)名就是導(dǎo)出的函數(shù)名,在編碼時(shí),寫這樣一個(gè)名字是不是很奇怪啊。當(dāng)我們使用extern “C”方式導(dǎo)出時(shí),截圖如下:
注意導(dǎo)出方式為C,而且函數(shù)名現(xiàn)在就是普通的Add了。我們再使用GetProcAddress時(shí),就可以直接指定Add了,而不用再加那一長串奇怪的名字了。
DLL導(dǎo)出變量
DLL定義的全局變量可以被調(diào)用進(jìn)程訪問;DLL也可以訪問調(diào)用進(jìn)程的全局?jǐn)?shù)據(jù)。
參考上面的內(nèi)容,自己在VS2010開發(fā)環(huán)境上測試了一遍,測試步驟如下:
1.將所需要的函數(shù)封裝成DLL.
首先創(chuàng)建DLL工程項(xiàng)目,命名為DllDemo,如下圖:
然后創(chuàng)建頭文件(MyCode.h)和.cpp文件(MyCode.cpp),并分別添加代碼:
MyCode.h頭文件:
#ifndef _MYCODE_H_
#define _MYCODE_H_
#ifdef DLLDEMO1_EXPORTS
#define EXPORTS_DEMO _declspec( dllexport )
#else
#define EXPORTS_DEMO _declspec(dllimport)
#endif
extern "C" EXPORTS_DEMO int Add (int a , int b);
#endif
MyCode.cpp文件:
#include "MyCode.h"
int Add ( int a , int b )
{
return ( a + b );
}
編譯工程,就會(huì)在Debug文件下生成DllDemo.dll文件。
2.加載時(shí)動(dòng)態(tài)鏈接方式調(diào)用DLL.
首先創(chuàng)建控制臺(tái)應(yīng)用程序,命名為DllTest,如下圖所示:
然后添加代碼:
// DllTest.cpp : 定義控制臺(tái)應(yīng)用程序的入口點(diǎn)。
#include "stdafx.h"
#include <iostream>
//#include "..\\DLLDemo1\\MyCode.h"
using namespace std;
#pragma comment(lib, "..\\debug\\DllDemo.lib")
//****************************************問題1
extern "C" _declspec(dllimport) int Add(int a, int b);
int _tmain(int argc, _TCHAR* argv[])
{
cout<<Add(2, 3)<<endl;
while(1);//程序運(yùn)行到這,方便看運(yùn)行結(jié)果
return 0;
}
運(yùn)行結(jié)果如下圖:
注意:導(dǎo)入庫文件的目錄必須在本工程的目錄下,也就是說要把生成的dll和lib文件都要拷貝到該工程的目錄下,因?yàn)椴辉僭撃夸浵拢M管修改了路徑,仍然提示找不到DllDemo.dll,不知道為什么?
3.運(yùn)行時(shí)動(dòng)態(tài)鏈接方式調(diào)用DLL.
和第二步一樣,創(chuàng)建控制臺(tái)應(yīng)用程序,命名為DllTest1,添加代碼如下:
// DllTest1.cpp : 定義控制臺(tái)應(yīng)用程序的入口點(diǎn)。
//
#include "stdafx.h"
#include <iostream>
#include <windows.h>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int _tmain(int argc, _TCHAR* argv[])
{
HMODULE hDll = LoadLibrary(_T("DllDemo.dll"));
if (hDll != NULL)
{
AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");
if (add != NULL)
{
cout<<add(2, 3)<<endl;
}
FreeLibrary(hDll);
}
while(1);
}
運(yùn)行結(jié)果如下圖:
4.以.def文件(模塊定義文件)方式導(dǎo)出函數(shù)(非_declspec(dllexport)方式導(dǎo)出函數(shù)):
首先創(chuàng)建DLL工程項(xiàng)目,命名為DllDemo,如下圖:
然后創(chuàng)建頭文件(MyCode.h)和.cpp文件(MyCode.cpp),并分別添加代碼:
MyCode.h頭文件:
#ifndef _MYCODE_H_
#define _MYCODE_H_
extern "C" int Add (int a , int b);
#endif
MyCode.cpp文件:
#include "MyCode.h"
int Add ( int a , int b )
{
return ( a + b );
}
然后添加模塊定義文件(.def文件):
添加代碼:
LIBRARY "DllDemo" //這里的字符串名和工程名要一致
EXPORTS
Add @1;Export the Add function
編譯工程,即刻生成DllDemo.dll文件。
使用def文件,生成了DLL,客戶端調(diào)用代碼如下:
// DllTest2.cpp : 定義控制臺(tái)應(yīng)用程序的入口點(diǎn)。
//
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int _tmain(int argc, _TCHAR* argv[])
{
HMODULE hDll = LoadLibrary("DllDemo.dll");
if (hDll != NULL)
{
AddFunc add = (AddFunc)GetProcAddress(hDll, MAKEINTRESOURCE(1));
if (add != NULL)
{
cout<<add(2, 3)<<endl;
}
FreeLibrary(hDll);
}
while(1);
}
工程代碼下載:
1.生成動(dòng)態(tài)鏈接庫(_declspec(dllexport)方式導(dǎo)出函數(shù))
2.生成動(dòng)態(tài)鏈接庫(以.def文件(模塊定義文件)方式導(dǎo)出函數(shù))
3.以加載時(shí)動(dòng)態(tài)鏈接方式調(diào)用DLL
4.以運(yùn)行時(shí)動(dòng)態(tài)鏈接方式調(diào)用DLL
5.以模塊定義方式(.def文件)建立的動(dòng)態(tài)鏈接庫的調(diào)用
遇到的問題:
1.庫導(dǎo)入的時(shí)候目錄的問題。對應(yīng)文中的問題1,后面有解釋。
2.字符集的問題(是Unicode字符集還是多字節(jié)集),兩種方案,一種修改字符集為多字節(jié)集,二是將字符串前面加 _T(""),文中問題2
3.不知道怎么通過模塊定義文件方式生成DLL,通過看參考博客的代碼找到了答案,主要修改頭文件,和添加模塊定義文件。
4.模塊定義文件中的庫文件名應(yīng)和工程名一致。