计系2复习(3)链接,静态链接与动态链接

目录

  • 前言
  • 链接过程
  • 目标文件
    • 可重定位的目标文件
  • 静态链接
    • 符号解析
      • 强弱符号
      • 同名符号解析规则
    • 重定位
    • 静态库
      • 符号引用的解析机制
      • 链接顺序
  • 动态链接
  • 题目
    • 1
    • 2
    • 3
    • 4
    • 5

前言

链接分为静态链接和动态链接,静态链接使得不同的源文件可以互相调用,形成模块化,而动态链接则通过加载/运行时链接解决了共享库在不同应用中复制多份的浪费问题。

链接过程

静态链接解决了不同程序之间模块化的问题。此外,修改一个源文件,不必重新编译整个应用程序,只需要编译修改的部分,再次链接即可。

例子:程序A.cpp可以调用B.cpp中的函数或者全局变量,下面通过一个简单的例子展示了从两个源文件到可执行的二进制文件的过程

计系2复习(3)链接,静态链接与动态链接_第1张图片

预处理器
编译器
汇编器
预处理器
编译器
汇编器
main.c
sum.c
main.i
sum.i
main.s
sum.s
main.o
sum.o
链接器
可执行的目标文件

以main.c为例,编译通常:

  1. 通过预处理器将 .c 文件翻译为一个 ASCII 码的 .i 文件(中间文件)
  2. 通过编译器将 .i 文件翻译为一个 ASCII 码的 .s 汇编语言代码文件
  3. 通过汇编器将 .s 文件翻译为一个二进制的 .o ,即可重定位的目标文件
  4. 链接器将 .o 和其他 .o ,以及一些必要的系统目标文件组合,生成最终的可执行二进制文件

目标文件

目标文件分为三类:

  1. 可执行的目标文件:可以直接被复制进内存并且执行
  2. 可重定位的目标文件:包含二进制代码和数据,可以在编译时和其他可重定位目标文件合并,创建一个可执行目标文件
  3. 共享目标文件:特殊的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并且链接

编译器和汇编器生成可重定位的目标文件,而链接器生成可执行的目标文件。

可重定位的目标文件

可重定位的目标文件由以下几个部分组成:
计系2复习(3)链接,静态链接与动态链接_第2张图片
下面给出各个部分的注解:

名称 英文全称 注解
.text text 已编译程序的机器代码
.rodata read only data 只读数据,比如printf的格式化字符串,switch语句的跳转表
.data data 已初始化的全局/静态C变量
.bss block storage start 未初始化的全局/静态C变量
.symtab symbol table 符号表,存放定义和引用的 函数/变量 信息
.rel.text relocated text text节中需要做重定位修改的指令位置的列表
.rel.data relocated data 被 引用/定义 的所有全局变量的重定位信息

静态链接

静态链接程序通常以一组可重定位的目标文件(.o文件)为输入,输出一个可执行的目标文件。

静态链接通常分为两个步骤:

  1. 符号解析
  2. 重定位

符号解析通常是对变量命函数名的解析,将符号引用和其定义关联起来。

而重定位则是将代码中涉及内存位置的操作,比如 jmp (pc+偏移)跳转语句,在 .o 文件中,这些位置通常都是相对每个 .o 的起始位置,而在合并 .o 之后,位置发生改变,需要重新定位。

符号解析

链接器符号分为三类:

  1. 全局符号:可以被其他模块引用的符号,如非静态函数和全局变量
  2. 局部符号:只由当前模块定义并引用的本地符号,比如静态函数和静态全局变量
  3. 外部符号:当前模块引用外部模块的符号

强弱符号

符号解析的重点在于解析同名符号,同名符号又通过强弱来区分:

  1. 函数名已初始化的全局变量 是强符号
  2. 未初始化的全局变量是弱符号

同名符号解析规则

链接器通过如下的规则进行同名符号解析:

  1. 不允许多个同名强符号,否则链接器报错
  2. 同时拥有同名强符号和弱符号,那么对弱符号的引用,解析为指向强符号的引用
  3. 如果同时存在多个同名弱符号,那么随机选择一个

下面的图表描述了弱弱符号和强弱符号共存时引用的情况:
计系2复习(3)链接,静态链接与动态链接_第3张图片

重定位

更加详细的版本【符号引用重定位 重定位PC相对引用 简单讲解】

符号解析完成之后,需要进行重定位。因为将不同的 .o 合并,而每个 .o 中的符号位置都是相对该 .o 而言的,所以需要进行重定位。

重定位分为三个步骤:

  1. 合并相同的节,比如将所有 .o 的 .text 节合并为可执行文件中新的 .text 节
  2. 对符号定义进行重定位,即确定新节中所有定义符号在虚拟地址空间中的地址(相对该新节的地址),比如为函数确定首地址,为变量确定首地址
  3. 对引用的符号(比如变量和函数)进行重定位,需要用到 rel.data 和 rel.text 节中的信息

重定位条目:
计系2复习(3)链接,静态链接与动态链接_第4张图片

重定位相当简单。假设main.c调用sum.c,以call指令(函数调用)PC+偏移的重定位为例,其实就是计算一下两者之间地址的差值即可,注意加上 r.addend 以补偿call指令的PC:

书上公式:
计系2复习(3)链接,静态链接与动态链接_第5张图片
图解:
计系2复习(3)链接,静态链接与动态链接_第6张图片

静态库

老式的静态链接解决方案,将所有 .o 文件打包为一个单独的库文件即 .a 文件(archive,存档文件)

如图:编译 atoi, printf, random函数等等,成libc.a共享库
计系2复习(3)链接,静态链接与动态链接_第7张图片

在构建可执行文件时,只需指定库文件名,然后链接器自动去对应的库中找用到的模块,并且只拷贝用到的模块。

符号引用的解析机制

链接到静态库时,通过如下的机制对外部引用进行解析:

  1. 按照命令行给出的顺序扫描 .o 和 .a 文件
  2. 扫描时遇到未解析的引用则存入临时表U
  3. 每遇到一个新的 .a / .o 都试图用其中的符号解析未知符号表U中的符号

如果扫描到最后,U中还有未知符号,则报错。

链接顺序

和静态库链接,也要按照基本法 引用解析机制,因为命令行给出的顺序不同,很可能导致不同的结果:

比如以下的互相调用的情况

调用
调用
调用
调用
func.o
libx.a
liby.a

则应该使用如下的命令行(即在最后补上一个x,解析U中的y的符号,从而形成闭环)
a

动态链接

静态库及其静态链接有很多缺点,比如:

  1. n个程序链接到库,库需要复制n份
  2. 修改系统库,需要每个应用程序重新链接(这对系统的更新是坏的)

所以出现动态链接及共享库的概念:

计系2复习(3)链接,静态链接与动态链接_第8张图片

共享库的优点有:

计系2复习(3)链接,静态链接与动态链接_第9张图片
共享库链接过程:
计系2复习(3)链接,静态链接与动态链接_第10张图片
后面的不考?咕了

题目

1

以下哪个工作不属于动态链接器完成的 ?

A、完成对引用的动态库函数在进程空间的布局。
B、完成动态库函数的编译和汇编。
C、完成可执行文件对动态库函数引用的符号解释。
D、完成可执行文件对动态库函数引用的地址确定。

答:编译和汇编是编译阶段的工作,不是链接阶段的,所以选B

2

ELF格式可用来保存哪些类型的目标文件?

答:可重定位的目标文件、可执行的目标文件、共享目标文件

3

计系2复习(3)链接,静态链接与动态链接_第11张图片
答:
m1中int x是强符号,m2中float x 是弱符号,所以绑定到强符号,A正确,B错误。

p1函数被分配到内存中代码区域,p1函数中的临时变量 p1 被分配到栈中,C正确

x,main和p1都不违反强符号只能出现一次的法则,故合法,D正确

4

假设调用关系如下:hello.c调用libx.a和liby.a中的函数,liby.a调用了libz.a的函数,libz.a调动了libx.a和liby.a中的函数,以下编译正确的是 。

A、gcc –o hello hello.c ./libx.a ./liby.a ./libz.a ./libx.a
B、gcc –o hello ./libx.a ./liby.a ./libz.a ./libx.a ./liby.a hello.c
C、gcc –o hello hello.c ./libx.a ./liby.a ./libz.a ./libx.a ./liby.a
D、gcc –o hello hello.c ./libx.a ./liby.a ./libz.a

答:由库链接时的符号解析机制(即U未知符号表的读写)可知,在最后解析 libz.a之后,会存在 libx, liby的符号未被解析,因此需再次添加 libx, liby,所以选 C

5

可重定位目标文件中机器代码、只读数据、未初始化的全局变量以及已初始化数据通常分别保存在哪些节里?

答:
机器代码:.text
只读数据:.rodata
未初始化的全局变量:.bss
初始化数据:.data

你可能感兴趣的:(计算机系统,链接,编译)