Windows 95 System Programming SECRENTS学习笔记(八)

类别:编程语言 点击:0 评论:0 推荐:

Windows 95 System Programming SECRETS读书笔记、心得

第五章 内存管理

Author: Kendiv

Last Update: Tuesday, January 11, 2005

 

虽然本书内容是根据Windows 95而写的,但是很多思路和细节对于我们研究Windows2000及其后续系统还是非常有用的。本读书笔记不是针对全书的,只针对原书的第3、5、8、10章。这四章分别如下:

 

第三章        模块、进程、线程

第五章  内存管理

第八章  PE与COFF OBJ格式

第十章  写一个Win32 API Spy

 

在每章结束时,我会将原书中针对Windows 95的示列程序移植到Windows 2000中,对于原文中提到的一些细节会提供在Windows 2000中发生的变化。同时补充了很多新的内容。

 

相关参考资料:

MSDN

《Windows核心编程》 Jeffrey Richter 著

《Windows环境下32位汇编语言程序设计》 罗云彬 著

《IBM PC 汇编语言程序设计(第五版)》   Peter Abel 著

 

 

第五章  内存管理(Memory Management)

摘要:

在这一章中,我们将碰触Windows 95内存管理中的各种观念。从CPU的分页机制,每个进程的地址空间,以及分享给每个进程的那些地址空间。在Win32 API方面,我将介绍VirtualXXX函数如何管理pages,也将介绍HeapXXX函数如何提供更高层的内存管理。从Win16衍生而来的堆函数如GlobalXXX和LocalXXX其实只不过是HeapXXX的轻薄包装而已。

 

理论上,Win32内存管理在其三个平台(NT、Windows 9x、 Win32s)之间应该是十分类似的。然而,了解微软在此一领域的轨迹之后,你应该可以预期Windows 95内存管理和NT和Win32s之间还是有许多差异(包括好的和坏的)。这一章我要讨论Win32内存管理的Windows 95版。注意,此处描述的许多概念也适用于NT和Win32s。

 

我把内存管理的子题目分为两个。第一个子题目与进程的地址空间、memory context、分页行为(例如“Copy on Write”)有关。第二个子题目是操作系统提供的内存管理函数。

 

以分页为基础的Windows 95内存管理

如果你打算真正了解Windows 95的内存管理架构。那么你就不可能对Intel 80386 CPU的内存分页(paging)避而不谈。内存分页技术其实远早于80386,但因为我们只对Windows 95如何在80386上使用分页机制感兴趣,所以我只使用80386的那些术语。如果你稍早了解分页(paging),你可以跳过此节。如果你对内存分页感觉十分陌生,或是你需要快速充电,那么就请安心地读下去。

 

内存分页(Memory Paging)

分页(paging)的主要理由是提供一种方法,让操作系统和CPU联手欺骗程序,使程序误以为它所拥有的内存比真正安装在机器上的还多。当程序读写内存中的某个单元时,它可能(也可能不)存取实际RAM上的一个单元。如果程序使用某一个地址,而该地址却没有对应到实际的RAM,CPU就会通知操作系统。操作系统于是采取必要措施,把该地址和实际的RAM联系起来。

 

如果所有执行中的程序的内存用量超过了实际内存安装量,操作系统可能需要从甲程序中抽出一块内存给乙程序使用。盲目的将一块使用中的内存另作他用,是灾难的开始。所以Windows 95必须把原来的内容另外保存在其他地方。这所谓的其他地方,就是硬盘。任何时候,操作系统以及所有应用程序所使用的内存中的数据,不是存储在RAM中,就是存储在硬盘之中。我这么说虽然略显粗糙,但目前为止还够用。

 

上面说的,以分页方式以及第二存储空间来模拟内存,一般称之为“虚拟内存”。Windows 95虚拟内存管理器(VMM32.VXD中的VMM模块)的一个基本工作就是提供虚拟内存,是应用程序受到的扰动达到最小。

 

最令许多人迷惑的地方就是,分页会影响CPU的内存定位。如果没有分页,程序交给CPU的地址,将等同于内存总线(memory bus)上的地址。例如,在真实模式应用程序中,你很容易根据segment:offset计算出实际地址:把segment乘以16再加上offset就是了。但是当分页机制启动后,程序所使用的地址可能和CPU送往内存总线的地址不同。分页机制为所有物理地址引入了两层间接性。当程序交给CPU一个地址,CPU使用该32位地址中的某些位寻找物理内存中的地址(它将在内存总线上传送)。CPU用来进行地址转化的表格,由操作系统控制,这么一来操作系统就可以告诉应用程序使用4GB地址空间中的任何一个地方,而不必在乎那里是不是真的物理内存。

 

之所以使用“分页”这个名词,是因为CPU并不提供以byte为单位的间接地址。内存地址的转化都是以4KB为单位。如果你使用分页机制来指定实际地址0x1000对应到程序的0x400000,实际地址0x1001就会对应到程序的0x400001,实际地址0x1FFF就会对应程序的0x400FFF。然而,下一个程序地址(也就是0x401000)是另一个4KB的开始,所以实际地址0x2000并不一定会对应到程序地址0x401000。也许程序地址0x401000映射到实际地址0x6000,或者根本没有映射到任何实际的RAM。所有映射关系都有操作系统分页机制控制。

 

除了允许操作系统提供虚拟内存,CPU对分页的支持还因此提供了一个很大的弹性,让操作系统在内存中安排各式各样的对象。所谓对象,我指的是像操作系统代码、应用程序代码、程序数据区、内存映射文件等等。操作系统所使用的内存布局成为地址空间布局(address space layout)。稍后我会描述Windows 95的地址空间。

 

分页的好处是,操作系统可以把各种操作系统对象散布在CPU的整个地址范围中。以Intel 386 CPU家族而言,地址范围是4GB。理论上可定位的整个物理内存范围称为CPU的地址空间(address space)。由于分页机制的存在,从而无需先转换那些地址,称之为线性地址(linear address)。至于转换后的,才是真正送到内存总线上的地址,称之为物理地址(physical address)。记住一点,任何情况下应用程序以及API所使用的,几乎都是线性地址,而非物理内存地址。

 

一旦拥有分页能力,操作系统就可以指定地址空间中的各个区段,给特定的数据使用,并预留空间以备增加数据。例如,当应用程序启动后,默认情况下Windows 95会保留1MB的地址空间作为应用程序的Stack。这并不意味着Windows 95马上就要把1MB RAM映射到这1MB地址空间中,而是说这块空间最大是1MB,实际上Windows 95是每次将一页(4KB)映射到那块保留的地址空间中的。

 

分页机制使得操作系统得以保留巨大的内存地址范围,却不需要先付出代价(我是指RAM),直到真正需要RAM为止。

 

任何时候,CPU的4GB地址空间中,每个4KB区段(即一页)有四种可能的状态:

l         状态1:Available,表示该页内存并没有保留给任何人。任何人可以通过分配动作用有它。企图读写该页都将会导致“Page Fault”异常情况(exception 0Eh)。稍后我将描述“Page Fault”异常情况。

l         状态2:Reserved,表示该页内存已经被某人要走了。但是,实际的物理内存还没有被映射到此地址上,也没有任何硬盘空间被保留以用来复制其内容。企图读写该页将会引发“Page Fault”异常情况(exception 0Eh)。操作系统会给该页的拥有者一个机会,把状态改为Committed and Present。

l         状态3:Committed and Present,表示该页已经被分配给某人,并且有一个程序已经用它来存储资料了。CPU的分页机制也已经把4KB的物理内存映射到该页地址上。读写此地址也就是读些映射来的物理内存。这个状态还有一个子状态称为Page Locked,表示这一页是Committed、Present、并保证决不会被置换(swapped out)出去。当一个页处于锁定状态时,永不会有物理内存映射到该页,直到锁定状态解除。

l         状态4:Committed and not-present,这和前一状态十分类似,程序已经配置此块内存并用其来存储资料。不同之处在于,操作系统已经决定,其他地方更迫切需要映射至此区的RAM。因此,CPU已经把此区内容复制到硬盘中,并把此区的每一页标记为Not Present(译注:Not Present表示地址并没有映射到物理内存中)

 

和状态1、状态2一样,如果这样的Page被存取,会发生Page Fault。不同之处在于当程序读取这样的地址,操作系统就会自动处理page fault异常情况并重新映射一块4MB RAM过来。然后操作系统会读取硬盘中原来的数据,再重新执行发生page fault之前的那个指令。于是程序完全不知道有page fault发生。这样透明化的以硬盘模拟RAM正是虚拟内存的基础。

 

Windows 95提供了一些VirtualXXX APIs,使得你的以配置许多pages并改变其属性。例如:VirtualAlloc、VirutalFree等等。稍后我将描述这些函数。

 

内存分页与选择器(Paging VS Selector)

如果你曾经写过Windows 3.x应用程序,你或许会怀疑分页动作如何与selector产生关系。在Intel CPU保护模式中执行的16位程序必须使用selector才能存取CPU地址空间中的某一块内存。每个Win16程序的代码段(code segment )都关联到一个selector;数据段(data segment)也是如此,从Global Heap中取出的任何一块内存也是如此。只要你写Win16程序,绝对不可能和selector老死不相往来。

 

Selector中最基本的数据就是它所指向的地址(也就是“base”地址)。在386机器上,Base总是在0-(4GB-1)之间。换句话说,一个selector有能力指向CPU地址空间中的任何一处。然而,base地址是一个线性地址而非实际地址,因此CPU的分页机制实际上隐藏在selector的支配之下。在Windows 3.1和Windows 95中,16位代码并没有想到分页和虚拟内存,它只是想到有大量内存可以使用。16位的Global Heap管理器从ring0操作系统中配置大块内存出来,并把它切割为小块,使它可以通过selector被程序取用。这些selector的base地址并不需要从4KB边界开始,而且也并非内存区中的每一页都必须映射到实际内存中。

 

如前所述,selector/segment管理系统并没有在分页这一层面上有什么贡献。它让底层的分页系统提供虚拟内存,并假设当它需要时,内存会在那儿。

 

如果你的程序在保护模式下执行,你也不可能避开selector。存取内存时它们是绝对必需品。Windows 95要求至少在一部386机器上运行,而386的一个关键性质就是你可以制作一个节区(segment),使它跨越整个4GB地址空间。也就是说可以做出一个selector,Base为0,limit为4GB。如果你把这样的selector载入CS、DS寄存器中,你就可以忘记所谓“节区”这回事,应用程序可以根据32位偏移地址(offset)就指出任何一个地址。这种情况下32位偏移地址就是线性地址。这种模式(使用base为0、limit为4GB的selector)被称为flat memory model(平滑内存模式),它与过去16位的small、medium、compact、huge模式都不相同。注意,虽然flat模式使得Win32程序中似乎不再出现节区,但CPU还是在底层使用节区进行管理。如果你混合16位代码和32位代码,这一点尤其重要。因为16位代码无法隐藏丑陋的街区。J

 

有了扩大而开放的节区,应用程序可以接触CPU地址空间中的任何位置。你可能会奇怪操作系统如何保护其内部数据资料以及其他不可以被应用程序弄乱的地方。对16位程序而言,这并不困难,因为selector决定了一个特定地址范围,让程序接触,而理论上操作系统决不会给出一个selector,使其base地址存取到它不该存取的地方。然而,Windows 3.x和Windows 95并不阻止你产生你自己的selector然后进入“游客止步区”晃荡一番。本章稍后我将反过来利用这个破绽。

 

如果一个Win32程序使用flat模式,操作系统如何能够约束其他程序,不让它们进入某些地址区域呢?事实上操作系统不再依赖节区大小,而是设定每一页的属性。例如,程序不应该盲目的对其代码区写入东西,所以操作系统把代码区的属性设定为只读。应用程序可以读取它们,但任何写入动作都会导致page fault。同样道理,程序持有一个已被丢弃的指针,就好像把东西写到一个尚未被配置的page一样。

 

操作系统把这些尚未被配置的page统统标记为not-present。企图接触这些地址将导致page fault。此外,操作系统可以把某一范围内的pages标记上supervisor属性,那么它们就只能被更高权限等级的代码(也就是操作系统的一部分,以及VxDs)处理。企图以较低权限等级的代码处理,就会导致page fault。如你所见,即使没有节区,Windows 95还是可以使用分页来有效保护敏感区域。分页的唯一缺点就是内存分配的最小单位是一页(Windows 95中一页大小为4KB),而不是16位节区中的一个byte。

 

 

Windows 95中的Win32进程地址空间

在Windows 3.x中,所有程序都在同一个地址空间中执行。所以任何程序都很容易读取另一个程序使用的内存。更糟的是,程序还可以改变其他程序的内存内容。这就给那些有bug的程序提供了一张通往地狱的车票。例如,在Windows 95中16位Windows程序甚至可以获取16位USER DGROUP的selector并随意写些垃圾进去。于是Windows就只好对你说拜拜了。

 

Windows 95给每个进程一个独立的地址空间。所谓“独立地址空间”,我的意思是程序只能看到它自己的内存,其他进程所使用的内存是不可处理的。更精确的说,Windows 95内存管理器使用CPU“以分页为基础”的内存管理哲学,确保只有当前进程所拥有的内存才会被映射到CPU的4GB地址空间中。其他进程所拥有的RAM并不会出现在当前进程的page tables中。这样做的最大好处是,一个有问题的程序最多只能破坏它自己,不会影响别人。

 

对于Windows的这种特性,其实在UNIX中已行之多年了,Windows NT也是如此。我们只能说,Windows 95这个桌面操作系统拥有了高级操作系统的最基本性质。

 

虽然把每个进程的内存分隔开来是很重要的,但某些内存还是要被所有进程共享的。也就是说所有进程的线性地址空间中的某些页应该被映射到相同的物理内存地址。为什么?最好的例子就是System DLLs。每个进程都需要KERNEL32.DLL,如果每个进程都要加载一份崭新的KERNEL32.DLL,那将是无可置疑的超级大浪费。因此,KERNEL32以及USER32等其他System DLLs应该驻留在共享内存中。当Windows操作系统切换Page Tables以便执行另一个进程时,它会把映射到共享内存中的那些Page Tables保留下来。我将以其他例子来说明共享内存的必要性。

 

由于Windows 95把不同进程的内存都分割开来,所以对Windows 95如何分配4GB地址空间的任何讨论都将离不开所谓的memory context概念。Memory context基本上是一系列的RAM Page,以及它们所映射的线性地址。用另一句话说,Memory context是操作系统给于一个进程的线性地址的视野(View)。

 

每个进程都有自己的memory context。当Windows 95调度器将某个进程暂停而让另一个进程执行,它必须把memory context也切换过来。由于每个进程都有自己的memory context,所以有时候它又被称为process context,有时也被称为address context。不论你把它叫做什么,记住一点,地址本身并没有什么意义,除非你指明这个地址在哪一个memory context中。

 

从最上层来看,Windows 95的Win32进程的内存布局十分简单。在4GB的地址空间中,最底部的2GB(0-7FFFFFFFh)保留给应用程序,2GB以上(80000000h-FFFFFFFFh)则保留给操作系统。这两个部分又都有进一步的分割。图5-1展示了4GB地址空间中的细节。

 

第一个4MB地址空间由系统虚拟器中的进程共享。其中位于1MB之下的第一部分,内含MS-DOS的内存映像(memory image),在Windows 95启动时载入。1MB之下的油区东西还包括16位Global Heap的较低部分。Windows 3.1中的所有16位Heap的线性地址,不是在1MB之下就是在2GB之上。如果它是以GMEM_FIXED属性分配而来的,那么常常就是在1MB之下。你会在地址空间最初4MB中看到许多16位System DLLs,因为它们中有许多(例如KRNL386)需要“Fixed并且PageLocked内存”。这是很重要的一点,稍后我还会在讨论。

 

下一个区域是4MB到2GB。这是Win32进程所使用的地址空间。每一个Win32进程把它自己的代码、数据、资源映射到这将近2GB的范围来。当memory context的切换动作发生时,其实就是换另一组page,映射到这个范围。除非特别指定,否则映射到此区域的RAM pages不能够被其他进程存取。除了应用程序的代码和数据,它所用到任何DLLs的代码和资料也放在此区域中。在这里面你还可以发现应用程序的Heap和Stack(每个线程都有一个stack)。

Win32程序默认被加载到非常低的地址(4MB)。除非你真地了解分页动作否则这样的概念有些不协调。怎么能够有一个以上的程序被加载到同一个地址呢?答案是:它们共享了相同的线性地址,但却不是相同的物理内存地址。一般而言,进程中的线性地址并不会映射到相同数值的物理地址。由于分页运算的关系,每个进程可以认为自己拥有的是4MB-2GB整个空间。它无法看到其他进程的内存,其他进程也无法看到它—即使彼此占用同一个线性地址。分页“魔术”使它们在实际上有所区别。

 

上述规则“为每个进程保存独立的4MB-2GB地址空间”的例外情况是:Windows 95认为“把相同的物理内存开放给同一个程序的多个副本(执行个体,Instance)共享”是安全的。拿程序代码来说,因为程序通常不会修改其代码,如果你执行同一程序的多个副本。那么Windows 95节省内存的做法是:把内含程序代码的物理内存映射到每个进程副本的地址空间中。

 

从最纯净的操作系统角度来看,如果每个16位进程都有自己的地址空间,类似32位进程那样,真是最好不过了。不幸的是大量16位程序都依赖“能够看到其他程序的内存”这种能力而生存下去。为了保留对16位程序的兼容性,Windows 95势必要提供比Win32进程更大的权利给它们。Windows NT 3.51让每个Win16进程都在它自己的地址空间中运行,但是因此消耗更多内存并导致更大的复杂性。Windows 95的设计人员似乎感觉这样的效益不值得其所付出的代价。

 

自从我看过Windows 95,有一个问题就引起我的兴趣:16位程序如何以不同进程的身份而任能够分享其地址空间?结论是:16位程序所使用的内存总是来自4MB以下和2GB以上,而那里就是所谓的全局共享区域。

 

现在让我们把目光转移到4GB的上半部分。从图5-1你可以看到它被分为两个部分:2GB至3GB之间给所有进程共享,并试图给ring3操作系统代码使用。在这个区域的最低部分,你将发现16位的Global Heap。而在它之上,你看到的是内存映射文件。这相当有趣,并且值得深思。

 

如果内存映射文件位于可被所有进程共享的区域,很显然任何进程都可以看到它,甚至不需要对它做映射动作(译注:指的是Win32 MapViewOfFile这个动作),使得,这样的假设是正确的。在Windows 95之中,一个内存映射文件可以被所有进程存取。这个情况与Windows NT不相同。Windows NT使用更精巧的分页模式,使内存映射文件只能够被“对此文件作了映射动作”的进程看到。

 

2GB-3GB区域的最上层为32位System DLLs(KERNEL32、USER32等等)的藏身之处。为了保留最多的空间给内存映射文件使用,ring3 System DLLs从3GB开始处向低处载入。下面是SoftICE/W MOD命令的输出片断,清晰的表示了这个事实:

其中第二栏是模块的载入地址。KERNEL32是第一个被载入的32位System DLL,极端接近3GB(地址: BFF700000)。接下来是USER32,位于BFF200000,并尽量和KERNEL32接壤。也许你会以为这些地址是在载入的时候计算出来的,不,不是这样。微软有一个工具(Win32 SDK中的ReBase.EXE),可以算出一个DLL需要多少地址空间,然后算出最佳载入位置,使这些System DLLs可以被尽量紧密地连接在一起。当这些System DLLs被编译器链接后(译注:当然不是被你),微软接着又修改DLLs,使它们拥有由ReBase.EXE所计算出来的最佳载入位置。这使得所有System DLLs能够以最快时间载入,Windows 95加载器不需要再对它们做“重定位(relocation)”的工作。

 

Windows 95地址空间的最后一块是3GB-4GB(C0000000h-FFFFFFFFh)。最后这1GB是给ring0系统组件(也就是VxDs)用的。这可以从SoftICE/W VxD命令的输出中观察得知。

 

共享内存(Sharing Memory)

Win16中的所有程序和所有DLLs所拥有的所有内存都可以被其他程序和DLLs存取。这是因为每个Win16进程使用的都是同一个区域描述表格(Local Descriptor Table,LDT)。因此,进程之间共享内存时很轻易的事:只要让两个(以上)程序使用相同的selector即可。将准备共享的内存设定为GMEM_SHARE属性,其实并非必要。是啊,不必理会微软信誓旦旦的警告。

 

现在让我们对比一下Windows 95的内存管理,它把每个Win32进程的地址空间都区分开来,除非你特别指定哪一块要共享。不幸的是,指定共享并不只是像使用GMEM_SHARE属性那么简单—事实上在GlobalAlloc中使用GMEM_SHARE是没有用的。也就是说GMEM_SHARE毫无用处:Win16根本不需要它,因为每样东西都可以共享;Win32则根本忽略它。

 

可能你曾经听一些所谓的Win32权威人士说过,在Windows 95或NT中共享内存的唯一方法就是使用内存映射文件(Memory Mapped File)。那的确是一种方法,但不是唯一方法。如果你只是想在同一程序的不同执行体(Instance)之间分享小量的内存,杀鸡何必用牛刀?虽然本书把焦点放在程序与程序之间的可读/可写数据的共享,但是别忘了,4GB地址空间有一半留给系统使用,它们总是可以被所有进程共享。

 

从底层来说,内存共享,只不过是把一页页的RAM映射到一个以上的进程的地址空间中。这些RAM可以被映射到相同的线性地址,也可以被映射到不同的线性地址。

 

在Windows 95中,通过内存映射文件(Memory Mapped File)而完成的内存共享区域,总是在不同的进程中有着相同的线性地址。稍后的PHYS程序会揭露此一事实。然而,在你的Win32程序中作此假设是很危险的,因为Windows NT并不保证内存映射文件在每个进程中有相同的线性地址。许多Win32程序设计书籍都涵盖有内存映射文件这个主题,所以我不打算在这里说太多。

 

最简单的内存共享方法反而没有太多人提起。事实上,只要在链接时指定程序的data section为SHARED属性,你就可以轻易的在同一程序的每个执行体(Instance)之间,或是DLL的每个使用者之间,共享这份资料。只要将Win32 DLL的data section指定为SHARED,其性质就会像Win16 DLL一样。真幸运,Windows 95给我们这么简单又有弹性的数据共享方式。你可以在EXE或DLL中产生多个data section,把所有你打算共享的数据放到其中一个data section,然后把它的属性设为SHARED。至于其他的data section仍然使用默认的属性(NONSHARED)。PHYS程序会示范这一切。

 

一般而言,微软编译器会把所有初始化过的数据放入一个.data的section中,然后留给它一个IMAGE_SCN_MEM_SHARED以外的属性。这会使得每当有一个执行体(Instance)产生,该数据段就会被复制一份,专门给执行体使用。为了共享内存,你可以要求编译器产生一个新的section,名称随你取,但只有前8个字有意义。例如:

#pragma data_seg(“sharedat”)

 

在#pragma之后,你可以声明任何你想要被共享的变量。你应该初始化这些变量,否则它们会被编译器放到另一个专门存放未初始化变量的data section中。

 

变量声明完毕后,如果你要恢复原来的data section属性,只要加上一行即可:

#pragma data_seg()

 

最后,你必须将你的共享心愿传达给编译器知道,你有两种方法,传统的方法是在DEF文件中设定section属性:

SECTION:

     SHAREDAT  FREAD WRITE SHARED

 

另一个做法是在链接器的命令行参数中指定属性。RWS代表Read、Write、Shared:

LINK /SECTION:SHAREDAT,RWS  <其他链接器选项及文件名>

 

#pragma comment(linker,”/SECTION:SHAREDAT,RWS)

 

我应该告诉你一些“使用者须知”之类的警告。如果你将你的数据初始化为程序代码或资料符号的指针,那么当DLL被载入与不同进程的不同线性地址上,事情会变得颇为有趣。看看这个表面上没有什么问题的变量声明(在一个可共享的data section中):

int i;

int * AddressOf_i = &i;

 

问题出在DLL被载入之前,AddressOf_i无法确定下来。因此,DLL必须内含一个待修正记录(fixup record),告诉载入器记得修正AddressOf_i的值。当DLL第一次载入,没有问题。但是如果另一个进程随后也载入此DLL,而载入地址却没有与前一个进程相同的话,由于AddressOf_i已被用于第一个进程(它是被共享的,不是吗),载入器不能够插手修改AddressOf_i的值。于是,对于第二个进程而言,AddressOf_i的值是错误的。利用指针,可以解决这个问题。我可以使用一个非共享的变量,内放一个指针指向共享数据。由于此指针是每个进程都有一个,所以载入器可以修正其值,使它在每个进程中都有正确无误的值。

 

除了将你的数据共享出来,Windows 95还可以共享其他内存。我已经说过,2GB以上全都是共享的。然而,Windows 95也微微开放了2GB以下的一部分区域。如果你执行一个程序的多个副本(Instance),或是在一个以上的进程中使用相同的DLL,那么每重复一份代码都是一种浪费。虽然code section并没有IMAGE_SCN_MEM_SHARED属性,Windows 95还是只载入一份程序代码,然后使用CPU的Page Table,把程序代码映射到其他的Memory Context之中。

 

这种分享code section的做法很好,唯一例外的就是当DLL没有办法载入到不同进程中的相同线性地址时。假设FOO.DLL被两个进程使用,进程A载入FOO.DLL并放到线性地址X处,进程B使用另一组DLLs(其中包括FOO.DLL)。当进程B载入FOO.DLL时,某些其他的DLLs已经占用了地址X,于是FOO.DLL只好使用其他地址。如果你的程序处于这种情况,解决之道是重新设定DLL的载入基址,设到一个从来没有其他进程使用的线性地址上。

本文地址:http://com.8s8s.com/it/it23677.htm