WIN32彙編: 17.動態鏈接庫

第十七課 動態鏈接庫


本課中,我們將學習DLLs,它們到底是什麼和如何創建它們。
 

理論:

如果您編程的時間非常長,就會發現很多的程序之間其實有相當多的重複代碼。每編一個程序就重寫一遍這些代碼既沒必要又浪費時間。在DOS時代,一般的做法是把這些重複的代碼寫成一個個的函數,然後把它們按類別放到不同的庫文件中去。當要使用這些函數時,只要把您的目標文件(.obj)文件和先前存放在庫文件中的函數進行鏈接,鏈接時鏈接器會從庫文件中抽取相關的信息並把它們插入到可執行文件中去。這個過程叫做靜態鏈接。C運行時庫就是一個好例子。這樣的庫的缺點是您在每一個調用庫函數的程序中都必須嵌入同一函數的拷貝,這顯然很浪費磁盤。在DOS時代畢竟每一時刻僅有一個程序在運行,所以浪費的還只是磁盤而已,在多任務的WINDOWS時代就不僅浪費磁盤,還要浪費寶貴的內存了。

在WINDOWS中,由於有多個程序同時運行,如果您的程序非常大的話,那將消耗相當多的內存。WINDOWS的解決辦法是:使用動態鏈接庫。動態鏈接庫從表面上看也是一大堆的通用函數,不過即使有多個程序調用了它,在內存中也僅僅只有動態鏈接庫的唯一一份拷貝。WINDOWS是通過分頁機制來作到這一點的。當然,庫的代碼只有一份,但是每一個應用程序要有自己單獨的數據段,要麼就會亂掉。
不象舊時的靜態鏈接庫,它並不會把這些函數的可執行代碼放入到應用程序中去,而是當程序已經在內存中運行時,如果需要調用該函數時才調入內存也即鏈接。這也就是爲什麼把它叫做“動態”的原因所在。另外您還可以動態地卸載動態鏈接庫,當然要求這時沒有其它的應用程序在使用它,否則就要一直等到最後一個使用它的函數也不再使用該動態鏈接庫時才能去卸載它。
爲了正確的調用庫和給庫函數分配內存空間,在編譯和鏈接應用程序時,必須把重定位等一些消息插入到執行代碼中去,以便載入正確的庫,並給庫函數分配正確的地址。
那麼這些信息從哪裏得到呢?引入庫。引入庫包含足夠的信息,鏈接器從中抽取足夠的信息(注意區別:靜態鏈接庫放入的是可執行代碼)把它們放入到可執行文件中去。當WINDOWS的加載器裝入應用程序查看到有DLL時,它會查找該庫文件,如果沒有查到,就報錯退出,否則就把它映射進進程的地址空間,並修正函數調用語句的地址。
如果沒有引入庫呢?當然我們也可以調用動態鏈接庫中的任意函數。只不過您必須知道調用的函數是否在庫中而且是否在庫的引出名字表中,另外還需要知道該函數的參數個數和參數的類型。

(譯者加:說到這裏,讓我想起了一件很有名的事。<<Undocumented Windows>>一書的作者Angel Schudleman 曾經利用此方法來跟蹤微軟Win3x系統動態鏈接庫中未公開的函數,因爲在微軟給程序員提供的系統動態鏈接庫的引入庫中沒有提供這些函數的原型,所以您無法在鏈接時把這些函數的信息鏈接到可執行文件中去,而爲了某種目的您又要使用這些函數,您就可以在執行時加載動態鏈接庫並得到這些函數的地址,從而和調用其它的庫函數一樣使用這些未公開的函數。由於這本書的巨大影響,當時許多程序員紛紛在它們的程序中調用未公開函數,甚至在寫商業程序時也這麼做。這種走偏峯的做法引起了微軟的反感,後來微軟在它Win3x的改進版中不再把那些未公開函數列入系統動態鏈接庫的引出名字表,這樣也就無法再利用這種方法來調用未公開的函數了。)

  • 當您讓系統的加載器爲您加載動態庫時,如果不能找到庫文件,它就會提示一條“A required .DLL file, xxxxx.dll is missing”,這樣您的應用程序就無法運行,即使該庫對您的應用程序來說並不重要。
  • 如果您選擇在程序運行時自己加載該庫,就沒有這種問題了。
  • 如果您知道足夠的信息,就可以調用系統未公開的函數。
  • 如果您調用LoadLibrary函數加載庫,就必須再調用GetProcAddress函數來得到每一個您想調用的函數的地址,GetProcAddress會在動態鏈接庫中查找函數的入口地址。由於多餘的步驟,這樣您的程序執行起來會慢一點,但是並不明顯。
明白了LoadLibrary函數的優缺點,下面我們就來看看如何產生一個動態鏈接庫。下面的代碼是一個動態鏈接庫的框架:

;--------------------------------------------------------------------------------------
;                           DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
includelib /masm32/lib/user32.lib
includelib /masm32/lib/kernel32.lib

.data
.code
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
        mov  eax,TRUE
        ret
DllEntry Endp
;---------------------------------------------------------------------------------------------------
;下面是一個空函數,您可以象下面一樣插入您的函數。
;----------------------------------------------------------------------------------------------------
TestFunction proc
    ret
TestFunction endp

End DllEntry

;-------------------------------------------------------------------------------------
;                              DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY   DLLSkeleton
EXPORTS   TestFunction
 

上面是一個動態鏈接庫的框架,每一個DLL必須有一個入口點函數,WINDOWS每一次在做下面的動作時會調用該入口點函數:

  • 當動態鏈接庫被加載時
  • 當動態鏈接庫卸載時
  • 同一進程的線程生成時
  • 同一進程的線程退出時

DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
        mov  eax,TRUE
        ret
DllEntry Endp

入口點函數的名稱無所謂只要您讓語句“END<函數名>”中的函數名和前面的相同就可以了。該函數共有三個參數,只有前面兩個是重要的。
hInstDLL是該動態鏈接庫模塊的句柄。它和進程的實例句柄不一樣。如果您以後要用,可以保存它,因爲以後再要獲得它不容易。
根據不同的時機,reason傳入的值可能是下面的四個值中的一個:

  • DLL_PROCESS_ATTACH 動態鏈接庫第一次插入進程的地址空間時。當傳入的參數是該值時,您可以做一些初始化的工作。
  • DLL_PROCESS_DETACH 動態鏈接庫從進程的地址空間卸出時。您可以在此做一些清理的工作。譬如:釋放內存等。
  • DLL_THREAD_ATTACH 新線程生成。
  • DLL_THREAD_DETACH 線程銷燬。

如果想要庫中的代碼繼續執行,返回TRUE,否則返回FALSE,那樣動態鏈接庫就不會加載了。譬如:您想分配一塊內存,如果不成功的話就退出,這時您就可以返回FALSE。那樣動態鏈接庫就不會加載了。
您可以加入的函數,它們的位置並不重要,把它們放在入口點函數的前面或後面都可以。只是如果您想要它們能被其它的程序調用的話,就必須把它們的名字放到模塊定義文件(.def)中去。
動態鏈接庫在它們自己的編譯過程就需要,而不只是提供給其它要引用它的程序參考。他們如下:

LIBRARY   DLLSkeleton
EXPORTS   TestFunction

第一行是必須的。LIBRARY 定義了DLL的模塊名稱。它必須和動態鏈接庫的名稱相同。
EXPORTS關鍵字告訴鏈接器該DLL的引出函數,也就是其它程序可以調用的函數。舉個例子:其它的程序想要調用函數TestFunction ,我們就把它放到EXPORTS中。
還有就是,鏈接器的選項中必須放入開關項:/DLL 和/DEF<DLL文件名>,就像下面這樣:

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:/masm32/lib DLLSkeleton.obj

編譯器的開關選項是一樣的,即:/c /coff /Cp。在您鏈接好後,鏈接器會生成.lib 和.dll文件。前者是引入庫,當其它的程序要調用您的動態鏈接庫中的函數時就需要該引入庫,以便把必要的信息加入到其可執行文件中去。
接下來我們來看看如何使用LoadLibrary函數來加載一個DLL。

;---------------------------------------------------------------------------------------------
;                                      UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include /masm32/include/windows.inc
include /masm32/include/user32.inc
include /masm32/include/kernel32.inc
includelib /masm32/lib/kernel32.lib
includelib /masm32/lib/user32.lib

.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0

.data?
hLib dd ?                                         ; 動態鏈接庫的句柄 (DLL)
TestHelloAddr dd ?                        ; TestHello 函數的地址

.code
start:
        invoke LoadLibrary,addr LibName
;---------------------------------------------------------------------------------------------------------
; 調用LoadLibrary,其參數是欲加載的動態鏈接庫的名稱。如果調用成功,將返回該DLL的句柄。 否則返回NULL。該句柄可以傳給 :library函數和其它需要動態鏈接庫句柄的函數。
;-----------------------------------------------------------------------------------------------------------
        .if eax==NULL
                invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
        .else
                mov hLib,eax
                invoke GetProcAddress,hLib,addr FunctionName
;-----------------------------------------------------------------------------------------------------------
; 當您得到了動態鏈接庫的句柄後,把它傳給GetProcAddress函數,再把您要調用的函數的名稱 也傳給該函數。如果成功的話,它:會返回想要的函數的地址,失敗的話返回NULL。除非卸載該 動態鏈接庫否則函數的地址是不會改變的,所以您可以把它保存到一個:全局變量中以備後用。
;-----------------------------------------------------------------------------------------------------------
                .if eax==NULL
                        invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
                .else
                        mov TestHelloAddr,eax
                        call [TestHelloAddr]
;-----------------------------------------------------------------------------------------------------------
; 以後您就可以和調用其它函數一樣調用該函數了。其中要把包含函數地址信息的變量用方括號括起來。
;-----------------------------------------------------------------------------------------------------------
                .endif
                invoke FreeLibrary,hLib
;-----------------------------------------------------------------------------------------------------------
;調用FreeLibrary卸載動態鏈接庫。
;-----------------------------------------------------------------------------------------------------------
        .endif
        invoke ExitProcess,NULL
end start

使用LoadLibrary函數加載動態鏈接庫,可能要自己多做一些工作,但是這種方法確實是提供了許多的靈活性。

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