📄 漫谈兼容内核之十一:windows dll的装入和连接.txt
字号:
if (WrongForwarder ||
ImportedModule->Flags & IMAGE_NOT_AT_BASE)
{
Status = LdrpProcessImportDirectory(Module, ImportedModule,
ImportedName);
. . . . . .
}
else if (ImportedModule->Flags & IMAGE_NOT_AT_BASE)
{
TRACE_LDR("Adjust imports for %s from %wZ\n",
ImportedName, &Module->BaseDllName);
Status = LdrpAdjustImportDirectory(Module,
ImportedModule, ImportedName);
. . . . . .
}
else if (WrongForwarder)
{
. . . . . .
Status = LdrpProcessImportDirectory(Module,
ImportedModule, ImportedName);
. . . . . .
}
else
{
/* nothing to do */
}
}
BoundImportDescriptorCurrent +=
BoundImportDescriptorCurrent->NumberOfModuleForwarderRefs + 1;
} //end while (BoundImportDescriptorCurrent->OffsetModuleName)
}
else if (ImportModuleDirectory) /* 普通引入 */
{
DPRINT("ImportModuleDirectory %x\n", ImportModuleDirectory);
ImportModuleDirectoryCurrent = ImportModuleDirectory;
while (ImportModuleDirectoryCurrent->dwRVAModuleName)
{
ImportedName = (PCHAR)Module->BaseAddress +
ImportModuleDirectoryCurrent->dwRVAModuleName;
TRACE_LDR("%wZ imports functions from %s\n",
&Module->BaseDllName, ImportedName);
Status = LdrpGetOrLoadModule(SearchPath, ImportedName,
&ImportedModule, TRUE);
. . . . . .
Status = LdrpProcessImportDirectoryEntry(Module,
ImportedModule, ImportModuleDirectoryCurrent);
. . . . . .
ImportModuleDirectoryCurrent++;
} //end while (ImportModuleDirectoryCurrent->dwRVAModuleName)
} //end if (ImportModuleDirectory)
if (TlsDirectory && TlsSize > 0)
{
LdrpAcquireTlsSlot(Module, TlsSize, FALSE);
}
return STATUS_SUCCESS;
}[/code]
前面讲过,映像头部的OptionalHeader中有个数组DataDirectory[],其中之一是重定位目录。除此之外,数组中还有“(普通)引入(import)”、“绑定引入(bound import)”、“引出(export)”、以及其它多种目录,但是我们在这里只关心“引入”和“绑定引入”。这两个目录都是用于库函数的引入,但是作用不同,目录项的数据结构也不同。
先看普通的“引入”,其数据结构为:
[code]typedef struct _IMAGE_IMPORT_MODULE_DIRECTORY
{
DWORD dwRVAFunctionNameList;
DWORD dwUseless1;
DWORD dwUseless2;
DWORD dwRVAModuleName;
DWORD dwRVAFunctionAddressList;
}
IMAGE_IMPORT_MODULE_DIRECTORY,*PIMAGE_IMPORT_MODULE_DIRECTORY;[/code]
这里的RVA是“相对虚拟地址(Relative Virtual Address)”的缩写,实际上就是在映像内部的位移。每个引入目录项代表着一个被引入模块,其模块名、即文件名在dwRVAModuleName所指的地方。需要从同一个被引入模块引入的函数通常有很多个,dwRVAFunctionNameList指向一个字符串数组,数组中的每一个字符串都是一个函数名;与此相对应,dwRVAFunctionAddressList则指向一个指针数组。这两个数组是平行的,同一个函数在两个数组中具有相同的下标。可想而知,从一个被引入模块中引入一个函数的过程大体上就是:根据函数名在被引入模块的引出目录中搜索,找到目标函数以后就把它实际装入后的入口地址填写到指针数组中的相应位置上。但是,这个过程可能是个开销相当大、速度比较慢的过程。为此,又发展起一种称为“绑定”的优化。
所谓绑定,就是在软件的“制造”过程中先对使用时的动态连接来一次预演,预演时假定所有的DLL都被装入到它们的愿望地址上,然后把预演中得到的被引入函数的地址直接记录在引入者模块中相应引入目录下的指针数组中。这样,使用软件时的动态连接就变得很简单快捷,因为实际上已经事先连接好了。其实“绑定引入”和静态连接并无实质的不同,只不过是软件的“静态连接,分块发行”,而不是“静态连接,整块发行”。但是,既然是“静态连接,分块提供”,实际使用时不再比对函数名,各模块的版本配套就成为一个问题,因为万一使用的某个DLL不是当初绑定时的版本,而且其引出目录又发生了变化,就有可能引起混乱。为此,PE格式增加了一种“绑定引入”目录,其目录项中加上了时戳字段。这样,只要引入者的“绑定引入”目录项和被引入者具有相同的时戳,就可以认定它们是当初绑定的“原配”。“绑定引入”目录项的数据结构如下:
[code]typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD NumberOfModuleForwarderRefs;
} IMAGE_BOUND_IMPORT_DESCRIPTOR,*PIMAGE_BOUND_IMPORT_DESCRIPTOR;[/code]
这里的TimeDateStamp就是时戳字段,OffsetModuleName指向被引入模块的模块名;另一个字段NumberOfModuleForwarderRefs用于“转引”,下面还会讲到。显然,一个“绑定引入”目录项、即一个IMAGE_BOUND_IMPORT_DESCRIPTOR数据结构,只是针对着一个具体的被引入模块。
但是,“绑定引入”毕竟不是很可靠的,万一发现版本不符就不能使用原先的绑定了。所以“绑定引入”不能单独存在,而必须有普通引入作为后备,这样在发生问题时就可以退到普通引入。
代码中首先通过RtlImageDirectoryEntryToData()分别从映像头部获取指向“引入”目录和“绑定引入”目录的指针。后者是前者的优化。一个映像可以没有“引入”目录,但不能没有“引入”目录却有“绑定引入”目录。每一个IMAGE_BOUND_IMPORT_DESCRIPTOR数据结构或IMAGE_IMPORT_MODULE_DIRECTORY数据结构都代表着一个需要引入的DLL,而一个“目录”就是一个这样的结构数组。然后,如果有“绑定引入”目录就优先按它来处理引入,否则就按普通的“引入”目录处理引入。
先看有“绑定引入”目录存在时的操作。
绑定引入是普通引入的优化,但是绑定引入有个引入者和被引入者之间的时戳TimeDateStamp是否相符的问题。如果不符就不能按“绑定引入”目录处理引入,而只好退而求其次,改成按普通“引入”目录处理引入。另一方面,所谓“绑定”是指当被引入模块装入在预定位置上时的地址绑定,如果被引入模块的装入位置变了,就得对原先所绑定的地址作相应的调整、即“重定位”。
还有个问题就是对于“转引(forward)”的处理。所谓“转引”,是指这样的情况:假定A引入B,而B又引入C;表面上A要引入B中的某个函数f,但是这个函数实际上是由C提供的,B只是转了一下手。如果A针对B的IMAGE_BOUND_IMPORT_DESCRIPTOR数据结构中的字段NumberOfModuleForwarderRefs为非0,就说明B存在着转引,此时紧随在IMAGE_BOUND_IMPORT_DESCRIPTOR数据结构的后面有着相应数量的IMAGE_BOUND_FORWARDER_REF数据结构。
[code]typedef struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp;
WORD OffsetModuleName;
WORD Reserved;
} IMAGE_BOUND_FORWARDER_REF,*PIMAGE_BOUND_FORWARDER_REF;[/code]
这里的OffsetModuleName指向被转引模块的模块名(在映像中的位移)。当然,被转引的模块都需要被装入。
明白了这些,前面if (BoundImportDescriptor){}里面的代码就不难理解了,那就是对于“绑定引入”目录中的每一个目录项实施下列的操作:
先通过LdrpGetOrLoadModule()找到或装入(映射)被引入模块的映像。首先当然是在模块队列中寻找,找不到就从)被引入模块的磁盘文件装入。
检查双方的时戳TimeDateStamp是否相符,如果不符就退而求其次,通过LdrpProcessImportDirectory()处理引入(见下)。当然,那样一来效率就要降低了。
假定时戳相符,如果目录项中的NumberOfModuleForwarderRefs非0,就要对每个需要被转引的模块执行LdrpGetOrLoadModule(),保证它们的映像已被装入。
如果某个被转引模块的时戳与转引目录项中的纪录不符,或者其映像装入地址与预定的不符,那么就又得退而求其次了。代码中先把WrongForwarder设成TRUE,然后据此调用LdrpProcessImportDirectory()。
如果转引没有问题,而只是某个直接的被引入模块的装入地址与绑定的不符,那就只要对引入者模块中相应引入目录下的函数指针作出调整即可,这是由LdrpAdjustImportDirectory()完成的。
下面是LdrpAdjustImportDirectory()的代码,读者可以自行阅读。
[code][__true_LdrInitializeThunk > LdrPEStartup() > LdrFixupImports()
> LdrpAdjustImportDirectory()]
static NTSTATUS
LdrpAdjustImportDirectory(PLDR_MODULE Module,
PLDR_MODULE ImportedModule, PCHAR ImportedName)
{
. . . . . .
ImportModuleDirectory = (PIMAGE_IMPORT_MODULE_DIRECTORY)
RtlImageDirectoryEntryToData(Module->BaseAddress,
TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, NULL);
. . . . . .
while (ImportModuleDirectory->dwRVAModuleName)
{
Name = (PCHAR)Module->BaseAddress + ImportModuleDirectory->dwRVAModuleName;
if (0 == _stricmp(Name, (PCHAR)ImportedName))
{
/* Get the import address list. */
ImportAddressList = (PVOID *)
(Module->BaseAddress + ImportModuleDirectory->dwRVAFunctionAddressList);
/* Get the list of functions to import. */
if (ImportModuleDirectory->dwRVAFunctionNameList != 0)
{
FunctionNameList = (PULONG)
(Module->BaseAddress + ImportModuleDirectory->dwRVAFunctionNameList);
}
else
{
FunctionNameList = (PULONG)
(Module->BaseAddress + ImportModuleDirectory->dwRVAFunctionAddressList);
}
/* Get the size of IAT. */
IATSize = 0;
while (FunctionNameList[IATSize] != 0L)
{
IATSize++;
}
/* Unprotect the region we are about to write into. */
IATBase = (PVOID)ImportAddressList;
IATSize *= sizeof(PVOID*);
Status = NtProtectVirtualMemory(NtCurrentProcess(),
&IATBase,
&IATSize,
PAGE_READWRITE,
&OldProtect);
. . . . . .
NTHeaders = RtlImageNtHeader (ImportedModule->BaseAddress);
Start = (PVOID)NTHeaders->OptionalHeader.ImageBase;
End = Start + ImportedModule->ResidentSize;
Offset = ImportedModule->BaseAddress - Start;
/* Walk through function list and fixup addresses. */
while (*FunctionNameList != 0L)
{
if (*ImportAddressList >= Start && *ImportAddressList < End)
{
(*ImportAddressList) += Offset;
}
ImportAddressList++;
FunctionNameList++;
}
/* Protect the region we are about to write into. */
Status = NtProtectVirtualMemory(NtCurrentProcess(),
&IATBase,
&IATSize,
OldProtect,
&OldProtect);
. . . . . .
}
ImportModuleDirectory++;
} //end while
return STATUS_SUCCESS;
}[/code]
由此可见,对于“绑定引入”的处理基本上只是验证一下,只要确认被引入模块已被装入、并且装入在预定的位置上就行了。即使有个别模块(DLL)没能装入在预定的位置上,毕竟用LdrpAdjustImportDirectory()调整一下也不是代价很大。所以“绑定引入”的效率确实是比较高的,但是,如果时戳不符,或者被转引的模块未能装入在预定的位置上,那就只好退下来改用LdrpProcessImportDirectory()了。
[code][__true_LdrInitializeThunk > LdrPEStartup() > LdrFixupImports()
> LdrpProcessImportDirectory()]
static NTSTATUS
LdrpProcessImportDirectory(
PLDR_MODULE Module,
PLDR_MODULE ImportedModule,
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -