WinCE 编程实验(第八章 动态链接库的载入分析)

来源:互联网 发布:ios客户端程序员招聘 编辑:程序博客网 时间:2024/06/14 17:19
 

第八章 动态链接库的载入分析

 

       动态链接库 (DLL) 一直以来都是Windows的重要基础,Windows CE也不例外。DLL对操作系统十分重要,本节的内容主要是分析loader.c中的程序代码,它负责加载EXEDLL。这里要讨论的是关于DLL的部分,如无特别说明,本章中所引用的程序代码,都是来自Windows CE原始程序代码树中的[CEROOT]/PRIVATE/WINCEOS/COREOS/NK/KERNEL/loader.c

 

8.1 loader.c中程序代码的组织结构

 

       loader.c主要透过以下的API函数,来完成NK核心加载EXEDLL处理程序的工作,这是使用者程序对DLLEXE程序操作的进入点。之后再透过一系列的函数呼叫,来完成DLLEXE的加载和卸载,以及对处理程序和执行绪的其它操作。由于我们现在要讨论的是有关DLL的部分,所以只列出了部分函式。

l          Win32 LoadLibrary call

         HANDLE SC_LoadLibraryW(LPCWSTR lpszFileName)

         HINSTANCE SC_LoadLibraryExW(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)

         HANDLE SC_LoadDriver(LPCWSTR lpszFileName)

         HANDLE SC_LoadKernelLibrary(LPCWSTR lpszFileName)

         HANDLE SC_LoadIntChainHandler(LPCWSTR lpszFileName, LPCWSTR lpszFunctionName,

                   BYTE bIRQ)

这几个函数基本上都是负责DLL的载入,它们都呼叫了LoadOneLibraryW这个函数,函数的原型如下:

         HANDLE LoadOneLibraryW(LPCWSTR lpszFileName, DWORD fLbFlags, WORD wFlags)

这个函数在加载DLL的前后,处理一些必要的细节工作。更重要的是呼叫了LoadOneLibraryPart2这个函数,加载的重要工作都在该函数的程序代码中完成,在本章的后面会详细分析它的内容。

l          Win32 FreeLibrary call

         HANDLE SC_FreeIntChainHandler(HANDLE hLib)

         BOOL SC_FreeLibrary(HANDLE hInst)

主要是FreeLibrary函数。这个API函数负责卸载DLL。它呼叫FreeOneLibrary这个函数,函数原型如下:

         BOOL FreeOneLibrary(PMODULE pMod, BOOL bCallEntry)

卸载的过程主要是由FreeOneLibraryPart2负责。在后面的章节会有具体的分析。

l          Win32 GetProcAddress call

         LPVOID SC_GetProcAddressA(HANDLE hInst, LPCSTR lpszProc)

         LPVOID SC_GetProcAddressW(HANDLE hInst, LPCSTR lpszProc)

这两个函数可以得到处理程序的地址。

 

8.2 module structure

 

       这是记录DLL信息的重要数据结构之一,每一个程序对应一个module,但一个module可以对应多个处理程序。在系统中维持一条已加载模块的串行,它是一个单向链接串行。第一个元素是pModList

 

程序代码8.1 module structure

typedef struct Module {

    LPVOID lpSelf;

    PMODULE pMod;

    LPWSTR lpszModName;

    DWORD inuse;

    DWORD calledfunc;

    WORD refcnt[MAX_PROCESSES];

    LPVOID BasePtr;

    DWORD DbgFlags;                

    LPDBGPARAM ZonePtr;            

    ulong startip;

    openexe_t oe;

    e32_lite e32;

    o32_lite *o32_ptr;

    DWORD breadcrumb;

    DWORD dwNoNotify;

    WORD wFlags;

    BYTE bTrustLevel;

    BYTE bPadding;

    PMODULE pmodResource;

    DWORD rwLow;

    DWORD rwHigh;

} Module;

 

/* 用于验证,指向自己的指标 */

/* 链接串行中的下一个模块 */

/* 模块名字 */

/* 使用状况的位向量 */

/* 被呼叫的进入点,但不退出 */

/* 处理程序的引用计数 */

/* DLL载入基址 */

/* 侦错旗标 */

/* Debug zone 指标 */

/* 基于0的进入点 */

/* 执行档指标 */

/* e32标头 */

/* O32串行指标 */

 

/* 每个处理程序对应一个位,当Notify被禁用时设为1 */

 

 

 

/* 包含资源的模块 */

/* ROM DLL中可擦写段的基地址 */

/* ROM DLL中可擦写段的高位地址 */

 

       refcnt[MAX_PROCESSES]是每个处理程序的引用计数。BasePtrDLL加载的基地址,这是比较需要注意的部分。oee32*o32_ptr放在后面介绍。

 

8.3 LoadOneLibraryPart2加载DLL的过程

 

减少DLL的引用计数

透过文件名字查找模块,得到pMod

模块已经加载到处理程序的地址空间了吗?

增加引用计数,

且如果是第一次加载处理程序,则复制字段

为模块分配内存

初始化 module 

structure

系统能找到指定的DLL档吗?

增加引用计数

使该模块成为pModList的第一个元素

细节处理

传回NULL

传回pMod

参数与载入已载入的mod符合吗?

传回NULL

不符合

符合

呼叫

LoadOneLibraryPart2

8.1  LoadOneLibraryPart2的基本执行步骤

 

       函数原型:

       PMODULE LoadOneLibraryPart2(LPWSTR lpszFileName, DWORD fLbFlags,WORD wFlags)

参数lpszFileName

指向DLL名字的字符串,该名字确定了模块 (module) 的文件名称,而与它储存在模块库中的名字无关。函式库模块以LIBRARY为关键词,在模块定义文件 (.def) 中定义。

参数hFile

是保留做将来使用的,现在必须是NULL

参数wFlags

指定加载模块时所要处理的工作。其值可以为0DONT_RESOLVE_DLL_REFERENCESLOAD_LIBRARY_AS_DATAFILE或者是LOAD_LIBRARY_IN_KERNEL

DONT_RESOLVE_DLL_REFERENCE:如果使用这个旗标,而且可执行模块本身是一个DLL,则系统并不呼叫DllMain来初始化和结束处理程序和执行绪。此外,一个DLL可能会引入包含在另一个DLL中的函数,而系统映像一个DLL时也会自动加载,当这个旗标被设置之后,系统就不再自动加载额外的DLL。加载过程如图8.1所示。 加载的过程首先设定wFlags的值,然后呼叫pMod = FindModByName(dllname),藉由查找pModList串行,来得到DLLpMod指标。对于已经加载的module,增加其引用次数。由以下的程序代码来完成:

         if (!(pMod->inuse & (1<<pCurProc->procnum))) {

                   pMod->inuse |= (1<<pCurProc->procnum);

                   pMod->calledfunc &= ~(1<<pCurProc->procnum);

         }

         return pMod->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.4 DLL加载过程 — InitModule的执行

 

       InitModule处理加载一个新的DLL所要做的大部分工作。函数原型如下:

         DWORD InitModule (PMODULE pMod, LPWSTR lpszFileName, LPWSTR lpszDllName,

                   DWORD fLbFlags, WORD wFlags)

在函数InitModule中,具体执行步骤如下:

1)      初始化pMod中的字段 (field)

2)      呼叫函数OpenADll,产生执行文件指标 (openexe_t)

3)      加载 modulee32 信息,产生e32标头信息 (e32_lite)

4)      检查bTrust level参数

5)      配置内存给DLL,取得Module->BasePtr,即加载DLL的基地址

6)      配置内存给name O32 对象,读取这个moduleO32信息

7)      改变module的名字

8)      复位位映射

9)      呼叫函数FindEntryPoint ,找到EXE的起始IP (或者DLL的进入点)

       整体来说,这个函数就是负责设定pMod各个字段的初始值、取得可执行档的指标以及EXEDLL的进入点。在这个过程中,将各个步骤、判断中的错误码,传回给LoadOneLibraryPart2,用来作为判断pMod是否建立成功的信息,以继续以后的工作。下面,将对这几个重要步骤作详细分析。

 

8.4.1呼叫OpenADll,产生执行档指标 (openexe_t)

 

       module中的oe是可执行档的指针,每个程序对应一个module,每个module对应一个可执行档指标。oe的型别openexe_t定义如下:

 

程序代码8.2 openexe_t structure

typedef struct openexe_t {

    union {

        int hppfs;

        HANDLE hf;

            TOCentry *tocptr;

    };

    BYTE filetype;

    BYTE bIsOID;

    WORD pagemode;   

    DWORD offset;

    union {

        Name *lpName;

        CEOID ceOid;

    };

} openexe_t;

 

 

// ppfs handle

// 对象储存指针

// rom entry pointer

 

//档案类型

 

//分页模式

//偏移

 

 

 

 

 

 

这实际上是与档案处理等有关的结构,描述可执行程序代码的地址、分页模式、偏移等信息。OpenADll呼叫OpenExe,由OpenExe呼叫SafeOpenExe,设定可执行档指标的工作基本上都在SafeOpenExe中完成。

         BOOL SafeOpenExe(LPWSTR lpszName, openexe_t *oeptr, BOOL bIsDLL, BOOL bAllowPaging,           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设定modulee32 标头信息

       module的成员e32的型别定义如下:

 

程序代码8.3 e32_lite structure

typedef struct e32_lite {          

    unsigned short  e32_objcnt;    

    BYTE          e32_cevermajor;

    BYTE          e32_ceverminor;

unsigned long   e32_stackmax; 

    unsigned long   e32_vbase;     

    unsigned long   e32_vsize;     

    unsigned long   e32_sect14rva; 

    unsigned long   e32_sect14size;   

struct info     e32_unit[LITE_EXTRA];

} e32_lite, *LPe32_list;

/* PE 32-bit .EXE header */

/* 内存对象个数 */

/* 版本信息 */

/* 版本信息 */

/* 堆栈的最大值 */

/* module的虚拟内存基地址 */

/* 整个映射的Virtual 大小 */

/* section 14 rva */

/* section 14 size */

/* Array of extra info units */

 

 

这是与内存相关的一组数据,在InitModule中有一段程序代码如下:

         // load O32 info for the module

         eptr = &pMod->e32;

         if (retval = LoadE32(&pMod->oe, eptr, &flags, &entry,        (pMod->wFlags &

                   LOAD_LIBRARY_AS_DATAFILE) ? 1 : 0, bAllowPaging, &pMod->bTrustLevel)) {

                  return retval;

         }

它藉由呼叫LoadE32函数读取DLL文件,设定e32标头的各部分内容,设定堆栈、虚拟内存基地址、映像等正确的值,使接下来DLL的内存配置工作能够顺利进行。

 

8.4.3 DLL的内存配置与Module->BasePtr的取得

       InitModule中的这一部分程序代码的主要作用是设定pMod->BasePtr。为了方便解释,图8.2是简化的流程图。

 

“execute in place” DLL 吗?

是载入核心地址空间吗?

呼叫函数 VirtualAlloc 设定 pMod->BasePtr

保留虚拟内存成功?

由上往下载入

修改DLL Load Base

取得连续的实体页面,设定pMod->BasePtr

藉由函数 MapPtrProc 设定pMod->BasePtr

8.2  DLL配置内存的过程

 

       由可执行档指标pMod->oe.filetype可得到档案类型。DLL有两种映像,一种是普通档案,需要将其加载到RAM中执行。为了节省时间,还有另一种方式就是XIP (Execute in place) 方式。顾名思义,就是可以立即执行,而不用加载RAM中,所以相应pMod->BasePtr的值也有两种不同的情况。如果不是XIP (Execute in place) DLL,则保留足够的虚拟内存地址空间以容纳整个映像。从已经存在的DLL的底部开始由上往下分配。

       DLL加载到核心地址空间 (kernel space) 或是使用者空间 (user space) 也有差别。对于加载核心地址空间的DLL,必须为其取得连续的实体页面。因为核心空间地址和虚拟内存地址之间是静态映像的,这是为了让核心执行时,不必进行地址的转换,藉以加快核心的执行。例如在Platform Builder 下产生的platform,启动时加载的features等就是加载核心的DLL

         if (wFlags & LOAD_LIBRARY_IN_KERNEL) {

                   PHYSICAL_ADDRESS paRet;

                   // Loading in the kernel address space

                   paRet = GetContiguousPages((DWORD) (eptr->e32_vsize + PAGE_SIZE - 1) / PAGE_SIZE,

                            0, 0);

                   if (paRet == INVALID_PHYSICAL_ADDRESS || !(pMod->BasePtr =

                            (PVOID) Phys2Virt(paRet))) {

                            return ERROR_OUTOFMEMORY;

                   }

         }

它先向系统要求连续的实体地址页面,取得实体地址,再将其直接映像到虚拟地址空间,并把虚拟内存地址传给pMod->BasePtr。有关GetContiguousPagesPhys2Virt的细节请参考其它的源文件。如果不是加载核心地址空间,则必须为其保留地址空间,以避免其它DLL加载同样的地址空间,而使该DLL的卸载发生问题。程序代码如下:

         } else {

                   // try to honor the Dll's relocation base to prevent relocation

                   if ((pTOC->ulKernelFlags & KFLAG_HONOR_DLL_BASE) && (eptr->e32_vbase +

                            eptr->e32_vsize < ROMDllLoadBase)) {

                            pMod->BasePtr = VirtualAlloc ((LPVOID)(ProcArray[0].dwVMBase +

                                     eptr->e32_vbase), eptr->e32_vsize, MEM_RESERVE | MEM_IMAGE,

                                     PAGE_NOACCESS);

 

                DEBUGMSG (pMod->BasePtr, (L"Loading DLL '%s' at the preferred loading address

                                     %8.8lx/n", lpszFileName, ZeroPtr (pMod->BasePtr)));

            }

其中,eptr->e32_vbasemodule映像的虚拟内存基地址,eptr->e32_vsizemodule的虚拟内存大小。利用VirtualAlloc在目前处理程序的虚拟内存地址空间保留一个区域,基地址是处理程序地址空间基地址ProcArray[0].dwVMBase + module映像的虚拟内存基地址。大小受模块影响。它使用参数MEM_RESERVE | MEM_IMAGE,只保留了处理程序的一部分虚拟内存空间,而没有实际分配物理内存。而且这部分空间不能透过其它内存配置的操作如mallocLocalAlloc使用,也不能被其它DLL占用。如果配置基地址时,发现要使用的区域已经被保留,即已经有其它DLL占用,造成配置虚拟内存失败,则由上往下配置。

                   // allocate top-down if we can't load it in the dll's preferred load base.

                   if (!pMod->BasePtr && !(pMod->BasePtr = VirtualAlloc ((LPVOID) ProcArray[0].

                            dwVMBase,eptr->e32_vsize, MEM_RESERVE | MEM_TOP_DOWN | MEM_IMAGE,

                            PAGE_NOACCESS))) {

                            return ERROR_OUTOFMEMORY;

                   }

         }

配置的基地址是目前处理程序的虚拟内存空间基地址,大小不变,只是由上往下配置。这样就得到了pMod->BasePtr。下面的程序代码修改DLL的加载基址,这是个全域变数。

         if (ZeroPtr(pMod->BasePtr) < (DWORD)DllLoadBase)

                  DllLoadBase = ZeroPtr(pMod->BasePtr);

如果要加载的DLLXIP的,即表示它是在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)) {

                            return ERROR_BAD_EXE_FORMAT;

                   }

         }

 

8.4.4 nameo32对象的内存配置

       o32对象是与存取控制有关的对象。结构定义如下。

         typedef struct o32_lite {

                   unsigned long       o32_vsize;

                   unsigned long       o32_rva;

                   unsigned long       o32_realaddr;

                   unsigned long       o32_access;

                   unsigned long       o32_flags;

                   unsigned long       o32_psize;

                   unsigned long       o32_dataptr;

         } o32_lite, *LPo32_lite;

 

 

8.4.5 复位位映射

       XIP映射需要重新寻址 (Relocate)

         if (pMod->oe.filetype != FT_ROMIMAGE) {

                   //

                   // Non-XIP image needs to be relocated.

                   //

                   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))) {

                            return ERROR_OUTOFMEMORY;

                   }

这里呼叫Relocate DLL重新寻址,传递的参数为:可执行文件指针eptro32标头信息pMod->o32_ptrDLL加载的基地址pMod->BasePtr,如果DLL加载到核心,则最后一个参数为0,否则传递的参数是目前处理程序虚拟内存空间的基址。这里还需要解释一下重新寻址 (Relocate) 的过程。重新寻址的过程,是将DLL映像地址定位到目前处理程序的地址空间,即地址0x00000000-0x02000000,从0x01FFFFFF开始由上往下,最顶端是coredll.dll,然后是其它DLL

       如果是XIP映射,即DLLROM中。如果module被加载内存的Slot1 (DLL高地址区域),或者加载到核心中,则需要记录为这个module而寻址的读写区。程序代码如下:

         o32_lite *optr = pMod->o32_ptr;

         //

         // If the module is loaded into the Slot 1 (DLL High) area or into

         // the kernel we need to record where the read/write section has

         // been located for this module.

         //

         if (IsModCodeAddr (pMod->BasePtr) || IsKernelVa(pMod->BasePtr)) {

                   // find the high/low of RW sections

                   for (loop = 0; loop < eptr->e32_objcnt; loop ++, optr ++) {

                            if ((optr->o32_flags & IMAGE_SCN_MEM_WRITE) && !(optr->o32_flags &

                                     IMAGE_SCN_MEM_SHARED)) {

                                     if (pMod->rwLow > optr->o32_realaddr)

                                               pMod->rwLow = optr->o32_realaddr;

                                     if (pMod->rwHigh < optr->o32_realaddr + optr->o32_vsize)

                                               pMod->rwHigh =  optr->o32_realaddr + optr->o32_vsize;

                            }

                   }

         }

 

8.4.6 EXE的起始IP

       这里是藉由呼叫函数FindEntryPoint ,来设定pMod->startip

         if (entry) {

                   if ((wFlags & LOAD_LIBRARY_IN_KERNEL) || !pMod->e32.e32_sect14rva)

                            pMod->startip = FindEntryPoint(entry,&pMod->e32,pMod->o32_ptr);

                   else {

                            HANDLE hLib;

                            if (!(hLib = LoadOneLibraryW (L"mscoree.dll",0,0)) || !(pMod->startip =

                                     (DWORD)GetProcAddressA (hLib,"_CorDllMain"))) {

                                     return ERROR_DLL_INIT_FAILED;

                            }

                   }

         }

如果是加载到核心,则呼叫FindEntryPoint函数,将pMode32信息和o32标头当作参数,寻找进入点的内存对象,最后取得实际地址。如果不是加载到核心,则是利用GetProcAddressA函数取得DLL的进入点。

 

8.5 实例分析

 

1.     范例环境的建立过程

1)      Platform Builder 4.0下,使用其所提供的emulator作为platformBSP,建立新的platform tiny kernel

2)      Build后产生新的Platform loader_test,它同时产生debugrelease版本。用debug版本侦错,追踪loader.c,可以看到这个用作测试的loader_test启动时加载各个DLL的过程。

3)      建立控制台应用程序,编译产生loader_test上的应用程序 console_test。要注意的是,因为这里建立起来的是tiny kernel,所以不支持一些C链接库函数。当然,你也可以建立其它类型的platform

4)      建立空的动态链接库dll_test,用console_test来呼叫dll_test,追踪DLL载入的过程。主要是看其加载的地址pMod->BasePtr。在Platform Buildertarget中看Modules and Symbols窗口,可以看到DLL加载的映像地址范围和重新寻址后的地址范围。

2.     启动时加载DLL

       loader_testdebug版本进行追踪,在loader.c的程序代码上设置断点,观察其在启动时,依次加载的几个DLL的情况。表8.1loader_test启动完成时,已加载的几个DLLEXE

 

8.1 载入的DLLEXE

Module

Image Address Range

Relocated Data Address Range

Status

coredll.dll

0x03FC0000-0x03FF6FFF

0x01FFF000-0x01FFF780

loaded

filesys.exe

0x04010000-0x04049FFF

 

loaded

fsdmgr.dll

0x03F80000-0x03F92FFF

0x01FF9000-0x01FF98F0

loaded

Kd.dll

0x80293000-0x802A9FFF

0x80354000-0x8035A584

loaded

Nk.exe

0x80220000-0x802B8FFF

0x80330000-0x8034A7E6

Loaded

relfsd.dll

0x03FB0000-0x03FB7FFF

0x01FFD000-0x01FFDABC

Loaded

shell.exe

0x06010000-0x0601CFFF

 

Loaded

toolhelp.dll

0x03FA0000-0x03FA3FFF

0x01FFB000-0x01FFB050

loaded

 

已经加载的module5DLL3EXE3EXE中,fileSys.exeSlot2nk.exe在核心地址空间,shell.exeSlot3,属于device部分。5DLL中,kd.dll也在核心地址空间。剩下的4DLL,都在Slot1。而Slot1正是载入ROM DLL的地方。由于现在还没有使用者程序,所以这几个DLL是原先储存于ROM中,XIP型的DLL(详细内存映像,请参照内存管理部分)Windows CE的内存映像中,Slot0 (地址0x00000000-0x02000000) 是目前处理程序的地址空间。DLL经过重新寻址后,对应到目前处理程序的地址空间。由上往下依次是coredll.dllrelfsd.dlltoolhelp.dllfsdmgr.dll

3.     使用者DLL加载过程小结

       每个DLL对应一个module结构,只有一个BasePtr,所以不同的处理过程调用相同DLL加载的基地址是一样的。module中还包含DLLEXE的可执行文件指针信息、档案信息、内存信息、存取控制信息等,这些都是不同的处理程序所要共享的。同一个处理程序对一个DLL的多次加载只会增加它的引用计数。它只会加载到处理程序的虚拟地址空间中一次,每次对它的引用都是引用相同的module,用相同的执行档指标,只是增加一个引用计数而已。多个处理程序加载相同的DLL时,因为使用共同的BasePtr,所以就必须保留每个处理程序的每个DLL的虚拟地址空间,这可以藉由DllLoadBase来达成。每次载入新的DLL后,就修改DllLoadBase的值,而再加载时就从这里开始。所以就保留了所有其它处理程序已经加载之DLL的空间。确保呼叫时不会出错。图8.3简单说明了这一加载过程。

 

 

DLL1

DLL2

DLL3

DLL1

Process1

process2

DllLoadBase(0)

DllLoadBase

(1)

(2)

process3

8.3 处理程序加载DLL的示意图

 

       process1process2process33个处理程序,各自有自己的处理程序空间。Process1加载DLL1时,从DllLoadBase所指的地址开始,将DLL加载到process1的虚拟地址空间,然后修改DllLoadBase的值到指向地址 (1)Process2也需要加载DLL1,但是DLL1已经加载过 (module链接串行中可以查出)。所以,还是引用process1加载DLL1时产生的module,由于用相同的BasePtr,所以加载相应的地址空间。Process3加载DLL2,它从 (1) 这个地址开始加载,DllLoadBase现在指向 (2) 的地址。而process1加载DLL3时,空下了DLL2的空间,以备它以后加载DLL2时使用。