1. 程式人生 > >如何編譯生成dll

如何編譯生成dll

動態連結庫是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"之後,變成了“[email protected]@[email protected]”,後面那一場串東西,實際上就是編譯器為了解決過載問題加入的東西。

這裡,我們為了呼叫方便,我們使用帶有 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函式時,填第二個引數,將會令人頭疼。

好了,結束。順便一提,此文實際上還說的比較簡略,如果想深入研究,還是找本書,細細研究幾遍的好。