Dll装载的实现
Dll装载主要由LdrInitializeThunk函数实现,具体如下
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;//链表
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;//基址
PVOID EntryPoint;//入口点
ULONG SizeOfImage;//映像大小
UNICODE_STRING FullDllName;//全路径
UNICODE_STRING BaseDllName;//本身的DLL名
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;//时间戳 很重要 用以校对绑定输入的有效性
} LDR_MODULE, *PLDR_MODULE;//LDR_MODULE的结构
VOID STDCALL
__true_LdrInitializeThunk (ULONG Unknown1, ULONG Unknown2,//Thunk是转换的意思,是编译的专用术语,这里我们可以把它等价了“动态链接”
ULONG Unknown3, ULONG Unknown4)
{
. . . . . .
DPRINT("LdrInitializeThunk()/n");//打印信息
if (NtCurrentPeb()->Ldr == NULL || NtCurrentPeb()->Ldr->Initialized == FALSE)//检测Ldr模块是否存在和peb->ldr->Initialized是否被初始化决定是否要初始化模块
{
Peb = (PPEB)(PEB_BASE); //获取进程环境快其固定地址在0x7FFDF000 有两种方式获取进程的peb,一种是NtCurrentPeb宏,另一种就是PEB_BASE
DPRINT("Peb %x/n", Peb);
ImageBase = Peb->ImageBaseAddress; //EXE 映像在用户空间的起点
. . . .
/* Initialize NLS data */ //语言本地化有关
RtlInitNlsTables (Peb->AnsiCodePageData, Peb->OemCodePageData,
Peb->UnicodeCaseTableData, &NlsTable);
RtlResetRtlTranslations (&NlsTable);
NTHeaders = (PIMAGE_NT_HEADERS)(ImageBase + PEDosHeader->e_lfanew);
. . . . . .
/* create process heap */
/*
NTSYSAPI PVOID RtlCreateHeap(
ULONG Flags,
PVOID HeapBase, if NULL, allocates system memory for the heap from the process's virtual address space.
SIZE_T ReserveSize,
SIZE_T CommitSize,
PVOID Lock,
PRTL_HEAP_PARAMETERS Parameters);
*/
RtlInitializeHeapManager();//初始化进程堆
Peb->ProcessHeap = RtlCreateHeap(HEAP_GROWABLE, NULL, //创建一个堆、 及其第一个区块,其初始的大小来自映像头部的建议值,其中 SizeOfHeapReserve 是估计的最大值,SizeOfHeapCommit是初始值,这是在编译/连接时确定的。
NTHeaders->OptionalHeader.SizeOfHeapReserve, //保留堆大小
NTHeaders->OptionalHeader.SizeOfHeapCommit, //提交堆大小
NULL, NULL);
/* create loader information */
//PEB 中的 ProcessHeap 字段指向本进程用户空间可动态分配的内存区块“堆”
Peb->Ldr = (PPEB_LDR_DATA)RtlAllocateHeap (Peb->ProcessHeap,//分配一个PEB_LDR_DATA大小的堆,handle是Peb->ProcessHeap,这里用的是RTlCreateHeap的堆
0,
sizeof(PEB_LDR_DATA));
. . . . . .
/* PEB 中的另一个字段 Ldr是个 PEB_LDR_DATA 结构指针,所指向的数据结构用来为本进程维持三个“模块”
队列、即 InLoadOrderModuleList、InMemoryOrderModuleList、InInitializationOrderModuleList。
所谓“模块”就是 PE 格式的可执行映像,包括 EXE映像和 DLL 映像.
两个模块队列的不同之处在于排列的次序,一个是按装入的先后,一个是按装入的位置
每当为本进程装入一个模块、即.exe 映像或 DLL 映像时,就要为其分配/创建一个LDR_MODULE 数据结构,
并将其挂入 InLoadOrderModuleList。然后,完成对这个模块的动态连接以后,就把它挂入
InInitializationOrderModuleList 队列.LDR_MODULE 数据结构中有三个队列头,因而可以同时挂在三个队列
中。*/
Peb->Ldr->Length = sizeof(PEB_LDR_DATA);//设置为PEB_LDR_DATA大小
Peb->Ldr->Initialized = FALSE;//未初始化
Peb->Ldr->SsHandle = NULL;
InitializeListHead(&Peb->Ldr->InLoadOrderModuleList);//初始化三个链表
InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList);
InitializeListHead(&Peb->Ldr->InInitializationOrderModuleList);
. . . . . .
/* add entry for ntdll */
NtModule = (PLDR_MODULE)RtlAllocateHeap (Peb->ProcessHeap,//分配一个LDR_MODULE大小的堆用以加载模块,实际上是一个_LDR_DATA_TABLE_ENTRY
0, //这里将ntdll这个模块串接到队列中
sizeof(LDR_MODULE));
. . . . . .
InsertTailList(&Peb->Ldr->InLoadOrderModuleList,//将新结点插入到顺序加载链表表尾
&NtModule->InLoadOrderModuleList);
InsertTailList(&Peb->Ldr->InInitializationOrderModuleList,//插入新结点到初始化模块链表表尾,因为ntdll无依赖dll,所以直接串接上
&NtModule->InInitializationOrderModuleList);
. . . . . .
/* add entry for executable (becomes first list entry) */
ExeModule = (PLDR_MODULE)RtlAllocateHeap (Peb->ProcessHeap,//将exe模块串接到链表里
0,
sizeof(LDR_MODULE));
. . . . . .
InsertHeadList(&Peb->Ldr->InLoadOrderModuleList,//插入到表头,所以实际上我们从LDR遍历的时候看到的第一个模块是该EXE模块
&ExeModule->InLoadOrderModuleList);
. . . . . .
/*当 CPU从 LdrPEStartup()返回时,EXE 对象需要直接或间接引入的所有 DLL 均已映射到用户空间并已完成连
接,对 EXE 模块的“启动” 、即初始化也已完成。
注意在调用 LdrPEStartup()时的参数 ImageBase 是目标 EXE 映像在用户空间的位置*/
EntryPoint = LdrPEStartup((PVOID)ImageBase, NULL, NULL, NULL);
. . . . . .
}
/* attach the thread */
RtlEnterCriticalSection(NtCurrentPeb()->LoaderLock); //进行加锁处理 对临界区进行处理
//目的是调用各个 DLL 的初始化过程,以及对 TLS、即“线程本地存储(Thread Local Storage)”的初始化
//TLS:有时候又确实需要让每个线程都对于同一个全局变量有一份自己的拷贝,TLS就是为此而设的
LdrpAttachThread();
RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);//释放锁
}
再复习下PEB结构的知识
为什么要进行动态链接呢,因为现在虽然ntdll和我们的EXE模块被加载到用户空间,但是并没有建立联系,我们的程序并不能直接调用ntdll里的函数,所以这就需要LdrInitializeThunk函数起到一个类似转换的作用,通过将各个模块串接起来实现关系的建立。大概流程是如上所述,首先ntdll和exe程序是在程序里,系统会进行RtlCreateHeap操作,Create一个映像头的默认值,然后就根据返回的Handle操作这块堆,首先将ntdll加载,然后挂钩链表,接着将exe进行挂钩,但是挂接EXE模块的时候,是插入到表头的,所以实际上遍历LDR的时候第一个可见模块是EXE模块的相关信息。LdrInitializeThunk只是简单的进行了初始化,创建了进程堆,将EXE模块和ntdll模块这两个模块挂接到InLoadOrderModule链表上,其余的模块是由LdrPEStartup函数挂接的,同时LdrPEStartup函数计算出了入口点,作为返回值返回。
/*
LdrPEStartup里所做的事情
1. Relocate, if needed the EXE.
2. Fixup any imported symbol.
3. Compute the EXE's entry point.
*/
//注意在调用 LdrPEStartup()时的参数 ImageBase 是目标 EXE 映像在用户空间的位置,Module是加载的模块链
PEPFUNC LdrPEStartup (PVOID ImageBase, HANDLE SectionHandle,
PLDR_MODULE* Module, PWSTR FullDosName)
{
//PE 映像的 NtHeader 中有个指针,指向一个 OptionalHeader。说是“Optional”,实际上却是关键性的。在
//OptionalHeader中有个字段 ImageBase,是具体映像建议、或者说希望被装入的地址,即愿望地址
. . . . . .
DosHeader = (PIMAGE_DOS_HEADER) ImageBase;
NTHeaders = (PIMAGE_NT_HEADERS) (ImageBase + DosHeader->e_lfanew);
/*
* If the base address is different from the
* one the DLL is actually loaded, perform any
* relocation.
*/
if (ImageBase != (PVOID) NTHeaders->OptionalHeader.ImageBase)//判断基址是否一致,若EXE基址与期望基址不一致则进行重定位处理,其具体处理是在LdrPerformRelocations中实现的
{
DPRINT("LDR: Performing relocations/n");
//ImageBase 是目标 EXE 映像在用户空间的位置
Status = LdrPerformRelocations(NTHeaders, ImageBase);//若基址不是默认基址 则进行重定位处理
. . . . . .
}
if (Module != NULL)//也即假如是dll程序,但是事实上不可能满足(事实上这个条件永不满足)
{
*Module = LdrAddModuleEntry(ImageBase, NTHeaders, FullDosName);//LdrAddModuleEntry是创建一个LDR_DATA_TABLE_ENTRY,并返回这个模块信息的指针。
(*Module)->SectionHandle = SectionHandle;
}
else//所以这才是正题 之前LdrInitializeThunk调用LdrPEStartup的时候,传进的MODULE正是null
{
Module = &tmpModule;
Status = LdrFindEntryForAddress(ImageBase, Module);//这里函数返回后Module的值是EXE模块的指针,即LDR_DATA_TABLE_ENTRY结构的指针
. . . . . .
}
. . . . . .
/*
* If the DLL's imports symbols from other
* modules, fixup the imported calls entry points.
*/
//它所处理的就是当前模块所需DLL模块的装入(如果尚未装入的话)和连接。如前所述,这个函数递归地施行于所有的模块,直至最底层的“叶节点”ntdll.dll为止,类似多对一的一棵树。
DPRINT("About to fixup imports/n");
Status = LdrFixupImports(NULL, *Module);//将串接模块送入LdrFixupImports来实现输入表的修复,这里传入的是 PLDR_MODULE 即EXE模块的链表位置
if (!NT_SUCCESS(Status))//若失败,打印修复失败时的DLL名称
{
DPRINT1("LdrFixupImports() failed for %wZ/n", &(*Module)->BaseDllName);
return NULL;
}
DPRINT("Fixup done/n");
. . . . . .
Status = LdrpInitializeTlsForProccess();//初始化Tls
. . . . . .
/*
* Compute the DLL's entry point's address.
*/
. . . . . .
if (NTHeaders->OptionalHeader.AddressOfEntryPoint != 0)//计算EntryPoint并返回
{
EntryPoint = (PEPFUNC) (ImageBase + NTHeaders->OptionalHeader.AddressOfEntryPoint);
}
DPRINT("LdrPEStartup() = %x/n",EntryPoint);
return EntryPoint;
}
/*
获取PLDR_MODULE的EXE模块的指针
*/
NTSTATUS NTAPI
LdrFindEntryForAddress(PVOID Address,
PLDR_DATA_TABLE_ENTRY *Module)
{
PLIST_ENTRY ModuleListHead;
PLIST_ENTRY Entry;
PLDR_DATA_TABLE_ENTRY ModulePtr;
DPRINT("LdrFindEntryForAddress(Address %p)n", Address);
if (NtCurrentPeb()->Ldr == NULL)
return(STATUS_NO_MORE_ENTRIES);
RtlEnterCriticalSection(NtCurrentPeb()->LoaderLock);//进行遍历操作前先加锁 防止其他线程破坏
ModuleListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;//按加载顺序进行遍历 现在获得的是PLIST_ENTRY
Entry = ModuleListHead->Flink;//指向第一个
if (Entry == ModuleListHead)//如果此时指向本身,那么表明没有结点
{
RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
return(STATUS_NO_MORE_ENTRIES);
}
while (Entry != ModuleListHead)//只要不指向表头,就一直遍历,Entry为遍历指针
{
ModulePtr = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);//获得该模块的头部
DPRINT("Scanning %wZ at %pn", &ModulePtr->BaseDllName, ModulePtr->DllBase);
if ((Address >= ModulePtr->DllBase) &&//如果传入的的Address在 DllBase~DllBase+SizeOfImage之间,表明找到了
((ULONG_PTR)Address <= ((ULONG_PTR)ModulePtr->DllBase + ModulePtr->SizeOfImage)))
{
*Module = ModulePtr;
RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
return(STATUS_SUCCESS);
}
Entry &