一、重定位

当链接器完成符号解析的步骤后,每个符号引用和符号定义都对应到了一起。此时,链接器已经知道了输入目标模块中的代码和数据段的大小,准备开始重定位工作,在重定位阶段链接器会将输入模块整合到一起并为每个符号赋予运行时地址。重定位分为两个步骤:

  • 重定位段和符号定义:在这个步骤,链接器将所有相同类型的段都整合进入一个新的聚合的段中。例如,所有的输入模块中的.data段都会被整合进入输出的可执行文件的.data段中。链接器接下来为新生成的段,原模块中的段以及原模块中的符号赋予运行时地址。当这步完成后,程序中的每一个指令和全局变量都有了一个独一无二的运行时地址内存地址。
  • 用段重定位符号引用:在这步中,链接器会修改代码和数据段中的每个符号引用,这时符号引用会指向正确的运行时地址。为了执行这步,链接器依赖一种在可重定位目标文件中的数据结构,称为重定位实体(relocation entries)

1.1 重定位实体

当汇编器生成目标模块时,它并不知道代码和数据最终在内存空间中的地址。它也不知道任何本模块引用的函数或全局变量的地址。所以每当汇编器遇到了一个最终位置未知的对象引用时,它就会生成一个重定位实体,来告知链接器如何在整合可重定位目标文件的时候修改这个引用。代码中的重定位实体放置在.rel.text段中。数据中的重定位实体放置在.rel.data中。

下图展示了ELF重定位实体的格式:

图7.8

其中:

  • offset是需要修改的引用的段内偏移量
  • symbol指代修改过的引用需要指向的符号
  • type告知编译器如何修改新的引用

ELF定义了11种不同的可重定位类型,其中很多都是晦涩难懂的,我们只需要关注两个最基本的:

  • R_386_PC32:指代重定位一个使用32位PC相对寻址(PC-relative address)的引用。一个PC相对寻址的地址是一个相对于当前程序计数器(program counter)的偏移量。当CPU执行到一个使用PC相对寻址的指令时(例如call指令),它会用当前程序计数器的值加上一个编码在指令内的32位值,来获取下一条要执行指令的内存地址。
  • R_386_32:重定位一个使用32位绝对寻址的引用。当使用绝对引用时,CPU直接利用指令内的32位值作为有效地址,不进行任何的修改。

1.2 重定位符号引用

下图是链接器重定位算法的伪代码:


第一行和第二行在每个段内都进行迭代,且遍历了段内的每一个可重定位实体。为了准确性,我们需要对图中的算法做一些假定,设每个段s都是一个字节码数组,每个重定位实体都是类型为Elf32_Rel的结构体(图7.8所示)。并且我们假定,当算法运行时,链接器已经为每个段选择了运行时地址(用ADDR(S)表示),且已经为每个符号选择好了运行时地址(用ADDR(r.symbol)表示)。

第三行计算了s中需要进行重定位的四字节大小的引用的地址。如果这个引用使用PC相对寻址,那么接下来它就被5-9行的代码所处理。如果这个引用使用绝对寻址方式,那么就会被11-13行的代码所处理。

refptr存储的值在后面的例子中进行了解释,它最终存储的值,在相对寻址方式下就是偏移量,在绝对寻址方式下就是地址值。而refaddr实际上存储的是当前指令寄存器PC-4的值(ADDR(s)+r.offset实际上是call X时X的偏移量,这个X就是PC-4的值)

1.2.1 重定位PC相对寻址引用的例子


上面是我们在图7.1(a)中的程序例子,其中main.o的.text部分中的main例程调用swap例程,swap例程在swap.o中定义。下面是由GNU objdump工具生成的call指令的反汇编列表:

6:   e8 fc ff ff ff              call 7<main+0x7>   swap();7:R_386_PC32 swap  relocation entry

从这个列表中,我们可以看到call指令从段偏移量0x6开始,它包含1字节操作码0xe8,后跟32位引用0xfffffc(代表小端编址表示下的十进制值−4,因为ADDR(s)+r.offset的值是PC-4,+4之后可以获得PC的值),这个引用的偏移量为7,因此这里是call 7。我们在下面的行中看到了此引用的重定位条目。(重定位条目和指令实际上存储在目标文件的不同段中。为了方便起见,objdump工具将它们放在一起显示。)重定位条目r由三个字段组成:

  • r.offset=0x7
  • r.symbol=swap
  • r.type=R_386_PC32

这些字段告知链接器从偏移量0x7处修改32位PC相对引用,以便在运行时指向swap例程。现在,假设链接器已经确定

ADDR(s) = ADDR(.text) = 0x80483b4

以及

ADDR(r.symbol) = ADDR(swap) = 0x80483c8

使用图7.9所示的算法,链接器首先计算出引用的运行时地址:

refaddr = ADDR(s)   + r.offset= 0x80483b4 + 0x7= 0x8048bb

接下来链接器的引用值从-4修改为0x9,这样引用在运行时就会指向swap例程:

*refptr = (unsigned)(ADDR(r.symbol) + *refptr - refaddr)上面的一个更便于理解的描述方式是:(unsigned)(ADDR(r.symbol) + (refaddr-*refptr)),中间括号内的值就是PC的值= (unsigned)(0x80483c8      + (-4)    - 0x80483bb)=(unsigned)(0x9)

在最后得到的可执行文件中,call指令就有着如下的形式:

80483ba: e8 09 00 00 00 call 80483c8 <swap>

在运行时,call指令将存储在地址0x80483ba处。当CPU执行call指令时,PC的值为0x80483bf,这是紧接call指令之后的指令的地址。为了执行call指令,CPU执行以下步骤:

  1. 当前PC入栈
  2. PC = PC + 0x9 = 0x80483bf + 0x9 = 0x80483c8

这时,程序下一条要执行的语句就变成了swap例程的第一条指令,这时就达到了我们想要的效果。

在call指令中创建了初始值为-4的引用的作用是很巧妙的。汇编程序使用此值来确保PC始终指向当前指令后面的指令。在具有不同指令大小和编码的不同机器上,该机器的汇编程序将使用不同的偏移。这是一个有效的技巧,允许链接器在无需了解特定机器的情况下进行重定位引用。

1.2.2 重定位绝对引用

仍然以图7.1中的示例程序为例,swap.o模块将全局指针bufp0初始化为全局buf数组的第一个元素的地址:

int *bufp0 = &buf[0];

由于bufp0是一个已经初始化了的数据对象,它将存储在swap.o可重定位目标模块的.data部分。但由于它被初始化为全局数组的地址,因此需要重定位它。以下是swap.o中.data节的反汇编列表(listing):

00000000 <bufp0>:0: 00 00 00 00                    int *bufp0 = &buf[0];0: R_386_32 buf               Relocation entry

我们看到.data部分包含一个32位引用,即bufp0指针,其值为0x0。重定位实体告知链接器这是一个32位绝对引用,从偏移量0开始,必须重定位它,使其指向符号buf。现在,假设链接器已经确定:

ADDR(r.symbol) = ADDR(buf) = 0x8049454

接下来链接器就会使用图7.9中算法的第13行更新引用:

*refptr = (unsigned) (ADDR(r.symbol) + *refptr)= (unsigned) (0x8049454 + 0)= (unsigned) (0x8049454)

在我们最后得到的可执行目标文件中,引用就变成了如下形式:

0804945c <bufp0>:804945c: 54 94 04 08

换句话说,链接器已经决定在运行时变量bufp0将位于内存地址0x804945c,并且将被初始化为0x8049454,这是buf数组的运行时地址。

图7.10展示了在可执行文件中重定位后的.text段和.data段信息。


图7.10

练习7.4

下面的几个问题都与图7.10有关

A. 重定位后的swap函数的十六进制地址是什么?
B. 重定位后的swap函数的十六进制值是多少?
C. 如果链接器决定将.text段的值从0x80483b8修改至0x80483b4,此时第五行的重定位引用的十六进制值将会变为多少?

答案:
A. 0x80483bb
B. 0x9
C. 0x9 关键点在于,无论.text的地址如何变化,引用和swap函数间的相对距离都是不变的。

二、可执行目标文件

C程序最初是ASCII文本文件的集合,经过编译链接后已经转换成一个二进制文件,其中包含了将程序加载到内存并运行它所需的所有信息。图7.11展示了ELF可执行文件中的各种信息:

可执行目标文件的格式很类似于可重定位目标文件。ELF首部描述了文件的整体格式。它还包括程序的入口点,即程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位对象文件中的节类似,只是这些节已重定位到其最终运行时内存地址。.init节中定义了一个小函数,称为_init,它将由程序的初始化代码调用。因为可执行文件是完全链接(重定位过)的,所以它不需要.rel节。

ELF可执行文件被设计为易于加载到内存,可执行文件的连续块会映射到连续内存段中。这个映射由段头表(segment header table)描述。图7.12显示了示例可执行文件p的段头表(利用OBJDUMP):

图 7.12

从段头表中,我们看到两个内存段将用可执行目标文件的内容初始化。第1行和第2行告诉我们,第一个段(代码段)与4KB(2122^{12}212)边界对齐,具有读/执行权限,从内存地址0x08048000开始,总内存大小为0x448字节,并用可执行目标文件的第一个0x448字节初始化,该部分文件包括ELF头、段头表和.init、.text和.rodata段。

第3行和第4行告诉我们,第二个段(数据段)与4KB边界对齐,具有读/写权限,从内存地址0x08049448开始,总内存大小为0x104字节,并用从文件偏移量0x448开始的0xe8字节进行初始化,在本例中,它是.data节的开始。段中的剩余字节对应于运行时将初始化为零的.bss数据。

三、加载可执行文件

要运行一个可执行目标文件p,我们可以在Unix的命令行打上这个文件的名字:

unix> ./p

由于p并不属于内置shell命令,此时shell假定p是一个可执行目标文件,它通过调用称为loader的常驻内存的操作系统代码来运行p。任何Unix程序都可以通过调用execve函数来调用加载程序。加载程序将可执行目标文件中的代码和数据从磁盘复制到内存中,然后跳转到第一条指令或入口点来运行程序。将程序复制到内存中然后运行它的过程称为加载(loading)。

每个Unix程序都有一个类似于图7.13中的运行时内存映像:

图 7.13

在32位Linux系统上,代码段从地址0x08048000开始。数据段紧跟在下一个4KB对齐的地址处。运行时堆在跟随读/写段之后的下一个4KB对齐地址,并通过调用malloc库来实现增长。还有一个为共享库保留的段。用户堆栈总是从最大的合法用户地址开始,然后向下增长(朝着较低的内存地址)。从用户堆栈上方开始的段是为操作系统的内存驻留部分(称为内核)中的代码和数据保留的。

当加载程序运行时,它会创建图7.13所示的内存映像。在可执行文件中的段头表的指导下,它将可执行文件的块复制到代码和数据段中。接下来,加载程序跳转到程序的入口点,该入口点始终是_start符号的地址。位于_start处的启动代码在目标文件crt1.o中定义,对于所有C程序都是相同的。图7.14显示了启动代码中调用的特定序列:

在从.text和.init节调用初始化例程之后,启动代码将调用atexit例程,该例程将在应用程序正常终止时附加应调用的例程列表。exit函数运行atexit注册的函数,然后通过调用_exit将控制权返回给操作系统。现在,启动代码会调用应用程序的主例程,该例程开始执行我们的C代码。在应用程序返回后,启动代码调用_exit例程,该例程将控制权返回给操作系统。

练习 7.5

A 为什么每一个C程序都需要一个main函数
B 为什么一个C main函数可以通过调用exit、通过return语句或者不通过上面两种方式均可以正常中止

答案:

A 因为在位于_startC启动代码中的特定执行序列中会调用main函数
B 如果main函数通过return语句结束,那么控制权会返回到启动例程中,接下来启动例程会通过调用_exit将控制权返回操作系统中。如果用户省略return语句效果也是一样的。如果main函数通过exit结束,那么exit最终也会通过调用_exit将控制权返回给操作系统。所以三种情况是相同的,控制权最终均会返还给操作系统

四、利用共享库进行动态链接

静态库有一些显著的缺点,它和所有软件一样,需要定期维护和更新。如果应用程序程序员想要使用库的最新版本,他们必须意识到库已经更改,然后根据更新的库重新链接程序。

另一个问题是几乎每个C程序都使用标准的I/O函数,比如printf和scanf。在运行时,这些函数的代码会在每个正在运行的进程内存中的文本段中进行复制。通常系统会运行50–100个进程,这是对稀缺内存系统资源的严重浪费。(内存的一个有趣的特性是,不管系统中有多少内存,它始终是一种稀缺资源。)

共享库是解决静态库缺点的一个方法。它是一种特殊的目标模块,这个模块在运行时可以在任意内存地址加载,并与内存中的程序进行链接。这个过程称为动态链接,由一个称为动态链接器的程序执行。

共享库也称为共享对象,Unix系统上通常用.so后缀表示。Microsoft操作系统也大量使用共享库,但称之为DLL(动态链接库)。

共享库以两种不同的方式“共享”。首先,在任何给定的文件系统中,都有一个特定库的.so文件。这个.so文件中的数据和代码由所有引用这个库的可执行文件共享,这与静态库相反,静态库的内容是复制的,并嵌入到引用库的可执行文件中。其次,内存中共享库的.text节的一个副本可以由不同的运行过程共享。在第9章中,我们将更详细地研究虚拟内存。

图7.15总结了图7.6中示例程序的动态链接过程:

为了构建图7.5中所需的libvector.so向量算术例程,我们将使用以下指向链接器的特殊指令调用编译器驱动程序:

unix> gcc -shared -fPIC -o libvector.so addvec.c multvec.c

-fPIC标志指示编译器生成与位置无关(position-independent)的代码。-shared标志指示链接器创建共享目标文件。

当我们创建了库后,我们就可以将它链接到图7.6所示的示例程序中:

unix> gcc -o p2 main2.c ./libvector.so

这将创建一个可执行对象文件p2,它的格式支持在运行时和libvector.so库进行链接。其基本思想是在创建可执行文件时静态地进行一部分链接,然后在加载程序时动态地完成链接过程。

需要注意的是,在此时libvector.so中还没有代码或数据被拷贝进入可执行文件p2中。链接器只会复制一些重定位和符号表信息,这些信息将允许libvector.so中的代码和数据的引用在运行时被解析。

当加载器加载并运行可执行文件p2时,它会加载部分链接过的可执行文件p2。接下来加载七会注意到p2存在一个.interp节,这个节中包含了动态链接器的路径明程,这个动态链接器本身也是一个共享目标(例如Linux系统中的LD-Linux.SO)。加载器此时不会将控制权移交给应用程序,而是运行动态链接器。

接下来动态链接器会通过如下的操作来完成重定位过程:

  • 将libc.so的text及data内容重定位进入某个内存段中
  • 将libvector.so的text及data内容重定位进入另外的内存段中
  • 将p2中的引用重定位到libc.so和libvector.so中定义的符号中

最终,动态链接器将控制权移交给引用程序。从这里开始,共享库的地址是固定的,在整个程序的执行期间不会发生改变。

五、从其他应用程序中加载并链接共享库

待补充 719

7.7-11 重定位过程描述+可执行目标文件的加载+共享库动态链接相关推荐

  1. 浏览器加载一个页面过程,本地的html文件的加载过程

    打开一个本地文件 读取返回给网页 import flask app=flask.Flask(__name__) @app.route('/index') def miaomiao():with ope ...

  2. MIPS架构下LW指令的重定位过程

    通常我们不会去关心指令重定位(relocation)的细节,编译器的ld过程已经帮助我们做好了.由于最近在移植CRIU,涉及到指令的重定位计算,不得不细细研究代码重定位的细节知识.之前的文章介绍了MI ...

  3. ELF文件的加载和动态链接过程

    本文的目的:大家对于Hello World程序应该非常熟悉,随便使用哪一种语言,即使还不熟悉的语言,写出一个Hello World程序应该毫不费力,但是如果让大家详细的说明这个程序加载和链接的过程,以 ...

  4. extjs5(03--项目中文件的加载过程)

    上一节中用sencha工具自动创建了一个项目,并且可以在浏览器中查看.现在我们来看看js类加载过程.如下图所示: 1、首先:浏览器中输入 localhost:1841 ,调用 index.html; ...

  5. 高通平台dtb文件的加载过程

    高通平台dtb文件的加载过程 高通平台对dts的两种打包方式 zImage-dtb dt.img zImage-dtb方式 zImage-dtb的编译 二进制文件查看 lk的加载过程代码分析 dt.i ...

  6. Django 1.11 bootstrap样式文件无法加载问题解决

    Django 1.11 bootstrap样式文件无法加载问题解决 参考文章: (1)Django 1.11 bootstrap样式文件无法加载问题解决 (2)https://www.cnblogs. ...

  7. 微信分享功能问题-描述内容换行,导致js加载失败

    jsContent内容存在换行符,导致js处理失败, 解决办法,在后台接口中替换'/r/n'为空,并截取前30位 weiXinJsSdk.setSharcontent(StringUtils.isNo ...

  8. 1wifi 简介(框架)2系统启动后的 wifi加载 过程-图解 3系统启动后的 wifi 加载过程

    http://blog.csdn.net/wh_19910525/article/details/7384480 一.wifi 组建原理: WIFI就是一种无线联网的技术,以前通过网线连接电脑,而现在 ...

  9. 试简述smtp通信的三个阶段的过程_从输入URL到页面加载的过程?《转载》

    这是我看过这个问题最完整/优质的回答了,转来分享 知乎的排版不太好,可以浏览博客原文: http://gaoxiang.ga/index.php/archives/36/​gaoxiang.ga 前言 ...

  10. jvm类加载过程_JVM类生命周期概述:加载时机与加载过程

    作者:菜鸟小于 https://www.cnblogs.com/Young111/p/11359700.html 一个.java文件在编译后会形成相应的一个或多个Class文件,这些Class文件中描 ...

最新文章

  1. 窃隐私、放高利贷,输入法的骚操作真不少!
  2. 前端开发VScode常用插件
  3. Android跨屏拖动item,android中如何实现两个listview中的item可以互相拖拽移位?
  4. ajax跨域请求的问题
  5. python文件读取写入实践_python文件写入实例分析
  6. c语言 炸弹文件,炸弹超人游戏c语言简板
  7. 昇腾AI计算,无惧618冲动消费
  8. 免费「模拟面试」福利反馈连载(20180128期)
  9. python 词云生成
  10. xmanager无法连接Linux服务器,xmanager连接linux出错问题怎么解决
  11. 家用双wan口路由器推荐_教你如何配置双WAN口路由器
  12. 下单以及订单处理流程描述
  13. button type属性
  14. 低学历转行3D建模有前途吗,游戏3D建模怎么样?
  15. 五年IT五股东风 吹奏一曲SaaS“东风破”
  16. Centos下Zookeeper的安装部署
  17. 一个比较好用而且免费的录屏软件
  18. 利用JAVA实现2+22+222+2222+22222的问题
  19. 【蓝桥杯】小蜜蜂老师—工厂灯光控制系统代码(IO模式)
  20. 网站日志分析工具汇总

热门文章

  1. Linux文件解压缩
  2. Python基础语法-05-装饰器
  3. 使用EasyUI加载树形菜单
  4. Kotlin实战【二】Kotlin基本要素
  5. 视频播放器本地代理服务设计
  6. Go基础-go的源码文件以及常用命令
  7. 控制网页frame vba_V8 bindings 设计isolate,context,world,frame之间的关系(翻译)
  8. c 远程登陆linux,Linux C/C++基础——Windows远程登录Linux
  9. linux系统管理与服务器配置【2008网络工程师】,Linux系统管理与服务器配置
  10. java datatable用法_C# DataTable用法示例详解|DataTable Select,sort排序,Linq过滤操作