程序的动态链接(1):概述

前言

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

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

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

动态链接实现

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

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

动态共享库

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

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

在Linux平台下,动态共享库主要使用ELF文件格式进行存储,一般以.so为后缀名,这里可以查看系统提供的C运行时动态库:
在这里插入图片描述

编译Hello World

Hello World程序是最简单的一个使用动态共享库的例子:

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("Hello World!\n");

    return 0;
}

默认情况下,当我们编译一个Hello World程序时,编译器以动态链接的方式将程序依赖的库加入进来,在Linux下可以查看生成Hello World可执行程序依赖的动态库信息:
在这里插入图片描述
动态链接机制将整个程序被分成两个部分:可执行文件以及程序所依赖的动态共享库。在编译生成可执行文件时,链接器只会复制动态库中的重定位和符号表信息,而对于动态库的任何代码和数据节都不会复制到可执行文件中。在实际运行程序时,动态链接器会依据可执行文件中记录的信息,加载依赖的动态库文件,并在内存中完成动态链接;最后,动态链接器将控制转移给应用程序。

参考资料

  • 《程序员的自我修养-链接、装载与库》
  • 《深入理解计算机系统》
posted @ 2020-08-02 20:57  Aspiresky  阅读(53)  评论(0编辑  收藏  举报