欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

程序的动态链接(1):简介

程序员文章站 2024-03-25 11:54:10
...

概述

在动态链接出现之前,可执行文件的生成都是使用静态链接的方式来生成。静态链接方式需要在程序运行前将所有的可重定位文件全部链接到一起,形成一个整体后才能加载到内存中运行,这种机制存在一些弊端:

  • 空间浪费:程序在使用静态库的时候需要将使用到的公用库函数的目标文件全部链接到程序中,这就导致不同程序会存在相同库文件的多个副本,造成空间浪费;
  • 模块更新困难:对于程序中任何参与链接的可重定位文件发生更新,整个程序都需要重新进行编译和发布。

为了解决静态链接的问题,动态链接不再从一开始就将程序的所有模块静态链接在一起,而是等到程序运行时才进行链接。

动态链接实现

动态链接的基本思想是将程序中的部分独立模块作为动态共享库实现,在链接生成可执行文件时,动态库只提供重定位和符号信息,而不实际参与链接;等到程序运行时,由动态链接器将程序依赖的动态共享库加载到进程的虚拟地址空间中,形成一个完整的程序后执行。动态链接的基本实现过程示意如下:
程序的动态链接(1):简介
动态链接的核心工作由动态链接器完成,在Linux平台下,使用的动态链接器一般为ld-linux.so。动态链接器需要完成的任务一般包括:

  • 动态链接器自举:动态链接器自身也是一个动态共享库文件,在加载动态链接的可执行文件时,必须要先加载动态链接器,并由动态链接器完成自身的初始化,即自举;
  • 加载动态库:可执行文件中记录了依赖的符号信息,动态链接器会根据依赖信息,读取包含依赖符号的动态库文件,并映射动态库的代码段和数据段到进程地址空间中;
  • 重定位和初始化:动态链接器遍历可执行文件和所有共享对象的重定位表,然后依据记录在GOT/PLT表中的重定位信息,对外部符号的引用进行修正;在完成重定位后,动态链接器调用动态库中.init中代码,以实现动态库特有的初始化流程。

动态共享库

从动态链接器的几个主要任务中可以看到,动态链接器大部分的工作都是在处理动态共享库。动态共享库是一个目标文件,在运行时,可以被加载到任意的地址,并与一个在内存中的程序进行链接。动态共享库包含两个关键的特性:

  • 动态共享库的指令部分是在多个进程之间共享的,但数据是进程独立的,即数据在每个进程中拥有独立的副本;
  • 动态共享库的最终加载地址在编译时是不确定的,而是等到加载时,由加载器从当前进程的虚拟地址空间中动态分配。

在Linux平台下,动态共享库主要使用ELF文件格式进行存储,一般以.so为后缀名,这里可以查看系统提供的C运行时动态库:
程序的动态链接(1):简介

动态库的创建

为了对动态库有个更直观的认识,这里使用下面的代码说明在Linux平台下如何创建和使用动态库库:
程序的动态链接(1):简介
使用gcc将addvec.csubvec.c编译成一个动态共享库文件,操作如下:
程序的动态链接(1):简介
其中:

  • -shared选项:指定链接器创建一个动态共享库文件;
  • -fPIC选项:指定编译器生成位置无关的代码。

动态库的使用

为了使用生成的动态库,我们在代码里调用libvector.so提供的外部接口:

#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;
}

可以使用gcc编译main.c并指定链接libvector.so文件:
程序的动态链接(1):简介
其中:

  • -L选项:指定动态库的路径;
  • -l选项:指定需要链接的动态库名。

动态链接机制将整个程序被分成两个部分:可执行文件以及程序所依赖的动态共享库。在编译生成可执行文件时,链接器只会复制动态库中的重定位和符号表信息,而对于动态库的任何代码和数据节都不会复制到可执行文件中。
在实际运行程序时,动态链接器会依据可执行文件中记录的信息,加载动态库libvector.so文件,并在内存中完成动态链接;最后,动态链接器将控制转移给应用程序。

参考资料

  • 《程序员的自我修养-链接、装载与库》
  • 《深入理解计算机系统》