新博客地址: vonsdite.cn
1. 靜態庫與動態庫的區別
靜態庫
函數和數據被編譯進一個二進制文件(.LIB)。在使用靜態庫下,在編譯連接可執行文件時,鏈接器從庫中複製這些函數和數據,並把它們和應用程序的其他模塊組合起來創建最終的可執行文件(.EXE)。當產品發佈時,只需要發佈可執行文件,不需要發佈使用的靜態庫。
特點
- 編譯後的可執行文件包含了所需要的函數的代碼,佔用磁盤空間較大。(但是可以避免出現用戶的電腦上沒有你開發時所用的庫的尷尬情形。)
- 如果多個調用相同庫的進程在內存中同時運行,內存中會存放多份相同的代碼。
動態庫
在使用動態庫的時候,往往提供兩個文件:引入庫(.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函數需要調用這兩個庫的某些函數的話,則會出錯。