0

0

Linux 动态链接与静态链接原来是这么回事?

WBOY

WBOY

发布时间:2024-02-05 17:45:02

|

1243人浏览过

|

来源于良许Linux教程网

转载

老规矩,先提出几个问题:

  • 为什么要进行动态链接?
  • 如何进行动态链接?
  • 什么是地址无关代码技术?
  • 什么是延迟绑定技术?
  • 如何在程序运行过程中进行显式链接?

为什么要进行动态链接?

动态链接的出现是为了解决静态链接的一些缺点:

  1. 节约内存和磁盘空间:如下图所示,
Linux 动态链接与静态链接原来是这么回事?

Program1和Program2分别包含Program1.o和Program2.o两个模块,他们都需要Lib.o模块。静态链接情况下,两个目标文件都用到Lib.o这个模块,所以它们同时在链接输出的可执行文件Program1和program2中有副本,同时运行时,Lib.o在磁盘和内存中有两份副本,当系统中有大量类似Lib.o的多个程序共享目标文件时,就会浪费很大空间。

  1. 静态链接对程序的更新部署和发布很不友好

假如一个模块依赖20个模块,当20个模块其中有一个模块需要更新时,需要将所有的模块都找出来重新编译出一个可执行程序才可以更新成功,每次更新任何一个模块,用户就需要重新获得一个非常大的程序,程序如果使用静态链接,那么通过网络来更新程序也会非常不便,一旦程序任何位置有一个小改动,都会导致整个程序重新下载。

为了解决静态链接的缺点,所以引入了动态链接,动态链接的内存分布如图,

Linux 动态链接与静态链接原来是这么回事?

多个程序依赖同一个共享目标文件,这个共享目标文件在磁盘和内存中仅有一份,不会产生副本,简单来讲就是不像静态链接一样对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接,把链接这个过程推迟到运行时才执行。动态链接的方式使得开发过程中各个模块更加独立,耦合度更小,便于不同的开发者和开发组织之间独立的进行开发和测试。

如何进行动态链接?

看如下代码:

// lib.c
#include 

void func(int i) {
   printf("func %d \n", i);
}
// Program.c
void func(int i);

int main() {
   func(1);
   return 0;
}

编译运行过程如下:

$ gcc -fPIC -shared -o lib.so lib.c
$ gcc -o test Program.c ./lib.so
$ ./test
$ func 1

通过-fPIC和-shared可以生成一个动态链接库,再链接到可执行程序就可以正常运行。

通过readelf命令可以查看动态链接库的segment信息:

~/test$ readelf -l lib.so

Elf file type is DYN (Shared object file)
Entry point 0x530
There are 7 program headers, starting at offset 64

Program Headers:
 Type           Offset             VirtAddr           PhysAddr
                FileSiz            MemSiz              Flags  Align
 LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                0x00000000000006e4 0x00000000000006e4  R E    0x200000
 LOAD           0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
                0x0000000000000218 0x0000000000000220  RW     0x200000
 DYNAMIC        0x0000000000000e20 0x0000000000200e20 0x0000000000200e20
                0x00000000000001c0 0x00000000000001c0  RW     0x8
 NOTE           0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
                0x0000000000000024 0x0000000000000024  R      0x4
 GNU_EH_FRAME   0x0000000000000644 0x0000000000000644 0x0000000000000644
                0x0000000000000024 0x0000000000000024  R      0x4
 GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                0x0000000000000000 0x0000000000000000  RW     0x10
 GNU_RELRO      0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
                0x00000000000001f0 0x00000000000001f0  R      0x1

Section to Segment mapping:
 Segment Sections...
  00     .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
  01     .init_array .fini_array .dynamic .got .got.plt .data .bss
  02     .dynamic
  03     .note.gnu.build-id
  04     .eh_frame_hdr
  05
  06     .init_array .fini_array .dynamic .got

可以看见动态链接模块的装载地址从0开始,0是无效地址,它的装载地址会在程序运行时再确定,在编译时是不确定的。

改一下程序:

// Program.c
#include 
void func(int i);

int main() {
   func(1);
   sleep(-1);
   return 0;
}

运行读取maps信息:

~/test$ ./test &
[1] 126
~/test$ func 1
cat /proc/126/maps
7ff2c59f0000-7ff2c5bd7000 r-xp 00000000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5bd7000-7ff2c5be0000 ---p 001e7000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5be0000-7ff2c5dd7000 ---p 000001f0 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5dd7000-7ff2c5ddb000 r--p 001e7000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5ddb000-7ff2c5ddd000 rw-p 001eb000 00:00 516391             /lib/x86_64-linux-gnu/libc-2.27.so
7ff2c5ddd000-7ff2c5de1000 rw-p 00000000 00:00 0
7ff2c5df0000-7ff2c5df1000 r-xp 00000000 00:00 189022             /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5df1000-7ff2c5df2000 ---p 00001000 00:00 189022             /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5df2000-7ff2c5ff0000 ---p 00000002 00:00 189022             /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5ff0000-7ff2c5ff1000 r--p 00000000 00:00 189022             /mnt/d/wzq/wzq/util/test/lib.so
7ff2c5ff1000-7ff2c5ff2000 rw-p 00001000 00:00 189022             /mnt/d/wzq/wzq/util/test/lib.so
7ff2c6000000-7ff2c6026000 r-xp 00000000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6026000-7ff2c6027000 r-xp 00026000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6227000-7ff2c6228000 r--p 00027000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6228000-7ff2c6229000 rw-p 00028000 00:00 516353             /lib/x86_64-linux-gnu/ld-2.27.so
7ff2c6229000-7ff2c622a000 rw-p 00000000 00:00 0
7ff2c62e0000-7ff2c62e3000 rw-p 00000000 00:00 0
7ff2c62f0000-7ff2c62f2000 rw-p 00000000 00:00 0
7ff2c6400000-7ff2c6401000 r-xp 00000000 00:00 189023             /mnt/d/wzq/wzq/util/test/test
7ff2c6600000-7ff2c6601000 r--p 00000000 00:00 189023             /mnt/d/wzq/wzq/util/test/test
7ff2c6601000-7ff2c6602000 rw-p 00001000 00:00 189023             /mnt/d/wzq/wzq/util/test/test
7fffee96f000-7fffee990000 rw-p 00000000 00:00 0                 [heap]
7ffff6417000-7ffff6c17000 rw-p 00000000 00:00 0                 [stack]
7ffff729d000-7ffff729e000 r-xp 00000000 00:00 0                 [vdso]

可以看到,整个进程虚拟地址空间中,多出了几个文件的映射,lib.so和test一样,它们都是被操作系统用同样的方法映射到进程的虚拟地址空间,只是它们占据的虚拟地址和长度不同.

从maps里可以看见里面还有libc-2.27.so,这是C语言运行库,还有一个ld-2.27.so,这是Linux下的动态链接器,动态链接器和普通共享对象一样被映射到进程的地址空间,在系统开始运行test前,会先把控制权交给动态链接器,动态链接器完成所有的动态链接工作后会把控制权交给test,然后执行test程序。

当链接器将Program.o链接成可执行文件时,这时候链接器必须确定目标文件中所引用的func函数的性质,如果是一个定义于其它静态目标文件中的函数,那么链接器将会按照静态链接的规则,将Program.o的func函数地址进行重定位,如果func是一个定义在某个动态链接共享对象中的函数,那么链接器将会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,将这个过程留在装载时再进行。

动态链接的方式

动态链接有两种方式:装载时重定位和地址无关代码技术。

装载时重定位:

在链接时对所有绝对地址的引用不作重定位,而把这一步推迟到装载时完成,也叫基址重置,每个指令和数据相当于模块装载地址是固定的,系统会分配足够大的空间给装载模块,当装载地址确定后,那指令和数据地址自然也就确定了。

然而动态链接模块被装载映射到虚拟空间,指令被重定位后对于每个进程来讲是不同的,没有办法做到同一份指令被多个进程共享,所以指令对不同的进程来说有不同的副本,还是空间浪费,怎么解决这个问题?使用fPIC方法。

地址无关代码:

指令部分无法在多个进程之间共享,不能节省内存,所以引入了地址无关代码的技术。我们平时编程过程中可能都见过-fPIC的编译选项,这个就代表使用了地址无关代码技术来实现真正的动态链接。

电子黄页系统
电子黄页系统

本程序本着开源分享的原则向广大网友提供下载,程序和数据很多是来自网上,本人不负责版权责任,仅仅大家学习参与,如用于商业作用,本人概不负责。电子黄页系统功能说明:1、 网址收录,自动查询pr值,百度收录,goolge收录,icp备案,排名等信息。2、 按城市按行业分类的企业黄页展示。3、 新闻发布,链接管理,会员管理。4、 静态生成新闻和页面。5、 自带6000条企业数据库。6、 界面风格为蓝色模板

下载

基本思想就是使用GOT(全局偏移表),这是一个指向变量或函数地址的指针数组,当指令要访问变量或者调用函数时,会去GOT中找到相应的地址进行间接跳转访问,每个变量或函数都对应一个地址,链接器在装载模块的时候会查找每个变量和函数的地址,然后填充GOT中的各个项,确保每个指针指向的地址正确。GOT放在数据段,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。

tips

 

-fpic和-fPIC的区别:它们都是地址无关代码技术,-fpic产生的代码相对较小较快,但是在某些平台会有些限制,所以大多数情况下都是用-fPIC来产生地址无关代码。

 

-fPIC和-fPIE的区别:一个作用于共享对象,一个作用于可执行文件,一个以地址无关方式编译的可执行文件被称作地址无关可执行文件。

 

-fpie和-fPIE的区别:类似于-fpic和-fPIC的区别

延迟绑定技术

在程序刚启动时动态链接器会寻找并装载所需要的共享对象,然后进行符号地址寻址重定位等工作,这些工作会减慢程序的启动速度,如果解决?

使用PLT延迟绑定技术,这里会单独有一个叫.PLT的段,ELF将 GOT拆分成两个表.GOT和.GOT.PLT,其中.GOT用来保存全局变量的引用地址,.GOT.PLT用来保存外部函数的地址,每个外部函数在PLT中都有一个对应项,在初始化时不会绑定,而是在函数第一次被用到时才进行绑定,将函数真实地址与对应表项进行绑定,之后就可以进行间接跳转。

显式运行时链接

支持动态链接的系统往往都支持显式运行时链接,也叫运行时加载,让程序自己在运行时控制加载的模块,在需要时加载需要的模块,在不需要时将其卸载。这种运行时加载方式使得程序的模块组织变得很灵活,可以用来实现一些诸如插件、驱动等功能。

通过这四个API可以进行显式运行时链接:

dlopen():打开动态链接库
dlsym():查找符号
dlerror():错误处理
dlclose():关闭动态链接库

参考这段使用代码:

#include 
#include 

int main() {
   
   void *handle;
   void (*f)(int);
   char *error;

   handle = dlopen("./lib.so", RTLD_NOW);
   if (handle == NULL) {
       printf("handle null \n");
       return -1;
  }
   f = dlsym(handle, "func");
   do {
       if ((error = dlerror()) != NULL) {
           printf("error\n");
           break;
      }
       f(100);
  } while (0);
   dlclose(handle);

   return 0;
}

编译运行:

$ gcc -o test program.c -ldl
$ ./test
func 100

总结

为什么要进行动态链接?为了解决静态链接浪费空间和更新困难的缺点。

动态链接的方式?装载时重定位和地址无关代码技术。

地址无关代码技术原理?通过GOT段实现间接跳转。

延迟加载技术原理?对外部函数符号通过PLT段实现延迟绑定及间接跳转。

如果进行显式运行时链接?通过头文件中的四个函数,代码如上。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

379

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

608

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

348

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

255

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

585

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

519

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

632

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

595

2023.09.22

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 6.4万人学习

Git 教程
Git 教程

共21课时 | 2.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号