Win32動態鏈接庫的創建與使用 -> VC++深入淺出

新博客地址: vonsdite.cn

1. 靜態庫與動態庫的區別

靜態庫

函數和數據被編譯進一個二進制文件(.LIB)。在使用靜態庫下,在編譯連接可執行文件時,鏈接器從庫中複製這些函數和數據,並把它們和應用程序的其他模塊組合起來創建最終的可執行文件(.EXE)。當產品發佈時,只需要發佈可執行文件,不需要發佈使用的靜態庫。

特點

  1. 編譯後的可執行文件包含了所需要的函數的代碼,佔用磁盤空間較大。(但是可以避免出現用戶的電腦上沒有你開發時所用的庫的尷尬情形。)
  2. 如果多個調用相同庫的進程在內存中同時運行,內存中會存放多份相同的代碼

動態庫

在使用動態庫的時候,往往提供兩個文件:引入庫(.lib)文件和DLL(.dll)文件引入文件包含DLL導出的函數和變量的符號名,而**.dll包含了該DLL的實際的函數和數據**。在使用動態庫的情況下,在編譯連接可執行文件時,只需要連接該DLL的引入庫文件,而該DLL的函數代碼和數據並不複製到可執行文件中,直到可執行程序運行時,才加載所需的DLL,將該DLL映射到進程的地址空間中,然後訪問DLL中導出的函數。此時,在發佈產品時,除了發佈可執行文件外,還要發佈程序將要調用的動態鏈接庫

特點

  • 使用動態庫的好處在於能夠節省磁盤空間和內存。如果多個應用程序需要訪問同樣的功能,那麼可以將該功能以DLL的形式提供,這樣一臺機器上只需要存在一份該DLL就可以了,從而節省了磁盤空間。如果多個程序調用同一個DLL,該DLL的頁面只需要存放在內存一次,所有的應用程序都可以共享它的頁面了。

2. 示例

首先,我們新建一個空的Win32動態鏈接庫,爲其增加兩個函數:

int add(int a, int b)
{
	return a + b;
}
 
int subtract(int a, int b)
{
	return a - b;
}

當build之後,在這個工程的Debug目錄下,就會有一個對應於工程明的dll文件。這個dll目前是無法使用的,因爲這兩個函數都沒有被“導出”。我們可以利用Visual Studio提供的命令行工具Dumpbin來查看:

D:\MyPrograms\mfc\CH_19_DLL1\Debug>dumpbin -exports CH_19_DLL1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file CH_19_DLL1.dll

File Type: DLL

  Summary
        7000 .data
        1000 .idata
        2000 .rdata
        2000 .reloc
       2A000 .text

其中並沒有與函數有關的信息。

爲了讓DLL導出一些函數,需要在每個要被導出的函數前面加上標示符_declspec(dllexport)

此時再次查看,就可以看到:

 ordinal hint RVA      name

        1    0 0000100A ?add@@YAHHH@Z
        2    1 00001005 ?subtract@@YAHHH@Z

其中?add@@YAHHH@Z和?subtract@@YAHHH@Z的意思在
https://blog.csdn.net/VonSdite/article/details/81165308這裏有解釋過。

  • 大致過程如下:
    首先,編譯器會給函數名前加一個?;其次,因爲是__cdecl調用,所以後面加上YA;再次,H代表的是int類型,3個H分別表示的是返回值,第一個參數,第二個參數,最後以@Z結尾。

隱式鏈接方式和顯示加載

有了動態鏈接庫以後,我們就要在程序中加載它們。
有兩種可選的方式:

  • 隱式鏈接方式加載DLL
  • 顯示加載DLL。

隱式加載

我們先看隱式加載。我們新建一個MFC對話框應用程序,然後給上面放兩個按鈕,一個用來做加法,另一個用來做減法。消息響應函數如下:

extern int add(int a, int b);
extern int subtract(int a, int b);
 
void CCH_19_DllTestDlg::OnBtnAdd() 
{
	// TODO: Add your control notification handler code here
	CString str;
	str.Format("5 +3 = %d",add(5,3));
	MessageBox(str);
}
 
void CCH_19_DllTestDlg::OnBtnSubtract() 
{
	// TODO: Add your control notification handler code here
	CString str;
	str.Format("5 - 3 = %d",subtract(5,3));
	MessageBox(str);	
}

在其中因爲要調用動態鏈接庫中的add和subtract函數,所以在這裏聲明爲外部函數

具體如何加載呢?
從我們的動態鏈接庫的debug目錄下,將***.lib文件複製到MFC程序所在的文件夾下**,然後在工程->設置->鏈接中增加整個lib文件,程序就能編譯鏈接通過了。我們可以使用Dumpbin來查看:

D:\MyPrograms\mfc\CH_19_DllTest\Debug>dumpbin -imports CH_19_D
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

Dump of file CH_19_DllTest.exe

File Type: EXECUTABLE IMAGE

  Section contains the following imports:

    CH_19_Dll1.dll
                4052C0 Import Address Table
                40508C Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference


                   1  ?subtract@@YAHHH@Z
                   0  ?add@@YAHHH@Z

其他部分省略。
我們看到,這個可執行文件需要導入subtract和add兩個函數。
此時,程序還是不能執行的,因爲.exe文件並不知道從哪裏獲得.dll文件,此時,編譯器會依次在當前可執行文件的目錄(.exe所在的目錄)當前目錄(.cpp所在的目錄)系統目錄環境變量目錄中查找。我們可以將對應的.dll文件也拷過去。程序就能成功執行了。

除了使用Dumpbin之外,我們也可以使用vc++提供的一個可視化的工具:Depends來查看

除了使用extern之外,我們還可以使用__declspec(dllimport)來表明該函數是從動態鏈接庫中加載的。即:

//extern int add(int a, int b);
//extern int subtract(int a, int b);
__declspec(dllimport) int add(int a, int b);
__declspec(dllimport) int subtract(int a, int b);

這只是一個說明性的例子,在實際中,我們通常不是這樣編寫程序的。因爲當我們把dll交給用戶以後,用戶並不知道dll中擁有那些函數,只能通過前面介紹的Dumpbin和Depends等編譯工具來猜測函數的原型,這是很不方便的。正確的做法是,爲這個dll增加一個頭文件,在頭文件添加函數的聲明及註釋。在頭文件添加:

__declspec(dllimport) int add(int a, int b);
__declspec(dllimport) int subtract(int a, int b);

注意,因爲頭文件是給用戶使用的,所以是指明這些函數是從dll中導入的。而在我們的對話框程序中,添加:

#include "..\CH_19_Dll1\CH_19_Dll1.h"

有的時候,我們希望dll庫中的函數不僅可以爲客戶使用,也能夠讓庫自身調用,那麼我們就要利用預編譯指令來實現了,在頭文件:

#ifdef DLL1_API
#else
#define DLL1_API __declspec(dllimport)
#endif
DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

首先判斷是否定義DLL1_API,如果定義,則什麼也不做,否則就會將DLL1_API定義爲__declspec(dllimport),然後用__declspec(dllimport)代替DLL1_API
而在源文件中:

#define DLL1_API _declspec(dllexport)
#include "CH_19_Dll1.h"
 
 
int add(int a, int b)
{
	return a + b;
}
 
int subtract(int a, int b)
{
	return a - b;
}

此時這些函數就跟我們之前寫的函數一樣了,可以互相調用
我們仔細分析一下。在編譯該dll時,源文件會定義DLL1_API,然後將頭文件展開。此時因爲已經定義了DLL1_API,所以直接編譯函數的聲明,表明這個函數是從動態鏈接庫中導出的。
而當用戶的程序調用該dll時,只要用戶程序中沒有定義DLL1_API,那麼就會定義DLL1_API__declspec(dllimport)

導出類

我們下面看看如何從動態鏈接庫中導出C++類:
在頭文件中聲明:

class DLL1_API Point
{
public:
	void output(int x, int y);
};

在源文件中定義:

void Point:: output(int x, int y)
{
	//獲得當前窗口的句柄
	HWND hwnd = GetForegroundWindow();
	//獲取DC
	HDC hdc = GetDC(hwnd);
	char buf[20];
	memset(buf,0,20);
	sprintf(buf,"x = %d, y= %d", x, y);
	TextOut(hdc,0,0,buf,strlen(buf));
	ReleaseDC(hwnd,hdc);
}

注意到,這裏使用了windows函數,和輸出函數,所以得包含頭文件Windows.h、stdio.h。
在應用程序中,新增一個按鈕來調用這個類的成員函數:

void CCH_19_DllTestDlg::OnBtnOutput() 
{
	// TODO: Add your control notification handler code here
	Point pt;
	pt.output(5,3);
}

我們可以再利用Dumpbin看看這個dll:

         1    0 00001014 ??4Point@@QAEAAV0@ABV0@@Z
         2    1 0000100A ?add@@YAHHH@Z
         3    2 0000100F ?output@Point@@QAEXHH@Z
         4    3 00001005 ?subtract@@YAHHH@Z

其中的?4Point@@QAEAAV0@ABV0@@Z是構造函數,?output@Point@@QAEXHH@Z中,@Point是表明它是Point類的函數,QAE表示它是public函數,X表示返回值類型爲void,HH表示有兩個int類型的參數。
其實,我們完全可以不導出整個類,而是隻導出其中的若干函數:

class Point
{
public:
	void DLL1_API output(int x, int y);//導出該函數
	void test();
};

下面的問題與前面的問題緊密相關,C++編譯器生成dll時,會對導出的函數名進行改編,但是不同的編譯器的改變規則不完全相同,特別的,如果是一個純C的編譯器序使用這個dll,則由於改編名字的不同,dll中的函數就不能被找到了
因此,我們希望動態鏈接庫文件在編譯時,導出函數的名稱不要發生改變。我們可以使用extern "C"來實現,指定的內容用C的方式編譯:

#ifdef DLL1_API
#else
#define DLL1_API extern "C"__declspec(dllimport)
#endif
DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

#define DLL1_API extern "C"_declspec(dllexport)
#include "CH_19_Dll1.h"
//#include <Windows.h>
#include <stdio.h>
 
int add(int a, int b)
{
	return a + b;
}
 
int subtract(int a, int b)
{
	return a - b;
}

注意,由於是使用的C語言,所以就得把類相關的代碼註釋掉了。我們再用dumpbin看看這個dll:

   ordinal hint RVA      name


         1    0 0000100A add
         2    1 00001005 subtract

此時,名字沒有發生改編。

引起名字改編的還有調用約定,在默認情況下,使用的cdecl:參數由右向左壓棧,由調用者清棧;假如我們改爲_stdcall:參數由右向左,由被調用的函數自己清棧,那麼函數的名字又會變爲:

   ordinal hint RVA      name


         1    0 00001005 _add@8
         2    1 0000100A _subtract@8

模塊定義

我們可以通過模塊定義文件來解決不同編譯器、不同語言之間的編譯器將函數明修飾的不一樣的問題。我們重新建立一個動態鏈接庫,爲其增加一個.cpp文件:
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

然後,我們在它的目錄下增加一個記事本文件,並把後綴名改爲.def,使用vc++將其打開,添加如下代碼:

LIBRARY CH_19_Dll2

EXPORTS
add
subtract

其中第一行是動態鏈接庫的名稱,EXPORTS語句表明的是將要導出的函數的名字,關於它的詳細用法,可以參考MSDN。此時,導出函數的名稱就統一了。

顯示加載

說了這麼多,都是隱式加載的。我們現在看看如何顯式加載,先看程序:

void CCH_19_DllTestDlg::OnBtnAdd() 
{
	// TODO: Add your control notification handler code here
	HINSTANCE hInst;
	//加載動態鏈接庫
	hInst = LoadLibrary("CH_19_Dll2.dll");
	typedef int(*ADDPROC)(int a, int b);
	//獲取函數地址
	ADDPROC Add = (ADDPROC)GetProcAddress(hInst,"add");
	if(!Add)
	{
		MessageBox("獲取函數地址失敗");
		return ;
	}
	CString str;
	str.Format("5 +3 = %d",Add(5,3));
	MessageBox(str);
}

注意,其中GetProcAddress的第二個參數,函數名通過.def指定。

我們看到**,隱式加載實現起來更爲簡單,加載好以後就可以直接使用**;而顯示加載更爲靈活,可以在需要時才加載dll。但是他的麻煩之處就在於一旦編譯器生成的新的函數名發生變化,那麼就得修改。比如,如果dll中的函數調用方式改爲stdcall,那麼這裏的函數指針也得改爲stdcall。而且如果dll並沒有使用.def來指定導出的名字,那麼這裏就得使用編譯器修改後的名字:?add@@YAHHH@Z,或者使用訪問序號:

	ADDPROC Add = (ADDPROC)GetProcAddress(hInst,MAKEINTRESOURCE(1));

這個訪問序號也可以通過dumpbin獲得

  ordinal hint RVA      name


        1    0 00001005 ?add@@YAHHH@Z

當然,在實際應用中,建議還是使用名字比較好,畢竟有意義的名字好於序號

其實,在windows加載dll時需要一個入口函數,就如同控制檯或DOS程序需要main函數、WIN32程序需要WinMain函數一樣。這個函數稱爲DllMain,聲明如下:

BOOL WINAPI DllMain(  HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved  );

其中第二個參數是調用的原因,可以使用一個switch/case語句來對於每種情況分別處理。這個函數是一個可選的函數,由於我們的dll只完成一些簡單的功能,所以沒有使用。需要注意的是,在這個函數中不要進行太複雜的的調用。因爲此時一些核心動態庫,比如user32.dll或者GDI32.dll等還沒有加載,如果我們的編寫的DllMain函數需要調用這兩個庫的某些函數的話,則會出錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章