📄 漫谈兼容内核之十一:windows dll的装入和连接.txt
字号:
. . . . . .
*BaseAddress = NULL;
Status = LdrpLoadModule(SearchPath, LoadFlags, Name, &Module, BaseAddress);
if (NT_SUCCESS(Status) && 0 == (LoadFlags & LOAD_LIBRARY_AS_DATAFILE))
{
RtlEnterCriticalSection(NtCurrentPeb()->LoaderLock);
Status = LdrpAttachProcess();
RtlLeaveCriticalSection(NtCurrentPeb()->LoaderLock);
if (NT_SUCCESS(Status))
{
*BaseAddress = Module->BaseAddress;
}
}
return Status;
}[/code]
显然,这个函数是LdrpLoadModule()和LdrpAttachProcess()的组合。首先通过LdrpLoadModule()装入目标DLL和以此为根的子树,然后通过LdrpAttachProcess()调用这些DLL的初始化函数。这里的重点是LdrpLoadModule()。
[code][__true_LdrInitializeThunk > LdrPEStartup() > LdrFixupImports()
> LdrpProcessImportDirectory() > LdrpProcessImportDirectoryEntry()
> LdrGetExportByOrdinal() > LdrFixupForward() > LdrLoadDll() > LdrpLoadModule()]
static NTSTATUS
LdrpLoadModule(IN PWSTR SearchPath OPTIONAL, IN ULONG LoadFlags,
IN PUNICODE_STRING Name, PLDR_MODULE *Module,
PVOID *BaseAddress OPTIONAL)
{
. . . . . .
if (Module == NULL)
{
Module = &tmpModule;
}
/* adjust the full dll name */
LdrAdjustDllName(&AdjustedName, Name, FALSE);
MappedAsDataFile = FALSE;
/* Test if dll is already loaded */
Status = LdrFindEntryForName(&AdjustedName, Module, TRUE);
if (NT_SUCCESS(Status))
{
RtlFreeUnicodeString(&AdjustedName);
if (NULL != BaseAddress)
{
*BaseAddress = (*Module)->BaseAddress;
}
}
else
{
/* Open or create dll image section */
Status = LdrpMapKnownDll(&AdjustedName, &FullDosName, &SectionHandle);
if (!NT_SUCCESS(Status))
{
MappedAsDataFile = (0 != (LoadFlags & LOAD_LIBRARY_AS_DATAFILE));
Status = LdrpMapDllImageFile(SearchPath, &AdjustedName, &FullDosName,
MappedAsDataFile, &SectionHandle);
}
. . . . . .
RtlFreeUnicodeString(&AdjustedName);
/* Map the dll into the process */
ViewSize = 0;
ImageBase = 0;
Status = NtMapViewOfSection(SectionHandle, NtCurrentProcess(),&ImageBase,
0, 0, NULL, &ViewSize, 0, MEM_COMMIT,
PAGE_READWRITE);
. . . . . .
if (NULL != BaseAddress)
{
*BaseAddress = ImageBase;
}
/* Get and check the NT headers */
NtHeaders = RtlImageNtHeader(ImageBase);
. . . . . .
if (MappedAsDataFile)
{
assert(NULL != BaseAddress);
if (NULL != BaseAddress)
{
*BaseAddress = (PVOID) ((char *) *BaseAddress + 1);
}
*Module = NULL;
RtlFreeUnicodeString(&FullDosName);
NtClose(SectionHandle);
return STATUS_SUCCESS;
}
/* If the base address is different from the one the DLL is actually loaded, perform any
relocation. */
if (ImageBase != (PVOID) NtHeaders->OptionalHeader.ImageBase)
{
Status = LdrPerformRelocations(NtHeaders, ImageBase);
. . . . . .
}
*Module = LdrAddModuleEntry(ImageBase, NtHeaders, FullDosName.Buffer);
(*Module)->SectionHandle = SectionHandle;
if (ImageBase != (PVOID) NtHeaders->OptionalHeader.ImageBase)
{
(*Module)->Flags |= IMAGE_NOT_AT_BASE;
}
if (NtHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL)
{
(*Module)->Flags |= IMAGE_DLL;
}
/* fixup the imported calls entry points */
Status = LdrFixupImports(SearchPath, *Module);
. . . . . .
RtlEnterCriticalSection(NtCurrentPeb()->LoaderLock);
InsertTailList(&NtCurrentPeb()->Ldr->InInitializationOrderModuleList,
&(*Module)->InInitializationOrderModuleList);
RtlLeaveCriticalSection (NtCurrentPeb()->LoaderLock);
}
return STATUS_SUCCESS;
}[/code]
调用参数Name可以是文件名,也可能是模块名(例如ntdll),所以先通过LdrAdjustDllName()加以必要的调整,如果是模块名就把它变成文件名。然后就通过LdrFindEntryForName()在已装入模块队列中寻找,如果找到就万事大吉,只要通过调用参数BaseAddress返回该模块映像装入后的起点地址就行了。否则就得要劳累一点了:
先通过LdrpMapKnownDll()在一个目录“\KnownDlls”下面寻找目标DLL。这个目录完全是为避免四处查找、加快装入速度而设的符号连接。如果找到就为目标DLL映像建立一个共享内存区、即Section。
如果不成功,就调用LdrpMapDllImageFile()找到目标文件,将其打开并验证其确系PE格式映像文件,然后为其建立一个Section。
至此,目标模块的映像文件已经打开并建立了一个Section,只要将它映射到本进程的用户空间就可以了。于是通过系统调用NtMapViewOfSection()将目标映像影射到用户空间。
在调用LdrpLoadModule()时,可以通过参数LoadFlags中的标志位LOAD_LIBRARY_AS_DATAFILE说明目标模块是作为数据文件映射的,因而就不存在库函数的动态连接问题。如果是那样的话,那么到这里就完成了。否则就还得再接再厉。
如果映像的实际装入地址不同于其“愿望地址”、即OptionalHeader.ImageBase,就通过LdrPerformRelocations()进行“重定位”,即对映像中的绝对地址加以调整。我们在前面已经看过有关的代码。
通过LdrAddModuleEntry()创建目标映像的LDR_MODULE数据结构,并把它挂入本进程的模块队列。
所装入的模块(DLL)本身又可能有引入要求,所以通过LdrFixupImports()处理其引入。在我们这个情景中,这是对LdrFixupImports()的递归调用(我们现在所处的位置正是从LdrFixupImports()逐层调用下来的),这样的递归调用一直要到被引入的模块本身不再要求引入(例如ntdll.dll)时为止。
? 通过InsertTailList()把所装入映像的LDR_MODULE数据结构挂入初始化队列。
回到前面LdrFixupForward()的代码中。既然被转引模块已经装入,接着就可以从中获取目标函数的入口了。
不过目标函数在被转引模块中有可能又是一个转引函数。那也不要紧,再调用LdrFixupForward()就是了。此时对LdrFixupForward()的调用是递归调用,我们就不需要再往下看了。这样,逐层转引就体现为对LdrFixupForward()的递归调用,一直到目标函数的真正的实现/引出者为止。
回到LdrFixupImports()的代码。理解了按“绑定引入”目录引入的代码,再看因为“绑定引入”目录不存在而只好按普通“引入”目录处理引入时的操作,就很简单了,这里只是直接调用LdrpProcessImportDirectoryEntry()。
前面我们看的是按序号引入,这里再提一下按函数名引入。从原理上说按函数名引入毫无新奇之处,比之按序号引入只是多了字符串比对。然而,要是对于引入的每一个函数都要在被引入模块的引出函数表中顺序扫描比对,那个开销也真是太大了。所以,为了提高效率,可以让要求引入的模块提供一个提示、即“Hint”。意思是“您先找一下这儿,看对不对,要是不对您再挨个儿找”。所以,“引入目录”中的函数名数组其实并不真是字符串数组,而是一个IMAGE_IMPORT_BY_NAME结构数组,这种数据结构的定义是:
[code]typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;[/code]
结构中的第一个成分就是Hint,其数值就是目标函数在被引入模块的引出函数表中最可能的下标,这是在引入者模块在编译/连接时设置好的。然后是一个变长的字符数组,这就是函数名字符串。调用LdrGetExportByName()时,可以把Hint也作为参数传下去,例如前面LdrpProcessImportDirectoryEntry()中的那段代码就是这样:
[code] {
IMAGE_IMPORT_BY_NAME *pe_name;
pe_name = RVA(Module->BaseAddress, *FunctionNameList);
*ImportAddressList = LdrGetExportByName(ImportedModule->BaseAddress,
pe_name->Name, pe_name->Hint);
. . . . . .
}[/code]
而LdrGetExportByName(),则首先以Hint作为下标在被引入模块的引出函数表中进行有针对的比对,比对不符才想别的办法,先是对分搜索,最后一手才是顺序搜索。一般使用Hint进行比对的命中率很高,所以效率也就大大提高了。
最后,回到前面__true_LdrInitializeThunk()的代码中,在完成了对所有模块的装入和连接以后,还调用了一个函数LdrpAttachThread(),这一方面是为了TLS的初始化,更重要的是要以DLL_THREAD_ATTACH为参数调用这些DLL的入口函数DllMain()。
[code]NTSTATUS
LdrpAttachThread (VOID)
{
. . . . . .
Status = LdrpInitializeTlsForThread();
if (NT_SUCCESS(Status))
{
ModuleListHead = &NtCurrentPeb()->Ldr->InInitializationOrderModuleList;
Entry = ModuleListHead->Flink;
while (Entry != ModuleListHead)
{
Module = CONTAINING_RECORD(Entry, LDR_MODULE,
InInitializationOrderModuleList);
if (Module->Flags & PROCESS_ATTACH_CALLED &&
!(Module->Flags & DONT_CALL_FOR_THREAD) &&
!(Module->Flags & UNLOAD_IN_PROGRESS))
{
TRACE_LDR("%wZ - Calling entry point at %x for thread attaching\n",
&Module->BaseDllName, Module->EntryPoint);
LdrpCallDllEntry(Module, DLL_THREAD_ATTACH, NULL);
}
Entry = Entry->Flink;
}
Entry = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Flink;
Module = CONTAINING_RECORD(Entry, LDR_MODULE, InLoadOrderModuleList);
LdrpTlsCallback(Module, DLL_THREAD_ATTACH);
}
. . . . . .
return Status;
}[/code]
这里的宏操作CONTAINING_RECORD定义为:
[code]#define CONTAINING_RECORD(address, type, field) \
((type*)((PCHAR)(address) - (PCHAR)(&((type *)0)->field)))[/code]
因为LDR_MODULE数据结构是通过其不同的队列头挂入不同队列的,所以从队列中获取的只是指向相应队列头的指针,要通过这样换算才能得到指向其LDR_MODULE数据结构的指针。
还有个问题,当CPU从__true_LdrInitializeThunk()返回、也就是从LdrInitializeThunk()返回时去了哪里。事实上,LdrInitializeThunk()是作为APC函数执行的,目的是为EXE映像的运行“打前站”,所以返回时又(间接地)回到了内核中,这正是下一片漫谈要讨论的话题。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -