1. 程式人生 > >dll工作基本原理

dll工作基本原理

Windows DLL基本原理

Windows系統平臺上,你可以將獨立的程式模組建立為較小的DLL(Dynamic Linkable Library)檔案,並可對它們單獨編譯和測試。在執行時,只有當EXE程式確實要呼叫這些DLL模組的情況下,系統才會將它們裝載到記憶體空間中。這種方式不僅減少了EXE檔案的大小和對記憶體空間的需求,而且使這些DLL模組可以同時被多個應用程式使用。Microsoft Windows自己就將一些主要的系統功能以DLL模組的形式實現。例如IE中的一些基本功能就是由DLL檔案實現的,它可以被其它應用程式呼叫和整合。一般來說,DLL是一種磁碟檔案(通常帶有DLL副檔名,是標準win32

可執行檔案-“PE”格式),它由全域性資料、服務函式和資源組成,在執行時被系統載入到程序的虛擬空間中,成為呼叫程序的一部分,程序中所有執行緒都可以呼叫其中的函式。如果與其它DLL之間沒有衝突,該檔案通常對映到程序虛擬空間的同一地址上。DLL模組中包含各種匯出函式,用於向外界提供服務。Windows在載入DLL模組時將程序函式呼叫與DLL檔案的匯出函式相匹配。

在Win32環境中,每個程序都複製了自己的讀/寫全域性變數。如果想要與其它程序共享記憶體,必須使用記憶體對映檔案或者宣告一個共享資料段。DLL模組需要的堆疊記憶體都是從執行程序的堆疊中分配出來的。

DLL檔案中包含一個匯出函式表(存在於PE的.edata節中)。這些匯出函式由它們的符號名和稱為標識號的整數與外界聯絡起來。函式表中還包含了DLL中函式的地址。當應用程式載入DLL模組時時,它並不知道呼叫函式的實際地址,但它知道函式的符號名和標識號。動態連結過程在載入的DLL模組時動態建立一個函式呼叫與函式地址的對應表。如果重新編譯和重建DLL檔案,並不需要修改應用程式,除非你改變了匯出函式的符號名和引數序列。

簡單的DLL檔案只為應用程式提供匯出函式,比較複雜的DLL檔案除了提供匯出函式以外,還呼叫其它DLL檔案中的函式。

每個DLL都有一個入口函式(DLLMain),系統在特定環境下會呼叫DLLMain。在下面的事件發生時會呼叫dll入口函式:1.程序裝載DLL。2.程序解除安裝DLL。3.DLL在被裝載之後建立了新執行緒。4. DLL在被裝載之後一個執行緒被終止了。

應用程式匯入函式與DLL檔案中的匯出函式進行連結有兩種方式:隱式連結和顯式連結。

隱式連結(load-time dynamic linking)是指在應用程式中不需指明DLL檔案的實際儲存路徑,程式設計師不需關心DLL檔案的實際裝載(由編譯器自動完成地址分配)。採用隱式連結方式,程式設計師在建立一個DLL檔案時,連結程式會自動生成一個與之對應的LIB匯入檔案。該檔案包含了每一個DLL匯出函式的符號名和可選的標識號,但是並不含有實際的程式碼。LIB檔案作為DLL的替代檔案被編譯到應用程式專案中。當程式設計師通過靜態連結方式編譯生成應用程式時,應用程式中的呼叫函式與LIB檔案中匯出符號相匹配,這些符號或標識號進入到生成的EXE檔案中。LIB檔案中也包含了對應的DLL檔名(但不是完全的路徑名),連結程式將其儲存在EXE檔案內部。當應用程式執行過程中需要載入DLL檔案時,Windows根據這些資訊發現並載入DLL,然後通過符號名或標識號實現對DLL函式的動態連結。我們使用的大部分系統Dll就是通過這樣的方式連結的。若找不到需要的Dll則會給出一個Dll缺少的錯誤訊息。

顯式連結(run-time dynamic linking)與此相反。使用者程式在編譯的時候並沒有指明需要哪些Dll,而是在執行起來之後呼叫Win32 的LoadLibary()函式,去裝載Dll。若沒有找到Dll則這個函式就會返回一個錯誤。在用LoadLibary()函式裝載Dll之後,應用程式還需要用GetProcAdress()函式去獲得Dll輸出函式的地址。顯式連結方式對於整合化的開發語言比較適合。有了顯式連結,程式設計師就不必再使用匯入檔案,而是直接呼叫Win32 的LoadLibary()函式,並指定DLL的路徑作為引數。還要說明一點的就是Known Dlls就是保證在通過LoadLibary()去裝載系統Dll的時候,只從特定的系統目錄去裝載,防止裝載錯。裝載的時候會去看登錄檔下是否有一樣的登錄檔鍵名。如果是裝載windows\system32\目錄下的對應的Dll。

Dll的搜尋順序,在Windows上有個登錄檔鍵值決定了Dll的搜尋順序:HKLM\System\CurrentControlSet\SessionManager\SafeDllSearchMode。在vista,server2003,xp sp2中這個值為1,在xp,2000 sp4中為0。1值時的搜素順序為:1.可執行檔案所在目錄,2.系統目錄windows\system32\,3. 16位系統目錄,4.windows目錄,5.當前程序目錄。6.環境變數PATH中的目錄。0值時的搜素順序為:1.可執行檔案所在目錄,2. 當前程序目錄。3.系統目錄windows\system32\,4. 16位系統目錄,5.windows目錄,6.環境變數PATH中的目錄。

DLL的載入與連線

Windows DLL裝入(除ntdll.dll外)和連線是通過ntdll.dll中一個函式LdrInitializeThunk實現的。先對LdrInitializeThunk()這個函式名作些解釋“Ldr顯然是“Loader”的縮寫。而“Thunk”意為“翻譯”、“轉換”、或者某種起著“橋樑”作用的東西。這個詞在一般的字典中是查不到的,但卻是個常見於微軟的資料、文件中術語。這個術語起源於編譯技術,表示一小片旨在獲取某個地址的程式碼,最初用於函式呼叫時“形參”和“實參”結合。後來這個術語有了不少新的特殊含義和使用,但是DLL的動態連線與函式呼叫時“形實結合”確實有著本質的相似。

由於Windows沒有公開這個函式的程式碼,所以學習起來比較困難,只能通過查閱一些資料來大概猜測這個函式的實現。這個過程中也參看了很多ReactOS(ReactOS是一個免費而且完全相容 Microsoft Windows XP 的作業系統。ReactOS 旨在通過使用類似構架和提供完整公共介面實現與 NT 作業系統二進位制下的應用程式和驅動裝置的完全相容。)的LdrInitializeThunk()函式實現原始碼。

在進入這個函式之前,目標 EXE映像已經被對映到當前程序的使用者空間,系統DLL ntdll.dll的映像也已經被對映,但是並沒有在EXE映像與ntdll.dll映像之間建立連線 (實際上 EXE映像未必就直接呼叫ntdll.dll中的函式)。LdrInitializeThunk()是ntdll.dll中不經連線就可進入的函式,實質上就是ntdll.dll的入口。除ntdll.dll以外,別的 DLL都還沒有被裝入(對映)。此外,當前程序(除核心中的“程序控制塊”EPROCESS等資料結構外)在使用者空間已經有了一個“程序環境塊”PEB,以及該程序的第一個“執行緒環境塊”TEB。這就是進入 LdrInitializeThunk()前的“當前形勢”。

PEB中有一個欄位Ldr是個PEB_LDR_DATA結構指標,所指向的資料結構用來為本程序維持三個“模組”佇列、即InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList。這裡所謂“模組”就是PE格式的可執行映像,包括EXE映像和DLL映像。前兩個佇列都是模組佇列,第三個是初始化佇列。兩個模組佇列的不同之處在於排列的次序,一個是按裝入的先後,一個是按裝入的位置。每當為本程序裝入一個模組、即.exe映像或DLL映像時,就要為其分配,建立一個LDR_DATA_TABLE_ENTRY資料結構,並將其掛入InLoadOrderModuleList。然後,完成對這個模組的動態連線以後,就把它掛入InInitializationOrderModuleList佇列,以便依次呼叫它們的初始化函式。相應地,LDR_DATA_TABLE_ENTRY資料結構中有三個佇列頭,因而可以同時掛在三個佇列中。在我做的小實驗當中就是通過查詢這三個佇列,來將當前程序的Dll載入資訊顯示出來的。具體的例項請見我的實驗說明文件。

在LdrInitializeThunk()中,最開始為做的事情就是將載入的模組資訊存放在PEB中的ldr欄位,如上面一段文字中所述。之後,LdrInitializeThunk()函式又呼叫了一個叫LdrPEStartup()的函式。LdrPEStartup()函式首先判斷了“期望地址”是否可用,PE映像的NtHeader(peb中有個ImageBaseAddress的地址,代表exe映像在使用者空間的位置,在這個地址指向的資料結構中就有NtHeader的結構)中有個指標,指向一個OptionalHeader。在OptionalHeader中有個欄位ImageBase,是具體映像建議、或者說希望被裝入的地址,我們稱之為“願望地址”。在裝入一個映像時,只要相應的區間(取決於它的期望地址和大小)空閒,就總正常裝入。但是如果與已經被佔用的區間相沖突,就只好利用LdrPerformRelocations()換個地方。

那麼映像的願望地址有著什麼物理的或者邏輯的意義呢?我們知道,軟體在編譯以後有個連線的過程,即為函式的呼叫者落實被呼叫函式的入口地址、為全域性變數(按絕對地址)的使用者落實變數地址的過程。連線有靜態和動態兩種,靜態連線是在“製造”軟體時進行的,而動態連線則是在使用軟體時進行的。儘管EXE模組和DLL模組之間的連線是動態連線,但是EXE或DLL模組內部的連線卻是靜態連線。既是靜態連線,就必須為模組的映像提供一個假定的起點地址。如果以此假定地址為基礎進行連線以後就不可變更,使用時必須裝入到這個地址上,那麼這個地址就是固定的“指定地址”了。早期的靜態連線往往都是使用指定地址的。但是,如果允許按假定地址連線的映像在實際使用時進行“重定位”,那麼這假定地址就是可浮動的“願望地址”了。可“重定位”的靜態連線當然比固定的靜態連線靈活。事實上,要是沒有可“重定位”的靜態連線技術,DLL的使用就無法實現,因為根本就不可能事先為所有可能的DLL劃定它們的裝入位置和大小。至於可“重定位”靜態連線的實現,則一般都是採用間接定址,通過指標來實現。

所謂重定位,就是計算出實際裝入地址與建議裝入地址間的位移a,然後調整每個重定位塊中的每一個重定位項、即指標,具體就是在指標上加a。而映像中使用的所有絕對地址(包括函式入口、全域性量資料的位置)實際上用的都是間接定址,每個這樣的地址都有個指標存在於某個重定位塊中。

完成了可能需要的EXE映像重定位以後,下一個主要的操作就是LdrFixupImports()了。實際上這才是關鍵所在,它所處理的就是當前模組所需DLL模組的裝入和連線。各DLL的程式入口記錄在它們的LDR_DATA_TABLE_ENTRY資料結構中藉助InInitializationOrderModuleList佇列就可依次呼叫所有DLL的初始化函式。

NtHeader的OptionalHeader中有個陣列DataDirectory[],其中之一是重定位目錄。除此之外,陣列中還有“(普通)引入(import)”、“繫結引入(bound import)”以及其它多種目錄,但是我們在這裡只關心“引入”和“繫結引入”。這兩個目錄都是用於庫函式的引入,但是作用不同,目錄項的資料結構也不同。每個引入目錄項都代表著一個被引入模組,其模組名、即檔名在dwRVAModuleName(ReactOS中的名字,下同)所指的地方。需要從同一個被引入模組引入的函式通常有很多個,dwRVAFunctionNameList指向一個字串陣列,陣列中的每一個字串都是一個函式名;與此相對應,dwRVAFunctionAddressList則指向一個指標陣列。這兩個陣列是平行的,同一個函式在兩個陣列中具有相同的下標。從一個被引入模組中引入一個函式的過程大體上就是:根據函式名在被引入模組的引出目錄中搜索,找到目標函式以後就把它實際裝入後的入口地址填寫到指標陣列中的相應位置上。但是,這個過程可能是個開銷相當大、速度比較慢的過程。為此,又發展起一種稱為“繫結”的優化。

所謂繫結,就是在軟體的編譯,連線過程中先對使用時的動態連線來一次預演,預演時假定所有的DLL都被裝入到它們的願望地址上,然後把預演中得到的被引入函式的地址直接記錄在引入者模組中相應引入目錄下的指標陣列中。這樣,使用軟體時的動態連線就變得很簡單快捷,因為實際上已經事先連線好了。其實“繫結引入”和靜態連線並無實質的不同。但是,各模組的版本配套就成為一個問題,因為萬一使用的某個DLL不是當初繫結時的版本,而且其引出目錄又發生了變化,就有可能引起混亂。為此,PE格式增加了一種“繫結引入”目錄,相關的機制會進行判斷。但是,“繫結引入”畢竟不是很可靠的,萬一發現版本不符就不能使用原先的綁定了。所以“繫結引入”不能單獨存在,而必須有普通引入作為後備。如果不符就不能按“繫結引入”目錄處理引入,而只好退而求其次,改成按普通“引入”目錄處理引入。另一方面,所謂“繫結”是指當被引入模組裝入在預定位置上時的地址繫結,如果被引入模組的裝入位置變了,就得對原先所繫結的地址作相應的調整、即“重定位”。

LdrFixupImports()函式首先從映像頭部獲取指向“引入”目錄和“繫結引入”目錄的指標。若存在“繫結引入”目錄,則先通過LdrpGetOrLoadModule()找到或裝入(對映)被引入模組的映像。首先當然是在模組佇列中尋找,找不到就從被引入模組的磁碟檔案裝入。之後檢查繫結版本是否一致,如果不一致就退而求其次,通過LdrpProcessImportDirectory()處理引入。當然,那樣一來效率就要降低了。如果一致,則返回(因為在“預演”中已經連線好,效率當然高了)。而LdrpProcessImportDirectory()才是真正意義上的動態連線!!(說了這麼多原來才開始……)。

LdrpProcessImportDirectory()首先根據目錄項中的兩個位移量取得分別指向函式名字串陣列和函式指標陣列的指標。這兩個陣列是平行的(前面有介紹),然後對字串陣列中的元素計數,得到該陣列的大小IATSize。顯然,函式指標陣列的大小也是IATSize。這裡IAT是“引入地址表(Imported Address Table)”的縮寫,其實就是函式指標陣列。這個陣列在映像內部,其所在的頁面在裝入映像時已被加上防寫,而下面要做的事正是要改變這些指標的值,所以先要通過NtProtectVirtualMemory()把這些頁面的訪問模式改成可讀可寫。做完這些準備之後,下面就是連線的過程了,那就是根據需要把被引入模組所引出的函式入口(地址)填寫到引入者模組的IAT中。與當前模組中的兩個陣列相對應,在被引入模組的“引出”目錄中也有兩個陣列,說明本模組引出函式的名稱和入口地址(在映像中的位移)。當然,這兩個陣列也是平行的。要獲取被引入模組中的函式入口有兩種方法,即按序號(Ordinal)引入和按函式名引入。從而分別呼叫LdrGetExportByOrdinal()和LdrGetExportByName()。這兩個函式都返回目標函式在本程序使用者空間中的入口地址,把它填寫入當前模組引入目錄函式指標陣列中的相應元素,就完成了一個函式的連線。當然,同樣的操作要迴圈實施於當前模組需要從給定模組引入的所有函式,並且(在上一層)迴圈實施於所有的被引入模組。完成了對一個被引入模組的連線之後,又呼叫NtProtectVirtualMemory()恢復當前模組中給定目錄項內函式指標陣列所在頁面的保護。

到此,我們大概的清楚Windows Dll的載入與連線過程。

總結與感想

以上的Dll載入與連線過程有點抽象與混亂,在這裡進行總結,絕體的函式載入關係如下:

DLL基本原理與載入連線的實現” style=”margin:0px;padding:0px;border:0px;list-style:none;”>

        </div>
            </div>
        </article>

相關推薦

dll工作基本原理

Windows DLL基本原理 Windows系統平臺上,你可以將獨立的程式模組建立為較小的DLL(Dynamic Linkable Library)檔案,並可對它們單獨編譯和測試。在執行時,只有當EXE程式確實要呼叫這些DLL模組的情況下,系統才會將

flannel vxlan工作基本原理及常見排障方法

寫在前面 最近用kubeadm鼓搗了幾個cluster叢集測試用,網路用的flannel。因為這些機器都不是純淨的環境(以前部署過其他的k8s或者有一些特別的設定),所以部署起來遇到了很多問題。看了下相關的文章,梳理了flannel的vxlan的工作原理,成功對這幾個環境進行了排障。本文主要是相關流程的筆記記

HDFS基本原理工作機制(一)——初識HDFS

HDFS簡介 HDFS 源於 Google 在2003年10月份發表的GFS(Google File System) 論文。 是 GFS 的一個克隆版本 HDFS(Hadoop Distributed File System)是Hadoop專案的核心子專案,是分散式計算中資料

gdb工作基本原理

Gdb組成部分 GDB由三個部分組成: (1)使用者介面user interface,除支援傳統的CLI介面還支援mi介面(ddd等工具使用) (2)符號處理層symbol handling,當gdb ./debugme後GDB會讀取檔案的符號資訊,之

DLL檔案常識and基本原理及修改方法

 一、DLL檔案常識   DLL是Dynamic Link Library的縮寫,意為動態連結庫。在Windows中,許多應用程式並不是一個完整的可執行檔案,它們被分割成一些相對獨立的動態連結庫,即DLL檔案,放置於系統中。當我們執行某一個程式時,相應的DLL檔案就會被呼叫。

貝葉斯算法的基本原理和算法實現

utf shape less 流程 我們 def .sh 詞向量 貝葉斯算法 一. 貝葉斯公式推導   樸素貝葉斯分類是一種十分簡單的分類算法,叫它樸素是因為其思想基礎的簡單性:就文本分類而言,它認為詞袋中的兩兩詞之間的關系是相互獨立的,即一個對象 的特征向量

JAVA語言開發基本原理

源文件 cli lips font 實現 環境 java字節碼 類庫 java開發工具 1.java編譯運行過程   java源文件(.java)經過編譯,編譯為java字節碼文件(.class),JVM來加載.class文件並運行.class文件。 2.JVM   不同系

哈希(Hash)與加密(Encrypt)的基本原理、區別及工程應用

class 區別 自己 裏的 lpad returns .net 角度 table 0、摘要 今天看到吉日嘎拉的一篇關於管理軟件中信息加密和安全的文章,感覺非常有實際意義。文中作者從實踐經驗出發,討論了信息管理軟件中如何通過哈希和加密進行數據保護。但是從文章評論

計算機程序的思維邏輯 17 - 繼承實現的基本原理

pass his aoe bin 原理 aer and 思維 bit %E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A8%8B%E5%BA%8F%E7%9A%84%E6%80%9D%E7%BB%B4%E9%80%BB%E8%BE%91%2017%20-%20

計算機程序的思維邏輯 12 - 函數調用的基本原理

san emd insight msm cgo xiang pri car 程序 spring%E6%9C%8D%E5%8A%A1%E5%AE%9A%E4%BD%8D%E5%99%A8%EF%BC%8C%E5%8F%AF%E5%9C%A8%E4%BB%BB%E4%BD%95

防盜鏈的基本原理與實現

rec eal limit ole 站點 new exceptio stub text 1. 我的實現防盜鏈的做法,也是參考該位前輩的文章。基本原理就是就是一句話:通過判斷request請求頭的refer是否來源於本站。(當然請求頭是來自於客戶端的,是可偽造的,暫不在本文

Objection基本原理

navi dex https jsb logs implement efault 默認 center 1,Objection 的簡介 就是一個依賴註入框架,github地址:https://github.com/atomicobject/objection 2,Objec

【SSH進階之路】Struts基本原理 + 實現簡單登錄(二)

target doctype 掌握 pack insert enter snippet file manage 上面博文,主要簡單的介紹了一下SSH的基本概念,比較宏觀。作為剛開始學習的人可以有一個總體上的認識,個人覺得對學習有非常好的輔助功能,它不不過

【轉】哈希(Hash)與加密(Encrypt)的基本原理、區別及工程應用

phy 理論 靈活運用 十分 實際應用 廣泛 tle 多網站 net 0、摘要 今天看到吉日嘎拉的一篇關於管理軟件中信息加密和安全的文章,感覺非常有實際意義。文中作者從實踐經驗出發,討論了信息管理軟件中如何通過哈希和加密進行數據保護。但是從文章評論中也可以

Kafka 基本原理

本地 fix streams 均衡 fig rgs exception format 公司 簡介 Apache Kafka是分布式發布-訂閱消息系統。它最初由LinkedIn公司開發,之後成為Apache項目的一部分。Kafka是一種快速、可擴展的、設計內在就是分布式的

API Hook基本原理和實現

use 概率 缺省 後綴 origin gif object cati mov API Hook基本原理和實現 2009-03-14 20:09 windows系統下的編程,消息message的傳遞是貫穿其始終的。這個消息我們可以簡單理解為一個有特定

分布式事務處理基本原理

分布式系統 保存 idt 用戶 新的 標準 nbsp 對數 兩個 事務是有一系列對系統中數據進行訪問與更新的操作組成的一個基本的程序邏輯執行單元。引入事務的概念有兩個目的,第一,事務對多個並發訪問的應用程序進行隔離,防止彼此幹擾,第二,事務為數據庫操作序列提供了一個失敗回復

zookeeper基本原理

基於 同步 服務集群 設計 服務 高性能 官方 可靠 需要 服務集群對外提供服務的過程中,有很多的配置需要隨時更新,服務間需要協調工作,這些信息如何推送到各個節點?並且保證信息的一致性和可靠性? 用Zookeeper實現了一 個配置管理中心,利用Zookeeper將配置信

交換機的基本原理與配置

mac地址 console 以太網幀 securecrt 楊書凡 交換機工作在數據鏈路層,負責網絡相鄰節點之間的數據通信,並進行流量控制,主要通過幀在對等層間數據傳輸。在物理線路上提供可靠的數據傳輸,對網絡層而言為一條無差錯的線路。 MAC地址 計算機聯網的必備硬件是網卡,每

路由器的基本原理與配置命令(靜態路由和默認路由)

路由技術 路由表 route命令 路由環路 楊書凡 路由器工作在OSI參考模型的網絡層,它的重要作用是為數據包選擇最佳路徑,最終送達目的地。那麽路由器是怎樣選擇路徑的呢?如果主機A要和主機B通信,就需要一種方法判斷源主機和目標主機所經過的最佳路徑,從而進行數據轉發,這就是路由技術。