原文地址: https://blog.csdn.net/qianchenglenger/article/details/21599235
動態鏈接庫是Windows的基石。所有的Win32 API函數都包含在DLL中。3個最重要的DLL是KERNEL32.DLL,它由管理內存、進程和線程的函數組成;USER32.DLL,它由執行用戶界面的任務(如創建窗口和發送消息)的函數組成;GDI32.DLL,它由繪圖和顯示文本的函數組成。在此,我們主要用實際的操作過程,簡要的說明如何創建自己的 Win32 DLL。
創建DLL工程
這里,我們為了簡要說明DLL的原理,我們決定使用最簡單的編譯環境VC6.0,如下圖,我們先建立一個新的Win32 Dynamic-Link Library工程,名稱為“MyDLL”,在Visual Studio中,你也可以通過建立Win32控制臺程序,然后在“應用程序類型”中選擇“DLL”選項,
點擊確定,選擇“一個空的DLL工程”,確定,完成即可。
一個簡單的dll
在第一步我們建立的工程中建立一個源碼文件”dllmain.cpp“,在“dllmain.cpp”中,鍵入如下代碼
#include <Windows.h>
#include <stdio.h>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf("DLL_PROCESS_ATTACH\n");
break;
case DLL_THREAD_ATTACH:
printf("DLL_THREAD_ATTACH\n");
break;
case DLL_THREAD_DETACH:
printf("DLL_THREAD_DETACH\n");
break;
case DLL_PROCESS_DETACH:
printf("DLL_PROCESS_DETACH\n");
break;
}
return TRUE;
}
之后,我們直接編譯,即可以在Debug文件夾下,找到我們生成的dll文件,“MyDLL.dll”,注意,代碼里面的printf語句,并不是必須的,只是我們用于測試程序時使用。而DllMain函數,是dll的進入/退出函數。
實際上,讓線程調用DLL的方式有兩種,分別是隱式鏈接和顯式鏈接,其目的均是將DLL的文件映像映射進線程的進程的地址空間。我們這里只大概提一下,不做深入研究,如果感興趣,可以去看《Window高級編程指南》的第12章內容。
隱式鏈接調用
隱士地鏈接是將DLL的文件影響映射到進程的地址空間中最常用的方法。當鏈接一個應用程序時,必須制定要鏈接的一組LIB文件。每個LIB文件中包含了DLL文件允許應用程序(或另一個DLL)調用的函數的列表。當鏈接器看到應用程序調用了某個DLL的LIB文件中給出的函數時,它就在生成的EXE文件映像中加入了信息,指出了包含函數的DLL文件的名稱。當操作系統加載EXE文件時,系統查看EXE文件映像的內容來看要裝入哪些DLL,而后試圖將需要的DLL文件映像映射到進程的地址空間中。當尋找DLL時,系統在系列位置查找文件映像。
1.包含EXE映像文件的目錄
2.進程的當前目錄
3.Windows系統的目錄
4.Windows目錄
5.列在PATH環境變量中的目錄
這種方法,一般都是在程序鏈接時控制,反映在鏈接器的配置上,網上大多數講的各種庫的配置,比如OPENGL或者OPENCV等,都是用的這種方法
顯式鏈接調用
這里我們只提到兩種函數,一種是加載函數
HINSTANCE LoadLibrary(LPCTSTR lpszLibFile);
HINSTANCE LoadLibraryEx(LPCSTR lpszLibFile,HANDLE hFile,DWORD dwFlags);
返回值HINSTANCE值指出了文件映像映射的虛擬內存地址。如果DLL不能被映進程的地址空間,函數就返回NULL。你可以使用類似于
LoadLibrary("MyDLL")
或者
LoadLibrary("MyDLL.dll")
的方式進行調用,不帶后綴和帶后綴在搜索策略上有區別,這里不再詳解。
顯式釋放DLL
在顯式加載DLL后,在任意時刻可以調用FreeLibrary函數來顯式地從進程的地址空間中解除該文件的映像。
BOOL FreeLibrary(HINSTANCE hinstDll);
這里,在同一個進程中調用同一個DLL時,實際上還牽涉到一個計數的問題。這里也不在詳解。
線程可以調用GetModuleHandle函數:
GetModuleHandle(LPCTSTR lpszModuleName);
來判斷一個DLL是否被映射進進程的地址空間。例如,下面的代碼判斷MyDLL.dll是否已被映射到進程的地址空間,如果沒有,則裝入它:
HINSTANCE hinstDll;
hinstDll = GetModuleHandle("MyDLL");
if (hinstDll == NULL){
hinstDll = LoadLibrary("MyDLL");
}
實際上,還有一些函數,比如 GetModuleFileName用來獲取DLL的全路徑名稱,FreeLibraryAndExitThread來減少DLL的使用計數并退出線程。具體內容還是參見《Window高級編程指南》的第12章內容,此文中不適合講太多的內容以至于讀者不能一下子接受。
DLL的進入與退出函數
說到這里,實際上只是講了幾個常用的函數,這一個小節才是重點。
在上面,我們看到的MyDLL的例子中,有一個DllMain函數,這就是所謂的進入/退出函數。系統在不同的時候調用此函數。這些調用主要提供信息,常常被DLL用來執行進程級或線程級的初始化和清理工作。如果你的DLL不需要這些通知,就不必再你的DLL源代碼中實現此函數,例如,如果你創建的DLL只含有資源,就不必實現該函數。但如果有,則必須像我們上面的格式。
DllMain函數中的ul_reason_for_call參數指出了為什么調用該函數。該參數有4個可能值: DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH、DLL_PROCESS_DETACH。
其中,DLL_PROCESS_ATTACH是在一個DLL首次被映射到進程的地址空間時,系統調用它的DllMain函數,傳遞的ul_reason_for_call參數為DLL_PROCESS_ATTACH。這只有在首次映射時發生。如果一個線程后來為已經映射進來的DLL調用LoadLibrary或LoadLibraryEx,操作系統只會增加DLL的計數,它不會再用DLL_PROCESS_ATTACH調用DLL的DllMain函數。
而DLL_PROCESS_DETACH是在DLL被從進程的地址空間解除映射時,系統調用它的DllMain函數,傳遞的ul_reason_for_call值為DLL_PROCESS_DETACH。我們需要注意的是,當用DLL_PROCESS_ATTACH調用DLL的DllMain函數時,如果返回FALSE,說明初始化不成功,系統仍會用DLL_PROCESS_DETACH調用DLL的DllMain。因此,必須確保沒有清理那些沒有成功初始化的東西。
DLL_THREAD_ATTACH:當進程中創建一個線程時,系統察看當前映射到進程的地址空間中的所有DLL文件映像,并用值DLL_THREAD_ATTACH調用所有的這些DLL的DllMain函數。該通知告訴所有的DLL去執行線程級的初始化。注意,當映射一個新的DLL時,進程中已有的幾個線程在運行,系統不會為已經運行的線程用值DLL_THREAD_ATTACH調用DLL的DllMain函數。
而DLL_THREAD_DETACH,如果線程調用ExitThread來終結(如果讓線程函數返回而不是調用ExitThread,系統會自動調用ExitThread),系統察看當前映射到進程空間的所有DLL文件映像,并用值DLL_THREAD_DETACH來調用所有的DLL的DllMain函數。該通知告訴所有的DLL去執行線程級的清理工作。
這里,我們需要注意的是,如果線程的終結是因為系統中的一個線程調用了TerminateThread,系統就不會再使用DLL_THREAD_DETACH來調用DLL和DllMain函數。這與TerminateProcess一樣,不再萬不得已時,不要使用。
下面,我們貼出《Window高級編程指南》中的兩個圖來說明上述四種參數的調用情況。
好的,介紹了以上的情況,下面,我們來繼續實踐,這次,建立一個新的空的win32控制臺工程TestDLL,不再多說,代碼如下:
#include <iostream>
#include <Windows.h>
using namespace std;
DWORD WINAPI someFunction(LPVOID lpParam)
{
cout << "enter someFunction!" << endl;
Sleep(1000);
cout << "This is someFunction!" << endl;
Sleep(1000);
cout << "exit someFunction!" << endl;
return 0;
}
int main()
{
HINSTANCE hinstance = LoadLibrary("MyDLL");
if(hinstance!=NULL)
{
cout << "Load successfully!" << endl;
}else {
cout << "Load failed" << endl;
}
HANDLE hThread;
DWORD dwThreadId;
cout << "createThread before " << endl;
hThread = CreateThread(NULL,0,someFunction,NULL,0,&dwThreadId);
cout << "createThread after " << endl;
cout << endl;
Sleep(3000);
cout << "waitForSingleObject before " << endl;
WaitForSingleObject(hThread,INFINITE);
cout << "WaitForSingleObject after " << endl;
cout << endl;
FreeLibrary(hinstance);
return 0;
}
代碼很好理解,但是前提是,你必須對線程有一定的概念。另外,注意,我們上面編譯的獲得的“MyDLL.dll"必須拷貝到能夠讓我們這個工程找到的地方,也就是上面我們提到的搜索路徑中的一個地方。
這里,我們先貼結果,當然,這只是在我機器上其中某次運行結果。
有了上面我們介紹的知識,這個就不是很難理解,主進程在調用LoadLibrary時,用DLL_PROCESS_ATTACH調用了DllMain函數,而線程創建時,用DLL_THREAD_ATTACH調用了DllMain函數,而由于主線程和子線程并行的原因,可能輸出的時候會有打斷。但是,這樣反而能讓我們更清楚的理解程序。
DllMain與C運行庫
”在前面對DllMain函數的討論中,我假設讀者使用Microsoft的Visual C++編譯器來建立自己的動態鏈接庫。當編寫DLL時,可能會需要一些C運行庫的啟動幫助。比方說,你建立的DLL中包含一個全局變量,它是一個C++類的實例。在DLL能使用該全局變量之前,必須調用了它的構造函數——這就是C運行時庫的DLL啟動代碼的工作。“
上面一段話也就是告訴我們,實際上,當DLL文件被映射到進程的地址空間中時,系統實際上調用的并不直接是DllMain函數,而是另外一個函數,需要先完成一些初始化工作,實際上,這個函數便是_DllMainCRTStartup函數。該函數初始化了C運行時庫,并確保當它接收到DLL_PROCESS_ATTACH通知時,所有的全局或靜態C++對象都被創建了。為了解釋這點,我們準備對以上的MyDLL.dll代碼進行一些修改,如下,其中增加了一個類A,以及定義了一個全局變量a。
#include <Windows.h>
#include <stdio.h>
class A{
public:
A(){
printf("A construct...");
}
~A(){
printf("A deconstruct...");
}
};
A a;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf("DLL_PROCESS_ATTACH\n");
break;
case DLL_THREAD_ATTACH:
printf("DLL_THREAD_ATTACH\n");
break;
case DLL_THREAD_DETACH:
printf("DLL_THREAD_DETACH\n");
break;
case DLL_PROCESS_DETACH:
printf("DLL_PROCESS_DETACH\n");
break;
}
return TRUE;
}
編譯為DLL后,替換掉原來的的MyDLL.dll,可以直接運行TestDLL.exe,可以看到,在DLL_PROCESS_ATTACH調用了類A的構造函數,而在DLL_PROCESS_DETACH之后,調用了類A 的析構函數。
在經過以上的證實之后,實際上,我們也可以理解為什么我們之前說不必在DLL的源代碼中實現一個DllMain函數,因為如果你沒有DllMain函數,C運行庫有它自己的一個DllMain函數。鏈接器鏈接DLL時,如果它在DLL的OBJ文件中找不到一個DllMain函數,就會鏈接C運行時的DllMain函數的實現。
從DLL中輸出函數和變量
當創建一個DLL時,實際上創建了一組能讓EXE或其他DLL調用的一組函數。當一個DLL函數能被EXE或另一個DLL文件使用時,它被稱為輸出了(exported)。
這里,我們只說一種方法,即用_declspec(dllexport) 的方法。當然,也可以用def文件,但是,我們最常用的還是 _declspec(dllexport)的方法。什么是輸出函數與輸出變量。簡單的來說,你開發一個dll之后,一般都是想讓別的程序員開發的應用程序或dll調用,而輸出變量就是為了完成這件事情的。想一想你在開發windows應用程序時調用的各種api,實際上,大部分也都是在dll中封起來的函數。廢話不多少,上代碼,我們還用以上的兩個工程,MyDLL工程和TestDLL工程,但是這次,大幅度修改了代碼,我們刪去了DllMain函數,增加了一個函數,和一個全局整形變量,同時也修改了類A,這次,我們準備先來點正常。
其中MyDLL工程中的代碼(注意,這里代碼沒有按照頭文件與源代碼的分離,僅僅為了更好理解知識,在工程項目中請勿模仿)
#include <Windows.h>
#include <stdio.h>
extern "C"{
class _declspec(dllexport) A{
public:
A(){
printf("A construct...\n");
}
const char * whoIsMe()
{
return "My name is A";
}
~A(){
printf("A deconstruct...\n");
}
};
_declspec(dllexport) A a;
_declspec(dllexport) int Add(int x,int y)
{
return x+y;
}
_declspec(dllexport) int g_nUsageCount = 3195;
}
這里需要注意的是 _declspec(dllexport) ,代表了dll導出的意思,編譯組建一下,你會發現,這次,我們得到的不在單單是一個dll文件,還有MyDLL.lib和MyDLL.exp文件,其中,這些文件的意思,請參見此文 .dll,.lib,.def 和 .exp文件
之后,我們這里決定先用上面提到的隱式鏈接的方法進行調用。我們需要先配置一下我們的TestDLL工程,配置方法如下,選擇“工程”->“設置”彈出一下窗口,選擇“鏈接”標簽頁,然后按照我們下面圈紅的部分,添加上“MyDLL.lib”文件以及相應的附加庫路徑(及lib所在的位置),這里我們為了方便起見,把MyDLL工程的Debug文件夾下生成的dll與lib均拷貝到了TestDLL的Debug文件夾下。
之后,修改TestDLL工程的源代碼如下,(這里,我們再次聲明,我們沒有用頭文件的方式,非常不建議這樣用。)
#include <Windows.h>
#include <stdio.h>
extern "C"{
_declspec(dllimport) int Add(int x,int y);
_declspec(dllimport) int g_nUsageCount;
class _declspec(dllimport) A{
public:
A();
const char * whoIsMe();
~A();
};
_declspec(dllimport) A a;
}
int main()
{
printf("%d\n",Add(5,3) );
printf("%d\n",g_nUsageCount);
printf("%s\n",a.whoIsMe());
printf("-----------------------------\n");
A b;
printf("%s\n",b.whoIsMe());
return 0;
}
這里,注意的是,在導出的位置,我們用的是_declspec(dllexport) ,而在這里導入的時候,我們聲明的時候,用的是 _declspec(dllimport) ,這個例子當中,我們分別導出了變量,函數,類。讀者僅僅需要注意的是導入和導出關鍵字的使用。運行結果如下:
另外,大家可能對為什么要用 extern "C"括起來表示好奇,這里可以先推后考慮,我們在說到如何顯式加載該文件時會提到。
這里,建議大家一下,如果將類A聲明部分的構造函數刪除,即改為
class _declspec(dllimport) A{
public:
const char * whoIsMe();
};
想想會發生什么,不妨動手試一下,這又是為什么?如果還理解,說明你可能對動態鏈接庫的lib文件理解不夠透徹,可以再讀一讀我們上面說的那篇文章 .dll,.lib,.def 和 .exp文件 。
到這里,實際上我們已經大致說完動態鏈接庫的相關內容,但是,既然我們上面提到了顯式調用,那么,想過沒有如果才能顯式調用我們現在的這個dll文件,還有,那個extern “C”到底是什么,這里,我們還是先推薦一篇文章, extern "C"的用法解析
看了這么多,快瘋了吧? 有點兒接受不了,告訴你,筆者也寫的快瘋了,come on ! 動手干活,先找一個工具,dumpbin,一般在你VC或VS的安裝目錄的某個bin文件夾下,搜一下就出來了(筆者的VC下的dumpbin不能用,所以用VS2013下的dumpbin了,但是,應該變化不大,如果不同,還請見諒),然后再cmd中運行,如筆者一樣以下的截圖一樣,加上 -EXPORTS 參數,如下,
之后,去掉extern “C”,如下,
#include <Windows.h>
#include <stdio.h>
//extern "C"{
class _declspec(dllexport) A{
public:
A(){
printf("A construct...\n");
}
const char * whoIsMe()
{
return "My name is A";
}
~A(){
printf("A deconstruct...\n");
}
};
_declspec(dllexport) A a;
_declspec(dllexport) int Add(int x,int y)
{
return x+y;
}
_declspec(dllexport) int g_nUsageCount = 3195;
//}
再次編譯成DLL,同樣使用dumpbin工具,再次運行,如下,
兩幅圖對比著看,主要看我們用紅框圈出來的部分,這樣,特別是一會兒我們準備調用的Add方法,很容易發現,有extern "C"的直接為“Add”,而去掉extern "C"之后,變成了“?Add@@YAHHH@Z”,后面那一場串東西,實際上就是編譯器為了解決重載問題加入的東西。
這里,我們為了調用方便,我們使用帶有 extern "C"的版本,TestDLL代碼如下:
#include <Windows.h>
#include <stdio.h>
int main()
{
HINSTANCE h = LoadLibrary("MyDLL");
int(*pAdd)(int,int);
pAdd = (int(__cdecl *)(int,int))(GetProcAddress(h,"Add"));
int sum = pAdd(239,23);
printf("sum is %d\n",sum);
FreeLibrary(h);
return 0;
}
之后,最后一幅圖,運行效果,
代碼中主要用到的就是 GetProcAddress函數,來獲取函數指針,之后通過函數指針調用Add函數,如果感興趣的話,可以將pAdd的值輸出出來,看一下,是否和我們用dumpbin看到的相互一致。而我們用extern "C"的原因在于,如果不使用的話,我們在調用GetProcAddress函數時,填第二個參數,將會令人頭疼。
好了,結束。順便一提,此文實際上還說的比較簡略,如果想深入研究,還是找本書,細細研究幾遍的好。