动态链接:共享库如何实现“一次编译,多程序调用执行”?

0.简介

在上一篇文章中我们对静态链接进行了介绍,明确了原理和优缺点,其中较为明显的缺点就是程序体积变大,更新困难。本节将对动态链接原理和使用进行介绍,说明其如何克服静态链接的问题以及如何做到“一次编译,多程序共享代码段执行”。

1.动态链接整体介绍

要理解动态链接,可以使用静态链接作为参考,静态链接是在可执行程序生成过程中进行的文件整合,符号解析以及重定位,而动态链接则是把链接过程推迟到了运行时,而在执行时进行链接需要解决的核心问题:如何知道那些符号静态链接,那些符号动态链接?动态库如何映射到进程地址空间以及如何重定位?全局变量如何生成副本?

我们逐个来看这些问题的解决方案,通过实际例子来进行演示说明,首先程序在编译时如何知道那些符号应该动态链接,那些符号需要静态链接?因为动态库也有符号表,所以链接器可以通过这个符号表来进行定位,确定其动态链接还是静态链接;接下来要解决的问题就是如何映射到进程地址并且重定位那?如果简单考虑,我们可以直接使用相对于基地址的偏移,将整个动态库映射到程序的某一块地址,然后将其内部操作数据和访问数据的指令都加上这个偏移,但是这样的话就无法共享指令集了(也就是编译不加-fPIC的编译和链接方式),而共享指令集这个是通过地址无关代码(PIC)来实现的(这个在下面会详细介绍);最后就是动态库中变量如何处理?这个是通过副本方式来进行处理的,也就是指令共享但数据是私有的,这个后面也会详细介绍。

本文下面使用的例子代码如下:

//lib.h
#ifndef LIB_H
#define LIB_H
void dynamic(int n);
#endif


//lib.c
#include "lib.h"
#include <stdio.h>
int a = 11;
void dynamic(int n)
{
    printf("dynamic %d\n", n);
}


//test1.c
#include "lib.h"
extern int a;
int main()
{
    dynamic(a);
    return 0;
}

2.地址无关代码

我们可以想象,如果自己是链接器的设计者要解决地址映射问题会怎么做,首先想到的可能就是要分离变和不变的部分,不变的部分可以共享,变的部分需要私有,链接器也正是这么做的,把数据以及需要修改的指令部分(如数据访问地址)都放在数据部分,让每个进程一个副本,而把不变部分都放到指令部分,多进程共享。

分离了变化和不变,接下来就是考虑如何让指令部分可以和地址无关了,遇到这种问题,最直接的思路就是首先确认地址访问的类型和找到现有有哪些确定的可用信息,我们分类型来看:

1)模块内部的函数调用,跳转(直接使用相对位置):模块内部的调用其相对位置固定,都可以直接采用相对位置或者使用寄存器来进行相对调用。

2)模块内部的数据访问(直接使用相对位置):也是通过相对位置,通过指令获取当前pc(指令地址)值,然后加上偏移量就可以得到数据地址。

3)模块间的数据访问(通过中间层访问):模块间的数据访问要做到地址无关我们就不能再考虑修改指令中的地址了,而是使用中间层的方式,我们在数据段建立一个GOT(global offset table),因为数据段每个进程都会自己有一个副本,所以每个GOT都可以装载时动态填入,其中记录变量的目标地址,这样我们就能访问这个GOT间接获取地址,指令部分就做到了地址无关。

4)模块间的函数调用、跳转(通过中间层间接访问):和数据访问类似,通过记录地址到GOT,只不过是got.plt段,只不过存储的是函数地址,我们可以看一下例子。

#编译生成a.out的可执行程序
gcc -fPIC -shared -o lib.so lib.c
gcc -o a.out test1.c ./lib.so
#查看GOT位置
objdump -h lib.so
#查看需要重定位的项
objdump -R a.out

3.数据段的地址无关性(如何复制)

了解了代码的地址无关性,接下来要保证的就是数据的地址无关性了,其实在上面的例子中对应的就是a,如果a当成普通变量,就会直接使用地址,所以链接器会给a创建一个副本,GOT就指向这个副本,所以我们看到的类型是R_X86_64_COPY。

4.绑定优化技术(延迟绑定PLT)

动态绑定中,程序不同模块间会有大量的函数调用,所以会有很多的重定位工作,者会大大降低程序启动速度,所以ELF使用到了一种延迟绑定的做法,当函数第一次使用时才进行符号查找和重定位。

5.关键段和链接流程

5.1 .interp段

interp段用来记录动态链接器的路径,objdump -s a.out。

5.2 .dynamic段

存储动态链接有关的信息,比如got段位置信息,readelf -d lib.so。

5.3 链接流程

通过这些段信息不难想象,其整体流程应该为根据interp启动链接器,然后装载共享对象,然后是重定位。

6.显示链接

了解了动态链接的原理,本节来讲述一个更为灵活的模块加载方式,其被称为显式运行时链接,可以让程序员自己在运行是控制加载的模块,很多插件机制就是这么做的,可以在启动时根据配置文件动态的调用像dlopen,dlclose等来加载库,然后调用指定函数去完成初始化工作,其常用函数有:dlopen、dlsym、dlerror、dlclose,此处不再详细介绍。

7.总结

本文对动态链接原理进行了讲解,说明了动态链接是如何克服静态链接问题,做到“一次编译,多程序共享”的,接下来文章会从实际出发,演示各类问题解决方案。

相关文章

cython如何调用C语言的函数?_c 中如何调用python

在 Cython 中调用 C 语言函数主要通过以下几种方式实现:1. 使用 cdef extern 声明外部 C 函数基本语法cdef extern from "头文件.h":返回类型...

C/C++函数调用的奥秘_c++函数调用原理

在C/C++编程的世界里,函数调用是程序运行的核心机制之一。然而,许多程序员在日常开发中,往往只关注代码的逻辑,而忽略了函数调用背后的底层细节。今天,就让我们一起深入探索C/C++函数调用的全过程,从...

C++成员函数如何工作?this指针、name mangling 成员函数指针解析

0.引言 在C++面向对象编程中,成员函数是对象行为的核心载体。我们每天都在使用成员函数,但却很少深入思考其底层的实现机制:为什么成员函数可以直接访问成员变量?编译器如何区分不同类的同名函数?静态成员...

C语言入门:学生成绩管理程序的完善(1):用文件保存数据

这是C语言入门的第27篇文章。今天讲学生成绩管理程序的完善:怎样利用文件来保存数据。还是昨天的问题:我怎么知道一个文件的内容是什么?我怎么知道比如一行有多少个数,多少个数以后是换行?这是因为文件是我们...

C语言应用笔记:常用的printf打印输出不同类型数据

我叫程序员阿虾, 在终端前摸过太多凌晨, 熟悉printf这一行字带来的安心与危险。今天想跟你聊聊我踩过的坑, 和一些别人不常说的细节, 用第一人称把经验交给你, 有点唠叨, 希望你少走弯路。为什么要...

C语言应用笔记:简单的最大最小值比较

使用宏定义实现泛型比较函数,用于求取两个值的最大值和最小值。核心宏定义解析#define MAX(x, y) ((x) > (y) ? (x) : (y)) // 返回两个值中较大的一个 #de...