一.多个工程共用
1.1语言运行时库
编程语言的库文件:将在所有程序中都很常用的,含有必备的基础功能的文件预先编译好所形成的大量目标文件。
优点:程序员生成程序时链接他们就可以省去自己的时间,方便程序的移植。
C语言标准库:具备以下几个头文件:
1.2静态链接库
一堆*.OBJ文件的内容合并在一起成为一个文件,包括这些*OBJ文件中的各个符号,以及各个符号的内容。静态链接库的后缀名一般是*.A或*.LIB
二.多道程序共存
2.1简单分区
将物理内存分割成几个块,一个块运行一个应用程序,或者放置一个应用程序的某个段。各个应用程序的各个段在链接时就决定好要放置在什么地方。
优点:
1.配置容易。不需要额外的硬件就可以实现,节约硬件成本。如果*obj在手,直接编写链接器脚本指导链接器做出此种链接行为即可。
2.权限控制简单。每个程序能够合法访问的物理内存范围组成内存保护表。设计硬件,针对每一次内存访问进行合法性检查,就可以防止访问越界。
内存保护单元(MPU):内存保护单元使用一个内存保护表,表中的每一项都包括“区间范围”和“区间权限”一对信息。由应用程序发起的每一次内存访问都需要经过内存保护表指定的权限检查。
(1)访问的地址必须落在某分区的地址范围之中。
(2)发起访问的性质(读写执行)必须是该分区的权限允许的。
问题:很多厂商为保护知识产权,减少程序体积等原因往往不会放出可二次链接的*obj文件。他们往往会放出*bin,*hex,*exe或其它格式的可执行文件,这些文件已经采用直接回填法将其链接在一个固定的地址,而且应用程序的符号信息也丢失了,无法进行二次链接。这种文件俗称“二进制球“,拒绝外界修改。
问题:再发生地址冲突,由于软件已经封死,很难再重定位一些段。所以简单分区只适用于应用程序大小固定,数量已知的场合。
2.2分段与分页
硬件地址翻译:采用添加额外硬件的方法,将应用程序发起的存储器访问的地址做系统性翻译,使得不同应用程序中对一个地址发起的访问实际对应内存总线上的不同地址。
虚拟地址:应用程序认为它自己再访问的地址。也叫逻辑地址。
物理地址:CPU实际送出到内存总线上的物理存储单元地址。也叫实地址。
XX地址空间:由XX地址组成的地址集合就叫做XX地址空间。
内存管理单元(MMU):一种能依照某种规则将对逻辑地址的访问转换成对对应的物理地址访问的硬件。
分段:
按段划分虚拟地址:从程序的逻辑组织出发,其基本单元是一个个段。那么,我们只需要将每个段重新映射到不同的物理地址就好了。为此,我们需要给每个段分配一个段号,同时每个段都对应一个物理地址区间,还拥有一个访问权限。每个程序的虚拟段到物理段的映射关系形成段表。
段式内存管理单元(S-MMU):常用于分段布局的存储器访问管理工具,具备按段地址重映射和访问权限管理两个职能。段式内存管理单元使用一张段表,每个段都包括“段号”“段物理地址范围”“段权限”三个部分。由应用程序发起的每一次内存访问都需要经过段表指定的的转换和检查:
(1)按照访问的虚拟地址中的段号信息查找相应的段。
(2)发起的访问的性质(读,写,执行)必须是该段的权限允许的。
(3)访问的物理地址=段基址+虚拟地址,且虚拟地址不得超过段长度。
此种方法在硬件上仅需要一组比较器和一组加法器电路就可以实现,是需要虚拟地址空间时的一种讨巧方法。
分段的问题:段的连续性不容易保持,因为一个段必须使用一整块物理内存。
分页:
按页划分虚拟地址:从物理地址空间的细粒度划分出发,将物理地址切割成一个个大小相等的页,并将每个虚拟页映射到不同的物理页。为此,我们需要给每个页分配一个页号,同时每个虚拟页对应一个物理页,还拥有一个访问权限。每个程序的虚拟页到物理页的映射关系组成页表。
页式内存管理单元(P-MMU):常用于分段布局的存储器访问管理工具,具备按页地址重映射和访问权限管理两个职能。页式内存管理单元使用一张页表,每个页都包括“页号”,“页物理地址”,“页权限”三个部分。由应用程序发起的每一次内存访问都需要经过页表指定的转换和检查:
(1)按照访问的虚拟地址中的页号信息查找相应的页。
(2)发起的访问的性质(读,写,执行)必须是该页的权限所允许的。
(3)访问的物理地址=页基址+页内偏移量。
问题 假设应用程序的地址空间有4GB,每个页4kB,每个页表项占据4字节, 每个页表的大小是?系统中若有100个应用程序,页表共占多少空间?(每个页表可以管理4GB/4KB=2^20个页,所以每个页表的大小就是4MB。对于100个应用程序,每个应用程序都需要一个页表,所以总共需要100个页表。因此,所有页表共占的空间为400MB.
三.多道程序共享
3.1动态链接库
静态链接的问题:
重复存储:库会被在每一个可执行文件中保留一个副本,浪费磁盘。
更新困难:每次更新库之后,都需要把用到它的可执行文件重新链接。
重复加载:库会被每一个虚拟地址空间中保留一个副本,进而导致在物理地址空间中也产生重复的副本,浪费内存。也浪费载入时间。
动态链接库:在可执行程序被加载时,作为独立文件单独加载的库文件。它不包括在可执行文件之内;可执行文件将在需要它们的时候加载它们。
外存的库共享:可执行文件中不再包含库文件。这样,库文件只要在外存中存在一份。这个特征是动态链接库的根本特征;判断一个库是不是动态链接库,就看调用它的可执行文件中是否包含它的内容。如果不包含,它就是动态链接库。
内存的库共享:物理地址空间中仅包含库文件(的代码段)的一个副本。这样,即使库文件被多个虚拟地址空间中被使用,它也只会占用一份物理内存,减少了内存用量,也减轻了缓存负担。
3.2外存的库共享
问题:现在有了静态链接库,能否直接将它作为动态链接库用呢?
解决方案:将静态链接器做成静态链接库然后静态链接到应用程序中,应用程序在启动时先启动静态链接器对自己和库进行静态链接后再运行。
优点:
(1)静态链接的工具链和库可以直接用。
(2)无需操作系统的加载器支持动态链接。
(3)采用直接回填法时不产生任何性能损失。
(4)用不到的符号可以不链接到运行时视图。
缺点:
(1)需要修改自己的各个段以添加静态库中的符号。
(2)如果采取直接回填法,还需要填充自己.text段里面的引用,速度很慢,程序启动需要很长时间。
(3)不管这些库文件在执行中是否真的用到了,程序运行前必须一次性链接程序声明引用的库,加剧了(2)。
(4)可执行文件中必须保留自己的全部链接信息。有可能被友商或者黑客拿去反向工程。 (5)外存库可以共享,但加载到内存之后还是会产生多个副本。
改进:最小化链接成本
接口的定义:应用程序和库之间是通过某些定义好的接口互相引用的。原则上,应用程序只要暴露这些接口就可以了,库也是一样。这样,没必要暴漏任何的多余信息,在加载应用程序时也不需要动态链接这些信息。
动态库的生成:在生成动态链接库时,将动态链接库中的符号都链接到某个固定的虚拟地址,然后将动态链接库中被导出的接口符号以外的符号都除去。这样,动态链接库中一切符号的位置都固定了,对自己的内部函数的引用不再需要链接。在加载动态链接库时,只要将它加载到那个预链接的固定虚拟地址就好,然后修改应用程序中对它的引用,指向动态链接库即可。
链接器的共享:前面的解决方案里面,链接器在每个可执行程序中都有一个副本。这实际上没必要,如果操作系统能够提供一个链接器并且在加载可执行文件的时候自动加载,就不需要把它放在可执行文件里面了。
动态链接器:加载动态链接库并将其与应用程序相链接的链接器。它在可执行文件加载时或应用程序运行时才运行。(静态链接器:在生成可执行文件时运行)
动态库重定位:在生成动态链接库时,包含一些重定位信息,告诉动态链接器,如果发生地址冲突,要修改动态链接库的什么地方来处理冲突。
3.3内存的库共享
改进:多个应用程序共享同一份内存副本
重定位的问题:将动态链接库重定位后,如果需要多个应用程序共享一份内存副本的话,不管这个库重定位到哪里,多个应用程序都必须以相同的虚拟地址引用这个库。
如果新加载进来的应用程序与已经加载好的动态库冲突怎么办?
如下图所示,加载应用程序1时库被加载进物理内存并映射到某个固定虚拟地址,加载应用程序2时库也被映射到应用程序2的同样的固定虚拟地址,但加载应用程序3时冲突了。
解决方案一:再加载一个新的副本,重定位到与应用程序3的本体不冲突的虚拟地址去。
问题:物理内存中有两个库副本了。比三个副本还是来得好,但是没达到目的。
解决方案二:规定动态链接库使用的地址范围为0x0000-0x7FFF,而应用程序只能使用0x8000-0xFFFF,这样不可能出现应用程序和动态链接库冲突的情况。但这限制了应用程序的布局,如果应用程序需要使用低地址呢?而且,每一个动态链接占据一个独立的虚拟地址。
改进:位置无关代码
位置无关代码:加载到任何绝对地址都可以直接运行的代码。其所有的地址访问(包括代码和变量)都相对于某指针,常见的是IP、SP或BP。
改进后,不会再出现任何冲突问题,因为同样的一段代码不管映射到什么虚拟地址都工作,任何动态库只在物理内存中加载一次就可以了,不需要关心虚拟地址冲突问题,因为我们压根就没动库的代码段,库的代码段都是相对寻址。
改进:库的数据段在内存中的共享
.rodata 本质上讲,.rodata和.text是一回事,只是不能执行罢了。它是只读的,共享它没有题。 .rwdata 这里包含可读写的数据,不同的应用程序可能会给库传递不同的参数,因此其实它们在逻辑上是独立的,不能共享。
.zidata 和.rwdata的情况是一样的,不能共享。
.heap 每个应用程序当然会以不同的方式使用堆。不能共享。这个段在库文件中一般是不存在的,因为应用程序才有堆。库是没有自己独立的堆的。
.stack和.heap的情况是一样的,不能共享。这个段一般也不存在,因为栈就是应用程序的栈。
问题:怎么处理这些不能共享的段? 提示考虑虚拟地址空间。
考虑虚拟地址空间,只要将库的可读写段映射到不同的物理地址就可以了,它们自然就分开了。当然,在虚拟地址空间,各个应用程序加载的库文件访问的地址还是那个地址。库的只读段则映射到同样的物理地址,避免产生不必要的拷贝。
改进:动态链接库调用动态链接库 链接库互相调用
如果一个动态链接库要调用另一个动态链接库,怎么办?
注意事项 :动态链接库的只读段在多个应用程序之间共享,不可更改, 无法做直接回填法。因为另一个动态链接库的加载地址在不同虚拟地址空间之间可能也是不一样的。
利用读写段多副本的特性,使用间接地址法,将自己引用的所有符号的地址都放在可读写段的某个表格中,每一次函数调用和全局变量存取时都先查询这个表格得到该符号在本虚拟地址空间中的地址,然后再访问那个地址即可。当然,调用内部的符号时只要使用IP,SP或BP相对寻址就可以了。
3.4惰性动态链接
改进:推迟链接直到需要之时
随着应用程序的发展,一个程序引用的动态库越来越多,这包括它们的语言运行时库、各种第三方库,甚至还有第三方库引用的第三方库引用的第三方库(!)
这些库之中有很多根本不会使用,可是为了方便编程但凡是个应用程序就会声明自己可能引用它们。如果在应用程序启动时就加载或 者映射这些库,机器就要卡死。那么,能否将动态链接库的加载和链接推迟到真正使用它们的一刻呢?
直接回填法:直接回填法哪怕对应用程序都做不到,因为我们不可能让应用程序空着地址去跑。一旦跑到那条没填充的指令就干脆出错了。
间接填充法:也有问题,因为库还没加载,那些间接地址填充什么才好呢?填充 0的话,又会出相同的问题了。
延迟绑定:在调用到某某符号或功能时才去查找和加载它。如果该符号或功能 一直不被调用,那么它们就永远不会被加载。
惰性动态链接:对于全局变量(一般而言不多),我们在程序启动之时就向间接跳转表填充它们的地址。对于函数(动态链接库的主要成分)则不同,我们可以在间接地址表里面填充一段专门用来查找函数真实地址的代码。这样,第一次函数调用将会跳转到查找代码,由查找代码来查找函 数的真实地址,再回填到间接地址表里面,最后再调用那个真实函数。下一次访问间接地址表进行函数调用的时候,就会直接调用到真实函数。
惰性动态加载:没有必要在程序启动时就映射整个动态库,仅加载其间接地址表就足够了。当程序访问间接地址表指向的地址,发现那里没有加载(映射)动态库,就会抛出异常,操作系统截获这个异常,在异常处理中再加载动态库的对应部分就可以了。
改进效果 :加载进来的仅仅是间接地址表,而且就连它也只加载了全局变量的地址,函数地址则未加载。其它一切都是用到了才会加载的。
思考:连接方法越灵活,功能越多越好吗?静态链接是否如仍有一席之地?
版本管理 动态链接库的二进制(制品)版本管理是老大难。哪个版本的DLL才能工作?我要到哪去找呢?(DLL地狱;部分企业惯用静态库)
安全性 共享得越多越不安全。一旦外存库被恶意修改,所有动态加载这个库的程序下次打开就会遭殃;一旦内存库代码段被恶意修改,当前正在使用这个库的所有程序立刻遭殃。
运行效率 越灵活,运行时经过的中间层越多,效率越低。
实时性 初始加载越不彻底,中间临时加载发生卡顿的可能性就越大。