9.1 DLL簡介
DLL即動態鏈接庫的縮寫,它相對於Linux下的共享對象。
Windows下的DLL文件和EXE文件實際上是一個概念,它們都是有PE格式的二進制文件。
微軟希望通過DLL機制加強軟件的模塊化設計,使得各種模塊之間能夠松散地組合、重用和升級。
9.1.1 進程地址空間和內存管理
一個DLL在不同進程中擁有不同的使用數據副本。在ELF中,由於代碼段是地址無關的,所以它可以實現多個進程之間共享一份代碼,但是DLL的代碼卻並不是地址無關的,所以它只是在某些情況下可以被多個進程間共享。
9.1.2 基地址和RVA
PE里面有兩個概念就是基地址和相對地址。當一個PE文件被加載時,其進程空間中起始地址就是基地址。對於任何一個PE文件來說,它都有一個優先裝載的基地址,這個值就是PE文件頭中的Image Base。
對於一個可執行EXE文件來說,Image Base一般值是0x400000,對於DLL文件來說,這個值一般是0x10000000。
9.1.3 DLL共享數據段
正常情況下,每個DLL的數據段在各個進程中都是獨立的,每個進程都擁有自己的副本。但是Windows允許將DLL的數據段設置成共享的,即任何進程都可以共享該DLL的同一份數據段。比較常見的做法是將一些需要共享的變量分離出來,放到另外一個數據段中,然后設置成進程之間共享的。也就是說,一個DLL有兩個數據段,一個是進程間共享,一個是私有的。
9.1.4 DLL的簡單例子
導出概念:在ELF中,共享庫所有的全局函數和變量在默認情況下都可以被其他模塊使用,也就是說ELF默認導出所有的全局符號。但是在DLL中情況是,我們需要顯式的告訴編譯器我們需要導出的某個符號,否則編譯器默認所有符號都不導出。當我們在程序中使用DLL導出符號時,這個過程被稱為導入。
在C/C++中,可以使用”_declspec”屬性關鍵字來修飾某個函數或者變量。當使用_declspec(dllexport)時,表示該符號是從本DLL導出符號,_declspec(dllimport)是表示該符號是從別的DLL導入符號。
9.1.5 創建DLL
__declspec(dllexport) double Add(double a, double b) { return a + b; } __declspec(dllexport) double Sub(double a, double b) { return a - b; } __declspec(dllexport) double Mul(double a, double b) { return a * b; }
執行:
cl /LDd Math.c
9.1.6 使用DLL
程序使用DLL的過程其實是引用DLL中的導出函數和符號過程,即導入過程。
#include<stdio.h> __declspec(dllimport) double Sub(double a,double b); int main(int argc,char **argv) { double result=Sub(3.0,2.0); printf("Result = %f/n",result); return 0; }
使用下面命令將TestMath.c編譯成TestMath.obj。
cl /c TestMath.c
使用鏈接器將TestMath.obj和Math.lib鏈接起來產生一個可執行文件TestMath.exe。
link TestMath.obj Math.lib
這個過程如下圖:
Math.lib中並不包含正在Math.c的代碼和數據,它描述Math.dll的導出符號,它包含了TestMath.o鏈接到Math.dll導入符號以及一部分樁代碼,又稱作”膠水”代碼。Math.lib文件被稱為導入庫。
9.1.7 使用模塊定義文件
聲明DLL中某個函數為導出函數的辦法有兩種:
- 一種就是前面使用的”__declspec(dllexport)”
- 另外一種就是采用模塊定義(.def)文件聲明。
.def文件用於控制鏈接過程,為鏈接器提供有關鏈接程序的導出符號、屬性、以及其他信息。
使用”_stdcall”調用規范的函數Add就會被修飾成”_Add@16”,前面以開頭,后面以@n結尾,n表示函數調用時參數所占堆棧空間大小。使用.def文件可以將導出函數重新命名。
微軟以DLL的形式提供Windows的API,而每個DLL中的導出函數又以這種”__stdcall”的方式聲明。但也采用了導出函數重命名的方法。
9.1.8 DLL顯示運行時鏈接
DLL也支持運行時鏈接,即運行時加載,Windows提供了3個API為:
- LoadLibrary,這個函數用來裝載一個DLL到進程空間,它的功能和dlopen類似
- GetProcAddress,用來查找某個符號地址,與dlsym類似
- FreeLibrary,用來卸載某個已加載的模塊,與dlclose類似
#include<Windows.h> #include<stdio.h> typedef double(*Func)(double, double); int main(int argc, char **argv) { Func function; double result; float i=1; HINSTANCE hinstLib = LoadLibrary("Math.dll"); if (hinstLib == NULL) { printf("ERROR"); } function =(Func)GetProcAddress(hinstLib, "Add"); if (function==NULL) { printf("ERROR"); } result = function(1.0, 6.0); FreeLibrary(hinstLib); printf("Result=%f\n", result); scanf_s("%f", i); return 0; }
9.2 符號導出導入表
9.2.1 導出表
當一個PE文件需要將一些函數或變量提供給其他PE文件使用時,我們把這種行為叫做符號導出,最典型的情況就是一個DLL將符號給EXE文件使用。
在Windows PE中,所有導出的符號被集中存放在被稱作導出表的結構中。導出表最簡單的結構上來看,它提供了一個符號名與符號地址的映射關系。
導出表的結構,它被存放在”Winnt.h”中:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image 導出地址表 //導出地址表EAT,它存放的是各個導出函數的RVA。 DWORD AddressOfNames; // RVA from base of image 符號名表 //它保存導出函數名字,所有函數名按照ASCII順序排序,以便於動態鏈接器在查找函數名字時可以速度更快。 DWORD AddressOfNameOrdinals; // RVA from base of image 名字序號對應表 //對應函數名表中的函數名所對應的序號值。 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
9.2.2 EXP文件
在創建DLL的同時也會得到一個EXP文件,這個文件實際上是鏈接器在創建DLL時的臨時文件。
EXP文件實際上是一個標准的PE/COFF目標文件,只不過它的擴展名不是.obj而是.exp。
9.2.3 導出重定向
將某個導出符號重定向到另外一個DLL。
實現機制:導出表的地址數組中包含的是函數的RVA,但是如果這個RVA指向的位置位於導出表中,那么表示這個符號被重定向了。
9.2.4 導入表
如果我們的程序使用到了來自DLL的函數和變量,那么我們就把這種行為叫做符號導入。
在ELF中,有”.got”,在windows中也有類似的機制,叫做導入表,當某個PE文件被加載時,Windows加載器會將所需要導入的函數地址確定並且將導入表中的元素調整到正確的地址,以實現動態鏈接的過程。
使用dumpbin查看一個模塊依賴哪些DLL,又導入哪些函數:

Microsoft (R) COFF/PE Dumper Version 14.10.25019.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file Math.dll File Type: DLL Section contains the following imports: KERNEL32.dll 1001A000 Import Address Table 1001A1D0 Import Name Table 0 time date stamp 0 Index of first forwarder reference 1A6 FreeLibrary 2A6 GetProcAddress 3B4 LoadLibraryA 212 GetCurrentProcess 5BB VirtualQuery 2AC GetProcessHeap 33F HeapFree 33B HeapAlloc 259 GetLastError 270 GetModuleHandleW 37B IsProcessorFeaturePresent 2C8 GetStartupInfoW 55B SetUnhandledExceptionFilter 59A UnhandledExceptionFilter 358 InitializeSListHead 11A DisableThreadLibraryCalls 2E1 GetSystemTimeAsFileTime 217 GetCurrentThreadId 213 GetCurrentProcessId 43E QueryPerformanceCounter 5E8 WideCharToMultiByte 3E0 MultiByteToWideChar 453 RaiseException 374 IsDebuggerPresent 579 TerminateProcess VCRUNTIME140D.dll 1001A0A4 Import Address Table 1001A274 Import Name Table 0 time date stamp 0 Index of first forwarder reference 25 __std_type_info_destroy_list 35 _except_handler4_common 2E __vcrt_GetModuleFileNameW 2F __vcrt_GetModuleHandleW 31 __vcrt_LoadLibraryExW 48 memset ucrtbased.dll 1001A0EC Import Address Table 1001A2BC Import Name Table 0 time date stamp 0 Index of first forwarder reference 2E1 _register_onexit_function 197 _initialize_onexit_table 196 _initialize_narrow_environment DC _configure_narrow_argv 2ED _seh_filter_dll 8E __stdio_common_vsprintf_s 57E wcscpy_s 548 strcpy_s 19A _initterm_e 199 _initterm 15 _CrtDbgReportW 14 _CrtDbgReport 82 __stdio_common_vfprintf 45 __acrt_iob_func 3E4 _wsplitpath_s 3C8 _wmakepath_s 565 terminate 10C _execute_onexit_table E7 _crt_at_quick_exit 544 strcat_s E8 _crt_atexit CA _cexit Summary 1000 .00cfg 1000 .data 1000 .idata 2000 .rdata 1000 .reloc 1000 .rsrc 6000 .text 10000 .textbss
在上面,我們會看到很多沒有用到的函數也在里面,這是因為在構建Windows DLL時,還鏈接了支持DLL運行的基本運行庫,這個基本運行庫需要用到Kerne132.dll,所有就有了這些導入函數。
在Windows中,系統的裝載器會確保任何一個模塊的依賴條件都得到滿足,每個PE文件所依賴的文件都被加載。
在動態鏈接過程中,如果某一個被依賴的模塊無法正確的加載,那么系統將會錯誤提升(比如:缺少某個DLL),並且終止運行該進程。
導入表結構體也在”Winnt.h”中:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) //導入名稱表,簡稱INT。和IAT一樣 } DUMMYUNIONNAME; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) //指向一個導入地址數組,IAT是導入表中最重要的結構,IAT中每個元素對應一個被導入的符號,元素的值在不同情況下有不同的含義。 } IMAGE_IMPORT_DESCRIPTOR;
延遲加載
當鏈接一個支持延遲載入的DLL時,鏈接器會產生與普通DLL導入非常類似的數據。但操作系統會忽略這些數據。當延遲載入的API第一次被調用時,由鏈接器添加特殊的樁代碼,這個樁代碼負責對DLL的裝載工作。然后這個樁代碼通過調用GetPROCAddress來找到被調用API地址
9.2.5 導入函數的調用
與GOT類似
9.3 DLL優化
DLL的代碼段和數據段本身並不是地址無關的,它默認需要被裝載到由ImageBase指定的目標地址中。如果目標地址被占用,那么就需要裝載到其他地址,便會引起整個DLL的Rebase。
符號和字符串的比較和查找過程也會影響DLL性能。
9.3.1 重定基地址(Rebasing)
PE的DLL中的代碼段並不是地址無光的,它在被裝載時有一個固定的目標地址,這個地址也就是PE里面所謂的基地址。
對於DLL來說,一個進程中,多個DLL不可以被裝載到同一個虛擬地址,每個DLL所占用的虛擬地址區域之間都不可以重疊。
Windows PE采用裝載時重定位:在DLL模塊裝載時,如果目標地址被占用,那么操作系統就會為它分配一塊新的空間,並且將DLL裝載到該地址。對於DLL每個絕對地址引用都進程重定位。
由於DLL內部地址都是基於基地址的,或者是相對於基地址的RVA。那么所有需要重定位的地址都只需要加上一個固定差值。PE里面把這種特殊的重定位過程叫做重定基地址。
EXE是不可以重定位的,不過這也沒問題,因為EXE文件是進程運行時第一個裝入虛擬空間的,所以它的地址不會被人搶占。
改變默認基地址
對於一個程序來說,它所用到的DLL基本是固定的程序每次運行時,這些DLL的裝載順序和地址也是一樣的。
系統DLL
Windows系統在進程空間中專門划出一塊0x70000000~0x80000000區域,用於映射這些常用的系統DLL。
9.3.2 序號
一個DLL中每一個導出的函數都有一個對應的序號。一個導出函數甚至沒有函數名,但它必須有唯一的序號。序號標示被導出函數地址在DLL導出表中位置。
9.3.3 導入函數綁定
當一個程序運行時,所有被依賴的DLL都會被裝載,並且一系列導入導出符號依賴關系都會被重新解析。這些DLL都會以同樣的順序被裝載到同樣的內存地址,所以它們的導出符號的地址都是不變的。
DLL綁定:使用editbin可以對EXE和DLL綁定。
editbin對綁定的程序的導入符號進行遍歷查找,找到以后就把符號的運行時的目標地址寫入到被綁定程序的導入表內。
INT這個數組就是用來保存綁定符號的地址的。
DLL綁定地址失效:
- 一種情況,被依賴的DLL更新導致DLL的導入函數地址發生變化。
- 另一種情況,被依賴的DLL在裝載時發生重定基址,導致DLL的裝載地址與被綁定時不一致。
第一種情況的失效,PE做法時當對程序綁定時,對於每個導入的DLL,鏈接器把DLL的時間戳和校驗和保存到被綁定的PE文件的導入表中。在運行時,Windows會核對將要被裝載的DLL與綁定時的DLL版本是否相同,並且確認該DLL沒有發生重定基址。
9.4 C++與動態鏈接
C++編寫動態鏈接庫在Windows平台下最好遵循以下指導:
- 所有的接口都應該抽象
- 所有的全局函數都應該使用”extern C”來防止名字修飾的不兼容
- 不要使用C++標准庫STL
- 不要使用異常
- 不要使用虛析構函數
- 不要在DLL里面申請內存
- 不要在接口中使用重載方法
9.5 DLL HELL
由於早期Windows缺乏一種很有效的DLL版本控制機制,DLL不兼容文件在Windows非常嚴重,被人們稱為DLL噩夢(DLL hell)。
DLL HELL發生的三種可能原因:
- 由使用舊版本的DLL替代原來一個新版本的DLL引起
- 由新版DLL中的函數無意發生改變而引起
- 由新版DLL按照引入一個新BUG
解決DLL Hell的方法
- 靜態鏈接
- 防止DLL覆蓋:使用windows保護技術
- 避免DLL沖突:每個應用程序擁有一份自己依賴的DLL
- .NET 下DLL Hell的解決方案:在.NET框架中,一個程序集有兩種類型:應用程序集以及庫程序。一個程序集包塊一個或多個文件,所以需要一個清單文件來描述程序集。這個清單文件叫做Manifest文件。Manifest文件描述了程序集的名字,版本號以及程序集的各種資源,同時也描述了該程序集的運行所依賴的資源,包括DLL以及其他資源文件等。操作系統會根據DLL的manifest文件去尋找對應的DLL並調用。