深入浅出计算机组成原理学习笔记:第十讲

一、为什么需要动态链接库

1、链接在生活中的应用

链接 其实有点像我们日常生活中的标准化、模块化生产、我们有一个可以生产标准螺帽的生产线,就可以生产很多个不同的螺帽,
只有需要螺帽,我们就可以通过链接的方式、去复制一个出来,放到需要的点,大道汽车、小到信箱

2、静态链接的缺点

但是、如我们有很多个程序都要通过装载器装载到内存的里面,那里面链接好的同样的功能代码,也需要再装载一遍、再占一遍内存空间。

这就好比,假设每个人有骑自行车的需求,那我们给每个人生产一辆自行车带在身边,固然大家都有自行车用,但是马路上肯定会特别拥挤

二、链接可以分动、静、共享运行升内存

1、内存不够用

2、链接过程

3、图解动态链接过程

三、地址无关很重要,相对地址解烦恼

1、地址无关

2、地址相关

3、动态共享库无法做到地址无关

 

四、PLT 和 GOT,动态链接的解决方案

1、示例代码

1、首先lib.h定义了动态链接库的一个函数show_me_the_money

1
2
3
4
5
6
7
[root@luoahong 10]# cat lib.h
#ifndef LIB_H
#define LIB_H
 
void show_me_the_money(int money);
 
#endif

2、lib.c包含了lib.h的实际实现

1
2
3
4
5
6
7
8
[root@luoahong 10]# cat lib.c
#include <stdio.h>
 
 
void show_me_the_money(int money)
{
    printf("Show me USD %d from lib.c \n", money);
}

3、然后show_me_poor.c调用了lib里面的函数

1
2
3
4
5
6
7
[root@luoahong 10]# cat show_me_poor.c
#include "lib.h"
int main()
{
    int money = 5;
    show_me_the_money(money);
}

4、最后,我们把lib.c变异成一个动态链接库,也就是.so文件

1
2
[root@luoahong 10]# gcc lib.c -fPIC -shared -o lib.so
[root@luoahong 10]# gcc -o show_me_poor show_me_poor.c ./lib.so

你可以看到,在编译的过程中,我们制定了一个-fPIC的参数。这个参数其实就是Position Independent Code 的意思,也就是我们要把这个编译成一个地址无关代码。

然后。我们再通过gcc编译show_me_poor动态链接了lib.so的可执行文件,在这些操作走完成了之后,我们把show_me_poor这个文件通过objdump出来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
……
0000000000400540 <show_me_the_money@plt-0x10>:
  400540:       ff 35 12 05 20 00       push   QWORD PTR [rip+0x200512]        # 600a58 <_GLOBAL_OFFSET_TABLE_+0x8>
  400546:       ff 25 14 05 20 00       jmp    QWORD PTR [rip+0x200514]        # 600a60 <_GLOBAL_OFFSET_TABLE_+0x10>
  40054c:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]
 
0000000000400550 <show_me_the_money@plt>:
  400550:       ff 25 12 05 20 00       jmp    QWORD PTR [rip+0x200512]        # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>
  400556:       68 00 00 00 00          push   0x0
  40055b:       e9 e0 ff ff ff          jmp    400540 <_init+0x28>
……
0000000000400676 <main>:
  400676:       55                      push   rbp
  400677:       48 89 e5                mov    rbp,rsp
  40067a:       48 83 ec 10             sub    rsp,0x10
  40067e:       c7 45 fc 05 00 00 00    mov    DWORD PTR [rbp-0x4],0x5
  400685:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  400688:       89 c7                   mov    edi,eax
  40068a:       e8 c1 fe ff ff          call   400550 <show_me_the_money@plt>
  40068f:       c9                      leave 
  400690:       c3                      ret   
  400691:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  400698:       00 00 00
  40069b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
……

完整代码

我们只关心整个可执行文件中的一部分内容。你应该可以看到,在main函数调用show_me_the_money的函数的时候,对应的代码是这样的:

1
call   400550 <show_me_the_money@plt>

这里后面一个@plt的关键字,代表了我们需要从PLT,也就是程序链接表里面找要挑用的函数,对应的地址则是400550这个地址

那么当我们把目录挪到上面的400550这个地址,你会看到里面进行了一次跳转,这个跳转指定国的跳转地址,你可以在后面的注释里可以看到

GLOBAL_OFFSET_TABLE+0x18。这里的GLOBAL_OFFSET_TABLE,就是我们接下来的要说的全局偏移表

1
400550:       ff 25 12 05 20 00       jmp    QWORD PTR [rip+0x200512]        # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>

五、GOT全局偏移表

在动态链接对应的共享库,我们在共享库的data section里面,保存了一张全局偏移表,虽然数据部分是各个动态链接它的应用程序里面各加载一份的。所有需要引用当前共享库外部的地址的指令,

都会查询GOT,来找到当前运行程序的虚拟内存里的对应位置。而GOT表里的数据,则是在我们加载一个个共享库的时候写进去的

不同的进程,调用同样的lib.so各自里面指向最终加载的动态链接库里面的虚拟内存地址是不同的

这样,虽然不同的程序调用的同样的动态库,各自的内存地址是独立的,挑用的有都是同一个动态库,但是不需要去修改动态库里面的代码所使用的地址

而是各个程序各自维护好自己的GOT,能够找到对应的动态库就好了

1、GOT表位于共享库自己的数据段里,GOT表在内存里和对应的代码位置之间的偏移量,始终是确定的,这样我们的共享库是地址无关的代码,

2、对应的各个程序只需要在物理内存里面加载同一份代码,而我们又要通过这个可以执行程序在加载时,生成的各个不相同的GOT表,来找到它需要调用到的外部变量和函数的地址

这是一个典型的、不修改代码、而是通过修改“地址数据“来进行关联的办法,它有点像我们在C语言里面用函数指针调用对应的函数,并不是通过预先已经确定好的函数名称

来调用,而是利用当时它在内存里面的动态地址来调用

六、总结延伸

这一讲、我们终于在静态链接和程序装载之后,利用动态链接把我们的内存利用到了极致。同样功能的代码生成共享库,我们只要在内存里面保留一份就好了,

这样我们不仅能够做到代码在开发阶段的复用,也能做到代码在运行阶段的复用

 

实际上、在进行Linux下的程序开发的时候,我们一直会用到各种各样的动态链接库,C语言的标准库在1MB以上。我们撰写任何一个程序可能都需要用到这个库,

常见的Linux服务器里,/usr/bin下面就有成千上外个可执行文件。如果每一个都把标准库静态链接进来的,几GB乃至几十GB的磁盘空间一下子就用出去了。

如果我们服务端的多进程应用要开上千个进程,几GB的内存空间也会一下子就用甩出去了,这个问题在过去计算机的内存较少的时候更佳显著

通过动态链接这个方式,可以说彻底解决这个问题,就像共享单车一样,如果仔细经营,是一个很有社会价值的事情,但是如果粗暴地把它变成无限制地

复制生产,每个人造一辆,只会在系统内知道大量无用的垃圾

 

过去05-09折五讲理,我们已经把程序怎么从源码变成指令、数据、并装载到内存里面,由CPU一条条执行下去的过程讲完了,希望你有所收获,对于一个程序,是怎么跑起来的,有了一个初步的认识

posted @   活的潇洒80  阅读(2130)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示