[音乐]
前几讲介绍了有关静态链接的符号解析和重定位两个重要过程,
并介绍了静态链接生成的完全链接的可执行目标文件的加载过程。
实际上在计算机系统中默认的都是采用动态链接方式,
而很少用静态链接方式,因此,本讲将 重点介绍动态链接的基本概念和主要思路。
包括动态链接的共享库文件、 自定义共享库的创建、
加载时的动态链接过程、 运行时的动态链接方式。
符号引用的方式共有4种:模块内调用或跳转、 模块内数据访问、
模块间调用或跳转、 模块间数据访问。
本讲将通过例子 介绍这4种不同符号引用方式下,如何构建位置无关代码。
首先我们来看一下动态链接
所用到的共享库,在介绍共享库之前呢,我们先看一看
静态库的一些缺点,我们在前面讲静态链接的时候用 到的是点a文件,也就是静态的库。
在进行静态链接的时候, 静态的库函数,比如说hello程序里面调用的printf这个函数。
这些函数的代码呢,是会包含在每个进程的代码段中的。
因为静态链接的时候,是通过把所有的点o文件进行合并,合并成
新的这个代码段,所以这个代码段里面包含了这些静态库函数的代码。
如果一个系统当中有上百个这样的并发的进程,这些进程里面
都会包含静态库函数,比如说像printf这样的函数, 就会造成主存资源的极大的浪费。
另外一个方面呢,静态库函数,比如说像printf
这种函数,再被进行链接生成可执行文件的时候, 它是合并到可执行文件当中去的。
而这个可执行文件显然它应该存在磁盘上面,显然在磁盘上面会有
很多很多的可执行文件,每个可执行文件里面都包含相同的这些库函数的代码。
那么这些相同的库函数,在不同的可执行文件 当中,就会产生磁盘空间的极大的浪费。
第三个缺点是,在采用静态链接的情况下,
静态的库函数必须要把它合并到可执行目标文件当中。
如果静态库函数修改了,有新的版本出现。
这个新的版本呢必须把它更新到可执行文件当中去。
因此,这个新版本必须定期的去下载。
然后要重新编译,然后重新链接,因此呢,静态的这种链接方式
更新非常困难,使用也不是很方便,它不能够自动进行更新。
那么针对于这三个缺点,后来人们就想出来
用另外的一种的方式,称为共享库的方式。
共享库在Linux里面称为动态共享对象,
也就是so文件,就是扩展文件名为 so的这样的这个文件,叫动态共享对象。
在Windows里面, 它称为动态链接库,就是
扩展名为dll的文件,叫动态链接库。
共享库实际上是包含很多目标模块的,实际上是包含很多
点o模块的,这些点o模块合并起来,形成一个共享
库文件,每一个点o模块,当然都含有代码和数据部分,跟静态链接是一样的。
只不过共享库它把公共的 所有程序可以调用的这些共享的代码
从程序当中分离出来,比如说printf这个代码。
它呢就不包含在调用它的那些程序当中。
从这些调用它的程序当中分离出来,专门存放
在一个共享库文件当中,使得printf这样的代码,
以及它操作的这个数据,在磁盘上只有一份,就在这个文件里面,在内存里面
也只有一个备份,然后呢,所有的进程 共享来调用这一个备份的代码。
这些在磁盘上面的共享库文件,可以在装入
某个程序的时候,或者某个程序运行过程当中 动态的加载进去,也就是说,在某
一个调用printf函数的程序, 装入执行或者运行过程当中来动态的加载
printf这个模块,这个是共享库的基本的概念。
这个共享库的链接可以是两种方式,第一种方式是
第一次加载的时候,就是load的时候,进行动态链接。
还一种呢是已经加载以后,在运行过程当中
进行链接,这是两种方式,如果是第一种方式,在加载运行
的时候,进行链接的话,通常是由一个动态链接器
自动处理的,Linux里面的动态连接器就是这个。
它本身也是一个共享库,本身也是一个so文件。
比如说标准C库对应的共享库,就是libc.so。
前面我们讲静态链接的时候讲过,标准C库它的名称是libc.a,
是个点a文件,现在呢共享库呢,它就是一个点so文件。
那么像这样的标准C库,通常是通过
这种方式被链接的,就是通过加载的时候被动态链接的。
有些情况下我们可以,运行的时候进行链接,那么运行
的时候链接呢,需要调用相应的接口函数来实现。
这种链接方式,在很多应用场合下都很有用。
比如说,我们分发一个软件包的时候,我们可以用
这种运行时链接的方式,或者构建高性能的web服务器,也是采用的这种方式。
共享库我们刚才讲过了,它在内存 当中只有一个备份,可以被所有的进程共享。
所以它可以节省内存空间,然后 这些共享的模块,像printf.o模块呀,
还有其它的这些共享的模块,它都是打包在共享库文件里面的。
而这个共享库文件在磁盘当中只有一个备份,比如说libc.so。
在磁盘当中它只要有一个这个文件就行了。
这一个文件可以被其它的所有的程序 共享链接,因此它节省了磁盘空间。
第三个共享库升级的时候 可以自动的加载到内存,并且和程序进行动态
的链接,所以不需要程序员去显式地去
更新这个库,并且显式地重新进行链接。
它自动的 更新了以后,它就在libc.so,比如说在这个库文件里面某个模块进行了更新。
那么下次动态链接的时候,直接链接的就是一个最新的库文件。
此外呢,有了共享库的概念以后呢,这些共享库我们可以把它分成很多模块。
然后每个共享库可以各自独立的,并且可以用不同的编程语言进行开发。
就是用不同的编程语言开发的这些共享库, 可以被动态的进行链接。
此外,第三方可以开发共享库。
这个共享库可以作为程序插件,这样的话程序功能的 扩展就很容易了,这是共享库的一些这个好处。
所以现在我们开发程序的时候,默认的方式都 是采用的这种动态链接共享库的这种方式。
因为动态链接,链接的都是共享库。
静态链接,链接的是静态库。
我们下面举一个例子,这个例子在前面我们讲静态链接的时候也讲过,有一个
myproc1.c文件和myproc2.c这两个文件。
这两个文件当中分别定义了两个函数,myfunc1这个函数和myfunc2这个函数,
都是调用了printf函数打印一串字符。
打印一串字符,前面我们讲的静态库生成的 时候,是要用ar这种归档程序来实现的。
现在我们要生成一个动态的库。
生成动态共享库之前,我们先也要对这两个 点c文件进行相应的预处理、
编译、 汇编 三个阶段,然后分别生成myproc1.o和myproc2.o这两个
可重定位文件,然后生成这两个可重定位文件以后,就可以用
这样一个命令,来把这两个文件打包生成一个
共享库文件mylib.so,这是一个共享库文件。
这个共享库文件给他的一个选项是,这个选项
当中指出,它是一种位置无关代码。
也就是说我们要把这两个点o文件打包生成这个共享库文件。
生成的这个代码呢,是一种位置无关代码。
所谓位置无关代码,并且是共享代码库share 是共享的。
所谓位置无关,是指的共享库代码的位置可以
不确定,也就是说我们生成的这个共享库,它被这个程序调用的时候,它可能装到这个位置。
被另外一个程序调用的时候,可能装到另外一位置,也就是共享库代码 它的位置可以是不确定的,所以是和位置无关。
第二个呢,即使这个共享库代码长度发生了改变,整个代码
里面相应的地方改变,那么调用它的程序也不要做任何修改。
不会影响调用它的程序,这个也就是说我们刚才前面讲过的,如果共享
库代码做了修改,那么调用它的程序根本可以不管。
下次启动这个程序,然后进行动态链接的时候,链接的是个
新的共享库,这个新的共享库能够自动地进行链接。
这样的话,这个程序调用的是一个修改过的、 新的一个长度,
进行了变化的共享库代码,也就是说,我们要让生成的这个共享库代码
应该具有这种特点,不会因为自己的长度发生变化,而影响
调用它的这个程序,满足这两个条件的这种代码就是位置无关的代码。
刚才我们讲的那个例子,如果我们已经生成了一个mylib.so。
然后呢,我们现在编写了一个程序main.c,在这个里面呢,调用了
这个共享库里面,就是mylib.so这个里面的一个函数
myfunc1(),这时候我们只要用这个命令行 来进行可执行文件的生成。
就是把main.o,也就是这个main.c对应的这个,通过这个命令
生成的main.o这个可重定位目标文件, 和mylib.so进行链接。
最终生成myproc这个可执行文件。
那么,很显然这个地方,它的链接的 库.so是共享库,所以它不是采用的静态链接。
它是采用的一种动态链接方式,这种动态链接就是
这个可执行文件,在加载的时候进行动态链接的。
这样一种方式,那很显然这个里面的调用关系是 main函数里面调用了myfunc1。
然后myfunc1呢又调用了printf函数,是这样的关系,所以我们来看
一看,这种加载实际动态链接的这个过程应该是什么样子的。
首先,用这个命令生成可执行文件myproc,这个过程是
一个静态链接的过程,就是在这一块里里面进行静态链接的过程。
把main.o和mylib.so进行静态链接。
因为这个里面实际上还调用了printf,而printf呢是在
libc.so这个共享库里面的。
所以执行这条命令的时候。
进行的是静态链接,静态链接会把main.o
和这两个共享库当中的重定位信息 和符号表信息进行静态链接。
生成一个可执行文件叫 myproc,然后这个可执行文件不是一个完全
链接的可执行文件,只是部分链接的可执行文件。
因为这个地方不是点a文件,是点so文件。
因为它是点so文件,所以这个地方并不会把这个printf
的对应的代码链接到这个可执行文件里面。
只是把这些从定位信息和符号表信息加到这个可执行文件当中,加载这个可执行文件
将要执行的时候,通过这个函数调用,这个函数调用会去调用一个加
载器,那么这个加载器实际上最终会去调用一个动态链接器。
这个动态链接器会把这个可执行文件和这两个so文件当中
的myproc1.o里面的代码和printf.o里面的代码
以及数据和这个进行链接。
动态链接器链接生成的重定位以后的这些代码, 实际上是放在存储空间的,它不会再放到
磁盘上去,只有它是在磁盘当中的。
所以这个动态连接器生成的是在存储空间当中的
一个完全链接的可执行目标,也就是说生成的这些代码当中,重定位的
地方已经完全进行了重定位了,是一个完全 链接的,所有的该重定位的地方已经进行了重定位。
这时候一条一条指令执行就没有问题了,这些指令 当然都是直接在存储器当中的,而不是在磁盘当中的。
所以我们可以看到,加载可执行文件的时候,实际上这个加载
器会从这个可执行文件的程序头表中。
这个可执行文件有一个程序头表,这个程序头表中它会发现有这么
一个段,这个段里面指明了动态链接器的路径名。
所以这个加载器会把这个动态链接器装载进来以后,
转到动态链接器去执行,这个段我们来看一下。
在这个里面有一个特殊的段,就是这个段,这个段里面指出它的
动态链接器的路径名是这个,在这个子目录下的这样的一个文件,
就是动态链接器,所以有了它以后,就可以把控制权交给动态链接器。
然后动态链接器呢,在完成重定位工作以后再把
控制权交给它,然后就把这个可执行文件当中的
main函数,就第一条指令,其中第一条指令执行。
然后,就可以把整个这个程序执行完了,这个是有关 加载的时候动态链接的过程。
这是在存储空间当中的一个完全链接的目标。
第二种链接方式是运行时的动态链接。
运行时的动态链接是通过一个动态链接器的接口
函数来进行的,比如说类UNIX系统当中
有很多的函数,这些函数称为 动态链接器接口。
这个文件有个头文件是它,因此我们对于刚才的那个例子,我们可以
写这么一个main函数,在前面先要把这个头文件 include进来,如果是运行时动态链接的话,
我们先要把这个mylib.so动态的 加载进来,就是先要把这个文件打开。
如果打开没有问题的话,就可以用打开的这个文件当中
找到myfunc1这个函数,实际上这是一个符号,在打开的这个
共享库文件找到一个符号,叫myfunc1的这个符号,实际上是一个函数。
找到指向这个函数的首地址,也就实际上是个指针,指向这个函数的指针。
赋给这个变量,这个是一个函数指针。
然后获得这个指针的这个过程,如果有问题就显示错误,然后就跳出来。
如果没有任何问题,就可以直接使用这个函数,使用完了以后进行关闭。
把这个共享库进行关闭,如果关闭有错误,那就是跳出,如果没有错误
就返回,就可以通过这样的一种函数调用的方式进行
运行时动态的链接。
[音乐] [音乐]