链接库的想法固然不错,但是共享库的解决方案才是现代的解决方案

静态链接库有以下缺点:

  • 在保存的可执行文件中存在重复 (每个函数都需要libc)
  • 在运行的可执行文件中重复
  • 系统库的小错误修复要求每个应用程序显式地重新链接
    也就是说,静态库是维护的是一个公共的函数库,我们需要用的时候也是相当于把它“手抄”到了自己的代码中
    比如说:几乎每个 C 程序都使用标准 I/O 函数,比如 printf 和 scanf。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行上百个进程的典型系统上,这将是对稀缺的内存系统资源的极大浪费

共享库解决这些问题

  • 包含代码和数据的目标文件,在它们的加载时或运行时,被动态地加载并链接到应用程序中
  • 也称: 动态链接库, DLLs, .so 文件
    共享库最关键的要点是:它可以在正在运行的可执行文件中热加载进去

共享库是以两种不同的方式来“共享”的

  • 当执行文件第一次加载和运行时(加载时链接),动态链接就会出现.
    • Linux 的常见情况是,由动态链接器(ld-linux.so)自动处理.
    • 标准 C 库 (libc.so)通常是动态链接的.
  • 动态链接也可以在程序启动后进行(运行时链接).
    • 在 Linux 中,这是通过调用 dlopen()接口完成的.
    • 分布式软件.
    • 高性能 web 服务器.
    • 运行时库打桩.
      首先,在任何给定的文件系统中,==对于一个库只有一个. so 文件==
      所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中
      其次,在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享

动态链接过程


这样就创建了一个可执行目标文件 prog2l,而此文件的形式使得它在运行时可以和 libvector.so 链接
基本的思路是当创建可执行文件时
静态执行一些链接 unix> gcc -shared -o libvector.so addvec.c multvec.c
然后在程序加载时,动态完成链接过程
认识到这一点是很重要的:此时,没有任何 libvector.so 的代码和数据节真的被复制到可执行文件 prog2l 中
反之,链接器复制了一些重定位和符号表信息,它们使得运行时可以解析对 libvector.so 中代码和数据的引用
当加载器加载和运行可执行文件 prog2l 时
它利用加载可执行文件中的方式,加载部分链接的可执行文件 prog2l
接着,它注意到 prog2l 包含一个 .interp 节,这一节包含动态链接器的路径名
动态链接器本身就是一个共享目标(如在 Linux 系统上的 ld-linux.so)
加载器不会像它通常所做地那样将控制传递给应用,而是加载和运行这个动态链接器(运行的是链接器)
然后,动态链接器通过执行下面的重定位完成链接任务

  • 重定位 libc.so 的文本和数据到某个内存段。
  • 重定位 libvector.so 的文本和数据到另一个内存段。
  • 重定位 prog2l 中所有对由 libc.so 和 libvector.so 定义的符号的引用。
    最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变

Linux 系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库

#include <dlfcn.h>
 
void *dlopen(const char *filename, int flag);
 
// 返回:若成功则为指向句柄的指针,若出错则为 NULL。

dlopen 函数加载和链接共享库 filenameo 用已用带 RTLD_GLOBAL 选项打开了的库解析 filename 中的外部符号。如果当前可执行文件是带 - rdynamic 选项编译的,那么对符号解析而言,它的全局符号也是可用的。flag 参数必须要么包括 RTLD_NOW,该标志告诉链接器立即解析对外部符号的引用,要么包括 RTLD_LAZY 标志,该标志指示链接器推迟符号解析直到执行来自库中的代码。这两个值中的任意一个都可以和 RTLD_GLOBAL 标志取或

#include <dlfcn.h>
 
void *dlsym(void *handle, char *symbol);
 
// 返回:若成功则为指向符号的指针,若出错则为 NULL。

dlsym 函数的输入是一个指向前面已经打开了的共享库的句柄和一个 symbol 名字,如果该符号存在,就返回符号的地址,否则返回 NULL

#include <dlfcn.h>
 
int dlclose (void *handle);
 
// 返回:若成功则为0,若出错则为-1.

如果没有其他共享库还在使用这个共享库,dlclose 函数就卸载该共享库

include <dlfcn.h>
 
const char *dlerror(void);
 
// 返回:如果前面对 dlopen、dlsym 或 dlclose 的调用失败,
// 则为错误消息,如果前面的调用成功,则为 NULL。

dlerror 函数返回一个字符串,它描述的是调用 dlopen、dlsym 或者 dlclose 函数时发生的最近的错误,如果没有错误发生,就返回 NULL。
下面是如何利用这个接口动态链接我们的 libvector.so 共享库,然后调用它的 addvec 例程。要编译这个程序,我们将以下面的方式调用 GCC:

linux> gcc -rdynamic -o prog2r dll.c -ldl

dll.c

#include <stdio.h>
#include <stdlib.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 library containing 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;
}