Windows動態鏈接庫

一、Windows的動態鏈接庫簡介

DLL即動態鏈接庫(Dynamic-Link Library)的縮寫,相當於Linux下的共享對象。Windows系統中大量採用DLL機制,甚至內核的結構很大程度依賴於DLL機制。Windows下的DLL文件和EXE文件實際上是一個概念,都是PE格式的二進制文件。

1.1 Windows下面的動態鏈接庫與Linux下面的動態鏈接庫的區別

(1)文件後綴不同

Linux動態庫的後綴是 .so 文件,而window則是 .dll 文件

(2)文件格式不同

(a)Linux下是ELF格式,即Executable and Linkable Format。在ELF之下,共享庫中所有的全局函數和變量在默認情況下都可以被其它模塊使用,即ELF默認導出所有的全局符號。

(b)Windows下面是PE格式的文件,即Portable Executable Format。DLL本質上也是PE文件,DLL需要顯示地“告訴”編譯器需要導出某個符號,否則編譯器默認所有的符號都不導出。

(c)動態鏈接庫的文件個數不一樣

Linux的動態鏈接庫就只有一個 .so 文件,還有與之對應的頭文件,而在Windows下面的動態庫有兩個文件,一個是引入庫(.LIB)文件,一個是動態庫(.DLL)文件,需要的頭文件(.h)文件。

(1)引入庫文件包含被DLL導出的函數的名稱和位置,對於導入庫而言,其實際的執行代碼位於動態庫中,導入庫只包含了地址符號表等,確保程序找到對應函數的一些基本地址信息。

(2)DLL文件包含實際的函數和數據,應用程序使用LIB文件鏈接到所需要使用的DLL文件,庫中的函數和數據並不複製到可執行文件中,因此在應用程序的可執行文件中,存放的不是被調用的函數代碼,而是DLL中所要調用的函數的內存地址,這樣當一個或多個應用程序運行是再把程序代碼和被調用的函數代碼鏈接起來,從而節省了內存資源。

總結:從上面的說明可以看出,Windows下面所創建的動態鏈接庫DLL和.LIB文件必須隨應用程序一起發行,否則應用程序將會產生錯誤。一般的動態庫程序有lib文件和dll文件,lib文件是編譯時期連接到應用程序中的,而dll文件纔是運行時纔會被調用的。

1.2 靜態鏈接庫和動態鏈接庫的異同點

如果採用靜態鏈鏈接庫(.lib),lib中的指令最終都會編譯到鏈接該靜態庫的exe(或dll)文件中,發佈軟件時,只需要發佈exe(或dll)文件,不需要.lib文件。

但是若使用動態鏈接庫(. dll),dll中的指令不會編譯到exe文件中,而是在exe文件執行期間,動態的加載和卸載獨立的dll文件,需要和exe文件一起發佈。

靜態鏈接庫不能再包含其他動態鏈接庫或靜態鏈接庫,而動態鏈接庫不受此限制,動態鏈接庫中可以再包含其他的動態鏈接庫和靜態鏈接庫。

來自:https://blog.csdn.net/w_y2010/article/details/80428067

1.3 動態鏈接庫的優點

1)節省內存和代碼重用:當應用程序使用動態鏈接時,多個應用程序可以共享磁盤上單個DLL副本

2)可擴展性:DLL文件與EXE文件獨立,只要接口不變,升級程序只需更新DLL文件不需要重新編譯應用程序

3)複用性:DLL的編制與具體的編程語言以及編譯器無關,不同語言編寫的程序只要按照函數調用約定就可以調用同一個DLL函數1

1.4 有下面的代碼

  • 頭文件framework.h
#pragma once
namespace mycal
{
	int add(int a, int b);

	int sub(int a, int b);
}
  • 實現代碼 framework.cpp
//framework.cpp
#include "framework.h"

int mycal::add(int x, int y)
{
	return x + y;
}

int mycal::sub(int x, int y)
{
	return x - y;
}

在Linux下,編譯成動態鏈接庫之後,會得到一個 xxx.so 文件,現在只要引入頭文件,包含動態庫路徑,就可以正常使用了,但是上面的代碼同樣在Windows下面,使用VS2017編譯成動態鏈接庫之後,的確不會報錯,只會的到一個 xxx.dll 文件,(不是還有一個對應的 xxx.lib文件嗎,哪裏去了呢?)

然後我們新建一個項目,按照 “頭文件路徑配置——庫文件路徑配置”的方法,編寫代碼,我們也可以調用到add這兩個函數,還有語法提示,因爲語法提示其實來自於頭文件,和庫文件沒關係,但是編譯卻不成功了,顯示調用的add以及sub都是錯誤的,這是爲什麼呢?

這是因爲前面說了Windows下面需要顯式的告訴編譯器,動態庫中有哪一些函數是可以導出使用的,上面沒有顯示說明,即add和sub實際上是不可以使用的,故而會報錯,怎麼辦呢?參見下面。

二、Windows平臺之下使用VS2017如何創建動態庫

2.1 解決未生成lib文件以及函數沒有顯式導出的問題

兩種方式來決定動態庫中到底哪些函數是可以導出供外部直接使用的,以及與此同時生成與dll對應的 .lib 文件。

(1)MSVC編譯器提供了關鍵字_declspec,來指定指定符號的導入導出,即_declspec屬性關鍵字

_declspec(dllexport) 表示該符號是從本DLL導出的符號:這是在定義DLL中的函數等源代碼是必須使用的,如果不顯式的導出某一些符號,則使用動態鏈接庫雖然沒有語法上的錯誤,但是她無法編譯,因爲dll中的函數沒有暴露出來,故而找不到。

_declspec(dllimport)表示該符號是從別的DLL中導入的:我們在使用動態鏈接庫DLL中暴露出來的函數的時候,可以直接使用暴露的函數,也可以通過顯示地導入函數,編譯器會產生質量更好的代碼。由於編譯器確切地知道了一個函數是否在一個DLL中,它就可以產生更好的代碼,不再需要間接的調用轉接。如下所示:

//顯式的導入dll中暴露出來的函數
__declspec(dllimport) void func1(void);
int main(void)
{
	func1();
}

後面會專門講如何使用 __declspec 來創建動態庫。

(2)使用"xxx.def"文件來聲明導入和導出符號

我們也可以不使用__declspec 來創建動態庫,我們就按照正常的程序編寫,如第一章節裏面的 myMath.h 和 myMath.cpp 裏面的內容,然後顯示的添加一個 xxx.def 文件,怎麼添加呢?

如下:右擊項目/添加/新建項,選擇如下的文件:
在這裏插入圖片描述
默認是使用source.def 我們可以自定義名稱。關於這個def文件是如何規定哪些內容是導出的,哪一些是不導出的,這裏暫時先不說明了,可以參考下面的幾篇文章:

https://blog.csdn.net/qwq1503/article/details/85696279

http://www.cnblogs.com/enterBeijingThreetimes/archive/2010/08/04/1792099.html

2.2 使用 __declspec 來創建動態庫的完整過程

(1)新建一個空項目或者是使用DLL模板都可以。

  • 添加頭文件 framework.h
//framework.h
#pragma once

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>

namespace mycal {
	__declspec(dllexport) int add(int a, int b);
	__declspec(dllexport) int sub(int a, int b);
}

注意:實際上就是在需要定義的函數前面添加一個 __declspec(dllexport) 是兩個短下劃線開頭哦!來表示這兩個函數是暴露出來可以供直接使用的,沒有暴露的函數,在動態鏈接庫中是沒辦法使用的。

2)實現函數的內容。

  • 定義一個 framework.cpp文件
//framework.cpp
#include "framework.h"

int mycal::add(int x, int y)
{
	return x + y;
}

int mycal::sub(int x, int y)
{
	return x - y;
}

這個地方和我們平時的實現完全一樣,實現的時候不再需要添加 __declspec(dllexport) 了。

(3)生成項目

比如我選擇生成 Debug x64位的結果,

生成之後得到如下的結果:
在這裏插入圖片描述
我們發現有一對配套的 xxx.dll 和 xxx.lib 文件,他們的大小不一樣哦!此lib文件只是dll文件中導出函數的聲明和定位信息,並不包含函數的實現,因此此lib文件只是在調用對應dll庫的工程編譯時使用,不需要隨exe發佈。

三、動態鏈接庫的使用

3.1 靜態調用dll

靜態調用是由編譯系統完成對dll文件的加載和應用程序結束時完成對dll的卸載,當調用某dll的應用程序結束時,則windows系統對該dll的應用記錄減1,直到使用該dll的所有應用程序都結束,即對該dll應用記錄爲0,操作系統會卸載該dll,靜態調用方法簡單,但不如動態調用適用。

前面說了,window上生成的動態鏈接庫的使用需要三個東西:頭文件,dll文件,與dll對應的lib文件。

這裏都具備了,現在新建一個項目,如何配置呢?

三步走配置

(1)第一步:配置包含路徑——即頭文件所在的路徑,將頭文件複製過來。
在這裏插入圖片描述

(2)第二步:配置庫路徑——即lib所在的路徑,將Dll_demo_1.lib也複製到當前工程文件夾中。
在這裏插入圖片描述

(3)第三步:添加鏈接,——將上面得到的Dll_demo_1.lib添加到鏈接器:

#pragma comment(lib,"Dll_demo_1.lib")

這一句是告訴編譯器與該dll相對應的.lib文件所在的路徑和文件名。在生成dll文件時,鏈接器會自動爲其生成一個對應的.lib文件,該文件包含了dll導出函數的符號名和序號(並沒有實際的代碼)。在應用程序中,.lib文件將作爲dll的替代文件參與編譯,編譯完成後,.lib文件就不需要了。

修改複製過來的 framework.h,將裏面的__declspec(dllexport)爲__declspec(dllimport):

#pragma once
#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>

namespace mycal {
	__declspec(dllimport) int add(int a, int b);
	__declspec(dllimport) int sub(int a, int b);
}

Source.cpp 代碼如下:

#include <iostream>
#include <myMath.h> //添加自己定義的頭
#pragma comment(lib,"Dll_demo_1.lib")
int main()
{
	int x = 50;
	int y = 10;
	int a,b;
	a = mycal::add(x, y);
	b = mycal::sub(x, y);
	printf("%d %d\n", a, b);
	getchar();
	return 0;
}

現在生成,生成沒有錯誤,但是運行依然會報錯,因爲運行的時候需動態鏈接庫,所以還需要配置動態鏈接庫。

一共有三種方式,本文采用最簡單的方式,直接將動態鏈接庫和可執行exe文件拷貝到一起即可。
在這裏插入圖片描述
然後運行上面的程序,得到結果如下:

120 80
上面就是整個動態鏈接庫的創建以及使用的過程。

注意1

1)可以修改.lib文件的文件名,只要在項目引用它時,使用它目前的名稱,便可以正確運行,但不能改變.dll文件的名字,不然也會出現找不到.dll文件的錯誤

2).dll文件必須和.exe放在一起,.exe文件在哪裏.dll文件也得在那裏,兩者“共存亡”,否則就會出現找不到.dll文件的錯誤

3)和靜態庫一樣,.dll文件也要和應用程序的位數相對應,要麼都是64位的,要麼都是32位的,不可交叉使用。

4)靜態調用不需要使用Win32API函數來加載和卸載Dll以及獲取Dll中導出函數的地址,這是因爲當通過靜態鏈接方式編譯生成程序時,編譯器會將.lib文件中導出函數的函數符號鏈接到生成的exe文件中,.lib文件中包含的與之對應的dll文件的文件名也被編譯存儲在exe文件內部,當應用程序運行過程中需要加載dll文件時,windows將根據這些信息查找並加載dll,然後通過符號名實現對dll函數的動態鏈接,這樣,exe將能直接通過函數名調用dll 的輸出函數,就像調用程序內部的其他函數一樣。

3.2 動態調用dll

動態調用是由程序員調用系統API函數加載和卸載dll,程序員可以決定dll文件何時加載,何時卸載,加載哪個dll文件,將dll文件的使用權完全交給程序員。

1)、新建控制檯項目,添加 main.cpp 文件,將剛剛生成的 Dll_demo_1.dll 文件拷貝到項目目錄下,main.cpp 代碼如下

#include "stdio.h"
#include <windows.h>

typedef int (*lpAddFun)(int ,int );//宏定義函數指針類型

int main()
{
	HINSTANCE hDll;//DLL 句柄
	lpAddFun addFun;//函數指針
	hDll = LoadLibrary(L"Dll_demo_1.dll");//動態獲取dll文件的路徑
	if (hDll!=NULL)
	{
		addFun =(lpAddFun)GetProcAddress(hDll,"add");//根據函數名在dll文件中獲取該函數的地址	
		if (addFun!=NULL)
		{
			int result =addFun(2,3);
			printf("2+3=%d",result);
		}

	FreeLibrary(hDll);
	}
	return 0;
}

運行結果:2+3=5

3.2.1 main.cpp分析

語句typedef int (*lpAddFun)(int ,int )定義了一個與add函數接收參數類型和返回值均相同的函數指針類型,隨後在main函數中定義了lpAddFun的實例addFun;在函數main中定義了一個DLL HISTANCE句柄實例hDll,通過Win32API函數LoadLibrary動態加載DLL模塊並將DLL模塊句柄賦給hDll
main函數中通過Win32API函數GetProcAddress得到所加載的DLL模塊中函數add的地址並賦值給addFun,經由函數指針addFun進行了對該DLLadd函數的調用;
在完成對dll的調用後,在main函數中通過Win32API函數FreeLibrary釋放已加載的DLL模塊。

通過以上的分析可知:
(a) 動態調用只需要dll文件即可,不需要對應的.h頭文件和.lib文件,一般情況下,只要有dll,就可以調用此dll中的導出函數。
(b) 在調用dll中的函數時,需要知道導出函數的函數簽名,若擁有dll對應的頭文件,可以參照頭文件即可,若沒有頭文件,使用特定工具也可以得到dll中導出函數的函數簽名
(c) DLL需要已某種特定的方式聲明導出函數。
(d) 應用程序需要以特定的方式調用DLL的淡出函數。

四、Visual Studio提供了一個命令行工具:Dumpbin

這個工具不需要自己安裝,我們通過安裝VS,可以直接使用,如下打開VS自帶的命令行工具,如下:
在這裏插入圖片描述
這個工具有什麼作用呢?簡而言之,它可以查看一個 lib文件 dll文件提供了哪一些函數,暴露出來可供使用的,它還可以查看 exe 文件包含了哪一些靜態庫和動態庫。

查看一下它的幫助信息:
在這裏插入圖片描述

如何使用呢?舉幾個簡單的例子,以本文所創建的動態庫和可執行程序作爲演示:

(1)查看Dll_demo_1.dll 暴露出來了哪一些函數可以使用——dumpbin -exports xxxx.dll(xxxx.lib文件也一樣的)

F:\OpenCV4.1.1_Test\2020_01_10\calcuDll\calculate\x64\Release>dumpbin -exports calculate.dll

總結:

查看導入:dumpbin -exports 文件名稱

查看導出:dumpbin -imports 文件名稱


  1. https://blog.csdn.net/qq_33757398/article/details/81545966 ↩︎ ↩︎

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