CSAPP:学习日志2---链接的复习

编译过程:

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,加载时或是运行时。

大多数编译系统提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器(cpp)编译器(ccl)汇编器(as)链接器(ld)

源程序经过 翻译器 生成 .o 的可重定位目标文件,最后,驱动程序运行链接器程序 ld ,将一些可重定位目标文件和系统目标文件组合起来,创建一个可执行目标文件prog:

gcc -o prog main.c sum.c

要运行可执行目标文件prog,我们在Linux shell的命令行上输入它的名字:

./prog

静态链接:

像 Linux LD 程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。

在 C 源文件中定义的全局变量和全局函数,在编译中称为符号,编译器并不关心局部变量或局部函数的名字,因为局部变量的创建和回收是通过用户栈进行管理的。而已初始化的全局变量需要在编译时就分配好空间,确定好地址。

为了构造可执行文件,链接器必须完成两个主要任务:符号解析,即将符号的声明和符号的定义联系在一起,和重定位,即确定符号引用的运行时地址。

- 符号解析:

汇编器在生成可重定位目标文件时,会在目标文件中生成一个符号表。符号表指明了文件中有哪些符号,符号是全局符号还是本地符号,符号的定义所在的节,以及在节中的相对位置。有些符号在本模块中引用,但是在其他模块中定义,汇编器无法确定这种外部符号的定义位置,而需要借助链接器进行符号解析步骤得到。链接器根据传入的各个目标文件完成符号解析后,所有模块中的符号引用都能与某个模块中的符号表条目对应起来。

链接器使用下面的规则来处理多重定义的符号:
—不允许有多个同名的强符号。
—如果有一个强符号和多个弱符号,那么选择强符号。
—如果有多个弱符号,那么从这些弱符号中任意选择一个。

符号和符号表:

在链接器的上下文中,有3种符号:

—由模块m(就是可重定位目标文件)定义,并能被其的模块引用的全局符号。称为全局链接器符号。
—由其他模块定义并被模块m引用的全局符号。称为外部符号。对应于定义在其他模块中的C函数和变量,可以被本模块引用,引用。
—只被模块m定义和引用的本地符号。称为本地链接器符号。对应于带static属性的C函数和全局变量.

PS:在函数内部定义的static变量,不在栈中管理。而是在.data和.bss中为每个定义分配空间,并且在.symtab中创建一个名字唯一的本地符号。

- 重定位:

可重定位目标文件中记录的位置都是一些相对位置,因为一个可执行文件往往需要多个可重定位目标文件生成,链接器在生成可执行文件的过程中需要将输入的所有可重定位目标文件中的各个节进行重新聚合,并为每个节确定运行时地址。这个时候所有指令和符号定义的运行时位置都确定了。

重定位由两步组成:

重定位节和符号定义——所有相同类型的节合并为同一类型的新的聚合节。将运行时存储器地址赋给—聚合节—每个模块定义的每个节—每个符号。完成这一步之后,程序中的每个指令和全局变量都有唯一的运行时存储器地址了。
重定位节中的符号引用——这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。——这一步依赖于重定位条目,这个条目就是.rel.text和.rel.data两个节。

目标文件

目标文件有3种形式:可重定位目标文件可执行目标文件共享目标文件

可重定位目标文件

在这里插入图片描述
ELF可重定位目标文件的格式包括:

----ELF头——16字节,包含:系统的字的大小和字节顺序,帮助链接器语法分析和解释目标文件的信息(ELF头的大小,目标文件的类型-可重定位-共享-可执行,机器类型-IA32,节头部表的文件偏移,节头部表中的条目大小和数量)。
节头部表——描述:不同节的位置和大小。节就是夹在ELF头和节头部表之间的。节头部表包含很多条目,每一个条目的大小都是一样的,每一个条目都对应一个节。
----.text——一个节,包含已编译的机器代码。
----.rodata—一个节,read only data,只读数据,比如printf语句中的格式串和switch的跳转表。
----.data——一个节,已初始化的全局C变量。局部变量在运行时保存在栈中,既不在.data,也不在.bss中。
----.bss——一个节,未初始化的全局C变量。这个节不占据实际的磁盘空间。区分初始化和未初始化是为了空间效率。(意思是,.data磁盘实际保存的只有初始化的全局变量)
----.symtab——一个节,符号表,程序中定义和引用的函数和全局变量的信息。每个ELF文件都有。
----.rel.text——一个节,服务于.text节,是描述.text中位置的列表。当链接器把本目标文件和其他文件结合时,这个小节,就要修改。
----.rel.data——一个节,服务于.data,被模块引用或定义的任何全局变量的重定位信息。同上,和其他文件结合时,这个小节,就要修改。
----.debug——一个节,一个调试符号表,表中记录着程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的c源文件。只有以-g选项调用编译驱动程序时才会得到这张表。
----.line——一个节,原始c源程序中的行号和.text节中机器指令之间的映射。同上-g才有。
----.strtab——一个节,一个字符串表,其中包含.symtab和.debug节中的符号表,以及节头部表中的节名字。字符串表,就是一个一个的字符串,字符串就是以NULL结尾的字符序列。

可执行目标文件

在这里插入图片描述
其特有的节包括:

  • 段头部表 将文件节映射到运行时存储器段 为加载提供便利
  • .init 用于指示程序计数器的值

相比可重定位目标文件,少了:

  • .ref.text 指示机器指令中需要重定位的符号
  • .ref.data 指示全局符号中需要重定位的符号
    与重定位有关的节
与静态库链接

我们每次链接都需要将所有相关的可重定位目标文件依次作为参数传入链接器,对于大型项目来说,这会是一个很繁琐且很容易出错的步骤。静态库是基于此背景下提出的一个解决方案。静态库就像是一个集合包,把许多重定位目标文件都聚合在一起作为一个整体。链接的时候,只需要将静态库整体作为参数传入即可。链接器会从静态库中取出生成可执行文件所需要的可重定位目标模块。

静态库可以通过 AR 工具生成:

gcc -c addvec.c multvec.c
gcc ar rcs libvector.a addvec.o multvec.o

在创建可执行文件时:

gcc -c main2.c
gcc -static -o prog2c main2.o ./libvector.a

或者等价使用:

gcc -c main2.c
gcc -static -o prog2c main2.o -L. -lvector

执行文件:

./prog2c

在此,我用书上的例子进行分析:
main2.c

include <stdio.h>
#include "vector.h"
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main() 
{
    addvec(x, y, z, 2);
    printf("z = [%d %d]\n", z[0], z[1]);
    return 0;
}

addvec.c

int addcnt = 0;
void addvec(int *x, int *y, int *z, int n) 
{
    int i;
    addcnt++;
    for (i = 0; i < n; i++)
	    z[i] = x[i] + y[i];
}

multvec.c

int multcnt = 0;
void multvec(int *x, int *y, int *z, int n) 
{
    int i;
    multcnt++;
    for (i = 0; i < n; i++)
	    z[i] = x[i] + y[i];
}

实际运行结果:
在这里插入图片描述

我们还可以通过readelf工具进一步研究目标文件的相关内容:

  • 选项 -h(elf header),显示ELF文件开始的文件头信息
  • 选项 -l(program headers),segments 显示程序头(段头)信息(如果有数据的话)
  • 选项 -S(section headers),sections 显示节头信息(如果有数据的话)
  • 选项 -s,symbols 显示符号表段中的项(如果有数据的话)
  • 选项 -a,all 显示全部信息,等价于 -h -l -S -s -r -d -V -A -I

在此,我用 readelf -h 查看 main2.o 的ELF头信息
在这里插入图片描述
Magic:表示该文件是ELF目标文件,第一个字节7f是固定的数,后面的45 4c 46这三个字节是E,L,F三个字符的ASCII码形式
类别(Class):表示文件的类型是64位的ELF格式
数据(Data):表示文件中类型的格式组织(大端法或小端法) 这里是小端法
版本(Version):ELF文件头的版本号 这里的版本号为1
OS/ABI: 表示操作系统的类型 这里是UNIX - System V
ABI 版本: 为ABI版本号 这里为0
类型(Type):表示ELF文件类型 这里的是可重定位目标文件REL (共有三种,还有两种为可执行目标文件EXEC和共享目标文件)。
系统架构(Machine): 表示机器的类型 这里为X86-64
版本(Version)(第二个):表示当前目标文件的版本号 这里是1。
入口点地址(Entry point address): 表示程序的虚拟地址入口点 这里为零(因为没有链接,是不可运行的程序)
程序头起点(Start of program headers): 这个程序没有程序头
Start of section headers: sections 头的开始处 这里的1000是十进制,即开始的地址为0x3E8
标志(Flags):这是一个与处理器相关联的标志 x86 平台(该机器)上此处为 0 。
本头的大小(Size of this header):ELF文件头的字节数 这里是64字节
标志头的大小(Size of program headers): 每个程序头的大小 因为不可运行,所以这里为0
Number of program headers: 程序头的数目 因为不可运行,所以这里为0
节头大小(Size of section headers): section头的大小 这里是64字节。
节头数量(Number of section headers): section头的数量 这里有13个
字符串表索引节头(Section header string table index): section 头字符串表索引号 这里的索引号为12

此处ELF头信息的解释节选于:
https://blog.csdn.net/qq_43919787/article/details/101831601

查看与静态库链接后的文件:
在这里插入图片描述
查看直接编译生成的文件:

在这里插入图片描述
发现直接编译生成的 pro2 文件类型是 DYN(共享目标文件),而与静态库链接生成的文件 pro 类型为EXEC(可执行文件)

使用 readelf -S 查看节头信息
在这里插入图片描述
使用 readelf -s 查看Symbols的内容
在这里插入图片描述

动态链接共享库

静态库仍然有一些明显的缺点,静态库和所有的软件一样,需要定期维护和更新。如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显式地将他们的程序与更新了的库重新链接。

另一个问题是几乎每个C程序都使用标准I/O函数,比如printf和scanf。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行上百个进程的典型系统上,这将是对稀缺的内存系统资源的极大浪费。(内存的一个有趣属性就是不论系统的内存有多大,它总是一种稀缺资源。磁盘空间和厨房的垃圾桶同样有这种属性。)

共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。共享库也称为共享目标(shared objet),在Linux系统中通常用.so后缀来表示。微软的操作系统大量地使用了共享库,它们称为DLL(动态链接库)。

我们可以用以下方式建立使用共享库:

gcc -shared -fpic -o libvector.so addvec.c multvec.c
gcc -o prog21 main2.c ./libvector.so

运行结果:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值