【面經筆記】Windows下的動態鏈接(DLL)

使用DLL的優點

共享、模塊化,可方便的組合,重用,升級


基地址和RVA

當一個PE文件裝載時,其進程地址空間中的起始地址就是基地址,對於可執行文件exe,一般爲0x400000,對於DLL文件一般爲0x10000000。

若該地址被佔用,則會選用其他空閒的地址,RVA就是一個地址相對於基地址的偏移。


聲明

可以通過_declspec(dllexport)表示該符號是從dll導出的符號,_declspec(dllimport)表示符號從別的dll導入的符號。

在C++中,如果希望導入導出的符號符合C的規範,必須在這個符號的定義之前加上extern” C”,防止C++編譯器進行符號修飾。

默認情況下,MSVC把C語言的函數當做_cdecl類型,這種情況下,它對該函數不進行任何符號修飾。

但一旦我們使用其他函數調用規範時,MSVC編譯器就會對符號名進行修飾。比如_stdcall調用規範的函數會被修飾爲以"_"開頭,以"@n"結尾,n表示函數調用時,參數所佔堆棧空間大小

我們常看到windows的API使用“WINAPI”方式聲明,其實“WINAPI”實際上是被定義爲_stdcall的宏。WINDOWS 的API函數都是__stdcall類型的。
但是WINDOWS 的API沒有出現“_Add@16”這種被修飾了的命名方式。這是因爲,它使用了.def文件將導出函數重命名。


調用約定:

函數的調用約定,顧名思義就是對函數調用的一個約束和規定(規範),描述了函數參數是怎麼傳遞和由誰清除堆棧的。它決定以下內容:(1)函數參數的壓棧順序,(2)由調用者還是被調用者把參數彈出棧,(3)以及產生函數修飾名的方法。

1.__cdecl 是C Declaration的縮寫(declaration,聲明),表示C語言默認的函數調用方法:所有參數從右到左依次入棧,由調用者負責把參數壓入棧,最後也是由調用者負責清除棧的內容,一般來說,這是 C/C++ 的默認調用函數的規則,MS VC 編譯器採用的規則則是這種規則。
2.__stdcall是StandardCall的縮寫,是C++的標準調用方式:所有參數從右到左依次入棧,由調用者負責把參數壓入棧,最後由被調用者負責清除棧的內容,Windows API 所採用的函數調用規則則是這種規則。

另外,採用 __cdecl 和 __stdcall 不同規則的函數所生成的修飾名也各爲不同,相同點則是生成的函數修飾名前綴都帶有下劃線,不同的則是後綴部分,當然,這兩者最大的不同點就在於恢復棧的方式不同,而且這點亦是最爲重要的。


Dll顯示運行時鏈接

windows提供了三個API

  • LoadLibrary():裝載DLL到進程的地址空間
  • GetProcAddress():查找符號的地址
  • FreeLibrary():卸載模塊

DLL的入口函數是什麼,什麼時候調用此函數?

http://blog.csdn.net/xiaxzhou/article/details/76589061

DllMain函數爲入口函數,(實際入口函數爲DllMainCRTStartUp)

被DLL用來執行一些與進程或線程有關的初始化和清理工作。

DllMain的第二個參數fdwReason指明瞭系統調用Dll的原因,它可能是:

  • DLL_PROCESS_ATTACH:

1、當創建新進程時,系統加載exe及所需DLL文件,然後,系統創建主線程,並用這個線程調用每個DLL的DLLMain函數,傳入DLL_PROCESS_ATTACH。所有DLL處理完後,系統執行C/C++啓動代碼,然後進入main函數。

2、顯示加載DLL過程,LoadLibrary的時候,系統加載DLL,並使用調用加載函數的線程執行DllMain函數,傳入DLL_PROCESS_ATTACH。

  • DLL_PROCESS_DETACH:

1、當將DLL從進程地址空間撤銷時,傳入DLL_PROCESS_DETACH。

2、當調用FreeLibrary時,如該線程的使用計數爲0時,操作系統纔會使用DLL_PROCESS_DETACH來調用DllMain。如使用計數大於0,則只是單純的減少該DLL的計數。

  • DLL_THREAD_ATTACH:

當進程創建一個新線程,則系統會檢查當前已映射到該進程空間中的所有DLL映像,並用DLL_THREAD_ATTACH來調用每個DLL的DllMain。這告訴DLL需要執行與線程相關的初始化。且由新創建的線程負責執行DLLMain函數。

DLL_THREAD_DETACH:

線程若要終止,會調用ExitThread,但是系統不會立即終止線程,而是會讓這個即將結束的線程用DLL_THREAD_DETACH來調用當前進程地址空間中的所有DLL鏡像的DllMain函數,告訴DLL執行與線程相關的清理工作。
當每個DLL的DllMain都處理完後,系統纔會真正的結束線程。


DLL的入口函數是必需的嗎?

DLL入口函數實際上是_DllMainCRTStartup,而不是DllMain。

_DllMainCRTStartup 會對DLL_PROCESS_ATTACH通知進行處理:初始化C/C++運行庫,並完成全局、靜態變量的構造。然後調用DllMain函數。

當收到DLL_PROCESS_DETACH消息時,_DllMainCRTStartup 會先調用DLLMain函數,DLLMain返回後,再調用DLL中所有全局變量,靜態變量的析構函數

_DllMainCRTStartup 對線程消息不會做任何處理。

DLLMain函數在源碼中不是必需的,連接DLL的時候,如果鏈接器無法在.obj文件中找到名爲DLLMain的函數,那麼它會鏈接C/C++運行庫提供的DLLMain函數。

如果不提供自己的DllMain,那麼C/C++運行庫會認爲我們不關心DLL_THREAD_ATTACH、DLL_THREAD_DETACH通知。


何爲DLL的基地址重定位,重定位會有哪些缺點?如何優化?

DLL裝載時,如果默認目標地址被佔用,那麼系統重新分配一塊空間將DLL裝載到這個地址。問題是DLL代碼段不是地址無關的如:MOV [0X00414540], 5,DLL所涉及的絕對地址引用怎麼辦?

答案是對每個絕對地址引用都進行重定位。

當鏈接器在構建模塊時,會將重定位段嵌入到生成的文件中,這個段包含一個字節偏移量的列表,每個字節偏移量表示一條機器指令所使用的一個內存地址。如果加載器能將模塊載入到默認基地址,則系統不會訪問模塊的重定位段。

如果無法加載到默認基地址,系統會打開模塊的重定位段,並遍歷其中的所有條目,對每個條目,加載器會先找到包含機器指令的那個存儲頁面,然後將模塊的默認首選基地址與實際地址差值,加到機器指令當前正在使用的內存地址上。

1、加載程序必須遍歷重定位段修改大量代碼
2、由於寫時賦值機制,會強制這些模塊的代碼頁面以系統的頁交換文件爲後備存儲器。這意味着,系統不能再拋棄模塊的代碼頁面,直接載入模塊在磁盤上的文件鏡像。

DLL優化:

1、修改默認加載基地址
2、模塊綁定:每次程序運行時,都要重新進行符號的查找、解析、和重定位。如果將導出函數的地址寫入到模塊的導入表中,省去每次啓動時的符號解析過程。

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