CMU15-213学习笔记(四)Linking 链接
编译过程
编译系统(compilation system):预处理器(pre-processor)、编译器(compiler)、汇编器(assembler)、链接器(linker)
-
预处理阶段:处理字符
#
开头的命令,即:- 将头文件的内容插入程序文本中
- 宏定义替换
- 条件编译(
#if
#ifdef
),不被编译的部分变为空行 - 删除注释
预处理的命令:
$gcc –E hello.c –o hello.i $cpp hello.c > hello.i #两条命令等价,都是预处理hello.c文件,然后重定向输出为hello.i文件
经过预编译处理后,得到的是预处理文件(如,hello.i) ,它还 是一个可读的文本文件 ,但不包含任何宏定义
-
编译阶段:通过编译器将源程序翻译成汇编程序(assembly-language program)
编译指令:
$gcc –S hello.i –o hello.s #将hello.i编译,重定向输出为hello.s文件 $gcc –S hello.c –o hello.s #进行两步操作,先对hello.c预处理,再进行编译 $/user/lib/gcc/i486-linux-gnu/4.1/cc1 hello.c #也可以直接调用cc1对hello.c进行编译,cc1前面是 该命令所在的位置
gcc实际上是GCC编译系统驱动程序,代表用户调用具体的预处理程序ccp、编译程序cc1和 汇编程序as等。从上面的代码可以看到,
gcc -S
等同于cc1
,gcc -E
等同于cpp
。 -
汇编阶段:将汇编程序翻译成机器语言指令,并将其打包成***可重定位目标程序(relocatable object program)***
汇编指令和机器指令一一对应,前者是后者的符号表示,它们都属于机器级指令,所构成的程序称为机器级代码
$gcc –c hello.s –o hello.o $gcc –c hello.c –o hello.o #此程序要经过三个阶段 $as hello.s -o hello.o #(as是一个汇编程序)
汇编结果是一个可重定位目标文件(如
hello.o
),其中包含的是不可读的二进制代码,必须用相应的工具软件来查看其内容(如objdump,gdb) -
链接阶段:链接器将各个可重定位目标文件(.o文件)合并成可执行目标文件。链接器使得分离编译成为可能。在编写大型程序时,可将模块分小,由此达到独立修改和编译不同模块的目的:未被修改的模块不用重新编译,而只需将修改后的模块编译,重新链接即可。e.g. hello.c中的printf函数存在printf.o(已经单独预编译了的目标文件)中,链接器将其合并后得到可执行文件
$gcc –static –o myproc main.o test.o $ld –static –o myproc main.o test.o
链接阶段gcc命令没有对应的参数,只要处理的源程序是
.o
,就认为现在正在链接。-o myproc
代表输出的可执行文件名为myproc
。-static
表示静态链接。
注意,这里gcc
的默认输出就是固定的a.out
(通过使用参数-o(output),可以指定输出文件的名称。 例如gcc b.c -o b.bin
,将生成可执行文件b.bin
,而不是默认的a.out
)
-E -S -c
b.c ------> b.i ------> b.s ------> b.o ------> a.out
gcc gcc as ld
链接
要使用GNU编译系统构造程序,需要在linux中输入以下命令调用GCC编译器驱动程序。将程序翻译为机器码并且链接
linux> gcc -Og -o prog main.c sum.c #-o prog 表示生成的可执行文件的名字为prog
首先使用cpp(c pre processor)、cc1、as将两个文件分别编译成重定位目标文件,再使用Linker,将两个文件连接在一起,生成可执行目标文件(包含两个文件中所有函数的代码和数据)。
要运行可执行文件./prog
,在linux shell命令行中输入它的名字。
linux> ./prog
shell调用操作系统中一个叫做加载器的函数,它将可执行文件中的代码和数据复制到内存,然后将控制转移到这个程序的开头。
![image-20210727180635744](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210727180635744.png)
链接器的由来
在机器代码中:
0:0101 0110
1:0010 0101 --|
2: …… |
3: …… |
4: …… |
5:0110 0111 <--
6: ……
假设:0010-jmp,第一条指令要求跳转到5,若在第5条指令前加入 指令,则程序员需重新计算jmp指令的目标地 址(重定位)。
汇编语言的出现,使用符号来表示跳转位置和变量位置。
0:0101 0110 add B
1:0010 0101 --| jmp L0
2: …… | ……
3: …… | ……
4: …… | ……
5:0110 0111 <-- L0:sub C
6: …… ……
高级编程语言出现后,我们在代码中会声明全局变量及函数,这些东西被称之为符号(symbol)。之后会调用变量及函数,也就是对符号的引用(reference)。
void swap() {…} /* define symbol swap */
swap(); /* reference symbol swap */
int *xp = &x; /* define symbol xp, reference x */
最终链接将多个.o文件合并为一个文件,所有的数据和程序处于同一虚拟地址空间中,这些符号的引用全部都要替换为在最终的可执行目标文件中的地址值。
链接器主要负责做两件事情
-
第一步:符号解析 Symbol resolution
- 所有定义的符号都会被保存在**符号表(symbol table)**中,而符号表会保存在由汇编器生成的 object 文件中(也就是
.o
文件)。符号表实际上是一个结构体数组,在.symtab
中,每一个表项是一个结构类型,每个表项包含符号名、长度和位置等信息。 - 将符号的引用存放在重定位节(
.rel.text
和.rel.data
)中.rel.text
存放代码的重定位信息.rel.data
存放数据的重定位信息
有了符号表节和重定位节后,在Symbol resolution阶段,链接器就可以给每个符号引用与一个符号定义建立关联,用作寻找对应符号的标志。
- 所有定义的符号都会被保存在**符号表(symbol table)**中,而符号表会保存在由汇编器生成的 object 文件中(也就是
-
第二步:重定位 Relocation
这一步所做的工作是把原先分开的代码和数据片段汇总成一个文件,将多个代码段和数据段分别合并为一个单独的代码段和数据段,重定位符号,把符号原先在
.o
文件中的相对位置转换成在可执行程序的绝对位置,确定每个符号的地址,并且据此更新引用处的地址为重定位后的地址,在指令中填入新的地址。
![image-20210728094433385](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728094433385.png)
注意!局部变量temp分配在栈中,不会在过程外被引用,因此不是符号定义
使用连接的好处:
- 模块化:我们可以把程序分散到不同的小的源代码中,而不是一个巨大的类中。这样带来的好处是可以复用常见的功能/库,比方说 Math library, standard C library.
- 效率:改动代码时只需要重新编译改动的文件,然后链接在一起就行了。而不需要重新编译所有的文件。而常用的函数和功能可以封装成库,提供给程序进行调用(节省空间,可执行文件和运行时内存中只需包含所调用函数的代码,而不需要包含整个共享库。例如:只包含
printf.o
的代码,不包含libc.a
中其他函数的代码)
连接过程的本质
![image-20210728104330368](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728104330368.png)
链接的本质:合并.o文件中相同的节,合并完后的格式还是ELF。
链接成可执行目标文件后的格式如下图左边所示,此时文件还存在磁盘中。在shell中输入可执行文件的名字后
linux> ./prog
shell调用操作系统中一个叫做加载器的函数,它将可执行文件中的代码和数据复制到内存,然后将控制转移到这个程序的开头。此时文件被复制到内存中,它在虚拟内存空间中的格式如下图右边所示。
![image-20210728105104365](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728105104365.png)
对.o文件反汇编得到代码和数据的地址是从0开始,而对可执行目标文件反汇编,代码和数据的地址就是虚拟内存空间中的地址了。
![image-20210728110000675](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728110000675.png)
目标文件的格式
目标代码指编译器和汇编器处理源代码后生成的机器语言目标代码。目标文件指包含目标代码的文件。所谓的目标文件(Object File)实际上是一个统称,具体来说有以下三种形式:
- 可重定位目标文件 Relocatable object file (
.o
file)- 每个
.o
文件都是由对应的.c
文件通过编译器和汇编器生成,包含代码和数据**,每个.o文件的代码和数据的地址都从0开始**。可以与其他可重定位目标文件合并创建一个可执行或共享的目标文件
- 每个
- 可执行目标文件 Executable object file (linux默认为
a.out
,windows中为*.exe
)- 由链接器生成,包含的代码和数据可以直接通过加载器加载到内存中充当进程执行的文件。代码和数据的地址就是虚拟内存空间中的地址
- 共享目标文件 Shared object file (
.so
file)- 在 windows 中被称为 Dynamic Link Libraries(DLLs),是特殊的可重定位目标文件,可以在加载或运行时被动态地加载进内存并链接,称为共享库文件。
上面提到的三种目标文件有统一的格式,即 Executable and Linkable Format(ELF),它是 .o
文件 .out
文件 .so
文件的统一格式。ELF文件分为两种视图:链接视图和执行视图。
![image-20210728113350103](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728113350103.png)
链接视图-可重定位目标文件
-
可被链接(合并)生成可执行文件或共享目标文件
-
静态链接库文件由若干个可重定位目标文件组成
-
包含代码、数据(已初始化.data和未初始化.bss)
-
包含重定位信息(指出哪些符号引用处需要重定位)
-
文件扩展名为.o(相当于Windows中的 .obj文件)
![image-20210728113926109](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728113926109.png)
-
.text节
- 源代码编译后的机器指令
-
.data 节
- 已初始化的全局和静态变量 ,.data节中存放具体的初始值,需要占磁盘空间
-
.bss 节
- 未初始化的全局和静态变量,和初始化为0的全局和静态变量。仅是占位符,在目标文件中不占据任何实际磁盘空间。.bss节中无需存放初始值,只要说明.bss中的每个变量将来在执行时占用几个字节即可。在运行时,在内存中分配初始值为0 。因此,.bss节实际上不占用磁盘空间,提高了磁盘空间利用率
![image-20210728121359988](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728121359988.png)
只有.text
、.data
、.bss
、.rodata
加载到内存中才会占用内存空间,而下图中的这些节只是在链接时才会用,链接完后这些节是不需要装入到内存中的。
![image-20210728121825965](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728121825965.png)
先通过ELF头找到节头表的位置(ELF头中有节头表的偏移量),再通过节头表找到对应的节(节头表中包含每个节的节名、偏移和大小)。
ELF头
使用 readelf -h
可以读取elf文件的头:
![image-20210728123104450](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728123104450.png)
- 魔数:文件开头几个字节通常用来确定文件的类型或格式
- Type:文件类型
- Entry point address:因为此文件是可重定位的文件,是链接视图,无法执行,所以装入的地址为0,也就是说根本无法执行。
- Start of program headers:等于0说明不包含程序头表
- Start of section headers:节头表的起始地址
- Size of section headers: 40 (bytes) 节头表每个表项的大小
- Number of section headers: 15 节头表一共有多少表项
- Section header string table index: 12 .strtab在节头表中的索引
节头表
节头表中表项的数据结构,表项描述了每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等 。以下是32位系统对应的数据结构(每个表项占40B)
![image-20210728131238484](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728131238484.png)
使用 readelf -S
可以读取目标文件的节头表的内容:一共有11个表项,每个表项都是上图的数据结构,每个表项对应elf中的一个节
![image-20210728131432600](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728131432600.png)
所有节的虚拟地址字段都是0,因为这是.o文件,是链接视图,而不是执行视图,无法被加载入内存中执行。】
执行视图—可执行目标文件
-
包含代码、数据(已初始化.data和未初始化.bss)
-
定义的所有变量和函数已有确定地址(虚拟地址空间中的地址)
-
符号引用处已被重定位,以指向所引用的定义符号
-
没有文件扩展名或默认为a.out(相当于Windows中的 .exe文件)
-
可被CPU直接执行,指令地址和指令给出的操作数地址都是虚拟地址
![image-20210728180911667](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728180911667.png)
可执行目标文件需要装入内存然后被执行,装入到内存的时候,需要映射到存储空间(虚拟地址空间)对应的段中。程序头表描述了可执行目标文件中的节和段的对应关系,而可重定位目标文件不会被装入内存空间,所以不需要程序头表。
使用 readelf -h
也可以读取可执行目标文件的头:
![image-20210728181838090](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728181838090.png)
可执行文件的存储器映射
所有的代码(.init
、.text
)和只读数据(.rodata
)映射到只读代码段(Text段),可读可写的数据映射到读写数据段(data段),其他的节不会被装入内存空间,而程序头表中的信息会描述可执行文件中的节映射到存储空间中的什么位置。
![image-20210728182257547](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728182257547.png)
可执行文件中的程序头表
程序头表描述可执行文件中的节与虚拟 空间中的存储段之间的映射关系,一个表项(32B)说明虚拟地址空间中 一个连续的段或一个特殊的节,以下是某可执行目标文件程序头表信息 ,有8个表项,其中两个为可装入段(即 Type=LOAD),分别是:
- 所有的代码(
.init
、.text
)和只读数据(.rodata
)还有ELF头、程序头表映射到只读代码段(Text段) - 可读可写的数据(
.data
、.bss
)映射到读写数据段(data段)
![image-20210728185501611](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728185501611.png)
注意:.bss在磁盘中不占用空间,但是加载到内存中后,占用虚拟内存空间(因为要给未初始化的变量赋值0)。
先将多个文件分别编译成可重定位目标文件,由elf头找到节头表Section header table,再由节头表找到对应的节在文件中的位置,合并这些可重定位目标文件中的相同的节,生成一个可执行目标文件,执行该文件时,根据程序头表找到节 对应加载到虚拟内存空间中的哪些段,将可执行目标文件的数据和代码加载进内存中。
符号表
![image-20210728233321092](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728233321092.png)
注意!void swap();
和extern int buf[];
不算符号的定义!
每个可重定位目标文件m都有一个符号表,它包含了在m中定义和引用的符号,具体来说是以下三种符号:
- Global symbols:定义在本目标模块的全局符号,可以被其他函数引用
- External symbols:在本目标模块中引用的全局符号,但是却没有定义在本模块。
- Local symbols:本模块的局部符号,仅由模块m定义和引用的本地符号,仅在本模块内可见。例如,在模块m中定义的带static 的C函数和全局变量。
注意!链接器局部符号不是指程序中的局部变量(分配在栈中的临时性变量),链接器不关心这种局部变量
![image-20210728232347959](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728232347959.png)
![image-20210728233637715](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210728233637715.png)
void swap();
和extern int buf[];
都不属于符号的定义!swap和buf这两个符号出现在符号表中的原因是:它们在本模块中被引用了。如果我们把源码中的引用删去,只留下void swap();
和extern int buf[];
,那么符号表中就不会出现这两个符号,说明它们不算符号的定义。
查看符号表使用 readelf -s
命令,注意!是小写的s。
![image-20210731144926860](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210731144926860.png)
而它们属于UND的原因则是:它们并没有在本模块中定义,所以是未定义的。
实际上符号定义在别的节中,不是定义在符号表中,符号表只是把这些符号的信息收集起来,有的是代码中的符号,属于.text;有的是初始化的全局或静态变量,属于.data;有的是未初始化的全局或静态变量,属于.bss(COM)。
符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件中的符号表中的一个确定的符号关联起来。
每个定义符号在代码段或数据段中都被分配了存储空间,将引用符号与定义符号建 立关联后,就可在重定位时将引用符号的地址重定位为相关联的定义符号的地址。“符号的定义”的实质是指被分配了存储空间。为函数名即指其代码所在区;为变量名即指其所占的静态数据区
- 本地符号在本模块内定义并引用,因此,其解析较简单,只要与本模块内唯一的定义符号关联即可。
- 全局符号(外部定义的、内部定义的)的解析涉及多个模块,故较复杂。
- 当链接器遇到一个不在当前模块中定义的符号时,会假设该符号是在其他某个模块中定义的,生成一个符号表条目,并把它交给链接器处理,如果该链接器在它的任何输入模块中都找不到这个被引用符号的定义,就会输出一条错误信息并终止。
先不纠结原型声明的问题。
强弱符号:
- 函数名和已初始化的全局变量名是强符号
- 未初始化的全局变量名是弱符号
![image-20210729115142651](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210729115142651.png)
注意!符号的类型都是相对于某个文件来说的。目标文件的符号表中只有在本文件中定义的符号!
比如,对于main.c来说,void swap()
就是弱符号,因为它在main.c文件中没有初始化;对于swap.c来说,extern int buf[]
就是弱符号,因为它在swap.c中没有初始化。而static int*bufp1
是静态变量,不是全局变量,所以它既不是强符号也不是弱符号。同理,之前的全局符号、外部符号、局部符号都是一样的。
以上的理解是错的!
符号的类型不是相对于某个文件来说的!符号表中不只包括在本文件中定义的符号,还包括引用的符号!
强弱符号是对符号定义来说的,不是针对引用。void swap();
和extern int buf[];
不是符号定义,所以既不是强符号也不是弱符号。static int*bufp1
是静态变量,不是全局变量,所以它既不是强符号也不是弱符号。
符号解析规则:
- 强符号不能多次定义
- 强符号只能被定义一次,否则链接错误
- 若一个符号被定义为一次强符号和多次弱符号,则按强定义为准
- 对弱符号的引用被解析为其强定义符号
- 若有多个弱符号定义,则任选其中一个
- 使用命令 gcc –fno-common链接时,会告诉链接器在遇到多个弱定义的全局符号时输出一条警告信息。
符号解析时只能有一个确定的定义(即每个符号仅占一处存储空间)
指针实际上就是汇编语言中的地址操作数。
![image-20210729174155712](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210729174155712.png)
![image-20210729174108807](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210729174108807.png)
![image-20210729174539630](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210729174539630.png)
静态链接
链接的时候会有两种模块,一种是可重定位目标文件,这些文件中可能会调用一些标准库中的函数,这些库称为静态库(.a文件),而库中包含多个.o模块。所以链接时还会包含静态库(.a文件)中的.o模块。
静态库 (.a archive files)
- 将所有相关的目标模块(.o)打包为一个单独的库文件(.a),称为静态库文件 ,也称存档文件(archive)
- 在构建可执行文件时,只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来
在gcc命令行中无需明显指定C标准库libc.a(默认库)
![image-20210729221424945](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210729221424945.png)
Archiver(归档器)允许增量更新,只要重新编译需修改的源码并将其.o文件替换到静态库中
链接静态库的过程:
![image-20210729221722379](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210729221722379.png)
- 首先使用
gcc –c myproc1.c myproc2.c
将这两个文件编译成.o文件 - 再用
ar rcs mylib.a myproc1.o myproc2.o
将刚才生成的两个.o 文件打包生成mylib.a
静态库文件。 - 而由于在main.c中调用了
mylib.a
静态库中的模块,所以main.o链接时要指明mylib.a
静态库:gcc –static –o myproc main.o ./mylib.a
(-static
表示静态链接,-o myproc
表示生成的可执行文件名为myproc)。
符号解析过程
符号解析时有三个集合:
- E:所有目标文件的集合,这些目标文件将被合并以组成可执行文件
- U:当前所有未解析的引用符号的集合。当符号解析结束时,如果U中还有未解析的符号引用时,则说明符号解析出现了问题。
- D:当前所有定义符号的集合
符号解析过程:
- 开始E、U、D为空,命令中给出的第一个链接的文件时main.o,所以首先扫描main.o,把它加入E。
- 在main.o中有符号表。在符号表中myfun1是未定义的符号,所以把myfun1加入U。而main是已定义的符号,所以将main加入D。main.o此时就处理完了。
- 接着扫描到 mylib.a,将U中所有符号(本例中为myfunc1)与 mylib.a中所有目标模块(myproc1.o和myproc2.o )依次匹配,发现在myproc1.o中定义了myfunc1 ,所以myproc1.o是需要链接的模块,故myproc1.o加入E。同时,由于myfunc1找到了定义,myfunc1从U转移到D。
- 在 myproc1.o中发现还有未解析符号printf,将其加到 U。不断在mylib.a的各模块上进行迭代以匹配U中的 符号,但是printf一直得不到解析。此时U中只有一个未解析符号printf,而D中有main和myfunc1。因为模块 myproc2.o没有被加入E中,因而它被丢弃。
- 接着,扫描默认的库文件libc.a,发现其目标模块printf.o定义了 printf,于是printf也从U移到D,并将 printf.o加入E,同时把它定义的所有符号 加入D,而所有未解 析符号加入U。 处理完libc.a时,U一定是空的。
解析结果: E中有main.o、myproc1.o、printf.o及其调用的模块,D中有main、myproc1、printf及其引用的符号,注意:E中无 myproc2.o!
被链接模块应按调用顺序指定,如果我们将链接的顺序调换一下:gcc –static –o myproc ./mylib.a main.o
,结果会:
- 首先,扫描mylib,因是静态库,应根据其中是否存在U中未解析符号对应的定义符号来确定哪个.o被加入E。因为开始U为空,故其中两个.o模块都不被加入E中而被丢弃。
- 然后,扫描main.o,将myfunc1加入U。此时由于静态库已被丢弃,所以直到最后它都不能被解析。
所以在链接时,我们应该将静态库放在最后
符号解析的过程实际上就是:
-
按照链接命令中的顺序,从左到右扫描文件。
每遇到一个新的.o 或 .a 中的模块,将其符号表中定义的符号加入D,未定义的符号加入U,同时试图用其来解析U中的符号,将U中的所有符号与所扫描的模块相匹配。
- 如果是.o模块,则可以直接加入E;
- 如果是静态库文件,只有匹配的模块才能加入E
并将被匹配的符号从U移到D。找出未定义的符号,加入U。找出已定义的符号,加入D。
- 对于.o模块,扫描结束就可以进入下一个文件
- 对于静态库文件,所有文件全部扫描结束或者U为空,都可以进入下一个文件。
一直扫描到链接命令中的文件序列结束。
符号解析的过程实际上就是:
- 给外部符号找到定义
- 收集所有已定义的符号(为了在下一步中重定位符号)
- 以及收集所有需要链接的模块(为了在下一步中合并为一个可执行文件)
的过程。
-lxxx=libxxx.a
,所以gcc -L. libtest.o -lmine
等同于gcc -L. libtest.o libmine.a
重定位
汇编器在遇到汇编代码中的助记符时(也就是变量名和函数名),由于汇编器不知道该变量或函数在文件合并后的地址,所以汇编器只能生成一个假的临时地址。同时生成一个重定位条目,告诉链接器,在链接时需要在这个位置进行重新定位,定位成合并后真正的地址。
- 数据引用的重定位条目在.rel_data节中
- 指令中引用的重定位条目在.rel_text节中
重定位条目和汇编后的机器代码在可重定位目标 (.o)文件中。
![image-20210731193031475](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210731193031475.png)
重定位条目中有重定位信息,反映出
- 需要重定位的符号引用的位置(
offset
,在节内(.data节或.text节)的偏移位置) - 绑定的定义符号名(
symbol
,在符号表中的索引) - 重定位类型(
type
,绝对地址(也就是把可执行文件中的符号在虚拟地址空间中的地址直接填入)或相对地址(符号地址相对这条指令的偏移))
用命令readelf -r main.o
可显示main.o中的重定位条目
![image-20210731224126165](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210731224126165.png)
符号解析完成后,可进行重定位工作,分三步
-
合并相同的节
将集合E的所有目标模块中相同的节合并成新节。例如,所有.text节合并作为可执行文件中的.text节
-
对定义符号进行重定位(确定符号定义的地址)
确定新节中所有定义符号在虚拟地址空间中的地址。例如,为函数确定首地址,进而确定每条指令的地址,为变量确定首地址
完成这一步后,每条指令和每个全局或局部变量都可确定地址
-
对引用符号进行重定位(修改符号引用的地址)
修改.text节和.data节中对每个符号的引用(地址)。需要用到在.rel_data和.rel_text节中保存的重定位信息
可执行文件的加载
![image-20210731230637942](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210731230637942.png)
通过调用execve系统调用函数来调用加载器,加载器(loader)根据可执行文件的程序(段)头表中的信息,将可执行文件的代码和数据从磁盘“拷贝”到存储器中(实际上不会真正拷贝,仅建立一种映射)。加载后,将PC(RIP)设定指向 Entry point (即符号_start处,此处是第一条执行的指令),最终执行main函数,以启动程序执行。
![image-20210731232914722](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210731232914722.png)
![image-20210731232931541](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210731232931541.png)
![image-20210731233013215](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210731233013215.png)
共享库和动态链接
静态库有一些缺点:
- 库函数(如printf)被包含在每个运行进程的代码段中(因为所有调用静态库函数的文件,都需要包含函数所在的.o模块),对于并发运行上百个进程的系统,造成极大的主存资源浪费
- 库函数(如printf)被合并在可执行目标文件中,磁盘上存放着数千个可执行文件,每个可执行文件都包含相同的库函数的代码,造成磁盘空间的极大浪费
- 静态链接时,静态库函数必须合并到可执行目标文件中,如果静态库函数修改了,需要重新编译和链接,更新到可执行文件中。更新困难,使用不便。
解决方案:Shared Libraries (共享库),Window称其为动态链接库(Dynamic Link Libraries,.dll文件) Linux称其为动态共享对象( Dynamic Shared Objects, .so文件)
- 共享库是包含很多.o模块的文件,每个模块都包含代码和数据(与静态链接相同)
- 把公共的,所有程序可以调用的,共享的代码从程序中分离出来,磁盘和内存中都只有一个备份(比如printf函数,不包含在调用它的程序当中,专门存放在一个共享库文件中)
- 可以动态地在调用共享库的程序装入时或运行时被加载并链接
所以,共享模块在内存中只有一个备份,被所有进程共享,节省内存空间。共享库文件在磁盘中也只有一个备份,被所有程序共享链接,节省磁盘空间。共享库升级时,被自动加载到内存和程序动态链接,使用方便。
动态链接可以按以下两种方式进行:
-
在第一次加载并运行时进行 (load-time linking).
Linux通常由动态链接器(ld-linux.so)自动处理,标准C库 (libc.so) 通常按这种方式动态被链接
-
在已经开始运行后进行(run-time linking).
在Linux中,通过调用 dlopen()等接口来实现,分发软件包、构建高性能Web服务器等
自定义一个动态共享库文件
gcc –shared –fPIC –o mylib.so myproc1.o myproc2.o
:–shared –fPIC
表示生成位置无关的共享代码库文件
- PIC:Position Independent Code,位置无关代码
- 保证共享库代码的位置可以是不确定的
- 即使共享库代码的长度发生变化,也不会影响调用它的程序
加载时动态链接
![image-20210801101959084](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210801101959084.png)
把main.o和mylib.so进行静态链接,将这些文件中的重定位信息和符号表信息进行静态链接,生成可执行文件myproc。这个可执行文件只是一个部分链接的可执行文件,因为链接的对象是.so共享库文件,所以并不会把代码链接进可执行文件中,只是把重定位信息和符号表信息加载到可执行文件中。
当可执行文件被加载到内存中时,调用execve加载器,加载器最终调用动态链接器(ld-linux.so),动态加载器会把之前部分链接的可执行文件myproc和共享库libc.so、mylib.so中的printf.o和myproc1.o代码和数据进行链接。动态链接器生成的重定位以后的代码,实际上是放在存储空间中的,不会放在磁盘中。
![image-20210801103429432](https://gitee.com/BoL0150/boleeimgbed/raw/master/image-20210801103429432.png)