1. 程式人生 > >對dll呼叫的理解

對dll呼叫的理解

0x00:dll和lib及其區別

靜態庫:在連結步驟中,聯結器將從庫檔案取得所需的程式碼,複製到生成的可執行檔案中,這種庫稱為靜態庫,其特點是可執行檔案中包含了庫程式碼的一份完整拷貝;缺點就是被多次使用就會有多份冗餘拷貝。即靜態庫中的指令都全部被直接包含在最終生成的 EXE 檔案中了。在vs中新建生成靜態庫的工程,編譯生成成功後,只產生一個.lib檔案

動態庫:動態連結庫是一個包含可由多個程式同時使用的程式碼和資料的庫,DLL不是可執行檔案。動態連結提供了一種方法,使程序可以呼叫不屬於其可執行程式碼的函式。函式的可執行程式碼位於一個 DLL 中,該 DLL 包含一個或多個已被編譯、連結並與使用它們的程序分開儲存的函式。在vs中新建生成動態庫的工程,編譯成功後,產生一個.lib檔案和一個.dll檔案

那麼上述靜態庫和動態庫中的lib有什麼區別呢?

靜態庫中的lib:該LIB包含函式程式碼本身(即包括函式的索引,也包括實現),在編譯時直接將程式碼加入程式當中

動態庫中的lib:該LIB包含了函式所在的DLL檔案和檔案中函式位置的資訊(索引),函式實現程式碼由執行時載入在程序空間中的DLL提供

總之,lib是編譯時用到的,dll是執行時用到的。如果要完成原始碼的編譯,只需要lib;如果要使動態連結的程式執行起來,只需要dll。

0x01:dll的生成

開啟vs:檔案->新建->專案->Win32控制檯應用程式->dll(可以使用預編譯)
產生的結果如圖:

其中 addfun.h 中的內容為:

#include "stdafx.h"


extern "C"
{
	_declspec(dllexport) int add(int a, int b);
	typedef int(*ApiAdd)(int, int);
}

** _declspec(dllexport)**是生成 dll 必須要使用的

然後1.cpp 中的內容為:

// 1.cpp : 定義 DLL 應用程式的匯出函式。

#include "stdafx.h"
#include "addfun.h"

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

為了理解 dll 中相關內容的結構和呼叫,需要檢視和更改 dllmain.cpp 中的內容

// dllmain.cpp : 定義 DLL 應用程式的入口點。
#include "stdafx.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中的內容都是自定義的
		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 中的這些引數:[^2]

  • hModule引數:指向DLL本身的例項控制代碼;

  • ul_reason_for_call引數:指明瞭DLL被呼叫的原因,可以有以下4個取值:

  • DLL_PROCESS_ATTACH

    當DLL被程序第一次呼叫時,導致DllMain函式被呼叫,同時ul_reason_for_call 的值為 DLL_PROCESS_ATTACH,如果同一個程序後來再次呼叫此DLL時,作業系統只會增加DLL的使用次數,不會再用DLL_PROCESS_ATTACH呼叫DLL的DllMain函式。

  • DLL_PROCESS_DETACH

    當DLL被從程序的地址空間解除對映時,系統呼叫了它的DllMain,傳遞的ul_reason_for_call值是DLL_PROCESS_DETACH

    注意:如果程序的終結是因為呼叫了TerminateProcess,系統就不會用DLL_PROCESS_DETACH來呼叫DLL的DllMain函式。這就意味著DLL在程序結束前沒有機會執行任何清理工作。

  • DLL_THREAD_ATTACH

    當程序建立一執行緒時,系統檢視當前對映到程序地址空間中的所有DLL檔案映像,並用值DLL_THREAD_ATTACH呼叫DLL的DllMain函式。 新建立的執行緒負責執行這次的DLL的DllMain函式,只有當所有的DLL都處理完這一通知後,系統才允許執行緒開始執行它的執行緒函式。

  • DLL_THREAD_DETACH:如果執行緒呼叫了ExitThread來結束執行緒(執行緒函式返回時,系統也會自動呼叫ExitThread),系統檢視當前對映到程序空間中的所有DLL檔案映像,並用DLL_THREAD_DETACH來呼叫DllMain函式,通知所有的DLL去執行執行緒級的清理工作。

    注意:如果執行緒的結束是因為系統中的一個執行緒呼叫了TerminateThread,系統就不會用值DLL_THREAD_DETACH來呼叫所有DLL的DllMain函式。

  • lpReserved引數:保留,目前沒什麼意義。

所以我們通過列印分支語句中的內容來判斷程序和執行緒的呼叫情況。
此時直接在vs中點選生成即可生成相關dll和lib檔案(位於專案的Debug目錄下,不要除錯執行)

0x02:dll的呼叫

直接在vs中新建一個C++專案,而在這個專案中需要用到上一步的就是dll檔案和.h標頭檔案,直接將兩個檔案複製到新建的專案中,然後在vs裡面右鍵專案裡面的標頭檔案、新增剛剛複製過來的.h檔案就可以,但是要注意需要修改如下:

#include "stdafx.h"
extern "C"
{
	//將生成dll的語句刪除即可
	int add(int a, int b);
	typedef int(*ApiAdd)(int, int);
}

接下來就是編寫主程式.cpp如下:

// use1.cpp : 定義控制檯應用程式的入口點。
//
#include "stdafx.h" 
#include <Windows.h> 
#include "addfun.h" 
#include<iostream> 
#include<stdlib.h> 
using namespace std; 

DWORD WINAPI ThreadFunc(LPVOID);

int _tmain(int argc, _TCHAR* argv[])
{
	int a = 2, b = 1, c = 0;
	HINSTANCE hDllInst = LoadLibrary("1.dll");
	ApiAdd myfun = 0; 
	myfun = (ApiAdd)GetProcAddress(hDllInst, "add");
	
	//建立執行緒
	HANDLE hThread;
	DWORD  threadId;
	hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId); // 建立執行緒
	printf("我是主執行緒, pid = %d\n", GetCurrentThreadId());  //輸出主執行緒pid
	//ExitThread(0);
	//CloseHandle(hThread);
	
	// youFuntionName 在DLL中宣告的函式名 
	if (myfun)
	{
		c = myfun(a, b);
	}
	Sleep(500);
	cout<< " 1 + 2 = " << c << endl;
	FreeLibrary(hDllInst);
	system("pause");
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID p)
{
	printf("我是子執行緒, pid = %d\n", GetCurrentThreadId());   //輸出子執行緒pid
	return 0;
}

上述程式碼中HINSTANCE hDllInst = LoadLibrary("1.dll");會出現波浪線錯誤,此時點選 專案->屬性->常規->字符集 修改為“使用多位元組字符集”即可。

這時通過手動建立執行緒才能產生 dll 中的DLL_THREAD_ATTACH條件,而建立的執行緒在這裡是一個子執行緒,主執行緒不會產生呼叫條件。

注意一定要在呼叫 dll 的程序程式碼中間建立執行緒,不然在程序結束後就無法產生呼叫執行緒的條件了

在創造執行緒條件時,我遇到了一點小問題,當時一開始並沒有使用 Sleep() 函式,導致最終出現的結果如下:

我的猜測是可能是執行緒建立的有點慢,導致先列印了“1+2” ,後來查了一下,可能是因為不Sleep就可能造成執行緒一直佔用CPU,從而使CPU得不到釋放,故後在程式碼中嘗試添加了“Sleep(500);” ,最終成功如下:

對比程式碼和結果就可以大概分析出整個流程,在此不做細述。

注:其實有兩種方法呼叫動態庫,一種隱式連結,一種顯示連結。具體情況可自行百度或檢視參考部落格 1


參考部落格: 1: https://www.cnblogs.com/TenosDoIt/p/3203137.html
2: https://blog.csdn.net/friendan/article/details/7659190