在加载完PE可执行文件后,回到kernel32的入口函数__wine_kernel_init中,接下来调用了函数LdrInitializeThunk。dll的装入和连接过程主要是该函数实现的。
函数部分代码如下图所示:
先用main_exe_file判断主模块是否已经被建立了,这是在wine_process_init函数中被赋值的一个句柄类型。
get_moderf的作用正如注释所说,是为可执行文件分配一个模块的引用结构。
在Wine装入和连接dll的时候,每个用到的dll以及exe,都对应于一个“模块的引用结构”,WINE_MODREF类型,并且装载过程中也将单个dll或exe视作一个模块。结构如下图所示:
其中LDR_MODULE类型来源于Windows。由于每个dll中都描述了其直接依赖的一组dll,所以需要用一个指针数组deps来记录。而nDeps顾名思义就是直接依赖的dll个数。
所以在装入和连接过程中,应该是有这样一颗树
然后就是对PEB信息进行填充。
该函数中直接负责dll装载和连接的,只有fixup_imports。函数代码如下图所示:
fixup_imports是用来将一个模块所依赖的所有模块都装入连接进来。当然对于标志位是LDR_DONT_RESOLVE_REFS,即不需要解析其引用的模块,或者该模块的引用数为0(这种情况当然只有ntdll.dll才可能),那么就可以直接返回了。而对于大多数依赖其他dll的模块而言,接下来就是要装入和连接它们了。
从LdrInitializeThunk看到,fixup_imports会从第一个模块,即exe开始,按照我们上面的图的依赖关系把所有需要的dll都装入连接起来。但这里必然会有重复的情况,比如gdi32.dll依赖于ntdll.dll,而kernel32.dll也依赖于ntdll.dll。不过,这不是fixup_imports考虑的问题,它只管接下来调用import_dll,后者将按照导入表imports[]中记录的模块,将它们一一装入,并为装入的模块中的所有函数分配好地址空间。
import_dll中与连接相关的代码涉及到PE格式的细节,就不展开讨论。总体上说,它会调用load_dll将目标模块装入,如果目标模块还依赖于其他dll,那么load_dll还将进一步装入被依赖的dll,直到所有被依赖的dll都被装入到进程空间为止。之后,import_dll再完成从函数名到地址的解析。循环往复过后,一个可执行文件依赖的dll都将被装入,用到的函数都将被译成地址,用于程序执行过程中的调用。
前面有讲到,EXE文件的加载也调用到了load_dll,但没有详细展开。该函数代码如下图所示:
首先,load_dll需要确定是否能找到需要装载的dll,并且该dll是否已经被装入,即调用find_dll_file。后者会根据进程参数块(ppb)中指定的各个路径,去查找是否有该dll的文件。如果找到,那么pwm就不为空,说明该dll已经被装入,不必再重复装载,返回即可。
get_load_order这个函数是专门为wineconfig准备的。我们知道,用wineconfig是可以为每个应用程序配置各种dll的使用搭配,有些dll可以使用built-in的,有些则可以用native的。这些信息最终都写到注册表中,而get_load_order就会从注册表中读取该dll的选用信息。
这里一个有意思的插曲,is_fake_dll函数判断找到的dll文件是否是假的dll。在~/.wine目录生成后,其下的drive_c/windows/system32下面,就产生了很多PE格式的dll。这些dll通常只有几k字节,内容也是一些简单的指令,很显然是无法使用的。至于Wine为什么要产生这些dll,还没进行过深入的分析,但是如果find_dll_file找到的是这样的dll,那么下面就会当作没有找到该dll处理。
下面的代码就是根据loadorder的值选择装入什么格式的dll,native的或者built-in的。从函数名就可以看出,load_native_dll是用于装入native dll的,而load_builtin_dll是用于装入built-in dll的。在开头提到过,Wine通过winebuilder将所有built-indll都做成了类似PE格式的dll,那么为什么这里装入时还有不同呢?
其实,built-in dll仍然是ELF格式,只是winebuilder刻意为它们各自写入了一个类似于PE格式中的header,这样,在装入过程中,仍然可以获得dll的描述信息,写到LDR_MODULE结构中。但是最终连接时,还是要根据ELF的头部来解决最终的装入连接问题。
因此可以看到,load_native_dll是调用Windows的 section系统调用将dll装入(前面有讲到),而load_builtin_dll则要通过wine_dll_load装载。
至此dll的装入连接过程就可以结束了。函数最后做了一个判断,需要说明一下。其实这时候,再回顾一下前面的过程会发现,如果代码执行到这个判断中,即nts == STATUS_SUCCESS的话,那么肯定是这个dll被装入到进程空间的那次。如果一个dll被多次引用而进入到load_dll的话,从第二次开始就不会进入到这个判断了。所以对同一个dll,只会执行一次if语句块中的代码。
参考:longene兼容内核论坛