8 动态链接库的载入分析.docx
《8 动态链接库的载入分析.docx》由会员分享,可在线阅读,更多相关《8 动态链接库的载入分析.docx(18页珍藏版)》请在冰豆网上搜索。
8动态链接库的载入分析
第八章動態連結程式庫的載入分析
動態連結程式庫(DLL)一直以來都是Windows的重要基礎,WindowsCE也不例外。
DLL對作業系統十分重要,本節的內容主要是分析loader.c中的程式碼,它負責載入EXE和DLL。
這裏要討論的是關於DLL的部分,如無特別說明,本章中所引用的程式碼,都是來自WindowsCE原始程式碼樹中的[CEROOT]\PRIVATE\WINCEOS\COREOS\NK\KERNEL\loader.c。
8.1loader.c中程式碼的組織結構
loader.c主要透過以下的API函數,來完成NK核心載入EXE和DLL處理程序的工作,這是使用者程式對DLL和EXE程式操作的進入點。
之後再透過一系列的函數呼叫,來完成DLL和EXE的載入和卸載,以及對處理程序和執行緒的其他操作。
由於我們現在要討論的是有關DLL的部分,所以只列出了部分函式。
●Win32LoadLibrarycall
HANDLESC_LoadLibraryW(LPCWSTRlpszFileName)
HINSTANCESC_LoadLibraryExW(LPCWSTRlpLibFileName,HANDLEhFile,DWORDdwFlags)
HANDLESC_LoadDriver(LPCWSTRlpszFileName)
HANDLESC_LoadKernelLibrary(LPCWSTRlpszFileName)
HANDLESC_LoadIntChainHandler(LPCWSTRlpszFileName,LPCWSTRlpszFunctionName,
BYTEbIRQ)
這幾個函數基本上都是負責DLL的載入,它們都呼叫了LoadOneLibraryW這個函數,函數的原型如下:
HANDLELoadOneLibraryW(LPCWSTRlpszFileName,DWORDfLbFlags,WORDwFlags)
這個函數在載入DLL的前後,處理一些必要的細節工作。
更重要的是呼叫了LoadOneLibraryPart2這個函數,載入的重要工作都在該函數的程式碼中完成,在本章的後面會詳細分析它的內容。
●Win32FreeLibrarycall
HANDLESC_FreeIntChainHandler(HANDLEhLib)
BOOLSC_FreeLibrary(HANDLEhInst)
主要是FreeLibrary函數。
這個API函數負責卸載DLL。
它呼叫FreeOneLibrary這個函數,函數原型如下:
BOOLFreeOneLibrary(PMODULEpMod,BOOLbCallEntry)
卸載的過程主要是由FreeOneLibraryPart2負責。
在後面的章節會有具體的分析。
●Win32GetProcAddresscall
LPVOIDSC_GetProcAddressA(HANDLEhInst,LPCSTRlpszProc)
LPVOIDSC_GetProcAddressW(HANDLEhInst,LPCSTRlpszProc)
這兩個函數可以得到處理程序的位址。
8.2modulestructure
這是記錄DLL資訊的重要資料結構之一,每一個程式對應一個module,但一個module可以對應多個處理程序。
在系統中維持一條已載入模組的串列,它是一個單向鏈結串列。
第一個元素是pModList。
程式碼8.1modulestructure
typedefstructModule{
LPVOIDlpSelf;
PMODULEpMod;
LPWSTRlpszModName;
DWORDinuse;
DWORDcalledfunc;
WORDrefcnt[MAX_PROCESSES];
LPVOIDBasePtr;
DWORDDbgFlags;
LPDBGPARAMZonePtr;
ulongstartip;
openexe_toe;
e32_litee32;
o32_lite*o32_ptr;
DWORDbreadcrumb;
DWORDdwNoNotify;
WORDwFlags;
BYTEbTrustLevel;
BYTEbPadding;
PMODULEpmodResource;
DWORDrwLow;
DWORDrwHigh;
}Module;
/*用於驗證,指向自己的指標*/
/*鏈結串列中的下一個模組*/
/*模組名字*/
/*使用狀況的位元向量*/
/*被呼叫的進入點,但不退出*/
/*處理程序的引用計數*/
/*DLL載入基址*/
/*偵錯旗標*/
/*Debugzone指標*/
/*基於0的進入點*/
/*執行檔指標*/
/*e32標頭*/
/*O32串列指標*/
/*每個處理程序對應一個位元,當Notify被禁用時設為1*/
/*包含資源的模組*/
/*ROMDLL中可讀寫段的基底位址*/
/*ROMDLL中可讀寫段的高位位址*/
refcnt[MAX_PROCESSES]是每個處理程序的引用計數。
BasePtr是DLL載入的基底位址,這是比較需要注意的部分。
oe、e32、*o32_ptr放在後面介紹。
8.3LoadOneLibraryPart2載入DLL的過程
呼叫
LoadOneLibraryPart2
圖8.1LoadOneLibraryPart2的基本執行步驟
函數原型:
PMODULELoadOneLibraryPart2(LPWSTRlpszFileName,DWORDfLbFlags,WORDwFlags)
參數lpszFileName
指向DLL名字的字串,該名字確定了模組(module)的檔案名稱,而與它儲存在模組庫中的名字無關。
函式庫模組以LIBRARY為關鍵字,在模組定義檔(.def)中定義。
參數hFile
是保留做將來使用的,現在必須是NULL。
參數wFlags
指定載入模組時所要處理的工作。
其值可以為0、DONT_RESOLVE_DLL_REFERENCES、LOAD_LIBRARY_AS_DATAFILE或者是LOAD_LIBRARY_IN_KERNEL。
DONT_RESOLVE_DLL_REFERENCE:
如果使用這個旗標,而且可執行模組本身是一個DLL,則系統並不呼叫DllMain來初始化和結束處理程序和執行緒。
此外,一個DLL可能會引入包含在另一個DLL中的函數,而系統映射一個DLL時也會自動載入,當這個旗標被設置之後,系統就不再自動載入額外的DLL。
載入過程如圖8.1所示。
載入的過程首先設定wFlags的值,然後呼叫pMod=FindModByName(dllname),藉由查找pModList串列,來得到DLL的pMod指標。
對於已經載入的module,增加其引用次數。
由以下的程式碼來完成:
if(!
(pMod->inuse&(1<procnum))){
pMod->inuse|=(1<procnum);
pMod->calledfunc&=~(1<procnum);
}
returnpMod->refcnt[pCurProc->procnum]++;
呼叫該module的處理程序是目前的處理程序。
將目前處理程序對該module的引用計數加一,傳回pMod,便完成了載入的過程。
如果module之前並未載入,則需要一系列的工作:
1)為module配置記憶體
2)初始化module,呼叫InitModule
3)增加引用計數
4)將這個新的pMod插入module鏈結串列中,作為第一個元素。
程式碼如下:
EnterCriticalSection(&ModListcs);
pMod->pMod=pModList;
pModList=pMod;
LeaveCriticalSection(&ModListcs)
8.4DLL載入過程—InitModule的執行
InitModule處理載入一個新的DLL所要做的大部分工作。
函數原型如下:
DWORDInitModule(PMODULEpMod,LPWSTRlpszFileName,LPWSTRlpszDllName,
DWORDfLbFlags,WORDwFlags)
在函數InitModule中,具體執行步驟如下:
1)初始化pMod中的欄位(field)
2)呼叫函數OpenADll,產生執行檔指標(openexe_t)
3)載入module的e32資訊,產生e32標頭資訊(e32_lite)
4)檢查bTrustlevel參數
5)配置記憶體給DLL,取得Module->BasePtr,即載入DLL的基底位址
6)配置記憶體給name和O32物件,讀取這個module的O32資訊
7)改變module的名字
8)重定位映射
9)呼叫函數FindEntryPoint,找到EXE的起始IP(或者DLL的進入點)
整體來說,這個函數就是負責設定pMod各個欄位的初始值、取得可執行檔的指標以及EXE或DLL的進入點。
在這個過程中,將各個步驟、判斷中的錯誤碼,傳回給LoadOneLibraryPart2,用來作為判斷pMod是否建立成功的資訊,以繼續以後的工作。
下面,將對這幾個重要步驟作詳細分析。
8.4.1呼叫OpenADll,產生執行檔指標(openexe_t)
module中的oe是可執行檔的指標,每個程式對應一個module,每個module對應一個可執行檔指標。
oe的型別openexe_t定義如下:
程式碼8.2openexe_tstructure
typedefstructopenexe_t{
union{
inthppfs;
HANDLEhf;
TOCentry*tocptr;
};
BYTEfiletype;
BYTEbIsOID;
WORDpagemode;
DWORDoffset;
union{
Name*lpName;
CEOIDceOid;
};
}openexe_t;
//ppfshandle
//物件儲存指標
//romentrypointer
//檔案類型
//分頁模式
//偏移
這實際上是與檔案處理等有關的結構,描述可執行程式碼的位址、分頁模式、偏移等資訊。
OpenADll呼叫OpenExe,由OpenExe呼叫SafeOpenExe,設定可執行檔指標的工作基本上都在SafeOpenExe中完成。
BOOLSafeOpenExe(LPWSTRlpszName,openexe_t*oeptr,BOOLbIsDLL,BOOLbAllowPaging,OEinfo_t*poeinfo)
SafeOpenExe還執行以下的工作:
1)尋找EXE檔案所在的目錄
2)按照指定路徑尋找檔案
3)在Windows目錄中尋找檔案
4)在根目錄中尋找檔案
在檔案系統中搜尋DLL檔案時,根據搜尋過程中的資訊設定其中各個欄位的值。
並藉由下面的程式碼取得檔案的儲存指標:
oeptr->hf=CreateFileW((LPWSTR)poeinfo->tmpname,GENERIC_READ,FILE_SHARE_READ,0,
OPEN_EXISTING,0,0);
即透過CreateFile開啟檔案。
如果在ROM中有備份,則設定oeptr->tocptr的值。
傳回結果:
如果找到,則傳回1,否則傳回0。
8.4.2設定module的e32標頭資訊
module的成員e32的型別定義如下:
程式碼8.3e32_litestructure
typedefstructe32_lite{
unsignedshorte32_objcnt;
BYTEe32_cevermajor;
BYTEe32_ceverminor;
unsignedlonge32_stackmax;
unsignedlonge32_vbase;
unsignedlonge32_vsize;
unsignedlonge32_sect14rva;
unsignedlonge32_sect14size;
struct infoe32_unit[LITE_EXTRA];
}e32_lite,*LPe32_list;
/*PE32-bit.EXEheader*/
/*記憶體物件個數*/
/*版本資訊*/
/*版本資訊*/
/*堆疊的最大值*/
/*module的虛擬記憶體基底位址*/
/*整個映射的Virtual大小*/
/*section14rva*/
/*section14size*/
/*Arrayofextrainfounits*/
這是與記憶體相關的一組資料,在InitModule中有一段程式碼如下:
//loadO32infoforthemodule
eptr=&pMod->e32;
if(retval=LoadE32(&pMod->oe,eptr,&flags,&entry,(pMod->wFlags&
LOAD_LIBRARY_AS_DATAFILE)?
1:
0,bAllowPaging,&pMod->bTrustLevel)){
returnretval;
}
它藉由呼叫LoadE32函數讀取DLL檔,設定e32標頭的各部分內容,設定堆疊、虛擬記憶體基底位址、映射等正確的值,使接下來DLL的記憶體配置工作能夠順利進行。
8.4.3DLL的記憶體配置與Module->BasePtr的取得
InitModule中的這一部分程式碼的主要作用是設定pMod->BasePtr。
為了方便解釋,圖8.2是簡化的流程圖。
是
圖8.2DLL配置記憶體的過程
由可執行檔指標pMod->oe.filetype可得到檔案類型。
DLL有兩種映射,一種是普通檔案,需要將其載入到RAM中執行。
為了節省時間,還有另一種方式就是XIP(Executeinplace)方式。
顧名思義,就是可以立即執行,而不用載入RAM中,所以相應pMod->BasePtr的值也有兩種不同的情況。
如果不是XIP(Executeinplace)DLL,則保留足夠的虛擬記憶體位址空間以容納整個映射。
從已經存在的DLL的底部開始由上往下分配。
DLL載入到核心位址空間(kernelspace)或是使用者空間(userspace)也有差別。
對於載入核心位址空間的DLL,必須為其取得連續的實體頁面。
因為核心空間位址和虛擬記憶體位址之間是靜態映射的,這是為了讓核心執行時,不必進行位址的轉換,藉以加快核心的執行。
例如在PlatformBuilder下產生的platform,啟動時載入的features等就是載入核心的DLL。
if(wFlags&LOAD_LIBRARY_IN_KERNEL){
PHYSICAL_ADDRESSpaRet;
//Loadinginthekerneladdressspace
paRet=GetContiguousPages((DWORD)(eptr->e32_vsize+PAGE_SIZE-1)/PAGE_SIZE,
0,0);
if(paRet==INVALID_PHYSICAL_ADDRESS||!
(pMod->BasePtr=
(PVOID)Phys2Virt(paRet))){
returnERROR_OUTOFMEMORY;
}
}
它先向系統要求連續的實體位址頁面,取得實體位址,再將其直接映射到虛擬位址空間,並把虛擬記憶體位址傳給pMod->BasePtr。
有關GetContiguousPages及Phys2Virt的細節請參考其他的原始檔案。
如果不是載入核心位址空間,則必須為其保留位址空間,以避免其他DLL載入同樣的位址空間,而使該DLL的卸載發生問題。
程式碼如下:
}else{
//trytohonortheDll'srelocationbasetopreventrelocation
if((pTOC->ulKernelFlags&KFLAG_HONOR_DLL_BASE)&&(eptr->e32_vbase+
eptr->e32_vsizepMod->BasePtr=VirtualAlloc((LPVOID)(ProcArray[0].dwVMBase+
eptr->e32_vbase),eptr->e32_vsize,MEM_RESERVE|MEM_IMAGE,
PAGE_NOACCESS);
DEBUGMSG(pMod->BasePtr,(L"LoadingDLL'%s'atthepreferredloadingaddress
%8.8lx\n",lpszFileName,ZeroPtr(pMod->BasePtr)));
}
其中,eptr->e32_vbase是module映射的虛擬記憶體基底位址,eptr->e32_vsize是module的虛擬記憶體大小。
利用VirtualAlloc在目前處理程序的虛擬記憶體位址空間保留一個區域,基底位址是處理程序位址空間基底位址ProcArray[0].dwVMBase+module映射的虛擬記憶體基底位址。
大小受模組影響。
它使用參數MEM_RESERVE|MEM_IMAGE,只保留了處理程序的一部分虛擬記憶體空間,而沒有實際分配實體記憶體。
而且這部分空間不能透過其他記憶體配置的操作如malloc和LocalAlloc使用,也不能被其他DLL佔用。
如果配置基底位址時,發現要使用的區域已經被保留,即已經有其他DLL佔用,造成配置虛擬記憶體失敗,則由上往下配置。
//allocatetop-downifwecan'tloaditinthedll'spreferredloadbase.
if(!
pMod->BasePtr&&!
(pMod->BasePtr=VirtualAlloc((LPVOID)ProcArray[0].
dwVMBase,eptr->e32_vsize,MEM_RESERVE|MEM_TOP_DOWN|MEM_IMAGE,
PAGE_NOACCESS))){
returnERROR_OUTOFMEMORY;
}
}
配置的基底位址是目前處理程序的虛擬記憶體空間基底位址,大小不變,只是由上往下配置。
這樣就得到了pMod->BasePtr。
下面的程式碼修改DLL的載入基址,這是個全域變數。
if(ZeroPtr(pMod->BasePtr)<(DWORD)DllLoadBase)
DllLoadBase=ZeroPtr(pMod->BasePtr);
如果要載入的DLL是XIP的,即表示它是在ROM中,不需載入到處理程序位址空間,如coredll.dll就是XIP型的DLL。
只需在ROM中找到要執行的DLL的基底位址,傳給pMod->BasePtr即可,如以下程式碼所示:
}else{
e32_rom*e32rp=(e32_rom*)pMod->oe.tocptr->ulE32Offset;
pMod->BasePtr=(LPVOID)MapPtrProc(e32rp->e32_vbase,ProcArray);
if((wFlags&LOAD_LIBRARY_IN_KERNEL)&&!
IsKernelVa(pMod->BasePtr)){
returnERROR_BAD_EXE_FORMAT;
}
}
8.4.4name和o32物件的記憶體配置
o32物件是與存取控制有關的物件。
結構定義如下。
typedefstructo32_lite{
unsignedlongo32_vsize;
unsignedlongo32_rva;
unsignedlongo32_realaddr;
unsignedlongo32_access;
unsignedlongo32_flags;
unsignedlongo32_psize;
unsignedlongo32_dataptr;
}o32_lite,*LPo32_lite;
8.4.5重定位映射
非XIP映射需要重新定址(Relocate)。
if(pMod->oe.filetype!
=FT_ROMIMAGE){
//
//Non-XIPimageneedstoberelocated.
//
if((pMod->oe.pagemode==PM_NOPAGING)&&
!
(pMod->wFlags&LOAD_LIBRARY_AS_DATAFILE)&&
!
Relocate(eptr,pMod->o32_ptr,(ulong)pMod->BasePtr,
((wFlags&LOAD_LIBRARY_IN_KERNEL)?
0:
ProcArray[0].dwVMBase))){
returnERROR_OUTOFMEMORY;
}
這裏呼叫Relocate對DLL重新定址,傳遞的參數為:
可執行檔指標eptr,o32標頭資訊pMod->o32_ptr,DLL載入的基底位址pMod->BasePtr,如果DLL載入到核心,則最後一個參數為0,否則傳遞的參數是目前處理程序虛擬記憶體空間的基址。
這裏還需要解釋一下重新定址(Relocate)的過程。
重新定址的過程,是將DLL映射位址定位到目前處理程序的位址空間,即位址0x00000000-0x02000000,從0x01FFFFFF開始由上往下,最頂端是coredll.dll,然後是其他DLL。
如果是XIP映射,即DLL在ROM中。
如果module被載入記憶體的Slot1(DLL高位址區域),或者載入到核心中,則需要記錄為這個module而定址的讀寫區。
程式碼如下:
o32_lite*optr=