博客简介
本篇博客是CSAPP链接章节的库部分总结,目录如下:
- 链接静态库
- 链接动态库
如何打包编程者经常使用的功能函数? 在静态库提出之前有2种方法:
-
Option 1: 将所有函数写入一个源代码文件
-
编程者需要将一个很大的目标文件链接到其程序
-
空间和时间性能都低
-
Option 2: 每一个函数形成一个单独的源文件
-
编程者需将自己的程序显示地链接到相应的二进制文 件
-
更有效,但对于编程者是额外负担
以上两种库处理方式并不能很好处理库链接
链接静态库
- 将所有相关的目标模块打包成为 一个单独的文件,称为静态库 (static library) , 它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。
- 相关的函数可以被编译为独立的目标模 块,然后封装成一个单独的静态库文件。然后,应用程序可以通过在命令行上指定单独的文件名 字来使用这些在库中定义的函数。比如,使用标准 C 库和数学库中函数的程序可以用形式如下 的命令行来编译和链接:
unix> gcc main.c /usr/lib/libm.a /usr/lib/libe.a
- 静态库以一种称为存档 (archive) 的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和 位置。存档文件名由后缀 .a 标识
- 增强了链接器,其可以通过在一个或多个存档中查找符号来解析外 部引用
- 若一个存档成员文件解析了引用,则可将其连接到可执行文件
libvector.a 静态库实例
- (1)假设我们想在一个叫 做 libvector.a 的静态库中提供向量例程:
- mulvec.c
/*mulvec.c*/
void mulvec(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++)
z[i]=x[i]*y[i];
}
- addvec.c
/* addvec.c*/
void addvec(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++)
z[i]=x[i]+y[i];
}
(2)使用AR工具创建一个库
linux> gcc -c addvec.c multvec.c
linux> ar rcs libvector.a addvec.o multvec.o
看到已经在目录下生成了一个libvector.a文件
(2)在头文件 vector.h 定义libvector.a 中例程的函数原型(一开始我没用这个也能正确编译)
/* vector.h */
void multvec(int *x,int *y,int *z,int n);
void addvec(int *x,int *y,int *z,int n);
(3)使用这个库,编写main2.c, 调用 addvec 库例程。
#include <stdio.h>
#include "vector.h"
int x[2]={1,2};
int y[2]={3,4};
int z[2];
int main()
{
addvec(x,y,z,2);
printf("z= [%d,%d]\n",z[0],z[1]);
return 0;
}
(4)编译和链接输入文件 main.o 和 libvector.a,执行指令:
gcc -O2 main2.c
gcc -static -o p2 main2.o ./libvector.a
- -static 参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并运行,在加载时无需更进一步的链接。
- 当链接器运行时,它判定 addvec.o 定义的 addvec 符号是被 main.o 引用的,所以它拷贝 addvec.o 到可执行文件。因为程序不引用任何由 multvec.o 定义的符号,所以链接器就不会拷贝这个模块到可执行文件。
- 链接器还会拷贝 libc.a 中的 printf.o 模块,以及许多 C 运行时系统中 的其他模块。链接库图形表示如下:
链接器如何使用静态库来解析引用
上述的静态库链接过程是怎样的?此处有一链接器使用静态库来解析引用的算法:
- 符号解析的阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。
- 维持集合:
① 可重定位目标文 件的集合E:(这个集合中的文件会被合并起来形成可执行文件)
② 一个未解析的符号(即引用了但是尚未定义的符号)集合 U
③ 在前面输入文件中已定义的符号集合D。
初始时, E、 U和 D 都是空的。
按照命令行顺序从左往右扫描,对于命令行上的每个输人文件f,如果f是一个:
- 目标文件:链接器把f添加到 E, 修改 U和 D来反映f中的符号定义和引用,next。
- 存档文件:链接器尝试匹配U中未解析的符号和由存档文件成员定义 的符号。如果某个存档文件成员 m, 定义了一个符号来解析 U 中的一个引用,那么就将 m 加到 E 中,并且链接器修改 U和 D来反映 m 中的符号定义和引用。对存档文件中所有的成员目标文件都反复进行这个过程,直到 U和 D都不再发生变化。任何不包含在 E 中的成员目标文件都简单地被丢弃,next。
- 当链接器完成对命令行上输入文件的扫描后, U是非空的,那么链接器就会输出一个 错误并终止。否则,合并和重定位E中的目标文件,从而构建输出的可执行文件。
静态库的缺陷:
- 文件输入顺序需要程序员特别注意
- 在存储中的可执行文件中有多个副本 (每一个函数均需要静态库文件 )
- 在运行中的可执行文件中存在多个副本
- 即便是对系统库进行小bug的修复,也需要对使用到这个库的所有应用显示地重新链接
动态(共享)链接库
共享库 (shared library) 是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模 块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程 称为动态链接 (dynamic linking), 是由一个叫做动态链接器 (dynamic linker) 的程序来执行的。
- 在 Unix 系统中通常用 .so 后缀来表示。
- Windows操作系统大量地利用了共享库,它们称为 DLL (动态链接库)
两种共享方式
- ① 在任何给定的文件系统中,对于一个库只有一个 .so 文件。所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌人到引用它们的可执行的文件中。
- ② 其次,在存储器中,一个共享 库的 .text 节的一个副本可以被不同的正在运行的进程共享。
创建共享库并完成链接
/*创建共享库libvector.so*/
gcc -shared -fPIC -o libvector.so addvec.c multvec.c
/*链接到程序中*/
unix> gee -o p2 main2.c ./libvector.so
- -fPIC 选项指示编译器生成与位置 无关的代码
- -shared 选项指示链接器创建一个共享的目标文件
这样就创建了一个可执行目标文件 p2, 而此文件的形式使得它在运行时可以和 libvector.so 链接。基本的思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。
- 当加载器加载和运行可执行文件 p2 时,加载部分链接的可执行文件 p2。接着,它注意到 p2 包含一个 .interp 节,这个节包含动态链接器的路径名,动态链接器本身就是一个共享目标(比如,在 Linux 系统上的 LO-LINUX.SO)。
- 加载器不再像它通 常那样将控制传递给应用,而是加载和运行这个动态链接器。
- 然后,动态链接器通过执行下面的重定位完成链接任务: ·重定位 libc.so 的文本和数据到某个存储器段。 ·重定位 libvector.so 的文本和数据到另一个存储器段。 ·重定位 p2中所有对由 libc.so 和 libvector.so 定义的符号的引用。
- 最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程 序执行的过程中都不会改变
运行时的动态链接
除了在运行前动态链接库,还可以用Linux 系统为动态链接器提的一个简单的接口,允许应用程序在运行时加载和链接共享库
① dlopen:函数加载和链接共享库 filename。用以前带 RTLD_GLOBAL 选项打开的库解析filename 中的外部符号。如果当前可执行文件是带 -rdynamic 选项编译的,那么对符号解析而言,它的全局符号也是可用的。 flag 参数必须要么包括 RTLD_NOW, 该标志告诉链接器立即 解析对外部符号的引用,要么包括 RTLD_LAZY 标志,该标志指示链接器推迟符号解析直到执行 来自库中的代码。这两个值中的任意一个都可以和 RTLD_GLOBAL 标志取或。
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
返回:若成功则为指向句柄的指针,若出错则为 N讥,L 。
② dlsym:函数的输入是一个指向前面已经打开共享库的句柄和一个符号名字,如果该符号存在, 就返回符号的地址,否则返回 NULL。
#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);
返回:若成功则为指向符号的指针,若出错则为 N口一L。
③ dlclose:如果没有其他共享库正在使用这个共享库, dlclose 函数就卸载该共享库。
#include <dlfcn.h>
int dlclose (void•handle);
返回:若成功则为 0, 若出错则为 -1 。
④ dlerror:函数返回一个字符串,它描述的是调用 dlopen 、 dlsym 或者 dlclose 函数时发生 的最近的错误,如果没有错误发生,就返回 NULL。
#include <dlfcn.h>
const cha工 *dlerror(void);
返回:如果前面对 dlopen,dlsym 或 dlclose 的调用失败, 则为错误消息,如果前面的调用成功,则为 NULL
一个运行链接例子:
#include <stdio.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main() {
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
/* dynamically load the shared lib that contains addvec() */
handle = dlopen("./libvector.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror()); exit(1);
}
/* get a pointer to the addvec() function we just loaded */
addvec = dlsym(handle, "addvec");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
/* Now we can call addvec() just like any other function */
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
/* unload the shared library */
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
- 要编译这个程序,我们将以下面的方式调用 GCC:
unix> gcc -rdynamic -O2 -o p3 dll.c -ldl