重学计算机组成原理(九)- 动态链接

把对应的不同文件内的代码段,合并到一起,成为最后的可执行文件

链接的方式,让我们在写代码的时候做到了“复用”。

同样的功能代码只要写一次,然后提供给很多不同的程序进行链接就行了。

“链接”其实有点儿像我们日常生活中的标准化、模块化生产。

有一个可以生产标准螺帽的生产线,就可生产很多不同的螺帽。

只要需要螺帽,都可以通过链接的方式,去复制一个出来,放到需要的地方

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

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

1 链接可以分动、静,共享运行省内存

我们上一节解决程序装载到内存的时候,讲了很多方法。说起来,最根本的问题其实就是内存空间不够用

如果能够让同样功能的代码,在不同的程序里面,不需要各占一份内存空间,那该有多好啊!

就好比,现在马路上的共享单车,我们并不需要给每个人都造一辆自行车,只要马路上有这些单车,谁需要的时候,直接通过手机扫码,都可以解锁骑行。

这个思路就引入一种新的链接方法,叫作动态链接(Dynamic Link)

相应的,我们之前说的合并代码段的方法,就是静态链接(Static Link)

在动态链接的过程中,我们想要“链接”的,不是存储在硬盘上的目标文件代码,而是加载到内存中的共享库(Shared Libraries)

这个加载到内存中的共享库会被很多个程序的指令调用到。

  • 在Windows下,这些共享库文件就是.dll文件,也就是Dynamic-Link Libary(DLL,动态链接库)
    用了“动态链接”的意思
  • 在Linux下,这些共享库文件就是.so文件,也就是Shared Object(一般我们也称之为动态链接库)。
    用了“共享”的意思

正好覆盖了两方面的含义。

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

要在程序运行的时候共享代码,这些机器码必须“地址无关

也就是说,我们编译出来的共享库文件的指令代码,是地址无关码(Position-Independent Code)

换句话说就是,这段代码,无论加载在哪个内存地址,都能够正常执行

如果还不明白,我给你举一个生活中的例子
如果我们有一个骑自行车的程序,要“前进500米,左转进入天安门广场,再前进500米”。
它在500米之后要到天安门广场了,这就是地址相关的。
如果程序是“前进500米,左转,再前进500米”,无论你在哪里都可以骑车走这1000米,没有具体地点的限制,这就是地址无关的。

大部分函数库其实都可以做到地址无关,因为它们都接受特定的输入,进行确定的操作,然后给出返回结果就好了。

无论是实现一个向量加法,还是实现一个打印的函数,这些代码逻辑和输入的数据在内存里面的位置并不重要。

而常见的地址相关的代码,比如绝对地址代码(Absolute Code)、利用重定位表的代码等等,都是地址相关的代码

回想一下我们之前讲过的重定位表。在程序链接的时候,我们就把函数调用后要跳转访问的地址确定下来了,这意味着,如果这个函数加载到一个不同的内存地址,跳转就会失败。

对于所有动态链接共享库的程序来讲,虽然我们的共享库用的都是同一段物理内存地址,但是在不同的应用程序里,它所在的虚拟内存地址是不同的。

没办法、也不应该要求动态链接同一个共享库的不同程序,必须把这个共享库所使用的虚拟内存地址变成一致。

如果这样的话,我们写的程序就必须明确地知道内部的内存地址分配。

那么问题来了,我们要怎么样才能做到,动态共享库编译出来的代码指令,都是地址无关码呢?

动态代码库内部的变量和函数调用都很容易解决,我们只需要使用相对地址(Relative Address)

各种指令中使用到的内存地址,给出的不是一个绝对的地址空间,而是一个相对于当前指令偏移量的内存地址

因为 整个共享库是放在一段连续的虚拟内存地址中的,无论装载到哪一段地址,不同指令之间的相对地址都是不变的

3 动态链接的解决方案

PLT和GOT

要实现动态链接共享库,也并不困难,和前面的静态链接里的符号表和重定向表类似

拿出一小段代码来看一看。

  • lib.h
    定义了动态链接库的一个函数 show_me_the_money
  • lib.c
    包含了lib.h的实际实现
  • show_me_poor.c
    调用了 lib 里面的函数
  • 把 lib.c 编译成了一个动态链接库,也就是 .so 文件
  • 最终生成文件集

在编译的过程中,指定了一个 -fPIC 的参数

其实就是Position Independent Code意,也就是要把这个编译成一个地址无关代码

然后,我们再通过gcc编译 show_me_poor 动态链接了 lib.so 的可执行文件

  • 在这些操作都完成了之后,我们把 show_me_poor 这个文件通过objdump出来看一下。
0000000000400540 <[email protected]>:
  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 <[email protected]>:
  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 <[email protected]>
  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的函数的时候,对应的代码是这样的:

这里后面有一个@plt的关键字,代表了我们需要从PLT,也就是程序链接表(Procedure Link Table)里面找要调用的函数。对应的地址呢,则是400580这个地址。

那当我们把目光挪到上面的 400580 这个地址,你又会看到里面进行了一次跳转,

  • 这个跳转指定的跳转地址,你可以在后面的注释里面可以看到:
    这里的 GLOBAL_OFFSET_TABLE,就是我接下来要说的全局偏移表。

在动态链接对应的共享库,我们在共享库的data section里面,保存了一张全局偏移表(GOT,Global Offset Table)

虽然共享库的代码部分的物理内存是共享的,但是数据部分是各个动态链接它的应用程序里面各加载一份的。

所有需要引用当前共享库外部的地址的指令,都会查询GOT,来找到当前运行程序的虚拟内存里的对应位置

而GOT表里的数据,则是在我们加载一个个共享库的时候写进去的。

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

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

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

GOT表位于共享库自己的数据段里

GOT表在内存里和对应的代码段位置之间的偏移量,始终是确定的

这样,共享库就是地址无关的代码,对应的各个程序只需在物理内存里加载同一份代码

而我们又要通过各个可执行程序在加载时,生成的各不相同的GOT表,找到它需要调用到的外部变量和函数的地址

这是一个典型的、不修改代码,而是通过修改“地址数据”来进行关联的办法

它有点像我们在C语言里面用函数指针来调用对应的函数,并不是通过预先已经确定好的函数名称来调用,而是利用当时它在内存里面的动态地址来调用。

4 总结

终于在静态链接和程序装载后,利用动态链接把我们的内存利用到了极致

同样功能的代码生成的共享库,我们只要在内存里面保留一份就好了

这样

  • 不仅能够做到代码在开发阶段的复用
  • 也能做到代码在运行阶段的复用。

实际上,在进行Linux程序开发,一直会用到各种各样的动态链接库。

C语言的标准库就在1MB以上。

撰写任何一个程序可能都需要用到这个库,常见的Linux服务器里,/usr/bin下面就有上千个可执行文件。

如果每一个都把标准库静态链接进来的,几GB乃至几十GB的磁盘空间一下子就用出去了。如果我们服务端的多进程应用要开上千个进程,几GB的内存空间也会一下子就用出去了。这个问题在过去计算机的内存较少的时候更加显著。

通过动态链接这个方式,可以说_彻底解决了这个问题_。

就像共享单车一样,如果仔细经营,是一个很有社会价值的事情,但是如果粗暴地把它变成无限制地复制生产,给每个人造一辆,只会在系统内制造大量无用的垃圾。

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

5 推荐阅读

想要更加深入地了解动态链接,推荐你可以读一读《程序员的自我修养:链接、装载和库》的第7章

里面深入地讲解了,动态链接里程序内的数据布局和对应数据的加载关系。

参考

  • 深入浅出计算机组成原理

原文地址:https://www.cnblogs.com/JavaEdge/p/11371239.html

时间: 2024-10-13 04:18:40

重学计算机组成原理(九)- 动态链接的相关文章

重学计算机组成原理(二)- 制定学习路线,攀登“性能”之巅

0 学习路线的知识点概括 学习计算机组成原理,就是学习计算机是如何协调运行的 计算机组成原理的英文叫Computer Organization Organization 意"组织机构". 该组织机构能够进行各种计算.控制.读取输入,进行输出,达成各种强大的功能. 把整个计算机组成原理的知识点拆分成了四大部分 计算机的基本组成 计算机的指令和计算 处理器设计 存储器和I/O设备. 0.1 计算机的基本组成 计算机的硬件组成 这些硬件,怎么对应到经典的冯·诺依曼体系结构的 除此之外,还需要

重学计算机组成原理(六)- 函数调用怎么突然Stack Overflow了!

用Google搜异常信息,肯定都访问过Stack Overflow网站 全球最大的程序员问答网站,名字来自于一个常见的报错,就是栈溢出(stack overflow) 从函数调用开始,在计算机指令层面函数间的相互调用是怎么实现的,以及什么情况下会发生栈溢出 1 栈的意义 先看一个简单的C程序 function.c 直接在Linux中使用GCC编译运行 [[email protected] Documents]$ vim function.c [[email protected] Document

重学计算机组成原理(十)- &quot;烫烫烫&quot;乱码的由来

程序 = 算法 + 数据结构 对应到计算机的组成原理(硬件层面) 算法 --- 各种计算机指令 数据结构 --- 二进制数据 计算机用0/1组成的二进制,来表示所有信息 程序指令用到的机器码,是使用二进制表示的 存储在内存里面的字符串.整数.浮点数也都是用二进制表示的 万物在计算机里都是0和1,搞清楚各种数据在二进制层面是怎么表示的,是我们的必修课. 在实际应用中最常遇到的问题,也就是文本字符串是怎么表示成二进制的,特别是我们会遇到的乱码究竟是怎么回事儿 在开发的时候,所说的Unicode和UT

重学计算机组成原理(十一)- 门电路的&quot;千里传音&quot;

人用纸和笔来做运算,都是用十进制,直接用十进制和我们最熟悉的符号不是最简单么? 为什么计算机里我们最终要选择二进制呢? 来看看,计算机在硬件层面究竟是怎么表示二进制的,你就会明白,为什么计算机会选择二进制. 1 怎么做到"千里传书" 马拉松的故事相信你听说过.公元前490年,在雅典附近的马拉松海边,发生了波斯和希腊之间的希波战争.雅典和斯巴达领导的希腊联军胜利之后,雅典飞毛腿菲迪皮德斯跑了历史上第一个马拉松,回雅典报喜.这个时候,人们在远距离报信的时候,采用的是派人跑腿,传口信或者送信

【重学计算机】机组D8章:输入输出系统

1. 输入输出系统概述 组成:外设.接口.总线.管理软件 基本功能 完成计算机内外的信息传递 保证CPU正确选择输出设备 利用缓冲等,实现主机与外设的速度匹配 特点:异步性.实时性.设备无关性 输入过程:CPU把地址值放入总线 --> CPU等候设备数据有效 --> CPU从总线读入数据存入寄存器 输出过程:CPU把地址值放入总线 --> CPU把数据值放入总线 --> 设备等数据有效取走数据 IO系统性能:存储IO.通信IO 连接特性:哪些设备可以和IO相连 IO系统容量:IO系

2017.0703.《计算机组成原理》-动态RAM

动态RAM 1.动静态的区别是存储原理的不同,但是它们的命名是由过程中的动作的差别,如动态的过程中有刷新的动作. 2.动态RAM比静态的RAM的集成度要高,功耗要小(集成度代表着完成一个相同的功能所需的器件数,动态RAM所需的MOS管数比静态所需的要少). 单管动态基本单元电路的集成度比三管动态基本单元电路要高. 3.三管动态基本单元电路

重学JAVA基础(三):动态代理

1.接口 public interface Hello { public void sayHello(); } 2.实例类 public class Hello2 { public void sayHello() { System.out.println("hello world2!"); } } public class Hello3 extends Hello2{ } public class HelloImpl implements Hello{ @Override public

【重学计算机】计组D1章:计算机系统概论

1.冯诺依曼计算机组成 主机(cpu+内存),外设(输入设备+输出设备+外存),总线(地址总线+数据总线+控制总线) 2.计算机层次结构 应用程序-高级语言-汇编语言-操作系统-指令集架构层-微代码层-硬件逻辑层 3.计算机性能指标 非时间指标 [字长]机器一次能处理的二进制位数 ,常见的有32位或64位 [总线宽度]数据总线一次能并行处理的最大信息位数,一般指运算器与存储器之间的数据总线的位数 [主存容量]主存的大小 [存储带宽]单位时间内与主存交换的二进制位数 B/s 时间指标 [主频f]时

【重学计算机】计组D3章:运算方法与运算器

1. 定点数运算及溢出 定点数加减法:减法化加法,用补码直接相加,忽略进位 溢出:运算结果超出了某种数据类型的表示范围 溢出检测方法:统一思想概括为正正得负或负负得正则溢出,正负或负正不可能溢出 方法1:V = XYS + XYS(XY为两个加数的符号位,S为结果的符号位,_表示非),那么V = 1则为溢出 方法2:V = C0 ⊕ C1(C0是最高数据位产生的进位,C1是符号位产生的进位),那么V = 1则为溢出 方法3:V = Xf1 ⊕ Xf2(数据采用变型补码 Xf1Xf2 X0X1X2