链接概述

经预处理器->编译器->汇编器处理后,源文件可被转化为一组可重定位目标文件,链接器将它们组合起来形成可执行文件。

每个可重定位目标文件由不同的“代码节”和“数据节”组成,每一个节都是一个连续的字节序列。由于每个可重定位目标文件的生成是独立的,因此组合时会出现两个问题:

一是若某个可重定位目标文件中使用了外部的全局变量,而此变量定义于另一个可重定位目标文件中,怎么办?

二是生成目标文件时如何确定地址。链接器生成的可执行文件在实际运行时,需要由加载器将其代码及数据复制到内存,再将控制转移到此程序的开头,若每个链接前的目标文件都以0为起始地址,势必会造成重合,怎么办?

这就是链接器要做的两件事:符号解析与重定位

符号解析:目标文件中可能会定义或引用一定的函数,全局变量或静态变量,这些都是所谓的“符号”,符号解析的目的就是把符号的定义和引用关联起来。

重定位:如上所述,第二个问题的解决方案就是重定位,链接器会把符号的定义与内存位置相关联,重定位时将符号的引用指向此内存位置。

可重定位目标文件

下图为典型的可重定位目标文件结构:

可重定位目标文件的内容以“节”的形式存在。

ELF头描述一些系统的和文件本身的信息。

节头部表描述不同节的位置和大小。

简略讲述几个重要的节:

.text节存放代码,.rodata节存放只读数据,.data节存放已初始化的全局和静态变量,.bss节存放未初始化或被初始化为0的全局和静态变量,此节不占用实际空间,只有在运行时才会被分配实际内存空间,另外局部变量在运行时被存放于栈中,不会出现在某个节中。.symtab节存放了符号表(符号表由汇编器构造)。

注意:静态局部变量不存放在栈中,而是会出现在符号表中。若同一目标文件中存在两个同名的静态局部变量,编译器会向汇编器输出两个不同名字的符号。

符号解析

当编译器遇到一个不是当前模块定义的符号时,会生成一个符号表条目,并交给链接器处理。链接器会在所有输入模块中寻找其定义,找不到时报错。

若出现多个目标文件定义相同名字的全局变量怎么办?

Linux系统的方法:编译器向汇编器输出全局变量对应的符号时,会标记是“强”符号(函数和已初始化的全局变量)还是“弱”符号(未初始化的全局变量),注意,静态变量独属于本模块,没有多重定义的问题,无需标记。汇编器将此信息编码在符号表内,链接器在链接时根据以下规则进行处理:

1.不允许有多个同名的强符号

2.如果有一个强符号和多个弱符号同名,选强符号。

3.若多个弱符号同名,随机选一个。

给个例子:

此时,假设x的地址是0x601020,y的地址是0x601024,运行程序后,赋值x时会覆盖到y,导致发生错误。

静态链接

所有相关的函数被编译为独立的目标模块,并被打包成一个单独的文件后,此文件被称为静态库,可以用做链接器的输入,当链接器构造可执行文件时,只会复制静态库里被应用程序引用的目标模块,这么做比起将每个独立的目标模块单独输入到链接器,显然可以简化链接器输入时的步骤,当然,对运行时内存的改善无优化,对于具有引用了相同标准目标模块的程序,运行时每次都需从静态库中拷贝这些目标模块。在Linux中静态库以“存档”的形式(一组连接起来的可重定位目标文件的集合)存放于磁盘,文件名后缀是.a。

举个例子: 

左边是两个独立的目标模块,右边的main函数要调用它们,此时先为左边两个函数创建一个静态库:

随后编译和链接输入文件main.o和libvector.a:

注意顺序:链接器从左到右来扫描可重定位文件和存档文件,我的理解是,定义必须出现在引用后面。另外,库一般放在命令行的结尾。

比如上面的main2.o中调用了addvec和multvec,所以放前面,随后出现libvector.a。

重定位

链接器在符号解析完成后开始进行重定位:将相同类型的节合并,为新的节和其中定义的符号赋予新的运行时内存地址,而对于各个节中的符号引用,则会通过“重定位条目”来重定位。

重定位条目:当汇编器遇到最终位置未知的目标引用,会生成重定位条目,告诉链接器在合并成可执行文件时如何修改引用,它存放于.rel.text节中,已初始化数据的重定位条目存放于.rel.data节中。

上图为一个典型的重定位条目格式,offset是节偏移,symbol标识引用应该指向的符号,type标识如何修改引用,addend是有符号常数,用于对引用作偏移调整。引用的重定位有两种修改方式:R_X86_64_PC32和R_X86_64_32,前者代表以相对引用重定位,后者代表以绝对引用方式重定位。

举个详细的例子帮助理解:

上图是一个main函数的汇编代码,里面有定义一个全局变量array,也有引用一个外部函数,sum。

先来看全局符号sum,其重定位条目如下:

假设链接器已经确定运行时.text节的地址为0x4004d0,此时:

节偏移为r.offset=0xf,因此,sum引用的内存地址位于0x4004d0+0xf=0x4004df处,即汇编代码中的第6行,位于e8后面,注意0xe8是指令的操作码。

r.symbol代表sum定义处的运行时地址,假设为0x4004e8。

r.type = R_X86_64_PC32,代表此时为相对引用。

r.addend用作偏移调整,这里是-4,我个人的理解是这代表了sum引用的内存地址到下一条指令地址的偏差,比如这里sum引用的内存地址是0x4004df,PC要执行的下一条指令的地址为0x4004e3(汇编代码中第8行)

综上,可以这么理解:由重定位条目sum我们可以定位到需要修改的引用地址为0x4004df,由r.addend可以定位到下一条指令的地址0x4004df-(-4)=0x4004e3,这个地址与sum定义处的运行时地址相差0x4004e8-0x4004e3=0x5。因此重定位时,汇编代码第6行的指令会被改为:

注意,这里0x5被写成05 00 00 00,这说明这里的表示方法是“小端法”(高字节位放高地址,低字节位放低地址)

实际运行时,执行到这条call指令时,PC值为0x4004e3(即下一条指令的地址),call指令执行时,这里用PC的值和05 00 00 00决定跳转后的地址,步骤如下:

1.将PC压栈

2.PC+0x5 = 0x4004e8->PC

此时会跳转到sum定义处的地址。

再来看全局符号array,其重定位条目如下:

r.type=R_X86_64_32,说明这里用的是绝对引用。

对array的引用位于汇编代码第4行,这是一条mov指令,起始于0x4004d0+0x9=0x4004d9处,它有1字节的操作码0xbf。

r.offset=0xa,告诉链接器要修改位于0x4004d0+0xa=0x4004da处的array引用地址

假设已经确定array定义地址为0x601018,结合第三章知识点,个人理解如下:由于这里是一条mov指令,不像call指令一样,call指令执行时是根据下一条指令地址(存于PC)来决定当前call指令中的跳转值的,是相对跳转,而mov指令是数据传送指令,直接将数据存入寄存器,无需作相对跳转,因此相应的r.addend=0,也因此重定位时直接修改此指令中的数据值即可,以下是重定位后对应的汇编代码:

可执行目标文件

典型的可执行目标文件的结构图:

它与之前的可重定位目标文件类似,具体不再赘述。

上图是可执行目标文件的程序头部表,前两行对应代码段,flags r-x代表有读/执行权限,此段开始于0x400000处,总共占内存大小为0x69c个字节

数据段有读/写权限,开始于0x600df8处,总共占内存大小为0x230个字节,可以推出.data节始于0x600df8-0xdf8=0x600000处,目标文件中大小为0x228字节,为什么会比总共占内存小0x230-0x228=8个字节?因为.bss节的数据只是占位符,位于可重定位目标文件时不占空间,链接成可执行文件后会为其初始化,分配内存空间。

加载可执行目标文件

运行可执行目标文件,是通过调用加载器来实现的,加载器会将程序复制到内存并运行。

上图是程序运行时的内存映像,具体不作解释,看书即可。

CSAPP阅读笔记-链接-第七章-P464-P500的更多相关文章

  1. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第七章:在Direct3D中绘制(二)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第七章:在Direct3D中绘制(二) 代码工程地址: https:/ ...

  2. JavaScript DOM编程艺术-学习笔记(第七章)

    第七章: 1.dom方法创建并且插入标签:(这种方法并没有改变文档的物理内容,而是在改变dom树) ①创建元素节点:createElement(); ②内部前插入:appendChild() ③创建文 ...

  3. Linux内核分析 读书笔记 (第七章)

    第七章 链接 1.链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行. 2.链接可以执行于编译时,也就是在源代码被翻译成机器代码时:也可以执行于 ...

  4. Kafka 权威指南阅读笔记(第三章,第四章)

    Kafka 第三章,第四章阅读笔记 Kafka 发送消息有三种方式:不关心结果的,同步方式,异步方式. Kafka 的异常主要有两类:一种是可重试异常,一种是无需重试异常. 生产者的配置: acks ...

  5. 《深入理解java虚拟机》读书笔记六——第七章

    第七章 虚拟机类加载机制 1.类加载的时机 虚拟机的类加载机制: 虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验.转换解析和初始化,最终形成了可以被虚拟机直接使用的Java类型,这 ...

  6. CSAPP阅读笔记-gcc常用参数初探-来自第三章3.2的笔记-P113

    gcc是一种C编译器,这次我们根据书上的代码尝试着使用它. 使用之前,先补充前置知识.编译器将源代码转换为可执行代码的流程:首先,预处理器对源代码进行处理,将#define指定的宏进行替换,将#inc ...

  7. CSAPP阅读笔记-struct, union, 数据对齐-来自第三章3.9的笔记-P183-P191

    1.数据对齐 为什么要对齐:通俗点解释就是CPU对数据访问时,每次都是取固定数量的字节数,假如一次取4个字节,若有个int存在0x01-0x04,则一次就能取出,若存在0x03-0x06,则需要分两次 ...

  8. CSAPP阅读笔记-汇编语言初探(控制类指令)-来自第三章3.6的笔记-P135-P163

    1.正溢出与负溢出: 首先,一个正数与一个负数相加,不可能溢出,因为结果的绝对值一定小于两个加数的绝对值,既然两个加数能合理表示出来,结果一定也能合理表示出来. 其次,正溢出是由于两个很大的正数相加, ...

  9. CSAPP阅读笔记-汇编语言初探(数据传送类指令)-来自第三章3.2-3.3的笔记-P115-P128

    1.如何由机器代码生成汇编代码? objdump -d再加上文件名即可直接在终端看到由反汇编器恢复的汇编代码.注意,文件名并不一定得是.o文件,任何可执行文件都可以. 结果如下: 仅列举了反汇编tes ...

随机推荐

  1. APP 市场需求网址

    http://mi.talkingdata.com/terminals.html?terminalType=4

  2. sql 脚本创建索引

    之前从没有用SqlServer数据库处理过大数据量的表,都是用Oracle,然后一般为数据量较大的表添加索引或主键都是用plsql工具,今天正好需要为一张保存于SqlServer数据库的千万级数据表增 ...

  3. C# 重写WndProc 拦截 发送 系统消息 + windows消息常量值

    接收拦截+发送消息 对于处理所有消息.net 提供了wndproc进行重写 WndProc(ref Message m)protected override void WndProc(ref Mess ...

  4. c# 字符串填充占位

    C#  字符串PadLeft函数的使用 1.Demo: 需求: 将111改变成0000111 使用字符串PadLeft函数可以解决: int num = 111; string s= num.ToSt ...

  5. 运维利器:钉钉机器人脚本告警(Linux Shell 篇)

    写在前面的话 目前换了几家公司,且最近几家都是以钉钉作为公司 OA 聊天工具,总的来说还是很不错的.最近去了新公司,由于公司以前没有运维,所以监控,做自动化等方面都没有实施,恰逢这个机会把最近做的关于 ...

  6. CentOS6.5上Zabbix3.0的RPM安装【二】-汉化

    六.汉化 zabbix实际是有中文语言的,我们可以通过修改web端源文件来开启中文语言.首先点击zabbix监控页面右上角管理员头像进入“用户基本资料设置页面“. 选择中文语言. 点击“Update” ...

  7. 2018沈阳网赛F--上下界网络流

    建图: 首先加一个源点s和汇点t,分别连接在二分图的左边和右边,每条弧的上下界为[L, R],二分图左边和右边之间连弧上下界为[0,1],其实就相当于连弧为1. 然后问题就转换为:有源汇最大流. 继续 ...

  8. bzoj3328: PYXFIB(单位根反演+矩阵快速幂)

    题面 传送门 题解 我们设\(A=\begin{bmatrix}1 & 1 \\ 1 & 0\end{bmatrix}\),那么\(A^n\)的左上角就是\(F\)的第\(n\)项 所 ...

  9. Linux更改文件或目录的所有者和所有组

    上节我们说了所有者和所有组的概念, 一个文件它的所有者是谁,属于哪个组的,不同的角色对其的操作权限是不一样的,详细信息请看上节Linux权限管理 这里我们主要说的是怎么去改变这个文件或目录的所有者和所 ...

  10. P3379 【模板】最近公共祖先(LCA)(LCT)

    \(\color{#0066ff}{ 题目描述 }\) 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. \(\color{#0066ff}{输入格式}\) 第一行包含三个正整数N.M. ...